3、CAN通信基础:CAN协议帧结构、CAN收发器原理、AURIX的CAN模块(MultiCAN+)、CAN报文收发实战

好,咱们进入第三章。这一章是UDS诊断的基石——CAN通信。说实话,很多做UDS开发的工程师,CAN底层的细节反而成了短板。我个人习惯是,先把CAN通信吃透,再谈诊断协议。不然你连报文都发不出去,还谈什么诊断?

3.1 CAN协议帧结构——你每天都在用,但未必真懂

CAN总线上的数据,是以「帧」为单位传输的。咱们UDS诊断最常用的是数据帧,而且是标准帧(11位ID)。为什么不用扩展帧?因为诊断协议里,11位ID完全够用,而且兼容性更好。

一个标准数据帧长什么样?我直接给你拆开看:

字段 位数 说明
SOF 1 帧起始,显性电平,同步用
ID 11 标识符,决定优先级。数值越小优先级越高
RTR 1 远程帧标志。诊断里几乎不用远程帧
IDE 1 扩展帧标志。标准帧这里为显性
DLC 4 数据长度。诊断报文通常是8字节
Data 0-64 实际数据。UDS的请求/响应就放在这里
CRC 15 循环冗余校验。硬件自动计算
ACK 2 应答槽。发送节点发出,接收节点拉低
EOF 7 帧结束

这里有个关键点:ID决定了报文的优先级。我在项目中遇到过,两个ECU同时发报文,ID小的那个总能抢到总线。所以诊断报文一般用较低的ID,比如0x7DF(功能寻址)或0x7E0(物理寻址请求)。

重点记忆:UDS诊断报文的数据段,第一个字节通常是PCI(协议控制信息),后面才是真正的诊断数据。比如单帧请求:0x02 0x10 0x01,其中0x02表示后续有2个字节数据,0x10是诊断会话控制SID,0x01是子功能。

3.2 CAN收发器原理——从逻辑电平到差分信号

你想想看,MCU的CAN控制器输出的是逻辑电平(3.3V或5V),但CAN总线传输的是差分信号。这中间谁在干活?CAN收发器

常用的收发器有TJA1040、TJA1050,AURIX开发板上一般配的是TJA1043。它的核心功能就两个:

  • 发送时:把TX引脚的逻辑电平,转换成CAN_H和CAN_L的差分电压
  • 接收时:把CAN_H和CAN_L的差分电压,转换成RX引脚的逻辑电平

差分信号的好处很明显:抗干扰能力强。CAN_H比CAN_L高2V左右,代表显性(逻辑0);两者电压接近,代表隐性(逻辑1)。

我的经验:调试CAN通信时,示波器一定要看CAN_H和CAN_L的差分波形,别只看单端。我曾经被一个共模干扰问题折腾了两天,最后发现是收发器电源纹波太大。嗯,从那以后我习惯在收发器VCC脚上加个100nF的瓷片电容。

3.3 AURIX的CAN模块——MultiCAN+到底强在哪?

AURIX的CAN模块叫MultiCAN+,说白了就是一个多CAN控制器。以TC275为例,它有3个CAN节点,每个节点有128个消息对象(Message Object)。

为什么叫「Multi」?因为它支持:

  • 多个CAN节点独立运行:每个节点可以配置不同的波特率
  • 消息对象灵活分配:128个MO可以任意分配给不同节点
  • FIFO和队列模式:接收报文可以自动排队,不用CPU一个个搬

我个人最喜欢的是它的网关模式。两个CAN节点之间可以直接转发报文,CPU都不用参与。这在做CAN路由时特别爽。

寄存器方面,你重点掌握这几个:

寄存器 作用
MO_CTR 消息对象控制寄存器,配置方向、ID、数据长度
MO_DATA 数据寄存器,存放8字节报文数据
MO_FCR 功能控制寄存器,配置FIFO、中断等
NCR 节点控制寄存器,启动/停止CAN节点

注意:MultiCAN+的寄存器访问有严格的时序要求。写MO寄存器前,必须先置位MO_CTR的MSGVAL位。我见过新手直接写MO_DATA,结果数据根本没进去。正确的顺序是:置MSGVAL → 写配置 → 清MSGVAL → 等待同步。

3.4 CAN报文收发实战——手把手教你发第一帧

理论说完了,咱们来点实际的。下面这段代码,演示如何在AURIX TC275上发送一帧CAN报文:

// 初始化CAN节点0,波特率500kbps
void Can_Init(void)
{
    // 1. 使能CAN模块时钟
    SCU_GENERAL->PMSWCR0.B.CANCLKEN = 1;
    
    // 2. 配置CAN节点0的波特率
    // 假设系统时钟80MHz,分频后得到CAN时钟40MHz
    // 500kbps = 40MHz / (1 + BRP) / (TSEG1 + TSEG2 + 1)
    // 取BRP=3, TSEG1=12, TSEG2=3
    CAN_NODE0->NBTR.B.BRP = 3;    // 波特率预分频
    CAN_NODE0->NBTR.B.TSEG1 = 12; // 时间段1
    CAN_NODE0->NBTR.B.TSEG2 = 3;  // 时间段2
    
    // 3. 启动CAN节点
    CAN_NODE0->NCR.B.CCE = 1;     // 允许配置更改
    CAN_NODE0->NCR.B.CALM = 0;    // 正常模式
    CAN_NODE0->NCR.B.CCE = 0;     // 锁定配置
}

// 发送一帧标准CAN报文
void Can_SendMessage(uint16_t id, uint8_t *data, uint8_t len)
{
    uint32_t mo_num = 1;  // 使用消息对象1
    
    // 1. 配置消息对象为发送
    CAN_NODE0->MO[mo_num].MO_CTR.B.MSGVAL = 0;  // 先清除有效位
    CAN_NODE0->MO[mo_num].MO_CTR.B.MSG_DIR = 1; // 发送方向
    CAN_NODE0->MO[mo_num].MO_CTR.B.MSG_CFG = 0; // 标准帧
    CAN_NODE0->MO[mo_num].MO_AR.B.ID = id;      // 设置ID
    
    // 2. 写入数据
    CAN_NODE0->MO[mo_num].MO_DATA.B.DLC = len;  // 数据长度
    for(int i = 0; i < len; i++)
    {
        CAN_NODE0->MO[mo_num].MO_DATA.B.DATA[i] = data[i];
    }
    
    // 3. 触发发送
    CAN_NODE0->MO[mo_num].MO_CTR.B.MSGVAL = 1;  // 使能消息对象
    CAN_NODE0->MO[mo_num].MO_CTR.B.TXRQ = 1;    // 请求发送
    
    // 4. 等待发送完成(实际项目中建议用中断)
    while(CAN_NODE0->MO[mo_num].MO_STAT.B.TXRQ == 1);
}

// 使用示例:发送诊断请求 02 10 01
uint8_t diag_data[] = {0x02, 0x10, 0x01};
Can_SendMessage(0x7E0, diag_data, 3);

避坑指南:我曾经在发送完成后忘记清除TXRQ位,导致同一个报文被重复发送了无数次。正确的做法是:在发送完成中断里,手动清除TXRQ位,或者重新配置MO。另外,发送完成后记得把MSGVAL清掉,不然下次配置时会报错。

接收报文也类似,只是把MSG_DIR设为0(接收方向),然后配置好接收ID和掩码。MultiCAN+支持FIFO接收模式,多个报文可以自动排队,CPU只需要在中断里读取即可。我建议你一开始先用轮询方式调试,跑通了再切中断。

最后说一句:CAN通信调试,示波器是王道。别光看代码,拿示波器抓一下CAN_H和CAN_L的波形,看看波特率对不对,帧格式对不对。我每次调CAN,第一件事就是看波形,确认物理层没问题了,再查协议层。

好了,这一章就到这里。下一章咱们正式进入UDS诊断协议的核心——服务与子功能。到时候你会看到,刚才发的那些CAN报文,到底承载了什么样的诊断逻辑。