Documentation

This commit is contained in:
Thomas Lovén 2022-01-20 23:53:23 +01:00
parent fb87e52a94
commit ef20d81bd8

View File

@ -1,9 +1,7 @@
# Enabling the APIC
> - [APIC reference] TODO
> - [PIC data sheet] TODO
> - [IOAPIC data sheet] TODO
> - [PIC data sheet](http://pdos.csail.mit.edu/6.828/2005/readings/hardware/8259A.pdf)
> - [IOAPIC data sheet](http://web.archive.org/web/20161130153145/http://download.intel.com/design/chipsets/datashts/29056601.pdf)
The Advanced Programmable Interrupt Controller (APIC) is not to be confused with (ACPI).
It's quite literally an upgrade to the legacy PIC and handles hardware interrupts and IRQs.
@ -164,3 +162,98 @@ After this I could trigger a keyboard interrupt!
Only in the emulator, though and not on my macbook. \
My guess is that the macbook does not emulate a PS/2 keyboard but only exposes the keyboard on USB.
## APIC Timer
After some experimenting with the PIT and the HPET timers, I decided to use the built-in APIC timer for timed interrupts. Mostly because it turned out to be pretty simple.
The APIC timer can be set up in periodic mode to fire an interrupt every set number of processor bus clock cycles divided by a divisor. To do this, write the divisor configuration and wanted interval to the Divide Configuration Register and Initial Count Register respectively.
Then set the Timer Mode bit to Periodic and write an interrupt vector to the Local Vector Table entry for the APIC Timer:
```c
*(uint32_t *)P2V(APIC_BASE + 0x3E0)) = 0xB;
*(uint32_t *)P2V(APIC_BASE + 0x380)) = microseconds * calibration_factor;
*(uint32_t *)P2V(APIC_BASE + 0x320)) = 1<<17 | timer_interrupt;
```
Since the cpu bus clock varies from machine to machine, the `calibration_factor` needs to be calculated or measured. A simple measurement method is as follows:
- Set the APIC timer counter to a known number
- Wait for a known interval
- Check how much the APIC timer counter has changed during this time
- Divide that difference by the known interval
And there we have the calibration factor.
The APIC timer is monotonically decreasing and triggers when it reaches zero, so the initial number is preferably very large, like `0xFFFFFFFF`.
Setting the divisor, masking its interrupt and setting the initual count will start it counting down immediately:
```c
*(uint32_t *)P2V(APIC_BASE + 0x3E0)) = 0xB;
*(uint32_t *)P2V(APIC_BASE + 0x380)) = 0xFFFFFFFF;
*(uint32_t *)P2V(APIC_BASE + 0x320)) = 1<<16;
```
For the known interval, I'll use the legacy Programmable Interval Timer - PIT.
The PIT is still present in pretty much every x86 system and - for historical reasons - always has a known frequency: 1.193182 MHz or 1193182 ticks per microsecond. Furthermore, one if its three output channels can be monitored by a port read, so we can set it up and then bussy wait until it triggers.
The PIT is a very old piece of hardware and for compatibility with IBM PC/AT computers it's connected to the PC speaker through the PS/2 keyboard controller. Let's say I do remember PC speakers (some motherboards still have a buzzer, actually), but I think I only ever saw an AT computer once.
Anyway. The procedure to wait a set number of milliseconds is as follows:
```c
// Clear bit 0 and one of the pc speaker port so we don't get any accidental sound
outb(0x61, inb(0x61) &= ~0x3);
// Set up the PIT for a single mode trigger (0x80) on channel 2 (0x30)
outb(0x43, 0x80 | 0x30);
// Write the wanted numbers of ticks to channel 2 of the PIT
// Low byte first
// Note that this needs to be 64 bits to avoid overflow
// If the calculation is done in milliseconds it fits in 32 bits, though
uint64_t ticks = 1193182 * microseconds/1000000;
outb(0x42, ticks & 0xFF);
outb(0x42, (ticks >> 8) & 0xFF);
// NB: For optimal accuracy, this is the best time to start the APIC timer
// Enable bit 0 on the pc speaker to start counting down
outb(0x61, inb(0x61) | 0x1);
// Wait until bit 6 on the pc speaker is set
while(!(inb(0x61) & 0x20));
```
Then finally, the remaining count is read from the APIC timer and the calibration factor is calculated. Note that this is not read from the same register as the initial count is written to:
```c
uint32_t remaining = *(uint32_t *)P2V(APIC_BASE + 0x390));
```
## A note about the Red Zone
While experimenting with counters and interrupts I ran into a problem. Sometimes the processor would page fault. The faulting address was always something that I would have assumed was stored in a function local variable, and after an hour or two of debugging I realized it happened after returning from an interrupt, but *only* if the interrupt happened when code was being executed in a function which didn't call any other functions.
That made me realize exactly what was going on.
The gcc compiler - at least the way it's been set up for Mittos - handles function calls according to something called the System V ABI. This specifies how arguments are passed to functions, how return values are returned and what registers must be preserved through function calls. It also regulates how the stack can be used.
Every function is responsible for allocating a bit of space on the stack called a Stack Frame. This frame starts at the stack pointer, so the function needs to adjust the stack pointer for the next function it calls. The Stack Frame is where the function is supposed to keep all of its local variables and temporary data.
There is, however, one exception to this - and that is a function that calls no other functions. It doesn't need to adjust the stack pointer, but is allowed to keep its local variables in an area at the top of the stack dubbed the Red Zone. From the setup from the calling function, the stack pointer will point to the bottom of the Red Zone, which becomes a problem if an interrupt happens.
On an interrupt, the processor immediately pushes some data to the stack - i.e. the instruction pointer, error numbers etc. So that would clobber the Red Zone. If the interrupt occurs when the processor is running user space code it starts by loading a known stack pointer - I'll get to that soon enough - so that's not a problem. But the kernel code can't be allowed to have a Red Zone.
Luckily there's a compiler flag for that.
So this was just a long winded way of saying:
Remember sure to add `-mno-red-zone` to `CFLAGS` when compiling kernel code.
## Relevant files
- `src/kernel/include/cpu.h`
- `src/kernel/cpu/cpu.c`
- `src/kernel/cpu/apic.c`
- `src/kernel/cpu/ioapic.c`
- `src/kernel/cpu/timer.c`