串行端口 - C#:与设备通信期间数据缓冲区的奇怪偏移

本文关键字:缓冲区 数据 通信 串行端口 | 更新日期: 2023-09-27 17:55:52

在我的应用程序中,我必须从通过COM端口连接的设备接收和处理一些数据。我部分地这样做。在该特定设备中,前两个字节是数据包的长度(减去2,因为它没有考虑这两个字节;所以它毕竟是数据包其余部分的长度)。然后,由于我知道设备倾向于缓慢地向我发送数据,所以我在循环中读取数据包的其余部分,直到读取所有数据。但就在这里,我遇到了奇怪的问题。让我们假设整个数据包(包括前两个长度的字节)看起来像这样:['a', 'b', 'c', 'd', 'e']。当我读取前两个字节("a"和"b")时,我希望数据包的其余部分看起来像这样:['c', 'd', 'e']。但相反,它看起来像这样:['b', 'c', 'd', 'e']。为什么响应的第二个字节仍在读取缓冲区中?为什么只有第二个,而没有前一个?

下面的代码显示了如何处理通信过程:

//The data array is some array with output data
//The size array is two-byte array to store frame-length bytes
//The results array is for device's response
//The part array is for part of the response that's currently in read buffer
port.Write(data, 0, data.Length);
//Receiving device's response (if there's any)
try
{
    port.Read(size, 0, 2); //Read first two bytes (packet's length) of the response
    //We'll store entire response in results array. We get its size from first two bytes of response
   //(+2 for these very bytes since they're not counted in the device's data frame)
    results = new byte[(size[0] | ((int)size[1] << 8)) + 2];
    results[0] = size[0]; results[1] = size[1]; //We'll need packet size for checksum count
    //Time to read rest of the response
    for(offset = 2; offset < results.Length && port.BytesToRead > 0; offset += part.Length)
    {
        System.Threading.Thread.Sleep(5); //Device's quite slow, isn't it
        try
        {
            part = new byte[port.BytesToRead];
            port.Read(part, 0, part.Length); //Here's where old data is being read
        }
        catch(System.TimeoutException)
        {
                //Handle it somehow
        }
        Buffer.BlockCopy(part, 0, results, offset, part.Length);
    }
    if(offset < results.Length) //Something went wrong during receiving response
        throw new Exception();
}
catch(Exception)
{
    //Handle it somehow
}

串行端口 - C#:与设备通信期间数据缓冲区的奇怪偏移

你犯了一个传统的错误,你不能忽略 Read() 的返回值。 它告诉您实际接收了多少字节。 它将至少为 1,不超过计数。 然而,接收缓冲区中存在许多,BytesToRead告诉你。 只需继续调用 Read(),直到您满意为止:

int cnt = 0;
while (cnt < 2) cnt += port.Read(size, cnt, 2 - cnt);

只需在代码的第二部分使用相同的代码,这样您就不会在没有 Sleep() 调用的情况下烧毁 100% 的核心。 请记住,当您读取大小时,TimeoutException 的可能性也很大,实际上更有可能。 如果在 cnt> 0 时抛出它,则这是一个致命的异常,您无法再重新同步。

好吧,很奇怪,但是当我分开读取前两个字节时:

port.Read(size, 0, 1); //Read first two bytes (packet's length) of the response
port.Read(size, 1, 1); //Second time, lol

无论我从设备接收哪种数据包,一切都很好。

SerialPort 的文档包含以下文本:

因为 SerialPort 类缓冲数据,并且流包含在 BaseStream 属性没有,两者可能会在如何 许多字节可供读取。属性可以 指示有要读取的字节,但这些字节可能不是 可访问 BaseStream 属性中包含的流,因为 它们已缓冲到 SerialPort 类。

这能解释为什么BytesToRead给你令人困惑的价值观吗?

就个人而言,我总是使用 DataReceived 事件,在我的事件处理程序中,我使用 ReadExisting() 读取所有立即可用的数据并将其添加到我自己的缓冲区中。我不试图在该级别对数据流施加任何意义,我只是缓冲它;相反,我通常会编写一个小的状态机,一次从缓冲区中取出一个字符,并将数据解析为所需的任何格式。

或者,您可以使用 ReactiveExtensions 生成接收字符的可观察序列,然后在该序列上分层观察者。我用几个扩展方法做到这一点,比如:

public static class SerialObservableExtensions
    {
    static readonly Logger log = LogManager.GetCurrentClassLogger();
    /// <summary>
    ///     Captures the <see cref="System.IO.Ports.SerialPort.DataReceived" /> event of a serial port and returns an
    ///     observable sequence of the events.
    /// </summary>
    /// <param name="port">The serial port that will act as the event source.</param>
    /// <returns><see cref="IObservable{Char}" /> - an observable sequence of events.</returns>
    public static IObservable<EventPattern<SerialDataReceivedEventArgs>> ObservableDataReceivedEvents(
        this ISerialPort port)
        {
        var portEvents = Observable.FromEventPattern<SerialDataReceivedEventHandler, SerialDataReceivedEventArgs>(
            handler =>
                {
                log.Debug("Event: SerialDataReceived");
                return handler.Invoke;
                },
            handler =>
                {
                // We must discard stale data when subscribing or it will pollute the first element of the sequence.
                port.DiscardInBuffer();
                port.DataReceived += handler;
                log.Debug("Listening to DataReceived event");
                },
            handler =>
                {
                port.DataReceived -= handler;
                log.Debug("Stopped listening to DataReceived event");
                });
        return portEvents;
        }
    /// <summary>
    ///     Gets an observable sequence of all the characters received by a serial port.
    /// </summary>
    /// <param name="port">The port that is to be the data source.</param>
    /// <returns><see cref="IObservable{char}" /> - an observable sequence of characters.</returns>
    public static IObservable<char> ReceivedCharacters(this ISerialPort port)
        {
        var observableEvents = port.ObservableDataReceivedEvents();
        var observableCharacterSequence = from args in observableEvents
                                          where args.EventArgs.EventType == SerialData.Chars
                                          from character in port.ReadExisting()
                                          select character;
        return observableCharacterSequence;
        }
}

ISerialPort 接口只是我从 SerialPort 类中提取的一个标头接口,这使我在进行单元测试时更容易模拟它。