Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PS/2 keyboard won't send keypress interrupts, but does respond to commands

I'm fairly new to OS development and I recently started a hobby project of creating a simple-as-possible text-only operating system. It's written in C with some help from assembly and uses GRUB for booting, and I've been testing it in VirtualBox and also occasionally putting it on a flash drive for testing on an ancient (~2009) laptop. So far I've implemented some basic text output functions, and I think my GDT and IDT implementations are okay given the lack of crashing lately. Currently I'm trying to get an interrupt-driven keyboard driver working.

I think I've got the PICs set up correctly, and it seems I've had luck in giving commands to the PS/2 controller and keyboard and capturing responses via an interrupt handler. For example, here's the debug output when giving the keyboard an identify command:

Initializing kernel...
Setting PS/2 controller status: 0x05
Sending keyboard command: 0xF2
Keyboard interrupt: 0xFA
Keyboard interrupt: 0xAB
Keyboard interrupt: 0x83

The data returned seems to be correct, and this proves that my interrupt handler is able to work multiple times in succession without crashing or anything, so I'm not too worried about my IDT or ISR implementation. Now here's the output when I send the 0xF4 command to the keyboard to start scanning for key presses:

Initializing kernel...
Setting PS/2 controller status: 0x05
Sending keyboard command: 0xF4
Keyboard interrupt: 0xFA

The interrupt with the "acknowledge" status code 0xFA seems promising, but afterwards nothing happens when I press keys. For both examples, I got the same results when running both in VirtualBox and on the laptop I've been using.

Here's some relevant code from the keyboard driver:

#define KEYBD_DATA 0x60
#define KEYBD_CMD 0x64

// wrapper for interrupt service routine written in assembly
extern void keyboard_interrupt();

// called from assembly ISR
void keyboard_handler() {
    u8 data = read_port(KEYBD_DATA);
    print("Keyboard interrupt: 0x");
    printx(data);
    putc('\n');
    pic_eoi();
}

// functions to print command before sending it to the port
void keyboard_command(u8 cmd) {
    print("Sending keyboard command: 0x");
    printx(cmd);
    putc('\n');
    write_port(KEYBD_DATA, cmd);
}

void controller_command(u8 cmd) {
    print("Sending controller command: 0x");
    printx(cmd);
    putc('\n');
    write_port(KEYBD_CMD, cmd);
}

void setup_keyboard() {

    // flush keyboard output
    while(read_port(KEYBD_CMD) & 1)
        read_port(KEYBD_DATA);

    // set interrupt descriptor table entry (default code segment and access flags)
    set_idt_entry(0x21, &keyboard_interrupt);

    // activate device
    write_port(KEYBD_CMD, 0xAE);
    wait();

    // get status
    write_port(KEYBD_CMD, 0x20);
    wait();
    u8 status = (read_port(KEYBD_DATA) | 1) & 0x05;
    print("Setting PS/2 controller status: 0x");
    printx(status);
    putc('\n');
    wait();

    // set status
    write_port(KEYBD_CMD, 0x60);
    wait();
    write_port(KEYBD_DATA, status);
    wait();

    // enable keyboard scanning
    keyboard_command(0xf4);
}

Not that I think it's the root of the problem, but here's the assembly part of the interrupt handler just in case (in GNU assembly):

.extern keyboard_handler
.global keyboard_interrupt

keyboard_interrupt:
    cli
    pusha
    cld
    call keyboard_handler
    popa
    sti
    iret

Here's the code that sets up the PICs beforehand:

#define MASTER_CMD 0x20
#define MASTER_DATA 0x21
#define SLAVE_CMD 0xA0
#define SLAVE_DATA 0xA1
#define PIC_EOI 0x20

// hopefully this gives a long enough delay
void wait() {
    for (u8 i = 0; i < 255; i++);
}

// alert the PICs that the interrupt handling is done
// (later I'll check whether the slave PIC needs to be sent the EOI, but for now it doesn't seem to hurt to give it anyway)
void pic_eoi() {
    write_port(MASTER_CMD, PIC_EOI);
    write_port(SLAVE_CMD, PIC_EOI);
    wait();
}

void setup_pic() {
    write_port(MASTER_CMD, 0x11);
    write_port(SLAVE_CMD, 0x11);
    wait();
    write_port(MASTER_DATA, 0x20);
    write_port(SLAVE_DATA, 0x28);
    wait();
    write_port(MASTER_DATA, 0x4);
    write_port(SLAVE_DATA, 0x2);
    wait();
    write_port(MASTER_DATA, 0x1);
    write_port(SLAVE_DATA, 0x1);
    wait();
    write_port(MASTER_DATA, 0x0);
    write_port(SLAVE_DATA, 0x0);
    wait();
}

Here's the order of initializations in the main part of the kernel:

// initialize global descriptor table and interrupt descriptor table
setup_gdt();
setup_idt();

// setup hardware interrupts
setup_pic();
setup_keyboard();
activate_idt(); // assembly routine with lidt and sti

I also know that the keyboard is in fact doing its thing and putting scan codes on port 0x60, and I've been able to get a polling method of getting keypresses working, but it's messy and it would make it much harder to handle things like key repetition and keeping track of the shift key. Let me know if more code is needed. Hopefully there's just something obvious I'm either forgetting or doing wrong :)

like image 577
Adam Avatar asked Jun 30 '19 23:06

Adam


1 Answers

General reasons why a specific IRQ, some IRQs, or all IRQs may not appear to work:

  • You haven't enabled interrupts on the CPU with sti (or equivalent)
  • You haven't enabled the interrupts(s) with a mask sent to the master and slave PICs when you initialise them.
  • Not properly acknowledging an EOI when an interrupt does occur can disable some or all interrupts depending on the priority of the interrupt.
  • You have disabled the PICs
  • You won't get a keyboard interrupt from the PS/2 keyboard unless you have sent a PS/2 controller configuration byte with bit 0 set (bit 1 is interrupt for the mouse)

I'd narrow down the problem space by masking off all external interrupts except the one you are testing. In your case you are interested in IRQ1. To mask off all external interrupts except IRQ1 you can change setup_pic so that:

write_port(MASTER_DATA, 0x0);
write_port(SLAVE_DATA, 0x0);

Becomes:

write_port(MASTER_DATA, ~0x2);
write_port(SLAVE_DATA, ~0x0);

Bits that are set mask off an interrupt and ones that are zero enable them. ~0x2 is the bitmask 0b11111101 and ~0x0 is the bitmask 0b11111111. That should disable all but IRQ1 (bit 1 of master PIC).


You discovered that the problem disappeared by using the suggestion above and then mention your default interrupt handler just does an IRET. You need to send a proper EOI even in your default do nothing IRQ handlers. Don't send EOIs for interrupts unless they come from the PICs. In your case IDT entry 0x20 to 0x2f (inclusive) need to have handlers that send proper EOIs. More detailed information on properly handling EOIs can be found on the OSDev Wiki

I'd guess what is going on is that on the first timer interrupt (IRQ0) you send no EOI, and that would effectively disable all external interrupts. Until an EOI is sent all external interrupts of equal or lower priority will be disabled. IRQ0 (timer) is the highest priority, so not sending an EOI effectively disables all external interrupts until an EOI is sent.

like image 128
Michael Petch Avatar answered Oct 05 '22 22:10

Michael Petch