UDS协议解析与移植与上位机
UDS
- 主要功能:读取故障,数据传输,上传下载,复位等
借鉴参考了shnsxz大佬的博客
借鉴参考了fish_study_csdn大佬的博客
借鉴参考了小猫爪大佬的博客
借鉴参考了心机之花大佬的专栏
CANFD提供的参考手册,强烈建议
诊断请求15765
第一字节
为了解决数据过长,即分包的问题,15765-2总共定义了4种类型的帧结构
表示四种帧的类型主要靠byte1的高四位,单帧为0,首帧为1,连续为2,流控为3
- 在单帧的情况下(即),SF_DL代表后面有几个字节数据,如果有没有使用的字节,通常要用0x55或0xAA来填充。
- 分包情况的工作流程,如下图所示
- FF_DL代表后面有几个字节数据,连续帧的SN是用于计数
首先,发送端通过FF(FirstFrame)启动通信,其中前两个字节的高位4bit标识为0001代表FF,低4bit与第二个字节共同表示总数据长度,这个数据长度也包含回复FF的数据,不包含连续帧的第一字节。接着,接收端回应FC(FlowControl),首字节高4bit为0011,低4bit的FlowStatus和第二个字节的BlockSize、第三个字节的SeperateTime共同指示接收速率。FlowStatus存在三种状态:CTS(0)、WT(1)、OVFLW(2)。
- FlowStatus的状态:FlowStatus为0时,允许发送ConsecutiveFrame;为1时要求暂停发送,恢复时再发FlowStatus=0通知;若因资源限制无法接收数据,则发送FlowStatus=2的FlowControl。
Stmin的状态:由控制图可知,Stmin的大小控制的是上一帧CF结束到开始下一帧CF中间的最小时间间隔
BS的状态:BS的大小是CF帧在没有流控帧的情况下能发多少帧,例如BS为5,那么CF能够发五帧,然后就要看新的流控帧如何调配。
CF就是承载FF无法完全承载的剩余数据了,它第一个字节的高4位为0010,低4bit用于标识CF的序列号,序列号从1开始,每发送一次CF增加1。
- 当总数据达到FF中的数据长度后停止发送
分段传输的诊断服务举例:
这是一个读取DTC的命令和应答。
03 19 02 08 55 55 55 55 (诊断仪发送的SingleFrame的request)
10 33 59 02 19 01 00 07 (ECU以FirstFrame开始传输的response)
30 00 00 55 55 55 55 55 (诊断仪发送的FC)
21 09 03 05 02 09 05 04 (ECU发送的CF)
22 07 09 05 06 06 09 05 (ECU发送的CF)
23 08 03 08 07 01 05 08 (ECU发送的CF)
24 07 01 06 08 07 01 0C (ECU发送的CF)
25 08 07 01 0D 08 07 03 (ECU发送的CF)
26 07 09 08 01 01 09 09 (ECU发送的CF)
27 01 07 09 AA AA AA AA (ECU发送的CF,此时传输结束)
BS和STmin等于0时,表示接收端可以以最快的速度来接收数据,发送端可以一次发送的CF数量不受限制。
其余字节14229
UDS报文
有子功能
1byte:Service ID(SID),服务标识符,相当于CCP的CMD,代表了这条诊断命令执行的什么功能。
1byte:Sub-function,当前服务标识符具体的操作,代表当前诊断服务的具体操作
- 其中Sub-function的8个bit,最高位的1bit用于抑制正响应。当最高位为1时,不会给出正响应;为0,给出正响应。
xbyte:Parameter,当前功能下的发送参数
例如:31 01 08 09为开启软件刷写检查,31 02 08 09为关闭
- 31为服务标识符,01为子功能ID,08 09为具体参数
没有子功能
1byte:Service ID,服务标识符
xbyte:Parameter,当前功能下的发送参数
通用介绍
- 正响应的意思是执行成功后,服务端返回报文报告执行成功
- 负响应的意思是执行失败后,服务端返回报文报告执行失败
负响应返回的报文:最高字节固定为7F,第二字节为被拒绝的SID,后续字节为被拒绝的原因
发送报文:27 05
回复:7F 27 13
正响应返回的报文:最高字节为SID基础上加上0x40,剩余为发送数据,与后续返回的数据,如果没有子功能
发送报文:27 05
回复:67 05 xx xx xx
诊断会话包含三个子功能:01 Default默认会话,02 Programming编程会话,03 Extended扩展会话,
ECU上电后,一般处于默认状态
编程会话:可以进行软件刷写等一系列操作
扩展会话:大部分诊断数据读写
如图为UDS诊断协议图片,进入01会话成功,进入02会话失败,进入03会话成功
物理寻址和功能寻址
- 物理寻址指的是请求端与单个响应端之间的通信,而功能寻址是请求段与多个响应端之间的通信。即请求端通过物理寻址方式发送请求时,只能有一个ECU可以回复响应;如果通过功能寻址方式发送请求时,同一网络中支持该功能寻址的所有ECU都需要回复响应。
- ECU设备有自己的物理地址和反馈地址。该地址会在项目初期就确定好。诊断仪向ECU A物理寻址,则ECU A在自己的反馈地址发送数据给诊断。诊断仪功能寻址,则ECU A\B\C都会在各自的反馈地址发送数据给诊断。
常见的SID诊断服务
10服务:诊断会话,10 01默认会话;10 02编程会话;10 03扩展会话;
11服务:该服务请求ECU根据复位的内容有效地执行ECU复位,11 01硬件复位;11 03软件复位
14服务:使用此服务来清楚ECU内存中的故障内存的诊断行行行;常见的请求命令为14FFFFFF
19服务:此服务读取ECU驻留诊断故障代码(DTC)信息的状态;常见用法是19 02 09读取当前和历史故障
22服务:该服务允许通过DID向ECU请求读取数据值;读取功能配置22 F1 01;网络配置22 F1 10等
27服务:该服务在向ECU请求写入数据时需要进行的解锁服务;请求种子2701;发送钥匙2702
28服务:用于“打开/关闭”ECU的某些消息的传输和/或接收,以及消息的通讯类型,常见的有28 01 只收不发
2E服务:该服务 允许通过DID在解锁条件下向ECU请求写入数据值;常见的为写入车辆的网络配2E F1 10 xx
2F服务:使用此服务来替换输入信号、内部ECU功能和\或控制由服务器的数据接口引用的电子系统的输出(执行器)
31服务:客户端使用此服务来启动/停止例程,并在服务器的内存中请求例程结果。通常刷写过程中会用到。
参考小趴菜_自动驾驶搬砖人大佬
- 其中22和2E读取和写入参数,是依据DID,DID的功能值需要公司内部进行定义,例如77 62,我定义的是读取硬件版本
- 22服务的例子:
// 0x23为诊断仪,0x24为下位机// 03代表单帧,有效值为三个字节// 22代表读取DID参数// 21 62为DID值,在我的工程里命令为为读取硬件版本0x230322216200000000// 10中的1代表为连续帧的首帧,013代表总数据长度,即19个字节// 62 21 62为回复DID命令,剩余字节为数据帧// 数据对应ASCII码,35 32 33数据对应'5' '2' '3'0x241013622162353233// 流控帧,告诉下位机用什么速率发剩余报文// 30中的3代表流控帧,0代表FS中的继续发送状态// 第一个00代表BS,在没有流控帧的情况下能发多少帧,第二个00代表stmin,两帧发送的时间间隔// 即告诉下位机全速发送0x233000000000000000// 21中的2代表连续帧,1代表是第一个连续帧,后面的是数据0x242133333333323835// 21中的2代表连续帧,2代表是第二个连续帧,后面的是数据// 6+7+6 = 19,所以的00不是数据0x242230302e31303000- 2E服务的例子:
// 02表示单帧,有效两个字节。10 03表示切换到扩展诊断0x230210030000000000// 06表示单帧,有效字节为6个。50 03是固定的// 前两个字节代表P2Server_max:ECU在接收到请求消息后,需要在50ms时间内发出响应消息的性能要求。后两个字节代表P2EServer_max:当ECU发送否定响应码为0x78后,到ECU再次发出响应消息最长5s时间。0x240650030032138800// 02同理,27 01代表请求获取密钥种子0x230227010000000000// 06 67 01同理,剩余的四个字节是种子值0x2406670166753a2578// 06同理,27 02代表发送密钥值,剩余的四个字节密钥值// 密钥算法两方确定好,上位机接收下位机种子,进行运算后发送给下位机,CCP协议和这个差不多// 下位机与自己运算获得的密钥对比进行解锁0x230627028384858600// key正确,发送正响应,负响应的话35代表key错误0x240267020000000000// 10代表连续首帧,14代表有20字节,2e写入DID指定数据// 20 90代表软件版本0x2310142e2090353231// 流控帧,全速发送0x243000000000000000// 连续帧0x2321343536353435330x232236343835313233// 传输完成,给出正响应0x24036e209000000000DTC相关知识
19服务,读取故障信息,常用的子功能有01,02。如19 01 01为读取状态位为1的故障码数量,19 02 01为读取状态位为1的故障码
参考博客:安娜_beier大佬的DTC相关内容梳理
DTC编码的解析
- 例如:D14C51,D1为High Byte、4C为Middle Byte、51为DTC Low Byte,D1的意思为车辆集成厂商自定义燃油测量,4C表示具体对象和类型,51表示需要编程。
- 和J1939类似,属于解释方法,可以完全不管PGN源地址等含义,直接当成扩展ID用也可以。
状态位:即19 02 01中的01,常用的有01代表当前故障,08代表历史故障,上位机相当于下发掩码
VCU需要根据博客中的置位条件,对DTC编码的状态进行置位,所以一般是建立一个结构体放编码及状态等信息,再根据上位机的掩码回传DTC编码及其他状态
//DTC code ,故障检测函数指针,故障检测周期,故障有效次数,存储地址,故障等级InitAddDTC(0x056001,DTC_MonitorFun_uFuelLevel_Alarm,10,1,ADDR_001,LEVEL_C);后续根据周期遍历所有,通过故障检测函数的返回值决定状态位的状态typedefunion{struct{uint8_tTestFailed:1;uint8_tTestFailedThisMonitoringCycle:1;uint8_tPendingDTC:1;uint8_tConfirmedDTC:1;uint8_tTestNotCompleteSinceLastClear:1;uint8_tTestFailedSinceLastClear:1;uint8_tTestNotCompleteThisMonitoringCycle:1;uint8_tWarningIndicatorRequested:1;}DTCbit;uint8_tDTCStatusByte;}DTCStatusType;下发190101报文回复,不考虑计数09代表下位机可用掩码,即01和08,当前故障和历史故障0001代表符合的只有一个DTC59010900000100下发190201590209往后就是所有符合01掩码的DTC的编码与状态 例如DTC编码056001他的故障为历史故障即0805600108.....// 19 02 01读取当前故障0x23Rx d80319020100000000// 1连续帧 ,00B代表有11个有效字节,59 02 09 代表正响应59,子服务02 下位机可用掩码09 即只支持当前和历史故障// 04 10 0E 09,04 10 0E代表当前故障的DTC,可以用博客的方法进行分析,我将其定义为1号传感器开路// 09从下一帧提上来的,代表该故障的DTC状态,09代表该故障是当前故障也是历史故障0x24Rx d8100B59020904100E// 全速发送,将所有DTC和其状态一起发完0x23Rx d83000000000000000// 04 10 0F 09 同理 04100F为DTC码,,我将其定义为2号传感器开路,09代表该故障是当前和历史故障0x24Rx d8210904100F090000// 19 02 08读取历史故障0x23Rx d80319020800000000// 10 0F 59 02 09同理,有效字节与正响应回复// 04 10 02 08 同理 041002为DTC码,08代表该故障是历史故障0x24Rx d8100F590209041002// 全速发送0x23Rx d83000000000000000// 04 10 0E 09 同理 DTC码为当前和历史故障0x24Rx d8210804100E090410// 04 10 0F 09 同理 DTC码为当前和历史故障0x24Rx d8220F090000000000// 清除所有故障0x23Rx d80414FF FF FF000000// 正响应0x24Rx d8015400000000000014服务,清除故障信息,参考博客:跟我学UDS
- 常用的有14 FF FF FF,即清除所有故障信息
代码刷写部分的例子
借鉴参考了车小猿大佬的UDS(十)应用层 34/36/37博客
- 删除具体报文,在此仅体现刷写流程
| 10 会话控制 | 03 | |
|---|---|---|
| 85 诊断故障码 | 02 | |
| 28 通信控制 | 03 | |
| 10 会话控制 | 02 | |
| 27 安全访问 | 先 01 | 后 02 |
| 31 例程控制 | 01 | |
| 34 请求下载 | 36 数据传输 | 37 退出传输 |
| 31 例程控制 | 01 | |
| 31 例程控制 | 01 | |
| 11 重启 | 01 |
UDS底层移植
我基于稀风大佬的代码,进行的移植
别的移植代码,这个内容更详细UDSDemo
注释完备,结构清晰,移植较为简单,没有遇到什么坑,使用暂未遇到问题,推荐使用
刷写的话,可以在切换到编程会话时进行软复位进入BOOT
- 或者在APP将接受到的数据存储到区域2,复位时BOOT对其校验并将其写入到APP区
使用Ringbuf时,有几个宏定义报错,在keil里增加支持gnu即可
可能算问题的只有两个:
- 10 02切换到boot时,等正相应回复完再切换,否则错误
- boot里11服务重启之前,先看看36服务的接收到的东西全写到FLASH了吗,写完校验完再重启
UDS上位机
- 上位机完成刷写、写DID等操作,使用pyqt结合各种UDS开源库较为简单
- 可以使用udsoncan,udsoncan也依赖于pythoncan和isotp,所以对各种常见设备也有较好的支持,如kvaser
- udsoncan官网,官网上各种API讲的都很详细,较为简单
- 需要注意的点有config中,“use_server_timing”: False,否则36服务会报p2超时问题
- hex文件的解析可以用intelhex库,在处理的时候分段的hex,可以进行填充FF操作,UDS分段不是不行,但感觉要动脑子。如果自己平时填充,建议:
- 填充方法1: 瑞萨的CS+中build tool->hex output options->Fill unused areas in the output ranges with the value可以选择填充FF,IDE就帮hex填充了。
- 填充方法2:使用hexview手动填充,操作简单。手动填充参考该博客
- 填充方法3:makefile中,elf转hex过程中将空白处填充FF,默认填充00。arm-none-eabi-objcopy -O ihex input.elf output.hex --gap-fill=0xff
- 刷写、读取写入DID可以封装为类,在按下按键后初始化一个线程并实例化执行,除此之外可以使能pyqtsingle,抛出执行过程中的错误,在界面显示
- 贴出上位机我用到的网址,client中是udsoncan的各种API
https://can-isotp.readthedocs.io/en/latest/
https://python-can.readthedocs.io/en/stable/
https://udsoncan.readthedocs.io/en/latest/index.html
https://udsoncan.readthedocs.io/en/latest/udsoncan/client.html
- 封装为exe后,报错 No module named 'can.interfaces.kvaser",问题是没有在你主文件引用这个库
参考网址:https://stackoverflow.com/questions/51312059/kvasers-can-library-has-been-loaded-but-program-executable-outputs-a-no-modul
刷写可能遇到的问题
一般工程都是通过ld或sct文件分成了几段,但实际每段不可能占满。
- 例如分了0x00和0x80,但实际0x00这一段只用了0x40的大小,剩下的部分是空着的
UDS刷写时34服务只会发0x00的首地址和空间的大小,在遇到空的地方就把0x80处的内容当成0x40后的内容进行写入,就导致缺了一块内容传输失败
所以就需要把0x40和0x80处的内容给填充,填充为0XFF占位。
- 方法一: 一般使用srec_cat.exe即可。经常变化大小的程序段一般在最后面,其他段的大小一般固定,填充地址可以直接写死,IDE调用bat脚本,实现段的填充。
srec_cat.exe aaa.hex-Intel-fill0xff0x400x80-o nnn.hex-Intel - 方法二:用python实现也可以,空白处的填充
deffill_gaps_with_ff(input_hex_file,output_hex_file=None):ih=IntelHex(input_hex_file)start_addr=ih.minaddr()end_addr=ih.maxaddr()segments=ih.segments()filled_ih=IntelHex()prev_end=start_addrforseg_start,seg_endinsegments:ifseg_start>prev_end:foraddrinrange(prev_end,seg_start):filled_ih[addr]=0xFFforaddrinrange(seg_start,seg_end):filled_ih[addr]=ih[addr]prev_end=seg_end fill_data_list=list(filled_ih.tobinarray())# filled_ih.write_hex_file(output_hex_file)returnfill_data_list- 方法三:上位机用bin文件,生成bin时makefile中的objcopy会自动在空白处填充,只需要在上位机中写死刷写的起始地址即可。
arm-none-eabi-objcopy -I ihex -O binary input.hex --gap-fill=0xFF output.bin
- 方法一: 一般使用srec_cat.exe即可。经常变化大小的程序段一般在最后面,其他段的大小一般固定,填充地址可以直接写死,IDE调用bat脚本,实现段的填充。
之前INCA可以分段刷写,APP和标定区单独刷,所以没遇到问题
- 如果一块刷,CCP刷写的时候,SET_MTA会设置几次呢,会不会遇到相同的问题?
又已经进行了测试,上位机也可以进行如下操作
- 获取每段的实际大小,当刷完该段后,主动发送37服务暂停刷写
- 然后重新发送34服务定位到新地址,继续刷写,往后以此类推
- 但请注意要确保你的控制器支持这样操作,有些控制器这样操作就会卡死
- 这种操作方法最简单,不需要自己再次处理,只需要控制器支持
