Главная » Delphi » Прием и передача данных с помощью параллельного потока

0

Мы рассмотрим прототип "эмулятора терминала" на том же примере с "часами". Я усложнил задачу, выставив свои "часы" в такой режим: они самостоятельно каждую секунду посылают значение секунд, а в начале каждой минуты выдают полную строку времени и даты (формат строки см. ранее). Старая возможность — получить в любой момент полное время по запросу посылкой команды $А2 — также имеется. Для того чтобы сформировать такой асинхронный протокол, мы и организуем параллельный поток, который будет посылать нам сообщения, если в буфере порта что-то есть. Для этого надо сначала установить событие, которое будет отслеживаться в потоке (не пользовательское сообщение, которое мы будем посылать форме— это само собой, — а именно внутреннее событие порта). Это делается с помощью функции SetConimMask, в которой устанавливается некий флаг. Из всего разнообразия вариантов этого флага мы выберем значение ev rxchab, тогда порт нам будет сигнализировать о том. что в буфере что-то есть (что именно— рассмотрим далее). О том, что установленное событие произошло, можно определить через функцию WaitCoranEvent.

Последняя представляет собой, как надо понимать, замкнутый цикл ожидания установки бита 0 регистра статуса UART в единичное состояние, что и означает "байт данных получен". Такой вывод я делаю на основе официального описания работы этой функции [14,31J: если не использовать "оверла- пинг", то она просто-напросто должна затормозить всю систему (на самом деле только, конечно, поток, в котором она выполняется), пока принятый байт не обнаружится. Насколько это описание соответствует действительности, посмотрим позже.

Заметки на полях

"Затормозимся" и мы на этом моменте — скажите сами, дорогой читатель, такой подход — это правильно? Если вам интересно, то в приложении 4 вы можете посмотреть, как устроен аналогичный механизм в абсолютно однозадачной операционной среде микроконтроллера. Так вот, там тоже предлагается аналогичная "тормозящая" процедура, которая, однако, может при правильном построении программы прерыввться, извините за тавтологию, прерываниями и в таком случае особенно ничего не тормозить. Но предлагать процедуру (по смыслу это — именно процедура, а никакая не функция), которая потенциально может намертво заморозить программу — тут я, вероятно, чего-то недопонимаю. Такие процедуры с замкнутым циклом, условие выхода из которого может не выполниться никогда — вообще нонсенс для высокоуровневого программирования. Почему было не сделать просто-напросто обычное Windows-событие по аппаратному прерыванию порта? Объяснение натянуть на эту ситуацию можно— все эти функции яаляются универсальными, применимыми к любой записи/чтению в файл, и все же это разработчиков, на мой взгляд, не извиняет.

Разумеется, выход из этой ситуации также предлагается — сильно напоминающий попытки достать одно известное место через другое не менее известное, но все же выход. Но прежде чем мы его рассмотрим, остановимся еще на особенностях работы waitcommEvent. Предположим, мы все сделали, как написано. Что и когда вернет нам эта функция? Если верить "пособию", то она вернет нам то самое значение ev rxchar и тогда, когда в приемном буфере окажется ровно один байт. А если они поступают быстро и программа не успевает их обрабатыват ь? Тогда мы обнаружим первый и второй байты (см. [32]), а об остальных просто не узнаем. И читать нужно не один байт (хотя мы и настраивались именно на это), а определять, сколько байтоа в буфере, и столько же читать. Это делается через функцию с неподходящим к случаю названием dearCemmErrer, которая возвращает структуру состояния порта (ccmstat), а в ней — размер буфера.

Я сейчас опишу особенности работы всех основных вариантов асинхронного чтения, как они предлагаются в различных источниках, по мере их усложнения. До собственно организации потока мы дойдем позднее, когда будет описан окончательный вариант программы, а пока предположим, что у нас уже имеется поток, в котором выполняется некий цикл асинхронного чтения, и при наступлении события ev rxchar он посылает главной форме сообщение wmcomport. Осталась также аналогичная описанному ранее процедура посылки команды, т. е. записи в порт через WriteFiie. При запуске программы на экране должно отображаться все, что часы посылают, а при посылке команды — еще и принятая строка. Все это также должно работать и в первом варианте работы часов, когда они сами ничего не посылают. Из описанного получается, что в параллельном потоке можно попробовать сделать примерно вот такой замкнутый цикл (без всяких отдельных событий типа ev bxchar):

var

StatCOM : TComStat; bb:boolean; TMask:DWord;

bb:=True;

while bb do {бесконечный цикл) begin

WaitcommEvent(hCOM,TMask,nil); {ожидаем приема) ClearCommError(hCOM,xn,0StatCOM);

{получаем состояние COM в StatCOM) xn:=StatCOM.cbInQue;

{в StatCOM.cblnQue – реальное количество байтов в буфере) if ReadFile(hCOM,ab,xn,xn,nil) then (читаем байты в ab) SendMessage(Forml.Handle,wmCOMPORT,1,0);

(посылаем сообщение главному окну)

end;

Будет ли это дело работать? При запуске из-под Windows 98 — просто отлично. Мало того, хотя, судя по описанию ранее, функция WaitcommEvent должна тормозить если не всю программу (она все же в параллельном потоке), то, по крайней мере, намертво блокировать доступ к порту, пока туда ничего не поступило — этого не происходит. Если вы попробуете из главного окна послать в порт команду через writeFile, она отлично пройдет даже и в первом варианте работы часов (во втором варианте с автомат ической посылкой WriteFile должна в любом случае срабатывать, когда при приходе байта WaitcommEvent "отпускает" систему до окончания его обработки). Увидев такую картину, я даже засомневался — зря, что ли ругался, все работает на самом деле гораздо лучше, чем описано? Но ситуация разъяснилась при переходе к Windows ХР — там-то все пошло, как положено и даже хуже. В частности, WriteFile, несмотря ни на какие наши Timeout, намертво "висла" и "вешала" при этом всю программу до прихода ближайшего байта, а если его так и не поступало, то Windows ХР еще и услужливо сообщала в заголовке, что "программа не отвечает". Получается, что и там и там — "недокументированные особенности", но если в 98-й они нам на руку, то в ХР — строго наоборот. Что делать? Придется прибегнуть к "оверлапингу".

Идея "оверлапинга" (overlapping) следующая: WaitcommEvent возвращает управление сразу, но мы должны теперь уже отслеживать даже не событие, заданное через установку флага ev rxchar, а некое другое, которое устанавливается в структуре overlapped через функцию createEvent. Красиво получается— сообщение, о том, что было сообщение, о том, что было сообщение…

В секции var надо объявить структуру

StrOvr : TOverlapped;

и в createFile добавить предпоследним параметром флаг file_flag_ overlapped. Остальные изменения в программе в связи с введением "оверла- пинга" будут приведены далее. Если мы запустим программу на этой стадии внесения изменений, она не будет работать вообще (точнее, будет непрерывно выполнять пустую операцию чтения и посылки сообщения), т. к. WaitCommEvent теперь не ожидает байта, а сразу прерывается. Для того чтобы все, что после нее выполнялось только а нужном случае, мы сначала попробуем применить вот какой прием: не будем проверять никаких лишних сообщений, а просто дождемся, пока WaitCommEvent возвратит нам через TMask нужную величину ev_rxchar:

while ЬЬ do {бесконечный цикл) begin TMask:=0;

WaitCommEvent(hCOM,TMask,@StrOvr); (ожидаем приема)

if tma s k=Ev_rxchar then

begin

ClearCommError(hCOM,xn,@StatCOM);

(получаем состояние COM в StatCOM) xn:=statcom.cblnQue;

(в StatCOM.cblnQue – реальное количество байтов в буфере) if ReadFile(hCOM,ab,xn,xn,@StrOvr) then /читаем байты в abI SendMessage(Forml.Handle,wmCOMPORT,1, 0); (посылаем сообщение) end; end;

Как ни удивительно, но этот абсолютно "незаконный" механизм будет работать! Байты принимаются, посылка команды работает, но… С одной оговоркой — более-менее "как надо" все это будет работать только в Windows ХР. В Windows 98 же нужное событие, когда TMask принимает значение ev rxchar (одно из миллионов в процессе непрерывного цикла) вполне может потеряться. На практике это не приведет к потере принятых байтов — в следующий раз считается два байта, ну и что? Но все это крайне некрасиво — а если следующего раза не будет? И вообще, наша цель — чтобы байты читались строго в момент прихода, а не через час после него. Так что придется делать согласно "пособию", а чтобы у вас на отладку нормально работающей процедуры ушло не больше недели (у меня по первому разу ушло три дня), я сейчас покажу самый простой и надежно работающий вариант. В этом варианте убраны все лишние проверки, по которым все равно непонятно, что делать, и опыт многолетней эксплуатации показывает, что эти проверки не нужны (но это не значит, что нужно о них забыть, если вы, например, делаете свой компонент Delphi для распространения, см. главу /):

while bb do {бесконечный: никлI begin

if not WaitComrnEvent (hCOM, TMask, @StrOvr) then /запускаем прием/

if GetLastError = ERROR_IO_PF.NPING

then WaitForSingleObject(StrOvc.hEvent, INFINITE);

I otiuiaev приема до бесконечности I CiearComrnError (hCOM, xr., @StatCOM) ;

{получаем состояние COM a StatCQM) xn:=3tatC0M.coinQue;

I в StatCCM. cbJnQue – реальное колтество байтов в буфере I if xn > 0 then

if RcadFile(hCOM,ab,xn,xn,@StrOvr) then {читаем байты в abj SendMessage(Forml.Handle,wmCOMPORT,1,0); {посылаем сообщение I end;

Теперь, зная как и что нужно делать в потоке, возьмемся за переделку программы. Новый проект COMproba я разместил в папке Glava21\2 (перенеся его из папки Glava21\l). Объявим в секции var новые переменные:

var

ThreadID:dword; COMThread : THandle; Stat.COM : TComStat; FlagCOM:boolean=False; StrOvr : TOverlapped; bb:boolean-True;

Я увеличил емкость массива ab до 1024 символов — хотя это в данном случае необязательно, в принципе максимум может быть равен емкости буфера, т е. 128 байтам. В функции createFile второй от конца параметр (под названием dwFiagsAndAttгibutes— пока там стоял ноль) заменим на константу FILE_FIAG_OVERLAPPED, И ВО всех вызовах функций ReadFile, VJriteFile И WaitComrnEvent последний параметр (nil) заменим на указатель на объявленную структуру @strOvr. В процедуру инициализации порта inicOM добавим оператор:

St rOvr.hEven t:=Crea teEvent(nil,True, False,nil);

Уберем с формы все компоненты для вывода (оставив только Label"7 для отображения текущего порта и comboBoxi), и вместо них поставим компонент Memo, у которого установим свойство Color в с 1 Wavy, свойство Font в "полужирный" 10 кегля цвета clAqua, в свойстве ScrollBars установим ssvert icai (как ни странно, но Memo прокручивается по мерс поступлении данных автоматически и специально об этом думать не надо), a Readonly в True. Свойство Lines ОЧИСТИМ.

Теперь напишем полностью процедуру, которая будет выполняться в потоке (она из одного приведенного ранее цикла и состоит). Назовем ее F.e-xJComPort:

procedure ReaaComPort; /это поток и естьj

var TMask:DWord;

begin

while bb do (бесконечный цикл) begin

if not WaitcommEvent(hCOM,TMask,SStrOvr) then (запускаем прием)

if GetLastError = ERROR_I0_PENDIKG

then WaitForSingleObject (StcOvr.hF.vent, INFINITE) ;

{ожидаем приема до бесконечностиj ClearCommError(hCOM,xn,@StatCOM);

(получаем состояние COM в StatCOM} xn:=3tatCOM.cblnQue;

{в StatCOM. cblnQue – реальное количество байтов я буфере) if xn > 0 then

if ReadFile(hCOM,ab,xn,xn,SStrOvr) then (читаем байты в ab) SendMessage(Forml.Handle,wmCOMPORT,1,0); (поси.паем сообщение) end; end;

Ее нужно разместить в самом начале программы. Теперь нужен обработчик нашего сообщения wmCOMPORT. Для этого в самом начале модуля, после uses, вставим такое объявление:

const

wmCOMPORT=wm_User+ll; (сообщение от порта) Ниже, в секции private добавим:

{ Private declarations }

procedure ReceiveCOM(var MSG:TMessage); message wmCOMPORT; Сама процедура ReceiveCOM будет выглядеть так:

procedure TForml.ReceiveCOM(var MSG:TMessage); (чтение очередного байта no сообщению wmCOMPORTI var i:integer; begin

ttime: =Tin>e; (фиксируем время прихода байта) if MilliSecondsBetween(told,ttime)>500 then

(если больше полсекунды, очищаем строку}

st: = ";

for i:=1 to xn do st:=st+hexb(ab[i])+’ ‘;

if Forml.Memol.Lines.Count>65000 then Forml.Memol.Lines.Clear; (больше 64К строк нельзя) Forml.Memol.Lines.Add(st); (выводим в Memo) told:=»ttime; (для сравнения в следующий раз) if FlagCOM then (если это была посылка) begin FlagCOM:=False; st:=”; (очищаем строку) Timerl.Enabled:=False; (выключаем таймер) end; end;

Здесь два момента пока непонятны— во-первых, зачем вначале очищать строку каждые полсекунды, и откуда берется для этого значение told? Очищать строку мы будем для того, чтобы принятые байты, которые разделены временным промежутком, у нас выводились построчно. Так как у нас промежуток равен 1 секунде, то полсекунды будет в самый раз. Если же они поступают подряд, то выведутся в одной строке. Значение told мы пока устанавливаем только автоматически при очередном выводе строки в Memol. Второй момент, который связан с таймером, сейчас прояснится. Установим на форму таймер, И его свойство Timerl.Enable В False. Для события onTimer напишем такой обработчик:

procedure TForml.TimerlTimer(Sender: TObject); begin (таймер на ошибку) ttime:=Time;

if SecondsBetween(told,ttime)>1 then (если через две секунды ничего нет) begin

Timerl.Enabled:=False; FlagCOM:=False;

Application.MessageBoxt’Устройство не обнаружено’,’Error’,MB_OK); end; end;

А когда мы его будем взводить? Естественно, по нажатию кнопки Запрос:

procedure TForml.ButtonlClick(Sender: TObject); begin (запрос}

if (hCOM-O) or (hCOM=INVALID_HANDLE_VALUE) then exit;. {если порт еще не инициализирован – выход}

PurgeComm(hCOM,PURGE_RXCLEAR); (очищаем буфер на всякий случаи) xb:=$А2;

WriteFile(hCOM,xb,1,xn,SStrOvr); stПослано: ‘+hexb(xb);

Forml.Memol.Lines.Add(st); {записали в Memo)

st:=”; {очистили строку)

told:=Time; {зафиксировали момент посылки}

FlagCOM:=True; (обозначаем посылку)

Timerl.Enabled:"True; (запускаем таймер)

end;

Осталось расправиться с событием ev rxchar и с потоком. Последняя операция, которая обычно так пугает новичков, проще простого. Добавим в самый конец процедуры IniCOM такие строки:

SetCommMask(hCOM,EV_RXCHAR); {отслеживаем событие в буфере) COMThread := CreateThread(nil,0,@ReadComPort,nil, 0,ThreadID);

(создаем поток) told:=Time; (фиксируем время запуска) st: = "; (очищаем строку для вывода)

Перед тем как закрывать форму или менять порт, поток надо уничтожать:

procedure TForml.FormDestroy(Sender: TObject); begin (уничтожаем COMl TerminateThread(COMThread, 0); (уничтожаем поток) CloseHandle(hCOM); (закрываем COM} end;

procedure TForml.ComboBoxlSelect(Sender: TObject); begin

TerminateThread(COMThread,0); {уничтожаем поток}

CloseHandle(hCOM); {закрываем старый COM}

stcom:=ComboBoxl.Text; {устанавливаем порт СОМ1,2,3,4)

IniCOM;

end;

Все! На основе этой программы вы спокойно можете делать любой приемопередатчик через последовательный порт. Естественно, выводить совершенно необязательно в НЕХ-форме. если функцию hexb заменить на chr, то вывс- дугся символы (туг нужно быть аккуратным, т. к. вывод символа с нулевым кодом в некоторые компоненты может все порушить), а если на intToStc. то выведутся десятичные числа.

Результат работы программы СОМргоЬа, как прототипа "эмулятора терминала", показан на рис. 20.2. Здесь вы видите, как часы в конце минуты выдали полную строку времени, а потом мы то же самое запросили отдельно.

Рис. 20.2. Результат работы модернизированной программы СОМргоЬа

Но прежде чем перейти, наконец, от теоретических примеров к практическим, я хочу сделать одно замечание. Если честно, то в большинстве случаев писать самим такие программы большого смысла нет. Есть множество различных готовых процедур, в том числе — оформленных в виде компонентов Delphi. Есть и универсальные для всех сред программирования компоненты ActiveX (правда, на поставляемый с Visual Basic компонент ActiveX под названием MSComm требуется отдельная платная лицензия). Технологию ActiveX, которая позволяет работать и с СОМ и с USB, и даже прямо с GPS- навигаторами, которыми мы займемся далее, мы в этой книге обходим — это отдельная обширная тема, к тому же подход этот по причинам, отчасти изложенным в главе 18, не является оптимальным. Теоретически мы при использовании любых сторонних компонентов теряем в гибкости — если вы изучите возможности использованных нами ранее функций подробнее [31], то увидите, что они позволяют осуществить очень много разнообразных вариантов. Другой вопрос — что гибкость эта не особенно и требуется, универсальные процедуры побайтного приема/передачи вполне успешно работают в большинстве случаев. И сейчас вы сами сравните, что проще — прямое программирование через API или использование таких компонентов.

Источник: Ревнч Ю. В.  Нестандартные приемы программирования на Delphi. — СПб.: БХВ-Петербург, 2005. — 560 е.: ил.

По теме:

  • Комментарии