2020.10.21
今天非常开心,实现了鼠标的移动,但通往32位模式的任务有点艰巨。稳住!
文档:harib05a
我们已经能从鼠标取得数据了,接来下就是解读这些数据让鼠标动起来,这听起来很简单。
先来对bootpack.c的HariMain函数进行一些修改:
//这次HariMain的修改部分 unsigned char mouse_dbuf[3], mouse_phase; enable_mouse(); mouse_phase = 0; /* 进入到等待鼠标的0xfa的状态 */ 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; /* 鼠标的3个字节都齐了,显示出来 */ 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); } } } }首先把最初读到的答复信息0xfa舍弃,然后将鼠标送来的数据按三个字节一组接收。
变量mouse_phase用来记住接收鼠标数据的工作进展到了什么阶段(phase)。接收到的数据放在mouse_dbuf[0-2]中。
试运行一下
操作鼠标,屏幕上出现了“38 FB FE”之类的3字节数字。
如果移动鼠标,这个“38”部分(mouse_dbuf[0])的3那一位会在0-3的范围内变化,如果只是移动鼠标8那一位不会有任何变化,只有点击鼠标时才会变化,这个值会在8-F之间变化。
"FB"部分(mouse_dbuf[1])和鼠标的左右移动有关,“FE”部分(mouse_dbuf[2])和鼠标的上下移动有关。
创建了一个结构体MOUSE_DEC。DEC是decode是缩写,创建这个结构体,把用来解读鼠标的变量都归结到一块。
在函数enable_mouse的最后,附加了mouse_phase = 0的操作。
将鼠标的解读从函数HariMain中剥离出来,放到mouse_decode函数里。HariMain又清晰了起来。
对mouse_decode函数修改
//bootpack.c 节选 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(medc->phase == 1)语句,判断第一字节对移动有反应的部分是否在0-3的范围内,对点击有反应的部分是否在8-F的范围内。如果不在以上范围内就会被舍弃。以防鼠标接触不良造成的数据丢失错位,对第一字节加上检查。
解读处理核心if(medc->phase == 3):鼠标键的状态放在dbuf[0]的低3位,使用&运算取出。x,y直接使用buf[1]和buf[2],但是只需要一字节对鼠标移动有反应的部分,所以int类型的x和y的8-31位都设置为1或0才能被正确解读。最后还要对y的符号取反,因为鼠标与屏幕的y方向正好相反。
修改显示部分
//HariMain 节选 } 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); } }检查btn的值并根据相应的值将s的值置换成相应的字符串。
试运行一下
一切正常。
文档:harib05d
鼠标的解读部分已经完成了,下面再改一改图形显示部分让鼠标动起来就行了。
//HariMain 节选 } 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); /* 描画鼠标 */ } }从鼠标移动开始部分是新加的。首先将原来的鼠标隐藏掉,然后根据鼠标解读出的数据描绘出新的鼠标。中间的if语句是用来防止鼠标跑到屏幕外面去。
下面是紧张刺激的测试环节。
从第五天到第八天,终于实现鼠标的移动了。
但是我们又发现了一点小问题,移动到下方,鼠标会把任务栏给吃了。暂时先把这个问题放一放。
下面说明一下进入32位模式大约100行的asm.head中的程序。
;asmhead.nas节选 ; PIC关闭一切中断 ; 根据AT兼容机的规格,如果要初始化PIC, ; 必须在CLI之前进行,否则有时会挂起。 ; 随后进行PIC的初始化。 MOV AL,0xff OUT 0x21,AL NOP ; 如果连续执行OUT指令,有些机种会无法正常运行 OUT 0xa1,AL CLI ; 禁止CPU级别的中断这段程序等同于以下内容的C程序:
io_out(PIC0_IMR, 0xff); /* 禁止主PIC的全部中断 */ io_out(PIC1_IMR, 0xff); /* 禁止从PIC的全部中断 */ io_cli(); /* 禁止CPU级别的中断*/当CPU进行模式转换时不允许有中断发生,而且后面还要进行PIC的初始化,也不允许中断的发生。
继续往下看
;asmhead.nas节选(续) ; 为了让CPU能够访问1MB以上的内存空间,设定A20GATE CALL waitkbdout MOV AL,0xd1 OUT 0x64,AL CALL waitkbdout MOV AL,0xdf ; enable A20 OUT 0x60,AL CALL waitkbdout这里的waitkbdout等同于wait_KBC_sendready。这段程序在C语言中大致如下:
#define KEYCMD_WRITE_OUTPORT 0xd1 #define KBC_OUTPORT_A20G_ENABLE 0xdf /* A20GATE的设定 */ wait_KBC_sendready(); io_out8(PORT_KEYCMD, KEYCMD_WRITE_OUTPORT); wait_KBC_sendready(); io_out8(PORT_KEYDAT, KBC_OUTPORT_A20G_ENABLE); wait_KBC_sendready(); /* 这句话是为了等待完成执行指令 */程序的基本结构与init_keyboard完全相同,功能仅仅是往KBC发送指令。
这里发送的指令是KBC的附属端口输出0xdf。这个附属端口,连接着主板上的很多地方,通过这个端口发送不同的指令就可以实现各种各样的控制功能。
这次输出0xdf的功能是让A20GATE信号线变成ON的状态。**这条信号线的作用是什么呢?**它能使内存的1MB以上的部分变成可使用状态。最初出现电脑的时候,CPU只有16位模式,所以内存最大也只有1MB。后来CPU可以使用很大的内存了,可是为了兼容旧版的操作系统,在执行激活指令之前,电路被限制为只能使用1MB内存。
最后的wait_KBC_sendready();是多余的,在此之后虽然不会往键盘送命令,但仍然要等到下一个命令能够送来为止。这是为了等待A20GATE的处理切实完成。
继续往下
;asmhead.nas节选 (续) ; 切换到保护模式 [INSTRSET "i486p"] ; “想要使用486指令”的叙述 LGDT [GDTR0] ; 设定临时GDT MOV EAX,CR0 AND EAX,0x7fffffff ; 设bit31为0(为了禁止分页) OR EAX,0x00000001 ; 设bit0为1(为了切换到保护模式) MOV CR0,EAX JMP pipelineflush pipelineflush: MOV AX,1*8 ; 可读写的段 32bit MOV DS,AX MOV ES,AX MOV FS,AX MOV GS,AX MOV SS,AXINSTRSET指令,是为了能够使用386以后的LGDT,EAX,CR0等关键字。
LGDT指令,不管三七二十一,先把GDT给读进来,对于这个暂定的GDT我们以后还要设置。
将CR0这一特殊的32位寄存器的值代入EAX,并将最高位置为0,最低位置为1,在将这个值返回给CR0。这样就完成了模式转换。
CR0(Contorl Register 0):非常重要的寄存器,只有操作系统才能操作它。
保护模式与先前的16位模式不同,段寄存器的解释不是16倍,而是能够使用GDT。在这种模式下,应用程序既不能随便改变段的设定,也不能使用操作系统专用的段。操作系统收到CPU的保护,所以称为保护模式。
保护模式分带保护的16位模式和带保护的32模式,我们要使用的是带保护的32位模式。
通过带入而切换到保护模式时,要马上执行JMP指令。**为什么要执行JMP指令呢?**进入保护模式后,机器语言的解释也发生了变化,CPU为了加快指令的执行速度而使用了管道(pipeline)机制。前一条指令还要执行的时候,就开始解释下一条指令甚至是再下一条指令。因为模式变了,就要重新解释一遍,所以加入了JMP指令。
进行保护模式后,段寄存器的意思也变了,除了CS以外的段寄存都从0x0000变成了0x0008(相当于“gdt+1”段)。CS保持原状是因为如果CS也变了,会找不到下条指令,所以把CS放到后面再处理。
继续往下
;asmhead.nas节选(续) ; bootpack的转送 MOV ESI,bootpack ; 转送源 MOV EDI,BOTPAK ; 转送目的地 MOV ECX,512*1024/4 CALL memcpy ; 磁盘数据最终转送到它本来的位置去 ; 首先从启动扇区开始 MOV ESI,0x7c00 ; 转送源 MOV EDI,DSKCAC ; 转送目的地 MOV ECX,512/4 CALL memcpy ; 所有剩下的 MOV ESI,DSKCAC0+512 ; 转送源 MOV EDI,DSKCAC+512 ; 转送目的地 MOV ECX,0 MOV CL,BYTE [CYLS] IMUL ECX,512*18*2/4 ; 从柱面数变换为字节数/4 SUB ECX,512/4 ; 减去 IPL CALL 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);memcpy是复制内存的函数,语法如下: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那里。上文中“转送数据大小”的计算有点复杂,因为它是以柱面数来计算的,所以需要减去启动区的那一部分长度。这样始于0x00100000的内存部分,就与磁盘的内容相吻合了。
memcpy(bootpack, BOTPAK, 512*1024/4);
bootpack是asmhead.nas的最后一个标签。haribote.sys是通过asmhead.bin和bootpack.hrb连接起来而生成的(可以通过Makefile确认),所以asmhead结束的地方,紧接着串连着bootpack.hrb最前面的部分。表示从bootpack的地址开始的512KB内容复制到0x00280000号地址去。为什么是512KB呢?内存多一些不会产生什么问题,所以这个长度要比bootpack.hrb的长度大出很多。
继续往下
;asmhead.nas节选(续) ; 必须由asmhead来完成的工作,至此全部完毕 ; 以后就交由bootpack来完成 ; bootpack的启动 MOV EBX,BOTPAK MOV ECX,[EBX+16] ADD ECX,3 ; ECX += 3; SHR ECX,2 ; ECX /= 4; JZ skip ; 没有要转送的东西时 MOV ESI,[EBX+20] ; 转送源 ADD ESI,EBX MOV EDI,[EBX+12] ; 转送目的地 CALL memcpy skip: MOV ESP,[EBX+12] ; 栈初始值 JMP DWORD 2 * 8 : 0x0000001b对bootpack.hrb的header(头部内容)进行解析,将执行所必须的数据传送过去。
[EBX + 16]…bootpack.hrb之后的第16号地址。值是0x11a8 [EBX + 20]…bootpack.hrb之后的第20号地址。值是0x10c8 [EBX + 12]…bootpack.hrb之后的第12号地址。值是0x00310000
上面这些值,是我们通过二进制编辑器打开harib05d的bootpack.hrb后确认的,这些值会因harib的版本不同而改变。
**最终这个memcpy到底用来做什么事情呢?**它会将bootpack.hrb第0x10c8字节开始的0x11a8字节复制到0x00310000号地址去。大家可能不明白为什么要做这种处理,但这个问题,必须要等到“纸娃娃系统”的应用程序讲完之后才能讲清楚,所以大家现在不懂也没关系,我们以后还会说明的。最后将0x310000代入到ESP里,然后用一个特别的JMP指令,将2 * 8 代入到CS里,同时移动到0x1b号地址。这里的0x1b号地址是指第2个段的0x1b号地址。第2个段的基地址是0x280000,所以实际上是从0x28001b开始执行的。这也就是bootpack.hrb的0x1b号地址。 这样就开始执行bootpack.hrb了。
0x00000000 - 0x000fffff : 虽然在启动中会多次使用,但之后就变空。(1MB) 0x00100000 - 0x00267fff : 用于保存软盘的内容。(1440KB) 0x00268000 - 0x0026f7ff : 空(30KB) 0x0026f800 - 0x0026ffff : IDT (2KB) 0x00270000 - 0x0027ffff : GDT (64KB) 0x00280000 - 0x002fffff : bootpack.hrb(512KB) 0x00300000 - 0x003fffff : 栈及其他(1MB) 0x00400000 - : 空
这个内存分布图是跟着感觉走决定出来的。从软盘读出来的东西,之所以要复制到0x00100000号以后的地址,就是因为我们意识中有这个内存分布图。
waitkbdout
;asmhead.nas节选(续) waitkbdout: IN AL,0x64 AND AL,0x02 IN AL,0x60 ; 空读(为了清空数据接收缓冲区中的垃圾数据) JNZ waitkbdout ; AND的结果如果不是0,就跳到waitkbdout RET这就是waitkbdout所完成的处理。基本上,如前面所说的那样,它与wait_KBC_sendready相同,但也添加了部分处理,就是从0X60号设备进行IN的处理。也就是说,如果控制器里有键盘代码,或者是已经累积了鼠标数据,就顺便把它们读取出来。
memcpy
;asmhead.nas节选(续) memcpy: MOV EAX,[ESI] ADD ESI,4 MOV [EDI],EAX ADD EDI,4 SUB ECX,1 JNZ memcpy ; 减法运算的结果如果不是0,就跳转到memcpy RET懂得都懂
最后一点
;asmhead.nas节选(续) 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 GDT0 ALIGNB 16 bootpack:ALIGNB指令的意思是,一直添加DB 0,直到时机合适的时候为止。什么是“时机合适”呢?大家可能有点不明白。ALIGNB 16的情况下,地址能被16整除的时候,就称为“时机合适”。如果最初的地址能被16整除,则ALIGNB指令不作任何处理。
如果标签GDT0的地址不是8的整数倍,向段寄存器复制的MOV指令就会慢一些。所以我们插入了ALIGNB指令。但是如果这样,“ALIGNB 8”就够了,用“ALIGNB 16”有点过头了。最后的“bootpack:”之前,也是“时机合适”的状态,所以笔者就适当加了一句“ALIGNB 16”。
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位的段起始地址。
到此为止,关于asmhead.nas的说明就结束了
最初状态时,GDT在asmhead.nas里,并不在0x00270000 ~ 0x0027ffff的范围里。IDT连设定都没设定,所以仍处于中断禁止的状态。应当趁着硬件上积累过多数据而产生误动作之前,尽快开放中断,接收数据。 因此,在bootpack.c的HariMain里,应该在进行调色板(palette)的初始化以及画面的准备之前,先赶紧重新创建GDT和IDT,初始化PIC,并执行“io_sti();”。
//bootpack.c节选 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);明天见!
