这几年的工作,从工控转到到嵌入式软件开发,再到目前的上位软件开发,虽然工作形式发生了变化,但实质都是在做各种各样的编程,针对不同行业的应用做编程工作,在这个过程中发现,单独一个领域的编程工作是相对比较简单的,人力市场上也很好招人,但是涉及到不同领域之间的跨领域人才,是相对奇缺的。而如果试图把不同领域之间的编程连接起来,就更加难上加难了。一般的开发过程是,软件工程师做上位开发,嵌入式工程师做下位的驱动,电气工程师负责PLC的编程及调试,如果涉及到这三者之间的相互通信,往往会出现问题。在此,就这三者之间的通信,发表一下个人的经验所得。
主要内容:
1、Modbus简单介绍,主要介绍Modbus的协议规范及在串行链路上的使用(本文只涉及Modbus RTU的使用);
2、Modbus在PLC中的使用,包括PLC与上位HMI及变频器的通信;
3、Modbus在嵌入式系统中的时候,主要介绍基于STM32嵌入式系统移植FreeModbus的实例;
4、Modbus在上位软件中的使用,介绍在C#程序中调用nmodbus.dll库,以及Modbus开源库libmodbus;
5、Modbus在Qt中的使用。
一、Modbus简介
1.1概述
Modbus是一种串行通信协议,是Modicon公司(现在的施耐德电气Schneider Electric)于1979年为使用可编程逻辑控制器(PLC)通信而发表。Modbus已经成为工业领域通信协议的业界标准(De facto),并且现在是工业电子设备之间常用的连接方式。
Modbus允许多个 (大约240个) 设备连接在同一个网络上进行通信,举个例子,一个由测量温度和湿度的装置,并且将结果发送给计算机。在数据采集与监视控制系统(SCADA)中,Modbus通常用来连接监控计算机和远程终端控制系统(RTU)。
MODBUS 是 OSI 模型第 7 层上的应用层报文传输协议,它在连接至不同类型总线或网络的设备之间提供客户机/服务器通信。
自从 1979 年出现工业串行链路的事实标准以来,MODBUS 使成千上万的自动化设备能够通信。目前,继续增加对简单而雅观的 MODBUS 结构支持。互联网组织能够使 TCP/IP 栈上的保留系统端口 502 访问 MODBUS。MODBUS 是一个请求/应答协议,并且提供功能码规定的服务。MODBUS 功能码是 MODBUS请求/应答 PDU 的元素。
MODBUS 是一项应用层报文传输协议,用于在通过不同类型的总线或网络连接的设备之间的客户机/服务器通信。
更详细的介绍请可以查看Modbus组织的官网:
The Modbus Organization
Modbus比较权威的介绍,可参看《Modbus协议规范》,网上资源特别多,这里就不放链接了。
1.2使用
这里仅针对Modbus调试软件,介绍一下Modbus的使用。Modbus常用的调试软件,Modbus主站“Modbus Poll”,Modbus从站“Modbus Slave”,在网上很好找的。
1.2.1 Modbus主站
主站支持的功能代码主要有:
01 Read Coils (0x)
02 Read Discrete Inputs (1x)
03 Read Holding Registers (4x)
04 Read Input Registers (3x)
05 Write Single Coil
06 Write Single Register
15 Write Multiple Coils
16 Write Multiple Registers
1.2.2 Modbus从站
主站支持的功能代码主要有:
01 Coil Status (0x)
02 Input Status (1x)
03 Holding Register (4x)
04 Input Registers (3x)
1.2.3 基本功能码的介绍
从调试软件可以看到,Modbus支持的数据模型主要是四种,如下表:
除此之外,Modbus还支持其他功能,但是在项目中使用的比较少,这里不做介绍。
1、 01(0x01)读线圈
使用该功能从一个远程设备中读取1-2000个连续的线圈状态。请求PDU指定了第一个线圈的地址和线圈的数目。在PDU中,从零开始寻址线圈,因此编号1-16的线圈寻址为0-15。
响应报文中的线圈按数据字段对每位一个线圈进行打包。状态被表示成1 = ON和0 = OFF。第一个数据字节的LSB(最低有效位)包含询问中所寻址的输出。其他线圈以此类推,一直到这个字节的高位端为止,并在后续字节中按照从低位到高位的顺序排列。
如果返回的输出数量不是8的倍数,将用零填充最后数据字节中的剩余位。字节计数字段指定了数据的全部字节数。
读取线圈请求
功能码 1 字节 0x01
起始地址 2 字节 0x0000-0Xffff
线圈数量 2 字节 1-2000(0x7D0)
读线圈响应
功能码 1 字节 0x01
字节计数 1 字节 N
线圈状态 N 字节 N或N+1
N = 输出数据/8,如果余数不等于0,则N = N+1
错误
功能码 1 字节 功能码+ 0x80
异常码 1 字节 01或02或03或04
例:请求读取离散量输入20-38(0x14-0x26,19bit 0x13)
请求 响应
字段名 十六进制 字段名 十六进制
功能码 01 功能码 01
起始地址Hi 00 字节数 03
起始地址Lo 13 输出状态27-20 CD
输出数量Hi 00 输出状态35-28 6B
输出数量Lo 13 输出状态38-36 05
将输出 27-20 的状态表示为十六进制CD,或二进制 1100 1101。输出 27 是这个字节的MSB,输出 20 是LSB。通常,将一个字节内的比特表示为MSB 位于左侧,LSB 位于右侧。第一字节的输出从左至右为 27 至 20。下一个字节的输出从左到右为 35 至28。当串行发射比特时,从LSB 向 MSB 传输:即20 . . .27、28 . . . 35 等等。在最后的数据字节中,将输出状态38-36 表示为十六进制 05,或二进制 0000 0101。输出38 是左侧第六个比特位置,输出 36 是这个字节的 LSB。用零填充五个剩余高位比特。注:用零填充五个剩余比特(一直到高位端)
2、 02(0x02)读离散输入
读离散输入请求(PDU)
功能码 1 字节 0x02
起始地址 2 字节 0x0000-0Xffff
输入数量 2 字节 1-2000(0x7D0)
读离散输入响应(PDU)
功能码 1 字节 0x02
字节计数 1 字节 N
输入状态 N x 1个字节
N = 输出数据/8,如果余数不等于0,则N = N+1
错误
功能码 1 字节 0x82
异常码 1 字节 01或02或03或04
例:请求读取离散量输入197-218(0xC5-0xDA,22bit 0x16)
请求 响应
字段名 十六进制 字段名 十六进制
功能码 02 功能码 02
起始地址Hi 00 字节数 03
起始地址Lo C4 输出状态201-197 AC
输出数量Hi 00 输出状态212-205 DB
输出数量Lo 16 输出状态218-213 35
3、 03(0x03)读保持寄存器
使用该功能码读取一个远程设备中保持寄存器连续块的内容。请求PDU 指定了起始寄存器地址和寄存器数量。从零开始寻址寄存器,因此,寻址寄存器 1-16为 0-15。将响应报文中的寄存器数据分成每个寄存器有两字节,在每个字节中直接地调整二进制内容。对于每个寄存器,第一个字节包括高位比特,并且第二个字节包括低位比特。
读保持寄存器请求(PDU)
功能码 1 字节 0x03
起始地址 2 字节 0x0000-0Xffff
寄存器数量 2 字节 1-125(0x7D)
读保持寄存器响应(PDU)
功能码 1 字节 0x03
字节计数 1 字节 2 x N
寄存器值 N x 2个字节
N = 输出数据/8,如果余数不等于0,则N = N+1
错误
功能码 1 字节 0x83
异常码 1 字节 01或02或03或04
例:请求读取寄存器108-110(0x6C-0x6E,3byte)
请求 响应
字段名 十六进制 字段名 十六进制
功能码 03 功能码 02
起始地址Hi 00 字节数 06
起始地址Lo 6B 寄存器Hi(108) 02
寄存器编号Hi 00 寄存器Lo(108) 2B
寄存器编号Lo 03 寄存器Hi(109) 00
寄存器Lo(109) 00
寄存器Hi(110) 00
寄存器Lo(110) 64
将寄存器 108 的内容表示为两个十六进制字节值 02 2B,或十进制 555。将寄存器109-110 的内容分别表示为十六进制 00 00 和 00 64,或十进制 0 和 100。
4、 04(0x04)读输入寄存器
使用该功能码读取一个远程设备中1-125的连续输入寄存器。请求PDU 指定了起始寄存器地址和寄存器数量。从零开始寻址寄存器,因此,寻址寄存器1-16 为 0-15。将响应报文中的寄存器数据分成每个寄存器有两字节,在每个字节中直接地调整二进制内容。对于每个寄存器,第一个字节包括高位比特,并且第二个字节包括低位比特。
读输入寄存器请求(PDU)
功能码 1 字节 0x04
起始地址 2 字节 0x0000-0xFFFF
输入寄存器数量 2 字节 1-125(0x7D)
读输入寄存器响应(PDU)
功能码 1 字节 0x04
字节计数 1 字节 2 x N
输入寄存器 N x 2个字节
N = 输出数据/8,如果余数不等于0,则N = N+1
错误
功能码 1 字节 0x84
异常码 1 字节 01或02或03或04
例:请求输入寄存器9(0x09,1byte)
请求 响应
字段名 十六进制 字段名 十六进制
功能码 04 功能码 02
起始地址Hi 00 字节数 02
起始地址Lo 08 输入寄存器Hi(9) 00
输入寄存器数量Hi 00 输入寄存器Lo(9) 0A
输入寄存器数量Lo 01
将输入寄存器 9 的内容表示为两个十六进制字节值00 0A,或十进制 10。
5、 05(0x05)写单个线圈
使用该功能码写一个远程设备上的单个输出为ON或者OFF。
请求 PDU 指定了线圈的地址。从零开始寻址线圈,因此,寻址线圈 1 对应的是0。线圈值域的常量说明请求的ON/OFF 状态。十六进制值 0XFF00 请求线圈为 ON。十六进制值0X0000 请求线圈为OFF。其它所有值均为非法的,并且对线圈不起作用。
正常响应是请求的应答,在写入线圈状态之后返回这个正常响应
写单个线圈请求(PDU)
功能码 1 字节 0x05
起始地址 2 字节 0x0000-0xFFFF
输入数量 2 字节 0-255(0xFF)
写单个线圈响应(PDU)
功能码 1 字节 0x05
输出地址 2 字节 0x0000-0xFFFF
输出值 2个字节 0x0000 / 0xFF00
错误
功能码 1 字节 0x85
异常码 1 字节 01或02或03或04
例:写线圈173位ON(0xAD)
请求 响应
字段名 十六进制 字段名 十六进制
功能码 05 功能码 05
输出地址Hi 00 输出地址Hi 00
输出地址Lo AC 输出地址Lo AC
输出值Hi FF 输出值Hi FF
输出值Lo 00 输出值Lo 00
6、 06(0x06)写单个寄存器
使用该功能码写一个远程设备上的单个保持寄存器。
请求 PDU 指定了被写入寄存器的地址。从零开始寻址寄存器,因此,寻址寄存器1 为0。正常响应是请求的应答,在写入寄存器内容之后返回这个正常响应。
写单个寄存器请求(PDU)
功能码 1 字节0x06
起始地址 2 字节0x0000-0xFFFF
输入数量 2 字节0x0000-0xFFFF
写单个寄存器响应(PDU)
功能码1 字节0x06
字节计数2 字节0x0000-0xFFFF
输入状态2个字节0x0000-0xFFFF
错误
功能码1 字节0x86
异常码1 字节01或02或03或04
例:将十六进制00 03写入寄存器2
请求响应
字段名十六进制字段名十六进制
功能码06功能码06
寄存器地址Hi00输出地址Hi00
寄存器地址Lo01输出地址Lo01
寄存器值Hi00输出值Hi00
寄存器值Lo03输出值Lo03
7、 15(0x0F)写多个线圈
在一个远程设备中,使用该功能码强制线圈序列中的每个线圈为ON 或OFF。请求PDU 指定了写线圈的地址。从零开始寻址线圈,因此,寻址线圈 1 为 0。请求数据域的内容说明了被请求的ON/OFF 状态。数据比特位置中的逻辑“1”请求相应输出为ON。域比特位置中的逻辑“0”请求相应输出为 OFF。正常响应返回功能码、起始地址和强制的线圈数量。
写多个线圈请求(PDU)
功能码 1 字节 0x0F
起始地址 2 字节 0x0000-0xFFFF
输出数量 2 字节 0x0001-0x07B0
字节数 1 字节 N
输出值 N x 1 字节
N=输出数量/8,如果余数不等于 0,那么 N = N+1
写多个线圈响应(PDU)
功能码 1 字节 0x0F
起始地址 2 字节 0x0000-0xFFFF
输出数量 2个字节 0x0001-0x07B0
错误
功能码 1 字节 0x8F
异常码 1 字节 01或02或03或04
例:
这是一个请求从线圈 20 开始写入10 个线圈的实例:请求的数据内容为两个字节:十六进制CD 01 (二进制 1100 1101 0000 0001)。使用下列方法,二进制比特对应输出。
Bit 1100110100000001
输出2726252423222120------2928
传输的第一字节(十六进制CD)寻址为输出27-20,在这种设置中,最低有效比特寻址为最低输出(20)。传输的下一字节(十六进制01)寻址为输出29-28,在这种设置中,最低有效比特寻址为最低输出(28)。应该用零填充最后数据字节中的未使用比特。
请求响应
字段名 十六进制 字段名 十六进制
功能码 0F 功能码 0F
起始地址Hi 00 起始地址Hi 00
起始地址Lo 13 起始地址Lo 13
输出数量Hi 00 输出数量Hi 00
输出数量Lo 0A 输出数量Lo 0A
字节数 02
输出值Hi CD
输出值Lo 01
8、 16(0x10)写多个寄存器
在一个远程设备中,使用该功能码写连续寄存器块(1 至约120 个寄存器)。在请求数据域中说明了请求写入的值。每个寄存器将数据分成两字节。正常响应返回功能码、起始地址和被写入寄存器的数量。
写多个寄存器请求(PDU)
功能码 1 字节 0x10
起始地址 2 字节 0x0000-0xFFFF
寄存器数量 2 字节 0x0001-0x0078
字节数 1 字节 2 x N
寄存器值 N x 2 字节 值
N=寄存器数量
写多个寄存器响应(PDU)
功能码 1 字节 0x10
起始地址 2 字节 0x0000-0xFFFF
输出数量 2个字节 1至123
错误
功能码 1 字节 0x90
异常码 1 字节 01或02或03或04
例:
将十六进制00 0A和01 02写入以2开始的两个寄存器
请求 响应
字段名 十六进制 字段名 十六进制
功能码 10 功能码 10
起始地址Hi 00 起始地址Hi 00
起始地址Lo 01 起始地址Lo 01
寄存器数量Hi 00 寄存器数量Hi 00
寄存器数量Lo 02 寄存器数量Lo 02
字节数 04
寄存器值Hi 00
寄存器值Lo 0A
寄存器值Hi 01
寄存器值Lo 02
其他有关Modbus的介绍及使用的讲解:
Modbus协议深入讲解 - National Instruments
二、Modbus在PLC中的使用
三、Modbus在嵌入式操作系统中的使用
FreeMODBUS - A free MODBUS ASCII/RTU and TCP implementation - SILA