资讯动态

Python控制ZLG USB-CAN:CAN总线自动化测试实战指南

发布时间:2026/9/28 1:30:31 来源:尧图企业网站定制
如果你干过嵌入式测试、ECU标定、工业设备联调这些活大概率对ZLG致远电子的USB-CAN分析仪不陌生驱动装上ZCANPRO打开点几下就能抓总线报文。可一碰到自动化测试脚本就头疼了——闭着眼睛点了两小时鼠标几千条压力报文还没发出去。这个时候用Python把ZLG CAN设备拉起来让上位机按你的节奏收发报文才是真正能干活的玩法。这篇指南从环境配置讲到实际收发把我踩过的一些坑一起分享出来。适合刚开始接触CAN总线的Python工程师、自动化测试岗位的朋友以及想脱离ZCANPRO、把CAN通信整合进自己测试框架的人。1. 为什么是Python加ZLG CAN先想清楚再动手1.1 场景对比ZCANPRO能做和不能做的事ZCANPRO是ZLG官方免费上位机适合现场抓包、手动点发甚至也带一点脚本能力但本质是图形工具很难嵌入自动化流水线。一次性要测200条周期报文的收发一致性把接收到的CAN帧实时写进时序数据库或者根据测试结果自动调整发送策略GUI就力不从心。Python的价值在于它能把你已经用ZCANPRO验证过的总线逻辑变成一个可复用的函数库initialize_bus()、send_frame()、read_frames()之后就能接到pytest、CI、性能测试脚本里也能轻松配合数据分析、可视化、云端上传。你不需要在电脑前守着程序自己会按计划发帧、收帧、判断结果、生成报告。1.2 三种Python方案怎么选直接用ctypes加载zlgcan.dll或ControlCAN.dll不依赖第三方CAN库最贴近底层可控制性最强适合想把原理搞透、后续要做特殊帧处理的人。代价是结构体定义、句柄管理、内存分配都要自己来。使用python-can社区维护的Python CAN库自带ZLG设备后端接口简单底层一个Bus对象就够用收发、过滤、异步读取都有。适合绝大多数日常测试脚本。在python-can之上再套CANopen如果总线协议栈是CANopen直接用canopen库PDO/SDO操作一行调用即可不需要自己拼报文、解析报文。我的建议是新手先用ctypes把一帧报文发出去把原理打通然后日常开发切到python-can。这样以后遇到python-can覆盖不到的功能你也知道底层是怎么回事排起错来不会两眼一抹黑。2. CAN协议补课不搞懂这几件事就写代码一定会踩坑2.1 数据帧和远程帧别把RTR位设错ZLG的报文结构体里有一个RemoteFlag字段这就是CAN协议里的RTR位。0表示数据帧1表示远程帧。远程帧本身不携带数据DataLen通常置0它用来请求对端节点发送对应ID的数据帧。很多新手把RemoteFlag设成1后还在Data里塞数据结果对端要么不回要么数据校验失败排查起来特别费劲。实际开发中远程帧用得并不多绝大多数ECU通信都是数据帧所以你写代码时先明确这一帧是干嘛的再决定RemoteFlag和DataLen。发数据帧时还有一个容易忽略的点DataLen最大是8字节经典CAN帧一次最多携带8个用户数据字节。如果你要发超过8字节的业务数据要么拆帧要么上CAN FD。2.2 ID仲裁为什么ID越小越优先CAN总线上多个节点同时发帧靠ID仲裁决定谁先上总线。ID数值越小显性优先级越高这就是“ID越小越快”的由来。用ZLG设备抓包时你经常看到低ID的周期报文牢牢占据总线高ID的偶发报文只能排队。做自动化测试时如果要发送一个高ID的低优先级报文并希望它及时被总线接收就必须考虑总线上是否有大量低ID报文在抢占资源不然可能一直仲裁失败表现为发送函数正常返回但对方根本没收到。扩展帧还是标准帧要分清报文结构体里的ExternFlag0是标准帧1是扩展帧。11位标准ID和29位扩展ID的仲裁规则不完全一样抓包解析时也要注意。很多时候抓到的ID看起来一样一个是标准帧一个是扩展帧其实是两个不同的报文。2.3 终端电阻与波特率物理层决定一切高速CAN总线两端各并一个120欧终端电阻要求端接阻值在105欧到130欧之间不然上升沿会过冲或反射出现位错误。测量方法很简单总线断电后在任意一端量CAN_H和CAN_L之间电阻如果量到约60欧说明两端终端电阻都在如果量到约120欧说明只有一端有终端电阻。波特率必须和总线上所有节点一致。ZLG设备初始化时通过Timing0/Timing1两个寄存器值设定波特率ZCANPRO里有波特率配置表500k常用0x00/0x1C250k常用0x01/0x1C具体值和设备晶振频率相关不能拿着别人的表到处套。新手最常见的坑就是代码里设125k对端设备却是500kCAN控制器错误计数器持续累积最终进入bus-off。2.4 采样点比波特率更难发现的坑采样点取在位的哪个位置决定了抗干扰能力。CAN协议中常规推荐采样点在75%到87.5%之间。ZLG SDK里Timing1的配置决定采样点位置虽然大多数场景默认配置能工作但在总线较长、节点较多或电磁环境较差时采样点设置不合理会出现偶发错误帧或CRC错误这类问题不看协议分析仪根本发现不了。我遇到过一条300米总线上偶尔丢一帧抓包软件显示CRC错误最后把采样点从75%调到85%就稳定了。如果你的项目总线比较长或现场干扰明显这个参数值得花时间研究而不是全程用默认值。3. 环境准备Python装好只是第一步3.1 Python解释器与IDEPython环境这块对写过代码的朋友很简单去python.org下载3.9或3.10版本安装时勾选Add Python to PATH然后一路下一步。IDE我用PyCharm Community版创建项目时选择系统里已有的解释器就行。不用刻意追求最新版Python有些Windows驱动DLL对过于新的版本没有验证我在3.13上遇到过ctypes行为变化导致的兼容问题3.9到3.11在公司内部项目里用得最稳。3.2 ZLG驱动与SDK安装设备到手后先装官方驱动包不同设备驱动略有差异。装完到设备管理器里看应该会出现一个USB-CAN设备。没看到就检查USB线是不是只有电源没有数据以及是否插了扩展坞。很多玄学问题最后都出在扩展坞上尤其是劣质扩展坞设备枚举成功但通信一开就崩。然后去官网下载对应型号的SDK开发包里面包含zlgcan.dll或ControlCAN.dll、头文件、C示例、ZCANPRO。建议把SDK里的DLL放到一个固定目录比如C:\zlg_sdk再把这个目录加到系统PATH。虽然也可以用绝对路径加载DLL但为了以后用python-can不折腾加入PATH最省事。3.3 确认DLL和设备识别状态打开PyCharm的Python Console执行import ctypes zlg ctypes.WinDLL(zlgcan.dll) print(zlg)能打印出DLL对象基本就说明加载成功。接着可以写一行调用ZCAN_GetDeviceNumberdev_num zlg.ZCAN_GetDeviceNumber() print(dev_num)如果返回大于0说明驱动和设备都在线。这个接口在部分老版本DLL里没有所以这一步报“函数不存在”别慌张改用ZCANPRO确认设备在线同样有效。4. 手写第一段Python代码加载DLL到CAN初始化4.1 为什么直接调DLL而不是等别人封装ctypes是Python标准库不用pip安装任何东西只要拿到头文件里的结构体定义就能调用。缺点是如果DLL函数参数类型不对容易造成内存踩坏进程直接崩溃。所以我一般先把结构体和函数原型定义好再写业务逻辑。下面以新版ZCAN库为例老版VCI接口套路类似只是函数名从ZCAN_换成VCI_参数里多个设备类型和设备索引。这里先掌握一套另一套看着头文件也能写出来。4.2 定义报文结构体和初始化配置CAN报文对象在C头文件里一般叫VCI_CAN_OBJ或ZCAN_CAN_OBJ字段基本一致。ctypes定义如下import ctypes class VCI_CAN_OBJ(ctypes.Structure): _pack_ 1 _fields_ [ (ID, ctypes.c_uint32), (TimeStamp, ctypes.c_uint32), (TimeFlag, ctypes.c_uint8), (SendType, ctypes.c_uint8), (RemoteFlag, ctypes.c_uint8), (ExternFlag, ctypes.c_uint8), (DataLen, ctypes.c_uint8), (Data, ctypes.c_uint8 * 8), (Reserved, ctypes.c_uint8 * 3), ]初始化配置结构体一般叫ZCAN_AUTO_INIT_STRUCT或VCI_INIT_CONFIGclass ZCAN_AUTO_INIT_STRUCT(ctypes.Structure): _pack_ 1 _fields_ [ (acc_code, ctypes.c_uint32), (acc_mask, ctypes.c_uint32), (filter, ctypes.c_uint8), (timing0, ctypes.c_uint8), (timing1, ctypes.c_uint8), (mode, ctypes.c_uint8), (canfd, ctypes.c_uint8), (pad, ctypes.c_uint8 * 3), ]注意不同版本的DLL结构体可能字段顺序不一样一定要以你下载的头文件为准不能从网上抄一份就用。结构体字段错一个字节轻则初始化失败重则整个Python进程崩溃。这也是为什么我建议先跑通“结构体定义ZCANPRO对拍”再写正式代码。给函数设置参数类型和返回类型同样重要能减少指针类型传错的问题zlg.ZCAN_OpenDevice.restype ctypes.c_int zlg.ZCAN_OpenDevice.argtypes [ctypes.c_int, ctypes.c_int, ctypes.c_int]4.3 初始化流程打开设备、取通道句柄、初始化、启动流程总共五步打开设备、取通道句柄、填初始化参数、让通道启动、开始收发。示例DEV_TYPE 21 # 在头文件里查不同设备型号对应的宏不一样 DEV_INDEX 0 CHANNEL 0 dev_handle zlg.ZCAN_OpenDevice(DEV_TYPE, DEV_INDEX, 0) print(dev_handle:, dev_handle) chan_handle zlg.ZCAN_GetDeviceChannel(dev_handle, CHANNEL) print(chan_handle:, chan_handle) cfg ZCAN_AUTO_INIT_STRUCT() cfg.acc_code 0 cfg.acc_mask 0xFFFFFFFF # 不滤波接收所有帧 cfg.filter 0 cfg.timing0 0x00 cfg.timing1 0x1C # 500kbps具体查你设备的波特率表 cfg.mode 0 # 0 正常模式1 只听模式2 回环模式 cfg.canfd 0 # 0 经典CAN1 表示CAN FD ret zlg.ZCAN_InitCAN(chan_handle, ctypes.byref(cfg)) print(init ret:, ret) ret zlg.ZCAN_StartCAN(chan_handle) print(start ret:, ret)参数说明acc_code和acc_mask配合使用来设定滤波范围。Mask0xFFFFFFFF且Filter0表示全部接收这是最省心的配置。如果你只想接收某个ID区间再去研究硬件滤波算法。mode0是正常模式总线两端或有其他节点时使用。mode2是回环模式自己发自己收不需要接任何外部总线适合第一次联调。只听模式只能接收不能发送用于观察总线。注意只听模式下如果总线上有其他节点的周期性报文你这边只收不发不会干扰总线。4.4 发送和接收把结构体传给DLL发送一帧标准ID为0x123的数据帧数据是01 02 03 04 05 06 07 08frame VCI_CAN_OBJ() frame.ID 0x123 frame.ExternFlag 0 # 标准帧 frame.RemoteFlag 0 # 数据帧 frame.DataLen 8 frame.Data[:8] bytes([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]) sent zlg.ZCAN_Transmit(chan_handle, ctypes.byref(frame), 1) print(sent:, sent)如果返回1说明这帧数据已经从设备发出去了。返回0则要检查链路。这里有个容易踩的坑设备发送成功只代表驱动把帧交给了CAN控制器不代表对端节点真的收到了所以发送函数的返回值仅供参考。接收函数ZCAN_Receive的最后一个参数是等待毫秒数传100表示最多等100毫秒有数据就返回实际帧数没数据返回0rx_buf (VCI_CAN_OBJ * 64)() cnt zlg.ZCAN_Receive(chan_handle, rx_buf, 64, 100) print(recv cnt:, cnt) for i in range(cnt): f rx_buf[i] print(hex(f.ID), bytes(f.Data[:f.DataLen]))这里一个小技巧接收缓冲区用ctypes数组一次性申请64个结构体DLL往这块连续内存里填数据返回实际数量。不要在循环里每次创建一个结构体去收DLL没法管理你新创建的对象。5. 从配置到通信一次完整收发试验5.1 单节点自测把回环模式跑通第一次写初始化代码建议直接设cfg.mode2回环模式。这个模式下设备内部把发送的数据直接送回接收缓冲区不需要总线上有任何其他节点非常适合验证DLL调用链路、结构体定义、收发流程是否正确。启动之后先用发送代码发一帧再调用接收代码大概率能在等待时间内收到同一帧。如果回环模式都收不到说明结构体或初始化参数有问题八成是Timing0/Timing1不匹配或结构体字段定义错位。我自己在第一次跑ZLG设备时就在结构体对齐上栽过跟头。C头文件里用了#pragma pack(1)我在ctypes里没设_pack_ 1导致结构体大小比DLL期望的大几个字节发送函数一直返回0。后来把所有结构体定义都加上_pack_ 1问题立刻消失。5.2 双节点通信两台设备或设备对ECU回环跑通后切换到正常模式把设备接到真实总线上或者用两个USBCAN设备和一根带终端电阻的总线对测。总线两端各接一个120欧电阻两个设备各连一端。A设备初始化500kB设备也初始化500kA发B收验证通过后再把程序里的波特率、ID、数据长度换成实际项目参数。这里我强烈建议先用ZCANPRO在电脑上看同一根总线的报文确认ZCANPRO能看到再把Python程序接进来。如果ZCANPRO都看不到说明问题在物理层或配置别让Python背锅。反过来如果ZCANPRO能看到Python收不到就集中排查滤波、通道号、DLL调用参数这些软件层面的问题。5.3 打印与日志别只用print接收循环里直接print在帧率低的时候没问题报文一多就会丢帧。Windows下print到控制台是同步IO遇到大量帧时接收缓冲区会溢出表现就是DLL返回的接收帧数越来越少甚至程序卡顿。简单做法是先把每帧组装成字符串放到日志队列或者只统计计数定时打印。我习惯用一个全局计数器收到一帧就加一每秒打一次总量这样日志不影响接收性能。如果确实需要存每帧详细数据用csv模块或sqlite批量写入都比print稳定得多。6. 常见问题与排查技巧实录6.1 打开设备失败怎么排查ZCAN_OpenDevice返回0或负数大多数情况是设备类型常量不对。先确认你手头的设备型号对应宏是多少不同型号差别很大。其次确认驱动是否安装、设备是否被ZCANPRO占用。ZCANPRO打开设备后会独占Python这边再去打开就会失败。最后检查设备索引多台设备插在同一台电脑时DEV_INDEX要对应你想用的那一台。把USB线从扩展坞换到主机高速USB口也值得一试。供电不足会导致设备枚举成功但通信不稳症状比打不开更隐蔽。我排查过一台USBCAN设备在扩展坞上偶发掉线换上主机直连USB口后连续跑了两天都没问题。6.2 接收不到数据先别怀疑代码回环模式能收到但接真总线收不到按顺序查波特率、终端电阻、CAN_H/CAN_L是否接反、滤波是否误开了。很多新手把acc_mask设成0等于只接收acc_code那一组帧其他全丢弃表现就是程序“偶尔能收到几帧但大量丢帧”。再一个容易被忽略的问题是地电位。USB分析仪和被测设备如果来自不同电源两个地之间存在电位差。CAN收发器允许一定范围的共模电压超过范围会产生错误帧极端情况下直接进入bus-off。6.3 CAN地偏移测试三个步骤和注意事项地偏移问题是总线通信不稳定但示波器又很难抓到的典型原因。我自己做过现场排查方法很简单三个步骤第一步用万用表交流电压挡测设备地线通常是DB9外壳或USB地与被测节点地之间的电压正常应该接近0V如果超过1V就要留意。第二步测CAN_H对地电压和CAN_L对地电压。总线空闲时两者应该都在2.5V附近差值接近0如果一端偏高另一端偏低说明偏置不在收发器典型值上要查收发器电路或共模电压。第三步用示波器两个通道分别接CAN_H和CAN_L用数学通道做A-B差分观察显性位差值和共模电压。只要CAN_H和CAN_L的整体基线在收发器允许范围内正常通信问题不大。注意事项测量时不要同时将探头地直接接在发动机或大功率设备外壳上容易引入地环路。不要在总线通信时直接拔插设备最好先让总线空闲。总线断电后测终端电阻更容易得到准确读数因为带电时收发器内部阻抗也会参与测量。6.4 Bus-off和丢帧的现场处理Bus-off是CAN控制器检测到大量错误后进入的离线状态。整车应用层经常看到某条CAN线进入bus-off然后节点停止发送报文。应用层处理一般是用计数器统计错误计数例如tRec/tEC或者通过诊断服务读取节点状态。用ZLG设备做测试时如果发现DLL返回的事件里有总线关闭事件或者长时间收不到原本周期发送的报文大概率是节点进入了bus-off。此时不能只靠重启上位机解决要查物理层终端电阻有没有脱落、CAN_H和CAN_L是否短路、地偏移是否过大。丢帧则更多是应用层问题接收等待时间太长、读取不频繁、接收缓冲区太小。ZCAN_Receive一次读取的最大帧数建议不低于64并且要放在while循环里持续读不要等攒够一批才去取。6.5 常见问题速查表现象常见原因排查建议DLL加载失败zlgcan.dll不在搜索路径用绝对路径加载或把DLL目录加入PATH打开设备返回0驱动没装好或设备被占用关掉ZCANPRO检查设备管理器初始化返回错误结构体字段不对或波特率配置错对照头文件检查结构体核对波特率表回环能收真总线收不到滤波配置或总线物理问题检查acc_mask测终端电阻和CAN电平偶发丢帧接收代码太慢或缓冲区太小增大接收数组减少打印操作总线通信中断且节点离线节点bus-off检查物理层、终端电阻、地偏移7. 进阶玩法python-can封装与CANopen7.1 用python-can替换手写ctypes手写ctypes把原理摸清后日常开发我推荐切到python-can。安装pip install python-can然后创建总线对象import can bus can.interface.Bus(interfacezlgcan, channel0, bitrate500000) msg can.Message(arbitration_id0x123, data[1,2,3,4], is_extended_idFalse) bus.send(msg) rx bus.recv(timeout1) print(rx)背后的zlgcan后端把DLL细节封装好了代码可读性高很多。python-can还自带日志记录、总线统计和虚拟总线测试临时造数很方便。唯一要注意的是版本匹配有些版本的python-can对不同DLL版本的适配有差异遇到奇怪异常先升级或降级python-can试试。7.2 CANopen高层协议如果项目用的是CANopen协议栈直接操作报文就会出现大量SDO分段报文解析、心跳超时处理的重复劳动。python-can生态里有一个canopen库可以这样用import canopen network canopen.Network() network.connect(interfacezlgcan, channel0, bitrate250000) node network.add_node(0x01, eds/your_device.eds) node.sdo[0x1010].write(0x01, bload) pdo node.pdo[1] pdo.save()这类高层封装非常适合设备参数配置、周期状态读取代码量少且有协议栈兜底。但它要求总线上节点行为严格符合CANopen规范如果项目用的是私有协议还是回到报文层面处理更灵活。7.3 CAN FD经典CAN验证完再上CAN FD比经典CAN多了BRS和ESI等字段数据长度也突破了8字节ZLG的USBCANFD系列支持CAN FD采集。结构体定义和初始化配置跟经典CAN不完全一样建议先跑通经典CAN收发确认DLL版本、设备型号支持CAN FD再把cfg.canfd设为1并配置数据段波特率。很多DLL版本对CAN FD的支持参数比较敏感遇到报文解析异常先查驱动版本升级记录。我在项目里遇到过一次同一台设备跑经典CAN完全正常切到CAN FD后偶发超时最后升级到官方最新驱动才解决。8. 我最后想分享的一点经验方法都写在上面了最后分享一下我个人的固定顺序先装驱动用ZCANPRO裸测再写最小Python脚本回环模式发一帧收一帧然后切到真实总线用ZCANPRO和Python同时抓包对比最后才接业务逻辑。这个顺序能省掉大量背锅时间——如果ZCANPRO本身收发正常那Python方向的排查就集中在DLL调用、结构体和滤波配置如果ZCANPRO也不正常就回到物理层查终端电阻、CAN电平和地偏移。问题定位永远是从物理层到协议层、从官方工具到自研代码别一上来就怀疑自己的程序也别一上来就怀疑设备。把排查顺序刻在脑子里CAN通信相关的坑大多能在一个小时内解决。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑