如何在按下键盘时防止重复字符出现

4

我想学习如何防止键盘在DOS下向屏幕和scanf发送多个字符。我正在使用内联汇编的Turbo-C。

如果键盘输入的字符是:

mmmmmmmmyyyyy nnnnnaaaaammmmmmeeeeee iiiiiissss HHHHaaaaiiiimmmm

则在控制台上看到并由scanf处理的字符应为:

my name is Haim

基本的输出来自我不能触摸的C代码。我必须实现eliminate_multiple_pressuneliminate_multiple_press,而不触碰中间的代码。enter image description here

到目前为止,我写的Turbo-C代码如下:

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

volatile char key;
volatile int i=0;
void interrupt (*Int9save) (void);

void interrupt kill_multiple_press()
{
 asm{
     MOV AL, 0
     MOV AH,1
     INT 16h
     PUSHF
     CALL DWORD PTR Int9save
     MOV AX,0
 }

 asm{
  JZ notSet
  MOV key, AL
  MOV AH, 04H
  INT 16H

 }
 notSet:
 //I am not sure what to do from here...............
  I also know that it should be related to the zero flag, but what I          
  wrote so far didn`t effect on multiple characters.
}

void eliminate_multiple_press()
{
 Int9save=getvect(9);
 setvect(9,kill_multiple_press);
}

void uneliminate_multiple_press()
{
  setvect(9,Int9save);
}

void main()
{
  char str[10000]="";
  clrscr();
  eliminate_multiple_press();
  printf("Enter your string: ");
  scanf("%s",&str);
  printf("\n%s",str);
  uneliminate_multiple_press();
 }

我得到的与解决方案相关的信息是可以在这个链接中找到的键盘BIOS例程:

我遇到的问题可能与不知道在标签notSet处该怎么做有关。解决方案似乎与使用缓冲区和寄存器AX(特别是AL)有关,但我真的不知道如何使scanf获取我需要的结果。有人有任何想法如何完成这段代码以达到期望的效果吗?


你是否按下键长达足够的时间以启动键重复功能?(某些BIOS允许您配置键重复延迟/速率)。如果这是正常的键重复,而您想避免它,则有两个选项:禁用键重复或使用获取按键/松开事件的不同接口。这两种情况可能都有BIOS调用;请搜索http://www.ctyme.com/rbrown.htm。 - Peter Cordes
古老的x86中断16h函数01h是一个BIOS调用,用于读取键盘状态。如果按下了键,该函数将会返回扫描码和其ASCII字符分别在AHAL寄存器中,并且Z标志位清零。这或多或少相当于MS C函数库中的kbhit()函数。但它并不会读取键盘,状态会一直保持,直到通过中断16h函数00h读取按键,这或多或少相当于MS C函数库中的getch()函数。这可能解释了为什么您从您认为的一个按键中收到多个值。 - Weather Vane
我还是不明白...如果全局字符与AL寄存器的值不相等(这意味着我按下按钮并立即释放它),我将其与全局字符进行比较并替换。如果字符等于AL值,我就不需要触碰全局字符。这个操作如何防止scanf获取双重字符????这里似乎缺少了什么..... - Haim
你为什么一开始要使用 Turbo C? - n. m.
1
@n.m. 你是因为技术原因(如C标准)反对TurboC本身,还是反对教授16位x86代码?我更喜欢选择Watcom-C进行16位开发(我仍然为客户的80186硬件开发)。作为学习工具,Turbo-C IDE并不差,尽管与Watcom相比,优化器留下了一些问题。教育机构通常在模拟器或旧硬件上教学,特别是在发展中国家。学术界也可能更喜欢使用经过验证的材料进行教学。如果使用Turbo-C符合所教授的课程材料,我认为没有任何问题。 - Michael Petch
显示剩余7条评论
1个回答

2
BIOS、DOS和C库(包括scanf)可以使用多个层次的缓冲区。当计算机启动时,中断向量表被修改为指向IRQ1/INT 9h(外部键盘中断),以便BIOS例程处理键入的字符。在最低级别上,通常会在BIOS数据区(BDA)中维护一个32字节的循环缓冲区来跟踪字符。您可以使用Int 16h BIOS调用与此低级别键盘缓冲区交互。如果您在中断时从BIOS键盘缓冲区中删除字符,则DOS和C库的scanf例程将永远看不到它们。
BIOS/中断级别消除重复字符的方法:
这似乎是为了通过拦截键盘输入的IRQ1中断并丢弃重复字符,从而消除所有在scanf函数中输入的重复字符的练习。一个新的键盘中断处理程序的思路是在DOS(最终是scanf)看到之前就消除掉这些重复字符。
  • 在变量中跟踪前一个按下的字符
  • 调用原始(保存的)中断9,以便BIOS更新键盘缓冲区和键盘标志,使其符合DOS的期望。
  • 使用Int 16h/AH=1h查询键盘是否有字符可用。如果没有字符可用,则零标志(ZF)将被设置,如果有一个字符可用,则清除。此键盘BIOS调用窥视键盘缓冲区的开头,而不实际删除下一个字符。
  • 如果有字符可用,则将其与前一个字符进行比较。
    • 如果它们不同,则使用当前字符更新前一个字符并退出
    • 如果它们相同,则使用Int 16h/AH=0h从键盘缓冲区中删除重复字符并退出

代码的Turbo-C 3.0x版本4:

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

volatile char key = 0;
void interrupt (*Int9save)(void);

void interrupt kill_multiple_press(void)
{
    asm {
     PUSHF
     CALL DWORD PTR Int9save       /* Fake an interrupt call to original handler */

     MOV AH, 1                     /* Peek at next key in buffer without removing it */
     INT 16h                     
     JZ noKey                      /* If no keystroke then we are finished */
                                   /*     If ZF=1 then no key */

     CMP AL, [key]                 /* Compare key to previous key */
     JNE updChar                   /*     If characters are not same, update */
                                   /*     last character and finish */

     /* Last character and current character are same (duplicate)
      * Read keystroke from keyboard buffer and throw it away (ignore it)
      * When it is thrown away DOS and eventually `scanf` will never see it */
     XOR AH, AH                    /* AH = 0, Read keystroke BIOS Call */

     INT 16h                       /* Read keystroke that has been identified as a */
                                   /*     duplicate in keyboard buffer and throw away */
     JMP noKey                     /* We are finished */
    }
updChar:
    asm {
     MOV [key], AL                 /* Update last character pressed */
    }
noKey:                             /* We are finished */
}

void eliminate_multiple_press()
{
    Int9save = getvect(9);
    setvect(9, kill_multiple_press);
}

void uneliminate_multiple_press()
{
    setvect(9, Int9save);
}

void main()
{
    char str[1000];
    clrscr();
    eliminate_multiple_press();
    printf("Enter your string: ");
    /* Get a string terminated by a newline. Max 999 chars + newline */
    scanf("%999[^\n]s", &str);
    printf("\n%s", str);
    uneliminate_multiple_press();
}

笔记

  • 1在键盘中断处理程序中,您要避免任何会阻塞等待键盘输入的键盘BIOS调用。如果使用Int 16h / AH = 0,请确保使用Int 16h / AH = 1先检查是否有字符可用,否则Int 16h / AH = 0将会阻塞,等待另一个字符到达。
  • 2删除重复字符并不等同于禁用键盘重复率。
  • 3因为重复字符在DOS例程看到它们之前已被删除(以及依赖于DOS的函数如scanf),所以它们永远不会被scanf看到。
  • 4某些修改可能需要进行以兼容Turbo-C 3.0x之外的版本。
  • 5此方法仅适用于scanf将间接进行BIOS调用以保持键盘缓冲区清除的情况。在键盘按键可能由BIOS缓冲的所有通用情况下,此代码不起作用。为了解决这个问题,键盘中断例程必须删除键盘缓冲区中的所有重复项,而不仅是此代码所做的头部重复项。
  • 6每个按键在BIOS键盘缓冲区(在BDA中)占用2个字节的空间。32个字节中的2个字节丢失,因为它们用于检测键盘缓冲区是否已满或为空。这意味着BIOS可以缓冲的最大按键数为15。

真的有效了!非常感谢您!但是,我仍然看不出哪一行代码防止了重复?我的意思是,关键变量如何影响输入?它真的必要吗?无论如何,还是非常感谢您! - Haim
@Haim:在代码注释中 XOR AH, AH /* AH = 0 */INT 16h /* Read keystroke and throw it away,读取了键盘缓冲区中已被识别的重复字符,并通过从键盘缓冲区中简单地删除它来有效地将其丢弃。它已经在 DOS 和 scanf 看到它之前的一个很早的时刻被移除了。BIOS 键盘缓冲区是由 IRQ1(中断9)放置字符的更低级别的位置,比 scanf 更低。 - Michael Petch
@Haim 如果你将 XOR AH, AHINT 16h 注释掉,你应该会发现所有的重复项都会出现。 - Michael Petch
1
鉴于kill_multiple_press是一个中断处理程序,它只能依赖于CS段正确。因此,我希望看到:CMP AL,[CS:key]MOV [CS:key],AL。而且CALLF也需要它吗?那么Turbo-C 3.0x是否会秘密地管理所需的段寄存器(包括保留它们)? - Fifoernik
1
@Fifoernik:这是一个好问题,答案是否定的。我在SO上写过其他键盘例程,它们明确使用CS(并且我将程序编写为微小代码模型,其中CS=DS)。那么问题是,为什么它们在这里明显缺失?这是因为代码是内联汇编,与GCC不同-Turbo-C实际上会生成所需的代码来根据变量更改段,如果函数标记为“interrupt”。 - Michael Petch
@Fifoernik:我从TCC中转储了这段代码生成的汇编,并将其放置在我的Web服务器上,网址为http://www.capp-sysware.com/misc/stackoverflow/51911808/kbddup.asm。中断处理程序生成的代码将_DS_设置为_DGROUP_,其中`Int9save`和`key`驻留。DOS当然会在加载时修复该段。 - Michael Petch

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接