在嵌入式实时系统里,Zephyr 这几年出现频率越来越高。它不是普通的 RTOS,而是由 Linux 基金会托管、面向物联网和多架构场景的开源实时操作系统。很多人在选型时会把它和 FreeRTOS 放在一起比较,但真正动手之后,第一个卡住的地方往往不是功能,而是环境搭建、west 工作流和 Kconfig 配置。这篇文章会围绕 Zephyr 的特殊点、环境搭建、Kconfig、与 FreeRTOS 的选型对比,以及一套实际可复现的跑通流程来展开,适合准备入门 Zephyr、或者正在做嵌入式项目选型的开发者阅读。
我先把结论放在前面:Zephyr 最值得关注的不是“它也是 RTOS”,而是它从设计上就把内核模块化、配置系统、硬件描述和大量物联网子系统组合在了一起。可以把它理解成一个“能裁剪的嵌入式 Linux 式工程框架”,而不是简单调度器。好处是复用能力极强,坏处是学习曲线比 FreeRTOS 陡。下面按实际落地顺序拆开讲。
1. Zephyr 特殊在哪:它不是“又一个小 RTOS”
1.1 内核模块化,配置是编译期的,不是运行时初始化
很多传统 RTOS 的内核功能是通过头文件宏、条件编译和不同的移植层来实现的。Zephyr 的做法更彻底:它把几乎所有能力都做成编译期配置项,用 Kconfig 来控制。你不需要在代码里写大量#ifdef,而是在prj.conf里声明CONFIG_XXX=y,然后在构建时真正决定某个文件、某个模块、某个驱动是否进入镜像。
这样做最大的好处是,代码里能看到完整的组件分布,不用靠“项目里有没有某个文件”来猜测功能是否启用。比如日志、栈回溯、shell、网络协议栈、蓝牙、传感器驱动,都可以按需打开或关闭。
但也有代价。第一次用 Zephyr 的人,经常会在配置阶段迷失。因为配置符号之间还有依赖关系:你打开了一个功能,它可能又依赖另一个功能;另一个功能又要求某个驱动或某段内存区域。这和传统 RTOS“直接把源码加进工程”的思路完全不同。
从工程角度看,这种设计更接近大型软件项目。开发者写的是“应用代码”,而硬件平台、内核裁剪、驱动选择被分离成独立层。对产品要长期维护、多硬件复用的场景来说,这个优势会越来越明显。
1.2 devicetree:让硬件描述和驱动逻辑分开
除了 Kconfig,Zephyr 另一个特殊点是 devicetree。简单说,它就是一套用 dts 文件描述硬件拓扑的机制:板子上有几个 GPIO、UART、I2C、SPI,引脚连接在哪个控制器上,中断号是多少,都可以用节点和属性写清楚。
驱动代码只需要去 dts 节点上读属性,而不是在驱动里写死板级信息。这样同一个驱动,可以复用到不同板卡上。更换硬件时,改 dts 或 dts overlay,比改 C 代码要直观得多。
这是 Zephyr 区别于很多 RTOS 的关键点。也用到一个常见直觉:你写驱动时不再关心“这个芯片挂在哪个地址”,而是关心“有哪些属性、是否使能”。硬件差异被下沉到配置文件后,应用代码可以保持相对稳定。
1.3 适合谁,不适合谁
Zephyr 适合这样几类场景:
- 产品目标是多型号、多硬件平台,希望尽量复用驱动和中间件。
- 项目需要蓝牙、Wi-Fi、Zigbee、Thread、MQTT、CoAP 等通信能力,不想自己从头移植协议栈。
- 团队希望用统一构建流程管理多个板级项目,而不是每个开发板一套独立 Makefile。
- 对安全、用户态、虚拟内存、TF-M 等高级能力有长期规划。
如果只是做一颗 MCU 上的简单灯控、单任务采集,或者团队成员非常熟悉某款轻量 RTOS、项目周期短且没有扩展需求,Zephyr 可能反而会显得重。低配置 MCU 能跑 Zephyr 不代表适合所有芯片,ram 和 flash 资源偏紧、J-Link 调试流程固定、产品生命周期短,这些情况要慎重。
2. 环境搭建:为什么 Zephyr 的第一个坑是 west 而不是编译器
2.1 在 Linux 上准备依赖
Zephyr 官方推荐 Linux 或 macOS 作为开发主机,Windows 下可以通过 WSL 使用。我第一次跑通时用的就是 Ubuntu。先准备基础依赖,这里以 Ubuntu 为例:
sudo apt update sudo apt install --no-install-recommends git cmake ninja-build gperf \ ccache dfu-util device-tree-compiler wget \ python3-dev python3-pip python3-setuptools python3-tk python3-wheel xz-utils file \ make gcc gcc-multilib g++-multilib libsdl2-dev libmagic1装完之后,再确认 Python 版本。Zephyr 构建脚本依赖 Python3 和 west 工具,如果你的系统里有多个 Python 版本,后面容易出问题。建议先用python3 --version确认环境,再使用pip3 install west。
这些依赖里面,cmake、ninja、gperf、dtc是构建的关键。device-tree-compiler用来处理 devicetree,gperf用来生成部分查找表,libsdl2-dev用于某些 QEMU 图形界面。缺一个,构建时可能报奇怪的错误,所以最好一次装全。
2.2 用 west 管理源码和 SDK
Zephyr 不推荐“去官网下载一个 release 源码包然后手动解压”,而是用 west 来管理整个 multi-repo 环境。
west 的作用不只是下载代码,它还能管理多个模块之间的依赖关系,包括 Zephyr 内核、可选模块、第三方库和 hal,然后统一构建。你可以把 west 理解成嵌入式领域的“包管理 + 构建入口”。
初始化一个标准工作目录:
mkdir ~/zephyrproject cd ~/zephyrproject west init -m https://github.com/zephyrproject-rtos/zephyr west updatewest init会拉一个最小的 manifest,west update再按 manifest 把其他仓库同步下来。这一步会花不少时间,和网络状况有关。同步完成后,目录里会有zephyr、bootloader、modules、tools等子目录。
然后是 Zephyr SDK。SDK 里包含交叉编译器、QEMU、OpenOCD 和一些调试工具。到 Zephyr 官方 SDK 发布页下载对应主机架构的安装包,例如zephyr-sdk-x.y.z_linux-x86_64.tar.xz,解压后运行:
cd zephyr-sdk-x.y.z ./setup.sh -c-c表示只安装命令行工具,不安装带图形界面的工具。如果你后面要调试目标板,再根据实际情况补充安装。
接着设置环境变量。在~/.bashrc或~/.zshrc里加入:
export ZEPHYR_TOOLCHAIN_VARIANT=zephyr export ZEPHYR_SDK_INSTALL_DIR=$HOME/zephyr-sdk-x.y.z source ~/zephyrproject/zephyr/zephyr-env.sh如果使用的是 west 命令构建,其实很多环境变量会自动处理,但显式设置 SDK 路径能减少疑惑。
2.3 跑通最小编译:hello_world on QEMU
环境准备好后,先不要碰自己的板子,先用 Zephyr 自带的 sample 确认构建链路是通的。
cd ~/zephyrproject/zephyr west build -b qemu_x86 samples/hello_world west build -t run如果一切正常,QEMU 会启动,并在终端输出Hello World! qemu_x86之类的内容。这一步能编译通过,说明编译器、CMake、west、Kconfig、devicetree 工具链都已经正常。
这里有一个判断标准:第一次跑通时,不要追求复杂功能,只要确认“构建能完成、QEMU 能启动、日志能输出”。如果卡在这一步,多半是环境问题,不用怀疑 Zephyr 本身。
2.4 环境类问题的通用排查顺序
环境搭建阶段最容易出问题的点,我按优先级排:
- Python 版本和 west 版本。
west --version能跑,但可能加载了错误的 Python 环境。 - 环境变量。
ZEPHYR_BASE是否指向 Zephyr 源码根目录。 - CMake 版本。Zephyr 对 CMake 有最低版本要求,版本过低会直接配置失败。
- SDK 路径。
ZEPHYR_SDK_INSTALL_DIR设置错误会导致找不到交叉编译器。 - 依赖缺失。
dts相关工具缺失时,会在 devicetree 处理阶段报错。
不要在没确认这些项目前就去改 Zephyr 源码。工具链没问题,后面问题才容易定位。
3. Kconfig 和 Workbench:配置系统才是 Zephyr 的“大脑”
3.1 Kconfig 不是简单的宏开关
Kconfig 在 Linux 内核里很常见,Zephyr 也使用了这套体系。它不只是打开或关闭某个宏,而是用树形依赖结构来描述“配置项之间的关系”。
每个配置项叫一个 symbol,比如:
config LOG bool "System log" default y这里表达的是“有一个 LOG 开关,默认打开”。依赖关系可以写成:
config LOG_MODE_IMMEDIATE bool "Immediate log mode" depends on LOG意思是不打开 LOG,就不会出现 LOG_MODE_IMMEDIATE。Zephyr 构建时会先分析这些依赖,再生成最终配置结果,进而决定哪些源码文件需要参与编译、哪些头文件被包含。
普通项目里,你一般只需要改prj.conf里的少量 symbol,比如日志级别、栈大小、子系统开关。但工程结构更大时,就会涉及 board 默认配置、应用配置、overlay 配置的叠加。规则是:越靠后的配置优先级越高,最终值会覆盖默认值。
3.2 menuconfig 和 workbench 类工具怎么用
纯粹靠手写prj.conf去探索配置项,效率很低。你很难记住某个配置的名字,也容易忽略依赖。Zephyr 自带一个基于终端菜单的配置界面:
west build -b qemu_x86 samples/hello_world -t menuconfig运行后,你可以按菜单层级浏览所有配置项,搜索某个 symbol,看到它的帮助信息、取值范围和依赖关系。对新手来说,这是比直接翻文档更快的配置学习方式。
如果你更习惯图形界面,也可以使用 IDE 插件或 workbench 类的 Kconfig 辅助工具。它们通常提供配置项搜索、依赖高亮、冲突提示,甚至生成可视化的依赖图。核心作用都一样:让你在配置复杂依赖时少犯错。
这里要提醒一句:Kconfig 工具能帮你写出合法配置,但不能保证这个配置适合你的硬件。比如CONFIG_MAIN_STACK_SIZE调大确实可能解决栈溢出,但会多占 ram。你需要结合日志、链接脚本和实际资源占用来判断,而不是只看配置界面。
3.3 一个实际配置例:调整日志、栈和驱动
假设我要在项目里打开系统日志和 shell,并调整主线程栈大小,可以在prj.conf里写:
CONFIG_LOG=y CONFIG_SHELL=y CONFIG_MAIN_STACK_SIZE=4096然后构建时,Zephyr 会从“默认配置 + board 默认配置 + prj.conf 配置”合成一套最终配置。你可以用下面命令查看最终结果:
west build -t menuconfig在菜单里找到对应选项,确认它已经从 n 变为 y,或者数值已经变化。
如果我要覆盖某个板级 dts 的引脚配置,通常会新建一个app.overlay文件:
/ { aliases { led0 = &led0; }; leds { compatible = "gpio-leds"; led0: led_0 { gpios = <&gpio0 5 GPIO_ACTIVE_HIGH>; label = "LED 0"; }; }; };然后在west build时通过-d指定 build 目录,或把 overlay 文件放在工程根目录下,让构建脚本自动识别。具体文件名和路径在不同版本里可能有差异,落地时看当前文档即可。
3.4 配置不生效时的常见原因
配置不生效,通常不是 Zephyr 没读取prj.conf,而是以下几个问题:
- 符号名写错。比如
CONFIG_MAINSTACK_SIZE少了个下划线,构建不会报错,但也不会生效。 - 被依赖条件拦截。某个 symbol 依赖其它 symbol 未打开,因此自动被隐藏或覆盖。
- 配置来源优先级冲突。同一条配置在 board 默认配置里被强制设定,应用配置无法覆盖。
- 改完没有重新构建。个别情况下,增量构建没有重新生成
autoconf.h,可以先west build -t clean或删除 build 目录。
遇到配置不生效,先执行west build -t menuconfig打开配置界面,搜索这个 symbol,看当前值和依赖原因。这是最快的定位方式。
4. Zephyr vs FreeRTOS:2026 年选型,到底在比什么
4.1 两者的定位差异
FreeRTOS 的定位是“轻量、简单、移植广泛”。它内核小,上手快,资料多,几乎任何 MCU 项目都能快速用起来。对于 8 位、16 位、小内存 32 位 MCU,或者团队对 RTOS 没有太多定制需求,FreeRTOS 是很稳的选择。
Zephyr 的定位则更接近“全功能物联网嵌入式 OS”。它不仅有内核,还有协议栈、文件系统、设备驱动模型、固件更新、安全子系统,甚至支持 MPU/MMU、用户态和虚拟内存。它更重,但可裁剪性也强。
从 2026 年项目选型角度看,关键问题是:你的产品未来要面对哪些需求?如果只是简单任务调度和信号量,FreeRTOS 足够;如果产品要持续接入多个通信协议、多个传感器、多种硬件版本,Zephyr 的模块化优势会体现出来。
4.2 用表格做一个可落地的对比
| 对比维度 | FreeRTOS | Zephyr |
|---|---|---|
| 内核规模 | 很小,通常只关注任务、队列、信号量、互斥量 | 模块化,可按需编译,基础内核可以裁剪到较小体积 |
| 学习曲线 | 相对平缓,API 简单 | 较陡,还要理解 Kconfig、devicetree、west |
| 配置方式 | 头文件宏和配置项为主 | Kconfig + devicetree,结构更强 |
| 驱动模型 | 依赖厂商 SDK 或自己封装 | 统一设备驱动模型,可复用性更好 |
| 网络/蓝牙 | 通常需要额外集成协议栈 | 自带多种协议栈和子系统 |
| 许可证 | MIT,较宽松 | Apache 2.0 |
| 构建流程 | 各 IDE/厂商工程格式不一 | 统一 west + CMake + Ninja |
| 适合场景 | 简单产品、资源紧张、快速落地 | 多硬件复用、复杂物联网设备、长期演进 |
| 生态热度 | 传统认知度高,资料多 | 厂商和社区支持增长快,但部分文档更新较快 |
表格只是参考。真正选型时,不能只看参数,还要看团队掌握程度、产品生命周期、编译工具链适配、功耗优化需求和调试手段。
4.3 选型判断流程
我一般建议按这几个步骤走:
- 先列出硬性资源约束,包括 flash、ram、CPU 主频、外设数量。
- 再列功能需求,比如要不要蓝牙、MQTT、OTA、文件系统、安全启动。
- 看现有团队熟悉哪套体系,短期培训成本是多少。
- 用 2 到 3 天做一个最小原型,分别跑通任务调度、驱动适配和网络连接。
- 如果两个都能满足,再考虑长期维护成本。Zephyr 代码结构更统一,但需要团队持续学习;FreeRTOS 更直接,但产品复杂度上来后,内部结构可能慢慢“自发演化”。
不要因为“Zephyr 很火”就盲目迁移。也不要因为“FreeRTOS 旧”就放弃。工程选型是在约束下做取舍。
4.4 我见过的最容易误判的选择场景
最容易误判的是“低资源 MCU 上到底能不能用 Zephyr”。答案不是“能”或“不能”,而是“哪部分能力被裁剪到多低”。
比如一个只有 64KB flash、8KB ram 的 MCU,理论上可以跑最小 Zephyr,但如果你要启用日志、内核 shell、蓝牙协议栈,空间立刻就不够了。FreeRTOS 在这类硬件上会更从容。
另一种误判是“Zephyr 太复杂,不适合小团队”。如果产品只有一块板、一个型号、一个长期维护的固件,团队又没接触过 Zephyr,复杂度确实高。但如果公司有多个产品线、多个派生型号,Zephyr 的统一硬件描述反而能降低跨项目维护成本。
5. 从 hello_world 到多线程:第一次把 Zephyr 跑起来的完整过程
5.1 最小项目结构
在 Zephyr 里建一个应用,通常只需要几个文件:
my_app/ ├── CMakeLists.txt ├── prj.conf └── src/ └── main.cCMakeLists.txt至少要有:
cmake_minimum_required(VERSION 3.20.0) find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) project(my_app) target_sources(app PRIVATE src/main.c)main.c最简单版:
#include <zephyr/kernel.h> void main(void) { printk("my app start\n"); }构建:
cd my_app west build -b qemu_x86 . west build -t run这里不需要手动写完整 Makefile,Zephyr 的 build system 会处理头文件路径、链接脚本和配置生成。
5.2 跑一个真实的多线程例子
一个 RTOS 的价值是任务调度和同步。Zephyr 定义线程最常用的方式是K_THREAD_DEFINE,它会在编译期静态定义线程,避免动态内存分配。
#include <zephyr/kernel.h> #define STACK_SIZE 1024 #define THREAD_PRIORITY 7 void thread_entry(void *p1, void *p2, void *p3) { while (1) { printk("thread running\n"); k_msleep(1000); } } K_THREAD_DEFINE(my_thread, STACK_SIZE, thread_entry, NULL, NULL, NULL, THREAD_PRIORITY, 0, 0); void main(void) { printk("main started\n"); k_sleep(K_FOREVER); }这段代码里,K_THREAD_DEFINE会在系统启动时自动创建线程,主线程通过k_sleep(K_FOREVER)挂起。跑起来后,你应该每隔一秒看到一条thread running日志。
要理解一个关键点:线程栈大小和优先级是编译期确定的。栈开太大浪费 ram,开太小会触发栈溢出检测。Zephyr 有专门的栈溢出检测配置,但调试时仍然要优先保证栈大小合理。
5.3 怎么确认任务调度和日志输出正常
判断任务调度是否正常,不要只看“有没有输出”。要看三个方面:
- 日志顺序是否稳定。比如两个线程先后打印,频率是否符合预期。
- 是否有栈溢出提示。Zephyr 的栈溢出检测会在异常时输出调用栈和崩溃原因。
- 系统 tick 是否正常。定时器相关 API 只有在调度的 tick 驱动下才能工作。
我建议先跑samples/synchronization或samples/philosophers这类官方示例。它们内部用到了多线程、信号量、互斥量,输出结果可以验证调度器行为。
5.4 扩展到传感器、通信等子系统时的注意点
如果项目要接传感器或者通信模组,不要直接在 main 函数里“裸写驱动”。Zephyr 的设备驱动模型通常要求你先把设备绑定到 devicetree 节点,然后在代码里通过device_get_binding或DEVICE_DT_GET获取设备实例。
#include <zephyr/device.h> #include <zephyr/drivers/sensor.h> const struct device *sensor_dev = DEVICE_DT_ANY_INST_OF(sensor_compatible);注意,不同传感器驱动在 devicetree 里的 compatible 名称不同,返回结果也需要检查是否为空。传统 MCU 开发里“直接用寄存器操作”到了 Zephyr 这里不是不行,但会浪费掉大量驱动复用机会。
扩展到蓝牙、Wi-Fi、Shell 等模块时,还要留意依赖配置。比如打开蓝牙,往往还得打开相应的日志、crypto、随机数、flash 分区等后台依赖。这些依赖有些是自动的,有些需要你手动在prj.conf里补充。先跑官方 sample,再改成自己的业务代码,是最保险的方式。
6. 启动失败、任务卡住、配置不生效:我的排查顺序
6.1 启动失败
启动失败通常表现为:烧录后没有任何日志、系统在某个初始化阶段复位,或者打印到一半卡住。
先不要去看驱动代码,按这个顺序排:
- 硬件连接是否正常,供电、时钟、复位引脚。
- 日志串口是否选对。Zephyr 的 console 默认和 UART 设备绑定,如果串口引脚和板级 dts 不一致,怎么打印都没输出。
- 构建配置是否选对了 board。
-b指定的板卡型号必须和硬件一致。 - 有没有旧固件干扰。有些板卡在调试接口上还要确认 boot mode。
如果这些都没问题,再打开内核 panic 输出。内核启动失败时,往往会有异常地址和调用栈,这时候才能定位到具体模块。
6.2 任务卡住或无输出
任务卡住,不等于系统死机。它可能只是进入了某个等待条件,比如等一个永远不会来的信号量,或者某个中断没有正确释放锁。
我的排查顺序:
- 看所有线程状态。在配置里打开 shell,然后用 shell 命令查看线程列表和栈使用率。
- 看是哪条代码路径阻塞。
printk加在等待前后,能快速缩小范围。 - 检查优先级反转和死锁。两个任务互相等对方资源时,系统会“卡死”,但中断仍然可以响应。
- 检查栈空间。栈溢出会导致不可预测行为,有时候表现为随机重启,而不是立即崩溃。
不要一上来就开最大调试功能。会在日志和中断路径上引入新的不确定性。
6.3 驱动和硬件不匹配
驱动问题在 Zephyr 里经常会表现为“设备返回 error”或“读到的数据全为 0”。核心原因是 devicetree 里的硬件描述和实际板子不一致。
排查时先做三件事:
- 用
devicetree生成文件确认节点是否存在。 - 检查
gpios、reg、interrupts属性是否写对。 - 看驱动初始化函数的返回值。很多驱动初始化失败是因为时钟或电源未打开。
如果是在自己设计的板卡上调试,尽量把 devicetree 里的别名和默认引脚设置先看一遍,再查硬件原理图。最容易出错的是 GPIO 编号和复用功能,光看芯片手册不够,Zephyr 的 GPIO 控制器也有自己的编号规则。
6.4 排查顺序表
| 现象 | 优先检查 | 二阶段检查 |
|---|---|---|
| 构建失败 | Python、CMake、SDK 路径 | west 版本、manifest 同步 |
| devicetree 相关错误 | DTC 工具、dts 语法、节点路径 | 板级文件、overlay 覆盖 |
| 烧录后无打印 | 串口选择、硬件连接、board 型号 | console 配置、启动日志级别 |
| 任务卡住 | 信号量/互斥量释放逻辑 | 栈大小、线程优先级 |
| 某个驱动报错 | devicetree 属性 | 驱动初始化返回值、时钟和电源 |
| 配置不生效 | Kconfig 符号名、依赖关系 | 增量构建缓存、配置来源优先级 |
这个表格不是万能的,但它能帮你避免最常见的“无效排查”。
7. 最后的落地建议:从选型到长期维护
如果你看完前面内容,正准备动手做第一个 Zephyr 工程,我建议你控制好预期。
先跑官方 sample,再写自己的多线程逻辑,最后再接驱动和通信模块。整个过程至少留出 2 到 3 天。头一天往往花在环境上,这是正常的。很多人一开始会抱怨“为什么这么麻烦”,但当你从一块板迁移到另一块板,只改 dts 和 board 配置,不需要重写驱动时,就会理解这套设计的意义。
如果是生产项目,还要提前把版本锁住。Zephyr 更新节奏快,模块多,不同版本之间 API 可能有差异。建议把west.ymlmanifest 和 Zephyr SDK 版本都提交到版本管理里,保证团队所有成员构建出同一个系统。
还要把 CICD 纳入构建流程。Zephyr 的多目标构建很适合自动化:同一份应用代码,可以在服务器上同时构建多个 board,然后检查镜像大小和编译告警。这样能避免“本地能跑,同事那边跑不了”的情况。
如果把 Zephyr 和 FreeRTOS 放在一起选,我更倾向用一句话区分:FreeRTOS 解决的是“传感器加任务调度”,Zephyr 解决的是“多硬件、多通信协议、长期演进的产品平台”。如果你的产品还处于初期验证,先选自己团队能立刻上手的方案;如果你想在一个统一框架里构建未来几年的产品线,Zephyr 值得投入。真正跑通一个 demo 再回来审视选型,比停留在文档和表格里做判断更有意义。