RT_Thread 内核启动分析
无论是硬件系统还是软件系统,了解它的第一步需要知道它和他的low level如何接轨,并如何实现在其上面启动跑起来。
RT_thread 一样 RT_Thread 支持多种平台和多种编译器,rtthread_startup()函数是RT_Thread规定的统一入口。
执行顺序是:系统先从启动文件开始运行,然后进入RT_Thread的启动函数,最后进入用户入口函数。 基本的启动流程可以参考
RT-Thread 文档中心https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/basic/basic?id=rt-thread-%e7%a8%8b%e5%ba%8f%e5%86%85%e5%ad%98%e5%88%86%e5%b8%83这里主要列些一些关键逻辑
对于不同平台来讲 可以通过不同的方式将rtthread_startup()加载在main函数前面 文档中心以MDK_ARM为例写了如何加载在main函数前面
我现在以RTThread Studio GCC编译器为例
#elif defined(__GNUC__)
/* Add -eentry to arm-none-eabi-gcc argument */
int entry(void)
{
rtthread_startup();
return 0;
}
程序内部 告诉我们在使用gcc编译时加入参数 --eentry 即可
工程 右键 属性 即可进入下图所示选项 可以在里面配置编译选项,目前我还没找到 --entry入口在哪里加进去了,留坑。
按照文档中心说的 startup函数内
这里主要说两个初始化 rt_components_board_init() 和 rt_components_init() 函数名字很相似,但是调用顺序和功能大不相同
rt_components_board_init():
通过调用drv_common 内的函数 完成板子驱动层对接HAL层的硬件初始化
主要堆初始化 时钟初始化 控制台初始化 组件板级初始化 这是板子上其他功能正常运行的前提。所以系统启动的第一步便是执行这个。
我们经常就可以看到 RTM_EXPORT()
如果在RT操作系统下 使用 MODULE 会通过下述代码 根据不同的编译器,采用不同的方式到处模块符号,这些符号信息会被记录在特定的数据结构和段中,方便模块加载和管理时使用,目前我没用到 宏定义为空, 就是不需要单独导出模块。
#ifdef RT_USING_MODULE
struct rt_module_symtab
{
void *addr;
const char *name;
};
#if defined(_MSC_VER)
#pragma section("RTMSymTab$f",read)
#define RTM_EXPORT(symbol) \
__declspec(allocate("RTMSymTab$f"))const char __rtmsym_##symbol##_name[] = "__vs_rtm_"#symbol;
#pragma comment(linker, "/merge:RTMSymTab=mytext")
#elif defined(__MINGW32__)
#define RTM_EXPORT(symbol)
#else
#define RTM_EXPORT(symbol) \
const char __rtmsym_##symbol##_name[] SECTION(".rodata.name") = #symbol; \
const struct rt_module_symtab __rtmsym_##symbol SECTION("RTMSymTab")= \
{ \
(void *)&symbol, \
__rtmsym_##symbol##_name \
};
#endif
rt_components_init():
首先rt_components_init()是main_thread_entry()内实现调用的。
这里值得一提的是:
/*
* Components Initialization will initialize some driver and components as following
* order:
* rti_start --> 0
* BOARD_EXPORT --> 1
* rti_board_end --> 1.end
*
* DEVICE_EXPORT --> 2
* COMPONENT_EXPORT --> 3
* FS_EXPORT --> 4
* ENV_EXPORT --> 5
* APP_EXPORT --> 6
*
* rti_end --> 6.end
*
* These automatically initialization, the driver or component initial function must
* be defined with:
* INIT_BOARD_EXPORT(fn);
* INIT_DEVICE_EXPORT(fn);
* ...
* INIT_APP_EXPORT(fn);
* etc.
*/
这是rt_thread比较有特点的初始化,隐藏显示调用,“自动”完成初始化的核心 ,下面分析一下怎么完成这个功能的。
1.如果使用了组件初始化的功能
就会定义一个宏
#define INIT_EXPORT(fn, level) \
RT_USED const init_fn_t __rt_init_##fn SECTION(".rti_fn." level) = fn
#endif
这个宏分开解释意思为:
RT_USED 根据编译器类型不同 告诉编译器此段代码不管有没有显示调用 都不能被优化
然后定义一个函数指针 将指针赋值为函数名,函数名即为函数地址 ,通过这种方式统一了代码架构 格式
section属性是__attribute__支持的一个属性,它的作用是指定变量或函数应该被放置在哪个段中。
常见的段有.text(存放代码) .data(存放已初始化的全局变量和静态变量) .bss(存放未初始化的全局变量和静态变量)等
通过section属性,可以创建自定义的段,或者将变量、函数放置到特定的已有段中。
上述即为把对应的函数和变量放到名为.rti_fn_level 段中 level为字符串
将上述两者结合起来,可以知道rt_thread 将段分为
.rti_fn.0
.rti_fn.0.end
.rti_fn.1
.rti_fn.1.end
.rti.2
.rti.3
.rti.4
.rti.5
.rti.6
按照既定规则,将函数保存到这些段地址。至于如何自动到这些段初始化呢? 以rt_components_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)();
}
宏定义时将函数地址统一编址命名为_rt_init前缀的变量,并按照初始化级别区分了
通过取内存里上述定义的函数指针的地址,遍历完成初始化。这里完成的是 2 3 4 5 6 的初始化也是按照先后初始化 但是统一在主线程完成初始化。
对于链接编译完成后 可以验证性的结果可以在生成的map文件内查看,可以看到
*(SORT(.rti_fn*))
.rti_fn.0 0x0800e0d0 0x4 ./rt-thread/src/components.o
0x0800e0d0 __rt_init_rti_start
.rti_fn.0.end 0x0800e0d4 0x4 ./rt-thread/src/components.o
0x0800e0d4 __rt_init_rti_board_start
.rti_fn.1 0x0800e0d8 0x4 ./drivers/drv_clk.o
0x0800e0d8 __rt_init_clock_information
.rti_fn.1.end 0x0800e0dc 0x4 ./rt-thread/src/components.o
0x0800e0dc __rt_init_rti_board_end
.rti_fn.6 0x0800e0e0 0x4 ./rt-thread/components/finsh/shell.o
0x0800e0e0 __rt_init_finsh_system_init
.rti_fn.6 0x0800e0e4 0x4 ./applications/HC_SRO4.o
0x0800e0e4 __rt_init_hcsr04_init
.rti_fn.6.end 0x0800e0e8 0x4 ./rt-thread/src/components.o
0x0800e0e8 __rt_init_rti_end
0x0800e0ec __rt_init_end = .
0x0800e0ec . = ALIGN (0x4)
[!provide] PROVIDE (__ctors_start__, .)
总结完上述结果后,我们就可以通过自动初始化机制,隐式完成对应的初始化设备,什么函数应该在什么顺序调用,描述里也有例子,多用就会越来越熟悉。