这个C#上位机模板程序是我在多个工业自动化项目中反复打磨出来的实战成果,专门用于与台达AS228系列PLC进行通信控制。不同于那些花哨的框架,这个模板采用最朴实的WinForm实现,核心目标是稳定、可靠、快速响应现场需求。
在工业现场摸爬滚打多年,我深刻体会到上位机程序最重要的不是界面多么炫酷,而是能在各种恶劣环境下稳定运行。这个模板包含了自动运行、手动调试、参数设置和页面切换四大核心功能模块,每个模块都经过实际项目验证。
特别说明:所有代码示例都来自真实项目,但隐去了客户敏感信息。寄存器地址等关键参数需要根据实际PLC配置调整。
台达AS228 PLC通常采用RS485串口通信,在C#中我们使用SerialPort类实现基础通信。以下是经过优化的串口配置方案:
csharp复制public class DeltaProtocol
{
private SerialPort _comPort;
private byte[] _readBuffer = new byte[256];
// 推荐使用这些参数与台达PLC通信
public bool Connect(string portName)
{
try {
_comPort = new SerialPort(
portName,
9600, // 波特率
Parity.Even, // 偶校验 - 这是台达协议的特殊要求
8, // 数据位
StopBits.One); // 停止位
_comPort.Handshake = Handshake.None;
_comPort.ReadTimeout = 500; // 超时设置很重要
_comPort.WriteTimeout = 500;
_comPort.DataReceived += DataReceivedHandler;
_comPort.Open();
return true;
}
catch (Exception ex) {
LogError($"串口连接失败: {ex.Message}");
return false;
}
}
}
关键参数说明:
PLC通信最复杂的就是数据接收和解析。我采用了双缓冲区的设计来应对数据分包问题:
csharp复制private void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e)
{
int bytesToRead = _comPort.BytesToRead;
byte[] tempBuffer = new byte[bytesToRead];
_comPort.Read(tempBuffer, 0, bytesToRead);
// 将新数据追加到主缓冲区
lock (_readBufferLock)
{
if (_readBufferOffset + bytesToRead > _readBuffer.Length)
{
Array.Resize(ref _readBuffer, _readBuffer.Length * 2);
}
Array.Copy(tempBuffer, 0, _readBuffer, _readBufferOffset, bytesToRead);
_readBufferOffset += bytesToRead;
}
// 启动协议解析任务
Task.Run(() => ParseReceivedData());
}
避坑经验:
自动运行是生产线的核心功能,需要稳定可靠地执行预设工艺流程。我的实现方案:
csharp复制public class AutoRunner
{
private DeltaProtocol _plc;
private CancellationTokenSource _cts;
public void StartAutoRun()
{
_cts = new CancellationTokenSource();
Task.Run(() => {
while (!_cts.IsCancellationRequested)
{
// 1. 读取PLC状态
int status = _plc.ReadStatusRegister();
// 2. 根据状态执行对应动作
switch (status)
{
case 0: // 待机状态
HandleIdleState();
break;
case 1: // 运行中
HandleRunningState();
break;
// 其他状态处理...
}
// 3. 适当延时避免CPU占用过高
Thread.Sleep(50);
}
}, _cts.Token);
}
private void HandleRunningState()
{
// 检查各传感器状态
bool sensor1 = _plc.ReadInput(0x1001);
bool sensor2 = _plc.ReadInput(0x1002);
// 根据工艺要求控制输出
if (sensor1 && !sensor2)
{
_plc.WriteCoil(0x2001, true); // 启动电机
}
// 更多逻辑...
}
}
注意事项:
手动调试是设备维护和故障排查的重要工具。我的实现采用了直接IO绑定的方式:
csharp复制// 手动控制气缸示例
private void btnCylinderForward_Click(object sender, EventArgs e)
{
if (!_plc.IsInManualMode)
{
MessageBox.Show("请先切换到手动模式");
return;
}
// 使用异步操作避免界面卡顿
Task.Run(() => {
try
{
// 置位输出
_plc.WriteCoil(0x3000, true);
// 保持200ms - 这个时间很关键
Thread.Sleep(200);
// 复位
_plc.WriteCoil(0x3000, false);
}
catch (Exception ex)
{
LogError($"气缸控制失败: {ex.Message}");
}
});
}
调试技巧:
设备参数管理是上位机的重要功能,我采用了XML存储+版本控制的方案:
csharp复制public class ParameterManager
{
private const string PARAM_DIR = "Parameters";
private const int MAX_HISTORY = 5;
public void SaveParameters(ParameterSet parameters)
{
// 确保目录存在
Directory.CreateDirectory(PARAM_DIR);
// 创建带时间戳的参数文件
string timestamp = DateTime.Now.ToString("yyyyMMddHHmmss");
string fileName = Path.Combine(PARAM_DIR, $"params_{timestamp}.xml");
// 序列化参数
var serializer = new XmlSerializer(typeof(ParameterSet));
using (var writer = new StreamWriter(fileName))
{
serializer.Serialize(writer, parameters);
}
// 清理旧版本
CleanupOldVersions();
}
private void CleanupOldVersions()
{
var files = Directory.GetFiles(PARAM_DIR, "params_*.xml")
.OrderByDescending(f => f)
.Skip(MAX_HISTORY);
foreach (var file in files)
{
try { File.Delete(file); }
catch { /* 记录日志 */ }
}
}
}
实用功能扩展:
为了解决PLC通信的竞争条件问题,我实现了基于BlockingCollection的操作队列:
csharp复制public class PlcOperationQueue
{
private BlockingCollection<PlcOperation> _queue = new BlockingCollection<PlcOperation>();
private DeltaProtocol _plc;
public PlcOperationQueue(DeltaProtocol plc)
{
_plc = plc;
StartConsumer();
}
private void StartConsumer()
{
Task.Factory.StartNew(() =>
{
foreach (var op in _queue.GetConsumingEnumerable())
{
try
{
ExecuteOperation(op);
}
catch (Exception ex)
{
LogError($"操作执行失败: {ex.Message}");
// 可以考虑加入重试逻辑
}
}
}, TaskCreationOptions.LongRunning);
}
private void ExecuteOperation(PlcOperation op)
{
switch (op.Type)
{
case OperationType.ReadCoil:
op.Result = _plc.ReadCoil(op.Address);
break;
case OperationType.WriteCoil:
_plc.WriteCoil(op.Address, op.Value);
break;
// 其他操作类型...
}
// 通知操作完成
op.CompletionSource?.TrySetResult(true);
}
public Task<bool> EnqueueReadCoil(int address)
{
var op = new PlcOperation {
Type = OperationType.ReadCoil,
Address = address,
CompletionSource = new TaskCompletionSource<bool>()
};
_queue.Add(op);
return op.CompletionSource.Task;
}
}
性能优化建议:
问题1:通信时断时续
问题2:PLC无响应
问题3:界面卡顿
通信优化:
界面优化:
内存管理:
基于WebSocket的远程监控实现方案:
csharp复制public class RemoteMonitor
{
private WebSocketServer _server;
private DeltaProtocol _plc;
private Timer _updateTimer;
public void Start(int port)
{
_server = new WebSocketServer($"ws://0.0.0.0:{port}");
_server.Start(socket => {
socket.OnOpen = () => Log("客户端连接");
socket.OnClose = () => Log("客户端断开");
socket.OnMessage = message => ProcessCommand(message);
});
// 定时推送数据
_updateTimer = new Timer(state => {
var data = CollectPlcData();
Broadcast(data);
}, null, 1000, 1000);
}
private void Broadcast(string data)
{
foreach (var session in _server.WebSocketServices["/"].Sessions)
{
try {
session.Send(data);
} catch { /* 记录日志 */ }
}
}
private string CollectPlcData()
{
// 收集需要监控的PLC数据
var data = new {
time = DateTime.Now,
status = _plc.ReadStatusRegister(),
inputs = _plc.ReadInputBlock(0x1000, 16),
outputs = _plc.ReadOutputBlock(0x2000, 16)
// 更多数据...
};
return JsonConvert.SerializeObject(data);
}
}
安全注意事项:
基于SQLite的轻量级数据记录方案:
csharp复制public class DataLogger
{
private SQLiteConnection _connection;
public void Initialize()
{
string dbFile = "DataLog.db";
bool isNew = !File.Exists(dbFile);
_connection = new SQLiteConnection($"Data Source={dbFile};Version=3;");
_connection.Open();
if (isNew)
{
CreateTables();
}
}
private void CreateTables()
{
string sql = @"
CREATE TABLE ProcessData (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Timestamp DATETIME NOT NULL,
MachineId TEXT NOT NULL,
Parameter1 REAL,
Parameter2 REAL,
-- 更多字段...
Status INTEGER
);
CREATE INDEX IX_ProcessData_Timestamp ON ProcessData(Timestamp);
CREATE INDEX IX_ProcessData_MachineId ON ProcessData(MachineId);";
using (var cmd = new SQLiteCommand(sql, _connection))
{
cmd.ExecuteNonQuery();
}
}
public void LogData(ProcessData data)
{
string sql = @"
INSERT INTO ProcessData
(Timestamp, MachineId, Parameter1, Parameter2, Status)
VALUES
(datetime('now'), @machineId, @param1, @param2, @status)";
using (var cmd = new SQLiteCommand(sql, _connection))
{
cmd.Parameters.AddWithValue("@machineId", data.MachineId);
cmd.Parameters.AddWithValue("@param1", data.Parameter1);
cmd.Parameters.AddWithValue("@param2", data.Parameter2);
cmd.Parameters.AddWithValue("@status", (int)data.Status);
cmd.ExecuteNonQuery();
}
}
}
数据分析扩展建议:
使用Inno Setup制作安装程序的配置示例:
ini复制[Setup]
AppName=PLC控制终端
AppVersion=1.0
DefaultDirName={pf}\PLCControl
DefaultGroupName=PLC控制
OutputDir=output
OutputBaseFilename=PLCControlSetup
Compression=lzma
SolidCompression=yes
[Files]
Source: "bin\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs
[Icons]
Name: "{group}\PLC控制终端"; Filename: "{app}\PLCControl.exe"
Name: "{commondesktop}\PLC控制终端"; Filename: "{app}\PLCControl.exe"
[Run]
Filename: "{app}\PLCControl.exe"; Description: "启动应用程序"; Flags: postinstall nowait
部署注意事项:
多层次的日志系统实现:
csharp复制public class Logger
{
private static Logger _instance;
private readonly List<ILogAppender> _appenders;
private Logger()
{
_appenders = new List<ILogAppender> {
new FileAppender("application.log"),
new ConsoleAppender(),
new DatabaseAppender()
};
}
public static Logger Instance => _instance ??= new Logger();
public void Log(LogLevel level, string message, Exception ex = null)
{
var entry = new LogEntry {
Timestamp = DateTime.Now,
Level = level,
Message = message,
Exception = ex
};
foreach (var appender in _appenders)
{
try {
appender.Append(entry);
} catch { /* 避免日志记录本身抛出异常 */ }
}
}
// 便捷方法
public static void Info(string message) => Instance.Log(LogLevel.Info, message);
public static void Error(string message, Exception ex = null) => Instance.Log(LogLevel.Error, message, ex);
// 其他级别...
}
public interface ILogAppender
{
void Append(LogEntry entry);
}
public class FileAppender : ILogAppender
{
private readonly string _filePath;
private readonly object _lock = new object();
public FileAppender(string filePath)
{
_filePath = filePath;
}
public void Append(LogEntry entry)
{
lock (_lock)
{
File.AppendAllText(_filePath, $"{entry.Timestamp:yyyy-MM-dd HH:mm:ss} [{entry.Level}] {entry.Message}\n");
if (entry.Exception != null)
{
File.AppendAllText(_filePath, $"Exception: {entry.Exception}\n");
}
}
}
}
日志管理建议:
将通信协议抽象为接口,便于支持多种PLC型号:
csharp复制public interface IPlcProtocol
{
bool Connect(string connectionString);
void Disconnect();
bool ReadCoil(int address);
int ReadRegister(int address);
bool WriteCoil(int address, bool value);
bool WriteRegister(int address, int value);
event EventHandler<DataReceivedEventArgs> DataReceived;
}
public class DeltaAs228Protocol : IPlcProtocol
{
// 台达AS228具体实现
}
public class SiemensS7Protocol : IPlcProtocol
{
// 西门子S7协议实现
}
// 使用时通过依赖注入
public class PlcService
{
private readonly IPlcProtocol _protocol;
public PlcService(IPlcProtocol protocol)
{
_protocol = protocol;
}
// 业务方法...
}
架构优势:
使用状态模式重构设备控制逻辑:
csharp复制public interface IDeviceState
{
void HandleStart();
void HandleStop();
void HandleError();
void HandleReset();
}
public class IdleState : IDeviceState
{
private readonly DeviceController _controller;
public IdleState(DeviceController controller)
{
_controller = controller;
}
public void HandleStart()
{
// 验证启动条件
if (_controller.CheckStartConditions())
{
_controller.ChangeState(new RunningState(_controller));
_controller.StartProcess();
}
}
// 其他方法实现...
}
public class RunningState : IDeviceState
{
// 类似实现...
}
public class DeviceController
{
private IDeviceState _currentState;
public DeviceController()
{
_currentState = new IdleState(this);
}
public void ChangeState(IDeviceState newState)
{
_currentState = newState;
}
// 委托方法到当前状态
public void Start() => _currentState.HandleStart();
public void Stop() => _currentState.HandleStop();
// 其他方法...
}
模式优点:
在工业现场,电磁干扰是常见问题。以下是我总结的抗干扰措施:
通信层面:
软件层面:
硬件层面:
准备工作:
调试步骤:
常见问题处理:
预测性维护:
能源管理:
质量管理:
通信协议:
架构演进:
数据分析:
这个模板程序经过多个项目的实战检验,虽然从技术角度看并不复杂,但正是这种朴实无华的设计让它能够在各种工业环境中稳定运行。在工业自动化领域,可靠性永远比花哨的功能更重要。