在“与 ISO-TP 交互”一节中介绍的 ISO-TP 层允许发送和接收最大长度为 4095 字节的任意有效载荷。现代车辆中的 ECU 很可能会将 ISO-TP 数据解释为本节所述的 UDS 命令。
统一诊断服务 (UDS) 是一种用于 ECU 诊断和编程的协议。网上有许多相关的教程,你也可以找到总结其工作原理的海报。
UDS 遵循简单的请求-响应机制:你向 ECU 发送一个有效载荷,然后会收到一个作为响应的有效载荷。
请求的第一个字节表示服务 ID;有效载荷的其余部分则表示该服务的参数。第二个字节通常(但并非总是)表示服务的子功能。
应答的第一个字节表示对请求的响应:要么是肯定响应(服务 ID + 0x40),要么是否定响应(0x7f);有效载荷的其余部分则代表参数。
RAMN ECU 支持多种 UDS 服务。它们为此使用以下 ISO-TP CAN ID:
如果您将 ECU A 用作 USB 转 CAN 适配器,则它不会通过 CAN 回应您的 UDS 请求;您应改用 USB 串行接口。
您可以通过查看 ramn_uds.c 源代码文件来确认 ECU 支持哪些标准服务:
除了标准服务之外,ECU A 还增加了两项自定义服务:
这些服务将在本指南的后续部分进行说明。
服务 ID 低于 0x10 的诊断服务由 J1979 标准定义,并在 J1979 服务(OBD-II PIDs)中进行了说明。
RAMN ECU 的 UDS/J1979 实现经过了轻微简化(包含大量虚拟参数),以更好地容错并更适合初学者使用。您尤其需要注意以下与实际 ECU 可能存在的差异:
-p 0:0 选项与 isotpsend 结合使用,以零字节对 CAN 消息进行填充。当您使用功能寻址时,只能使用单帧命令(有效载荷小于 7 字节)。如果 ECU 无法处理以功能寻址方式接收到的命令,通常将完全不会作出响应。
如果 ECU 接受了您的请求,它将以一个有效载荷作为响应,该有效载荷以您所请求的服务 ID 加上 0x40 开头。例如,如果您请求的服务 ID 是 0x3E,并且 ECU 接受了该请求,则其响应将以 0x7E(0x3E + 0x40)开头。如果您请求的服务 ID 是 0x10,并且 ECU 接受了该请求,则其响应将以 0x50(0x10 + 0x40)开头。
服务通常允许您通过将服务参数的第一个比特设置为“1”(抑制肯定响应)来禁用肯定响应。
如果 ECU 拒绝了您的请求,它将以一个以 0x7F 开头的三字节有效载荷进行回复。其中,第二个字节表示被拒绝的服务,第三个字节表示错误代码。您也可以在 ramn_uds.c 中找到可能的错误代码列表。您最有可能遇到的错误代码包括:
您还可能遇到与时间相关的“错误”:
这些错误代码是极其宝贵的信息来源,有助于您学习如何正确使用 UDS 服务。
您可以分别使用 isotpsend 和 isotprecv 来发送 UDS 命令并接收其响应。此外,您还可以使用 isotpdump 以 UDS 助记符显示 UDS 流量,从而便于解析。
如果您尚未完成,请确保将您的 RAMN 配置为 CAN Linux 接口(参见将 slcan 转换为 socketCAN)。
例如,要与接口 can0 上的 ECU B(使用 CAN ID 0x7e1/0x7e9)进行交互,请打开终端并输入:
此终端将显示您 UDS 请求的响应。再打开一个终端,并输入:
该终端将显示 UDS 流量(包括请求和响应)。
最后,打开第三个终端,你将使用它来输入你的 UDS 请求。例如,你可以通过以下命令发送一个 UDS 请求:
其中“3E 00”表示你要使用“Tester Present”服务,并携带参数“0x00”。Tester Present 是一种简单的服务,用于告知 ECU 当前正在进行操作,以防止其超时。
你应该在 isotprecv 终端上观察到收到了回复“7E 00”:

这里的 7E 等于 0x3E+0x40,表示 ECU 已接受你的请求(并以参数“0x00”进行响应)。你应该在 isotpdump 终端上看到,它已自动为你解析了该请求:

如果你发送了一个无效的命令,例如将“3E 00”误发为“3E 00 00”,你应该会观察到不同的 UDS 通信流量:

ECU 的响应为“7F 3E 13”。“7F”表示服务 ID 为“3E”的请求因错误代码“13”而被拒绝,正如您在 isotpdump 终端中所看到的,“13”的含义是“消息长度错误或格式无效”。
如果您想与除 ECU B 之外的其他 ECU 进行通信,则需要为所有命令更改 -s 和 -d 选项。您可以使用 candump can0,7e0:7f0 代替 isotpdump 来显示任何 UDS 流量,但它不会为您解析 UDS 命令。
对于许多服务而言,第一个参数仅为 7 位长,且第一位用于指示是否抑制正面响应。如果你使用参数“0x80”,实际上发送的是参数“0x00”,并要求 ECU 在无错误时无需应答。
例如,如果你输入以下命令:
除非发生错误,否则你将不会收到来自 ECU 的任何响应。
你可能希望使用别名来缩短上述命令。你可以通过以下脚本创建有用的别名:
你需要为每个打开的终端执行此脚本(可以简单地将内容复制粘贴到终端中并执行)。执行之后,你将能够通过以下方式从 ECU B 接收数据:
并简单地通过以下方式将数据发送至 ECU B:
您也可以类似地使用命令别名 dumpECUB 来转储上述流量。
许多 UDS 服务默认情况下不可用,您首先需要使用诊断会话控制服务请求一个“诊断会话”。默认情况下,ECU 处于“默认会话”(代码 0x01)。其他可能的标准会话包括“编程会话”(代码 0x02)、“扩展诊断会话”(代码 0x03)以及“安全系统诊断会话”(代码 0x04)。
真实的 ECU 通常会验证车辆状态是否允许会话切换。例如,在驾驶车辆时,您不应能够启动编程会话。如果将 RAMN 连接到驾驶模拟器,ECU 将检查车辆当前是否处于停止状态。如果未满足此条件,ECU 将以错误代码“0x22”进行响应,该代码表示“条件不正确”。
要使用此服务,只需将会话代码作为唯一参数提供,例如启动一个编程会话:

此服务可用于重置 ECU。它仅有一个参数,即重置类型。RAMN ECU 仅支持重置类型 0x01(硬重置),使用方法如下:
或者,如果您不希望 ECU 在接收到请求时作出响应:
如果 ECU 处于默认会话状态,则不会接受重置请求;您必须先使用诊断会话控制 (0x10) 才能使用此服务。
此命令支持功能寻址。如果您希望同时重置所有 ECU,可以将这些命令发送至 ID 0x7df:
请注意,这里的 -d 7e9 并不重要;该命令会被所有 ECU 接收并处理。

这是从 ECU 读取数据的一种常用服务。它接受两个字节作为参数,这两个字节代表您希望读取的 16 位数据标识符 (DID)。尽管某些 ECU 允许一次性读取多个 DID,但 RAMN ECU 每次请求仅允许读取一个 DID。
部分 DID 具有标准含义(请参阅本教程以获取列表)。例如,您可以使用 DID 0xF184 查询 ECU 固件的编译时间:

您也可以使用 DID 0xF18C 查询 ECU 的序列号硬件(该硬件在每个 ECU 中应是唯一的):

通过 ID 读取数据可能会返回较长的数据负载,这意味着它们将通过 ISO-TP 进行分段传输。您必须主动设置一个 ISO-TP 接收器(例如,使用 isotprecv -s 7e1 -d 7e9 -l can0 )。如果没有激活的接收器,ECU 将无法接收到继续传输所需的“流控制帧”,您只会观察到其响应中的“首帧”。
您也可以使用 UDS 写入任意 DID。只需提供您想要写入的 DID 以及要写入的数据即可。例如,DID 0xF190 指的是 ECU 的车辆识别码(VIN)。您可以使用通过标识符写入数据服务,将一个 17 位字符的字符串写入 DID 0xF190。请务必先启动一个编程会话:
然后,使用通过标识符写入数据服务:
现在,您应该能够使用通过标识符读取数据来读取之前写入内存的任意 VIN:

该值被写入闪存,因此即使在复位后仍会保留。如果您重新刷写 ECU 并重置其内存,通过标识符读取数据服务可能会提示您的请求超出范围。
您可以安装 xxd 工具,以便在可读的 ASCII 文本与 isotpsend 和 isotprecv 所使用的十六进制字符串之间轻松进行转换:
使用以下命令可将 ASCII 字符串(例如 VIN0123456789ABCD)转换为 isotpsend 可识别的十六进制字符串(例如 2E F1 90 56 49 4E 30 31 32 33 34 35 36 37 38 39 41 42 43 44):
反之亦然:
汽车爱好者通常希望获取的一类信息是诊断故障代码(DTC)。DTC 是车辆中出现的问题报告,并由 ISO 15031-6 标准定义。
DTC 由一个字母(U、C、P 或 B)和四个数字组成。
字母表示问题所属的领域:“U”代表网络(ECU A),“C”代表底盘(ECU B),“P”代表动力总成(ECU C),“B”代表车身(ECU D)。第一位数字表明该 DTC 是标准定义(“0”)还是制造商特定定义(“1”)。
例如,DTC“P0650”表示动力总成领域出现了问题。其中的“0”表示该 DTC 为标准定义,在此上下文中,“6”代表“计算机输出电路”,而“50”则表示“故障指示灯(MIL)控制电路故障”。
网上有大量关于 DTC 解读的信息可供参考。如果第一位数字是零,则 DTC 具有唯一定义;但如果第一位是“1”,则其定义因制造商而异,并且在不同车辆中可能有不同的含义。
在较早的 KWP2000 协议中,DTC 曾以两个字节的形式存储,该协议早于 UDS。UDS 引入了第三个字节,即故障类型字节(FTB),以报告更多关于问题的详细信息。用于定义 DTC 值的两个字节分别称为“高字节”和“中字节”,而“低字节”则代表 FTB。
例如,DTC P0650 在 UDS 中的表示如下:
在 UDS 负载中,这将显示为 06 50 <FTB>。
最后,一个字节的最低位用于指示 DTC 的状态。该字节的每一位代表一个标志,其定义如下:
因此,ECU 总共会向您发送每个 DTC 四个字节。要请求读取 DTC,您需要提供一个子功能和一个 DTC 状态掩码。RAMN ECU 支持子功能 0x01,该功能返回与所提供掩码匹配的 DTC 数量;还支持子功能 0x02,该功能在同一帧中串联返回实际的 DTC。
例如,如果您想请求待处理的 DTC 数量,可以使用子功能 0x01,并设置参数 0x04(必须将“待处理 DTC”置位):
ECU 会以六个字节进行响应:
出于演示目的,RAMN ECU 在复位时会确保其内存中至少包含一个 DTC。该 DTC 的标志位始终标记为待处理状态,并且 ECU 不允许您通过掩码进行过滤。
您可以使用以下命令询问 ECU B 内存中有多少个 DTC:
您还可以使用以下命令要求 ECU B 向您发送所有 DTC:

“59 01 04 00 00 00 01”的含义是:ECU 接受了您针对服务 0x19 和子功能 0x01(读取 DTC 数量)的请求,它仅支持 0x04 掩码(用于待处理 DTC),采用 DTC 格式 0x00,并且内存中存储了 0x0001 个 DTC。
“59 02 04 45 63 00 04”的含义是:ECU 接受了您针对服务 0x19 和子功能 0x02(读取 DTC)的请求,它支持 0x04 掩码,并且存在一个 DTC:0x4563,故障类型位为 0x00,状态为 0x04(待处理)。0x4563 以“01”开头,表示这是一个底盘域的 DTC(“C”),因此对应的 DTC 为“C0563”(出于演示目的,此处为任意设定)。
此服务可用于从 ECU 的内存中清除 DTC。它使用一个 3 字节的参数来指示要清除哪一组 DTC。与排放相关的系统 DTC 可以通过“00 00 00”来清除,而所有 DTC 则可通过“FF FF FF”来清除。其他可能的值则由制造商自定义。
例如,您可以使用以下方式清除 ECU B 内存中的所有 DTC:
您可以通过在执行该命令前后分别读取 DTC 的数量,来验证 DTC 是否已被清除。ECU 在复位事件后会自动重新生成 DTC。

此服务允许临时禁用 DTC,以防止 ECU 在诊断过程中添加新的 DTC。您可以使用子功能 0x01 来允许新 DTC 的生成,使用 0x02 来禁用它们。
启用 DTC:
禁用 DTC:
某些服务可能要求您先解锁 ECU,然后才能使用。您会发现这些服务,因为它们会对您的请求回复错误代码 0x33(拒绝安全访问)。
安全访问服务可用于解锁 ECU。
安全访问可用于通过 ECU 实现简单的质询/响应身份验证。您首先需要从 ECU 请求一个“种子”。然后,您必须执行一些绝密算法,以从该种子计算出一个“密钥”,并将该“密钥”发送到 ECU 以解锁它。请注意,此处的密钥并不指加密密钥;而是指对质询的响应。
此服务有不同的安全级别。要请求级别 1 的种子,请使用以下命令:
您应该注意,ECU 会向您发送一个 4 字节的种子作为响应。该种子由 ECU 的真随机数生成器 (TRNG) 生成。您可以请求任意数量的种子。

要解锁 ECU,您需要计算种子值与 0x12345678 的异或值。这仅用于演示目的,并不是一个安全的身份验证机制。
在 Linux 中,您可以使用以下命令计算要发送到 ECU 的“密钥”(以 7D 70 9F 4D 为例):
您可以使用与请求相同的命令,但将安全级别加 1,以将您的答案(在本例中为 6F 44 C9 35)发送到 ECU:
如果 ECU 接受您的 UDS 请求(第一个字节为 0x67),则表示您提供了正确的“密钥”,ECU 现已解锁到级别 1。

在尝试使用安全访问时,您可能会遇到以下错误代码:
例程控制服务可用于实现标准服务未涵盖的功能。例程通过一个两位字节的标识符进行识别。与 DID 类似,UDS 标准定义了许多标准例程标识符,但标识符 0x0200 至 0xDFFF 为 ECU 专用。
例程控制服务可配合三种子功能使用:
此服务使用以下参数:<子功能> <例程标识符> <可选例程参数>。RAMN ECU 提供了以下例程:
由于这些例程可能会修改 ECU 闪存,因此请勿随意操作,除非您清楚自己在做什么。有关如何使用这些例程,请参阅 ramn_uds.c 。
例如,您可以要求 ECU B 停止发送周期性报文,方法如下:
并且你可以要求 ECU B 恢复发送周期性报文,方法是:
按地址读取内存服务可用于读取任意 ECU 地址。您自然需要提供两个参数:地址和要读取的字节数。
与按标识符读取数据服务所使用的 DID 不同,实际 ECU 地址的大小可能因微控制器架构而异。因此,您必须提供第三个参数,用于指定地址和内存字段的大小(地址长度格式标识符)。该参数为一个字节,其高 4 位表示“size”参数的大小,低 4 位表示“address”参数的大小。
按地址读取内存参数的格式为<格式标识符> <地址> <size>。
例如,假设您想从地址 0x08000000(RAMN ECU 程序闪存的起始地址)读取 4 个字节。“4”可以容纳在一个字节中,因此您可以使用一个字节来提供要读取的大小。STM32 微控制器使用的地址为 32 位长(4 字节)。因此,您可以使用格式标识符 0x14(地址 4 字节,size 1 字节)。
你可以通过以下方式请求内存读取:
格式标识符指的是大小参数的尺寸(它并非您想要读取的字节数),这一点可能会让一些人感到困惑。如果您愿意,可以将大小参数(4)作为 2 字节或 4 字节的参数来提供。因此,以下命令与上述命令严格等效:
该服务会立即返回所读取的数据。例如,您可以使用以下方式读取 ECU B 程序闪存的前 256 字节(地址为 0x08000000):

请注意,在这种情况下,请求和响应均为分片的 ISO-TP 帧,因此您必须在另一个终端上启用 isotprecv。
同样,您也可以读取微控制器的 RAM(起始地址为 0x20000000):

如果你想了解 RAMN ECU 将变量存储在哪些地址上,必须编译固件,并查看构建过程生成的“.map”文件。
按地址写入内存与按地址读取内存的工作方式相同,只是它额外需要一个参数来指定您希望写入到指定地址的数据。由于这允许覆盖 RAM(且仅限于 RAM),如果您不清楚自己在做什么,可能会导致 ECU 崩溃。
例如,您可以使用以下命令将“01 02 03 04”写入内存地址 0x20000000:

此命令仅在您首先请求编程会话并通过安全访问(0x27)解锁 ECU 后才会被接受。您可以通过在操作前后使用按地址读取内存功能来验证内存是否已正确写入。变量地址取决于您所使用的固件的确切版本。在本示例中使用的版本中,0x20000000 对应于 CAN 适配器的“错误状态”变量,该变量可被覆盖而不会影响稳定性。如果您不清楚您的命令覆盖了哪些变量,则应重置 ECU。
此服务可用于更改 CAN 总线的波特率,例如,在通过 UDS 重新刷写 ECU 时可提高速度。它需要两个步骤:
请注意,虽然真实的 ECU 在诊断会话结束后会自动恢复到原始波特率,但 RAMN ECU 则会保持当前波特率直至下次复位。标准实现采用功能寻址和正面响应抑制,以同时向所有 ECU 发送切换命令。然而,RAMN ECU 会在切换波特率前先关闭其 CAN 控制器并等待一秒钟,以更好地容忍更多的时序问题,因此您可以单独与每个 ECU 进行通信。
您可以使用此服务,它包含以下三种子功能之一:
RAMN ECU 支持子功能 0x01 和 0x03。您必须为子功能 0x01 提供的参数是一个字节的标识符:
要切换到新的波特率,请使用子功能 0x03,无需任何参数。如果您希望在无错误时让 ECU 不予响应,请改用 0x83。
在更改 ECU 的波特率时,您还需要同步更新 CAN 适配器的波特率。如果您使用的是 slcan 适配器,则需要重启 slcand,并使用 -s 选项(参见 CAN 波特率)。
对于 125000 bps 使用 -s4,250000 bps 使用 -s5,500000 bps 使用 -s6,1000000 bps 使用 -s8。
例如,可以使用以下命令将所有 ECU 的 RAMN 波特率逐一更新为 1000000 bps(假设您的接口为/dev/ttyACM0):
这还将重启您的 CAN 接口,因此您需要重新启动所有 CAN 命令。

你可以通过使用功能寻址,将命令同时发送至所有 ECU,从而简化通信流量,并利用正面响应抑制功能,要求它们在未发生错误时无需应答。
这样仅需两条 CAN 报文即可实现相同的波特率变更。

此服务可用于向 ECU 请求数据“上传”。请注意,在嵌入式系统中,“上传”通常指的是从 ECU 到计算机的数据传输(这意味着您正在将数据“下载”到您的计算机)。这可用于转储 RAMN ECU 的闪存。虽然使用按地址读取内存(0x23)服务也可以达到相同的效果,但该服务在可用时支持压缩和加密。
其使用方法如下:
此服务包含多个参数:
例如,你可以通过以下方式请求从地址 0x08000000 转移 256(0x100)字节:
ECU 应当回复:

传输数据用于实现由另一服务启动的传输(例如,请求上传 (0x35) 或请求下载 (0x34))。每次调用传输数据服务对应于一个数据块的传输。您必须提供一个“块计数器”,第一个数据块的计数器从 0x01 开始。当计数器达到 0xFF 后会溢出,并从 0x00 重新开始。
例如,在请求上传 (0x35) 示例之后,您可以执行以下命令:
ECU 将以块计数器的回显作为响应,随后发送待读取的数据。
如果您尝试读取或写入超出指定数据大小范围的数据,将收到错误代码 0x24,表示“请求序列错误”。

如果您正在写入数据,则必须在块计数器之后提供数据字节,而 ECU 仅会以块计数器的回显作为响应。
请求传输退出服务用于终止传输。如果您希望确认下载或启动另一项传输,则必须调用此服务。此服务可能包含可选参数,但 RAMN ECU 无需任何参数,因此只需简单地发送“37”即可:
ECU 应当以“77”作为回应。

请求下载的工作方式与请求上传 (0x35) 相同,但数据位于客户端的请求端,而非 ECU 的应答端。仅当 RAMN ECU 具有双内存区(微控制器参考号以 CET6 结尾)时才支持此功能。与按地址写入内存 (0x3D) 不同,此服务可将数据写入 ECU 的闪存,例如用于固件更新。
例如,您可以使用以下命令在地址 0x08000000 处启动 0x100 字节的下载:
ECU 将指定后续“传输数据”调用中必须包含的数据大小,因此您必须根据 ECU 返回的值进行相应调整。
完成固件文件的“请求下载”传输后,并不会立即使 ECU 使用您上传的新固件。您还需要结合例程控制来验证新固件。对于 RAMN,这可以通过以下方式实现:
低于 0x10 的服务由 J1979 标准定义(参见 OBD-II PID)。RAMN 支持以下 J1979 服务:
此服务可用于读取车辆的当前数据。使用时需提供一个字节的参数,该参数表示您希望读取的数据(“PID”)。PID 0x00 用于指示 ECU 支持哪些 PID(从 0x01 到 0x20)。ECU 将以 4 个字节(0x20 位)进行响应,当对应 PID 被支持时,相应位将被置为 1(请参考此链接以获取示意图)。如果 PID 0x20 被支持,则它依次用于指示从 0x21 到 0x40 的哪些 PID 被支持,以此类推。
例如,您可以使用以下命令询问 ECU C 支持哪些 PID:

请注意,isotpdump 无法解析这些命令,因为从技术上讲,它们并非 UDS 服务。
ECU 以 41 00 BE 1F A8 13 响应命令 01 00。与 UDS 类似,41 对应于服务 ID 加 0x40,表示这是一个肯定响应。第二个字节是对请求参数的回显。
BE 的二进制表示为 10111110,这意味着 ECU C 支持 PID 1、3、4、5、6、7,但不支持 PID 2 和 8。您可以通过尝试读取 PID 1、2 和 3 来验证这一观察结果。与 UDS 类似,否定响应由 0x7F 加上请求的服务 ID 和错误代码组成。

您可以在 OBD-II PIDs 的维基百科页面上找到每个 PID 的含义和格式。以下 PID 可能尤其值得关注:
请注意,RAMN 的 J1979 实现所返回的许多值均为占位符值,可能无法正确反映模拟器的状态。不过,PID 0x1F 和 0x49 应始终得到正确实现:

在此情况下,ECU C 回复当前加速踏板位置为 0xFB(98%),我们可以观察到两次读取 PID 0x1F 命令之间间隔了 4 秒。
服务 0x03 用于请求 ECU 存储的 DTC。与 UDS 不同,该服务无需提供任何参数。相反,若要请求待处理 DTC 或永久性 DTC,应使用其他服务(分别为 0x07 和 0x0A)。需要注意的是,RAMN 会将服务 0x07 和 0x0A 视为服务 0x03 的别名,因此您也可以直接使用它们。
有关 DTC 的更多信息,请参阅读取 DTC 信息(0x19)。此服务每个 DTC 仅返回 2 个字节(没有状态掩码或 FTB)。您可以使用以下命令要求 ECU C 显示其存储的 DTC:

ECU C 的响应为 43 01 01 72。第一个字节 43 表示肯定回答,01 表示存在一个 DTC,其值为 0172。
此服务可用于清除 DTC,也可在不带参数的情况下使用。例如,您可以使用以下命令来读取 DTC、清除 DTC 并再次读取 DTC:

此服务与请求当前诊断数据 (0x01) 类似,用于读取车辆信息。它同样使用一个字节的参数来指定要读取的 PID,其中 PID 0x00 用于指示支持哪些 PID。PID 0x02 对应 VIN,PID 0x0A 对应 ECU 名称。您可以通过以下命令读取这些 PID:

请注意,如果您尚未将 VIN 写入 ECU,则返回的答案将仅为零。