车载底层 CAN 通信和上层 UDS 诊断协议开发,是很多做 ECU、TBOX、域控制器甚至车载测试的朋友迟早要碰的一摊活。我刚入手第一个量产项目时,光是 CAN 采样点和 UDS 超时这两件事就被折腾了好几轮,后面啃完规范和源码才明白,所谓“协议栈开发”其实就两件事:把物理链路上的每一个 bit 理顺,再把诊断仪和 ECU 之间的每一次问答管好。这篇文章基于我整理过的一套技术文档脱敏后拿出来,把底层 CAN 链路层、数据链路层的细节、UDS 各服务实现,以及两者之间衔接的工程问题完整讲一遍。适合正在接触整车网络、车载测试或者嵌入式诊断开发的工程师做参考,新手可以顺着思路建立完整的协议栈概念,老手也可以直接跳到自己关心的参数和排查章节。
1. 项目全景:CAN + UDS 协议栈到底在做什么
1.1 这套系统在整车上处于什么位置
先明确边界。完整的车载诊断链路,从外到内大概是这样:诊断仪(Tester)通过 OBD 口接入整车网络,经过网关路由,到达目标 ECU。ECU 内部再细分的话,最底下是 CAN 收发器和 MCU 的 CAN 控制器,往上是数据链路层的帧收发,再往上是 ISO-TP 传输层负责把超过 8 字节的诊断消息分段传输,最上面才是 UDS 应用层,处理各种诊断服务请求和响应。
我们做这套开发时,目标 MCU 是 STM32F407,CAN 控制器内置,收发器用 TJA1050,波特率 500 kbps,标准帧格式,工程上跑在一条完整的台架网络里。这套架构很典型,不是某个小众方案,所以整套思路放到后面的项目里几乎可以原样套用。你要理解的是:开发过程中真正难的不是某个寄存器不会配,而是链路层参数、传输层状态机和应用层服务三者之间的配合逻辑,一个地方错位,排查起来非常痛苦。
1.2 协议栈为什么必须分层
我做过一个不成熟的项目,早期图省事把协议处理全堆在主循环里,UDS 请求解析逻辑、CAN 收发处理、状态管理全揉在一起。结果就是每加一个诊断服务都要动一大片代码,测试的时候稍微压一点时序就乱套,后来实在撑不住,才痛下决心按标准分工重构。
标准分工是四层:物理层和数据链路层由 CAN 控制器硬件完成,软件只需要负责配置位时序和收发缓冲区;传输层实现 ISO-TP 的分帧重组,说白了就是把 8 字节一帧的 CAN 消息拼成完整的诊断消息;应用层实现 UDS 服务,比如会话切换、读取数据、安全访问、故障码读取;最上层是应用对接层,把 UDS 请求映射到实际的功能模块,比如读写 VIN 码、读取软件版本号。
这种分层的好处很直接:CAN 底层改波特率、换收发器时,应用层代码不用动;UDS 从经典 CAN 挪到 CAN FD 甚至车载以太网时,传输层只需要做适配,服务逻辑基本原样保留。我后来做 CAN FD 诊断时深有体会,底层换了一茬,但上层 UDS 服务代码几乎没有改动,这就是分层带来的最实在的收益。
1.3 方案选型:自己写协议栈还是买现成的
市面上有商业化协议栈可以买,比如 Vector 的协议栈、EB tresos、AUTOSAR 的诊断栈,功能全、经过大量量产验证,但价格不菲,而且配套的工具链和许可很重,小团队和前期验证阶段未必扛得住。
我们当时的选择是:ISO-TP 层自己写,UDS 服务层自己写,底层的 CAN 驱动用 MCU 厂商的库函数。有同事问我说,自己写的协议栈靠谱吗?我的看法是,只要把状态机的细节抠清楚,配合充分的测试,完全够用。实际上很多中低端 ECU 的量产项目也是这么做的,关键不在于是不是商业栈,而在于你有没有把规范和边界条件吃透。自己写的另一个好处是,出问题的时候可以直接看源码定位,不必对着黑盒推测。
2. 物理层和链路层:CAN 通信开发的硬核细节
2.1 CAN 物理层的基础要求
很多人一开始就把协议栈写好了,结果上车跑不通,最后拿示波器一看,总线波形惨不忍睹。CAN 物理层的核心是双线差分信号,CANH 和 CANL 之间的电压差决定显性还是隐性。显性时差分电压约 2 V,隐性时接近 0 V。总线的两端必须各接一个 120 欧姆终端电阻,用来匹配阻抗、减少信号反射。
终端电阻这个坑我踩过不止一次。台架上偷懒只在一端接了 120 欧姆,另一端没接,低速短距离测试时感觉不到问题,一旦总线稍微加长或者 EM C 环境复杂一点,就会出偶发错误帧。所以无论多急,两端 120 欧姆一定要到位。选择收发器时,也要考虑速率匹配。TJA1050 适合 500 kbps 级别的经典 CAN,如果做 CAN FD 高速数据段,就要选支持更高速度的型号,比如 TJA1044,否则数据段跑到 2 Mbps 以上时波形会明显失真。
总线长度和波特率的关系也要心里有数。经验参考值:1 Mbps 时总线长度尽量控制在 40 米以内,500 kbps 控制 100 米左右,250 kbps 可以到 250 米左右。这些不是硬性标准,但超过之后出现的问题基本都是物理层信号质量导致的,协议层怎么调都不解决问题。
2.2 位时间、采样点与波特率怎么算
CAN 协议里,一个数据位的时间被拆成若干份,每份叫一个 Time Quantum(Tq)。位时间由四段组成:同步段(SYNC_SEG)、传播段(PROP_SEG)、相位缓冲段 1(PS1)、相位缓冲段 2(PS2)。采样点通常落在 PS1 和 PS2 的交界处,也就是大约在位的 70% 到 90% 位置之间。
为什么采样点这么敏感?因为 CAN 总线上多个节点是“线与”结构,没有主从时钟同步,只能靠帧起始沿不断对齐。如果采样点太靠前,后续累积的相位误差可能还没被重同步修正就把位采样了;如果太靠后,留给相位缓冲段 2 的余量太小,同样容易采错。经典 CAN 常用 75% 到 87.5% 的采样点,CAN FD 仲裁段和数据段有不同的推荐值,数据段一般要求 80% 以上。
波特率的计算本质上是数学题。频率已知,先通过波特率分频器 BRP 得到 Tq 的时长,再决定一个位由多少个 Tq 组成。比如外部时钟 8 MHz,BRP 设为 1,则 Tq 是 1/8000000 秒,即 125 ns。目标波特率 500 kbps,位时间 2 微秒,2 微秒除以 125 ns 等于 16 Tq。如果配置为 16 Tq,那同步段固定 1 Tq,剩下 15 Tq 分成传播段加 PS1 的 12 Tq 和 PS2 的 3 Tq,采样点位置就是 (1+12)/16 = 81.25%。这个值符合多数场景的要求。
实际芯片配置时,不要只对着库函数的参数乱填,先把采样点和各段长度算清楚,再去看寄存器填值。我见过有人把 PS2 填成 15 Tq,采样点几乎到位的末尾,整车环境下通信几乎无法用,一查就是参数没算过,纯靠猜。
2.3 CAN 时钟误差为什么会导致丢帧
“CAN 时钟误差”是网上的热搜词,也是实际项目中特别容易忽视的一环。每个 ECU 的 CAN 控制器都有独立时钟源,晶振精度和温漂各有差异。正常情况下,理想波特率大家可能都是 500 kbps,但不同节点实际工作频率会有微小偏差。CAN 协议的容错机制就是通过“硬同步”和“重同步”来吸收这种偏差,保证不同速率的节点也能稳定通信。
硬同步发生在帧起始的下降沿,所有节点都以这个沿为基准,把位定时重新对齐。重同步则发生在帧内部的边沿,当某一位的实际边沿与节点预期位置存在相位差时,节点会调整 PS1 或 PS2 的长度来补偿。能补偿多少,取决于重同步跳转宽度 SJW,SJW 取值一般在 1 到 4 Tq 之间,不能超过 PS2 的长度。
时钟误差容忍上限和位时间、SJW 直接相关。工程上常用的估算方式是:允许的相位偏差大约是 min(PS1 - SJW, PS2 - SJW) 除以一个位时间。举个例子,上面的 16 Tq 配置,PS1+传播段是 12 Tq,PS2 是 3 Tq,SJW 取 2 Tq,那 x = min(12-2, 3-2) = 1 Tq,一个位 16 Tq,所以裕量约 6.25%。这个裕量看起来不大,但 CAN 规范要求参与通信的节点总误差要控制在 1.5% 以内,So 真正影响你的是最差情况下的波特率误差分配。比如晶振精度 ±0.3%,两个节点各自偏差叠加,再加上采样点设置带来的误差,很容易超出容限。
我自己处理过一个案例:某个模块换了便宜的晶振,标称精度 0.5%,单独测试时没异常,接入整车网络后偶发总线错误。最后定位到该节点时钟误差太大,重同步也补不过来,换成温补晶振后问题消失。所以在设计阶段就要定好晶振选型,不要在这种地方省钱。
2.4 采样点与 SJW 配置的实操计算
直接给出一个我常用而且验证过的配置示例。假设外设时钟 8 MHz,目标波特率 500 kbps,位时间 16 Tq,搭配采样点 81.25%、SJW = 2 Tq。
| 参数 | 计算过程 | 配置值 |
|---|---|---|
| Tq | 1 / (8 MHz / BRP) = 1 / 8 MHz = 125 ns | BRP = 1 |
| 位时间 | 1 / 500 kbps = 2 μs = 16 Tq | 16 Tq |
| 同步段 | 固定 1 Tq | 1 Tq |
| 传播段 + PS1 | 12 Tq | 12 Tq |
| PS2 | 16 - 1 - 12 = 3 Tq | 3 Tq |
| 采样点 | (1 + 12) / 16 = 81.25% | 81.25% |
| SJW | 不超过 PS2,取 2 Tq | 2 Tq |
配置完成后,我习惯用示波器直接看波形,配合 CAN 分析仪连续跑几十万帧压测,检查错误帧计数是否为零。这一步做好了,再往上写 UDS 服务才有意义,链路层不干净,上层再稳也会被拖下水。
3. UDS 诊断协议服务层实现要点
3.1 诊断寻址与会话切换是怎么工作的
UDS 跑在 CAN 网络上,第一步要知道怎么找到目标 ECU。整车诊断通常用两类寻址:物理寻址和功能寻址。物理寻址点对点,诊断仪发送请求时指定目标 ECU 的 ID,目标 ECU 只做点对点响应;功能寻址则是广播式,比如一个 ID 对应所有 ECU,诊断仪发一次,所有支持该服务的 ECU 都执行并响应。常用约定是请求物理寻址 ID 0x7E0,响应 ID 0x7E8,功能寻址 ID 0x7DF。
功能寻址要特别注意响应冲突。如果一次广播查询目标 ECU 的数量很多,多 ECU 同时回响应帧,总线仲裁后可能互相覆盖,造成响应丢失。量产项目里通常会对功能寻址的服务做严格限制,只允许查询类服务,不允许执行写入类操作,避免多个 ECU 同时响应甚至冲突。
诊断会话决定了 ECU 当前允许执行哪些服务。UDS 的 10 服务,即诊断会话控制,常用子功能是 01 默认会话、02 编程会话、03 扩展会话。默认会话是上电后的状态,只能做基础的数据读取;扩展会话解锁更高级的读写操作;编程会话通常配合固件刷写使用。会话切换的时序管理和状态迁移一定要做成状态机,不能靠 if-else 堆逻辑。
我见过一个低级错误:扩展会话下处理完 27 服务解锁,某次异常复位回到默认会话,诊断仪继续发写入请求,结果一直等不到预期响应。原因是 ECU 侧复位后没有把会话状态清掉,还在默认会话下接收了扩展会话才允许的命令。会话切换后必须重置安全访问状态、待机激活定时器这些相关状态,这是一个非常重要的实现细节。
3.2 ISO-TP 分帧与诊断消息重组
UDS 的消息长度经常超过 8 字节,而经典 CAN 一帧最多只能携带 8 字节数据。因此 UDS 在 CAN 上的传输依赖 ISO-TP(ISO 15765-2),它定义了四种帧类型:单帧、首帧、连续帧、流控帧。
单帧用于长度不超过 7 字节的诊断消息,第一个字节的高四位为 0,低四位表示后面数据字节数;首帧用于长度超过 7 字节的消息,第一个字节的高四位为 1,低 12 位表示完整消息长度,首帧后面再带 6 字节数据;连续帧是数据主体,第一个字节高四位为 2,低四位为序列号,从 1 开始递增到 15 后回绕;流控帧用于接收方告诉发送方“可以继续发”或者“等待”。
ISO-TP 状态机的核心是连续帧序号校验。发送方每发一个连续帧,序号必须递增,接收方如果发现序号跳变,要能够识别并请求重发或者直接丢弃。很多刚接触诊断开发的人都会在长报文响应上栽跟头,比如读故障码快照数据,响应消息 40 字节,如果 ISO-TP 状态机没有正确维护接收缓冲区和序号,就会出现响应不完整或乱序。
这里给一个接收连续帧的关键实现思路:接收单帧,把数据拷贝到缓冲区,置位“消息完整”标志;接收首帧,记录总长度,初始化接收缓冲区,准备接收连续帧;接收连续帧,校验序号和长度,续写缓冲区;接收流控帧通常只在发送方向使用。整个处理过程建议放在 CAN 接收中断的回调里只做数据搬运和状态打点,真正的服务解析放在主循环或任务中,避免中断处理耗时过长导致丢帧。
3.3 UDS 高频服务逐个拆解
会话切换服务 10 服务前面已经提到,接下来是实际开发中用得最多的几个服务。
22 服务按 DID 读取数据,比如读 VIN 码、读软件版本号、读硬件版本号。DID 是 2 字节标识符,0xF190 是常见的 VIN 码 DID。响应格式是服务 ID 加子功能加 DID 加数据。读操作相对简单,但要检查请求的 DID 是否存在,不存在就回 NRC 0x31。
2E 服务按 DID 写入数据,通常需要安全访问解锁才能执行。写入 VIN 操作的流程通常是:先切换扩展会话,然后 27 服务解锁,再发 2E 写入。2E 的响应要等 ECU 真的把数据处理完成后再回复,不要先回成功再做操作。
27 服务安全访问是诊断开发里最容易出细节问题的地方。流程是诊断仪发 27 01 请求种子,ECU 回一个随机数种子,诊断仪用约定的算法计算密钥并通过 27 02 发回,ECU 核对密钥正确后解锁。常见 NRC 是 0x37,表示延迟时间未到。为了防止暴力破解,ECU 在连续多次密钥错误后要拉长延迟,开发时要留好计数器和时间戳。安全算法本身不要明文写在协议栈里,最好独立成一个模块,量产时做代码保护。
19 服务读取 DTC 信息。DTC 就是故障码,每个 DTC 有一个 3 字节编号和 1 字节状态掩码。19 服务的子功能很多,常用的 01 按状态掩码读 DTC、02 按掩码读 DTC 快照、04 读快照记录等。DTC 状态掩码每一位都有含义,bit 0 表示测试失败,bit 1 表示当前周期失败,bit 2 表示待定,bit 3 表示已确认,bit 6 表示历史故障,bit 7 表示警告指示灯。实际测试中很多人用掩码 0xFF 读全部,但有些场景要求只读当前已确认的故障,掩码应该按需求精确设置。
31 服务例程控制,子功能 01 启动、02 停止、03 请求结果。例程 ID 是 2 字节,比如 0x0203 检查编程条件、0xFF00 擦除内存、0xFF01 写入数据。例程执行时间可能很长,ECU 会在处理过程中回 0x78 表示正在处理,但 0x78 不能无限发,超时上限通常是 5 秒。如果例程确定执行不了,就明确回 NRC,不要一直挂 pending,否则会让诊断仪端直接崩溃。
34 36 37 三个服务配合起来做固件刷写。34 请求下载,指定下载地址和大小,ECU 返回允许的最大块长度;36 传输数据,按照块号逐块发送,块号从 1 开始,上下循环;37 请求退出传输。固件刷写的时序敏感性很高,连续的 36 请求之间不能超过 ECU 内部的超时限制,诊断仪侧必须严格按一定节奏发送,否则会中断下载。
3.4 超时、NRC 与状态机设计
每收到一条诊断请求,ECU 必须在一定时间内给出响应,这个时间叫 P2。量产项目一般要求 P2 为 50 毫秒。如果 ECU 无法在 P2 内完成处理,必须先回一个 0x78 的待处理响应,同时开启 P2* 定时器,P2* 通常是 5 秒。也就是说,整个处理过程中,诊断仪一直在等,ECU 必须在规定时间内不少于一次地回复,要么最终结果,要么 0x78。
NRC 的返回也讲究时机。请求长度错误、子功能不支持、会话不满足、安全访问没解锁、条件不满足等,都要明确回对应的 NRC。常见 NRC 对照可以做成表,开发时直接按表核对:
| NRC 值 | 含义 | 典型触发场景 |
|---|---|---|
| 0x10 | 一般拒绝 | 请求无法执行,但无更精确错误码 |
| 0x12 | 子功能不支持 | 发送了未定义子功能 |
| 0x13 | 请求消息长度错误 | 报文长度和格式不符 |
| 0x22 | 条件不满足 | 会话模式不对或条件未达成 |
| 0x24 | 请求序列错误 | 服务执行顺序有误,比如 36 前没有发 34 |
| 0x31 | 请求超出范围 | DID 不存在、例程 ID 无效、地址越界 |
| 0x33 | 安全访问被拒绝 | 未解锁就执行受保护服务 |
| 0x35 | 密钥错误 | 安全访问密钥校验失败 |
| 0x37 | 延迟时间未到 | 安全访问尝试过于频繁 |
| 0x72 | 一般编程错误 | 擦除或下载过程中出错 |
状态机设计的核心是:任何时刻,协议栈都清楚自己处在哪个状态,收到什么消息能做什么动作。我习惯把会话状态、安全访问状态、传输层状态分别用枚举变量管理,状态迁移集中在一个函数里处理,避免业务代码到处修改状态。协议栈的每个入口在接收诊断请求后,第一件事是校验合法性,再决定是否执行,不要边执行边校验,否则很容易在中间状态收到意外消息。
4. 协议栈与驱动层衔接的工程细节
4.1 帧接收缓存与消息队列怎么设计
诊断 CAN 报文的接收通常发生在中断上下文,处理则放在任务上下文。中间靠一个缓存队列来衔接。最基础的做法是环形缓冲,接收中断把 CAN 帧数据写入环形缓冲,主循环或 RTOS 任务周期性取帧处理。要注意的是,环形缓冲的读写指针要保证原子性,中断里只能写,任务里只能读,防止读写冲突弄丢数据。
缓冲区大小也要评估。诊断场景下,如果诊断仪使用功能寻址,同一时刻可能进来多帧;ISO-TP 首帧和连续帧之间也可能有较短的间隔。缓冲区太小会丢帧,太大浪费内存。我习惯的做法是,按总线负载率和最长连续接收帧数估算,一般节点用 16 到 32 条帧缓冲就足够,但如果有大量诊断刷写请求,缓冲区适当加大到 64。
还有一种容易被忽视的身份过滤问题。功能寻址的目标是所有节点,物理寻址只针对自己,但总线上所有报文都会进接收中断。如果没有在驱动层过滤 ID,ISO-TP 层可能把别的 ECU 的响应也当作自己的诊断数据。所以收到一帧 CAN 数据后,第一步就是比对 CAN ID,是自己的物理诊断 ID 或者功能诊断 ID 才进入 ISO-TP 处理逻辑。
4.2 任务调度与看门狗策略
有了缓存和队列,还得安排执行时机。很多小型 ECU 方案没有 RTOS,主循环轮询时间片。诊断协议站的处理任务最好保证相对高频的轮询,比如每 5 到 10 毫秒跑一次,因为 UDS 的 P2 超时、ISO-TP 的接收超时都需要定时检查。如果主循环里其他模块的任务耗时很长,就会拖慢诊断响应,这是很多人忽略的点。我在项目里单独分了一个 5 ms 定时中断作为协议栈的心跳,专门处理超时更新和周期发送。
看门狗策略也很有讲究。如果看门狗刷新放在主循环里,某个模块卡死可能主循环还在跑,看门狗照样被喂,失效保护就没意义。更好的做法是,看门狗放在独立的高优先级定时中断里喂,同时要关联协议栈的状态机。比如协议栈如果长时间停留在“正在刷写”状态,超过了预期时间,说明逻辑出错了,可以让看门狗强制复位。但这里有个反直觉的点:刷写过程中 CAN 通信繁忙,如果看门狗刷新逻辑和诊断处理产生干扰,反而会出问题。我习惯在刷写模式里关闭部分非关键任务,把 CPU 时间让给协议栈,等刷写完成再恢复。
5. 工具链与测试验证方法
5.1 常用工具怎么选、怎么用于排查
开发调试时最常用的三类工具是 CAN 分析仪、总线示波器和诊断测试上位机。CAN 分析仪用来收发报文,查看总线上到底跑了哪些帧,帧 ID、数据内容、时间戳一目了然。PCAN 和周立功的 CANTest 都是性价比很高的选择,Vector 的 CANoe 功能最全,可以仿真网络节点,适合系统级测试,但价格也最高。
示波器用于物理层排查,要看的是差分电压、位宽、边沿是否陡峭。如果位宽偏差超过预期,或者显性电平幅度不够,就能确认物理层问题。实际定位 CAN 通信问题时,我建议遵循“由底向上”的顺序:先看波形,再看报文,最后才查协议逻辑。波形不正常,直接修物理层;波形正常,再抓报文确认有没有错误帧;报文正常,还是丢帧,那就查采样点、时钟误差、重同步配置。
诊断测试上位机可以模拟诊断仪,逐条发送 UDS 请求,手工验证很费时间,建议用自动化脚本。开源的方案是 python-can 加 udsoncan 库,脚本里写测试用例,一键跑完回归测试,非常高效。我们内部把常用诊断用例做成了脚本集,每次协议栈代码有改动就先全量跑一遍,然后再做台架测试。
5.2 诊断测试用例怎么设计
测试用例不能只测“正常路径”,负向测试才是诊断工程的核心。
| 测试类型 | 测试内容 | 预期结果 |
|---|---|---|
| 会话切换 | 默认/扩展/编程会话间切换,非法会话切换请求 | 正常切换;非法请求回 NRC 0x12 或 0x22 |
| 安全访问 | 正确密钥解锁、错误密钥、连续多次错误 | 正确解锁;错误回 0x35;连续错误回 0x37 |
| 22/2E 读写 | 有效 DID、无效 DID、未解锁写 | 正常读写;无效 DID 回 0x31 |
| 19 读 DTC | 不同状态掩码、无故障时读取 | 返回对应 DTC 记录或空列表 |
| 31 例程 | 有效例程、无效例程、会话不满足 | 正常执行;无效回 0x31 |
| 刷写流程 | 34/36/37 完整流程、块序号错误、超时中断 | 完整流程成功;错误场景回对应 NRC |
| ISO-TP | 单帧、多帧、首帧+连续帧+流控、序号跳变 | 正确重组完整消息;异常场景丢弃或报错 |
测试环境建议搭一个简单的“盒中网”:一个 ECU、一个诊断仪(或上位机)、一个 CAN 分析仪,三者并联在同一路 CAN 网络中,终端电阻按实际要求接好。这样既能复现问题,又可以随时抓包对比。
6. 高频问题与现场排障实录
6.1 高频问题速查表
把我在开发过程中积累的最常见问题整理成一张表,方便你现场排查:
| 现象 | 可能原因 | 排查手段 |
|---|---|---|
| 总线上所有节点都收不到报文 | 波特率配置错误或终端电阻缺失 | 示波器看波形,逐个节点核对波特率 |
| 偶尔丢帧,尤其是长线束时 | 采样点位置不合理、SJW 太小 | 重新计算位时序,调整采样点到 80% 左右 |
| 通信时好时坏,拔插终端电阻后改善 | 终端电阻接触不良或缺一个端接 | 检查两端 120 欧姆电阻 |
| 诊断仪请求有响应,但响应内容不对 | UDS 服务解析逻辑错误或 DID 映射错位 | 抓取原始报文,逐字节核对请求和响应 |
| 长报文响应不完整 | ISO-TP 连续帧序号错乱或缓冲区溢出 | 打印连续帧序号,检查接收缓冲区大小 |
| 27 服务一直回 0x37 | 上一次解锁延迟时间未到 | 检查解锁时间戳,确保延迟超时后才允许下一次尝试 |
| 19 服务读不到 DTC | 状态掩码设置不对或 DTC 未记录 | 确认 DTC 状态掩码,使用 0xFF 全量读取测试 |
| 刷写过程中中断下载 | 36 服务请求间隔超时、块序号错误 | 检查块序号递增逻辑和发送节奏 |
6.2 三个现场排障案例
第一个案例是偶发总线错误帧。台架测试压力一上来,错误帧计数开始增长。示波器看波形,波特率误差在正常范围,终端电阻也没有问题。后来查配置,发现 SJW 只设了 1 Tq,重同步能力偏弱。把 SJW 调到 2 Tq 之后,错误帧归零。这就是典型的重同步裕量不足。
第二个案例是 UDS 下载中断。诊断仪擦除成功后,连续发 36 服务传数据,发送到一半 ECU 就不响应了。抓包发现,ECU 回复了 NRC 0x73,表示块序列号错误。排查之后发现,ECU 侧在处理连续帧时把首帧的序号也算进去了一次,导致序号整体偏移。修正 ISO-TP 状态机后恢复正常。
第三个案例是功能寻址响应冲突。诊断仪用 0x7DF 功能寻址同时查询多个 ECU 的软件版本,每个 ECU 都回响应,结果上位机收到的响应时有时无。我们一开始以为是 ECU 响应慢,后来抓包才发现是多个响应帧在总线上互相抢占,低优先级帧被高优先级帧冲掉。最终方案是改用物理寻址逐台查询,避开了冲突,同时也保证了数据可靠性。
个人经验收尾
我把这个项目做完后的最大体会有两点。第一,协议开发一定要按“波形先于协议、协议先于逻辑”的顺序推进,物理层不干净,上层写再多代码都是白搭。第二,调试时保留完整的日志非常关键,我至今保留着每次台架测试的 CAN 抓包文件,出了疑难问题就回头翻历史报文对比,很多看似无头绪的 bug 就是这么找到的。另外提醒一句,安全访问、27 服务这类机制是用来保护车辆和车主利益的,开发时一定不要做绕过或破解这一类的事。后续如果你的项目要往 CAN FD 或者车载以太网诊断方向走,这套分层思路同样通用,底层换掉,上层服务逻辑基本可以原样搬过去。