Как сделать задержку выполнения программы?

Писал 11 лет назад статейку на тему задержек (на другом форуме)...

Как произвести задержку на несколько миллисекунд?

Существует много способов осуществить задержку, рассмотрим некоторые из них.

1. Первый способ (PTDelay в исходнике ниже) основан на том факте, что в системе существует два таймера: программируемый таймер i8253/i8254, частота которого 1193181 Гц (~ 1.19 МГц, подробнее о нём см. в теме Воспроизведение звука из PC Speaker'а) и счётчик BIOS, расположенный в памяти по адресу 0040:046C и занимающий 4 байта. Читая их значения можно измерять время с точностью более микросекунды (см. процедуру GetTimerValue, более расширенный вариант для языка Turbo/Borland Pascal см. в аттаче)! Процедура PTDelay читает первые значения таймеров и добавляет к ним мсек*1193181/1000 (т.к. отсчёт ведётся по 1/1000 секунды), где мсек - необходимая задержка в миллисекундах. После этого процедура в цикле читает новые значения таймеров и вычитает из них полученное ранее значение. Если результат получился отрицательным, значит время ещё не вышло, и цикл повторяется, если положительным, то осуществляется выход из процедуры.

P.S. В данной программе я использовал математический сопроцессор, т.к. в противном случае пришлось бы либо использовать регистры процессора 386+ (а здесь я их нигде не использовал), либо писать процедуру деления больших чисел, которая сама по себе довольно громоздкая.

P.P.S. Я использую режим таймера 2, а не 3 (как инициализирует BIOS) потому, что в этом случае повышается точность таймера, т.к. в режиме 3 таймер возвращает только нечётные значения (т.е. фактически точность меньше в 2 раза). При использовании режима 2 никаких нарушений в работе системы не происходит!

2. Второй способ (PasDelay в исходнике ниже) используется в языке программирования Turbo/Borland Pascal. Т.к. стандартная процедура работает некорректно на современных (быстрых) процессорах, мне пришлось немного её подправить (исправленные turbo.tpl и tpp.tpl вместе с исходниками(!) можно скачать здесь, а подробности прочесть здесь). Этот метод использует только счётчик BIOS, меняющийся ~ 18.2 раза в секунду (т.е. каждые ~ 55 миллисекунд), но это совершенно не означает, что этот метод настолько неточен. В процедуре инициализации вычисляется количество "холостых" циклов (PasDelayLoop), которые происходят за 1 тик (изменение) счётчика, и зависит это число от производительности процессора. После этого полученное число делится на 55 (причём с правильным округлением, обратите внимание на инструкцию mov ax,-28). Т.о, получается количество циклов за 1 миллисекунду, которое записывается в переменную DelayCnt. При вызове процедуры PasDelay тот же холостой цикл прогоняется DelayCnt*мсек раз, где мсек - необходимая задержка в миллисекундах.

3. Третий способ (TickDelay в исходнике ниже) один из самых простых и неточных. В нём используется только значение счётчика BIOS. Количество миллисекунд делится на 55 (вернее, умножается на 1193 и берётся старшее слово результата - так будет немного точнее) и к результату прибавляется единица (т.к. во-первых, частное округляется в меньшую сторону, а во-вторых, при входе в процедуру тик уже может заканчиваться, а он будет считаться целым). Получается количество тиков, которые нужно выждать, что затем и производится.

4. Ну и, наконец, четвёртый способ (Int15Delay в исходнике ниже) - самый простой, но и самый ненадёжный. Здесь используется прерывание 15h:

Задержка с помощью прерывания:

Вход:  AH = 86h
       CX:DX = количество миллисекунд (CX - старшее слово, DX - младшее)
Выход: CF=NC=0 - задержка завершена
       CF=CY=1 - функция занята

В чём же её ненадёжность? Во-первых, кроме функции ah=86h существует функция ax=8300h, которая устанавливает флаг

.

• Можно придумать ещё кучу способов, например, измерить частоту процессора и ожидать необходимое число тактов процессора. Но реализовывать такой способ на чистом ассемблере я не буду, т.к. он работает только как минимум на Pentium и ничем не лучше первого способа. Если Вам всё же интересно, можете скачать вариант на Turbo/Borland Pascal'е (см. аттач, tscdelay.pas)...

Теперь несколько слов о том, какой способ лучше всего использовать. Первый способ самый точный и надёжный, но и самый громоздкий. Хотя... не знаю почему, но под Windows счётчик BIOS изменяется не сразу, а только через несколько тиков системного программируемого таймера. Поэтому примерно один из 1300 (на моём компьютере) вызов GetTimerValue даёт сбой на 55 миллисекунд. В чистом DOS'е всё работает чётко! Второй способ достаточно точный на коротких задержках (в несколько десятков или сотен миллисекунд), но для больших задержек вместо него лучше использовать третий способ. Про четвёртый уже сказано достаточно.

А вот, собственно, и долгожданный исходник:

AssemblerВыделить код

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222

; tasm /m delay.asm
; tlink /t /x delay.obj
 
.MODEL Tiny
.286
.CODE
ORG     100h
 
LOCALS
 
Start:
 
                call    InitTimer      ; Выполняем один раз! (для PTDelay)
                call    InitPasDelay   ; Выполняем один раз! (для PasDelay)
                mov     al,'.'
                int     29h            ; Выводим точку
 
                mov     ax,1000
                call    PTDelay        ; Ожидаем 1 секунду
                mov     al,'1'
                int     29h            ; Выводим '1'
 
                mov     ax,1000
                call    PasDelay       ; Ожидаем 1 секунду
                mov     al,'2'
                int     29h            ; Выводим '2'
 
                mov     ax,1000
                call    TickDelay      ; Ожидаем ~ 1 секунду
                mov     al,'3'
                int     29h            ; Выводим '3'
 
                mov     ax,1000
                call    Int15Delay     ; Ожидаем 1 секунду
                mov     al,'4'
                int     29h            ; Выводим '4'
 
                mov     al,'.'
                int     29h            ; Выводим точку
                xor     ah,ah
                int     16h            ; Ждём нажатия клавиши
                int     20h            ; Выходим из программы
 
; -- Процедуры --------------------------------------------------------------;
 
; Инициализировать таймер (установить режим и коэффициент пересчёта)
InitTimer       proc
                cli                    ; На всякий случай запретим прерывания
                mov     al,34h         ; Режим 2 канала 0
                out     43h,al
                xor     al,al          ; 65536 циклов между IRQ
                out     40h,al
                out     40h,al
                sti
                ret
InitTimer       endp
 
; Получить значение таймера в DX:CX:AX (AX младшее слово)
GetTimerValue   proc
                cld
                xor     ax,ax
                mov     es,ax
                mov     si,46Ch        ; ES:SI = 0000h:046Ch = системный счётчик BIOS
                mov     cx,es:[si+0]   ; CX = первое значение счётчика BIOS (младшее слово)
                out     43h,al         ; "Защёлкиваем" канал 0 таймера i8253/i8254
                cli
                mov     bx,es:[si+0]
                mov     dx,es:[si+2]   ; DX:BX = второе значение счётчика BIOS
                in      al,40h
                mov     ah,al
                in      al,40h
                xchg    ah,al          ; AX = значение таймера i8253/i8254
                sti
                cmp     cx,bx          ; Первое значение счётчика BIOS равно второму ?
                je      @@Ok           ; Да! Оставляем как есть (DX:CX), иначе...
                or      ax,ax          ; Счётчик изменился ПОСЛЕ "защёлкивания" (между OUT и CLI) ?
                js      @@Swap         ; Нет! Идём переносить BX в CX, иначе...
                or      bx,bx          ; При изменении таймера изменилось старшее слово ?
                jne     @@NoOverflow   ; Нет!
                dec     dx             ; Да, корректируем старшее слово (DX)
@@NoOverflow:   cmp     ax,?           ; Здесь это аналогично jmp @@Ok (только короче и быстрее)
                ORG $-2
@@Swap:         mov     cx,bx          ; DX:CX = DX:BX, если счётчик изменился между MOV CX и OUT
@@Ok:           not     ax             ; Обратный отсчёт -> Прямой отсчёт
                ret
GetTimerValue   endp
 
; Осуществить задержку AX миллисекунд
; Использует GetTimerValue
PTDelay         proc
                or      ax,ax
                jz      @@Exit         ; Если AX=0, то сразу выходим
 
                mov     bp,sp          ; Сохраняем SP в BP
                push    ax             ; Сохраняем нужное нам число в стеке
                call    GetTimerValue  ; Получаем текущее значение таймера
                xchg    bx,ax
                mov     si,cx
                mov     di,dx          ; Сохраняем его в DI:SI:BX
 
                finit                  ; Инициализируем сопроцессор
                fild    word ptr [bp-2]; Загружаем в стек кол-во миллисекунд
                push    012h
                push    034DDh         ; Заносим в стек число 1193181
                fild    dword ptr [bp-6]; Загружаем его в сопроцессор
                fmul                   ; Перемножаем эти числа
                push    1000
                fild    word ptr [bp-8]
                fdiv                   ; И делим на 1000
                fistp   dword ptr [bp-4]; А затем помещаем обратно в стек (округлив)
                mov     ax,[bp-4]
                mov     dx,[bp-2]      ; Читаем из стека результат в DX:AX
                mov     sp,bp          ; Восстанавливаем SP
                add     bx,ax
                adc     si,dx
                adc     di,0           ; Добавляем DX:AX к DI:SI:BX
@@Repeat:
                push    bx si
                call    GetTimerValue  ; Получаем текущее значение таймера
                pop     si bx
                sub     ax,bx
                sbb     cx,si
                sbb     dx,di          ; Вычитаем DI:SI:BX из DX:CX:AX
                or      dx,dx
                js      @@Repeat       ; Повторяем, если получилось < 0
@@Exit:         ret
PTDelay         endp
 
PasDelayCnt     dw      0              ; Используется в процедуре PasDelay
 
; Инициализировать переменную PasDelayCnt для работы процедуры PasDelay
; Использует PasDelayLoop
InitPasDelay    proc
                xor     ax,ax
                mov     es,ax
                mov     di,46Ch
                mov     bl,es:[di]
@@Wait:         cmp     bl,es:[di]
                je      @@Wait         ; Ждём следующий тик
                mov     bl,es:[di]
                mov     ax,-28
                cwd                    ; DX = 0FFFFh
                call    PasDelayLoop   ; Ждём следующий тик
                not     ax
                not     dx             ; Инвертируем DX:AX
                mov     cx,55
                xchg    bx,ax          ; Сохранить AX в BX
                xchg    ax,dx          ; Записать DX в AX
                cwd                    ; DX = 0
                div     cx             ; Делим DX:AX на CX (55)
                mov     PasDelayCnt[2],ax ; Сохраняем старшее слово
                xchg    ax,bx          ; Восстанавливаем AX из BX
                div     cx             ; Делим DX:AX на CX (55)
                mov     PasDelayCnt[0],ax ; Сохраняем младшее слово
                ret
InitPasDelay    endp
 
; Ожидать либо DX:AX циклов, либо изменения байта по адресу ES:[DI]
; (в зависимости от того, что произойдёт раньше)
PasDelayLoop    proc
@@Repeat:       sub     ax,1
                sbb     dx,0
                jc      @@Exit
                cmp     bl,es:[di]
                je      @@Repeat
@@Exit:         ret
PasDelayLoop    endp
 
; Осуществить задержку AX миллисекунд (этот метод используется в языке Pascal)
; Использует PasDelayLoop
PasDelay        proc
                xchg    cx,ax          ; Заносим AX в CX
                jcxz    @@Exit         ; Если CX=0, то сразу выходим
 
                mov     ax,40h
                mov     es,ax
                xor     di,di
                mov     bl,es:[di]     ; BL = байт по адресу 0040h:0000h (не меняется)
@@Repeat:       mov     ax,PasDelayCnt[0]
                mov     dx,PasDelayCnt[2]
                call    PasDelayLoop   ; Ждём DX:AX циклов
                loop    @@Repeat       ; CX раз
@@Exit:         ret
PasDelay        endp
 
; Осуществить задержку AX миллисекунд (с точностью до 55 миллисекунд)
; Ничего не использует, весьма простой способ
TickDelay       proc
                mov     cx,1193
                mul     cx             ; Делим AX на 55 и прибавляем 1
                inc     dx             ; Получаем в DX кол-во тиков таймера
 
                xor     ax,ax
                mov     es,ax
                mov     si,46Ch
                mov     bx,es:[si+0]
                mov     cx,es:[si+2]   ; Читаем таймер BIOS в CX:BX
                add     bx,dx
                adc     cx,ax          ; Добавляем DX к CX:BX (AX=0)
@@Repeat:
                mov     ax,es:[si+0]
                mov     dx,es:[si+2]   ; Читаем таймер BIOS в DX:AX
                sub     ax,bx
                sbb     dx,cx          ; Вычитаем CX:BX из DX:AX
                js      @@Repeat       ; Повторяем, если получилось <= 0
                ret
TickDelay       endp
 
; Осуществить задержку AX миллисекунд через int 15h
; Ничего не использует, самый простой, но не всегда надёжный способ
; Устанавливает флаг CF=CY=1, если функция занята
Int15Delay      proc
                mov     cx,1000
                mul     cx             ; Умножаем AX на 1000
                mov     cx,dx
                mov     dx,ax          ; Переносим DX:AX в CX:DX
                mov     ah,86h
                int     15h            ; Вызываем int 15h
                ret
Int15Delay      endp
 
END             Start

4