【无标题】
零基础讲解 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 目录里能看到解开的源码、配置和中间产物,最适合围观。但直接在里面改文件,下次清理或重建,改动直接蒸发。正确姿势是:
- 用日志和临时实验定位问题(这时候随便改
tmp,反正会重建); - 想清楚这个修改属于哪一层;
- 用 Recipe、
.bbappend、配置片段或补丁把它固化; - 重新构建验证;
- 提交,写清楚为什么改。
金句:会改文件,只是让这一次构建通过;会把修改放进正确的层级,才叫在维护 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 里是什么 → 内核解析到什么 → 驱动执行没有 → 硬件响应没有。
实验三:故意把一盏灯从图纸上划掉
找一块有完整备份、可恢复的实验板(重要!),挑个不致命的外设:
- 备份原始镜像和 DTB;
- 把某个非关键节点从
okay改成disabled; - 走正规构建流程重新生成、部署 DTB;
- 重启,用实验二的命令观察:设备没了、驱动日志变了;
- 恢复备份,再验一遍。
同一内核、同一驱动,只改图纸,设备就凭空消失——设备树是"描述"而非"驱动"的最好证明,比背十遍定义都管用。
实验四:驱动在源码树里 ≠ 在板上跑着
内核里一个功能的"在不在",隔着六道门:
① 配置启用了吗(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 fs | root= 参数、存储驱动、分区、文件系统 |
| 挂载成功但进不了 Shell | init、动态链接器、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 的理解会从"让板子亮起来"升级为"让硬件平台可构建、可验证、可维护、可交付"。
十三、总结:全家福
全文伏笔回收,一个都不欠:
| 坑 | 问题 | 答案在哪 |
|---|---|---|
| #1 | BSP 到底长什么样? | 第八章:在 Yocto 里是一组 Layer,不是压缩包 |
| #2 | BSP 边界谁说了算? | 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,就是沿着启动链和设备链路,把每一处软件假设和硬件事实逐项对齐。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)