В этой статье мы рассмотрим пример асинхронного приема данных из СОМ порта средствами Delphi. То-есть опрос порта будет производиться непрерывно, до тех пор пока мы его не остановим, а прочитанную информацию она будет записывать в Memo. Чтобы процедура опроса не "подвешивала" наше приложение, мы организуем прослушивание порта в отдельном потоке. Поэтому прежде чем Вам начать практиковаться по настоящей статье, автор предполагает что читатель уже знаком с темой потоков в Delphi. Пример конечно примитивен, поскольку демонстрирует лишь минимум функций, которые можно реализовать для связи по трем проводкам (GND, RX и TX), но эта простота вовсе не снижает интереса к нашей с Вами теме. По скольку дает "живое" представление о принципе асинхронного чтения, да еще и на "сквозном" примере.
И так приступим:
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;
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); {Делаем синхронный вызов процедуры загрузки буфера в
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) Поместим на форму следующие компоненты; 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;
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]; {Загружаем выбранную
begin
W:=Form1.ListBox1.Items.Strings[form1.ListBox1.ItemIndex]; {Загружаем выбранную
запись в переменную}
Adr:=PWideChar(W); {Преобразуем ее в тип годный для использования в функции открытия порта (WideChar);}
ComFile:=CreateFile(Adr, Generic_Read+Generic_Write, 0, nil,
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;
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:=''; //Очищаем переменную буфера;
Buffer:=''; //Очищаем переменную буфера;
В общем как то так... Ну что создаем поток?
4) Создавать поток будем без помощи мастера, для этого прямо в нашем листинге после раздела:
public
{ Public declarations }
end;
Объявим наш поток:
MyThread=class(Tthread)
private{ private declarations }
protected
procedure execute; override; // Главная процедура потока;
procedure OutToMemo; //Процедура вывода в Мемо содержимого переменной буфера;
end;
private{ private declarations }
protected
procedure execute; override; // Главная процедура потока;
procedure OutToMemo; //Процедура вывода в Мемо содержимого переменной буфера;
end;
В раздел глобальных переменных добавим переменную для нашего потока
MyThr:MyThread; //Переменная потока чтения;
Выделяем procedure execute; override; жмем Ctrl+Shift+C и в теле появившейся главной процедуры запишем:
procedure MyThread.execute;
beginOverRead.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;
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;
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;
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;
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;
begin
PurgeComm(ComFile, Purge_TXabort or Purge_RXabort or Purge_TXclear or Purge_RXclear);
Buffer:='';
Memo1.Lines.Clear;
end;
теперь в процедуре FormClose закрытия приложения запишем:
begin
if MyThr <> nil then //Если поток запущен;
MyThr.Terminate; //Останавливаем его;
CloseHandle(ComFile); //Закрываем Порт;
end;
Ну вот как бы и все, теперь наш проект готов к запуску, но прежде настроим аппаратную часть.
Для этого нам потребуется любая программка - терминал для отправки данных, 2 COM порта на материнке, если таковые отсутствуют, тогда 2 конвертера USB>COM. Я например использую 2 вот таких переходничка и соединяю их тремя проводками по следующей схеме:
Один переходничок у меня в системе определился как COM3, второй как COM4. Запускаем наше приложение, подключаемся к порту COM3 и нажимаем нашу кнопку начала чтения, на этом "прослушивание" порта началось. Теперь запускаем программку терминал, для примера я использую COM Port Toolkit 4.0 После запуска терминал нужно настроить - выбрать порт для подключения (в моем случае это COM4) и установить настройки DСB (скорость, количество бит информации, контроль четности, количество стоповых бит) такие же как у нашей программки:
или символы,
в общем если Вы все выполняли последовательно и внимательно, то в Mемо нашей программы будет отображаться все, что Вы будете отправлять из терминала. На всякий случай прилагаю исходник нашего с Вами примера. На этом пока все, всем удачи. В следующей статье рассмотрим наверное отправку данных в порт и немного доработаем наш сегодняшний пример.
Ура получилось наконец-то! Умом догадывался как, а тут все по полочкам. Спасибо. Жаль что редко пишешь.
ОтветитьУдалить