问题描述

控制系统使用的是STM32F4+UCOSII 抢占型内核,最近一段时间出现了程序跑一段时间之后操作系统直接死掉的问题,表现为:操作系统中设有优先级很低的呼吸灯任务,只要操作系统在正常工作,呼吸灯就会不停的跳动,但是当出现问题时,呼吸灯停止跳动,控制底盘运动的任务也死掉,底盘处于失控状态,LCD所在的任务也死掉,不再进行刷新,推测为所有的操作系统的任务均死掉,不能正常工作,但是中断仍然可以响应,写在定时器中断中的急停操作是可以执行的。

问题分析

  • 怀疑是进入了一些错误中断

  • 怀疑是指针或者堆栈出了问题

  • 怀疑最高优先级的任务中存在死循环

  • 怀疑操作系统中存在一些bug

  • 怀疑是中断引起的程序一直在中断中,不能进入正常任务
    解决方案

  • 对错误中断进行排查分析,程序中开启的错误中断响应只有HardFault,在进入HardFault的中断服务函数后会进行相应的处理

      /* *
         * @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)
            {
                     BEEP_TOGGLE;
                     Delay_ms(500);
            }
     }

因此,从表现上看基本可以排除进入Hard Fault的可能,除此之外,Hard Fault的中断优先级为 -1,如果程序是死在HardFault中,那么其他的中断响应应该也是无法响应的,这与问题的表现,中断仍然可以响应矛盾,因此可以断定,不是进入了HardFault和其他的一些问题中断。

  • 关于指针和堆栈的问题,一般有问题会直接进Hard Fault,而此时基本已经排除了Hard Fault的问题,除此之外也尝试扩大了硬件和操作系统的堆栈,问题没有解决,确定不是这里有问题。

  • 关于操作系统的Bug在网上查了不少,早期的操作系统确实存在问题但是系统中使用的2.92版本应该是不存在此类Bug的。

总结

上述对问题的解释其实有点牵强,因为大多数时候发送和接收都是这样进行处理的,但是都没有出现问题,而且DMA的使用也使用了很长时间也没有出现过问题,并且就算出问题也是随机的,并不是每次都出问题,所以怀疑出现问题应该是跟各个中断之间的优先级、响应时间、以及具体的时序有一些复杂的关系,具体是什么关系就很难寻找了,推荐的做法就是防范于未然,将不用的中断统统关闭,这样总是没有坏处的。

后续:问题的解决

原因
造成该问题的原因是程序进入USART中断,但触发中断的标志位没有被清除,一直循环触发中断,程序卡在中断中出不来

当使用

 USART_ITConfig(USART1, USART_IT_RXNE, ENABLE)

使能串口接收中断时,实际上 USART_CR1 寄存器中 RXNEIE 被置 1
但是当RXNEIE = 1时 同时还启动了串口的 ORE 中断

ORE:上溢错误 (Overrun error)
在 RXNE = 1 的情况下,当移位寄存器中当前正在接收的字准备好传输到 RDR 寄存器时,该
位由硬件置 1。如果 USART_CR1 寄存器中 RXNEIE = 1,则会生成中断。该位由软件序列清
零(读入 USART_SR 寄存器,然后读入 USART_DR 寄存器)。
0:无上溢错误
1:检测到上溢错误
注意: 当该位置 1 时, RDR 寄存器的内容不会丢失,但移位寄存器会被覆盖。如果 EIE 位置 1,
则在进行多缓冲区通信时会对 ORE 标志生成一个中断。

当发生上溢错误时程序会响应串口中断进入到如下的中断函数中

void USART1_IRQHandler(void)
{
    u8 temp;
    if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
    {
        USART_ClearFlag(USART1, USART_FLAG_RXNE);       //USART_FLAG_RXNE    
        USART_ClearITPendingBit(USART1, USART_IT_RXNE);
         temp = USART_ReceiveData(USART1);
    }
}

但是由于此时触发的是ORE中断,中断函数中的内容并不能满足ORE状态标志位被清除的条件:

读入 USART_SR 寄存器,然后读入 USART_DR 寄存器

因此该标志位始终存在,无法被清除,串口中断一直被触发
程序卡死

解决方案
中断函数中加入如下内容

void USART1_IRQHandler(void)
{
    u8 temp;

    if(USART_GetITStatus(USART1, USART_IT_ORE_RX) != RESET)    //读入USART_SR 
    {
          USART_ReceiveData(USART1);       //读入USART_DR 
    }
    
    else if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
    {
        USART_ClearFlag(USART1, USART_FLAG_RXNE);       
        USART_ClearITPendingBit(USART1, USART_IT_RXNE);
         temp = USART_ReceiveData(USART1);
    }
}

其中

    if(USART_GetITStatus(USART1, USART_IT_ORE_RX) != RESET)    //读入USART_SR 
    {
          USART_ReceiveData(USART1);       //读入USART_DR 
    }

满足了ORE被清除的条件
读入 USART_SR 寄存器,然后读入 USART_DR 寄存器

特别注意
根据参考手册中显示
ORE:上溢错误 (Overrun error)
在 RXNE = 1 的情况下,当移位寄存器中当前正在接收的字准备好传输到 RDR 寄存器时,该
位由硬件置 1。如果 USART_CR1 寄存器中 RXNEIE = 1,则会生成中断。该位由软件序列清
零(读入 USART_SR 寄存器,然后读入 USART_DR 寄存器)。
0:无上溢错误
1:检测到上溢错误
注意: 当该位置 1 时, RDR 寄存器的内容不会丢失,但移位寄存器会被覆盖。如果 EIE 位置 1,
则在进行多缓冲区通信时会对 ORE 标志生成一个中断。

ORE上溢错误产生中断有两种方式,其中之一是由于普通模式下一个字节尚未处理完,又接收到一个字节。另外一种方式是在开启了错误中断标志位EIE时,在使用DMA进行数据传输时若发生上溢则会产生中断

EIE:错误中断使能 (Error interrupt enable)
对于多缓冲区通信( USART_CR3 寄存器中 DMAR = 1),如果发生帧错误、上溢错误或出
现噪声标志( USART_SR 寄存器中 FE = 1 或 ORE = 1 或 NF = 1),则需要使用错误中断
使能位来使能中断生成。
0:禁止中断
1:当 USART_CR3 寄存器中的 DMAR = 1 并且 USART_SR 寄存器中的 FE = 1 或 ORE = 1
或 NF = 1 时,将生成中断。

与此相对应的,库函数中的中断定义也有两种

/** @defgroup USART_Interrupt_definition 
  * @{
  */
  
#define USART_IT_PE                          ((uint16_t)0x0028)
#define USART_IT_TXE                         ((uint16_t)0x0727)
#define USART_IT_TC                          ((uint16_t)0x0626)
#define USART_IT_RXNE                        ((uint16_t)0x0525)
#define USART_IT_ORE_RX                      ((uint16_t)0x0325) /* In case interrupt is generated if the RXNEIE bit is set */
#define USART_IT_IDLE                        ((uint16_t)0x0424)
#define USART_IT_LBD                         ((uint16_t)0x0846)
#define USART_IT_CTS                         ((uint16_t)0x096A)
#define USART_IT_ERR                         ((uint16_t)0x0060)
#define USART_IT_ORE_ER                      ((uint16_t)0x0360) /* In case interrupt is generated if the EIE bit is set */
#define USART_IT_NE                          ((uint16_t)0x0260)
#define USART_IT_FE                          ((uint16_t)0x0160)

/** @defgroup USART_Legacy 
  * @{
  */
#define USART_IT_ORE                          USART_IT_ORE_ER

如果在进行中断标志位的查询时使用

if(USART_GetITStatus(USART1, USART_IT_ORE) != RESET)

其实只查询了打开EIE情况下的溢出中断标志位

#define USART_IT_ORE_ER                      ((uint16_t)0x0360) /* In case interrupt is generated if the EIE bit is set */

而如果没有打开EIE的话,当然是查不到RXNEIE

#define USART_IT_ORE_RX                      ((uint16_t)0x0325) /* In case interrupt is generated if the RXNEIE bit is set */

所对应的中断标志的
这也是有不少博客中说存在Bug,查询不到中断标志的原因。

因此如果是打开了EIE产生了溢出中断需要在中断服务函数中添加的是

if(USART_GetITStatus(USART1, USART_IT_ORE) != RESET)    //读入USART_SR 
{
      USART_ReceiveData(USART1);       //读入USART_DR 
}

或者

 if(USART_GetITStatus(USART1, USART_IT_ORE_ER ) != RESET)    //读入USART_SR 
    {
          USART_ReceiveData(USART1);       //读入USART_DR 
    }

而如果没有打开EIE,则需要添加的是

    if(USART_GetITStatus(USART1, USART_IT_ORE_RX) != RESET)    //读入USART_SR 
    {
          USART_ReceiveData(USART1);       //读入USART_DR 
    }