引言
最近在一个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命令测试报文。至此,该问题算是初步得到解决。
希望这篇文章能帮你省下一些排查时间。如果对内容有任何疑问或建议,欢迎留言交流。