《30天自制操作系统》 day8 小结
鼠標控制與32位模式切換
在編寫代碼的時候可以對照書中的代碼,以便及時找出自己的錯誤。
- 鼠標控制與32位模式切換
- 鼠標解讀1
- 整理
- 鼠標解讀2
- 移動鼠標指針
- 通往32位模式之路
1. 鼠標解讀(1)
已經能從鼠標取得數據了,緊接著的問題是要解讀這些數據,調查鼠標是怎么移動的,然后結合鼠標的動作,讓鼠標指針相應地動起來。
首先對bootpack.c中的HariMain進行一些修改:
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_phase == 0) /*等待鼠標的0xfa的狀態*/{if (i == 0xfa){mouse_phase = 1;}} else if (mouse_phase == 1) {/*等待鼠標的第一字節*/mouse_dbuf[0] = i;mouse_phase = 2;} else if (mouse_phase == 2) {/* 等待鼠標的第二字節 */mouse_dbuf[1] = i;mouse_phase = 3;} else if (mouse_phase == 3) {mouse_dbuf[2] = i;mouse_phase = 1;/*鼠標的三個字節都齊了,顯示出來*/sprintf(s, "%02X %02X %02X", mouse_dbuf[0], mouse_dbuf[1], mouse_dbuf[2]);boxfill8(binfo->vram, binfo->scrnx, COL8_008484, 32, 16, 32 + 8 * 8 - 1, 31);putfonts8_asc(binfo->vram, binfo->scrnx, 32, 16, COL8_FFFFFF, s);}}}}實際上是將HariMain中for循環部分進行修改,首先把最初讀到的0xfa舍棄掉。之后,每次從鼠標那里送過來的數據都應該是3個字節一組的,所以每當數據累積到3個字節,就把他顯示在屏幕上。
變量mouse_phase用來記住接受鼠標數據的工作進展到了什么階段(phase)。接受到的數據放在mouse_dbuf[0~2]內。
if (mouse_phase == 0) /*等待鼠標的0xfa的狀態*/{各種處理; } else if (mouse_phase == 1) {/*等待鼠標的第一字節*/各種處理;} else if (mouse_phase == 2) {/* 等待鼠標的第二字節 */各種處理;} else if (mouse_phase == 3) {/*鼠標的三個字節都齊了,顯示出來*/各種處理;}這部分就是對于不同的mouse_phase值,相應地做各種不同的處理。顯示結果如下(鼠標移動過):
屏幕上除了括號內的還有三字節數字,即mouse_dbuf[0],mouse_dbuf[1],mouse_dbuf[2]里的數據。”08”部分0會在0~3的范圍內變化,”8”只有在點擊鼠標時才會有變化,值在8~F之間。第二個字節與鼠標的左右移動有關,第三個字節與鼠標的上下移動有關。
2. 整理
在HariMain函數中出現的unsigned char mouse_dbuf[3], mouse_phase;聲明可以放到函數前的結構體里:
struct MOUSE_DEC {unsigned char buf[3], phase; };并在這句話修改為:
struct MOUSE_DEC mdec;創建的這個結構體MOUSE_DEC,DEC是decode的縮寫,用這個結構日把鼠標所需要的變量都歸總到一塊兒。
然后將鼠標的解讀從函數HariMain的接受信息處理中剝離出來,放到了mouse_decode函數。
3. 鼠標解讀(2)
struct MOUSE_DEC {unsigned char buf[3], phase;int x, y ,btn; };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) {/* 等待鼠標第一字節的階段 */if ((dat & 0xc8) == 0x08) {/*如果第一字節正確*/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;}mdec->y = - mdec->y; /*鼠標的y方向與畫面符號相反*/return 1;}return -1; /* 應該不可能到這里來 */ }結構體里增加的幾個變量用于存放解讀結果,這幾個變量是x、y和btn,分別用于存放移動信息和鼠標按鍵狀態。
if (mdec->phase == 1)這個語句用于判斷第一字節對移動有反應的部分是否在0~3的范圍內;同時還要判斷第一字節對點擊有反應的部分是否在8~F的范圍內,如果不在以上數據范圍內就被舍去。這樣做是因為鼠標連線可能會由接觸不良,這樣產生的數據就有錯位,不能順利解讀。
if (mdec->phase == 3)語句是解讀處理的核心。鼠標鍵的狀態放在buf[0]的低3位,我們只取出這3位。十六進制的0x07相當于二進制的0000 0111,通過與運算(&)取出低3位。
x,y基本上直接使用buf[1]和buf[2],但是需要使用第一字節中對鼠標移動有反應的幾位,將x和y的第8位及第8位以后全部都設成1,或全部都保留為0,就能正確解讀x和y。
解讀最后對y符號進行了取反操作是因為鼠標與屏幕的y方向正好相反,為了配合畫面方向,就對y符號進行了取反操作。
鼠標數據解讀完成之后接下來修改顯示部分:
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);}}如果mdec.btn的最低位是1,就把s的第2個字符(第一個字符是s[0])換成‘L’。執行結果如下:
最左邊的字符是因為有按鍵。
4. 移動鼠標指針
現在就是讓鼠標指針在屏幕上動起來啦,感覺好激動,終于能動了。
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 * 8 - 1, my + 15); /*隱藏鼠標*/mx += mdec.x;my += mdec.y;if (mx < x){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, "(%sd, %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); /*顯示坐標*/putfonts8_8(binfo->vram, binfo->scrnx, 16, 16, mx, my, mcursor, 16); /*描畫鼠標*/}先隱藏到鼠標指針,然后根據取得的鼠標數據解讀得到的位移量,讓鼠標顯示在屏幕上。
mx += mdec.x; my += mdec.y;是為了防止鼠標指針跑到屏幕外進行的調整。運行看看,鼠標能動啦!
5. 通往32位模式之路
這里講解了asmhead.nas中的程序。
; PIC關閉一切中斷 ; 根據AT兼容機的規格,如果要初始化PIC, ; 必須在CLI之前進行,否則有時會掛起, ; 隨后進行PIC的初始化MOV AL,0xffOUT 0x21,ALNOP ; 如果連續執行OUT指令,有些機種會無法正常運行OUT 0xa1,ALCLI ; 禁止CPU級別的中斷這段程序等同于一下內容的C程序。
io_out8(PIC0_IMR, 0xff ); /* 禁止主PIC的全部中斷 */ io_out8(PIC1_IMR, 0xff ); /* 禁止從PIC的全部中斷 */ Io_cli(); /*禁止CPU級別的中斷*/為了讓CPU能夠訪問1MB以上的內存空間,設定A20GATE
CALL waitkbdoutMOV AL,0xd1OUT 0x64,ALCALL waitkbdoutMOV AL,0xdf ; enable A20OUT 0x60,ALCALL waitkbdout這里的waitbdout等同于wait_KBC_sendread,等同于C語言中的:
#define KEYCMD_WRITE_OUTPORT 0xd1 #define KBC_OUTPORT_A20G_ENABLE 0xdf/* A20GATE的設定*/Wait_KBC_sendready();Io_out8(PORT_KEYCMD, KEYCMD_WRITE_OUTPORT);Waite_KBC_sendready();Io_out8(PORT_KEYDATA, KBC_OUTPORT_A20G_ENABLE);Waite_KBC_sendready(); /*這句話是為了等待完成執行指令*/程序的基本結構與init_keyboard完全相同,功能僅僅是往鍵盤控制電路發送指令。這里發送的指令,是指令鍵盤控制電路的附屬端口輸出0xdf。這個附屬端口,連接著主板上的很多地方,通過這個端口發送不同的指令,就可以實現各種各樣的控制功能。
這次輸出0xdf所要完成的功能,是讓A20GATE信號線變成ON的狀態。這條信號線的作用是使內存的1MB以上的部分變成可使用狀態。Waite_KBC_sendready();是多余的,在此之后,雖然不會往鍵盤送命令,但仍然要等到下一個命令能夠送來為止。這是為了等待A20GATE的處理切實完成。
; 切換到保護模式[INSTRSET "i486p"] ; “想要使用486指令”的敘述LGDT [GDTR0] ; 設定臨時GDTMOV EAX,CR0AND EAX,0x7fffffff ; 設bit31為0(為了禁止頒)OR EAX,0x00000001 ; 設bit0為1(為了切換到保護模式)MOV CR0,EAXJMP pipelineflush pipelineflush:MOV AX,1*8 ; 可讀寫的段 32bitMOV DS,AXMOV ES,AXMOV FS,AXMOV GS,AXMOV SS,AXINSTRSET指令,是為了能夠使用386以后的LGDT,EAX, CR0等關鍵字。LGDT指令將暫定的GDT讀進來。然后將CR0這一特殊的32寄存器的值帶入EAX,并將最高位置為0,最低位置為1,再將這個值返回給CR0寄存器。這樣就完成了模式轉換,進入到不用頒的保護模式。CR0,也就是control register 0,是一個非常重要的寄存器,只有操作系統才能操作它。
保護模式是指操作系統受到CPU的保護,應用程序既不能隨便改變段的設定,又不能使用操作系統專用的段。
通過帶入CR0而切換到保護模式時,要馬上執行JMP指令。因為變成保護模式后,機器語言的解釋要發生變化。CPU為了加快指令的執行速度而使用了管道(piprline)這一機制。也就是說,前一條指令還在執行的時候就開始解釋下一條甚至再下一條指令。因為模式變了,就要重新解釋一遍,所以加入了JMP指令。
并且在進入保濕模式以后,段寄存器的意思也變了(不再是乘以16后再加算的意思了),除了CS以外所有段寄存器的值都從0x0000變成了0x0008.CS保持原狀是因為如果CS也變了,會造成混亂,所以只有CS要放到后面再處理。0x0008,相當于“gdt+1”的段。
; bootpack的傳送MOV ESI,bootpack ; 傳送源MOV EDI,BOTPAK ; 傳送目的地MOV ECX,512*1024/4CALL memcpy; 磁盤數據最終轉送到它本來的位置去; 首先從啟動扇區開始MOV ESI,0x7c00 ; 傳送源MOV EDI,DSKCAC ; 傳送目的地MOV ECX,512/4CALL memcpy; 所有剩下的MOV ESI,DSKCAC0+512 ; 傳送源MOV EDI,DSKCAC+512 ; 傳送目的地MOV ECX,0MOV CL,BYTE [CYLS]IMUL ECX,512*18*2/4 ; 從柱面數變換為字節數/4SUB ECX,512/4 ; 減去IPLCALL memcpy這部分程序只是在調用memcpy函數,同樣可以理解成C語言形式(寫法可能不正確,但中心思想是類似的):
memcpy(bootpack, BOTPAK, 512*1024/4); memcpy(0x7c00, DSKCAC, 512/4 ); memcpy(DSKCAC0+512, DSKCAC+512, cyls*512*18*2/4 - 512/4);函數mencpy是賦值內存的函數,語法如下:
memcpy(轉送源地址, 轉送目的地址, 轉送數據的大小);
轉送數據大小是以雙字節位單位的,所以數據大小用字節數除以4來指定。
memcpy(0x7c00, DSKCAC, 512/4 );
DSKCAC是0x00100000,所以上面這句話就是從0x7c00復制512字節到0x00100000。這正好是將啟動扇區復制到1MB以后的內存去。
memcpy(DSKCAC0+512, DSKCAC+512, cyls*512*18*2/4 - 512/4);
它的意思是將始于0x00008200的磁盤內容,復制到0x00100200那里。
上文中“轉送數據大小”的計算有點復雜,因為它是以柱面數來計算的,所以需要減去啟動區的那一部分長蘇。IMUL是乘法運算,SUB是減法運算。
bootpack是asmhead,nas的最后一個標簽,haribote.hrb連接起來而生成的,所以asmhead結束的地方,緊接著串聯著bootpack.hrb最前面的部分。
memcpy(bootpack, BOTPAK, 512*1024/4);
→從bootpack的地址開始的512KB內容復制到0x00280000號地址去
這里仍然是在做memcpy,它對bootpack.hrb的header進行解析,將執行必需的數據傳送過去。EBX里帶入的是BOTPAK,所以值如下:
[EBX + 16]……bootpack.hrb之后的第16號地址。值是0x11a8 [EBX + 20]……bootpack.hrb之后的第20號地址。值是0x10c8 [EBX + 12]……bootpack.hrb之后的第12號地址。值是0x00310000上面這些值是通過二進制編輯器,打開harib05d的bootpack.hrb后確認的。這些值因harib的版本不同而有所變化。
SHA指令是向右移位指令,相當于“ECX>>=2;”,JZ時條件轉移指令,來自英文jump if zero,根據前一個計算結果是否為0來決定是否跳轉。
最終這個memcpy的作用是將bootpack.hrb第0x10c8字節開始的0x11a8字節復制到0x00310000號地址去。最后將0x310000代入到ESP里,然后用一個特別的JMP指令,將2*8代入到CS里,同時移動到0x1b號。這里的0x1b號地址是指第2個段的0x1b號地址。第2個段的基地址是0x280000,所以實際上是從0x28001b開始執行的。也就是bootpack.hrb的0x1b號地址。
然后是這個我們制作的這個“紙娃娃系統”的內存分布圖:
0x00000000 - 0x000fffff : 雖然在啟動中會多次使用,但之后就變空。(1MB) 0x00100000 - 0x00267fff : 用于保存軟盤的內容。(1440KB) 0x00268000 - 0x0026f7ff : 空(30KB) 0x0026f800 - 0x0026ffff : IDT(64KB) 0x00270000 - 0x0027ffff : GDT(64KB) 0x00280000 - 0x002fffff : bootpack.hrb(512KB) 0x00300000 - 0x003ffff : 棧及其他(1MB) 0x00400000 - : 空 waitkbdout:IN AL,0x64AND AL,0x02IN AL,0x60 ; 空讀(為了清空數據接收緩沖區中的垃圾數據)JNZ waitkbdout ; AND的結果如果不是0,就跳到waitkbdoutRETwaitbdout與wait_KBC_sendready相同,但也添加了部分處理,就是從0x60號設備進行IN的處理。如果控制器里有鍵盤代碼,或者已經積累了鼠標數據,就順便把它們讀取出來。JNC與JZ相反,意思是“jump if not zero”
memcpy:MOV EAX,[ESI]ADD ESI,4MOV [EDI],EAXADD EDI,4SUB ECX,1JNZ memcpy ; 減法運算的結果如果不是0,就跳轉到memcpyRET復制內存的程序。
ALIGNB 16 GDT0: RESB 8 ; NULL selector DW 0xffff,0x0000,0x9200,0x00cf ; 可以讀寫的段(segment)32bit DW 0xffff,0x0000,0x9a28,0x0047 ; 可以執行的段(segment)32bit(bootpack用)DW 0 GDTR0: DW 8*3-1 DD GDT0ALIGNB 16 bootpack:ALIGNB指令的意思是一直添加DBO,直到時機合適的時候為止,即最初的地址能被16整除。如果標簽GDT0的地址不是8的整數倍,想段寄存器復制的MOV指令就會慢一些。所以插入了ALIGNB指令。
GDT0也是一種特定的GDT,0號是空區域(null sector),不能夠在那里定義段。1號和2號分別由下式設定:
Set_segmdesc(gdt + 1, 0xffffffff, 0x00000000, AR_DATA32_RW); Set_segmdesc(gdt + 2, LIMIT_BOTPAK, ADR_BOTPAK, AR_CODE32_ER);GDTR0時LGDT指令,意思是通知GDT0說“有了GDT喲”。在GDT0里,寫入了16位的段上限,和32位的段起始地址。
最初狀態時,GDT在asmhead.nas里,并不在0x00270000~0x0027ffff的范圍里。IDT連設定都沒設定,所以扔處于中斷禁止的狀態。應當趁著硬件上積累過多數據而產生誤動作之前,盡快開放終端,接收數據。因此,在bootpack.c的HariMain里,應該在進行調色板(palette)的初始化以及畫面的準備之前,先趕緊重新創建GDT和IDT,初始化PIC,并執行”io_sti();”。
總結
以上是生活随笔為你收集整理的《30天自制操作系统》 day8 小结的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【UnityDragonBones】纸娃
- 下一篇: Linux Kernel ‘mp_get