摘要:操作系统实际上我们接触的很多,比如说windows,安卓、IOS、linux都是一种操作系统。单片机也有它自己的操作系统,叫做实时操作系统。那么这种实时操作系统和我们用的这些系统有什么区别呢?
我们经常使用的这些实际上是非实时的操作系统。为什么说它是非实时的,因为它的内核实际上是对任务进行时间片轮转的调度方式。比如说有3个任务,分别是任务A,任务B和任务C。那么在时间片轮转的调度机制里,它会让任务A运行一断时间,然后切换到任务B,然后切换到任务C,这样子不断的轮转。
两个任务间通过 Systick 轮转调度的简单模式
那么这样有一个什么缺点呢?如果有一台自动驾驶的汽车里面任务C,是用来检测障碍物和躲避障碍物的,如果任务C不能得到及时的执行的话,有可能这一台自动驾驶的汽车就会撞到障碍物上,实际上这样是非常危险。所以我们就出现了实时的操作系统,它支持抢占式调度机制,也就是说我们可以把任务C的优先级提高。这样当任务C就绪的时候,就先运行任务C,就保证了任务C的实时性。在操作系统中,最基础的功能就是实现任务调度。
接下来了解一下FreeRTOS,实时操作系统的任务调度。在了解实时操作系统之前,要先了解一下内核,这里用ARM Cortex‐M3内核作为模板。首先我们先来了解一下CPU寄存器,这个是CM3的CPU寄存器的表。CM3 拥有通用寄存器 R0‐R15 以及一些特殊功能寄存器。R0‐R12 是最“通用目的”的,但是绝大多数的 16 位指令只能使用 R0‐R7(低组寄存器),而 32 位的Thumb‐2指令则可以访问所有通用寄存器。特殊功能寄存器有预定义的功能,而且必须通过专用的指令来访问。
Cortex‐M3 的寄存器组
可以看得到,前面这里都是通用寄存器。它们分为有低位寄存器(所有指令都能访问它们)和高位寄存器(只有很少的 16 位 Thumb 指令能访问它们)。那么它们为什么要这样分呢?实际上在ARM内核的早期版本,ARM指令和Thumb指令可以访问的寄存器不一样,所以就分有低位寄存器和高位寄存器。还有后面的R13、R14和R15分别是栈指针、连接寄存器和PC程序指针寄存器。
除此之外CM3还有一些特寄存器。
大家有没有想过当CPU进入中断的时候,实际上是相当于打断了之前的任务。那么在执行完中断之后,CPU又是如何返回到原来的任务?而保证原来的任务不丢失的呢?
在进入中断之前,也就是在左半部分,我们先把CPU寄存器里面的值送入内存中,也称为压栈。然后再运行中断服务函数,在运行中断服务函数的时候,CPU寄存器会被改写。但是这并没有什么关系,因为当中断结束之后,返回到原来的任务的时候,之前CPU寄存器的值就会被从内存中取出,也叫做弹栈。那么通过这样一个机制,就保证了原来的进程的数据不丢失。
那么接下来我们来了解一下CM3的压栈顺序?
入栈顺序以及入栈后堆栈中的内容 第3列所示
上图是Cortex-M3进入中断时,硬件的压栈顺序。也就是说在它进入中断的时候,硬件会自动把这几个寄存器压栈。分别是PC指针、xPSR特殊寄存器、R0到R3通用寄存器、R12通用寄存器,还有LR连接寄存器(保存函数的返回地址)会被压入栈中。按照下面第三列的标号顺序保存到内存中。
那么在压入栈成功之后,当中断执行完成,返回到原来的进程中时,栈里面的内容就会被弹出到CPU寄存器中它的弹出顺序和压入顺序刚好是相反的。也就是说先弹出LR,然后这样依次往下这样子弹出,因为栈是先进后出,所以它是这样一个出栈顺序。
前面我们知道CPU一共有R0-R15以及几个特殊的寄存器。在中断函数到来时上面几个寄存器是硬件自动压入栈中的,那么还有几个是软件压入栈中的,这又如何理解?
举个例子:
程序在执行
if(a<=b) a=b;
时候,突然来了中断。任何程序,最终都会转换为机器码,上述C代码可以转换为右边的汇编指令。
对于这4条指令,它们可能随时被异常打断,怎么保证异常处理完后,被打断的程序还能正确运行?
这4条指令涉及R0、R1寄存器,程序被打断时、恢复运行时,R0、R1要保持不变,执行完第3条指令时,比较结果保存在程序状态寄存器PSR里,程序被打断时、恢复运行时,程序状态寄存器保持不变。这4条指令,读取a、b内存,程序被打断时、恢复运行时,a、b内存保持不变。内存保持不变,这很容易实现,程序不越界就可以。所以,关键在于R0、R1、程序状态寄存器要保持不变(当然不止这些寄存器):
在处理异常前,把这些寄存器保存在栈中,这称为保存现场,也就是压栈。
在处理完异常后,从栈中恢复这些寄存器,这称为恢复现场,也就是弹栈。
再举一个例子:
void A() { B(); }
比如函数A调用函数B,函数A应该知道:R0-R3是用来传参数给函数B的;函数B可以肆意修改R0-R3;函数A不要指望函数B帮你保存R0-R3;保存R0-R3,是函数A的事情;对于LR、PSR也是同样的道理,保存它们是函数A的责任。由硬件帮我们完成。
对于函数B:我用到R4-R11中的某一个,我都会在函数入口保存、在函数返回前恢复,从内存中弹栈到CPU的寄存器中;保证在B函数调用前后,函数A看到的R4-R11保存不变。
假设函数B就是异常/中断处理函数,函数B本身能保证R4-R11不变,那么保存现场时,硬件只需要保存R0-R3,R12,LR,PSR和PC这8个寄存器。
那么接下来我们来了解一下CM3的两种特殊中断机制。当CM3开始响应一个中断时,会在它看不见的体内奔涌起三股暗流:
入栈:把8个寄存器的值压入栈。
取向量:从向量表中找出对应的服务程序入口地址。
选择堆栈指针MSP/PSP,更新堆栈指针SP,更新连接寄存器LR,更新程序计数器PC。
第一种叫做咬尾中断
我们知道,在进入中断的时候需要执行入栈,而退出中断的时候需要执行出栈。那么当两个中断来临的时候,像这样在第一个中断执行完成之后,要执行第二个中断。在CM3 处理器内核中是不会再执行出栈和入栈的。也就是说这里节省了出栈和入栈的时间,实际上相当于第2个中断把第一个中断的尾巴咬掉。也就是没有让它再出栈,所以这就被称为咬尾中断。
第二种中断机制叫做晚到中断
晚到中断就是说,当有一个高优限级的任务来临时,之前低优先级的任务取向量还没有完成的时候(之前低优先级的任务还没有从向量表中找出对应的服务程序入口地址),那么这一次压栈就是为高优先级任务做的。也就是说就算高优先级的中断晚到了,它仍然可以用低优先级中断压入的栈。
CM3 处理器内核中断表
在实时操作系统中,经常用到的是这三个中断 PendSV、Systick、SVC。
那么在FreeRTOS中Systick这个中断是用来提供实时操作系统的时钟周期的。而PendSV这个是可悬挂中断,是用来切换进程的。SVC在FreeRTOS中只用了一次,也就是启动第一个进程的时候用到了它。
__asm void vPortSVCHandler( void ) { /* *INDENT-OFF* */ PRESERVE8 ldr r3, = pxCurrentTCB //取出当前的任务控制块 ldr r1, [ r3 ] //使用 pxCurrentTCBConst 获取 pxCurrentTCB 地址 ldr r0, [ r1 ] //pxCurrentTCB 中的第一项是栈顶任务 ldmia r0 !, { r4 - r11 } //手动将R4-R11,R14寄存器压栈 msr psp, r0 //恢复任务栈指针 isb mov r0, # 0 msr basepri, r0 //打开所有的中断 orr r14, # 0xd bx r14 /* *INDENT-ON* */ }
系统异常清单
那么有些人可能就会问了,为什么我不直接在Systick中切换任务呢?而是要在PendSV中切换任务呢?那我们就可以来看一下:
发生 IRQ 时上下文切换的问题
如果在Systick中断到来时,前面有一个中断正在执行,也就是这里的IRQ正在执行。那它就会被打断,然后Systick执行上下文来切换,这时候切换到任务b,它要等待一断时间直到下一次上下文切换,切换回原来IRQ这个中断执行的内容。这样中断才能被执行完成,但是这样我们可以看得到,中断被严重的耽误了,所以这样做实际上是不方便。而且容易出错的。
这时候它们就想出一种办法,说我在Systick中我判断这个时候有没有中断在执行,如果有那么我们就不切换,如果没有我们就切换。这样呢实际上也会造成一个问题,就是如果这个中断函数的中断时间和Systick差不多,比如说如果这是一个定时器中断,这是Systick系统时钟中断。它们的中断周期都是1毫秒,那么它们经常就会面临着两个同时到来的情况。这样就有可能导致进程迟迟无法切换,导致了延误的产生,所以这样做也不是很好。
所以就出现了PendSV可悬挂中断
使用PendSV控制上下文切换
在这种中断中有什么好处呢?我们可以看得到,在Systick中它只将PendSV的中断位挂起,也就是说,它不执行经常切换的这个操作。而是等到后面,当所有的中断执行完成之后在PendSV中执行上下文切换,这样既保证了任务的及时切换,也保证了中断的及时执行。PendSV异常会自动延迟上下文切换的请求,直到其它的ISR都完成了处理后才放行。为实现这个机制,需要把PendSV编程为最低优先级的异常。如果 OS 检测到某 IRQ正在活动并且被Systick抢占,它将悬起一个PendSV异常,以便缓期执行上下文切换。
那么在PendSV中到底是怎么样进行进程切换?在这里用的是汇编语言写的。
__asm void xPortPendSVHandler( void ) { extern uxCriticalNesting; extern pxCurrentTCB; extern vTaskSwitchContext; PRESERVE8 mrs r0, psp//将当前进程栈指针保存在R0寄存器中 isb ldr r3, =pxCurrentTCB //取出当前的任务控制块 ldr r2, [ r3 ] //将任务控制块地址保存在R2寄存器中 stmdb r0 !, { r4 - r11 } //手动将R4-R11,R14寄存器压栈 str r0, [ r2 ] //将当前的栈顶地址写入控制块 stmdb sp !, { r3, r14 } mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY//将这个宏所代表的立即数写入R0寄存器,而这个宏是用户想要屏蔽的最高优先级中断 msr basepri, r0 //将刚刚R0寄存器的值写入特殊寄存器basepriority中,这个寄存器可以对中断进行细腻的控制它可以将高于这个优先级的中断不屏蔽,而低于这个优先级的中断屏蔽 dsb isb bl vTaskSwitchContext mov r0, #0 msr basepri, r0 //取消中断屏蔽 ldmia sp !, { r3, r14 } //将当前的栈指针从R3寄存器中恢复,这个时候R3寄存器存的值是刚刚从下一任务控制块取 ldr r1, [ r3 ] ldr r0, [ r1 ] //将新任务的栈顶保存到R0寄存器中 ldmia r0 !, { r4 - r11 } //手动将R4-R11以及R14寄存器弹栈 msr psp, r0 isb bx r14 //异常返回,返回后硬件将自动恢复其余寄存器,并且使用进程栈指针。 nop /* *INDENT-ON* */ }
那么我们刚刚已经了解到了,FreeRTOS实时操作系统的最基本的功能任务切换。但是如果想做一个完善的实时操作系统,还需要非常多的其它的东西,比如说列表和列表项、任务通知、低功耗模式任务控制块及对堆栈处理内存管理、空闲任务、对信号量、软件定时器、事件标志组等等这些内容。
看看程序中具体是怎么实现中断的
下面这张表来自《ARM Cortex-M3权威指南》
在Cortex-M3中有15个异常中断,对应在stm32中如下图
在启动文件中不仅有异常,还有中断,其实中断也是属于一种异常。我们说中断的时候,更多的说的是某一种设备发出的信号比如GPIO模块:发信号给CPU比如12C控制器发送完数据,发出信号给CPU比如UART接收到一个数据之后也会产生中断注意了:中断属于异常。除了中断外其他异常一般有哪些呢:复位:也是一种异常,发生了各种错误:属于异常。
当我们板子复位的时候CPU会执行中断向量表中的Reset_Handler执行这个函数。
当我们板子看门狗中断时的时候CPU会执行中断向量表中的WWDG_IRQHandler执行这个函数。
你肯定有这样一个疑问?CPU怎么知道跳转到中断向量表中的执行哪一个函数呢?
这肯定是硬件确定,因为这时候软件还没有开始执行,硬件确定当前发生的是哪一个异常,哪一个中断,当恢复的时候由软件触发、硬件恢复。
/** * @brief This function handles NMI exception. * @param None * @retval None */ void NMI_Handler(void) { } /** * @brief This function handles Hard Fault exception. * @param None * @retval None */ void HardFault_Handler(void) { /* Go to infinite loop when Hard Fault exception occurs */ while (1) { } } /** * @brief This function handles Memory Manage exception. * @param None * @retval None */ void MemManage_Handler(void) { /* Go to infinite loop when Memory Manage exception occurs */ while (1) { } } /** * @brief This function handles Bus Fault exception. * @param None * @retval None */ void BusFault_Handler(void) { /* Go to infinite loop when Bus Fault exception occurs */ while (1) { } } /** * @brief This function handles Usage Fault exception. * @param None * @retval None */ void UsageFault_Handler(void) { /* Go to infinite loop when Usage Fault exception occurs */ while (1) { } } /** * @brief This function handles SVCall exception. * @param None * @retval None */ void SVC_Handler(void) { } /** * @brief This function handles Debug Monitor exception. * @param None * @retval None */ void DebugMon_Handler(void) { } /** * @brief This function handles PendSVC exception. * @param None * @retval None */ void PendSV_Handler(void) { } /** * @brief This function handles SysTick Handler. * @param None * @retval None */ void SysTick_Handler(void) { }
好了,现在你知道MCU的中断流程和RTOS的的基本原理了吧?
审核编辑:刘清
评论
查看更多