You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C#串口高速通信数据丢失、CPU占用过高及UI卡顿问题求助

C#串口高速接收数据的性能与丢包问题

问题场景

从单片机接收高速数据(每秒最多36000字节,拆分为2000个18字节数据包),波特率1000000,使用DataReceived事件,通过Virtual Serial Port Driver虚拟配对COM1/COM2测试。出现两个核心问题:

  • 保存数据到CSV时发生丢包
  • CPU占用率高达50%-60%,findPacket函数持续高负载,导致UI、CSV写入、在线绘图等功能卡顿

模拟器串口配置与数据生成代码

SerialPort serialPort = new SerialPort("COM1", 1000000, Parity.None, 8, StopBits.One);
serialPort.Handshake = Handshake.None;
serialPort.Open();
serialPort.DataReceived += new SerialDataReceivedEventHandler(sp_DataReceived);

byte[] packet = new byte[] { 83, 84, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 69, 78 };
ulong packetCount = 0;
while (packetCount < 600000) // 修正原代码多余的分号
{
    byte[] timeBytes = BitConverter.GetBytes(packetCount);
    packet[2] = timeBytes[0]; 
    packet[3] = timeBytes[1]; 
    packet[4] = timeBytes[2]; 
    packet[5] = timeBytes[3];
    serialPort.Write(packet, 0, packet.Length); // 修正原代码未指定串口对象的Write调用
    packetCount++;
}

数据包格式:头部{83,84},尾部{69,78},总长度18字节

主应用串口配置

SerialPort _serialPort = new SerialPort("COM2", 1000000, Parity.None, 8, StopBits.One);
_serialPort.Handshake = Handshake.None;
_serialPort.DataReceived += new SerialDataReceivedEventHandler(sp_DataReceived);
_serialPort.ReadBufferSize = 100;

主应用DataReceived事件代码

private void sp_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
    int count = _serialPort.BytesToRead;
    byte[] receivedTemp = new byte[count];
    _serialPort.Read(receivedTemp, 0, count);
    _totalReceived.AddRange(receivedTemp);
    findPacket();
}

数据包解析函数findPacket

int findPacket(int index = 0, bool headerFounded = false)
{
    while (index + 1 < _totalReceived.Count)
    {
        if (_totalReceived[index] == 83 && _totalReceived[index + 1] == 84)
        {
            int headerIndex = index;
            int packetLen = 14; 
            int footerIndex = findPacket(index + 2 + packetLen, true);
            if (footerIndex > -1 && (packetLen == 0 || (footerIndex == headerIndex + packetLen + 2)))
            {
                List<byte> data = _totalReceived.GetRange(headerIndex + 2, footerIndex - (headerIndex + 2));
                _totalReceived.RemoveRange(headerIndex, footerIndex + 2 - headerIndex);
                if (headerIndex > 0 && !headerFounded)
                    _totalReceived.RemoveRange(0, headerIndex);

                calcPacket(data);
                continue;
            }
            else if (footerIndex != -1)
            { 
                 index++; 
                 continue; 
            }
            break;
        }

        if (headerFounded && _totalReceived[index] == 69 && _totalReceived[index + 1] == 78)
            return index;

        index++;
    }

    return -1;
}

问题根源分析

  1. ReadBufferSize设置过小:仅100字节,远低于每秒36000字节的吞吐量,容易导致串口缓冲区溢出丢包
  2. findPacket函数效率极低:
    • 递归调用+循环遍历,每次DataReceived都触发全量扫描,高负载下CPU被占满
    • 频繁调用List<T>.RemoveRange和GetRange,这些操作是O(n)复杂度,大量数据时性能急剧下降
  3. DataReceived事件中执行同步解析+业务逻辑:事件回调在IO线程执行,阻塞线程导致后续数据接收不及时,同时抢占UI线程资源(如果calcPacket包含UI操作)
  4. 模拟器代码错误:原while循环多了分号,导致无限循环;Write方法未指定serialPort对象,实际无法发送数据

优化方案

1. 调整串口缓冲区大小

将ReadBufferSize设置为至少2倍峰值吞吐量(比如8192字节,预留足够缓冲空间):

_serialPort.ReadBufferSize = 8192;

2. 重构数据包解析逻辑,避免低效操作

放弃递归和List的频繁修改,改用环形缓冲区+状态机解析:

  • 用固定大小的byte数组作为环形缓冲区,避免List的内存重分配和移动
  • 维护解析状态(寻找头部、接收数据、寻找尾部),逐字节或批量匹配,无需全量扫描
    示例伪代码:
private byte[] _ringBuffer = new byte[8192];
private int _bufferHead = 0;
private int _bufferTail = 0;
private enum ParseState { LookingForHeader, ReceivingData, LookingForFooter }
private ParseState _currentState = ParseState.LookingForHeader;
private int _dataReceivedCount = 0;
private readonly object _bufferLock = new object();

private void sp_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
    lock (_bufferLock)
    {
        int bytesRead = _serialPort.Read(_ringBuffer, _bufferTail, _ringBuffer.Length - _bufferTail);
        _bufferTail = (_bufferTail + bytesRead) % _ringBuffer.Length;
    }
}

// 后台线程定期解析缓冲区
private void StartParsingThread()
{
    Task.Run(() =>
    {
        while (_isRunning)
        {
            lock (_bufferLock)
            {
                ParseBuffer();
            }
            Thread.Sleep(10); // 避免空转CPU
        }
    });
}

private void ParseBuffer()
{
    while (GetBufferLength() >= 2)
    {
        switch (_currentState)
        {
            case ParseState.LookingForHeader:
                if (PeekByte(0) == 83 && PeekByte(1) == 84)
                {
                    AdvanceBuffer(2);
                    _currentState = ParseState.ReceivingData;
                    _dataReceivedCount = 0;
                }
                else
                {
                    AdvanceBuffer(1);
                }
                break;
            case ParseState.ReceivingData:
                int needed = 14 - _dataReceivedCount;
                int available = GetBufferLength();
                int take = Math.Min(needed, available);
                // 读取数据到临时存储或直接处理
                _dataReceivedCount += take;
                AdvanceBuffer(take);
                if (_dataReceivedCount == 14)
                {
                    _currentState = ParseState.LookingForFooter;
                }
                break;
            case ParseState.LookingForFooter:
                if (PeekByte(0) == 69 && PeekByte(1) == 78)
                {
                    AdvanceBuffer(2);
                    ProcessParsedData();
                    _currentState = ParseState.LookingForHeader;
                }
                else
                {
                    _currentState = ParseState.LookingForHeader;
                }
                break;
        }
    }
}

private int GetBufferLength()
{
    return _bufferTail >= _bufferHead ? _bufferTail - _bufferHead : _ringBuffer.Length - _bufferHead + _bufferTail;
}

private byte PeekByte(int offset)
{
    int pos = (_bufferHead + offset) % _ringBuffer.Length;
    return _ringBuffer[pos];
}

private void AdvanceBuffer(int count)
{
    _bufferHead = (_bufferHead + count) % _ringBuffer.Length;
}

3. 异步分离解析与业务逻辑

  • DataReceived事件仅负责读取数据到缓冲区,不做解析
  • 用独立后台线程定期解析缓冲区数据
  • CSV写入、绘图等IO/UI操作,通过Dispatcher(WPF)或Invoke(WinForms)异步提交到UI线程,避免阻塞解析线程
    示例:
private void ProcessParsedData()
{
    // 批量写入CSV:保持StreamWriter长期打开,减少IO开销
    _csvWriter.WriteLine("数据内容");
    
    // WPF UI更新示例
    Application.Current.Dispatcher.Invoke(() =>
    {
        // 更新图表或UI控件
    });
}

4. 优化CSV写入性能

  • 使用StreamWriter保持文件打开状态,避免每次写入都创建新文件句柄
  • 积累一定数量的数据包后再一次性写入,减少IO操作次数

内容的提问来源于stack exchange,提问作者Rezamn65

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 14:40:59