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 确实在发帧,但没人应答。
"那一定是物理层问题了。"
开始排查:
- 120Ω 终端电阻 → 有
- 收发器供电 5V → 有
- 换一个收发器 → 试了,无效
- 修改供电方式 → 试了,无效
用 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 - 1、TimeSeg1 - 1、TimeSeg2 - 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 代码本身更有价值。

浙公网安备 33010602011771号