1. 项目概述
去年接手了一个空压机系统的上位机开发项目,用C#从零搭建了一套完整的工业控制软件。这个项目虽然规模不大,但涵盖了硬件通讯、数据存储、可视化展示等工业自动化领域的典型需求,是个非常典型的"麻雀虽小五脏俱全"的案例。
整个系统需要与西门子S7-200 SMART PLC通过Modbus RTU协议进行实时通讯,采集压力、温度等工艺参数,同时将数据存储到SQL Server数据库,并提供数据可视化、报表导出、用户管理等功能。最有趣的是,为了直观展示管道压力状态,我还用GDI+实现了动态水流动画效果。
2. 核心模块设计与实现
2.1 硬件通讯模块
通讯模块是整个系统的基石,我们选择了Modbus RTU协议通过RS485串口与PLC通讯。这里有几个关键点需要注意:
- 串口配置:必须与PLC端严格匹配,包括波特率、数据位、停止位和校验位。我们的PLC使用的是9600波特率、8数据位、无校验、1停止位的配置。
csharp复制_serialPort = new SerialPort("COM3", 9600, Parity.None, 8, StopBits.One);
- 数据接收处理:采用事件驱动模式,在DataReceived事件中处理返回数据。这里要特别注意线程安全问题,所有UI更新必须通过Invoke方法回到UI线程。
csharp复制_serialPort.DataReceived += (sender, e) => {
byte[] buffer = new byte[_serialPort.BytesToRead];
_serialPort.Read(buffer, 0, buffer.Length);
if(CheckCRC(buffer)) {
float pressure = BitConverter.ToSingle(new byte[]{buffer[3],buffer[4],buffer[5],buffer[6]}, 0);
Invoke(new Action(() => pressureGauge.Value = pressure));
}
};
- CRC校验:Modbus RTU协议要求对每条报文进行CRC校验。我们实现了手动校验算法,相比使用现成库,这样更能理解校验原理,也便于调试。
csharp复制private bool CheckCRC(byte[] data){
ushort crc = 0xFFFF;
foreach(byte b in data.Take(data.Length-2)) {
crc ^= b;
for(int i=0; i<8; i++) {
crc = (crc & 1) != 0 ? (ushort)((crc >> 1) ^ 0xA001) : (ushort)(crc >> 1);
}
}
return BitConverter.ToUInt16(data, data.Length-2) == crc;
}
注意:调试通讯时,示波器是必备工具。我们曾遇到因电磁干扰导致的数据丢包问题,通过示波器观察波形最终发现是现场电机干扰导致,通过增加终端电阻和屏蔽线解决了问题。
2.2 数据存储模块
考虑到工业环境下的数据量和可靠性要求,我们选择了SQL Server作为数据库,并使用Dapper作为ORM工具。相比Entity Framework,Dapper更轻量,性能也更好,特别适合工业场景下的高频数据存取。
- 批量插入优化:实时数据采集会产生大量记录,如果单条插入会严重影响性能。我们采用了定时批量插入的策略:
csharp复制var pressureList = new ConcurrentQueue<PressureRecord>();
Timer insertTimer = new Timer(_ => {
if(pressureList.Count > 0) {
using(var conn = new SqlConnection(connStr)) {
var batch = new List<PressureRecord>();
while(pressureList.TryDequeue(out var record) && batch.Count < 5000) {
batch.Add(record);
}
conn.Execute("INSERT INTO PressureLog VALUES(@Time,@Value)",
batch,
commandTimeout:30);
}
}
}, null, 5000, 5000);
- 数据库索引优化:随着数据量增长,查询性能会明显下降。我们为常用查询字段添加了适当的索引,特别是时间戳和设备ID字段。
sql复制CREATE NONCLUSTERED INDEX IX_PressureLog_Time
ON PressureLog (RecordTime DESC)
INCLUDE (PressureValue);
- 连接池管理:工业环境下网络可能不稳定,需要合理配置连接池参数,避免连接泄漏。
csharp复制string connStr = "Server=.;Database=AirCompressor;Integrated Security=True;"
+ "Pooling=true;Min Pool Size=5;Max Pool Size=100;Connection Lifetime=300;";
2.3 用户界面与可视化
2.3.1 动态水管动画
为了让操作人员直观了解管道压力状态,我们使用GDI+实现了动态水流动画。核心是使用贝塞尔曲线绘制水管,并通过线性渐变模拟水流效果。
csharp复制protected override void OnPaint(PaintEventArgs e) {
// 计算控制点位置,根据压力值调整曲线形状
float offset = (pressure - 0.5f) * 50;
ctrl1 = new PointF(startPoint.X + 50, startPoint.Y + offset);
ctrl2 = new PointF(endPoint.X - 50, endPoint.Y - offset);
using (var path = new GraphicsPath()) {
path.AddBezier(startPoint, ctrl1, ctrl2, endPoint);
e.Graphics.DrawPath(new Pen(waterColor, 15), path);
// 流动效果通过渐变色的位置变化实现
var rect = new RectangleF(
Math.Min(startPoint.X, endPoint.X) - 20,
Math.Min(startPoint.Y, endPoint.Y) - 20,
Math.Abs(startPoint.X - endPoint.X) + 40,
Math.Abs(startPoint.Y - endPoint.Y) + 40);
var brush = new LinearGradientBrush(rect,
Color.Transparent,
Color.FromArgb(180, 0, 255, 255),
45);
e.Graphics.FillPath(brush, path);
}
// 添加压力值文本
e.Graphics.DrawString($"{pressure:0.00} MPa",
this.Font,
Brushes.Black,
new PointF((startPoint.X + endPoint.X)/2,
(startPoint.Y + endPoint.Y)/2));
}
2.3.2 报表导出功能
我们对比了NPOI、ClosedXML和EPPlus三个Excel操作库,最终选择了EPPlus,主要因为它在处理公式和条件格式方面更为方便。
csharp复制using (var pck = new ExcelPackage()) {
var ws = pck.Workbook.Worksheets.Add("压力报告");
// 填充数据
ws.Cells["A1"].LoadFromDataTable(dataTable, true);
// 设置列宽
ws.Column(1).Width = 20;
ws.Column(2).Width = 15;
// 添加条件格式
var range = ws.Cells["B2:B100"];
var cond = range.ConditionalFormatting.AddGreaterThan();
cond.Formula = "0.8";
cond.Style.Fill.BackgroundColor.Color = Color.OrangeRed;
// 保存文件
File.WriteAllBytes("压力报告.xlsx", pck.GetAsByteArray());
}
2.4 用户管理系统
工业软件的安全性同样重要,我们实现了基于角色的用户管理系统,特别注意了密码的安全存储。
- 密码哈希处理:使用SHA256加盐算法存储密码,避免明文存储的安全风险。
csharp复制public string HashPassword(string raw) {
// 每个用户有独立的盐值
string salt = Guid.NewGuid().ToString("N").Substring(0, 8);
using var sha = SHA256.Create();
byte[] bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(raw + salt));
return $"{salt}:{BitConverter.ToString(bytes).Replace("-","")}";
}
- 登录验证:
csharp复制public bool VerifyPassword(string username, string password) {
var user = GetUser(username);
if(user == null) return false;
string[] parts = user.PasswordHash.Split(':');
if(parts.Length != 2) return false;
string salt = parts[0];
string storedHash = parts[1];
using var sha = SHA256.Create();
byte[] bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(password + salt));
string computedHash = BitConverter.ToString(bytes).Replace("-","");
return storedHash.Equals(computedHash, StringComparison.OrdinalIgnoreCase);
}
3. 项目经验总结
3.1 通讯稳定性优化
工业现场环境复杂,通讯稳定性是最大的挑战。我们总结了几点经验:
-
超时重试机制:每条指令发送后设置合理的超时时间,超时后自动重试,但要注意重试次数不宜过多。
-
心跳检测:定期发送心跳指令检测通讯状态,发现异常及时报警。
-
数据缓存:网络中断时缓存数据,恢复后补传历史数据。
-
错误恢复:通讯错误后要有自动恢复机制,避免人工干预。
3.2 性能优化技巧
-
UI响应优化:数据刷新使用双缓冲技术减少闪烁,耗时操作放在后台线程。
-
数据库批处理:如前所述,批量插入大幅提升性能。
-
内存管理:及时释放GDI+资源,避免内存泄漏。
csharp复制// 正确释放资源的模式
using (var image = new Bitmap(width, height))
using (var g = Graphics.FromImage(image))
using (var brush = new SolidBrush(color))
{
// 绘图操作
}
3.3 测试与调试
工业软件测试要特别注意:
-
模拟测试:开发PLC模拟器,在没有实际设备时也能测试通讯逻辑。
-
压力测试:模拟高频率数据采集,测试系统稳定性。
-
现场调试:准备好调试工具(如串口监视器、示波器),记录详细的日志。
4. 扩展思考
这个项目虽然已经交付,但还有不少可以优化的地方:
-
分布式架构:考虑将数据采集、存储、展示分离,提高系统扩展性。
-
云端同步:增加数据同步到云端的功能,实现远程监控。
-
机器学习:采集足够数据后,可以加入异常检测算法,提前预警设备故障。
-
容器化部署:使用Docker容器部署,简化安装和升级过程。
工业软件开发最吸引人的地方在于,你写的每一行代码都能直接看到它对物理世界的影响。当看到空压机随着你的软件指令精准运转时,那种成就感是普通业务系统开发难以比拟的。这个项目让我深刻体会到,好的工业软件不仅要有扎实的技术功底,还需要对工业现场有深入的理解,两者缺一不可。
