自定义通信协议设计

C# 上位机开发 - 自定义通信协议设计(一篇搞定 建议收藏)

专栏导航上一篇:CRC16校验算法实现 | 下一篇:ModbusRTU主站实现


前言

当 PLC 与 PC 之间不走标准 Modbus,而由设备厂家定义 私有协议 时,上位机工程师要负责 帧格式、版本演进、异常码、重传 。本文以经典结构 AA 55 | CMD | LEN | DATA | CRC16 为例,给出 ProtocolFrameProtocolParserCustomProtocolService 完整 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主站实现