Search code examples
assemblykeyboardx86-16attscancodes

How to generate a keyboard interrupt in assembly 8086


I'm trying to code a program that recognizes the keyboard and print a letter (not the one I pressed) in the screen when a key is pressed. I managed to make a program that recognizes the keyboard and print the letter, however, it prints the letter more than once, depending on the key it generates more than two (for example printscreen key and delete). I suspect that it might be the keyboard buffer, already tried to clean it but without any success. My code right now is that one:

.code16                   
.text                  
.globl _start;

_start:
    # prints "X" -> Just to know that the application started
    movb $'X' , %al      
    movb $0x0e, %ah     
    int  $0x10   

    # Ensure we are starting from the beginning of the interrupt vector
    movw $0x0, %ax          # Putting the value 0 in ax to store in Data Segment Register (ds)
    movw %ax, %ds           # Putting 0 in Data Segment Register(ds)
    movw $0x204,%bx         # Going to address 0x204 -> 512 in the interruput vector  200 <- 80 * 4bytes, 204 <- 200 + 4bytes 
    movw $0x7d00, (%bx)     # Putting the address 0x7d00 in the %bx addres, 0x7d00 = = _start addres + 256

    # First PIC (master)
    movb $0x11, %al
    outb  %al, $0x20  # Sending ICW1 in the first PIC -> 0001 0001: ICW4 is required
    movb $0x80, %al
    outb  %al, $0x21  # Sending ICW2 in the first PIC -> 1000 0000: defines the interrupt that will be called (80)
    movb $0x04, %al
    outb  %al, $0x21  # Sending ICW3 in the first PIC -> 0000 0100: has a skave
    movb $0x01, %al
    outb  %al, $0x21  # Sending ICW4 in the first PIC -> 0000 0001: 8086/8088 mode
    movb $0xfd, %al 
    outb %al, $0x21   # Inhibiting all interrupts except keyboard (OCW1)

    # Second PIC (slave)
    movb $0x11, %al
    outb  %al, $0xa0  # Sending ICW1 in the second PIC -> 0001 0001: ICW4 is required
    movb $0x80, %al
    outb  %al, $0xa1  # Sending ICW2 in the second PIC -> 1000 0000: defines the interrupt that will be called (80)
    movb $0x02, %al
    outb  %al, $0xa1  # Sending ICW3 in the second PIC -> 0000 0010: 
    movb $0x01, %al
    outb  %al, $0xa1  # Sending ICW4 in the second PIC -> 0000 0001: 8086/8088 mode
    movb $0xff, %al 
    outb %al, $0xa1   # Inibindo todas as interrupções, exceto teclado (OCW1)

    movb $0xfd, %al     # Sending OCW1 in the first PIC
    outb %al, $0x21
    movb $0xff, %al     # Sending OCW1 in the second PIC
    outb %al, $0xa1


    # Loop to get interrupt
    mov $0x0, %cl
_loop:          #infinity loop
    cmp $0x0, %cl
    je _loop
    hlt
intKBD:
    . = _start + 256

    movb $0x20, %al    # Sending OCW2 in the first PIC -> EOI
    outb %al, $0x20

.checkKeyboard:
    in $0x64, %al
    and $0x1, %al
    jz .emptyKeyboard
    in $0x60, %al
    jmp .fullKeyboard

    #If keyboard is mepty -> Prints V
.emptyKeyboard:
    movb $'V', %al
    movb $0x0e, %ah
    int $0x10

    #If keyboard is full -> Prints K
.fullKeyboard:
    movb $'K', %al
    movb $0x0e, %ah
    int $0x10
    iret

_end:   #Signature boot
    . = _start + 510
        .byte 0x55
        .byte 0xaa

Any piece of code or directions are very appreciated


Solution

  • I managed to make a program that recognizes the keyboard and print the letter, however, it prints the letter more than once, depending on the key it generates more than two (for example printscreen key and delete)

    That's normal. The keyboard sends variable length scan codes; and (depending on how the keyboard is configured, or not configured in your case) could send 4 bytes when a key is pressed and then another 4 bytes when a key is released.

    Ideally you'd have some kind of state machine to convert the variable length scan codes into a fixed size "key code"; where the states are like "waiting for first byte of scan code", "waiting for second byte of scan code after already receiving an 0xE0 byte", etc. Also don't forget that the keyboard can send things that are not part of any scan code; like "I acknowledge your last command", "Something went wrong, resend the last command please" and "Hello, I just got (re)plugged in"; which can either bypass your state machine or cause it to be reset.

    That "key code" is whatever you want it to be. I personally like the idea of making it a 16 bit integer that reflects the physical location of the key (e.g. like key_code = (row << 8) | column maybe).

    Once you've got some kind of fixed size "key code" you can use it as an index into tables to determine what the key is, and if there's a corresponding ASCII character or a corresponding sequence of Unicode code points, and if there's special behavior (if it's supposed to control an LED, if it's some kind of meta-key, etc). The main reason for a fixed size "key code" is that variable length scan codes are awful for lookup tables (you don't want a table with 4 billion entries to support 4-byte scan codes).

    Note that (for supporting different keyboard layouts, internationalization, etc) all of this should use tables loaded from a "keyboard layout file". Even if you ignore most of the world and only support one keyboard layout (e.g. "US Qwerty") you'll find that different keyboards have different non-standard extensions (e.g. multi-media keys, etc). Also, normal software (applications, games, etc) will have to deal with this variability too; and a good OS should allow software to do reverse lookups (e.g. ask the OS "For the third keyboard, what is the key on row 0, column 4?" and get information, possibly including a text description like "Increase speaker volume", from the OS). The conversion from scan-code to key-code also means that completely different types of keyboards (e.g. USB keyboards) can convert whatever they get from the keyboard into the same "key codes", and use the same keyboard layout files.

    You probably should also be aware that there are multiple scan code sets, the default is "scan code set 2", other scan codes may not be supported; and the traditional "8042" keyboard controller in PCs (and not the keyboard) is capable of translating "scan code set 2" (from the keyboard) into "scan code set 1" for backward compatibility with obsolete junk from the 1970s; and (for BIOS) this scan code translation is enabled by default for the 1st PS/2 port (and doesn't exist for the 2nd PS/2 port). The consequence of these things mean that your keyboard driver "possibly must" support scan code set 2 (to support a keyboard is plugged into 2nd PS/2 port) and therefore the translation done by the "8042" controller should be disabled (so the keyboard driver doesn't need to support scan code set 1).

    The other thing you should be aware of is that most modern computers use USB keyboards; where the firmware might (for BIOS) or might not (for UEFI) have annoying dodgy junk to try emulate a PS/2 keyboard. This emulation is notoriously unreliable and can/will prevent the proper initialization of the "8042" controller; and you should enumerate PCI devices, find USB controllers, disable any legacy "PS/2 emulation" nonsense; then initialize the "8042" controller properly (then start your PS/2 keyboard driver).

    Finally; the keyboard controller driver should be separate from the keyboard driver; and the keyboard controller's driver can/should auto-detect the type of device plugged in during boot and when devices are plugged in. E.g. if the computer had "bar code scanner in port 1, mouse in port 2" and the user unplugs the mouse and plugs in a keyboard (leading to "keyboard in port 2"); then the keyboard controller driver should arrange for the mouse driver to be stopped and a keyboard driver to be started. The keyboard driver should never touch any IO port and should only ever ask the controller driver to send/receive bytes on its behalf. The controller driver may start 2 instances of the same keyboard driver (e.g. if it's "keyboard in 1st port, another keyboard in 2nd port"); and the different instances of the same keyboard driver may use different "keyboard layout files".


    On an unrelated matter; I'd also recommend having a defined "point during boot where OS takes control of the hardware". Before this point you shouldn't trash the firmware's hardware configuration for anything and should rely on BIOS functions (e.g. the "int 016 with ah=0, get keystroke" function); and after this point you are responsible for all hardware and can reconfigure anything you like, but should not expect any BIOS function to work.

    You can't guarantee that messing with one piece of hardware won't break other BIOS functions. (And can't guarantee "BIOS int 0x10" doesn't have a time delay that happens to rely on a timer IRQ that happens to rely on the PIC chip's configuration).