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;
}
实现效果如下: