c# 上位机作为控制端与下位机通信方式

在C#上位机开发中,与下位机(如PLC、单片机等)稳定通信至关重要

下面这个表格详细对比了几种主流通信方式

通讯方式 及 核心协议 关键特点 及 依赖库实现
串口通信 (RS-232/RS-485)​ -UART 硬件简单、成本低、协议简单;需配置参数一致(波特率、数据位等)-System.IO.Ports.SerialPort
TCP/IP 网络通信​ -TCP 传输距离远、速率高、可靠(TCP),依赖网络环境 -System.Net.Sockets,TcpListener
UDP通信 -UDP 传输距离远、速率高、实时(UDP),依赖网络环境 -System.Net.Sockets,UdpClient
Modbus -Modbus RTU/ASCII, Modbus TCP​ 工业标准、主从模式、简单可靠;常用库简化开发 -NModbus库 (NuGet安装)

串口通信 (SerialPort)

串口通信是一种非常常见且基础的数据传输方式

初始化配置:使用 System.IO.Ports.SerialPort类。关键是要确保参数与下位机完全一致

描述:串口通信是上位机与下位机(如单片机、PLC)常用的通信方式,RS232 适合点对点,RS485 支持多点通信

特点:
1.简单、稳定,适合短距离通信
2.需要配置波特率、数据位、校验位等且保持与下位机一致
应用场景:工业设备控制、传感器数据采集

实例化并配置串口参数

csharp 复制代码
// 实例化并配置串口参数
SerialPort mySerialPort = new SerialPort();
mySerialPort.PortName = "COM3";     // 端口号,如COM1, COM2等
mySerialPort.BaudRate = 9600;      // 波特率,如9600, 115200等
mySerialPort.DataBits = 8;         // 数据位
mySerialPort.Parity = Parity.None; // 校验位
mySerialPort.StopBits = StopBits.One; // 停止位
mySerialPort.ReadTimeout = 500;    // 读取超时(毫秒)
mySerialPort.WriteTimeout = 500;   // 写入超时(毫秒)
// 订阅数据接收事件(异步处理)
mySerialPort.DataReceived += new SerialDataReceivedEventHandler(SerialPort_DataReceived);

打开连接

csharp 复制代码
try {
    mySerialPort.Open();
    Console.WriteLine("串口打开成功。");
} catch (Exception ex) {
    Console.WriteLine($"串口打开失败: {ex.Message}");
}

发送数据

csharp 复制代码
// 发送字符串
string message = "Hello, Device!";
mySerialPort.Write(message);

// 或发送字节数组(适用于二进制协议或HEX指令)
byte[] dataToSend = new byte[] { 0xAA, 0x01, 0x0D, ... };
mySerialPort.Write(dataToSend, 0, dataToSend.Length);

接收数据:通过 DataReceived事件处理。务必注意数据包完整性问题,一次触发接收的数据可能不是一个完整的数据包

csharp 复制代码
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e) {
    SerialPort sp = (SerialPort)sender;
    int bytesToRead = sp.BytesToRead;
    byte[] buffer = new byte[bytesToRead];
    sp.Read(buffer, 0, bytesToRead);

    // 处理接收到的数据 buffer
    // 例如,转换为十六进制字符串显示
    string receivedHex = BitConverter.ToString(buffer);
    Console.WriteLine($"收到数据: {receivedHex}");

    // **关键:对于自定义协议,需要在此进行数据包拼接和完整性判断**
    // 例如,根据帧头、帧尾、长度字段等判断是否是一个完整包
    // 可参考后续的"数据包完整性"部分
}

关闭连接

csharp 复制代码
if (mySerialPort != null && mySerialPort.IsOpen) {
    mySerialPort.Close();
    mySerialPort.Dispose(); // 释放资源
}

关键细节

数据包完整性:DataReceived事件是异步触发的,且触发时机和每次接收的数据量不确定。必须设计接收缓冲区,根据自定义协议(如包含帧头、数据长度、校验和、帧尾的格式)来拼接完整数据包,并进行校验

下面的代码演示了一种简单的处理思路:

csharp 复制代码
private List<byte> _receiveBuffer = new List<byte>(); // 接收缓冲区
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e) {
    SerialPort sp = (SerialPort)sender;
    byte[] tempBuffer = new byte[sp.BytesToRead];
    sp.Read(tempBuffer, 0, tempBuffer.Length);
    _receiveBuffer.AddRange(tempBuffer); // 新数据加入缓冲区

    // 检查并处理缓冲区中的数据包
    ProcessBuffer();
}
private void ProcessBuffer() {
    // 示例:假设协议格式为 [帧头0xA5] [数据长度N] [N字节数据] [校验和] [帧尾0x5A]
    while (_receiveBuffer.Count >= 5) { // 至少包含帧头、长度(1字节)、校验和、帧尾
        int headerIndex = _receiveBuffer.IndexOf(0xA5);
        if (headerIndex < 0) {
            _receiveBuffer.Clear(); // 没有找到帧头,清空无效数据
            break;
        }
        if (headerIndex > 0) {
            _receiveBuffer.RemoveRange(0, headerIndex); // 丢弃帧头前的杂乱数据
            continue;
        }
        if (_receiveBuffer.Count < 3) break; // 数据不够,等待下次接收
        byte dataLength = _receiveBuffer[1]; // 获取数据段长度
        int totalPacketLength = dataLength + 4; // 总包长 = 帧头(1) + 长度(1) + 数据(N) + 校验(1) + 帧尾(1)
        if (_receiveBuffer.Count < totalPacketLength) break; // 数据包不完整,退出循环等待

        // 提取完整数据包
        byte[] packet = _receiveBuffer.GetRange(0, totalPacketLength).ToArray();
        // 检查帧尾
        if (packet[totalPacketLength - 1] != 0x5A) {
            _receiveBuffer.RemoveAt(0); // 帧尾错误,丢弃帧头,继续查找下一个帧头
            continue;
        }
        // 校验和检查 (示例为异或校验)
        byte checksum = 0;
        for (int i = 1; i < totalPacketLength - 2; i++) { // 从长度字节到数据末尾
            checksum ^= packet[i];
        }
        if (checksum == packet[totalPacketLength - 2]) {
            // 校验通过,处理有效数据包 packet[2..(totalPacketLength-2)]
            OnValidPacketReceived(packet);
        } else {
            // 校验失败,记录日志
            Console.WriteLine("校验和错误!");
        }
        // 从缓冲区中移除已处理的数据包
        _receiveBuffer.RemoveRange(0, totalPacketLength);
    }
}

这是一个简化示例,实际协议可能更复杂。核心思想是维护一个缓冲区,并按照协议规则从中提取和验证完整的数据包

跨线程访问UI:DataReceived事件在非UI线程中执行。如果要在其中更新界面控件(如TextBox、Label),必须通过 Control.Invoke或 Dispatcher.Invoke切换回UI线程

csharp 复制代码
this.Invoke(new Action(() => {
    textBoxReceivedData.AppendText($"收到: {Encoding.ASCII.GetString(buffer)}\r\n");
}));

TCP/IP通信

TCP/IP 是可靠的面向连接协议,适合上位机与下位机之间需要高可靠性的通信

特点:
1.提供可靠的数据传输,适合长连接
2.数据格式需自定义,灵活性高
应用场景:自定义协议通信、实时数据传输

建立连接(使用 TcpClient)

csharp 复制代码
using System.Net.Sockets;

TcpClient tcpClient = new TcpClient();
try {
    await tcpClient.ConnectAsync("192.168.1.100", 12345); // 服务器IP和端口
    Console.WriteLine("已连接到服务器。");
    NetworkStream stream = tcpClient.GetStream();
    // 启动单独任务或线程接收数据
    _ = Task.Run(() => ReceiveData(stream));
} catch (Exception ex) {
    Console.WriteLine($"连接失败: {ex.Message}");
}

发送数据

csharp 复制代码
string message = "MEAS:VOLTage:ALL?\n"; // 示例指令
byte[] data = Encoding.UTF8.GetBytes(message);
await stream.WriteAsync(data, 0, data.Length);

接收数据:在独立线程或异步任务中循环读取

csharp 复制代码
private async Task ReceiveData(NetworkStream stream) {
    byte[] buffer = new byte[1024];
    while (tcpClient.Connected) {
        try {
            int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
            if (bytesRead == 0) {
                Console.WriteLine("连接已关闭。");
                break; // 连接已断开
            }
            string receivedData = Encoding.UTF8.GetString(buffer, 0, bytesRead);
            Console.WriteLine($"从服务器接收: {receivedData}");
            // 触发事件或回调,通知UI更新(注意跨线程调用)
        } catch (Exception ex) {
            Console.WriteLine($"接收数据异常: {ex.Message}");
            break;
        }
    }
}

关闭连接

csharp 复制代码
stream?.Close();
tcpClient?.Close();

关键细节
1.粘包与拆包:与串口类似,TCP是流式协议,消息边界不清晰。也需要定义应用层协议(如消息长度前缀、特定分隔符)来确保正确拆分数据包
2.异步操作:推荐使用 ConnectAsync, ReadAsync, WriteAsync等异步方法,避免阻塞UI线程
3.连接状态管理:需要维护连接状态(IsConnected),并在发送和接收时进行判断。妥善处理断线重连逻辑

UDP 协议

UDP 是无连接的协议,速度快但不可靠,适合实时性要求高但允许少量丢包的场景

特点:
1.无连接,传输效率高
2.数据格式需自定义
应用场景:实时传感器数据、视频流传输

代码示例(上位机发送和接收 UDP 数据)

csharp 复制代码
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
 
class Program
{
    static void Main(string[] args)
    {
        try
        {
            // 创建 UDP 客户端
            UdpClient udpClient = new UdpClient();
            IPEndPoint remoteEndPoint = new IPEndPoint(IPAddress.Parse("192.168.1.100"), 12345); // 下位机地址
 
            // 发送数据
            string message = "Hello, UDP Device!";
            byte[] sendBytes = Encoding.ASCII.GetBytes(message);
            udpClient.Send(sendBytes, sendBytes.Length, remoteEndPoint);
            Console.WriteLine($"发送: {message}");
 
            // 接收数据
            IPEndPoint receiveEndPoint = new IPEndPoint(IPAddress.Any, 0);
            byte[] receiveBytes = udpClient.Receive(ref receiveEndPoint);
            string receivedData = Encoding.ASCII.GetString(receiveBytes);
            Console.WriteLine($"接收: {receivedData} from {receiveEndPoint}");
 
            // 关闭客户端
            udpClient.Close();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"错误: {ex.Message}");
        }
    }
}

Modbus协议

Modbus是工业领域广泛应用的标准协议

C# 实现核心步骤与代码 (以Modbus TCP为例)

使用 NModbus​ 库(通过NuGet安装 NModbus)可以极大简化开发

csharp 复制代码
using Modbus.Device;
using System.Net.Sockets;

string ipAddress = "192.168.1.100"; // 下位机(从站)IP
int port = 502; // Modbus TCP默认端口

using (TcpClient client = new TcpClient(ipAddress, port)) {
    // 创建Modbus主站
    ModbusIpMaster master = ModbusIpMaster.CreateIp(client);
    
    byte slaveId = 1; // 从站地址
    ushort startAddress = 0; // 起始寄存器地址
    ushort numRegisters = 10; // 要读取的寄存器数量
    
    // 读取保持寄存器(功能码03)
    ushort[] holdingRegisters = master.ReadHoldingRegisters(slaveId, startAddress, numRegisters);
    
    // 写入单个线圈(功能码05)
    master.WriteSingleCoil(slaveId, 0, true);
    
    // 写入单个寄存器(功能码06)
    master.WriteSingleRegister(slaveId, 1, 1234);
    
    // 输出结果
    for (int i = 0; i < holdingRegisters.Length; i++) {
        Console.WriteLine($"寄存器 {startAddress + i}: {holdingRegisters[i]}");
    }
}

关键细节
1.寄存器地址:注意协议中的寄存器地址通常是从0开始的。有些设备文档可能使用从1开始的地址,需要转换
2.功能码:不同功能码对应不同操作(如01读线圈,02读离散输入,03读保持寄存器,04读输入寄存器,05写单个线圈,06写单个寄存器等)
3.异常处理:库方法可能会抛出 ModbusException,需要捕获处理

通用注意事项与最佳实践

无论选择哪种方式,以下几点都至关重要:
定义应用层协议:通信双方必须预先严格约定数据格式,例如帧头、数据长度、指令类型、有效数据、校验和(如CRC、求和、异或)、帧尾。校验和能有效发现传输错误

超时与重试机制 :发送指令后应设置合理的等待响应超时。超时后触发重发,并限制重试次数,避免无限等待。

资源释放:通信对象(如 SerialPort, TcpClient)使用了非托管资源。务必使用 using语句或显式调用 Close()/Dispose()确保资源释放

线程安全:在异步接收数据时,对共享资源(如发送队列、连接状态标志)的访问要使用锁(lock)等机制保证线程安全

日志记录:记录关键的通信事件(如连接建立、数据发送接收、错误异常),便于后期调试和故障排查

相关推荐
奋斗的牛马3 小时前
OFDM理解
网络·数据库·单片机·嵌入式硬件·fpga开发·信息与通信
烛阴3 小时前
从零开始掌握C#核心:变量与数据类型
前端·c#
蓁蓁啊3 小时前
Ubuntu 虚拟机文件传输到 Windows的一种好玩的办法
linux·运维·windows·单片机·ubuntu
EVERSPIN4 小时前
32位MCU芯片国产品牌(32系列单片机常用型号有哪些)
单片机·嵌入式硬件·mcu单片机·32系列单片机
yue0084 小时前
C# 生成指定位数的编号
开发语言·c#
爱吃汽的小橘4 小时前
使用DSI TX IP驱动LCD显示屏
单片机·嵌入式硬件
红黑色的圣西罗4 小时前
C# List.Sort方法总结
开发语言·c#
芯联智造5 小时前
【stm32协议外设篇】- PAJ7620手势识别传感器
c语言·stm32·单片机·嵌入式硬件
从零点5 小时前
STM32F407运动资源分配
stm32·单片机·嵌入式硬件