今天5月1号,趁着四天假期,我选择好好提升自己,打算这四天学习一个物联网操作系统,物联网时代的到来,mcu上的物联网操作系统,就变得尤为重要,上周在ST峰会上看到了阿里的著作,被深深的吸引住了。立下flag,一:好好学习英语 二:学习两门的物联网联网技术:蓝牙mesh和lora组网 三:学习一门物联网操作系统 四:接触物联网云平台搭建 。本来是想学习阿里的AliOS Things操作系统的,但是刚出的系统,资料很是少, 从而学习了开源的RT—Thread。不说废话,我们直接来看看系统的运行过程。
RT—Thread提供了丰富的资料,我觉得官网推出的资料就是最好的,因为没有人比开发自己的开发人员更加熟悉自己的操作系统。官方文档地址:https://www.rt-thread.org/document/site/tutorial/
这个文档已经说了很明白了,我再这只是做一个补充说明,加深我的记忆。
刚入门首先来用mdk的模式器来仿真一下系统的工作流程吧,毕竟还没把代码移植到板子上。首先得获得官方的一个最简单的例程, 在上面的网址上有,需要的请自己去下载,我们来介绍一下系统的文件结构
各个目录所包含的文件类型的描述如下表所示:
目录名 | 描述 |
---|---|
applications | RT-Thread 应用程序。 |
rt-thread | RT-Thread 的源文件。 |
- components | RT-Thread 的各个组件目录。 |
- include | RT-Thread 内核的头文件。 |
- libcpu | 各类芯片的移植代码,此处包含了 STM32 的移植文件。 |
- src | RT-Thread 内核的源文件。 |
- tools | RT-Thread 命令构建工具的脚本文件。 |
drivers | RT-Thread 的驱动,不同平台的底层驱动具体实现。 |
Libraries | ST 的 STM32 固件库文件。 |
kernel-sample-0.1.0 | RT-Thread 的内核例程。 |
在目录下,有一个 project.uvprojx 文件,它是本文内容所引述的例程中的一个 MDK5 工程文件,双击 “project.uvprojx” 图标,打开此工程文件:
在工程主窗口的左侧 “Project” 栏里可以看到该工程的文件列表,这些文件被分别存放到如下几个组内,分别是:
目录组 | 描述 |
---|---|
Applications | 对应的目录为 rtthread_simulator_v0.1.0/applications,它用于存放用户应用代码。 |
Drivers | 对应的目录为 rtthread_simulator_v0.1.0/drivers,它用于存放 RT-Thread 底层的驱动代码。 |
STM32_HAL | 对应的目录为 rtthread_simulator_v0.1.0/Libraries/CMSIS/Device/ST/STM32F1xx,它用于存放 STM32 的固件库文件。 |
kernel-sample | 对应的目录为 rtthread_simulator_v0.1.0/kernel-sample-0.1.0,它用于存放 RT-Thread 的内核例程。 |
Kernel | 对应的目录为 rtthread_simulator_v0.1.0/src,它用于存放 RT-Thread 内核核心代码。 |
CORTEX-M3 | 对应的目录为 rtthread_simulator_v0.1.0/rt-thread/libcpu,它用于存放 ARM Cortex-M3 移植代码。 |
DeviceDrivers | 对应的目录为 rtthread_simulator_v0.1.0/rt-thread/components/drivers,它用于存放 RT-Thread 驱动框架源码。 |
finsh | 对应的目录为 rtthread_simulator_v0.1.0/rt-thread/components/finsh,它用于存放 RT-Thread 命令行 finsh 命令行组件。 |
现在我们点击一下窗口上方工具栏中的按钮,对该工程进行编译,如图所示:
编译的结果显示在窗口下方的 “Build” 栏中,没什么意外的话,最后一行会显示“0 Error(s), * Warning(s).”,即无任何错误和警告。
在编译完 RT-Thread/STM32 后,我们可以通过 MDK-ARM 的模拟器来仿真运行 RT-Thread。点击窗口右上方的按钮或直接按 “Ctrl+F5” 进入仿真界面,再按 F5 开始运行,然后点击该图工具栏中的按钮或者选择菜单栏中的 “View→Serial Windows→UART#1”,打开串口 1 窗口,可以看到串口的输出只显示了 RT-Thread 的 LOGO,这是因为用户代码是空的,其模拟运行的结果如图所示:
我们可以通过输入Tab键或者 help + 回车
输出当前系统所支持的所有命令,如下图所示。
一般了解一份代码大多从启动部分开始,同样这里也采用这种方式,先寻找启动的源头。以 MDK-ARM 为例,MDK-ARM 的用户程序入口为 main() 函数,位于 main.c 文件中。系统启动后先从汇编代码 startup_stm32f103xe.s 开始运行,然后跳转到 C 代码,进行 RT-Thread 系统功能初始化,最后进入用户程序入口 main()。
下面我们来看看在 components.c 中定义的这段代码:
//components.c 中定义
/* re-define main function */
int $Sub$$main(void)
{
rt_hw_interrupt_disable();
rtthread_startup();
return 0;
}
在这里 $Sub$$main
函数仅仅调用了 rtthread_startup()
函数。RT-Thread 支持多种平台和多种编译器,而 rtthread_startup()
函数是 RT-Thread 规定的统一入口点,所以 $Sub$$main
函数只需调用 rtthread_startup()
函数即可。例如采用 GNU
GCC
编译器编译的 RT-Thread
,就是直接从汇编启动代码部分跳转到 rtthread_startup()
函数中,并开始第一个 C 代码的执行的。在 components.c
的代码中找到 rtthread_startup()
函数,我们将可以看到 RT-Thread 的启动流程:
int rtthread_startup(void)
{
rt_hw_interrupt_disable();
/* board level initalization
* 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();
/* start scheduler */
rt_system_scheduler_start();
/* never reach here */
return 0;
}
这部分启动代码,大致可以分为四个部分:
1. 初始化与系统相关的硬件;
2. 初始化系统内核对象,例如定时器,调度器;
3. 初始化系统设备,这个主要是为 RT-Thread 的设备框架做的初始化;
4. 初始化各个应用线程,并启动调度器。
在这里我要做几点说明, 因为官方文档在这里讲解的比较少,比较难理解:
1. 首先来说说$Sub$$main和$Super$$main 函数的用法
这是一种特殊模式:用于有一个已经存在且不能被改变的函数的情况(比如不能更改的库函数);使用这两个模式可以帮原函数打补丁,如存在一个函数foo();
$Sub$ $foo :定义的新功能函数,在foo()函数之前/后使用$Sub$ $foo 可以添加一些新的程序代码。
$Super$ $foo :就是原始的未修补的foo函数,使用这个$Super$ $foo函数将直接跳转到foo()函数。
下面以stm32f10xxx的int main(void)函数为例:
(1) 上电后,运行启动代码startup_stm32f10xxx.s
(2) 从系统初始化(SystemInit)开始执行,将函数地址赋给R0寄存器,跳转到R0地址执行并返回此处。
(3) 将__main函数地址给R0,将函数地址赋给R0,跳转到R0地址执行,不返回。
(4) 跳转到$Sub$$main(自己定义该函数)。
#if defined (__CC_ARM)
extern int $Super$$main(void);
/* re-define main function */
int $Sub$$main(void)
{
preWork() ; // do somthing before call main
$Super$$main(); // 跳转到 main()
return 0;
}
#endif
讲到这里我们来看看代码中关于这个得用法:
首先在components.c文件中
这里表示,这个函数在main函数之前运行,那么这个函数首先关闭了中断,然后再是rttread系统的启动函数。在该文件下还有
$Super$$main()函数表示将跳转到main函数运行。系统之所以这样的做法是,把用户的main函数和系统的main函数分开,系统的main函数比用户的main函数更前执行,这样就可以避免接触底层,注重应用层开发。
2.接着我们详细来介绍一下系统的运行过程:
(1)我们就不分析汇编代码了,直接从c文件入手,首先进入的是components.c文件中的int $Sub$$main(void)函数,该函数首先运行rt_hw_interrupt_disable()函数, 该函数失能中断
(2)接着运行rtthread_startup()函数,该函数对系统做了初始化,首先rt_hw_interrupt_disable()对失能了硬件中断,为啥要失能两次?? 有待思考, 接着rt_hw_board_init() 板级的初始化,rt_show_version()函数打印了系统的版本号 rt_system_timer_init()函数初始化了系统的定时器。rt_system_scheduler_init()初始化系统调度器。rt_system_signal_init()初始化信号量 ,rt_application_init() 函数是应用初始化,我们具体来看看这个函数
(3)rt_application_init(): 首先定义了一个rt的事件结构体变量,然后调用rt_thread_create创建了一个指向main_thread_entry函数的事件,最后启动了这个事件
(4)我们接着看看main_thread_entry函数,这个函数首先初始化了components, 然后调用了main函数,这里应该不能马上调用,因为系统还没有跑起来,只有等系统调用了,才能运行,分析到这我们就不分析了,接着就分析rt_thread_idle_init函数了, 这是初始化idle,至于这个是啥 我暂时也不知道, 然后再rt_system_scheduler_start 系统调度开始了,上面我们创建了一个main函数的事件, 这就可以运行了
上面的启动代码基本上可以说都是和 RT-Thread 系统相关的,那么用户如何加入自己的应用程序的初始化代码呢?RT-Thread 将 main 函数作为了用户代码入口,只需要在 main 函数里添加自己的代码即可。
int main(void)
{
/* user app entry */
return 0;
}
提示
注:为了在进入 main 程序之前,完成系统功能初始化,可以使用 $sub$$
和 $super$$
函数标识符在进入主程序之前调用另外一个例程,这样可以让用户不用去管 main() 之前的系统初始化操作。详见 ARM® Compiler v5.06 for µVision® armlink User Guide。
我们到此第一节就介绍完了,我们接着学习第二节