我第一次看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 PROCEXPORT Reset_Handler [WEAK]IMPORT SystemInitIMPORT __mainCPSID I ; 关中断LDR R0, =0xE000ED08LDR R1, =__VectorsSTR R1, [R0]LDR R2, [R1]MSR MSP, R2LDR R0, =SystemInitBLX R0CPSIE i ; 开中断LDR R0, =__mainBX R0ENDP
因此,当代码要执行__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_HEAPALIGN(8)static rt_uint8_t main_stack[RT_MAIN_THREAD_STACK_SIZE];struct rt_thread main_thread;#endifvoid 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函数即可。
438