BIOS中断号大全 bios中断设置_保护模式


保护模式,虽然让我们的操作系统进入了更加丰富的世界,但是也对我们使用计算机带来了更多的不便,比如一个突出的问题就是实模式的BIOS中断已经完全失效了。BIOS中断可以说得上是实模式下的开发利器,我们要在屏幕上显示字符、接收键盘的输入字符以及从磁盘读取数据等,都只需要一条指令:INT XXH即可,剩下的事情就是坐享其成,就算我们一点都不知道BIOS中断底层下面的实现原理。但是现在不行了,我们遇到的第一个问题就是如何驱动键盘输入,让操作系统能和用户互动。

一、中断概念

要让键盘成功驱动起来,需要引入一个新的概念:中断。中断我们实际并不陌生,因为在计算机上随便打开一个外围设备,都会找到类似端口和IRQ的东西,这个IRQ就是设备的中断端口号,在XP系统上,会有中文的“中断端口号”文字标记。


BIOS中断号大全 bios中断设置_服务程序_02


那么什么是中断呢?这个从字面意思都能理解,不再过多解释。中断是计算机的灵魂,正是有了中断才让计算机具备“消息机制”,它是计算机在任何时候都能够马上响应用户的一切需求。可以说没有中断,就没有计算机应用。

在刚开始接触实模式的时候,我第一次听说“BIOS中断”这个说法简直觉得不可以理解。因为凭借我多年的计算机硬件开发经验,中断一定是有外围和内部(比如时钟)设备触发的行为,怎么我们主动去屏幕显示一个字符也能叫“中断”了呢?后来才明白,其实这种BIOS中断可以理解为BIOS系统为用户提供的一些封装好了的“API”而已,它之所以也被叫中断,原因在于这部分“API”的运转模式是采用的“中断机制”:中断向量+中断服务程序。所以,可以理解BIOS中断其实是一种“假中断”,我们更多的时候喜欢叫“软中断”。

但是今天,我们即将要做的键盘驱动,才是真正意义上的中断---硬中断,是由实实在在的硬件动作发起的中断。

二、中断机制

如果现在就把教科书上的中断机制理论摆上,容易让人头晕。我现在只需用几点推断就可以把中断机制讲清楚。

(一) 中断向量---中断号

计算机并不知道我们什么时候会按下键盘,因此它会一直在等待。一般我们操作的时候,又是按鼠标又是手写屏又是敲键盘等,计算机怎么来区分是哪个设备再动了呢?那就给每个设备都编个号,这个号就叫中断号,中断号是CPU识别中断来源的唯一标记。注意IRQXX不叫中断号,它叫中断端口号,这是两个不同的概念,通过下一小节会明白。教科书上一般把中断号叫中断向量,因为它在国外的教材中用的单词是:Vector。

(二) 中断控制芯片8259A

每个设备的中断号在出厂的时候会有个默认的值,但是我们还可以通过编程来自定义每个设备的中断号,这个可编程的东西就是下面两块中断控制芯片:8259A。出厂的时候,每个设备的中断号和中断芯片的端口号一致的。如下图:键盘中断是挂结在端口IRQ1上的,它就表示键盘的中断号就是1,所以可以看到,默认情况下这些设备的中断号范围分别是0-7和8-15。


BIOS中断号大全 bios中断设置_龙芯2k按键中断驱动_03


但是做为CPU厂商的INTEL公司和主板厂商的IBM公司不知道怎么沟通的,在保护模式下,INTEL设计的CPU把0-20号的中断号给占用了,具体分配名单如下:


BIOS中断号大全 bios中断设置_保护模式_04


你说这不是扯的吗?IBM公司只有自己调整来适应CPU,所以我们就必须通过8259A编程来对每个设备的中断号进行重新设置,可以从20号开始。

(三) 中断向量描述符IDT

计算机通过8259A拿到每个设备的中断号之后,要对其进行信号处理,这个信号处理就是中断程序或者中断服务程序。当鼠标、键盘、硬盘等外围设备一起在叫唤的时候,怎么应付得过来? 于是计算机又用了比较老套的方法:为每一个设备(中断号)建立一个专门的映射关系:表。用这个表就可以映射出每个设备的中断号和中断程序地址的一、一对应关系。


BIOS中断号大全 bios中断设置_服务程序_05

IDT逻辑

看到没?这个解决思路和我们在“计算机自制操作系统(十):32位保护模式”中GDT的出现一模一样!所以接下来的事情就是顺理成章了:机制完全等同于GDT。那么这个表名称就叫中断描述符IDT,它的数据结构定义为:


BIOS中断号大全 bios中断设置_龙芯2k按键中断驱动_06

IDT结构

那么GDT在之前是怎么实现的?通过GDTR。同样的过程,IDT也需要一个IDTR。结构:


BIOS中断号大全 bios中断设置_BIOS中断号大全_07

IDTR结构

(四)中断服务程序

但是中断程序的寻址过程要比GDT多一个步骤,因为在上面表IDT中的“中断程序地址”0xXXXX不是直接的内存地址,它是个间接指向中断程序的逻辑地址。那么由这个逻辑地址如何翻译到真实的内存地址呢?这个就需要动用GDT了嘛,这个寻址过程实际上就是刚进入保护模式的时候,代码段的寻址过程(通过指定CS和32位偏移)。综合所有过程,最终通过如下方法找到中断服务程序:


BIOS中断号大全 bios中断设置_中断向量_08

CPU中断处理寻址全过程

(五)中断流程

综上,我们要为某一设备建立起中断机制,需用的标准流程如下:

1.初始化8259A:为设备分配中断号。

2.建立ITD表

2.1 定位ITD:通过ITDR把ITD描述符表写进内存(定义位置和大小)。

2.2 新建ITD描述符:映射设备中断号和中断服务程序地址之间的关系。

3.编写中断服务程序:这是中断发起的最终归宿,程序需放在应用代码段之内。

中断实现是不是非常的简单?没错。

三、中断实现

我们本章的任务是实现键盘驱动,我们就按上面学习到的中断标准流程来实现这个任务。

首先,我们把之前定义的各个组织和程序再重新做一次规划,完整的结构如下图。彩色部分为自定义的存储区域,主要调整的地方是把堆栈区增加了一些,由原来的0x7c00---0x7a00调整为0x7c00---0x7000。红色部分为本次我们的中断描述表IDT定义区。


BIOS中断号大全 bios中断设置_服务程序_09

X86计算机内存分配示意图

该图囊括了X86计算机的内存分配情况,值得收藏。

(一) 芯片8259A初始化


Init8259A:
mov al, 011h
out 020h, al ; 主8259, ICW1.
call io_delay

out 0A0h, al ; 从8259, ICW1.
call io_delay

mov al, 020h ; IRQ0 对应中断向量 0x20
out 021h, al ; 主8259, ICW2.
call io_delay

mov al, 028h ; IRQ8 对应中断向量 0x28
out 0A1h, al ; 从8259, ICW2.
call io_delay

mov al, 004h ; IR2 对应从8259
out 021h, al ; 主8259, ICW3.
call io_delay

mov al, 002h ; 对应主8259的IR2
out 0A1h, al ; 从8259, ICW3.
call io_delay

mov al, 001h
out 021h, al ; 主8259, ICW4.
call io_delay

out 0A1h, al ; 从8259, ICW4.
call io_delay

mov al, 11111101b ; 开启键盘中断
;mov al, 11111111b ; 屏蔽主8259所有中断
out 021h, al ; 主8259, OCW1.
call io_delay

mov al, 11111111b ;  下一步开启鼠标中断
out 0A1h, al ; 从8259, OCW1.
call io_delay
ret


这里掌握两个关键点:1.就是需要重新设置设备的中断号:IRQ0 对应中断向量 0x20,那么键盘IRQ1对应的中断号是0x21。2.打开键盘中断。

(二) 建立IDT表


idt_size dw 2048-1     ;IDT 表的大小 ;(总字节数减一)
idt_base dd 0x00006000 ;IDT的物理地址

createidt:
lidt    [idt_size]    ;将idt的地址和大小写入idtr生效   默认DS

mov cx,256
mov di,0
idttable:
mov word [es:di],intservice-512   ;中断服务程序偏移地址0_15位:
mov word [es:di+0x02],0x08        ;保护模式下的段选择符 0x8=代码段
mov dword [es:di+0x04],0000000000000000_1_00_01110_00000000b
;偏移16_31位:0;P=1;DPL=0; Saved_1_1_0 : 3 ; // 保留,需设为 110
;P : 1 ; // P 位;DPL : 2 ; // 特权位;Saved_0 : 1 ; // 保留,需设为0 D : 1 ; // D 位
;Saved_1_1_0 : 3 ; // 保留,需设为 110;UnUsed ; // 未使用,须设为全零
add di,8
loop idttable
ret


因为最多256个中断,每个中断描述符长度8B,所以我们设置一个2KB字节的表界限,其地址是0x00006000 。必须注意,由于最终的中断服务程序一定是在我们的代码段内,所以这个IDT中的段选择子就只能是保护模式下的CS,也即0x8。我在这里中断服务程序偏移地址有一个-512的过程,是因为我这次是吧中断服务程序和MBR放在一起编译的,因此需要扣除掉MBR的长度。由于我们本次只是演示一下键盘输入中断能否正常触发,因此把所有256个中断号的中断服务程序都指向了一个统一的内存地址:intservice。

(三)中断服务程序


intservice:
cli ;应禁止中断

mov al,  0x61
out 0x20, al  ;PIC0_OCW2
in  al,  0x60  ;从键盘读入按键扫描码

mov byte [0xb8000+24*160+0x12],al
mov byte [0xb8000+24*160+0x13],0x0c

mov               al , 0x20  ;告诉硬件,中断处理完毕,即发送 EOI 消息
out               0x20 , al
out               0xa0 , al

IRET


这个中断服务程序显然是将键盘输入的扫描码打印在屏幕上。可以看出,它是通过芯片8259A向I/O端口0x61发起请求(这个0x61应该就代表键盘),然后又通过I/O端口0x60才把键盘的输入值读入,这是键盘输入的标准化操作。可以看到进入中断服务程序的时候,为了避免这个时候来新的中断,首先是禁止了中断,理论上讲程序在退出之前应该需要重新开启中断才对啊,为什么这里没有呢?这个问题留给读者取思考。

最终,我们将汇编程序编译之后启动计算机,并按下按键Ctrl的显示效果:首先它显示OK,表明中断处理成功,其次是它打印出了我们的键盘输入值"¥"。


BIOS中断号大全 bios中断设置_服务程序_10


但是奇怪,为什么我们按的键是Ctrl,屏幕怎么会显示出“¥”呢?我们查一下¥的ASCII码:


BIOS中断号大全 bios中断设置_服务程序_11


而9D正是Ctrl键释放的扫描码:


BIOS中断号大全 bios中断设置_保护模式_12

键盘扫描码

所以,本次键盘中断输入算是成功。

(四)任意中断服务程序IDT配置

我们上一节是把所有256个中断源产生的中断在IDT表中注册为指向了同一个中断服务程序,其实是不准确的,因为并没有建立好各中断号和中断服务程序之间的一一对应关系。所有,接来下我们就将中断进行精准定位。这里设置了2个中断:除法(除数为0或除法溢出)错误的内部中断和键盘外部中断。前者的中断号是0x00,后者是0x21。IDT表配置程序如下:


mov ah,0
mov al,0x21        ;注册键盘中断程序
mov bl,8
mul bl
mov di,ax
mov word [es:di],keyservice-512
mov word [es:di+0x02],0x08
mov dword [es:di+0x04],0000000000000000_1_00_01110_00000000b


mov ah,0          ;注册0号除法错误中断程序
mov al,0x00
mov bl,8
mul bl
mov di,ax
mov word [es:di],diverror-512
mov word [es:di+0x02],0x08
mov dword [es:di+0x04],0000000000000000_1_00_01110_00000000b


可以看到,由于一个中断注册需要8个字节,因此采用了中断号*8的定位方式。我们怎么触发除法错误的0号中断呢?可以编写一个除数为0的程序进行验证,其实最直接的方式是用如下指令:int 00h。这就是中断调用,不要以为进入保护模式后,就不能再使用中断调用,INT指令是照样可以执行的,使用它CPU就会转到相应的中断服务程序去执行。通常说的保护模式下不能使用中断调用,指的是不能使用实模式下BIOS功能的那部分中断服务程序而已。因为保护模式下中断向量表指向已经完全变成自定了,而且中断服务程序都是自己编写的,当然不能用之前BIOS厂家提供的。但是,INT中断会将CPU指向中断服务程序入口,这个机制并没有变化。通过程序测试结果正常:产生了除法异常中断,如果按下键盘,还会产生键盘中断。


BIOS中断号大全 bios中断设置_中断向量_13


(五)完整键盘中断演示程序


NUMsector      EQU    6       ; 设置读取到的软盘最大扇区编号()
NUMheader      EQU    0        ; 设置读取到的软盘最大磁头编号(01)
NUMcylind      EQU    0        ; 设置读取到的软盘柱面编号

mbrseg         equ    7c0h     ;启动扇区存放段地址
newseg         equ    800h     ;跳出MBR之后的新段地址
gdtseg         equ    7e0h     ;GDT区段地址
idtseg         equ    600h     ;IDT区段地址


jmp   start
msgwelcome:     db '------Wlecome Jiang Os------','$'
msgstep1:       db 'Step1:now  is   in   mbr','$'
msgmem1:        db 'Memory Address is---','$'
msgcs1:         db 'CS:????H','$'

cylind  db 'Cylind:?? $',0    ; 设置开始读取的柱面编号
header  db 'Header:?? $',0    ; 设置开始读取的磁头编号
sector  db 'Sector:?? $',2    ; 设置从第2扇区开始读
FloppyOK db 'Read OK','$'
Fyerror db 'Read Error' ,'$'

start:

call  inmbr
call  floppyload
jmp   newseg:0        ;通过此条指令跳出MBR区


inmbr:
mov   ax,mbrseg   ;为显示各种提示信息做准备
mov   ds,ax
mov   ax,newseg
mov   es,ax   ;为读软盘数据到内存做准备,因为读软盘需地址控制---ES:BX
call  inmbrshow
call  showcs
call  newline
call  newline
call  newline
ret



inmbrshow:
mov   si,msgwelcome
call  printstr
call  newline
call  newline
mov   si,msgstep1
call  printstr
call  newline
mov   si,msgmem1
call  printstr
ret


printstr:                  ;显示指定的字符串, 以'$'为结束标记
      mov al,[si]
      cmp al,'$'
      je disover
      mov ah,0eh
      int 10h
      inc si
      jmp printstr
disover:
      ret

newline:                     ;显示回车换行
      mov ah,0eh
      mov al,0dh
      int 10h
      mov al,0ah
      int 10h
      ret


showcs:                       ;展示CS的值
      mov  ax,cs

      mov  dl,ah
      call HL4BIT
      mov  dl,  BH
      call  ASCII
      mov  [msgcs1+3],dl

      mov  dl,  Bl
      call  ASCII
      mov  [msgcs1+4],dl

      mov  dl,al
      call HL4BIT
      mov  dl,  BH
      call  ASCII
      mov  [msgcs1+5],dl

      mov  dl,  Bl
      call  ASCII
      mov  [msgcs1+6],dl

      mov  si,  msgcs1
      call printstr

      ret


;-----------------将16进制数字(1位)转换成ASCII码,输入DL,输出DL------
ASCII:   CMP  DL,9
         JG   LETTER  ;DL>OAH
         ADD  DL,30H  ;如果是数字,加30H即转换成ASCII码
         RET
LETTER:  ADD  DL,37H  ;如果是A~F,加37H即转换成ASCII码
         RET

;-----------------取出1个字节Byte的高4位和低4位,输入DL,输出BH和BL----------

HL4BIT:  MOV  DH,dl
         MOV  BL,dl
         SHR  DH,1
         SHR  DH,1
         SHR  DH,1
         SHR  DH,1
         MOV  BH,DH                    ;取高4位
         AND  BL,0FH                   ;取低4位
         RET


floppyload:
     call    read1sector
     MOV     AX,ES
     ADD     AX,0x0020
     MOV     ES,AX                ;一个扇区占512B=200H,刚好能被整除成完整的段,因此只需改变ES值,无需改变BP即可。
     inc   byte [sector+11]
     cmp   byte [sector+11],NUMsector+1
     jne   floppyload             ;读完一个扇区
     mov   byte [sector+11],1
     inc   byte [header+11]
     cmp   byte [header+11],NUMheader+1
     jne   floppyload             ;读完一个磁头
     mov   byte [header+11],0
     inc   byte [cylind+11]
     cmp   byte [cylind+11],NUMcylind+1
     jne   floppyload             ;读完一个柱面

     ret


numtoascii:     ;将2位数的10进制数分解成ASII码才能正常显示。如柱面56 分解成出口ascii: al:35,ah:36
     mov ax,0
     mov al,cl  ;输入cl
     mov bl,10
     div bl
     add ax,3030h
     ret

readinfo:       ;显示当前读到哪个扇区、哪个磁头、哪个柱面
     mov si,cylind
     call  printstr
     mov si,header
     call  printstr
     mov si,sector
     call  printstr
     ret



read1sector:                      ;读取一个扇区的通用程序。扇区参数由 sector header  cylind控制

       mov   cl, [sector+11]      ;为了能实时显示读到的物理位置
       call  numtoascii
       mov   [sector+7],al
       mov   [sector+8],ah

       mov   cl,[header+11]
       call  numtoascii
       mov   [header+7],al
       mov   [header+8],ah

       mov   cl,[cylind+11]
       call  numtoascii
       mov   [cylind+7],al
       mov   [cylind+8],ah

       MOV        CH,[cylind+11]    ; 柱面从0开始读
       MOV        DH,[header+11]    ; 磁头从0开始读
       mov        cl,[sector+11]    ; 扇区从1开始读

        call       readinfo        ;显示软盘读到的物理位置
        mov        di,0
retry:
        MOV        AH,02H            ; AH=0x02 : AH设置为0x02表示读取磁盘
        MOV        AL,1            ; 要读取的扇区数
        mov        BX,    0         ; ES:BX表示读到内存的地址 0x0800*16 + 0 = 0x8000
        MOV        DL,00H           ; 驱动器号,0表示第一个软盘,是的,软盘。。硬盘C:80H C 硬盘D:81H
        INT        13H               ; 调用BIOS 13号中断,磁盘相关功能
        JNC        READOK           ; 未出错则跳转到READOK,出错的话则会使EFLAGS寄存器的CF位置1
           inc     di
           MOV     AH,0x00
           MOV     DL,0x00         ; A驱动器
           INT     0x13            ; 重置驱动器
           cmp     di, 5           ; 软盘很脆弱,同一扇区如果重读5次都失败就放弃
           jne     retry

           mov     si, Fyerror
           call    printstr
           call    newline
           jmp     exitread
READOK:    mov     si, FloppyOK
           call    printstr
           call    newline
exitread:
           ret


times 510-($-$$) db 0
db 0x55,0xaa

;-------------------------------------------------------------------------------
;------------------此为扇区分界线,线上为第1扇区,线下为第2扇区-----------------
;-------------------------------------------------------------------------------

jmp    newprogram


gdt_size dw 32-1     ;GDT 表的大小 ;(总字节数减一)
gdt_base dd 0x00007e00 ;GDT的物理地址

idt_size dw 2048-1     ;IDT 表的大小 ;(总字节数减一)
idt_base dd 0x00006000 ;IDT的物理地址

msgstep2:       db 'Step2:now  jmp  out  mbr','$'
mesmem2:        db 'Will Visit Memory Address is---','$'
msgcs2:         db '0X:????(CS):XXXX','$'

msgstep3:       db 'Step3:now  enter  protect  mode','$'

newprogram:
mov     ax,newseg      ;跳转到新地址8000H之后,全部寄存器启用新的段地址
sub     ax,20h         ;要调整一下DS的值才能正确访问新程序中的数据
mov     ds,ax          ;ds用于打印数据的寻址

call  outmbr
call  showcsnew

call  showprotect



mov     ax,gdtseg
mov     es,ax          ;es用于gdt区寻址    gdt存放起始地址:0x00007e00h
call    creategdt

mov     ax,idtseg
mov     es,ax          ;es用于idt区寻址    ipt存放起始地址:0x00006000h
call    createidt

jmp  next


createidt:
lidt    [idt_size]    ;将idt的地址和大小写入idtr生效   默认DS

mov cx,256
mov di,0
idttable:
mov word [es:di],keyservice-512   ;中断服务程序偏移地址0_15位:
mov word [es:di+0x02],0x08        ;保护模式下的段选择符 0x8=代码段
mov dword [es:di+0x04],0000000000000000_1_00_01110_00000000b
;偏移16_31位:0;P=1;DPL=0; Saved_1_1_0 : 3 ; // 保留,需设为 110
;P : 1 ; // P 位;DPL : 2 ; // 特权位;Saved_0 : 1 ; // 保留,需设为0 D : 1 ; // D 位
;Saved_1_1_0 : 3 ; // 保留,需设为 110;UnUsed ; // 未使用,须设为全零
add di,8
loop idttable


mov ah,0
mov al,0x21        ;注册键盘中断程序
mov bl,8
mul bl
mov di,ax
mov word [es:di],keyservice-512
mov word [es:di+0x02],0x08
mov dword [es:di+0x04],0000000000000000_1_00_01110_00000000b


mov ah,0          ;注册0号除法错误中断程序
mov al,0x00
mov bl,8
mul bl
mov di,ax
mov word [es:di],diverror-512
mov word [es:di+0x02],0x08
mov dword [es:di+0x04],0000000000000000_1_00_01110_00000000b

ret


creategdt:          ;创建DPT子程序

lgdt [gdt_size] ;将gdt的地址和大小写入gdtr生效     默认DS

;创建0#描述符,它是空描述符,这是处理器的要求
mov dword [es:0x00],0x00
mov dword [es:0x04],0x00

;创建#1描述符,保护模式下的代码段描述符
mov dword [es:0x08],0x8000ffff
mov dword [es:0x0c],0x00409800

;创建#2描述符,保护模式下的数据段描述符(文本模式下的显示缓冲区)
;mov dword [es:0x10],0x8000ffff
;mov dword [es:0x14],0x0040920b

mov dword [es:0x10],0x0000ffff  ;(把DS的基地址定义为0)
mov dword [es:0x14],0x00cf9200  ; (标志位G=1,表示以4KB为单位,总界限FFFFF*4KB=4GB

;创建#3描述符,保护模式下的堆栈段描述符
mov dword [es:0x18],0x00007000  ;堆栈区栈顶下限7000
mov dword [es:0x1c],0x00409600
ret





outmbr:
call  newlinenew
call  newlinenew
mov   si,msgstep2
call  printstrnew
call  newlinenew
mov   si,mesmem2
call  printstrnew
ret


showprotect:
call  newlinenew
call  newlinenew
mov   si,msgstep3
call  newlinenew
call  printstrnew
call  newlinenew
ret


showcsnew:                       ;展示CS的值
      mov  ax,cs

      mov  dl,ah
      call HL4BITnew
      mov  dl,  BH
      call  ASCIInew
      mov  [msgcs2+3],dl

      mov  dl,  Bl
      call  ASCIInew
      mov  [msgcs2+4],dl

      mov  dl,al
      call HL4BITnew
      mov  dl,  BH
      call  ASCIInew
      mov  [msgcs2+5],dl

      mov  dl,  Bl
      call  ASCIInew
      mov  [msgcs2+6],dl

      mov  si,  msgcs2
      call printstrnew

      ret

printstrnew:                  ;显示指定的字符串, 以'$'为结束标记
      mov al,[si]
      cmp al,'$'
      je disovernew
      mov ah,0eh
      int 10h
      inc si
      jmp printstrnew
disovernew:
      ret

newlinenew:                     ;显示回车换行
      mov ah,0eh
      mov al,0dh
      int 10h
      mov al,0ah
      int 10h
      ret

;-----------------将16进制数字(1位)转换成ASCII码,输入DL,输出DL------
ASCIInew:   CMP  DL,9
         JG   LETTERnew  ;DL>OAH
         ADD  DL,30H  ;如果是数字,加30H即转换成ASCII码
         RET
LETTERnew:  ADD  DL,37H  ;如果是A~F,加37H即转换成ASCII码
         RET

;-----------------取出1个字节Byte的高4位和低4位,输入DL,输出BH和BL----------

HL4BITnew:  MOV  DH,dl
         MOV  BL,dl
         SHR  DH,1
         SHR  DH,1
         SHR  DH,1
         SHR  DH,1
         MOV  BH,DH                    ;取高4位
         AND  BL,0FH                   ;取低4位
         RET


next:
in al,0x92    ;打开A20地址线
or al,0000_0010B
out 0x92,al

cli ;保护模式下中断机制尚未建立,应禁止中断

mov eax,cr0  ;打开保护模式开关
or eax,1
mov cr0,eax

;进入保护模式... ...
jmp dword 0x0008:inprotectmode-512 ;16位的描述符选择子:32位偏移;这里需要扣除掉512B的MBR偏移量

[bits 32]
inprotectmode:

;在屏幕上显示"Protect mode",验证保护模式下的数据段设置正确
mov ax,00000000000_10_000B ;加载数据段选择子(0x10)
mov ds,ax
call  protshow

;通过堆栈操作,验证保护模式下的堆栈段设置正确
mov ax,00000000000_11_000B ;加载堆栈段选择子
mov ss,ax                  ;7000-7c00为此次设计的堆栈区
mov esp,0x7c00             ;7c00固定地址为栈底,
                           ;7000为栈顶的最低地址(通过载堆栈段选择子的段界限值设置)
mov  ebp,esp ;保存堆栈指针
push byte '#' ;压入立即数#(字节)后,执行push指令,esp会自动减4
sub ebp,4
cmp ebp,esp ;判断ESP是否减4
jnz over    ;如果堆栈工作正常则打印出pop出来的值和其它字符
pop eax
call stackshow


call Init8259A
sti ;开中断

over:

int 00h

jmp $

protshow:
mov byte [0xb8000+20*160+0x00],'P'  ;屏幕第20行开始显示
mov byte [0xb8000+20*160+0x01],0x0c
mov byte [0xb8000+20*160+0x02],'R'
mov byte [0xb8000+20*160+0x03],0x0c
mov byte [0xb8000+20*160+0x04],'O'
mov byte [0xb8000+20*160+0x05],0x0c
mov byte [0xb8000+20*160+0x06],'T'
mov byte [0xb8000+20*160+0x07],0x0c
mov byte [0xb8000+20*160+0x08],'E'
mov byte [0xb8000+20*160+0x09],0x0c
mov byte [0xb8000+20*160+0x0a],'C'
mov byte [0xb8000+20*160+0x0b],0x0c
mov byte [0xb8000+20*160+0x0c],'T'
mov byte [0xb8000+20*160+0x0d],0x0c
mov byte [0xb8000+20*160+0x0e],'-'
mov byte [0xb8000+20*160+0x0f],0x0c
mov byte [0xb8000+20*160+0x10],'M'
mov byte [0xb8000+20*160+0x11],0x0c
mov byte [0xb8000+20*160+0x12],'O'
mov byte [0xb8000+20*160+0x13],0x0c
mov byte [0xb8000+20*160+0x14],'D'
mov byte [0xb8000+20*160+0x15],0x0c
mov byte [0xb8000+20*160+0x16],'E'
mov byte [0xb8000+20*160+0x17],0x0c
mov byte [0xb8000+20*160+0x18],' '
mov byte [0xb8000+20*160+0x19],0x0c
mov byte [0xb8000+20*160+0x1a],'!'
mov byte [0xb8000+20*160+0x1b],0x0c
mov byte [0xb8000+20*160+0x1c],'!'
mov byte [0xb8000+20*160+0x1d],0x0c
mov byte [0xb8000+20*160+0x1e],'!'
mov byte [0xb8000+20*160+0x1f],0x0c
ret


stackshow:
mov byte [0xb8000+22*160+0x00],'S'
mov byte [0xb8000+22*160+0x01],0x0c
mov byte [0xb8000+22*160+0x02],'t'
mov byte [0xb8000+22*160+0x03],0x0c
mov byte [0xb8000+22*160+0x04],'a'
mov byte [0xb8000+22*160+0x05],0x0c
mov byte [0xb8000+22*160+0x06],'c'
mov byte [0xb8000+22*160+0x07],0x0c
mov byte [0xb8000+22*160+0x08],'k'
mov byte [0xb8000+22*160+0x09],0x0c
mov byte [0xb8000+22*160+0x0a],':'
mov byte [0xb8000+22*160+0x0b],0x0c
mov byte [0xb8000+22*160+0x0c],al     ;打印出pop出来的值
mov byte [0xb8000+22*160+0x0d],0x0c
mov byte [0xb8000+22*160+0x0e],','
mov byte [0xb8000+22*160+0x0f],0x0c
mov byte [0xb8000+22*160+0x10],'O'
mov byte [0xb8000+22*160+0x11],0x0c
mov byte [0xb8000+22*160+0x12],'K'
mov byte [0xb8000+22*160+0x13],0x0c
mov byte [0xb8000+22*160+0x14],'!'
mov byte [0xb8000+22*160+0x15],0x0c
ret


Init8259A:
mov al, 011h
out 020h, al ; 主8259, ICW1.
call io_delay

out 0A0h, al ; 从8259, ICW1.
call io_delay

mov al, 020h ; IRQ0 对应中断向量 0x20
out 021h, al ; 主8259, ICW2.
call io_delay

mov al, 028h ; IRQ8 对应中断向量 0x28
out 0A1h, al ; 从8259, ICW2.
call io_delay

mov al, 004h ; IR2 对应从8259
out 021h, al ; 主8259, ICW3.
call io_delay

mov al, 002h ; 对应主8259的IR2
out 0A1h, al ; 从8259, ICW3.
call io_delay

mov al, 001h
out 021h, al ; 主8259, ICW4.
call io_delay

out 0A1h, al ; 从8259, ICW4.
call io_delay

mov al, 11111101b ; 开启键盘中断
;mov al, 11111111b ; 屏蔽主8259所有中断
out 021h, al ; 主8259, OCW1.
call io_delay

mov al, 11111111b ;  下一步开启鼠标中断
out 0A1h, al ; 从8259, OCW1.
call io_delay
ret


io_delay:
nop
nop
nop
nop
ret


keyservice:
cli ;应禁止中断
mov byte [0xb8000+24*160+0x00],'I'
mov byte [0xb8000+24*160+0x01],0x0c
mov byte [0xb8000+24*160+0x02],'n'
mov byte [0xb8000+24*160+0x03],0x0c
mov byte [0xb8000+24*160+0x04],'t'
mov byte [0xb8000+24*160+0x05],0x0c
mov byte [0xb8000+24*160+0x06],':'
mov byte [0xb8000+24*160+0x07],0x0c
mov byte [0xb8000+24*160+0x08],'O'
mov byte [0xb8000+24*160+0x09],0x0c
mov byte [0xb8000+24*160+0x0a],'K'
mov byte [0xb8000+24*160+0x0b],0x0c
mov byte [0xb8000+24*160+0x0c],'!'
mov byte [0xb8000+24*160+0x0d],0x0c
mov byte [0xb8000+24*160+0x0e],'-'
mov byte [0xb8000+24*160+0x0f],0x0c
mov byte [0xb8000+24*160+0x10],'-'
mov byte [0xb8000+24*160+0x11],0x0c

mov al,  0x61
out 0x20, al  ;PIC0_OCW2
in  al,  0x60  ;从键盘读入按键扫描码

mov byte [0xb8000+24*160+0x12],al
mov byte [0xb8000+24*160+0x13],0x0c

mov               al , 0x20  ;告诉硬件,中断处理完毕,即发送 EOI 消息
out               0x20 , al
out               0xa0 , al

IRET


diverror:
mov byte [0xb8000+23*160+0x00],'d'
mov byte [0xb8000+23*160+0x01],0x0c
mov byte [0xb8000+23*160+0x02],'i'
mov byte [0xb8000+23*160+0x03],0x0c
mov byte [0xb8000+23*160+0x04],'v'
mov byte [0xb8000+23*160+0x05],0x0c
mov byte [0xb8000+23*160+0x06],'e'
mov byte [0xb8000+23*160+0x07],0x0c
mov byte [0xb8000+23*160+0x08],'r'
mov byte [0xb8000+23*160+0x09],0x0c
mov byte [0xb8000+23*160+0x0a],'r'
mov byte [0xb8000+23*160+0x0b],0x0c
mov byte [0xb8000+23*160+0x0c],'o'
mov byte [0xb8000+23*160+0x0d],0x0c
mov byte [0xb8000+23*160+0x0e],'r'
mov byte [0xb8000+23*160+0x0f],0x0c

IRET


五、键盘数据处理

键盘驱动成功只是硬件通道的打通,表明CPU具有获取键盘数据的途径。但其实,后面会面临一个最为关键的问题:CPU运行的速度很快,它不可能一直等着键盘输入一个数据就处理一个。为此,诞生了一个计算机系统中常见的问题:如何协调CPU与外围设备之间的数据沟通。最好的方式就是通过:数据缓冲区。分配一段内存,键盘等外围设备只管响应用户的动作将输入数据写入缓冲区,CPU只管按自己的节奏从缓冲区读出数据,这样一个读写互不影响的机制保证了CPU不会漏掉任何一个键盘数据,也不用在键盘长期不动的情况下浪费时间来等待。

很显然,键盘、鼠标等外围设备的数据缓冲区类型是FIFO,在《30天》这本书中,描述缓冲区的逻辑结构图是:


BIOS中断号大全 bios中断设置_龙芯2k按键中断驱动_14


这是一个环形结构的FIFO缓冲区,用它就可以完全来管理键盘、鼠标等外围设备的数据输入。我们用C语言来定义一个FIFO缓冲区控制器:


struct FIFO8 {
	unsigned char *buf;
	int p, q, size, free, flags;
};


这个FIFO缓冲区控制器核心的地方就是要指向一个已经分配好且长度为size的内存区*buf,由于每次键盘输入数据一个字节就可以容纳,因此这个内存区指针用char型就可以了。

这样,具体应用的时候,我们初始化FIFO缓冲区可以通过如下函数实现:


/* FIFO数据缓冲区初始化 */
void fifo8_init(struct FIFO8 *fifo, int size, unsigned char *buf)

{
	fifo->size = size; /* 缓冲区总空间大小 */
	fifo->buf = buf;   /* 指向缓冲区数据在内存中的存放位置 */
	fifo->free = size; /* 剩余空间大小 */
	fifo->flags = 0;   /* 缓冲区是否写满 */
	fifo->p = 0; /* 缓冲区写指针 */
	fifo->q = 0; /* 缓冲区读指针 */

}


而且这个FIFO缓冲区控制器是可以通用的,你可以把它的 *buf指向一个定义好的键盘内存数据小段:fifo->buf=&keyfifo;也可以把它的 *buf指向定义好的鼠标内存数据小段:如fifo->buf=&keyfifo。这样就能实现CPU读取各类外围设备的输入数据。