第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。这样互不干扰。

收发流程大致如下:

  1. 发送:检查总线状态 → 组装报文 → 写入发送邮箱 → 等待发送完成中断
  2. 接收:验收滤波器匹配 → 存入接收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怎么知道你要升级哪个扇区?