Почему блокируется com порт при чтении из под php
Необходимо работать напрямую с com портом (купюроприемник cashcode), ubuntu 16.04 & php7.1 При включении пк, при первом обращении к com порту происходит подвисание ком порта при попытке считать ответ. Временно лечу запуском сторонней программы (которая нормально с ком портами работает), после нее все работает как надо. Смотрел исходники одной из такой программ, ничего сверх естественного там не происходит, никаких изменений в конфигурацию порта или чего подобного не вносится. либа для работы с ком портом Хелпер для купюроприемника Сам контроллер
Отслеживать
задан 30 ноя 2017 в 8:02
13 1 1 бронзовый знак
modemmanager уже снесли?
30 ноя 2017 в 11:06
скорость на порту выставить надо
30 ноя 2017 в 11:45
@alexanderbarakin, modemmanager то тут причем ?)
30 ноя 2017 в 12:29
@eri, да, конечно. Все настройки прописаны и сверены побуквенно с теми, что указаны в исходниках рабочей программы.
30 ноя 2017 в 12:30
modemmanager может блокировать интерфейсные файлы последовательных устройств. логика работы у него такая. причём, что самое плохое — спорадически. если работаете с последовательным устройством самостоятельно, я бы рекомендовал для начала удалить этот пакет.
30 ноя 2017 в 12:42
1 ответ 1
Сортировка: Сброс на вариант по умолчанию
В исходниках видно что для доступа к порту используется функция fopen — это обычное чтение из файла. Она не устанавливает параметры подключения на порту. Также не вижу в коде никакой другой функции для инициализации порта.
Когда стартуете стороннюю программку — она в первую очередь устанавливает скорость и стоп-биты, контроль потока.
Для корректной работы рекомендую настроить порт до запуска php или средствами php до открытия порта.
Как изменить параметры последовательного порта в Linux (c opennet)
Вывод на экран текущих параметров:
stty -F /dev/ttyS0 -a setserial -g /dev/ttyS0
Включение Hardware flow control (RTS/CTS):
stty -F /dev/ttyS0 crtscts
Установка скорости порта:
stty -F /dev/ttyS0 9600
Установка прерывания и типа UART:
setserial /dev/ttyS0 auto_irq skip_test autoconfig setserial /dev/ttyS0 irq 5 uart 16550A skip_test
Очистка занятых COM портов в Windows
02.02.2022
itpro
PowerShell, Windows 10, Windows 7, Windows Server 2019
комментариев 26
Когда вы подключаете к компьютеру новое COM или некоторые USB устройства (например, USB модем, мобильный телефон, Bluetooth адаптер, конвертеров последовательных портов в USB и т.п.), Windows определяет его через механизм Plug-n-Play и назначит ему номер COM порта в диапазоне от 1 до 255 (COM1, COM2, COM3 и т.д.). При повторном подключении этого же устройств, ему назначается номер COM порта (Communication Port, или Serial port), зарезервированный за ним ранее. Новым устройствам выдается первый незанятый номер COM порта. Не редкость, когда внешние устройства при подключении создают сразу несколько COM портов (в моем случае после подключения адаптера Bluetooth, в системе появилось сразу 10 . новых COM портов).
Ряд приложений (как правило, довольно древних), способны адресовать только двухзначные номера COM портов, и отказываются работать с COM100 и выше. Или видят только COM1-COM9 устройства. Что делать, если подключенное устройство получило высокий номер COM порта? Можно ли сбросить нумерацию для зарезервированных COM портов и удалить назначенные порты?
Изменить номер COM порта устройства в Windows
В Windows можно вручную изменить номер COM порта, назначенный устройству. Предположим требуемый COM порт уже занят, и мы хотим его попробовать его освободить. Для этого:
- Откройте диспетчер устройств (Device Manager) командой devmgmt.msc;
- В меню выберите View->Show Hidden Devices;
- Затем разверните Ports (COM & LPT) и найдите в списке устройство;
- Перейдите на вкладку Port Settings и нажмите кнопку Advanced;
- Текущий номер COM порта, назначенный устройству указан в поле COMPortNumber;
- Чтобы изменить его, раскройте выпадающий список и выберите номер COMпорта, который хотите задать.
Но чаще всего изменить номер COM порт на другой не получится, так как все «низкие» COM порт уже используются (in use);
Однако такой способ позволяет освободить занятый COM порт не во всех случаях.
Вы можете вывести полный список занятых COM портов в Windows с помощью PowerShell:
Get-WMIObject Win32_SerialPort | Select-Object Name,DeviceID,Description
Можно определить номер COM порта для определённого устройства по его имени, например:
Get-WMIObject Win32_SerialPort | Where-Object < $_.Name -like "*Arduino*">|select name, deviceid
Или
Get-WMIObject Win32_SerialPort | Where-Object < $_.Name -like "*GPS*">|select name, deviceid
Определяем процесс, который использует COM в Windows
Вы не сможете освободить COM порт устройства, которое используется Windows или запущенной программой, процессом. Сначала вам нужно завершить процесс/программу, которая использует сейчас COM порт. Чтобы определить имя процесса, который использует COM порт понадобится утилита Process Explorer (https://docs.microsoft.com/en-gb/sysinternals/downloads/process-explorer).
Сначала нужно вывести имя службы, которая использует COM порт. Выполните команду PowerShell:
get-pnpdevice -class Ports -ea 0| Select Name, PNPDeviceID, Status, Service
Имя службы данного COM порта указано в столбце Service. Например, для COM2 это Serial. Теперь нужно запустить Process Explorer с правами администратора и выбрать в меню Find -> Find Handle or DLL. В строке Hangde or DLL substring введите значение Service, полученное ранее. В нашем примере это Serial .
Process Explorer должен показать процесс, который использует сейчас ваш COM порт. Чтобы освободить COM порт, завершите процесс или программу.
Сброс назначенных COM портов Windows через реестр
Информация об используемых COM портах хранится в ключе реестра CommDB в разделе HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\COM Name Arbiter
- Откройте редактор реестра (regedit.exe) и перейдите в указанную ветку.
Важно! Настоятельно рекомендуем предварительно создать резервную копию данной ветки реестра (File -> Export). В том случае, если что-то пойдет не так, вы сможете вернуться к первоначальной конфигурации COM портов.
Значение параметра ComDB в двоичном формате определяет список COM портов, зарезервированных в системе. Каждый бит определяет состояние соответствующего порта (от 1 до 255). К примеру, если нам нужно оставить резервацию только для COM3, hex значение ComDB будет равно 04 (0000 0100);
Важно! Будьте предельно внимательными, ни в коем случае не добавьте в этот параметр дополнительные байт, иначе система может начать падать в BSOD.
Если нужно полностью сбросить все привязки COM, измените значение ключа ComDB на 0.
Примечание. В разделе HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\SERIALCOMM можно увидеть полный список COM портов, доступных в Windows.В разделе HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\SERIALCOMM можно увидеться список COM портов, подключенных к системе.
Также вы можете использовать следующие две бесплатные утилиты для очистки занятых (In Use) COM портов:
- COM Name ArbiterTool – утилита для освобождения занятых COM портов. Запустите утилиту с правами администратора, выберите COM порты, которые вы хотите освободить и нажмите Clear unused Reservationsи Remove non-present devices;
- Device Cleanup Tool – утилита используется для поиска в реестре информации о ранее подключенных устройств (в ветке HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum) и удаления неиспользуемых устройств и очистки резерваций COM портов.
Предыдущая статья Следующая статья
9.2. [DKSF 60.5 IU] Как настроить COM порт?
Для того, чтобы настроить COM порт, необходимо перейти на страницу «COM ПОРТ» web-интерфейса устройства:
Использование порта UART
Функция порта UART – режим, в котором работает COM порт (RS-232/R-S485). Доступные варианты: «Отключен», «TCP-COM». По умолчанию: Отключен
Функция «TCP-COM» используется для туннелирования порта RS-232/RS-485 через сеть TCP/IP.
Настройки связи сеть — порт
В этом разделе задаются параметры работы функции «TCP-COM»:
Порт TCP сервера – устройство выступает в роли сервера. Этот параметр задаёт номер TCP порта, который слушает устройство UniPing v3, ожидая подключения клиентского ПО. Необходимо явно сконфигурировать подходящий номер порта. Можно выбрать любое значение в диапазоне 1 – 65535, кроме порта 80, который используется встроенным HTTP-сервером. Рекомендуется использовать значение больше 1024. По умолчанию: 0
Длина принятых последовательных данных для отправки сетевого пакета (1..512 байт) – количество байт, принятых от порта RS-232/RS-485, при достижении которого устройство отправит накопленные данные в форме «полезной нагрузки» TCP/IP пакета по Ethernet интерфейсу. Если данное число байт не достигнуто, пакет будет отправлен по таймауту получения данных от RS-232/RS-485 порта. По умолчанию: 96 байт
Таймаут ожидания новых последовательных данных перед отправкой сетевого пакета (5..999 мс) – интервал времени ожидания данных на последовательном порту. Если в течение этого интервала не пришло новых данных, то устройство отправит в сеть те данные, которые есть в буфере на данный момент, даже если количество данных в буфере не достигло необходимого размера, указанного в поле «Длина принятых последовательных данных для отправки сетевого пакета (1..512 байт)». По умолчанию: 100 мс
Счётчик переполнений буферов – счётчик увеличивается каждый раз, когда у устройства переполняется внутренний буфер данных для передачи в порт RS-232/RS-485. Счётчик обнуляется при перезагрузке устройства.
Поток данных в направлении из сети в последовательный порт регулируется изменением поля WINDOW на стороне устройства. В обратном направлении поток не регулируется, так как сеть Ethernet заведомо быстрее последовательного порта RS-232/RS-485. Сигналы «CTS», «RTS» для управления потоком не используются.
Настройки последовательного порта
Тип интерфейса – определяет какой интерфейс будет использоваться: RS-232 или RS-485 . По умолчанию: RS-232
Разъём RS-232 и разъём RS-485 используют общую внутреннюю шину устройства, поэтому невозможно одновременное использование этих интерфейсов! Кроме того, при использовании RS-485 необходимо физически отключить любые устройства подключённые к разъёму RS-232 и наоборот.
Скорость порта, бит/с – скорость порта RS-232/RS-485. По умолчанию: 9600 бит/с
Длина слова, бит – количество бит в слове, передаваемом/принимаемом по RS-232/RS-485. По умолчанию: 8
Чётность – тип контроля чётности в данных передаваемых по RS-232/RS-485. «No» – контроль чётности отключён, «Odd» – добавлять число единичных битов данных до нечётности, «Even» – добавлять число единичных битов данных до чётности. По умолчанию: No
Число стоп-битов – количество стоп-бит в слове RS-232/RS-485. По умолчанию: 1
После установки всех требуемых настроек нажмите кнопку «Применить изменения».
Работа с СОМ-портом на Си в linux
Каждый, кто постоянно занимается электроникой и программирует встраиваемые устройства, неизбежно сталкивается с необходимостью работы с СОМ-портом под линуксом. Недаром, моя статья «UART и с чем его едят» спустя 11 лет после выпуска набирает просмотры и комментарии.
Для разработчиков чаще всего появляется задача сопрячь какой-то датчик, либо удалённое устройство с одноплатником. Да что греха таить, сейчас в 2021 году UART, наверное, самый распространённый интерфейс для обмена данными, несмотря на всю его архаичность. Далеко за примером ходить не надо, внутри смартфона, лежащего у вас в кармане, будет с десяток UART интерфейсов, самый известный из которых — это подключение SIM-карты.
Поэтому умение работать с СОМ-портами в 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 вместе с ними): всё есть файл. Файл может быть регулярным на диске, к которому мы все привыкли, файлом может быть канал (именованный или не именованный) для передачи данных, передача данных по сети, тоже по сути, работа с файл-сокетом (только не именованным). Таким образом, разобравшись с функциями работы с файлами, мы частично разберёмся с работой СОМ-порта.
Файл можно: создать, открыть, закрыть, удалить, прочитать и записать в файл. Всё это системные вызовы в ядро, для работы с данными, которые хранятся или передаются с помощью файла. Поскольку создание и удаление файла нас не интересует, далее их не рассматриваю. Этим системным вызовам соответствуют следующие имена функций. Привожу пример, вместе с заголовочными файлами, где они описаны:
#include #include #include int open(const char *pathname, int flags); //Либо int open(const char *pathname, int flags, mode_t mode); //открыть файл
Принимает на вход имя файла, флаги опции открытия, и, если расширенная функция, то и права доступа. Но нас это тоже пока не волнует. Возвращает дескриптор (описатель) открытого файла, число больше нуля. В любом другом случае – это ошибка открытия файла.
#include int close(int fd); //закрыть файл
Закрывает файл, на вход принимает дескриптор файла. Возвращает нуль в случае успеха.
Самые интересные системные вызовы для нас — это чтение и запись.
#include ssize_t read(int fd, void *buf, size_t count);
read() пытается записать count байтов файлового описателя fd в буфер, адрес которого начинается с buf.
Если количество count равно нулю, то read( ) возвращает это нулевое значение и завершает свою работу. Если count больше, чем SSIZE_MAX, то результат не может быть определён.
При успешном завершении вызова возвращается количество байтов, которые были считаны (нулевое значение означает конец файла), а позиция файла увеличивается на это значение. Если количество прочитанных байтов меньше, чем количество запрошенных, то это не считается ошибкой: например, данные могли быть почти в конце файла, в канале, на терминале, или read() был прерван сигналом. В случае ошибки возвращаемое значение равно -1, а переменной errno присваивается номер ошибки. В этом случае позиция файла не определена.
❒ Обратите внимание! Количество запрашиваемых байт на чтение может не соответствовать реальному количеству считанных байт. Оно будет меньше, либо равно запрошенному.
#include ssize_t write(int fd, const void *buf, size_t count);
write записывает до count байтов из буфера buf в файл, на который ссылается файловый дескриптор fd.
В случае успешного завершения возвращается количество байтов, которые были записаны (ноль означает, что не было записано ни одного байта). В случае ошибки возвращается -1, а переменной errno присваивается соответствующее значение.
❒ Следует помнить, что запись байт не гарантирует, что все байты будут записаны. Следует также контролировать количество байт, которое было записано, и в цикле дописывать их до окончания.
С точки зрения ядра, функции read и write осуществляют копирование данных из ядра в пространство пользователя (для чтения), и копирование из пространства пользователя в ядро (в случае записи). Таким образом, важно понимать, что функция записи не гарантирует реальную запись в устройство, а только копирование во внутренние буфера ядра. И в случае чтения из файл-устройства порта, вы читаете не физически из устройства, а просто копируете накопленные данные, которое драйвер ядра уже получил из порта ввода-вывода, и сложил во внутренний буфер.
Файл-устройство COM-порта
Как уже было сказано, в ОС Linux всё есть файл. Файл-устройство COM-портов обычно располагается в каталоге с именем /dev/tty* , где вместо звёздочки может стоять любая последовательность символов или её не быть вовсе. Эти устройства обладают сходным интерфейсом, который был выведен десятилетия назад для последовательных терминалов TeleType (см. фотографию в начале поста) и получил название tty . Реализация конечного имени устройства зависит от текущей реализации операционной системы. Например, классический COM1 в Ubuntu выглядеть, как /dev/ttyS0 . А если подключить USB-COM переходник, то файл-устройство будет иметь имя /dev/ttyUSB0 , конечно, если он единственный в системе, либо другой порядковый номер на конце, если воткнуто несколько шнурков. Часто, составные устройства (например, lte-модемы), определяются как /dev/ttyACM0 .
Если вы откроете виртуальный терминал и просто введёте:
ls -la /dev/tty*
то увидите множество файл-устройств. К одному из них подключён ваш виртуальный терминал — это файл-устройство “/dev/tty”. Проще говоря, стандартные потоки ввода-вывода и стандартный поток ошибок идёт в это файл-устройство. В данном случае командная оболочка (в моём случае bash) использует его для ввода-вывода. Можем в этом убедиться введя:
~$ echo "Hello" > /dev/tty Hello
Здесь мы командой 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
Поэтому необходимо конфигурировать СОМ-порт для работы в сыром режиме, и не обрабатывать эти символы. Конфигурацию текущего терминала в консоли можно посмотреть командой:
stty -F /dev/tty -a
И на выходе мы получим следующий результат:
~$ stty -F /dev/tty -a speed 38400 baud; rows 51; columns 238; line = 0; intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = ; eol2 = ; swtch = ; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; discard = ^O; min = 1; time = 0; -parenb -parodd -cmspar cs8 -hupcl -cstopb cread -clocal -crtscts -ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff -iuclc -ixany -imaxbel iutf8 opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0 isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt echoctl echoke -flusho -extproc
Обратите внимание: несмотря на то что данное файл-устройство виртуальное, и не имеет реального воплощения в железе, у него всё равно есть скорость и управляющие флаги. Это флаги структуры termios. Как работать с этими флагами, что они значат и для чего нужны, мы разберёмся в следующей главе.
Рекомендую запомнить эту команду. Если какая-то программа меняет настройки порта, то они сохраняются и наследуются дальше. Например, если программа переводит терминал в «неканонический режим», то если она не вернёт настройки терминала, то он так и останется в этом режиме, и остальные программы будут уже работать с такими настройками.
В моей практике была ситуация, когда я создавал программу, для работы с удалённым устройством через СОМ-порт. И при отладке, работа с данным устройством шла нестабильно: то работала, то нет. Параллельно проверял работу с ним отдельной программой cutecom и с ней всё работает как часы, но до её запуска, у меня ничего не работало. И вдруг я понял, что после того как я поотлаживал её, внезапно у меня всё заработало!
Это означало, что этот софт ставит какие-то дополнительные флаги, которая моя программа не ставила. И на помощь пришла утилита stty. Получаем параметры порта до запуска cutecom, после инициализации моей программой и после запуска. Смотрим разницу:
stty -a -F /dev/ttyUSB0 > first cutecom^C stty -a -F /dev/ttyUSB0 > second diff first second
В результате оказалось, что программа cutecom снимает флаг ISIG и устанавливает флаг IGNBRK . Так что даже на этапе отладки бывают подставы.
▍ Работа с файл-устройством терминала
Все манипуляции tty осуществляются с помощью одной структуры struct termios , а также нескольких функций, определённых в заголовочном файле . Из этих функций широко применяются только шесть. Когда не нужно устанавливать скорость передачи данных по линии, используются только две наиболее важные функции — tcgetattr() и tcsetattr() .
#include struct termios < tcflag_t c_iflag; /* флаги режима ввода */ tcflag_t c_oflag; /* флаги режима вывода */ tcflag_t c_cflag; /* флаги управляющего режима */ tcflag_t c_lflag; /* флаги локального режима */ cc_t c_line; /* дисциплина линии связи */ cc_t c_cc[NCCS]; /* управляющие символы */ >; int tcgetattr(int fd, struct termios * tp); int tcsetattr(int fd, int oact, struct termios * tp);
Почти в каждом случае программы должны использовать tcgetattr() для получения текущих установок устройства, модифицировать эти установки, а затем применять tcsetattr() для активизации модифицированных установок.
Для того чтобы понять как конфигурировать файл-устройства терминала (телетайпа, СОМ-порта, как будет угодно), разберём синтетический пример.
#include #include #include #include #include int main (int argc, char ** argv) < struct termios oldsettings, newsettings; tcgetattr(fileno(stdin), &oldsettings); newsettings = oldsettings; newsettings.c_lflag &= ~(ECHO|ICANON|ISIG); newsettings.c_cc[VMIN] = 1; newsettings.c_cc[VTIME] = 1; tcsetattr(fileno(stdin), TCSANOW, &newsettings); printf("press [q] to quit\n"); char rd_ch = '\0'; int fd_in = open ("/dev/tty", O_RDONLY); while(rd_ch != 'q') < int read_count = read(fd_in, &rd_ch, 1); >tcsetattr(fileno(stdin), TCSANOW, &oldsettings); return EXIT_SUCCESS; >
Давайте разберём, что же делает данная программа: она переводит терминал ввода-вывода в неканонический режим, отключая эхо показа клавиш на экране, блокируя все специальные символы, вызывающие сигнал, и ожидает в цикле ввода всего одного символа «q» для выхода.
Работа программы выглядит так:
./pressq press [q] to quit $
Завершить работу программы комбинациями клавиш ctrl-c, ctrl-d и другими невозможно, так как драйвер терминала не обрабатывает соответствующие сигналы. На экране введённая информация не отображается. После завершения работы терминал восстанавливает своё первоначальное состояние.
Разберём код программы.
struct termios oldsettings, newsettings;
Здесь мы создаём два экземпляра структуры termios. Для того, чтобы сохранить текущее состояние нашего терминала и потом иметь возможность его восстановить и новое.
tcgetattr(fileno(stdin), &oldsettings);
С помощью функции fileno(stdin) мы получаем дескриптор файла стандартного потока вывода, а функция tcgetattr получает текущие установки устройства в структуру oldsettings.
newsettings = oldsettings;
newsettings.c_lflag &= ~(ECHO|ICANON|ISIG);
Здесь мы снимаем флаги локального эха, снимаем флаг канонического режима терминала и отключаем получение сигналов. Подробнее о флагах структуры termios можно посмотреть тут.
newsettings.c_cc[VMIN] = 1; newsettings.c_cc[VTIME] = 1;
Здесь указывается минимальное количество символов, которое будет передано за раз — для неканонического ввода (то количество символов, которое будет отдано функции read до истечения таймаута VTIME ). А также таймаут, в децисекундах, после которого будет отдано накопившееся количество символов функции read . Если символов нет, то read вернёт нуль (см. главу «Основы ввода-вывода в linux: всё есть файл»).
Это место немного сложное, поясню. Если у нас не будет ввода, то функция read будет считывать нуль символов, каждые 0,1 с. Если символ приходит, то функция read будет срабатывать при каждом 1-м символе. Если функция read блокирующая!
tcsetattr(fileno(stdin), TCSANOW, &newsettings);
Записываем новые настройки в наш терминал.
char rd_ch = '\0'; int fd_in = open ("/dev/tty", O_RDONLY); while(rd_ch != 'q')
Здесь идёт открытие файл-устройства «/dev/tty» c помощью функции open только для чтения. Можно было также разыменовать дескриптор стандартного ввода через fileno , но сделал так для наглядности. После этого, в цикле мы производим блокирующее чтение из этого файл-устройства по одному символу, пока не получим считанный символ, равный ‘q’.
tcsetattr(fileno(stdin), TCSANOW, &oldsettings);
Восстанавливаем настройки терминала.
▍ Создадим настоящее устройство передачи данных
Предыдущий пример, хоть и был синтетическим, тем не менее часто используется, особенно при вводе паролей, либо при работе с псевдографикой в терминалах. Но наша задача, всё же, получить работу с живым реальным устройством. Для этого на Arduino сделаем вот такой скетч:
void setup() < Serial.begin(9600); >void loop()
Мы просто шлём строку «01234567890123456789\n\r» на скорости 9600 каждые две секунды. Умышленно сделал ASCII-символы, чтобы было проще отлаживать.
В терминале вводим:
dmesg -w
Таким образом, мы можем в реальном времени мониторить сообщения ядра. Подключаем 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.
$ ls -la /dev/ttyUSB0 crw-rw---- 1 root dialout 188, 0 Oct 21 11:38 /dev/ttyUSB0
Для того чтобы с этим файл-устройством можно было работать не из-под root , надо добавить вашего пользователя в группу dialout . Для этого введём команду
sudo usermod -a -G dialout $USER
И после этого необходимо завершить сеанс, чтобы изменения вступили в силу. Лично я хардкорщик, и просто ребутаю систему.
sudo reboot
После успешной перезагрузки, можно проконтролировать корректность работы этого порта. Зададим скорость порта и попробуем прочитать из него содержимое.
$ stty -F /dev/ttyUSB0 9600 $ cat /dev/ttyUSB0 0123456789 0123456789 0123456789 0123456789
Сообщения появляются раз в две секунды, значит всё корректно работает.
Небольшой полезный хак. Если требуется читать и анализировать сырые данные штатными средствами (без установки дополнительного ПО), то можно использовать программу hexdump :
▍ Простейший пример блокирующего чтения из СОМ-порта
Разберём пример блокирующего чтения из COM-порта был взят отсюда и немного модифицирован. Все исходники будут доступны в отдельном репозитории к этой статье.
Блокирующее чтение, как следует из названия, блокирует программу в системном вызове read до тех пор, пока не появятся данные, которые может получить read. Если данных нет, то этот вызов будет ждать вечно и ничего работать не будет. Этот режим работы наиболее прост для понимания работы с СОМ-портом, и с него лучше всего начинать работать. Примеры работы с портом, разобранные выше, тот же cat и hexdump используют именно блокирующее чтение. Однако, на практике в реальных задачах, такое чтение применяется достаточно редко.
Разберём основные части кода SerialPort_read.c:
fd = open("/dev/ttyUSB0",O_RDWR | O_NOCTTY);
Обратите внимание на флаги открытия: O_RDWR – открываем для чтения и записи, O_NOCTTY – терминал не может управлять данным процессом (тот терминал, с которого мы запускаем это приложение, может управлять процессом, например, послать сигнал при комбинации ctrl-c).
struct termios SerialPortSettings; tcgetattr(fd, &SerialPortSettings);
Инициализируем структуру termios и получаем текущие значения структуры.
cfsetispeed(&SerialPortSettings,B9600); cfsetospeed(&SerialPortSettings,B9600);
Задаём скорость на чтение и на запись. Обратите внимание, что скорости на чтение и на запись могут быть разными. Также скорость задаётся макросами, начинающиеся с символа “B” (они описаны в termios.h). Могут принимать следующие значения:
B0 B50 B75 B110 B134 B150 B200 B300 B600 B1200 B1800 B2400 B4800 B9600 B19200 B38400 B57600 B115200 B230400
Нулевая скорость, B0, используется для завершения связи.
Далее идут стандартные настройки порта:
SerialPortSettings.c_cflag &= ~PARENB;
Отключаем бит чётности (если флаг очищен).
SerialPortSettings.c_cflag &= ~CSTOPB;
Если флаг установлен, то стоп-бит равен двум, если очищен (в этом случае), то равен одному.
SerialPortSettings.c_cflag &= ~CSIZE;
Очищаем маску размера данных.
SerialPortSettings.c_cflag |= CS8;
Устанавливаем размер передаваемых данных, равный восьми битам.
SerialPortSettings.c_cflag &= ~CRTSCTS;
Отключаем аппаратное управление потоком данных (RTS/CTS).
SerialPortSettings.c_cflag |= CREAD | CLOCAL;
Включаем приёмник, игнорируем контрольные линии модема.
SerialPortSettings.c_iflag &= ~(IXON | IXOFF | IXANY);
Отключаем управление потоком данных при вводе и выводе, отключаем возможность символов запускать ввод.
SerialPortSettings.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
❒ Частая ошибка: Разница между c_iflag и c_lflag , пишутся практически одинаково, и без проблем скомпилируются, если вы поставите не те флаги, но работать не будет. В коде очень часто бывают подобные ошибки (в т. ч. и у меня).
Переводим терминал в неканонический режим (мы это уже разбирали).
SerialPortSettings.c_oflag &= ~OPOST;
Отключаем режим ввода, определяемый реализацией по умолчанию.
Теперь самое интересное (с этими параметрами мы ещё поиграемся). Хоть мы их разбирали уже, посмотрим какое будет поведение у них в нашем случае.
SerialPortSettings.c_cc[VMIN] = 40;
Считываем за раз только 40 символов.
SerialPortSettings.c_cc[VTIME] = 10;
Считываем каждую секунду. То есть, если значение отличное от нуля, функция read будет разблокирована по этому значению в децисекундах.
tcsetattr(fd,TCSANOW,&SerialPortSettings)
Записываем значение структуры termios .
Вы осознаёте теперь всю сложность работы с COM-портом? При этом мы не разобрали даже половины его возможностей, а уже это оказывается дико сложным. В любом случае придётся внимательно разобраться со всеми флагами структуры termios , чтобы понять, как корректно работать с портом.
Чтение у нас происходит в бесконечном цикле:
while (1) < tcflush(fd, TCIFLUSH); //очищаем старые данные в буфере RX bytes_read = read(fd,&read_buffer,32); //запрос на чтение 32-х байт printf("\n\n Bytes Rxed -%d", bytes_read); for(i=0;i
Скомпилируем и посмотрим, что же она будет у нас считывать.
$ gcc SerialPort_read.c -o SerialPort_read $ ./SerialPort_read +----------------------------------+ | Serial Port Read | +----------------------------------+ ttyUSB0 Opened Successfully BaudRate = 9600 StopBits = 1 Parity = none Bytes Rxed -11 0123456789 Bytes Rxed -11 0123456789 ….
Таким образом, при данных настройках корректно вычитывает наше сообщение.
Аналогично запись (системный вызов write) может быть блокирующей и не блокирующей, но в данном случае это не очень критично, так как после копирования данных в буфер ядра запись разблокируется.
▍ Программные способы перехвата сообщений в порту
Для отладки передаваемых сообщений через COM-порт удобно использовать перехватчик протокола. В простейшем виде, это могут быть ещё два дополнительных СОМ-порта, которые слушают передачу. Но существуют и программные средства, причём кроссплатформенные. Лично я рекомендую программу IO Ninja Serial Monitor:
Для начала необходимо установить модули ядра, для этого качаем архив tdevmon, собираем его и делаем insmod для собранного драйвера. Подробнее вот тут.
После чего запускаем программу ioninja, несколько кликов мышью, и всё отлично работает!
▍ Как сделать неблокирующее чтение?
Неблокирующее чтение отличается от блокирующего чтения тем, что если в буфере ядра linux нет данных, то функция read не будет ожидать их получения и сразу вернёт управление программе. С одной стороны, это очень удобно, но с другой, если вы ожидаете посылку, то вам придётся в цикле читать, пока не получите данные. Для этих целей существует такие вещи как — select, poll и epoll. В рамках этой статьи нет возможности разобрать работу с ними, поэтому буду краток.
В своём вебинаре о работе с СОМ-портом (ссылки будут ниже), я разбирал пример чтения карт-ридера магнитных карт. Там у меня уже готовая библиотека работы с СОМ-портом. Её мы и разберём. Пример обитает тут.
Не буду разбирать подробно всю программу, пробегусь по основным моментам. Остальное в ней всё достаточно очевидно, и многое мы уже разобрали. Всё будет в файле uart.c.
Чтобы перевести СОМ-порт в неблокирующий режим, после открытия порта, нам необходимо файловый дескриптор перевести в режим неблокирующего чтения. Это делается с помощью функции.
fcntl(fd, F_SETFL, FNDELAY);
Таким образом, теперь, мы будем выходить сразу, после функции read, вне зависимости есть у нас данные или нет.
Сейчас стоит задача ожидать данные в буфере ядра, и если их нет то выходить по какому-то таймауту, возвращая управления программе. Для этого напишем следующую функцию.
int read_com(int fd, int len , int timeout, uint8_t * buff) < int ret = 0; struct pollfd fds; fds.fd=fd; fds.events = POLLIN; poll(&fds, 1, timeout); if(fds.revents & POLLIN) < ret = read(fd, buff, len); >if(ret <0)< ret = 0; >return ret; >
На вход функция принимает файловый дескриптор открытого порта, длину запрашиваемых данных, таймаут в миллисекундах, после которого возвращается управление системе и указатель на буфер, куда будут считаны данные.
Управление осуществляется с помощью структуры:
struct pollfd fds;
В неё мы записываем файловый дескриптор, который хотим мониторить.
fds.fd=fd;
Указываем, то что мы хотим мониторить событие получение данных:
fds.events = POLLIN;
И, соответственно, взводим наш «сторожевой таймер».
poll(&fds, 1, timeout);
Из этой функции мы выйдем либо по таймауту, либо по получению данных.
Дальше мы проверяем событие, которое произошло. И если событие соответствует POLLIN, то производим чтение данных.
И после чего возвращаем количество считанных данных или нуль в случае ошибки или нуля данных. Это не очень корректно, так как всё же лучше обработать все возникающие ошибки, а не отправлять нуль. Но это тестовый пример и можно так сделать.
Аналогичная функция есть и для записи. Для чего это нужно, ведь мы просто копируем данные в ядро? Да очень просто, если у вас режим передачи RS-485 управляется линией DTR/RTS, либо GPIO, то вы должны точно знать, когда данные были отправлены, чтобы правильно выставить эти пины, меняя режим передачи на приём. Выглядит это всё следующим образом.
int write_com(int fd, uint8_t * buf, size_t size, int timeout) < int ret = 0; struct pollfd fds; fds.fd=fd; fds.events = POLLOUT; poll(&fds, 1, timeout); if(fds.revents & POLLOUT)< #ifdef USE_RTS set_rts(fd,RTS_SET); #endif ret = write(fd, (uint8_t*)buf, size); tcdrain(fd); #ifdef USE_RTS set_rts(fd,RTS_CLR); #endif >if(ret!=size) return 0; return 1; >
Здесь poll настроен на передачу. Единственное, на что стоит обратить внимание, это на вызов функции tcdrain(fd); Эта функция будет ожидать, пока все данные вывода, записанные на объект, на который ссылается fd , не будут переданы. Функцию int set_rts(int fd, int on) мы разберём в следующей главе.
▍ Как подрыгать ножкой DTR и RTS, а также пара слов о RS-485
Кроме системных вызовов read и write , есть системный вызов ioctl , который позволяет осуществлять тонкую настройку. Будем честны, вот эта работа со структурой termios , внизу имеет системный вызов ioctl , который записывает все настройки в ядро. Но это от нас скрыто библиотечными функциями. Системный вызов ioctl не стандартизован и очень опасен, но позволяет производить тонкую настройку порта. Приведу пример функции установки или снятия сигнала rts.
int set_rts(int fd, int on) < int flags; ioctl(fd, TIOCMGET, &flags); if (on) < flags |= TIOCM_RTS; >else < flags &= ~TIOCM_RTS; >ioctl(fd, TIOCMSET, &flags); return 1; >
Изначально мы получаем состояние флагов порта в переменную &flags .
ioctl(fd, TIOCMGET, &flags);
После чего, либо снимаем флаг состояния TIOCM_RTS, либо устанавливаем его.
И после этого записываем его
ioctl(fd, TIOCMSET, &flags);
Аналогично выглядит функция работы с сигналом DTR.
int set_dtr(int fd, int on) < int flags; ioctl(fd, TIOCMGET, &flags); if (on) < flags |= TIOCM_DTR; >else < flags &= ~TIOCM_DTR; >ioctl(fd, TIOCMSET, &flags); return 1; >
С RS-485 в линуксе всё обстоит неважно. Этот интерфейс совершенно не стандартизирован, и каждый производитель может делать всё, что захочет. Где-то его нужно инициализировать в ядре, и драйвер ядра будет автоматически переключать приёмо-передатчик, где-то это всё реализовано аппаратно. Тут всё как бог на душу положит. Поэтому, с RS-485 приходится разбираться на месте. Но моя практика показывает, что всё написанное в документации – не актуально. Единственный эффективный путь – это лезть в код ядра и смотреть, что же там происходит на самом деле.
▍ Выводы
Очень сложно в рамках маленькой статьи рассказать о таком сложном явлении в linux, как работа с терминалами. Получается так, что надо сначала понять почему COM-порт не порт, а на самом деле терминал, и как его превратить обратно в порт. Этот терминал имеет множество режимов работы и функционала. И, в силу исторических причин, несёт это огромное наследие дальше. Работа с другими интерфейсами в linux (тем же i2c сильно проще). Реальная работа с железом, именно с портами идёт в драйверах внутри ядра linux. Здесь мы взаимодействуем с интерфейсом, который предоставляет нам драйвер.
Если хочется сделать красиво, то правильно будет чтение из порта осуществлять в отдельном программном потоке, а основная программа будет работать в другом потоке. Дальше уже передачу данных осуществлять с помощью междупоточного взаимодействия. Тут можно и посылать сигналы основному потоку, после получения данных (аналог прерывания по получению), либо любое другое приятное решение на любой вкус и цвет.
Для тех, кто ничего не понял в этой статье, либо просто хочет расширить свой кругозор, рекомендую посмотреть мой вебинар о программировании СОМ-портов под линукс. Там я достаточно подробно и с живыми примерами рассказываю об особенностях работы с ними.
Видео материал:
Примеры к данным видео можно найти вот тут.
Все программы, которые продемонстрированные были в данной статье можно найти у меня на гитхабе.
▍ P.S.
Ранее эта статья была опубликована на сайте easyelectronics.ru, но я считаю что читателям хабра, которые не посещают этот блог, тоже будет очень полезно с ней ознакомиться. Тем более, что информация до сих пор очень актуальна. Да и тема настолько огромна, что в рамках одной статьи невозможно всё рассказать.
- Блог компании RUVDS.com
- Программирование
- Системное программирование
- Разработка под Linux
- Производство и разработка электроники