嵌入式开发 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 位 CRC | 17/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 | 备注 |
|---|---|---|---|
| MCP2517FD | SPI | ✅ 支持 | Microchip 主力型号,资料最丰富 |
| MCP2518FD | SPI | ✅ 支持 | MCP2517FD 改进版,ESD 增强 |
| TCAN1042/44 | 直连 MCU CAN | ✅ 支持 | 内置 CAN 控制器 + 收发器 |
| TJA1043 | 直连 MCU CAN | ❌ 经典 CAN | NXP 经典型号,不支持 FD |
我们选 MCP2517FD——这颗 SPI 接口 CAN FD 控制器是目前 DIY 和工业项目中最容易上手的选择,配合 SN65HVD230 或 TJA1050 收发器就能搭建完整节点。
硬件清单
| 物料 | 规格 | 数量 | 参考价格 |
|---|---|---|---|
| STM32F103C8T6 最小系统板 | ARM Cortex-M3, 72 MHz | 2 块 | ¥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,接收端啥都收不到。
排查步骤:
- 用万用表测 CAN_H 和 CAN_L 之间的静态电压,正常应在 2.5 V 左右
- 确认总线两端各接了一个 120 Ω 终端电阻,总阻抗应接近 60 Ω
- 用示波器测 CAN_H 波形,应该能看到清晰的差分方波
- 检查 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。
排查:
- 确认 SPI 模式为 Mode 0(CPOL=0, CPHA=0),这是 MCP2517FD 唯一支持的模式
- 检查 片选(CS)引脚时序——必须在每次传输前拉低、传输后拉高
- 确认 SPI 时钟 不超过 10 MHz(MCP2517FD 最大支持速率)
- 使用逻辑分析仪抓取 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 的水比较深,踩过的坑我都在这儿列出来了,希望对你有用!