RTT小师弟 · 2021年09月10日

RT-Thread STM32系列CAN发送卡死问题与根本解决

本文由RT-Thread论坛用户@DIODEX原创发布:https://club.rt-thread.org/ask/article/3034.html

STM32 CAN发送卡死问题与根本解决(RTT底层自身问题)

1 Bug导致的现象

  • 问题1 RTT 4.0.2 CAN2没有连接CAN设备(或连接的设备未上电)时,一旦CAN2启动发送,RTT即卡死(此Bug官方在4.0.3修复了)
  • 问题2 RTT 4.0.2、4.0.3中,CAN正在发送的过程中,如果CAN线硬件出现松动,或CANH CANL出现临时短路,则线程会卡死在CAN发送的 rt_device_write

2 分析与解决

2.1 CAN2没连接设备时CAN发送会卡死

  此问题出现在RTT4.0.3之前的版本中。
  drv_can.c中的 CAN2_SCE_IRQHandler() 中,case RT_CAN_BUS_ACK_ERR 中的if中将drv_can2写成了drv_can1,改正之后即可解决CAN2在发送遇到设备无应答时出现程序卡死的问题。
  原因分析:这里的CAN2_SCE_IRQHandler函数为发送出错的处理中断函数,当CAN2没有连接CAN设备(或连接的设备未上电)时,如果CAN2发送了报文,则一段时间后就会进入这个错误处理函数,而这里的if写成了can1,导致错误没有正常处理,导致程序卡死。

2.2 CAN发送过程硬件不稳或CANH-CANL短路后程序卡死

  此问题出现在RTT4.0.3以及之前的版本中。笔者在4.0.2和4.0.3中均进行过测试。

2.2.1 修改方法

  类似的卡死问题,有些资料给出的解决方案是打开CAN初始化时的AutoRetransmission功能,这个方法看似也能解决问题,但其实是治标不治本。在2.2.2 问题分析中进行详细原因说明。

  在drv_can.c中找到CAN1_TX_IRQHandler,该函数主要部分为if-elseif-elseif,我们需要在最后一个elseif结束后添加else,如下代码中的倒数第4行:

/**
 * @brief This function handles CAN1 TX interrupts. transmit fifo0/1/2 is empty can trigger this interrupt
 */
void CAN1_TX_IRQHandler(void)
{
    rt_interrupt_enter();
    CAN_HandleTypeDef *hcan;
    hcan = &drv_can1.CanHandle;
    if (__HAL_CAN_GET_FLAG(hcan, CAN_FLAG_RQCP0))
    {
        if (__HAL_CAN_GET_FLAG(hcan, CAN_FLAG_TXOK0))
        {
            rt_hw_can_isr(&drv_can1.device, RT_CAN_EVENT_TX_DONE | 0 << 8);
        }
        else
        {
            rt_hw_can_isr(&drv_can1.device, RT_CAN_EVENT_TX_FAIL | 0 << 8);
        }
        /* Write 0 to Clear transmission status flag RQCPx */
        SET_BIT(hcan->Instance->TSR, CAN_TSR_RQCP0);
    }
    else if (__HAL_CAN_GET_FLAG(hcan, CAN_FLAG_RQCP1))
    {
        if (__HAL_CAN_GET_FLAG(hcan, CAN_FLAG_TXOK1))
        {
            rt_hw_can_isr(&drv_can1.device, RT_CAN_EVENT_TX_DONE | 1 << 8);
        }
        else
        {
            rt_hw_can_isr(&drv_can1.device, RT_CAN_EVENT_TX_FAIL | 1 << 8);
        }
        /* Write 0 to Clear transmission status flag RQCPx */
        SET_BIT(hcan->Instance->TSR, CAN_TSR_RQCP1);
    }
    else if (__HAL_CAN_GET_FLAG(hcan, CAN_FLAG_RQCP2))
    {
        if (__HAL_CAN_GET_FLAG(hcan, CAN_FLAG_TXOK2))
        {
            rt_hw_can_isr(&drv_can1.device, RT_CAN_EVENT_TX_DONE | 2 << 8);
        }
        else
        {
            rt_hw_can_isr(&drv_can1.device, RT_CAN_EVENT_TX_FAIL | 2 << 8);
        }
        /* Write 0 to Clear transmission status flag RQCPx */
        SET_BIT(hcan->Instance->TSR, CAN_TSR_RQCP2);
    }
    else
    {
      rt_hw_can_isr(&drv_can1.device, RT_CAN_EVENT_TX_FAIL | 0 << 8);
    }
    rt_interrupt_leave();
}

  同理,需要在CAN2_TX_IRQHandler中的最后添加else,注意是drv_can2

    else
    {
      rt_hw_can_isr(&drv_can2.device, RT_CAN_EVENT_TX_FAIL | 0 << 8);
    }

  接着,需要在_can_sendmsg中注释掉Change CAN state对应的程序,如下程序中的#if(0)---#endif

    /*check select mailbox  is empty */
    switch (1 << box_num)
    {
    case CAN_TX_MAILBOX0:
        if (HAL_IS_BIT_SET(hcan->Instance->TSR, CAN_TSR_TME0) != SET)
        {
#if (0)
            /* Change CAN state */
            hcan->State = HAL_CAN_STATE_ERROR;
#endif
            /* Return function status */
            return -RT_ERROR;
        }
        break;
    case CAN_TX_MAILBOX1:
        if (HAL_IS_BIT_SET(hcan->Instance->TSR, CAN_TSR_TME1) != SET)
        {
            // HERO_EDIT_BEGIN
#if (0)
            /* Change CAN state */
            hcan->State = HAL_CAN_STATE_ERROR;
#endif
            // HERO_EDIT_END
            /* Return function status */
            return -RT_ERROR;
        }
        break;
    case CAN_TX_MAILBOX2:
        if (HAL_IS_BIT_SET(hcan->Instance->TSR, CAN_TSR_TME2) != SET)
        {
#if (0)
            /* Change CAN state */
            hcan->State = HAL_CAN_STATE_ERROR;
#endif
            /* Return function status */
            return -RT_ERROR;
        }
        break;
    default:
        RT_ASSERT(0);
        break;
    }

  完成以上修改之后就已经解决CAN卡死的问题了。

2.2.2 程序分析

  当CAN发送时遇到CAN线被短接的情况或者接触不良的情况,则stm32会产生发送完成中断,但相关标志位均为0,这个情况在STM32的手册里也没有描述,但是测试发现确实存在,下文称这种情况为没有标志位的中断。这个现象是导致CAN短接或接触不良时程序卡死的根本原因。
  如果初始化CAN时没有打开CAN外设的发送失败自动重发功能(RTT初始化CAN时默认不会打开自动重发),则CAN线的硬件连接恢复后STM32也不会再次产生发送完成中断,即这次发送已经失败了,并且出现了一次没有标志位的发送完成中断,此后也不会再有这次发送的发送完成中断了。
  RTT的STM32CAN发送函数rt_device_write(...)被调用时,启动发送之后,线程会挂起在一个completion信号量上,这个信号量将在CAN发送完成中断服务函数里释放,但中断服务函数中写的释放信号量是有条件的,条件是查到对应的标志位为1时释放对应的信号量。则如果出现了没有标志位的中断,该次中断中程序是不会释放completion信号量的,而由于CAN发送线程现在已经挂起,也不会发起下一轮发送,不再次启动发送则STM32不会再次产生发送完成中断,只有中断中释放信号量才能让CAN发送线程继续发送,而只有CAN发送线程再次正常启动发送时才能进入中断并释放信号量。这就陷入了死循环,导致发送CAN的线程被永远挂在了这个信号量上。
  尝试解决这个问题的第一步就是在发送完成中断中,程序进行标志位判断的地方加个else,遇到没有标志位的中断时也得发信号量,并且返回RT_CAN_EVENT_TX_FAIL,后续的0<<8表示的是失败事件是由第一个CAN发送邮箱产生的。此时虽然无法通过标志位来判断产生发送完成中断的邮箱,但是由于RTT的CAN驱动实际只使用了第一个发送邮箱,所以只要中断就一定是第一个发送邮箱产生的。
  这样可以解决发CAN线程被永远挂起在completion的问题,但是改了之后发现CAN还是会卡死。再次debug发现,当出现没有标志位的中断时,由于我们刚才改的程序会发送一个RT_CAN_EVENT_TX_FAIL标志,而原有的CAN驱动会在接收到这个标志的时候将HAL的CANstate置位为ERROR,这会导致以后都是ERROR了,后续发送之前CAN驱动会检查CANstate是否为ERROR,如果是ERROR就不会发送。所以把这个置位ERROR的代码注释掉就好了,大量实测证明这样做不会导致其它问题。
  完成以上修改后,CAN驱动效果非常好,接触不良和can线临时短路等情况都不会造成程序卡住。

2.3 其它方法与分析

2.3.1 自动重发相关

  一些文档中通过修改CAN初始化代码以开启AutoRetransmission功能,也可以从表面上解决这个问题,因为如果失败之后自动重发,则重新启动发送且发送成功后,总会产生正常的中断,此时就能释放信号量,发送线程就不再被挂起在completion信号量中了。但是这样的修改方式有缺点,即当CAN线硬件异常导致有一段时间不能正常发送时,rt_device_write函数将一直处于挂起在信号量上的状态,直到上次要发送的数据被成功发送,且由于实际发送次数>调用发送函数的次数,发出的CAN报文没有ACK时容易出现信号量溢出的问题。
  采用自动重发的解决方法,则只要调用rt_device_write函数,函数返回时这帧数据就一定已经正常发送了,如果发送不正常的话CAN外设就会自动重发。这种情况会导致一帧发送失败后会一直等待CAN线恢复后再发,会导致CAN线硬件异常时调用rt_device_write函数的用户线程被挂起较长时间,且这段时间内有可能出现信号量溢出的问题,导致程序卡死。
  如果按照本文介绍的方法修改底层,想实现类似发送失败重发的功能,可以自己通过软件实现,每次rt_device_write之后检查返回值,如果发送不成功,则重新发送当前数据。

2.4 修改底层注意事项

  本文修改的drv_can.c文件为rtt底层\library中的通用文件,如果多个bsp共用该library文件夹,则修改后多个bsp都会受到影响,修改时需要仔细检查,做好记录或备份。

推荐阅读
关注数
8071
内容数
181
小而美的物联网操作系统,经过14年的累积发展,RT-Thread 已经拥有一个国内最大的嵌入式开源社区,同时被广泛应用于能源、车载、医疗、消费电子等多个行业,累积装机量超过4亿台,成为国人自主开发、国内最成熟稳定和装机量最大的开源 RTOS。
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息