Windows работа с последовательным портом

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

Открытие порта[править]

С последовательными и параллельными портами в Win32 работают как с файлами. Для открытия порта используется функция CreateFile. Эта функция предоставляется Win32 API. Ее прототип выглядит так:

  HANDLE CreateFile(
     LPCTSTR               lpFileName,
     DWORD                 dwDesiredAccess,
     DWORD                 dwShareMode,
     LPSECURITY_ATTRIBUTES lpSecurityAttributes,
     DWORD                 dwCreationDistribution,
     DWORD                 dwFlagsAndAttributes,
     HANDLE                hTemplateFile
  );

lpFileName[править]

Указатель на строку с именем открываемого или создаваемого файла. Формат этой строки может быть очень «хитрым». В частности можно указывать сетевые имена для доступа к файлам на других компьютерах. Можно открывать логические разделы или физические диски и работать в обход файловой системы.

Последовательные порты имеют имена «COM1», «COM2», «COM3», «COM4», «COM5», «COM6», «COM7», «COM8», «COM9». Для доступа к портам, чей номер больше 9, необходимо указывать имя порта как «\\.\COMx», где x — номер порта. Например, «\\.\COM72» (в нотации языка C/C++ строка будет выглядеть «\\\\.\\COM72»). Такой синтаксис подходит для любого номера порта. Точно так же они назывались в MS-DOS. Параллельные порты называются «LPT1», «LPT2» и так далее.

dwDesiredAccess[править]

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

  • 0 Опрос атрибутов устройства без получения доступа к нему.
  • GENERIC_READ Файл будет считываться.
  • GENERIC_WRITE Файл будет записываться.
  • GENERIC_READ|GENERIC_WRITE Файл будет и считываться и записываться.

dwShareMode[править]

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

lpSecurityAttributes[править]

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

dwCreationDistribution[править]

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

dwFlagsAndAttributes[править]

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

hTemplateFile[править]

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

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

Закрытие порта[править]

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

BOOL CloseHandle(
     HANDLE hObject
     );

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

Пример открытия/закрытия на языке C[править]

   #include <windows.h>
   //. . .
   HANDLE Port;
   //. . .
   Port = CreateFile(L"\\\\.\\COM2", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
   if (Port == INVALID_HANDLE_VALUE) {
      MessageBox(NULL, "Невозможно открыть последовательный порт", "Error", MB_OK);
      ExitProcess(1);
   }
   //. . .
   CloseHandle(Port);
   //. . .

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

Структура DCB[править]

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

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

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

DCBlength[править]

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

BaudRate[править]

Скорость передачи данных. Возможно указание следующих констант: 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 снимается при открытии порта. У открытого порта может быть изменён функцией EscapeCommFunction.
  • DTR_CONTROL_ENABLE          Сигнал DTR устанавливается при открытии порта. У открытого порта может быть изменён функцией EscapeCommFunction.
  • DTR_CONTROL_HANDSHAKE   Сигнал DTR автоматически устанавливается/снимается в ходе работы с портом. Не может быть изменён функцией EscapeCommFunction.

fDsrSensitivity[править]

Задает чувствительсть коммуникационного драйвера к состоянию линии [[|DSR]]. Если это поле равно TRUE, то все принимаемые данные игнорируются драйвером (коммуникационный драйвер расположен в операционной системе), за исключением тех, которые принимаются при установленом сигнале DSR.

fTXContinueOnXoff[править]

Задает, прекращается ли передача при переполнении приемного буфера и передаче драйвером символа XoffChar. Если это поле равно TRUE, то передача продолжается, несмотря на то, что приемный буфер содержит более XoffLim символов и близок к переполнению, а драйвер передал символ XoffChar для приостановления потока принимаемых данных. Если поле равно FALSE, то передача не будет продолжена до тех пор, пока в приемном буфере не останется меньше XonLim символов и драйвер не передаст символ XonChar для возобновления потока принимаемых данных. Таким образом это поле вводит некую зависимость между управлением входным и выходным потоками информации.

fOutX[править]

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

fInX[править]

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

fErrorChar[править]

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

fNull[править]

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

fRtsControl[править]

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

  • RTS_CONTROL_DISABLE          Сигнал RTS снимается при открытии порта. У открытого порта может быть изменён функцией EscapeCommFunction.
  • RTS_CONTROL_ENABLE           Сигнал RTS устанавливается при открытии порта. У открытого порта может быть изменён функцией EscapeCommFunction.
  • RTS_CONTROL_HANDSHAKE   Сигнал RTS автоматически устанавливается/снимается в ходе работы с портом. Не может быть изменён функцией EscapeCommFunction. Сигнал RTS устанавливается, когда приемный буфер заполнен менее, чем на половину, и снимается, когда буфер заполняется более чем на три четверти.
  • RTS_CONTROL_TOGGLE          Задаёт, что сигнал RTS установлен, когда есть данные для передачи. Когда все символы из передающего буфера переданы, сигнал снимается.

fAbortOnError[править]

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

fDummy2[править]

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

wReserved[править]

Не используется, должно быть установлено в 0.

XonLim[править]

Задает минимальное число символов в приемном буфере перед посылкой символа XON.

XoffLim[править]

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

ByteSize[править]

Определяет число информационных бит в передаваемых и принимаемых байтах. Число информационных бит может быть в диапазоне от 4 до 8.

Parity[править]

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

  • EVENPARITY     Дополнение до четности
  • MARKPARITY     Бит четности всегда 1
  • NOPARITY         Бит четности отсутствует
  • ODDPARITY       Дополнение до нечетности
  • SPACEPARITY   Бит четности всегда 0

StopBits[править]

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

  • ONESTOPBIT     Один стоповый бит
  • ONE5STOPBIT   Полтора стоповых бита
  • TWOSTOPBITS     Два стоповых бита

XonChar[править]

Задает символ XON используемый как для приема, так и для передачи. Обычно 0x11 (17).

XoffChar[править]

Задает символ XOFF используемый как для приема, так и для передачи. Обычно 0x13 (19).

ErrorChar[править]

Задает символ, использующийся для замены символов с ошибочной четностью.

EofChar[править]

Задает символ, использующийся для сигнализации о конце данных.

EvtChar[править]

Задает символ, использующийся для сигнализации о событии.

wReserved1[править]

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

Замечания[править]

Если структура DCB содержит конфигурацию для последовательного порта, совместимого с 8250, то к значениям полей ByteSize и StopBits применяются следующие ограничения:

  • Количество информационных бит должно быть от 5 до 8.
  • Не допускается использование 5 информационных бит с 2 стоповыми битами, также как 6, 7 или 8 информационных бит с 1,5 стоповыми битами.

Заполнение структуры DCB[править]

Структура COMMTIMEOUTS[править]

winbase.h

typedef struct _COMMTIMEOUTS {
    DWORD ReadIntervalTimeout;          /* Maximum time between read chars. */
    DWORD ReadTotalTimeoutMultiplier;   /* Multiplier of characters.        */
    DWORD ReadTotalTimeoutConstant;     /* Constant in milliseconds.        */
    DWORD WriteTotalTimeoutMultiplier;  /* Multiplier of characters.        */
    DWORD WriteTotalTimeoutConstant;    /* Constant in milliseconds.        */
} COMMTIMEOUTS,*LPCOMMTIMEOUTS;

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

Чтобы операция ReadFile немедленно возвращала управление со всеми полученными данными (асинхронный режим) следует задавать следующие значения:

ReadIntervalTimeout=0xFFFFFFFF;
ReadTotalTimeoutConstant=0;
ReadTotalTimeoutMultiplier=0;

ReadTotalTimeoutMultiplier — Множитель, используемый, чтобы вычислить полный период времени простоя для операций чтения, в миллисекундах. Для каждой операции чтения, это значение умножается на затребованное число байтов, которые читаются.

ReadTotalTimeoutConstant — Константа, используемая, чтобы вычислить полный (максимальный) период времени простоя для операций чтения, в миллисекундах. Для каждой операции чтения, это значение добавляется к произведению члена структуры ReadTotalTimeoutMultiplier и прочитанного числа байтов.

Значение нуля и для члена ReadTotalTimeoutMultiplier, и для члена ReadTotalTimeoutConstant указывает, что полное время простоя не используются для операций чтения.

WriteTotalTimeoutMultiplier — Множитель, используемый, чтобы вычислить полный период времени простоя для операций записи, в миллисекундах. Для каждой операции записи, это значение умножается на число записываемых байтов.

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

Значение нуля и для члена WriteTotalTimeoutMultiplier, и для члена WriteTotalTimeoutConstant указывает, что полное время простоя не используются для операций записи.

Заполнение структуры COMMTIMEOUTS[править]

Вариант 1: (максимальная задержка при чтении и записи = TIMEOUT)

 	COMMTIMEOUTS CommTimeOuts;
 	CommTimeOuts.ReadIntervalTimeout = 0xFFFFFFFF;
 	CommTimeOuts.ReadTotalTimeoutMultiplier = 0;
 	CommTimeOuts.ReadTotalTimeoutConstant = TIMEOUT;
 	CommTimeOuts.WriteTotalTimeoutMultiplier = 0;
 	CommTimeOuts.WriteTotalTimeoutConstant = TIMEOUT;

Вариант 2: Инициализация значениями (без задержки при чтении)

 	COMMTIMEOUTS CommTimeOuts={0xFFFFFFFF,0,0,0,1500};

Пример настройки порта[править]

Структура COMMPORT[править]

Стандартный диалог настройки порта[править]

Для настройки параметров COM — порта может быть вызвано штатное окно Windows. Вызов осуществляется функцией CommConfigDialog(), которая в качестве параметров принимает имя настраиваемого порта, хендл родительского окна и указатель на структуру COMMCONFIG.
Следует отметить, что для корректного вызова окна, структура COMMCONFIG должна быть заполнена значениями заранее.
Настройку структуры можно выполнить вручную или при помощи функции GetCommConfig().
Например:

/* Получение существующих настроек */
unsigned long new_size = 0;
if (!GetCommConfig(port->handle, &port->settings,&new_size))
	goto error;
/* Вызов окна и установка новых параметров */
if (CommConfigDialog(port->name, 0, &port->settings)) {
	if (SetCommConfig(port->handle, &port->settings, port->settings.dwSize))
		return 1;
	goto error;
}

Выделение памяти для структуры COMMPORT[править]

Прием и передача данных[править]

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

CreateFile("COM1", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);

Предпоследний параметр dwFlagsAndAttributes должен быть равен 0.
После успешного открытия порта, данные могут быть считаны или записаны при помощи функций ReadFile() и WriteFile().

HANDLE port = CreateFile("COM1", GENERIC_READ | GENERIC_WRITE, 0, NULL,
			OPEN_EXISTING, 0, NULL);
unsigned char dst[1024] = {0};

unsigned long size = sizeof(dst);
if(port!= INVALID_HANDLE_VALUE) 
	if(ReadFile(port,dst,size, &size,0))
		printf("\nRead %d bytes",size);

Функция ReadFile/WriteFile осуществляет чтение/запись из файла (устройства) начиная с текущей позиции после окончания чтения обновляет указатель в файле.

BOOL ReadFile(
 HANDLE hFile, // хендл файла 
 LPVOID lpBuffer, //указатель на буфер 
 DWORD nNumberOfBytesToRead, // размер данных 
 LPDWORD lpNumberOfBytesRead, //размер считанных данных
 LPOVERLAPPED lpOverlapped //структура OVERLAPPED
);

Недостатком этого способа является то, что вызывая функцию ReadFile(), мы не знаем есть ли данные для чтения. Можно циклически проверять их наличие, но это приводит к дополнительным расходам времени ЦП.
Поэтому на практике часто удобней использовать асинхронный режим. Для этого при вызове функции CreateFile() параметр dwFlagsAndAttributes должен быть равен FILE_FLAG_OVERLAPPED.

CreateFile("COM1", GENERIC_READ | GENERIC_WRITE, 0, NULL,
		OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);

Далее, необходимо настроить реакцию порта на события при помощи функции SetCommMask() и используя функции WaitCommEvent() и WaitForSingleObject() ожидать событие или тайм аут.
Например:

const int READ_TIME = 100;
OVERLAPPED sync = {0};
int reuslt = 0;
unsigned long wait = 0, read = 0, state = 0;
	
/* Создаем объект синхронизации */
sync.hEvent = CreateEvent(NULL,TRUE,FALSE,NULL);

/* Устанавливаем маску на события порта */
if(SetCommMask(port, EV_RXCHAR)) {
	/* Связываем порт и объект синхронизации*/
	WaitCommEvent(port, &state, &sync);
	/* Начинаем ожидание данных*/	
	wait = WaitForSingleObject(sync.hEvent, READ_TIME);
	/* Данные получены */		
	if(wait == WAIT_OBJECT_0) {
		/* Начинаем чтение данных */
		ReadFile(port, dst, size, &read, &sync);
		/* Ждем завершения операции чтения */
		wait = WaitForSingleObject(sync.hEvent, READ_TIME);
		/* Если все успешно завершено, узнаем какой объем данных прочитан */
		if(wait == WAIT_OBJECT_0) 
			if(GetOverlappedResult(port, &sync, &read, FALSE)) 
				reuslt = read;
	}
}
CloseHandle(sync.hEvent);

Сброс порта[править]

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

Код для работы с COM-портом. Многострадальный, соответственно относительно простой и понятный, при этом обходит основные подводные камни. Надеюсь, может быть полезен.

tty.h[править]

 #ifndef TTY_H
 #define TTY_H

 #define NOMINMAX //иначе API windows определит макросы min и max, конфликтующие с std::max и std::min в vector
 #include <windows.h>

 #include <vector>
 #include <string>

 using namespace std;

 struct TTY {

 	TTY();
 	virtual ~TTY();

 	bool IsOK() const;
	
 	void Connect(const string& port, int baudrate);
 	void Disconnect();

 	virtual void Write(const vector<unsigned char>& data);
 	virtual void Read(vector<unsigned char>& data);
	
 	HANDLE m_Handle;

 };

 struct TTYException {
 };

 #endif

tty.cpp[править]

 #include "tty.h"
#include <iostream>
#include <assert.h>
#include <windows.h>

  using namespace std;

 static int TIMEOUT = 1000;

 TTY::TTY() {
 	m_Handle = INVALID_HANDLE_VALUE;
 }

 TTY::~TTY() {
 	Disconnect();
 }

  bool TTY::IsOK() const {
      return m_Handle != INVALID_HANDLE_VALUE;
  }

 void TTY::Connect(const string& port, int baudrate) {

 	Disconnect();
	
 	m_Handle =
 		CreateFile(
 		port.c_str(), 
 		GENERIC_READ | GENERIC_WRITE,
 		0,
 		NULL,
 		OPEN_EXISTING, 
 		FILE_ATTRIBUTE_NORMAL,
 		NULL);
	
 	if(m_Handle == INVALID_HANDLE_VALUE) {
 		throw TTYException();
 	}
	
 	SetCommMask(m_Handle, EV_RXCHAR);
 	SetupComm(m_Handle, 1500, 1500);

 	COMMTIMEOUTS CommTimeOuts;
 	CommTimeOuts.ReadIntervalTimeout = 0xFFFFFFFF;
 	CommTimeOuts.ReadTotalTimeoutMultiplier = 0;
 	CommTimeOuts.ReadTotalTimeoutConstant = TIMEOUT;
 	CommTimeOuts.WriteTotalTimeoutMultiplier = 0;
 	CommTimeOuts.WriteTotalTimeoutConstant = TIMEOUT;

 	if(!SetCommTimeouts(m_Handle, &CommTimeOuts)) {
 		CloseHandle(m_Handle);
                m_Handle = INVALID_HANDLE_VALUE;
 		throw TTYException();
 	}
	
 	DCB ComDCM;
	
 	memset(&ComDCM,0,sizeof(ComDCM));
 	ComDCM.DCBlength = sizeof(DCB);
 	GetCommState(m_Handle, &ComDCM);
 	ComDCM.BaudRate = DWORD(baudrate);
 	ComDCM.ByteSize = 8;
 	ComDCM.Parity = NOPARITY;
 	ComDCM.StopBits = ONESTOPBIT;
 	ComDCM.fAbortOnError = TRUE;
 	ComDCM.fDtrControl = DTR_CONTROL_DISABLE;
 	ComDCM.fRtsControl = RTS_CONTROL_DISABLE;
 	ComDCM.fBinary = TRUE;
 	ComDCM.fParity = FALSE;
 	ComDCM.fInX = FALSE;
        ComDCM.fOutX = FALSE;
 	ComDCM.XonChar = 0;
 	ComDCM.XoffChar = (unsigned char)0xFF;
 	ComDCM.fErrorChar = FALSE;
 	ComDCM.fNull = FALSE;
 	ComDCM.fOutxCtsFlow = FALSE;
 	ComDCM.fOutxDsrFlow = FALSE;
 	ComDCM.XonLim = 128;
 	ComDCM.XoffLim = 128;

 	if(!SetCommState(m_Handle, &ComDCM)) {
 		CloseHandle(m_Handle);
 		m_Handle = INVALID_HANDLE_VALUE;
 		throw TTYException();
 	}

 }

 void TTY::Disconnect() {

 	if(m_Handle != INVALID_HANDLE_VALUE)
        {
 	   CloseHandle(m_Handle);
           m_Handle = INVALID_HANDLE_VALUE;
 	}

 }

 void TTY::Write(const vector<unsigned char>& data) {

 	if(m_Handle == INVALID_HANDLE_VALUE) {
 		throw TTYException();
 	}

 	DWORD feedback;
 	if(!WriteFile(m_Handle, &data[0], (DWORD)data.size(), &feedback, 0) || feedback != (DWORD)data.size()) {
 		CloseHandle(m_Handle);
 		m_Handle = INVALID_HANDLE_VALUE;
 		throw TTYException();
 	}

 	// In some cases it's worth uncommenting
 	//FlushFileBuffers(m_Handle);

 }

 void TTY::Read(vector<unsigned char>& data) {

 	if(m_Handle == INVALID_HANDLE_VALUE) {
 		throw TTYException();
 	}

 	DWORD begin = GetTickCount();
 	DWORD feedback = 0;

 	unsigned char* buf = &data[0];
 	DWORD len = (DWORD)data.size();
	
 	int attempts = 3;
 	while(len && (attempts || (GetTickCount()-begin) < (DWORD)TIMEOUT/3)) {

 		if(attempts) attempts--;

 		if(!ReadFile(m_Handle, buf, len, &feedback, NULL)) {
 			CloseHandle(m_Handle);
 			m_Handle = INVALID_HANDLE_VALUE;
 			throw TTYException();
 		}

 		assert(feedback <= len);
 		len -= feedback;
 		buf += feedback;
	
 	}

 	if(len) {
 		CloseHandle(m_Handle);
 		m_Handle = INVALID_HANDLE_VALUE;
 		throw TTYException();
 	}

 }

using namespace std;

int main(int argc, char *argv[])
{
    TTY tty;
    tty.Connect("COM4",9600);
    
    for(int i=0;i<1000;i++) {
            
        std::vector<unsigned char> the_vectsor;
        the_vectsor.push_back(5);
        tty.Read(the_vectsor);
    
                              
        std::cout << (char)(the_vectsor[0]); //output text
    
    }
    
    system("PAUSE");
    return EXIT_SUCCESS;
}

serial port programming tutorial using win32 api and FT232 usb toserial converter

This tutorial teaches you to program your Windows PC to communicate with an external peripheral like data acquisition boards, USB GPIO Boards,Relay boards etc through its serial port (Virtual or Real).

Here i am assuming that you are using a Virtual COM port  based any of the numerous IC’s available on the market.

The tutorial is aimed at Software/Embedded developers who may want to interface real world sensors like DAQ’s,USB Relays,USB GPIO boards ,Temperature sensors,Humidity sensors etc to Windows PC .

The software is written using C language and communicates with the Serial Port using Win32 API.

All codes have been tested on a 64bit Windows 10 System.

All the codes in this tutorial are licensed under MIT License making it eligible to be used for developing proprietary software applications.

Join Our Youtube Channel for more Info

Contents

  • Sourcecodes
  • Compilers and IDE’s used
  • Finding out your COM port Number
  • Windows10 Accounts 
  • Opening and Closing a Serial Port
  • Configuring the DCB Structure
  • Setting Timeouts
  • Writing Data to Serial Port
  • Microcontroller Interfacing with PC 
  • Reading from the Serial Port

Isolated USB to Serial Converters

One major issue with interfacing real word devices with your PC’s is that High voltage spurious transients can couple on to your USB or RS232 or RS485 lines.These high voltage transients can cause severe damage to your computer or its ports.

It is highly recommended to use an

  • Isolated USB port or
  • Isolated USB to Serial/RS232/RS485 converter

to protect your PC .

Sourcecodes

link to xanthium enterprises repo containing Serial Programming Code using Win32 API on Github

  • Please note that the source codes on the website show only the relevant sections to highlight the process of programming the serial port.
  • Contact Us to Enquire about the full Source codes

Compilers and IDE’s used

Since we are developing on Windows platform,

  • Recommended IDE is Visual Studio (Professional or Community Edition).

In Visual Studio,there is no longer an option for creating a C project.

So create a Win32 C++ project and save your source file with a .C extension (Eg: MySourceFile.C).Compiler would automatically do the rest.

GCC/MinGW

You can also compile the code using GCC  or MinGW  after a few tweaks.

The Code uses some Microsoft specific safe C functions like 

  • strcat_s() and 
  • scanf_s() 

which are used to get the COM number from the user and create the COM port address string (Eg «\\.\COM9» ).The code will compile on GCC after removing those functions.

Make sure that variables referenced by those functions are also changed.

Finding out your COM port Number

A little bit of History,

Serial ports are simple 9 pin legacy ports used by the PC to communicate with external peripherals like Printers,Data Acquisition Sytems,Printers etc.

The simple protocol and the 3 pin hardware physical lines (RX,TX,GND) made it quite popular among manufacturers who wanted an easy way to connect their devices with a computer.

In the 2020’s, the old DB9 Serial port is found only on industrial motherboards and old PC’s.The once ubiquitous serial port have been completely replaced by the familiar USB standard.

The arrival of cheap USB to UART chips like FTD FT232RL,TUSB3410,CP2102 have made it quite easy to upgrade the existing software that used to talk over serial to USB protocol.These chips provide a virtual serial port that behaves like a real one but transmits the data through USB.

If your PC does not have any hardware serial ports (RS232 DB9 ports), you can use USB to Serial Converter’s like USB2SERIAL.(non Isolated).


In Windows ,

Serial ports are named as COM1,COM2 ,COM3.. etc .COM1 and COM2 usually refer to the hardware serial ports present in the PC (quite rare now)

while COM numbers in double digits like COM32,COM54,COM24.. etc are given to USB to Serial Converters or PCI serial port extenders.

To find out the COM number corresponding to your serial port,

  • Open Device Manager by right clicking on My Computer icon and selecting ManageDevice Manager.or
  • by typing «Device Manager» on the taskbar search box in Windows 10

Under Ports(COM & LPT) you can see the parallel and serial ports (COM) detected by your system.

Finding out your COM port number under windows

If your PC has any hardware ports, it will be shown either as COM1 or COM2 under the Ports Section.

I am using a FTDI based USB to Serial Converter (USB2SERIAL)which is recognized as COM24 (this may be different under your system).

If you double click on COM24,you can see the details of the corresponding port.

USB serial port properties dialog on windows

Windows10 Accounts 

The codes/executables are able to open the connection to the serial port under both Administrator Account and Standard User Account in Windows 10.

Opening and Closing a Serial Port

we use the CreateFileA() function to open a serial port.

CreateFile() is a Win 32 function which is used to create or open a file, stream or an IO device like serial port.

On success CreateFileA() will return a handle which is then used to refer the connection in all subsequent operations.

After opening a serial port using the CreateFileA() function you should close it with CloseHandle() function, otherwise port will become unavailable to other programs.

Now let’s write a small program to open and close a serial port on Windows.

Open a text editor like notepad  and type the below code and save it as “serial.c”.If you are using IDE like VS Express, use the one integrated with it. 

#include<windows.h>
#include<stdio.h>
int main()
{
  HANDLE hComm;

  hComm = CreateFileA(\\\\.\\COM24”,                //port name
                      GENERIC_READ | GENERIC_WRITE, //Read/Write
                      0,                            // No Sharing
                      NULL,                         // No Security
                      OPEN_EXISTING,// Open existing port only
                      0,            // Non Overlapped I/O
                      NULL);        // Null for Comm Devices

  if (hComm == INVALID_HANDLE_VALUE)
      printf(“Error in opening serial port”);
  else
      printf(“opening serial port successful”);

  CloseHandle(hComm);//Closing the Serial Port

  return 0;
}

Configuring the DCB Structure
In Windows ,settings like Baud rate ,Number of start/Stop bits,data formats etc for the serial port are controlled by the DCB structure.

To Configure the DCB structure we use two functions,
GetCommState() function which retrieves the current control settings of the serial port and
SetCommState() function which configures the serial port with the new values in DCB structure provided by us.

ReadIntervalTimeout Specifies the maximum time interval between arrival of two bytes. If the arrival time exceeds these limits the ReadFile() function returns.

ReadTotalTimeoutConstant is used to calculate the total time-out period for read operations. For each read operation, this value is added to the product of the ReadTotalTimeoutMultiplier member and the requested number of bytes.

ReadTotalTimeoutMultiplier is used to calculate the total time-out period for read operations. For each read operation, this value is multiplied by the requested number of bytes to be read.

WriteTotalTimeoutConstant similar to ReadTotalTimeoutConstant but for write operation.

WriteTotalTimeoutMultiplier similar to ReadTotalTimeoutMultiplier but for write operation.

After this you have to set the values using SetCommTimeouts() function.

Writing Data to Serial Port

Writing data to the opened serial port is accomplished by the WriteFile() function. WriteFile() function can be used to write both into the files and I/O ports.

Status = WriteFile(hComm,        // Handle to the Serial port
                   lpBuffer,     // Data to be written to the port
                   
dNoOFBytestoWrite,  //No of bytes to write
                   &
dNoOfBytesWritten, //Bytes written
                   NULL);

If your PC does not have any hardware serial ports you can use any USB to Serial Converters(I am using USB2SERIAL).

Microcontroller Interface

I have interfaced a microcontroller MSP430G2553/ATmega328P to the serial port like this

Null modem serial connection between microcontroller(MSP430) and PC serial port using USB2SERIAL

You can use any microcontroller of your choice like 8051,AVR or ARM(LPC2148).

The Controller waits for a character to be received and lights up the corresponding LED.

The code for interfacing MSP430  is available in the Github repo.

.If you want to know how to configure the MSP430 controller UART you can check this tutorial.

MSP430 connected to USB to Serial Converter

We have also included code for

  • interfacing your PC with ATmega328P microcontroller using Serial port

in the Repo too. The connection setup is similar to the MSP430.

Please note that if you are using a DB9 RS232 Serial Port of your PC, you will have to build a RS232 signal level converter at the microcontroller side to decode the RS232 signal.
Directly connecting the PC’s RS232 Serial port to MSP430 ‘s pins will damage the chip.

 

Here is the screen shot of the Program writing into serial port of Windows 7 Computer

Please note that i have changed the names of the C files in the latest release.

Use the files in the «PC_Transmits» folder.

compiling and running the serial port write program on windows 7

Here is the screen shot of the Program writing into serial port of Windows 10Computer

serial port programming tutorial on Windows 10 Windows 7 using Win32/64 API

Reading from the Serial Port
Reading from the serial port is accomplished by the ReadFile() function.

One way to do that is to use polling where the ReadFile() continuously reads from the serial port and checks for any received characters.
Other way is to setup an event and let windows notify us when a character is received.

We are going to use the second method here, following are the steps.

1. Create an Event for a particular action like character reception, change in modem lines etc using SetCommMask() function .
2. Ask windows to wait for the event set by SetCommMask() function using WaitCommEvent() and notify us when the condition happens.
3. Call ReadFile () to read the received data from the Serial port.

Functions used are

SetCommMask() is used to set the events to be monitored for a communication device

WaitCommEvent() is used to wait for the events set by SetCommMask() to happen, i

Codes running on Windows 7 PC.

Please note that i have changed the names of the C files in the latest release.

Use the files in the «PC_Receives» folder.

Waiting  for data reception

Reset the Micro controller to transmit the string “Hello from MSP430”.

Data received by the PC serial port

Codes running on Windows 10 PC.

Serial port driver communication and programming tutorial using win32 API on Windows 10 windows 7

Check out our next section, 

If you want to know how to control the RTS and DTR pins of the serial port .

Часто бывает необходимо, чтобы работа с устройством поддерживалась как в windows, так и в linux. В моем случае нужно было обеспечить работу com-порта в приложении, написанном на с/с++ с использованием кроссплатформенной библиотеки QT. Штатной поддержки программирования портов в QT нет (да и ни к чему это). Поэтому в win32 для работы с портом будем использовать средства WinAPI. В linux системах же, как известно для работы с устройствами используются специальные файлы.

Итак, взяв на вооружение всем знакомый gcc и его windows-аналог mingw, напишем нехитрый код.

Код будет довольно таки искусственным, т.к. ставится цель не более, чем показать принципы работы с com-портами в двух операционках. Для управления компилятором будем использовать директивы определения компилятора и ОС (__MINGW32__ и __linux). Com-портом в нашем случае является устройство dev/ttyS0 и COM1.

Инициализация

#ifdef __MINGW32__
#include <windows.h>
#endif
#ifdef __linux
#include <sys/types.h>
#include <fcntl.h>
#endif
#include <stdio.h>

int main() {

#ifdef __MINGW32__
HANDLE hSerial = CreateFile

("COM1",GENERIC_READ | GENERIC_WRITE,0,NULL,OPEN_EXISTING,0,NULL);
if (hSerial == INVALID_HANDLE_VALUE) {
printf("Error opening port\r\n");
return -1;
}
#endif

#ifdef __linux
int fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY );
if (fd <0) {
printf("Error opening port\n");
return -1;
}
#endif

...

Здесь в win32 используется функция CreateFile, параметры по ссылке в коде. Функция возвращает хендл на на устройство, в которым мы дальше будем работать. Аналогично работает и open в linux.

Запись

char * data; // данные записи в порт
data[0] = 0x01;

#ifdef __MINGW32__
DWORD dwBytesWrite = 0; // кол-во записанных байтов
if(!WriteFile

(hSerial, data, 1, &dwBytesWrite, NULL)){
printf("write error\r\n");
}
#endif
#ifdef __linux
int iOut = write(fd, data, 1);
if (iOut < 0){
printf("write error\n");
}
#endif

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

Чтение

char * buffer;

#ifdef __MINGW32__
if(!ReadFile

(hSerial, buffer, 1, &dwBytesWrite, NULL)){
printf("read error\r\n");
}
#endif
#ifdef __linux
int iOut = read(fd, buffer, 1);
if (iOut < 0){
printf("read error\n");
}
#endif

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

#ifdef __MINGW32__
CloseHandle(hSerial);
#endif
#ifdef __linux
close(fd);
#endif

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

Надеюсь, что эта практика пригодится кому-то, кроме меня самого.
Приятного си-программирования!

Sometimes we require to communicate with an external device like a printer, microcontroller board or any serial device using the serial port of a windows machine. There is a lot of serial application available like Hercules, HyperTerminal, Docklight, ..etc.

We can use any one of them for serial communication but sometimes we require to create our own custom serial application for communication. In windows, it is easy to create the custom serial application using the win32 API.

In this blog post, we will learn serial port programming using the Win32 API. In Windows, serial port programming is very easy, MSDN provide all the required win32 API information which require for the serial port programming.

You can also see the below articles that how to change the properties of com-port like baud rate, parity using the Windows API.

  • Change properties of COM Port Using win32 API
  • Get COM PORT of USB Serial Device using the VID and PID

Finding COM port Number of a Device

In windows, the serial device will display in com port section of device manager with name as COM1, COM2, COM3, COM4.. etc. Generally, COM1 and COM2 refer to the hardware serial ports present in the PC and another com port number is export when any serial device or USB to serial device attached to PC. It also possible that com id could be virtual ( for example static virtual com port).

In my laptop, I have attached an Arduino board (Atmega 2560) and its COM id would be shown in com port section (Control Panel > Device Manager > Ports).

port class

Compilers and IDE’s used

Here I have used Visual Studio 2013. You can also use MinGW (Minimalist GNU for Windows) an Open Source programming tool.  Here I am assuming that you know how to create the console application using the visual studio and familiar with win32 API.

Opening a Serial Port

In Windows using the CreateFile(), we can open the serial port. The CreateFile() is a Win32 API that creates or opens a file or I/O device.

On success CreateFile() returns a handle that can be used to access the file or device depending on the flags and attributes specified.

HANDLE CreateFile(
    LPCTSTR lpFileName,          // pointer to name of the file
    DWORD dwDesiredAccess,       // access (read-write) mode
    DWORD dwShareMode,           // share mode
    LPSECURITY_ATTRIBUTES lpSecurityAttributes,
    // pointer to security attributes
    DWORD dwCreationDisposition,  // how to create
    DWORD dwFlagsAndAttributes,  // file attributes
    HANDLE hTemplateFile         // handle to file with attributes to
    // copy
);

Closing a Serial Port

You should remember that after opening the com port using the CreateFile(), you have to close it by calling the CloseHandle() otherwise it will be unavailable to other applications.

BOOL CloseHandle(
  HANDLE hObject
);

On success CloseHandle() returns zero.

Serial port communication Application

Let see an example code where I am talking with a Mega2560 Arduino device. I have written a small code for mega2560 in which it receives serial data. If received data is “aticleworld” then it will send a message “Welcome to AticleWorld !” or otherwise it will send the message “Please Send Correct Message”.

Mega2560 serial code,

#include <SoftwareSerial.h>

void setup()
{
    // initialize serial ports
    Serial.begin(9600);    // USB serial port 0
}


void loop()
{
    String msg = "";
    // check for data byte on USB serial port
    if (Serial.available())
    {
        // get byte from USB serial port
        while(Serial.available())
        {
            msg = Serial.readString();// read the incoming data as string
        }
        if( msg == "aticleworld")
        {
            //Send data to usb serial port
            Serial.write(" Welcome to AticleWorld !");
        }
        else
        {
            //Send data to usb serial port
            Serial.write("Please Send Correct Message");
        }
    }
}

You can purchase Mega2560 from Amazon,  Click here

Serial Application for the console,

In the below console application, I am using the win32 API to open the com port and sending the data to the open COM port.  See the below video where I have downloaded the Arduino code in the Arduino board and communicating with this board using the console application.

#include "stdafx.h"
#include <Windows.h>
#include <stdio.h>
#include <string.h>

int main(void)
{

    HANDLE hComm;  // Handle to the Serial port
    BOOL   Status; // Status
    DCB dcbSerialParams = { 0 };  // Initializing DCB structure
    COMMTIMEOUTS timeouts = { 0 };  //Initializing timeouts structure
    char SerialBuffer[64] = { 0 }; //Buffer to send and receive data
    DWORD BytesWritten = 0;          // No of bytes written to the port
    DWORD dwEventMask;     // Event mask to trigger
    char  ReadData;        //temperory Character
    DWORD NoBytesRead;     // Bytes read by ReadFile()
    unsigned char loop = 0;
    wchar_t pszPortName[10] = { 0 }; //com port id
    wchar_t PortNo[20] = { 0 }; //contain friendly name



    //Enter the com port id
    printf_s("Enter the Com Port: ");
    wscanf_s(L"%s", pszPortName, (unsigned)_countof(pszPortName));
    swprintf_s(PortNo, 20, L"\\\\.\\%s", pszPortName);

    //Open the serial com port
    hComm = CreateFile(PortNo, //friendly name
                       GENERIC_READ | GENERIC_WRITE,      // Read/Write Access
                       0,                                 // No Sharing, ports cant be shared
                       NULL,                              // No Security
                       OPEN_EXISTING,                     // Open existing port only
                       0,                                 // Non Overlapped I/O
                       NULL);                             // Null for Comm Devices

    if (hComm == INVALID_HANDLE_VALUE)
    {
        printf_s("\n Port can't be opened\n\n");
        goto Exit2;
    }

    //Setting the Parameters for the SerialPort
    dcbSerialParams.DCBlength = sizeof(dcbSerialParams);

    Status = GetCommState(hComm, &dcbSerialParams); //retreives  the current settings
    if (Status == FALSE)
    {
        printf_s("\nError to Get the Com state\n\n");
        goto Exit1;
    }

    dcbSerialParams.BaudRate = CBR_9600;      //BaudRate = 9600
    dcbSerialParams.ByteSize = 8;             //ByteSize = 8
    dcbSerialParams.StopBits = ONESTOPBIT;    //StopBits = 1
    dcbSerialParams.Parity = NOPARITY;      //Parity = None

    Status = SetCommState(hComm, &dcbSerialParams);
    if (Status == FALSE)
    {
        printf_s("\nError to Setting DCB Structure\n\n");
        goto Exit1;
    }

    //Setting Timeouts
    timeouts.ReadIntervalTimeout = 50;
    timeouts.ReadTotalTimeoutConstant = 50;
    timeouts.ReadTotalTimeoutMultiplier = 10;
    timeouts.WriteTotalTimeoutConstant = 50;
    timeouts.WriteTotalTimeoutMultiplier = 10;
    if (SetCommTimeouts(hComm, &timeouts) == FALSE)
    {
        printf_s("\nError to Setting Time outs");
        goto Exit1;
    }


    printf_s("\n\nEnter your message: ");
    scanf_s("%s", SerialBuffer, (unsigned)_countof(SerialBuffer));


    //Writing data to Serial Port
    Status = WriteFile(hComm,// Handle to the Serialport
                       SerialBuffer,            // Data to be written to the port
                       sizeof(SerialBuffer),   // No of bytes to write into the port
                       &BytesWritten,  // No of bytes written to the port
                       NULL);
    if (Status == FALSE)
    {
        printf_s("\nFail to Written");
        goto Exit1;
    }

    //print numbers of byte written to the serial port
    printf_s("\nNumber of bytes written to the serail port = %d\n\n", BytesWritten);

    //Setting Receive Mask
    Status = SetCommMask(hComm, EV_RXCHAR);

    if (Status == FALSE)
    {
        printf_s("\nError to in Setting CommMask\n\n");
        goto Exit1;
    }


    //Setting WaitComm() Event
    Status = WaitCommEvent(hComm, &dwEventMask, NULL); //Wait for the character to be received
    if (Status == FALSE)
    {
        printf_s("\nError! in Setting WaitCommEvent()\n\n");
        goto Exit1;
    }

    //Read data and store in a buffer
    do
    {
        Status = ReadFile(hComm, &ReadData, sizeof(ReadData), &NoBytesRead, NULL);
        SerialBuffer[loop] = ReadData;
        ++loop;
    }
    while (NoBytesRead > 0);

    --loop; //Get Actual length of received data

    printf_s("\nNumber of bytes received = %d\n\n", loop);

    //print receive data on console
    printf_s("\n\n");
    int index = 0;
    for (index = 0; index < loop; ++index)
    {
        printf_s("%c", SerialBuffer[index]);
    }

    printf_s("\n\n");

Exit1:
    CloseHandle(hComm);//Closing the Serial Port
Exit2:
    system("pause");
    return 0;
}

Recommended Posts for you:

  • Best 5 C Books.
  • Get COM PORT of USB Serial Device using the VID and PID.
  • Reading And Writing Windows Registry Using WinAPI
  • Install the port monitor silently without user interaction.
  • C++ Interview Questions with Answers.
  • C-Sharp Interview Questions.
  • Python Interview Questions with Answer.
  • Memory Layout in C.
  • 100 C interview questions, your interviewer might ask.
  • C Interview Questions for the experience.
  • 10 questions about dynamic memory allocation
  • File handling in C, in few hours.
Reference: https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-createfilea

2000, 2007, 29.11.2017

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

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

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

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

Сразу хочу оговориться, что Windows требует точного соблюдения аппаратного протока обмена с внешними устройствами.
Другими словами, у Вас не получится управлять, например, светодиодом подключенным к одному из выводов параллельного порта.
Просто потому, что система будет требовать отработки и сигналов STROBE и ACK. Если Вас это не устраивает, то выход один —
писать собственный драйвер вооружившись DDK. Это, конечно, очень интересная тема, но в данной статье я не буду ее касаться.

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

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

        HANDLE CreateFile(
            LPCTSTR               lpFileName,
            DWORD                 dwDesiredAccess,
            DWORD                 dwShareMode,
            LPSECURITY_ATTRIBUTES lpSecurityAttributes,
            DWORD                 dwCreationDistribution,
            DWORD                 dwFlagsAndAttributes,
            HANDLE                hTemplateFile
        );

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

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

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

0 Опрос атрибутов устройства без получения доступа к нему.
GENERIC_READ Файл будет считываться.
GENERIC_WRITE Файл будет записываться.
GENERIC_READ|GENERIC_WRITE    Файл будет и считываться и записываться.

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

lpSecurityAttributes
Задает атрибуты защиты файла. Не поддерживается только в Windows 95/98/Me. Однако при работе с портами должен в любом
случае равняться NULL.

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

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

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

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

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

        BOOL CloseHandle(
            HANDLE hObject
        };

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

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

        #include <windows.h>

            . . .

        HANDLE port;

            . . .

        port=CreateFile("COM2",GENERIC_READ|GENERIC_WRITE,0,NULL,OPEN_EXISTING,0,NULL);
        if(port==INVALID_HANDLE_VALUE) {
            MsgBox(NULL,"Не возможно открыть последовательный порт","Error",MB_OK);
            ExitProcess(1);
        }

            . . .

        CloseHandle(port);

            . . .

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

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

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

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

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

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

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

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

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

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

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

RTS_CONTROL_DISABLE Запрещает использование линии RTS
RTS_CONTROL_ENABLE Разрешает использование линии RTS
RTS_CONTROL_HANDSHAKE     Разрешает использование RTS рукопожатия. Драйвер устанавливает сигнал RTS когда приемный буфер
заполнен менее, чем на половину, и сбрасывает, когда буфер заполняется более чем на три четверти.
RTS_CONTROL_TOGGLE Задает, что сигнал RTS установлен, когда есть данные для передачи. Когда все символы из передающего
буфера переданы, сигнал сбрасывается.

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

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

        BOOL BuildCommDCB(LPCTSTR lpDef, LPDCB lpDCB);

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

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

        baud=1200 parity=N data=8 stop=1
lpDCB
Указатель на заполняемую структуру DCB. При этом структура должна быть уже создана и заполнена нулями, кроме поля
DCBlength, которое должно содержать корректное значение. Возможно так же использование уже заполненой структуры DCB,
например полученой вызовом одной из функций чтения параметров порта.

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

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

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

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

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

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

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

    • fInX, fOutX,fOutXDsrFlow, fOutXCtsFlow устанавливаются в FALSE
    • fDtrControl устанавливается в DTR_CONTROL_ENABLE
    • fRtsControl устанавливается в RTS_CONTROL_ENABLE
  • Для строк вида 9600,n,8,1,x (заканчивающихся символом х):

    • fInX, fOutX устанавливаются в TRUE
    • fOutXDsrFlow,fOutXCtsFlow устанавливаются в FALSE
    • fDtrControl устанавливается в DTR_CONTROL_ENABLE
    • fRtsControl устанавливается в RTS_CONTROL_ENABLE
  • Для строк вида 9600,n,8,1,p (заканчивающихся символом p):

    • fInX, fOutX устанавливаются в FALSE
    • fOutXDsrFlow,fOutXCtsFlow устанавливаются TRUE
    • fDtrControl устанавливается в DTR_CONTROL_HANDSHAKE
    • fRtsControl устанавливается в RTS_CONTROL_HANDSHAKE

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

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

        BOOL GetCommState(
            HANDLE hFile,
            LPDCB  lpDCB
        );

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

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

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

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

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

        BOOL SetCommState(
            HANDLE hFile,
            LPDCB  lpDCB
        );

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

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

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

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

        typedef struct _COMMTIMEOUTS {
            DWORD ReadIntervalTimeout;
            DWORD ReadTotalTimeoutMultiplier;
            DWORD ReadTotalTimeoutConstant;
            DWORD WriteTotalTimeoutMultiplier;
            DWORD WriteTotalTimeoutConstant;
        } COMMTIMEOUTS,*LPCOMMTIMEOUTS;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        BOOL BuildCommDCBAndTimeouts(
            LPCTSTR        lpDef,
            LPDCB          lpDCB,
            LPCOMMTIMEOUTS lpCommTimeouts
        );

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

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

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

        BOOL GetCommTimeouts(
            HANDLE         hFile,
            LPCOMMTIMEOUTS lpCommTimeouts
        );

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

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

        BOOL SetCommTimeouts(
            HANDLE         hFile,
            LPCOMMTIMEOUTS lpCommTimeouts
        );

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

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

        #include <windows.h>

                . . .

        DCB          *dcb;
        COMMTIMEOUTS  ct;
        HANDLE        port;

                . . .

        dcb=(DCB*)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,sizeof(DCB));
        dcb->DCBlength=sizeof(DCB);
        BuildCommDCB("baud=9600 parity=N data=8 stop=1",dcb);
        dcb->fNull=TRUE;
        
        ct.ReadIntervalTimeout=10;
        ct.ReadTotalTimeoutMultiplier=ct.ReadTotalTimeoutConstant=0;
        ct.WriteTotalTimeoutMultiplier=ct.WriteTotalTimeoutConstant=0;

        port=CreateFile("COM2",GENERIC_READ|GENERIC_WRITE,0,NULL,OPEN_EXISTING,0,NULL);
    
        SetCommState(port,dcb);
        SetCommTimeouts(port,&ct);
        HeapFree(GetProcessHeap(),0,dcb);

                . . .

        CloseHandle(port);

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

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

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

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

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

        typedef struct _COMMPROP {
            WORD  wPacketLength;       // packet size, in bytes
            WORD  wPacketVersion;      // packet version
            DWORD dwServiceMask;       // services implemented
            DWORD dwReserved1;         // 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
Битовая маска. Для коммуникационных устройств всегда SP_SERIALCOMM, включая модемы.

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 56K бпс.
BAUD_57600       57600 бпс.
BAUD_115200 115200 бпс.
BAUD_128K 128K бпс.
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 Telnet
PST_UNSPECIFIED Неизвестное устройство
PST_X25 Устройство стандарта 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Х     Специальный широкий канал через аппаратную последовательную линию.

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

STOPBITS_10 Один стоповый бит
STOPBITS_15 Полтора стоповыx бита
STOPBITS_20 Два стоповых бита
PARITY_NONE Без четности
PARITY_ODD Доплнение до нечетности
PARITY_EVEN Дополнение до четности
PARITY_MARK Бит четности всегда «1»
PARITY_SPACE     Бит четности всегда «0»

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

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

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

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

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

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

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

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

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

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

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

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

        BOOL GetCommProperties(
            HANDLE     hFile,
            LPCOMMPROP lpCommProp
        );

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

        #include <windows.h>

            . . .

        HANDLE    port;
        COMMPROP *pr;
    
            . . .

        port=CreateFile("COM2",GENERIC_READ|GENERIC_WRITE,0,NULL,OPEN_EXISTING,0,NULL);
        pr=(COMMPROP*)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,sizeof(COMMPROP));
        GetCommProperties(port,pr);
        if(pr->wPacketLength != sizeof(COMMPROP)) {
            pr=(COMMPROP*)HeapRealloc(GetProcessHeap(),HEAP_ZERO_MEMORY,pr,pr->wPacketLength);
            pr->wProvSpec1=COMMPROP_INITIALIZED;
            GetCommProperties(port,pr);
        }

            . . .

        HeapFree(GetProcessHeap(),0,pr);
        CloseHandle(port);

            . . .

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

        typedef struct _COMM_CONFIG {
            DWORD dwSize;
            WORD  wVersion;
            WORD  wReserved;
            DCB   dcb;
            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.

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

        BOOL GetCommConfig(
            HANDLE       hCommDev,
            LPCOMMCONFIG lpCC,
            LPDWORD      lpdwSize
        );

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

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

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

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

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

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

            . . .

        COMMCONFIG  *cf;
        DWORD        sz;
        HANDLE       port;

            . . .
        cf=(COMMCONFIG*)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,sizeof(COMMCONFIG));
        cf->dwSize=sizeof(COMMCONFIG);
        GetCommConfig(port,cf,&sz);
        if(sz > sizeof(COMMCONFIG)) {
            cf=(COMMCONFIG*)HeapRealloc(GetProcessHeap(),HEAP_ZERO_MEMORY,cf,sz);
            cf->dwSize=sz;
            GetCommConfig(port,cf,&sz);
        }

            . . .
    
        HeapFree(GetProcessHeap(),0,cf);
        CloseHandle(port);

            . . .

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

        BOOL CommConfigDialog(
            LPTSTR       lpszName,
            HWND         hWnd,
            LPCOMMCONFIG lpCC
        );

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

Пример диалога настройки порта

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

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

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

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

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

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

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

        BOOL SetCommConfig(
            HANDLE       hCommDev,
            LPCOMMCONFIG lpCC,
            DWORD        dwSize
        );

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

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

        BOOL GetDefaultCommConfig(
            LPCSTR       lpszName,
            LPCOMMCONFIG lpCC,
            LPDWORD      lpdwSize
        );

        BOOL SetDefaultCommConfig(
            LPCSTR       lpszName,
            LPCOMMCONFIG lpCC,
            DWORD        dwSize
        );

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

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

        BOOL SetupComm(
            HANDLE hFile,
            DWORD  dwInQueue,
            DWORD  dwOutQueue
        );

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

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

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

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

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

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

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

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

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

        BOOL PurgeComm(
            HANDLE hFile,
            DWORD  dwFlags
        );

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

PURGE_TXABORT Немедленно прекращает все операции записи, даже если они не завершены
PURGE_RXABORT     Немедленно прекращает все операции чтения, даже если они не завершены
PURGE_TXCLEAR Очищает очередь передачи в драйвере
PURGE_RXCLEAR Очищает очередь приема в драйвере

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

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

        BOOL FlushFileBuffers(
            HANDLE hFile
        );

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

        #include <windows.h>
        #include <string.h>

            . . .

        DCB           dcb;
        COMMTIMEOUTS  ct;
        HANDLE        port;
        DWORD         bc;
        char         *buf_out="Test string";
        char         *buf_in;

            . . .

        dcb.DCBlength=sizeof(DCB);
        BuildCommDCB("baud=9600 parity=N data=8 stop=1",&dcb);
        dcb.fNull=TRUE;
    
        ct.ReadIntervalTimeout=10;

        ct.ReadTotalTimeoutMultiplier=ct.ReadTotalTimeoutConstant=0;
        ct.WriteTotalTimeoutMultiplier=ct.WriteTotalTimeoutConstant=0;

        port=CreateFile("COM2",GENERIC_READ|GENERIC_WRITE,0,NULL,OPEN_EXISTING,0,NULL);

        SetCommState(port,dcb);
        SetCommTimeouts(port,&ct);
        PurgeComm(port,PURGE_TXCLEAR|PURGE_RXCLEAR);
        SetupComm(port,256,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,buf_in);
        CloseHandle(port);

            . . .

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

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

        BOOL TransmitCommChar(
            HANDLE hFile,
            char   cChar
        );

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

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

        BOOL SetCommBreak(
            HANDLE hFile
        );

        BOOL ClearCommBreak(
            HANDLE hFile
        );

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

Более тонкое управление потоком данным позволяет осуществить функция:

        BOOL EscapeCommFunction(
            HANDLE hFile,
            DWORD  dwFunc
        );

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

CLRDTR Сбрасывает сигнал DTR
CLRRTS Сбрасывает сигнал RTS
SETDTR Устанавливет сигнал DTR
SETRTS Устанавливает сигнал RTS
SETXOFF Симулирует прием символа XOFF
SETXON Симулирует прием символа XON
SETBREAK     Переводит выходную линию передатчика в состояние разрыва. SetCommBreak является упрощенной формой данного вызова.
CLRBREAK Снимает состояние разрыва для выходной линии передатчика. ClearCommBreak является упрощенной формой данного вызова.

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

        BOOL ClearCommError(
            HANDLE    hFile,
            LPDWORD   lpErrors,
            LPCOMSTAT lpStat
        );

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

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

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

CE_BREAK Обнаружено состояние разрыва связи
CE_DNS Только для Windows95. Параллельное устройство не выбрано.
CE_FRAME Ошибка обрамления.
CE_IOE Ошибка ввода-вывода при работе с портом
CE_MODE Запрошеный режим не поддерживается, или неверный описатель hFile. Если данный бит установлен, то
значение остальных бит не имеет значение.
CE_OOP Только для Windows95. Для параллельного порта установлен сигнал «нет бумаги».
CE_OVERRUN     Ошибка перебега (переполнение аппаратного буфера), следующий символ потерян.
CE_PTO Только для Windows95. Тайм-аут на параллельном порту.
CE_RXOVER Переполнение приемного буфера или принят символ после символа конца файла (EOF)
CE_RXPARITY Ошибка четности
CE_TXFULL Переполнение буфера передачи

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

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

        typedef struct _COMSTAT
            DWORD fCtsHold:1;
            DWORD fDsrHold:1;
            DWORD fRlsdHold:1;
            DWORD fXoffHold:1;
            DWORD fXoffSent:1;
            DWORD fEof:1;
            DWORD fTxim:1;
            DWORD fReserved:25;
            DWORD cbInQue;
            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
Зарезервировано и не используется.

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

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

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

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

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

        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 равно нулю, то отслеживание событий запрещается. Разумеется всегда можно получить текущую маску отслеживаемых
событий с помощью функции

        BOOL GetCommMask(
            HANDLE  hFile,
            LPDWORD lpEvtMask
        );

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

EV_EVENT1 Устройство-зависимое событие
EV_EVENT2 Устройство-зависимое событие
EV_PERR Ошибка принтера
EV_RX80FULL     Приемный буфер заполнен на 80 процентов

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

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

        BOOL WaitCommEvent(
            HANDLE       hFile,
            LPDWORD      lpEvtMask,
            LPOVERLAPPED lpOverlapped,
        );

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

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

        #include <windows.h>

            . . .

        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);
        dcb.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,&mask,NULL);
        ReadFile(port,buf,100,&bc,NULL);
        
        CloseHandle(port);

            . . .

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

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

        typedef struct _OVERLAPPED {
            DWORD  Internal;
            DWORD  InternalHigh;
            DWORD  Offset;
            DWORD  OffsetHigh;
            HANDLE hEvent;
        } OVERLAPPED, *LPOVERLAPPED;

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

ВНИМАНИЕ!!! Дескриптор файла, в данном случае дескриптор файла порта, является синхронизирующим объектом
ядра (согласно официальной документации Microsoft). Это означает, что его можно использовать в функциях ожидания
событий наравне с дескрипторами событий. Таким образом в поле hEvent в структуре OVERLAPPED можно занести NULL и
ожидать освобождения дескриптора файла, а не дескриптора события. Это действительно работает в Windows NT. Однако
в Windows95/98 все совсем иначе. Обсуждение ошибок, неточностей и прочих проблем документации оставим в стороне.
Просто замечу, что в Windows95/98 поле hEvent должно содержать корректный дескриптор объекта event В ЛЮБОМ СЛУЧАЕ!!!
Иначе функции асинхронного ввода/вывода будут работать более чем странным образом. Кроме того, мы должны ожидать
освобождения именно дескриптора этого события, а не дескриптора файла.

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

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

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

        BOOL GetOverlappedResult(
            HANDLE       hFile,
            LPOVERLAPPED lpOverlapped,
            LPDWORD      lpcbTransfer,
            BOOL         fWait
        );

Параметр hFile определяет дескриптор опрашиваемого файла, lpOverlapped задает адрес структуры OVERLPPED. Третий параметр
задает адрес переменной, куда будет помещено количество считанных или записанных байт. Соответствующий параметр функций
ReadFile и WriteFile, хоть и ДОЛЖЕН БЫТЬ ЗАДАН НЕ НУЛЕВЫМ, не позволяет получить количество переданных байт, так как на
момент возврата управления из функций чтения/записи не передано ни одного байта. Параметр fWait означает, должна ли функция
GetOverlappedResult ждать завершения операции ввода/вывода. Если этот параметр равен  FALSE, то функция немедленно вернет
управление. При этом код возврата будет TRUE, если операция завершена, или FALSE, если операция не завершена. В последнем
случае код ошибки возвращаемой функцией GetLastError будет ERROR_IO_INCOMPLETE. Если функция GetOverlappedResult завершилась
с кодом возврата FALSE, и другим кодом ошибки, то ошибка произошла именно при вызове самой функции. Если параметр fWait
равен TRUE, то функция будет дожидаться завершения операции ввода-вывода.

Замечу, что ожидать завершения ввода/вывода с помощью функции GetOverlappedResult не самое правильное решение. При
работе с дисковым файлом операция завершится гарантированно, а при работе с последовательным или параллельным портом
совсем не обязательно. Представьте, что Вы не настроили тайм-ауты последовательного порта, а подключенное устройство
неисправно. GetOverlappedResult будет ждать вечно, так как нет способа указать максимальное время ожидания. Ждать
завершения ввода/вывода лучше с помощью функций:

        DWORD WaitForSingleObject(
            HANDLE hObject,
            DWORD  dwTimeot
        );

        DWORD WaitForMultipleObjects(
            DWORD    cObjects,
            LPHANDLE lpHandles,
            BOOL     bWaitAll,
            DWORD    dwTimeout
        );

Как следует из названия, эти функции предназначены для ожидания одного или нескольких объектов. Однако следует вспомнить
примечание, которое я привел к описанию структуры OVERLAPPED! Поэтому не мудрствуя лукаво будем ожидать только объекты event.

Функция WaitForSingleObject ожидает только один объект задаваемый первым параметром. Вторым параметром задается максимальное
время ожидания наступления события в миллисекундах. Если вместо времени указана магическая величина INFINITE, то событие будет
ожидаться вечно.

Функция WaitForMultipleObjects ждет несколько событий. Первый параметр указывает сколько именно, а второй задает массив
дескрипторов этих событий. Замечу, что один и тот же дескриптор нельзя указывать в этом массиве более одного раза. Третий
параметр задает тип ожидания. Если он равен TRUE, то ожидается наступление всех событий. Если FALSE, то наступления любого
одного из указанных. И естественно тоже можно задать максимальное время ожидания последним параметром.

Если событие наступило, то функции возвращают значения от WAIT_OBJECT_0 до WAIT_OBJECT_0+cObject-1. Естественно, что
WaitForSingleObject может вернуть только WAIT_OBJECT_0 (если конечно не произошло ошибки). Если произошла ошибка, то будет
возвращено WAIT_FAILED. При превышении максимального времени ожидания функции вернут WAIT_TIMEOUT.

Вернусь к объектам event, которые мы собственно и используем для ожидания. Поясню, почему для наших целей требуются
события с ручным сбросом. Функции ReadFile и WriteFile в асинхронном режиме первым делом сбрасывают (переводят в
занятое состояние) как дескриптор файла, так и дескриптор объекта event заданный в структуре OVERLAPPED. Когда операция
чтения или записи завершается система устанавливает эти дескрипторы в свободное состояние. Тут все логично. Однако и
функции WaitForSingleObject и WaitForMultipleObjects для событий с автоматическим сбросом так же выполняют их перевод
в занятое состояние при вызове. Для событий с ручным сбросом этого не происходит. Теперь представьте, что операция
ввода/вывода завершилась ДО вызова WaitForSingleObject. Представили? Для событий с автоматическим сбросом снова будет
выполнен перевод объекта в занятое состояние. Но освобождать то его будет некому! Более подробная информация об объектах
event выходит за рамки этой статьи.

Теперь небольшой пример. Все подробности, не относящиеся к работе в асинхронном режиме я опускаю.

        #include <windows.h>
        #include <string.h>

            . . .

        HANDLE        port;
        char*         buf;
        OVERLAPPED    ovr;
        DWORD         bc;

            . . .

        port=CreateFile("COM2",GENERIC_READ,0,NULL,OPEN_EXISTING,
        FILE_FLAG_OVERLAPPED,NULL);

        memset(&ovr,0,sizeof(ovr));
        ovr.hEvent=CreateEvent(NULL,FALSE,FALSE,NULL);
        ReadFile(port,buf,buf_size,&bc,&ovr);

        /* Выполняем некую полезную работу */

        if(WaitForSingleObject(ovr.hEvent,10000)==WAIT_OBJECT_0) {
            GetOverlappedResult(port,&ovr,&bc,FALSE);
        } else {

        /* Обработка ошибки */

        }

        CloseHandle(port);
        CloseHandle(ovr.hEvent);

В этом примере переменная bc, предназначенная для получения количества считанных байт, после вызова ReadFile будет равна
0, так как никакой передачи информации еще не было. После вызова GetOverlappedResult в эту переменную будет помещено число
реально считанных байт.

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

Вернемся ненадолго с структуре OVERLAPPED и функциям ReadFile и WriteFile. Для дискового ввода/вывода возможно задать
одновременно несколько конкурирующих операций чтения/записи. Однако для каждой такой операции необходимо использовать
свою структуру OVERLAPPED. Для работы с портами нельзя задавать конкурирующие операции. Точнее можно, но только в Windows NT.
Поэтому для целей совместимости лучше этого не делать.

Теперь, уже совсем кратко, еще об одной возможности, реализованной только в Windows NT. Речь идет о «тревожном вводе-выводе».
Эта возможность реализуется функциями ReadFileEx, WriteFileEx и SleepEx. Суть использования данных функций такова. Вы
вызываете расширенную функцию записи или чтения, которая имеет еще один параметр — адрес функции завершения. После чего,
вызвав расширенную функцию засыпания, освобождаете процессор. После завершения ввода/вывода Ваша функция завершения будет
вызвана системой. Причем вызвана ТОЛЬКО в том случае, если ваша программа вызвала SleepEx. Нетрудно заметить, что данный
вариант работы подходит для систем с большим количеством портов и работающих в режиме ответа по требованию. Например, сервер
с мультипортовым контроллером последовательного порта, к которому подключены модемы.

Теперь, но ОЧЕНЬ кратко, залезем в еще большие дебри. Предположим, что протокол обмена с Вашим устройством, подключенным
к последовательному порту, очень сложен (передаются большие и сложные структуры данных). При этом Ваша программа должна
получать уже полностью принятую и проверенную информацию. Предположим так же, что Ваша программа занимается очень большими
и сложными вычислениями и ей нет времени отвлекаться на обработку ввода/вывода. Да и сложность ее такова, что встраивание
фонового ввода/вывода сделает ее трудно прослеживаемой и неустойчивой. Чувствуете, куда я клоню? Правильно, к выделению
всех тонкостей ввода/вывода в отдельный поток. Возможно выделение и в отдельную задачу, но в этом случае мы не получим
никакой выгоды, а накладные расходы на переключение задач гораздо больше, нежели на переключение потоков в одной задаче.

Потоки создаются функцией CreateThread, и уничтожаются функциями ExitThread и TerminateThread. Принцип работы таков. Вы
создаете поток. При этом управление получает Ваша функция потока. Она работает параллельно, как минимум, основному потоку
Вашей программы. Функция открывает порт и выполняет все необходимые настройки. Затем она выполняет весь ввод/вывод, при
чем совершенно не важно, используется синхронный или асинхронный режим. При засыпании потока (при синхронном режиме)
остальные потоки Вашей программы продолжат выполняться. Когда завершится необходимый обмен информацией с устройством и
данные будут готовы для передачи основной программе Ваш поток установит некий флаг, который будет воспринят основной
программой как готовность данных. После их обработки и формирования блока выходной информации основной поток установит
другой флаг, который будет воспринят потоком ввода-вывода как готовность данных для передачи. При этом в качестве флагов
можно использовать как объекты event, так и обычные переменные (ведь все потоки задачи выполняются в едином адресном
пространстве). В случае использования обычных глобальных переменных не забудьте в их определения добавить модификатор
volatile. Он обозначает, что переменная может измениться асинхронно и компилятор не должен строить иллюзий насчет возможности
ее оптимизации. В противном случае у Вас ничего не получится. Так как в потоке ввода/вывода, выполняющемся параллельно
основному потоку программы, можно использовать асинхронный ввод/вывод, то достаточно просто реализуется возможность
обработки большого количества портов. Фактически поток ввода/вывода будет работать еще и параллельно самому себе.
При запуске такой задачи на многопроцессорной машине выгода от использования многопоточности будет очевидна, поскольку
потоки будут выполняться на разных процессорах.

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


Вы можете обсудить данную статью или задать вопросы автору на форуме

  • Windows просит создать пин код
  • Windows переключение между рабочими столами мышкой
  • Windows прекращает работу в россии
  • Windows перезагрузка в определенное время
  • Windows работа с мобильными устройствами