我正在开发一个基于运行 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 已经捕获的数据)。