目录
1,辅助工具。
1,虚拟串口工具(vspd.exe)
2,Modubus模拟主站(Modbus Poll)
3,Modbus模拟从站(Modbus Slave)
4,OPC服务软件(KEPServerEx V4.0)。
下载链接:https://download.csdn.net/download/lingxiao16888/89516857
2,初识Modbus。
1,Modbus协议是一种应用层报文传输协议,包含RTU,ASCII,TCP(分配的端口号为:502)三种报文类型。
2,标准的Modbus协议物理层接口有RS232,RS485,RS422,以太网口。采用主从(Master/Slave)方式通信。
3,Modbus主从站协议原理
Modubus串行链路协议是一种主从协议。在同一时刻,只有一个主站,一个从站或者多个从站(从站最大编号247)连接在同一串行总线。
Modbus通信由主站发起,从站在没有收到主站的请求时是不会发送数据。
从站之间互不通信。
主站在同一时刻只能发起一个Modbus事务请求。
主站有两种模式对从站发起请求:广播,单播。
4,Modbus帧结构
Modbus串行链路协议是一种主从协议。
网络上的每个从站必须有唯一的地址(从1到247)
从站地址用于寻址从站设备,由主站发起。
地址0用于广播模式,不需要响应。
RS-485,RS-232定义了标准的物理端口,提高了操作性。
3,基于ModbusRTU的通信。
3.1,RTU与ASCII模式区别
3.2,Modbus存储区
3.3,报文格式
3.4,异常代码
3.5,详细文档连接 。
https://download.csdn.net/download/lingxiao16888/89516928?spm=1001.2014.3001.5503
3.6,代码
目的:通过标准Modbus协议完成对线圈,寄存器的读写。
- 效果展示:
3.6.1,准备
第1步,使用虚拟串口软件创建虚拟串口。
第2步,使用ModbusSlave软件建立虚拟Modbus从站。
配置通信连接参数
配置功能定义(是进行线圈读写还是寄存器读写)
定义数据显示格式
注明:此截图中的ID=1;F=01表示从站地址为01,功能码为01(即读取线圈,具体参考前面报文格式阐述)
3.6.2,UI部分
3.6.3,UI代码部分
public partial class Form1 : Form
{
ModbusSerial modbus;
int slaveStation = 10;
Timer timer = new Timer();
public Form1()
{
InitializeComponent();
UIControlStatusChange(false);
timer.Interval = 1000;
foreach (Control item in groupBox1.Controls)
{
if (item is GroupBox)
{
foreach (Control c in item.Controls)
{
if (c is Button)
{
(c as Button).Click += Button_Click;
}
}
}
}
InitDataLoad();
}
void InitDataLoad()
{
//获取串口号
comboPortName.DataSource = SerialPort.GetPortNames();
//波特率
comboBaudRate.DataSource = new int[] { 4800, 9600, 19200 };
comboBaudRate.SelectedIndex = 1;
//数据位
comboDataBits.DataSource = new int[] { 5, 6, 7, 8 };
comboDataBits.SelectedIndex = 3;
//校验位
comboParity.DataSource = Enum.GetNames(typeof(Parity));
//停止位----不支持None所以应该None去除
comboStopBits.DataSource = Enum.GetNames(typeof(StopBits)).Where(item => !item.Equals("None", StringComparison.CurrentCultureIgnoreCase)).ToArray();
modbus = new ModbusSerial();
modbus.ErrorMessage += Modbus_ErrorMessage;
}
private void Button_Click(object sender, EventArgs e)
{
lblErrorMsg.Text = "";
}
private void Modbus_ErrorMessage(int arg1, string arg2)
{
this.Invoke(new Action(() =>
{
lblErrorMsg.Text = $"从站[{arg1}]出现异常,原因:{arg2}";
}));
}
/// <summary>
/// 变更UI控件状态
/// </summary>
/// <param name="status">True:已连接,False:连接断开</param>
void UIControlStatusChange(bool status)
{
if (status)
{
gbParam.Enabled = false;
btnConnect.BackColor = Color.LightGreen;
btnConnect.Text = "断开连接";
groupBox1.Enabled = true;
}
else
{
gbParam.Enabled = true;
btnConnect.BackColor = Color.Red;
btnConnect.Text = "连接";
groupBox1.Enabled = false;
}
}
private void btnConnect_Click(object sender, EventArgs e)
{
if (btnConnect.Text.Equals("连接"))
{
if (modbus.IsOpen)
{
modbus.Close();
}
//更新端口的设置
if (comboPortName.DataSource == null || comboPortName.Items.Count==0)
{
MessageBox.Show("没有可用的串口!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
string portName = comboPortName.Text;
int baudRate = Convert.ToInt32(comboBaudRate.Text);
int dataBits = Convert.ToInt32(comboDataBits.Text);
Parity parity = (Parity)Enum.Parse(typeof(Parity), comboParity.Text);
StopBits stopBits = (StopBits)Enum.Parse(typeof(StopBits), comboStopBits.Text);
modbus.BaudRate = baudRate;
modbus.DataBits = dataBits;
modbus.Parity = parity;
modbus.StopBits = stopBits;
modbus.Open(portName);
}
else
{
if (modbus.IsOpen)
{
modbus.Close();
}
}
UIControlStatusChange(modbus.IsOpen);
}
//读取多个线圈
private void btnReadCoils_Click(object sender, EventArgs e)
{
int address; int len;
if (!int.TryParse(txtCoilsAddress.Text.Trim(), out address))
{
txtCoilsAddress.Focus();
MessageBox.Show("线圈地址错误!");
return;
}
if (!int.TryParse(txtCoilsCount.Text.Trim(), out len))
{
txtCoilsCount.Focus();
MessageBox.Show("线圈数量只能为正整数!");
return;
}
bool[] reuslt = modbus.ReadCoils(slaveStation, address, len);
if (reuslt == null) return;
StringBuilder sb = new StringBuilder();
foreach (var item in reuslt)
{
if (item)
{
sb.Append(" 1");
}
else
{
sb.Append(" 0");
}
}
lblCoilsResult.Text = sb.ToString();
}
private void txtSlave_TextChanged(object sender, EventArgs e)
{
if (!int.TryParse(txtSlave.Text.Trim(), out slaveStation))
{
slaveStation = 10;
}
}
private void btnForced_Click(object sender, EventArgs e)
{
int address;
if (!int.TryParse(txtCoilsWriteAdress.Text.Trim(), out address))
{
txtCoilsWriteAdress.Focus();
MessageBox.Show("线圈写入地址错误!");
return;
}
if (btnForced.Text.Equals("ON"))
{
bool result = modbus.ForcedCoil(slaveStation, address, false);
if (result)
{
btnForced.Text = "OFF";
btnForced.BackColor = Color.Red;
}
}
else
{
bool result = modbus.ForcedCoil(slaveStation, address, true);
if (result)
{
btnForced.Text = "ON";
btnForced.BackColor = Color.LightGreen;
}
}
}
private void btnRegistersRead_Click(object sender, EventArgs e)
{
int address; int len;
if (!int.TryParse(txtRegistersAdress.Text.Trim(), out address))
{
txtRegistersAdress.Focus();
MessageBox.Show("寄存器地址错误!");
return;
}
if (!int.TryParse(txtRegistersCount.Text.Trim(), out len))
{
txtRegistersCount.Focus();
MessageBox.Show("寄存器数量只能为正整数!");
return;
}
short[] result = modbus.ReadHoldingRegisters(slaveStation, address, len);
if (result == null) return;
StringBuilder sb = new StringBuilder();
foreach (var item in result)
{
sb.Append(" " + item.ToString());
}
lblRegistersResult.Text = sb.ToString();
}
private void btnReadFloat_Click(object sender, EventArgs e)
{
int address; int len;
if (!int.TryParse(txtRegistersAdress.Text.Trim(), out address))
{
txtRegistersAdress.Focus();
MessageBox.Show("寄存器地址错误!");
return;
}
if (!int.TryParse(txtRegistersCount.Text.Trim(), out len))
{
txtRegistersCount.Focus();
MessageBox.Show("寄存器数量只能为正整数!");
return;
}
float[] result = modbus.ReadFloatRegisters(slaveStation, address, len);
if (result == null) return;
StringBuilder sb = new StringBuilder();
foreach (var item in result)
{
sb.Append(" " + item.ToString());
}
lblRegistersResult.Text = sb.ToString();
}
private void btnWriteShort_Click(object sender, EventArgs e)
{
int address; short val;
if (!int.TryParse(txtWriteShortAdress.Text.Trim(), out address))
{
txtWriteShortAdress.Focus();
MessageBox.Show("寄存器地址错误!");
return;
}
if (!short.TryParse(txtWriteShortVal.Text.Trim(), out val))
{
txtWriteShortVal.Focus();
MessageBox.Show("写入的值只能为正整数!");
return;
}
if (modbus.WriteSingleRegister(slaveStation, address, val))
{
MessageBox.Show("Short数已成功写入");
}
else
{
MessageBox.Show("Short数写入失败");
}
}
private void btnWriteFloat_Click(object sender, EventArgs e)
{
int address; float val;
if (!int.TryParse(txtFloatWriteAddress.Text.Trim(), out address))
{
txtFloatWriteAddress.Focus();
MessageBox.Show("寄存器地址错误!");
return;
}
if (!float.TryParse(txtWriteFloatVal.Text.Trim(), out val))
{
txtWriteFloatVal.Focus();
MessageBox.Show("写入的值只能为正整数!");
return;
}
if (modbus.WriteFloatRegister(slaveStation, address, val))
{
MessageBox.Show("Float数已成功写入");
}
else
{
MessageBox.Show("Float数写入失败");
}
}
//给线圈批量赋值
private void btnBatch_Click(object sender, EventArgs e)
{
int address; short val;
if (!int.TryParse(txtCoils.Text.Trim(), out address))
{
txtCoils.Focus();
MessageBox.Show("线圈地址错误!");
return;
}
if (!short.TryParse(txtCoilsVal.Text.Trim(), out val))
{
txtCoilsVal.Focus();
MessageBox.Show("写入的值只能为正整数!");
return;
}
bool result = modbus.WriteMultipleCoils(slaveStation, address, 16, new short[] { val });
if (result)
{
MessageBox.Show("线圈已批量写入");
}
else
{
MessageBox.Show("线圈批量失败");
}
}
private void btnMulShortWrite_Click(object sender, EventArgs e)
{
int address; short val; int len;
if (!int.TryParse(txtMulAdress.Text.Trim(), out address))
{
txtMulAdress.Focus();
MessageBox.Show("寄存器地址错误!");
return;
}
if (!short.TryParse(txtMulShortVal.Text.Trim(), out val))
{
txtMulShortVal.Focus();
MessageBox.Show("写入的值只能为正整数!");
return;
}
if (!int.TryParse(txtMulCount.Text.Trim(), out len))
{
txtMulCount.Focus();
MessageBox.Show("寄存器数量只能为正整数!");
return;
}
if (modbus.WriteMultipleRegisters(slaveStation, address, len, Enumerable.Repeat(val, len).ToArray()))
{
MessageBox.Show("寄存器已批量写入");
}
else
{
MessageBox.Show("寄存器批量写入失败");
}
}
private void comboPortName_DropDown(object sender, EventArgs e)
{
comboPortName.DataSource = SerialPort.GetPortNames();
}
}
3.6.4,Modbus类
public class ModbusSerial : IDisposable
{
SerialPort serialPortObject = null;
/// <summary>
/// 目标站点号
/// </summary>
int targetStation = 0;
/// <summary>
/// 消息接收到标志位
/// </summary>
bool msgRecFlag = false;
/// <summary>
/// 接收到有效的数据
/// </summary>
byte[] RecBytes = null;
AutoResetEvent slim = new AutoResetEvent(true);
/// <summary>
/// 返回的异常信息
/// </summary>
public event Action<int, string> ErrorMessage;
/// <summary>
/// 构建基于SerialPort的Modbus
/// </summary>
/// <param name="baudRate">波特率</param>
/// <param name="dataBits">数据位</param>
/// <param name="parity">校验位</param>
/// <param name="stopBits">停止位</param>
public ModbusSerial(int baudRate, int dataBits, Parity parity, StopBits stopBits)
{
serialPortObject = new SerialPort();
serialPortObject.BaudRate = baudRate;
serialPortObject.DataBits = dataBits;
serialPortObject.Parity = parity;
serialPortObject.StopBits = stopBits;
serialPortObject.DataReceived += SerialPortObject_DataReceived;
}
/// <summary>
/// 该构造方法需通过属性设置串口参数
/// </summary>
public ModbusSerial()
{
serialPortObject = new SerialPort();
serialPortObject.DataReceived += SerialPortObject_DataReceived;
}
ModbusSerial(SerialPort port)
{
serialPortObject = port;
serialPortObject.DataReceived += SerialPortObject_DataReceived;
}
static ModbusSerial modbus;
/// <summary>
/// 获取Modbus
/// </summary>
/// <param name="port"></param>
/// <returns></returns>
public static ModbusSerial CreateModbus(SerialPort port)
{
if (modbus == null)
{
modbus = new ModbusSerial(port);
}
return modbus;
}
private void SerialPortObject_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
int num = serialPortObject.BytesToRead;
if (num == 0) return;
byte[] bytes = new byte[num];
//需要通过事件监听不可采用循环阻塞接收,如果采用循环阻塞将抛出异常
serialPortObject.Read(bytes, 0, num);
//对消息进行解析
//检查报文是否正确
msgRecFlag = true;
byte[] checkSum = Checkhelper.CrcModbus(bytes, 0, bytes.Length - 2);
if (checkSum[0] == bytes[bytes.Length - 1] && checkSum[1] == bytes[bytes.Length - 2])
{
//验证通过
byte stationNo = bytes[0];
targetStation = stationNo;
byte commandCode = bytes[1];
if (commandCode > 80)
{
//异常信息
ErrorCode errorCode = (ErrorCode)bytes[2];
//这里使用异步调用如果使用同步调用将大大增加阻塞时间导致后面的AutoResetEvent等待超时
ErrorMessage?.BeginInvoke(stationNo, errorCode.ToString(), null, null);
}
else
{
FunctionCode function = (FunctionCode)commandCode;
byte length = bytes[2];
switch (function)
{
case FunctionCode.ReadCoils:
case FunctionCode.ReadDiscreteInputs:
//返回字节长度
RecBytes = new byte[length];
Array.Copy(bytes, 3, RecBytes, 0, length);
break;
case FunctionCode.ReadHoldingRegisters:
case FunctionCode.ReadInputRegisters:
//读取寄存器数据
RecBytes = new byte[length];
Array.Copy(bytes, 3, RecBytes, 0, length);
break;
case FunctionCode.WriteSingleCoil:
case FunctionCode.WriteSingleRegister:
case FunctionCode.WriteMultipleCoils:
case FunctionCode.WriteMultipleRegisters:
RecBytes = new byte[4];
Array.Copy(bytes, 2, RecBytes, 0, RecBytes.Length);
break;
case FunctionCode.ReportSlaveID:
break;
case FunctionCode.MaskWriteRegister:
break;
case FunctionCode.ReadWriteRegister:
break;
default:
break;
}
}
}
else
{
ErrorMessage?.BeginInvoke(0, "报文校验未通过,可能传送中收到干扰!", null, null);
}
//接收完成,取消主线程阻塞
slim.Set();
}
/// <summary>
/// 串口波特率
/// </summary>
public int BaudRate
{
get
{
return serialPortObject.BaudRate;
}
set
{
serialPortObject.BaudRate = value;
}
}
/// <summary>
/// 数据位长度
/// </summary>
public int DataBits
{
get
{
return serialPortObject.DataBits;
}
set
{
serialPortObject.DataBits = value;
}
}
/// <summary>
/// 奇偶校验协议
/// </summary>
public Parity Parity
{
get
{
return serialPortObject.Parity;
}
set
{
serialPortObject.Parity = value;
}
}
/// <summary>
/// 停止位
/// </summary>
public StopBits StopBits
{
get
{
return serialPortObject.StopBits;
}
set
{
serialPortObject.StopBits = value;
}
}
/// <summary>
/// 当前的SerialPort对象
/// </summary>
public SerialPort SerialPortObject
{
get
{
return serialPortObject;
}
}
/// <summary>
/// 获取计算机所有的串行端口
/// </summary>
public string[] SerialPorts
{
get
{
return SerialPort.GetPortNames();
}
}
/// <summary>
/// 打开指定的串口
/// </summary>
/// <param name="serialPortName"></param>
public void Open(string serialPortName)
{
if (serialPortObject.IsOpen)
{
serialPortObject.Close();
}
serialPortObject.PortName = serialPortName;
serialPortObject.Open();
}
/// <summary>
/// 关闭串口
/// </summary>
public void Close()
{
if (serialPortObject.IsOpen)
{
serialPortObject.Close();
}
}
/// <summary>
/// 串口是否打开
/// </summary>
public bool IsOpen
{
get
{
return serialPortObject.IsOpen;
}
}
/// <summary>
///发送报文
/// </summary>
/// <param name="bytes"></param>
/// <param name="protocol">报文协议</param>
void Send(string sendMsg)
{
//对消息进行判断是否合规
if (!Regex.IsMatch(sendMsg, @"^[0-9a-fA-F]+$"))
{
throw new Exception("报文错误,存在非16进制字符");
}
byte[] bytes = new byte[sendMsg.Length / 2];
for (int i = 0; i < sendMsg.Length / 2; i++)
{
bytes[i] = Convert.ToByte(sendMsg.Substring(i * 2, 2), 16);
}
//添加校验码,并发送
byte[] checkSum = Checkhelper.CrcModbus(bytes, 0, bytes.Length);
var checkarr = checkSum.Reverse().ToArray();
byte[] msgBytes = new byte[bytes.Length + checkSum.Length];
Array.Copy(bytes, msgBytes, bytes.Length);
Array.Copy(checkarr, 0, msgBytes, bytes.Length, checkarr.Length);
serialPortObject.Write(msgBytes, 0, msgBytes.Length);
}
/// <summary>
/// 编制报文
/// </summary>
/// <param name="slaveStation">从站站号</param>
/// <param name="function">功能代码</param>
/// <param name="devAdress">起始地址</param>
/// <param name="devLenght">数量</param>
/// <param name="values">写入的值</param>
/// /// <param name="fromBase">如果写入的数值类型所占用的byte,例如short类型为2,int类型为4,flost类型为4,double类型为8</param>
/// <param name="set">强制线圈通断,true:ON ;false:OFF</param>
void ComposeMessage(int slaveStation, FunctionCode function, int devAdress, int devLenght = 1, byte[] values = null, int fromBase = 2, bool set = false)
{
lock (this)
{
slim.Reset();
StringBuilder sb = new StringBuilder();
//添加从站点,功能代码,起始位置
sb.AppendFormat("{0}{1}{2}", slaveStation.ToString("x2"), ((int)function).ToString("X2"), devAdress.ToString("X4"));
switch (function)
{
case FunctionCode.ReadCoils:
case FunctionCode.ReadDiscreteInputs:
case FunctionCode.ReadHoldingRegisters:
case FunctionCode.ReadInputRegisters:
//读取的数量,占用2byte
sb.Append(devLenght.ToString("X4"));
break;
case FunctionCode.WriteSingleCoil:
if (set)
{
//线圈置ON
sb.Append("FF00");
}
else
{
//线圈置OFF
sb.Append("0000");
}
break;
case FunctionCode.WriteSingleRegister:
//向单个寄存器写入值,占用两个byte
int curVal = BitConverter.ToInt16(values, 0);
sb.Append(curVal.ToString("X4"));
break;
case FunctionCode.WriteMultipleCoils:
//向多个线圈写入值
//1,需操作的线圈数量,数值占用两个byte
sb.Append(devLenght.ToString("X4"));
//2,需要操作的寄存器占用的字节数,数值占用1个byte
sb.Append(((int)Math.Ceiling(devLenght * 1.0 / 8)).ToString("X2"));
//3,写入线圈状态值
foreach (var item in values)
{
sb.Append(item.ToString("X2"));
}
break;
case FunctionCode.WriteMultipleRegisters:
//向多个寄存器写入值
//1,需操作的寄存器数量,数值占用两个byte
sb.Append((values.Length * 2 / fromBase).ToString("X4"));
//2,需要操作的寄存器占用的字节数,数值占用1个byte
sb.Append((values.Length).ToString("X2"));
//3,写入寄存器的值,根据frombase判定值
//此时姑且以占2个Byte计算
for (int i = 0; i < values.Length; i++)
{
sb.Append(values[i].ToString("X2"));
}
break;
case FunctionCode.ReportSlaveID:
break;
case FunctionCode.MaskWriteRegister:
break;
case FunctionCode.ReadWriteRegister:
break;
default:
break;
}
RecBytes = null;
Send(sb.ToString().Trim());
serialPortObject.DiscardInBuffer();
//100ms等待消息回复
msgRecFlag = slim.WaitOne(3000);
System.Threading.Thread.Sleep(20);
if (!msgRecFlag)
{
ErrorMessage?.Invoke(slaveStation, $"从站:{slaveStation},回复超时");
}
}
}
/// <summary>
/// 强制指定单个线圈动作
/// </summary>
/// <param name="slaveStationNo">从站站号</param>
/// <param name="devAdress">线圈地址</param>
/// <param name="onOrOff">True为ON,false为Off</param>
/// <returns></returns>
public bool ForcedCoil(int slaveStationNo, int devAdress, bool set)
{
ComposeMessage(slaveStationNo, FunctionCode.WriteSingleCoil, devAdress, set: set);
if (slaveStationNo == targetStation && msgRecFlag && RecBytes != null)
{
if (RecBytes[2] == 0xff && RecBytes[3] == 0 && set)
{
return true;
}
if (RecBytes[3] == 0 && RecBytes[2] == 0 && !set)
{
return true;
}
}
return false;
}
/// <summary>
/// 读取多个线圈状态
/// </summary>
/// <param name="slaveStationNo"></param>
/// <param name="devAdress"></param>
/// <param name="iLenght"></param>
public bool[] ReadCoils(int slaveStationNo, int devAdress, int iLenght)
{
ComposeMessage(slaveStationNo, FunctionCode.ReadCoils, devAdress, iLenght);
if (slaveStationNo == targetStation && msgRecFlag && RecBytes != null)
{
BitArray array = new BitArray(RecBytes);
if (iLenght > array.Count)
{
throw new Exception("线圈查询结果异常!");
}
bool[] coils = new bool[iLenght];
for (int i = 0; i < iLenght; i++)
{
coils[i] = array[i];
}
return coils;
}
else
{
return null;
}
}
/// <summary>
/// 向多个线圈写入值
/// </summary>
/// <param name="slaveStationNo">从站站号</param>
/// <param name="devAdress"></param>
/// <param name="iLenght">地址长度</param>
/// <param name="arr">写入的值</param>
public bool WriteMultipleCoils(int slaveStationNo, int devAdress, int iLenght, short[] arr)
{
if ((int)Math.Ceiling(iLenght * 1.0 / 16) != arr.Length)
{
throw new Exception("线圈数量与所赋值数量不匹配");
}
byte[] bytes = new byte[arr.Length * 2];
int index = 0;
foreach (var item in arr)
{
byte[] temBytes = BitConverter.GetBytes(item);
Array.Copy(temBytes, 0, bytes, index, temBytes.Length);
index += temBytes.Length;
}
ComposeMessage(slaveStationNo, FunctionCode.WriteMultipleCoils, devAdress, iLenght, bytes);
if (slaveStationNo == targetStation && msgRecFlag && RecBytes != null && RecBytes.Length == 4)
{
int startAddress = BitConverter.ToInt16(new byte[] { RecBytes[1], RecBytes[0] }, 0);
int addressLen = BitConverter.ToInt16(new byte[] { RecBytes[3], RecBytes[2] }, 0);
if (startAddress == devAdress && addressLen == iLenght)
{
return true;
}
else
{
return false;
}
}
else
{
return false;
}
}
/// <summary>
/// 读取从站多个寄存器的值
/// </summary>
/// <param name="slaveStationNo"></param>
/// <param name="devAdress">起始寄存器</param>
/// <param name="iLenght">寄存器数量</param>
public short[] ReadHoldingRegisters(int slaveStationNo, int devAdress, int iLenght)
{
ComposeMessage(slaveStationNo, FunctionCode.ReadHoldingRegisters, devAdress, iLenght);
if (slaveStationNo == targetStation && msgRecFlag && RecBytes != null)
{
List<short> shortList = new List<short>();
for (int i = 0; i < RecBytes.Length; i += 2)
{
shortList.Add(BitConverter.ToInt16(new byte[] { RecBytes[i + 1], RecBytes[i] }, 0));
}
return shortList.ToArray();
}
else
{
return null;
}
}
/// <summary>
/// 读取单精度浮点数
/// </summary>
/// <param name="slaveStationNo">从站站点</param>
/// <param name="devAdress">起始地址</param>
/// <param name="iLenght">连续读取浮点数的个数</param>
/// <returns></returns>
public float[] ReadFloatRegisters(int slaveStationNo, int devAdress, int iLenght)
{
ComposeMessage(slaveStationNo, FunctionCode.ReadHoldingRegisters, devAdress, iLenght * 2);
if (slaveStationNo == targetStation && msgRecFlag && RecBytes != null)
{
List<float> floatList = new List<float>();
for (int i = 0; i < RecBytes.Length; i += 4)
{
floatList.Add(BitConverter.ToSingle(new byte[] { RecBytes[i + 1], RecBytes[i], RecBytes[i + 3], RecBytes[i + 2] }, 0));
}
return floatList.ToArray();
}
else
{
return null;
}
}
/// <summary>
/// 向从站单个寄存器写入值
/// </summary>
/// <param name="slaveStationNo"></param>
/// <param name="devAdress"></param>
/// <param name="value"></param>
public bool WriteSingleRegister(int slaveStationNo, int devAdress, short value)
{
ComposeMessage(slaveStationNo, FunctionCode.WriteSingleRegister, devAdress, values: BitConverter.GetBytes(value));
if (slaveStationNo == targetStation && msgRecFlag && RecBytes != null && RecBytes.Length == 4)
{
int startAddress = BitConverter.ToInt16(new byte[] { RecBytes[1], RecBytes[0] }, 0);
int val = BitConverter.ToInt16(new byte[] { RecBytes[3], RecBytes[2] }, 0);
if (startAddress == devAdress && val == value)
{
return true;
}
else
{
return false;
}
}
else
{
return false;
}
}
/// <summary>
/// 写入单精度浮点数
/// </summary>
/// <param name="slaveStationNo">从站站点</param>
/// <param name="devAdress">寄存器地址</param>
/// <param name="value">值</param>
/// <returns>true写入成功,false写入失败</returns>
public bool WriteFloatRegister(int slaveStationNo, int devAdress, float value)
{
//进行拆解组合
byte[] bytes = BitConverter.GetBytes(value);
byte[] valBytes = new byte[] { bytes[1], bytes[0], bytes[3], bytes[2] };
ComposeMessage(slaveStationNo, FunctionCode.WriteMultipleRegisters, devAdress, values: valBytes, fromBase: 4);
if (slaveStationNo == targetStation && msgRecFlag && RecBytes != null)
{
short adress = BitConverter.ToInt16(new byte[] { RecBytes[1], RecBytes[0] }, 0);
short registerCount = BitConverter.ToInt16(new byte[] { RecBytes[3], RecBytes[2] }, 0);
if (adress == devAdress && registerCount == 2)
{
return true;
}
else
{
return false;
}
}
else
{
return false;
}
}
/// <summary>
/// 向从站多个寄存器写入值
/// </summary>
/// <param name="slaveStationNo"></param>
/// <param name="devAdress"></param>
/// <param name="iLenght"></param>
/// <param name="arr"></param>
public bool WriteMultipleRegisters(int slaveStationNo, int devAdress, int iLenght, short[] arr)
{
if (iLenght != arr.Length)
{
throw new Exception("寄存器数量与所赋值数量不匹配");
}
byte[] bytes = new byte[arr.Length * 2];
int index = 0;
foreach (var item in arr)
{
byte[] temBytes = BitConverter.GetBytes(item);
byte[] temBytes2 = new byte[] { temBytes[1], temBytes[0] };
Array.Copy(temBytes2, 0, bytes, index, temBytes2.Length);
index += temBytes.Length;
}
ComposeMessage(slaveStationNo, FunctionCode.WriteMultipleRegisters, devAdress, iLenght, bytes, fromBase: 4);
if (slaveStationNo == targetStation && msgRecFlag && RecBytes != null && RecBytes.Length == 4)
{
int startAddress = BitConverter.ToInt16(new byte[] { RecBytes[1], RecBytes[0] }, 0);
int addressLen = BitConverter.ToInt16(new byte[] { RecBytes[3], RecBytes[2] }, 0);
if (startAddress == devAdress && addressLen == iLenght)
{
return true;
}
else
{
return false;
}
}
else
{
return false;
}
}
public void Dispose()
{
serialPortObject?.Dispose();
}
}
3.6.5,校验码类
/// <summary>
/// 数据校验帮助类,计算常见的CRC校验;LRC校验;BCC校验;累加和校验
/// </summary>
public class Checkhelper
{
#region CRC校验说明
// CRC即循环冗余校验码(Cyclic Redundancy Check):是数据通信领域中最常用的一种查错校验码,
//其特征是信息字段和校验字段的长度可以任意选定。循环冗余检查(CRC)是一种数据传输检错功能,对数据进行多项式计算,
//并将得到的结果附在帧的后面,接收设备也执行类似的算法,以保证数据传输的正确性和完整性。
// CRC算法参数模型解释:
// NAME:参数模型名称。
//WIDTH:宽度,即CRC比特数。
// POLY:生成项的简写,以16进制表示。例如:CRC-32即是0x04C11DB7,忽略了最高位的"1",即完整的生成项是0x104C11DB7。
//INIT:这是算法开始时寄存器(crc)的初始化预置值,十六进制表示。
//REFIN:待测数据的每个字节是否按位反转,True或False。
// REFOUT:在计算后之后,异或输出之前,整个数据是否按位反转,True或False。
// XOROUT:计算结果与此参数异或后得到最终的CRC值。
#endregion
// **********************************************************************
// Name: CRC-4/ITU x4+x+1
// Poly: 0x03
// Init: 0x00
// Refin: true
// Refout: true
// Xorout: 0x00
//*************************************************************************
/// <summary>
/// CRC-4/ITU
/// </summary>
/// <param name="buffer"></param>
/// <param name="start"></param>
/// <param name="len"></param>
/// <returns></returns>
public static byte[] Crc1(byte[] buffer, int start = 0, int len = 0)
{
if (buffer == null || buffer.Length == 0) return null;
if (start < 0) return null;
if (len == 0) len = buffer.Length - start;
int length = start + len;
if (length > buffer.Length) return null;
byte crc = 0;// Initial value
for (int i = start; i < length; i++)
{
crc ^= buffer[i];
for (int j = 0; j < 8; j++)
{
if ((crc & 1) > 0)
crc = (byte)((crc >> 1) ^ 0x0C);//0x0C = (reverse 0x03)>>(8-4)
else
crc = (byte)(crc >> 1);
}
}
return new byte[] { crc };
}
// **********************************************************************
// Name: CRC-5/EPC x5+x3+1
// Poly: 0x09
// Init: 0x09
// Refin: false
// Refout: false
// Xorout: 0x00
//*************************************************************************
/// <summary>
/// CRC-5/EPC
/// </summary>
/// <param name="buffer"></param>
/// <param name="start"></param>
/// <param name="len"></param>
/// <returns></returns>
public static byte[] Crc2(byte[] buffer, int start = 0, int len = 0)
{
if (buffer == null || buffer.Length == 0) return null;
if (start < 0) return null;
if (len == 0) len = buffer.Length - start;
int length = start + len;
if (length > buffer.Length) return null;
byte crc = 0x48;// Initial value: 0x48 = 0x09<<(8-5)
for (int i = start; i < length; i++)
{
crc ^= buffer[i];
for (int j = 0; j < 8; j++)
{
if ((crc & 0x80) > 0)
crc = (byte)((crc << 1) ^ 0x48);// 0x48 = 0x09<<(8-5)
else
crc = (byte)(crc << 1);
}
}
return new byte[] { (byte)(crc >> 3) };
}
// **********************************************************************
// Name: CRC-5/ITU x5+x4+x2+1
// Poly: 0x15
// Init: 0x00
// Refin: true
// Refout: true
// Xorout: 0x00
//*************************************************************************
/// <summary>
/// CRC-5/ITU
/// </summary>
/// <param name="buffer"></param>
/// <param name="start"></param>
/// <param name="len"></param>
/// <returns></returns>
public static byte[] Crc3(byte[] buffer, int start = 0, int len = 0)
{
if (buffer == null || buffer.Length == 0) return null;
if (start < 0) return null;
if (len == 0) len = buffer.Length - start;
int length = start + len;
if (length > buffer.Length) return null;
byte crc = 0;// Initial value
for (int i = start; i < length; i++)
{
crc ^= buffer[i];
for (int j = 0; j < 8; j++)
{
if ((crc & 1) > 0)
crc = (byte)((crc >> 1) ^ 0x15);// 0x15 = (reverse 0x15)>>(8-5)
else
crc = (byte)(crc >> 1);
}
}
return new byte[] { crc };
}
// **********************************************************************
// Name: CRC-5/USB x5+x2+1
// Poly: 0x05
// Init: 0x1F
// Refin: true
// Refout: true
// Xorout: 0x1F
//*************************************************************************
/// <summary>
/// CRC-5/USB
/// </summary>
/// <param name="buffer"></param>
/// <param name="start"></param>
/// <param name="len"></param>
/// <returns></returns>
public static byte[] Crc4(byte[] buffer, int start = 0, int len = 0)
{
if (buffer == null || buffer.Length == 0) return null;
if (start < 0) return null;
if (len == 0) len = buffer.Length - start;
int length = start + len;
if (length > buffer.Length) return null;
byte crc = 0x1F;// Initial value
for (int i = start; i < length; i++)
{
crc ^= buffer[i];
for (int j = 0; j < 8; j++)
{
if ((crc & 1) > 0)
crc = (byte)((crc >> 1) ^ 0x14);// 0x14 = (reverse 0x05)>>(8-5)
else
crc = (byte)(crc >> 1);
}
}
return new byte[] { (byte)(crc ^ 0x1F) };
}
// **********************************************************************
// Name: CRC-6/ITU x6+x+1
// Poly: 0x03
// Init: 0x00
// Refin: true
// Refout: true
// Xorout: 0x00
//*************************************************************************
/// <summary>
/// CRC-6/ITU
/// </summary>
/// <param name="buffer"></param>
/// <param name="start"></param>
/// <param name="len"></param>
/// <returns></returns>
public static byte[] Crc5(byte[] buffer, int start = 0, int len = 0)
{
if (buffer == null || buffer.Length == 0) return null;
if (start < 0) return null;
if (len == 0) len = buffer.Length - start;
int length = start + len;
if (length > buffer.Length) return null;
byte crc = 0;// Initial value
for (int i = start; i < length; i++)
{
crc ^= buffer[i];
for (int j = 0; j < 8; j++)
{
if ((crc & 1) > 0)
crc = (byte)((crc >> 1) ^ 0x30);// 0x30 = (reverse 0x03)>>(8-6)
else
crc = (byte)(crc >> 1);
}
}
return new byte[] { crc };
}
// **********************************************************************
// Name: CRC-7/MMC x7+x3+1
// Poly: 0x09
// Init: 0x00
// Refin: false
// Refout: false
// Xorout: 0x00
//*************************************************************************
/// <summary>
/// CRC-7/MMC
/// </summary>
/// <param name="buffer"></param>
/// <param name="start"></param>
/// <param name="len"></param>
/// <returns></returns>
public static byte[] Crc6(byte[] buffer, int start = 0, int len = 0)
{
if (buffer == null || buffer.Length == 0) return null;
if (start < 0) return null;
if (len == 0) len = buffer.Length - start;
int length = start + len;
if (length > buffer.Length) return null;
byte crc = 0;// Initial value
for (int i = start; i < length; i++)
{
crc ^= buffer[i];
for (int j = 0; j < 8; j++)
{
if ((crc & 0x80) > 0)
crc = (byte)((crc << 1) ^ 0x12);// 0x12 = 0x09<<(8-7)
else
crc = (byte)(crc << 1);
}
}
return new byte[] { (byte)(crc >> 1) };
}
// **********************************************************************
// Name: CRC8 x8+x2+x+1
// Poly: 0x07
// Init: 0x00
// Refin: false
// Refout: false
// Xorout: 0x00
//*************************************************************************
/// <summary>
/// CRC8
/// </summary>
/// <param name="buffer"></param>
/// <param name="start"></param>
/// <param name="len"></param>
/// <returns></returns>
public static byte[] Crc7(byte[] buffer, int start = 0, int len = 0)
{
if (buffer == null || buffer.Length == 0) return null;
if (start < 0) return null;
if (len == 0) len = buffer.Length - start;
int length = start + len;
if (length > buffer.Length) return null;
byte crc = 0;// Initial value
for (int i = start; i < length; i++)
{
crc ^= buffer[i];
for (int j = 0; j < 8; j++)
{
if ((crc & 0x80) > 0)
crc = (byte)((crc << 1) ^ 0x07);
else
crc = (byte)(crc << 1);
}
}
return new byte[] { crc };
}
// **********************************************************************
// Name: CRC-8/ITU x8+x2+x+1
// Poly: 0x07
// Init: 0x00
// Refin: false
// Refout: false
// Xorout: 0x55
//*************************************************************************
/// <summary>
/// CRC-8/ITU
/// </summary>
/// <param name="buffer"></param>
/// <param name="start"></param>
/// <param name="len"></param>
/// <returns></returns>
public static byte[] Crc8(byte[] buffer, int start = 0, int len = 0)
{
if (buffer == null || buffer.Length == 0) return null;
if (start < 0) return null;
if (len == 0) len = buffer.Length - start;
int length = start + len;
if (length > buffer.Length) return null;
byte crc = 0;// Initial value
for (int i = start; i < length; i++)
{
crc ^= buffer[i];
for (int j = 0; j < 8; j++)
{
if ((crc & 0x80) > 0)
crc = (byte)((crc << 1) ^ 0x07);
else
crc = (byte)(crc << 1);
}
}
return new byte[] { (byte)(crc ^ 0x55) };
}
// **********************************************************************
// Name: CRC-8/MAXIM x8+x5+x4+1
// Poly: 0x31
// Init: 0x00
// Refin: true
// Refout: true
// Xorout: 0x00
//*************************************************************************
/// <summary>
/// CRC-8/MAXIM
/// </summary>
/// <param name="buffer"></param>
/// <param name="start"></param>
/// <param name="len"></param>
/// <returns></returns>
public static byte[] Crc9(byte[] buffer, int start = 0, int len = 0)
{
if (buffer == null || buffer.Length == 0) return null;
if (start < 0) return null;
if (len == 0) len = buffer.Length - start;
int length = start + len;
if (length > buffer.Length) return null;
byte crc = 0;// Initial value
for (int i = start; i < length; i++)
{
crc ^= buffer[i];
for (int j = 0; j < 8; j++)
{
if ((crc & 1) > 0)
crc = (byte)((crc >> 1) ^ 0x8C);// 0x8C = reverse 0x31
else
crc = (byte)(crc >> 1);
}
}
return new byte[] { crc };
}
// **********************************************************************
// Name: CRC-8/ROHC x8+x2+x+1
// Poly: 0x07
// Init: 0xFF
// Refin: true
// Refout: true
// Xorout: 0x00
//*************************************************************************
/// <summary>
/// CRC-8/ROHC
/// </summary>
/// <param name="buffer"></param>
/// <param name="start"></param>
/// <param name="len"></param>
/// <returns></returns>
public static byte[] Crc10(byte[] buffer, int start = 0, int len = 0)
{
if (buffer == null || buffer.Length == 0) return null;
if (start < 0) return null;
if (len == 0) len = buffer.Length - start;
int length = start + len;
if (length > buffer.Length) return null;
byte crc = 0xFF;// Initial value
for (int i = start; i < length; i++)
{
crc ^= buffer[i];
for (int j = 0; j < 8; j++)
{
if ((crc & 1) > 0)
crc = (byte)((crc >> 1) ^ 0xE0);// 0xE0 = reverse 0x07
else
crc = (byte)(crc >> 1);
}
}
return new byte[] { crc };
}
/// Z1协议校验码计算
static byte[] table = { 0x00, 0x1C, 0x38, 0x24, 0x70, 0x6C, 0x48, 0x54, 0xE0, 0xFC,
0xD8, 0xC4, 0x90, 0x8C, 0xA8, 0xB4, 0xDC, 0xC0, 0xE4, 0xF8,
0xAC, 0xB0, 0x94, 0x88, 0x3C, 0x20, 0x04, 0x18, 0x4C, 0x50,
0x74, 0x68, 0xA4, 0xB8, 0x9C, 0x80, 0xD4, 0xC8, 0xEC, 0xF0,
0x44, 0x58, 0x7C, 0x60, 0x34, 0x28, 0x0C, 0x10, 0x78, 0x64,
0x40, 0x5C, 0x08, 0x14, 0x30, 0x2C, 0x98, 0x84, 0xA0, 0xBC,
0xE8, 0xF4, 0xD0, 0xCC, 0x54, 0x48, 0x6C, 0x70, 0x24, 0x38,
0x1C, 0x00, 0xB4, 0xA8, 0x8C, 0x90, 0xC4, 0xD8, 0xFC, 0xE0,
0x88, 0x94, 0xB0, 0xAC, 0xF8, 0xE4, 0xC0, 0xDC, 0x68, 0x74,
0x50, 0x4C, 0x18, 0x04, 0x20, 0x3C, 0xF0, 0xEC, 0xC8, 0xD4,
0x80, 0x9C, 0xB8, 0xA4, 0x10, 0x0C, 0x28, 0x34, 0x60, 0x7C,
0x58, 0x44, 0x2C, 0x30, 0x14, 0x08, 0x5C, 0x40, 0x64, 0x78,
0xCC, 0xD0, 0xF4, 0xE8, 0xBC, 0xA0, 0x84, 0x98, 0xA8, 0xB4,
0x90, 0x8C, 0xD8, 0xC4, 0xE0, 0xFC, 0x48, 0x54, 0x70, 0x6C,
0x38, 0x24, 0x00, 0x1C, 0x74, 0x68, 0x4C, 0x50, 0x04, 0x18,
0x3C, 0x20, 0x94, 0x88, 0xAC, 0xB0, 0xE4, 0xF8, 0xDC, 0xC0,
0x0C, 0x10, 0x34, 0x28, 0x7C, 0x60, 0x44, 0x58, 0xEC, 0xF0,
0xD4, 0xC8, 0x9C, 0x80, 0xA4, 0xB8, 0xD0, 0xCC, 0xE8, 0xF4,
0xA0, 0xBC, 0x98, 0x84, 0x30, 0x2C, 0x08, 0x14, 0x40, 0x5C,
0x78, 0x64, 0xFC, 0xE0, 0xC4, 0xD8, 0x8C, 0x90, 0xB4, 0xA8,
0x1C, 0x00, 0x24, 0x38, 0x6C, 0x70, 0x54, 0x48, 0x20, 0x3C,
0x18, 0x04, 0x50, 0x4C, 0x68, 0x74, 0xC0, 0xDC, 0xF8, 0xE4,
0xB0, 0xAC, 0x88, 0x94, 0x58, 0x44, 0x60, 0x7C, 0x28, 0x34,
0x10, 0x0C, 0xB8, 0xA4, 0x80, 0x9C, 0xC8, 0xD4, 0xF0, 0xEC,
0x84, 0x98, 0xBC, 0xA0, 0xF4, 0xE8, 0xCC, 0xD0, 0x64, 0x78,
0x5C, 0x40, 0x14, 0x08, 0x2C, 0x30
};
public static byte[] Crc11(byte[] buffer, int start = 0, int len = 0)
{
if (buffer == null || buffer.Length == 0) return null;
if (start < 0) return null;
if (len == 0) len = buffer.Length - start;
int length = start + len;
if (length > buffer.Length) return null;
int i;
byte crc = 0x00;
int tableIndex;
for (i = start; i < length; i++)
{
tableIndex = crc ^ (buffer[i] & 0xFF);
crc = table[tableIndex];
}
return new byte[] { crc };
}
// **********************************************************************
// Name: CRC-12 x16+x12+x5+1
// Poly: 0x80
// Init: 0x0000
// Refin: true
// Refout: true
// Xorout: 0x0000
//*************************************************************************
/// <summary>
/// CRC-12
/// </summary>
/// <param name="buffer"></param>
/// <param name="start"></param>
/// <param name="len"></param>
/// <returns></returns>
public static byte[] Crc12(byte[] buffer, int start = 0, int len = 0)
{
if (buffer == null || buffer.Length == 0) return null;
if (start < 0) return null;
if (len == 0) len = buffer.Length - start;
int length = start + len;
if (length > buffer.Length) return null;
ushort crc = 0;// Initial value
short iQ = 0, iR = 0;
for (int i = start; i < length; i++)
{
// 多项式除法
// 如果该位为1
if ((buffer[i] & (0x80 >> iR)) > 0)
{
// 则在余数尾部添1否则添0
crc |= 0x01;
}
// 如果12位除数中的最高位为1,则够除
if (crc >= 0x1000)
{
crc ^= 0x180D;
}
crc <<= 1;
iR++;
if (8 == iR)
{
iR = 0;
iQ++;
}
}
// 对后面添加的12个0做处理
for (int i = 0; i < 12; i++)
{
if (crc >= 0x1000)
{
crc ^= 0x180D;
}
crc <<= 1;
}
crc >>= 1;
byte[] ret = BitConverter.GetBytes(crc);
Array.Reverse(ret);
return ret;
}
// **********************************************************************
// Name: CRC-16/CCITT x16+x12+x5+1
// Poly: 0x1021
// Init: 0x0000
// Refin: true
// Refout: true
// Xorout: 0x0000
//*************************************************************************
/// <summary>
/// CRC-16/CCITT
/// </summary>
/// <param name="buffer"></param>
/// <param name="start"></param>
/// <param name="len"></param>
/// <returns></returns>
public static byte[] Crc13(byte[] buffer, int start = 0, int len = 0)
{
if (buffer == null || buffer.Length == 0) return null;
if (start < 0) return null;
if (len == 0) len = buffer.Length - start;
int length = start + len;
if (length > buffer.Length) return null;
ushort crc = 0;// Initial value
for (int i = start; i < length; i++)
{
crc ^= buffer[i];
for (int j = 0; j < 8; j++)
{
if ((crc & 1) > 0)
crc = (ushort)((crc >> 1) ^ 0x8408);// 0x8408 = reverse 0x1021
else
crc = (ushort)(crc >> 1);
}
}
byte[] ret = BitConverter.GetBytes(crc);
Array.Reverse(ret);
return ret;
}
// **********************************************************************
// Name: CRC-16/CCITT FALSE x16+x12+x5+1
// Poly: 0x1021
// Init: 0xFFFF
// Refin: false
// Refout: false
// Xorout: 0x0000
//*************************************************************************
/// <summary>
/// CRC-16/CCITT FALSE
/// </summary>
/// <param name="buffer"></param>
/// <param name="start"></param>
/// <param name="len"></param>
/// <returns></returns>
public static byte[] Crc14(byte[] buffer, int start = 0, int len = 0)
{
if (buffer == null || buffer.Length == 0) return null;
if (start < 0) return null;
if (len == 0) len = buffer.Length - start;
int length = start + len;
if (length > buffer.Length) return null;
ushort crc = 0xFFFF;// Initial value
for (int i = start; i < length; i++)
{
crc ^= (ushort)(buffer[i] << 8);
for (int j = 0; j < 8; j++)
{
if ((crc & 0x8000) > 0)
crc = (ushort)((crc << 1) ^ 0x1021);
else
crc = (ushort)(crc << 1);
}
}
byte[] ret = BitConverter.GetBytes(crc);
Array.Reverse(ret);
return ret;
}
// **********************************************************************
// Name: CRC-16/DNP x16+x13+x12+x11+x10+x8+x6+x5+x2+1
// Poly: 0x3D65
// Init: 0x0000
// Refin: true
// Refout: true
// Xorout: 0xFFFF
//*************************************************************************
/// <summary>
/// CRC-16/DNP
/// </summary>
/// <param name="buffer"></param>
/// <param name="start"></param>
/// <param name="len"></param>
/// <returns></returns>
public static byte[] Crc15(byte[] buffer, int start = 0, int len = 0)
{
if (buffer == null || buffer.Length == 0) return null;
if (start < 0) return null;
if (len == 0) len = buffer.Length - start;
int length = start + len;
if (length > buffer.Length) return null;
ushort crc = 0;// Initial value
for (int i = start; i < length; i++)
{
crc ^= buffer[i];
for (int j = 0; j < 8; j++)
{
if ((crc & 1) > 0)
crc = (ushort)((crc >> 1) ^ 0xA6BC);// 0xA6BC = reverse 0x3D65
else
crc = (ushort)(crc >> 1);
}
}
byte[] ret = BitConverter.GetBytes((ushort)~crc);
Array.Reverse(ret);
return ret;
}
// **********************************************************************
// Name: CRC-16/IBM x16+x15+x2+1
// Poly: 0x8005
// Init: 0x0000
// Refin: true
// Refout: true
// Xorout: 0x0000
//*************************************************************************
/// <summary>
/// CRC-16/IBM
/// </summary>
/// <param name="buffer"></param>
/// <param name="start"></param>
/// <param name="len"></param>
/// <returns></returns>
public static byte[] Crc16(byte[] buffer, int start = 0, int len = 0)
{
if (buffer == null || buffer.Length == 0) return null;
if (start < 0) return null;
if (len == 0) len = buffer.Length - start;
int length = start + len;
if (length > buffer.Length) return null;
ushort crc = 0;// Initial value
for (int i = start; i < length; i++)
{
crc ^= buffer[i];
for (int j = 0; j < 8; j++)
{
if ((crc & 1) > 0)
crc = (ushort)((crc >> 1) ^ 0xA001);// 0xA001 = reverse 0x8005
else
crc = (ushort)(crc >> 1);
}
}
byte[] ret = BitConverter.GetBytes(crc);
Array.Reverse(ret);
return ret;
}
// **********************************************************************
// Name: CRC-16/MAXIM x16+x15+x2+1
// Poly: 0x8005
// Init: 0x0000
// Refin: true
// Refout: true
// Xorout: 0xFFFF
//*************************************************************************
/// <summary>
/// CRC-16/MAXIM
/// </summary>
/// <param name="buffer"></param>
/// <param name="start"></param>
/// <param name="len"></param>
/// <returns></returns>
public static byte[] Crc17(byte[] buffer, int start = 0, int len = 0)
{
if (buffer == null || buffer.Length == 0) return null;
if (start < 0) return null;
if (len == 0) len = buffer.Length - start;
int length = start + len;
if (length > buffer.Length) return null;
ushort crc = 0;// Initial value
for (int i = start; i < length; i++)
{
crc ^= buffer[i];
for (int j = 0; j < 8; j++)
{
if ((crc & 1) > 0)
crc = (ushort)((crc >> 1) ^ 0xA001);// 0xA001 = reverse 0x8005
else
crc = (ushort)(crc >> 1);
}
}
byte[] ret = BitConverter.GetBytes((ushort)~crc);
Array.Reverse(ret);
return ret;
}
// **********************************************************************
// Name: CRC-16/MODBUS x16+x15+x2+1
// Poly: 0x8005
// Init: 0xFFFF
// Refin: true
// Refout: true
// Xorout: 0x0000
//*************************************************************************
/// <summary>
/// CRC-16/MODBUS
/// </summary>
/// <param name="buffer"></param>
/// <param name="start"></param>
/// <param name="len"></param>
/// <returns></returns>
public static byte[] CrcModbus(byte[] buffer, int start = 0, int len = 0)
{
if (buffer == null || buffer.Length == 0) return null;
if (start < 0) return null;
if (len == 0) len = buffer.Length - start;
int length = start + len;
if (length > buffer.Length) return null;
ushort crc = 0xFFFF;// Initial value
for (int i = start; i < length; i++)
{
crc ^= buffer[i];
for (int j = 0; j < 8; j++)
{
if ((crc & 1) > 0)
crc = (ushort)((crc >> 1) ^ 0xA001);// 0xA001 = reverse 0x8005
else
crc = (ushort)(crc >> 1);
}
}
byte[] ret = BitConverter.GetBytes(crc);
Array.Reverse(ret);
return ret;
}
// **********************************************************************
// Name: CRC-16/USB x16+x15+x2+1
// Poly: 0x8005
// Init: 0xFFFF
// Refin: true
// Refout: true
// Xorout: 0xFFFF
//*************************************************************************
/// <summary>
/// CRC-16/USB
/// </summary>
/// <param name="buffer"></param>
/// <param name="start"></param>
/// <param name="len"></param>
/// <returns></returns>
public static byte[] Crc19(byte[] buffer, int start = 0, int len = 0)
{
if (buffer == null || buffer.Length == 0) return null;
if (start < 0) return null;
if (len == 0) len = buffer.Length - start;
int length = start + len;
if (length > buffer.Length) return null;
ushort crc = 0xFFFF;// Initial value
for (int i = start; i < length; i++)
{
crc ^= buffer[i];
for (int j = 0; j < 8; j++)
{
if ((crc & 1) > 0)
crc = (ushort)((crc >> 1) ^ 0xA001);// 0xA001 = reverse 0x8005
else
crc = (ushort)(crc >> 1);
}
}
byte[] ret = BitConverter.GetBytes((ushort)~crc);
Array.Reverse(ret);
return ret;
}
// **********************************************************************
// Name: CRC-16/X25 x16+x12+x5+1
// Poly: 0x1021
// Init: 0xFFFF
// Refin: true
// Refout: true
// Xorout: 0xFFFF
//*************************************************************************
/// <summary>
/// CRC-16/X25
/// </summary>
/// <param name="buffer"></param>
/// <param name="start"></param>
/// <param name="len"></param>
/// <returns></returns>
public static byte[] Crc20(byte[] buffer, int start = 0, int len = 0)
{
if (buffer == null || buffer.Length == 0) return null;
if (start < 0) return null;
if (len == 0) len = buffer.Length - start;
int length = start + len;
if (length > buffer.Length) return null;
ushort crc = 0xFFFF;// Initial value
for (int i = start; i < length; i++)
{
crc ^= buffer[i];
for (int j = 0; j < 8; j++)
{
if ((crc & 1) > 0)
crc = (ushort)((crc >> 1) ^ 0x8408);// 0x8408 = reverse 0x1021
else
crc = (ushort)(crc >> 1);
}
}
byte[] ret = BitConverter.GetBytes((ushort)~crc);
Array.Reverse(ret);
return ret;
}
// **********************************************************************
// Name: CRC-16/XMODEM x16+x12+x5+1
// Poly: 0x1021
// Init: 0x0000
// Refin: false
// Refout: false
// Xorout: 0x0000
//*************************************************************************
/// <summary>
/// CRC-16/XMODEM
/// </summary>
/// <param name="buffer"></param>
/// <param name="start"></param>
/// <param name="len"></param>
/// <returns></returns>
public static byte[] Crc21(byte[] buffer, int start = 0, int len = 0)
{
if (buffer == null || buffer.Length == 0) return null;
if (start < 0) return null;
if (len == 0) len = buffer.Length - start;
int length = start + len;
if (length > buffer.Length) return null;
ushort crc = 0;// Initial value
for (int i = start; i < length; i++)
{
crc ^= (ushort)(buffer[i] << 8);
for (int j = 0; j < 8; j++)
{
if ((crc & 0x8000) > 0)
crc = (ushort)((crc << 1) ^ 0x1021);
else
crc = (ushort)(crc << 1);
}
}
byte[] ret = BitConverter.GetBytes(crc);
Array.Reverse(ret);
return ret;
}
// **********************************************************************
// Name: CRC32 x32+x26+x23+x22+x16+x12+x11+x10+x8+x7+x5+x4+x2+x+1
// Poly: 0x04C11DB7
// Init: 0xFFFFFFFF
// Refin: true
// Refout: true
// Xorout: 0xFFFFFFFF
//*************************************************************************
/// <summary>
/// CRC32
/// </summary>
/// <param name="buffer"></param>
/// <param name="start"></param>
/// <param name="len"></param>
/// <returns></returns>
public static byte[] Crc22(byte[] buffer, int start = 0, int len = 0)
{
if (buffer == null || buffer.Length == 0) return null;
if (start < 0) return null;
if (len == 0) len = buffer.Length - start;
int length = start + len;
if (length > buffer.Length) return null;
uint crc = 0xFFFFFFFF;// Initial value
for (int i = start; i < length; i++)
{
crc ^= buffer[i];
for (int j = 0; j < 8; j++)
{
if ((crc & 1) > 0)
crc = (crc >> 1) ^ 0xEDB88320;// 0xEDB88320= reverse 0x04C11DB7
else
crc = crc >> 1;
}
}
byte[] ret = BitConverter.GetBytes(~crc);
Array.Reverse(ret);
return ret;
}
// **********************************************************************
// Name: CRC32/MPEG-2 x32+x26+x23+x22+x16+x12+x11+x10+x8+x7+x5+x4+x2+x+1
// Poly: 0x04C11DB7
// Init: 0xFFFFFFFF
// Refin: false
// Refout: false
// Xorout: 0x00000000
//*************************************************************************
/// <summary>
/// CRC32/MPEG-2
/// </summary>
/// <param name="buffer"></param>
/// <param name="start"></param>
/// <param name="len"></param>
/// <returns></returns>
public static byte[] Crc23(byte[] buffer, int start = 0, int len = 0)
{
if (buffer == null || buffer.Length == 0) return null;
if (start < 0) return null;
if (len == 0) len = buffer.Length - start;
int length = start + len;
if (length > buffer.Length) return null;
uint crc = 0xFFFFFFFF;// Initial value
for (int i = start; i < length; i++)
{
crc ^= (uint)(buffer[i] << 24);
for (int j = 0; j < 8; j++)
{
if ((crc & 0x80000000) > 0)
crc = (crc << 1) ^ 0x04C11DB7;
else
crc = crc << 1;
}
}
byte[] ret = BitConverter.GetBytes(crc);
Array.Reverse(ret);
return ret;
}
//***************************************************************
/* 纵向冗余校验(Longitudinal Redundancy Check,简称:LRC)是通信中常用的一种校验形式,也称LRC校验或纵向校验。
它是一种从纵向通道上的特定比特串产生校验比特的错误检测方法。在行列格式中(如磁带),LRC经常是与VRC一起使用,这样就会
为每个字符校验码。在工业领域Modbus协议Ascii模式采用该算法。 LRC计算校验码,具体算法如下:*/
// 1、对需要校验的数据(2n个字符)两两组成一个16进制的数值求和。
// 2、将求和结果与256求模。
// 3、用256减去所得模值得到校验结果(另一种方法:将模值按位取反然后加1)。
/// <summary>
/// LRC校验(纵向冗余校验)工业领域Modbus协议Ascii模式采用该算法
/// </summary>
/// <param name="buffer"></param>
/// <param name="start"></param>
/// <param name="len"></param>
/// <returns></returns>
public static byte[] Lrc(byte[] buffer, int start = 0, int len = 0)
{
if (buffer == null || buffer.Length == 0) return null;
if (start < 0) return null;
if (len == 0) len = buffer.Length - start;
int length = start + len;
if (length > buffer.Length) return null;
byte lrc = 0;// Initial value
for (int i = start; i < len; i++)
{
lrc += buffer[i];
}
lrc = (byte)((lrc ^ 0xFF) + 1);
return new byte[] { lrc };
}
/// <summary>
/// BCC(Block Check Character/信息组校验码),因校验码是将所有数据异或得出,故俗称异或校验。具体算法是:将每一个字节的数据(一般是两个16进制的字符)进行异或后即得到校验码。
/// </summary>
/// <param name="buffer"></param>
/// <param name="start"></param>
/// <param name="len"></param>
/// <returns></returns>
public static byte[] Bcc(byte[] buffer, int start = 0, int len = 0)
{
if (buffer == null || buffer.Length == 0) return null;
if (start < 0) return null;
if (len == 0) len = buffer.Length - start;
int length = start + len;
if (length > buffer.Length) return null;
byte bcc = 0;// Initial value
for (int i = start; i < len; i++)
{
bcc ^= buffer[i];
}
return new byte[] { bcc };
}
/// <summary>
/// 检验和(checksum),在数据处理和数据通信领域中,用于校验目的地一组数据项的和。它通常是以十六进制为数制表示的形式。如果校验和的数值超过十六进制的FF,也就是255. 就要求其补码作为校验和。通常用来在通信中,尤其是远距离通信中保证数据的完整性和准确性。
/// </summary>
/// <param name="buffer"></param>
/// <param name="start"></param>
/// <param name="len"></param>
/// <returns></returns>
public static byte[] allAdd(byte[] buffer, int start = 0, int len = 0)
{
if (buffer == null || buffer.Length == 0) return null;
if (start < 0) return null;
if (len == 0) len = buffer.Length - start;
int length = start + len;
if (length > buffer.Length) return null;
byte bcc = 0;// Initial value
for (int i = start; i < len; i++)
{
bcc ^= buffer[i];
}
return new byte[] { bcc };
}
}
/// <summary>
/// 操作功能码
/// </summary>
public enum FunctionCode
{
/// <summary>
/// 读写从站输出线圈状态0XXXX状态
/// </summary>
ReadCoils = 1,
/// <summary>
/// 只读从站输入线圈状态1XXXX状态
/// </summary>
ReadDiscreteInputs = 2,
/// <summary>
/// 读写从站保存寄存器4XXXX状态
/// </summary>
ReadHoldingRegisters = 3,
/// <summary>
/// 只读从站输入寄存器3XXXX值
/// </summary>
ReadInputRegisters = 4,
/// <summary>
/// 强制从站单个线圈状态
/// </summary>
WriteSingleCoil = 5,
/// <summary>
/// 向从站单个寄存器写入值
/// </summary>
WriteSingleRegister = 6,
/// <summary>
/// 强制从站多个线圈状态
/// </summary>
WriteMultipleCoils = 15,
/// <summary>
/// 向从站多个寄存器写入值
/// </summary>
WriteMultipleRegisters = 16,
ReportSlaveID = 17,
MaskWriteRegister = 22,
ReadWriteRegister = 23
}
/// <summary>
/// 从站返回异常代码
/// </summary>
public enum ErrorCode
{
/// <summary>
/// 非法功能
/// </summary>
IllegalFunction,
/// <summary>
/// 非法数据地址
/// </summary>
IllegalDataAddress,
/// <summary>
/// 非法数据
/// </summary>
IllegalData,
/// <summary>
/// 相关设备故障
/// </summary>
RelatedEquipmentFailure,
/// <summary>
/// 确认
/// </summary>
Verify,
/// <summary>
/// 忙碌、拒绝执行
/// </summary>
Busy,
}
4,基于ModbusTCP通信。
4.1,ModbusTCP报文格式参考链接
https://www.cnblogs.com/Kirito-Asuna-Yoyi/p/ModbusTCP.html
注明:ModbusTCP是基于以太网Socket通信的协议,无需校验码。
4.2,代码
4.2.1,准备
配置连接参数。
配置功能
4.2.2,UI部分
4.2.3,UI代码部分
public partial class Form1 : Form
{
ModbusTCP modbus;
int slaveStation = 10;
Timer timer = new Timer();
public Form1()
{
InitializeComponent();
UIControlStatusChange(false);
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
modbus = new ModbusTCP();
modbus.ErrorMessage += Modbus_ErrorMessage;
timer.Interval = 1000;
foreach (Control item in groupBox1.Controls)
{
if (item is GroupBox)
{
foreach (Control c in item.Controls)
{
if (c is Button)
{
(c as Button).Click += Button_Click;
}
}
}
}
}
private void Button_Click(object sender, EventArgs e)
{
lblErrorMsg.Text = "";
}
private void Modbus_ErrorMessage(int arg1, string arg2)
{
this.Invoke(new Action(() =>
{
lblErrorMsg.Text = $"从站[{arg1}]出现异常,原因:{arg2}";
}));
}
/// <summary>
/// 变更UI控件状态
/// </summary>
/// <param name="status">True:已连接,False:连接断开</param>
void UIControlStatusChange(bool status)
{
if (status)
{
txtIP.Enabled = false;
txtPort.Enabled = false;
txtSlave.Enabled = false;
btnConnect.BackColor = Color.LightGreen;
btnConnect.Text = "断开连接";
groupBox1.Enabled = true;
}
else
{
txtIP.Enabled = true;
txtPort.Enabled = true;
txtSlave.Enabled = true;
btnConnect.BackColor = Color.Red;
btnConnect.Text = "连接";
groupBox1.Enabled = false;
}
}
private void btnConnect_Click(object sender, EventArgs e)
{
if (btnConnect.Text.Equals("连接"))
{
if (modbus.IsConnected)
{
modbus.Disconnect();
}
IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse(txtIP.Text.Trim()), int.Parse(txtPort.Text.Trim()));
modbus.RemoteEndPoint = endPoint;
modbus.Connect();
}
else
{
if (modbus.IsConnected)
{
modbus.Disconnect();
}
}
UIControlStatusChange(modbus.IsConnected);
}
//读取多个线圈
private void btnReadCoils_Click(object sender, EventArgs e)
{
int address; int len;
if (!int.TryParse(txtCoilsAddress.Text.Trim(), out address))
{
txtCoilsAddress.Focus();
MessageBox.Show("线圈地址错误!");
return;
}
if (!int.TryParse(txtCoilsCount.Text.Trim(), out len))
{
txtCoilsCount.Focus();
MessageBox.Show("线圈数量只能为正整数!");
return;
}
bool[] reuslt = modbus.ReadCoils(slaveStation, address, len);
if (reuslt == null) return;
StringBuilder sb = new StringBuilder();
foreach (var item in reuslt)
{
if (item)
{
sb.Append(" 1");
}
else
{
sb.Append(" 0");
}
}
lblCoilsResult.Text = sb.ToString();
}
private void txtSlave_TextChanged(object sender, EventArgs e)
{
if (!int.TryParse(txtSlave.Text.Trim(), out slaveStation))
{
slaveStation = 10;
}
}
private void btnForced_Click(object sender, EventArgs e)
{
int address;
if (!int.TryParse(txtCoilsWriteAdress.Text.Trim(), out address))
{
txtCoilsWriteAdress.Focus();
MessageBox.Show("线圈写入地址错误!");
return;
}
if (btnForced.Text.Equals("ON"))
{
bool result = modbus.ForcedCoil(slaveStation, address, false);
if (result)
{
btnForced.Text = "OFF";
btnForced.BackColor = Color.Red;
}
}
else
{
bool result = modbus.ForcedCoil(slaveStation, address, true);
if (result)
{
btnForced.Text = "ON";
btnForced.BackColor = Color.LightGreen;
}
}
}
private void btnRegistersRead_Click(object sender, EventArgs e)
{
int address; int len;
if (!int.TryParse(txtRegistersAdress.Text.Trim(), out address))
{
txtRegistersAdress.Focus();
MessageBox.Show("寄存器地址错误!");
return;
}
if (!int.TryParse(txtRegistersCount.Text.Trim(), out len))
{
txtRegistersCount.Focus();
MessageBox.Show("寄存器数量只能为正整数!");
return;
}
short[] result = modbus.ReadHoldingRegisters(slaveStation, address, len);
if (result == null) return;
StringBuilder sb = new StringBuilder();
foreach (var item in result)
{
sb.Append(" " + item.ToString());
}
lblRegistersResult.Text = sb.ToString();
}
private void btnReadFloat_Click(object sender, EventArgs e)
{
int address; int len;
if (!int.TryParse(txtRegistersAdress.Text.Trim(), out address))
{
txtRegistersAdress.Focus();
MessageBox.Show("寄存器地址错误!");
return;
}
if (!int.TryParse(txtRegistersCount.Text.Trim(), out len))
{
txtRegistersCount.Focus();
MessageBox.Show("寄存器数量只能为正整数!");
return;
}
float[] result = modbus.ReadFloatRegisters(slaveStation, address, len);
if (result == null) return;
StringBuilder sb = new StringBuilder();
foreach (var item in result)
{
sb.Append(" " + item.ToString());
}
lblRegistersResult.Text = sb.ToString();
}
private void btnWriteShort_Click(object sender, EventArgs e)
{
int address; short val;
if (!int.TryParse(txtWriteShortAdress.Text.Trim(), out address))
{
txtWriteShortAdress.Focus();
MessageBox.Show("寄存器地址错误!");
return;
}
if (!short.TryParse(txtWriteShortVal.Text.Trim(), out val))
{
txtWriteShortVal.Focus();
MessageBox.Show("写入的值只能为正整数!");
return;
}
if (modbus.WriteSingleRegister(slaveStation, address, val))
{
MessageBox.Show("Short数已成功写入");
}
else
{
MessageBox.Show("Short数写入失败");
}
}
private void btnWriteFloat_Click(object sender, EventArgs e)
{
int address; float val;
if (!int.TryParse(txtFloatWriteAddress.Text.Trim(), out address))
{
txtFloatWriteAddress.Focus();
MessageBox.Show("寄存器地址错误!");
return;
}
if (!float.TryParse(txtWriteFloatVal.Text.Trim(), out val))
{
txtWriteFloatVal.Focus();
MessageBox.Show("写入的值只能为正整数!");
return;
}
if (modbus.WriteFloatRegister(slaveStation, address, val))
{
MessageBox.Show("Float数已成功写入");
}
else
{
MessageBox.Show("Float数写入失败");
}
}
//给线圈批量赋值
private void btnBatch_Click(object sender, EventArgs e)
{
int address; short val;
if (!int.TryParse(txtCoils.Text.Trim(), out address))
{
txtCoils.Focus();
MessageBox.Show("线圈地址错误!");
return;
}
if (!short.TryParse(txtCoilsVal.Text.Trim(), out val))
{
txtCoilsVal.Focus();
MessageBox.Show("写入的值只能为正整数!");
return;
}
bool result = modbus.WriteMultipleCoils(slaveStation, address, 16, new short[] { val });
if (result)
{
MessageBox.Show("线圈已批量写入");
}
else
{
MessageBox.Show("线圈批量失败");
}
}
private void btnMulShortWrite_Click(object sender, EventArgs e)
{
int address; short val;int len;
if (!int.TryParse(txtMulAdress.Text.Trim(), out address))
{
txtMulAdress.Focus();
MessageBox.Show("寄存器地址错误!");
return;
}
if (!short.TryParse(txtMulShortVal.Text.Trim(), out val))
{
txtMulShortVal.Focus();
MessageBox.Show("写入的值只能为正整数!");
return;
}
if (!int.TryParse(txtMulCount.Text.Trim(), out len))
{
txtMulCount.Focus();
MessageBox.Show("寄存器数量只能为正整数!");
return;
}
if (modbus.WriteMultipleRegisters(slaveStation, address, len, Enumerable.Repeat(val, len).ToArray()))
{
MessageBox.Show("寄存器已批量写入");
}
else
{
MessageBox.Show("寄存器批量写入失败");
}
}
}
4.2.4,ModbusTCP部分
public class ModbusTCP : IDisposable
{
#region 说明
//---------------------------------------------------
//前8个字节的规律是一模一样了,都是标识号+modbus号+长度+站号,后面基本是跟地址和长度,或是直接是地址和数据。
//---------------------------------------------------
//功能码0x01:读线圈
//发送的数据--------------------------------------------
//根据协议指定,需要填写长度为12的byte数组
//byte[0] byte[1] byte[2] byte[3] byte[4] byte[5] byte[6] byte[7] byte[8] byte[9] byte[10] byte[11]
//byte[0] byte[1]: 消息号---------随便指定,服务器返回的数据的前两个字和这个一样
//byte[2] byte[3]:modbus标识,强制为0即可
//byte[4] byte[5]:指示排在byte[5] 后面所有字节的个数,也就是总长度-6
//byte[6]:站号,对于TCP协议来说,某些情况不重要,可以随便指定,对于rtu及ascii来说,就需要选择设备的站号信息。
//byte[7]:功能码,这里就需要填入我们的真正的想法了
//byte[8] byte[9]:起始地址,比如我们想读取地址0的数据,就填 00 00 ,如果我们想读取地址1000的数据,怎么办,填入 03 E8 ,也就是将1000转化十六进制填进去。
//byte[10] byte[11]:指定想读取的数据长度,比如我们就想读取地址0的一个数据,这里就写 00 01,如果我们想读取地址0-999共计一个数据的长度,就写 03 E8。和起始地址是一样的。
//有了上面的格式之后,如下
//00 00 00 00 00 06 FF 01 00 00 00 01
//表示:消息号设为0,站号FF,功能码01,地址01,长度01
//响应的数据--------------------------------------------
//00 00 00 00 00 04 FF 01 01 00 共计10个字节的数据。
//byte[0] byte[1] :消息号,发送指令是多少,这里就是多少。
//byte[2] byte[3]:必须都为0,代表这是modbus 通信
//byte[4] byte[5]:指示byte[5] 后面的所有字节数,这里是00 04,如果后面共有100个,那么这里就是 00 64
//byte[6]:站号,发送为FF,那么这里也就是FF
//byte[7]:功能码,发送指令为01的功能码,这里也是01,和发送的指令是一致的
//byte[8]:指示byte[8] 后面跟随的字节数量,因为跟在byte[8] 后面的就是真实的数据,结果就在byte[8] 后面
//byte[9]:真实的数据,一个byte有8位,这里只读取了一个位数据,所有这里的有效值只是byte[9] 的最低位,二进制为 0000 0000 我们看到最低位为0,所以最终我们读取的地址0的线圈为断。
// 功能码0x05:写单个线圈
//如果指定地址0的线圈为通:00 00 00 00 00 06 FF 05 00 00 FF 00
// 前面的含义都是一致的,不同的是: 05 00 00 FF 00
//05 是功能码, 00 00 是指定的地址,如果想地址1000为通,那么就为 03 E8,规定数据 FF 00 线圈通,00 00线圈断,其他任何的值都对结果无效。
//响应数据: 00 00 00 00 00 06 FF 05 00 00 FF 00 因执行写入的操作,是不带读取数据,所以服务器会直接复制一遍你的指令并返回。
//下面再举例一些方便理解(我们只需要指定地址及是否通断的情况即可):
//写入地址100为通: 00 00 00 00 00 06 FF 05 00 64 FF 00
//写入地址1000为断:00 00 00 00 00 06 FF 05 03 E8 00 00
// 功能码0x0F:写入多个线圈
// 00 00 00 00 00 84 FF 0F 00 00 03 E8 7D ...(后面跟125个byte,都是00)
//00 00 :消息标识号,任意数字,服务器回复相同数字。
//00 00 :modbus标志号,规定为00 。
//00 84 :0x0084转化十进制就是132,也就是说,00 84(不包含00 84)后面跟了132个字节
//FF :站号
//0F :功能码
//00 00:起始地址,此处就是0,如果起始地址为100,那么就写00 64,如果起始地址为1000,那么就写03 E8
//03 E8: 我们需要写的数据长度,如果需要写1000个线圈,就是03 E8,如果写999个线圈,那么就是03 E7。
//7D : 这个字节代表后面跟随的真实写入的数据的长度,为125个字节。
//响应的数据:
//00 00 00 00 00 06 FF 0F 00 00 03 E8
// 功能码0x03:读取寄存器的值
//该功能码实现寄存器的数据读取,我们需要知道的是,一个寄存器占2个byte,而且是高位在前,地位在后,那么如果寄存器0的数据为1000,那么我们读取到的数据就是03 E8,这是我们最终想要的东西,03功能码和01功能码很接近,就是功能码替换一下,返回的数据解析不一样而已,比如我们需要读取地址0的寄存器数据:
//00 00 00 00 00 06 FF 03 00 00 00 01
//功能码03,读取寄存器,地址0,长度1 返回如下:
//00 00 00 00 00 05 FF 03 02 03 E8
//02 03 E8:02代表后面的字节长度是2个字节,03 E8是真实的数据了,代表了寄存器0存储了1000这个数据。
#endregion
Socket socketObject = null;
/// <summary>
/// 目标站点号
/// </summary>
int targetStation = 0;
/// <summary>
/// 消息接收到标志位
/// </summary>
bool msgRecFlag = false;
/// <summary>
/// 接收到有效的数据
/// </summary>
byte[] RecBytes = null;
AutoResetEvent slim = new AutoResetEvent(true);
/// <summary>
/// 返回的异常信息
/// </summary>
public event Action<int, string> ErrorMessage;
/// <summary>
/// 消息号
/// </summary>
int MsgId = 0;
CancellationTokenSource source;
public ModbusTCP()
{
}
EndPoint remoteEndPoint;
/// <summary>
/// 远程终结点
/// </summary>
public EndPoint RemoteEndPoint
{
get
{
remoteEndPoint = socketObject.RemoteEndPoint;
return remoteEndPoint;
}
set
{
remoteEndPoint = value;
}
}
/// <summary>
/// 本地终结点
/// </summary>
public EndPoint LocalEndPoint
{
get
{
return socketObject.LocalEndPoint;
}
}
/// <summary>
/// 当前的Socket对象
/// </summary>
public Socket SocketObject
{
get
{
return socketObject;
}
}
bool isConnected = false;
public bool IsConnected
{
get
{
if (socketObject==null || !socketObject.Connected)
{
isConnected = false;
}
return isConnected;
}
}
/// <summary>
/// 使用RemoteEndPoint属性值建立与远程主机的连接
/// </summary>
/// <param name="serialPortName"></param>
public void Connect()
{
if (socketObject != null)
{
Disconnect();
}
socketObject = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socketObject.Connect(remoteEndPoint);
isConnected = socketObject.Connected;
source = new CancellationTokenSource();
if (IsConnected)
{
DataReceivedTask();
}
}
/// <summary>
/// 建立与远程主机的连接
/// </summary>
/// <param name="address">远程主机IP</param>
/// <param name="port">远程主机端口号</param>
public void Connect(string address, int port)
{
if (socketObject != null)
{
Disconnect();
}
socketObject = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socketObject.Connect(IPAddress.Parse(address), port);
isConnected = socketObject.Connected;
source = new CancellationTokenSource();
if (IsConnected)
{
DataReceivedTask();
}
}
/// <summary>
/// 关闭连接
/// </summary>
/// <param name="reuseSocket">如果关闭当前连接后可以重用此套接字,则为 true;否则为 false。</param>
public void Disconnect()
{
if (socketObject != null && IsConnected)
{
socketObject.Shutdown(SocketShutdown.Both);
socketObject.Disconnect(false);
socketObject.Close();
socketObject.Dispose();
socketObject = null;
}
isConnected = false;
source.Cancel();
}
/// <summary>
/// 接收数据任务
/// </summary>
async void DataReceivedTask()
{
try
{
await Task.Run(() =>
{
while (true)
{
if (source.Token.IsCancellationRequested)
{
source.Token.ThrowIfCancellationRequested();
}
// int num = socketObject.Available;
// if (num == 0) return;
byte[] data = new byte[10 * 1024 * 1024];
byte[] bytes;
try
{
int len = socketObject.Receive(data);
if (len == 0)
{
isConnected = false;
return;
}
bytes = new byte[len];
Array.Copy(data, bytes, len);
}
catch (Exception)
{
isConnected = false;
return;
}
//对消息进行解析
//检查报文是否正确
msgRecFlag = true;
//ModbusTCP无校验位
//获取salve Station No
byte stationNo = bytes[6];
targetStation = stationNo;
//功能码
byte commandCode = bytes[7];
if (commandCode > 80)
{
//异常信息
ErrorCode errorCode = (ErrorCode)bytes[8];
ErrorMessage?.BeginInvoke(stationNo, errorCode.ToString(),null,null);
}
else
{
FunctionCode function = (FunctionCode)commandCode;
//有效数据占用的字节数
byte length = bytes[8];
switch (function)
{
case FunctionCode.ReadCoils:
case FunctionCode.ReadDiscreteInputs:
//返回字节长度
RecBytes = new byte[length];
Array.Copy(bytes, 9, RecBytes, 0, length);
break;
case FunctionCode.ReadHoldingRegisters:
case FunctionCode.ReadInputRegisters:
//读取寄存器数据
RecBytes = new byte[length];
Array.Copy(bytes, 9, RecBytes, 0, length);
break;
case FunctionCode.WriteSingleCoil:
case FunctionCode.WriteSingleRegister:
case FunctionCode.WriteMultipleCoils:
case FunctionCode.WriteMultipleRegisters:
RecBytes = new byte[4];
Array.Copy(bytes, 8, RecBytes, 0, RecBytes.Length);
break;
case FunctionCode.ReportSlaveID:
break;
case FunctionCode.MaskWriteRegister:
break;
case FunctionCode.ReadWriteRegister:
break;
default:
break;
}
}
//接收完成,取消主线程阻塞
slim.Set();
}
});
}
catch (OperationCanceledException ex)
{
socketObject.Disconnect(true);
}
catch (Exception ex)
{
ErrorMessage?.Invoke(targetStation, "出现异常,原因:" + ex.Message);
}
}
/// <summary>
///发送报文
/// </summary>
/// <param name="bytes"></param>
/// <param name="protocol">报文协议</param>
void Send(string sendMsg)
{
//对消息进行判断是否合规
if (!Regex.IsMatch(sendMsg, @"^[0-9a-fA-F]+$"))
{
throw new Exception("报文错误,存在非16进制字符");
}
byte[] bytes = new byte[sendMsg.Length / 2];
for (int i = 0; i < sendMsg.Length / 2; i++)
{
bytes[i] = Convert.ToByte(sendMsg.Substring(i * 2, 2), 16);
}
//ModbusTCP不需要添加校验码
// byte[] checkSum = Checkhelper.CrcModbus(bytes, 0, bytes.Length);
// var checkarr = checkSum.Reverse().ToArray();
// byte[] msgBytes = new byte[bytes.Length + checkSum.Length];
byte[] msgBytes = new byte[bytes.Length];
Array.Copy(bytes, msgBytes, bytes.Length);
// Array.Copy(checkarr, 0, msgBytes, bytes.Length, checkarr.Length);
socketObject.Send(msgBytes, 0, msgBytes.Length, SocketFlags.None);
}
/// <summary>
/// 编制报文
/// </summary>
/// <param name="slaveStation">从站站号</param>
/// <param name="function">功能代码</param>
/// <param name="devAdress">起始地址</param>
/// <param name="devLenght">数量</param>
/// <param name="values">写入的值</param>
/// /// <param name="fromBase">如果写入的数值类型所占用的byte,例如short类型为2,int类型为4,flost类型为4,double类型为8</param>
/// <param name="set">强制线圈通断,true:ON ;false:OFF</param>
void ComposeMessage(int slaveStation, FunctionCode function, int devAdress, int devLenght = 1, byte[] values = null, int fromBase = 2, bool set = false)
{
lock (this)
{
slim.Reset();
StringBuilder sb = new StringBuilder();
int afterBytesCount = 0;
sb.Append(devAdress.ToString("X4"));
switch (function)
{
case FunctionCode.ReadCoils:
case FunctionCode.ReadDiscreteInputs:
case FunctionCode.ReadHoldingRegisters:
case FunctionCode.ReadInputRegisters:
//功能码0x01:读线圈
//发送的数据--------------------------------------------
//根据协议指定,需要填写长度为12的byte数组
//byte[0] byte[1] byte[2] byte[3] byte[4] byte[5] byte[6] byte[7] byte[8] byte[9] byte[10] byte[11]
//byte[0] byte[1]: 消息号---------随便指定,服务器返回的数据的前两个字和这个一样
//byte[2] byte[3]:modbus标识,强制为0即可
//byte[4] byte[5]:指示排在byte[5] 后面所有字节的个数,也就是总长度-6
//byte[6]:站号,对于TCP协议来说,某些情况不重要,可以随便指定,对于rtu及ascii来说,就需要选择设备的站号信息。
//byte[7]:功能码,这里就需要填入我们的真正的想法了
//byte[8] byte[9]:起始地址,比如我们想读取地址0的数据,就填 00 00 ,如果我们想读取地址1000的数据,怎么办,填入 03 E8 ,也就是将1000转化十六进制填进去。
//byte[10] byte[11]:指定想读取的数据长度,比如我们就想读取地址0的一个数据,这里就写 00 01,如果我们想读取地址0-999共计一个数据的长度,就写 03 E8。和起始地址是一样的。
//有了上面的格式之后,如下
//00 00 00 00 00 06 FF 01 00 00 00 01
//表示:消息号设为0,站号FF,功能码01,地址01,长度01
afterBytesCount = 6;
sb.Append(devLenght.ToString("X4"));
break;
case FunctionCode.WriteSingleCoil:
// 功能码0x05:写单个线圈
//如果指定地址0的线圈为通:00 00 00 00 00 06 FF 05 00 00 FF 00
// 前面的含义都是一致的,不同的是: 05 00 00 FF 00
//05 是功能码, 00 00 是指定的地址,如果想地址1000为通,那么就为 03 E8,规定数据 FF 00 线圈通,00 00线圈断,其他任何的值都对结果无效。
//响应数据: 00 00 00 00 00 06 FF 05 00 00 FF 00 因执行写入的操作,是不带读取数据,所以服务器会直接复制一遍你的指令并返回。
//下面再举例一些方便理解(我们只需要指定地址及是否通断的情况即可):
//写入地址100为通: 00 00 00 00 00 06 FF 05 00 64 FF 00
//写入地址1000为断:00 00 00 00 00 06 FF 05 03 E8 00 00
afterBytesCount = 6;
if (set)
{
//线圈置ON
sb.Append("FF00");
}
else
{
//线圈置OFF
sb.Append("0000");
}
break;
case FunctionCode.WriteSingleRegister:
//向单个寄存器写入值,占用两个byte
afterBytesCount = 6;
int curVal = BitConverter.ToInt16(values, 0);
sb.Append(curVal.ToString("X4"));
break;
case FunctionCode.WriteMultipleCoils:
// 功能码0x0F:写入多个线圈
// 00 00 00 00 00 84 FF 0F 00 00 03 E8 7D ...(后面跟125个byte,都是00)
//00 00 :消息标识号,任意数字,服务器回复相同数字。
//00 00 :modbus标志号,规定为00 。
//00 84 :0x0084转化十进制就是132,也就是说,00 84(不包含00 84)后面跟了132个字节
//FF :站号
//0F :功能码
//00 00:起始地址,此处就是0,如果起始地址为100,那么就写00 64,如果起始地址为1000,那么就写03 E8
//03 E8: 我们需要写的数据长度,如果需要写1000个线圈,就是03 E8,如果写999个线圈,那么就是03 E7。
//7D : 这个字节代表后面跟随的真实写入的数据的长度,为125个字节。
int len = (int)Math.Ceiling(devLenght * 1.0 / 8);
afterBytesCount = 7 + len;
sb.Append(devLenght.ToString("X4"));
sb.Append(len.ToString("X2"));
foreach (var item in values)
{
sb.Append(item.ToString("X2"));
}
break;
case FunctionCode.WriteMultipleRegisters:
//向多个寄存器写入值
//1,需操作的寄存器数量,数值占用两个byte
afterBytesCount = 7 + values.Length;
//2,寄存器数量
sb.Append((values.Length*2 / fromBase ).ToString("X4"));
//2,需要操作的寄存器占用的字节数,数值占用1个byte
sb.Append((values.Length).ToString("X2"));
//3,写入寄存器的值,根据frombase判定值
//此时姑且以占2个Byte计算
for (int i = 0; i < values.Length; i++)
{
sb.Append(values[i].ToString("X2"));
}
break;
case FunctionCode.ReportSlaveID:
break;
case FunctionCode.MaskWriteRegister:
break;
case FunctionCode.ReadWriteRegister:
break;
default:
break;
}
//---------------------------------------------------
//前8个字节的规律是一模一样了,都是:消息ID+modbus号+长度+站号,后面基本是跟地址和长度,或是直接是地址和数据。
//byte[0] byte[1]: 消息号---------随便指定,服务器返回的数据的前两个字和这个一样
//byte[2] byte[3]:modbus标识,强制为0即可
//byte[4] byte[5]:指示排在byte[5] 后面所有字节的个数,也就是总长度
//byte[6]:站号,对于TCP协议来说,某些情况不重要,可以随便指定,对于rtu及ascii来说,就需要选择设备的站号信息。
//byte[7]:功能码,这里就需要填入我们的真正的想法了
//---------------------------------------------------
//添加从站点,功能代码,起始位置
if (MsgId > 1000) MsgId = 0;
MsgId++;
string msg = $"{MsgId.ToString("X4")}0000{afterBytesCount.ToString("X4")}{ slaveStation.ToString("X2")}{((int)function).ToString("X2")}";
msg += sb;
RecBytes = null;
Send(msg.Trim());
//100ms等待消息回复
msgRecFlag = slim.WaitOne(3000);
System.Threading.Thread.Sleep(20);
if (!msgRecFlag)
{
ErrorMessage?.Invoke(slaveStation, $"从站:{slaveStation},回复超时");
}
}
}
/// <summary>
/// 强制指定单个线圈动作
/// </summary>
/// <param name="slaveStationNo">从站站号</param>
/// <param name="devAdress">线圈地址</param>
/// <param name="onOrOff">True为ON,false为Off</param>
/// <returns></returns>
public bool ForcedCoil(int slaveStationNo, int devAdress, bool set)
{
ComposeMessage(slaveStationNo, FunctionCode.WriteSingleCoil, devAdress, set: set);
if (slaveStationNo == targetStation && msgRecFlag && RecBytes != null)
{
if (RecBytes[2] == 0xff && RecBytes[3] == 0 && set)
{
return true;
}
if (RecBytes[3] == 0 && RecBytes[2] == 0 && !set)
{
return true;
}
}
return false;
}
/// <summary>
/// 读取多个线圈状态
/// </summary>
/// <param name="slaveStationNo"></param>
/// <param name="devAdress"></param>
/// <param name="iLenght"></param>
public bool[] ReadCoils(int slaveStationNo, int devAdress, int iLenght)
{
ComposeMessage(slaveStationNo, FunctionCode.ReadCoils, devAdress, iLenght);
if (slaveStationNo == targetStation && msgRecFlag && RecBytes != null)
{
BitArray array = new BitArray(RecBytes);
if (iLenght > array.Count)
{
throw new Exception("线圈查询结果异常!");
}
bool[] coils = new bool[iLenght];
for (int i = 0; i < iLenght; i++)
{
coils[i] = array[i];
}
return coils;
}
else
{
return null;
}
}
/// <summary>
/// 将short值写入到多个线圈
/// </summary>
/// <param name="slaveStationNo">从站站号</param>
/// <param name="devAdress"></param>
/// <param name="iLenght">地址长度</param>
/// <param name="arr">写入的值</param>
public bool WriteMultipleCoils(int slaveStationNo, int devAdress, int iLenght, short[] arr)
{
if ((int)Math.Ceiling(iLenght * 1.0 / 16) != arr.Length)
{
throw new Exception("线圈数量与所赋值数量不匹配");
}
byte[] bytes = new byte[arr.Length * 2];
int index = 0;
foreach (var item in arr)
{
byte[] temBytes = BitConverter.GetBytes(item);
Array.Copy(temBytes, 0, bytes, index, temBytes.Length);
index += temBytes.Length;
}
ComposeMessage(slaveStationNo, FunctionCode.WriteMultipleCoils, devAdress, iLenght, bytes);
if (slaveStationNo == targetStation && msgRecFlag && RecBytes != null && RecBytes.Length == 4)
{
int startAddress = BitConverter.ToInt16(new byte[] { RecBytes[1], RecBytes[0] }, 0);
int addressLen = BitConverter.ToInt16(new byte[] { RecBytes[3], RecBytes[2] }, 0);
if (startAddress == devAdress && addressLen == iLenght)
{
return true;
}
else
{
return false;
}
}
else
{
return false;
}
}
/// <summary>
/// 读取从站多个寄存器的值
/// </summary>
/// <param name="slaveStationNo"></param>
/// <param name="devAdress">起始寄存器</param>
/// <param name="iLenght">寄存器数量</param>
public short[] ReadHoldingRegisters(int slaveStationNo, int devAdress, int iLenght)
{
ComposeMessage(slaveStationNo, FunctionCode.ReadHoldingRegisters, devAdress, iLenght);
if (slaveStationNo == targetStation && msgRecFlag && RecBytes != null)
{
List<short> shortList = new List<short>();
for (int i = 0; i < RecBytes.Length; i += 2)
{
shortList.Add(BitConverter.ToInt16(new byte[] { RecBytes[i + 1], RecBytes[i] }, 0));
}
return shortList.ToArray();
}
else
{
return null;
}
}
/// <summary>
/// 读取单精度浮点数
/// </summary>
/// <param name="slaveStationNo">从站站点</param>
/// <param name="devAdress">起始地址</param>
/// <param name="iLenght">连续读取浮点数的个数</param>
/// <returns></returns>
public float[] ReadFloatRegisters(int slaveStationNo, int devAdress, int iLenght)
{
ComposeMessage(slaveStationNo, FunctionCode.ReadHoldingRegisters, devAdress, iLenght * 2);
if (slaveStationNo == targetStation && msgRecFlag && RecBytes != null)
{
List<float> floatList = new List<float>();
for (int i = 0; i < RecBytes.Length; i += 4)
{
floatList.Add(BitConverter.ToSingle(new byte[] { RecBytes[i + 1], RecBytes[i], RecBytes[i + 3], RecBytes[i + 2] }, 0));
}
return floatList.ToArray();
}
else
{
return null;
}
}
/// <summary>
/// 向从站单个寄存器写入值
/// </summary>
/// <param name="slaveStationNo"></param>
/// <param name="devAdress"></param>
/// <param name="value"></param>
public bool WriteSingleRegister(int slaveStationNo, int devAdress, short value)
{
ComposeMessage(slaveStationNo, FunctionCode.WriteSingleRegister, devAdress, values: BitConverter.GetBytes(value));
if (slaveStationNo == targetStation && msgRecFlag && RecBytes != null && RecBytes.Length == 4)
{
int startAddress = BitConverter.ToInt16(new byte[] { RecBytes[1], RecBytes[0] }, 0);
int val = BitConverter.ToInt16(new byte[] { RecBytes[3], RecBytes[2] }, 0);
if (startAddress == devAdress && val == value)
{
return true;
}
else
{
return false;
}
}
else
{
return false;
}
}
/// <summary>
/// 写入单精度浮点数
/// </summary>
/// <param name="slaveStationNo">从站站点</param>
/// <param name="devAdress">寄存器地址</param>
/// <param name="value">值</param>
/// <returns>true写入成功,false写入失败</returns>
public bool WriteFloatRegister(int slaveStationNo, int devAdress, float value)
{
//进行拆解组合
byte[] bytes = BitConverter.GetBytes(value);
byte[] valBytes= new byte[] { bytes[1], bytes[0], bytes[ 3], bytes[2] };
ComposeMessage(slaveStationNo, FunctionCode.WriteMultipleRegisters, devAdress, values: valBytes, fromBase: 4);
if (slaveStationNo == targetStation && msgRecFlag && RecBytes != null)
{
short adress = BitConverter.ToInt16(new byte[] { RecBytes[1], RecBytes[0] }, 0);
short registerCount = BitConverter.ToInt16(new byte[] { RecBytes[3], RecBytes[2] }, 0);
if (adress == devAdress && registerCount == 2)
{
return true;
}
else
{
return false;
}
}
else
{
return false;
}
}
/// <summary>
/// 向从站多个寄存器写入值
/// </summary>
/// <param name="slaveStationNo"></param>
/// <param name="devAdress"></param>
/// <param name="iLenght"></param>
/// <param name="arr"></param>
public bool WriteMultipleRegisters(int slaveStationNo, int devAdress, int iLenght, short[] arr)
{
if (iLenght != arr.Length)
{
throw new Exception("寄存器数量与所赋值数量不匹配");
}
byte[] bytes = new byte[arr.Length * 2];
int index = 0;
foreach (var item in arr)
{
byte[] temBytes = BitConverter.GetBytes(item);
byte[] temBytes2 = new byte[] { temBytes[1], temBytes[0] };
Array.Copy(temBytes2, 0, bytes, index, temBytes2.Length);
index += temBytes.Length;
}
ComposeMessage(slaveStationNo, FunctionCode.WriteMultipleRegisters, devAdress, iLenght, bytes,fromBase:4);
if (slaveStationNo == targetStation && msgRecFlag && RecBytes != null && RecBytes.Length == 4)
{
int startAddress = BitConverter.ToInt16(new byte[] { RecBytes[1], RecBytes[0] }, 0);
int addressLen = BitConverter.ToInt16(new byte[] { RecBytes[3], RecBytes[2] }, 0);
if (startAddress == devAdress && addressLen == iLenght)
{
return true;
}
else
{
return false;
}
}
else
{
return false;
}
}
public void Dispose()
{
socketObject.Close();
socketObject.Dispose();
}
}
5,基于OPC服务的Modbus通信
5.1,OPC介绍
1,OPC协议
OPC是Object Linking and Embedding(OLE)for Process Controls的缩写,是微软公司得对象连接和嵌入技术在过程控制方面的应用。OPC主要基于微软的OLE(现在的Active X),COM(部件对象模型)和DCOM(分布式部件对象模型)技术。
2,常见的OPCServer
1,KEPWSRE 2,SIMATIC NET 3,PC ACCESS(PC ACCESS SMART) 4,Matrikon,Knight公司得OPC软件。
3,OPC读写方式。
3.1,同步方式
客户端发送请求,必须等待服务器响应全部完成后才能返回,期间处于等待状态。
多客户端向服务器操作时,客户端程序产生阻塞。
同步通讯适用于客户端较少,数据量较少的场合。
3.2,异步方式。
客户端发送请求后立即返回,不需要等待服务器的响应,可以进行其他操作。
服务器完成响应后再通知客户端。
相对于同步通讯,异步通讯的效率更高。
3.3,订阅模式。
客户端发送请求后立即返回,不需要等待服务器的响应,可以进行其他操作。
服务器有数据发生变化时,自动刷新客户端数据。
客户端只向服务端发送一次请求。
3.4,OPC存在同步写和异步写两种模式。
4,OPC访问接口方式。
OPC主要包含两种接口:Custom标准接口和OLE自动化标准接口。
自定义接口是一组COM接口,主要用于C++语言的应用程序开发。
自动化接口是一组OLE接口,主要用于VB,DELPHI,Excel等基本脚本语言的应用程序开发。
OPC基金会提供了OPCRcw动态链接库,OPC NET COM 和 OPC NET API,将复杂的OPC规范封装为简单的C#类,从而可以较为简单的实现数据访问。
5,OPCClient开发
使用OPCNetApi2.0,需要使用OPCNetAPI.dll。该dll由OPC Foundation提供,需付费注册会员。
使用自动化接口,需要使用OPCDAAuto.dll.不需付费注册,比较简单,但是不够灵活。
使用自定义接口,需要用到多个.Net Wrapper:OpcRcw.Ae.dll,OpcRcw.Bath.dll,OpcRcw.sec.dll,OpcRcw.Da.dll等
5.2,OPCDAAuto.dll链接
https://download.csdn.net/download/lingxiao16888/89517377
5.3,配置OPC服务软件KEPServerEx V4.0
创建Modbus通信频道
选择设备驱动为modbus serial
通信参数设置
创建Tag
点击OPCQuickClient 测试通信,通信OK。
5.4,代码
public partial class Form1 : Form
{
OPCHelper opc = new OPCHelper();
List<OPCItem> opcList = new List<OPCItem>();
public Form1()
{
InitializeComponent();
dataGridView1.AutoGenerateColumns = false;
btnRefresh_Click(null, null);
opc.AsyncWriteComplete += Opc_AsyncWriteComplete;
}
private void Opc_AsyncWriteComplete(List<OPCItem> result)
{
dataGridView1.DataSource = null;
dataGridView1.DataSource = result;
}
private void btnRefresh_Click(object sender, EventArgs e)
{
comboNode.Items.Clear();
//获取HostName
foreach (var item in Dns.GetHostAddresses(Environment.MachineName))
{
if (comboNode.Items.Contains(Dns.GetHostEntry(item).HostName))
continue;
comboNode.Items.Add(Dns.GetHostEntry(item).HostName);
}
//如果存在则将第一项设置为当前项
comboNode.SelectedIndex = comboNode.Items.Count > 0 ? 0 : -1;
}
private void comboNode_SelectedIndexChanged(object sender, EventArgs e)
{
if (comboNode.SelectedItem != null)
{
//获取所有可用的OPC服务
var servers = OPCHelper.GetOPCServers(comboNode.Text);
comboServer.DataSource = servers;
}
}
private void comboServer_SelectedIndexChanged(object sender, EventArgs e)
{
}
private void btnConnect_Click(object sender, EventArgs e)
{
if (btnConnect.Text.Equals("Connect", StringComparison.CurrentCultureIgnoreCase))
{
if (comboServer.SelectedItem != null)
{
opcList.Clear();
opc.Connected(comboServer.Text.Trim());
//获取OPC上所有注册的tag
if (opc.IsConnected)
{
var tags = opc.GetOPCServerTags();
lsbBrowser.DataSource = tags;
btnConnect.Text = "Disconnect";
btnConnect.BackColor = Color.LightGreen;
}
}
else
{
MessageBox.Show("请先选择需要连接OPC服务!");
}
}
else
{
opc.Disconnect();
if (!opc.IsConnected)
{
opcList.Clear();
lsbBrowser.DataSource = null;
dataGridView1.DataSource = null;
btnConnect.Text = "Connect";
btnConnect.BackColor = Color.Red;
}
}
}
private void btnRead_Click(object sender, EventArgs e)
{
if(dataGridView1.Rows.Count==0)
{
MessageBox.Show("请先添加需要读取的寄存器!");
return;
}
if (opc.IsConnected)
{
List<string> tags = opcList.Select(item => item.Tag).ToList();
var list= opc.ReadMultipleRegisters<float>(tags,ckAsync.Checked);
if (list != null)
{
StringBuilder sb = new StringBuilder();
foreach (var item in list)
{
sb.AppendFormat("{0} ", item);
}
MessageBox.Show(sb.ToString());
}
}
else
{
MessageBox.Show("未连接服务!");
return;
}
}
private void lsbBrowser_DoubleClick(object sender, EventArgs e)
{
if (lsbBrowser.SelectedItem != null)
{
string tag = lsbBrowser.SelectedItem.ToString();
if (!opcList.Any(item => item.Tag.Equals(tag)))
{
opcList.Add(new OPCItem { Tag = tag });
}
dataGridView1.DataSource = null;
dataGridView1.DataSource = opcList;
}
}
private void dataGridView1_CellDoubleClick(object sender, DataGridViewCellEventArgs e)
{
if (dataGridView1.CurrentRow != null)
{
//获取当前值
if (opc.IsConnected)
{
DataGridViewRow row = dataGridView1.CurrentRow;
string tag = row.Cells["Tag"].Value.ToString();
if (row.Cells["Value"].Value == null)
{
btnRead_Click(null, null);
}
string val = row.Cells["Value"].Value?.ToString();
WriteFrm frm = new WriteFrm(tag, val);
frm.StartPosition = FormStartPosition.CenterScreen;
frm.ModifyCompleted += Frm_ModifyCompleted;
frm.ShowDialog();
}
}
}
private void Frm_ModifyCompleted(string tag, float val, bool async)
{
if (opc.IsConnected)
{
opc.WriteSingleValue<float>(tag, val, async);
btnRead_Click(null, null);
}
}
}
//定义一个委托
public delegate void AsyncReadCompleteEventHandler(List<OPCItem> result);
/// <summary>
/// 基于OPC协议(对象连接与嵌入流程控制)的读写类
/// </summary>
public class OPCHelper
{
//--------------使用指南-----------------------
//第1步,使用静态方法获取本机或者局域网的hostName或者IpAdress所拥有的OPS服务
// public static string[] GetOPCServers(string hostNameorAdress)
//第2步,使用实例化方法连接OPS服务
// public bool Connected(string serverName)
//第3步,在连接的基础上使用实例化方法获取对应OPC服务器上所声明定义的所有可用寄存器
// public string[] GetOPCServerTags()
//第4步,在连接的基础上使用实例化方法对寄存器进行读写
// public void WriteMultipleRegisters(List<string> tags, List<object> values, bool async = true)
// public List<T> ReadMultipleRegisters<T>(List<string> tags, bool async = true)
//第5步,如果使用的是异步读取,请在异步读取完成事件中接收值。
// public event AsyncReadCompleteEventHandler AsyncWriteComplete;
//***********************************************************************************************
//***********************************************************************************************
//--------------特别注意事项----------------------
//第1点:
//OPC中数组Array有效的下标是从1开始,至于下标为0的值需要填充同类型的任意值,int类型一般采用0,string类型一般为"0"
//同样,下标1开始的才是有效的值才可以进行取值操作。
//如果按照C#中数组从下表0开始赋值取值操作,将抛出异常。
//
//第2点
//进行异步或者同步写入时
//OPC.SyncWrite(int NumItems, ref Array ServerHandles, ref Array Values, out Array Errors);
//参数Values必须是Object类型
//----------------------------------------------
//定义OPC相关对象
OPCServer kepServer = new OPCServer();
OPCGroups kepGroups;
OPCGroup kepGroup;
OPCBrowser kepBrowser;
Dictionary<int, string> currentItemId = new Dictionary<int, string>();
//定义OPC标签错误
Array iErrors;
bool isConnected;
/// <summary>
/// 服务句柄
/// </summary>
Array ServerHandles;
int cancelId;
/// <summary>
/// 异步读取完成事件
/// </summary>
public event AsyncReadCompleteEventHandler AsyncWriteComplete;
/// <summary>
/// OPC是否连接
/// </summary>
public bool IsConnected
{
get
{
if (kepGroup == null)
{
isConnected = false;
}
return isConnected;
}
}
/// <summary>
/// 当前OPCOPCGroup服务,可用于寄存器的读取与写入
/// </summary>
public OPCGroup CurrentOPCGroup
{
get
{
return kepGroup;
}
}
/// <summary>
/// 根据hostName或者Adress获取可用的OPC服务
/// </summary>
/// <param name="hostNameorAdress"></param>
/// <returns>返回可用的服务列表</returns>
public static List<string> GetOPCServers(string hostNameorAdress)
{
//获取hostName
string hostName = Dns.GetHostEntry(hostNameorAdress).HostName;
//注意:这里须使用Object接收值,若使用Var,后续进行遍历时将报异常
OPCServer opcServer = new OPCServer();
object result = opcServer.GetOPCServers(hostName);
List<string> list = new List<string>();
foreach (var item in (Array)result)
{
list.Add(item.ToString());
}
return list;
}
/// <summary>
/// 连接OPC服务
/// </summary>
/// <param name="serverName">OPC服务名</param>
/// <returns></returns>
public bool Connected(string serverName)
{
try
{
kepServer.Connect(serverName);
isConnected = true;
kepServer.OPCGroups.RemoveAll();
kepGroups = kepServer.OPCGroups;
kepGroups.DefaultGroupDeadband = 0;
kepGroup = kepGroups.Add("Test");
kepGroup.IsActive = true;
kepGroup.IsSubscribed = true;
kepGroup.UpdateRate = 250;
//监视数据变化事件获取值
// kepGroup.DataChange += KepGroup_DataChange;
kepGroup.AsyncReadComplete += KepGroup_AsyncReadComplete;
kepGroup.AsyncWriteComplete += KepGroup_AsyncWriteComplete;
return true;
}
catch (Exception)
{
return false;
}
}
private void KepGroup_AsyncWriteComplete(int TransactionID, int NumItems, ref Array ClientHandles, ref Array Errors)
{
// throw new NotImplementedException();
}
private void KepGroup_AsyncReadComplete(int TransactionID, int NumItems, ref Array ClientHandles, ref Array ItemValues, ref Array Qualities, ref Array TimeStamps, ref Array Errors)
{
List<OPCItem> opcItemList = new List<OPCItem>();
//获取值
for (int i = 0; i < NumItems; i++)
{
//kepOPC有效所有从1开始
int clientId = Convert.ToInt32(ClientHandles.GetValue(i + 1));
string tag = null;
if (currentItemId.Keys.Contains(clientId))
{
tag = currentItemId[clientId];
}
object val = ItemValues.GetValue(i + 1);
//TimeStamps:是标准时间即格林时间是0时区计时,所以这里需要+8
DateTime dt = Convert.ToDateTime(TimeStamps.GetValue(i + 1)).AddHours(8);
opcItemList.Add(new OPCItem { Tag = tag, Value = val.ToString(), Time = dt });
}
AsyncWriteComplete?.Invoke(opcItemList);
}
/// <summary>
/// 获取OPC服务中所有已定义可用的Tag即定义的寄存器名
/// </summary>
/// <returns></returns>
public string[] GetOPCServerTags()
{
if (!IsConnected)
{
throw new Exception("OPC服务未连接!");
}
kepBrowser = kepServer.CreateBrowser();
kepBrowser.ShowBranches();
kepBrowser.ShowLeafs(true);
List<string> list = new List<string>();
foreach (var item in kepBrowser)
{
list.Add(item.ToString());
}
return list.ToArray();
}
/// <summary>
/// 向OPC的多个寄存器写入多个值
/// </summary>
/// <param name="tags">OPC服务上定义的寄存器名</param>
/// <param name="values">写入的值的集合</param>
/// <param name="async">是否异步写入</param>
public void WriteMultipleRegisters(List<string> tags, List<object> values, bool async = true)
{
if (!IsConnected)
{
throw new Exception("OPC未连接,写入失败!");
}
if (kepGroup.OPCItems.Count > 0)
{
//移除项目
kepGroup.OPCItems.Remove(kepGroup.OPCItems.Count, ref ServerHandles, out iErrors);
}
var itemIds = new List<string>(tags);
//OPC Array中有效值是从下标1开始,所以需对下标0填充任意值,这里填充"0"
itemIds.Insert(0, "0");
Array ItemIds = itemIds.ToArray();
List<int> clientList = new List<int>();
//OPC Array中有效值是从下标1开始,所以需对下标0填充任意值,这里填充 0
clientList.Add(0);
for (int i = 0; i < tags.Count; i++)
{
clientList.Add(i + 20);
}
Array ClientHandles = clientList.ToArray();
kepGroup.OPCItems.AddItems(tags.Count, ref ItemIds, ref ClientHandles, out ServerHandles, out iErrors);
//------------------特别注意----------------
//写入的值的类型只能是Object类型,如果是其他类型将抛出异常
List<object> writeValues = new List<object>(values);
writeValues.Insert(0,default(object));
Array valueArray = writeValues.ToArray();
if (async)
{
kepGroup.AsyncWrite(1, ref ServerHandles, valueArray, out iErrors, 0, out cancelId);
}
else
{
kepGroup.SyncWrite(1, ref ServerHandles, ref valueArray, out iErrors);
}
}
/// <summary>
/// 从OPC中读取多个寄存器的值,当选择异步模式时返回null,真实的值通过AsyncReadComplete事件获取
/// </summary>
/// <param name="tags">OPC服务上定义的寄存器名</param>
/// <param name="async">是否异步读取</param>
/// <returns>异步读取时返回null</returns>
public List<T> ReadMultipleRegisters<T>(List<string> tags, bool async = true)
{
if (!IsConnected)
{
throw new Exception("OPC未连接,读取失败!");
}
if (kepGroup.OPCItems.Count > 0)
{
//移除项目
kepGroup.OPCItems.Remove(kepGroup.OPCItems.Count, ref ServerHandles, out iErrors);
}
var itemIds = new List<string>(tags);
//OPC Array中有效值是从下标1开始,所以需对下标0填充任意值,这里填充"0"
itemIds.Insert(0, "0");
Array ItemIds = itemIds.ToArray();
List<int> clientList = new List<int>();
//OPC Array中有效值是从下标1开始,所以需对下标0填充任意值,这里填充 0
clientList.Add(0);
for (int i = 0; i < tags.Count; i++)
{
clientList.Add(i + 20);
}
Array ClientHandles = clientList.ToArray();
kepGroup.OPCItems.AddItems(tags.Count, ref ItemIds, ref ClientHandles, out ServerHandles, out iErrors);
//获取服务句柄,并写入到字典中
currentItemId.Clear();
for (int i = 0; i < ItemIds.Length - 1; i++)
{
string tag = ItemIds.GetValue(i + 1).ToString();
int id = Convert.ToInt32(ClientHandles.GetValue(i + 1));
currentItemId.Add(id, tag);
}
if (async)
{
kepGroup.AsyncRead(ItemIds.Length - 1, ref ServerHandles, out iErrors, 1, out cancelId);
return null;
}
else
{
Array result;
object qualities;
object tiemstamps;
kepGroup.SyncRead(2, ItemIds.Length - 1, ref ServerHandles, out result, out iErrors, out qualities, out tiemstamps);
List<T> list = new List<T>();
for (int i = 0; i < result.Length; i++)
{
list.Add((T)result.GetValue(i + 1));
}
return list;
}
}
/// <summary>
/// 写入单个值
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="tag">寄存器在OPC上的tag名</param>
/// <param name="value">写入的值</param>
/// <param name="async">是否异步</param>
public void WriteSingleValue<T>(string tag, T value, bool async = true)
{
WriteMultipleRegisters(new List<string> { tag }, new List<object> { value }, async);
}
/// <summary>
/// 读取单个值,当选择异步模式时返回null,真实的值通过AsyncReadComplete事件获取
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="tag">寄存器在OPC上的tag名</param>
/// <param name="async">是否异步,异步时返回null,具体的值通过AsyncReadComplete事件获取</param>
public List<T> ReadSingleValue<T>(string tag, bool async = true)
{
return ReadMultipleRegisters<T>(new List<string> { tag }, async);
}
/// <summary>
/// 断开OPC连接
/// </summary>
/// <returns></returns>
public bool Disconnect()
{
try
{
if (kepGroup == null)
{
isConnected = false;
return false;
}
kepServer.Disconnect();
isConnected = false;
return true;
}
catch (Exception)
{
return false;
}
}
}
public class OPCItem
{
public string Tag { get; set; }
public string Value { get; set; }
public DateTime Time { get; set; }
}