根据个人的学习方向,学习FreeRTOS。由于野火小哥把FreeRTOS讲得比较含蓄,打算在本专栏尽量细化一点。作为个人笔记,仅供参考或查阅。
配套资料:FreeRTOS内核实现与应用开发实战指南、野火FreeRTOS配套视频源码、b站野火FreeRTOS视频、CM3权威指南。搭配来看更佳哟!!!
下一节关于任务定义与任务切换,涉及很多CM3内核相关知识,特开展一节粗略谈谈。来自于CM3权威指南。
Cortex-M3 处理器拥有 R0-R15 的寄存器组,还有着一些特殊功能寄存器。
其中 R13 作为堆栈指针 SP。SP 有两个,但在同一时刻只能有一个可以看到。
主堆栈指针(MSP):复位后缺省使用的堆栈指针,用于操作系统内核以及异常处理例程(包括中断服务例程)
进程堆栈指针(PSP):由用户的应用程序代码使用。
堆栈指针的最低两位永远是 0,这意味着堆栈总是 4 字节对齐的。
特殊功能寄存器:xPSR(程序状态寄存器,x=A、I、E)、中断屏蔽寄存器(PRIMASK、FAULTMASK、BASEPRI)、控制寄存器(CONTROL)
程序状态寄存器:记录ALU标志(0标志,进位标志,负数标志,溢出标志),执行状态,以及当前正服务的中断号。
APSR:应用程序状态寄存器
IPSR: 中断程序状态寄存器
EPSR:执行程序状态寄存器
中断屏蔽寄存器:
PRIMASK:PRIMASK可以屏蔽除NMI和HardFault外的所有异常和中断。单一位的寄存器。缺省值为0,表示开着中断。在很多应用中需要暂时屏蔽所有的中断进行一些对时序要求较高的任务,不然容易发生bug,例如I2C通讯。此时可以使用PRIMASK寄存器。
CPSIE I; //PRIMASK = 0 (开中断)
CPSID I; //PRIMASK = 1 (关中断)
FAULTMASK:FAULTMASK可以屏蔽除NMI外的所有异常和中断。单一位的寄存器。缺省值为0,表示开着异常。FAULTMASK与PRIMASK的区别是FAULTMASK可以把HardFault也屏蔽掉。FAULTMASK会在异常退出时自动清零。
CPSIE F; //FAULTMASK = 0 (开异常)
CPSID F; //FAULTMASK = 1 (关异常)
BASEPRI:当我们需要屏蔽优先级低于某一阈值的中断,我们可以往 BASEPRI中写入该阈值。寄存器位数最多9位(由表达优先级的位数决定)。缺省值为0,表示开着所有中断。例如要屏蔽优先级不高于0X40的中断(注意 stm32是高四位控制优先级,低四位默认为0)。
MOVS R0,#0X40
MSR BASEPRI,R0;
在FreeRTOS中开关中断就是通过 BASEPRI寄存器实现的。
控制寄存器:
CONTROL:定义特权状态,并且决定使用哪一个堆栈指针。
位 | 功能 |
CONTROL[1] | 堆栈指针选择 0=主堆栈指针MSP 1=进程堆栈指针PSP Handle模式下,只允许使用MSP。 Thread模式下,可选择MSP或PSP。 |
CONTROL[0] | 0=特权级的Thread模式 1=用户级的Thread模式 Handle模式永远都是特权级。 |
Handle模式中,CONTROL[1]总为0,CONTROL[0]可以选0或1。
因此,仅当处于特权级的线程模式下,CONTROL[1]位才可写,其它场合下禁止写此位。改变处理器的模式也有其它的方式:在异常返回时,通过修改 LR 的位 2,也能实现模式切换。
仅当在特权级下操作时才允许写CONTROL[0]。一旦进入了用户级,唯一返回特权级的途径,就是触发一个(软)中断,再由服务例程改写该位。
NMI指不可屏蔽中断,当系统发生致命故障时,使用NMI。当发生NMI的因素时,不接受任何其他中断,启动紧急情况中断处理程序,并运行紧急情况处理程序软件。
HardFalut指硬件故障带来的异常中断。
异常(exception):打断程序顺序执行的事件。有 3 个系统异常:复位, NMI 以及硬 fault,它们有固定的优先级,并且它们的优先级号是负数,从而高于所有其它异常。
除了外部中断外, 当有指令执行了“非法操作”, 或者访问被禁的内存区间, 因各种错误产生的 fault, 以及不可屏蔽中断发生时,都会打断程序的执行,这些情况统称为异常。
在不严格的上下文中,异常与中断也可以混用。另外, 程序代码也可以主动请求进入异常状态的(常用于系统调用)。
操作模式和特权级别
操作模式:Handle模式或Thread模式。用于区别普通应用程序的代码和异常服务例程的代码(包括中断服务例程的代码)。
特权级别:特权机和用户级。
复位后,处理器默认进入线程模式,特权极访问。在特权级下,程序可以访问所有范围的存储器(如果有 MPU,还要在 MPU 规定的禁地之外),并且可以执行所有指令。
在特权级下的程序可以为所欲为,但也可能会把自己给玩进去——切换到用户级。一旦进入用户级,再想回来就得走“法律程序”了——用户级的程序不能简简单单地试图改写 CONTROL 寄存器就回到特权级,它必须先“申诉”:执行一条系统调用指令(SVC)。这会触发 SVC 异常,然后由异常服务例程(通常是操作系统的一部分)接管,如果批准了进入,则异常服务例程修改 CONTROL 寄存器,才能在用户级的线程模式下重新进入特权级。事实上,从用户级到特权级的唯一途径就是异常:如果在程序执行过程中触发了一个异常,处理器总是先切换入特权级, 并且在异常服务例程执行完毕退出时,返回先前的状态。
通过引入特权级和用户级,就能够在硬件水平上限制某些不受信任的或者还没有调试好的程序,不让它们随便地配置涉及要害的寄存器,因而系统的可靠性得到了提高。进一步地,如果配了 MPU,它还可以作为特权机制的补充——保护关键的存储区域不被破坏,这些区域通常是操作系统的区域。举例来说,操作系统的内核通常都在特权级下执行,所有没有被MPU禁掉的存储器都可以访问。在操作系统开启了一个用户程序后, 通常都会让它在用户级下执行,从而使系统不会因某个程序的崩溃或恶意破坏而受损。
当前优先级被存储在 xPSR 的专用字段中。当一个异常发生时,硬件会自动比较该异常的优先级是否比当前的异常优先级更高。
如果发现来了更高优先级的异常,处理器就会中断当前的中断服务例程(或者是普通程序),而服务新来的异常——即立即抢占。
既可以屏蔽优先级低于某个阈值的中断/异常(设置BASEPRI寄存器),也可以全体封杀(设置PRIMASK和FAULTMASK寄存器)。这是为了让时间关键( time-critical)的任务能在死线(deadline,或称最后期限)到来前完成,而不被干扰。
MPU:存储器保护单元
Cortex-M3 有一个可选的存储器保护单元。配上它之后,就可以对特权级访问和用户级访问分别施加不同的访问限制。
当检测到犯规(violated)时, MPU 就会产生一个 fault 异常,可以由fault 异常的服务例程来分析该错误,并且在可能时改正它。
MPU 最常见的使用就是由操作系统使用 MPU,以使特权级代码的数据(包括操作系统本身的数据)不被其它用户程序弄坏。
MPU 在保护内存时是按区(region)管理的。它可以把某些内存区设置成只读,从而避免了那里的内容意外被更改;还可以在多任务系统中把不同任务之间的数据区隔离。
SVC和PendSV
SVC(系统服务调用,亦简称系统调用)和 PendSV(可悬起系统调用),它们多用在上了操作系统的软件开发中。 SVC 用于产生系统函数的调用请求。例如,操作系统通常不让用户程序直接访问硬件,而是通过提供一些系统服务函数,让用户程序使用 SVC 发出对系统服务函数的呼叫请求,以这种方法调用它们来间接访问硬件。因此,当用户程序想要控制特定的硬件时,它就要产生一个SVC 异常,然后操作系统提供的 SVC 异常服务例程得到执行,它再调用相关的操作系统函数,后者完成用户程序请求的服务。
PendSV(可悬起的系统调用),它和 SVC 协同使用。一方面, SVC 异常是必须在执行 SVC 指令后立即得到响应的(对于 SVC 异常来说,若因优先级不比当前正处理的高,或是其它原因使之无法立即响应,将上访成硬 fault),应用程序执行 SVC 时都是希望所需的请求立即得到响应。另一方面, PendSV 则不同,它是可以像普通的中断一样被悬起的(不像SVC 那样会上访)。 OS 可以利用它“缓期执行”一个异常——直到其它重要的任务完成后才执行动作。悬起 PendSV 的方法是:手工往 NVIC 的 PendSV 悬起寄存器中写 1。悬起后,如果优先级不够高,则将缓期等待执行。
PendSV 的典型使用场合是在上下文切换时(在不同任务之间切换)。例如,一个系统中有两个就绪的任务,上下文切换被触发的场合可以是:
执行一个系统调用
系统滴答定时器(SYSTICK)中断,(轮转调度中需要)
PendSV 异常会自动延迟上下文切换的请求,直到其它的 ISR 都完成了处理后才放行。为实现这个机制,需要把 PendSV 编程为最低优先级的异常。如果 OS 检测到某 IRQ 正在活动并且被 SysTick 抢占,它将悬起一个 PendSV 异常,以便缓期执行上下文切换。
NVIC
NVIC 的寄存器以存储器映射的方式来访问,除了包含控制寄存器和中断处理的控制逻辑之外, NVIC 还包含了 MPU、 SysTick 定时器以及调试控制相关的寄存器。
所有 NVIC 的中断控制/状态寄存器都只能在特权级下访问。不过有一个例外——软件触发中断寄存器可以在用户级下访问以产生软件中断。所有的中断控制/状态寄存器均可按字/半字/字节的方式访问。
SYSTICK
SysTick定时器被捆绑在NVIC中,用于产生SysTick异常(异常号: 15)。在以前,操作系统还有所有使用了时基的系统,都必须一个硬件定时器来产生需要的“滴答”中断,作为整个系统的时基。滴答中断对操作系统尤其重要。例如,操作系统可以为多个任务许以不同数目的时间片,确保没有一个任务能霸占系统;或者把每个定时器周期的某个时间范围赐予特定的任务等,还有操作系统提供的各种定时功能,都与这个滴答定时器有关。因此,需要一个定时器来产生周期性的中断,而且最好还让用户程序不能随意访问它的寄存器,以维持操作系统“心跳”的节律。
Cortex-M3处理器内部包含了一个简单的定时器。因为所有的CM3芯片都带有这个定时器,软件在不同 CM3器件间的移植工作就得以化简。该定时器的时钟源可以是内部时钟(FCLK, CM3上的自由运行时钟),或者是外部时钟( CM3处理器上的STCLK信号)。有4个寄存器控制SysTick定时器。
SysTick 是一个24 位的倒计数定时器,当计到 0 时,将从 RELOAD 寄存器中自动重装载定时初值。只要不把它在SysTick 控制及状态寄存器中的使能位清除,就永不停息。
SysTick 的最大使命,就是定期地产生异常请求,作为系统的时基。 OS 都需要这种“滴答”来推动任务和时间的管理。如欲使能 SysTick 异常,则把 STCSR.TICKINT 置位。另外,如果把向量表重定位到了 SRAM 中,还需要为 SysTick 异常建立向量,提供其服务例程的入口地址。
异常与中断
当CM3开始响应一个中断时,会有三个动作:
入栈: 通过数据总线把xPSR, PC, LR, R12以及R3-R0由硬件自动压入适当的堆栈中。
如果当响应异常时,当前的代码正在使用PSP,则压入PSP,也就是使用进程堆栈;否则就压入MSP,使用主堆栈。一旦进入了服务例程,就将一直使用主堆栈。
取向量:通过指令总线从向量表中找出正确的异常向量,然后在服务程序的入口处预取指。即从向量表中找出对应的服务程序入口地址。入栈和取向量是同时进行的。
更新寄存器:在入栈和取向量操作完成之后,执行服务例程之前,还要更新一系列的寄存器:
SP:在入栈后会把堆栈指针(PSP或MSP)更新到新的位置。在执行服务例程时,将由MSP负责对堆栈的访问。即选择堆栈指针MSP/PSP,更新SP。
PSR: 更新IPSR位段(地处PSR的最低部分)的值为新响应的异常编号。
PC:在取向量完成后, PC将指向服务例程的入口地址。即更新PC。
LR: 在进入异常服务程序后,将自动更新LR的值为特殊的EXC_RETURN。在异常进入时由系统计算并赋给LR,并在异常返回时使用它。 即更新LR。
EXC_RETURN的二进制值除了最低4位外全为1,而其最低4位则有另外的含义。
在启动了中断返回序列后,下述的处理就将进行:
出栈:先前压入栈中的寄存器在这里恢复。内部的出栈顺序与入栈时的相对应,堆栈指针的值也改回先前的值。
更新NVIC寄存器:伴随着异常的返回,它的活动位也被硬件清除。对于外部中断,倘若中断输入再次被置为有效,悬起位也将再次置位,新一次的中断响应序列也可随之再次开始。
咬尾中断和晚到中断
咬尾中断:当处理器在响应某异常时,如果又发生其它异常,但它们优先级不够高,则被阻塞。我们可以先入栈,处理完IRQ1后(按道理,再出栈。再入栈处理IRQ2),直接处理IRQ2,完事了再出栈。看上去就好像IRQ2把IRQ1的尾巴给咬了一样。
晚到异常:当CM3对某异常的响应序列还处在早期:入栈的阶段,尚未执行其服务例程时,如果此时收到了高优先级异常的请求,则本次入栈就成了为高优先级中断所做的了——入栈后,将执行高优先级异常的服务例程。
比如,若在响应某低优先级异常#1的早期,检测到了高优先级异常#2,则只要#2没有太晚,就能以“晚到中断”的方式处理——在入栈完毕后执行ISR #2。在ISR #2执行完毕后,则以“咬尾中断”方式,来启动ISR #1的执行。
如果异常#2来得太晚,以至于已经执行了ISR #1的指令,则按普通的抢占处理,这会需要更多的处理器时间和额外32字节的堆栈空间。
上实时操作系统
要在 CM3 中创建可靠扛打的系统,必须两手抓,两手都要硬。典型情况下,一个真正健壮的 CM3
软件系统是要使用实时操作系统内核的,通常会符合如下的要求:
服务例程使用 MSP(在“非基级线程模式”中会讲到例外情况)
尽管异常服务例程使用 MSP,但是它们在形式上返回后,内容上却可以依然继续——而且此时还能使用 PSP,从而实现“可抢占的系统调用”,大幅提高实时性能
通过 SysTick,实时内核的代码每隔固定时间都被调用一次,运行在特权级水平上,负责任务的调度、任务时间管理以及其它系统例行维护
用户应用程序以线程的形式运行,使用 PSP,并且在用户级下运行
内核在执行关键部位的代码时,使用 MSP,并且在辅以 MPU 时, MSP 对应的堆栈只允许特权级访问
扩展小知识:
Cortex-M3 只使用 Thumb-2 指令集。CM3 并不支持所有的 Thumb-2 指令, ARMv7-M 的规格书只要求实现 Thumb-2 的一个子集。
Cortex-M3 处理器使用一个 3 级流水线。流水线的 3 个级分别是:取指,解码和执行。
如果在程序执行的从头到尾,都只给每个中断提供固定的中断服务例程(这也是目前单片机开发的绝大多数情况),则可以把向量表放到 ROM 中。在这种情况下不需要运行时重建向量表。然而,如果想让自己的设备能随机应变地对付各种复杂情况,就常常需要动态地改变中断服务例程,更新向量表就是必需的了。此时,向量表必须被转移到可读写存储器中(如内存)。在把向量表重定位之前,往往要把现有的向量表往新的位置复制一份。需要拷贝的向量主要是系统异常的服务例程,如各种 fault 的、 NMI 的以及 SVC 的等等。如果没有建立好这些向量就启用了新的向量表,就可能会在响应异常时把不可预料的地址取出,程序极有可能跑飞。当我们把所有必要的向量都填好后,就可以启用了新的向量表了。然后继续往里面加入新的中断向量。