第四章:上位机软件开发——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回个数据。我在一条老式清洗线上用过,几百个点位,跑得很稳。

核心流程就三步:

  1. 建立TCP连接(端口502)
  2. 构造Modbus报文(功能码+起始地址+数据长度)
  3. 解析响应数据

给你看个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}");
    }
}
注意:Modbus没有安全机制。我曾经在客户现场遇到过,有人误发了写指令,把配方参数改了。建议你在上位机里加个“写保护”开关。

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()
我的经验:OPC UA的地址空间(Address Space)是树形结构。第一次对接时,让PLC工程师把变量名按“设备/模块/参数”的层级组织好,后面维护会省很多事。

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")
避坑指南:我曾经遇到过日志文件把磁盘写满的情况。一定要加日志轮转(Rotating),或者设置磁盘使用上限。另外,日志里别写中文,有些PLC的编码不统一,会乱码。

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()
注意:多线程最怕死锁和资源竞争。我建议你给每个线程起个有意义的名字(比如"DataCollector"、"UIRefresher"),调试时一眼就能看出问题。

4.5 本章知识体系总览

下面这张图,是我自己整理的上位机开发核心逻辑。你照着这个框架搭,基本不会漏东西:

上位机软件核心架构 通信层 OPC UA (订阅/轮询) Modbus TCP (请求/响应) SECS/GEM (半导体专用) 数据层 数据采集 (轮询/订阅) 日志记录 (结构化/轮转) 缓存队列 (Queue) 业务逻辑层 报警判断 (阈值/趋势) 配方管理 (读写/校验) 状态机 (FSM) 展示层 (UI) WPF/WinForms (C#) PyQt/Tkinter (Python) Web Dashboard (可选)

这张图里,从上到下是数据流的方向。通信层拿到原始数据,数据层做缓存和持久化,业务层做判断和决策,最后展示层让人看懂。每一层之间用队列或事件解耦,这样改一层不影响其他层。

最后说一句:上位机开发没有银弹。C#在Windows生态下集成度高,Python在数据处理和原型验证上快。我的建议是:核心通信用C#,数据分析脚本用Python,两者通过文件或数据库交换数据。这样各取所长,项目推进最快。

公众号:蓝海资料掘金营,微信deep3321