LuatOS 课程-008 讲:Modbus RTU 通信模式
作者:马梦阳 | 最后修改:2026-01-07
Hello,大家好,我是马梦阳。
欢迎大家来到合宙 LuatOS 直播课堂,一起学习 LuatOS 课程。
第一部分:LuatOS 课程背景
因为今天是我们 LuatOS 系列课程的第 008 讲,同时也是 LuatOS Modbus 专题课程的第一讲,所以在这里我就不重复讲解整个 LuatOS 课程的背景了;
如果您还不清楚 LuatOS 课程背景,可以访问:LuatOS 课程背景 这个链接,进行了解;
第二部分:LuatOS Modbus 课程讲哪些内容
今天是 LuatOS Modbus 的第一讲:Modbus RTU 通信模式
LuatOS Modbus 第一讲课程主要包含以下几个部分:
1、Modbus 总体介绍;
2、Modbus 通信机制与协议规范;
3、Modbus 数据模型;
4、Modbus 功能码;
5、Modbus 通信协议模式;
6、LuatOS exmodbus 扩展库介绍(重点讲解 Modbus RTU 相关 API);
7、LuatOS 上的 Modbus RTU 应用开发流程;
第三部分:Modbus 总体介绍
3.1 Modbus 的诞生背景
在 1979 年,那时候的中国还处于改革开放初期,而工业界正面临一个“大麻烦”。
当时的 PLC 厂商,例如 艾伦-布拉德利(Allen-Bradley,AB)、西门子、通用电气(General Electric,GE)......每个公司都像是独立的“小王国”,自己搞一套通信协议,谁也不跟谁兼容!
这就导致了一个什么结果?
- 你买了 A 家的 PLC,想用 B 家的上位机软件去监控?不行!
- 想把传感器、仪表传到 PLC 上?不好意思,得找厂家专门开发驱动,又贵又慢!
- 整个系统就像一张拼图,每块都是不同品牌,根本拼不到一起!
这叫啥?江湖人称——“七国八制”!
并不是说真有七个国家,是说市面上协议多得像战国七雄,各搞各的,谁也管不了谁!
这种局面,不仅让用户头疼,也让整个行业的发展卡住了脖子!
于是,一家叫 Modicon 的公司(现在属于施耐德电气 Schneider Electric)站了出来,他们的工程师 John D.(这位大佬名字有点模糊,但贡献巨大!)带着团队,憋出了一个神协议——Modbus!
他们设计的时候,便定了三条“铁律”:
第一,简单到爆!
协议结构清晰,功能码就那么几个,工程师看一天就能上手,调试起来不头秃!
第二,完全开源免费!
这是最关键的一步!Modicon 把协议规范直接公开,谁都能拿去用,不用交钱,不用签合同!
——这在当时简直是“革命性”的操作!别的厂商都在藏着掖着,它直接摊开给你看!
第三,通用性强!
采用“主从架构”——一个主控电脑,管理一堆设备(PLC、传感器、仪表)。
所以,为什么 Modbus 能活到现在,40 多年还被广泛使用?
就是因为这三条“铁律”!
不炫技、不复杂,但解决了最实际的问题——让设备之间能“说上话”!
3.2 Modbus 的发展历史
很多人以为 Modbus 发明完就火了——其实根本不是! 它最初只是 Modicon 自家 PLC 的“内部通讯工具”,连名字都没几个人知道 至于它是怎么从“小众协议”变成“工业普通话”的? 我们接着往下看。
发展历史我将其分成了三个阶段:
阶段一:1979 – 1990s 中期 — 串口时代,野蛮生长
- Modbus 由 Modicon 公司(后被施耐德电气 Schneider Electric 收购)于 1979 年首创,最初作为其 PLC 与智能设备(如传感器、仪表)之间的专用通信协议。
-
协议运行在 RS-232 或 RS-485 物理层上,支持两种传输方式:
- Modbus RTU:紧凑的二进制编码,效率高,适用于噪声环境;
- Modbus ASCII:以可读字符(十六进制)编码,便于调试,但效率较低。
-
由于 协议结构极其简单、无授权费用、文档易于逆向,大量第三方设备厂商在未获官方许可的情况下自行实现 Modbus 接口,并集成到变送器(特指将物理量(如压力、温度等)转换为标准电信号(如 4–20 mA 或 0–10 V),并通过 Modbus 接口(如 RTU 或 TCP)将这些数据以数字形式上传的设备)、HMI(人机界面,用于操作人员与控制系统交互,显示实时数据、报警信息,并允许操作员下发控制命令(如启停设备、修改参数))、驱动器(控制电机的转速、方向、启停等,常用于风机、泵、传送带等设备)等产品中。
- 这一时期的 Modbus 虽无正式标准,却凭借“事实标准”(de facto standard)的地位,在工业现场迅速扩散,为后续生态奠定用户基础。
阶段二:1990s 末 – 2001 年 — 转型与融合,拥抱网络
- 关键转折点:1990 年代末,Modicon 主动开放 Modbus 协议规范,公开技术文档,鼓励第三方厂商开发兼容产品,并停止对协议使用的法律限制。
- 与此同时,工业以太网兴起,传统串行通信在速度、距离和 IT 融合方面面临瓶颈。
-
2000 年,Modicon 正式发布 Modbus TCP 规范,将 Modbus 应用层协议映射到标准 TCP/IP 协议栈上:
- 使用 IP 地址 + 端口 502 定位设备;
- 引入 MBAP 头部(含事务 ID、协议 ID、长度字段);
- 依赖 TCP 的可靠性,取消 CRC 校验和从站地址字段(Unit Identifier 保留用于网关场景)。
-
此阶段完成了 Modbus 从“串行总线协议”到“工业网络协议”的关键跃迁,打通 OT(运营技术)与 IT(信息技术)的边界,为远程监控、系统集成和数字化转型铺平道路。
阶段三:2002 年至今 — 开放治理,全球标准
-
2002 年,为推动协议的中立化与可持续发展,Schneider Electric 联合多家工业自动化厂商,发起成立 Modbus International Developers Association(Modbus-IDA),负责:
- 制定一致性测试规范;
- 提供互操作性认证;
- 维护官方技术文档。
-
2004 年,为进一步增强开放性与国际代表性,Modbus-IDA 的全部职能移交至新成立的非营利独立组织 —— Modbus Organization, Inc.(在美国注册),全面接管:
- 协议规范维护(官网:https://modbus.org);
- 一致性测试与认证服务;
- 成员协作与生态推广。
-
Modbus 协议本身完全开放、免费,任何厂商均可自由实现;但若需使用“Modbus 认证”标志或参与标准演进,则需加入成员计划并通过官方测试。
-
协议生态持续扩展,衍生出多种传输方式以适应不同场景:
- Modbus TCP:标准以太网协议,主流于现代工业网络;
- Modbus RTU over Ethernet:通过 TCP 或 UDP 透传原始 RTU 帧,常用于串口服务器或网关;
- Modbus Plus:施耐德专用高速令牌环网络(已逐渐退出主流)。
-
现状:据行业普遍估计,Modbus 仍是全球部署最广泛的工业通信协议之一,广泛应用于 PLC、HMI、变频器、智能仪表、能源管理系统等设备,尤其在中小型自动化系统、国产设备和成本敏感场景中占据主导地位。
3.3 Modbus 官方资料下载地址
所有 Modbus 协议规范均由 Modbus Organization 免费公开提供, 开发者应以 https://modbus.org 发布的最新版本为准,避免使用过时或非官方文档。
1. Modbus 官方网站:https://modbus.org
2. 规范与实现指南资料:https://www.modbus.org/modbus-specifications
第四部分:Modbus 通信机制与协议规范
4.1 Modbus 通信架构模型
Modbus 协议是 OSI 七层模型中第 7 层上的应用层报文传输协议,它在连接不同类型总线或网络的设备之间提供客户端/服务器通信。
它主要是定义了应用层的消息结构(功能码 + 数据),其底层依赖于其他通信方式来实现完整的通信功能,包括传统的串行总线(RS-232/RS-485)、专有网络(Modbus+),以及现代的以太网(Modbus TCP/IP)。
Modbus 的通信架构模型如下图所示:

通过这张图可以很清晰地看出 Modbus 协议的分层架构以及在不同物理和网络层上的实现方式,直观说明 Modbus 应用层如何“嫁接”到多种底层传输技术上。
接下来,我们从上到下、从左到右进行解读:
1、顶层:Modbus 应用层
这是整张图的核心。无论底层使用何种物理或网络技术,Modbus 应用层定义了设备之间数据交换的规则和格式。这是统一、标准化的部分。
2、底层实现方式
从应用层往下,有四条不同的“通道”,代表了 Modbus 协议在不同硬件和网络环境下的具体实现:
1. 最左侧:“Other”
这是一个占位符,只是表示除了图中列举的三种方式外,Modbus 应用层还可以运行在其他的物理层或传输层上。
2. 第二个:“MODBUS+ / HDLC”
这是 Modicon 公司开发的一种专有高速网络协议,它基于 HDLC(High-level Data Link Control)协议,并使用特定的物理介质(例如双绞线)。
特点是速度较快,但是属于私有协议,兼容性不如 Modbus RTU、Modbus TCP。
3. 第三个:“Client / Server”
这是最经典的 Modbus RTU 或 Modbus ASCII 的实现方式。物理层采用 EIA/TIA-232 或 EIA/TIA-485,也就是我们常说的 RS232 或 RS485.
在这种模式下,通信通常是主从式(Master/Slave),因此,我们也可以将客户端称为主站,服务器称为从站。
4. 最右侧:“Modbus on TCP”
这是 Modbus 应用层数据被封装在 TCP/IP 数据包中的形式。也被称为 Modbus TCP/IP。通过 TCP/IP 和以太网协议进行通信。
4.2 Modbus 协议数据结构
Modbus 协议定义了一个与基础通信层无关的简单协议数据单元(PDU)。
这句话的意思是,不论你是使用任何方式进行传输 Modbus 信息,这部分结构都是完全一样。
特定总线或网络上的 Modbus 协议映射能够在应用数据单元(ADU)上引入一些附加字段。
这句话的意思是,当你决定通过某种方式进行传输这个 PDU 时,你需要为其套上一个额外的附加字段,这个附加字段与 PDU 组合后称为 ADU。
通用协议结构的格式如下图所示:

接下来对 PDU 和 ADU 进行说明:
4.2.1 PDU(Protocol Data Unit,协议数据单元)
PDU 是独立于通信网络的、纯粹的 Modbus 请求或响应参数,由 功能码 + 数据 两部分组成。
PDU 作为 Modbus 协议的核心,特点是对于任何传输方式来说其结构都是完全相同的。
例如,一个读保持寄存器的请求 PDU 结构永远都是
[功能码][起始地址高字节][起始地址低字节][寄存器数量高字节][寄存器数量低字节]
其中,功能码固定为 0x03,起始地址、寄存器数量需要根据具体请求进行修改。
4.2.2 ADU(Application Data Unit,应用数据单元)
ADU 是实际网络中传输的完整数据帧,由 附加地址 + PDU + 差错校验码 三部分组成。
ADU 的特点是 PDU 为了在特定网络(如串行线、TCP/IP)上传输而添加的“头”和“尾”。
不同的传输方式,ADU 的结构也会不同。
PDU 与 ADU 最大长度限制说明: Modbus PDU 最大 253 字节是历史遗留设计,源于 RS-485 ADU 最大 256 字节的限制; 在串行通信中,ADU = PDU + 1 字节地址 + 2 字节 CRC = 256 字节; 在 TCP 中,ADU = PDU + 7 字节 MBAP = 260 字节;
4.3 客户端/服务器 与 主站/从站 的概念辨析
在 Modbus 协议中,“客户端/服务器”和“主站/从站”是描述通信角色的两种常用术语,它们在功能上高度相似,但在语义来源和适用场景上略有不同。
4.3.1 客户端 (Client) 与 服务器 (Server)
- 来源: 来源于计算机网络领域的通用模型。
-
定义:
- 客户端:主动发起请求的一方,负责发送功能码和数据请求。
- 服务器:被动响应请求的一方,负责执行操作并返回结果。
-
适用场景: 主要用于 Modbus TCP/IP 等基于网络协议栈的实现中,更符合现代 IT 架构的表达习惯。
- 特点: 强调“请求-响应”的交互模式,不强调物理拓扑或控制权。
4.3.2 主站 (Master) 与 从站 (Slave)
- 来源: 来源于工业自动化和串行通信的历史传统。
-
定义:
- 主站:拥有通信控制权的一方,负责轮询或指令下发。
- 从站:被动响应主站指令,无权主动发起通信。
-
适用场景: 主要用于 Modbus RTU/ASCII 等串行通信方式中,强调“控制权集中”和“单主多从”的物理拓扑。
- 特点: 更强调控制关系和时序管理,常见于 RS-485 总线系统。
4.3.3 两者关系说明
| 术语 | 对应角色 | 是否可互换 | 说明 |
| 客户端 (Client) | 主站 (Master) | 在大多数上下文中可以 | 尤其在 Modbus TCP 中更常用 |
| 服务器 (Server) | 从站 (Slave) | 在大多数上下文中可以 | 尤其在 Modbus RTU 中更常用 |
特别说明: 目前 Modbus 组织(Modbus Organization)已正式推荐使用 “客户端/服务器” 术语替代“主站/从站”,以避免潜在的社会文化敏感性问题,并统一跨平台、跨协议的表述。 但在实际工程文档和设备手册中,“主站/从站”仍被广泛使用。
4.4 Modbus 事务处理机制
4.4.1 正常响应

在启动请求阶段,客户端将功能码和数据请求发给服务器。服务器收到客户端发来的请求后进行分析与处理操作。处理结束后,服务器向客户端发送处理结果,即返回功能码和数据响应。
在正常响应时,返回的功能码为客户端请求时的功能码,数据响应为实际请求到的数据。
4.4.2 异常响应

在异常响应时,返回的功能码为客户端请求时的功能码 + 0x80,称为差错码。数据响应为异常码。
此处简单介绍一下什么是差错码和异常码:
-
差错码(Error Code):
- 它本质上是原始请求功能码的最高位被置为 1
- 原始功能码是一个 1 字节数值,范围是 1~127,HEX 表示为 0x01~0x7F
- 将这个值加上 128,即
0x80,就得到了差错码。此时范围是 129~255,HEX 表示为 0x81~0xFF - 作用:告诉客户端“你刚才发的请求不正确”。例如,如果客户端请求时的功能码为
0x03,那么异常响应中的差错码就是0x83(0x03 + 0x80)
-
异常码(Exception Code):
- 也占用 1 个字节数值,紧跟在差错码后面,用于具体说明出错的原因
- 作用:告诉客户端究竟是哪里出了问题
- 举 4 个常见的异常码:
- 01:非法功能码,表示从站设备不支持请求的功能码
- 02:非法数据地址,表示请求的寄存器地址不存在或者无效
- 03:非法数据值,表示请求的数据字段无效
- 04:从站设备故障,表示从站设备在执行请求时发生内部错误
第五部分:Modbus 数据模型
5.1 Modbus 四种基本数据类型
注:Modbus 协议中通常将这四类称为“数据对象”(Data Objects),但在工程实践中常简称为“数据类型”,本文将沿用此习惯进行介绍。
Modbus 协议定义了四种基本类型(线圈、离散输入、输入寄存器、保持寄存器)。采取这样的划分是基于工业控制系统中常见的硬件接口特性、数据访问需求以及通信效率的综合考量。通过将数据按照功能、读写属性和物理意义进行分类,Modbus 在保持协议间接性的同时,可以有效映射真实设备的输入输出行为,为不同厂商设备之间的互操作性提供清晰、一致的数据模型基础。
5.1.1 线圈(Coils)
- 中文别名:数字输出、开关输出、DO(Digital Output)
- 存储单元:单比特(1 bit)
- 数值范围:两种状态:0(OFF)或 1(ON)
- 访问权限:可读可写
-
功能码:
- 01(0x01):读单个或多个线圈
- 05(0x05):写单个线圈
- 15(0x0F):写多个线圈
-
典型用途:控制继电器、开关、指示灯等数字输出设备
- 实际应用举例:
- 控制继电器的吸合或断开
- 控制电机的启动或停止
- 控制阀门的开启或关闭
- 控制指示灯的点亮或熄灭
5.1.2 离散输入(Discrete Inputs)
- 中文别名:数字输入、开关输入、DI(Digital Input)
- 存储单元:单比特(1 bit)
- 数值范围:两种状态:0(OFF)或 1(ON)
- 访问权限:只读
-
功能码:
- 02(0x02):读单个或多个离散输入
-
典型用途:读取限位开关、按钮、传感器等数字输入状态
- 实际应用举例:
- 读取一个按钮是否被按下
- 检测一个限位开关是否触发
- 判断门磁传感器是否报警(门 开/关)
- 查看故障报警信号的状态
5.1.3 输入寄存器(Input Registers)
- 中文别名:模拟量输入、只读寄存器、AI(Analog Input)
- 存储单元:16 位(2 bytes)
- 数值范围:0~65535(无符号)或者 -32768~32767(有符号,属于非协议标准,需要主/从站设备配合实现)
- 访问权限:只读
-
功能码:
- 04(0x04):读单个或多个输入寄存器
-
典型用途:读取传感器温度、压力、电压等模拟量输入值
- 实际应用举例:
- 读取温度传感器的测量值(如 25.4℃)
- 读取压力传感器的实际压力
- 读取流量计的当前流量
- 读取设备运行的累计时间(通常由设备内部维护,此处只供读取)
5.1.4 保持寄存器(Holding Registers)
- 中文别名:模拟量输出、读写寄存器、AO(Analog Output)
- 存储单元:0~65535(无符号)或者 -32768~32767(有符号,属于非协议标准,需要主/从站设备配合实现)
- 访问权限:可读可写
-
功能码:
- 03(0x03):读单个或多个保持寄存器
- 06(0x06):写单个保持寄存器
- 16(0x10):写多个保持寄存器
-
典型用途:存储配置参数、设定值、设备状态等可被主站设备修改的数据
-
实际应用举例:
- 写入方面:设置目标温度、设定电机转速、修改报警阈值、发送控制命令代码
- 读取方面:读取设备内部计算的参数、获取系统状态信息、读取预置的配方数据
5.1.5 基本数据类型一览表
| 数据类型(中文) | 数据类型(英文) | 存储单元 | 访问权限 | 功能码 | 典型用途 |
| 线圈 | Coils | 1 bit | 可读可写 | 01(0x01):读单个或多个线圈 05(0x05):写单个线圈 15(0x0F):写多个线圈 | 控制继电器、开关、指示灯等数字输出设备 |
| 离散输入 | Discrete Inputs | 1 bit | 只读 | 02(0x02):读单个或多个离散输入 | 读取限位开关、按钮、传感器等数字输入状态 |
| 输入寄存器 | Input Registers | 16 bit | 只读 | 04(0x04):读单个或多个输入寄存器 | 读取传感器温度、压力、电压等模拟量输入值 |
| 保持寄存器 | Holding Registers | 16 bit | 可读可写 | 03(0x03):读单个或多个保持寄存器 06(0x06):写单个保持寄存器 16(0x10):写多个保持寄存器 | 存储配置参数、设定值、设备状态等可被主站设备修改的数据 |
5.2 Modbus 数据区与寻址
从实现的角度来看,Modbus 协议将这四种数据类型概念性地组织为四个独立的数据区。
每个数据区都是一个线性的地址空间:
| 数据区 | 逻辑地址范围 | 访问类型 | 数据类型 |
| 线圈 | 00001 - 09999 | 读/写 | 1 位 |
| 离散输入 | 10001 - 19999 | 只读 | 1 位 |
| 输入寄存器 | 30001 - 39999 | 只读 | 16 位 |
| 保持寄存器 | 40001 - 49999 | 读/写 | 16 位 |
注意:表中“逻辑地址”是面向用户的编号系统。实际 Modbus 报文(PDU)中传输的是从 0 开始的偏移量,也正是我接下来开始介绍的 5.3 地址映射。
5.3 Modbus 地址映射
5.3.1 核心概念
1. 寄存器地址/偏移量(Register Address/Offset)
- 含义:Modbus 报文 PDU 中实际传输的寄存器/线圈起始地址,是从 0 开始的 16 位无符号整数偏移量。
- 特点:范围理论上为 0-65535,但具体支持的地址范围由从站设备内部定义的数据模型决定。
- 示例:读取保持寄存器 40001 -> 实际协议地址为 0x0000;读取 40010 -> 协议地址为 0x0009。
2. 逻辑地址/参考地址(Logical Address/Reference Address)
- 含义:面向用户和工程师的地址表示法,用于配置和编程时直观区分数据类型。由功能前缀 + 序号组成。
-
特点:通常为 5 位十进制数,前缀固定表示数据区类型:
0xxxx:线圈(Coils)1xxxx:离散输入(Discrete Inputs)3xxxx:输入寄存器(Input Registers)4xxxx:保持寄存器(Holding Registers)
-
示例:
40001,30002,10005,00003
5.3.2 转换关系与规则
转换的核心规则是:协议地址 = 逻辑地址 - 基地址
这个“基地址”因数据类型而异,从而形成不同的逻辑地址范围
| 数据类型 | 逻辑地址范围 | 协议地址范围 (报文中使用) | 功能码 | 转换公式 |
| 线圈 | 00001 - 09999 | 0 - 9998 | 01, 05, 15 | 协议地址 = 设备地址 - 1 |
| 离散输入 | 10001 - 19999 | 0 - 9998 | 02 | 协议地址 = 设备地址 - 10001 |
| 输入寄存器 | 30001 - 39999 | 0 - 9998 | 04 | 协议地址 = 设备地址 - 30001 |
| 保持寄存器 | 40001 - 49999 | 0 - 9998 | 03, 06, 16 | 协议地址 = 设备地址 - 40001 |
重要提示:
表中的“9998”是一个常规上限,由于协议地址是 16 位无符号整数,其理论范围是 0-65535,此时 5 位数已经无法满足,于是也有许多厂商使用 6 位数字进行表示:
0xxxxx:线圈(Coils)1xxxxx:离散输入(Discrete Inputs)3xxxxx:输入寄存器(Input Registers)4xxxxx:保持寄存器(Holding Registers)
这是对原始 5 位数表示的扩展,我们在平时使用时,一定要先查阅各个产品的设备手册,查看逻辑地址和协议地址范围。
5.3.3 转换示例
假如你在 HMI(人机界面)或者上位机软件中配置了一个数据标签,标签指向保持寄存器 40100。
1. 确定数据类型:地址以 4 开头,说明是保持寄存器。
2. 应用转换公式:协议地址 = 设备地址 - 40001。
3. 进行计算:40100 - 40001 = 99,这是十进制数值,十六进制表示为 0x0063。
当主站设备需要读取这个标签时,它会组成这样一个 PDU 报文:
- 功能码:
0x03(读保持寄存器) - 起始地址:
0x000x63(高字节为 0,低字节为 99) - 数量:
0x000x01(读取 1 个寄存器)
从站设备在收到请求后,便会去自己的保持寄存器数据区中,查找协议地址为 99 的寄存器,然后组成响应报文后返回给主站设备
5.4 Modbus 数据模型相关说明
- Modbus 协议本身只定义了数据的类型和访问方式,但并没有规定这些数据具体代表什么含义或如何被使用。
-
尽管协议定义了四种独立的数据区,但从抽象或实现的角度看,可以认为它们在功能或存储上是相互重叠、相互表示的。
- 解释一:从逻辑上看,一个 16 位的寄存器可以看作是 16 个独立的比特。因此,从数据存储的潜力上来说,保持寄存器的地址空间(每个地址存 16 位)可以“覆盖”或“包含”线圈的地址空间(每个地址存 1 位)。同理,输入寄存器的地址空间也可以“覆盖”离散输入的地址空间。
- 解释二:在某些系统的内存实现中,这四种数据区可能最终都会映射到同一片物理内存或处理逻辑上。虽然 Modbus 协议将它们分为四种数据区,但底层硬件可能用统一的方式管理它们。
-
对于数据区中的任何一项,Modbus 协议都允许单个地选择 65536 个数据项。
- Modbus 协议允许在一次请求中,批量写入多个连续的地址。你不需要为了读取 10 个连续的保持寄存器而发送 10 次单独的请求,只需要发送一次请求,并告诉从站设备“从地址 X 开始,给我 Y 个寄存器的值”。
- 虽然可以批量读写,但一次操作所能处理的最大数据量是有限制的。这个限制不是来自地址空间(65536),而主要取决于所使用的功能码和 Modbus 传输模式。
- 通过 Modbus 处理的所有数据放置在设备应用存储器中。但是,存储器的物理地址不应该与数据参考混淆。要求仅仅是数据参考与物理地址的链接。
- Modbus 功能码中使用的 Modbus 逻辑参考数字是以 0 开始的无符号整数索引。
第六部分:Modbus 功能码
Modbus 协议定义了三类功能码,分别为公共功能码、用户定义功能码、保留功能码。
在介绍这三类功能码之前,有一点需要注意,我们平时在使用这些功能码时需要先去查阅设备手册,查看使用的设备都支持哪些功能码,包括公共功能码也并非是全部都支持。
6.1 公共功能码(Public Function Codes)
这是最常用、被官方 Modbus 协议标准明确定义、保证唯一性且得到最广泛支持的功能码。它们用于执行最核心的数据读写操作。
这类功能码又可细分为核心数据访问功能、诊断功能和其他专用功能三个类别。
1. 核心数据访问功能码
这些是用于读写设备基本数据(线圈、离散输入、寄存器)的最常用功能。
| 功能名称 | 功能码 (十进制) | 功能码 (十六进制) | 描述 |
| 读取离散输入 | 02 | 02 | 读取只读的物理开关量输入点 |
| 读取线圈 | 01 | 01 | 读取可读可写的开关量输出点 |
| 写单个线圈 | 05 | 05 | 写入单个开关量输出点 |
| 写多个线圈 | 15 | 0F | 写入多个连续的开关量输出点 |
| 读取输入寄存器 | 04 | 04 | 读取只读的模拟量输入或数据 |
| 读取保持寄存器 | 03 | 03 | 读取可读可写的模拟量输出或数据存储区 |
| 写单个寄存器 | 06 | 06 | 写入单个寄存器值 |
| 写多个寄存器 | 16 | 10 | 写入多个连续的寄存器值 |
| 读/写多个寄存器 | 23 | 17 | 在一个请求中同时执行读和写寄存器操作 |
| 屏蔽写寄存器 | 22 | 16 | 对寄存器进行“与”/“或”掩码操作,用于修改特定位 |
2. 诊断功能码
这些功能码用于查询通信状态、设备异常和设备标识信息,主要用于调试和监控。
| 功能名称 | 代码 (十进制) | 代码 (十六进制) | 描述 |
| 读取异常状态 | 07 | 07 | 获取一个8位异常状态字节 |
| 诊断 | 08 | 08 | 运行一系列子功能码的诊断测试 |
| 获取通信事件计数器 | 11 | 0B | 获取通信事件计数器的状态 |
| 获取通信事件日志 | 12 | 0C | 获取通信事件日志 |
| 报告从站设备ID | 17 | 11 | 报告从站设备的描述和运行状态 |
| 读取设备标识 | 43 | 2B | 读取设备的制造商、产品型号等标识信息 |
3. 其他专用功能
| 功能名称 | 代码 (十进制) | 代码 (十六进制) | 描述 |
| 读取FIFO队列 | 24 | 18 | 从先入先出队列中读取数据 |
| 读取文件记录 | 20 | 14 | 读取设备文件记录中的数据 |
| 写入文件记录 | 21 | 15 | 向设备文件记录中写入数据 |
| 封装接口传输 | 43 | 2B | 传输设备自定义或私有数据 |
| CANopen 通用参考 | 43 | 2B | 用于与CANopen设备映射相关的功能 |
6.2 用户定义功能码(User-Defined Function Codes)
这类功能码保留给公司或个人,用于为特定的设备开发自定义的功能。
- 范围:65 至 72 和 100 至 110。
-
特点:
- Modbus 官方标准没有为这些代码定义任何行为。
- 其功能和实现完全由用户(设备厂商)自行定义。
- 不能保证被选功能码的使用是唯一的。
- 使用这些功能码的设备之间在进行通信时不具备通用性。必须完全依赖设备厂商提供的文档来了解如何使用它们。
6.3 保留功能码(Reserved Function Codes)
这些功能码在过去被一些传统的 Modbus 设备使用,或者被保留给未来可能的标准扩展使用。在新设备和项目中应避免使用它们。
- 范围:8/19;8/21-65535,9,10,13,14,41,42,90,91,125,126,127 功能码。
- 特点:它们没有公共定义,可能在某些老旧系统中存在,但不在现代 Modbus 标准之内。
6.4 功能码示例描述
6.4.1 (0x01)读取线圈
该功能码用于从从站设备中读取 1 ~ 2000 个连续的线圈(Coil)状态。
请求 PDU 中指定了起始地址(即所请求的第一个线圈的地址)以及线圈数量。
在 PDU 中,线圈地址从 0 开始。因此,编号为 1 - 16 的线圈,其地址为 0 - 15。
响应报文中的线圈状态以每比特表示一个线圈的方式打包在数据字段中。
状态值 1 表示 ON(开启),0 表示 OFF(关闭)。
第一个数据字节的最低有效位(LSB) 对应查询中指定的起始地址线圈,其余线圈依次向该字节的高位排列,并在后续字节中继续从低位到高位依次填充。
如果返回的线圈数量不是 8 的整数倍,则最后一个数据字节的高位将用 0 填充。
字节数字段表示数据字段中完整字节的数量。
请求 PDU:
| 功能码 | 1 个字节 | 0x01 |
| 起始地址 | 2 个字节 | 0x0000 ~ 0xFFFF (0 ~ 65535) |
| 线圈数量 | 2 个字节 | 0x0001 ~ 0x07D0 (1 ~ 2000) |
响应 PDU:
| 功能码 | 1 个字节 | 0x01 |
| 字节数 | 1 个字节 | N |
| 线圈状态 | N 个字节 |
N = 线圈数量 / 8 。
异常响应:
| 差错码 | 1 个字节 | 功能码 + 0x80 |
| 异常码 | 1 个字节 | 01 / 02 / 03 / 04 ... |
举例分析:
以下为读取线圈 20 - 38 的请求示例:
| 请求 | 响应 | ||
| 字段名称 | (十六进制) | 字段名称 | (十六进制) |
| 功能码 | 01 | 功能码 | 01 |
| 起始地址高字节 | 00 | 字节数 | 03 |
| 起始地址低字节 | 13 | 输出状态(27 - 20) | CD |
| 输出数量高字节 | 00 | 输出状态(35 - 28) | 6B |
| 输出数量低字节 | 13 | 输出状态(38 - 36) | 05 |
线圈 27 - 20 的状态以字节值 CD(十六进制)表示,即二进制 1100 1101。
其中,线圈 27 位于该字节的最高有效位(MSB),线圈 20 位于最低有效位(LSB)。
按照惯例,字节内的比特位从左到右依次表示 MSB 到 LSB。
因此,第一个字节中的线圈从左到右依次为 27、26、25、24、23、22、21、20。
下一个字节则包含线圈 35 - 28,同样从左到右排列。
然而,在串行传输时,比特位是从 LSB 到 MSB 依次发送的,即:
20 → 21 → … → 27,接着 28 → 29 → … → 35,依此类推。
在最后一个数据字节中,线圈 38 - 36 的状态以字节值 05(十六进制) 表示,即二进制 0000 0101。
其中,线圈 36 位于 LSB(最右侧),线圈 38 位于从左往右数的第 6 位(即 bit 2,从 0 开始计数)。
该字节中剩余的高 5 位(bit 7 到 bit 3)均以 0 填充。
6.4.2 (0x02)读取离散输入
该功能码用于从从站设备中读取 1 ~ 2000 个连续的离散输入状态。
请求 PDU 中指定了起始地址(即所请求的第一个离散输入的地址)以及离散输入的数量。
在 PDU 中,离散输入的地址从 0 开始。因此,编号为 1 - 16 的离散输入,其地址为 0 - 15。
响应报文中的离散输入状态以每比特表示一个离散输入的方式打包在数据字段中。
状态值 1 表示 ON(开启),0 表示 OFF(关闭)。
第一个数据字节的最低有效位(LSB) 对应查询中指定的起始地址离散输入,其余离散输入依次向该字节的高位排列,并在后续字节中继续从低位到高位依次填充。
如果返回的离散输入数量不是 8 的整数倍,则最后一个数据字节的高位将用 0 填充。
字节数字段表示数据字段中完整字节的数量。
请求 PDU:
| 功能码 | 1 个字节 | 0x02 |
| 起始地址 | 2 个字节 | 0x0000 ~ 0xFFFF (0 ~ 65535) |
| 输入数量 | 2 个字节 | 0x0001 ~ 0x07D0 (1 ~ 2000) |
响应 PDU:
| 功能码 | 1 个字节 | 0x02 |
| 字节数 | 1 个字节 | N |
| 输入状态 | N 个字节 |
N = 输入数量 / 8 。
异常响应:
| 差错码 | 1 个字节 | 功能码 + 0x80 |
| 异常码 | 1 个字节 | 01 / 02 / 03 / 04 ... |
举例分析:
以下为读取离散输入 197 - 218 的请求示例:
| 请求 | 响应 | ||
| 字段名称 | (十六进制) | 字段名称 | (十六进制) |
| 功能码 | 02 | 功能码 | 02 |
| 起始地址高字节 | 00 | 字节数 | 03 |
| 起始地址低字节 | C4 | 输入状态(204 - 197) | AC |
| 输入数量高字节 | 00 | 输入状态(212 - 205) | DB |
| 输入数量低字节 | 16 | 输入状态(218 - 213) | 35 |
离散输入 204 - 197 的状态以字节值 AC(十六进制) 表示,即二进制 1010 1100。
其中,离散输入 204 位于该字节的最高有效位(MSB,最左侧),离散输入 197 位于最低有效位(LSB,最右侧)。
离散输入 218 - 213 的状态以字节值 35(十六进制) 表示,即二进制 0011 0101。 其中,离散输入 213 位于 LSB(最右侧),离散输入 218 位于从左往右数的第 3 位(即 bit 5,从 0 开始计数)。
6.4.3 (0x03)读取保持寄存器
该功能码用于读取从从站设备中读取 1 ~ 125 个连续的保持寄存器。
请求 PDU 中指定了寄存器起始地址和要读取的寄存器数量。
在 PDU 中,寄存器地址从 0 开始。因此,编号为 1 - 16 的寄存器,其地址为 0 - 15。
响应报文中的寄存器数据以每个寄存器占用两个字节的方式打包,二进制数值在 16 位空间中右对齐(即采用标准整数格式)。
对于每个寄存器,第一个字节包含高字节(高 8 位),第二个字节包含低字节(低 8 位)。
请求 PDU:
| 功能码 | 1 个字节 | 0x03 |
| 起始地址 | 2 个字节 | 0x0000 ~ 0xFFFF (0 ~ 65535) |
| 寄存器数量 | 2 个字节 | 0x0001 ~ 0x007D (1 ~ 125) |
响应 PDU:
| 功能码 | 1 个字节 | 0x03 |
| 字节数 | 1 个字节 | N × 2 |
| 寄存器值 | N × 2 个字节 |
N = 保持寄存器的数量 。
异常响应:
| 差错码 | 1 个字节 | 功能码 + 0x80 |
| 异常码 | 1 个字节 | 01 / 02 / 03 / 04 ... |
举例分析:
以下为读取保持寄存器 108 - 110 的请求示例:
| 请求 | 响应 | ||
| 字段名称 | (十六进制) | 字段名称 | (十六进制) |
| 功能码 | 03 | 功能码 | 03 |
| 起始地址高字节 | 00 | 字节数 | 06 |
| 起始地址低字节 | 6B | 寄存器值高字节(108) | 02 |
| 寄存器数量高字节 | 00 | 寄存器值低字节(108) | 2B |
| 寄存器数量低字节 | 03 | 寄存器值高字节(109) | 00 |
| 寄存器值低字节(109) | 00 | ||
| 寄存器值高字节(110) | 00 | ||
| 寄存器值低字节(110) | 64 | ||
保持寄存器 108 的内容以两个字节表示为 02 2B(十六进制),即十进制 555。
保持寄存器 109 - 110 的内容分别为 00 00 和 00 64(十六进制),即十进制 0 和 100。
6.4.4 (0x04)读取输入寄存器
该功能码用于从从站设备中读取 1 ~ 125 个连续的输入寄存器。
请求 PDU 中指定了寄存器起始地址和要读取的寄存器数量。
在 PDU 中,寄存器地址从 0 开始。因此,编号为 1 - 16 的输入寄存器,其地址为 0 - 15。
响应报文中的寄存器数据以每个寄存器占用两个字节的方式打包,二进制数值在 16 位空间中右对齐(即采用标准整数格式)。
对于每个寄存器,第一个字节包含高字节(高 8 位),第二个字节包含低字节(低 8 位)。
请求 PDU:
| 功能码 | 1 个字节 | 0x04 |
| 起始地址 | 2 个字节 | 0x0000 ~ 0xFFFF (0 ~ 65535) |
| 寄存器数量 | 2 个字节 | 0x0001 ~ 0x007D (1 ~ 125) |
响应 PDU:
| 功能码 | 1 个字节 | 0x04 |
| 字节数 | 1 个字节 | N × 2 |
| 寄存器值 | N × 2 个字节 |
N = 输入寄存器的数量 。
异常响应:
| 差错码 | 1 个字节 | 功能码 + 0x80 |
| 异常码 | 1 个字节 | 01 / 02 / 03 / 04 ... |
举例分析:
以下是读取输入寄存器 9 的请求示例:
| 请求 | 响应 | ||
| 字段名称 | (十六进制) | 字段名称 | (十六进制) |
| 功能码 | 04 | 功能码 | 04 |
| 起始地址高字节 | 00 | 字节数 | 02 |
| 起始地址低字节 | 08 | 寄存器值高字节(9) | 00 |
| 寄存器数量高字节 | 00 | 寄存器值低字节(9) | 0A |
| 寄存器数量低字节 | 01 | ||
输入寄存器 9 的内容以两个字节表示为 00 0A(十六进制),即十进制 10。
6.4.5 (0x05)写入单个线圈
该功能码用于将从站设备中的单个线圈强制置为 ON 或 OFF。
请求数据字段中的一个常量用于指定所需的 ON/OFF 状态:
- 值为 FF00(十六进制)表示将线圈置为 ON;
- 值为 0000(十六进制)表示将线圈置为 OFF;
- 所有其他值均为非法,不会对线圈状态产生任何影响。
请求 PDU 中指定了要操作的线圈地址。线圈地址从 0 开始,因此编号为 1 的线圈,其地址为 0。
ON/OFF 状态由输出值字段中的常量指定:
- 0xFF00 表示 ON,
- 0x0000 表示 OFF,
- 其他值无效,线圈状态保持不变。
正常响应是在线圈状态成功写入后,原样返回请求报文的内容(即请求的回显)。
请求 PDU:
| 功能码 | 1 个字节 | 0x05 |
| 输出地址 | 2 个字节 | 0x0000 ~ 0xFFFF (0 ~ 65536) |
| 输出值 | 2 个字节 | 0x0000 / 0xFF00 |
响应 PDU:
| 功能码 | 1 个字节 | 0x05 |
| 输出地址 | 2 个字节 | 0x0000 ~ 0xFFFF (0 ~ 65535) |
| 输出值 | 2 个字节 | 0x0000 / 0xFF00 |
异常响应:
| 差错码 | 1 个字节 | 功能码 + 0x80 |
| 异常码 | 1 个字节 | 01 / 02 / 03 / 04 ... |
举例分析:
以下是将线圈 173 置为 ON 的写请求示例:
| 请求 | 响应 | ||
| 字段名称 | (十六进制) | 字段名称 | (十六进制) |
| 功能码 | 05 | 功能码 | 05 |
| 输出地址高字节 | 00 | 输出地址高字节 | 00 |
| 输出地址低字节 | AC | 输出地址低字节 | AC |
| 输出值高字节 | FF | 输出值高字节 | FF |
| 输出值低字节 | 00 | 输出值低字节 | 00 |
6.4.6 (0x06)写入单个寄存器
该功能码用于向从站设备中写入一个保持寄存器。
请求 PDU 中指定了要写入的寄存器地址。寄存器地址从 0 开始,因此编号为 1 的寄存器,其地址为 0。
正常响应是在寄存器内容成功写入后,原样返回请求报文的内容(即请求的回显)。
请求 PDU:
| 功能码 | 1 个字节 | 0x06 |
| 寄存器地址 | 2 个字节 | 0x0000 ~ 0xFFFF (0 ~ 65536) |
| 寄存器值 | 2 个字节 | 0x0000 ~ 0xFFFF (0 ~ 65535) |
响应 PDU:
| 功能码 | 1 个字节 | 0x06 |
| 寄存器地址 | 2 个字节 | 0x0000 ~ 0xFFFF (0 ~ 65536) |
| 寄存器值 | 2 个字节 | 0x0000 ~ 0xFFFF (0 ~ 65535) |
异常响应:
| 差错码 | 1 个字节 | 功能码 + 0x80 |
| 异常码 | 1 个字节 | 01 / 02 / 03 / 04 ... |
举例分析:
以下是将寄存器 2 写入值 00 03(十六进制)的请求示例:
| 请求 | 响应 | ||
| 字段名称 | (十六进制) | 字段名称 | (十六进制) |
| 功能码 | 06 | 功能码 | 06 |
| 寄存器地址高字节 | 00 | 寄存器地址高字节 | 00 |
| 寄存器地址低字节 | 01 | 寄存器地址低字节 | 01 |
| 寄存器值高字节 | 00 | 寄存器值高字节 | 00 |
| 寄存器值低字节 | 03 | 寄存器值低字节 | 03 |
6.4.7 (0x0F)写多个线圈
该功能码用于将从站设备中一段连续线圈序列中的每一个线圈强制置为 ON 或 OFF。
请求 PDU 中指定了要操作的线圈起始地址和数量。线圈地址从 0 开始,因此编号为 1 的线圈,其地址为 0。
请求数据字段中的每一位指定了对应线圈的目标状态:
- 某一位为逻辑 ‘1’,表示将对应的线圈置为 ON;
- 某一位为逻辑 ‘0’,表示将对应的线圈置为 OFF。
正常响应将返回功能码、起始地址以及被强制写入的线圈数量。
请求 PDU:
| 功能码 | 1 个字节 | 0x0F |
| 起始地址 | 2 个字节 | 0x0000 ~ 0xFFFF (0 ~ 65536) |
| 输出数量 | 2 个字节 | 0x0001 ~ 0x07B0 (1 ~ 1968) |
| 字节数 | 1 个字节 | N |
| 输出值 | N × 1 个字节 |
N = 线圈数量 / 8 。
响应 PDU:
| 功能码 | 1 个字节 | 0x05 |
| 起始地址 | 2 个字节 | 0x0000 ~ 0xFFFF (0 ~ 65536) |
| 输出数量 | 2 个字节 | 0x0001 ~ 0x07B0 (1 ~ 1968) |
异常响应:
| 差错码 | 1 个字节 | 功能码 + 0x80 |
| 异常码 | 1 个字节 | 01 / 02 / 03 / 04 ... |
举例分析:
以下是一个从线圈 20 开始,连续写入 10 个线圈的请求示例:
请求的数据内容为两个字节:CD 01(十六进制),即二进制 1100 1101 0000 0001。
这些二进制位与线圈的对应关系如下:
位(bit):1 1 0 0 1 1 0 1 0 0 0 0 0 0 0 1
对应线圈: 27 26 25 24 23 22 21 20 - - - - - - 29 28
第一个传输的字节(CD hex)对应线圈 27 - 20,其中最低有效位(LSB)对应线圈 20。
下一个传输的字节(01 hex)对应线圈 29 - 28,同样,最低有效位对应线圈 28。
最后一个数据字节中未使用的高位应填充为 0。
| 请求 | 响应 | ||
| 字段名称 | (十六进制) | 字段名称 | (十六进制) |
| 功能码 | 0F | 功能码 | 0F |
| 起始地址高字节 | 00 | 起始地址高字节 | 00 |
| 起始地址低字节 | 13 | 起始地址低字节 | 13 |
| 输出数量高字节 | 00 | 输出数量高字节 | 00 |
| 输出数量低字节 | 0A | 输出数量低字节 | 0A |
| 字节数 | 02 | ||
| 输出值 | CD | ||
| 输出值 | 01 | ||
6.4.8 (0x10)写多个寄存器
该功能码用于向从站设备中写入 1 - 123 个连续的寄存器块。
请求数据字段中指定了要写入的寄存器值,数据以每个寄存器两个字节的方式打包。
正常响应将返回功能码、寄存器起始地址以及成功写入的寄存器数量。
请求 PDU:
| 功能码 | 1 个字节 | 0x10 |
| 起始地址 | 2 个字节 | 0x0000 ~ 0xFFFF (0 ~ 65536) |
| 寄存器数量 | 2 个字节 | 0x0001 ~ 0x0078 (1 ~ 123) |
| 字节数 | 1 个字节 | N × 2 |
| 寄存器值 | N × 2 个字节 |
N = 寄存器数量 。
响应 PDU:
| 功能码 | 1 个字节 | 0x10 |
| 起始地址 | 2 个字节 | 0x0000 ~ 0xFFFF (0 ~ 65536) |
| 寄存器数量 | 2 个字节 | 0x0001 ~ 0x0078 (1 ~ 123) |
异常响应:
| 差错码 | 1 个字节 | 功能码 + 0x80 |
| 异常码 | 1 个字节 | 01 / 02 / 03 / 04 ... |
举例分析:
以下是一个从寄存器 2 开始,连续写入 2 个寄存器的请求示例:
请求写入的寄存器内容分别为 00 0A(十六进制)和 01 02(十六进制)。
| 请求 | 响应 | ||
| 字段名称 | (十六进制) | 字段名称 | (十六进制) |
| 功能码 | 10 | 功能码 | 10 |
| 起始地址高字节 | 00 | 起始地址高字节 | 00 |
| 起始地址低字节 | 01 | 起始地址低字节 | 01 |
| 寄存器数量高字节 | 00 | 输出数量高字节 | 00 |
| 寄存器数量低字节 | 02 | 输出数量低字节 | 02 |
| 字节数 | 04 | ||
| 寄存器值高字节 | 00 | ||
| 寄存器值低字节 | 0A | ||
| 寄存器值高字节 | 01 | ||
| 寄存器值低字节 | 02 | ||
6.5 异常响应
6.5.1 异常码说明
当主站设备向从站设备发送请求时,它期望得到一个正常响应。主站设备的查询可能导致以下四种情况发生其一:
- 如果从站设备收到请求且无通信错误,并能正常处理该查询,则返回一个正常响应。
- 如果从站设备因通信错误未能收到请求,则不会返回任何响应。主站设备程序最终将处理此请求的超时状态。
- 如果从站设备收到请求,但检测到通信错误(奇偶校验、LRC、CRC 等错误),则不会返回任何响应。主站设备程序最终将处理此请求的超时状态。
- 如果从站设备收到请求且无通信错误,但无法处理它(例如,请求读取一个不存在的输出或寄存器),从站设备将返回一个异常响应,通知主站设备错误的性质。
异常响应消息有两个字段,使其与正常响应区分开来:
功能码字段:
- 在正常响应中,从站设备会在响应的功能码字段中原样回显原始请求的功能码。
- 所有功能码的最高有效位(MSB)均为 0(它们的值都低于 80 十六进制)。
- 在异常响应中,从站设备会将功能码的 MSB 设置为 1。
- 这使得异常响应中的功能码值恰好比正常响应时的值高 80 十六进制。
- 通过设置功能码的 MSB,主站设备应用程序可以识别出异常响应,并可以检查数据字段中的异常代码。
数据字段:
- 在正常响应中,从站设备可能在数据字段中返回数据或统计信息(即请求中所要求的任何信息)。
- 在异常响应中,从站设备则在数据字段中返回一个异常代码。
- 该代码定义了导致此次异常的从站设备端的具体情况。
主站设备请求与从站设备异常响应示例:
| 请求 | 响应 | ||
| 字段名 | (十六进制) | 字段名 | (十六进制) |
| 功能码 | 01 | 功能码 | 81 |
| 起始地址高字节 | 04 | 异常代码 | 02 |
| 起始地址低字节 | A1 | ||
| 输出数量高字节 | 00 | ||
| 输出数量低字节 | 01 | ||
在这个示例中,主站设备向从站设备发送了一个请求。功能码(01)用于读取输出状态操作。它请求读取线圈地址 1185(04A1 十六进制) 的输出状态。
需要注意的是,此处设置的输出数量字段的值为 0001 ,因此只读取一个输出。
如果设置的地址在从站设备中不存在,从站设备将会返回异常码为 02 的异常响应。表示从站设备接收到的是非法数据地址。
6.5.2 异常码列表
异常码列表如下:
| 异常码 (十六进制) | 名称 | 产生原因举例 |
| 01 | 非法功能 (ILLEGAL FUNCTION) | 从站设备不支持请求报文中的功能码。例如,向一个只支持读保持寄存器(03)的传感器发送读线圈(01)命令。 |
| 02 | 非法数据地址 (ILLEGAL DATA ADDRESS) | 请求中指定的数据地址是从站设备不允许的或不存在的。例如,试图读取一个起始地址为999的保持寄存器,但该设备只有0-99的寄存器。 |
| 03 | 非法数据值 (ILLEGAL DATA VALUE) | 请求数据字段中的值对于从站设备来说是无效的。例如,在写多个寄存器(16)时,请求的寄存器数量为0,或者字节计数字节与后续数据不匹配。 |
| 04 | 服务器(从站)设备故障 (SERVER DEVICE FAILURE) | 从站设备在处理请求的过程中发生了不可恢复的错误。这是一个通用错误,通常表示设备内部故障,例如存储器故障、软件异常等。 |
| 05 | 确认 (ACKNOWLEDGE) | 从站设备已接受请求并正在处理中,但需要很长的处理时间。主站设备应稍后重新查询,而不是再次发送相同请求。 |
| 06 | 服务器(从站)设备忙 (SERVER DEVICE BUSY) | 从站设备正忙于处理长时间命令。主站设备应当稍后重试相同的请求。 |
| 08 | 存储器奇偶校验错误 (MEMORY PARITY ERROR) | 从站设备尝试读取记录文件,但检测到存储器中存在奇偶校验错误。主站设备可以重试请求,但从站设备可能需要进行检修。 |
| 0A | 网关路径不可用 (GATEWAY PATH UNAVAILABLE) | 通常与网关设备相关,表示网关配置错误或无法处理请求。 |
| 0B | 网关目标设备响应失败 (GATEWAY TARGET DEVICE FAILED TO RESPOND) | 网关无法从目标设备(即最终的被访问从站)获得响应。这通常意味着目标设备离线或存在通信问题。 |
第七部分:Modbus 通信协议模式
第七部分将详细介绍三种常见的 Modbus 通信协议模式以及扩展协议模式。
7.1 Modbus RTU 通信协议模式
Modbus RTU 是 Modbus 协议最早、最常用的串行传输模式,以其高效率和二进制传输著称,广泛应用于工业自动化领域。
7.1.1 物理层与网络基础
Modbus RTU 协议通常运行在串行通信物理层上,最常用的是 RS-485 标准,在某些点对点场景下也会使用 RS-232。
-
传输介质:
- RS-485:使用双绞线(屏蔽双绞线更佳)进行传输。支持多点通信,是构建工业设备网络的首选。
- RS-232:通常使用 DB9 或 DB25 连接器和电缆。主要用于点对点短距离通信。
-
网络拓扑:
- RS-485 网络:采用总线型拓扑(或称“手拉手”连接),所有设备并联在同一条总线上。网络两端需要安装终端电阻(通常为 120 欧姆)以抑制信号反射。
- RS-232 连接:严格的点对点拓扑,仅支持一台主站和一台从站通信。
-
核心关系:
- 协议:Modbus RTU(定义帧格式和通信规则)
- 物理层标准:RS-485(多设备网络)或 RS-232(点对点连接)
7.1.2 帧格式
Modbus RTU 协议报文(或称数据帧)由以下几个部分组成,它们按顺序连续传输,共同构成一次完整的通信:
| 从站地址 | 功能码 | 数据 | CRC-16校验 |
| 1 个字节 | 1 个字节 | 0 ~ 252 个字节 | 2 个字节 |
1. 从站地址
| 0 | 1 ~ 247 | 248 ~ 255 |
|---|---|---|
| 广播地址 | 从站地址 | 保留 |
-
该字段占 1 个字节。取值范围为 1 ~ 247(十进制)。
-
其中地址 0 被保留为广播地址,设置为广播地址时,所有从站设备都会接收并处理数据,但是不会向主站设备进行响应。
2. 功能码
- 该字段占 1 个字节。作用是指示从站设备执行操作,详细说明在“第六部分:Modbus 功能码”。
3. 数据
- 根据 Modbus RTU 帧格式规范,该字段占 0 ~ 252 个字节。实际长度和内容完全由功能码决定。它包含了请求的详细信息(例如: 要读取的寄存器起始地址和数量)或者要写入的具体数值。
4. CRC-16 校验
- 该字段占 2 个字节。CRC-16 校验即循环冗余校验码。由主站设备根据前面所有字节(从站地址、功能码、数据)计算得出,并附在报文末尾。从站设备收到报文后会用相同算法重新计算 CRC,并与接收到的 CRC 值进行比对。如果两者不匹配,则表明传输过程中发生错误,该帧报文将被丢失,从而保证数据完整性。
示例:
-
向单个保持寄存器写入数据:
- 请求报文格式:[从站地址][功能码][寄存器起始地址 寄存器值][CRC16 校验]
- 举例:01 06 00 00 00 01 48 0A
- 01 :从站地址
- 06 :向单个保持寄存器写入数据功能码
- 00 00 :寄存器起始地址
- 00 01 :写入的单个寄存器数据
- 48 0A :CRC16 校验值
- 响应报文格式:[从站地址][功能码][寄存器起始地址 寄存器值][CRC16 校验]
- 举例:01 06 00 00 00 01 48 0A
- 01 :从站地址
- 06 :向单个保持寄存器写入数据功能码
- 00 00 :寄存器起始地址
- 00 01 :写入的单个寄存器数据
- 48 0A :CRC16 校验值
- 异常响应报文格式:[从站地址][功能码][异常码][CRC16 校验]
- 举例:01 86 02 83 A0
- 01 :从站地址
- 86 :原功能码为 06,最高位为 1 表示异常响应
- 02 :非法数据地址(请求中指定的数据地址是从站设备不允许的或不存在的)
- 83 A0 :CRC16 校验值
-
向多个保持寄存器写入数据:
- 请求报文格式:[从站地址][功能码][寄存器起始地址 寄存器数量 寄存器值字节数 寄存器值][CRC16 校验]
- 举例:01 10 00 00 00 02 04 00 01 00 05 62 6C
- 01 :从站地址
- 10 :写多个寄存器功能码
- 00 00 :寄存器起始地址
- 00 02 :要写入的寄存器数量
- 04 :寄存器值的字节数
- 00 01 :写入的第一个寄存器值
- 00 05 :写入的第二个寄存器值
- 62 6C :CRC16 校验值
- 响应报文格式:[从站地址][功能码][寄存器起始地址 寄存器数量][CRC16 校验]
- 举例:01 10 00 00 00 02 41 C8
- 01 :从站地址
- 10 :写多个寄存器功能码
- 00 00 :寄存器起始地址
- 00 02 :要写入的寄存器数量
- 41 C8 :CRC16 校验值
- 异常响应报文格式:[从站地址][功能码][异常码][CRC16 校验]
- 举例:01 90 02 CD C1
- 01 :从站地址
- 90 :原功能码为 10,最高位为 1 表示异常响应
- 02 :非法数据地址(请求中指定的数据地址是从站设备不允许的或不存在的)
- CD C1 :CRC16 校验值
-
读取保持寄存器数据:
- 请求报文格式:[从站地址][功能码][寄存器起始地址 寄存器数量][CRC16 校验]
- 举例:01 03 00 00 00 02 C4 0B
- 01 :从站地址
- 03 :读单/多个保持寄存器功能码
- 00 00 :寄存器起始地址
- 00 02 :要读取的寄存器数量
- C4 0B :CRC16 校验值
- 响应报文格式:[从站地址][功能码][寄存器值字节数 寄存器值][CRC16 校验]
- 举例:01 03 04 00 01 00 05 6B F0
- 01 :从站地址
- 03 :读单/多个保持寄存器功能码
- 04 :寄存器值字节数
- 00 01 :读取的第一个寄存器值
- 00 05 :读取的第二个寄存器值
- 6B F0 :CRC16 校验值
- 异常响应报文格式:[从站地址][功能码][异常码][CRC16 校验]
- 举例:01 83 02 C0 F1
- 01 :从站地址
- 83 :原功能码为 03,最高位为 1 表示异常响应
- 02 :非法数据地址(请求中指定的数据地址是从站设备不允许的或不存在的)
- C0 F1 :CRC16 校验值
7.1.3 传输特点
1. 二进制传输
- 数据以原始的 8 位二进制字节流形式直接传输。在文档或调试中,每个字节通常用两个十六进制字符表示(如
0x4A),但这仅用于人类阅读,实际串口上不传输任何十六进制字符。该模式编码效率高,在相同波特率下比 ASCII 模式具有更高的数据吞吐量。
2. 波特率与同步
- 通信双方(主站设备和所有从站设备)必须设置为相同的波特率(如 9600, 19200 等),否则无法正确解码。
- 依靠起始位和停止位来实现字符帧的同步。每个字节都封装在起始位和停止位之间,接收方通过起始位来同步并读取后续的数据位。
3. 空闲间隔要求
- 报文帧之间必须保持一定的静默时间(总线空闲时间),该时间必须大于传输 3.5 个字符所需的时间。
- 帧内的字符间隔必须小于传输 1.5 个字符的时间。
- 这些要求是 RTU 模式区分报文开始与结束的关键机制(帧分隔),替代了 ASCII 模式中的特定起始/结束字符。
4. 特别说明:
由于需要遵循 t1.5 和 t3.5 定时器规则,Modbus RTU 接收驱动器可能会涉及大量中断处理。在高通信波特率下,将会导致较高的 CPU 负载。因此,当波特率等于或者低于 19200Baud 时,必须严格遵守这两个定时器的时限要求。对于大于 19200Baud 的波特率,这两个定时器应采用固定值:建议将字符间超时(t1.5)设置为 750us ,帧间延迟(t3.5)设置为 1.750ms 。
7.1.4 参数配置
在一个 Modbus RTU 模式中,所有设备的串行通信参数必须完全一致,否则无法通信。主要参数配置包括:
注:以下为官方文档描述,在实际使用时需要查阅设备手册,与设备手册中规定的参数一致。
1. 波特率 (Baud Rate) - 定义通信速度(比特每秒)。常见值有:9600、19200、38400 等。9600 是最常用的默认值。
2. 数据位 (Data Bits) - 固定为 8 位,最低有效位最先发送。这是 Modbus RTU 标准的规定。
3. 停止位 (Stop Bits) - 可以是 1 位 或 2 位。该设置与校验位的选择直接相关。
4. 校验位 (Parity) - 无 (None): 无奇偶校验。此时停止位必须设置为 2 位。 - 奇 (Odd): 奇校验。 - 偶 (Even): 偶校验。这是 Modbus 协议规定的默认和首选模式。当使用奇校验或偶校验时,停止位设置为 1 位。
7.2 Modbus ASCII(American Standard Code for Information Interchange)
Modbus ASCII 是 Modbus 协议的另一种串行传输模式,它采用可打印的 ASCII 字符编码,牺牲了效率但换来了极强的可读性,便于调试和诊断。
7.2.1 物理层与网络基础
与 Modbus RTU 相同,Modbus ASCII 协议也运行在串行通信物理层上,主要基于 RS-485 或 RS-232 标准。其物理层特性和网络拓扑与 RTU 模式完全一致。
- 传输介质:双绞线(用于 RS-485)或 串行电缆(用于 RS-232)。
-
网络拓扑:
- RS-485:总线型拓扑,需注意终端电阻。
- RS-232:点对点拓扑。
-
核心关系:
- 协议:Modbus ASCII(采用 ASCII 编码的 Modbus 协议)
- 物理层标准:RS-485 或 RS-232
注意:Modbus ASCII 和 Modbus RTU 可以共享相同的物理网络(如相同的 RS-485 总线),但绝不能在同一个网络上混合使用,因为它们的帧格式和编码完全不同,会导致通信失败。
7.2.2 帧格式
Modbus ASCII 协议报文有明确的开始和结束标志,其帧结构如下:
| 起始符 | 从站地址 | 功能码 | 数据 | LRC校验 | 结束符 |
| 1 个字符 | 2 个字符 | 2 个字符 | 0 ~ 2 × 252 个字符 | 2 个字符 | 2 个字符 |
1. 起始符
- 起始符字段占 1 个字符。固定为冒号字符
:(ASCII 值为0x3A)。 - 它标志着报文的开始,接收方通过检测起始符来同步并开始接收一个新帧。
2. 从站地址
- 从站地址字段占 2 个字符。使用 ASCII 字符表示。
- 例如,地址为
17(十六进制0x11) 的从站,在帧中会以两个字符'1''1'(ASCII 值为0x31,0x31) 表示。
3. 功能码
- 功能码字段占 2 个字符。使用 ASCII 字符表示。
- 例如,功能码
03(读保持寄存器) 会以两个字符'0''3'(ASCII 值为0x30,0x33) 表示。
4. 数据
- Modbus 协议规定,Modbus ASCII 模式中,数据字段占 0 ~ 2 × 252 个字符。使用 ASCII 字符表示。
- 每个 8 位二进制字节被拆分为两个 4 位半字节,并分别转换为对应的 ASCII 字符。
- 例如,数据字节
0x4B会转换为'4'(0x34) 和'B'(0x42) 两个字符进行传输。
5. LRC 校验
- LRC 校验字段占 2 个字符。
- LRC 校验即纵向冗余校验。发送方计算一个校验和,并将其转换为两个 ASCII 字符附加在报文末尾。接收方执行相同的计算进行验证。
6. 结束符
- 结束符字段占 2 个字节。固定为回车符和换行符
CRLF(ASCII 值为0x0D,0x0A)。它们标志着报文的结束。
示例:
-
向单个保持寄存器写入数据:
- 报文格式:[:][从站地址][功能码][寄存器起始地址 寄存器值][LRC 校验][\r\n]
- 举例::010600000001B8\r\n
- ‘:’ :起始符,HEX 值为 3A
- ‘0’ ‘1’ :从站地址,HEX 值为 30 31
- ‘0’ ‘6’ :向单个保持寄存器写入数据功能码,HEX 值为 30 36
- ‘0’ ‘0’ ‘0’ ‘0’ :寄存器起始地址,HEX 值为 30 30 30 30
- ‘0’ ‘0’ ‘0’ ‘1’ :写入的单个寄存器数据,HEX 值为 30 30 30 31
- ‘B’ ‘8’ :LRC 校验值,HEX 值为 42 38
- ‘\r’ ‘\n’ :结束符,HEX 值为 0D 0A
- 响应报文格式:[:][从站地址][功能码][寄存器起始地址 寄存器值][LRC 校验][\r\n]
- 举例::010600000001B8\r\n
- ‘:’ :起始符,HEX 值为 3A
- ‘0’ ‘1’ :从站地址,HEX 值为 30 31
- ‘0’ ‘6’ :向单个保持寄存器写入数据功能码,HEX 值为 30 36
- ‘0’ ‘0’ ‘0’ ‘0’ :寄存器起始地址,HEX 值为 30 30 30 30
- ‘0’ ‘0’ ‘0’ ‘1’ :写入的单个寄存器数据,HEX 值为 30 30 30 31
- ‘B’ ‘8’ :LRC 校验值,HEX 值为 42 38
- ‘\r’ ‘\n’ :结束符,HEX 值为 0D 0A
- 异常响应报文格式:[:][从站地址][功能码][异常码][LRC 校验][\r\n]
- 举例::018602CF\r\n
- ‘:’ :起始符,HEX 值为 3A
- ‘0’ ‘1’ :从站地址,HEX 值为 30 31
- ‘8’ ‘6’ :原功能码为 06,最高位为 1 表示异常响应,HEX 值为 38 36
- ‘0’ ‘2’ :非法数据地址(请求中指定的数据地址是从站设备不允许的或不存在的),HEX 值为 30 36
- ‘C’ ‘F’ :LRC 校验值,HEX 值为 43 46
- ‘\r’ ‘\n’ :结束符,HEX 值为 0D 0A
-
向多个保持寄存器写入数据:
- 请求报文格式:[:][从站地址][功能码][寄存器起始地址 寄存器数量 寄存器值字节数 寄存器值][LRC 校验][\r\n]
- 举例::0110000000020400010005D2\r\n
- ‘:’ :起始符,HEX 值为 3A
- ‘0’ ‘1’ :从站地址,HEX 值为 30 31
- ‘1’ ‘0’ :写多个寄存器功能码,HEX 值为 31 30
- ‘0’ ‘0’ ‘0’ ‘0’ :寄存器起始地址,HEX 值为 30 30 30 30
- ‘0’ ‘0’ ‘0’ ‘2’ :要写入的寄存器数量,HEX 值为 30 30 30 32
- ‘0’ ‘4’ :寄存器值的字节数,HEX 值为 30 34
- ‘0’ ‘0’ ‘0’ ‘1’ :写入的第一个寄存器值,HEX 值为 30 30 30 31
- ‘0’ ‘0’ ‘0’ ‘5’ :写入的第二个寄存器值,HEX 值为 30 30 30 35
- ‘D’ ‘2’ :LRC 校验值,HEX 值为 44 32
- ‘\r’ ‘\n’ :结束符,HEX 值为 0D 0A
- 响应报文格式:[:][从站地址][功能码][寄存器起始地址 寄存器数量][LRC 校验][\r\n]
- 举例::011000000002BC\r\n
- ‘:’ :起始符,HEX 值为 3A
- ‘0’ ‘1’ :从站地址,HEX 值为 30 31
- ‘1’ ‘0’ :写多个寄存器功能码,HEX 值为 31 30
- ‘0’ ‘0’ ‘0’ ‘0’ :寄存器起始地址,HEX 值为 30 30 30 30
- ‘0’ ‘0’ ‘0’ ‘2’ :要写入的寄存器数量,HEX 值为 30 30 30 32
- ‘B’ ‘C’ :LRC 校验值,HEX 值为 42 43
- ‘\r’ ‘\n’ :结束符,HEX 值为 0D 0A
- 异常响应报文格式:[:][从站地址][功能码][异常码][LRC 校验][\r\n]
- 举例::019002D4\r\n
- ‘:’ :起始符,HEX 值为 3A
- ‘0’ ‘1’ :从站地址,HEX 值为 30 31
- ‘9’ ‘0’ :原功能码为 10,最高位为 1 表示异常响应,HEX 值为 39 30
- ‘0’ ‘2’ :非法数据地址(请求中指定的数据地址是从站设备不允许的或不存在的),HEX 值为 30 36
- ‘D’ ‘4’ :LRC 校验值,HEX 值为 44 34
- ‘\r’ ‘\n’ :结束符,HEX 值为 0D 0A
-
读取保持寄存器数据:
- 请求报文格式:[:][从站地址][功能码][寄存器起始地址 寄存器数量][LRC 校验][\r\n]
- 举例::010300000002BA\r\n
- ‘:’ :起始符,HEX 值为 3A
- ‘0’ ‘1’ :从站地址,HEX 值为 30 31
- ‘0’ ‘3’ :读单/多个保持寄存器功能码,HEX 值为 30 33
- ‘0’ ‘0’ ‘0’ ‘0’ :寄存器起始地址,HEX 值为 30 30 30 30
- ‘0’ ‘0’ ‘0’ ‘2’ :要读取的寄存器数量,HEX 值为 30 30 30 32
- ‘B’ ‘A’ :LRC 校验值,HEX 值为 42 41
- ‘\r’ ‘\n’ :结束符,HEX 值为 0D 0A
- 响应报文格式:[:][从站地址][功能码][寄存器值字节数 寄存器值][LRC 校验][\r\n]
- 举例::0103040001000552\r\n
- ‘:’ :起始符,HEX 值为 3A
- ‘0’ ‘1’ :从站地址,HEX 值为 30 31
- ‘0’ ‘3’ :读单/多个保持寄存器功能码,HEX 值为 30 33
- ‘0’ ‘4’ :寄存器值字节数,HEX 值为 30 34
- ‘0’ ‘0’ ‘0’ ‘1’ :读取的第一个寄存器值,HEX 值为 30 30 30 31
- ‘0’ ‘0’ ‘0’ ‘5’ :读取的第二个寄存器值,HEX 值为 30 30 30 35
- ‘5’ ‘2’ :LRC 校验值,HEX 值为 35 32
- ‘\r’ ‘\n’ :结束符,HEX 值为 0D 0A
- 异常响应报文格式:[:][从站地址][功能码][异常码][LRC 校验][\r\n]
- 举例::018302D2\r\n
- ‘:’ :起始符,HEX 值为 3A
- ‘0’ ‘1’ :从站地址,HEX 值为 30 31
- ‘8’ ‘3’ :原功能码为 03,最高位为 1 表示异常响应,HEX 值为 38 33
- ‘0’ ‘2’ :非法数据地址(请求中指定的数据地址是从站设备不允许的或不存在的),HEX 值为 30 36
- ‘D’ ‘2’ :LRC 校验值,HEX 值为 44 32
- ‘\r’ ‘\n’ :结束符,HEX 值为 0D 0A
7.2.3 传输特点
1. ASCII 字符传输
-
所有数据,包括地址、命令和校验值,都以可打印的 ASCII 字符(0-9, A-F)形式在网络上传输。
-
这使得任何能够显示 ASCII 码的终端软件或简单的调试工具都可以直接监控、解析和调试通信过程,数据一目了然。
2. 效率低但可读性强
-
效率低:这是 ASCII 模式最大的缺点。因为每个信息字节都需要用两个 ASCII 字符来传输,有效数据的吞吐量仅为 RTU 模式的一半。例如,传输一个二进制值
0xFE,RTU 只需传 1 个字节(0xFE),而 ASCII 需要传'F''E'这 2 个字节(0x46,0x45)。 -
可读性强:这是其最大优点。人类可以直接读懂线上的数据。一个典型的 ASCII 帧看起来像:
:1103006B0003E9<CR><LF>,很容易识别出地址、功能码和数据。
7.2.4 参数配置
在一个 Modbus ASCII 模式中,所有设备的串行通信参数必须完全一致,否则无法通信。主要参数配置包括:
注:以下为官方文档描述,在实际使用时需要查阅设备手册,与设备手册中规定的参数一致。
1. 波特率 (Baud Rate) - 定义通信速度(比特每秒)。常见值有:9600、19200、38400 等。9600 是最常用的默认值。
2. 数据位 (Data Bits) - 固定为 7 位,最低有效位最先发送。这是 Modbus ASCII 标准的规定。
3. 停止位 (Stop Bits) - 可以是 1 位 或 2 位。该设置与校验位的选择直接相关。
4. 校验位 (Parity)
-
无 (None): 无奇偶校验。此时停止位必须设置为 2 位。
-
奇 (Odd): 奇校验。
-
偶 (Even): 偶校验。这是 Modbus 协议规定的默认和首选模式。当使用奇校验或偶校验时,停止位设置为 1 位。
7.3 Modbus TCP(Transmission Control Protocol)
Modbus TCP 是 Modbus 协议家族中为适应现代工业以太网环境而发展的变种。它将传统的 Modbus 应用层协议封装在标准的 TCP/IP 协议栈中,使其能够通过以太网进行通信,从而实现了高速、远距离、多设备的网络化集成。
7.3.1 物理层与网络基础
Modbus TCP 运行在完整的 TCP/IP 协议栈之上,其物理层依赖于标准的以太网技术。
-
物理介质:
- 双绞线(如 CAT5e、CAT6):最常见的选择,通信距离约 100 米。
- 光纤:用于长距离通信(可达数公里)和高电磁干扰环境。
- 无线:通过 Wi-Fi 等无线以太网技术实现灵活部署。
-
网络拓扑:
- 支持星型、环型(需配合交换机协议)等复杂拓扑。
- 通过交换机和路由器可以轻松组建大规模、分层级的网络。
-
协议栈关系:
- 应用层:Modbus 协议
- 传输层:TCP(提供可靠连接)
- 网络层:IP(处理寻址和路由)
- 数据链路层/物理层:以太网(如 IEEE 802.3 标准)
7.3.2 帧格式
Modbus TCP 协议报文在标准的 Modbus 协议数据单元(PDU)前添加了 MBAP 头,形成完整的应用数据单元(ADU)。其帧结构如下:
| MBAP头 | 功能码 | 数据 |
| 7 个字节 | 1 个字节 | 0 ~ 252 个字节 |
1. MBAP 头 (MBAP Header - Modbus Application Protocol Header)
-
共 7 字节,专门为 TCP/IP 传输设计,包含路由和管理信息。
-
事务标识符 (Transaction Identifier)
- 该字段占 2 个字节。由客户端(主站)生成,用于将请求与响应配对。服务器(从站)在回应中必须复制相同的事务标识符。这在异步通信中尤为重要,可以区分多个并发请求的响应。
-
协议标识符 (Protocol Identifier)
- 该字段占 2 个字节。对于 Modbus TCP,此值固定为
0x0000。其他值保留用于未来扩展。
- 该字段占 2 个字节。对于 Modbus TCP,此值固定为
-
长度 (Length)
- 该字段占 2 个字节。表示其后所有字节的数量,包括单元标识符(1 个字节)、功能码(1 个字节) 和数据域(0 ~ 252 个字节)。
- 计算公式:
长度 = 1 (单元标识符) + 1 (功能码) + N (数据长度)
-
单元标识符 (Unit Identifier)
- 该字段占 1 个字节。用于标识连接在网关后面的子设备。在简单的单设备网络中,此字段常被用作从站地址,功能与 Modbus RTU/ASCII 中的从站地址相同。如果不需要网关路由,通常设置为
0x00或0xFF,或者直接使用从站地址(如0x01)。
- 该字段占 1 个字节。用于标识连接在网关后面的子设备。在简单的单设备网络中,此字段常被用作从站地址,功能与 Modbus RTU/ASCII 中的从站地址相同。如果不需要网关路由,通常设置为
2. 功能码 (Function Code)
- 该字段占 1 个字节。与 Modbus RTU/ASCII 中的功能码完全一致。例如:01(读线圈)、03(读保持寄存器)、05(写单个线圈)、06(写单个寄存器)等。
3. 数据 (Data) - 该字段占 0 ~ 252 个字节。含义和内容与 Modbus RTU 完全相同,由功能码决定。包含请求或响应的具体参数或数值。
示例:
-
向单个保持寄存器写入数据:
- 请求报文格式:[事务标识符 协议标识符 后续字节数 从站地址][功能码][寄存器起始地址 寄存器值]
- 举例:00 01 00 00 00 06 01 06 00 00 00 01
- 00 01 :事务标识符
- 00 00 :协议标识符(modbus 固定为 00 00)
- 00 06 :后续字节数
- 01 :从站地址
- 06 :向单个保持寄存器写入数据功能码
- 00 00 :寄存器起始地址
- 00 01 :写入的单个寄存器数据
- 响应报文格式:[事务标识符 协议标识符 后续字节数 从站地址][功能码][寄存器起始地址 寄存器值]
- 举例:00 01 00 00 00 06 01 06 00 00 00 01
- 00 01 :事务标识符
- 00 00 :协议标识符(modbus 固定为 00 00)
- 00 06 :后续字节数
- 01 :从站地址
- 06 :向单个保持寄存器写入数据功能码
- 00 00 :寄存器起始地址
- 00 01 :写入的单个寄存器数据
- 异常响应报文格式:[事务标识符 协议标识符 后续字节数 从站地址][功能码][异常码]
- 举例:00 01 00 00 00 03 01 86 02
- 00 01 :事务标识符
- 00 00 :协议标识符(modbus 固定为 00 00)
- 00 03 :后续字节数
- 01 :从站地址
- 86 :原功能码为 06,最高位为 1 表示异常响应
- 02 :非法数据地址(请求中指定的数据地址是从站设备不允许的或不存在的)
-
向多个保持寄存器写入数据:
- 请求报文格式:[事务标识符 协议标识符 后续字节数 从站地址][功能码][寄存器起始地址 寄存器数量 寄存器值字节数 寄存器值]
- 举例:00 01 00 00 00 0B 01 10 00 00 00 02 04 00 01 00 05
- 00 01 :事务标识符
- 00 00 :协议标识符(modbus 固定为 00 00)
- 00 0B :后续字节数
- 01 :从站地址
- 10 :写多个寄存器功能码
- 00 00 :寄存器起始地址
- 00 02 :要写入的寄存器数量
- 04 :寄存器值的字节数
- 00 01 :写入的第一个寄存器值
- 00 05 :写入的第二个寄存器值
- 响应报文格式:[事务标识符 协议标识符 后续字节数 从站地址][功能码][寄存器起始地址 寄存器数量]
- 举例:00 01 00 00 00 06 01 10 00 00 00 02
- 00 01 :事务标识符
- 00 00 :协议标识符(modbus 固定为 00 00)
- 00 06 :后续字节数
- 01 :从站地址
- 10 :写多个寄存器功能码
- 00 00 :寄存器起始地址
- 00 02 :要写入的寄存器数量
- 异常响应报文格式:[事务标识符 协议标识符 后续字节数 从站地址][功能码][异常码]
- 举例:00 01 00 00 00 03 01 90 02
- 00 01 :事务标识符
- 00 00 :协议标识符(modbus 固定为 00 00)
- 00 03 :后续字节数
- 01 :从站地址
- 90 :原功能码为 10,最高位为 1 表示异常响应
- 02 :非法数据地址(请求中指定的数据地址是从站设备不允许的或不存在的)
-
读取保持寄存器数据:
- 请求报文格式:[事务标识符 协议标识符 后续字节数 从站地址][功能码][寄存器起始地址 寄存器数量]
- 举例:00 01 00 00 00 06 01 03 00 00 00 02
- 00 01 :事务标识符
- 00 00 :协议标识符(modbus 固定为 00 00)
- 00 06 :后续字节数
- 01 :从站地址
- 03 :读单/多个保持寄存器功能码
- 00 00 :寄存器起始地址
- 00 02 :要读取的寄存器数量
- 响应报文格式:[事务标识符 协议标识符 后续字节数 从站地址][功能码][寄存器值字节数 寄存器值]
- 举例:00 01 00 00 00 07 01 03 04 00 01 00 05
- 00 01 :事务标识符
- 00 00 :协议标识符(modbus 固定为 00 00)
- 00 07 :后续字节数
- 01 :从站地址
- 03 :读单/多个保持寄存器功能码
- 00 01 :读取的第一个寄存器值
- 00 05 :读取的第二个寄存器值
- 异常响应报文格式:[事务标识符 协议标识符 后续字节数 从站地址][功能码][异常码]
- 举例:00 01 00 00 00 03 01 83 02
- 00 01 :事务标识符
- 00 00 :协议标识符(modbus 固定为 00 00)
- 00 03 :后续字节数
- 01 :从站地址
- 83 :原功能码为 03,最高位为 1 表示异常响应
- 02 :非法数据地址(请求中指定的数据地址是从站设备不允许的或不存在的)
7.3.3 传输特点
1. 基于以太网/TCP/IP
-
Modbus TCP 运行在标准的以太网物理层和数据链路层之上,使用 TCP(传输控制协议)作为传输层协议。
-
这使得它可以利用现有的企业局域网(LAN)和互联网基础设施进行通信,传输距离远,布线方便。
2. 端口 502
- 虽然标准规定使用 502 端口,但许多设备允许用户自定义其他端口号以适应特定的网络安全策略(如防火墙规则)。然而,502 端口是 Modbus TCP 的默认和标准端口,在未明确指定的情况下应使用此端口。
3. 无校验(依赖 TCP)
-
取消了 CRC 校验。这是因为 TCP 协议本身已经提供了连接导向、数据包排序、重传机制和错误校验,确保了数据的可靠、有序和无误传输。在可靠的 TCP 连接上增加 CRC 校验是冗余的。
-
因此,Modbus TCP 帧结构比 RTU 帧更简洁,效率更高。
4. 客户端/服务器模型
- Modbus TCP 将传统的主站(Master)改称为客户端(Client),从站(Slave)改称为服务器(Server)。
- 客户端主动向服务器发起 TCP 连接并发送请求,服务器被动监听并响应请求。一个服务器可以同时处理多个客户端的连接。
7.3.4 参数配置
在 Modbus TCP 模式中,通信双方需要配置正确的网络参数才能建立连接。主要参数包括:
1. IP 地址 (IP Address)
- 每个设备(客户端和服务器)必须在网络中拥有唯一的 IP 地址。
2. 子网掩码 (Subnet Mask)
- 用于判断设备是否位于同一局域网内。
3. 网关 (Gateway)
- 当需要与不同网段的设备通信时,需要设置网关地址。
4. TCP 端口号 (TCP Port)
- 服务器监听的端口号,默认为 502。客户端需要向服务器的 IP 地址和此端口发起连接。
5. 连接超时 (Connection Timeout)
- 客户端等待服务器建立连接或响应的最长时间。
7.4 扩展协议模式
除了最常见的 Modbus RTU、Modbus ASCII 和 Modbus TCP 之外,还存在其他一些协议形式或变种。 它们主要是为了适应不同的物理介质或满足特定的行业需求。
| 协议形式 | 物理层/网络 | 主要特点 | 典型应用场景 |
| Modbus RTU | RS-485/RS-232 | 二进制编码、CRC校验、效率高 | 工业现场、设备级通信 |
| Modbus ASCII | RS-485/RS-232 | ASCII字符编码、LRC校验、可读性好 | 调试、诊断(较少使用) |
| Modbus TCP/IP | 以太网 (TCP/IP) | 基于以太网、MBAP报文头、无需校验 | 控制系统集成、工业物联网 |
| Modbus Plus | 专用令牌网 (1Mbps) | proprietary(莫迪康专有)、高速、令牌环 | 施耐德/莫迪康特定系统 |
| Modbus over UDP | 以太网 (UDP/IP) | 基于无连接的UDP、实时性稍好 | 对实时性有要求的部分局域网应用 |
| Modbus Secure | 同Modbus TCP/IP | 在Modbus TCP基础上增加了TLS加密 | 对安全性有较高要求的工业环境 |
| Modbus over TLS/SSL | 同Modbus TCP/IP | 类似Modbus Secure,提供通信加密 | 需要安全传输的远程监控 |
第八部分:LuatOS 上的 exmodbus 扩展库
docs 链接:https://docs.openluat.com/osapi/ext/exmodbus/
8.1 常量详解
8.1.1 通信模式常量
8.1.1.1 exmodbus.RTU_MASTER
常量含义:modbus RTU 主站;
数据类型:number;
示例代码:-- 创建 modbus RTU 主站
local create_config = {
-- 串口配置参数;
mode = exmodbus.RTU_MASTER, -- 通信模式
uart_id = 1, -- UART 端口号
baud_rate = 115200, -- 波特率
data_bits = 8, -- 数据位
stop_bits = 1, -- 停止位
parity_bits = uart.None, -- 校验位
byte_order = uart.LSB, -- 字节顺序
rs485_dir_gpio = 23, -- RS485 方向引脚
rs485_dir_rx_level = 0, -- RS485 接收方向电平
}
local rtu_master = exmodbus.create(create_config)
8.1.1.2 exmodbus.RTU_SLAVE
常量含义:modbus RTU 从站;
数据类型:number;
示例代码:-- 创建 modbus RTU 从站
local create_config = {
-- 串口配置参数;
mode = exmodbus.RTU_SLAVE, -- 通信模式
uart_id = 1, -- UART 端口号
baud_rate = 115200, -- 波特率
data_bits = 8, -- 数据位
stop_bits = 1, -- 停止位
parity_bits = uart.None, -- 校验位
byte_order = uart.LSB, -- 字节顺序
rs485_dir_gpio = 23, -- RS485 方向引脚
rs485_dir_rx_level = 0, -- RS485 接收方向电平
}
local rtu_slave = exmodbus.create(create_config)
8.1.1.3 exmodbus.ASCII_MASTER
常量含义:modbus ASCII 主站;
数据类型:number;
示例代码:-- 创建 modbus ASCII 主站
local create_config = {
-- 串口配置参数;
mode = exmodbus.ASCII_MASTER, -- 通信模式
uart_id = 1, -- UART 端口号
baud_rate = 115200, -- 波特率
data_bits = 8, -- 数据位
stop_bits = 1, -- 停止位
parity_bits = uart.None, -- 校验位
byte_order = uart.LSB, -- 字节顺序
rs485_dir_gpio = 23, -- RS485 方向引脚
rs485_dir_rx_level = 0, -- RS485 接收方向电平
}
local ascii_master = exmodbus.create(create_config)
8.1.1.4 exmodbus.ASCII_SLAVE
常量含义:modbus ASCII 从站;
数据类型:number;
示例代码:-- 创建 modbus ascii 从站
local create_config = {
-- 串口配置参数;
mode = exmodbus.ASCII_SLAVE, -- 通信模式
uart_id = 1, -- UART 端口号
baud_rate = 115200, -- 波特率
data_bits = 8, -- 数据位
stop_bits = 1, -- 停止位
parity_bits = uart.None, -- 校验位
byte_order = uart.LSB, -- 字节顺序
rs485_dir_gpio = 23, -- RS485 方向引脚
rs485_dir_rx_level = 0, -- RS485 接收方向电平
}
local rtu_slave = exmodbus.create(create_config)
8.1.1.5 exmodbus.TCP_MASTER
常量含义:modbus TCP 主站(客户端);
数据类型:number;
示例代码:-- 创建 modbus tcp 主站
local create_config = {
-- 网络参数配置
mode = exmodbus.TCP_MASTER, -- 通信模式:TCP主站
adapter = socket.LWIP_ETH, -- 网卡 ID:LwIP 协议栈的以太网卡
ip_address = "192.168.1.100", -- 从站 IP 地址
port = 6000, -- 从站端口号
}
8.1.1.6 exmodbus.TCP_SLAVE
常量含义:modbus TCP 从站(服务器);
数据类型:number;
示例代码:-- 创建 modbus tcp 从站
local create_config = {
-- 网络配置参数;
mode = exmodbus.TCP_SLAVE, -- 通信模式
adapter = socket.LWIP_ETH, -- 网卡 ID:LwIP 协议栈的以太网卡
port = 6000, -- 监听本地端口
}
local tcp_slave = exmodbus.create(create_config)
8.1.2 数据类型常量
8.1.2.1 exmodbus.COIL_STATUS
常量含义:线圈;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 1 线圈 0-1 的状态时,配置读命令的字段参数
local read_config = {
slave_id = 1, -- 从站地址 1
reg_type = exmodbus.COIL_STATUS, -- 寄存器类型:线圈
start_addr = 0x0000, -- 起始地址 0
reg_count = 0x0002, -- 读取 2 个寄存器
timeout = 1000 -- 超时时间 1000 ms
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
8.1.2.2 exmodbus.INPUT_STATUS
常量含义:离散输入;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 1 离散输入 0-1 的状态时,配置读命令的字段参数
local read_config = {
slave_id = 1, -- 从站地址 1
reg_type = exmodbus.INPUT_STATUS, -- 寄存器类型:离散输入
start_addr = 0x0000, -- 起始地址 0
reg_count = 0x0002, -- 读取 2 个寄存器
timeout = 1000 -- 超时时间 1000 ms
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
8.1.2.3 exmodbus.INPUT_REGISTER
常量含义:输入寄存器;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 1 输入寄存器 0-1 的值时,配置读命令的字段参数
local read_config = {
slave_id = 1, -- 从站地址 1
reg_type = exmodbus.INPUT_REGISTER, -- 寄存器类型:输入寄存器
start_addr = 0x0000, -- 起始地址 0
reg_count = 0x0002, -- 读取 2 个寄存器
timeout = 1000 -- 超时时间 1000 ms
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
8.1.2.4 exmodbus.HOLDING_REGISTER
常量含义:保持寄存器;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 1 保持寄存器 0-1 的值时,配置读命令的字段参数
local read_config = {
slave_id = 1, -- 从站地址 1
reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
start_addr = 0x0000, -- 起始地址 0
reg_count = 0x0002, -- 读取 2 个寄存器
timeout = 1000 -- 超时时间 1000 ms
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
8.1.3 操作类型常量
8.1.3.1 exmodbus.READ_COILS
常量含义:读线圈状态;
数据类型:number;
示例代码:-- 1. 创建从站
-- 2. 接收主站请求时,判断收到的请求属于哪种操作类型
local function callback(request)
log.info("exmodbus_test", "rtu_slave 收到主站请求")
if request.func_code == exmodbus.READ_COILS then
log.info("exmodbus_test", "操作类型为读线圈状态")
-- 用户代码
end
end
modbus:on(callback)
8.1.3.2 exmodbus.READ_DISCRETE_INPUTS
常量含义:读离散输入状态;
数据类型:number;
示例代码:-- 1. 创建从站
-- 2. 接收主站请求时,判断收到的请求属于哪种操作类型
local function callback(request)
log.info("exmodbus_test", "rtu_slave 收到主站请求")
if request.func_code == exmodbus.READ_DISCRETE_INPUTS then
log.info("exmodbus_test", "操作类型为读离散输入状态")
-- 用户代码
end
end
modbus:on(callback)
8.1.3.3 exmodbus.READ_HOLDING_REGISTERS
常量含义:读保持寄存器;
数据类型:number;
示例代码:-- 1. 创建从站
-- 2. 接收主站请求时,判断收到的请求属于哪种操作类型
local function callback(request)
log.info("exmodbus_test", "rtu_slave 收到主站请求")
if request.func_code == exmodbus.READ_HOLDING_REGISTERS then
log.info("exmodbus_test", "操作类型为读保持寄存器")
-- 用户代码
end
end
modbus:on(callback)
8.1.3.4 exmodbus.READ_INPUT_REGISTERS
常量含义:读输入寄存器;
数据类型:number;
示例代码:-- 1. 创建从站
-- 2. 接收主站请求时,判断收到的请求属于哪种操作类型
local function callback(request)
log.info("exmodbus_test", "rtu_slave 收到主站请求")
if request.func_code == exmodbus.READ_INPUT_REGISTERS then
log.info("exmodbus_test", "操作类型为读输入寄存器")
-- 用户代码
end
end
modbus:on(callback)
8.1.3.5 exmodbus.WRITE_SINGLE_COIL
常量含义:写单个线圈状态;
数据类型:number;
示例代码:-- 1. 创建从站
-- 2. 接收主站请求时,判断收到的请求属于哪种操作类型
local function callback(request)
log.info("exmodbus_test", "rtu_slave 收到主站请求")
if request.func_code == exmodbus.WRITE_SINGLE_COIL then
log.info("exmodbus_test", "操作类型为写单个线圈状态")
-- 用户代码
end
end
modbus:on(callback)
8.1.3.6 exmodbus.WRITE_SINGLE_HOLDING_REGISTER
常量含义:写单个保持寄存器;
数据类型:number;
示例代码:-- 1. 创建从站
-- 2. 接收主站请求时,判断收到的请求属于哪种操作类型
local function callback(request)
log.info("exmodbus_test", "rtu_slave 收到主站请求")
if request.func_code == exmodbus.WRITE_SINGLE_HOLDING_REGISTER then
log.info("exmodbus_test", "操作类型为写单个保持寄存器")
-- 用户代码
end
end
modbus:on(callback)
8.1.3.7 exmodbus.WRITE_MULTIPLE_HOLDING_REGISTERS
常量含义:写多个保持寄存器;
数据类型:number;
示例代码:-- 1. 创建从站
-- 2. 接收主站请求时,判断收到的请求属于哪种操作类型
local function callback(request)
log.info("exmodbus_test", "rtu_slave 收到主站请求")
if request.func_code == exmodbus.WRITE_MULTIPLE_HOLDING_REGISTERS then
log.info("exmodbus_test", "操作类型为写多个保持寄存器")
-- 用户代码
end
end
modbus:on(callback)
8.1.3.8 exmodbus.WRITE_MULTIPLE_COILS
常量含义:写多个线圈状态;
数据类型:number;
示例代码:-- 1. 创建从站
-- 2. 接收主站请求时,判断收到的请求属于哪种操作类型
local function callback(request)
log.info("exmodbus_test", "rtu_slave 收到主站请求")
if request.func_code == exmodbus.WRITE_MULTIPLE_COILS then
log.info("exmodbus_test", "操作类型为写多个线圈状态")
-- 用户代码
end
end
modbus:on(callback)
8.1.4 异常码常量
8.1.4.1 exmodbus.ILLEGAL_FUNCTION
常量含义:不支持请求的功能码;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 1 保持寄存器 0-1 的值时,配置读命令的字段参数
local read_config = {
slave_id = 1, -- 从站地址 1
reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
start_addr = 0x0000, -- 起始地址 0
reg_count = 0x0002, -- 读取 2 个寄存器
timeout = 1000 -- 超时时间 1000 ms
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
-- 4. 判断从站响应状态
if read_result.status == exmodbus.STATUS_EXCEPTION then
log.info("exmodbus_test", "收到从站 1 的 modbus 标准异常响应,异常码为", read_result.execption_code)
if read_result.execption_code == exmodbus.ILLEGAL_FUNCTION then
log.info("exmodbus_test", "不支持请求的功能码")
end
end
8.1.4.2 exmodbus.ILLEGAL_DATA_ADDRESS
常量含义:请求的数据地址无效或超出范围;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 1 保持寄存器 0-1 的值时,配置读命令的字段参数
local read_config = {
slave_id = 1, -- 从站地址 1
reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
start_addr = 0x0000, -- 起始地址 0
reg_count = 0x0002, -- 读取 2 个寄存器
timeout = 1000 -- 超时时间 1000 ms
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
-- 4. 判断从站响应状态
if read_result.status == exmodbus.STATUS_EXCEPTION then
log.info("exmodbus_test", "收到从站 1 的 modbus 标准异常响应,异常码为", read_result.execption_code)
if read_result.execption_code == exmodbus.ILLEGAL_DATA_ADDRESS then
log.info("exmodbus_test", "请求的数据地址无效或超出范围")
end
end
8.1.4.3 exmodbus.ILLEGAL_DATA_VALUE
常量含义:请求的数据值无效;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 1 保持寄存器 0-1 的值时,配置读命令的字段参数
local read_config = {
slave_id = 1, -- 从站地址 1
reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
start_addr = 0x0000, -- 起始地址 0
reg_count = 0x0002, -- 读取 2 个寄存器
timeout = 1000 -- 超时时间 1000 ms
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
-- 4. 判断从站响应状态
if read_result.status == exmodbus.STATUS_EXCEPTION then
log.info("exmodbus_test", "收到从站 1 的 modbus 标准异常响应,异常码为", read_result.execption_code)
if read_result.execption_code == exmodbus.ILLEGAL_DATA_VALUE then
log.info("exmodbus_test", "请求的数据值无效")
end
end
8.1.4.4 exmodbus.SLAVE_DEVICE_FAILURE
常量含义:从站在执行操作时发生内部错误;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 1 保持寄存器 0-1 的值时,配置读命令的字段参数
local read_config = {
slave_id = 1, -- 从站地址 1
reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
start_addr = 0x0000, -- 起始地址 0
reg_count = 0x0002, -- 读取 2 个寄存器
timeout = 1000 -- 超时时间 1000 ms
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
-- 4. 判断从站响应状态
if read_result.status == exmodbus.STATUS_EXCEPTION then
log.info("exmodbus_test", "收到从站 1 的 modbus 标准异常响应,异常码为", read_result.execption_code)
if read_result.execption_code == exmodbus.SLAVE_DEVICE_FAILURE then
log.info("exmodbus_test", "从站在执行操作时发生内部错误")
end
end
8.1.4.5 exmodbus.ACKNOWLEDGE
常量含义:请求已接受,但需要长时间处理;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 1 保持寄存器 0-1 的值时,配置读命令的字段参数
local read_config = {
slave_id = 1, -- 从站地址 1
reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
start_addr = 0x0000, -- 起始地址 0
reg_count = 0x0002, -- 读取 2 个寄存器
timeout = 1000 -- 超时时间 1000 ms
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
-- 4. 判断从站响应状态
if read_result.status == exmodbus.STATUS_EXCEPTION then
log.info("exmodbus_test", "收到从站 1 的 modbus 标准异常响应,异常码为", read_result.execption_code)
if read_result.execption_code == exmodbus.ACKNOWLEDGE then
log.info("exmodbus_test", "请求已接受,但需要长时间处理")
end
end
8.1.4.6 exmodbus.SLAVE_DEVICE_BUSY
常量含义:从站正忙,无法处理请求;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 1 保持寄存器 0-1 的值时,配置读命令的字段参数
local read_config = {
slave_id = 1, -- 从站地址 1
reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
start_addr = 0x0000, -- 起始地址 0
reg_count = 0x0002, -- 读取 2 个寄存器
timeout = 1000 -- 超时时间 1000 ms
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
-- 4. 判断从站响应状态
if read_result.status == exmodbus.STATUS_EXCEPTION then
log.info("exmodbus_test", "收到从站 1 的 modbus 标准异常响应,异常码为", read_result.execption_code)
if read_result.execption_code == exmodbus.SLAVE_DEVICE_BUSY then
log.info("exmodbus_test", "从站正忙,无法处理请求")
end
end
8.1.4.7 exmodbus.NEGATIVE_ACKNOWLEDGE
常量含义:无法执行编程功能;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 1 保持寄存器 0-1 的值时,配置读命令的字段参数
local read_config = {
slave_id = 1, -- 从站地址 1
reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
start_addr = 0x0000, -- 起始地址 0
reg_count = 0x0002, -- 读取 2 个寄存器
timeout = 1000 -- 超时时间 1000 ms
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
-- 4. 判断从站响应状态
if read_result.status == exmodbus.STATUS_EXCEPTION then
log.info("exmodbus_test", "收到从站 1 的 modbus 标准异常响应,异常码为", read_result.execption_code)
if read_result.execption_code == exmodbus.NEGATIVE_ACKNOWLEDGE then
log.info("exmodbus_test", "无法执行编程功能")
end
end
8.1.4.8 exmodbus.MEMORY_PARITY_ERROR
常量含义:内存奇偶校验错误;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 1 保持寄存器 0-1 的值时,配置读命令的字段参数
local read_config = {
slave_id = 1, -- 从站地址 1
reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
start_addr = 0x0000, -- 起始地址 0
reg_count = 0x0002, -- 读取 2 个寄存器
timeout = 1000 -- 超时时间 1000 ms
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
-- 4. 判断从站响应状态
if read_result.status == exmodbus.STATUS_EXCEPTION then
log.info("exmodbus_test", "收到从站 1 的 modbus 标准异常响应,异常码为", read_result.execption_code)
if read_result.execption_code == exmodbus.MEMORY_PARITY_ERROR then
log.info("exmodbus_test", "内存奇偶校验错误")
end
end
8.1.4.9 exmodbus.GATEWAY_PATH_UNAVAILABLE
常量含义:网关路径不可用;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 1 保持寄存器 0-1 的值时,配置读命令的字段参数
local read_config = {
slave_id = 1, -- 从站地址 1
reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
start_addr = 0x0000, -- 起始地址 0
reg_count = 0x0002, -- 读取 2 个寄存器
timeout = 1000 -- 超时时间 1000 ms
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
-- 4. 判断从站响应状态
if read_result.status == exmodbus.STATUS_EXCEPTION then
log.info("exmodbus_test", "收到从站 1 的 modbus 标准异常响应,异常码为", read_result.execption_code)
if read_result.execption_code == exmodbus.GATEWAY_PATH_UNAVAILABLE then
log.info("exmodbus_test", "网关路径不可用")
end
end
8.1.4.10 exmodbus.GATEWAY_TARGET_NO_RESPONSE
常量含义:网关目标设备无响应;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 1 保持寄存器 0-1 的值时,配置读命令的字段参数
local read_config = {
slave_id = 1, -- 从站地址 1
reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
start_addr = 0x0000, -- 起始地址 0
reg_count = 0x0002, -- 读取 2 个寄存器
timeout = 1000 -- 超时时间 1000 ms
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
-- 4. 判断从站响应状态
if read_result.status == exmodbus.STATUS_EXCEPTION then
log.info("exmodbus_test", "收到从站 1 的 modbus 标准异常响应,异常码为", read_result.execption_code)
if read_result.execption_code == exmodbus.GATEWAY_TARGET_NO_RESPONSE then
log.info("exmodbus_test", "网关目标设备无响应")
end
end
8.1.5 响应结果常量
8.1.5.1 exmodbus.STATUS_SUCCESS
常量含义:收到响应数据且数据有效;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 1 保持寄存器 0-1 的值时,配置读命令的字段参数
local read_config = {
slave_id = 1, -- 从站地址 1
reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
start_addr = 0x0000, -- 起始地址 0
reg_count = 0x0002, -- 读取 2 个寄存器
timeout = 1000 -- 超时时间 1000 ms
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
-- 4. 判断从站响应状态
if read_result.status == exmodbus.STATUS_SUCCESS then
log.info("exmodbus_test", "收到响应数据且数据有效")
-- 用户代码
end
8.1.5.2 exmodbus.STATUS_DATA_INVALID
常量含义:收到响应数据但数据损坏/校验失败;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 1 保持寄存器 0-1 的值时,配置读命令的字段参数
local read_config = {
slave_id = 1, -- 从站地址 1
reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
start_addr = 0x0000, -- 起始地址 0
reg_count = 0x0002, -- 读取 2 个寄存器
timeout = 1000 -- 超时时间 1000 ms
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
-- 4. 判断从站响应状态
if read_result.status == exmodbus.STATUS_DATA_INVALID then
log.info("exmodbus_test", "收到响应数据但数据损坏/校验失败")
-- 用户代码
end
8.1.5.3 exmodbus.STATUS_EXCEPTION
常量含义:收到 modbus 标准异常响应;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 1 保持寄存器 0-1 的值时,配置读命令的字段参数
local read_config = {
slave_id = 1, -- 从站地址 1
reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
start_addr = 0x0000, -- 起始地址 0
reg_count = 0x0002, -- 读取 2 个寄存器
timeout = 1000 -- 超时时间 1000 ms
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
-- 4. 判断从站响应状态
if read_result.status == exmodbus.STATUS_EXCEPTION then
log.info("exmodbus_test", "收到 modbus 标准异常响应")
-- 用户代码
end
8.1.5.4 exmodbus.STATUS_TIMEOUT
常量含义:无任何响应(超时);
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 1 保持寄存器 0-1 的值时,配置读命令的字段参数
local read_config = {
slave_id = 1, -- 从站地址 1
reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
start_addr = 0x0000, -- 起始地址 0
reg_count = 0x0002, -- 读取 2 个寄存器
timeout = 1000 -- 超时时间 1000 ms
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
-- 4. 判断从站响应状态
if read_result.status == exmodbus.STATUS_TIMEOUT then
log.info("exmodbus_test", "无任何响应(超时)")
-- 用户代码
end
8.2 函数详解
8.2.1 exmodbus.create(config)
功能
创建并返回一个新的 modbus 主/从站实例;
注意事项
1、创建实例时需要将串口或者网络参数填写完整并且正确;
参数
由于 rtu/ascii 与 tcp 需要配置不同的 config 配置文件,因此下方将会分别进行介绍:
config(创建 RTU/ASCII 实例时)
参数含义:创建 RTU 或 ASCII 主/站实例时的配置文件,用于配置串口参数;
此参数为 table 类型,字段参数如下:
{
-- 参数含义:通信模式;
-- 数据类型:number;
-- 取值范围:
-- exmodbus.RTU_MASTER;
-- exmodbus.RTU_SLAVE;
-- exmodbus.ASCII_MASTER;
-- exmodbus.ASCII_SLAVE;
-- 是否必选:必选;
-- 参数示例:-- 配置通信模式为 modbus RTU 主站;
-- mode = exmodbus.RTU_MASTER
mode
-- 参数含义:串口 ID,uart0 写 0,uart1 写 1,以此类推;
-- 数据类型:number;
-- 取值范围:最大值取决于设备;
-- 是否必选:必选;
-- 参数示例:-- 配置串口号为 1;
-- uart_id = 1
uart_id
-- 参数含义:波特率;
-- 数据类型:number;
-- 取值范围:可选择波特率表:{2000000,921600,460800,230400,115200,57600,38400,19200,9600,4800,2400};
-- 是否必选:可选(默认 115200);
-- 参数示例:-- 配置波特率为 115200;
-- baud_rate = 115200
baud_rate
-- 参数含义:数据位;
-- 数据类型:number;
-- 取值范围:7/8;
-- 是否必选:可选(默认 8);
-- 参数示例:-- 配置数据位为 8;
-- data_bits = 8
data_bits
-- 参数含义:停止位;
-- 数据类型:number;
-- 取值范围:0.5/1/1.5/2 等;
-- 是否必选:可选(默认 1);
-- 参数示例:-- 配置停止位为 1;
-- stop_bits = 1
stop_bits
-- 参数含义:校验位;
-- 数据类型:number;
-- 取值范围:参考uart api中的常量详解;
-- uart.None;
-- uart.Even;
-- uart.Odd;
-- 是否必选:可选(默认 uart.None);
-- 参数示例:-- 配置校验位为无校验;
-- parity_bits = uart.None
parity_bits
-- 参数含义:字节顺序;
-- 数据类型:number;
-- 取值范围:参考uart api中的常量详解;
-- uart.LSB
-- uart.MSB
-- 是否必选:可选(默认 uart.LSB);
-- 参数示例:-- 配置字节顺序为小端序;
-- byte_order = uart.LSB
byte_order
-- 参数含义:RS485 方向 GPIO 引脚;
-- 数据类型:number;
-- 取值范围:根据设备选择可用的 GPIO;
-- 是否必选:可选(默认 0xffffffff);
-- 参数示例:-- 配置 RS485 方向 GPIO 引脚为 23;
-- rs485_dir_gpio = 23
rs485_dir_gpio
-- 参数含义:RS485 接收方向电平;
-- 数据类型:number;
-- 取值范围:0/1;
-- 是否必选:可选(默认为 0);
-- 参数示例:-- 配置 RS485 接收方向电平为 0;
-- rs485_dir_rx_level = 0
rs485_dir_rx_level
}
数据类型:table;
取值范围:暂无;
是否必选:是;
参数实例:-- 创建 modbus RTU 主站
local config = {
mode = exmodbus.RTU_MASTER, -- 通信模式:RTU 主站
uart_id = 1, -- 串口 ID:uart1
baud_rate = 115200, -- 波特率:115200
data_bits = 8, -- 数据位:8
stop_bits = 1, -- 停止位:1
parity_bits = uart.None, -- 校验位:无校验
byte_order = uart.LSB, -- 字节顺序:小端序
rs485_dir_gpio = 23, -- RS485 方向转换 GPIO 引脚
rs485_dir_rx_level = 0 -- RS485 接收方向电平:0 为低电平,1 为高电平
}
local rtu_master = exmodbus.create(config)
-- 创建 modbus ASCII 主站
local config = {
mode = exmodbus.ASCII_MASTER, -- 通信模式:RTU 主站
uart_id = 1, -- 串口 ID:uart1
baud_rate = 115200, -- 波特率:115200
data_bits = 8, -- 数据位:8
stop_bits = 1, -- 停止位:1
parity_bits = uart.None, -- 校验位:无校验
byte_order = uart.LSB, -- 字节顺序:小端序
rs485_dir_gpio = 23, -- RS485 方向转换 GPIO 引脚
rs485_dir_rx_level = 0 -- RS485 接收方向电平:0 为低电平,1 为高电平
}
local ascii_master = exmodbus.create(config)
config(创建 TCP 实例时)
参数含义:创建主/站实例时的配置文件,用于配置串口参数或者网络参数;
此参数为 table 类型,字段参数如下:
{
-- 参数含义:通信模式;
-- 数据类型:number;
-- 取值范围:
-- exmodbus.TCP_MASTER;
-- exmodbus.TCP_SLAVE;
-- 是否必选:必选;
-- 参数示例:-- 配置通信模式为 modbus TCP 主站;
-- mode = exmodbus.TCP_MASTER
mode
-- 参数含义:网卡 ID;
-- 数据类型:number;
-- 取值范围:参考socket api中的常量详解;
-- 是否必选:可选;
-- 参数示例:-- 配置网卡 ID 为 LwIP 协议栈的以太网卡;
-- _adapter_ = socket.LWIP_ETH
adapter
-- 参数含义:服务器地址;
-- 数据类型:string;
-- 取值范围:不可超过 191 字符;
-- 是否必选:必选;
-- 参数示例:-- 配置服务器地址为 192.168.1.100;
-- ip_address = "192.168.1.100"
ip_address
-- 参数含义:服务器端口号;
-- 数据类型:number;
-- 取值范围:暂无;
-- 是否必选:必选;
-- 参数示例:-- 配置服务器端口号为 502;
-- port = 502
port
-- 参数含义:是否是UDP,true表示UDP,false或者nil表示TCP;
-- 数据类型:boolean;
-- 取值范围:true/false;
-- 是否必选:可选(默认 TCP);
-- 参数示例:-- 配置不使用 UDP 协议,使用 TCP 协议;
-- _is_udp_ = false
is_udp
-- 参数含义:是否为加密传输;
-- 数据类型:boolean;
-- 取值范围:true/false;
-- 取值范围:可选
-- 参数示例:-- 配置不使用加密传输;
-- _is_tls_ = false
is_tls
-- 参数含义:连接空闲多长时间后,开始发送第一个 keepalive 探针报文;
-- 数据类型:number;
-- 取值范围:大于 0 的正整数;
-- 是否必选:可选(默认为不开启)
-- 参数示例:-- 配置连接空闲 300 秒后,开始发送第一个 keepalive 探针报文;
-- _keep_idle_ = 300
keep_idle
-- 参数含义:发送第一个探针后,如果没收到 ACK 回复,间隔多久再发送下一个探针,单位为秒;
-- 数据类型:number;
-- 取值范围:大于 0 的正整数;
-- 是否必选:可选;
-- 参数示例:-- 配置发送第一个探针后,如果没收到 ACK 回复,10 秒后再发送下一个探针;
-- keep_interval = 10
keep_interval
-- 参数含义:总共发送多少次探针后,如果依然没有回复,则判定连接已断开;
-- 数据类型:number;
-- 取值范围:大于 0 的正整数;
-- 是否必选:可选;
-- 参数示例:-- 配置总共发送 3 次探针后,如果依然没有回复,则判定连接已断开;
-- _keep_cnt_ = 3
keep_cnt
-- 参数含义:TCP 模式下的服务器 ca 证书数据,UDP模式下的 PSK;
-- 数据类型:string;
-- 取值范围:暂无;
-- 是否必选:可选;
-- 参数示例:-- 配置不验证 ca 证书或者 PSK;
-- _server_cert = nil_
server_cert
-- 参数含义:TCP 模式下的客户端证书数据,UDP 模式下的 PSK-ID;
-- 数据类型:string;
-- 取值范围:暂无;
-- 是否必选:可选;
-- 参数示例:-- 配置不验证 客户端证书 或者 PSK-ID;
-- _client_cert = nil_
client_cert
-- 参数含义:TCP 模式下的客户端私钥加密数据;
-- 数据类型:string;
-- 取值范围:暂无;
-- 是否必选:可选;
-- 参数示例:-- 配置不使用客户端私钥加密数据;
-- _client_key = nil_
client_key
-- 参数含义:TCP 模式下的客户端私钥口令数据;
-- 数据类型:string;
-- 取值范围:暂无;
-- 是否必选:可选;
-- 参数示例:-- 配置不使用客户端私钥口令数据;
-- _client_password = nil_
client_password
}
数据类型:table;
取值范围:暂无;
是否必选:是;
参数实例:-- 创建 modbus TCP 主站
local config = {
mode = exmodbus.TCP_MASTER, -- 通信模式:TCP 主站
adapter = socket.LWIP_ETH, -- 网卡 ID:LwIP 协议栈的以太网卡
ip_address = "192.168.1.100", -- 服务器 IP 地址:192.168.1.100(主站:服务器 IP;从站:本地 IP,从站可以不用填此参数)
port = 502, -- 服务器端口号:502(主站:服务器端口;从站:本地端口)
is_udp = false, -- 是否使用 UDP 协议:不使用 UDP 协议,false/nil 表示使用 TCP 协议
is_tls = false, -- 是否使用加密传输:不使用加密传输,false/nil 表示不使用加密
keep_idle = 300, -- 连接空闲多长时间后,开始发送第一个 keepalive 探针报文:300 秒
keep_interval = 10, -- 发送第一个探针后,如果没收到 ACK 回复,间隔多久再发送下一个探针:10 秒
keep_cnt = 3, -- 总共发送多少次探针后,如果依然没有回复,则判断连接已断开:3 次
server_cert = nil, -- TCP 模式下的服务器 CA 证书数据,UDP 模式下的 PSK:如果客户端不需要验证服务器证书,则设为 nil 或空着
client_cert = nil, -- TCP 模式下的客户端证书数据,UDP 模式下的 PSK-ID:如果服务器不需要验证客户端证书,则设为 nil 或空着
client_key = nil, -- TCP 模式下的客户端私钥加密数据:如果服务器不需要验证客户端私钥,则设为 nil 或空着
client_password = nil -- TCP 模式下的客户端私钥口令数据:如果服务器不需要验证客户端私钥口令,则设为 nil 或空着
}
local tcp_master = exmodbus.create(config)
返回值
local mb = exmodbus.create(config)
mb
含义说明:modbus 实例对象;
数据类型:table;
取值范围:暂无;
返回值示例:-- 创建 modbus RTU 主站
local create_config = {
-- 串口配置参数;
mode = exmodbus.RTU_MASTER, -- 通信模式:RTU 主站
uart_id = 1, -- 串口 ID:uart1
baud_rate = 115200, -- 波特率:115200
data_bits = 8, -- 数据位:8
stop_bits = 1, -- 停止位:1
parity_bits = uart.None, -- 校验位:无校验
byte_order = uart.LSB, -- 字节顺序:小端序
rs485_dir_gpio = 23, -- RS485 方向转换 GPIO 引脚
rs485_dir_rx_level = 0 -- RS485 接收方向电平:0 为低电平,1 为高电平
}
local rtu_master = exmodbus.create(config)
示例
-- 创建 modbus RTU 主站
local create_config = {
-- 串口配置参数;
mode = exmodbus.RTU_MASTER, -- 通信模式:RTU 主站
uart_id = 1, -- 串口 ID:uart1
baud_rate = 115200, -- 波特率:115200
data_bits = 8, -- 数据位:8
stop_bits = 1, -- 停止位:1
parity_bits = uart.None, -- 校验位:无校验
byte_order = uart.LSB, -- 字节顺序:小端序
rs485_dir_gpio = 23, -- RS485 方向转换 GPIO 引脚
rs485_dir_rx_level = 0 -- RS485 接收方向电平:0 为低电平,1 为高电平
}
local rtu_master = exmodbus.create(config)
8.2.2 modbus:read(config)
功能
主站向从站发送读取操作请求(阻塞接口);
支持通过“字段参数方式”或“原始帧方式”传入 config 配置参数,传入格式详见下方 config 参数示例;
如果你需要发送的请求报文是符合 modbus 标准格式,可以使用“字段参数方式”或者“原始帧方式”;
如果你需要发送的请求报文是非标准格式,必须使用“原始帧方式”,使用“字段参数方式”会导致解析的数据不正确;
注意事项
1、在调用此接口之前,需要先确保对应实例对象有效;
2、请求范围需要符合 modbus 标准协议要求;
参数
由于该接口支持通过“字段参数方式”或“原始帧方式”传入 config 配置参数,因此下方将会分别进行介绍:
config(字段参数方式传入时)
参数含义:向从站发送读取操作请求时的配置参数;
该参数为 table 类型,字段如下:
{
-- 参数含义:从站地址(ID);
-- 数据类型:number;
-- 取值范围:0 ~ 247(0 为广播地址);
-- 是否必选:必选;
-- 参数示例:-- 配置从站地址为 1;
-- slave_id = 1
slave_id
-- 参数含义:寄存器类型(数据类型);
-- 数据类型:number;
-- 取值范围:
-- exmodbus.COIL_STATUS;
-- exmodbus.INPUT_STATUS;
-- exmodbus.INPUT_REGISTER;
-- exmodbus.HOLDING_REGISTER;
-- 是否必选:必选;
-- 参数示例:-- 配置寄存器类型(数据类型)为保持寄存器;
-- reg_type = exmodbus.HOLDING_REGISTER
reg_type
-- 参数含义:寄存器起始地址;
-- 数据类型:number;
-- 取值范围:0 ~ 65535;
-- 是否必选:必选;
-- 参数示例:-- 配置寄存器起始地址为 0;
-- start_addr = 0
start_addr
-- 参数含义:寄存器数量;
-- 数据类型:number;
-- 取值范围:1 ~ 125;
-- 是否必选:必选;
-- 参数示例:-- 配置寄存器数量为 2;
-- reg_count = 2
reg_count
-- 参数含义:超时时间,单位毫秒;
-- 数据类型:number;
-- 取值范围:暂无;
-- 是否必选:可选,默认为 1 秒;
-- 参数示例:-- 配置超时时间为 1 秒;
-- timeout = 1000
timeout
}
数据类型:table;
取值范围:暂无;
是否必选:必选;
参数示例:-- 1. 创建主站
-- 2. 读取操作时的配置参数(字段参数模式)
-- 读取从站 1 保持寄存器 0-1 的值时,配置读命令的字段参数
-- 超时时间为 1 秒(1000 毫秒)
local read_config = {
slave_id = 1, -- 从站地址
reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型
start_addr = 0, -- 寄存器起始地址
reg_count = 2, -- 寄存器数量
timeout = 1000 -- 超时时间
}
-- 3. 执行读取请求
local read_result = modbus:read(read_config)
config(原始帧方式传入时)
参数含义:向从站发送读取操作请求时的配置参数;
该参数为 table 类型,字段如下:
{
-- 参数含义:原始请求帧;
-- 数据类型:string;
-- 取值范围:暂无;
-- 是否必选:可选;
-- 参数示例:-- 读取从站 1 保持寄存器 0-1 的值时,配置读命令原始请求帧
-- 超时时间为 1 秒(1000 毫秒)
-- raw_request = string.char( -- 原始报文帧
-- 0x01, -- 从站地址
-- 0x03, -- 功能码
-- 0x00, 0x00, -- 寄存器起始地址
-- 0x00, 0x02, -- 寄存器数量
-- 0xC4, 0x0B -- CRC 16 校验
-- )
raw_request
-- 参数含义:超时时间,单位毫秒;
-- 数据类型:number;
-- 取值范围:暂无;
-- 是否必选:可选,默认为 1 秒;
-- 参数示例:-- 配置超时时间为 1 秒;
-- timeout = 1000
timeout
}
数据类型:table;
取值范围:暂无;
是否必选:必选;
参数示例:-- 1. 创建主站
-- 2. 读取操作时的配置参数(原始帧模式)
-- 读取从站 1 保持寄存器 0-1 的值时,配置读命令的字段参数
-- 超时时间为 1 秒(1000 毫秒)
local read_config = {
raw_request = string.char( -- 原始报文帧
0x01, -- 从站地址
0x03, -- 功能码
0x00, 0x00, -- 寄存器起始地址
0x00, 0x02, -- 寄存器数量
0xC4, 0x0B -- CRC 16 校验
)
timeout = 1000 -- 超时时间
}
-- 3. 执行读取请求
local read_result = modbus:read(read_config)
返回值
local result = modbus:read(config)
result
含义说明:向从站发送读取请求后的结果数据;
返回值为 table 类型,字段如下:
{
-- 参数含义:响应结果状态码;
-- 数据类型:number;
-- 取值范围:
-- exmodbus.STATUS_SUCCESS;
-- exmodbus.STATUS_DATA_INVALID;
-- exmodbus.STATUS_EXCEPTION;
-- exmodbus.STATUS_TIMEOUT;
status
-- 参数含义:异常码;
-- 数据类型:number;
-- 取值范围:暂无(取决于 modbus 标准异常码);
execption_code
-- 参数含义:寄存器值;
-- 该参数为 table 类型,字段如下:
{
[start_addr] = , -- 第一个寄存器
[start_addr + 1] = , -- 第二个寄存器
...
...
...
}
-- 数据类型:table;
-- 取值范围:1 ~ 125;
data
-- 参数含义:原始响应帧;
-- 数据类型:string;
-- 取值范围:暂无;
raw_response
}
数据类型:table
取值范围:暂无;
返回值示例:-- 1. 创建主站
-- 2. 执行读取请求
local result = modbus:read(read_config)
-- 3. 判断从站响应状态
if result.status == exmodbus.STATUS_SUCCESS then
log.info("收到响应数据且数据有效")
elseif result.status == exmodbus.STATUS_DATA_INVALID then
log.info("收到响应数据但数据损坏/校验失败")
elseif result.status == exmodbus.STATUS_EXCEPTION then
log.info("收到 modbus 标准异常响应")
elseif result.status == exmodbus.STATUS_TIMEROUT then
log.info("无任何响应(超时)")
end
示例
-- 1. 创建主站
-- 2. 执行读取请求
local result = modbus:read(config)
-- 3. 判断从站响应状态
if result.status == exmodbus.STATUS_SUCCESS then
log.info("收到响应数据且数据有效")
elseif result.status == exmodbus.STATUS_DATA_INVALID then
log.info("收到响应数据但数据损坏/校验失败")
elseif result.status == exmodbus.STATUS_EXCEPTION then
log.info("收到 modbus 标准异常响应")
elseif result.status == exmodbus.STATUS_TIMEROUT then
log.info("无任何响应(超时)")
end
8.2.3 modbus:write(config)
功能
主站向从站发送写入操作请求(阻塞接口);
支持通过“字段参数方式”或“原始帧方式”传入 config 配置参数,传入格式详见下方 config 参数示例;
如果你需要发送的请求报文是符合 modbus 标准格式,可以使用“字段参数方式”或者“原始帧方式”;
如果你需要发送的请求报文是非标准格式,必须使用“原始帧方式”,使用“字段参数方式”会导致解析的数据不正确;
注意事项
1、在调用此接口之前,需要先确保对应实例对象有效;
2、请求范围需要符合 modbus 标准协议要求;
参数
由于该接口支持通过“字段参数方式”或“原始帧方式”传入 config 配置参数,因此下方将会分别进行介绍:
config(字段参数方式传入时)
参数含义:向从站发送写入操作请求时的配置参数;
该参数为 table 类型,字段如下:
{
-- 参数含义:从站地址(ID);
-- 数据类型:number;
-- 取值范围:0 ~ 247(0 为广播地址);
-- 是否必选:必选;
-- 参数示例:-- 配置从站地址为 1;
-- slave_id = 1
slave_id
-- 参数含义:寄存器类型(数据类型);
-- 数据类型:number;
-- 取值范围:
-- exmodbus.COIL_STATUS;
-- exmodbus.INPUT_STATUS;
-- exmodbus.INPUT_REGISTER;
-- exmodbus.HOLDING_REGISTER;
-- 是否必选:必选;
-- 参数示例:-- 配置寄存器类型(数据类型)为保持寄存器;
-- reg_type = exmodbus.HOLDING_REGISTER
reg_type
-- 参数含义:寄存器起始地址;
-- 数据类型:number;
-- 取值范围:0 ~ 65535;
-- 是否必选:必选;
-- 参数示例:-- 配置寄存器起始地址为 0;
-- start_addr = 0
start_addr
-- 参数含义:寄存器数量;
-- 数据类型:number;
-- 取值范围:1 ~ 125;
-- 是否必选:必选;
-- 参数示例:-- 配置寄存器数量为 2;
-- reg_count = 2
reg_count
-- 参数含义:寄存器值;
-- 该参数为 table 类型,字段如下:
-- {
-- [start_addr] = , -- 第一个寄存器
-- [start_addr + 1] = , -- 第二个寄存器
-- ...
-- ...
-- ...
-- }
-- 数据类型:table;
-- 取值范围:1 ~ 125;
-- 是否必选:必选;
-- 参数示例:-- 配置第一个寄存器值为 123,第二个寄存器值为 345;
-- data =
-- {
-- [start_addr] = 123, -- 第一个寄存器值
-- [start_addr + 1] = 345, -- 第二个寄存器值
-- }
data
-- 参数含义:控制写单个寄存器时使用写单个功能码还是写多个功能码;
-- 数据类型:boolean;
-- 取值范围:true(使用写多个功能码)/false(使用写单个功能码);
-- 是否必选:可选;
-- 参数示例:-- 配置写单个寄存器时使用写多个功能码
-- force_multiple = true
force_multiple
-- 参数含义:超时时间,单位毫秒;
-- 数据类型:number;
-- 取值范围:暂无;
-- 是否必选:可选,默认为 1 秒;
-- 参数示例:-- 配置超时时间为 1 秒;
-- timeout = 1000
timeout
}
数据类型:table;
取值范围:暂无;
是否必选:必选;
参数示例:-- 1. 创建主站
-- 2. 写入操作时的配置参数(字段参数模式)
-- 写入从站 1 保持寄存器 0-1 的值时,配置写命令的字段参数
-- 超时时间为 1 秒(1000 毫秒)
local write_config = {
slave_id = 1, -- 从站地址
reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型
start_addr = 0, -- 寄存器起始地址
reg_count = 2, -- 寄存器数量
data = -- 寄存器值
{
[start_addr] = 123, -- 第一个寄存器值
[start_addr + 1] = 345, -- 第二个寄存器值
},
timeout = 1000 -- 超时时间
}
-- 3. 执行写入请求
local write_result = modbus:write(write_config)
config(原始帧方式传入时)
参数含义:向从站发送写入操作请求时的配置参数;
该参数为 table 类型,字段如下:
{
-- 参数含义:原始请求帧;
-- 数据类型:string;
-- 取值范围:暂无;
-- 是否必选:可选;
-- 参数示例:-- 写入从站 1 保持寄存器 0-1 的值时,配置写命令的原始请求帧
-- raw_request = string.char( -- 原始报文帧
-- 0x01, -- 从站地址
-- 0x10, -- 功能码
-- 0x00, 0x00, -- 寄存器起始地址
-- 0x00, 0x02, -- 寄存器数量
-- 0x00, 0x7B, -- 第一个寄存器数值
-- 0x01, 0x59, -- 第二个寄存器数值
-- 0x24, 0x71 -- CRC 16 校验
-- )
raw_request
-- 参数含义:超时时间,单位毫秒;
-- 数据类型:number;
-- 取值范围:暂无;
-- 是否必选:可选,默认为 1 秒;
-- 参数示例:-- 配置超时时间为 1 秒;
-- timeout = 1000
timeout
}
数据类型:table;
取值范围:暂无;
是否必选:必选;
参数示例:-- 1. 创建主站
-- 2. 写入操作时的配置参数(原始帧模式)
-- 写入从站 1 保持寄存器 0-1 的值时,配置写命令的字段参数
-- 超时时间为 1 秒(1000 毫秒)
local write_config = {
raw_request = string.char( -- 原始报文帧
0x01, -- 从站地址
0x10, -- 功能码
0x00, 0x00, -- 寄存器起始地址
0x00, 0x02, -- 寄存器数量
0x00, 0x7B, -- 第一个寄存器数值
0x01, 0x59, -- 第二个寄存器数值
0x24, 0x71 -- CRC 16 校验
)
timeout = 1000 -- 超时时间
}
-- 3. 执行写入请求
local write_result = modbus:write(write_config)
返回值
local result = modbus:write(config)
result
含义说明:向从站发送写入请求后的结果;
返回值为 table 类型,字段如下:
{
-- 参数含义:响应结果状态码;
-- 数据类型:number;
-- 取值范围:
-- exmodbus.STATUS_SUCCESS
-- exmodbus.STATUS_DATA_INVALID
-- exmodbus.STATUS_EXCEPTION
-- exmodbus.STATUS_TIMEOUT
status
-- 参数含义:异常码;
-- 数据类型:number;
-- 取值范围:暂无;
execption_code
-- 参数含义:原始响应帧;
-- 数据类型:string;
-- 取值范围:暂无;
raw_response
}
数据类型:table
取值范围:暂无;
返回值示例:-- 1. 创建主站
-- 2. 执行写入请求
local result = modbus:write(write_config)
-- 3. 判断从站响应状态
if result.status == exmodbus.STATUS_SUCCESS then
log.info("收到响应数据且数据有效")
elseif result.status == exmodbus.STATUS_DATA_INVALID then
log.info("收到响应数据但数据损坏/校验失败")
elseif result.status == exmodbus.STATUS_EXCEPTION then
log.info("收到 modbus 标准异常响应")
elseif result.status == exmodbus.STATUS_TIMEROUT then
log.info("无任何响应(超时)")
end
示例
-- 1. 创建主站
-- 2. 执行写入请求
local result = modbus:write(write_config)
-- 3. 判断从站响应状态
if result.status == exmodbus.STATUS_SUCCESS then
log.info("收到响应数据且数据有效")
elseif result.status == exmodbus.STATUS_DATA_INVALID then
log.info("收到响应数据但数据损坏/校验失败")
elseif result.status == exmodbus.STATUS_EXCEPTION then
log.info("收到 modbus 标准异常响应")
elseif result.status == exmodbus.STATUS_TIMEROUT then
log.info("无任何响应(超时)")
end
8.2.4 modbus:destroy()
功能
销毁已创建的主/从站示例对象;
注意事项
暂无;
参数
无;
返回值
无;
示例
modbus:destroy()
8.2.5 modbus:on(callback)
功能
此接口仅限设备做从站时使用;
当收到主站请求数据时,通过 callback 通知应用脚本处理;
应用脚本处理完之后,在 callback 中通知返回值,告知 exmodbus 扩展库返回给主站;
注意事项
1、在调用此接口之前,需要先确保对应实例对象有效;
参数
callback
参数含义:事件回调函数;格式为:
function callback(request)
-- 用户代码
end
该回调函数接收 requset 一个参数,request 参数说明如下:
-- 参数含义:主站发送请求时的请求数据;
-- 该参数为 table 类型,字段如下:
{
-- 参数含义:从站地址(ID);
-- 数据类型:number;
-- 取值范围:0 ~ 247(0 为广播地址);
slave_id
-- 参数含义:功能码;
-- 数据类型:number;
-- 取值范围:暂无;
func_code
-- 参数含义:寄存器类型(数据类型);
-- 数据类型:number;
-- 取值范围:
-- exmodbus.COIL_STATUS;
-- exmodbus.INPUT_STATUS;
-- exmodbus.INPUT_REGISTER;
-- exmodbus.HOLDING_REGISTER;
reg_type
-- 参数含义:寄存器起始地址;
-- 数据类型:number;
-- 取值范围:0 ~ 65535;
start_addr
-- 参数含义:寄存器数量;
-- 数据类型:number;
-- 取值范围:1 ~ 125;
reg_count
-- 参数含义:寄存器值;
-- 该参数为 table 类型,字段如下:
{
[start_addr] = , -- 第一个寄存器
[start_addr + 1] = , -- 第二个寄存器
...
...
...
}
-- 数据类型:table;
-- 取值范围:1 ~ 125;
data
}
-- 数据类型:table;
-- 取值范围:暂无;
-- 是否必选:必选;
request
数据类型:function;
取值范围:暂无;
是否必选:必选;
参数示例:-- 1. 创建从站
-- 2. 注册主站请求处理回调函数
function callback(request)
-- 用户代码
end
-- 3. 注册主站请求处理回调函数
modbus:on(callback)
返回值
无;
示例
-- 0. 初始化一些参数
-- 当前从站地址(ID 号)
local SLAVE_ID = 1
-- 寄存器映射表(按类型组织)
local modbus_data = {
coils = {}, -- 线圈,可读可写,布尔值 (0/1)
inputs = {}, -- 输入状态,只读,布尔值 (0/1)
input_registers = {}, -- 输入寄存器,只读,16 位无符号整数
holding_registers = {} -- 保持寄存器,可读可写,16 位无符号整数
}
-- 初始化一些默认值,便于测试
for i = 0, 3 do
modbus_data.coils[i] = 0
modbus_data.inputs[i] = 1
modbus_data.input_registers[i] = 100 + i
modbus_data.holding_registers[i] = 200 + i
end
-- 1. 创建从站
-- 2. 注册主站请求处理回调函数
local function callback(request)
log.info("exmodbus_test", "收到主站请求")
-- 检查从站 ID 是否匹配
if request.slave_id ~= SLAVE_ID then
log.info("exmodbus_test", "从站 ID 不匹配,请求从站 ID 为", request.slave_id, ",当前从站 ID 为", SLAVE_ID)
return nil
end
-- 根据功能码和寄存器类型,匹配对应的数据表
local data_table = nil
local is_write = false -- 标记是否为写操作
-- 检查请求的功能码是否支持
if request.func_code == exmodbus.READ_COILS then -- 读线圈
data_table = modbus_data.coils
elseif request.func_code == exmodbus.READ_DISCRETE_INPUTS then -- 读离散输入
data_table = modbus_data.inputs
elseif request.func_code == exmodbus.READ_HOLDING_REGISTERS then -- 读保持寄存器
data_table = modbus_data.holding_registers
elseif request.func_code == exmodbus.READ_INPUT_REGISTERS then -- 读输入寄存器
data_table = modbus_data.input_registers
elseif request.func_code == exmodbus.WRITE_SINGLE_COIL or request.func_code == exmodbus.WRITE_MULTIPLE_COILS then -- 写单个/多个线圈
is_write = true
data_table = modbus_data.coils
elseif request.func_code == exmodbus.WRITE_SINGLE_HOLDING_REGISTER or request.func_code == exmodbus.WRITE_MULTIPLE_HOLDING_REGISTERS then -- 写单个/多个保持寄存器
is_write = true
data_table = modbus_data.holding_registers
else
-- 不支持的功能码
log.info("exmodbus_test", "不支持的功能码: ", request.func_code)
return exmodbus.ILLEGAL_FUNCTION
end
-- 检查数据地址是否有效
local end_addr = request.start_addr + request.reg_count - 1
-- 假设每种寄存器的最大地址是 3 (即 0 - 3)
if request.start_addr < 0 or end_addr > 3 then
log.info("exmodbus_test", "数据地址超出范围,起始地址为", request.start_addr, "结束地址为", end_addr)
return exmodbus.ILLEGAL_DATA_ADDRESS
end
-- 处理读取操作
if not is_write then
-- 构造响应数据表
local response = {}
for i = 0, request.reg_count - 1 do
local addr = request.start_addr + i
response[addr] = data_table[addr]
end
log.info("exmodbus_test", "读取成功,返回数据: ", table.concat(response, ", "))
return response
end
-- 处理写入操作
if is_write then
-- 执行写入操作
for i = 0, request.reg_count - 1 do
local addr = request.start_addr + i
data_table[addr] = request.data[addr]
log.info("exmodbus_test", "写入成功,写入地址: ", addr, "写入数据: ", request.data[addr])
end
return {} -- 返回空表表示成功
end
end
-- 3. 注册主站请求处理回调函数
modbus:on(callback)
第九部分:LuatOS 上的 Modbus 应用开发流程
本部分将针对一种通信模式进行实践:Modbus RTU 通信协议模式下的 Modbus 应用开发。
Modbus ASCII 和 Modbus TCP 这两种模式下的 Modbus 应用开发将在后续直播中进行介绍。
Modbus RTU 主站应用开发:
示例代码:https://gitee.com/openLuat/LuatOS/tree/master/module/Air8000/demo/modbus/rtu_master
操作文档:https://docs.openluat.com/air8000/luatos/app/modbus/modbus_RTU_master/
Modbus RTU 从站应用开发:
示例代码:https://gitee.com/openLuat/LuatOS/tree/master/module/Air8000/demo/modbus/rtu_slave
操作文档:https://docs.openluat.com/air8000/luatos/app/modbus/modbus_RTU_slave/
9.1 分析项目代码
9.1.2 Modbus RTU 主站
文件说明:
1. main.lua:主程序入口文件。
2. param_field.lua:RTU 主站应用模块(字段参数方式)。
3. raw_frame.lua:RTU 主站应用模块(原始帧方式)。
4. temp_hum_sensor.lua:485 温湿度传感器读取模块。
演示功能:
1. 将设备配置为 modbus RTU 主站模式
2. 与从站 1 和 从站 2 进行通信 - 对从站 1 进行 2 秒一次的读取保持寄存器 0-1 操作 - 对从站 2 进行 4 秒一次的写入保持寄存器 0-1 操作
3. 读取温湿度传感器数据 - 配置 modbus RTU 主站,读取温湿度传感器数据 - 每 2 秒读取一次传感器数据并解析温度和湿度值
注意事项:
1. 该示例程序需要搭配 exmodbus 扩展库使用。
2. 在 main.lua 中 require "param_field" 模块,可以演示标准 modbus RTU 请求报文格式的使用方式。
3. 在 main.lua 中 require "raw_frame" 模块,可以演示非标准 modbus RTU 请求报文格式的使用方式。
4. 在 main.lua 中 require "temp_hum_sensor" 模块,可以演示读取 485 温湿度传感器数值的使用方式。
5. require "param_field"、require "raw_frame" 和 require "temp_hum_sensor",不要同时打开,否则功能会有冲突。
特别说明:
关于 RTU 报文,exmodbus 扩展库支持通过 字段参数 或 原始帧 两种方式进行配置。
这两种配置方式本质都由用户将其放入 table 中在调用接口时传入,区别如下:
1. 字段参数方式
这种方式需要用户将请求报文进行解析后,将其放入 table 中,例如:
-- 读取请求:
local config = {
slave_id = 1, -- 从站地址
reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
start_addr = 0x0000, -- 寄存器起始地址
reg_count = 0x0002, -- 寄存器数量
timeout = 1000 -- 超时时间
}
-- 写入请求:
local config = {
slave_id = 2, -- 从站地址
reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
start_addr = 0x0000, -- 寄存器起始地址
reg_count = 0x0002, -- 寄存器数量
data = {
[start_addr] = 0x0012, -- 寄存器 0 的值
[start_addr + 1] = 0x0034, -- 寄存器 1 的值
}
force_multiple = true, -- 是否强制使用多个寄存器写入操作(写多个线圈功能码:0x0F;写多个寄存器功能码:0x10)
timeout = 1000 -- 超时时间
}
2. 原始帧方式
这种方式只需要用户将原始请求报文放入 table 中,例如:
-- 读取请求:
local config = {
raw_request = string.char(
0x01, -- 从站地址
0x03, -- 功能码:读取保持寄存器
0x00, 0x00, -- 寄存器起始地址
0x00, 0x02, -- 寄存器数量
0xC4, 0x0B -- CRC16校验码
)
timeout = 1000 -- 超时时间
}
-- 写入请求:
local config = {
raw_request = string.char(
0x02, -- 从站地址
0x10, -- 功能码:写入保持寄存器
0x00, 0x00, -- 寄存器起始地址
0x00, 0x02, -- 寄存器数量
0x04, -- 字节数量
0x00, 0x12, -- 寄存器 0 的值
0x00, 0x34, -- 寄存器 1 的值
0x5D, 0x39 -- CRC16校验码
)
timeout = 1000 -- 超时时间
}
如果你需要发送的请求报文是符合 modbus RTU 标准格式,可以使用 字段参数 或者 原始帧 方式。
如果你需要发送的请求报文是非标准格式,必须使用 原始帧 方式,使用 字段参数 方式会导致解析的数据不正确。
9.1.3 Modbus RTU 从站
文件说明:
1. main.lua:主程序入口文件。
2. rtu_slave_manage.lua:RTU 从站应用模块;
演示功能:
1. 将设备配置为 modbus RTU 从站模式
2. 等待并且应答主站请求
注意事项:
1. 该示例程序需要搭配 exmodbus 扩展库使用
2. 设备作为 modbus RTU 从站模式时,仅支持接收 modbus RTU 标准格式的请求报文
3. 进行回应时也需要符合 modbus RTU 标准格式
9.2 Air8000 开发板上演示项目功能
准备硬件环境:
1、Air8000 开发板一块
2、TYPE-C USB 数据线一根
3、USB-RS485 串口板
此处购买链接仅为推荐,如有问题请直接联系店家
演示 Modbus RTU 主站应用时额外需要准备以下设备:
1、气体浓度变送器(RS485 版)
如果你是小白,建议直接购买同款变送器,由于不同型号的温湿度模块默认的参数也会有所区别
准备软件环境:
1、烧录工具:Luatools 下载调试工具
2、内核固件:Air8000 V2018 版本(理论上最新版本固件也可以,如果使用最新版本的固件不可以,可以烧录 V2018-1 固件对比验证)
3、脚本文件:Air8000 脚本文件
4、模拟工具:摩尔信使(MThings)官网(用于模拟 modbus 主/从站设备)
接下来为大家依次演示 Air8000 作为 Modbus RTU 主站和作为 Modbus RTU 从站时如何连接,如何与对端进行通信。
第十部分:Modbus RTU 通信模式常见问题分析
后续开发中有遇到常见问题时会在此处记录..