根据hex文件制作UDS统一诊断服务CAN多帧报文-python

Posted 晓翔仔

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了根据hex文件制作UDS统一诊断服务CAN多帧报文-python相关的知识,希望对你有一定的参考价值。

 1. 背景

        统一诊断服务 (Unified diagnostic services , UDS) 由ISO-14229系列标准定义。

        诊断通信的过程从用户角度来看非常容易理解,诊断仪发送诊断请求(request),ECU给出诊断响应(response),而UDS就是为不同的诊断功能的request和response定义了统一的内容和格式。

        在UDS的通信种,CAN报文的传输方式根据内容长短分为 单帧 和多帧传输。

        单帧的组包简单而多帧的组包比较复杂,本博客主要讨论多帧。

        CAN报文的帧分为标准帧,扩展帧,远程帧等。大多数情况下使用标准帧。

        标准帧每帧报文是8个字节。

        如果一个UDS报文在一个帧里发不完,那么就需要以多帧的格式组包,然后依次发送。

        对于0x36服务(文件传输)而言,因为文件大多比较长,基本都要以多帧的形式发送,这是一个需要多帧组包的常见场景。

        如果你对UDS的服务还不了解的话,可以查看这篇博客

统一诊断服务(UDS)_晓翔仔的博客-CSDN博客_统一诊断服务

2.  CAN报文多帧的格式特点

        0X36服务一个文件可以拆解为多个block,每个block会根据0x36服务之前发送的0x34请求的响应拆分为多个fluid(流)。

        一个完整的fluid(流)的结构可以分为三个部分,如下图:

fluid的第一行

        第1个字节的第一个1表示这是一个多帧。

        第1字节后一个1+第2个字节(红色)表示fluid的长度。

        第3个字节(橙色)表示服务类型,36是文件传输服务。

        第4个字节(绿色)表示fluid序号,从0X01开始递增(注意:0xff后面接着0x00)。

        此后都是数据内容(图中d表示数据)。很容易知道 fluid的长度(红色)等于 “数据总长度+2”

fluid的第二部分

        每行第1个字节都是序列号,从21到2f,所以fluid的第二部分最多有15行。

        每行的第2到8个字节是数据

fluid的第三部分

        每行第1个字节都是序列号,从20到2f,2f后面接着20,循环往复,可以有很长很长。

        每行的第2到8个字节是数据。

3. 代码实现

        有一个".hex"结尾的文件,已知它不用拆分block,每个fluid的长度要求是254。

        现在诊断仪需要将整个文件以多帧的形式发送到指定ECU,那么应该以什么形式的报文发送过去呢?

        我这里用python写了组包的代码。代码如下:

import math
import string
import time
import zlib


def list_to_hex_string(list_data):
    list_str = '[ '
    for x in list_data:
        list_str += '0x:02X,'.format(x)
    list_str += ' ]'
    return list_str


def printlist(list):
    print(list_to_hex_string(list))
    # print(list)
    # time.sleep(0.001)  # 为了保证打印的时序,可以每打印一条睡眠一会儿


def main():
    # 要注意  如果hex文件首行或者末行不是数据,请将非数据行删除后再填写filename
    filename = "001-APP.hex"
    fluidLengthMax = 254  # 每个fluidCount长度上限 可能是1098 254等,重要节点109 221
    if fluidLengthMax <= 4:
        print("fluidLengthMax too short!")
        return
    # 获取所有报文
    f = open(filename, 'r')
    file_data = f.readlines()
    block = []
    for content in file_data:
        content = content.replace("\\n", "")
        if content[0:7] == ':020000':  # 这是非数据行,不能要
            continue
        block.append(content[9:-2])  # 前8个是地址,后2个是校验码
    # packageData是总的所有报文内容
    packageData = "".join(block)
    BlockLength = len(packageData) // 2
    print('数据内容的总长度是:', BlockLength)
    if BlockLength <= 4:
        print("data length too short!")
        return

    # packageData是总的所有报文内容,报文字符两两一组,以16进制字符串的形式写入dataList列表
    dataList = []
    for i in range(0, BlockLength):
        dataList.append(packageData[2 * i] + packageData[2 * i + 1])
    for i in range(0, BlockLength):
        dataList[i] = int(dataList[i], 16)
    # print(dataList)

    # Compute a CRC-32 checksum of data
    dataListCRC32 = zlib.crc32(bytes(dataList))
    print("crc32 of dataList is:%#x" % dataListCRC32)

    # 计算packageData可以被分解为fluidCount个1098长度的块,注意 一个字节长度用16进制表示是2位
    fluidCount = math.floor(len(dataList) / fluidLengthMax)
    print('fluidCount is ', fluidCount)
    lastFluidLength = BlockLength - fluidCount * fluidLengthMax
    print('the length of last fluid is ', lastFluidLength)
    # return
    list_data_out = []  # list_data_out用于计算CRC
    # 每个 fluid长度是 fluidLengthMax, 但是最后一个fluid的长度是 lastFluidLength
    for m in range(0, fluidCount + 1):  # 遍历每一个fluid
        if m == fluidCount:
            # print("这是最后一块fluid")
            fluidLength = lastFluidLength  # 最后一块的fluid的长度由计算而来
            if fluidLength == 0:  # 最后一块的fluid长度是0的话,那就直接退出
                break
        else:
            fluidLength = fluidLengthMax  # 不是最后一个fluid,长度是固定值,

        # 将所有报文以一个字节长度为一组放到列表dataList里,之后打印内容从dataList取
        dataListfluid = []
        for i in range(m * fluidLengthMax, m * fluidLengthMax + fluidLength):
            dataListfluid.append(dataList[i])

        # print("*****************Next print is the %dth fluid" % m)
        # 打印第一个帧 (格式如 164c3601********)
        list0 = [0x14, 0x4c, 0x36, 0x01, 0x55, 0x55, 0x55, 0x55]  # 初始化,这里的值只是一个格式的范例
        # 第0位赋值 左起4个bit一定是1(表示多帧的首诊),左起5-8个bit连同下一位的8个bit是长度 长度包括了0x36和fluidCount,所以需要+2
        list0[0] = (fluidLength + 2) // 0x100 + 0x10
        # 第1位赋值
        list0[1] = (fluidLength + 2) % 0x100
        # 第3位赋值 这一位对应fluidCount+1,每个fluid依次递增,注意若增长到0xff,下一个是0x00
        list0[3] = (m + 1) % 0x100
        # 第4位到第7位赋值
        if fluidLength >= 4:  # 这一帧可以填满
            for j in range(4, 8):
                list0[j] = dataListfluid[j - 4]
                list_data_out.append(list0[j])
            printlist(list=list0)
        else:  # 这一帧不够填满
            for j in range(4, 4 + fluidLength):
                list0[j] = dataListfluid[j - 4]
                list_data_out.append(list0[j])
            printlist(list=list0)
            if m == fluidCount:  # 如果是最后一个fluid,并且剩余长度还不足以填满一帧,意味着所有数据处理完成
                print('all input data print over')
            break
        if fluidLength == 4:  # 这一帧填满时,fluid已到结尾,只有最后一个fluid才可能出现这样情况
            break

        # 打印多帧21-2F
        list1 = []
        dataList_position = 4  # 若执行到这一步,那么数据列表章的位置只可能是4
        for i in range(0x21, 0x30, 1):
            list1.append(i)
            if dataList_position > fluidLength - 7:  # 判断是否是多帧的最后一帧,如果是,则将最后一阵填充结束后退出循环
                # print('the last line ,position is:', dataList_position, ' fluidLength is:', fluidLength)
                for k in range(dataList_position, fluidLength):
                    list1.append(dataListfluid[k])  # 最后一帧第一部分报文数据填写
                    list_data_out.append(dataListfluid[k])
                for k in range(7 - fluidLength + dataList_position):
                    list1.append(0x55)  # 最后一帧第二部分填充符填写,填充0x55
                    dataList_position = fluidLength
                printlist(list=list1)
                break
            for j in range(dataList_position, dataList_position + 7):  # 不是最后一帧,全部填充为数据
                list1.append(dataListfluid[j])
                list_data_out.append(dataListfluid[j])  # 用于计算CRC
            printlist(list=list1)
            # 每打印一行,便要清空list1,并且 BlockLength加7
            list1 = []
            dataList_position = dataList_position + 7
            if dataList_position == fluidLength:  # 长度正好结束,那么直接退出
                break

        if dataList_position == fluidLength:
            if m == fluidCount:  # 判断成立说明最后一帧已经处理结束
                print('all input data print over')
                break
            else:  # 本fluid结束,进入下一个fluid计算
                continue

        list2 = []
        # 打印多帧20-2F
        for i in range(1, 1000000):  # 这里循环次数需要设很大,中途某次循环检测到打印的是最后一行,会自动退出的
            for j in range(0x20, 0x30, 1):
                list2.append(j)
                if dataList_position > fluidLength - 7:  # 每行打印前要判断是否是多帧的最后一帧,如果是,则将最后一阵填充结束后退出循环
                    # print('the last line ,position is:', dataList_position, ' fluidLength is:', fluidLength)
                    for k in range(dataList_position, fluidLength):
                        list2.append(dataListfluid[k])  # 最后一帧打印,填写数据部分
                        list_data_out.append(dataListfluid[k])
                    for k in range(7 - fluidLength + dataList_position):
                        list2.append(0x55)  # 最后一帧打印,填写填充符部分
                    dataList_position = fluidLength
                    printlist(list=list2)
                    list2 = []
                    break
                for k in range(dataList_position, dataList_position + 7):  # 不是最后一帧,全部填充为数据
                    list2.append(dataListfluid[k])
                    list_data_out.append(dataListfluid[k])  # 用于计算CRC
                printlist(list=list2)
                # 每打印一行,便要清空list2,并且 BlockLength加7
                list2 = []
                dataList_position = dataList_position + 7
                if dataList_position == fluidLength:  # 长度正好结束,那么直接退出
                    break
            if dataList_position == fluidLength:  # 判断成立说明最后一帧已经处理结束
                break
    data_out_CRC32 = zlib.crc32(bytes(list_data_out))
    print("crc32 of printed data is:%#x" % data_out_CRC32)
    if data_out_CRC32 == dataListCRC32:
        print("crc32 check pass!")
        return 'true'
    else:
        print("crc32 check fail!")
        return 'false'


if __name__ == '__main__':
    main()

4.测试验证        

        测试前,将一个名为“*.hex“的文件放在代码同级目录下,获取每个fluidCount长度上限赋给 fluidLengthMax变量。代码执行结果如下:

数据内容的总长度是: 97031
crc32 of dataList is:0x80399f9d
fluidCount is  382
the length of last fluid is  3
[ 0x11,0x00,0x36,0x01,0x4B,0x4F,0x53,0x54, ]
......
[ 0x10,0x05,0x36,0x7F,0x6B,0x23,0x00,0x55, ]
all input data print over
crc32 of printed data is:0x80399f9d
crc32 check pass!

经过物理平台测试,本博客python编写的构造多帧报文的代码是正确的。

如何看懂UDS诊断报文

UDS介绍

UDS(Unified Diagnostic Services,统一的诊断服务)诊断协议是ISO 15765 和ISO 14229 定义的一种汽车通用诊断协议,位于OSI模型中的应用层,它可在不同的汽车总线(例如CAN, LIN, Flexray, Ethernet 和 K-line)上实现。UDS协议的应用层定义是ISO 14229-1,目前大部分汽车厂商均采用UDS on CAN的诊断协议。

UDS本质上是一系列的服务,共包含6大类26种。每种服务都有自己独立的ID,即SID。

  • SID:Service Identifier,诊断服务ID。UDS本质上是一种定向的通信,是一种交互协议(Request/Response),即诊断方给ECU发送指定的请求数据(Request),这条数据中需要包含SID。
  • 如果是肯定的响应(Positive Response),回复[SID+0x40],如请求10,响应50;请求22,响应62。
  • 如果是否定的响应(Negative Response),回复7F+SID+NRC,回复的是一个声明。

肯定响应和否定响应的形式一定要熟记。

常用服务介绍

UDS的26种服务中,有7种很重要。它们分别是:

  • $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(待机握手)。

 

下面对这7个服务进行解读。

$10诊断会话

$10包含3个子功能,

  • 01 Default,
  • 02 Programming,
  • 03 Extended,

ECU上电时,进入的是默认会话(Default)。如果您进入了一个非默认会话的状态,一个定时器会运转,如果一段时间内没有请求,那么到时间后,诊断退回到默认会话01。当然,我们有一个$3E的服务,可以使诊断保持在非默认的状态。

报文包含4种类型,即

  • SID,
  • SID+SF(Sub-function),
  • SID+DID(Data Identifier)(读写用),
  • SID+SF+DID。

NRC:Negative Response Code(否定响应码)。如果ECU拒绝了一个请求,它会回应一个NRC。不同的NRC有不同的含义。

例子:以CAN总线网络举例。

八个数据字节,第一字节被网络层占用

  • 请求(Request):

02 10 02 xx xx xx xx xx

02中的0代表网络层单帧SF,2代表 数据域有2个字节;10是SID,02是子功能

  • 肯定响应:

02 50 02 xx xx xx xx xx

02同上,10+40表示对SID的肯定回复,02是子功能。

  • 否定响应:

03 7F 10 22 xx xx xx xx;

03同上,7F表示否定响应,10是SID,22是NRC。

$3E待机握手

$3E服务用于向服务器指示诊断仪仍然连接在网络上,之前已经激活的诊断服务功能可以仍然保持激活状态。

例子:

02 3E 80 00 00 00 00 00,发送一个3E服务的报文,保持非默认会话状态。80表示无需回复。

$27安全访问

27服务,加上一个子服务,再加上一个钥匙,这样的服务请求可以进行解锁。

比如下面的例子,2n-1是某个子服务,通过首轮种子的请求,首轮ECU会返回67+01+AA+BB+CC+DD,AA~DD就是种子了。之后第二轮,诊断端会利用种子进行运算(利用整车厂的算法),生成k1(不一定是1个字节),那么发送请求,27+02+[k1]。ECU同样也会通过种子算出k2。当k1和k2匹配时,解锁(Unlocked)成功。

  • 例子:

Rx: 02 27 05 00 00 00 00 00 安全访问,05子功能
Tx: 07 67 05 08 27 11 F0 77 肯定响应,回复了对应安全级别的种子
Rx: 06 27 06 FF FF FF FF 00 发送密钥,4个FF。注意06是与05成对使用的。
Tx: 03 7F 27 78 00 00 00 00 否定响应,7F+27+NRC
Tx: 02 67 06 00 00 00 00 00 肯定响应,通过安全校验

$22读数据

$22读数据,
Request(请求):

22+DID(Data Identifier,通常是两个字节)

Response(响应):

62+DID+Data

DID有一部分已经被ISO 14229-1规定了。比如0xF186就是当前诊断会话数据标识符,0xF187就是车厂备件号数据标识符,0xF188就是车厂ECU软件号码数据ID,0xF189就是车厂ECU软件版本号数据标识符。

$2E写数据

$22写数据,
Request(请求):

2E+DID+Data

Response(响应):

6E+DID

注意,比如0xF186这个DID不支持直接写入数据,需要用$10来进行会话转换。也就是说,对于写数据的请求,一般来说需要在一个非默认会话,或解锁的状态下才能进行

$19 读DTC

DTC(diagnostic trouble code):如果系统检测到了一个错误,它将其存储为DTC。DTC可表现为:一个显而易见的故障:通讯信号的丢失(不会使故障灯亮起);排放相关的故障;安全相关的错误等。DTC可以揭示错误的位置和错误类型。通常DTC占用3个字节,OBD II占用两个字节。

故障码包括四个大类,分别是PCBU,P是powertrain动力系统,C是Chassis底盘,B是Body车身,U是network通信系统。一个DTC信息占用4个字节。最后一个字节是DTC的状态。前两个字节是我们熟知的类似P0047的故障码。

DTCHighByteDTCMiddleByteDTCLowByteDTCStatus
Byte 1Byte 2Byte 3Byte 4

$19 拥有28个子服务(Sub-Function)。常用的子服务有02(通过DTC状态掩码读取DTC),04(读取快照信息),06(读取扩展信息),0A(读ECU支持的所有DTC数据)。

$14清除DTC

清除(复位)DTC格式,它可以改变DTC的状态。3个FF代表清除所有DTC。

Request:14+FF+FF+FF;
Response:54 。

诊断报文解析

UDS 的诊断数据的发送与接收都是基于CAN,所以每个数据流都包含基本的CAN Message 的架构

CAN Message =CAN ID + CAN DATA

根据上篇UDS文章的叙述,每一个PDU 包含控制信息PCI,数据信息Data.

 

网络层 PDU(协议数据单元)PCI(协议控制信息)格式:具体如下图所示:

帧类型bit7-4bit3-0Byte 2Byte 3
单帧PCItype=0SF_DLN/AN/A
首帧PCItype=1FF_DLFF_DLN/A
连续帧PCItype=2SNN/AN/A
流控帧PCItype=3FSBSST_min

 

综上所述,N_PDU =N_PCI+N_DATA, N_PCI的值主要集中的前三个字节N_DATA值主要集中在后面7位字节。其中,

  • SF_DL 代表单帧中数据字节数(取值0-7),
  • FF_DL代表 连续帧中的数据字节数(12bit可表四8~4095),
  • SN代表此帧为连续帧中的第几帧,(0、1、2...E、F、0、1...)
  • FS流控制帧,有三种状态:继续发送0、保持等待1、数据溢出2
  • BS规定发送端允许持续传输连续帧数目的最大值(0~255),
  • STmin限定连续帧相互之间所允许的最小时间间隔。

先面用连个例子进行说明,请参考!

例子 1--- 单帧的数据传输与接收

[图片上传失败...(image-b66bab-1538824826939)]

数据发送: 02 27 09
数据反馈: 03 7F 27 7E ---==否定的响应==(Negative Response),回复==7F+SID+NRC==,回复的是一个声明

数据发送: 02 10 40
数据反馈: 06 50 40 00 32 01 F4 ---==肯定的响应==(Positive Response),回复[==SID+0x40==],就是请求10,响应40;回复的是一组数据

由于这个数据发送与接收都是单帧传输,所以第一个数据的高四位均为0,四个数据流中的第一个字节的低四位,02,03,02,06代表的为此帧数据含有几个字节,多余的数据位都用 00或者AA行填充。

例子2 --- 多帧的数据接收与传输

[图片上传失败...(image-b5e84b-1538824826939)]

数据发送:

  • 06 19 04 00 01 00 00 00

数据反馈:

  • 10 1E 59 04 00 01 00 27
  • 30 00 00 00 00 00 00 00
  • 21 00 0B FF FF FF FF FF
  • 22 FF FF FF FF FF FF FF
  • 23 FF FF FF FF FF FF FF
  • 24 FF FF FF AA AA AA AA

数据发送为单帧,所以06代表发送的数据中含有6个字节,

回复为Positive Response,为连续帧。

  • 10中的1代表连续帧的首帧,==01E代表此连续帧含有30个字节==,
  • 30代表此连续帧的流控制帧,
  • 21,22,23,24代表连续帧中的第几帧,21代表第一帧,22代表第二帧,依此类推,其中AA为填充位。

参考资料:



作者:智车科技
链接:https://www.jianshu.com/p/b5805e734ed6
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

以上是关于根据hex文件制作UDS统一诊断服务CAN多帧报文-python的主要内容,如果未能解决你的问题,请参考以下文章

如何看懂UDS诊断报文

如何看懂UDS诊断报文

如何看懂UDS诊断报文

详解UDS CAN诊断:什么是UDS(ISO 14229)诊断?

UDS-CAN网络层传输协议

基于CAN总线的汽车诊断协议UDS(ECU底层模块移植开发)