ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

LabVIEW 与周立功 USBCAN 实现 UDS 诊断自动化测试

LabVIEW 与周立功 USBCAN 实现 UDS 诊断自动化测试

简介:这是一份面向汽车电子测试工程师与LabVIEW开发者的UDS诊断服务测试工具,基于LabVIEW 2018开发,配合周立功USBCAN系列(支持USBCAN1-2及EU系列)硬件使用,可快速验证ECU诊断服务的响应情况。使用者只需输入请求ID、响应ID与诊断服务,即可发起测试并观察返回结果,适合诊断协议调试、ECU功能验证及自动化测试入门等场景。资源包共305个文件,约6.45MB,以160个xml配置与126个dll动态库为主,另有ini配置、h头文件、lib库及aliases别名文件,并附带可执行程序,整体结构完整、开箱即用。目前已有2236人学习下载,说明其在诊断测试领域具备一定参考价值。通过该工具,读者可省去从零搭建诊断测试环境的成本,直接获得可运行的测试入口,并借助其中的库文件与配置理解USBCAN通信与UDS服务调用的组织方式,为后续二次开发或集成到自有测试流程提供基础。

1. 用 LabVIEW 和周立功 USBCAN 搭一套 UDS 诊断服务测试台

手里有一块周立功 USBCAN 盒子,装好驱动,插上 OBD 线,打开 LabVIEW,很多人第一反应是「这俩能凑一块做 UDS 诊断吗」。答案是能,而且这套组合在零部件厂和整车厂的台架测试里非常常见:USBCAN 负责物理层和 CAN 报文收发,LabVIEW 负责上层调度、诊断请求构造、响应解析和自动化判定。UDS(统一诊断服务,ISO 14229)跑在 CAN 之上,靠的是 0x7DF 功能寻址或 0x7E0/0x7E8 物理寻址这一套请求-响应机制。这篇文章不讲空泛概念,只讲怎么用 LabVIEW 调周立功的 DLL,把 10、11、22、2E、27、31 这些常见服务一条条跑通,并且把测试脚本做成能重复执行、能自动判 PASS/FAIL 的形态。适合正在做 ECU 诊断测试、刷写验证、产线 EOL 检测的工程师,也适合刚接触 UDS 想找个能落地的上位机方案的人。

2. 先搞清楚 USBCAN 和 UDS 在 LabVIEW 里怎么对接

2.1 周立功 USBCAN 的二次开发接口长什么样

周立功的 USBCAN 系列(常见如 USBCAN-II、USBCAN-2E-U 等)提供了一套动态链接库,核心文件通常是ControlCAN.dll,配套头文件ControlCAN.h。LabVIEW 通过「调用库函数节点」(Call Library Function Node,简称 CLN)来调这些导出函数。整个调用链是固定的几步:

  1. VCI_OpenDevice打开设备
  2. VCI_InitCAN初始化指定 CAN 通道,设置波特率
  3. VCI_StartCAN启动通道
  4. VCI_Transmit发报文,VCI_Receive收报文
  5. VCI_CloseDevice关闭设备

这里最容易翻车的地方是结构体对齐。VCI_CAN_OBJ这个结构体在 C 里定义了一堆字段,LabVIEW 的 CLN 如果按默认对齐去配,传进去的数据就会错位,现象是设备能打开但发不出报文,或者收到的 ID 全是乱的。我一般会在 CLN 里把结构体按「簇」来映射,并且严格对照头文件里的字段顺序和类型宽度来配。

// ControlCAN.h 里 VCI_CAN_OBJ 的关键字段(示意,以实际头文件为准) typedef struct { UINT ID; // 报文 ID UINT TimeStamp; // 时间戳 BYTE TimeFlag; // 是否使用时间戳 BYTE SendType; // 发送类型:0 正常,1 单次 BYTE RemoteFlag; // 远程帧标志 BYTE ExternFlag; // 扩展帧标志 BYTE DataLen; // 数据长度 BYTE Data[8]; // 数据 BYTE Reserved[3]; } VCI_CAN_OBJ;

在 LabVIEW 里对应建一个簇,字段顺序、类型宽度必须一一对上。ID和TimeStamp是 32 位无符号整型,几个 Flag 是 8 位无符号整型,Data是 8 字节数组。配错一个字段,后面全乱。

2.2 UDS 请求-响应在 CAN 上的帧格式

UDS 本身是应用层协议,它不管底下是 CAN 还是 CAN FD 还是 DoIP。跑在经典 CAN 上时,一帧最多 8 字节,所以 UDS 报文要按 ISO-TP(ISO 15765-2)做分段。单帧(SF)格式是:第一个字节高 4 位是帧类型,低 4 位是数据长度。比如发10 03(进入扩展会话),实际 CAN 数据是02 10 03 00 00 00 00 00,第一个字节0x02表示单帧、后面有 2 个有效字节。

多帧就复杂了,首帧(FF)、连续帧(CF)、流控帧(FC)要按顺序来。做诊断测试时,如果只是发短请求、收短响应,单帧基本够用。但像 22 服务读长数据、或者 2E 写大块数据,就会碰到多帧。这时候 ISO-TP 的流控参数(BS 块大小、STmin 最小间隔)如果和 ECU 对不上,就会出现发了一半卡住、或者 ECU 回 NRC 0x21(busyRepeatRequest)的情况。

2.3 在 LabVIEW 里封装一个「发一帧收一帧」的基础函数

不管后面做多少服务,底层都是「发一帧、等响应、超时判定」这个循环。我一般先封装一个基础 VI,输入是 CAN ID、数据数组、超时时间,输出是响应 ID、响应数据、是否超时。

VI 名称:CAN_SendReceive.vi 输入: - DeviceHandle (I32):VCI_OpenDevice 返回的设备句柄 - ChannelIndex (U8):CAN 通道号,0 或 1 - TxID (U32):发送 ID,物理寻址常用 0x7E0 - TxData (U8 数组):UDS 请求数据,已按 ISO-TP 打包 - TimeoutMs (U32):等待响应超时,一般 1000~2000ms 输出: - RxID (U32):响应 ID,物理寻址常用 0x7E8 - RxData (U8 数组):响应数据 - IsTimeout (Bool):是否超时

内部逻辑:先调VCI_Transmit发出去,然后进一个带超时的循环,反复调VCI_Receive收报文,收到 ID 匹配的就把数据取出来,超时到了就置IsTimeout为真退出。这里有个细节:VCI_Receive是非阻塞的,返回 0 表示没收到,返回正数表示收到了几帧。所以循环里要加个小延时(比如 1~5ms),不然 CPU 跑满。

参数说明:TimeoutMs不能设太短,ECU 处理诊断请求需要时间,尤其是 27 安全访问做种子密钥计算、31 例程执行,给 2000ms 比较稳。TxID和RxID要按实际 ECU 的寻址方式来,功能寻址发 0x7DF、收 0x7E8,物理寻址发 0x7E0、收 0x7E8,不同项目可能不一样,看诊断规范。

3. 把 10、11、22、2E、27、31 这些服务一条条跑通

3.1 会话切换和复位:10 和 11 服务

10 服务(DiagnosticSessionControl)是几乎所有诊断流程的第一步。默认会话是 01,扩展会话是 03,编程会话是 02。发10 03进扩展会话,ECU 正常回50 03加上一堆参数(P2 时间等)。如果回7F 10 12,说明 ECU 不支持这个会话或者当前条件不满足。

11 服务(ECUReset)用来复位 ECU,子功能 01 是硬复位、02 是钥匙关复位、03 是软复位。发11 01,正常回51 01,然后 ECU 会重启,这时候 CAN 上可能会短暂丢报文,测试脚本要能处理这个空窗期。

步骤: 1. 构造请求数据:TxData = [0x02, 0x10, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00] 2. 调用 CAN_SendReceive.vi,TxID=0x7E0,TimeoutMs=2000 3. 判断 RxData[0] 是否为 0x06(单帧,6 字节有效) 4. 判断 RxData[1] 是否为 0x50(10 服务的肯定响应 SID) 5. 判断 RxData[2] 是否为 0x03(回显子功能) 6. 如果 RxData[1] == 0x7F,说明是否定响应,RxData[3] 是 NRC 码

逻辑说明:UDS 肯定响应的 SID 是请求 SID 加 0x40,所以 10 对应 50、11 对应 51、22 对应 62、2E 对应 6E、27 对应 67、31 对应 71。否定响应固定是7F开头,后面跟请求 SID 和 NRC。这个规律记住,解析响应的时候能省很多事。

3.2 读数据和写数据:22 和 2E 服务

22 服务(ReadDataByIdentifier)按 DID 读数据,请求格式是22加两个字节的 DID。比如读 VIN 一般是22 F1 90,正常回62 F1 90加 17 字节 VIN。如果 VIN 比较长,响应会走多帧,这时候 ISO-TP 的连续帧处理就派上用场了。

2E 服务(WriteDataByIdentifier)按 DID 写数据,请求格式是2E加 DID 加数据。写之前一般要先过 27 安全访问,不然 ECU 会回7F 2E 33(securityAccessDenied)。

22 服务测试步骤: 1. TxData = [0x03, 0x22, 0xF1, 0x90, 0x00, 0x00, 0x00, 0x00] 2. 发送并等待响应 3. 如果 RxData[1] == 0x62,解析后面的数据 4. 如果响应是多帧,先收首帧,再发流控帧,再收连续帧 2E 服务测试步骤: 1. 先执行 27 服务解锁 2. TxData = [0x04, 0x2E, 0xF1, 0x90, 0x01, 0x00, 0x00, 0x00](示意) 3. 发送并等待响应 4. 判断 RxData[1] 是否为 0x6E

参数说明:DID 是两字节,范围 0x0000~0xFFFF,具体哪个 DID 对应什么数据要看诊断数据库(CDD/ODX 文件)。写数据时要注意数据长度,2E 请求的总长度不能超过 ISO-TP 单帧能承载的 7 字节(首字节算帧头),超了就要走多帧。多帧的流控参数 BS 一般设 0(表示 ECU 可以连续发完),STmin 设 0 或 10ms,具体看 ECU 要求。

3.3 安全访问:27 服务的种子密钥流程

27 服务是诊断测试里最容易出问题的一环。流程是:先发27 01请求种子,ECU 回67 01加种子;然后用种子算出密钥,发27 02加密钥;ECU 验证通过回67 02,不通过回7F 27 35(invalidKey)。

密钥算法每个项目不一样,常见的有固定密钥、异或、查表、AES 等。测试阶段如果拿不到算法,可以先用固定密钥或者已知的测试密钥跑通流程,验证 LabVIEW 这边的收发和解析没问题,再换真实算法。

27 服务测试步骤: 1. TxData = [0x02, 0x27, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00] 2. 发送并等待响应,正常回 67 01 + 种子 3. 从 RxData 里提取种子(一般是 4 字节或 8 字节) 4. 调用密钥算法计算密钥 5. TxData = [0x06, 0x27, 0x02, 密钥字节...] 6. 发送并等待响应,正常回 67 02 7. 如果回 7F 27 35,说明密钥算错了 8. 如果回 7F 27 37,说明请求序列超时,要重新请求种子

注意:27 服务有尝试次数限制,连续输错密钥可能会触发 ECU 锁定,需要等一段时间或者断电重启。测试脚本里最好加个计数器,错几次就停下来人工检查,别硬跑。

3.4 例程控制:31 服务的启动和结果查询

31 服务(RoutineControl)用来执行 ECU 内部的例程,比如擦除 Flash、校验内存、自检等。子功能 01 是启动例程,02 是停止例程,03 是查询例程结果。请求格式是31加子功能加两字节例程 ID 加可选参数。

31 服务测试步骤: 1. 启动例程:TxData = [0x04, 0x31, 0x01, 0xFF, 0x00, 0x00, 0x00, 0x00] 2. 等待响应,正常回 71 01 FF 00 3. 如果例程执行时间较长,ECU 可能先回 7F 31 78(responsePending) 4. 收到 78 后要继续等,直到收到最终响应 5. 查询结果:TxData = [0x03, 0x31, 0x03, 0xFF, 0x00, 0x00, 0x00, 0x00] 6. 正常回 71 03 FF 00 加结果数据

参数说明:例程 ID 是两字节,具体值看诊断规范。7F 31 78这个 NRC 很特殊,它不是失败,而是「还在处理,别急」。测试脚本里要专门处理这个:收到 78 就把超时重置,继续等,直到收到非 78 的响应或者总超时到了。我一般会把单次等待超时设 2000ms,总超时设 10000ms,防止例程跑太久。

4. 避坑:USBCAN 调 LabVIEW 做 UDS 测试最容易踩的五个坑

4.1 设备打开成功但发不出报文

现象:VCI_OpenDevice返回 1(成功),VCI_InitCAN也返回 1,但VCI_Transmit一直返回 0,CAN 总线上用示波器或者另一个 CAN 盒看不到任何报文。

原因:最常见的是VCI_INIT_CONFIG结构体里的波特率参数配错。周立功的波特率不是直接填 500000,而是填一个预分频值,比如 500kbps 对应0x1C(具体值看手册)。另外,如果设备是双通道的,VCI_InitCAN的通道号传错了,初始化的是另一个通道,发报文自然发不出去。

解决:对照手册确认波特率参数,用周立功自带的 CANTest 软件先验证设备能正常收发,再回到 LabVIEW 里调。通道号从 0 开始试,0 不行试 1。

4.2 收到的报文 ID 和数据全是乱的

现象:VCI_Receive返回正数,但取出来的 ID 和数据跟预期完全对不上,有时候 ID 看起来像数据、数据看起来像 ID。

原因:VCI_CAN_OBJ结构体在 LabVIEW 里的簇映射错了。最常见的是字段顺序搞反,或者Data数组的宽度配成了 16 位而不是 8 位。LabVIEW 的 CLN 对结构体对齐很敏感,C 里默认按 4 字节对齐,LabVIEW 如果按 1 字节对齐去配,整个结构体就错位了。

解决:在 CLN 配置里把结构体对齐方式改成和 C 一致,或者干脆把VCI_CAN_OBJ拆成几个独立的参数来传,避开结构体映射。我一般用后者,虽然麻烦点,但不容易出错。

4.3 多帧响应收不全,卡在中间

现象:发 22 服务读长数据,ECU 回了首帧,但后面的连续帧收不到,或者收到一半就超时了。

原因:ISO-TP 的流控帧没发对。收到首帧后,测试端要发一个流控帧(FC)告诉 ECU 可以继续发,流控帧格式是30加 BS 加 STmin。如果 BS 设成 0 表示可以连续发,STmin 设成 0 表示最小间隔。如果流控帧没发或者参数不对,ECU 就不发后续的连续帧了。

解决:在CAN_SendReceive.vi里加多帧处理逻辑,收到首帧(首字节高 4 位是 1)后,解析出总长度,然后发流控帧,再循环收连续帧(首字节高 4 位是 2),直到收满总长度。流控帧的 BS 一般设 0,STmin 设 0 或 10。

4.4 27 服务密钥算对了但 ECU 还是回 35

现象:种子拿到了,密钥按算法算出来,发过去 ECU 回7F 27 35,说密钥无效。

原因:种子的字节序搞反了。有些 ECU 回的种子是大端,有些是小端,密钥算法如果按固定字节序去算,遇到另一种就错了。另外,种子可能有有效时间,拿到种子后太久没发密钥,种子过期了也会回 35。

解决:先确认种子的字节序,可以拿已知正确的密钥反推验证。种子拿到后尽快算密钥发出去,别在中间加太多延时。如果还是不行,检查密钥算法的输入是不是包含了其他固定值(比如常量、DID 等)。

4.5 测试脚本跑着跑着就卡死

现象:脚本连续跑几十条用例后,突然卡在某个VCI_Receive循环里出不来,或者 LabVIEW 界面无响应。

原因:VCI_Receive在没收到报文时返回 0,如果循环里没加延时,CPU 占用率会飙到 100%,LabVIEW 主线程被拖死。另外,如果设备句柄没正确释放,反复打开关闭设备会导致资源泄漏,跑一段时间后设备就打不开了。

解决:接收循环里加 1~5ms 的延时,用 LabVIEW 的「等待(ms)」函数。设备句柄在程序结束时一定要调VCI_CloseDevice释放,最好用「错误簇」串起来,保证异常时也能走到释放那一步。

5. 把单条测试变成能自动判 PASS/FAIL 的测试序列

5.1 用表格驱动测试用例

单条服务跑通之后,下一步是把所有用例组织起来。我一般用一个二维字符串数组或者 CSV 文件来存测试用例,每行是一条:用例编号、服务名、请求数据、期望响应、超时时间。LabVIEW 读进来之后循环执行,每条用例的结果写到一个结果数组里,最后统一判定。

用例表结构(CSV 示意): CaseID,Service,Request,Expected,Timeout 001,SessionControl,02 10 03 00 00 00 00 00,50 03,2000 002,ECUReset,02 11 01 00 00 00 00 00,51 01,2000 003,ReadDID,03 22 F1 90 00 00 00 00,62 F1 90,2000 004,SecurityAccess,02 27 01 00 00 00 00 00,67 01,2000 005,RoutineControl,04 31 01 FF 00 00 00 00,71 01,5000

逻辑说明:Request列是十六进制字符串,LabVIEW 里用「从字符串扫描」函数转成 U8 数组。Expected列是期望响应的前缀,判定的时候不要求完全相等,只要响应以期望前缀开头就算 PASS,这样能兼容响应后面跟的不同数据。Timeout列是这条用例的超时时间,31 服务这种耗时的例程给长一点。

5.2 响应判定的三种模式

实际测试里,响应判定不能只做「完全匹配」,太死板。我一般支持三种模式:

  • 前缀匹配:响应以期望数据开头就算 PASS,适合大多数肯定响应
  • 包含匹配:响应里包含期望数据就算 PASS,适合响应里夹杂了其他信息的场景
  • 正则匹配:用正则表达式匹配,适合响应格式不固定但有一定规律的场景
判定逻辑(伪代码): if 判定模式 == "前缀": PASS = (RxData 的前 N 字节 == Expected 的前 N 字节) elif 判定模式 == "包含": PASS = (Expected 在 RxData 中出现) elif 判定模式 == "正则": PASS = 正则匹配(RxData 转十六进制字符串, Expected)

参数说明:前缀匹配的 N 就是 Expected 的长度。包含匹配要注意 Expected 不能太短,不然容易误判。正则匹配性能差一点,用例多的时候慎用。

5.3 测试报告和失败重试

跑完一轮之后,把结果写成一个报告文件,每条用例记录:用例编号、请求、实际响应、期望响应、PASS/FAIL、耗时。FAIL 的用例单独列出来,方便定位。

失败重试我一般加一层:某条用例 FAIL 之后,自动重试 1~2 次,如果重试还 FAIL 才最终判 FAIL。这样能过滤掉一些偶发的总线干扰或者 ECU 忙导致的失败。但 27 服务这种有次数限制的用例不要自动重试,容易触发锁定。

重试逻辑: for retry in range(0, MaxRetry): 执行用例 if PASS: break else: 等待 100ms if not PASS: 最终判 FAIL,记录到报告

MaxRetry 一般设 1 或 2,别设太多,不然跑一轮的时间会拉很长。

5.4 一个我踩过的坑:时间戳和日志

早期做这套测试台的时候,我没在日志里加时间戳,结果一条用例 FAIL 了,回头看日志完全不知道是哪个时间点出的问题,也没法跟 CAN 总线上的其他报文做时间对齐。后来我在每条用例执行前后都打时间戳,精确到毫秒,日志里还记录发送和接收的原始 CAN 帧,这样出问题的时候能直接对照总线数据排查。

另一个习惯是:每次测试开始前,先把设备打开、通道初始化、发一帧测试报文确认链路通,再开始跑用例。这个「握手」步骤能提前发现设备没插好、波特率不对这类低级问题,省得跑了一半才发现。

这套 LabVIEW 加周立功 USBCAN 的方案,核心就是把底层 CAN 收发封装稳,把 UDS 服务的请求构造和响应解析做扎实,再用表格驱动的方式把用例组织起来。难点不在 LabVIEW 本身,而在对 UDS 协议细节和 ISO-TP 分段的把握,以及对 USBCAN 接口的熟悉程度。我自己的习惯是每加一个新服务,先用 CANTest 手动发一帧确认 ECU 有正常响应,再回到 LabVIEW 里写代码,这样能把「协议问题」和「代码问题」分开,排查起来快很多。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进