第四章:上位机软件开发——C#与Python的实战选择
做半导体设备的上位机,说白了就是给机器装个“大脑”。这个大脑要能看懂PLC在说什么,要把数据存下来,还要让人看得懂。我做了十几年自动化,换过好几套语言框架,今天跟你聊聊我的真实体会。
4.1 上位机与PLC通信:OPC UA vs Modbus TCP
通信协议这块,我建议你重点掌握两种:OPC UA和Modbus TCP。为什么是这两个?因为半导体厂里,老设备用Modbus,新设备几乎都标配OPC UA。
4.1.1 Modbus TCP——简单粗暴,但够用
Modbus TCP说白了就是“你问我答”。上位机发个请求,PLC回个数据。我在一条老式清洗线上用过,几百个点位,跑得很稳。
核心流程就三步:
- 建立TCP连接(端口502)
- 构造Modbus报文(功能码+起始地址+数据长度)
- 解析响应数据
给你看个C#的简单例子:
// 使用NModbus库,这是我个人比较推荐的
using (var client = new TcpClient("192.168.1.100", 502))
{
var factory = new ModbusFactory();
var master = factory.CreateMaster(client);
// 读取保持寄存器,起始地址0,读取10个
ushort[] values = master.ReadHoldingRegisters(1, 0, 10);
foreach (var v in values)
{
Console.WriteLine($"寄存器值: {v}");
}
}
4.1.2 OPC UA——面向未来的选择
OPC UA比Modbus复杂得多,但它解决了几个痛点:安全认证、数据模型、跨平台。现在新上的半导体设备,尤其是SECS/GEM那套体系,基本都走OPC UA。
用Python实现OPC UA客户端,我推荐opcua-asyncio库:
from opcua import Client
client = Client("opc.tcp://192.168.1.200:4840")
try:
client.connect()
# 获取节点,路径要跟PLC工程师确认
temp_node = client.get_node("ns=2;s=Temperature")
temp_value = temp_node.get_value()
print(f"当前温度: {temp_value}°C")
# 写数据
setpoint_node = client.get_node("ns=2;s=Setpoint")
setpoint_node.set_value(150.0)
finally:
client.disconnect()
4.2 数据采集与日志记录——别等出事了才后悔
数据采集这件事,我吃过亏。有一次设备报警,查了半天日志,发现采样间隔设成了10秒,关键数据全丢了。从那以后,我对日志系统有了执念。
4.2.1 采集策略:轮询 vs 订阅
| 策略 | 适用场景 | 优缺点 |
|---|---|---|
| 轮询(Polling) | 数据变化慢,点位少 | 实现简单,但浪费带宽 |
| 订阅(Subscription) | 高频数据,点位多 | 实时性好,但实现复杂 |
我个人习惯的做法是:关键参数(温度、压力、流量)用订阅,辅助参数(状态位、报警码)用轮询。这样既保证了实时性,又不会把CPU跑满。
4.2.2 日志记录——结构化才是王道
别用txt文件了,真的。我推荐用SQLite或者时序数据库。每条日志至少包含:时间戳、设备ID、参数名、值、质量戳(Good/Bad)。
Python里用logging模块就能搞定:
import logging
from logging.handlers import RotatingFileHandler
# 按大小轮转,每个文件10MB,保留5个
handler = RotatingFileHandler(
"equipment.log",
maxBytes=10*1024*1024,
backupCount=5
)
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
handler.setFormatter(formatter)
logger = logging.getLogger("DataCollector")
logger.addHandler(handler)
logger.setLevel(logging.INFO)
# 记录一条数据
logger.info(f"CHAMBER_1|Temperature|150.2|Good")
4.3 UI界面设计原则——工程师的审美也很重要
说实话,很多搞自动化的兄弟不太在意UI。但你想,操作员每天对着你的界面8小时,界面丑了真的会误操作。我总结了几条原则:
- 信息层级清晰:最重要的数据(报警、关键参数)放在左上角,次要的放右边或下面
- 颜色有含义:绿色=运行,红色=报警,黄色=警告,灰色=停机。别自己发明颜色
- 字体统一:别用花体字,微软雅黑或Segoe UI就挺好
- 操作反馈:点击按钮后,要有状态变化(变灰、转圈、文字提示)
用C#的WPF做界面,我习惯用MVVM模式。数据绑定写好了,UI和逻辑彻底分离,改起来方便。
4.4 多线程与异步处理——别让界面卡死
这是新手最容易翻车的地方。你想想看,如果数据采集和UI刷新在同一个线程里,采集卡一下,界面就卡住了。操作员会疯掉的。
4.4.1 C#中的Task与async/await
C#里我推荐用Task.Run配合async/await。这样UI线程不会被阻塞:
private async void btnStart_Click(object sender, RoutedEventArgs e)
{
btnStart.IsEnabled = false;
try
{
// 在后台线程采集数据
var data = await Task.Run(() => CollectData());
// 回到UI线程更新界面
txtTemperature.Text = data.Temperature.ToString();
}
catch (Exception ex)
{
MessageBox.Show($"采集失败: {ex.Message}");
}
finally
{
btnStart.IsEnabled = true;
}
}
private DataModel CollectData()
{
// 模拟耗时操作
Thread.Sleep(100);
return new DataModel { Temperature = 150.2 };
}
4.4.2 Python中的多线程与队列
Python有GIL锁,多线程做计算密集型任务效果不好。但做IO密集型(比如网络通信、文件读写)完全够用。我一般用queue.Queue来在线程间传数据:
import threading
import queue
import time
data_queue = queue.Queue()
def collector():
"""数据采集线程"""
while True:
# 从PLC读取数据
temp = read_from_plc()
data_queue.put(("Temperature", temp, time.time()))
time.sleep(0.1)
def logger():
"""日志记录线程"""
while True:
name, value, timestamp = data_queue.get()
log_to_database(name, value, timestamp)
# 启动线程
t1 = threading.Thread(target=collector, daemon=True)
t2 = threading.Thread(target=logger, daemon=True)
t1.start()
t2.start()
4.5 本章知识体系总览
下面这张图,是我自己整理的上位机开发核心逻辑。你照着这个框架搭,基本不会漏东西:
这张图里,从上到下是数据流的方向。通信层拿到原始数据,数据层做缓存和持久化,业务层做判断和决策,最后展示层让人看懂。每一层之间用队列或事件解耦,这样改一层不影响其他层。
公众号:蓝海资料掘金营,微信deep3321