引言
本文以不带内存保护的嵌入式实时操作系统FreeRTOS为研究对象,在TMS570处理器平台上移植了带内存保护功能的操作系统FreeRTOSMPU,不仅可以保护RTOS内核代码不被任务破坏并防止内核数据崩溃,也可以保护系统外设不被任务无意地修改,确保检测出任务堆栈溢出[2],从而提高系统的可靠性。
1 FreeRTOSMPU简介
1.1 FreeRTOS简介
FreeRTOS操作系统是一个开源、轻型、简单、可移植、可裁减的免费实时操作系统,能够很好地移植到各种体系结构的微型处理器[3]。FreeRTOS仅仅包含了任务调度、任务管理、时间管理、内存管理和任务间的通信和同步等基本功能[4],不提供输入/输出管理、文件系统、网络等额外的服务,用户可根据需要自行实现。
1.2 MPU概览
MPU(Memory Protection Unit,内存保护单元)是CortexR4F内核的一个组件,可以用来捕获非法或者危险的内存访问。通过MPU可以设置保护区域的属性和访问权限,当处理器访问内存的一个区域时,MPU比较该区域的访问权限和当时的处理器模式。如果请求符合区域访问标准,则MPU允许内核读/写主存;否则,MPU产生一个异常信号。
MPU在执行其功能时,是以region为单位的。一个region其实就是一段连续的地址,只是它们的位置和范围都要满足一些限制(对齐方式,最小容量等)。CortexR4的MPU共支持8个regions,还允许把每个region进一步划分成更小的子region。此外,还允许启用一个背景region(即没有MPU时的全部地址空间),不过它只能由特权级享用。在启用MPU后,就不得再访问定义之外的地址区间,也不得访问未经授权的region。否则,将以“访问违例”处理,触发MPU错误。MPU定义的regions可以相互交迭。如果某块内存落在多个region中,则访问属性和权限将由编号最大的region来决定[5]。
MPU可以通过下列方法提高系统的可靠性[6]:
◆ 阻止应用程序破坏操作系统使用的代码和数据;
◆ 阻止一个任务访问其他任务的数据区,从而把任务隔开;
◆ 可以把关键数据区设置为只读,从根本上消除了被破坏的可能;
◆ 检测意外的存储访问,如堆栈溢出、数组越界等。
1.3 FreeRTOSMPU简介
FreeRTOSMPU操作系统是在FreeRTOS操作系统的基础上加入内存保护模块而来的,其特点如下[2]:
① 可以创建任务运行在特权模式下或者用户模式下,用户模式下的任务只能访问它们自己的堆栈和最多3个用户自定义的内存区域,在任务被创建时用户自定义的内存区域被指定给任务,但是在运行时可以重新配置;
② 用户模式下的任务不能共享数据存储,但是用户模式下的任务可以使用标准队列和信号量机制来传递信息;
③ 特权模式下的任务可以自己进入用户模式,但是一旦进入用户模式,它就不能再回到特权模式;
④ 位于Flash区域的FreeRTOS API只有在进入特权模式下才能访问;
⑤ 位于RAM区域的RTOS内核维护的数据只有在进入特权模式下才能访问;
⑥ 系统外设只有在处理器进入特权模式下才能访问,标准外设可以被任何代码访问,也可通过一个用户自定义的内存区域来显式地保护。
与没有内存保护模块的标准FreeRTOS操作系统相比,FreeRTOSMPU可以用来保护RTOS内核代码不被任务破坏并防止内核数据崩溃,也可以保护系统外设不被任务无意地修改,确保检测出任务堆栈溢出,从而提高了软件的可靠性。当然,不足之处是引入内存保护模块后,操作系统运行需要的RAM、ROM资源会增加,上下文切换时还需要进行MPU相关的设置,因此任务切换时间增加。
2 TMS570硬件架构简介
TMS570LS系列是TI公司推出的基于ARM CortexR4F处理器的锁步双内核微处理器。本系统使用的TMS570LS20216最高运行频率达到160 MHz,配置2 MB带 ECC的内置Flash和160 KB带 ECC的RAM,带有TI核心安全策略,其可提供双核锁步CPU架构、硬件PBIST、MPU和片上时钟及电压监控等一些关键功能的安全特性来满足汽车电子、铁路和航天应用的需求[7]。
3 FreeRTOSMPU操作系统的移植过程
本移植针对TMS570LS系列芯片,使用的编译软件是Code Composer Studio v5.3.0,操作系统版本是V7.4.0,由于FreeRTOS官方提供的demo中没有在TMS570上实现MPU功能,因此本文移植主要参考的是针对ARM CortexM3实现的FreeRTOSMPU,移植的主要内容集中在3个文件:portmacro.h,port.c,portasm.asm。
其中,portmacro.h文件主要是关于编译器相关的数据类型的定义、CPU架构相关的定义、临界区管理函数的宏定义和函数声明等。port.c文件中包含了与移植有关的C函数,包括堆栈初始化函数、任务调度器启动函数、设置默认MPU函数等。portasm.asm文件主要实现内核任务切换处理,包括上下文保存和恢复、启动第一个任务等。以下主要针对关键模块的移植实现做详细的分析与说明。
3.1 堆栈初始化
图1 任务堆栈结构
堆栈的初始化是嵌入式操作系统移植的关键,是针对硬件结构以及堆栈保存特点进行的设置。堆栈初始化函数主要作用是初始化任务的堆栈结构,定义堆栈的上下文内容,将所有的寄存器值保存到堆栈中,使其看起来像刚刚发生过中断一样。堆栈保存格式如图1所示。
在堆栈的结构中,第一个入栈的是任务函数的起始地址,SP保存的是任务堆栈栈顶,然后预留一定空间给寄存器R12~R1,R0则保存传递给任务的参数,最后设置CPSR和ulUsingFPU标志位。
portSTACK_TYPE *pxPortInitialiseStack( portSTACK_TYPE *pxTopOfStack, pdTASK_CODE pxCode, void *pvParameters, portBASE_TYPE xRunPrivileged){
portSTACK_TYPE *pxOriginalTOS;
pxOriginalTOS = pxTopOfStack;
pxTopOfStack--;
/*首先堆栈上存放的是返回地址,也就是任务运行的起始地址,加上偏移地址4使返回地址看起来是从IRQ中断服务程序中返回的*/
*pxTopOfStack=(portSTACK_TYPE)pxCode + 4;
pxTopOfStack--;
*pxTopOfStack = ( portSTACK_TYPE ) 0x00000000;/* R14入栈 */
pxTopOfStack--;
*pxTopOfStack = ( portSTACK_TYPE ) pxOriginalTOS;
/* SP入栈 */
pxTopOfStack--;
/*给寄存器预留空间*/
pxTopOfStack -= portSPACE_BETWEEN_TOS_AND_PARAMETERS;
/* 函数参数通过R0传递 */
*pxTopOfStack = ( portSTACK_TYPE ) pvParameters;
/* R0入栈 */
pxTopOfStack--;
/*设置CPSR为用户模式或者系统模式, 中断使能 */
……
pxTopOfStack--;
/* 最后入栈的是 ulUsingFPU,默认不使用FPU */
*pxTopOfStack = pdFALSE;
return pxTopOfStack;
}
3.2 任务调度器的启动
在创建任务之后将进行任务调度,首先配置MPU,然后设置并启动系统的时钟节拍,初始化临界区嵌套次数为0,最后启动优先级最高的就绪任务。具体程序如下:
portBASE_TYPE xPortStartScheduler(void){
/* 配置默认的MPU*/
prvSetupDefaultMPU();
/* 设置操作系统所使用的定时器中断 */
prvSetupTimerInterrupt();
/* 初始化临界区嵌套次数 */
ulCriticalNesting = 0;
/* 启动第一个任务*/
vPortStartFirstTask();
return pdFAIL;
}
3.3 设置默认的MPU
FreeRTOS-MPU操作系统在任务调度器启动时默认配置了5个区域,配置的流程图如图2所示,配置完成后内存区域分布如表1所列。
图2 MPU默认配置流程图
3.4 初始化TCB中xMPUSettings成员
在任务创建时,xTaskCreateRestricted()会调用vPortStoreTaskMPUSettings()对TCB的结构体成员xMPU_SETTINGS xMPUSettings进行初始化,初始化的结果根据创建任务时提供的参数不同而不同。如果创建任务时定义了区域,那么此函数的作用是初始化如下3个区域:区域4是任务堆栈区、5和6是用户自定义区域。这3个区域只能被当前任务访问,见表2。
表1 默认配置的MPU区域
表2 任务堆栈和用户自定义区域
如果创建任务时没有定义区域,那么任务的MPU配置见表3,区域4是整个RAM区,区域5是用户堆栈区,区域6无效。
表3 无自定义区时MPU配置
3.5 保存任务上下文
此上下文的保存是按照进入ISR中断来编写的,入栈顺序是返回地址、用户模式下的LR、R13R0、SPSR,然后根据ulFPUContextConst的值确定任务是否维护FPU上下文。由于在初始化堆栈时默认不使用FPU,所以无需保存FPU上下文,最后把新的任务栈顶赋值给任务TCB结构体的第一个成员。保存任务上下文核心代码如下:
portSAVE_CONTEXT .macro
DSB
STMDBSP!, {R0};R0入栈
STMDBSP,{SP}^;用户模式下SP入栈
SUBSP, SP, #4
LDMIASP!,{R0};用户模式下SP传给R0
STMDBSPR0!, {LR};当前模式LR即返回地址入栈
MOVSPLR, R0; 现在LR已经入栈,因此可以
;用来代替R0
LDMIASP!, {R0};R0出栈下面要把它推入任务栈
STMDBSPLR,{R0~LR}^;把用户模式下R0~R14推
;入任务栈
SUBSPLR, LR, #60
MRSSPR0, SPSR
STMDBSPLR!, {R0}; SPSR 入栈
STMDBSPLR!, {R0};保存标志位
LDRSPR0,pxCurrentTCBConst
;取当前任务的TCB
LDRSPR0, [R0]
STRSPLR, [R0] ; 把新的栈顶存储在TCB中
.endm
3.6 恢复任务上下文
根据TCB中任务的MPU设置来设置内存区域4、5和6,然后判断FPU标志位,由于没有保存FPU上下文,所以不需要恢复,再次把SPSR、R0R14出栈,最后修正LR的值,程序返回。恢复任务上下文流程图如图3所示。
图3 恢复任务上下文流程图
任务代码略——编者注。
3.7 启动第一个任务
首先把CPU工作模式变为管理模式,然后修复之前在pxPortInitialiseStack函数中入栈的寄存器值,程序自动跳到第一个任务去执行。
vPortStartFirstTask
CPS #0x13;管理模式
portRESTORE_CONTEXT;恢复任务上下文
4 移植测试
由于本文移植的是FreeRTOS-MPU操作系统,要想验证是否移植成功,需要进行两方面的测试:首先测试任务之间能否正确地调度与切换,然后测试MPU能否实现对地址空间的保护。硬件测试平台选用的是Keil公司生产的MCBTMS570开发板。
4.1 任务调度测试
在main函数里用xTaskCreate()创建两个LED闪烁的任务vLED1Task和vLED2Task,优先级分别是3和4,让两个任务在初始化后不断的任务切换,LED灯交替亮灭。vLED1Task代码略——编者注。
void vLED1Task( void *pvParameters ) {
while ( 1 ) {
gioSetBit(gioPORTA, 1, 1 ); /*设置PORTA1,点亮LED1*/
vTaskDelay(500); /*挂起任务,延时500个时钟滴答*/
gioSetBit(gioPORTA, 1, 0 );/*复位PORTA1,熄灭LED1*/
vTaskDelay(500);/*挂起任务,延时 500 个时钟滴答*/
}
}
程序运行的结果是两个LED灯不断的闪烁,时间间隔是500 ms,因此任务能正常调度。
4.2 MPU测试
在main函数中用xTaskCreateRestricted()创建两个用户模式下的任务,这两个任务各定义一个region,在任务函数中分别访问内核数据、内核代码、另一个任务自定义的region和另一个任务堆栈。
void vTaskFunction1(void *pvParameters){
while ( 1 ){
test = (*(unsigned long *)0x08001614);/*访问内核
/*数据*/
test = (*(unsigned long *)0x00001000); /*访问内核
/*代码*/
test = cReadWriteArray2[3]; /*访问其他任务自定义的
/*区域*/
test = xTaskStack2[0]; /*访问其他任务的堆栈*/
test= (*(unsigned long *)0xFFFFFF00); /*访问系统
/*区寄存器*/
test = (*(unsigned long *)0x00500000); /*访问预留
/*地址*/
}
}
实验结果表明MPU可阻止应用程序破坏操作系统使用的代码和数据、阻止一个任务访问其他任务的数据区、阻止用户模式任务访问系统外设、阻止任务访问未定义区域。由此可见,用户模式下的任务仅仅只能访问它们自己的堆栈和用户自定义的内存区域。
综合上述两个测试,表明FreeRTOSMPU操作系统移植成功。
结语
FreeRTOSMPU操作系统利用内部的内存保护模块能有效地保护内核代码和数据,实现任务之间的隔离与保护,提高了系统可靠性,因此本移植可以应用在对可靠性要求高的行业(如航天领域等)。