C# 上位机开发 - 自定义通信协议设计(一篇搞定 建议收藏)
专栏导航 :上一篇:CRC16校验算法实现 | 下一篇:ModbusRTU主站实现
前言
当 PLC 与 PC 之间不走标准 Modbus,而由设备厂家定义 私有协议 时,上位机工程师要负责 帧格式、版本演进、异常码、重传 。本文以经典结构 AA 55 | CMD | LEN | DATA | CRC16 为例,给出 ProtocolFrame、ProtocolParser、CustomProtocolService 完整 C# 实现,可直接套用到板卡、视觉相机、定制采集器。
帧格式定义
| 偏移 | 长度 | 字段 | 说明 |
|---|---|---|---|
| 0 | 2 | HEAD | 固定 0xAA 0x55,同步字 |
| 2 | 1 | CMD | 命令字,如 0x01 读、0x02 写 |
| 3 | 1 | LEN | DATA 长度 N,0~250 |
| 4 | N | DATA | 载荷 |
| 4+N | 2 | CRC16 | Modbus 算法,覆盖 CMD~DATA |
设计原则:头固定、长度显式、尾部 CRC;禁止在 DATA 里出现未转义的假头,或依赖"不会出现 AA 55"的假设------现场会教你做人。
ProtocolFrame 构建与解析
csharp
using System;
using UpperComputer.Communication.Checksum;
namespace UpperComputer.Communication.Protocol
{
public sealed class ProtocolFrame
{
public const byte Head0 = 0xAA;
public const byte Head1 = 0x55;
public byte Command { get; set; }
public byte[] Data { get; set; } = Array.Empty<byte>();
public byte[] ToBytes()
{
if (Data.Length > 250) throw new ArgumentOutOfRangeException(nameof(Data));
var body = new byte[2 + Data.Length];
body[0] = Command;
body[1] = (byte)Data.Length;
if (Data.Length > 0)
Buffer.BlockCopy(Data, 0, body, 2, Data.Length);
var withCrc = Crc16Modbus.Append(body);
var frame = new byte[2 + withCrc.Length];
frame[0] = Head0;
frame[1] = Head1;
Buffer.BlockCopy(withCrc, 0, frame, 2, withCrc.Length);
return frame;
}
public static bool TryParse(ReadOnlySpan<byte> frame, out ProtocolFrame result)
{
result = null;
if (frame.Length < 2 + 2 + 2) return false;
if (frame[0] != Head0 || frame[1] != Head1) return false;
var bodyAndCrc = frame.Slice(2);
if (!Crc16Modbus.Verify(bodyAndCrc.ToArray())) return false;
byte cmd = bodyAndCrc[0];
byte len = bodyAndCrc[1];
if (bodyAndCrc.Length != 2 + len + 2) return false;
var data = bodyAndCrc.Slice(2, len).ToArray();
result = new ProtocolFrame { Command = cmd, Data = data };
return true;
}
}
}
ProtocolParser(流式)
csharp
using System.Collections.Generic;
namespace UpperComputer.Communication.Protocol
{
public class ProtocolParser
{
private readonly List<byte> _buf = new List<byte>(512);
public void Feed(ReadOnlySpan<byte> chunk)
{
foreach (byte b in chunk) _buf.Add(b);
}
public bool TryReadFrame(out ProtocolFrame frame)
{
frame = null;
for (int i = 0; i <= _buf.Count - 2; i++)
{
if (_buf[i] != ProtocolFrame.Head0 || _buf[i + 1] != ProtocolFrame.Head1)
continue;
if (_buf.Count - i < 6) return false;
byte len = _buf[i + 4];
int total = 2 + 2 + len + 2;
if (_buf.Count - i < total) return false;
var slice = _buf.GetRange(i, total).ToArray();
if (ProtocolFrame.TryParse(slice, out frame))
{
_buf.RemoveRange(0, i + total);
return true;
}
}
if (_buf.Count > 4096) _buf.Clear();
return false;
}
}
}
CustomProtocolService(串口 + 解析 + 请求应答)
csharp
using System;
using System.Threading;
using System.Threading.Tasks;
namespace UpperComputer.Communication.Protocol
{
public class CustomProtocolService : IDisposable
{
private readonly SerialPortManager _port;
private readonly ProtocolParser _parser = new ProtocolParser();
private readonly SemaphoreSlim _responseLock = new SemaphoreSlim(1, 1);
private TaskCompletionSource<ProtocolFrame> _pending;
public CustomProtocolService(SerialPortManager port)
{
_port = port;
_port.DataReceived += OnDataReceived;
}
private void OnDataReceived(object sender, byte[] data)
{
_parser.Feed(data);
while (_parser.TryReadFrame(out var frame))
{
_pending?.TrySetResult(frame);
}
}
public async Task<ProtocolFrame> SendAndWaitAsync(ProtocolFrame request, int timeoutMs = 2000)
{
await _responseLock.WaitAsync();
try
{
_pending = new TaskCompletionSource<ProtocolFrame>(TaskCreationOptions.RunContinuationsAsynchronously);
_port.Send(request.ToBytes());
using var cts = new CancellationTokenSource(timeoutMs);
await using (cts.Token.Register(() => _pending.TrySetCanceled()))
{
return await _pending.Task;
}
}
finally
{
_pending = null;
_responseLock.Release();
}
}
public void Dispose()
{
_port.DataReceived -= OnDataReceived;
}
}
}
生产环境应匹配 请求序号 、区分 主动上报 与 应答,避免错配 TCS。
命令表示例
| CMD | 方向 | DATA | 说明 |
|---|---|---|---|
| 0x01 | PC→设备 | 通道号 | 读测量值 |
| 0x81 | 设备→PC | 4 字节 float | 应答 |
| 0x02 | PC→设备 | 设定值 | 写参数 |
| 0xFF | 设备→PC | 1 字节 code | 异常 |
版本与兼容
在 DATA 首字节加 协议版本 ;未知 CMD 返回 0xFF + 错误码;文档与代码 同仓库维护。
FAQ
Q1:LEN 用 2 字节更好吗?
数据可能超 255 时用 小端 UINT16;本文 LEN 1 字节适合轻量采集。
Q2:AA 55 出现在 DATA?
转义:0xAA 0x56 表示 0xAA 等;或改用更长的魔数 + CRC 重同步。
Q3:半双工 485 应答太快?
Thread.Sleep(InterFrameDelayMs) 或等设备手册 最小响应时间。
小结
定头 + 长 + 数据 + CRC 是私有协议万金油。下一篇进入工业标准 Modbus RTU 主站。
专栏导航 :上一篇:CRC16校验算法实现 | 下一篇:ModbusRTU主站实现