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

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

一、目的

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文件可以确认上面的描述

首先上图中的每个符号类型都是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官网组件初始化

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6