Мне часто задают вопросы о работе с СОМ портами из программ, написаных для Windows 95/98/NT. Причем чаще всего спрашивают разработчики всевозможных управляющих устройств. Эти устройства либо были разработаны давно, еще в эпоху MS-DOS, либо разрабатываются сейчас. Но объединяет их одно - устройство должно подключаться к компьютеру, в большинстве случаев через RS-232 (COM), реже, через Centronics (LPT).
В литературе, чаще всего, управление последовательным и параллельным портами описывается на уровне регистров этих портов, причем примеры программ приводятся на языке Assembler. Это не удивительно. Последовательный порт довольно медленное устройство, к тому же специфическое. Поэтому в программах работающих с портами используются прерывания. Параллельный порт быстрее, но тоже медленный и не менее специфичный. Взять хотя бы возможность этого порта работать в двух направлениях, да еще и с использованием ПДП (DMA).
Написать программу, управляющую устройством через COM порт, для MS-DOS не так сложно. Это частенько делали не программисты, а сами разработчики устройства. Сложнее было сделать красивый и удобный интерфейс пользователя. Этим обычно занимались профессиональные программисты. С платформой Win32 дело обстоит сложнее. Но только на первый взгляд. Конечно напрямую работать с регистрами портов нельзя, Windows это не позволяет, зато можно не обращать внимания на тонкости различных реализаций (i8055, 16450, 16550A) и не возиться с обработкой прерываний.
Описание программирования будет состоять из подробного описания функций, специфических для работы с портами, краткого описания функций работы с файлами (с портами в Win32 работают как с файлами), краткого описания функций многопотоковой обработки и, естественно, примеров программ.
Сразу хочу оговориться, что Windows требует точного соблюдения аппаратного протока обмена с внешними устройствами. Другими словами, у Вас не получится управлять, например, светодиодом подключенным к одному из выводов параллельного порта. Просто потому, что система будет требовать отработки и сигналов STROBE и ACK. Если Вас это не устраивает, то выход один - писать собственный драйвер вооружившись DDK. Это, конечно, очень интересная тема, но в данной статье я не буду ее касаться.
Как я уже говорил, с последовательными и параллельными портами в Win32 работают как с файлами. Следовательно, начинать надо с открытия порта как файла. Использовать привычные функции open и fopen при этом нельзя, необходимо воспользоваться функцией CreateFile. Эта функция предоставляется Win32 API. Ее прототип выглядит так: |
HANDLE CreateFile( LPCTSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDistribution, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile ); |
Функция имеет много параметров, большинство из которых нам не нужны. Приведу краткое описание параметров:
lpFileName |
|
Указатель на строку с именем открываемого или создаваемого файла. Формат этой строки может быть очень хитрым. В частности можно указывать сетевые имена для доступа к файлам на других компьютерах. Можно открывать логические разделы или физические диски и работать в обход файловой системы. Однако для наших задач это не нужно. Последовательные порты имеют имена "COM1", "COM2", "COM3", "COM4" и так далее. Точно так же они назывались в MS-DOS, так что ничего нового тут нет. Параллельные порты называются "LPT1", "LPT2" и так далее. Учтите, что если у Вас к порту СОМ1 подключена мышь, Windows не даст открыть этот порт. Аналогично не удастся открыть LPT1 если подключен принтер. А вот с модемом дела обстоят немного по другому. Если какая-либо программа использует модем, например вы дозвонились до своего провайдера Internet, то Вашей программе не удастся открыть порт к которому подключен модем. Во всех остальных случаях порт будет открыт и Вы сможете работать с модемом сами, из своей программы. |
|
dwDesiredAccess |
|
Задает тип доступа к файлу. Возможно использование следующих значений:
0 |
Опрос атрибутов устройства без получения доступа к нему. |
GENERIC_READ |
Файл будет считываться. |
GENERIC_WRITE |
Файл будет записываться. |
В данном примере открывается порт СОМ2 для чтения и записи, используется синхронный режим обмена. Проверяется успешность открытия порта, при ошибке выводится сообщение и программа завершается. Если порт открыт успешно, то он закрывается.
Открыв порт мы получили его в свое распоряжение. Теперь с портом может работать только наша программа. Однако, прежде чем мы займемся вводом/выводом, мы должны настроить порт. Это касается только последовательных портов, для которых мы должны задать скорость обмена, параметры четности, формат данных и прочее. Кроме того существует несколько специфичных для Windows параметров. Речь идет о тайм-аутах, которые позволяют контролировать как интервал между принимаемыми байтами, так и общее время приема сообщения. Есть возможность управлять состоянием сигналов управления модемом. Но обо всем по порядку.
Основные параметры последовательного порта описываются структурой DCB. Временные параметры структурой COMMTIMEOUTS. Существует еще несколько информационных и управляющих структур, но они используются реже. Настройка порта заключается в заполнении управляющих структур и последующем вызове функций настройки.
Поскольку основную информацию содержит структура DCB с ее описания и начнем: |
typedef struct _DCB { DWORD DCBlength; // sizeof(DCB) DWORD BaudRate; // current baud rate DWORD fBinary:1; // binary mode, no EOF check DWORD fParity:1; // enable parity checking DWORD fOutxCtsFlow:1; // CTS output flow control DWORD fOutxDsrFlow:1; // DSR output flow control DWORD fDtrControl:2; // DTR flow control type DWORD fDsrSensitivity:1; // DSR sensitivity DWORD fTXContinueOnXoff:1; // XOFF continues Tx DWORD fOutX:1; // XON/XOFF out flow control DWORD fInX:1; // XON/XOFF in flow control DWORD fErrorChar:1; // enable error replacement DWORD fNull:1; // enable null stripping DWORD fRtsControl:2; // RTS flow control DWORD fAbortOnError:1; // abort reads/writes on error DWORD fDummy2:17; // reserved WORD wReserved; // not currently used WORD XonLim; // transmit XON threshold WORD XoffLim; // transmit XOFF threshold BYTE ByteSize; // number of bits/byte, 4-8 BYTE Parity; // 0-4=no,odd,even,mark,space BYTE StopBits; // 0,1,2 = 1, 1.5, 2 char XonChar; // Tx and Rx XON character char XoffChar; // Tx and Rx XOFF character char ErrorChar; // error replacement character char EofChar; // end of input character char EvtChar; // received event character WORD wReserved1; // reserved; do not use } DCB; |
Если внимательно присмотреться, то можно заметить, что эта структура содержит почти всю управляющую информацию, которая в реальности располагается в различных регистрах последовательного порта. Теперь разберемся, что означает каждое из полей самой важной структуры:
DCBlength |
|
Задает длину, в байтах, структуры DCB. Используется для контроля корректности структуры при передаче ее адреса в функции настройки порта. |
|
BaudRate |
|
Скорость передачи данных. Возможно указание следующих констант: CBR_110, CBR_300, CBR_600, CBR_1200, CBR_2400, CBR_4800, CBR_9600, CBR_14400, CBR_19200, CBR_38400, CBR_56000, CBR_57600, CBR_115200, CBR_128000, CBR_256000. Как видно, эти константы соответствуют всем стандартным скоростям обмена. На самом деле, это поле содержит числовое значение скорости передачи, а константы просто являются символическими именами. Поэтому можно указывать, например, и CBR_9600, и просто 9600. Однако рекомендуется указывать символические константы. |
|
fBinary |
|
Включает двоичный режим обмена. Win32 не поддерживает недвоичный режим, поэтому данное поле всегда должно быть равно 1, или логической константе TRUE (что предпочтительней). В Windows 3.1, если это поле было равно FALSE, включался текстовый режим обмена. В этом режиме поступивший на вход порта символ заданый полем EofChar свидетельствовал о конце принимаемых данных. |
|
fParity |
|
Включает режим контроля четности. Если это поле равно TRUE, то выполняется проверка четности, при ошибке, в вызывающую программу, выдается соответсвующий код завершения. |
|
fOutxCtsFlow |
|
Включает режим слежения за сигналом CTS. Если это поле равно TRUE и сигнал CTS сброшен, передача данных приостанавливается до установки сигнала CTS. Это позволяет подключеному к компьютеру прибору приостановить поток передаваемой в него информации, если он не успевает ее обрабатывать. |
|
fOutxDsrFlow |
|
Включает режим слежения за сигналом DSR. Если это поле равно TRUE и сигнал DSR сброшен, передача данных прекращается до установки сигнала DSR.
|
|
fDtrControl |
|
Задает режим управления обменом для сигнала DTR. Это поле может принимать следующие значения:
DTR_CONTROL_DISABLE |
Запрещает использование линии DTR |
DTR_CONTROL_ENABLE |
Разрешает использование линии DTR |
DTR_CONTROL_HANDSHAKE |
Разрешает использование рукопожатия для выхода из ошибочных ситуаций. Этот режим используется, в частности, модемами при восстановленни в ситуации потери связи. |
|
fDsrSensitivity |
|
Задает чувствительсть коммуникационного драйвера к состоянию линии DSR. Если это поле равно TRUE, то все принимаемые данные игнорируются драйвером (коммуникационный драйвер расположен в операционной системе), за исключением тех, которые принимаются при установленом сигнале DSR. |
|
fTXContinueOnXoff |
|
Задает, прекращается ли передача при переполнении приемного буфера и передаче драйвером символа XoffChar. Если это поле равно TRUE, то передача продолжается, несмотря на то, что приемный буфер содержит более XoffLim символов и близок к переполнению, а драйвер передал символ XoffChar для приостановления потрока принимаемых данных. Если поле равно FALSE, то передача не будет продолжена до тех пор, пока в приемном буфере не останется меньше XonLim символов и драйвер не передаст символ XonChar для возобновления потока принимаемых данных. Таким образом это поле вводит некую зависимость между управлением входным и выходным потоками информации. |
|
fOutX |
|
Задает использование XON/XOFF управления потоком при передаче. Если это поле равно TRUE, то передача останавливается при приеме символа XoffChar, и возобновляется при приеме символа XonChar. |
|
fInX |
|
Задает использование XON/XOFF управления потоком при приеме. Если это поле равно TRUE, то драйвер передает символ XoffChar, когда в приемном буфере находится более XoffLim, и XonChar, когда в приемном буфере остается менее XonLim символов. |
|
fErrorChar |
|
Указывает на необходимость замены символов с ошибкой четности на символ задаваемый полем ErrorChar. Если это поле равно TRUE, и поле fParity равно TRUE, то выполняется замена.
|
|
fNull |
|
Определяет действие выполняемое при приеме нулевого байта. Если это поле TRUE, то нулевые байты отбрасываются при передаче. |
|
fRtsControl |
|
Задает режим управления потоком для сигнала RTS. Если это поле равно 0, то по умолчанию подразумевается RTS_CONTROL_HANDSHAKE. Поле может принимать одно из следующих значений:
RTS_CONTROL_DISABLE |
Запрещает использование линии RTS |
RTS_CONTROL_ENABLE |
Разрешает использование линии RTS |
RTS_CONTROL_HANDSHAKE |
Разрешает использование RTS рукопожатия. Драйвер устанавливает сигнал RTS когда приемный буфер заполнен менее, чем на половину, и сбрасывает, когда буфер заполняется более чем на три четверти. |
RTS_CONTROL_TOGGLE |
Задает, что сигнал RTS установлен, когда есть данные для передачи. Когда все символы из передающего буфера переданы, сигнал сбрасывается. |
|
fAbortOnError |
|
Задает игнорирование всех операций чтения/записи при возникновении ошибки. Если это поле равно TRUE, драйвер прекращает все операции чтения/записи для порта при возникновении ошибки. Продолжать работать с портом можно будет только после устранения причины ошибки и вызова функции ClearCommError. |
|
fDummy2 |
|
Зарезервировано и не используется. |
|
wReserved |
|
Не используется, должно быть установлено в 0. |
|
XonLim |
|
Задает минимальное число символов в приемном буфере перед посылкой символа XON. |
|
XoffLim |
|
Определяет максимальное количество байт в приемном буфере перед посылкой символа XOFF. Максимально допустимое количество байт в буфере вычисляется вычитанием данного значения из размера применого буфера в байтах. |
|
ByteSize |
|
Определяет число информационных бит в передаваемых и принимаемых байтах. |
|
Parity |
|
Определяет выбор схемы контроля четности. Данное поле должно содержать одно из следующих значений:
EVENPARITY |
Дополнение до четности |
MARKPARITY |
Бит четности всегда 1 |
NOPARITY
| Бит четности отсутствует |
ODDPARITY |
Дополнение до нечетности |
SPACEPARITY |
Бит четности всегда 0 |
|
StopBits |
|
Задает количество стоповых бит. Поле может принимать следующие значения:
ONESTOPBIT |
Один стоповый бит |
ONE5STOPBIT |
Полтора стоповых бита |
TWOSTOPBIT |
Два стоповых бита |
|
XonChar | |
|
Задает символ XON используемый как для примема, так и для передачи. |
|
XoffChar |
|
Задает символ XOFF используемый как для примема, так и для передачи. |
|
ErrorChar |
|
Задает символ, использующийся для замены символов с ошибочной четностью. |
|
EofChar |
|
Задает символ, использующийся для сигнализации о конце данных. |
|
EvtChar |
|
Задает символ, использующийся для сигнализации о событии. |
|
wReserved1 |
|
Зарезервировано и не используется. |
Так как поля структуры DCB используются для конфигурирования микросхем портов, на них накладываются некоторые ограничения. Размер байта должен быть 5, 6, 7 или 8 бит. Комбинация из пяти битного байта и двух стоповых бит является недопустимой. Так же как и комбинация из шести, семи или восьми битного байта и полутора стоповых бит.
Только что рассмотренная нами структура DCB самая большая из всех, использующихся для настройки последовательных портов. Но она и самая важная. Заполнение всех полей этой структуры может вызвать затруднения, так как надо очень четко представлять как работает последовательный порт. Поэтому ручную установку полей можно порекомендовать опытным программистам. Если же Вы чувствуете себя не очень уверено, воспользуйтесь функцией BuildCommDCB, которая позволяет заполнить поля структуры DCB на основе строки, по синтаксису аналогичной строке команды mode. Вот как выглядит прототип этой функции:
BOOL BuildCommDCB(LPCTSTR lpDef, LPDCB lpDCB); |
Как видно, функция очень проста и имеет всего два параметра:
lpDef |
|
Указатель на строку с конфигурационной информацией в формате команды mode. Например, следующая строка задает скорость 1200, без четности, 8 бит данных и 1 стоповый бит.
baud=1200 parity=N data=8 stop=1 |
lpDCB |
|
Указатель на заполняемую структуру DCB. При этом структура должна быть уже создана и заполнена нулями, кроме поля DCBlength, которое должно содержать корректное значение. Возможно так же использование уже заполненой структуры DCB, например полученой вызовом одной из функций чтения параметров порта. |
В случае успершного завершения функция BuildCommDCB возвращает не нулевое значение. В случае ошибки возвращается 0.
Обычно функция BuldCommDCB изменяет только явно перечисленые в строке lpDef поля. Однако существуют два исключения из этого правила:
При задании скорости обмена 110 бит в секунду автоматически устанавливается формат обмена с двумя стоповыми битами. Это сделано для совместимости с командой mode из MS-DOS или Windows NT.
По умолчанию запрещается программное (XON/XOFF) и аппаратное управление потоком. Вы должны вручную заполнить требуемые поля DCB если требуется управление потоком.
Функция BuilCommDCB поддерживает как новый, так и старый форматы командной строки mode. Однако, Вы не можете смешивать эти форматы в одной строке.
Новый формат строки позволяет явно задавать значения для полей DCB отвечающих за управление потоком. При использовании старого формата существуют следующие соглашения:
Для строк вида 9600,n,8,1 (не заканчивающихся символами x или p):
- fInX, fOutX,fOutXDsrFlow, fOutXCtsFlow устанавливаются в FALSE
- fDtrControl устанавливается в DTR_CONTROL_ENABLE
- fRtsControl устанавливается в RTS_CONTROL_ENABLE
Для строк вида 9600,n,8,1,x (заканчивающихся символом х):
- fInX, fOutX устанавливаются в TRUE
- fOutXDsrFlow,fOutXCtsFlow устанавливаются в FALSE
- fDtrControl устанавливается в DTR_CONTROL_ENABLE
- fRtsControl устанавливается в RTS_CONTROL_ENABLE
Для строк вида 9600,n,8,1,p (заканчивающихся символом p):
- fInX, fOutX устанавливаются в FALSE
- fOutXDsrFlow,fOutXCtsFlow устанавливаются TRUE
- fDtrControl устанавливается в DTR_CONTROL_HANDSHAKE
- fRtsControl устанавливается в RTS_CONTROL_HANDSHAKE
Следует заметить, что функция BuildCommDCB только заполняет поля DCB указанными значениями. Это подготовительный шаг к конфигурированию порта, но не само конфигурирование, которое выполняется рассматриваемыми далее функциями. Поэтому Вы можете вызвать BuildCommDCB для общего заполнения структуры DCB, затем изменить значения не устраивающих Вас полей, и после этого вызывать функцию конфигурирования порта.
Заполнить DCB можно еще одним способом. Вызовом функции GetCommState. Эта функция заполняет DCB информацией о текущем состоянии устройства, точнее о его настройках. Вот как она выглядит:
BOOL GetCommState( HANDLE hFile, LPDCB lpDCB ); |
Функция очень проста и имеет всего два параметра:
hFile |
|
Описатель открытого файла коммуникационного порта. Этот описатель возвращается функцией CreateFile. Следовательно, прежде чем получить параметры порта, Вы должны его открыть. Для функции BuildCommDCB это не требовалось. |
|
lpDCB |
|
Указатель на DCB. Для DCB должен быть выделен блок памяти. |
При успешном завершении функция возвращает ненулевое значение. При ошибке нуль. Получить параметры порта можно в любой момент, а не только при начальной настройке.
Заполнив DCB можно приступать к собственно конфигурированию порта. Это делается с помощью функции SetCommState:
BOOL SetCommState( HANDLE hFile, LPDCB lpDCB ); |
Эта функция имеет точно такие же параметры, как GetCommState. Различается только направление передачи информации. GetCommState считывает информацию из внутренних управляющих структур и регистров порта, а SetCommState наоборот заносит ее. Следует быть осторожным при вызове функции SetCommState, поскольку она изменит параметры даже в том случае, если очереди приема/передачи не пусты, что может вызвать искажение потока передаваемых или принимаемых данных.
Еще одна тонкость этой функции заключется в том, что она завершится с ошибкой, если поля XonChar и XoffChar в DCB содержат одинаковые значения.
Как всегда, в случае успешного завершения возвращается отличное от нуля значение, а в случае ошибки - нуль.
Следующей важной управляющей структурой является COMMTIMEOUTS. Она определяет параметры временных задержек при приеме и передаче. Значения, задаваемые полями этой структуры, оказывают большое влияние на работу функций чтения/записи. |
typedef struct _COMMTIMEOUTS { DWORD ReadIntervalTimeout; DWORD ReadTotalTimeoutMultiplier; DWORD ReadTotalTimeoutConstant; DWORD WriteTotalTimeoutMultiplier; DWORD WriteTotalTimeoutConstant; } COMMTIMEOUTS,*LPCOMMTIMEOUTS; |
Поля структуры COMMTIMEOUTS имеют следующие значения:
ReadIntervalTimeout |
|
Максимальное время, в миллисекундах, допустимое между двумя последовательными символами считываемыми с коммуникационной линии. Во время операции чтения временной период начинает отсчитываться с момента приема первого символа. Если интервал между двумя последовательными символами превысит заданое значение, операция чтения завершается и все данные, накопленые в буфере, передаются в программу. Нулевое значение данного поля означает, что данный тайм-аут не используется. Значение MAXDWORD, вместе с нулевыми значениями полей ReadTotalTimeoutConstant и ReadTotalTimeoutMultiplier, означает немедленный возврат из операции чтения с передачей уже принятого символа, даже если ни одного символа не было получено из линии. |
|
ReadTotalTimeoutMultiplier |
|
Задает множитель, в миллисекундах, используемый для вычисления общего тайм-аута операции чтения. Для каждой операции чтения данное значение умножается на количество запрошеных для чтения символов. |
|
ReadTotalTimeoutConstant |
|
Задает константу, в миллисекундах, используемую для вычисления общего тайм-аута операции чтения. Для каждой операции чтения данное значение прибавляется к результату умножения ReadTotalTimeoutMultiplier на количество запрошеных для чтения символов. Нулевое значение полей ReadTotalTimeoutMultiplier и ReadTotalTimeoutConstant означает, что общий тайм-аут для операции чтения не используется.
|
typedef struct _COMM_CONFIG { DWORD dwSize; WORD wVersion; WORD wReserved; DCB dcb; DWORD dwProviderSubType; DWORD dwProviderOffset; DWORD dwProviderSize; WCHAR wcProviderData[1]; } COMMCONFIG, *LPCOMMCONFIG; |
Основной частью этой структуры является уже знакомый нам DCB. Остальные поля содержат вспомогательную информацию, которая, для наших целей, не представляет особого интереса (однако эта информация может быть полезной для получения дополнительных данных о порте). Познакомимся поближе с полями:
dwSize |
|
Задает размер структуры COMMCONFIG в байтах |
|
wVersion |
|
Задает номер версии структуры COMMCONFIG. Должен быть равным 1. |
|
wReserved |
|
Зарезервировано и не используется |
|
dcb |
|
Блок управления устройством (DCB) для порта RS-232. |
|
dwProviderSubType |
|
Задает тип устройства и формат устройство-зависимого блока информации. Фактически это тип порта. Конкретные значения данного поля приведены в описании структуры COMMPROP выше. |
|
dwProviderOffset |
|
Смещение, в байтах, до устройство-зависимого блока информации от начала структуры. |
|
dwProviderSize |
|
Размер, в байтах, устройство-зависимого блока информации. |
|
wcProviderData |
|
Устройство-зависимый блок информации. Это поле может быть любого размера или вообще отсутствовать. Поскольку структура COMMCONFIG может быть в дальшейшем расширена, для определения положения данного поля следует использовать dwProviderOffset. Если dwProviderSubType PST_RS232 или PST_PARALLELPORT, то данное поле отсутствует. Если dwProviderSubType PST_MODEM, то данное поле содержит структуру MODEMSETTINGS. |
Не смотря на то, что нам нужен только DCB, приходится иметь дело со всеми полями. Заполнение данной структуры противоречивыми данными может привести к неправильной настройке порта, поэтому следует пользоваться функцией GetCommConfig:
BOOL GetCommConfig( HANDLE hCommDev, LPCOMMCONFIG lpCC, LPDWORD lpdwSize ); |
Параметры функции следующие:
hCommDev |
|
Описатель открытого коммуникационного порта. |
|
lpCC |
|
Адрес выделеного и заполненого нулями, кроме поля dwSize, блока памяти под структуру COMMCONFIG. В поле dwSize нужно занести размер структуры COMMCONFIG. После вызова функции все поля структуры будут содержать информацию о текущих параметрах порта. |
|
lpdwSize |
|
Адрес двойного слова, которое после воврата из функции будет содержать число фактически переданных в структуру байт. |
В случае успешного завершения функция возвращает ненулевое значение.
Как всегда не обошлось без тонкостей. Структура COMMPROP имеет перемнную длину, поэтому затруднительно сразу выделить требуемый блок памяти. Как и в случае с функцией GetCommProperties, функцию GetCommConfig придется вызывать дважды:
. . . COMMCONFIG *cf; DWORD sz; HANDLE port; . . . cf=(COMMCONFIG*)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,sizeof(COMMCONFIG)); cf->dwSize=sizeof(COMMCONFIG); GetCommConfig(port,cf,&sz); if(sz > sizeof(COMMCONFIG)) { cf=(COMMCONFIG*)HeapRealloc(GetProcessHeap(),HEAP_ZERO_MEMORY,cf,sz); cf->dwSize=sz; GetCommConfig(port,cf,&sz); } . . . HeapFree(GetProcessHeap(),0,cf); CloseHandle(port); . . .
Теперь, имея заполненую корректной информацией структуру COMMCONFIG, можно позволить пользователю выполнить настройку параметров с помощью функции CommConfigDialog:
BOOL CommConfigDialog( LPTSTR lpszName, HWND hWnd, LPCOMMCONFIG lpCC ); |
Вызов этой функции приводит к отображению примерно такого диалогового окна:
Вид окна может отличаться от приведенного. Это зависит от операционной системы и динамической библиотеки, предоставленной производителем порта.
Познакомимся с параметрами функции CommConfigDialog:
lpszName |
|
Указатель на строку с именем порта для которого отображается диалоговое окно. К реальному имени порта эта строка не имеет никакого отношения, она просто выводится в заголовке окна.
|
|
hWnd |
|
Описатель окна, которое владеет данным диалоговым окном. Должен быть передан корректный описатель окна-владельца или NULL, если у диалогового окна нет владельца. |
|
lpCC |
|
Указатель на структуру COMMCONFIG. Эта структура содержит начальные установки используемые для отображения в диалоговом окне, и установленные пользователем изменения, при завершении диалога. |
Как и большинство других функций Win32 API, функция CommConfigDialog возвращает отличное от нуля значение, в случае успешного завершения, и нуль, если возникла ошибочная ситуация.
Функция CommConfigDialog не выполняет настройки порта. Она все лишь позволяет пользователю изменить некоторые поля в блоке DCB, содержащемся в структуре COMMCONFIG. Разумеется, Вы можете изменить установленые пользователем некорректные значения или выполнить дополнительные настройки после вызова функции GetCommConfig. Фактическая настройка порта выполняется функцией SetCommConfig:
BOOL SetCommConfig( HANDLE hCommDev, LPCOMMCONFIG lpCC, DWORD dwSize ); |
Параметры имеют тоже самое значение, как и в функции GetCommConfig. Следует заметить, что описаные три функции позволяют настраивать и некоторые параметры модема, если он подключен к порту и опознан системой. Впрочем, эта возможность может отсутствовать, если она не предусмотрена производителем оборудования.
Обратите внимание на кнопку "Restore Defaults". Вы в состоянии управлять ее поведением, правда опосредовано, с помощью функций GetDefaultCommConfig и SetDegaultCommConfig. Вот их прототипы: |
BOOL GetDefaultCommConfig( LPCSTR lpszName, LPCOMMCONFIG lpCC, LPDWORD lpdwSize );
BOOL SetDefaultCommConfig( LPCSTR lpszName, LPCOMMCONFIG lpCC, DWORD dwSize ); |
Эти функции очень похожи на GetCommConfig и SetCommConfig, но предназначены совсем для другой цели. Предположим, что Ваше устройство, по умолчанию, работает на скорости 175 бит в секунду и обменивается пятибитными символами. Системные же умолчания - 9600 бит в секунду и 8 бит в символе. Что бы пользователь, при нажатии на кнопку "Restore Defaults", получал не системные, а Ваши умолчания, воспользуйтесь функциями GetDefaultCommConfig и SetDefaultCommConfig. SetDefaultCommConfig не настраивает порт, это выполняется функцией SetCommConfig, а изменяет параметры во внутренней области коммуникационного драйвера.
Теперь познакомимся с функцией SetupComm, которая, на самом деле, совсем не то, что следует из названия. |
BOOL SetupComm( HANDLE hFile, DWORD dwInQueue, DWORD dwOutQueue ); |
Эту функцию скорее следовало назвать SetCommQueueSize, поскольку все, что она делает, это устанавливает размеры (в байтах) очередей приема и передачи. Причем размеры рекомендуемые. В общем случае, система сама в состоянии определить требуемый размер очередей, однако Вы можете вмешаться в этот процесс. Внутренние очереди драйвера позволяют избежать потери данных, если Ваша программа не успевает их считывать, и пауз в работе программы, если она передает данные слишком быстро. Размер очереди выбирается немного большим максимальной длины сообщения. Например, для протокола YMODEM, пакет данных которого имеет длину 1024 байт, разумным размером очереди будет 1200 байт.
Указаный Вами размер очереди будет принят драйвером к сведению. Но он оставляет за собой право внести коррективы или вообще отвергнуть устанавливаемое значение. В последнем случае функция завершится с ошибкой.
Внешние устройства управления объектами, чаще всего подключаемые к портам, обычно обмениваются с компьютером короткими сообщениями. Соответственно и вызов функции SetupComm не требуется. Однако, если Ваше устройство передает или принимает блоки данных длиной в несколько тысяч байт, рекомендуется установить размеры очередей драйвера.
Давайте сделаем паузу в изучении функций настройки и получения состояния коммуникационных портов. Пора от слов переходить к делу, а именно к приему и передаче данных. Начнем с синхронного чтения/записи, это проще.
Прием и передача данных выполняется функциями ReadFile и WriteFile, то есть теми же самыми, которые используются для работы с дисковыми файлами. Вот как выглядят прототипы жтих функций: |
BOOL ReadFile( HANDLE hFile, LPVOID lpBuffer, DWORD nNumOfBytesToRead, LPDWORD lpNumOfBytesRead, LPOVERLAPPED lpOverlapped );
BOOL WriteFile( HANDLE hFile, LPVOID lpBuffer, DWORD nNumOfBytesToWrite, LPDWORD lpNumOfBytesWritten, LPOVERLAPPED lpOverlapped ); |
Вы наверняка работали с этими функциями и знаете значение их параметров. Но я все таки кратко остановлюсь на их описании:
hFile |
|
Описатель открытого файла коммуникационного порта. |
|
lpBuffer |
|
Адрес буфера. Для операции записи данные из этого буфера будут передаваться в порт. Для операции чтения в этот буфер будут помещаться принятые из линии данные. |
|
nNumOfBytesToRead, nNumOfBytesToWrite |
|
Число ожидаемых к приему или предназначеных к передаче байт. |
|
nNumOfBytesRead, nNumOfBytesWritten |
|
Число фактически принятых или переданых байт. Если принято или передано меньше данных, чем запрошено, то для дискового файла это свидетельствует об ошибке, а для коммуникационного порта совсем не обязательно. Причина в тайм-аутах. |
|
lpOverlapped |
|
Адрес структуры OVERLAPPED, используемой для асинхронных операций. Подробнее как с структурой, так и с асинхронными операциями мы познакомимся позже. Для синхронных операций данный параметр должен быть равным NULL. |
Еще раз коснусь темы тайм-аутов. Если Вы не используете ни общий, ни межбайтный тайм-ауты для операции чтения и внешнее устройство прекратило передачу, то Ваша программа будет вечно ждать завершения синхронной операции. Другими словами она зависнет. Аналогичный результат может быть при использовании программного или аппаратного управления потоком. Если же тайм-ауты используются, то операция чтения нормально завершится. Только количество считанных байт будет меньше количества запрошеных для чтения. Это не обязательно свидетельствует об ошибке. Например программа может по тайм-ауту определять конец очередного блока данных. Аналогично и для операции записи, с той лишь разницей, что неполная передача данных из буфера, скорее всего, будет свидетельствовать о проблеме во внешнем устройстве. То есть будет считаться ошибкой.
Коммуникационный порт не совсем обычный файл. Например, для него нельзя выполнить операцию позиционирования файлового указателя. С другой стороны, порт позволяет управлять потоком, что нельзя делать с обычным файлом. Настало время познакомиться с функциями управления приемом/передачей данных через коммуникационные порты. Поскольку первой операцией, после открытия порта, является его сброс, то и начнем с функции выполняющей требуемые действия.
BOOL PurgeComm( HANDLE hFile, DWORD dwFlags ); |
Вызов этой функции позволяет решить две задачи: очистить очереди приема/передачи в драйвере и завершить все находящиеся в ожидании запросы ввода/вывода. Какие именно действия выполнять задается вторым параметром (значения можно комбинировать с помощью побитовой операции OR:
PURGE_TXABORT |
Немедленно прекращает все операции записи, даже если они не завершены |
PURGE_RXABORT |
Немедленно прекращает все операции чтения, даже если они не завершены |
PURGE_TXCLEAR |
Очищает очередь передачи в драйвере |
PURGE_RXCLEAR |
Очищает очередь приема в драйвере |
Вызов этой функции нужен для отбрасывания мусора, который может находиться в приемном буфере на момент запуска программы, или как результат ошибки в работе устройства. Очистка буфера передачи и завершение операций ввода/вывода так же потребуются при ошибке, как процедура восстановления, и при завершении программы, для красивого выхода.
Следует помнить, что очистка буфера передачи, как и экстреное завершение операции записи, не выполняют передачу данных находящихся в этом буфере. Данные просто отбрасываются. Если же передача остатка данных необходима, то перед вызовом PurgeComm следует вызвать функцию:
BOOL FlushFileBuffers( HANDLE hFile ); |
Приведу пример выполнения настройки порта и выполнения чтения/записи данных. |
#include <windows.h> #include <string.h>
. . .
DCB dcb; COMMTIMEOUTS ct; HANDLE port; DWORD bc;
char *buf_out="Test string"; char *buf_in;
. . .
dcb.DCBlength=sizeof(DCB); BuildCommDCB("baud=9600 parity=N data=8 stop=1",&dcb); dcb.fNull=TRUE;
ct.ReadIntervalTimeout=10; ct.ReadTotalTimeoutMultiplier=ct.ReadTotalTimeoutConstant=0; ct.WriteTotalTimeoutMultiplier=ct.WriteTotalTimeoutConstant=0;
port=CreateFile("COM2",GENERIC_READ|GENERIC_WRITE,0,NULL,OPEN_EXISTING,0,NULL);
SetCommState(port,dcb); SetCommTimeouts(port,&ct); PurgeComm(port,PURGE_TXCLEAR|PURGE_RXCLEAR); SetupComm(port,256,256);
. . .
buf_in=(char*)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,strlen(buf_out)+1); WriteFile(port,buf_out,strlen(buf_out),&bc,NULL); ReadFile(port,buf_in,strlen(buf_out),&bc,NULL);
HeapFree(GetProcessHeap(),0,buf_in); CloseHandle(port);
. . . |
Если на COM2 установить перемычку между сигналами TxD и RxD, то переменная buf_in, после выполнения ReadFile, будет содержать ту же информацию, что и buf_out. Других пояснений пример не требует, все уже было подробно рассмотрено раньше.
Иногда требуется срочно передать символ, имеющий определенное специальное значение, а в очереди передатчика уже есть данные, которые нельзя терять. В этом случае можно воспользоваться функцией: |
BOOL TransmitCommChar( HANDLE hFile, char cChar ); |
Данная функция передает один (и только один) внеочередной байт в линию, не смотря на наличие данных в очереди передатчика, и перед этими данными. Однако управление потоком действует. Функцию можно вызвать только синхронно. Более того, если байт экстренных данных, от предыдущего вызова этой функции, еще не передан в линию (например из-за функций управления потоком), то попытка экстренной передачи еще одного байта завершится ошибкой. Если Вы используете программное управление потоком, то символы приостановки и возобновления передачи (обычно CTRL-S и CTRL-Q), лучше всего передавать именно этой функцией.
Последовательный канал передачи данных можно перевести в специальное состояние, называемое разрывом связи. При этом передача данных прекращается, а выходная линия переводится в состояние "0". Приемник, обнаружив, что за время необходимое для передачи стартового бита, битов данных, бита четности и стоповых битов, приемная линия ни разу не перешла в состояние "1", так же фиксирует у себя состояние разрыва. |
BOOL SetCommBreak( HANDLE hFile );
BOOL ClearCommBreak( HANDLE hFile ); |
Следует заметить, что состояние разрыва линии устанавливается аппаратно. Поэтому нет другого способа возобновить прерваную, с помощью SetCommBreak, передачу данных, кроме вызова ClearCommBreak.
Более тонкое управление потоком данным позволяет осуществить функция: |
BOOL EscapeCommFunction( HANDLE hFile, DWORD dwFunc ); |
Выполняемое действие определяется вторым параметром, который может принимать одно из следующих значений:
CLRDTR |
Сбрасывает сигнал DTR |
CLRRTS |
Сбрасывает сигнал RTS |
SETDTR |
Устанавливет сигнал DTR |
SETRTS |
Устанавливает сигнал RTS |
SETXOFF |
Симулирует прием символа XOFF |
SETXON |
Симулирует прием символа XON |
SETBREAK |
Переводит выходную линию передатчика в состояние разрыва. SetCommBreak является упрощенной формой данного вызова. |
CLRBREAK |
Снимает состояние разрыва для выходной линии передатчика. ClearCommBreak является упрощенной формой данного вызова. |
Приостановить прием/передачу данных может и возникновение любой ошибки при установленом в TRUE поле fAbortOnError в структуре DCB использованой для настройки режимов работы коммуникационного порта. В этом случае, для восстановления нормальной работы порта, следует использовать функцию:
BOOL ClearCommError( HANDLE hFile, LPDWORD lpErrors, LPCOMSTAT lpStat ); |
Эта функция не только сбрасывает признак ошибки для соответсвующего порта, но и возвращает более подробную информацию об ошибке. Кроме того, возможно получение информации о текущем состоянии порта. Вот что означают параметры:
hFile |
|
Описатель открытого файла коммуникационного порта. |
|
lpErrors |
|
Адрес переменной, в которую заносится информация об ошибке. В этой переменной могут быть установлены один или несколько из следующих бит:
CE_BREAK |
Обнаружено состояние разрыва связи |
CE_DNS |
Только для Windows95. Параллельное устройство не выбрано. |
CE_FRAME |
Ошибка обрамления. |
CE_IOE |
Ошибка ввода-вывода при работе с портом |
CE_MODE |
Запрошеный режим не поддерживается, или неверный описатель hFile. Если данный бит установлен, то значение остальных бит не имеет значение. |
CE_OOP |
Только для Windows95. Для параллельного порта установлен сигнал "нет бумаги". |
CE_OVERRUN |
Ошибка перебега (переполнение аппаратного буфера), следующий символ потерян. |
CE_PTO |
Только для Windows95. Тайм-аут на параллельном порту. |
CE_RXOVER |
Переполнение приемного буфера или принят символ после символа конца файла (EOF) |
CE_RXPARITY |
Ошибка четности |
CE_TXFULL |
Переполнение буфера передачи |
|
lpStat |
|
Адрес структуры COMMSTAT. Должен быть указан, или адрес выделенного блока памяти, или NULL, если не требуется получать информацию о состоянии. |
Если с информацией об ошибке все ясно, то со структурой COMMSTAT мы еще не встречались. Вот она:
typedef struct _COMSTAT DWORD fCtsHold:1; DWORD fDsrHold:1; DWORD fRlsdHold:1; DWORD fXoffHold:1; DWORD fXoffSent:1; DWORD fEof:1; DWORD fTxim:1; DWORD fReserved:25; DWORD cbInQue; DWORD cbOutQue; } COMSTAT, *LPCOMSTAT; |
Поля структуры имеют следующее значение:
fCtsHold |
|
Передача приостановлена из-за сброса сигнала CSR. |
|
fDsrHold |
|
Передача приостановлена из-за сброса сигнала DSR. |
|
fRlsdHold |
|
Передача приостановлена из-за ожидания сигнала RLSD (receive-line-signal-detect). Более известное название данного сигнала - DCD (обнаружение несущей). |
|
fXoffHold |
|
Передача приостановлена из-за приема символа XOFF. |
|
fXoffSent |
|
Передача приостановлена из-за передачи символа XOFF. Следующий передаваемый символ обязательно должен быть XON, поэтому передача собственно данных тоже приостанавливается |
|
fEof |
|
Принят символ конца файла (EOF). |
|
fTxim |
|
В очередь, с помощью TransmitCommChar, поставлен символ для экстреной передачи. |
|
fReserved |
|
Зарезервировано и не используется. |
|
cbInQue |
|
Число символов в приемном буфере. Эти символы приняты из линии но еще не считаны функцией ReadFile. |
|
cbOutQue |
|
Число символов в передающем буфере. Эти символы ожидают передачи в линию. Для синхронных операций всегда 0. |
Теперь Вы знаете почти все о работе с последовательными и параллельными портами в синхронном режиме. Особенности непосредственной работы с модемами я не буду рассматривать, так как существует большой набор высокоуровневых функций и протоколов, таких как TAPI, специально предназначеных для работы с модемами. Если Вас все же интересует эта тема, то почитайте описания функции GetCommModemStatus, и структур MODEMDEVCAPS и MODEMSETTINGS. В остальном работа с модемом ничем не отличается от работы с обычным портом.
Синхронный режим обмена довольно редко оказывается подходящим для серьезной работы с внешними устройствами через последовательные порты. Вместо полезной работы Ваша программа будет ждать завершения ввода/вывода, ведь порты работают значительно медленнее процессора. Да и гораздо лучше отдать время процессора другой программе, чем крутиться в цикле ожидая какого-либо события. Другими словами, пришло время знакомиться с асинхронной работой с портами.
Начнем с событий связаных с последовательными портами. Вы указываете системе осуществлять слежение за возникновением связанных с портом событий устанавливая маску с помощью функции
BOOL SetCommMask( HANDLE hFile, DWORD dwEvtMask ); |
Маска отслеживаемых событий задается вторым параметром. Можно указывать любую комбинацию следующих значений:
EV_BREAK |
Состояние разрыва приемной линии |
EV_CTS |
Изменение состояния линии CTS |
EV_DSR |
Изменение состояния линии DSR |
EV_ERR |
Ошибка обрамления, перебега или четности |
EV_RING |
Входящий звонок на модем (сигнал на линии RI порта) |
EV_RLSD |
Изменение состояния линии RLSD (DCD) |
EV_RXCHAR |
Символ принят и помещен в приемный буфер |
EV_RXFLAG |
Принят символ заданый полем EvtChar структуры DCB использованой для настройки режимов работы порта |
EV_TXEMPTY |
Из буфера передачи передан последний символ |
Если dwEvtMask равно нулю, то отслеживание событий запрещается. Разумеется всегда можно получить текущую маску отслеживаемых событий с помощью функции
BOOL GetCommMask( HANDLE hFile, LPDWORD lpEvtMask ); |
Вторым параметром задается адрес переменной принимающей значение текущей установленой маски отслеживаемых событий. В дополнение к событиям, перечисленым в описании функции SetCommMask, данная функция может возвратить следующие:
EV_EVENT1 |
Устройство-зависимое событие |
EV_EVENT2 |
Устройство-зависимое событие |
EV_PERR |
Ошибка принтера |
EV_RX80FULL |
Приемный буфер заполнен на 80 процентов |
Эти дополнительные события используются внутри драйвера. Вы не должны переустанавливать состояние их отслеживания.
Когда маска отслеживаемых событий задана, Вы можете приостановить выполнение своей программы до наступления события. При этом программа не будет занимать процессор. Это выполняется вызовом функции
BOOL WaitCommEvent( HANDLE hFile, LPDWORD lpEvtMask, LPOVERLAPPED lpOverlapped, ); |
Замечу, что в переменной, адресуемой вторым параметром, не будут устанавливаться внутренние события драйвера (перечислены в описании функции GetCommMask). В единичное состояние установятся только те биты, которые соответствуют реально произошедшим событиям.
Адрес структуры OVERLAPPED требуется для асинхронного ожидания (возможно и такое). Однако пока будем полагать, что порт открыт для синхронных операций, следовательно этот параметр должен быть NULL. Замечу только, что при асинхронном ожидании данная функция может завершиться с ошибкой, если в процессе этого ожидания будет вызвана функция SetCommMask для переустановки маски событий. Кроме того, связанное со структурой OVERLAPPED событие (объект создаваемый функцией CreateEvent, а не событие порта) должно быть с ручным сбросом. Вообще, поведение функции с ненулевым указателем на структуру OVERLAPPED аналогично поведению функций чтения и записи. Теперь коротенький пример: |
#include <windows.h>
. . .
DCB dcb; COMMTIMEOUTS ct; HANDLE port; DWORD mask; DWORD bc; char buf[101];
. . .
dcb.DCBlength=sizeof(DCB); BuildCommDCB("baud=9600 parity=N data=8 stop=1",&dcb); dcb.fNull=TRUE;
ct.ReadIntervalTimeout=10; ct.ReadTotalTimeoutMultiplier=ct.ReadTotalTimeoutConstant=0; ct.WriteTotalTimeoutMultiplier=ct.WriteTotalTimeoutConstant=0;
port=CreateFile("COM2",GENERIC_READ,0,NULL,OPEN_EXISTING,0,NULL);
SetCommState(port,dcb); SetCommTimeouts(port,&ct); PurgeComm(port,PURGE_RXCLEAR);
. . .
SetCommMask(port,EV_RXCHAR); WaitCommEvent(port,&mask,NULL); ReadFile(port,buf,100,&bc,NULL);
CloseHandle(port);
. . . |
В данном примере ожидается начало сообщения (первый полученый символ), после чего вызывается функция чтения.
Освобождать процессор на время ожидания хорошо, но хотелось бы параллельно с вводом/выводом делать какую-либо полезную работу. Что бы это стало возможным, необходимо в качестве параметра dwFlagsAndAttributes вместо 0 указать FILE_FLAG_OVERLAPPED. Кроме того, для функций ReadFile, WriteFile и WaitCommEvent необходимо в качестве параметра lpOverlapped указывать адрес правильно инициализированной структуры OVERLAPPED. Вот как выглядит эта структура: |
typedef struct _OVERLAPPED { DWORD Internal; DWORD InternalHigh; DWORD Offset; DWORD OffsetHigh; HANDLE hEvent; } OVERLAPPED, *LPOVERLAPPED; |
Подробно описывать поля этой структуры не буду, поскольку данная статья не о файловом вводе/выводе вообще, а о работе с портами. Для наших целей, за исключением WaitCommEvent, можно просто обнулить все поля этой структуры. Для WaitCommEvent поле hEvent должно содержать корректный описатель объекта "событие". Что бы все стало понятно, надо разобраться с таким обязательным атрибутом параллельной работы как синхронизация.
ВНИМАНИЕ!!! Дескриптор файла, в данном случае дескриптор файла порта, является синхронизирующим объектом ядра (согласно официальной документации Microsoft). Это означает, что его можно использовать в функциях ожидания событий наравне с дескрипторами событий. Таким образом в поле hEvent в структуре OVERLAPPED можно занести NULL и ожидать освобождения дескриптора файла, а не дескриптора события. Это действительно работает в Windows NT. Однако в Windows95/98 все совсем иначе. Обсуждение ошибок, неточностей и прочих проблем документации оставим в стороне. Просто замечу, что в Windows95/98 поле hEvent должно содержать корректный дескриптор объекта event В ЛЮБОМ СЛУЧАЕ!!! Иначе функции асинхронного ввода/вывода будут работать более чем странным образом. Кроме того, мы должны ожидать освобождения именно дескриптора этого события, а не дескриптора файла.
Синхронизация нужна для упорядочения доступа к совместно используемым объектам. Предположим, что две программы одновременно пытаются изменить значение общей переменной. Каков будет результат? Скорее всего неопределенный. Что бы этого избежать требуется разрешать доступ второй программы к переменной только после того, как с ней закончила работать первая программа.
Для синхронизации используются различные методы: семафоры, блокировки, события, критические секции и т.п. События являются простейшими синхронизирующими объектами. Они могут находиться только в двух состояниях: установленом (событие произошло или наступило) и сброшеном (собитие не произошло или не наступило). События создаются функцией CreateEvent и разрушаются функцией CloseHandle. Установить событие можно функцией SetEvent, а сбросить ResetEvent.
Фнкции записи/чтения для файла открытого для асинхронного ввода/вывода будут немедленно возвращать управление с кодом ошибки ERROR_IO_PENDING. Это означает, что асинхронная операция успешно стартовала. Если возвращается другой код ошибки, то операция не стартовала (например из-за ошибки в параметрах). Теперь Вы можете спокойно заниматься другой работой периодически проверяя, завершилась ли операция ввода/вывода. Эта проверка выполняется функцией |
BOOL GetOverlappedResult( HANDLE hFile, LPOVERLAPPED lpOverlapped, LPDWORD lpcbTransfer, BOOL fWait ); |
Параметр hFile определяет дескриптор опрашиваемого файла, lpOverlapped задает адрес структуры OVERLPPED. Третий параметр задает адрес переменной, куда будет помещено количество считанных или записанных байт. Соответсвующий параметр функций ReadFile и WriteFile, хоть и ДОЛЖЕН БЫТЬ ЗАДАН НЕ НУЛЕВЫМ, не позволяет получить количество переданных байт, так как на момент возврата управления из функций чтения/записи не передано ни одного байта. Параметр fWait означает, должна ли функция GetOverlappedResult ждать завершения операции ввода/вывода. Если этот параметр равет FALSE, то функция немедленно вернет управление. При этом код возврата будет TRUE, если операция завершена, или FALSE, если операция не завершена. В послед случае код ошибки возвращаемой функцией GetLastError будет ERROR_IO_INCOMPLETE. Если функция GetOverlappedResult завершилась с кодом возврата FALSE, и другим кодом ошибки, то ошибка произошла именно при вызове самой функции. Если параметр fWait равен TRUE, то функция будет дожидаться завершения операции ввода-вывода.
Замечу, что ожидать завершения ввода/вывода с помощью функции GetOverlappedResult не самое правильное решение. При работе с дисковым файлом операция завершится гарантированно, а при работе с последовательным или параллельным портом совсем не обязательно. Представьте, что Вы не настроили тайм-ауты последовательного порта, а подключенное устройство неисправно. GetOverlappedResult будет ждать вечно, так как нет способа указать максимальное время ожидания. Ждать завершения ввода/вывода лучше с помощью функций: |
DWORD WaitForSingleObject( HANDLE hObject, DWORD dwTimeot );
DWORD WaitForMultipleObjects( DWORD cObjects, LPHANDLE lpHandles, BOOL bWaitAll, DWORD dwTimeout ); |
Как следует из названия, эти функции предназначены для ожидания одного или нескольких объектов. Однако следует вспомнить примечание, которое я привел к описанию структуры OVERLAPPED! Поэтому не мудрствуя лукаво будем ожидать только объекты event.
Функция WaitForSingleObject ожидает только один объект задаваемый первым параметром. Вторым параметром задается максимальное время ожидания наступления события в миллисекундах. Если вместо времени указана магическая величина INFINITE, то событие будет ожидаться вечно.
Функция WaitForMultipleObjects ждет несколько событий. Первый параметр указывает сколько именно, а второй задает массив дескрипторов этих событий. Замечу, что один и тот же дескриптор нельзя указывать в этом массиве более одного раза. Третий параметр задает тип ожидания. Если он равен TRUE, то ожидается наступление всех событий. Если FALSE, то наступления любого одного из указанных. И естественно тоже можно задать максимальное время ожидания последним параметром.
Если событие наступило, то функции возвращают значения от WAIT_OBJECT_0 до WAIT_OBJECT_0+cObject-1. Естественно, что WaitForSingleObject может вернуть только WAIT_OBJECT_0 (если конечно не произошло ошибки). Если произошла ошибка, то будет возвращено WAIT_FAILED. При превышении максимального времени ожидания функции вернут WAIT_TIMEOUT.
Вернусь к объектам event, которые мы собственно и используем для ожидания. Поясню, почему для наших целей требуются события с ручным сбросом. Функции ReadFile и WriteFile в асинхронном режиме первым делом сбрасывают (переводят в занятое состояние) как дескриптор файла, так и дескриптор объекта event задананный в структуре OVERLAPPED. Когда операция чтения или записи завершается система устанавливает эти дескрипторы в свободное состояние. Тут все логично. Однако и функции WaitForSingleObject и WaitForMultipleObjects для событий с автоматическим сбросом так же выполняют их перевод в занятое состояние при вызове. Для событий с ручным сбросом этого не происходит. Теперь представьте, что операция ввода/вывода завершилась ДО вызова WaitForSingleObject. Представили? Для событий с автоматическим сбросом снова будет выполнен перевод объекта в занятое состояние. Но освобождать то его будет некому! Более подробная информация об объектах event выходит за рамки этой статьи.
Теперь небольшой пример. Все подробности, не относящиеся к работе в асинхронном режиме я опускаю. |
#include <windows.h> #include <string.h>
. . .
HANDLE port; char* buf; OVERLAPPED ovr; DWORD bc;
. . .
port=CreateFile("COM2",GENERIC_READ,0,NULL,OPEN_EXISTING, FILE_FLAG_OVERLAPPED,NULL);
memset(&ovr,0,sizeof(ovr)); ovr.hEvent=CreateEvent(NULL,FALSE,FALSE,NULL); ReadFile(port,buf,buf_size,&bc,&ovr);
/* Выполняем некую полезную работу */
if(WaitForSingleObject(ovr.hEvent,10000)==WAIT_OBJECT_0) { GetOverlappedResult(port,&ovr,&bc,FALSE); } else {
/* Обработка ошибки */
}
CloseHandle(port); CloseHandle(ovr.hEvent); |
В этом примере переменная bc, предназначенная для получения количества считанных байт, после вызова ReadFile будет равна 0, так как никакой передачи информации еще не было. После вызова GetOverlappedResult в эту переменную будет помещено число реально считанных байт.
Безусловно, можно придумать очень сложные схемы распараллеливания ввода/вывода и вычислений, базирующиеся на использовании асинхронных операций и объектов event. Позволю себе не приводить реально работающих примеров программ. Таких программ работающих в реальном масштабе времени много, но они очень сложны и громоздки для этой статьи.
Вернемся ненадолго с структуре OVERLAPPED и функциям ReadFile и WriteFile. Для дискового ввода/вывода возможно задать одновременно несколько конкурирующих операций чтения/записи. Однако для каждой такой операции необходимо использовать свою структуру OVERLAPPED. Для работы с портами нельзя задавать конкурирующие операции. Точнее можно, но только в Windows NT. Поэтому для целей совместимости лучше этого не делать.
Теперь, уже совсем кратко, еще об одной возможности, реализованной только в Windows NT. Речь идет о "тревожном вводе-выводе". Эта возможность реализуется функциями ReadFileEx, WriteFileEx и SleepEx. Суть использования данных функий такова. Вы вызываете расширенную функцию записи или чтения, которая имеет еще один параметр - адрес функции завершения. После чего, вызвав расширенную функцию засыпания, освобождаете процессор. После завершения ввода/вывода Ваша функция завершения будет вызвана системой. Причем вызвана ТОЛЬКО в том случае, если ваша программа вызвала SleepEx. Нетрудно заметить, что данный вариант работы подходит для систем с большим количеством портов и работающих в режиме ответа по требованию. Например, сервер с мультипортовым контроллером последовательного порта, к которому подключены модемы.
Теперь, но ОЧЕНЬ кратко, залезем в еще большие дебри. Предположим, что протокол обмена с Вашим устройством, подключенным к последовательному порту, очень сложен (передаются большие и сложные структуры данных). При этом Ваша программа должна получать уже полностью принятую и проверенную информацию. Предположим так же, что Ваша программа занимается очень большими и сложными вычислениями и ей нет времени отвлекаться на обработку ввода/вывода. Да и сложность ее такова, что встраивание фонового ввода/вывода сделает ее трудно прослеживаемой и неустойчивой. Чувствуете, куда я клоню? Правильно, к выделению всех тонкостей ввода/вывода в отдельный поток. Возможно выделение и в отдельную задачу, но в этом случае мы не получим никакой выгоды, а накладные расходы на переключение задач гораздо больше, нежели на переключение потоков в одной задаче.
Потоки создаются функцией CreateThread, и уничтожаются функциями ExitThread и TerminateThread. Принцип работы таков. Вы создаете поток. При этом управление получает Ваша функция потока. Она работает параллельно, как минимум, основному потоку Вашей программы. Функция открывает порт и выполняет все необходимые настройки. Затем она выполняет весь ввод/вывод, при чем совершенно не важно, используется синхронный или асинхронный режим. При засыпании потока (при синхронном режиме) остальные потоки Вашей программы продолжат выплняться. Когда завершится необходимый обмен информацией с устройством и данные будут готовы для передачи основной программе Ваш поток установит некий флаг, котрый будет воспринят основной программой как готовность данных. После их обработки и формирования блока выходной информации основной поток установит другой флаг, который будет воспринят потоком ввода-вывода как готовность данных для передачи. При этом в качестве флагов можно использовать как объекты event, так и обычные переменные (ведь все потоки задачи выполняются в едином адресном прогстранстве). В случае использования обычных глобальных переменных не забудте в их определения добавить модификатор volatile. Он обозначает, что переменная может измениться асинхронно и компилятор не должен строить иллюзий насчет возможности ее оптимизации. В противном случае у Вас ничего не получится. Так как в потоке ввода/вывода, выполняющемся параллельно основному потоку программы, можно использовать асинхронный ввод/вывод, то достаточно просто реализуется возможность обработки большого количества портов. Фактически поток ввода/вывода будет работь еще и параллельно самому себе. При запуске такой задачи на многопроцессорной машине выгода от использования многопоточности будет очевидна, поскольку потоки будут выполняться на разных процессорах.
На этом, пожалуй, следует остановиться. Асинхронные режимы и многозадачность темы отдельных больших статей. Эти статьи будут написаны и выложены на сервер. Информации этой статьи достаточно, что бы Вы смогли уверенно начать работать с портами. Безусловно, не обойтись без чтения подробных описаний на упомянутые здесь функции и структуры. Без детальнейшей проработки, иногда очень изощренных, алгоритмов. Я постарался дать общую картину проблемы и путей ее решения. Насколько это удалось, судить Вам. |
|
|
|
|
|
|
|
|
|
|
|
|
| |
|
| |
|
| |
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
| |
|
| |
|
|
|
|
|
|
|
| |
|
| |
|
| |
|
| |
|
|
|
| |
|
| |
|
| | | | | |
|
|
| | |
|
|
|