Как открывать, читать и писать из последовательного порта в C?

140

Я немного запутался в чтении и записи в последовательный порт. У меня есть USB-устройство в Linux, которое использует драйвер конвертера последовательного USB-устройства FTDI. Когда я подключаю его, он создает: / dev / ttyUSB1.

Я думал, что будет просто открыть и читать / писать из него на C. Я знаю скорость передачи и информацию о четности, но похоже, что для этого нет стандарта?

Я что-то упускаю или кто-то может указать мне правильное направление?

гничи
источник
1
РЕДАКТИРОВАТЬ: Я бы посмотрел на ссылку Ribram. Однако суть остается в том, что, хотя последовательное устройство представлено в виде файла, устройства часто имеют более конкретные интерфейсы, реализованные с помощью системных вызовов, таких как ioctlи fcntl.
Мистер Шикаданс,
1
Понимание UNIX termios VMIN и VTIME - отличный ресурс для понимания VTIME и VMIN, которые используются для обработки характеристик блокировки read () на последовательном порту.
flak37
Не используйте код из "Serial Programming HOWTO" Фреркинга, упомянутый в первом комментарии. Они не написаны для совместимости с POSIX, поэтому примеры кода не переносимы и могут не работать для вас надежно.
опилки

Ответы:

249

Я написал это очень давно ( с 1985 по 1992 год, с тех пор было внесено всего несколько изменений ) и просто копирую и вставляю нужные биты в каждый проект.

Вы должны позвонить cfmakerawпо ttyполученному от tcgetattr. Вы не можете обнулить a struct termios, настроить его, а затем установить с ttyпомощью tcsetattr. Если вы используете метод обнуления, то вы будете испытывать необъяснимые периодические сбои, особенно в BSD и OS X. «Необъяснимые периодические сбои» включают зависание read(3).

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

int
set_interface_attribs (int fd, int speed, int parity)
{
        struct termios tty;
        if (tcgetattr (fd, &tty) != 0)
        {
                error_message ("error %d from tcgetattr", errno);
                return -1;
        }

        cfsetospeed (&tty, speed);
        cfsetispeed (&tty, speed);

        tty.c_cflag = (tty.c_cflag & ~CSIZE) | CS8;     // 8-bit chars
        // disable IGNBRK for mismatched speed tests; otherwise receive break
        // as \000 chars
        tty.c_iflag &= ~IGNBRK;         // disable break processing
        tty.c_lflag = 0;                // no signaling chars, no echo,
                                        // no canonical processing
        tty.c_oflag = 0;                // no remapping, no delays
        tty.c_cc[VMIN]  = 0;            // read doesn't block
        tty.c_cc[VTIME] = 5;            // 0.5 seconds read timeout

        tty.c_iflag &= ~(IXON | IXOFF | IXANY); // shut off xon/xoff ctrl

        tty.c_cflag |= (CLOCAL | CREAD);// ignore modem controls,
                                        // enable reading
        tty.c_cflag &= ~(PARENB | PARODD);      // shut off parity
        tty.c_cflag |= parity;
        tty.c_cflag &= ~CSTOPB;
        tty.c_cflag &= ~CRTSCTS;

        if (tcsetattr (fd, TCSANOW, &tty) != 0)
        {
                error_message ("error %d from tcsetattr", errno);
                return -1;
        }
        return 0;
}

void
set_blocking (int fd, int should_block)
{
        struct termios tty;
        memset (&tty, 0, sizeof tty);
        if (tcgetattr (fd, &tty) != 0)
        {
                error_message ("error %d from tggetattr", errno);
                return;
        }

        tty.c_cc[VMIN]  = should_block ? 1 : 0;
        tty.c_cc[VTIME] = 5;            // 0.5 seconds read timeout

        if (tcsetattr (fd, TCSANOW, &tty) != 0)
                error_message ("error %d setting term attributes", errno);
}


...
char *portname = "/dev/ttyUSB1"
 ...
int fd = open (portname, O_RDWR | O_NOCTTY | O_SYNC);
if (fd < 0)
{
        error_message ("error %d opening %s: %s", errno, portname, strerror (errno));
        return;
}

set_interface_attribs (fd, B115200, 0);  // set speed to 115,200 bps, 8n1 (no parity)
set_blocking (fd, 0);                // set no blocking

write (fd, "hello!\n", 7);           // send 7 character greeting

usleep ((7 + 25) * 100);             // sleep enough to transmit the 7 plus
                                     // receive 25:  approx 100 uS per char transmit
char buf [100];
int n = read (fd, buf, sizeof buf);  // read up to 100 characters if ready to read

Значения скорости являются B115200, B230400, B9600, B19200, B38400, B57600, B1200, B2400, B4800и т.д. Значение четности не 0(то есть без проверки четности), PARENB|PARODD(включить четность и использовать нечетный), PARENB(включить четность и использовать даже), PARENB|PARODD|CMSPAR(четность знака), и PARENB|CMSPAR( пространственный паритет).

«Blocking» устанавливает, read()ожидает ли порт на порту прибытия указанного количества символов. Установка отсутствия блокировки означает, что read()возвращается любое количество доступных символов, не дожидаясь большего, до предела буфера.


Дополнение:

CMSPARнужен только для выбора четности меток и пробелов, что редко. Для большинства приложений его можно не указывать. Мой заголовочный файл /usr/include/bits/termios.hпозволяет определять CMSPARтолько в том случае, если определен символ препроцессора __USE_MISC. Это определение встречается (в features.h) с

#if defined _BSD_SOURCE || defined _SVID_SOURCE
 #define __USE_MISC     1
#endif

Во вступительных комментариях <features.h>говорится:

/* These are defined by the user (or the compiler)
   to specify the desired environment:

...
   _BSD_SOURCE          ISO C, POSIX, and 4.3BSD things.
   _SVID_SOURCE         ISO C, POSIX, and SVID things.
...
 */
Wallyk
источник
1
@wallyk: На моем компьютере нет файлов с именем ttyUSB, единственные файлы с именем USB - это «usbmon». Но у ПК действительно много USB-портов. Итак, как мне их настроить?
Bas
3
@Bas: Если это Linux, используйте команду, lsusbчтобы увидеть все USB-устройства. Они могут называться по-другому, если в вашей системе есть собственные udevправила; см. /etc/udev/rules.d/ Может быть, оттуда вы сможете выбрать порт, который ищете. Конечно, указав, а затем отключив / отключив порт, вы сможете определить разницу.
wallyk
1
@ wallyk Я не могу получить какой-либо вывод (не могу писать) с использованием четности пространства (PARENB | CMSPRAR). Но я умею общаться с маркой Parity. Есть идеи, как это решить?
Bas
6
Критику этого кода см. На stackoverflow.com/questions/25996171/…
опилки
2
Например, я отправил данные на устройство ttyUSB0, и они вышли из моего устройства tty, которое я фактически использовал. Я буквально спамил свой терминал, используя этот код. Ответ ниже из опилок - более безопасная реализация.
Owl
51

Для демонстрационного кода, который соответствует стандарту POSIX, как описано в разделах «Правильная настройка режимов терминала» и « Руководство по последовательному программированию для операционных систем POSIX» , предлагается следующее.
Этот код должен правильно выполняться с использованием Linux на процессорах x86, а также на процессорах ARM (или даже CRIS).
По сути, он основан на другом ответе, но неточные и вводящие в заблуждение комментарии были исправлены.

Эта демонстрационная программа открывает и инициализирует последовательный терминал со скоростью 115200 бод для неканонического режима, который является максимально портативным.
Программа передает жестко закодированную текстовую строку на другой терминал и откладывает выполнение вывода.
Затем программа входит в бесконечный цикл для приема и отображения данных с последовательного терминала.
По умолчанию полученные данные отображаются в виде шестнадцатеричных байтовых значений.

Чтобы программа обрабатывала полученные данные как коды ASCII, скомпилируйте программу с символом DISPLAY_STRING, например

 cc -DDISPLAY_STRING demo.c

Если полученные данные представляют собой текст ASCII (а не двоичные данные), и вы хотите читать их как строки, заканчивающиеся символом новой строки, см. Этот ответ для примера программы.


#define TERMINAL    "/dev/ttyUSB0"

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

int set_interface_attribs(int fd, int speed)
{
    struct termios tty;

    if (tcgetattr(fd, &tty) < 0) {
        printf("Error from tcgetattr: %s\n", strerror(errno));
        return -1;
    }

    cfsetospeed(&tty, (speed_t)speed);
    cfsetispeed(&tty, (speed_t)speed);

    tty.c_cflag |= (CLOCAL | CREAD);    /* ignore modem controls */
    tty.c_cflag &= ~CSIZE;
    tty.c_cflag |= CS8;         /* 8-bit characters */
    tty.c_cflag &= ~PARENB;     /* no parity bit */
    tty.c_cflag &= ~CSTOPB;     /* only need 1 stop bit */
    tty.c_cflag &= ~CRTSCTS;    /* no hardware flowcontrol */

    /* setup for non-canonical mode */
    tty.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
    tty.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
    tty.c_oflag &= ~OPOST;

    /* fetch bytes as they become available */
    tty.c_cc[VMIN] = 1;
    tty.c_cc[VTIME] = 1;

    if (tcsetattr(fd, TCSANOW, &tty) != 0) {
        printf("Error from tcsetattr: %s\n", strerror(errno));
        return -1;
    }
    return 0;
}

void set_mincount(int fd, int mcount)
{
    struct termios tty;

    if (tcgetattr(fd, &tty) < 0) {
        printf("Error tcgetattr: %s\n", strerror(errno));
        return;
    }

    tty.c_cc[VMIN] = mcount ? 1 : 0;
    tty.c_cc[VTIME] = 5;        /* half second timer */

    if (tcsetattr(fd, TCSANOW, &tty) < 0)
        printf("Error tcsetattr: %s\n", strerror(errno));
}


int main()
{
    char *portname = TERMINAL;
    int fd;
    int wlen;
    char *xstr = "Hello!\n";
    int xlen = strlen(xstr);

    fd = open(portname, O_RDWR | O_NOCTTY | O_SYNC);
    if (fd < 0) {
        printf("Error opening %s: %s\n", portname, strerror(errno));
        return -1;
    }
    /*baudrate 115200, 8 bits, no parity, 1 stop bit */
    set_interface_attribs(fd, B115200);
    //set_mincount(fd, 0);                /* set to pure timed read */

    /* simple output */
    wlen = write(fd, xstr, xlen);
    if (wlen != xlen) {
        printf("Error from write: %d, %d\n", wlen, errno);
    }
    tcdrain(fd);    /* delay for output */


    /* simple noncanonical input */
    do {
        unsigned char buf[80];
        int rdlen;

        rdlen = read(fd, buf, sizeof(buf) - 1);
        if (rdlen > 0) {
#ifdef DISPLAY_STRING
            buf[rdlen] = 0;
            printf("Read %d: \"%s\"\n", rdlen, buf);
#else /* display hex */
            unsigned char   *p;
            printf("Read %d:", rdlen);
            for (p = buf; rdlen-- > 0; p++)
                printf(" 0x%x", *p);
            printf("\n");
#endif
        } else if (rdlen < 0) {
            printf("Error from read: %d: %s\n", rdlen, strerror(errno));
        } else {  /* rdlen == 0 */
            printf("Timeout from read\n");
        }               
        /* repeat read to get full message */
    } while (1);
}
опилки
источник
1
Многое из этого можно было бы заменить cfmakerawправильным?
CMCDragonkai
Другие примеры, которые я видел, также открывают порт с помощью O_NDELAYили O_NONBLOCK. В cmrr.umn.edu/~strupp/serial.html упоминается, что если вы откроете файловый дескриптор с этими флагами, то VTIMEигнорируется. Тогда в чем разница между запуском с O_NONBLOCKфайловым дескриптором и его использованием VTIME?
CMCDragonkai
@CMCDragonkai - Это намного сложнее, чем вы написали. См. Stackoverflow.com/questions/25996171/…, который ссылается на принятый ответ на этот вопрос. Кстати, даже если вы откроете терминал в неблокирующем режиме, вы все равно сможете вернуться в режим блокировки с помощью fcntl ()
опилки
Извините за вопрос новичка, но где вы выходите из цикла do while в главном меню или он повторяется навсегда?
bakalolo
1
@bakalolo - это простой демонстрационный код, который можно получать и отображать вечно. Цель - переносимый код, который будет компилироваться (без ошибок) и работать надежно (в отличие от другого ответа). Может быть добавлен тест для определения конца сообщения; с необработанными данными определение пакета сообщения зависит от протокола. Или этот код можно изменить, чтобы просто сохранить полученные данные в кольцевом буфере для обработки другим потоком, как описано в этом ответе .
опилки