上位机基础-PLC通信篇
Posted 聆听微风
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了上位机基础-PLC通信篇相关的知识,希望对你有一定的参考价值。
上位机基础-通信PLC篇
1. ModbusRTU协议(测试与实现)
1. Modbus Slave 的使用教程
以读取输出线圈功能为例(RTU模式使用CRC校验,Ascii 使用LRC校验):
主站:11 01 00 13 00 1B CRC
含义:读取11H从站的输出线圈(01 功能码 是输出线圈) ,起始地址0013H(19->00020),读取的线圈个数001BH(27)个
报文的起始地址为0,但是寄存器的最小地址为1.所以对应的地址需要后移动一个。
即读取从站输出线圈从0020-0046
从站报文: 11 01 04 CD 6B B2 05 CRC
从站返回输出线圈 0020-0046。
CD= 1100 1101对应 0020--0027 八个线圈位置
2. 使用NModbus4包进行参数读取
public Form1()
InitializeComponent();
SerialPort serialPort;
private void Form1_Load(object sender, EventArgs e)
serialPort = new SerialPort("COM2", 9600, Parity.None, 8, StopBits.One);
serialPort.Open();
private void button1_Click(object sender, EventArgs e)
ModbusSerialMaster modbusMaster = ModbusSerialMaster.CreateRtu(serialPort);
byte addr = 17;
ushort startAddr = 19;
ushort count = 27;
bool[] boolData = modbusMaster.ReadCoils(addr, startAddr, count);
string ldata = string.Empty;
/// 先把bool 转换为 string 2进制
foreach (bool item in boolData)
ldata += item == true ? 1 : 0;
ldata= ldata.Trim(\' \').Replace(" ", "");
textBox1.Text = StringBinTOStringHex(ldata);
public string StringBinTOStringHex(string str)
int Cnt = (str.Length / 4);
bool IsLastFourData = str.Length % 4 == 0 ? true : false;
if (!IsLastFourData) Cnt++;
string lResult = string.Empty;
for (int i = 0; i < Cnt; i++)
if (str.Length < 4)
str = str.PadLeft(4, \'0\');
string temp = str.Substring(0, 4);
if (i == Cnt - 1 && !IsLastFourData)
lResult += Convert.ToInt16(temp, 2).ToString("X2");
else
lResult += Convert.ToInt16(temp, 2).ToString("X");
if (i % 2 == 1)
lResult += " ";
str = str.Remove(0, 4);
return lResult;
2. PLC通信(配置与代码实现)
基础知识:PLC的各种数据类型
数据类型 | 位数 | 案例 | 说明 |
---|---|---|---|
Bool | 布尔,1位 | DB9.DBX7.0 | DB9块Bool类型,偏移量为7,第一位的布尔数据 |
Byte | 字节,8位 | DB9.DBB6 | DB9块byte类型,偏移量为6的字节数据 |
Word | 字,16位 | DB9.DBW4 | DB9块字类型,偏移量为4的字数据 |
Dword | 双字,32位 | ||
Sint | 有符号短整数,8位 | ||
Usint | 无符号短整数,8位 | ||
Int | 有符号整数,16位 | ||
UInt | 无符号整数,16位 | ||
Real | 32位单精度 | DB9.DBD0 | DB9块Real类型,偏移量为0的单精度数据 |
LReal | 64位双精度 |
PC端通信配置
首先对需要通信的PLC模块进行设置
- 设置当前模块为允许通信
-
取消 “优化的块访问”
-
记住当前需要通信的设备ip地址
方法1:S7-PLCSIM Advanced
- 下载的时候,选择接口为虚拟适配器
-
配置仿真器
选择适配器,配置网络地址,然后点击Start
- 下载程序到设备。先搜索设备地址,然后点击下载
-
点击装载
-
点击Run
-
查看仿真器,状态灯会显示绿色
方法2:NetToPLCsim
PLC模块通信配置略过
-
点击仿真
-
点击开始搜索,并且下载程序
-
仿真器点击Run
-
配置NetToPLCsim
先配置本机的IP地址,这个是选择网卡中的任何一个IP地址都行
配置需要访问的PLC模块地址
配置卡槽号信息:
点击Start
注意最重要的一点
程序访问的时候,使用你本地的IP地址
代码实现
第一部分:连接PLC并且初始化
- 安装指定的PLC类库
-
初始化连接
Plc myPlc = new Plc(CpuType.S71500, "192.168.255.105", 0, 1); myPlc.Open(); if (myPlc.IsConnected)MessageBox.Show(“连接成功”);
关于写入与读取对应转换说明如下:
bool -> bit
byte -> byte
Usint,Uint,(小于等于16位)。统一使用 ushort接收。
int,word (小于等于16位)。统一使用 short接收。
Dword(大于16为,小于等于32位)。使用int接收.
Real ,用Float 接收
第二部分:读取并且写入对应类型的PLC数据
第一种方法,指定地址读写 ( 使用myPlc.Read()方法进行读,使用myPlc.Write()方法进行写入)
使用案例(略):
有部分命令格式不知道。以后有空填坑
//Bool
plc.Write("DB1.DBX0.0", true);
var IsRight = plc.Read("DB1.DBX0.0");
Console.WriteLine("DB1.DBX0.0: " + IsRight);
//Int
plc.Write("DB1.DBW2.0", Convert.ToInt16(1));
int Score = (ushort)plc.Read("DB1.DBW2.0");
Console.WriteLine("DB1.DBW2.0: " + Score);
// Real
plc.Write("DB1.DBD4.0", Convert.ToSingle(1.1));
var Money = ((uint)plc.Read("DB1.DBD4.0")).ConvertToFloat();
Console.WriteLine("DB1.DBD4.0: " + Money);
//String写入
var temp = Encoding.ASCII.GetBytes("Chen"); //将val字符串转换为字符数组
var bytes = S7.Net.Types.S7String.ToByteArray("Chen", temp.Length);
plc.WriteBytes(DataType.DataBlock, 1, 8, bytes);
//String读取
var reservedLength = (byte)plc.Read(DataType.DataBlock, 1, 8, VarType.Byte, 1);//获取字符串长度
var Name = (string)plc.Read(DataType.DataBlock, 1, 8, VarType.S7String, reservedLength);//获取对应长度的字符串
Console.WriteLine("DB1.8.0: " + Name);
// DInt
plc.Write("DB1.DBD264.0", Convert.ToInt32(20));
var dIntVar = (uint)plc.Read("DB1.DBD264.0");
Console.WriteLine("DB1.DBD264.0: " + dIntVar);
// DWord
plc.Write("DB1.DBD268.0", 123456);
var dWordVar = (uint)plc.Read("DB1.DBD268.0");
Console.WriteLine("DB1.DBD268.0: " + dWordVar);
// Word
plc.Write("DB1.DBD270.0", 12345678);
var wordVar = (uint)plc.Read("DB1.DBD270.0");
Console.WriteLine("DB1.DBD270.0: " + wordVar);
第二种方法, 解析读写。
需要指定DB的类型、DB号、起始地址、PLC数据类型及读取数量。虽然它需要传入的参数变多了,但是当需要读取多个地址连续且类型相同的变量时,仅需修改最后的读取数量,S7NetPlus就会自动读取这一连串的地址,并按照指定的变量类型解析出对应的值,
函数说明:
public object Read(DataType dataType, int db, int startByteAdr, VarType varType, int varCount, byte bitAdr = 0);
//读取
bool result = (bool)plc.Read(DataType.DataBlock, 10, 0, VarType.Bit, 1);
//写入
plc.Write(DataType.DataBlock, 10, 0, true);
代码如下(读取代码):
var Real = myPlc.Read(DataType.DataBlock, 9, 0, VarType.Real,1);
var Int = myPlc.Read(DataType.DataBlock, 9, 4, VarType.Int, 1);
byte Byte = (byte)myPlc.Read(DataType.DataBlock, 9, 6, VarType.Byte, 1);
var Word = myPlc.Read(DataType.DataBlock, 9, 8, VarType.Word, 1);
var Dword = myPlc.Read(DataType.DataBlock, 9, 10, VarType.DWord, 1);
var Uint = myPlc.Read(DataType.DataBlock, 9, 14, VarType.Int, 1);
var LReal = myPlc.Read(DataType.DataBlock, 9, 16, VarType.LReal, 1);
var lBool = myPlc.Read(DataType.DataBlock, 9, 24, VarType.Bit, 1);
写入代码:
myPlc.Write(DataType.DataBlock, 9, 0, 6.5f);
myPlc.Write(DataType.DataBlock, 9, 4, (ushort)1);
myPlc.Write(DataType.DataBlock, 9, 6, (byte)51);
myPlc.Write(DataType.DataBlock, 9, 8, (ushort)11);
myPlc.Write(DataType.DataBlock, 9, 24, false);
前两种方法,每次读取都是建立新的TCP连接,比较消耗计算机资源。
详细见文档表述:
This method reads a single variable from the plc, by parsing the string and returning the correct result. While this is the easiest method to get started, is very inefficient because the driver sends a TCP request for every variable.
第三种方法, 块读取与写入(未验证)。
一次性从连接中读取所有需要查看的信息,然后进行解析。
public byte[] ReadBytes(DataType dataType, int db, int startByteAdr, int count)
//读取数据选择从DB块中读取,db设置为1,起始地址为0,读取18个字节
var bytes = plc.ReadBytes(DataType.DataBlock, 1, 0, 18);
//取字节0中的第0位
var db1Bool1 = bytes[0].SelectBit(0);
Console.WriteLine("DB1.DBX0.0:" + db1Bool1);
//取字节0中的第1位
bool db1Bool2 = bytes[0].SelectBit(1); ;
Console.WriteLine("DB1.DBX0.1:" + db1Bool2);
//跳到字节2并连续取两个字节数据
int IntVariable = S7.Net.Types.Int.FromByteArray(bytes.Skip(2).Take(2).ToArray());
Console.WriteLine("DB1.DBW2.0:" + IntVariable);
//...
double RealVariable = S7.Net.Types.Real.FromByteArray(bytes.Skip(4).Take(4).ToArray());
Console.WriteLine("DB1.DBD4.0:" + RealVariable);
//...
int dIntVariable = S7.Net.Types.DInt.FromByteArray(bytes.Skip(8).Take(4).ToArray());
Console.WriteLine("DB1.DBD8.0: " + dIntVariable);
//...
uint dWordVariable = S7.Net.Types.DWord.FromByteArray(bytes.Skip(12).Take(4).ToArray());
Console.WriteLine("DB1.DBD12.0: " + Convert.ToString(dWordVariable, 16));
//...
ushort wordVariable = S7.Net.Types.Word.FromByteArray(bytes.Skip(16).Take(2).ToArray());
Console.WriteLine("DB1.DBW16.0: " + Convert.ToString(wordVariable, 16));
public void WriteBytes(DataType dataType, int db, int startByteAdr, byte[] value)
字符串读取与写入
//String读取
byte[] data = plc.ReadBytes(DataType.DataBlock, 10, 2, 254);
string result = Encoding.Default.GetString(data);
//Wstring读取
byte[] data = plc.ReadBytes(DataType.DataBlock, 10, 4, 508);
string result = Encoding.BigEndianUnicode.GetString(data);
在S7-1500中,一个String类型的变量占用256个字节,但是第一个字节是总字符数,第二个字节是当前字符数,所以真正的字符数据是从第三个字节开始的,共254个字节。
同理,WString类型其实就是双字节的Sring,也就是说一个字符占用两个字节,所以一个WString类型的变量占用512个字节,第一、二个字节是总字符数,第三、四个字节是当前字符数,真正的字符数据是从第五个字节开始的,共508个字节。
按照以上示例的方法,读取上来的字符串后面会带很多个"\\0"的字符,那是因为后面的空字节也读取上来了,正式使用时可以考虑使用.Replace("\\0", "")来去除,或者解析第二个字节来获取字符长度进而转码。
当写入字符串时,则需要根据不同的数据类型来生成对应字符串的字节数组,然后将该数组写入到指定地址中即可。
需要注意的是,String类型的编码格式对应的是ASCII,而WString的则是C#中的BigEndianUnicode格式。在WString中,由于总长度与当前字符数是都是双字节数,所以在转换成字节数组的时候存在高低字节顺序问题。在这里就有一个大坑:这两个变量在C#中转换出来的字节数组跟PLC中存储的,高低字节是反过来的。这也就是为什么下面的WString的示例中需要对总字符数和当前字符数的两个字节数组进行反转。
/// <summary>
/// 获取西门子PLC字符串数组--String
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
private byte[] GetPLCStringByteArray(string str)
byte[] value = Encoding.Default.GetBytes(str);
byte[] head = new byte[2];
head[0] = Convert.ToByte(254);
head[1] = Convert.ToByte(str.Length);
value = head.Concat(value).ToArray();
return value;
/// <summary>
/// 获取西门子PLC字符串数组--WString
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
private byte[] GetPLCWStringByteArray(string str)
byte[] value = Encoding.BigEndianUnicode.GetBytes(str);
byte[] head = BitConverter.GetBytes((short)508);
byte[] length = BitConverter.GetBytes((short)str.Length);
Array.Reverse(head);
Array.Reverse(length);
head = head.Concat(length).ToArray();
value = head.Concat(value).ToArray();
return value;
//写入String
string str = "Example";
plc.Write(DataType.DataBlock, 10, 0, GetPLCStringByteArray(str));
//写入WString
string str = "示例";
plc.Write(DataType.DataBlock, 10, 0, GetPLCWStringByteArray(str));
旧版本的单次字节读取是有字节数限制的,每一次读取的最大字节数为200,如果需要读写更多的字节,则需要多次读写并进行拼接,以下提供两种方法,可供参考:
/// <summary>
/// 循环读取
/// </summary>
/// <param name="numBytes">要读取的字节数</param>
/// <param name="db">DB号</param>
/// <param name="startByteAdr">起始地址</param>
/// <returns></returns>
private byte[] CyclicReadMultipleBytes(int numBytes, int db, int startByteAdr = 0)
byte[] resultBytes = new byte[0];
int index = startByteAdr;
while (numBytes > 0)
var maxToRead = Math.Min(numBytes, 200);
byte[] bytes = plc.ReadBytes(DataType.DataBlock, db, index, maxToRead);
if (bytes == null)
return null;
resultBytes = resultBytes.Concat(bytes).ToArray();
numBytes -= maxToRead;
index += maxToRead;
return resultBytes;
/// <summary>
/// 递归读取
/// </summary>
/// <param name="numBytes">要读取的字节数</param>
/// <param name="db">DB号</param>
/// <param name="startByteAdr">起始地址</param>
/// <returns></returns>
public static byte[] RecursiveReadMultipleBytes(int numBytes, int db, int startByteAdr = 0)
byte[] result = new byte[0];
if (numBytes > 200)
byte[] temp = plc.ReadBytes(DataType.DataBlock, db, startByteAdr, 200);
numBytes -= 200;
result = temp.Concat(RecursiveReadMultipleBytes(numBytes, db, startByteAdr + 200)).ToArray();
else
byte[] temp = plc.ReadBytes(DataType.DataBlock, db, startByteAdr, numBytes);
result = result.Concat(temp).ToArray();
return result;
return result;
上位机开发之单片机通信实践
经常会有一些学员会问到上位机与单片机之间通信的问题,而我们经常会讲上位机与PLC之间通信,那么其实对上位机开发来说,不管是和PLC通信,还是和单片机通信,通信原理都是一样的。PLC的本质就是单片机,在单片机的基础上添加一些外围电路并形成产品化,即构成了PLC控制器。今天在这里给大家分享一个上位机与单片机通信的实例,希望对大家开发上位机有所启发。
1. 单片机硬件介绍
只要做上位机开发,就离不开通信协议。一般来说,单片机可以与上位机之间以串口通信为主,当然也不排除现在有的单片机也集成了以太网口。就串口通信而言,常用的几种通信方式,包括串口自定义协议、Modbus协议、CAN总线,接下来介绍的这个单片机是某个锂电池的核心板,它主要是支持Modbus协议和CAN总线的方式。
图表 1 单片机硬件
2. 通信测试
(1)这里我们选择的是基于485总线的ModbusRTU通信协议,如果要实现上位机开发,需要单片机开发人员提供一份通信变量表,如果读取变量较多或者不连续,需要进行分组读取。
(2)通信变量表一般包含参数名称、Modbus地址、存储区、数据类型、换算公式等内容,能够将通信变量表看明白并完成通信测试,是能够完成上位机开发的前提。
(3)这里,我截取部分变量表跟大家做一个分析:
图表 2 Modbus寄存器表
上表中,以电芯总电压为例,Modbus地址为0x1003,对应十进制即为4099,寄存器地址即为44100,读取类型为ushort类型,换算公式为读取之后乘以0.01,比如读取值为5630,即为5.63V。
分析明白之后,我们就可以先用Modbus Poll软件来一波初步测试,如果需要Modbus软件资料的,可以关注一下喜科堂官方关注:dotNet工控上位机,然后像聊天一样发送关键词:Modbus软件套装即可。这里我们需要通过485转USB连接到电脑中,然后通过设备管理器,看到端口号为COM4。
图表 3 通信端口
打开Modbus Poll软件,通信参数选择COM4、9600、N、8、1,读取寄存器起始地址为4099,读取长度为10,具体配置如下图所示:
图表 4 通信参数配置
图表 5 通信读取配置
配置完成后,即可读取到单片机的数据,具体如下图所示:
图表 6 ModbusPoll读取
(4)实现读取之后,我们分析一下结果,4099读取到的值为4206,说明当前电池的电压为42.06V。我们可以用实际开发完成的上位机软件做下对比,验证一下数据是否正确:
图表 7 上位机软件
(5) 我们也可以用喜科堂通信测试平台来做下测试,测试结果如下:
3. 整体总结
本文主要针对单片机的Modbus通信实例做了较为详尽的描述,由于篇幅有限,仅仅介绍了通信测试部分,对于后续的项目实战部分,会通过后续的文章进行进一步的阐述。
公众号:thinger_swj
以上是关于上位机基础-PLC通信篇的主要内容,如果未能解决你的问题,请参考以下文章
西门子200/300PLC通过CHNet-S7200/300与海得上位机软件ModbusTCP通信
三菱Q系列PLC与上位机易控组态软件ModbusTCP通信案例