CAN总线入门:从协议帧到STM32实战

假设你在做一个电机控制项目:STM32作为主控,需要向电机驱动器发送转速指令,电机驱动器还要返回当前转速和温度。

电机驱动器在2米外的设备末端,周围有大量电磁干扰。用I2C?距离太远抗不住干扰。用UART?只能点对点,挂不了多个设备。

这时候就需要CAN总线------汽车和工业控制的标准通信协议。


一、CAN是什么?

CAN(Controller Area Network,控制器局域网)是德国博世(Bosch)公司开发的多主串行通信协议,专为汽车和工业环境设计。

博世(Bosch)是德国的一家大型工程技术公司,CAN协议就是他们为汽车内部各电子模块之间通信而发明的。

1.1 CAN和你熟悉的I2C/SPI/UART有什么不同?

对比项 UART I2C SPI CAN
线数 2线(TX/RX) 2线(SDA/SCL) 4线(MOSI/MISO/SCK/CS) 2线(CAN_H/CAN_L)
距离 几十厘米 几十厘米 几十厘米 可达1000米(低速)
抗干扰 强(差分信号)
多设备 点对点 多设备(地址区分) 多设备(CS片选) 多设备(ID区分)
速率 最高几Mbps 100kHz-1MHz 几十MHz 最高1Mbps

CAN最大的优势:两根线差分传输,抗干扰极强,能在恶劣的电磁环境(汽车发动机舱)中稳定通信,且一条总线可以挂多个设备。

1.2 应用场景

  • 汽车:发动机控制、仪表盘、ABS刹车系统之间的通信(你车里的OBD接口就是CAN)
  • 电机驱动:主控与电机驱动器通信,发送转速指令、接收状态反馈
  • 工业控制:PLC与传感器/执行器通信
  • 机器人:机械臂各关节电机与主控通信

二、CAN物理层(硬件怎么接线)

2.1 接线方式

所有设备并联在两根线上(CAN_H和CAN_L):

复制代码
     STM32主控        电机驱动器        传感器节点
     ┌──────┐        ┌──────┐        ┌──────┐
     │ CAN_H├──┬─────┤CAN_H ├┬───────┤CAN_H │
     │ CAN_L├──┴─────┤CAN_L ├┴───────┤CAN_L │
     └──┬───┘        └──────┘        └──┬───┘
        │                               │
    120Ω电阻                         120Ω电阻
    (终端电阻)                       (终端电阻)

终端电阻 :总线两端各加一个120Ω电阻,用于匹配线路阻抗,防止信号反射(就像水流到管子末端如果没有缓冲会反弹回来)。

忘了加终端电阻是CAN不通的最常见原因!

2.2 差分信号是什么意思?

CAN不用单根线的0V/3.3V表示0和1,而是用两根线的电压差来表示:

复制代码
显性(逻辑0):CAN_H ≈ 3.5V, CAN_L ≈ 1.5V → 压差2V → 表示"0"
隐性(逻辑1):CAN_H ≈ 2.5V, CAN_L ≈ 2.5V → 压差0V → 表示"1"

为什么抗干扰强? 因为干扰会同时作用在两根线上(共模干扰),但差分信号只看两根线的差值,干扰被抵消了。这就像两个人同时被推了一下,但他们之间的距离不变。

2.3 "线与"原理(仲裁的基础)

CAN总线的物理设计决定了:只要有一个节点发送显性(0),总线就是显性(0)。只有所有节点都发隐性(1),总线才是隐性(1)。这叫"线与"------就像并联的开关,任何一个按下(0),输出就是0。

这就是CAN仲裁的物理基础(见第四节)。


三、CAN帧结构

CAN标准帧由以下部分组成:

复制代码
┌──────┬────────┬─────┬────────┬──────┬─────┬───────┬──────┐
│ 帧起始│ 仲裁段  │控制段│  数据段 │CRC段 │ACK段│ 帧结束 │ 间隔  │
│  1位  │ 12/32位 │ 6位 │0-64位  │ 16位 │ 2位 │ 7位   │ 3位  │
└──────┴────────┴─────┴────────┴──────┴─────┴───────┴──────┘
字段 内容 解释
仲裁段 11位ID(标准帧) + RTR ID决定优先级;RTR区分数据帧(0)和远程帧(1)
控制段 IDE + DLC IDE区分标准帧(0)/扩展帧(1);DLC=数据长度(0-8字节)
数据段 0-8字节数据 实际要传的数据
CRC段 15位CRC + 1位界定符 自动校验,不用你管
ACK段 2位 发送方发1,接收方回0表示"我收到了"

术语解释

  • RTR(Remote Transmission Request,远程传输请求):RTR=0是数据帧(我要发数据),RTR=1是远程帧(我请求别人发数据给我)。实际项目中远程帧很少用,大部分都是数据帧。
  • IDE(Identifier Extension,标识符扩展):IDE=0用11位标准ID,IDE=1用29位扩展ID。一般用标准帧就够了。
  • DLC(Data Length Code,数据长度码):表示数据段有几个字节,范围0-8。CAN一帧最多传8字节。

重点:CAN一帧最多传8字节。要传更多数据(比如一个结构体有20字节),需要自己分包发送。


四、仲裁机制(CAN的核心)

4.1 具体场景理解

假设你的项目里有3个设备挂在CAN总线上:

  • 主控STM32:ID=0x100(最高优先级,发转速指令)
  • 电机驱动器:ID=0x200(返回状态数据)
  • 温度传感器:ID=0x300(定期上报温度)

如果3个设备同时想发数据,谁的ID小谁先发,这就是仲裁。

4.2 仲裁过程

多个节点同时发送时,ID越小优先级越高。仲裁通过"线与"实现:

复制代码
电机驱动器发送 ID=0x200 = 0010 0000 0000
温度传感器发送 ID=0x300 = 0011 0000 0000

逐位比较(从高位开始):
  电机:  0 0 1 0  0 0 0 0  0 0 0 0
  温度:  0 0 1 1  0 0 0 0  0 0 0 0
              ↑
        第3位:电机发0(显性),温度发1(隐性)
        由于"线与"机制,总线值=0(显性)
        温度传感器发现自己发的1变成了0 → 仲裁失败,停止发送
        电机驱动器继续发送完整帧

结果:ID小的电机驱动器赢了,温度传感器等总线空闲后重发

4.3 为什么叫"非破坏性仲裁"?

因为优先级高的帧完整无损地发送成功,优先级低的帧只是等待重发,数据不会被破坏。这比以太网的"冲突就全部重发"高效得多。


五、STM32 bxCAN配置实战

5.1 bxCAN是什么?

STM32F1内部集成的CAN控制器叫 bxCAN(Basic Extended CAN,基本扩展CAN)。"基本"是因为它比高端CAN控制器(如CAN-FD)功能少一些,但对新手项目完全够用。它支持标准帧和扩展帧,有14个过滤器(F1系列)。

5.2 CubeMX配置

复制代码
Connectivity → CAN → 勾选

在 Parameter Settings 里配置:
  Bit Timing:
    Prescaler: 9          ← 分频系数
    Time Quanta in Bit Segment 1 (BS1): 6 TQ
    Time Quanta in Bit Segment 2 (BS2): 1 TQ
    Resync Jump Width (SJW): 1 TQ
    → 波特率 = 36MHz / (9 × (1+6+1)) = 500kbps

5.3 波特率参数详解(新手必看)

这个公式新手最容易看不懂,我来逐个解释:

复制代码
BaudRate = APB1时钟 / (Prescaler × (1 + BS1 + BS2))

每个参数是什么意思?

CAN把一个bit的时间分成若干个TQ(Time Quanta,时间量子)。你可以把TQ理解为CAN时钟的"最小时间单位"。

一个bit = 1个同步段(SYNC) + BS1段 + BS2段,其中SYNC固定1个TQ:

复制代码
一个bit的时间组成:
├── SYNC (1 TQ) ──┤── BS1 (6 TQ) ──┤── BS2 (1 TQ) ──│
                                         ↑
                                    采样点(在这里读取总线电平)
  • Prescaler(分频系数):APB1时钟(36MHz)除以Prescaler,得到TQ的频率。Prescaler=9 → TQ频率=36MHz/9=4MHz → 1个TQ=0.25微秒
  • BS1(时间段1):采样点之前的时间,用来等待信号稳定。BS1=6表示6个TQ
  • BS2(时间段2):采样点之后的时间,用来处理采样结果。BS2=1表示1个TQ
  • SJW(重新同步跳跃宽度):时钟有误差时允许调整的最大TQ数,一般设1

计算500kbps

复制代码
1个bit = (1 + 6 + 1) = 8个TQ
1个TQ = 1/(36MHz/9) = 0.25微秒
1个bit = 8 × 0.25 = 2微秒
波特率 = 1/2微秒 = 500kbps ✓

采样点位置建议在75%-87.5%处:采样点 = (1+BS1)/(1+BS1+BS2) = 7/8 = 87.5%,合理。

5.4 发送代码(电机控制场景)

c 复制代码
/* 场景:主控STM32向电机驱动器发送转速指令
 * 协议约定:ID=0x200,数据[0-1]=目标转速(rpm),数据[2]=方向(0正转/1反转) */

CAN_TxHeaderTypeDef tx_header;
uint8_t tx_data[8] = {0};
uint32_t tx_mailbox;

/* 组装数据 */
int16_t target_rpm = 3000;  /* 目标转速3000rpm */
tx_data[0] = target_rpm & 0xFF;        /* 转速低8位 */
tx_data[1] = (target_rpm >> 8) & 0xFF; /* 转速高8位 */
tx_data[2] = 0;                         /* 0=正转 */

/* 填充帧头 */
tx_header.StdId = 0x200;          /* 标准ID=0x200 */
tx_header.IDE = CAN_ID_STD;       /* 使用标准帧(11位ID) */
tx_header.RTR = CAN_RTR_DATA;     /* 数据帧(不是远程帧) */
tx_header.DLC = 3;                /* 数据长度3字节 */
tx_header.TransmitGlobalTime = DISABLE;

/* 发送 */
HAL_CAN_AddTxMessage(&hcan, &tx_header, tx_data, &tx_mailbox);

5.5 接收代码(中断方式)

c 复制代码
CAN_RxHeaderTypeDef rx_header;
uint8_t rx_data[8];

/* 1. 配置过滤器:先设为接收所有帧(调试阶段) */
CAN_FilterTypeDef filter;
filter.FilterIdHigh = 0;
filter.FilterIdLow = 0;
filter.FilterMaskIdHigh = 0;
filter.FilterMaskIdLow = 0;
filter.FilterFIFOAssignment = CAN_RX_FIFO0;
filter.FilterBank = 0;
filter.FilterMode = CAN_FILTERMODE_IDMASK;
filter.FilterScale = CAN_FILTERSCALE_32BIT;
filter.FilterActivation = ENABLE;
HAL_CAN_ConfigFilter(&hcan, &filter);

/* 2. 启动CAN并开启接收中断 */
HAL_CAN_Start(&hcan);
HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING);

/* 3. 接收回调函数(CAN收到帧时HAL库自动调用) */
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
    HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rx_header, rx_data);
    
    /* 场景:接收电机驱动器返回的状态数据
     * 协议约定:ID=0x181,数据[0-1]=当前转速,数据[2]=温度 */
    if (rx_header.StdId == 0x181) {
        int16_t current_rpm = rx_data[0] | (rx_data[1] << 8);
        uint8_t temp = rx_data[2];
        printf("电机转速: %d rpm, 温度: %d°C\r\n", current_rpm, temp);
    }
}

六、过滤器配置

6.1 过滤器是什么?为什么需要?

STM32的CAN控制器有14个过滤器(F1系列)。如果你只关心ID=0x181的帧(电机状态),不关心其他帧,可以用过滤器把不需要的帧挡在CPU之外,减少中断次数。

6.2 过滤器模式

列表模式 :只接收指定ID的帧(精确匹配)

掩码模式:接收符合某种规律的ID(用mask过滤)

6.3 掩码模式配置详解

c 复制代码
/* 只接收ID=0x180~0x1FF的帧 */
filter.FilterIdHigh = 0x180 << 5;     /* ID左移5位对齐 */
filter.FilterIdLow = 0;
filter.FilterMaskIdHigh = 0x700 << 5; /* mask也左移5位 */
filter.FilterMaskIdLow = 0;
filter.FilterMode = CAN_FILTERMODE_IDMASK;
filter.FilterScale = CAN_FILTERSCALE_32BIT;

mask规则:mask为1的位必须匹配,为0的位忽略。

  • mask=0x700(二进制111 0000 0000)表示只看ID的高3位
  • 0x180的高3位=001,0x1FF的高3位=001,所以0x180~0x1FF都能通过
  • 0x200的高3位=010,不匹配,被过滤

为什么ID要左移5位? 因为STM32的32位过滤器中,11位标准ID存放在高16位的高11位,低5位是其他信息(RTR、IDE等)。所以标准ID要左移5位才能对齐到正确位置。这个你不用记,知道"标准ID左移5位"这个规则就行。


七、常见踩坑

坑1:CAN不通------终端电阻(最常见!)

现象:CAN发送函数一直返回超时,或者完全收不到数据。

原因:CAN总线两端必须各加一个120Ω电阻。只有一个电阻或没有电阻,信号反射导致通信失败。

解决:用万用表量CAN_H和CAN_L之间的电阻,如果有两个120Ω并联,应该量到60Ω。量不到就说明缺电阻。

坑2:只有一个节点,CAN发不出去(新手必踩!)

现象:STM32发CAN帧,发送函数返回超时错误,帧根本没发出去。

原因 :CAN协议规定,发送方发出帧后,必须有至少一个其他节点回复ACK(应答信号)。如果你只有一个STM32板子,没有第二个CAN节点,没有人回复ACK,发送方认为通信失败,一直重发直到超时。

解决

  • 方案1:接两个CAN节点(两个STM32,或一个STM32+一个CAN分析仪)
  • 方案2:STM32的CAN有个"Loopback模式"(回环模式),自发自收,不需要外部节点。调试阶段可以用这个模式测试代码是否正确。CubeMX里 Operating ModeLoopback 即可。

这是新手最困惑的问题:明明代码是对的,接线是对的,就是发不出去。因为CAN不像UART可以单节点发,CAN必须有应答。

坑3:波特率不匹配

现象:两个节点都接好了,但互相收不到数据。

原因:两个节点的波特率必须完全一致,差1%都可能通信失败。

解决:用上面的公式仔细算两边的波特率是否一致。常见错误:一个节点APB1=36MHz(F1),另一个APB1=42MHz(F4),同样的Prescaler和BS1/BS2值波特率不同。

坑4:过滤器配错收不到数据

现象:发数据的一方发了,但接收方收不到。

原因:过滤器设太严格,把想要的帧也过滤掉了。

解决:调试阶段先把过滤器设为全接收(ID=0, mask=0,表示不过滤任何帧),确认通信正常后再设过滤规则。

坑5:CAN收发器没接

现象:STM32的CAN_TX/CAN_RX引脚直接连到另一块的CAN_TX/CAN_RX,不通。

原因 :STM32的CAN引脚输出的是TTL电平(0/3.3V),不是CAN总线的差分信号。中间需要一个CAN收发器芯片(如TJA1050、SN65HVD230)把TTL电平转成差分信号。

复制代码
STM32 CAN_TX ──→ [TJA1050] ──→ CAN_H
STM32 CAN_RX ←── [TJA1050] ←── CAN_L

买开发板时注意有没有带CAN收发器。有些便宜的板子只有STM32的CAN引脚引出,没有收发器芯片,直接连是通不了的。


八、总结

要点 内容
CAN特点 差分两线、抗干扰强、多主、仲裁、CRC
帧结构 ID+DLC+数据(0-8字节)+CRC+ACK
仲裁 ID越小优先级越高,非破坏性仲裁
终端电阻 总线两端各120Ω
波特率 APB1/(Prescaler×(1+BS1+BS2))
必须两节点 单节点发不出去(没ACK),调试用Loopback模式
收发器 STM32需要外接CAN收发器(TJA1050等)

一句话总结 :CAN的核心是**"差分传输抗干扰 + ID仲裁定优先级 + 终端电阻120Ω×2 + 至少两个节点才能通信"**。


相关文章推荐


作者:嵌入式阿蔡 | 一枚准大四的嵌入式菜鸟,正在打牢基础,记录成长

相关推荐
恒锐丰科技韩生2 小时前
率能 SS8847E|2.7~15V 双通道 H 桥电机驱动 ETSSOP16 散热封装 家电 / 打印 / 舞台灯光专用
嵌入式硬件·硬件工程
离凌寒2 小时前
一、对stm32mp157配置freertos后,任务挂掉无法启动问题总结
stm32·单片机·嵌入式硬件
LONGZETECH2 小时前
无人机组装调试实训高损耗痛点分析与虚拟仿真教学落地方案
c语言·开发语言·架构·无人机
luj_17683 小时前
大航海时代:沉浸式财商实战沙盒
c语言·开发语言·网络·经验分享·算法
Navigator_Z3 小时前
LeetCode //C - 1200. Minimum Absolute Difference
c语言·算法·leetcode
LuminousCPP3 小时前
数据结构基础篇(二):顺序表与链表全方位对比|从内存布局到 CPU 缓存理解底层差异
c语言·数据结构·经验分享·链表·缓存
Hello_Damon_Nikola5 小时前
CH573从入门到精通
c语言·开发语言·单片机·嵌入式硬件
oshan20125 小时前
华大小华HC32F460单片机在线烧录
单片机·嵌入式硬件
LCG元5 小时前
STM32 USB HID 自定义免驱设备开发:从报告描述符到上位机 64 字节收发实战
stm32·单片机·嵌入式硬件