2. 系统裁剪基础
做嵌入式系统裁剪,说白了就是让软件和硬件对上话。但硬件千奇百怪,CPU内核不同、外设地址不同、中断号不同……你总不能每换一块板子就把整个内核重写一遍吧?
所以就有了硬件抽象层、设备树、板级支持包这三样东西。我刚开始做适配那会儿,也分不清它们到底谁管谁。后来踩了几个坑才明白——HAL是接口规范,设备树是描述语言,BSP是落地实现。今天咱们就把这三块掰开揉碎了讲清楚。
核心观点:硬件适配的本质,就是把“硬件差异”隔离在BSP层,让上层代码只跟HAL打交道。设备树则是连接硬件描述与驱动代码的“桥梁”。
2.1 硬件抽象层(HAL)——给上层一个“统一接口”
HAL这个概念,最早是从大型机下放到嵌入式的。它的目标很简单:把硬件相关的操作封装成标准API。比如读写GPIO,不管你是STM32还是i.MX,上层调用的都是 HAL_GPIO_WritePin()。
我个人习惯把HAL分成三层:
- 芯片级HAL:直接操作寄存器,跟具体MCU绑定。比如操作定时器、ADC。
- 板级HAL:封装板载外设,比如LED、按键、传感器。这一层会调用芯片级HAL。
- 应用层接口:给业务代码调用的统一API,比如
Board_LED_On()。
我在项目中遇到过一个问题:某款国产MCU的GPIO输出寄存器是32位对齐的,而另一款是16位对齐。如果应用层直接操作寄存器,换芯片就得改所有代码。后来我们统一封装了一层HAL,上层只调用 HAL_GPIO_SetPin(),底层差异全在HAL里消化掉。嗯,这就是HAL的价值。
我的建议:写HAL时,接口参数尽量用枚举和结构体,别用裸的int。比如 HAL_GPIO_WritePin(GPIO_PIN_5, PIN_HIGH) 就比 write_gpio(5, 1) 可读性强得多。你想想看,半年后你自己回来看代码,枚举比魔法数字友好太多了。
2.2 设备树(DTS)——把硬件“说清楚”
设备树是Linux内核引入的硬件描述机制。说白了,就是用一种树形结构,把CPU、内存、外设、中断、时钟这些硬件信息写在一个文本文件里。内核启动时解析这个文件,就知道板子上有什么硬件、地址是多少、怎么配置。
一个典型的设备树节点长这样:
/ {
model = "TI AM335x BeagleBone Black";
compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x10000000>; /* 256 MB */
};
leds {
compatible = "gpio-leds";
led0 {
label = "beaglebone:green:usr0";
gpios = <&gpio1 21 GPIO_ACTIVE_HIGH>;
linux,default-trigger = "heartbeat";
};
};
};
这里有几个关键点:
- compatible:驱动匹配的关键字。内核根据这个字符串找到对应的驱动。
- reg:描述地址和大小。比如内存起始0x80000000,大小256MB。
- gpios:描述GPIO引脚。哪个控制器、第几号、有效电平。
我曾经犯过一个低级错误:写设备树时把 reg 的地址写成了32位,但芯片是64位的,结果内存只认到4GB。查了两天才发现是设备树里少了个 #address-cells = <2>。所以啊,设备树里的每个属性都不是随便写的,它直接决定了内核能不能正确识别硬件。
注意:设备树编译工具是 dtc,源文件后缀 .dts,编译后生成 .dtb 二进制文件。内核启动时由bootloader加载dtb到内存,然后解析。如果你改了dts但没重新编译dtb,那等于没改——我见过有人改完dts直接烧录,结果板子起不来,就是这个原因。
2.3 板级支持包(BSP)——把HAL和设备树“串起来”
BSP是一个集合概念。它包含了:
- 引导程序(bootloader)
- 内核(含设备树)
- 驱动代码
- HAL层实现
- 配置文件(如内核裁剪的defconfig)
一个典型的BSP目录结构长这样:
bsp/
├── bootloader/ # u-boot 或 barebox
│ ├── board/ # 板级配置
│ └── cpu/ # CPU相关代码
├── kernel/ # Linux内核源码
│ ├── arch/ # 架构相关
│ │ └── arm/boot/dts/ # 设备树文件
│ └── drivers/ # 驱动代码
├── hal/ # 硬件抽象层
│ ├── include/ # HAL头文件
│ └── src/ # HAL实现
├── configs/ # 内核配置
│ └── myboard_defconfig
└── tools/ # 编译工具链、烧录脚本
我参与过一个项目,团队把BSP和应用程序放在同一个仓库里。结果每次应用更新都要重新编译整个BSP,耗时半小时。后来我们把BSP独立成子模块,应用只依赖HAL的接口头文件,编译时间降到2分钟。所以我的建议是:BSP一定要跟应用代码解耦,哪怕只是目录级别的分离。
2.4 三者的关系——一张图说清楚
下面这张图展示了HAL、设备树、BSP在系统启动和运行时的协作关系:
从这张图可以看得很清楚:
- BSP 是整个大框,包含了从bootloader到驱动的所有硬件相关代码。
- HAL 是驱动层之上的抽象接口,让上层代码不直接操作寄存器。
- 设备树 是硬件描述文件,驱动通过API读取它来获取硬件配置。
说白了,BSP就是“这板子专属的软件包”,HAL是“统一的编程接口”,设备树是“硬件的说明书”。三者缺一不可。
2.5 实际项目中的避坑指南
做硬件适配这些年,我总结了几条经验,分享给你:
| 坑点 | 表现 | 解决办法 |
|---|---|---|
| 设备树地址写错 | 内存只认到一半,或外设无法访问 | 用 dtc -I dtb -O dts 反编译确认地址值 |
| HAL接口不统一 | 换芯片后应用层代码大量修改 | 接口参数用枚举/结构体,避免裸int |
| BSP与应用耦合 | 编译时间长,维护困难 | BSP独立成子模块,应用只依赖HAL头文件 |
| 忘记更新dtb | 改了dts但板子没变化 | 编译脚本里加一步自动生成dtb的规则 |
我的习惯:每次修改设备树后,我都会在启动日志里搜一下 OF: fdt 开头的行,确认内核确实加载了我新编译的dtb。这一步只要10秒,但能省下几小时的调试时间。
好了,这一章的内容就到这里。硬件适配不是一蹴而就的事,但把HAL、设备树、BSP这三者的关系理清了,后面做裁剪和移植就会顺手很多。