Эта статья планировалась для ClarionOnline ( http://www.clariononline.com ) в июле 1999, но это не было сделано . . . .
Синхронные последовательные коммуникации
Один из вопросов, который наиболее часто задается в новостных группах по Clarion, включает некоторый род взаимодействия с последовательными устройствами, либо при попытке вывести данные на принтер, либо при чтении ответа от чего-либо. Обычный ответ на этот вопрос вертится около строк "попробуй Clacom" (от Gap Development, http://www.gapdev.com/ ) или "попробуй WinEvent" (от Capesoft, http://www.capesoft.com/ ). Однако, есть и другой ответ - использовать Windows API, что и делают эти продукты третьих фирм. Итак, если вы еще до сих пор не поняли, эта статья посвящена последовательным коммуникациям.
16-бит против 32-бит
В 16-разрядном Windows API имеется несколько функций (фактически, 17) относящихся исключительно к коммуникациям. Они охватывают все начиная от открытия и инициализации порта, чтения/записи данных, проверки ошибок и т.д.
В 32-разрядном API, некоторые из этих функций (включая наиболее важные - чтение и запись в порт) просто не существуют. Это оттого что, компьютеризация сделала шаг назад в 15 лет, и сейчас вы должны открывать файл с именем COM1" и писать ваш код как будто вы читаете и пишете в обычный файл.
Из-за такой разницы между ними, сейчас я сконцентрируюсь только на 16-разядном API, а 32-разрядный API будет освещен позднее в другой статье.
Структура Device Control Block
Структура DCB используется для конфигурирования порта когда он открывается. DCB говорит какой используется протокол (DTS/CTS, XON/XOFF и т.д.), устанавливает скорость, четность, количество стоп-бит всю основную конфигурационную информацию порта. На языке C она определяется следующим образом:
typedef struct tagDCB /* dcb */ {
BYTE Id;
UINT BaudRate;
BYTE ByteSize;
BYTE Parity;
BYTE StopBits;
UINT RlsTimeout;
UINT CtsTimeout;
UINT DsrTimeout;
UINT fBinary :1;
UINT fRtsDisable :1;
UINT fParity :1;
UINT fOutxCtsFlow :1;
UINT fOutxDsrFlow :1;
UINT fDummy :2;
UINT fDtrDisable :1;
UINT fOutX :1;
UINT fInX :1;
UINT fPeChar :1;
UINT fNull :1;
UINT fChEvt :1;
UINT fDtrflow :1;
UINT fRtsflow :1;
UINT fDummy2 :1;
char XonChar;
char XoffChar;
UINT XonLim;
UINT XoffLim;
char PeChar;
char EofChar;
char EvtChar;
UINT TxDelay; } DCB;
Одна интересная вещь - это группа полей в средине структуры с суффиксами :1 и :2. Это декларация C о том, что эти поля имеют длину 1 или 2 бита - очевидно, что мы не можем написать так на Clarion, поэтому что же делать в Calarion-версии? Но так как суммарная длина этих полей равна 16 разрядам, то мы можем заменить эту группу 16-битным полем типа Long, следующим образом:
DeviceControlBlock Group,Type Id Byte BaudRate UShort ByteSize Byte Parity Byte StopBits Byte RlsTimeout UShort CtsTimeout UShort DsrTimeout UShort Bitmask UShort XonChar Byte XoffChar Byte XonLim UShort XoffLim UShort PeChar Byte EofChar Byte EvtChar Byte TxDelay UShort End
Для установки этих битовых полей нужно использовать функцию BOR языка Clarion и таким образом устанавливать только те биты, которые хотим.
Ниже описаны отдельные поля (биты) поля Bitmask.
1 1 1 1 1 1
5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
x 0 Binary mode, no EOF
x 1 Enable parity checking
x 2 CTS output flow control
x 3 DSR output flow control
x x 4,5 DTR flow control type
. . Disables the DTR line on device open and leaves it disabled.
. x Enables the DTR line on device open and leaves it on.
x . Enables DTR handshaking.
x 6 DSR sensitivity
x 7 XOFF continues transmitting
x 8 XON/XOFF output flow control
x 9 XON/XOFF input flow control
x 10 Enable error replacement
x 11 Enable null stripping
x x 12,13 RTS flow control
. . Disables the RTS line when the device is opened and leaves it disabled
. x Enables the RTS line when the device is opened and leaves it on.
x . Enables RTS handshaking. The driver raises the RTS line when
the "type-ahead" (input) buffer is less than one-half
full and lowers the RTS line when the buffer is more than
three-quarters full.
x x Specifies that the RTS line will be high if bytes are available
for transmission. After all buffered bytes have been sent,
the RTS line will be low.
x 14 Abort reads/writes on error
Например, при использовании DTR/DSR, вероятно нужно установить разряды 0,3,5 и 14. Это можно сделать при помощи функции BOR таким образом:
Loc:Dcb:Bitmask = Bor(Loc:Dcb:bitmask,100000000101001B)
При использовании XON/XOFF нужно установить разряды 0,8,9 и 14 в Bitmask, и также установить символы XON и XOFF в структуре DCB:
Loc:Dcb:Bitmask = Bor(Loc:Dcb:bitmask,100001100000001B) Loc:DCB:XonChar = 17 Loc:DCB:XoffChar = 19
Открытие и инициализация порта
Перед тем как что-либо делать с последовательным портом (или параллельным, неважно), нужно сначала его открыть и инициализировать. Это делается при помощи следующих функций:
OpenComm(LONG PtrToPortName, WORD SizeOfRxBuffer, WORD SizeOfTxBuffer),SIGNED,PASCAL,RAW BuildCommDCB(LONG PtrToControlString,LONG PtrToDCB),BOOL,PASCAL,RAW SetCommState(LONG PtrToDCB),BOOL,PASCAL,RAW
OpenComm действительно открывает порт, и возвращает указатель, который нужен для использования всех следующих вызовов функций.
BuildCommDCB создает для порта device-control block. SetCommState берет DCB и применяет его к порту.
Например, для открытия порта COM1, ваш код может быть таким:
Loc:PortString = ‘COM1’
Loc:PortHandle = OpenComm(Address(Loc:PortString),1024,4196)
If Loc:PortHandle > 0 Then
Loc:ControlString = ‘COM1:9600,N,8,1’
If BuildCommDcb(Address(Loc:ControlString),Address(Loc:Dcb)) = 0
SetCommState(Address(Loc:Dcb))
End
End
Этот код не делает ничего фантастического, наподобие установки протокола или проверки ошибок, но я уверен, что вы поняли основную идею. BuildCommDCB устанавливает поля по информации из управляющей строки, но если вы хотите больше этого (например, устанавливает "рукопожатие" RTS/CTS), то нужно вручную установить нужные разряды в DCB bitmask. Обычно это нужно делать после вызова BuildCommDCB (иначе BuildCommDCB очистит только что установленные разряды), но перед вызовом SetCommState (иначе установки не дадут эффекта).
OpenComm возвращает хендл порта, обычно начинающийся с 1 и увеличивающийся для каждого последующего открываемого порта. Если он возвращает значение меньше нуля, то это сообщение об ошибке:
| Константа | Значение | Смысл |
| IE_BADID | -1 | Неправильный или неподдерживаемый идентификатор устройства. |
| IE_OPEN | -2 | Устройство уже открыто. |
| IE_NOPEN | -3 | Устройство не открыто. |
| IE_MEMORY | -4 | Функция не может выделить очереди. |
| IE_DEFAULT | -5 | Ошибка в параметре по умолчанию. |
| IE_HARDWARE | -10 | "Железо" недоступно. |
| IE_BYTESIZE | -11 | Неверно указанно byte size. |
| IE_BAUDRATE | -12 | Устройством не поддерживается эта baud rate. |
Теперь, вы можете также использовать этот код для открытия любого параллельного порта только установите имя порта в LPT1. Если вы собираетесь использовать Windows API для передачи через параллельные порты, стоит также запомнить, что для них не нужно создавать DCB. Параллельный порт не имеет baud rate, четности и т.п., и он точно не заботится о "рукопожатии".
Чтение и запись в порт
Для чтения и записи в порт мы будем использовать функции API - ReadComm и WriteComm.
WriteComm(SIGNED PortHandle,LONG PtrToDataBuffer,SIGNED NumberOfByteToWrite),SIGNED,PASCAL,RAW ReadComm(SIGNED PortHandle,LONG PtrToDataBuffer,SIGNED NumberOfBytesToRead),SIGNED,PASCAL,RAW
Обе эти функции принимают хендл порта (который мы получили из OpenComm), указатель на Cstring, которая хранит данные для для записи или чтения, и число байт, с которым мы хотим работать. Возвращаемое значение из каждой функции является числом байт, которое действительно прочитано иле записано.
Например, для записи в устройство мы должны сделать что то наподобие этого:
Loc:DataString = ‘Hello World’ Loc:Len = Len(Clip(Loc:DataString)) If WriteComm(Loc:PortHandle,Address(Loc:DataString),Loc:Len) = Loc:Len ! Все байты записаны нормально Else ! Что то записано, но не все End
Для чтения из устройства, это будет выглядеть так:
X# = ReadComm(Loc:PortHandle,Address(Loc:DataString),4096) If x# = 0 Then ! Вообще ничего не прочитано Else ! Что то прочитано, не не ожидаемые 4K End
Чтение из порта обычно более сложно чем запись, потому что нужно знать сколько байт ожидается принять из порта. Можно пытаться читать больше байт, чем ожидается (например, 4К) , или сделать цикл, в котором читать по одному байту пока ReadComm не возвратит 0. Лично я предпочитаю первый метод - чем меньше вы делаете вызовов API, тем быстрее будет ваш код, но в конце концов это все зависит от вашей реализации.
Также, хотя можно использовать WriteComm для записи данных в параллельные устройства (такие как принтер), нельзя использовать ReadComm для чтения DeviceStatus. Для параллельных устройств ReadComm всегда возвращает 0.
Проверка ошибок
Иногда вы хотите, или должны, проверить порт на ошибки. Если вы пытаетесь послать в порт 20 байт данных и только 10 записано, то хочется знать почему? Для выяснения этого, нужно вызвать функцию GetCommError:
GetCommError(SIGNED PortHandle,LONG PointerToComStat),SIGNED,PASCAL,RAW
Структура ComStat принимает информацию из GetCommError. Она не может сама возвращать чего либо, потому что принимает возвращаемое значение из последней вызванной коммуникационной функции. Сама структура ComStat объявляется следующим образом:
typedef struct tagCOMSTAT { /* cmst */
BYTE status;
UINT cbInQue;
UINT cbOutQue;
} COMSTAT;
Clarion-овское представление этой структуры таково:
ComStat Group,Type Status Byte cbInQ UShort cbOutQ UShort End
Как вы видите, структура ComStat тоже сообщает сколько байт осталось в принимаемой или принимающей очереди данных (размер которой указывается при вызове OpenComm).
Возвращаемое значение из GetCommError ( которое в действительности является возвращаемым значением последней вызванной коммуникационной функции) является битовой маской различных состояний ошибок. Ее значения:
| Константа | Значение | Смысл |
| CE_RXOVER | 1 | Приемная очередь переполнена. Либо не было места в приемном буфере, либо или был принят символ после принятого символа end-of-file. |
| CE_OVERRUN | 2 | Символ не был прочитан из устройства из-за того что поступил другой символ. Символ потерян. |
| CE_RXPARITY | 4 | Устройство обнаружило ошибку четности. |
| CE_FRAME | 8 | Устройство обнаружило framing error. |
| CE_BREAK | 16 | Устройство обнаружило условие прерывания. |
| CE_CTSTO | 32 | Таймаут CTS (clear-to-send). |
| CE_DSRTO | 64 | Таймаут DSR (data-set-ready). |
| CE_RLSDTO | 128 | Таймаут RLSD (receive-line-signal-detect). |
| CE_TXFULL | 256 | Очередь устройства была заполнена при попытке функции поместить в очередь символ. |
| CE_PTO | 512 | Таймаут при попытке взаимодействия с параллельным устройством. |
| CE_IOE | 1024 | Ошибка I/O возникла при попытке взаимодействия с параллельным устройством. |
| CE_DNS | 2048 | Параллельное устройство не выбрано. |
| CE_OOP | 4096 | .Параллельное устройство сообщает что не бумаги. |
| CE_MODE | 32768 | Запрашиваемый режим не поддерживается. Если установлен, то CE_MODE is the only valid error. |
Поле Status может принимать следующие значения:
| Константа | Значение | Смысл |
| CSTF_CTSHOLD | 1 | Указывает что ожидается сигнал CTS (clear-to-send) для посылки. |
| CSTF_DSRHOLD | 2 | Указывает что ожидается сигнал DSR (data-set-ready) для посылки. |
| CSTF_RLSDHOLD | 4 | Указывает что ожидается сигнал RLSD (receive-line-signal-detect) посылки. |
| CSTF_XOFFHOLD | 8 | Указывает что ожидается сигнал как результат принятого символа XOFF. |
| CSTF_XOFFSENT | 16 | Указывает что ожидается сигнал как результат переданного символа XOFF. Передача останавливается когда символ XOFF передается и используется системой как получение следующего сигнала такого как XON, независимо от действительного символа. |
| CSTF_EOF | 32 | Указывает что был принят символ end-of-file (EOF). |
| CSTF_TXIM | 64 | Указывает что ожидается символ. |
Closing the port
После окончания работы с устройством нужно закрыть порт. Мы делаем это при помощи функции API CloseComm:
CloseComm(SIGNED PortHandle),SIGNED,PASCAL,RAW
Например,
If CloseComm(Loc:PortHandle) <> 0 ! Ошибка закрытия порта End
Хотя порт будет закрыт при выходе из программы, вы можете открыть порт когда это нужно. Если порт не закрыть, то второй и последующие вызовы OpenPort не пройдут из-за того что порт уже открыт.
Асинхронное чтение
Честно говоря, асинхронное чтение это не то в чем у меня есть много опыта. По моему, есть два основных пути сделать это:
Сейчас, первый метод относится больше к временам ДОС, и я не честно могу рекомендовать вам его использовать, ведь sub-classed процедуры так легко писть. Однако, если вы имеете общий исходный текст, который компилируется в 16-разрядные и 32-разрядные приложения, то вы можете иметь искушение пойти первым путем, только из-за того что EnableCommNotification это API-функция, которая больше не существует для 32-бит. Но это опять зависит от ваших действительных обстоятельств..
Выводы
Если вы собираетесь просто писать и читать из последовательного устройства, то использование API действительно довольно легко. Наиболее сложная часть это правильная установка структуры DCB для обработки протокола "рукопожптия" если вы решили его использовать. Если нужно больше, особенно асинхронное чтение, то я рекомендую продукты третьих фирм, но как минимум вы теперь имеете понятие о том как они работают.
К моей домашней странице. Или пишите мне paula@attglobal.net
(c) 1999 Paul Attryde