目录
- [C#实现欧姆龙 HostLink 通讯协议库](#实现欧姆龙 HostLink 通讯协议库)
- 背景介绍
- [欧姆龙 PLC 内存区域介绍](#欧姆龙 PLC 内存区域介绍)
 - [欧姆龙 PLC 数据类型对应](#欧姆龙 PLC 数据类型对应)
 - [欧姆龙 PLC 与 PC 的 RS232 接线线序](#欧姆龙 PLC 与 PC 的 RS232 接线线序)
 
 - HostLink通讯报文分析
 - HostLink通讯协议库的C#实现
 - C#控制台测试功能
 
 - 背景介绍
 
C#实现欧姆龙 HostLink 通讯协议库
运行环境:VS2022 .net framework4.8
通讯库项目地址(Gitee):通讯库项目Gitee 仓库
控制台测试项目地址(Gitee):控制台测试项目Gitee 仓库
HostLink 通讯手册链接(蓝奏云):SYSMAC Series Communications Commands
官方的 HostLink 串口通讯示例(蓝奏云):C-Mode || FINS-Mode
通讯工具(蓝奏云):Commix 1.4
概要 :根据欧姆龙的 HostLink 通讯协议手册内容,使用串口 实现了 PC 与 PLC 的通讯,能够通过C-Mode 与FINS-Mode 两种模式实现 PC 读写 PLC 的CIO、WR、HR、DM 四个内存区 的内容(同步/异步方法 ),而且可以以较高的通讯效率获取所需数据、错误代码等信息,最后用一个 C#控制台项目测试了通讯库功能
背景介绍
HostLink 协议是欧姆龙 PLC 与主机通讯的一种公开协议,PC 可通过 HostLink 命令对 PLC 的运行状态、I/O 点位的读写
HostLink 分为 C-Mode 和 FINS-Mode 两种模式
C-Mode 命令是专门的主机链路通信命令,它们由主机发出并发送至 CPU 单元。可连接用于串行通信的设备有 CPU 单元、串行通信单元和串行通信板。
FINS-Mode 命令是报文服务通信命令,它们不依赖于特定的传输路径。它们可用于各种网络(控制器链路、以太网等)和串行通信(主机链路)。它们可以从 CPU 单元、特殊 I/O 单元或主机发出,也可以发送到其中任何一个单元。可发送的具体命令取决于目的地。

欧姆龙 PLC 内存区域介绍
| 内存区域名 | 区域说明 | 
|---|---|
| CIO | I/O 继电器区 | 
| WR | 内部辅助继电器区 | 
| HR | 保持继电器区 | 
| DM | 数据存储区 | 
| TIM | 定时器区 | 
| CNT | 计数器区 | 
| IR | 变址寄存器区 | 
| DR | 数据寄存器 | 
| AR | 特殊辅助继电器区 | 
| TR | 暂存区 | 
| TK | 状态标志、时钟脉冲、任务标志 | 
欧姆龙 PLC 数据类型对应
| PLC 数据类型 | PC 数据类型 | 
|---|---|
| Bit | bool | 
| Byte | ushort | 
| DWord | uint | 
| Int | short | 
| Dint | int | 
| float | float | 
| String | string | 
欧姆龙 PLC 与 PC 的 RS232 接线线序

HostLink通讯报文分析
根据C-Mode和FINS的报文进行分析
C-Mode通讯报文分析
发送报文如下图所示

正常接收报文如下图所示

通讯出错接收报文如下图所示

使用串口工具Commix进行测试如下图所示
TS命令:

RD、WD命令:

FINS-Mode通讯报文分析
发送报文如下图所示

接收报文如下图所示

FINS指令配置如下图所示

使用串口工具Commix进行测试如下图所示
0101、0102命令:

HostLink通讯协议库的C#实现
核心实现(FCS校验码生成、串口收发)
HostLinkCore.cs
在这里我本来想给异步的方法加上一个SemaphoreSlim 来限制通讯的信号量的,但是后来想了想还是让使用者在外面自己加好了:)
SemaphoreSlim具体的使用方法可以参考文章的C#控制台项目
            
            
              CSharp
              
              
            
          
          /// <summary>
/// 通过命令帧计算FCS异或校验码并加上结束符
/// </summary>
/// <param name="CommandFrame">命令帧</param>
/// <returns>4位字节数组,包含FCS校验码与结束符</returns>
public static List<byte> HostLinkEndCode(List<byte> CommandFrame)
{
    try
    {
        List<byte> EndCode = new List<byte>();
        short FCSNum = HostLinkFCS(CommandFrame);
        string HexString = FCSNum.ToString("X2");
        EndCode.AddRange(Encoding.ASCII.GetBytes(HexString));
        EndCode.AddRange(new List<byte> { 0x2A, 0x0D });
        return EndCode;
    }
    catch (Exception)
    {
        throw;
    }
}
/// <summary>
/// 对字节数组进行异或运算得到数值
/// </summary>
/// <param name="CommandFrame">命令帧</param>
/// <returns>异或运算结果(short类型)</returns>
public static short HostLinkFCS(List<byte> CommandFrame)
{
    try
    {
        short CheckNum = 0;
        foreach (byte FrameNum in CommandFrame)
        {
            CheckNum ^= FrameNum;
        }
        return CheckNum;
    }
    catch (Exception)
    {
        throw;
    }
}
/// <summary>
/// 检查回复帧是否完整
/// </summary>
/// <param name="frame">回复帧</param>
/// <returns>完整结尾:返回True;不完整:返回False</returns>
private static bool CheckResponseFrame(List<byte> frame)
{
    if (frame.Count > 0 && frame[frame.Count - 1] == (byte)0x0D)
    {
        return true;
    }
    else
    {
        return false;
    }
}
/// <summary>
/// HostLink报文发送与接收代码
/// </summary>
/// <param name="serialPort">串口实例</param>
/// <param name="CommandFrame">命令帧</param>
/// <returns>回复帧</returns>
public static List<byte> HostLinkCommCore(SerialPort serialPort, List<byte> CommandFrame)
{
    try
    {
        List<byte> ResponseFrame = new List<byte>();
        //发送报文
        serialPort.Write(CommandFrame.ToArray(), 0, CommandFrame.Count);
        //循环读取数据
        while (serialPort.IsOpen)
        {
            if (serialPort.BytesToRead > 0)
            {
                byte[] buffer = new byte[serialPort.BytesToRead];
                serialPort.Read(buffer, 0, buffer.Length);
                ResponseFrame.AddRange(buffer);
                if (CheckResponseFrame(ResponseFrame))
                {
                    return ResponseFrame;
                }
            }
        }
        return null;
    }
    catch (Exception)
    {
        throw;
    }
}
/// <summary>
/// HostLink报文发送与接收代码(异步方法)
/// </summary>
/// <param name="serialPort">串口实例</param>
/// <param name="CommandFrame">命令帧</param>
/// <returns>回复帧</returns>
public static async Task<List<byte>> HostLinkCommCoreAsync(SerialPort serialPort, List<byte> CommandFrame)
{
    try
    {
        List<byte> ResponseFrame = new List<byte>();
        //发送报文
        await serialPort.BaseStream.WriteAsync(CommandFrame.ToArray(), 0, CommandFrame.Count);
        //循环读取数据
        while (serialPort.IsOpen)
        {
            if (serialPort.BytesToRead > 0)
            {
                byte[] buffer = new byte[serialPort.BytesToRead];
                await serialPort.BaseStream.ReadAsync(buffer, 0, buffer.Length);
                ResponseFrame.AddRange(buffer);
                if (CheckResponseFrame(ResponseFrame))
                {
                    return ResponseFrame;
                }
            }
        }
        return null;
    }
    catch (Exception)
    {
        throw;
    }
}
        C-Mode实现
CmodeEndCode.cs
            
            
              CSharp
              
              
            
          
          /// <summary>
/// 对比回复帧中的EndCode,看内容是否相符
/// </summary>
/// <param name="ResponseFrame">回复帧</param>
/// <returns>返回结束代码</returns>
public static string CatchEndCode(List<byte> ResponseFrame, int EndCodeAdr)
{
    try
    {
        List<byte> ResponseEndCode = ResponseFrame.GetRange(EndCodeAdr, 2);
        string EndCodeContents = null;
        if (ResponseEndCode.SequenceEqual(new List<byte> { 0x30, 0x30 }))
        {
            EndCodeContents = "00";
        }
        else
        {
            foreach (var EndCodeMap in EndCodeMapSeq)
            {
                if (ResponseEndCode.SequenceEqual(EndCodeMap.Key))
                {
                    EndCodeContents = EndCodeMap.Value;
                    break;
                }
            }
        }
        return EndCodeContents;
    }
    catch (Exception ex)
    {
        throw ex;
    }
}
private static readonly Dictionary<List<byte>, string> EndCodeMapSeq = new Dictionary<List<byte>, string>
{
    {new List<byte> { 0x30, 0x31 },"EndCode: 01;  Contents: Not executable in RUN mode" },
    {new List<byte> { 0x30, 0x32 },"EndCode: 02;  Contents: Not executable in MONITOR mode" },
    {new List<byte> { 0x30, 0x33 },"EndCode: 03;  Contents: UM write-protected" },
    {new List<byte> { 0x30, 0x34 },"EndCode: 04;  Contents: Address over" },
    {new List<byte> { 0x30, 0x42 },"EndCode: 0B;  Contents: Not executable in PROGRAM mode" },
    {new List<byte> { 0x31, 0x33 },"EndCode: 13;  Contents: FCS error" },
    {new List<byte> { 0x31, 0x34 },"EndCode: 14;  Contents: Format error" },
    {new List<byte> { 0x31, 0x35 },"EndCode: 15;  Contents: Entry number data error" },
    {new List<byte> { 0x31, 0x36 },"EndCode: 16;  Contents: Command not supported" },
    {new List<byte> { 0x31, 0x38 },"EndCode: 18;  Contents: Frame length error" },
    {new List<byte> { 0x31, 0x39 },"EndCode: 19;  Contents: Not executable" },
    {new List<byte> { 0x32, 0x30 },"EndCode: 20;  Contents: Could not create I/O table" },
    {new List<byte> { 0x32, 0x31 },"EndCode: 21;  Contents: Not executable due to CPU Unit CPU error" },
    {new List<byte> { 0x32, 0x33 },"EndCode: 23;  Contents: User memory protected" },
    {new List<byte> { 0x41, 0x33 },"EndCode: A3;  Contents: Aborted due to FCS error in trans-mission data" },
    {new List<byte> { 0x41, 0x34 },"EndCode: A4;  Contents: Aborted due to format error in transmission data" },
    {new List<byte> { 0x41, 0x35 },"EndCode: A5;  Contents: Aborted due to entry number data error in transmission data" },
    {new List<byte> { 0x41, 0x38 },"EndCode: A8;  Contents: Aborted due to frame length error in transmission data" }
};
        CmodeHeaderCode.cs
            
            
              CSharp
              
              
            
          
          internal class CmodeHeaderCode
{
    public static readonly byte[] HeaderCode_RR = { 0x52, 0x52 };
    public static readonly byte[] HeaderCode_RD = { 0x52, 0x44 };
    public static readonly byte[] HeaderCode_WR = { 0x57, 0x52 };
    public static readonly byte[] HeaderCode_WD = { 0x57, 0x44 };
    public static readonly byte[] HeaderCode_TS = { 0x54, 0x53 };
    public static readonly byte[] HeaderCode_MS = { 0x4D, 0x53 };
    public static readonly byte[] HeaderCode_SC = { 0x53, 0x43 };
}
        功能实现的代码过长,请自行到通讯库项目Gitee 仓库查看吧
FINS-Mode实现
定义一个FinsResult的类用来接收所需要的信息
(目前通信库反馈的大部分数据都是Bool与String列表,所以里面分开成两个Datas)
FinsResult.cs
            
            
              CSharp
              
              
            
          
          /// <summary>
/// Fins通信结果类
/// </summary>
public class FinsResult
{
    /// <summary>
    /// FINS通信状态
    /// </summary>
    public bool IsSuccessed { get; set; }
    /// <summary>
    /// FINS通信结果信息
    /// </summary>
    public string ResultMessage { get; set; }
    /// <summary>
    /// 通信命令帧
    /// </summary>
    public string CommandFrame { get; set; }
    /// <summary>
    /// 通信回复帧
    /// </summary>
    public string ResponseFrame { get; set; }
    /// <summary>
    /// 数据列表(bool类型)
    /// </summary>
    public List<bool> Datas_BoolList { get; set; }
    /// <summary>
    /// 数据列表(string类型)
    /// </summary>
    public string Datas_String { get; set; }
    /// <summary>
    /// 完整构造函数
    /// </summary>
    /// <param name="isSuccessed">FINS通信状态</param>
    /// <param name="resultMessage">FINS通信结果信息</param>
    /// <param name="commandFrame">通信命令帧</param>
    /// <param name="responseFrame">通信回复帧</param>
    /// <param name="datas">数据列表</param>
    public FinsResult(bool isSuccessed, string resultMessage, string commandFrame, string responseFrame, List<bool> datas1, string datas2)
    {
        IsSuccessed = isSuccessed;
        ResultMessage = resultMessage;
        CommandFrame = commandFrame;
        ResponseFrame = responseFrame;
        Datas_BoolList = datas1;
        Datas_String = datas2;
    }
    //五参数构造函数(带bool数据列表)
    public FinsResult(bool isSuccessed, string resultMessage, string commandFrame, string responseFrame, List<bool> datas) :
        this(isSuccessed, resultMessage, commandFrame, responseFrame, datas, null)
    { }
    //五参数构造函数(带string数据列表)
    public FinsResult(bool isSuccessed, string resultMessage, string commandFrame, string responseFrame, string datas) :
        this(isSuccessed, resultMessage, commandFrame, responseFrame, null, datas)
    { }
    //四参数构造函数(无数据反馈)
    public FinsResult(bool isSuccessed, string resultMessage, string commandFrame, string responseFrame) :
        this(isSuccessed, resultMessage, commandFrame, responseFrame, null, null)
    { }
    //两参数构造函数(出错时返回)
    public FinsResult(bool isSuccessed, string resultMessage) : this(isSuccessed, resultMessage, null, null, null, null) { }
}
        通讯库定义了一些常用的内存区域枚举类,使用时可以提前设定好
FinsIOMemoryAreaAddress.cs
            
            
              CSharp
              
              
            
          
          /// <summary>
/// FinsIO内存区域地址类
/// </summary>
public class FinsIOMemoryAreaAddress
{
    /// <summary>
    /// 内存区域代码
    /// </summary>
    public FinsMemoryAreaTypeEnum AreaType { get; set; }
    /// <summary>
    /// Word起始地址
    /// </summary>
    public ushort WordAddress { get; set; }
    /// <summary>
    /// Bit起始地址
    /// </summary>
    public ushort BitAddress { get; set; }
}
        FinsMemoryAreaTypeEnum.cs
            
            
              CSharp
              
              
            
          
          /// <summary>
/// Fins内存区域类型
/// </summary>
public enum FinsMemoryAreaTypeEnum
{
    CIOBit,
    CIOWord,
    WRBit,
    WRWord,
    HRBit,
    HRWord,
    DMBit,
    DMWord
}
        FINS指令配置的代码如下
FinsFrameConfig.cs
            
            
              CSharp
              
              
            
          
          public class FinsFrameConfig
{
    public string ICF { get; set; }
    public string RSV { get; set; }
    public string GCT { get; set; }
    public string DNA { get; set; }
    public string DA1 { get; set; }
    public string DA2 { get; set; }
    public string SNA { get; set; }
    public string SA1 { get; set; }
    public string SA2 { get; set; }
    public string SID { get; set; }
}
        功能实现的代码过长,请自行到通讯库项目Gitee 仓库查看吧
C#控制台测试功能
HostLinkDevice.cs
            
            
              CSharp
              
              
            
          
          public class HostLinkDevice
{
    public HostLinkDevice(string portName, int baudRate, Parity parity, int dataBits, StopBits stopBits)
    {
        PLC = new HostLinkFinsDevice(portName, baudRate, parity, dataBits, stopBits);
    }
    //HostLinkFins设备
    private HostLinkFinsDevice PLC;
    //Fins帧参数设置
    private static FinsFrameConfig finsFrameConfig = new FinsFrameConfig()
    {
        ICF = "00",
        DA2 = "00",
        SA2 = "00",
        SID = "00"
    };
    /// <summary>
    /// 串口打开
    /// </summary>
    public void Connect()
    {
        try
        {
            PLC.Connect();
        }
        catch (Exception)
        {
            throw;
        }
    }
    /// <summary>
    /// 串口关闭
    /// </summary>
    public void Disconnect()
    {
        try
        {
            PLC.DisConnect();
        }
        catch (Exception)
        {
            throw;
        }
    }
    /// <summary>
    /// 读取CIO区的连续Bit位方法(异步方法)
    /// </summary>
    /// <param name="WordAdr">读取起始Word地址</param>
    /// <param name="BitAdr">读取起始Bit地址</param>
    /// <param name="ReadCount">读取Bit位数量</param>
    /// <returns>返回通信结果的信息</returns>
    public async Task<List<bool>> ReadCIOBitAsync(ushort WordAdr, ushort BitAdr, ushort ReadCount)
    {
        try
        {
            FinsResult finsResult;
            //IO内存区域设置
            FinsIOMemoryAreaAddress IOMemoryAreaAdr = new FinsIOMemoryAreaAddress()
            {
                AreaType = FinsMemoryAreaTypeEnum.CIOBit,
                WordAddress = WordAdr,
                BitAddress = BitAdr
            };
            //获取FINS通信结果
            finsResult = await PLC.Read_MemoryAreaAsync(IOMemoryAreaAdr, finsFrameConfig, ReadCount);
            if (finsResult.IsSuccessed)
            {
                if (finsResult.ResultMessage.Equals("OK"))
                {
                    return finsResult.Datas_BoolList;
                }
                else
                {
                    throw new Exception($"{finsResult.ResultMessage}{Environment.NewLine}{Environment.NewLine}发送命令帧:{finsResult.CommandFrame}{Environment.NewLine}接收回复帧:{finsResult.ResponseFrame}");
                }
            }
            else
            {
                throw new Exception($"{finsResult.ResultMessage}{Environment.NewLine}{Environment.NewLine}发送命令帧:{finsResult.CommandFrame}{Environment.NewLine}接收回复帧:{finsResult.ResponseFrame}");
            }
        }
        catch (Exception)
        {
            throw;
        }
    }
    /// <summary>
    /// 读取W区的连续Bit位方法(异步方法)
    /// </summary>
    /// <param name="WordAdr">读取起始Word地址</param>
    /// <param name="BitAdr">读取起始Bit地址</param>
    /// <param name="ReadCount">读取Bit位数量</param>
    /// <returns>返回通信结果的信息</returns>
    public async Task<List<bool>> ReadWRBitAsync(ushort WordAdr, ushort BitAdr, ushort ReadCount)
    {
        try
        {
            FinsResult finsResult;
            //IO内存区域设置
            FinsIOMemoryAreaAddress IOMemoryAreaAdr = new FinsIOMemoryAreaAddress()
            {
                AreaType = FinsMemoryAreaTypeEnum.WRBit,
                WordAddress = WordAdr,
                BitAddress = BitAdr
            };
            //获取FINS通信结果
            finsResult = await PLC.Read_MemoryAreaAsync(IOMemoryAreaAdr, finsFrameConfig, ReadCount);
            if (finsResult.IsSuccessed)
            {
                if (finsResult.ResultMessage.Equals("OK"))
                {
                    return finsResult.Datas_BoolList;
                }
                else
                {
                    throw new Exception($"{finsResult.ResultMessage}{Environment.NewLine}{Environment.NewLine}发送命令帧:{finsResult.CommandFrame}{Environment.NewLine}接收回复帧:{finsResult.ResponseFrame}");
                }
            }
            else
            {
                throw new Exception($"{finsResult.ResultMessage}{Environment.NewLine}{Environment.NewLine}发送命令帧:{finsResult.CommandFrame}{Environment.NewLine}接收回复帧:{finsResult.ResponseFrame}");
            }
        }
        catch (Exception)
        {
            throw;
        }
    }
    /// <summary>
    /// 写入W区的连续Bit位方法(异步方法)
    /// </summary>
    /// <param name="WordAdr">写入起始Word地址</param>
    /// <param name="BitAdr">写入起始Bit地址</param>
    /// <param name="WriteCount">写入Bit位数量</param>
    /// <param name="WriteData">写入数据</param>
    /// <returns>返回通信结果的信息</returns>
    public async Task<string> WriteWRBitAsync(ushort WordAdr, ushort BitAdr, ushort WriteCount, string WriteData)
    {
        try
        {
            FinsResult finsResult;
            //IO内存区域设置
            FinsIOMemoryAreaAddress IOMemoryAreaAdr = new FinsIOMemoryAreaAddress()
            {
                AreaType = FinsMemoryAreaTypeEnum.WRBit,
                WordAddress = WordAdr,
                BitAddress = BitAdr
            };
            //获取FINS通信结果
            finsResult = await PLC.Write_MemoryAreaAsync(IOMemoryAreaAdr, finsFrameConfig, WriteCount, WriteData);
            if (finsResult.IsSuccessed)
            {
                return finsResult.ResultMessage;
            }
            else
            {
                throw new Exception($"{finsResult.ResultMessage}{Environment.NewLine}{Environment.NewLine}发送命令帧:{finsResult.CommandFrame}{Environment.NewLine}接收回复帧:{finsResult.ResponseFrame}");
            }
        }
        catch (Exception)
        {
            throw;
        }
    }
}
        Program.cs
            
            
              CSharp
              
              
            
          
          internal class Program
{
    static async Task Main(string[] args)
    {
        #region C-Mode方式
        HostLinkCmodeDevice plc1 = new HostLinkCmodeDevice("COM5", 115200, Parity.None, 8, StopBits.One);
        SemaphoreSlim semaphoreSlim_Comm1 = new SemaphoreSlim(1, 1);
        plc1.Connect();
        var Task1 = Task.Run(async () =>
        {
            await semaphoreSlim_Comm1.WaitAsync();
            Stopwatch stopwatch1 = new Stopwatch();
            stopwatch1.Start();
            string str1 = await plc1.TestCommandAsync("123123");
            stopwatch1.Stop();
            Console.WriteLine("TestCommand:");
            Console.WriteLine(str1);
            Console.WriteLine($"花费时间:{stopwatch1.ElapsedMilliseconds}ms");
            semaphoreSlim_Comm1.Release();
        });
        var Task2 = Task.Run(async () =>
        {
            await semaphoreSlim_Comm1.WaitAsync();
            Stopwatch stopwatch1 = new Stopwatch();
            stopwatch1.Start();
            string str1 = await plc1.Read_DMAreaAsync(0, 3);
            stopwatch1.Stop();
            Console.WriteLine("ReadDM:");
            Console.WriteLine(str1);
            Console.WriteLine($"花费时间:{stopwatch1.ElapsedMilliseconds}ms");
            semaphoreSlim_Comm1.Release();
        });
        await Task.WhenAll(Task1, Task2);
        plc1.DisConnect();
        semaphoreSlim_Comm1.Dispose();
        #endregion
        #region FINS-Mode方式
        //HostLinkDevice plc2 = new HostLinkDevice("COM5", 115200, Parity.None, 8, StopBits.One);
        //SemaphoreSlim semaphoreSlim_Comm2 = new SemaphoreSlim(1, 1);
        //plc2.Connect();
        //var Task3 = Task.Run(async () =>
        //{
        //    await semaphoreSlim_Comm2.WaitAsync();
        //    Stopwatch stopwatch2 = new Stopwatch();
        //    stopwatch2.Start();
        //    var list1 = await plc2.ReadCIOBitAsync(100, 0, 3);
        //    stopwatch2.Stop();
        //    Console.WriteLine("Read CIOBit:");
        //    foreach (var item in list1)
        //    {
        //        Console.WriteLine(item);
        //    }
        //    Console.WriteLine($"花费时间:{stopwatch2.ElapsedMilliseconds}ms");
        //    semaphoreSlim_Comm2.Release();
        //});
        //var Task4 = Task.Run(async () =>
        //{
        //    await semaphoreSlim_Comm2.WaitAsync();
        //    Stopwatch stopwatch2 = new Stopwatch();
        //    stopwatch2.Start();
        //    var list1 = await plc2.ReadWRBitAsync(0, 0, 3);
        //    stopwatch2.Stop();
        //    Console.WriteLine("Read WRBit:");
        //    foreach (var item in list1)
        //    {
        //        Console.WriteLine(item);
        //    }
        //    Console.WriteLine($"花费时间:{stopwatch2.ElapsedMilliseconds}ms");
        //    semaphoreSlim_Comm2.Release();
        //});
        //var Task5 = Task.Run(async () =>
        //{
        //    await semaphoreSlim_Comm2.WaitAsync();
        //    Stopwatch stopwatch2 = new Stopwatch();
        //    stopwatch2.Start();
        //    var list1 = await plc2.WriteWRBitAsync(0, 0, 3, "000100");
        //    stopwatch2.Stop();
        //    Console.WriteLine("Write WRBit:");
        //    Console.WriteLine(list1);
        //    Console.WriteLine($"花费时间:{stopwatch2.ElapsedMilliseconds}ms");
        //    semaphoreSlim_Comm2.Release();
        //});
        //await Task.WhenAll(Task3, Task4, Task5);
        //plc2.Disconnect();
        //semaphoreSlim_Comm2.Dispose();
        #endregion
        Console.ReadKey();
    }
}
        具体项目请自行到控制台测试项目Gitee 仓库查看吧
测试结果
控制台输出图如下:
C-Mode方式

FINS-Mode方式
