Linux serial port programming

Linux.yaroslavl.ru

Peter Baumann
Русский перевод А.Гавва (2002 г.Львов).


Revision History
Revision 1.01 2001-08-26 Revised by: glf
New maintainer, converted to DocBook
Revision 1.0 1998-01-22 Revised by: phb
Initial document release

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

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

Это — первое обновление начального релиза Linux Serial Programming HOWTO. Первичное назначение этого обновления — изменение авторской информации и конвертирование документа в формат DocBook. Относительно технического содержимого, на данный момент времени, изменений очень мало. Подчистка технического содержимого не может быть произведена за одну ночь, но я буду работать над этим когда мне позволит время.

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

Все примеры тестировались при использовании i386 Linux Kernel 2.0.29.

This document is copyrighted (c) 1997 Peter Baumann, (c) 2001 Gary Frerking and is distributed under the terms of the Linux Documentation Project (LDP) license, stated below.

Unless otherwise stated, Linux HOWTO documents are copyrighted by their respective authors. Linux HOWTO documents may be reproduced and distributed in whole or in part, in any medium physical or electronic, as long as this copyright notice is retained on all copies. Commercial redistribution is allowed and encouraged; however, the author would like to be notified of any such distributions.

All translations, derivative works, or aggregate works incorporating any Linux HOWTO documents must be covered under this copyright notice. That is, you may not produce a derivative work from a HOWTO and impose additional restrictions on its distribution. Exceptions to these rules may be granted under certain conditions; please contact the Linux HOWTO coordinator at the address given below.

In short, we wish to promote dissemination of this information through as many channels as possible. However, we do wish to retain copyright on the HOWTO documents, and would like to be notified of any plans to redistribute the HOWTOs.

If you have any questions, please contact

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

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

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

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

Как указывалось ранее, здесь мало изменений касающихся технического содержимого.

Автор благодарит Mr. Strudthoff, Michael Carter, Peter Waltenberg, Antonino Ianella, Greg Hankins, Dave Pfaltzgraff, Sean Lincolne, Michael Wiedmann, и Adrey Bonar.

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

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

Для создания нуль-модемного кабеля вам необходимо перекрутить линии TxD (передача) и RxD (прием). Описание кабеля см. в секции 7 Serial-HOWTO.

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

Устройства /dev/ttyS* предпологаются для подключения терминалов к вашей системе Linux и конфигурируются для этого во время запуска системы. Об этом необходимо помнить при программировании связи непосредственно через последовательные порты. Например, порты могут быть сконфигурированы во время запуска системы таким образом, что они возвращают «эхо» вводимого символа обратно, что обычно должно быть изменено при передаче данных.

Все параметры могут быть легко сконфигурированы из программы. Конфигурация сохраняется в структуре struct termios , которая объявлена в :

Этот файл также содержит все объявления флагов. Флаги режима ввода в c_iflag управляют всей обработкой ввода, которая подразумевает что символы посылаемые от устройства могут быть обработаны перед тем как они будут прочитаны вызовом read . Аналогично c_oflag управляет обработкой вывода. c_cflag содержит следующие установки для порта: скорость передачи данных, число битов в передаваемом символе, число стоп-битов, и т.д. Флаги локального режима храняться в c_lflag и определяют будет-ли производиться «эхо» для вводимых символов, какие сигналы будут посылаться программе, и т.д. В заключение, массив c_cc описывает управляющие символы для конца файла, остановки передачи, и т.д. Значения по умолчанию для управляющих символов описаны в . Флаги описываются в странице руководства (man page) termios(3) . Структура termios содержит c_line (дисциплина линии) элемент, который не используется в POSIX-совместимых системах.

Здесь будут представлены три различных концепции ввода. Для определенного приложения должна быть выбрана подходящая концепция. Если возможно, то не зацикливайте чтение одиночного символа для получения целой строки. Когда я так делал, я терял символы, в то время как read для целой строки не показывал ни одной ошибки.

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

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

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

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

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

Читайте также:  Hp driver linux rpm

Подход показанный ниже выглядит сложно, но необходимо не забывать о том, что Linux — это многозадачная операционная система. Системный вызов select не будет загружать процессор (CPU) при ожидании ввода, в то время как цикл ожидания пока ввод станет доступен будет замедлять одновременное выполнение других процессов.

Все примеры программ порождены из miniterm.c . Размер приемного буфера ограничен 255 символами, подобно максимальной длине строки при обработке канонического ввода (
или

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

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

Don’t forget to give the appropriate serial ports the right permissions Не забудьте предоставить правильные привелегии доступа к соответствующему последовательному порту (например: chmod a+rw /dev/ttyS1 )!

В режиме обработки неканонического ввода, не производится построчная сборка ввода и обработка ввода (очистка, удаление слов, и т.д.) не производится. Два параметра управляют поведением этого режима: c_cc[VTIME] устанавливает символьный таймер, и c_cc[VMIN] устанавливает минимальное количество символов которые необходимо принять для удовлетворения операции чтения.

Если MIN > 0 и TIME = 0, то MIN устанавливает количество символов которые необходимо принять для удовлетворения операции чтения. Так как TIME установлено в нуль, то таймер не используется.

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

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

Если MIN = 0 и TIME = 0, то чтение будет удовлетворено немедленно. При этом операция чтения вернет число фактически доступных символов или число запрошенных символов. Согласно Antonino (см. содействие), вы можете выдать fcntl(fd, F_SETFL, FNDELAY); перед чтением для получения таких же результатов.

При модификации newtio.c_cc[VTIME] и newtio.c_cc[VMIN] могут быть проверены все режимы описанные выше.

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

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

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

Этот пример будет «вываливаться» в таймаут через 1 секунду. Если произошел таймаут, то вызов select будет возвращать 0, однако позаботьтесь чтобы Timeout определялся временем актуального ожидания ввода в select . Если значение таймаута — нуль, то вызов select будет возвращать управление немедленно.

Linux Serial-HOWTO описывает как установить и сконфигурировать последовательные порты, а также содержит дополнительную информацию об оборудовании.

Справочная страница руководства (man page) termios(3) описывает все флаги для структуры termios .

Источник

Работа с СОМ-портом на Си в linux

Каждый, кто постоянно занимается электроникой и программирует встраиваемые устройства, неизбежно сталкивается с необходимостью работы с СОМ-портом под линуксом. Чаще всего появляется задача сопрячь какой-то датчик, либо удалённое устройство с одноплатником. Да, что греха таить, сейчас в 2020 году UART, наверное, самый распространённый интерфейс для обмена данными, несмотря на всю его архаичность.
Поэтому умение работать с СОМ-портами в linux особенно важно. И вот, казалось бы, UART, древнейший интерфейс, всё должно быть известно и понятно, и даже опытные программисты ломают зубы, работая с ним в линуксе. Особенный цирк с конями начинается при работе с передачей сырых данных по RS-485. Не знаю ни одного программиста, который бы не хватил горя при разработке ПО для таких решений. Самое забавное, что с более новомодным i2c работать в линуксе куда проще и понятнее, чем с ортодоксальным UART. Чтобы не было путаницы дальше, всё семейство UART (RS-232, RS-485, UART 5V, UART 3,3 и т.д.) по тексту я буду называть COM-порт или UART. Мы говорим в статье не о физическом интерфейсе, а о программной стороне вопроса.
В этой статье я хочу показать, как писать свои программы, работающие с UART в ОС Linux. И неважно на каком языке вы пишете программу для работы с UART (python, c, c++, bash, php, perl и т.д.), принцип работы и грабли будут одни и те же, так как всё равно всё упирается в системные вызовы к ядру. А не понимание того, что там происходит и приводит к различным трудноуловимым багам.


Телетайп, на который БЭВМ могли выводить и получать данные. Фото взято здесь

Исторически сложилось, что СОМ-порт в UNIX использовался как терминал. То есть, как устройство для отображения и получения информации работы с ЭВМ. Отсюда идёт всё базовое наследие работы UART.

Основы ввода-вывода в linux: всё есть файл

Принцип операционных систем типа Unix (и GNU Linux вместе с ними): всё есть файл. Файл может быть регулярным на диске, к которому мы все привыкли, файлом может быть именованный или не именованный, канал для передачи данных, передача данных по сети, тоже по сути, работа с файл-сокетом (только не именованным). Таким образом разобравшись с функциями работы с файлами мы частично разберёмся с работой СОМ-порта.
Файл можно: создать, открыть, закрыть, удалить, прочитать и записать в файл. Всё это системные вызовы в ядро, для работы с данными, которые хранятся или передаются с помощью файла. Поскольку создание и удаление файла нас не интересует, далее их не рассматриваю. Этим системным вызовам соответствуют следующие имена функций. Привожу, в пример, вместе с заголовочными файлами, где они описаны:

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

Закрывает файл, на вход принимает дескриптор файла. Возвращает нуль в случае успеха.
Самые интересные системные вызовы для нас, это чтение и запись.

read() пытается записать count байтов файлового описателя fd в буфер, адрес которого начинается с buf.
Если количество count равно нулю, то read() возвращает это нулевое значение и завершает свою работу. Если count больше, чем SSIZE_MAX, то результат не может быть определён.
При успешном завершении вызова возвращается количество байтов, которые были считаны (нулевое значение означает конец файла), а позиция файла увеличивается на это значение. Если количество прочитанных байтов меньше, чем количество запрошенных, то это не считается ошибкой: например, данные могли быть почти в конце файла, в канале, на терминале, или read() был прерван сигналом. В случае ошибки возвращаемое значение равно -1, а переменной errno присваивается номер ошибки. В этом случае позиция файла не определена.

Обратите внимание! Что количество запрашиваемых байт на чтение, может не соответствовать реальному количеству считаных байт! Оно будет меньше, либо равно запрошенному.

write записывает до count байтов из буфера buf в файл, на который ссылается файловый дескриптор fd.
В случае успешного завершения возвращается количество байтов, которые были записаны (ноль означает, что не было записано ни одного байта). В случае ошибки возвращается -1, а переменной errno присваивается соответствующее значение.

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

Читайте также:  Брандмауэр windows удаленное подключение

С точки зрения ядра, функции read и write осуществляют копирование данных из ядра в пространство пользователя (для чтения), и копирование из пространства пользователя в ядро (в случае записи). Таким образом, важно понимать, что функция записи не гарантирует реальную запись в устройство, а только копирование во внутренние буфера ядра. И в случае чтение из файл-устройства порта, вы читаете не физически из устройства, а просто копируете накопленные данные, которое драйвер ядра уже получил из порта ввода-вывода и сложил во внутренний буфер.

Файл-устройство COM-порта

Как уже было сказано, в ОС Linux всё есть файл. Файл-устройство COM-портов обычно располагается в каталоге с именем /dev/tty*, где вместо звёздочки может стоять любая последовательность символов или её не быть вовсе. Эти устройства обладают сходным интерфейсом, который был выведен десятилетия назад для последовательных терминалов TeleType (см. фотографию в начале поста) и получил название tty. Реализация конечного имени устройства зависит от текущей реализации операционной системы. Например, классический COM1 в Ubuntu выглядеть, как /dev/ttyS0. А если подключить USB-COM переходник, то файл-устройство будет иметь имя /dev/ttyUSB0, конечно если он единственный в системе, либо другой порядковый номер на конце, если воткнуто несколько шнурков. Часто, составные устройства (например, lte-модемы), определяются как /dev/ttyACM0.
Если вы откроете виртуальный терминал и просто введёте:

то увидите множество файл-устройств. К одному из них подключен ваш виртуальный терминал это файл-устройство “/dev/tty”. Проще говоря, стандартный поток ввода, вывода и стандартный поток ошибок идёт в это файл-устройство. В данном случае командная оболочка (в моём случае bash) использует его для ввода-вывода. Можем в этом убедиться введя:

Здесь мы командой echo вывели сообщение и перенаправили его в файл /dev/tty, в результате получили его на экране нашего терминала. Если вспомнить, как было всё организовано раньше, то терминал «подключен» к СОМ-порту и представляет собой пассивную железку, что к нему приходит, то он и выводит. Точно также отправляет сырые данные, введённые с клавиатуры. Таким образом, когда мы записали данные с помощью команды “echo” в файл-устройство (системный вызов write), то эти данные были отправлены на виртуальный порт и мы их увидели на экране нашего терминала.
Теперь важный момент, вы, наверняка знаете, что если работает некоторая программа и нажать ctrl-c, то программа получает SIGINT. Как же это работает? Если вы заметите, то набирая ctrl-c, вы посылаете символ «^c»: ETX (Конец текста, посылает сигнал уничтожения), ASCII 0x03. Драйвер СОМ-порта видя получение этого символа, посылает сигнал программе, которая сейчас владеет вводом-выводом. Таким образом, драйвер терминала может управляться специальными символами. Получение этих специальных символов, может очищать экран, менять цвет выводимых символов и т.д. Например, следующие символы могут выполнять следующие действия:

  • (ctrl-c) ^C → ETX (Конец текста, посылает сигнал уничтожения), ASCII 0x03
  • (ctrl-d) ^D → EOT (Конец передачи, завершает ввод), ASCII 0x04
  • ^H → BS (Backspace, \b ), ASCII 0x08
  • ^J → LF (подача линии, \n ), ASCII 0x0A
  • ^L → FF (канал формы, новая страница, очистка терминала), ASCII 0x0C
  • ^M → CR (возврат каретки, \r ), ASCII 0x0D

Как несложно догадаться, если в сыром режиме передачи данных эти символы будут переданы, то драйвер последовательного порта пошлёт нашей программе такие сигналы, и не известно, к чему это может привести.
Поэтому необходимо конфигурировать СОМ-порт для работы в сыром режиме, и не обрабатывать эти символы. Конфигурацию текущего терминала в консоли можно посмотреть командой:

И на выходе мы получим следующий результат:

Обратите внимание, несмотря на то что данное файл-устройство виртуальное и не имеет реального воплощения в железе, у него всё равно есть скорость и управляющие флаги. Это флаги структуры termios. Как работать с этими флагами, что они значат и для чего нужны, мы разберёмся в следующей главе.
Рекомендую запомнить эту команду. Если какая-то программа меняет настройки порта, то они сохраняются и наследуются дальше. Например, если программа переводит терминал в «неканонический режим», то если она не вернёт настройки терминала, то он так и останется в этом режиме, и остальные программы будут уже работать с такими настройками.

Работа с файл-устройством терминала

Все манипуляции tty осуществляются с помощью одной структуры, struct termios, а также нескольких функций, определенных в заголовочном файле . Из этих функций широко применяются только шесть. Когда не нужно устанавливать скорость передачи данных по линии, используются только две наиболее важных функции — tcgetattr() и tcsetattr().

Почти в каждом случае программы должны использовать tcgetattr() для получения текущих установок устройства, модифицировать эти установки, а затем применять tcsetattr() для активизации модифицированных установок.

Для того, чтобы понять, как конфигурировать файл-устройства терминала (телетайпа, СОМ-порта, как будет угодно), разберём синтетический пример.

Давайте разберём, что же делает данная программа: она переводит терминал ввода-вывода в неканонический режим, отключая эхо показа клавиш на экране, блокируя все специальные символы, вызывающие сигнал, и ожидает в цикле ввода всего одного символа «q» для выхода.
Работа программы выглядит так:

Завершить работу программы комбинациями клавиш ctrl-c, ctrl-d и другими невозможно, так как драйвер терминала не обрабатывает соответствующие сигналы. На экране введённая информация не отображается. После завершения работы терминал восстанавливает своё первоначальное состояние.
Разберём код программы.

Здесь мы создаём два экземпляра структуры termios. Для того чтобы сохранить текущее состояние нашего терминала и потом иметь возможность его восстановить и новое.

С помощью функции fileno(stdin) мы получаем дескриптор файла стандартного потока вывода, а функция tcgetattr получает текущие установки устройства в структуру oldsettings.

Здесь мы снимаем флаги локального эха, снимаем флаг канонического режима терминала и отключаем получение сигналов. Подробнее о флагах структуры termios можно посмотреть тут.

Здесь указывается минимальное количество символов, которое будет передано за раз — для неканонического ввода (то количество символов, которое будет отдано функции read до истечения таймаута VTIME). А также таймаут, в децисекундах, после которого будет отдано накопившееся количество символов функции read. Если символов нет, то read вернёт нуль (см. главу «Основы ввода-вывода в linux: всё есть файл»).
Это место немного сложное, поясню. Если у нас не будет ввода, то функция read будет считывать нуль символов, каждые 0,1 с. Если символ приходит, то функция read будет срабатывать при каждом 1-м символе. Если функция read блокирующая!

Записываем новые настройки в наш терминал.

Здесь идёт открытие файл-устройства «/dev/tty» c помощью функции open только для чтения. Можно было также разыменовать дескриптор стандартного ввода через fileno, но сделал так для наглядности. После этого, в цикле мы производим блокирующее чтение из этого файл-устройства по одному символу, пока не получим считанный символ, равный ‘q’.

Восстанавливаем настройки терминала.

Создадим настоящее устройство передачи данных

Предыдущий пример, хоть и был синтетическим, тем не менее часто используется, особенно при вводе паролей, либо при работе с псевдографикой в терминалах. Но наша задача, всё же, получить работу с живым реальным устройством. Для этого на Arduino сделаем такое вот синтетический пример.

Мы просто шлём строку «01234567890123456789\n\r» на скорости 9600 каждые две секунды. Умышленно сделал ASCII-символы, чтобы было проще отлаживать.
В терминале вводим

Таким образом мы можем в реальном времени мониторить сообщения ядра. Подключаем Arduino-плату к нашему компьютеру и видим следующие сообщения:

[83343.555600] usb 2-2: new full-speed USB device number 3 using ohci-pci
[83344.099687] usb 2-2: New USB device found, idVendor=1a86, idProduct=7523, bcdDevice= 2.54
[83344.099694] usb 2-2: New USB device strings: Mfr=0, Product=2, SerialNumber=0
[83344.099698] usb 2-2: Product: USB2.0-Serial
[83344.199375] usbcore: registered new interface driver usbserial_generic
[83344.205220] usbserial: USB Serial support registered for generic
[83344.247903] usbcore: registered new interface driver ch341
[83344.248341] usbserial: USB Serial support registered for ch341-uart
[83344.248819] ch341 2-2:1.0: ch341-uart converter detected
[83344.311747] usb 2-2: ch341-uart converter now attached to ttyUSB0

Для нас самая главная информация, что нас СОМ-порт определился, как ttyUSB0 и обитает он соответственно в /dev/ttyUSB0.

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

И перезагрузим систему.

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

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

Простейший пример блокирующего чтения из СОМ-порта

Разберём пример блокирующего чтения из COM-порта был взят отсюда и немного модифицирован. Все исходники будут доступны в отдельном репозитории к этой статье.

Читайте также:  Если потерял установочный диск windows

Блокирующее чтение, как следует из названия, блокирует программу в системном вызове read до тех пор, пока не появятся данные, которые может получить read. Если данных нет, то этот вызов будет ждать вечно и ничего работать не будет. Этот режим работы наиболее прост для понимания работы с СОМ-портом, и с него лучше всего начинать работать. Примеры работы с портом, разобранные выше, тот же cat и hexdump используют именно блокирующее чтение. Однако, на практике в реальных задачах, такое чтение применяется достаточно редко.

Разберём основные части кода SerialPort_read.c:

Обратите внимание на флаги открытия: O_RDWR – открываем для чтения и записи, O_NOCTTY – терминал не может управлять данным процессом (тот терминал, с которого мы запускаем это приложение, может управлять процессом, например послать сигнал при комбинации ctrl-c).

Инициализируем структуру termios и получаем текущие значения структуры.

Задаём скорость на чтение и на запись. Обратите внимание, что скорости на чтение и на запись могут быть разными. Так же скорость задаётся макросами, начинающиеся с символа “B” (они описаны в termios.h). Могут принимать следующие значения:

Нулевая скорость, B0, используется для завершения связи.

Далее идут стандартные настройки порта:

Отключаем бит чётности (если флаг очищен).

Если флаг установлен, то стоп-бит равен двум, если очищен (в этом случае), то равен одному.

Очищаем маску размера данных.

Устанавливаем размер передаваемых данных, равный восьми битам.

Отключаем аппаратное управление потоком данных (RTS/CTS).

Включаем приёмник, игнорируем контрольные линии модема.

Отключаем управление потоком данных при вводе и выводе, отключаем возможность символов запускать ввод.

Переводим терминал в неканонический режим (мы это уже разбирали).

Отключаем режим ввода, определяемый реализацией по умолчанию.

Теперь самое интересное (с этими параметрами мы ещё поиграемся). Хоть мы их разбирали уже, посмотрим какое будет поведение у них в нашем случае.

Считываем за раз только 40 символов.

Считываем каждую секунду. То есть, если значение отличное от нуля, функция read будет разблокирована по этому значению в децисекундах.

Записываем значение структуры termios.

Вы осознаёте теперь всю сложно работы с COM-портом? При этом мы не разобрали даже половины его возможностей, а уже это оказывается дико сложным. В любом случае придётся внимательно разобраться со всеми флагами структуры termios, чтобы понять, как корректно работать с портом.
Чтение у нас происходит в бесконечном цикле.

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

Таким образом, при данных настройках корректно вычитывает наше сообщение.

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

Как сделать неблокирующее чтение?

Неблокирующее чтение отличается от блокирующего чтения тем, что если в буфере ядра linux нет данных, то функция read не будет ожидать их получения и сразу вернёт управление программе. С одной стороны это очень удобно, но с другой, если вы ожидаете посылку, то вам придётся в цикле читать, пока не получите данные. Для этих целей существует такие вещи как — select, poll и epoll. В рамках этой статьи нет возможности разобрать работу с ними, поэтому буду краток.
В своём вебинаре о работе с СОМ-портом (ссылки будут ниже), я разбирал пример чтения карт-ридера магнитных карт. Там у меня уже готовая библиотека работы с СОМ-портом. Её мы и разберём. Пример обитает тут.

Не буду разбирать подробно всю программу, пробегусь по основным моментам. Остальное в ней всё достаточно очевидно, и многое мы уже разобрали. Всё будет в файле uart.c.
Чтобы перевести СОМ-порт в неблокирующий режим, после открытия порта, нам необходимо файловый дескриптов перевести в режим неблокирующего чтения. Это делается с помощью функции.

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

На вход функция принимает файловый дескриптор открытого порта, длину запрашиваемых данных, таймаут в миллисекундах, после которого возвращается управление системе и указатель на буфер, куда будут считаны данные.
Управление осуществляется с помощью структуры:

В неё мы записываем файловый дескриптор, который хотим мониторить.

Указываем, то что мы хотим мониторить событие получение данных:

И соответственно взводим наш «сторожевой таймер».

Из этой функции мы выйдем, либо по таймауту, либо по получению данных.

Дальше мы проверяем событие, которое произошло. И если событие соответствует POLLIN, то производим чтение данных.
И после чего возвращаем количество считанных данных или нуль в случае ошибки или нуля данных. Это не очень корректно, так как всё же лучше обработать все возникающие ошибки, а не отправлять нуль. Но это тестовый пример и можно так сделать.
Аналогичная функция есть и для записи. Для чего это нужно, ведь мы просто копируем данные в ядро? Да очень просто, если у вас режим передачи RS-485 управляется линией DTR/RTS, либо GPIO, то вы должны точно знать, когда данные были отправлены, чтобы правильно выставить эти пины, меняя режим передачи на приём. Выглядит это всё следующим образом.

Здесь poll настроен на передачу. Единственное, на что стоит обратить внимание, это на вызов функции tcdrain(fd); Эта функция будет ожидать, пока все данные вывода, записанные на объект, на который ссылается fd, не будут переданы. Функцию int set_rts(int fd, int on) мы разберём в следующей главе.

Как подрыгать ножкой DTR и RTS, а также пара слов о RS-485

Кроме системных вызовов read и write, есть системный вызов ioctl, который позволяет осуществлять тонкую настройку. Будем честны, вот эта работа со структурой termios, внизу имеет системный вызов ioctl, который записывает все настройки в ядро. Но это от нас скрыто библиотечными функциями. Системный вызов ioctl не стандартизован и очень опасен, но позволяет производить тонкую настройку порта. Приведу пример функции установки или снятия сигнала rts.

Изначально мы получаем состояние флагов порта в переменную &flags.

После чего, либо снимаем флаг состояния TIOCM_RTS, либо устанавливаем его.
И после этого записываем его

Аналогично выглядит функция работы с сигналом DTR.

С RS-485 в линуксе всё обстоит не важно. Этот интерфейс совершенно не стандартизирован, и каждый производитель может делать всё что захочет. Где-то его нужно инициализировать в ядре, и драйвер ядра будет автоматически переключать приёмо-передатчик, где-то это всё реализовано аппаратно. Тут всё как бог на душу положит. Поэтому, с RS-485 приходится разбираться на месте. Но моя практика показывает, что всё что написано в документации – не актуально. Единственный эффективный путь – это лезть в код ядра и смотреть, что же там происходит на самом деле.

Выводы

Очень сложно в рамках маленькой статьи рассказать о таком сложном явлении в linux, как работа с терминалами. Получается так, что надо сначала понять почему COM-порт не порт, а на самом деле терминал, и как его превратить обратно в порт. Этот терминал имеет множество режимов работы и функционала. И, в силу исторических причин, несёт это огромное наследие дальше. Работа с другими интерфейсами в linux (тем же i2c сильно проще). Di_halt просил меня рассказать о примерах работы с прерываниями и прочим, но это всё лишняя информация. Реальная работа с железом, именно с портами идёт в драйверах внутри ядра linux. Здесь мы взаимодействуем с интерфейсом, который предоставляет нам драйвер.
Если хочется сделать красиво, то правильно будет чтение из порта осуществлять в отдельном программном потоке, а основная программа будет работать в другом потоке. Дальше уже передачу данных осуществлять с помощью между поточного взаимодействия. Тут можно и посылать сигналы основному потоку, после получения данных (аналог прерывания по получению), либо любое другое приятное решение на любой вкус и цвет.
Для тех, кто ничего не понял в этой статье, либо просто хочет расширить свой кругозор, рекомендую посмотреть мой вебинар о программировании СОМ-портов под линукс. Там я достаточно подробно и с живыми примерами рассказываю об особенностях работы с ними.

Примеры к данным видео можно найти вот тут.

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

Источник

Оцените статью