16 小时 CAN 调试的经验和教训

STM32G431 + SIT1042 + CubeMX + FDCAN。目标是让 ESC 通过 CAN 发遥测帧给 PCAN-View。
理论上一小时的事,实际用了 16 小时、12 次 CubeMX 重新生成、60+ 次 GDB 连线、无数次"这不可能"。


"CAN 就是一个串口,波特率对了线接对了就能通。"

这句话是我在下午 2 点说的。晚上 10 点我还在示波器前。

凌晨 3 点,我终于收到了 ID=0x02 的遥测帧。16 小时后,发现真正的问题只有两个——一个收发器引脚极性,一个波特率计算公式。剩下 14 个小时全在错误的岔路上。

这篇文章复盘整个过程,以及为什么一个看起来"很简单"的 CAN 调试会失控。更重要的是——以后如何避免?


时间线

13:00 — 一切开始

按照 SDN Motor Control SDK 的架构,搭建了 CAN 的 Comm_Driver_t 抽象层(类似 Sensor_Driver_t)。CubeMX 生成 FDCAN 初始化代码。FDCAN 是 STM32 的 CAN 外设,支持经典 CAN 2.0 和 CAN FD。

CubeMX 配置:

  • PB9 (TX) / PA11 (RX)
  • 经典 CAN 模式
  • 1Mbps

烧录 → PCAN-View 打开 → 什么都没收到。

正常的开始。开始排查。


13:30 — 第一个坑:CubeMX 生成顺序

进入 GDB,断在 CAN_Comm_Init,发现 FDCAN 寄存器全是垃圾值。

仔细一看 main.c

MX_MotorControl_Init();  // 这里会调用 MCboot → g_comm.Init() → CAN_Comm_Init()
MX_FDCAN1_Init();        // ← FDCAN 还没初始化!

CubeMX 把 FDCAN 初始化放在了 MotorControl 之后。MotorControl 的启动流程里就要调 CAN 初始化,但 FDCAN HAL 还没跑。

:交换顺序。


13:45 — 第二个坑:StdFiltersNbr

交换顺序后烧录,进入 Error_Handler()。单步追踪发现 HAL_FDCAN_ConfigFilter 返回错误。

CubeMX 生成的 StdFiltersNbr = 0。Filter 都没配,ConfigFilter 当然失败。

:在 CubeMX Filter Configuration 里把 Standard Filter Number 改成 1。


14:00 — 真正的噩梦开始

烧录。PCAN-View 依然没有帧。

但这次没有 Error_Handler——代码全跑完了。用 GDB 读到:

FDCAN PSR = 0x07E4   → LEC = 4 (Bit1 Error)
FDCAN ECR = 0x1F00F8 → TEC = 248

Bit1 Error 是"FDCAN 在总线上读到了和自己发出的不匹配的电平"。TEC=248 是连续失败了 248 次。

这意味着:FDCAN 在发帧,但发出去的东西不对。

"正常,"我想,"应该是波特率配置有问题。"


14:15-17:00 — TXBC 死循环(最严重的错误)

接下来三个小时陷入了 TXBC 寄存器配置的无底洞。

错误一:寄存器偏移搞错

STM32G431 FDCAN 的 TXBC (Tx Buffer Configuration) 寄存器偏移是 0x0C0,不是 0x034。但我一直以为它在 0x034。每次 GDB 读到 0,我就得出结论"寄存器写不进去"。

然后开始怀疑编译器优化——加 volatile、加 __asm volatile("dmb")、加内存屏障、加回读验证。

全部无效。因为我从头到尾读的就不是 TXBC。

错误二:把正常的默认值判为 BUG

即使最后读到了正确的 TXBC=0,我也认为"TX FIFO 没配置好"。实际上 TXBC=0(默认值)意味着1 元素 FIFO + 偏移 0——这是够用的。所有 HAL 驱动只设置了 TFQM(FIFO/Queue Mode),TBSA 和 TFQS 保留默认值——这是 ST 的 HAL 设计,不是 bug。

浪费 3 小时在解决一个不存在的问题上。

两个认知错误的叠加效应:

TXBC 读错地址 → 值是 0 → 怀疑写失败
→ 加了各种机制"强制写" → 仍然失败(因为读的还是错地址)
→ 自我强化:"这不可能,编译器一定有问题"
→ 越来越深的错误方向

17:00-18:00 — 对比例程

终于停下来,去看了 STM32 官方 CAN 例程。发现我们缺了 HAL_FDCAN_ConfigGlobalFilter。补上。

眼尖的同事可能会注意到例程跑的不是 1Mbps 而是 500kbps,但我们当时没吃透这一点。


18:00-21:00 — 示波器 + 收发器排查

拿出示波器看 CAN_H 波形。

发现一个周期性的错误帧脉冲——大概 13μs 一圈。FDCAN 确实在发帧,但没人应答。

"那一定是物理层问题了。"

开始排查:

  1. 120Ω 终端电阻 → 有
  2. 收发器供电 5V → 有
  3. 换一个收发器 → 试了,无效
  4. 修改供电方式 → 试了,无效

用 GDB 读取了 PSR 和 ECR 几十次,TEC 从 0 涨到 248 再涨到 Bus-Off 边缘。帧发出去,但被打回来。

就是这个时候我们仍然不知道 SIT1042 的 STB 脚极性。


21:00 — 突破

突然意识到,可能收发器根本没进入正常模式。翻 SIT1042 数据手册:

Pin 8 (STB): Standby mode selection
LOW = High-speed mode  
HIGH = Standby mode

LOW 才是正常工作模式。 和我们直觉相反。

检查 CubeMX 生成的代码:

// CubeMX 的错误顺序
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_11, GPIO_PIN_SET);  // 先写 HIGH
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);                // Init 重置到 LOW!

CubeMX 把 WritePin(SET) 放在 GPIO_Init 之前,然后 GPIO_Init 会清除输出状态。所以实际 STB 脚是 LOW——恰恰是正确状态。但我们后来为了"显式控制",在 Init 后又加了一次 WritePin(SET)把收发器关掉了。

修:WritePin(GPIOC, GPIO_PIN_11, GPIO_PIN_RESET) 在 Init 之后调用。


22:00 — 第二突破:波特率公式

STB 修好后,示波器上波形明显改善,但 PCAN-View 还是收不到帧。

"波段率对吧?"我们调到 500kbps。
CubeMX 设 Prescaler=34。

烧录。示波器显示 nice clean waveform。PCAN-View...还是没收到。

在 GDB 里读了一个从未读过的寄存器——NBTP(位时序配置寄存器):

NBTP = 0x00200601
NBRP = 32 (prescaler register)
NTSEG1 = 6
NTSEG2 = 1

等等。Prescaler=34 的话,按照 HAL 的公式:

HAL 写 NBRP = NominalPrescaler - 1 = 34 - 1 = 33
实际 tq = (NBRP + 1) / FDCAN_CLK = 34 / 170M = 200ns
Bit time = (1 + 7 + 2) × 200ns = 10 × 200ns = 2.0μs = 500kbps ✓

但如果设置 NominalPrescaler = 33:

HAL 写 NBRP = 33 - 1 = 32
tq = 33 / 170M = 194.1ns
Bit time = 1.941μs = 515.15kbps
误差 = 3%

CAN 协议对波特率误差的容忍度是 1-2%。3% 就是太多了。

我们之前调的 Prescaler=33 产生了 515kbps——和 PCAN-View 设的 500kbps 差了 3%,PCAN 解码失败!


22:15 — 终于通了

设置 Prescaler=34(500kbps 精确)。

烧录。打开 PCAN-View,配置 500kbps。

ID 0x02。帧频率 200Hz。角度、速度、电流、电压全部更新。

通了。16 小时。两个变量。


根因分析

追溯所有错误,最终能归到两个根本因素:

根因 1:缺乏清晰的调试策略

应该做的(慢而正确):

示波器 → 确认帧结构 → NBTP 寄存器 → 计算实际波特率 
→ 查 SIT1042 数据手册 → 确认 STB 极性和实际电平 
→ PCAN 波特率匹配 → 完成

实际做的(快而无效):

修改代码 → 怀疑 HAL → 手动写寄存器 → 怀疑编译器 
→ 修改代码 → 怀疑 HAL → 读错寄存器 → 怀疑芯片 
→ 修改代码 → 换收发器 → 最终看数据手册 → 发现是 STB 脚

根因 2:不了解 HAL 的内部行为

HAL 将 CubeMX UI 数值转换为硬件寄存器值的过程中有多处隐藏的减法(NominalPrescaler - 1TimeSeg1 - 1TimeSeg2 - 1)。不认识这层转换导致反复计算错误。

永远用 GDB 读实际的硬件寄存器值来仲裁。 硬件是你的终极真相来源,不是 HAL 代码,不是 CubeMX,不是你的推断。


如何系统性地避免这种事情

1. 读写实际寄存器(不要猜测)

任何配置问题,第一步总是读硬件寄存器

  • TXBC(0x400064C0)
  • NBTP(0x4000641C)
  • CCCR(0x40006418)
  • PSR(0x40006444)
  • ECR(0x40006440)

寄存器偏移从 CMSIS 头文件中找,不要靠记忆。

2. 示波器第一,软件第二

CAN 总线活动出问题时,示波器胜过 100 行 GDB 输出。看到错误帧 → 立刻进入物理层排查:终端电阻、收发器模式、供电。

3. 搞清楚工具链之间的差异

STM32CubeMX 生成代码 → make 编译 → stm32-for-vscode 烧录。每一步都是独立工具。

每次烧录后,在代码中加一个计数器(volatile static int i)并检查它是否递增——确认确实烧录的是刚编译的固件。

4. 小心 HAL 陷阱

特别是"配置寄存器"这种东西。例如,一个叫 Prescaler 的东西:

  • CubeMX UI 里叫 "Nominal Prescaler"
  • 代码里叫 hfdcan1.Init.NominalPrescaler
  • 硬件寄存器收到的值是 NominalPrescaler - 1

如果"什么东西名字像 P 但实际是 P-1"没搞清楚,永远都调不对。

5. 一次只改一个变量

改变收发器模式 + 改变波特率 + 改变终端电阻 + 改变 AutoRetransmission = 不知道哪个修好了。依次进行,验证每一步。


结语

CAN 通讯是成熟的技术。它也确实在 2 个变量调好后 10 分钟就能跑起来。但如果用错了调试策略、相信了错误的假设、在一个不相关的方向深挖——什么成熟技术都不够用。

这次调试暴露的不只是对 FDCAN 或 SIT1042 的不熟悉,更是调试方法论上的系统性弱点:没有先查数据手册、没有先读寄存器、没有一次只改一个变量。

修好这些习惯,比修好 CAN 代码本身更有价值。

posted @ 2026-07-27 03:15  BorisDimitri  阅读(21)  评论(0)    收藏  举报