C#实现欧姆龙 HostLink 通讯协议库


目录

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-ModeFINS-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#控制台项目

/// <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

/// <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

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

/// <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

/// <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

/// <summary>
/// Fins内存区域类型
/// </summary>
public enum FinsMemoryAreaTypeEnum
{
    CIOBit,
    CIOWord,
    WRBit,
    WRWord,
    HRBit,
    HRWord,
    DMBit,
    DMWord
}

FINS指令配置的代码如下

FinsFrameConfig.cs

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

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

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方式