Bare Metal Rust 3: Configure your PIC to handle interrupts correctly
Want to build your own kernel in Rust? See Bare Metal Rust to get started.
We’re almost ready to write a keyboard driver in Rust! But first, we need to deal with two obstacles: setting up the PIC, and handling interrupts without crashing. This is one of the most frustrating steps, as Julia Evans explains in her hilarious and very helpful post After 5 days, my OS doesn’t crash when I press a key:
- Turn interrupts on (
sti
).- The OS AGAIN crashes every time i press a key. Read “I Can’t Get Interrupts Working” again. This is called “I’m receiving EXC9 instead of IRQ1 when striking a key?!” Feel on top of this.
- Remap the PIC so that interrupt
i
gets mapped toi + 32
, because of an Intel design bug. This basically looks like just typing in a bunch of random numbers, but it works.- 12. THE OS IS STILL CRASHING WHEN I PRESS A KEY. This continues for 2 days.
We’re going to follow Julia Evans’ roadmap. (She saved me several days of suffering.) And once we’re past these next few obstacles, things will get easier. Let’s talk to the PIC first.
The 8295/8295A Programmable Interrupt Controller
We’re going to with the retro approach here, and handle interrupts using the 8295 PIC. You can read all about it on the OSDev wiki, as usual. The PIC works fine in 64-bit mode, but someday, if we want to support multiple processors and I/O, we’ll eventually need to support the newer APIC and IOAPIC. But for now, let’s keep it simple.
Technically, the x86 architecture has two PIC chips, usually known as PIC1 and PIC2. PIC1 handles external interrupts 0–7, and PIC2 handles 8–15. PIC2 is actually chained into interrupt 2 on PIC1, which means that we’ll frequently need to talk to them as a pair.
Unfortunately, the modern x86 architecture reserves CPU interrupts 0-31 for processor exceptions. This means that when we press a key, the CPU will think it just received the “EXC9” mentioned by Julia Evans, which the Intel manual tells me is “Coprocessor-Segment-Overrun Exception.” So we need to tell our PIC that, no, McGyver and Miami Vice are no longer cutting-edge television, that there’s this new-fangled thing called 386 Protected Mode, and that it needs to start mapping interrupts at offset 32.
Making Port
more paranoid
In an earlier post, we talked about “ports”, and we defined a
cpuio::Port
that we could use to talk to common hardware peripherals.
This had an odd interface, where new
was marked as unsafe
(because
some ports are dangerous), but where read
and write
were marked as
safe. The theory is that whoever called new
could control who had access
to read
and write
.
But dschatzberg on /r/rust pointed out that this wasn’t
always a helpful way to represent the hardware, because it placed too big a
burden on the code that called new
. So let’s create a second
UnsafePort
, which works just like Port
except all the methods are
marked as unsafe
. Here’s the new code from the cpuio crate:
pub struct UnsafePort<T: InOut> {
port: u16,
phantom: PhantomData<T>,
}
impl<T: InOut> UnsafePort<T> {
pub const unsafe fn new(port: u16) -> UnsafePort<T> {
UnsafePort { port: port, phantom: PhantomData }
}
pub unsafe fn read(&mut self) -> T {
T::port_in(self.port)
}
pub unsafe fn write(&mut self, value: T) {
T::port_out(self.port, value);
}
}
We can use this to access any hardware which is intrinsically “unsafe,” in
the Rust sense of the term, where merely calling read
and write
might
cause undefined behavior and violate Rust’s safety guarantees.
This is appropriate for working with the PIC, because a misconfigured PIC
can cause interrupts to corrupt memory.
Talking to a single PIC
I learned everything that follows from the OSDev wiki’s PIC page. So please don’t assume I’m any kind of expert on this stuff—I’m going to make mistakes, and I spent a bunch of time watching QEMU reboot. Your corrections and improvements would definitely be appreciated!
Each PIC has two 8-bit ports: one for sending commands, and one for sending
and receiving data. Additionally, each PIC has an offset
, which we’ll
use to control the mapping from PIC interrupts to CPU interrupts. We can
represent this as:
extern crate cpuio;
use cpuio::UnsafePort;
struct Pic {
offset: u8,
command: UnsafePort<u8>,
data: UnsafePort<u8>,
}
Each PIC handles a total of 8 interrupts, starting at the CPU interrupt
offset
and counting from there. So let’s define some helper methods:
impl Pic {
fn handles_interrupt(&self, interupt_id: u8) -> bool {
self.offset <= interupt_id && interupt_id < self.offset + 8
}
unsafe fn end_of_interrupt(&mut self) {
self.command.write(CMD_END_OF_INTERRUPT);
}
}
We’ll use the end_of_interrupt
command to tell the PIC when we’re done
processing an interupt.
Representing a chained pair of PICs
But our PIC1 and PIC2 chips are chained together, and they work as a pair. So let’s create a data structure to represent them:
pub struct ChainedPics {
pics: [Pic; 2],
}
Let’s make it easy to set up PIC1 and PIC2, using the standard port addresses for each, and custom interrupt offsets:
impl ChainedPics {
pub const unsafe fn new(offset1: u8, offset2: u8) -> ChainedPics {
ChainedPics {
pics: [
Pic {
offset: offset1,
command: UnsafePort::new(0x20),
data: UnsafePort::new(0x21),
},
Pic {
offset: offset2,
command: UnsafePort::new(0xA0),
data: UnsafePort::new(0xA1),
},
]
}
}
}
Now we’re ready to actually configure our PIC.
How would we do this in C? The OSDev wiki knows!
We can find an example PIC_remap
function on the OSDev wiki.
Looking at their code, we can see it has the following structure:
// Save interrupt masks.
a1 = inb(PIC1_DATA);
a2 = inb(PIC2_DATA);
// Send command: Begin 3-byte initialization sequence.
// Send data 1: Set interrupt offset.
// Send data 2: Configure chaining.
// Send data 3: Set mode.
// Restore interrupt masks.
outb(PIC1_DATA, a1);
outb(PIC2_DATA, a2);
The initialization of the two PIC chips is interleaved, apparently because it takes the chips a while to process the messages. If we send messages too quickly, the PICs get confused.
First, we begin the initialization. We need to write a byte to each port,
and call a function io_wait
to slow us down even further (more on that
later):
// Send command: Begin 3-byte initialization sequence.
outb(PIC1_COMMAND, ICW1_INIT+ICW1_ICW4);
io_wait();
outb(PIC2_COMMAND, ICW1_INIT+ICW1_ICW4);
io_wait();
Now our PICs each expect us to send a 3-byte initialization sequence. The first byte is the offset:
// Send data 1: Set interrupt offset.
outb(PIC1_DATA, offset1);
io_wait();
outb(PIC2_DATA, offset2);
io_wait();
We follow this with some instructions on how the two PICs are chained together:
// Send data 2: Configure chaining.
outb(PIC1_DATA, 4);
io_wait();
outb(PIC2_DATA, 2);
io_wait();
And finally we specify what mode we want the PICs to run in:
// Send data 3: Set mode.
outb(PIC1_DATA, ICW4_8086);
io_wait();
outb(PIC2_DATA, ICW4_8086);
io_wait();
Translating initialization to Rust
Now, in Rust, we could just use outb
directly, and declare values
like PIC1_DATA
and ICW4_8086
as constants. This would allow us to
translate the C code literally, and it would be perfectly fine for a chip
as simple as the PIC.
But later on, we’ll be dealing with more complicated hardware, which has lots of I/O ports, and complicated messages. So let’s try to write our Rust at a higher level of abstraction. This is overkill now, but it might make our more complicated drivers easier to understand later on.
First, a skeleton:
impl ChainedPics {
// ...
pub unsafe fn initialize(&mut self) {
let mut wait_port: cpuio::Port<u8> = cpuio::Port::new(0x80);
let mut wait = || { wait_port.write(0) };
let saved_mask1 = self.pics[0].data.read();
let saved_mask2 = self.pics[1].data.read();
// Send initialization command and data bytes.
// ...
self.pics[0].data.write(saved_mask1);
self.pics[1].data.write(saved_mask2);
}
}
But what’s up with wait_port
and wait
? Well, do you remember io_wait
in the C code above? We need to wait for a while to slow down our
initialization sequence. Normally, we’d definite a function like udelay
and call udelay(2)
to pause for approximately two microseconds. But this
won’t work, because don’t have a clock yet! Most of the available clocks
require us to have set up our interrupts, and setting up interrupts
requires correctly configured PICs.
There’s a traditional way to break this deadlock, which is demonstrated by kexec’s PIC driver and explained by the OSDev wiki:
movb $0x11, %al
outb %al, $0x20
outb %al, $0x80
Recognize those first two lines? We just saw them above in C, where they
appeared as outb(PIC1_COMMAND, ICW1_INIT+ICW1_ICW4)
. The third line
writes the same data to port 0x80, which is apparently harmless, but which
takes just long enough for the PICs to be ready again. If it’s good enough
for kexec, we can probably assume it works on most real hardware.
In Rust, we can declare a Port
for 0x80 as follows:
let mut wait_port: cpuio::Port<u8> = cpuio::Port::new(0x80);
…and declare a local wait
function as a closure (a function which can
access local variables) like this:
let mut wait = || { wait_port.write(0) };
Now we can translate the initialization sequence into Rust as follows:
// Send command: Begin 3-byte initialization sequence.
self.pics[0].command.write(CMD_INIT);
wait();
self.pics[1].command.write(CMD_INIT);
wait();
// Send data 1: Set interrupt offset.
self.pics[0].data.write(self.pics[0].offset);
wait();
self.pics[1].data.write(self.pics[1].offset);
wait();
The remaining two data bytes are left as an exercise for the reader.
The last two pieces of our API
First, we need some way to identity which interrupts are managed by our
ChainedPics
. This is easy:
impl ChainedPics {
// ...
pub fn handles_interrupt(&self, interrupt_id: u8) -> bool {
self.pics.iter().any(|p| p.handles_interrupt(interrupt_id))
}
}
And finally, we need a way to tell the PIC that we’ve handled an interrupt. This is slighly tricky, because the interrupts from PIC2 are chained through PIC1:
impl ChainedPics {
// ...
pub unsafe fn notify_end_of_interrupt(&mut self, interrupt_id: u8) {
if self.handles_interrupt(interrupt_id) {
if self.pics[1].handles_interrupt(interrupt_id) {
self.pics[1].end_of_interrupt();
}
self.pics[0].end_of_interrupt();
}
}
}
Using our PICs
In our kernel, we can declare our PICs as follows, using
spin::Mutex
as recommended by Philipp Oppermann:
extern crate spin;
use spin::Mutex;
static PICS: Mutex<ChainedPics> =
Mutex::new(unsafe { ChainedPics::new(0x20, 0x28) });
We can initialize our PICs as follows:
PICS.lock().initialize();
…and let them know when we’re done processing an interrupt:
PICS.lock().notify_end_of_interrupt(interrupt_id);
Not yet implemented: Masking & spurious interrupts
The current version of this code offers no way to selectively mask and unmask interrupts. We won’t need that right away.
We also have no support for handling “spurious interrupts.” The OSDev wiki explains:
When an IRQ occurs, the PIC chip tells the CPU (via. the PIC’s INTR line) that there’s an interrupt, and the CPU acknowledges this and waits for the PIC to send the interrupt vector. This creates a race condition: if the IRQ disappears after the PIC has told the CPU there’s an interrupt but before the PIC has sent the interrupt vector to the CPU, then the CPU will be waiting for the PIC to tell it which interrupt vector but the PIC won’t have a valid interrupt vector to tell the CPU.
To get around this, the PIC tells the CPU a fake interrupt number. This is a spurious IRQ. The fake interrupt number is the lowest priority interrupt number for the corresponding PIC chip (IRQ 7 for the master PIC, and IRQ 15 for the slave PIC).
There are several reasons for the interrupt to disappear. In my experience the most common reason is software sending an EOI at the wrong time. Other reasons include noise on IRQ lines (or the INTR line).
Linux keeps track of spurious interrupts. You can see the counters by running:
cat /proc/irq/*/spurious | less
Apparently, there are other kinds of spurious interrupts besides the ones described above. Linux Weekly News has an article talking about how modern Linux kernels handle this on especially unreliable hardware. But I’m going to try to ignore this until after we get the keyboard working.
Available on GitHub & crates.io
This code is available
as a Rust crate named pic8259_simple
, which you can
import into your kernel by add the following your Cargo.toml
file:
[dependencies]
pic8259_simple = "0.1.0"
spin = "0.3.4"
You can submit bug reports (and pull requests) on GitHub. Your improvements are welcome!
Where to next?
We’re getting closer to a working keyboard! We still need to set up interrupts, and then write a simple keyboard driver.
But in the meantime, why not check out Philipp Oppermann’s post Allocating Frames, which explains how to figure out how much RAM you have available, and allocate it on demand?
If you have any questions—or if you want to suggest improvements to this code—I’ll be keeping on eye on the Reddit discussion.
Want to build your own kernel in Rust? See Bare Metal Rust to get started.
Want to contact me about this article? Or if you're looking for something else to read, here's a list of popular posts.