恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

Linux串口通信实战:Modbus RTU协议解析与Python轮询实现

  • 首页
  • 资讯中心
  • /
  • Linux串口通信实战:Modbus RTU协议解析与Python轮询实现

相关资讯

OpenClaw大模型实战手册:从文档自动化到知识库搭建的落地指南 2026/10/16 3:02:19
Shield Optimizer 完全指南:免费为 Android TV 一键甩掉 89 个臃肿预装应用 2026/10/16 3:02:19
语音交互 2 - 语音前端处理 2026/10/16 3:02:19

最新资讯

多模融合时序数据库实战:从数据孤岛到统一SQL引擎的国产化之路
CycleGAN斑马变马:非配对图像翻译的结构主体代码分析
MySQL索引失效排查与治理:从EXPLAIN到慢查询优化实战
软考软件设计师通关指南:高频考点与下午题套路全解析
双目ORB-SLAM2在Euroc数据集上的配置与调试实战
三菱PLC上位机通信实战:Mx Component软元件读取避坑指南

今日推荐

Java八种基本类型深度解析:从内存布局到实战选型
基础:常用快捷键——IDEA、VS Code、PyCharm 与 Linux 终端效率速查
解决ultraedit下verilog .v文件中文注释乱码:把编码配置改到TaoToken辅助排查

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

Linux串口通信实战:Modbus RTU协议解析与Python轮询实现

发布时间:2026/10/16 3:07:20
Linux串口通信实战:Modbus RTU协议解析与Python轮询实现 1. 从一个真实场景说起为什么要在嵌入式设备上跑 Modbus RTU如果你手头有一块树莓派或者类似的 Linux 开发板又恰好需要和工业现场的 PLC、电表、温控器、变频器打交道那你大概率绕不开Modbus RTU这个协议。它诞生于上世纪七十年代末至今仍然是工业自动化领域最通用的串行通信协议之一。原因很简单协议本身足够轻量帧结构清晰对硬件资源要求极低而且几乎所有主流工控设备都支持它。我最初接触这个组合是因为一个模拟项目需要让一块运行 Linux 的开发板作为主站轮询三台从站设备——一台温湿度变送器、一台单相电表、一台简单的继电器模块。需求听起来不复杂但真正动手之后才发现从串口参数配置、帧间隔控制到 CRC 校验、异常码处理每一个环节都有坑。市面上的教程要么只讲协议理论要么只给一段跑不通的代码很少有人把在 Linux 开发板上用 Python 跑 Modbus RTU 主站这件事从头到尾讲透。这篇文章就是把我自己踩过的坑、验证过的方案、以及实际跑通之后的经验完整地梳理出来。内容会覆盖Modbus RTU 的帧结构到底长什么样、Linux 下串口怎么配置才稳、Python 用哪个库最省心、轮询逻辑怎么写才不会丢包、遇到超时和异常码怎么排查。无论你是刚接触工业通信的新手还是已经用过 Modbus 但想换到 Linux 平台的老手应该都能从里面找到有用的东西。提示本文所有代码和配置均基于通用 Linux 环境不涉及任何特定厂商的私有协议或工具。示例中的设备地址、寄存器地址均为虚构实际使用时请查阅你手头设备的通信手册。2. Modbus RTU 帧结构拆解别急着写代码先把这 256 字节搞清楚2.1 一帧数据里到底装了什么Modbus RTU 的一帧数据本质上就是一串字节。它的结构非常紧凑没有多余的握手和包头具体格式如下字段长度说明从站地址1 字节范围 1~2470 为广播地址功能码1 字节决定操作类型如 0x03 读保持寄存器数据域N 字节具体参数长度由功能码决定CRC 校验2 字节低字节在前高字节在后以最常用的读保持寄存器功能码 0x03为例主站发送的请求帧格式是从站地址 0x03 起始寄存器地址高字节 起始寄存器地址低字节 寄存器数量高字节 寄存器数量低字节 CRC 低字节 CRC 高字节。总共 8 个字节。从站正常响应时返回从站地址 0x03 字节数 数据高字节 数据低字节 ... CRC 低字节 CRC 高字节。这里有一个非常容易搞错的地方Modbus 协议中所有多字节数值都是大端序高字节在前但 CRC 校验值却是小端序低字节在前。我见过不少人在拼接请求帧时把寄存器地址写成了小端结果从站要么不响应要么返回异常码 0x02非法数据地址。这个细节在协议文档里往往一笔带过但实际调试时能卡你半天。2.2 帧间隔RTU 模式的隐形时钟Modbus RTU 和 Modbus ASCII 最大的区别在于RTU 模式没有明确的起始和结束字符它依靠时间间隔来划分帧边界。协议规定帧内字符之间的间隔不能超过 1.5 个字符时间帧与帧之间的间隔至少为 3.5 个字符时间。所谓一个字符时间取决于波特率。以 9600 bps、8 数据位、无校验、1 停止位为例一个字符共 10 位1 起始位 8 数据位 1 停止位所以一个字符时间 10 / 9600 ≈ 1.04 毫秒。3.5 个字符时间就是大约 3.65 毫秒。在裸机单片机时代这个间隔通常由定时器硬件来保证。但在 Linux 这种非实时操作系统上串口读写是通过内核驱动和用户态程序协作完成的调度延迟可能达到几毫秒甚至几十毫秒。如果你在用户态手动控制帧间隔很容易因为系统调度抖动导致间隔不稳定进而出现从站不响应或响应被截断的问题。我的做法是尽量依赖串口驱动本身的帧间隔处理而不是在应用层用 sleep 去凑时间。Linux 的串口子系统特别是termios框架在设置好波特率后驱动会按照波特率自动处理字符间的时序。对于帧间间隔可以在发送完一帧后等待从站响应之前留出足够的超时时间通常 100~500 毫秒而不是精确地去卡 3.5 个字符时间。因为从站设备本身的响应时间往往远大于 3.5 个字符时间精确控制帧间隔在 Linux 上既不现实也没必要。2.3 CRC 校验手算一遍就再也不会忘CRC-16/Modbus 的算法并不复杂但第一次接触的人容易被各种参数搞晕。它的关键参数是多项式 0xA001这是 0x8005 的反转表示初始值 0xFFFF输入和输出都不反转结果异或 0x0000。下面是一段可以直接用的 Python 实现def crc16_modbus(data: bytes) - bytes: crc 0xFFFF for byte in data: crc ^ byte for _ in range(8): if crc 0x0001: crc (crc 1) ^ 0xA001 else: crc 1 # 返回低字节在前 return bytes([crc 0xFF, (crc 8) 0xFF])这段代码我用了很多年在各种设备上验证过没有出过错。需要注意的是计算 CRC 时只对从站地址 功能码 数据域进行计算不包含 CRC 本身。收到响应后也要先校验 CRC再解析数据。如果 CRC 不匹配直接丢弃这一帧不要尝试去修复它。注意有些设备手册会写CRC 高字节在前这通常是指计算结果的数值表示而不是传输顺序。实际传输时Modbus RTU 规定 CRC 低字节先发。如果你发现从站不响应可以试着把 CRC 两个字节对调一下再发但更可能的问题是寄存器地址或功能码不对。3. Linux 串口配置为什么你的 /dev/ttyUSB0 总是读不到数据3.1 串口设备节点的识别与权限在 Linux 上USB 转串口设备通常会被识别为/dev/ttyUSB0、/dev/ttyUSB1这样的节点而板载串口可能是/dev/ttyS0、/dev/ttyAMA0等。插上设备后先用dmesg | grep tty看一下内核有没有正确识别。如果看到类似ch341-uart converter now attached to ttyUSB0的输出说明驱动加载正常。接下来是权限问题。默认情况下/dev/ttyUSB0的属主是root普通用户没有读写权限。你可以用sudo运行程序但更好的做法是把当前用户加入dialout组sudo usermod -aG dialout $USER然后重新登录使组权限生效。这样以后就不需要每次都sudo了。我见过不少人因为权限问题折腾半天以为是代码写错了其实只是串口打不开。3.2 termios 参数设置八个参数一个都不能少Linux 下配置串口核心就是填充termios结构体。虽然 Python 的pyserial库已经帮你封装好了但理解底层参数仍然很重要因为一旦通信异常你需要知道去检查什么。关键参数包括波特率必须和从站设备完全一致。常见的 9600、19200、38400、115200 都要支持。数据位Modbus RTU 通常用 8 位。校验位可以是无校验N、偶校验E、奇校验O。很多设备默认无校验但工业现场为了抗干扰有时会用偶校验。停止位通常 1 位少数设备用 2 位。流控Modbus RTU 一般不用硬件流控设为无流控。用pyserial打开串口的典型代码如下import serial ser serial.Serial( port/dev/ttyUSB0, baudrate9600, bytesizeserial.EIGHTBITS, parityserial.PARITY_NONE, stopbitsserial.STOPBITS_ONE, timeout0.5, # 读超时 0.5 秒 write_timeout0.5 # 写超时 0.5 秒 )这里timeout参数非常关键。它决定了read()调用在没收到数据时最多阻塞多久。如果设得太短可能从站还没响应就超时了设得太长轮询周期会被拖慢。我的经验是对于 9600 波特率、响应较慢的设备设 0.5 秒比较稳妥对于 115200 波特率、响应快的设备0.1 秒就够了。3.3 一个容易被忽略的坑串口被占用有时候你明明代码没问题但就是打不开串口报Permission denied或Device or resource busy。这通常是因为有另一个进程占用了串口。可能是之前调试时开的minicom没关也可能是某个后台服务在轮询。用lsof /dev/ttyUSB0可以查看哪个进程占用了它。另外某些开发板上的蓝牙服务会占用板载串口如果你用的是/dev/ttyAMA0可能需要先禁用蓝牙对它的占用。还有一个隐蔽的问题USB 转串口线拔插后设备节点号可能会变。比如原来是ttyUSB0重新插拔后变成了ttyUSB1。如果你的程序硬编码了ttyUSB0就会找不到设备。解决办法是使用 udev 规则创建固定名称的符号链接或者通过设备的序列号来识别。对于快速验证可以先ls /dev/ttyUSB*确认当前节点。4. Python 实现 Modbus RTU 主站从零封装一个够用的轮询器4.1 库的选择pymodbus 还是自己撸Python 生态里做 Modbus 通信最常用的是pymodbus库。它支持 RTU 和 TCP功能码覆盖全面文档也算清楚。但我在实际项目中发现pymodbus的版本迭代比较快不同版本之间的 API 有差异有时候升级后老代码就跑不通了。而且对于简单的轮询场景它的抽象层反而增加了调试难度。所以我的选择是对于功能简单的项目直接用pyserial自己封装请求和响应解析。这样每一行代码都在自己掌控之中出了问题也容易定位。下面我就以自己封装的ModbusRTUClient类为例讲一下核心逻辑。4.2 请求帧的构造与发送构造请求帧的核心就是按照前面讲的帧结构拼接字节然后计算 CRC。以读保持寄存器为例def build_read_holding_registers(self, slave_addr, start_addr, count): frame bytearray() frame.append(slave_addr) frame.append(0x03) frame.append((start_addr 8) 0xFF) frame.append(start_addr 0xFF) frame.append((count 8) 0xFF) frame.append(count 0xFF) crc crc16_modbus(frame) frame.extend(crc) return bytes(frame)发送之前最好先清空接收缓冲区避免上一轮的残留数据干扰。pyserial提供了reset_input_buffer()和reset_output_buffer()方法。发送时用ser.write(frame)然后等待响应。4.3 响应解析与异常码处理从站的响应有两种正常响应和异常响应。正常响应的功能码与请求相同异常响应的功能码是请求功能码加上 0x80。比如请求 0x03异常响应就是 0x83后面跟一个字节的异常码。常见的异常码有异常码含义可能原因0x01非法功能码从站不支持该功能0x02非法数据地址寄存器地址不存在0x03非法数据值数据域中的值超出范围0x04从站设备故障设备内部错误0x05确认从站已接受请求正在处理0x06从站设备忙设备暂时无法处理解析响应时先检查长度是否足够再校验 CRC然后判断功能码。如果功能码的最高位是 1说明是异常响应读取异常码并抛出对应的异常信息。我在代码里会把这些异常码映射成可读的文字方便日志排查。4.4 轮询逻辑顺序、间隔与重试轮询多个从站时最忌讳的是发完一个请求立刻发下一个。因为从站处理请求需要时间而且总线上只能有一个主站发送数据。正确的做法是发送请求后等待响应收到响应或超时后再等待一小段时间比如 50 毫秒然后发送下一个请求。对于超时的请求可以重试 1~2 次。如果连续重试都失败记录错误并跳过该从站继续轮询下一个。不要让一个故障从站阻塞整个轮询循环。下面是一个简化的轮询框架import time def poll_devices(client, devices): results {} for dev in devices: for attempt in range(3): try: data client.read_holding_registers( dev[addr], dev[start], dev[count] ) results[dev[name]] data break except ModbusTimeoutError: if attempt 2: results[dev[name]] None print(f设备 {dev[name]} 超时跳过) time.sleep(0.05) except ModbusException as e: results[dev[name]] None print(f设备 {dev[name]} 异常: {e}) break time.sleep(0.05) # 帧间间隔 return results这个框架虽然简单但已经能覆盖大部分中小型项目的需求。关键点在于每个设备独立处理异常重试次数有限帧间有固定间隔。5. 调试实战从完全没反应到稳定轮询的排查链路5.1 第一步确认物理层和串口参数当通信完全没反应时不要急着改代码。先确认三件事接线对不对、串口参数对不对、设备地址对不对。Modbus RTU 通常用两线制A 接 AB 接 B有些设备标注为 D 和 D-对应关系要查手册。如果接反了数据收发会完全乱掉。串口参数方面波特率、数据位、校验位、停止位必须和从站完全一致差一个都不行。我习惯用minicom或picocom先手动发一帧数据看看。比如用minicom打开串口按CtrlA然后O进入配置设置好波特率等参数然后直接输入十六进制字节。如果从站有响应说明物理层和参数没问题问题在代码如果没响应就要继续查硬件和参数。5.2 第二步用示波器或逻辑分析仪看波形如果手头有逻辑分析仪抓一下 A/B 线上的差分信号能非常直观地看到主站有没有发出数据、从站有没有响应。我遇到过一种情况主站发送的数据完全正确但从站就是不响应后来用逻辑分析仪一看发现从站的响应信号幅度非常弱几乎被噪声淹没。原因是总线末端没有加终端电阻信号反射导致通信失败。加上 120 欧姆终端电阻后通信立刻稳定了。5.3 第三步检查代码中的字节序和寄存器地址如果物理层没问题但代码总是收到异常码 0x02那多半是寄存器地址不对。Modbus 协议中的寄存器地址有两种表示方式协议地址和设备地址。协议地址从 0 开始设备地址通常从 1 开始而且不同设备手册的写法不一样。有的手册写保持寄存器 40001这里的 40001 是设备地址对应的协议地址是 0。有的手册直接写寄存器地址 0x0000那就是协议地址。搞混了就会访问到错误的寄存器。另外有些设备的数据是 32 位浮点数占用两个连续的保持寄存器。读取时数量要设为 2解析时要把两个 16 位寄存器拼成一个 32 位值再按 IEEE 754 标准转成浮点数。字节序可能是高字在前也可能是低字在前需要根据设备手册确认。5.4 第四步处理电磁干扰和接地问题工业现场的电磁环境往往比较恶劣变频器、接触器、电机启停都会产生干扰。如果通信时好时坏偶尔出现 CRC 错误或超时可以考虑以下措施使用屏蔽双绞线屏蔽层单端接地缩短通信线长度降低波特率在总线两端加终端电阻把通信线和动力线分开走线避免平行敷设。这些措施看起来是硬件问题但实际项目中通信不稳定的根源有一大半都在这里。6. 性能优化与长期运行稳定性让轮询器连续跑几个月不出问题6.1 轮询周期的合理设定轮询周期不是越短越好。周期太短从站可能来不及响应导致超时和重试反而降低效率。周期太长数据刷新不及时影响监控效果。我的经验是先测量单个设备的平均响应时间然后乘以设备数量再加上足够的余量。比如三个设备每个响应时间约 50 毫秒那么轮询周期至少设为 300 毫秒比较稳妥。如果对实时性要求高可以提高波特率到 115200这样单帧传输时间会大幅缩短。6.2 日志与异常记录长期运行的程序必须有日志。但日志不能太多否则磁盘很快被写满也不能太少否则出了问题无从查起。我的做法是正常轮询不记录只在超时、CRC 错误、异常码出现时记录一条日志包含时间戳、设备地址、错误类型。另外每隔一段时间比如一小时记录一次统计信息包括总请求数、成功数、失败数、平均响应时间。这样既能掌握运行状况又不会产生大量日志。6.3 串口异常恢复有时候串口会因为硬件干扰或驱动问题进入异常状态表现为read()一直返回空或抛出SerialException。这时候需要关闭串口再重新打开。我通常会在捕获到SerialException后先ser.close()等待 1 秒再重新serial.Serial(...)打开。如果连续多次打开失败就记录错误并等待更长时间再试。这个恢复机制能让程序在无人值守的情况下自动从偶发故障中恢复。6.4 资源占用与看门狗在资源有限的开发板上Python 程序的内存占用和 CPU 占用也需要关注。轮询间隔中适当sleep避免忙等待。如果开发板支持硬件看门狗可以启用它防止程序卡死。另外定期检查串口文件描述符是否泄漏确保每次异常后都正确关闭了串口。提示如果你的项目需要 7x24 小时运行建议用systemd把程序注册成服务设置自动重启。这样即使程序崩溃也能在几秒内恢复。7. 几个实际项目中总结出来的小技巧第一个技巧是关于超时时间的动态调整。不同从站的响应速度差异很大统一设一个超时时间往往不合适。我的做法是第一次轮询时给每个设备设一个较长的超时比如 1 秒记录下实际响应时间然后后续轮询把超时设为实际响应时间的 2~3 倍。这样既能保证不误判超时又能让轮询更紧凑。第二个技巧是关于批量读取。如果多个连续的寄存器都需要读取尽量用一次请求读完而不是分多次读。比如要读地址 0 到地址 9 的 10 个寄存器一次请求读 10 个比读 10 次每次读 1 个效率高得多。但要注意有些设备对单次读取的寄存器数量有限制比如最多 125 个超过会返回异常。第三个技巧是关于数据缓存。如果某个设备偶尔超时不要立刻把它的数据标记为无效。可以保留上一次的有效数据并标注数据可能过期。这样上层监控界面不会因为一次偶发超时就出现数据跳变。第四个技巧是关于串口参数的运行时修改。有些项目需要在运行过程中切换波特率比如先以 9600 连接再切换到 115200。这时候不能直接改ser.baudrate正确做法是关闭串口重新以新参数打开。因为termios的某些参数在串口打开后修改可能不生效。这些技巧看起来都是小细节但在实际项目中正是这些细节决定了程序是能跑还是跑得稳。我在多个模拟项目中反复验证过这些做法最长的一个连续运行了三个多月没有出现需要人工干预的故障。希望这些经验能帮你少走一些弯路。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号