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

时间:2019-06-25 15:30:25

标签: c++ linux serial-port nmea

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

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

Output from picocom

我的程序的结果很接近,但有时混合在一起。

Output from my program

这是我的代码:

#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标志引起的?

3 个答案:

答案 0 :(得分: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

答案 1 :(得分:2)

  

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()的缓冲区大小必须比实际缓冲区大小小1,以便为字符串终止符保留至少一个字节的位置。

您的代码的另一个问题是无法测试 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问题。 br /> 如果您打算阅读和处理程序中文本的 lines 行,则您不想模仿 picocom 并使用非规范的阅读。
相反,您可以并且应该使用规范的I / O,以便 read()将在缓冲区中返回完整的行(假设缓冲区足够大)。
您的程序不是从串行端口读取,而是从串行 terminal 读取。当接收到的数据是行终止文本时,当终端设备将为您解析接收到的数据并检测行终止字符时,没有理由读取原始字节。
不用执行其他答案中建议的所有额外编码,而是利用系统中已内置的功能。

有关阅读行,请参见Serial Communication Canonical Mode Non-Blocking NL DetectionWorking with linux serial port in C, Not able to get full data


附录

  

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

我不知道该怎么写。

您已阅读termios man 页面吗?

  

规范模式下:

     
      
  • 逐行提供输入。输入行是   当键入行定界符之一(NL,EOL,EOL2;或   开始时的EOF    行)。除EOF以外,行分隔符包含在 read (2)返回的缓冲区中。
  •   

  

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

您是否想知道我对“完整” 的理解与您对“完整” 的使用是否有所不同?

您是否读过我已经写过的注释?“如果您按照我的建议编写程序,[然后] $应该是缓冲区中的第一个字符”
是的, 您应该期望”“对read()的每次调用都将返回带有$ ...的整行”

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

答案 2 :(得分:1)

如果您只想在终端上正确打印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所示,这只是对原始代码的一个快速调整,以使其至少在视觉上可以正常工作,但是如果您的设备以高频发送帧,则可能无法正常工作。< / p>

当然,还有更多的方法可以执行此操作,对于termios的此特定问题,我能想到的最好的方法是使用规范读取。例如,参见来自TLDP的this example

enter image description here