StuBS
APIC-Timer

Die APIC-Architektur beinhaltet auf jedem der CPU-lokalen APICs einen hochauflösenden Timer, der zum Generieren von Interrupts genutzt werden kann: Der APIC-Timer

Funktionsweise des APIC-Timers

Ein Timer wird durch einen Taktgeber umgesetzt, der rhythmisch Stromstöße ausgibt. Bspw. kann dafür ein Quarz verwendet werden, der bei Anlegen einer Spannung vibriert. Das Signal vom Quarz wird durch eine Beschaltung in ein glattes Rechteck-Signal umgewandelt und verstärkt, damit die logischen Gatter damit umgehen können.

Aufbau des Timers im Local APIC

Das Signal wird dann durch einen Prescaler (oder Divider) geteilt, sodass z.B. aus einer Frequenz f wird die Freqzenz f/2 erzeugt wird. Das Teilen in Zweierpotenzen ist dabei besonders einfach durch hintereinander geschaltete T-Flip-Flips zu realisieren. Der APIC-Timer bietet einstellbare Prescaler-Stufen, die das Signal jeweils um Zweierpotenzen vorteilen.

Das geteilte Signal wird dann verwendet, um einen 32-Bit Zähler (CCNT) periodisch herunter zu zählen. Dieser Zähler wird mit dem Wert des Initial Count Registers (ICNT) vorinitialisiert und dann mit jedem Takt (nach der Vorteilung) um eins dekrementiert. Der CCNT gibt somit den aktuellen Stand des Timers an. Mit jedem Takt wird der Wert des Zählers gegen Null verglichen; ist er Null, wird ein Timer-Interrupt an den Prozessor verschickt. Außerdem wird das Current Count Register erneut auf den Wert des Initial Count Registers gesetzt (wenn der Timer im periodischen Modus läuft). Der Wert des ICNT ist frei einstellbar.

Frequenz und Ticks

Der Timer soll genutzt werden, um periodische Interrupts zu schicken, z.B. alle 5 ms. Damit der Timer in genau diesem Zeitintervall Interrupts sendet, ist es nötig, den Prescaler und das ICNT passend einzustellen. Das wiederum hängt von der Frequenz des Timers ab.

Beim LAPIC-Timer ist die Frequenz jedoch nicht fest spezifiziert, sondern hardwareabhängig. Sie muss daher durch Kalibrierung mit einer anderen Zeitquelle (bei der die Frequenz bekannt ist) bestimmt werden. Dafür verwenden wir den PIT (Programmable Interval Timer); der Code für die Verwendung des PITs ist bereits in der Vorgabe enthalten. Mithilfe dessen muss die Frequenz vom APIC-Timer in LAPIC::Timer::ticks() bestimmt werden. Diese Funktion liefert die Clock Ticks pro Millisekunde.

Mithilfe der Frequenz kann man die Anzahl der benötigten Ticks pro Interrupt (ohne Divider) folgendermaßen berechnen:

\[ \mathrm{ticks} = \frac{\mu s \times \text{LAPIC::Timer::ticks()}}{ 1000 } \]

Dabei ist zu beachten, dass CCNT und ICNT 32-Bit-Register sind und (logischerweise) nicht überlaufen dürfen.

Bei der Multiplikation von \(\mu\) mit dem von LAPIC::Timer::ticks() zurückgegeben Wert kann z.B. bereits der 32-Bit Wertebereich überlaufen werden. In diesem Fall sollte die Operation darum im 64-Bit Werteraum durchgeführt werden (uint64_t).

Anschließend muss aus den Ticks noch der korrekte Divider und ICNT-Wert berechnet werden. Die resultierenden Werte sollen den Timer so genau wie möglich einstellen.

Programmierung des Timers

Vier 32-Bit Register (memory-mapped) sind für die Programmierung des APIC-Timers verantwortlich:

AddresseRegistername
0xfee00320LVT Timer Register (aka Timer Control Register)
0xfee00380Initial Count Register (ICNT)
0xfee00390Current Count Register (CCNT)
0xfee003e0Divide Configuration Register (TimeDIV)

Die beiden Count-Register (ICNT und CCNT) dienen dazu, den Startwert des Timers zu setzen, beziehungsweise den aktuellen Wert auszulesen. Zusätzlich kann mit einem Initial Count-Wert von 0 der Timer komplett gestoppt werden. Ein anschließendes Schreiben des ICNT mit einem Wert ungleich 0 startet den Timer.

Das Verhalten des Timers kann über das Kontrollregister und den Divisor beeinflusst werden. Der Aufbau des Kontrollregisters ist:

Bit(s)WertBedeutung
31-19(reserviert)
18-17Betriebsmodus
0 one-shot (stoppt bei 0)
1 periodic (startet wieder mit ICR-Wert)
2 TSC-Deadline (absolute Zeit über ein spezielles Register IA32_TSC_DEADLINE vergleichen, ICR und CCR ignorieren)
3 reserviert
16Interruptmaske
0 Interrupts werden zugestellt
1 Interrupts sind deaktiviert
15-13(reserviert)
12Interrupt-Status (read-only)
0 idle
1 Interrupt pending
11-8(reserviert)
7-0Vektornummer
des auszulösenden Interrupts

Die Zuordnung des Divisors auf entsprechende Registerwerte ist dem Intel-Manual zu entnehmen, aber auch bereits durch die Hilfsfunktion LAPIC::Timer::getClockDiv() als Teil der Vorgabe implementiert.