在嵌入式项目中做 RTOS 选型时,我之前也一度在 FreeRTOS、RT-Thread、Zephyr 之间反复纠结。FreeRTOS 资料多、上手快,但组件生态和配置管理相对零散;Zephyr 功能强大、代码规范,但 west 工作流和 Kconfig 配置体系对刚接触的人来说存在一定门槛。最近在评估 2026 年新项目时,我专门把 Zephyr 的环境搭建、Kconfig 配置、任务开发跑了一遍,又把它与 FreeRTOS 做了横向对比,发现很多资料只讲碎片化命令,缺少一条龙式的闭环流程。本文就基于实际体验,整理一份从零开始的 Zephyr 实战教程,内容涵盖环境搭建、west 使用、Kconfig 配置、任务/串口示例、Workbench for Zephyr 的 Kconfig 操作方式,以及与 FreeRTOS 的选型对比。无论是想入门 Zephyr 的新手,还是正在做嵌入式选型评估的开发者,都可以参考这份笔记快速跑通流程。
1. Zephyr 是什么,为什么值得关注
1.1 从“可裁剪的 RTOS”到“连接嵌入式与 Linux 的桥梁”
Zephyr 是一个由 Linux 基金会托管的开源实时操作系统,采用 Apache 2.0 许可证。它与 FreeRTOS 这类传统 RTOS 最大的不同在于,Zephyr 的设计目标不是做一个“最小的任务调度器”,而是提供一个模块化、可裁剪、跨架构、安全感知的嵌入式操作系统平台。
用更通俗的话说,Zephyr 更像是一个“为嵌入式准备的 Linux 式开发环境”:它使用 Kconfig 管理配置,使用 CMake 组织构建,使用 devicetree 描述硬件,让应用开发者可以像写 Linux 驱动一样写嵌入式代码。同时它支持 ARM Cortex-M、RISC-V、x86、Xtensa、ARC 等多种架构,并提供了统一设备驱动模型(Device Driver Model)、线程安全的内核对象、网络协议栈(包括 Thread、BLE、Wi-Fi、LwM2M 等)、文件系统、加密库和 OTA 升级框架。
在具体应用场景上,Zephyr 常见于:
- 物联网传感器节点与边缘计算设备。
- 可穿戴设备、低功耗蓝牙设备。
- 工业控制与医疗设备中的安全关键系统。
- 需要远程升级、加密通信的联网产品。
- 教学科研中替代 Linux 做底层系统学习。
Zephyr 为什么值得关注?因为它的抽象层做得比较彻底。你写的驱动和应用代码,可以较方便地从一个厂商芯片平台迁移到另一个厂商芯片平台。对团队来说,这意味着“一次开发,多处适配”的现实可能性,但也意味着学习曲线比直接用厂商 SDK 要陡峭一些。
1.2 Zephyr 与 Linux 的相似之处
很多人第一次看到 Zephyr 的源码目录时,会有一种熟悉感:boards/、drivers/、dts/、samples/、subsys/、modules/,这些结构确实借鉴了 Linux 内核的组织方式。Zephyr 的构建系统也使用 CMake 加 Kconfig,应用通过prj.conf或Kconfig文件定义配置,再通过 devicetree 描述硬件连接。
这种设计既有好处,也有学习成本:
- 好处:代码结构清晰,分层合理,社区和工具链(尤其是 VS Code、Zephyr Workbench)都在围绕这一套体系优化。
- 成本:开发者需要理解 Kconfig 语法、devicetree 绑定、CMake 变量传递等概念,不能像 STM32CubeMX 那样“图形化生成初始化代码”就完事。
所以,学 Zephyr 的核心不是背 API,而是理解它的配置流程和构建流程。这也是本文重点讲 Kconfig 和 west 的原因。
2. 开发环境准备与版本说明
2.1 本文实验环境
考虑到不同开发者使用不同操作系统,下面分别说明。本文实际运行环境为:
| 项目 | 说明 |
|---|---|
| 操作系统 | Ubuntu 22.04 LTS(Windows 11 下同样可操作) |
| Zephyr 版本 | 以当前主流的 Zephyr 3.x / 4.x 系列为例 |
| 构建工具 | west、CMake、ninja |
| 编译器 | Zephyr SDK 自带工具链(GCC) |
| 目标板 | 以 QEMU Cortex-M3 与 STM32F407 开发板为例 |
| IDE 辅助 | VS Code + Zephyr Workbench 插件 |
需要说明的是,Zephyr 升级速度较快,不同版本的依赖和默认行为可能存在差异。你在实际搭建时不要死板照抄版本号,而是根据官方文档和自己的目标平台灵活调整。本文重点关注配置思路,而不是固化某个版本。
2.2 安装系统依赖
在 Ubuntu/Debian 系统上,先安装基础依赖:
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 libmagic1Windows 用户建议使用 West 官方推荐的 MSYS2 环境,但我个人更推荐在 WSL2(Windows Subsystem for Linux)里按 Linux 流程操作,因为 Zephyr 大部分工具链脚本都默认兼容 Linux 环境。
macOS 用户需要安装 Homebrew,然后执行类似brew install cmake ninja gperf ccache qemu dtc wget的安装命令,并安装 Python 3 环境。
2.3 安装 west 与拉取 Zephyr 源码
west 是 Zephyr 的元工具(meta-tool),它负责多仓库管理、构建、刷写等操作。安装方式:
pip3 install west然后创建一个工作目录,并拉取 Zephyr 源码:
mkdir zephyr-project && cd zephyr-project west init -m https://github.com/zephyrproject-rtos/zephyr --mr main cd zephyr west updatewest init会在目录下生成.west/config,west update会根据 manifest 文件拉取所有需要用到的模块(如 hal、cmsis、littlefs 等)。这个过程会花一些时间,网络环境不稳定时建议分多次执行,或配置代理(注意,这里所说的代理指常规网络访问方式,请遵守本地上网规范)。
拉取完成后,可以把 Zephyr 的环境变量写入 shell 配置:
echo "export ZEPHYR_BASE=$(pwd)" >> ~/.bashrc echo "export ZEPHYR_TOOLCHAIN_VARIANT=zephyr" >> ~/.bashrc source ~/.bashrcZEPHYR_BASE是 Zephyr 源码根目录,编译时 west 会依赖这个变量。如果你的项目很多,建议用west config管理不同工程的路径。
2.4 安装 Zephyr SDK
Zephyr SDK 包含预编译工具链、QEMU、OpenOCD 等工具。到 Zephyr SDK 官方 Release 页面下载对应版本的 tar 包,例如zephyr-sdk-0.16.8_linux-x86_64.tar.xz,然后解压安装:
cd ~ wget https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v0.16.8/zephyr-sdk-0.16.8_linux-x86_64.tar.xz tar xf zephyr-sdk-0.16.8_linux-x86_64.tar.xz cd zephyr-sdk-0.16.8 ./setup.shsetup.sh会引导配置 udev 规则和 toolchain 路径。运行后可以用以下命令验证:
export ZEPHYR_TOOLCHAIN_VARIANT=zephyr export ZEPHYR_SDK_INSTALL_DIR=~/zephyr-sdk-0.16.8有一点要特别注意:Zephyr SDK 的版本和 Zephyr 内核主版本之间有一定配套关系。如果版本差异过大,编译时可能出现工具链内部错误或链接失败,建议先阅读官方“Getting Started Guide”中的版本对应说明。
2.5 用 Workbench for Zephyr 辅助开发
命令行方式足够完成全部开发流程,但如果你想提高效率,可以使用 VS Code 的 Zephyr Workbench 插件。Zephyr Workbench 提供了图形化的项目创建、Kconfig 配置、构建和调试支持。
我在实际使用中发现,Workbench 最有价值的功能是Kconfig 图形化编辑。它把 Kconfig 的层层依赖用菜单方式展示出来,你可以直接在 UI 里看到哪些配置被选中、哪些被依赖项自动屏蔽、哪些与 devicetree 有关联。相比纯文本编辑prj.conf,这种方式对初学者友好很多,尤其是排查“为什么我写的配置没生效”这类问题时,可视化视图能快速定位问题。
不过需要提醒的是,Workbench 的图形化配置最终仍然写回prj.conf或构建生成的.config,所以不要依赖图形界面而忽略 Kconfig 本身的语法。后面我们会单独讲 Kconfig 在命令行下怎么高效使用。
3. Kconfig 配置体系深入拆解
3.1 Kconfig 是 Zephyr 的“总开关”
Kconfig 最初来自 Linux 内核配置系统,Zephyr 沿用了这一套机制。简单理解,Kconfig 就是一套“菜单 + 选项 + 依赖”的配置语言,它决定了一个 Zephyr 镜像里哪些模块被编译、哪些功能被开启、哪些参数被设置。
Zephyr 的配置项以CONFIG_开头。例如:
CONFIG_GPIO:启用 GPIO 驱动子系统。CONFIG_SERIAL:启用串口驱动。CONFIG_THREAD_MONITOR:启用线程监控。CONFIG_HEAP_MEM_POOL_SIZE:设置堆内存池大小。
配置值会在编译时以-DCONFIG_XXX=YYY的方式传给 C 代码,源码中的#ifdef CONFIG_XXX和#if CONFIG_XXX == YYY就会根据这些配置剪裁代码。
3.2 Kconfig 的文件层级
Zephyr 中的 Kconfig 配置不是写在一个文件里。它分布在多个层级:
| 路径 | 作用 |
|---|---|
zephyr/Kconfig | 顶层入口,include 所有子系统的 Kconfig |
zephyr/arch/*/Kconfig | 架构相关配置 |
zephyr/drivers/*/Kconfig | 驱动配置 |
zephyr/subsys/*/Kconfig | 子系统配置,如网络、蓝牙、文件系统 |
boards/<厂商>/<板卡>/Kconfig.defconfig | 板级默认配置 |
boards/<厂商>/<板卡>/<板卡>_defconfig | 板级初始化配置 |
应用目录下的prj.conf | 项目级配置 |
配置的优先级可以概括为:越靠近具体项目,优先级越高。也就是说,应用prj.conf里的配置会覆盖板级默认配置。
3.3 最小示例:在 prj.conf 中开启串口
我们举一个最常见的例子。假设目标板是qemu_cortex_m3,你希望开启串口并设置波特率。在应用根目录创建prj.conf:
CONFIG_SERIAL=y CONFIG_UART_CONSOLE=y CONFIG_BAUD_RATE=115200解释一下:
CONFIG_SERIAL=y:启用串口驱动框架。CONFIG_UART_CONSOLE=y:使用串口作为控制台输出。CONFIG_BAUD_RATE=115200:设置波特率。
不是所有配置项都是 bool 类型,还有 int 和 string 类型,例如:
CONFIG_MAIN_STACK_SIZE=4096 CONFIG_BOOT_BANNER_STRING="My Zephyr App"这里CONFIG_MAIN_STACK_SIZE是 int 类型,后面的数字不能加引号;CONFIG_BOOT_BANNER_STRING是 string 类型,需要用引号包裹。
3.4 用 menuconfig 可视化调整配置
在应用目录下执行:
west build -b qemu_cortex_m3 -t menuconfig这会解析当前构建的配置文件,弹出一个基于 ncurses 的图形化配置界面。你可以像 Linux 的make menuconfig一样,用方向键浏览菜单,按空格或回车修改选项,按?查看帮助,按/搜索配置项。
这是排查配置问题的利器。比如你怀疑串口没生效,可以在菜单里搜索UART_CONSOLE,查看它当前的值、依赖条件、被哪个文件引用。这比盲目改prj.conf有效得多。
3.5 Kconfig 条件依赖与默认值
Kconfig 配置项之间经常存在依赖关系。例如,某个 driver 只有在CONFIG_GPIO=y时才可选,某个协议栈只有在CONFIG_NETWORKING=y时才显示。
这种依赖关系的定义位于各模块的Kconfig文件中。看一个简化的例子:
config FOO_DRIVER bool "Enable FOO driver" default y depends on GPIO help This enables the FOO device driver.表示:CONFIG_FOO_DRIVER是一个 bool 配置,默认开启,但只有在CONFIG_GPIO=y时才可见、可选。如果你在prj.conf里写了CONFIG_FOO_DRIVER=y但忘了开CONFIG_GPIO,配置系统不会立刻报错,但最终可能不会生效,或者会在menuconfig里自动变成灰色。
因此,看到“配置没生效”时,不要只盯着prj.conf,还要检查依赖链上层的配置是否满足。
3.6 Kconfig 与 devicetree 的分工
很多新手容易混淆 Kconfig 和 devicetree。简单区分:
- Kconfig 管“软件功能”:是否启用某个子系统、设置线程栈大小、开启某种协议。
- devicetree 管“硬件连接”:某个外设挂在哪个地址、使用哪个中断号、GPIO 引脚是哪个。
举个例子:你要使用 SPI,硬件上 SPI 外设的基地址、中断号由 devicetree 描述;但要不要启用 SPI 驱动框架、SPI 时钟频率默认值,则由 Kconfig 的CONFIG_SPI=y控制。
在 Zephyr 开发中,两者配合使用。比如打开CONFIG_SPI=y后再写设备树 overlay 文件(.overlay),添加具体 SPI 设备节点,才能让驱动 probe 到设备。后面实战部分会展示它们如何协作。
4. 完整实战:创建一个带串口输出和多线程任务的应用
这一节我们从零开始创建一个可运行的 Zephyr 应用,包含串口输出、两个线程的创建与调度、以及基于 devicetree 的 GPIO 点灯示例。目标平台先用qemu_cortex_m3验证逻辑,再提供一个 STM32 平台下的移植参考。
4.1 创建应用目录结构
Zephyr 应用通常有固定结构:
my_app/ ├── CMakeLists.txt ├── prj.conf ├── src/ │ └── main.c └── boards/ └── qemu_cortex_m3.overlayboards/目录可以放板级 overlay 文件,也可以放到应用根目录下。我们先创建基础文件:
mkdir -p my_app/src cd my_app4.2 编写 CMakeLists.txt
# 文件路径:my_app/CMakeLists.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)这里find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})是必须的,它会导入 Zephyr 的构建系统。project(my_app)会生成app这个 target,target_sources把主源码文件加入构建。
4.3 编写 prj.conf
# 文件路径:my_app/prj.conf CONFIG_SERIAL=y CONFIG_UART_CONSOLE=y CONFIG_BAUD_RATE=115200 CONFIG_MAIN_STACK_SIZE=2048 CONFIG_THREAD_MONITOR=y CONFIG_GPIO=y解释:CONFIG_SERIAL和CONFIG_UART_CONSOLE用于串口控制台输出;CONFIG_MAIN_STACK_SIZE设置 main 线程栈大小;CONFIG_THREAD_MONITOR开启线程监控,便于调试时查看线程状态;CONFIG_GPIO开启 GPIO 驱动。
4.4 编写 main.c
下面写一个包含两个线程的示例。一个线程周期性打印日志,另一个线程模拟采集数据,主线程负责初始化并启动它们。
// 文件路径:my_app/src/main.c #include <zephyr/kernel.h> #include <zephyr/sys/printk.h> #include <zephyr/device.h> #include <zephyr/drivers/uart.h> /* 线程栈定义 */ #define THREAD_A_STACK_SIZE 1024 #define THREAD_B_STACK_SIZE 1024 /* 线程优先级:数值越小优先级越高 */ #define THREAD_A_PRIORITY 7 #define THREAD_B_PRIORITY 8 K_THREAD_STACK_DEFINE(thread_a_stack, THREAD_A_STACK_SIZE); K_THREAD_STACK_DEFINE(thread_b_stack, THREAD_B_STACK_SIZE); static void thread_a_entry(void *arg1, void *arg2, void *arg3) { ARG_UNUSED(arg1); ARG_UNUSED(arg2); ARG_UNUSED(arg3); while (1) { printk("Thread A: running, uptime = %lld ms\n", k_uptime_get()); k_sleep(K_MSEC(1000)); } } static void thread_b_entry(void *arg1, void *arg2, void *arg3) { ARG_UNUSED(arg1); ARG_UNUSED(arg2); ARG_UNUSED(arg3); while (1) { printk("Thread B: running, uptime = %lld ms\n", k_uptime_get()); k_sleep(K_MSEC(2000)); } } int main(void) { printk("Zephyr multi-thread demo started!\n"); k_thread_create(&my_thread_a_data, thread_a_stack, THREAD_A_STACK_SIZE, thread_a_entry, NULL, NULL, NULL, THREAD_A_PRIORITY, 0, K_NO_WAIT); k_thread_name_set(&my_thread_a_data, "thread_a"); k_thread_create(&my_thread_b_data, thread_b_stack, THREAD_B_STACK_SIZE, thread_b_entry, NULL, NULL, NULL, THREAD_B_PRIORITY, 0, K_NO_WAIT); k_thread_name_set(&my_thread_b_data, "thread_b"); return 0; }这段代码演示了 Zephyr 中最常用的线程 API:
K_THREAD_STACK_DEFINE用于定义线程栈。k_thread_create创建线程。k_thread_name_set给线程起名,方便调试。k_sleep让出 CPU。k_uptime_get获取系统启动至今的毫秒数。
4.5 构建并运行
在my_app目录下执行:
west build -b qemu_cortex_m3如果构建成功,可以运行:
west build -t run或者手动用 QEMU 运行:
qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -kernel build/zephyr/zephyr.elf预期输出类似:
Zephyr multi-thread demo started! Thread A: running, uptime = 2000 ms Thread B: running, uptime = 2000 ms Thread A: running, uptime = 3000 ms Thread A: running, uptime = 4000 ms Thread B: running, uptime = 4000 ms ...为什么第一次打印是uptime = 2000 ms?因为两个线程是在 main 函数里创建的,但return 0后 main 线程退出,调度器可能先运行完初始化,再切到两个业务线程。这个细节可以忽略,重点是确认线程能够独立运行、周期打印。
4.6 加入 GPIO 点灯(基于 STM32 的适配思路)
QEMU 环境下 GPIO 操作看不到物理效果,如果要上板验证,我们可以把目标平台换成stm32f407g_disc1或stm32f411e_disco,硬件上 LED 引脚不同,需要 overlay 文件。
下面是一个通用的 overlay 写法,将 LED 引脚设置为 GPIO 输出:
// 文件路径:my_app/boards/stm32f407g_disc1.overlay / { leds { compatible = "gpio-leds"; green_led: green_led { gpios = <&gpiof 9 GPIO_ACTIVE_HIGH>; label = "Green LED"; }; }; };然后修改prj.conf:
CONFIG_GPIO=y CONFIG_GPIO_STM32=y再修改main.c,使用 devicetree API:
#include <zephyr/drivers/gpio.h> #define LED0_NODE DT_NODELABEL(green_led) static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios); void led_init(void) { if (!device_is_ready(led.port)) { printk("LED device not ready\n"); return; } gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE); } void led_toggle(void) { gpio_pin_toggle_dt(&led); }重点解释:
DT_NODELABEL(green_led)获取 overlay 中定义的节点。GPIO_DT_SPEC_GET把设备树节点的gpios属性转成gpio_dt_spec结构体。gpio_pin_configure_dt和gpio_pin_toggle_dt是带设备树描述的操作 API。
这种写法把“硬件引脚”和“业务逻辑”分离了。你从stm32f407g_disc1换到nucleo_l476rg,只需要改 overlay 文件,main.c 的驱动代码不用动,这正是 Zephyr 设备树体系的优势。
5. Zephyr 与 FreeRTOS 深度对比:选型思路
5.1 为什么总有人拿 Zephyr 和 FreeRTOS 比
FreeRTOS 是过去十几年最成功的嵌入式 RTOS 之一,被 AWS 收购后生态进一步扩展;Zephyr 则背靠 Linux 基金会,获得了大量芯片厂商、IoT 云平台的支持。两者不是一个维度的项目,但在“给项目选 RTOS”时,你会经常在它们之间做选择。
从实际项目选型角度看,我们需要关注下面几个维度:
| 对比维度 | Zephyr | FreeRTOS |
|---|---|---|
| 开源协议 | Apache 2.0,允许商用闭源 | MIT 许可,允许商用闭源 |
| 内核代码 | 模块化,更接近 Linux 风格 | 简洁,调度器代码量小 |
| 构建系统 | CMake + west + Kconfig + devicetree | 官方 IDE/CMake/厂商 SDK 混合 |
| 硬件抽象 | 统一设备驱动模型,板级支持多 | 依赖厂商移植层,各厂商风格不一 |
| 组件生态 | 蓝牙、网络、加密、OTA、日志、shell 都内置 | 需要自己集成第三方组件或选择商业版本 |
| 学习曲线 | 较陡峭,需要理解构建和配置体系 | 较平缓,上手快 |
| 内存占用 | 内核裁剪得当可以很小,但基础框架偏大 | 极小,适合 MCU 资源紧张场景 |
| 社区活跃度 | 上升期,芯片厂商支持多 | 成熟稳定,资料数量庞大 |
| 适合项目 | 中大型物联网产品、需要长期维护、跨平台迁移 | 小型 MCU 项目、快速原型验证、对资源有极端要求 |
5.2 内核与任务模型差异
FreeRTOS 的任务模型非常经典:xTaskCreate创建任务,任务函数是一个无限循环,通过vTaskDelay延时,通过队列和信号量通信。几乎每个嵌入式开发者都熟悉这一套。
Zephyr 的线程模型在概念上类似,但 API 更丰富。比如使用k_thread_create创建线程,k_sleep延时,k_msgq消息队列,k_sem信号量。Zephyr 还有 workqueue、fifo、lifo、mutex、event 等多种同步原语,设计上更像一个完整操作系统,而不只是调度器。
看一个并行的例子。FreeRTOS 创建任务:
xTaskCreate(vTaskFunction, "TaskA", configMINIMAL_STACK_SIZE, NULL, 1, NULL);Zephyr 创建线程:
k_thread_create(&thread_a_data, thread_a_stack, THREAD_A_STACK_SIZE, thread_a_entry, NULL, NULL, NULL, THREAD_A_PRIORITY, 0, K_NO_WAIT);两者底层都是创建可调度实体,但 Zephyr 的k_thread结构里包含了更丰富的内核对象信息,例如线程名称、堆栈统计、CPU 使用统计等。配合CONFIG_THREAD_MONITOR=y,你可以用 shell 或调试器查看每个线程的状态。
5.3 配置体系差异
FreeRTOS 使用FreeRTOSConfig.h头文件配置,每个工程一份,配置项直接是宏定义。优点是直观,缺点是难以统一管理不同板卡、不同模块之间的依赖关系。
Zephyr 用 Kconfig 配置,配置项分散在各模块的 Kconfig 文件中,最终生成统一的.config。这带来几个好处:
- 配置项有依赖关系,可以避免“配了 A 但忘了 B”的错误。
- 配置项有文档说明和默认值。
- 可以按 board 维度做默认配置。
- 构建时自动检查依赖。
但也有一个缺点:写prj.conf时要知道配置项叫什么名字,如果不熟悉,需要搜索。Workbench for Zephyr 的图形化 Kconfig 配置界面在这里很有帮助,它把可配置项按类别展示出来,降低记忆负担。
5.4 生态与选型建议
从 2026 年项目选型角度看,我的建议可以概括为三句话:
- 如果项目是“传感器 + 轻量通信 + 电池供电”,资源极其紧张,团队又非常熟悉 FreeRTOS,继续用 FreeRTOS 完全合理。
- 如果项目是“ARM Cortex-M/RISC-V 上的中大型 IoT 网关、需要蓝牙/网络/OTA/加密/多传感器管理”,或者希望代码跨芯片平台复用,Zephyr 的综合成本可能更低。
- 如果项目有产品化、长期维护、模块化扩展需求,Zephyr 的系统化配置和统一驱动模型会更省心。
FreeRTOS 的优势在于简单和可控性强;Zephyr 的优势在于生态和标准化。两者并不是非此即彼的关系,甚至 FreeRTOS 和 Zephyr 在合入同一条产品线时,可以在不同档位的设备上共存。
6. 常见问题与排查思路
6.1 west build 失败:Toolchain 无法找到
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
构建报Unable to find toolchain或CMake Error: Zephyr SDK not found | 未设置ZEPHYR_TOOLCHAIN_VARIANT和ZEPHYR_SDK_INSTALL_DIR | 检查环境变量并 export 后重新构建 |
排查命令:
echo $ZEPHYR_BASE echo $ZEPHYR_SDK_INSTALL_DIR echo $ZEPHYR_TOOLCHAIN_VARIANT如果环境变量为空,重新设置并写入~/.bashrc。还有一种情况是 SDK 目录权限不足,导致setup.sh没有写入 udev 规则,虽然这不一定阻止编译,但可能影响后续烧写。
6.2 配置项写了没生效
常见原因有三种:
- 配置项拼写错误,例如把
CONFIG_SERIAL写成CONFIG_SERIALL。 - 配置项依赖的条件未满足,比如某个驱动依赖
CONFIG_GPIO,但未开启。 - 配置项被更高优先级的配置覆盖,例如板级
defconfig中设置了CONFIG_XXX=n,应用prj.conf中没有显式设置或设置为y但依赖不满足。
解决方法:使用menuconfig搜索该配置项,查看它的状态和依赖链。
6.3 设备树 overlay 不生效
编译时如果没有为某个 board 选择正确的 overlay 文件,或 overlay 文件名与 board 名不匹配,设备树节点就不会被加载。
排查方法:
- 确认
boards/<board名>.overlay文件名与-b参数一致。 - 也可以在
west build时显式指定:
west build -b stm32f407g_disc1 -- -DDTC_OVERLAY_FILE=boards/stm32f407g_disc1.overlay- 查看生成的设备树文件:
cat build/zephyr/zephyr.dts6.4 内存不足或链接失败
Zephyr 默认会链接一些子系统,即使你的应用只用到其中一部分。如果 ROM/RAM 超限,会看到类似:
region `FLASH' overflowed by 1234 bytes解决方案:
- 裁剪配置,关闭不需要的子系统,例如
CONFIG_NETWORKING=n、CONFIG_BT=n。 - 调整链接器分区或使用
CONFIG_SIZE_OPTIMIZATIONS=y。 - 检查是否不小心开启了 debug 日志,
CONFIG_LOG=y会增加不少内存占用。
6.5 串口输出乱码或没有输出
原因通常有:
- 波特率设置不一致。
- 串口驱动未正确初始化。
- 控制台和 UART 驱动冲突。
- QEMU 下需要加
-nographic参数。
排查步骤:先确认prj.conf中CONFIG_UART_CONSOLE=y;再检查 board 的串口配置;最后在应用启动早期调用printk测试。
7. 最佳实践与工程建议
7.1 严格维护版本锁定
Zephyr 的 west manifest 文件(west.yml)锁定了所有模块的 Git 修订版本。建议把整个zephyr-project目录纳入 Git 管理,并定期提交west.yml的变化。这样团队协作时,每个人执行west update都能拿到一致的版本。
实际项目中不要直接使用main分支,而是选择一个次版本分支(如v3.7-branch)或特定 release tag,避免上游破坏性变更影响产品开发。
7.2 配置管理:区分板级与应用
不要把所有配置都堆在prj.conf里。建议:
- 应用公共配置放在根目录
prj.conf。 - 不同板卡的差异化配置放在
boards/<board>.conf。 - 硬件描述差异放在
boards/<board>.overlay。
这样做的优点是:新增一款板卡时,只需要增加对应的.conf和.overlay,应用源码可以保持不变。
7.3 日志与调试意识
Zephyr 有很完善的日志系统(subsys/logging),建议不要只用printk。推荐使用:
#include <zephyr/logging/log.h> LOG_MODULE_REGISTER(my_app, LOG_LEVEL_INF); LOG_INF("System started"); LOG_ERR("Something wrong: %d", err);日志系统支持后端灵活配置,可以输出到串口、RTT、网络等。与printk相比,它能按模块和级别过滤,对长期维护非常重要。
7.4 使用 devicetree 而不是硬编码引脚
在应用代码中尽量通过DT_NODELABEL、DT_ALIAS获取引脚信息,不要直接在源码里写死 GPIO 端口和引脚号。这样当硬件改版时,只需改 overlay,业务代码的 diff 会小很多,review 也更轻松。
7.5 安全与生产环境注意
- 生产环境 OTA 功能必须验证回滚机制。
- 涉及安全功能(加密密钥、安全启动)时,不要把密钥写在源码库中。
- 对生产设备进行配置变更前,先在一台设备上验证,确认无误后再批量操作。
- 权限和访问控制遵循最小权限原则。
7.6 自动化测试
Zephyr 提供了twister测试工具,可以为应用编写测试用例并自动运行。在 CI 中执行时,可以结合 QEMU 或物理开发板。建议在新项目初期就加入 twister 配置,避免后期补测试成本太高。
8. 总结与学习路线
8.1 本文关键点回顾
通过本文,我们从零走通了 Zephyr 的完整开发链路:
- 理解了 Zephyr 和 FreeRTOS 在定位、构建、配置上的本质差异。
- 完成了 west、Zephyr SDK 和 Workbench for Zephyr 的环境搭建。
- 学习了 Kconfig 的基本语法、文件层级和 menuconfig 的使用方式。
- 实现了一个包含串口输出、多线程任务、GPIO 点灯的示例应用。
- 梳理了常见构建、配置、设备树问题的排查思路。
- 总结了 Zephyr 项目在版本管理、配置管理、日志与安全性方面的最佳实践。
走完这套流程,你应该具备独立创建一个 Zephyr 应用并移植到具体开发板的能力。
8.2 下一步学习建议
如果继续深入,建议按这个顺序推进:
- 学习 devicetree 的绑定语法,掌握
dts文件中compatible、reg、interrupts的写法。 - 阅读 Zephyr 官方
samples/目录下的 BLE、USB、网络示例,了解子系统如何与内核交互。 - 尝试把一个具体传感器驱动封装成 Zephyr sensor driver 风格。
- 使用
twister为你的应用编写测试。 - 尝试在 CI 中集成 Zephyr 构建与烧录,形成可持续集成的嵌入式交付流程。
8.3 实际项目中的优先风险
在正式项目中使用 Zephyr 时,优先关注这几个风险:
- 工具链和 SDK 版本漂移导致构建环境不一致,务必锁定 west manifest。
- 内存与 Flash 占用评估要提前做,不能等硬件回来才发现资源不够。
- 外设驱动的成熟度因平台而异,选型前先用官方板卡跑一遍相关外设 demo。
- 团队学习成本要纳入排期,Zephyr 不是“半天能上手”的库,它更像一个操作系统。
最后建议你动手把上面那个示例代码跑一遍,哪怕只是在 QEMU 中运行,也会比只看文章理解更深。如果你想在真实板卡上验证,优先选择官方支持列表里的开发板,踩坑会少很多。
如果本文对你有帮助,可以收藏备用,后续做 Zephyr 开发时随时翻一翻。你有任何踩坑疑问,也欢迎在评论区交流。