【问题标题】:How to read from serial port like picocom on Linux?如何从 Linux 上的 picocom 等串口读取数据?
【发布时间】:2021-08-23 14:28:32
【问题描述】:

我有一个 gps 模块,它每 1 秒向串口发送一次数据(NMEA 语句)。我一直在尝试从 C++ 程序中读取它。

用picocom读取串口时,数据以干净的方式显示,每一行都有一个NMEA语句)。

我的程序的结果很接近,但有时会混淆。

这是我的代码:

#include <iostream>
#include <stdio.h>
#include <string.h>
#include <fcntl.h> 
#include <errno.h> 
#include <termios.h> 
#include <unistd.h> 

int main(){

    struct termios tty;
    memset(&tty, 0, sizeof tty);

    int serial_port = open("/dev/ttyUSB0", O_RDWR);

    // Check for errors
    if (serial_port < 0) {
        printf("Error %i from open: %s\n", errno, strerror(errno));
    }

        // Read in existing settings, and handle any error
    if(tcgetattr(serial_port, &tty) != 0) {
        printf("Error %i from tcgetattr: %s\n", errno, strerror(errno));
    }

    tty.c_cflag &= ~PARENB; // Clear parity bit, disabling parity (most common)
    tty.c_cflag &= ~CSTOPB; // Clear stop field, only one stop bit used in communication (most common)
    tty.c_cflag |= CS8; // 8 bits per byte (most common)
    tty.c_cflag &= ~CRTSCTS; // Disable RTS/CTS hardware flow control (most common)
    tty.c_cflag |= CREAD | CLOCAL; // Turn on READ & ignore ctrl lines (CLOCAL = 1)
    tty.c_lflag &= ~ICANON;
    tty.c_lflag &= ~ECHO; // Disable echo
    tty.c_lflag &= ~ECHOE; // Disable erasure
    tty.c_lflag &= ~ECHONL; // Disable new-line echo
    tty.c_lflag &= ~ISIG; // Disable interpretation of INTR, QUIT and SUSP
    tty.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL); // Disable any special handling of received bytes
    tty.c_oflag &= ~OPOST; // Prevent special interpretation of output bytes (e.g. newline chars)
    tty.c_oflag &= ~ONLCR; // Prevent conversion of newline to carriage return/line feed
    tty.c_cc[VTIME] = 10;   
    tty.c_cc[VMIN] = 0;
    // Set in/out baud rate to be 9600
    cfsetispeed(&tty, B9600);
    cfsetospeed(&tty, B9600);

    // Save tty settings, also checking for error
    if (tcsetattr(serial_port, TCSANOW, &tty) != 0) {
        printf("Error %i from tcsetattr: %s\n", errno, strerror(errno));
    }

    // Allocate memory for read buffer, set size according to your needs
    char read_buf [24];
    memset(&read_buf, '\0', sizeof(read_buf));

    while(1){
        int n = read(serial_port, &read_buf, sizeof(read_buf));
        std::cout << read_buf ;
    }

    return 0;
}

picocom 如何正确显示数据?是由于我的缓冲区大小还是 VTIMEVMIN 标志?

【问题讨论】:

标签: c++ linux serial-port nmea


【解决方案1】:

picocom 如何正确显示数据?

显示输出的“正确性”仅仅是人类感知或归因于自然发生事件的“顺序”(和/或模式)的倾向。

Picocom 只是一个“minimal dumb-terminal emulation program”,与其他终端仿真程序一样,只是显示接收到的内容。
您可以调整行终止行为,例如在收到换行符时附加回车(以便 Unix/Linux 文本文件正确显示)。
但除此之外,您所看到的就是收到的内容。 picocom 没有应用任何处理或格式化。

根据您发布的输出,GPS 模块显然正在输出以换行和回车结尾的 ASCII 文本行。
无论(终端仿真器)程序如何读取此文本,即一次一个字节或每次随机数个字节,只要每个接收到的字节以与接收到的顺序相同的顺序显示,显示就会有序显示,清晰可辨。


是由于我的缓冲区大小还是 VTIME 和 VMIN 标志?

VTIME 和 VMIN 值不是最佳值,但真正的问题是您的程序存在错误,导致某些接收到的数据显示多次。

while(1){
    int n = read(serial_port, &read_buf, sizeof(read_buf));
    std::cout << read_buf ;
}

read() 系统调用只返回一个字节数(或错误指示,即 -1),而不返回字符串。
您的程序对该字节数不执行任何操作,而只是显示该缓冲区中的任何内容(和所有内容)。
每当最新的 read() 没有返回足够的字节来覆盖缓冲区中已经存在的内容时,就会再次显示旧字节。

您可以通过将原始程序的输出与以下调整进行比较来确认此错误:

unsigned char read_buf[80];

while (1) {
    memset(read_buf, '\0', sizeof(read_buf));  // clean out buffer
    int n = read(serial_port, read_buf, sizeof(read_buf) - 1);
    std::cout << read_buf ;
}

请注意,传递给 read() 的缓冲区大小需要比实际缓冲区大小小一,以便为字符串终止符保留至少一个字节位置。

未能测试 read() 的返回代码是否存在错误,这是您的代码存在的另一个问题。
所以下面的代码是对你的改进:

unsigned char read_buf[80];

while (1) {
    int n = read(serial_port, read_buf, sizeof(read_buf) - 1);
    if (n < 0) {
        /* handle errno condition */
        return -1;
    }
    read_buf[n] = '\0';
    std::cout << read_buf ;
}

您不清楚您是否只是在尝试模拟 picocom,或者您的程序的另一个版本在从您的 GPS 模块读取数据时遇到问题,您决定发布此 XY 问题。 如果您打算在程序中读取和处理 文本,那么您不想模拟 picocom 并使用非规范读取。
相反,您可以并且应该使用规范 I/O,以便 read() 将在缓冲区中返回一个完整的行(假设缓冲区足够大)。

您的 Linux 程序不是从串行端口读取,而是从串行终端读取。
当接收到的数据是行终止文本时,当终端设备(和行规)可以为您解析接收到的数据并检测行终止字符时,没有理由读取原始字节。
与其做另一个答案中建议的所有额外编码/处理,不如利用操作系统中已经内置的功能。

有关阅读行,请参阅 Serial Communication Canonical Mode Non-Blocking NL DetectionWorking with linux serial port in C, Not able to get full data,以及 Canonical Mode Linux Serial Port 以获得简单而完整的 C 程序。


附录

我很难理解“您可以而且应该使用规范 I/O,以便 read() 将在您的缓冲区中返回一个完整的行”。

我不知道怎么写才能更清楚。

您是否阅读过 termios ma​​n 页面?

规范模式下:

  • 输入逐行可用。输入线是 当键入行分隔符之一时可用(NL、EOL、EOL2;或 开头的EOF 线)。除了 EOF 的情况外,行分隔符包含在 read(2) 返回的缓冲区中。

我应该期望每次调用 read() 都会返回一个带有 $... 的完整行,还是应该实现一些逻辑来读取并用一整行 ASCII 文本填充缓冲区?

您是否想知道我对 "complete" 的含义与您对 "full" 的使用之间是否有区别?

您是否阅读了我已经写过的评论“如果您按照我的建议编写程序,[那么] $ 应该是缓冲区中的第一个字符”
所以是的, 您应该期望 “每次调用 read() 都会返回一个完整的行,其中包含 $...”

您需要研究我已经写的内容以及提供的链接。

【讨论】:

  • 是的,我对我的问题还不够清楚,这是我第一次尝试读取 gps 数据,我打算将数据存储到某个缓冲区中以处理这些数据。我不必显示它,所以我想我只会检测带有字符'$'的行的开头和它的结尾以正确存储它。我使用 picocom 来查看串口的输出,我只是想知道 picocom 是如何显示数据的。
  • 但是“您的程序不是从串行端口读取,而是从串行终端读取”是什么意思?我真的不明白为什么我的程序从串行终端读取
  • 如果您的程序在微控制器上运行,它可能是裸机,即访问 UART。但是当有像 Linux 这样的操作系统时,你就无法访问 UART。而不是 /dev/uart0 设备是 /dev/tty... 这是一个终端。见Linux serial drivers。串行终端的 read() 从系统缓冲区而不是任何硬件中获取字节。 “所以我想我只检测带有字符'$'的行的开头......” - 如果您按照我的建议编写程序,$ 应该是第一个缓冲区中的字符。
  • 好的!我现在明白了,感谢您的精确!
  • 见附录回答。
【解决方案2】:

您遇到“框架”错误。

你不能依赖 read() 总是从头到尾得到一个 NMEA 句子。

您需要将读取的数据添加到缓冲区的末尾,然后检测缓冲区中每个 NMEA 句子的开头和结尾,并在找到时从缓冲区开头删除每个检测到的句子。

像这样:

FOREVER
  read some data and add to end of buffer
  if start of buffer does not have start of NMEA sentence
    find start of first NMEA sentence in buffer
    if no sentence start found
      CONTINUE
    delete from begining of buffer to start of first sentence
  find end of first NMEA sentence in buffer
  if no sentence end in buffer
    CONTINUE
  remove first sentence from buffer and pass to processing

如果您希望 NMEA 应用程序在现实世界中可靠地工作,那么处理帧错误非常重要。这种事:

         received                                       output
$GPRMC,,V,,,,,,,,,N*53
                                                $GPRMC,,V,,,,,,,,,N*53
$GPVTG,,,,,,,,N*30
                                                $GPVTG,,,,,,,,N*30
$GPRMC,,V,,,,,,,,,N*53$GPVTG,,,,,,,,N*30
                                                $GPRMC,,V,,,,,,,,,N*53
                                                $GPVTG,,,,,,,,N*30
$GPRMC,,V,,,
                                                ----
,,,,,,N*53
                                                $GPRMC,,V,,,,,,,,,N*53

执行此操作的代码可在 https://gist.github.com/JamesBremner/291e12672d93a73d2b39e62317070b7f

【讨论】:

  • 是的,我理解逻辑,我必须检测带有“$”的 NMEA 句子的开头。但是由于 picocom 不知道这是 NMAE,它如何检测每个数据的开始和结束?是在寻找 LF 字符吗?
  • @Juju¯_(ツ)_/¯
【解决方案3】:

如果您只想在终端上正确打印 NMEA 帧,您可以先使用 FIONREAD 确定缓冲区中存储的字节数,只需将循环更改为:

// Allocate memory for read buffer, set size according to your needs
int bytesWaiting;
while(1){

    ioctl(serial_port, FIONREAD, &bytesWaiting);
    if (bytesWaiting > 1){
        char read_buf [bytesWaiting+1];
        memset(&read_buf, '\0', sizeof(read_buf));
        int n = read(serial_port, &read_buf, sizeof(read_buf));
        std::cout << read_buf;
        }
    }

return 0;
}

我已经使用 gpsfeed+ 使用修改后的循环测试了您的代码,它生成 gps 坐标并通过串行端口以 NMEA 格式输出它们,并且打印输出非常完美(参见屏幕截图)。正如下面的comments 所示,这只是对原始代码的快速调整以使其正常工作,至少从视觉角度来看是这样,但如果您的设备以高频率发送帧,它可能无法正常工作。

当然,还有很多方法可以做到这一点,对于 termios 的这个特殊问题,我能想到的最好方法是使用规范读取。例如,请参阅来自 TLDP 的 this example

【讨论】:

  • 这种方法不可靠。如果句子仅以 1Hz 发送,它可能几乎 100% 工作,但许多 NMEA 设备发送数据的频率远高于此,并且在许多应用程序中,有多个 NMEA 设备发送数据。在这种情况下,您将收到应该准备好处理的框架错误。
  • 你是对的,但问题是要求读取像 picocom 这样的数据,而这正是我的答案所提供的:对原始代码进行快速调整以使其以这种方式工作。我还指出了通过规范阅读解决这个问题的正确方法。有时您不是在寻找最佳解决方案。无论如何,我会用你的评论编辑我的答案,以确保没有人得到错误的印象。
  • 恕我直言,提供无法扩展到现实世界的解决方案是一种伤害。几年前,我的第一个 NMEA 应用程序交付给了一位帆船操作员 - 我整个夏天都在一艘帆船的内部,试图在 40 摄氏度的高温和潮湿环境中调试框架错误。
  • 我的观点相同。但不幸的是,有时或可能经常是现实世界是你必须让事情快速运转的世界。我希望每个人都有资源和意志力来写the Right Stuff
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-09-06
  • 1970-01-01
  • 2021-11-30
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多