Copyright Ó2000 Панкратов M. Б.( panmb@)
Программирование последовательного порта в Win32
Большинству счастливых обладателей персонального компьютера последовательный[1] порт известен как разъем на задней панели системного блока, к которому они подключают мышку. Но он может пригодиться и для многих других целей: для подключения внешнего модема, второго компьютера для обмена файлами по нуль-модемному кабелю, и, наконец, для любых других «нестандартных» электронных устройств, которым нужно обмениваться данными с персональным компьютером типа цифрового фотоаппарата или электронного органайзера. Последовательный порт также очень часто используют как канал обмена информацией в промышленных системах управления и сбора данных, например, для обмена данными с микроконтроллерами.
В любом случае если перед вами встала задача написать программу, которая обменивается данными через последовательный порт, работающую под Windows95 или NT и использующая API Win32, это довольно нетривиальная задача. При использовании Win32 API работа с последовательном портом существенно отличается от программирования оного устройства в DOS или Windows 3.x. Кроме того, Windows поддерживает TAPI (Telephone API), с помощью которого реализуются интерфейс с модемами и контроль за совершением звонков. Поэтому если вы разрабатываете приложения для работы с модемом, то лучше использовать TAPI. (в данной статье не рассматривается)
Для написания приложения, работающего с последовательным портом с использованием Win32 API, читатель должен быть знаком с такими фундаментальными понятиями Win32, как многопоточное программирование и синхронизация потоков. Подробную информацию по этим вопросом можно найти в [1].
Работу с последовательным портом можно разделить на несколько этапов:
§ открытие порта;
§ установка его параметров;
§ цикл чтения/запись данных в порт;
§ закрытие порта.
Начнём порядку.
Открытие порта.
Для открытия порта используются функция CreateFile. В первом параметре функции в виде строки задаётся номер открываемого порта COM1, COM2 и т. д. Win32 не реализует механизма определения количества последовательных портов, установленных в системе. Традиционно на персональном компьютере присутствуют четыре порта: COM1, COM2, COM3 и COM4, хотя в некоторых системах их может быть больше. Поэтому в разрабатываемой программе желательно предусмотреть возможность выбора номера порта пользователем.
Вторым параметром задается тип доступа к порту, для чтения/записи необходимо указать флаги GENERIC_READ|GENERIC_WRITE. В многопоточной среде, которой является Windows, существует вероятность того, что к данному порту обратятся несколько процессов. Поэтому в следующий параметр, определяющий режим разделения порта между разными процессами. заносим нуль, что означает, что после открытия порта его уже нельзя будет повторно открыть до тех пор, пока его не закроют.
Четвертый параметр указывает на структуру SECURITY_ATTRIBUTES, позволяющую сообщить информацию о защите связанного с портом объекта ядра, в данном случае никакой особой защиты не нужно, заносим в него NULL. В следующий параметр заноситься флаг OPEN_EXISTING, указывающий, что следует открывать существующий порт. Предпоследний параметр в данном случае служит для задания метода, который операционная система использует для чтения и записи информации в порт, заносим в него флаг FILE_FLAG_OVERLAPPED, назначение которого описывется ниже, в разделе чтение/запись данных. И наконец, последний параметр задает описатель открытого файла, в данном случае не используется, заносим в него NULL.
HANDLE hCom;
hCom = CreateFile(“COM1”,
GENERIC_READ|GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,
NULL);
if (hCom == INVALID_HANDLE_VALUE)
// ошибка открытия порта;
При успешном открытии порта CreateFile взращает дескриптор порта, а если произошла ошибка, то возвращает значение INVALID_HANDLE_VALUE. На это стоит обратить внимание так как, проверка типа:
if(hCom==NULL) then // Error
неправильна, и во время работы программы может привести к ошибке.
Установка параметров последовательного порта.
Установка параметров последовательного порта осуществляется путём заполнения полей структуры Device Control Block (DCB). В ней задаются все наиболее важные свойства порта: скорость передачи данных, количество бит данных, стоповых бит, контроль чётности и др. Всего структура содержит около 27 полей, наиболее важные из них приведены в таблице 1. Более подробно описание структуры DCB есть в Windows SDK. Инициализировать DCB можно несколькими методами.
Первый метод заключается в использовании функции GetCommState, с помощью которой поля структуры заполняются значениями по умолчанию установленными операционной системой.
DCB dcb = {0};
if (!GetCommState(hCom, &dcb))
// ошибка получения параметров DCB
else
// структура DCB инициализирована
После этого необходимо модифицировать нужные поля DCB. В следующем примере устанавливаются следующие параметры порта: скорость передачи данных 4800 байт/сек, пакет содержит восемь бит данных, два стоповых бита, контроль по чётности
dcb. BaudRate = 4800;
teSize = 8;
dcb. Parity = EVENPARITY;
dcb. StopBits = TWOSTOPBIT;
Чтобы изменения параметров вступили в силу необходимо использовать функцию SetCommState:
if(!SetCommState(hCom, &dcb))
// ошибка установки параметров DCB
else
// параметры DCB установлены
Второй метод установки параметров порта заключается в использовании функции BuildCommDCB. Эта функция заполняет поля скорости передачи, контроля чётности, количества бит данных и стоповых бит структуры DCB на основе данных переданных в первом аргументе функции в виде строки. В поля структуры, отвечающие за контроль передачи, заносятся значения по умолчанию. Остальные поля DCB остаются без изменений. Этот способ удобен, когда конфигурация задаётся в командной строке или храниться в реестре.
DCB dcb;
FilMemmory(&dcb, sizeof(dcb), 0);
dcb. DCBlength = sizeof(dcb);
if(!BuildCommDCB(“4800,n,8,2”, &dcb))
// ошибка заполнения DCB
else
// структура DCB инициализирована
Чтение и запись данных
Чтение и запись данных в последовательный порт в Win32 реализуются двумя способами синхронным (nonoverlapped) и асинхронным (overlapped). При синхронном способе возврат из вызванной функции ввода - вывода не происходит до тех пор, пока не закончится чтение или запись данных. Таким образом, поток, вызвавший функцию чтения записи, оказывается заблокированным до тех пор, пока не завершиться операции ввода-вывода. После завершения операции ввода-вывода поток разблокируется и его выполнение продолжается. В приложениях использующих многопотоковость Windows, эта проблема не является серьезным препятствием: другие потоки продолжают работать, в то время когда поток, выполняющий ввод-вывод, оказывается заблокированным. Но следует учитывать, что если другие потоки вызывают функции работы с портом, когда другой поток ждёт завершения операции ввода-вывода, то они тоже окажутся заблокированными.
При асинхронном способе происходит моментальный возврат из вызывающий функции, даже если операция ввода-вывода еще не закончена. Программа может использовать время между вызовами функций инициализации и окончания ввода-вывода для выполнения какого-либо фонового кода.
Поэтому если Вы хотите использовать асинхронный ввод-вывод в своём приложении, укажите флаг FILE_FLAG_OVERLAPPED при вызове функции CreateFile, если же вы используете синхронный ввод вывод занисите в этот параметр NULL.
Какой из двух методов следует выбирать при разработке приложений? Если для Вас наиболее важны соображения переносимости приложения, то следует отдать предпочтение синхронному методу, т. к. далеко не все платформы поддерживают асинхронный ввод-вывод. С другой стороны, с помощью асинхронного доступа, корректно написанная программа работает более эффективно. Поэтому если вы разрабатываете программу для Windows 95 или NT и не собираетесь переносить Вашу программу, скажем на UNIX, то следует остановить свой выбор на асинхронном методе.
Не следует также увлекаться созданием большого количества потоков в Вашей программе. Каждый поток отнимает значительные ресурсы процессора и памяти, замедляет производительность системы в целом. Поэтому при создании многопоточных приложений нужен разумный компромисс.
При синхронном методе операция ввода-вывода происходит в два этапа: инициализация операции ввода-вывода и определение её завершения. Инициализация операции заключается в правильном заполнении членов структуры OVERLAPPED, создания события со сбросом вручную для определения момента завершения операции ввода-вывода и вызова функции ReadFile для чтения или WriteFile для записи данных. Для определения завершения выполнения операции ввода–вывода необходимо применить одну из функций ожидания события, а также проверить результат выполнения асинхронной операции на предмет ошибок.
Чтение
Чтение данных из последовательного порта осуществляется с помощью функции ReadFile. Эта функция применяется как для синхронного, так и для асинхронного чтения. Для синхронного чтения при вызове функции в последний параметр необходимо занести NULL. Для асинхронного метода необходимо передать адрес предварительно инициализированной структуры OVERLAPPED. Для операций ввода-вывода необходимо инициализировать только поле hEvent этой структуры, остальные поля функциями чтения/записи не используются и в них заносятся нули. Ниже приведенный фрагмент кода иллюстрирует асинхронное чтение данных.
#define TIMEOUT 500
DWORD dwRead, dwWait;
OVERLAPPED Sync = {0};
// Создаём событие для контроля за асинхронным чтением
Sync. hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if(Sync. hEvent==NULL)
// Ошибка создания события …
// Начинаем чтение…
if(!ReadFile(hCom, pBuffer, BUFFER_SIZE, &dwRead, &Sync)) {
if(GetLastError() != ERROR_IO_PENDING)
// Ошибка чтения данных
else {
// Ожидаем завершение операции чтения
dwWait = WaitForSingleObject(Sync. hEvent, TIMEOUT);
switch (dwWait) {
case WAIT_OBJECT_0: // Операция чтения закончена
if(!GetOverlappedResult(hCom, &Sync, &dwRead, FALSE))
// Ошибка выполения операции
else
// Обработка полученных данных
ProcessData(pBuffer, dwRead);
break;
case WAIT_TIMEOUT:
// Операция чтения данных ещё не закончилась
// можно занятся чем-нибудь полезным
break;
default:
// Ошибка выполнения WaitForSingleObject
break;
}
}
}
else
// Операция чтения уже завершилась,
// можно обрабатывать полученные данные
ProcessData(pBuffer, dwRead);
CloseHandle(Sync. hEvent);
После инициализации процедуры чтения данных, вызывается функция WaitForSingleObject, которая ожидает завершения операции чтения данных определенное время, указываемое в параметре TIMEOUT (в миллисекундах). Если в этом параметре указать ноль, то функция немедленно возвращает состояние объекта: находится ли он в занятом (WAIT_TIMEOUT) или свободном состоянии (WAIT_OBJECT_0). Этой же функции передается дескриптор события hEvent структуры OVERLAPPED, который и указывает на тестируемый объект. Следует заметить, что данная функция возвращает не результат операции ввода вывода, а только её состояние: закончена она или нет. Узнать результат операции ввода-вывода можно с помощью функции GetOverlappedResult.
Запись
Передача данных через последовательный порт очень похожа на чтение данных и использует тот же API. Вот пример:
DWORD dwWrite, dwWait;
OVERLAPPED Sync = {0};
// Создаём событие для контроля за асинхронной записью
Sync. hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if(Sync. hEvent==NULL)
// Ошибка создания события …
// Начинаем запись…
if(!WriteFile(hCom, pBuffer, BUFFER_SIZE, &dwWrite, &Sync)) {
if(GetLastError() != ERROR_IO_PENDING)
// Ошибка записи данных
else {
// Ожидаем завершение операции записи
dwWait = WaitForSingleObject(Sync. hEvent, INFINITE);
switch(dwWait) {
case WAIT_OBJECT_0: // Операция чтения закончена
if(!GetOverlappedResult(hCom, &Sync, &dwWrite, FALSE))
// Ошибка выполения операции
else
// Запись завершена
break;
default:
// Ошибка выполнения WaitForSingleObject
break;
}
}
}
else
// Запись завершена
CloseHandle(Sync. hEvent);
Обратите внимание, что в выше приведенном примере при вызове функции WaitForSingleObject в последнем параметре, который задает значение таймаута, указано значение INFINITE. Указав это значения, функция будет ожидать завершения операции ввода-вывода до бесконечности. Таким образом, программа, выполняющая операцию записи «подвиснет». Для решения этой проблемы можно использовать несколько методов:
§ поместить код, выполняющий запись данных в отдельный поток, что позволяет выполняться другим потокам, пока происходит запись данных.
§ Использовать установки структуры COMMTIMEOUTS для превентивного завершения операции ввода-вывода после истечения таймаута чтения или записи.
§ Указывать при каждом вызове функции WaitForSingleObject действительное значение таймаута. Такой подход может породить проблемы, если программа начнёт следующий вывод данных, а предыдущий ещё не закончен.
Вместо применения функции WaitForSingleObject можно использовать функцию GetOverlappedResult, передав в последнем параметре TRUE
// После вызова WriteFile и проверки ошибок
if(!GetOverlappedResult(hCom, &Sync, &dwWrite, TRUE))
// Ошибка
else
// Запись данных закончена
Распространенной ошибкой при использовании асинхронных методов ввода-вывода является повторное использование структуры OVERLAPPED до завершения операции чтения-записи, в которой она использовалась. Если Вы хотите начать другую асинхронную операцию до завершения предыдущей, необходимо инициализировать новую структуру OVERLAPPED и создать событие со сбросом вручную, записав его дескриптор в поле hEvent этой структуры.
Мониторинг состояния порта
Рассмотрим типичный пример: с компьютера посылается запрос на получение данных от внешнего устройства, подключенного к последовательному порту, в ответ на который устройство посылает пакет с данными. Как определить момент времени, когда нужно начинать считывать данные? Можно конечно пытаться считывать данные в бесконечном цикле, но сами понимаете, это не самый лучший из способов.
Для мониторинга состояния последовательного порта необходимо установить маску события, при возникновении которого будет оповещено приложение. Установка маски производится с помощью функции SetCommMask, а извещение о наступлении события получают с помощью функции WaitCommEvent. Возможные маски событий, устанавливаемые функцией SetCommMask, приведены в таблице 2.
DWORD dwCommMask;
dwCommMask = EV_RXCHAR | EV_TXEMPTY;
if(!SetCommMask(hCom, dwCommMask))
// Ошибка установка маски
Ожидать наступления события можно также синхронно и асинхронно. Для синхронного метода занесите в последний параметр функции WaitCommEvent NULL. Функция не вернёт управление программе то тех пор, пока не наступает событие указанное при помощи функции SetCommMask. А если событие не произойдет, то и программа «подвиснет». К тому же, в Windows 95 имеется баг, заключающийся в том, что флаг EV_RING не распознаётся этой операционной системой. При асинхронном методе, как и в случае чтения/записи, следует использовать структуру OVERLAPPED и событие со сбросом вручную.
#define STATUS_TIMEOUT 500
DWORD dwRes, dwCommEvent;
BOOL fWaiting = FALSE;
OVERLAPPED Sync = {0};
Sync. hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if(Sync. hEvent == NULL)
// Ошибка создания события …
for( ; ; ) {
if(!fWaiting) {
if(!WaitCommEvent(hCom, &dwCommEvent, &Sync)) {
if(GetLastError() == ERROR_IO_PENDING)
fWaiting = TRUE;
else
// Ошибка в WaitCommEvent
break;
}
else
// Сообщаем о наступлении события
ReportStatusEvent(dwCommEvent);
}
// Check on overlapped operation.
if(fWaiting) {
// Ожидаем наступления события
dwRes = WaitForSingleObject(Sync. hEvent, STATUS_TIMEOUT);
switch(dwRes) {
case WAIT_OBJECT_0: // Событие произошло
if(!GetOverlappedResult(hCom, &Sync, &dwRes, FALSE))
// Ошибка
else
// Сообщаем о наступлении события
ReportStatusEvent(dwCommEvent);
// Устанавливанм флаг ожидания нового события
fWaiting = FALSE;
break;
case WAIT_TIMEOUT:
// Операция ещё не завершилась
// можно занятся чем-нибудь полезным
break;
default:
// Ошибка в WaitForSingleObject
break;
}
}
}
CloseHandle(Sync. hEvent);
Используя флаг EV_RXCHAR можно определить момент, когда байт данных попадает в порт и помещается во входной буфер. Таким образом, у нас появляется возможность вызывать функцию ReadFile, когда данные уже находятся во входном буфере, вместо того чтобы ждать, когда эти данные появятся. Приводимый ниже пример ждёт прихода символа во входной буфер и считывает его, затем операция повторяется снова.
DWORD dwCommEvent, dwRead;
char chRead;
if(!SetCommMask(hCom, EV_RXCHAR))
// Ошибка установка маски
for( ; ; ) {
// Ждём появления символа
if(WaitCommEvent(hCom, &dwCommEvent, NULL)) {
if(ReadFile(hCom, &chRead, 1, &dwRead, NULL))
// Байт прочитан
else
// Ошибка чтения
break;
}
else
// Ошибка в WaitCommEvent
break;
}
В приведенном выше фрагменте сидит маленький баг, который не виден невооруженным глазом, но может привести к печальным последствиям во время работы программы использующий подобный алгоритм. Суть ошибки в следующем: Во входной буфер поступает символ, который вызывает событие EV_RXCHAR, программа считывает поступивший байт. Теперь предположим во входной буфер поступают два байта. Первый байт считывается, как было описано выше, второй байт выставляет флаг EV_RXCHAR, и когда программа снова вызывает функцию WaitCommEvent флаг EV_RXCHAR уже установлен и программа считывает второй байт. Теперь рассмотрим случай, когда один за другим в последовательный порт поступают три или более байт. Первый байт вызывает событие EV_RXCHAR и считывается, в это время второй байт выставляет тот же флаг EV_RXCHAR, прибытие третьего байта вызывает установку этого же флага (а он уже установлен появлением второго байта!). Далее второй байт считывается функцией ReadFile, флаг сбрасывается и … третий байт остаётся во входном буфере, а Ваша программа об этом и не знает!
Описанную выше проблему можно решить следующим образом программа уведомляется о поступлении во входной буфер данных (WaitCommEvent), а далее она считывает (ReadFile) данные в цикле не проверяя прихода новых данных, пока не считает нулевой байт:
DWORD dwCommEvent, dwRead;
char chRead;
if(!SetCommMask(hCom, EV_RXCHAR))
// Ошибка установка маски
for( ; ; ) {
// Ждём появления символа
if(WaitCommEvent(hCom, &dwCommEvent, NULL)) {
do {
if(ReadFile(hCom, &chRead, 1, &dwRead, NULL))
// Байт прочитан
else
// Ошибка чтения
break;
} while (dwRead);
}
else
// Ошибка в WaitCommEvent
break;
}
Этот способ достаточно прост, но также не лишен недостатка, приходится всё время считывать по одному байту. Есть ли способ узнать, сколько байт находится во входном буфере и считать данные за один вызов функции ReadFile? Оказывается, есть, с помощью функции Win32 API ClearCommError, с помощью которой можно узнать, сколько символов находится во входном буфере. Такой метод несколько сложнее, но эффективнее. Предлагаю читателю разобраться в деталях самостоятельно.
За рамками данной статье остались такие темы, как обработка ошибок передачи/приёма данных и контроль статуса порта, управление внутренним таймаутом передачи данных Windows, а также контроль передачи. Для интересующихся, могу порекомендовать статью в Internet на сайте Microsoft[2]. От туда же можно скачать исходные тексты программы работы с последовательным портом. Для интересующихся физическими характеристиками последовательного порта могут оказаться полезными источниками [3, 4].
DCBlength | Размер структуры DCB в байтах. |
BaudRate | Скорость передачи данных |
fBinary | Устанавливает режим передачи двоичных данных. Всегда TRUE, т. к. Win32 не поддерживает не двоичные режимы передачи данных. |
fParity | Контроль чётности. Если TRUE, происходит проверка чётности и система сообщает о наличии ошибок при их возникновении. |
fOutxCtsFlow | Указывает способ контроля линии CTS (clear to send) для управления передачей. Если TRUE и CTS имеет низкий потенциал, посылка данных приостанавливается до тех пор, пока на линии CTS не появится высокий потенциал |
fOutxDsrFlow | Указывает способ контроля линии DSR (data set ready) для управления передачей. Если TRUE и DSR имеет низкий потенциал, посылка данных приостанавливается до тех пор, пока на линии CTS не появится высокий потенциал. |
fDtrControl | Устанавливает способ контроля над DTR (data terminal ready). Может принимать следующие значения DTR_CONTROL_DISABLE, DTR_CONTROL_ENABLE и DTR_CONTROL_HANDSHAKE |
fDsrSensitivity | Если TRUE, драйвер последовательного порта игнорирует полученную информацию при низком потенциале на линии DSR |
fTXContinueOnXoff | Если TRUE, после посылки символа XOFF передача продолжается |
fOutX | Если TRUE, передача останавливается при получении символа XOFF и возобновляется при получении XON. |
fInX | Если TRUE, то при наличии во входном буфере XoffLim байт посылает символ XOFF, а при наличии XonLim посылается символ XON. |
fErrorChar | Если TRUE, а также fParity тоже TRUE, то байты, полученные с ошибкой контроля четности, будут заменены символом указанным в поле ErrorChar. |
fNull | Если TRUE, то нулевые байты отбрасываются. |
fRtsControl | Устанавливает способ контроля над RTS (request to send). Может принимать следующие значения RTS _CONTROL_DISABLE, RTS _CONTROL_ENABLE, RTS _CONTROL_HANDSHAKE и RTS_CONTROL_TOGGLE. |
fAbortOnError | Если TRUE, драйвер последовательного порта при возникновении ошибки прекращает все операции приёма и передачи данных. Драйвер сообщает об ошибке (ERROR_IO_ABORTED) и пользователь должен вызвать функцию ClearCommError для возобновления операций ввода-вывода. |
XonLim | Указывает минимальное количество байт во входном буфере, при котором происходит посылка символа XON. |
XoffLim | Указывает максимальное количество байт во входном буфере, при котором происходит посылка символа XOFF. |
Parity | Способ контроля чётности, может принимать следующие значения: EVENPARITY, MARKPARITY, NOPARITY, ODDPARITY. |
StopBits | Количество стоповых бит, может принимать значения: ONESTOPBIT, ONE5STOPBIT, TWOSTOPBIT. |
XonChar | Значение символа XON (для приёма и передачи). |
XoffChar | Значение символа XOFF (для приёма и передачи). |
ErrorChar | Значение символа используемого для замены байт принятых с ошибкой чётности. |
EofChar | Значение символа передаваемого при конце передачи данных. |
EvtChar | Значение символа, которое вызывает наступление события EV_RXFLAG |
Таблица 1. Поля структуры DCB.
EV_BREAK | Break получен на входе |
EV_CTS | Сигнал CTS (clear to send) изменил свое состояние. Для определения состояния линии CTS используйте функцию GetCommModemStatus |
EV_DSR | Сигнал DSR (data set ready) изменил свое состояние. Для определения состояния линии DSR используйте функцию GetCommModemStatus |
EV_ERR | Ошибка состояния линии. К ошибкам линии относятся: CE_FRAME, CE_OVERRUN и CE_RXPARITY. Для выяснения причины возникновения ошибки используйте функцию ClearCommError. |
EV_RING | Индикатор звонка |
EV_RSLD | Сигнал RLSD (receive line signal detected) изменил свое состояние. Для определения состояния линии RLSD используйте функцию GetCommModemStatus |
EV_RXCHAR | Получен и помещён во входной буфер новый символ |
EV_RXFALG | Получен и помещён во входной буфер управляющий символ. Управляющий символ задаётся в поле EvtChar структуры DCB. |
EV_TXEMPTY | Был послан последний символ из выходного буфера |
Таблица 2. Маски событий.
С автором статьи можно связаться по электронной почте panmb@
Литература:
1. Джеффри Рихтер “Windows для профессионалов”, “Русская редакция“ М.,1995
2. Allen Denever “Serial Communications in Win32” (MSDN Library) http://premium. /msdn/library/techart/msdn_serial. htm
3. http://www. *****/cp1251/protocols/signal. html
4. http://buratino. *****/win/RS-232.html
[1] Последовательный порт также часто называют serial port, com port или RS-232


