APP(應用程序層)、BSP(板級支持包層)、HAL(硬件抽象層)三層架構,通過明確的分層職責,降低硬件與軟件的耦合度,讓開發更模塊化、維護更高效,尤其適合多硬件平台迭代的項目。
一、分級思想
做過嵌入式開發的人都遇到過這樣的問題:
- 為 A 型號單片機寫的 GPIO 控制代碼,換 B 型號後要重新改寄存器操作;
- 同一個傳感器驅動,在不同開發板上要調整引腳定義、中斷號配置;
- 項目迭代時換硬件,應用邏輯代碼跟着改得 “面目全非”。
這些問題的根源,在於 “軟件直接依賴硬件細節”。而三層架構的核心作用,就是在 “應用邏輯” 和 “硬件實現” 之間加兩道 “隔離牆”——HAL 層屏蔽硬件差異,BSP 層適配具體板卡,最終讓 APP 層專注於業務,無需關心底層硬件。
二、三層架構核心職責拆解
三層架構從下到上依次是 HAL 層、BSP 層、APP 層,每一層都有嚴格的職責邊界,上層只能通過下層提供的 “接口” 調用功能,不能直接操作下層的內部實現。
1. 最底層:HAL 層(Hardware Abstraction Layer,硬件抽象層)——“統一硬件接口,屏蔽芯片差異”
HAL 層是離硬件最近的一層,直接操作芯片寄存器,但它不針對某個具體開發板,只針對 “芯片型號”(如 STM32F4、NXP RT1176)。核心職責是把芯片的硬件功能,封裝成 “統一的、無差異的接口”,讓上層(BSP/APP)不用再寫寄存器操作代碼。
(1)HAL 層做什麼?
- 封裝芯片外設功能:把 GPIO、ADC、UART、定時器等外設的初始化、讀寫、中斷配置,做成標準化函數。比如同樣是 “配置 GPIO 為推輓輸出”,STM32 的寄存器操作是
GPIO_InitTypeDef結構體配置,NXP 的是gpio_pin_config_t結構體配置,HAL 層會把這兩種操作統一成hal_gpio_init()接口,上層調用時不用管底層是哪種芯片。 - 屏蔽寄存器差異:不同芯片的寄存器地址、位定義完全不同(如 STM32 的 GPIO 輸出寄存器是
ODR,NXP 的是DR),HAL 層把這些差異 “藏起來”,上層只需傳 “引腳號、輸出電平” 等參數,不用記寄存器地址。 - 提供基礎硬件能力:比如
hal_adc_read()(讀 ADC 值)、hal_uart_send()(串口發數據)、hal_timer_start()(啓動定時器),這些接口的函數名、參數列表在不同芯片的 HAL 層中保持一致。
(2)HAL 層不做什麼?
- 不關心具體開發板的引腳用途(比如 “PA0 是 LED 還是按鍵”);
- 不處理板級硬件差異(比如同樣是 STM32F4,開發板 A 的 LED 接 PA0,開發板 B 的 LED 接 PB5,HAL 層不管這個);
- 不包含應用邏輯(比如 “LED 閃爍 5 次”,HAL 層只提供 “亮 / 滅” 接口,不做閃爍邏輯)。
(3)HAL 層示例代碼(以 GPIO 為例):
// HAL層頭文件(hal_gpio.h):統一接口聲明
#ifndef __HAL_GPIO_H
#define __HAL_GPIO_H
// 統一的GPIO方向枚舉(不管什麼芯片,方向只有輸入/輸出)
typedef enum {
HAL_GPIO_DIR_INPUT, // 輸入模式
HAL_GPIO_DIR_OUTPUT // 輸出模式
} hal_gpio_dir_t;
// 統一的GPIO電平枚舉
typedef enum {
HAL_GPIO_LEVEL_LOW = 0, // 低電平
HAL_GPIO_LEVEL_HIGH = 1 // 高電平
} hal_gpio_level_t;
// 統一的GPIO初始化函數(參數:芯片GPIO端口、引腳號、方向)
void hal_gpio_init(uint8_t port, uint8_t pin, hal_gpio_dir_t dir);
// 統一的GPIO寫電平函數(參數:端口、引腳號、電平)
void hal_gpio_write(uint8_t port, uint8_t pin, hal_gpio_level_t level);
// 統一的GPIO讀電平函數(返回:引腳當前電平)
hal_gpio_level_t hal_gpio_read(uint8_t port, uint8_t pin);
#endif
// HAL層源文件(以STM32F4為例,hal_gpio_stm32f4.c):封裝芯片寄存器操作
#include "hal_gpio.h"
#include "stm32f4xx.h"
void hal_gpio_init(uint8_t port, uint8_t pin, hal_gpio_dir_t dir) {
GPIO_InitTypeDef gpio_init;
// 1. 使能GPIO時鐘(STM32F4的時鐘使能邏輯)
if (port == 0) __HAL_RCC_GPIOA_CLK_ENABLE(); // port=0對應GPIOA
else if (port == 1) __HAL_RCC_GPIOB_CLK_ENABLE(); // port=1對應GPIOB
// 2. 配置GPIO方向(統一接口參數轉STM32寄存器配置)
gpio_init.Pin = (1 << pin); // 引腳號(如pin=0對應PA0)
gpio_init.Mode = (dir == HAL_GPIO_DIR_OUTPUT) ? GPIO_MODE_OUTPUT_PP : GPIO_MODE_INPUT;
gpio_init.Pull = GPIO_NOPULL;
gpio_init.Speed = GPIO_SPEED_FREQ_LOW;
// 3. 調用STM32標準庫函數初始化GPIO(操作寄存器)
HAL_GPIO_Init((GPIO_TypeDef*)(GPIOA_BASE + port*0x400), &gpio_init);
}
void hal_gpio_write(uint8_t port, uint8_t pin, hal_gpio_level_t level) {
// 調用STM32標準庫函數寫電平(屏蔽寄存器差異)
HAL_GPIO_WritePin((GPIO_TypeDef*)(GPIOA_BASE + port*0x400), (1 << pin), level);
}
// 如果換NXP RT1176,只需重寫hal_gpio_nxp_rt1176.c,接口函數名/參數完全不變
2. 中間層:BSP 層(Board Support Package,板級支持包層)——“適配具體板卡,關聯硬件用途”
BSP 層是 “芯片” 與 “開發板” 之間的橋樑,它基於 HAL 層的接口,針對具體開發板(如 “STM32F4 探索者開發板”“NXP RT1176 評估板”)做適配,核心是 “把芯片引腳和板上硬件對應起來”。
比如同樣是 STM32F4 芯片,開發板 A 的 “LED1” 接 PA0,開發板 B 的 “LED1” 接 PB5,BSP 層會把 “LED1” 這個板級硬件,和具體的引腳、HAL 接口綁定,讓上層(APP)不用管 “LED1 接哪個引腳”。
(1)BSP 層做什麼?
- 定義板級硬件映射:把開發板上的硬件(如 LED、按鍵、傳感器)和芯片引腳對應起來,用宏定義封裝(比如
BSP_LED1_PORT=0(GPIOA)、BSP_LED1_PIN=0)。 - 封裝板級硬件接口:基於 HAL 層接口,封裝 “板級硬件專屬函數”,比如
bsp_led1_init()(初始化 LED1)、bsp_led1_toggle()(翻轉 LED1)、bsp_key1_read()(讀按鍵 1 狀態)。 - 處理板級硬件差異:比如開發板 A 的按鍵用下拉電阻(按下為高電平),開發板 B 的按鍵用上拉電阻(按下為低電平),BSP 層會在
bsp_key1_read()中處理這個差異,返回統一的 “按下 = 1,鬆開 = 0” 結果給 APP 層。 - 初始化板級硬件:提供
bsp_board_init()函數,統一初始化開發板上的所有硬件(LED、按鍵、串口、傳感器),APP 層只需調用這一個函數,不用逐個初始化外設。
(2)BSP 層不做什麼?
- 不直接操作芯片寄存器(所有硬件操作都通過 HAL 層接口);
- 不包含複雜應用邏輯(比如 “按鍵按下後 LED 閃爍 5 次”,BSP 層只提供 “按鍵讀”“LED 翻轉” 接口,不做閃爍邏輯);
- 不跨開發板適配(開發板 A 的 BSP 代碼,不能直接用在開發板 B 上)。
(3)BSP 層示例代碼(以 STM32F4 探索者開發板為例):
// BSP層頭文件(bsp_board.h):板級硬件接口聲明
#ifndef __BSP_BOARD_H
#define __BSP_BOARD_H
// 1. 板級硬件引腳映射(關聯“LED1”和具體引腳)
#define BSP_LED1_PORT 0 // 對應GPIOA(HAL層的port=0)
#define BSP_LED1_PIN 0 // 對應PA0
#define BSP_KEY1_PORT 1 // 對應GPIOB
#define BSP_KEY1_PIN 1 // 對應PB1
// 2. 板級硬件接口聲明
void bsp_board_init(void); // 板級硬件統一初始化
void bsp_led1_init(void); // LED1初始化
void bsp_led1_toggle(void); // LED1翻轉
uint8_t bsp_key1_read(void); // 讀按鍵1狀態(1=按下,0=鬆開)
#endif
// BSP層源文件(bsp_board.c):基於HAL層實現板級功能
#include "bsp_board.h"
#include "hal_gpio.h"
// 板級硬件統一初始化
void bsp_board_init(void) {
bsp_led1_init(); // 初始化LED1
// 可擴展:初始化按鍵、串口、傳感器等
}
// LED1初始化(調用HAL層接口)
void bsp_led1_init(void) {
// 調用HAL層的GPIO初始化函數,配置LED1為輸出
hal_gpio_init(BSP_LED1_PORT, BSP_LED1_PIN, HAL_GPIO_DIR_OUTPUT);
// 初始狀態:LED1滅
hal_gpio_write(BSP_LED1_PORT, BSP_LED1_PIN, HAL_GPIO_LEVEL_LOW);
}
// LED1翻轉(調用HAL層接口)
void bsp_led1_toggle(void) {
hal_gpio_level_t cur_level;
// 讀當前電平
cur_level = hal_gpio_read(BSP_LED1_PORT, BSP_LED1_PIN);
// 寫相反電平
hal_gpio_write(BSP_LED1_PORT, BSP_LED1_PIN, (cur_level == HAL_GPIO_LEVEL_LOW) ? HAL_GPIO_LEVEL_HIGH : HAL_GPIO_LEVEL_LOW);
}
// 讀按鍵1狀態(處理板級硬件差異)
uint8_t bsp_key1_read(void) {
hal_gpio_level_t key_level;
// 開發板A的按鍵是下拉電阻,按下時為高電平
key_level = hal_gpio_read(BSP_KEY1_PORT, BSP_KEY1_PIN);
// 返回統一結果:1=按下,0=鬆開
return (key_level == HAL_GPIO_LEVEL_HIGH) ? 1 : 0;
}
// 如果換開發板B(按鍵接PB5,上拉電阻),只需修改宏定義和read函數:
// #define BSP_KEY1_PORT 1
// #define BSP_KEY1_PIN 5
// uint8_t bsp_key1_read(void) {
// key_level = hal_gpio_read(BSP_KEY1_PORT, BSP_KEY1_PIN);
// return (key_level == HAL_GPIO_LEVEL_LOW) ? 1 : 0; // 上拉電阻按下為低電平
// }
3. 最上層:APP 層(Application Layer,應用程序層)——“專注業務邏輯,完全脱離硬件”
APP 層是嵌入式系統的 “業務核心”,直接面向用户需求(如 “温濕度監測”“電機控制”“數據上傳”),它只調用 BSP 層提供的接口,完全不關心底層是哪種芯片、哪個開發板。
(1)APP 層做什麼?
- 實現業務邏輯:比如 “每 5 秒讀一次温濕度,超過閾值則點亮 LED 並串口報警”“按鍵按下後電機正轉 3 秒,再反轉 2 秒”,這些和具體功能相關的代碼都在 APP 層。
- 調用 BSP 層接口:所有硬件操作都通過 BSP 層的函數實現,比如用
bsp_led1_toggle()控制 LED,用bsp_key1_read()讀按鍵,用bsp_uart_send()發數據。 - 組織系統流程:比如初始化完成後進入主循環,處理傳感器數據、用户輸入、外設控制等,是系統的 “大腦”。
(2)APP 層不做什麼?
- 不調用 HAL 層接口(除非特殊需求,否則完全依賴 BSP 層);
- 不涉及任何硬件細節(如引腳號、寄存器、時鐘配置);
- 不修改底層代碼(換硬件時,APP 層代碼一行不用改)。
(3)APP 層示例代碼(温濕度監測場景):
// APP層源文件(app_temp_hum.c):業務邏輯實現
#include "bsp_board.h"
#include "bsp_sensor.h" // 假設BSP層封裝了温濕度傳感器接口
#include "bsp_uart.h" // 假設BSP層封裝了串口接口
#include "delay.h" // 延時函數
// 業務邏輯:温濕度監測(超過閾值報警)
void app_temp_hum_monitor(void) {
float temp, hum;
const float TEMP_THRESHOLD = 30.0; // 温度閾值30℃
const float HUM_THRESHOLD = 60.0; // 濕度閾值60%
// 1. 初始化系統(調用BSP層統一接口)
bsp_board_init();
bsp_uart_init(115200); // 初始化串口,波特率115200
bsp_sensor_init(); // 初始化温濕度傳感器
// 2. 主循環:處理業務
while (1) {
// 2.1 讀温濕度(調用BSP層傳感器接口)
bsp_sensor_read(&temp, &hum);
// 2.2 串口打印數據(調用BSP層串口接口)
bsp_uart_printf("Temp: %.1f℃, Hum: %.1f%%\r\n", temp, hum);
// 2.3 閾值判斷:超過則點亮LED報警
if (temp > TEMP_THRESHOLD || hum > HUM_THRESHOLD) {
bsp_led1_toggle(); // 翻轉LED(報警)
} else {
hal_gpio_write(BSP_LED1_PORT, BSP_LED1_PIN, HAL_GPIO_LEVEL_LOW); // LED滅
}
// 2.4 延時5秒(等待下一次檢測)
delay_ms(5000);
}
}
// 主函數:啓動APP業務
int main(void) {
app_temp_hum_monitor(); // 調用APP層業務函數
return 0;
}
// 重點:如果換NXP RT1176開發板,只需替換HAL層和BSP層代碼,APP層代碼完全不變!
三、三層架構的核心優勢
- 代碼可移植性極大提升:換芯片時改 HAL 層,換開發板時改 BSP 層,APP 層 “一次編寫,到處運行”。比如上面的温濕度監測代碼,從 STM32F4 移植到 NXP RT1176,只需重寫
hal_gpio_nxp_rt1176.c和bsp_board_nxp_rt1176.c,APP 層一行代碼不用動。 - 模塊化開發,分工明確:硬件工程師負責 HAL 層(封裝芯片驅動),板級工程師負責 BSP 層(適配開發板),應用工程師負責 APP 層(實現業務),各司其職,不用互相理解對方的細節。
- 維護成本降低:硬件故障時只需排查 HAL/BSP 層,業務邏輯修改時只需動 APP 層,不會 “牽一髮而動全身”。比如開發板上的 LED 引腳換了,只需改 BSP 層的
BSP_LED1_PIN宏定義,不用改 APP 層的閃爍邏輯。 - 複用性高:HAL 層的驅動可以複用到同芯片的所有項目,BSP 層的板級接口可以複用到同開發板的不同業務,避免重複造輪子。
四、實際項目中的注意事項
- 接口設計要 “穩定”:HAL 層和 BSP 層的接口一旦確定,儘量不要修改,否則會影響上層所有依賴該接口的代碼。比如
hal_gpio_init()的參數列表,確定後就不要再加 / 減參數。 - 避免跨層調用:嚴格遵守 “APP→BSP→HAL” 的調用順序,APP 層不要直接調用 HAL 層接口,否則會破壞分層邏輯,降低可移植性。
- HAL 層優先用廠商 SDK:多數芯片廠商(如 ST、NXP)都提供官方 HAL 庫(如 STM32 HAL 庫、NXP SDK),儘量基於官方庫封裝,不要自己從零寫寄存器操作,減少 bug。
- BSP 層要 “最小化”:BSP 層只封裝板級必要功能,不要把應用邏輯放進來。比如 “LED 閃爍” 是應用邏輯,應該在 APP 層用
bsp_led1_toggle()+delay_ms()實現,而不是在 BSP 層寫一個bsp_led1_blink()函數。