是的,我指的是真正的MS-DOS,而不是Windows的cmd.exe
shell控制台。
在MS-DOS中,是否有一种类似于WinAPI中的GetAsyncKeyState()
函数的方法来检测按键是否按下?
目前我正在使用kbhit()
和getch()
,但是它们非常慢,第一个字符后会有延迟,不允许同时按下多个键等问题。
我正在使用Turbo C++ 3.1。有人能帮忙吗?
(顺便说一句,不要问我为什么在这样一个古老的系统上编写游戏)
是的,我指的是真正的MS-DOS,而不是Windows的cmd.exe
shell控制台。
在MS-DOS中,是否有一种类似于WinAPI中的GetAsyncKeyState()
函数的方法来检测按键是否按下?
目前我正在使用kbhit()
和getch()
,但是它们非常慢,第一个字符后会有延迟,不允许同时按下多个键等问题。
我正在使用Turbo C++ 3.1。有人能帮忙吗?
(顺便说一句,不要问我为什么在这样一个古老的系统上编写游戏)
Turbo C++、MS-DOS或BIOS没有提供与Windows函数GetAsyncKeyState
对应的功能。 BIOS仅跟踪按下哪些修饰键(Shift、Ctrl或Alt),它不跟踪其他任何键。如果您想要做到这一点,需要直接与键盘控制器通信,并监视键盘接收到的按下和释放扫描码。
为此,您需要挂接键盘中断(IRQ 1,INT 0x09),从键盘控制器读取扫描码,然后更新自己的键盘状态表。
以下是演示如何实现此操作的简单程序:
#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;
}
以上代码已使用Borland C++ 3.1编译,并在VirtualBox下运行的DOSBox和MS-DOS 6.11中进行了测试。它显示了键盘的当前状态,即一串0和1的字符串,1
表示按下了与该位置扫描码对应的键。按ESC键退出程序。
请注意,程序不会链接原始键盘处理程序,因此在钩住键盘中断时,正常的MS-DOS和BIOS键盘功能将无法使用。还要注意,在退出之前,它会恢复原始的键盘处理程序。这很关键,因为MS-DOS本身不会这样做。它还正确处理发送两个字节扫描码的扩展键,这是您在此回答中链接到的问题代码的问题所在。
我开个玩笑,请问为什么你要在su上编写游戏...
在MS-DOS中,“API”函数是作为中断服务程序实现的。在x86汇编语言中,您可以使用INT
指令并指定要执行的中断号。大多数中断需要在执行INT
之前将其“参数”设置在某些寄存器中。当INT
指令将控制返回到您的代码时,它的结果将被放置在特定的寄存器和/或标志中,如中断调用文档所定义。
我不知道Turbo C++如何实现中断,因为那是在我涉足编程之前的事情,但我知道它允许您执行它们。请在Google上查找语法或检查Turbo C++文档。
当你在搜索时,知道这些是中断会让你解决问题的90%。 Ralf Brown编译并发布了一个著名的DOS和BIOS中断代码列表。 如果你认真对待复古编程,它们也应该可以在任何一本DOS编程书籍中找到,你应该考虑去获取一本。在亚马逊上买一本二手书只需要花费几美元。大多数人现在认为这些已经没有什么用处了。
这里有一个网站列出了DOS中断21h可用的子功能。对于你使用的相关功能来说,01
、06
、07
和08
是最重要的。这些基本上就是C标准库函数getch
在底层执行的操作。我觉得很难想象,但我听说过程序员们在那个年代通过直接调用DOS中断来加速处理。我质疑的原因是,我无法想象运行时库实现者会如此愚蠢地提供不必要的缓慢实现。但也许他们确实这样做了。
INT 16
和01h
子功能。按箭头键会触发两个键盘中断?(int 09h) 这个问题中的实现很好,如果有人因某种原因想要一个现成的函数,请看这里:
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;
}
(编辑:将char更正为unsigned char,以便将此函数的返回值放入类似于scancode & 0x80
的“if”语句中实际起作用)
当按下一个键时,它会返回其中之一的扫描码,列在这里http://www.ctyme.com/intr/rb-0045.htm ,当它被释放时,它返回相同的扫描码,但与80h进行OR运算。
如果你在游戏循环中实际运行这个函数,你最终会溢出BIOS键盘缓冲区,计算机会发出哔哔声。当然,一种释放键盘缓冲区的方法是while(kbhit()) getch();
,但由于我们处于286实模式并且有所有硬件可以操作,这里有一个更低级别的解决方案:
void free_keyb_buf() {
*(char*)(0x0040001A) = 0x20;
*(char*)(0x0040001C) = 0x20;
}
如果你想知道它是如何工作的以及为什么会这样,请看这里:
BIOS键盘缓冲区从0040:001Ah
开始,并且长成这样:2字节的“头”指针,2字节的“尾”指针和32字节的2字节扫描码。 “尾”指针指示从键盘缓冲区开始读取的位置,“头”指针指示停止的位置。 因此,通过将它们都设置为0x20
(因此它们实际上指向0040:0020h
),我们基本上欺骗计算机认为没有准备好提取的新按键。
这是我从键盘获取输入的代码: (它并没有完全针对所有可能的按键和某些其他事情进行完成,但对于大多数程序和游戏来说它工作得非常好。)
此外,还有一些处理“扩展”键的代码。扩展键的常规扫描码前缀为0xE0。甚至还有更多疯狂的细节,所以我不会涵盖它,但是这是基本可行的代码。
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);
}
}
#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