如何从 Linux 上的 picocom 等串口读取数据?

How to read from serial port like picocom on Linux?

我有一个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, '[=11=]', sizeof(read_buf));

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

    return 0;
}

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

您遇到 "framing" 个错误。

你不能依赖 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

How does picocom manage to display data correctly?

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

Picocom 只是一个“minimal dumb-terminal emulation program”,与其他终端仿真程序一样,它只显示接收到的内容。
您可以调整 line-termination 行为,例如在收到换行符时附加回车符 return(以便 Unix/Linux 文本文件正确显示)。
但除此之外,您看到的显示就是收到的内容。 picocom.

没有应用任何处理或格式

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


Is is due to my buffer size or maybe VTIME and VMIN flags ?

VTIME 和 VMIN 值不是最优的,但真正的问题是您的程序有一个错误,导致某些接收到的数据显示不止一次。

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

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

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

unsigned char read_buf[80];

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

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

未能测试来自 read() 的 return 代码的错误条件是您的代码的另一个问题。
所以下面的代码是对你的改进:

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] = '[=12=]';
    std::cout << read_buf ;
}

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

您的 Linux 程序不是从串口 端口 读取,而是从串口 终端 .
当接收到的数据是 line-terminated 文本时,当(相反)终端设备(和线路规则)可以为您解析接收到的数据并检测行终止字符时,没有理由读取原始字节。
不要执行另一个答案中建议的所有额外 coding/processing,而是利用操作系统中已经内置的功能。

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


附录

I'm having troubles to understand "Instead you can and should use canonical I/O so that read() will return a complete line in your buffer".

不知道怎么写才更清楚。

你读过 termios man 页面了吗?

In canonical mode:

  • Input is made available line by line. An input line is available when one of the line delimiters is typed (NL, EOL, EOL2; or EOF at the start of line). Except in the case of EOF, the line delimiter is included in the buffer returned by read(2).

Should i expect that each call to read() will return a full line with $... or should i implement some logic to read and fill the buffer with a full line of ASCII text?

你想知道我对 "complete" 的理解与你对 "full" 的理解是否有区别吗?

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

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

如果您只想在终端上正确打印 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, '[=10=]', sizeof(read_buf));
        int n = read(serial_port, &read_buf, sizeof(read_buf));
        std::cout << read_buf;
        }
    }

return 0;
}

我已经使用下面的 gpsfeed+ which generates gps coordinates and outputs them on NMEA format through the serial port and the printout is perfect (see screenshot). As indicated in the 使用修改后的循环测试了您的代码,这只是对原始代码的快速调整以使其正常工作,至少从视觉的角度来看是这样,但它可能不会如果您的设备以高频率发送帧,则工作。

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