共计 3380 个字符,预计需要花费 9 分钟才能阅读完成。
我第一次看RT-Thread的代码时,产生了一个直觉判断:这东西就是比FreeRTOS复杂一点,启动流程多了一层包装,本质还是main函数负责初始化,调度器最后启动。不过最后看完启动过程后,发现他用了两个好玩的扩展用法,也就是$Sub$$main和$Super$$main。这两个扩展用法仅限于MDK内部集成的ARMCC编译器。
这两个扩展用法做到了一个什么效果呢?

它让代码看起来有点莫名其妙,因为你看到的是多线程从main函数创建并进行调度,但是看不到RTT本身的调度器和组件是从哪里初始化的。
先来看看这个扩展用法是什么意思,有什么功能。
void ExtraFunc(void); /* 用户自己实现的外部函数*/
{
//这里执行一些你想要执行的代码。
}
void $Sub$$function(void)
{
ExtraFunc(); /* 可以做一些前置的设置工作 */
$Super$$function(); /* 跳回到原始的function函数 */
}
/* 在执行function函数时,执行会先执行function的扩展函数$Sub$$function,在扩展函数里面执行一些你想要的前置代码,当前置操作完成后,最后必须调用$Super$$function函数通过它回到我们原始的function函数 */
void function(void)
{
/* 原本的函数实体 */
}
通过上面的函数可以看出,这个扩展用法主要是用于在执行特定函数之前,做一些必要的前置工作。它可以神不知鬼不觉的执行一些让调用者无需关心的事情。
要注意的是$Sub$$main和$Super$$main必须成对出现,也就是:
void $Sub$$fun(void)
{
$Super$$fun();
}
如果像上面这么写,就等于脱裤子放屁——多此一举了。但是如果在回调Super之前增加一点东西,就等于偷偷的做了一些操作。
下面看一下RT-Thread的初始化是怎么运用这个扩展的。在RTT工程中,初始化代码集中在kernel/rt-thread/src/components.c文件里面。
int $Sub$$main(void)
{
rt_hw_interrupt_disable();
rtthread_startup();
return 0;
}
在component文件中可以找到Sub-main的定义,他首先执行一个关闭全局中断操作后,就开始了RTTHread的初始化函数。

如之前所说,我们的单片机在startup初始化汇编代码最后会调用__main标号,也就是我们的main函数。
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT SystemInit
IMPORT __main
CPSID I ; 关中断
LDR R0, =0xE000ED08
LDR R1, =__Vectors
STR R1, [R0]
LDR R2, [R1]
MSR MSP, R2
LDR R0, =SystemInit
BLX R0
CPSIE i ; 开中断
LDR R0, =__main
BX R0
ENDP
因此,当代码要执行__main时,就会先跳转到上面的C语言函数,这里就会增加RTT的启动函数。根据扩展用法,RTT的启动函数里面最终一定会调用$Super$$main();在回到main函数中去。
把rtthread_startup函数贴出来
int rtthread_startup(void)
{
/* 关闭中断 */
rt_hw_interrupt_disable(); (1)
/* 板级硬件初始化
* 注意: 在板级硬件初始化函数中把要堆初始化好(前提是使用动态内存)
*/
rt_hw_board_init(); (2)
/* 打印 RT-Thread 版本号 */
rt_show_version(); (3)
/* 定时器初始化 */
rt_system_timer_init(); (4)
/* 调度器初始化 */
rt_system_scheduler_init(); (5)
#ifdef RT_USING_SIGNALS
/* 信号量初始化 */
rt_system_signal_init(); (6)
#endif
/* 创建初始线程 */
rt_application_init(); (7)
/* 定时器线程初始化 */
rt_system_timer_thread_init(); (8)
/* 空闲线程初始化 */
rt_thread_idle_init(); (9)
/* 启动调度器 */
rt_system_scheduler_start(); (10)
/* 绝对不会回到这里 */
return 0; (11)
}
在上面函数中可以看到,先执行了rtapplicationinit()创建初始线程,然后会调用rtsystemscheduler_start()调度器。这里要记住这个顺序。
/* 使用动态内存时需要用到的宏:rt_config.h中定义 *///
#define RT_USING_USER_MAIN
#define RT_MAIN_THREAD_STACK_SIZE 256
#define RT_THREAD_PRIORITY_MAX 32
/* 使用静态内存时需要用到的宏和变量:在component.c定义 */ //
#ifdef RT_USING_USER_MAIN
#ifndef RT_MAIN_THREAD_STACK_SIZE
#define RT_MAIN_THREAD_STACK_SIZE 2048
#endif
#endif
#ifndef RT_USING_HEAP
ALIGN(8)
static rt_uint8_t main_stack[RT_MAIN_THREAD_STACK_SIZE];
struct rt_thread main_thread;
#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_THREAD_PRIORITY_MAX / 3, (初始线程优先级)
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_THREAD_PRIORITY_MAX / 3, (初始线程优先级)
20);
RT_ASSERT(result == RT_EOK);
(void)result;
#endif
/* 启动线程 */
rt_thread_startup(tid);//
}
上面的rtapplicationinit函数中创建了一个初始线程,名字叫main,函数名称为mainthreadentry。因此我们要继续看一下mainthreadentry中做了什么。
void main_thread_entry(void *parameter)//
{
extern int main(void);
extern int $Super$$main(void);
/* RT-Thread 组件初始化 */
rt_components_init();
/* 调用$Super$$main()函数,去到main */
$Super$$main();
}
在mainthreadentry中执行了组件初始化和Super-main的回调,也就是跑到了整个项目中的main函数中去了。
函数到此为止,其实调度器还没有开始执行。我们在原始的main函数中创建很多的新线程后,整个系统的调度才开始执行起来。

最后看一下原本的main函数
int main(void)
{
return 0;
}
到此为止,如果我们直接从main函数中开始创建线程任务做应用,其实完全不需要关心RTT的启动和组件的初始化,这种写法确实不错,至少不用修改MDK的启动文件了。
如果换成gcc的话,其实就是简单的改一下汇编代码就可以了,让汇编代码来控制先调用RTT的startup函数,然后startup函数中在去调用用户的main函数即可。
