目录
要学习FreeRtos,必然少不了与CPU内核打交道,本章选择STM32F10X系列单片机内嵌ARM Cortex-M3核(简称CM3)分析,由于内核功能比较多,我们只摘取与操作系统相关的部分。更为详细的内核知识,推荐阅读《CM3权威指南CnR2 宋岩 译》。
注1:该文基于STM32F103ZE单片机+keil5编译器进行分析,仅分析单片机内部FLASH启动模式。
注2:该文基于FreeRtos V9.0.0版本分析
一、keil5编译STM32程序的存储分析
1.1 CM3内核寻址空间映射
如下图所示:
对于STM32F103ZE型号的单片机,Code区采用容量为512kB总线接口的NorFlash;SRAM区为64kB的SRAM存储器,暂不考虑扩展SRAM。
1.2 程序静态存储和动态执行
静态存储和动态执行如下图所示:
各存储区说明如下表:
存储区 | 属性 | 存储位置 | 运行位置 | 存储内容 | |
CODE | 只读 | NorFlash | NorFlash | 代码区:所有程序指令。 (起始为MSP初始化值+Reset向量) | |
RO-DATA | 只读 | NorFlash | NorFlash | 常量区:const修饰的常量、字符串。 | |
RW-DATA | 读/写 | NorFlash | SRAM | 变量区:初始化为非0的全局、静态变量。 | |
ZI-DATA | 变量区 | 读/写 | 无 | SRAM (运行中分配) | 变量区:未初始化和初始化为0的全局、静态变量。 |
heap | 读/写 | 无 | SRAM (运行中分配) | 堆区:使用编译器微库,用于malloc申请动态内存。 | |
stack | 读/写 | 无 | SRAM (运行中分配) | 栈区:特权级线程(Thread)、中断(异常)模式的局部变量。 |
(1)单片机内部FLASH启动模式下,ICode总线只能从NorFLASH取指令,代码段只能在NorFlash运行。
(2)总线接口的NorFlash可以用作只读内存,RO-DATA(只读数据)可以不搬移至内存。
(3)heap段使用需要启动keil的微库,才能通过malloc等操作进行动态内存申请,操作系统虽然支持使用该方式分配系统堆区及各任务栈、信号量等,但最好不要使用,使用操作系统自带的分配方式更为可靠。
(4)stack段默认给特权级线程、中断(异常)模式使用,由MSP寄存器控制出入栈,如果切换到用户模式,自动切换为进程栈指针寄存器(PSP),用户模式需要重新申请栈区,PSP指向用户栈区;操作系统中的堆区可以根据需求设置在RW-DATA段、ZI-DATA堆区、ZI-DATA变量区。
(5)ZI-DATA区:该类变量初始值全为0,故不体现在静态存储中,在运行态由指令进行内存分配,分配代码由Keil编译器自动生成,我们只需要在工程配置和启动文件中将存储器参数和堆栈申请空间配置好就行,如下所示:
Keil编译器存储器设置NorFLASH、SRAM:
启动文件(.s)中主程序堆(heap)空间分配:
启动文件(.s)中主程序栈(stack)空间分配:
编译结果:
Map文件查看:
二、CM3操作模式和特权极别
- CM3有2种操作模式: 处理者模式(或中断(异常)模式 handler mode )、线程(Thread mode)模式。
- CM3有2种权利级别: 特权级、用户级,特权级使用MSP栈指针寄存器,用户级使用PSP栈指针寄存器。两种级别的栈相互独立。
- 处理者模式(handler mode)只能运行在特权级别。
- 用户级对系统控制空间(SCS)的访问将被阻止——该空间包含了配置寄存器 组以及调试组件的寄存器组。还禁止使用 MRS/MSR 访问,除了 APSR 之外的特殊功能寄存器。如果以身试法,则对于访问特殊功能寄存器的,访问操作被忽略;而对于访问 SCS 空间的,将 触发fault 异常。
- 软件触发中断寄存器可以在用户级下访问以产生软件中断(利用这个特性实现用户模式到特权模式转变)。
各种模式之间的切换:
模式切换 | 触发条件 | 栈指针 |
特权级Thread 模式 | 上电启动 | MSP |
特权级Thread 模式->特权级handler模式 | 中断、异常触发 R14更新为0XFFFFFFF9 | MSP |
特权级Thread 模式->用户级Thread 模式 | 操作寄存器CONTROL[0]置1 | MSP切换为PSP |
特权级handler模式->特权级Thread 模式 | 调用指令“BX R14” 其中R14=0XFFFFFFF9 | MSP |
特权级handler模式->用户级Thread 模式 | 调用指令“BX R14” 其中R14=0XFFFFFFFD | MSP切换为PSP |
用户级Thread 模式->特权级handler模式 | 中断、异常触发 R14更新为0XFFFFFFFD | PSP切换为MSP |
用户级Thread 模式->特权级Thread 模式 | 主动触发异常(SVCall异常) 调用指令“BX R14”返回异常 其中R14=0XFFFFFFF9 | PSP切换为MSP |
三、CM3内核环境相关寄存器
CM3用于环境保存与恢复的寄存器主要包括通用寄存器组、状态寄存器组。
3.1 通用寄存器组,
如下图所示:
(1)通用寄存器
R0-R12 都是 32 位通用寄存器,用于数据操作、暂存。绝大多数 16 位 Thumb 指令只能访问 R0-R7,而 32 位 Thumb-2 指令可以访问所有寄存器。
(2)堆栈指针(SP)
R13用于栈指针(SP),指向栈区(stack),通过入栈出栈分配和释放临时变量。比如一个函数中定义了一些临时变量,调整SP指针(入栈)分配临时变量的存储空间,函数返回时,调整SP指针(出栈)释放掉堆栈空间,所以临时变量初始值是个不确定的值,因为出栈仅是调整SP指针,并不对栈空间进行归零等操作,且临时变量不宜过多,防止栈空间溢出。
CM3内核拥有两个堆栈指针:
主堆栈指针(MSP):特权级别下使用,复位后缺省使用的堆栈指针,初始值指向内存ZI-DATA栈区(Stack)的栈底(一般是高地址),代码区第一条指令就是MSP的初始化值,装置上电会先更新MSP,然后在执行reset。
进程堆栈指针(PSP):用户级别下使用,在操作系统中,指向任务的栈区,任务栈区一般从操作系统动态内存区分配,操作系统动态内存区可以根据可以根据需求设置在RW-DATA段、ZI-DATA堆区、ZI-DATA变量区。
(3)连接寄存器(LD)
R14用作连接寄存器(LD),连接寄存器当调用一个子程序时,R14存储指令返回地址,如果只有 1 级子程序调用的代码无需访问内存(堆栈内存),从而提高了子程序调用的效率。如果多于 1 级,则需要把前一级的 R14 值压到堆栈里。
程序进入handler模式(中断(异常))时,R14自动入栈保存,保存后R14更新为0xFFFFFFFX,通过指令”BX R14”进行异常返回。X的bit0为1表示返回thumb状态,bit1和bit2表示返回后sp用msp还是psp及返回到特权级别还是用户级别。合法的返回值如下所示:
0xFFFF_FFF1 | 返回handler模式【应用于中断嵌套的场景】 |
0xFFFF_FFF9 | 返回线程模式,并使用主堆栈(SP=MSP)【返回特权级线程(Thread)模式】 |
0xFFFF_FFFD | 返回线程模式,并使用线程堆栈(SP=PSP)【返回用户级线程(Thread)模式】 |
(4) 程序计数寄存器(PC)
R15用作程序计数寄存器(PC),程序计数寄存器指向NorFLASH代码区。正常运行,取指令完成,PC自动加1,既PC指向下一条指令,如果执行跳转指令或者直接修改PC寄存器的值, 就能改变程序的执行流。
3.2 状态寄存器组
状态字寄存器组包括:应用程序 PSR(APSR)、 中断号 PSR(IPSR)、执行 PSR(EPSR),环境保存时是三个寄存器会合并为一个32位xPSR进行保存。
3.3 CM3内核环境自动保存功能
与一些高端内核不同,CM3由线程模式(Thread)进入handler模式会自动进行部分寄存器的入栈保存,下图参考自《CM3权威指南CnR2 宋岩 译》:
进入handler模式后R14自动更新为0xFFFFFFFX。
四、CM3中断
FreeRtos操作系统涉及CM3的三个中断,如下图:
4.1 SVCall(软中断:系统服务调用)
可在优先级比SVCall低的异常(中断)或线程中执行该指令,可以立刻切换到该异常模式。特权级和用户级线程模式(Thread)均可以调用该服务进入handler模式。
使用方法:执行指令“SVC 立即数”,在FreeRtos系统中,仅用于启动第一个任务。
4.2 PendSV(软中断:可悬起系统调用)
与SVCall相反,可以在高优先级中断(异常)中设置为悬起,等所有高优先级中断返回后,再执行PendSV,俗称“缓期执行”,所以PendSV的优先级一般会设置为最低。
也可以在用户级或特权级线程模式调用该服务进入handler模式。
在FreeRtos系统中,用于上下文切换,悬起 PendSV 的方法是:手工往 NVIC 的 PendSV 悬起寄存器中写 1。
4.3 SysTick(系统定时器),
可编程的定时中断(自检),为操作系统提供时间片,进行轮转和抢占式调度。设置方法可以参考《CM3权威指南CnR2 宋岩 译》。
五、FreeRtos启动和运行
5.1启动过程
FreeRtos启动如下图所示(仅分析统一初始化任务后,启动任务调度):
5.2 任务控制块结构
5.2.1存储结构
任务A初始化后的存储结构,如下图所示:
注1:操作系统的内存区可以分配到RW-DATA区、ZI-DATA变量、ZI-DATA堆区(heap)。
注2:任务初始化会将环境恢复数据初始值入栈。
5.2.1源码释义(TCB)
控制块结构体定义如下:
typedef struct tskTaskControlBlock
{
volatile StackType_t * pxTopOfStack;/*< 栈顶指针,必须是TCB第1成员*/
#if ( portUSING_MPU_WRAPPERS == 1 )
xMPU_SETTINGS xMPUSettings; /*< MPU设置被定义为端口层的一部分。必须TCB第2成员*/
#endif
ListItem_t xStateListItem; /*< 任务状态链表条目*/
ListItem_t xEventListItem; /*< 事件链表条目*/
UBaseType_t uxPriority; /*< 任务当前的优先级。 0优先级最低*/
StackType_t * pxStack; /*< 栈最低地址*/
char pcTaskName[ configMAX_TASK_NAME_LEN ]; /*< 任务名称,最大16字符*/
#if ( portSTACK_GROWTH > 0 )
StackType_t * pxEndOfStack; /*<栈最高地址*/
#endif
#if ( portCRITICAL_NESTING_IN_TCB == 1 )
UBaseType_t uxCriticalNesting; /*< 临界区进入嵌套*/
#endif
// 可视化跟踪调试
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxTCBNumber; /*< 控制块编号,初始化时记录,唯一*/
UBaseType_t uxTaskNumber; /*< 任务编号,动态修改*/
#endif
// 互斥量功能
#if ( configUSE_MUTEXES == 1 )
UBaseType_t uxBasePriority; /*< 原优先级,用于取消优先级继承 */
UBaseType_t uxMutexesHeld; /*< 互斥信号持有个数*/
#endif
// 任务钩子
#if ( configUSE_APPLICATION_TASK_TAG == 1 )
TaskHookFunction_t pxTaskTag;
#endif
// 私有协程指针
#if ( configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0 )
void * pvThreadLocalStoragePointers[ configNUM_THREAD_LOCAL_STORAGE_POINTERS ];
#endif
// 运行时间统累计
#if( configGENERATE_RUN_TIME_STATS == 1 )
uint32_t ulRunTimeCounter;
#endif
// 配置新库重入
#if ( configUSE_NEWLIB_REENTRANT == 1 )
struct _reent xNewLib_reent;
#endif
// 任务通知
#if( configUSE_TASK_NOTIFICATIONS == 1 )
volatile uint32_t ulNotifiedValue;
volatile uint8_t ucNotifyState;
#endif
// 动态分配时设置TRUE,用于释放内存
#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
uint8_t ucStaticallyAllocated;
#endif
// 强制终止阻塞功能
#if( INCLUDE_xTaskAbortDelay == 1 )
uint8_t ucDelayAborted;
#endif
} tskTCB;
5.3 启动第一个任务
程序启动第一个任务的方法是执行“SWI 0”指令,触发SVCall异常,执行“BX R14”退出异常(其中R14=0xFFFFFFFD),进入用户级线程模式执行运行态任务指令。
5.3.1源码释义(SVCall中断)
__asm void vPortSVCHandler( void )
{
PRESERVE8 /* 8字节对齐*/
ldr r3, =pxCurrentTCB /* 加载R3=&pxCurrentTCB */
ldr r1, [r3] /* 加载R1=pxCurrentTCB */
ldr r0, [r1] /* 加载R0=pxCurrentTCB->pxTopOfStack*/
ldmia r0!, {r4-r11} /* 将R0用作栈指针,从pxCurrentTCB栈中恢复R4-R11*/
msr psp, r0 /* 恢复psp=R0,指向当前任务栈顶R0存储位置*/
isb /* 指令同步排序,与流水线相关*/
mov r0, #0
msr basepri, r0 /* 停止屏蔽任何中断.*/
orr r14, #0xd /* 将R14设置为用户级线程模式R14=0xFFFFFFF9|0xd=0xFFFFFFFD*/
bx r14 /* 执行bx r14,将切换至用户级线程模式,使用PSP栈指针自动恢复R0-R3、
R12、R14、R15、xPSR,并且PSP指向了栈底位置*/
}
注:内核由特权级线程切换至handler模式,R14=0XFFFFFFF9,将R14修改为0XFFFFFFFD,调用“BX R14”,可返回至用户级线程模式。
5.3.2 启动流程
5.4 上下文切换详解
上下文切换在PendSv中断服务中进行。
5.4.1上下文切换场景
1、时间片切换:SysTick中断定时打断任务运行,由用户级线程模式切换至特权级Handler模式,再由SysTick中断挂起PenSv异常,SysTick中断执行完成后响应PendSv,在PendSv中进行上下文切换,切换完成后,返回用户级线程模式继续执行切换后的任务。
2、运行任务切换至非运行态:当前运行任务阻塞或挂起后,线程主动启动PendSv软中断,由用户级线程模式切换至特权级Handler模式,在PendSv中断服务程序中进行上下文切换,切换完成后,返回用户级线程模式执行切换后的任务。
5.4.2源码释义(PendSv中断)
// 上下文切换(抢占式)
__asm void xPortPendSVHandler( void )
{
extern uxCriticalNesting;
extern pxCurrentTCB;
extern vTaskSwitchContext;
PRESERVE8
mrs r0, psp /* 【注1.1:环境保存】PSP->R0*/
isb /* 【注1.2:环境保存】指令同步排序,与流水线相关*/
ldr r3, =pxCurrentTCB /* 【注1.3:环境保存】加载R3=&pxCurrentTCB*/
ldr r2, [r3] /* 【注1.4:环境保存】加载R2=pxCurrentTCB */
stmdb r0!, {r4-r11} /* 【注1.5:环境保存】R0用作栈指针,手动入栈R4-R11至切换前的任务栈.*/
str r0, [r2] /* 【注1.6:环境保存】保存栈顶 pxCurrentTCB->pxTopOfStack=R0*/
stmdb sp!, {r3, r14} /* 【注2.1:任务切换】入主栈r3-r14 */
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0 /* 【注2.2:任务切换】屏蔽部分中断*/
dsb /* 【注2.3:任务切换】数据同步隔离*/
isb /* 【注2.4:任务切换】指令同步排序*/
bl vTaskSwitchContext /* 【注2.5:任务切换】跳转至函数vTaskSwitchContext执行*/
mov r0, #0 /* 【注2.6:任务切换】清空R0*/
msr basepri, r0 /* 【注2.7:任务切换】取消中断屏蔽*/
ldmia sp!, {r3, r14} /* 【注2.8:任务切换】出主栈 R3-R14*/
ldr r1, [r3] /* 【注3.1:环境恢复】加载R1=pxCurrentTCB*/
ldr r0, [r1] /* 【注3.2:环境恢复】加载R1=pxCurrentTCB->pxTopOfStack. */
ldmia r0!, {r4-r11} /* 【注3.3:环境恢复】R0用作栈指针,手动从出栈切换后的任务出栈R4-R11*/
msr psp, r0 /* 【注3.4:环境恢复】恢复psp=R0. */
isb /* 【注3.5:环境恢复】指令同步排序*/
bx r14 /* 【注3.6:环境恢复】R14=0XFFFFFFFDL,将切换至用户级线程模
式, 使PSP栈指针自动恢复R0-R3、R12、R14、R15、xPSR,
并且PSP指向了栈底位置*/
nop
}
5.4.3上下文切换流程详解
依据运行态任务阻塞自动启动上下文切换的场景分析,假设任务A由运行态切换到了阻塞态,而任务B是当前最高优先级的就绪态任务,任务A主动启动PendSv中断,上下文切换过程如下所示:
由上图可见,对比切换前和切换后,任务A和任务B的栈状态正好相反,任务A进行了环境保存,任务B进行了环境恢复。