# 30 Day os day 2 前一天,大概會涉及一些asm 和 background 一些基礎知識讀起來有點硬,我算抽取蠻多其他話,只讀引用的片段應該蠻好理解的 今天大概會往中斷的部分前,一路到滑鼠鍵盤控制和優化整個程式碼架構等等,讓程式可讀性比較好一點 # 結構化 文字顯示與 GDT/IDT 初始化 ## HariMain 跟之前的方式來比作者想用struct 來重新封裝 這些bios 的一些 infomation ```c= struct BOOTINFO { char cyls, leds, vmode, reserve; short scrnx, scrny; char *vram; }; void HariMain(void) { char *vram; int xsize, ysize; struct BOOTINFO *binfo; init_palette(); binfo = (struct BOOTINFO *) 0x0ff0; xsize = (*binfo).scrnx; ysize = (*binfo).scrny; vram = (*binfo).vram; } ``` binfo = (struct BOOTINFO *)0x0ff0; 本来想写“binfo =0x0ff0;”的,但由于总出警告,很讨厌,所以我们就进行了类型转换。 设定了指针地址以后,这 12 个字节的结构体用起来就没问题了。这样我们可以不再直 接使用内存地址,而是使用*binfo 来表示这个内存地址上 12 字节的结构体。这与“char *p;” 中的*p 表示 p 地址的 1 字节是同样道理。 前面说过,想要表示结构体 abc 中的 scrnx 时,就用 abc.scrnx。与此类似,这里用 (*binfo).scrnx 来表示。需要括号的理由在 5.2 节中已经写了。因此语句写作: xsize = (*binfo).scrnx; > > 這邊可以稍微複習一下之前的,也就是你假設沒指定型態,0x0ff0 就不知道要讀到哪裡 > > `xsize = (*binfo).scrnx; 也可以寫成 xsize = binfo->scrnx;` # 文字顯示 ![](https://i.imgur.com/hNVVlW8.png) 其實就是作者想要用 char 陣列去表示一個a ``` static char font_A[16] = { 0x00, 0x18, 0x18, 0x18, 0x18, 0x24, 0x24, 0x24, 0x24, 0x7e, 0x42, 0x42, 0x42, 0xe7, 0x00, 0x00 }; ``` 作者後續還有做了一個這個function 來顯示我們剛剛字串陣列的文字 ## putfont8 0x80也就是二进制数10000000,它与d进行“与” 运算的结果如果是0,就说明d的最左边一位是0。 這個就是假設你沒值得話 if ((d & 0x80) != 0) { p[0] = c; } p可以看到指向螢幕的像素點,假設運算後結果等於0則這個點就沒畫東西則反之 ```c= void putfont8(char *vram, int xsize, int x, int y, char c, char *font) { int i; char *p, d /* data */; for (i = 0; i < 16; i++) { p = vram + (y + i) * xsize + x; d = font[i]; if ((d & 0x80) != 0) { p[0] = c; } if ((d & 0x40) != 0) { p[1] = c; } if ((d & 0x20) != 0) { p[2] = c; } if ((d & 0x10) != 0) { p[3] = c; } if ((d & 0x08) != 0) { p[4] = c; } if ((d & 0x04) != 0) { p[5] = c; } if ((d & 0x02) != 0) { p[6] = c; } if ((d & 0x01) != 0) { p[7] = c; } } return; } ``` ![](https://i.imgur.com/tTjk2MK.png) 只能顯示 a是不夠的字母還有數字那些特殊符號要通通顯示 平木敬太郎先生和圣人(Kiyoto)先生 還做了一個字體產生器 hankaku.txt 想辦法透過makefont.exe 這些字體產生後的陣列塞到 hankaku 裡 extern char hankaku[4096]; 的陣列 可以看到他前面是 char 0x41 對應到 c語言的 char ascii 正是一樣的,所以又衍生出print字串另一個版本 ```txt char 0x41 ........ ...**... ...**... ...**... ...**... ..*..*.. ..*..*.. ..*..*.. ..*..*.. .******. .*....*. .*....*. .*....*. ***..*** ........ ........ ``` ## HariMain 可以看到這些偏移是根據 ascii 來進行偏移到正確的文字上 ```c void HariMain(void) { struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0; extern char hankaku[4096]; init_palette(); init_screen(binfo->vram, binfo->scrnx, binfo->scrny); putfont8(binfo->vram, binfo->scrnx, 8, 8, COL8_FFFFFF, hankaku + 'A' * 16); putfont8(binfo->vram, binfo->scrnx, 16, 8, COL8_FFFFFF, hankaku + 'B' * 16); putfont8(binfo->vram, binfo->scrnx, 24, 8, COL8_FFFFFF, hankaku + 'C' * 16); putfont8(binfo->vram, binfo->scrnx, 40, 8, COL8_FFFFFF, hankaku + '1' * 16); putfont8(binfo->vram, binfo->scrnx, 48, 8, COL8_FFFFFF, hankaku + '2' * 16); putfont8(binfo->vram, binfo->scrnx, 56, 8, COL8_FFFFFF, hankaku + '3' * 16); for (;;) { io_hlt(); } } ``` ![](https://i.imgur.com/v9mrfUv.png) # putfonts8_asc 這邊又把 putfont 抽成一個fucntion ```c= void putfonts8_asc(char *vram, int xsize, int x, int y, char c, unsigned char *s) { extern char hankaku[4096]; for (; *s != 0x00; s++) { putfont8(vram, xsize, x, y, c, hankaku + *s * 16); x += 8; } return; } ``` 只要透過 > putfonts8_asc(binfo->vram, binfo->scrnx, 8, 8, COL8_FFFFFF, "ABC 123"); 就可以顯示任意 字串 ## HariMain ```c void HariMain(void) { struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0; init_palette(); init_screen(binfo->vram, binfo->scrnx, binfo->scrny); putfonts8_asc(binfo->vram, binfo->scrnx, 8, 8, COL8_FFFFFF, "ABC 123"); putfonts8_asc(binfo->vram, binfo->scrnx, 31, 31, COL8_000000, "Haribote OS."); putfonts8_asc(binfo->vram, binfo->scrnx, 30, 30, COL8_FFFFFF, "Haribote OS."); for (;;) { io_hlt(); } } ``` ![](https://i.imgur.com/g32pP7s.png) # 顯示variable 進行debug 作者想要透過printf 去顯示在記憶體中某一個值並且轉為字串,可以看到他用到呢 sprintf 這個東西本來就是在做格式化輸出的東西 那麼下面的程式碼就是從 binfo 讀 scrnx 轉成字串並放在s 再透過 putfonts8_asc 去顯示到相對應的位置 ``` sprintf(s, "scrnx = %d", binfo->scrnx); putfonts8_asc(binfo->vram, binfo->scrnx, 16, 64, COL8_FFFFFF, s); ``` sprintf函数的使用方法是:sprintf(地址,格式,值,值,值,……)。这里的地址指定所生 成字符串的存放地址。格式基本上只是单纯的字符串,如果有%d这类记号,就置换成后面的值 的内容。除了%d,还有%s,%x等符号,它们用于指定数值以什么方式变换为字符串。%d将数 值作为十进制数转化为字符串,%x将数值作为十六进制数转化为字符串。 关于格式的详细说明 %d 单纯的十进制数 %5d 5位十进制数。如果是123,则在前面加上两个空格,变成" 123",强制达到5位 %05d 5位十进制数。如果是123,则在前面加上0,变成"00123",强制达到5位 %x 单纯的十六进制数。字母部分使用小写abcdef %X 单纯的十六进制数。字母部分使用大写ABCDEF %5x 5位十六进制数。如果是456(十进制),则在前面加上两个空格,变成" 1c8",强制达到5位。还有%5X的 形式 %05x 5位十六进制数。如果是456(十进制),则在前面加上两个0,变成"001c8",强制达到5位。还有%05X的形式 ![](https://i.imgur.com/tbDBxk4.png) # 顯示滑鼠 一樣去構造一個滑鼠的 char 陣列 ## init_mouse_cursor8 這個內容只是根據替換裡面的元素來產生相對應的像素點 ```c= void init_mouse_cursor8(char *mouse, char bc) /* 准备鼠标指针(16×16) */ { static char cursor[16][16] = { "**************..", "*OOOOOOOOOOO*...", "*OOOOOOOOOO*....", "*OOOOOOOOO*.....", "*OOOOOOOO*......", "*OOOOOOO*.......", "*OOOOOOO*.......", "*OOOOOOOO*......", "*OOOO**OOO*.....", "*OOO*..*OOO*....", "*OO*....*OOO*...", "*O*......*OOO*..", "**........*OOO*.", "*..........*OOO*", "............*OO*", ".............***" }; int x, y; for (y = 0; y < 16; y++) { 8 100 …… 第 5 天:结构体、文字显示与 GDT/IDT 初始化 for (x = 0; x < 16; x++) { if (cursor[y][x] == '*') { mouse[y * 16 + x] = COL8_000000; } if (cursor[y][x] == 'O') { mouse[y * 16 + x] = COL8_FFFFFF; } if (cursor[y][x] == '.') { mouse[y * 16 + x] = bc; } } } return; } ``` ## putblock8_8 仔細看的話他只是把我們的 滑鼠讀進來,可能後續也有需要對這個螢幕進行繪圖之類的function 吧 ```c void putblock8_8(char *vram, int vxsize, int pxsize, int pysize, int px0, int py0, char *buf, int bxsize) { int x, y; for (y = 0; y < pysize; y++) { for (x = 0; x < pxsize; x++) { vram[(py0 + y) * vxsize + (px0 + x)] = buf[y * bxsize + x]; } } return; } ``` ``` init_mouse_cursor8(mcursor, COL8_008484); putblock8_8(binfo->vram, binfo->scrnx, 16, 16, mx, my, mcursor, 16); ``` ![](https://i.imgur.com/ja9uUZa.png) # GDT/IDT 初始化 ## segment 可能會先講個大概詳細可能要對著書去看,又要開始補background 囉 上一節可以看到畫出我們的滑鼠但是要怎麼讓它動才是關鍵。 我們又要再一次回到底層做一些撰寫與設定 這樣滑鼠才能開始移動 segment分段的概念也就是 像是組合語言 ORG 0 ORG 1324 這樣去把記憶體分成一段一段,還記得之前的BOOT加載那個地方就是,這邊還沒講到分頁的概念 > 記憶體分段(英語:Memory segmentation),一種電腦記憶體的管理技術,它將電腦的主記憶體分成許多區段(segment或sections)。當處理器要進行記憶體 ... > 分頁(英語:Paging),是一種作業系統裡記憶體管理的一種技術,可以使電腦的主記憶體可以使用儲存在輔助記憶體中的資料。 作業系統會將輔助記憶體(通常是磁盘)中的資料分割成固定大小的區塊,稱為「頁」(pages)。 當不需要時,將分頁由主記憶體(通常是内存)移到輔助記憶體;當需要時,再將資料取回,載入主記憶體中 需要注意的一点是,我们用16位的时候曾经讲解过的段寄存器。这里的分段,使用的就是 这个段寄存器。但是16位的时候,如果计算地址,只要将地址乘以16就可以了。但现在已经是 32位了,不能再这么用了。如果写成“MOV AL,[DS:EBX]”,CPU会往EBX里加上某个值来计 算地址,这个值不是DS的16倍,而是DS所表示的段的起始地址。即使省略段寄存器(segment register)的地址,也会自动认为是指定了DS。这个规则不管是16位模式还是32位模式,都是一 样的。 上次我們用到的 [DS:EBX] 特殊寫法可以根據register 進行偏移 一般默認的話 DS不會顯示,位置計算就是EBX DS*16 +BX ![](https://i.imgur.com/YvwWm8U.png) 為了防止這些記憶體片段因為撰寫的緣故發生覆蓋到其他地方,所以我們要選擇的記憶體位置要沒有干擾到其他程式的片段,目前還沒講到記憶體管理,從分段開始慢慢深入 依照上述要構造一個分段需要得知以下內容我們才可以對記憶體做偏移 * 段的大小是多少 * 段的起始地址在哪里 * 段的管理属性(禁止写入,禁止执行,系统专用等) segment register 可以用0~8191的数。因为segment register是16位,所以 本来应该能够处理0~65535范围的数,但由于CPU设计上的原因,段寄存器的低3位不能使用。因 此能够使用的段号只有13位,能够处理的就只有位于0~8191的区域了。 ## GDT 也就是segment register 只能處理 8192個段,這也就是GDT GDT是“global(segment)descriptor table”的缩写,意思是全局段号记录表。将这些数据整 齐地排列在内存的某个地方,然后将内存的起始地址和有效设定个数放在CPU内被称作GDTR①的 特殊寄存器中,设定就完成了。 ## IDT IDT是"interrupt descriptor table"的缩写,直译过来就是“中断记录表”。当CPU遇到 外部状况变化,或者是内部偶然发生某些错误时,会临时切换过去处理这种突发事件。这就是中 断功能 我们拿电脑的键盘来举个例子。以CPU的速度来看,键盘特别慢,只是偶尔动一动。就算是 重复按同一个键,一秒钟也很难输入50个字符。而CPU在1/50秒的时间内,能执行200万条指令 (CPU主频100MHz时)。CPU每执行200万条指令,查询一次键盘的状况就已经足够了。如果查询 得太慢,用户输入一个字符时电脑就会半天没反应。 要是设备只有键盘,用“查询”这种处理方法还好。但事实上还有鼠标、软驱、硬盘、光驱、 网卡、声卡等很多需要定期查看状态的设备。其中,网卡还需要CPU快速响应。响应不及时的话, 数据就可能接受失败,而不得不再传送一次。如果因为害怕处理不及时而靠查询的方法轮流查看 各个设备状态的话,CPU就会穷于应付,不能完成正常的处理。 正是为解决以上问题,才有了中断机制。各个设备有变化时就产生中断,中断发生后,CPU 暂时停止正在处理的任务,并做好接下来能够继续处理的准备,转而执行中断程序。中断程序执 行完以后,再调用事先设定好的函数,返回处理中的任务。正是得益于中断机制,CPU可以不用 一直查询键盘,鼠标,网卡等设备的状态,将精力集中在处理任务上。 ![](https://i.imgur.com/PpBs5oU.png) > 總而言之我們要控制中斷就要找到中斷號然後想辦法讓CPU根據中斷號找到相對應的FUCNTION並執行 > 至於這些中斷號要放在哪裡與就是 IDT它們預先把這些位置放在記憶體某一個區段當發生 中斷時再去根據這個TABLE進行查表 ## bootpack.c 這邊簡單來看CODE就是 作者在記憶體裡面找到一塊沒人用的ADDRESS 也就是 0x00270000; GDT 0x0026f800; IDT GDT 設置8192個段號 IDT 設置256個中斷號 將它們的 STRUCT 裡面的參數都初始化為0 這就是下面這些程式碼做的事 ```c= struct SEGMENT_DESCRIPTOR{ short limit_low, base_low; char base_mid, access_right; char limit_high, base_high; }; struct GATE_DESCRIPTOR { short offset_low, selector; char dw_count, access_right; short offset_high; }; void init_gdtidt(void) 9 GDT 与 IDT 的初始化(harib02i) { struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) 0x00270000; struct GATE_DESCRIPTOR *idt = (struct GATE_DESCRIPTOR *) 0x0026f800; int i; /* GDT的初始化 */ for (i = 0; i < 8192; i++) { set_segmdesc(gdt + i, 0, 0, 0); } set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, 0x4092); set_segmdesc(gdt + 2, 0x0007ffff, 0x00280000, 0x409a); load_gdtr(0xffff, 0x00270000); /* IDT的初始化 */ for (i = 0; i < 256; i++) { set_gatedesc(idt + i, 0, 0, 0); } load_idtr(0x7ff, 0x0026f800); return; } void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar) { if (limit > 0xfffff) { ar |= 0x8000; /* G_bit = 1 */ limit /= 0x1000; } sd->limit_low = limit & 0xffff; sd->base_low = base & 0xffff; sd->base_mid = (base >> 16) & 0xff; sd->access_right = ar & 0xff; sd->limit_high = ((limit >> 16) & 0x0f) | ((ar >> 8) & 0xf0); sd->base_high = (base >> 24) & 0xff; return; } void set_gatedesc(struct GATE_DESCRIPTOR *gd, int offset, int selector, int ar) { gd->offset_low = offset & 0xffff; gd->selector = selector; gd->dw_count = (ar >> 8) & 0xff; gd->access_right = ar & 0xff; gd->offset_high = (offset >> 16) & 0xffff; return; } ``` ## naskfunc.nas 這相對應的是 这个函数用来将指定的段上限(limit)和地址值赋值给名为GDTR的48位寄存器。这是一个 很特别的48位寄存器,并不能用我们常用的MOV指令来赋值。给它赋值的时候,唯一的方法就 是指定一个内存地址,从指定的地址读取6个字节(也就是48位),然后赋值给GDTR寄存器。 完成这一任务的指令,就是LGDT。 该寄存器的低16位①(即内存的最初2个字节)是段上限,它等于“GDT的有效字节数  1”。 今后我们还会偶尔用到上限这个词,意思都是表示量的大小,一般为“字节数 1”。剩下的高32 位(即剩余的4个字节),代表GDT的开始地址。 在最初执行这个函数的时候,DWORD[ESP + 4]里存放的是段上限,DWORD[ESP+8]里存放 的是地址。具体到实际的数值,就是0x0000ffff和0x00270000。把它们按字节写出来的话,就成 了[FF FF 00 00 00 00 27 00](要注意低位放在内存地址小的字节里② )。为了执行LGDT,笔者希 望把它们排列成[FF FF 00 00 27 00]的样子,所以就先用“MOV AX,[ESP + 4]”读取最初的0xffff, 然后再写到[ESP + 6]里。这样,结果就成了[FF FF FF FF 00 00 27 00],如果从[ESP + 6]开始读6 字节的话,正好是我们想要的结果。 naskfunc.nas的_load_idtr设置IDTR的值,因为IDTR与GDTR结构体基本上是一样的,程序也 非常相似。 ```ASM _load_gdtr: ; void load_gdtr(int limit, int addr); MOV AX,[ESP+4] ; limit MOV [ESP+6],AX LGDT [ESP+6] RET _load_idtr: ; void load_idtr(int limit, int addr); MOV AX,[ESP+4] ; limit MOV [ESP+6],AX LIDT [ESP+6] RET ``` ## set_segmdesc https://www.csie.ntu.edu.tw/~wcchen/asm98/asm/proj/b85506061/chap2/segment.html 要詳細理解的話主要就是要根據 segment 需要的 info 填入相對應的value 也就是 * 段的大小 * 段的起始地址 * 段的管理属性(禁止写入,禁止执行,系统专用等) address base 為32 位元 被拆為 base_low (2 bytes) base_mid (1 bytes) base_high (1 bytes) 再來就是 limit_low(2 bytes) limit_height(1 bytes) 8*3 = 24 這邊暫時理解就是因為光是這些結構可能就塞滿 32位元 以他這邊來說就是上面的表示方式 sd->access_right = ar & 0xff; ar使用來管理權限 就是怕有些人透過替換 gdt或idt的 address 來更改,這樣就可以達到做出類似保護的機制 最后再来讲一下12位的段属性。段属性又称为“段的访问权属性”,在程序中用变量名 xxxx0000xxxxxxxx ar的低8位从80286时代就已经有了,如果要详细说明的话,够我们说一天的了,所以这里只 是简单地介绍一下。 00000000(0x00):未使用的记录表(descriptor table)。 10010010(0x92):系统专用,可读写的段。不可执行。 10011010(0x9a):系统专用,可执行的段。可读不可写。 11110010(0xf2):应用程序用,可读写的段。不可执行。 11111010(0xfa):应用程序用,可执行的段。可读不可写。 “系统专用”,“应用程序用”什么的,听着让人摸不着头脑。都是些什么东西呀?在32位模 式下,CPU有系统模式(也称为“ring0”①)和应用模式(也称为“ring3”)之分。操作系统等“管 理用”的程序,和应用程序等“被管理”的程序,运行时的模式是不同的。 比如,如果在应用模式下试图执行LGDT等指令的话,CPU则对该指令不予执行,并马上告 诉操作系统说“那个应用程序居然想要执行LGDT,有问题!”。另外,当应用程序想要使用系统 专用的段时,CPU也会中断执行,并马上向操作系统报告“那个应用程序想要盗取系统信息。也 有可能不仅要盗取信息,还要写点东西来破坏系统呢。” “想要盗取系统信息这一点我明白,但要阻止LGDT的执行这一点,我还是不懂。”可能有人 会有这种疑问。当然要阻止啦。因为如果允许应用程序执行LGDT,那应用程序就会根据自己的 需要,偷偷准备GDT,然后重新设定LGDT来让它执行自己准备的GDT。这可就麻烦了。有了这 个漏洞,操作系统再怎么防守还是会防不胜防。 CPU到底是处于系统模式还是应用模式,取决于执行中的应用程序是位于访问权为0x9a的 段,还是位于访问权为0xfa的段。 下面再说一下段上限。它表示一个段有多少个字节。可是这里有一个问题,段上限最大是 4GB,也就是一个32位的数值,如果直接放进去,这个数值本身就要占用4个字节,再加上基址 (base),一共就要8个字节,这就把整个结构体占满了。这样一来,就没有地方保存段的管理属 性信息了,这可不行。 因此段上限只能使用20位。这样一来,段上限最大也只能指定到1MB为止。明明有4GB,却 只能用其中的1MB,有种又回到了16位时代的错觉,太可悲了。在这里英特尔的叔叔们又想了一 个办法,他们在段的属性里设了一个标志位,叫做Gbit。这个标志位是1的时候,limit的单位不解 释成字节(byte),而解释成页(page)。页是什么呢?在电脑的CPU里,1页是指4KB。 这样一来,4KB × 1M = 4GB,所以可以指定4GB的段。总算能放心了。顺便说一句,G bit 的“G”,是“granularity”的缩写,是指单位的大小。 这20位的段上限分别写到limit_low和limit_high里。看起来它们好像是总共有3字节,即24位, 但实际上我们接着要把段属性写入limit_high的高4位里,所以最后段上限还是只有20,好复杂呀 access_right或ar来表示。因为12位段属性中的高4位放在limit_high的高4位里,所以程序里有意把 ar当作如下的16位构成来处理:。 ar的高4位被称为“扩展访问权”。为什么这么说呢?因为这高4位的访问属性在80286的时代 还不存在,到386以后才可以使用。这4位是由“GD00”构成的,其中G是指刚才所说的G bit,D 是指段的模式,1是指32位模式,0是指16位模式。这里出现的16位模式主要只用于运行80286的 程序,不能用于调用BIOS。所以,除了运行80286程序以外,通常都使用D=1的模式。 ar的高4位被称为“扩展访问权”。为什么这么说呢?因为这高4位的访问属性在80286的时代 还不存在,到386以后才可以使用。这4位是由“GD00”构成的,其中G是指刚才所说的G bit,D 是指段的模式,1是指32位模式,0是指16位模式。这里出现的16位模式主要只用于运行80286的 程序,不能用于调用BIOS。所以,除了运行80286程序以外,通常都使用D=1的模式。 ```c= void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar) { if (limit > 0xfffff) { ar |= 0x8000; /* G_bit = 1 */ limit /= 0x1000; } sd->limit_low = limit & 0xffff; sd->base_low = base & 0xffff; sd->base_mid = (base >> 16) & 0xff; sd->access_right = ar & 0xff; sd->limit_high = ((limit >> 16) & 0x0f) | ((ar >> 8) & 0xf0); sd->base_high = (base >> 24) & 0xff; return; } ``` set_gatedesc 這個fucntion 後續再進行設備中斷的時候會講解到 ## PIC “programmable interrupt controller”的缩写,意思是“可编程中断控制器”。PIC 与中断的关系可是很密切的哟。它到底是什么呢?在设计上,CPU单独只能处理一个中断,这不 够用,所以IBM的大叔们在设计电脑时,就在主板上增设了几个辅助芯片。现如今它们已经被集 成在一个芯片组里了。 PIC是将8个中断信号①集合成一个中断信号的装置。PIC监视着输入管脚的8个中断信号,只 要有一个中断信号进来,就将唯一的输出管脚信号变成ON,并通知给CPU。IBM的大叔们想要 通过增加PIC来处理更多的中断信号,他们认为电脑会有8个以上的外部设备,所以就把中断信号 设计成了15个,并为此增设了2个PIC。 那它们的线路是如何连接的呢?如下页图所示。 与CPU直接相连的PIC称为主PIC(master PIC),与主PIC相连的PIC称为从PIC(slave PIC)。 主PIC负责处理第0到第7号中断信号,从PIC负责处理第8到第15号中断信号。master意为主人, slave意为奴隶,笔者搞不清楚这两个词的由来,但现在结果是不论从PIC如何地拼命努力,如果 主PIC不通知给CPU,从PIC的意思也就不能传达给CPU。或许是从这种关系上考虑,而把它们一 个称为主人,一个称为奴隶。 大概理解就是大部分電腦有兩個PIC, http://stenlyho.blogspot.com/2008/08/pic.html 第一個 PIC IRQ2 對應 另一個 PIC 假設 PIC 裡的 8~15 任一發生中斷則 統一給IRQ2管理 IRQ 0 : System timer. 系統時間. IRQ 1 : Keyboard. 鍵盤. IRQ 3 : Com2. 串列埠. IRQ 4 : Com1. 串列埠. IRQ 5 : Parallel port 2. 並列埠 IRQ 6 : Floppy Disk. 軟碟機. IRQ 7 : Parallel port 1. 並列埠. IRQ 8 : Real Time Clock. 時鐘. IRQ 9 : INT 0AH IRQ 10 : 保留 IRQ 11 : 保留 IRQ 12 : PS/2 mouse. 滑鼠. IRQ 13 : Coprocessor. 輔助(協)處理器. IRQ 14 : Primary IDE. 主 IDE. 如硬碟機. IRQ 15 : Secondary IDE. 副 IDE. ## int.c ![](https://i.imgur.com/IwMjWdc.png) ```c= void init_pic(void) /* PIC的初始化 */ { io_out8(PIC0_IMR, 0xff ); /* 禁止所有中断 */ io_out8(PIC1_IMR, 0xff ); /* 禁止所有中断 */ io_out8(PIC0_ICW1, 0x11 ); /* 边沿触发模式(edge trigger mode) */ io_out8(PIC0_ICW2, 0x20 ); /* IRQ0-7由INT20-27接收 */ io_out8(PIC0_ICW3, 1 << 2); /* PIC1由IRQ2连接 */ io_out8(PIC0_ICW4, 0x01 ); /* 无缓冲区模式 */ io_out8(PIC1_ICW1, 0x11 ); /* 边沿触发模式(edge trigger mode) */ io_out8(PIC1_ICW2, 0x28 ); /* IRQ8-15由INT28-2f接收 */ io_out8(PIC1_ICW3, 2 ); /* PIC1由IRQ2连接 */ io_out8(PIC1_ICW4, 0x01 ); /* 无缓冲区模式 */ io_out8(PIC0_IMR, 0xfb ); /* 11111011 PIC1以外全部禁止 */ io_out8(PIC1_IMR, 0xff ); /* 11111111 禁止所有中断 */ return; } ``` 大家可能会对此有兴趣,所以再详细介绍一下。中断发生以后,如果CPU可以受理这个 中断,CPU就会命令PIC发送2个字节的数据。这2个字节是怎么传送的呢?CPU与PIC用IN 或OUT进行数据传送时,有数据信号线连在一起。PIC就是利用这个信号线发送这2个字节数 据的。送过来的数据是“0xcd 0x??”这两个字节。由于电路设计的原因,这两个字节的数据 在CPU看来,与从内存读进来的程序是完全一样的,所以CPU就把送过来的“0xcd 0x??”作 为机器语言执行。这恰恰就是把数据当作程序来执行的情况。这里的0xcd就是调用BIOS时 使用的那个INT指令。我们在程序里写的“INT 0x10”,最后就被编译成了“0xcd 0x10”。所 以,CPU上了PIC的当,按照PIC所希望的中断号执行了INT指令。 # 中斷設置 keybord 中斷 int.c ```c void inthandler21(int *esp) /* 来自PS/2键盘的中断 */ { struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO; boxfill8(binfo->vram, binfo->scrnx, COL8_000000, 0, 0, 32 * 8 - 1, 15); putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, "INT 21 (IRQ-1) : PS/2 keyboard"); for (;;) { io_hlt(); } } ``` ## naskfunc.nas CPU并不懂栈的机制,它只是执行了实现栈功能的指令而已。所以,即使是PUSH太多,或 者POP太多这种没有意义的操作,基本上CPU也都会遵照执行。 所以,如果写了以下程序, PUSH EAX PUSH ECX PUSH EDX 各种处理 POP EDX POP ECX POP EAX 在“各种处理”那里,即使把EAX,ECX,EDX改了,最后也还会恢复回原来的值……其实 ES、DS这些寄存器,也就是靠PUSH和POP等操作而变回原来的值的。 还有一个不怎么常见的指令PUSHAD,它相当于: PUSH EAX PUSH ECX PUSH EDX PUSH EBX PUSH ESP PUSH EBP PUSH ESI PUSH EDI 反过来,POPAD指令相当于按以上相反的顺序,把它们全都POP出来。6 中断处理程序的制作(harib03e) > 结果,这个函数只是将寄存器的值保存到栈里,然后将DS和ES调整到与SS相等,再调用 > _inthandler21,返回以后,将所有寄存器的值再返回到原来的值,然后执行IRETD。内容就这些。 > 如此小心翼翼地保存寄存器的值,其原因在于,中断处理发生在函数处理的途中,通过IRETD从 > 中断处理返回以后,如果寄存器的值乱了,函数就无法正常处理下去了,所以一定要想尽办法让 > 寄存器的值返回到中断处理前的状态。 ```asm _asm_inthandler21: PUSH ES PUSH DS PUSHAD MOV EAX,ESP PUSH EAX MOV AX,SS MOV DS,AX MOV ES,AX CALL _inthandler21 POP EAX POPAD POP DS POP ES IRETD ``` ```asm _asm_inthandler2c: PUSH ES PUSH DS PUSHAD MOV EAX,ESP PUSH EAX MOV AX,SS MOV DS,AX MOV ES,AX CALL _inthandler2c POP EAX POPAD POP DS POP ES IRETD ``` 这次是以INT 0x20 - 0x2f 接收中断信号IRQ0 -15而设定的。 这里大家可能又会有疑问了。“直 接用INT 0x00~0x0f就不行吗?这样与IRQ的号码不就一样了吗?为什么非要加上0x20?”不要着 急,先等笔者说完再问嘛。是这样的,INT 0x00~0x1f不能用于IRQ,仅此而已。 之所以不能用,是因为应用程序想要对操作系统干坏事的时候,CPU内部会自动产生INT 0x00~0x1f,如果IRQ与这些号码重复了, CPU就分不清它到底是IRQ,还是CPU的系统保护通知。 /* IDT的设定 */ > set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 * 8, AR_INTGATE32); > set_gatedesc(idt + 0x2c, (int) asm_inthandler2c, 2 * 8, AR_INTGATE32); ![](https://i.imgur.com/xqBgbiA.png) # 鍵盤中斷 ## 獲取鍵盤編碼 ![](https://i.imgur.com/ccvbOKA.png) ## int.c ```c #define PORT_KEYDAT 0x0060 void inthandler21(int *esp) { struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO; unsigned char data, s[4]; io_out8(PIC0_OCW2, 0x61); /* 通知PIC"IRQ-01已经受理完毕" */ data = io_in8(PORT_KEYDAT); sprintf(s, "%02X", data); boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31); putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s); return; } ``` 首先请把目光转移到“io_out8(PIC0_OCW2, 0x61);”这句话上。这句话用来通知PIC“已经 知道发生了IRQ1中断哦”。如果是IRQ3,则写成0x63。也就是说,将“0x60+IRQ号码”输出给 OCW2就可以。执行这句话之后,PIC继续时刻监视IRQ1中断是否发生。反过来,如果忘记了执 行这句话,PIC就不再监视IRQ1中断,不管下次由键盘输入什么信息,系统都感知不到了。 # 中斷處理優化 ## int.c ```c= struct KEYBUF { unsigned char data, flag; }; #define PORT_KEYDAT 0x0060 struct KEYBUF keybuf; void inthandler21(int *esp) { unsigned char data; io_out8(PIC0_OCW2, 0x61); /* 通知PIC IRQ-01已经受理完毕 */ data = io_in8(PORT_KEYDAT); if (keybuf.flag == 0) { keybuf.data = data; keybuf.flag = 1; } return; } ``` ## bootpack.c 开始先用io_cli指令屏蔽中断。为什么这时要屏蔽中断呢?因为在执行其后的处理时,如果 有中断进来,那可就乱套了。我们先将中断屏蔽掉,去看一看keybuf.flag的值是什么。 如果flag的值是0,就说明键还没有被按下,keybuf.data里没有值保存进来。在keybuf.data里 有值被保存进来之前,我们无事可做,所以干脆就去执行io_hlt。但是,由于已经执行io_cli屏蔽 了中断,如果就这样去执行HLT指令的话,即使有什么键被按下,程序也不会有任何反应。所以 STI和HLT两个指令都要执行,而执行这两个指令的函数就是io_stihlt①。执行HLT指令以后,如果 收到了PIC的通知,CPU就会被唤醒。这样,CPU首先会去执行中断处理程序。中断处理程序执 行完以后,又回到for语句的开头,再执行io_cli函数。 HariMain ```c= ..... for (;;) { io_cli(); if (keybuf.flag == 0) { io_stihlt(); } else { i = keybuf.data; keybuf.flag = 0; io_sti(); sprintf(s, "%02X", i); boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31); 2 putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s); } } ``` # 新增FIFO緩衝區 主要要解決 左ctl 會發生兩次中斷,但是我們假設沒有緩衝區便不知道這個訊號是什麼,因為假設照之前的寫法則,滿了數據會拋棄掉 ## 第一代緩衝區 缺點 ![](https://i.imgur.com/ZnoF5RX.png) 往前補位這部分時間比較耗時。 ```c Struct KEYBUF { unsigned char data1, data2, data3, data4, ... }; ``` 但这样一来,程序就变长了,所以将它写成下面这样: ```c struct KEYBUF { unsigned char data[4]; }; ``` ### int.c ```c= struct KEYBUF { unsigned char data[32]; int next; }; void inthandler21(int *esp) { unsigned char data; io_out8(PIC0_OCW2, 0x61); /* 通知PIC IRQ-01已经受理完毕 */ data = io_in8(PORT_KEYDAT); if (keybuf.next < 32) { keybuf.data[keybuf.next] = data; keybuf.next++; } return; } ``` ### bootpack.c HariMain ```c= for (;;) { io_cli(); if (keybuf.next == 0) { io_stihlt(); } else { i = keybuf.data[0]; keybuf.next--; for (j = 0; j < keybuf.next; j++) { keybuf.data[j] = keybuf.data[j + 1]; } io_sti(); sprintf(s, "%02X", i); boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31); putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s); } } ``` ## 第二代緩衝區 難道要不斷擴充緩衝區,其實就是他提到的並用不了這麼多,在我們讀到緩衝區最後一位的時候,前面的資料或許早就空掉了我們只要在這環狀,循環讀值就好 ![](https://i.imgur.com/QiHcjM0.png) ![](https://i.imgur.com/IulQojE.png) ### bootpack.h ```c struct KEYBUF { unsigned char data[32]; int next_r, next_w, len; }; 变量len是指缓冲区能记录多少字节的数据。 ``` ### int.c ```c void inthandler21(int *esp) { unsigned char data; 5 整理 FIFO 缓冲区(harib04e) …… 135 io_out8(PIC0_OCW2, 0x61); /* 通知 IRQ-01已经受理完毕 */ data = io_in8(PORT_KEYDAT); if (keybuf.len < 32) { keybuf.data[keybuf.next_w] = data; keybuf.len++; keybuf.next_w++; if (keybuf.next_w == 32) { keybuf.next_w = 0; } } return; } ``` 以上无非是将我们的说明写成了程序而已,并没什么难点。倒不如这样说,正是因为看了以 上程序,大家才能搞清楚笔者想要说什么。读出数据的程序如下: ### bootpack.c ```c for (;;) { io_cli(); if (keybuf.len == 0) { io_stihlt(); } else { i = keybuf.data[keybuf.next_r]; keybuf.len--; keybuf.next_r++; if (keybuf.next_r == 32) { keybuf.next_r = 0; } io_sti(); sprintf(s, "%02X", i); boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31); putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s); } ``` ## 第三代緩衝區 前面在鍵盤的部分,因為考慮到滑鼠稍微動一下數據也很多的情況下,因為鍵盤和滑鼠結構差不多,所以我們打算用struct 來重新封裝。 因為解讀我們根據不同的中斷去呼叫不同的fucntion 則,我們是拿fucntion 裡的data來進行到底是 keyborad 的按鍵值還是 Mouse 的 data 這邊資料就已經分開了所以不太需要擔心重複,我們只要負責把資料撈到結構裡,怎麼解讀是看中斷後呼叫的fucntion 裡面我們在來對data做出相對應的解讀。 ### fifo.c ```c struct FIFO8 { unsigned char *buf; int p, q, size, free, flags; }; ``` ```c= void fifo8_init(struct FIFO8 *fifo, int size, unsigned char *buf) /* 初始化FIFO缓冲区 */ { fifo->size = size; fifo->buf = buf; fifo->free = size; /* 缓冲区的大小 */ fifo->flags = 0; fifo->p = 0; /* 下一个数据写入位置 */ fifo->q = 0; /* 下一个数据读出位置 */ return; } ``` ```c= #define FLAGS_OVERRUN 0x0001 int fifo8_put(struct FIFO8 *fifo, unsigned char data) /* 向FIFO传送数据并保存 */ { if (fifo->free == 0) { /* 空余没有了,溢出 */ fifo->flags |= FLAGS_OVERRUN; return -1; } fifo->buf[fifo->p] = data; fifo->p++; if (fifo->p == fifo->size) { fifo->p = 0; } fifo->free--; return 0; } ``` ```c= int fifo8_get(struct FIFO8 *fifo) /* 从FIFO取得一个数据 */ { int data; if (fifo->free == fifo->size) { /* 如果缓冲区为空,则返回 -1 */ return -1; } data = fifo->buf[fifo->q]; fifo->q++; if (fifo->q == fifo->size) { fifo->q = 0; } fifo->free++; return data; } ``` ```c= int fifo8_status(struct FIFO8 *fifo) /* 报告一下到底积攒了多少数据 */ { return fifo->size - fifo->free; } ``` ### int.c 重構後的中斷fucntion ```c= struct FIFO8 keyfifo; void inthandler21(int *esp) { unsigned char data; io_out8(PIC0_OCW2, 0x61); /* 通知PIC,说IRQ-01的受理已经完成 */ data = io_in8(PORT_KEYDAT); fifo8_put(&keyfifo, data); return; } ``` HariMain ```c= char s[40], mcursor[256], keybuf[32]; fifo8_init(&keyfifo, 32, keybuf); for (;;) { io_cli(); if (fifo8_status(&keyfifo) == 0) { io_stihlt(); } else { i = fifo8_get(&keyfifo); io_sti(); sprintf(s, "%02X", i); boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31); putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s); } } ``` # 滑鼠中斷 总而言之,我们必须发行指令,让下面两个装置有效,一个是鼠标控制电路,一个是鼠标本 身。通过上面的说明,大家应该已经明白了,要先让鼠标控制电路有效。如果先让鼠标有效了, 那时控制电路还没准备好数据就来了,可就麻烦了,因为控制电路还处理不了。 现在来说说控制电路的设定。事实上,鼠标控制电路包含在键盘控制电路里,如果键盘控制 电路的初始化正常完成,鼠标电路控制器的激活也就完成了。 ![](https://i.imgur.com/xOZPQjM.png) ## bootpack.c ```c= #define PORT_KEYDAT 0x0060 #define PORT_KEYSTA 0x0064 #define PORT_KEYCMD 0x0064 #define KEYSTA_SEND_NOTREADY 0x02 #define KEYCMD_WRITE_MODE 0x60 #define KBC_MODE 0x47 void wait_KBC_sendready(void) { /* 等待键盘控制电路准备完毕 */ for (;;) { if ((io_in8(PORT_KEYSTA) & KEYSTA_SEND_NOTREADY) == 0) { break; } } return; } void init_keyboard(void) { /* 初始化键盘控制电路 */ wait_KBC_sendready(); io_out8(PORT_KEYCMD, KEYCMD_WRITE_MODE); wait_KBC_sendready(); io_out8(PORT_KEYDAT, KBC_MODE); return; } #define KEYCMD_SENDTO_MOUSE 0xd4 #define MOUSECMD_ENABLE 0xf4 ``` 可以看到這一段 wait_KBC_sendready 這一行就是在等 鍵盤控制電路準備完成, ```c= void enable_mouse(void) { /* 激活鼠标 */ wait_KBC_sendready(); io_out8(PORT_KEYCMD, KEYCMD_SENDTO_MOUSE); wait_KBC_sendready(); io_out8(PORT_KEYDAT, MOUSECMD_ENABLE); return; /* 顺利的话,键盘控制器会返回ACK(0xfa) */ } ``` 更改後應該可以看到滑鼠中斷已經被接收 ![](https://i.imgur.com/CRlNoZF.png) # 接收滑鼠中斷資料 ## int.c ```c struct FIFO8 mousefifo; void inthandler2c(int *esp) /* 来自PS/2鼠标的中断 */ { unsigned char data; io_out8(PIC1_OCW2, 0x64); /* 通知PIC1 IRQ-12的受理已经完成 */ io_out8(PIC0_OCW2, 0x62); /* 通知PIC0 IRQ-02的受理已经完成 */ data = io_in8(PORT_KEYDAT); fifo8_put(&mousefifo, data); return; } ``` ## HariMain 可以看到我們大部分的函數已經抽出來了,最下面滑鼠中斷電路讀資料則擴充到128,因為它無時無刻都在動 ```c= void HariMain(void) { struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO; char s[40], mcursor[256], keybuf[32], mousebuf[128]; int mx, my, i; init_gdtidt(); init_pic(); io_sti(); /* IDT/PIC的初始化已经完成,于是开放CPU的中断 */ fifo8_init(&keyfifo, 32, keybuf); fifo8_init(&mousefifo, 128, mousebuf); io_out8(PIC0_IMR, 0xf9); /* 开放PIC1和键盘中断(11111001) */ io_out8(PIC1_IMR, 0xef); /* 开放鼠标中断(11101111) */ init_keyboard(); init_palette(); init_screen8(binfo->vram, binfo->scrnx, binfo->scrny); mx = (binfo->scrnx - 16) / 2; /* 计算画面中心坐标 */ my = (binfo->scrny - 28 - 16) / 2; init_mouse_cursor8(mcursor, COL8_008484); putblock8_8(binfo->vram, binfo->scrnx, 16, 16, mx, my, mcursor, 16); sprintf(s, "(%d, %d)", mx, my); putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, s); enable_mouse(); for (;;) { io_cli(); if (fifo8_status(&keyfifo) + fifo8_status(&mousefifo) == 0) { io_stihlt(); } else { if (fifo8_status(&keyfifo) != 0) { i = fifo8_get(&keyfifo); io_sti(); sprintf(s, "%02X", i); boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31); putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s); } else if (fifo8_status(&mousefifo) != 0) { i = fifo8_get(&mousefifo); io_sti(); sprintf(s, "%02X", i); boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 32, 16, 47, 31); putfonts8_asc(binfo->vram, binfo->scrnx, 32, 16, COL8_FFFFFF, s); } } } ``` 有了上述這些基本概念後半段就是大概對滑鼠的移動和 左鍵右鍵做處理 直接來看程式碼比較快 struct MOUSE_DEC mdec; 結構可以看到 buf透過mouse_decode 不斷讀入數據直到buf滿 則return 1 進到迴圈裏面才進行繪圖與decode ```c= /* bootpackのメイン */ #include "bootpack.h" #include <stdio.h> extern struct FIFO8 keyfifo, mousefifo; struct MOUSE_DEC { unsigned char buf[3], phase; int x, y, btn; }; void enable_mouse(struct MOUSE_DEC *mdec); int mouse_decode(struct MOUSE_DEC *mdec, unsigned char dat); void init_keyboard(void); void HariMain(void) { struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO; char s[40], mcursor[256], keybuf[32], mousebuf[128]; int mx, my, i; struct MOUSE_DEC mdec; init_gdtidt(); init_pic(); io_sti(); /* IDT/PIC的初始化已经完成,于是开放CPU的中断 */ fifo8_init(&keyfifo, 32, keybuf); fifo8_init(&mousefifo, 128, mousebuf); io_out8(PIC0_IMR, 0xf9); /* 开放PIC1和键盘中断(11111001) */ io_out8(PIC1_IMR, 0xef); /* 开放鼠标中断(11101111) */ init_keyboard(); init_palette(); init_screen8(binfo->vram, binfo->scrnx, binfo->scrny); mx = (binfo->scrnx - 16) / 2; /* 计算画面中心坐标 */ my = (binfo->scrny - 28 - 16) / 2; init_mouse_cursor8(mcursor, COL8_008484); putblock8_8(binfo->vram, binfo->scrnx, 16, 16, mx, my, mcursor, 16); sprintf(s, "(%d, %d)", mx, my); putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, s); enable_mouse(&mdec); for (;;) { io_cli(); if (fifo8_status(&keyfifo) + fifo8_status(&mousefifo) == 0) { io_stihlt(); } else { if (fifo8_status(&keyfifo) != 0) { i = fifo8_get(&keyfifo); io_sti(); sprintf(s, "%02X", i); boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 16, 15, 31); putfonts8_asc(binfo->vram, binfo->scrnx, 0, 16, COL8_FFFFFF, s); } else if (fifo8_status(&mousefifo) != 0) { i = fifo8_get(&mousefifo); io_sti(); if (mouse_decode(&mdec, i) != 0) { /* 3字节都凑齐了,所以把它们显示出来*/ sprintf(s, "[lcr %4d %4d]", mdec.x, mdec.y); if ((mdec.btn & 0x01) != 0) { s[1] = 'L'; } if ((mdec.btn & 0x02) != 0) { s[3] = 'R'; } if ((mdec.btn & 0x04) != 0) { s[2] = 'C'; } boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 32, 16, 32 + 15 * 8 - 1, 31); putfonts8_asc(binfo->vram, binfo->scrnx, 32, 16, COL8_FFFFFF, s); /* 鼠标指针的移动 */ boxfill8(binfo->vram, binfo->scrnx, COL8_008484, mx, my, mx + 15, my + 15); /* 隐藏鼠标 */ mx += mdec.x; my += mdec.y; if (mx < 0) { mx = 0; } if (my < 0) { my = 0; } if (mx > binfo->scrnx - 16) { mx = binfo->scrnx - 16; } if (my > binfo->scrny - 16) { my = binfo->scrny - 16; } sprintf(s, "(%3d, %3d)", mx, my); boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 0, 0, 79, 15); /* 隐藏坐标 */ putfonts8_asc(binfo->vram, binfo->scrnx, 0, 0, COL8_FFFFFF, s); /* 显示坐标 */ putblock8_8(binfo->vram, binfo->scrnx, 16, 16, mx, my, mcursor, 16); /* 描画鼠标 */ } } } } } #define PORT_KEYDAT 0x0060 #define PORT_KEYSTA 0x0064 #define PORT_KEYCMD 0x0064 #define KEYSTA_SEND_NOTREADY 0x02 #define KEYCMD_WRITE_MODE 0x60 #define KBC_MODE 0x47 void wait_KBC_sendready(void) { /* 等待键盘控制电路准备完毕 */ for (;;) { if ((io_in8(PORT_KEYSTA) & KEYSTA_SEND_NOTREADY) == 0) { break; } } return; } void init_keyboard(void) { /* 初始化键盘控制电路 */ wait_KBC_sendready(); io_out8(PORT_KEYCMD, KEYCMD_WRITE_MODE); wait_KBC_sendready(); io_out8(PORT_KEYDAT, KBC_MODE); return; } #define KEYCMD_SENDTO_MOUSE 0xd4 #define MOUSECMD_ENABLE 0xf4 void enable_mouse(struct MOUSE_DEC *mdec){ /* 鼠标有效 */ wait_KBC_sendready(); io_out8(PORT_KEYCMD, KEYCMD_SENDTO_MOUSE); wait_KBC_sendready(); io_out8(PORT_KEYDAT, MOUSECMD_ENABLE); /* 顺利的话,ACK(0xfa)会被送过来 */ mdec->phase = 0; /* 等待0xfa的阶段 */ return; } int mouse_decode(struct MOUSE_DEC *mdec, unsigned char dat){ if (mdec->phase == 0) { /* 等待鼠标的0xfa的阶段 */ if (dat == 0xfa) { mdec->phase = 1; } return 0; } if (mdec->phase == 1) { /* 等待鼠标第一字节的阶段 */ mdec->buf[0] = dat; mdec->phase = 2; return 0; } if (mdec->phase == 2) { /* 等待鼠标第二字节的阶段 */ mdec->buf[1] = dat; mdec->phase = 3; return 0; } if (mdec->phase == 3) { /* 等待鼠标第二字节的阶段 */ mdec->buf[2] = dat; mdec->phase = 1; mdec->btn = mdec->buf[0] & 0x07; mdec->x = mdec->buf[1]; mdec->y = mdec->buf[2]; if ((mdec->buf[0] & 0x10) != 0) { mdec->x |= 0xffffff00; } if ((mdec->buf[0] & 0x20) != 0) { mdec->y |= 0xffffff00; } /* 鼠标的y方向与画面符号相反 */ mdec->y = - mdec->y; return 1; } /* 应该不可能到这里来 */ return -1; } ``` 對應到程式碼為day 8第八天的結果 ![](https://i.imgur.com/g3Tysnh.png)