做汽车电子开发的人,应该都绕不开UDS这三个字母。Unified Diagnostic Services,统一诊断服务,标准代号ISO 14229-1,说人话就是一套让诊断仪和ECU之间“对话”的规则。你刷写ECU、读故障码、做产线检测、搞OTA,底层跑的都是这一套。标题里带着“05-”的编号,我猜这是某个系列教程的第五篇,前面几篇大概讲过CAN总线、报文格式、网络管理这些基础,那这篇正好落到应用层,可以动手串起来用了。
这篇不打算念标准条款。标准文档写得很晦涩,我在项目里真正用到的其实就是几个核心概念和服务。我按自己做诊断开发时的理解,把这套东西拆开揉碎讲清楚:它到底解决什么问题、核心服务怎么用、报文长什么样、怎么模拟一次真实的诊断流程、以及我实际调板子时踩过的坑。不管你是刚转行做车载测试,还是写ECU固件想补诊断这块,应该都能有点收获。
1. 先搞清楚UDS到底在解决什么问题
1.1 没有UDS的时候,诊断是怎么做的
早年间,每家ECU供应商都有自己的私有诊断协议。你诊断仪想跟ECU通信,就得按供应商的私有指令来。一个项目里往往有多个供应商的ECU,你做售后诊断工具、产线检测工具,就得挨个适配各家协议,代码里全是“if 供应商A then... else if 供应商B then...”这种逻辑,维护成本非常高。
后来业界发现这条路走不通,于是统一了应用层协议,这就是UDS。统一之后,诊断仪和ECU之间变成了标准化的“语义通信”:服务编号是统一的,会话管理是统一的,故障码格式是统一的,就连“没权限、数据不对、条件不满足”这类错误码也是统一的。你写一套诊断逻辑,就能覆盖整车绝大多数ECU。这才是UDS真正的价值:它把诊断从“一厂商一套黑话”变成了“行业通用普通话”。
1.2 UDS在OSI模型里的位置
经常有人把UDS和CAN混淆。CAN是物理层和数据链路层,负责把报文从一个节点传到另一个节点;UDS是应用层协议,负责定义“数据里装的到底是什么含义”。中间还隔着一层ISO 15765-2,也就是常说的DoCAN,负责网络层的分包和重组。
为什么需要这个传输层?因为CAN单帧数据最多8个字节,而诊断数据经常超过8字节。比如读一个VIN码,响应可能20字节;刷写固件更是几百KB的大块数据。这些长数据必须拆成多个CAN帧发送,接收方再按顺序装回去。UDS只管“说什么”,ISO 15765-2管“怎么把长话拆成短句说”。入门阶段你可以把这两层分开看:先理解UDS服务的含义,再理解长数据怎么分包,最后合起来就能看懂完整报文了。
1.3 会话、子功能、服务这几个概念必须先理清
UDS里有三个基础概念特别容易绕晕:服务、子功能、会话。
服务就是请求报文的第一个字节,也叫SID。比如0x22是读数据,0x2E是写数据,0x10是切换会话,0x27是安全访问。每个服务后面可以带子功能,比如0x10后面跟0x01、0x02、0x03,分别表示切到默认会话、编程会话、扩展会话。子功能是“这个服务具体要干什么”。
会话则代表ECU当前所处的工作状态,类似一个权限等级。默认会话(0x01)权限最低,只能做读故障码、读VIN、保活这类基本操作;扩展会话(0x03)开放更多诊断能力,比如执行器测试、配置参数读取;编程会话(0x02)权限最高,用于刷写固件,通常还会要求先做安全访问解锁。
这三个概念贯穿整个UDS体系。你操作任何一个ECU,基本都会遇到“先切换会话,再解锁,再执行具体服务”这样的组合流程。不理解权限模型,很多负响应码你会莫名其妙。
2. 核心服务逐个拆解
2.1 0x10 诊断会话控制
0x10是最常碰到的服务,功能是切换会话。诊断仪上电后,ECU默认停在默认会话。你想做写数据、例程控制、刷写这类操作,必须先切到扩展会话或编程会话。
请求格式看一个实例:进扩展会话,UDS报文内容是两个字节[10, 03],经ISO-TP封包后CAN帧数据为02 10 03 00 00 00 00 00。这里的第一个字节02是ISO-TP的单帧长度指示,表示后面有两个有效诊断字节,不是UDS服务的一部分。这个细节新手很容易懵,我见过有人把02当成服务号去查,结果半天对不上。
正响应一般长这样:06 50 03 00 32 01 F4。50是0x10的正响应SID(原SID加0x40),03表示已切到扩展会话,后面跟的是P2定时时间和P2*定时时间,单位毫秒。负响应则是03 7F 10 22,0x22是负响应码,含义是条件不满足。
这里要特别注意:不少ECU对会话切换有条件限制。比如从扩展会话切编程会话,可能要求车速为0、发动机停止、点火状态正确,否则直接回0x22。我在台架上调某个ECU时,发现一切编程会话就报条件不满足,排查了一圈,最后发现是没给模拟点火信号。调试遇到0x22,先别急着怀疑协议解析,多半是整车条件没满足。
2.2 0x27 安全访问
安全访问是给诊断操作“上锁”的服务,编号0x27。刷写、标定、写VIN这类高风险操作,ECU不会让你随便执行,必须先通过安全校验。
流程是两步:诊断仪先发02 27 01 ...请求种子(seed);ECU回正响应,带一串随机数种子;诊断仪用约定好的算法算出密钥(key),再发02 27 02 ...带key上去;ECU校验通过后回正响应,解锁完成。
这个机制为什么重要?因为刷写和配置操作如果可以被随便触发,车载系统很容易被恶意破坏或篡改。种子的存在让通信具备了一定的防重放能力:每次seed不同,就算你录了一段合法报文反复重放,也过不了验证。
实际开发中的坑主要在算法和时序上。不同整车厂用的密钥算法完全不同,有的就是简单异或加移位,有的是真正的加密算法,算法资料通常在整车厂手里,第三方工具必须拿授权。还有一个很容易踩的坑:连续输错key,ECU会进入锁定状态,锁定期间你连seed都拿不到,必须等解锁时间窗口过去。写过诊断上位机的人应该都体会过被0x36、0x37支配的感觉。
2.3 0x22和0x2E 数据读写
0x22是读数据,0x2E是写数据,这两个服务对标的都是DID(Data Identifier,数据标识符)。DID是两个字节,用来唯一标识一个数据对象,比如0xF190是VIN码,0xF18C是软件版本号,0xF15C可能是一组标定参数。
读数据的请求很简单,比如读VIN:UDS报文是[22, F1, 90],CAN帧数据为03 22 F1 90 00 00 00 00,正响应是06 62 F1 90 4C 53 56 47这样的格式,62是正响应SID,F190是DID,后面就是数据内容。如果数据超过单帧容量,就触发多帧传输,这个下一节细讲。
写数据0x2E的请求格式是03 2E F1 90 4C,正响应SID是6E。注意,写操作通常需要同时满足两个条件:扩展会话激活,而且已经完成安全访问解锁。另外不是所有DID都能写,产品级ECU里可写的DID往往少得可怜,绝大多数是只读的。我在实车测试时遇到过一个问题:写完DID后ECU没有立即生效,后来查规范发现有些DID要触发一次例程控制才会真正写入非易失存储器。这提醒我们,DID读写和例程控制往往是配合使用的。
2.4 0x31 例程控制
例程控制0x31是“让ECU做一件具体事情”的服务,它不像读数据那么“文静”,更像是“点击运行按钮”。比如传感器自学习、节气门位置标定、零件老化测试、DTC清除后的自检,都通过例程控制来触发。
请求格式:04 31 01 F1 90 ...,31是SID,01是子功能(启动例程),F190是例程ID。正响应是0x71 + 01 + 例程ID,后面可能带一段结果数据。子功能还有02停止例程、03请求例程执行结果。
例程控制有几个实践要点。第一,例程执行时间长时,ECU不一定立刻响应最终结果,而会先回0x78(响应待定),等例程跑完再回正响应。诊断仪侧必须处理好0x78的等待逻辑,别一看还没回正响应就报超时。第二,例程参数往往和整车条件强相关,测试例程前要看清楚前置条件。我没少在这种事上吃亏:例程ID、子功能都正确,但ECU就是回0x22,最后发现是某个使能信号没置位。
2.5 0x19 读取故障码
0x19是售后诊断里最常用的服务,没有之一。它负责读DTC(Diagnostic Trouble Code,诊断故障码)。故障码的格式是三个字节,比如P0100在UDS里会编码成01 00 00。
0x19的子功能很多,不同子功能做的事情完全不一样。入门阶段至少分清这几个:01按状态掩码统计DTC数量;02按状态掩码读取DTC列表;04读取某个DTC的快照记录(冻结帧);0A读取所有DTC及其状态。请求示例:03 19 02 01,表示读取状态掩码为0x01的DTC列表,正响应格式是06 59 02 01 80 01 23 ...,后面就是DTC码和状态字节。
为什么要按状态掩码过滤?因为同一个DTC在ECU内部可能处于多种状态:当前存在、历史发生、已确认、测试失败等等。掩码的作用是只关心你想看的那一类。售后场景里,客户报故障但你在诊断仪上读不到DTC,十有八九是掩码没选对,只读了“当前存在”而故障已经跳成“历史”状态了。另外,清除故障码是0x14服务,一般也要先解锁才能执行。
讲到这里,基础的诊断服务就覆盖得差不多了。除了上面这些,还有0x28通信控制、0x34请求下载、0x36传输数据、0x37请求传输退出这组刷写服务,它们和例程控制配合起来就是完整的OTA刷写链。入门阶段可以把这套流程当成“大块数据上传”的范例来理解:请求下载获得许可,然后按块传输,最后退出并校验。
3. 报文长什么样:寻址、帧格式与时间参数
3.1 CAN寻址方式:物理寻址与功能寻址
UDS跑在CAN上,首先要解决“这帧报文发给谁”的问题。CAN标识符在诊断场景里分两种用途:物理寻址和功能寻址。
物理寻址是一对一的:诊断仪用某个专属ID发请求给特定ECU,该ECU用自己的响应ID回消息。比如请求ID是0x7E0,ECU响应ID是0x7E8,这就是一对物理地址。功能寻址是广播式的:所有ECU共享同一个请求ID,比如0x7DF,一帧广播发出去,所有支持该服务的ECU都会收到并处理。但ECU的响应仍然用各自的物理响应ID返回,避免互相冲突。
实际使用中怎么选?写入、刷写、解锁这类一对一操作,必须用物理寻址,防止“误伤”其他ECU;而一些全局性的命令,比如“所有ECU回默认会话”“所有ECU报告DTC状态”,用功能寻址更高效。需要注意,CAN ID在不同整车网络可能完全不同,有的平台用0x7E0/0x7E8,有的平台用0x710/0x718,一定要以具体车型的诊断规范为准,不要想当然套用。
3.2 单帧、多帧、流控帧怎么区分
CAN帧最多8字节数据,而诊断报文经常超长,所以网络层ISO 15765-2定义了四种帧类型。判断方法很简单,看CAN数据第一个字节的高4位:
- 0x0是单帧(SF):低4位表示后面有效诊断数据长度,后面最多7字节有效数据。
- 0x1是首帧(FF):低4位结合第二字节组成一个12位长度值,表示整个诊断消息的总长度,后面只能带6字节载荷。
- 0x2是连续帧(CF):低4位是序列号,从1开始,循环到0xF后回0,每次携带最多7字节数据。
- 0x3是流控帧(FC):第二字节高4位是流控状态,0表示继续发送,1表示等待,2表示溢出终止;第三字节是块大小BS;第四字节是STmin,连续帧之间的最小间隔。
举个例子。诊断仪请求读VIN,ECU返回的响应长度是20字节(3字节SID+DID,加17字节VIN),超过单帧上限,于是分多帧发:
第一帧首帧:10 14 62 F1 90 4C 53 56 ...。0x10表示首帧,0x14等于十进制的20,即整个诊断消息总长20字节;后面跟的是前6字节数据。
诊断仪收到首帧后,需要回一帧流控:30 00 14 ...。0x30表示继续发送,BS为0表示不限制连续帧数量,STmin为0x14表示每个连续帧之间至少间隔20毫秒。
ECU收到流控后开始发连续帧:第一帧是21 ...,第二帧是22 ...,依此类推。接收方把所有连续帧按序号拼接,再加上首帧的前6字节,就能重组出完整的20字节响应。
多帧机制是UDS抓包里最常被问到的地方,而且和刷写速度强相关。BS和STmin这两个参数直接决定了传输速率:STmin设得过大,刷写就慢;设得过小,接收方CAN控制器可能来不及处理导致丢帧。调试刷写性能时,合理配置这两个参数效果立竿见影。
3.3 正响应与负响应
UDS的响应机制有一个非常优雅的规律:请求SID是0x22,正响应SID就是0x62;请求SID是0x10,正响应就是0x50。规则是正响应SID = 请求SID + 0x40。
而负响应统一是:0x7F + 请求SID + NRC。NRC是负响应码,用来说明拒绝原因。比如ECU不支持某个服务,响应就是03 7F 22 11,0x11表示“服务不支持”。
为什么正响应要加0x40而不是原样返回?因为诊断仪经常同时和多个ECU通信,总线上一堆响应帧,加0x40后可以直接根据SID判断这是哪个请求的响应,不必额外加标识。负响应里的NRC是排查问题最重要的线索。我在调试板子时第一件事就是抓负响应码,根据NRC基本能定位到是权限不够、报文格式错,还是当前状态下根本不该调用服务。
3.4 时间参数P2、P2*、S3
诊断通信不是发完就不管了,它依赖几个时间参数保证双方行为一致。
P2是诊断仪发出请求后,等待ECU开始响应消息的最大间隔时间,默认标准是50毫秒。ECU如果在这段时间内无法给出最终响应,会先回一个0x78负响应,含义是“请求已经收到,正在处理,请继续等待”。这时诊断仪的等待窗口就要切换到P2*,P2*默认是5000毫秒,给ECU足够的时间完成操作。理解成“先回答催办单,再交正式结果”就对了。
S3是会话超时时间,默认5000毫秒。如果诊断仪在S3时间内没有发送任何诊断请求,ECU会自动从扩展会话或编程会话退回默认会话。这个机制的目的是防止诊断结束后ECU一直停在解锁的高权限状态,相当于不操作就自动锁门。写上位机时,一是要定时发0x3E保活请求防止会话超时,二是在程序退出、断连前主动发一条0x10 01回默认会话,别把ECU晾在高权限状态不管。
4. 实操:从零模拟一次“读VIN”诊断流程
4.1 工具与运行环境
前面讲的都是理论,这一节我带你实际走一遍完整流程。目标很简单:通过CAN总线,向一个ECU发送读VIN的服务请求,抓回响应并解析出VIN码。
我用的是USB转CAN适配器加Python的python-can库,纯脚本模拟诊断仪。好处是开源方案成本低,交互过程每一帧报文都能打印出来,非常适合入门理解。你手里如果有带CAN接口的开发板或者商业CAN分析工具,思路完全一样。
准备步骤:
- 把适配器接到电脑,确认驱动正常。
- 安装python-can库,
pip install python-can。 - 确认CAN通道名称和波特率。整车CAN一般是500kbps,调试时先确认ECU那边的终端电阻匹配,否则通信不稳定。
- 如果用的是真实ECU,先查诊断规范确认物理寻址ID和响应ID,比如0x7E0和0x7E8。
下面代码基于socketcan接口的写法,如果你用的是其他USB设备,替换接口类型和通道参数即可。
4.2 一次完整报文序列逐步看
先看完整逻辑链路。第一步,确认ECU在默认会话并可通信,我直接发切换扩展会话的请求。
请求:02 10 03 00 00 00 00 00。02是ISO-TP单帧长度,10是会话控制SID,03是扩展会话子功能。
正常情况下,ECU回:06 50 03 00 32 01 F4 ...。50是正响应SID,03表示已切换为扩展会话,00 32 01 F4表示P2和P2*时间。如果这一步都没反应,先查寻址ID、波特率、总线物理连接,别往下走。
第二步,发读VIN请求:03 22 F1 90 00 00 00 00。03表示有效诊断数据3字节,22是读取服务的SID,F190是VIN码对应的DID。
由于VIN是17字节字符,加上响应头3字节,整条响应20字节,超过单帧7字节上限,会走多帧流程:
首帧:10 14 62 F1 90 4C 53 56 ...。前面讲过,0x14是十进制的20,表示总长度。
诊断仪回流控帧:30 00 14 ...。请求ECU继续发送,帧间隔20毫秒。
连续帧:21 47 41 41 2D 31 32 33 ...。序号从1开始递增,每个连续帧最多7字节数据。
接收端逻辑:收到首帧后解析总长度,回流控帧,然后按序号接收连续帧,检查序号是否连续,最后按序拼接。如果中途发现超时、序号跳变、流控溢出,就丢弃整包并报错。
4.3 用Python模拟诊断流程
下面给一个精简可用的Python示例,包含发送请求、收帧、简单多帧重组。
import can import time class UdsTester: def __init__(self, channel, req_id, resp_id, bitrate=500000): self.req_id = req_id self.resp_id = resp_id self.bus = can.interface.Bus(channel=channel, bustype='socketcan', bitrate=bitrate) self.rx_buffer = bytearray() self.expected_length = 0 def send_uds(self, uds_bytes): # uds_bytes是UDS协议数据,比如 [0x22, 0xF1, 0x90] length = len(uds_bytes) if length <= 7: frame_data = bytes([0x00 | length]) + bytes(uds_bytes) + b'\x00' * (8 - length - 1) msg = can.Message(arbitration_id=self.req_id, data=frame_data, is_extended_id=False) self.bus.send(msg) # 超过7字节时,多帧发送逻辑可自行扩展,这里先只演示单帧请求 def recv_single_can_frame(self, timeout=0.2): msg = self.bus.recv(timeout) if msg is None: raise TimeoutError('CAN frame timeout') return msg.data def recv_uds_response(self, timeout=2.0): # 简化版:只处理单帧和首帧+连续帧场景 data = self.recv_single_can_frame(timeout) frame_type = data[0] >> 4 if frame_type == 0x00: length = data[0] & 0x0F return bytes(data[1:1 + length]) elif frame_type == 0x01: self.expected_length = ((data[0] & 0x0F) << 8) | data[1] self.rx_buffer.extend(data[2:8]) # 回流控帧 fc = bytes([0x30, 0x00, 0x14]) self.bus.send(can.Message(arbitration_id=self.req_id, data=fc + b'\x00' * (8 - len(fc)), is_extended_id=False)) while len(self.rx_buffer) < self.expected_length: cf = self.recv_single_can_frame(timeout) if (cf[0] >> 4) != 0x02: raise RuntimeError('expected continuous frame') self.rx_buffer.extend(cf[1:8]) result = bytes(self.rx_buffer[:self.expected_length]) self.rx_buffer.clear() return result elif frame_type == 0x02: raise RuntimeError('unexpected continuous frame') else: raise RuntimeError('unexpected frame type') tester = UdsTester('can0', req_id=0x7E0, resp_id=0x7E8) tester.send_uds([0x10, 0x03]) resp = tester.recv_uds_response() print('session switch resp:', resp.hex()) tester.send_uds([0x22, 0xF1, 0x90]) resp = tester.recv_uds_response() print('read VIN resp:', resp.hex()) if resp[0] == 0x62 and resp[1:3] == b'\xF1\x90': vin = resp[3:].decode('ascii', errors='ignore') print('VIN:', vin)这个示例能跑通“切会话+读VIN”的完整流程。真实的刷写工具还要实现多帧发送、重传机制、0x78等待逻辑这些,但核心框架就是这样的两层循环:外层处理服务,内层处理CAN帧。
运行代码时我习惯把每一帧原始报文都打印出来,包括发送和接收,方便对照总线上真实抓包数据。实际开发中,很多问题往往不是协议逻辑错,而是底层CAN参数没配对,比如通道名、终端电阻、波特率。
5. 常见问题与排查速查
5.1 负响应码速查
调试UDS时,负响应码是最高效的线索。我整理了一份常用NRC对照,建议收藏:
| NRC | 含义 | 常见触发场景 |
|---|---|---|
| 0x10 | 一般拒绝 | 请求条件不对,但不适合用更具体码来表达 |
| 0x11 | 服务不支持 | ECU根本没有该SID,比如老版本固件未实现 |
| 0x12 | 子功能不支持 | SID存在,但子功能值不对,或当前会话不允许 |
| 0x13 | 报文长度或格式错误 | 长度和SID要求的格式不匹配,或填充字节不对 |
| 0x14 | 请求数据超出范围 | 参数值超出允许范围,比如DID不存在 |
| 0x22 | 条件不满足 | 最常见,整车条件、会话状态、使能条件不满足 |
| 0x31 | 请求超出范围 | 例程ID错误,或者例程参数非法 |
| 0x33 | 安全访问被拒绝 | 还没解锁就想执行高权限操作 |
| 0x35 | 无效密钥 | 安全访问第二步的key算错了 |
| 0x36 | 尝试次数超限 | 连续输错key次数过多,进入锁定 |
| 0x37 | 所需时间延迟未到 | 锁定时间没结束,ECU拒绝新的seed请求 |
| 0x78 | 响应待定 | 请求已收到,ECU正在处理,请继续等待 |
看到0x22不要慌,先自查三件事:会话状态对不对、整车条件是否满足、是否还没解锁。看到0x12优先查子功能在当前会话是否被允许。看到0x31查例程ID和参数表。
5.2 收不到响应和超时问题怎么查
请求发出去后完全没响应,是最让人头疼的。我的排查顺序固定是这样:
第一,物理层。CAN_H和CAN_L电压对不对,终端电阻是不是120欧姆左右,波特率是否一致。很多“偶发无响应”最后都是接触不良。
第二,寻址ID。确认你发的请求ID是不是ECU配置的物理请求ID,响应ID是不是ECU返回的那个。有的ECU请求和响应ID不是固定的0x7E0/0x7E8,而是自定义地址,查文档要仔细。
第三,ECU状态。ECU是否进入低功耗模式或总线休眠,是否处于bootloader引导模式。引导模式下支持的UDS服务集合和正常应用模式完全不同。
第四,报文是否真的上了总线。用CAN分析工具抓一下,看请求帧有没有发出去。我遇到过适配器驱动异常,代码里send成功了但总线上一帧都没有的情况。
关于0x78的处理经验:收到0x78后,诊断仪的等待超时要切换成P2*时间。有些ECU跑一个长例程可能要好几十秒,如果你的上位机用固定2秒超时,那就会误报“无响应”。正确做法是收到0x78后进入轮询等待,直到ECU回出最终正响应或负响应。
5.3 多帧重组踩坑实录
多帧传输是入门后期最容易出问题的地方,分享几个我实际踩过的坑。
首帧声明的长度与实际数据不匹配。ISO-TP允许首帧声明一个总长度,然后连续帧数据可能不够或者超出。接收端必须按声明长度截取,多余部分丢弃,不能盲信缓冲区里的全部数据。我在某个ECU上就遇到它声明长度是30字节,实际VIN只有17字节,后面全是垃圾填充,解析时差点把多余字节也当VIN内容写进数据库。
STmin设置不当导致丢帧。流控帧里STmin设得太小,ECU发送速度飞快,接收端CAN控制器FIFO直接溢出,表现为连续帧序号跳变。这种情况下先把STmin调大到20毫秒以上测试,确认没问题再逐步调优。
BS块大小协商问题。有的ECU要求发送完一个块(比如BS=8)后必须等诊断仪再回一帧流控才能继续发下一块,如果你的诊断仪不遵守块大小约定,ECU就会停住不发了。做刷写工具时,一定要根据ECU的声明动态处理BS,不能写死。
连续帧序号递增和回绕。CF序号从1开始,到0xF后回0。很多新手没处理回绕逻辑,文件大于一定尺寸后重组就错乱。写接收逻辑时要用模16递增校验,而不是简单加1比较。
还有一个很多人忽略的点:多帧重组过程中,其他ECU的物理响应帧也在总线上,接收循环里必须按响应ID过滤,只收目标ECU的帧,否则会把广播帧、其他ECU的响应帧混进来。
最后分享一点个人体会
做UDS协议开发这些年,我最大的感受是:协议框架本身不难,难的是和具体ECU的脾性打交道。每家的会话迁移条件、安全种子算法、例程前置条件、时间参数配置都不一样,规范文档写得再详尽,也架不住实车上抓一次真实报文来得直接。入门阶段不要贪多,把0x10、0x22、0x27、0x31、0x19这几个服务跑通,把单帧多帧、流控重组这些底层机制搞清楚,剩下的大部分工作其实就是查表对照和积累实战经验。
我自己到现在还保留着一个小习惯,无论调什么ECU,第一件事永远是抓一遍“默认会话下读VIN + 读DTC”的原始报文存起来。这套基线报文既能验证总线物理连接没问题,又能确认寻址ID、波特率是否匹配,还能顺带看清分辨率。是你排查问题时的第一把尺子。建议你也在最开始就建立这个习惯。