硬件:正点原子精英版STM32F103ZET6
软件:RT-Thread-v4.0.1
一、搭建环境
(1)进入到rt-thread\bsp\stm32f10x-HAL目录下,在此处打开ComEmu命令行界面(不会的同学请参考官网提供的示例:Env的使用方法),并输入menuconfig命令,然后有如下界面:
配置选项如图所示,这里主要介绍一下RT-Thread Components选项的配置:进入到RT-Thread Components选项,选择了The main() function as user entry function;这里的意思是将main()作为用户的入口函数,稍后会讲到这里的具体作用;
(2)保存配置,退出;
之后编译有多种方法可选:
①使用命令行scons命令编译,其中编译器是ARM-GCC,编译结果是bin文件;
②使用scons –target=mdk5命令,构建keil5的开发环境,然后打开project.uvprojx工程,这里告诉大家一个小技巧,因为每次修改配置后,这个project.uvprojx工程都会根据template.uvprojx工程重新生成一遍,然后在keil界面里设置有关硬件参数,再编译烧写,所以,可以根据自己的板子,把template.uvprojx利用文本工具打开,修改里面的硬件参数,例如:我的板子的主芯片是STM32F103ZET6,而template.uvprojx中设置的是
直接把STM32F103RC用STM32F103ZE替换掉,另外包括时钟,RAM ROM设置这里也可以改:
根据自己需要做修改,就省得每次都要进keil重新配置了;keil4工程类似如此;
③IAR编译环境,没有尝试过编译STM32,就不多说了;
(3)构建环境完成,然后留给我们的main()函数里面啥都没有,但是恭喜你,你可以烧写到你的板子上验证一下,RT-Thread是可以跑起来的,把串口打开可以看到一堆打印在刷屏,就这么简单?中间到底发生了什么?
二、启动分析
接下来,根据之前的配置简要分析RT-Thread的启动过程了;
在startup_stm32f103xe.s有下面一段汇编:
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT __main
IMPORT SystemInit
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
由此处进入到C语言的main函数;但这里进入到的并不是那个什么都没的main()函数中,而是在rt-thread/src/components.c中,有这么一段代码:
#ifdef RT_USING_USER_MAIN
void rt_application_init(void);
void rt_hw_board_init(void);
int rtthread_startup(void);
#if defined(__CC_ARM) || defined(__CLANG_ARM) //这个宏表示采用ARM-GCC编译器编译的
extern int $Super$$main(void);
/* re-define main function */
int $Sub$$main(void)
{
rt_hw_interrupt_disable();
rtthread_startup();
return 0;
}
#elif defined(__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();
rt_hw_interrupt_disable();
rtthread_startup();
return 0;
}
#elif defined(__GNUC__) //这个宏表示采用arm-none-eabi-gcc编译器编译的
extern int main(void);
/* Add -eentry to arm-none-eabi-gcc argument */
int entry(void)
{
rt_hw_interrupt_disable();
rtthread_startup();
return 0;
}
#endif
这里是预编译宏,根据不同编译器选择对应启动入参,这里不作详细介绍,有兴趣可以了解一下;所以我们用得到的代码就是:
extern int $Super$$main(void);
/* re-define main function */
int $Sub$$main(void)
{
rt_hw_interrupt_disable();
rtthread_startup();
return 0;
}
关键在于$Sub$$main和$Super$$main;查相关资料这里简单解释一下:
这是一种特殊模式:用于有一个已经存在且不能被改变的函数的情况,使用这两个模式可以帮原函数打补丁,如存在一个函数foo():
$Sub$$foo:定义的新功能函数,原先的foo()的入口变为$Sub$$foo(),在$Sub$$foo里面可以对foo()函数功能重写;
$Super$$foo:指的是原始的foo()函数,使用$Super$$foo时,用户调用的实际上就是foo();
因此,那段汇编执行完成后调到的main()是这个$Sub$$main;然后往下执行,注意函数rtthread_startup();里面才是主要的:
int rtthread_startup(void)
{
rt_hw_interrupt_disable(); //关中断
/* board level initialization
* NOTE: please initialize heap inside board initialization.
*/
rt_hw_board_init(); //板级初始化
/* show RT-Thread version */
rt_show_version(); //显示内核版本信息
/* timer system initialization */
rt_system_timer_init(); //系统时钟初始化
/* scheduler system initialization */
rt_system_scheduler_init(); //任务调度器初始化
#ifdef RT_USING_SIGNALS //如果定义了信号
/* signal system initialization */
rt_system_signal_init(); //信号初始化
#endif
/* create init_thread */
rt_application_init(); //创建应用初始化
/* timer thread initialization */
rt_system_timer_thread_init(); //系统定时器线程初始化
/* idle thread initialization */
rt_thread_idle_init(); //空闲线程初始化
#ifdef RT_USING_SMP //如果定义了多核处理器,这里肯定不支持
rt_hw_spin_lock(&_cpus_lock);
#endif /*RT_USING_SMP*/
/* start scheduler */
rt_system_scheduler_start(); //启动任务调度器,永不返回了
/* never reach here */
return 0;
}
都是一堆初始化的操作,在这里我们最起码能知道:启动任务调度器的时候,已经有两个线程可以跑起来了:(1)系统软定时器线程;(2)空闲线程;分别进入到其中看下就可以明白之前不断打印刷屏的是啥玩意了,所以即使外面的main()我们啥都没写,系统起来还是没问题的;
再关注一下rt_application_init()函数,同样是在这个文件里的,看下它的实现:
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);
}
这里很简单,就是创建一个叫”main”的线程,然后启动它,线程入口是main_thread_entry,继续看:
void main_thread_entry(void *parameter)
{
extern int main(void);
extern int $Super$$main(void);
/* RT-Thread components initialization */
rt_components_init();
#ifdef RT_USING_SMP
rt_hw_secondary_cpu_up();
#endif
/* invoke system main function */
#if defined(__CC_ARM) || defined(__CLANG_ARM)
$Super$$main(); /* for ARMCC. */
#elif defined(__ICCARM__) || defined(__GNUC__)
main();
#endif
}
关键的东东来了,首先声明了main()和$Super$$main();再有一个初始化,然后去调用$Super$$main();,按照之前的解释(忘记了回过头再看下),这里的调用就是那个什么都没有的main()函数了,然后吭哧吭哧的就可以执行用户的业务,所以我们自己的应用程序就是完善那个什么都没有的main函数,在main里面创建一个线程跑下,看是不是这样,这里提供一个点灯的demo程序:https://download.csdn.net/download/kuangzuxiaon/11390602
三、总结
RT-Thread给我们的嵌入式环境搭建提供了极大便利,官方能想得到的几乎都给你干了,关键在于他们提供的Env工具,按照其它嵌入式系统的做法,没有有Env工具,必须从源码着手去撘环境,移植RTT的内核,这种做法我尝试过,总会有一些莫名的报错,估计还是系统架构不太熟悉,不过最终也是可以跑起来的,之前也出过一个例程可以参考:https://download.csdn.net/download/kuangzuxiaon/11008669,当然这种做法不会受限于Env工具,对于工程文件组织可以随心所欲,而不用去修改配置脚本,还是不能忘本;系统环境和启动流程搞清楚了,然后就是对接业务需求,驱动和应用开发;