1. BSP与设备树概述:BSP概念、设备树起源、设备树在Linux内核中的作用

1.1 什么是BSP?说白了就是板级支持包

做嵌入式开发这么多年,我经常被问到:“BSP到底是什么?”

其实很简单。BSP就是Board Support Package,板级支持包。它是一层软件,夹在硬件和操作系统之间。你想想看,一块开发板上有CPU、内存、串口、网卡、GPIO……这些东西怎么让Linux内核认识?靠的就是BSP。

我个人习惯把BSP拆成三块来看:

  • 引导加载程序:比如U-Boot,负责把内核从Flash搬到内存里
  • 内核驱动:让Linux内核能操作板子上的各种外设
  • 板级配置:告诉内核“我这块板子用了哪个GPIO做LED,哪个I2C接了触摸屏”

我在项目中遇到过最头疼的事,就是换了一块Flash芯片,结果BSP里要改十几个地方。为什么?因为早期Linux内核把板级信息硬编码在C代码里了。改一个引脚,得翻遍arch/arm/mach-xxx目录下的文件。嗯,这就要说到设备树的由来了。

核心观点:BSP的本质是“硬件描述 + 驱动代码”的集合。设备树的出现,就是为了把“硬件描述”这部分从代码中剥离出来。

1.2 设备树的起源:为什么要有设备树?

设备树(Device Tree)的诞生,说白了是被逼出来的。

在Linux 2.6时代,ARM架构的板级信息全写在C文件里。每个厂商、每块板子都要在arch/arm/mach-xxx/下加一堆代码。我记得当时光是维护这些板级文件,内核社区就快疯了。为什么?因为代码量太大,而且重复性极高。

举个例子:

// 老式板级文件写法
static struct resource uart_resources[] = {
    {
        .start = 0x10000000,
        .end   = 0x10000FFF,
        .flags = IORESOURCE_MEM,
    },
    {
        .start = IRQ_UART0,
        .end   = IRQ_UART0,
        .flags = IORESOURCE_IRQ,
    },
};

static struct platform_device uart_device = {
    .name = "my_uart",
    .id   = 0,
    .num_resources = ARRAY_SIZE(uart_resources),
    .resource = uart_resources,
};

你看,这还只是一个UART。一块板子上有几十个外设,每个都要这么写。而且换一块板子,这些代码基本要重写。

2006年左右,PowerPC架构率先引入了设备树。ARM架构直到2011年才开始大规模采用。我当时参与的一个项目,正好赶上了这个过渡期。一边要维护老式的板级文件,一边要学习设备树语法。说实话,刚开始觉得设备树挺麻烦的,但用顺手之后,真香。

我的建议:如果你现在还在用老式板级文件做新项目,赶紧迁移到设备树。这不是技术偏好问题,而是生态问题。Linux内核社区已经不再接受新的板级文件补丁了。

1.3 设备树在Linux内核中的作用

设备树到底在Linux内核里扮演什么角色?我用一句话概括:它是硬件信息的“数据库”。

内核启动时,会解析设备树文件(.dtb),然后根据里面的描述去初始化硬件。设备树告诉内核:

  • CPU是什么型号,有几个核
  • 内存的起始地址和大小
  • 各个外设挂在哪个总线上,地址是多少
  • 中断号、时钟频率、DMA通道等等

来看一个实际的设备树节点:

uart0: serial@10000000 {
    compatible = "my_company,my_uart";
    reg = <0x10000000 0x1000>;
    interrupts = <0 33 4>;
    clocks = <&uart_clk>;
    status = "okay";
};

这个节点描述了一个UART控制器。compatible字段告诉内核用哪个驱动,reg字段告诉内核寄存器地址范围,interrupts字段告诉内核中断号。内核的驱动框架会根据这些信息自动匹配和初始化。

我曾经踩过一个坑:设备树里reg字段的地址写错了,结果内核一访问UART寄存器就死机。查了两天才发现是地址偏移了一位。嗯,设备树这东西,写的时候一定要仔细核对硬件手册。

1.4 设备树的核心文件结构

一个完整的设备树项目,通常包含这几类文件:

文件类型 扩展名 作用
设备树源文件 .dts 描述具体板子的硬件信息
设备树头文件 .dtsi SoC级别的通用描述,可被多个.dts包含
设备树二进制文件 .dtb 编译后的设备树,内核直接使用
设备树补丁文件 .dtbo 用于运行时覆盖设备树节点

我个人习惯把.dtsi文件当作“SoC级描述”,把.dts文件当作“板级描述”。比如一个芯片厂商提供imx6ull.dtsi,里面定义了CPU、内存控制器、各种外设的框架。然后具体的板子,比如“野火i.MX6ULL开发板”,再写一个imx6ull-fire.dts,去填充具体的引脚复用、设备地址等信息。

注意:设备树不是万能的。它只描述“硬件有什么”,不描述“硬件怎么用”。驱动的逻辑代码还是要写在C文件里。别指望设备树能帮你写驱动。

1.5 设备树在启动流程中的位置

为了让你更直观地理解,我画了一张设备树在Linux启动流程中的位置图:

设备树在Linux启动流程中的位置 Bootloader 加载 .dtb 到内存 内核解压 解析 .dtb 文件 设备树解析 创建 platform_device 驱动匹配 compatible 匹配 驱动初始化 读取 reg, interrupts 等 设备树核心作用 1. 硬件信息描述 2. 驱动与硬件解耦 3. 支持多板型 4. 运行时覆盖 5. 减少内核代码

从这张图你可以看到,设备树贯穿了整个启动流程。Bootloader把.dtb文件加载到内存,内核解压后解析设备树,然后根据设备树创建platform_device,最后驱动通过compatible字段匹配到对应的设备。

我刚开始学设备树时,总以为它只是“另一种配置文件”。后来才明白,它其实是内核驱动模型的核心一环。没有设备树,platform总线驱动模型就玩不转。

避坑指南:我曾经在调试一个I2C设备时,发现驱动死活不probe。查了半天,原来是设备树里status字段写成了"disable"而不是"disabled"。设备树语法检查很宽松,但写错了它也不会报错,只是默默忽略。所以,写完设备树一定要用dtc工具检查语法。

1.6 本章小结

这一章我们聊了三个核心概念:

  • BSP:板级支持包,是硬件和操作系统之间的桥梁
  • 设备树起源:为了解决ARM架构板级代码泛滥的问题
  • 设备树的作用:作为硬件信息的数据库,驱动内核的设备模型

说白了,设备树就是让硬件描述和驱动代码分家。你改硬件配置,不用改C代码,改改.dts文件就行。这个思路,在大型项目中特别有用。我参与过的项目,一块SoC用在十几种板子上,每个板子就一个.dts文件,维护起来轻松多了。

下一章,我们会深入设备树的语法细节。到时候我会拿实际项目中的设备树文件来拆解,看看每个节点、每个属性到底是什么意思。

可可精品资料库