4. CAN通信协议栈:CAN硬件初始化、报文收发(中断模式与轮询模式)、CAN FD简介、UDS on CAN基础

好,咱们进入第四章。这一章聊的是CAN通信协议栈,说白了就是Bootloader的“嘴巴”——它怎么跟外界说话。

我个人习惯把CAN协议栈分成三层来看:硬件层、驱动层、应用层。硬件层管寄存器,驱动层管收发,应用层管UDS。咱们一层层剥开。

4.1 CAN硬件初始化——别小看这一步

很多新手上来就写收发函数,结果发现CAN总线死活没反应。为什么?初始化没做对。

TC3xx的CAN模块叫MultiCAN+,它内部有多个CAN节点(Node),每个Node独立工作。初始化时,我建议按这个顺序来:

  1. 时钟使能:先给CAN模块喂时钟,不然它“睡着”了。
  2. 引脚配置:把CAN_RX和CAN_TX对应的GPIO脚配成ALT功能。我记得有一次项目,板子回来CAN不通,查了半天发现是引脚复用没配对。
  3. 节点配置:选哪个Node,配波特率。TC3xx的波特率靠位时序寄存器(Bit Timing)算,公式是:Baud = fCAN / (BRP * (TSEG1 + TSEG2 + 1))
  4. 中断使能:如果用中断模式,这一步不能忘。
  5. 启动节点:最后写控制寄存器,让Node进入“运行态”。

核心要点:初始化完成后,一定要读状态寄存器确认Node是否真的进入了“Bus Off”或“Error Active”以外的正常状态。我曾经见过初始化代码跑完,Node还卡在“初始化态”,结果总线一直没信号。

代码示例(伪代码风格,实际寄存器名以TC3xx手册为准):

void Can_Init(void)
{
    /* 1. 时钟使能 */
    SCU_GATE->CLK_SET = SCU_GATE_CLK_CAN;

    /* 2. 引脚配置:P10.2 = CAN0_RX, P10.3 = CAN0_TX */
    PORT10->IOCR0 = 0x10;  // ALT1功能

    /* 3. 节点配置:CAN Node0, 500kbps */
    CAN_NODE0->NCR = 0x01;          // 进入初始化模式
    CAN_NODE0->NBTR = 0x001C0003;   // BRP=3, TSEG1=7, TSEG2=3
    CAN_NODE0->NCR = 0x02;          // 进入运行模式

    /* 4. 中断使能(可选) */
    CAN_NODE0->NIPR = 0x0001;       // 使能接收中断

    /* 5. 检查状态 */
    while(CAN_NODE0->NSR & 0x08);   // 等待初始化完成
}

小技巧:波特率计算时,我习惯把采样点设在75%~80%之间。TC3xx的位时序寄存器里,TSEG1和TSEG2的比例决定了采样点位置。比如500kbps,我常用BRP=3,TSEG1=7,TSEG2=3,采样点就在78%左右。

4.2 报文收发——中断模式 vs 轮询模式

收发报文是CAN通信的核心。TC3xx的MultiCAN+提供了两种方式:中断和轮询。怎么选?看场景。

4.2.1 中断模式

中断模式适合实时性要求高的场景。比如Bootloader里,你希望一收到UDS请求就立刻响应,而不是傻等。

实现思路:

  • 使能接收中断(RX Interrupt)。
  • 在中断服务函数(ISR)里读取报文,存入FIFO或队列。
  • 主循环里处理队列中的报文。

我曾经踩过一个坑:中断里直接调用UDS处理函数,结果处理时间太长,导致后续报文丢失。后来我改成“中断只收,主循环处理”,问题就解决了。

void CAN_ISR_Handler(void)
{
    uint32 msg_id;
    uint8 data[8];

    /* 读取报文 */
    msg_id = CAN_NODE0->MO->AR;    // 报文ID
    for(int i=0; i<8; i++) {
        data[i] = CAN_NODE0->MO->DB[i]; // 数据
    }

    /* 放入队列(不处理) */
    Queue_Push(msg_id, data);

    /* 清除中断标志 */
    CAN_NODE0->NIR = 0x01;
}

注意:中断里不要做复杂操作。比如动态内存分配、打印调试信息,这些都会拖慢中断响应。我见过有人中断里用printf,结果CAN总线直接超时。

4.2.2 轮询模式

轮询模式简单粗暴,适合调试或低负载场景。主循环里不断检查接收标志位。

代码示例:

void Can_PollingReceive(void)
{
    if(CAN_NODE0->NSR & 0x01) {  // 检查接收标志
        uint32 msg_id = CAN_NODE0->MO->AR;
        uint8 data[8];
        for(int i=0; i<8; i++) {
            data[i] = CAN_NODE0->MO->DB[i];
        }
        /* 处理报文 */
        UDS_Process(msg_id, data);
    }
}

轮询的缺点很明显:CPU一直占着,干不了别的。但好处是代码简单,调试时一眼能看出问题。我刚开始做Bootloader时,先用轮询把功能调通,再改成中断优化性能。

4.3 CAN FD简介——更快、更大

CAN FD(Flexible Data Rate)是CAN 2.0的升级版。说白了,它解决了两个痛点:

  • 速度更快:数据段可以跑到8Mbps(传统CAN最高1Mbps)。
  • 数据更多:一帧最多64字节(传统CAN只有8字节)。

TC3xx的MultiCAN+原生支持CAN FD。但要注意:CAN FD的硬件和传统CAN不完全兼容。你想想看,如果总线上既有CAN 2.0节点又有CAN FD节点,它们能通信吗?答案是不能直接通信。CAN FD节点可以发传统CAN帧,但反过来不行。

在Bootloader里用CAN FD的好处是:刷写速度更快。比如一个256KB的固件,用传统CAN每帧8字节,要发32768帧;用CAN FD每帧64字节,只要4096帧。时间直接省了87.5%。

实际经验:我在一个项目里把Bootloader从CAN 2.0升级到CAN FD,刷写时间从45秒降到了6秒。但代价是硬件成本增加——CAN FD收发器比普通收发器贵一点。

CAN FD的初始化跟传统CAN类似,只是位时序要配两套:仲裁段(Arbitration Phase)和数据段(Data Phase)。仲裁段通常用500kbps,数据段可以配到2Mbps或更高。

/* CAN FD位时序配置示例 */
CAN_NODE0->NBTR = 0x001C0003;   // 仲裁段:500kbps
CAN_NODE0->FBTR = 0x001C0001;   // 数据段:2Mbps (BRP=1)

4.4 UDS on CAN基础——Bootloader的灵魂

UDS(统一诊断服务)是应用层协议,它跑在CAN总线之上。Bootloader里,UDS负责接收刷写请求、擦除Flash、写入数据、校验完整性。

UDS on CAN的报文格式很简单:

  • CAN ID:通常用0x7E0(请求)和0x7E8(响应)。
  • 数据段:第一个字节是服务ID(SID),后面是参数。

举个例子,刷写时常用的服务:

服务ID 名称 作用
0x10 诊断会话控制 进入编程会话
0x27 安全访问 解锁刷写权限
0x34 请求下载 告诉ECU我要刷数据了
0x36 传输数据 实际发送固件数据
0x37 请求退出传输 结束本次传输
0x31 例程控制 执行擦除、校验等操作

UDS on CAN的底层实现,其实就是CAN报文的收发。你只需要把UDS的请求报文打包成CAN帧发出去,再把收到的CAN帧解析成UDS响应。

我建议在Bootloader里维护一个简单的UDS状态机:

  • 空闲态:等待请求。
  • 编程态:正在刷写。
  • 传输态:正在传输数据块。

每个状态对应不同的服务处理逻辑。比如在“传输态”下,收到0x36(传输数据)就写入Flash,收到0x37(请求退出)就结束传输。

避坑指南:我曾经在UDS处理里忘了检查“安全访问”状态,结果刷写过程中被恶意报文攻击,ECU直接变砖。后来我加了一个标志位,只有通过安全访问后才能执行刷写服务。嗯,这个坑踩得值。

好了,这一章的内容就这些。CAN初始化、收发模式、CAN FD、UDS on CAN,这四个点串起来就是Bootloader的通信骨架。下一章咱们聊Flash驱动——怎么把数据写进芯片里。