只读分享解决方案文档原理方案设计📅 2026-10-14🔖 Rev 3.20.0⏳ 还有3日到期
🔧解决方案&应用市场分析 -> 原理方案设计 -> 嵌入式驱动&系统开发 -> Zephyr_系统开发指南

Zephyr_系统开发指南

同系列 · 原理方案设计
扫码打开本页
微信扫码

Zephyr 系统开发指南

编制日期 2026-10-14

面向 Cortex-M / RISC-V 的 Zephyr 系统级开发规范 · 覆盖 west 工作区、内核线程调度、设备模型、Devicetree 与 Kconfig、构建系统、存储子系统、低功耗、测试与 MCUboot OTA · 系统总文档,驱动开发为其配套子文档

1000+
官方支持开发板
跨 Nordic / NXP / ST / Espressif 等
DT + Kconfig
描述与配置
硬件与选型全靠声明,不手写板级代码
west
构建与模块管理
CMake + Kconfig + Python 工具链三件套
native_sim
主机仿真
逻辑可先在 PC 上跑通再上板
0 容忍
ISR 中阻塞调用
中断上下文禁止 k_mutex_lock / k_sem_take

结论:Zephyr 的系统设计,本质是用「Devicetree 描述硬件 + Kconfig 裁剪功能 + 设备模型统一初始化」把「几百块板卡各自手写板级代码」变成「一份应用配置 + 一份 overlay」。 它比 FreeRTOS 多了设备树、统一的设备初始化等级与可复用的网络/蓝牙/存储子系统,比 Linux 少了MMU 与进程抽象——代价是必须接受 Kconfig 与 DT 这两层编译期抽象的学习曲线。 线上故障最集中的四处:overlay 写了但设备没使能(status 仍为 disabled)、设备就绪检查被跳过、ISR 里调用了阻塞 API、栈与堆被低估后随机踩踏。管住这四处,系统稳定性解决八成。

报告总结

本文是 Zephyr 系统级开发总文档:从 west 工作区与工具链、内核线程与同步原语、设备模型与初始化等级、Devicetree 与 Kconfig、构建系统,到工作队列、日志诊断、存储子系统、网络蓝牙、性能低功耗、可靠性与 MCUboot OTA、测试与发布,构成一条完整链路。 具体的驱动编写(外设驱动、存储器件驱动、文件系统挂载)由配套的《Zephyr 驱动开发指南》承接;本文只讲「在这一层如何把系统组装起来、如何把驱动挂进系统」。器件级电气与时序细节仍以各器件的设计指南为准。

路线 1 · 移植新板 / 新芯片
从零立骨架
第 2 章 west 工作区与 SDK → 第 5 章设备模型与初始化等级 → 第 6 章 Devicetree 与 overlay → 第 11 章构建系统 → 第 20 章常见坑 → 附录 D 核查清单。
重点:三种应用形态(2.3)、初始化等级(5.3)、overlay 生效顺序(6.2)
路线 2 · 排查启动与设备不工作
设备没反应 / 编译过了跑不起来
第 5.4 节设备就绪检查 → 第 6.5 节 Devicetree 排查 → 第 13 章日志与 Shell → 第 13.5 节决策树 → 第 20.3 节启动错误速查。
重点:__device_dts_ord_N 未定义(6.5)、device_is_ready(5.4)
路线 3 · 做存储与数据持久化
配网信息 / 日志 / 升级镜像落盘
第 14 章存储子系统 → 14.2 Flash 分区 → 14.3 NVS 与 ZMS 选型 → 14.4 文件系统 → 14.6 掉电安全 → 第 17 章 OTA。
重点:NVS 页大小 32KB 限制(14.3)、fstab 自动挂载(14.4)
路线 4 · 优化功耗与体积
电池供电 / Flash 装不下
第 10 章电源管理 → 第 16 章性能资源与启动优化 → 16.1 ROM/RAM 预算 → 16.5 日志与体积权衡 → 附录 A 配置模板。
重点:设备级 PM 策略(10.3)、唤醒源(10.4)、日志裁剪(16.5)
路线 5 · 选型对比
Zephyr / Linux / FreeRTOS / RT-Thread / AliOS
第 1.5 节五系统横向对照 → 第 20 章 FAQ → 附录 D 核查清单。
重点:DT 与 Kconfig 的代价(1.5)、存储子系统完备度(14.1)

使用约定

  • API、目录与配置项以 Zephyr 3.7 LTS / 4.x 为主线,兼顾 3.5 及更早版本;差异明显处单独标注。目标平台以 Cortex-M3/M4/M33/M55、RISC-V 为主。
  • 代码示例只保留系统层逻辑;具体寄存器操作统一写成 /* SoC 层提供 */ 注释。
  • 标 红线 为强制约束,标 警惕 为高频踩坑点,标 建议 为经验推荐值。
  • 文中出现的默认数值(栈大小、优先级、缓冲区大小、宏名)以官方仓库与 Kconfig 为参考;不同版本宏名与默认值可能变化,落地前以工程内 build/zephyr/.config 与 zephyr.dts 为准。
  • 芯片参数、时钟、外设行为一律以官方 datasheet 与 Zephyr 官方文档 / 源码为准。

1 · 文档概述与 Zephyr 架构

这一章解决三件事:这份文档覆盖到哪一层、Zephyr 由哪些层构成、它和 Linux / FreeRTOS / RT-Thread 的边界在哪。读完再决定后面该精读哪几章。

1.1 编写目的、适用范围与读者

本指南面向基于 Zephyr 做系统级开发的工程团队:把一颗裸的 MCU 变成可量产、可升级、可运维的终端设备。它要回答的不是「某个寄存器怎么写」,而是「一堆驱动、子系统与应用如何按正确顺序长成一个系统」。

维度本指南覆盖本指南不覆盖
目标读者系统 / BSP 工程师、驱动工程师、应用与联网工程师、测试与发布负责人只写业务逻辑、不接触系统的上层应用开发
硬件范围无 MMU 或轻量 MMU 的 MCU:Cortex-M0/M3/M4/M33/M55、RISC-V、AArch64带 MMU 的 Cortex-A 应用处理器(走 Linux 分支)
软件范围west 工作区、内核线程与同步、设备模型、DT 与 Kconfig、构建、存储、电源、测试、MCUboot OTA单个器件的电气与命令时序(见各器件设计指南)
文档定位系统总文档;驱动编写由配套的驱动开发指南承接云平台侧配置与业务规则

1.2 版本基线、支持平台与分支策略

Zephyr 有两条实际在用的版本线:3.7 LTS(长期支持,稳定性优先,适合量产)与 4.x(新特性跟进更快)。本指南以 3.7 LTS 为主、4.x 兼容,差异较大处显式标注。

基线项说明对开发的影响
内核可抢占实时内核,协作式与抢占式时间片均支持;调度算法可选 EDF 与 Meta IRQ内核行为由 Kconfig 决定,不是改源码
硬件描述Devicetree(.dts / .dtsi / .overlay),编译期由 dtc 生成 C 头文件改硬件配置不碰驱动代码,只碰 overlay
功能裁剪Kconfig 声明式配置,menuconfig / guiconfig 交互所有可选功能都有一枚 CONFIG_ 宏
构建west(多仓库管理 + 构建入口)驱动 CMake(构建规则)+ Kconfig(配置)+ Python(工具)一个 west build 完成从配置到链接的全过程
设备模型统一 struct device + API 表,按初始化等级与优先级自动 init驱动只需实现 init 与 API,无需自己排顺序
调试内置 Shell、日志分级、tracing、栈保护、崩溃捕获多数问题不必接仿真器即可定位
官方硬件1000+ 官方支持开发板,覆盖 Nordic / NXP / ST / Espressif / Renesas 等新项目优先选已支持板,把精力留给业务
基线选择建议

量产项目一律锁定 3.7 LTS 或明确的主线 tag,不要长期跟随 main 分支。Zephyr 的 API 与设备树绑定在主线演进中会调整,跟着 main 走意味着每次同步都可能要改应用代码。 正确做法是:应用仓库独立,通过 west.yml 固定各个依赖的 commit(见 19.1),需要升级时一次性升并回归,而不是日常被动跟随。

1.3 术语与缩写

术语全称 / 含义出现场景
ZephyrLinux 基金会下的开源实时操作系统,面向资源受限设备全文
westZephyr 的多仓库管理与构建命令行工具初始化、构建、烧录、升级模块
Devicetree / DT描述硬件拓扑的树形结构,编译期生成 C 宏节点定义、overlay、驱动取址
overlay叠加在基础设备树之上的补丁文件应用级硬件变更
bindingYAML 文件,声明某类节点有哪些属性及其约束自定义节点必需
Kconfig配置系统,定义全部可选功能与编译期选项prj.conf、menuconfig
prj.conf项目配置文件,Kconfig 片段根目录,声明 CONFIG_ 宏
Kconfig 合并链板级 conf、芯片 conf、prj.conf、命令行 overlay 的合并顺序见 7.2
struct deviceZephyr 的设备对象,内核用它统一管理所有驱动驱动注册、应用取用
DEVICE_DT_INST_DEFINE声明式设备注册宏,为每个实例生成设备对象驱动代码的核心宏
初始化等级 / level设备初始化的阶段,从 PRE_KERNEL_1 到 APPLICATION设备依赖排序(见 5.3)
device_is_ready检查设备是否初始化成功应用取用设备前必查
k_work_q工作队列,把工作项延迟到线程上下文执行中断里只发事件,处理放工作队列
NVSNon-Volatile Storage,轻量键值存储,ID 为 16 位配置与少量参数
ZMSZephyr Memory Storage,键值存储,支持 32/64 位 ID 与大数据高容量存储、外部 Flash
MTD / flash_mapFlash 分区抽象层,向上提供分区对象分区与存储后端的基础
fstab文件系统表节点,支持开机自动挂载LittleFS / FatFs 挂载
settings配置持久化子系统,可挂 NVS / ZMS / FCB / 文件子系统级配置存储
MCUbootZephyr 生态的引导加载程序安全启动、镜像升级与回滚
sysbuild多镜像构建系统,取代早期的 west build -t boot应用 + bootloader 联合构建
native_sim把 Zephyr 编译成宿主机原生程序运行主机侧快速仿真验证

1.4 五层架构总览

Zephyr 采用分层架构 + 编译期声明双视角:分层决定「谁能调用谁」,声明决定「编译时包含什么、硬件怎么连」。两者叠加的效果是——应用只写业务与配置,硬件细节全部由设备树供给。

Zephyr 五层架构(自上而下依赖,越往下越贴近硬件)⑤ 应用层 applications / samples / your app业务线程main() 入口Zephyr Shell命令行网络应用socket / HTTP服务与用例settings 消费者④ 子系统层 subsystems(可裁剪中间件)存储NVS / ZMS / FS网络TCP/IP 栈蓝牙BLE 主机+控制器日志与追踪log / tracing③ 设备模型层 device model(统一 API + 初始化等级)设备对象struct device设备 API驱动操作接口初始化等级PRE_KERNEL_1→APP设备树绑定DT_INST 宏展开② 内核层 kernel(可抢占实时内核)线程与调度k_thread / SMP同步原语mutex / sem / fifo工作队列k_work_q内存与定时器slab / k_timer① 硬件抽象层 arch / SoC / board(含 SoC HAL 与板级 DTS)arch 内核移植层SoC 层驱动与时钟board 层 DTS 与分区Kconfig 裁剪配置关键约束:上层只能调用本层及以下暴露的 API;硬件细节一律由 Devicetree 描述,不在驱动里写死。
图 1 · Zephyr 五层架构:应用 / 子系统 / 设备模型 / 内核 / 硬件抽象
  • ⑤ 应用层:你的业务代码与官方 samples。唯一「自己写」的一层,也是唯一不随芯片变化的层。
  • ④ 子系统层:存储、网络、蓝牙、日志、追踪等可裁剪的中间件。裁剪由 Kconfig 决定,未使能的子系统链接时完全不进镜像。
  • ③ 设备模型层:Zephyr 的枢纽。struct device 承载状态,API 表承载操作,初始化等级决定谁先起来。它让「换一颗芯片不改应用」成为可能。
  • ② 内核层:线程、调度、同步原语、内存分配器、工作队列、定时器。多核下由 SMP 支撑。
  • ① 硬件抽象层:arch(内核移植)、SoC 层(时钟与外设驱动)、board 层(板级 DTS 与分区)、以及贯穿全局的 Kconfig 裁剪。
警惕:不要跨层直接操作

应用里直接写寄存器、或驱动里直接改设备树以外的东西,都会破坏「换板只改 overlay」的承诺。应用只通过设备 API 说话;寄存器操作只允许出现在 SoC 层与设备驱动内部;板级差异只允许出现在 DTS / overlay 中。

1.5 与 Linux / FreeRTOS / RT-Thread / AliOS 的差异

选型时最常问的问题是「Zephyr 比 Linux 多了什么、比 FreeRTOS 多了什么」。下表按能力维度对照,深色列为 Zephyr。

五种系统的关键能力横向对照(深色格为本指南重点覆盖项)能力维度ZephyrLinuxFreeRTOSRT-ThreadAliOS Things硬件描述Devicetree 强依赖Device Tree无(手写)无(手写)无(静态配置)功能裁剪Kconfig 声明式menuconfigconfig 宏menuconfigyaml 组件设备初始化统一等级+优先级probe 延迟无统一机制分层 INIT九级加载统一设备 API设备 API 表inode + 文件操作无统一设备层设备框架VFS网络栈内置 TCP/IP + BLE内核态协议栈需外挂需外挂SAL + LwIP存储子系统MTD/NVS/FS 完整MTD + ext4 等需自行实现DFS + FATFSKV + FATFS进程/MMU 隔离无(MCU 裸机)有无无部分支持多核 SMP支持支持有限有限有限结论:Zephyr 处在「RTOS 的轻量」与「Linux 的完备」之间 —— 硬件抽象最完整、代价是编译期抽象层最多。
图 2 · 五种系统能力横向对照:DT / Kconfig / 设备模型 / 子系统完备度

逐条说明差异的实际含义:

维度Zephyr 的做法带来的收益付出的代价
硬件描述设备树强依赖,驱动不写死寄存器与引脚同一驱动跨厂商复用,换料只改 overlay必须学 DT 语法与绑定,调试链路多一层
功能裁剪Kconfig 声明,未启用即不链接镜像小、无死代码、可复现配置项数量大,需要理解依赖关系
设备初始化统一等级 + 同级 prio 排序驱动作者不必手工排 init 顺序等级选错会导致启动期竞态(见 5.3)
设备访问设备 API 表,不经文件系统类型安全、错误码统一无法像 Linux 那样用 open/read 统一访问
网络与蓝牙内置 TCP/IP 栈与完整 BLE 协议栈IoT 设备无需外挂协议栈内存占用高于精简方案,需精细裁剪
存储MTD 分区 + NVS / ZMS / LittleFS / FatFs数据持久化开箱即用抽象层多,学习成本在「该选哪个后端」
内存保护MCU 上用 MPU 做线程隔离比裸 RTOS 更安全无进程与虚拟内存,不能跑 Linux 应用
启动与升级MCUboot + sysbuild 多镜像安全启动与 A/B 回滚生态成熟镜像布局需预先规划,升级路径设计不可省
选型一句话

需要完整网络/蓝牙生态 + 跨厂商硬件复用 + 量产级安全启动,且芯片带 MCU 级资源,选 Zephyr; 需要极简可控、几乎不用中间件,或团队对 DT/Kconfig 无从下手,选 FreeRTOS; 需要应用处理器 + 复杂文件系统 + 进程隔离,选 Linux; 需要国产 RTOS 生态与组件化 yaml 构建,选 RT-Thread 或 AliOS Things。

1.6 阅读路线图

你的目标推荐阅读顺序预计时长
移植一颗新 MCU / 新板卡2 → 6 → 5 → 11 → 13 → 20 → 附录 D半天读完,动手 2~3 天
现有板子跑不起来 / 设备无反应5.4 → 6.5 → 13.3 → 13.5 → 20.31 小时定位
做数据持久化与存储布局14 全章 → 17.4 → 附录 A半天
做低功耗产品10 全章 → 16.1 → 16.4 → 14.6半天
量产前做发布准备17 → 18 → 19 → 附录 D1 天
做 Zephyr 驱动移植先读本篇第 5、6、13 章 → 转向《Zephyr 驱动开发指南》—
关于本篇与驱动篇的分工

本篇讲的是「系统怎么组装」:工作区、配置、设备模型、构建、存储布局、低功耗、OTA。 《Zephyr 驱动开发指南》讲的是「驱动怎么写:目录结构、DT 绑定、Kconfig、具体总线与器件驱动的完整范例。 两者共享同一套 Devicetree 与 Kconfig 基础,因此建议先读本篇第 5、6 章,再进入驱动篇。

2 · 开发环境与 west 工作区

这一章解决:怎么把一套能复现的 Zephyr 开发环境搭起来,以及 west 工作区、Zephyr SDK、你的应用三者的关系。环境没搭对,后面所有章节的排查都会失真。

2.1 宿主机依赖与 Zephyr SDK

Zephyr 对宿主机有明确要求。SDK 与工具链版本必须与你的 Zephyr 基线匹配,这是最常见的「明明代码没错却编译不过」的根因。

依赖项版本要求 / 说明校验方式
Zephyr SDK与 Zephyr 版本配套的独立工具链集合(含各架构交叉编译器与调试器)设置 ZEPHYR_SDK_INSTALL_DIR 后执行 west build,CMake 前段会打印 SDK 路径
CMake≥ 3.20(Zephyr 对最低版本有硬性检查)cmake --version
Python≥ 3.10(含 pip、venv、west、pyelftools 等)python3 --version
dtc设备树编译器;SDK 内自带时无需单独装构建日志中 -- Building DT 段无报错即正常
Gitwest 依赖 git 拉取所有 projectgit --version
westpip 安装的 Zephyr 前端工具pip install west,west --version
pip 依赖Zephyr 的 scripts/requirements.txt 中列出的 Python 包pip install -r zephyr/scripts/requirements.txt
红线:SDK 与 Zephyr 版本必须配套

Zephyr 会检查 SDK 版本是否与源码基线匹配,用错版本会在 CMake 配置阶段直接报版本不匹配并中止,而不是给出有意义的编译错误。 另外注意主机上若同时装了系统包与 pip 包的同名工具(如 cmake、west),PATH 顺序会决定实际调用哪一个——排查「参数不被识别」类怪问题时,先确认 which 指向哪里。

2.2 west 工作区初始化与模块管理

west 解决的是「Zephyr 由几十个仓库组成」的问题:它按 manifest 把所有 project 拉到同一层目录,并保证各仓库处于声明的 revision 上。

west 工作区结构:manifest 仓库 + 多个 project 检出在同一层工作区根 zephyrproject/(由 west init 生成,含 .west/config)west.yml 声明本工作区需要哪些 project、哪个 revision、哪些 activezephyr/内核 + 驱动 + 子系统(主仓库)modules/hal_* 厂商 HAL(多仓库)bootloader/MCUboot(安全启动)你的应用/app 或 my-product(业务代码)构建输出 build/(由 west build 生成,可随时删除重建)build/zephyr/zephyr.dts 合并后的最终设备树(排查 DT 问题第一站)build/zephyr/include/generated/ 生成的 DT 头文件与 autoconf.h(最终生效的 CONFIG_)build/zephyr/zephyr.map 链接映射,查 ROM/RAM 占用与大符号工具链(独立安装)Zephyr SDK 交叉编译器集合 | CMake ≥ 3.20 | Python ≥ 3.10 | dtc 设备树编译器west 负责「多仓库拉取与版本对齐」,CMake 负责「编译」,Kconfig 负责「裁剪」——三者各司其职。
图 3 · west 工作区结构:manifest 仓库、多个 project 检出、独立的构建输出

初始化步骤

  1. 装依赖:先装 Python、CMake、Git,再 pip install west,最后装匹配的 Zephyr SDK。
  2. 初始化工作区:west init ~/zephyrproject(已有仓库可 west init -l <已有目录> 就地接管)。
  3. 更新全部仓库:west update,把 manifest 声明的所有 project 拉到指定 revision。
  4. 装 Python 包:pip install -r ~/zephyrproject/zephyr/scripts/requirements.txt。
  5. 设置环境变量:把 ZEPHYR_BASE 指向 ~/zephyrproject/zephyr,把 ZEPHYR_TOOLCHAIN_VARIANT 设为 zephyr(用 SDK 时)。
  6. 验证:构建一次官方 sample,确认工具链与串口日志都通。
# 1) 装 west 与 Python 依赖
pip install west
pip install -r ~/zephyrproject/zephyr/scripts/requirements.txt

# 2) 初始化工作区(首次)
west init ~/zephyrproject
cd ~/zephyrproject
west update

# 3) 指定 SDK 并验证
export ZEPHYR_TOOLCHAIN_VARIANT=zephyr
export ZEPHYR_SDK_INSTALL_DIR=~/zephyr-sdk-0.17.0

# 4) 跑通第一个应用(以官方 sample 验证环境)
west build -b qemu_cortex_m3 zephyr/samples/hello_world
west build -t run          # native_sim / qemu 可直接跑,不需要真板
警惕:west update 会改动所有仓库

west update 会把每个 project 切到 manifest 声明的 revision,你在这些仓库里的本地未提交修改可能被覆盖或产生冲突。 因此永远不要在 Zephyr 官方仓库里直接改代码——需要改动时,走 11.5 的 Zephyr module 或 out-of-tree 驱动机制。 另外,west update 后若行为异常,先确认 west list 里各仓库的版本是否与预期一致。

2.3 三种应用形态与选型

Zephyr 官方定义了三种应用与源码的相对位置关系。选错了会导致 ZEPHYR_BASE 找不到、或应用被 Zephyr 更新冲掉。

三种应用形态与选型:T2 应用清单仓库是推荐的生产形态T1 · 仓库内应用zephyr/samples/hello_world优点:随 Zephyr 源码分发,零配置缺点:跟着 Zephyr 更新走适用:改官方 sample 试用生产环境不推荐T2 · 应用清单仓库(推荐)my-product/ 自有仓库west.yml 固定 Zephyr / MCUboot /HAL 的 commit优点:可复现、可 CI、版本自控缺点:需自己管 west.yml生产项目默认选这一种与 19.1 的版本管理建议一致模块按需拉取,不多拉T3 · 独立目录应用工作区之外,需设 ZEPHYR_BASE优点:与 Zephyr 完全隔离缺点:环境变量易配错适用:跨团队、跨公司协作或需要与现有 CMake 项目集成纪律:T2/T3 形态下 ZEPHYR_BASE 必须正确;配错时 CMake 报的错常与真实原因无关。
图 4 · 三种应用形态:T1 仓库内、T2 应用清单仓库、T3 独立目录

三个要点:

  • T2 是生产项目的默认选择:你的应用就是一个独立的 Git 仓库,通过 west.yml 把 Zephyr、MCUboot、厂商 HAL 的 commit 钉死。构建可复现、CI 好做、升级节奏自控(详见 19.1)。
  • T1 适合验证:改官方 sample 最省事,但不要用于量产——它会随 Zephyr 仓库变动。
  • T3 适合跨团队:应用与 Zephyr 彻底分离,但要自己保证 ZEPHYR_BASE 与 SDK 环境变量在所有机器上一致。

2.4 工程目录结构详解

一个结构良好的应用目录,能让新同事在半天内看懂「配置在哪、代码在哪、板级差异在哪」。推荐如下布局:

my-product/
├── west.yml                 # 清单:钉住 zephyr / mcu_boot / hal_* 的 commit
├── CMakeLists.txt            # find_package(Zephyr) + target_sources
├── prj.conf                  # 基础 Kconfig:所有板子共用的配置
├── VERSION                   # 版本号(签名与上报用)
├── app.overlay               # 基础 overlay:所有板子共用的硬件变更
├── boards/
│   ├── <board1>.conf         # 板级配置:只放该板特有项
│   ├── <board1>.overlay      # 板级 overlay:引脚 / 外设差异
│   └── custom_board/         # 自定义板:dts / defconfig / yaml / Kconfig.board
├── src/
│   ├── main.c                # 应用入口
│   ├── ble/                  # 按子系统分目录,各自独立编译
│   │   └── CMakeLists.txt
│   ├── sensors/
│   │   └── CMakeLists.txt
│   └── storage/              # 存储业务逻辑
│       └── CMakeLists.txt
├── include/                  # 对外头文件
├── drivers/                  # out-of-tree 驱动(不进 Zephyr 源码树)
│   └── my_flash/
│       ├── CMakeLists.txt
│       ├── my_flash.c
│       └── dts/bindings/vendor,my-flash.yaml
├── tests/                    # ztest 用例与 testcase.yaml
│   ├── CMakeLists.txt
│   └── prj.conf
├── scripts/                  # 构建、烧录、签名辅助脚本
└── child_image/              # 子镜像配置(如 MCUboot 的 Kconfig 片段)
    └── mcuboot.conf
为什么 src/ 要按子系统再分目录

这不是洁癖:每个子目录一个 CMakeLists.txt 意味着一个子系统由一个人负责,两个工程师改不同子系统时几乎不会产生合并冲突。 把所有源文件堆在一个 target_sources 列表里,短期省事,长期会在团队规模变大后成为冲突重灾区。

2.5 west 常用命令速查

命令作用常用场景
west init [-l 目录]初始化或接管工作区首次搭建 / 接管已有仓库
west update拉取并切到 manifest 声明的 revision初始化后;升级 Zephyr 后
west list列出各 project 及当前 revision确认版本是否对齐
west build -b <board> <app>构建指定板的应用日常构建
west flash烧录上次构建的产物上板验证
west debug启动调试器并停在入口调崩溃 / 断点
west attach附加到运行中的目标看活体状态
west build -t menuconfig交互式配置裁剪功能、查依赖
west build -t run构建并运行(native_sim / qemu)主机仿真
west build -t pristine清理构建目录后重建改了 Kconfig 却不生效时
west manifest --resolve更新 west.yml 锁定的 revision主动升级依赖
west compare对比 project 之间的差异跨仓库定位改动
改了 Kconfig 却没生效

这是 Zephyr 里最高频的「诡异现象」:Kconfig 的依赖关系变了,但 CMake 缓存不知道。 解决办法是 west build -t pristine 清掉构建目录重建(不要手动去删 .config,删不干净)。 养成习惯:凡是改过 Kconfig 或新增驱动目录,验证前先 pristine 一次。

2.6 构建产物与烧录

Zephyr 不支持 in-tree 构建,产物全部落在独立的 build/ 目录。知道每个产物是什么、留哪个做现场分析,才能在出问题时不至于抓瞎。

产物路径用途
合并后设备树build/zephyr/zephyr.dts排查设备树问题的第一站;可看到 overlay 合并后的最终结果
生成头文件build/zephyr/include/generated/含 devicetree_unfixed.h 与 autoconf.h(最终生效的 CONFIG_)
elf 产物build/zephyr/zephyr.elf调试与栈回溯解析;量产归档必需
bin 产物build/zephyr/zephyr.bin烧录用(无符号)
链接映射build/zephyr/zephyr.map查 ROM/RAM 占用、定位大符号、分析栈大小
二进制尺寸build/zephyr/zephyr.stat各段占用明细,配合 16.1 做预算
配置快照build/zephyr/.config复现构建、排查宏定义差异
建议:量产版本一并归档

把每次量产的 zephyr.elf、zephyr.map、.config、zephyr.dts 四件套一并归档。 线上崩溃时,只有与现场固件完全一致的 elf 才能正确解析栈回溯——版本对不上的符号表解析出来的行号全是错的,等于没有信息。

2.7 native_sim 主机仿真

Zephyr 有一个常被忽视的能力:把整个系统编译成宿主机原生程序,在 PC 上跑起来。板级代码与真实外设不可用,但内核、驱动逻辑、协议栈、存储子系统都能真实执行。

# 编译为宿主机程序并直接运行(无需任何硬件)
west build -b native_sim ~/zephyrproject/my-product
west build -t run

# 常用配套手段
#   - 单元测试:把被测逻辑写成 ztest,用 native_sim 跑
#   - 逻辑层:协议解析、状态机、算法、数据结构都能在此验证
#   - 配合 CONFIG_ARCH_POSIX,可用 ptrace/gdb 调宿主程序
能测什么,不能测什么

能测:内核行为、驱动状态机与错误处理、协议栈与解析逻辑、文件系统读写、并发与竞态、内存泄漏(宿主工具链友好)。 不能测:真实时序、电气特性、Flash 与 SRAM 的真实磨损、无线性能、中断延迟、功耗。 经验做法是:把「逻辑正确性」在 native_sim 上穷尽验证,把「时序与电气」留到真板压测,两边职责不重叠。

3 · 内核线程与调度

这一章解决:线程是怎么创建与切换的、优先级怎么设才算合理、栈与内核内存从哪来、多核下要注意什么。线程模型没想清楚,后面的并发缺陷与环境无关 bug 会集中爆发。

3.1 线程模型与生命周期

Zephyr 内核是可抢占的实时内核:默认按优先级抢占,支持协作式调度(优先级设为负数)与时间片(可选)。一个应用在编译期最多可用的线程数由配置决定,运行时由内核统一管理。

Zephyr 线程状态机与迁移条件就绪 READY等待被调度运行 RUNNING占用 CPU阻塞 BLOCKED等对象 / 延时挂起 SUSPENDk_thread_suspend终止 TERMINATEDk_thread_abort抢占阻塞就绪让出挂起恢复abort关键点:Zephyr 的阻塞是「对象驱动」的线程阻塞在 mutex / sem / fifo / queue / poll 上;对象可用时自动进就绪并触发一次抢占判定。因此「忘记唤醒」是 Zephyr 最典型的死锁成因,且往往不报错——只能靠 4.4 节的检查手段发现。
图 5 · Zephyr 线程状态机:就绪 / 运行 / 阻塞 / 挂起 / 终止与迁移条件

创建线程的基本形态如下,注意启动延迟与栈大小两个参数:

#include <zephyr/kernel.h>

/* 线程入口:返回即终止 */
static void sensor_thread(void *a, void *b, void *c)
{
    while (1) {
        /* 周期性工作 */
        sample_and_process();
        k_msleep(1000);       /* 让出 CPU,进入阻塞态 */
    }
}

/* 静态方式:栈与控制块由应用提供,链接期确定,无运行时分配 */
K_THREAD_STACK_DEFINE(sensor_stack, 2048);
static struct k_thread sensor_thr;
static struct k_sem sensor_sem;

K_THREAD_DEFINE(sensor_tid, 2048, sensor_thread, NULL, NULL, NULL,
                5, 0, 500);    /* 优先级 5、启动延迟 0、立即启动 */

/* 动态方式:栈由内核内存池分配,适合数量与栈大小运行期才确定的场景 */
int sensor_start(void)
{
    k_tid_t tid = k_thread_create(&sensor_thr, sensor_stack,
                                  K_THREAD_STACK_SIZEOF(sensor_stack),
                                  sensor_thread, NULL, NULL, NULL,
                                  5, 0, K_NO_WAIT);
    if (tid == NULL) {
        return -EAGAIN;   /* 内存不足 */
    }
    return 0;
}
静态优先于动态

能用 K_THREAD_DEFINE 就不要用 k_thread_create。 静态方式栈在链接期确定:不会被运行时内存不足打断、启动时序可预测、map 文件里能直接查到栈位置,栈溢出也更容易用 CONFIG_THREAD_STACK_SENTINEL 定位。 动态方式只在「线程数量或栈大小由运行期输入决定」时使用(例如按不同产品型号加载不同功能)。

3.2 优先级与抢占模型

Zephyr 的优先级数值越小越优先,范围通常是 -1 到 NUM_PRIORITIES-1。其中负值表示协作式(不可被抢占,只在显式让出时切换),非负表示抢占式。

优先级范围类型抢占行为典型用途
负数(如 -1)协作式不会被任何线程抢占,只能主动让出或阻塞延迟敏感、必须按序跑完的处理线程
0(默认)抢占式可被优先级更高者抢占;同级按配置的时间片轮转普通后台线程(数量最多的一档)
1 ~ 15抢占式(高)优先级高于 0 档,优先获得 CPU采样、I/O 收发、控制回路等有实时要求的线程
ISR 优先级中断优先级与线程优先级是两套独立体系见 9.2;高优先级 ISR 可打断任何线程

三条排优先级的实用准则

  1. 线程优先级数量控制在 5~7 档以内。档位越多,每档至少一个线程,栈与内存开销线性增长;档位过密则优先级反转的复杂度上升。
  2. 0 档留给「普通后台」,所有不需要实时性的业务线程都放这一档;实时线程只给真正有时序要求的那几条。常见错误是给每个外设都开一个高优先级线程,导致高优先级线程长期霸占 CPU。
  3. 用完高优先级必须主动阻塞:实时线程做完事就 k_msleep 或等信号量,不要空转等待。红线 空转的高优先级线程会让所有低优先级线程饿死,是「系统偶尔卡住」的头号原因。
优先级继承(priority inheritance)如何避免优先级反转时间线t0t1(H 请求锁)t2(L 释放锁)低优先级 L拿锁 → 被抢占中优先级 M空转高优先级 H等锁 → 继承 L 的优先级L 继续执行并释放锁(受益)优先级数值越小越高:-1 协作 0 默认 1~15 抢占无继承时的后果:L 被 M 抢占 → H 一直等 L 的锁 → L 永远拿不到 CPU → 系统卡死启用优先级继承后:H 等待时临时提升 L 的优先级,L 尽快跑完释放锁,H 立即获得。
图 6 · Zephyr 优先级与抢占时间线
红线:绝不在高优先级线程里做阻塞等待

高优先级线程里写 while (!ready) { k_msleep(10); } 这种「轮询 + 延时」看似安全,实际问题是它把系统最低响应时间拉长到 10 ms,且掩盖了真正的同步设计缺失。 正确做法是用同步原语阻塞等待(k_sem_take / k_msgq_get / k_fifo_get),让 CPU 在等待期间真正释放。

3.3 调度器实现机制

Zephyr 的调度器有两个实现可换,行为差异明显:

实现配置宏机制适用场景
Meta IRQ(默认)无(默认)调度逻辑在内核态 IRQ 上下文中完成,内核不占用普通中断号绝大多数 MCU 应用;延迟可预测,调试直观
EDF(最早截止期优先)CONFIG_SCHED_EDF每个线程带截止期,按最早截止期调度;支持 k_thread_deadline_set()需要「周期性截止期」语义的任务;TWS(时间触发)场景
时间片轮转CONFIG_TIMESLICING同级线程按固定时间片轮流执行同级任务需公平推进时开;会增加切换开销

调度器锁存点(z_reschedule)发生在中断退出、阻塞 API 返回、显式让出三个时机。理解这一点能解释一个常见现象:为什么在中断里改优先级后,被提升的线程不是立刻运行,要等中断返回——因为调度判定在中断退出时统一做。

/* 典型正确用法:中断只做「标记 + 唤醒」,重活放工作队列 */
static void sensor_isr(const struct device *dev)
{
    struct sensor_ctx *ctx = dev->data;
    /* ISR 内允许:读设备状态、置标志、k_sem_give / k_work_enqueue */
    k_sem_give(&ctx->data_ready);
    /* ISR 内禁止:k_mutex_lock、k_msleep、打印、动态内存分配 */
}

3.4 线程栈与内核内存

Zephyr 的栈有两种来源,这与 FreeRTOS 常见做法不同,也是最易踩坑的地方。

栈来源配置宏内存归属特点与注意
主栈(系统栈)CONFIG_MAIN_STACK_SIZE由 CONFIG_MAIN_STACK_SIZE 决定,可由 CONFIG_USERSPACE 影响内核自身与 main 线程使用;主栈溢出是最高频的崩溃原因
线程栈CONFIG_THREAD_STACK_INFO 等由 k_thread_create 从系统内存池分配栈大小由你指定;SMP 下每核另有内核栈
静态栈K_THREAD_STACK_DEFINE链接期分配在 BSS 段栈溢出可用 sentinel 检测(见 8.4)
红线:主栈不是「够用就行」

CONFIG_MAIN_STACK_SIZE 的默认值通常只够跑最小示例。一旦应用里有深层调用链(协议栈解析、文件格式化、JSON 解析、printf 浮点),默认主栈很快就不够。 典型崩法是:平时好好的,一收到特定报文就 HardFault,栈回溯显示 printf 或解析函数——这类问题的根因几乎总是主栈偏小。 做法:把主栈调到 4 KB 起步,并打开栈 sentinel 观察实际水位(见 8.4、16.2)。

3.5 SMP 与多核支持

Zephyr 支持对称多核(SMP),但多核支持需要 SoC 与板级配合:SoC 层需实现 CONFIG_SMP 下的 CPU 启动与中断路由,板级 DTS 需描述各核(通常是 cpus 节点)。

事项单核(默认)多核(CONFIG_SMP)注意
内核栈一个主栈每核一个内核栈各核栈不能互相踩;SMP 下主栈归属需确认
线程亲和性不需要k_thread_cpu_set() 绑定到指定核绑定后可减少 cache 抖动与伪共享
全局变量普通变量即可需 atomic_t / 原子操作保护跨核共享普通 int 的 ++ 在多核下不安全
中断路由单一中断向量表需 CPU 接口层指定目标核由 SoC 层实现,不是应用层关心的事
实测建议绝大多数 MCU 应用无需 SMP只在有明确并行收益时开启SMP 会显著增加调试难度与栈占用
经验:能单核就别上 SMP

SMP 带来的调试复杂度上升(竞态出现概率、性能不可复现、栈水位难评估)通常大于它带来的收益。 单核下用「多线程 + 优先级 + 工作队列」已经能榨干绝大多数 MCU 的并发能力。 确有并行收益时(例如双核分别跑协议栈与控制),再开启 SMP 并认真做亲和性规划。

4 · 同步原语与线程通信

这一章解决:用什么原语保护共享资源、怎么在线程间传数据、死锁与优先级反转怎么防。并发缺陷是 Zephyr 系统里最难复现也最难查的一类问题,核心纪律是「提前设计」而不是「事后调试」。

4.1 互斥量与信号量

Zephyr 的互斥类原语有四种,最常用的两个是 k_mutex 与 k_sem,选错会导致隐性 bug。

Zephyr 同步原语:按「保护什么」与「等什么」两类选互斥类:保护共享资源的互斥访问k_mutex() · 互斥量线程上下文,可加超时,优先级继承k_sem() · 信号量计数信号量,ISR 可 give,线程 takek_spinlock() · 自旋锁中断上下文用,优先级禁止同级atomic_t() · 原子变量轻量自增/比较交换,不睡传递类:在线程间传递数据或事件k_msgq() · 消息队列定长消息,按值收发,可加超时k_fifo() · FIFO字节流,ISR 可 put,内核不分配消息k_poll() · 信号量接口设备与文件系统统一的通知机制k_event() · 事件位多位标志广播,用于状态变化通知
图 7 · Zephyr 同步原语全景:互斥类(mutex/sem/spinlock/atomic)与传递类(msgq/fifo/poll/event)
原语可否在 ISR 中使用是否睡眠优先级继承典型用途
k_mutex否是有(默认)保护共享数据结构、驱动内互斥
k_semgive 可以,take 不行take 时睡无(不持有锁)ISR 通知线程、同步节拍
k_spinlock可以(两种上下文)否无中断与线程共享的小临界区
atomic_t可以否无计数器、标志位、简单状态
红线:k_mutex 不能在中断里用

k_mutex_lock() 在 ISR 中调用是确定的错误:它可能让出 CPU,而中断上下文不允许让出。 这与 FreeRTOS 中「中断里不能用 xSemaphoreTake(..., portMAX_DELAY)」是同一条纪律,只是 Zephyr 直接禁用更彻底。 中断里要发信号就用 k_sem_give()(它不阻塞),或更推荐直接把工作项丢进工作队列(见 12.1)。

另外三条容易忽略的细节:

  • 一个锁只保护一个资源。不要用一把全局大锁保护所有共享数据——正确做法是「每个资源一把锁」,减少锁竞争与持锁时间。
  • 解锁必须用 k_mutex_unlock 且路径唯一。用 goto 汇到统一出口解锁,是 C 语言里最稳的写法。
  • k_sem 没有优先级继承。所以它不能用来保护共享资源,只能用来「通知」。用信号量做互斥是常见且隐蔽的错误。
/* 推荐写法:单资源单锁 + 统一出口解锁 */
struct dev_ctx {
    struct k_mutex lock;          /* 只保护本驱动的状态 */
    struct device_state state;
    bool busy;
};

static int dev_write(struct dev_ctx *c, const void *buf, size_t len)
{
    int rc = 0;

    if (k_mutex_lock(&c->lock, K_FOREVER) != 0) {
        return -EBUSY;
    }
    if (c->busy) {                 /* 业务条件判断也放在锁内 */
        rc = -EBUSY;
        goto out;                  /* 统一出口,避免漏解锁 */
    }
    c->busy = true;
    /* 真正的寄存器操作放在这里,且尽量短 */
    hw_write(buf, len);

out:
    if (rc == 0) {
        c->state = STATE_IDLE;
    }
    c->busy = false;
    k_mutex_unlock(&c->lock);
    return rc;
}

4.2 消息队列与 FIFO

需要在线程间传数据时用消息队列或 FIFO,而不是轮询共享变量 + 标志位。

原语传递内容内核是否分配ISR 可用适用
k_msgq定长结构体,按值拷贝发送时需用户先给缓冲池put 可以控制命令、传感器采样值、状态包
k_fifo字节流不分配,用户提供的缓冲put 可以串口收发的环形缓冲、日志输出
k_fifo_alloc字节流内核分配缓冲put 可以不想自己管缓冲时的省事选择

两条实战要点:

  1. 优先用 k_msgq 而非「环形缓冲 + 锁」。手写环形缓冲的锁保护与满/空判断是 bug 高发区;消息队列把这些边界处理内建了。
  2. 消息体不要直接传大结构体指针,除非你保证指针指向的对象在接收侧处理完之前不会被释放。传指针的风险是生命周期不受控——这是嵌入式里最难查的一类 use-after-free。
/* 推荐:定长消息按值传递,生命周期完全可控 */
struct sensor_sample {
    uint32_t timestamp;   /* ms */
    int16_t  temp_c10;    /* 温度 x10,避免浮点 */
    uint16_t humidity_c100;
};

K_MSGQ_DEFINE(sample_q, sizeof(struct sensor_sample), 8, 4);

/* 生产者(可以是线程或工作队列) */
static void on_sample_ready(const struct sensor_sample *s)
{
    /* 非阻塞发送:队列满时返回 -ENOMSG,由调用方决定丢弃还是覆盖 */
    int rc = k_msgq_put(&sample_q, s, K_NO_WAIT);
    if (rc == -ENOMSG) {
        /* 必须处理「满了」:丢弃最旧、覆盖新值、或降采样 */
    }
}

/* 消费者线程 */
static void consumer_thread(void *a, void *b, void *c)
{
    struct sensor_sample s;
    while (1) {
        if (k_msgq_get(&sample_q, &s, K_FOREVER) == 0) {
            process(&s);
        }
    }
}
队列满了怎么办:必须有明确策略

「队列满时丢最新」还是「丢最旧」还是「触发降采样」——这是必须由业务决定的策略,不能留空。 Zephyr 的 k_msgq_put 在队列满且传 K_NO_WAIT 时返回 -ENOMSG,不会阻塞、不会覆盖,所以这个分支一定会被走到。 把处理写进代码里,而不是靠日志事后发现数据丢了。

4.3 事件位与广播

当一个状态变化需要通知多个订阅方时,用事件位(k_event)比广播指针更安全——它只传递「发生了什么」,不传递「数据在哪」。

/* 事件位:只传状态,不传指针,从根上避免悬垂指针 */
#define EVT_SENSOR_READY   BIT(0)
#define EVT_SENSOR_ERROR   BIT(1)
#define EVT_LOW_BATTERY    BIT(2)

/* 发送方:设置位并广播 */
k_event_set(&app_events, EVT_SENSOR_READY);

/* 接收方:等待任意位或指定位 */
uint32_t events = EVT_SENSOR_READY | EVT_SENSOR_ERROR;
k_event_wait(&app_events, events, false, K_FOREVER);

/* 非阻塞轮询版本:适合在主循环里检查 */
if (k_event_test(&app_events, EVT_LOW_BATTERY)) {
    handle_low_battery();
}

k_event 的价值在于把「状态」与「数据」分离:接收者被唤醒后,自己决定去哪里读数据(例如从一个受锁保护的上下文中读)。 这比传递指针更安全,也更容易扩展(新增订阅者不改发送方)。

4.4 死锁与优先级反转

并发缺陷有两类典型形态:死锁是「双方互等」的直接卡死,优先级反转是「高优先级被低优先级间接阻塞」的隐性饥饿。两者都必须在设计阶段消除,靠调试是碰运气。

两类并发缺陷:死锁(互等)与优先级反转(隐性饥饿)死锁:两个线程各持一把锁再等对方线程 A持有 lock1线程 B持有 lock2A 等 lock2永不释放 lock1B 等 lock1永不释放 lock2解法:固定全局加锁顺序(按地址排序)解法:缩小临界区,锁内不做阻塞操作解法:用 k_mutex_lock(..., K_NO_WAIT) 先试探症状:全部线程停住,CPU 空转或进入 idle优先级反转:高优先级被低优先级间接阻塞L 低优先级持有锁M 中优先级抢占 L 霸占 CPUH 高优先级等 L 的锁 → 拿不到解法:k_mutex 启用优先级继承(默认开启)解法:缩短临界区,让 L 尽快释放症状:H 的响应时间随机变大,非确定性
图 8 · 并发缺陷:死锁的四种解法与优先级反转的两种解法

死锁的预防清单

  • 全局固定加锁顺序:需要同时持多把锁时,按锁地址(或编号)升序加锁,绝不反序。
  • 临界区内不做阻塞操作:锁内禁止 k_msleep、k_msgq_get(K_FOREVER)、printf、动态分配。
  • 用带超时的获取:关键路径用 k_mutex_lock(&lock, K_MSEC(50)),超时后走降级路径,而不是无限等待。
  • 用 K_NO_WAIT 试探:不可重入的场合先试探,失败就返回 -EBUSY 交给上层重试,避免原地死等。
  • 锁的数量控制在最小:能用单锁解决就不要两锁;需要两锁时优先考虑「合并成一个锁 + 临界区内处理」。

优先级反转的预防清单

  • 保护共享数据一律用 k_mutex(默认开启优先级继承),不要用 k_sem 做互斥。
  • 缩短持有时间:持锁期间只做「必须原子完成」的最短操作,耗时计算放到锁外。
  • 不要在锁内做 I/O:Flash 读写、SPI 传输、串口发送都可能长时间占用总线,放在锁内会显著拉长临界区。
  • 提高共享资源的持有者优先级:如果某资源被一个固定线程独占,可把该线程优先级提到使用者之上。
经验:把「锁」收敛到少数几处

代码里 k_mutex_lock 出现次数的多少,直接决定并发缺陷的概率。 健康的做法是让共享状态集中在一两个上下文里(例如一个驱动线程 + 一个工作队列),其余部分通过消息传递交互——这样锁的数量自然被压到最低,也更容易验证正确性。

4.5 原语选型决策表

你要做的事选这个不要用原因
ISR 通知线程有数据了k_sem_give 或 k_work_enqueuek_msgq_put(可能分配)ISR 内应避免可能分配内存的调用
保护驱动的设备状态k_mutexk_sem信号量无优先级继承,且语义上不表达「互斥」
中断与线程共享一个计数器atomic_tk_mutex自旋锁在中断里也不该长时间持锁,原子操作更短
给线程传一条控制命令k_msgq共享全局变量 + 标志位边界处理(满/空)内建,避免竞态
缓冲串口不定长收发的字节流k_fifok_msgq 逐字节字节流场景 FIFO 更贴合,零额外分配
多个模块都要知道「状态变了」k_event传递指针回调只传状态不传指针,无生命周期风险
跨模块读取当前状态受锁保护的 getter直接暴露全局变量直接暴露会让所有调用方都成为潜在竞态源
相关报告 · 交叉引用 5 器件全部展开收起 ▾
点开类目即可展开:具体文档 / 章节(点章直接跳转到该章)
eMMC
电路设计指南
软件设计规范
Linux 驱动开发
样机问题排查
量产问题排查
SPI NOR
电路设计指南
软件设计规范
Linux 驱动开发
样机问题排查
量产问题排查2
系统设计指南
SPI NAND
电路设计指南
软件设计规范
Linux 驱动开发
样机问题排查
量产问题排查4
全志平台 SPI NAND NOR
章节(点章跳转,共 11 章)
海思平台 SPI NAND 量产常见失败
章节(点章跳转,共 11 章)
烧录成功但不开机 SPI NAND
章节(点章跳转,共 12 章)
瑞芯微平台 SPI NAND eMMC
章节(点章跳转,共 11 章)
系统设计指南3
DDR/LPDDR(DRAM)
电路设计指南4
软件设计规范4
DDR2
章节(点章跳转,共 16 章)
DDR3
章节(点章跳转,共 16 章)
DDR4
章节(点章跳转,共 17 章)
DDR5
章节(点章跳转,共 17 章)
SD NAND
电路设计指南
软件设计规范
Linux 驱动开发
量产问题排查
其他提及器件(共 2)
PPI NAND(并行NAND)
电路设计指南
软件设计规范
Linux 驱动开发
量产问题排查