只读分享解决方案文档原理方案设计📅 2026-10-05🔖 Rev 1.4⏳ 26天有效(至 2026-11-05)
🔧解决方案&应用市场分析 -> 原理方案设计 -> 嵌入式驱动&系统开发 -> Zephyr_系统开发指南

Zephyr_系统开发指南

生成时间 2026-10-06 15:32:39 有效期至 2026-11-05 15:32:39(26天有效(至 2026-11-05))
同系列 · 原理方案设计

Zephyr 系统开发指南

编制日期 2026-10-05

面向 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直接暴露全局变量直接暴露会让所有调用方都成为潜在竞态源
扫码打开本页
微信扫码 · 展会可扫

再分享给同事

同事可直接打开这份资料《Zephyr_系统开发指南》,在线阅读;完整章节请下载芯参谋。