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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.24 21:48:28