打印

破坏STM32中断机制引发的异常

[复制链接]
5205|16
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
ifreecoding|  楼主 | 2012-3-22 23:02 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 ifreecoding 于 2012-3-28 21:56 编辑

由于排版不好,直接上pdf附件 破坏STM32中断机制引发的异常.pdf (143.21 KB)

现象描述
我将一个具有实时任务切换功能的小型嵌入式操作系统内核成功的从具有ARM7内核的ADUC7024芯片移植到了具有cortex内核的LM3S8962芯片,然而在移植到同样具有cortex内核的STM32F103VB芯片上却出了问题,程序运行一段时间就跑飞,最终查明是任务切换过程破坏了cortex内核的中断机制所致,但为何同样采用了cortex内核的LM3S8962芯片却没有出现该问题?本文将向你讲述这其中的原因,同时你还可以了解到操作系统任务切换的基本原理以及cortex中断方面的一些知识。
背景知识介绍
在介绍问题定位过程前我们先了解一下实时嵌入式操作系统任务切换的基本原理。
对于嵌入式设备,程序一般存储在ROM中,有些芯片可以直接运行ROM中的指令,将变量中的数据存放在RAM中,有些芯片则可以将ROM中的指令搬移到RAM中执行,RAM中既存放指令又存放数据。不管哪种方式,程序执行过程中都需要使用芯片内核里的寄存器,这些寄存器用来存放程序运行过程中需要使用的数据,用来指示芯片的状态,这些寄存器距离芯片内核最近,速度最快,支持更多的寻址方式,因此程序执行过程大部分的操作都是使用这些寄存器完成的。
芯片通过寄存器执行程序空间的指令,不停的将临时数据从寄存器中存储到数据空间,将数据空间的临时数据恢复到寄存器中参与运算,这就是程序的运行过程。

1 芯片内部结构(忽略cache)

程序的执行只与指令和数据相关,指令是不可修改的,编译后就确定了,能改变的只有数据,但指令需要对数据进行判断,走不同的指令分支,这些寄存器中保存的数据就控制了程序执行的分支,因此可以说操作系统任务切换的过程就是将任务A的寄存器数据存储到数据空间,然后再从数据空间将任务B数据恢复到寄存器中的过程,这样操作系统就完成了从任务A切换到任务B的上下文切换,有关操作系统切换过程更详细的介绍可以参考我写的《底层工作者手册之嵌入式操作系统内核》,可以在我的博客blog.sina.com.cn/ifreecoding下载相关的文档、源代码和演示视频。



图2 任务切换过程

实时嵌入式操作系统会采用一个硬件定时器以固定的频率产生任务调度,这就是我们所说的tick中断。当tick中断到来时操作系统就会产生调度,进行上面所说的寄存器备份、恢复操作,完成任务切换。对于其它非tick中断是不需要进行任务切换的,但当这些中断发生时,芯片会从一般的程序跳转到中断服务程序去执行,其实这也相当于是一种“任务切换”,也需要备份、恢复这些寄存器,这种情况对于没有使用操作系统的程序也是存在的。这些寄存器的备份、恢复过程有些是硬件自动完成的,有些则是由C编译器在编译代码时自动生成的代码来完成的,因此,中断过程中寄存器的备份、恢复过程对于软件程序员来说是透明的,我们在编写程序时看不到这个过程,可以不用关心这一过程。

接下来我们再了解一下STM32的中断机制。
Cortex内核发生中断时,硬件会自动将XPSRPCLRR12R3R2R1R08个寄存器压入栈(详细介绍可以参考《底》2.1、2.3和5.1节),其余的R4~R11LRXPSR寄存器的备份则需要由C编译器去做。硬件为啥只将这8个寄存器入栈呢,因为AAPCS——Procedure Call Standard for the ARM Architecture对传递函数值所使用的寄存器做了规定,其中R0~R3R12是接口寄存器,父函数可以通过这5个寄存器向其调用的子函数传递函数参数,子函数可以直接使用这5个寄存器,而子函数若需要使用其它的寄存器则需要由子函数先备份,以免破坏父函数保存在这些寄存器中的数值,然后才能使用。C编译器需要遵守这个规定,以保证不同的编译器编译出的二进制代码是兼容的。中断的产生具有随机性,中断服务程序无法知道被其打断的函数在哪些寄存器中保存了有用的数值,因此为了不破坏寄存器中的数据,C编译器需要在中断服务程序里对所有的寄存器进行备份,然后才能使用这些寄存器。LR寄存器保存了子函数/中断服务程序的返回地址,因此也需要C编译器来备份。PC寄存器保存的是发生子函数/中断服务程序调用那一时刻的程序指针,我认为没有备份的必要,但备份下来会看的更直观些。XPSR寄存器中保存的是子函数/中断服务程序调用那一时刻的芯片状态,因此这个需要备份。
Cortex内核在中断发生时硬件会自动入栈上述8个寄存器,因此C编译器就不需要再做重复的工作了,下面我们对比一下ARM7内核与cortex内核C编译器在子函数/中断服务程序调用时需要对寄存器做的备份工作,其中ARM7内核在中断发生时硬件不会自动将寄存器压入堆栈的:

子函数

中断服务程序

ARM7

R0~R3R12可以直接使用。
若使用其它寄存器,需要先备份后使用。
所有寄存器都需要先备份后使用。
cortex

R0~R3R12可以直接使用。
若使用其它寄存器,需要先备份后使用。
R0~R3R12可以直接使用。
若使用其它寄存器,需要先备份后使用。

1 ARM7cortex内核寄存器备份对比

可以看出C编译器在cortex内核上实现了统一,无论是发生子函数调用还是中断服务程序调用均只需要做相同的工作就可以了,cortex内核中断自动压入上述8个寄存器的设计简化了C编译器的设计,并且可以提高程序的执行效率(必须备份的寄存器由硬件完成,不需要由软件花费指令周期来完成了)。
你是否有这样的疑问:在cortex内核中,LR寄存器已经在中断发生时由硬件备份了,为什么在中断里还需要由软件再备份一次?这是因为cortex内核会将中断的返回地址LR压入栈,同时会将一个特殊的值“EXC_RETURN”存入LR寄存器中,有关EXC_RETURN的知识这里就不介绍了,你可以参考《底》5.1节中的内容。

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的波特率就必须修改库函数的代码,这样的话这些函数就不是真正意义上的库了,而是需要与用户代码绑定在一起使用的。
沙发
gaochy1126| | 2012-3-23 10:07 | 只看该作者
即将学习stm32,不过楼主讲的太多,有些乱,不过感谢分享

使用特权

评论回复
板凳
香水城| | 2012-3-25 12:52 | 只看该作者
发裤子,有理论,有分析,很到位!

使用特权

评论回复
地板
hsbjb| | 2012-3-25 18:43 | 只看该作者
就是排版有点问题:L

使用特权

评论回复
5
baidudz| | 2012-3-25 18:47 | 只看该作者
图好像也没有显示出来

使用特权

评论回复
6
xblei| | 2012-3-26 09:16 | 只看该作者
路过瞧瞧。。

使用特权

评论回复
7
lixiang69| | 2012-8-18 23:10 | 只看该作者
写的极好

使用特权

评论回复
8
dfsa| | 2012-8-19 23:31 | 只看该作者
发裤子,有理论,有分析,很到位!
香水城 发表于 2012-3-25 12:52

的确是很好的**

使用特权

评论回复
9
qqatscau| | 2012-8-26 11:18 | 只看该作者
顶!!!

使用特权

评论回复
10
yybj| | 2012-8-26 15:49 | 只看该作者
总结的很好,多谢共享

使用特权

评论回复
11
秋天落叶| | 2012-8-26 15:53 | 只看该作者
LZ很有思想

使用特权

评论回复
12
zhbd| | 2013-8-8 15:06 | 只看该作者
好**

使用特权

评论回复
13
gonggu8181| | 2013-8-8 19:15 | 只看该作者
谢谢分享

使用特权

评论回复
14
hawksabre| | 2013-8-8 21:03 | 只看该作者
好**   好好看看   谢谢了   楼主   顶一个

使用特权

评论回复
15
ren0zhe| | 2013-9-11 08:54 | 只看该作者
好**,顶一个

使用特权

评论回复
16
1398117027| | 2014-8-30 18:55 | 只看该作者
**不错,就是图片挂了

使用特权

评论回复
17
STM32初学者| | 2014-8-30 20:33 | 只看该作者
路过瞧瞧。。

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

2

主题

68

帖子

3

粉丝