CAN通信协议
一、基础概念
- 物理连接:
- TX/RX无需交叉连接(高速CAN默认)
- 采用差分信号传输(抗干扰性强)
- 通信方式:
- 广播式通信:发送方主动广播数据(数据帧)
- 请求式通信:接收方主动请求数据(遥控帧)
- 接收方通过拉低总线电平(显性电平0)确认接收(类似停止位)
- 关键术语:
- DLC:数据长度码(1~8字节)
- IDE:标识符扩展位(0=标准帧11位ID,1=扩展帧29位ID)
- RTR:远程传输请求位(0=数据帧,1=遥控帧)
- r0/r1:保留位(为协议升级预留)
二、帧类型与结构
|
帧类型 |
用途 |
|---|---|
|
数据帧 |
主动广播数据(发送设备→所有设备) |
|
遥控帧 |
请求数据(接收设备→发送设备,无数据段) |
|
错误帧 |
设备检测错误时通知总线(破坏当前通信) |
|
过载帧 |
接收设备未准备好时延缓发送方 |
|
帧间隔 |
分隔数据帧/遥控帧与其他帧 |
数据帧结构(标准格式):
|
字段 |
功能 |
|---|---|
|
SOF (帧起始) |
显性电平0,标志传输开始 |
|
ID (标识符) |
11位,区分功能并决定优先级(值越小优先级越高) |
|
RTR |
0=数据帧 |
|
IDE |
0=标准帧 |
|
DLC |
4位,指定数据段长度(0~8字节) |
|
Data |
实际数据(0~8字节) |
|
CRC |
15位循环冗余校验 + 1位界定符(隐性1) |
|
ACK |
发送方发隐性1,接收方拉低为显性0确认 |
|
EOF (帧结束) |
7位隐性1,标志传输结束 |
扩展帧差异:
- 29位ID(11位基础ID + 18位扩展ID)
- 新增SRR位(固定隐性1,保证标准帧优先)
三、核心机制
- 非破坏性仲裁:
- 线与特性:总线显性0覆盖隐性1(0 & x = 0,1 & 1 = 1)
- 回读机制:设备发送后回读总线电平,确认是否发送成功
- 仲裁规则:
- 逐位比较ID,显性0优先级高
- ID相同时:数据帧 > 遥控帧,标准帧 > 扩展帧
- 位填充规则:
- 发送方:连续5个相同电平后插入一个反向电平(填充位)
- 接收方:移除填充位恢复原始数据
- 作用:提供定时同步、区分错误帧/过载帧、保持总线活跃
- 再同步机制:
- 位时间划分:同步段(SS) + 传播段(PTS) + 相位缓冲段1(PBS1) + 相位缓冲段2(PBS2)
- 接收方通过调整PBS1/PBS2(±SJW值)对齐采样点
四、错误处理
- 错误类型:
|
错误类型 |
触发条件 |
|---|---|
|
位错误 |
发送电平与总线电平不一致 |
|
填充错误 |
连续检测到6个相同电平(违反填充规则) |
|
CRC错误 |
接收方计算的CRC与帧内CRC不匹配 |
|
格式错误 |
固定格式字段(如界定符、EOF)电平错误 |
|
ACK错误 |
发送方未检测到ACK显性0(无设备接收) |
- 错误状态转换:
|
状态 |
行为 |
|---|---|
|
主动错误 |
可正常通信,检测错误时发送主动错误帧(破坏总线) |
|
被动错误 |
可正常通信,检测错误时发送被动错误帧(不破坏总线) |
|
总线关闭 |
禁止通信(需检测128次连续11位隐性1恢复) |
- 错误计数器:
- TEC(发送错误计数器):错误发送时增加,成功发送时减少
- REC(接收错误计数器):接收错误时增加,成功接收时减少
- 状态转换阈值:
- TEC/REC < 128 → 主动错误
- TEC/REC ≥ 128 → 被动错误
- TEC > 255 → 总线关闭
五、物理层特性
- 无时钟线:设备约定波特率,通过位时序同步
- 采样点:位于数据位中心附近(通过再同步调整)
- 总线空闲:连续11位隐性1(设备仅此时可发起发送)
六、STM32 CAN外设简介
- 特性:
- 支持CAN 2.0A/B
- 最高波特率1 Mbps
- 3个发送邮箱(可配置优先级)
- 2个接收FIFO(3级深度)
- 14个过滤器组(互联型28个)
- 关键功能:
- 自动重传控制
- 时间触发通信模式
- 自动离线恢复
- 接收FIFO溢出处理可配置
关键总结表
|
机制 |
核心规则 |
|---|---|
|
优先级仲裁 |
ID值小者优先 → 数据帧 > 遥控帧 → 标准帧 > 扩展帧 |
|
位填充 |
连续5相同电平 → 插入1反向电平 |
|
总线空闲判定 |
连续11位隐性1 |
|
错误状态切换 |
TEC/REC ≥ 128 → 被动错误;TEC > 255 → 总线关闭 |
|
ACK机制 |
发送方发1,接收方拉低为0确认 |
CAN总线仲裁机制示例
下面是一个用C语言实现的简化版CAN总线仲裁模拟程序,配合波形图解释CAN总线的核心机制。
1 #include <stdio.h> 2 3 #include <stdlib.h> 4 5 #include <stdbool.h> 6 7 #include <string.h> 8 9 #include <unistd.h> 10 11 // CAN节点结构体 12 13 typedef struct { 14 15 int id; // 节点ID 16 17 bool active; // 节点是否活跃 18 19 int state; // 0:发送显性位, 1:发送隐性位 20 21 int bit_index; // 当前发送的位索引 22 23 char frame[14]; // 要发送的帧(SOF+ID+RTR+IDE+控制段) 24 25 } CAN_Node; 26 27 // 总线状态枚举 28 29 typedef enum { 30 31 RECESSIVE, // 隐性 (逻辑1) 32 33 DOMINANT // 显性 (逻辑0) 34 35 } BusState; 36 37 // 模拟CAN总线 38 39 void simulate_can_bus() { 40 41 // 创建三个节点 42 43 CAN_Node nodes[3]; 44 45 46 47 // 初始化节点 (ID越小优先级越高) 48 49 nodes[0] = (CAN_Node){0x123, true, -1, 0, {0,0,0,1,0,0,1,0,0,0,1,1,0,0}}; // ID: 0x123 50 51 nodes[1] = (CAN_Node){0x124, true, -1, 0, {0,0,0,1,0,0,1,0,0,1,0,0,0,0}}; // ID: 0x124 52 53 nodes[2] = (CAN_Node){0x122, true, -1, 0, {0,0,0,1,0,0,1,0,0,0,1,0,0,0}}; // ID: 0x122 (最高优先级) 54 55 56 57 printf("CAN总线仲裁模拟 - 节点ID: 0x%X, 0x%X, 0x%X\n\n", 58 59 nodes[0].id, nodes[1].id, nodes[2].id); 60 61 62 63 BusState bus = RECESSIVE; 64 65 bool arbitration_complete = false; 66 67 int winner_id = -1; 68 69 70 71 // 逐位模拟总线传输 72 73 for (int bit_pos = 0; bit_pos < 14; bit_pos++) { 74 75 // 重置总线为隐性 76 77 bus = RECESSIVE; 78 79 80 81 // 每个节点发送当前位 82 83 for (int i = 0; i < 3; i++) { 84 85 if (nodes[i].active && bit_pos < strlen(nodes[i].frame)) { 86 87 // 发送显性位会覆盖隐性位 88 89 if (nodes[i].frame[bit_pos] == '0') { 90 91 bus = DOMINANT; 92 93 } 94 95 } 96 97 } 98 99 100 101 // 节点检测总线状态 102 103 for (int i = 0; i < 3; i++) { 104 105 if (nodes[i].active && bit_pos < strlen(nodes[i].frame)) { 106 107 // 回读机制:比较发送值与总线实际值 108 109 if (nodes[i].frame[bit_pos] == '1' && bus == DOMINANT) { 110 111 // 仲裁失败:发送隐性位但读到显性位 112 113 nodes[i].active = false; 114 115 printf("位 %2d: 节点 0x%X 仲裁失败\n", bit_pos, nodes[i].id); 116 117 } 118 119 } 120 121 } 122 123 124 125 // 检查仲裁是否完成 126 127 int active_count = 0; 128 129 for (int i = 0; i < 3; i++) { 130 131 if (nodes[i].active) { 132 133 active_count++; 134 135 winner_id = nodes[i].id; 136 137 } 138 139 } 140 141 142 143 // 打印当前总线状态 144 145 printf("位 %2d: 总线状态: %s | 发送节点: ", 146 147 bit_pos, bus == DOMINANT ? "显性(0)" : "隐性(1)"); 148 149 150 151 for (int i = 0; i < 3; i++) { 152 153 if (nodes[i].active && bit_pos < strlen(nodes[i].frame)) { 154 155 printf("0x%X:%c ", nodes[i].id, nodes[i].frame[bit_pos]); 156 157 } 158 159 } 160 161 printf("\n"); 162 163 164 165 // 仲裁完成判断 166 167 if (!arbitration_complete && active_count == 1) { 168 169 printf("\n>>> 仲裁完成! 胜出节点: 0x%X <<<\n\n", winner_id); 170 171 arbitration_complete = true; 172 173 } 174 175 176 177 // 位之间延迟 178 179 usleep(300000); 180 181 } 182 183 184 185 printf("\n传输结束\n"); 186 187 } 188 189 int main() { 190 191 simulate_can_bus(); 192 193 return 0; 194 195 }

程序输出解析
运行上述程序会显示类似以下输出:
1 CAN总线仲裁模拟 - 节点ID: 0x123, 0x124, 0x122 2 3 位 0: 总线状态: 显性(0) | 发送节点: 0x123:0 0x124:0 0x122:0 4 5 位 1: 总线状态: 显性(0) | 发送节点: 0x123:0 0x124:0 0x122:0 6 7 位 2: 总线状态: 显性(0) | 发送节点: 0x123:0 0x124:0 0x122:0 8 9 位 3: 总线状态: 显性(0) | 发送节点: 0x123:1 0x124:1 0x122:1 10 11 位 4: 总线状态: 显性(0) | 发送节点: 0x123:0 0x124:0 0x122:0 12 13 位 5: 总线状态: 显性(0) | 发送节点: 0x123:0 0x124:0 0x122:0 14 15 位 6: 总线状态: 显性(0) | 发送节点: 0x123:1 0x124:1 0x122:1 16 17 位 7: 总线状态: 显性(0) | 发送节点: 0x123:0 0x124:0 0x122:0 18 19 位 8: 总线状态: 显性(0) | 发送节点: 0x123:0 0x124:0 0x122:0 20 21 位 9: 总线状态: 显性(0) | 发送节点: 0x123:0 0x124:1 0x122:0 22 23 位 10: 总线状态: 显性(0) | 发送节点: 0x123:1 0x124:0 0x122:1 24 25 位 11: 总线状态: 显性(0) | 发送节点: 0x122:0 26 27 位 12: 总线状态: 显性(0) | 发送节点: 0x122:0 28 29 位 13: 总线状态: 显性(0) | 发送节点: 0x122:0 30 31 传输结束

CAN总线波形图与仲裁过程
1 位位置: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 2 3 ----------------------------------------------------- 4 5 节点0x122: 0 0 0 1 0 0 1 0 0 0 1 0 0 0 6 7 节点0x123: 0 0 0 1 0 0 1 0 0 0 1 1 0 0 8 9 节点0x124: 0 0 0 1 0 0 1 0 0 1 0 0 0 0 10 11 ----------------------------------------------------- 12 13 总线状态: 0 0 0 1 0 0 1 0 0 0 1 0 0 0 14 15 ↑ ↑ ↑ ↑ 16 17 SOF | | 仲裁结束 18 19 差异点 仲裁失败点

关键机制解释:
- 线与特性:
- 当任何节点发送显性位(0)时,总线表现为显性(0)
- 只有当所有节点都发送隐性位(1)时,总线才表现为隐性(1)
- 非破坏性仲裁:
- 所有节点从SOF(显性位)开始同时发送
- 逐位比较ID(高位先发送)
- 在位9:节点0x123发送0(显性),节点0x124发送1(隐性),总线为0
- 节点0x124检测到自己发送1但总线为0 → 仲裁失败,停止发送
- 在位10:节点0x122发送1(隐性),节点0x123发送1(隐性),总线为1
- 在位11:节点0x122发送0(显性),节点0x123发送1(隐性) → 节点0x123仲裁失败
- 优先级规则:
- ID值越小优先级越高(0x122 < 0x123 < 0x124)
- 前8位所有节点ID相同(00100100)
- 第9位:0x122和0x123发送0(显性),0x124发送1(隐性) → 0x124失败
- 第10位:所有节点发送1(隐性) → 无冲突
- 第11位:0x122发送0(显性),0x123发送1(隐性) → 0x123失败
- 帧结构:
- 位0:SOF(帧起始),固定为显性(0)
- 位1-11:11位标识符(ID)
- 位12:RTR(远程传输请求),0表示数据帧
- 位13:IDE(标识符扩展),0表示标准帧
波形图详解:
1 位位置: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 2 3 ----------------------------------------------------- 4 5 总线波形: __ __ __ -- __ __ -- __ __ __ -- __ __ __ 6 7 0 0 0 1 0 0 1 0 0 0 1 0 0 0 8 9 图例: "__" = 显性(0), "--" = 隐性(1)

- SOF(位0):显性起始位,标志帧开始
- ID字段(位1-11):
- 前8位(位1-8):00100100 (所有节点相同)
- 位9:0(显性) → 淘汰ID高位为1的节点
- 位10:1(隐性) → 无冲突
- 位11:0(显性) → 淘汰发送1的节点
- 控制段(位12-13):RTR=0(数据帧),IDE=0(标准帧)
通过这个示例,可以看到CAN总线通过非破坏性仲裁实现多节点无冲突通信,优先级高的节点能够无损完成传输,而优先级低的节点会自动退出发送等待下次机会。
这里推荐江科大的CAN总线教学视频,我觉得讲得特别好
CAN总线入门教程-全面细致 面包板教学 多机通信_哔哩哔哩_bilibili
浙公网安备 33010602011771号