C# WPF串口每100ms请求数据丢包问题排查与解决
问题描述
在WPF应用中设置定时器每100ms通过SerialPort发送数据请求,每次请求应收到160字节的数据包(通过_serialPort.DataReceived += _serialPort_DataReceived;处理),但10秒内仅收到80个包,远低于预期的100个。当将_readTimer.Interval = 10;时,10秒内能收到98个左右的包,接近预期。需要分析100ms间隔发送请求时丢包的原因,并给出解决办法。
代码片段
public DataTransfer(SerialPort serialPort) { _serialPort = new SerialPort("COM3", 38400, Parity.None, 8, StopBits.One); _readTimer = new Timer(); _readTimer.Interval = 10; _readTimer.Elapsed += _readTimer_Elapsed; _readTimer.AutoReset = true; OpenSerialPort(); } public void OpenSerialPort() { _serialPort.Open(); _serialPort.WriteTimeout = 50; _serialPort.ReadTimeout = 100; _serialPort.DataReceived += _serialPort_DataReceived; // Start the timer _readTimer.Start(); } private void _readTimer_Elapsed(object? sender, ElapsedEventArgs e) { byte[] temp = new byte[9]; byte[] data = BitConverter.GetBytes(560); temp = PacketIO.ReadRequest(data, PacketIO.Addr.Addr1); IsRead = true; _serialPort.BaseStream.BeginWrite(temp, 0, temp.Length, writeCallback, null); _unReadTimer.Start(); } private void writeCallback(IAsyncResult ar) { _serialPort.BaseStream.EndWrite(ar); }
原因分析
- 异步写操作无并发控制:当前代码直接在定时器事件中发起异步写请求,未检查前一次写操作是否完成。100ms间隔下,ThreadPool线程调度可能导致多次写操作重叠,引发串口写缓冲区溢出,或设备端收到乱序/重叠请求后无法生成有效响应。而10ms间隔时,线程池任务密集反而可能串行执行写操作,减少冲突。
- DataReceived事件未完整读取缓冲区数据:如果
_serialPort_DataReceived中仅读取固定160字节,未清空缓冲区所有可用数据,100ms间隔的响应会残留数据,下一次响应到来时发生数据粘连,导致解析错误被丢弃。10ms间隔时响应连续触发事件,反而能及时分批读取数据,降低粘连概率。 - 设备端响应时序不匹配:部分串口设备在处理前一次响应时会丢弃新请求。100ms间隔看似足够,但设备处理延迟+串口传输延迟可能导致新请求到达时设备仍在忙碌,从而丢包。10ms间隔时设备进入连续处理状态,响应效率反而更高。
解决方案
- 添加线程安全的并发写控制:用原子操作确保同一时间只有一个写请求执行,避免并发冲突:
private int _isRead = 1; // 1表示可发起请求,0表示正在处理 private void _readTimer_Elapsed(object? sender, ElapsedEventArgs e) { // 原子检查并锁定请求状态 if (Interlocked.Exchange(ref _isRead, 0) != 1) return; byte[] temp = PacketIO.ReadRequest(BitConverter.GetBytes(560), PacketIO.Addr.Addr1); _serialPort.BaseStream.BeginWrite(temp, 0, temp.Length, writeCallback, null); _unReadTimer.Start(); } private void writeCallback(IAsyncResult ar) { _serialPort.BaseStream.EndWrite(ar); // 写操作完成,释放请求锁 Interlocked.Exchange(ref _isRead, 1); } - 完整读取串口缓冲区数据并处理粘包:在DataReceived事件中读取所有可用数据,用全局缓冲区缓存并拆分完整数据包:
private List<byte> _receiveBuffer = new List<byte>(); 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); ProcessReceivedData(buffer); } private void ProcessReceivedData(byte[] rawData) { _receiveBuffer.AddRange(rawData); // 循环拆分完整的160字节数据包 while (_receiveBuffer.Count >= 160) { byte[] completePacket = _receiveBuffer.Take(160).ToArray(); _receiveBuffer.RemoveRange(0, 160); // 处理完整数据包 HandleCompletePacket(completePacket); } } private void HandleCompletePacket(byte[] packet) { // 你的数据包解析逻辑 } - 改用DispatcherTimer适配WPF线程模型:WPF中
DispatcherTimer在UI线程触发,避免ThreadPool线程带来的同步问题:private DispatcherTimer _readTimer; public DataTransfer(SerialPort serialPort) { _serialPort = new SerialPort("COM3", 38400, Parity.None, 8, StopBits.One); _readTimer = new DispatcherTimer(); _readTimer.Interval = TimeSpan.FromMilliseconds(100); _readTimer.Tick += _readTimer_Tick; OpenSerialPort(); } private void _readTimer_Tick(object? sender, EventArgs e) { if (Interlocked.Exchange(ref _isRead, 0) != 1) return; byte[] temp = PacketIO.ReadRequest(BitConverter.GetBytes(560), PacketIO.Addr.Addr1); _serialPort.BaseStream.BeginWrite(temp, 0, temp.Length, writeCallback, null); _unReadTimer.Start(); } - 优化串口超时设置:将
ReadTimeout调整为大于单个响应的传输时间(如200ms),避免微小延迟导致读取超时:_serialPort.ReadTimeout = 200;
内容的提问来源于stack exchange,提问作者Amirhossein Sadeghy
相关产品推荐
相关产品推荐

