C#与三菱PLC串口通信源码实现(基于MC协议)

一、核心代码框架

基于System.IO.Ports.SerialPort实现三菱FX系列PLC的串口通信,支持D寄存器、M位元件读写操作,包含指令帧构建、校验和计算、响应解析等关键模块。

csharp 复制代码
using System;
using System.IO.Ports;
using System.Text;
using System.Threading;

public class MitsubishiPLC
{
    private SerialPort _serialPort;
    private const byte STX = 0x02;
    private const byte ETX = 0x03;
    private const byte ENQ = 0x05;
    private const byte ACK = 0x06;
    private const byte NAK = 0x15;

    public MitsubishiPLC(string portName, int baudRate = 9600)
    {
        _serialPort = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One);
        _serialPort.DataReceived += SerialPort_DataReceived;
    }

    // 打开串口
    public bool Connect()
    {
        try
        {
            if (!_serialPort.IsOpen)
            {
                _serialPort.Open();
                Thread.Sleep(100); // 等待初始化
                return true;
            }
            return false;
        }
        catch (Exception ex)
        {
            Log($"串口打开失败: {ex.Message}");
            return false;
        }
    }

    // 关闭串口
    public void Disconnect()
    {
        if (_serialPort.IsOpen) _serialPort.Close();
    }

    // 读取D寄存器(示例读取D0-D9)
    public ushort[] ReadDRegisters(string startAddr, int count)
    {
        byte[] cmd = BuildReadCommand('D', startAddr, count);
        byte[] response = SendCommand(cmd);
        return ParseResponse(response);
    }

    // 写入单个D寄存器
    public bool WriteDRegister(string address, ushort value)
    {
        byte[] cmd = BuildWriteCommand('D', address, new ushort[] { value });
        byte[] response = SendCommand(cmd);
        return CheckAck(response);
    }

    // 构建读取指令
    private byte[] BuildReadCommand(char device, string addr, int count)
    {
        string cmdStr = $"{ENQ}{0x30}{0x30}{device}{addr.PadLeft(4, '0')}{count:X2}03";
        byte[] data = Encoding.ASCII.GetBytes(cmdStr);
        byte checksum = CalculateChecksum(data);
        return data.Concat(new byte[] { checksum, ETX }).ToArray();
    }

    // 构建写入指令
    private byte[] BuildWriteCommand(char device, string addr, ushort[] values)
    {
        string valueHex = string.Join("", values.Select(v => v.ToString("X4")));
        string cmdStr = $"{ENQ}{0x30}{0x30}{device}{addr.PadLeft(4, '0')}{values.Length:X2}{valueHex}";
        byte[] data = Encoding.ASCII.GetBytes(cmdStr);
        byte checksum = CalculateChecksum(data);
        return data.Concat(new byte[] { checksum, ETX }).ToArray();
    }

    // 发送指令并接收响应
    private byte[] SendCommand(byte[] command)
    {
        try
        {
            _serialPort.DiscardInBuffer();
            _serialPort.Write(command, 0, command.Length);
            Thread.Sleep(100); // 等待响应

            if (_serialPort.BytesToRead > 0)
            {
                byte[] response = new byte[_serialPort.BytesToRead];
                _serialPort.Read(response, 0, response.Length);
                return response;
            }
            return null;
        }
        catch (TimeoutException)
        {
            Log("通信超时");
            return null;
        }
    }

    // 校验和计算
    private byte CalculateChecksum(byte[] data)
    {
        byte sum = 0;
        foreach (byte b in data)
        {
            sum += b;
        }
        return (byte)(0 - sum); // 补码校验
    }

    // 解析响应数据
    private ushort[] ParseResponse(byte[] response)
    {
        if (response[0] != STX || response[1] != 0x30) return null;
        int length = response[2] - 0x30;
        ushort[] data = new ushort[length / 2];
        for (int i = 0; i < length; i += 2)
        {
            data[i / 2] = (ushort)(response[3 + i] << 8 | response[4 + i]);
        }
        return data;
    }

    // 校验ACK响应
    private bool CheckAck(byte[] response)
    {
        return response != null && response[0] == ACK;
    }

    // 日志记录
    private void Log(string message)
    {
        File.AppendAllText("plc_log.txt", $"{DateTime.Now}: {message}\r\n");
    }

    // 数据接收事件
    private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
    {
        // 处理实时数据流(如心跳检测)
    }
}

// 使用示例
var plc = new MitsubishiPLC("COM1");
if (plc.Connect())
{
    ushort[] data = plc.ReadDRegisters("0000", 10);
    plc.WriteDRegister("0010", 1234);
}

二、关键功能解析
  1. 指令帧结构

    • 起始符0x02 (STX)

    • 站号 :PLC地址(ASCII码,如0x30表示站号0)

    • 指令码0x30表示写操作,0x31表示读操作

    • 数据地址 :4位ASCII码(如D0000表示D寄存器起始地址)

    • 数据长度 :2位十六进制(如0A表示读取10个字)

    • 校验和:所有字节累加取补码

    • 结束符0x03 (ETX)

  2. 通信参数配置

    csharp 复制代码
    _serialPort.BaudRate = 9600;    // 波特率
    _serialPort.DataBits = 8;       // 数据位
    _serialPort.StopBits = StopBits.One; // 停止位
    _serialPort.Parity = Parity.None; // 校验位
  3. 异常处理策略

    • 超时重试:发送指令后等待100ms,未收到响应则重试3次

    • 校验失败:丢弃错误帧并记录日志

    • 连接状态检测 :定期发送ENQ指令检测PLC在线状态


三、扩展功能实现
  1. 批量读写优化

    csharp 复制代码
    public ushort[] ReadDBlock(string startAddr, int count)
    {
        string cmd = $"{ENQ}00FF{startAddr.PadLeft(4, '0')}{count:X4}03";
        byte[] data = Encoding.ASCII.GetBytes(cmd);
        byte checksum = CalculateChecksum(data);
        _serialPort.Write(data.Concat(new byte[] { checksum, ETX }).ToArray());
    
        // 等待完整响应(最大3秒)
        byte[] response = ReadCompleteResponse();
        return ParseResponse(response);
    }
  2. M位元件操作

    csharp 复制代码
    public bool WriteM(string address, bool[] bits)
    {
        string hexStr = "";
        foreach (bool bit in bits)
        {
            hexStr += bit ? "1" : "0";
        }
        byte[] cmd = Encoding.ASCII.GetBytes($"{ENQ}00FFM{address.PadLeft(4, '0')}{hexStr.Length}00{hexStr}");
        byte checksum = CalculateChecksum(cmd);
        _serialPort.Write(cmd.Concat(new byte[] { checksum, ETX }).ToArray());
        return CheckAck(_serialPort.ReadExisting().ToByteArray());
    }

四、调试与优化建议
  1. 串口调试工具

    • 使用PuttySecureCRT验证基础通信,设置参数:

      csharp 复制代码
      波特率: 9600 | 数据位:8 | 停止位:1 | 校验:无
  2. 性能监控

    csharp 复制代码
    public class PerformanceMetrics
    {
        public long MessagesSent { get; set; }
        public long MessagesReceived { get; set; }
        public double CPUUsage { get; set; }
        public double MemoryUsage { get; set; }
    }
  3. 日志记录

    csharp 复制代码
    public static class Logger
    {
        public static void Log(string message)
        {
            File.AppendAllText("plc_log.txt", 
                $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}{Environment.NewLine}");
        }
    }

参考代码 C#与三菱PLC通讯源码 串口通讯 www.youwenfan.com/contentcsq/57509.html

五、完整项目结构
bash 复制代码
PLC_Communication/
├── MitsubishiPLC/          # 核心通信类
│   ├── MitsubishiPLC.cs    # 主类
│   └── ProtocolParser.cs   # 报文解析工具
├── TestApp/                # 测试程序
│   ├── MainForm.cs         # 主界面
│   └── PLCConfigForm.cs    # 参数配置界面
└── Resources/              # 配置文件
    └── plc_params.json     # PLC参数存储

六、注意事项
  1. 通信参数一致性:确保PLC与上位机的波特率、数据位、停止位、校验位完全一致

  2. 数据缓冲区管理:批量读写时注意PLC的接收缓冲区大小限制(通常≤256字节)

  3. 实时性要求:关键控制指令建议采用中断接收方式

  4. 安全防护:工业环境中需增加光电隔离和防浪涌电路

相关推荐
二十雨辰2 小时前
[python]-函数
开发语言·python
码农水水2 小时前
中国邮政Java面试被问:容器镜像的多阶段构建和优化
java·linux·开发语言·数据库·mysql·面试·php
福楠2 小时前
C++ STL | map、multimap
c语言·开发语言·数据结构·c++·算法
ytttr8732 小时前
地震数据频率波数域变换与去噪的MATLAB实现
开发语言·matlab
墨瑾轩3 小时前
C# PictureBox:5个技巧,从“普通控件“到“图像大师“的蜕变!
开发语言·c#·swift
墨瑾轩3 小时前
WinForm PictureBox控件:3个让图片“活“起来的骚操作,90%的开发者都踩过坑!
开发语言·c#
Ethernet_Comm3 小时前
从 C 转向 C++ 的过程
c语言·开发语言·c++
难得的我们3 小时前
C++与区块链智能合约
开发语言·c++·算法
jllllyuz3 小时前
基于MATLAB的D2D通信模式选择仿真
开发语言·网络·matlab