代码自动初始化
概述
在嵌入式开发过程中,可能会遇到初始化代码自动初始化,比如RTT中就运用到这项技术。那么初始化代码是如何做到自动化调用的呢?
在嵌入式实际开发过程中,往往需要对 bsp 部分进行外设配置,以及一些模块、参数进行初始化,常见的方法如下:
- 将各个部分的初始化代码分别封装成一个单独的函数,然后再main函数刚开始的地方进行调用已实现初始化;
该方法简单粗暴,但是此方法存在一些不足: - 所有初始化均需要在main函数内调用,不能做到彻底解耦
- 框架设计不友好,部分非业务部分初始化,如软件框架类的初始化,往往不希望还需要业务在main函数内调用初始化
那有没有一种方法能解决上述的问题呢?
答案是肯定的,本文将向大家介绍一种自动初始化的实现方式。注意,此方案在不同的平台,由于链接器使用的链接脚本可能不一致,容易出现问题,需要大家重点注意,细节在下文中将详细介绍。
案例讲解
为了更好的阐述代码自动初始化的技术,本文通过一个demo程序进行讲解如何实现该技术,以下是该demo程序:
typedef void (*init_func)(void);
#define INIT_EXPORT(fn, level) const init_func init_##fn __attribute__((used, section(".init."level))) = fn
void start(void)
{
return;
}
INIT_EXPORT(start, "1");
void end(void)
{
return;
}
INIT_EXPORT(end, "4");
接下来分析上述代码:
关于宏定义:#define INIT_EXPORT(fn, level) const init_func init_##fn attribute((used, section(".init."level))) = fn的代码分析如下:
- 首先定义一个函数指针typedef void (*init_func)(void);用于所有初始化函数的指针类型;
- 定义宏INIT_EXPORT(fn, level) 作为一个函数接口,后续的初始化函数可通过此接口加入自动初始化列表内
- const init_func init_##fn,其中##为连接符,假设参数fn为test,则init_##fn为init_test;init_func 为指令类型
- const init_func init_##fn与const int *p是同一个概念,只不过数据的类型不一样
- const init_func init_##fn定义了一个init_func类型的指针变量,此指针还没有赋值初始化,因此将参数fn赋值给此变量,宏定义可以简化为#define INIT_PORT(fn, level) const init_func init_##fn = fn(__attribute__先忽略)
- attribute((used, section(".init."level))) 这是一个关键字进行修饰,可以做如下拆解:
- attribute((used))用来告诉编译器,此函数如果没有被调用,也不要被优化
- attribute((section(".init."level)))用来修饰的内容放入指定的段“.init.”level中(编译器在你编程的时候自动完成),注意level为宏的第二个传入参数,传入来的时候是一个字符串,所以不用加#加进行拼接,C语言中两个字符串会自动拼接。
综述分析:宏#define INIT_EXPORT(fn, level) const init_func init_##fn attribute((used, section(".init.“level))) = fn,其实定义了一个init_func类型的函数指针,并将此函数指针通过__attribute__((used, section(”.init."level)))指定存放到特定的段内。
demo程序中定义了两个函数:
- void start(void)
- void end(void)
并通过宏INIT_EXPORT(fn,level)添加到自动初始化段内 - INIT_EXPORT(start, “1”);
- INIT_EXPORT(end, “6”);
注意事项:
- level参数是一个字符串,而不是数字
- 用户自定义初始化函数编译器会自动添加到start和end的level值中间,是由于编译器采用__attribute__((section(".init."level))) 指定放入到指定段的时候会针对其进行排序,keil默认采用根据名称的方式排序,因此需要放置在用户自定义的,放置在用 start 和 end 中间,方便后续遍历 start 和 end 中间部分进行完整初始化
最后的vhdl_board_init初始化函数:
void vhdl_board_init(void)
{
const init_func *fn_ptr = NULL;
for (fn_ptr = &init_start; fn_ptr < &init_end; fn_ptr++)
{
(*fn_ptr)();
}
}
此函数在main函数中调用,通过for循环遍历start和end中间的段,并调用进行初始化。
通过上述一系列的操作,用户初始化函数时只需要调用宏INIT_EXPORT(fn,level)即可完成自动初始化。
为了针对不同类型的初始化,可以将start和end之间距离拉大点,用作区别不同类型的初始化,如下所示
INIT_EXPORT(start, "1");
INIT_EXPORT(end, "6");
并且对外开放相应的接口宏
#define INIT_BSP_PORT(func) INIT_EXPORT(func, "2");
#define INIT_DATA_PORT(func) INIT_EXPORT(func, "3");
用户通过调用 BSP_INIT_PORT 和 DATA_INIT_PORT 进行注册
检测初始是否成功??
检测MAP文件
在上述方法中,我们使用 attribute((section(".init."level))) 将数据存入指定的段内,那么是否成功了呢,以及排序方式是否符合我们的预期呢?接下来我们需要通过检查map文件进行确认。
keil / MDK 环境检查
keil/mdk编译完程序之后,双击工程栏处的工程,可以查看map文件,还不知道的可以网上搜下
以下是上述demo的map文件中关键字段,我们可以看到我们需要的数据存放在正确的段内,并且排序也是正常的,注意一定要检查排序和字段是否完整!
gcc 环境检查
详细过程见gcc环境检查
综上所述:按照demo程序编写初始化代码是不是变得更见简单了。