假设你在做一个电机控制项目: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 Mode选Loopback即可。
这是新手最困惑的问题:明明代码是对的,接线是对的,就是发不出去。因为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 + 至少两个节点才能通信"**。
相关文章推荐:
作者:嵌入式阿蔡 | 一枚准大四的嵌入式菜鸟,正在打牢基础,记录成长