写在前面:UDS实践性强,逻辑复杂,很多服务非要体验过一次才能理解,导致包括我在内的初学者感觉晦涩难懂,不明觉厉,因此将自己的理解写下来、整理下来,与君共勉。
零、UDS诊断命令备忘录
常见的UDS报文
常见的20个NRC
一、简介
UDS(Unified Diagnostic Services,统一的诊断服务)诊断协议是在汽车电子ECU环境下的一种诊断通信协议,在ISO 14229中规定。它是从ISO 14230-3(KWP2000)和ISO 15765-3协议衍生出来的。“统一”这个词意味着它是一个“国际化的”而非”公司特定的”标准。到目前为止,这种通信协议被用在几乎所有由OEM一级供应商所制造的新ECU上面。这些ECU控制车辆的各种功能,包括电控燃油喷射系统(EFI),发动机控制系统,变速箱,防抱死制动系统(ABS),门锁,制动器等。
诊断工具与车内的所有控制单元均有连接,且这些控制单元均启用了UDS服务。不同于仅使用OSI模型第一层、第二层的CAN协议,UDS服务使用OSI模型的第五层和第七层(会话层和应用层)。服务ID(SID)和与服务相关的参数包含在CAN数据帧的8个数据字节中,这些数据帧是从诊断工具发出的。
目前市面上的新车都具有用于车外诊断的诊断接口,这使得我们可以用电脑或诊断工具(业内称为测试器Tester)连接到车辆的总线系统上。因此,UDS中定义的消息可以发送到支持UDS服务的控制器(业内称ECU)。这样我们就可以访问各个控制单元的故障存储器或用新的固件更新ECU的程序。除此之外,UDS还用于下线检测时把一些信息(如VIN码)写入到汽车的各个零部件中。这些功能也是UDS最为核心的功能。
使用电脑进行车辆诊断,诊断线插在OBD接口上
为什么我们要设计UDS这样的诊断协议呢?在汽车诊断协议诞生之前,修车只能靠师傅的经验,因为汽车零部件不会告诉你它哪里出了问题。但有了诊断协议之后,一旦零部件出了问题或者出过问题,它们会把故障信息保存在内存里面,维修师傅就可以通过通信总线读取这些故障信息,比如一个ECU经历欠压故障之后,它会将欠压故障代表的DTC(诊断故障码)存储起来,可选择性保存的还有发生故障时的快照信息(比如此时的车速、读到的电压值等)。快照信息有助于测试工程师和售后技师查找发生故障的原因。
除了CAN总线以外,UDS也可在不同的汽车总线(例如 LIN, Flexray, Internet 和K-line)上实现。
如下图所示,ISO 14229也就是UDS协议仅对应用层、会话层做出了定义。这里有个疑问,UDS专指ISO 14229-1吗?这种说法是不对的,UDS包含了ISO 14229下属的7个子协议,其中ISO 14229-2还是会话层的,所以UDS仅包括应用层的说法也是错误的。
说明下,我们本篇文章我们仅使用CAN来描述UDS。对于CAN来说,物理层和数据链路层遵循ISO 11898协议;网络层方面,Classical CAN仅有8个字节的数据场与应用层处理多帧数据的需求构成了矛盾,ISO 15765-2协议解决了该问题,我们用CAN的8字节数据场会腾出一到两个字节的做法,来体现网络层的控制信息。
如果希望深入学习下UDS网络层的知识,请移步:
UDS网络层/TP层(ISO 15765-2)的解读
排放相关的诊断内容,即ISO 15031-5主要针对OBD协议,为法规强制要求燃油车满足的协议,电动车是无需满足的。燃油车通常既满足UDS协议,又满足OBD协议,这两个协议不冲突。小伙伴们有没有发现UDS协议的服务ID(SID)最小的是0x10,那是因为小于0x10的服务是OBD协议中规定的。
学习UDS之前,希望您对CAN的基础知识有初步的了解,知道一个CAN帧的基本构成,熟悉至少一种CAN盒的使用方法。协议方面,应通过PPT、论文、原版英文协议重点学习ISO 15765-2和ISO 14229-1的协议内容,之后可以将Git上的开源UDS协议栈移植到你熟悉的嵌入式平台上,进行数据收发;或使用CAN盒与支持UDS诊断的设备进行数据收发,对UDS有一个大致的认识。切记知行合一,实践很重要。
摘自ISO 14229-1-2013
摘自ISO 14229-1-2013
摘自恒润科技公开资料
摘自恒润科技公开资料
二、UDS的服务
UDS本质上是一系列服务的集合。UDS的服务包含6大类,共26种。每种服务都有自己独立的ID,即SID。
SID:Service Identifier,诊断服务ID。UDS本质上是一种定向的通信,是一种交互协议(Request/Response),即诊断方(Tester)给ECU发送指定的请求数据(Request),这条数据中需要包含SID,且SID处于该应用层数据的第一个字节。如果是肯定的响应(Positive Response),首字节回复[SID+0x40],举例子就是请求0x10,响应0x50;请求0x22,响应0x62。如果是否定的响应(Negative Response),首字节回复0x7F,第二字节回复刚才询问的SID。比如Tester请求0x10服务,我想进入编程模式,ECU给出否定响应,首字节0x7F,第二字节回复0x10,代表我否定你的0x10服务请求,第三字节是NRC(否定响应码),代表我否定你的依据。
肯定响应和否定响应的形式一定要熟悉。
通常,在CAN总线中,Addressing information寻址信息会在CAN的帧ID中体现出来,例外是远程寻址,但不常使用。所谓的寻址信息包含了源地址(Source Address)和目标地址(Target Address),就是这条信息是由谁发给谁的,类似于收件人和发件人。当然,ECU回信给Tester时,ECU就变成源地址了。因此源地址和目标地址在UDS中并不是一成不变的。
UDS的寻址模式分两种,一种是物理寻址(点对点、一对一),根据物理地址的不同进行访问,但只能访问单个ECU节点,Tester为SA源地址,ECU作为TA目标地址;对应的,另一种是功能寻址(广播、一对多),根据功能的不同进行访问,它能访问多个ECU节点,对于标准帧来说,通常是0x7DF。
每一个ECU都有2个CAN的诊断帧ID,分别对应物理寻址的收与发。通常由主机厂来确定不同ECU的这两个特定的诊断ID。比如0x701对应接收Tester的消息,0x709对应发给Tester的消息。
UDS的26种服务
UDS的服务分为6大类,但常用的服务是加背景色的15种。这15种服务又可粗略地划分为权限控制、读取数据/信息、写入数据/信息、通信控制、功能控制这几类(注:这几类是我自己划分的)。
ISO14229-1英文原文中大篇幅的对26种服务进行了解释,英文原文链接如下。学习时如果遇到困惑可以找标准中的相关语句进行翻译,协议是所有学习材料的根本。
ISO14229-1-2013-汽车诊断协议.pdfmax.book118.com/html/2018/0725/5214111100001303.shtm
本文重点介绍以下几个服务:$10 Diagnostic Session Control(诊断会话),$14 Clear Diagnostic Information(清除诊断信息),$19 Read DTC Information,$22 Read Data By Identifier(通过ID读数据),$27 Security Access(安全访问),$2E Write Data By Identifier(通过ID写数据),$3E Tester Present(待机握手)。其他的服务楼主有时间会补齐,敬请期待。
26种服务,其中15种较为常用
少了0x38
$10诊断会话 Diagnostic Session Control
$10包含3个子功能,01 Default默认会话,02 Programming编程会话,03 Extended扩展会话,ECU上电时,进入的是默认会话(Default)。
为什么设计三个会话模式呢?因为权限问题。默认会话权限最小,可操作的服务少;扩展模式通常用于解锁高权限诊断服务,例如写入数据/参数、读写诊断码;编程模式用于解锁bootloader相关的诊断服务,即程序烧录。
题外话,讲个故事。这三个会话模式好比普通项目成员(默认会话)、项目组长(扩展会话)和会计(编程会话)的关系,小职员权限最小,小职员有的权限项目组长全有,项目组长还多了些其他的高端权限(如写数据、例程控制)。会计则不同,它有些自己独有的权限(刷写程序),但项目组的很多权限它没有(读/擦故障码),因为它只干会计相关的事,本身不参与项目。
这里来一张权限表格。带颜色的区域代表需要解锁操作。
15个服务在不同会话中的权限情况(本表仅作参考)
如果您进入了一个非默认会话的状态,一个定时器会运转,如果一段时间内没有请求,那么到时间后,诊断退回到默认会话01(最低权限)。当然,我们有一个$3E的服务,可以使诊断保持在非默认的状态。
UDS的请求命令有4种构成方式,即SID,SID+SF(Sub-function),SID+DID(Data Identifier)(读写用),SID+SF+DID。每种服务都有自己不同的构成方式,查看服务说明即可,不用死记硬背。
NRC:Negative Response Code(否定响应码)。如果ECU拒绝了一个请求,做出否定响应(Negative Response),它会在第三字节回复一个NRC。不同的NRC有不同的含义。本文开头时有一个常见NRC的图,当然完整版请参照ISO14229附录A。
这里提一下一个特殊的NRC——0x78,requestCorrectlyReceived-ResponsePending(RCRRP,请求已被正确接收-回复待定)。这个NRC表明请求消息被正确地接收,请求消息中的所有参数都是有效的,但是要执行的操作还没有完成,Server端还没有准备好接收另一个请求。一旦请求的服务已经完成,服务器应该发送一个积极的响应或消极的响应,响应代码应与此不同。这个NRC的消极响应可以被Server端重复,直到被请求的服务完成并且最终的响应消息被发送。
当使用此NRC时,服务器应始终发送最终响应(不管正响应还是负响应),与suppress-PosRspMsgIndicationBit值(抑制肯定响应位)或NRCs(SNS、SFNS、SNSIAS、SFNSIAS和ROOR)对功能寻址的处理请求的响应抑制要求无关。这里的有一个特殊的知识点,即什么情况下,功能寻址是不需要给出否定响应的呢?如果被测ECU需要回复上面5个NRC(SNS 0x11、SFNS 0x12、SNSIAS 0x7E、SFNSIAS 0x7F和ROOR 0x31)时,不需要回复否定响应。
NRC 0x78坏了规矩的例子
14229-1协议第329页
例子:以CAN总线网络举例。CAN帧一共8个字节,第一字节被网络层占用。(ISO 15765-2的知识)
进入01会话成功,进入02会话失败,进入03会话成功
请求(Request):02 10 02 xx xx xx xx xx ; 02是网络层单帧SF,表示应用层包含有2个字节,10是服务ID(SID),02是子功能——进入编程会话。但ECU婉拒了它的请求。
摘自ISO 14229-1:2013 p39
我们看下上面的图表,这个是ISO 14229定义的0x10服务应具有的请求报文格式,M意为Mandatory 强制。可以看到0x10服务仅有两个字节,整条报文是“服务ID+子功能”,比较简单。
肯定响应:02 50 02 xx xx xx xx xx;02即应用层含两个字节,50=10+40表示对SID的肯定回复,02是子功能。
否定响应:03 7F 10 7E xx xx xx xx;03同上,7F表示否定响应,10是SID,7E是NRC(否定响应码)。
$3E待机握手
$3E服务用于向服务器指示诊断仪仍然连接在网络上,之前已经激活的诊断服务功能可以仍然保持激活状态。
例子:02 3E 80 00 00 00 00 00,发送一个3E服务的报文,保持非默认会话状态。80表示无需回复。
$27安全访问
$27安全访问:ECU当中有很多数据是整车厂独有的,并不希望开放给所有客户,它需要做一个保密的设定。我们在读取一些特殊数据的时候,要先进行一个安全解锁。ECU上电之后是一个锁定的状态(Locked),我们通过$27服务,加上一个子服务,再加上一个钥匙,这样的服务请求可以进行解锁。比如下面的例子,2n-1是一个子服务,这里我们先用n=1,即01子服务来举例子。通过首轮Tester种子的请求(27+01),ECU会返回67+01+AA+BB+CC+DD,AA~DD就是种子了。之后第二轮,诊断端的Tester会利用种子进行运算(根据整车厂的算法),生成k1(不一定是1个字节),之后发送请求,子服务是2n,这里我们还是假定n=1,即02子服务。这样Tester发出的就是27+02+[k1]。之后,ECU同样也会根据第一轮的种子自行算出k2。当ECU检查出k1和k2完全一致时,解锁(Unlocked)成功。
$27安全访问服务的否定响应服务ID也是7F。还记得刚才否定响应的格式吗?7F+27+NRC(否定响应码)。
实际通信的截图,黄色位置是密钥区域
例子:
Tester:02 27 05 00 00 00 00 00 安全访问,05子功能
ECU:06 67 05 08 27 11 F0 00 肯定响应,回复了对应安全级别的种子
Tester:06 27 06 FF FF FF FF 00 发送密钥,4个FF。注意06是与05成对使用的。
ECU:03 7F 27 78 00 00 00 00 若为否定响应,7F+27+NRC
ECU:02 67 06 00 00 00 00 00 若为肯定响应,通过安全校验
细说下安全验证算法。安全验证算法包括1个核心,3个主体。
第一个主体通常和ECU有关。比如我们先用22服务读取ECU的SN,取其中4个字节,作为“调味料”参与,显然这个“调味料”对于这个ECU来说是不变的,也能通过22服务方便的读取到。
第二个主体seed,通常与ECU的运行时间有关系,是主料,在27服务发送奇数子功能时回复。seed通常一直在发生变化,无法发现其规律。
第三个主体是执行次数,就是算法要执行几轮。执行1轮和2轮得到的结果肯定是不一样的对吧。
最大的核心就是算法了。举个简单的算法,比如seed和ECU SN前4个字节加一下,循环左移两位,执行3轮,return这个数作为key,结束。安全验证就是一把锁,算法越复杂,短时间解开的成本越高,越不易被破解掉。如果失败次数过多还会触发惩罚机制,一段时间内都无法再尝试解锁,防止人为的破解。
$22读数据
$22读数据,Request(请求):22+DID(Data Identifier,通常是两个字节)
Response(响应):62+DID+Data
DID有一部分已经被ISO 14229-1规定了。比如0xF186就是当前诊断会话数据标识符,0xF187就是车厂备件号数据标识符,0xF188就是车厂ECU软件号码数据ID,0xF189就是车厂ECU软件版本号数据标识符。
14229-1协议第339页
$2E写数据
$2E写数据,Request(请求):2E+DID+Data
Response(响应):6E+DID
写入一个VIN码
正确的顺序是10开头的帧请求、30开头的帧回复、21开头再请求、22开头继续请求、03开头回复确认。我们一帧一帧来看。
10 14根据ISO15765-2代表这是一组多帧中的首帧(属于传输层的信息),一会要发0x14=20个字节的有效数据。之后是2E+F190(代表这是VIN码)+VIN码的前3个字节。意思是作为外部工具,想写入一个VIN码数据。这件事情正常是发生在车辆下线时。
30 00 0A是TP层(传输层)的信息,表示这是一个流控帧,ECU发出的,表示可以一直连续发,但连续帧最短的间隔时间要求是10ms。
21是TP层的信息,表示这是一个连续帧,序号为1。后面是VIN码的第4字节到第10字节。
22是TP层的信息,表示这是一个连续帧,序号为2。后面是VIN码的第11字节到第17字节。
03是TP层的信息,这里说的这个TP层的信息是传不到应用层的,即这是一个用完就会抛弃的信息。03的0表示这是一个单帧,3表示后面有3个有效字节。6E表示我们确认执行了2E服务的请求,这个请求写入的ID是F1 90,即VIN码。
注意,比如0xF190等DID不支持直接写入数据,需要用$10来进行会话转换。也就是说,对于写数据的请求,一般来说需要在一个扩展会话,和安全等级1的状态下才能进行。
$19 读DTC
19服务是一套诊断服务中的重中之重。协议中篇幅长达63页,通信举例达到了18个。可以说没有19服务,就没有完整的UDS。
DTC(diagnostic trouble code):如果系统检测到了一个错误,它将存储为DTC。DTC可表现为:一个显而易见的故障;通讯信号的丢失(不会使故障灯亮起);排放相关的故障;安全相关的错误等。DTC可以揭示错误的位置和错误类型。通常DTC占用3个字节,OBD II占用两个字节。图中FTB为Fault Type Byte。
故障码包括四个大类,分别是PCBU,P是powertrain动力系统,C是Chassis底盘,B是Body车身,U是network通信系统。一个DTC信息占用4个字节。最后一个字节是DTC的状态。DTMMiddleByte和DTCLowByte两个字节是我们熟知的类似P0047(ISO15031中的故障码)中“0047”的纯数字故障码。第一个字节在乘用车中,前两个bit代表P/C/B/U(动力/底盘/车身/网络)中的一个,之后六个bit是数字,合在一起的样子形如“C01”。第一个字节的前2个bit中,用00/01/10/11分别表示P/C/B/U。(感谢aymjwwl007)
举个例子,U312345这个故障码(我杜撰的),它的状态是Test failed叠加Confirmed,那么DTC信息这四个字节就应该是0xF1(二进制11110001),0x23,0x45,0x09。
这是ISO15031中的故障码,和UDS中的故障码在长度上有一定的不同
$19拥有28个子服务(Sub-Function)。常用的子服务有:
01 (读取符合掩码条件的DTC数量)(必须支持),后面的参数是DTC状态掩码,若为01表示我想读当前故障,若为08表示我想读历史故障,若为09表示当前故障和历史故障都想读。关注公众号【车端】
在肯定回复时,组合应该是59(19+40) - 01 (子功能) - 09 (本ECU所支持的掩码条件)-01 DTC的格式(ISO14229-1为01) - 00 01 (目前满足条件的DTC有一个)
02(读取符合掩码条件的DTC列表及其状态)(必须支持),后面的参数是DTC状态掩码,解读同上。
在肯定回复是,59 - 02(子功能)- 09(本ECU所支持的掩码条件) - XX XX XX ( DTC,车厂定义 ) - 01 (这个故障码怎么了,01表示当前故障)
04(读取快照信息),也叫冻结帧。
06(读取扩展信息)。
0A(读取ECU支持的所有DTC列表及其状态)(必须支持)。这个就不必发DTC状态掩码了。所有支持的DTC列表及其状态都会打印出来。
黄色框是DTC,紧跟着的是DTC状态
同时读取当前/历史故障,黄色框是DTC,紧跟着的是DTC状态
刚才提到,一个DTC除了它自己的3个字节,还有一个字节专门用于表达DTC的状态,这个字节我们叫它DTC状态掩码。这个状态字节每个位的含义下面列举出来。注意,在实际项目中,并不是所有的DTC状态都是支持的。DTC状态掩码前7个位的理解是UDS的一个难点。
关于DTC状态掩码更详细的解释参见文末的学习资料22,张老师有详细的解答。
DTC状态掩码,默认值0x50
Request/Response。括号标识循环,可以读出很多DTC
$14清除DTC
清除(复位)DTC格式,它可以改变DTC的状态。DTC状态中的八个位,除bit4和bit6外均会被清零,包含当前故障(TestFailed)和历史故障(ConfirmedDTC)。bit4和bit6这两个testNotCompleted开头的会被强制置1。
3个FF代表清除所有DTC。
Request:14+FF+FF+FF;
Response:54 。
14服务
$2F IO控制
该服务可以通过DID(数据标识符)来进行输入信号的替换和控制零部件负载输出。这是一个用在产线上较多的服务。该报文的请求至少由4个字节组成。第一个字节是2F,第二第三字节是DID,其中第二字节是高位。第四字节是input Output Control Parameter(并不算一个子功能),可以看做IO控制类型。
IO控制类型分为4类,
00是控制权还给ECU,Return Control To ECU。
01是复位为默认值,Reset to Default。
02是冻结当前的状态,Freeze Current State。
03是短暂接管控制权,Short Term Adjustment。
若控制类型是00-02这三种,请求报文是4个字节。
若控制类型是03,请求报文的第五字节是控制代码,可以是数字量,比如01是开,00是关;也可以是模拟量,比如空调风门的开度。
2F服务,黄色区域为2个字节的DID
上面这个图可以理解为,关闭开关,之后打开开关,之后控制权还给ECU,之后想复位回默认值,但是发现ECU不支持。这里NRC用0x22是否准确,还望大神告知。
2F服务有一个问题,如果通过2F服务修改了某个值,后续也不把控制权还给ECU,那么这个修改的有效时间是一直持续下去?
正确的做法是如果扩展会话超时,即切回默认会话,此时控制权应还给ECU,毕竟 2F的03子功能是"暂时接管控制权"。
6种模式的配置
非默认会话在实际中又细分为编程会话(Programming Diagnostic Session)和扩展会话(Extended)。在UDS的实际应用中,我们需要对26种服务针对不同会话、不同寻址模式的支持度进行配置。
也就是说,物理寻址+默认会话、物理寻址+编程会话、物理寻址+扩展会话、功能寻址+默认会话、功能寻址+编程会话、功能寻址+扩展会话,共6个模式。那么我们可以脑补一个26行、6列的表格了。
举个例子,对于10、11、3E、22(22有分歧)服务,它们需要支持所有的6个模式(物理+功能寻址)。
对于14、19服务,DTC相关,要求支持默认+扩展会话的4个模式(物理+功能寻址)。
对于27服务,即安全访问服务,仅支持扩展+物理、编程+物理2个模式。
对于2E、2F服务,仅支持扩展+物理1个模式,且要求安全等级为1。
对于34、36、37服务,涉及程序下载,仅支持编程+物理1个模式,且要求安全等级为2。
对于28、85服务,有些要求支持编程+扩展会话的4个模式,有些则要求仅支持扩展会话的2个模式。
对于31服务,要求安全等级为1,有些要求支持扩展+物理、编程+物理2个模式,有些则要求仅支持扩展+物理1种模式。
抑制肯定响应指示位的配置
抑制肯定响应指示位(Suppress Positive Response Message Indication Bit)顾名思义,这个位是用来抑制肯定响应的。即本应回复肯定响应帧,但是发出方要求对方静默,不需要对方回复肯定响应。这个位的位置和子服务在同一个字节(应用层数据第二字节),为bit7,高位。
比如SID是0x10,子服务是0x01,如果是抑制肯定响应的话,子服务这个字节要改成0x81。这样发下去ECU就不会回复肯定响应了。
通常,10、28、3E、85服务是需要支持抑制肯定响应这个功能的。11服务部分厂家也是要求支持的。
以上只是一些粗浅的理解。对26种服务更详细的解读请拉到屏幕下方参考张老师的8篇文章。张老师也开通了微信公众号,"汽车ECU网络诊断技术"。可以去关注一下。
UDS应用的设备
在UDS诊断产品中知名度最高,应用最广泛的是德国Vector公司的CAN卡 VN1630/1640 配合其CANoe 软件,Vector 产品功能齐全,适合系统级汽车总线开发,被大部分汽车厂商采用。通常工程师先用Vector的CANdela进行cdd文件的开发,之后将该cdd文件导入CANoe.diva中进行功能测试。下面的链接是Vector提供的全套解决方案,里面的CANdesc是UDS代码生成软件。
ECU诊断开发解决方案 - 图文 - 百度文库wenku.baidu.com/view/7754e9a3770bf78a652954cc.html
Vector 产品很好用,节省开发时间,但价格昂贵,不适用于小厂或规模性采购。目前市面上有很多CAN 厂商(如Kvaser, ZLG 等)能提供低成本、体积小、驱动简单、开放API 的设备,很适合进行UDS相关的二次开发。
讨论
问题:如果出现了UDS请求并发的情况,比如2E服务,正在写入、正在处理时,突然有11服务请求闯入,此时ECU会如何处理呢?
观点:此时应继续执行完2E的写入服务,11服务的请求将会被丢弃,不会有任何回复。
四、AUTOSAR中的UDS
AUTOSAR使用DCM、DEM、FIM、DET模块来共同实现诊断。我们主要讨论前两个模块。
DCM,全称Diagnostic Communication Manager,为诊断服务提供通用接口。DCM帮我们处理总线上的诊断请求,在其提供的callback函数中,我们可以添加诸如DID数据、【关注公众号【车端】】27运算结果等信息。
DEM,全称Diagnostic Event Manager,为DCM提供错误信息。应用层可以调用DEM提供的接口以决定某个DTC被Test为Pass或Fail。
对于DCM来说,这个模块还是太大了,我们又将其分为3个独立的子模块。
DSL,全称Diagnostic Session Layer,用于确保诊断请求和响应的数据流,监控诊断请求和响应的时序,管理诊断会话和安全等级。这一层是DCM的最外圈,叫他外环吧,它直接与PduR相接触,获取CanTp经由PduR上传的数据。
DSD,全称Diagnostic Service Dispatcher,用于处理诊断数据流。它位于DCM的中环,夹在DSL和DSP的中间。
DSP,全称Diagnostic Service Processing,实现每个具体的诊断服务,位于DCM的内环,这一层事无巨细。比如22服务、27服务的callback正是源于这一层。
审核编辑:郭婷
全部0条评论
快来发表一下你的评论吧 !