4. CAN通信栈配置:CAN驱动初始化、CAN报文收发、基于CAN的UDS传输层实现

好,咱们进入第四章。这一章可以说是整个Bootloader的「血管系统」——CAN通信栈。没有它,你后面写的UDS、刷写逻辑全是空中楼阁。

我个人习惯把CAN通信栈拆成三层来看:最底层的CAN驱动、中间的CAN接口层、以及上层的CAN传输层(也就是CAN TP)。说白了,就是让数据从物理总线,一路顺畅地跑到你的应用层手里。

4.1 CAN驱动初始化——别小看这几步

CAN驱动的初始化,很多人觉得就是调个函数完事。嗯,我刚开始也这么想,直到有一次在实车上,ECU死活不上总线……查了两天,发现是初始化顺序搞反了。

在AUTOSAR架构下,CAN驱动的初始化通常包含以下几个关键步骤:

  1. 硬件复位与时钟使能——先让CAN控制器从复位状态出来,再把时钟打开。这一步漏了,后面全白搭。
  2. 引脚配置——CAN_TX和CAN_RX引脚要配成复用功能。我见过有人配成GPIO输出,结果总线一直显性电平,整个网络都被他拉死了。
  3. 波特率设置——这个要跟整车网络一致。通常250kbps或500kbps。设置不对,你发出去的报文没人听得懂。
  4. 中断使能——接收中断、错误中断、状态改变中断。我个人建议至少把接收中断打开,轮询方式在Bootloader里不太靠谱。
  5. 启动CAN控制器——从初始化模式切换到正常运行模式。

核心要点:初始化完成后,一定要检查CAN控制器的状态寄存器。确认它真的进入了「正常运行模式」,而不是卡在初始化模式里。我曾经在一个项目里,芯片复位后CAN控制器状态位一直没置起来,结果就是发不出任何报文。

代码示例(简化版,基于AUTOSAR接口风格):

/* CAN驱动初始化函数 */
void Can_Init(const Can_ConfigType* Config)
{
    /* 1. 硬件复位 */
    Can_HwReset();

    /* 2. 配置引脚 */
    Can_PinMuxConfig();

    /* 3. 设置波特率 - 500kbps */
    Can_SetBaudrate(500000U);

    /* 4. 配置中断 */
    Can_EnableInterrupt(CAN_INTERRUPT_RX);

    /* 5. 启动CAN控制器 */
    Can_StartController();

    /* 6. 检查状态 - 我习惯加个超时等待 */
    while (Can_GetState() != CAN_STATE_READY)
    {
        /* 超时处理,防止死等 */
        if (timeout-- == 0U)
        {
            /* 上报错误 */
            Det_ReportError(CAN_MODULE_ID, CAN_INSTANCE_ID, CAN_E_INIT_FAILED);
            break;
        }
    }
}

小技巧:初始化时,我习惯先发一条「空报文」来验证硬件链路。比如发一个ID为0x000的数据帧,看能不能正常发送完成。这招帮我抓出过好几次硬件虚焊的问题。

4.2 CAN报文收发——别让数据卡在路上

CAN报文收发,听起来简单,但Bootloader场景下有几个坑要注意。

发送方面:

  • Bootloader里通常用轮询+中断混合模式。发送时先把报文写入发送邮箱,然后等待发送完成中断或轮询状态位。
  • 注意发送优先级。UDS诊断请求的响应报文,优先级要高于普通网络管理报文。你想想看,如果ECU响应诊断请求被别的报文堵住了,诊断仪那边就超时报错了。
  • 发送缓冲区要管理好。我遇到过发送邮箱满了,新报文写不进去,结果丢帧的情况。

接收方面:

  • 一定要用硬件过滤。CAN控制器都有接收过滤寄存器,把不关心的ID直接滤掉。不然CPU会被大量无关中断淹没。
  • 接收中断服务程序要短。我见过有人在中断里做UDS协议解析……那实时性简直灾难。正确做法是:中断里只把报文搬到接收队列,解析工作交给后台任务。
  • 注意接收超时。如果连续几秒收不到任何报文,可能是总线离线了,需要触发错误恢复机制。

警告:千万不要在CAN中断服务函数里调用printf或者写Flash!中断服务函数必须快进快出。我曾经接手过一个项目,前任工程师在中断里写日志到EEPROM,结果一收报文就卡死,整个ECU直接掉线。

代码示例(报文发送):

/* CAN报文发送函数 */
Std_ReturnType Can_Write(Can_HwHandleType Hth, const Can_PduType* PduInfo)
{
    Std_ReturnType ret = E_NOT_OK;

    /* 检查发送邮箱是否可用 */
    if (Can_CheckTxMailboxFree(Hth) == TRUE)
    {
        /* 写入硬件发送邮箱 */
        Can_WriteTxMailbox(Hth, PduInfo);

        /* 触发发送 */
        Can_TriggerTx(Hth);

        ret = E_OK;
    }
    else
    {
        /* 邮箱满,返回忙状态 */
        ret = CAN_BUSY;
    }

    return ret;
}

4.3 基于CAN的UDS传输层实现——这才是重头戏

UDS诊断协议跑在CAN上,靠的是CAN TP(Transport Protocol,传输协议)。ISO 15765-2里定义得清清楚楚。说白了,就是解决一个问题:UDS报文可能很长(比如刷写数据有4096字节),但CAN一帧只能发8字节,怎么办?

答案就是:分包发送,组包接收。

CAN TP的核心机制:

  • 单帧(SF):报文长度 ≤ 7字节时,一帧搞定。
  • 首帧(FF):报文长度 > 7字节时,第一帧告诉对方「我要发这么多数据」。
  • 连续帧(CF):后续数据一帧一帧发,每帧带序号。
  • 流控帧(FC):接收方告诉发送方「慢点,我处理不过来」或者「继续发」。

嗯,这里要注意:流控机制是CAN TP里最容易出问题的地方。接收方处理不过来时,会发FC帧要求发送方暂停。如果发送方不理睬,继续猛发,数据就会丢失。

实战经验:我在做Bootloader刷写时,遇到过一个问题:刷写到一半,ECU突然不响应了。查了半天,发现是发送方连续帧发得太快,接收方的缓冲区溢出了。后来在流控帧里加了「STmin(最小间隔时间)」参数,设为10ms,问题就解决了。所以,STmin不要设成0,除非你确定接收方处理能力足够强。

CAN TP的状态机:

状态 说明 常见问题
IDLE 空闲状态,等待接收或发送请求
SF_RX/TX 单帧接收/发送中 长度字段解析错误
FF_RX/TX 多帧接收/发送中,已收到/发送首帧 首帧长度与实际数据不符
CF_RX/TX 连续帧接收/发送中 序号错乱、丢帧、超时
WAIT_FC 发送方等待流控帧 流控帧丢失导致死等

代码示例(CAN TP接收状态机片段):

/* CAN TP接收状态机处理 */
void CanTp_RxStateMachine(CanTp_RxChannelType* channel, Can_PduType* frame)
{
    switch (channel->state)
    {
        case CANTP_RX_IDLE:
            /* 判断是单帧还是首帧 */
            if (frame->length & 0xF0) /* PCI类型判断 */
            {
                /* 首帧,进入多帧接收 */
                channel->state = CANTP_RX_FF;
                channel->expectedLen = ((frame->data[0] & 0x0F) << 8) | frame->data[1];
                channel->receivedLen = 0U;
                /* 发送流控帧 */
                CanTp_SendFlowControl(channel, CANTP_FC_CTS, 0U, 10U);
            }
            else
            {
                /* 单帧,直接回调上层 */
                channel->state = CANTP_RX_SF;
                CanTp_RxIndication(channel->id, frame);
                channel->state = CANTP_RX_IDLE;
            }
            break;

        case CANTP_RX_CF:
            /* 接收连续帧,检查序号 */
            if (frame->data[0] == channel->expectedSn)
            {
                /* 序号正确,拷贝数据 */
                CanTp_CopyData(channel, frame);
                channel->expectedSn++;
                /* 检查是否收完 */
                if (channel->receivedLen >= channel->expectedLen)
                {
                    /* 收完了,回调上层 */
                    CanTp_RxIndication(channel->id, &channel->pdu);
                    channel->state = CANTP_RX_IDLE;
                }
            }
            else
            {
                /* 序号错误,触发错误处理 */
                channel->state = CANTP_RX_ERROR;
            }
            break;

        /* 其他状态处理... */
    }
}

避坑指南:我曾经在实现CAN TP时,忘记处理「连续帧序号回绕」的情况。连续帧序号只有4位(0-15),发完15后下一帧序号应该是0。如果没处理好,接收方会认为序号错误而丢弃报文。这个bug藏得很深,直到做压力测试时才暴露出来。

4.4 小结一下

CAN通信栈的配置,说白了就是三件事:让CAN控制器能正常工作、让报文能正确收发、让长报文能可靠传输

我个人觉得,最容易出问题的环节反而是最基础的CAN初始化。很多人觉得简单,随便写写,结果硬件没起来都不知道。而CAN TP的实现,只要严格按照ISO 15765-2的状态机来写,一般不会有大问题。

下一章我们会讲UDS诊断协议栈的实现,到时候这些CAN通信栈的代码就会真正跑起来。你想想看,诊断请求进来,CAN TP拆包,UDS解析,然后执行刷写……整个链路就通了。

好,这一章就到这儿。记住:通信栈是基础,基础不牢,地动山摇