MCU上实现initcall机制#

类似于linux驱动模块框架module_init(init_fun),该方法在Linux内核中叫做“initcall机制”。

1. 设计思想#

利用编译器的特性,在程序编译时将各模块初始化函数顺序存放到内存的指定地址,然后在程序运行时,从这块内存区域中依次调用各模块的初始化函数,自动完成各个模块的初始化工作,增强了程序的高内聚低耦合特性,提高了软件的质量。

试验环境:#

  • 工具链:Keil5
  • 开发板:STM32F103ZE

2. 修改链接脚本#

修改链接脚本的目的是:新定义一个“程序段”,该段用来存放各个模块的初始化函数(指针)。

首先我们看一下原本的链接脚本。

在Keil中新建一个工程,编译后在工程目录下的Objects文件夹中会自动生成一个以.sct结尾的链接脚本, 其内容如下:

; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************

LR_IROM1 0x08000000 0x00080000  {    ; load region size_region
  ER_IROM1 0x08000000 0x00080000  {  ; load address = execution address
   *.o (RESET, +First)
   *(InRoot$$Sections)
   .ANY (+RO)
   .ANY (+XO)
  }

  ; 此区域(RW_IRAM1)使用所有的内存(64KB)
  RW_IRAM1 0x20000000 0x00010000  {  ; RW data
   .ANY (+RW +ZI)
  }
}

下面我们在链接脚本中新增加一个RW_IRAM2段,并且设置RW_IRAM1段大小为48K,RW_IRAM2段大小为16K。

RW_IRAM2段(0x2000c000~0x00004000)用来顺序存放各模块的初始化函数,该段在STM32运行时会被放到相应的内存区域

示例源码如下:

; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************
LR_IROM1 0x08000000 0x00080000  {    ; load region size_region
  ER_IROM1 0x08000000 0x00080000  {  ; load address = execution address
   *.o (RESET, +First)
   *(InRoot$$Sections)
   .ANY (+RO)
   .ANY (+XO)
  }

  ;RW_IRAM1为48K,供应用程序使用
  RW_IRAM1 0x20000000 0x0000c000  {  ; RW data
   .ANY (+RW +ZI)
  }

  ;RW_IRAM2为16K,用于存放各个模块的初始化函数
  RW_IRAM2 0x2000c000 0x00004000  {  ; RW data
   *.o(RAMCODE)
  }
}

对于只读的变量也可以将其存放到Flash,只需将段地址改为Flash的地址(0x08000000~0x00080000)即可。

更详细的ARM Compiler链接脚本语法,请查阅ARM官方文档:

Scatter File Syntax

友情提示:keil中链接脚本的修改方法如下图所示:

上述操作之后,链接脚本文件将会在Keil的编辑器中被打开。

3. 将指定的函数存放到指定的程序段#

上面我们对STM32的内存进行了分配(链接脚本中的分段),16K大小的RW_IRAM2内存区域将被用来存储初始化函数, 那么怎么才能将指定的函数放进这个区域呢?

3.1 实现方法#

// 初始化函数的原型,函数原型可根据实际需求调整
typedef void (*init_function_list)(uint8_t taskID);

// 通过MODULE_INIT(func),func函数在编译时将会被链接到RAMCODE内存区,既我们为初始化函数预先分配的内存区域
#define _init __attribute__((used, section("RAMCODE")))
#define MODULE_INIT(func) init_function_list _fn_##func _init = func

3.2 解释#

// 将指定的代码放到指定的段
__attribute__((section("section_name")))

// 告诉编译器不要优化该代码,即使它没有被引用
__attribute__((used))

// 这两个属性可以一起使用
__attribute__((used, section("RAMCODE")))

// 展开宏MODULE_INIT(func)
// 其实就是声明一个init_function_list类型的函数指针_fn_func;
// 并且使用__attribute__((used, section("RAMCODE")))修饰指针变量_fn_func,使该变量保存在"RAMCODE"段;
// 然后_fn_func = func。
init_function_list _fn_func __attribute__((used, section("RAMCODE"))) =  func

更详细的信息请查阅ARM官方文档,相关链接如下:

__attribute__((section("name"))) function attribute

__attribute__((used)) function attribute

3.3 存放的顺序#

因为该方法是利用编译器的特性,所以当有多个MODULE_INIT(func)时,先被编译的将存放在前面(低地址)。

但是模块的初始化有顺序要求怎么办?

有时一个模块的正常使用必须在某个模块初始化之后,比如一个模块需要打印Log,那么Log模块必须在它之前被初始化。 怎么解决了?

其实很简单,在调用初始化函数时,我们是根据内存地址顺序调用的,那么将Log模块的初始化函数放在其前面就可以了。

在Keil中可以这么做:

还有一种方法就是,分为多个初始化函数程序段,比如:

RAMCODE1
RAMCODE2
RAMCODE3
RAMCODE4

将需要先初始化的模块放到靠前的程序段中。

在程序运行的时候,先调用RAMCODE1段中的初始化函数,然后调用RAMCODE2段中的初始化函数,依次执行。

小编有话说:

MODULE_INIT(func)方法(或者说initcall机制),主要是应用于各个独立模块的初始化,其初衷就是为了降低耦合。如果在设计程序时各个初始化模块依然相互依赖,那么就违背了我们的初衷(即使可以通过编译顺序解决),所以在设计程序时一定要注意。

4. 统一调用初始化函数#

将所有的初始化函数都写进特定内存区域后,我们只需知道这个区域的起止地址,就可以统一遍历调用所有的初始化函数, 从而实现对各个模块的初始化。

typedef void (*init_function_list)(uint8_t taskID);

void do_init(void)
{
    uint8_t taskID = 0;
    init_function_list *call = (init_function_list *)&Image$$RW_IRAM2$$Base;  // RW_IRAM2段的起始地址
    init_function_list *end = (init_function_list *)&Image$$RW_IRAM2$$Limit;  // RW_IRAM2段的结束地址
  do
    {
        (*call)(taskID++);
        call++;
    }while(call < end);
}

关于ARM链接器标号的更多信息,请查阅ARM官方文档:

Image$$ execution region symbols

5. 使用举例#

Talk is cheap. Show me the code.

完整示例工程源码请点击下载

  • module1.c,module2.c,module3.c 为3个独立的模块,其初始化函数都为内部函数(使用static修饰),符合高内聚低耦合的软件设计思想
  • main.c中,do_init函数直接通过初始化函数的内存地址,对初始化函数逐个进行调用

简单示例代码如下:

  • module1.c
static void module1_init(uint8_t taskID)
{
    // write your code ...
}

MODULE_INIT(module1_init);
  • module2.c
static void module2_init(uint8_t taskID)
{
    // write your code ...
}

MODULE_INIT(module2_init);
  • module3.c
static void module3_init(uint8_t taskID)
{
    // write your code ...
}

MODULE_INIT(module3_init);
  • main.c
void do_init(void)
{
    uint8_t taskID = 0;
    init_function_list *call = (init_function_list *)&Image$$RW_IRAM2$$Base;
    init_function_list *end = (init_function_list *)&Image$$RW_IRAM2$$Limit;
  do
    {
        (*call)(taskID++);
        call++;
    }while(call < end);
}


int main()
{
    // init all module in the function 
    do_init();

    // write your code ...

    while(1)
    {
        // write your code ...
    }
}