Sources
Delphi Russian Knowledge Base
DRKB - это самая большая и удобная в использовании база знаний по Дельфи в рунете, составленная Виталием Невзоровым

Как реализовать сверхточный таймер?

01.01.2007

Windows is not a real time operating system so it is not really able to reliably achieve high accuracy timing without using a device driver. The best I have been able to get is a few nanoseconds using QueryPerformanceCounter. This is the procedure I use:

var
 
WaitCal: Int64;
 
procedure
Wait(ns: Integer);
var
 
Counter, Freq, WaitUntil: Int64;
begin
 
if QueryPerformanceCounter(Counter) then
 
begin
   
QueryPerformanceFrequency(Freq);
   
WaitUntil := Counter + WaitCal + (ns * (Freq div 1000000));
   
while Counter < WaitUntil do
     
QueryPerformanceCounter(Counter);
 
end
 
else
   
Sleep(ns div 1000);
end;

To get improved accuracy do this a little while before using Wait()

var
 
Start, Finish: Int64;
 
Application.ProcessMessages;
Sleep(10);
QueryPerformanceCounter(Start);
Wait(0);
QueryPerformanceCounter(Finish);
WaitCal := Start - Finish;

A trick I have found to increase the reliability of this on my computer is to call Wait like this:

Application.ProcessMessages;
Sleep(0);
DoSomething;
Wait(10);
DoSomethingElse;

Взято из https://www.lmc-mediaagentur.de/dpool


Unit Counter;           (* Written by Jin *)
{$O-,F-,S-,N-,R-,Q-}
Interface
 
Type
   tTimerValue
= record
     
Micro: Word;      { Счётчик 8253/8254 }
     
Counter: Longint  { Счётчик BIOS }
   
End;
 
Const
   
MicroFreq = 1193181 { $1234DD };    { Частота обновления счётчика Micro (1/сек) }
   
CounterFreq = MicroFreq / 65536;    { Частота обновления счётчика Counter (1/сек) }
   
MicroInterval = 1 / MicroFreq;      { Интервал обновления счётчика Micro (сек) }
   
CounterInterval = 1 / CounterFreq;  { Интервал обновления счётчика Counter (сек) }
 
Var
   
BIOSCounter: Longint absolute $0040:$006C;
{ Системный счётчик (обновляется CounterFreq раз/сек, }
{ то есть каждые CounterInterval секунд)              }
 
Procedure InitTimer;
{ Инициализировать таймер (перевести в нужный режим работы).       }
{ Эту  процедуру необходимо выполнять перед использованием функций }
{ и  процедур  для получения значения таймера (или счётчика), если }
{ Вы  в своей программе изменили режим работы таймера. В противном }
{ случае эта процедура Вам не понадобится, так как она выполняется }
{ в секции инициализации модуля (сразу после запуска программы) !  }
Procedure GetTimerValue(var Timer: tTimerValue);
{ Записать значение таймера в переменную Timer }
Function GetTimerSec: Real;
{ Получить значение таймера в секундах точностью до 1 мкс) }
Function GetTimerMillisec: Longint;
{ Получить значение таймера в миллисекундах }
 
Procedure GetTimerDifference(var Older, Newer, Result: tTimerValue);
{ Записать разницу значений Newer и Older в переменную Result }
Function GetTimerDifSec(var Older, Newer: tTimerValue): Real;
{ Получить разницу значений Newer и Older в секундах }
Function GetTimerDifMillisec(var Older, Newer: tTimerValue): Longint;
{ Получить разницу значений Newer и Older в миллисекундах }
 
Function ConvTimer2Sec(var Timer: tTimerValue): Real;
{ Получить количество секунд по значению переменной Timer }
Function ConvTimer2Millisec(var Timer: tTimerValue): Longint;
{ Получить количество миллисекунд по значению переменной Timer }
Procedure ConvSec2Timer(Sec: Real; var Timer: tTimerValue);
{ Преобразовать значение секунд Sec типа Real в тип tTimerValue }
Procedure ConvMillisec2Timer(Millisec: Longint; var Timer: tTimerValue);
{ Преобразовать значение миллисекунд Millisec типа Longint в тип tTimerValue }
 
Procedure ResetCounter;
{ Сбросить  счётчик (то есть принять текущее значение таймера за ноль для }
{ процедуры GetCounterValue и функции GetCounterSec)                      }
Procedure GetCounterValue(var Timer: tTimerValue);
{ Записать значение счётчика в переменную Timer }
Function GetCounterSec: Real;
{ Получить значение секунд счётчика }
Function GetCounterMillisec: Longint;
{ Получить значение миллисекунд счётчика }
 
Procedure Delay(MS: Word);
{ Задержка MS миллисекунд (1 сек = 1000 мс) }
Procedure DelaySec(Sec: Real);
{ Задержка Sec секунд }
Procedure MDelay(N: Longint);
{ Задержка N * MicroInterval секунд (приближённо N * 0.838095813 мкс). }
{ Если Вам нужны наиболее точные короткие задержки, лучше использовать }
{ эту  процедуру, так как она даёт наименьшую погрешность по сравнению }
{ с двумя предыдущими процедурами.                                     }
 
Implementation
Var Now: tTimerValue;
Var Zero: tTimerValue;
 
Procedure InitTimer; assembler;
Asm
   mov al
,34h      { Режим 2 таймера 0 }
   
out 43h,al
   xor al
,al       { 65536 циклов до IRQ }
   
out 40h,al
   
out 40h,al
End
 
Procedure GetTimerValue; assembler;
Asm
   cld
   xor ax
,ax
   mov es
,ax
   mov bx
,46Ch     { DS:BX = 0000h:046Ch = Таймер BIOS }
   cli
   mov dx
,es:[bx]
   mov cx
,es:[bx+2]{ CX:DX = Первое значение таймера BIOS }
   sti
   
out 43h,al      { Замораживаем таймер 8253/8254 }
   cli
   mov si
,es:[bx]
   mov di
,es:[bx+2]{ DI:SI = Второе значение таймера BIOS }
   
in al,40h
   mov ah
,al
   
in al,40h
   sti
   xchg ah
,al      { AX = Таймер 8253/8254 }
   
not ax          { Обратный отсчёт -> Прямой отсчёт }
   cmp dx
,si       { Первое значение таймера BIOS равно второму значению ? }
   je
@Ok          { Да! Оставляем как есть (CX:DX), иначе... }
   
or ax,ax        { Таймер BIOS изменился после заморозки таймера 8253/8254 (между OUT и CLI) ? }
   js
@Ok          { Да! Оставляем как есть (CX:DX), иначе... }
   mov dx
,si
   mov cx
,di       { CX:DX = DI:SI, если таймер BIOS изменился между STI и OUT }
@Ok:
   les di
,Timer
   stosw          
{ Low Word }
   xchg ax
,dx
   stosw          
{ Middle Word }
   xchg ax
,cx
   stosw          
{ High Word - Записаны из CX:DX:AX }
End
 
Function GetTimerSec;
Begin
   
GetTimerValue(Now);
   
GetTimerSec := ConvTimer2Sec(Now)
End;
 
Function GetTimerMillisec;
Begin
   
GetTimerMillisec := Trunc(GetTimerSec*1000)
End;
 
Procedure GetTimerDifference; assembler;
Asm
   cld
   push ds
   lds si
,Newer
   lodsw          
{ Low Word }
   xchg cx
,ax
   lodsw          
{ Middle Word }
   xchg dx
,ax
   lodsw          
{ High Word }
   xchg cx
,ax      { Прочитаны в CX:DX:AX }
   lds si
,Older
   
sub ax,[si]
   sbb dx
,[si+2]
   sbb cx
,[si+4]   { Вычитаем Older из Newer }
   les di
,Result
   stosw          
{ Low Word }
   xchg ax
,dx
   stosw          
{ Middle Word }
   xchg ax
,cx
   stosw          
{ High Word - Записано из CX:DX:AX }
   pop ds
End
 
Function GetTimerDifSec;
Begin
   
GetTimerDifference(Older, Newer, Now);
   
GetTimerDifSec := ConvTimer2Sec(Now)
End;
 
Function GetTimerDifMillisec;
Begin
   
GetTimerDifMillisec := Trunc(GetTimerDifSec(Older, Newer)*1000)
End;
 
Function ConvTimer2Sec;
Begin
   ConvTimer2Sec
:= (Timer.Counter*65536 + Timer.Micro) / MicroFreq
End;
 
Function ConvTimer2Millisec;
Begin
   ConvTimer2Millisec
:= Trunc(ConvTimer2Sec(Timer)*1000)
End;
 
Procedure ConvSec2Timer;
Begin
   
Timer.Counter := Trunc(Sec * CounterFreq);
   
Timer.Micro := Trunc(Sec * MicroFreq) mod 65536
End;
 
Procedure ConvMillisec2Timer;
Begin
   
Timer.Counter := Trunc(Millisec/1000 * CounterFreq);
   
Timer.Micro := Trunc(Millisec/1000 * MicroFreq) mod 65536
End;
 
Procedure ResetCounter;
Begin
   
GetTimerValue(Zero)
End;
 
Procedure GetCounterValue;
Begin
   
GetTimerValue(Timer);
   
GetTimerDifference(Zero, Timer, Timer)
End;
 
Function GetCounterSec;
Begin
   
GetTimerValue(Now);
   
GetTimerDifference(Zero, Now, Now);
   
GetCounterSec := ConvTimer2Sec(Now)
End;
 
Function GetCounterMillisec;
Begin
   
GetCounterMillisec := Trunc(GetCounterSec*1000)
End;
 
Procedure Delay;
Var Zero: Longint;
Begin
   
If MS <= 0 then Exit;
   
Zero := GetTimerMillisec;
   
Repeat
   
Until GetTimerMillisec-Zero >= MS
End;
 
Procedure DelaySec;
Var Zero: Real;
Begin
   
If Sec <= 0 then Exit;
   
Zero := GetTimerSec;
   
Repeat
   
Until GetTimerSec-Zero >= Sec
End;
 
Procedure MDelay;
Label Check;
Var Zero: tTimerValue;
Begin
   
If N <= 0 then Exit;
   
GetTimerValue(Zero);
 
Check:
   
GetTimerValue(Now);
   
GetTimerDifference(Zero, Now, Now);
   
Asm
      mov ax
,word ptr Now
      mov dx
,word ptr Now+2  { DX:AX - Прошедшее время }
{      mov cx,word ptr Now+4
     
or cx,cx
      jnz
@Exit}
      cmp dx
,word ptr N+2    { Проверяем старшие слова }
      jb
Check
      cmp ax
,word ptr N      { Проверяем младшие слова }
      jb
Check
   
@Exit:
   
EndEnd;
 
Begin
   
InitTimer
End.

 

И вот ещё программа-тестер:

Uses Counter;
Var
   
Ans: Char;
   i
: Longint;
   
Sec: Real;
 
Begin
   
Asm
      mov ah
,0Dh
     
int 21h      { Сбрасываем кэш }
      mov ax
,1681h
     
int 2Fh      { Запрещаем Windows Task Switch }
   
End
 
   
Write('Без задержки...');
   
ResetCounter;
   
Sec := GetCounterSec;
   
WriteLn(#8#8#8': прошло ', Sec: 0: 6, ' сек');
 
   
Write('1000 раз холостой цикл...');
   
ResetCounter;
   
For i := 1 to 1000 do ;
   
Sec := GetCounterSec;
   
WriteLn(#8#8#8': прошло ', Sec: 0: 6, ' сек');
 
   
Write('1000 раз по 0 сек...');
   
ResetCounter;
   
For i := 1 to 1000 do
     
DelaySec(0);
   
Sec := GetCounterSec;
   
WriteLn(#8#8#8': прошло ', Sec: 0: 6, ' сек');
 
   
WriteLn('-------------------------------------------------');
 
   
Write('1 раз 1 сек...');
   
ResetCounter;
   
DelaySec(1);
   
Sec := GetCounterSec;
   
WriteLn(#8#8#8': прошло ', Sec: 0: 6, ' сек');
 
   
Write('1000 раз по 0.001 сек...');
   
ResetCounter;
   
For i := 1 to 1000 do
     
DelaySec(0.001);
   
Sec := GetCounterSec;
   
WriteLn(#8#8#8': прошло ', Sec: 0: 6, ' сек');
 
   
Write('10000 раз по 0.0001 сек...');
   
ResetCounter;
   
For i := 1 to 10000 do
     
DelaySec(0.0001);
   
Sec := GetCounterSec;
   
WriteLn(#8#8#8': прошло ', Sec: 0: 6, ' сек');
 
   
Write('100000 раз по 0.00001 сек...');
   
ResetCounter;
   
For i := 1 to 100000 do
     
DelaySec(0.00001);
   
Sec := GetCounterSec;
   
WriteLn(#8#8#8': прошло ', Sec: 0: 6, ' сек');
 
   
Write('119318 раз по 1/119318.1 сек...');
   
ResetCounter;
   
For i := 1 to 119318 do
     
MDelay(10);
   
Sec := GetCounterSec;
   
WriteLn(#8#8#8': прошло ', Sec: 0: 6, ' сек');
 
   
WriteLn('-------------------------------------------------');
 
   
Write('Запускать тесты по микросекундам (м.б. очень долгими) [Y/N] ? : ');
   
Asm
   
@Repeat:
      xor ah
,ah
     
int 16h
     
or al,20h
      cmp al
,'y'
      je
@Ok
      cmp al
,'n'
      jne
@Repeat
   
@Ok:
      mov
Ans,al
   
End
   
WriteLn(Ans);
 
   
If Ans = 'y' then
   
Begin
     
Write('1000000 раз по 0.000001 сек...');
     
ResetCounter;
     
For i := 1 to 1000000 do
         
DelaySec(0.000001);
     
Sec := GetCounterSec;
     
WriteLn(#8#8#8': прошло ', Sec: 0: 6, ' сек');
 
     
Write('1193181 раз по 1/1193181 сек...');
     
ResetCounter;
     
For i := 1 to 1193181 do
         
MDelay(1);
     
Sec := GetCounterSec;
     
WriteLn(#8#8#8': прошло ', Sec: 0: 6, ' сек')
   
End;
 
   
Asm
      mov ax
,1682h
     
int 2Fh      { Разрешаем Windows Task Switch }
   
EndEnd.

 
Не забывайте, что погрешности, которые будет выдавать программа-тестер будут из-за того, что какое-то время тратиться на вызов процедуры, циклы и т.д. (т.к. там используются процедуры DelaySec, MDelay).... Но если вызвать ResetCounter, а через некоторое время GetCounterSec, то результат будет точным (собственно, именно так здесь и измеряются погрешности)! И можно вызывать его (GetCounterSec) ещё хоть 10000 раз! ;D

Кстати, запускайте тестер только в полноэкранном режиме, т.к. программа отключает многозадачность Windows, и на экране вы ничего не увидите (будет впечатление, что прога повисла).

Автор: 7jin

Взято из https://forum.sources.ru


А вот ещё один способ (работает только на Pentium или выше)....

Unit TSCDelay;          (* Работает только на Pentium  то не всегда ;) *)
{$O-,F-,G+,S-,R-}
Interface
 
Var
   
CPUClock: Longint;   { Тактовая частота процессора (гц) }
 
Procedure CalcCPUClock;
{ Вычислить тактовую частоту процессора и записать в переменную CPUClock. }
Procedure MDelay(N: Longint);
{ Производит задержку в N микросекунд. Задержки более 4294967296/CPUClock }
{ (на 300 ~ 14) секунд будут работать неправильно из-за переполнения!!! }
{ Перед  использованием  это  процедуры  необходимо установить правильное }
{ значение  переменной  CPUClock.  Это  можно  сделать либо вручную, либо }
{ выполнив процедуру CalcCPUClock.                                        }
Procedure TDelay(N: Longint);
{ Производит задержку в N тактов процессора }
 
Implementation
Uses Dos;
Var
   SaveInt08
: Pointer;
   
Stage: Byte;
 
Procedure SpeedCounter; far; assembler;  { Наш IRQ 0 }
Asm
   push ax
   push ds
   mov ax
,seg @Data
   mov ds
,ax
   inc
Stage            { Прибавляем к Stage единицу }
   mov al
,20h
   
out 20h,al           { Посылаем сигнал "конец IRQ" }
   pop ds
   pop ax
   iret                
{ Выходим }
End
 
Procedure CalcCPUClock;
Begin
   
Asm
      mov ah
,0Dh
     
int 21h                     { Сбрасываем кэш }
      mov ax
,1681h
     
int 2Fh                     { Отключаем Windows Task Switch }
     
in al,0A1h                  { Маски IRQ 8-15 }
      mov ah
,al
     
in al,21h                   { Маски IRQ 0-7 }
      push ax                    
{ Сохраняем маски }
      mov al
,0FEh
     
out 21h,al                  { Запрещаем IRQ 1-7 (нулевой нам нужен) }
      inc ax
     
out 0A1h,al                 { Запрещаем IRQ 8-15 }
      mov al
,36h
     
out 43h,al                  { Устанавливаем нормальный режим работы таймера }
      xor al
,al
     
out 40h,al
     
out 40h,al                  { 65536 циклов до IRQ 0 }
      mov
Stage,0                 { Готовимся к началу отсчёта }
   
End
   
GetIntVec(8, SaveInt08);       { Сохраняем старый IRQ 0 }
   
SetIntVec(8, @SpeedCounter);   { Устанавливаем свой IRQ 0 }
   
Asm
   @1
:cmp Stage,1
      jne @1                      
{ Цикл до первого IRQ 0 }
      db
0Fh,31h  { RDTSC }
      db
66h; xchg cx,ax          { Запоминаем значение счётчика }
   @2
:cmp Stage,2
      jne @2                      
{ Цикл до второго IRQ 0 }
      db
0Fh,31h  { RDTSC }
      db
66h; sub ax,cx           { Вычитаем из текущего значение счётчика запомненное }
      db
66h,0B9h; dd 1234DDh     { mov ecx,1234DDh }
      db
66h; mul cx              { Умножаем значение на 1193181 }
      db
66h,0Fh,0ACh,0D0h,10h    { shrd eax,edx,16 - делим на 65536 }
      db
66h; mov word ptr CPUClock,ax { Записываем результат в CPUClock }
      pop ax
     
out 21h,al                  { Восстанавливаем маску IRQ 0-7 }
      mov al
,ah
     
out 0A1h,al                 { Восстанавливаем маску IRQ 8-15 }
   
End
   
SetIntVec(8, SaveInt08);       { Восстанавливаем старый IRQ 0 }
   
Asm
      mov ax
,1682h
     
int 2Fh                     { Включаем Windows Task Switch }
   
EndEnd;
 
Procedure MDelay; assembler;
Asm
   db
0Fh,31h  { RDTSC }
   db
66h; push ax
   db
66h; push dx           { Сохраняем счётчик в стеке }
   db
66h; mov ax,word ptr N
   db
66h; mov cx,word ptr CPUClock
   db
66h; mul cx            { Умножаем N на CPUClock }
   db
66h,0B9h; dd 1000000   { mov ecx,1000000 }
   db
66h; div cx            { Затем делим на 1000000 }
   db
66h; xchg si,ax        { Сохраняем значение в ESI }
   db
66h; pop cx
   db
66h; pop bx            { Восстанавливаем значение счётчика в ECX:EBX }
 @
:db 0Fh,31h  { RDTSC }
   db
66h; sub ax,bx
   db
66h; sbb dx,cx         { Вычитаем из текущего счётчика ECX:EBX }
   db
66h; or dx,dx          { Старшая часть разницы д.б. всегда 0, проверяем это }
   jnz
@Exit                 { Нет - выходим! }
   db
66h; cmp ax,si         { Проверяем - прошло ли столько, сколько нам надо }
   jb @                      
{ Нет - ждём ещё }
 
@Exit:
End
 
Procedure TDelay; assembler;
Asm
   db
0Fh,31h  { RDTSC }
   db
66h; mov bx,ax
   db
66h; mov cx,dx         { Сохраняем счётчик в ECX:EBX }
 @
:db 0Fh,31h  { RDTSC }
   db
66h; sub ax,bx
   db
66h; sbb dx,cx         { Вычитаем из текущего счётчика ECX:EBX }
   db
66h; or dx,dx          { Старшая часть разницы д.б. всегда 0, проверяем это }
   jnz
@Exit                 { Нет - выходим! }
   db
66h; cmp ax,word ptr N { Проверяем - прошло ли столько, сколько нам надо }
   jb @                      
{ Нет - ждём ещё }
 
@Exit:
End
 
End.
nbsp;

И программа-тестер:

Uses TSCDelay;
Var N: Longint;
Begin
   
CalcCPUClock;
   
WriteLn('Тактовая частота процессора: ', CPUClock/1000000: 0: 3,' МГц');
   
Write('Введите количество микросекунд (не более ', 4294967296.0/CPUClock: 0: 3, ' млн): ');
   
ReadLn(N);
   
Write('Задержка...');
   
MDelay(N);
   
WriteLn(' всё!')
End.

Автор: 7jin

Взято из https://forum.sources.ru