嵌入式 Linux 与 Yocto 构建

本文系统讲解嵌入式 Linux 与 Yocto 构建的工程落地,回答什么时候该上 Linux、引导链怎么串、Yocto 的层与配方怎么组织、设备树怎么改、根文件系统如何做只读与 A/B 更新、长期维护怎么跟 CVE 等实战问题。覆盖 U-Boot 与 FIT、BitBake 与 bbappend、systemd 启动优化,附权衡取舍与常见坑清单。

引言

当物联网设备从「一个传感器节点」升级为「一个带屏、跑协议转换、做边缘推理、还能远程运维的网关」时,裸机或 RTOS 就不再合适了。你需要进程隔离、文件系统、网络协议栈、包管理、多任务调度,以及一个能跑容器和 Python 的运行环境——这些正是 Linux 的强项。嵌入式 Linux 与服务器 Linux 的差别不在内核本身,而在「构建」:你不能用发行版预编译的包,必须为自己的板子定制一套完整镜像。

定制的复杂度集中在一个问题上:一个可启动的 Linux 系统由引导固件、内核、设备树、根文件系统四部分组成,它们各自有版本,且必须互相匹配。手工维护这套东西(下载交叉工具链、编内核、拼 rootfs)在第一个项目还能忍,一旦要支持三个硬件版本、每月跟一次安全补丁,就会失控。Yocto 就是为解决这个失控问题而生的:用配方(recipe)描述每个组件怎么构建,用层(layer)组织定制,用 BitBake 做依赖解析与缓存,最终产出可复现的镜像。

本文按「选型判断 → 引导链 → U-Boot → BSP → Yocto 基础 → 层与配方 → 设备树 → 根文件系统 → systemd → 长期维护 → 与 RTOS 的边界 → 调试」的顺序展开。MCU 侧的开发见 ESP32 与 STM32 嵌入式开发 ,实时任务调度见 RTOS 与 FreeRTOS 任务调度 。

目录

  1. 什么时候该上嵌入式 Linux
  2. 引导链:从 ROM 到内核
  3. U-Boot 与启动参数
  4. BSP 与 SoC 支持
  5. Yocto 与 BitBake 基础
  6. 层、配方与 bbappend
  7. 设备树
  8. 根文件系统与只读设计
  9. systemd 与启动优化
  10. 长期维护与安全更新
  11. 与裸机、RTOS 的边界
  12. 调试手段
  13. 权衡取舍
  14. 常见坑清单
  15. 小结

1. 什么时候该上嵌入式 Linux

选 Linux 还是 RTOS,本质是问「这个产品需要通用计算环境吗」。下面这张表给出判断依据:

需求倾向 RTOS / 裸机倾向 Linux
实时性硬实时(微秒级)软实时(毫秒级)
内存几十 KB 到几百 KB64MB 起,通常 256MB 以上
功耗电池数年常供电或大电池
功能单一采集或控制多协议、多进程、文件系统
升级单固件 A/B全镜像 A/B 或包级更新
成本几元到几十元几十元到几百元

典型分界点:需要跑 TCP/IP 之上的多种协议(MQTT、HTTP、CoAP、数据库客户端)、需要文件系统与日志、需要跑容器或脚本语言、需要多进程隔离时,Linux 更划算;只是采样加通信、对功耗和成本敏感、需要确定性响应时,RTOS 更合适。

很多产品是异构的:一颗 Cortex-A 跑 Linux 做网关和界面,一颗 Cortex-M 跑 RTOS 做实时采集与控制,两者用 UART、SPI 或共享内存通信。这种「A 加 M」组合兼顾了通用计算与硬实时,代价是两颗芯片的固件要分别维护。

2. 引导链:从 ROM 到内核

嵌入式 Linux 的启动是一串接力,每一棒负责把下一棒加载到内存并跳过去。

上电
 └─ BootROM(SoC 固化代码)
     └─ SPL / TPL(一级引导,初始化 DDR)
         └─ U-Boot(二级引导,加载内核)
             └─ Linux Kernel(解压自解压镜像,挂载根文件系统)
                 └─ init(systemd / busybox init)

BootROM 是 SoC 厂商固化在芯片里的代码,不可改,它从固定的介质(eMMC、SD、SPI NOR)读第一级引导。SPL(Secondary Program Loader)很小(几十 KB),唯一职责是初始化 DDR 并把完整的 U-Boot 加载到内存——因为在 DDR 初始化之前,片上 SRAM 装不下 U-Boot。有些 SoC 用 TPL(Tertiary)再多一级,或者直接把 SPL 和 U-Boot 合成一个镜像。

关键工程点:引导链上每一级的「偏移」和「格式」都由 SoC 规定,写错偏移设备就是砖。所以量产烧录必须用厂商工具(如 i.MX 的 mfgtools、全志的 PhoenixSuit)或经过验证的脚本,不能手工 dd。另一个点是「启动介质选择」:多数 SoC 通过引脚或 eFuse 决定从 SD、eMMC 还是网络启动,量产板要固化从 eMMC 启动,并把 SD 作为救援通道。

3. U-Boot 与启动参数

U-Boot 是事实标准的二级引导,负责加载内核、传参、以及提供命令行做救援。

printenv                                      # 查看所有环境变量
setenv bootargs "console=ttyS0,115200 root=/dev/mmcblk0p2 rootwait rw"
setenv bootcmd "fatload mmc 0:1 ${kernel_addr_r} Image; booti ${kernel_addr_r} - ${fdt_addr_r}"
saveenv                                       # 保存环境变量到持久存储
boot                                          # 执行 bootcmd

几个核心概念:bootcmd 是自动启动时执行的命令序列,bootargs 是传给内核的启动参数(内核命令行),${kernel_addr_r}、${fdt_addr_r} 是内存地址变量。现代做法是用 FIT 镜像(Flattened Image Tree,.its 文件描述),把内核、设备树、ramdisk 打包成一个带签名与校验的镜像,U-Boot 一次加载并校验完整性——这是做安全启动的基础。

内核命令行(bootargs)决定根文件系统在哪、控制台在哪、以及一堆内核行为。常见参数:root= 指定根分区(/dev/mmcblk0p2 或 PARTUUID=),rootwait 等存储就绪,console= 指定串口控制台,ro/rw 指定读写模式,quiet 抑制日志。用 PARTUUID 而不是设备名更稳,因为设备枚举顺序可能变。

安全启动(Secure Boot)在 U-Boot 层做两件事:一是校验下一级镜像的签名(FIT 镜像带签名,U-Boot 用内置公钥验证),二是锁死环境变量(防止攻击者改 bootargs 挂载自己的 rootfs)。这一步是整条信任链的锚点,公钥的哈希要烧进 SoC 的 OTP/eFuse。

4. BSP 与 SoC 支持

BSP(Board Support Package)是「让 Linux 在特定板子上跑起来」的一整套东西:引导固件、内核补丁、设备树、驱动、以及构建配置。选 SoC 时 BSP 的成熟度比芯片参数更重要。

因素说明影响
上游支持内核主线是否有该 SoC 的支持有则长期维护省事
厂商 BSP 版本厂商提供的内核版本与补丁质量决定初始开发速度
文档完整度数据手册、引脚复用、参考设计决定调试难度
社区活跃度论坛、issue、第三方板决定踩坑成本
长期供货芯片生命周期承诺决定产品寿命

优先选「内核主线有支持」的 SoC(如 i.MX、Allwinner 的部分型号、TI AM335x、瑞芯微 RK 系列),因为上游支持意味着安全补丁能持续跟。纯靠厂商 BSP 的芯片,一旦厂商停止维护,内核就停在某个老版本,CVE 越积越多。

BSP 版本策略:锁定一个厂商 BSP 作为基线,把厂商补丁与自己的改动分层管理(Yocto 的层机制正好做这个)。升级 BSP 时先在一个分支上验证,再合并到产品分支,不要直接在产品线上跟厂商最新版。

5. Yocto 与 BitBake 基础

Yocto 是一套构建框架,BitBake 是它的构建引擎(类似 Make,但带依赖解析和任务调度)。核心概念:

概念含义例子
Recipe(配方)描述一个组件怎么构建busybox_1.36.bb
Layer(层)一组相关配方的集合meta-oe、meta-imx
Class(类)可复用的构建逻辑autotools.bbclass
Task(任务)配方内的构建步骤do_fetch、do_configure、do_compile
Image(镜像)最终产出的根文件系统core-image-minimal
DISTRO发行版配置poky、fsl-imx-xwayland
MACHINE目标硬件imx8mm-evk
source poky/oe-init-build-env build              # 初始化构建环境(每个终端会话一次)
bitbake core-image-minimal                       # 构建最小镜像
bitbake -c compile busybox                       # 构建某个具体配方
bitbake -c listtasks busybox                     # 查看某配方的可用任务
bitbake -e busybox | grep ^SRC_URI               # 查看配方的实际变量取值
devtool modify busybox                           # 用 devtool 修改并回写配方
devtool finish busybox meta-mylayer

BitBake 的两个重要机制是共享状态缓存(sstate-cache)和下载缓存(DL_DIR)。sstate 缓存让没改动的配方不重复构建,第一次全量构建可能几小时,之后增量几分钟。DL_DIR 存所有源码包,CI 上要持久化,否则每次都重新下载。这两个缓存是 Yocto 团队协作和 CI 提速的关键。

6. 层、配方与 bbappend

Yocto 的定制原则是「不改上游,只加层」。所有修改都放在自己的层里,通过 bbappend 覆盖或追加上游配方,这样升级上游时冲突最小。

meta-mylayer/
├── conf/
│   └── layer.conf                 声明层名、优先级、依赖
├── recipes-core/
│   └── images/
│       └── my-product-image.bb    自定义镜像
└── recipes-kernel/
    └── linux/
        └── linux-imx_%.bbappend   覆盖内核配方(% 匹配任意版本)

my-product-image.bb 示例:
require recipes-core/images/core-image-base.bb

IMAGE_INSTALL:append = " my-app mqtt-client openssh-sshd"
IMAGE_FEATURES:append = " ssh-server-openssh"
IMAGE_FEATURES:remove = "debug-tweaks"    关闭调试特性以缩小镜像

bbappend 的命名规则是 原配方名_版本.bbappend,用 % 通配版本可以跨版本生效。覆盖变量的三种语法:= 直接赋值,+=/:append 追加,=+/:prepend 前置。注意 :append 和 += 有微妙差别(前者在解析末期展开,后者立即展开),处理变量拼接时优先用 :append。

conf/layer.conf 里要声明 BBFILE_PRIORITY,优先级高的层覆盖低的。层依赖用 LAYERDEPENDS 声明,bitbake-layers 工具能检查层之间的依赖与冲突:

bitbake-layers show-layers          # 列出所有层与优先级
bitbake-layers show-recipes         # 列出所有配方与所在层
bitbake-layers create-layer meta-new # 创建新层骨架

7. 设备树

设备树(Device Tree)用数据描述硬件,让同一个内核二进制支持不同板子。它把「硬件长什么样」从内核代码里剥离出来,是 ARM 嵌入式 Linux 的标准做法。

// my-board.dts 片段:定义一个 I2C 上的传感器
&i2c2 {
    status = "okay";
    clock-frequency = <400000>;

    bme280: sensor@76 {
        compatible = "bosch,bme280";
        reg = <0x76>;
        vddd-supply = <&vdd_3v3>;
    };
};

&uart1 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_uart1>;
};

关键概念:compatible 是节点与驱动匹配的字符串(驱动里用 of_match_table 匹配它),reg 是设备地址,status = "okay" 启用节点(默认可能是 disabled)。设备树源文件分 .dts(板级)与 .dtsi(SoC 级,被 include)。编译用 dtc,产物是 .dtb,运行时内核把它展开成 /proc/device-tree。

设备树覆盖(Overlay)允许在基础设备树之上动态叠加改动,常用于「同一 SoC、不同扩展板」的产品线。改设备树时最容易犯的错是引脚复用(pinctrl)配错——引脚要么没配、要么与其他外设冲突,表现为设备探测不到或功能异常。调试时先看 dmesg 里驱动是否 probe 成功,再对照原理图核对 pinctrl 与 reg。

8. 根文件系统与只读设计

根文件系统(rootfs)是设备运行时的一切。生产环境的核心原则是根分区只读,把可变数据放到独立分区。

分区布局示例(eMMC):
  p1  boot      64MB   内核、设备树、U-Boot 环境(可写但很少动)
  p2  rootfs-a  512MB  根文件系统 A(只读,squashfs 或 ext4 只读挂载)
  p3  rootfs-b  512MB  根文件系统 B(A/B 更新的另一半)
  p4  data      1GB+   可变数据:日志、配置、数据库(可写)

只读根文件系统的做法有两种:一是直接烧 squashfs(压缩只读文件系统),二是 ext4 挂载为 ro 再加 overlayfs 把写操作重定向到内存或 data 分区。overlayfs 方案更灵活——系统看起来可写,但所有改动都在上层,重启即还原,配合 tmpfs 还能省 flash 写入。

A/B 更新(也叫双分区更新)是嵌入式 Linux 的标准升级方式:新镜像写到非当前分区,切换启动标志,重启进入新系统,确认健康后再标记为稳定;启动失败则自动回滚到旧分区。实现框架有 RAUC、SWUpdate、mender 三套,它们在「镜像格式」「签名」「回滚判定」上各有取舍,细节见 OTA 固件升级与差分更新 。

数据分区要挂载到 data,并在这里放日志、配置、以及任何需要持久化的东西。把日志写到只读根分区会导致「写失败但程序不报错」,是现场最难查的一类问题。另外要给数据分区留足空间并监控剩余量,写满会导致服务崩溃。

9. systemd 与启动优化

systemd 是嵌入式 Linux 的主流 init,它用「单元(unit)」描述服务、挂载、定时器等,按依赖关系并行启动。相比 SysV init 的串行脚本,systemd 的并行能力能显著缩短启动时间。

; /lib/systemd/system/my-app.service
[Unit]
Description=My IoT App
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/bin/my-app --config /data/etc/app.conf
Restart=on-failure
RestartSec=5
WatchdogSec=30
; 资源限制与安全加固
MemoryMax=128M
ProtectSystem=strict
ReadWritePaths=/data
NoNewPrivileges=true

[Install]
WantedBy=multi-user.target

几个实用点:Restart=on-failure 让服务崩溃后自动重启,WatchdogSec 配合 sd_notify 实现应用级看门狗(比硬件看门狗更精细),MemoryMax 限制内存防止单个服务拖垮系统。安全加固项(ProtectSystem、NoNewPrivileges、PrivateTmp)能限制服务的攻击面,边缘设备尤其值得开。

启动优化先量化再优化:

systemd-analyze                    # 总启动时间
systemd-analyze blame              # 每个服务耗时排序
systemd-analyze critical-chain      # 关键路径
systemd-analyze plot > boot.svg    # 可视化启动时序

常见优化:把非关键服务设为 After= 而非 Requires=(不阻塞启动)、用 Type=notify 精确报告就绪、合并或延迟启动耗时服务、精简镜像里的服务数量。目标是把「到业务可用的时间」压到几秒内,而不是纠结内核启动的零点几秒。

10. 长期维护与安全更新

嵌入式 Linux 产品通常要求 5 到 10 年生命周期,长期维护是必答题而非加分项。

维护项做法频率
内核用 LTS 内核(如 6.6 LTS),跟稳定分支补丁按需
用户空间跟 Yocto 的 LTS 发行版(如 kirkstone)年度小版本
CVE 扫描用 cve-check 类工具扫镜像里的已知漏洞每月
依赖更新定期升级有漏洞的库(openssl、busybox)按 CVE 优先级
复现构建固定所有 SRCREV,保证可复现每次发布

Yocto 提供了 cve-check 类,能在构建时扫描配方对应的 CVE 并输出报告。把它接进 CI,每次构建产出 CVE 清单,才能系统性地管理安全债。关键是固定版本:所有配方用 SRCREV 锁死到具体提交,不用 AUTOREV,否则每次构建拉到的代码不同,无法复现、也无法审计。

安全更新的节奏建议:高危 CVE 在评估影响后一周内出补丁,中低危按季度汇总。更新包要能通过 OTA 通道下发,且升级本身要可回滚——这正是第 8 节 A/B 分区的价值。软件物料清单(SBOM)越来越被合规要求,Yocto 能生成 SPDX 格式的 SBOM,建议从第一个版本就开始产出。

11. 与裸机、RTOS 的边界

Linux 和 RTOS 不是互斥的,理解各自边界才能设计好异构系统。

维度裸机 / RTOS嵌入式 Linux
实时性硬实时,微秒级抖动软实时,毫秒级抖动(PREEMPT_RT 可到几十微秒)
启动时间毫秒级秒级
内存占用KB 级MB 级
功耗极低高(需常供电)
开发效率低(无文件系统、无进程)高(脚本、容器、调试工具全)
可靠性无 MMU 隔离进程隔离,崩溃不拖垮系统

需要硬实时的控制回路(电机、编码器、安全联锁)应留在 MCU 上,Linux 侧只做非实时的高层逻辑。Linux 侧即使开了 PREEMPT_RT 补丁,也受调度、中断、缓存等因素影响,抖动通常在几十微秒量级,达不到 MCU 的确定性。

异构通信的常见方式:UART(简单、慢)、SPI(快、需协议)、共享内存加中断(最快、最复杂)。设计时要明确「谁主导」:通常 Linux 侧做主控与决策,MCU 侧做采集与执行,通过一个明确定义的二进制协议交互。协议要带长度、校验、序列号,并处理一端重启的情况。

12. 调试手段

嵌入式 Linux 的调试工具链比 MCU 丰富,但需要知道用哪个。

问题类型工具说明
启动失败串口控制台看 U-Boot 与内核早期日志
驱动不工作dmesg、lsmod、/proc/device-tree看 probe 是否成功
应用崩溃strace、gdb、core dump跟踪系统调用与栈
性能问题perf、ftrace、topCPU 热点与调度延迟
内核问题JTAG + kgdb内核级单步
网络问题tcpdump、ss、ip抓包与连接状态

串口控制台是第一工具:从 U-Boot 到内核到 init,所有早期日志都从串口出来,没有串口基本等于盲调。生产板一定要引出串口测试点。ftrace 能跟踪内核函数调用与调度延迟,是做实时性分析和定位卡顿的利器。perf 做用户空间与内核的性能剖析,火焰图能一眼看出热点。

内核崩溃(panic)时如果配了 pstore 或串口日志,能看到最后的调用栈。应用崩溃要开 core dump 并用交叉 gdb 分析。建议在产品上保留一个「诊断模式」,能按需开启详细日志并通过 边缘计算与边缘网关 里讲的日志回传通道送到云端,避免现场靠猜。

13. 权衡取舍

决策点方案 A方案 B判据
发行版Yocto 自建Debian/Ubuntu 定制需要极致裁剪与长期可控选 Yocto,快速原型可用 Debian
引导U-Boot直接内核(无 U-Boot)需要 A/B、救援、多启动项用 U-Boot,极简设备可省
根文件系统squashfs 只读ext4 加 overlayfs追求最小与防篡改用 squashfs,需灵活写用 overlayfs
更新粒度全镜像 A/B包级更新全镜像简单可靠,包级省流量但依赖管理复杂
内核厂商 BSP 内核上游 LTS 内核初期用 BSP 快速上手,成熟后向 LTS 靠拢
实时性普通内核PREEMPT_RT软实时用普通内核,毫秒级确定性需求上 RT 补丁

一条原则:把「可复现构建」和「可回滚更新」当作第一天就要建的基线,而不是后期补。嵌入式 Linux 的复杂度不在于让系统跑起来,而在于让它在五年后还能安全地升级。

14. 常见坑清单

  1. 现象:板子完全没输出。原因:引导镜像写错偏移或 DDR 初始化失败。规避:用厂商烧录工具,核对引导介质与偏移。
  2. 现象:内核起来了但挂不上根文件系统。原因:root= 设备名不对或 rootwait 缺失。规避:用 PARTUUID 并加 rootwait。
  3. 现象:设备树改了但没生效。原因:用了旧 dtb 或没重新编译进 boot 分区。规避:确认启动加载的是新 dtb,检查 dtb 时间戳。
  4. 现象:驱动探测不到设备。原因:pinctrl 引脚复用配置错误或 status 未置 okay。规避:对照原理图核对 pinctrl,检查 dmesg 的 probe 日志。
  5. 现象:系统跑几天后无法写入。原因:只读根分区上写日志失败,或 data 分区写满。规避:日志写 data 分区,监控剩余空间并轮转。
  6. 现象:增量构建行为不一致。原因:用了 AUTOREV 或未固定 SRCREV。规避:所有配方锁死版本,CI 持久化 sstate 与 DL_DIR。
  7. 现象:OTA 升级后设备起不来。原因:A/B 标志未切换或新镜像校验失败。规避:实现启动计数与自动回滚,升级后校验签名。
  8. 现象:systemd 服务启动顺序错乱。原因:依赖关系写成 Requires= 导致阻塞或时序错误。规避:用 After=/Wants= 表达顺序与弱依赖,用 sd_notify 报告就绪。
  9. 现象:CVE 越积越多无人管。原因:没有 CVE 扫描流程,依赖停在老版本。规避:CI 接入 cve-check,定期升级高危库。
  10. 现象:量产板与开发板行为不同。原因:eFuse、启动模式、器件差异未纳入构建配置。规避:为量产板单独建 MACHINE,固化启动介质与安全启动。

15. 小结

嵌入式 Linux 的工程核心是「用构建系统管住复杂度」。引导链、内核、设备树、根文件系统四部分必须版本匹配,Yocto 用配方与层把这套匹配关系固化下来,让定制可复现、可审计、可升级。掌握引导链、设备树、只读根文件系统与 A/B 更新这四块,就能覆盖大部分产品的需求。

长期维护是嵌入式 Linux 最容易被低估的部分。选主线支持的 SoC、锁死版本、接 CVE 扫描、产出 SBOM、保证可回滚更新,这些工作在项目初期看不出价值,却决定了产品五年后是否还能安全地打补丁。

下一步建议:实时任务与 MCU 侧调度在 RTOS 与 FreeRTOS 任务调度一文中有系统展开;镜像级升级与差分的实现细节属于 OTA 固件升级一文的范围;把 Linux 网关接进云边体系的整体设计见边缘计算与边缘网关一文。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「物联网」更多文章

  1. 工业物联网协议与网关
  2. 边缘 AI 推理
  3. 设备配网与批量运维