Hier ist ist Beispiel für den lokalen APIC Timer und dem Timestamp Counter (TSC):
Der lokale APIC bietet den genausten Timer des x86, da er von der Prozessor Busgeschwindigkeit abhängt. Daher muss er vorher kalibriert werden, um die Busgeschwindigkeit herauszubekommen.
Der TSC gibt die Anzahl der vergangenen Taktzyklen zurück, seitdem man den Prozessor eingeschaltet hat. So kann man ihn auch für Delays benutzen.
Der folgende Code ist in C, ich hoffe mal, dass du ihn selbst in Assembler umsetzen kannst.
#define LAPIC_REG_ID 0x020
#define LAPIC_REG_VERSION 0x030
#define LAPIC_REG_TPRI 0x080
#define LAPIC_REG_EOI 0x0b0
#define LAPIC_REG_SIVR 0x0f0
#define LAPIC_REG_ESR 0x280
#define LAPIC_REG_ICR1 0x300
#define LAPIC_REG_ICR2 0x310
#define LAPIC_REG_LVTT 0x320
#define LAPIC_REG_LINT0 0x350
#define LAPIC_REG_LVT3 0x370
#define LAPIC_REG_ICRT 0x380
#define LAPIC_REG_CCRT 0x390
#define LAPIC_REG_TDCR 0x3e0
// der lokale APIC befindet sich _normalerweise_ an 0xFEE00000
// das kann unterschiedlich sein, steht in einem MSR und wird vom BIOS programmiert
// wenn man den wert auslesen will, müsste man die multiprozessor informationen lesen, den code dafür poste ich nicht, da er viel zu lang wäre
// der apic ist aber in allen mir bekannten system an 0xFEE00000 also kann man sich normalerweise darauf verlassen
// bochs emuliert den apic code nur, wenn es dafür kompiliert wurde, qemu garnicht.
// für deine zwecke ist rdtscll() warscheinlich besser geeignet.
unsigned long *g_mp_lapic = (unsigned long *)0xFEE00000;
unsigned long long g_cpu_speed;
unsigned long long g_cpu_speed_temp;
unsigned long g_lapic_tempcount;
volatile unsigned long g_lapic_divisor;
// isr_lapic_timer() macht garnichts (nur IRET), isr_lapic_calibrate() ruft ruft lapic_calibrate_irq() auf
extern void isr_lapic_timer();
extern void isr_lapic_calibrate();
void lapic_calibrate() {
unsigned char byte;
g_lapic_tempcount = 0;
g_lapic_divisor = 0;
g_cpu_speed = 0;
g_cpu_speed_temp = 0;
// setup real time clock
// unmask IRQ 8
outByte(0x21, 0xFB);
outByte(0xA1, 0xFE);
// set interrupt gate
// interrupt gate an 0x28/0x80 setzen, mit pl 0
interrupt_setentry(0x28, isr_lapic_calibrate, 0);
interrupt_setentry(0x80, isr_lapic_timer, 0);
// set real time clock frequency
outByte(0x70, 0xA);
byte = inByte(0x71) & 0xF0;
outByte(0x70, 0xA);
outByte(0x71, byte | 0xF);
// setup apic timer
lapic_write(LAPIC_REG_LVTT, 0x80);
lapic_write(LAPIC_REG_TDCR, 0xA);
lapic_write(LAPIC_REG_ICRT, 0xFFFFFFFF);
// enable interrupt
outByte(0x70, 0xB);
byte = inByte(0x71);
outByte(0x70, 0xB);
outByte(0x71, byte | 0x40);
outByte(0x70, 0xC);
inByte(0x71);
while(g_lapic_divisor == 0) {
asm volatile ("nop");
}
// disable apic timer
lapic_write(LAPIC_REG_ICRT, 0);
lapic_write(LAPIC_REG_TDCR, 0xB);
// disable real time clock interrupt
outByte(0x70, 0xB);
outByte(0x71, byte & 0xBF);
debug_info("bus speed: %u hz\n", g_lapic_divisor);
// meine debug_info funktion kann keine long longs verarbeiten
debug_info("cpu speed: %u hz\n", (unsigned long)g_cpu_speed);
}
unsigned long long rdtscll() {
unsigned long long result;
asm volatile ("rdtsc" : "=A" (result));
return result;
}
void lapic_calibrate_irq() {
// read status register c
outByte(0x70, 0xC);
inByte(0x71);
unsigned long bus = lapic_read(LAPIC_REG_CCRT);
unsigned long long cpu = rdtscll();
if(g_lapic_tempcount == 0) {
g_lapic_tempcount = bus;
}else if(g_lapic_divisor == 0){
g_lapic_divisor = (g_lapic_tempcount - bus) * (128 / 2);
}
if(g_cpu_speed_temp == 0) {
g_cpu_speed_temp = cpu;
}else if(g_cpu_speed == 0) {
// wir haben nur eine halbe sekunde gewartet, daher * 2
// wenn du nicht weißt, wie du long longs multiplizieren und subtrahieren kannst in assembler, kann ich dir dafür einen codeschnippsel geben.
g_cpu_speed = (cpu - g_cpu_speed_temp) * 2;
}
}
// usecs microsekunden warten (1000 microsekunden = 1 millisekunde)
void lapic_delay(unsigned long usecs) {
unsigned long ticks = g_lapic_divisor / 1000000;
lapic_write(LAPIC_REG_ICRT, ticks * usecs);
while(1) {
if(lapic_read(LAPIC_REG_CCRT) == 0)
break;
asm volatile ("nop");
}
}
void lapic_write(unsigned short reg, unsigned long value) {
*(unsigned long*)((unsigned long)g_mp_lapic + reg) = value;
}
unsigned long lapic_read(unsigned short reg) {
return *(unsigned long*)((unsigned long)g_mp_lapic + reg);
}
Nach einem Aufruf von lapic_calibrate() steht in g_lapic_divisor der Bus Speed in HZ und in g_cpu_speed die Taskfrequenz in HZ.
Mit RDTSC kannst du dann solange warten, bis eine bestimmte Anzahl an Taken vergangen ist. Du musst warscheinlich viele Funktion an dein OS anpassen.^^
Der APIC hat den Vorteil, dass er auch Interrupts generieren kann. Den APIC kann man aber nur im PM benutzen, da er sich an einer Addresse befindet, die man im RM nicht ansprechen kann. Wenn du ihn nicht verwenden willst, kannst du einfach alle LAPIC spezifischen Codeteile löschen, er wird nicht zur Kalibrierung benötigt.