1. 项目背景与核心价值
三菱FX系列PLC作为工业自动化领域的经典控制器,在国内中小型自动化项目中占据着重要地位。而C#凭借其强大的Windows窗体开发能力和稳定的.NET框架,成为上位机开发的首选语言之一。这个开源项目将两者结合,提供了一个可直接用于工业现场通信的参考实现。
我在2018年参与某包装生产线改造时,首次接触到这个源码框架。当时客户要求在不更换原有FX3U PLC的情况下,实现生产数据可视化。这个项目中的串口通信模块为我节省了近两周的开发时间,其稳定性和易扩展性给我留下了深刻印象。
2. 开发环境搭建要点
2.1 硬件准备清单
- 三菱FX系列PLC(支持RS232/485接口)
- USB转串口线(推荐使用FTDI或CH340芯片)
- 24V直流电源(为PLC供电)
- 标准9针串口线(或根据PLC型号定制的通信电缆)
注意:市面上常见的PL2303芯片转换器在Win10系统下可能出现驱动兼容性问题,建议优先选用FTDI方案。
2.2 软件环境配置
- 安装Visual Studio 2019/2022(社区版即可)
- 添加NuGet包:
bash复制
Install-Package SerialPortLib -Version 2.0.0 Install-Package M2Mqtt -Version 4.3.0 - 三菱GX Works2(用于PLC程序验证)
- 串口调试助手(推荐XCOM或VSPD虚拟串口工具)
2.3 常见环境问题排查
当遇到通信异常时,建议按以下顺序检查:
- 设备管理器确认COM端口号与程序设置一致
- 用万用表测量PLC通信端口的TX/RX信号电压(应在±12V范围内)
- 通过GX Works2的通信测试功能验证PLC端口正常
- 使用串口调试助手进行自发自收测试
3. 核心通信协议解析
3.1 三菱专用协议格式
典型的读写命令帧结构如下:
code复制STX 命令码 地址区 数据长度 数据 ETX 校验和
示例读取D100开始的10个寄存器:
csharp复制byte[] cmd = new byte[] {
0x02, // STX
0x30, 0x34, // 读命令码
0x44, 0x31, 0x30, 0x30, // D100地址
0x30, 0x31, 0x30, // 读取长度10
0x03, // ETX
0x37, 0x31 // 校验和
};
3.2 校验和计算方法
采用简单的字节累加后取低8位:
csharp复制byte CalculateChecksum(byte[] data)
{
byte sum = 0;
for(int i=1; i<data.Length-2; i++) {
sum += data[i];
}
return (byte)(sum & 0xFF);
}
3.3 通信超时处理机制
建议采用三层超时保护:
- 字节间超时(50ms)
- 帧间超时(200ms)
- 全局超时(3000ms)
4. 关键代码模块详解
4.1 串口通信核心类
csharp复制public class FXSerialPort
{
private SerialPort _port;
private readonly Queue<byte[]> _sendQueue = new();
public void Open(string portName, int baudRate)
{
_port = new SerialPort(portName, baudRate, Parity.Even, 7, StopBits.One);
_port.DataReceived += DataReceivedHandler;
_port.Open();
}
private void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e)
{
byte[] buffer = new byte[_port.BytesToRead];
_port.Read(buffer, 0, buffer.Length);
// 协议解析逻辑...
}
}
4.2 数据转换工具类
csharp复制public static class DataConverter
{
public static float ToFloat(byte[] data, int startIndex)
{
// 三菱浮点数的特殊格式处理
int sign = (data[startIndex] & 0x80) == 0x80 ? -1 : 1;
int exponent = data[startIndex] & 0x7F;
int mantissa = (data[startIndex+1] << 16) | (data[startIndex+2] << 8) | data[startIndex+3];
return sign * mantissa * Math.Pow(10, exponent - 64);
}
}
4.3 线程安全队列实现
csharp复制public class ConcurrentQueue<T>
{
private readonly Queue<T> _queue = new();
private readonly object _lock = new();
public void Enqueue(T item)
{
lock(_lock) {
_queue.Enqueue(item);
}
}
public bool TryDequeue(out T result)
{
lock(_lock) {
if(_queue.Count > 0) {
result = _queue.Dequeue();
return true;
}
result = default;
return false;
}
}
}
5. 典型应用场景实现
5.1 实时数据监控界面
csharp复制public partial class MonitorForm : Form
{
private readonly Timer _refreshTimer = new();
private readonly FXSerialPort _plc = new();
public MonitorForm()
{
_refreshTimer.Interval = 500;
_refreshTimer.Tick += (s,e) => UpdateUI();
_plc.DataReceived += OnDataReceived;
}
private void UpdateUI()
{
this.Invoke((MethodInvoker)delegate {
lblTemperature.Text = _plc.GetValue("D100").ToString("F1");
progressBar1.Value = (int)_plc.GetValue("D101");
});
}
}
5.2 历史数据存储方案
推荐使用SQLite实现本地缓存:
csharp复制public class DataLogger
{
private readonly SQLiteConnection _db;
public DataLogger()
{
_db = new SQLiteConnection("Data Source=log.db");
_db.CreateTable<PlcData>();
}
public void LogData(string address, float value)
{
_db.Insert(new PlcData {
Timestamp = DateTime.Now,
Address = address,
Value = value
});
}
}
5.3 报警处理模块
csharp复制public class AlarmManager
{
private readonly Dictionary<string, Alarm> _alarms = new();
public void CheckAlarms(float value, string address)
{
if(_alarms.TryGetValue(address, out var alarm)) {
bool triggered = value > alarm.HighLimit || value < alarm.LowLimit;
if(triggered != alarm.IsActive) {
alarm.IsActive = triggered;
On
