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

1

9

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 :)

Whiplash answered 30/6, 2019 at 23:55 Comment(6)
Have you tried booting it in BOCHS, so you can use its built-in debugger to single-step even when interrupts are disabled? And without time passing inside the guest machine. Once you're booted into 32-bit or 64-bit mode, you could also use GDB with QEMU. (BOCHS knows about real-mode segmentation, GDB doesn't). IDK if VirtualBox can act as a GDB remote, but I highly recommend using a VM setup that allows single-step and breakpoint debugging of the whole machine.Indecipherable
in pic_eoi what happens if you remove write_port(SLAVE_CMD, PIC_EOI); wait(); ? No need to delay after sending EOI either. You shouldn't be sending an EOI to the slave if the interrupt occurred on the master. I am aware of your comment, just don't do it. When sending to the PS/2 controller you should be properly waiting on the input buffer status bit becoming 0. When reading data you should be waiting for the output status bit to be set to 1. Do you have a github project or somewhere we can see the entire project?Bleed
It is unclear if you are sending EOI's properly for all the other interrupts.Bleed
If building with optimizations on this line for (u8 i = 0; i < 255; i++); may be optimized entirely away and give no delay.Bleed
I tried your suggestion of changing the interrupt masks given to the PICs and that got it to work! I guess that means there was some other interrupt getting in the way? When the IDT is initialized all the entries are set to a default of calling an "ignore" routine that does nothing but iret, and interrupt 0x21 is the only one currently set to anything else. What exactly was happening that was causing interrupt 0x21 to be ineffective? Also, no Github project yet, but I was planning on making one soon after cleaning up a bit and getting this working.Whiplash
Ohhh that makes sense. I added a call to pic_eoi() in the ignore routine and removed the PIC bitmask and it's working fine now. At least that was an easy fix Thanks for the help!Whiplash
B
9

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.

Bleed answered 1/7, 2019 at 1:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.