CAN 位时序与 500kbps 参数推导:HAL 里看不到 0x001A0005,但它就在那

发布时间:2026/10/28 19:01:11
CAN 位时序与 500kbps 参数推导:HAL 里看不到 0x001A0005,但它就在那

做车载 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)补偿总线传播延迟11TimeSeg1 = CAN_BS1_11TQ
相位段2(TS2)吸收时钟误差2TimeSeg2 = 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:05预分频 6
TS1[3:0]19:1610段长 11 tq
TS2[2:0]22:201段长 2 tq
SJW[1:0]25:2401 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Ω 左右。

只接一端会出现什么?偶发丢帧,然后随着错误计数上升变成完全收不到。