Главная » Delphi » О программах реального времени

0

Любой асинхронный обмен в одну сторону — когда с устройства поступает некий непрерывный поток данных — требует от принимающей стороны реализации режима реального времени. В этом режиме основная задача— "не потерять ни капли жира с этого драгоценного гуся" (" Три мушкетера"), т. е. пи одного байта данных. Данные передаются обычно кадрами, отдельные байты в которых часто связаны между собой (например, они образуют многобайтовые числа), и ошибка в приеме такого кадра может дорого обойтись. Windows принципиально не может обеспечить режим реального времени, если это касается длительностей порядка миллисекунд и даже десятков миллисекунд— в этом случае, например, если вы параллельно со своей программой запустили что-то ресурсоемкое (хотя бы просто инициализировали запуск другой достаточно громоздкой программы), то ваша программа будет заторможена, и вы можете что-то потерять. Конечно, на практике все эти соображения могут относиться в основном к случаю сравнительно медленных компьютеров, но тут дело не в самой по себе скорости работы. Максимальное время переключения между потоками в семействе NT составляет порядка миллисекунд даже на быстрых компьютерах и линейно растет с увеличением числа потоков (сравните— за 1 мс можно передать/принять целый байт со скоростью всего 9600 бод). Среднее время может быть намного меньше, но в общем случае гарантий не может дать никто. Разумеется, мы можем из-за этого потерять байт-другой и при передаче запрос/ответ, но вероятность этого существенно меньше, т. к. нам не надо следить за системой непрерывно. Обычное значение кванта времени, выделяемое конкретному потоку, составляет не менее нескольких десятков миллисекунд, и за это время буфер обычного размера (см. далее) мы гарантированно успеваем очистить, а вот в случае непрерывного поступления данных это может оказаться не так.

Способ обеспечения режима для конкретной программы, более-менее напоминающего режим реального времени, можно в принципе обеспечить с помощью регулирования приоритета конкретного потока (thread). У потоков может быть семь уровнен относительных приоритетов (от Idle до Time critical), а у процесса — шесть классов (от Idle class до Real time class). При назначении общего приоритета они комбинируются. Вот так можно обеспечить процессу и его потоку максимальный приоритет [40]:

var

PricrityCiass, Priority: Integer; Iзапоминаем для восстановления:j

PriorityCiass := GetPrioricyClass(GetCurrentProcess); Priority :•= GetThreadPriori’cy (GetCurrentThread) ; Iустанавливаем наивысший:}

SetPriorityClass(GetСл:rrentPгосess, REALTIME_PRIORITY_CLASS); SetThreadPrior i ty(GetCurrentThread, THREAD_PRIORITY_TIME_CRITICAL);

Iвосстанавливаем обратно:}

SetThreadPriority(GetCurrentThread, Priority); SetPriorityClass(GetCurrentProcess, PriorityClass);

В реальности это следует делать только иа достаточно короткое время, потому что иначе, если даже Windows рано или поздно и не рухнет, то работать в такой системе с чем-то еще будет очень затруднительно. Лучше все это делать в консольном приложении, запускаемом в режиме командной строки, когда графическая оболочка с ее чудовищным количеством сообщений не загружена. Вопрос, однако: разве это жизнь? Лично я бы в таком случае лучше сделал однозадачную DOS-программу, которая гарантированно владеет всеми ресурсами, а самое правильное решение— использовать ОС QNX, которая специально для таких целей имеет гарантированное максимальное время переключения между задачами.

Но вот, например, для периодического точного измерения времени коротких процессов этот механизм в совокупности с введенной, начиная с процессоров Pentium ММХ (а также и с AMD К5), командой rcicsc (Read Time Stamp Counter), вполне можно использовать. Команда rdtsc возвращает в регистрах edx:eax восьмибайтное слово, равное количеству "тиков" CPU, прошедшего с момента его последнего сброса. Так как когда-то никто не был уверен, что команда поддерживается данным конкретным ассемблером (например, включенным в состав Object Pascal), ее традиционно вызывают просто в виде опкода OF 31:

var TickLo, TickHi:dword;

asm

dw 310Fh; {rdtsc} mov TickLo, eax mov TickHi, edx

end;

Эту процедуру вызывают в начале и по окончании события, время которого измеряется, и вычитают из второго значения первое. Причем вычитание можно произвести прямо в ассемблерной процедуре:

asm

dw 310Fh sub eax, TickLo sbb edx, TickHi mov TickLo, eax mov TickHi, edx

end;

На время этого измерения потоку назначают максимальный приоритет по методике, указанной ранее. В результате вы получаете в TickHi ,-TickLo точное значение "тиков" процессора за интересующий вас период, что потенциально может дать чудовищную точность. Подводный камень тут один: превратить это в единицы времени можно только, зная достаточно точно частоту процессора, а это обычно проблема, т. к. измерить ее можно либо через системный таймер, либо через RTC (Real Time Clock, часы реального времени), которые находятся в CMOS-чипе с резервной батарейкой. Чтобы убедиться, насколько может врать системный таймер, установите на форм\ компонент Memo и запустите по какому-нибудь событию простую программку, которая выполняет четырьмя разными способами секундную задержк>. и измеряет ее длительность:

uses DateUtils;

var

tt,told:TDateTime; i:integer;

Memol.Lines.Clear;

tol’d:=Time;

for i:=1 to 1000 do

sleep(1);

tt:=Time; (измеряем время)

st:=IntToStr(MilliSecondsBetween(tt,told));

Memol.Lines,Add(st);

told:=Time;

for i:=1 to 100 do

sleep(lO);

tt:=Time; (измеряем времяI

st:=IntToStr(MilliSecondsBetweenttt, told));

Memol. Lines .Add (st.) ;

told:=Time;

for i:=l to 10 do

sleep(100);

tt:=Time; (измеряем время)

st:=IntTcStr(MilliSecondsBetween(tt,told));

Memol.Lines.Add(st);

told;=T.ime;

sleep(1000);

tt:=Time; (измеряем время) st:=IntToStr(MilliSecondsBetween(tt,told)); Memol.Lines.Add(st);

Ручаюсь, что по выполнении этой программы у вас глаза на лоб полезут, особенно если вы ее повторите несколько раз. Вот что выводится в Memo в одном из моих экспериментов (в среде Windows 98):

5770 1480 1049 979

Первое значение может "гулять" в пределах нескольких секунд, но даже относительно длинные промежутки отмеряются с совершенно неприемлемой для измерения времени погрешностью порядка 2—5%. Отметим, что в Windows ХР, согласно моим наблюдениям, ситуация одновременно и хуже и лучше: малые промежутки времени вообще не отмеряются, можно сказать, никак (первое число составило около 15 секунд), а большие (секунда и выше) измеряются точнее. Так что обычно частоту процессора измеряют несколько раз, применяя ту же rdtsc, и затем усредняют полученные значения. Заметим также, что в ноутбуках с механизмом подстройки тактовой частоты все это просто может не заработать.

Углубляться в эту тему далее мы не будем, заметим еще только, что на практике Windows-программы общего назначения, которые требуют соблюдения интервалов времени с достаточно высокими точностями, никто и не делает— возросшая мощность микроконтроллеров позволяет подобные критичные функции передать самому устройству, а в компьютер передавать уже результаты расчетов, как, к примеру, и поступают приемники GPS (что при взгляде со стороны, согласитесь, выглядит несколько странновато — процессор в PC заведомо мощнее любого микроконтроллера, а занимается всякой ерундой вроде обработки движений мышн— но тут уж ничего не поделаешь). "Прилично" себя ведет Windows только на временных отрезках порядка 1 сек или несколько менее.

Для того чтобы гарантированно избежать ошибок при обмене, организуют различные проверки и синхронизации. Самой распространенной из проверок на то, что байты не были искажены при передаче по линии, является сравнение контрольных сумм. Контрольная сумма оговоренного количества байтов рассчитывается на отправляющей стороне и передается в конце основной посылки. Чтобы не посылать большое число, в которое может вылиться обычная сумма, ее упаковывают. Самый простой метод формирования более- менее уникальной для данной последовательности контрольной суммы — нахождение для нее так называемого "дополнения до 2" в виде контрольного байта.

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

Контрольный байт рассчитывается следующим образом: обычную сумму всех байтов строки вычитают из любой степени числа 2, большей, чем полученная сумма (но не меньшей 256). Младший байт полученной разности и будет искомым значением контрольного байта. На практике этот алгоритм идентичен следующему: мы изначально приравниваем значение контрольной суммы 0, а потом последовательно вычитаем из нее все входящие в сумму значения байтов по очереди. На самом деле такой метод проверки целостности сообщения не идеален — эта сумма, например, не изменится, если байты просто переставить местами. Однако, когда возможные ошибки имеют вероятностный характер, этого вполне достаточно— и сама вероятность сбоя мала, а вероятность возникновения двойной ошибки в битах именно так, чтобы два байта поменяли свое значение друг на друга, можно не принимать во внимание.

Но искажение при передаче — не столь уж часто встречающаяся вещь. Другой момент при приеме в реальном времени гораздо важнее— программа может быть запущена тогда, когда передающий прибор уже включен и поток данных вовсю поступает в компьютер. Вероятность попасть при этом в середину кадра ни в коем случае нельзя сбрасывать со счетов. (Заставлять при этом пользователя соблюдать строго определенный порядок включения — самое крайнее любительство, какое только можно придумать.) Конечно, можно заставить приемную программу проверять паузу между посылками, оговорив, что она не меньше, например, чем 4 мс. и не больше 8 мс— мне приходилось выполнять подобную задачу, и более неудобного способа синхронизации потока придумать трудно. Поэтому всегда, когда посылка состоит из двух и более связанных между собой байтов, их следует сопровождат ь так называемой "сигнатурой" — неким сочетанием байтов с определенным значением. Обычно достаточно, чтобы первые два байта имели всегда определенную величину— даже если такое же сочетание может встретиться внутри посылки, вероятность попасть именно на него в момент включения тем меньше, чем больше временной промежуток между кадрами. Но для большей надежности можно, например, дублировать принятые в виде сигнатуры байты внутри потока дважды — такое сочетание в начале посылки исключено. А как узнать конец посылки? Самое удобное — также заканчивать ее определенной сигнатурой (если, разумеется, длина посылки не строго оговорена). Все перечисленные способы вместе (включая подсчет контрольной суммы) и использует, например, упомянутый протокол Garmin.

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

По теме:

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