上位机采用串口与下位机信,使用modbus通信协议控制和采集数据
可以参考一下(直接翻到modbus的章节):
MODBUS技术协议(第三章).pdf · chuan/临时的 - 码云 - 开源中国 (gitee.com)
首先实现通信用的modbus类
代码先实现了3个基础的功能 读输出状态、读保存寄存器和强置单线圈
public class Modbus
{
#region 基础设置
private SerialPort serialPort;
private byte[]? ReceivedData;
private bool ReceiveFlag=false;
public Modbus(string portName, int baudRate, Parity parity,
StopBits stopBits, int dataBits)
{
serialPort = new SerialPort();
serialPort.PortName = portName;
serialPort.BaudRate = baudRate;
serialPort.Parity = parity;
serialPort.StopBits = stopBits;
serialPort.DataBits = dataBits;
}
public void OpenModbusConnet()
{
try
{
serialPort.Open();
serialPort.DataReceived += SerialPort_DataReceived;
}
catch (Exception ex)
{
throw new Exception("串口打开失败:" + ex.Message);
}
}
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
int ReceivedDataLenth = serialPort.BytesToRead;
ReceivedData = new byte[ReceivedDataLenth];
serialPort.Read(ReceivedData, 0, ReceivedDataLenth);
ReceiveFlag=true;
}
~Modbus()
{
if (serialPort is not null)
{
if (serialPort.IsOpen)
serialPort.Close();
}
}
#endregion
//等待数据返回,超过200毫秒退出等待
private void awiteReceive()
{
ReceiveFlag= false;
DateTime dateTime = DateTime.Now;
while (ReceiveFlag == false)
{
var t= DateTime.Now-dateTime;
if(t.Milliseconds>200)
ReceiveFlag= true;
}
}
/// <summary>
/// 发送报文
/// </summary>
/// <param name="cmd"></param>
/// <exception cref="Exception"></exception>
private void SendCommand(byte[] cmd)
{
if (!serialPort.IsOpen)
{
throw new Exception("modbus连接未打开");
return;
}
try
{
serialPort.Write(cmd,0,cmd.Length);
awiteReceive();
}
catch (Exception ex)
{
throw new Exception("发送失败:"+ex.Message);
}
}
#region 读输出状态 功能码0x01
/// <summary>
/// 读输出状态 功能码0x01
/// </summary>
/// <param name="SalavAddr"></param>
/// <param name="StartAddr"></param>
/// <param name="coils"></param>
/// <returns>返回接收到的报文 <see langword="byte[]"/></returns>
public byte[] ReadCoilsStatus(byte SalavAddr, int StartAddr, int coils)
{
byte[] Cmd= FillMessage(SalavAddr,0x01, StartAddr, coils);
try
{
SendCommand(Cmd);
}
catch (Exception ex)
{
throw ex;
}
if (ReceivedData is not null && ReceivedData.Length is >3 )
{
if (ReceivedData[1] == (byte)0x01)
{
if (verifyReciveCRC(ReceivedData))
{
return ReceivedData;
}
}
}
return null!;
}
#endregion
#region 读取保持型(保存)寄存器 功能码0x03
/// <summary>
/// 读取保持型寄存器 功能码0x03
/// </summary>
/// <param name="SalavAddr"></param>
/// <param name="StartAddr"></param>
/// <param name="RegisterCount"></param>
/// <returns>返回接收到的报文<see langword="byte[]"/></returns>
public byte[] ReadRegister(byte SalavAddr, int StartAddr, int RegisterCount)
{
byte[] Cmd = FillMessage(SalavAddr, 0x03, StartAddr, RegisterCount);
try
{
SendCommand(Cmd);
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
if (ReceivedData is not null && ReceivedData.Length is > 3)
{
if (ReceivedData[1] == (byte)0x03)
{
if (verifyReciveCRC(ReceivedData))
{
return ReceivedData;
}
}
}
return null!;
}
#endregion
#region 强置单线圈 功能码0x05
public enum ColiStatus
{
ON=0xFF,
OFF=0x00
}
/// <summary>
/// 设置当个线圈的状态为ON高电平或低电平OFF
/// </summary>
/// <param name="SalavAddr"></param>
/// <param name="CoilAddr"></param>
/// <param name="ONOFF">断通标志=FF00,置线圈 ON(1)
/// 断通标志=0000,置线圈 OFF(0)</param>
/// <returns>返回接收到的报文<see langword="byte[]"/></returns>
public byte[] ForceSingleCoil(byte SalavAddr, int CoilAddr, ColiStatus ONOFF)
{
byte[] Cmd = FillMessage(SalavAddr, 0x05, CoilAddr,
ONOFF== ColiStatus.ON ?65280:0000);//65280 ==0xFF00
try
{
SendCommand(Cmd);
}
catch (Exception ex)
{
throw ex;
}
if (ReceivedData is not null && ReceivedData.Length is > 3)
{
if (ReceivedData[1] == (byte)0x05)
{
if (verifyReciveCRC(ReceivedData))
{
return ReceivedData;
}
}
}
return null!;
}
#endregion
/// <summary>
/// 填充8位数据报文
/// </summary>
/// <param name="SalavAddr">从机地址</param>
/// <param name="FCode">功能码</param>
/// <param name="StartAddr">起始地址</param>
/// <param name="dataBit">2位数据位</param>
/// <returns>填充好的报文</returns>
private byte[] FillMessage(byte SalavAddr, byte FCode, int StartAddr, int dataBit)
{
byte[] message = new byte[8];
message[0] = SalavAddr;
message[1] = FCode;
message[2] = (byte)((StartAddr - StartAddr % 256) / 256);//起始地址高位
message[3] = (byte)(StartAddr % 256);//起始地址地位
message[4] = (byte)((dataBit - dataBit % 256) / 256);
message[5] = (byte)(dataBit % 256);
var crc = CRC16(message);
message[6] = crc[0];
message[7] = crc[1];
return message;
}
[DebuggerHidden]
public byte[] CRC16(byte[] byteData)
{
byte[] CRC = new byte[2];
UInt16 wCrc = 0xFFFF;
for (int i = 0; i < byteData.Length - 2; i++)
{
wCrc ^= Convert.ToUInt16(byteData[i]);
for (int j = 0; j < 8; j++)
{
if ((wCrc & 0x0001) == 1)
{
wCrc >>= 1;
wCrc ^= 0xA001;
}
else
{
wCrc >>= 1;
}
}
}
CRC[1] = (byte)((wCrc & 0xFF00) >> 8);
CRC[0] = (byte)(wCrc & 0x00FF);
return CRC;
}
/// <summary>
/// 校验接收到的数据是否正确
/// </summary>
/// <param name="byteData"></param>
/// <returns>正确返回true</returns>
[DebuggerHidden]
private bool verifyReciveCRC(byte[] byteData)
{
var resCrc = CRC16(byteData);
if (resCrc[0] == byteData[byteData.Length - 2] &&
resCrc[1] == byteData[byteData.Length - 1])
return true;
else return false;
}
}
如果需要其他可能码基本都是复制粘贴改一下 if (ReceivedData[1] == (byte)0x05) 这个功能码就好
使用modbus类要先实例化一个,然后调用打开连接方法,就可以正常调用(使用)功能方法了
可以使用仿真软件进行测试
试了一下是能正常使用并返回正确报文
Modbus拓展方法:
再编写一个类,为返回的报文提供数据解析
public static class ModbusExtension
{
/// <summary>
/// 提取读线圈返回的报文数据
/// </summary>
/// <param name="modbus"></param>
/// <param name="bytes"></param>
/// <returns><see langword="List<bool>"/> 下标0为起始地址线圈 </returns>
public static List<bool> BytesOfReadColisToBoolList(this Modbus modbus, byte[] bytes)
{
List<bool> coilsStatas = new List<bool>();
if (bytes is not null && bytes.Length >= 5)
{
if (bytes[1] == (byte)0x01)
{
for (int i = 0; i < Convert.ToInt32(bytes[2]); i++)
{
for (int j = 0; j < 8; j++)
{
coilsStatas.Add((byte)((byte)((byte)(bytes[i + 3] >> j) << 7) >> 7) == (byte)0x01 ? true : false);
}
}
}
}
return coilsStatas;
}
/// <summary>
/// 提取读寄存器返回的报文数据
/// </summary>
/// <param name="modbus"></param>
/// <param name="bytes"></param>
/// <returns><see langword="int[]"/>从0开始,每一项为一个寄存器的值
/// error return null</returns>
public static int[] BytesOfReadKeepRegisterToIntArr(this Modbus modbus,byte[] bytes)
{
if (bytes is null || bytes.Length < 3) return null!;
int dataLenth = (int)bytes[2];
int[] result = new int[dataLenth / 2];
int databitStart = 3;
int RegisterLen = 2;
int j = 0;
try
{
for (int i = databitStart; i < dataLenth * 2; i += RegisterLen)
{
if (i >= bytes.Length) break;
result[j] = bytes[i];
result[j] <<= 8;
result[j] |= bytes[i + 1];
j++;
}
}
catch (Exception)
{
}
return result;
}
}
简单测试
使用Winform测试
只有一个窗体代码如下
using CModbus;
using System.IO.Ports;
using Timer = System.Windows.Forms.Timer;
namespace WinFormsApp1
{
public partial class Form1 : Form
{
private Modbus? modbus;
public Form1()
{
InitializeComponent();
string[] portName= SerialPort.GetPortNames();
this.comboBox1.DataSource= portName;
}
private void button1_Click(object sender, EventArgs e)
{
string portname=this.comboBox1.SelectedValue.ToString()!;
modbus = new Modbus(portname,9600,Parity.None,StopBits.One,8);
modbus.OpenModbusConnet();
}
private void button2_Click(object sender, EventArgs e)
{
if (modbus is not null)
{
Timer t = new Timer();
t.Interval = 1000;
t.Tick += (_, _) =>
{
var coilsData= modbus.ReadCoilsStatus(0x01,0000,0008);
var str1= coilsData.Select(x => x.ToString("X2")+" ").ToArray();
string t1="";
string t2 = "";
var coilStatus=modbus.BytesOfReadColisToBoolList(coilsData);
for (int i = 0; i < str1.Length; i++)
{
t1 += str1[i];
}
for (int i = 0; i < coilStatus.Count; i++)
{
t2 += coilStatus[i].ToString()+" ";
}
textBox1.Text= t1;
textBox2.Text= t2;
//var RegisterData = modbus.ReadRegister(0x01, 0000, 0008);
//var str2 = RegisterData.Select(x => x.ToString("X2") + " ").ToArray();
//string t3 = "";
//string t4 = "";
//var RegisterValue = modbus.BytesOfReadKeepRegisterToIntArr(RegisterData);
//for (int i = 0; i < str2.Length; i++)
//{
// t3 += str2[i];
//}
//for (int i = 0; i < RegisterValue.Length; i++)
//{
// t4 = RegisterValue[i].ToString()+" ";
//}
//textBox3.Text = t3;
//textBox4.Text = t4;
};
t.Start();
}
}
}
}
}
读取8个线圈结果如下:
读寄存器:
最后简单实现了一下温度采集和一些控制
下位机链接:http://t.csdn.cn/TC52y
最后直接附上源码链接了:(3条消息) 【免费】c#上位机(温室监控系统源码)资源-CSDN文库