суббота, 16 июня 2018 г.

COM порт (RS-232) Асинхронный обмен данными. Чтение данных ч.4 (практика).

    В этой статье мы рассмотрим пример асинхронного приема данных из СОМ порта средствами Delphi. То-есть опрос порта будет производиться непрерывно, до тех пор пока мы его не остановим, а прочитанную информацию она будет записывать в Memo. Чтобы процедура опроса не "подвешивала" наше приложение, мы организуем прослушивание порта в отдельном потоке. Поэтому прежде чем Вам начать практиковаться по настоящей статье, автор предполагает что читатель уже знаком с темой потоков в Delphi. Пример конечно примитивен, поскольку демонстрирует лишь минимум функций, которые можно реализовать для связи по трем проводкам (GND, RX и TX), но эта простота вовсе не снижает интереса к нашей с Вами теме. По скольку дает "живое" представление о принципе асинхронного чтения, да еще и на "сквозном" примере.
    И так приступим:


1)  Поместим на форму следующие компоненты; Listbox - 1шт, Button - 4шт и компонент Memo - 1шт. Расставим их примерно как на рисунке ниже:


Для чего? Мы с Вами напишем программу с помощью которой  нажав кнопку - "Сканировать", сможем увидеть доступные COM порты в нашем ListBoxе.  Кликнув по выделенному порту двойным щелчком мыши, мы к нему подключимся. После подключения нам станет доступна опция чтения, которая после того как мы нажмем на кнопку - "Начать чтение", запустится и будет крутиться в отдельном потоке цикл прослушивания порта, и по мере обнаружения информации в буфере, будет помещать ее в компонент Memo. Ну и собственно на любом этапе работы программы, мы сможем очистить содержимое приема или вовсе разорвать связь с текущим портом, чтобы переключиться на любой другой свободный COM порт, ну или вовсе завершить работу нашего приложения. И так поехали дальше...

2) Объявим глобально следующие переменные:

var
Form1:TForm1;
Adr:PWideChar; //Переменная номера COM порта;
W:WideString; //Промежуточная переменная;
ComFile:THandle; //Хендл ком порта;
Dcb:TDCB; //Структура настроек порта;
ComStat:TComStat; //Переменная состояния порта;
Timeouts:TCommTimeouts; //Переменная таймаутов;
OverRead:TOverlapped;
Buffer:array [0..255] of AnsiChar; //Массив данных AnsiChar;

Btr, Temp, Mask, Signal:DWORD;

Свойство Enabled у Button2 и Button3 установим в false, свойство ReadOnly компонента Memo установим в true;
В событии OnClick Button1 пропишем код для пересчета существующих портов и помещения их в наш ListBox:

procedure TForm1.Button1Click(Sender: TObject);{Сканируем порты, найденные помещаем в listBox;}
Var i:Integer;
begin
 for i:=0 to 10 do
  begin
   ComFile:=CreateFile(Pchar('COM'+intToStr(i+1)), Generic_Read or Generic_Write, 0, nil, open_existing, file_flag_overlapped,0);
    if ComFile<>invalid_handle_value then
    begin
    Listbox1.Items.Add('COM'+ IntToStr(i+1));
    CloseHandle(ComFile);
    end;
   Button1.Enabled:=False;
//Деактивируем клавишу сканирования;
   end;
 end;

3) B событии OnDblClick нашего ListBoxа запускаем и настраиваем выбранный порт

procedure TForm1.ListBox1DblClick(Sender: TObject);
begin
W:=Form1.ListBox1.Items.Strings[form1.ListBox1.ItemIndex];
{Загружаем выбранную
запись в переменную}
Adr:=PWideChar(W);
{Преобразуем ее в тип годный для использования в функции открытия порта (WideChar);}
ComFile:=CreateFile(Adr, Generic_Read+Generic_Write, 0, nil,
Open_Existing, File_Flag_Overlapped, 0); {Открываем порт для асинхронной работы;}
if ComFile=Invalid_Handle_Value  then
 begin
 ShowMessage('
Не удалось открыть порт'); {Если не удается, выводим сообщение об ошибке;}
 exit;
 end;

PurgeComm(ComFile, Purge_TXabort or Purge_RXabort or Purge_TXclear or Purge_RXclear);
//Очищаем буферы приема и передачи и очередей чтения/записи;

 GetCommState(ComFile, DCB);
//Настраиваем DCB настройки порта;
 with DCB do
 begin
 BaudRate:=9600;
 ByteSize:=8;
 Parity:=NoParity;
 StopBits:=OneStopBit;
 end;
 if not SetCommState(ComFile, DCB) then
 begin
 ShowMessage('Порт не настроен');
//Если не удается выводим сообщение об ошибке;
 CloseHandle(ComFile);
 exit;
 end;

if ComFile <> INVALID_HANDLE_VALUE then
begin
GetCommTimeouts(ComFile, Timeouts);
{ Чтение текущих таймаутов и настройка параметров структуры CommTimeouts }
Timeouts.ReadIntervalTimeout:=MAXDWORD;
//Таймаут между двумя символами;
Timeouts.ReadTotalTimeoutMultiplier:=0;
//Общий таймаут операции чтения;
Timeouts.ReadTotalTimeoutConstant:=0;
//Константа для общего таймаута операции чтения;
Timeouts.WriteTotalTimeoutMultiplier:=0;
//Общий таймаут операции записи;
Timeouts.WriteTotalTimeoutConstant:=0;
//Константа для общего таймаута операции записи;
SetCommTimeouts(ComFile, Timeouts);
//Установка таймаутов;
end;

SetupComm(ComFile, 4096, 4096);
//Настройка буферов;
if not SetupComm(ComFile, 4096, 4096) then
//Ошибка настройки буферов;
begin
ShowMessage('
Ошибка настройки буферов');
CloseHandle(ComFile);
exit;
end;

SetCommMask(ComFile, EV_RXchar);
{Устанавливаем маску для срабатывания по событию - "Прием байта в порт"}
ListBox1.Enabled:=False; //
Деактивируем listbox;
Button2.Enabled:=True; //
Активируем кнопку "Разъединить";
Button3.Enabled:=True;
//Деактивируем кнопку "Начать чтение";
end;

Что мы тут сделали?  
Во-первых открыли порт для асинхронной работы: - File_Flag_Overlapped. Во-вторых настроили скорость обмена (9600 бод), количество бит в посылке (8), контроль четности (NoParity), указали количество стоповых бит (OneStopBit). Также мы настроили таймауты и размеры буферов порта, но по большому счету, для нашего примера можно обойтись и без них (если их не указывать и не настраивать, система все настроит автоматически), но раз уже мы их настроили, то пусть будут. В-третьих, мы установили маску - EV_RXchar - ожидание прихода байта. Это необходимо для нашего потока чтения. Смысл какой? В отдельном потоке, который мы сейчас создадим будет крутиться цикл, для которого приход байта в порт (Mask and EV_RXchar) <>0 - будет сигналом для чтения состояния порта, получения количества этих байт и если это количество не равно 0, (If Btr.Size<>0) то запуску чтения содержимого в
переменную буфера ReadFile(ComFile, Buffer, SizeOf(Buffer), Temp, @OverRead); и дальнейшую обработку этой переменной:
Form1.Memo1.Lines.Text:=Form1.Memo1.Lines.Text+(String(buffer)); {Загружаем в Memo содержимое буфера;}
Buffer:=''; //Очищаем переменную буфера; 
В общем как то так... Ну что создаем поток?

4) Создавать поток будем без помощи мастера, для этого прямо в нашем листинге после раздела:

public
   { Public declarations }
end;

Объявим наш поток:
MyThread=class(Tthread)
private
{ private declarations }
protected
procedure execute; override;
// Главная процедура потока;
procedure OutToMemo;
//Процедура вывода в Мемо содержимого переменной буфера;
end;

В раздел глобальных переменных добавим переменную для нашего потока
MyThr:MyThread; //Переменная потока чтения;


Выделяем  procedure execute; override; жмем Ctrl+Shift+C и в теле появившейся главной процедуры запишем:

procedure MyThread.execute;
begin
 OverRead.Hevent:=CreateEvent(Nil, True, True, Nil);
//Сигнальный объект событие для ассинхронных операций;
 While not MyThr.Terminated do
//Пока поток не остановлен;
 begin
  WaitCommEvent(ComFile, Mask, @OverRead);
//Ожидаем события (поступление байта);
  Signal:=WaitForSingleObject(OverRead.hEvent, Infinite);
{Приостанавливаем поток до тех пор пока байт не поступит;}
  if (Signal=Wait_Object_0) then
//Если байт поступил;
  begin
   if GetOverlappedResult(ComFile, OverRead, Temp, true) then
{Проверяем успешность завершения операции;}
   begin
    if ((Mask and EV_RXchar)<>0) then
//Если маска соответствует,
    begin
     ClearCommError(ComFile, Temp, @ComStat);
//Заполняем структуру ComStat;
     Btr:=ComStat.CbInQue;
//Получаем из структуры количество байт;
     If Btr.Size<>0 then
//Если байты присутствуют,
     begin
     ReadFile(ComFile, Buffer, SizeOf(Buffer), Temp, @OverRead);
//Читаем порт;
     Synchronize(OutToMemo);
{Делаем синхронный вызов процедуры загрузки буфера в     
     Memo;}
     end;
     end;
    end
   end;
  end;
 CloseHandle(OverRead.Hevent);
 end;

Для реализации процедуры чтения, выделяем procedure OutToMemo; в разделе объявления нашего потока, жмем Ctrl+Shift+C и в теле появившейся процедуры запишем:

procedure MyThread.OutToMemo; //Процедура вывода в Memo;
begin
Form1.Memo1.Lines.Text:=Form1.Memo1.Lines.Text+(String(buffer));
{Загружаем в Memo содержимое буфера;}
Buffer:='';
//Очищаем переменную буфера;
end;

5) Теперь задействуем запуск только что нами созданной процедуры чтения, для этого дважды
кликаем на Button3 и в появившейся процедуре записываем:

procedure TForm1.Button3Click(Sender: TObject); //Запуск потока чтения;
begin
MyThr:=MyThread.Create(false);
//Создаем поток чтения;
MyThr.FreeOnTerminate:=true;
//Запускаем поток чтения;
MyThr.Priority:=tpNormal;
//Устанавливаем приоритет;
Button3.Enabled:=False;
//Делаем клавишу чтения неактивной;
end;

для возможности разорвать соединение  дважды
кликаем на Button2 и в появившейся процедуре запишем:

procedure TForm1.Button2Click(Sender: TObject); {Очищаем listBox, Прерываем поток, Закрываем соединение;}
begin
if  MyThr <> nil then
//Если поток чтения запущен,
MyThr.Terminate;
//Тогда уничтожаем его;
ListBox1.Clear;
//Очищаем содержимое ListBox1;
CloseHandle(ComFile);
//Закрываем порт;
Button1.Enabled:=True;
//Делаем активной кнопку "Сканировать";
Button2.Enabled:=False;
//Деактивируем кнопку "Разъединить";
Button3.Enabled:=False;
//Деактивируем кнопку "Начать чтение";
ListBox1.Enabled:=True;
//ListBox1 делаем активным;
end;

для очистки содержимого Memo  дважды кликаем на Button4 и в появившейся процедуре запишем:

procedure TForm1.Button4Click(Sender: TObject); {Очистка Memo, буферов приема, передачи и очередей.}
begin
PurgeComm(ComFile, Purge_TXabort or Purge_RXabort or Purge_TXclear or Purge_RXclear);
Buffer:='';
Memo1.Lines.Clear;
end;

теперь в процедуре FormClose закрытия приложения запишем:

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction); //Закрытие програмы;
begin
if  MyThr <> nil then
//Если поток запущен;
MyThr.Terminate;
//Останавливаем его;
CloseHandle(ComFile);
//Закрываем Порт;
end;


Ну вот как бы и все, теперь наш проект готов к запуску, но прежде настроим аппаратную часть.
Для этого нам потребуется любая программка - терминал для отправки данных, 2 COM порта на материнке, если таковые отсутствуют, тогда 2 конвертера USB>COM. Я например использую 2 вот таких переходничка и соединяю их тремя проводками по следующей схеме:



RXD одного конвертера соединяем с TXD другого и наоборот, плюс массу одного конвертера соединяем с массой другого. Если Вы будете использовать штатные COM порты на матплате то нужно будет соединить их нульмодемным кабелем.

Один переходничок у меня в системе определился как COM3, второй как COM4. Запускаем наше приложение, подключаемся к порту COM3 и нажимаем нашу кнопку начала чтения, на этом "прослушивание" порта началось. Теперь запускаем программку терминал, для примера я использую  COM Port Toolkit 4.0 После запуска терминал нужно настроить - выбрать порт для подключения (в моем случае это COM4) и установить настройки DСB (скорость, количество бит информации, контроль четности, количество стоповых бит) такие же как у нашей программки:
 
Далее заходим в пункт меню "Сообщение" - "Отправить" и через его интерфейс пробуем отправлять различный текст, можно из файла, можно просто какие нибудь одиночные слова
или символы,

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






1 комментарий:

  1. Ура получилось наконец-то! Умом догадывался как, а тут все по полочкам. Спасибо. Жаль что редко пишешь.

    ОтветитьУдалить

Примечание. Отправлять комментарии могут только участники этого блога.