ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

车载CAN与UDS诊断协议栈开发:从采样点到ISO-TP服务实现

车载CAN与UDS诊断协议栈开发:从采样点到ISO-TP服务实现 车载底层 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 是 STM32F407CAN 控制器内置收发器用 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 QuantumTq。位时间由四段组成同步段SYNC_SEG、传播段PROP_SEG、相位缓冲段 1PS1、相位缓冲段 2PS2。采样点通常落在 PS1 和 PS2 的交界处也就是大约在位的 70% 到 90% 位置之间。为什么采样点这么敏感因为 CAN 总线上多个节点是“线与”结构没有主从时钟同步只能靠帧起始沿不断对齐。如果采样点太靠前后续累积的相位误差可能还没被重同步修正就把位采样了如果太靠后留给相位缓冲段 2 的余量太小同样容易采错。经典 CAN 常用 75% 到 87.5% 的采样点CAN FD 仲裁段和数据段有不同的推荐值数据段一般要求 80% 以上。波特率的计算本质上是数学题。频率已知先通过波特率分频器 BRP 得到 Tq 的时长再决定一个位由多少个 Tq 组成。比如外部时钟 8 MHzBRP 设为 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采样点位置就是 (112)/16 81.25%。这个值符合多数场景的要求。实际芯片配置时不要只对着库函数的参数乱填先把采样点和各段长度算清楚再去看寄存器填值。我见过有人把 PS2 填成 15 Tq采样点几乎到位的末尾整车环境下通信几乎无法用一查就是参数没算过纯靠猜。2.3 CAN 时钟误差为什么会导致丢帧“CAN 时钟误差”是网上的热搜词也是实际项目中特别容易忽视的一环。每个 ECU 的 CAN 控制器都有独立时钟源晶振精度和温漂各有差异。正常情况下理想波特率大家可能都是 500 kbps但不同节点实际工作频率会有微小偏差。CAN 协议的容错机制就是通过“硬同步”和“重同步”来吸收这种偏差保证不同速率的节点也能稳定通信。硬同步发生在帧起始的下降沿所有节点都以这个沿为基准把位定时重新对齐。重同步则发生在帧内部的边沿当某一位的实际边沿与节点预期位置存在相位差时节点会调整 PS1 或 PS2 的长度来补偿。能补偿多少取决于重同步跳转宽度 SJWSJW 取值一般在 1 到 4 Tq 之间不能超过 PS2 的长度。时钟误差容忍上限和位时间、SJW 直接相关。工程上常用的估算方式是允许的相位偏差大约是 min(PS1 - SJW, PS2 - SJW) 除以一个位时间。举个例子上面的 16 Tq 配置PS1传播段是 12 TqPS2 是 3 TqSJW 取 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。参数计算过程配置值Tq1 / (8 MHz / BRP) 1 / 8 MHz 125 nsBRP 1位时间1 / 500 kbps 2 μs 16 Tq16 Tq同步段固定 1 Tq1 Tq传播段 PS112 Tq12 TqPS216 - 1 - 12 3 Tq3 Tq采样点(1 12) / 16 81.25%81.25%SJW不超过 PS2取 2 Tq2 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-TPISO 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 前没有发 340x31请求超出范围DID 不存在、例程 ID 无效、地址越界0x33安全访问被拒绝未解锁就执行受保护服务0x35密钥错误安全访问密钥校验失败0x37延迟时间未到安全访问尝试过于频繁0x72一般编程错误擦除或下载过程中出错状态机设计的核心是任何时刻协议栈都清楚自己处在哪个状态收到什么消息能做什么动作。我习惯把会话状态、安全访问状态、传输层状态分别用枚举变量管理状态迁移集中在一个函数里处理避免业务代码到处修改状态。协议栈的每个入口在接收诊断请求后第一件事是校验合法性再决定是否执行不要边执行边校验否则很容易在中间状态收到意外消息。4. 协议栈与驱动层衔接的工程细节4.1 帧接收缓存与消息队列怎么设计诊断 CAN 报文的接收通常发生在中断上下文处理则放在任务上下文。中间靠一个缓存队列来衔接。最基础的做法是环形缓冲接收中断把 CAN 帧数据写入环形缓冲主循环或 RTOS 任务周期性取帧处理。要注意的是环形缓冲的读写指针要保证原子性中断里只能写任务里只能读防止读写冲突弄丢数据。缓冲区大小也要评估。诊断场景下如果诊断仪使用功能寻址同一时刻可能进来多帧ISO-TP 首帧和连续帧之间也可能有较短的间隔。缓冲区太小会丢帧太大浪费内存。我习惯的做法是按总线负载率和最长连续接收帧数估算一般节点用 16 到 32 条帧缓冲就足够但如果有大量诊断刷写请求缓冲区适当加大到 64。还有一种容易被忽视的身份过滤问题。功能寻址的目标是所有节点物理寻址只针对自己但总线上所有报文都会进接收中断。如果没有在驱动层过滤 IDISO-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连续错误回 0x3722/2E 读写有效 DID、无效 DID、未解锁写正常读写无效 DID 回 0x3119 读 DTC不同状态掩码、无故障时读取返回对应 DTC 记录或空列表31 例程有效例程、无效例程、会话不满足正常执行无效回 0x31刷写流程34/36/37 完整流程、块序号错误、超时中断完整流程成功错误场景回对应 NRCISO-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 或者车载以太网诊断方向走这套分层思路同样通用底层换掉上层服务逻辑基本可以原样搬过去。
返回列表