RT-Thread系列--组件初始化

一、目的

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官网组件初始化

你可能感兴趣的:(RT-Thread,开发语言,mcu)