2. Zephyr电源管理基础
做嵌入式产品,尤其是低功耗设备,你很快会发现一个现实:Zephyr 默认配置太“胖”了。它为了通用性,把很多你可能根本用不到的模块都编译了进去。我刚开始用 Zephyr 做一款温湿度传感器节点时,默认固件 200KB+,而我的 Flash 只有 128KB。嗯,这时候你就得学会“动刀”——Kconfig 裁剪。
说白了,Kconfig 就是 Zephyr 的“乐高说明书”。它告诉你每个模块是什么、依赖谁、能关不能关。这一章,我就带你从零开始,构建一个真正能跑在资源受限 MCU 上的最小系统。
2.1 Kconfig 到底是什么?
Kconfig 不是 Zephyr 独创的,它来自 Linux 内核。Zephyr 把它拿过来,做了一套自己的配置体系。每个模块(比如 GPIO、I2C、调度器)都有一个对应的 Kconfig 文件,里面定义了各种配置选项。
举个例子,你想用 I2C,就得先打开 CONFIG_I2C。但光打开这个还不够,还得看你用的是哪个 I2C 控制器,比如 STM32 的,那就得再开 CONFIG_I2C_STM32。这些选项之间还有依赖关系,Kconfig 会自动帮你检查。
autoconf.h 头文件,C 代码里通过 #ifdef CONFIG_XXX 来决定编译哪些部分。所以,裁剪 Kconfig 就是裁剪代码。
2.2 配置的三种方式
Zephyr 提供了三种配置方式,我按使用频率排个序:
| 方式 | 命令 | 适用场景 |
|---|---|---|
| menuconfig(推荐) | west build -t menuconfig |
交互式图形界面,适合探索和调试 |
| prj.conf 文件 | 直接编辑 prj.conf |
项目级配置,最常用,适合版本管理 |
| 命令行覆盖 | west build -- -DCONFIG_XXX=y |
临时测试,不持久化 |
我个人习惯用 prj.conf 做主力,因为它干净、可追溯。但遇到复杂的依赖关系时,我会打开 menuconfig 看看哪些选项被自动选中了,心里更有底。
/ 键可以搜索选项,输入关键字就能找到。比如搜“USB”,所有 USB 相关选项就列出来了。这个功能我几乎每次都用。
2.3 最小系统构建实战
好,理论说完了,咱们直接上手。假设你用的是 Nordic nRF52832 或者 STM32L0 这类资源紧张的芯片,目标是跑一个 GPIO 点灯 + 一个定时器中断。Flash 预算控制在 32KB 以内。
第一步,创建一个空项目。然后打开 prj.conf,开始“做减法”。
2.3.1 关闭不需要的内核特性
Zephyr 默认的调度器是抢占式的,还带了时间片轮转。如果你的应用只有两三个线程,其实可以关掉一些东西:
# 最小化内核配置
CONFIG_SCHED_CPU_MASK=n # 单核芯片,不需要 CPU 亲和性
CONFIG_SCHED_DEADLINE=n # 不用 deadline 调度
CONFIG_SCHED_SCALABLE=n # 线程数少,用简单调度器
CONFIG_SCHED_MULTIQ=n # 多队列调度,关掉
CONFIG_TIMESLICING=n # 时间片轮转,关掉
CONFIG_NUM_COOP_PRIORITIES=0 # 不用协作式优先级
CONFIG_NUM_PREEMPT_PRIORITIES=4 # 只留 4 个抢占优先级,够用
我曾经在一个项目中,光是把 CONFIG_SCHED_SCALABLE 关掉,就省了 2KB 的 Flash。你想想看,对于 32KB 的总空间,2KB 可不是小数目。
2.3.2 裁剪驱动和子系统
Zephyr 的驱动框架很完善,但代价是代码量大。我们只保留必要的:
# 只保留 GPIO 和 TIMER
CONFIG_GPIO=y
CONFIG_COUNTER=y # 用 COUNTER API 做定时器
# 关掉所有不用的总线
CONFIG_I2C=n
CONFIG_SPI=n
CONFIG_UART_CONSOLE=n # 如果不需要串口打印,关掉!
CONFIG_SERIAL=n
# 关掉日志和 shell
CONFIG_LOG=n
CONFIG_SHELL=n
# 关掉蓝牙、USB 等无线协议
CONFIG_BT=n
CONFIG_USB=n
CONFIG_UART_CONSOLE 后,printk() 就失效了。调试阶段建议先开着,量产时再关。我吃过这个亏——关掉后程序跑飞了都不知道。
2.3.3 调整内存和堆栈
Zephyr 默认的主栈大小是 2KB,对于简单应用来说太大了。我们可以缩小:
# 缩小系统栈
CONFIG_MAIN_STACK_SIZE=512
CONFIG_IDLE_STACK_SIZE=256
CONFIG_ISR_STACK_SIZE=512
# 缩小堆大小(如果不用动态内存分配)
CONFIG_HEAP_MEM_POOL_SIZE=0
这里要注意,CONFIG_HEAP_MEM_POOL_SIZE=0 意味着你不能用 k_malloc()。如果你的代码里没有动态分配,那就大胆关掉。我一般只在需要 printf 浮点数打印时才开堆,其他时候一律关。
2.4 验证裁剪效果
配置改完后,怎么知道省了多少?用 west build -t ram_report 和 west build -t rom_report 可以看内存和 Flash 占用。这是我之前一个项目的对比数据:
| 配置项 | 默认配置 | 裁剪后 | 节省 |
|---|---|---|---|
| ROM (Flash) | 48.2 KB | 18.6 KB | 29.6 KB |
| RAM | 12.4 KB | 3.8 KB | 8.6 KB |
| 启动时间 | ~15 ms | ~4 ms | 11 ms |
看到没?裁剪后 Flash 从 48KB 降到了 18.6KB,RAM 也省了一大截。对于低功耗设备来说,RAM 的节省意味着你可以用更小的芯片,成本直接下降。
2.5 Kconfig 依赖关系图解
为了让你更直观地理解 Kconfig 选项之间的依赖关系,我画了一张图。这张图展示了从顶层配置到底层驱动的典型链路:
这张图里,绿色节点是我们主动打开的,红色是主动关闭的。橙色和紫色是 Kconfig 自动推导出来的依赖项。你会发现,打开 CONFIG_GPIO 后,Kconfig 会自动帮你选上对应的 HAL 驱动和时钟源。你不需要手动去配那些底层选项,省心不少。
2.6 避坑指南
裁剪过程中,有几个坑我踩过,分享给你:
- 依赖链断裂: 有时候你关了一个选项,但另一个模块依赖它,编译直接报错。解决办法是在 menuconfig 里看
Depends on和Selects字段,理清关系。 - 过度裁剪导致功能异常: 比如关掉
CONFIG_TIMESLICING后,如果你的线程里用了阻塞等待,其他同优先级线程可能永远得不到 CPU。这时候要么改优先级,要么重新打开时间片。 - 调试信息丢失: 关掉
CONFIG_LOG后,系统出问题很难定位。我建议在开发阶段保留CONFIG_LOG_MINIMAL,它只占几百字节,但能输出关键错误。
west build -t rom_report 看 Flash 变化。如果发现某个选项关掉后 ROM 没减少,说明它本来就没被编译进去,不用纠结。
2.7 小结
这一章的核心就一句话:Kconfig 裁剪的本质是“按需编译”。你不需要理解每一个选项,但要知道去哪里查、怎么验证。最小系统不是固定的,它取决于你的硬件资源和功能需求。我给你的那个配置模板,只是一个起点。实际项目中,你可能还需要保留 I2C 驱动传感器,或者留一个串口做调试——那就按需打开。