2. 可移植性设计原则:硬件抽象层(HAL)概念、分层设计思想、接口与实现分离
好,咱们接着聊。上一章我们说了可移植性为什么重要,这一章就深入讲讲具体怎么落地。说白了,就是三个核心原则:硬件抽象层(HAL)、分层设计、接口与实现分离。这三个东西,你吃透了,Bootloader的可移植性就稳了一大半。
2.1 硬件抽象层(HAL)——给硬件穿上“外套”
什么是HAL?我个人的理解是:在硬件和软件之间,加一层“翻译官”。这个翻译官负责把硬件的“方言”翻译成统一的“普通话”。
举个例子。你写Bootloader,肯定要操作串口吧?不同芯片的串口寄存器地址、配置方式都不一样。如果你在代码里直接写 *(volatile uint32_t *)0x40011000 = 0x05;,那完了,换个芯片就得改所有代码。
HAL的做法是什么?
- 定义一组统一的函数,比如
hal_uart_init()、hal_uart_send()、hal_uart_recv()。 - 在底层,针对不同芯片实现这些函数。
- 上层代码只调用这些HAL函数,根本不知道底层是哪个芯片。
我在项目中遇到过最典型的例子:一个产品用了ST的STM32F4,后来因为成本原因要换成GD32。如果没有HAL层,那真是噩梦——所有串口、GPIO、Flash操作全要重写。但因为我们一开始就设计了HAL,换芯片只改了底层几个文件,上层代码一行没动。
核心思想:HAL不是让你“不用管硬件”,而是让你“把硬件差异关在笼子里”。
2.2 分层设计思想——别让代码“糊成一团”
分层设计,说白了就是各司其职。我习惯把Bootloader分成三层:
| 层次 | 职责 | 示例 |
|---|---|---|
| 应用层 | 业务逻辑,比如固件升级流程、校验、跳转 | app_update_firmware() |
| 中间层 | 协议解析、数据管理、状态机 | protocol_parse_packet() |
| 硬件抽象层 | 直接操作硬件寄存器 | hal_flash_erase() |
你想想看,如果这三层混在一起,会是什么后果?一个串口接收函数里,既在解析协议,又在操作Flash,还在判断升级状态。这种代码,别说移植了,自己看都费劲。
分层的好处很明显:
- 隔离变化:硬件变了,只改HAL层;协议变了,只改中间层;业务逻辑变了,只改应用层。
- 便于测试:每一层都可以单独测试。我经常在PC上先模拟HAL层,把应用层逻辑调通了再下板子。
- 团队协作:不同人负责不同层,互不干扰。
一个小建议:分层不是越多越好。对于Bootloader这种资源受限的场景,三层就够了。层数太多,函数调用开销和代码体积都会增加。
2.3 接口与实现分离——把“承诺”和“做法”分开
这个原则,我理解就是面向接口编程。接口是“承诺”,实现是“做法”。
具体怎么做?用C语言的话,我习惯用结构体封装函数指针。比如:
// 接口定义(在头文件中)
typedef struct {
void (*init)(void);
void (*send)(const uint8_t *data, uint32_t len);
uint32_t (*recv)(uint8_t *buffer, uint32_t max_len);
} uart_interface_t;
// 实现(在源文件中)
static void stm32_uart_init(void) { /* STM32的初始化代码 */ }
static void stm32_uart_send(...) { /* STM32的发送代码 */ }
const uart_interface_t uart_stm32 = {
.init = stm32_uart_init,
.send = stm32_uart_send,
.recv = stm32_uart_recv,
};
上层代码怎么用?很简单:
// 应用层代码
extern const uart_interface_t uart_stm32;
void app_main(void) {
uart_stm32.init();
uart_stm32.send("Hello", 5);
}
换芯片的时候,只需要换一个接口实例:
// 换成GD32的实现
extern const uart_interface_t uart_gd32;
// 把 uart_stm32 改成 uart_gd32 即可
嗯,这里要注意:接口定义要足够通用,别把某个芯片的特性暴露出去。我曾经见过有人把STM32的DMA通道号写在接口参数里,结果换芯片时发现GD32的DMA机制完全不同,接口根本没法复用。
避坑指南:接口参数尽量用抽象类型。比如用 uint32_t 表示地址,而不是用 uint8_t*;用 uint32_t 表示时间,而不是用某个芯片的定时器计数值。
2.4 三个原则如何协同工作
这三个原则不是孤立的,它们是一个整体:
- 分层设计 决定了代码的组织结构。
- HAL 是分层设计中“硬件相关层”的具体实现方式。
- 接口与实现分离 是让分层真正“解耦”的关键技术。
我举个例子你就明白了。假设你要写一个Flash驱动:
- 分层设计告诉你:Flash驱动属于HAL层,上层不要直接调用Flash寄存器。
- HAL告诉你:定义
hal_flash_read()、hal_flash_write()、hal_flash_erase()这些函数。 - 接口与实现分离告诉你:把函数声明放在
hal_flash.h,把具体实现放在hal_flash_stm32.c或hal_flash_gd32.c。
这样,当你要从STM32移植到GD32时,只需要:
- 新建一个
hal_flash_gd32.c,实现所有HAL函数。 - 在编译脚本里把
hal_flash_stm32.c换成hal_flash_gd32.c。 - 上层代码一行不改,重新编译即可。
我曾经用这个方式,三天内把一套Bootloader从ARM Cortex-M3移植到了RISC-V平台。说实话,大部分时间都花在熟悉新芯片的手册上,代码改动量其实很小。
总结一句话:可移植性不是靠“运气”,而是靠“设计”。HAL、分层、接口分离,就是这三个设计原则,让你的Bootloader“一次编写,到处运行”。
下一章,我们会具体聊聊如何设计一个通用的HAL接口,包括常见的UART、Flash、GPIO、定时器等外设的抽象方法。到时候我会拿出一些实际项目中的代码片段,咱们一起分析。