如何在汇编 8086 中产生键盘中断

How to generate a keyboard interrupt in assembly 8086

我正在尝试编写一个程序来识别键盘并在按下某个键时在屏幕上打印一个字母(不是我按下的那个)。我设法制作了一个识别键盘并打印字母的程序,但是,它会多次打印字母,具体取决于它生成的键超过两个(例如 printscreen 键和删除键)。我怀疑它可能是键盘缓冲区,已经尝试清理它但没有成功。我现在的代码是:

.code16                   
.text                  
.globl _start;

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

    # Ensure we are starting from the beginning of the interrupt vector
    movw [=10=]x0, %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 [=10=]x204,%bx         # Going to address 0x204 -> 512 in the interruput vector  200 <- 80 * 4bytes, 204 <- 200 + 4bytes 
    movw [=10=]x7d00, (%bx)     # Putting the address 0x7d00 in the %bx addres, 0x7d00 = = _start addres + 256

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

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

    movb [=10=]xfd, %al     # Sending OCW1 in the first PIC
    outb %al, [=10=]x21
    movb [=10=]xff, %al     # Sending OCW1 in the second PIC
    outb %al, [=10=]xa1


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

    movb [=10=]x20, %al    # Sending OCW2 in the first PIC -> EOI
    outb %al, [=10=]x20

.checkKeyboard:
    in [=10=]x64, %al
    and [=10=]x1, %al
    jz .emptyKeyboard
    in [=10=]x60, %al
    jmp .fullKeyboard

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

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

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

非常感谢任何一段代码或指示

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)

这很正常。键盘发送变长扫描码;并且(取决于键盘的配置方式,或者在您的情况下未配置)可以在按下键时发送 4 个字节,然后在释放键时发送另外 4 个字节。

理想情况下,您需要某种状态机将可变长度的扫描码转换为固定大小的“键码”;其中状态类似于“等待扫描码的第一个字节”、“在已经收到 0xE0 字节后等待扫描码的第二个字节”等。另外不要忘记键盘可以发送不属于任何内容的内容扫码;像“我确认你的最后一个命令”,“出了点问题,请重新发送最后一个命令”和“你好,我刚刚(重新)插入”;它可以绕过您的状态机或使其重置。

那个“关键代码”是你想要的任何东西。我个人喜欢将其设为反映密钥物理位置的 16 位整数的想法(例如 key_code = (row << 8) | column 可能)。

一旦你得到某种固定大小的“键码”,你就可以将它用作 tables 的索引来确定键是什么,以及是否有相应的 ASCII 字符或相应的Unicode 代码点序列,以及是否有特殊行为(如果它应该控制 LED,如果它是某种 meta-key,等等)。固定大小的“关键代码”的主要原因是可变长度扫描代码对于查找 tables 很糟糕(你不希望 table 具有 40 亿个条目来支持 4 字节扫描代码).

请注意(为了支持不同的键盘布局、国际化等)所有这些都应该使用从“键盘布局文件”加载的 tables。即使您忽略了世界上大部分地区并且只支持一种键盘布局(例如“US Qwerty”),您也会发现不同的键盘具有不同的 non-standard 扩展名(例如 multi-media 键等)。此外,普通软件(应用程序、游戏等)也必须处理这种可变性;一个好的 OS 应该允许软件进行反向查找(例如询问 OS “对于第三个键盘,第 0 行第 4 列的键是什么?”并获取信息,可能包括文本OS 中的“增加扬声器音量”之类的描述)。从 scan-code 到 key-code 的转换也意味着完全不同类型的键盘(例如 USB 键盘)可以将它们从键盘获得的任何内容转换为相同的“键码”,并使用相同的键盘布局文件.

您可能还应该知道,有多个扫描码集,默认为“扫描码集2”,其他扫描码可能不支持; PC 中传统的“8042”键盘控制器(而非键盘)能够将“扫描码集 2”(来自键盘)转换为“扫描码集 1”,以便向后兼容 1970 年代的旧垃圾;并且(对于 BIOS)此扫描代码转换默认为第一个 PS/2 端口启用(第二个 PS/2 端口不存在)。这些事情的结果意味着你的键盘驱动程序“可能必须”支持扫描代码集 2(以支持键盘插入第二个 PS/2 端口),因此应该禁用“8042”控制器完成的转换(所以键盘驱动不需要支持扫描码集1)。

您应该注意的另一件事是大多数现代计算机都使用 USB 键盘;固件可能(对于 BIOS)或可能不会(对于 UEFI)有烦人的狡猾垃圾来尝试模拟 PS/2 键盘。这种仿真是出了名的不可靠,can/will 阻止了“8042”控制器的正确初始化;你应该枚举 PCI 设备,找到 USB 控制器,禁用任何遗留的“PS/2 仿真”废话;然后正确初始化“8042”控制器(然后启动您的 PS/2 键盘驱动程序)。

终于;键盘控制器驱动程序应该与键盘驱动程序分开;和键盘控制器的驱动程序 can/should auto-detect 引导期间插入的设备类型以及插入设备时的类型。例如如果计算机有“端口 1 中的条形码扫描仪,端口 2 中的鼠标”并且用户拔下鼠标并插入键盘(导致“端口 2 中的键盘”);然后键盘控制器驱动程序应该安排停止鼠标驱动程序并启动键盘驱动程序。键盘驱动程序不应该接触任何 IO 端口,并且只应该代表它向控制器驱动程序请求 send/receive 字节。控制器驱动程序可以启动同一键盘驱动程序的 2 个实例(例如,如果它是“第一个端口中的键盘,第二个端口中的另一个键盘”);并且同一键盘驱动程序的不同实例可能使用不同的“键盘布局文件”。


在一个不相关的matte上;我还建议定义一个“引导期间 OS 控制硬件的点”。在此之前,您不应该破坏固件的硬件配置,而应该依赖 BIOS 函数(例如“int 016 with ah=0, get keystroke”函数);在此之后,您将负责所有硬件并可以重新配置您喜欢的任何东西,但不应期望任何 BIOS 功能都能正常工作。

您不能保证弄乱一个硬件不会破坏其他 BIOS 功能。 (并且不能保证“BIOS int 0x10”没有时间延迟,它恰好依赖于定时器 IRQ,而定时器 IRQ 恰好依赖于 PIC 芯片的配置)。