/notes/mcu/stm32-bxcan-debug

Firmware//22 分钟

STM32 bxCAN 开发与调试:从差分波形到可靠收发

以 STM32F4 bxCAN 为例,串起物理层、位时序、滤波器、可靠收发、ISO-TP 分包与台架调试,把 CAN 从“能收发”落到可验证的工程闭环。

STM32bxCANCAN 2.0HALFreeRTOS示波器

学习目标与边界

这篇笔记以 STM32F4 bxCAN + CAN 2.0B 为对象,覆盖智能座舱网关节点和测试台架中最常见的收发、滤波、波形与故障定位问题。bxCAN 是经典 CAN 控制器:单帧数据区最大 8 字节,不支持 CAN FD 的 64 字节载荷和可变速率数据相位。

先分清三层:控制器只认识帧和错误状态;收发器负责差分电平;线束、终端、电源和节点数量共同决定总线是否真的稳定。软件“发送成功”不等于线上一定有正确波形。

本文中的 HAL 示例用于说明接口边界和错误路径,引脚、CAN 收发器型号、PCLK1 频率、中断优先级及 FreeRTOS 配置必须以实际板卡为准

物理层:先确认总线真的存在

显性、隐性与差分测量

高速 CAN 通过 CAN_HCAN_L 的电压差传输逻辑值,默认空闲状态是隐性:

总线状态逻辑值CAN_H 典型电平CAN_L 典型电平差分电压 CAN_H - CAN_L
隐性 Recessive1约 2.5 V约 2.5 V约 0 V
显性 Dominant0约 3.5 V约 1.5 V约 2 V

显性位会覆盖隐性位,因此 CAN 的仲裁无需破坏正在传输的报文:多个节点同时起发时,最先在 ID 位发送 0 的节点保留总线控制权。数值越小的 ID 优先级越高,这也是报文 ID 设计必须从系统层面统一规划的原因。

台架上采集到的双通道波形

图:台架示波器波形。图中两路呈互补变化,可作为时序观察参考;要确认真实 CAN 物理层质量,应使用差分探头直接测量 CAN_H - CAN_L,不要只凭两路单端波形判断。

上电前的三项检查

  1. 终端电阻: 高速 CAN 主干两端各放一个 120 ohm。断电后在 CAN_HCAN_L 间测得约 60 ohm,通常说明两个终端都在;约 120 ohm 常表示只剩一个终端;远小于 60 ohm 则要排查重复终端或短路。
  2. 拓扑与支线: 主干应是线性拓扑,节点支线尽量短。星型分支、过长支线和未端接分支会形成反射,波特率越高越难容忍。
  3. 收发器状态: 检查 STBENRS 等管脚是否处于正常工作态,确认 MCU 与收发器共地,且收发器电源满足型号要求。静默、待机或缺少参考地都会造成“控制器看似正常、总线上没有有效帧”。

一帧报文是怎样占用总线的

标准帧与扩展帧

经典 CAN 数据帧由 SOF、仲裁场、控制场、数据场、CRC、ACK、EOF 等字段组成:

  1. SOF: 1 个显性位,所有节点据此开始同步。
  2. ID: 标准帧为 11 位,扩展帧为 29 位;它既表达报文语义,也决定仲裁优先级。
  3. RTR: 0 为数据帧,1 为远程帧。远程帧在新设计中应谨慎使用,很多网络协议不再依赖它。
  4. IDE: 0 为标准帧,1 为扩展帧。
  5. DLC: 4 位长度代码。对 bxCAN 经典帧而言,实际数据长度只能是 08 字节。
  6. 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 的可行参数:

参数推导
Prescaler6TQ 时钟 = 42 MHz / 6 = 7 MHz
BS111 TQ采样点前的传播段与相位段
BS22 TQ采样点后的相位段
总 TQ141 + 11 + 2
比特率500 kbit/s42 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
CAN1013
CAN21427

该分界必须由同一套初始化策略统一配置。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 2Mask bit 2
仅标准帧01
仅扩展帧11
不区分帧格式任意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 字节数据为例,典型流程是:

  1. 发送端先发 First Frame0x10 0x11 表示总长度 17,后续放 6 字节数据。
  2. 接收端返回 Flow Control:例如 0x30 0x00 0x00,表示可继续发送、无块大小限制、无额外间隔要求。
  3. 发送端依次发送 Consecutive Frame0x21 + 7 字节,再发 0x22 + 剩余 4 字节
  4. 任一方超时、序号不连续、长度超限或收到错误的 Flow Control,都应丢弃当前会话并记录原因。

因此,业务层不应假定“连续收到三帧就是同一条消息”。至少要有会话状态、源/目标寻址(如适用)、序号、长度、超时和错误计数;若接的是诊断网络,优先复用经过验证的 ISO-TP/UDS 实现。

错误状态与台架定位顺序

从错误计数器理解总线状态

CAN 控制器会维护发送错误计数 TEC 和接收错误计数 REC

状态典型门限工程含义
Error ActiveTEC < 128REC < 128节点正常参与错误标志发送
Error PassiveTEC >= 128REC >= 128节点仍可通信,但错误标志行为改变,应立即记录和分析
Bus-OffTEC > 255发送器脱离总线,必须按系统策略恢复

错误警告阈值通常是 96。HAL_CAN_GetError() 可以用于采样错误类型,但不要只在出问题时临时打印:建议维护计数器并定期上报 ACKCRCstuffformbitbus-off 事件。

推荐排查顺序

  1. 断电测阻: 先确认 CAN_HCAN_L 约为 60 ohm,再确认没有对地短路。
  2. 检查时钟与波特率: 所有节点必须使用相同标称波特率和兼容的采样点。
  3. 用正常节点制造 ACK: 两节点正常模式、正确端接;单节点发帧出现 ACK error 是预期现象。
  4. 从 Loopback 分层: CAN_MODE_LOOPBACK 通过,说明控制器和软件路径基本可用;正常模式失败则重点看收发器、线束、端接和另一个节点。
  5. 查看错误状态与波形: 重点抓首个错误发生前的帧,不要只看错误发生后的重复重传波形。
  6. 确认过滤器: 临时放宽为接收全部以排除过滤器,再逐步收紧到目标 ID 与帧格式。

一份可执行的验收清单

  • 断电测得总线终端电阻约 60 ohm,收发器供电和待机脚状态正确。
  • 已用实际 PCLK1 推导 PrescalerBS1BS2,并由 CAN 分析仪确认波特率。
  • CAN1/CAN2 的 filter bank 分界一致;标准/扩展帧与 RTR 的掩码位符合预期。
  • 初始化顺序为“配置过滤器 -> 启动 -> 激活通知”,中断入口与 NVIC 已连通。
  • 发送 API 显式传入长度,校验 0..8,邮箱满和 HAL 失败不会触发全局死机。
  • ISR 只完成取帧与 FromISR 投递;协议解析、重传和日志在任务上下文执行。
  • 已分别验证 Loopback、双节点正常通信、无终端、单节点无 ACK、错误恢复五种台架场景。
  • 长数据使用有序号、长度和超时的传输层,不用“连续若干帧”猜测边界。

结论

bxCAN 调通并不难,难的是让系统在端接缺失、节点离线、邮箱拥塞、滤波配置错误和中断压力下仍能给出可解释的行为。先用物理层和位时序建立事实,再用过滤器和任务边界控制数据流,最后用错误计数与台架复现验证恢复策略,CAN 才能从“偶尔收发成功”变成可靠的工程基础设施。