Архивы: по дате | по разделам | по авторам

И снова о проблеме Delay

Архив
автор : Юрий Ревич   27.10.1998

В "Компьютерре" #36 (264) И. Книжный и А. Пятошин весьма доступно рассказали о причинах возникновения проблемы Delay - когда многие программы, написанные на Паскале и годами безупречно работавшие на 286/386/486-х машинах, на современной технике либо отказывают вовсе, либо работают с такой скоростью, что не уследишь (попробуйте запустить на Pentium, скажем, популярные лет пять назад Columns). Здесь пойдет речь о другом - как избежать этой проблемы во вновь создаваемых программах? То есть как сформировать задержку таким образом, чтобы ее величина не зависела бы от частоты и типа процессора? Сначала поговорим о DOS-программах.

Я насчитал четыре способа, которые различаются степенью простоты реализации, точностью получаемых задержек (кстати, способ коррекции кода, предлагаемый в упомянутой выше публикации, обеспечивает лишь работоспособность, но никак не точность величины задержки) и, что иногда немаловажно, величиной минимальной задержки:

  1. Использование программируемого системного таймера 8253 (8254).
  2. Написание собственной процедуры Delay с учетом ошибок ее реализации в Турбо-Паскале.
  3. Использование системных часов (прерывание 1Сh).
  4. Использование КМОП-часов реального времени (RTC).

Использование таймера 8253 - довольно громоздкое решение, которое может повлечь проблемы (см., например, статью А. Бондарева в "Компьютерре" #252-253), однако позволяющее получать достаточно точные задержки практически любой продолжительности, вплоть до микросекунд. Интересующихся отсылаю к популярной в свое время книге Р. Джордейна (Справочник программиста персональных компьютеров типа IBM PC, XT и AT. Пер. с англ. - М.: Финансы и статистика, 1992).

Второй способ заключается в следующем: вы организуете пустой цикл, как это делалось у разработчиков Паскаля, но не обрываете процедуру при переполнении регистра, а считаете число переполнений. При этом не обязательно ограничиваться временем в один таймерный тик (продолжительность которого составляет 54,925 " 55 мс; соответственно, при делении на 55 вы получите число пустых циклов на 1 мс - это и есть разрешающая способность процедуры), а чтобы повысить точность для малых времен задержек, лучше считать время, которое пошло на заранее заданное число переполнений счетчика. Отсчитывать время удобно просто вызовом стандартной процедуры GetTime до и после выполнения цикла. Сам цикл может выглядеть, например, так:

asm
@ma: mov cx,0FFFFh
@mb: loop @mb
dec nnn
cmp nnn,0
jne @ma {если nnn не равно нулю, то по новой}
end;

Здесь nnn- заданное число переполнений. После этого вы преобразуете в число время "до" и время "после" с учетом сотых долей секунды, вычитаете их друг из друга (не забудьте учесть переход через значения минут и, возможно, часов) и, как и в Паскале, делите общее число циклов на время. Разрешающая способность получается равной 10 мкс. Порочность этого метода в том, что, во-первых, он не позволяет получить одинаково удобную процедуру для всех компьютеров (по моим данным, пустой цикл на машине с процессором AMD 386/40 выполняется примерно в пятьдесят раз медленнее, чем на AMD К6/166), соответственно подбор величины nnn превращается в весьма трудную задачу; во-вторых, из-за этого не получается необходимая (и хоть сколько-нибудь наперед известная) точность величины задержки.

Третий, довольно простой и, так сказать, штатный способ - использование прерывания 1Сh, которое вызывается с каждым таймерным тиком, то есть 18,206 раза в секунду (55 мс = 1/18,2 - здесь у Игоря Книжного и Александра Пятошина ошибка), и изначально ничего не делает: в соответствующем месте памяти стоит единственная инструкция IRET. Для организации отсчета необходимо перехватить вектор этого прерывания и записать туда, что нам надо:

{$F+} {инициализация дальних вызовов процедур, на всякий случай}
procedure timer; interrupt;
begin
inc (tall) {tall - глобальная переменная типа, скажем, Word}
end;
...
{основная программа}
getintvec($1C,intv); {intv - переменная типа pointer, сохраняет старый вектор}
setintvec($1C,@timer);

Организовать собственно задержку можно, например, так:

tall:=0;
while tall<18 do begin end;

Величина задержки составит приблизительно одну секунду. (Внутри процедуры while можно организовать какое-нибудь действие: например, вывести счетчик или бегущую строку.) В конце программы необходимо вернуть вектор прерывания на место:

setintvec($1C,intv);

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

Четвертый способ - использование RTC - довольно громоздкий, но зато позволяет получить точно известные времена задержек, соответствующие ряду значений частот, которые получаются от деления входной частоты КМОП-часов (32768 Гц) на степени двойки от 0 до 15 (минимальная частота - 2 Гц, соответственно максимальная задержка - 0,5 с; но можно и комбинировать). Делается это следующим образом: в RTC имеется байтовый регистр $0А, четыре первых бита которого (0-3) устанавливают коэффициент деления входной частоты для некоего счетчика. Например, установив эти биты в 0110b (значение по умолчанию), получим на выходе счетчика импульсы каждую 1/1024 с. Внимание! Остальные биты этого регистра трогать нельзя, иначе RTC будут считать нечто несусветное! Выход счетчика, в свою очередь, вызывает аппаратное прерывание RTC (IRQ 8), которое DOS трансформирует в программное прерывание номер $70. Предварительно необходимо разрешить это аппаратное прерывание в регистре $0В (бит 6) микросхемы RTC. Для доступа к регистрам RTC нужно записать номер нужного байта в порт $70 (совпадение номера порта с номером прерывания DOS случайное), затем прочитать или записать нужный байт в порту $71.

Пример доступа к регистру задания коэффициента деления (в примере изменяется коэффициент деления с шести на пятнадцать, что дает частоту прерываний ровно 2 Гц) и разрешения прерывания:

asm
mov al,0Ah {непосредственно число в порт нельзя посылать, только через al}
out 70h,al
in al,71h
mov bl,al {al нам понадобится}
or bl, 00001111b {старшие биты не трогаем, в младшие - все единицы, то есть число 15}
mov al,0Ah
out 70h,al {вообще-то, можно по второму разу подряд номер регистра не задавать, но на всякий случай}
mov al,bl
out 71h,al
{теперь разрешаем прерывания по этому счетчику - бит 6 регистра В установить в 1, все аналогично}
mov al,0Bh
out 70h,al
in al,71h
mov bl,al
or bl, 01000000b
mov al,0Bh
out 70h,al
mov al,bl
out 71h,al
end;

Теперь нужно перехватить вектор прерывания 70h, а дальше все аналогично способу 3. В конце программы надо восстановить вектор и, на всякий случай, отменить разрешение прерываний и восстановить коэффициент деления, для этого нужно проделать все то же самое, но (в данном случае) применить команду AND вместо OR: "and bl, 10111111b" и "and bl, 11110110b" соответственно. Маленькое замечание: как показал эксперимент, старые компьютеры на больших частотах (на 386/40 примерно выше 2048 Гц) сбиваются, не успевая выполнять прерывание. Однако на современных машинах этого происходить не должно.

Два слова про Delphi и Windows. В Object Pascal по понятным причинам вообще нет процедуры Delay. Поэтому, если вдруг задержка потребуется, ее нужно организовать. Остановимся кратко на двух способах.

Первый аналогичен способу 3 для DOS, только используется не прерывание, а штатный компонент Delphi под названием Timer. С ним связано единственное событие (OnTimer), которое происходит с частотой, задаваемой свойством Timer.Interval (один интервал равен тем самым 55 мс). Сначала надо объявить глобальную переменную (пусть это будет tall типа integer), установить Interval в 1 и написать следующую обработку события:

procedure TForm1.Timer1Timer(Sender: TObject);
begin
inc(tall)
end;

Затем можно ввести задержку, как мы это делали ранее:

tall:=0;
Application.ProcessMessages;
while tall<91 do begin
Application.ProcessMessages end;
{будет задержка в 5 секунд}

Без Application.ProcessMessages внутри цикла, который позволяет программе прочесть очередное значение tall, ваше приложение просто зациклится.

Второй способ для Windows аналогичен способу 2 для DOS, то есть сводится к написанию некоторой процедуры, выполняющей пустой цикл. Здесь это выглядит не столь некрасиво, как в DOS, так как встроенный ассемблер Delphi поддерживает расширенные 32-разрядные регистры (EAX, ECX) и это снимает проблему переполнения (0FFFFFFFh пустых циклов на Pentium 200 MMX выполняется примерно за секунду, что приемлемо). Я не буду подробно расписывать процедуру, укажу только, что для измерения времени удобно использовать тип TDateTime, в котором дробная часть хранит значения времени:

tt:=frac(dt); {здесь tt - имеет тип Extended, dt - тип TDateTime}

Нужно считать время "до" и "после" цикла (в формате TDateTime), вычесть, а затем перевести время в число указанным способом. Если теперь умножить число tt на 105, то получим значение времени в секундах (с огромной точностью, которая ни в коей мере не отражает истинное положение дел, так как обновление показаний системных часов происходит каждые 55 мс).

© ООО "Компьютерра-Онлайн", 1997-2022
При цитировании и использовании любых материалов ссылка на "Компьютерру" обязательна.