Demilade Sonuga's blog

All posts

The Programmable Interrupt Controller II

2023-02-28 · 14 min read

To see hardware interrupts in action, we're going to start with the timer.

Referring back to this post, the timer is the device connected on line 0 of the first PIC, so its interrupt, because of the mapping, will invoke the 32nd entry in the Interrupt Descriptor Table. The timer, by default, generates an interrupt 100 times per second. This implies that when we put an entry in the 32nd slot in the IDT, its service routine will be called about a hundred times per second (In theory. If a service routine takes a second to run, then it can't be executed more than once in a second).

But before we get on with the timer, we first need to take a look at interrupt masking.

Interrupt Masking

This is a scheme that the PIC uses to determine which interrupt requests to answer and which to respond to. Each PIC has an internal register that holds the value of the interrupt mask. An interrupt mask is an 8-bit number whose bits tell the PIC which lines to ignore. A 1 means "don't answer" while a 0 means "answer". For example, if the first PIC has an interrupt mask of 0b11111001, then interrupt requests from the keyboard and the second PIC will be answered because bit 1 and bit 2, which correspond to the keyboard and second PIC's lines, are set to 0.

To enable the first PIC to handle timer interrupts, we write to its data port after telling it about its environment:

In pics.rs

impl PICs {
// ...Others

pub fn init(&mut self) {
// ...Others

self.second.data.write(MODE_8086);
wait();

// Setting the interrupt masks
// Line 0 on the first PIC is for the timer,
// so setting it to 0
self.first.data.write(0b11111110);
self.second.data.write(0b11111111);
}
}

Now, when the PICs init function is called, we can be sure that the timer interrupt will be handled.

Entering A Timer Interrupt Entry

This is just a matter of creating another service routine, creating an entry with it and putting that entry in the IDT's 32nd slot.

// ...Others
use interrupts::{IDT, Entry, ServiceRoutine, ServiceRoutineWithErrCode, ServiceRoutineWithNoReturn};

mod game;

mod pics;
use pics::PICs;

// ...Others
static mut TSS: Option<TSS> = None;
static mut IDT: Option<IDT> = None;
static mut PICS: Option<PICs> = None; // NEW

#[no_mangle]
extern "efiapi" fn efi_main(
handle: *const core::ffi::c_void,
sys_table: *mut SystemTable,
) -> usize {
// ...Others

setup_idt(cs);
setup_pics(); // NEW

// DELETED: unsafe { core::arch::asm!("int 0") };

game::blasterball(screen);
// ...Others
}

// ...Others

fn setup_idt(sel: SegmentSelector) {
// ...Others
idt.double_fault = Entry::exception(ServiceRoutineWithNoReturn(double_fault_handler), sel);
idt.interrupts[0] = Entry::interrupt(ServiceRoutine(timer_handler), sel); // NEW
let pointer = idt.as_pointer();
// ...Others
}

// NEW:
// Creates a new PICs instance and initializes it
fn setup_pics() {
let pics: &mut PICs;
unsafe {
PICS = Some(PICs::new());
pics = PICS.as_mut().unwrap();
}
pics.init();
}

// ...Others

extern "x86-interrupt" fn timer_handler(frame: interrupts::InterruptStackFrame) {
let screen = get_screen().unwrap();
write!(screen, "!");
}

// ...Others

If you run the code now, you'll get the game scene with a single exclamation mark at the upper right corner of the screen:

A Single Exclamation Mark

At the beginning of this post, it was said that the timer generates interrupts 100 times per second. But the timer's interrupt service routine gets called in the code only once, hence the single exclamation mark.

The culprit of this problem is the subject of the next section.

End Of Interrupt

At the end of a hardware interrupt's service routine, a command called the end of interrupt (EOI) command must be sent to the command ports of the PICs involved. For example, for the timer interrupt, the EOI command must be sent only to the first PIC because the request goes only through the first PIC. But for an interrupt that is raised from the second PIC, like the mouse, both the first and second PICs must be sent the EOI command.

Another interrupt will not be received until the EOI command is sent. This explains the single exclamation mark at the corner of the screen.

To tackle this, we need a function to send this EOI command.

In main.rs

// ...Others

extern "x86-interrupt" fn timer_handler(frame: interrupts::InterruptStackFrame) {
let screen = get_screen().unwrap();
write!(screen, "!");
get_pics().unwrap().end_of_interrupt(); // NEW
}

// ...Others

pub fn get_screen() -> Option<&'static mut &'static mut Screen> { /* ...Others */ }

// NEW:
pub fn get_pics() -> Option<&'static mut PICs> {
unsafe { PICS.as_mut() }
}

// ...Others

In pics.rs

impl PICs {
// ...Others

// Sends the EOI command to the PICs in charge of an interrupt
// that was just handled
pub fn end_of_interrupt(&mut self) {

}
}

The problem that comes next is how we can know if the interrupt is from the second or the first PIC, so we can determine whether or not to send an EOI command to the second PIC.

To do this, we can view the chained PIC as a single PIC with lines 0 - 7 of the first PIC being lines 0 - 7 of the chained one and lines 0 - 7 of the second PIC being lines 8 - 15 of the chained PIC. For example, the timer's number remains 0 because it is line 0 on the first PIC, but the mouse's number will be 12 because it's line 4 on the second PIC.

This number can then be passed as an argument to the end_of_interrupt function:

// ...Others
const MODE_8086: u8 = 0x01;
const END_OF_INTERRUPT: u8 = 0x20;

// ...Others

impl PICs {
// ...Others

// DELETED: pub fn end_of_interrupt(&mut self) {
pub fn end_of_interrupt(&mut self, number: u8) { // NEW
self.first.command.write(END_OF_INTERRUPT);
if number >= 8 {
self.second.command.write(END_OF_INTERRUPT);
}
}
}

As for the timer's service routine:

extern "x86-interrupt" fn timer_handler(frame: interrupts::InterruptStackFrame) {
let screen = get_screen().unwrap();
write!(screen, "!");
// DELETED: get_pics().unwrap().end_of_interrupt();
// Signalling that the timer interrupt has been handled
get_pics().unwrap().end_of_interrupt(0); // NEW
}

Running the code now will result in exclamation marks written on the screen.

Take Away

For the full code, go to the repo

In The Next Post

We'll get to the keyboard interrupt.

References