3. Zephyr RTOS 概述:为 Bootloader 学习奠定基础
Zephyr RTOS 是一个面向资源受限嵌入式设备的开源实时操作系统,其设计目标并非简单的任务调度,而是构建一个可裁剪、可扩展、安全且支持多架构的完整嵌入式软件平台。对于学习 Zephyr Bootloader 而言,理解 Zephyr 的项目架构、构建系统、设备树(Device Tree)和 Kconfig 配置机制,是打开 Bootloader 源码与链接脚本之门的钥匙。Bootloader 本质上也是一个特殊的 Zephyr 应用,它遵循相同的构建规则,却承担着与普通应用截然不同的系统初始化职责。本章将系统梳理这四个核心支柱,并揭示它们如何共同影响 Bootloader 的编译、链接与运行。
上图展示了 Zephyr 构建系统的核心数据流。应用层通过 prj.conf 和 board_defconfig 向 Kconfig 提供配置意图,通过 .dts/.dtsi 文件向设备树提供硬件描述。两者在 CMake 构建引擎中交汇,生成最终的 autoconf.h 与 devicetree.h,进而控制源码编译和链接脚本生成。对于 Bootloader,这一流程决定了其代码体积、外设驱动加载方式以及内存分区布局。
3.1 Zephyr 项目架构:从模块化到分层
核心原理:内核与子系统的解耦
Zephyr 项目采用高度模块化的分层架构。最底层是硬件抽象层(HAL),直接与 SoC 寄存器交互;其上是内核(Kernel),提供线程调度、中断管理和同步原语;再往上是各类子系统,如蓝牙、USB、文件系统和网络协议栈。这种分层的核心价值在于:Bootloader 可以只编译内核的最小子集,甚至不启动调度器,仅利用内核提供的链表、内存拷贝等基础工具。
Zephyr 的源码目录结构直接反映了这种架构:
zephyr/
├── arch/ # 架构相关代码 (ARM, RISC-V, X86)
├── boards/ # 板级支持包 (含 dts 和 defconfig)
├── drivers/ # 外设驱动
├── kernel/ # 内核核心
├── lib/ # 库函数 (如 libc 最小实现)
├── subsys/ # 子系统 (蓝牙, 网络, 存储)
├── include/ # 公共头文件
└── scripts/ # 构建辅助脚本 (如 west)
对于 Bootloader 开发者,最需要关注的是 arch/ 下的启动代码(reset.S、prep_c.c)和 kernel/ 中的初始化流程。Bootloader 通常从 z_cstart() 开始执行,该函数在 kernel/init.c 中定义,负责完成从汇编到 C 环境的过渡。
判断方法:如何识别 Bootloader 所需的模块
判断一个模块是否被 Bootloader 需要,可以遵循“最小依赖原则”:
- 启动路径:是否需要多级引导?若仅需单级,则跳过
CONFIG_BOOTLOADER_MCUBOOT相关逻辑。 - 外设需求:Bootloader 通常只需要 UART(日志)、Flash(固件存储)、GPIO(状态指示)。通过 Kconfig 关闭不需要的驱动。
- 调度需求:若 Bootloader 采用轮询方式而非多线程,则关闭
CONFIG_MULTITHREADING,可显著减少 RAM 占用。
west build -b <board> -d build_bootloader 来构建它,与构建普通应用无本质区别。
案例拆解:一个最小 Bootloader 的模块选择
假设我们为 STM32F4 开发板编写一个串口下载 Bootloader。在 prj.conf 中,我们做如下裁剪:
# 关闭多线程与内核服务
CONFIG_MULTITHREADING=n
CONFIG_TIMESLICING=n
# 仅启用串口与 GPIO
CONFIG_SERIAL=y
CONFIG_UART_CONSOLE=y
CONFIG_GPIO=y
# 关闭不需要的子系统
CONFIG_NETWORKING=n
CONFIG_BT=n
CONFIG_USB=n
通过上述配置,最终生成的镜像仅包含 UART 驱动、GPIO 驱动和最小内核初始化代码,RAM 占用可控制在 4KB 以内。这验证了 Zephyr 模块化架构对 Bootloader 的友好性。
常见误区与复盘
- 误区一:认为 Bootloader 必须使用完整内核。 实际上,Zephyr 允许在无调度器模式下运行,此时中断处理采用简单的中断栈。
- 误区二:忽略
arch/层代码。 启动代码中的向量表重定位、栈指针初始化是 Bootloader 正确运行的前提,这些都在架构层实现。 - 复盘: 在裁剪模块时,务必检查
Kconfig的依赖关系。例如,关闭MULTITHREADING后,某些驱动可能因依赖内核 API 而无法编译,需同步调整驱动配置。
3.2 构建系统:CMake 与 West 的协同
核心原理:West 作为元工具,CMake 作为构建引擎
Zephyr 的构建系统基于 CMake,但通过 west(Zephyr's meta-tool)进行封装。West 不仅管理多仓库(如 zephyr、mcuboot、hal_stm32),还提供统一的构建命令入口。其核心流程如下:
west build解析CMakeLists.txt,该文件通过find_package(Zephyr)引入 Zephyr 的构建模板。- 构建模板读取
BOARD变量,加载对应的board.cmake和board_defconfig。 - CMake 生成
build.ninja文件,Ninja 负责实际的编译与链接。
对于 Bootloader,理解这一流程至关重要,因为 Bootloader 的链接脚本(.ld)通常由 CMake 根据设备树和 Kconfig 动态生成。
操作流程:构建一个 Bootloader 的完整命令
# 1. 初始化 Zephyr 环境
source zephyr-env.sh
# 2. 构建 Bootloader (以 MCUboot 为例)
west build -b stm32f4_disco -d build_mcuboot bootloader/mcuboot/boot/zephyr
# 3. 查看生成的镜像
ls build_mcuboot/zephyr/
# 输出: zephyr.elf, zephyr.bin, zephyr.hex, zephyr.map
其中 zephyr.map 文件是分析内存布局的关键,它显示了每个段(.text, .data, .bss)的起始地址和大小。
案例拆解:链接脚本的动态生成
Zephyr 的链接脚本并非静态文件,而是由 arch/common/CMakeLists.txt 根据设备树中的 flash 节点和 sram 节点自动生成。例如,若设备树中定义:
flash0: flash@8000000 {
reg = <0x08000000 0x100000>; /* 起始地址 0x08000000,大小 1MB */
};
sram0: memory@20000000 {
reg = <0x20000000 0x30000>; /* 起始地址 0x20000000,大小 192KB */
};
CMake 会生成如下链接脚本片段:
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 0x100000
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 0x30000
}
Bootloader 开发者可以通过修改设备树中的 reg 属性来调整内存布局,而无需手动编辑 .ld 文件。
常见误区与复盘
- 误区一:直接修改
zephyr.ld文件。 该文件是生成的,修改会被覆盖。正确做法是修改设备树或使用CMakeLists.txt中的zephyr_linker_sources()添加自定义段。 - 误区二:忽略
west build -t menuconfig的交互式配置。 该命令可图形化查看 Kconfig 选项,对排查依赖问题极有帮助。 - 复盘: 当 Bootloader 链接失败时,优先检查
build.log中的链接命令,确认-T参数指向的链接脚本是否正确。
3.3 设备树:硬件描述的“数据驱动”
核心原理:从 DTS 到 C 宏的转换
设备树(Device Tree)是一种描述硬件资源的数据结构,Zephyr 使用 .dts(设备树源文件)和 .dtsi(包含文件)来描述 SoC 和板级外设。构建时,gen_defines.py 脚本将 DTS 转换为 devicetree.h,其中包含大量宏定义,供驱动源码使用。
例如,UART 节点的定义:
uart0: serial@40011000 {
compatible = "st,stm32-uart";
reg = <0x40011000 0x400>;
interrupts = <37 0>;
status = "okay";
};
生成的宏包括:
#define DT_N_S_serial_40011000_BASE_ADDRESS 0x40011000
#define DT_N_S_serial_40011000_SIZE 0x400
#define DT_N_S_serial_40011000_IRQ_0 37
驱动代码通过 DT_N_S_serial_40011000_BASE_ADDRESS 访问寄存器地址,实现了硬件描述与代码逻辑的分离。
判断方法:如何定位 Bootloader 所需的外设节点
Bootloader 通常需要以下节点:
| 外设 | 设备树节点 | Bootloader 用途 |
|---|---|---|
| Flash 控制器 | flash0 | 固件读取与写入 |
| UART | uart0 | 调试日志与固件接收 |
| GPIO | gpioa, gpiob | LED 状态指示、按键检测 |
| 时钟 | clk_hse, clk_hsi | 系统时钟配置 |
在 boards/<arch>/<board>.dts 中,这些节点通常已被定义。Bootloader 开发者只需确保其 status 属性为 "okay"。
案例拆解:通过 Overlay 调整 Flash 分区
MCUboot 需要定义固件槽位(slot0, slot1)和暂存区(scratch)。这可以通过设备树 Overlay 实现,而无需修改原始 DTS 文件。创建 mcuboot.overlay:
/ {
chosen {
zephyr,flash = &flash0;
};
};
&flash0 {
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
boot_partition: partition@0 {
label = "mcuboot";
reg = <0x0 0x10000>; /* 64KB for bootloader */
};
slot0_partition: partition@10000 {
label = "image-0";
reg = <0x10000 0x40000>; /* 256KB */
};
slot1_partition: partition@50000 {
label = "image-1";
reg = <0x50000 0x40000>;
};
};
};
构建时通过 -DOVERLAY_CONFIG=../mcuboot.overlay 引入。这种方式保证了板级 DTS 的纯净性,便于维护。
常见误区与复盘
- 误区一:混淆
reg属性中的地址与大小。 地址是外设的基地址,大小是地址空间长度,而非内存大小。 - 误区二:忽略
chosen节点。chosen节点用于指定默认控制台、默认 Flash 等,Bootloader 依赖zephyr,console和zephyr,flash属性。 - 复盘: 当外设驱动无法匹配时,使用
west build -t devicetree可以输出最终的设备树文件,便于排查节点状态。
3.4 Kconfig 配置:符号解析与依赖管理
核心原理:Kconfig 的层次化与依赖
Kconfig 是 Zephyr 的配置系统,它定义了一系列符号(Symbol),每个符号可以是一个布尔值(bool)、整型(int)或字符串(string)。符号之间存在依赖关系,例如:
config MCUBOOT
bool "Enable MCUboot support"
select BOOTLOADER_MCUBOOT
depends on FLASH
当用户启用 MCUBOOT 时,Kconfig 会自动选择 BOOTLOADER_MCUBOOT,并检查 FLASH 是否已启用。这种机制确保了配置的一致性。
操作流程:配置 Bootloader 的 Kconfig 选项
- 在
prj.conf中设置基础选项,如CONFIG_BOOTLOADER_MCUBOOT=y。 - 使用
west build -t menuconfig打开图形界面,搜索BOOT相关选项。 - 检查
.config文件,确认所有依赖项已自动启用。
关键配置项示例:
# 启用签名验证
CONFIG_BOOT_SIGNATURE_TYPE_ECDSA_P256=y
# 设置固件槽位大小
CONFIG_BOOT_MAX_IMG_SECTORS=128
# 启用串口日志
CONFIG_BOOT_LOG_LEVEL_INF=y
案例拆解:裁剪 Bootloader 的日志输出
假设我们需要将 Bootloader 的日志级别从 INFO 降为 WARNING,以减少串口输出时间。在 prj.conf 中修改:
CONFIG_BOOT_LOG_LEVEL_WRN=y
# 同时关闭不必要的模块日志
CONFIG_LOG_MODE_IMMEDIATE=n
重新构建后,通过 strings zephyr.bin | grep "wrn" 验证日志字符串是否减少。这一调整在量产阶段可显著缩短启动时间。
常见误区与复盘
- 误区一:直接修改
.config文件。 该文件是生成的,修改会在下次构建时被覆盖。正确做法是修改prj.conf或board_defconfig。 - 误区二:忽略
depends on导致的静默失效。 当依赖不满足时,Kconfig 会忽略该选项,但不会报错。务必使用menuconfig检查符号状态。 - 复盘: 在团队协作中,建议将
prj.conf的变更记录在 Git 提交信息中,并附上menuconfig的截图,便于他人理解配置意图。
prj.conf 中显式启用所需驱动,并检查 devicetree.h 中是否生成了对应的宏。
3.5 综合实战:从零构建一个带签名验证的 Bootloader
为了将上述知识串联起来,我们设计一个综合案例:为 nRF52840 DK 板构建一个支持 ECDSA 签名验证的 MCUboot Bootloader。
背景与约束: 该 Bootloader 需要验证应用固件的签名,确保只有经过授权的固件才能运行。硬件资源:1MB Flash,256KB RAM。开发环境:Zephyr 3.5,MCUboot 1.10。
初始现象与目标: 默认 MCUboot 配置不启用签名验证,我们需要修改配置并验证签名流程。
关键输入参数:
- 签名算法:ECDSA P-256
- Flash 分区:Bootloader 64KB,Slot0 448KB,Slot1 448KB,Scratch 64KB
- 密钥文件:
root-ec-p256.pem
操作步骤:
# 1. 生成签名密钥
imgtool keygen -k root-ec-p256.pem -t ecdsa-p256
# 2. 构建 Bootloader
west build -b nrf52840dk_nrf52840 -d build_mcuboot bootloader/mcuboot/boot/zephyr \
-DCONFIG_BOOT_SIGNATURE_TYPE_ECDSA_P256=y \
-DCONFIG_BOOT_SIGNATURE_KEY_FILE="<abs_path>/root-ec-p256.pem"
# 3. 构建应用并签名
west build -b nrf52840dk_nrf52840 -d build_app samples/hello_world
west sign -d build_app -t imgtool --key root-ec-p256.pem --algo ecdsa-p256
# 4. 合并镜像并烧录
west flash -d build_mcuboot
west flash -d build_app
结果对比: 未启用签名时,Bootloader 直接跳转到 Slot0;启用后,若签名验证失败,Bootloader 会尝试从 Slot1 启动或进入恢复模式。
风险与代价: 签名验证增加了约 2KB 的代码体积和约 50ms 的启动延迟(在 64MHz 主频下)。此外,密钥管理是安全的关键,私钥泄露将导致整个信任链崩溃。
复盘经验: 在开发阶段,可以使用 CONFIG_BOOT_SIGNATURE_TYPE_NONE 快速迭代;在量产阶段,务必启用签名并妥善保管私钥。同时,建议在 CI 流程中加入签名步骤,确保证书的可追溯性。
3.6 总结与知识结构梳理
本章围绕 Zephyr 的四大核心组件展开,它们之间的关系可以总结为:设备树描述“有什么硬件”,Kconfig 决定“用什么功能”,CMake 负责“怎么编译”,而项目架构则定义了“代码放哪里”。对于 Bootloader 开发者,掌握这四者的协同是理解链接脚本、启动流程和驱动加载的前提。
| 组件 | 核心作用 | Bootloader 关注点 | 常见工具/命令 |
|---|---|---|---|
| 项目架构 | 模块化分层 | 最小内核裁剪 | west list |
| 构建系统 | 编译链接 | 链接脚本生成 | west build, ninja |
| 设备树 | 硬件描述 | Flash 分区、外设地址 | west build -t devicetree |
| Kconfig | 功能配置 | 签名、日志、驱动开关 | menuconfig |