嵌入式驱动开发:量产级工程化实战(第 4 篇)——驱动分层——HAL 到底该多薄
一次换芯片引发的重构前年我接手了一个项目前团队用 STM32F103 做了一款数据采集器代码量大概两万行。项目要升级主控换成 STM32G474因为需要更高的 ADC 采样率和浮点运算能力。硬件工程师改完板子软件团队开始移植。按常理STM32F1 到 STM32G4都是 ST 的芯片HAL 库也兼容应该一两周就能搞定。结果移植了三周还没跑通。排查发现问题出在驱动的分层结构上。前团队写代码时把业务逻辑和硬件操作混在了一起。举几个典型例子例子一一个读取温度的函数里面直接操作了 ADC 的寄存器同时也做了温度换算和滤波计算。换芯片后 ADC 寄存器变了温度换算和滤波逻辑也要跟着改。例子二一个 UART 发送函数里面直接调用了HAL_UART_Transmit同时嵌入了 Modbus 协议帧的构造。换芯片后 HAL 库版本变了函数签名有变化协议部分也跟着受影响。例子三一个 Flash 存储模块把 SPI Flash 的读写和文件系统逻辑写在同一个文件里而且直接引用了具体的 GPIO 引脚定义。换芯片后引脚变了整个文件都要改。根本问题没有清晰的分层。硬件相关的代码和业务逻辑耦合在一起换硬件就得改业务代码。这次移植最后花了五周。其中三周是在剥离耦合。如果一开始就分好层移植可能只需要三天。这就是驱动分层的价值把变化的部分和不变的部分隔离开。硬件会变但业务逻辑尽量不变。分层的目的就是让硬件变化时只需要改最少的地方。一、分层的核心原则依赖方向什么是好的分层分层不是简单地把代码分成几个文件夹。分层要解决的核心问题是当底层变化时上层要不要改。一个好的分层结构应该满足上层依赖下层的接口不依赖下层的实现。下层不知道上层的存在。硬件相关的代码集中在最底层上层代码不直接操作寄存器。用一张图表示关键点箭头方向是单向的上层依赖下层下层不知道上层。常见的错误分层错误一没有 HAL 层驱动直接操作寄存器。// 驱动层直接操作寄存器 void UART_SendByte(uint8_t byte) { while (!(USART1-SR USART_SR_TXE)); USART1-DR byte; }换芯片后USART1、USART_SR_TXE、USART_SR_TXE这些都要改。如果这个函数被上层调用了几百次改起来就是灾难。错误二HAL 层太厚把业务逻辑塞进去。// HAL 层里做了业务逻辑 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { // 解析 Modbus 协议 if (ParseModbusByte(huart-Instance-DR)) { HandleModbusCommand(); } }HAL 回调函数应该是纯粹的硬件事件通知不应该包含协议解析。换芯片时这个回调函数可能被重写协议逻辑就丢了。错误三驱动层直接调用应用层函数。// 驱动层反向依赖应用层 void Sensor_ReadComplete(void) { // 驱动层调用应用层的函数 App_OnSensorDataReady(); }这违反了依赖方向。驱动层应该只提供数据不关心谁用、怎么用。二、HAL 层该多薄HAL 层的职责边界HAL 层是唯一允许直接操作寄存器的层。它的职责是屏蔽芯片差异。上层调用HAL_UART_Send()不关心是 STM32 还是 NXP不关心是 UART1 还是 UART2。提供最小可用的操作接口。初始化、发送、接收、控制。不包含业务逻辑。不做协议解析、不做数据换算、不做状态管理。判断 HAL 层是否合理的一个标准换一颗芯片需要改哪些文件如果只改 HAL 层的文件说明分层合理。如果驱动层、中间件层、应用层都要改说明 HAL 层太薄或者没分好。HAL 接口的设计原则原则一接口要稳定。HAL 接口一旦定义就要尽量稳定。因为上层代码依赖它。如果接口频繁变化上层就要跟着改分层就失去了意义。原则二接口要最小化。只暴露上层需要的东西。不要把所有寄存器操作都封装成 HAL 函数。比如// 好的 HAL 接口最小化 typedef struct { uint32_t baudrate; uint8_t data_bits; uint8_t stop_bits; uint8_t parity; } UART_Config_t; int HAL_UART_Init(uint8_t port, const UART_Config_t *config); int HAL_UART_Send(uint8_t port, const uint8_t *data, uint32_t len); int HAL_UART_Receive(uint8_t port, uint8_t *data, uint32_t len); int HAL_UART_SendAsync(uint8_t port, const uint8_t *data, uint32_t len, void (*callback)(int result));// 不好的 HAL 接口暴露太多细节 void HAL_UART_SetBaudrate(USART_TypeDef *instance, uint32_t baud); void HAL_UART_EnableTX(USART_TypeDef *instance); void HAL_UART_EnableRX(USART_TypeDef *instance); void HAL_UART_SetWordLength(USART_TypeDef *instance, uint8_t bits); // ... 几十个函数上层要自己组合原则三用句柄Handle而不是全局变量。// 好的做法用句柄 typedef struct UART_Handle UART_Handle_t; UART_Handle_t *HAL_UART_Open(uint8_t port, const UART_Config_t *config); int HAL_UART_Send(UART_Handle_t *handle, const uint8_t *data, uint32_t len); void HAL_UART_Close(UART_Handle_t *handle); // 不好的做法用全局变量 extern UART_HandleTypeDef huart1; extern UART_HandleTypeDef huart2;句柄的好处是支持多实例便于测试可以传入 mock 句柄避免全局变量带来的耦合。HAL 层的实现示例以 UART 为例HAL 层的实现// hal_uart.h —— 接口定义 #ifndef HAL_UART_H #define HAL_UART_H #include stdint.h typedef enum { HAL_UART_PORT_1 0, HAL_UART_PORT_2, HAL_UART_PORT_MAX } HAL_UART_Port_t; typedef struct { uint32_t baudrate; uint8_t data_bits; // 7, 8, 9 uint8_t stop_bits; // 1, 2 uint8_t parity; // 0none, 1odd, 2even } HAL_UART_Config_t; typedef void (*HAL_UART_RxCallback_t)(uint8_t byte, void *user_data); typedef void (*HAL_UART_TxCompleteCallback_t)(int result, void *user_data); int HAL_UART_Init(HAL_UART_Port_t port, const HAL_UART_Config_t *config); int HAL_UART_RegisterRxCallback(HAL_UART_Port_t port, HAL_UART_RxCallback_t callback, void *user_data); int HAL_UART_Send(HAL_UART_Port_t port, const uint8_t *data, uint32_t len); int HAL_UART_SendAsync(HAL_UART_Port_t port, const uint8_t *data, uint32_t len, HAL_UART_TxCompleteCallback_t callback, void *user_data); int HAL_UART_DeInit(HAL_UART_Port_t port); #endif// hal_uart_stm32.c —— STM32 实现 #include hal_uart.h #include stm32g4xx_hal.h static UART_HandleTypeDef huarts[HAL_UART_PORT_MAX]; static HAL_UART_RxCallback_t rx_callbacks[HAL_UART_PORT_MAX]; static void *rx_user_data[HAL_UART_PORT_MAX]; static uint8_t rx_byte[HAL_UART_PORT_MAX]; int HAL_UART_Init(HAL_UART_Port_t port, const HAL_UART_Config_t *config) { if (port HAL_UART_PORT_MAX) return -1; huarts[port].Instance (port HAL_UART_PORT_1) ? USART1 : USART2; huarts[port].Init.BaudRate config-baudrate; huarts[port].Init.WordLength (config-data_bits 8) ? UART_WORDLENGTH_8B : UART_WORDLENGTH_9B; huarts[port].Init.StopBits (config-stop_bits 1) ? UART_STOPBITS_1 : UART_STOPBITS_2; huarts[port].Init.Parity (config-parity 0) ? UART_PARITY_NONE : (config-parity 1) ? UART_PARITY_ODD : UART_PARITY_EVEN; huarts[port].Init.Mode UART_MODE_TX_RX; huarts[port].Init.HwFlowCtl UART_HWCONTROL_NONE; huarts[port].Init.OverSampling UART_OVERSAMPLING_16; if (HAL_UART_Init(huarts[port]) ! HAL_OK) { return -1; } // 启动接收中断 HAL_UART_Receive_IT(huarts[port], rx_byte[port], 1); return 0; } // HAL 回调只做数据传递不做业务处理 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { for (int i 0; i HAL_UART_PORT_MAX; i) { if (huart-Instance huarts[i].Instance) { if (rx_callbacks[i]) { rx_callbacks[i](rx_byte[i], rx_user_data[i]); } HAL_UART_Receive_IT(huarts[i], rx_byte[i], 1); break; } } }// hal_uart_mock.c —— 用于单元测试的 mock 实现 int HAL_UART_Init(HAL_UART_Port_t port, const HAL_UART_Config_t *config) { // 记录调用返回成功 mock_record_call(HAL_UART_Init, port, config); return 0; } int HAL_UART_Send(HAL_UART_Port_t port, const uint8_t *data, uint32_t len) { mock_record_send(port, data, len); return 0; }这个设计的价值应用层和驱动层只依赖hal_uart.h。换芯片时只需要换hal_uart_stm32.c为hal_uart_nxp.c上层代码一行不改。单元测试时用hal_uart_mock.c替换不需要真实硬件。HAL 层该多薄一个判断标准HAL 层应该薄到只做硬件操作不做任何决策。具体来说一个简单的测试如果把 HAL 层的代码打印出来给一个不懂业务的人看他应该只能看到硬件操作看不到业务逻辑。三、驱动层设备驱动怎么做驱动层的职责驱动层在 HAL 层之上负责把一个具体的设备传感器、Flash、屏幕封装成可用的接口。它知道设备的特性比如某个传感器的寄存器地址、某个 Flash 的扇区大小但不涉及业务逻辑。驱动层的核心任务是设备初始化序列。比如某个传感器需要先写配置寄存器再等待一段时间再读 ID 确认。数据读写。把设备的原始数据转换成有意义的物理量比如 ADC 值转温度。错误处理。处理设备特有的错误通信超时、校验失败。状态管理。跟踪设备的状态已初始化、忙、错误。驱动层接口设计示例以 SPI Flash 驱动为例// drv_flash.h —— 驱动接口 #ifndef DRV_FLASH_H #define DRV_FLASH_H #include stdint.h #include stdbool.h typedef struct DrvFlash DrvFlash_t; typedef struct { uint32_t sector_size; // 扇区大小 uint32_t total_size; // 总容量 uint32_t page_size; // 页大小 } DrvFlash_Info_t; // 创建/销毁 DrvFlash_t *DrvFlash_Create(void); void DrvFlash_Destroy(DrvFlash_t *flash); // 初始化 int DrvFlash_Init(DrvFlash_t *flash); // 读写 int DrvFlash_Read(DrvFlash_t *flash, uint32_t addr, uint8_t *buf, uint32_t len); int DrvFlash_Write(DrvFlash_t *flash, uint32_t addr, const uint8_t *buf, uint32_t len); int DrvFlash_EraseSector(DrvFlash_t *flash, uint32_t addr); // 信息 int DrvFlash_GetInfo(DrvFlash_t *flash, DrvFlash_Info_t *info); // 状态 bool DrvFlash_IsReady(DrvFlash_t *flash); #endif// drv_flash_w25qxx.c —— W25Qxx 系列 Flash 驱动实现 #include drv_flash.h #include hal_spi.h #include hal_gpio.h #define W25Q_CMD_READ_ID 0x9F #define W25Q_CMD_READ_DATA 0x03 #define W25Q_CMD_PAGE_PROGRAM 0x02 #define W25Q_CMD_SECTOR_ERASE 0x20 #define W25Q_CMD_READ_STATUS 0x05 #define W25Q_CMD_WRITE_ENABLE 0x06 struct DrvFlash { HAL_SPI_Handle_t *spi; HAL_GPIO_Pin_t cs_pin; DrvFlash_Info_t info; bool initialized; }; // 私有函数发送命令 static int flash_send_cmd(DrvFlash_t *flash, uint8_t cmd) { HAL_GPIO_Write(flash-cs_pin, 0); int ret HAL_SPI_Transfer(flash-spi, cmd, NULL, 1); HAL_GPIO_Write(flash-cs_pin, 1); return ret; } // 私有函数等待忙状态结束 static int flash_wait_ready(DrvFlash_t *flash, uint32_t timeout_ms) { uint32_t start HAL_GetTick(); uint8_t status; do { HAL_GPIO_Write(flash-cs_pin, 0); uint8_t cmd W25Q_CMD_READ_STATUS; HAL_SPI_Transfer(flash-spi, cmd, NULL, 1); HAL_SPI_Transfer(flash-spi, NULL, status, 1); HAL_GPIO_Write(flash-cs_pin, 1); if (!(status 0x01)) return 0; // 不忙了 if (HAL_GetTick() - start timeout_ms) { return -2; // 超时 } } while (1); } int DrvFlash_Init(DrvFlash_t *flash) { // 读取 ID确认设备存在 HAL_GPIO_Write(flash-cs_pin, 0); uint8_t cmd W25Q_CMD_READ_ID; uint8_t id[3]; HAL_SPI_Transfer(flash-spi, cmd, NULL, 1); HAL_SPI_Transfer(flash-spi, NULL, id, 3); HAL_GPIO_Write(flash-cs_pin, 1); if (id[0] 0x00 || id[0] 0xFF) { return -1; // 设备不存在 } // 根据 ID 填充 info flash-info.sector_size 4096; flash-info.page_size 256; flash-info.total_size 8 * 1024 * 1024; // 8MB flash-initialized true; return 0; } int DrvFlash_Write(DrvFlash_t *flash, uint32_t addr, const uint8_t *buf, uint32_t len) { if (!flash-initialized) return -1; // 写使能 flash_send_cmd(flash, W25Q_CMD_WRITE_ENABLE); // 页编程 HAL_GPIO_Write(flash-cs_pin, 0); uint8_t cmd W25Q_CMD_PAGE_PROGRAM; HAL_SPI_Transfer(flash-spi, cmd, NULL, 1); uint8_t addr_bytes[3] {addr 16, addr 8, addr}; HAL_SPI_Transfer(flash-spi, addr_bytes, NULL, 3); HAL_SPI_Transfer(flash-spi, (uint8_t *)buf, NULL, len); HAL_GPIO_Write(flash-cs_pin, 1); // 等待写完 return flash_wait_ready(flash, 1000); }驱动层的关键点只依赖 HAL 接口hal_spi.h、hal_gpio.h不直接操作寄存器封装了设备的特性W25Q 的命令集、状态寄存器提供清晰的错误码不涉及业务逻辑不关心写入的是什么数据驱动层如何做到可替换一个常见的需求是产品有多个型号用的 Flash 型号不同W25Q64、W25Q128、GD25Q64。如果驱动层写死了 W25Q换型号就要改代码。解决方案驱动接口 具体实现分离。// drv_flash.h —— 通用接口 typedef struct DrvFlash DrvFlash_t; // 驱动操作表虚函数表 typedef struct { int (*init)(DrvFlash_t *flash); int (*read)(DrvFlash_t *flash, uint32_t addr, uint8_t *buf, uint32_t len); int (*write)(DrvFlash_t *flash, uint32_t addr, const uint8_t *buf, uint32_t len); int (*erase_sector)(DrvFlash_t *flash, uint32_t addr); } DrvFlash_Ops_t; struct DrvFlash { const DrvFlash_Ops_t *ops; void *priv; // 具体驱动的私有数据 }; // 通用 API int DrvFlash_Init(DrvFlash_t *flash) { return flash-ops-init(flash); } int DrvFlash_Read(DrvFlash_t *flash, uint32_t addr, uint8_t *buf, uint32_t len) { return flash-ops-read(flash, addr, buf, len); }// drv_flash_w25qxx.c —— W25Q 实现 static const DrvFlash_Ops_t w25q_ops { .init w25q_init, .read w25q_read, .write w25q_write, .erase_sector w25q_erase_sector, }; DrvFlash_t *DrvFlash_CreateW25Q(HAL_SPI_Handle_t *spi, HAL_GPIO_Pin_t cs) { DrvFlash_t *flash malloc(sizeof(DrvFlash_t)); flash-ops w25q_ops; flash-priv malloc(sizeof(W25Q_Priv_t)); // 初始化 priv return flash; }应用层只依赖DrvFlash_t和通用 API不关心具体是哪个型号。在初始化时根据硬件版本选择不同的创建函数DrvFlash_t *flash; if (hardware_version HW_V1) { flash DrvFlash_CreateW25Q(spi, cs); } else { flash DrvFlash_CreateGD25Q(spi, cs); } DrvFlash_Init(flash);四、中间件层通用服务的复用中间件层的定位中间件层是跨项目复用的通用服务。它们不依赖具体硬件也不依赖具体业务。典型的中间件包括环形缓冲区Ring Buffer协议解析器Modbus、JSON、自定义协议日志系统文件系统FatFS、LittleFS状态机框架事件总线中间件层的特点与硬件无关。不包含任何寄存器操作。与业务无关。不包含具体的业务逻辑。可独立测试。可以在 PC 上编译运行不需要 MCU。接口稳定。一旦定义很少变化。中间件层的示例环形缓冲区// ring_buffer.h —— 中间件层 #ifndef RING_BUFFER_H #define RING_BUFFER_H #include stdint.h #include stdbool.h typedef struct { uint8_t *buf; uint32_t size; volatile uint32_t head; // 写指针 volatile uint32_t tail; // 读指针 } RingBuffer_t; void RingBuffer_Init(RingBuffer_t *rb, uint8_t *buf, uint32_t size); bool RingBuffer_Put(RingBuffer_t *rb, uint8_t byte); bool RingBuffer_Get(RingBuffer_t *rb, uint8_t *byte); uint32_t RingBuffer_Count(RingBuffer_t *rb); void RingBuffer_Flush(RingBuffer_t *rb); #endif// ring_buffer.c #include ring_buffer.h void RingBuffer_Init(RingBuffer_t *rb, uint8_t *buf, uint32_t size) { rb-buf buf; rb-size size; rb-head 0; rb-tail 0; } // 单生产者调用ISR 或单个任务 bool RingBuffer_Put(RingBuffer_t *rb, uint8_t byte) { uint32_t next_head (rb-head 1) % rb-size; if (next_head rb-tail) { return false; // 满了 } rb-buf[rb-head] byte; rb-head next_head; return true; } // 单消费者调用单个任务 bool RingBuffer_Get(RingBuffer_t *rb, uint8_t *byte) { if (rb-head rb-tail) { return false; // 空了 } *byte rb-buf[rb-tail]; rb-tail (rb-tail 1) % rb-size; return true; }这个环形缓冲区可以在 PC 上单元测试// test_ring_buffer.c void test_ring_buffer_basic(void) { uint8_t buf[10]; RingBuffer_t rb; RingBuffer_Init(rb, buf, 10); // 写入 5 个字节 for (int i 0; i 5; i) { assert(RingBuffer_Put(rb, i) true); } assert(RingBuffer_Count(rb) 5); // 读取 5 个字节 for (int i 0; i 5; i) { uint8_t byte; assert(RingBuffer_Get(rb, byte) true); assert(byte i); } assert(RingBuffer_Count(rb) 0); }中间件层的价值一次编写多个项目复用。一次测试所有项目受益。五、应用层业务逻辑的归属应用层该做什么应用层是业务逻辑的归属地。它知道要做什么不关心怎么做。比如数据采集流程先读传感器再滤波再判断阈值再上报协议处理收到 Modbus 请求解析执行构造响应状态机设备的状态转换逻辑任务调度创建哪些任务优先级怎么定应用层不直接操作硬件也不直接调用 HAL。它通过驱动层的接口访问设备通过中间件层提供的服务处理数据。应用层与驱动层的交互应用层通过驱动接口获取数据然后做业务处理// app_sensor.c —— 应用层 #include drv_temperature.h #include ring_buffer.h #include filter.h static DrvTemperature_t *temp_sensor; static RingBuffer_t temp_history; void App_SensorInit(void) { temp_sensor DrvTemperature_Create(HAL_I2C_PORT_1, 0x48); DrvTemperature_Init(temp_sensor); static uint8_t history_buf[64]; RingBuffer_Init(temp_history, history_buf, 64); } void App_SensorTask(void *pvParameters) { for (;;) { // 通过驱动接口读取原始数据 float raw_temp; if (DrvTemperature_Read(temp_sensor, raw_temp) 0) { // 应用层做业务处理滤波 float filtered Filter_Apply(temp_filter, raw_temp); // 应用层做业务判断阈值告警 if (filtered TEMP_ALARM_THRESHOLD) { App_TriggerAlarm(ALARM_HIGH_TEMP); } // 应用层决定是否上报 if (ShouldReport(filtered)) { App_ReportTemperature(filtered); } } vTaskDelay(pdMS_TO_TICKS(1000)); } }关键点应用层知道温度超过阈值要告警但不知道温度传感器是 I2C 接口还是 SPI 接口。驱动层知道怎么读 I2C 寄存器但不知道读了之后要判断阈值。应用层与中间件层的交互应用层使用中间件提供的服务但不关心中间件的实现// app_protocol.c —— 应用层 #include modbus.h // 中间件层 #include drv_uart.h static Modbus_t modbus; static DrvUart_t *uart; void App_ProtocolInit(void) { uart DrvUart_Create(HAL_UART_PORT_1, 115200); DrvUart_Init(uart); Modbus_Init(modbus, MODBUS_SLAVE, 0x01); } void App_ProtocolTask(void *pvParameters) { uint8_t byte; uint8_t response[256]; uint32_t response_len; for (;;) { // 从驱动层读取数据 if (DrvUart_ReadByte(uart, byte, 100) 0) { // 交给中间件层的 Modbus 解析器 ModbusResult_t result Modbus_ParseByte(modbus, byte); if (result MODBUS_FRAME_COMPLETE) { // 应用层处理业务逻辑 App_HandleModbusRequest(modbus, response, response_len); // 通过驱动层发送响应 DrvUart_Write(uart, response, response_len); } } } }六、分层带来的收益一个真实对比回到开头那个移植项目。如果一开始就分好层移植的工作量会是什么样需要改的文件实际移植时间大约 3 天。其中 2 天是重写 HAL 层1 天是调试。对比没有分层的版本5 周。其中 3 周在剥离耦合2 周在调试。分层带来的收益是 10 倍以上的效率差异。而且这个收益在每次换芯片、每次升级硬件时都会重复获得。七、常见误区与踩坑记录误区一HAL 层太厚变成业务层有些项目的 HAL 层里塞满了业务逻辑比如// HAL 层里做了协议解析 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (ParseModbusByte(...)) { HandleModbusCommand(...); } }问题换芯片时这个回调函数要重写业务逻辑就丢了。或者换协议时要改 HAL 层而 HAL 层本不该关心协议。正确做法HAL 回调只做数据传递协议解析放在中间件层业务处理放在应用层。误区二驱动层直接调用应用层// 驱动层反向依赖 void DrvSensor_OnDataReady(float value) { App_OnSensorData(value); // 驱动层调用应用层 }问题依赖方向反了。驱动层应该只提供数据不关心谁用。正确做法驱动层提供回调注册接口应用层注册回调// 驱动层 typedef void (*DrvSensor_Callback_t)(float value, void *user_data); void DrvSensor_RegisterCallback(DrvSensor_t *sensor, DrvSensor_Callback_t callback, void *user_data); // 应用层 void App_OnSensorData(float value, void *user_data) { // 业务处理 } DrvSensor_RegisterCallback(sensor, App_OnSensorData, NULL);误区三中间件层依赖硬件有些中间件比如日志系统为了性能直接操作 UART 寄存器。这破坏了中间件层的可移植性。正确做法中间件层通过接口与硬件交互。比如日志系统提供一个输出函数接口由应用层注入具体的 UART 发送函数// 中间件层 typedef void (*Log_OutputFunc_t)(const char *str); void Log_Init(Log_OutputFunc_t output); void Log_Info(const char *fmt, ...); // 应用层 void App_LogOutput(const char *str) { DrvUart_Write(uart, (const uint8_t *)str, strlen(str)); } Log_Init(App_LogOutput);误区四分层过度接口太多分层是为了降低耦合不是为了看起来专业。如果一个小项目只有三个文件硬要分成 HAL/驱动/中间件/应用四层反而增加了复杂度。判断标准如果这个项目永远不会换芯片也不会复用代码那分两层就够了硬件层 业务层。分层程度应该与项目的复杂度、生命周期、复用需求匹配。误区五接口不稳定频繁变化HAL 接口一旦定义就应该尽量稳定。如果因为某个项目的特殊需求频繁修改接口说明接口设计有问题。正确做法接口设计时考虑通用性。如果某个项目有特殊需求通过扩展接口而不是修改接口来满足。比如// 基础接口 int HAL_UART_Send(HAL_UART_Port_t port, const uint8_t *data, uint32_t len); // 扩展接口不修改基础接口 int HAL_UART_SendAsync(HAL_UART_Port_t port, const uint8_t *data, uint32_t len, HAL_UART_TxCompleteCallback_t callback, void *user_data);八、本篇小结驱动分层的核心是隔离变化。硬件会变业务逻辑尽量不变。分层的目的是让硬件变化时只需要改最少的地方。第一分层的核心原则是依赖方向。上层依赖下层的接口不依赖下层的实现。下层不知道上层的存在。硬件相关的代码集中在 HAL 层上层代码不直接操作寄存器。第二HAL 层要薄。只做硬件操作不做任何决策。HAL 层是唯一允许直接操作寄存器的层它屏蔽芯片差异提供最小可用的操作接口。第三驱动层封装设备特性。它知道设备的寄存器地址、初始化序列、错误处理方式但不涉及业务逻辑。通过接口 实现分离支持多型号设备的可替换。第四中间件层提供跨项目复用的通用服务。环形缓冲区、协议解析器、日志系统、文件系统这些与硬件无关、与业务无关的代码应该独立成层便于测试和复用。第五应用层是业务逻辑的归属地。它知道要做什么不关心怎么做。通过驱动层接口访问设备通过中间件层服务处理数据。分层的收益是复利式的。每次换芯片、每次升级硬件、每次新增型号分层带来的效率差异都会重复获得。下一篇我们讲资源受限下的日志系统设计。这是模块一的最后一篇也是把前面所有内容串起来的一篇。我会展示怎么用环形缓冲区 DMA 低优先级日志任务设计一个在 64KB RAM 的 MCU 上运行、不影响实时性、又能保留足够现场信息的日志系统。思考题你的项目里驱动层有没有直接操作寄存器如果有换芯片时需要改多少地方HAL 层应该提供同步发送还是异步发送接口还是两者都要如果中间件层需要分配内存比如动态创建对象应该由谁提供内存分配函数欢迎在评论区留下你的答案。下一篇见。 点赞 收藏 关注如果这篇文章让你对驱动分层有了清晰的认识点赞让更多同行看到收藏方便随时查阅关注不错过下一篇《资源受限下的日志系统设计》。