1、客户端选择
客户端可以是一个程序或一个设备,这里我以C#WINFORM程序来实现客户机与PLC的Modbustcp服务器通信,开发环境是VS2019,.NET Framework版本是4.7.2
2、创建winform程序
创建类库
编写C#各种类的转换库,该库由我提供,不用操心,文章最后提供。
项目引入这个类库
3、引入Nmodbus4协议
找到项目,找到引用,右键“管理nuget程序”,在下面对话框操作
4、界面布局如下:
布局中用到的是下拉框combobox,文本框textbox,按钮button,标签label
这个IP地址和端口号是与这里对应
5、窗体定义两个变量,并引入对应的命令空间
ModbusIpMaster master = null;//modbus对象
TcpClient tcpClient = null;//tcp客户端对象
6、连接按钮代码
private void btnOpen_Click(object sender, EventArgs e)
{
string ip = txtIPAddress.Text.Trim();
bool t = IsIP(ip);
if (t)
{
try
{
int port = int.Parse(txtPort.Text.Trim());
tcpClient = new TcpClient();
tcpClient.Connect(ip, port);//连接到主机
master = ModbusIpMaster.CreateIp(tcpClient);//Ip 主站
master.Transport.ReadTimeout = 1000;//读超时
master.Transport.WriteTimeout = 1000;//写超时
master.Transport.Retries = 3;//尝试重复连接次数
master.Transport.WaitToRetryMilliseconds = 200;//尝试重复连接间隔
lblMessage.Text = "连接成功!";
btnOpen.Enabled = false;
}
catch (Exception ex)
{
MessageBox.Show("连接失败," + ex.Message);
}
}
else
{
MessageBox.Show("无效的ip地址!");
}
}
7、读取的代码--ushort类型
本例子中只用到了读取保存寄存器这个功能码,即ReadHoldingRegisters(从站地址,开始地址,寄存器数量)
/// <summary>
/// 读取
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void myread_Click(object sender, EventArgs e)
{
//由于NModbus4读取到寄存器的数据都是ushort类型
//功能码
string readType = cboReadTypes.Text.Trim();
//从站地址
byte slaveAddr = byte.Parse(txtRSlaveId.Text.Trim());
//开始地址
ushort startAddr = ushort.Parse(txtRStartAddress.Text.Trim());
//读取数量
ushort readCount = ushort.Parse(txtRCount.Text.Trim());
switch (readType)
{
case "读线圈":
bool[] blVals = master.ReadCoils(slaveAddr, startAddr, readCount);
txtReadDatas1.Text = string.Join(",", blVals.Select(b => b ? "1" : "0"));
break;
case "读输入线圈":
bool[] blInputVals = master.ReadInputs(slaveAddr, startAddr, readCount);
txtReadDatas1.Text = string.Join(",", blInputVals.Select(b => b ? "1" : "0"));
break;
case "读保持寄存器":
//情况1:ushort到ushort类型:即读取无符号的整数,如23,89,处理方法是:原封不动
//ushort[] uDatas = master.ReadHoldingRegisters(slaveAddr, startAddr, readCount);
//txtReadDatas.Text = string.Join(",", uDatas);
//功能码
string dataType = cmddatatype.Text.Trim();
switch (dataType)
{
case "ushort":
//利用token循环读取
ushortctsRead = new CancellationTokenSource();
Task.Run(new Action(() =>
{
ReadUshortFromPLC(slaveAddr, startAddr, readCount);
}), ushortctsRead.Token);
break;
case "short":
//利用token循环读取
shortctsRead = new CancellationTokenSource();
Task.Run(new Action(() =>
{
ReadShortFromPLC(slaveAddr, startAddr, readCount);
}), shortctsRead.Token);
break;
case "float":
//利用token循环读取
floatctsRead = new CancellationTokenSource();
Task.Run(new Action(() =>
{
ReadFloatFromPLC(slaveAddr, startAddr, readCount);
}), floatctsRead.Token);
break;
}
break;
case "读输入寄存器":
ushort[] uDatas1 = master.ReadInputRegisters(slaveAddr, startAddr, readCount);
txtReadDatas1.Text = string.Join(",", uDatas1);
break;
}
}
这里要注意,
NModbus4读取到寄存器的数据都是ushort类型
NModbus4读取到寄存器的数据都是ushort类型
代码中用到ReadUshortFromPLC方法,ReadShortFromPLC方法,ReadFloatFromPLC方法在本文最后链接都会提供
运行程序,连接成功,读取数据
注意这里,从站地址一般都是1,除非你改了,开始地址是0,表示寄存器的起始地址,数量是3,表示读取3个寄存器数量,也就是前面3个变量,m1-speed,m1-duaror,m1-level
这里为什么数量不能是4,因为第4个变量是real,它占2个寄存器,即占4个字节,它不是ushort类型,这里地址也不能是%DB3.DBW4这种写法,这不是S7协议读取变量,是MODBUS读取寄存器,两者不一样的,别糊涂了,各位长老。
8、读取的代码--float类型
很多人搞不清楚这个开始地址和数量,这个开始地址是Modbus的地址,Modbus地址编号从0开始,因此8个变量的地址就是0,1,2,3,4,5,6,7,数量是指要读取的寄存器个数,word占一个,real占2个,这里很难理解,比较绕比较晕,一个是PLC地址,一个是MODBUS地址
我们要读的温度是第3个寄存器,它是real类型,占2个寄存器数量
如果要读取“摩头2温度”,怎么读了?
大家想想,为什么开始地址是8?
9、写入的代码--ushort类型
/// <summary>
/// 写入
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnWrite_Click(object sender, EventArgs e)
{
//功能码
string writeType = cboWriteTypes.Text.Trim();
//从站地址
byte slaveAddr = byte.Parse(txtWSlaveId.Text.Trim());
//开始地址
ushort startAddr = ushort.Parse(txtWStartAddress.Text.Trim());
//数量
//实际数量
string objWriteVals = "";
string dataType = cmddatatype2.Text.Trim();
switch (dataType)
{
case "ushort":
objWriteVals = txtWriteDatas1.Text.Trim();
break;
case "short":
objWriteVals = txtWriteDatas2.Text.Trim();
break;
case "float":
objWriteVals = txtWriteDatas3.Text.Trim();
break;
}
ushort writeCount = ushort.Parse(txtWCount.Text.Trim());
ushort objWCount = (ushort)objWriteVals.Split(',').Length;
//实际数量与要求数量不一致,不允许操作
if (writeCount != objWCount)
{
MessageBox.Show("写入值的数量不正确!");
return;
}
string vals = objWriteVals;
switch (writeType)
{
case "写单线圈":
bool blVal = vals == "1" ? true : false;
try
{
master.WriteSingleCoil(slaveAddr, startAddr, blVal);
MessageBox.Show("【单线圈】写入成功!");
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
break;
case "写单保持寄存器":
ushort uVal01 = ushort.Parse(vals);
try
{
master.WriteSingleRegister(slaveAddr, startAddr, uVal01);
MessageBox.Show("【单保持寄存器】写入成功!");
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
break;
case "写多线圈":
bool[] blVals = vals.Split(',').Select(s => s == "1" ? true : false).ToArray();//bool数组
try
{
master.WriteMultipleCoils(slaveAddr, startAddr, blVals);
MessageBox.Show("【多线圈】写入成功!");
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
break;
case "写多保持寄存器":
try
{
//功能码
//string dataType = cmddatatype2.Text.Trim();
switch (dataType)
{
case "ushort":
情况1:写入无符号的整数,即写入ushort数据,如写入33,44
ushort[] uVals01 = vals.Split(',').Select(s => ushort.Parse(s)).ToArray();
master.WriteMultipleRegisters(startAddr, uVals01);
break;
case "short":
//情况2:写入有符号的整数,即写入short数据,如写入-133,-65,98等,处理方法是:short[]=>byte[]=>ushort[],情况2包括了情况1
short[] uVals02 = vals.Split(',').Select(s => short.Parse(s)).ToArray();
byte[] y2 = ByteArrayLib.GetByteArrayFromShortArray(uVals02);
ushort[] ushorts2 = UShortLib.GetUShortArrayFromByteArray(y2);
master.WriteMultipleRegisters(startAddr, ushorts2);
MessageBox.Show("【short类型数据】写入成功!");
break;
case "float":
//情况3:写入有符号的小数,即写入float数据,如写入-6.3,-2.65,56.893,51,-465等,处理方法是:float[]=>byte[]=>ushort[],情况3包括了情况2和情况1
float[] uVals03 = vals.Split(',').Select(s => float.Parse(s)).ToArray();
byte[] y3 = ByteArrayLib.GetByteArrayFromFloatArray(uVals03);
ushort[] ushorts3 = UShortLib.GetUShortArrayFromByteArray(y3);
master.WriteMultipleRegisters(startAddr, ushorts3);
MessageBox.Show("【float类型数据】写入成功!");
break;
}
情况2:写入有符号的整数,即写入short数据,如写入-133,-65,98等,处理方法是:short[]=>byte[]=>ushort[],情况2包括了情况1
//short[] uVals02 = vals.Split(',').Select(s => short.Parse(s)).ToArray();
//byte[] y = ByteArrayLib.GetByteArrayFromShortArray(uVals02);
//ushort[] ushorts = UShortLib.GetUShortArrayFromByteArray(y);
//master.WriteMultipleRegisters(slaveAddr, startAddr, ushorts);
情况3:写入有符号的小数,即写入float数据,如写入-6.3,-2.65,56.893,51,-465等,处理方法是:float[]=>byte[]=>ushort[],情况3包括了情况2和情况1
//float[] uVals02 = vals.Split(',').Select(s => float.Parse(s)).ToArray();
//byte[] y = ByteArrayLib.GetByteArrayFromFloatArray(uVals02);
//ushort[] ushorts = UShortLib.GetUShortArrayFromByteArray(y);
//master.WriteMultipleRegisters(slaveAddr, startAddr, ushorts);
MessageBox.Show("【多保持寄存器】写入成功!");
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
break;
}
}
写入成功,同时读取的也是刚才写的值,在博途的监控表中看到
10、写入的代码--float类型
写入负数
我们向“摩头2温度”这个寄存器写入数据
再看博途中的数据
11、小结
客户端创建tcp client对象,然后modbus利用tcp对象创建modbus通信,然后通过不同数据类型读写PLC数据,成功了
代码链接:
链接:https://pan.baidu.com/s/1aCqv3eSX-7SXAdGtrGNpTw
提取码:kyqo