mov eax , cr0
or eax , 0x01
mov cr0 , eax



back to months list

Project : The "Microkernel" Operating System

Journal Entry Date : 2024.11.26

Today I made some progress on PS/2 driver, because I wanted to make a proper keyboard and mouse driver!

I referenced this article from osdev.org for the device specification.

Before we delve into it, a few critical things need to be addressed. The PS/2 controller has two I/O ports: 0x60 and 0x64. The 0x60(Data) port contains the data received from the PS/2 controller. We can also write data to it if needed. The 0x64(Command) port sends commands and receives the controller's status. When we read the port 0x64, we get something called Status Register, which, as the name suggests, indicates the status of the PS/2 controller. When we write to port 0x64, however, we are writing to the command register, which we will delve into later.

Please check out the article I referenced up there, which explains this whole lot better than me. (Thank you, osdev!)

We'll be starting from the initialization. According to the article, these are the steps for initializing ps/2 controller :

  1. Disable devices. We can send specific commands that disable both first and second PS/2 ports through the 0x64 port.
  2. Flush the output buffer. To flush the output buffer, we need to poll the data from the output buffer(which is the port 0x60.) Status Register tells us whether the output buffer is full or empty, so we need to constantly read out the port 0x60 until the Status Register tells us it's empty.
  3. Set the controller's configuration byte. PS/2 Controller's Configuration Byte allows you to configure settings like interrupt or clock. What we want to achieve here is to temporarily disable IRQs of the PS/2 ports, which we can do by modifying the configuration bytes.
  4. Perform controller self-test.
  5. To perform self-test of the controller, we just have to send self-test command and wait for the self-test result from the data port.
  6. Check whether it is a dual channel.
  7. We can get this information by just reading the Status Register. Same as before.
  8. Interface Test. Similar to all the other things we've done before, there's commands for Interface Test of both first and second PS/2 Port. After sending the command, we can just wait for the test result from the data port.
  9. Enable the devices. If we pass the interface test, we can now finally enable the devices. We can now enable both devices' interrupts by modifying configuration bytes and send commands that enable the devices.
  10. Reset devices. Finally, we send the reset commands to both of devices and wrap up the initialization process.

Check out the implementation on this github link.

Now that we have initialized the PS/2 controller and also enabled the devices, what we need to do is just send commands directly to the second PS/2 device(which is the PS/2 mouse) and make it transmit the data. I referenced this article for all the specifications of commands.

To enable the mouse:

  1. Set the device settings to default. The command 0xF6 sets the device to default state, but we can't just directly send the command to PS/2's controller port. We need to first send command 0xD4 — which writes next byte to second PS/2 port's input buffer — to the PS/2 controller's command port(0x64) and then send the command 0xF6 to the PS/2's data port(0x60).
  2. Enable the data packet streaming. All we need to do is enable the data packet streaming, which finally sends all the juicy mouse movements out to the PS/2's data port(0x60). Sending the command 0xF4 to the secondary PS/2 device enables data reporting of that device.

Since the code is not that long, here's the implementation:

void enable_mouse(void) {
    ps2::wait_for_input_buffer_status(PS2_EMPTY); // set the settings to default
    io_write_byte(PS2_COMMAND_PORT , 0xD4); 
    ps2::wait_for_input_buffer_status(PS2_EMPTY);
    io_write_byte(PS2_DATA_PORT , 0xF6);
    ps2::wait_for_output_buffer_status(PS2_FULL);
    io_read_byte(PS2_DATA_PORT);

    ps2::wait_for_input_buffer_status(PS2_EMPTY); // enable data packet streaming
    io_write_byte(PS2_COMMAND_PORT , 0xD4); 
    ps2::wait_for_input_buffer_status(PS2_EMPTY);
    io_write_byte(PS2_DATA_PORT , 0xF4);
    debug::out::printf("Enabling data packet streaming\n");

    // clear out the output buffer
    ps2::wait_for_output_buffer_status(PS2_FULL);
    byte buffer = io_read_byte(PS2_DATA_PORT);
}

Now, here's the very important thing. As you know, the IRQ number of the PS/2 mouse is 12, which is equivalent to interrupt number 44. Here's the quirky thing: you must unmask IRQ number 2 to make the IRQ12 receive the interrupt. I don't know why, but it's just designed that way.

I was puzzled as to why the mouse interrupt didn't occur, and I happened to stumble upon the solution coincidentally! How weird is that?

void ps2::initialize_interrupt(void) {
    interrupt::general::register_interrupt(0x20+1  , ps2_interrupt_handler_irq1 , INTERRUPT_HANDLER_HARDWARE|INTERRUPT_HANDLER_LEVEL_KERNEL);
    
    // somehow you have to unmask the interrupt number 2 to make mouse work... how weird! 
    interrupt::general::set_interrupt_mask(0x20+2 , false);
    interrupt::general::register_interrupt(0x20+12 , ps2_interrupt_handler_irq12 , INTERRUPT_HANDLER_HARDWARE|INTERRUPT_HANDLER_LEVEL_KERNEL);
}

One more problem we need to address is the interrupt system. As of now, we don't have a proper interrupt system for numerical interrupts. Individual interrupt handlers must all have architecture-dependent interrupt wrapper that quickly gets redundant in the code. To solve this problem, we need a similar solution as to how we take care of the "hardware-specific" interrupts---providing the pre-declared interrupt wrapper functions. Thus, my next goal is to establish an interrupt system that can simplify the interrupt handler by implementing pre-declared interrupt wrapper functions(just like the "hardware-specific" interrupts.)