目录
- [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数据访问。
关键要点:
- 协议特点:基于TCP/IP的二进制协议,采用Qna-3E帧格式
- 报文结构:帧头 + 指令数据 + 实际数据 + 帧尾
- 内存区域:区分位设备(X/Y/M等)和字设备(D/W/Z等)
- 数据格式:支持大端序(ABCD)和小端序(DCBA)
- 批量操作:支持批量读写位数据和字数据,提高通讯效率
- 错误处理:完善的异常处理和重试机制
- 性能优化:合理使用批量操作和连接管理
最佳实践:
- 使用批量操作减少通讯次数
- 根据PLC设置选择正确的数据格式
- 实现完善的错误处理和日志记录
- 对于大数据传输,采用分批读取策略
- 使用连接池管理多个PLC连接
- 实现心跳机制保持连接活跃