第4章:CAN通信协议基础
各位同学,今天我们来聊聊CAN通信。说实话,CAN总线在汽车电子里就像人的神经系统。没有它,ECU之间就是孤岛。我做了这么多年Bootloader,最深的体会就是——搞不定CAN,就别谈OTA升级。
4.1 CAN帧结构——报文长什么样?
CAN报文,说白了就是一串有规矩的二进制数据。标准帧有11位ID,扩展帧有29位ID。我个人习惯用标准帧,简单可靠。
一个完整的CAN帧包含这些部分:
- SOF(起始帧):1个显性位,告诉总线“我要发数据了”
- 仲裁场:11位ID + RTR位。ID越小优先级越高
- 控制场:IDE位 + 保留位 + DLC(数据长度码)
- 数据场:0-8字节,真正干活的地方
- CRC场:15位校验 + 1位界定符
- ACK场:2位,接收节点回应
- EOF:7个隐性位,帧结束
重点记忆:CAN帧最大数据长度是8字节。很多新手问我为什么不能发更多?这是协议规定的,想传大数据就得拆包。
我在项目中遇到过一个问题:某ECU发送的报文ID是0x123,另一个ECU发0x124。按理说0x123优先级更高,但实际测试发现0x124总是先发出去。查了半天,原来是ID位搞反了——CAN总线是显性位优先,二进制数值小的ID优先级高。0x123的二进制是000100100011,0x124是000100100100,你想想看,哪个小?
4.2 CAN报文收发——怎么发?怎么收?
CAN收发其实不复杂。发送时,节点先监听总线。总线空闲了,就发SOF。如果两个节点同时发,仲裁机制会决定谁赢。
接收呢?每个节点都有验收滤波器。只收自己感兴趣的ID。我刚开始做的时候,把验收滤波器设成了全通,结果CPU被CAN中断淹没了。嗯,这是个坑。
我的经验:验收滤波器一定要配好。一般做法是:Bootloader阶段用标准ID,应用层用扩展ID。这样互不干扰。
收发流程大致如下:
- 发送:检查总线状态 → 组装报文 → 写入发送邮箱 → 等待发送完成中断
- 接收:验收滤波器匹配 → 存入接收FIFO → 触发接收中断 → 读取数据
我曾经遇到一个诡异问题:OTA升级到一半,突然收不到应答了。排查发现是接收FIFO满了,但中断没处理。后来我在代码里加了FIFO溢出检测,再也没出过这问题。
4.3 CAN驱动开发要点——怎么写才靠谱?
写CAN驱动,说白了就是操作寄存器。不同MCU的CAN外设不一样,但核心逻辑相通。
我个人习惯这样组织驱动代码:
- 初始化层:配置波特率、工作模式、验收滤波器
- 发送层:提供发送接口,支持轮询和中断两种模式
- 接收层:注册回调函数,上层应用注册处理函数
- 错误处理层:总线关闭、错误被动等状态处理
这里给个简单的发送函数示例:
// CAN报文发送函数
uint8_t CAN_SendMessage(uint32_t id, uint8_t *data, uint8_t len)
{
// 检查长度
if (len > 8) return 0;
// 等待发送邮箱空闲
while (!(CAN->TSR & CAN_TSR_TME0));
// 填写报文
CAN->sTxMailBox->TIR = (id << 21) | CAN_TIR_TXRQ;
CAN->sTxMailBox->TDTR = len;
for (int i = 0; i < len; i++) {
CAN->sTxMailBox->TDLR = data[i] << (i * 8);
}
// 请求发送
CAN->sTxMailBox->TIR |= CAN_TIR_TXRQ;
return 1;
}
注意:实际项目中,发送函数要考虑超时处理。我曾经见过一个ECU,因为总线被拉死,发送函数一直卡在while循环里,导致整个系统挂掉。加个超时判断,保平安。
接收中断处理也有讲究:
void CAN_RX_IRQHandler(void)
{
uint32_t id;
uint8_t data[8];
uint8_t len;
// 读取接收FIFO
if (CAN->RF0R & CAN_RF0R_FMP0) {
id = (CAN->sFIFOMailBox->RIR >> 21) & 0x7FF;
len = CAN->sFIFOMailBox->RDTR & 0x0F;
// 读取数据
uint32_t dlr = CAN->sFIFOMailBox->RDLR;
uint32_t dhr = CAN->sFIFOMailBox->RDHR;
for (int i = 0; i < len; i++) {
data[i] = (i < 4) ? (dlr >> (i * 8)) : (dhr >> ((i-4) * 8));
}
// 释放FIFO
CAN->RF0R |= CAN_RF0R_RFOM0;
// 回调上层处理
CAN_MessageCallback(id, data, len);
}
}
你想想看,中断里做太多事会怎样?对,影响实时性。所以我建议中断里只做数据搬运,具体业务逻辑放到任务里处理。
4.4 CAN网络管理基础——节点怎么协同?
CAN网络管理,说白了就是让各个ECU知道彼此的状态。汽车上几十个ECU,谁醒了谁睡了,总得有个规矩。
常见的网络管理机制有两种:
- OSEK NM:基于环网,节点轮流发管理报文
- AUTOSAR NM:基于广播,每个节点独立发管理报文
我个人更倾向AUTOSAR NM,灵活,适合OTA场景。Bootloader升级时,需要让其他ECU保持唤醒状态,不能让他们睡过去。
网络管理报文一般长这样:
| 字节 | 含义 | 说明 |
|---|---|---|
| 0 | 源节点ID | 发送方标识 |
| 1 | 控制位 | 唤醒/休眠/保持 |
| 2-7 | 用户数据 | 可自定义 |
关键点:OTA升级期间,Bootloader必须持续发送网络管理报文。否则其他ECU以为总线空闲了,进入休眠模式,升级就断了。
我记得有一次做台架测试,升级到一半突然失败。查日志发现是目标ECU进入了休眠。原因就是Bootloader没发网络管理报文,ECU的看门狗超时后触发了休眠。从那以后,我在Bootloader初始化阶段就启动了网络管理定时发送。
网络管理的状态机也不复杂:
- 初始化:上电后进入,等待网络就绪
- 唤醒:收到唤醒报文或本地事件触发
- 正常运行:周期性发送管理报文
- 准备休眠:停止发送,等待其他节点确认
- 休眠:低功耗模式
做OTA升级时,Bootloader要强制让网络保持在“正常运行”状态。我一般会在升级开始前发一个“保持唤醒”的广播报文,升级完成后发“允许休眠”。
避坑指南:我曾经在升级完成后忘记发“允许休眠”,结果ECU一直醒着,静态电流超标。后来在测试规范里加了一条:升级完成后必须验证网络状态。
好了,CAN通信基础就讲到这里。下一章我们聊聊UDS协议,这是Bootloader和诊断工具沟通的语言。你想想看,没有UDS,Bootloader怎么知道你要升级哪个扇区?