Cortex内核支持中断嵌套,所谓中断嵌套就是高优先级的中断可以打断低优先级的中断转而去执行高优先级的中断服务程序,当高优先级中断服务程序执行完毕再去接着执行低优先级的中断服务程序。若在高优先级中断服务程序执行过程中产生了低优先级中断,那么低优先级中断需要等高优先级中断服务程序执行完毕才能去执行。对于后者两个中断是串行执行的,如果按照每个中断备份、恢复寄存器的过程,那么高优先级中断先将上述8个寄存器压入栈中,等执行完毕再从栈中弹出这8个寄存器,紧接着低优先级中断再将这8个寄存器压入栈中,等执行完毕再从栈中弹出,这么做对这8个寄存器重复入栈出栈2次,浪费了时间,cortex内核采用咬尾中断机制避免这种问题发生,来看下图:
图3 咬尾中断切换过程对比
所谓咬尾中断是指高优先级中断服务程序在执行过程中发生了低优先级中断,那么在高优先级中断服务程序执行完毕后直接去执行低优先级中断服务程序,低优先级中断服务程序执行完毕后才将高优先级中断压入堆栈的8个寄存器数据弹出,这中间就减少了高优先级中断出栈以及低优先级入栈的过程。
另外还会有一种情况,低优先级中断先发生,而在低优先级中断压栈的过程中又发生了高优先级的中断,这时高优先级中断就会抢占低优先级中断,如果高优先级中断再压栈然后执行,再出栈,低优先级中断执行再出栈,这样对这8个寄存器又是重复入栈出栈2次,做了无用功,cortex内核采用晚到中断机制避免这种问题发生,来看下图:
图4 晚到中断切换过程对比
所谓晚到中断是指在低优先级中断压栈的过程中又发生了高优先级的中断,那么这个压栈过程就算是这个高优先级中断的压栈,压栈之后执行高优先级中断。
另外还有一种情况,在低优先级中断服务程序执行过程中发生了高优先级中断,高优先级中断会抢占低优先级中断,但此时低优先级中断已经执行了,已经使用寄存器了,至于使用了哪些寄存器就无法得知了,因此在这种情况下,为了使高优先级中断服务程序不破坏低优先级中断保存在这8个寄存器中的数据还需要由高优先级中断将这8个寄存器再次备份一次(其它寄存器由C编译器备份)。
图5 高优先级中断抢占正在运行的低优先级中断
XPSR寄存器最低9个bits保存的是当前的中断号,比如说tick中断的中断号是15,那么在tick中断服务程序运行时XPSR的最低9个bits的值就是15,如果程序没有进入中断那么该值为0。因此在图3、4、5所表述的中断中,在开始进入中断时XPSR寄存器最低9bits为0,中间中断运行、切换过程中XPSR寄存器最低9bits为当前中断的中断号,直到出中断时XPSR最低9bits才恢复为0。
图6 XPSR寄存器
基础知识已经介绍的差不多了,下面开始进行原因分析。
原因分析
在前面“现象描述”中已经说过,这个小型嵌入式操作系统在ARM7内核的ADUC7024芯片上运行正常,在cortex内核的LM3S8962芯片上也运行正常,但在同为cortex内核的STM32F103VB芯片上就出问题了,程序运行一段时间就跑飞,死到异常中断服务程序HardFault_Handler里面,这说明触发了硬件异常。LM3S8962芯片和STM32F103VB芯片使用的是相同的芯片内核,最大不同之处在于芯片的外设,而这个小型嵌入式操作系统只使用了一个串口外设,这个串口的配置也看不出什么异常。另外的不同之处在于这两种芯片所采用的驱动库函数不同,由于库函数写的都比较高深莫测,还要结合硬件芯片资料对照着看,工作量比较大,所以不从库函数作为入口点定位此问题,直接从出现问题的HardFault_Handler函数出发定位此问题。
Cortex内核在进入异常服务程序HardFault_Handler之前一定是有一条指令触发了异常,硬件会将这条指令的地址存入LR寄存器中,这其中的细节请参考我写的另一篇文档“教你如何找到导致程序跑飞的指令”,这里就不详细介绍了。通过HardFault_Handler函数找到触发异常的指令在触发软中断服务程序的MDS_TaskSwiSched函数里,但在这个函数里也看不出有问题的指令,将断点打在这条“触发异常”的指令上,发现也不是每次都会引发异常,查看用到的相关寄存器也没有发现异常,这说明没有问题的指令+没有问题的数据在某些时候会概率性触发异常,这个问题就比较难解决了。找了一段时间也没找到问题原因,因为出现异常的地方几乎都是与中断相关的,后来就排查到了中断配置,一看中断配置,立刻豁然开朗,原因找到了。
原来STM32F103VB芯片的驱动库函数在设置tick中断时将tick中断的优先级配置为15,而其它中断优先级则默认为0,在cortex内核中0优先级高于15,这就说明tick中断的优先级是最低的。按照操作系统的实现方法,任务调度中断也就是tick中断必须是最低优先级的,因为操作系统在tick中断中实现任务上下文切换,如果tick中断优先级不是最低的话,那么它就可能会打断其它中断,这样tick中断的上下文切换就不是在任务与任务之间进行了,而是可能会在中断与任务之间进行了,这样中断执行到一半就切换出去了,中断就不会结束了,程序运行的结果必然会出问题。从这点来说,STM32F103VB芯片驱动库函数将tick中断优先级设置为最低并没有什么不妥。
但,cortex内核还提供了一个PendSV中断,这个中断是可以被延迟执行的软中断,将PendSV中断优先级设置为最低,操作系统就可以将定时产生的tick中断任务调度和由软件随机发起的任务调度都统一到PendSV中断服务程序里去完成。但STM32F103VB芯片驱动库中设置tick中断的函数将tick中断的优先级设置为最低,这样在tick中断触发PendSV中断时,PendSV中断就会打断tick中断的执行,发生任务调度,如果另有一个任务B也处于ready态,并且优先级比任务A还要高,那么在任务调度时就会将任务A的寄存器数值保存到任务A的栈中,又从任务B的栈中取回任务B的寄存器数值,返回到任务B去执行。问题就出现在这里,任务备份寄存器时都处于非中断状态,也就是说任务备份XPSR时XPSR最低的9bits一定是0,所以任务B栈中保存的XPSR的最低9bits一定是0,PendSV中断执行任务调度返回的XPSR是任务B保存的XPSR,最低9bits是0,而此时却是PendSV中断执行完毕需要返回到tick中断的情况,相当于图5中高优先级中断返回到低优先级中断的情况,这就要求XPSR最低9bits一定不是0,这样由于XPSR数值的冲突就出现了异常,程序进入了HardFault_Handler异常中断服务程序,导致了本文所描述的异常。
如果切换前的任务A是处于ready态的最高优先级任务,那么这次任务调度相当于是把任务A的寄存器中的数值(XPSR最低9bits不是0,因为是在中断中备份的)备份到任务栈中,然后又从任务栈中取回了刚刚备份的寄存器数值,相当于没有任务切换,就不会发生异常。因此这种异常情况是随机出现的,只有在不同任务切换时才会发生。
ARM7内核的ADUC7024芯片之所以没有出现这个问题是因为ARM7内核不支持中断嵌套。同为cortex内核的LM3S8962芯片之所以没有出现这个问题是因为TI的驱动库没有更改中断优先级,所有的中断优先级都为0,保证了PendSV中断是最低优先级的这一限制,因此没有出错。
定位问题时我并没有意识到发生了中断嵌套,因此在出错指令的地方查看XPSR最低9bits是0便认为是正确的,而其它指令和数据本来就没有问题也就无法发现它们的问题了,所以定位这个问题时就觉得很奇怪,没有任何异常就会触发了一个异常中断,还是概率性的,而且在ARM7内核和同为cortex内核的LM3S8962芯片上却不出现。
解决方法
将PendSV中断优先级设置为最低即可。
经验总结
出现这次异常的原因很简单,是中断优先级错误导致的。为什么会出现中断优先级错误?是因为没有对所使用的中断设置中断优先级,而是采用了默认的中断优先级,而在调用驱动库tick初始化函数时库函数却修改了tick中断的优先级。
由此得出的结论是:
1.最好对所使用的部分做初始化操作,不要使用默认值。
2.在可以发生中断嵌套的芯片上,要保证操作系统任务调度中断是最低优先级的。
另外说一点,感觉ST3.50的库比较奇怪,有些库函数的配置参数并没有以函数参数的形式提供给用户使用,而是需要用户直接修改库函数代码中的参数才能实现,比如说我使用串口功能,串口库函数直接将串口的波特率设置成了115200,如果我想使用9600的波特率就必须修改库函数的代码,这样的话这些函数就不是真正意义上的库了,而是需要与用户代码绑定在一起使用的。 |