# 上位机基础开发指南:从入门到实践

在高度自动化的今天,上位机的作用越来越关键。简单来说,它就是我们与底层硬件沟通的 “大脑” 和 “眼睛”,不仅负责高效指挥下位机干活,还给我们提供直观的监控和操作界面。

# 什么是上位机?

你可能会问,上位机到底是个啥?别急,我们一步步来看。

上位机,顾名思义,是站在更高层面的计算机,它通常具备三大核心能力:

  • 硬件通信能力:能接收硬件上报的各种信息,也能向下位机发送指令,实现精准控制;
  • 数据处理能力:将从设备采集到的原始数据进行加工处理,将它们转化为有价值的信息;
  • 人机交互能力:为我们操作人员提供一个清晰、易用的窗口,让我们能方便地与整个系统 “对话”。

# 上位机与下位机的却别

在一个典型的分层控制系统中:

  • 上位机 (Upper Computer): 通常是个人计算机 (PC)、工作站或服务器。它负责系统的整体协调、任务调度、数据显示和用户交互,它是命令的发起者;
  • 下位机 (Lower Computer): 通常是可编程逻辑控制器 (PLC)、单片机 (MCU)、嵌入式系统或其他专用控制器,它直接连接到传感器和执行器,执行上位机下达的具体指令,并采集现场数据,它是命令的执行者。

# 上位机的常见应用场景

  • 工业自动化:在工厂自动化生产线中,上位机用于监控整个生产流程,控制机器人、传送带等设备;
  • 数据采集系统:用于从各种传感器收集数据,进行实时显示、记录和分析;
  • 楼宇自动化:控制建筑物的照明、空调、安防等系统;
  • 测试与测量:在实验室或测试设备中,用于控制测试过程、记录测试数据并生成报告;
  • 嵌入式系统开发:在开发嵌入式系统时,上位机常用于程序的下载、调试和与目标板的通信。

# 在系统中的功能

在一个典型的系统中,上位机主要承担以下基础交互功能:

  • 控制指令发送:比如启动 / 停止设备、设定参数等;
  • 状态监控:实时了解设备的运行状态;
  • 数据可视化:将枯燥的数据以图表等形式直观展示出来。

# 上位机的系统组成及交互模式

上位机的系统可以主要分为硬件和软件部分。

上位机系统组成

在本次实验中,上位机的交互模式如下图:

交互模式

# 工欲善其事必先利其器:开发环境与基础框架

明确了上位机的角色后,我们来看看如何为它搭建一个坚实的 “舞台”—— 选择合适的开发工具和构建基础框架。

# 开发工具和语言大比拼

市面上有不少优秀的开发工具和编程语言,我们来对比几款主流选择:

# C# (.NET)

  • 优势:依托强大的.NET 框架,性能和稳定性表现出色,特别适合开发对可靠性要求高的工业应用。Visual Studio 提供了强大的开发、调试工具和丰富的生态系统,包括大量的第三方库和社区支持。微软近年来也在大力推广其跨平台应用开发;
  • 劣势:学习曲线相对陡峭一些,界面开发的灵活性在某些方面可能不如一些新兴技术。

# Python

  • 优势:以简洁的语法和高效的开发效率著称。对于快速原型开发、数据分析和一些轻量级应用,Python 配合 PyCharm 是非常不错的选择。它拥有庞大的社区和海量的库,跨平台性也非常好,学习门槛相对较低;
  • 劣势:在对性能要求极致的场景,或者大型复杂工程的管理和维护方面,Python 可能会遇到一些挑战。

# LabVIEW

  • 优势:一款独特的图形化编程环境,尤其在测试测量和自动化领域应用广泛。它的优势在于图形化编程带来的快速开发体验,以及与各种匹配硬件设备的无缝集成能力和良好的实时性。数据可视化也是 LabVIEW 的强项;
  • 劣势:生态相对封闭,商业授权的成本较高,对于习惯文本编程的开发者来说需要一定的适应期,而且在大型项目的协作开发和长期维护方面可能存在一些不便。

本次分享我们主要聚焦于 C#。

# 搭建你的 C# 开发环境

  1. 下载 Visual Studio:你可以从 微软官方网站 下载最新版本的 Visual Studio。
  2. 安装必要组件:
    • 务必勾选 “.NET 桌面开发” 选项,这是开发 Windows 上位机应用的基础。
    • 建议选中包含 “NuGet 包管理器” 的相关组件,NuGet 是我们管理第三方库的重要工具。

# 理解 C# 中的 “命名空间”

在 C# 中,命名空间 (Namespace) 是一个非常重要的概念,可以把它想象成代码的 “文件夹”。它的主要作用有两个:

  1. 避免命名冲突:不同团队开发的库可能包含同名的类,通过命名空间可以清晰地将它们区分开。
  2. 提供代码的组织层级:让项目结构更清晰。

我们常用的命名空间有:

  • System.Windows:主要负责图形用户界面的开发,比如我们常用的应用窗口都可以尝试用这个命名空间中的工具搭建。
  • System.Data:用于数据库交互。
  • System.IO:用于文件和流操作,包括串口通信等。
  • System.Threading.Tasks:进行并发和异步编程的核心,提供了任务并行库 (TPL) 以及非常优雅的 async/await 异步编程模式。

# 第三方库集成与依赖管理:告别重复造轮子

现代软件开发很少会从零开始造轮子,我们会大量依赖第三方库来加速开发。

  • 调用非 .NET DLL
    有时候,我们可能需要调用一些并非由 .NET 开发的库,比如一些底层的 C++ 硬件驱动库。这时,我们可以使用 DllImport 特性。这就像在 C# 代码中声明一个 “外部函数”,告诉编译器这个函数的实现在哪个 DLL 文件中。使用时,务必确保这个 DLL 文件位于程序可以找到的路径(通常是项目的 bin\Debugbin\Release 目录)。
using System.Runtime.InteropServices; 
public class NativeWrapper { 
    [DllImport("ThirdPartyLib.dll")] 
    public static extern int NativeMethod(); 
}
  • 解决 NuGet 包的 “版本冲突”
    NuGet 是.NET 平台的包管理器,极大地简化了库的查找、安装和更新。但有时,我们项目中不同的 NuGet 包可能会依赖同一个库的不同版本,这就可能导致冲突。
    我们可以通过在项目的 app.config 文件中配置 bindingRedirect 来强制使用特定版本的库,有效避免因版本不一致引发的麻烦。

# 核心模块剖析

有了坚实的基础,接下来我们深入了解上位机的核心模块,看看它们是如何协同工作的。

# 通信是桥梁:CAN 与 Ethernet 协议详解

在实验测试这类场景中,上位机需要与电机控制器等下位机进行通信,以发送控制指令、参数,并采集电机状态。
通信协议的选择至关重要,需要根据实际需求(如干扰、距离、速率等)来定。在我们的案例中,考虑到现场设备强大的电磁干扰,选择了 CAN 总线和以太网分别进行数据传输。
通信模式选择

# CAN 协议

CAN (Controller Area Network) 最初是为汽车应用而开发的,后来凭借其高可靠性和良好的实时性,逐渐成为国际标准,广泛应用于工业自动化、医疗设备等领域。

# 物理层标准

CAN 具备两种标准模式:
CAN标准模式

  • 低速 CAN (开环总线网络)
    遵循 ISO11519-2 标准,通常用于速率要求不高但传输距离较远的场合,比如最大传输距离可达 1 公里(速率为 40kbps 时),最高通讯速率为 125kbps。这种网络中,总线两根线是独立的,不形成闭环,每根总线上通常需要串联一个 2.2 千欧的电阻。

  • 高速 CAN (闭环总线网络)
    遵循 ISO11898 标准,适用于高速、短距离通信(如 40 米 / 1Mbps),最高速率 1Mbps。总线两端各需一个 120 欧姆的终端匹配电阻。

同时,与 I2C、SPI 等使用时钟信号同步的通信方式不同,CAN 是一种异步通讯机制,仅通过 CAN_High 和 CAN_Low 两根信号线以差分信号的形式进行通讯。使用差分信号进行传输具备两优势:

  • 抗干扰能力强:差分信号能有效抵御共模干扰。
  • 抑制电磁辐射:减少对周围设备的干扰。

CAN差分信号

# 位同步机制

由于是异步通信,没有单独的时钟线,CAN 节点之间需要预先约定好相同的波特率。为了在传输过程中抵抗噪声干扰、补偿晶振误差,并确保接收节点能在正确的时间点对总线电平进行采样,CAN 协议引入了 位同步机制 ,尽可能保证 “位” 作为传输过程中信息最小单位的可靠性。
每一位的时间被划分为四个段:同步段 (SS)、传播时间段 (PTS)、相位缓冲段 1 (PBS1) 和相位缓冲段 2 (PBS2)。这些段的长度以一个最小时间单位 Tq(Time Quantum)来衡量,一个完整的位由 8 到 25 个 Tq 组成。CAN 控制器能够自动调整 PTS、PBS1 和 PBS2 的长度来进行再同步,调整的上限称为再同步跳转宽度 (SJW),通常为 1 到 4 个 Tq。 NBT 代表的是一位的总 Tq 数,等于这四个段的 Tq 数之和。由此,波特率的计算公式可以表示为:

𝑁𝐵𝑇=(1+𝑛𝑃𝑇𝑆+𝑛𝑃𝐵𝑆1+𝑛𝑃𝐵𝑆2)×𝑇q𝑁𝐵𝑇=(1+𝑛_{𝑃𝑇𝑆}+𝑛_{𝑃𝐵𝑆1}+𝑛_{𝑃𝐵𝑆2} )\times 𝑇_q

Baudrate=1/NBTBaudrate = 1 / NBT

位同步

# CAN 报文

在原始数据段的前面加上传输起始标签、片选 (识别) 标签和控制标签,在数据的尾段加上 CRC 校验标签、应答标签和传输结束标签,依据特定的格式打包好,即可使用一个通道表达各种信号了,各种标签起到了协同传输的作用。当整个数据包被传输到其它设备时,只需依据格式解读,即可还原原始数据,这样的报文就被称为 CAN 的帧。
为了更有效地控制通讯,CAN 一共规定了 5 种类型的帧。

帧类型 用途
数据帧 向外发送数据
遥控帧 向远端节点请求数据
错误帧 通知远端校验错误请求重发
过载帧 用于通知远端,本节点还未做好接收准备
帧间隔 用于将数据帧、遥控帧与先前的帧分离

我们以最常用的标准数据帧为例:
数据帧 起始结束

  • 帧起始(Start of Frame, SOF)
    1 个显性 bit 位,用于通知各节点将有数据传输。
  • 帧结束(End of Frame,EOF)
    帧结束表示该该帧的结束的段。由 7 个隐性 bit 位构成。

数据帧 仲裁

  • 仲裁段
    仲裁段决定报文优先级和总线冲突解决的核心机制:当存在多个节点同时发送时,将逐位比较 ID,发送隐性位但检测到总线为显性位的节点会退出竞争,ID 最小的报文总能优先发送(非破坏性仲裁)。
    仲裁段主要包含了以下内容:
    • 11 位标识符 (ID):定义报文身份和优先级,ID 值越小,优先级越高。仲裁段是标准数据帧和拓展数据帧的主要区别,标准数据帧仅包含 11bit 的帧 ID,拓展帧则包含 29bit 的帧 ID。
      因此相对于标准帧,在 ID 足够使用时采用拓展帧可造成约 18.51%~45.45% 的总线资源浪费。
    • 远程传输请求位 (RTR):显性 (0) 为数据帧,隐性 (1) 为遥控帧。数据帧优先级高于同 ID 遥控帧。
    • 标识符扩展位 (IDE):显性 (0) 为标准帧 (11 位 ID),隐性 (1) 为扩展帧 (29 位 ID)。

数据帧 其他帧

  • 控制段 (Control Field)
    共 6 位,前两位是保留位 (r0, r1),默认为显性 (0);
    后四位是数据长度码 (DLC - Data Length Code):用二进制表示本帧数据段包含的字节数,范围从 0 到 8。
  • 数据段
    包含实际要传输的应用数据,长度由 DLC 指定,最多 8 个字节。如果 DLC 为 0,则没有数据段。
  • CRC 段
    包含 15 位的 CRC 校验码和 1 位隐性的 CRC 界定符。发送节点根据从 SOF 到数据段的内容计算 CRC 码,接收节点进行同样的计算并比较,如果不一致,则认为数据出错。
  • 应答段
    包含 2 位。第一位是 ACK 槽 (ACK Slot),第二位是 ACK 界定符 (ACK Delimiter,隐性)。发送节点在发送完 CRC 段后,会在此处发送一个隐性位。任何正确接收到报文的节点,会在 ACK 槽期间发送一个显性位作为应答。如果发送方在 ACK 槽检测到显性位,则认为报文至少被一个节点正确接收。否则,会认为发送失败并可能触发错误处理或重传。

# 实例解析

假设我们定义了一个 CAN ID 为 0x300 的数据帧,用于传输设备的温度和状态信息。
数据帧实例
在这一个数据帧中,数据段的第一个字节的高 4 位表示系统运行状态,低 4 位表示 MOS 管的开关状态;第二个字节表示一号通道控制器的温度 1,实际温度值是接收到的字节值减去一个约定的偏移量……
那么,在 C# 代码中,当我们接收到 ID 为 0x300 的数据帧后,就可以以这样的代码进行逐一解析:

if (obj.RemoteFlag == 0) // 判断是数据帧而非远程帧 
    if ((obj.ID) == 0x300) { // 查询方式进行 CAN 数据接收 
        MOSState = (byte)(obj.Data[0] & 0x0F); // 开管状态
        SYSRun   = (byte)((obj.Data[0] & 0xF0) >> 4); // 运行状态 
        TemprtQ1 = ((Int16)(obj.Data[1]) - 60); // 一通道控制器温度 1 
        TemprtQ2 = ((Int16)(obj.Data[2]) - 60); // 一通道控制器温度 2 
        TemprtQ3 = ((Int16)(obj.Data[3]) - 60); // 一通道控制器温度 3 
        TemprtQ4 = ((Int16)(obj.Data[4]) - 60); // 二通道控制器温度 1 
        TemprtQ5 = ((Int16)(obj.Data[5]) - 60); // 二通道控制器温度 2 
        TemprtQ6 = ((Int16)(obj.Data[6]) - 60); // 二通道控制器温度 3 
        TemprtM  = ((Int16)(obj.Data[7]) - 60); // 电机温度 
    
    // …… 数据处理及存储
}

# Ethernet 协议

以太网通信是我们最常用的基于 TCP/IP 协议的数据传输方式。在 C# 中,主要依赖 System.NetSystem.Net.Sockets 这两个命名空间来实现。
TCP/UDP

我们主要会用到两种传输层协议,分别是 TCP 和 UDP:

  • TCP 是一种面向连接的可靠的协议。它在数据传输前需要通过 “三次握手” 建立连接,能保证数据按序、无差错地到达对方,并且有流量控制和拥塞控制机制。因此,TCP 非常适合那些对数据完整性和可靠性要求高的应用。当然,它的网络开销也相对较大。
  • UDP 是一种无连接的不可靠的协议。它发送数据前不需要建立连接,直接将数据包扔给网络系统中,不保证数据能否到达、是否按序到达,也没有重传机制。它的优点是开销小、延迟低、传输效率高。因此,UDP 常用于那些对实时性要求高,但能容忍少量丢包的应用。

在我们的案例中,为了保证大量数据的流式传输并尽可能减少网络开销,选择了 UDP 协议,但 UDP 的校验和功能较弱,因此在应用层额外引入了数据校验机制。具体的定义见下图。
以太网通信协议

可以看到在参数定义中,我们计算了数据的 MD5 哈希值,并随数据一同发送,接收方在收到数据后也进行同样的计算并比对,以确保数据的准确性。

# UI 设计

有了强大的内核,我们还需要一个美观、易用的 “外壳”—— 用户界面。好的 UI 设计能极大提升用户体验和工作效率。

这里展示了一个典型的上位机界面,通常会根据功能划分为几个区域,合理的布局能在紧张的调试过程中快速找到所需信息并进行操作。比如,大家看到的这个示例中:

  • 左上角是 CAN 通信配置区,用来设置波特率、选择 CAN 设备等。
  • 左侧中间的核心区域是主控区,放置一些关键的启停按钮,以及重要转速设定等基本功能。
  • 再往下方是控制参数输入区,允许进行一些控制参数的调试,以备不时之需。
  • 右侧两块主要有低动态数据展示区以及流数据展示区,根据数据的时变特性将他们分类后分别置于两个区域便于监测。
  • 最下方为功能区,一般与核心控制功能关系不大,设定完成后无需进一步操作,这里提供的功能是开放该上位机的控制权限给局域网内的其他设备、保存数据到数据库 以及 进行图像刷新设置等。

上位机UI

# 流数据展示

实时数据的可视化是上位机的一大亮点,尤其是曲线图。这里也将分享两种方式用于图标的绘制。

# 使用 Windows Forms 的 Chart 控件

最简单直接的方案是采用 Windows Forms Chart 控件。在.NET Framework 4.0 及更高版本中,微软提供了一个功能相当完善的图表控件库。
它的优点很明显:首先是内置集成,不需要额外安装第三方库,直接拖拽到窗体上就能用;其次是开发简便快捷,API 设计直观,上手容易;再者,它支持的图表类型非常全面,折线图、柱状图、饼图、散点图等等应有尽有,能满足大部分需求。
当然,它也有一些局限性,在测试过程中我们发现当需要展示的数据量非常大,或者刷新频率非常高的时候,这种展示方法可能会遇到性能瓶颈,图表可能会出现卡顿、延迟;另外,由于是 Windows Forms 的组件,跨平台开发会受到限制。

using System.Windows.Forms.DataVisualization.Charting; 
private void ChartInit() { 
    Chart1.Series[0].ChartType = SeriesChartType.FastLine;
    Chart1.Series[0].Color = System.Drawing.Color.DarkGreen; 
    Chart1.Series[1].ChartType = SeriesChartType.FastLine; 
    Chart1.Series[1].Color = System.Drawing.Color.DarkRed; 
}
public void DataUpdate() { 
    Chartid.Add(id); 
    Chartiq.Add(iq); 
    ExpiredDataHandle();
    Displayid = Chartid.ToArray(); 
    Displayiq = Chartiq.ToArray(); 
}
public void ChartLoad() { 
    Chart1.Series[0].Points.DataBindY(Displayid); 
    Chart1.Series[1].Points.DataBindY(Displayiq); 
}

# 使用 HTML5 绘制

随着 Web 技术的发展,使用 HTML5、CSS 和 JavaScript 来构建富客户端应用也成为了一种趋势。我们可以在 C# 应用中嵌入一个 Web 浏览器控件,这也是当今很多手机及计算机应用开发软件的思路。在浏览器中加载本地或远程的 HTML 页面来绘制图表。有很多优秀的 JavaScript 图表库,它们功能强大,并且通常具有更好的性能和跨平台潜力。
这种方式的优势在于:可以利用 Web 前端生态丰富的资源和强大的表现力,实现非常精美的可视化效果;并且,如果 UI 逻辑用 Web 技术实现,未来迁移到其他平台也会相对容易。
挑战则在于:需要在 C# 后端与 JavaScript 前端之间进行数据交互和通信,可能会增加一些复杂性;同时,也需要开发者具备一定的前端知识。

使用 HTML5 绘图的架构图如下图所示:
HTML5 绘图
这里也给出了最基本的初始化 C# 中图表的方式。

private ChromiumWebBrowser ChartBrowser; 
private void InitChart() { 
    CefSettings settings = new CefSettings(); 
    settings.Locale = "zh-CN"; 
    settings.CefCommandLineArgs.Add("enable-gpu"); 
    Cef.Initialize(settings); 
    try { 
        string currentPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); 
        if (currentPath == null) { 
            throw new Exception($"Failed to {nameof(InitChart)}, Failed to GetDirectoryName"); 
        }
        string filepath = currentPath + @"\html\dynamic-data-SetPeriod-Fps.html"; 
        if (System.IO.File.Exists(filepath)) { 
            ChartBrowser = new ChromiumWebBrowser(filepath); // 创建实例,将浏览器放入容器中 
            ChartBrowser.Dock = DockStyle.Fill; 
            ChartBrowser.Parent = panelChart; 
            ChartBrowser.MenuHandler = new MenuHandler(); 
        } else throw new Exception("Can't Find Files\n" + filepath); 
        ChartTimePeriod = 10, Fps = 60; // 图像展示默认参数配置 
    } 
    catch (System.Exception ex) 
    { 
        MessageBox.Show("Error:" + ex.Message, "Error"); 
    } 
}

# 参考文献

第 8 章 CAN 总线及其接口 — Embedded System and IoT Application 1.2.0 documentation
CAN 协议深度解析 - 简单易懂协议详解 - 知乎

Cover: Short hair Firefly!(Harukix@Pixiv15004315)120578115