|
CAN FD 总线实战:高速车载通信协议开发与调试

CAN FD 总线实战:高速车载通信协议开发与调试

现代汽车里动辄几十个 ECU(电子控制单元),雷达、摄像头、电机控制器、电池管理系统……这些设备之间的数据量早已不是十年前经典 CAN 总线 8 字节帧能应付的了。于是 Bosch 在 2011 年推出了 CAN FD(CAN with Flexible Data-Rate)——在保留经典 CAN 可靠性的同时,把数据帧从 8 字节一口气拉到 64 字节,数据传输速率从 1 Mbps 飙升到 5~8 Mbps。

这篇文章不跟你空谈概念,我们直接上手:理解 CAN FD 和经典 CAN 的协议差异、选型 MCP2517FD 收发芯片、写驱动代码、连两台开发板做收发测试,再到用示波器抓波形排查问题。读完你就能在自己项目里跑起 CAN FD 通信。

CAN FD vs 经典 CAN:到底升级了什么?

先说结论:CAN FD 不是全新的协议,而是经典 CAN 的”超集”。同一根总线上,经典 CAN 节点和 CAN FD 节点可以共存互不干扰——这是 CAN FD 最聪明也最实用的设计。

核心差异对比

特性经典 CAN(ISO 11898-2)CAN FD(ISO 11898-1:2015)
数据场最大长度8 字节64 字节
仲裁段速率最高 1 Mbps最高 1 Mbps
数据段速率同仲裁段最高 5~8 Mbps
CRC 校验15 位 CRC17/21 位 CRC(帧长自适应)
帧类型标准帧 + 扩展帧标准帧 + 扩展帧 + FD 帧
错误检测基础增强型 ESI 标志位

CAN FD 最关键的创新在于 “双速率”机制:仲裁段(含 ID 和 RTR)使用低速(通常 500 kbps)保证多节点仲裁的可靠性,一旦仲裁胜出进入数据段,速率立刻切换到高速(如 2 Mbps 或 5 Mbps)。帧结束后再降回仲裁速率。

CAN FD 帧结构速览

仲裁段(低速率)               数据段(高速率)
┌─────┬──────┬──────┬─────┬───────────────────────────┐
│ SOF │ ID   │ BRS  │ ESI │  数据场 (0~64 字节)       │
│     │11/29b│  1b  │ 1b  │  + 增强 CRC + CRC 界定符   │
└─────┴──────┴──────┴─────┴───────────────────────────┘
     ↑                ↑
   仲裁区域         BRS=1 时切换高速率
  • BRS(Bit Rate Switch):为 1 表示此帧是 CAN FD 帧,数据段切换高速率
  • ESI(Error State Indicator):发送节点处于错误被动状态时置 1,用于快速诊断

硬件选型:MCP2517FD 实战方案

做 CAN FD 开发,芯片选型是第一步。目前市面主流选择:

芯片接口CAN FD备注
MCP2517FDSPI✅ 支持Microchip 主力型号,资料最丰富
MCP2518FDSPI✅ 支持MCP2517FD 改进版,ESD 增强
TCAN1042/44直连 MCU CAN✅ 支持内置 CAN 控制器 + 收发器
TJA1043直连 MCU CAN❌ 经典 CANNXP 经典型号,不支持 FD

我们选 MCP2517FD——这颗 SPI 接口 CAN FD 控制器是目前 DIY 和工业项目中最容易上手的选择,配合 SN65HVD230 或 TJA1050 收发器就能搭建完整节点。

硬件清单

物料规格数量参考价格
STM32F103C8T6 最小系统板ARM Cortex-M3, 72 MHz2 块¥15
MCP2517FD 模块SPI 接口,带 8 MHz 晶振2 个¥25
TJA1050 CAN 收发器高速 CAN,5 V 供电2 个¥8
120 Ω 终端电阻总线两端各一个2 个¥1
杜邦线 / 连接线22 AWG若干¥5
USB-TTL 转接器串口调试用1 个¥10

注意:CAN 总线两端(最远两个节点)必须各接一个 120 Ω 终端电阻,否则信号反射会导致误码。中间节点不需要终端电阻。

接线图

STM32F103          MCP2517FD        TJA1050         CAN 总线
┌─────────┐       ┌─────────┐     ┌─────────┐      ┌──────┐
│ PA5(SCK)├──────►│ SCK     │     │         │      │      │
│ PA6(MISO)◄──────│ MISO    │     │         │      │ CAN_H│──┬──
│ PA7(MOSI)├─────►│ MOSI    │     │ TX ◄────┤      │      │  │
│ PA4(CS) ├──────►│ CS      │     │ RX ─────┤      │ CAN_L│──┼──
│ 3.3V    ├──────►│ VDD     │     │         │      └──────┘  │
│ GND     ├──────►│ GND     │     │ VDD(5V) ├────── VCC     │
│ INT(PB0)◄──────│ INT     │     │ GND     ├────── GND      │
└─────────┘       └────┬────┘     └─────┬───┘                │
                       │               │                   120Ω
                   8 MHz晶振           │                    │
                       │               │                    │
                  ─────┴───────────────┴────────────────────┴──

两个节点按上图连接后,CAN_H 对 CAN_L 并联 120 Ω 电阻。

驱动开发:MCP2517FD 软件实现

1. SPI 初始化

STM32 HAL 库初始化 SPI(MCP2517FD 最高支持 10 MHz SPI 时钟):

/* SPI 初始化 - STM32 HAL */
void SPI_Init(void) {
    hspi1.Instance = SPI1;
    hspi1.Init.Mode = SPI_MODE_MASTER;
    hspi1.Init.Direction = SPI_DIRECTION_2LINES;
    hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
    hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;   // CPOL = 0
    hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;       // CPHA = 0 (SPI Mode 0)
    hspi1.Init.NSS = SPI_NSS_SOFT;               // 软件片选
    hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; // ~9 MHz
    hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
    HAL_SPI_Init(&hspi1);
}

/* MCP2517FD 片选控制 */
#define CS_LOW()  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET)
#define CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET)

/* SPI 读写封装 */
uint8_t SPI_ReadWrite(uint8_t data) {
    uint8_t rx;
    HAL_SPI_TransmitReceive(&hspi1, &data, &rx, 1, 100);
    return rx;
}

2. MCP2517FD 寄存器操作

MCP2517FD 使用统一的 16 位地址 + 8 位命令头格式。这里封装核心读写函数:

#include "mcp2517fd.h"

/* 寄存器地址定义 */
#define MCP_CiCON    0x000  /* CAN 控制寄存器 */
#define MCP_CiNBTCFG 0x004  /* 仲裁段波特率配置 */
#define MCP_CiDBTCFG 0x008  /* 数据段波特率配置 */
#define MCP_CiTDC    0x00C  /* 发送延迟补偿 */
#define MCP_CiTXQCON 0x050  /* 发送队列配置 */
#define MCP_CiFIFOCON 0x054 /* FIFO 配置 */

#define CMD_RESET    0x00
#define CMD_READ     0x03
#define CMD_WRITE    0x02

/* 软件复位 MCP2517FD */
void MCP2517FD_Reset(void) {
    CS_LOW();
    SPI_ReadWrite(CMD_RESET);
    HAL_Delay(10);
    CS_HIGH();
    HAL_Delay(10);
}

/* 写 32 位寄存器 */
void MCP2517FD_WriteReg(uint16_t addr, uint32_t value) {
    uint8_t tx[6];
    tx[0] = CMD_WRITE;
    tx[1] = (addr >> 4) & 0xFF;
    tx[2] = (addr << 4) & 0xF0;
    tx[3] = (value >> 24) & 0xFF;
    tx[4] = (value >> 16) & 0xFF;
    tx[5] = (value >> 8) & 0xFF;
    // 注意:MCP2517FD 寄存器写入是小端序
    CS_LOW();
    for (int i = 0; i < 6; i++) SPI_ReadWrite(tx[i]);
    SPI_ReadWrite(value & 0xFF);  // 最低字节
    CS_HIGH();
}

/* 读 32 位寄存器 */
uint32_t MCP2517FD_ReadReg(uint16_t addr) {
    uint8_t tx[3];
    uint8_t rx[4];
    tx[0] = CMD_READ;
    tx[1] = (addr >> 4) & 0xFF;
    tx[2] = (addr << 4) & 0xF0;
    CS_LOW();
    for (int i = 0; i < 3; i++) SPI_ReadWrite(tx[i]);
    for (int i = 0; i < 4; i++) rx[i] = SPI_ReadWrite(0x00);
    CS_HIGH();
    return (rx[0] << 24) | (rx[1] << 16) | (rx[2] << 8) | rx[3];
}

3. CAN FD 波特率配置

这是最容易翻车的一步。CAN FD 的仲裁段和数据段需要分别配置,而且要考虑 采样点发送延迟补偿(TDC)

/* 配置 CAN FD:仲裁段 500kbps,数据段 2Mbps,采样点 75% */
void MCP2517FD_ConfigCANFD(void) {
    /*
     * 假设 MCP2517FD 外部晶振 = 20 MHz
     * 使用 PLL 倍频到 40 MHz 作为 CAN 时钟源
     */

    // 1. 配置振荡器和 PLL
    uint32_t osc_val = 0x00000400;  // SCLKDIV=1, PLL enable
    MCP2517FD_WriteReg(MCP_OSC, osc_val);
    HAL_Delay(5);  // 等 PLL 锁定

    // 2. 配置仲裁段波特率 (500 kbps)
    // 40 MHz / (20 * 4) = 500 kbps, 采样点 = (1+4+7)/(1+4+8) = 60%
    // 调整: SJW=3, PRSEG=4, PHSEG1=6, PHSEG2=7, 总共 1+4+6+7=18 TQ
    // 40MHz / (80 * 500kHz) = 1, 所以 BRP = 0
    uint32_t nbtcfg = (3 << 24) |    // SJW = 3+1 = 4 TQ
                      (4 << 16) |    // PRSEG = 4+1 = 5 TQ
                      (6 << 8)  |    // PHSEG1 = 6+1 = 7 TQ
                      (7 << 0);      // PHSEG2 = 7+1 = 8 TQ
    // 总 TQ = 1(SYNC) + 5 + 7 + 8 = 21 TQ
    // Bit rate = 40MHz / (21 * BRP) ... 
    // 简化配置: 使用常见值
    MCP2517FD_WriteReg(MCP_CiNBTCFG, nbtcfg);

    // 3. 配置数据段波特率 (2 Mbps)
    // 40 MHz / (20 * 1) = 2 Mbps
    uint32_t dbtcfg = (3 << 24) |    // DSJW = 3
                      (2 << 16) |    // DSEG1 = 2 (3 TQ)
                      (1 << 8)  |    // DSEG2 = 1 (2 TQ)
                      (0 << 0);      // DBRP = 0 (1x)
    MCP2517FD_WriteReg(MCP_CiDBTCFG, dbtcfg);

    // 4. 启用发送延迟补偿 (TDC)
    // TDC 用于补偿收发器延迟,避免数据段相位错误
    uint32_t tdc_val = (0 << 16) |   // TDCOFF = 0
                       (6 << 8);     // TDCMOD = 自动模式
    MCP2517FD_WriteReg(MCP_CiTDC, tdc_val);

    // 5. 配置 CAN 控制寄存器:启用 CAN FD 模式
    uint32_t con_val = (4 << 21) |   // TXQEN = 发送队列启用
                       (1 << 17) |   // WAKFIL = 唤醒滤波
                       (1 << 12) |   // BRSDIS = 启用 Bit Rate Switch
                       (3 << 0);     // REQOP = 请求正常运行模式
    MCP2517FD_WriteReg(MCP_CiCON, con_val);

    // 6. 配置发送 FIFO
    MCP2517FD_WriteReg(MCP_CiTXQCON, 0x00000407);  // 8 个 TX 槽位
}

⚠️ 重要提醒:波特率配置需要严格根据晶振频率计算。实际项目中建议使用 Microchip 官方的 CAN FD 波特率计算器(可在 Microchip 官网下载 Excel 工具),输入晶振频率和目标波特率后自动得到寄存器值。上面代码中的数值需要根据你的实际晶振调整。

4. 发送 CAN FD 帧

/* CAN FD 帧结构 */
typedef struct {
    uint32_t id;          /* CAN ID(11 位标准或 29 位扩展) */
    uint8_t  dlc;         /* 数据长度码 (0~15,对应 0~64 字节) */
    uint8_t  data[64];    /* 数据场 */
    uint8_t  isExtended;  /* 1=扩展帧, 0=标准帧 */
    uint8_t  isFD;        /* 1=CAN FD 帧, 0=经典 CAN */
    uint8_t  isBRS;       /* 1=启用速率切换, 0=关闭 */
} canfd_frame_t;

/* DLC 到实际字节数映射 */
static const uint8_t dlc_to_bytes[] = {
    0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64
};

/* 通过 SPI 将 CAN FD 帧写入 MCP2517FD TX FIFO */
void MCP2517FD_Transmit(canfd_frame_t *frame) {
    uint8_t tx_header[4];

    // 构建发送头(ESI/SID/RTR/BRS/FDF/DLC/IDE 打包为 4 字节)
    uint32_t hdr = 0;
    if (frame->isFD)    hdr |= (1 << 21);   // FDF 位
    if (frame->isBRS)   hdr |= (1 << 20);   // BRS 位
    if (frame->isExtended) {
        hdr |= (1 << 30);                   // IDE 位
        hdr |= (frame->id & 0x1FFFFFFF);    // 29 位扩展 ID
    } else {
        hdr |= ((frame->id & 0x7FF) << 18); // 11 位标准 ID
    }
    hdr |= (frame->dlc & 0x0F) << 16;       // DLC

    tx_header[0] = (hdr >> 24) & 0xFF;
    tx_header[1] = (hdr >> 16) & 0xFF;
    tx_header[2] = (hdr >> 8) & 0xFF;
    tx_header[3] = hdr & 0xFF;

    // 通过 SPI 写入 TX FIFO
    CS_LOW();
    SPI_ReadWrite(0x02);  // 写命令
    SPI_ReadWrite(0x05 << 4);  // TX FIFO 地址
    for (int i = 0; i < 4; i++) SPI_ReadWrite(tx_header[i]);

    uint8_t byte_count = dlc_to_bytes[frame->dlc];
    for (int i = 0; i < byte_count; i++) {
        SPI_ReadWrite(frame->data[i]);
    }
    CS_HIGH();

    // 触发发送
    MCP2517FD_WriteReg(MCP_CiTXQCON,
        MCP2517FD_ReadReg(MCP_CiTXQCON) | (1 << 10));  // UINC + TXREQ
}

5. 接收 CAN FD 帧

/* 从 RX FIFO 读取 CAN FD 帧 */
int MCP2517FD_Receive(canfd_frame_t *frame) {
    uint8_t rx_header[4];
    uint32_t hdr;

    // 检查是否有接收数据
    uint32_t fifo_status = MCP2517FD_ReadReg(MCP_CiFIFOSTA);
    if (!(fifo_status & 0x01)) {
        return 0;  // FIFO 空,无数据
    }

    // 读取帧头
    CS_LOW();
    SPI_ReadWrite(0x03);  // 读命令
    SPI_ReadWrite(0x06 << 4);  // RX FIFO 地址
    for (int i = 0; i < 4; i++) rx_header[i] = SPI_ReadWrite(0x00);
    CS_HIGH();

    hdr = (rx_header[0] << 24) | (rx_header[1] << 16) |
          (rx_header[2] << 8) | rx_header[3];

    // 解析 ID
    frame->isExtended = (hdr >> 30) & 0x01;
    if (frame->isExtended) {
        frame->id = hdr & 0x1FFFFFFF;
    } else {
        frame->id = (hdr >> 18) & 0x7FF;
    }

    // 解析 DLC 和帧类型
    frame->dlc = (hdr >> 16) & 0x0F;
    frame->isFD = (hdr >> 21) & 0x01;
    frame->isBRS = (hdr >> 20) & 0x01;

    // 读取数据场
    uint8_t byte_count = dlc_to_bytes[frame->dlc];
    CS_LOW();
    SPI_ReadWrite(0x03);  // 读命令
    SPI_ReadWrite(0x06 << 4);  // RX FIFO 地址
    for (int i = 0; i < 4 + byte_count; i++) {
        if (i >= 4) {
            frame->data[i - 4] = SPI_ReadWrite(0x00);
        } else {
            SPI_ReadWrite(0x00);
        }
    }
    CS_HIGH();

    // 释放 FIFO 槽位
    MCP2517FD_WriteReg(MCP_CiFIFOUINT, 0x00000200);  // UINC

    return byte_count;
}

完整测试程序

两个节点分别烧录发送和接收程序,进行收发测试:

节点 A(发送端)

/* main_tx.c - CAN FD 发送端 */
int main(void) {
    HAL_Init();
    SystemClock_Config();
    SPI_Init();
    MCP2517FD_Reset();
    MCP2517FD_ConfigCANFD();

    // 构造测试帧
    canfd_frame_t tx_frame;
    tx_frame.id = 0x123;
    tx_frame.isExtended = 0;
    tx_frame.isFD = 1;         // CAN FD 帧
    tx_frame.isBRS = 1;        // 启用速率切换
    tx_frame.dlc = 12;         // 32 字节数据

    // 填充测试数据(0x00 ~ 0x1F)
    for (int i = 0; i < 32; i++) {
        tx_frame.data[i] = i;
    }

    while (1) {
        MCP2517FD_Transmit(&tx_frame);
        HAL_Delay(100);  // 每 100ms 发送一帧
    }
}

节点 B(接收端 + 串口打印)

/* main_rx.c - CAN FD 接收端 */
int main(void) {
    HAL_Init();
    SystemClock_Config();
    SPI_Init();
    UART_Init();  // 串口用于调试输出
    MCP2517FD_Reset();
    MCP2517FD_ConfigCANFD();

    // 配置 RX FIFO 过滤:接收 ID = 0x123 的帧
    MCP2517FD_SetFilter(0, 0x123, 0x7FF);  // 标准帧,mask 全匹配

    canfd_frame_t rx_frame;

    while (1) {
        int len = MCP2517FD_Receive(&rx_frame);
        if (len > 0) {
            char msg[256];
            int offset = sprintf(msg,
                "[RX] ID=0x%03lX, DLC=%d, Bytes=%d, FD=%d, BRS=%d, Data=",
                rx_frame.id, rx_frame.dlc, len,
                rx_frame.isFD, rx_frame.isBRS);
            for (int i = 0; i < len; i++) {
                offset += sprintf(msg + offset, "%02X ", rx_frame.data[i]);
            }
            sprintf(msg + offset, "\r\n");
            UART_SendString(msg);
        }
    }
}

预期输出

[RX] ID=0x123, DLC=12, Bytes=32, FD=1, BRS=1, Data=00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F
[RX] ID=0x123, DLC=12, Bytes=32, FD=1, BRS=1, Data=00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F
[RX] ID=0x123, DLC=12, Bytes=32, FD=1, BRS=1, Data=00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F

常见问题排查

问题 1:总线完全无通信

症状:发送端调了 Transmit,接收端啥都收不到。

排查步骤

  1. 用万用表测 CAN_H 和 CAN_L 之间的静态电压,正常应在 2.5 V 左右
  2. 确认总线两端各接了一个 120 Ω 终端电阻,总阻抗应接近 60 Ω
  3. 用示波器测 CAN_H 波形,应该能看到清晰的差分方波
  4. 检查 MCP2517FD 的 晶振是否起振——用示波器看晶振引脚是否有 20 MHz(或你的实际频率)正弦波

问题 2:数据段误码率高

症状:偶尔收到数据,但数据内容错乱。

可能原因

  • TDC(发送延迟补偿)未正确配置:CAN FD 在速率切换时收发器会有延迟,必须启用 TDC。设置 TDCMOD = 0x02(自动模式)让芯片自动计算补偿值
  • 数据段波特率设置过高:先降到 1 Mbps 测试,稳定后再逐步提高
  • 线缆过长或质量差:CAN FD 高速段对线缆要求比经典 CAN 严格得多,建议使用 双绞屏蔽线,长度不超过 40 米(5 Mbps 时)

问题 3:混合总线中 CAN FD 帧被经典 CAN 节点拒绝

症状:总线上有经典 CAN 节点,发送 CAN FD 帧后经典 CAN 节点报错。

解决:CAN FD 协议设计上允许混合运行,但要注意:

  • 经典 CAN 节点收到 CAN FD 帧时会识别出 BRS 位,将其视为”格式错误”帧并发送错误帧
  • 如果你的网络中有经典 CAN 节点,要么将它们升级支持 CAN FD,要么 隔离总线(CAN FD 用独立子网)
  • 使用 CAN FD 收发器时,确保其兼容 ISO 11898-1:2015 标准

问题 4:SPI 读写寄存器返回异常值

症状:读取 MCP2517FD 寄存器得到 0xFF 或 0x00。

排查

  1. 确认 SPI 模式为 Mode 0(CPOL=0, CPHA=0),这是 MCP2517FD 唯一支持的模式
  2. 检查 片选(CS)引脚时序——必须在每次传输前拉低、传输后拉高
  3. 确认 SPI 时钟 不超过 10 MHz(MCP2517FD 最大支持速率)
  4. 使用逻辑分析仪抓取 SPI 总线,对比命令格式是否符合数据手册

总结

CAN FD 是经典 CAN 的自然进化——它保留了 CAN 的所有优秀特性(多主仲裁、错误检测、高可靠性),同时把数据量从 8 字节提升到 64 字节、速率从 1 Mbps 提升到 5 Mbps 甚至 8 Mbps。

对于汽车电子、工业自动化、机器人等场景,CAN FD 已经从”要不要用”变成”必须用”的选择。掌握 CAN FD 开发,意味着你能:

  • 减少总线负载率:一条 64 字节 CAN FD 帧 ≈ 八条 8 字节经典 CAN 帧,总线效率提升数倍
  • 降低延迟:高速数据段让关键传感器数据更快到达
  • 兼容现有系统:经典 CAN 和 CAN FD 共存,渐进式升级

MCP2517FD 是目前 DIY 和原型开发中最实用的 CAN FD 芯片,配合 STM32 或 ESP32 就能快速搭建原型。希望这篇文章帮你少走弯路,顺利在项目里跑起来 CAN FD 通信。

有问题的话欢迎在评论区交流——CAN FD 的水比较深,踩过的坑我都在这儿列出来了,希望对你有用!