我带过一个四人小组做 nRF Connect SDK 的手表固件。Review 代码的时候发现一个很不舒服的味道:main.c已经 200 多行了,一半是初始化调用,sensor_init()、display_init()、battery_init()……新人加个模块,第一件事就是去main.c占个坑。
我跟他们说:Zephyr 自己不是早就有答案了吗?SYS_INIT你天天在用——写一行宏,没人调用它,开机自动执行。业务模块凭什么不能享受同等待遇?
这一篇就是那次内部重构的完整复盘:三关,把"手动初始化"改造成"注册即生效",每一关都给你一个当场验证有没有成功的手段,不靠猜。
先交代背景知识,一段话说完:Zephyr 的静态注册基于 iterable section 机制——注册项(一个结构体,里面装函数指针)被放进专属链接器 section,链接期收集到一起、打上起止边界符号,运行期框架在边界内逐个取出来调用。原理篇(上一篇)讲过细节,这里直接上实操。
第一关:让数据聚起来
目标:任意源文件里注册的东西,链接后落在同一个分区里。
先设计注册项。我们的场景是传感器模块注册,每个模块交出一个"描述 + 初始化函数 + 采样函数":
/* sensor_registry.h */ #ifndef SENSOR_REGISTRY_H #define SENSOR_REGISTRY_H #include <zephyr/sys/iterable_sections.h> struct sensor_module { const char *name; int (*init)(void); int (*sample)(void); uint8_t interval_sec; /* 采样周期 */ }; #define SENSOR_MODULE_DEFINE(_name, _init, _sample, _interval) \ const STRUCT_SECTION_ITERABLE(sensor_module, _name) = { \ .name = #_name, \ .init = _init, \ .sample = _sample, \ .interval_sec = _interval, \ } #endif然后在两个模块里各注册一个(注意:这两个文件互相不知道对方存在,也不 include main.c):
/* accel.c */ static int accel_init(void) { /* 加速度计初始化 */ return 0; } static int accel_sample(void) { /* 读一次数据 */ return 0; } SENSOR_MODULE_DEFINE(accel, accel_init, accel_sample, 1);/* heart_rate.c */ static int hr_init(void) { return 0; } static int hr_sample(void) { return 0; } SENSOR_MODULE_DEFINE(heart_rate, hr_init, hr_sample, 5);验证:编译后objdump -h build/zephyr/zephyr.elf | grep sensor_module。
你会看到一个叫sensor_module_area的输出分区,大小是 2 × sizeof(struct sensor_module)。如果 grep 不到——说明收集没发生,原因九成是下面第二关的文件还没接。
第二关:让数据活着
上面 grep 不到的原因,是这一关没过:注册项没有被任何代码"直接引用",两道关卡都想弄死它——编译器想优化掉(__used__属性拦住,注册宏里已带),链接器--gc-sections想丢弃整个 section(要靠链接器脚本的KEEP拦)。
所以需要一个链接器片段,新建sensor.ld:
/* sensor.ld */ #include <zephyr/linker/iterable_sections.h> ITERABLE_SECTION_ROM(sensor_module, Z_LINK_ITERABLE_SUBALIGN)再在模块的CMakeLists.txt里接入构建,一行:
zephyr_linker_sources(SECTIONS sensor.ld)ITERABLE_SECTION_ROM展开后自带KEEP(*(SORT_BY_NAME(...)))——收集、按名排序、保活三件事一次做完,还顺手打上_sensor_module_list_start/_sensor_module_list_end两个边界符号。
两个配对规则记牢,配错不报错但落错内存区:
- 声明带
const(只读表,函数指针表一律建议这样)→ITERABLE_SECTION_ROM+ CMakeSECTIONS; - 声明不带
const(运行期要改字段)→ITERABLE_SECTION_RAM+DATA_SECTIONS。
验证:重跑objdump -h,这次分区出现了;再跑nm build/zephyr/zephyr.elf | grep sensor_module,能看到accel、heart_rate两个符号,以及首尾边界符号。
第三关:让数据跑起来
最后一关最短——框架代码里一个循环:
/* sensor_framework.c */ #include "sensor_registry.h" void sensor_poll(void) { STRUCT_SECTION_FOREACH(sensor_module, m) { if (m->sample) { m->sample(); } } }STRUCT_SECTION_FOREACH就是在两个边界符号之间做指针步进遍历。全程不需要知道有几个模块:新人再加一个temp.c写一行SENSOR_MODULE_DEFINE(temp, ...),sensor_poll()一行不改,新模块自动被轮询。
想让它开机全自动?加一层SYS_INIT:
static int sensors_startup(void) { STRUCT_SECTION_FOREACH(sensor_module, m) { if (m->init) { m->init(); } } return 0; } SYS_INIT(sensors_startup, APPLICATION, 99);到此重构完成。前后对比:main.c瘦回 30 行;模块增删只动自己的 .c 文件;新人再也不用去公共文件里占坑,合并冲突直接消失。
什么时候别用
这套机制不是免费的午餐,两种情况我建议你老老实实写手动调用:
模块就两三个。机制引入了头文件、.ld 文件、CMake 改动三样"仪式性"代码,两三个模块的收益覆盖不了成本,直接调函数五秒钟的事。
模块有严格初始化顺序依赖。iterable section 的顺序由SORT_BY_NAME按变量名字典序决定——隐式顺序对有依赖的初始化(先起总线、再挂外设)是坑。Zephyr 内核自己都用SORT按优先级数值排(SYS_INIT 的 level/prio 就是干这个的),你要顺序依赖就得自己加优先级字段和排序逻辑,复杂度立刻上来。
我的经验线:模块超过 5 个、彼此无顺序依赖,上;否则先不上。
写在最后
三关总结成一句话:第一关靠链接器把散落各文件的注册项聚成堆,第二关用__used__+KEEP双保险让它活到运行期,第三关框架在边界符号间扫堆调用。Zephyr 把这三关包装成了两个宏 + 一行 CMake,你抄的其实是它整个内核的模块组织哲学——SYS_INIT、SHELL_CMD_REGISTER、BT_GATT_SERVICE_DEFINE,翻开看全是同一副骨架。
有用的话点个在看,让更多工程师看到。你们的main.c里现在堆了多少个 init?评论区报个数。
标签:
ZephyrNordic嵌入式链接器模块化设计