
简介Modbus通信测试工程压缩包面向STM32-F0/F1/F2系列单片机开发者设计内含完整源码、编译中间文件与调试辅助文件适合刚接触MODBUS协议栈移植或需要做工业设备通信联调的嵌入式工程师与学习者。压缩包共含70个文件以C源文件.c/.h、Keil编译中间文件.o/.crf、依赖文件.d与工程配置文件.uvproj/.uv2/.uvopt为主另附可直接烧录的.hex固件以及.map/.axf调试映像总大小仅727KB导入工程即可对照源码阅读。已有163人学习工程按USART、DMA、TIMER、LED、LCD、MODBUS等模块划分能从外设初始化、寄存器读写、串口收发、中断处理到通信超时与校验异常检测理清一条完整的Modbus主从站实现链路。配合hex固件可直接下载观察实际通信效果后续移植到其他STM32型号或扩展功能时也可复用其中的驱动封装与协议处理思路节省重复开发时间。1. 先从“电子-modbusTest.zip”说起一个 Modbus 测试包到底帮你省什么事手头有一包设备要测打开串口助手输入十六进制帧一边盯着返回报文一边掰着手指算 CRC这是很多电子工程师都经历过的场景。尤其是刚接手一个 Modbus 协议设备时寄存器长度、功能码、字节序、超时参数全部压在一个人脑子里换个人就得重新捋一遍。我见过最夸张的情况是A同学用串口调试工具点了一个下午最后发现是设备空闲时自动进入低功耗压根没在 listen。“电子-modbusTest.zip”这类压缩包乍看不起眼但它通常是一个打包好的 Modbus 主站/从站测试脚本集把协议帧构造、返回校验、批量巡检和日志回放都收敛到几个文件里。它解决的正是生产线调试、硬件联调、验收测试这三类场景里最费时间的通信验证问题。适合做硬件测试、嵌入式软件开发、以及设备选型评估的工程师直接用。2. 把 Modbus 测试的基础说到能直接落地协议三个要点和测试工具选型2.1 三个必须搞清的协议要点RTU/TCP、寄存器地址、功能码Modbus 测试包再方便也绕不开协议本身的三个基础概念否则你连脚本里的参数不知道怎么填。第一个是传输模式。RTU 走串口以字节帧为单位1 个起始位、8 个数据位、1 个停止位可选校验TCP 走网络端口默认 502。同一套寄存器读写逻辑在两个模式下的区别只在封装层业务功能码是一样的。你拿到“电子-modbusTest.zip”的时候第一件事就是看它的 config 里写的是mode: rtu还是tcp这决定后面所有串口参数是否生效。第二个是寄存器模型。Modbus 把数据分成四张表线圈可读写位、离散输入只读位、保持寄存器可读写 16 位、输入寄存器只读 16 位。测试包里最常见的操作是读保持寄存器功能码 0x03和写单个保持寄存器功能码 0x06。设备手册一般会给一个地址映射表比如40001是设备温度40002是湿度。注意这里的 40001 是 PLC 习惯叫法报文里真正的协议地址是 0x0000。这个“减 1”的转换是新手最容易翻车的地方。第三个是功能码。读线圈是 0x01读离散输入 0x02读保持寄存器 0x03读输入寄存器 0x04写单个线圈 0x05写单个寄存器 0x06写多个线圈 0x0F写多个寄存器 0x10。测试包的核心逻辑不外乎构建这些帧、发送、解析响应的寄存器值。你只需要知道当你点开某个脚本看到function_code3它就是在读保持寄存器。2.2 测试工具怎么选脚本方案为什么比上位机更省事市面上有很多 Modbus 测试工具比如通用串口调试助手、Modbus Poll、或者专门的 GUI 测试上位机。它们的优点是上手快点几个下拉框就能读取数据。但放到批量场景里就不好用了你要测 20 台设备、每台设备读 30 个寄存器GUI 工具只能一台一台手点遇到某个寄存器越界你还要肉眼判断哪个值不合理更别提把测试记录导出成统一格式的报文日志给现场同事做问题定位。脚本方案“电子-modbusTest.zip”的做法通常是把主站逻辑写成可重复执行的 Python 脚本配合一个 CSV 或 JSON 配置表。这样做有三个好处一是回归测试可重放同一组报文反复执行结果统一输出二是参数集中化修改从站地址、波特率、寄存器列表不需要改代码三是日志标准化脚本自动记录时间戳、原始帧、解析值出了问题可以直接复盘。我一般先看包里有没有regmap.csv或config.json有的话基本就是这个套路。2.3 最小可运行环境Windows 下搭建 Python 测试空间最常见的运行环境是 Windows因为公司生产测试电脑多半是 Windows。先把 Python 装好然后用 pip 安装 Modbus 库和串口库。这里我建议用虚拟环境避免污染系统 Python。python -m venv modbus_env modbus_env\Scripts\activate pip install pymodbus pyserial这段命令创建了一个叫modbus_env的虚拟环境并激活它。然后安装pymodbus和pyserial。pymodbus是 Modbus 协议实现库支持 RTU 和 TCPpyserial是串口通信底层依赖在 Windows 上操作 COM 口全靠它。如果只做 TCP 测试pyserial可以省掉但多数标着“电子”的 Modbus 测试包都涉及 RS485 设备所以装全更省心。需要注意库版本。pymodbus从 3.0 开始 API 变化很大老的 2.x 代码直接跑会报一堆 attribute error。如果“电子-modbusTest.zip”里的代码写的是from pymodbus.client.sync import ModbusTcpClient那多半是基于 2.x写成from pymodbus.client import ModbusTcpClient的是 3.x。我一般会根据源码的 import 语句反推该装哪个版本必要时执行pip install pymodbus2.5.3或pip install pymodbus3.5.2。装错版本不会出红色大错误而是跑到一半崩给你看玄学得很。3. 解压后的工程在讲什么典型目录拆分和配置文件解析3.1 一个 Modbus 测试包的经典文件结构拿到压缩包解压之后不要急着运行先扫一眼目录。一个设计得比较规整的测试包会有这些文件主入口脚本、协议封装模块、从站模拟器、配置文件和寄存器映射表。我看到的“电子-modbusTest.zip”案例基本是这个骨架modbusTest/ ├── main.py # 主入口负责读取配置并调度测试 ├── modbus_master.py # 主站逻辑封装读、写、批量巡检 ├── modbus_slave.py # 从站模拟器没有硬件时模拟设备 ├── config.json # 串口/网络/超时/轮询间隔等参数 ├── regmap.csv # 寄存器列表地址、名称、读写类型、缩放系数 ├── requirements.txt # 第三方依赖 └── README.md # 使用说明和注意事项这个结构对应了一种很常见的测试方法配置驱动。main.py读取config.json拿到通信参数再读取regmap.csv拿到要测的寄存器清单然后通过modbus_master.py执行实际读写。而modbus_slave.py用于反向模拟当你手里没有设备或者设备还没焊好时先用它充当从站验证主站脚本本身没写错。我不建议去改动结构里的模块化设计除非你只是为了快速验证某个寄存器。保持配置与逻辑分离后面加测点会轻松很多。3.2 配置文件里的角色参数串口、从站地址、超时重试背后的意义打开config.json你会看到类似下面内容{ mode: rtu, port: COM5, baudrate: 9600, bytesize: 8, parity: N, stopbits: 1, timeout: 1.0, retries: 3, poll_interval: 0.5, slave_id: 1 }每个字段都对应一个测试行为。mode是rtu还是tcp如果是tcp那port要改成 IP 地址加端口比如port: 192.168.1.30:502。baudrate、bytesize、parity、stopbits是串口通信的物理参数必须和设备一致否则设备收到的是噪声根本不会回帧。timeout是发送一帧后等待响应的最长时间单位秒。这个值不能设得太大否则单个寄存器读失败会拖慢整体巡检也不能太小某些慢速 PLC 或仪表可能在 200ms 之后才回。我一般先按 1.0 秒试稳定了再往下压。retries是单次请求失败后的重试次数。注意重试会引入额外耗时比如超时 1 秒、重试 3 次最坏情况一个寄存器要等 3 秒。poll_interval是连续两轮请求之间的间隔这个参数直接决定测试节奏。slave_id是从站地址范围 1-247如果设备是 0那多半是广播地址不会回响应。基本配置改错任何一个脚本都会表现出“发送正常、但收不到数据”的假象。3.3 依赖清单和安装顺序装错库版本会有什么后果requirements.txt通常长这样pymodbus3.5.2 pyserial3.5安装顺序不太重要重要的是版本锁定。为什么我要强调锁版本因为pymodbus3.x 内部结构在 3.0-3.6 之间也有微调比如某些线程模型和安全校验函数的签名变化。你在 A 机器上pip install pymodbus装了 3.6在 B 机器上装了 3.2同一个脚本可能一边正常一边报TypeError: write_registers() got an unexpected keyword argument slave。我见过最典型的一次翻车是测试包代码里用了client.write_registers(address, values, slave2)这写法的前提是安装的pymodbus版本在这个位置接受slave参数。但新版换成了device_id结果报了TypeError。解决方案不是去改源码里所有的参数名而是直接看 README 里要求的版本号然后按pip install pymodbus3.5.2安装。如果压缩包没有 README就打开requirements.txt上面写什么版本就装什么版本不要装 latest。另外一点在 Windows 上如果装了多个 Python 环境pip可能指向错误的环境。执行pip --version看清路径或者直接用python -m pip install ...避免出现“装了一半项目能用、一半报找不到模块”的尴尬。4. 用代码把测试跑起来主站、从站和批量巡检的三段可抄作业4.1 主站读寄存器的最小 Python 脚本不管包里的代码封装得多花哨底层一定是最简单的主站读写。先把这段最小脚本跑通后面再套用封装就心里有底。下面是一个基于pymodbus3.x 的读保持寄存器示例from pymodbus.client import ModbusSerialClient client ModbusSerialClient( portCOM5, baudrate9600, bytesize8, parityN, stopbits1, timeout1, ) client.connect() # 地址 0 对应手册里的 40001读 2 个寄存器 result client.read_holding_registers(address0, count2, slave1) if not result.isError(): print(寄存器0:, result.registers[0]) print(寄存器1:, result.registers[1]) else: print(读取失败:, result) client.close()逻辑说明ModbusSerialClient负责建立串口连接read_holding_registers发送功能码 0x03。address是协议地址从 0 开始手册上的 40001 相当于地址 0。slave1是从站 ID要和设备侧一致。result.registers返回一个整数列表长度为count。如果设备返回异常码result.isError()为真此时不要直接访问registers否则会得到空列表。参数说明里最坑的是address和slave。很多设备手册给的地址表是 PLC 风格的 40001、40002容易让人直接填 40001 进去结果实际访问的是协议地址 40001设备会报非法地址。正确做法是手册地址减 1。另外在pymodbus3.x 中unit参数已经改成slave如果从 2.x 迁移代码这里极易踩坑。4.2 从站模拟器没有硬件也能验证通信链路当硬件还没到位时可以先启动一个从站模拟器用主站脚本去读它验证整条链路是否通。很多“electron”项目的 Modbus 测试包都自带modbus_slave.py原理是起一个线程监听串口或 TCP 端口按功能码返回预置数据。下面是用pymodbus自带的StartSerialServer实现的从站模拟from pymodbus.server import StartSerialServer from pymodbus.datastore import ModbusSequentialDataBlock, ModbusSlaveContext, ModbusServerContext # 初始化数据存储区地址 0-9 的保持寄存器预置为 100-109 store ModbusSlaveContext( diModbusSequentialDataBlock(0, [0]*10), coModbusSequentialDataBlock(0, [0]*10), hrModbusSequentialDataBlock(0, list(range(100, 110))), irModbusSequentialDataBlock(0, [0]*10), ) context ModbusServerContext(slavesstore, singleTrue) StartSerialServer( context, portCOM5, baudrate9600, parityN, stopbits1, bytesize8, )这段代码要点是ModbusSequentialDataBlock创建连续地址的数据块这里给保持寄存器hr地址 0-9 填了 100 到 109主站读地址 0 会拿到 100地址 1 会拿到 101。ModbusSlaveContext管理四张数据表singleTrue表示所有从站 ID 共享同一份数据测试单设备时序足够用了。启动后主站脚本里的串口参数要和这段保持一致尤其是 COM 口不能冲突。我建议在正式连硬件之前先用这个模拟器跑一遍主站脚本。一方面确认线序、驱动是否正常另一方面把超时参数校准到一个稳定值。等真实设备连上后如果报文格式有差异再单独排查。4.3 批量巡检用一张表驱动几十个寄存器测试真正的效率提升来自批量。regmap.csv的存在就是为了把测点从代码里剥离出来。一个典型表格如下address,name,type,scale,min_value,max_value 0,temperature,holding,0.1,-40,80 1,humidity,holding,0.1,0,100 2,status,input,1,0,255 3,firmware_version,holding,1,0,65535批量巡检脚本读取这张表循环执行读取并且可以根据min_value/max_value做简单范围判断。下面是一段可运行的示例import csv from pymodbus.client import ModbusSerialClient client ModbusSerialClient(portCOM5, baudrate9600, timeout1) client.connect() with open(regmap.csv, newline) as f: reader csv.DictReader(f) for row in reader: addr int(row[address]) name row[name] result client.read_holding_registers(addressaddr, count1, slave1) if result.isError(): print(f[FAIL] {name}: 无响应) continue raw result.registers[0] scaled raw * float(row[scale]) print(f[OK] {name}: raw{raw} scaled{scaled}) client.close()逻辑说明脚本逐行读取 CSV把name和scale都映射到读取结果上。scale是把原始整数换算成物理量的系数比如温度寄存器原始值 250乘以 0.1 就是 25.0 度。这个做法比在代码里手写每个寄存器地址要直观得多也方便现场测试人员直接改 CSV 增删测点。参数说明csv.DictReader默认以第一行作为字段名所以 CSV 里不要加多余表头。read_holding_registers的count这里固定为 1但如果寄存器表是按连续块组织的可以改成多个避免逐地址请求拖慢巡检。对于输入寄存器要改成read_input_registers功能码 0x04别弄混。批量巡检最大的价值不是省那几个“写代码”的时间而是把测试记录变成可查的数据。你可以把每条结果追加到日志文件或者输出成 JSON后面做自动化回归就顺理成章了。5. Modbus 测试避坑与排查几个典型现场问题的现象、原因和解决5.1 设备无响应但串口线是通的地址和波特率之争现象主站脚本发送帧后一直超时报No response。用示波器看串口 TX 端有波形设备也上电了但就是不回。原因最常见的是从站地址和波特率不匹配。某些设备出厂默认地址是 247 或 2不是 1有些设备 RS485 接口上电后要等 1-2 秒才进入通信状态。还有一个隐蔽原因是校验位不匹配比如设备设置为偶校验 Even而配置里写 None结果设备把主站发送来的帧视为无效。解决先用一个简单的串口回环测试排除物理层问题。把 RS485 转换器的 A/B 短接发一串数据自己收能收到说明串口和线材没问题。然后逐个试从站地址列表[1, 2, 247]同时试parity的N/E/O三档。我习惯在config.json里把这三个配置做成可快速切换的参数用脚本循环扫描地址而不是手改代码。5.2 读到一堆 0 或数值明显不对寄存器大小端和功能码不匹配现象请求发送成功也有响应帧但解析出来的寄存器值是 0或者数值和手册对不上比如温度显示 2560 但实际只有 25 度。原因一个是大小端问题。Modbus 寄存器是 16 位但设备可能将一个 32 位浮点数拆成两个寄存器且字节序是先高后低还是先低后高不同厂家标准不一样。另一个原因是寄存器地址偏移。你读保持寄存器读到的是别的信息而真正的数据在输入寄存器0x04里。解决先用模拟器对着手册构造一个已知值检查pymodbus返回的registers列表顺序。如果两个寄存器的值需要拼成一个 32 位整数可以试验两种拼接顺序。下面是一段拼接示例raw_low result.registers[0] raw_high result.registers[1] value_big_endian (raw_high 16) | raw_low value_little_endian (raw_low 16) | raw_high逻辑说明寄存器列表的顺序映射为地址递增。如果你的设备手册里说“地址 0 是低 16 位地址 1 是高 16 位”那value_big_endian是正确解。反之则是value_little_endian。至于功能码打开设备的手册看数据是放在“保持寄存器”还是“输入寄存器”里然后把read_holding_registers换成read_input_registers。5.3 频率一高就丢帧轮询间隔和超时设置现象单次读取都成功但把poll_interval调到 100ms 之后开始出现偶发超时甚至设备直接掉线。原因设备侧 MCU 处理 Modbus 请求需要时间特别是 EEPROM 操作或滤波计算不是每个主站请求都能立刻响应。频繁请求会导致设备来不及处理或者设备侧有一个基础的通信错误恢复机制触发后会自动忽略后续请求几秒钟。解决把poll_interval调回 500ms 以上同时把timeout放宽到 1 秒。我一般先用二分法压poll_interval从 1000ms 往下试能稳定跑 100 次不掉帧的时间间隔才是可用值。另外一个容易忽略的点是retries重试机制会和轮询间隔叠加如果一轮请求超时 1 秒重试 3 次那么实际上已经占用了 4 秒后面的请求全部堆积。这种情况下表面看是丢帧其实是排队阻塞。5.4 报文能发出去但从站模拟器收不到半双工和串口复用问题现象同一个串口主站脚本和从站模拟器同时跑结果两边都超时仿佛谁也没收到谁的帧。原因RS485 是半双工通信同一时刻只能有一个发送方。如果你在电脑上同时打开两个串口工具或者主站脚本和从站模拟器抢同一个 COM 口那必然冲突。另一个常见场景是 USB 转 RS485 模块的自动收发切换电路没拉起来导致发送结束后仍占着总线。解决确认物理层只有一个发送源。测试时先用从站模拟器和主站脚本各自单独占用 COM 口不要同时启动两个实例。如果从站模拟器要用 COM5主站脚本就必须用 COM6通过两个 USB 转 RS485 适配器连同一对 A/B 线。要是只有一条 USB 转 485那就把主站和从站改成 TCP 模式走本机回环地址绕开串口半双工限制。6. 把测试做得更值钱回放异常帧和自动化回归的进阶技巧跑通基础读写、批量巡检只说明你会用这个工具包。真正让测试有价值的部分是把一次偶然的故障变成可反复验证的回归用例。我的做法是在测试包里加一个“异常帧回放”模块当现场出现某帧导致设备死机或返回异常码时先用带原始帧日志的主站脚本记录下完整的请求和响应然后保存成一个 JSON 文件。回归测试时用pymodbus的底层帧构造接口重新发送这组帧断言返回码是否与预期一致。from pymodbus.pdu import ModbusRequest from pymodbus.client import ModbusSerialClient client ModbusSerialClient(portCOM5, baudrate9600, timeout1) client.connect() # 记录下来的异常请求帧这里以读保持寄存器地址 0xFF 为例 request ModbusRequest(function_code0x03, address0xFF, count1) response client.execute(request, slave1) print(原始响应码:, response.function_code) client.close()逻辑说明ModbusRequest可以构造任意功能码和地址的组合client.execute直接执行这个请求不经过高级封装。这样就能还原那些在高级接口里被校准得很正常、但真实设备却不接受的边界请求。比如地址越界、寄存器和功能码错配、超过协议最大长度等这些用例才是回归测试的护城河。进阶技巧是把整个测试过程做成一个带时间戳的回归脚本每次改动固件后跑一遍 baseline对比新旧日志里的错误码分布。我习惯把config.json里的slave_id和timeout做成命令行参数这样同一条回归脚本可以在多个测试台之间切换不用改代码。说到底Modbus 测试包的核心不是“能读几个寄存器”而是“能不能在你离开现场之后仍然复现和定位问题”。我从很多次踩坑里学到的教训就是所有现场偶发故障都要先把原始帧存成文件再做任何重试或换线。这样就算这次没查明白下一回测试还能拿着同一帧继续复现。希望这篇笔记能帮你在做 Modbus 调试时少走几次弯路也祝你的测试台永远不会出现“刚才明明还好好的”这句话。本文还有配套的精品资源点击获取