很多朋友在写单片机程序时,常会遇到这样的问题:所有模块的初始化函数(比如LED初始化、串口初始化、传感器初始化),都要手动在main函数里一一调用,不仅代码混乱、维护麻烦,而且新增或删除模块时,还要修改main函数,违背了“高内聚、低耦合”的原则。
其实在Linux系统中,module_init机制的核心思想也是一样的,Linux内核本身就是高度模块化的设计——驱动开发者只需通过module_init宏注册驱动初始化函数,无需修改内核核心代码,内核启动时会自动遍历所有注册的初始化函数,完成驱动加载;当模块卸载时,通过module_exit宏注册的退出函数会被自动调用,这也是Linux驱动“热插拔”特性的基础之一。
module_init机制,简单说就是「自动注册、统一初始化」—— 每个模块的初始化函数,通过宏定义“注册”到系统中,程序启动后,自动遍历所有注册的初始化函数并执行。
可能有朋友会说:“单片机没有操作系统(比如Linux)里的module_init宏,怎么实现?” 其实原理很简单,核心就是利用「编译器特性」和「函数指针数组」,手动模拟出这一机制。今天就以STM32为例来介绍如何实现这一功能;
我们先分析一下linux 中module_init宏
定位到Linux内核源码中的include/linux/init.h,可以看到有如下代码:
// 省略// 省略intinit_module(void) __attribute__((alias(#initfn)));// 省略
第一种情况:静态编译(#ifndefMODULE)。当MODULE未定义时,module_init(x)被宏定义为__initcall(x)。__initcall是Linux内核中用于标记静态初始化函数的宏,被该宏标记的函数会被放入内核的.initcall段中。其核心源码同样位于include/linux/init.h中,相关定义如下(只粘贴了相关的内容):
//...typedefint(*initcall_t)(void);//...//...//...staticinitcall_t __initcall___attribute__((__section__(".initcall.init"), __cold__)) = fn;//...// ...staticinitcall_t __initcall___attribute__((__section__(".initcall"
源码解析:__initcall宏的核心作用,是通过编译器__attribute__((__section__))指令,将初始化函数fn放入内核的.initcall相关段中(不同优先级对应不同子段)。其中__used属性确保函数不被编译器优化删除,__cold__属性标记该函数为冷函数(仅启动时调用,优化编译)。被该宏标记的函数,会被内核启动流程自动遍历执行,从而完成静态模块的初始化。这种方式下,模块会被直接编译到内核镜像中,内核启动时就完成初始化,无法动态卸载。
而内核启动时,正是通过do_initcalls函数来遍历并执行所有注册的静态初始化函数,该函数源码位于内核init/main.c中,核心实现如下:
staticvoid__initdo_initcalls(void){intlevel;for(level =0; level < ARRAY_SIZE(initcall_levels) - 1; level++)do_initcall_level(level);}
函数解析:do_initcalls函数是静态初始化的“总入口”,其核心逻辑是按优先级顺序遍历所有初始化层级。其中:
-
initcall_levels是一个数组,存储了前文提到的不同优先级.initcall段(如.initcall0.init、.initcall1.init等)的起始地址,对应pure_initcall、core_initcall等不同优先级的初始化宏。 -
ARRAY_SIZE(initcall_levels) - 1用于获取优先级层级总数,避免越界。 -
do_initcall_level(level)是底层执行函数,其核心实现(简化版,贴合内核源码逻辑)如下,传入优先级level后,会遍历该优先级下所有注册的初始化函数并依次调用,确保高优先级的初始化函数(如内核核心模块)先执行,低优先级函数(如设备驱动)后执行:
staticvoid__initdo_initcall_level(intlevel){// 省略:参数校验、打印调试信息等冗余代码initcall_t *fn;// 定义函数指针,用于遍历当前优先级的初始化函数// 遍历当前优先级level对应的.initcall段:从当前层级起始地址,到下一层级起始地址for(fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)do_one_initcall(*fn);// 调用单个初始化函数,完成该函数的执行}
函数解析:do_initcall_level是do_initcalls函数的底层执行载体,专门负责处理单个优先级层级的初始化,核心逻辑拆解如下:
-
initcall_t *fn:定义与前文一致的初始化函数指针,用于遍历当前优先级下的所有初始化函数。 -
initcall_levels[level]:获取当前优先级level对应的.initcall段起始地址(比如level=1对应.core_initcall宏注册的函数所在段起始);initcall_levels[level+1]则是下一个优先级层级的起始地址,两者构成当前优先级的地址范围,确保只遍历当前层级的函数,不跨层级执行。 -
do_one_initcall(*fn):真正执行单个初始化函数的接口,传入当前遍历到的函数指针(*fn即具体的初始化函数),完成该模块的初始化;该函数内部会做函数合法性校验、执行状态判断等,确保初始化过程稳定。
do_initcalls函数的层级遍历,高优先级先执行,满足内核启动时“先核心、后外设”的初始化逻辑。
第二种情况:动态加载(#else分支)。当MODULE被定义时,module_init(initfn)会通过__attribute__((alias(#initfn)))定义一个别名函数init_module,该别名指向我们自己编写的初始化函数initfn。此时模块会被编译为.ko(内核模块)文件,后续可通过insmod命令动态加载到内核中——内核加载模块时,会自动调用init_module函数(即我们的初始化函数);卸载模块时,通过rmmod命令调用module_exit注册的退出函数,实现模块的热插拔,这也是Linux驱动动态开发的核心方式。
1.机制原理
module_init机制的核心,本质是把所有模块的初始化函数地址,存到一个数组里,程序启动后,循环调用这个数组里的所有函数。
用到两个关键知识点
-
函数指针:可以理解为“存放函数地址的变量”,通过函数指针,我们可以间接调用函数(比如void (*init_func)(void); 就是一个指向“无参数、无返回值”初始化函数的指针)。
-
编译器section特性:我们可以通过编译器指令,将所有注册的初始化函数指针,集中存放在指定的内存区域(section),后续只需找到这个区域的起始和结束地址,就能遍历所有函数。
流程:模块注册 → 函数指针存入指定section → 程序启动 → 遍历section中的函数指针 → 依次调用(完成所有模块初始化)。
下面我们讲实现方式:
我使用的是keil
我把我的代码贴出来
.h文件
.c文件typedefint(*initcall_t)(void);constinitcall_t __initcall___attribute__((section(".__initcall.0.b"), used)) = fn
staticconstinitcall_t__initcall_sentinel_start__attribute__((used,section(".__initcall.0.a"))) =0;staticconstinitcall_t__initcall_sentinel_end__attribute__((used,section(".__initcall.0.c"))) =0;voidmodule_init_call(void){constinitcall_t*call_start = &__initcall_sentinel_start;constinitcall_t*call_end = &__initcall_sentinel_end;while(call_start < call_end){if(*call_start)(*call_start)();call_start++;}}
在初始化函数下面添加 module_init宏如:
int test_init(void){return0;}module_init(test_init);
编译之后我们在.map文件里可以看到

初始化函数被放在的相应的区域里面
module_init_call函数放在了Reset_Handler,每次上电启动的时候就会调用
大家可以根据不同的优先级设置多个数组,跟linux一个区遍历整个区域去做初始化参考链接:https://blog.csdn.net/weixin_37571125/article/details/78665184Reset_Handler PROCEXPORT Reset_HandlerIMPORT SystemInitIMPORT __mainIMPORT module_init_callLDR R0, =0xE000ED88 ; 使能浮点运算 CP10,CP11LDR R1,[R0]ORR R1,R1,#(0xF << 20)STR R1,[R0]LDR R0, =SystemInitBLX R0LDR R0, =module_init_callBLX R0LDR R0, =__mainBX R0ENDP
https://zhuanlan.zhihu.com/p/615272622
-
单片机
+关注
关注
6078文章
45591浏览量
673962 -
嵌入式
+关注
关注
5210文章
20680浏览量
337363 -
Linux
+关注
关注
88文章
11822浏览量
219600
发布评论请先 登录
linux内核使用链接脚本模仿module_init机制实战
单片机嵌入式Internet技术的Web应用实现
什么是嵌入式单片机?嵌入式单片机详情汇总
详解嵌入式Linux设备驱动篇module_init
linux驱动的入口函数module_init的加载和释放
单片机与嵌入式的转化
单片机与嵌入式区别
单片机or嵌入式linux
单片机是嵌入式的子类
单片机和嵌入式的区别
IAR 实现类linux驱动模块框架module_init(init_fun)
嵌入式2---在单片机里实现module_init机制
评论