要了解如何从键盘获取输入数据,就必须首先了解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 v2.2.0 ee(easy editor)文本编辑器 C标准库函数 uheap(单独的用户堆空间) atapi驱动BUG zenglfs文件系统BUG 分页BUG 堆算法BUG等修复

zenglOX v2.0.0 E1000系列网卡驱动, PCI驱动, PS/2控制器驱动, 以太网,ARP,IP,UDP,DHCP,ICMP协议, dhcp,arp,ipconf,ping,lspci命令行程式

zenglOX v3.0.0与v3.0.1 GUI窗口界面

zenglOX v1.4.0与v1.4.1 通过ATA驱动读写硬盘里的数据, BUG修复, VirtualBox与VMware的调试功能

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

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