入行十年,CAN总线是我接触最多的车载通信协议。从最早在4S店用诊断仪读故障码,到后来自己写飞控的CAN驱动,再到现在做整车电子架构,对CAN的理解也在不断加深。这篇文章我会把CAN总线的方方面面都讲透,争取让看完的人能真正理解CAN是怎么工作的,而不只是会用。
前言
CAN(Controller Area Network)是目前汽车电子中使用最广泛的通信协议,几乎所有的车载ECU都通过CAN总线互联。这篇文章会从最底层的物理信号讲起,一直到上层的应用协议,配合大量代码示例。
一、CAN总线概述与发展历史
1.1 为什么需要CAN总线
在CAN出现之前,汽车电子系统是怎样的?我见过80年代的老车,ECU之间通信靠的是点对点连线。一个信号从A传到B,就拉一根线;B再传给C,再拉一根线。
问题很明显:
- 线束爆炸:一辆现代汽车有上百个ECU,点对点连接需要的线束重量能达到几十公斤
- 可靠性差:接点多,故障点就多
- 扩展困难:加个新功能就得重新布线
- 成本高:铜线贵,工时也贵
80年代初,博世(Bosch)的工程师们开始琢磨这个问题。他们的思路是:能不能用一条总线把所有节点串起来,大家共享这条通信线路?
这就是CAN的由来。
1.2 发展历程
| 年份 | 里程碑 |
|---|---|
| 1983 | 博世开始CAN项目研发 |
| 1986 | CAN协议在SAE大会首次公开发表 |
| 1987 | Intel发布第一款CAN控制器芯片82526 |
| 1991 | 奔驰W140(S级)首次量产搭载CAN总线 |
| 1993 | ISO 11898标准发布 |
| 2003 | ISO 11898-1/2/3/4标准体系完善 |
| 2012 | CAN FD(Flexible Data-rate)发布 |
| 2015 | CAN FD写入ISO 11898-1 |
1.3 CAN的技术特点
CAN能成功不是偶然的,它的设计哲学很适合汽车这种应用场景:
多主架构(Multi-Master)
没有主从之分,任何节点都可以主动发送数据。这对汽车太重要了------你不能让发动机ECU等着某个主节点轮询才能报警。
消息导向(Message-Oriented)
CAN不关心"谁发的",只关心"发的是什么"。每条消息有个ID,接收方根据ID决定要不要收。
非破坏性仲裁(Non-Destructive Arbitration)
多个节点同时发送时,ID小的优先,ID大的自动退让。整个过程不会破坏任何数据。
强大的错误处理
CAN的错误检测能力覆盖率超过99.99%。
二、物理层深度解析
2.1 高速CAN(ISO 11898-2)
这是汽车上用得最多的CAN物理层标准。
电气特性
高速CAN用差分信号传输,两根线分别叫CAN_H(High)和CAN_L(Low):
显性电平(Dominant,逻辑0):
CAN_H = 3.5V (典型值,范围2.75V~4.5V)
CAN_L = 1.5V (典型值,范围0.5V~2.25V)
差分电压 Vdiff = CAN_H - CAN_L = 2.0V
隐性电平(Recessive,逻辑1):
CAN_H = 2.5V (典型值,范围2.0V~3.0V)
CAN_L = 2.5V (典型值,范围2.0V~3.0V)
差分电压 Vdiff = 0V
共模电压 Vcom = (CAN_H + CAN_L) / 2 = 2.5V
为什么用差分信号?抗干扰能力强。外界干扰通常是共模的,差分接收时会被抵消。
总线拓扑
高速CAN要求线性拓扑(总线型),不支持星型或树型:
正确的拓扑(线性):
┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
│Node1│ │Node2│ │Node3│ │Node4│
└──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘
│ │ │ │
═══╧═════════╧═════════╧═════════╧═══ CAN总线
120Ω 120Ω
终端电阻 终端电阻
终端电阻
高速CAN总线两端必须各接一个120Ω电阻。两个120Ω并联等于60Ω。
怎么判断终端电阻是否正确?用万用表测CAN_H和CAN_L之间的电阻:
- 正常:约60Ω(两个120Ω并联)
- 缺一个电阻:120Ω
- 都没有:开路
- 短接:接近0Ω
传输距离与速率
| 波特率 | 最大总线长度 | 典型应用 |
|---|---|---|
| 1 Mbps | 25m | 实验室、短距离 |
| 500 kbps | 100m | 动力总成 |
| 250 kbps | 250m | 底盘 |
| 125 kbps | 500m | 车身 |
2.2 CAN收发器
收发器(Transceiver)是连接CAN控制器和物理总线的桥梁。
常用芯片:
| 芯片 | 厂商 | 类型 | 特点 |
|---|---|---|---|
| TJA1050 | NXP | 高速 | 经典款,便宜 |
| TJA1042 | NXP | 高速 | 带静默模式 |
| MCP2551 | Microchip | 高速 | 兼容5V和3.3V |
| SN65HVD230 | TI | 高速 | 3.3V供电 |
三、数据链路层详解
3.1 帧类型
CAN定义了四种帧类型:
数据帧(Data Frame)
用于传输数据,是最常用的帧类型。
数据帧结构(标准格式):
┌───┬──────────┬───┬───┬───┬──────┬────────────────┬──────────┬─────┬─────┬───────┐
│SOF│Identifier│RTR│IDE│r0 │ DLC │ Data Field │ CRC │ ACK │ EOF │ IFS │
│ │ 11bit │ │ │ │ 4bit │ 0-8 bytes │ 15bit │ 2bit│ 7bit│ 3bit │
└───┴──────────┴───┴───┴───┴──────┴────────────────┴──────────┴─────┴─────┴───────┘
1b 11b 1b 1b 1b 4b 0-64 bits 15+1b 2b 7b ≥3b
SOF: Start of Frame,帧起始,固定为显性(0)
Identifier: 标识符,决定消息优先级和内容含义
RTR: Remote Transmission Request,0=数据帧,1=远程帧
IDE: Identifier Extension,0=标准格式,1=扩展格式
r0: 保留位,固定为显性(0)
DLC: Data Length Code,数据长度,0-8
Data: 数据字段,0-8字节
CRC: 循环冗余校验,15位+1位界定符
ACK: 应答域,2位(1位槽+1位界定符)
EOF: End of Frame,帧结束,7个隐性位
IFS: Interframe Space,帧间隔,至少3个隐性位
3.2 非破坏性仲裁
当多个节点同时发送时,CAN用"线与"机制进行仲裁:
-
显性电平(0)会覆盖隐性电平(1)
-
所有节点都发隐性时,总线才是隐性
假设三个节点同时发送,ID分别是:
Node A: 0x100 = 0001 0000 0000
Node B: 0x123 = 0001 0010 0011
Node C: 0x0FF = 0000 1111 1111仲裁过程(从高位到低位):
位位置: 10 9 8 7 6 5 4 3 2 1 0
────────────────────────────────────────────
Node A: 0 0 0 1 0 0 0 0 0 0 0
Node B: 0 0 0 1 0 0 1 0 0 1 1
Node C: 0 0 0 0 1 1 1 1 1 1 1
────────────────────────────────────────────
总线: 0 0 0 0 ...(Node C获胜)
↑
Node A、B检测到自己发1但总线是0
→ 仲裁失败,停止发送,等待重试结果:ID最小的Node C(0x0FF)赢得仲裁
3.3 位填充(Bit Stuffing)
CAN用位填充来保证足够的信号跳变,用于时钟同步。
规则:发送5个连续相同的位后,自动插入一个相反的位
原始数据: 11111 00000 111 00 1111111
填充后: 111110 000001 111 00 111110 11
↑ ↑ ↑
插入0 插入1 插入0
接收方自动去除填充位,恢复原始数据
3.4 CRC校验
CAN使用15位CRC,生成多项式是:x^15 + x^14 + x^10 + x^8 + x^7 + x^4 + x^3 + 1
c
// CAN CRC-15计算
uint16_t can_crc15(uint8_t *data, int bits)
{
uint16_t crc = 0;
uint16_t polynomial = 0x4599;
for(int i = 0; i < bits; i++) {
int byte_idx = i / 8;
int bit_idx = 7 - (i % 8);
uint8_t bit = (data[byte_idx] >> bit_idx) & 0x01;
uint16_t crc_next = ((crc << 1) | bit) & 0x7FFF;
if(crc & 0x4000) {
crc_next ^= polynomial;
}
crc = crc_next;
}
return crc;
}
四、位时序与同步机制
4.1 位时间结构
CAN是异步通信,没有独立的时钟线,靠位同步来保持收发一致。
一个位时间分成四段:
一个位时间(Bit Time)
├─────────────────────────────────────────────┤
│ SYNC_SEG │ PROP_SEG │ PHASE_SEG1 │ PHASE_SEG2 │
│ 1 Tq │ 1-8 Tq │ 1-8 Tq │ 1-8 Tq │
│ │ │ ↑ │ │
│ │ │ 采样点 │ │
采样点位置很关键,一般设在75%-80%。
4.2 波特率计算
波特率 = f_clk / (BRP × (SYNC + PROP + PHASE1 + PHASE2))
c
// 波特率配置计算示例
// 假设CAN时钟 = 36MHz,目标波特率 = 500kbps
// Tq数 = 36MHz / 500kbps / BRP
// 设BRP = 4,则Tq数 = 36M / 500k / 4 = 18
// 分配:SYNC=1, PROP=5, PHASE1=6, PHASE2=6
// 采样点 = (1+5+6)/18 = 66.7%
typedef struct {
uint16_t brp; // 预分频 1-1024
uint8_t sjw; // 同步跳转宽度 1-4
uint8_t ts1; // PROP + PHASE1
uint8_t ts2; // PHASE2
} CAN_BitTiming_t;
// 500kbps配置
CAN_BitTiming_t timing_500k = {
.brp = 4,
.sjw = 1,
.ts1 = 11, // PROP(5) + PHASE1(6)
.ts2 = 6
};
五、错误处理机制
5.1 错误类型
CAN定义了五种错误:
| 错误类型 | 说明 |
|---|---|
| 位错误 | 发送的位和监测到的不一致 |
| 填充错误 | 检测到6个连续相同的位 |
| CRC错误 | CRC校验失败 |
| 格式错误 | 固定格式位域检测到非法值 |
| ACK错误 | 没有收到应答 |
5.2 错误计数器
每个CAN控制器维护两个错误计数器:
- TEC(Transmit Error Counter):发送错误计数
- REC(Receive Error Counter):接收错误计数
5.3 错误状态
TEC或REC > 127
┌──────────┐ ────────────────→ ┌───────────────┐
│ Error │ │ Error │
│ Active │ ←──────────────── │ Passive │
│ │ TEC和REC < 128 │ │
└──────────┘ └───────┬───────┘
│ TEC > 255
↓
┌───────────────┐
│ Bus Off │
└───────────────┘
六、CAN控制器软件实现
6.1 发送函数
c
typedef struct {
uint32_t id;
uint8_t ide; // 0=标准帧,1=扩展帧
uint8_t rtr; // 0=数据帧,1=远程帧
uint8_t dlc;
uint8_t data[8];
} CAN_TxMessage_t;
int CAN_Transmit(CAN_TypeDef *can, CAN_TxMessage_t *msg)
{
// 查找空闲邮箱
uint8_t mailbox = 0xFF;
uint32_t tsr = can->TSR;
if(tsr & CAN_TSR_TME0) mailbox = 0;
else if(tsr & CAN_TSR_TME1) mailbox = 1;
else if(tsr & CAN_TSR_TME2) mailbox = 2;
else return -1;
CAN_TxMailBox_TypeDef *txbox = &can->sTxMailBox[mailbox];
// 设置ID
if(msg->ide) {
txbox->TIR = (msg->id << 3) | CAN_TI0R_IDE;
} else {
txbox->TIR = (msg->id << 21);
}
if(msg->rtr) txbox->TIR |= CAN_TI0R_RTR;
// 设置DLC和数据
txbox->TDTR = msg->dlc & 0x0F;
txbox->TDLR = ((uint32_t)msg->data[3] << 24) |
((uint32_t)msg->data[2] << 16) |
((uint32_t)msg->data[1] << 8) |
msg->data[0];
txbox->TDHR = ((uint32_t)msg->data[7] << 24) |
((uint32_t)msg->data[6] << 16) |
((uint32_t)msg->data[5] << 8) |
msg->data[4];
// 请求发送
txbox->TIR |= CAN_TI0R_TXRQ;
return mailbox;
}
6.2 接收滤波配置
c
typedef struct {
uint32_t filter_id;
uint32_t filter_mask;
uint32_t filter_fifo;
uint8_t filter_bank;
uint8_t filter_mode; // 0=掩码模式,1=列表模式
uint8_t filter_scale; // 0=16位,1=32位
uint8_t filter_activation;
} CAN_FilterConfig_t;
void CAN_ConfigFilter(CAN_TypeDef *can, CAN_FilterConfig_t *config)
{
// 进入滤波器初始化模式
can->FMR |= CAN_FMR_FINIT;
// 关闭滤波器
can->FA1R &= ~(1 << config->filter_bank);
// 设置滤波器模式
if(config->filter_mode)
can->FM1R |= (1 << config->filter_bank);
else
can->FM1R &= ~(1 << config->filter_bank);
// 设置滤波器位宽
if(config->filter_scale)
can->FS1R |= (1 << config->filter_bank);
else
can->FS1R &= ~(1 << config->filter_bank);
// 设置滤波器关联的FIFO
if(config->filter_fifo)
can->FFA1R |= (1 << config->filter_bank);
else
can->FFA1R &= ~(1 << config->filter_bank);
// 设置滤波器值
can->sFilterRegister[config->filter_bank].FR1 = config->filter_id;
can->sFilterRegister[config->filter_bank].FR2 = config->filter_mask;
// 启用滤波器
if(config->filter_activation)
can->FA1R |= (1 << config->filter_bank);
// 退出滤波器初始化模式
can->FMR &= ~CAN_FMR_FINIT;
}
6.3 接收处理
c
typedef struct {
uint32_t id;
uint8_t ide;
uint8_t rtr;
uint8_t dlc;
uint8_t data[8];
uint8_t fmi;
uint32_t timestamp;
} CAN_RxMessage_t;
int CAN_Receive(CAN_TypeDef *can, uint8_t fifo, CAN_RxMessage_t *msg)
{
CAN_FIFOMailBox_TypeDef *rxbox;
uint32_t rfr;
if(fifo == 0) {
rfr = can->RF0R;
rxbox = &can->sFIFOMailBox[0];
} else {
rfr = can->RF1R;
rxbox = &can->sFIFOMailBox[1];
}
// 检查FIFO是否有消息
if((rfr & 0x03) == 0) return -1;
// 读取ID
uint32_t rir = rxbox->RIR;
msg->ide = (rir & CAN_RI0R_IDE) ? 1 : 0;
msg->rtr = (rir & CAN_RI0R_RTR) ? 1 : 0;
if(msg->ide)
msg->id = (rir >> 3) & 0x1FFFFFFF;
else
msg->id = (rir >> 21) & 0x7FF;
// 读取DLC和数据
uint32_t rdtr = rxbox->RDTR;
msg->dlc = rdtr & 0x0F;
msg->fmi = (rdtr >> 8) & 0xFF;
msg->timestamp = (rdtr >> 16) & 0xFFFF;
uint32_t rdlr = rxbox->RDLR;
uint32_t rdhr = rxbox->RDHR;
msg->data[0] = rdlr & 0xFF;
msg->data[1] = (rdlr >> 8) & 0xFF;
msg->data[2] = (rdlr >> 16) & 0xFF;
msg->data[3] = (rdlr >> 24) & 0xFF;
msg->data[4] = rdhr & 0xFF;
msg->data[5] = (rdhr >> 8) & 0xFF;
msg->data[6] = (rdhr >> 16) & 0xFF;
msg->data[7] = (rdhr >> 24) & 0xFF;
// 释放FIFO
if(fifo == 0)
can->RF0R |= CAN_RF0R_RFOM0;
else
can->RF1R |= CAN_RF1R_RFOM1;
return 0;
}
七、消息调度器实现
c
#define MAX_TX_MESSAGES 32
#define MAX_RX_MESSAGES 64
typedef struct {
uint32_t id;
uint8_t dlc;
uint16_t cycle_ms;
uint16_t offset_ms;
bool enabled;
void (*pack_func)(uint8_t *data);
} TxMessageConfig_t;
typedef struct {
uint32_t id;
uint16_t timeout_ms;
bool enabled;
void (*unpack_func)(const uint8_t *data, uint8_t dlc);
void (*timeout_func)(void);
} RxMessageConfig_t;
typedef struct {
TxMessageConfig_t config;
uint32_t last_tx_time;
uint32_t tx_count;
} TxMessageState_t;
typedef struct {
RxMessageConfig_t config;
uint32_t last_rx_time;
uint32_t rx_count;
bool timeout_flag;
bool received_once;
} RxMessageState_t;
typedef struct {
TxMessageState_t tx_messages[MAX_TX_MESSAGES];
uint8_t tx_count;
RxMessageState_t rx_messages[MAX_RX_MESSAGES];
uint8_t rx_count;
uint32_t current_time;
} CAN_Scheduler_t;
// 周期调度
void CAN_Scheduler_Tick(CAN_Scheduler_t *scheduler, uint32_t current_time_ms)
{
scheduler->current_time = current_time_ms;
// 处理发送
for(int i = 0; i < scheduler->tx_count; i++) {
TxMessageState_t *tx = &scheduler->tx_messages[i];
if(!tx->config.enabled) continue;
uint32_t elapsed = current_time_ms - tx->last_tx_time;
if(elapsed >= tx->config.cycle_ms) {
uint8_t data[8] = {0};
if(tx->config.pack_func)
tx->config.pack_func(data);
CAN_TxMessage_t msg = {
.id = tx->config.id,
.ide = 0,
.rtr = 0,
.dlc = tx->config.dlc
};
memcpy(msg.data, data, tx->config.dlc);
if(CAN_Transmit(CAN1, &msg) >= 0) {
tx->last_tx_time = current_time_ms;
tx->tx_count++;
}
}
}
// 处理接收超时
for(int i = 0; i < scheduler->rx_count; i++) {
RxMessageState_t *rx = &scheduler->rx_messages[i];
if(!rx->config.enabled || !rx->received_once) continue;
uint32_t elapsed = current_time_ms - rx->last_rx_time;
if(elapsed > rx->config.timeout_ms && !rx->timeout_flag) {
rx->timeout_flag = true;
if(rx->config.timeout_func)
rx->config.timeout_func();
}
}
}
八、ISO-TP传输层
ISO 15765-2定义了CAN上的传输层协议,用于传输超过8字节的数据:
c
// 帧类型
typedef enum {
ISOTP_SF = 0, // Single Frame
ISOTP_FF = 1, // First Frame
ISOTP_CF = 2, // Consecutive Frame
ISOTP_FC = 3 // Flow Control
} ISOTP_FrameType_t;
// 发送数据
int ISOTP_Transmit(ISOTP_Session_t *session, const uint8_t *data, uint16_t len)
{
if(len == 0 || session->state != ISOTP_IDLE) return -1;
memcpy(session->tx_buffer, data, len);
session->tx_len = len;
session->tx_offset = 0;
session->tx_sn = 1;
uint8_t frame[8] = {0};
if(len <= 7) {
// 单帧
frame[0] = (ISOTP_SF << 4) | len;
memcpy(&frame[1], data, len);
session->can_send(session->config.tx_id, frame, 8);
} else {
// 首帧
frame[0] = (ISOTP_FF << 4) | ((len >> 8) & 0x0F);
frame[1] = len & 0xFF;
memcpy(&frame[2], data, 6);
session->tx_offset = 6;
session->state = ISOTP_WAIT_FC;
session->can_send(session->config.tx_id, frame, 8);
}
return 0;
}
九、CAN FD简介
CAN FD是CAN 2.0的升级版:
| 特性 | CAN 2.0 | CAN FD |
|---|---|---|
| 数据场波特率 | 最高1Mbps | 最高8Mbps |
| 数据长度 | 0-8字节 | 0-64字节 |
| CRC | 15位 | 17/21位 |
c
// CAN FD DLC映射
uint8_t dlc_to_bytes(uint8_t dlc)
{
const uint8_t map[] = {0,1,2,3,4,5,6,7,8,12,16,20,24,32,48,64};
return map[dlc & 0x0F];
}
uint8_t bytes_to_dlc(uint8_t bytes)
{
if(bytes <= 8) return bytes;
if(bytes <= 12) return 9;
if(bytes <= 16) return 10;
if(bytes <= 20) return 11;
if(bytes <= 24) return 12;
if(bytes <= 32) return 13;
if(bytes <= 48) return 14;
return 15;
}
十、调试与故障排查
10.1 常见问题排查
| 问题 | 可能原因 | 排查方法 |
|---|---|---|
| 完全不通信 | 接线错误、终端电阻、波特率 | 万用表测电阻,示波器看波形 |
| 偶发丢帧 | 干扰、采样点、总线负载 | 调整采样点,计算负载率 |
| Bus Off | 严重错误累积 | 检查物理层,实现自动恢复 |
10.2 调试代码
c
void CAN_DebugPrint(const char *prefix, uint32_t id, bool is_ext,
const uint8_t *data, uint8_t dlc)
{
printf("%s %s 0x%08X [%d] ", prefix, is_ext ? "EXT" : "STD", id, dlc);
for(int i = 0; i < dlc; i++)
printf("%02X ", data[i]);
printf("\n");
}
void CAN_PrintStatus(CAN_TypeDef *can)
{
uint32_t esr = can->ESR;
uint8_t tec = (esr >> 16) & 0xFF;
uint8_t rec = (esr >> 24) & 0xFF;
printf("TEC: %d, REC: %d, State: ", tec, rec);
if(esr & CAN_ESR_BOFF) printf("BUS OFF\n");
else if(esr & CAN_ESR_EPVF) printf("Error Passive\n");
else if(esr & CAN_ESR_EWGF) printf("Error Warning\n");
else printf("OK\n");
}
总结
这篇文章从物理层到应用层,把CAN总线的方方面面都讲了一遍。核心要点:
- 物理层:差分信号、终端电阻、收发器选型
- 数据链路层:帧结构、非破坏性仲裁、位填充
- 位时序:采样点配置是通信稳定的关键
- 错误处理:五种错误类型、三种错误状态
- 软件协议栈:分层架构、消息调度、ISO-TP传输
CAN虽然是30多年前的技术了,但在汽车电子领域依然是主力。希望这篇文章对你有帮助。
参考资料:
- ISO 11898-1:2015 CAN数据链路层和物理信令
- ISO 11898-2:2016 CAN高速物理层
- ISO 15765-2 诊断通信传输层
- Bosch CAN Specification 2.0
- STM32F4 参考手册 - bxCAN章节
这篇文章写了好几个晚上,希望能帮到正在学习CAN的朋友。有问题欢迎评论区讨论!