一般了解一份代码大多从启动部分开始,同样这里也采用这种方式,先寻找启动的源头。
RT-Thread支持多种平台和多种编译器,而rtthread_startup()函数是RT-Thread规定的统一启动入口。
一般执行顺序是:系统先从启动文件开始运行,然后进入RT-Thread的启动函数rtthread_startup(),最后进入用户入口函数main(),如下图所示:
以MDK-ARM为例,用户程序入口为main函数,位于main.c文件中。系统启动后先从汇编代码startup_stm32f103xe.s开始运行,然后跳转到C代码,进行RT-Thread系统启动,最后进入用户程序入口函数main()。
为了在进入main()之前完成RT-Thread系统功能初始化,我们使用了MDK的扩展功能。
可以给main添加 S u b Sub Sub 的前缀符号作为一个新功能函数 的前缀符号作为一个新功能函数 的前缀符号作为一个新功能函数SubKaTeX parse error: Can't use function '$' in math mode at position 8: main,这个$̲Submain,这个 S u b Sub Sub m a i n 可以先调用一些补充在 m a i n 之前的功能函数,再调用 main可以先调用一些补充在main之前的功能函数,再调用 main可以先调用一些补充在main之前的功能函数,再调用Super$$main转到main()函数执行,这样可以让用户不去管main()之前的系统初始化操作。
在components.c中定义的这段代码:
int $Sub$$main(void)
{
rtthread_startup();
return 0;
}
int rtthread_startup(void)
{
rt_hw_interrupt_disable();
/* 板级初始化:需要在该函数内部进行系统堆的初始化 */
rt_hw_board_init();
/* 打印RT-Thread版本信息 */
rt_show_version();
/* 定时器初始化 */
rt_system_timer_init();
/* 调度器初始化 */
rt_system_scheduler_init();
#ifdef RT_USING_SIGNALS
/* 信号初始化 */
rt_system_signal_init();
#endif
/* 由此创建一个用户 main 线程 */
rt_application_init();
/* 定时器线程初始化 */
rt_system_timer_thread_init();
/* 空闲线程初始化 */
rt_thread_idle_init();
/* 启动调度器 */
rt_system_scheduler_start();
/* 不会执行至此 */
return 0;
}
这部分启动代码,大致可以分为四个部分:
启动调度器之前,系统所创建的线程并不会立马运行,它们会处于就绪状态等待系统调度;待启动调度器之后,系统才转入第一个线程开始运行,根据调度规则,选择的是就绪队列中优先级最高的线程。
rt_hw_board_init()中完成系统时钟设置,为系统提供心跳、串口初始化,将系统输入输出端绑定到这个串口,后续系统运行信息就会从串口打印出来。
main()函数是RT-Thread的用户代码入口,用户可以在main()函数里添加自己的应用。
一般MCU包含的存储空间有:片内Flash与片内RAM。
RAM相当于内存,Flash相当于硬盘。
编译器会将一个程序分类为好几个部分,分别存储在MCU不同的存储区。
Keil工程在编译完之后,会有相应的程序所占用的空间提示信息。
linking...
Program Size: Code=48008 RO-data=5660 RW-data=604 ZI-data=2124
After Build - User command \#1: fromelf --bin.\\build\\rtthread-stm32.axf--output rtthread.bin
".\\build\\rtthread-stm32.axf" - 0 Error(s), 0 Warning(s).
Build Time Elapsed: 00:00:07
上面提到的 Program Size 包含以下几个部分:
1)Code:代码段,存放程序的代码部分;
2)RO-data:只读数据段,存放程序中定义的常量;
3)RW-data:读写数据段,存放初始化为非 0 值的全局变量;
4)ZI-data:0 数据段,存放未初始化的全局变量及初始化为 0 的变量;
编译完工程会生成一个.map的文件,该文件说明了各个函数占用的尺寸和地址,在文件最后几行也说明了上面几个字段的关系:
Total RO Size (Code + RO Data) 53668 ( 52.41kB)
Total RW Size (RW Data + ZI Data) 2728 ( 2.66kB)
Total ROM Size (Code + RO Data + RW Data) 53780 ( 52.52kB)
程序运行之前,需要有文件实体被烧录到STM32的Flash中,一般是bin或者hex文件,该被烧录文件称为可执行映像文件。
RO段中保存了Code、RO-data的数据,RW段保存了RW-data的数据,由于ZI-data都是0,所以未包含在映像文件中。
STM32在上电启动之后默认从Flash启动,启动之后会将RW段中的RW-data搬运到RAM中,但不会搬运RO段,即CPU的执行代码从Flash中读取,另外根据编译器给出的ZI地址和大小分配出ZI段,并将这块RAM区域清零。
其中动态内存堆为未使用的RAM空间,应用程序申请和释放的内存块都来自该空间。
如下面的例子:
rt_uint8_t *msg_ptr;
msg_ptr = (rt_uint8_t *)rt_malloc(128);
rt_memset(msg_ptr, 0, 128);
代码中的msg_ptr指针指向128字节内存空间位于动态内存堆空间中。
而一些全局变量则是存放于 RW 段和 ZI 段中,RW 段存放的是具有初始值的全局变量(而常量形式的全局变量则放置在 RO 段中,是只读属性的),ZI 段存放的系统未初始化的全局变量。
自动初始化机制是指初始化函数不需要被显示调用,只需要在函数定义处通过宏定义的方式进行申明,就会在系统启动过程中被执行。
例如在串口驱动中调用一个宏定义告知系统初始化需要调用的函数,代码如下:
int rt_hw_usart_init(void) /* 串口初始化函数 */
{
rt_hw_serial_register(&serial1, "uart1",RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_INT_RX,uart);
return 0;
}
INIT_BOARD_EXPORT(rt_hw_usart_init); /* 使用组件自动初始化机制 */
示例代码最后的INIT_BOARD_EXPORT(rt_hw_usart_init)表示使用自动初始化功能,按照这种方式,rt_hw_usart_init()函数就会被系统自动调用,那么它是在哪里被调用的呢?
在系统启动流程图中,有两个函数:rt_components_board_init() 与 rt_components_init(),其后的带底色方框内部的函数表示被自动初始化的函数,其中:
“board init functions” 为所有通过 INIT_BOARD_EXPORT(fn) 申明的初始化函数。
“pre-initialization functions” 为所有通过 INIT_PREV_EXPORT(fn)申明的初始化函数。
“device init functions” 为所有通过 INIT_DEVICE_EXPORT(fn) 申明的初始化函数。
“components init functions” 为所有通过 INIT_COMPONENT_EXPORT(fn)申明的初始化函数。
“enviroment init functions” 为所有通过 INIT_ENV_EXPORT(fn) 申明的初始化函数。
“application init functions” 为所有通过 INIT_APP_EXPORT(fn)申明的初始化函数。
rt_conponents_board_init()函数执行的比较早,主要初始化相关硬件环境,执行这个函数时将会遍历通过INIT_BOARD_EXPORT(fn)申明的初始化函数表,并调用各个函数。
rt_conponents_init()函数会在操作系统运行起来之后创建的main线程里被调用执行,这个时候硬件环境和操作系统已经初始化完成,可以执行应用相关代码。
rt_components_init() 函数会遍历通过剩下的其他几个宏申明的初始化函数表。
RT-Thread的自动初始化机制使用了自定义RTI符号段,将需要在启动时进行初始化的函数指针放到了该段中,形成一张初始化函数表,在系统启动过程中会遍历该表,并调用表中的函数,达到自动初始化的目的。
初始化函数主动通过这些宏接口进行申明,如INIT_BOARD_EXPORT(rt_hw_usart_init),,链接器会自动收集所有被申明的初始化函数,放到 RTI 符号段中,该符号段位于内存分布的 RO 段中,该 RTI 符号段中的所有函数在系统初始化时会被自动调用。
RT-Thread内核采用面向对象的设计思想进行设计,系统级的基础设施都是一种内核对象,例如线程,信号量,互斥量,定时器等。
内核对象分为两类:静态内核对象和动态内核对象。
静态内核对象通常放在RW段和ZI段,在系统启动后在程序中初始化;
动态内核对象则是从内存堆中创建的,而后手工做初始化。
thread1 是一个静态线程对象,而 thread2 是一个动态线程对象。
thread1对象的内存空间,包括线程控制块thread1与栈空间thread1_stack都是编译时决定的,因为代码中不存在初始值,都统一放在未初始化数据段中。
静态对象会占用RAM空间,不依赖于内存堆管理器,内存分配时间确定。
动态对象则依赖于内存堆管理器,运行时申请RAM空间,当对象被删除后,占用的RAM空间被释放。