Каталог статей

Главная » Статьи » Мои статьи

Работа с COM-портом с помощью потоков ab

Создание потока средствами WinAPI

Сначала рассмотрим создание потока средствами WINAPI на примере потока чтения данных из порта, а потом по аналогии создадим поток записи.

Создание потока чтения

Перед тем как создать поток, используя средства WinAPI, необходимо объявить и создать функцию, которая будет выполняться потоком. Эта функция имеет прототип

DWORD WINAPI ThreadFunc(LPVOID);

где ThreadFunc - имя функции, задаваемое пользователем. Например:

DWORD WINAPI ReadThread(LPVOID);

В эту функцию и запишем основной код потока чтения:

//---------------------------------------------------------------------------

//главная функция потока, реализует приём байтов из COM-порта
DWORD WINAPI ReadThread(LPVOID)
{
 COMSTAT comstat; //структура текущего состояния порта, в данной программе
 //используется для определения количества принятых в порт байтов
 DWORD btr, temp, mask, signal; //переменная temp используется в качестве заглушки

 overlapped.hEvent = CreateEvent(NULL, true, true, NULL); // создать сигнальный объект-событие
 // для асинхронных операций
 SetCommMask(COMport, EV_RXCHAR); //установить маску на срабатывание по событию приёма байта в порт
 while(1) //пока поток не будет прерван, выполняем цикл
 {
 WaitCommEvent(COMport, &mask, &overlapped); // ожидать события приёма байта
 // (это и есть перекрываемая операция)
 signal = WaitForSingleObject(overlapped.hEvent, INFINITE); //приостановить поток до прихода байта
 if(signal == WAIT_OBJECT_0) //если событие прихода байта произошло
 {
 //проверяем, успешно ли завершилась перекрываемая операция WaitCommEvent
 if(GetOverlappedResult(COMport, &overlapped, &temp, true))
 if((mask & EV_RXCHAR)!=0) //если произошло именно событие прихода байта
 {
 ClearCommError(COMport, &temp, &comstat); //нужно заполнить структуру COMSTAT
 btr = comstat.cbInQue; //и получить из неё количество принятых байтов
 if(btr) //если действительно есть байты для чтения
 {
 ReadFile(COMport, bufrd, btr, &temp, &overlapped); //прочитать байты из порта в буфер программы
 counter+=btr; //увеличиваем счётчик байтов
 ReadPrinting(); //вызываем функцию для вывода данных на экран и в файл
 }
 }
 }
 }
}

//---------------------------------------------------------------------------

Здесь цикл реализуется как while(1), то есть бесконечный цикл. Это связано с особенностями завершения потоков, которые будут рассмотрены ниже. Сам же алгоритм потока чтения при использовании WINAPI в целом практически больше ничем не отличается от алгоритма, реализованного на TThread, за исключением разве только использования дополнительных функций (наподобие Printing() в TThread, об этом подробнее сказано ниже).

Сам поток создаётся функцией CreateThread. При успешном завершении эта функция возвращает дескриптор потока, который необходимо запомнить в переменную типа HANDLE. Например:

HANDLE reader;
reader = CreateThread(NULL, 0, ReadThread, NULL, 0, NULL);

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

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

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

- третий параметр является указателем на функцию, которую будет выполнять поток. В данном случае это функция ReadThread. Следует заметить, что функция должна быть объявлена с соглашением вызова WINAPI (к сожалению, авторы данной статьи не знают, что это такое) и возвращать 32-битный код выхода (параметр типа DWORD). Прототип функции см. выше.

- четвёртый параметр, равный NULL, - это 32-битное значение параметра, передаваемого в поток. В данном случае этот параметр не используется, поэтому равен NULL.

- пятый параметр, равный 0, означает, что поток запускается сразу после создания.

- шестой параметр представляет собой указатель на 32-битную переменную (типа DWORD), в которую будет помещён идентификатор создаваемого потока. Так как нам идентификатор потока не нужен, в качестве этого параметра указываем NULL.

Поток чтения создаётся в функции открытия порта COMOpen.

Теперь обратим внимание на использование дополнительной функции для вывода принятых байтов в файл, а результатов операции - в строку состояния. Основное отличие WINAPI состоит в том, что в этом случае можно работать с графическими компонентами и файлами сразу в главной функции потока, так как при использовании WINAPI все сообщения поступают в очередь главного потока программы и им же обрабатываются. Поэтому вывод в файл и в строку состояния можно включить прямо в цикл чтения байтов потока чтения. Но в данном случае мы оформили вывод в отдельную функцию ReadPrinting(), чтобы сохранить наглядность кода потока чтения и сделать его отличие от кода потока чтения, использующего TThread, наименьшим. Функция ReadPrinting() представляет собой обычную функцию, которая объявляется и вызывается также как и обычная функция.

void ReadPrinting(void);

//---------------------------------------------------------------------------

//выводим принятые байты на экран и в файл (если включено)
void ReadPrinting()
{
 Form1->Memo1->Lines->Add((char*)bufrd); //выводим принятую строку в Memo

 //выводим счётчик в строке состояния
 Form1->StatusBar1->Panels->Items[2]->Text = "Всего принято " + IntToStr(counter) + " байт";

 if(Form1->CheckBox3->Checked == true) //если включен режим вывода в файл
 {
 write(handle, bufrd, strlen(bufrd)); //записать в файл данные из приёмного буфера
 }
 memset(bufrd, 0, BUFSIZE); //очистить буфер (чтобы данные не накладывались друг на друга)
}
//---------------------------------------------------------------------------

После того как поток больше не нужен, его следует завершить. Поток чтения завершается при закрытии порта, в функции COMClose(). Для этого используется функция TerminateThread().

BOOL TerminateThread(HANDLE hThread, DWORD dwExitCode);

Первый её параметр - дескриптор завершаемого потока, второй параметр - 32-х битный код завершения (выхода) потока.

Например:

TerminateThread(reader,0);

Прерывание потока не обязательно удалит объект потока. Для того чтобы объект потока был удалён, необходимо, чтобы все дескрипторы потока были освобождены.

Следует отметить, что при прерывании потока, созданного средствами WINAPI, поток прерывается жёстко, и это может произойти в любом месте его выполнения, а значит, освобождать дескриптор сигнального объекта-события внутри кода потока, как это сделано в программе, использующей класс TThread, в данном случае не очень надёжный способ. (К тому же внутри потока используется бесконечный цикл while(1).) Поэтому, используя функцию TerminateThread(), после неё нужно дополнительно освобождать дескриптор объекта события, находящийся в структуре типа OVERLAPPED, связанной с потоком, а также ещё вручную освобождать и дескриптор потока.

//если поток чтения работает, завершить его; проверка if(reader) обязательна, иначе возникают ошибки
if(reader)
 {TerminateThread(reader,0);
 CloseHandle(overlapped.hEvent); //нужно закрыть объект-событие
 CloseHandle(reader);
 }

По этой причине внутри потока используется бесконечный цикл while(1), а не цикл с условием проверки, прерван поток или нет, как это делается при использовании класса потоков TThread.

В принципе использование функции TerminateThread() - это грубый метод завершения потока. Здесь его можно применить, так как поток выполняет простую работу, не связанную с критическими секциями, ядром (kernel32) или динамическими библиотеками (DLL). Но для серьёзных проектов, использующих именно эти методы работы, TerminateThread не подойдёт.

Теоретически можно сделать по аналогии со свойством Terminated потоков TThread. То есть создать глобальную переменную-флаг (например, flag), которую устанавливать в единицу, когда нужно выполнить завершение потока, а в цикле самого потока поставить вместо while(1) условие while(!flag). Тогда получится мягкое завершение потока, как и в случае с TThread и Terminated, то есть можно будет выполнить какой-нибудь код перед завершением потока (например, освобождение дескриптора сигнального объекта-события). Но в этом случае после установки флага в единицу, нужно дождаться, когда поток завершиться, чтобы затем освободить его дескриптор. Например, для этих целей можно сделать второй флаг flag2 и с помощью while(!флаг2) ждать, когда поток установит его в единицу, что будет означать, что поток завершился.

То есть примерно код будет выглядеть так (например, для потока reader класса ReadThread):

//пример кода завершения потока WinAPI "мягким" способом
flag=1;
while(!flag2){}
flag=0; flag2=0;
CloseHandle(reader);

//как для этого нужно изменить код потока:
//главная функция потока, реализует приём байтов из COM-порта
DWORD WINAPI ReadThread(LPVOID)
{
 ..........

 while(!flag) //пока поток не будет прерван, выполняем цикл
 {
 .........
 }
CloseHandle(overlapped.hEvent);
flag2=1;
}

Можно сделать и без второго флага: сделать так, чтобы поток просто сбрасывал flag в ноль:

//пример кода завершения потока WinAPI "мягким" способом
flag=1;
while(!flag){}
CloseHandle(reader);

//как для этого нужно изменить код потока:
//главная функция потока, реализует приём байтов из COM-порта
DWORD WINAPI ReadThread(LPVOID)
{
 .......
 CloseHandle(overlapped.hEvent);
 flag=0;
}

Следует отметить, что данный случай подходит, если в программе реализован только один поток. Попробуйте изменить код так, чтобы он работал для двух потоков. Может, вы сможете придумать и более удобный способ. Например, для управления потоками WINAPI можно использовать сигнальные объекты-события и другие подобные объекты.

В нашем же случае используется именно функция TerminateThread.

Полностью поток чтения выглядит так:

HANDLE reader; //дескриптор потока чтения из порта

DWORD WINAPI ReadThread(LPVOID);

//-----------------------------------------------------------------------------
//............................... поток ReadThead .............................
//-----------------------------------------------------------------------------

void ReadPrinting(void);

//---------------------------------------------------------------------------

//главная функция потока, реализует приём байтов из COM-порта
DWORD WINAPI ReadThread(LPVOID)
{
 COMSTAT comstat; //структура текущего состояния порта, в данной программе
 //используется для определения количества принятых в порт байтов
 DWORD btr, temp, mask, signal; //переменная temp используется в качестве заглушки

 //создать сигнальный объект-событие для асинхронных операций
 overlapped.hEvent = CreateEvent(NULL, true, true, NULL);

 SetCommMask(COMport, EV_RXCHAR); //установить маску на срабатывание по событию приёма байта в порт
 while(1) //пока поток не будет прерван, выполняем цикл
 {
 WaitCommEvent(COMport, &mask, &overlapped); // ожидать события приёма байта
 // (это и есть перекрываемая операция)
 signal = WaitForSingleObject(overlapped.hEvent, INFINITE); //приостановить поток до прихода байта
 if(signal == WAIT_OBJECT_0) //если событие прихода байта произошло
 {
 //проверяем, успешно ли завершилась перекрываемая операция WaitCommEvent
 if(GetOverlappedResult(COMport, &overlapped, &temp, true))
 if((mask & EV_RXCHAR)!=0) //если произошло именно событие прихода байта
 {
 ClearCommError(COMport, &temp, &comstat); //нужно заполнить структуру COMSTAT
 btr = comstat.cbInQue; //и получить из неё количество принятых байтов
 if(btr) //если действительно есть байты для чтения
 {
 ReadFile(COMport, bufrd, btr, &temp, &overlapped); //прочитать байты из порта в буфер программы
 counter+=btr; //увеличиваем счётчик байтов
 ReadPrinting(); //вызываем функцию для вывода данных на экран и в файл
 }
 }
 }
 }
}

//---------------------------------------------------------------------------

//выводим принятые байты на экран и в файл (если включено)
void ReadPrinting()
{
 Form1->Memo1->Lines->Add((char*)bufrd); //выводим принятую строку в Memo

 //выводим счётчик в строке состояния
 Form1->StatusBar1->Panels->Items[2]->Text = "Всего принято " + IntToStr(counter) + " байт";

 if(Form1->CheckBox3->Checked == true) //если включен режим вывода в файл
 {
 write(handle, bufrd, strlen(bufrd)); //записать в файл данные из приёмного буфера
 }
 memset(bufrd, 0, BUFSIZE); //очистить буфер (чтобы данные не накладывались друг на друга)
}

//---------------------------------------------------------------------------
Создание потока записи. Использование функций ResumeThread() и SuspendThread()

Для демонстрации потоков на WINAPI была сделана только одна версия программы, в которой для потока записи сразу реализована демонстрация использования запуска и останова потока.

В целом реализация останова и запуска потоков на средствах WINAPI аналогична использованию останова и запуска потоков на TThread с помощью методов Resume() и Suspend(), только в данном случае используются функции ResumeThread() и SuspendThread().

Прототипы этих функций:

DWORD ResumeThread(HANDLE hThread); 

DWORD SuspendThread(HANDLE hThread);

Эти функции получают в качестве аргумента дескриптор потока, который нужно запустить (ResumeThread()) или остановить (SuspendThread()). Рассмотрим последовательно создание потока и использование этих функций.

Поток записи создаётся аналогично потоку чтения с помощью функции CreateThread().

Для этого сначала объявляем и создаём рабочую функцию потока:

//---------------------------------------------------------------------------

//главная функция потока, выполняет передачу байтов из буфера в COM-порт
DWORD WINAPI WriteThread(LPVOID)
{
 DWORD temp, signal; //temp - переменная-заглушка

 overlappedwr.hEvent = CreateEvent(NULL, true, true, NULL); //создать событие
 while(1)
 {
 //записать байты в порт (перекрываемая операция!)
 WriteFile(COMport, bufwr, strlen(bufwr), &temp, &overlappedwr);

 //приостановить поток, пока не завершится перекрываемая операция WriteFile
 signal = WaitForSingleObject(overlappedwr.hEvent, INFINITE);

 //если операция завершилась успешно
 if((signal == WAIT_OBJECT_0) && (GetOverlappedResult(COMport, &overlappedwr, &temp, true)))
 {
 //вывести сообщение об этом в строке состояния
 Form1->StatusBar1->Panels->Items[0]->Text = "Передача прошла успешно";
 }
 //иначе вывести в строке состояния сообщение об ошибке
 else {Form1->StatusBar1->Panels->Items[0]->Text = "Ошибка передачи";}

 SuspendThread(writer);
 }
}

//---------------------------------------------------------------------------

Как вы можете заметить, здесь вывод состояния операции в панель состояния осуществляется сразу в главной функции потока. То есть в отличие от программы с TThread в версии с WINAPI поток записи состоит только из одной функции.

Кроме того, вы должны обратить внимание, что здесь также как и в потоке чтения на WINAPI используется бесконечный цикл while(1) из-за той же особенности завершения работы потока.

Самая последняя операция внутри цикла потока - это останов потока с помощью функции SuspendThread(), который аналогичен использованию метода Suspend() в потоке записи на TThread. То есть здесь поток также останавливает сам себя.

После того как создана функция потока, необходимо создать сам поток:

HANDLE writer;
writer = CreateThread(NULL, 0, WriteThread, NULL, CREATE_SUSPENDED, NULL);

Поток записи, как и поток чтения, создаётся в функции открытия порта COMOpen().

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

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

//---------------------------------------------------------------------------

//кнопка "Передать"
void __fastcall TForm1::Button1Click(TObject *Sender)
{
 memset(bufwr,0,BUFSIZE); //очистить программный передающий буфер,
 //чтобы данные не накладывались друг на друга
 PurgeComm(COMport, PURGE_TXCLEAR); //очистить передающий буфер порта
 strcpy(bufwr,Form1->Edit1->Text.c_str()); //занести в программный передающий буфер строку из Edit1

 ResumeThread(writer); //активировать поток записи данных в порт

}

//---------------------------------------------------------------------------

Здесь всё аналогично обработчику кнопки "Передать" в программе с TThread второй версии (где используются останов и запуск потоков), за тем лишь исключением, что для запуска потока используется WINAPI функция ResumeThread().

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

Поток записи, как и поток чтения, существует всё время, пока открыт COM-порт. Когда порт закрывается, и поток записи больше не нужен, он уничтожается (в функции COMClose()) таким же образом, как и поток чтения:

//если поток записи работает, завершить его; проверка if(writer) обязательна, иначе возникают ошибки
if(writer)
 {TerminateThread(writer,0);
 CloseHandle(overlappedwr.hEvent); //нужно закрыть объект-событие
 CloseHandle(writer);
 }

Здесь следует отметить отличие от кода уничтожения потока записи для TThread (вторая версия программы), которое заключается в том, что здесь не нужно активировать поток для его уничтожения, так как функция TerminateThread уничтожает поток без проверки каких-либо условий. Зато если вы будете использовать "мягкое" уничтожение потока (как описывалось выше), то в этом случае вам необходимо будет также использовать пробуждение потока для его завершения.

Функцию TerminateThread также можно использовать и для немедленного принудительного завершения потоков TThread, для этого в неё передаётся дескриптор потока (свойство Handle), например:

TerminateThread((*void)reader->Handle, 0);

Если завершить поток записи writer на TThread с помощью этой функции, а не с помощью метода Terminate(), то активировать поток методом Resume() для уничтожения не требуется.

Приведём полный код потока записи в порт на WINAPI.

HANDLE writer; //дескриптор потока записи в порт

DWORD WINAPI WriteThread(LPVOID);

//-----------------------------------------------------------------------------
//............................... поток WriteThead ............................
//-----------------------------------------------------------------------------

//---------------------------------------------------------------------------

//главная функция потока, выполняет передачу байтов из буфера в COM-порт
DWORD WINAPI WriteThread(LPVOID)
{
 DWORD temp, signal; //temp - переменная-заглушка

 overlappedwr.hEvent = CreateEvent(NULL, true, true, NULL); //создать событие
 while(1)
 {
 //записать байты в порт (перекрываемая операция!)
 WriteFile(COMport, bufwr, strlen(bufwr), &temp, &overlappedwr);

 //приостановить поток, пока не завершится перекрываемая операция WriteFile
 signal = WaitForSingleObject(overlappedwr.hEvent, INFINITE);

 //если операция завершилась успешно
 if((signal == WAIT_OBJECT_0) && (GetOverlappedResult(COMport, &overlappedwr, &temp, true)))
 {
 //вывести сообщение об этом в строке состояния
 Form1->StatusBar1->Panels->Items[0]->Text = "Передача прошла успешно";
 }
 //иначе вывести в строке состояния сообщение об ошибке
 else {Form1->StatusBar1->Panels->Items[0]->Text = "Ошибка передачи";}

 SuspendThread(writer);
 }
}

//---------------------------------------------------------------------------

Сравнение использования класса TThread и средств WINAPI

Одно из отличий заключается в том, что в потоках WINAPI можно использовать обращение к графическим компонентам и файлам, так как сообщения потоков WINAPI ставятся в очередь сообщений главного потока процесса. Таким образом избегаются конфликты между потоками при обращении к разделяемым компонентам или файлам. А вот в TThread для этого используется метод Synchronize(), который выполняет то же самое - ставит сообщения в очередь главного потока процесса, за счёт чего можно избежать конфликтов между потоками.

Второе отличие - это наличие у потоков TThread возможности "мягкого" завершения с использованием метода Terminate() и свойства Terminated, об этом подробно рассказывалось выше. Для потоков WINAPI организовать "мягкое" завершение сложнее, поэтому используется принудительное завершение потоков с помощью функции TerminateThread. (По крайней мере, авторам данной статьи способ завершения потока WINAPI, аналогичный "мягкому" завершению потока TThread, за исключением примера с использованием флагов, не известен.)

Самое же главное отличие состоит в том, что потоки TThread строятся на основе классов, а потоки WINAPI - на основе функций.

Описание программы

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

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

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

//конструктор формы, обычно в нём выполняется инициализация элементов формы
__fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner)
{
 //инициализация элементов формы при запуске программы
 Form1->Label5->Enabled = false;
 Form1->Label6->Enabled = false;
 Form1->Button1->Enabled = false;
 Form1->CheckBox1->Enabled = false;
 Form1->CheckBox2->Enabled = false;
}

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

void COMOpen(void); //открыть порт
void COMClose(void); //закрыть порт
Функция открытия и инициализации порта COMOpen()

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

//---------------------------------------------------------------------------

//функция открытия и инициализации порта
void COMOpen()
{
 String portname; //имя порта (например, "COM1", "COM2" и т.д.)
 DCB dcb; //структура для общей инициализации порта DCB
 COMMTIMEOUTS timeouts; //структура для установки таймаутов
 
 portname = Form1->ComboBox1->Text; //получить имя выбранного порта

 //открыть порт, для асинхронных операций обязательно нужно указать флаг FILE_FLAG_OVERLAPPED
 COMport = CreateFile(portname.c_str(),GENERIC_READ | GENERIC_WRITE, 0,
 NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
 //здесь:
 // - portname.c_str() - имя порта в качестве имени файла,
 // c_str() преобразует строку типа String в строку в виде массива типа char, иначе функция не примет
 // - GENERIC_READ | GENERIC_WRITE - доступ к порту на чтение/записть
 // - 0 - порт не может быть общедоступным (shared)
 // - NULL - дескриптор порта не наследуется, используется дескриптор безопасности по умолчанию
 // - OPEN_EXISTING - порт должен открываться как уже существующий файл
 // - FILE_FLAG_OVERLAPPED - этот флаг указывает на использование асинхронных операций
 // - NULL - указатель на файл шаблона не используется при работе с портами

 if(COMport == INVALID_HANDLE_VALUE) //если ошибка открытия порта
 {
 Form1->SpeedButton1->Down = false; //отжать кнопку
 //вывести сообщение в строке состояния
 Form1->StatusBar1->Panels->Items[0]->Text = "Не удалось открыть порт";
 return;
 }

 //инициализация порта

 dcb.DCBlength = sizeof(DCB); //в первое поле структуры DCB необходимо занести её длину,
 //она будет использоваться функциями настройки порта
 //для контроля корректности структуры

 //считать структуру DCB из порта
 if(!GetCommState(COMport, &dcb))
 {
 //если не удалось - закрыть порт и вывести сообщение об ошибке в строке состояния
 COMClose();
 Form1->StatusBar1->Panels->Items[0]->Text = "Не удалось считать DCB";
 return;
 }

 //инициализация структуры DCB
 dcb.BaudRate = StrToInt(Form1->ComboBox2->Text); //задаём скорость передачи 115200 бод
 dcb.fBinary = TRUE; //включаем двоичный режим обмена
 dcb.fOutxCtsFlow = FALSE; //выключаем режим слежения за сигналом CTS
 dcb.fOutxDsrFlow = FALSE; //выключаем режим слежения за сигналом DSR
 dcb.fDtrControl = DTR_CONTROL_DISABLE; //отключаем использование линии DTR
 dcb.fDsrSensitivity = FALSE; //отключаем восприимчивость драйвера к состоянию линии DSR
 dcb.fNull = FALSE; //разрешить приём нулевых байтов
 dcb.fRtsControl = RTS_CONTROL_DISABLE; //отключаем использование линии RTS
 dcb.fAbortOnError = FALSE; //отключаем остановку всех операций чтения/записи при ошибке
 dcb.ByteSize = 8; //задаём 8 бит в байте
 dcb.Parity = 0; //отключаем проверку чётности
 dcb.StopBits = 0; //задаём один стоп-бит

 //загрузить структуру DCB в порт
 if(!SetCommState(COMport, &dcb))
 {
 //если не удалось - закрыть порт и вывести сообщение об ошибке в строке состояния
 COMClose();
 Form1->StatusBar1->Panels->Items[0]->Text = "Не удалось установить DCB";
 return;
 }

 //установить таймауты
 timeouts.ReadIntervalTimeout = 0; //таймаут между двумя символами
 timeouts.ReadTotalTimeoutMultiplier = 0; //общий таймаут операции чтения
 timeouts.ReadTotalTimeoutConstant = 0; //константа для общего таймаута операции чтения
 timeouts.WriteTotalTimeoutMultiplier = 0; //общий таймаут операции записи
 timeouts.WriteTotalTimeoutConstant = 0; //константа для общего таймаута операции записи

 //записать структуру таймаутов в порт
 //если не удалось - закрыть порт и вывести сообщение об ошибке в строке состояния
 if(!SetCommTimeouts(COMport, &timeouts))
 {
 COMClose();
 Form1->StatusBar1->Panels->Items[0]->Text = "Не удалось установить тайм-ауты";
 return;
 }

 //установить размеры очередей приёма и передачи
 SetupComm(COMport,2000,2000);

 //создать или открыть существующий файл для записи принимаемых данных
 handle = open("test.txt", O_CREAT | O_APPEND | O_BINARY | O_WRONLY, S_IREAD | S_IWRITE);

 if(handle==-1) //если произошла ошибка открытия файла
 {
 //вывести сообщение об этом в командной строке
 Form1->StatusBar1->Panels->Items[1]->Text = "Ошибка открытия файла";
 Form1->Label6->Hide(); //спрятать надпись с именем файла
 Form1->CheckBox3->Checked = false; //сбросить и отключить галочку
 Form1->CheckBox3->Enabled = false;
 }
 //иначе вывести в строке состояния сообщение об успешном открытии файла
 else { Form1->StatusBar1->Panels->Items[0]->Text = "Файл открыт успешно"; }

 PurgeComm(COMport, PURGE_RXCLEAR); //очистить принимающий буфер порта

 reader = new ReadThread(false); //создать и запустить поток чтения байтов
 reader->FreeOnTerminate = true; //установить это свойство потока,
 //чтобы он автоматически уничтожался после завершения

}

//---------------------------------------------------------------------------

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

1) Сначала объявляются необходимые переменные: строковая переменная для имени порта, а также структуры типа DCB и COMMTIMEOUTS, которые понадобятся для инициализации порта.

String portname; //имя порта (например, "COM1", "COM2" и т.д.)
DCB dcb; //структура для общей инициализации порта DCB
COMMTIMEOUTS timeouts; //структура для установки таймаутов

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

portname = Form1->ComboBox1->Text; //получить имя выбранного порта

3) теперь с помощью функции CreateFile открываем порт.

//открыть порт, для асинхронных операций обязательно нужно указать флаг FILE_FLAG_OVERLAPPED
 COMport = CreateFile(portname.c_str(),GENERIC_READ | GENERIC_WRITE, 0,
 NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
 //здесь:
 // - portname.c_str() - имя порта в качестве имени файла,
 // c_str() преобразует строку типа String в строку в виде массива типа char, иначе функция не примет
 // - GENERIC_READ | GENERIC_WRITE - доступ к порту на чтение/записть
 // - 0 - порт не может быть общедоступным (shared)
 // - NULL - дескриптор порта не наследуется, используется дескриптор безопасности по умолчанию
 // - OPEN_EXISTING - порт должен открываться как уже существующий файл
 // - FILE_FLAG_OVERLAPPED - этот флаг указывает на использование асинхронных операций
 // - NULL - указатель на файл шаблона не используется при работе с портами

Функция открывает порт как файл. Она возвращает дескриптор открытого порта.

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

1) portname.c_str() - это имя порта. c_str() - метод класса TString, который преобразует строковую переменную в массив типа char, так как функция принимает имя порта только в виде массива char.

2) GENERIC_READ | GENERIC_WRITE - этот параметр означает, что доступ к порту будет осуществляться на чтение/запись.

3) 0 - означает, что порт не может быть общедоступным (shared) - этот параметр для порта всегда должен иметь такое значение.

4) NULL - дескриптор порта не наследуется, используется дескриптор безопасности по умолчанию.

5) OPEN_EXISTING - порт обязательно должен открываться как уже существующий файл.

6) FILE_FLAG_OVERLAPPED - этот флаг указывает на то, что с портом будут выполняться асинхронные перекрываемые операции.

7) NULL - означает, что указатель на файл шаблона не используется - это обязательно для работы с портами.

В переменную COMport типа HANDLE запоминается возвращаемый дескриптор порта. Далее работа с портом будет осуществляться именно через него.

Затем проверяем, удалось ли открыть порт. Если попытка была неудачной, функция CreateFile вернёт значение INVALID_HANDLE_VALUE. Тогда отжимаем кнопку "Открыть-Закрыть порт", выводим соответствующее сообщение об ошибке и выходим из функции. Если порт открыт успешно, переходим к его инициализации.

if(COMport == INVALID_HANDLE_VALUE) //если ошибка открытия порта
 {
 Form1->SpeedButton1->Down = false; //отжать кнопку
 //вывести сообщение в строке состояния
 Form1->StatusBar1->Panels->Items[0]->Text = "Не удалось открыть порт";
 return;
 }

5) Инициализация начинается со структуры dcb. Для начала нужно считать её значение. Для этого сначала в первое поле структуры DCBlength заносим значение размера структуры:

//в первое поле структуры DCB необходимо занести её длину,
//она будет использоваться функциями настройки порта для контроля корректности структуры
dcb.DCBlength = sizeof(DCB);

А затем из порта с помощью функции GetCommState считываем в неё текущие настройки:

//считать структуру DCB из порта
if(!GetCommState(COMport, &dcb))
 {
 //если не удалось - закрыть порт и вывести сообщение об ошибке в строке состояния
 COMClose();
 Form1->StatusBar1->Panels->Items[0]->Text = "Не удалось считать DCB";
 return;
 }

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

Зачем считывать настройки из порта? Это полезно в том случае, если нужно изменить только те из них, которые нам нужны, а значения остальных нам неизвестны.

После того как считали структуру dcb, заполняем нужные нам поля настройками.

//инициализация структуры DCB
 dcb.BaudRate = StrToInt(Form1->ComboBox2->Text); //задаём скорость передачи 115200 бод
 dcb.fBinary = TRUE; //включаем двоичный режим обмена
 dcb.fOutxCtsFlow = FALSE; //выключаем режим слежения за сигналом CTS
 dcb.fOutxDsrFlow = FALSE; //выключаем режим слежения за сигналом DSR
 dcb.fDtrControl = DTR_CONTROL_DISABLE; //отключаем использование линии DTR
 dcb.fDsrSensitivity = FALSE; //отключаем восприимчивость драйвера к состоянию линии DSR
 dcb.fNull = FALSE; //разрешить приём нулевых байтов
 dcb.fRtsControl = RTS_CONTROL_DISABLE; //отключаем использование линии RTS
 dcb.fAbortOnError = FALSE; //отключаем остановку всех операций чтения/записи при ошибке
 dcb.ByteSize = 8; //задаём 8 бит в байте
 dcb.Parity = 0; //отключаем проверку чётности
 dcb.StopBits = 0; //задаём один стоп-бит

Самые важные для нас параметры, которые нужно настроить - это скорость передачи (BaudRate), разрешение приёма нулевых байтов (так как они могут появиться среди передаваемых данных) (fNULL), установка восьми бит данных в байте (ByteSize) и одного стоп-бита (StopBits) и отключение проверки чётности (Parity). Также нам необходимо отключить использование линий CTS, DSR, DTR, RTS (fOutxCtsFlow, fOutxDsrFlow, fDtrControl, fDsrSensitivity, fRtsControl), а также остановку выполнения операций чтения/записи при ошибке (fAbortOnError) и включить двоичный режим обмена (fBinary).

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

//загрузить структуру DCB в порт
 if(!SetCommState(COMport, &dcb))
 {
 //если не удалось - закрыть порт и вывести сообщение об ошибке в строке состояния
 COMClose();
 Form1->StatusBar1->Panels->Items[0]->Text = "Не удалось установить DCB";
 return;
 }

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

6) Теперь инициализируем структуру COMMTIMEOUTS. Так как таймауты использовать не будем, заполняем все поля структуры нулями:

//установить таймауты
 timeouts.ReadIntervalTimeout = 0; //таймаут между двумя символами
 timeouts.ReadTotalTimeoutMultiplier = 0; //общий таймаут операции чтения
 timeouts.ReadTotalTimeoutConstant = 0; //константа для общего таймаута операции чтения
 timeouts.WriteTotalTimeoutMultiplier = 0; //общий таймаут операции записи
 timeouts.WriteTotalTimeoutConstant = 0; //константа для общего таймаута операции записи

И записываем структуру таймаутов в порт:

//записать структуру таймаутов в порт
//если не удалось - закрыть порт и вывести сообщение об ошибке в строке состояния
if(!SetCommTimeouts(COMport, &timeouts))
 {
 COMClose();
 Form1->StatusBar1->Panels->Items[0]->Text = "Не удалось установить тайм-ауты";
 return;
 }

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

Иначе - продолжаем инициализацию.

Возникает вопрос: зачем вообще нужны таймауты. Они нужны а том случае, если устройство (программа на ПК) ожидает приём определённого числа байтов, а приходит только часть их. Поэтому чтобы устройство (программа) не ждало неизвестное количество времени приёма недостающих байтов, устанавливают таймауты. Тогда, не получив недостающие байты, устройство (программа) отключится по истечении таймаута и не "зависнет".

Но так как тут у нас работают потоки, которые постоянно находятся в режиме приёма данных, и чтобы не усложнять программу, мы таймауты не используем.

7) Теперь устанавливаем размеры очередей приёма и передачи:

//установить размеры очередей приёма и передачи
SetupComm(COMport,2000,2000);

Эта функция устанавливает внутренний буфер драйвера устройства. В принципе её использование не обязательно. Здесь мы ставим большой объём данных на случай быстрого заполнения буфера устройства при использовании высоких скоростей передачи данных и непрерывного потока данных. Но как показывает практика - они не нужны, так как принимается по 8 байтов (редко больше) - но пока непонятно, с чем это связано. Возможно с тем, что эти значения являются как бы рекомендацией драйверу устройства, и могут им игнорироваться.

Категория: Мои статьи | Добавил: MAS (26.11.2012)
Просмотров: 2909 | Рейтинг: 0.0/0
Всего комментариев: 0
Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]
Категории раздела
Мои статьи [51]
Справочные данные [165]
Справочные данные
Наш опрос
Оцените мой сайт
Всего ответов: 30
Статистика

Онлайн всего: 1
Гостей: 1
Пользователей: 0
Форма входа
Баннер
$$
Друзья сайта
  • Спортсменам
  • Огородникам СЮД(А
  • Строим вместе
  • Мир развлечений
  • Кто хочет похудеть
  • Здоровье у нас одно
  • Юмор
  • Кулинарные рецепты
  • Картинки_Заставки_Демотивоторы
  • Семья
  • Кто на мальчишник
  • Металлообработка
  • Кто на девишник
  • Блоки питания
  • Смерть соседям
  • Радиомедведь
  • Кибермедведь
  • Радиосайт
  • Деревообработка
  • Поиск