本文档对应以下代码:
fc_auto_init 是 core/ 里负责“分阶段自动初始化注册”的模块。
它解决的问题不是“怎么初始化某个对象”,而是:
- 如何把初始化函数注册到固定阶段
- 如何给这些初始化函数设优先级
- 如何在不同编译器下通过段机制把它们收集起来
- 如何在运行时按顺序执行
它本质上提供的是一个“初始化调度框架”。
当前实现把初始化划分为四个阶段:
ENVmain()之前,适合纯数据结构和基础环境CLOCK时钟配置后DEVICE外设初始化阶段APP应用层或系统启动后阶段
整体关系如下:
上电 / 程序装载
|
v
fc_section_init_env()
|
v
main()
|
+--> 时钟配置完成 -> fc_section_init_clock()
|
+--> 外设基础初始化 -> fc_section_init_device()
|
+--> 应用任务启动后 -> fc_section_init_app()
其中只有 ENV 阶段可以在 USE_FC_AUTO_INIT=1 时自动通过 constructor 触发。
后面三个阶段都需要用户显式调用。
+--------------------------------------------------------------+
| 各功能模块的 init 函数 |
| fc_default_port_init / fc_log_init / 自定义 board_init |
+-------------------------------+------------------------------+
|
v
+--------------------------------------------------------------+
| INIT_EXPORT_* 宏层 |
| INIT_EXPORT_ENV / CLOCK / DEVICE / APP |
+-------------------------------+------------------------------+
|
v
+--------------------------------------------------------------+
| 链接段 / section 收集层 |
| fc_section_0 / 1 / 2 / 3 |
+-------------------------------+------------------------------+
|
v
+--------------------------------------------------------------+
| fc_section_init_* 运行层 |
| 按 order 从小到大依次执行 |
+--------------------------------------------------------------+
所以 fc_auto_init 的角色不是“初始化实现者”,而是“初始化注册器 + 调度器”。
真正被放进段里的不是裸函数指针,而是:
typedef struct
{
fc_auto_init_func_t func;
size_t order;
} fc_auto_init_elem_t;这意味着每个注册项有两部分信息:
func实际执行的函数order当前阶段内的优先级
数字越小越早执行。
头文件里还提供了几组默认 order:
FC_PORT_INIT_ORDER = 100FC_TRANS_INIT_ORDER = 110FC_LOG_INIT_ORDER = 120
这些是给基础模块之间拉开依赖顺序用的。
最常用的是这四个:
INIT_EXPORT_ENV(func[, order])INIT_EXPORT_CLOCK(func[, order])INIT_EXPORT_DEVICE(func[, order])INIT_EXPORT_APP(func[, order])
它们最终都会走到:
FC_INIT_EXPORT()
把一个 fc_auto_init_elem_t 放进对应段。
头文件里有三组关键底层宏:
SECTION_EXTERN(section_name)ELEM_EXPORT(section_name, elem)section_info(section_name, type_ptr, count)
它们的作用分别是:
- 声明段起止符号
- 往段里放元素
- 在运行时取出段首地址和元素数量
fc_auto_init 的一个重点是: 它不是只写给某一个编译器的。
头文件里按三类工具链做了适配:
- ARMCC / armclang
- IAR
- GCC
适配思路都是一致的:
- 把元素放进命名 section
- 通过 section 起止符号拿到整段范围
- 运行时把这段解释成
fc_auto_init_elem_t[]
需要注意的是,源码里对:
- IAR
- GCC
都保留了 need test 的 warning,这说明当前作者对这些路径的意图是明确的,但并没有像 ARMCC 路径那样完全宣称已经长期验证。
因此在跨工具链移植时,建议优先先做一次段符号验证。
运行时真正干活的是:
fc_section_init_env()fc_section_init_clock()fc_section_init_device()fc_section_init_app()
每个函数内部都做了两件关键的事。
这个宏通过静态布尔变量保证:
- 每个阶段只执行一次
所以即使外部重复调用 fc_section_init_device(),也不会重复跑设备初始化。
这个宏的运行方式不是“先排序再执行”,而是:
- 先遍历一遍,找出本段元素数、最小 order、最大 order
- 从最小 order 开始执行
- 每轮再扫描一遍,找出比当前更大的最小 order
- 直到本段全部执行完
也就是说,它本质上是一个“小规模按 order 分层扫描”的实现。
这样做的特点是:
- 不需要额外排序内存
- 实现简单
- 适合嵌入式里注册项本来就不多的场景
代价是:
- 时间复杂度不是最优
- 但初始化阶段通常只跑一次,这个代价完全可以接受
同一个 order 下的多个元素会按段内枚举顺序执行。
这通常接近于:
- 链接顺序
- 对象文件顺序
但不应把它当作一个强保证。
如果两个初始化之间存在硬依赖,最好直接把 order 拉开。
fc_auto_init.c 末尾放了一个空函数:
__void__func
并把它注册进四个阶段。
它的作用不是功能逻辑,而是:
- 避免空 section 带来的链接器/编译器告警
所以它属于框架兜底,不是给业务层调用的。
最典型的用法如下:
static void my_board_init(void)
{
/* board init */
}
static void my_uart_init(void)
{
/* uart init */
}
INIT_EXPORT_ENV(my_board_init, 50);
INIT_EXPORT_DEVICE(my_uart_init, 200);然后在程序启动流程里:
int main(void)
{
/* ENV 阶段可由 constructor 自动执行 */
board_clock_config();
fc_section_init_clock();
board_device_init();
fc_section_init_device();
app_start();
fc_section_init_app();
}从 core/ 的依赖关系看,fc_auto_init 更像横向基础设施:
+----------------------+
| fc_port_init |
+----------------------+
|
v
+--------------------------------------------------------------+
| fc_auto_init |
| 段注册 / 优先级 / 分阶段执行 |
+--------------------------------------------------------------+
^
|
+----------------------+
| fc_log_init |
+----------------------+
它不直接做业务,但很多模块会借它把自己的默认初始化挂到统一时序上。
适合放进自动初始化的内容:
- 纯静态对象初始化
- 默认端口、默认日志、协议表注册
- 不依赖复杂运行时上下文的基础设施
不建议放进去的内容:
- 依赖任务上下文的逻辑
- 依赖外部对象已经动态创建的逻辑
- 有明显重入副作用的大型业务流程
简单说:
fc_auto_init适合“框架搭骨架”- 不适合“把整个业务流程塞进 constructor”
最后有几条实际使用时最容易踩到的点:
ENV阶段自动执行并不等于所有阶段都自动执行- 同优先级顺序不要过度依赖,最好显式错开
order - 段机制依赖工具链,移植时先验证 section 起止符号
- 初始化函数必须是“无参、无返回值”
- 如果还想增加更多阶段,建议仿照现有头文件和
fc_auto_init.c的方式扩展,而不是直接把业务逻辑硬塞进当前 4 段里