CrizerPL
CrizerPL

Reputation: 287

Checking if a key is down in MS-DOS (C/C++)

Yes, I mean real MS-DOS, not Windows' cmd.exe shell console.

Is there a way to check if a key is down in MS-DOS, analogically to the GetAsyncKeyState() function in WinAPI?

Currently I'm using kbhit() and getch(), but it's really slow, has a delay after the first character, doesn't allow multiple keys at the same time etc.

I'm using Turbo C++ 3.1. Can anyone help?

(by the way, don't ask why I'm coding my game on such an ancient system)

Upvotes: 15

Views: 5743

Answers (4)

SeanRamey
SeanRamey

Reputation: 823

So, I've gone through all this stuff somewhat recently, and just happen to have the code that you need. (Also, I will link you some great books to get information from in pdf format.)

So, the way that this works, is you need to overwrite the Interrupt Vector Table in memory at index 9h. The Interrupt Vector Table is simply a table of memory addresses that point to a piece of code to run when that interrupt is triggered (these are called interrupt handler routines or ISRs). Interrupt 9h is triggered when the keyboard controller has a scancode ready for use.

Anyways, we first need to overwrite the old int9h ISR by calling the KeyboardInstallDriver() function. Now, when int9h is triggered, the KeyboardIsr() function is called, and it gets the scancode from the keyboard controller, and sets a value in the keyStates[] array to either 1 (KEY_PRESSED) or 0 (KEY_RELEASED) based on the value of the scan code that was retrieved from the keyboard controller.

After the corresponding value in the keyStates[] array has been set, then you can call KeyboardGetKey() giving it the scancode of the key that you want to know the state of, and it will look it up in the keyStates[] array and return whatever the state is.

There is a lot of details to this, but it's WAY too much to write on here. All the details can be found in the books that I will link here: IBM PC Technical Reference, IBM PC XT Technical Reference, IBM PC AT Technical Reference, Black Art of 3D Game Programming

Hopefully those links stay active for a while. Also, the "Black Art of 3D Game Programming" book is not always completely accurate on every little detail. Sometimes there are typos, and sometimes there is misinformation, but the IBM Technical References have all the details (even if they are a bit cryptic at times), but they have no example code. Use the book to get the general idea, and use the references to get the details.

Here is my code for getting input from the keyboard: (it's not completely finished for all the possible keys and certain other things, but it works quite well for most programs and games.)

Also, there is some code to handle the "extended" keys. The extended keys have 0xE0 prefixed to their regular scan code. There are even more crazy details with this, so I'm not gonna cover it, but, there is the mostly working code, anyways.

keyboard.h

#ifndef KEYBOARD_H_INCLUDED
#define KEYBOARD_H_INCLUDED

#include "keyboard_scan_codes.h"

unsigned char   KeyboardGetKey(unsigned int scanCode);
void            KeyboardClearKeys();
void            KeyboardInstallDriver();
void            KeyboardUninstallDriver();
void            KeyboardDumpScancodeLog();

#endif // KEYBOARD_H_INCLUDED

keyboard.c

#define MAX_SCAN_CODES 256
#define KEYBOARD_CONTROLLER_OUTPUT_BUFFER 0x60
#define KEYBOARD_CONTROLLER_STATUS_REGISTER 0x64
#define KEY_PRESSED 1
#define KEY_RELEASED 0
#define PIC_OPERATION_COMMAND_PORT 0x20
#define KEYBOARD_INTERRUPT_VECTOR 0x09

// PPI stands for Programmable Peripheral Interface (which is the Intel 8255A chip)
// The PPI ports are only for IBM PC and XT, however port A is mapped to the same
// I/O address as the Keyboard Controller's (Intel 8042 chip) output buffer for compatibility.
#define PPI_PORT_A 0x60
#define PPI_PORT_B 0x61
#define PPI_PORT_C 0x62
#define PPI_COMMAND_REGISTER 0x63

#include <dos.h>
#include <string.h>
#include <stdio.h>
#include <conio.h>

#include "keyboard.h"

void interrupt (*oldKeyboardIsr)() = (void *)0;

unsigned char keyStates[MAX_SCAN_CODES];

unsigned char keyCodeLog[256] = {0};
unsigned char keyCodeLogPosition = 0;
static unsigned char isPreviousCodeExtended = 0;

unsigned char KeyboardGetKey(unsigned int scanCode)
{
    // Check for the extended code
    if(scanCode >> 8 == 0xE0)
    {
        // Get rid of the extended code
        scanCode &= 0xFF;
        return keyStates[scanCode + 0x7F];
    }
    else
    {
        return keyStates[scanCode];
    }
}

void KeyboardClearKeys()
{
    memset(&keyStates[0], 0, MAX_SCAN_CODES);
}

void interrupt far KeyboardIsr()
{
    static unsigned char scanCode;
    unsigned char ppiPortB;

    _asm {
        cli // disable interrupts
    };

    /* The keyboard controller, by default, will send scan codes
    // in Scan Code Set 1 (reference the IBM Technical References
    // for a complete list of scan codes).
    //
    // Scan codes in this set come as make/break codes. The make
    // code is the normal scan code of the key, and the break code
    // is the make code bitwise "OR"ed with 0x80 (the high bit is set).
    //
    // On keyboards after the original IBM Model F 83-key, an 0xE0
    // is prepended to some keys that didn't exist on the original keyboard.
    //
    // Some keys have their scan codes affected by the state of
    // the shift, and num-lock keys. These certain
    // keys have, potentially, quite long scan codes with multiple
    // possible 0xE0 bytes along with other codes to indicate the
    // state of the shift, and num-lock keys.
    //
    // There are two other Scan Code Sets, Set 2 and Set 3. Set 2
    // was introduced with the IBM PC AT, and Set 3 with the IBM
    // PS/2. Set 3 is by far the easiest and most simple set to work
    // with, but not all keyboards support it.
    //
    // Note:
    // The "keyboard controller" chip is different depending on
    // which machine is being used. The original IBM PC uses the
    // Intel 8255A-5, while the IBM PC AT uses the Intel 8042 (UPI-42AH).
    // On the 8255A-5, port 0x61 can be read and written to for various
    // things, one of which will clear the keyboard and disable it or
    // re enable it. There is no such function on the AT and newer, but
    // it is not needed anyways. The 8042 uses ports 0x60 and 0x64. Both
    // the 8255A-5 and the 8042 give the scan codes from the keyboard
    // through port 0x60.

    // On the IBM PC and XT and compatibles, you MUST clear the keyboard
    // after reading the scancode by reading the value at port 0x61,
    // flipping the 7th bit to a 1, and writing that value back to port 0x61.
    // After that is done, flip the 7th bit back to 0 to re-enable the keyboard.
    //
    // On IBM PC ATs and newer, writing and reading port 0x61 does nothing (as far
    // as I know), and using it to clear the keyboard isn't necessary.*/

    scanCode = 0;
    ppiPortB = 0;

    ppiPortB = inp(PPI_PORT_B); // get the current settings in PPI port B
    scanCode = inp(KEYBOARD_CONTROLLER_OUTPUT_BUFFER); // get the scancode waiting in the output buffer
    outp(PPI_PORT_B, ppiPortB | 0x80); // set the 7th bit of PPI port B (clear keyboard)
    outp(PPI_PORT_B, ppiPortB); // clear the 7th bit of the PPI (enable keyboard)

    // Log scancode
    keyCodeLog[keyCodeLogPosition] = scanCode;
    if(keyCodeLogPosition < 255)
    {
        ++keyCodeLogPosition;
    }


    // Check to see what the code was.
    // Note that we have to process the scan code one byte at a time.
    // This is because we can't get another scan code until the current
    // interrupt is finished.
    switch(scanCode)
    {
    case 0xE0:
        // Extended scancode
        isPreviousCodeExtended = 1;
        break;
    default:
        // Regular scancode
        // Check the high bit, if set, then it's a break code.
        if(isPreviousCodeExtended)
        {
            isPreviousCodeExtended = 0;
            if(scanCode & 0x80)
            {
                scanCode &= 0x7F;
                keyStates[scanCode + 0x7F] = KEY_RELEASED;
            }
            else
            {
                keyStates[scanCode + 0x7F] = KEY_PRESSED;
            }
        }
        else if(scanCode & 0x80)
        {
            scanCode &= 0x7F;
            keyStates[scanCode] = KEY_RELEASED;
        }
        else
        {
            keyStates[scanCode] = KEY_PRESSED;
        }
        break;
    }

    // Send a "Non Specific End of Interrupt" command to the PIC.
    // See Intel 8259A datasheet for details.
    outp(PIC_OPERATION_COMMAND_PORT, 0x20);

    _asm
    {
        sti // enable interrupts
    };
}

void KeyboardInstallDriver()
{
    // Make sure the new ISR isn't already in use.
    if(oldKeyboardIsr == (void *)0)
    {
        oldKeyboardIsr = _dos_getvect(KEYBOARD_INTERRUPT_VECTOR);
        _dos_setvect(KEYBOARD_INTERRUPT_VECTOR, KeyboardIsr);
    }
}

void KeyboardUninstallDriver()
{
    // Make sure the new ISR is in use.
    if(oldKeyboardIsr != (void *)0)
    {
        _dos_setvect(KEYBOARD_INTERRUPT_VECTOR, oldKeyboardIsr);
        oldKeyboardIsr = (void *)0;
    }
}

void KeyboardDumpScancodeLog()
{
    FILE *keyLogFile = fopen("keylog.hex", "w+b");
    if(!keyLogFile)
    {
        printf("ERROR: Couldn't open file for key logging!\n");
    }
    else
    {
        int i;
        for(i = 0; i < 256; ++i)
        {
            fputc(keyCodeLog[i], keyLogFile);
        }


        fclose(keyLogFile);
    }
}

keyboard_scan_codes.h (simply defines all the scancodes to the qwerty button layout)

#ifndef KEYBOARD_SCAN_CODES_H_INCLUDED
#define KEYBOARD_SCAN_CODES_H_INCLUDED


// Original 83 Keys from the IBM 83-key Model F keyboard
#define SCAN_NONE              0x00
#define SCAN_ESC               0x01
#define SCAN_1                 0x02
#define SCAN_2                 0x03
#define SCAN_3                 0x04
#define SCAN_4                 0x05
#define SCAN_5                 0x06
#define SCAN_6                 0x07
#define SCAN_7                 0x08
#define SCAN_8                 0x09
#define SCAN_9                 0x0A
#define SCAN_0                 0x0B
#define SCAN_MINUS             0x0C
#define SCAN_EQUALS            0x0D
#define SCAN_BACKSPACE         0x0E
#define SCAN_TAB               0x0F
#define SCAN_Q                 0x10
#define SCAN_W                 0x11
#define SCAN_E                 0x12
#define SCAN_R                 0x13
#define SCAN_T                 0x14
#define SCAN_Y                 0x15
#define SCAN_U                 0x16
#define SCAN_I                 0x17
#define SCAN_O                 0x18
#define SCAN_P                 0x19
#define SCAN_LEFT_BRACE        0x1A
#define SCAN_RIGHT_BRACE       0x1B
#define SCAN_ENTER             0x1C
#define SCAN_LEFT_CONTROL      0x1D
#define SCAN_A                 0x1E
#define SCAN_S                 0x1F
#define SCAN_D                 0x20
#define SCAN_F                 0x21
#define SCAN_G                 0x22
#define SCAN_H                 0x23
#define SCAN_J                 0x24
#define SCAN_K                 0x25
#define SCAN_L                 0x26
#define SCAN_SEMICOLON         0x27
#define SCAN_APOSTROPHE        0x28
#define SCAN_ACCENT            0x29
#define SCAN_TILDE             0x29 // Duplicate of SCAN_ACCENT with popular Tilde name.
#define SCAN_LEFT_SHIFT        0x2A
#define SCAN_BACK_SLASH        0x2B
#define SCAN_Z                 0x2C
#define SCAN_X                 0x2D
#define SCAN_C                 0x2E
#define SCAN_V                 0x2F
#define SCAN_B                 0x30
#define SCAN_N                 0x31
#define SCAN_M                 0x32
#define SCAN_COMMA             0x33
#define SCAN_PERIOD            0x34
#define SCAN_FORWARD_SLASH     0x35
#define SCAN_RIGHT_SHIFT       0x36
#define SCAN_KP_STAR           0x37
#define SCAN_KP_MULTIPLY       0x37 // Duplicate of SCAN_KP_STAR
#define SCAN_LEFT_ALT          0x38
#define SCAN_SPACE             0x39
#define SCAN_CAPS_LOCK         0x3A
#define SCAN_F1                0x3B
#define SCAN_F2                0x3C
#define SCAN_F3                0x3D
#define SCAN_F4                0x3E
#define SCAN_F5                0x3F
#define SCAN_F6                0x40
#define SCAN_F7                0x41
#define SCAN_F8                0x42
#define SCAN_F9                0x43
#define SCAN_F10               0x44
#define SCAN_NUM_LOCK          0x45
#define SCAN_SCROLL_LOCK       0x46
#define SCAN_KP_7              0x47
#define SCAN_KP_8              0x48
#define SCAN_KP_9              0x49
#define SCAN_KP_MINUS          0x4A
#define SCAN_KP_4              0x4B
#define SCAN_KP_5              0x4C
#define SCAN_KP_6              0x4D
#define SCAN_KP_PLUS           0x4E
#define SCAN_KP_1              0x4F
#define SCAN_KP_2              0x50
#define SCAN_KP_3              0x51
#define SCAN_KP_0              0x52
#define SCAN_KP_PERIOD         0x53

// Extended keys for the IBM 101-key Model M keyboard.
#define SCAN_RIGHT_ALT         0xE038
#define SCAN_RIGHT_CONTROL     0xE01D
#define SCAN_LEFT_ARROW        0xE04B
#define SCAN_RIGHT_ARROW       0xE04D
#define SCAN_UP_ARROW          0xE048
#define SCAN_DOWN_ARROW        0xE050
#define SCAN_NUMPAD_ENTER      0xE01C
#define SCAN_INSERT            0xE052
#define SCAN_DELETE            0xE053
#define SCAN_HOME              0xE047
#define SCAN_END               0xE04F
#define SCAN_PAGE_UP           0xE049
#define SCAN_PAGE_DOWN         0xE051
#define SCAN_KP_FORWARD_SLASH  0xE035
#define SCAN_PRINT_SCREEN      0xE02AE037

#endif // KEYBOARD_SCAN_CODES_H_INCLUDED

Upvotes: 2

CrizerPL
CrizerPL

Reputation: 287

pressing on the arrows keys shoots two Keyboard interrupts ? ( int 09h ) The implementation in this question works just fine, so if anyone for some reason wants a ready function for this, here you go:

unsigned char read_scancode() {
    unsigned char res;
    _asm {
        in al, 60h
        mov res, al
        in al, 61h
        or al, 128
        out 61h, al
        xor al, 128
        out 61h, al
    }
    return res;
}

(EDIT: corrected char to unsigned char so putting this function's return value in "if" statements with things like scancode & 0x80 actually works)

When a key is pressed, it returns one of the scancodes listed there http://www.ctyme.com/intr/rb-0045.htm and when it's released it returns the same scancode but ORed with 80h.

If you actually run this in a game loop you'll eventually overflow the BIOS keyboard buffer and the computer will beep at you. A way to free the keyboard buffer is of course while(kbhit()) getch(); but since we are on 286 realmode and we have all of our hardware to f*ck with, here's a more low-level solution:

void free_keyb_buf() {
    *(char*)(0x0040001A) = 0x20;
    *(char*)(0x0040001C) = 0x20;
}

If you're looking for explanation how and why does it work, here you go:

The BIOS keyboard buffer starts at 0040:001Ah and looks like this: 2-byte "head" pointer, 2-byte "tail" pointer and 32 bytes of 2-byte scancodes. The "tail" pointer indicates where to start reading from the keyboard buffer, the "head" pointer indicates where to stop. So by setting both of these to 0x20 (so they actually point to 0040:0020h) we basically trick the computer into thinking that there are no new keystrokes ready for extraction.

Upvotes: 6

Ross Ridge
Ross Ridge

Reputation: 39621

There is no function provided by Turbo C++, MS-DOS or the BIOS that corresponds to Windows function GetAsyncKeyState. The BIOS only keeps track of which modifier keys (Shift, Ctrl, or Alt) are held down, it doesn't track any of the other keys. If you want to do that you need to talk to the keyboard controller directly and monitor the make (key pressed) and break (key released) scan codes it receives from the keyboard.

To do that you'll need to hook the keyboard interrupt (IRQ 1, INT 0x09), read the scancodes from the keyboard controller and then update your own keyboard state table.

Here's a simple program that demonstrates how do this:

#include <conio.h>
#include <dos.h>
#include <stdio.h>

unsigned char normal_keys[0x60];
unsigned char extended_keys[0x60];

static void interrupt 
keyb_int() {
    static unsigned char buffer;
    unsigned char rawcode;
    unsigned char make_break;
    int scancode;

    rawcode = inp(0x60); /* read scancode from keyboard controller */
    make_break = !(rawcode & 0x80); /* bit 7: 0 = make, 1 = break */
    scancode = rawcode & 0x7F;

    if (buffer == 0xE0) { /* second byte of an extended key */
        if (scancode < 0x60) {
            extended_keys[scancode] = make_break;
        }
        buffer = 0;
    } else if (buffer >= 0xE1 && buffer <= 0xE2) {
        buffer = 0; /* ingore these extended keys */
    } else if (rawcode >= 0xE0 && rawcode <= 0xE2) {
        buffer = rawcode; /* first byte of an extended key */
    } else if (scancode < 0x60) {
        normal_keys[scancode] = make_break;
    }

    outp(0x20, 0x20); /* must send EOI to finish interrupt */
}

static void interrupt (*old_keyb_int)();

void
hook_keyb_int(void) {
    old_keyb_int = getvect(0x09);
    setvect(0x09, keyb_int);
}

void
unhook_keyb_int(void) {
    if (old_keyb_int != NULL) {
        setvect(0x09, old_keyb_int);
        old_keyb_int = NULL;
    }
}

int
ctrlbrk_handler(void) {
    unhook_keyb_int();
    _setcursortype(_NORMALCURSOR);
    return 0;
}

static
putkeys(int y, unsigned char const *keys) {
    int i;
    gotoxy(1, y);
    for (i = 0; i < 0x30; i++) {
        putch(keys[i] + '0');
    }
}

void
game(void) {
    _setcursortype(_NOCURSOR);
    clrscr();
    while(!normal_keys[1]) {
        putkeys(1, normal_keys);
        putkeys(2, normal_keys + 0x30);
        putkeys(4, extended_keys);
        putkeys(5, extended_keys + 0x30);
    }
    gotoxy(1, 6);
    _setcursortype(_NORMALCURSOR);
}

int
main() {
    ctrlbrk(ctrlbrk_handler);
    hook_keyb_int();
    game();
    unhook_keyb_int();
    return 0;
}   

The code above has been compiled with Borland C++ 3.1 and tested under DOSBox and MS-DOS 6.11 running under VirtualBox. It shows the current state of keyboard a string of 0's and 1's, a 1 indicating that the key corresponding to that position's scan code is being pressed. Press the ESC key to exit the program.

Note that the program doesn't chain the original keyboard handler, so the normal MS-DOS and BIOS keyboard functions will not work while the keyboard interrupt is hooked. Also note that it restores original keyboard handler before exiting. This is critical because MS-DOS won't do this itself. It also properly handles extended keys that send two byte scan codes, which was the problem with the code in the question you linked to in your answer here.

Upvotes: 14

Cody Gray
Cody Gray

Reputation: 244843

Why are you coding your game on su…just kidding!

In MS-DOS, the "API" functions are implemented as interrupt servicers. In x86 assembly language, you use the INT instruction and specify the number of the interrupt that you want to execute. Most of the interrupts require that their "parameters" be set in certain registers before executing the INT. After the INT instruction returns control to your code, its result(s) will have been placed in certain registers and/or flags, as defined by the interrupt call's documentation.

I have no idea how Turbo C++ implements interrupts, since that pre-dates my involvement with programming, but I do know that it allows you to execute them. Google around for the syntax, or check your Turbo C++ documentation.

Knowing that these are interrupts will get you 90% of the way to a solution when you're searching. Ralf Brown compiled and published a famous list of DOS and BIOS interrupt codes. They should also be available in any book on DOS programming—if you're serious about retro-programming, you should definitely consider getting your hands on one. A used copy on Amazon should only set you back a few bucks. Most people consider these worthless nowadays.

Here is a site that lists the sub-functions available for DOS interrupt 21h. The ones that would be relevant to your use are 01, 06, 07, and 08. These are basically what the C standard library functions like getch are going to be doing under the hood. I find it difficult to imagine, but I have heard reports that programmers back in the day found it faster to call the DOS interrupts directly. The reason I question that is that I can't imagine the runtime library implementers would have been so stupid as to provide unnecessarily slow implementations. But maybe they were.

If the DOS interrupts are still too slow for you, your last recourse would be to use BIOS interrupts directly. This might make an appreciable difference in speed because you're bypassing every abstraction layer possible. But it does make your program significantly less portable, which is the reason operating systems like DOS provided these higher level function calls to begin with. Again, check Ralf Brown's list for the interrupt that is relevant to your use. For example, INT 16 with the 01h sub-function.

Upvotes: 12

Related Questions