CAN 总线高负载诊断完全指南:总线错误帧计数、TEC/REC 状态机与 Bus-Off 恢复策略
一、引言
CAN 总线作为车载网络骨干,其可靠性直接影响车辆功能安全。在量产车型中,动力 CAN(500kbps)和底盘 CAN(500kbps)的常态负载率控制在 30%-50%,但故障场景下(如节点持续重发、终端电阻开路、线束短路)负载率可能飙升至 90% 以上,触发错误帧雪崩效应,最终导致节点进入 Bus-Off 状态完全脱离总线。
本文从 CAN 控制器的错误检测硬件机制出发,系统阐述 TEC(Transmit Error Counter)/REC(Receive Error Counter)状态机的工作原理,结合 SJA1000 和 MCP2515 两款经典控制器的寄存器级别诊断方法,给出一套从错误检测到 Bus-Off 恢复的完整工程方案。
二、原理剖析
2.1 CAN 错误检测的五种类型
CAN 2.0B 协议定义了五种错误检测机制,每种错误都会导致发送节点或接收节点的错误计数器递增:
2.2 TEC/REC 状态机
CAN 控制器根据 TEC/REC 值在三种错误状态之间切换,状态转移规则由 ISO 11898-1 严格定义:
状态转移要点:
- Active → Passive:TEC ≥ 128 或 REC ≥ 128。进入被动状态后,节点发送的错误标志为隐性位序列(6 位全 1),不会破坏其他节点的通信。
- Passive → Bus-Off:TEC ≥ 256。触发 Bus-Off 后,控制器硬件自动将输出驱动器置为隐性,节点从总线上完全脱离。
- Bus-Off 恢复:控制器检测到 128 次 11 个连续隐性位后,将 TEC/REC 清零并回到 Active 状态。在 SJA1000 中,该计数由硬件完成;在 MCP2515 中,需软件辅助计数。
2.3 错误帧的物理信号特征
在示波器上,错误帧表现为在正常帧的 ACK 界定符之前或之后出现 6 个连续显性位(主动错误标志),违反 CAN 的位填充规则(5 个相同极性位后应插入一个相反极性位)。错误帧叠加后,显性位总数可达 12 位(第一个节点 6 位 + 第二个节点检测到后的 6 位叠加)。
三、代码实现
/** * @file can_diag.c * @brief CAN总线诊断与Bus-Off恢复驱动 * @note 基于SJA1000独立CAN控制器,通过SPI接口与MCU通信 * @note 实现TEC/REC读取、错误状态监控和自动Bus-Off恢复 */ #include <stdio.h> #include <stdint.h> #include <stdbool.h> /* SJA1000 寄存器地址(BasicCAN模式) */ #define SJA1000_REG_CONTROL 0x00 #define SJA1000_REG_STATUS 0x02 #define SJA1000_REG_TX_ERR_CNT 0x04 /* TEC 发送错误计数器 */ #define SJA1000_REG_RX_ERR_CNT 0x05 /* REC 接收错误计数器 */ #define SJA1000_REG_ECC 0x06 /* 错误代码捕获寄存器 */ #define SJA1000_REG_RX_MSG_CNT 0x1D /* 接收消息计数器 */ /* 状态寄存器位定义 */ #define SJA1000_STATUS_BUS_OFF (1 << 7) /* 总线关闭状态 */ #define SJA1000_STATUS_ERR_PASS (1 << 6) /* 错误被动状态 */ #define SJA1000_STATUS_TX_OK (1 << 3) /* 发送成功 */ #define SJA1000_STATUS_RX_OK (1 << 0) /* 接收成功 */ /* ECC 错误类型编码 */ #define SJA1000_ECC_BIT_ERR 0x00 /* 位错误 */ #define SJA1000_ECC_FORM_ERR 0x01 /* 格式错误 */ #define SJA1000_ECC_STUFF_ERR 0x02 /* 填充错误 */ #define SJA1000_ECC_OTHER_ERR 0x03 /* 其他错误 */ /* Bus-Off恢复参数 */ #define BUSOFF_RECOVERY_INTERVAL_MS 100 /* 恢复检测间隔 */ #define BUSOFF_MAX_RECOVERY_ATTEMPTS 10 /* 最大恢复尝试次数 */ /* ========== 诊断数据结构 ========== */ typedef struct { uint8_t tec; /* 发送错误计数器 (0-255) */ uint8_t rec; /* 接收错误计数器 (0-255) */ uint8_t last_ecc; /* 最近一次错误类型 */ uint32_t tx_ok_cnt; /* 累计发送成功计数 */ uint32_t rx_ok_cnt; /* 累计接收成功计数 */ uint32_t total_err; /* 累计错误计数 */ uint32_t bus_off_cnt; /* Bus-Off事件计数 */ bool is_bus_off; /* 当前是否处于Bus-Off */ bool is_err_passive; /* 当前是否处于错误被动 */ } can_diag_info_t; /* ========== 驱动函数声明 ========== */ static int sja1000_read_reg(uint8_t reg, uint8_t *val); static int sja1000_write_reg(uint8_t reg, uint8_t val); static int sja1000_reset(void); /** * @brief 读取SJA1000的TEC和REC值 * @param[out] tec 发送错误计数器值 * @param[out] rec 接收错误计数器值 * @return 0成功,负值失败 * * @note TEC和REC位于相邻寄存器,使用连续读取以减少SPI事务 */ static int sja1000_read_error_counters(uint8_t *tec, uint8_t *rec) { uint8_t buf[2]; int ret; if (tec == NULL || rec == NULL) { return -1; } /* SJA1000支持地址自增,从TX_ERR_CNT开始连续读2字节 */ ret = sja1000_read_reg(SJA1000_REG_TX_ERR_CNT, &buf[0]); if (ret < 0) { fprintf(stderr, "[错误] 读取TEC寄存器失败: %d\n", ret); return ret; } ret = sja1000_read_reg(SJA1000_REG_RX_ERR_CNT, &buf[1]); if (ret < 0) { fprintf(stderr, "[错误] 读取REC寄存器失败: %d\n", ret); return ret; } *tec = buf[0]; *rec = buf[1]; return 0; } /** * @brief 收集完整的CAN诊断信息 * @param[out] info 诊断信息结构体指针 * @return 0成功,负值失败 */ int can_diag_collect(can_diag_info_t *info) { uint8_t status, ecc; int ret; if (info == NULL) { fprintf(stderr, "[错误] can_diag_collect: info为NULL\n"); return -1; } /* 读取错误计数器 */ ret = sja1000_read_error_counters(&info->tec, &info->rec); if (ret < 0) { return ret; } /* 读取状态寄存器 */ ret = sja1000_read_reg(SJA1000_REG_STATUS, &status); if (ret < 0) { fprintf(stderr, "[错误] 读取状态寄存器失败: %d\n", ret); return ret; } /* 解析状态位 */ info->is_bus_off = (status & SJA1000_STATUS_BUS_OFF) ? true : false; info->is_err_passive = (status & SJA1000_STATUS_ERR_PASS) ? true : false; /* 累加成功计数 */ if (status & SJA1000_STATUS_TX_OK) info->tx_ok_cnt++; if (status & SJA1000_STATUS_RX_OK) info->rx_ok_cnt++; /* 读取错误代码寄存器 */ ret = sja1000_read_reg(SJA1000_REG_ECC, &ecc); if (ret == 0) { info->last_ecc = ecc & 0x03; /* 低2位为错误编码 */ if (ecc != 0) info->total_err++; } /* 检测Bus-Off状态变化 */ static bool prev_bus_off = false; if (info->is_bus_off && !prev_bus_off) { info->bus_off_cnt++; printf("[警告] 检测到Bus-Off事件 (第%u次), TEC=%u, REC=%u\n", info->bus_off_cnt, info->tec, info->rec); } prev_bus_off = info->is_bus_off; return 0; } /** * @brief Bus-Off自动恢复流程 * @param info 当前诊断信息 * @return 0恢复成功,负值恢复失败 * * @note 恢复策略: * 1. 硬件复位SJA1000控制器 * 2. 重新配置波特率和过滤器 * 3. 等待128×11bit隐性位检测完成 * 4. 验证TEC/REC清零 */ int can_diag_busoff_recovery(can_diag_info_t *info) { uint8_t tec, rec; int ret; static int attempt = 0; if (!info->is_bus_off) { printf("[信息] 当前未处于Bus-Off状态,跳过恢复\n"); return 0; } if (++attempt > BUSOFF_MAX_RECOVERY_ATTEMPTS) { fprintf(stderr, "[错误] Bus-Off恢复已达最大尝试次数(%d),将节点标记为永久故障\n", BUSOFF_MAX_RECOVERY_ATTEMPTS); attempt = 0; return -1; /* 上报永久故障,需人工介入 */ } printf("[信息] 启动Bus-Off恢复流程(attempt %d/%d)...\n", attempt, BUSOFF_MAX_RECOVERY_ATTEMPTS); /* 步骤1:硬件复位控制器 */ ret = sja1000_reset(); if (ret < 0) { fprintf(stderr, "[错误] SJA1000硬件复位失败\n"); return ret; } /* 步骤2:等待128×11bit隐性位(500kbps下约2.8ms,取5ms余量) */ /* SJA1000硬件自动完成该计数,软件只需轮询等待 */ /* 步骤3:轮询确认Bus-Off位已清除 */ int wait_cnt = 0; while (wait_cnt < 50) { /* 5ms × 50 = 250ms 超时 */ uint8_t status; ret = sja1000_read_reg(SJA1000_REG_STATUS, &status); if (ret < 0) return ret; if (!(status & SJA1000_STATUS_BUS_OFF)) { printf("[信息] Bus-Off标志已清除\n"); break; } /* 延时5ms */ wait_cnt++; } if (wait_cnt >= 50) { fprintf(stderr, "[错误] Bus-Off恢复超时(250ms)\n"); return -2; } /* 步骤4:验证TEC/REC已清零 */ ret = sja1000_read_error_counters(&tec, &rec); if (ret < 0) return ret; if (tec != 0 || rec != 0) { fprintf(stderr, "[警告] 恢复后错误计数器未清零: TEC=%u, REC=%u\n", tec, rec); /* 手动清零:进入Reset模式后写入0 */ sja1000_write_reg(SJA1000_REG_TX_ERR_CNT, 0); sja1000_write_reg(SJA1000_REG_RX_ERR_CNT, 0); } /* 步骤5:重新配置CAN参数 */ /* TODO: 调用波特率配置函数,重新设置BTR0/BTR1 */ /* TODO: 清除并重新配置接收过滤器 */ attempt = 0; /* 恢复成功,重置计数 */ printf("[信息] Bus-Off恢复成功\n"); return 0; } /** * @brief 周期性CAN诊断任务(建议在1ms或10ms定时器中调用) */ void can_diag_poll_task(void) { static can_diag_info_t diag_info = {0}; int ret; ret = can_diag_collect(&diag_info); if (ret < 0) { /* 诊断采集失败可能表示SPI通信异常 */ return; } /* 动态调整:检测到错误被动状态时记录日志 */ if (diag_info.is_err_passive) { printf("[警告] CAN进入错误被动状态 TEC=%u REC=%u 总错误=%u\n", diag_info.tec, diag_info.rec, diag_info.total_err); } /* 触发Bus-Off恢复 */ if (diag_info.is_bus_off) { can_diag_busoff_recovery(&diag_info); } } /* ========== SPI底层驱动(平台相关,此处为接口声明) ========== */ static int sja1000_read_reg(uint8_t reg, uint8_t *val) { /* 实现:通过SPI发送(reg | 0x00读取命令),接收1字节数据 */ (void)reg; (void)val; return 0; } static int sja1000_write_reg(uint8_t reg, uint8_t val) { /* 实现:通过SPI发送(reg | 0x80写入命令) + val */ (void)reg; (void)val; return 0; } static int sja1000_reset(void) { /* 实现:通过硬件复位引脚拉低→延时→拉高 */ return 0; }四、边界分析
错误计数器的溢出行为:TEC/REC 为 8 位无符号计数器。ISO 11898-1 定义了饱和规则——TEC 达到 255 后不再递增,但在满足 Bus-Off 条件(TEC ≥ 256)时,规范要求实现一个内部 ≥256 的判定逻辑。SJA1000 通过一个额外的硬件标志位实现,MCP2515 则直接使用 TEC 寄存器的最高位配合内部逻辑。
Bus-Off 恢复的时序约束:恢复需要检测 128 次 11 个连续隐性位。在 500kbps 下,11 个隐性位 = 22μs,128 次 × 22μs = 2.816ms。但实际恢复时间远大于此,因为总线空闲不等于"11 个连续隐性位"——每出现一个显性位(SOF),计数即被复位。在高负载总线上,恢复可能长达数秒。
诊断信息的时间窗口:can_diag_collect()函数采集的是瞬时值。TEC 和 REC 在两次采集之间可能剧烈波动。对于需要精准定位错误帧的排查场景,应配合 CAN 分析仪记录错误帧的时间戳和类型,仅靠寄存器读取无法回溯。
恢复策略的风险:自动 Bus-Off 恢复可能掩盖总线物理层故障。如果节点因线束间歇短路而反复进入 Bus-Off,自动恢复会导致 CAN 总线出现周期性的错误风暴。生产级实现应在 1 分钟内 Bus-Off 超过 3 次时,停止自动恢复并上报 DTC(诊断故障码)。
五、总结
TEC/REC 是 CAN 诊断的"血压值":持续监控错误计数器的变化趋势比单次绝对值更有诊断意义。TEC 的阶跃式增长(+8)提示 ACK/Bit Error,递增式增长(+1)提示 Stuff/Form/CRC Error。
Bus-Off 恢复需要分级策略:单次 Bus-Off 可自动恢复,短时间内重复发生应降级为手动恢复并记录 DTC,避免错误风暴扩散。
ECC 寄存器是定位故障的入口:SJA1000 的 ECC 寄存器记录最近一次错误的类型和位位置,配合 CAN 分析仪可精确定位到具体节点和帧。
错误被动状态是预警窗口:在 TEC ≥ 96(已超过 128 的 75%)时即触发预警,给应用层留出反应时间。
终端电阻不可忽视:约 40% 的现场 CAN 通信异常源于终端电阻缺失或阻值偏差。标准高速 CAN 要求总线两端各接 120Ω ±1% 终端电阻,差分信号幅度应在 1.5V-3.0V 之间。