modbus协议
modbus协议
1 modbus协议
1.1 简介
Modbus诞生于1979年,包含Mod和Bus两部分。首先它是一种bus,即总线协议,总线就意味着有主机,有从机,这些设备在同一条总线上。
Modbus支持单主机,多个从机,最多支持247个从机设备。关于Mod,因为这种协议最早被用在PLC控制器中,准确的说是莫迪康(Modicon)公司的PLC控制器,这也是Modbus名称的由来。后来Modicon被施耐德电器收购,Modbus协议广泛用于工业控制器、HMI和传感器,逐渐被其他厂商所接受,成为工业领域通信协议的业界标准,并且现在是工业电子设备之间常用的连接方式。
优点:
- Modbus协议是开放、公开发表且无版权要求的,可以广泛应用。
- Modbus协议支持多种电器接口,包括RS232、RS485、TCP/IP等。
- Modbus协议消息帧格式简单、紧凑且易于理解。用户容易理解和使用,制造商易于开发和集成,方便形成工业控制网络。
1.2 Modbus通信过程
Modbus是一种一主多从的通信协议。在Modbus通信中,只有主设备可以发送请求,其他从设备接收主机发送的请求数据并进行响应,即半双工通讯。从设备可以是各种外围设备,如I/O传感器、阀门、网络驱动器或其他类型的测量设备。从站处理信息并使用Modbus将其数据发送给主站。换句话说,不能在同一时间内进行Modbus同步通信。主机一次只能向一个从机发送请求,总线上每次只有一个数据进行传输,即主机发送请求,从机响应。
1.3Modbus消息帧
Modbus协议定义了一个与基础通信层无关的简单协议数据单元PDU。
1.3.1 通用数据帧
地址域 | 功能码 | 数据 | 校验码 |
1字节 | 1字节 | N字节 | CRC:16字节,LRC:1字节 |
说明:每个划分字段都用16进制表示。
- 地址域:从机设备地址,通常1-247为有效地址。0为广播地址(用于接收主机的广播数据),每个从机在总线上地址必须唯一,只有与主机发送的地址码相符的从机才能响应返回数据。主节点通过将要联络的从节点地址放入消息中的地址域来选取需要通信的从设备。当从设备发送回应消息时,需要把自己的地址放入回应的地址域中,以便主节点知道是哪个设备做出的响应。
- 功能码:表明主节点请求数据的类型。当主节点向从设备发送消息时,功能码将告诉从设备需要执行哪些行为。例如:读取输入的开关状态,读一组寄存器的数据内容等。
- 数据:包含寄存器地址和寄存器数据等。
- 校验码:对数据进行冗余校验的结果,有CRC和LRC两种校验方式。
1.4 Modbus数据编码
Modbus使用Big-Endian表示地址和数据项。这意味着发送多字节时,首先发送最高有效位。例如:
寄存器大小 | 值 |
16 | 0x1234 |
则发送的第一个字节为0x12,然后发送0x34。
1.5 Modbus数据模型
为了抽象PLC中可访问的数据,Modbus协议定义了数据模型概念,分四种可访问的数据类型:
类型 | 大小 | 访问权限 | 元素地址前缀编码 | 元素地址范围(0~65535) | 元素地址范围(0~9999) |
输出线圈(Coils) | 1Bit | 可读可写 | 0 | 000000~065535 | 00000~09999 |
输入离散量(Discrete Input) | 1Bit | 只读 | 1 | 100000~165535 | 10000~19999 |
输入寄存器(Input Registers) | 16Bit | 只读 | 3 | 300000~365535 | 30000~39999 |
保持寄存器(Holding Registers) | 16Bit | 可读可写 | 4 | 400000~465535 | 40000~49999 |
- 输出线圈:属于开关量,数值范围ON或OFF。
- 输入离散量:属于离散量,数值范围ON或OFF。
- 输入寄存器:16Bit的寄存器,可以用作模拟量或16位打包输入点。
- 保持寄存器:16Bit的寄存器,既可以用作模拟量或16位打包输入点,也可以用作模拟量或16位打包输出点。
1.6 Modbus地址模型
数据模型时一种抽象,在实际使用时必须将其映射到真实物理存储区才能被访问。
Modbus允许设备将四种数据分别映射到不同的存储区块中,各个区块之间相互独立,使用不同的功能码可读取到不同数值。如下图所示:
数据模型中每一种数据类型最多允许有65506个元素,元素的地址编号从0开始,因此地址范围位:0~65535。
需要说明的是:65536是Modbus协议允许的最大元素范围,实际应用中一般不需要这么大的存储区,因此PLC厂家普遍采用10000以内的地址范围。
引入元素地址前缀编码,是为了简化数据模型与设备存储区的对应关系。
- 应用举例(单片机映射方法)
针对单片机可以通过以下方法来映射Modbus的虚拟地址:定义一个数组、寄存器起始地址、寄存器数量,寄存器数量最多可以有9999个。但实际情况下通常没有这么多,因此按照使用数量来定义,比如这里定义了9个(即,9990~9999)。
#define COILS_ADDR_START (9990) #define COILS_ADDR_END (9999) #define COILS_COUNT (9999 - 9990) #define DISCRETE_INPUT_ADDR_START (19990) #define DISCRETE_INPUT_ADDR_END (19999) #define DISCRETE_INPUT_COUNT (19999 - 19990) #define INPUT_REGISTERS_ADDR_START (39990) #define INPUT_REGISTERS_ADDR_END (39999) #define INPUT_REGISTERS_COUNT (39999 - 39990) #define HOLDING_REGISTERS_ADDR_START (49990) #define HOLDING_REGISTERS_ADDR_END (49999) #define HOLDING_REGISTERS_COUNT (49999 - 49990) bool coilsBuf[COILS_COUNT]; bool discreteInputBuf[DISCRETE_INPUT_COUNT]; unsigned short inputRegistersBuf[INPUT_REGISTERS_COUNT]; unsigned short holdingRegistersBuf[HOLDING_REGISTERS_COUNT];
根据主机提供的读地址或者写地址减去我们定义的寄存器起始地址,就可以转化为对应数据的数组索引,再根据主机提供的读数量或者写数量(注意这里是寄存器个数,而不是字节数)就可以知道索引范围。
1.7 功能编码
前面了解到主机设备可以访问或修改从机设备中存储的数据,为了方便主设备使用Modbus协议访问和修改从设备中存储的数据,Modbus协议根据数据模型和功能指定了一系列功能码。
功能码 | 名称 | 功能描述 |
01 | 读线圈状态 | 读位(读N个bit),读从机线圈寄存器,位操作 |
02 | 读输入离散量 | 读位(读N个bit),读从机离散输入寄存器,位操作 |
03 | 读多个保持寄存器 | 读整型,字符型,状态字,浮点型(读N 个 word)读保持寄存器,字节操作 |
04 | 读输入寄存器 | 读整型,状态字,浮点型(读 N 个word)读输入寄存器,字节操作 |
05 | 写单个线圈 | 写位(写 1 个 bit)—写线圈寄存器,位操作 |
06 | 写单个保持寄存器 | 写整型,字符型,状态字,浮点型(写一个 word )写保持寄存器,字节操作 |
07 | 读取异常状态 | 取得 8 个内部线圈的通断状态,这 8 个线圈的地址由控制器决定,用户逻辑可以将这些线圈定义,以说明从机状态,短报文适宜于迅速读取状态 |
08 | 回送诊断校验 | 把诊断校验报文送从机,以对通信处理进行评鉴 |
09 | 编程(只用于484) | 使主机模拟编程器作用,修改 PC 从机逻辑 |
0A | 控询(只用于484) | 可使主机与一台正在执行长程序任务从机通信,探询该从机是否已完成其操作任务,仅在含有功能码 9 的报文发送后,本功能码才发送 |
0B | 读取事件计数 | 可使主机发出单询问,并随即判定操作是否成功,尤其是该命令或其他应答产生通信错误时 |
0C | 读取通讯事件记录 | 可是主机检索每台从机的 ModBus 事务处理通信事件记录。如果某项事务处理完成,记录会给出有关错误 |
0D | 编程(184/384/484/584) | 可使主机模拟编程器功能修改 PC 从机逻辑 |
0E | 探询(184/384/484/584) | 可使主机与正在执行任务的从机通信,定期控询该从机是否已完成其程序操作,仅在含有功能 13 的报文发送后,本功能码才得发送 |
0F | 写多个线圈 | 可以写多个线圈强置一串连续逻辑线圈的通断 |
10 | 写多个保持寄存器 | 写多个保持寄存器把具体的二进制值装入一串连续的保持寄存器 |
11 | 报告从机标识 | 可使主机判断编址从机的类型及该从机运行指示灯的状态 |
12 | (884 和 MICRO84) | 可使主机模拟编程功能,修改 PC 状态逻辑 |
13 | 重置通信链路 | 发生非可修改错误后,是从机复位于已知状态,可重置顺序字节 |
14 | 读取通用参数(584L) | 显示扩展存储文件中的数据信息 |
15 | 写入通用参数(584L) | 把通用参数写入扩展存储文件 |
16~40 | 保留做扩展功能备用 | |
41~48 | 保留以备用户功能所用 | 留作用户功能的扩展编码 |
49~77 | 非法功能 | |
78~7F | 保留 | 留作内部作用 |
80~FF | 保留 | 用于异常应答 |
Modbus定义了大量的功能代码,但是更为常用的功能代码只有如下部分功能。
功能码 | 名称 | 功能 | 对应的地址类型 |
2 使用Modbus Poll 与 Modbus Slave 进行仿真
3 使用libmodbus库进行应用编码
3.1 常用函数
3.1.1 通信对象
modbus_t *mb;
3.1.2 创建一个串口通信对象
成功返回指针,失败则返回NULL,会调用malloc申请内存。
mb = modbus_new_rtu("/dev/ttySP1", 115200, 'N', 8, 1); //linux mb = modbus_new_rtu("COM1", 115200, 'N', 8, 1); //windows
3.1.3 创建一个TCP通信对象
mb = modbus_new_tcp("127.0.0.1", 5002); //TCP/IP
3.1.4 设置从机地址
成功返回0,否则返回-1。
int slave=1; modbus_set_slave(mb, slave);
3.1.5 连接主机
成功返回0,否则返回-1。
modbus_connect(mb);
3.1.6 设置响应超时时间
设置响应超时时间1s,200ms
modbus_set_response_timeout(mb, 1, 200000);
3.1.7 读取寄存器数据
读取寄存器数据,起始地址为2,数量为5,保持到table数组中。
成功返回5,否则返回-1。
uint16_t *table; ret = modbus_read_registers(mb, 2, 5, table);
3.1.8 写单个寄存器
写单个寄存器,地址2写入56,成功返回1,否则返回-1。
modbus_write_register(mb, 2, 56);
3.1.9 写多个寄存器
写多个寄存器,起始地址12,写入5个数据,成功返回5,否则返回-1。
uint16_t table[5] = {11, 22, 33, 44, 55}; modbus_write_registers(mb, 12, 5, table);
3.1.10 写单个线圈
写单个线圈,线圈地址11写入TRUE,成功返回1,否则返回-1。
modbus_write_bit(mb, 11, TRUE);
3.1.11 查看错误信息
char *err_str; err_str = modbus_strerror(errno);
3.1.12 关闭设备和释放内存
modbus_close(mb);
modbus_free(mb);
3.2 libmodbus实战
3.2.1 主机设备编码
实现对地址为1的从设备,读取地址为15、16、17的保持寄存器数据,进行+1操作后,再写入从设备。
// ModbusPoll.cpp : This file contains the 'main' function. Program execution begins and ends there. // #include "stdio.h" #include "stdlib.h" #include "string.h" #include "modbus.h" #define PORT_NAME "COM1" int main(int argc, char* argv[]) { int ret; uint16_t table[3]; modbus_t* mb; char port[20]; printf("argc = %d, argv[1] = %s\n", argc, argv[1]); if (argc == 2) strcpy(port, argv[1]); else strcpy(port, PORT_NAME); printf("libmodbus modbu-rtu master demo: %s, 9200, N, 8, 1\n", port); mb = modbus_new_rtu(port, 9200, 'N', 8, 1); if (mb == NULL) { modbus_free(mb); printf("new rtu failed: %s\n", modbus_strerror(errno)); return 0; } modbus_set_slave(mb, 1); ret = modbus_connect(mb); if (ret == -1) { modbus_close(mb); modbus_free(mb); printf("connect failed: %s\n", modbus_strerror(errno)); return 0; } while (1) { ret = modbus_read_registers(mb, 0x0F, 3, table); if (ret == 3) printf("read success : 0x%02x 0x%02x 0x%02x \n", table[0], table[1], table[2]); else { printf("read error: %s\n", modbus_strerror(errno)); break; } for (int i = 0; i < 3; i++) table[i] += 1; ret = modbus_write_registers(mb, 0x0F, 3, table); if (ret == 3) printf("write success: 0x%02x 0x%02x 0x%02x \n", table[0], table[1], table[2]); else { printf("write error: %s\n", modbus_strerror(errno)); break; } Sleep(1000); } modbus_close(mb); modbus_free(mb); system("pause"); return 0; }
3.2.2 从机设备代码
创建从机设备,地址为1,初始化了3个保持寄存器,地址分别为15、16、17,数据分别为0x1001、0x1002、0x1003。
// ModbusSlave.cpp : This file contains the 'main' function. Program execution begins and ends there. // #include "stdio.h" #include "stdlib.h" #include "string.h" #include "modbus.h" #define PORT_NAME "COM2" int main(int argc, char* argv[]) { int ret = 0; uint8_t device = 1; uint8_t query[MODBUS_RTU_MAX_ADU_LENGTH] = {0}; modbus_t* mb; modbus_mapping_t* mb_mapping; char port[20]; printf("argc = %d, argv[1] = %s\n", argc, argv[1]); if (argc == 2) strcpy(port, argv[1]); else strcpy(port, PORT_NAME); printf("libmodbus modbu-rtu slave demo: %s, 9200, N, 8, 1\n", port); mb = modbus_new_rtu(port, 9200, 'N', 8, 1); if (mb == NULL) { modbus_free(mb); printf("new rtu failed: %s\n", modbus_strerror(errno)); return 0; } //register: 15/16/17 mb_mapping = modbus_mapping_new_start_address(0, 0, 0, 0, 15, 3, 0, 0); if (mb_mapping == NULL) { modbus_free(mb); printf("new mapping failed: %s\n", modbus_strerror(errno)); return 0; } //保持寄存器数据 mb_mapping->tab_registers[0] = 0x1001; mb_mapping->tab_registers[1] = 0x1002; mb_mapping->tab_registers[2] = 0x1003; modbus_set_slave(mb, device); ret = modbus_connect(mb); if (ret == -1) { modbus_free(mb); printf("connect failed: %s\n", modbus_strerror(errno)); return 0; } printf("create modbus slave success\n"); while (1) { do { ret = modbus_receive(mb, query); //轮询串口数据, } while (ret == 0); if (ret > 0) //接收到的报文长度 { printf("len=%02d: ", ret); for (int idx = 0; idx < ret; idx++) { printf(" %02x", query[idx]); } printf("\n"); modbus_reply(mb, query, ret, mb_mapping); } else { printf("quit the loop: %s", modbus_strerror(errno)); modbus_mapping_free(mb_mapping); break; } } modbus_close(mb); modbus_free(mb); return 0; }
3.2.3 运行
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 提示词工程——AI应用必不可少的技术
· 地球OL攻略 —— 某应届生求职总结
· 字符编码:从基础到乱码解决
· SpringCloud带你走进微服务的世界