第4章:数据链路层——CAN协议帧格式与驱动开发

各位同学,欢迎来到第四章。这一章我们聊聊CAN总线,这是TBOX通信的基石。说实话,CAN协议看起来简单,但坑不少。我当年刚接触TBOX开发时,就在CAN ID过滤上栽过跟头,板子一上电就疯狂丢帧,查了三天才发现是掩码配错了。

好,咱们正式开始。

4.1 CAN协议帧格式:标准帧 vs 扩展帧

CAN总线上的数据,是以“帧”为单位传输的。最常见的两种帧:数据帧和远程帧。咱们先看数据帧,它又分标准帧和扩展帧。

标准帧(CAN 2.0A):ID长度11位。说白了,就是给每个消息一个编号,范围0~0x7FF。我习惯把标准帧叫“短ID”,够用但不够灵活。

扩展帧(CAN 2.0B):ID长度29位。范围0~0x1FFFFFFF。为什么需要这么长?因为现代汽车电子系统太复杂了,ECU多、信号多,11位ID根本不够分。你想想看,一个高端车型可能有上百个ECU,每个ECU发几十条消息,标准帧的ID空间很快就用完了。

两种帧的结构对比如下:

字段 标准帧 扩展帧
SOF 1 bit 1 bit
仲裁场(ID) 11 bit 29 bit
控制场(IDE位) 0(显性) 1(隐性)
数据场 0~8 byte 0~8 byte
CRC场 15 bit 15 bit
ACK场 2 bit 2 bit
EOF 7 bit 7 bit

这里有个关键点:IDE位(Identifier Extension)。标准帧的IDE是显性(0),扩展帧的IDE是隐性(1)。CAN控制器就是靠这个位来区分两种帧的。

重要提醒:标准帧和扩展帧虽然ID长度不同,但可以共存于同一总线上。不过要注意,标准帧的优先级永远高于扩展帧——因为IDE位在仲裁时,显性位会赢得总线。

4.2 CAN ID过滤机制

CAN总线是广播式的,所有节点都能收到所有消息。但你的TBOX可能只关心其中几十个ID。怎么办?靠硬件过滤。

CAN控制器内部有过滤寄存器,可以设置一组“白名单”。只有匹配的ID才能进入接收缓冲区,不匹配的直接丢弃。这样做的好处是:减轻CPU负担,避免中断风暴。

过滤模式主要有两种:

  • 掩码模式(Mask Mode):设置一个ID掩码,掩码位为1的位必须匹配,为0的位忽略。举个例子:掩码0x7F0,ID 0x123和0x133都能通过,因为低4位被忽略了。
  • 列表模式(List Mode):直接列出允许通过的ID列表。简单粗暴,但寄存器数量有限(通常4~8个)。

我建议在实际项目中,优先使用掩码模式。为什么?因为列表模式太死板了。我曾经在一个项目中,需要接收0x100~0x1FF共256个ID,如果用列表模式,得配256个寄存器,根本不够用。但用掩码模式,一个掩码0x700就搞定了。

实战技巧:配置掩码时,记得把IDE位也考虑进去。如果你只想接收扩展帧,就把IDE位对应的掩码设为1。否则,标准帧和扩展帧会混在一起,解析时容易出错。

4.3 CAN收发器驱动开发

CAN控制器负责协议处理,但物理层信号需要收发器来完成。收发器把CAN控制器的数字信号,转换成CANH/CANL上的差分信号。

驱动开发的核心,就是配置收发器的工作模式。常见的模式有:

  • 正常模式:收发都工作,正常通信。
  • 静默模式:只收不发,用于监听总线。
  • 待机模式:低功耗,唤醒后恢复。
  • 休眠模式:功耗最低,需要特定唤醒信号。

以常用的TJA1040为例,它的STB引脚控制模式:

// TJA1040 模式控制
#define TJA1040_NORMAL_MODE  0  // STB = 0
#define TJA1040_STANDBY_MODE 1  // STB = 1

void tja1040_set_mode(uint8_t mode) {
    if (mode == TJA1040_NORMAL_MODE) {
        HAL_GPIO_WritePin(STB_GPIO_Port, STB_Pin, GPIO_PIN_RESET);
    } else {
        HAL_GPIO_WritePin(STB_GPIO_Port, STB_Pin, GPIO_PIN_SET);
    }
}

嗯,这里要注意:收发器的初始化顺序很重要。我习惯先配置GPIO,再初始化CAN控制器,最后使能收发器。顺序反了,可能会在总线上产生毛刺信号,干扰其他节点。

避坑指南:我曾经在量产阶段遇到一个问题——TBOX上电后,总线偶尔出现连续错误帧。排查了三天,最后发现是收发器的VIO引脚供电时序问题。CAN控制器先初始化了,但收发器还没准备好,导致输出引脚处于不确定状态。解决方案:在收发器初始化代码中增加100ms延时,确保电源稳定。

4.4 CAN总线错误处理

CAN总线不是完美的,错误总会发生。但CAN协议本身有很强的错误检测机制,包括:

  • 位错误:发送节点监控总线,发现发送的电平和实际电平不一致。
  • 填充错误:连续5个相同位后,没有插入相反位。
  • CRC错误:接收节点计算的CRC和发送的不一致。
  • 形式错误:帧格式不符合规范。
  • 应答错误:发送节点没有收到ACK。

当错误发生时,CAN控制器会记录错误计数。发送错误计数(TEC)和接收错误计数(REC)分别统计。当某个计数超过阈值,节点会进入不同状态:

状态 条件 行为
错误主动 TEC < 128 且 REC < 128 正常通信,发送主动错误标志
错误被动 TEC > 127 或 REC > 127 只能发送被动错误标志,且发送前需等待8个隐性位
总线关闭 TEC > 255 完全脱离总线,不再参与通信

你想想看,如果TBOX进入总线关闭状态,那所有CAN通信都断了,后果很严重。所以驱动中必须实现错误恢复机制。

我常用的做法是:在中断服务函数中读取错误状态寄存器,如果检测到总线关闭,就执行恢复流程:

void CAN_ErrorHandler(CAN_HandleTypeDef *hcan) {
    uint32_t error_code = HAL_CAN_GetError(hcan);
    
    if (error_code & HAL_CAN_ERROR_BUSOFF) {
        // 总线关闭,执行恢复
        printf("[WARN] CAN bus-off detected, recovering...\n");
        
        // 步骤1:请求进入初始化模式
        hcan->Instance->MCR |= CAN_MCR_INRQ;
        while ((hcan->Instance->MSR & CAN_MSR_INAK) == 0);
        
        // 步骤2:清除错误计数
        hcan->Instance->ESR = 0;
        
        // 步骤3:退出初始化模式
        hcan->Instance->MCR &= ~CAN_MCR_INRQ;
        while ((hcan->Instance->MSR & CAN_MSR_INAK) != 0);
        
        // 步骤4:重新启动通信
        HAL_CAN_Start(hcan);
        
        printf("[INFO] CAN bus recovered successfully\n");
    }
}

核心要点:不要频繁触发总线恢复。如果总线上存在持续故障(比如CANH/CANL短路),反复恢复只会加重总线负载。我建议在恢复失败3次后,增加一个30秒的冷却时间,或者上报故障给上层应用。

最后说一句,CAN错误处理是TBOX稳定性的最后一道防线。很多开发者在功能调试时忽略了它,结果到了实车测试阶段,总线一抖动就死机。我的经验是:从一开始就把错误处理代码写进去,哪怕只是打印日志,也比没有强。

好,这一章就到这里。下一章我们讲网络层,聊聊UDS诊断协议——这可是TBOX和ECU沟通的“官方语言”。


公众号:蓝海资料掘金营,微信deep3321