常被误解的开、关总中断话题

描述

我们在使用ARM Cortex-M内核芯片进行产品开发时,有时可能需要暂时开辟一个相对清静、不被打扰的程序执行环境,以确保某些操作可靠顺利完成。这时我们往往会使用所谓开、关总中断的指令代码来完成。我们可以对特殊功能寄存器PRIMASK写1来关闭/屏蔽优先级不高于0【数字大于0】的所有可配置中断的中断响应。对其写0,放弃关闭/屏蔽功能,即所谓的开总中断。

关于使用PRIMASK寄存器关闭/屏蔽所有可配置中断的做法还有其它等效操作,比如使用CPSID指令和CPSIE指令或调用相关CMSIS函数。【如下图所示,左边是汇编指令,右边是对应CMSIS函数】

中断

不过,在实际应用中很多人对这这个关、开总中断操作有些误解,尤其对关总中断误解更甚。以为关总中断就在NVIC那里关闭了所有中断响应允许,或者说连外设端的中断请求的使能也关闭了,中断请求事件标志也无效了,其实并不是这样的。若出于误解而使用开、关总中断指令或函数往往就得不到自己想要的结果、或者结果让人困惑不解。

下面基于Cortex -M4内核的STM32L4芯片做些验证。这里我们拿STM32L476开发板验证下相关内容【具体使用哪款STM32芯片不重要】。

中断

我这里开启片内TIM1/TIM2/TIM3/TIM4四个定时器的更新事件中断,其中TIM1/TIM2/TIM3的时间参数完全一样,而TIM4跟前三者相比,除了溢出时间参数【就是ARR】稍微小一点外,其它配置一样,四者同时启动,并确保让TIM4一定先进入中断服务程序[即ISR]。

它们的中断优先级设置各不相同,TIM4的最低。现在假设TIM4中断服务程序里要执行一段特别重要的事情,通过代码设计保证CPU刚进入TIM4中断服务程序时另外3个定时器一定还没有触发更新事件。为了便于测试,我也确保启动程序后每个TIMER都只产生一次更新事件。他们的优先级配置如下图所示【此处优先级高4位全部用作抢占优先级,子优先级都一样,不做配置】:

中断

上面各中断优先级的数字是基于优先级寄存器的高4位而言的,显然TIM4的优先级最低【数字越大优先级越低】。我在CPU进入TIM4 ISR时立即调用所谓的关闭总中断函数,然后一段时间后才调用所谓的打开总中断的函数,确保TIM4 ISR执行完毕之前另外3个TIMER的更新中断请求都产生了。

中断

我在每个中断ISR里执行一条输提示,基于上面的测试代码我们看看输出结果【我借助于RTC把执行各中断的时间点也输出了】:

中断

尽管在TIM4中断里一开始就调用了disable_irq()函数,可是,它并没有关闭TIM1/TIM2/TIM3的中断请求和后续响应。从上图不难看出,TIM4/TIM1/TIM2/TIM3依次得到响应执行。怎么感觉那个关闭总中断的函数啥也没关掉呢?这也正是被很多人误会的地方。

其实,这里调用disable_irq()函数的目的就是关闭/屏蔽所有优先级不高于0的可配置中断的响应,以保障当前TIM4 ISR的顺畅执行。这就相当于将当前TIM4 的中断优先级从4提升到0了。因此,让原本优先级高于TIM4的TIM1/TIM2/TIM3中断优先级反而低于当前TIM4ISR的执行优先级,即使在TIM4 ISR过程中发生了TIM1/TIM2/TIM3中断请求也没法得到响应。

而且,调用disable_irq()函数并不对被关闭/屏蔽中断的原有参数和配置做任何改变。什么意思呢?那些暂时被屏蔽中断的中断响应允许位、中断请求使能位以及触发事件标志等都不会因暂时被屏蔽而发生改变。同样,enable_irq()函数也不改变之前被屏蔽中断的原有参数和配置,它只是放弃刚才的屏蔽功能【或说优先级提升功能】。所以,当执行enable_irq()函数后,那些一度被屏蔽/关闭的中断请求都会按照之前各自优先级而被CPU响应执行。

我们可以在TIM4 ISR的适当位置后打上断点【断点处4个TIMER的更新事件一定都产生了】看看各个中断的响应情况:【详见下图】

中断

图中E、P、A是下方Eable/Pending/Active单词的首字母。Enable表示中断请求是否在NVIC端得到响应允许;Pending表示中断请求等待CPU的响应执行;Active表示中断服务程序正在被执行。

显然,只有TIM4服务程序在被执行中,其它3个都是挂起状态,等待被响应。也就是说disable_irq()函数并不直接影响别的中断请求的产生和响应挂起状态,它通过提升当前执行程序的优先级变相地实现了屏蔽/关闭其它优先级不高于0的中断请求的响应,即这个关闭是从执行效果上来说的,相当于变相提升了中断响应门槛;enable_irq()函数相当于将提升的门槛拿走恢复原貌。

如果其它参数都不变,我注释掉TIM4 ISR里的关闭总中断的代码,看看运行结果会怎么样呢?【见下图中TIM4 ISR 代码和输出结果】

中断

虽然代码保障了TIM4最先进入中断服务程序,但由于其它几个更高优先级的更新事件随即产生而发生抢占,反而TIM4最后完成中断执行。我们可以观察上面输出顺序及记录的时间点,请特别留意TIM3 ISR与 TIM4 ISR的输出时间点是相同的,因分辨率问题时间太接近没区分开来。不过这反而可以更清晰地看出TIM4被抢占了,TIM3一执行完马上回到TIM4的输出【这个结果跟TIM4的被抢占时间点密切相关】。

如果说,我在上面配置和代码基础上,进入TIM4 ISR后依然先关闭总中断,在打开总中断前对TIM1/TIM2/TIM3的几个更新事件标志做清零,结果会怎么样呢?【相信很多人想看看这个结果,不妨先心里猜测下。】

中断

基于上面调整后的代码,编译运行。不论我怎么反复点击运行,上图右边的输出结果纹丝不动?这个结果是正确、合理的吗?符合你心里预期否?

我这里清除的只是外设端的中断请求事件标志,怎么早已生效的中断请求就消失了呢?结合前面的分析,在TIM4 ISR里运行延时程序就是为了确保另外的TIM1/TIM2/TIM3的中断请求得以生效,请求生效后并会在NVIC的中断响应挂起寄存器的相应位置1,等待执行。

这是怎么回事呢?原理上说不通啊?

其实,我们看到的只是表象。真相是TIM1/TIM2/TIM3的ISR都得到执行了,为什么没看到TIM1/TIM2/TIM3 ISR运行的结果输出呢?这跟我们ISR代码处理有关。

首先可以肯定,这里做中断请求事件标志清零时,它们3个中断请求早就生效并处于响应挂起状态。对事件标志清零也不会影响到NVIC的中断挂起位的。当TIM4 ISR执行完毕后,TIM1/TIM2/TIM3照样基于优先级高低相继运行各自ISR。但有个问题,就是各自的ISR代码里都会检测更新事件标志,由于该标志在TIM4 ISR早就被做了清除而成为无效标志,所以在它们3个各自ISR里因检测到事件标志无效就都没有继续往下运行而提前退场,说直接点就是没有运行到结果输出代码就返回了。

既然这样,我不妨将输出结果的代码放在各自ISR的入口处,免掉检查标志位这个环节,然后我们继续看看结果。【结果输出在各自更新中断回调函数里完成。修改后的中断代码如下图所示:】

中断

基于上面的代码调整,再看看结果输出:

中断

这个输出结果就跟我们分析和预期的一致。通过这个实验表明,对于在NVIC端生效处于响应挂起的中断请求,只是清除相应的事件标志是不会影响它的后续执行的。如果我在上面TIM4 ISR里面执行开启总中断操作前希望处于挂起状态的TIM1/TIM2/TIM3中断请求不要再被执行了,那要如何操作呢?

首先,清除请求事件标志是应该的,而且还要清除它们各自的NVIC端的中断响应挂起位。我们可以通过调用__NVIC_ClearPendingIRQ(IRQn)函数对特定中断响应挂起位清零。

中断

对TIM4ISR代码再稍加调整【见上图】,运行后就只能看到TIM4 ISR的输出结果了。其它3个中断请求半路被TIM4 ISR给清除了,即使再开总中断也于事无补。

关于被误解的开、关总中断的话题,拉拉扯扯不知不觉也聊了这么多。所谓关总中断,实质上将当前执行程序的优先级提升到0,变相提高了期望打断当前执行程序的中断事件的响应门槛,开总中断就是取消对当前执行程序的优先级提升,恢复原貌。这里顺便贴出几个常用的有关中断响应的CMSIS函数供参考备忘。

中断

今天的分享就到这里,愿本文的分享能带给您一些收获。祝君好运。下次再聊。

哦,最后补充下,文中的串口终端输出结果,我是通过CPU直接向UART外设的数据寄存器轮询式写数据实现的。本公众号文字力求不拖泥带水,特意提醒这句给有心人。

审核编辑:汤梓红

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分