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; }
问题根源分析
- ReadBufferSize设置过小:仅100字节,远低于每秒36000字节的吞吐量,容易导致串口缓冲区溢出丢包
- findPacket函数效率极低:
- 递归调用+循环遍历,每次DataReceived都触发全量扫描,高负载下CPU被占满
- 频繁调用
List<T>.RemoveRange和GetRange,这些操作是O(n)复杂度,大量数据时性能急剧下降
- DataReceived事件中执行同步解析+业务逻辑:事件回调在IO线程执行,阻塞线程导致后续数据接收不及时,同时抢占UI线程资源(如果calcPacket包含UI操作)
- 模拟器代码错误:原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
相关产品推荐
相关产品推荐

