【Zephyr|ESP32-S3】基础学习:完成第一个应用 “Hello World!”
【Zephyr|ESP32-S3】基础学习:完成第一个应用 “Hello World!”
哈喽,我是余火,一个普通的牛马打工人,目前正在学如何使用Zephyr RTOS。
上篇完成了 VS Code + Workbench for Zephyr 开发环境的搭建,环境跑通之后,自然要创建第一个工程来验证整条工具链——编译能不能通过、固件能不能烧进去、串口能不能看到输出。
对于嵌入式开发来说,第一个程序永远是 “Hello World”。这个程序本身很简单,但它能帮你验证开发环境的每一环是否正常工作:CMake 构建系统能不能正确解析项目文件、交叉编译工具链能不能生成目标平台的固件、烧录工具能不能把固件写入芯片、串口通信能不能正常收发数据。这些环节后续每篇文章都会用到,所以先花时间确保它们都能跑通是值得的。
这次用 Workbench 从零创建工程,跑通编译烧录,再改代码加上循环打印,顺便认识 Zephyr 的几个基础 API。
改了哪些东西
Workbench 创建 hello_world 工程时会自动生成以下文件。后续所有 Zephyr 工程都遵循同样的目录结构:
| 文件 | 作用 | 能否删除 |
|---|---|---|
src/main.c |
应用主入口,main() 是 Zephyr 内核启动后创建的默认线程 |
❌ 必需 |
CMakeLists.txt |
CMake 构建文件,告诉构建系统如何编译这个应用 | ❌ 必需 |
prj.conf |
Kconfig 配置文件,控制内核和应用的功能开关 | ⚠️ 可空(使用默认配置) |
README.rst |
应用说明文档,reStructuredText 格式 | ✅ 不影响编译 |
💡 Zephyr 的
main()和裸机main()不一样:在裸机开发中,main()是整个程序的入口,执行完就结束。在 Zephyr 里,main()是内核启动后创建的第一个线程,有独立的栈空间和优先级。main()返回后线程退出,但内核和其他线程继续运行——不会像裸机那样"程序结束"。
CMakeLists.txt 和 prj.conf 详解
这两个文件是 Zephyr 工程的构建核心,搞清楚它们你就搞清了 Zephyr 的编译流程和配置体系。
CMakeLists.txt
每个 Zephyr 应用都必须有这个文件,它是 CMake 构建系统的入口。Workbench 自动生成的模板非常标准,总共就四行有效代码:
# 最低 CMake 版本要求
cmake_minimum_required(VERSION 3.20.0)
# 引入 Zephyr 构建框架
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
# 项目名称(会出现在构建日志中)
project(hello_world)
# 指定应用源文件
target_sources(app PRIVATE src/main.c)
逐行解释:cmake_minimum_required 指定 CMake 最低版本;find_package(Zephyr) 是最关键的一行,它引入 Zephyr 的构建框架,包括交叉编译配置、链接脚本、板级配置等;project() 声明项目名称,这个名字会出现在编译输出的信息里;target_sources() 指定这个应用包含哪些源文件。后续如果工程有多个 .c 文件,在这里追加即可。
prj.conf
prj.conf 使用 Kconfig 语法。Zephyr 内核的功能通过 CONFIG_ 宏开关控制,在这个文件里写一行等价于在代码里 #define。Workbench 创建工程时会自动添加调试相关的配置:
# 优化等级设为 -Og(调试友好,保留符号信息)
CONFIG_DEBUG_OPTIMIZATIONS=y
# 线程感知支持(调试器能显示线程列表)
CONFIG_DEBUG_THREAD_INFO=y
# 生成每个函数的栈使用报告(编译后查看栈占用)
CONFIG_STACK_USAGE=y
# 输出 HEX 格式固件(部分烧录器需要)
CONFIG_BUILD_OUTPUT_HEX=y
# 打印内存使用统计(RAM/Flash 占用信息)
CONFIG_OUTPUT_PRINT_MEMORY_USAGE=y
目前你不需要逐行理解这些配置的含义,只需要知道:后续如果编译报错说某个功能没开启,大概率是在 prj.conf 里少写了一行 CONFIG_XXX=y。到时候会具体讲。
💡 Kconfig 是 Zephyr 的配置核心,类似 Linux 内核的 menuconfig。
CONFIG_前缀的宏控制内核的方方面面——串口驱动、蓝牙协议栈、文件系统、日志等级等。你可以用终端执行west build -t menuconfig打开图形化配置界面浏览所有可用选项。
创建 Hello World 工程
点击 VS Code 左侧 Workbench 面板中的 Add Application,进入应用创建界面。新版本 Workbench 合并了"新建"和"导入"两个入口,旧版插件需要点击 Create New Application。

在弹出的界面中选择 Create new application,找到 hello_world 示例(Zephyr 官方自带的入门例程,位于 samples/hello_world),点击 Create。如果你之前没用过 Zephyr 的模板系统,不用担心——Workbench 会自动处理所有依赖关系,从模板复制文件到你的 workspace 目录中。

点击 Create 后,Workbench 在 workspace 的 applications 文件夹下创建工程目录。此时 VS Code 左侧资源管理器中可以看到新项目,展开后能看到前面提到的四个文件——src/main.c、CMakeLists.txt、prj.conf、README.rst。
编译工程
在 VS Code 的 Workbench 面板中点击 Build,或者打开终端手动执行:
west build -b esp32s3_devkitc

-b esp32s3_devkitc 指定目标板名称。如果你的开发板型号不同,需要换成对应的板名(在 zephyr/boards/ 目录下查找)。
第一次编译时间较长,因为需要编译整个 Zephyr 内核和所有驱动。好消息是 Zephyr 默认使用增量编译——后续只修改了 src/main.c,重新 Build 时只会重新编译这一个文件然后重新链接,通常几秒钟就完成。
编译完成后,build/ 目录下会生成以下输出文件:
| 输出文件 | 格式 | 用途 |
|---|---|---|
zephyr.bin |
原始二进制 | west flash 默认烧录使用这个文件 |
zephyr.hex |
Intel HEX | 部分第三方烧录器需要这个格式 |
zephyr.elf |
ELF(含调试符号) | GDB / OpenOCD 调试时使用 |
zephyr.map |
内存映射文本 | 查看 RAM / Flash 占用分布 |
💡 清理构建缓存:如果编译出现奇怪的链接错误(比如改了头文件但新代码没生效),试试
west build -b esp32s3_devkitc --pristine,它会清除所有缓存后从头编译,相当于裸机开发中的make clean && make。
烧录到 ESP32-S3
用 USB 数据线连接 ESP32-S3-DevKitC-1 开发板和电脑。在 Workbench 面板点击 Flash,或使用终端命令:
west flash

west flash 会自动检测已连接的开发板并烧录 build/zephyr.bin。如果电脑上连接了多块开发板,可以通过 -r 参数指定烧录器(如 -r idvid)。正常情况下,终端会显示烧录进度和最终的 Done 提示。
打开串口助手,波特率设为 115200(ESP32-S3 的默认串口波特率),选择正确的 COM 口。按一下开发板上的 RST 复位键触发重启,可以看到 ESP32-S3 的启动日志和应用的打印输出。默认模板只打印一行信息就结束了,串口不会再有新内容。
修改代码:增加循环打印
默认模板只打印一行就结束,验证一下编译烧录链路没问题后,现在改代码让程序持续运行——每秒打印一次系统运行时间,方便后续观察程序是否正常工作。
本节用到的 Zephyr 内核 API:
| API | 头文件 | 作用 |
|---|---|---|
k_uptime_get_32() |
<zephyr/kernel.h> |
返回系统启动后的毫秒数(uint32_t,约 49 天溢出回零) |
k_sleep(timeout) |
<zephyr/kernel.h> |
让当前线程休眠指定时间,休眠期间 CPU 可执行其他线程 |
K_SECONDS(n) |
<zephyr/kernel.h> |
秒 → 内核 tick 的时间转换宏 |
K_MSEC(n) |
<zephyr/kernel.h> |
毫秒 → 内核 tick 的时间转换宏 |
修改后的完整 src/main.c:
#include <stdio.h>
#include <zephyr/kernel.h>
int main(void)
{
/* 开机打印板名,CONFIG_BOARD_TARGET 由构建系统自动注入 */
printf("hello renyuan %s\n", CONFIG_BOARD_TARGET);
/* 无限循环:每秒打印一次运行时间 */
for(;;) {
printf("hello renyuan (uptime: %u ms)\n", k_uptime_get_32());
k_sleep(K_SECONDS(1));
}
return 0;
}
逐行说明:
#include <stdio.h>:提供printf函数,标准 C 库头文件#include <zephyr/kernel.h>:Zephyr 内核服务头文件,提供k_sleep、k_uptime_get_32、K_SECONDS等 APICONFIG_BOARD_TARGET:Zephyr 构建系统自动注入的宏,展开为当前目标板名称(如esp32s3_devkitc),不需要手动#definefor(;;):经典无限循环写法,main()线程永不退出k_uptime_get_32():获取系统启动至今的毫秒数,用来验证程序在持续运行K_SECONDS(1):Zephyr 提供的时间宏,将 1 秒转为内核时钟周期数,比手写1000更安全
💡 Zephyr 的时间宏为什么比手写数值更安全:
K_SECONDS(n)、K_MSEC(n)会根据CONFIG_SYS_CLOCK_TICKS_PER_SEC(内核 tick 频率)自动计算对应的 tick 数。ESP32-S3 的 tick 频率是 1000 Hz,所以K_SECONDS(1)恰好等于 1000。但如果换到 nRF52832 等平台,默认 tick 频率是 32768 Hz,K_SECONDS(1)就不等于 1000 了。直接写毫秒数值在这种平台上会出现精度偏差。
修改保存后再次 Build → Flash,串口助手可以看到每秒一条打印,uptime 数值持续递增:

到这里,创建→编译→烧录→串口验证这条完整的开发链路就全部跑通了,后续每篇文章都会重复这套流程。
printk 和 printf 的区别
Zephyr 提供了自己的打印函数 printk,和标准 C 库的 printf 有区别:
| 特性 | printk |
printf |
|---|---|---|
| 头文件 | <zephyr/kernel.h> |
<stdio.h> |
| 依赖 | 仅依赖内核,不依赖标准 C 库 | 需要启用 CONFIG_NEWLIB_LIBC |
| 格式化能力 | 支持 %d、%s、%x 等常用格式 |
完整的 C 标准格式化 |
| ISR 中可用 | ✅ 可以(内核直接实现) | ❌ 不行(依赖库可能阻塞) |
| 推荐场景 | 日志输出、调试打印 | 需要复杂格式化时 |
本篇用的是 printf,因为它更符合大多数开发者的习惯。后续文章会逐步切换到 printk,因为它不依赖标准库、更轻量,而且在 ISR 上下文中也能安全调用。对初学者来说,两者语法基本一致,用哪个都能跑通,知道区别即可。
常见问题
Q:Build 报错 undefined reference to 'printf'?
A:printf 来自标准 C 库,确认 src/main.c 顶部包含了 #include <stdio.h>。如果 prj.conf 中没有启用标准库(CONFIG_NEWLIB_LIBC=y),printf 可能不可用,改用 Zephyr 原生的 printk 即可——只需 #include <zephyr/kernel.h>,不需要额外配置。
Q:串口助手看不到任何输出?
A:检查三个点:① 波特率是否设为 115200;② 首次烧录后需要按一下 RST 复位键触发重启;③ COM 口是否选对——在 Windows 设备管理器的"端口"分类中确认 ESP32-S3 对应的端口号。
Q:west flash 提示找不到串口设备?
A:ESP32-S3 开发板上通常使用 CP2102 或 CH343 芯片做 USB 转串口。Windows 下需要安装对应驱动:CP2102 去 Silicon Labs 官网下载,CH343 去 WCH 官网下载。安装成功后设备管理器中会出现 COM 口设备。
Q:增量编译后烧录,串口输出还是旧代码的结果?
A:确认 Build 日志中没有 error,并且最后一行显示编译成功。如果编译成功但输出没变化,可能是烧录失败——检查 USB 连接是否稳定,或者尝试手动执行 west flash 查看烧录过程的详细日志。按 RST 复位后观察是否有 *** Booting Zephyr OS *** 开机日志(每次复位都会打印)。
总结
本篇用 Workbench 从零创建了 hello_world 工程,跑通了创建→编译→烧录→串口输出的完整开发链路,并在此基础上加了 k_sleep + k_uptime_get_32 实现每秒打印运行时间。编译烧录这套流程后续每篇都会反复用到。
希望我的笔记能对你有一点点点的帮助!欢迎关注一起学习👇
更多推荐


所有评论(0)