Programmable Interval Timer: Difference between revisions

Added source tags for assembly and C source code.
[unchecked revision][unchecked revision]
(→‎See Also: Add Wikipedia external link.)
(Added source tags for assembly and C source code.)
Line 1:
The '''Programmable Interval Timer''' ('''PIT''') chip (also called 8253/8254 chip) basically consists of an oscillator, a prescaler and 3 independent frequency dividers. Each frequency divider has an output, which is used to allow the timer to control external circuitry (for example, IRQ 0).
 
 
== The Oscillator ==
 
The oscillator used by the PIT chip runs at (roughly) 1.193182 MHz. The reason for this requires a trip back into history (to the latter half of the 1970's)...
 
Line 9 ⟶ 7:
 
== Frequency Dividers ==
 
The basic principle of a frequency divider is to divide one frequency to obtain a slower frequency. This is typically done by using a counter. Each "pulse" from the input frequency causes the counter to be decreased, and when that counter has reached zero a pulse is generated on the output and the counter is reset. For example, if the input signal is 200 Hz and the counter is reset to a value of ten each time, then the output frequency would be 200/10, or 20 Hz.
 
Line 19 ⟶ 16:
 
== PIT Timer Accuracy ==
 
The accuracy of the PIT timer depends on the quality of the oscillator used, and is typically accurate to within +/- 1.73 seconds per day. There are many causes for this inaccuracy, however because of this there isn't much point in specifying times or frequencies to more than five or six digits. Despite this, I like to be as accurate as possible!
 
== Outputs ==
 
Each channel (or frequency divider) has an output signal that is used for a different purpose.
 
=== Channel 0 ===
 
The output for PIT channel 0 is connected to the PIC chip, so that it generates an "IRQ 0". Typically during boot the BIOS sets channel 0 with a count of 65536, which gives an output frequency of 18.2065 Hz (or an IRQ every 54.9254 mS). Channel 0 is probably the most useful PIT channel, as it is the only channel that is connected to an IRQ.
 
=== Channel 1 ===
 
The output for PIT channel 1 was once used (in conjunction with the DMA controller's channel 0) for refreshing the DRAM (Dynamic Random Access Memory) or RAM. Typically, each bit in RAM consists of a capacitor which holds a tiny charge representing the state of that bit, however (due to leakage) these capacitors need to be "refreshed" periodically so that they don't forget their state.
 
Line 37 ⟶ 30:
 
=== Channel 2 ===
 
The output of PIT channel 2 is connected to the PC speaker, so that frequency of the output determines the frequency of the sound produced by the speaker. This is the only channel where the gate input can be controlled by software (via. bit 0 of I/O port 0x61), and the only channel where it's output can be read by software (via. bit 5 of I/O port 0x61). Details of how to program the PC speaker can be found [[PC_Speaker|here]].
 
== I/O Ports ==
 
The PIT chip uses the following I/O ports:
 
Line 85 ⟶ 76:
 
== Operating Modes ==
 
While each operating mode behaves differently, some things are common to all operating modes. This includes:
 
Line 98 ⟶ 88:
 
=== Mode 0 - Interrupt On Terminal Count ===
 
For this mode, when the mode/command register is written the output signal goes low and the PIT waits for the reload register to be set by software. After the reload register has been set, the current count will be set to the reload value on the next falling edge of the (1.193182 MHz) input signal. Subsequent falling edges of the input signal will decrement the current count (if the gate input is high on the preceding rising edge of the input signal).
 
Line 106 ⟶ 95:
 
=== Mode 1 - Hardware Re-triggerable One-shot ===
 
This mode is similar to mode 0 above, however counting doesn't start until a rising edge of the gate input is detected. For this reason it is not usable for PIT channels 0 or 1 (where the gate input can't be changed).
 
Line 118 ⟶ 106:
 
=== Mode 2 - Rate Generator ===
 
This mode operates as a frequency divider.
 
Line 134 ⟶ 121:
 
=== Mode 3 - Square Wave Generator ===
 
For mode 3, the PIT channel operates as a frequency divider like mode 2, however the output signal is fed into an internal "flip flop" to produce a square wave (rather than a short pulse). The flip flop changes its output state each time its input state (or the output of the PIT channel's frequency divider) changes. This causes the actual output to change state half as often, so to compensate for this the current count is decremented twice on each falling edge of the input signal (instead of once), and the current count is set to the reload value twice as often.
 
Line 151 ⟶ 137:
 
=== Mode 4 - Software Triggered Strobe ===
 
Mode four operates as a retriggerable delay, and generates a pulse when the current count reaches zero.
 
Line 163 ⟶ 148:
 
=== Mode 5 - Hardware Triggered Strobe ===
 
Mode 5 is similar to mode 4, except that it waits for the rising edge of the gate input to trigger (or re-trigger) the delay period (like mode 1). For this reason it is not usable for PIT channels 0 or 1 (where the gate input can't be changed).
 
Line 175 ⟶ 159:
 
== Counter Latch Command ==
 
To prevent the current count from being updated, it is possible to "latch" a PIT channel using the latch command. To do this, send the value CC000000 (in binary) to the mode/command register (I/O port 0x43), where 'CC' corresponds to the channel number. When the latch command has been sent, the current count is copied into an internal "latch register" which can then be read via. the data port corresponding to the selected channel (I/O ports 0x40 to 0x42). The value kept in the latch register remains the same until it has been fully read, or until a new mode/command register is written.
 
Line 183 ⟶ 166:
 
== Read Back Command ==
 
The read back command is a special command sent to the mode/command register (I/O port 0x43). The "read back" is not supported on the old 8253 chips but should be supported on all AT and later computers except for PS/2 (i.e. anything that isn't obsolete will support it).
 
Line 235 ⟶ 217:
 
== Reading The Current Count ==
 
To read the current count using the "lobyte only" or "hibyte only" access modes, you can just do an "in al,0x40" (for PIT channel 0) without problems. For frequencies higher than 4.7 KHz it can be easiest to set the high byte of the reload value to zero, and then use the "lobyte only" access mode to minimize hassles.
 
For the "lobyte/hibyte" access mode you need to send the latch command (described above) to avoid getting wrong results. If any other code could try set the PIT channel's reload value or read its current count after you've sent the latch command but before you've read the highest 8 bits, then you have to prevent it. Disabling interrupts works for single CPU computers. For example, to read the count of PIT channel 0 you could use something like:
 
<source lang="asm">
read_PIT_count :
read_PIT_count:
pushfd
pushfd
cli
cli
mov al,00000000b ;al = channel in bits 6 and 7, remaining bits clear
mov al, 00000000b ; al = channel in bits 6 and 7, remaining bits clear
out 0x43,al ;Send the latch command
out 0x43, al ; Send the latch command
in al, 0x40 ; al = low byte of count
mov ah, al ; ah = low byte of count
in al, 0x40 ; al = high byte of count
rol ax, 8 ; al = low byte, ah = high byte (ax = current count)
popfd
ret
</source>
 
== Setting The Reload Value ==
 
To set the reload value, just sent the value/s to the corresponding data port. For the "lobyte only" or "hibyte only" access modes this only takes a single "out 0x40,al" (for PIT channel 0).
 
For the "lobyte/hibyte" access mode you need to send the lowest 8 bits, then the highest 8 bits. If any other code could try set the PIT channel's reload value or read its current count after you've sent the lowest 8 bits but before you've sent the highest 8 bits, then you have to prevent it. Disabling interrupts works for single CPU computers. For example:
 
<source lang="asm">
set_PIT_count:
set_PIT_count:
pushfd
pushfd
cli
cli
out 0x40,al ;Set low byte of reload value
out rol ax0x40,8 al ;al =Set highlow byte, ahof =reload low bytevalue
rol ax, 8 out 0x40,al ;Set al = high byte, ofah reload= valuelow byte
out rol ax0x40,8 al ;al = low byte, ah =Set high byte (ax = originalof reload value)
rol ax, 8 ; al = low byte, ah = high byte (ax = original reload value)
popfd
popfd
ret
ret
</source>
 
It should be notes that a reload value of zero can be used to specify a divisor of 65536. This is how the BIOS gets an IRQ 0 frequency as low as 18.2065 Hz.
 
== PIT Channel 0 Example Code ==
 
To complete this page I've to included the following example code. This code was written for NASM, but hasn't been tested.
 
Line 283 ⟶ 266:
To begin with, this following code contains all of the data used by this example. It is assumed that the ".bss" section is filled with zeros.
 
<source lang="asm">
section .bss
section .bss
system_timer_fractions: resd 1 ; Fractions of 1 mS since timer initialized
system_timer_mSsystem_timer_fractions: resd 1 ; NumberFractions of whole1 mS since timer initialized
IRQ0_fractionssystem_timer_mS: resd 1 ; FractionsNumber of 1whole mS betweensince timer IRQsinitialized
IRQ0_mSIRQ0_fractions: resd 1 ; NumberFractions of whole1 mS between IRQs
IRQ0_frequencyIRQ0_mS: resd 1 ; Actual frequencyNumber of PITwhole mS between IRQs
PIT_reload_valueIRQ0_frequency: resw resd 1 ; CurrentActual PITfrequency reloadof valuePIT
PIT_reload_value: resw 1 ; Current PIT reload value
section .text
section .text
</source>
 
Next is the handler for IRQ 0. It's fairly simple (all it does it add 2 64 bit fixed point values and send an EOI to the PIC chip).
 
<source lang="asm">
IRQ0_handler:
IRQ0_handler:
push eax
push ebxeax
push ebx
mov eax, [IRQ0_fractions]
mov ebx, [IRQ0_mS] ; eax.ebx = amount of time between IRQs
add [system_timer_fractions], eax ; ;Update system timer tick fractions
adc [system_timer_mS], ebx ; Update system timer tick milli-seconds
mov al, 0x20
out 0x20, al ; Send the EOI to the PIC
 
pop ebx
pop eax
iretd
</source>
 
Now for the tricky bit - the initialization routine. It should be noted that the PIT can't generate some frequencies, for example if you want 8000 Hz then you've got a choice of 8007.93 Hz or 7954.544 Hz. In this case the following code will find the nearest possible frequency. Once it has calculated the nearest possible frequency it will reverse the calculation to find the actual frequency selected (rounded to the nearest integer, intended for display purposes only).
Line 314 ⟶ 301:
For some extra accuracy, I also use "3579545 / 3" instead of 1193182 Hz. This is mostly pointless due to inaccurate hardware (I just like being correct).
 
<source lang="asm">
;Input
; ebx Desired PIT frequency in Hz
Line 410 ⟶ 398:
popad
ret
</source>
 
I should point out that you'd also need to install an IDT entry for IRQ 0, and unmask it in the PIC chip (or I/O APIC).
Line 418 ⟶ 407:
===Using the IRQ to Implement <tt>sleep</tt>===
The PIT's generating a hardware interrupt every ''n'' milliseconds allows you to create a simple timer. Start with a global variable x that contains the delay:
<source lang="asm">
SEGMENT .DATA
CountDown DD 0
Line 458 ⟶ 448:
POP EBP
RETN 8
</source>
 
In a multitasking system, consider using a linked list or array of these CountDown variables. If your multitasking system supports interprocess communication, you can also store the semaphore/exchange where two processes can talk to, have the interrupt send a message to the waiting process when the timer is done, and have the waiting process block all execution until that message comes:
 
#define COUNTDOWN_DONE_MSG 1
<source lang="c">
struct TimerBlock {
#define COUNTDOWN_DONE_MSG 1
EXCHANGE e;
struct TimerBlock {
u32int CountDown;
EXCHANGE e;
} timerblocks[20];
u32int CountDown;
} timerblocks[20];
void TimerIRQ(void) /* called from Assembly */
 
{
void TimerIRQ(void) /* called from Assembly */
u8int i;
{
u8int for (i = 0; i < 20; i++)
if (timerblocks[i].CountDown > 0) {
for (i = 0; i < 20; timerblocks[i].CountDown--;++)
if (timerblocks[i].CountDown ==> 0) {
SendMessage(timerblocks[i].e, COUNTDOWN_DONE_MESSAGE)CountDown--;
if (timerblocks[i].CountDown == 0)
}
SendMessage(timerblocks[i].e, COUNTDOWN_DONE_MESSAGE);
}
}
}
void Sleep(u32int delay)
 
{
void Sleep(u32int delay)
struct TimerBlock *t;
{
struct TimerBlock *t;
if ((t = findTimerBlock()) == nil)
 
return;
if ((t = findTimerBlock()) == nil)
t->CountDown = delay;
return;
WaitForMessageFrom(t->e = getCrntExch());
t->CountDown = delay;
}
WaitForMessageFrom(t->e = getCrntExch());
}
</source>
 
In your documentation, note the interval of the timer. For example, if the timer interval is 10 milliseconds per tick, tell the programmer to issue
 
Sleep(100);
<source lang="c">
Sleep(100);
</source>
 
to sleep for a single second.
 
Line 500 ⟶ 498:
 
=== Threads ===
 
=== External Links ===
[[wikipedia:Programmable Interval Timer | Programmable Interval Timer]] on Wikipedia
252

edits