Modbus协议
Modbus协议是应用于电子控制器上的一种通用语言。通过此协议,可以实现控制器相互之间、控制器经由网络和真实设备之间的通信。
它是一种Master/Slave协议,它并不关心是谁与谁在通信。
Modbus特点
- 标准开放、公开发表、无版税要求、无许可证费(没有费用);
- 支持多种电气接口(RS232、RS422、RS485、RJ45);各种介质传输(双绞线、网线),支持串口、网口;
- 格式简单、紧凑、通俗易懂,容易上手(好用) ;
提前准备仿真工具
Modbus Slave
从机模拟软件,用于开发和调试;Modbus Poll
主机模拟软件;Configure Virtual Serial Port Driver
虚拟串口端口软件;
- 启动虚拟串口助手
- 添加端口
- 启动主从机
- 从机连接串口(先连接从机)
- 从机连接成功
- 主机连接串口
- 主机连接成功
通过c#连接
- 关闭仿真工具的主机连接,使用c#代码替代仿真工具的主机连接
SerialPort对象
在System.IO.Ports
命名空间,表示串行端口资源。
new SerialPort(portName, baudRate, parity, dataBits, stopBits);
SerialPort参数
BaudRate
: 波特率,传输速率一般为9600
PortName
: 端口名
Parity
: 奇偶校验位
DataBits
: 数据位,实际数据位,一般用8位
StopBits
: 停止位
- 一般记住9600-N-8-1即可,9600波特率,N无检验位,8位数据位,1位停止位。
NModbus
- NModbus:是Modbus协议的C#实现。提供与Modbus从站兼容设备和应用程序的连接。支持ASCII、RTU、TCP和UDP协议。
- NModbus4: 一个github的开源实现类库,是NModbus的一个分支。
- 地址:https://github.com/NModbus4/NModbus4
相关读写方法: 每个方法都有各自的异步方法实现。
bool[] ReadCoils 读线圈 01
bool[]ReadInputs 读输入线圈 02
ushort[] ReadHoldingRegisters 读保持寄存器 03
ushort[] ReadInputRegisters 读输入寄存器 04
void WriteSingleCoil 写单线圈 05
void WriteSingleRegister 写单保存寄存器 06
void WriteMultipleCoils 写多线圈 15
void WriteMultipleRegisters 写多保持寄存器 16
ushort[] ReadWriteMultipleRegisters 读写多个保持寄存器 23
NModbus4使用方式
- 将NModbus4类库导入项目中(可直接在Nuget中搜索到并安装到当前项目中)
- 引用命名空间
- 开始使用
串口通信
1)创建SerialPort对象,配置相关参数,打开串口 2)调用相应方法创建Modbus主站对象(RTU、ASCII) 3) 配置参数 4)调用相关方法执行操作
网口通信
1) 定义IP地址、端口号,创建TcpClient/UdpClient 2) 建立与远程的连接 3)创建Modbus 主站对象(IP) 4)配置相关参数 5)调用相关方法执行操作
打开连接
// 串口
SerialPort port = null;
// 串口通信主站
ModbusSerialMaster serialMaster = null;
// 网口通信主站
ModbusIpMaster ipMaster = null;
// 串口通信主站和网口通信主站都实现了的接口
IModbusMaster master = null;
bool isRTU= true;
// 1. 串口
port = new SerialPort("COM2", 9600, Parity.None, 8, StopBits.One);//创建串口
if(port != null) {
port.Open();
// RTU主站
if (isRTU)
serialMaster = ModbusSerialMaster.CreateRtu(port);
else
// Ascii
serialMaster = ModbusSerialMaster.CreateAscii(port);
master = serialMaster;
}
// 2. TCP网口
TcpClient tcpClient = new TcpClient();
tcpClient.Connect(192.168.1.100, 8000);//连接到主机
ipMaster = ModbusIpMaster.CreateIp(tcpClient);//Ip 主站
master = ipMaster;
// 3. UDP
UdpClient udpClient = new UdpClient();
udpClient.Connect(192.168.1.100, 8000);//连接到主机
ipMaster = ModbusIpMaster.CreateIp(udpClient);//Ip 主站
master = ipMaster;
关闭连接
if(port!=null&&port.IsOpen) port.Close();//关闭串口连接
// client是上面的tcp或udp连接
else if(client!=null &&client.Connected)
{
client.Close();
}
master.Dispose();
lblMessage.Text = "关闭成功!";
数据
寄存器: 存储数据的容器,寄存器有多个,每个寄存器有编号,用寄存器地址区分不同的寄存器;
存储区 | 对象类型 | 寄存器地址 | 访问类型(针对程序) | 说明 |
---|---|---|---|---|
离散量输入线圈 | 单个bit | 10001至19999 | 只读 | I/O系统提供这种类型数据 |
线圈 | 单个bit | 00001至09999 | 读写 | 通过应用程序改变这种类型数据 |
输入寄存器 | 16-位字 | 30001至39999 | 只读 | I/O系统提供这种类型数据 |
保持寄存器 | 16-位字 | 40001至49999 | 读写 | 通过应用程序改变这种类型数据 |
功能码:向服务器指示将执行哪种操作:位、字、双字;
- 仿真工具主站功能设置,一一对应
在菜单栏setUp -- Read Write Definition打开:
注意:主从站读写时,线圈寄存器应一一对应,线圈对应线圈,寄存器对应寄存器,否则无法读写;
数据读取
在从站对应的读取
private void btnRead_Click(object sender, EventArgs e)
{
string readType = cboReadTypes.Text.Trim();
// 从站地址
byte slaveAddr = byte.Parse(txtRSlaveId.Text.Trim());
// 开始地址
ushort startAddr = ushort.Parse(txtRStartAddress.Text.Trim());
// 读取数量
ushort readCount = ushort.Parse(txtCount.Text.Trim());
switch(readType)
{
case "读线圈":
bool[] blVals = master.ReadCoils(slaveAddr, startAddr, readCount);
// 将读取到的blVals元素,true改为1,false改为0,再转成字符串以","连接
txtReadDatas.Text = string.Join(",", blVals.Select(b => b ? "1" : "0"));
break;
case "读输入线圈":
bool[] blInputVals = master.ReadInputs(slaveAddr, startAddr, readCount);
txtReadDatas.Text = string.Join(",", blInputVals.Select(b => b ? "1" : "0"));
break;
case "读保持寄存器":
ushort[] uDatas= master.ReadHoldingRegisters(slaveAddr, startAddr, readCount);
txtReadDatas.Text = string.Join(",", uDatas);
break;
case "读输入寄存器":
ushort[] uDatas1 = master.ReadInputRegisters(slaveAddr, startAddr, readCount);
txtReadDatas.Text = string.Join(",", uDatas1);
break;
}
}
数据写入
- 写单线圈
private void btnWrite_Click(object sender, EventArgs e)
{
string writeType = cboWriteTypes.Text.Trim();
string vals = txtRegisterWVals.Text.Trim();
//从站地址
byte slaveAddr = byte.Parse(txtWSlaveId.Text.Trim());
//开始地址
ushort startAddr = ushort.Parse(txtWStartAddress.Text.Trim());
switch (writeType)
{
case "写单线圈":
bool blVal = vals == "1" ? true : false;
master.WriteSingleCoil(slaveAddr, startAddr, blVal);
break;
case "写单保持寄存器":
// 转成无符号
ushort uVal01 = ushort.Parse(vals);
master.WriteSingleRegister(slaveAddr, startAddr, uVal01);
break;
case "写多线圈":
bool[] blVals = vals.Split(',').Select(s => s == "1" ? true : false).ToArray();//bool数组
master.WriteMultipleCoils(slaveAddr, startAddr, blVals);
break;
case "写多保持寄存器":
ushort[] uVals02 = vals.Split(',').Select(s => ushort.Parse(s)).ToArray();
master.WriteMultipleRegisters(slaveAddr, startAddr, uVals02);
break;
}
}
- 写单保持寄存器
- 写多线圈
1
1
1
1
1
1
1
1
1
1