Эта статья планировалась для 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 не пройдут из-за того что порт уже открыт.

Асинхронное чтение

Честно говоря, асинхронное чтение это не то в чем у меня есть много опыта. По моему, есть два основных пути сделать это:

  1. Подключить порт к событию таймера чтобы смотреть нет ли новых данных для чтения
  2. Вызывать EnableCommNotification и написать sub-class процедуру для обработки события WM_COMMNOTIFY

Сейчас, первый метод относится больше к временам ДОС, и я не честно могу рекомендовать вам его использовать, ведь sub-classed процедуры так легко писть. Однако, если вы имеете общий исходный текст, который компилируется в 16-разрядные и 32-разрядные приложения, то вы можете иметь искушение пойти первым путем, только из-за того что EnableCommNotification это API-функция, которая больше не существует для 32-бит. Но это опять зависит от ваших действительных обстоятельств..

Выводы

Если вы собираетесь просто писать и читать из последовательного устройства, то использование API действительно довольно легко. Наиболее сложная часть это правильная установка структуры DCB для обработки протокола "рукопожптия" если вы решили его использовать. Если нужно больше, особенно асинхронное чтение, то я рекомендую продукты третьих фирм, но как минимум вы теперь имеете понятие о том как они работают.


К моей домашней странице. Или пишите мне paula@attglobal.net

(c) 1999 Paul Attryde

 

1