Версия для печати


Асинхронный режим чтения из Com-порта
http://www.delphikingdom.com/asp/viewitem.asp?catalogID=723

Александр Терехов
дата публикации 06-12-2002 14:58

Асинхронный режим чтения из Com-порта

Вступление

Порядок запуска и работы "службы" (назовем все описываемое ниже так) Com-портов состоит из нескольких достаточно хорошо описанных шагов ( см. статьи по теме ):
  1. Инициализация Com-порта посредством вызова функции CreateFile.
  2. Установка параметров Com-порта посредством последовательного вызова функций GetCommState и SetCommState, а также SetupComm.
  3. Установка параметров тайм-аутов для чтения и записи - GetCommTimeouts и SetCommTimeouts.
  4. Собственно записи в Com-порт - WriteFile и чтения из него - ReadFile.
  5. Закрытие порта по окончанию работ CloseHandle.
Очень большой сложности описанные выше шаги не представляют, однако реализация чтения данных из порта в асинхронном (неблокирующем) режиме заставляет почесать затылок. Об этом и поговорим.

Чтение из Com-порта.

Судя по контексту справки, касающейся функции CreateFile, для "отлова" момента поступления данных в Com-порт следует использовать функцию WaitCommEvent. Предварительно установив маску SetCommMask на то событие, которое хотелось бы отследить. Нужное событие наступает - вызываем функцию ReadFile для чтения поступающих данных.

Казалось бы все в порядке, но... Вызов функции WaitCommEvent насмерть тормозит приложение, пока какие-либо данные не поступят в Com-порт.

Можно конечно, просто взять и запустить в непрерывном цикле ReadFile, однако приложение хотя и будет как-то шевелиться, но это шевеление скорее всего будет напоминать предсмертные судороги.

Как выход из ситуации многие предлагают использовать потоки (thread), забывая при этом описать как это делать :)

Итак потоки.

В модуле Classes для потоков определен специальный класс TThread. Для создания потоков специалисты рекомендуют использовать именно его, а не создавать потоки используя BeginThread и EndThread, т.к. библиотека VCL не является защищенной для потоков в такой реализации. Следуя советам экспертов, для организации контроля поступающих данных в Com-порт и будем использовать готовый класс TThread.

В раздел interface определим тип переменных этого класса, переопределив только один метод класса - Execute, ну и дополнительно объявим свой метод, который и займется опросом Com-порта.

Type
  //определим тип TComThread - наследника класса TThread
  TCommThread = class(TThread)
  private
  //процедура, занимающаяся опросом порта
    Procedure QueryPort;
  protected
  //переопределим метод запуска потока
     Procedure Execute; override;
end;

Далее в разделе глобальных переменных определим поток-переменную полученного выше типа
CommThread:TCommThread; 
//наш поток, в котором будет работать процедура опроса порта
Затем в разделе implementation начинаем ваять.
ВНИМАНИЕ!!!
К этому времени порт уже должен быть инициализирован функцией CreateFile.
  1. 1. Инициализируем поток, используя метод Create.

    Procedure StartComThread;
    //инициализация нашего потока
    Begin {StartComThread}
    	//пытаемся инициализировать поток
    	CommThread:=TCommThread.Create(False);
    	//проверяем получилось или нет
    	If CommThread = Nil Then
    	Begin {Nil}
    		//ошибка, все выключаем и выходим
    		SysErrorMessage(GetLastError);
    		fmMain.btnStop.Click;
    		Exit;
    	End; {Nil}
    End; {StartComThread}
    

    Куски кода взяты из файла проекта, поэтому нажимание на кнопку btnStop главной формы fmMain - это "примочки" примера, не обращайте внимания.

  2. Запускаем процедуру опроса порта в нашем потоке.

    Procedure TCommThread.Execute;
    Begin {Execute}
    	Repeat
    		QueryPort;
    		//процедура опроса порта будет производиться 
    		//пока поток не будет прекращен
    	Until Terminated;
    End;  {Execute}
    

  3. Реализуем асинхронные опрос порта и чтение из него данных

    Procedure TCommThread.QueryPort;
    Var
    	MyBuff:Array[0..1023] Of Char;//буфер для чтения данных
    	ByteReaded:Integer; //количество считанных байт
    	Str:String;         //вспомогательная строка
    	Status:DWord;       //статус устройства (модема)
    Begin {QueryPort}
    //получим статус COM-порта устройства (модема)
    	If Not GetCommModemStatus(hPort,Status) Then
    	Begin {ошибка при получении статуса модема}
    	//ошибка, все выключаем и выходим
    		SysErrorMessage(GetLastError);
    		fmMain.btnStop.Click;
    		Exit;
    	End;  {ошибка при получении статуса модема}
    	//Обработаем статус устройства (модема) и будем 
            //включать(выключать) лампочки
    	//готовность устройства (модема) получать данные
    	fmMain.imgCTSOn.Visible:=((Status AND MS_CTS_ON)=MS_CTS_ON);
    	//готовность устройства (модема) к сеансу связи
    	fmMain.imgDSROn.Visible:=((Status AND MS_DSR_ON)=MS_DSR_ON);
    	//принимаются данные с линии сигнала
    	fmMain.imgRLSDOn.Visible:=((Status AND MS_RLSD_ON)=MS_RLSD_ON);
    	//входящий звонок
    	fmMain.imgRingOn.Visible:=((Status AND MS_RING_ON)=MS_RING_ON);
    
    	//читаем буфер из Com-порта
    	FillChar(MyBuff,SizeOf(MyBuff),#0);
    	If Not ReadFile(hPort,MyBuff,SizeOf(MyBuff),ByteReaded,Nil) Then
    	Begin {ошибка при чтении данных}
    		//ошибка, все закрываем и уходим
    		SysErrorMessage(GetLastError);
    		fmMain.btnStop.Click;
    		Exit;
    	End; {ошибка при чтении данных}
    	//данные пришли
    	If ByteReaded>0 Then
    	Begin {ByteReaded>0}
    		//посчитаем общее количество прочитанных байтов
    		ReciveBytes:=ReciveBytes+ByteReaded;
    		//преобразуем массив в строку
    		Str:=String(MyBuff);
    		//отправим строку на просмотр
    		fmMain.Memo1.Text:=fmMain.Memo1.Text+ Str;
    		//покажем количество считанных байтов
    		fmMain.lbRecv.Caption:='recv: '+IntToStr(ReciveBytes)
    			+' bytes...';
    	End; {ByteReaded>0}
    End; {QueryPort}
    
    

На этом по поводу использования потоков для считывания данных из Com-порта, пожалуй, все.

Прилагающийся пример

Следуя правилам хорошего тона, прикладываю ко всему написанному работающий пример.
В примере используется самое доступное устройство для пользователей интернет - модем (на Com-порту). В качестве "примочек" я использовал лампочки, которые включаются (или выключаются) при изменении статуса модема. Можно было прикрутить лампочки-детекторы входящих-выходящих сигналов, но вместо них используются счетчики байтов.
Реализация кода включения-выключения не самая лучшая: можно было бы использовать TImageList для хранения изображений лампочек. Но почему-то ??? (кто знает почему - напишите) использование ImageList.GetBitmap при наличии запущенного потока "подвешивает" приложение насмерть. Причем это происходит под Windows'98, если тоже самое делать под Windows'95, то все в порядке.

Для проверки работоспособности примера попробуйте понабирать AT-команды

Да, еще. Проект написан под Delphi3, при использовании Delphi более свежих версий возможны ошибки "несовпадения типов".
В этом случае поменяйте типы "ошибочных" переменных с Integer на Cardinal.

Скачать проект — ComPort.zip (17K)
архив обновлен автором 10.12.02 (добавлена функция обработки кодов ошибок с переводом их значений на русский язык)

Терехов Александр
декабрь 2002г.
Другие небольшие статьи,
примеры и программы можете найти на сайте автора