Работа с COM-портами под Windows

altПрактически любому компьютеру приходится связываться с внешними устройствами. Практически любому программисту приходилось (приходится, придется) ваять программы под эти устройства. Огромное количество внешних устройств общаются с компьютером посредством RS-232. Отсюда и огромное количество вопросов от начинающих разработчиков. Количество вопросов на тему «как мне записать/принять данные с com-порта» на форумах по программированию не убывает, а скорее растет. Именно количество этих вопросов побудило меня к написанию статьи. Хотелось бы подчеркнуть, что статья предназначена именно для новичков в этом вопросе, и соответственно я старался упростить изложение материала.

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

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

Модуль для работы с портом содержит четыре процедуры:

PortInit — инициализация работы и запуск потока приема данных с порта.
KillComm — собственно убийство потока приема и самого порта.
WriteComm — запись в порт.
Процедура PortInit.
Для начала необходимо создать порт и получить его идентификационный номер (хотя, строго говоря, создать файл и получить хэндл). Делается это одной функцией CreateFile:

CommHandle:= CreateFile(‘COM1’,GENERIC_READ or GENERIC_WRITE, 0, nil,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL or FILE_FLAG_OVERLAPPED,0);
CommHandle — хэндл, то есть номер созданного безобразия. Тип — THandle. В дальнейшем работаем в программе только с ней.
Первый параметр ‘COM1’ — собственно имя порта. Его соответственно можно менять (обратите внимание это не тип string, а тип PChar). Остальные установки достаточно стандартны и менять их часто не придется. Хотя конечно можно (и нужно) и по хэлпу полазить и дать волю любопытству.

Теперь настраиваем параметры порта, а так же маску. Маска — это описание такого события, которое порт будет ждать, и по которому будет вестись обработка событий. В данном примере рассматриваем частный случай — приход символа «возврат каретки». Но потрудитесь посмотреть описание функции SetCommMask (клавиша F1 находиться сверху слева клавиатуры). Очень полезно знать, по каким событиям можно еще организовать обработку.

SetCommMask(CommHandle, EV_RXFLAG); — устанавливаем маску EV_RXFLAG — «обработка по определенному символу». Иными словами, как только в порт придет необходимый символ — то программа начнет обрабатывать данное событие. При отслеживании нескольких событий они задаются через or (логическое или).

Сам символ задаем в DCB-структуре. DCB- структура — это управляющая структура Вашего порта. Ключевая штуковина. Необходимо ее заполнить. Собственно в ней определяются настройки порта.

GetCommState(CommHandle,DCB); — получаем текущее DCB.
DCB.BaudRate:=CBR_9600; — устанавливаем скорость работы.
DCB.Parity:=NOPARITY; — устанавливаем отсутствие проверки на четность
DCB.ByteSize:=8; — 8 бит в передаваемом байте.
DCB.StopBits:=OneStopBit; — одиночный стоп-бит.
DCB.EvtChar:=chr(13); — вот собственно задаем символ для SetCommMask. В данном случае — возврат каретки.
SetCommState(cId,DCB); — ну теперь собственно прописываем исправленное DCB.

Естественно это не все параметры DCB, а только самые важные и часто используемые. Всю структуру DCB, а так же все значения параметров можно получить с помощью все той же красивой клавиши под названием F1.

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

CommThread := CreateThread(nil,0,@ReadComm,nil,0,ThreadID);
Переменная CommThread — хэндл, но уже на поток.
ReadComm — собственно процедура, по которой и производиться обработка. «Собака» означает, что передаем не имя процедуры, а ее адрес. ThreadID — идентификатор потока. Тема параллельных потоков (или нитей) сама по себе интересная, но объяснение ее не входит в задачи этой статьи, хотя изучение данного материала настоятельно рекомендую. А пока просто запомните — данная строка запускает процедуру ReadComm параллельно основной программе.
Процедура ReadComm
Теперь пристально рассмотрим процедуру ReadComm. Во-первых, это обычная процедура, входя состав модуля. Во-вторых, все ее содержимое зацикливается с помощью while true do. Но это не смертельно. Все-таки запускаем процедуру исключительно в отдельном потоке, так что беды не будет. Поток же убивается по мере необходимости.

Итак, процедура содержит цикл и в цикле ждет событие

WaitCommEvent(CommHandle,TransMask,@Ovr);
Тут, собственно, наш поток останавливается и ждет какого-либо события прописанного в SetCommMask (в процедуре PortInit). Как только событие произошло программа идет дальше
(TransMask and EV_RXFLAG)=EV_RXFLAG — этим выражением мы проверяем, а то ли событие произошло, что нам надо. Вроде то самое. Тут надо отметить, что в SetCommMask можно поставить с помощью оператора or несколько событий. Тогда в обработчике после ожидания надо соответственно предусмотреть и несколько if then. Что бы прописать реакцию на каждое событие.
Идем дальше.
ClearCommError(CommHandle,Errs,@Stat); — вопреки названию, здесь эта функция очищает не ошибки, а собственно факт прихода события. Без нее RXFLAG так и останется висеть. Можете попробовать угадать, что будет на следующем цикле приема.

Ну а потом идет собственно прием Kols := Stat.cbInQue; — берем количество байт в буфере порта
ReadFile(CommHandle,Resive,Kols,Kols,@Ovr); — считываем все в массив Resive.
Далее все полученное в Resive надо обработать. Как это делается — вопрос конкретного разработчика, удобства и конечно протокола обмена. Одно могу сказать точно — НЕ ДЕЛАЙТЕ как сделано в примере. Не надо из потока обращаться к визуальным компонентам (например, к Panel1 J). Лучшее решение — чтобы юнит по работе с портом вообще не видел главный юнит и главную форму. Вывод данных на экран или на обработку можно сделать или по таймеру или, например, послав пользовательское сообщение (message), ну а в обработчике организовать вывод принятой информации на экран.

Процедура WriteComm.
Собственно она производит запись в порт. В примере — один байт.

KolByte:=1;
Transmit[0]:=chr(A);
WriteFile(CommHandle,Transmit,KolByte,KolByte,@Ovr);
Как послать несколько байт? Ну, уже должны догадаться :), что переменная Kolbyte — это и есть собственно количество посылаемых байт. Послать строку еще проще. Заполняете KolByte количеством символов в строке, а вместо массива Transmite используем строковый тип, только не в паскалевском формате, а в PChar (он же си-шный формат строки, он же null-terminated string). Надо отметить, что во всех функциях API используется именно этот строковый стандарт, очевидно по причине написания самой операционки на Си. Еще раз позволю себе намекнуть на использования хелпов и на этот раз отошлю на описание функции StrPcopy. Это поможет, надеюсь.
Процедура KillComm
Завязываем использования порта и все подметаем за собой. TerminateThread(CommThread,0); — «убиение» параллельного потока приема
CloseHandle(CommHandle); — «убиение» собственно файла-порта.
Заключение
Данного примера и комментариев я надеюсь, хватит, чтобы самостоятельно разобраться и своять программу, которая будет общаться с необходимой железякой. Хотелось бы напоследок озвучить еще вопросы, которые часто задаются на форумах:

1) Как мне управлять модемом?
Модем — по сути, тот же порт. Берем протокол описания команд (книжка, обычна идущая в комплекте с модемом) и пилите, Шура, пилите… Например, что бы повесить трубку, надо послать строку ‘ATZ’ в модем.
2) Какой компонент мне использовать для работы с com-портом?
Не нужны никакие компоненты. API для этих целей вполне достаточно. Да и как видите не сложно это. А универсальности побольше будет.
3) Как мне сделать импульсы определенной длины на com-порте?
В данном примере этого нет. И этого нельзя сделать на обычных информационных контактах приема передачи. Для этого можно использовать, например ножку порта DTR и функцию EscapeCommFunction(CommHandle, SETDTR); Опять обращаемся к хелпу и находим, что можно во втором параметре и сбрасывать тот же DTR. С помощи манипуляции временем и чередованием параметров мы и достигаем цели и задачи.
Кстати, подобным образом можно подавать питание на маленькие, низко потребляющие устройства (как, например, сделано для мыши) или соорудить коммутатор на несколько устройств и переключать их. Штука крайне полезная.
4) Как мне принимать импульсы на com-порт и узнать их длину?
Задача обратная. Реализуется так же как и предыдущая. Используем функцию GetModemStatus, вместо EscapeCommFunction. Надо отметить, что эту функцию я ручками не щупал. Но по описанию — должно работать.

Понравилась статья? Поделиться с друзьями: