ОС и разработчик - Менеджер com-порта - PRCY⮭net
В этой статье описан класс CComPortManager, который позволяет выполнять операции записи/чтения с com-портом (по стандартному протоколуRS-232).

Конечно же это не первая реализация менеджера, существуют менеджеры и более крутые, реализованные в виде компонентов, и с более широким набором функций. Вы наверняка найдёте их с помощью любого поисковика. Не стану останавливаться на их обзоре. Моя скромная задача - показать основы работы с com-портом на примере открытого кода представленного класса CComPortManager. Кроме того, возможностей этого класса достаточно для большинства задач по обмену данными между ПК и внешними устройствами, использующими протокол RS-232. Но - вам решать, что использовать.

(В примерах использована среда VC++6.0.)


Ниже в статье будет следующее

* описано как встроить класс в новый проект test (несколько муторный процесс :) но в самом конце статьи ждёт сурпрыз - способ копирования класса),
* описано как работают процедуры класса
* также будет приведён пример использования класса.
* небольшой мануал по работе с коммуникационным портом.



файлы приложения,

которые будут упомянуты ниже в статье, можно взять здесь.


Итак, встраиваем класс в новый проект.


Создаём "пустой" проект test (dialog-based)

* Нажимаем Ctrl+N. Во вкладке Projects выбираем MFC AppWizard (exe). В окошке Location указываем путь, где будет создан проект. В окошке Project name пишем имя проекта - test. Нажимаем OK.
* Выбираем тип проекта - Dialog based. Давим кнопку Finish, затем OK.
* Сохраняем проект (кнопка SaveAll на тулбаре)


Теперь добавим вручную наш класс CComPortManager в проект. Делаем заготовку для нашего класса:

* Добавляем к ресурсам новый диалог. В окне Workspace во вкладке ResourceView добираемся до папки ресурсов "Dialog". Правой кнопкой мыши (RMB) щёлкаем по папке - и выбираем InsertDialog. Новый диалог сразу откроется в редакторе ресурсов. Щелкаем по этому диалогу левой кнопкой (LMB) , тем самым выделив его, затем Alt+Enter - откроется окно свойств. Выберите вкладку General. В окошке ID напишите идентификатор IDD_dlgCPM_SETDIALOG.
* Добавляем к проекту класс с именем CComPortManager. В окне Workspace во вкладке ClassView щёлкаем RMB по корню дерева списка классов. Выбираем NewClass. В списке Class type выбираем MFC Class, задаём имя класса CComPortManager, выбираем базовый класс - CDialog, выбираем ID диалога - IDD_dlgCPM_SETDIALOG. Нажимаем OK.
* Сохраняем проект.


Закрываем среду VC++6.0. Теперь открываем папку проекта и находим там файлы, которые надо будет подправить -
CComPortManager.h
CComPortManager.cpp
test.rc
Resource.h

(Далее, везде, где написано "открываем файл ..." имеется "щёлкаем по файлу для его открытия". Это замечание я сделал из-за того, что файлы также можно открывать и редактировать в MS Блокноте, а по умолчанию они откроются в Студии. Исключение составляет: файл *.rc - его редактировать удобнее именно в блокноте. Файл *.dsw - это файл проекта, его надо просто "запускать".)

Заголовочный файл и файл реализации.


Открываем файл CComPortManager.h. В этом файле находим описание класса:

Код:
/////////////////////////////////////////////////////////////////////////////
// CComPortManager dialog
class CComPortManager : public CDialog
{
...
...
};

и заменяем на описание нашего класса CComPortManager
(текст описания можно найти в файле приложения CComPortManager_h_class.txt)

Код:
/////////////////////////////////////////////////////////////////////////////
// CComPortManager dialog

class CComPortManager : public CDialog
{
public:
CComPortManager(CWnd* pParent = NULL);//конструктор
~CComPortManager();//деструктор

//Структура свойств порта. Эта структура хранит все основные
//настройки, используемые для открытия порта.
struct PORTPROPERTY
{
//закладка для элементов диалога//

//флаг показа окна настроек при открытии порта
bool bShowSetupOnOpenPort;
CString Name;//имя порта "COM1","COM2"...

DWORD CommInQueueSize;//размер очереди приема, байты
DWORD CommOutQueueSize;//размер очереди передачи, байты

DWORD BaudRate;//скорость передачи данных
BYTE ByteSize;//кол-во бит/байт (4-8)
BYTE Parity;//режим контроля чётности
BYTE StopBits;//кол-во стоповых бит
//Межбайтный таймаут при чтении, мс
DWORD ReadIntervalTimeout;
//Множитель для таймаута при чтении байта, мс
DWORD ReadTotalTimeoutMultiplier;
//Дополнительный таймаут при чтении, мс
DWORD ReadTotalTimeoutConstant;
//Множитель для таймаута при записи, мс
DWORD WriteTotalTimeoutMultiplier;
//Дополнительный таймаут при записи, мс
DWORD WriteTotalTimeoutConstant;

//конструктор
PORTPROPERTY();
//оператор присваивания
operator=(const PORTPROPERTY& Prop);
};

//структура для передачи данных потоку,
//который отслеживает "звонок" (RING)
struct PointersForThread
{
HANDLE* m_portH;//указатель на описатель файла порта
bool* bIsRing;//указатель на флаг звонка
//конструктор
PointersForThread(){m_portH=NULL;bIsRing=false;}
};

private:
HANDLE m_portH;//описатель файла порта
PORTPROPERTY m_undo_PortProp;//для хранения старых настроек
PORTPROPERTY m_curr_PortProp;//для хранения текущих настроек
PointersForThread m_PFT;
//копирование данных из контролов в переменные свойств
void Copy_controls_to_property();
//копирование свойств в контролы
void Copy_property_to_controls();
void PrepareUndo();//подготовить откат
void MakeUndo();//выполнить откат

///////////// интерфейс класса //////////////
public:
int DoModal();//показать окно настроек COM порта
bool PortIsOpen();//запрос состояния порта
bool OpenPort();//открыть порт
bool ClosePort();//закрыть порт
DWORD WritePort(BYTE *bufer,DWORD N);//запись в порт
DWORD ReadPort(BYTE *bufer,DWORD N);//чтение из порта
//получить свойства порта
void GetCurrPortPropery(PORTPROPERTY *CPMpp);
//установить свойства порта
void SetCurrPortPropery(PORTPROPERTY *CPMpp);
//запуск потока для отслеживания звонка RING
void RunRingPolling(bool* bIsRing);
//очистить порт
bool ClearPort();
///////////////////////
private:
// Dialog Data
//{{AFX_DATA(CComPortManager)
enum { IDD = IDD_dlgCPM_SETDIALOG };
int m_nBaudRate;
int m_nByteSize;
int m_nParity;
int m_nStopBits;
BOOL m_BOOLShowSetupOnOpenPort;
int m_nPortName;
UINT m_edReadIntervalTimeout;
UINT m_edReadTotalTimeoutMultiplier;
UINT m_edReadTotalTimeoutConstant;
UINT m_edWriteTotalTimeoutMultiplier;
UINT m_edWriteTotalTimeoutConstant;
DWORD m_edCommInQueueSize;
DWORD m_edCommOutQueueSize;
//}}AFX_DATA

// Overrides
// ClassWizard generated virtual function overrides
//{{AFX_VIRTUAL(CComPortManager)
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support
//}}AFX_VIRTUAL
// Implementation
protected:
// Generated message map functions
//{{AFX_MSG(CComPortManager)
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};

Сохраняем файл CComPortManager.h. Теперь открываем файл реализации CComPortManager.cpp. В этом файле нужно заменить весь текст после строк :



Код:
/////////////////////////////////////////////////////////////////////////////
// CComPortManager dialog
...
...


на текст реализации нашего класса CComPortManager.
(Здесь текст реализации не приводится, поскольку он довольно большой. Его можно найти в файле приложения CComPortManager_cpp_class.txt.
Ниже будут приводится фрагменты кода из файла.)

Сохраните полученный файл реализации CComPortManager.cpp.


Теперь нужно подправить
ресурс диалога для класса


В блокноте открываем файл проекта test.rc

1) В этом файле находим описание свойств и контролов (элементов управления) диалога. Найдите строки

Код:
/////////////////////////////////////////////////////////////////////////////
//
// Dialog

и немного далее этих строк - текст

Код:
IDD_dlgCPM_SETDIALOG DIALOG DISCARDABLE 0, 0, 186, 95
STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "Dialog"
FONT 8, "MS Sans Serif"
BEGIN
DEFPUSHBUTTON "OK",IDOK,129,7,50,14
PUSHBUTTON "Cancel",IDCANCEL,129,24,50,14
END


замените этот текст на из файла приложения CComPortManager_SRC_DIALOG.txt.

Файл test.rc пока не закрывайте.

2) Добавьте в test.rc ещё описание данных комбо-боксов - прямо сразу после только что добавленного текста
(файл приложения CComPortManager_SRC_COMBOBOX.txt.

3) Теперь в файле test.rc найдите текст

Код:
/////////////////////////////////////////////////////////////////////////////
//
// DESIGNINFO
//

#ifdef APSTUDIO_INVOKED
GUIDELINES DESIGNINFO DISCARDABLE
BEGIN
IDD_dlgCPM_SETDIALOG, DIALOG
BEGIN
LEFTMARGIN, 7
RIGHTMARGIN, 20
TOPMARGIN, 7
BOTTOMMARGIN, 70
END
END
#endif // APSTUDIO_INVOKED


Здесь задаются размеры габаритной синей штрихпунктирной рамки, которая при редактировании ресурса ограничивает максимальные положения контролов. Сейчас эта рамка имеет размеры той заготовки диалога, которую мы сделали. Эту рамку проще подправить в запущенном редакторе - если попытаться курсором изменить размеры этой рамки, то она автоматом подгонит свои размеры как надо. Но можно и выставить размеры вручную, они в нашем случае такие:
Код:
LEFTMARGIN, 7
RIGHTMARGIN, 228
TOPMARGIN, 7
BOTTOMMARGIN, 72

Сохраните файл test.rc.



Теперь надо добавить и пронумеровать
идентификаторы элементов управления


Откройте файл проекта Resource.h. Там вы увидите такой текст (только значения идентификаторов могут быть другими)

Код:
//{{NO_DEPENDENCIES}}
// Microsoft Developer Studio generated include file.
// Used by test.rc
//
#define IDM_ABOUTBOX 0x0010
#define IDD_ABOUTBOX 100
#define IDS_ABOUTBOX 101
#define IDD_TEST_DIALOG 102
#define IDR_MAINFRAME 128
#define IDD_dlgCPM_SETDIALOG 129

// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 130
#define _APS_NEXT_COMMAND_VALUE 32771
#define _APS_NEXT_CONTROL_VALUE 1000
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif




добавьте после строкИ ,
Код:
#define IDD_dlgCPM_SETDIALOG 129


содержащей идентификатор нашего диалога, идентификаторы элементов управления диалога:
(файл приложения CComPortManager_RES_CONTROLS.txt.


Код:
#define IDC_cbxBAUDRATE 1004
#define IDC_cbxBYTESIZE 1005
#define IDC_cbxPARITY 1006
#define IDC_cbxSTOPBITS 1007
#define IDC_cbxPORTNAME 1008
#define IDC_chkSHOWSETUPONOPENPORT 1009
#define IDC_edREADINTERVALTIMEOUT 1010
#define IDC_edREADTOTALTIMEOUTMULTIPLIER 1011
#define IDC_edREADTOTALTIMEOUTCONSTANT 1012
#define IDC_edWRITETOTALTIMEOUTMULTIPLIER 1013
#define IDC_edWRITETOTALTIMEOUTCONSTANT 1014
#define IDC_edCOMMINQUEUESIZE 1015
#define IDC_edCOMMOUTQUEUESIZE 1016

Замечание: значения идентификаторов контролов не должны повторяться в пределах одного и того же ресурса диалога. При добавлении контролов через редактор ресурсов, за этим следит Визард.
Раз мы добавляем вручную, то должны убедится, что значения идентификаторов различны. (В нашем примере это так и есть :) )

Сохраните файл Resource.h.

Теперь запускаем файл test.dsw - откроется наш проект. Не забываем подправляем пресловутую синюю рамку на диалоге (по желанию, конечно).




Описание интерфейса класса CComPortManager


Интерфейс включает в себя следующие процедуры


Код:
1) //показать окно настроек COM порта
int DoModal();
2) //запрос состояния порта
bool PortIsOpen();
3) //открыть порт
bool OpenPort();
4) //закрыть порт
bool ClosePort();
5) //запись в порт
DWORD WritePort(BYTE *bufer,DWORD N);
6) //чтение из порта
DWORD ReadPort(BYTE *bufer,DWORD N);
7) //получить свойства порта
void GetCurrPortPropery(PORTPROPERTY *CPMpp);
8) //установить свойства порта
void SetCurrPortPropery(PORTPROPERTY *CPMpp);
9) //очистить порт
bool ClearPort();
10) //запуск потока для отслеживания звонка RING
void RunRingPolling(bool* bIsRing);

Теперь подробнее о каждой


* Показать окно настроек COM порта
int DoModal();
Выводит на экран диалог, в котором отображаются текущие настройки порта (вернее те настройки, с которыми порт будет открыт, когда в очередной раз будет вызвана OpenPort() ). Здесь можно изменить настройки порта, выбрав нужные значения. Текущие настройки хранятся в переменной curr_PortProp. Перед выводом диалога на экран настройки сохраняются в переменной undo_PortProp, затем "копируются" в контролы. Диалог выводится и показывает текущие настройки.
Если изменение настроек подтверждается кнопкой OK, то данные из контролов "копируются" в curr_PortProp. При отмене - curr_PortProp восстанавливается из undo_PortProp и настройки не поменяются.

* Запрос состояния порта
bool PortIsOpen();
Производит проверку хендла порта m_portH на действительность. Если хендл имеет значение INVALID_HANDLE_VALUE, то есть хендл недействителен, и порт считается закрытым (возврат false). Иначе - возвращается true, порт открыт.

* Процедура открытия порта
bool OpenPort();
Каждый экземпляр класса CComPortManager может в любой момент открыть только один порт.
Хендл порта инкапсулирован в объекте класса CComPortManager и недоступен прямо через интерфейс. Он там создаётся , там же и умирает. При открытии порта используются настройки из переменной curr_PortProp, которую можно настроить через диалог. Если порт нормально открылся, возвращается true.
Если установлен флаг curr_PortProp.bShowSetupOnOpenPort, то перед открытием порта автоматически будет выведено на экран окно настроек порта.
Подробнее об открытии порта и описание управляющих структур вы найдёте ниже по течению, "Работа с коммуникационным портом COM в программах для Win32".

* Процедура закрытия порта
bool ClosePort();
Здесь, если хендл действителен, производится очистка очередей ввода-вывода порта и хендл закрывается. Затем хендлу в любом случае присваивается значение INVALID_HANDLE_VALUE, то есть делаем его недействительным. Если всё прошло нормально, возвращается true.

* Запись в COM порт и

* чтение из COM порта по протоколу RS-232
DWORD WritePort(BYTE *bufer,DWORD N);//запись в порт
DWORD ReadPort(BYTE *bufer,DWORD N);//чтение из порта
В процедуры передаются: указатель на буфер типа BYTE и количество байт, которое надо передать/принять. Длина буфера должна быть больше или равна N, иначе может произойти нехорошая ошибка :) . Процедуры возвращают количество действительно переданных/принятых байт. В случае любых ошибок процедуры возвращают 0.

* void GetCurrPortPropery(PORTPROPERTY *CPMpp); //получить свойства порта

* void SetCurrPortPropery(PORTPROPERTY *CPMpp); //установить свойства порта
- позволяют программно задавать настройки порта. Пример:

CComPortManager CPM;
CComPortManager::PORTPROPERTY CPMpp;

CPM.GetCurrPortPropery(&CPMpp); //читаем настройки
CPMpp.bShowSetupOnOpenPort=true; //изменяем
CPMpp.Name="COM2";
CPM.SetCurrPortPropery(&CPMpp); //записываем обратно

CPM.OpenPort(); //открываем порт


* Очистка порта
bool ClearPort(); //очистить порт
Производит очистку буферов ввода/вывода порта и прекращает все незавершенные операции ввода-вывода.

* Запуск потока для отслеживания звонка RING
void RunRingPolling(bool* bIsRing);
Процедура предназначена для фонового отслеживания "Звонка модема" (сигнал RING). Она запускает поток RingPolling(LPVOID pnt), работающий параллельно с основным процессом. В качестве аргумента LPVOID pnt в поток передаётся передаётся указатель на структуру PointersForThread, содержащую указатели на хендл порта и флаг наличия звонка. Перед запуском потока этот флаг нужно сбросить. Поток начинает опрашивать состояние "звонка" процедурой GetCommModemStatus() и устанавливает флаг при возникновении звонка. При этом поток прекращает работу.
Естественно, флаг, переменная типа bool, указатель которого передан в поток, должен существовать ДОЛЬШЕ существования потока. Например быть членом класса, экземпляр которого существует всю продолжительность работы программы. Также желательно не закрывать порт во время работы потока. Если порт не открыт то поток не будет запущен. Остановить работающий поток можно присвоив флагу bIsRing значение true.




ОБРАТИТЕ внимание на следующий момент: если при работающем потоке закрыть программу, то в памяти останется "мусор". Для избежания такой радости нужно запретить пользователю закрывать программу во время , когда значение переменной *bIsRing равно false. Это можно сделать следующим образом.

1) Для Dialog based приложения.

Нужно заблокировать обработчики сообщений от кнопок IDOK и IDCANCEL.
Открываем в редакторе ресурсов файл реализации диалога (в данном примере testDlg.cpp). Жмём Ctrl+W (открывается визард). Во вкладке Message Maps в окне Objects IDs выбираем IDCANCEL. В окне Messages - двойной щелчок по BN_CLICKED, будет создан обработчик. Затем то же самое сделайте с IDOK. Закройте визард - вы увидите оба обработчика

Код:
void CTestDlg::OnCancel(){CDialog::OnCancel();}
void CTestDlg::OnOK(){CDialog::OnOK();}

Допустим флаг m_bIsRing - это ..хм.. член главного диалога программы.
Нужно добавить в обоих обработчиках перед вызовом обработчика базового класса:

Код:
void CTestDlg::OnCancel()
{
if (!m_bIsRing)
{
::AfxMessageBox("текст предупреждения");
return;
}
CDialog::OnCancel();
}

А можно так:

Код:
void CTestDlg::OnCancel()
{
if (!m_bIsRing){m_bIsRing=true;return;}
CDialog::OnCancel();
}

Тогда при повторном нажатии поток скорее всего уже остановится.

2) Для приложений с MainFrame/CView.

Нужно заблокировать обработчик сообщения WM_CLOSE класса CMainFrame (файл MainFrm.cpp). В редакторе ресурсов открываем файл реализации MainFrm.cpp. Жмём Ctrl+W. В окне Objects IDs выбираем CMainFrame. В окне Messages - WM_CLOSE, будет создан обработчик.
void CMainFrame::OnClose(){CFrameWnd::OnClose();}

Тут несколько вариантов расположения флага - в CView, в theApp, в самом CMainFrame. Если в CMainFrame - тут всё понятно.

Если m_bIsRing находится в theApp (это класс самого Приложения, эта переменная единственная, её класс и она сама определены в файле "название_класса_программы".cpp). Можете найти , где расположено определение theApp через поиск (Find In Files). это определение имеет вид
"название_класса_программы" theApp;
Для доступа к theApp из CMainFrame её надо описать в классе CMainFrame как внешнюю:

Код:
extern "название_класса_программы" theApp;
void CMainFrame::OnClose()
{
if(theApp.m_bIsRing)
{::AfxMessageBox("сначала остановите генерацию строки");return;}
CFrameWnd::OnClose();
}


Если m_bIsRing находится в CView, то для приложения с одной вьюхой - доступ можно получить так:
((CMyView*)GetActiveView())->m_bIsRing;

Однако следует помнить, что тотального контроля ошибок в классе нет (всё предусмотреть невозможно) и не следует пытаться, например, изменить настройки порта при открытом порте. Сначала закрываем порт, потом уже меняем.


Пример использования класса ComPortManager


Код:
#include "ComPortManager.h"

CComPortManager CPM; //переменная-менеджер
bool flag;//флаг звонка
BYTE bufer[5]={0,5,2,3,4}; //буфер с данными

CPM.DoModal(); //показ настроек

if(CPM.OpenPort()) //открываем порт
{
::AfxMessageBox("порт открыт");
}
else
{
::AfxMessageBox("ошибка при открытии порта");
}

CPM. ClearPort(); //очистка порта

WritePort(bufer, 5); // записываем 5 байт из буфера

//ждём "звонка" (ВНИМАНИЕ - если звонка не будет, то программа зависнет,
//это просто для примера приведено)
flag=false;
RunRingPolling(&flag); //запускаем ожидание звонка

while(!flag);//ждём до талого снега


ReadPort(bufer, 5);// читаем 5 байт из порта

CPM.ClosePort(); //закрываем порт


Теперь о том, как добавить в диалог элементы для настроек других параметров, если в этом возникнет необходимость.

К примеру, на диалоге ещё нет (на самом деле уже есть, и весь добавляемый тут код тоже уже есть :) ) окна для задания величины очереди приёма (CommInQueueSize).

* Открываем окно настроек в редакторе ресурсов.
* Помещаем элемент Static и пишем в него "Размер очереди приёма драйвера порта, байт".
* Помещаем элемент Edit и открываем его свойства. Во вкладке General Присваиваем ему идентификатор IDC_edCOMMINQUEUESIZE. Во вкладке Styles выбираем выравнивание текста (Align text) == Left . Устанавливаем флажок Number, чтобы пользователь мог вводить только цифры.
* Добавляем переменную-член, связанную с окном ввода. Удерживая Ctrl дважды щёлкаем LMB по окну Edit. Открывается диалог Add Member Variable для ввода имени и типа переменной. Если визард ругается вроде "Parsing error: Unexpected end-of-line. Input line: "DDX_ ......." , то найдите эти строки (в начале файла реализации) и сделайте так, чтобы строка не была разорвана переводом строки, а вся умещалась в одной строке.
Итак, в диалоге Add Member Variable в окне Member variable type пишем имя m_edCommInQueueSize, в окне Variable type выбираем тип DWORD. Жмём OK.
* Все ключевые места в тексте заголовочного файла и файла реализации класса помечены строкой
"//закладка для элементов диалога//"


Выполните команду из главного меню Edit->Find in Files... и найдите все вхождения этих строк. Их должно быть 6. Список найденных строк будет выведен в окне Output внизу экрана. Двойным щелчком по строке можно перейти к ней.

-Первая закладка - в процедуре CComPortManager::Copy_controls_to_property(). В конце процедуры добавляем строку кода:
curr_PortProp.CommInQueueSize=m_edCommInQueueSize;

-Вторая - в процедуре CComPortManager::Copy_property_to_controls(). Добавляем:
m_edCommInQueueSize=curr_PortProp.CommInQueueSize;

-Третья - в процедуре CComPortManager::PORTPROPERTY::PORTPROPERTY(). Добавляем:
CommInQueueSize=1000;//размер очереди приёма

-Четвёртая - CComPortManager::PORTPROPERTY::operator=(const PORTPROPERTY& Prop). Добавляем:
CommInQueueSize=Prop.CommInQueueSize;

-Пятая - bool CComPortManager::OpenPort(). Здесь надо - по смыслу - скопировать значение из curr_PortProp.CommInQueueSize в нужную переменную (здесь приведены строки кода из файла реализации)-

//установка размеров очередей I/O
SetupComm(m_portH,curr_PortProp.CommInQueueSize,curr_PortProp.CommOutQueueSize);

- Шестая - в заголовке struct PORTPROPERTY{}. Добавляем описание переменной для хранения свойства:
DWORD CommInQueueSize;//размер очереди приема




РАБОТА С КОММУНИКАЦИОННЫМ ПОРТОМ COM В ПРОГРАММАХ ДЛЯ WIN32


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

С последовательными COM (впрочем как и с параллельными - LPT) портами в 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" и так далее. Точно так же они назывались в MS-DOS, так что ничего нового тут нет. Параллельные порты называются "LPT1", "LPT2" и так далее.
Учтите, что если у Вас к порту СОМ1 подключена мышь, Windows не даст открыть этот порт. Аналогично не удастся открыть LPT1 если подключен принтер. А вот с модемом дела обстоят немного по другому. Если какая-либо программа использует модем, например вы дозвонились до своего провайдера Internet, то Вашей программе не удастся открыть порт к которому подключен модем. Во всех остальных случаях порт будет открыт и Вы сможете работать с модемом сами, из своей программы.

dwDesiredAccess Задает тип доступа к файлу. Возможно использование следующих значений:
NULL Опрос атрибутов устройства без получения доступа к нему.
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);

Функция имеет единственный параметр - описатель закрываемого объекта. При успешном завершении функция возвращает не нулевое значение, при ошибке NULL. Теперь пример (достаточно очевидный):
Код:
#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 TxD
WORD 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. Если это поле равно NULL, то по умолчанию подразумевается 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 - самая большая из всех, использующихся для настройки последовательных портов. Но она и самая важная. Заполнение всех полей этой структуры вручную может вызвать затруднения, так как надо очень четко представлять как работает последовательный порт. Поэтому для установки полей воспользуемся функцией GetCommState. Эта функция заполняет DCB информацией о текущем состоянии устройства, точнее о его настройках. А потом мы поменяем только те настройки, которые нужно. Вот как выглядит прототип функции:

Код:
BOOL GetCommState(HANDLE hFile,LPDCB lpDCB);

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

При успешном завершении функция возвращает TRUE. При ошибке - FALSE. Получить параметры порта можно в любой момент, а не только при начальной настройке. Заполнив DCB можно приступать к собственно конфигурированию порта. Это делается с помощью функции SetCommState:

BOOL SetCommState(HANDLE hFile, LPDCB lpDCB);

Эта функция имеет точно такие же параметры, как GetCommState. Различается только направление передачи информации. Следует быть осторожным при вызове функции SetCommState, поскольку она изменит параметры даже в том случае, если очереди приема/передачи не пусты, что может вызвать искажение потока передаваемых или принимаемых данных. Еще одна тонкость этой функции заключается в том, что она завершится с ошибкой, если поля XonChar и XoffChar в DCB содержат одинаковые значения. Как всегда, в случае успешного завершения возвращается TRUE, а в случае ошибки - FALSE.

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

Код:
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 / 9600 + 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 миллисекунды, то операция чтения так же будет завершена.
Как и для заполнения структуры DCB, для COMMTIMEOUTS существует функция считывания установленных в системе значений. Это функция GetCommTimeouts:

Код:
BOOL GetCommTimeouts(HANDLE hFile,LPCOMMTIMEOUTS lpCommTimeouts);

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

Код:
BOOL SetCommTimeouts(HANDLE hFile,LPCOMMTIMEOUTS lpCommTimeouts);

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

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

Код:
BOOL SetupComm(HANDLE hFile,DWORD dwInQueue,DWORD dwOutQueue);

Эту функцию скорее следовало назвать SetCommQueueSize, поскольку все, что она делает, это устанавливает размеры (в байтах) очередей приема и передачи. Причем размеры рекомендуемые. В общем случае, система сама в состоянии определить требуемый размер очередей, однако Вы можете вмешаться в этот процесс. Внутренние очереди драйвера позволяют избежать потери данных, если Ваша программа не успевает их считывать, и пауз в работе программы, если она передает данные слишком быстро.
Размер очереди выбирается немного большим максимальной длины сообщения примерно на 20%. Указанный Вами размер очереди будет принят драйвером к сведению. Но он оставляет за собой право внести коррективы или вообще отвергнуть устанавливаемое значение. В последнем случае функция завершится с ошибкой. Внешние устройства управления объектами, чаще всего подключаемые к портам, обычно обмениваются с компьютером короткими сообщениями. Соответственно и вызов функции 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);

Ниже очень кратко рассмотрены ещё несколько функций ( для полного описания обратитесь к источнику материала).

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

Приостановить прием/передачу данных может и возникновение любой ошибки при установленном в TRUE поле fAbortOnError в структуре DCB использованной для настройки режимов работы коммуникационного порта. В этом случае, для восстановления нормальной работы порта, следует использовать функцию:
Код:
BOOL ClearCommError(HANDLE hFile, LPDWORD lpErrors,LPCOMSTAT lpStat);
Эта функция не только сбрасывает признак ошибки для соответствующего порта, но и возвращает более подробную информацию об ошибке.

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

Код:
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 аналогично поведению функций чтения и записи.

Освобождать процессор на время ожидания хорошо, но хотелось бы параллельно с вводом/выводом делать какую-либо полезную работу. Что бы это стало возможным, необходимо в качестве параметра dwFlagsAndAttributes функции CreateFile() вместо 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 выходит за рамки этой статьи).


в описании работы с коммуникационным портом COM в программах для win32 использованы материалы статьи Олега Титова, размещённой здесь


Обратите внимание на следующую удобную возможность среды VC++6.0.

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


1) Чаще всего весь код класса CXx расположен в двух файлах:
Xx.h -заголовочный файл
Xx.cpp -файл реализации.

в этом случае можно:
а) просто скопировать данные файлы из папки первого проекта в папку второго, затем, открыв проект назначения, добавить файлы в дерево проекта
б) или: найти класс в дереве классов первого проекта, нажать на класс правой кнопкой мыши и добавить в галерею классов. Затем открываем второй проект и выполняем: Project->Add To Project->Components And Controls ищем там папку с названием, как у проекта-источника, а в этой папке файл "Xx.ogx". Вставляем этот "компонент" в проект.

2) Случай, когда класс - производный от CDialog и имеет ресурс-диалог (наш случай :) ). Самый приятный вариант - выполнить пункт 1) б). Ресурс диалога скопируется автоматом, и при том корректно будут выбраны числовые значения для идентификаторов.

Если кроме всего прочего для работы класса требуются другие (дополнительные) файлы, подключаемые при помощи "#include" ,то эти файлы просто копируем из папки в папку, а затем включаем в дерево проекта.
Если в классе используются другие ресурсы (иконки, битмапы и прочее), копируем эти ресурсы перетаскиванием файлов, или через export-import (правой кнопкой мыши по ресурсу в дереве проекта)


Примечание. После копирования класса откомпилируйте проект и определите и исправьте все нестыковки (например скорее всего будет ругань на файл "имя_исходного_проекта.h" - переправить на название проекта назначения . А в файлах, где будут не найдены идентификаторы ресурсов, возможно достаточно будет включить в начале файла строку #include "Resource.h"). Рекомендуется удалить из папки проекта назначения файл *.CLW, затем в проекте нажать Ctrl+W, ОК, ОК. Тем самым файл дерева классов будет создан заново.


______________________________
с Уважением, Алексей Журавлёв
Information
  • Posted on 31.01.2010 21:37
  • Просмотры: 2101