Spiga

通信协议:三菱

2026-03-15 16:38:47

一、三菱PLC协议

1. 三菱PLC设备

  • PLC以及相关通信模块:FX、Q、R、L、A
  • 通信模块:C24、E71

2. 三菱PLC存储区与访问规则

3. 通信协议

MC协议:

  • 公开
  • 支持Ascii以及二进制两种数据格式
  • 协议体系:串口:1C、2C、3C、4C 以太网:4E、3E、1E
  • Modbus、MX-Component

请求流程,WireShark监控通信过程,McProtocol库测试使用:仿真环境验证

  • (1)建立TCP连接 Scoket Connect
  • (2)执行相关操作 读、写

字节序:

小端 byte[] bytes {低 高 _ _ _ _ } 报文中的字节序小端模式

123 0x00,0x7B bytes[0]=0x7B bytes[1]=0x00

4. 功能操作分类

  • 成批读出:字、位 :指定一个地址 D0 指定一个数量 返回相关数据 连续请求数据
  • 字单位的随机读出 :不连续的地址请求 D0 D100 W10 每个地址读一个长度
  • 多块成批读出 :不连续的地址 地址带长度 位按字处理?

  • 成批写入:字、位
  • 随机写入:字、位
  • 多块成批写入

5. 成批读出报文

// 发送批量读取请求报文:0401
// 按字从 D05 地址开始请求 3个地址的数据
//        D存储区,一个地址就是一个字(两个字节)
//        3*2=6个字节的数据响应返回
// 按位从 M10 地址开始请求5个地址的数据      10 -> 0A 00 00
//        M存储区,按位进行地址分配(ON/OFF)
//        3个字节    M10  M11  M12  M13  M14
//                     0x11     0x00      0x10
//
// *******按位从X123地址请求********
//        起始地址  123    00 01 23   ->   0x23 0x01 0x00
// 
// 按字从 M10 地址开始 请求20个地址的数据  M10 M11 M12 ..... M29
//        一个字存16个状态   20-》需要2个字
//        0x00 0x00    0x00 0x00
//        第一个字节:1000 1000      M17 M16 M15 M14 M13 M12 M11 M10
//        第二个字节:0001 0000      M25 M24 M23 M22 M21 M20 M19 M18
//        0000 1010                                 M29 M28 M27 M26
//        0000 0000
byte[] bytes = new byte[] {
    0x50,0x00,
    0x00,
    0xFF,
    0xFF,0x03,
    0x00,

    // 剩余字节长度
    0x0C,0x00,  // 注意小端字节序
    0x10,0x00,  // 响应时间  *250ms

    // 主指令
    0x01,0x04,
    // 子指令  
    0x00,0x00, // 表示按字处理
    //0x01,0x00, // 表示按位处理
    // 地址
    //0x05,0x00,0x00,
    //0x09,0x00,0x00,
    0x15,0x00,0x00,       // 15-16-17-18-19-1A-1B-1C-1D-1E
    // 区域
    //0xA8,  // D
    //0x90,  // M
    0x9D,   // Y
    // 数量
    0x02,0x00
};
socket.Send(bytes);

6. 成批写入报文

// 发送批量写入请求报文:1401
// 按字写入D30开始的三个地址,分别写入123、124、125
//       在写入数量后跟上3个字,6个字节的数据 0x7b,0x00   0x7C,0x00   0x7D,0x00
// 按位写入M00开始写入10个状态:分别写入:01,00,01,01,01,00,00,01,00,01
//       在写入数量后跟上5个字节    0x10, 0x11,0x10,0x01,0x01
// 按字写入M00开始写入10个状态:分别写入:01,00,01,01,01,00,00,01,00,01
//       在写入数量后跟上2个字节   1001 1101       0000 0010
//                               0x9D            0x02
byte[] bytes = new byte[] {
    0x50,0x00,
    0x00,
    0xFF,
    0xFF,0x03,
    0x00,

    0x12,0x00,
    0x10,0x00,

    0x01,0x14,
    0x00,0x00,
    0x1E,0x00,0x00,
    0xA8,
    0x03,0x00,// 写入数量
    0x7B,0x00,
    0x7C,0x00,
    0x7D,0x00
};

// 写入M5开始的三个地址数据,01、01、01
bytes = new byte[] {
    0x50,0x00,
    0x00,
    0xFF,
    0xFF,0x03,
    0x00,

    0x0E,0x00,
    0x10,0x00,

    0x01,0x14,
    0x01,0x00,
    0x05,0x00,0x00,
    0x90,
    0x03,0x00,
    0x11,0x10
};
//
bytes = new byte[] {
    0x50,0x00,
    0x00,
    0xFF,
    0xFF,0x03,
    0x00,

    0x0E,0x00,
    0x10,0x00,

    0x01,0x14,
    0x00,0x00,
    0x30,0x00,0x00,
    0x90,
    0x01,0x00,// 1个字  16个位状态  Y30----Y3F
    0x35,0x89
};
socket.Send(bytes);

7. 随机读出报文

// 发送随机读取请求报文:0403
// 1、没有位操作
// 2、主要涉及字与双字类型的请求
// 3、操作响应结果只有一个当前地址的数据,不涉及相关地址开始的读取长度问题,只有一个
byte[] bytes = new byte[] {
    0x50,0x00,
    0x00,
    0xFF,
    0xFF,0x03,
    0x00,

    0x1C,0x00, // 长度需要调整
    0x10,0x00,


    0x03,0x04,
    0x00,0x00,

    0x03,// 后面跟着的前三个地址,按字进行数据返回
    0x02,// 后面跟着的后两个地址,按双字进行数据返回
    // D100 --响应数据返回2个字节     D100
    0x64,0x00,0x00,
    0xA8,
    // M00 --响应数据返回2个字节,    M0-M15
    0x00,0x00,0x00,
    0x90,
    // X120 --响应数据返回2个字节     X120-X12F   或者  X120 - X127、X130-X137
    0x20,0x01,0x00,
    0x9C,

    // D125 --响应数据返回4个字节     D125\D126
    0x7D,0x00,0x00,
    0xA8,
    // M16 --响应数据返回4个字节      M10-M41    
    0x10,0x00,0x00,
    0x90,
};
socket.Send(bytes);

8. 随机写入报文

// 发送随机写入请求报文:1402
// 按位将XA0地址设置为ON(0x01)  XA1地址设置为OFF(0x00)
// 按字分两种情况:字-将D0、M100-M115开始写入相就的字数据
//                双字-将D1000-D1001   Y120-Y127/Y130-Y137/Y140-Y147/Y150-Y157
//                                    Y120-Y12F/Y130-Y13F
byte[] bytes = new byte[] {
    0x50,0x00,
    0x00,
    0xFF,
    0xFF,0x03,
    0x00,

    0x11,0x00,// 长度需要调整
    0x10,0x00,

    0x02,0x14,
    0x01,0x00,//按位处理

    0x02,// 处理的位点数

    0xA0,0x00,0x00,
    0x9C,
    0x01,// 需要设置的状态结果 ON

    0xA1,0x00,0x00,
    0x9C,
    0x01 // 需要设置的状态结果 ON
};
//socket.Send(bytes);
//return;

/////****************************
bytes = new byte[] {
    0x50,0x00,
    0x00,
    0xFF,
    0xFF,0x03,
    0x00,

    0x24,0x00,// 长度需要调整
    0x10,0x00,

    0x02,0x14,
    0x00,0x00,//按字处理

    0x02,// 处理的字点数
    0x02,// 处理的双字点数

    0x00,0x00,0x00,
    0xA8,
    0x6F,0x00,// 向D0写入111

    0x50,0x00,0x00,
    0x90,
    0x45,0x82, // 向M100开始的位地址写入16个状态。
              // 这里需要将这一个字,拆分成16个位进行设置,
              // 从第一个字节的最低位开始,进行第一个状态的提交
              // 第一个字节:0100 0101  -- M107 M106 M105 M104 M103 M102 M101 M100
              // 第二个字节:1000 0010  -- M115 M114 M113 M112 M111 M110 M109 M108

    // 0x03,0xE8
    0xE8,0x03,0x00,
    0xA8,
    0x7B,0x00,0x7C,0x00, // 向D1000-D1001地址写入2个字的数据
                         // 32位的数据类型   float   4.5:0x40 0x90 0x00 0x00

    0x28,0x01,0x00,
    0x9D,
    0x01,0x00,0x00,0x80  // 向Y120开始的32个位地址写入状态数据
              // 第一个字节:0000 0001  -- Y127 Y126 Y125 Y124 Y123 Y122 Y121 Y120
              // 第二个字节:0000 0000  
              // 第三个字节:0000 0000  
              // 第四个字节:1000 0000  

};
socket.Send(bytes);

9. 多块成批读出报文

// 一个报文里同时处理字、位
byte[] bytes = new byte[] {
    // 头部信息
    0x50,0x00,
    0x00,
    0xFF,
    0xFF,0x03,
    0x00,

    0x26,0x00,// 长度需要调整
    0x10,0x00,

    0x06,0x04,
    0x00,0x00,// 子指令
    /// 处理字的地址数量
    0x02,
    /// 处理位的地址数量
    0x03,

    // D0 - 4
    0x00,0x00,0x00,
    0xA8,
    0x04,0x00, // 请求4个点  D0、D1、D2、D3
    // D100 - 2
    0x64,0x00,0x00,
    0xA8,
    0x02,0x00, // 请求2个点  D100-D101

    // X10 - 2
    0x10,0x00,0x00,
    0x9C,
    0x02,0x00, // 请求2个点  32个位   X10-X1F  X20-X2F

    // M16 - 2
    0x10,0x00,0x00,
    0x90,
    0x02,0x00, // 请求2个点  32个位   M16-M48
    

    // M96 - 2
    0x50,0x00,0x00,
    0x90,
    0x02,0x00, // 请求2个点  32个位   M96-M128
};
socket.Send(bytes);

10. 多块成批写入报文

// 一个报文里同时处理字、位
byte[] bytes = new byte[] {
    // 头部信息
    0x50,0x00,
    0x00,
    0xFF,
    0xFF,0x03,
    0x00,

    0x40,0x00,// 长度需要调整
    0x10,0x00,

    0x06,0x14,
    0x00,0x00,// 子指令
    /// 处理字的地址数量
    0x02,
    /// 处理位的地址数量
    0x03,

    // D0 - 4
    0x00,0x00,0x00,
    0xA8,
    0x04,0x00, // 请求4个点  D0、D1、D2、D3
    0x01,0x00,0x02,0x00,0x03,0x00,0x04,0x00,// 一共写入8个字节的数据

    // D100 - 2
    0x64,0x00,0x00,
    0xA8,
    0x02,0x00, // 请求2个点  D100-D101
    0x01,0x00,0x02,0x00, // 一共写入4个字节的数据

    // X10 - 3
    0x10,0x00,0x00,
    0x9C,
    0x03,0x00, // 请求2个点  32个位   X10-X1F  X20-X2F
    0x01,0x00,0x02,0x00,0x03,0x00,// 一共写入6个字节的数据,48个状态

    // M10 - 2
    0x10,0x00,0x00,
    0x90,
    0x02,0x00, // 请求2个点  32个位   M10-M41
    0x01,0x00,0x02,0x00,// 一共写入4个字节的数据,32个状态

    // M100 - 2
    0x50,0x00,0x00,
    0x90,
    0x02,0x00, // 请求2个点  32个位   M100-M131
    0x01,0x00,0x02,0x00,// 一共写入4个字节的数据,32个状态
};
socket.Send(bytes);

11. PLC启停报文

// 1001
static void Mc3E_Run()
{
    Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
    socket.Connect("192.168.1.128", 6000);
    byte[] bytes = new byte[] {
        // 头部
        0x50,0x00,
        0x00,
        0xFF,
        0xFF,0x03,
        0x00,

        0x0A,0x00,// 长度需要调整
        0x10,0x00,

        0x01,0x10,
        0x00,0x00,
        0x03,0x00,// 是否强制执行:0x01不强制执行   0x03强制执行
        0x00,// 清除模式 :00 不清空;01 清空锁存以外;02 清空全部
        0x00// 默认值
    };
    socket.Send(bytes);
}
// 1002
static void Mc3E_Stop()
{
    Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
    socket.Connect("192.168.1.128", 6000);
    byte[] bytes = new byte[] {
        // 头部
        0x50,0x00,
        0x00,
        0xFF,
        0xFF,0x03,
        0x00,

        0x08,0x00,// 长度需要调整
        0x10,0x00,

        0x02,0x10,
        0x00,0x00,
        0x01,0x00,// 是否强制执行:0x01不强制执行   0x03强制执行
    };
    socket.Send(bytes);
}

// 0101
static void Mc3E_PlcInfo()
{
    Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
    socket.Connect("192.168.1.128", 6000);
    byte[] bytes = new byte[] {
        // 头部
        0x50,0x00,
        0x00,
        0xFF,
        0xFF,0x03,
        0x00,

        0x06,0x00,// 长度需要调整
        0x10,0x00,

        0x01,0x01,
        0x00,0x00
    };
    socket.Send(bytes);

    // 响应:型号名称
}

附录

  • 附录一,命令代码

  • 附录二,结束代码-异常判断

结束代码为5BH/“5B”的情况下,结束代码之后包含异常代码(10H~21H)的数据和00H/“00”

二、三菱PLC通信库封装

1. 功能清单

  • TCP连接
  • 读取封装
  • 地址解析
  • 地址分组
  • 写入封装
  • 效率测试
  • 自动重连(心跳)
  • 异步读写(优化)

2. 定义枚举与数据字典

public enum Areas
{
    X = 0x9C,
    Y = 0x9D,
    M = 0x90,
    L = 0x92,
    S = 0x98,
    B = 0xA0,
    F = 0x93,
    TS = 0xC1,
    TC = 0xC0,
    TN = 0xC2,
    CS = 0xC4,
    CC = 0xC3,
    CN = 0xC5,
    D = 0xA8,
    W = 0xB4,
    R = 0xAF,
    ZR = 0xB0
}

public enum RequestType
{
    BIT = 0x01,
    WORD = 0x00
}

public class DataParameter
{
    public Areas Area { get; set; }

    public string Address { get; set; } = null!;

    public int Count { get; set; }

    public List<byte>? Datas { get; set; }
}

internal class Status
{
    public static Dictionary<string, string> Errors = new Dictionary<string, string>()
    {
        {"0055","将运行中写入设置为不允许时,通过对象设备向CPU模块发出了运行中数据写入请求" },
        {"C050","在“通信数据代码设置”中,设置了ASCII代码通信时,接收了不能转换为二进制代码的ASCII代码" },
        {"C051","写入或读取点数超出了允许范围" },
        {"C052","写入或读取点数超出了允许范围" },
        {"C053","写入或读取点数超出了允许范围" },
        {"C054","写入或读取点数超出了允许范围" },
        {"C056","写入及读取请求超出了最大地址" },
        {"C058","ASCII- 二进制转换后的请求数据长与字符部分(文本的一部分)的数据数不相符" },
        {"C059","指令、子指令的指定有误,或者在CPU模块中禁止使用的指令、子指令" },
        {"C05B","CPU 模块不能对指定软元件进行写入及读取" },
        {"C05C","请求内容中存在有错误。(以位为单位对字软元件进行了写入及读取等)" },
        {"C05D","未进行监视登录" },
        {"C05F","是不能对对象CPU模块执行的请求" },
        {"C060","请求内容中存在有错误。(对位软元件进行的数据指定中存在有错误等)" },
        {"C061","请求数据长与字符部分(文本的一部分)的数据数不相符" },
        {"C06F","“通信数据代码设置”为二进制时,接收了ASCII的请求报文。此外,设置为ASCII时,接收了二进制的请求报文" },
        {"C070","不能对对象站进行软元件存储器的扩展指定" },
        {"C200","远程口令有错误" },
        {"C201","通信所使用的端口处于远程口令的锁定状态。或者“通信数据代码设置”为ASCII代码时,由于处于远程口令的锁定状态,因此无法将子指令以后转换为二进制代码" },
        {"C204","与发出了远程口令解锁处理请求的对象设备不相同" },
    };
}

3. 封装基础方法

我们已经有好几个协议封装的经验了,这里我们提前把一些基础方法封装好

private byte[] SendAndReceive(byte[] bytes)
{
    socket.Send(bytes);

    byte[] resp = new byte[1024];
    socket.Receive(resp);

    return resp;
}

private byte[] GetAddress(string address, Areas area)
{
    int addr = 0;
    int.TryParse(address, out addr);
    List<byte> addr_bytes = new List<byte>();
    if (new Areas[] { Areas.X, Areas.Y }.Contains(area))
    {
        string addr_str = address.PadLeft(6, '0');	//填充0
        try
        {
            byte[] _bytes = Convert.FromHexString(addr_str);
            Array.Reverse(_bytes);
            addr_bytes.AddRange(_bytes);
        }
        catch { throw new Exception("地址格式错误"); }
    }
    else
    {
        addr_bytes.Add((byte)(addr % 256));
        addr_bytes.Add((byte)(addr / 256 % 256));
        addr_bytes.Add((byte)(addr / 256 / 256 % 256));
    }

    return addr_bytes.ToArray();
}

private void CheckResponse(byte[] resp)
{
    // 检查状态码
    string state = resp[10].ToString("X2") + resp[9].ToString("X2");
    if (state != "0000")
    {
        if (Status.Errors.ContainsKey(state))
            throw new Exception(Status.Errors[state]);
        else
            throw new Exception("未知错误");
    }
}
  • 公共方法封装
public (Areas, string) GetAddress(string addr)
{
    string prefix = addr.Substring(0, 2);
    if (Enum.TryParse<Areas>(prefix, out Areas area))
    {
        string start = addr.Substring(2);
        return (area, start);
    }
    prefix = addr.Substring(0, 1);
    if (Enum.TryParse<Areas>(prefix, out area))
    {
        string start = addr.Substring(1);

        return (area, start);
    }
    throw new Exception("地址有误,请确认");
}

// 数据转换
// 从字节到数据的转换
public List<T> GetDatas<T>(byte[] bytes)
{
    List<T> datas = new List<T>();
    if (typeof(T) == typeof(bool))
    {
        foreach (byte b in bytes)
        {
            dynamic d = (b == 0x01);
            datas.Add(d);
        }
    }
    else if (typeof(T) == typeof(string))
    {
        dynamic d = Encoding.UTF8.GetString(bytes);
        datas.Add(d);
    }
    else
    {
        int size = Marshal.SizeOf<T>();

        Type tBitConverter = typeof(BitConverter);
        MethodInfo[] mis = tBitConverter.GetMethods(BindingFlags.Public | BindingFlags.Static);
        if (mis.Count() <= 0) return datas;
        MethodInfo mi = mis.FirstOrDefault(m => m.ReturnType == typeof(T) && m.GetParameters().Count() == 2)!;

        for (int i = 0; i < bytes.Length; i += size)
        {
            byte[] data_bytes = bytes.ToList().GetRange(i, size).ToArray();

            if (BitConverter.IsLittleEndian)
            {
                Array.Reverse(data_bytes);
            }
            dynamic v = mi.Invoke(tBitConverter, new object[] { data_bytes, 0 })!;
            datas.Add(v);
        }
    }
    return datas;
}

// 从数据到字节的转换
public byte[] GetBytes<T>(params T[] values)
{
    List<byte> bytes = new List<byte>();
    if (typeof(T) == typeof(bool))
    {
        foreach (var v in values)
        {
            bytes.Add((byte)(bool.Parse(v.ToString()!) ? 0x01 : 0x00));
        }
    }
    else if (typeof(T) == typeof(string))
    {
        byte[] str_bytes = Encoding.UTF8.GetBytes(values[0].ToString()!);
        bytes.AddRange(str_bytes);
    }
    else
    {
        foreach (var v in values)
        {
            dynamic d = v;
            byte[] v_bytes = BitConverter.GetBytes(d);
            if (BitConverter.IsLittleEndian)
            {
                Array.Reverse(v_bytes);
            }
            bytes.AddRange(v_bytes);
        }
    }
    return bytes.ToArray();
}

4. 批量读写

public byte[] Read(Areas area, string addr, ushort count, RequestType type = RequestType.WORD)
{
    byte[] addr_bytes = this.GetAddress(addr, area);

    byte[] bytes = new byte[] {
        0x50,0x00,
        0x00,
        0xFF,
        0xFF,0x03,
        0x00,

        // 剩余字节长度
        0x0C,0x00,  // 注意小端字节序
        0x10,0x00,  // 响应时间  *250ms

        // 主指令
        0x01,0x04,
        // 子指令  
        (byte)type,0x00, 
        // 地址
        addr_bytes[0],
        addr_bytes[1],
        addr_bytes[2],      
        // 区域
        (byte)area,  
        // 数量
        (byte)(count%256),
        (byte)(count/256%256)
    };
    byte[] resp = this.SendAndReceive(bytes);

    // 检查状态码
    this.CheckResponse(resp);

    // 获取数据字节,进行返回
    // 字   2字节  count*2个字节      位软元件    count*16位信息
    // 位   1个2个状态   count/2

    if (RequestType.WORD == type)
    {
        // 按字进行位软元件请求
        byte[] values_bytes = resp.Skip(11).Take(count * 2).ToArray();
        List<byte> data_values = new List<byte>();

        // 关于这个列表可以将所有位软元件编号都放进来
        if (new Areas[] { Areas.X, Areas.Y, Areas.M }.Contains(area))
        {
            foreach (byte value in values_bytes)
            {
                for (int i = 0; i < 8; i++)
                {
                    data_values.Add((byte)((value & (1 << i)) > 0 ? 0x01 : 0x00));
                }
            }
        }
        else// 按字进行字软元件请求
        {
            for (int i = 0; i < count * 2; i += 2)
            {
                byte[] temp = values_bytes.Skip(i).Take(2).ToArray();
                Array.Reverse(temp);
                data_values.AddRange(temp);

                // 或者
                //data_values.Add(values_bytes[i + 1]);
                //data_values.Add(values_bytes[i]);
            }
        }
        return data_values.ToArray();
    }
    else if (RequestType.BIT == type)
    {
        // 0x11  0x01
        // 0x01 0x01 0x00 0x01
        byte[] state_bytes = resp.Skip(11).Take(count / 2 + (count % 2)).ToArray();
        byte[] state_value = new byte[count];
        int index = 0;
        for (int i = 0; i < count; i++)
        {
            if (i % 2 == 0)
                state_value[i] = (byte)((state_bytes[index] & (1 << 4)) > 0 ? 0x01 : 0x00);
            else
            {
                state_value[i] = (byte)((state_bytes[index] & 1) > 0 ? 0x01 : 0x00);
                index++;
            }
        }

        return state_value;
    }
    else
        return null;
}

// X0  XA0   X1A    M0  D0  
public byte[] Read(string address, ushort count, RequestType type = RequestType.WORD)
{
    (Areas area, string start) = this.GetAddress(address);
    return this.Read(area, start, count, type);
}

// datas:{0x01,0x00,0x00,0x01,0x01}
// datas字节按大端处理
public void Write(byte[] datas, Areas area, string addr, RequestType type = RequestType.WORD)
{
    int count = 0;
    // 数据处理
    List<byte> data_bytes = new List<byte>();
    if (type == RequestType.WORD)
    {
        // 这里只是部分们软元件,其他自行添加
        if (new Areas[] { Areas.X, Areas.Y, Areas.M }.Contains(area))
        {
            count = (int)Math.Ceiling(datas.Length * 1.0 / 16);

            int byte_count = (int)(count * 2);
            byte[] bytes = new byte[byte_count];
            int index = -1;
            for (int i = 0; i < datas.Length; i++)
            {
                if (i % 8 == 0)
                    index++;

                byte current = 0x00;
                if (datas[i] == 0x01)
                    current = (byte)(current | (1 << (i % 8)));

                bytes[index] |= current;
            }
            data_bytes = new List<byte>(bytes);
        }
        else
        {
            count = datas.Length / 2;
            for (int i = 0; i < datas.Length; i += 2)
            {
                data_bytes.Add(datas[i + 1]);
                data_bytes.Add(datas[i]);
            }
        }
    }
    else if (type == RequestType.BIT)
    {
        count = datas.Length;

        byte[] temp = new byte[datas.Length / 2 + (datas.Length % 2)];
        int index = 0;
        for (int i = 0; i < datas.Length; i++)
        {
            byte d = 0x00;
            if (i % 2 == 0 && datas[i] == 0x01)
            {
                d = (byte)(d | (1 << 4));
                temp[index] |= d;
            }
            if (i % 2 != 0 && datas[i] == 0x01)
            {
                d = (byte)(d | 1);
                temp[index] |= d;
                index++;
            }
        }
        data_bytes = new List<byte>(temp);
    }

    //  地址处理
    byte[] addr_bytes = this.GetAddress(addr, area);

    List<byte> req_bytes = new List<byte>{
        0x50,0x00,
        0x00,
        0xFF,
        0xFF,0x03,
        0x00,

        0x12,0x00,  // 长度
        0x10,0x00,

        0x01,0x14,
        (byte)type,0x00,
        addr_bytes[0],
        addr_bytes[1],
        addr_bytes[2],
        (byte)area,
        (byte)(count%256),
        (byte)(count/256%256),// 写入数量
    };
    req_bytes.AddRange(data_bytes);
    int len = 12 + data_bytes.Count;
    req_bytes[7] = (byte)(len % 256);
    req_bytes[8] = (byte)(len / 256 % 256);

    byte[] resp_bytes = this.SendAndReceive(req_bytes.ToArray());

    // 检查状态码
    this.CheckResponse(resp_bytes);
}

public void Write(byte[] datas, string address, RequestType type = RequestType.WORD)
{
    (Areas area, string start) = this.GetAddress(address);
    this.Write(datas, area, start, type);
}

5. 随机读写

// 随机读写   按字、双字读取
public void RandomRead(List<DataParameter> w_address, List<DataParameter> dw_address)
{
    List<byte> bytes = new List<byte>{
        0x50,0x00,
        0x00,
        0xFF,
        0xFF,0x03,
        0x00,

        // 剩余字节长度
        0x0C,0x00,  // 注意小端字节序
        0x10,0x00,  // 响应时间  *250ms

        0x03,0x04,
        0x00,0x00,

        (byte)w_address.Count,
        (byte)dw_address.Count,
    };

    foreach (var address in w_address)
    {
        byte[] addr_bytes = this.GetAddress(address.Address, address.Area);
        // 拼接请求的地址
        bytes.AddRange(addr_bytes);
        bytes.Add((byte)address.Area);
    }
    foreach (var address in dw_address)
    {
        byte[] addr_bytes = this.GetAddress(address.Address, address.Area);
        // 拼接请求的地址
        bytes.AddRange(addr_bytes);
        bytes.Add((byte)address.Area);
    }

    // 计算请求长度  字节数
    int len = 8 + (w_address.Count + dw_address.Count) * 4;
    bytes[7] = (byte)(len % 256);
    bytes[8] = (byte)(len / 256 % 256);

    byte[] resp = this.SendAndReceive(bytes.ToArray());
    // 检查响应状态
    this.CheckResponse(resp);

    // 数据字节整理
    int index = 11;
    foreach (var address in w_address)
    {
        if (address.Datas == null)
            address.Datas = new List<byte>();

        byte[] values_bytes = resp.Skip(index).Take(2).ToArray();
        if (new Areas[] { Areas.X, Areas.Y, Areas.M }.Contains(address.Area))
        {
            foreach (byte value in values_bytes)
            {
                for (int i = 0; i < 8; i++)
                {
                    address.Datas.Add((byte)((value & (1 << i)) > 0 ? 0x01 : 0x00));
                }
            }
        }
        else
        {
            Array.Reverse(values_bytes);
            address.Datas.AddRange(values_bytes);
        }
        index += 2;
    }
    foreach (var address in dw_address)
    {
        if (address.Datas == null)
            address.Datas = new List<byte>();

        byte[] values_bytes = resp.Skip(index).Take(4).ToArray();
        if (new Areas[] { Areas.X, Areas.Y, Areas.M }.Contains(address.Area))
        {
            foreach (byte value in values_bytes)
            {
                for (int i = 0; i < 8; i++)
                {
                    address.Datas.Add((byte)((value & (1 << i)) > 0 ? 0x01 : 0x00));
                }
            }
        }
        else
        {
            Array.Reverse(values_bytes);
            address.Datas.AddRange(values_bytes);
        }

        index += 4;
    }
}

// 按位进行写入
public void RandomWriteBit(List<DataParameter> address)
{
    List<byte> bytes = new List<byte>{
        0x50,0x00,
        0x00,
        0xFF,
        0xFF,0x03,
        0x00,

        // 剩余字节长度
        0x0C,0x00,  // 注意小端字节序
        0x10,0x00,  // 响应时间  *250ms

        0x02,0x14,
        0x01,0x00,// 按位操作

        (byte)address.Count,
    };
    foreach (var item in address)
    {
        byte[] addr_bytes = this.GetAddress(item.Address, item.Area);
        bytes.AddRange(addr_bytes);
        bytes.Add((byte)item.Area);
        // 数据部分
        if (item.Datas == null || item.Datas.Count == 0)
            throw new Exception("需要写入的数据有异常,无法写入");
        else
        {
            if (item.Datas[0] > 0)
                bytes.Add(0x01);
            else
                bytes.Add(0x00);
        }
    }
    int len = 7 + address.Count * 5;
    bytes[7] = (byte)(len % 256);
    bytes[8] = (byte)(len / 256 % 256);

    byte[] resp = this.SendAndReceive(bytes.ToArray());

    this.CheckResponse(resp);
}

// 按字、双字进行写入
public void RandomWrite(List<DataParameter> w_address, List<DataParameter> dw_address)
{
    List<byte> bytes = new List<byte>{
        0x50,0x00,
        0x00,
        0xFF,
        0xFF,0x03,
        0x00,

        // 剩余字节长度
        0x0C,0x00,  // 注意小端字节序
        0x10,0x00,  // 响应时间  *250ms

        0x02,0x14,
        0x00,0x00,// 按字操作

        (byte)w_address.Count,
        (byte)dw_address.Count,

    };
    foreach (var address in w_address)
    {
        byte[] addr_bytes = this.GetAddress(address.Address, address.Area);
        // 拼接请求的地址
        bytes.AddRange(addr_bytes);
        bytes.Add((byte)address.Area);

        if (new Areas[] { Areas.X, Areas.Y, Areas.M }.Contains(address.Area))
        {
            if (address.Datas == null || address.Datas.Count != 16)
                throw new Exception("需要写入的数据有异常,无法写入");
            byte[] data_bytes = new byte[2];
            int index = 0;
            // 将16个状态字节,转换成两个字节,一个字节8个位进行汇总
            for (int i = 0; i < 16; i++)
            {
                if (i > 0 && i % 8 == 0) index++;
                byte bit = (byte)(address.Datas[i] == 0x01 ? 0x01 : 0x00);
                data_bytes[index] |= (byte)(bit << (i % 8));
            }
            bytes.AddRange(data_bytes);// 将
        }
        else
        {
            if (address.Datas == null || address.Datas.Count != 2)
                throw new Exception("需要写入的数据有异常,无法写入");
            bytes.AddRange(address.Datas);
        }
    }
    foreach (var address in dw_address)
    {
        byte[] addr_bytes = this.GetAddress(address.Address, address.Area);
        // 拼接请求的地址
        bytes.AddRange(addr_bytes);
        bytes.Add((byte)address.Area);


        if (new Areas[] { Areas.X, Areas.Y, Areas.M }.Contains(address.Area))
        {
            if (address.Datas == null || address.Datas.Count != 32)
                throw new Exception("需要写入的数据有异常,无法写入");
            byte[] data_bytes = new byte[4];
            int index = 0;
            // 将16个状态字节,转换成两个字节,一个字节8个位进行汇总
            for (int i = 0; i < 32; i++)
            {
                if (i > 0 && i % 8 == 0) index++;
                byte bit = (byte)(address.Datas[i] == 0x01 ? 0x01 : 0x00);
                data_bytes[index] |= (byte)(bit << (i % 8));
            }
            bytes.AddRange(data_bytes);// 将
        }
        else
        {
            if (address.Datas == null || address.Datas.Count != 4)
                throw new Exception("需要写入的数据有异常,无法写入");
            bytes.AddRange(address.Datas);
        }
    }

    int len = 8 + w_address.Count * 6 + dw_address.Count * 8;
    bytes[7] = (byte)(len % 256);
    bytes[8] = (byte)(len / 256 % 256);

    byte[] resp = this.SendAndReceive(bytes.ToArray());

    this.CheckResponse(resp);
}

6. 多块读写

public void MultiBlockRead(List<DataParameter> w_address, List<DataParameter> b_address)
{
    List<byte> bytes = new List<byte>{
        0x50,0x00,
        0x00,
        0xFF,
        0xFF,0x03,
        0x00,

        // 剩余字节长度
        0x0C,0x00,  // 注意小端字节序
        0x10,0x00,  // 响应时间  *250ms

        0x06,0x04,
        0x00,0x00,

        (byte)w_address.Count,
        (byte)b_address.Count,
    };

    foreach (var address in w_address)
    {
        byte[] addr_bytes = this.GetAddress(address.Address, address.Area);
        // 拼接请求的地址
        bytes.AddRange(addr_bytes);
        bytes.Add((byte)address.Area);

        bytes.Add((byte)(address.Count % 256));
        bytes.Add((byte)(address.Count / 256 % 256));
    }
    foreach (var address in b_address)
    {
        if (!new Areas[] { Areas.X, Areas.Y, Areas.M }.Contains(address.Area))
        {
            throw new Exception("位地址有误");
        }
        byte[] addr_bytes = this.GetAddress(address.Address, address.Area);
        // 拼接请求的地址
        bytes.AddRange(addr_bytes);
        bytes.Add((byte)address.Area);

        bytes.Add((byte)(address.Count % 256));// 数量  表示多少字
        bytes.Add((byte)(address.Count / 256 % 256));
    }

    int len = 8 + (w_address.Count + b_address.Count) * 6;
    bytes[7] = (byte)(len % 256);
    bytes[8] = (byte)(len / 256 % 256);

    byte[] resp = this.SendAndReceive(bytes.ToArray());
    // 检查响应状态
    this.CheckResponse(resp);

    // 解析数据
    int index = 11;
    foreach (var address in w_address)
    {
        if (address.Datas == null)
            address.Datas = new List<byte>();

        int byte_count = address.Count * 2;
        byte[] values_bytes = resp.Skip(index).Take(byte_count).ToArray();
        if (new Areas[] { Areas.X, Areas.Y, Areas.M }.Contains(address.Area))
        {
            foreach (byte value in values_bytes)
            {
                for (int i = 0; i < 8; i++)
                {
                    address.Datas.Add((byte)((value & (1 << i)) > 0 ? 0x01 : 0x00));
                }
            }
        }
        else
        {
            for (int i = 0; i < values_bytes.Length; i += 2)
            {
                address.Datas.Add(values_bytes[i + 1]);
                address.Datas.Add(values_bytes[i]);
            }
        }
        index += byte_count;
    }
    foreach (var address in b_address)
    {
        if (address.Datas == null)
            address.Datas = new List<byte>();

        int byte_count = address.Count * 2;
        byte[] values_bytes = resp.Skip(index).Take(byte_count).ToArray();

        foreach (byte value in values_bytes)
        {
            for (int i = 0; i < 8; i++)
            {
                address.Datas.Add((byte)((value & (1 << i)) > 0 ? 0x01 : 0x00));
            }
        }
        index += byte_count;
    }
}

public void MultiBlockWrite(List<DataParameter> w_address, List<DataParameter> b_address)
{
    List<byte> bytes = new List<byte> {
        // 头部信息
        0x50, 0x00,
        0x00,
        0xFF,
        0xFF, 0x03,
        0x00,

        0x40, 0x00,// 长度需要调整
        0x10, 0x00,

        0x06, 0x14,
        0x00, 0x00,// 子指令

        (byte)w_address.Count,
        (byte)b_address.Count,
    };
    foreach (var address in w_address.Union(b_address))
    {
        byte[] addr_bytes = this.GetAddress(address.Address, address.Area);
        // 拼接请求的地址
        bytes.AddRange(addr_bytes);
        bytes.Add((byte)address.Area);

        if (address.Datas!.Count % 2 > 0)
            throw new Exception("数据准备有误");

        if (new Areas[] { Areas.X, Areas.Y, Areas.M }.Contains(address.Area))
        {
            if (address.Datas == null || address.Datas.Count % 16 != 0)
                throw new Exception("需要写入的数据有异常,无法写入");
            byte[] data_bytes = new byte[address.Datas.Count / 8];
            int index = 0;
            // 将16个状态字节,转换成两个字节,一个字节8个位进行汇总
            for (int i = 0; i < address.Datas.Count; i++)
            {
                if (i > 0 && i % 8 == 0) index++;
                byte bit = (byte)(address.Datas[i] == 0x01 ? 0x01 : 0x00);
                data_bytes[index] |= (byte)(bit << (i % 8));
            }
            bytes.AddRange(data_bytes);// 将
        }
        else
        {
            if (address.Datas == null || address.Datas.Count != 4)
                throw new Exception("需要写入的数据有异常,无法写入");
            bytes.AddRange(address.Datas);
        }
    }

    int len = w_address.Union(b_address).Sum(a => a.Datas!.Count / 2 + 5) + 8;
    bytes[7] = (byte)(len % 256);
    bytes[8] = (byte)(len / 256 % 256);

    byte[] resp = this.SendAndReceive(bytes.ToArray());
    // 检查响应状态
    this.CheckResponse(resp);
}

多块的代码未测试

7. PLC启停

public enum ExecuteType
{
    Normal = 0x01,
    Force = 0x03
}

public enum CleanMode
{
    Normal = 0x00,
    WithoutLock = 0x01,
    All = 0x02
}

// PLC 启停
public void PlcRun(ExecuteType et = ExecuteType.Normal, CleanMode cm = CleanMode.Normal)
{
    byte[] bytes = new byte[] {
        // 头部
        0x50,0x00,
        0x00,
        0xFF,
        0xFF,0x03,
        0x00,

        0x0A,0x00,// 长度需要调整
        0x10,0x00,

        0x01,0x10,
        0x00,0x00,
        (byte)et,0x00,// 是否强制执行:0x01不强制执行   0x03强制执行
        (byte)cm,// 清除模式 :00 不清空;01 清空锁存以外;02 清空全部
        0x00// 默认值
    };
    byte[] resp = this.SendAndReceive(bytes);

    this.CheckResponse(resp);
}

public void PlcStop(ExecuteType et = ExecuteType.Normal)
{
    byte[] bytes = new byte[] {
        // 头部
        0x50,0x00,
        0x00,
        0xFF,
        0xFF,0x03,
        0x00,

        0x08,0x00,// 长度需要调整
        0x10,0x00,

        0x02,0x10,
        0x00,0x00,
        (byte)et,0x00,// 是否强制执行:0x01不强制执行   0x03强制执行
    };

    byte[] resp = this.SendAndReceive(bytes);

    this.CheckResponse(resp);
}

8. 测试

static void CustomLibTest()
{
    Mc3E mc3E = new Mc3E("192.168.174.128", 6000);
    mc3E.Open();

    // 数据转换测试
    List<ushort> values = new List<ushort>() {
        111,222,333,444
    };

    byte[] v_bytes = mc3E.GetBytes<ushort>(values.ToArray());
    mc3E.Write(v_bytes, Areas.D, "100");

    //byte[] v_bytes = mc3E.Read(Areas.D, "100", 4);
    //byte[] v_bytes = mc3E.Read("D100", 4);
    //byte[] v_bytes = mc3E.Read("X1A", 16, RequestType.BIT);
    var vs = mc3E.GetDatas<bool>(v_bytes);

    // 批量读取测试--------------------------------------------
    byte[] result = mc3E.Read(Base.Areas.D, "100", 10);
    // 007B  

    result = mc3E.Read(Base.Areas.X, "100", 10, Base.RequestType.BIT);
    result = mc3E.Read(Base.Areas.X, "1A0", 3, Base.RequestType.BIT);
    // 10个状态

    Console.WriteLine(string.Join(" ", result.Select(b => b.ToString("X2"))));

    // 批量写入测试----------------------------------------------
    mc3E.Write(new byte[] { 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01 },
        Base.Areas.X, "8", Base.RequestType.BIT);
    mc3E.Write(new byte[] { 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01 },
        Base.Areas.D, "8", Base.RequestType.WORD);
    mc3E.Write(new byte[] { 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01 },
        Base.Areas.Y, "abde", Base.RequestType.WORD);

    // 随机读取测试----------------------------------------------
    List<DataParameter> w = new List<DataParameter>
    {
        new DataParameter{ Area=Areas.D,Address="100"},
        new DataParameter{ Area=Areas.X,Address="10"},// 地址按照16的倍数进行指定
    };
    List<DataParameter> dw = new List<DataParameter>
    {
        new DataParameter{ Area=Areas.D,Address="120"},
        new DataParameter{ Area=Areas.M,Address="16"},// 地址按照16的倍数进行指定
    };
    mc3E.RandomRead(w, dw);

    // 随机位写入测试----------------------------------------------
    List<DataParameter> bit = new List<DataParameter>
    {
        new DataParameter{ Area=Areas.X,Address="1A",Datas=new List<byte>{ 0x01} },
        new DataParameter{ Area=Areas.M,Address="10",Datas=new List<byte>{ 0x01} },// 地址按照16的倍数进行指定
    };
    mc3E.RandomWriteBit(bit);

    // 随机字写入测试----------------------------------------------
    List<DataParameter> word = new List<DataParameter>
    {
        new DataParameter{ Area=Areas.D,Address="10",Datas=new List<byte>{ 0x01,0x02} },
        new DataParameter{ Area=Areas.M,Address="32",Datas=new List<byte>{ 0x01,0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01 } },// 地址按照16的倍数进行指定
    };
    List<DataParameter> dword = new List<DataParameter>
    {
        new DataParameter{ Area=Areas.D,Address="20",Datas=new List<byte>{ 0x01,0x02,0x03,0x04} },
        new DataParameter{ Area=Areas.X,Address="20",Datas=new List<byte>{ 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01 } },// 地址按照16的倍数进行指定
    };
    mc3E.RandomWrite(word, dword);

    // 多块成批读取测试
    List<DataParameter> wordBlock = new List<DataParameter>
    {
        new DataParameter{ Area=Areas.D,Address="10",Count=3},
        new DataParameter{ Area=Areas.M,Address="0",Count=2},// 地址按照16的倍数进行指定
    };
    List<DataParameter> bitBlock = new List<DataParameter>
    {
        new DataParameter{ Area=Areas.Y,Address="0",Count=2 },
        new DataParameter{ Area=Areas.X,Address="20",Count=1},// 地址按照16的倍数进行指定
    };
    mc3E.MultiBlockRead(wordBlock, bitBlock);

    wordBlock = new List<DataParameter>
    {
        new DataParameter{ Area=Areas.D,Address="10",Datas=new List<byte>{ 0x01,0x02,0x03,0x04} },
        new DataParameter{ Area=Areas.M,Address="0",Datas=new List<byte>{ 0xFF,0xFF} },// 地址按照16的倍数进行指定
    };
    bitBlock = new List<DataParameter>
    {
        new DataParameter{ Area=Areas.Y,Address="0",Datas=new List<byte>{ 0xFF,0xFF} },
        new DataParameter{ Area=Areas.X,Address="20",Datas=new List<byte>{ 0xFF,0xFF} },// 地址按照16的倍数进行指定
    };
    mc3E.MultiBlockWrite(wordBlock, bitBlock);           
    
    mc3E.PlcRun(cm: CleanMode.All);
    mc3E.PlcStop();
}