ttyUSBx 数据停滞,usbmon/wireshark 显示数据

ttyUSBx 数据停滞,usbmon/wireshark 显示数据

我正在开发一个基于运行 raspbian 的 raspberry pi 和通过 USB 连接的模拟/数字转换器 (ADC) 的传感器平台。ADC 应以 128Hz 提供数据,并且相当可靠。对于我的目的而言,这些数据是时间敏感的,因此在 128Hz 数据上拥有合理准确的时间戳非常重要。

我正在使用 ttyUSBx 与 ADC 交互。我发现一个问题,数据似乎停滞了,然后在稍后的时间点涌入。例如:以 128Hz 接收数据,然后发生“停滞”。大约 0.05 秒内,我没有看到任何数据接收。然后在“停滞”结束后,我预期的最后 0.05 秒内的所有数据都很快出现(比 128Hz 更快),然后数据按预期接收。

作为调试工作的一部分,我使用 wireshark 和 usbmon 来监控从 ADC 接收的帧。这些日志似乎显示接收的帧没有任何停顿。这让我很困惑。我对 Linux 如何处理 USB 没有足够的经验来解决这个问题,所以我不确定如何从这里开始(虽然不是因为没有尝试)。我知道 Linux 内核中的 TTY 层和通用 USB 层之间存在差异,但我不确定为什么 usbmon 能够看到 TTY 看不到的东西。

我还使用stress-ng对系统进行了一些压力测试,发现当I/O饱和时也会出现此问题。我不确定它们是否相关,但这是我发现的唯一与我的问题始终相似的事情。

我认为还值得注意的是,系统时钟由 NTPsec 使用 GPS 进行训练,并且我已经排除了 NTP 更新或偏移是问题的一部分。我没有看到 NTP 更新(包括跳跃)与此问题之间存在任何关联。

我的目标是找到一种以一致的 128Hz 接收数据的方法,因此我愿意接受任何允许我在 raspbian 中的当前平台上做到这一点的解决方案。真的,任何有关 TTY 的工作方式可能与 usbmon 不同的信息都将不胜感激。

编辑:这里有一些额外的信息来帮助澄清问题。如果我可以提供任何其他信息来进一步澄清问题,请告诉我。

该项目的所有代码均用 C++ 编写。

串行端口打开并O_RDWR | O_NOCTTY | O_NDELAY指向/dev/ttyUSBx(通常为0或1)并配置如下:

    tio.c_iflag = IGNBRK | IGNPAR;
    tio.c_oflag = 0;
    tio.c_cflag = CS8 | CREAD | CLOCAL;
    tio.c_lflag = 0;
    tio.c_cc[VTIME] = 1;
    tio.c_cc[VMIN]  = 0;

相关代码如下:

int driver_serial::init() {


    //
    // Open the serial port device file.
    //

    // O_RDWR   - Open for reading and writing.
    // O_NOCTTY - The port never becomes the controlling terminal for 
    //            the process.
    // O_NDELAY - Use no-blocking I/O. Ignore control characters and 
    //            transmit raw data- REMOVED as it overwrites VMIN and VTIME

    if ((fd = open((char *) port.c_str(), O_RDWR | O_NOCTTY | O_NDELAY)) < 0) {
        std::cerr << "Couldn't open serial port " << port << std::endl;
        return (-1);
    }

    //
    // Check if we are indeed dealing with a serial device.
    //

    if (!isatty(fd)) {
        fprintf(stderr,"The specified port does not correspond "
                "to a serial device!\n");
        return (-1);
    }

    //
    // Get the current configuration of the serial interface.
    //

    if (tcgetattr(fd, &tio) < 0) {
        std::cerr << "Could not get confiuration of serial port " << port
                << std::endl;
        return (-1);
    }

    //
    // Set the input flags (c_iflag).
    //

    // IGNBRK    -  Ignore break conditions
    // IGNPAR    -  Ignore parity errors
    tio.c_iflag = IGNBRK | IGNPAR;

    //
    // Set the output flags (c_oflag)
    //

    // No output processing.
    tio.c_oflag = 0;

    //
    // Set the flag constants (c_cflag)
    //

    // CS8     - Eight bits per byte
    // CREAD   - Enable to receive data
    // CLOCAL  - Ignore modem control lines
    tio.c_cflag = CS8 | CREAD | CLOCAL;

    //   
    // Set the local flags (c_lflag)
    //

    // No higher-level input processing, non-canonical.
    tio.c_lflag = 0;

    //
    // Set some special characters (c_cc).
    //

    // Wait max. 10 ms for one byte to receive.
    tio.c_cc[VTIME] = 1;
    tio.c_cc[VMIN]  = 0;

    //
    // Write serial port configuration
    //
    if (tcsetattr(fd, TCSADRAIN, &tio) < 0) {
        fprintf(stderr,"Serial port configuration could not be written!");
        return (-1);
    }

    //
    // Flush the serial port buffer
    //
    tcflush(fd, TCIOFLUSH);

    // Serial port successfully configured.
    return (0);

}

这些数据被传递给接收函数,该函数不断从串行端口读取数据并将其插入到缓冲区中,buf直到找到终止字符。收到第一个字符时,将保存数据的时间戳。以下是相关代码:

int driver_serial::receive(char *buf, int buflen, double timeout,
        struct timespec *recv_time) {

    // Received char
    unsigned char c;

    // Counter for received termination characters
    int n = 0;
    // Counter for received characters
    int i = 0;

    // Starting time and current time
    struct timespec start_time, end_time;

    //////////////////////////////////////////////////////////////////////////////
    //
    // Receive data until termination characters arrive.
    //
    //////////////////////////////////////////////////////////////////////////////

    // Empty the buffer (may be dangerous if buflen is longer than the buffer)
    memset(buf, 0, buflen);

    // Timeout in nanoseconds
    if (timeout>0){
        clock_gettime(CLOCK_REALTIME, &start_time);
    }

    // Repeat loop until all termination characters have been received
    while (n < termlen) {
        
        //
        // Check for timeout
        //
        if (timeout > 0) {
            clock_gettime(CLOCK_REALTIME, &end_time);
            if ( (end_time.tv_sec-start_time.tv_sec)+end_time.tv_nsec/1e9 >=
                start_time.tv_nsec/1e9+timeout ) {
                fprintf(stderr, "Warning: Timeout. No response received.\n");
                return (-1);
            }
        }
        
        // 
        // We receive something. Let's check. 
        //
        if (read(fd, &c, 1) > 0) {

            // A correct termination character has arrived
            if (c == term[n]) {
                n++;
                buf[i++] = c;
            }
            // A character was received
            else if (!ignnul || c != 0) {
                // Time of first received character is saved
                if (i == 0) {
                    clock_gettime(CLOCK_REALTIME, recv_time);
                }
                // Reset termination check if not all characters are received in a row
                if (n > 0) {
                    i -= n;
                    buf[i] = 0;
                    n = 0;
                }
                // Save character to buffer
                buf[i++] = c;
            }
            // Check for buffer overrun
            if (i >= buflen) {
                flush();
                fprintf(stderr, "Warning: %d characters received."
                                "Buffer overrun.\n", buflen);
                return (-1);
            }

        }

    }

    // 
    // Receive was successful if we arrive here.
    //

    // Set zero character correctly to ignore termination characters.
    buf[i - n] = 0;

    return (0);

}

缓冲区receive传递到主循环,主循环接收函数的响应receive并对其进行解析。然后,这些数据被放入辅助缓冲区 ( buffer) 中,以便在程序的其他地方使用。这还会将函数的时间戳插入receive数据对象中。以下是相关代码:

int driver_obs_obsdaq::freerun(double freq) {

    // Time of measurement
    struct timespec recv_time, time;
    // The vector data
    data_obs_vector data(5);
    // The receive buffer
    char buf[200]="";
    // Data parser function
    int (driver_obs_obsdaq::*parse_data)(data_obs_vector* data, char* buf);


    // Infinite loop
    while (1) {

        // Receive data
        if (receive(buf, sizeof(buf), 0, &recv_time) >= 0) {

            // Set the time
            data.set_time(&recv_time);

            // Parse data from answer
            if ( (this->*parse_data)(&data, buf) >= 0 ){

                            // Calibrate the measurement/ASCII
                            driver_obs::cal->calibrate(&data);

            }

            // Write measurement to buffer
            buffer->put(&data);

        }

    }

    return (0);

}

在我们拥有一个数据对象缓冲区之后,它们最终会在其他函数中用于数据处理和过滤。其中一个简单的结果就是将其写入文件,这就是我首先注意到我的问题的原因。我写入文件的数据有停顿,但 usbmon 似乎没有这个问题。据我了解,我的问题很可能发生在接收代码获取字符以读取第一个字符之前,因为这也是我获取时间戳的地方。

答案1

[考虑将未来的编程问题发布到 Stackoverflow.com。]

“停滞”最可能的原因是(正常)处理调度和/或有缺陷的计时数据。您构建的程序占用大量 CPU。来自用户空间的读取系统调用仅从系统缓冲区获取字节。您的程序不是有效地等待(又称阻塞)数据,而是反复轮询系统(termios)缓冲区。
请注意,用户空间中的时间戳本质上是不准确的。termios
初始化不完整且不可移植。
需要重写/重新设计。


请注意,使用clock_gettime(CLOCK_REALTIME,...)测量时间间隔并不可靠。CLOCK_REALTIME可能会进行调整以与挂钟时间同步。请参阅CLOCK_REALTIME 和 CLOCK_MONOTONIC 之间的区别?


你的程序使用非阻塞 I/O,然后继续重复发出读()(仅一个字节)直到返回数据。这是低效的代码,会浪费进程的“时间片”,并且会在操作系统调度其他进程时导致明显的进程暂停。请参阅完全公平调度器

阻塞模式将允许使用 termios VMIN 和 VTIME 参数来优化读()将会回归。参见这是阻塞与非阻塞串行读取的答案了解更多详情。
不要尝试每次系统调用只读取一个字节,而是尝试每次系统调用获取尽可能多的数据。请参阅这是从串行端口解析完整消息的答案示例代码:


... 我不确定为什么 usbmon 能够看到 TTY 看不到的东西。

Usbmon 捕获数据的级别比用户空间中的程序低得多。Usbmon 依赖于内核中对 USB 堆栈的特殊访问(又称钩子),这种访问先于使用串行终端.
研究图 3Linux 串行驱动程序。USB 子系统占据该图中的“低级驱动程序”和“硬件”的底层。
您的程序仅在经过几次缓冲区复制和潜在的调度延迟后才获取数据(usbmon 已经捕获的数据)。

相关内容