ПРОГРАММИРОВАНИЕ

Работа с коммуникационными портами (СОМ и LPT) в программах для Win32



Мне часто задают вопросы о работе с СОМ портами из программ, написанных для Windows 95/98/NT. Причем чаще всего спрашивают разработчики всевозможных управляющих устройств. Эти устройства либо были разработаны давно, еще в эпоху MS-DOS, либо разрабатываются сейчас. Но объединяет их одно — устройство должно подключаться к компьютеру, в большинстве случаев через RS-232 (СОМ), реже, через Centronics (LPT).

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

Написать программу, управляющую устройством через СОМ порт, для MS-DOS не так сложно. Это частенько делали не программисты, а сами разработчики устройства. Сложнее было сделать красивый и удобный интерфейс пользователя. Этим обычно занимались профессиональные программисты. С платформой Win32 дело обстоит сложнее. Но только на первый взгляд. Конечно, напрямую работать с регистрами портов нельзя, Windows это не позволяет, зато можно не обращать внимания на тонкости различных реализаций (i8055, 16450, 16550А) и не возиться с обработкой прерываний.

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


Функция CreateFile

Как я уже говорил, с последовательными и параллельными портами в Win32 работают как с файлами. Следовательно, начинать надо с открытия порта как файла. Использовать привычные функции open и fopen при этом нельзя, необходимо воспользоваться функцией CreateFile. Эта функция предоставляется Win32 API. Ее прототип выглядит так:

HANDLE CreateFile (

LPCTSTR lpFileName,

DWORD dwDesiredAccess,

DWORD dwShareMode,

LPSECURITY_ATTRIBUTES IpSecurityAttributes,

DWORD dwCreationDistribution,

DWORD dwFlagsAndAttributes,

HANDLE hTemplateFile

);

Функция имеет много параметров, большинство из которых нам не нужны. Приведу краткое описание параметров:


∙ IpFileName

Указатель на строку с именем открываемого или создаваемого файла. Формат этой строки может быть очень хитрым. В частности можно указывать сетевые имена для доступа к файлам на других компьютерах. Можно открывать логические разделы или физические диски и работать в обход файловой системы. Однако для наших задач это не нужно. Последовательные порты имеют имена "СОМ1", "COM2", "COM3", "COM4" и так далее. Точно так же они назывались в MS-DOS, так что ничего нового тут нет. Параллельные порты называются "LPT1", "LPT2" и так далее. Учтите, что если у Вас к порту СОМ1 подключена мышь, Windows не даст открыть этот порт. Аналогично не удастся открыть LPT1 если подключен принтер. А вот с модемом дела обстоят немного по другому. Если какая-либо программа использует модем, например вы дозвонились до своего провайдера Internet, то Вашей программе не удастся открыть порт к которому подключен модем. Во всех остальных случаях порт будет открыт и Вы сможете работать с модемом сами, из своей программы.


∙ dwDesiredAccess

Задает тип доступа к файлу. Возможно использование следующих значений:

• 0 — Опрос атрибутов устройства без получения доступа к нему.

• GENERICREAD — Файл будет считываться.

• GENERICWRITE — Файл будет записываться.

• GENERIC_READ|GENERIC_WRITE — Файл будет и считываться и записываться.


∙ dwShareMode

Задает параметры совместного доступа к файлу. Коммуникационные порты нельзя делать разделяемыми, поэтому данный параметр должен быть равен 0.


∙ IpSecurityAttributes

Задает атрибуты защиты файла. Поддерживается только в Windows NT. Однако при работе с портами должен в любом случае равняться NULL.


∙ dwCreationDistribution

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


∙ dwFlagsAndAttributes

Задает атрибуты создаваемого файла. Так же управляет различными режимами обработки. Для наших целей этот параметр должен быть или равным 0, или FILE_FLAG_OVERLAPPED. Нулевое значение используется при синхронной работе с портом, a FILE_FLAG_OVERLAPPED при асинхронной, или другими словами, при фоновой обработке ввода/вывода. Подробнее про асинхронный ввод/вывод я расскажу позже.


∙ hTemplateFile

Задает описатель файла-шаблона. При работе с портами всегда должен быть равен NULL.

При успешном открытии файла, в нашем случае порта, функция возвращает описатель (HANDLE) файла. При ошибке INVALID HANDLE VALUE. Код ошибки можно получить, вызвав функцию GetLastError, но ее описание выходит за рамки данной статьи.


Функция CloseHandle

Открытый порт должен быть закрыт перед завершением работы программы. В Win32 закрытие объекта по его описателю выполняет функция CloseHandle:

BOOL CloseHandle(

HANDLE hObject

);

Функция имеет единственный параметр — описатель закрываемого объекта. При успешном завершении функция возвращает не нулевое значение, при ошибке нуль.

Теперь пример (достаточно очевидный):

#include

HANDLE port;

port=CreateFile"COM2",GENERIC_READ|GENERIC_WRIТЕ,0,NULL,OPEN_EXISTING,0,NULL);

}:

if(port==INVALID_HANDLE_VALUE) {{

MsgBox(NULL,"Невозможно открыть последовательный порт","Error",MB OK);

ExitProcess(1);

}

CloseHandle(port);

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

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

Основные параметры последовательного порта описываются структурой DCB. Временные параметры структурой COMMTIMEOUTS. Существует еще несколько информационных и управляющих структур, но они используются реже. Настройка порта заключается в заполнении управляющих структур и последующем вызове функций настройки.


Структура DCB

Поскольку основную информацию содержит структура 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 flnX: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;

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


∙ DCBIength

Задает длину, в байтах, структуры DCB. Используется для контроля корректности структуры при передаче ее адреса в функции настройки порта.


∙ Baud Rate

Скорость передачи данных. Возможно указание следующих констант: 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.


∙ flnX

Задает использование XON/XOFF управления потоком при приеме. Если это поле равно TRUE, то драйвер передает символ XoffChar, когда в приемном буфере находится более XoffLim, и XonChar, когда в приемном буфере остается менее XonLim символов.


∙ fErrorChar

Указывает на необходимость замены символов с ошибкой четности на символ, задаваемый полем ErrorChar. Если это поле равно TRUE, и поле fParity равно TRUE, то выполняется замена.


∙ fNull

Определяет действии, е выполняемое при приеме нулевого байта. Если это поле TRUE, то нулевые байты отбрасываются при передаче.


∙ fRtsControl

Задает режим управления потоком для сигнала RTS. Если это поле равно 0, то по умолчанию подразумевается RTSCONTROLHANDSHAKE. Поле может принимать одно из следующих значений:

• RTS_CONTROL_DISABLE — Запрещает использование линии RTS

• RTS_CONTROL_ENABLE — Разрешает использование линии RTS

• RTS_CONTROL_HANDSHAKE — Разрешает использование RTS рукопожатия. Драйвер устанавливает сигнал RTS когда приемный буфер заполнен менее, чем на половину, и сбрасывает, когда буфер заполняется более чем на три четверти.

• RTS_CONTROL_TOGGLE — Задает, что сигнал RTS установлен, когда есть данные для пе редачи. Когда все символы из передающего буфера переданы, сигнал сбрасывается.


∙ fAbortOn Error

Задает игнорирование всех операций чтения/записи при возникновении ошибки. Если это поле равно 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 бит. Комбинация из пяти битного байта и двух стоповых бит является недопустимой. Так же как и комбинация из шести, семи или восьми битного байта и полутора стоповых бит.


Функция BuildCommDCB

Только что рассмотренная нами структура DCB самая большая из всех, использующихся для настройки последовательных портов. Но она и самая важная. Заполнение всех полей этой структуры может вызвать затруднения, так как надо очень четко представлять, как работает последовательный порт. Поэтому ручную установку полей можно порекомендовать опытным программистам. Если же Вы чувствуете себя не очень уверено, воспользуйтесь функцией BuildCommDCB, которая позволяет заполнить поля структуры DCB на основе строки, по синтаксису аналогичной строке команды mode. Вот как выглядит прототип этой функции:

BOOL BuildCommDCB(LPCTSTR lpDef, LPDCB lpDCB);

Как видно, функция очень проста и имеет всего два параметра:

∙ lpDef

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

baud=1200 parity=N data=8 stop=l


∙ lpDCB

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

В случае успешного завершения функция BuildCommDCB возвращает не нулевое значение. В случае ошибки возвращается 0.

Обычно функция BuldCommDCB изменяет только явно перечисленные в строке lpDef поля. Однако существуют два исключения из этого правила:

• При задании скорости обмена 110 бит в секунду автоматически устанавливается формат обмена с двумя стоповыми битами. Это сделано для совместимости с командой mode из MS-DOS или Windows NT.

• По умолчанию запрещается программное (XON/XOFF) и аппаратное управление потоком. Вы должны вручную заполнить требуемые поля DCB если требуется управление потоком.

Функция BuilCommDCB поддерживает как новый, так и старый форматы командной строки mode. Однако, Вы не можете смешивать эти форматы в одной строке.

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

— Для строк вида 9600,n,8,1 (не заканчивающихся символами х или р):

• flnX, fOutX,fOutXDsrFlow, fOutXCtsFlow устанавливаются в FALSE

• fDtrControl устанавливается в DTR_CONTROL_ENABLE

• fRtsControl устанавливается в RTS_CONTROL_ENABLE

— Для строк вида 9600,n,8,1,х (заканчивающихся символом х):

• flnX, fOutX устанавливаются в TRUE

• fOutXDsrFlow,fOutXCtsFlow устанавливаются в FALSE

• fDtrControl устанавливается в DTR_CONTROL_ENABLE

• fRtsControl устанавливается в RTS_CONTROL_ENABLE

— Для строк вида 9600,n,8,1,р (заканчивающихся символом р):

• flnX, fOutX устанавливаются в FALSE

• fOutXDsrFlow,fOutXCtsFlow устанавливаются TRUE

• fDtrControl устанавливается в DTR_CONTROL_HANDSHAKE

• fRtsControl устанавливается в RTS_CONTROL_HANDSHAKE

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


Функция GetCommState

Заполнить DCB можно еще одним способом. Вызовом функции GetCommState. Эта функция заполняет DCB информацией о текущем состоянии устройства, точнее о его настройках. Вот как она выглядит:

BOOL GetCommState(

HANDLE hFile,

LPDCB lpDCB

);

Функция очень проста и имеет всего два параметра:


∙ hFile

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


∙ lpDCB

Указатель на DCB. Для DCB должен быть выделен блок памяти.

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


Функция SetCommState

Заполнив DCB можно приступать к собственно конфигурированию порта. Это делается с помощью функции SetCommState:

BOOL SetCommState(

HANDLE hFile,

LPDCB lpDCB

);

Эта функция имеет точно такие же параметры, как GetCommState. Различается только направление передачи информации. GetCommState считывает информацию из внутренних управляющих структур и регистров порта, а SetCommState наоборот заносит ее. Следует быть осторожным при вызове функции SetCommState, поскольку она изменит параметры даже в том случае, если очереди приема/передачи не пусты, что может вызвать искажение потока передаваемых или принимаемых данных.

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

Как всегда, в случае успешного завершения возвращается отличное от нуля значение, а в случае ошибки — нуль.


Структура COMMTIMEOUTS

Следующей важной управляющей структурой является COMMTIMEOUTS. Она определяет параметры временных задержек при приеме и передаче. Значения, задаваемые полями этой структуры, оказывают большое влияние на работу функций чтения/записи.

typedef struct _COMMTIMEOUTS {{

DWORD ReadlntervalTimeout;

DWORD ReadTotalTimeoutMultiplier;

DWORD ReadTotalTimeoutConstant;

DWORD WriteTotalTimeoutMultiplier;

DWORD WriteTotalTimeoutConstant;

} COMMTIMEOUTS,*LPCOMMTIMEOUTS;

Поля структуры COMMTIMEOUTS имеют следующие значения:


∙ ReadlntervalTimeout

Максимальное время, в миллисекундах, допустимое между двумя последовательными символами, считываемыми с коммуникационной линии. Во время операции чтения временной период начинает отсчитываться с момента приема первого символа. Если интервал между двумя последовательными символами превысит заданное значение, операция чтения завершается и все данные, накопленные в буфере, передаются в программу. Нулевое значение данного поля означает, что данный тайм-аут не используется. Значение MAXDWORD, вместе с нулевыми значениями полей ReadTotalTimeoutConstant и ReadTotalTimeoutMultiplier, означает немедленный возврат из операции чтения с передачей уже принятого символа, даже если ни одного символа не было получено из линии.


∙ ReadTotalTimeoutMultiplier

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


∙ ReadTotalTimeoutConstant

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


∙ WriteTotalTimeoutMuItiplier

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


∙ WriteTotalTimeoutConstant

Задает константу, в миллисекундах, используемую для вычисления общего тайм-аута операции записи. Для каждой операции записи данное значение прибавляется к результату умножения WriteTotalTimeoutMultiplier на количество записываемых символов. Нулевое значение полей WriteTotalTimeoutMultiplier и WriteTotalTimeoutConstant означает, что общий тайм-аут для операции записи не используется.

По тайм-аутам обычно возникает много вопросов. Поэтому попробую объяснить подробнее. Пусть мы считываем 50 символов из порта со скоростью 9600. При этом используется 8 бит на символ, дополнение до четности и один стоповый бит. Таким образом на один символ в физической линии приходится 11 бит (включая стартовый бит). 50 символов на скорости 9600 будут приниматься 50 * 11 / 9600 = 0.0572916 секунд, или примерно 57.3 миллисекунды, при условии ну левого интервала между приемом последовательных символов. Если интервал между символами составляет примерно половину времени передачи одного символа, т. е. 0.5 миллисекунд, то время приема будет 50 * 11 / 9600 + 49 * 0.0005 = 0.0817916 секунд, или примерно 82 миллисекунды. Если в процессе чтения прошло более 82 миллисекунд, то мы вправе предположить, что произошла ошибка в работе внешнего устройства и прекратить считывание, избежав тем самым зависания программы. Это и есть общий тайм-аут операции чтения. Аналогично существует и общий там-аут операции записи.

Если тайм-аут при чтении понятен, то тайм-аут при записи вызывает недоумение. В самом деле, что нам мешает передавать? Управление потоком! Внешнее устройство может использовать, например, аппаратное управление потоком. При этом пропадание питания во внешнем устройстве заставит компьютер приостановить передачу данных. Если не контролировать тайм-аут возможно точно такое же зависание компьютера, как и при операции чтения.

Общий тайм-аут зависит от количества участвующих в операции чтения/записи символов и среднего времени передачи одного символа с учетом межсимвольного интервала. Если символов много, например 1000, то на общем времени выполнения операции начинают сказываться колебания времени затрачиваемого на один символ или времени межсимвольного интервала. Поэтому тайм-ауты в структуре COMMTIMEOUTS задаются двумя величинами. Таким образом формула для вычисления общего тайм-аута операции, например чтения, выглядит так:

NumOfChar * ReadTotalTimeoutMultiplier + ReadTotalTimeoutConstant,

где NumOfChar это число символов запрошенных для операции чтения.

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

Теперь небольшой пример. ReadTotalTimeoutMultiplier = 2, ReadTotalTimeoutConstant = 1, ReadlntervalTimeout = 1,

считывается 250 символов. Если операция чтения завершится за 250 * 2 + 1 = 501 миллисекунду, то будет считано все сообщение. Если операция чтения не завершится за 501 миллисекунду, то она все равно будет завершена. При этом будут возвращены символы, прием которых завершился до истечения тайм-аута операции. Остальные символы могут быть получены следующей операцией чтения. Если между началами двух последовательных символов пройдет более 1 миллисекунды, то операция чтения так же будет завершена.

Надеюсь, что теперь тайм-ауты не будут вызывать у Вас затруднений. Для завершения темы тайм-аутов рассмотрим один частный случай. Если поля ReadlntervalTimeout и ReadTotalTimeoutMultiplier установлены в MAXDWORD, а ReadTotalTimeoutConstant больше нуля и меньше MAXDWORD, то выполнение операции чтения подчиняется следующим правилам:

• Если в буфере есть символы, то чтение немедленно завершается и возвращается символ из буфера;

• Если в буфере нет символов, то операция чтения будет ожидать появления любого символа, после чего она немедленно завершится;

• Если в течении времени, заданного полем ReadTotalTimeoutConstant, не будет принято ни одного символа, операция чтения завершится по тайм-ауту.

• Помните функцию BuildCommDCB? Существует еще функция BuildCommDCBAndTimeouts, которая позволяет заполнить не только структуру DCB, но и структуру COMMTIMEOUTS.


Функция BuildCommDCBAndTimeouts

Функция BuildCommDCBAndTimeouts позволяет заполнить не только структуру DCB, но и структуру COMMTIMEOUTS. Вот как выглядит ее прототип:

BOOL BuildCommDCBAndTimeouts(

LPCTSTR lpDef,

LPDCB lpDCB,

LPCOMMTIMEOUTS lpCommTimeouts

);

Как видно, у этой функции, по сравнению с BuildCommDCB, появился третий параметр. Это указатель на структуру COMMTIMEOUTS. Конфигурационная строка при вызове этой функции должна задаваться в новом формате. Если эта строка содержит подстроку "TO=ON", то устанавливаются общие тайм-ауты для операций чтения и записи. Это значения устанавливаются на основе информации о скорости передачи и формате посылки. Если конфигурационная строка содержит подстроку "TO=OFF", то устанавливается режим работы без тайм-аутов. Если конфигурационная строка не содержит подстроки "ТО=ххх" или эта подстрока имеет неверное значение, то указатель на структуру COMMTIMEOUTS просто игнорируется. При этом функция BuilCommDCBAndTimeouts оказывается идентичной функции BuildCommDCB.

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


Функция GetCommTimeouts

Как и для заполнения структуры DCB, для COMMTIMEOUTS существует функция считывания установленных в системе значений. Это функция GetCommTimeouts:

BOOL GetCommTimeouts(

HANDLE hFile,

LPCOMMTIMEOUTS lpCommTimeouts

);

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


Функция SetCommTimeouts

Заполнив структуру COMMTIMEOUTS можно вызывать функцию установки тайм-аутов порта. Это функция называется SetCommTimeouts:

BOOL SetCommTimeouts(

HANDLE hFile,

LPCOMMTIMEOUTS lpCommTimeouts

);

Параметры этой функции тоже достаточно очевидны. Установку тайм-аутов можно производить как до установки параметров порта, так и после. Последовательность вызова функций SetCommState и SetCommTimeouts не имеет никакого значения. Главное, что бы все настройки были завершены до начала ввода/вывода информации.

Теперь приведу пример настройки порта:

#include

DCB * dсb;

COMMTIMEOUTS ct;

HANDLE port;

dcb=(DCB*)HeapAlloc(GetProcessHeap(),HEAP_Z ERO_MEMORY,sizeof(DCB));

dcb->DCBlength=sizeof(DCB);

BuildCommDCB("baud=9600 parity=N data=8 stop=1",dсb);

dcb->fNull=TRUE;

ct.ReadIntervalTimeout=10;

ct.ReadTotalTimeoutMultiplier=ct.ReadTotalTimeoutConstant=0;

ct.WriteTotalTimeoutMultiplier=ct.WriteTotalTimeoutConstant=0;


port=CreateFile(COM2",GENERIC_READ|GENERIC_WRIТЕ,0,NULL,OPEN_EXISTING,0,NULL);

SetCommState(port,dсb);

SetCommTimeouts(port,&ct);

HeapFree(GetProcessHeap 0,0,dсb);

CloseHandle(port);

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

Первым делом, с помощью функции HeapAlloc, выделяется и заполняется нулями область памяти для DCB. Затем в поле DCBlength заносится размер структуры DCB в байтах. Зачем это нужно обсуждалось выше, при описании данного поля. Для общего (и наглядного) заполнения DCB использована функция BuildCommDCB. Будем считать, что нас устраивает информация занесенная в DCB, но требуется игнорировать нулевые байты при приеме. Так как BuildCommDCB не выполняет требуемых действий мы вручную изменяем соответствующее поле. Далее мы заполняем информацию о таймаутах. Общие тайм-ауты операций чтения и записи не используются, конец сообщения определяется по тайм-ауту между двумя последовательными символами большему 10 миллисекунд. Теперь можно открыть порт, что делается функцией CreateFile, и выполнить его настройку вызвав функции SetCommState и SetCommTimeots. После установки параметров порта структура DCB становится не нужной, поэтому можно освободить занимаемую ей память. Структура COMMTIMEOUTS в примере размещена статически, поэтому выделять под нее память и освобождать ее не требуется. Наконец, мы закрываем порт перед завершением.

Функции HeapAlloc и HeapFree занимаются выделением и освобождением памяти из куч, которых в программе может быть несколько. Вместо этих функций можно использовать malloc (calloc) и free. Однако использование функций предоставляемых Win32 API позволяет сократить размер программы, что может быть не маловажно, если работа с портами ведется из DLL (например Вы пишете своеобразный псевдодрайвер для своего устройства). Есть и другие аргументы в пользу этой точки зрения, которую я Вам, впрочем, не навязываю.

Рассмотренные структуры и функции позволяют программировать порт на достаточно низком уровне. Их, в большинстве случаев, более чем достаточно даже для тонкой настройки порта. Однако бывают и исключения. Например, под именем СОМ1 может скрываться вовсе не привычный порт RS-232, а какая-нибудь экзотика. Или порт может не позволять задавать скорость более 9600.


Структура COMMPROP

Исчерпывающая информация о возможностях коммуникационного устройства и драйвера содержится в структуре COMMPROP:

typedef struct _COMMPROP {{

WORD wPacketLength; // packet size, in bytes

WORD wPacketVersion; // packet version

DWORD dwServiceMask; // services implemented

DWORD dwReservedl; // reserved

DWORD dwMaxTxQueue; // max Tx bufsize, in bytes

DWORD dwMaxRxQueue; // max Rx bufsize, in bytes

DWORD dwMaxBaud; // max baud rate, in bps

DWORD dwProvSubType; // specific provider type

DWORD dwProvCapabilities; // capabilities supported

DWORD dwSettableParams; // changable parameters

DWORD dwSettableBaud; // allowable baud rates

WORD wSettableData; // allowable byte sizes

WORD wSettablestopParity; // stop bits/parity allowed

DWORD dwCurrentTxQueue; // Tx buffer size, in bytes

DWORD dwCurrentRxQueue; // Rx buffer size, in bytes

DWORD dwProvSpec1; // provider-specific data

DWORD dwProvSpec2; // provider-specific data

WCHAR wcProvChar[1]; // provider-specific data

} COMMPROP;

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

∙ wPacketLength

Задает размер, в байтах, структуры COMMPROP.


∙ wPacketVersion

Номер версии структуры.


∙ dwServiceMask

Битовая маска. Для коммуникационных устройств всегда SPSERIALCOMM, включая модемы.


∙ dwReserved1

Зарезервировано и не используется.


∙ dwMaxTxQueue

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


∙ dwMaxRxQueue

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


∙ dwMaxBaud

Максимально допустимая скорость обмена, в битах в секунду (бпс). Возможны следующие значения данного поля:

• BAUD_075 — 75 бпс

• BAUD_110 — 110 бпс

• BAUD_134_5 — 134.5 бпс

• BAUD_150 — 150 бпс

• BAUD_300 — 300 бпс

• BAUD_600 — 600 бпс

• BAUD_1200 — 1200 бпс

• BAUD_1800 — 1800 бпс

• BAUD_2400 — 2400 бпс

• BAUD_4800 — 4800 бпс

• BAUD_7200 — 7200 бпс

• BAUD_9600 — 9600 бпс

• BAUD_14400 — 14400 бпс

• BAUD_19200 — 19200 бпс

• BAUD_38400 — 38400 бпс

• BAUD_56K — 56К бпс

• BAUD_57600 — 57600 бпс

• BAUD_115200 — 115200 бпс

• BAUD_128K — 128К бпс

• BAUD_USER — Допускается программирование скорости обмена


∙ dwProvSubType

Тип коммуникационного порта. Возможны следующие значения данного поля:

• PST_FAX — Факс

• PST_LAT LAT — протокол

• PST_MODEM — Модем

• PST_NETWORK_BRIDGE — Сетевой мост

• PST_PARALLELPORT — Параллельный порт

• PST_RS232 — Последовательный порт RS-232

• PST_RS422 — Порт RS-422

• PST_RS423 — Порт RS-423

• PST_RS449 — Порт RS-449

• PST_SCANNER — Сканнер

• PST_TCPIP_TELNET — Протокол TCP/IP TelnetR

• PST_UNSPECIFIED — Неизвестное устройство

• PST_Х25 — Устройство стандарта X.25


∙ dwProvCapabilities

Битовая маска. Определяет возможности предоставляемые устройством. Возможны следующие значения:

• PCF_16BITMODE — Поддерживается специальный 16-битный режим.

• PCF_DTRDSR — Поддерживаются сигналы DTR/DSR.

• PCF_INTTIMEOUTS — Поддерживается межсимвольный тайм-аут.

• PCF_PARITY_CHECK — Поддерживается контроль четности.

• PCF_RLSD — Поддерживается определение наличия сигнала в приемной линии.

• PCF_RTSCTS — Поддерживаются сигналы RTS/CTS.

• PCF_SETXCHAR — Поддерживаются задаваемые символы XON/XOFF.

• PCF_SPECIALCHARS — Поддерживаются спецсимволы.

• PCF_TOTALTIMEOUTS — Поддерживаются общие тайм-ауты (ожидаемое время).

• PCF_XONXOFF — Поддерживается программное (XON/XOFF) управление потоком.

• PCF_XONXOFF — Поддерживается программное (XON/XOFF) управление потоком.


∙ dwSettableParams

Битовая маска. Определяет допустимые для изменения параметры. Возможны следующие значения:

• SP_BAUD — Скорость обмена.

• SP_DATABITS — Бит в символе.

• SP_HANDSHAKING — Рукопожатие (управление потоком).

• SP_PARITY — Четность.

• SP_PARITY_CHECK — Контроль четности.

• SP_RLSD — Детектирование наличия сигнала в приемной линии.

• SP_STOPBITS — Количество стоповых бит.


∙ dwSettableBaud

Битовая маска. Определяет допустимый набор скоростей обмена. Допустимые для данного поля значения указаны в описании поля dwMaxBaud.


∙ wSettableData

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

• DATABITS_5 — 5 бит

• DATABITS_6 — 6 бит

• DATABITS_7 — 7 бит

• DATABITS_8 — 8 бит

• DATABITS_16 — 16 бит

• DATABITS_16Х — Специальный широкий канал через аппаратную последовательную линию.


∙ wSettableStop Parity

Битовая маска. Определяет допустимое количество стоповых бит и режимы четности. Возможны следующие значения:

• STOPBITS_10 — Один стоповый бит

• STOPBITS_15 — Полтора стоповых бита

• STOPBITS_20 — Два стоповых бита

• PARITY_NONE — Без четности

• PARITY_ODD — Дополнение до нечетности

• PARITY_EVEN — Дополнение до четности

• PARITY_MARK — Бит четности всегда "1"

• PARITY_SPACE — Бит четности всегда "0"


∙ dwCurrentTxQueue

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


∙ dwCurrentRxQueue

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


∙ dwProvSped

Устройство-зависимые данные. Программа должна игнорировать содержимое данного поля, за исключением случаев, когда Вы точно знаете формат этих данных. Занесите в данное поле значение COMMPROP_INITIALIZED, если поле wPacketLength уже содержит правильное значение.


∙ dwProvSpec2

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


∙ wcProvChar

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


Информация хранящаяся в структуре COMMPROP требуется редко, так как чаще всего точно известно с каким типом портов будет работать программа.

Остановлюсь чуть подробнее на описании некоторых полей. Поле wPacketLength играет несколько иную роль, чем поле DCBlength структуры DCB, хотя из его описания это не следует. Секрет прост. Поле wcProvChar, расположенное в конце структуры, может содержать, а может и не содержать, данных. Вы не в состоянии это узнать не запросив информацию. В свою очередь, перед запросом информации Вы должны выделить (и обнулить) память под структуру COMMPROP. Поэтому последовательность шагов для получения всей информации следующая:

• Выделить память под структуру COMMPROP.

• Запросить информацию у системы вызвав функцию GetCommProperties.

• Если поле wPacketLength содержит значение большее sizeof(COMMPROP), то имеется дополнительная информация. Для ее получения измените размер ранее выделенного блока памяти, новый размер должен быть равен значению занесенному системой в поле wPacketLength. Установите в поле wProvSpecl значение COMMPROP INITIALIZED, это будет означать, что выделен достаточный блок памяти для получения дополнительной информации. Повторно вызовите функцию GetCommProperties.

Чаще всего дополнительная информация представлена в виде структуры MODEMDEVCAPS, которая размещается на месте поля wcProvChar, если поле dwProvSubType содержит значение PST MODEM.

Получить информацию об устройстве в виде структуры COMMPROP можно уже упоминавшейся функцией GetCommProperies. Вот как выглядит ее прототип:

BOOL GetCommProperties(

HANDLE hFile,

LPCOMMPROP lpCommProp

);

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

#include

HANDLE port;

COMMPROP *pr;

port=CreateFile("COM2",GENERIC_READ|GENERIC_WRIТЕ,0,NULL,OPEN_EXISTING,0,NULL);

pr=(COMMPROP*)HeapAlloc(GetProcessHeapO,HEAP_ZERO_MEMORY,sizeof(COMMPROP));

GetCommProperties(port,pr);

if(pr->wPacketLength!= sizeof(COMMPROP)) {{

pr=(COMMPROP*)HeapRealloc(GetProcessHeap(),HEAP_ZERO_MEMORY,pr,pr- >wPacketLength);

pr->wProvSpecl=COMMPROP_INITIALIZED;

GetCommProperties(port,pr);

}

HeapFree(GetProcessHeap(),0,pr);

CloseHandle(port);

He всегда настройку порта можно жестко зашить в код программы. Внешние устройства могут позволять изменять параметры линии связи, чаще всего скорость обмена, которая зависит от длины соединительного кабеля. В таких случаях разумно предоставить пользователю самому задавать режимы обмена. Можно самому разработать соответствующий настроечный диалог, а можно воспользоваться стандартным, предоставляемым операционной системой, а точнее, производителем порта. Стандартный диалог выводится функцией CommConfigDialog, которая работает со структурой COMMCONFIG. Как и в случае со структурой DCB, заполнять структуру COMMCONFIG можно вручную или вызовом соответствующих функций.


Структура COMMCONFIG

Начнем с самой структуры COMMCONFIG:

typedef struct _COMM_CONFIG {{

DWORD dwSize;

WORD wVersion;

WORD wReserved;

DCB dсb;

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.


Функция GetCommConfig

Несмотря на то, что нам нужен только DCB, приходится иметь дело со всеми полями. Заполнение данной структуры противоречивыми данными может привести к неправильной настройке порта, поэтому следует пользоваться функцией GetCommConfig:

BOOL GetCommConfig(

HANDLE hCommDev,

LPCOMMCONFIG lpCC,

LPDWORD lpdwSize

);

Параметры функции следующие:

∙ hCommDev

Описатель открытого коммуникационного порта.


∙ IpCC

Адрес выделенного и заполненного нулями, кроме поля dwSize, блока памяти под структуру COMMCONFIG. В поле dwSize нужно занести размер структуры COMMCONFIG. После вызова функции все поля структуры будут содержать информацию о текущих параметрах порта.


∙ IpdwSize

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

В случае успешного завершения функция возвращает ненулевое значение.

Как всегда не обошлось без тонкостей. Структура COMMPROP имеет переменную длину, поэтому затруднительно сразу выделить требуемый блок памяти. Как и в случае с функцией GetCommProperties, функцию GetCommConfig придется вызывать дважды:

COMMCONFIG *cf;

DWORD sz;

HANDLE port;

сf=(COMMCONFIG*)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,sizeof(COMMCONFIG));

cf->dwSize=sizeof(COMMCONFIG);

GetCommConfig(port,cf,&sz);

if(sz > sizeof(COMMCONFIG)) {{

сf=(COMMCONFIG*)HeapRealloc(GetProcessHeap(),HEAP_ZERO_MEMORY,cf,sz);

cf->dwSize=sz;

GetCommConfig(port,cf,&sz);

};

...

HeapFree(GetProcessHeap(),0,cf);

CloseHandle(port);


Функция CommConfiaDialoa

Теперь, имея заполненую корректной информацией структуру COMMCONFIG, можно позволить пользователю выполнить настройку параметров с помощью функции CommConfigDialog:

BOOL CommConfigDialog(

LPTSTR IpszName,

HWND hWnd,

LPCOMMCONFIG IpCC

);



Вызов этой функции приводит к отображению примерно такого диалогового окна:

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

Познакомимся с параметрами функции CommConflgDialng:


∙ IpszName

Указатель на строку с именем порта, для которого отображается диалоговое окно. К реальному имени порта эта строка не имеет никакого отношения, она просто выводится в заголовке окна.


∙ hWnd

Описатель окна, которое владеет данным диалоговым окном. Должен быть передан корректный описатель окна-владельца или NULL, если у диалогового окна нет владельца.


∙ IpCC

Указатель на структуру COMMCONFIG. Эта структура содержит начальные установки, используемые для отображения в диалоговом окне, и установленные пользователем изменения, при завершении диалога.

Как и большинство других функций Win32 API, функция CommConfigDialog возвращает отличное от нуля значение, в случае успешного завершения, и нуль, если возникла ошибочная ситуация.

Функция CommConfigDialog не выполняет настройки порта. Она все лишь позволяет пользователю изменить некоторые поля в блоке DCB, содержащемся в структуре COMMCONFIG. Разумеется, Вы можете изменить установленные пользователем некорректные значения или выполнить дополнительные настройки после вызова функции GetCommConfig.


Функция SetCommConfig

Фактическая настройка порта выполняется функцией SetCommConfig:

BOOL SetCommConfig(

HANDLE hCommDev,

LPCOMMCONFIG IpCC,

DWORD dwSize

);

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


Функции GetDefaultCommConfig и SetDegaultCommConfig

Обратите внимание на кнопку "Restore Defaults". Вы в состоянии управлять ее поведением, правда опосредовано, с помощью функций GetDefaultCommConfig и SetDegaultCommConfig. Вот их прототипы:

BOOL GetDefaultCommConfig(

LPCSTR IpszName,

LPCOMMCONFIG IpCC,

LPDWORD lpdwSize

);

BOOL SetDefaultCommConfig(

LPCSTR IpszName,

LPCOMMCONFIG IpCC,

DWORD dwSize

);

Эти функции очень похожи на GetCommConfig и SetCommConfig, но предназначены совсем для другой цели. Предположим, что Ваше устройство, по умолчанию, работает на скорости 175 бит в секунду и обменивается пятибитными символами. Системные же умолчания — 9600 бит в секунду и 8 бит в символе. Что бы пользователь, при нажатии на кнопку "Restore Defaults", получал не системные, а Ваши умолчания, воспользуйтесь функциями GetDefaultCommConfig и SetDefaultCommConfig. SetDefaultCommConfig не настраивает порт, это выполняется функцией SetCommConfig, а изменяет параметры во внутренней области коммуникационного драйвера.


Функция SetupComm

Теперь познакомимся с функцией SetupComm, которая, на самом деле, совсем не то, что следует из названия.

BOOL SetupComm(

HANDLE hFile,

DWORD dwInQueue,

DWORD dwOutQueue

);

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

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

Внешние устройства управления объектами, чаще всего подключаемые к портам, обычно обмениваются с компьютером короткими сообщениями. Соответственно и вызов функции SetupComm не требуется. Однако, если Ваше устройство передает или принимает блоки данных длиной в несколько тысяч байт, рекомендуется установить размеры очередей драйвера.

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


Функции Read File и WriteFile

Прием и передача данных выполняется функциями 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

Число ожидаемых к приему или предназначенных к передаче байт.


∙ nNumOfBytes Read, nNumOfBytesWritten

Число фактически принятых или переданных байт. Если принято или передано меньше данных, чем запрошено, то для дискового файла это свидетельствует об ошибке, а для коммуникационного порта совсем не обязательно. Причина в тайм-аутах.


∙ lpOverlapped

Адрес структуры OVERLAPPED, используемой для асинхронных операций. Подробнее как с структурой, так и с асинхронными операциями мы познакомимся позже. Для синхронных операций данный параметр должен быть равным NULL.


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


Функция PuraeComm

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

BOOL PurgeComm(

HANDLE hFile,

DWORD dwFlags

);

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

• PURGE_TXABORT — Немедленно прекращает все операции записи, даже если они не завершены

• PURGE_RXABORT — Немедленно прекращает все операции чтения, даже если они не завершены

• PURGE_TXCLEAR — Очищает очередь передачи в драйвере

• PURGE_RXCLEAR — Очищает очередь приема в драйвере

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


Функция FlushFileBuffers

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

BOOL FlushFileBuffers(

HANDLE hFile

);

Приведу пример выполнения настройки порта и выполнения чтения/записи данных.

#include

#include

DCB dсb;

COMMTIMEOUTS ct;

HANDLE port;

DWORD bс;

char *buf out="Test string";

char *buf_in;

dсb.DCBlength=sizeof(DCB);

BuildCommDCB("baud=9600 parity=N data=8 stop=l",&dcb);

dсb.fNull=TRUE;

ct.ReadIntervalTimeout=10;

ct.ReadTotalTimeoutMultiplier=ct.ReadTotalTimeoutConstant=0;

ct.WriteTotalTimeoutMultiplier=ct.WriteTotalTimeoutConstant=0;


port=CreateFile("COM2",GENERIC_READ|GENERIC_WRIТЕ,0,NULL,OPEN_EXISTING, 0,NULL);

SetCommState(port,dсb);

SetCommTimeouts(port,&ct);

PurgeComm(port,PURGE_TXCLEAR PURGE_RXCLEAR);

SetupComm(port,25 6,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,0,buf_in);

CloseHandle(port);

Если на COM2 установить перемычку между сигналами TxD и RxD, то переменная bufin, после выполнения ReadFile, будет содержать ту же информацию, что и buf_out. Других пояснений пример не требует, все уже было подробно рассмотрено раньше.


Функция TransmitCommChar

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

BOOL TransmitCommChar(

HANDLE hFile,

char cChar

);

Данная функция передает один (и только один) внеочередной байт в линию, не смотря на наличие данных в очереди передатчика, и перед этими данными. Однако управление потоком действует. Функцию можно вызвать только синхронно. Более того, если байт экстренных данных, от предыдущего вызова этой функции, еще не передан в линию (например, из-за функций управления потоком), то попытка экстренной передачи еще одного байта завершится ошибкой. Если Вы используете программное управление потоком, то символы приостановки и возобновления передачи (обычно CTRL-S и CTRL-Q), лучше всего передавать именно этой функцией.


Функции SetCommBreak и ClearCommBreak

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

BOOL SetCommBreak(

HANDLE hFile

);

BOOL ClearCommBreak(

HANDLE hFile

);


Функция EscapeCommFunction

Следует заметить, что состояние разрыва линии устанавливается аппаратно. Поэтому нет другого способа возобновить прерванную, с помощью SetCommBreak, передачу данных, кроме вызова ClearCommBreak. Более тонкое управление потоком данным позволяет осуществить функция:

BOOL EscapeCommFunction(

HANDLE hFile,

DWORD dwFunc

);

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

• CLRDTR — Сбрасывает сигнал DTR

• CLRRTS — Сбрасывает сигнал RTS

• SETDTR — Устанавливает сигнал DTR

• SETRTS — Устанавливает сигнал RTS

• SETXOFF — Симулирует прием символа XOFF

• SETXON — Симулирует прием символа XON

• SETBREAK — Переводит выходную линию передатчика в состояние разрыва. SetCommBreak является упрощенной формой данного вызова.

• CLRBREAK — Снимает состояние разрыва для выходной линии передатчика. ClearCommBreak является упрощенной формой данного вызова.


Функция ClearCommError

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

BOOL ClearCommError(

HANDLE hFile,

LPDWORD IpErrors,

LPCOMSTAT IpStat

);

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


∙ hFile

Описатель открытого файла коммуникационного порта.


∙ IpErrors

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

• CE_BREAK — Обнаружено состояние разрыва связи

• CE_DNS — Только для Windows95. Параллельное устройство не выбрано.

• CE_FRAME — Ошибка обрамления.

• СЕ_IOЕ — Ошибка ввода-вывода при работе с портом

• СЕ_MODE — Запрошенный режим не поддерживается, или неверный описатель hFile. Если данный бит установлен, то значение остальных бит не имеет значение.

• СЕ_ООР — Только для Windows95. Для параллельного порта установлен сигнал "нет бумаги".

• CE_OVERRUN — Ошибка перебега (переполнение аппаратного буфера), следующий символ потерян.

• СЕ_РТО — Только для Windows95. Тайм-аут на параллельном порту.

• CE_RXOVER — Переполнение приемного буфера или принят символ после символа конца файла (EOF)

• CE_RXPARITY — Ошибка четности

• CE_TXFULL — Переполнение буфера передачи


∙ IpStat

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


Структура COMMSTAT

Если с информацией об ошибке все ясно, то со структурой COMMSTAT мы еще не встречались. Вот она:

typedef struct _COMSTAT

DWORD fCtsHold:1;

DWORD fDsrHoldr:1;

DWORD fRlsdHold:1;

DWORD fXoffHold:1;

DWORD fXoffSent:1;

DWORD fEof:1;

DWORD fTxim:1;

DWORD fReserved:25;

DWORD cblnQue;

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

Зарезервировано и не используется.


∙ cblnQue

Число символов в приемном буфере. Эти символы приняты из линии но еще не считаны функцией ReadFile.


∙ cbOutQue

Число символов в передающем буфере. Эти символы ожидают передачи в линию. Для синхронных операций всегда 0.


Теперь Вы знаете почти все о работе с последовательными и параллельными портами в синхронном режиме. Особенности непосредственной работы с модемами я не буду рассматривать, так как существует большой набор высокоуровневых функций и протоколов, таких как TAPI, специально предназначенных для работы с модемами. Если Вас все же интересует эта тема, то почитайте описания функции GetCommModemStatus, и структур MODEMDEVCAPS и MODEMSETTINGS. В остальном работа с модемом ничем не отличается от работы с обычным портом.

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


Функция SetCommMask

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

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 равно нулю, то отслеживание событий запрещается.


Функция GetCommMask

Разумеется, всегда можно получить текущую маску отслеживаемых событий с помощью функции

BOOL GetCommMask(

HANDLE hFile,

LPDWORD IpEvtMask

);

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

• EV_EVENT1 — Устройство-зависимое событие

• EV_EVENT2 — Устройство-зависимое событие

• EV_PERR — Ошибка принтера

• EV_RX80FULL — Приемный буфер заполнен на 80 процентов

Эти дополнительные события используются внутри драйвера. Вы не должны переустанавливать состояние их отслеживания.


Функция WaitCommEvent

Когда маска отслеживаемых событий задана, Вы можете приостановить выполнение своей программы до наступления события. При этом программа не будет занимать процессор. Это выполняется вызовом функции

BOOL WaitCommEvent(

HANDLE hFile,

LPDWORD IpEvtMask,

LPOVERLAPPED lpOverlapped,

);

Замечу, что в переменной, адресуемой вторым параметром, не будут устанавливаться внутренние события драйвера (перечислены в описании функции GetCommMask). В единичное состояние установятся только те биты, которые соответствуют реально произошедшим событиям.

Адрес структуры OVERLAPPED требуется для асинхронного ожидания (возможно и такое). Однако пока будем полагать, что порт открыт для синхронных операций, следовательно этот параметр должен быть NULL. Замечу только, что при асинхронном ожидании данная функция может завершиться с ошибкой, если в процессе этого ожидания будет вызвана функция SetCommMask для переустановки маски событий. Кроме того, связанное со структурой OVERLAPPED событие (объект создаваемый функцией CreateEvent, а не событие порта) должно быть с ручным сбросом. Вообще, поведение функции с ненулевым указателем на структуру OVERLAPPED аналогично поведению функций чтения и записи. Теперь коротенький пример:

#include

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);

dсb.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,Smask,NULL);

ReadFile(port,buf,10 0,&bc,NULL);

CloseHandle(port);

В данном примере ожидается начало сообщения (первый полученный символ), после чего вызывается функция чтения. Освобождать процессор на время ожидания хорошо, но хотелось бы параллельно с вводом/выводом делать какую-либо полезную работу. Что бы это стало возможным, необходимо в качестве параметра dwFlagsAndAttributes вместо 0 указать FILE_FLAG_OVERLAPPED. Кроме того, для функций ReadFile, WriteFile и WaitCommEvent необходимо в качестве параметра lpOverlapped указывать адрес правильно инициализированной структуры OVERLAPPED.


Структура OVERLAPPED

Структура OVERLAPPED выглядит следующим образом:

typedef struct _OVERLAPPED {{

DWORD Internal;

DWORD InternalHigh;

DWORD Offset;

DWORD OffsetHigh;

HANDLE hEvent;

} OVERLAPPED, *LPOVERLAPPED;

Подробно описывать поля этой структуры не буду, поскольку данная статья не о файловом вводе/выводе вообще, а о работе с портами. Для наших целей, за исключением WaitCommEvent, можно просто обнулить все поля этой структуры. Для WaitCommEvent поле hEvent должно содержать корректный описатель объекта "событие". Что бы все стало понятно, надо разобраться с таким обязательным атрибутом параллельной работы как синхронизация.


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

Для синхронизации используются различные методы: семафоры, блокировки, события, критические секции и т. п. События являются простейшими синхронизирующими объектами. Они могут находиться только в двух состояниях: установленном (событие произошло или наступило) и сброшенном (событие не произошло или не наступило). События создаются функцией CreateEvent и разрушаются функцией CloseHandle. Установить событие можно функцией SetEvent, а сбросить ResetEvent.

Титов Олег

Загрузка...