HAL库大雷预警!STM32 HAL库CAN启动超时解决办法

引言

最近在一个AGV控制模块的驱动开发场景,需要测试封装的协议帧是否符合CANopen协议格式。主控平台使用STM32F103ZET6,驱动对象为同毅的IXL-II系列舵轮驱动器,使用ST官方提供的HAL库以及bxCAN控制器作为底层抽象基础进行CAN报文发送测试。整个测试前置过程严格按照标准流程进行:

配置引脚 -> 配置bxCAN及过滤器初始化参数 -> 执行Init初始化 -> 执行Start启动bxCAN

然而同样的初始化流程,这次却卡在了HAL_CAN_Start()里,返回HAL_TIMEOUT。通过反复检查,所有相关配置完全正确无误。如果您也有如下问题:

  • HAL_CAN_Init()初始化成功,但是后续启动失败。
  • 经检查所有相关配置参数均无误。
  • 使用HAL库作为底层抽象。

那么这篇文章很大概率能够帮助到你。

现象复现

先声明一下开发环境: STM32F103ZET6. 外部8MHz晶振走PLL倍频至72MHz主频. 使用CAN1实例.

以上是相关GPIO引脚配置。bxCAN参数配置由于篇幅所限此处仅给出配置说明:

  • 波特率 :500 kbps(CAN_BAUDRATE_500K
  • 模式 :环回模式(MODE_LOOPBACK
  • SJW :1 Tq(Cus_CAN_SJW_1Tq
  • 自动重发 :关闭(is_AutoRestransmission = false
  • 自动唤醒 :关闭(is_AutoWakeUP = false
  • 过滤器 :全通模式(Cus_CAN_FILTERMODE_IDMASK,掩码/ID 均设为 0x00)

通过调试可以看到HAL_CAN_Init()返回正确状态HAL_OK

此时寄存器: MCR = 0x00010011. MSR = 0x00000409. 记住这两个寄存器值,后续将会作为我们排故的最佳凭证。

流程来到 HAL_CAN_Start()。执行CLEAR_BIT,软件清除MCR寄存器的INRQ位,使bxCAN设备进入正常工作模式。

流程继续往下,while ((hcan->Instance->MSR & CAN_MSR_INAK) != 0U)检测超时,置TIMEOUT状态,返回HAL_ERROR! bxCAN控制器启动失败。

以上便是整个错误流程的复现。有趣的是,该问题可稳定复现,且在该芯片之前跑的CAN例程中从未出现过该情况。值得注意:整个测试过程中 CAN 总线接口(PA11/PA12)处于完全悬空状态,未连接任何收发器或终端电阻,引脚仅靠GPIO内部上拉。这意味着所有观测到的现象完全来自芯片内部行为,排除了外部总线电平干扰的可能性。

为了排除是因为工程原因导致的潜在问题,我使用 STM32CubeMX 重新生成了一个最小测试工程。这个工程仅包含 CAN 外设初始化和发送测试,完全不使用任何自定义封装库,全部代码由 CubeMX 生成。结果现象依然同上:初始化超时。如下图所示,HAL_CAN_Start()返回HAL_ERROR,进入while(1)死循环。

哪怕是 ST 官方 CubeMX 生成的标准工程,HAL_CAN_Start() 依然无法成功启动。

问题剖析及解决方案

回顾我们之前成功Init之后记录的MSR,MCR寄存器的值:

MCR = 0x00010011. MSR = 0x00000409.

结合参考手册的寄存器映射图

(CAN_MCR寄存器位0)

(CAN_MSR寄存器位0 3 10)

根据参考手册,对照成功Init之后的MCR/MSR寄存器的值,可以看到上图中4个关键位均被置起。当流程来到HAL_CAN_Start()时,该API内部通过CLEAR_BIT将INRQ位置0,使CAN控制器从初始化模式进入正常模式,当成功进入正常收发工作模式后,硬件会自动清除MSR中的INAK位。HAL_CAN_Start()内部通过在规定的CAN_TIMEOUT_VALUE时间窗口内检测该位是否被硬件清0来判断CAN设备是否启动成功。我们之所以会启动失败,是因为INAK位并没有被硬件清0!

回顾硬件清空INAK的规则:

当CAN退出初始化模式时硬件对该位清'0' (需要跟CAN总线同步)。这里跟CAN总线同步是指, 硬件需要在CAN的RX引脚上检测到连续的11位隐性位。

在CAN总线中,当RX引脚上连续接收到11个高电平(隐性电平时)即可视为总线空闲状态。同时,先前我们已经提到我们的环境并没有接入CAN总线,而是使用回环模式,并且PA11/PA12悬空通过内部上拉。也就是说,我们的配置理应满足条件:根据参考手册,SAMP 位(Bit 10)记录的是最近一次 RX 引脚的实际采样电平。当前该位为 1,说明总线实际处于高电平(隐性)状态。但为何硬件依然拒绝清零INAK

  • CAN_TIMEOUT_VALUE 超时过短?这是一个可能的方向,当超时窗口过短时,INAK可能来不及改变HAL内部就已经超时了。HAL库中该宏被定义为 10(ms),我将其修改为了50ms的时间窗口,但是依旧超时,因此排除该情况。
  • 引脚电平不稳?我将PA11(CAN_RX)接入示波器,观察到PA11电平被稳定上拉至3.3v附近,因此也排除该情况。

至此常规排查手段全部失效,在后续的深入排查中,发现每次复位后、进入 main 函数时,MSR 的值均为 0x0000 0C0A。但查阅手册可以看到 MSR 寄存器的复位值为 0x0000 0C02,两者差异正好是 WKUI 位(Bit 3)

也就是说,WKUI 在芯片复位后的某一时刻被硬件置位了,而 HAL 库的 HAL_CAN_Start() 并未对该标志做任何处理 !该标志一旦置位,硬件会认为当前总线正处于"唤醒处理"或"活动状态",从而跳过或忽略对 11 个连续隐性位的空闲检测流程 ,导致 INAK 无法清零,最终导致HAL的状态机超时。

至于 WKUI 为何会在复位后被置位,目前原因暂不明。我自己的推测是:芯片复位后,PA11 在时钟使能瞬间产生了一次电平跳变(如浮空状态下的上电抖动),被处于睡眠模式的 bxCAN 误判为帧起始(SOF),从而锁存了 WKUI。但该推测无法解释"为何每次复位都能稳定复现",参考手册中并未描述此类行为,网络上也很难找到相关的文献记载。

目前可以确定的事实是:

  • MSR复位值异常,WKUI被硬件稳定置起。
  • HAL_CAN_Start()因为INAK无法被清除而超时。

至于 WKUI 被置位的深层原因,本文不作定论。但问题的现象、定位过程和解决方案均已明确,以下给出可直接应用的补丁代码。

解决方案

解决方法很简单,绕过HAL的状态机,自己实现一个HAL_CAN_Start()方法。

js 复制代码
void CAN_forceStart( void )
{
  // 先清除 WKUI
  if ( CAN1->MSR & CAN_MSR_WKUI ) 
  {
    CAN1->MSR = CAN_MSR_WKUI;
  }

  // 确保不在睡眠模式
  CAN1->MCR &= ~CAN_MCR_SLEEP;

  // 请求退出初始化(清除 INRQ)
  CAN1->MCR &= ~CAN_MCR_INRQ;

  // 等待 INAK 清零,但给一个短超时(5000 次循环)
  uint32_t timeout = 5000;
  while ( (CAN1->MSR & CAN_MSR_INAK) && timeout ) 
  {
    timeout--;
  }

  // 如果超时了(INAK 仍为 1),发送一帧空数据来强制唤醒
  if ( CAN1->MSR & CAN_MSR_INAK ) 
  {
    // 等待邮箱空闲
    while ((CAN1->TSR & CAN_TSR_TME0) == 0);

    // 发送一个"虚拟帧"(DLC=0,ID=0x7,不影响总线)
    CAN1->sTxMailBox[0].TIR = (0x7 << 21) | CAN_TI0R_TXRQ;
    CAN1->sTxMailBox[0].TDTR = 0;            // DLC=0,无数据
    CAN1->sTxMailBox[0].TIR |= CAN_TI0R_TXRQ;

    // 再次等待 INAK 清零(这次应该很快)
    timeout = 1000;
    while ((CAN1->MSR & CAN_MSR_INAK) && timeout) 
    {
      timeout--;
    }
  }
}

该补丁应在HAL_CAN_Init()成功初始化并返回之后,在HAL_CAN_Start()之前被调用。补丁内部原理与HAL的实现是类似的,但是HAL库在启动时是假设硬件是完全干净的,没有考虑WKUI位被异常置起的情况

(启动补丁调用示例)

我们先手动清除WKUI位,再将INRQ位清0,请求进入正常收发模式。但是在实际测试中,就算手动将WKUI位清0,硬件依然不会清除MSR中的INAK位,如下图所示

可以看到短超时过后,MSR最低位的INAK依然为置起状态。对于这种情况,我们需要发送一帧dummy报文来强行启动设备。如下图所示:提交报文请求后,可以看到INAK被清0,MSR寄存器也进入了正常工作状态。

发一帧测试报文,MSR 归零------本质是强制外设完成一次完整的收发状态机循环,把残留的异常状态冲刷掉 。这实际上利用了bxCAN 的一个特性:发送请求(TXRQ)会强制控制器完成状态转移 。当硬件处于初始化模式(INAK=1)时,写入 TXRQ 会迫使它立即退出初始化、进入发送流程,同时自动将 INAK 清零。这也是为什么补丁能用一帧 dummy 报文把外设从卡死状态里拉出来。这里需要提到的是:尽管补丁过后外设已经能够正常收发工作,但是后续的HAL_CAN_Start()调用是必须的,因为补丁只管把外设从卡死状态拉出来,后续HAL抽象层的状态机同步仍然需要HAL的专用API驱动

验证

打上补丁后,逻辑分析仪成功捕获到了两帧报文:强制启动时用于冲刷异常状态的dummy报文,以及NMT命令测试报文。至此,该问题算是初步得到解决。

希望这篇文章能帮你省下一些排查时间。如果对内容有任何疑问或建议,欢迎留言交流。

相关推荐
大辉狼_音频架构2 小时前
Vol. NXP SOF Arch
嵌入式
用户805533698032 天前
主线 U-Boot 上 RK3506:和闭源 rkbin 拔河的三个隐性契约
linux·嵌入式
荣--5 天前
在 strip 二进制 + 基址随机化的栈里做崩溃去重 —— 三阶段算法与一行 Crash Flag
嵌入式·崩溃分析·栈指纹·去重算法
释然小师弟5 天前
Android开发十年:反思与回顾
android·后端·嵌入式
FreakStudio6 天前
W55MH32L-EVB 上手测评:硬件 TCP/IP 加持的以太网单片机,MicroPython 零门槛开发
python·单片机·嵌入式·大学生·面向对象·并行计算·电子diy·电子计算机
bush411 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
国产化创客11 天前
ESP32 CameraWebServer 原生摄像头项目全解析
物联网·开源·嵌入式·实时音视频·智能硬件
goldenrolan11 天前
学习型红外控制系统稳定性挂测工装专项总结
软件测试·python·stm32·嵌入式·红外
w4ysonch11 天前
我手搓了一套适用于任何嵌入式项目的跨线程通信API
嵌入式