陈瑞雪,王宜怀,王庭琛
(苏州大学 计算机与科学技术学院,江苏 苏州 215006)
RT-Thread(Real-Time Thread)是一款嵌入式实时操作系统(Real-Time Operating System,RTOS),具有组件完整丰富、简易开发、超低功耗等特点,2006年由中国开源社区主导开发,因其具有浅显易懂且方便移植的特点而被广泛应用在农业、车载、医疗等领域。但基于RTOS进行RT-Thread嵌入式开发需要对其启动过程有深刻的认识,涉及到时间嘀嗒处理、堆空间初始化以及线程创建、调度机制等方面的设置。截至目前,有关操作系统的启动研究集中在mbedOS操作系统、Windows操作系统以及MINIX3操作系统的启动过程等方面,缺乏对RT-Thread的启动剖析研究。因此,本文将利用Cortex-M4F内核的STM32微控制器,基于STM32CubeIDE 1.0.2开发环境和SD-RT-Thread工程框架分析RT-Thread的启动流程,对从芯片上电启动运行到entry函数,最终转入RT-Thread启动的全过程进行剖析,并与一些关键的代码及注释、流程图、PendSV中断服务程序相结合分析其实现原理。充分理解RTThread的启动流程,不仅可以帮助读者从微观层面来理解RTOS的启动过程,也有助于开发人员设计出相应速度快、稳定性强的嵌入式系统。
SD-RT-Thread工程框架的启动过程由芯片上电启动和RT-Thread操作系统启动两部分组成,如图1所示。进入应用程序的入口点函数entry时,调用启动函数rtthread_startup(),系统控制权将转移给RT-Thread,由它完成一系列前期准备工作后实现线程调度。
图1 RT⁃Thread工程框架启动流程
芯片上电启动始于调用启动文件startup_stm32l431rctx.s,这个文件包含了中断向量表及系统启动代码。在中断向量表中,每个中断向量号对应一个中断源,中断向量号与中断源是一一对应的关系,按照中断向量号的固定地址存放中断服务程序入口地址(每个占用4 B)。本文采用的MCU其中段向量表的位置在存储区0x0800 0000~0x0800 0190的这段地址范围。
最后,跳转entry函数,调用rtthread_startup()函数启动操作系统,并启动初始线程来调用main函数。在main函数的内部调用自启动任务函数app_init来初始化外设模块、初始化有关变量、使能中断模块等,创建并启动其他用户线程。
芯片上电后开始启动,执行到entry函数时,实际调用总启动函数rtthread_startup开始RT-Thread操作系统的启动,其具体调用关系和执行流程如图2所示,主要完成内核资源初始化、主线程和空闲线程的初始化以及调度器的启动等工作。
图2 RT⁃Thread启动过程总流程
2.1.1 板级硬件资源初始化
板级硬件初始化主要完成系统时钟SysTick初始化和堆初始化。
1)SysTick初始化
SysTick是RT-Thread整个系统的时钟基准,系统通过每次“嘀嗒”进入中断服务程序对任务状态进行管理。在进行板级硬件初始化时,首先调用_SysTick_Config()函数初始化系统时钟基准SysTick,该函数将SystemCoreClock/RT_TICK_PER_SECOND作为参数传入。SystemCoreClock系统时钟频率为48 MHz,RT_TICK_PER_SECOND是宏定义设置的嘀嗒频率,默认为1 000 Hz,因此系统SysTick调度的频率被设置为1 1 000 Hz=1 ms一次。
//系统时间嘀嗒初始化
_SysTick_Config SystemCoreClock/RT_TICK_PER_SECOND;
2)堆初始化
堆是用于存放临时变量的区域,一般由程序员动态分配,最终也由程序员释放。在内存中,堆区的位置介于静态区和栈区之间,使用时按照RAM区地址由低到高的顺序申请。与其他操作系统不同之处在于,RTThread中系统使用的堆空间是自定义的一个静态数组rt_heap,在内存中属于bss区,大小由RT_HEAP_SIZE决定,目前将其宏定义为1 024,也可以根据工程需求修改(不能超过内部静态RAM区大小)。RT-Thread启动时会 通 过rt_heap_begin_get()、rt_heap_end_get()获 取rt_heap的起始地址及结束地址,并将这两个地址作为参数传入rt_system_heap_init()函数对堆内存进行初始化。
2.1.2 定时器初始化
其他操作系统如mbed OS中,当线程需要延时时,系统会获取当前正在运行的线程,阻塞该线程并将阻塞的线程根据延时时长插入到等待队列或延时队列中,最后获取当前优先级最高的就绪态线程并切换就绪态线程为运行态。RT-Thread内部没有设置等待队列或延时队列,而是通过定义一个双向链表类型的全局系统定时器来类比延时队列。在每个线程中内置一个定时器,在线程需要延时时,系统先挂起该线程并启动内置的定时器,将定时器按照线程延时时间长短插入到升序排列的rt_timer_list链表中。
在初始化系统定时器列表时,rt_timer_list里面的成员是一个双向链表的根节点,每个节点均有指向下一个节点的指针next和指向前一个节点的指针prev,初始化时将这两个指针都指向该节点。初始化好的系统定时器列表如图3所示。
图3 初始化好的系统定时器列表
2.1.3 调度器初始化
RT-Thread中的调度器用于切换线程,即找到就绪列表中最高优先级的线程并执行。调度器初始化主要完成初始化整个线程就绪列表(rt_thread_priority_table)为空、初始化当前线程的优先级(rt_current_priority)为空闲线程的优先级(31)、初始化当前线程控制块指针(rt_current_thread)为空和初始化线程就绪优先级组(rt_thread_ready_priority_group)为0等内容。
rt_thread_priority_table是一个一维数组,大小默认为32。
rt_list_t rt_thread_priority_table[32];
在该数组中,每个下标表示与之相同的优先级,比如位0表示优先级0。当线程就绪时,根据优先级插入到相同下标的链表中。有三个线程就绪时的链表挂载情况如图4所示。
图4 就绪列表的挂载情况
初始时,并未有任何就绪线程,故将就绪列表初始化为空。空的线程就绪列表如图5所示,每个索引初始化成对应优先级的双链表根节点。
图5 空的线程就绪列表
可以将线程就绪优先级组rt_thread_priority_group看作是一个32位的整型数,与rt_thread_priority_table相似,每一个位对应一个优先级,如图6所示。若某线程准备好,其优先级为P(0≤P≤31),则 将rt_thread_priority_group的位置为1,再根据这一位在线程优先级表的对应位置插入线程,如此便可以快速地找到线程在线程优先级表中插入和移除的位置。
图6 线程就绪优先级组的位号与线程优先级对应的关系
定时器和调度器初始化完成之后,系统创建主线程和空闲线程,然后将它们加入就绪列表,便于调度器启动后可以立即运行主线程来执行用户程序,并确保CPU在无任务时通过空闲线程保持运转。
2.2.1 创建主线程
RT-Thread启动时,需要先创建一个主线程为其分配运行所需的资源,设置入口函数、线程栈、优先级和时间片等。在调度器启动后,在入口函数中调用main()函数创建用户线程,流程如图7所示。当所有用户线程都成功创建后,终止主线程。
图7 主线程和用户线程创建关系
主线程的创建主要由函数rt_application_init()完成,整个过程由创建线程和启动线程两部分组成。函数调用关系如图8所示。
图8 创建主线程的函数调用关系
1)创建主线程
受嵌入式环境下的低资源条件限制,主线程在执行完必要的任务后可以回收其资源,因此系统调用rt_thread_create()函数来创建主线程。该函数创建线程采用动态分配内存的方式,即该线程控制块(TCB)和线程栈的内存都从堆内存中申请分配,若堆内存不足,内存分配失败,则整个线程创建失败。创建成功后若线程不再运行也可主动删除线程来释放对应的内存资源。
创建主线程的过程由内核对象分配rt_object_allocate()、内核内存分配RT_KERNEL_MALLOC()、线程实际初始化_rt_thread_init等函数构成。rt_object_allocate()从堆空间中动态申请分配TCB并进行初始化;RT_KERNEL_MALLOC()从堆空间中动态申请空间用于线程栈;_rt_thread_init()中初始化TCB各项属性,包括线程的入口函数和参数、线程栈的地址、大小和sp指针以及线程内置定时器等。
2)启动主线程
线程创建并初始化结束后,此时启动主线程并不意味着真正启动运行主线程,而是为了调度器的调度运行进行相应的初始化,完成的主要工作有:设置线程当前优先级及掩码值;将线程插入就绪列表;判断是否进行一次调度。由于此时还处于RT-Thread启动过程中,调度器还未启动,所以当前线程不进行调度。
2.2.2 创建空闲线程
空闲线程的创建由函数rt_thread_idle_init()完成,与创建主线程相似的是都先创建并初始化线程,然后调用相同的rt_thread_startup()函数启动线程,不同之处在于创建线程的方式。创建空闲线程采用的是创建静态线程对象的方式,实际调用的函数是rt_thread_init()。该方式适用于创建长期的线程对象,其TCB和线程栈的空间在编译之前就已经确定,申请后不可释放。这是由于空闲线程的主要任务是在内核无用户线程时被内核执行,使CPU保持运行状态,同时对终止的无效线程进行资源回收的工作,它始终存在于系统内。
主线程和空闲线程创建完成后,启动调度器切换线程,主要实现的功能有:首先找到系统当前就绪列表中最高优先级的线程,然后通过rt_hw_context_switch_to()函数实现第一次的线程切换。此时,线程就绪队列中有主线程(优先级为10)和空闲线程(优先级为31),因此从线程就绪队列中选择主线程开始运行。
2.3.1 第一次线程切换函数
第一次线程切换函数rt_hw_context_switch_to()采用汇编编程,其功能是为触发PendSV进行第一次线程切换做前期准备工作,内部用到的一些变量定义如下所示,其功能流程如图9所示。
图9 rt_hw_context_switch_to()函数执行流程
该函数执行结束后,PendSV中断立即被触发,执行中断处理程序,PendSV_Handler进行线程的真正切换。
2.3.2 PendSV中断服务程序
PendSV中断服务程序主要分为上文保存和下文切换两部分,该函数执行流程如图10所示。首先获取中断标志位并清零,然后判断rt_interrupt_from_thread的值是否为0。如果是则表示系统进行第一次线程切换,不用做上文保存的工作,直接执行下文切换;如果不为0则需要先保存上文,然后再切换到下文。下文切换要做的工作主要有:加载下一个即将运行的线程栈的内容到CPU寄存器,更改PC指针和PSP指针,从而实现程序的跳转。
图10 PendSV_Handler中断处理程序执行流程
FLASH区一般用于存放中断向量表、程序代码和常数等。本文选用STM32L431芯片,片内FLASH地址范围为0x0800_0000~0x0803_FFFF,共256 KB。其中前256 B为中断向量表,共有98个中断向量。RT-Thread启动后FLASH中各区的分配情况如表1所示。
表1 FLASH中的各区分配情况
3.2.1 RT-Thread启动后RAM使用情况分析
静态随机存储器SRAM是RAM的一种,一般用来存储静态变量、临时变量、全局变量等。在STM32L431芯片中,SRAM的大小为64 KB,地址范围为0x2000_0000~0x2001_0000。该芯片栈空间是按照地址从高向低的方向生长的,而堆空间的生长方向与之相反,以此减少栈空间和堆空间的重叠错误。RT-Thread启动后RAM中各个段分配情况如表2所示。特别要注意的是,heap段是根据定义的静态数组rt_heap来决定的,因此在编译后rt_heap也属于bss段;同时stack段未有相关的初始操作,默认从RAM地址的最大值+1当作栈顶向下使用。
表2 RAM中的各段分配情况
3.2.2 各线程RAM分配情况分析
RT-Thread启动时,系统先后建立了主线程main、空闲线程idle,这两个线程的RAM分配情况如表3所示。表中的成员名在线程控制块结构体中定义,数据采用十六进制表示,可以通过对程序进行单步调试获得,这些数据会因每次程序的运行而有所变化;sp的值等于stack_addr+stack_size-68(栈帧大小为68,其中前64 B为固定区域,用于保存线程上下文,即在线程切换上下文时保存R0~R12、R14、R15、xPSR等16个寄存器的值,还有4 B为未使用到的FPU标志位flag)。
表3 系统线程的RAM分配情况
在主线程函数app_init中建立3个用户线程:绿灯线程thd_greenlight、红灯线程thd_redlight和蓝灯线程thd_bluelight。主线程在这三个用户线程启动完后进入终止状态,所以此时系统中实际存在的是空闲线程、绿灯线程、红灯线程和蓝灯线程这四个线程,它们对应的RAM分配如表4所示。
表4 主线程终止后线程的RAM分配情况
RT-Thread启动后,空闲线程、绿灯线程、红灯线程和蓝灯线程这四个线程之间的指向关系如图11所示。在RT-Thread中就绪列表的每个优先级对应一条双向链表,即31优先级的空闲线程处于一个链表,10优先级的红灯线程、蓝灯线程和绿灯线程处于一个链表。此处以10优先级对应的链表为例,可在最先启动的红灯线程中输出就绪列表中10优先级的链表状况,输出结果为:
图11 就绪列表中用户线程之间的关系
其中0x2000_1368地址为就绪列表中10优先级对应的双链表根节点,即0x2000_18C0、0x2000_1B40、0x2000_1640分别对应着绿灯线程、红灯线程和蓝灯线程。由于线程是通过自身控制块的tlist节点成员接入就绪列表中,故与TCB地址有着0x14的偏移,以红灯线程为例,即0x2000_1B40=0x2000_1B2C+0x14。
本文通过对SD-RT-Thread工程框架的启动流程进行分析,简要地给出芯片上电启动过程,结合相关源码及注释、流程图、表格等重点分析RT-Thread的启动过程;详细分析从板级资源到调度器的初始化过程、主线程和空闲线程的创建过程以及调度器的启动过程;最后分析STM32L431的存储器使用情况,并深入研究在RTThread启动后RAM的使用情况以及各线程的分配情况。通过剖析,读者可以快速理解RT-Thread的启动过程,了解内部工作机制,同时也可为RT-Thread在更多的领域应用起到推动作用。