На главную Наши проекты:
Журнал   ·   Discuz!ML   ·   Wiki   ·   DRKB   ·   Помощь проекту
ПРАВИЛА FAQ Помощь Участники Календарь Избранное RSS
msm.ru
! Перед отправкой сообщения внимательно прочтите правила раздела!!!
1. Все статьи должны быть оформлены согласно Правил оформления статей.
2. Любые обсуждения должны происходить в специальной теме, обсуждение в любых других темах раздела запрещены.
3. Запрещается писать статьи о создании и распространении вирусов, троянов и других вредоносных программ!
4. За грамотно написанные и правильно оформленные статьи авторы награждаются DigiMoney.

Дополнительные ссылки:
Желаю творческих успехов! ;)
Модераторы: Jin X
  
    > Задержка на несколько миллисекунд , DOS, Общие вопросы
       
      Как произвести задержку на несколько миллисекунд?

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

      • Первый способ (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 никаких нарушений в работе системы не происходит!

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

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

      • Ну и, наконец, четвёртый способ (Int15Delay в исходнике ниже) - самый простой, но и самый ненадёжный. Здесь используется прерывание 15h:
        ExpandedWrap disabled
          Задержка с помощью прерывания:
          Вход:  AH = 86h
                 CX:DX = количество миллисекунд (CX - старшее слово, DX - младшее)
          Выход: CF=NC=0 - задержка завершена
                 CF=CY=1 - функция занята
        В чём же её ненадёжность? Во-первых, кроме функции ah=86h существует функция ax=8300h, которая устанавливает флаг (бит 7 байта по указанному адресу) по истечении указанного промежутка времени, и во время активности этой функции функция ah=86h не работает. Во-вторых, я слышал, что эта функция вообще не работает на некоторых компьютерах (XT не в счёт). Первая проблема решаема: для этого необходимо в начало процедуры Int15Delay добавить инструкции mov ax,8301h + int 15h (только нужно это сделать так, чтобы не потерять передаваемое функции значение регистра ax), они отменят работу функции ax=8300h. Но в этом случае возможен сбой в какой-то другой программе, а именно в той, которая вызывала эту функцию. Вторую проблему таким образом не решить, хотя можно скомбинировать, например, этот и третий способ, т.е. если данная функция вернёт CF=CY=1, то можно использовать TickDelay... но я бы, честно говоря, так делать не стал (если бы речь шла о серьёзной программе) :) .

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

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

      А вот, собственно, и долгожданный исходник:
      ExpandedWrap disabled
        ; 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

      Прикреплённый файлПрикреплённый файлdelay.zip (11.56 Кбайт, скачиваний: 312)
      0 пользователей читают эту тему (0 гостей и 0 скрытых пользователей)
      0 пользователей:


      Рейтинг@Mail.ru
      [ Script execution time: 0,0292 ]   [ 15 queries used ]   [ Generated: 27.04.24, 18:19 GMT ]