lesson15

advertisement
Using the 8254 Timer-Counter
Understanding the role of the
system’s 8254 programmable
Interval-Timer/Counter
The 8254 PIT
• The 8254 Programmable Interval-timer is
used by the PC system for (1) generating
timer-tick interrupts (rate is 18.2 per sec),
(2) performing dynamic memory-refresh
(reads ram once every 15 microseconds),
and (3) generates ‘beeps’ of PC speaker
• When the speaker-function isn’t needed,
the 8254 is available for other purposes
Displaying ‘Time-Of-Day’
• Algorithm steps:
– Get the count of timer-interrupts so far today
– Convert these ‘timer-ticks’ into seconds
– Breakdown the total number of seconds today
into Hours, Minutes, Seconds, and AM/PM
– Convert numerical values into digit-strings
– Output these results to the video terminal
Where’s the ‘tick’ counter?
main memory
0x00500
0040:006C
0x00400
tick_count
ROM-BIOS DATA AREA
Interrupt Vector Table
(for real-mode)
0x00000
Number of timer-tick
interrupts so far today
(longword at 0x0046C)
Getting the ‘tick’ count
• The ROM-BIOS interrupt-handler for the
timer interrupt stores the tick-count as a
32-bit integer located at address 0x046C
(it’s in the ROM-BIOS DATA AREA)
• In real-mode, we can get it like this:
xor
mov
mov
mov
%ax, %ax
%ax, %fs
%fs:0x046C, %eax
%eax, total_ticks
# address segment zero
# using FS register
# copy tick-count to EAX
# save in a local variable
segment-override prefix (segment used would be %ds)
Converting ‘ticks’ to seconds
total_ticks_today
total_seconds_today =
number of ticks-per-second
The number of ‘ticks-per-second’ is based upon the way
the PC’s timing hardware has been programmed
Input/Output frequencies
• The input-pulses to each Timer-channel is
a long established PC standard, based on
the design of the chrystal oscillator chip:
1,193,182 pulses-per-second (Hertz)
• The frequency of the output-pulses from
any Timer-channel is determined by how
that channel’s Latch was programmed
Three timer/counter ‘channels’
8284
PCLK
1193182 Hz
CLK0
GATE0
Channel 0
Channel 1
Port 0x61, bit #0
OUT1
DRAM refresh
Port 0x61, bit #5
CLK2
GATE2
Interrupt IRQ0
Port 0x61, bit #4
CLK1
GATE1
OUT0
Channel 2
8254 PIT
+5 V
Port 0x61, bit #1
OUT2
AND
speaker
Controlling timer-channel 2
I/O port 0x61 (aka ‘Port_B’)
7
6
5
4
3
2
1
0
r/o
r/o
r/o
r/o
r/w
r/w
r/w
r/w
OUT2
status
OUT1
status
RAM
parity
error
I/O
channel
error
SPKR GATE2
control control
I/O
channel
checking
enabled
RAM
parity
checking
enabled
Counter decrements when pulsed
COUNT REGISTER
CLK
MSB
MSB
LSB
LSB
LATCH REGISTER
GATE
STATUS
TIMER/COUNTER CHANNEL
OUT
8254 Command-Port
7
6
CHANNEL
Channel-ID
00 = chn 0
01 = chn 1
10 = chn 2
5
4
COMMAND
3
2
OUTPUT MODE
1
0
binary
/ BCD
Output Mode
Counting Mode
Command-ID
000 = one-shot level 0 = binary
00 = Latch
001 = retriggerable
1 = BCD
01 = LSB r/w
010 = rate-generator
10 = MSB r/w
11 = LSB-MSB r/w 011 = square-wave
100 = software strobe
101 = hardware strobe
Commands are sent to the 8254 via io/port 0x43
Programming a PIT channel
• Step 1: send command to PIT (port 0x43)
• Step 2: read or write the channel’s Latch
– via port 0x40 for channel 0
– via port 0x41 for channel 1
– via port 0x42 for channel 2
A ten-millisecond delay
• In future lessons we will want to create a
time-delay of ten-milliseconds (allowing
some hardware to finish its initialization)
• We can do it using the Timer Channel 2
• We program its ‘Latch Register’ with the
Timer Input-Pulse Frequency, multiplied by
1/100 (i.e., 1193182 / 100 = 11932)
• We specify the ‘one-shot’ counting mode
Code for the 10ms delay
# enable Timer Channel 2
in
$0x61, %al
and
$0x0C, %al
or
$0x01, %al
out
%al, $0x61
# program Channel 2 for “one-shot” countdown
mov
$0xB0, %al
out
%al, $0x43
# write Channel 2 Latch-Register (LSB/MSB)
mov
$11932, %ax
out
%al, $0x42
mov
%ah, %al
out
%al, $0x42
poll:
# delay until OUT2 signal is activated (bit 5)
in
$0x61, %al
test
$0x20, %al
jz
poll
# disable Timer Channel 2
in
$0x61, %al
and
$0x0C, %al
out
%al, $0x61
Standard BIOS programming
• For Channel 0 (the ‘timer-tick’ interrupt)
the Latch is programmed during system
startup with a value of zero
• But the Timer interprets zero as 65,536
• So the frequency of the output-pulses from
Timer-channel 0 is equal to this quotient:
output-frequency = input-frequency / frequency-divisor
= 1193182 / 65536 (approximately 18.2)
Consequently…
• To compute ‘total_seconds’ from ‘total_ticks’:
total_seconds = total_ticks / ticks_per_second
= total_ticks / (1193182 / 65536)
= ( total_ticks * 65536 ) / 1193183
• We can use the x86 CPU’s integer-arithmetic
instructions MUL (multiply) and DIV (divide)
How ‘MUL’ works
Before executing the MUL instruction…
EAX
reg (or mem)
multiplicand (32-bits)
multiplier (32-bits)
32-bit operands
Here’s the instruction…
mull
reg_or_mem
After executing the MUL instruction…
EDX
EAX
64-bit product
product (64-bits)
How ‘DIV’ works
Before executing the DIV instruction…
EDX
EAX
dividend (64-bits)
64-bit dividend
reg (or mem)
divisor (32-bits)
32-bit operand
Here’s the instruction…
divl
reg_or_mem
After executing the DIV instruction…
EDX
EAX
32-bit remainder
32-bit quotient
two results (32-bits)
Implementing the conversion
• So use MUL and DIV to convert ‘ticks’ into
‘seconds’, like this:
# total_seconds = ( total_ticks * FREQ_DIVISOR ) / PULSES_PER_SEC
mov
mov
mul
mov
div
mov
total_ticks, %eax
$FREQ_DIVISOR, %ecx
%ecx
$PULSES_PER_SEC, %ecx
%ecx
%eax, total_seconds
# Now integer-quotient is in EAX, and integer-remainder is in EDX
‘Time-Of-Day’ Format
HH:MM:SS am/pm
hours
seconds
minutes
morning or
afternoon
So we need to compute four numerical values from the ‘total_seconds’ integer
Our four time-parameters
We use these arithmetical ideas:
– total_minutes = ( total_seconds / 60 );
ss = ( total_seconds % 60 );
– total_hours = (total_minutes / 60 );
mm = ( total_minutes % 60 );
– total_halfdays = (total_hours / 12 );
hh = (total_hours % 12 );
– Total_days = ( total_halfdays / 2 );
xm = total_halfdays % 2;
A subtle refinement
• Our ‘total_seconds’ value was gotten with
an integer-division operation, so there’s
likely to be some ‘round-off’ error
• How can we be sure we use the ‘closest’
integer to the actual quotient?
• We should remember the ‘rounding’ rule!
• When ‘remainder’ is equal or greater than
1/2 of ‘divisor’, ‘quotient’ gets incremented
How to implement rounding?
• There is more than one way to do it – i.e.,
the “amateur’s” way or the “expert’s” way
• Knowledge of the CPU’s architecture and
instruction-set can assist
• The ‘obvious’ method:
• if ( 2 * remainder >= divisor ) ++quotient;
• But this uses a multiply and a conditional
jump-instruction (inefficient!)
Avoiding inefficiency…
• Replace the ‘multiply’ with an ‘addition’
• Use ‘subtract’ and ‘add-with-carry’ instead
of using ‘compare’ and ‘conditionally-jump’
# Recall: quotient was in EAX, remainder was in EDX, divisor was in ECX
add
%edx, %edx
# doubles the remainder
sub
%ecx, %edx
# computes: 2*quotient – divisor
# now carry-flag is clear in case 2*quotient >= divisor
cmc
# complement the carry-flag bit
# now carry-flag is set in case 2*quotient >= divisor
adc
$0, %eax
# add the carry-flag to the quotient
# So this achieves the same effect as the ‘rounding rule’, but wit no jump!
In-class exercise
• Can you enhance our ‘timeoday.s’ demo to
make it more dramatic (and later useful)
by creating a loop within its ‘main’ routine,
so that it continues to read and display the
time (until the user presses a key)?
• HINTS: Use an INT-0x16 keyboard service
to ‘peek’ into the keyboard-queue, and
omit the ‘\n’ (newline) control-code from
the ‘report’ message-string
Download