RT-Thread里面有个特别有意思的软件设计叫做组件自动初始化。
有些小伙伴可能是第一次听说,所以这边我解释一下,请看下面的代码片段
static void clock_init() {
// 时钟初始化
}
static void uart_init() {
// 串口初始化
}
static void i2c_init() {
// I2C初始化
}
int main() {
clock_init();
uart_init();
i2c_init();
// 业务代码
}
在main函数中我们依次调用了clock_init/uart_init/i2c_init这些必要的初始化操作,如果后续我们还要添加pwm的初始化,我们需要再次在main函数里面添加pwm_init调用这样的代码;但是当一个系统中各种各样的初始化比较多时,我们很容易忘记对某个模块或者功能调用初始化函数。
那有没有一种更加高效简单的并且不易出错的方式呢?那就是组件初始化。
本篇就给大家详细讲解一下RT-Thread的组件初始化实现原理。
本篇涉及到的知识点比较多,每个知识点都需要理解。
在正式介绍之前,大家需要知道几个基本知识点。
函数指针类型和函数指针变量
编译器基本知识
GNU属性扩展__attribute__
指针引用与解引用
在RT-Thread源码有这样两个宏定义
#define RT_SECTION(x) __attribute__((section(x)))
通常情况下编译器会将代码放置在.code段中,数据放置在.data或者.bss段;有些时候我们可能想将某个代码或者数据放置在特殊的段内,就可以使用section属性,具体用法如下
int a RT_SECTION(".mydata") = 10;
RT_SECTION(".mybss") int b;
int my_function() RT_SECTION(".mycode");
int my_funciton() {
return 0;
}
上面的代码片段中,全局变量a放置在.mydata段,全局变量b放置在.mybss段,my_function函数放置在.mycode段。
关于section的详细说明请查看
Variable Attributes - Using the GNU Compiler Collection (GCC)
#define RT_USED __attribute__((used))
有些时候我们可能定义了一些函数或者变量并没有被引用,编译可能会将这些函数或者变量从目标文件中去除,used属性的作用就是保留这些符号。
接下来我们来看一下跟组件初始化有关的宏定义
/* initialization export */
#ifdef RT_USING_COMPONENTS_INIT
typedef int (*init_fn_t)(void);
#ifdef _MSC_VER
#pragma section("rti_fn$f",read)
#if RT_DEBUG_INIT
struct rt_init_desc
{
const char* level;
const init_fn_t fn;
const char* fn_name;
};
#define INIT_EXPORT(fn, level) \
const char __rti_level_##fn[] = ".rti_fn." level; \
const char __rti_##fn##_name[] = #fn; \
__declspec(allocate("rti_fn$f")) \
RT_USED const struct rt_init_desc __rt_init_msc_##fn = \
{__rti_level_##fn, fn, __rti_##fn##_name};
#else
struct rt_init_desc
{
const char* level;
const init_fn_t fn;
};
#define INIT_EXPORT(fn, level) \
const char __rti_level_##fn[] = ".rti_fn." level; \
__declspec(allocate("rti_fn$f")) \
RT_USED const struct rt_init_desc __rt_init_msc_##fn = \
{__rti_level_##fn, fn };
#endif
#else
#if RT_DEBUG_INIT
struct rt_init_desc
{
const char* fn_name;
const init_fn_t fn;
};
#define INIT_EXPORT(fn, level) \
const char __rti_##fn##_name[] = #fn; \
RT_USED const struct rt_init_desc __rt_init_desc_##fn RT_SECTION(".rti_fn." level) = \
{ __rti_##fn##_name, fn};
#else
#define INIT_EXPORT(fn, level) \
RT_USED const init_fn_t __rt_init_##fn RT_SECTION(".rti_fn." level) = fn
#endif
#endif
#else
#define INIT_EXPORT(fn, level)
#endif
/* board init routines will be called in board_init() function */
#define INIT_BOARD_EXPORT(fn) INIT_EXPORT(fn, "1")
/* pre/device/component/env/app init routines will be called in init_thread */
/* components pre-initialization (pure software initialization) */
#define INIT_PREV_EXPORT(fn) INIT_EXPORT(fn, "2")
/* device initialization */
#define INIT_DEVICE_EXPORT(fn) INIT_EXPORT(fn, "3")
/* components initialization (dfs, lwip, ...) */
#define INIT_COMPONENT_EXPORT(fn) INIT_EXPORT(fn, "4")
/* environment initialization (mount disk, ...) */
#define INIT_ENV_EXPORT(fn) INIT_EXPORT(fn, "5")
/* application initialization (rtgui application etc ...) */
#define INIT_APP_EXPORT(fn) INIT_EXPORT(fn, "6")
首先看看一下关于INIT_EXPORT的宏定义
typedef int (*init_fn_t)(void);
#if RT_DEBUG_INIT
struct rt_init_desc
{
const char* fn_name;
const init_fn_t fn;
};
#define INIT_EXPORT(fn, level) \
const char __rti_##fn##_name[] = #fn; \
RT_USED const struct rt_init_desc __rt_init_desc_##fn RT_SECTION(".rti_fn." level) = \
{ __rti_##fn##_name, fn};
#else
#define INIT_EXPORT(fn, level) \
RT_USED const init_fn_t __rt_init_##fn RT_SECTION(".rti_fn." level) = fn
#endif
首先关于代码行
typedef int (*init_fn_t)(void);
这是一个函数指针类型声明,没有入参,返回值为int。关于函数指针类型和函数指针变量的说明请看上面的链接,这边不再赘述。
其次代码行
RT_USED const init_fn_t __rt_init_##fn RT_SECTION(".rti_fn." level) = fn
RT_USED我们已经讲解过了,用于限定函数或者变量属性的。
##是用于宏定义中拼接字符使用的
假如我在代码中按照下面的代码片段调用INIT_EXPORT宏
int myfunction() {
return 0;
}
INIT_EXPORT(myfunction, "1");
展开后
__attribute__((used)) const init_fn_t __rt_init_myfunction __attribute__((section(".rti_fn.1"))) = myfunction;
注意此处的__rt_init_myfunction是个函数指针类型变量,指向myfunction这个函数,既然是变量那么其类型就是Data,并且这个变量最终放在.rti_fn.1段中。
/* board init routines will be called in board_init() function */
#define INIT_BOARD_EXPORT(fn) INIT_EXPORT(fn, "1")
/* pre/device/component/env/app init routines will be called in init_thread */
/* components pre-initialization (pure software initialization) */
#define INIT_PREV_EXPORT(fn) INIT_EXPORT(fn, "2")
/* device initialization */
#define INIT_DEVICE_EXPORT(fn) INIT_EXPORT(fn, "3")
/* components initialization (dfs, lwip, ...) */
#define INIT_COMPONENT_EXPORT(fn) INIT_EXPORT(fn, "4")
/* environment initialization (mount disk, ...) */
#define INIT_ENV_EXPORT(fn) INIT_EXPORT(fn, "5")
/* application initialization (rtgui application etc ...) */
#define INIT_APP_EXPORT(fn) INIT_EXPORT(fn, "6")
这几个宏都是INIT_EXPORT的扩展,区别在于通过不同的宏限定后其段名的区别分别为.rti_fn.1、.rti_fn.2、.rti_fn.3、.rti_fn.4、.rti_fn.5
注意这边关于编译链接有个知识点
编译器会按照段名对符号进行排序,排序方式默认是按照字符的升序排序;也就是说用INIT_BOARD_EXPORT的限定的函数符号的地址肯定比INIT_APP_EXPORT限定的地址小。
有些博客中没有提到这点,或许是认为大家都对编译链接的过程很了解。
有了上面介绍的知识点后,我们从代码层面来详细说明组件初始化的实现
static int rti_start(void)
{
return 0;
}
INIT_EXPORT(rti_start, "0");
static int rti_board_start(void)
{
return 0;
}
INIT_EXPORT(rti_board_start, "0.end");
static int rti_board_end(void)
{
return 0;
}
INIT_EXPORT(rti_board_end, "1.end");
static int rti_end(void)
{
return 0;
}
INIT_EXPORT(rti_end, "6.end");
上面的代码片段定义了rti_start/rti_board_start/rti_board_end/rti_end这几个函数本身之外,还定义了__rt_init_rti_start/__rt_init_rti_board_start/__rt_init_rti_board_end/__rt_init_rti_end这四个init_fn_t类型的函数指针类型变量,并且这些变量的值依次为对应的函数地址。
关于这一点我们通过map文件可以确认上面的描述
首先上图中的每个符号__rt_init_rti_start/__rt_init_rti_board_start/__rt_init_rti_board_end/__rt_init_rti_end的类型都是Data,也就是变量,其次因为每个变量的类型都是函数指针类型,故大小都是4字节,最后每个变量的地址也是按照段名的升序排序。
RT-Thread中关于组件初始化代码
void rt_components_board_init(void)
{
#if RT_DEBUG_INIT
int result;
const struct rt_init_desc *desc;
for (desc = &__rt_init_desc_rti_board_start; desc < &__rt_init_desc_rti_board_end; desc ++)
{
rt_kprintf("initialize %s", desc->fn_name);
result = desc->fn();
rt_kprintf(":%d done\n", result);
}
#else
volatile const init_fn_t *fn_ptr;
for (fn_ptr = &__rt_init_rti_board_start; fn_ptr < &__rt_init_rti_board_end; fn_ptr++)
{
(*fn_ptr)();
}
#endif /* RT_DEBUG_INIT */
}
我们只关注这段代码
volatile const init_fn_t *fn_ptr;
for (fn_ptr = &__rt_init_rti_board_start; fn_ptr < &__rt_init_rti_board_end; fn_ptr++)
{
(*fn_ptr)();
}
for循环中首先对变量__rt_init_rti_board_start取地址赋值给fn_ptr这个函数指针类型的指针,然后再解引用,因为__rt_init_rti_board_start这个变量的值是一个函数地址,所以
(*fn_ptr)();
就是执行__rt_init_rti_board_start变量引用的函数;由于通过组件初始化宏
INIT_BOARD_EXPORT(fn)
限定的函数都有一个对应的变量来记录其地址,这些变量的地址都被限定在变量__rt_init_rti_board_start/__rt_init_rti_board_end的地址区间内,故可以通过此循环依次执行这些函数,例如上图中__rt_init_mpu_init变量就保存的是mpu_init函数的地址。
void rt_components_init(void)
{
#if RT_DEBUG_INIT
int result;
const struct rt_init_desc *desc;
rt_kprintf("do components initialization.\n");
for (desc = &__rt_init_desc_rti_board_end; desc < &__rt_init_desc_rti_end; desc ++)
{
rt_kprintf("initialize %s", desc->fn_name);
result = desc->fn();
rt_kprintf(":%d done\n", result);
}
#else
volatile const init_fn_t *fn_ptr;
for (fn_ptr = &__rt_init_rti_board_end; fn_ptr < &__rt_init_rti_end; fn_ptr ++)
{
(*fn_ptr)();
}
#endif /* RT_DEBUG_INIT */
}
其中代码段
volatile const init_fn_t *fn_ptr;
for (fn_ptr = &__rt_init_rti_board_end; fn_ptr < &__rt_init_rti_end; fn_ptr ++)
{
(*fn_ptr)();
}
代码基本相同,只是for循环限定的函数是__rt_init_rti_board_end/__rt_init_rti_end两个地址区间内的。
另外需要注意的是rt_components_init是main_thread_entry线程函数执行,也就是说此时调度器已经运行;rt_components_board_init在rt_hw_board_init函数被调用,此时调度器还未运行。
最后关于组件初始化的一些参考资料大家可以查看RT-Thread官网组件初始化