APIC Timer
The great benefit of Local APIC timer that it's hardwired to each CPU core, as opposite to Programmable Interval Timer which is a separate circuit. Because of this, no need to any resource management, which make things easier. The downside is it's oscillating at CPU frequency, which varies from machine to machine, while PIT uses a standard frequency (1193182 Hz). To make use of it, you have to know how many interrupts/sec it's capable of.
Enabling APIC timer
- First you have to enable the Local APIC hardware by writing it's MSR.
- After that, you have to specify a spurious interrupt and software enable the APIC (this step is necessary).
- Finally, you specify APIC timer interrupt number and operation mode.
You can find more detailed information in Intel manual vol3A Chapter 9.
Initializing
There're several ways to do this, but all of them use a different, CPU bus frequency independent clock source to do that. Examples: Real Time Clock, TimeStamp Counter, PIT or even polling CMOS registers. In this tutorial we will use the good old PIT, as it's the easiest. Steps need to be done:
- Reset APIC to a well known state
- Enable APIC timer
- Reset APIC timer counter
- Wait a specific amount of time measured by a different clock
- Get number of ticks from APIC timer counter
- Adjust it to a second
- Divide it by the quantum of your choice (results X)
- Make the APIC timer fire an interrupt at every X ticks
The APIC timer can be set to make a tick (decrease counter) at a given frequency, which is called "divide value". This means you have to multiply APIC timer counter ticks by this divide value to get the true CPU bus frequency. You could use value of 1 (ticks on every bus cycle) up to 128 (ticks on every 128th cycle). See Intel manual vol3A Chapter 9.5.4 on details. Note that according to my tests, Bochs seems not to handle divide value of 1 properly, so I will use 16.
Prerequires
Before we start, let's define some constant and functions.
apic = the linear address where you have mapped the APIC registers
APIC_APICID = 20h
APIC_APICVER = 30h
APIC_TASKPRIOR = 80h
APIC_EOI = 0B0h
APIC_LDR = 0D0h
APIC_DFR = 0E0h
APIC_SPURIOUS = 0F0h
APIC_ESR = 280h
APIC_ICRL = 300h
APIC_ICRH = 310h
APIC_LVT_TMR = 320h
APIC_LVT_PERF = 340h
APIC_LVT_LINT0 = 350h
APIC_LVT_LINT1 = 360h
APIC_LVT_ERR = 370h
APIC_TMRINITCNT = 380h
APIC_TMRCURRCNT = 390h
APIC_TMRDIV = 3E0h
APIC_LAST = 38Fh
APIC_DISABLE = 10000h
APIC_SW_ENABLE = 100h
APIC_CPUFOCUS = 200h
APIC_NMI = (4<<8)
TMR_PERIODIC = 20000h
TMR_BASEDIV = (1<<20)
;Interrupt Service Routines
isr_dummytmr: mov dword [apic+APIC_EOI], 0
iret
isr_spurious: iret
;function to set a specific interrupt gate in IDT
;al=interrupt
;ebx=isr entry point
writegate: ...
ret
I will also assume that you have a working IDT, and you have a function to write a gate for a specific interrupt: writegate(intnumber,israddress). Furthermore, to make things simple, I'll assume that you did not changed the default interrupt mapping found in almost every tutorial:
- interrupt 0-31: exceptions
- interrupt 32: timer, IRQ0
- interrupt 39: spurious irq, IRQ7
If you've already changed this, modify accordingly.
Example code in ASM
Here's a possible way to initialize APIC timer in fasm syntax assembly:
;you should read MSR, get APIC base and map to "apic"
;you should have used lidt properly
;set up isrs
mov al, 32
mov ebx, isr_dummytmr
call writegate
mov al, 39
mov ebx, isr_spurious
call writegate
;initialize LAPIC to a well known state
mov dword [apic+APIC_DFR], 0FFFFFFFFh
mov eax, dword [apic+APIC_LDR]
and eax, 00FFFFFFh
or al, 1
mov dword [apic+APIC_LDR], eax
mov dword [apic+APIC_LVT_TMR], APIC_DISABLE
mov dword [apic+APIC_LVT_PERF], APIC_NMI
mov dword [apic+APIC_LVT_LINT0], APIC_DISABLE
mov dword [apic+APIC_LVT_LINT1], APIC_DISABLE
mov dword [apic+APIC_TASKPRIOR], 0
;okay, now we can enable APIC
;global enable
mov ecx, 1bh
rdmsr
bts eax, 11
wrmsr
;software enable, map spurious interrupt to dummy isr
mov dword [apic+APIC_SPURIOUS], 39+APIC_SW_ENABLE
;map APIC timer to an interrupt, and by that enable it in one-shot mode
mov dword [apic+APIC_LVT_TMR], 32
;set up divide value to 16
mov dword [apic+APIC_TMRDIV], 03h
;ebx=0xFFFFFFFF;
xor ebx, ebx
dec ebx
;initialize PIT Ch 2 in one-shot mode
;waiting 1 sec could slow down boot time considerably,
;so we'll wait 1/100 sec, and multiply the counted ticks
mov dx, 61h
in al, dx
and al, 0fdh
or al, 1
out dx, al
mov al, 10110010b
out 43h, al
;1193180/100 Hz = 11931 = 2e9bh
mov al, 9bh ;LSB
out 42h, al
in al, 60h ;short delay
mov al, 2eh ;MSB
out 42h, al
;reset PIT one-shot counter (start counting)
in al, dx
and al, 0feh
out dx, al ;gate low
or al, 1
out dx, al ;gate high
;reset APIC timer (set counter to -1)
mov dword [apic+APIC_TMRINITCNT], ebx
;now wait until PIT counter reaches zero
@@: in al, dx
and al, 20h
jz @b
;stop APIC timer
mov dword [apic+APIC_LVT_TMR], APIC_DISABLE
;now do the math...
xor eax, eax
xor ebx, ebx
dec eax
;get current counter value
mov ebx, dword [apic+APIC_TMRCURRCNT]
;it is counted down from -1, make it positive
sub eax, ebx
inc eax
;we used divide value different than 1, so now we have to multiply the result by 16
shl eax, 4 ;*16
xor edx, edx
;moreover, PIT did not wait a whole sec, only a fraction, so multiply by that too
mov ebx, 100 ;*PITHz
mul ebx
;-----edx:eax now holds the CPU bus frequency-----
;now calculate timer counter value of your choice
;this means that tasks will be preempted 1000 times in a second. 100 is popular too.
mov ebx, 1000
xor edx, edx
div ebx
;again, we did not use divide value of 1
shr eax, 4 ;/16
;sanity check, min 16
cmp eax, 010h
jae @f
mov eax, 010h
;now eax holds appropriate number of ticks, use it as APIC timer counter initializer
@@: mov dword [apic+APIC_TMRINITCNT], eax
;finally re-enable timer in periodic mode
mov dword [apic+APIC_LVT_TMR], 32 or TMR_PERIODIC
;setting divide value register again not needed by the manuals
;although I have found buggy hardware that required it
mov dword [apic+APIC_TMRDIV], 03h
Example code in C
void apic_timer_init(uint32 quantum){
uint32 tmp, cpubusfreq;
//set up isrs
writegate(32,isr_dummytmr);
writegate(39,isr_spurious);
//initialize LAPIC to a well known state
(uint32*)(apic+APIC_DFR)=0xFFFFFFFF;
(uint32*)(apic+APIC_LDR)=((uint32*)(apic+APIC_LDR)&0x00FFFFFF)|1);
(uint32*)(apic+APIC_LVT_TMR)=APIC_DISABLE;
(uint32*)(apic+APIC_LVT_PERF)=APIC_NMI;
(uint32*)(apic+APIC_LVT_LINT0)=APIC_DISABLE;
(uint32*)(apic+APIC_LVT_LINT1)=APIC_DISABLE;
(uint32*)(apic+APIC_TASKPRIOR)=0;
//okay, now we can enable APIC
//global enable
cpuSetAPICBase(cpuGetAPICBase());
//software enable, map spurious interrupt to dummy isr
(uint32*)(apic+APIC_SPURIOUS)=39|APIV_SW_ENABLE;
//map APIC timer to an interrupt, and by that enable it in one-shot mode
(uint32*)(apic+APIC_LVT_TMR)=32;
//set up divide value to 16
(uint32*)(apic+APIC_TMRDIV)=0x03;
//initialize PIT Ch 2 in one-shot mode
//waiting 1 sec could slow down boot time considerably,
//so we'll wait 1/100 sec, and multiply the counted ticks
outb(0x61,inb(0x61)&0xFD)|1);
outb(0x43,0xB2);
//1193180/100 Hz = 11931 = 2e9bh
outb(0x42,0x9B); //LSB
in(0x60); //short delay
outb(0x42,0x2E); //MSB
//reset PIT one-shot counter (start counting)
(uint8)tmp=inb(0x61)&0xFE;
outb(0x61,(uint8)tmp); //gate low
outb(0x61,(uint8)tmp|1); //gate high
//reset APIC timer (set counter to -1)
(uint32*)(apic+APIC_TMRINITCNT)=0xFFFFFFFF;
//now wait until PIT counter reaches zero
while(!(inb(0x61)&0x20));
//stop APIC timer
(uint32*)(apic+APIC_LVT_TMR)=APIC_DISABLE;
//now do the math...
cpubusfreq=((0xFFFFFFFF-(uint32*)(apic+APIC_TMRINITCNT))+1)*16*100;
tmp=cpubusfreq/quantum/16;
//sanity check, now tmp holds appropriate number of ticks, use it as APIC timer counter initializer
(uint32*)(apic+APIC_TMRINITCNT)=(tmp<16?16:tmp);
//finally re-enable timer in periodic mode
(uint32*)(apic+APIC_LVT_TMR)=32|TMR_PERIODIC;
//setting divide value register again not needed by the manuals
//although I have found buggy hardware that required it
(uint32*)(apic+APIC_TMRDIV)=0x03;
}