For this implementation task you will set up the programmable interrupt controller (PIC) to deliver interrupts to your kernel whenever a keyboard key is pressed or released. Make sure your kernel is correctly handling standard exceptions before starting this implementation task; if your kernel can’t deal with an internally-generated interrupt then it won’t be able to respond to keyboard interrupts.
The PIC is responsible for notifying a processor when an event occurs in some external device. The way it does this is by generating an interrupt request (IRQ), which causes the processor to run an interrupt handaler. The PIC supports 16 standard IRQs, numbered from zero to 15. The keyboard is associated with IRQ 1. By default, IRQs are all masked, meaning that hardware requests for interrupts are ignored; we need to unmask IRQ 1 to receive interrupts when a key is pressed or released.
But before we do that, we need to deal with an oversight in IBM’s design for the PIC. By default, IRQs 0–7 raise interrupts 8–15, and IRQs 8–15 raise interrupts 112–119. That means key presses would cause interrupt 9, which is already used for the “coprocessor segment overrun” exception. The number for an interrupt identifies its source, so we want IRQs to use different interrupt numbers. We can do this by remapping the PIC to a different interrupt range. There are a few steps to set this up, so please be careful not to miss any.
The processor communicates with legacy hardware devices like the PIC or the keyboard using port input and output.
The helper functions below wrap inline assembly that sends and receives individual bytes of data on CPU ports.
You can safely drop all of this code into a file port.h for use in the later parts of this implementation.
#pragma once
#include <stdint.h>
// Standard port input/output operations
// Source: https://wiki.osdev.org/Inline_Assembly/Examples#I.2FO_access
static inline void outb(uint16_t port, uint8_t val) {
__asm__("outb %0, %1" : : "a"(val), "Nd"(port));
}
static inline uint8_t inb(uint16_t port) {
uint8_t ret;
__asm__("inb %1, %0" : "=a"(ret) : "Nd"(port));
return ret;
}
static inline void io_wait() {
outb(0x80, 0);
}
Now that we have port I/O helper functions, we can remap the PIC.
To make sure this works smoothly, you can directly use the code below for the file pic.h, which declares three functions to interact with the PIC.
#pragma once
#include <stdint.h>
// Ports to talk to PICs
#define PIC1 0x20
#define PIC2 0xA0
#define PIC1_COMMAND PIC1
#define PIC1_DATA (PIC1+1)
#define PIC2_COMMAND PIC2
#define PIC2_DATA (PIC2+1)
// Message sent to a PIC when finished with an IRQ
#define PIC_EOI 0x20
// Interrupt numbers for IRQs
#define IRQ0_INTERRUPT 0x20
#define IRQ1_INTERRUPT 0x21
#define IRQ2_INTERRUPT 0x22
#define IRQ3_INTERRUPT 0x23
#define IRQ4_INTERRUPT 0x24
#define IRQ5_INTERRUPT 0x25
#define IRQ6_INTERRUPT 0x26
#define IRQ7_INTERRUPT 0x27
#define IRQ8_INTERRUPT 0x28
#define IRQ9_INTERRUPT 0x29
#define IRQ10_INTERRUPT 0x2a
#define IRQ11_INTERRUPT 0x2b
#define IRQ12_INTERRUPT 0x2c
#define IRQ13_INTERRUPT 0x2d
#define IRQ14_INTERRUPT 0x2e
#define IRQ15_INTERRUPT 0x2f
/// Initialize the PICs to pass IRQs starting at 0x20
void pic_init();
/// Mask an IRQ by number (0-15)
void pic_mask_irq(uint8_t num);
/// Unmask an IRQ by number (0-15)
void pic_unmask_irq(uint8_t num);
Now that we have pic.h, use the code below for pic.c to implement the declared functions.
The procedure for remapping the PIC and masking/unmasking IRQs comes straight out of an IBM manual, so it can be a bit cryptic.
#include "pic.h"
#include <stdint.h>
#include <stddef.h>
#include "port.h"
#define ICW1_ICW4 0x01
#define ICW1_SINGLE 0x02
#define ICW1_INTERVAL4 0x04
#define ICW1_LEVEL 0x08
#define ICW1_INIT 0x10
#define ICW4_8086 0x01
#define ICW4_AUTO 0x02
#define ICW4_BUF_PIC2 0x08
#define ICW4_BUF_PIC1 0x0C
#define ICW4_SFNM 0x10
/**
* Initialize the PICs to pass IRQs starting at 0x20
* Based on code from https://wiki.osdev.org/PIC
*/
void pic_init() {
// Start initializing PICs
outb(PIC1_COMMAND, ICW1_INIT | ICW1_ICW4);
io_wait();
outb(PIC2_COMMAND, ICW1_INIT | ICW1_ICW4);
io_wait();
// Set offset for primary PIC
outb(PIC1_DATA, IRQ0_INTERRUPT);
io_wait();
// Set offset for secondary PIC
outb(PIC2_DATA, IRQ8_INTERRUPT);
io_wait();
// Tell primary PIC there is a secondary at IRQ2
outb(PIC1_DATA, 0x04);
io_wait();
// Tell secondary PIC its identity
outb(PIC2_DATA, 0x02);
io_wait();
// Finish initialization
outb(PIC1_DATA, ICW4_8086);
io_wait();
outb(PIC2_DATA, ICW4_8086);
io_wait();
// Mask all IRQs by default
outb(PIC1_DATA, 0xFF);
outb(PIC2_DATA, 0xFF);
// Enable interrupts
__asm__("sti");
}
/// Mask an IRQ by number (0-15)
void pic_mask_irq(uint8_t num) {
// Which PIC do we need to talk to?
if (num < 8) {
// Get the current mask
uint8_t mask = inb(PIC1_DATA);
// Update the mask
mask |= 1<<num;
outb(PIC1_DATA, mask);
} else if (num < 16) {
// Get the current mask
uint8_t mask = inb(PIC2_DATA);
// Update the mask
mask |= 1<<(num - 8);
outb(PIC2_DATA, mask);
}
}
/// Unmask an IRQ by number (0-15)
void pic_unmask_irq(uint8_t num) {
// Which PIC do we need to talk to?
if (num < 8) {
// Get the current mask
uint8_t mask = inb(PIC1_DATA);
// Update the mask
mask &= ~(1<<num);
outb(PIC1_DATA, mask);
} else if (num < 16) {
// Get the current mask
uint8_t mask = inb(PIC2_DATA);
// Update the mask
mask &= ~(1<<(num - 8));
outb(PIC2_DATA, mask);
}
}
Once you have the code above added to your project, you’ll need to call the pic_init() function from _start.
After that, you’ll need to set an interrupt handler for IRQ1_INTERRUPT (defined in pic.h) that will run when a key is pressed.
And finally, call pic_unmask_irq(1) to allow interrupts for key press events.
The keyboard interrupt handler will need to read the key code, then tell the PIC it is finished handling the interrupt so the next interrupt can be delievered.
To read the key code, call inb(0x60);
the returned value is not a character, but a value from keyboard scan code set 1.
You’ll eventually need to convert scan codes to characters, but for now you could just print the value in hexadecimal for debugging purposes.
To acknowledge the interrupt from the PIC, call outb(PIC1_COMMAND, PIC_EOI).
This sends the end-of-interrupt message to the first PIC, which is responsible for delivering IRQs 0–7.
If you miss this step, you will only get an interrupt for the first key press!
Once you can reliably receive interrupts from the keyboard you can start working on translating those interrupts to character codes.
Our next implementation task will be to write a function called kgetc to read keyboard input on demand, but for now you can just print characters as soon as they are pressed.