2. 硬件适配基础:HAL、设备树与BSP结构

做嵌入式系统裁剪,说白了就是让软件和硬件对上话。但硬件千奇百怪,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在系统启动和运行时的协作关系:

HAL、设备树与BSP协作关系图 HAL、设备树与BSP协作关系 应用层(业务代码) 硬件抽象层(HAL) 驱动层(调用HAL + 解析设备树) 硬件(CPU/外设/内存) 设备树(DTS/DTB) 描述硬件资源 驱动通过API读取 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这三者的关系理清了,后面做裁剪和移植就会顺手很多。