Чтение с Arduino Serial USB с использованием C++

В моем Arduino я загрузил программу

void setup() {
  Serial.begin(9600);
}

void loop() {
  Serial.println("Hello from Arduino!");
  delay(1000);
}

На своем компьютере (Raspberry Pi) я запускаю следующую программу на C++, в основном скопированную с https://blog.mbedded.ninja/programming/operating-systems/linux/linux-serial-ports-using-c-cpp/

// Заголовки библиотеки C
#include <stdio.h>
#include <string.h>

// Заголовки Linux
#include <fcntl.h> // Содержит файловые элементы управления, такие как O_RDWR
#include <errno.h> // Целочисленная ошибка и функция strerror()
#include <termios.h> // Содержит определения управления терминалом POSIX
#include <unistd.h> // запись(), чтение(), закрытие()

int main(int argc, char *argv[])
{
    // Открываем последовательный порт. При необходимости измените путь к устройству (в настоящее время установлено стандартное устройство с кабелем FTDI USB-UART)
    int serial_port = open("/dev/ttyACM0", O_RDWR);

    // Создаем новую структуру termios, мы называем ее 'tty' по соглашению
    termios tty = {};

    // Чтение существующих настроек и обработка любой ошибки
    if(tcgetattr(serial_port, &tty) != 0) {
        printf("Error %i from tcgetattr: %s\n", errno, strerror(errno));
    }

    tty.c_cflag &= ~PARENB; // Очистить бит четности, отключив четность (чаще всего)
    tty.c_cflag &= ~CSTOPB; // Очистить стоповое поле, для связи используется только один стоповый бит (чаще всего)
    tty.c_cflag |= CS8; // 8 бит на байт (чаще всего)
    tty.c_cflag &= ~CRTSCTS; // Отключить аппаратное управление потоком RTS/CTS (чаще всего)
    tty.c_cflag |= CREAD | CLOCAL; // Включаем READ & игнорировать строки ctrl (CLOCAL = 1)

    tty.c_lflag &= ~ICANON;
    tty.c_lflag &= ~ECHO; // Отключить эхо
    tty.c_lflag &= ~ECHOE; // Отключить стирание
    tty.c_lflag &= ~ECHONL; // Отключаем эхо новой строки
    tty.c_lflag &= ~ISIG; // Отключить интерпретацию INTR, QUIT и SUSP
    tty.c_iflag &= ~(IXON | IXOFF | IXANY); // Отключить s/w управление потоком
    tty.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL); // Отключить любую специальную обработку полученных байтов

    tty.c_oflag &= ~OPOST; // Предотвратить специальную интерпретацию выходных байтов (например, символы новой строки)
    tty.c_oflag &= ~ONLCR; // Предотвратить преобразование новой строки в возврат каретки/перевод строки

    tty.c_cc[VTIME] = 50;    // Подождать до 1 с (10 децисекунд), возвращаясь, как только будут получены какие-либо данные.
    tty.c_cc[VMIN] = 0;

    // Устанавливаем скорость входящей/исходящей передачи 9600
    cfsetispeed(&tty, B9600);
    cfsetospeed(&tty, B9600);

    // Сохраняем настройки tty, а также проверяем на наличие ошибок
    if (tcsetattr(serial_port, TCSANOW, &tty) != 0) {
        printf("Error %i from tcsetattr: %s\n", errno, strerror(errno));
    }

    // Запись в последовательный порт
    const char* msg = "Hello from raspberry pi!\n";
    write(serial_port, msg, sizeof(msg));

    while (true) {
        // Чтение байтов. Поведение read() (например, блокирует?,
        // как долго он блокируется?) зависит от конфигурации
        // настройки выше, в частности, VMIN и VTIME
        char chr;
        int num_bytes = read(serial_port, &chr, 1);
        // n — количество прочитанных байтов. n может быть равен 0, если байты не были получены, а также может быть равен -1, чтобы сигнализировать об ошибке.
        if (num_bytes < 0) {
            printf("Error reading: %s", strerror(errno));
        }
        if (num_bytes == 1) {
            printf("%c", chr);
        }
    }

    close(serial_port);

    return 0;
}

В основном это работает, за исключением того, что первая строка, распечатанная на моем Raspberry Pi, иногда может быть искажена, а последующие распечатки иногда представляют собой пустые строки. Иногда при запуске кажется, что многие строки уже буферизованы.

например, 1

pi@raspberrypi:~ $ ./serial_read 
Hlo from Arduino!
Hello from Arduino!

например, 2

pi@raspberrypi:~ $ ./serial_read 
He
Hello from Arduino!
Hello from Arduino!
Hello from Arduino!
Hello from Arduino!
Hello from Arduino!
Hello from Arduino!

Hello from Arduino!
Hello from Arduino!

Мне интересно, почему это происходит и как я могу получить более чистый вывод.

Обновление: вот обновленная версия, которая по-прежнему имеет те же проблемы, но заботится о блокировке и обработке сигналов.

#include <string>
#include <stdexcept>
// Заголовки библиотеки C
#include <stdio.h>
#include <string.h>
#include <time.h>

// Заголовки Linux
#include <fcntl.h> // Содержит файловые элементы управления, такие как O_RDWR
#include <errno.h> // Целочисленная ошибка и функция strerror()
#include <termios.h> // Содержит определения управления терминалом POSIX
#include <unistd.h> // запись(), чтение(), закрытие()
#include <cstdlib>
#include <csignal>
#include <sys/file.h>

int serial_port;

void signalHandler( int signum ) {
   // очищаем и закрываем здесь все
   // завершаем программу
    close(serial_port);
    exit(signum);  
}

int main(int argc, char *argv[])
{
    signal(SIGINT, signalHandler);  
    // Открываем последовательный порт. При необходимости измените путь к устройству (в настоящее время установлено стандартное устройство с кабелем FTDI USB-UART)
    serial_port = open("/dev/ttyACM0", O_RDWR);

    if(flock(serial_port, LOCK_EX | LOCK_NB) == -1) {
        throw std::runtime_error("Serial port with file descriptor " + 
                                 std::to_string(serial_port) + " is already locked by another process.");
    }


    // Создаем новую структуру termios, мы называем ее 'tty' по соглашению
    termios tty = {};

    // Чтение существующих настроек и обработка любой ошибки
    if(tcgetattr(serial_port, &tty) != 0) {
        printf("Error %i from tcgetattr: %s\n", errno, strerror(errno));
    }

    tty.c_cflag &= ~PARENB; // Очистить бит четности, отключив четность (чаще всего)
    tty.c_cflag &= ~CSTOPB; // Очистить стоповое поле, для связи используется только один стоповый бит (чаще всего)
    tty.c_cflag |= CS8; // 8 бит на байт (чаще всего)
    tty.c_cflag &= ~CRTSCTS; // Отключить аппаратное управление потоком RTS/CTS (чаще всего)
    tty.c_cflag |= CREAD | CLOCAL; // Включаем READ & игнорировать строки ctrl (CLOCAL = 1)

    tty.c_lflag &= ~ICANON;
    tty.c_lflag &= ~ECHO; // Отключить эхо
    tty.c_lflag &= ~ECHOE; // Отключить стирание
    tty.c_lflag &= ~ECHONL; // Отключаем эхо новой строки
    tty.c_lflag &= ~ISIG; // Отключить интерпретацию INTR, QUIT и SUSP
    tty.c_iflag &= ~(IXON | IXOFF | IXANY); // Отключить s/w управление потоком
    tty.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL); // Отключить любую специальную обработку полученных байтов

    tty.c_oflag &= ~OPOST; // Предотвратить специальную интерпретацию выходных байтов (например, символы новой строки)
    tty.c_oflag &= ~ONLCR; // Предотвратить преобразование новой строки в возврат каретки/перевод строки

    tty.c_cc[VTIME] = 0;    // Подождать до 1 с (10 децисекунд), возвращаясь, как только будут получены какие-либо данные.
    tty.c_cc[VMIN] = 0;

    // Устанавливаем скорость входящей/исходящей передачи 9600
    cfsetispeed(&tty, B9600);
    cfsetospeed(&tty, B9600);

    // Сохраняем настройки tty, а также проверяем на наличие ошибок
    if (tcsetattr(serial_port, TCSANOW, &tty) != 0) {
        printf("Error %i from tcsetattr: %s\n", errno, strerror(errno));
    }

    // Запись в последовательный порт
    const char* msg = "Hello from raspberry pi!\n";
    write(serial_port, msg, sizeof(msg));

    while (true) {
        // Чтение байтов. Поведение read() (например, блокирует?,
        // как долго он блокируется?) зависит от конфигурации
        // настройки выше, в частности, VMIN и VTIME
        char chr;
        int num_bytes;
        do {
            num_bytes = read(serial_port, &chr, 1);
            if (num_bytes < 0) {
                printf("Error reading: %s", strerror(errno));
                break;
            }
            if (num_bytes == 1) {
                printf("%c", chr);

            }
        }
        while (num_bytes);

        /* wait for 10ms before reading again,
           important to yield regularly so you don't lock up the CPU */
        nanosleep((const struct timespec[]){{0, 10000000L}}, NULL);
        // n — количество прочитанных байтов. n может быть равен 0, если байты не были получены, а также может быть равен -1, чтобы сигнализировать об ошибке.
    }

    return 0;
}

Обновление: Я только что заметил, что вывод Serial Monitor из Arduino IDE имеет ту же проблему!

Hno!
Hello from Arduino!
Hello from Arduino!
Hello from Arduino!
Hello from Arduino!
Hello from Arduino!
Hello from Arduino!

, 👍0

Обсуждение

ваш вопрос не связан с Arduino... он принадлежит https://stackoverflow.com/questions, @jsotola

почему вы удивляетесь, что первая строка неполная? ... дождаться конца строки перед отображением входящего текста, @jsotola

@jsotola Код печатает вводимый символ за символом. Не могли бы вы предложить мне буферизовать символы, пока я не увижу \n, а затем распечатать все буферизованные символы? Я предполагаю, что это будет означать, что \n особенный, но все же я не вижу, как это повлияет на ранее буферизованные символы., @Mark

Также я заметил, что при вызове printf("%c", chr); может быть num_bytes равен 0 вместо 1. Я отредактировал программу для защиты от этого, но поведение осталось прежним., @Mark

если вы включаете радио, вы всегда слышите первую песню с самого начала? ... игнорировать все символы, пока не будет получено \n, @jsotola

По вашей аналогии песня «Привет от Arduino!\n», поэтому я никогда не ожидал услышать «Привет от Arduino!\n» (пример 1) или «Он\n» (пример 2), @Mark

Пожалуйста, покажите нам скетч, который вы используете для получения данных., @VE7JRO

Я открываю Arduino IDE и нажимаю на последовательный монитор. Когда я открываю Arduino IDE, скрипт, который заполняет текстовое поле, является первым блоком кода в вопросе., @Mark


1 ответ


2

Установите тайм-аут блокировки равным 0 с помощью:

tty.c_cc[VTIME] = 0;
tty.c_cc[VMIN] = 0;

Таким образом, вам не нужно ждать в read() поступления данных: просто возьмите то, что доступно, распечатайте и продолжайте работу.

Затем включите time.h и попробуйте переписать свой цикл следующим образом:

    while (true) {
        // Чтение байтов. Поведение read() (например, блокирует?,
        // как долго он блокируется?) зависит от конфигурации
        // настройки выше, в частности, VMIN и VTIME
        char chr;
        int num_bytes = 0;
        do {
            num_bytes = read(serial_port, &chr, 1);
            if (num_bytes < 0) {
                printf("Error reading: %s", strerror(errno));
                break;
            }
            if (num_bytes == 1) {
                printf("%c", chr);
            }
        }
        while (num_bytes);
        /* wait for 10ms before reading again,
           important to yield regularly so you don't lock up the CPU */
        nanosleep((const struct timespec[]){{0, 10000000L}}, NULL);
    }

Поскольку вы на самом деле никогда не закрываете порт (я предполагаю, что вы используете CTRL+C для завершения выполнения), он, скорее всего, останется открытым при каждом запуске этого скрипта, поэтому строки, которые вы заметили, уже буферизуются, когда вы «переоткрываете» его. Вы можете посмотреть, как использовать сигналы для обнаружения CTRL+C и выхода из цикла и вашего процесса. А пока просто посчитайте полученные байты, пока не дойдете до некоторого максимума, а затем завершите цикл, просто чтобы посмотреть, все ли работает так, как ожидалось.

,

Спасибо, попробую ваше предложение. У вас есть теория о том, почему исходные данные искажены?, @Mark

Я опробовал ваши предложения, включая установку обработчика сигналов, но в поведении программы нет никакой разницы., @Mark

@Mark Обновите вопрос с помощью нового сценария. Вы все еще получаете недостающие символы в середине потока? Я обычно думаю, что это, вероятно, связано с тем, как вы настроили последовательный порт, больше ничего не имеет смысла. Вы используете Arduino Uno, подключенную через USB к RPi? Serial Monitor печатает данные правильно и без искажений?, @SoreDakeNoKoto

Как будто какой-то другой процесс читает из того же потока, что и вы. Вы можете попробовать материал из шагов 15 и 16 вашего руководства, посмотреть, помогут ли они обеспечить эксклюзивный доступ и, по крайней мере, исключить такую возможность., @SoreDakeNoKoto

На ваши вопросы, да, все еще отсутствуют символы, и я использую Arduino Mega, подключенную через USB к RPi. Серийный монитор не искажен. Я попробую выполнить шаги 15 и 16 и отчитаюсь, а также опубликую обновленный скрипт. Спасибо!, @Mark

Шаг 15 не применялся, потому что в моей системе нет файла /etc/inittab, @Mark

О... Я только что заметил, что у последовательного монитора такая же проблема!, @Mark

@Mark Хорошо, это к чему-то приводит, тогда ваш нынешний сценарий C, вероятно, не виноват. Проверьте свой список запущенных процессов, посмотрите, может быть, у вас есть какой-то старый экземпляр вашего скрипта, который все еще работает и считывает данные из порта, даже если это не имеет смысла. Затем подключите Arduino к компьютеру. Посмотрите, не получает ли последовательный монитор ПК (также попробуйте Putty) искаженные символы, как RPi. Некоторая комбинация этих тестов должна помочь сузить круг вопросов., @SoreDakeNoKoto