零基础讲解 BSP——陪一块开发板从毛坯住进精装房

导读:本文从一个最朴素的问题出发:同样是 Linux,为什么一块板子能启动的镜像,换块板子就黑屏、没网、没声音,甚至彻底装死?全程只用一个比喻贯穿——把开发板当成一套毛坯房,把通用 Linux 当成物业管理系统,把 BSP 当成让这套房子通水、通电、交付入住的一整套装修工程资料。然后照着"工具 → 设计模式 → 设计哲学 → 历史演进 → 408 底层内功"的剥洋葱路线,一层层挖下去。全文按纯小白的阅读体验来写:会一点 C、能在命令行里 cd 和 ls,就够了。

〇、写在前面:我被 “BSP” 这个词伏击了

最近博主在学习嵌入式 AI 和 Jetson,想认真搞 Yocto,结果反复出现BSP:

  • 芯片厂商官网说:“请先下载我们的 BSP。”
  • 内核工程师说:“这个驱动还没进 BSP。”
  • Yocto 教程说:“把配置放进你的 BSP Layer。”
  • 硬件工程师说:“这块板换了颗 PHY,BSP 得改。”

如果你也是这个状态,别慌,这篇笔记就是为你写的。老规矩,先把全文最重要的结论压在这儿,后面所有内容都围着它转:

BSP(Board Support Package,板级支持包):让一个"通用操作系统"在某块"具体电路板"上完成启动、认识硬件、驱动硬件、稳定运行,所需要的全部板级软件、配置、固件和适配信息。

划重点:关键词不是 “Package(包)”,而是 “Board Support(支持这块板)”。它不一定是一个压缩包,里面也不一定有叫 bsp.bin 的文件——它更像一整卷装修工程档案。

它到底以什么形态存在?我先卖个关子,等讲到 Yocto 那一章再揭晓(**本文埋的第一个坑)。

这里做个类比:

  • Linux 内核是一套非常能干的通用物业管理系统:管理 CPU、内存、进程、文件、设备,样样精通——但它只懂"怎么当物业",不懂"你家在哪"。
  • 你的开发板是一套毛坯房:主体结构完好,但没有任何一届物业知道你家电闸在哪、水管怎么走、门禁用哪把钥匙。
  • BSP,就是让这套房子从"能看"变成"能住"的全部装修工程资料:户型图、电路改造单、设备清单、施工工序,一样都不能少。

框架搭好了,开工。

一、登场人物表

读长篇小说前先翻一遍人物表,是防迷路的好习惯。这场"装修大戏"的演员如下:

概念在戏里扮演职责
开发板一套毛坯房提供真实硬件
SoC楼体结构与预埋管线芯片里集成 CPU、GPU 和一堆控制器
通用 Linux 内核通用物业管理系统管 CPU、内存、进程、文件、设备
BSP本户型的装修工程方案让通用系统适配这块具体的板
Bootloader(如 U-Boot)交付前的工程队早期初始化 + 把内核搬进内存
设备树(Device Tree)竣工图与设备清单描述板上有什么、接在哪
驱动(Driver)各工种老师傅真正动手操作某类硬件
Firmware(固件)某些设备自带的专用说明书下载到设备内部跑的小程序
Rootfs(根文件系统)家具与日用工具Shell、库、服务和应用都住这
Yocto装配式装修工厂按配方自动获取、编译、组装一切
MACHINE户型编号为哪块板构建
DISTRO物业管理制度系统级发行策略
IMAGE交付清单最终镜像里装什么

现在看不懂完全没关系,先混个脸熟。每位角色正式出场时,你回头再看这张表,会越看越顺。

还要提前纠正一个常见误解:很多人以为 BSP 是应用和内核之间"薄薄的一层"。恰恰相反,它是一条纵向链路,从上电那一刻一直贯到系统交付:

Boot ROM 认识的启动介质(启动拨码怎么设、镜像放哪)
        │
        ▼
SPL / TF-A / U-Boot 等启动组件
        │
        ▼
Linux 内核配置、板级补丁、驱动
        │
        ▼
设备树 / 固件
        │
        ▼
机器配置、构建元数据、烧写与测试脚本

顺带说一句:Rootfs、SDK、示例程序算不算 BSP 的一部分?各家厂商约定并不统一(这本身就是个值得琢磨的问题,先埋个坑 ,第六章揭晓)。学的时候抓住"职责"就好,别急着分地盘。

二、灵魂拷问:同一个 Linux,凭什么不能通刷所有板子?

想象一个美好的世界:Linux 官方出一个万能镜像,下载、烧写、上电、起飞,所有开发板通吃。
但为什么这个世界不存在?

先做个小扫盲(已经懂的同学快速划过)。开发板的心脏是一颗 SoC(System on Chip,片上系统)——把 CPU、GPU、内存控制器、中断控制器,以及 UART(串口)、I²C、SPI、USB 这些控制器的"大脑",统统塞进一颗芯片。而**开发板(Board)**是拿这颗 SoC 当心脏,在外面焊上 DDR 内存颗粒、eMMC 存储芯片、电源管理芯片、网口 PHY、摄像头座子、音频编解码器……组成的一整块电路板。

SoC 是楼,而开发板是楼里那套具体的户型,一颗SoC可以装载在多种开发板上。

于是幻觉来了:“这颗 SoC 已经被 Linux 官方支持了,那所有用它的板子,是不是下载个镜像就能直接启动?”

想得美。 SoC 支持只解决"这栋楼的主体结构和预埋管线已被认识";BSP 要解决的是"你这套具体户型怎么接线、装哪些电器"。同一颗 SoC 的两块板子,可能差出这些花样:

硬件差异对软件的影响
DDR 颗粒型号、容量或布线不同Bootloader 的内存初始化参数不同
调试串口焊在不同引脚Pinmux 不改,你连启动日志都看不到
网口 PHY 换了颗芯片驱动、复位时序、MDIO 地址全要跟着换
eMMC / SD / SPI Flash 布局不同启动设备、分区表、烧写方式不同
摄像头接到不同的 MIPI CSI 接口设备树、时钟、供电、I²C 地址全变
电源管理芯片不同各路电压多少、上电先后顺序全变
有没有板载 Wi-Fi、GPU、NPU决定要不要额外的固件和用户态库

表里有些名词没见过?快速扫盲,全部一句话版本:

  • DDR:焊在板上的内存颗粒,PC"内存条"的贴片版;
  • eMMC:焊在板上的"硬盘",Flash 颗粒 + 控制器的合体;
  • 串口(UART):调试用的"对讲机",启动日志全从这吐出来,波特率是双方约好的语速;
  • Pinmux(引脚复用):一根引脚今天兼职串口、明天兼职 GPIO,兼职表由硬件决定,软件照抄;
  • PHY:网口上的物理层收发芯片,相当于网线这栋楼的水表井;MDIO 是 CPU 查岗 PHY 用的小总线;
  • PMIC:电源管理芯片,全板的配电箱;
  • MIPI CSI:摄像头的高速专用通道。

现在请记住这个反直觉的结论,它是理解 BSP 的第一把钥匙:

CPU 会执行 ARM 指令,只说明它"识字";但它天生不认识你的电路板——说明书(板级信息)得有人喂给它。

用毛坯房的话说:同一套物业系统可以服务一千个小区,但每个小区的电闸位置、门禁型号、水管走向都不一样。通用能力可以复用,入户接线没法偷懒。

再补一刀:BSP 的边界,各家厂商划得也不一样。 有的只给启动和内核适配;有的把示例 Rootfs、GPU/NPU 用户态库、交叉编译工具链、烧写脚本、测试程序统统塞进来,一起叫 BSP。所以拿到一份 BSP,别急着背定义,先问一句:“你这次到底交付了哪些东西?”

三、开工大吉:从按下电源键,到 Shell 亮起来

理论先行容易睡着,我们直接跟剧情走。把你的手指想象成按在开发板的电源键上——3、2、1,上电。

这一瞬间,这块板子是一间彻底的黑房子。 DDR 还没初始化,是一滩"没通电的混沌"——读写它就像对着一段没插电的内存条念咒语;Linux 内核还躺在 eMMC 某个分区里睡大觉;文件系统、驱动框架,统统不存在。谁来点火?

上电 / 复位
   │
   ▼
Boot ROM(开发商浇筑在混凝土里的代码)
   │  从 SD / eMMC / SPI Flash 里找下一棒
   ▼
SPL(先遣队:先把时钟和 DDR 搞定)
   │
   ▼
TF-A / OP-TEE(可选:安全世界的活,暂时路过)
   │
   ▼
U-Boot(工程队主力:初始化更多外设,搬大件)
   │  把内核、DTB、(可选的)initramfs 搬进内存
   ▼
Linux Kernel(物业进场:看竣工图,派工)
   │
   ▼
挂载 Rootfs(家具、工具进场)
   │
   ▼
启动 PID 1(systemd 或 BusyBox init,开荒管家)
   │
   ▼
Shell 提示符亮起——你可以敲命令了

一棒接一棒,每一棒的存在都因为同一件事:下一棒需要的环境,还没准备好。
第一棒:Boot ROM。 芯片出厂时固化在硅片内部的一小段代码,断电不丢、谁也改不了。它能力很有限,只会按启动引脚的电平(俗称"启动拨码")去 SD、eMMC、SPI Flash 这几个"约定好的抽屉"里翻下一棒程序。它是整个系统的信任起点——混凝土里预埋的东西,谈不上好用,但一定在。

第二棒:SPL(Secondary Program Loader)。 Boot ROM 翻出来的这段小代码,先遣队属性:把时钟树调好、把 DDR 训练好——通水、通电。干完这些,房子才勉强算"有基本居住条件"。

第三棒(可选):TF-A / OP-TEE。 现代平台常在这里处理安全启动、可信执行环境。小白阶段只需要知道它们存在、别被名词吓到,路过即可。

第四棒:U-Boot。 工程队主力进场。它的日常:初始化更多外设,从存储或网络把内核镜像、DTB(编译后的设备树)、可选的 initramfs(内核启动早期用的临时迷你根文件系统,可以没有)三件大件搬进内存的指定位置,设置好启动参数(根文件系统在哪、调试串口是哪个),最后把手一挥:

booti ${kernel_addr_r} ${ramdisk_addr_r} ${fdt_addr_r}

这条命令翻译成人话:“内核、initramfs、户型图都已就位,物业同志,钥匙和图纸给你,进场吧。”(没有 initramfs 的话,中间参数写 -;具体细节以你的平台文档为准——记住思想就行,别背命令。)

Jetson 同学看这里:如果你玩的是 Orin 系列(AGX Orin / Orin NX / Orin Nano),会发现启动器不是 U-Boot 而是 UEFI——NVIDIA 用一套基于 EDK2 的 UEFI 替了这一棒;更早的 Nano/Xavier 上则是 CBoot。别慌,换皮不换魂:不管叫 U-Boot、CBoot 还是 UEFI,这一棒的职责都一样——把下一棒搬进内存、递钥匙。另外,NVIDIA 给 Jetson 的整套板级软件就叫 L4T(Linux for Tegra),你在 JetPack 里下载的"BSP",官方口径就是 L4T——看到这个词别再愣住。

第五棒:Linux 内核。 物业进场,先搭自己的管理体系(内存管理、调度器、中断、驱动模型),然后做一件和 BSP 最相关的事:展开竣工图(设备树),挨个认设备,按图派工(匹配驱动)。这一步是第四章的主菜。

第六棒:Rootfs + PID 1。 内核跑起来不等于系统能用,还差家具:Shell、C 库、系统服务,全在根文件系统里。内核挂载它之后,启动第一个用户态进程(PID 1,systemd 或 BusyBox init),由它把整个用户空间拉起来。

两个新手经典翻车现场,提前打个预防针:

  • Unable to mount root fs:根文件系统没挂上。查存储驱动、分区、文件系统类型、root= 启动参数。
  • No working init found:挂是挂上了,但里面找不到 init。查 Rootfs 内容是不是烧对了。

四、竣工图:设备树——毛坯房不会自己报户口

现在回答一个关键问题:内核进场后,它怎么知道这块板上有几个 I²C 控制器、摄像头接在哪个口、那颗传感器的地址是多少?

你可能会想:让硬件自己报呗!可惜,毛坯房里的开关、插座、灯,不会主动跑到物业办公室报户口。

确实有"会报户口"的设备:PCI、USB 这类总线天生带枚举能力,插上去就按协议自报家门和资源,像刷工牌进门。但嵌入式 SoC 上大量设备挂在"平台总线"上——某个 I²C 控制器固定在哪个地址、用哪路中断、时钟从哪来、引脚复用成什么功能……这些信息硬件自己说不出来,必须有人把事实写成文档喂给内核。这份文档,就是设备树(Device Tree)。

先钉死一句话,这是学设备树最容易歪的地方:

设备树是"硬件描述",不是"驱动程序"。它告诉内核"有什么硬件、资源在哪",但不负责"怎么一步步驱动它"。

图纸归图纸,干活归干活。

4.1 图纸也分总图和户型图

设备树源文件分两层:SoC 级 .dtsi(楼栋通用图纸:描述这颗芯片内部有哪些 UART、I²C、SPI、中断控制器)和板级 .dts(你这套户型:启用了哪些控制器、外面接了什么器件、用什么引脚和供电)。

看一段典型写法(示意,具体节点名以平台文档为准):

&i2c1 {                          /* 引用 SoC 图纸里的 1 号 I²C 控制器 */
    status = "okay";             /* 启用它 */
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_i2c1>; /* 这几根引脚复用成 I²C 功能 */
    clock-frequency = <100000>;   /* 总线跑 100 kHz */

    demo_sensor@50 {              /* 总线上住着个地址 0x50 的传感器 */
        compatible = "acme,demo-sensor";
        reg = <0x50>;
        status = "okay";
    };
};

翻译成人话:“1 号 I²C 总线,启用,引脚用默认组,速率 100k;这条线上住着一个地址 0x50 的传感器,归 acme,demo-sensor 这位师傅管。”

而"这位师傅"——驱动侧——是这样挂牌营业的:

static const struct of_device_id demo_of_match[] = {
    { .compatible = "acme,demo-sensor" },   /* 我接 acme 家的 demo-sensor */
    { }
};
MODULE_DEVICE_TABLE(of, demo_of_match);

两边通过 compatible 这个"工种编号"对上暗号,内核的撮合流程跑起来:

设备树节点里的 compatible
        │
        ▼
和驱动注册的匹配表比对
        │
        ▼
对上了 → 调用驱动的 probe()(师傅上门)
        │
        ▼
从图纸领资源:寄存器地址、中断、时钟、复位、GPIO、DMA
        │
        ▼
初始化硬件,向内核注册设备能力(/dev/xxx 就绪)

图纸描述"有什么",驱动负责"怎么操作",内核负责"组织撮合"——BSP 最核心的三角关系,就是这三位。

(一个内核凭什么能跑在成千上万块不同的板子上?靠的就是"驱动通用 + 图纸描述差异"这个分工。它背后的设计思想第六章揭晓。)

4.2 坑王之王:status = "okay" 不等于"能用了"

无数新手栽在这:把 disabled 改成 okay,烧板,重启,设备纹丝不动,当场怀疑人生。

其实 okay 只表示"启用这个节点",相当于竣工图上把这盏灯从草稿改成正式。但图纸写"这里有一盏灯",不代表灯会亮——电线拉了没、开关接了没、灯泡拧了没、电工派单了没,一样都不能少。翻译成技术语言,一个设备真正跑起来至少要同时满足:

  • 总线控制器已启用;
  • 驱动已编译进内核,或作为模块加载了;
  • 引脚复用(Pinmux)正确;
  • 时钟、电源、复位时序正确;
  • 地址、中断号和原理图一致;
  • 芯片本身没虚焊、没坏。

(至于"源码树里明明有这个驱动、板上就是一动不动"这种悬案,我们留到第九章实验四专门断案)

4.3 第二个坑:改了 DTS,不等于生效了

.dts(文本源文件)要经过 DTC 编译成 .dtb(二进制),由 Bootloader 加载进内存、交给内核,才算真正投入使用。所以你半夜改的那行 DTS,中间隔着一条完整流水线:

改源码 → 重新构建 → 生成新 DTB → 部署到正确分区 → Bootloader 加载的确实是它

任何一环掉链子,你的修改都是无效操作。上板可以在系统里直接验证"内核正在用的图纸":

cat /proc/cmdline                              # 内核启动参数(U-Boot 递的条子)
ls /sys/firmware/devicetree/base               # 运行中的设备树
dmesg | grep -Ei 'i2c|spi|mmc|ethernet|probe'  # 设备注册与匹配日志

装了 dtc 工具的话,还能把运行中的设备树导出来看:

dtc -I fs -O dts /sys/firmware/devicetree/base -o /tmp/running.dts

补一刀(compatible 的命名规矩):"厂商,设备",中间一个英文逗号——"acme,demo-sensor"、"nxp,pca9555"。前半段是公司或组织,后半段是具体型号,全球唯一不打架——比城市里到处重名的"建设路"文明多了。

五、老师傅进场:驱动是怎么照着图纸干活的

图纸说清了"有什么",接下来看老师傅们"怎么干"。驱动的世界里,翻来覆去就三件看家本事:按面板(MMIO)、听门铃(中断)、雇搬运工(DMA)。

5.1 MMIO:按设备的控制面板

嵌入式外设大多把自己的控制寄存器映射到 CPU 的物理地址空间里,叫 MMIO(Memory-Mapped I/O,内存映射 I/O)——就是内存管理里说的"统一编址":CPU 眼里只有门牌号,总线自动路由。通俗讲,每个外设面板上有一排按钮和仪表盘,每个按钮都有门牌号,CPU 读写这些门牌号,就是按按钮、看仪表盘:

  • 往"控制寄存器"写 1:启动发送;
  • 读"状态寄存器":看看数据到没到;
  • 往"数据寄存器"写字节:真的发出去。

设备树里 reg 描述的,就是这类寄存器区间的门牌号。驱动在 probe() 里领资源的姿势大致是:

res  = platform_get_resource(pdev, IORESOURCE_MEM, 0);  /* 按图纸领地址 */
base = devm_ioremap_resource(dev, res);                 /* 映射成可访问的虚拟地址 */
writel(1, base + CTRL_REG);                             /* 按下"启动"按钮 */
val  = readl(base + STATUS_REG);                        /* 看一眼仪表盘 */

但千万别把它当普通变量。硬件寄存器有"副作用":有的读一下就清零,有的必须按特定宽度访问,有的写完要内存屏障。这是 C 语言思维和硬件思维的分水岭。

5.2 中断:设备办完事,主动按你家门铃

如果 CPU 不停读状态寄存器问"好了没?好了没?"——这叫轮询,简单粗暴,但 CPU 全耗在盯梢上,白瞎了这颗法拉利。更优雅的方案是中断:设备干完活,通过中断控制器按门铃,CPU 放下手里的活、保存现场、开门处理、完事继续。

工程上为了不让"开门"这个动作堵住整栋楼,惯用上半部/下半部分工:

  • 上半部(硬中断):只做最紧急的——确认事件、读关键状态,几微秒内完事;
  • 下半部(线程化中断 / 工作队列):耗时的搬运和加工,退到后台慢慢干;
  • 用户进程通过 poll/epoll 睡着等结果,事件到了内核再叫醒它。

这里藏着一个直通 408 的深水区问题,先埋个雷:为什么中断处理函数里不能睡觉?(答案在第七章,和"中断上下文根本不是进程"有关。)

5.3 DMA:大块数据,别让 CPU 亲自扛

摄像头一帧好几 MB、网卡一秒几十万个包——全靠 CPU 一个字节一个字节搬的话,法拉利天天干搬运工的活,算力全烧在搬砖上。**DMA(Direct Memory Access,直接存储器访问)**就是雇来的专职搬运工:CPU 只下个单"把 A 处的 N 字节搬到 B 处",DMA 自己成块地搬(还能配突发传输,一趟顺一串),搬完发个中断:“货齐了,验收。”

(如果你读过博主那篇《在做菜中学习内存管理》,这位搬运工是老熟人了——顺便,Cache 一致性这个老坑在 BSP 里同样埋着:DMA 把新数据写进 RAM,CPU 的 Cache 里还囤着旧副本,CPU 一查"命中",端出来的却是隔夜菜。所以驱动里要用正经的 DMA API 管理缓冲区,而不是凭感觉"刷一下 Cache"。)

5.4 deferred probe:师傅在楼下等电梯,不是跑路了

看内核日志常见这个词,翻译一下:某驱动的 probe() 被推迟了——因为它依赖的时钟、电源或别的驱动还没就位。不是失败,是排队,内核稍后会重试。看到它别急着改代码,先查依赖链。

到这里,启动链、图纸、师傅三件套齐了。是时候把洋葱拿出来了。

六、剥洋葱时间:从"会用"到"看穿"

前面五章是"工具层"的 BSP。但学技术如果只停在工具,名词背一堆,换个平台就抓瞎。现在把 BSP 从四个剖面重新看一遍:设计模式(章法)→ 设计哲学(价值观)→ 历史演进(来路)→ 408 内功(地基)。

6.1 第一层:设计模式——BSP 里的章法

① 适配器模式:万能插座。 Linux 用统一的 V4L2 管摄像头、ALSA 管音频、网络设备接口管网卡、I²C/SPI 框架管总线设备。可各家芯片的寄存器八仙过海。怎么办?让驱动去当适配器:上层接口稳定,硬件差异全部压到适配层。这就是可移植性的来源——变化关进笼子,稳定留给上层。

② 插件与注册:工种名录。 驱动不是被内核写死调用的,而是自己注册进总线框架、声明"我接这些设备",内核碰到匹配设备才喊它。物业不用把全小区的维修步骤写进总手册,只需维护一张工种名录:电路找电工、水路找水工。

③ 数据驱动:把"差异"从代码搬进数据 早年间,地址、中断、引脚全硬编码在 C 文件里,板子一多,内核里堆满"长得很像、只是数字不同"的板级文件。设备树把"这块板的事实"抽成声明式数据,驱动只保留通用算法。于是——一份通用内核 + 每板一张图纸,适配成千上万块板。这就是第四章那个坑的答案:不是内核神,是"算法"和"接线事实"分了家、各自安放。

④ 分层与组合:不复制整座工厂。 Yocto 的 Layer 和 .bbappend(第八章主角)是同一思路:不复制别人的整套配方,只叠加一张"差异补充单"。上游一升级,你合并起来就轻松——因为你只拥有"你和别人不一样的那部分"。

6.2 第二层:设计哲学——BSP 的价值观

① 机制与策略分离 Linux 内核提供机制(调度、内存、驱动模型、协议栈——“怎么做”),BSP 提供策略输入(这块板要什么、怎么接——“用什么”)。判断某内容"该不该进 BSP",就问一句:它描述的是"这块板的事实",还是"这个产品的偏好"?前者进 BSP,后者进 Distro / Image 层。混在一起的短期收益是"能编过",长期代价是没人敢动的巨石。

② 可移植性 = 差异"有地方安放"。 可移植不是"写一份神奇代码自动适配所有板"——那种代码不存在。而是:通用代码尽量复用、必要差异集中表达、上层接口保持稳定、构建结果可以复现。优秀的 BSP 不是没有板级代码,而是每段板级代码都知道自己该住哪:进内核?进设备树?进 Bootloader?还是压根不该进 BSP?

③ 启动链 = 逐层建立世界 为什么上电不直接跳内核?因为复杂环境不是天上掉下来的,是逐层搭起来的:Boot ROM 是出厂可信起点 → SPL 把时钟和 DDR 立起来 → U-Boot 拿到更舒服的环境,干加载和诊断 → 内核建立完整的资源管理 → 用户空间提供服务。每一棒只假设"上一棒已就绪",每一棒只为下一棒"创造前提"。这个思想你以后会在操作系统启动、分布式系统初始化、甚至接手烂尾项目时反复遇到。

④ 可复现构建:把"老师傅手感"变成"施工规范"。 一台机器上偶然编出的镜像,不等于可维护的 BSP。工程交付要能回答:源码从哪来、什么版本、打了哪些补丁、内核什么配置、固件哪个版本、镜像怎么烧、结果怎么验。Yocto 最大的价值之一,就是把"某位工程师记得怎么做"变成可审阅、可重建的元数据。

6.3 第三层:历史演进——它为什么长成这样

2000 年代,各家板厂往内核里塞自己的板级 C 文件,内容高度雷同、只是地址和中断不同;SoC 一颗接一颗地出,板级文件指数级堆积。2011 年,Linus 终于忍无可忍,在邮件列表公开吐槽 ARM 子系统混乱,甚至放话要把 ARM 这摊子扔出主线。这场风波直接推动了 ARM 板级代码大清洗,设备树正是在这个背景下全面上位——历史再次证明:把差异写进数据、把通用留给代码,是规模化的唯一出路。

由此你还要分清两条代码线:

  • Vendor BSP(厂商线):优先让某款芯片功能尽快可用,补丁多、含专有固件,交付快但维护累;
  • Mainline Linux(上游线):进官方主线、统一框架、长期维护,但新硬件支持未必立刻完整。

二者不是"山寨 vs 正统",而是交付速度与维护周期之间的工程权衡。做产品,先想清楚你站在哪条线上,别把两条线的内核、DTB、固件随手混搭。

七、第四层:BSP 指向的 408 内功

顺着 BSP 往下挖,会不可避免地撞上计算机考研 408 的四座大山。好消息是:BSP 是把 408 串成一条线的最佳现场之一。

7.1 操作系统:内核编程,没你想的那么玄

很多同学一听"内核编程"就腿软。其实第五章你已经看过内核代码了——那个 of_device_id 数组就是。内核编程还是 C 语言,只是规矩变了:你的代码跑在内核地址空间,没有 libc、不能随便睡觉、并发随处可见、写崩了整机 Oops。而驱动,恰好是内核编程里门槛最低、反馈最快的那扇门——改一处、编模块、插上板、看日志,循环极短。

  • 系统调用:应用怎么跟驱动说话?open()/read()/write()/ioctl(),用户态一敲门,陷入内核执行——这是用户态与内核的会面礼;
  • 进程与 IPC:两个进程想共享摄像头数据,靠内核在中间管账——你遇到的每个 /dev 节点背后,都站着进程与内核的协作。以后学管道、共享内存、信号量这些进程通信手段,嵌入式里最常见的落地现场之一就在这;
  • 中断与并发(填上第五章的雷):中断上下文之所以不能睡觉,是因为它不是进程——没有可调度的实体,没有"醒来再说"的资格,一睡等于把调度器晾在原地。所以中断里只干快活,慢活交给下半部;
  • 虚拟内存:驱动的 ioremap、用户态的 mmap,背后都是页表和地址映射。

7.2 计算机组成原理:总线和时序的物理现实

  • 寄存器地址写错,为什么可能挂总线?(地址译码、总线仲裁)
  • DDR 初始化失败,后面为什么无从谈起?(内存子系统是一切的地基)
  • DMA 为什么有 Cache 一致性问题?(存储层级)
  • RGMII 千兆网口为什么要调时钟延迟?(高速信号时序)

"驱动调不通"很多时候不是 C 语言问题,而是软件描述与硬件时序对不上。

7.3 数据结构与算法:图纸上和工厂里的结构

  • 设备树本身就是棵树(总线上挂设备,树套树);
  • Yocto/BitBake 把所有任务建成有向无环图(DAG),决定先做谁、后做谁、谁能并行——所以你改一个底层 Recipe 会触发大面积重建,不是 BitBake 折磨你,是依赖它的所有人都得重做;
  • 网络、音频、块设备里全是环形缓冲区和队列。

7.4 计算机网络:"板子没网"是一整条链路

"开发板没网"是句废话式的抱怨,实际链路是:

RJ45 网口 → PHY(物理层收发器)→ SoC 的 MAC 控制器
   → Linux 网络驱动(DMA + 描述符环 + NAPI)→ 协议栈
   → IP / 路由 / DNS → Socket → 你的应用

BSP 管前半段(PHY 型号、复位 GPIO、MDIO 地址、设备树、驱动);ping 不通还得查后半段。分层排查,别一句"BSP 没网"全打包。

八、装修工厂:BSP 在 Yocto 里到底长什么样(填坑 #1)

是时候揭晓第一个坑了。先纠正一句流传很广的错误说法:"Yocto 是一个 Linux 发行版。"更准确地说:Yocto Project 提供构建嵌入式 Linux 所需的工具、元数据和参考组件;OpenEmbedded 是其中的构建框架,BitBake 按配方执行任务,Poky 是参考发行版。你用它构建属于你自己的系统,而不是下载一个现成桌面系统来装。

在这个世界里,BSP 不是一个压缩包,而是一个(或一组)Layer。要理解这句话,先把几个名词一次认齐:

名词回答的问题毛坯房比喻
BitBake谁按依赖调度所有任务?工程调度中心
Recipe(.bb)某个软件怎么获取、编译、安装?一道工序的施工卡
Layer一组可复用的 Recipe + 配置 + 补丁一册分类施工资料
.bbappend不改原配方,怎么追加修改?对原施工卡的补充单
MACHINE为哪块板构建?户型编号
DISTRO用什么发行策略?物业管理制度
Image Recipe最终镜像里装什么?交付清单

BSP Layer 重点回答 MACHINE 相关的问题:默认用哪个内核和 Bootloader、哪些内核配置和补丁、哪份设备树、要哪些固件、串口和存储怎么配。一个典型的 BSP Layer 长这样:

meta-demo-bsp/
├── conf/
│   ├── layer.conf                    # 本层的自我介绍
│   └── machine/
│       └── demo-board.conf           # 户型编号的档案
├── recipes-bsp/
│   └── u-boot/
│       ├── u-boot_%.bbappend          # 给 U-Boot 施工卡打补充单
│       └── u-boot/
│           ├── demo-board.cfg         # 配置片段
│           └── 0001-board-fix.patch   # 板级补丁
└── recipes-kernel/
    └── linux/
        ├── linux-yocto_%.bbappend     # 给内核施工卡打补充单
        └── linux-yocto/
            ├── demo-board.cfg         # 内核配置片段
            ├── demo-board.dts          # 本户型竣工图
            └── 0001-add-demo-board.patch

户型档案(machine conf)里写着这样的内容:

PREFERRED_PROVIDER_virtual/kernel ?= "linux-yocto"   # 内核用哪家
KERNEL_DEVICETREE = "vendor/demo-board.dtb"          # 用哪张竣工图
SERIAL_CONSOLES = "115200;ttyAMA0"                   # 调试串口和波特率

补充单(.bbappend)的核心动作是"在我这层补充差异,不动你的原件":

FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"
SRC_URI:append = " file://demo-board.cfg"
SRC_URI:append = " file://0001-board-fix.patch"

这就是 6.1 节"分层与组合"的落地:你只拥有你和别人不一样的那部分。

8.1 一次构建,工厂里发生了什么

读取 Layer 与配置
      ▼
确定 MACHINE / DISTRO / IMAGE / 软件版本
      ▼
BitBake 解析所有 Recipe,建任务依赖图(DAG)
      ▼
fetch → unpack → patch → configure → compile → install
      ▼
package → 组装 Rootfs、内核、DTB、Bootloader、完整镜像
      ▼
产物落在 tmp/deploy/images/<machine>/

这张流程图还是你的排障地图:下载失败查 fetch 和网络、打补丁失败查 patch 和源码版本、功能没生效查 configure 和最终配置、编译报错查 compile 日志、文件没进镜像查 install 和 Image 配置、板子不启动查部署和烧写。按工序定位问题,别一把梭重跑。

Jetson 同学再看一眼:社区给 Jetson 维护的 Yocto 层叫 meta-tegra(OE4T 社区出品);NVIDIA 官方 L4T / JetPack 发布包,就是 NVIDIA 替你整理好的"厂商 BSP 全家桶"。现在这两句话说的是什么,你已经有概念了。

8.2 最重要的工程纪律:别直接改 tmp

tmp 目录里能看到解开的源码、配置和中间产物,最适合围观。但直接在里面改文件,下次清理或重建,改动直接蒸发。正确姿势是:

  1. 用日志和临时实验定位问题(这时候随便改 tmp,反正会重建);
  2. 想清楚这个修改属于哪一层;
  3. 用 Recipe、.bbappend、配置片段或补丁把它固化;
  4. 重新构建验证;
  5. 提交,写清楚为什么改。

金句:会改文件,只是让这一次构建通过;会把修改放进正确的层级,才叫在维护 BSP。

九、验房实测:别光听我说,跑一遍

理论讲得再热闹,不如亲手看一眼。四个实验,难度递增。

实验一:先用现成户型练手

在配好的 Yocto 环境里:

bitbake-layers show-layers
bitbake -e core-image-minimal | grep -E '^(MACHINE|DISTRO)='

然后挑一个当前版本官方支持的 MACHINE,构建最小镜像:

MACHINE=<官方支持的机器名> bitbake core-image-minimal
ls tmp/deploy/images/<机器名>/

去产物目录玩"认物游戏":哪个是内核、哪个是 DTB、哪个是 Bootloader、哪个是完整磁盘镜像、哪个是 Rootfs?不同板子的命名和格式不一样,别背扩展名,认内容(文件名里通常带 Image、dtb、rootfs 这些字样)。也可以先用 QEMU 跑起来看启动日志——但要清醒:QEMU 证明你会构建,不等于真实板卡的 BSP 完成,真板子还有时序、原理图、外部器件的坑。

实验二:在板子上找设备树的证据

进板子 Shell 后:

cat /proc/cmdline                  # U-Boot 递给内核的条子
ls /sys/firmware/devicetree/base  # 内核正在用的竣工图
cat /proc/interrupts              # 谁在按门铃、按了几次
dmesg | grep -Ei 'probe|defer'    # 师傅上门记录 + 等电梯名单

把看到的内容和源码、原理图对照:节点在吗?地址对吗?驱动匹配了吗?中断计数随设备活动变吗?这一步在建立证据链:源码写了什么 → DTB 里是什么 → 内核解析到什么 → 驱动执行没有 → 硬件响应没有。

实验三:故意把一盏灯从图纸上划掉

找一块有完整备份、可恢复的实验板(重要!),挑个不致命的外设:

  1. 备份原始镜像和 DTB;
  2. 把某个非关键节点从 okay 改成 disabled;
  3. 走正规构建流程重新生成、部署 DTB;
  4. 重启,用实验二的命令观察:设备没了、驱动日志变了;
  5. 恢复备份,再验一遍。

同一内核、同一驱动,只改图纸,设备就凭空消失——设备树是"描述"而非"驱动"的最好证明,比背十遍定义都管用。

实验四:驱动在源码树里 ≠ 在板上跑着

内核里一个功能的"在不在",隔着六道门:

① 配置启用了吗(y 编进内核 / m 编成模块 / 没开)
      ▼
② 编译进镜像或模块了吗
      ▼
③ 打包进 Rootfs 了吗(模块的话)
      ▼
④ 部署到板子上了吗
      ▼
⑤ 加载了吗(y 自带,m 要 modprobe)
      ▼
⑥ probe 成功了吗(设备树还得匹配上)

所以"我在源码里明明看到这个驱动了",在证据学上一文不值——六道门全过才算数。lsmod 看模块、dmesg 看 probe 记录、/dev 和 /sys 看设备,一处处对。

十、万一黑屏:Bring-up 是一门证据学

Bring-up(板卡点亮)= 让一块新板从"只有硬件"走到"系统稳定"的全过程。黑屏时刻,最忌讳的是像无头苍蝇一样乱刷镜像。正确姿势是沿着启动链,一段一段建立事实。

先核对最小事实:供电电压对吗?启动拨码(选从哪个介质启动)对吗?串口线 TX/RX 没接反吧?波特率对吗?烧的镜像确实是这块板的吗?手边有已知能启动的对照镜像吗?

然后按症状定位:

症状优先排查
串口一个字都不吐供电、时钟、复位、启动拨码、Boot ROM 认不认这个介质、串口接线
Bootloader 有输出但 DDR 报错DDR 参数、训练、早期初始化
能加载内核,之后无输出内核格式与加载地址、DTB、串口控制台参数
内核起来了但某外设失踪设备树 status、地址、Pinmux、时钟、电源、驱动配置
一串 deferred probe依赖的时钟 / 电源 / 复位 / 其他驱动是否就绪
Unable to mount root fsroot= 参数、存储驱动、分区、文件系统
挂载成功但进不了 Shellinit、动态链接器、C 库、Rootfs 内容
网口没链路PHY、复位、MDIO、RGMII 时钟——再查 IP 配置

最后一条铁律:一次只改一个变量。 同时换内核、DTB、Rootfs、启动参数,就算最后亮了,你也不知道是哪一下救的;不亮更是无头案。保留可用基线、单变量替换、保存日志、记录版本——这就是 BSP 工程与玄学刷机的分界线。

十一、八个高频灵魂拷问(快问快答)

Q1:BSP 就是驱动合集吗?
不是。驱动只是工种。BSP 还包括 Bootloader、设备树、机器配置、内核配置、固件、构建元数据、烧写与测试内容。

Q2:BSP 就是 Linux 内核吗?
不是。内核是通用物业系统,BSP 是让物业适配你这套房的工程档案。厂商 BSP 里可能"包含"一个内核源码树,但概念上不能画等号。

Q3:设备树就是驱动吗?
不是。图纸描述事实,驱动实现操作。只有图纸没有驱动,设备照样躺着不动。

Q4:有 BSP 就一定有完整 Rootfs 吗?
不一定。核心是板级适配;Rootfs 装什么由 Distro 和 Image 决定。厂商有时顺手打包示例 Rootfs,别当成义务。

Q5:Yocto 就是 BSP 吗?
不是。Yocto 是构建体系(装修工厂),BSP 是被构建的板级内容(工程档案)。工厂里可以有 BSP Layer,也可以有 Distro Layer、软件 Layer、应用 Layer。

Q6:BSP 和 HAL(硬件抽象层)啥关系?
思想同源(都为屏蔽差异),范围不同。HAL 偏向"一层接口",嵌入式 Linux 的 BSP 是横跨启动、内核、设备树、固件、构建系统的交付概念,比 HAL 大得多。

Q7:能进 Shell,BSP 就算完事了?
早着呢。摄像头、显示、音频、网络、休眠唤醒、稳定性、OTA 升级、安全启动、生产测试……"亮机"只是万里长征第一步。

Q8:学 BSP 必须先啃完整个 Linux 内核?
不必须,也不现实。正确路径是沿真实问题向下钻:跑通现成板 → 跟一次完整启动 → 盯一个设备节点 → 跟一次 probe() → 跟一次构建。在问题中补内核和硬件知识,比按目录通读内核高效一百倍。

十二、给小白的路线图:沿证据链学,别沿名词表背

阶段一:建立最小世界观。 C 语言、基本 Linux 命令、交叉编译概念,以及 CPU / RAM / Flash / 总线 / 中断各自是干嘛的。目标:能看懂启动日志,不再把一切问题统称"系统坏了"。

阶段二:跑通一个现成 BSP。 挑文档全、串口日志正常、烧写可恢复的板子(玩 Jetson 的同学,官方 JetPack + L4T 起步最省心),全程不改任何东西:构建(或下载)→ 认产物 → 烧写 → 启动 → 看设备树和日志 → 改一个低风险配置再验。韦东山这类重动手的中文教程适合铺主线;B 站视频挑那些讲原理图和启动链的,跳过纯"照着敲命令"的。

阶段三:集中攻克三大件。 U-Boot(内核放哪、怎么加载、参数怎么传)、设备树 + 驱动模型(compatible 怎么匹配、probe() 怎么拿资源)、Yocto 分层构建(怎么找到最终生效的 Recipe、怎么打补丁、怎么部署产物)。官方文档优先:Yocto Project 文档、内核设备树文档、U-Boot 文档;具体平台再查 SoC 手册、原理图和对应 BSP Layer。想更系统地补内核内功,可以玩 xv6 / ucore 这类教学操作系统——几百行代码实现一个迷你内核,比通读 Linux 舒服太多。

阶段四:产品化能力。 启动时间优化、稳定性与压测、OTA 与 A/B 分区、Secure Boot、上游化与许可合规。到这一步,你对 BSP 的理解会从"让板子亮起来"升级为"让硬件平台可构建、可验证、可维护、可交付"。

十三、总结:全家福

全文伏笔回收,一个都不欠:

坑问题答案在哪
#1BSP 到底长什么样?第八章:在 Yocto 里是一组 Layer,不是压缩包
#2BSP 边界谁说了算?6.2:机制与策略分离——板级事实进 BSP,产品偏好出去
#3启动为啥分这么多棒?6.2:复杂环境逐层搭建,每棒只为下棒创造前提
#4一个内核凭啥适配万千板?6.1:数据驱动,差异进图纸、算法留代码
#5驱动在树里 ≠ 在跑?实验四:配置→编译→打包→部署→加载→探测,六道门

最后,把毛坯房的比喻收拢成一张全家福:

问题对应组件一句话
上电谁先跑?Boot ROM / SPL / U-Boot当前启动链走到哪一棒了?
内核怎么知道板上有啥?设备树硬件事实描述对了吗?
谁真正动寄存器?驱动匹配并 probe 成功了吗?
大块数据谁搬?DMA映射和 Cache 一致性对了吗?
系统里跑哪些程序?Rootfs / init用户空间完整吗?
为哪块板构建?MACHINE / BSP Layer户型选对了吗?
系统策略谁定?DISTRO别和户型问题混为一谈
镜像里装什么?Image Recipe软件真的进交付物了吗?
怎么重复得到同一结果?Yocto / Recipe / 补丁修改固化且可复现了吗?

如果全文只让你带走一句话,那就带走这句:

BSP 不是某个神秘文件,而是让"通用软件"正确理解并驱动"这块具体硬件"的一整套工程实现——学 BSP,就是沿着启动链和设备链路,把每一处软件假设和硬件事实逐项对齐。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐