第4章 UDS 服务详解(下):0x2F、0x31、0x34、0x35、0x36、0x37

好,咱们接着往下聊。上一章我们把UDS的诊断和读写服务讲透了,这一章要啃的,是真正跟「控制」和「数据传输」相关的硬骨头。说白了,这些服务才是ECU刷写、标定、执行特殊动作的核心。我个人习惯把这几个服务称为「动手派」——它们不只是看看数据,而是要实实在在地改变ECU的状态。

4.1 输入输出控制 (0x2F) —— 让ECU听你的

0x2F服务,全称是InputOutputControlByIdentifier。你想想看,平时我们读数据是用0x22,那是只读不写。但有时候,我们需要强制让某个输出引脚拉高,或者强制给某个传感器一个模拟值,这时候就得靠0x2F了。

我记得有一次在台架上调试一个雨量传感器,ECU死活不认我给的模拟信号。后来发现,就是0x2F服务的控制参数没配对。嗯,这里要注意,0x2F不是你想控制就能控制的,它有三个核心要素:

  • 控制参数(Control Option):你打算怎么控制?是强制到某个值、冻结当前值、还是恢复为默认值?
  • 数据标识符(DID):你要控制哪个输入或输出?这个DID跟0x22读的那个DID是同一套编号体系。
  • 控制值(Control Value):你要设成什么值?比如让PWM占空比变成50%。

核心要点:0x2F服务一旦激活,ECU会暂时放弃对那个信号的自控权。如果你不主动发「恢复默认控制」的命令,ECU可能就一直保持你设的那个值。我见过有工程师做完测试忘了恢复,结果车子开出去报了一堆故障码。

来看一个典型的请求报文格式:

请求: 0x2F + DID(2字节) + 控制参数(1字节) + 控制值(可变长度)
示例: 02 2F 01 02 03 00 64
      含义:请求控制DID=0x0102,控制参数=0x03(强制到指定值),控制值=0x0064(100)

避坑指南:我曾经在做一个BCM项目时,用0x2F控制车灯输出。结果发现,有些ECU在收到0x2F后,会立即响应一个肯定应答,但实际输出根本没变。为什么?因为ECU内部有安全校验,你得先通过安全访问(0x27)才行。所以,0x2F通常跟0x27是绑定的,别漏了这一步。

4.2 例程控制 (0x31) —— 让ECU跑一段程序

0x31服务,RoutineControl。这个服务很有意思,它不像0x2F那样直接控制一个信号,而是让ECU执行一段预设好的「例程」。你可以把它理解成ECU内部的一个小程序,你通过诊断命令来启动、停止或者查询它的执行结果。

我个人习惯把例程分成三类:

  • 启动例程(Start):让ECU开始跑某段代码。比如擦除Flash、执行自检、复位某些学习值。
  • 停止例程(Stop):让正在跑的例程停下来。注意,不是所有例程都支持中途停止。
  • 请求例程结果(Request Results):查询上次例程跑完后的状态码或数据。

你想想看,为什么需要0x31?因为有些操作不是简单的「读」或「写」能搞定的。比如你要擦除一个Flash扇区,你得先发0x31启动擦除例程,然后ECU内部会执行一系列复杂的时序操作,完成后给你一个结果。这个过程,用0x2F是做不到的。

重要提醒:0x31服务的例程ID(Routine ID)是跟具体ECU强相关的。同一个ID,在A款ECU上是「自学习」,在B款ECU上可能是「自毁程序」。所以,做一致性测试时,一定要对照着ODX文件或者ECU的规范来,别想当然。

报文格式也很直观:

请求: 0x31 + 子功能(1字节) + 例程ID(2字节) + 可选参数
示例: 03 31 01 02 03 00 0A
      含义:子功能=0x01(启动),例程ID=0x0203,参数=0x000A(10)

4.3 数据传输三剑客:0x34、0x35、0x36、0x37

接下来这四个服务,我习惯把它们放在一起讲。因为它们是一个完整的「数据传输生命周期」:请求下载、请求上传、传输数据、请求退出。说白了,这就是ECU刷写和读取大块数据的标准流程。

4.3.1 请求下载 (0x34) —— 告诉ECU:我要写数据了

0x34服务,RequestDownload。它的作用就是跟ECU打个招呼:「嘿,我要往你肚子里塞数据了,你准备好没?」ECU收到后,会告诉你它能接受的最大数据块大小,以及一个用于后续传输的地址或块序号。

我记得第一次做刷写协议栈时,被0x34的「内存地址」和「内存大小」参数搞晕过。后来才明白,这两个参数不是随便填的,它们必须跟ECU的Flash分区对齐。比如,你要写的是Bootloader区域,那地址范围就不能落在Application区。

请求: 0x34 + 数据格式(1字节) + 地址和长度信息(可变)
示例: 34 00 44 00 10 00 00 00 00 10 00
      含义:数据格式=0x00(无加密),地址长度=4字节,长度长度=4字节
            地址=0x00100000,长度=0x00001000(4KB)

避坑指南:我曾经遇到一个ECU,0x34请求发过去,它返回了否定应答0x31(请求超出范围)。查了半天,发现是地址对齐问题——ECU要求地址必须是4字节对齐的,而我传了个奇数地址。所以,做0x34之前,先搞清楚ECU的对齐要求。

4.3.2 请求上传 (0x35) —— 反过来,从ECU读大块数据

0x35服务,RequestUpload。跟0x34正好相反,它是告诉ECU:「我要从你肚子里读数据了,你准备好没?」通常用于读取ECU的完整内存、日志文件或者标定数据。

你想想看,为什么不用0x22来读大块数据?因为0x22一次只能读一个DID,而且数据长度有限制。而0x35配合0x36,可以连续传输任意大小的数据块,效率高得多。

报文格式跟0x34几乎一样,只是服务ID不同:

请求: 0x35 + 数据格式(1字节) + 地址和长度信息(可变)
示例: 35 00 44 00 10 00 00 00 00 10 00
      含义:跟0x34类似,只是方向反了

4.3.3 传输数据 (0x36) —— 真正的搬运工

0x36服务,TransferData。这是整个数据传输流程的核心。不管是下载还是上传,实际的数据都是通过0x36一包一包地传的。每一包数据都有一个块序号(Block Sequence Counter),ECU用这个序号来检查有没有丢包或者乱序。

我个人习惯把0x36的传输比作「流水线作业」:你发一包,ECU收一包,然后回复一个肯定应答。如果ECU回复否定应答,比如0x21(忙),你就得等一会儿再重发。如果回复0x13(报文长度错误),那你就得检查数据长度是不是超过了0x34协商的最大值。

请求: 0x36 + 块序号(1字节) + 数据(可变长度)
示例: 36 01 01 02 03 04 ... (最多4095字节)
      含义:块序号=0x01,后面跟着实际数据

重要提醒:块序号是从0x01开始的,不是0x00。而且,每一包的块序号必须连续递增。如果你发了序号0x01,下一包跳到了0x03,ECU会认为你丢包了,直接回复否定应答。我曾经在调试时犯过这个错,查了整整一天才发现是序号计数器没初始化。

4.3.4 请求传输退出 (0x37) —— 收尾工作

0x37服务,RequestTransferExit。数据传输完了,你得告诉ECU一声:「好了,没数据了,你可以收工了。」ECU收到后,会做一些收尾工作,比如校验数据的完整性、更新校验和、或者重启相关模块。

嗯,这里要注意,0x37不是可选的。如果你不发送0x37,ECU可能会一直处于「等待数据」的状态,导致后续的诊断服务无法正常执行。我见过有工程师刷写完成后忘了发0x37,结果ECU死在那里,只能断电重启。

请求: 0x37 + 可选参数(通常为空)
示例: 37 00
      含义:请求退出传输,不带额外参数

总结一下数据传输流程:

  1. 先发0x34(下载)或0x35(上传),协商传输参数。
  2. 然后循环发0x36,一包一包地传数据。
  3. 最后发0x37,通知ECU传输结束。

这个流程,说白了就是UDS刷写的标准模板。不管你是刷Bootloader、Application还是标定数据,都逃不出这个套路。

4.4 实战中的常见坑

讲了这么多,我最后再分享几个实战中遇到的坑,希望能帮大家少走弯路:

  • 超时问题:0x36的传输间隔不能太快,也不能太慢。太快了ECU处理不过来,会回0x21(忙);太慢了,ECU可能会认为传输超时,自动退出。我一般建议间隔控制在10-50ms之间。
  • 数据长度:0x36每包的数据长度,不能超过0x34协商的「最大数据块大小」。如果超过了,ECU会回0x13(报文长度错误)。
  • 安全访问:大多数ECU要求先通过0x27安全访问,才能使用0x2F、0x31、0x34等服务。别想着跳过这一步,ECU没那么傻。
  • 会话状态:0x2F和0x31通常需要在扩展会话(0x03)下才能使用。如果你在默认会话(0x01)下发这些服务,ECU会回0x7F(服务不支持)。

好了,这一章的内容就到这里。下一章我们会聊UDS的传输层和网络层,那才是真正考验协议栈功底的地方。咱们下回见。