在嵌入式系統開發中,隨著項目復雜度的提升和跨平臺需求的日益增長,“想到什么就寫什么”的裸機編程方式已難以應對。采用分層架構是保障代碼可維護性、可移植性和可測試性的關鍵。其中,硬件抽象層和驅動層的劃分與協作,是整個硬件相關代碼設計的核心。
我們可以用一個“公司”的比喻來理解:
硬件本身:好比是公司的基層員工(例如,一臺打印機)。
驅動層:是部門經理。他非常了解自己部門員工(硬件)的每一個細節和脾氣(寄存器、時序、電氣特性)。他直接管理和命令這些員工,但其他部門的人不能直接調用他的員工。
HAL層:是公共接口或項目經理。他定義了一套標準的、易于理解的“工作請求”規范(例如,“打印這份文件”)。業務部門的人只需要向項目經理提出標準請求,而無需關心項目經理是找A部門的打印機還是B部門的打印機完成的。
這樣說就清晰很多了吧,搞清楚概念是寫代碼的第一步,現在,我們從技術角度給出定義:
驅動層是直接與硬件寄存器打交道的軟件層。它負責最底層的硬件操作,是唯一“知道”硬件具體細節(如芯片型號、外設寄存器地址、中斷向量號等)的代碼。
核心職責:
高度依賴硬件,如果換一個MCU型號,那么驅動層代碼幾乎需要重寫,是實現特定功能的,這里的代碼專注于讓一個特定的硬件外設工作起來。
HAL層位于驅動層之上,應用程序之下。它定義了一組統一的、標準化的API接口,向上層應用(或中間件)提供硬件服務,從而將應用邏輯與具體的硬件實現解耦。
核心職責:
接口標準化,為同一類硬件功能提供統一的函數接口。
屏蔽硬件差異,上層應用調用HAL函數,無需關心底層信息。
資源管理:對一些共享資源進行管理和仲裁。
提供便捷服務,封裝常用操作流程,簡化上層調用。
這是一個硬件無關性的不隨硬件變化的代碼,且具有可移植性,也就是說當更換硬件平臺時,只需重新實現或適配HAL層下層的驅動,而上層應用代碼無需修改或只需極少修改。
一個典型的分層結構如下(以STM32發送串口數據為例):
應用層 需要發送字符串 “Hello”,調用 HAL_UART_Transmit(&huart1, (uint8_t*)"Hello", 5, HAL_MAX_DELAY);。
HAL層 收到請求后,會進行參數檢查,然后調用底層驅動函數來具體操作硬件。先檢查UART狀態,啟動傳輸,等待發送完成。
驅動層 將字符 ‘H’ 寫入 USART1->TDR 寄存器,檢查 USART1->ISR 寄存器中的TXE(發送緩沖區空)標志位,依次發送剩余字符。
硬件 根據寄存器的配置,將數據通過TX引腳以特定的波特率發送出去。
場景:點亮一個LED(連接在PC13引腳)
// 直接操作寄存器,高度依賴STM32
#include "stm32f1xx.h" // 特定系列的頭文件
void LED_Init(void) {
// 1. 使能GPIOC時鐘
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
// 2. 配置PC13為推挽輸出,最大速度50MHz
GPIOC->CRH &= ~(GPIO_CRH_MODE13 | GPIO_CRH_CNF13);
GPIOC->CRH |= GPIO_CRH_MODE13_0;
}
void LED_On(void) {
// 拉低引腳點亮LED(假設LED共陽)
GPIOC->BSRR = GPIO_BSRR_BR13;
}
void LED_Off(void) {
// 拉高引腳熄滅LED
GPIOC->BSRR = GPIO_BSRR_BS13;
}
int main(void) {
LED_Init();
LED_On();
// ... 其他代碼
}
上面的寫法如果要把代碼移植到STM32F4系列,RCC和GPIO的寄存器結構完全不同,就需要修改LED_Init,LED_On,LED_Off中的所有寄存器操作。
// hal_led.h - HAL層頭文件,接口是穩定的
#ifndef __HAL_LED_H
#define __HAL_LED_H
typedef enum {
LED_STATE_OFF = 0,
LED_STATE_ON
} LedState_TypeDef;
void HAL_LED_Init(void);
void HAL_LED_SetState(LedState_TypeDef state);
LedState_TypeDef HAL_LED_GetState(void);
#endif
// hal_led.c - HAL層實現,封裝了底層驅動調用
#include "hal_led.h"
#include "driver_led.h" // 包含特定硬件的驅動
void HAL_LED_Init(void) {
DRIVER_LED_Init(); // 調用驅動層初始化
}
void HAL_LED_SetState(LedState_TypeDef state) {
if (state == LED_STATE_ON) {
DRIVER_LED_On();
} else {
DRIVER_LED_Off();
}
}
// driver_led.c - 驅動層,針對STM32F1
#include "stm32f1xx.h"
void DRIVER_LED_Init(void) {
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
GPIOC->CRH &= ~(GPIO_CRH_MODE13 | GPIO_CRH_CNF13);
GPIOC->CRH |= GPIO_CRH_MODE13_0;
}
void DRIVER_LED_On(void) {
GPIOC->BSRR = GPIO_BSRR_BR13;
}
void DRIVER_LED_Off(void) {
GPIOC->BSRR = GPIO_BSRR_BS13;
}
// main.c - 應用層,完全不知道底層是STM32F1還是F4
#include "hal_led.h"
int main(void) {
HAL_LED_Init();
HAL_LED_SetState(LED_STATE_ON); // 應用層只關心“設置LED狀態”
// ... 其他代碼
}
在項目初期,根據產品功能需求,設計好HAL層的頭文件,驅動工程師根據HAL接口的要求,去實現具體的硬件操作,上層可以調用下層,下層絕不能調用上層。HAL層可以調用驅動層,但驅動層絕不能感知HAL層的存在。
在這種情況下,你的“應用層”就是基于官方HAL庫編寫的代碼,應用層的業務邏輯得到了完美保護。
雖然會在項目初期增加一些設計成本和少量的性能開銷,但從軟件工程的角度看,其帶來的可維護性、可移植性和團隊協作效率的提升,無疑是極具價值的投資。
???????????????? END ???????????????
關注我的微信公眾號,回復“星球”加入知識星球,有問必答。
點擊“閱讀原文”查看知識星球詳情,歡迎點分享、收藏、點贊、在看。