嵌入式2---在单片机里实现module_init机制

电子说

1.4w人已加入

描述

嵌入式2---在单片机里实现module_init机制

很多朋友在写单片机程序时,常会遇到这样的问题:所有模块的初始化函数(比如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,可以看到有如下代码:

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
#ifndef MODULE// 省略#define module_init(x)  __initcall(x);// 省略#else
#define module_init(initfn)     int init_module(void) __attribute__((alias(#initfn)));// 省略#endif

第一种情况:静态编译(#ifndef MODULE)。当MODULE未定义时,module_init(x)被宏定义为__initcall(x)。__initcall是Linux内核中用于标记静态初始化函数的宏,被该宏标记的函数会被放入内核的.initcall段中。其核心源码同样位于include/linux/init.h中,相关定义如下(只粘贴了相关的内容):

 
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
//...typedef int (*initcall_t)(void);//...#define __initcall(fn) device_initcall(fn)//...#define device_initcall(fn)     __define_initcall(fn, 6)//...#define __initcall(fn)     static initcall_t __initcall_##fn __used     __attribute__((__section__(".initcall.init"), __cold__)) = fn;//...#define device_initcall(fn)     __define_initcall(fn, 6)// ...#define __define_initcall(fn, id)     static initcall_t __initcall_##fn##id __used     __attribute__((__section__(".initcall" #id ".init"), __cold__)) = fn;

源码解析:__initcall宏的核心作用,是通过编译器__attribute__((__section__))指令,将初始化函数fn放入内核的.initcall相关段中(不同优先级对应不同子段)。其中__used属性确保函数不被编译器优化删除,__cold__属性标记该函数为冷函数(仅启动时调用,优化编译)。被该宏标记的函数,会被内核启动流程自动遍历执行,从而完成静态模块的初始化。这种方式下,模块会被直接编译到内核镜像中,内核启动时就完成初始化,无法动态卸载。

而内核启动时,正是通过do_initcalls函数来遍历并执行所有注册的静态初始化函数,该函数源码位于内核init/main.c中,核心实现如下:

 
  •  
  •  
  •  
  •  
  •  
  •  
  •  
static void __init do_initcalls(void){    int level;
    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后,会遍历该优先级下所有注册的初始化函数并依次调用,确保高优先级的初始化函数(如内核核心模块)先执行,低优先级函数(如设备驱动)后执行:

 
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
static void __init do_initcall_level(int level){    // 省略:参数校验、打印调试信息等冗余代码    initcall_t *fn; // 定义函数指针,用于遍历当前优先级的初始化函数
    // 遍历当前优先级level对应的.initcall段:从当前层级起始地址,到下一层级起始地址    for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)        do_one_initcall(*fn); // 调用单个初始化函数,完成该函数的执行}

函数解析:do_initcall_leveldo_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机制的核心,本质是把所有模块的初始化函数地址,存到一个数组里,程序启动后,循环调用这个数组里的所有函数。

用到两个关键知识点

  1. 函数指针:可以理解为“存放函数地址的变量”,通过函数指针,我们可以间接调用函数(比如void (*init_func)(void); 就是一个指向“无参数、无返回值”初始化函数的指针)。

  2. 编译器section特性:我们可以通过编译器指令,将所有注册的初始化函数指针,集中存放在指定的内存区域(section),后续只需找到这个区域的起始和结束地址,就能遍历所有函数。

流程:模块注册 → 函数指针存入指定section → 程序启动 → 遍历section中的函数指针 → 依次调用(完成所有模块初始化)。

 

 

下面我们讲实现方式:

 

我使用的是keil 

我把我的代码贴出来

.h文件

  •  
  •  
  •  
  •  
  •  
typedef int (*initcall_t)(void);#define __init__attribute__((unused))#define module_init(fn) const initcall_t __initcall_##fn __attribute__((section(".__initcall.0.b"), used)) = fn
.c文件
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
static const initcall_t __initcall_sentinel_start    __attribute__((used, section(".__initcall.0.a"))) = 0;
static const initcall_t __initcall_sentinel_end    __attribute__((used, section(".__initcall.0.c"))) = 0;

void module_init_call (void){
const initcall_t *call_start = &__initcall_sentinel_start;    const initcall_t *call_end = &__initcall_sentinel_end;while(call_start < call_end){if(*call_start)(*call_start)();call_start++;}}

在初始化函数下面添加 module_init宏如:

  •  
  •  
  •  
  •  
  •  
  •  
int  test_init(void){return 0;}module_init(test_init);

编译之后我们在.map文件里可以看到

初始化函数被放在的相应的区域里面

module_init_call函数放在了Reset_Handler,每次上电启动的时候就会调用

 
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
Reset_Handler    PROC                 EXPORT  Reset_Handler             [WEAK]        IMPORT  SystemInit        IMPORT  __mainIMPORT  module_init_call                 LDR     R0, =0xE000ED88    ; 使能浮点运算 CP10,CP11                 LDR     R1,[R0]                 ORR     R1,R1,#(0xF << 20)                 STR     R1,[R0]                 LDR     R0, =SystemInit                 BLX     R0 LDR     R0, =module_init_call                 BLX     R0                 LDR     R0, =__main                 BX      R0                 ENDP
大家可以根据不同的优先级设置多个数组,跟linux一个区遍历整个区域去做初始化参考链接:https://blog.csdn.net/weixin_37571125/article/details/78665184

https://zhuanlan.zhihu.com/p/615272622

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 相关推荐
  • 热点推荐
  • L

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分