mittos65/documentation/6 - apic.md
2022-01-20 23:53:23 +01:00

259 lines
12 KiB
Markdown

# Enabling the APIC
> - [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.
Each processor in a multiprocessor system has its own APIC which is controlled by reading and writing values to memory addresses starting at `0xFEE00000`.
## Enabling the APIC
The Local APIC is enabled by setting the Spurious Interrupt Vector register at `0xFEE000F0`.
This contains the IDT entry number for the spurious interupt handler (`0xFF`) and setting the bit `0x100` enables receiving interrupts.
## Testing the APIC
To test that the APIC works, an interrupt can be triggered by writing to the APIC Interrupt Command Registers.
```c
*(uint32_t *)P2V(APIC_BASE + 0x310)) = 0;
*(uint32_t *)P2V(APIC_BASE + 0x300)) = 0x50;
```
This should fire interrupt 0x50.
## Disabling Legacy PIC
Even if modern computers have a more powerful I/O APIC, there's still a PIC around, which will need to be disabled before we start receiving interrupts.
Since GRUB already initialized the PIC, there's no need to re-initialize it as is normally done when it is to be *used*. All I need to do is mask out every interrupt:
```c
outb(PIC1_ADDR | 1, 0xFF);
outb(PIC2_ADDR | 1, 0xFF);
```
### Minimal PIC initialization
In my research, I came up with the shortest way to initialize the PIC, and even if it's not necessary I don't want it to go to waste so I'm writing it down here.
An osdev tutorial would probably tell you to initialize a pic like this:
```c
#define PIC1_COMMAND 0x20
#define PIC1_DATA 0x21
#define PIC2_COMMAND 0xA0
#define PIC2_DATA 0xA1
outb(PIC1_COMMAND, 0x11);
outb(PIC1_DATA, 0x20);
outb(PIC1_DATA, 0x04);
outb(PIC1_DATA, 0x01);
outb(PIC2_COMMAND, 0x11); // ICW1
outb(PIC2_DATA, 0x28); // ICW2
outb(PIC2_DATA, 0x02); // IWC3
outb(PIC2_DATA, 0x01); // ICW4
```
Most tutorials also interlace the commands so every other one goes to the primary and every other to the secondary pic. I chose to split them here for clarity.
First of all, there's technically no PICN_COMMAND and PICN_DATA. Instead the lowestmost bit of the port number is part of the data, so it's really:
```c
outb(PIC1_ADDR, 0x11);
outb(PIC1_ADDR | 1, 0x20);
... etc
```
Now, each of the four lines per pic is called an Initialization Command Word, ICW1 through ICW4. The first one has two bits set. 0x10 saying to start (re-)initialization and 0x01 saying there will be a ICW4.
ICW2 tells the PIC what processor interrupts the first IRQ on the PIC maps to. Since we won't be using the PIC, we can just set this to whatever, like 0.
ICW3 tells the PIC how it should work together with its companion PICs. This is only read in if the PIC knows there are more than one PIC in the system. But by setting the second bit of ICW1, we can make it think it's all alone and therefore not expect an ICW3.
ICW 4 tells the PIC which processor architecture it should expect since different processors handle interrupts differently. 0x1 in ICW4 means an 8086 based architecture. Since we won't be using the PIC it doesn't really matter what it thinks. Therefore ICW 4 can be skipped, which it will be if the first bit in ICW1 is cleared.
So the final minimal PIC (re-)initialization becomes:
```c
outb(PIC1_ADDR, 0x12);
outb(PIC1_ADDR | 1, 0);
outb(PIC2_ADDR, 0x12);
outb(PIC2_ADDR | 1, 0);
```
I just thought that was neat.
## Enabling the I/O APIC
The I/O APIC handles external hardware interrupts and distribute them to the correct processor Local APICs.
The IOAPIC is controlled by a number of registers that are selected by writing to `IOREGSEL` at a base address that is found in the ACPI tables. Then the value can be read or written to `IOWIN` at `IOREGSEL + 0x10`
```c
static uint32_t ioapic_read(int reg) {
uint32_t volatile *ioregsel = P2V(ioapic.addr);
uint32_t volatile *iowin = P2V(ioapic.addr + 0x10);
*ioregsel = reg;
return *iowin;
}
static void ioapic_write(int reg, uint32_t value) {
uint32_t volatile *ioregsel = P2V(ioapic.addr);
uint32_t volatile *iowin = P2V(ioapic.addr + 0x10);
*ioregsel = reg;
*iowin = value;
}
```
In order to know where to redirect an interrupt the IOAPIC looks into its I/O Redirection Table. This can be set up by writing to some registers.
While IOWIN is 32 bits wide, each IOREDTBL entry is 64 bits, so two read or write operations are required to register `0x10 + 2*IRQ` and `0x11 + 2*IRQ`.
```
static uint64_t ioapic_read_redirection(int irq) {
union iored retval;
retval.l = ioapic_read(0x10 + 2*irq);
retval.h = ioapic_read(0x11 + 2*irq);
return retval.val;
}
static void ioapic_write_redirection(int irq, uint64_t val) {
union iored value;
value.val = val;
ioapic_write(0x10 + 2*irq, value.l);
ioapic_write(0x11 + 2*irq, value.h);
}
```
Here a `union` structure is used in order to access the same data either as a 64 bit integer (for passing to and from the functions), as two 32 bit values (for reading from/writing to the registers) or in the structured form below:
```c
union iored{
struct {
uint8_t vector; // Interrupt that's triggered on the APIC
uint8_t flags;
uint8_t mask;
uint32_t reserved;
uint8_t target; // APIC id to redirect to
}__attribute__((packed));
struct {
uint32_t l;
uint32_t h;
};
uint64_t val;
};
```
There's a total of 24 IOREDTLB entries, and for the initial setup I chose to map them all to LAPIC 0 starting at interrupt 0x20. I also masked all of them:
```c
for(int i = 0; i < 24; i++) {
iored.val = ioapic_read_redirection(i);
iored.vector = IRQ_BASE+i;
iored.flags = 0x0;
iored.mask |= 1;
iored.target = 0x0;
ioapic_write_redirection(i, iored.val);
}
```
Then I unmasked the keyboard interrupt at IRQ2, remembering to check it agains the redirections saved from the ACPI tables.
```c
iored.val = ioapic_read_redirection(irq_redirects[1]);
iored.mask &= ~0x1;
ioapic_write_redirection(irq_redirects[1], iored.val);
```
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`