学习目标与边界
这篇笔记以 STM32F4 bxCAN + CAN 2.0B 为对象,覆盖智能座舱网关节点和测试台架中最常见的收发、滤波、波形与故障定位问题。bxCAN 是经典 CAN 控制器:单帧数据区最大 8 字节,不支持 CAN FD 的 64 字节载荷和可变速率数据相位。
先分清三层:控制器只认识帧和错误状态;收发器负责差分电平;线束、终端、电源和节点数量共同决定总线是否真的稳定。软件“发送成功”不等于线上一定有正确波形。
本文中的 HAL 示例用于说明接口边界和错误路径,引脚、CAN 收发器型号、PCLK1 频率、中断优先级及 FreeRTOS 配置必须以实际板卡为准。
物理层:先确认总线真的存在
显性、隐性与差分测量
高速 CAN 通过 CAN_H 与 CAN_L 的电压差传输逻辑值,默认空闲状态是隐性:
| 总线状态 | 逻辑值 | CAN_H 典型电平 | CAN_L 典型电平 | 差分电压 CAN_H - CAN_L |
|---|---|---|---|---|
| 隐性 Recessive | 1 | 约 2.5 V | 约 2.5 V | 约 0 V |
| 显性 Dominant | 0 | 约 3.5 V | 约 1.5 V | 约 2 V |
显性位会覆盖隐性位,因此 CAN 的仲裁无需破坏正在传输的报文:多个节点同时起发时,最先在 ID 位发送 0 的节点保留总线控制权。数值越小的 ID 优先级越高,这也是报文 ID 设计必须从系统层面统一规划的原因。

图:台架示波器波形。图中两路呈互补变化,可作为时序观察参考;要确认真实 CAN 物理层质量,应使用差分探头直接测量 CAN_H - CAN_L,不要只凭两路单端波形判断。
上电前的三项检查
- 终端电阻: 高速 CAN 主干两端各放一个
120 ohm。断电后在CAN_H与CAN_L间测得约60 ohm,通常说明两个终端都在;约120 ohm常表示只剩一个终端;远小于60 ohm则要排查重复终端或短路。 - 拓扑与支线: 主干应是线性拓扑,节点支线尽量短。星型分支、过长支线和未端接分支会形成反射,波特率越高越难容忍。
- 收发器状态: 检查
STB、EN、RS等管脚是否处于正常工作态,确认 MCU 与收发器共地,且收发器电源满足型号要求。静默、待机或缺少参考地都会造成“控制器看似正常、总线上没有有效帧”。
一帧报文是怎样占用总线的
标准帧与扩展帧
经典 CAN 数据帧由 SOF、仲裁场、控制场、数据场、CRC、ACK、EOF 等字段组成:
- SOF: 1 个显性位,所有节点据此开始同步。
- ID: 标准帧为 11 位,扩展帧为 29 位;它既表达报文语义,也决定仲裁优先级。
- RTR:
0为数据帧,1为远程帧。远程帧在新设计中应谨慎使用,很多网络协议不再依赖它。 - IDE:
0为标准帧,1为扩展帧。 - DLC: 4 位长度代码。对 bxCAN 经典帧而言,实际数据长度只能是
0到8字节。 - CRC 与 ACK: CRC 覆盖帧内容,ACK 槽由任何正确接收帧的其他节点拉为显性。只有发送节点自己在线上时,发送端通常会不断看到 ACK error。
连续 5 个相同比特后,发送器会自动插入 1 个反相位,这就是位填充。因此 8 字节帧占用的总线时间不是常数;做带宽预算时,应按最坏位填充、仲裁重发、错误帧和周期抖动留余量,而不是只用 DLC * 8 估算。
从示波器反推波特率
在稳定的单个位时间区域测得最小位宽 Tbit 后:
bitrate = 1 / Tbit
Tbit = 2 us -> 500 kbit/s
Tbit = 4 us -> 250 kbit/s
Tbit = 8 us -> 125 kbit/s
测量时应避开边沿毛刺、错误帧和位填充后的局部变化,并用 CAN 分析仪解码结果交叉确认。若只看到不规则短脉冲,先排查波特率不一致和终端电阻,再怀疑应用层协议。
bxCAN 位时序:把 PCLK1 配成真实的比特率
bxCAN 的时间量子(TQ)来自 APB1 外设时钟:
bitrate = PCLK1 / (Prescaler * (1 + BS1 + BS2))
sample point = (1 + BS1) / (1 + BS1 + BS2)
其中同步段 SyncSeg 固定为 1 TQ,SJW 用于边沿同步时的微调。STM32F407 常见配置是 PCLK1 = 42 MHz;以下是一组用于 500 kbit/s 的可行参数:
| 参数 | 值 | 推导 |
|---|---|---|
Prescaler | 6 | TQ 时钟 = 42 MHz / 6 = 7 MHz |
BS1 | 11 TQ | 采样点前的传播段与相位段 |
BS2 | 2 TQ | 采样点后的相位段 |
| 总 TQ | 14 | 1 + 11 + 2 |
| 比特率 | 500 kbit/s | 42 MHz / (6 * 14) |
| 采样点 | 85.7% | 12 / 14 |
static void Can_ConfigTiming(CAN_HandleTypeDef *hcan)
{
hcan->Init.Prescaler = 6U;
hcan->Init.Mode = CAN_MODE_NORMAL;
hcan->Init.SyncJumpWidth = CAN_SJW_1TQ;
hcan->Init.TimeSeg1 = CAN_BS1_11TQ;
hcan->Init.TimeSeg2 = CAN_BS2_2TQ;
hcan->Init.TimeTriggeredMode = DISABLE;
hcan->Init.AutoBusOff = ENABLE;
hcan->Init.AutoWakeUp = DISABLE;
hcan->Init.AutoRetransmission = ENABLE;
hcan->Init.ReceiveFifoLocked = DISABLE;
hcan->Init.TransmitFifoPriority = DISABLE;
}
这段参数不能脱离时钟树直接复用:先用 CubeMX、时钟寄存器或 HAL_RCC_GetPCLK1Freq() 确认实际 PCLK1。对于安全相关网络,是否开启 AutoBusOff、自动重传和发送优先级,需要由系统的故障恢复策略决定。
接收滤波器:共享资源与位布局
CAN1、CAN2 的 28 组共享滤波器
STM32F407 中的 CAN1 与 CAN2 共享 28 组 filter bank,且 CAN2 依赖 CAN1 的时钟域。SlaveStartFilterBank 是全局分界:
SlaveStartFilterBank = 14 | 可用 bank |
|---|---|
| CAN1 | 0 到 13 |
| CAN2 | 14 到 27 |
该分界必须由同一套初始化策略统一配置。CAN2 使用 CAN1 的 bank,或两个外设给出不一致的分界,都是典型的“初始化返回成功但接收不到”的隐患。
32 位规则的真实含义
32 位 scale 下,比较字的主要布局是:
标准帧:StdId[10:0] << 21 | IDE(bit 2) | RTR(bit 1)
扩展帧:ExtId[28:0] << 3 | IDE(bit 2) | RTR(bit 1)
在掩码模式中,掩码位为 1 表示该位参与比较,为 0 表示忽略。IDE 位位于 bit 2:
| 目标 | ID 比较字 bit 2 | Mask bit 2 |
|---|---|---|
| 仅标准帧 | 0 | 1 |
| 仅扩展帧 | 1 | 1 |
| 不区分帧格式 | 任意 | 0 |
16 位 scale 主要用于高效匹配标准帧规则,不能承载完整的 29 位扩展 ID。列表模式是精确白名单;掩码模式适合匹配一段有规律的 ID。不要把“越小的 FilterBank 优先级越高”理解为报文仲裁优先级:报文仲裁只由 ID 决定,filter bank 只影响本节点接收路径的匹配和 FIFO 分配。
下面示例只允许标准数据帧 0x321 进入 FIFO0:
static HAL_StatusTypeDef Can_ConfigStdFilter(
CAN_HandleTypeDef *hcan,
uint32_t filter_bank)
{
const uint32_t id_word = 0x321U << 21;
const uint32_t mask_word =
(0x7FFU << 21) | (1U << 2) | (1U << 1);
CAN_FilterTypeDef filter = {0};
filter.FilterBank = filter_bank;
filter.FilterMode = CAN_FILTERMODE_IDMASK;
filter.FilterScale = CAN_FILTERSCALE_32BIT;
filter.FilterIdHigh = (uint16_t)(id_word >> 16);
filter.FilterIdLow = (uint16_t)id_word;
filter.FilterMaskIdHigh = (uint16_t)(mask_word >> 16);
filter.FilterMaskIdLow = (uint16_t)mask_word;
filter.FilterFIFOAssignment = CAN_FILTER_FIFO0;
filter.FilterActivation = ENABLE;
filter.SlaveStartFilterBank = 14U;
return HAL_CAN_ConfigFilter(hcan, &filter);
}
可靠收发:初始化、邮箱和中断边界
固定初始化顺序
外设时钟、GPIO AF、NVIC 和 HAL_CAN_Init() 就绪后,按以下顺序操作:
HAL_CAN_ConfigFilter
-> HAL_CAN_Start
-> HAL_CAN_ActivateNotification
滤波器应在启动前配置;通知要在总线开始接收前打开。若使用 HAL_CAN_RxFifo0MsgPendingCallback(),必须同时确认 CANx_RX0_IRQn 的 NVIC 使能,以及 stm32f4xx_it.c 中确实调用了 HAL_CAN_IRQHandler(&hcanx)。
发送不能把邮箱拥塞升级为系统死机
发送路径的目标是:参数非法立即失败,邮箱满时可观测地拒绝或由上层排队,硬件返回错误时把状态交给调用者处理,不在驱动层调用 Error_Handler()。
static bool Can_TrySendStdData(
CAN_HandleTypeDef *hcan,
uint16_t std_id,
uint8_t *data,
uint8_t length)
{
CAN_TxHeaderTypeDef header = {0};
uint8_t empty_data = 0U;
uint32_t mailbox = 0U;
if ((hcan == NULL) || (std_id > 0x7FFU) ||
((data == NULL) && (length != 0U)) || (length > 8U)) {
return false;
}
if (HAL_CAN_GetTxMailboxesFreeLevel(hcan) == 0U) {
return false;
}
header.StdId = std_id;
header.IDE = CAN_ID_STD;
header.RTR = CAN_RTR_DATA;
header.DLC = length;
if (length == 0U) {
data = &empty_data;
}
return HAL_CAN_AddTxMessage(hcan, &header, data, &mailbox) == HAL_OK;
}
sizeof(data) 在这里得到的是指针宽度而不是载荷长度,所以 length 必须是显式接口参数。若业务不能接受丢包,应由任务上下文中的队列、重试策略和超时预算决定何时再发;不要在 ISR 回调中阻塞等待发送邮箱。
接收回调只做最小工作
回调运行在中断路径中,适合做取帧、长度校验、投递消息和唤醒任务,不适合格式化字符串、长循环、协议解析或大量日志输出。使用 FreeRTOS 时,ISR 内只能使用 ...FromISR() API,且 CAN 中断优先级必须满足 configMAX_SYSCALL_INTERRUPT_PRIORITY 的约束。
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
CanFrame frame = {0};
BaseType_t task_woken = pdFALSE;
if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0,
&frame.header, frame.data) != HAL_OK) {
return;
}
frame.length = frame.header.DLC;
if (frame.length > 8U) {
return;
}
(void)xQueueSendFromISR(can_rx_queue, &frame, &task_woken);
portYIELD_FROM_ISR(task_woken);
}
CanFrame 的存储、队列满后的丢弃计数和协议解析应由设备/服务层拥有。若多个 CAN 控制器共用回调,还需先明确句柄归属,例如 if (hcan == &hcan1),不要把句柄对象与地址混用。
超过 8 字节:按协议分包,而不是盲目切片
经典 CAN 的长数据必须定义传输层。ISO 15765-2(ISO-TP)提供了通用思路:PCI 占用首字节或前两字节,接收端据此识别帧类型、总长度和序号。
以传输 17 字节数据为例,典型流程是:
- 发送端先发 First Frame:
0x10 0x11表示总长度 17,后续放 6 字节数据。 - 接收端返回 Flow Control:例如
0x30 0x00 0x00,表示可继续发送、无块大小限制、无额外间隔要求。 - 发送端依次发送 Consecutive Frame:
0x21 + 7 字节,再发0x22 + 剩余 4 字节。 - 任一方超时、序号不连续、长度超限或收到错误的 Flow Control,都应丢弃当前会话并记录原因。
因此,业务层不应假定“连续收到三帧就是同一条消息”。至少要有会话状态、源/目标寻址(如适用)、序号、长度、超时和错误计数;若接的是诊断网络,优先复用经过验证的 ISO-TP/UDS 实现。
错误状态与台架定位顺序
从错误计数器理解总线状态
CAN 控制器会维护发送错误计数 TEC 和接收错误计数 REC:
| 状态 | 典型门限 | 工程含义 |
|---|---|---|
| Error Active | TEC < 128 且 REC < 128 | 节点正常参与错误标志发送 |
| Error Passive | TEC >= 128 或 REC >= 128 | 节点仍可通信,但错误标志行为改变,应立即记录和分析 |
| Bus-Off | TEC > 255 | 发送器脱离总线,必须按系统策略恢复 |
错误警告阈值通常是 96。HAL_CAN_GetError() 可以用于采样错误类型,但不要只在出问题时临时打印:建议维护计数器并定期上报 ACK、CRC、stuff、form、bit 和 bus-off 事件。
推荐排查顺序
- 断电测阻: 先确认
CAN_H与CAN_L约为60 ohm,再确认没有对地短路。 - 检查时钟与波特率: 所有节点必须使用相同标称波特率和兼容的采样点。
- 用正常节点制造 ACK: 两节点正常模式、正确端接;单节点发帧出现 ACK error 是预期现象。
- 从 Loopback 分层:
CAN_MODE_LOOPBACK通过,说明控制器和软件路径基本可用;正常模式失败则重点看收发器、线束、端接和另一个节点。 - 查看错误状态与波形: 重点抓首个错误发生前的帧,不要只看错误发生后的重复重传波形。
- 确认过滤器: 临时放宽为接收全部以排除过滤器,再逐步收紧到目标 ID 与帧格式。
一份可执行的验收清单
- 断电测得总线终端电阻约
60 ohm,收发器供电和待机脚状态正确。 - 已用实际
PCLK1推导Prescaler、BS1、BS2,并由 CAN 分析仪确认波特率。 - CAN1/CAN2 的 filter bank 分界一致;标准/扩展帧与 RTR 的掩码位符合预期。
- 初始化顺序为“配置过滤器 -> 启动 -> 激活通知”,中断入口与 NVIC 已连通。
- 发送 API 显式传入长度,校验
0..8,邮箱满和 HAL 失败不会触发全局死机。 - ISR 只完成取帧与
FromISR投递;协议解析、重传和日志在任务上下文执行。 - 已分别验证 Loopback、双节点正常通信、无终端、单节点无 ACK、错误恢复五种台架场景。
- 长数据使用有序号、长度和超时的传输层,不用“连续若干帧”猜测边界。
结论
bxCAN 调通并不难,难的是让系统在端接缺失、节点离线、邮箱拥塞、滤波配置错误和中断压力下仍能给出可解释的行为。先用物理层和位时序建立事实,再用过滤器和任务边界控制数据流,最后用错误计数与台架复现验证恢复策略,CAN 才能从“偶尔收发成功”变成可靠的工程基础设施。