
简介面向Python硬件二次开发者的ZLG示例包围绕DTU 200UWGR及CAN-DTU系列无线数据传输单元提供经官方工程师修复后可直接运行的Python二次开发程序。资源定位于物联网与工业控制场景适合需要定制DTU通信功能、扩展协议适配或排查官方示例问题的Python工程师。压缩包内含124个文件以XML数据与配置文件、DLL底层驱动库、INI参数文件为主另附一个Python脚本及编译后的PYC文件整体仅1.86MB体积精简但覆盖核心调用接口XML用于定义通信参数DLL封装各型号设备底层接口INI保存默认运行参数。已有1161人学习下载。通过这套demo开发者可快速理解DTU连接建立、CAN总线数据收发、无线链路状态管理等关键环节避免重复踩坑并能复用其中代码段构建自身业务逻辑从设备初始化到数据上报形成完整开发参考大幅缩短基于ZLG硬件的项目开发周期。1. 这块板卡到底能干什么以及为什么我用Python来做二次开发做嵌入式或者车载总线相关的朋友对ZLG致远电子应该不陌生。它的USBCAN系列、CANalyst-II分析仪以及各类串口服务器、采集模块几乎是实验室和现场调试的常备工具。以前我最早接触ZLG的时候用的还是它自带的厂商上位机软件功能确实全但一旦遇到“要把采集的数据接进自己的业务系统”这种需求厂商软件就完全使不上劲了。这时候就需要做二次开发。传统做法是用C或者C#去调用厂家提供的DLL接口但对于我们这种以测试和数据处理为主、又不常写桌面程序的团队来说C的门槛和开发周期都不太划算。后来我发现ZLG官方其实有Python的demo而且接口封装得比较干净调用逻辑和C接口是一一对应的只是换成了ctypes或者官方提供的Python扩展库来实现。用Python做这件事的好处非常直接写起来快调试方便能直接和数据分析、报表生成、数据库对接这些活无缝衔接。这篇东西适合谁看如果你手里有ZLG的CAN卡或者串口设备想用Python快速把数据读回来、把控制指令发出去但不想从头去啃DLL文档或者你已经在用别的语言做二次开发想看看Python这条路能不能提高效率那这篇文章就是写给你的。我尽量把从环境搭建到实际跑通的细节都交代清楚包括我在现场踩过的坑。2. 环境准备别急着写代码先把依赖理清楚2.1 Python版本和系统架构的选择先说结论我测试用的是Python 3.8到3.10之间的版本64位系统Windows 10/11都跑过Linux环境下用官方提供的Linux库也能跑通。网上有些人说Python 3.10以上会有兼容问题我实测下来并没有碰到大问题但如果你用的是旧版ZLG库建议还是保守一点用Python 3.8。这里有一个很多人容易忽略的点ZLG提供的动态库是有32位和64位之分的你的Python解释器架构必须和动态库架构一致否则加载DLL的时候会直接报“模块找不到”或者“不是有效的Win32应用程序”。我自己就犯过这个错电脑上装了64位Python结果从旧项目里拷了一个32位的DLL折腾了一下午才发现是架构不匹配。安装Python的时候记得把“Add Python to PATH”勾上这个不勾的话后续命令行执行pip会非常痛苦。装完以后建议顺手把pip源换成国内镜像不然下载依赖动不动就超时。2.2 ZLG驱动与动态库文件ZLG的板卡安装好之后一般会带一个驱动光盘或者官网下载的驱动包。驱动装好之后你可以把它自带的库文件找出来。不同系列的设备库文件名不太一样常见的有设备系列库文件名主要用途USBCAN-I/IIControlCAN.dllCAN接口卡标准接口CANalyst-IIControlCAN.dll同样走ControlCAN接口串口设备zlg_serial.dll 或系统串口串口通信以太网设备ZLGCAN.dll网络型CAN设备这里最常用的是ControlCAN.dll几乎所有的CAN分析仪二次开发都是从这一套接口入手的。如果你下载的demo包里有Python示例大概率它也是直接加载这个DLL然后用ctypes去调用里面的导出函数。2.3 demo项目目录和文件清单我拿到的官方Python demo解压之后大概是这个结构zlg_python_demo/ ├── demo/ │ ├── test_can.py │ ├── test_serial.py │ └── common.py ├── lib/ │ ├── ControlCAN.dll │ └── kernel.dll └── README.txt看起来非常简单但实际运行之前你需要确认几件事第一lib目录下的DLL是不是和你板卡型号匹配第二Python能不能找得到这个DLL的路径第三有没有缺失的节点或者依赖比如某些demo会用到numpy或者pandas来做数据处理这就得先pip install。官方README里写的是“安装缺失的节点”其实翻译成人话就是运行前把需要的第三方库装齐。如果你拿到的是一个残缺的demo包里面只有Python代码没有DLL那也正常因为DLL通常在驱动安装目录里比如C:\Program Files\ZLG\...。你需要把DLL路径加入系统的PATH环境变量或者在Python代码里用绝对路径加载。我个人建议是不要改动官方库文件的位置而是把DLL复制到你的项目lib目录下然后代码里通过相对路径去加载。这样换电脑、交给同事跑的时候都不会出问题。3. 核心API解析看懂ControlCAN这几个函数demo基本就懂了一半3.1 接口初始化和设备打开ZLG的CAN接口设计思路是很统一的。不管你是用官方上位机还是自己写代码第一步永远是打开设备第二步是初始化第三步是启动CAN通道。这个顺序不能乱就像你开机得先按电源键不能直接去按显示器开关。Python里通过ctypes加载DLL的方式如下import ctypes import os # 加载DLL注意路径 dll_path os.path.join(os.path.dirname(__file__), lib, ControlCAN.dll) can ctypes.WinDLL(dll_path) # Windows环境用WinDLL加载之后调用打开设备的接口# VCI_OpenDevice(设备类型, 设备索引, 保留参数) # 设备类型比如USBCAN-I对应4USBCAN-II对应4具体按官方头文件定义 device_type 4 device_index 0 reserved 0 result can.VCI_OpenDevice(device_type, device_index, reserved) if result 1: print(设备打开成功) else: print(设备打开失败错误码, result)这里的返回值特别容易让人困惑。C接口里返回1表示成功0表示失败但有些老版本的库在异常情况下返回的是负值比如-1。所以判断条件最好是result 1而不是result ! 0否则可能把错误值当作成功处理。3.2 初始化CAN通道和启动打开设备只是第一步接下来要初始化通道参数比如波特率、工作模式。ZLG的接口里这个参数通常通过一个结构体VCI_INIT_CONFIG传入用Python的ctypes定义它class VCI_INIT_CONFIG(ctypes.Structure): _fields_ [ (AccCode, ctypes.c_uint32), # 验收码 (AccMask, ctypes.c_uint32), # 屏蔽码 (Reserved, ctypes.c_uint32), # 保留 (Filter, ctypes.c_ubyte), # 滤波器方式 (Timing0, ctypes.c_ubyte), # 波特率参数0 (Timing1, ctypes.c_ubyte), # 波特率参数1 (Mode, ctypes.c_ubyte), # 工作模式0正常1只听 ]这个结构体里的很多参数比如验收码、屏蔽码如果你只是做基础收发直接用默认值就行。最容易出问题的是波特率参数因为ZLG用的不是直接填波特率数值而是通过Timing0和Timing1两个字节来确定。这是从CAN控制器底层寄存器映射过来的。比如常用的几个波特率波特率Timing0Timing1125Kbps0x030x1C250Kbps0x010x1C500Kbps0x000x1C1Mbps0x000x14这些数值在官方头文件里都有千万别自己乱改。如果你发现总线那边设备波特率是250K而你这里默认填了500K那结果是收不到任何数据而且不会报错排查起来非常头疼。初始化函数和启动函数的调用顺序config VCI_INIT_CONFIG() config.AccCode 0x00000000 config.AccMask 0xFFFFFFFF config.Filter 0 # 不滤波接收所有帧 config.Timing0 0x01 config.Timing1 0x1C # 250Kbps config.Mode 0 # 正常模式 can.VCI_InitCAN(device_type, device_index, 0, ctypes.byref(config)) # 启动CAN通道0 表示CAN0通道 can.VCI_StartCAN(device_type, device_index, 0)这里有一个小细节如果你用的是双通道设备启动另一个通道时把最后一个参数改成1就行。但初始化的时候每个通道都要单独调用一次VCI_InitCAN。3.3 数据接收轮询和中断式怎么选ZLG的CAN卡接收数据有两种常见方式一种是开一个线程定时去读取缓冲区数据另一种是依赖驱动底层的中断回调在Python里实现起来比较绕。官方demo里通常用的是第一种也就是轮询。接收接口的定义和发送类似但它需要额外定义一个接收帧的结构体class VCI_CAN_OBJ(ctypes.Structure): _fields_ [ (ID, ctypes.c_uint32), # 帧ID (TimeStamp, ctypes.c_uint32), # 时间戳 (TimeFlag, ctypes.c_ubyte), # 是否使用时间戳 (SendType, ctypes.c_ubyte), # 发送类型 (RemoteFlag, ctypes.c_ubyte), # 远程帧标志 (ExternFlag, ctypes.c_ubyte), # 扩展帧标志 (DataLen, ctypes.c_ubyte), # 数据长度 (Data, ctypes.c_ubyte * 8), # 数据 (Reserved, ctypes.c_ubyte * 3), ]读取数据时先创建一个数组用来存放多帧数据然后调用接收函数recv_buff (VCI_CAN_OBJ * 100)() while True: count can.VCI_GetReceiveNum(device_type, device_index, 0) if count 0: actual can.VCI_Receive(device_type, device_index, 0, recv_buff, count, 100) for i in range(actual): frame recv_buff[i] print(fID{hex(frame.ID)}, Data{bytes(frame.Data[:frame.DataLen]).hex()}) time.sleep(0.01)这里注意两个点VCI_GetReceiveNum返回缓冲区里有多少帧VCI_Receive的第三个参数是等待超时时间毫秒如果你传0那就变成了非阻塞模式缓冲区没数据时直接返回0。实际开发时我习惯先用GetReceiveNum查数量再按数量去收这样不会丢帧。3.4 数据发送最简单的报文发送发送一个CAN报文的核心逻辑就是填充一个VCI_CAN_OBJ结构体然后调用发送函数frame VCI_CAN_OBJ() frame.ID 0x123 frame.SendType 0 # 正常发送1表示单次发送 frame.RemoteFlag 0 # 数据帧 frame.ExternFlag 0 # 标准帧 frame.DataLen 8 frame.Data[0] 0x01 frame.Data[1] 0x02 send_result can.VCI_Transmit(device_type, device_index, 0, ctypes.byref(frame), 1) if send_result 1: print(发送成功) else: print(发送失败)发送这里有一个比较容易踩的坑VCI_Transmit的第四个参数是pSend它指向一个VCI_CAN_OBJ结构体数组而第五个参数是发送的帧数。如果你一次只发一帧传1没问题但如果你想一次发多帧不能重复调用Transmit而是要构造一个结构体数组一次传进去。频繁单帧调用会明显拉低发送效率尤其是在高负载测试的时候。4. 实操过程从demo到完整小工具我的一次完整复现记录4.1 先把官方demo跑起来我这次拿到的demo里test_can.py是最核心的示例。它的大致逻辑是打开设备、初始化通道、启动CAN、循环接收数据并打印、然后发送一帧测试报文。我把路径切换到项目目录直接运行python test_can.py结果第一遍运行就报错了提示AttributeError: function VCI_OpenDevice not found。这个报错的意思是DLL加载成功了但里面找不到对应的导出函数。我马上意识到可能是DLL不对于是用一个小工具查看了DLL的导出表发现这个版本的ControlCAN.dll导出的是OpenDevice而不是VCI_OpenDevice。原来ZLG早期版本的库函数命名和后来不一样后面为兼容老客户新版本库才同时保留了两种名称。解决方案很简单把代码里所有VCI_前缀的函数名映射成标准名称。如果你拿到的是老库可以用getattr去做一层名字兼容open_func getattr(can, VCI_OpenDevice, None) if open_func is None: open_func getattr(can, OpenDevice)跑通之后我又遇到了第二个问题设备打开成功初始化也OK但一直收不到总线上别的节点发来的数据。排查过程放到下一节细说这里先继续讲代码层面。4.2 数据解析现场把收到的原始帧变成结构化数据demo只能让你看到数据长什么样但实际项目中你需要把CAN帧里的裸数据解析成真实的物理量。比如某型号的BMS会在ID为0x351的报文里通过字节0和字节1发送电池总电压字节3发送温度这种解析逻辑就必须自己写。我这里做了一个简单的解析函数以字典的方式返回结果def parse_bms_frame(frame): if frame.ID ! 0x351: return None data bytes(frame.Data[:frame.DataLen]) voltage (data[0] | (data[1] 8)) / 10.0 # 单位0.1V temperature data[3] - 40 # 偏移量40 return { voltage: voltage, temperature: temperature, }这种解析脚本写起来很简单但要注意字节序和偏移量每个厂商的定义都不一样一定要对着协议文档来。我就见过有人把高低字节搞反解析出来的电压直接翻了几十倍还在现场找了半天原因。4.3 整个demo的完整代码架构我最终完成的这个小工具结构大概是这样的can_tool/ ├── main.py ├── zlg_driver.py ├── parser.py └── config.yamlzlg_driver.py封装了设备打开、关闭、发送、接收所有底层操作对上层只暴露四个方法open()、close()、send(frame_dict)、receive(timeout)。parser.py处理协议解析。config.yaml存设备类型、波特率、通道号这些配置。这样做的好处是换设备型号或者换波特率的时候只需要改配置文件完全不用动代码。一个小建议接收线程里不要直接做数据处理最好把原始帧塞进队列里由另一个线程去消费。这样可以保证接收速率稳定不会被解析逻辑拖慢。5. 常见问题与排查技巧实录5.1 问题速查表先对着看我在不同电脑、不同设备上跑ZLG Python demo前前后后遇到过不少问题。下面这个表是我整理出来频率最高的几类现象可能原因解决办法导入DLL失败提示找不到模块DLL路径不对或Python位数与DLL位数不一致用绝对路径加载确认Python是32位还是64位函数调用报错function not foundDLL版本太老函数名不同用getattr做名称兼容或者换新版DLL设备打开失败返回0驱动未安装、USB线没插好、设备被占用先检查设备管理器再试重新插拔USBCAN通道初始化失败波特率参数填错对照官方头文件或者上面给的波特率表能打开但收不到数据波特率不匹配、滤波配置不对、总线没有终端电阻检查总线上的波特率确认所有节点的滤波设置发送失败返回-1通道未启动或总线关闭确认调用了VCI_StartCAN检查总线是否短路程序退出后设备仍被占用没调用CloseDevice写代码时用try...finally确保关闭设备5.2 现场排查记录为什么设备打开正常却收不到数据有一次我在台架上调试换了一个新的ZLG设备demo跑起来设备打开、初始化全部正常但就是收不到数据。第一反应是波特率问题查了一遍发现配置没改。然后又检查了总线的终端电阻也正常。最后用厂商上位机软件打开一看发现设备因为之前被别的程序占用过驱动状态异常通道没有彻底释放。解决办法是把设备从USB口拔掉重新插上并且在代码里关闭设备后加一个短暂的time.sleep(0.5)给驱动一点释放时间。从那次以后我在代码里都会加上设备重试机制打开失败之后延时1秒重试三次这在实际环境中非常管用因为USB设备在系统睡眠唤醒或热插拔之后经常需要一点时间重新枚举。5.3 几个别人踩过、你也容易踩的坑第一不要在高频接收的时候用print每一帧。Python的print是同步阻塞的帧率一高缓冲区很快就满了丢帧丢到怀疑人生。我把数据改成批量写入CSV之后测试5000帧/秒完全没压力。第二多线程里调用DLL函数时要小心。如果两个线程同时调用发送函数有可能会发生资源竞争。我建议给发送操作加一个锁或者统一由一个线程负责所有发出操作。第三DLL的加载路径不要写死成C:\Windows\System32也不要依赖系统PATH。最好放在项目目录下用os.path.join(os.path.dirname(__file__), ...)去定位。这样项目拷给别人时DLL自带不会因为每台电脑的环境变量不同而跑不起来。6. 最后的建议与扩展方向从我个人的实际使用来看ZLG的Python二次开发接口并没有想象中那么神秘它本质上就是对一套C接口的ctypes封装。难点不在于接口本身而在于你对自己要解决的应用场景是否有足够清晰的规划。你是要做总线监控还是要做自动化测试或者是要做产线数据采集这三种场景下的代码架构是完全不同的。如果你只是做监控类工具把demo改一改就够了如果是做自动化测试我强烈建议把设备操作封装成一个类再结合pytest写用例这样可以在CI里自动跑总线回归测试。还有一个很实用的扩展方向是配合数据分析。CAN数据录下来之后用pandas做离线分析再配合matplotlib画曲线可以非常快地定位到总线上偶发异常的时间点。这一步很多纯嵌入式工程师不熟悉但正是Python这个生态能带给你的额外价值。最后在使用过程中记得保留好本来的C语言头文件对照着看毕竟Python这一层封装再漂亮最终调用的还是底层那套函数遇到问题翻头文件往往比翻demo代码更有效。本文还有配套的精品资源点击获取