GKMLT通讯工具箱(WPF MVVM) - 04-三菱MC通讯

目录

  • [1. 通讯协议概述](#1. 通讯协议概述)
  • [2. 协议栈结构](#2. 协议栈结构)
  • [3. 报文格式详解](#3. 报文格式详解)
  • [4. 通讯原理](#4. 通讯原理)
  • [5. 调用方法与API](#5. 调用方法与API)
  • [6. 实际应用示例](#6. 实际应用示例)
  • [7. 故障排除](#7. 故障排除)

1. 通讯协议概述

1.1 三菱MC通讯简介

三菱MC通讯协议是用于与三菱Q系列PLC进行数据交换的二进制通讯协议。该协议基于TCP/IP网络,采用Qna-3E帧格式,实现了高效的PLC数据访问。相比ASCII通讯方式,二进制通讯具有更高的数据传输效率和更好的实时性。

1.2 主要特点

  • 基于TCP/IP:利用标准以太网进行通讯(默认端口5000)
  • 二进制协议:采用Qna-3E二进制帧格式,传输效率高
  • 客户端/服务器模式:PC作为客户端主动连接PLC
  • 丰富的内存区域:支持X、Y、M、D等多种内存类型
  • 灵活的数据格式:支持大端序(ABCD)和小端序(DCBA)
  • 批量操作支持:支持批量读写位数据和字数据

1.3 与ASCII协议对比

特性 二进制协议 (Qna-3E) ASCII协议 (Qna-3E)
数据格式 二进制字节 ASCII字符
传输效率 高(占用带宽少) 低(占用带宽多)
解析复杂度 中等 简单
适用场景 大数据量、实时性要求高 小数据量、调试便利

2. 协议栈结构

2.1 分层架构

复制代码
┌─────────────────────────────────────┐
│   应用层 (Application Layer)         │
│   Qna-3E Protocol (读/写指令)        │
├─────────────────────────────────────┤
│   传输层 (Transport Layer)           │
│   TCP (Transmission Control Protocol)│
├─────────────────────────────────────┤
│   网络层 (Network Layer)             │
│   IP (Internet Protocol)             │
└─────────────────────────────────────┘

2.2 各层功能说明

2.2.1 TCP传输层
  • 功能:提供可靠的面向连接的数据传输
  • 端口:默认5000(可配置)
  • 作用:确保数据完整、有序传输
2.2.2 Qna-3E应用层
  • 功能:定义具体的PLC操作指令
  • 协议类型:二进制格式
  • 操作类型:批量读取字、批量读取位、批量写入字、批量写入位

3. 报文格式详解

3.1 完整报文结构

复制代码
┌──────────────┬───────────────────┬──────────────┬──────────────┐
│  Header      │  Command Data     │   Data       │   Footer     │
│  (固定长度)   │    (指令相关)      │  (变长)       │   (固定长度)  │
└──────────────┴───────────────────┴──────────────┴──────────────┘

3.2 Qna-3E帧头格式

cpp 复制代码
struct Qna3E_Header {
    uint8_t  network_no;        // 网络编号,通常0x00
    uint8_t  dst_module_no;     // 目标模块站号,通常0x00  
    uint8_t  request_data_len;  // 请求数据长度(不包含头部本身)
    uint8_t  reserve1;          // 保留字段1
    uint8_t  reserve2;          // 保留字段2
    uint8_t  reserve3;          // 保留字段3
    uint8_t  response_data_len; // 响应数据长度
    uint8_t  reserve4;          // 保留字段4
};

示例:

复制代码
50 00 00 FF FF 03 00 00 10 00 14 00 00 10

3.3 批量读取字报文

3.3.1 读取请求报文
cpp 复制代码
struct Read_Word_Request {
    Qna3E_Header header;           // Qna-3E帧头
    uint8_t  command;               // 指令码:0x01 (批量读取)
    uint8_t  sub_command;           // 子指令码:0x04 (字读取)
    
    // 设备地址信息
    uint8_t  device_type;           // 设备类型
    uint8_t  address_bytes[3];      // 设备地址(3字节,大端序)
    uint8_t  data_length[2];        // 读取长度(2字节,大端序)
};

设备类型编码:

csharp 复制代码
// 字设备类型
0xA8: D - 数据寄存器
0xAC: W - 链接寄存器  
0xAA: Z - 变址寄存器
0xA9: TN - 定时器触点
0xAB: SN - 计数器触点
0xAD: CN - 计数器当前值

// 位设备类型  
0x9C: X - 输入继电器
0x9D: Y - 输出继电器
0x90: M - 内部继电器
0x92: L - 锁存继电器
0x94: F - 报警器
0x9A: B - 链接继电器
0x96: V - 边界继电器

实际读取请求示例(读取D100开始的10个字):

复制代码
// Header部分
50 00 00 FF FF 03 00 00     // 帧头(网络编号=0,目标站号=0,数据长度=0x0010)

// 指令部分
01                          // 指令码:0x01 (批量读取)
04                          // 子指令:0x04 (字读取)

// 设备地址信息
A8                          // 设备类型:0xA8 (D寄存器)
00 64                       // 地址:0x0064 = 100 (D100)
00 00                       // 地址高字节

// 读取长度
00 0A                       // 长度:10个字
3.3.2 读取响应报文
cpp 复制代码
struct Read_Word_Response {
    Qna3E_Header header;           // Qna-3E帧头
    uint8_t  response_code;        // 响应码:0x00 (成功)
    uint8_t  sub_command;          // 子指令码:0x04
    uint16_t end_code;             // 结束代码:0x0000 (正常结束)
    uint8_t  data[];               // 实际数据(每个字2字节)
};

实际读取响应示例:

复制代码
// Header部分  
D0 00 00 FF FF 03 00 00     // 帧头(响应网络编号=0xFF)

// 响应状态
00                          // 响应码:0x00 (成功)
04                          // 子指令:0x04
00 00                       // 结束代码:0x0000 (正常)

// 数据部分(20字节 = 10个字)
00 01 00 02 00 03 00 04     // 数据:1, 2, 3, 4, ...
00 05 00 06 00 07 00 08     //      5, 6, 7, 8, ...
00 09 00 0A                 //      9, 10

3.4 批量读取位报文

3.4.1 读取位请求报文
cpp 复制代码
struct Read_Bit_Request {
    Qna3E_Header header;           // Qna-3E帧头
    uint8_t  command;               // 指令码:0x01 (批量读取)
    uint8_t  sub_command;           // 子指令码:0x01 (位读取)
    
    // 设备地址信息
    uint8_t  device_type;           // 设备类型(位设备)
    uint8_t  address_bytes[3];      // 设备地址
    uint8_t  data_length[2];        // 读取点数
};

实际读取位请求示例(读取M0开始的16个位):

复制代码
// Header部分
50 00 00 FF FF 03 00 01     // 数据长度=0x0001

// 指令部分
01                          // 指令码:0x01
01                          // 子指令:0x01 (位读取)

// 设备地址信息
90                          // 设备类型:0x90 (M继电器)
00 00                       // 地址:0x0000 (M0)
00                          // 高字节

// 读取长度
00 10                       // 长度:16个位
3.4.2 读取位响应报文
cpp 复制代码
struct Read_Bit_Response {
    Qna3E_Header header;           // Qna-3E帧头
    uint8_t  response_code;        // 响应码
    uint8_t  sub_command;          // 子指令码
    uint16_t end_code;             // 结束代码
    uint8_t  data[];               // 位数据(每个位1字节,0x00=OFF, 0x01=ON)
};

实际读取位响应示例:

复制代码
// Header部分
D0 00 00 FF FF 03 00 11     // 数据长度=0x0011

// 响应状态
00                          // 响应码:0x00
01                          // 子指令:0x01
00 00                       // 结束代码:0x0000

// 位数据(16字节 = 16个位)
01 00 01 01 00 00 01 00     // 位值:1,0,1,1,0,0,1,0
01 01 01 01 00 00 01 01     //       1,1,1,1,0,0,1,1

3.5 批量写入字报文

3.5.1 写入请求报文
cpp 复制代码
struct Write_Word_Request {
    Qna3E_Header header;           // Qna-3E帧头
    uint8_t  command;               // 指令码:0x02 (批量写入)
    uint8_t  sub_command;           // 子指令码:0x03 (字写入)
    
    // 设备地址信息
    uint8_t  device_type;           // 设备类型
    uint8_t  address_bytes[3];      // 设备地址
    uint8_t  data_length[2];        // 写入长度
    uint8_t  data[];                // 写入数据
};

实际写入字请求示例(写入D100-D103共4个字):

复制代码
// Header部分
50 00 00 FF FF 03 00 0C     // 数据长度=0x000C

// 指令部分
02                          // 指令码:0x02 (批量写入)
03                          // 子指令:0x03 (字写入)

// 设备地址信息
A8                          // 设备类型:0xA8 (D寄存器)
00 64                       // 地址:0x0064 = 100 (D100)  
00                          // 高字节

// 写入长度
00 04                       // 长度:4个字

// 写入数据
00 01 00 02 00 03 00 04     // 数据:1, 2, 3, 4
3.5.2 写入响应报文
cpp 复制代码
struct Write_Word_Response {
    Qna3E_Header header;           // Qna-3E帧头
    uint8_t  response_code;        // 响应码
    uint8_t  sub_command;          // 子指令码
    uint16_t end_code;             // 结束代码
};

实际写入响应示例:

复制代码
// Header部分
D0 00 00 FF FF 03 00 00     // 数据长度=0x0000

// 响应状态
00                          // 响应码:0x00
03                          // 子指令:0x03
00 00                       // 结束代码:0x0000 (写入成功)

3.6 批量写入位报文

3.6.1 写入位请求报文
cpp 复制代码
struct Write_Bit_Request {
    Qna3E_Header header;           // Qna-3E帧头
    uint8_t  command;               // 指令码:0x02 (批量写入)
    uint8_t  sub_command;           // 子指令码:0x02 (位写入)
    
    // 设备地址信息
    uint8_t  device_type;           // 设备类型
    uint8_t  address_bytes[3];      // 设备地址
    uint8_t  data_length[2];        // 写入点数
    uint8_t  data[];                // 位数据
};

实际写入位请求示例(写入Y0-Y7共8个位):

复制代码
// Header部分
50 00 00 FF FF 03 00 09     // 数据长度=0x0009

// 指令部分
02                          // 指令码:0x02 (批量写入)
02                          // 子指令:0x02 (位写入)

// 设备地址信息
9D                          // 设备类型:0x9D (Y继电器)
00 00                       // 地址:0x0000 (Y0)
00                          // 高字节

// 写入长度
00 08                       // 长度:8个位

// 位数据
01 01 01 00 00 01 01 01     // 位值:1,1,1,0,0,1,1,1

4. 通讯原理

4.1 连接建立过程

复制代码
Client (PC)                    Server (PLC)
    │                               │
    │─────── TCP SYN ──────────────>│
    │<────── TCP SYN+ACK ───────────│
    │─────── TCP ACK ──────────────>│
    │                               │
    │        TCP连接已建立           │
    │                               │
    │───── Qna-3E Read Request ────>│
    │                             │
    │<─── Qna-3E Read Response ────│
    │                               │
    │        数据交换完成            │

4.2 数据读取流程

复制代码
Application    │    MelsecMcBinary     │    Network    │    PLC
    │                │                    │              │
    │ ReadByteArray  │                    │              │
    │──────────────>│                    │              │
    │                │ Build Qna-3E Frame │              │
    │                │───────────────────>│              │
    │                │                    │  TCP/IP      │
    │                │                    │(Port 5000)   │
    │                │                    │─────────────>│
    │                │                    │              │ Process
    │                │                    │<─────────────│ Request
    │                │<───────────────────│              │
    │                │ Parse Response     │              │
    │<──────────────│                    │              │
    │   Data         │                    │              │

4.3 地址解析机制

4.3.1 三菱地址格式
复制代码
[区域代码][地址编号]

示例:
D100      - 数据寄存器D100
X0        - 输入继电器X0  
Y10       - 输出继电器Y10
M100      - 内部继电器M100
TN0       - 定时器触点TN0
4.3.2 内存区域分类

位设备区域(1位):

csharp 复制代码
public enum BitDeviceType
{
    X  = 0x9C,  // 输入继电器
    Y  = 0x9D,  // 输出继电器
    M  = 0x90,  // 内部继电器
    L  = 0x92,  // 锁存继电器
    F  = 0x94,  // 报警器
    V  = 0x96,  // 边界继电器
    B  = 0x9A   // 链接继电器
}

字设备区域(16位):

csharp 复制代码
public enum WordDeviceType
{
    D  = 0xA8,  // 数据寄存器
    W  = 0xAC,  // 链接寄存器
    Z  = 0xAA,  // 变址寄存器
    TN = 0xA9,  // 定时器触点
    SN = 0xAB,  // 计数器触点
    CN = 0xAD   // 计数器当前值
}
4.3.3 地址编码规则
csharp 复制代码
// 伪代码示例
function EncodeMitsubishiAddress(area, address):
    device_type = GetDeviceTypeCode(area)
    encoded_address = new byte[3]
    
    // 地址为3字节大端序
    encoded_address[0] = (address >> 16) & 0xFF
    encoded_address[1] = (address >> 8) & 0xFF  
    encoded_address[2] = address & 0xFF
    
    return { device_type, encoded_address }

// 示例:D100编码
// device_type = 0xA8
// address = [0x00, 0x64, 0x00]  // 100的16进制表示

5. 调用方法与API

5.1 MelsecMcBinary类主要属性

csharp 复制代码
public class MelsecMcBinary : TCPBase
{
    // 通讯参数
    public string IPAddress { get; set; }      // PLC IP地址(继承自TCPBase)
    public int Port { get; set; }              // 通讯端口,默认5000(继承自TCPBase)
    
    // 协议参数
    public DataFormat DataFormat { get; set; } // 数据格式(大端/小端)
    public byte NetworkNo { get; set; }        // 网络编号,默认0x00
    public byte DstModuleNo { get; set; }      // 目标模块站号,默认0x00
    
    // 超时设置(继承自TCPBase)
    public int ConnectTimeOut { get; set; }    // 连接超时
    public int ReceiveTimeOut { get; set; }    // 接收超时
}

5.2 数据格式说明

csharp 复制代码
public enum DataFormat
{
    DCBA,  // 小端序(Little Endian)- 低字节在前
    ABCD   // 大端序(Big Endian)- 高字节在前
}

// 示例:数值0x12345678的存储方式
// DCBA格式:78 56 34 12
// ABCD格式:12 34 56 78

5.3 连接管理API

5.3.1 建立连接
csharp 复制代码
// 创建实例(指定数据格式)
MelsecMcBinary plc = new MelsecMcBinary(DataFormat.DCBA);

// 设置参数
// 注意:MelsecMcBinary没有IPAddress和Port属性
// 需要在Connect方法中传递这些参数

// 建立连接
OperateResult result = plc.Connect("192.168.1.100", 5000);
if (result.IsSuccess)
{
    // 连接成功
}
else
{
    // 连接失败,检查result.Message
}
5.3.2 断开连接
csharp 复制代码
plc.DisConnect();

5.4 数据读取API

5.4.1 批量读取字数据
csharp 复制代码
// 读取D100开始的10个字
OperateResult<byte[]> result = plc.ReadByteArray("D100", 10);

if (result.IsSuccess)
{
    byte[] data = result.Content;  // 20字节(10个字 × 2字节)
    
    // 解析数据(根据数据格式)
    short value1 = BitConverter.ToInt16(data, 0);   // D100
    short value2 = BitConverter.ToInt16(data, 2);   // D101
    // ...
}
5.4.2 批量读取位数据
csharp 复制代码
// 读取M0开始的16个位
OperateResult<bool[]> result = plc.ReadBoolArray("M0", 16);

if (result.IsSuccess)
{
    bool[] bitData = result.Content;  // 16个位
    
    // 访问位数据
    bool m0 = bitData[0];   // M0的状态
    bool m1 = bitData[1];   // M1的状态
    // ...
}

5.5 数据写入API

5.5.1 批量写入字数据
csharp 复制代码
// 准备写入数据(2字节 = 1个字)
byte[] writeData = new byte[] { 0x01, 0x00, 0x02, 0x00 };

// 写入D100-D101
OperateResult result = plc.WriteByteArray("D100", writeData);

if (result.IsSuccess)
{
    // 写入成功
}
5.5.2 批量写入位数据
csharp 复制代码
// 准备写入数据(每个位1字节,0x00=OFF, 0x01=ON)
bool[] writeBits = new bool[] { true, false, true, true, false, false, true, false };

// 写入Y0-Y7
OperateResult result = plc.WriteBoolArray("Y0", writeBits);

if (result.IsSuccess)
{
    // 写入成功
}

6. 实际应用示例

6.1 基础读写操作

csharp 复制代码
using NET8_CommModbusMcS7Lib;

class MitsubishiMCExample
{
    MelsecMcBinary plc;
    
    // 初始化连接
    public bool Initialize()
    {
        plc = new MelsecMcBinary(DataFormat.DCBA);
        
        var result = plc.Connect("192.168.1.100", 5000);
        return result.IsSuccess;
    }
    
    // 批量读取生产数据
    public byte[] ReadProductionData()
    {
        // 读取D100-D149(50个字 = 100字节)
        var result = plc.ReadByteArray("D100", 50);
        
        if (result.IsSuccess)
        {
            return result.Content;
        }
        else
        {
            Console.WriteLine($"读取失败: {result.Message}");
            return null;
        }
    }
    
    // 读取设备状态位
    public DeviceStatus ReadDeviceStatus()
    {
        // 读取M0-M7(8个状态位)
        var result = plc.ReadBoolArray("M0", 8);
        
        if (result.IsSuccess)
        {
            bool[] bits = result.Content;
            return new DeviceStatus
            {
                IsRunning = bits[0],      // M0
                HasAlarm = bits[1],       // M1
                IsManual = bits[2],       // M2
                IsError = bits[3]         // M3
            };
        }
        
        return null;
    }
    
    // 写入控制指令
    public bool WriteControlCommand(bool startCommand)
    {
        // 写入Y0(启动信号)
        bool[] writeData = new bool[] { startCommand };
        var result = plc.WriteBoolArray("Y0", writeData);
        
        return result.IsSuccess;
    }
    
    // 写入设定参数
    public bool WriteParameter(int address, short value)
    {
        // 将短整数值转换为字节数组
        byte[] writeData = BitConverter.GetBytes(value);
        
        // 构造地址字符串
        string addr = $"D{address}";
        
        var result = plc.WriteByteArray(addr, writeData);
        return result.IsSuccess;
    }
}

// 设备状态数据模型
public class DeviceStatus
{
    public bool IsRunning { get; set; }
    public bool HasAlarm { get; set; }
    public bool IsManual { get; set; }
    public bool IsError { get; set; }
}

6.2 实际应用:生产线监控系统

csharp 复制代码
public class ProductionLineMonitor
{
    private MelsecMcBinary plc;
    private bool isMonitoring;
    
    public bool StartMonitoring()
    {
        plc = new MelsecMcBinary(DataFormat.DCBA);
        
        var result = plc.Connect("192.168.1.100", 5000);
        if (!result.IsSuccess)
        {
            Console.WriteLine($"连接失败: {result.Message}");
            return false;
        }
        
        isMonitoring = true;
        Task.Run(() => MonitorLoop());
        return true;
    }
    
    private void MonitorLoop()
    {
        while (isMonitoring)
        {
            try
            {
                // 读取生产数据
                var productionData = ReadProductionData();
                
                // 读取设备状态
                var deviceStatus = ReadDeviceStatus();
                
                // 更新界面显示
                UpdateUI(productionData, deviceStatus);
                
                // 检查报警状态
                if (deviceStatus?.HasAlarm == true)
                {
                    HandleAlarm();
                }
                
                Thread.Sleep(500); // 500ms采集周期
            }
            catch (Exception ex)
            {
                Console.WriteLine($"监控异常: {ex.Message}");
            }
        }
    }
    
    private ProductionData ReadProductionData()
    {
        // 读取D100-D119(20个字 = 40字节)
        var rawData = plc.ReadByteArray("D100", 20);
        
        if (rawData.IsSuccess)
        {
            byte[] data = rawData.Content;
            
            return new ProductionData
            {
                Temperature = BitConverter.ToInt16(data, 0) / 10.0f,
                Pressure = BitConverter.ToInt16(data, 2) / 100.0f,
                FlowRate = BitConverter.ToInt16(data, 4),
                ProductCount = BitConverter.ToUInt16(data, 6),
                Speed = BitConverter.ToInt16(data, 8),
                CurrentProduct = BitConverter.ToInt16(data, 10)
            };
        }
        
        return null;
    }
    
    private DeviceStatus ReadDeviceStatus()
    {
        // 读取X0-X7(8个输入位)
        var result = plc.ReadBoolArray("X0", 8);
        
        if (result.IsSuccess)
        {
            bool[] bits = result.Content;
            return new DeviceStatus
            {
                IsRunning = bits[0],      // X0: 运行信号
                HasAlarm = bits[1],       // X1: 报警信号
                IsManual = bits[2],       // X2: 手动模式
                IsError = bits[3],        // X3: 错误状态
                IsReady = bits[4],        // X4: 就绪状态
                IsBusy = bits[5],         // X5: 忙碌状态
                IsComplete = bits[6],     // X6: 完成信号
                IsHome = bits[7]          // X7: 原点信号
            };
        }
        
        return null;
    }
    
    private void HandleAlarm()
    {
        // 读取报警代码(D200)
        var alarmCodeData = plc.ReadByteArray("D200", 1);
        if (alarmCodeData.IsSuccess)
        {
            short alarmCode = BitConverter.ToInt16(alarmCodeData.Content, 0);
            Console.WriteLine($"报警发生!代码: {alarmCode}");
            
            // 执行报警处理逻辑
            // ...
        }
    }
    
    private void UpdateUI(ProductionData data, DeviceStatus status)
    {
        Console.WriteLine($"温度: {data?.Temperature}°C, 压力: {data?.Pressure}Bar");
        Console.WriteLine($"运行: {status?.IsRunning}, 报警: {status?.HasAlarm}");
    }
    
    public void StopMonitoring()
    {
        isMonitoring = false;
        plc?.DisConnect();
    }
}

// 生产数据模型
public class ProductionData
{
    public float Temperature { get; set; }    // 温度
    public float Pressure { get; set; }       // 压力
    public int FlowRate { get; set; }         // 流量
    public ushort ProductCount { get; set; }  // 产品计数
    public short Speed { get; set; }          // 速度
    public short CurrentProduct { get; set; } // 当前产品号
}

6.3 高级应用:配方管理系统

csharp 复制代码
public class RecipeManager
{
    private MelsecMcBinary plc;
    
    public RecipeManager()
    {
        plc = new MelsecMcBinary(DataFormat.DCBA);
    }
    
    // 从PLC读取配方
    public Recipe ReadRecipe(int recipeNumber)
    {
        // 配方存储在D1000开始,每个配方占用50个字(100字节)
        int baseAddress = 1000 + (recipeNumber * 50);
        
        var result = plc.ReadByteArray($"D{baseAddress}", 50);
        
        if (result.IsSuccess)
        {
            return ParseRecipe(result.Content, recipeNumber);
        }
        
        return null;
    }
    
    // 写入配方到PLC
    public bool WriteRecipe(Recipe recipe)
    {
        int baseAddress = 1000 + (recipe.RecipeNumber * 50);
        byte[] recipeData = BuildRecipeData(recipe);
        
        var result = plc.WriteByteArray($"D{baseAddress}", recipeData);
        return result.IsSuccess;
    }
    
    // 解析配方数据
    private Recipe ParseRecipe(byte[] data, int recipeNumber)
    {
        return new Recipe
        {
            RecipeNumber = recipeNumber,
            RecipeName = Encoding.ASCII.GetString(data, 0, 20).TrimEnd('\0'),
            Temperature = BitConverter.ToInt16(data, 20) / 10.0f,
            Pressure = BitConverter.ToInt16(data, 22) / 100.0f,
            Speed = BitConverter.ToInt16(data, 24),
            Duration = BitConverter.ToUInt16(data, 26),
            MixerSpeed = BitConverter.ToInt16(data, 28),
            CoolingTemp = BitConverter.ToInt16(data, 30) / 10.0f,
            HeatingTemp = BitConverter.ToInt16(data, 32) / 10.0f,
            Enabled = data[34] != 0
        };
    }
    
    // 构建配方数据
    private byte[] BuildRecipeData(Recipe recipe)
    {
        byte[] data = new byte[100];  // 50个字
        
        // 写入配方名称(20字节)
        byte[] nameBytes = Encoding.ASCII.GetBytes(recipe.RecipeName.PadRight(20, '\0'));
        Array.Copy(nameBytes, 0, data, 0, 20);
        
        // 写入参数(从偏移20开始)
        BitConverter.GetBytes((short)(recipe.Temperature * 10)).CopyTo(data, 20);
        BitConverter.GetBytes((short)(recipe.Pressure * 100)).CopyTo(data, 22);
        BitConverter.GetBytes(recipe.Speed).CopyTo(data, 24);
        BitConverter.GetBytes((ushort)recipe.Duration).CopyTo(data, 26);
        BitConverter.GetBytes(recipe.MixerSpeed).CopyTo(data, 28);
        BitConverter.GetBytes((short)(recipe.CoolingTemp * 10)).CopyTo(data, 30);
        BitConverter.GetBytes((short)(recipe.HeatingTemp * 10)).CopyTo(data, 32);
        data[34] = (byte)(recipe.Enabled ? 1 : 0);
        
        return data;
    }
    
    // 选择当前配方
    public bool SelectRecipe(int recipeNumber)
    {
        // 将配方编号写入D500
        byte[] recipeNumData = BitConverter.GetBytes((short)recipeNumber);
        var result = plc.WriteByteArray("D500", recipeNumData);
        
        if (result.IsSuccess)
        {
            // 触发配方加载信号(Y10)
            bool[] signalData = new bool[] { true };
            plc.WriteBoolArray("Y10", signalData);
            
            Thread.Sleep(100);
            
            // 清除信号
            signalData[0] = false;
            plc.WriteBoolArray("Y10", signalData);
            
            return true;
        }
        
        return false;
    }
}

// 配方数据模型
public class Recipe
{
    public int RecipeNumber { get; set; }
    public string RecipeName { get; set; }
    public float Temperature { get; set; }    // 温度
    public float Pressure { get; set; }       // 压力
    public short Speed { get; set; }          // 速度
    public ushort Duration { get; set; }      // 持续时间
    public short MixerSpeed { get; set; }     // 搅拌速度
    public float CoolingTemp { get; set; }    // 冷却温度
    public float HeatingTemp { get; set; }    // 加热温度
    public bool Enabled { get; set; }         // 是否启用
}

7. 故障排除

7.1 常见问题与解决方案

7.1.1 连接失败

症状 :Connect()方法返回失败
可能原因

  • IP地址错误
  • PLC未开机或网络不通
  • 端口号不正确(默认5000)
  • 防火墙阻止连接

解决方案

csharp 复制代码
// 1. 检查网络连接
Ping ping = new Ping();
var reply = ping.Send("192.168.1.100");
if (reply.Status != IPStatus.Success)
{
    Console.WriteLine("网络不通,请检查IP地址和网络连接");
    return;
}

// 2. 尝试不同端口
foreach (int port in new[] { 5000, 5001, 5002 })
{
    var result = plc.Connect("192.168.1.100", port);
    if (result.IsSuccess)
    {
        Console.WriteLine($"连接成功,端口: {port}");
        break;
    }
}

// 3. 检查防火墙设置
// 确保允许应用程序访问网络和端口5000
7.1.2 读取数据返回错误

症状 :ReadByteArray()返回失败或ErrorCode不为0
可能原因

  • 地址不存在
  • 地址超出范围
  • PLC处于停止状态
  • 地址类型错误

解决方案

csharp 复制代码
// 安全读取函数
public byte[] SafeReadData(string address, int length)
{
    // 验证地址格式
    if (string.IsNullOrEmpty(address) || address.Length < 2)
    {
        Console.WriteLine("地址格式错误");
        return null;
    }
    
    // 验证长度
    if (length < 1 || length > 1000)
    {
        Console.WriteLine("读取长度超出范围");
        return null;
    }
    
    // 分批读取大数据
    const int maxBatchSize = 120;
    List<byte> allData = new List<byte>();
    
    int currentAddress = ExtractAddressNumber(address);
    string areaCode = address.Substring(0, 1);
    
    for (int offset = 0; offset < length; offset += maxBatchSize)
    {
        int currentBatch = Math.Min(maxBatchSize, length - offset);
        string currentAddr = $"{areaCode}{currentAddress + offset}";
        
        var result = plc.ReadByteArray(currentAddr, currentBatch);
        
        if (!result.IsSuccess)
        {
            Console.WriteLine($"读取失败: {result.Message}");
            return null;
        }
        
        allData.AddRange(result.Content);
    }
    
    return allData.ToArray();
}

// 提取地址数字
private int ExtractAddressNumber(string address)
{
    string numericPart = address.Substring(1);
    if (int.TryParse(numericPart, out int addressNum))
    {
        return addressNum;
    }
    return 0;
}
7.1.3 数据格式错误

症状 :读取的数据与PLC显示不一致
可能原因

  • 数据格式不匹配(大端/小端)
  • 数据类型解析错误
  • 缩放因子未考虑

解决方案

csharp 复制代码
// 数据格式转换工具类
public class MitsubishiDataConverter
{
    // 根据数据格式读取整数
    public static short ReadInt16(byte[] data, int offset, DataFormat format)
    {
        if (format == DataFormat.ABCD)
        {
            // 大端序:高字节在前
            return (short)((data[offset] << 8) | data[offset + 1]);
        }
        else
        {
            // 小端序:低字节在前
            return (short)(data[offset] | (data[offset + 1] << 8));
        }
    }
    
    // 根据数据格式读取浮点数
    public static float ReadSingle(byte[] data, int offset, DataFormat format)
    {
        byte[] temp = new byte[4];
        
        if (format == DataFormat.ABCD)
        {
            // 大端序
            temp[0] = data[offset];
            temp[1] = data[offset + 1];
            temp[2] = data[offset + 2];
            temp[3] = data[offset + 3];
        }
        else
        {
            // 小端序
            temp[0] = data[offset + 3];
            temp[1] = data[offset + 2];
            temp[2] = data[offset + 1];
            temp[3] = data[offset];
        }
        
        return BitConverter.ToSingle(temp, 0);
    }
    
    // 写入数据时格式转换
    public static byte[] ConvertToDataFormat(short value, DataFormat format)
    {
        byte[] data = new byte[2];
        
        if (format == DataFormat.ABCD)
        {
            // 大端序
            data[0] = (byte)((value >> 8) & 0xFF);
            data[1] = (byte)(value & 0xFF);
        }
        else
        {
            // 小端序
            data[0] = (byte)(value & 0xFF);
            data[1] = (byte)((value >> 8) & 0xFF);
        }
        
        return data;
    }
}

7.2 性能优化

7.2.1 批量操作优化
csharp 复制代码
// 不好的做法:多次单独读取
public void BadReadExample()
{
    for (int i = 0; i < 100; i++)
    {
        var result = plc.ReadByteArray($"D{i}", 1);  // 100次通讯
    }
}

// 好的做法:一次批量读取
public void GoodReadExample()
{
    var result = plc.ReadByteArray("D0", 100);  // 1次通讯
    
    if (result.IsSuccess)
    {
        byte[] allData = result.Content;
        // 处理100个字的数据
    }
}
7.2.2 连接管理优化
csharp 复制代码
public class MitsubishiConnectionManager
{
    private static Dictionary<string, MelsecMcBinary> connections = 
        new Dictionary<string, MelsecMcBinary>();
    
    public static MelsecMcBinary GetConnection(string ipAddress, int port = 5000)
    {
        string key = $"{ipAddress}:{port}";
        
        if (!connections.ContainsKey(key))
        {
            var plc = new MelsecMcBinary(DataFormat.DCBA);
            var result = plc.Connect(ipAddress, port);
            
            if (result.IsSuccess)
            {
                connections[key] = plc;
            }
            else
            {
                throw new Exception($"连接失败: {result.Message}");
            }
        }
        
        return connections[key];
    }
    
    public static void CloseAllConnections()
    {
        foreach (var plc in connections.Values)
        {
            plc.DisConnect();
        }
        connections.Clear();
    }
    
    public static void CloseConnection(string ipAddress, int port = 5000)
    {
        string key = $"{ipAddress}:{port}";
        
        if (connections.ContainsKey(key))
        {
            connections[key].DisConnect();
            connections.Remove(key);
        }
    }
}
7.2.3 异步操作优化
csharp 复制代码
public class AsyncMitsubishiOperations
{
    private MelsecMcBinary plc;
    
    public async Task<ProductionData> ReadProductionDataAsync()
    {
        return await Task.Run(() =>
        {
            var result = plc.ReadByteArray("D100", 20);
            
            if (result.IsSuccess)
            {
                return ParseProductionData(result.Content);
            }
            
            return null;
        });
    }
    
    public async Task<bool> WriteDataAsync(string address, byte[] data)
    {
        return await Task.Run(() =>
        {
            var result = plc.WriteByteArray(address, data);
            return result.IsSuccess;
        });
    }
    
    public async Task<List<ProductionData>> ReadMultipleProductionLinesAsync()
    {
        var tasks = new List<Task<ProductionData>>();
        
        // 读取多条生产线数据
        for (int i = 0; i < 5; i++)
        {
            int lineId = i;
            tasks.Add(Task.Run(() => ReadProductionLine(lineId)));
        }
        
        var results = await Task.WhenAll(tasks);
        return results.Where(data => data != null).ToList();
    }
    
    private ProductionData ReadProductionLine(int lineId)
    {
        int address = 100 + (lineId * 20);
        var result = plc.ReadByteArray($"D{address}", 20);
        
        return result.IsSuccess ? ParseProductionData(result.Content) : null;
    }
    
    private ProductionData ParseProductionData(byte[] data)
    {
        // 数据解析逻辑
        return new ProductionData
        {
            Temperature = BitConverter.ToInt16(data, 0) / 10.0f,
            Pressure = BitConverter.ToInt16(data, 2) / 100.0f,
            // ...
        };
    }
}

8. 总结

三菱MC通讯协议是一个高效、可靠的工业通讯协议,特别适合与三菱Q系列PLC进行数据交换。通过理解其Qna-3E帧格式和报文结构,开发者可以实现稳定的PLC数据访问。

关键要点:

  1. 协议特点:基于TCP/IP的二进制协议,采用Qna-3E帧格式
  2. 报文结构:帧头 + 指令数据 + 实际数据 + 帧尾
  3. 内存区域:区分位设备(X/Y/M等)和字设备(D/W/Z等)
  4. 数据格式:支持大端序(ABCD)和小端序(DCBA)
  5. 批量操作:支持批量读写位数据和字数据,提高通讯效率
  6. 错误处理:完善的异常处理和重试机制
  7. 性能优化:合理使用批量操作和连接管理

最佳实践:

  • 使用批量操作减少通讯次数
  • 根据PLC设置选择正确的数据格式
  • 实现完善的错误处理和日志记录
  • 对于大数据传输,采用分批读取策略
  • 使用连接池管理多个PLC连接
  • 实现心跳机制保持连接活跃
相关推荐
不会编程的懒洋洋4 小时前
WPF XAML+布局+控件
xml·开发语言·c#·视觉检测·wpf·机器视觉·视图
雨浓YN5 小时前
GKMLT通讯工具箱(WPF MVVM) - 06-OPCUA通讯
wpf
雨浓YN5 小时前
GKMLT通讯工具箱(WPF MVVM) - 03-西门子S7通讯
wpf
雨浓YN8 小时前
GKMLT通讯工具箱(WPF MVVM) - 05-WebAPI通讯
wpf
软泡芙1 天前
【WPF 】MVVM 设计模式在 WPF 中的实战应用
设计模式·wpf
张小俊_1 天前
WPF 跨线程 UI 更新与硬编码赋值引发的 Bug 排查
c#·bug·wpf
七夜zippoe2 天前
DolphinDB在工业物联网中的优势
物联网·wpf·工业物联网·优势·dolphindb
heimeiyingwang2 天前
【架构实战】观察者模式在分布式系统中的应用
观察者模式·架构·wpf
bugcome_com2 天前
WPF + Microsoft.ToolKit.Mvvm 技术指南与实战项目
microsoft·wpf