Василий Пивко дата публикации 28-03-2005 08:22 Работа с СОМ-портом в Windows (W9x, W2k)
Благодаря Набережных С., переработан участок кода, связанный с обработкой асинхронных операций.
Отличия между W9x, W2k лишь в том, что API-функции у них имеют разные типы параметров (integer-cardinal, например).
Структура TDCB содержит большинство настроек порта. Без понимания назначения ее содержимого нередко трудно разобраться в причинах тех или иных "затыков" в работе программы.
DCB в качестве параметров имеют функции GetCommState и SetCommState.
Итак, по порядку.
- DCBlength : DWORD — размер самой структуры DCB. Задает длину, в байтах, структуры TDCB. Используется для контроля корректности структуры при передаче ее адреса в функции настройки порта. Дело в том, что существует еще одна структура с параметрами порта для API совсем "нижнего уровня", несколько отличающаяся от рассматриваемой.
- BaudRate : DWORD — скорость передачи данных. Возможно указание любого значения, однако установленная скорость будет кратна скорости 115200. Дело в том, что в самой микросхеме порта есть два регистра, являющиеся непосредственно предделителем и делителем. Еще по опыту DOS программирования не знаю, как "добраться" до предделителя, но на делитель уже приходят такты частотой 115200. Соответственно, при записи в делитель "1" (этим занимаются API) — скорость будет 115200, "2" — 57600, "3" — 38400, "4" — 28800 и т.д. Возьмите калькулятор и вы можете получить весь возможный ряд скоростей. Соответственно, в делитель пишется K = 115200 div BaudRate.
- Flags : DWORD — набор флагов (перечислены от младшего к старшему биту), определяющих:
Название по Help | Разм., бит | Назначение |
fBinary | 1 | Включает двоичный режим обмена. Другого и не поддерживается, поэтому всегда = "1". |
fParity | 1 | "1" — есть контроль четности по принципу значения поля Parity(см.далее), "0" — нет контроля четности (фактически при передаче не добавляется бит четности). |
fOutxCtsFlow | 1 | "1" — при установке модемного CTS в "0" передача приостанавливается до установки CTS в "1". |
fOutxDtrFlow | 1 | "1" — при установке модемного DSR в "0" передача приостанавливается до установки DSR в "1". |
fDtrControl | 2 | "00" (DTR_CONTROL_DISABLE) — запрещает "вручную" управлять (EscapeCommFunction) модемной линией DTR. "01" (DTR_CONTROL_ENABLE) — разрешает "вручную" управлять (EscapeCommFunction) модемной линией DTR. С остальными значениями не приходилось сталкиваться. |
fDsrSensitivity | 1 | "1" — если в процессе приема DSR установится в "0" — все принимаемые байты игнорируются драйвером Windows. |
fTXContinueOnXoff | 1 | "1" — передача не прерывается при переполнении приемного буфера (наличии в нем больше чем XoffLim символов), драйвер при этом передает символ XonChar. "0" — при переполнении приемного буфера (наличии в нем больше чем XoffLim символов) передача будет прервана (драйвер при этом передает символ XoffChar) до тех пор, пока в приемном буфере не останется меньше XonLim символов и драйвер не передаст символ XonChar для возобновления потока принимаемых данных. Никогда не пользовался этим управлением потоками, вернее не оказывалось подобной ситуации (всегда "0"). |
fOutX | 1 | "1" — передача останавливается при приеме символа XoffChar, и возобновляется при приеме символа XonChar. Никогда не пользовался этим управлением потоками, вернее не оказывалось подобной ситуации (всегда "0"). |
fInX | 1 | "1" — передает символ XoffChar, когда в приемном буфере находится более XoffLim, и XonChar, когда в приемном буфере остается менее XonLim символов. Никогда не пользовался этим управлением потоками, вернее не оказывалось подобной ситуации (всегда "0"). |
fErrorChar | 1 | "1" — если есть ошибка при проверке на четность и бит fParity в Flags (см. выше) равен "1", то выполняется замена принятого символа на ErrorChar (см. далее). |
fNull | 1 | "1" — если принят байт "00h", то он отбрасывается и не помещается во входной буфер. Всегда ставил "0", поскольку работал с устройствами, которые передавали байты в диапазоне 00h-FFh. |
fRtsControl | 2 | "00" (RTS_CONTROL_DISABLE) — запрещает "вручную" управлять (EscapeCommFunction) модемной линией RTS. "01" (RTS_CONTROL_ENABLE) — разрешает "вручную" управлять (EscapeCommFunction) модемной линией RTS. "10" (RTS_CONTROL_HANDSHAKE) — драйвер устанавливает сигнал RTS, когда приемный буфер заполнен менее, чем на половину, и сбрасывает, когда буфер заполняется более чем на три четверти. "11" (RTS_CONTROL_TOGGLE) — сигнал RTS устанавливается, когда есть данные для передачи. При передаче последнего байта — RTS сбрасывается. |
fAbortOnError | 1 | "1" — драйвер прекращает все операции чтения/записи для порта при возникновении ошибки. Работа драйвера возобновится после устранения причины ошибки и вызова функции ClearCommError. |
fDummy2 | 17 | "00000000000000000" — зарезервирован. |
|
- wReserved : WORD — не используется, должно быть установлено в 0.
- XonLim: WORD — задает минимальное число символов в приемном буфере перед посылкой драйвером символа XonChar (см. далее).
- XoffLim: WORD — задает максимальное число символов в приемном буфере перед посылкой драйвером символа XoffChar (см. далее).
- ByteSize : BYTE — определяет число информационных бит в передаваемых и принимаемых байтах. Запрещенные значения:
— 5 в комбинации с StopBits=TWOSTOPBITS, —6,7,8 в комбинации с StopBits=ONE5STOPBITS.
- Parity: BYTE — определяет выбор схемы контроля четности. Данное поле должно содержать одно из следующих значений:
— EVENPARITY (2 по Windows.pas) дополнение до четности; — MARKPARITY (3) бит четности всегда 1; — NOPARITY (0) бит четности отсутствует; — ODDPARITY (1) дополнение до нечетности; — SPACEPARITY (4) бит четности всегда 0.
- StopBits: BYTE — определяет количество стоповых бит. Поле может принимать следующие значения:
— ONESTOPBIT (0 по Windows.pas) один стоповый бит; — ONE5STOPBIT (1) полтора стоповых бита; — TWOSTOPBIT (2) два стоповых бита.
- XonChar: CHAR — задает символ Xon используемый для регулирования приема/передачи.
- XoffChar: CHAR — задает символ Xoff используемый для регулирования приема/передачи.
- ErrorChar: CHAR — задает символ используемый для замены символа с ошибкой по четности.
- EofChar: CHAR — задает символ при приеме которого генерируется эвент EV_RXFLAG (см. далее).
- EvtChar: CHAR — зарезервирован.
- wReserved1: WORD — зарезервирован.
Назначение этой структуры — определить таймауты выполнения функций ReadFile и WriteFile. Они актуальны в том случае, например, когда передача регулируется состояниями модемных линий DSR и CTS, или с внешнего устройства идет непрерывный поток байт. Т.е., чтобы вызов этих функций не "завешивал" выполнение программы при, допустим, отсутствии CTS при передаче байт вызовом WriteFile, или приеме непрерывного потока байт при вызове ReadFile. Я всегда устанавливал значения этой структуры таким образом, чтобы функции ReadFile и WriteFile возвращали результат немедленно, а именно ReadFile — все имеющиеся байты в приемном буфере (в том числе — 0 байт в случае их отсутствия), а WriteFile тем самым показывая, что все байты помещены в буфер передачи драйвера, а уже тот в "фоновом" режиме записывал их в микросхему порта. В самом начале я вообще не трогал эту структуру, пока в одном из комплексов не проявилась неприятная вещь — за сутки системное время отставало до двух часов (комплекс работал в круглосуточном режиме). Причем, то же самое ПО на другой машине (другим релизом Windows и, соответственно, драйвера порта) к такому результату не приводило. Внимательное изучение Help натолкнуло меня на догадку, что разные версии драйверов по-разному инициализируют эту структуру. Правда, это было еще на W9x.
Для чтения и записи таймаутов используются GetCommTimeouts и SetCommTimeouts соответственно.
Поля структуры TCOMMTIMEOUTS имеют следующее назначение:
- ReadIntervalTimeout : DWORD — максимальный временной промежуток (в мсек), допустимый между двумя принимаемыми байтами. Если интервал между двумя последовательными байтами превысит заданное значение, операция чтения ReadFile завершается с возвратом всех данных из приемного буфера. Нулевое значение данного поля означает, что данный тайм-аут не используется. Комбинация:
ReadIntervalTimeout := MAXDWORD; ReadTotalTimeoutMultiplier := 0; ReadTotalTimeoutConstant := 0; приводит к тому, что функция ReadFile возвращает немедленно все имеющиеся байты в приемном буфере. Я всегда пользуюсь такими настройками в своем подходе.
- ReadTotalTimeoutMultiplier : DWORD — задает множитель (в мсек), используемый для вычисления общего тайм-аута операции чтения. Для каждой операции чтения данное значение умножается на количество запрошенных для чтения байт.
- ReadTotalTimeoutConstant : DWORD — задает константу (в мсек), используемую для вычисления общего тайм-аута операции чтения. Для каждой операции чтения данное значение плюсуется к результату умножения ReadTotalTimeoutMultiplier на количество запрошенных для чтения байт. Нулевое значение полей ReadTotalTimeoutMultiplier и ReadTotalTimeoutConstant означает, что общий тайм-аут для операции чтения не используется.
- WriteTotalTimeoutMultiplier : DWORD — задает множитель (в мсек), используемый для вычисления общего тайм-аута операции записи. Для каждой операции записи данное значение умножается на количество записываемых байт.
- WriteTotalTimeoutConstant : DWORD — задает константу (в мсек), используемую для вычисления общего тайм-аута операции записи. Для каждой операции записи данное значение прибавляется к результату умножения WriteTotalTimeoutMultiplier на количество записываемых байт. Нулевое значение полей WriteTotalTimeoutMultiplier и WriteTotalTimeoutConstant означает, что общий тайм-аут для операции записи не используется.
Досконально не разбирался с этой структурой. Сам всегда использовал только поле cbInQue.
Поля структуры TCOMSTAT имеют следующее назначение:
- Flags : DWORD — набор флагов (перечислены от младшего к старшему биту), определяющих:
Название по Help | Разм., бит | Назначение |
fCtsHold | 1 | "1" — передача остановлена, порт ожидает установки CTS в "1". |
fDsrHold | 1 | "1" — передача остановлена, порт ожидает установки DSR в "1". |
fRlsdHold | 1 | "1" — передача остановлена, порт ожидает установки RLSD в "1". |
fXoffHold | 1 | "1" — передача остановлена, порт принял XoffChar, ожидает приема XonChar. |
fXoffSent | 1 | "1" — передача остановлена (из-за переполнения буфера приема), порт передал XoffChar. |
fEoff | 1 | "1" — принят EofChar. |
fTxim | 1 | "1" — в буфере передачи есть байты записанные с помощью функции TransmitCommChar. |
fReserved | 25 | зарезервировано. |
|
- cbInQue : DWORD — количество байт в буфере приема.
- cbOutQue : DWORD — количество байт в буфере передачи.
Маленькое замечание — в Windows.pas объявление несколько иное (на мой взгляд, различие — несущественное), а именно:
- Flags : Byte — TComStateFlags;
- Reserved: array[0..2] of Byte;
- cbInQue: DWORD;
- cbOutQue: DWORD.
Еще раз оговорюсь, что всегда работаю в асинхронном режиме, поэтому экспериментировал только в рамках этого режима и приводимые значения параметров функций относятся только к этому режиму. Как будут себя вести они же при синхронном режиме времени экспериментировать просто не было.
Собственно именно с помощью нее открывается порт, т.е. получаем хэндл порта для дальнейшей работы.
Объявление этой функции таково.
function CreateFile(lpFileName: PChar; dwDesiredAccess, dwShareMode: DWORD;
lpSecurityAttributes : LPSECURITY_ATTRIBUTES;
dwCreationDistribution, dwFlagsAndAttributes: DWORD;
hTemplateFile : THandle) : THandle,
где:
- lpFileName — указатель на строку нуль-терминированную. Обращаю внимание, что при вызове корректнее писать вот так '\\.\COM1', иначе в W2k столкнулся с проблемой открытия порта с номером выше 4, например, при указании 'COM9';
- dwDesiredAccess — режим открытия порта (запись, чтение, запись/чтение);
- dwShareMode — режим "расшаривания", всегда использовал 0 для монопольного открытия;
- lpSecurityAttributes — структура неких атрибутов открытия, всегда использовал nil;
- dwCreationDistribution — всегда OPEN_EXISTING для портов;
- dwFlagsAndAttributes — всегда FILE_FLAG_OVERLAPPED для асинхронного режима;
- hTemplateFile — всегда 0 для портов.
Пример вызова —
hCid := CreateFile('\\.\COM2',GENERIC_READ or GENERIC_WRITE, 0, nil,
OPEN_EXISTING, FILE_FLAG_OVERLAPPED,0).
Функция возвращает хэндл порта, для использования во всех остальных функциях (обычно — первый параметр). Соответственно, без нее — никак.
Функция закрытия порта.
function CloseHandle(hPort: THandle) : boolean;
где:
- hPort : THandle — хэндл порта от CreateFile.
Функции необходимы для чтения/записи структуры DCB порта.
Объявления этих функций абсолютно одинаковы:
function G(S)etCommState(hPort: THandle; DCB : TDCB) : boolean;
где:
- hPort : THandle — хэндл порта от CreateFile;
- DCB : TDCB — указатель на структуру DCB.
Лучше всего объявить переменную типа TDCB, проинициализировать ее вызовом GetCommState, изменить нужные параметры в DCB, а потом вызвать SetCommState.
Функции необходимы для разрешения возникновения необходимых эвентов от порта, а фактически (это делает драйвер порта) — для программирования маски прерываний микросхемы порта. Перечень возможных эвентов таков —
- EV_RXCHAR — эвент о принятии хотя бы одного байта;
- EV_RXFLAG — эвент о принятии байта равного EofChar из DCB (никогда не пользовался со времен 16-разрядного Windows, где "пропустить" эвент — было "раз плюнуть");
- EV_TXEMPTY — эвент говорящий лишь о том, что в порт можно еще что-нибудь записать, при этом отнюдь не о том, что переданы все байты от предыдущей записи. Даже когда не было буфера FIFO в микросхеме порта — это прерывание возникало сразу после записи первого байта в порт, но это из опыта программирования под DOS и программирования микроконтроллеров, типа 51 машины;
- EV_CTS, EV_DSR — эвенты при изменении состояния CTS и DSR;
- EV_RLSD — этот эвент для работы с модемами появление/пропадание несущей;
- EV_BREAK — эвент при установке линии Rx в состояние break, вызванное вызовом на другом конце, если это компьютер, функции SetCommBreak для установки линии Тх в состояние break;
- EV_ERR — эвент при ошибке в приеме, например из-за нарушения четности;
- EV_RING — эвент о наличии сигнала на 9-ой ножке DB9 (для модемов — это наличие сигнала вызова по телефонной линии).
Объявления этих функций абсолютно одинаковы:
function G(S)etCommMask(hPort: THandle; Events : DWORD) : boolean;
где:
- hPort : THandle — хэндл порта от CreateFile;
- Events : DWORD — указатель на маску эвентов.
Функции необходимы для чтения и записи настроек таймаутов порта.
Объявления этих функций абсолютно одинаковы:
function G(S)etCommTimeouts(hPort: THandle; lpCommTimeouts : TCommTimeouts) : boolean;
где:
- hPort : THandle — хэндл порта от CreateFile;
- lpCommTimeouts : TCommTimeouts — указатель на TCommTimeouts.
Функция для чтения текущего состояния регистра состояния порта (состояния модемных линий).
function GetCommModemStatus(hPort: THandle; var lpModems : DWORD) : boolean;
где:
- hPort : THandle — хэндл порта от CreateFile;
- lpModems : DWORD — переменная с состояниями модемных линий.
После вызова функции lpModems содержит комбинацию следующих констант:
- MS_CTS_ON — состояние линии CTS;
- MS_DSR_ON — состояние линии DSR;
- MS_RING_ON — состояние 9 ножки DB9 порта (наличие вызывного сигнала в телефонной линии);
- MS_RLSD_ON — наличие/отсутствие несущей модемной связи.
Функция необходима для установки размеров внутренних буферов приема и передачи драйвера порта.
function SetupComm(hPort: THandle; lpIn, lpOut : DWORD) : boolean;
где:
- hPort : THandle — хэндл порта от CreateFile;
- lpIn : DWORD — длина буфера для чтения;
- lpOut : DWORD — длина буфера для записи.
Необходимо устанавливать размер буфера для записи заведомо больше, чем максимальное количество байт, которые Вы будете записывать в порт однократным вызовом WriteFile.
Функция необходима для сброса ошибок, получения текущего кода ошибок и чтения структуры состояния порта.
function ClearCommError(hPort: THandle; lpErrors : DWORD; lpCommStat : PComStat) : boolean;
где:
- hPort : THandle — хэндл порта от CreateFile;
- lpErrors : DWORD — указатель на маску ошибок;
- lpCommStat : PComStat — указатель на TComStat.
В 16-разрядных Windows вызов функции был обязателен в процедуре обработки сообщения CommNotify. В 32-разрядных Windows экспериментов не ставил, но всегда вызывал после WaitCommEvent.
Функция возврата (ожидания) эвентов от СОМ порта.
function WaitCommEvent(hPort: THandle; lpMask : DWORD; lpOver : POverlapped ) : boolean;
где:
- hPort : THandle — хэндл порта от CreateFile;
- lpMask : DWORD — указатель на маску эвентов;
- lpOver : POverlapped — указатель на TOverlapped.
При удачном завершении функции переменная lpMask содержит маску полученных эвентов. При неудаче и при GetLastError=ERROR_IO_PENDING для получения маски эвентов необходимо вначале вызвать Wait-функцию (например WaitForSingleObject) на ожидание установки hEvent структуры lpOver (параметр WaitCommEvent ) в сигнальное состояние. А потом, при удачном ожидании вызвать GetOverlappedResult, после выполнения которой lpMask содержит маску полученных эвентов. Так гласит MSDN.
Функции необходимы для чтения и записи информации из и в порт.
function Read(Write)File(hPort: THandle; lpBuff : pointer; lpCnt : DWORD;
var lpCntByte : DWORD; lpOver : POverlapped) : boolean;
где:
- hPort : THandle — хэндл порта от CreateFile;
- lpBuff : pointer — указатель на буфер для записи принятых байт;
- lpCnt : DWORD — количество байт для чтения (записи);
- lpCntByte : DWORD — количество считанных в lpBuff байт (записанных в порт из lpBuff);
- lpOver : POverlapped — указатель на TOverlapped.
В случае удачного завершения переменная lpCntByte содержит количество байт, считанных в lpBuff в случае ReadFile или записанных в порт из lpBuff в случае WriteFile.
Функция предназначена для немедленной передачи байта портом. Отличается от WriteFile тем, что запись ведется не в конец буфера передачи драйвера порта, а в его начало для немедленной записи в FIFO передачи микросхемы.
function TransmitCommChar(hPort: THandle; Chr : Char) : boolean;
где:
- hPort : THandle — хэндл порта от CreateFile;
- Chr : Char — символ для немедленной передачи.
Никогда не пользовался, поэтому не могу прокомментировать.
Не буду претендовать на истину в первой инстанции, поскольку меня зовут Василий, а не ….(сами догадались), но считаю свой подход наиболее правильным и универсальным.
Перечислю основные "аксиомы".
- Работа с портом только в асинхронном режиме, дабы не "подвисать" из-за особенностей конкретного устройства "висящего" на СОМ-порту;
- Открытие порта с нужными настройками осуществляется сразу по старту программы, дабы не попасть в ситуацию "нужен порт, а его какая-то сволочь забрала под себя". Если возможность выбора порта и его настроек заложена в интерфейсе пользователя, то вначале выполняется закрытие порта, а затем открытие его с пользовательскими настройками (естественно с проверкой успешности операции);
- Закрытие порта — перед закрытием приложения;
- Запись информации в порт осуществляется только в основном потоке. Возможен вариант записи в порт в дочернем потоке в случае, когда одна транзакция обмена состоит из нескольких фаз по жесткому алгоритму, но первая фаза записи — все равно должна быть в основном потоке;
- Обработка всех эвентов от порта и, связанные с этим, действия выполняются в дочернем потоке (чтение принятых байт, запрос состояния модемных линий и т.п.). Передача информации, полученной в дочернем потоке, основному потоку только посредством PostMessage;
- Анализ текущей транзакции обмена с устройством проводится в основном потоке, и только по принципу — "передал, взвел таймер", "принял, опустил таймер, проанализировал полученные данные".
type
TModems = packed record
DSR,CTS,RING,RLSD : boolean;
end;
Var
cId : THandle; //дескриптор порта
DCB : TDCB; //DCB порта
TimeOuts : TCommTimeouts; // таймауты порта
Stat : TComStat; //статус порта
Modems : TModems; //состояние модемных линий
RecivBuff : array[0..255] of byte; //буфер принимаемых байт
CntByte : integer; //количество принятых байт в буфере
Terminated : boolean; //флажок для корректного закрытия порта
Пример процедуры инициализации порта.
function InitsComm(Num : integer) : boolean;
var
ThreadId : Dword;
begin
Result := False;
//получаем дескриптор порта в асинхронном режиме
cId := CreateFile(PChar('\\.\COM'+ IntToStr(Num),
GENERIC_READ or GENERIC_WRITE,
0,nil,OPEN_EXISTING,
FILE_FLAG_OVERLAPPED,0);
if cId = INVALID_HANDLE_VALUE then Exit;
//устанавливаем маску эвентов (фактически маску прерываний)
//в данном случае будем иметь возникновение эвентов по принятию
//хотя бы одного байта и возможности записи в порт еще байт(ов)
if not (SetCommMask(cId,EV_RXCHAR or EV_TXEMPTY) and
//устанавливаем размер внутренних буферов приема-передачи в //драйвере порта
SetupComm(cId,256,256) and
//очищаем буферы приема-передачи (в принципе необязательно)
PurgeComm(cId,PURGE_TXABORT or PURGE_RXABORT or
PURGE_TXCLEAR or PURGE_RXCLEAR) and
//получаем текущее DCB порта
GetCommState(cId,DCB))
then begin
CloseHandle(cId);
Exit;
end;
//изменяем DCB
DCB.BaudRate := 9599;//реальная скорость будет 9600
DCB.ByteSize := 8;
DCB.Parity := NoParity;
DCB.StopBits := OneStopBit;
//выполняем настройку порта с новым DCB
if not SetCommState(cId,DCB) then begin
CloseHandle(cId);
Exit;
end;
//получаем текущие параметры таймаутов
GetCommTimeouts(cId,TimeOuts);
//настраиваем текущие параметры таймаутов таким образом,
//чтобы ReadFile и WriteFile возвращали значения немедленно
TimeOuts.ReadIntervalTimeout := MAXDWORD;
TimeOuts.ReadTotalTimeoutMultiplier := 0;
TimeOuts.ReadTotalTimeoutConstant := 0;
TimeOuts.WriteTotalTimeoutMultiplier := 0;
TimeOuts.WriteTotalTimeoutConstant := 0;
//выполняем настройку порта с новыми таймаутами
if not SetCommTimeouts(cId,TimeOuts) then begin
CloseHandle(cId);
Exit;
end;
//опускаем флаг завершения дочернего потока
Terminated := False;
//стартуем дочерний поток (функция потока - ReadsComm)
//для обработки эвентов порта и устанавливаем приоритет
CommThread := CreateThread(nil,0,@ReadsComm,nil,0,ThreadID);
if CommThread = 0 then begin
CloseHandle(cId);
Exit;
end;
SetThreadPriority(CommThread,8);
end;
Result := True;
end;
После успешного выполнения этой функции имеем полное право записывать в порт, используя его хэндл cId.
Приведу пример реализации функции с подробными комментариями.
function ReadsComm : longint; stdcall;
var
//честно говоря, так и не понял причину, но переменную,
//типа TOverlapped, если она используется, нужно
//объявлять в каждой процедуре(функции)
Ovr : TOverlapped;
Events : array[0..1] of THandle;
Buff : array[0..255] of byte;
Trans, Signal, Mask, Kols, ModemState : DWord;
i : integer;
Reciv : pointer;
begin
Result := 0;
//выполняем инициализацию структуры для асинхронного режима
FillChar(Ovr,SizeOf(TOverlapped),0);
Ovr.hEvent := CreateEvent(nil,TRUE,FALSE,#0);
Events[0] := Ovr.hEvent;
//стартуем бесконечный цикл обработки эвентов порта
while not Terminated do begin
Mask := 0;
if not WaitCommEvent(cId,Mask,@Ovr) then begin
if ERROR_IO_PENDING = GetLastError() then begin
//это случай, когда ожидание переводится в фоновый режим
//и для получения маски эвентов нужно дождаться
//выставления эвента Ovr.hEvent в сигнальное состояние
//и вызвать GetOverlappedResult
if WaitForSingleObject(Ovr.hEvents,INFINITE)= WAIT_OBJECT_0 then
GetOverlappedResult(сId,Ovr,Trans,False);
end;
end;
if Terminated then break;
//выполняем очищение ошибок с получением статуса порта
ClearCommError(cId,Errs,@Stat);
//проверяем маску эвентов на эвент принятия байт
If (Mask and EV_RXCHAR) = EV_RXCHAR then begin
if Terminated then break;
//пытаемся прочитать максимальное количество байт
ReadFile(cId,Buff,256,Kols,@Ovr);
if Terminated then break;
//если хотя бы один байт считан - возможен вариант,
//что байты вызвавшие эвент уже были считаны ранее
//порт же не ждет, пока поток получит ресурсы
if Kols > 0 then begin
//переписываем считанную информацию в промежуточный буфер
Move(Buff[0],RecivBuff[CntByte],Kols);
Inc(CntByte,Kols);
//если накоплено больше 48 байт (см. после функции пояснение)
//генерим новый поинтер на принятую информацию
//и переписываем считанную информацию
if CntByte > 48 then begin
GetMem(Reciv,sizeof(CntByte));
Move(RecivBuff[0],Byte(Reciv^),CntByte);
//посылаем сообщение основному потоку
PostMessage(Handle,cmRxByte,Integer(Reciv),Kols);
CntByte := 0;
end;
end;
//проверяем маску эвентов на эвент изменения модемных линий
if ((Mask and EV_DSR) = EV_DSR) or
((Mask and EV_CTS) = EV_CTS) or
((FMask and EV_RING) = EV_RING) or
((FMask and EV_RLSD) = EV_RLSD) then begin
if Terminated then break;
//получаем состояние модемных линий
GetCommModemStatus(cId,ModemState);
//генерим новый поинтер на принятую информацию
//и переписываем туда состояние модемных линий
GetMem(Reciv,sizeof(TModems));
TModems(Reciv^).DSR := (ModemState and MS_DSR_ON) =
MS_DSR_ON;
TModems(Reciv^).CTS := (ModemState and MS_CTS_ON) =
MS_CTS_ON;
TModems(Reciv^).RING := (ModemState and MS_RING_ON) =
MS_RING_ON;
TModems(Reciv^).RLSD := (ModemState and MS_RLSD_ON) =
MS_RLSD_ON;
//посылаем сообщение основному потоку
PostMessage(Handle,cmModems,Integer(Reciv),0);
end;
//далее по тому же принципу, что и изменение модемных линий
//можно написать обработку эвентов EV_BREAK и EV_ERR
//...........
//...........
end;
CloseHandle(Ovr.hEvent);
end;
Маленькое, но существенное замечание. Старайтесь первичную обработку информации от порта осуществлять в функции потока. Поясню свою мысль. Любой обмен двоичной информацией подразумевает некую синхронизацию в этой информации, т.е. любое устройство передает данные некими пакетами с полями — преамбула, информация, окончание пакета. Соответственно, постарайтесь в функции потока осуществлять синхронизацию, проверку на корректность принятых данных, а затем уже генерить указатель на информацию, его заполнение и передачу сообщения. В процессе отладки точку останова нельзя ставить в теле функции потока (если порт шлет непрерывно, то возможны катаклизмы — Дельфи сам то остановится, но драйвер порта не останавливается, соответственно возможны потери данных из-за переполнения буфера драйвера). В примере основному потоку информация передается порциями не менее 48 байт, и вот почему. Те, кто с портом работал в 16-разрядных Windows знает, что если по порту информация идет на скорости выше 19200 и ее средний темп также выше, то приложение просто захлебывалось от сообщений CommNotify (точное название не помню). Т.е. механизм был такой: есть прерывание — драйвер послал сообщение, всё. Та же самая ситуация будет и в 32-разрядах, если Вы будете слать сообщения после каждого ReadFile в потоке обработке порта.
Таким образом, в процессе отладки посылайте основному потоку по 10-20 байт, в процедуре обработки отладьте синхронизацию принимаемых данных. Потом код процесса синхронизации перенесите в дочерний поток и уже посылайте основному потоку только непосредственно информационную часть принимаемого потока.
Еще раз повторюсь, что это для случая, когда обмен с устройством идет не в диалоговом режиме, и средний темп передачи данных устройством выше 19200.
Поскольку для обработки эвентов порта используется дочерний поток, то функция закрытия порта должна иметь вид.
function CloseComm : boolean;
begin
//устанавливаем флаг завершения
Terminated := True;
//если в этот момент поток находится в WaitCommEvent
// - принудительно заставляем завершиться
SetCommMask(cId,0);
//передаем ресурсы системе, чтобы поток получил ресурсы
Sleep(10);
//закрываем хэндл порта
CloseHandle(cId);
Result := True;
end;
Пробовал использовать TerminateThread, но это приводило к некорректному завершению потока (о чем и написано в хэлпе) — без освобождения всех ресурсов, выделенных потоку при старте.
Этот обработчик должен находится в юните основной формы.
Приведу пример обработчика принятых байт.
const
cmRxByte = wmUser;
type
TfmMain = class(TForm)
.....
private
procedure ObrMess(var Msg : TMessage); message cmRxByte;
.....
end;
.....
procedure TfmMain.ObrMess(var Msg : TMessage);
var
i, Cnt : integer;
S : string;
Buff : pointer;
begin
//записываем принятые байты в S
Buff := Msg.WParam;
Cnt := Msg.LParam;
S := '';
for i := 0 to Cnt-1 do begin
if Byte(Buff^) >= $20
then S := S + Chr(Buff^)
else S := S + IntToHex(Byte(Buff^),2) + '_';
inc(Buff);
end;
//добавляем S в Memo
Memo1.Lines.Add(S);
//освобождаем память, выделенную в дочернем потоке
FreeMem(Pointer(Msg.WParam));
end;
В принципе в этом обработчике должен диссэблироваться таймер слежения за нормальным обменом с устройством.
Приведу примерную реализацию функции записи в порт. Можно воспользоваться API-функцией WriteFile, но тогда в возвращаемом функцией значении записанных байт в асинхронном режиме всегда будет 0. Поясню назначение параметров.
- hComm — хэндл порта
- Buff — указатель на массив байт для передачи
- Count — количество байт для передачи
- Writed — количество байт, записанных в буфер передачи драйвера
- Wait — если = True, то Writed = Count (других вариантов не встречал), в противном случае Writed = 0, для асинхронного режима.
function WriteComm(hComm : THandle; Buff : array of byte; Count : integer; Wait : boolean) : integer;
var
//честно говоря, так и не понял причину, но переменную,
//типа TOverlapped, если она используется, нужно
//объявлять в каждой процедуре(функции)
Ovr : TOverlapped;
Code : DWord;
//эта переменная не нужна
S : string;
begin
//инициализируем TOverlapped структуру
FillChar(Ovr,SizeOf(TOverlapped),0);
Ovr.hEvent := CreateEvent(nil,TRUE,FALSE,#0);
//пытаемся записать
if not WriteFile(hComm,Buff,Count,Result,@Ovr) and Wait
then begin
if (GetLastError() = ERROR_IO_PENDING) and
(WaitForSingleObject(Ovr.hEvent,INFINITE)= WAIT_OBJECT_0)
then GetOverlappedResult(сId,Ovr,Result,False);
end;
CloseHandle(Ovr.hEvent);
end;
Собственно, в заключение и сказать нечего. Разве что — желаю удачи в написании приложений, работающих с устройствами, подключенными к СОМ-портам. Мне кажется, я осветил все нюансы этого "многотрудного" дела.
За многолетнюю работу по написанию подобных программ я убедился, как тяжело интерфейсчику договориться с аппаратчиком. Еще хорошо, когда он сидит напротив или в соседней комнате. Хуже когда есть готовое устройство, и в нем изменить ничего нельзя. Хорошо, если есть подробное описание протокола работы с ним. И уж совсем плохо, когда есть только "фирменная" программа по обслуживанию устройства (как правило DOS-овская).
В любом случае, для всех серьезно занимающихся подобными проблемами советую поиметь сниффер порта. Он — отличный "третейский судья" в тяжбе "то ли я — тупой, то ли — этот чертов ящик, соединенный с СОМ-портом".
Кстати имеется таковой. Пакет состоит из двух драйверов (для W9x и W2k соответственно) и непосредственно просмотрщик результатов. Пакет был написан чисто от безысходности, поскольку мало того, что протокол работы с устройством неизвестен, так и еще к компьютеру с ПО и устройством физически подойти было нельзя (для других случаев у меня есть коробочка, которая ставится в разрыв кабеля связи и посылает все события происходящие в порту на отдельный выход и ничего неспособная сказать о том, как настроен порт). Боже упаси нарушить нормальную работу штатного ПО. Хорошо, что этот комп сидел в локальной сети. Правда, тот пакет работал в отложенном режиме — по накопленному файлу. Разработанный же пакет — полный "сниффер на лету".
А, вот еще что. Не пытайтесь набивать код непосредственно из статьи. Возможны ошибки при компиляции. Реально 100%-работающий пример работы с портом, вернее с двумя портами одного компа, соединенными нуль-модемным кабелем прилагаю к статье. Исходники немного отличаются от приведенных, но не принципиально. Соответственно и комментариев там меньше.
В примере эмулируется работа с неким устройством (его эмулирует СОМ2), подключенного к СОМ1. Обмен ведется пакетами следующего вида — преамбула (4 байта синхронизации и 1 байт количества байт в информационной части пакета) и информационная часть (либо 5 байт информации и байт контрольной суммы (COM2), либо 10 байт информации (COM2), либо 0-2 байта информации (СОМ1 и СОМ2)). Для ясности будем считать, что устройство — СОМ2, а СОМ1 — это РС.
Устройство начинает передавать информационные пакеты после получения пакета без информационной части, который оно возвращает обратно в качестве подтверждения.
Получив подтверждение, обработчик основного потока взводит таймер циклической посылки пакетов от устройства.
При смене типа информации — РС посылает соответствующий пакет устройству с требованием смены типа информации. Устройство не отвечает на эту команду, а просто начинает передавать указанную информацию.
При нажатии кнопки "запросить статус" — РС посылает соответствующий пакет устройству с требованием сообщить статус. Устройство однократно передает пакет со статусом. Статус — это word, установку бит которого можно сделать чекбоксами, расположенными ниже.
Можно поуправлять двумя модемными концами.
Еще раз прошу прощения, но в исходниках примера комментариев маловато.
Ну еще раз — всем удачи!
К материалу прилагаются файлы:
[COM-порт]
Обсуждение материала [ 24-11-2015 06:04 ] 88 сообщений |