Stoper i czas pod DOS em

thrashkura
Poszukując sposobu oprogramowania zegara systemowego, czy raczej napisania stopera, znalazłem wiele pytań ale mało odpowiedzi. Problem dotyczył odmierzania czasu do transmisji przez LPT, albo sprawdzania szybkości procedur. Jednym z ciekawszych rozwiązań było wykorzystanie instrukcji  

RDTSC mikroprocesora Pentium, oto link:

Jak zmierzyć czas wykonywania operacji

Co jednak zrobić, jeśli nie chcemy wykorzystywać aż tak potężnej mocy obliczeniowej, a wystarczy nam poczciwy 286? Wykorzystać zegar systemowy z układu 8253.
Jest on sprzężony z przerwaniem sprzętowym IRQ0 i programowym 1Ch, generuje je 18.2 raza na sekundę. Wystarczy podpiąć się pod 1Ch i możemy 18.2 raza na sekundę zmieniać zmienną licznik. Jak się podpiąć pod 1Ch ? Przeczytaj help dla instrukcji GetIntVect, jest tam właściwie gotowy program.

uses Dos, Crt;

var
  licznik : word;
  Int1CSave : Pointer;
{$F+,S-,W-}
procedure TimerHandler; interrupt;
  begin
    { Timer ISR }
    {Tutaj wpisz swoją procedurę obsługi przerwania, np.}
      licznik:=licznik + 1;
  end;
{$F-,S+}
begin
  GetIntVec($1C,Int1CSave);
  SetIntVec($1C,Addr(TimerHandler));
  writeln('Press ANYKEY to exit');
  repeat 
  gotoxy(1,1);
  write(licznik);
  until Keypressed;
  SetIntVec($01C,Int1CSave);
end.

Istnieje inny, chyba prostszy sposób wykorzystania licznika systemowego, można go zerować i odczytywać jego wartość. Licznik ten jest 64bitowy, uruchomiony zostaje chyba przy starcie komputera, dlatego odczytując go mamy w zasadzie przypadkową wartość. Tak samo jak odczyt przebiega zapis stanu licznika, wystarczy zapisać zero i licznik "jak nowy". Dostęp do stanu licznika mamy w pamięci pod adresem 0040h:006Ch. Oto przykład:

asm    {zerowanie licznika}
   push ds    {data segment lepiej zachować, aby nie wysypać programu}
   mov ax,0040h
   mov ds,ax
   mov di,006Ch
   mov ax,0     {wartość licznika} 
   mov ds:[di],ax    {zapisujemy młodsze słowo}
   mov ds:[di+2],ax  {oraz starsze słowo}
   pop ds      {przywrócenie starego rejestru segmentu danych}
end; 

A teraz przykład zapisu stanu licznika wartościami dwu zmiennych a i b:

var  a,b:word;
{...}
asm
   {zanim zmienimy ds trzeba odczytać wartości zmiennych} 
   mov bx,a   {młodsze słowo licznika}
   mov cx,b   {starsze słowo licznika}
   push ds    {data segment lepiej zachować, aby nie wysypać programu}
   mov ax,0040h
   mov ds,ax
   mov di,006Ch
   mov ax,bx     {wartość licznika - młodsze słowo} 
   mov ds:[di],ax    {zapisujemy młodsze słowo}
   mov ax,cx
   mov ds:[di+2],ax  {oraz starsze słowo}
   pop ds      {przywrócenie starego rejestru segmentu danych}
end;

I w końcu przykład odczytu stanu licznika:

asm
   push ds    {data segment lepiej zachować, aby nie wysypać programu}
   mov ax,0040h
   mov ds,ax
   mov di,006Ch
   mov ax,ds:[di]    {odczytujemy młodsze słowo}
   mov bx,ax     {zapamiętujemy wartość licznika - młodsze słowo} 
   mov ax,ds:[di+2]  {oraz starsze słowo}
   pop ds      {przywrócenie starego rejestru segmentu danych}
   mov a,bx   {młodsze słowo licznika}
   mov b,ax   {starsze słowo licznika}
end; 

Co zrobić kiedy częstotliwość zegara 18.2 nie jest dla nas zadowalająca? Otóż...

Wartość częstotliwości generacji IRQ0 (dalej F) jest obliczana w następujący sposób:

F = 1193181 / dzielnik

Dzielnik to dowolna 16-bitowa liczba (od 0 do 65535). Standartowo dzielnik wynosi 65535, dlatego F=18.2. Jeśli chcemy obliczyć nową wartość dzielnika, to obliczamy go tak:

nowy_dzielnik = 1193181 / nowa_F

np. dla 100Hz, nowy dzielnik = 11931.81, zwiększamy do najbliższej liczby całkowitej,
otrzymujemy 11932.

Teraz trzeba zaprogramować 8253 nową wartością dzielnika, korzystamy z portu 43h i 40h:

Opis portów PC: http://lpt.strona.pl/ports.txt

asm
   mov al,36h
   out 43h,al         {najpierw instrukcja dla licznika}

   mov ax,11932   {nowa czestotliwosc}
   out 40h,al          {zaczynamy od mniej znaczacego bajtu}
   mov al,ah
   out 40h,al         {i bardziej znaczacy bajt}
end; 

UWAGA!
Programując układ 8253 dla nowych częstotliwości należy wziąć pod uwagę ew. konsekwencje. Jeśli jakiś inny program rezydentny (np. system operacyjny) jest podpięty pod to (IRQ0) przerwanie, to zmieniając jego częstotliwość możemy mieć wpływ na częstotliwość pewnych procesów w systemie. Jeśli by ktoś napisał rezydentny wygaszacz ekranu który korzystał by z IRQ0 dla obliczania czasu po jakim ma się uruchomić, to przyspieszenie IRQ0 zmniejszy czas pojawiania się owego wygaszacza. Mamy tutaj zakres zmian od 18.2 Hz do 1.1MHz, można więc pewne procesy przyspieszyć setki tysięcy razy. Należy więc ostrożnie korzystać z programowania zegara, najlepiej obejrzeć pod Debuggerem zawartość procedury przerwania 1Ch, jeśli będzie zawierała tylko komendę IRET, to znaczy to że jest pusta i można swobodnie podpiąć się pod nie.

4 komentarzy

Nice tutorial. It's helpful to know.

A very interesting topic that I have reviewed, I think this is one of the most important information for me. And I'm glad to read your article. Thanks for sharing!

Plato is widely believed to have been a student of Socrates and to have been deeply influenced by his teacher's unjust death. http://newtopdownloadoftorrent.com Their current "happiness" status is shown by a sun and cloud symbol above their head.

Przeniosłem do assemblera. Bo to głównie jego dotyczy. Choć nie wiem czy i kategoryzacja "Pascal" nie była by wskazana.