第八天 鼠标控制与32位模式切换
1.鼠标解读(1)
首先要说明一下,鼠标发送回来的数据是三个字节一组的,所以我们要以三个字节为单位处理鼠标数据。
unsigned char mouse_dbuf[3], mouse_phase;
enable_mouse();
mouse_phase = 0;
while (1)
{
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) /* 等待鼠标的第1字节 */
{
mouse_dbuf[0] = i;
mouse_phase = 2;
}
else if (mouse_phase == 2) /* 等待鼠标的第2字节 */
{
mouse_dbuf[1] = i;
mouse_phase = 3;
}
else if (mouse_phase == 3) /* 等待鼠标的第3字节 */
{
mouse_dbuf[2] = i;
mouse_phase = 1;
sprintf(s, "%02X %02X %02X", mouse_dbuf[0], mouse_dbuf[1], mouse_dbuf[2]); /* 将鼠标的3个字节显示出来 */
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)舍弃,在读取完鼠标发送的三个字节后,将他们显示在屏幕上。
运行得到的结果如图所示:
移动鼠标和按下按键会使数据发生改变,这些数据的详细内容会放在第3节讲解。
2.稍事整理
HariMain有点乱,作者在这节就只进行了代码的整理。
struct MOUSE_DEC
{
unsigned char buf[3], phase;
};
void enable_mouse(struct MOUSE_DEC *mdec);
int mouse_decode(struct MOUSE_DEC *mdec, unsigned char dat);
void HariMain(void)
{
...
struct MOUSE_DEC mdec;
...
enable_mouse(&mdec);
while (1)
{
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, "%02X %02X %02X", mdec.buf[0], mdec.buf[1], mdec.buf[2]); /* 将鼠标的3个字节显示出来 */
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);
}
}
}
}
void enable_mouse(struct MOUSE_DEC *mdec) /* 激活鼠标 */
{
wait_KBC_sendready();
io_out8(PORT_KEYCMD, KEYCMD_SENDTO_MOUSE); /* 把发往0x60端口的数据发给鼠标 */
wait_KBC_sendready();
io_out8(PORT_KEYDAT, MOUSECMD_ENABLE); /* 允许鼠标向主机发送数据包 */
/* 顺利的话,键盘控制器会返送回ACK(0xfa) */
mdec->phase = 0;
}
int mouse_decode(struct MOUSE_DEC *mdec, unsigned char dat)
{
if (mdec->phase == 0) /* 等待鼠标返回ACK(0xfa) */
{
if (dat == 0xfa)
{
mdec->phase = 1;
}
return 0;
}
else if (mdec->phase == 1) /* 等待鼠标的第1字节 */
{
mdec->buf[0] = dat;
mdec->phase = 2;
return 0;
}
else if (mdec->phase == 2) /* 等待鼠标的第2字节 */
{
mdec->buf[1] = dat;
mdec->phase = 3;
return 0;
}
else if (mdec->phase == 3) /* 等待鼠标的第3字节 */
{
mdec->buf[2] = dat;
mdec->phase = 1;
return 1;
}
return -1; /* 应该不会到这里来 */
}
此次修改添加了记录鼠标数据的结构体,便于操作。
主要逻辑没有发生变化就不展示效果了,反正和上一节一样。
3.鼠标解读
在讲代码之前,我要向大家介绍PS/2鼠标。
标准的PS/2鼠标支持以下输入:X(左右)位移、Y(上下)位移、左键、中键、右键(没有滚轮)。标准的PS/2鼠标以3字节的数据包格式发送给主机,三个数据意义如下:
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | |
第一字节 | Y溢出 | X溢出 | Y符号 | X符号 | 1 | 中键 | 右键 | 左键 |
第一字节 | X方向移动 | |||||||
第一字节 | Y方向移动 |
- Y溢出:0 - 没有溢出;1 - 表示Y坐标的变化量超出-256~255的范围。
- X溢出:0 - 没有溢出;1 - 表示X坐标的变化量超出-256~255的范围。
- Y符号:Y坐标变化的符号位。1 - 表示负数,即鼠标向下移动。
- X符号:X坐标变化的符号位。1 - 表示负数,即鼠标向右移动。
- 中键:1 - 中键按下
- 右键:1 - 右键按下
- 左键:1 - 左键按下
- 第二、三字节表示鼠标在X、Y坐标上的变化量。
了解了这些知识后我们再来看代码。
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) /* 等待鼠标返回ACK(0xfa) */
{
if (dat == 0xfa)
{
mdec->phase = 1;
}
return 0;
}
else if (mdec->phase == 1) /* 等待鼠标的第1字节 */
{
if ((dat & 0xc8) == 0x08) /* 如果第一字节正确 */
{
mdec->buf[0] = dat;
mdec->phase = 2;
}
return 0;
}
else if (mdec->phase == 2) /* 等待鼠标的第2字节 */
{
mdec->buf[1] = dat;
mdec->phase = 3;
return 0;
}
else if (mdec->phase == 3) /* 等待鼠标的第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;
return 1;
}
return -1; /* 应该不会到这里来 */
}
mouse_decode函数在0xfa阶段,等到了0xfa信号就到第一字节的阶段。在第一字节阶段,有这样一个判断(dat & 0xc8) == 0x08,参考上面的表,第一字节的bit3始终为一,这段代码就是检测是否接受的是第一字节。第二字节阶段没什么好说的。第三字节阶段代码中,mdec->btn = mdec->buf[0] & 0x07; mdec->x = mdec->buf[1]; mdec->y = mdec->buf[2];分别保存按键状态,x和y方向的移动量。但这段代码我不太理解:mdec->x |= 0xffffff00;这段代码是对x方向的数据进行处理,但这不是取反码、补码或移码操作。
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);
}
}
当三个字节取完后,根据不同的按键状态显示不同的信息。
完成了这些步骤,我们就可以移动鼠标了!
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, 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);
}
}
这段代码的代码逻辑是:接收鼠标发来的三个数据,并对数据进行处理,得到按键信息和移动信息,将这些信息显示在屏幕上,然后刷新鼠标的位置。代码虽然长,但理解起来并不难。
效果如下。
能完成鼠标的移动是真的不容易(在理解代码的情况下)。不过这也只是一个简陋的程序,不足之处也有许多,之后也会有改进的方案,使它变得更加完美。
5.通向32位模式之路
一开始学习这本书的时候,我是抱着稳扎稳打的心态,一定要理解每一句代码。但是,作者在之前直接就在asmhead.nas添加了不少内容,我当时人都懵了,这么多行代码书里面怎么没有解释,网上也找不到答案,也只得放一放,想着等学完这本书再去学学汇编搞明白,结果作者把解释放在了第8章。。。
; 使PIC不接受一切中断
; 根据AT兼容机的规格,如果要初始化PIC
; 必须把它放在CLI之前,否则有时会挂起
; 随后进行PIC的初始化
MOV AL, 0xff
OUT 0x21, AL
NOP ; 有的机型不能炼虚使用OUT指令,加上NOP延时
OUT 0xa1, AL
CLI ; 禁止CPU层面插队
如注释所示,这段代码用于禁止中断。
; 为了能够从CPU访问1MB以上的存储器,设定A20 GATE
MOV AL, 0x02
OUT 0x92, AL ; 92h端口的bit1控制着A20,为1时打开
这段代码和书上的不一样,但实现的功能是一样的,都是设定A20 GATE(至于什么是A20 GATE,请自行百度查询,在此讲解的话篇幅会变很长),我也是因为书上的代码太不好理解了,才在网上搜索相关解释,结果发现有很简单的实现方法,我现在学完了第11章也没有出什么错误。
; 保护模式转移
[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 ; 可读区段32位
MOV DS, AX
MOV ES, AX
MOV FS, AX
MOV GS, AX
MOV SS, AX
LGDT指令,不管三七二十一,把随意准备的GDT给读进来。对于这个暂定的GDT,我们以后还要重新设置。然后将CR0这一特殊的32位寄存器的值代人EAX,并将最高位置为0,最低位置为1,再将这个值返回给CR0寄存器,其意义是禁止分页和转换为保护模式。CR0,也就是 control register0,是一个非常重要的寄存器,只有操作系统才能操作它。
保护模式与先前的16位模式不同,段寄存器的解释不是16倍,而是能够使用GDT。在这种模式下,应用程序既不能随便改变段的设定,又不能使用操作系统专用的段。操作系统受到CPU的保护,所以称为保护模式。
这个JMP指令看起来是不是很多余?因为变成保护模式后,机器语言的解释要发生变化。CPU为了加快指令的执行速度而使用了管道( pipeline)这一机制,就是说,前一条指令还在执行的时候,就开始解释下一条甚至是再下一条指令。因为模式变了,就要重新解释一遍,所以加入了JMP指令。
而且在程序中,进入保护模式以后,段寄存器的意思也变了(不再是乘以16后再加算的意思了),除了CS以外所有段寄存器的值都从0x000变成了0x0008。CS保持原状是因为如果CS也变了,会造成混乱,所以只有CS要放到后面再处理。0x0008,相当于“gdt+1”的段。
; boookpack的转送
MOV ESI, bootpack ; 传送源
MOV EDI, BOTPAK ; 传送地址
MOV ECX, 512 * 1024 / 4 ; 一共转移512KB数据
CALL memcpy
; 顺便将磁盘数据也向原来的位置传送
; 首先从引导扇区开始
MOV ESI, 0x7c00 ; 传送源
MOV EDI, DSKCAC ; 传送地址
MOV ECX, 512 / 4 ; IPL有512字节
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
ECX在这里记录的是要传送的次数,因为memcpy是以4字节为单位传送的,所以传送次数就是字节数除以4。memcpy功能的代码如下所示:
memcpy:
MOV EAX, [ESI]
ADD ESI, 4
MOV [EDI], EAX
ADD EDI, 4 ; 每次从原地址复制4个字节到目的地址
SUB ECX, 1
JNZ memcpy ; 如果减法的结果不是0,则进入memcpy
RET
在ESI中放入传送源地址,在EDI中放入传送目的地址,在ECX中放入要传送的次数,再调用memcpy就可以实现复制了。
; 启动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
现在,操作系统的内存分布如下:
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 - : 空
接着是剩余的部分:
ALIGNB 16 ; 填充字节0直到地址能被16整除
GDT0:
RESB 8 ; 选择器(NULL selector)
DW 0xffff, 0x0000, 0x9200, 0x00cf ; 可读区段32位
DW 0xffff, 0x0000, 0x9a28, 0x0047 ; 可执行段32位(用于bootpack)
DW 0
GDTR0:
DW 8 * 3 - 1 ; 16位的段上限
DD GDT0 ; 32位的段起始地址
ALIGNB 16
bootpack:
ALIGNB指令的意思是,一直添加DB0,直到地址能被16整除为止,如果最初的地址能被16整除,则ALIGNB指令不作任何处理。
GDTR0是LGDT指令,意思是通知GDT0有GDT了。在GDT0里,写人了16位的段上限,和32位的段起始地址。
最后在HariMain中要存在io_sti()函数,用以开放中断,接收硬件数据。
本节的内容还是很简单的,实现鼠标的移动也是一件令人振奋的事情。不过,最后一节的内容我也不太清楚,所以没能讲得很详细,请大家见谅。在后面的内容中,涉及到硬件操作的内容会变少,而涉及到算法理解的内容会增多,所以需要更多的时间去理解代码。希望大家能继续学习下去!