How to check keys status in x86 assembly?

DieJay picture DieJay · May 10, 2012 · Viewed 11.7k times · Source

I took x86 assembly as a hobby this past january so I could make games that would work on old 8086-powered computers like the PCj and Tandy 1000, but the books I found don't exactly teach much on that specific topic. While some dos and bios interrupts kind of do the job, they're far from perfect.

My main issue is reading the keyboard status for pressed keys without halting the program. I found a few methods, but they're very limited. INT 21h, AH 0Ch reads the last pressed key, but in a text-edition fashion. Not only does it read only one key at a time, but the notepad-like hit detection makes it impossible to know how long the key has been held. I've also seen references to the ports 60h to 64h during my Google travels, but it's just that, references. Actual explanations and working code is virtually non-existent. Or maybe I'm just that bad at using search engines.

What I need to know is whether a key is held down or not. The best solution would be to have a buffer/array of all the keyboard keys and read its state; 1 means it's down, 0 means it's not. Or just having access to a list of the last keys to have been hit and released would be nice (with a way to clear that buffer, of course). Can anyone point me in the right direction?

Edit: First off, I should have mentioned that I use Borland TASM. Now I compiled your code and it works great and all, even though I'm almost shy to admit I don't understand half of it. I tried to make it compatible with TASM but all it does is create garbage on the screen and freeze.

Here's what I came up with;

.MODEL TINY
.STACK 256

.DATA
kbdbuf DB 128 DUP (0)

msg1 db "Press and hold ESC", 13, 10, "$"
msg2 db "ESC pressed, release ESC", 13, 10, "$"
msg3 db "ESC released", 13, 10, "$"

.CODE
main PROC
    org 0100h
    mov ax, @data
    mov ds, ax

    xor     ax, ax
    mov     es, ax

    cli                         ; update ISR address w/ ints disabled
    push    word [es:9*4+2]     ; preserve ISR address
    push    word [es:9*4]
    lea si, irq1isr
    mov     word [es:9*4], si   ; requires a register
    mov     [es:9*4+2],cs
    sti

        mov     ah, 9
        lea     dx, msg1
        int     021h                ; print "Press and hold ESC"

    test1:
        mov     al, [kbdbuf + 1]    ; check Escape key state (Esc scan code = 1)
        or      al, al
        jz      test1               ; wait until it's nonzero (pressed/held)

        lea     dx, msg2
        int     021h                ; print "ESC pressed, release ESC"

    test2:
        mov     al, [kbdbuf + 1]    ; check Escape key state (Esc scan code = 1)
        or      al, al
        jnz     test2               ; wait until it's zero (released/not pressed)

        lea     dx, msg3            ; print "ESC released"
        int     021h

    cli                         ; update ISR address w/ ints disabled
    pop     word [es:9*4]       ; restore ISR address
    pop     word [es:9*4+2]
    sti

    ret

    irq1isr:
    push    ax bx

    ; read keyboard scan code
    in      al, 060h

    ; update keyboard state
    xor     bh, bh
    mov     bl, al
    and     bl, 07Fh            ; bx = scan code
    shr     al, 7               ; al = 0 if pressed, 1 if released
    xor     al, 1               ; al = 1 if pressed, 0 if released
    mov     [cs:bx+kbdbuf], al

    ; send EOI to XT keyboard
    in      al, 061h
    mov     ah, al
    or      al, 080h
    out     061h, al
    mov     al, ah
    out     061h, al

    ; send EOI to master PIC
    mov     al, 020h
    out     020h, al

    pop     bx ax
    iret
main ENDP

END main

I'm not sure if I even coded the interrupt right. And heck if I know how the ports 060h - 064h work.

Answer

Alexey Frunze picture Alexey Frunze · May 10, 2012

Here's how you can do it:

; compile with NASM: nasm.exe -f bin kbd.asm -o kbd.com

bits 16
org 0x100

    xor     ax, ax
    mov     es, ax

    cli                         ; update ISR address w/ ints disabled
    push    word [es:9*4+2]     ; preserve ISR address
    push    word [es:9*4]
    mov     word [es:9*4], irq1isr
    mov     [es:9*4+2],cs
    sti

    call    test

    cli                         ; update ISR address w/ ints disabled
    pop     word [es:9*4]       ; restore ISR address
    pop     word [es:9*4+2]
    sti

    ret

test:
    mov     ah, 9
    mov     dx, msg1
    int     0x21                ; print "Press and hold ESC"

test1:
    mov     al, [kbdbuf + 1]    ; check Escape key state (Esc scan code = 1)
    or      al, al
    jz      test1               ; wait until it's nonzero (pressed/held)

    mov     dx, msg2
    int     0x21                ; print "ESC pressed, release ESC"

test2:
    mov     al, [kbdbuf + 1]    ; check Escape key state (Esc scan code = 1)
    or      al, al
    jnz     test2               ; wait until it's zero (released/not pressed)

    mov     dx, msg3            ; print "ESC released"
    int     0x21

    ret

irq1isr:
    pusha

    ; read keyboard scan code
    in      al, 0x60

    ; update keyboard state
    xor     bh, bh
    mov     bl, al
    and     bl, 0x7F            ; bx = scan code
    shr     al, 7               ; al = 0 if pressed, 1 if released
    xor     al, 1               ; al = 1 if pressed, 0 if released
    mov     [cs:bx+kbdbuf], al

    ; send EOI to XT keyboard
    in      al, 0x61
    mov     ah, al
    or      al, 0x80
    out     0x61, al
    mov     al, ah
    out     0x61, al

    ; send EOI to master PIC
    mov     al, 0x20
    out     0x20, al

    popa
    iret

kbdbuf:
    times   128 db 0

msg1 db "Press and hold ESC", 13, 10, "$"
msg2 db "ESC pressed, release ESC", 13, 10, "$"
msg3 db "ESC released", 13, 10, "$"

Run it in DOS/Win9x/NT/2K/XP/32-bit Vista/7 or DosBox.

UPDATE: TASM version:

; file: kbdt.asm
; compile with TASM/TLINK:
;   tasm.exe kbdt.asm
;   tlink.exe /t kbdt.obj

.286

code segment use16
assume cs:code, ds:code, ss:code
org 100h

main:
    xor     ax, ax
    mov     es, ax

    cli                         ; update ISR address w/ ints disabled
    push    word ptr es:[9*4+2]     ; preserve ISR address
    push    word ptr es:[9*4]
    mov     word ptr es:[9*4], offset irq1isr
    mov     es:[9*4+2],cs
    sti

    call    test0

    cli                         ; update ISR address w/ ints disabled
    pop     word ptr es:[9*4]   ; restore ISR address
    pop     word ptr es:[9*4+2]
    sti

    ret

test0:
    mov     ah, 9
    mov     dx, offset msg1
    int     21h                 ; print "Press and hold ESC"

test1:
    mov     al, [kbdbuf + 1]    ; check Escape key state (Esc scan code = 1)
    or      al, al
    jz      test1               ; wait until it's nonzero (pressed/held)

    mov     dx, offset msg2
    int     21h                 ; print "ESC pressed, release ESC"

test2:
    mov     al, [kbdbuf + 1]    ; check Escape key state (Esc scan code = 1)
    or      al, al
    jnz     test2               ; wait until it's zero (released/not pressed)

    mov     dx, offset msg3     ; print "ESC released"
    int     21h

    ret

irq1isr:
    pusha

    ; read keyboard scan code
    in      al, 60h

    ; update keyboard state
    xor     bh, bh
    mov     bl, al
    and     bl, 7Fh             ; bx = scan code
    shr     al, 7               ; al = 0 if pressed, 1 if released
    xor     al, 1               ; al = 1 if pressed, 0 if released
    mov     cs:[bx+kbdbuf], al

    ; send EOI to XT keyboard
    in      al, 61h
    mov     ah, al
    or      al, 80h
    out     61h, al
    mov     al, ah
    out     61h, al

    ; send EOI to master PIC
    mov     al, 20h
    out     20h, al

    popa
    iret

kbdbuf      db 128 dup (0)

msg1 db "Press and hold ESC", 13, 10, "$"
msg2 db "ESC pressed, release ESC", 13, 10, "$"
msg3 db "ESC released", 13, 10, "$"

code ends

end main