mittos65/documentation/6 - apic.md
2022-01-17 20:43:21 +01:00

166 lines
6.2 KiB
Markdown

# Enabling the APIC
> - [APIC reference] TODO
> - [PIC data sheet] TODO
> - [IOAPIC data sheet] TODO
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.