Как читать с последовательного порта, как Picocom на Linux? - PullRequest
2 голосов
/ 25 июня 2019

У меня есть модуль GPS, который отправляет данные (предложение NMEA) каждые 1 секунду на последовательный порт.Я пытался прочитать это из программы на 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 правильно отображает данные?Это из-за размера моего буфера или, может быть, VTIME и VMIN флагов?

Ответы [ 3 ]

2 голосов
/ 26 июня 2019

Как Picocom правильно отображает данные?

«Правильность» отображаемого результата - это просто человеческая тенденция воспринимать или приписывать «порядок» (и / или образец) естественным событиям.

Picocom - это просто " минимальная программа эмуляции немого терминала ", которая, как и другие программы эмуляции терминала, просто отображает то, что получено.
Вы можете настроитьповедение завершения строки, например добавление возврата каретки при получении перевода строки (чтобы текстовые файлы 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 и использовать неканоническое чтение.
Вместо этоговы можете и должны использовать канонический ввод-вывод, чтобы read () возвращал полную строку в вашем буфере (при условии, что буфер достаточно большой).
Ваша программа не читает с последовательного интерфейса порт , но с последовательного терминала .Когда принятые данные представляют собой завершенный строкой текст, нет смысла читать необработанные байты, когда оконечное устройство проанализирует полученные для вас данные и обнаружит символы завершения строки.
Вместо выполнения всего дополнительного кодирования, предложенного в другом ответе, используйте возможности, уже встроенные в систему.

Для чтения строк см. Последовательная связь Канонический режим Неблокирующее обнаружение NL и Работа с последовательным портом linux в C, Невозможнополучить полные данные


ADDENDUM

У меня проблемы с пониманием " Вместо этого вы можете и должны использовать канонический I/ O, чтобы read () вернула полную строку в вашем буфере".

Я не знаю, как написать это, чтобы быть более понятным.

Читали ли вы страницу termios man ?

В каноническом режиме:

  • Ввод доступен построчно. Строка ввода доступно, если введен один из разделителей строк (NL, EOL, EOL2; или EOF в начале линии). За исключением случая EOF, разделитель строк включается в буфер, возвращаемый read (2).

Должен ли я ожидать, что каждый вызов read () будет возвращать полную строку с $ ... или я должен реализовать некоторую логику для чтения и заполнения буфера полной строкой текста ASCII?

Вам интересно, есть ли разница между моим значением "полный" и вашим использованием "полный" ?

Читали ли вы комментарий, где я уже писал "Если вы пишете свою программу так, как я предлагаю, [тогда], что $ должен быть первым символом в буфере" ?
Так да, вы должны ожидать "что каждый вызов read () будет возвращать полную строку с $ ..." .

Вы должны изучить то, что я уже написал, а также ссылки, предоставленные.

2 голосов
/ 25 июня 2019

Вы получаете "кадрирующие" ошибки.

Вы не можете полагаться на 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 голос
/ 26 июня 2019

Если вы просто хотите правильно распечатать кадры 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 через последовательный порт, и распечатка безупречна (см. Скриншот). Как указано в комментариях ниже, это просто быстрая настройка исходного кода, чтобы он работал должным образом, по крайней мере с визуальной точки зрения, но он может не работать, если ваше устройство отправляет кадры с высокой частотой .

Конечно, есть еще много способов сделать это, лучшее, что я могу придумать для этой конкретной проблемы с termios, - это использовать каноническое чтение. См. Например этот пример из TLDP.

enter image description here

...