本文由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都会受到影响,修改时需要仔细检查,做好记录或备份。