32-разрядные синхронные коммуникации


Как я отметил в моей предыдущей статье 16-bit serial communications, имеется большая разница между 16-и и 32-х разрядными вызовами API, которые вы должны делать для коммуникации через последовательный порт. Для 32-разрядного API Microsoft удалил зависящие от порта вызовы API функций OpenPort(), ReadPort(), WritePort() и ClosePort() и сделал чтобы файловые вызовы API - CreateFile(), ReadFile(), WriteFile() и CloseHandle() работали как с файлами так и с портами (и mailslots, pipes и sockets и множеством других вещей, которые не рассматриваются в этой статье).  В этой статье я коснусь только разницы между 16-и  32-разрядными API, так как предполагаю что вы уже прочли предыдущую статью как много лучшее введение в предмет и обзор.

Открытие и инициализация порта

До того как вы сможете писать в последовательный порт, вам нужно сначала его открыть и инициализировать.  Чтобы это сделать нужно сделать три (минимум, или больше) вызова функций API:

CreateFile(LONG PtrToFilename,|
			DWORD DesiredAccess,|
			DWORD SharedMode,|
			LONG SecurityAttributes,|
			DWORD CreationDisposition,|
			DWORD Flags,|
			HANDLE TemplateFile),Handle,Raw,Pascal,Name('CreateFileA')

BuildCommDCB(LONG PtrToDeviceControlString,|
			LONG PtrToDCB),BOOL,RAW,PASCAL,Name('BuildCommDCBA')

SetCommState(HANDLE PortHandle,LONG PtrToDCB),BOOL,RAW,PASCAL

CreateFile() открывает порт, и возвращает хендл, который будет испоользоваться для всех последующих вызовов. BuildCommDCB() строит для порта device-control block.  SetCommState() принимает DCB и применяет его к порту. 

Для открытия порта нудно сделать что то подобное этому:

Loc:PortString = ‘COM1’
Loc:Access = GENERIC_READ + GENERIC_WRITE
Loc:PortHandle = CreateFile( Address(Loc:PortString), Loc:Access ,0 ,NULL ,OPEN_EXISTING ,0 ,NULL)
If Loc:ReturnValue <> INVALID_HANDLE_VALUE
 Loc:ControlString = ‘COM1:9600,N,8,1’
 If BuildCommDCB(Address(Loc: ControlString),Address(Loc:Dcb)) <> 0
  SetCommState(Loc:PortHandle,Address(Loc:DCB))
 End
End

Этот код работает точно также как 16-разрядный код, только изменился один вызов API. CreateFile() возвращает хендл порта. BuildCommDCB() устанавливает поля из управляющей строки, но если вам нужно больше (например, установить "рукопожатие" RTS/CTS) то нужно вручную установить нужные биты в DCB bitmask. Обычно это нужно делать после вызова BuildCommDCB() (иначе BuildCommDCB() очистит только что установленные разряды), но перед вызовом SetCommState (иначе установки не дадут эффекта).

В отличие от OpenPort(), которая возвращает последовательный номер каждого открываемого порта, CreateFile() возвращает хендл, который обычно может быть любым допустимым значением. Если вы открыли два порта, то нельзя положиться на то что хендл второго порта имеет следующий последовательный номер - вы можете получить значения хендлов 72 и 24, вместо 1 и 2 в 16-разядном коде. Если хендл порта равен INVALID_HANDLE_VALUE (-1) то нужно вызвать функцию API  GetLastError(), чтобы понять почему  вызов CreateFile() не прошел.

У CreateFile() имеется проблема с номерами портов больше чем 9. Если у вас есть digiboard или порт с номером больше 10, то имя порта должно быть в следующем формате '\\.\COM10'. Не спрашивайте почему, я понятия не имею. Этот же механизм именования портов будет дейсвовать для всех портов, поэтому если вы строите строку обозначения порта на лету во время выполнения, то должны использовать такой код:

Loc:PortString = '\\.\COM' & Loc:PortNumber 

Как и в 16-разрядном коде можно использовать CreateFile() для открытия параллельного порта. Вы не можете вызвать для параллельного порта большинство из коммуникационных функций (таких как SetupComm() или SetCommTimeouts()). Если вы попытаетесь это сделать, то получите ошибку ERROR_INVALID_FUNCTION.

Когда вы открываете последовательный или параллельный порт при помощи CreateFile(), нужно установить параметр CreationDisposition в OPEN_EXISTING и параметр TemplateFile в NULL. Можно открыть порт для чтения, записи или чтения и записи. Если порт открывается для асинхронной связи, то можжно открыть порт в режиме overlapped установив параметр FILE_FLAG_OVERLAPPED в параметре Flags.

Чтение и запись в порт

Для чтения и записи в порт мы используем вызовы API  ReadFile() и WriteFile():

ReadFile(HANDLE HandleToFile, |
	LONG PtrToBuffer, |
	DWORD NumberOfBytesToRead, |
	LONG PtrToNumberOfBytesRead, |
	LONG PtrToOverlappedStructure),BOOL,Raw,Pascal,Proc

WriteFile(HANDLE HandleToFile,|
	LONG PtrToBuffer,|
	DWORD NumberOfBytesToWrite, |
	LONG PtrToNumberOfBytesWritten, |
	LONG PtrToOverlappedStructure),BOOL,Raw,Pascal

Как видите, обе функции принимают одинаковое число и типы параметров - хендл порта (который мы получили от CreateFile()), указательна буфер данных, число байт для чтения/записи и указатель на переменную, содержащую количество действительно прочитанных/записанных байт. Последний параметр является указателем на структуру OVERLAPPED, и используется в асинхронном вводе и выводе. Вам нужно только передать туда значение, если вы открываете порт для overlapped I/O с флагом FILE_FLAG_OVERLAPPED. Возвращаемое значение из обеих функций равно нулю (если возникла ошибка), или не ноль(если все нормально).

Запись данных в устройство довольно проста:

Loc:OutString= 'Hello World'
Loc:Len = Len(Clip(Loc:OutString))
Loc:ReturnCode = WriteFile(Loc:PortHandle, Address(Loc:OutString),|
	Loc:Len, Address(Loc:BytesWritten), NULL)
If Loc:Len <> Loc:BytesWritten
  ! Не записали все байты, которые хотели
End

Чтение из устройства эквивалентно:

Loc:BytesToRead = 20
X# = ReadFile(Loc:PortHandle, Address(Loc:InBuffer), Loc:BytesToRead, |
Address(Loc:BytesRead), NULL)
If x# = 0 Or Loc:BytesRead = 0
 ! Ничего не прочитано - ошибка, или нечего читать
End

ReadFile() может либо возвратить нуль (сообщение об ошибке), или значение не равное нулю (чтение успешно) но в действительности ничего не прочитать. Как быстро будет возврат из ReadFile() если нет данных, зависит от величины таймаута, который можно установить при помощи SetCommTimeouts() после открытия порта.

Контроль ошибок порта

16-разрядная функция GetCommError() также исчезла при апгрейде до 32-разрядов. Для определения ошибок порта нужно вызывать ClearCommError(). Она в основном эквивалентна своему предшественнику, исключая то что она возвращает код ошибки и посылает код ошибки порта назад как дополнительный параметр.

ClearCommError(Long PortHandle, |
	Long PortErrorCode, |
	Long PtrToComStat),Bool,Raw,Pascal,Proc

Подобно GetCommError() она также принимает указатель на структуру ComStat. 32-разрядная структура эквивалентна 16-разрядной, хотя изменилась длина полей:

ComStat                 Group,Pre(CS)
FlagsBitmask              DWord ! Bitmask of flags
InQue                     DWord ! Number of bytes on input Q
OutQue                    DWord ! Number of bytes on output Q
                        End

Как можно видеть, структура ComStat все еще сообщает сколько байт осталось во входном и выходном буферах данных, хотя теперь ее размер указывается в вызове  SetupComm() а не OpenComm().

Используются только младшие 7 разрядов флагов битовой маски, а старшие 25 не используются. Если вы сравните 32-разрядную версию с 16-разрядной, то увидите их подобие.

Bit Bit Name 16-bit Equivalent Meaning
1 fCtsHold  CSTF_CTSHOLD  Specifies whether transmission is waiting for the CTS (clear-to-send) signal to be sent. If this member is TRUE, transmission is waiting. 
2 fDsrHold  CSTF_DSRHOLD Specifies whether transmission is waiting for the DSR (data-set-ready) signal to be sent. If this member is TRUE, transmission is waiting. 
3 fRlsdHold  CSTF_RLSDHOLD Specifies whether transmission is waiting for the RLSD (receive-line-signal-detect) signal to be sent. If this member is TRUE, transmission is waiting. 
4 fXoffHold  CSTF_XOFFHOLD  Specifies whether transmission is waiting because the XOFF character was received. If this member is TRUE, transmission is waiting. 
5 fXoffSent  CSTF_XOFFSENT  Specifies whether transmission is waiting because the XOFF character was transmitted. If this member is TRUE, transmission is waiting. Transmission halts when the XOFF character is transmitted to a system that takes the next character as XON, regardless of the actual character. 
6 fEof  CSTF_EOF  Specifies whether the end-of-file (EOF) character has been received. If this member is TRUE, the EOF character has been received. 
7 fTxim  CSTF_TXIM  If this member is TRUE, there is a character queued for transmission that has come to the communications device by way of the TransmitCommChar() function. The communications device transmits such a character ahead of other characters in the device's output buffer. 

Второй параметр, PortErrors, является битовой маской содержащей тип ошибки. Это идентично возвращаемому значению 16-разрядной функции GetCommError(), исключая то, что некоторых ошибок (CE_CTSTO, CE_DSRTO и CE_RLSDTO) больше не существует.

Constant Value Meaning
CE_RXOVER 1 Receiving queue overflowed. There was either no room in the input queue or a character was received after the end-of-file character was received.
CE_OVERRUN 2 Character was not read from the hardware before the next character arrived. The character was lost.
CE_RXPARITY 4 Hardware detected a parity error.
CE_FRAME 8 Hardware detected a framing error.
CE_BREAK 16 Hardware detected a break condition.
CE_TXFULL 256 Transmission queue was full when a function attempted to queue a character.
CE_PTO 512 Timeout occurred during an attempt to communicate with a parallel device.
CE_IOE 1024 I/O error occurred during an attempt to communicate with a parallel device.
CE_DNS 2048 Parallel device was not selected.
CE_OOP 4096 Parallel device signaled that it is out of paper.
CE_MODE 32768 Requested mode is not supported. If set, CE_MODE is the only valid error.

Закрытие порта

В точности так как и для 16-разрядов, исключая то, что функция API другая :)

CloseHandle(HANDLE PortHandle),BOOL,PASCAL,RAW

Например,

If Loc:PortHandle <> INVALID_HANDLE_VALUE
 X# = CloseHandle(Loc:PortHandle)
 If x# = 0
  ! Ошибка закрытия порта
 End
End

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

Not that I've ever got it to work, но я знаю, что нужно использовать

SetCommMask(LONG PortHandle,LONG EventMask),BOOL,RAW,PASCAL

WaitCommEvent(Long PortHandle,Long EventMask,Long PtrToOverlappedStructure),Bool,Raw,Pascal,Proc

Вам не нужно использовать overlapped I/O для того чтобы заставить работать асинхронный обмен. Нужно использовать SetCommMask() чтобы сообщить ОС о каких событиях нужно известить, и затем использовать WaitCommEvent() для ожидания в блокирующем цикле, до тех пор пока не произойдет событие. Можно использовать что-либо подобное:

If SetCommMask(PortHandle,EV_RXCHAR) <> 0
 Loop
  If WaitCommEvent(PortHandle, Loc:EventMask, NULL) <> 0
   If Band(Loc:EventMask, EV_RXCHAR)
    ! Принят символ из коммуникационного порта
   End
  End
 End ! Loop
Else
 ! Невозможно установить маску
End

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

Эту проблему решает использование перекрывающегося (overlapped) I/O. В overlapped I/O вызов WaitCommEvent() не будет блокировать если нет данных - он будет возвращаться немедленно. Однако, вы должны создать событие (Windows-кое событие, то что Clarion-программисты называют событиями всем остальным известно как message) припомощи вызова функции API CreateEvent().

Нмкогда не делая overlapped I/O, я не могу на самом деле ответить на вопросы о нем, кроме как сказать что в документации MSDN есть всеl. Смотрите раздел "Monitoring Communications Events" для примера того как открывать последовательный порт для overlapped I/O, создавать event mask для мониторинга сигналов CTS и DSR, и затем ожидать возникновения события


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

(c) 1999 Paul Attryde

 

1