Some time ago I saw a note on the mailinglist from someone in need for a flexible timer function. For this, there are several concepts.
First, there is the timertick which is updated every 55 ms. For long
time delays, this is the best method. Just read the timervalue at
0000:046C, add the desired delay (in 55 ms intervals) and wait until the
timer reaches that value.
A second approach is to use modern BIOS-ses which have a timingfunction
in BIOS interrupt 15h, but this is "only" present on machines from 1990
or later.
A third approach is to reprogram the RTC chip. No big deal, and there's
a very accurate timer in it (upto 8 kHz) which even has interrupt
capabillities for automated functions and simple multitaskings.
But by far the best way (and most universal and accurate) is to use the
"spare" timer in your PC's 8254 chip.
This chip can be put in many operating modes, but we want it to do the
following:
- start counting at a certain value
- count down
- latched reading mode
- no influence on further PC operation
The counting sequence for the PC is as follows:
- there are 2^16 BIOS-timervalue updates per hour
- there are 2^16 8254 clockpulses per timertick
So, there are 2^32 clockpulses per hour. This boils down to one clock
pulse being around 838 ns. Not bad.
In order to make things very clear I use Modula-2 to show how the
routines are coded. Modula is an extremely structured language, so I use
it as a kind of Meta-Assembler or Pseudo-Assembler.
For those not too familiar with Modula: a CARDINAL is not an old man in
a dress, but a 16 bit unsigned integer.
Here comes.....
---------- OpenTimer ---------------------------- Start ----------
PROCEDURE OpenTimer; (* open timer chip in mode 2 *)
BEGIN
ASM
MOV AL, 34H
OUT 43H, AL
XOR AL, AL
OUT 40H, AL
OUT 40H, AL
END;
END OpenTimer;
---------- OpenTimer ----------------------------- End -----------
The value 34h is constructed as follows:
bit function
----- ---------------------------
6 - 7 select counter (0 - 3)
4 - 5 Read/write mode
1 - 3 Select countermode
0 Binary or BCD
For this case we selected:
- counter 00
- read/write two bytes from/to counterchip
- Mode 2
- binary values
These few lines open the timer in "Mode 2" and prime the down counting
register to 0000. I would love to elaborate on the code, but this is all
which is needed....
It is kind of handy if you restore the state of your machine after your
application stops using the CPU. Therefore there is the following
function to restore "normal" operation of this channel.
---------- CloseTimer --------------------------- Start ----------
PROCEDURE CloseTimer; (* close timer chip *)
BEGIN
ASM
MOV AL, 36H
OUT 43H, AL
XOR AL, AL
OUT 40H, AL
OUT 40H, AL
END;
END CloseTimer;
---------- CloseTimer ---------------------------- End -----------
This function just restores the timer to it's default mode and clears
the counting registers. The value "36h" means:
- counter 00
- read/write two bytes from/to counterchip
- Mode 3
- binary values
---------- ReadTimer ---------------------------- Start ----------
PROCEDURE ReadTimer () : CARDINAL; (* read timer *)
VAR Time : CARDINAL;
BEGIN
ASM
MOV AL, 6
OUT 43H, AL
IN AL, 40H
MOV AH, AL
IN AL, 40H
XCHG AH, AL
MOV [Time], AX
END;
RETURN Time;
END ReadTimer;
---------- ReadTimer ----------------------------- End -----------
After we opened the timer, it might be a good idea to also use it. This
is done in a two-step operation:
- current value of counting register is stored in On-Chip buffer
- the low byte is read in first
- the high byte is read in second
- low and high byte are put in right order
Make sure you always read in TWO bytes, else you will run into framing
errors. Also keep in mind that this is a DOWN-COUNTER!
The value "6" which is sent to the 8254 first might be wrong, but in all
my software it just works fine. It selects Channel 0 to be latched. The
lower four bits of this word should be "don't care" bits, but I prefer
"not to fix a running program".
---------- MilliSeconds ------------------------- Start ----------
PROCEDURE MilliSeconds (ms : CARDINAL);
VAR MaxCount : CARDINAL;
BEGIN
MaxCount := 65535 - ms * 1193;
OpenTimer;
WHILE ReadTimer () > MaxCount DO
(* Nothing! *)
END;
CloseTimer;
END MilliSeconds;
---------- MilliSeconds -------------------------- End -----------
This function has some deliberate errors inside. I calculate MaxCount
such that it is too big. Reason: in Modula I do not control math
operations as well as in ASM (of course!) That's why I subtract the
value from 65,535 instead of 65,536. In ASM I would have used a NOT
operation, but for Modula this is good enough.
Furthermore I use the number 1193 to go from counting pulses to
milliseconds. It's a not too big number so it is good enough to use in
integer arithmatics.
This "MilliSeconds" routine is a dumb waiting-procedure. It calculates a
stop-value for the counter, initialises the counter to mode 2 and value
0000 and then waits until the timer reaches there. Next it closes the
timer and it's all over.
The next function, which was made for diagnostic purposes, shows that in
an application you would have to correct for the
---------- TestTimer ---------------------------- Start ----------
PROCEDURE TestTimer;
VAR First, Last, Delta, k : CARDINAL;
BEGIN
OpenTimer;
First := ReadTimer ();
WriteCard (First, 6); Write (Tab);
FOR k := 1 TO 10000 DO
(* Nothing! *)
END;
Last := ReadTimer ();
Delta := First - Last;
WriteCard (Delta, 6); WriteLn;
CloseTimer;
END TestTimer;
---------- TestTimer ----------------------------- End -----------
You could use this routine to calibrate a timingloop, but on modern PC
architectures this could well lead to disasters. Modern CPU's are so
damned fast, that your loopcounter will overflow.
Therefore this calibration technique is only useful for modifying
inherently slow routines, like those using I/O operations. For some
reason, I/O operations still need around one microsecond each, so these
will slow down the routine enough to make sure there will be no overflow
in the loop-counters.
A friend of mine just uses IN instructions from some silly address to
get reasonably accurate timingloops, assuming that 1 IN operation is
about 1 microsecond. Bit it could well lead to trouble on modern PCI
hardware.
All in all, for most delay-routines, the dumb waiting function is by far
the best since it is the most reliable and accurate to less than a
microsecond. But if you need this many digits, use compensated software,
that takes into account the time to read the timers twice -- because you
need to keep in mind that also this routine relies heavily on I/O
instructions, so it is not infinitely fast!
In a future article I will describe how to use the RTC chip for
generating timing signals and how to use it via the Programmable
Interrupt Controller in automatic mode. That article will be pure ASM
again, so don't be worried about this detour into Modula.
|