Источник: Электронная библиотека www.ibiblio.org

Прямая ссылка на источник: http://www.ibiblio.org/pub/Linux/docs/HOWTO/other-formats/pdf/Serial-Programming-HOWTO.pdf

Программирование последовательных портов

Gary Frerking (Гэри Фреркинг)

gary@frerking.org

Peter Baumann (Питер Бауман).

История версий

Редакция 1.01 2001−08−26 Правил: glf

Новый сопровождающий, преобразовано в DocBook

Редакция 1.0 1998-01-22 Правил: phb

Первоначальная версия документа

Этот документ описывает программу коммуникации с устройствами через последовательный порт на ОС Linux.

1.  Введение

Это самоучитель по программированию последовательных портов в Linux. Описывается программная связь с внешними устройствами / компьютерами через последовательный порт под Linux. Различные техники разъясняют: канонический в/в (передаются / принимаются только законченные линии), асинхронный в/в и ожидание ввода от нескольких источников.

Это первое обновление самоучителя по программированию последовательных портов под Linux. Основная цель обновления – смена сопровождающего и преобразование документа в формат DocBook. С точки зрения технического содержания, очень мало, если вообще что-либо изменилось в настоящее время. Радикальные изменения в содержании не произойдут сразу же, так что будем работать по нему, пока есть возможность.

Если вы ждали часа, когда кто-нибудь возьмётся за сопровождение этого самоучителя, то ваше желание исполнилось. Пожалуйста пришлите мне отзывы, если таковые есть. Буду очень признателен.

Все примеры были проверены с помощью i386 Linux Kernel 2.0.29.

1.1   Авторские права

Авторскими правами на этот документ владеют (с) 1997 Питер Бауманн, (с) 2001 Гэри Фреркинг. Распространяется он в соответствии с Linux Documentation Project (LDP) лицензией, суть которой изложена ниже.

Если не указано иное, авторскими правами на документы-самоучители по Linux обладают соответствующие авторы. Документы-самоучители могут воспроизводиться или распространятся на любом физическом или электронном носителе до тех пор, пока на всех копиях сохраняется уведомление об авторских правах на этот документ. Коммерческое распространение разрешается и даже поощряется, но автор хотел бы получать уведомление обо всех таких случаях.

Все переводы, производные работы или совокупности работ, включающие любые документы-самоучители по Linux, должны быть охвачены этим уведомлением об авторских правах. Таким образом, вы не можете подготовить производные самоучители и ввести дополнительные ограничения на распространение. Исключения могут быть предоставлены при определённых условиях – обращайтесь к координатору документов-самоучителей по адресу, указанному ниже.

В общем, мы бы хотели способствовать распространению данной информации по максимально возможному числу каналов.

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

Если у вас возникли вопросы, обращайтесь linux-howto@metalab.unc.edu

1.2   Оговорки

Авторы не несут ответственности за материалы, изложенные в этом документе. Используйте концепции, примеры и другое содержимое на свой страх и риск. Поскольку речь идёт о новой редакции этого документа, могут быть ошибки и неточности, которые, конечно, могут нанести вред вашей системе. Приступайте к выполнению с осторожностью, и, хотя это маловероятно, но автор(ы) не несут никакой ответственности за это.

Все авторские права принадлежат авторам, если иное специально не оговорено. Использование терминов в данном документе не должно рассматриваться как ссылка на товарный знак или на знак обслуживания.

Называние конкретных продуктов или марок не должно рассматриваться как одобрение.

Вам настоятельно рекомендуется выполнить резервное копирование системы перед выполнением и регулярно сохранять крупные архивы.

1.3   Новые версии

Как уже упоминалось, в ближайшее время вряд ли появится что-нибудь новенькое.

1.4   Об авторах

Первоначальный автор благодарит мистера Стрюдхофа, Майкла Картера, Питера Волтенберга, Антонио Ионеллу, Грега Хэнкинса, Дейва Фальцграфа, Шона Линкольна, Майкла Видмана и Эдри Бонар.

1.5   Обратная связь

Обратная связь, безусловно, приветствуется в этом документе. Без ваших отзывов и материалов этот документ не может существовать. Пожалуйста, присылайте ваши дополнения, замечания и критику на следующий электронный адрес: gary@frerking.org.

2.  Начало

2.1   Отладка

Самый лучший способ отладки вашего кода – это запустить Linux на другой машине и соединить оба компьютера прямым кабелем. Используйте miniterm (доступен как LDP-самоучитель программиста (ftp://sunsite.unc.edu/pub/Linux/docs/LDP/programmers−guide/lpg−0.4.tar.gz в каталоге примеров) для отправки символов к Linux-машине. Miniterm может быть очень легко поставлен и будет передавать весь ввод с клавиатуры прямо на последовательный порт. Для этого только нужно определить или убедиться в наличии следующей строчки.

#define MODEMDEVICE "/dev/ttyS0"

Установите его на ttyS0 для COM1, ttyS1 для COM2 и т.д. Это необходимо для того, чтобы убедится в том, что все передаваемы напрямую символы (без обработки), находятся за линией. Чтобы проверить соединение, запустите miniterm на обоих компьютерах, только разного типа. Вводимые с одного компьютера символы должны появиться на другом. При этом нужно помнить, что вводимые символы не отражаются на мониторе компьютера-источника.

Чтобы сделать прямой кабель, нужно пересечь TxD (передача) и RxD (получение) линии. Описание кабеля – см. раздел 7.

Также возможно использовать всего один компьютер для тестирования последовательных портов, если у вас есть два неиспользованных порта. Вы можете запустить два miniterm в двух разные сессиях консоли. Если вы отсоединили мышь для освобождения последовательного порта, не забудьте перенаправить /dev/mouse. Если вы используете много портовую последовательную карту, удостоверьтесь в правильной её настройке. У меня она было настроена неверно и всё прекрасно работало, пока я использовал только один компьютер. Когда я подключился к другому компьютеру, порт начал терять символы. Исполнение двух программ на одном компьютере не полностью асинхронно.

2.2   Настройки порта

Устройства /dev/ttyS* предназначены для подключения терминалов к вашей машине и настроены на такую работу после запуска. Это необходимо учитывать при программировании связи с низкоуровневым устройством. Таким образом, порты настроены на ответную передачу пришедших символов обратно на источник. Они должны быть настроены на передачу.

Все параметры могут быть легко настроены в рамках программы. Конфигурация хранится в структуре struct termios, которая определена в <asm/termbits.h>:

#define NCCS 19

        struct termios {

                tcflag_t c_iflag;               /* input mode flags */

                tcflag_t c_oflag;               /* output mode flags */

                tcflag_t c_cflag;               /* control mode flags */

                tcflag_t c_lflag;               /* local mode flags */

                cc_t c_line;                    /* line discipline */

                cc_t c_cc[NCCS];                /* control characters */

        };

Этот файл также включает все флаги определений. В режиме ввода флаги c_iflags принимают все входные символы, а это значит, что все символы могут быть предварительно обработаны до непосредственной обработки.

Точно также флаги c_oflag обрабатывают все выходные данные. c_cflag содержит настройки порта, такие как baudrate, битов на символ, стоп биты и т.д. В локальном режиме флаги c_lflag определяют, будут ли символы повторятся на устройстве вывода, передаваться в вашу программу и т.д. Наконец, массив c_cc определяет контроль символов конца файла, остановки и т.д. Значения по умолчанию для контроля определены в <asm/termios.h>. Флаги описаны в руководстве termios (3). Структура termios содержит поле c_line (линия дисциплины), которое используется в POSIX-совместимых системах.

2.3   Концепции ввода последовательных устройств

Всего существуют три концепции. Соответствующая концепция должна быть выбрана, исходя из значения. Если это возможно, не делайте циклов на получение всей строки.

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

2.3.1    Каноническая обработка ввода

Это обычный режим ввода терминалов, хотя также могут быть полезны для общения с другими DL устройствами ввода. Режим, когда обрабатывается сразу вся строка сразу. Строка по умолчанию заканчивается символом NL (ASCII LF), а также символами конца файла или конца строки. Символ CR (символ конца строки для DOS/Windows) по умолчанию не является завершающим символом.

Каноническая обработка ввода также может обрабатывать очистку, удаление слова, символы, замену, перевод CR в NL и т.д.

2.3.2    Неканоническая обработка ввода

Неканоническая обработка позволяет принимать фиксированное количество символов, что позволяет использовать таймер. Этот режим следует использовать, если ваше приложение всегда читает фиксированное количество символов или подключённое устройство всегда посылает фиксированные очереди символов.

2.3.3    Асинхронный ввод

Оба описанных способа могут быть использованы как синхронном, так и в асинхронном режимах. По умолчанию используется синхронный режим, в котором чтение будет блокировать работу потока, пока символ не придет. В асинхронном режиме заявка на чтение завершится сразу же, а затем соответствующему обработчику придёт сигнал о завершении чтения.

2.3.4    Ожидание ввода из нескольких источников

Это не отдельный режим ввода, но он будет полезен, если вы работаете с несколькими устройствами. В моём приложении я обработал ввод с TCP/IP сокета и с последовательного порта с другого компьютера почти одновременно. Пример программы, приведенной ниже, показывает ожидание ввода от двух разных источников. Как только поступят данные от любого из них, они будут обработаны, затем программа продолжит ожидание.

Подход, показанный ниже, может показаться довольно сложным, но не стоит забывать, что Linux является многопрофильной средой обработки данных. Выбор системного вызова не будет загружать процессор, тогда как цикл по устройствам ввода будет замедлять других исполнителей.

3.     Примеры программ

Все примеры были заимствованы из miniterm.c. Тип впереди буфера был ограничен до 255 символов, как максимальная длина строки для канонической обработки ввода (<linux/limits.h> или <posix1_lim.h>).

См. комментарии в коде для объяснения использования различных режимов ввода. Я надеюсь, что этот код понятен. Пример канонического ввода прокомментирован наиболее полно, остальные примеры комментируются только там, где они отличаются от первого, чтобы подчеркнуть различия.

Описание не полное, так что рекомендуется экспериментировать с примером для получения наилучших результатов для вашего приложения.

Не забудьте дать соответствующие права доступа последовательным портам доступа (например: Chmod А + RW / dev/ttyS1).

3.1   Каноническая обработка ввода

#include <sys/types.h>

        #include <sys/stat.h>

        #include <fcntl.h>

        #include <termios.h>

        #include <stdio.h>

        /* baudrate settings are defined in <asm/termbits.h>, which is

        included by <termios.h> */

        #define BAUDRATE B38400           

        /* change this definition for the correct port */

        #define MODEMDEVICE "/dev/ttyS1"

        #define _POSIX_SOURCE 1 /* POSIX compliant source */

        #define FALSE 0

        #define TRUE 1

        volatile int STOP=FALSE;

        main()

        {

          int fd,c, res;

          struct termios oldtio,newtio;

          char buf[255];

        /*

          Open modem device for reading and writing and not as controlling tty

          because we don't want to get killed if linenoise sends CTRL−C.

        */

         fd = open(MODEMDEVICE, O_RDWR | O_NOCTTY );

         if (fd <0) {perror(MODEMDEVICE); exit(−1); }

         tcgetattr(fd,&oldtio); /* save current serial port settings */

         bzero(&newtio, sizeof(newtio)); /* clear struct for new port settings */

        /*

          BAUDRATE: Set bps rate. You could also use cfsetispeed and cfsetospeed.

          CRTSCTS : output hardware flow control (only used if the cable has

                    all necessary lines. See sect. 7 of Serial−HOWTO)

          CS8     : 8n1 (8bit,no parity,1 stopbit)

   CLOCAL  : local connection, no modem contol

          CREAD   : enable receiving characters

        */

         newtio.c_cflag = BAUDRATE | CRTSCTS | CS8 | CLOCAL | CREAD;

        /*

          IGNPAR  : ignore bytes with parity errors

          ICRNL   : map CR to NL (otherwise a CR input on the other computer

                    will not terminate input)

          otherwise make device raw (no other input processing)

        */

         newtio.c_iflag = IGNPAR | ICRNL;

        /*

         Raw output.

        */

         newtio.c_oflag = 0;

        /*

          ICANON  : enable canonical input

          disable all echo functionality, and don't send signals to calling program

        */

         newtio.c_lflag = ICANON;

        /*

          initialize all control characters

          default values can be found in /usr/include/termios.h, and are given

          in the comments, but we don't need them here

        */

         newtio.c_cc[VINTR]    = 0;     /* Ctrl−c */

         newtio.c_cc[VQUIT]    = 0;     /* Ctrl−\ */

         newtio.c_cc[VERASE]   = 0;     /* del */

         newtio.c_cc[VKILL]    = 0;     /* @ */

         newtio.c_cc[VEOF]     = 4;     /* Ctrl−d */

         newtio.c_cc[VTIME]    = 0;     /* inter−character timer unused */

         newtio.c_cc[VMIN]     = 1;     /* blocking read until 1 character arrives */

         newtio.c_cc[VSWTC]    = 0;     /* '\0' */

         newtio.c_cc[VSTART]   = 0;     /* Ctrl−q */

         newtio.c_cc[VSTOP]    = 0;     /* Ctrl−s */

         newtio.c_cc[VSUSP]    = 0;     /* Ctrl−z */

         newtio.c_cc[VEOL]     = 0;     /* '\0' */

         newtio.c_cc[VREPRINT] = 0;     /* Ctrl−r */

         newtio.c_cc[VDISCARD] = 0;     /* Ctrl−u */

         newtio.c_cc[VWERASE]  = 0;     /* Ctrl−w */

         newtio.c_cc[VLNEXT]   = 0;     /* Ctrl−v */

         newtio.c_cc[VEOL2]    = 0;     /* '\0' */

        /*

          now clean the modem line and activate the settings for the port

        */

         tcflush(fd, TCIFLUSH);

         tcsetattr(fd,TCSANOW,&newtio);

        /*

          terminal settings done, now handle input

          In this example, inputting a 'z' at the beginning of a line will

          exit the program.

        */

         while (STOP==FALSE) {     /* loop until we have a terminating condition */

         /* read blocks program execution until a line terminating character is

            input, even if more than 255 chars are input. If the number

            of characters read is smaller than the number of chars available,

  subsequent reads will return the remaining chars. res will be set

            to the actual number of characters actually read */

            res = read(fd,buf,255);

            buf[res]=0;             /* set end of string, so we can printf */

            printf(":%s:%d\n", buf, res);

            if (buf[0]=='z') STOP=TRUE;

         }

         /* restore the old port settings */

         tcsetattr(fd,TCSANOW,&oldtio);

        }

3.2   Неканоническая обработка ввода

В режиме неканонической обработки ввод не привязан к входной строчной обработке. Очистка, удаление и т.д. не происходи. Задаются два параметра: c_cc[VTIME], который задаёт интервал таймера и c_cc[VMIN], задающий минимальное число символов к передаче.

Если MIN = 0 и TIME > 0, TIME выступает в качестве таймаута. Чтение закончится либо при чтении любого символа, либо по истечении таймера (t = TIME *0.1 s). Если время было превышено, символ возвращён не будет.

Если MIN > 0 и TIME > 0, TIME выступает в качестве таймаута между символами. Чтение закончится либо при достижении нужного количества символов, либо при превышении времени между символами. Таймер становится активным после ввода первого символа и сбрасывается после ввода каждого символа.

Если MIN = 0 и TIME = 0, чтение закончится немедленно. При этом будет возвращено либо востребованное количество символов, либо количество символов в наличии. По словам Антонио (см. вклад), можно запустить fcntl(fd, F_SETFL, FNDELAY); перед чтением для достижения того же результата.

Изменяя newtio.c_cc[VTIME] и newtio.c_cc[VMIN] могут опробованы все описанные режимы.

#include <sys/types.h>

      #include <sys/stat.h>

      #include <fcntl.h>

      #include <termios.h>

      #include <stdio.h>

      #define BAUDRATE B38400

      #define MODEMDEVICE "/dev/ttyS1"

      #define _POSIX_SOURCE 1 /* POSIX compliant source */

      #define FALSE 0

      #define TRUE 1

      volatile int STOP=FALSE;

      main()

      {

        int fd,c, res;

struct termios oldtio,newtio;

        char buf[255];

        fd = open(MODEMDEVICE, O_RDWR | O_NOCTTY );

        if (fd <0) {perror(MODEMDEVICE); exit(−1); }

        tcgetattr(fd,&oldtio); /* save current port settings */

        bzero(&newtio, sizeof(newtio));

        newtio.c_cflag = BAUDRATE | CRTSCTS | CS8 | CLOCAL | CREAD;

        newtio.c_iflag = IGNPAR;

        newtio.c_oflag = 0;

        /* set input mode (non−canonical, no echo,...) */

        newtio.c_lflag = 0;

        newtio.c_cc[VTIME]    = 0;   /* inter−character timer unused */

        newtio.c_cc[VMIN]     = 5;   /* blocking read until 5 chars received */

        tcflush(fd, TCIFLUSH);

        tcsetattr(fd,TCSANOW,&newtio);

        while (STOP==FALSE) {       /* loop for input */

          res = read(fd,buf,255);   /* returns after 5 chars have been input */

          buf[res]=0;               /* so we can printf... */

          printf(":%s:%d\n", buf, res);

          if (buf[0]=='z') STOP=TRUE;

        }

        tcsetattr(fd,TCSANOW,&oldtio);

      }

3.3   Асинхронный ввод

#include <termios.h>

      #include <stdio.h>

      #include <unistd.h>

      #include <fcntl.h>

      #include <sys/signal.h>

      #include <sys/types.h>

      #define BAUDRATE B38400

      #define MODEMDEVICE "/dev/ttyS1"

      #define _POSIX_SOURCE 1 /* POSIX compliant source */

      #define FALSE 0

      #define TRUE 1

      volatile int STOP=FALSE;

      void signal_handler_IO (int status);   /* definition of signal handler */

      int wait_flag=TRUE;                    /* TRUE while no signal received */

      main()

      {

        int fd,c, res;

        struct termios oldtio,newtio;

        struct sigaction saio;           /* definition of signal action */

        char buf[255];

/* open the device to be non−blocking (read will return immediatly) */

        fd = open(MODEMDEVICE, O_RDWR | O_NOCTTY | O_NONBLOCK);

        if (fd <0) {perror(MODEMDEVICE); exit(−1); }

        /* install the signal handler before making the device asynchronous */

        saio.sa_handler = signal_handler_IO;

        saio.sa_mask = 0;

        saio.sa_flags = 0;

        saio.sa_restorer = NULL;

        sigaction(SIGIO,&saio,NULL);

        /* allow the process to receive SIGIO */

        fcntl(fd, F_SETOWN, getpid());

        /* Make the file descriptor asynchronous (the manual page says only

           O_APPEND and O_NONBLOCK, will work with F_SETFL...) */

        fcntl(fd, F_SETFL, FASYNC);

        tcgetattr(fd,&oldtio); /* save current port settings */

        /* set new port settings for canonical input processing */

        newtio.c_cflag = BAUDRATE | CRTSCTS | CS8 | CLOCAL | CREAD;

        newtio.c_iflag = IGNPAR | ICRNL;

        newtio.c_oflag = 0;

        newtio.c_lflag = ICANON;

        newtio.c_cc[VMIN]=1;

        newtio.c_cc[VTIME]=0;

        tcflush(fd, TCIFLUSH);

        tcsetattr(fd,TCSANOW,&newtio);

        /* loop while waiting for input. normally we would do something

           useful here */

        while (STOP==FALSE) {

          printf(".\n");usleep(100000);

          /* after receiving SIGIO, wait_flag = FALSE, input is available

             and can be read */

          if (wait_flag==FALSE) {

            res = read(fd,buf,255);

            buf[res]=0;

            printf(":%s:%d\n", buf, res);

            if (res==1) STOP=TRUE; /* stop loop if only a CR was input */

            wait_flag = TRUE;      /* wait for new input */

          }

        }

        /* restore old port settings */

        tcsetattr(fd,TCSANOW,&oldtio);

      }

      /***************************************************************************

      * signal handler. sets wait_flag to FALSE, to indicate above loop that     *

      * characters have been received.                                           *

      ***************************************************************************/

      void signal_handler_IO (int status)

      {

        printf("received SIGIO signal.\n");

        wait_flag = FALSE;

      }

3.4   Ожидание ввода от нескольких источников

Этот раздел находится на минимальном уровне разработки. Он предназначен для предоставления намёка, соответственно и пример короткий. Он будет работать не только с портами, но и с любым набором файловых дескрипторов.

Вызов select и сопровождающие макросы используют fd_set. Это битовый массив, каждый бит которого определяет правильность соответствующего дескриптора. Select будет принимать на вход fd_set с битами, установленными для соответствующих дескрипторов и возвращать fd_set с битами, указывающими на событие ввода, вывода или исключение. Вся возможная работа с fd_set предусмотрена макросами. Более полная информация – справочник select (2).

#include <sys/time.h>

      #include <sys/types.h>

      #include <unistd.h>

      main()

      {

        int    fd1, fd2;  /* input sources 1 and 2 */

        fd_set readfs;    /* file descriptor set */

        int    maxfd;     /* maximum file desciptor used */

        int    loop=1;    /* loop while TRUE */

        /* open_input_source opens a device, sets the port correctly, and

           returns a file descriptor */

        fd1 = open_input_source("/dev/ttyS1");   /* COM2 */

        if (fd1<0) exit(0);

        fd2 = open_input_source("/dev/ttyS2");   /* COM3 */

        if (fd2<0) exit(0);

        maxfd = MAX (fd1, fd2)+1;  /* maximum bit entry (fd) to test */

        /* loop for input */

        while (loop) {

          FD_SET(fd1, &readfs);  /* set testing for source 1 */

          FD_SET(fd2, &readfs);  /* set testing for source 2 */

          /* block until input becomes available */

          select(maxfd, &readfs, NULL, NULL, NULL);

          if (FD_ISSET(fd1))         /* input from source 1 available */

            handle_input_from_source1();

          if (FD_ISSET(fd2))         /* input from source 2 available */

            handle_input_from_source2();

        }

      }

В данном примере ожидание на событие будет бесконечным. Если вам нужен таймаут, просто замените вызов select:

int res;

        struct timeval Timeout;

        /* set timeout value within input loop */

        Timeout.tv_usec = 0;  /* milliseconds */

        Timeout.tv_sec  = 1;  /* seconds */

        res = select(maxfd, &readfs, NULL, NULL, &Timeout);

        if (res==0)

        /* number of file descriptors with input = 0, timeout occurred. */

В этом примере таймаут 1 секунда. Если он превышен, select вернёт 0, но будьте осторожны, таймаут уменьшается на время, которое реально ждал select. Если установить таймаут в 0, select вернёт 0 немедленно.

4.     Другие источники информации

·        Самоучитель по последовательным портам в Linux описывает, как установить последовательные порты, а также содержит информацию об аппаратуре.

·        Программирование последовательных портов для POSIX-совместимых систем, Michael Sweet (Майкл Свит).

·        Руководство по termios (3) описывает все флаги на termios структуры.