一次"换芯片"引发的重构
前年我接手了一个项目,前团队用 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; // 0=none, 1=odd, 2=even } 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 层应该提供"同步发送"还是"异步发送"接口?还是两者都要?
如果中间件层需要分配内存(比如动态创建对象),应该由谁提供内存分配函数?
欢迎在评论区留下你的答案。下一篇见。
💖 点赞 + 收藏 + 关注
如果这篇文章让你对驱动分层有了清晰的认识,点赞让更多同行看到,收藏方便随时查阅,关注不错过下一篇《资源受限下的日志系统设计》。