要了解如何从键盘获取输入数据,就必须首先了解PS/2 Controller(PS/2控制器),有关PS/2控制器的相关内容可以参考...

    v0.0.10的项目地址:

    github.com地址:https://github.com/zenglong/zenglOX (只包含git提交上来的源代码)
   
    Dropbox地址:点此进入Dropbox网盘  (该版本位于zenglOX_v0.0.10的文件夹,该文件夹里的zip压缩包为源代码)

    sourceforge地址:https://sourceforge.net/projects/zenglox/files  (该版本位于zenglOX_v0.0.10的文件夹,该文件夹里的zip压缩包为源代码)

    有关zenglOX的编译及gdb调试的方法,请参考zenglOX v0.0.1里的文章。

    要了解如何从键盘获取输入数据,就必须首先了解PS/2 Controller(PS/2控制器),有关PS/2控制器的相关内容可以参考 http://wiki.osdev.org/%228042%22_PS/2_Controller 该链接里的文章,下面只做个简单的介绍。

    PS/2控制器(通常也被称作"键盘控制器")位于主板上,早期的控制器是一个单一的芯片(即Intel 8042芯片),现在则是 Advanced Integrated Peripheral (高级的集成外设芯片)的一部分。

    PS/2控制器的控制原理图如下:


图1

    从上图可以看到,PS/2控制器的右侧分别连接了Keyboard(键盘)和Mouse(鼠标),键盘的输入信号会先传到PS/2控制器,再由该控制器通过IRQ通道向IRQ1发送中断请求,CPU收到该中断请求后,就会在IDT(中断描述符表)里查找并跳转到对应的处理例程,在处理例程中就可以通过0x60端口从PS/2控制器里获取到所需的数据(比如键盘输入的按键扫描码,扫描码的概念下面会介绍)。

    另外,从上图还可以看到,PS/2控制器除了可以控制键盘,鼠标之类的PS/2外设,还可以触发CPU Reset(即系统复位),CPU Reset的相关内容请参考上面提到的 http://wiki.osdev.org/%228042%22_PS/2_Controller 该链接里的文章。

    根据上面图1所示,我们知道PS/2控制器会将键盘的输入事件转成IRQ1中断请求,因此,我们就可以在键盘的初始化代码里先设置好键盘的中断回调函数,和键盘相关的代码主要包含在新增的zlox_keyboard.c文件里:

// zlox_keyboard.c implement interrupt and functions of keyboard
..........................................
..........................................

ZLOX_VOID zlox_initKeyboard()
{
	zlox_register_interrupt_callback(ZLOX_IRQ1,&zlox_keyboard_callback);

	led_status = 0; /* All leds off */
	zlox_setleds();
}


    上面的zlox_initKeyboard函数就是键盘的初始化代码,该代码里通过zlox_register_interrupt_callback(ZLOX_IRQ1,&zlox_keyboard_callback)函数将IRQ1的中断回调函数设置为zlox_keyboard_callback,该回调函数也位于zlox_keyboard.c文件里,下面会进行介绍,在初始化代码中,还有一个zlox_setleds函数,该函数是用于设置键盘的LED灯的。

    键盘上主要有ScrollLock,NumberLock和CapsLock三个LED,这里需要先了解下PS/2控制器的两个I/O端口:0x60和0x64,这两个端口的作用如下:

IO Port Access Type Purpose
0x60 Read/Write Data Port
0x64 Read Status Register
0x64 Write Command Register

    0x60是Data Port(数据端口),可以从该端口读取数据(例如键盘中断回调函数里,会利用该端口来读取输入的按键扫描码),还可以向该端口写入一些命令,向该端口写入的命令会发送给PS/2连接的外设(如键盘),这样就可以向键盘直接发送一些命令,比如设置LED灯的命令。

    0x64端口也可以进行读或写的操作,当从该端口读取数据时,得到的是Status Register(状态寄存器)的值,状态寄存器是用于存储PS/2控制器的各种状态的,该寄存器里各二进制位的含义如下:

Bit Meaning
0 Output buffer status (0 = empty, 1 = full) 
(must be set before attempting to read data from IO port 0x60)
1 Input buffer status (0 = empty, 1 = full) 
(must be clear before attempting to write data to IO port 0x60 or 
IO port 0x64)
2 System Flag 
Meant to be cleared on reset and set by firmware 
(via. PS/2 Controller Configuration Byte) 
if the system passes self tests (POST)
3 Command/data 
(0 = data written to input buffer is data for PS/2 device, 
1 = data written to input buffer is data for 
PS/2 controller command)
4 Unknown (chipset specific) 
May be "keyboard lock" 
(more likely unused on modern systems)
5 Unknown (chipset specific) 
May be "receive time-out" or 
"second PS/2 port output buffer full"
6 Time-out error (0 = no error, 1 = time-out error)
7 Parity error (0 = no error, 1 = parity error)

    zenglOX里主要用到的是位0和位1,PS/2有两个buffer数据缓冲(每个都是一个字节的大小),即上表里显示的Output buffer和Input buffer,上表的Output buffer(输出缓冲)是相对于PS/2控制器的,当PS/2控制器接收到键盘之类的外设的输入信号时,就会将数据放置到Output buffer,同时将状态寄存器的位0设置为1,表示Output buffer里有数据,此时,操作系统的程式就可以通过0x60端口将Output buffer里的数据读取出来。

    另一个Input buffer里存储的是操作系统程式通过0x60或0x64端口写入的等待发送给PS/2控制器或外设的数据,必须在状态寄存器的位1为0时(即Input buffer为空时),才可以向0x60或0x64端口写入数据。

    向0x64端口写入的命令字节是发送给PS/2控制器的,由于zenglOX里并没有向PS/2控制器发送什么命令,所以这里就不多做介绍,有关PS/2控制器的命令详情可以参考上面提到的 http://wiki.osdev.org/%228042%22_PS/2_Controller 链接里的PS/2 Controller Commands部分。

    设置LED灯主要是向键盘外设发送命令(通过向0x60端口写入命令字节),PS/2键盘可接受的命令字节可以参考 http://wiki.osdev.org/PS/2_Keyboard 该链接里的文章,下面只显示和LED灯设置有关的命令:

Command Byte     Data Byte/s
0xED
LED states: 
 
Bit Use
0 ScrollLock
1 NumberLock
2 CapsLock
 
Note: Other bits may be used in 
international keyboards 
for other purposes 
(e.g. a Japanese keyboard might use 
bit 4 for a "Kana mode" LED).
   
    可以先向0x60写入0xED的Command Byte(命令字节),再向0x60写入Data Byte(数据字节),数据字节的位0对应ScrollLock,当位0被设置为1时,表示打开ScrollLock的LED灯,当位0被清零时,则表示关闭ScrollLock的LED灯,同理,位1对应NumberLock,位2对应CapsLock , 有些日式键盘可能会使用位4作为特殊用途的LED 。

    因此,zlox_keyboard.c文件的zlox_setleds函数的代码如下:

ZLOX_VOID zlox_setleds()
{
	zlox_outb(0x60, 0xED);
	while(zlox_inb(0x64) & 2);
	zlox_outb(0x60, led_status);
	while(zlox_inb(0x64) & 2);
}


    zlox_setleds函数里的led_status是一个全局变量,用于表示需要设置的和LED相关的数据字节,在每次向0x60端口写入数据后,都需要通过while(zlox_inb(0x64) & 2)读取0x64端口,来取得当前的状态寄存器的值,如果该值的位1被清零,则表示Input buffer为空闲状态,即上一次的输入已经执行完毕,才可以对0x60端口进行下一次的写入操作。

    在讲解键盘的中断回调函数之前,有必要先了解下按键扫描码,当在键盘上按下某个键时,键盘就会向PS/2控制器发送一个扫描码,通过扫描码的值就可以知道是哪个键被按下了,有三种类型的扫描码:scan code set 1(扫描码集1),scan code set 2(扫描码集2),scan code set 3(扫描码集3) 这三种。

    其中,scan code set 1是最原始的扫描码集合,scan code set 2是现在的键盘默认支持的扫描码,scan code set 3是比较新的更复杂的一种扫描码。

    zenglOX里使用的是scan code set 1,为何不用scan code set 2呢? 这是因为默认的初始状态下,PS/2控制器处于Translation mode(翻译模式),该模式会将键盘产生的不同类型的扫描码转成scan code set 1类型,所以从0x60端口读取到的扫描码默认会是scan code set 1的扫描码。

    如果你自己编写的键盘驱动里要使用scan code set 2或scan code set 3的扫描码的话,就必须先将Translation mode(翻译模式)给关闭掉。关闭的方法请参考上面提到的 http://wiki.osdev.org/%228042%22_PS/2_Controller 链接里的PS/2 Controller Configuration Byte部分,该部分的配置字节里有一个First PS/2 port translation的内容,该内容可以用于关闭Translation mode(翻译模式)。

    scan code set 1的完整扫描码集合(包含了每个按键对应的扫描码的值)请参考 http://wiki.osdev.org/PS/2_Keyboard 链接的Scan Code Set 1部分(仅用于US美式键盘)。

    扫描码可以在键盘的中断回调函数里通过0x60端口来获取到,在获取到扫描码后,就可以将扫描码通过数组映射为对应按键的ASCII码了。

    在上面初始化键盘时,已经将键盘的中断回调函数设置为了zlox_keyboard_callback函数,该函数的定义如下:

// zlox_keyboard.c implement interrupt and functions of keyboard
...............................................
...............................................
static ZLOX_VOID zlox_keyboard_callback(/*ZLOX_ISR_REGISTERS * regs*/)
{
	ZLOX_UINT32 key = zlox_inb(0x60);
	ZLOX_UINT32 key_ascii = 0;	
	
	/* 'LED Keys', ie, Scroll lock, Num lock, and Caps lock */
	if(key == 0x3A)	/* Caps Lock */
	{
		led_status ^= ZLOX_LED_CAPS_LOCK;
		zlox_setleds();
	}
	if(key == 0x45)	/* Num Lock */
	{
		led_status ^= ZLOX_LED_NUM_LOCK;
		zlox_setleds();
	}
	if(key == 0x46) /* Scroll Lock */
	{
		led_status ^= ZLOX_LED_SCROLL_LOCK;
		zlox_setleds();
	}

	if(key == 0x1D && !(control_keys & ZLOX_CK_CTRL))	/* Ctrl key */
		control_keys |= ZLOX_CK_CTRL;
	if(key == 0x80 + 0x1D)	/* Ctrl key depressed */
		control_keys &= (0xFF - ZLOX_CK_CTRL);
	if((key == 0x2A || key == 0x36) && !(control_keys & ZLOX_CK_SHIFT))	/* Shift key */
		control_keys |= ZLOX_CK_SHIFT;
	if((key == 0x80 + 0x2A) || (key == 0x80 + 0x36))	/* Shift key depressed */
		control_keys &= (0xFF - ZLOX_CK_SHIFT);
	if(key == 0x38 && !(control_keys & ZLOX_CK_ALT))
		control_keys |= ZLOX_CK_ALT;
	if(key == 0x80 + 0x38)
		control_keys &= (0xFF - ZLOX_CK_ALT);
		
	if((control_keys & ZLOX_CK_SHIFT) && (led_status & ZLOX_LED_CAPS_LOCK)) 
		key_ascii = scanToAscii_table[key][6]; 
	else if(control_keys & ZLOX_CK_SHIFT) 
		key_ascii = scanToAscii_table[key][1];
	else if(control_keys & ZLOX_CK_CTRL) 
		key_ascii = scanToAscii_table[key][2];
	else if(control_keys & ZLOX_CK_ALT) 
		key_ascii = scanToAscii_table[key][3];	
	else if((control_keys & ZLOX_CK_SHIFT) && (led_status & ZLOX_LED_NUM_LOCK)) 
		key_ascii = scanToAscii_table[key][7];
	else if(led_status & ZLOX_LED_CAPS_LOCK) 
		key_ascii = scanToAscii_table[key][5];
	else if(led_status & ZLOX_LED_NUM_LOCK) 
		key_ascii = scanToAscii_table[key][4];
	else if(control_keys == 0) 
		key_ascii = scanToAscii_table[key][0];

	if(key_ascii != 0)
	{
		if(key_ascii <= 0xFF)
		{
			keyboard_buffer[keyboard_buffer_size] = key_ascii;
			keyboard_buffer_size++;
			zlox_monitor_put(key_ascii);
		}
		else
		{
			keyboard_buffer[keyboard_buffer_size] = (key_ascii & 0xFF);
			keyboard_buffer[keyboard_buffer_size+1] = (key_ascii & 0xFF00);
			keyboard_buffer_size += 2;
		}
	}
}


    上面代码通过zlox_inb(0x60)读取0x60端口,并从该端口里获取到键盘输入的扫描码,然后先根据扫描码判断是否按下的是Caps Lock键(对应扫描码为0x3A),Num Lock键(扫描码为0x45),或者Scroll Lock键(扫描码为0x46),如果是这三个键,则通过zlox_setleds函数打开或关闭对应的LED灯。

    然后将扫描码和Ctrl,Shift,Alt三个控制键的扫描码进行比较,并根据比较的结果设置control_keys变量,通过该控制变量的值以及LED灯的状态,就可以从scanToAscii_table数组里将扫描码映射为对应的ASCII值。

    scanToAscii_table是一个全局的二维数组,里面存储了scan code set 1类型的扫描码对应的ASCII值。

    例如,w按键的scan code set 1类型的扫描码为0x11,对应十进制为17,则scanToAscii_table[17]为一维数组,表示w按键在各种状态下的ASCII值:

// zlox_keyboard.c implement interrupt and functions of keyboard
.........................................
.........................................

ZLOX_UINT32 scanToAscii_table[][8] = 
{
/* 	ASCII -	Shift - Ctrl - 	Alt - 	Num - 	Caps - 	Shift Caps - 	Shift Num */
{  	0,	0,	0,	0,	0,	0,	0,		0},
{	0x1B,	0x1B,	0x1B,	0,	0x1B,	0x1B,	0x1B,		0x1B},
/* 1 -> 9 */
{	0x31,	0x21,	0,	0x7800,	0x31,	0x31,	0x21,		0x21},
{	0x32,	0x40,	0x0300,	0x7900,	0x32,	0x32,	0x40,		0x40},
{	0x33,	0x23,	0,	0x7A00, 0x33,	0x33,	0x23,		0x23},
{	0x34,	0x24,	0,	0x7B00, 0x34,	0x34,	0x24,		0x24},
{	0x35,	0x25,	0,	0x7C00,	0x35,	0x35,	0x25,		0x25},
{	0x36,	0x5E,	0x1E,	0x7D00, 0x36,	0x36,	0x5E,		0x5E},
{	0x37,	0x26,	0,	0x7E00,	0x37,	0x37,	0x26,		0x26},
{	0x38,	0x2A,	0,	0x7F00, 0x38,	0x38,	0x2A,		0x2A},
{	0x39,	0x28,	0,	0x8000, 0x39,	0x39,	0x28,		0x28},
{	0x30,	0x29,	0,	0x8100,	0x30,	0x30,	0x29,		0x29},
/* -, =, Bksp, Tab */
{	0x2D,	0x5F,	0x1F,	0x8200,	0x2D,	0x2D,	0x5F,		0x5F},
{	0x3D,	0x2B,	0,	0x8300,	0x3D,	0x3D,	0x2B,		0x2B},
{	0x08,	0x08,	0x7F,	0,	0x08,	0x08,	0x08,		0x08},
{	0x09,	0x0F00,	0,	0,	0x09,	0x09,	0x0F00,		0x0F00},
/*	QWERTYUIOP[] */
{	0x71,	0x51,	0x11,	0x1000,	0x71,	0x51,	0x71,		0x51},
{	0x77,	0x57,	0x17,	0x1100,	0x77,	0x57,	0x77,		0x57},
.........................................
.........................................


    上面的{0x77, 0x57, 0x17, 0x1100, 0x77, 0x57, 0x77, 0x57}就是scanToAscii_table[17]对应的一维数组,其中第一个元素0x77为"w"小写字母的ASCII值,第二个元素0x57为"W"大小字母的ASCII值,当Caps Lock对应的LED灯打开时,根据zlox_keyboard_callback中断回调函数里的代码:

// zlox_keyboard.c implement interrupt and functions of keyboard
............................................
............................................
static ZLOX_VOID zlox_keyboard_callback(/*ZLOX_ISR_REGISTERS * regs*/)
{
............................................
............................................
	else if(led_status & ZLOX_LED_CAPS_LOCK) 
			key_ascii = scanToAscii_table[key][5];
	else if(led_status & ZLOX_LED_NUM_LOCK)
			key_ascii = scanToAscii_table[key][4];
	else if(control_keys == 0)
			key_ascii = scanToAscii_table[key][0];
............................................
............................................
}


    会得到scanToAscii_table[key][5]的ASCII值,如果是w按键则是scanToAscii_table[17][5]即0x57,也就是"W"大小字母的ASCII值,当没有按下任何控制键时,即control_keys为0时,会得到scanToAscii_table[17][0]即0x77,也就是"w"小写字母的ASCII值。

    这样就可以由扫描码得到编程所需的ASCII值了,如果ASCII值在小于等于255的范围内,则将ASCII值通过zlox_monitor_put函数显示到屏幕上:

// zlox_keyboard.c implement interrupt and functions of keyboard
............................................
............................................
static ZLOX_VOID zlox_keyboard_callback(/*ZLOX_ISR_REGISTERS * regs*/)
{
............................................
............................................
	if(key_ascii != 0)
	{
		if(key_ascii <= 0xFF)
		{
			keyboard_buffer[keyboard_buffer_size] = key_ascii;
			keyboard_buffer_size++;
			zlox_monitor_put(key_ascii);
		}
		else
		{
			keyboard_buffer[keyboard_buffer_size] = (key_ascii & 0xFF);
			keyboard_buffer[keyboard_buffer_size+1] = (key_ascii & 0xFF00);
			keyboard_buffer_size += 2;
		}
	}
}


    以上就是和键盘相关的代码。

    zlox_kernel.c文件里通过调用zlox_initKeyboard函数来完成键盘的初始化工作:

/*zlox_kernel.c Defines the C-code kernel entry point, calls initialisation routines.*/
..........................................
..........................................

//zenglOX kernel main entry
ZLOX_SINT32 zlox_kernel_main(ZLOX_MULTIBOOT * mboot_ptr, ZLOX_UINT32 initial_stack)
{
..........................................
..........................................

	// 初始化系统调用
	zlox_initialise_syscalls();

	zlox_initKeyboard();

	zlox_syscall_monitor_write("=========================\n");	

	zlox_syscall_monitor_write("Keyboard is init now!\n");

	zlox_syscall_monitor_write("=========================\n");

	// 切换到ring 3的用户模式
	zlox_switch_to_user_mode();

	zlox_syscall_monitor_write("I'm in user mode!\n");

	zlox_syscall_monitor_write("Hello world!\nwelcome to zenglOX v0.0.10!\nplease input some char> ");

	for(;;)
		;

	return 0;
}


    该版本在bochs里的运行情况如下:


图2

    上图里,红框框部分的this is my input ....的内容是我从键盘上输入的字符。

    上面只介绍了和键盘相关的一部分内容,有关USB Legacy Support(USB传统支持,可将USB键盘等设备模拟为PS/2设备),以及PS/2控制器初始化等的内容,请参考 http://wiki.osdev.org/%228042%22_PS/2_Controller 链接里的文章。

    OK,就到这里,休息,休息一下 o(∩_∩)o~~
 
上下篇

下一篇: zenglOX v0.0.11 ELF format(ELF可执行文件格式)与execve系统调用

上一篇: zenglOX v0.0.9 User Mode(用户模式)

相关文章

zenglOX v1.6.0 保护模式下, VGA图形模式驱动程式

zenglOX v0.0.5 分页

zenglOX v3.2.0 USB v1.1, UHCI, USB KeyBoard, USB Mouse

zenglOX v0.0.11 ELF format(ELF可执行文件格式)与execve系统调用

zenglOX v0.0.4 IRQ(中断请求)与PIT(可编程间隔定时器)

zenglOX v1.1.0 通过ATAPI读取光盘里的数据