做车载 ECU 诊断工具链,第一步绕不过去的是把 CAN 波特率配准。我用的是STM32F407ZGT6最小系统板, 它的CAN_BTR寄存器有0x001A0005这个值,要把它从 42MHz 推导出来,才敢说"会 CAN"。
一、先确认你的时钟到底是多少
CAN 挂在 APB1 上。STM32F407 在 168MHz 主频下,APB1 分频是 4,所以:
PCLK1 = 168MHz / 4 = 42MHz这一步必须实测,不能凭记忆。时钟配置必须对,根据具体型号的板子去配置,不然会浪费很多时间在调试上。
在 main() 里加这一行,先把它打印出来:
//HAL 替你算,42MHz 板上就是 42000000 uint32_t pclk1 = HAL_RCC_GetPCLK1Freq(); printf("PCLK1=%lu\r\n", (unsigned long)pclk1);看到PCLK1=42000000再往下走。不是这个数,后面每个参数都是错的。
二、一个 CAN 位由几段组成
一个比特时间被切成若干"时间份额"(tq),由四段组成:
| 段 | 作用 | 本项目取值 | 对应 HAL 字段/宏 |
|---|---|---|---|
| 同步段 SYNC_SEG | 固定 1 tq,用于硬同步 | 1 | 固定,无需配置 |
| 传播段 + 相位段1(合称 TS1) | 补偿总线传播延迟 | 11 | TimeSeg1 = CAN_BS1_11TQ |
| 相位段2(TS2) | 吸收时钟误差 | 2 | TimeSeg2 = CAN_BS2_2TQ |
| 同步跳转宽度 SJW | 重同步允许的最大调整量 | 1(不算在14TQ内) | SyncJumpWidth = CAN_SJW_1TQ |
| 合计 | 14TQ |
于是:
一个位的时间 = 14 tq × BRP预分频三、把 84 拆成 14 × 6
CAN 位速率 500 kbps,意味着一位占 1/500000 =0.000002秒:
42,000,000 / 500,000 = 84 个 PCLK1 时钟周期也就是说:一个 CAN 位要吃掉 84 个 APB1 时钟。而一位 = 14 tq,所以每个 tq = 6 个时钟:
84 = 14 × 6这 6 就是预分频值(prescaler),写进寄存器时要减 1:BRP 字段 = 5。
四、写进 BTR 的四个字段
CAN_BTR的位定义(RM0090 §32.9.5):
| 字段 | 位 | 值 | 含义 |
| BRP[9:0] | 9:0 | 5 | 预分频 6 |
| TS1[3:0] | 19:16 | 10 | 段长 11 tq |
| TS2[2:0] | 22:20 | 1 | 段长 2 tq |
| SJW[1:0] | 25:24 | 0 | 1 tq |
拼起来:
BRP=5 → 0b0000000101 TS1=10 → 0b1010 << 16 = 0x000A0000 TS2=1 → 0b001 << 20 = 0x00100000 SJW=0 0x000A0000 | 0x00100000 | 0x5 = 0x001A0005这就是0x001A0005的全部来历。不是魔法数字,是三个字段位移拼出来的。
HAL 等价写法:hcan2.Init.Prescaler = 6; TimeSeg1 = CAN_BS1_11TQ; TimeSeg2 = CAN_BS2_2TQ; SyncJumpWidth = CAN_SJW_1TQ;—— HAL 会在HAL_CAN_Init()里自动完成上面所有位移与拼装,把0x001A0005写进CAN_BTR。你在.c里搜不到这个数,是因为拼装发生在 HAL 内部(stm32f4xx_hal_can.c的CAN_Init())。想看实际值,在调试器里展开CAN2 → BTR,或打印CAN2->BTR。
五、采样点为什么是 85.7%
采样点 = 在一位的第几个 tq 去读电平:
(1 + 11) / 14 = 12/14 = 85.7%CAN 行业惯例是把采样点放在 75%~87.5%。偏后是为了抵消总线传播延迟和节点间时钟偏差;但也不能太靠后,否则留给相位段2的余量不足,时钟稍微偏一点就采错。
我的取值是 12/14 = 85.7%,落在惯用区间靠后的位置。
六、代码
我的can.c里实际写法:
/* 位时序:42MHz / 500k = 84 = (1 + 11 + 2) × 6 采样点 = 12/14 = 85.7% */ #define CAN_PRESCALER_500K 6 #define CAN_PRESCALER_250K 12 #define CAN_PRESCALER_125K 24 #define CAN_BS1_TQ CAN_BS1_11TQ #define CAN_BS2_TQ CAN_BS2_2TQ #define CAN_SJW_TQ CAN_SJW_1TQ void CAN2_Init(uint8_t baud_sel, uint8_t loopback) { uint32_t presc; switch (baud_sel) { case CAN_250K: presc = CAN_PRESCALER_250K; break; /* 12 */ case CAN_125K: presc = CAN_PRESCALER_125K; break; /* 24 */ default: presc = CAN_PRESCALER_500K; break; /* 6 */ } hcan2.Instance = CAN2; hcan2.Init.Prescaler = presc; /* → BTR.BRP = presc-1 */ hcan2.Init.Mode = loopback ? CAN_MODE_LOOPBACK : CAN_MODE_NORMAL; hcan2.Init.SyncJumpWidth = CAN_SJW_TQ; /* → BTR.SJW = 0 */ hcan2.Init.TimeSeg1 = CAN_BS1_TQ; /* → BTR.TS1 = 10 */ hcan2.Init.TimeSeg2 = CAN_BS2_TQ; /* → BTR.TS2 = 1 */ hcan2.Init.AutoBusOff = ENABLE; /* → MCR.ABOM = 1 */ hcan2.Init.AutoWakeUp = DISABLE; hcan2.Init.AutoRetransmission = ENABLE; hcan2.Init.ReceiveFifoLocked = DISABLE; hcan2.Init.TransmitFifoPriority = DISABLE; HAL_CAN_Init(&hcan2); /* 内部自动拼出 0x001A0005 写进 CAN_BTR */ HAL_CAN_Start(&hcan2); printf("PCLK1=%lu\r\n", (unsigned long)HAL_RCC_GetPCLK1Freq()); }HAL 视角:上面 4 个Init字段最终被HAL_CAN_Init()拼成CAN_BTR = 0x001A0005。想亲眼看到,在HAL_CAN_Init()之后打断点,Keil 的 Register 窗口展开CAN2 → BTR即可;或加一行printf("BTR=0x%08lX\r\n", (unsigned long)CAN2->BTR);。
这三个字段就是原来那三个寄存器:Prescaler→BTR.BRP,TimeSeg1/TimeSeg2→BTR.TS1/TS2,AutoBusOff→MCR.ABOM,Mode→MCR.INRQ。
HAL 在HAL_CAN_Init()里替你做了「进初始化 → 写 BTR → 退出初始化」这三步,所以你看不到while (INAK)的等待。
顺序不能反:寄存器版的坑(BTR只能在 INAK=1 时写)在 HAL 里被封装掉了,但改波特率必须重新调HAL_CAN_Init(),不能只改结构体——它不会自动生效。再次强调:BTR只有在初始化模式(INAK=1)下才能写,正常模式下写进去不生效而且不报错——这是最容易浪费一晚上的坑。
七、三个我踩过的坑
坑 1:只用 CAN2,也必须开 CAN1 的时钟。
STM32F407 有 CAN1 和 CAN2,CAN1 是主机,CAN2 是它的从机,CAN2 的滤波器组和共享 SRAM 都要通过 CAN1 管理。我的can.h里已用宏封装:
#define CAN_CAN1_CLK_ENABLE() do { __HAL_RCC_CAN1_CLK_ENABLE(); } while(0) #define CAN_CAN2_CLK_ENABLE() do { __HAL_RCC_CAN2_CLK_ENABLE(); } while(0)在HAL_CAN_MspInit()里两个都调用。不开 CAN1 时钟,一个报文都收不到,而且不报任何错。
坑 2:CAN2 的接收中断必须用 CAN2 自己的向量。
STM32F407 上 CAN1 和 CAN2 各有独立的中断向量:CAN1_RX0_IRQn(编号 20)、CAN2_RX0_IRQn(编号 64)。用 CAN2 就必须开CAN2_RX0_IRQn、写CAN2_RX0_IRQHandler。我的can.h里用宏做了一层封装:
#define CAN_CAN2_RX_IRQn CAN2_RX0_IRQn #define CAN_CAN2_RX_IRQHandler CAN2_RX0_IRQHandler写成CAN1_RX0_IRQHandler永远进不来——现象是"发得出去、一帧收不到"。
坑 3:两端各一个 120Ω,少一个就丢帧。
CAN 是差分总线,终端电阻用来消除信号反射。两个节点时,两个 120Ω 分别在最远的两端,并联后总线阻抗约 60Ω。我用万用表量过,断电状态量 CANH 和 CANL 之间就是 60Ω 左右。
只接一端会出现什么?偶发丢帧,然后随着错误计数上升变成完全收不到。