第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