Modbus RTU
与 Modbus TCP 的区别
一般在工业场景中,使用 Modbus RTU 的场景更多一些,Modbus RTU 基于串行协议进行收发数据,包括 RS232/485 等工业总线协议。采用主从问答式(master / slave)通信。
与 Modbus TCP 不同的是,RTU 没有报文头 MBAP 字段,但是在尾部增加了两个 CRC 检验字节(CRC16),因为网络协议中自带校验,所以在 TCP 协议中不需要使用 CRC 校验码。
RTU 和 TCP 的总体使用方法基本一致,只是在创建 Modbus 对象时有所不同。TCP 需要传入网络socket 信息;而 RTU 需要传入串口相关信息。
特点
通信
采用主从问答式(master / slave)通信,由主机发起,一问一答。
设置串口参数
波特率:9600
数据位:8
停止位:1
无流控
协议格式(地址码 + 功能码 + 数据 + 校验码)
Modbus RTU 数据帧包含:地址码、功能码、数据、校验码。
地址码: 从机 ID
功能码: 同 Modbus TCP
数据: 起始地址、数量、数据
CRC 校验码: 两个字节,对 地址码、功能码、数据 进行校验,可以通过函数自动生成
报文详解
(👆 链接至另一博主,放心跳转)
以 03 功能码为例:
主机 ——> 从机:
从机 ——> 主机:
模拟器的安装、配置、使用
实际硬件产品成本较高,可以使用一系列 Modbus 软件模拟器,进行数据模拟,从而分析 Modbus RTU 协议。
所用工具
Modbus Slave、vspd 虚拟串口、UartAssist 串口调试工具、虚拟机
安装与配置
一)vspd 虚拟串口的安装
1)将压缩包解压后,双击 vspd.exe 文件进行安装;
2)打开软件,添加 COM1 和 COM2 端口(用完之后记得删除端口);
3)打开设备管理器,出现如下图所示即可;
4)可以汉化,将 Cracked 下的文件复制到软件安装目录即可。
二)虚拟机绑定端口
1)VMware 虚拟机(注意不是 ubuntu)在系统关机(必须是关机状态,挂起不行)状态下,
点击:虚拟机 ——> 设置 ——> 硬件 ——> 添加串行端口,添加 COM1;
2)添加完成后,第一次使用需要将电脑重启;
3)重启之后,打开虚拟机,点击虚拟机 ——> 可移动设备 ——> 串行端口 ——> 连接;
4)在终端输入dmesg|grep tty,查看对应的设备文件,其中默认的会有 ttyS0 文件,
其余一个(ttyS1 或 ttyS2)就是虚拟串口对应的设备文件。
三)测试通信
1)Windows 下打开串口调试工具,选择好串口 COM2 ——> COM1,设置对应的波特率;
2)以下步骤在虚拟机下完成,在虚拟机安装 minicom 软件;sudo apt-get install minicom
3)在终端执行 sudo minicom -s ,选择 Serial port setup;
4)设置设备文件,波特率,关闭流控;(按 Ctrl + 相应字母)
5)回车,保存修改,选择 Save setup as dfl;
6)可以在以下界面输入字符,查看串口助手的显示情况;
7)测试通信(终端输入不可见);
8)退出:Ctrl + A,然后按 Z,在弹出的界面里输入X,即可退出。
四)将 Modbus Slave 模拟器作为 RTU 设备的从机
虚拟机绑定 COM1 端口,Modbus Slave 连接 COM2 端口,虚拟机通过编程测试串口通信;
五)可能遇到的问题
虚拟串口完成主机与 vmware 下虚拟机进行串口通信
VSPD 虚拟串口工具 —— 从此告别硬件串口调试
vmware 虚拟机检测不到 vspd 虚拟串口问题
(👆 链接至其他博主,放心跳转)
Modbus 库
库的安装
安装与配置
1)在 linux 中解压压缩包,tar -xvf libmodbus-3.1.7.tar.gz ;
2)进入源码目录,创建文件夹(存放头文件、库文件);
cd libmodbus-3.1.7 mkdir install
3)执行脚本 configure,进行安装配置(指定安装目录);
./configure--prefix=$PWD/install
4)执行 make 和 make install
make // 编译make install // 安装
5)执行完成后会在 install 文件夹下产生对应的头文件、库文件。
使用
1、一般操作:
gcc xxx.c -I ./install/include/modbus -L ./install/lib -lmodbus
./a.out
-I : 后需要指定出头文件的路径(大写的i)
-L : 后需要指定库的路径
-l : 后需要指定库名(小写的L)
2、要想编译方便,可以将头文件和库文件放到系统路径下:
sudo cp install/include/modbus/*.h /usr/include
sudo cp install/lib/* -r /lib -d
后期编译时,就可以直接 gcc xxx.c -lmodbus,
头文件默认搜索路径:/usr/include、/usr/local/include
库文件默认搜索路径:/lib、/usr/lib
函数接口
0x01(modbus_read_bits)
int modbus_read_bits(modbus_t *ctx, int addr, int nb, uint8_t *dest); 功能:读取线圈状态,可读取多个连续线圈的状态(对应功能码为0x01)
参数:ctx : Modbus实例addr : 寄存器起始地址nb : 寄存器个数dest : 得到的状态值
0x02(modbus_read_input_bits)
int modbus_read_input_bits(modbus_t *ctx, int addr, int nb, uint8_t *dest); 功能:读取输入状态,可读取多个连续输入的状态(对应功能码为0x02)
参数:ctx : Modbus 实例addr : 寄存器起始地址nb : 寄存器个数dest : 得到的状态值
返回值:成功:返回nb的值
0x03(modbus_read_registers)
int modbus_read_registers(modbus_t *ctx, int addr, int nb, uint16_t *dest); 功能:读取保持寄存器的值,可读取多个连续保持寄存器的值(对应功能码为0x03)
参数:ctx : Modbus 实例addr : 寄存器起始地址nb : 寄存器个数dest : 得到的寄存器的值
返回值:成功:读到寄存器的个数失败:-1
0x04(modbus_read_input_registers)
int modbus_read_input_registers(modbus_t *ctx, int addr, int nb, uint16_t *dest);功能:读输入寄存器的值,可读取多个连续输入寄存器的值(对应功能码为0x04)
参数:ctx : Modbus 实例addr : 寄存器起始地址nb : 寄存器个数dest : 得到的寄存器的值
返回值:成功:读到寄存器的个数失败:-1
0x05(modbus_write_bit)
int modbus_write_bit(modbus_t *ctx, int addr, int status);功能:写入单个线圈的状态(对应功能码为0x05)
参数:ctx : Modbus 实例addr : 线圈地址status: 线圈状态
返回值:成功:0失败:-1
0x06(modbus_write_register)
int modbus_write_register(modbus_t *ctx, int addr, int value);功能:写入单个寄存器(对应功能码为0x06)
参数: ctx : Modbus 实例addr : 寄存器地址value : 寄存器的值
返回值:成功:0失败:-1
0x0F(modbus_write_bits)
int modbus_write_bits(modbus_t *ctx, int addr, int nb, const uint8_t *src);功能:写入多个连续线圈的状态(对应功能码为15)
参数:ctx : Modbus 实例addr : 线圈地址nb : 线圈个数src : 多个线圈状态
返回值:成功:0失败:-1
0x10(modbus_write_registers)
int modbus_write_registers(modbus_t *ctx, int addr, int nb, const uint16_t *src);功能:写入多个连续寄存器(对应功能码为16)
参数:ctx : Modbus 实例addr : 寄存器地址nb : 寄存器的个数src : 多个寄存器的值
返回值:成功:0失败:-1
编程流程
1)创建实例(modbus_new_tcp / modbus_new_rtu)
modbus_t *modbus_new_tcp(const char *ip, int port); 功能:以 TCP 方式创建 Modbus 实例,并初始化
参数:ip : ip 地址port: 端口号
返回值:成功:Modbus 实例失败:NULL
modbus_t *modbus_new_rtu(const char *device, int baud, char parity, int data_bit, int stop_bit);功能:用于创建一个用于 Modbus RTU 通信的 modbus_t 结构体实例
参数:device: 要打开的串口设备的路径(例如:"/dev/ttyUSB0")baud: 波特率(如 9600、19200 等)parity: 校验位(可选值:'N' - 无校验、'E' - 偶校验、'O' - 奇校验)data_bit: 数据位(常用值为 8)stop_bit: 停止位(常用值为 1)返回值:成功:Modbus 实例失败:NULL
2)设置从机地址(modbus_set_slave)
int modbus_set_slave(modbus_t *ctx, int slave);
功能:设置从机ID
参数:ctx : Modbus 实例slave: 从机 ID
返回值:成功:0失败:-1
3)建立连接(modbus_connect)
int modbus_connect(modbus_t *ctx);
功能:和从机(slave)建立连接
参数:ctx: Modbus 实例
返回值:成功:0失败:-1
4)各种操作(见函数接口)
5)关闭套接字(modbus_close)
void modbus_close(modbus_t *ctx);
功能:关闭套接字
参数:ctx:Modbus 实例
6)释放实例(modbus_free)
void modbus_free(modbus_t *ctx);
功能:释放 Modbus 实例
参数:ctx:Modbus 实例
练习:
// 和 Slave 通信,读保持寄存器的三个值#include <stdio.h>
#include <modbus.h>
#include <stdlib.h>
#include <string.h>
#include <modbus-rtu.h>int main(int argc, char const *argv[])
{ if (argc != 3){printf("Please input %s <ip> <port>. \n", argv[0]);return -1;}modbus_t *ctx;ctx = modbus_new_tcp(argv[1], atoi(argv[2]));// ctx = modbus_new_rtu("/dev/ttyS1", 9600, N, 8, 1);if (ctx == NULL){perror("Failed to modbus_new_tcp"); // "Failed to modbus_new_rtu"return -1;}if (modbus_set_slave(ctx, 1) < 0){perror("Failed to modbus_set_slave");return -1;}if (modbus_connect(ctx) < 0){perror("Failed to modbus_connect");return -1;}uint16_t dest[32] = {};if (modbus_read_registers(ctx, 0, 3, dest) < 0){perror("Failed to modbus_read_registers");return -1;}for (int i = 0; i < 3; i++)printf("%#x ", dest[i]);putchar(10);for (int i = 0; i < 3; i++)printf("%d ", dest[i]);putchar(10);modbus_close(ctx);modbus_free(ctx);return 0;
}
运行结果如下:
注意:
1、使用 Modbus TCP 协议时,将 slave 的 connect 设置为“Modbus TCP/IP”。
2、使用 Modbus RTU 协议时,将 slave 的 connect 设置为“Serial Port”。
小目标:
编程实现采集传感器数据和控制硬件设备(传感器和硬件通过 slave 模拟)。
传感器:2个,光线传感器、加速度传感器(x \ y \ z);
硬件设备:2个,LED灯、蜂鸣器。
要求:
1、多任务编程:多线程、多进程
2、循环 1s 采集一次数据,并将数据打印至终端
3、同时从终端输入指令控制硬件设备
0 1:LED 灯开
0 0:LED 灯关
1 1:蜂鸣器开
1 0:蜂鸣器关
// 同步实现#include <stdio.h>
#include <modbus.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>modbus_t *ctx;
sem_t sem1, sem2;void *collector(void *arg){uint16_t *dest = (uint16_t *)arg;while (1){sleep(5);sem_wait(&sem1);if (modbus_read_registers(ctx, 0, 4, dest) < 0){perror("Failed to modbus_read_registers");return NULL;}for (int i = 0; i < 4; i++)printf("%d ", dest[i]);putchar(10);sem_post(&sem2);}pthread_exit(0);
}void *control(void *arg){uint8_t writer[2];while (1){sem_wait(&sem2);printf("Please set status of LED or BUZZER: ");for (int i = 0; i < 2; i++)scanf("%hhu", &writer[i]);modbus_write_bit(ctx, writer[0], writer[1]);sem_post(&sem1);}pthread_exit(0);
}int main(int argc, char const *argv[])
{ if (argc != 3){printf("Please input %s <ip> <port>. \n", argv[0]);return -1;}ctx = modbus_new_tcp(argv[1], atoi(argv[2]));if (ctx == NULL){perror("Failed to modbus_new_tcp");return -1;}if (modbus_set_slave(ctx, 1) < 0){perror("Failed to modbus_set_slave");return -1;}if (modbus_connect(ctx) < 0){perror("Failed to modbus_connect");return -1;}uint16_t dest[32] = {};pthread_t tid1, tid2;sem_init(&sem1, 0, 1);sem_init(&sem2, 0, 0);if (pthread_create(&tid1, NULL, collector, dest)){perror("Failed to create a thread named collector");return -1;}pthread_detach(tid1);if (pthread_create(&tid2, NULL, control, NULL)){perror("Failed to create a thread named input");return -1;}pthread_detach(tid2);while (1);modbus_close(ctx);modbus_free(ctx);return 0;
}
实现效果如下: