这里介绍的启动过程是以 STM32F407 芯片为例;
使用的启动文件是适配 keil MDK 编译环境的,startup_stm32f407xx.S
启动文件;
RT-Thread 版本是 3.1.5 版本代码。
每款MCU芯片都有对应的官方使用汇编文件编写的启动文件 start_up_xxx.S
,这个启动文件主要是完成芯片时钟初始化,配置芯片的中断向量表、全局/静态变量初始化、设置栈顶指针等工作。
在完成这些工作后,如果没有使用 RT-Thread 系统,那么就会跳转到用户的 main 函数,执行用户相关的代码了。下面是 startup_stm32f407xx.S
这个启动文件的 Resert_handler 的汇编代码。
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT SystemInit
IMPORT __main
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
其中调用了 SystemInit 函数初始化了系统时钟之后,就跳转到了 __main 函数了,这个函数是 keil MDK 内置的一个函数,主要是完成数据段重定位、bss段清零等工作,然后最终跳转到用户 main 函数。
上面介绍的就是没有添加 RT-Thread 的芯片上电启动过程。当我们添加了 RT-Thread 之后,启动过程还是一样的,只是在调用 main 函数之前,调用了 RT-Thread 提供的一部分初始化代码,然后由 RT-Thread 启动 main 函数。即:
如下,是加入了 RT-Thread 之后的启动流程。
当运行完启动文件之后,就会运行 rtthread_startup(void)
这个RT-Thread 统一的入口函数,这个入口函数主要完成了以下工作:
厂商提供的 startup_stm32f407xx.S
这个启动文件中,可以了解到最后是跳转到了 __main 函数运行的。跳转到这个函数之后,为什么会先运行 RT-Thread 提供的 rtthread_startup(void)
入口函数呢?
这就涉及到 __CC_ARM 编译器(keil MDK就是用这个编译器的)环境下的 $Sub$$
与$Super$$
补丁功能了,使用这个功能可以在不修改原函数的情况下在函数之前或者之后添加希望运行的代码。如对 main 函数的修饰作用:
$Sub$$main
:新定义的main函数“补丁”,在运行 main 函数之前会先运行这个函数。$Super$$main
:调用这个函数将直接跳到原始 mian 函数。而RT-Thread 在 components.c 文件中使用上面介绍的两个修饰符,如下面一段代码所示:
#if defined(__CC_ARM) || defined(__CLANG_ARM) // 使用 __CC_ARM 或者 __CLANG_ARM 编译器,比如 keil MDK 使用的就是 __CC_ARM
extern int $Super$$main(void);
/* re-define main function */
int $Sub$$main(void) // 这个就是RT-Thread实现的main函数的补丁函数,运行main函数之前会先运行这个函数
{
rtthread_startup();
return 0;
}
#elif defined(__ICCARM__) // 使用 __ICCARM 编译器,比如 IAR
extern int main(void);
/* __low_level_init will auto called by IAR cstartup */
extern void __iar_data_init3(void);
int __low_level_init(void)
{
// call IAR table copy function.
__iar_data_init3();
rtthread_startup();
return 0;
}
#elif defined(__GNUC__)
/* Add -eentry to arm-none-eabi-gcc argument */
int entry(void) // 使用 GCC 编译器
{
rtthread_startup();
return 0;
}
#endif
上面代码中,我们只看使用了 keil MDK 开发环境的那段代码。可以看出定义了一个 int $Sub$$main(void)
函数。
也就是说,在启动文件最后跳转到 __main 函数后,这个函数想要运行 main 函数之前,会先调用 int $Sub$$main(void)
函数,而这个函数则调用了 rtthread_startup()
函数,所以才会先运行 rtthread_startup()
这个函数的。过程就是:
然后在 rtthread_startup()
函数内部,会调用一个 应用程序初始化函数 rt_application_init()
,下面是这个函数的定义,以及这个函数有关的代码。
/* the system main thread */
void main_thread_entry(void *parameter)
{
extern int main(void);
extern int $Super$$main(void);
#ifdef RT_USING_COMPONENTS_INIT
/* RT-Thread components initialization */
rt_components_init();
#endif
/* invoke system main function */
#if defined(__CC_ARM) || defined(__CLANG_ARM)
$Super$$main(); /* for ARMCC. */ // 这里实际上就是调用了用户实现的main函数
#elif defined(__ICCARM__) || defined(__GNUC__)
main();
#endif
}
void rt_application_init(void)
{
rt_thread_t tid;
#ifdef RT_USING_HEAP
tid = rt_thread_create("main", main_thread_entry, RT_NULL,
RT_MAIN_THREAD_STACK_SIZE, RT_MAIN_THREAD_PRIORITY, 20);
RT_ASSERT(tid != RT_NULL);
#else
rt_err_t result;
tid = &main_thread;
result = rt_thread_init(tid, "main", main_thread_entry, RT_NULL,
main_stack, sizeof(main_stack), RT_MAIN_THREAD_PRIORITY, 20);
RT_ASSERT(result == RT_EOK);
/* if not define RT_USING_HEAP, using to eliminate the warning */
(void)result;
#endif
rt_thread_startup(tid);
}
可以看出 rt_application_init()
这个函数内部创建了一个 main 线程,而这个线程函数最终调用了 $Super$$main()
函数,而对于 ARMCC 编译器语法,实际上就是相当于跳转到了用户定义的 main 函数了。