【读书笔记-《30天自制操作系统》-7】Day8

本篇讲解实现鼠标移动的最后工作,并且讲解前面没有详细说明的进入32位模式的过程。
在这里插入图片描述

1. 完成鼠标移动功能

鼠标一次发送的按键信息共有3个字节,首先对主程序中信息的数据处理进行优化。

	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);
				}
			}
		}
	}

每次从鼠标接收到的数据都是三个字节一组的,每当集齐三个字节的数据,就显示出来。运行之后的显示如图:
在这里插入图片描述其中B8展示的是键盘按下的数据,28 02 FF为移动鼠标后展示的数据。
第一个字节28中,“2”这一位在移动鼠标时会在0-3范围内变化;而只移动鼠标,“8”这一位不会变化,只有按下鼠标按键(左键,右键,滚轮)时才会发生变化,变化范围在8-F之间。
第二和第三字节分别于鼠标的左右移动和上下移动有关。
根据这些数据的含义,可以对鼠标按键数据进行解读:

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坐标取自第二第三字节。鼠标与屏幕的y方向相反,需要进行取反操作。

完成了鼠标数据的解读,就可以实现鼠标的移动了。

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); /* 描绘出鼠标箭头 */
				}
			}
		}
	}

实现鼠标的移动其实很简单,获取到鼠标的数据后,先隐藏鼠标指针,然后在鼠标指针的坐标上加上解读出来的偏移量,获取鼠标箭头新的坐标。另外对x,y坐标进行了限制,防止鼠标跑到画面之外。获得新的坐标后,将当前的箭头隐藏,在新的坐标处重新描绘出鼠标箭头,就完成了鼠标箭头的移动。
在这里插入图片描述
在这里插入图片描述
为了实现鼠标移动,介绍了这么多准备知识,最后终于完成了。

其实还有一点小问题,比如鼠标移动到下面的任务栏,会变成这样:
在这里插入图片描述
作者说这是因为没有考虑叠加显示的原因,在后面会再进行解释。

2. 进入32位模式过程详解

前面【读书笔记-《30天自制操作系统》-2】Day3中在导入C语言时略过了一段汇编程序。到了这里,作者才详细讲解这部分代码。

在这里插入图片描述

首先是禁止中断。

; PIC关闭一切中断
;	根据AT兼容机的规格,如果要初始化PIC
;	必须在CLI之前进行,否则有时会挂起
;	随后进行PIC的初始化

		MOV		AL,0xff
		OUT		0x21,AL
		NOP						; 如果连续执行OUT指令,有些机种会无法正常执行
		OUT		0xa1,AL

		CLI						; 禁止CPU级别的中断

以上的程序目的在于禁止主PIC,从PIC以及CPU级别的所有中断,然后进行PIC的初始化。NOP指令什么也不做,是让CPU休息一个时钟长的时间。

下面是重点:为了访问1MB以上的内存,使能A20地址线。


```c
; 为了让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,这段程序完成的功能是往键盘控制电路发送指令。这里向键盘控制电路发送0xdf,就可以完成使能A20的工作,1M以上的地址CPU就可以访问到了。因为之前只有16位CPU时,内存只有1M。而后来的CPU为了向前兼容,还是保留了这一阶段,在激活A20之前,操作系统能够访问的内存还是只有1M。

接下来切换到保护模式:

[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,AX

INSTRSET指令是为了能够使用386以后的LGDT,EAX,CR0等关键字。
这里LGDT指令先载入一个临时的GDT。前文已经讲解了,进入32位模式之后还要重新设置GDT。
下面四条指令是通过EAX将CR0寄存器中的值的最高位置位0,最低位置为1,这样就进入了保护模式。CR0是一个重要的寄存器,只有操作系统可以操作。
在保护模式下,应用程序不能随意修改段的设定,也不能使用操作系统专用的段,操作系统受到CPU的保护,因此称为保护模式。
切换到保护模式后,马上就要执行JMP指令。这是因为进入保护模式后,机器语言的解释发生变化,CPU为了加快指令的执行速度使用了管道机制(pipeline),即前一条指令还在执行时,就开始解释下一条甚至再下一条指令。模式发生了变化,因此需要重新解释一次。

; 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

以上代码实质上是在进行数据的复制。
数据的转送是是以双字为单位的,因此转送的长度需要用字节数/4。

先来看上面程序中的第二段。即从0x7c00开始,复制512字节到DSKCAC位置。DSKCAC是0x00100000,因此上面其实是将启动区复制到1M内存之后的位置。
同理,第三段就是将从0x00008200开始的磁盘内容复制到0x00100200的位置,也就是启动区之后的全部内容。这里是通过柱面数来换算复制的长度,最后还要减去启动区的长度。
最后,第一段实现的是将bootpack标签开始的512KB复制到0x00280000号地址。

以上的汇编语言放在程序asmhead.nas文件中,在编译时生成的asmhead.bin与主函数所在的文件bootpack.c生成的bootpack.hrb连接起来。上面这些代码执行完毕,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

从效果上来看,这部分代码还是在复制数据,类似于memcpy。
bootpack.c生成的bootback.hrb中加上了header,这里解析header,将执行所必需要的数据传送过去。
[EBX+16] …… bootpack.hrb之后的第16号地址,值是0x11a8
[EBX+20] ……bootpack.hrb之后的第20号地址,值是0x10c8
[EBX+12] ……bootpack.hrb之后的第12号地址,值是0x00310000

最终实现的效果就是将bootpack.hrb的第0x10c8字节开始的0x11a8字节复制到0x00310000号地址。至于这样操作的意义,还要等本书的整个操作系统讲完才能讲解清楚。
将0x310000带入到ESP,跳转到2*8:0x000001b,即第2个段的0x1b号地址。第2个段的基地址是0x280000,所以是从0x28001b继续执行,也就是bootpack.hrb的0x1b号地址。这样就开始执行bootpack.hrb了。

最后再来看一下作者整理的本书所制作的操作系统的内存分布图:
0x00000000 - 0x000fffff:虽然在启动中会多次使用,但之后就会变空。(1MB)
0x00100000 - 0x00267fff:用于保存软盘的内容。(1440KB)
0x00268000 - 0x0026f7ff:空。(30KB)
0x0026f800 - 0x0026fff:IDT (2KB)
0x00270000 - 0x0027ffff:GDT (64KB)
0x00280000 - 0x002fffff:bootpack.hrb (512KB)
0x00300000 - 0x003fffff:栈及其他 (1MB)
0x00400000 - 空
对于读者来说可能无所谓,但是有了这张图的规划,作者的开发才更加井井有条吧。

本篇的难度有些提高,要更加努力了。下一篇的内容是内存管理,敬请期待。

  • 13
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值