在破土而出的那刻,小草就做好了拼尽全力成长的准备,加油,永远热情向前! |
本文主要介绍了Modbus读取输出线圈的状01H功能码的询问报文和响应报文的格式以及含义,也通过编码实现读取线圈状态示例,希望对你理解Modbus如何读取线圈状态有点帮助。欢迎点赞 + 关注,第一时间了解更多Modbus信息。 |
【2】知识点概括:
【1】掌握Modbus串口通信,读取输出线圈状态 01H功能码 主站询问报文和从站应答报文内容含义;
【2】使用winform 设计简易的UI,使用modbus slave工具模拟Modbus通讯;
【3】ModbusRTU类:串口对象MyCom,打开关闭串口方法,拼接报文,发送和接收报文,解析报文及异常处理;
【4】ByteArray类:字节数组类初始化,方法,目的是为了拼接报文方便;
【5】UI窗体:combox,button以及listview控件,窗体内容初始化,读取方法;
【6】其他:CRC校验算法,混合锁;
【3】原理介绍(BYI):
读取输出线圈 功能码:01H
主站询问报文格式:
从站地址 | 功能码 | 起始地址(高位) | 起始地址(低位) | 线圈数量(高位) | 线圈数量(低位) | CRC |
---|---|---|---|---|---|---|
0x11 | 0x01 | 0x00 | 0x13 | 0x00 | 0x1B | XXXX |
主站询问报文格式:从站地址+功能码+起始地址(高位)+起始地址(低位)+线圈数量(高位)+线圈数量(低位)+CRC
从站应答报文格式:
从站地址 | 功能码 | 字节计数 | 线圈状态20-27 | 线圈状态28-35 | 线圈状态36-43 | 线圈状态44-46 | CRC |
---|---|---|---|---|---|---|---|
0x11 | 0x01 | 0x04 | 0xCD | 0x6B | 0xB2 | 0x05 | XXXX |
从站应答报文格式:从站地址+功能码+字节计数+线圈状态(根据实际确定数量)+CRC校验
【4】应用场景介绍:
我们是通过模拟的方式进行的modbus串行链路上的通讯测试
实现的是线圈输出状态的读取和获得:
【5】具体示例(代码,图片,注释及应用实践注意事项)
//创建winform的窗体,涉及下拉框combox和button按钮,listview和imagelist的使用;
//【1】类库DAL中的类===>>>封装:
【1-1】ByteArray类:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DAL
{
//这是一组具有相同特征的类或类结构的集合;
public class ByteArray
{
//这个字节数组类,我们主要是实现对获取字节数组的各种构造方法
List<byte> listByte = null;
//初始化字节数组集合
public ByteArray()
{
listByte = new List<byte>();
}
//添加单个字节
public void Add(byte item)
{
listByte.Add(item);
}
//添加数组
public void Add(byte[] item)
{
listByte.AddRange(item);
}
//清空
public void Clear()
{
listByte = new List<byte>();
}
//获取数组
public byte[] array
{
get { return listByte.ToArray(); }
}
}
}
【1-2】Common类:在公共类中我们定义了一个DataFormat数据顺序格式的枚举和一个混合锁;锁的作用:防止我们同步进行读写操作时会产生冲突;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace DAL
{
//枚举类型
public enum DataFormat
{
ABCD,
BADC,
CDAB,
DCBA
}
#region 简单的混合锁
/// <summary>
/// 一个简单的混合线程同步锁,采用了基元用户加基元内核同步构造实现
/// 我们使用锁:就是解决读写同步的矛盾问题
/// </summary>
public sealed class SimpleHybirdLock : IDisposable
{
#region IDisposable Support
private bool disposedValue = false; // 要检测冗余调用
void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
// TODO: 释放托管状态(托管对象)。
}
// TODO: 释放未托管的资源(未托管的对象)并在以下内容中替代终结器。
// TODO: 将大型字段设置为 null。
m_waiterLock.Close();
disposedValue = true;
}
}
// TODO: 仅当以上 Dispose(bool disposing) 拥有用于释放未托管资源的代码时才替代终结器。
// ~SimpleHybirdLock() {
// // 请勿更改此代码。将清理代码放入以上 Dispose(bool disposing) 中。
// Dispose(false);
// }
// 添加此代码以正确实现可处置模式。
/// <summary>
/// 释放资源
/// </summary>
public void Dispose()
{
// 请勿更改此代码。将清理代码放入以上 Dispose(bool disposing) 中。
Dispose(true);
// TODO: 如果在以上内容中替代了终结器,则取消注释以下行。
// GC.SuppressFinalize(this);
}
#endregion
/// <summary>
/// 基元用户模式构造同步锁
/// </summary>
private Int32 m_waiters = 0;
/// <summary>
/// 基元内核模式构造同步锁
/// </summary>
private AutoResetEvent m_waiterLock = new AutoResetEvent(false);
/// <summary>
/// 获取锁
/// </summary>
public void Enter()
{
if (Interlocked.Increment(ref m_waiters) == 1) return;//用户锁可以使用的时候,直接返回,第一次调用时发生
//当发生锁竞争时,使用内核同步构造锁
m_waiterLock.WaitOne();
}
/// <summary>
/// 离开锁
/// </summary>
public void Leave()
{
if (Interlocked.Decrement(ref m_waiters) == 0) return;//没有可用的锁的时候
m_waiterLock.Set();
}
/// <summary>
/// 获取当前锁是否在等待当中
/// </summary>
public bool IsWaitting => m_waiters != 0;
}
#endregion
}
//【1-3】ModbusRTU类:这是ModbusRTU的类,我们通过new的方式可以创建ModbusRTU的对象;符合职责明确元素,对象的属性,方法应该单独放置在对象的类中;UI中如需要使用,创建对象进行调用即可。
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace DAL
{
public class ModbusRTU
{
# region 01:对象或者属性
//建立连接和断开连接
//读取输出线圈,读取输入线圈.....
//定义串口通信的对象
private SerialPort MyCom = new SerialPort();
//创建通信超时属性
private int ReadTimeOut { get; set; } = 2000;
public int WriteTimeOut { get; set; } = 2000;
//读取返回报文超时时间
public int RecTimeOut { get; set; } = 2000;
//睡眠时间
public int SleepTime { get; set; } = 20; //这个睡眠时间,20ms很重要,原来写的是2000ms,这样就会造成超时报错!
//字节顺序
public DataFormat DataFormat { get; set; } = DataFormat.ABCD;
//创建一个互斥锁对象===>>>>后面我们发送报文的时候会使用这个互斥锁===>>>
private SimpleHybirdLock InteractiveLock = new SimpleHybirdLock();
#endregion
#region 02:打开关闭串口
/// <summary>
/// 打开串口
/// </summary>
/// <param name="iBaudRate"></param>
/// <param name="iPortName"></param>
/// <param name="iDataBits"></param>
/// <param name="iParity"></param>
/// <param name="iStopBits"></param>
public void OpenMyCom(int iBaudRate,string iPortName,int iDataBits,Parity iParity,StopBits iStopBits)
{
if (MyCom.IsOpen)
{
MyCom.Close();
}
//串口通信对象实例化
MyCom = new SerialPort(iPortName,iBaudRate,iParity,iDataBits,iStopBits);
//设置超时时间
MyCom.ReadTimeout = this.ReadTimeOut;
MyCom.WriteTimeout = this.WriteTimeOut;
//打开串口
MyCom.Open();
}
/// <summary>
/// 关闭串口
/// </summary>
public void CloseMyCom()
{
if (MyCom.IsOpen)
{
MyCom.Close();
}
}
#endregion
#region 03:读取输出线圈 功能码01H
public byte[] ReadOutputStatus(int iDevAddress, int iniAddress, int iReadLength)
{
//第一步:拼接报文
ByteArray SendCommand = new ByteArray();
SendCommand.Add(new byte[] { (byte)iDevAddress, 0x01, (byte)(iniAddress / 256), (byte)(iniAddress % 256) });
SendCommand.Add(new byte[] { (byte)(iReadLength / 256), (byte)(iReadLength % 256) });
SendCommand.Add(Crc16(SendCommand.array, 6));//【注意】:CRC校验,是将前面的6个字节的东西拿过来校验
//第二步:发送和接收报文
int sendByteLength = 0;
if (iReadLength % 8 == 0)
{
sendByteLength = iReadLength / 8;
}
else
{
sendByteLength = iReadLength / 8 + 1;
}
byte[] responseByteArray = new byte[5 + sendByteLength];
if (SendData(SendCommand.array, ref responseByteArray))
{
//第三步:解析报文: //验证:功能码+字节计数
if (responseByteArray[1]==0x01 && responseByteArray[2]==sendByteLength)
{
return GetByteArray(responseByteArray, 3, sendByteLength);
}
else
{
return null;
}
}
else
{
return null;
}
}
#endregion
#region 通用发送报文并接受
/// <summary>
/// 通用发送报文并接收
/// </summary>
/// <param name="sendByte"></param>
/// <param name="responseByteArray"></param>
/// <returns></returns>
public bool SendData(byte[] sendByte ,ref byte[] responseByteArray)
{
InteractiveLock.Enter();
try
{
//发送报文
MyCom.Write(sendByte, 0, sendByte.Length);
//定义一个Buffer
byte[] buffer = new byte[1024];
//定义一个内存
MemoryStream ms = new MemoryStream();
//定义读取开始时间:
DateTime start = DateTime.Now;
//【1】获取当前缓冲区的值,判断是否有值,如果有值,读取过来,放到内存中,接着再判断,如果当前缓冲区的值为0,说明读完了。
//【2】如果每次读取都读不到,我们用超时时间来做判断;
while (true)
{
Thread.Sleep(SleepTime);
if (MyCom.BytesToRead >= 1)
{
int spcount = MyCom.Read(buffer, 0, buffer.Length);
ms.Write(buffer, 0, spcount);
}
else
{
//判断是否超时
if ((DateTime.Now - start).TotalMilliseconds > this.RecTimeOut)
{
ms.Dispose();
return false;
}
else if (ms.Length > 0)
{
break;
}
}
}
responseByteArray = ms.ToArray();
ms.Dispose();
return true;
}
catch (Exception)
{
return false;
}
finally
{
//解锁
InteractiveLock.Leave();
}
}
#endregion
#region 自定义截取字节数组
/// <summary>
/// 自定义截取字节数组
/// </summary>
/// <param name="dest">目标字节数组</param>
/// <param name="offset">偏移量</param>
/// <param name="count">数量</param>
/// <returns></returns>
private byte[] GetByteArray(byte[] dest,int offset,int count)
{
byte[] res = new byte[count];
if (dest != null && dest.Length >= offset + count)
{
for (int i = 0; i < count; i++)
{
res[i] = dest[offset + i];
}
return res;
}
else
{
return null;
}
}
#endregion
#region CRC算法校验
private static readonly byte[] aucCRCHi = {
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
0x00, 0xC1, 0x81, 0x40
};
private static readonly byte[] aucCRCLo = {
0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7,
0x05, 0xC5, 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E,
0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09, 0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9,
0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC,
0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3,
0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32,
0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4, 0x3C, 0xFC, 0xFD, 0x3D,
0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A, 0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38,
0x28, 0xE8, 0xE9, 0x29, 0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF,
0x2D, 0xED, 0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26,
0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60, 0x61, 0xA1,
0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4,
0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F, 0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB,
0x69, 0xA9, 0xA8, 0x68, 0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA,
0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5,
0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0,
0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97,
0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C, 0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E,
0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98, 0x88, 0x48, 0x49, 0x89,
0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C,
0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83,
0x41, 0x81, 0x80, 0x40
};
private byte[] Crc16(byte[] pucFrame, int usLen)
{
int i = 0;
byte[] res = new byte[2] { 0xFF, 0xFF };
UInt16 iIndex = 0x0000;
while (usLen-- > 0)
{
iIndex = (UInt16)(res[0] ^ pucFrame[i++]);
res[0] = (byte)(res[1] ^ aucCRCHi[iIndex]);
res[1] = aucCRCLo[iIndex];
}
return res;
}
#endregion
}
}
//【2】、窗体代码:
using DAL;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace Modbus01Practice
{
public enum StoreArea
{
输出线圈0x,
输入状态1x,
输出寄存器4x,
输入寄存器3x
}
public enum VarType
{
Bit,
Short,
UShort,
Int,
UInt,
Float
}
public partial class FrmModbusRTU : Form
{
public FrmModbusRTU()
{
InitializeComponent();
this.Load += FrmodbusRTU_Load;
}
//添加引用后才能创建:
private ModbusRTU objModbus = new ModbusRTU();
//系统当前时间
private string CurrentTime
{
get { return DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"); }
}
//创建连接标志位
private bool IsConnected = false;
#region Load初始化
private void FrmodbusRTU_Load(object sender, EventArgs e)
{
//初始化控件
lst_Info.Columns[1].Width = lst_Info.ClientSize.Width - lst_Info.Columns[0].Width;
//端口号
string[] PortList = SerialPort.GetPortNames();
if (PortList.Length > 0)
{
this.cmb_Port.Items.AddRange(PortList);
this.cmb_Port.SelectedIndex = 0;
}
//波特率
this.cmb_Paud.DataSource = new string[] { "2400", "4800", "9600", "19200", "38400" };
this.cmb_Paud.SelectedIndex = 2;
//校验位
this.cmb_Parity.DataSource = Enum.GetNames(typeof(Parity));
//停止位
this.cmb_StopBits.DataSource = Enum.GetNames(typeof(StopBits));
this.cmb_StopBits.SelectedIndex = 1;
//数据格式
this.cmb_DataFormat.DataSource = Enum.GetNames(typeof(DataFormat));
//存储区
this.cmb_StoreArea.DataSource = Enum.GetNames(typeof(StoreArea));
//数据类型==初始化,进行绑定
this.cmb_VarType.DataSource = Enum.GetNames(typeof(VarType));
}
#endregion
#region 建立连接和断开连接
private void btn_Connect_Click(object sender, EventArgs e)
{
if (IsConnected)
{
AddLog(1, "ModbusRTU已经连接,请勿重复连接");
return;
}
//如果没有连接,则建立连接
try
{
objModbus.OpenMyCom(int.Parse(this.cmb_Paud.Text.Trim()), this.cmb_Port.Text.Trim(), int.Parse(this.txt_DataBits.Text.Trim()), (Parity)(Enum.Parse(typeof(Parity), this.cmb_Parity.SelectedItem.ToString(), false)), (StopBits)(Enum.Parse(typeof(StopBits), this.cmb_StopBits.SelectedItem.ToString(), false)));
}
catch (Exception ex)
{
IsConnected = false;
AddLog(1,"ModbusRTU连接失败"+ex.Message);
return;
}
IsConnected = true;
AddLog(0,"ModbusRTU连接成功");
}
private void btn_DisConn_Click(object sender, EventArgs e)
{
objModbus.CloseMyCom();
IsConnected = false;
AddLog(0,"断开连接ModbusRTU");
}
#endregion
#region 通用读取方法
private void btn_Read_Click(object sender, EventArgs e)
{
if (!IsConnected)
{
AddLog(1,"请先检查与Modbus从站之间的连接");
return;
}
//UI读写测试中,我们对应UI中的属性
ushort DevAdd = 0;
ushort Address = 0;
ushort Length = 0;
if (!ushort.TryParse(this.txt_SlaveAdd.Text.Trim(),out DevAdd))
{
AddLog(1,"写入失败,从站地址必须为正整数");
return;
}
if (!ushort.TryParse(this.txt_Variable.Text.Trim(), out Address))
{
AddLog(1, "写入失败,起始地址必须为正整数");
return;
}
if (!ushort.TryParse(this.txt_Length.Text.Trim(), out Length))
{
AddLog(1, "写入失败,读取长度必须为正整数");
return;
}
//获取选择的变量类型
ValueType varType = (VarType)(Enum.Parse(typeof(VarType),this.cmb_VarType.SelectedItem.ToString(),false));
//获取选择的存储区
StoreArea storeArea = (StoreArea)(Enum.Parse(typeof(StoreArea),this.cmb_StoreArea.SelectedItem.ToString(),false));
//创建字节数组
byte[] result = null;
switch (varType)
{
case VarType.Bit:
switch (storeArea)
{
case StoreArea.输出线圈0x:
result = objModbus.ReadOutputStatus(DevAdd, Address, Length);
break;
case StoreArea.输入状态1x:
case StoreArea.输出寄存器4x:
case StoreArea.输入寄存器3x:
AddLog(1,"读取失败,存储区类型不正确");
return;
}
string binarystring = string.Empty;
if (result != null)
{
foreach (var item in result)
{
char[] c = Convert.ToString(item, 2).PadLeft(8, '0').ToCharArray();
Array.Reverse(c);
binarystring += new string(c);
}
AddLog(0, "读取成功,结果为:"+binarystring.Substring(0,Length));
}
else
{
AddLog(1,"读取失败,请检查地址、类型或者连接状态");
}
break;
default:
break;
}
}
#endregion
#region 写入日志的通用方法
/// <summary>
/// 写入日志的通用方法
/// </summary>
/// <param name="type">我们日志的类型</param>
/// <param name="info">日志的具体信息</param>
private void AddLog(int type, string info)
{
ListViewItem lst = new ListViewItem(" " + CurrentTime, type);
lst.SubItems.Add(info);
lst_Info.Items.Insert(0, lst);
}
#endregion
#region 修改字节顺序
//数据格式下拉框的选择时改变事件方法:
private void cmb_DataFormat_SelectedIndexChanged(object sender, EventArgs e)
{
if (objModbus != null)
{
objModbus.DataFormat = (DataFormat)(Enum.Parse(typeof(DataFormat), this.cmb_DataFormat.SelectedItem.ToString(), false));
}
}
#endregion
}
}
//【3】连接读取输出线圈的结果:
【6】总结
(1)原理:清楚modbus主站询问报文格式和从站应答报文格式内容含义很重要;
(2)职责明确:公共类,ModbusRTU类,ByteArray类,通用方法,UI各司其职,需要时就调用,保证了逻辑清晰,可扩展;
(3)对象的应用:SerialPort串口对象及属性、方法,ModbusRTU对象的成员等是如何运用;
(4)零碎知识点:读写冲突加锁,CRC校验,load初始化,超时验证,数据格式大小端等知识点;
作者:小羊,软件开发/技术写作者,欢迎关注,我们一起探讨有趣的编程历程。 |