Modbus 数据帧业务
有常用功能码和添加了32bit寄存器读写的功能
//#define d等待应答 //下位机执行动作后再应答时间
//#define DEBUG //调试模式
//#define Debug3S //数据帧节拍调试(每帧等3秒)
using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace help
{
//1申明委托》委托命令
public delegate void WT_GET_数据(string cmd);//ui显示发送命令
public delegate void WT_SET_数据(byte[] cmd);//其他能实现发送功能的对象
/// <summary>
/// modbus业务:配置数据帧
/// </summary>
public class Help_ModBus
{
#region 字段
Help_SerialPort comx = null;//硬件串口RS485
byte Address = 0x00;//默认00广播
#endregion
#region 属性
/// <summary>
/// 3.5个字符 1000/9600x(3.5x11=38.5)约 4.01ms + 下位机响应时间 20ms
/// </summary>
public int delay_帧头等待 { get; set; } // 帧发送前延时
public int delay_帧尾等待 { get; set; } //下位机应答时间
public int delay_下位机执行时间 { get; set; } = 70;//70ms //下位机应答时间
public Help_SerialPort help_SerialPort { get => comx; set => comx = value; }
public bool WT_是否使用委托 { get; set; } = false;//有硬件串口就不需要委托
//2定义委托对象(3绑定方法)
public event WT_GET_数据 wt_get数据;//委托=》呼叫上ui层 让上层显示:发送命令
public event WT_SET_数据 wt_set数据;//委托=》呼叫上ui层 让上层:发送命令
/// <summary>
/// 避免get数据帧过短
/// </summary>
public int set_最短帧字节 { get => comx.set_每帧字节; set => comx.set_每帧字节 = value; }
//设置站号
public byte address { get => Address; set => Address = value; }
#endregion
#region 构造
/// <summary>
/// 业务:数据帧
/// </summary>
public Help_ModBus()//默认构造
{
comx = new Help_SerialPort();//硬件串口RS485
set_最短帧字节 = 8;//最短字节8(大于7触发接收事件)
}
//public Help_ModBus(SerialPort com)//默认构造
//{
// comx = new Help_SerialPort(com);
// set_最短帧字节 = 8;//最短字节8(大于7触发接收事件)
//}
private void showdata(string obj)
{
//throw new NotImplementedException();
}
#endregion
#region 自定义方法
//public void set_address(byte add)
//{
// Address = add;
//}
//延时
private void delay(int i)//大于模块应答速度ms
{
Thread.Sleep(i);
}
/// <summary>
/// 数据帧发送
/// </summary>
/// <param name="stringcmd">配置报文</param>
private string TX发送(string stringcmd)
{
string str = "help_Modbus文件:TX准备状态,数据clear";
#if (DEBUG1)
if (stringcmd == "7F06016E0BB8E4B7")
{
//检查故障帧位置 (打断点)
}
#endif
#region 帧头延时
#endregion
byte[] buffer = TxCMD(stringcmd);//准备报文
comx.TXrs485_发送命令(buffer, stringcmd);//下达命令,并委托ui显示
#region 帧尾延时
//帧间隔3.5字符: 1除以波特率,乘以38.5
//delay((int)(uint)(1000.0 / comx.serialPort.BaudRate * 38.5));//每帧间隔3.5字符
delay(delay_帧尾等待);//10ms 伺服手册规定
//============================
//下位机的执行时间
#if (d等待应答)
delay(delay_下位机执行时间);//大于模块响应速度ms
#endif
#endregion
//=======3 收尾===========================================
str = comx.get_应答报文.ToString();//最后一帧数据
#if(Debug3S)//数据帧节拍调试
Thread.Sleep(3000);
#endif
return str;
}
//==read======================================
#region Read读取: 0x01读出线圈 0x02读开关量 0x03读寄存器 0x04模拟量采集
/// <summary>
/// 01: 读输出线圈
/// 功能码:01读输出的单线圈数量
/// 01 01 0000 0018 3C00
/// 表示01站 ,01功能码,第0000点开始 ,0018表示读24个线圈,3C00表示CRC校验 可以用串口助手16进制发送看效果
/// </summary>
/// <param name="head">开始位置</param>
/// <param name="num">数量</param>
public string Read_io_out(UInt16 head, UInt16 num)//第n个线圈开始,线圈数量
{
string addr = Address.ToString("X2");//从站 01
string cmd = addr + "01"//功能码 01 //读输出线圈
+ head.ToString("X4")//偏移量 0000
+ num.ToString("X4");//线圈数量 0018
string stringcmd = StringCMD(cmd);//带CRC的完整命令
if (WT_是否使用委托)
{
wt_get数据(stringcmd);//显示发送命令
wt_set数据(TxCMD(stringcmd));//发送命令
}
else
{
string rx = TX发送(stringcmd);//下位机应答数据
return rx;
}
return "help_Modbus.Read_io_out?";
}
/// <summary>
/// 02: 读输入开关量
/// 功能码:02 读输入开关量
/// 01 02 00 00 00 14 78 05
/// 表示01站 ,02功能码,第0000点开始 ,0014表示读20个开关,7805表示CRC校验 可以用串口助手16进制发送看效果
/// </summary>
/// <param name="head">开始位置</param>
/// <param name="num">数量</param>
public string Read_io_in(UInt16 head, UInt16 num)//第n个开关开始,开关数量
{
string addr = Address.ToString("X2");//从站 01
string cmd = addr + "02"//功能码 02 //读输入开关
+ head.ToString("X4")//偏移量 0000
+ num.ToString("X4");//开关数量 0014
string stringcmd = StringCMD(cmd);//带CRC的完整命令
if (WT_是否使用委托)
{
wt_get数据(stringcmd);//显示发送命令
wt_set数据(TxCMD(stringcmd));//发送命令
}
else
{
string rx = TX发送(stringcmd);//下位机应答数据
return rx;
}
return "help_Modbus.Read_io_in?";
}
/// <summary>
/// 03: 读寄存器
/// 功能码:03 读寄存器
/// 01 03 00 00 00 1D 85 C3
/// 表示01站 ,03功能码,第0000个寄存器开始 ,001D表示读29个寄存器,85C3表示CRC校验
/// </summary>
/// <param name="head">开始位置</param>
/// <param name="num">数量</param>
public string Read_Reg(UInt16 head, UInt16 num)//第n个保持寄存器开始,寄存器数量(n*16bit)
{//num=2;会返回32bit,因为1个寄存器是16bit
string addr = Address.ToString("X2");//从站 01
string cmd = addr + "03"//功能码 03 // 读寄存器
+ head.ToString("X4")//偏移量 0000
+ num.ToString("X4");//寄存器数量 001D
string stringcmd = StringCMD(cmd);//带CRC的完整命令
if (WT_是否使用委托)
{
wt_get数据(stringcmd);//显示发送命令
wt_set数据(TxCMD(stringcmd));//发送命令
}
else
{
string rx = TX发送(stringcmd);//下位机应答数据
return rx;
}
return "help_Modbus.Read_Reg?";
}
/// <summary>
/// 04: 读采集寄存器
/// 功能码:04 读采集寄存器 AD模块
/// 01 04 00 13 00 01 C0 0F
/// 0013表示第19个寄存器开始,0001表示读取1个寄存器值
/// </summary>
/// <param name="head">开始位置</param>
/// <param name="num">数量</param>
public string Read_Reg_ADC(UInt16 head, UInt16 num)
{
string addr = Address.ToString("X2");//从站 01
string cmd = addr + "04"//功能码 04 //模拟量采集值
+ head.ToString("X4")//偏移量 0013
+ num.ToString("X4");//寄存器数量 0001
string stringcmd = StringCMD(cmd);//带CRC的完整命令
if (WT_是否使用委托)
{
wt_get数据(stringcmd);//显示发送命令
wt_set数据(TxCMD(stringcmd));//发送命令
}
else
{
string rx = TX发送(stringcmd);//下位机应答数据
return rx;
}
return "help_Modbus.Read_Reg_ADC?";
}
#endregion
//==writer======================================
#region Set写入 0x05单线圈 0x06单寄存器 0x0F出线圈 0x10批量
/// <summary>
/// 功能码:05 设置输出线圈
/// 01 05 00 01 FF 00 DD FA
/// 0001表示D1线圈,FF00表示打开(0000表示关闭)
/// 物理位置+1
/// </summary>
/// <param name="io">位置</param>
public string Set_io(UInt16 io)
{
string addr = Address.ToString("X2");//从站地址:1字节
string cmd = addr //从站编号
+ "05" // 05 设置输出线圈
+ io.ToString("X4") // 0001 线圈编号
+ "FF00";//打开 //开关设置 : FF00 开 0000 关
string stringcmd = StringCMD(cmd);//带CRC的完整命令
//4呼叫委托
if (WT_是否使用委托)
{
wt_get数据(stringcmd);//显示发送命令
wt_set数据(TxCMD(stringcmd));//发送命令
}
else
{
string rx = TX发送(stringcmd);//下位机应答数据
return rx;
}
return "help_Modbus.Set_io?";
}
/// <summary>
/// 05: 设置输出线圈
/// 01 05 00 01 00 00 9C 0A //关闭D1线圈
/// </summary>
/// <param name="io">位置</param>
public string Rest_io(UInt16 io)
{
string addr = Address.ToString("X2");
string cmd = addr //从站编号
+ "05" // 05 设置输出线圈
+ io.ToString("X4") // 0001 线圈编号
+ "0000";//关闭 //开关设置 : FF00 开 0000 关
string stringcmd = StringCMD(cmd);//带CRC的完整命令
if (WT_是否使用委托)
{
wt_get数据(stringcmd);//显示发送命令
wt_set数据(TxCMD(stringcmd));//发送命令
}
else
{
string rx = TX发送(stringcmd);//下位机应答数据
return rx;
}
return "help_Modbus.Rest_io?";
}
/// <summary>
/// 06: 设置单个寄存器值
/// 只能设置1个,且bit16
/// 01 06 00 01 AB CD 66 AF
/// 设置0001寄存器值是0xABCD
/// </summary>
/// <param name="reg">位置</param>
/// <param name="bit16">bit16值</param>
public string Write_Reg16Bit(UInt16 reg, UInt16 bit16)//默认16bit//写寄存器(寄存器号,数值)
{//06码只能设置16bit值
string addr = Address.ToString("X2");//限制字符串宽度
string cmd = addr //从站编号 "01"
+ "06" // 06 设置单个寄存器
+ reg.ToString("X4") // 0001 寄存器编号
+ bit16.ToString("X4"); // ABCD 数据
string stringcmd = StringCMD(cmd);//带CRC的完整命令
if (WT_是否使用委托)
{
wt_get数据(stringcmd);//显示发送命令
wt_set数据(TxCMD(stringcmd));//发送命令
}
else
{
string rx = TX发送(stringcmd);//下位机应答数据
return rx;
}
return "help_Modbus.Write_Reg16Bit?";
}
/// <summary>
/// 寄存器写32bit值
/// 利用0x10功能码扩展的方法
/// </summary>
/// <param name="reg">位置</param>
/// <param name="data">bit32值</param>
public string Write_Reg32bit(UInt16 reg, Int32 data)
{//06功能码只能写16bit, 所以用10功能码写32bit
//Convert.
byte[] buffer = new byte[4];
buffer[0] = (byte)(data >> 8); // 0H 第1个寄存器的高字节
buffer[1] = (byte)(data & 0xff); // 0L 第1个寄存器的低字节
buffer[2] = (byte)(data >> 24); // 1H 第2个寄存器的高字节
buffer[3] = (byte)(data >> 16); // 1L 第2个寄存器的低字节
//=====//写32bit寄存器==============================
string start = Write_Regs(reg, 2, buffer);//写2个16bit //一帧数据
return start;//最后一帧数据
}
/// <summary>
/// 功能码:0F 写多个线圈
/// 写io(起始位,线圈数量,设定值)
/// 01 0F 0000 0004 01 F0 3E D2
/// 第0000个线圈开始,0004表示只修改4个线圈,01表示发送1个字节 F0表示设定值 3ED2表示CRC校验码。
/// </summary>
/// <param name="head">开始位置</param>
/// <param name="num">数量</param>
/// <param name="data">参数</param>
public string Write_io(UInt16 head, UInt16 num, byte[] data)
{
string addr = Address.ToString("X2");
string cmd = addr + "0F"
+ head.ToString("X4")//偏移量
+ num.ToString("X4")//线圈数量
+ data.Length.ToString("X2")//数据长度
+ BytesToHexStrings(data);//数据值
string stringcmd = StringCMD(cmd);//带CRC的完整命令
if (WT_是否使用委托)
{
wt_get数据(stringcmd);//显示发送命令
wt_set数据(TxCMD(stringcmd));//发送命令
}
else
{
string rx = TX发送(stringcmd);//下位机应答数据
return rx;
}
return "help_Modbus.Write_io?";
}
/// <summary>
/// 功能码:10 写多个寄存器
/// 写寄存器(寄存器号,值)
/// 01 10 0005 0001 02 00 03 E6 04
/// 从 0005寄存器开始(断网保护寄存器)0001表示只修改一个寄存器,02表示要发送2字节 0003表示2字节的值,这样断网超过3秒线圈就复位。
/// 01 10 0005 0001 02 07 08 A5 F3
/// 0005寄存器断网保护超过1800秒就失效。0708表示1800秒。
/// </summary>
/// <param name="head">开始位置</param>
/// <param name="num">寄存器数量</param>
/// <param name="data">参数</param>
public string Write_Regs(UInt16 head, UInt16 num, byte[] data)// 寄存器号,寄存器数量,数值
{
string addr = Address.ToString("X2");//从站 01
string cmd = addr + "10"//功能码 10 // 写多个寄存器
+ head.ToString("X4")//偏移量 0005 //寄存器地址
+ num.ToString("X4")//寄存器数量 0001 // 寄存器数量 // 2为 32bit
+ data.Length.ToString("X2")//包字节 02 //字节数量
+ BytesToHexStrings(data);//数据值 // 0H 0L 1H 1L 2H 2L 3H 3L。。。。
string stringcmd = StringCMD(cmd);//带CRC的完整命令
if (WT_是否使用委托)
{
wt_get数据(stringcmd);//显示发送命令
wt_set数据(TxCMD(stringcmd));//发送命令
}
else
{
string rx = TX发送(stringcmd);//下位机应答数据
return rx;
}
return "help_Modbus.Write_Regs?";
}
#endregion
#region 文本处理
#region 字符串转等效数组
/// <summary>
/// 字符串转等效数组
/// </summary>
/// <param name="data">原料</param>
/// <returns>成品</returns>
public byte[] StringsToHexBytes(string data)//change转换,string等效的byte[]
{
//清除所有空格
string str = data.Replace(" ", "");
//若字符个数为奇数,补一个0
str += str.Length % 2 != 0 ? "0" : "";
byte[] bytes = new byte[str.Length / 2];//用于返回结果
for (int i = 0, c = bytes.Length; i < c; i++)//获取bytes值
{
bytes[i] = Convert.ToByte(str.Substring(i * 2, 2), 16);
}
return bytes;
}
#endregion
#region 数组转等效字符串
/// <summary>
/// 数组转等效字符串
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
public string BytesToHexStrings(byte[] data)//change转换 , byte[ ]等效的string
{
string str = string.Empty;
for (int i = 0; i < data.Length; i++)
{
str += data[i].ToString("X2");
}
return str;
}
#endregion
#region 数组计算CRC
/// <summary>
/// 数组计算CRC
/// </summary>
/// <param name="bytes">原料</param>
/// <param name="b">false小端</param>
/// <returns></returns>
public byte[] getCRC16(byte[] bytes, bool b)//数组,端向,默认flase小端
{
int len = bytes.Length;
if (len > 0)
{
ushort crc = 0xFFFF;
for (int i = 0; i < len; i++)
{
crc = (ushort)(crc ^ (bytes[i]));
for (int j = 0; j < 8; j++)
{
crc = (crc & 1) != 0 ? (ushort)((crc >> 1) ^ 0xA001) : (ushort)(crc >> 1);
}
}
byte hi = (byte)((crc & 0xFF00) >> 8); //高位置
byte lo = (byte)(crc & 0x00FF); //低位置
if (b)//端向
{
return new byte[] { hi, lo };//大端
}
else
{//默认小端
return new byte[] { lo, hi, };//小端
}
}
return new byte[] { 0, 0 };
}
/// <summary>
/// 字符串计算CRC
/// </summary>
/// <param name="cmd">字符串</param>
/// <returns>CRC</returns>
public string getCRC16(string cmd)
{
return BytesToHexStrings(getCRC16(StringsToHexBytes(cmd), false));//CRC
}
#endregion
#region StringCMD
/// <summary>
/// 完整的命令
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
public string StringCMD(string data)//转换 带CRC的命令(用于ui查看)
{
//return data + BytesToHexStrings(getCRC16(StringsToHexBytes(data), false));//命令+CRC
return data + getCRC16(data); // 命令 + CRC
}
#endregion
#region TxCMD
/// <summary>
/// 发送的数据帧
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
public byte[] TxCMD(string data)//change转换 串口发送命令
{
data.Replace(" ", "");//去除空白
byte[] buffer = StringsToHexBytes(data);
return buffer;//TX的
}
#endregion
#endregion
#endregion
#region 委托
#endregion
}
}