一、STM32 单片机的启动流程
单片机上电后,会首先执行定义在startup 文件 中的Reset_Handler 函数,Reset_Handler 函数会首先执行SystemInit 函数,执行完之后,再执行我们常见的main 函数。
二、RT-Thread 启动函数是怎么被调用的?
2.1 启动流程概述
RT-Thread 系统的统一入口函数为 rtthread_startup()
,通过利用各个编译器的特性,实现隐式调用 rtthread_startup() 函数完成系统的初始化,启动流程如下:
这样做的一个好处是:用户可以不用自己去初始化RT-Thread,按照以前编写单片机程序的经验,直接在main函数编写自己的任务程序即可。
2.2 keil mdk 下实现在main函数之前执行rtthread_startup 函数
扩展main函数
keil mdk 编译器用 $Sub$$
和 $Super$$
这两个符号来扩展main 函数。可以实现在
- 在调用main函数时,自动调用
$$Sub$$main
这个新定义的函数代替main函数。 - 通过调用
$Super$$main
函数来实现对main 的调用。
如下是官方文档(https://developer.arm.com/documentation/dui0474/m/accessing-and-managing-symbols-with-armlink/use-of–super—and–sub—to-patch-symbol-definitions)的一个例程
void ExtraFunc(void); extern void $Super$$foo(void):
/* this function is called instead of the original foo() */
void $Sub$$foo(void)
{
ExtraFunc(); /* does some extra setup work */
$Super$$foo(); /* calls the original foo() function */
/* To avoid calling the original foo() function
* omit the $Super$$foo(); function call.
*/
}
这里新定义了一个 函数$Sub$$foo
,在程序中调用foo
函数的地方,编译器调用$Sub$$foo
函数代替foo
函数,如果还想调用foo
函数,则通过调用$Super$$foo()
来实现。
实现分析
首先定义$Sub$$main
,在启动文件中需要调用main
函数的时候,调用$Sub$$main
函数。后面再统一介绍 $Super$$main();
是怎么被调用的。
int $Sub$$main( void )
{
rtthread_startup();
return 0;
}
2.3 gcc 下实现在main函数之前执行rtthread_startup 函数
gcc 开发环境下,是通过修改启动文件实现的。在启动文件调用main 函数的地方,改成调用一个新的函数entry()
,然后在entry
函数中,调用rtthread_startup 函数。
启动文件修改
//修改前:
bl SystemInit
bl main
//修改后:
bl SystemInit
bl entry /* 修改此处,由 main 改为 entry */
entry 函数的定义
int entry(void)
{
rtthread_startup();
return 0;
}
三、rtthread_startup 的实现分析
主要介绍如下三个函数:
3.1 rt_application_init
这个函数创建main 线程
tid = rt_thread_create( "main", main_thread_entry, RT_NULL,
RT_MAIN_THREAD_STACK_SIZE, RT_MAIN_THREAD_PRIORITY, 20 );
main 线程的入口函数为main_thread_entry 函数,该函数定义如下:
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. */
#elif defined(__ICCARM__) || defined(__GNUC__)
main();
#endif
}
该函数主要是调用rt 组件的初始化 和我们编写的main函数。
3.2 rt_thread_idle_init
这个函数负责创建idle 线程,以后设计idle 线程的信息,可以通过这个入口查询。
3.3 rt_system_scheduler_start
在rtthread_startup 函数的最后,使能系统调度,程序将不在从rtthread_startup 函数 后面的语句。
四、修改RT-THREAD 的启动流程
4.1 为什么要修改这个流程?
4.1.1 当程序中要实现OAT 功能时
实现ota 功能时,我们的程序需要重定位中断向量表,按照RT_Thread 的启动流程,我们只能在SystemInit 的末尾 或者在执行rtthread_startup 函数前重设中断向量表,因为rtthread_startup 函数中涉及到中断的处理和中断的调用,在它之后已经不合适。
我既不想修改SystemInit 函数,也不想每个工程都去修改一次RT-Thread 的源码去插入一句格格不入的中断向量重定向,我希望在main函数中调用中断向量重定向。
4.1.2 当工程使用gcc 开发时
在使用gcc 开发时,需要修改启动文件。当切换不同容量的芯片时,需要更换启动文件,这时候容易忘记更改启动文件。
另外,我也不想修改启动文件。
4.1.3 当工程有些信号需要上电即刻处理时
按照默认流程,上电先执行rtthread_startup 函数,在任务调度至main线程时,才执行我们的代码。当有些信号需要上电即刻处理时,我们只能去修改RT-Thread 源码了,在RT-Thread 源码中插入一堆应用相关的代码,不合适。
4.2 尝试修改?
直接注释components.c 中 rtthread_startup() 函数的调用处理,然后将rtthread_startup 的调用放到main 中执行,这样行不行?
不行!!!
通过上面的分析可知rtthread_startup 创建了main 线程,main 线程的入口函数调用了main函数,如果我们直接把rtthread_startup 放到main 中执行,相当于形成了一种递归调用,直接死机。
4.3 目前可行的一种修改方式
- 在main 中调用rtthread_startup 初始化系统;
- 重新定义 main 线程的入口函数执行的函数
int main( void )
{
rtthread_startup();
return 0;
}
简化main 线程的入口函数,在调用main 的地方,调用_main_thread_entry();
void main_thread_entry( void *parameter )
{
#ifdef RT_USING_COMPONENTS_INIT
/* RT-Thread components initialization */
rt_components_init();
#endif
_main_thread_entry();
}
新定义 _main_thread_entry 函数,调用我们的应用程序
int _main_thread_entry( void )
{
app_log( "_main_thread_entry\n" );
//...
}