第5课:中断和异常2
下载源代码
声明:转载请保留:
译者:http://www.cppblog.com/jinglexy
原作者:xiaoming.mo at skelix dot org
MSN & Email: jinglexy at yahoo dot com dot cn
目标
在上一节课中,我们介绍了如何处理异常。本课中,我们讲学习如何在skelix中使用时钟中断和键盘。
可编程定时器通常使用主板上的8253/8254芯片。经过设置后,它每隔一段时间会触发一个中断,我们将利用它来实现多任务的抢占。这个定时器芯片有3个独立的计数器模块,可以使用0x40~0x42端口来操作它们,每个计数器有自己的16为计数寄存器,并且可以在6中状态下工作运行,这个计数器可以使用BCD或者16进制。通过0x43端口可以访问这3个计数寄存器,该控制端口操作字格式如下:
位 7,6
|
Select Counter ,选择对那个计数器进行操作。 “00” 表示选择 Counter 0 , “01” 表示选择 Counter 1 , “10” 表示选择 Counter 2 , “11” 表示 Read-Back Command (仅对于 8254 ,对于 8253 无效)
|
位 5,4
|
Read/Write/Latch 格式位。 “00” 表示锁存( Latch )当前计数器的值; “01” 只读写计数器的高字节 ( MSB ); “10” 只读写计数器的低字节( LSB ); “11” 表示先读写计数器的 LSB ,再读写 MSB
|
位 3-1
|
Mode bits ,控制各通道的工作模式。 “000” 对应 Mode 0 ; “001” 对应 Mode 1 ; “010” 对应 Mode 2 ; “011”对应 Mode 3 ; “100” 对应 Mode 4 ; “101” 对应 Mode 5
|
位 0
|
控制计数器的存储模式。 0 表示以二进制格式存储, 1 表示计数器中的值以 BCD 格式存储
|
根据Intel手册,系统上电后,8254状态是不可知的。工作模式,计数值,输出基数都是不可知的。只有对它进行编程,才能使用各个计数器模块。现在我们来看下程序:
05/timer/time.c
void timer_install(int hz)
{ // 设置定时器多长时间发送一个中断给cpu
unsigned int divisor = 1193180/hz;
由于外部晶振电路频率是1193180, 所以设置计数器值为1193180/hz,
表示当计数从0累加到1193180/hz后发一个方波脉冲给cpu
outb(0x36,
0x43); //
二进制,工作模式为3,先写LSB再写MSB
outb(divisor&0xff, 0x40);
outb(divisor>>8, 0x40);
outb(inb(0x21)&0xfe, 0x21); // 设置PIC1 的掩码第0位:允许时钟中断
}
volatile unsigned int timer_ticks = 0;
void do_timer(void)
{ //
时钟中断处理程序
int x , y;
++timer_ticks;
get_cursor(&x, &y);
set_cursor(71, 24);
kprintf(KPL_DUMP, "%x", timer_ticks);
set_cursor(x, y);
outb(0x20,
0x20); //
发送eoi:通知PIC1 中断例程已处理完成,可以接收新的中断了
因为timer只链接在PIC1上,所以不需要 oub(0x20, 0xa0) 来告知PIC2 了
}
另外我们还需要改一些其他文件:
05/timer/include/isr.h
#define VALID_ISR
(32+1) // 我们添加了一个新的ISR例程到isr表中
05/timer/isr.s
isr: .long
divide_error, isr0x00, debug_exception, isr0x01
.long breakpoint,
isr0x02, nmi, isr0x03
.long overflow,
isr0x04, bounds_check, isr0x05
.long
invalid_opcode, isr0x06, cop_not_avalid, isr0x07
.long
double_fault, isr0x08, overrun, isr0x09
.long invalid_tss,
isr0x0a, seg_not_present, isr0x0b
.long
stack_exception, isr0x0c, general_protection, isr0x0d
.long page_fault,
isr0x0e, reversed, isr0x0f
.long
coprocessor_error, isr0x10, reversed, isr0x11
.long reversed,
isr0x12, reversed, isr0x13
.long reversed,
isr0x14, reversed, isr0x15
.long reversed,
isr0x16, reversed, isr0x17
.long reversed,
isr0x18, reversed, isr0x19
.long reversed,
isr0x1a, reversed, isr0x1b
.long reversed, isr0x1c,
reversed, isr0x1d
.long reversed,
isr0x1e, reversed, isr0x1f
.long do_timer,
isr0x20 # <<<<< 添加的项 >>>>>>
isrNoError
0x1b
isrNoError
0x1c
isrNoError
0x1d
isrNoError
0x1e
isrNoError
0x1f
isrNoError
0x20 #
<<<<< 添加的项 >>>>>>
05/timer/init.c
void
init(void) {
char wheel[] = {'\\', '|', '/', '-'};
int i = 0;
idt_install();
pic_install();
timer_install(100); //
每秒中100次时钟中断,linux是这样的,windows是200次
sti();
// 不要忘了使能中断哦
for (;;)
{ __asm__
("movb %%al,
0xb8000+160*24"::"a"(wheel[i]));
if (i == sizeof wheel)
i = 0;
else
++i;
}
}
我们还是使用以前的Makefile,当然需要加入新的模块到 KERNEL_OBJS
中:
05/timer/Makefile
KERNEL_OBJS= load.o init.o isr.o timer.o libcc.o scr.o
kprintf.o exceptions.o
运行make编译一把,在让vmware执行一下final.img,是不是很有成就感?
键盘
好啦,我们已经知道在屏幕上显示一些东西了,现在学习按键处理,然后在显示出来。
当一个键按下时,一个8位扫描码会发送给计算机。例如,‘a’键按下后,扫描码0x1e(取决于键盘布局,不要告诉我你用的日本键盘,鄙视一个)发送,当翻开按键时,扫描码最高位置一并发送:0x1e | 0x80 = 0x9e。对于一些特殊按键,如break,home键等,这里不做处理,有兴趣的同学可以查找相关资料。一些可见按键对于本课来说已足够。
下面是我使用的按键码表,如果你确实用到了特殊键盘,最好找到对应的资料。
扫描码
|
键
|
扫描码
|
键
|
.
|
......
|
......
|
......
|
......
|
|
|
|
01
|
ESC
|
02
|
1!
|
03
|
2@
|
04
|
3#
|
05
|
4$
|
06
|
5%
|
07
|
6^
|
08
|
7&
|
09
|
8*
|
0A
|
9(
|
0B
|
0)
|
0C
|
-_
|
0D
|
=+
|
0E
|
<--
|
0F
|
TAB
|
10
|
qQ
|
11
|
wW
|
12
|
eE
|
13
|
rR
|
14
|
tT
|
15
|
yY
|
16
|
uU
|
17
|
iI
|
18
|
oO
|
19
|
pP
|
1A
|
[{
|
1B
|
]}
|
1C
|
Enter
|
1D
|
LCTL
|
1E
|
aA
|
1F
|
sS
|
20
|
dD
|
21
|
fF
|
22
|
gG
|
23
|
hH
|
24
|
jJ
|
25
|
kK
|
26
|
lL
|
27
|
;:
|
28
|
'"
|
29
|
`~
|
2A
|
LSHT
|
2B
|
\|
|
2C
|
zZ
|
2D
|
xX
|
2E
|
cC
|
2F
|
vV
|
30
|
bB
|
31
|
nN
|
32
|
mM
|
33
|
,<
|
34
|
.>
|
35
|
/?
|
36
|
RSHT
|
37
|
**
|
38
|
LALT
|
39
|
SPACE
|
3B-44
|
F1-F10
|
57
|
F11
|
58
|
F12
|
正如你看到的,ctrl,shift,alt键以普通扫描码发送,所以我们可以重新映射这些键以显示到屏幕上。
访问键盘控制器仍然经由端口:
05/keyboard/kb.c
void
do_kb(void) {
int com;
void (*key_way[0x80])(void) = {
/*00*/unp, unp, pln, pln, pln, pln,
pln, pln,
/*08*/pln, pln, pln, pln, pln, pln, pln,
pln,
/*10*/pln, pln, pln, pln, pln, pln, pln,
pln,
/*18*/pln, pln, pln, pln, pln, ctl, pln,
pln,
/*20*/pln, pln, pln, pln, pln, pln, pln,
pln,
/*28*/pln, pln, shf, pln, pln, pln, pln,
pln,
/*30*/pln, pln, pln, pln, pln, pln, shf,
pln,
/*38*/alt, pln, unp, fun, fun, fun, fun,
fun,
/*40*/fun, fun, fun, fun, fun, unp, unp,
unp,
/*48*/unp, unp, unp, unp, unp, unp, unp,
unp,
/*50*/unp, unp, unp, unp, unp, unp, unp,
fun,
/*58*/fun, unp, unp, unp, unp, unp, unp,
unp,
/*60*/unp, unp, unp, unp, unp, unp, unp,
unp,
/*68*/unp, unp, unp, unp, unp, unp, unp,
unp,
/*70*/unp, unp, unp, unp, unp, unp, unp,
unp,
/*78*/unp, unp, unp, unp, unp, unp, unp,
unp,
};
上面是一堆函数指针,当获取到扫描码后,我们使用这个表找到相关的处理函数,进行相关的处理。unp是未定义的键,pln是可显示字符,ctl是控制键ctrl,shf是控制键shift,alt是控制键alt,fun函数用于F1到F12。
com = 0;
scan_code =
inb(0x60); //
从8042的0x60端口获取扫描码
(*key_way[scan_code&0x7f])(); //
0x7f是放开按键的掩码
/* 按键已处理 */
outb((com=inb(0x61))|0x80, 0x61); //
当我们从0x60端口读完扫描码后,这个扫描码并不会自动删除,
outb(com&0x7f,
0x61);
// 同时也阻止了我们读下一系列按键,所以我们需要通知键盘控制器按键已处理,
// 做法很简单:只需要通过0x61端口的最高位disable和re-enable键盘即可,
// 位 7: 0=Enable keyboard; 1=Disable
keyboard
outb(0x20,
0x20);
// 发送EOI:中断处理已完成
}
static unsigned char shf_p =
0; // 保存Ctrl, Shift 和 Alt键的状态
static unsigned char ctl_p = 0;
static unsigned char alt_p = 0;
static unsigned char scan_code;
// 当前处理的扫描码
/* 这个函数用于打印可打印字符 */
static void
pln(void) {
static const char key_map[0x3a][2] = {
/*00*/{0x0, 0x0}, {0x0, 0x0}, {'1', '!'},
{'2', '@'},
/*04*/{'3', '#'}, {'4', '$'}, {'5', '%'},
{'6', '^'},
/*08*/{'7', '&'}, {'8', '*'}, {'9',
'('}, {'0', ')'},
/*0c*/{'-', '_'}, {'=', '+'},
{'\b','\b'},{'\t','\t'},
/*10*/{'q', 'Q'}, {'w', 'W'}, {'e', 'E'},
{'r', 'R'},
/*14*/{'t', 'T'}, {'y', 'Y'}, {'u', 'U'},
{'i', 'I'},
/*18*/{'o', 'O'}, {'p', 'P'}, {'[', '{'},
{']', '}'},
/*1c*/{'\n','\n'},{0x0, 0x0}, {'a', 'A'},
{'s', 'S'},
/*20*/{'d', 'D'}, {'f', 'F'}, {'g', 'G'},
{'h', 'H'},
/*24*/{'j', 'J'}, {'k', 'K'}, {'l', 'L'},
{';', ':'},
/*28*/{'\'','\"'},{'`', '~'}, {0x0,
0x0}, {'\\','|'},
/*2c*/{'z', 'Z'}, {'x', 'X'}, {'c', 'C'},
{'v', 'V'},
/*30*/{'b', 'B'}, {'n', 'N'}, {'m', 'M'},
{',', '<'},
/*34*/{'.', '>'}, {'/', '?'}, {0x0,
0x0}, {'*', '*'},
/*38*/{0x0, 0x0}, {' ', ' '} };
定义可打印字符码表,键key_map[?][0]是shift未按下的扫描码对应的字符,
key_map[?][1]责对应按下shift的键码。
if (scan_code & 0x80)
return;
// 已经按下这个键了,那就什么也不做
print_c(key_map[scan_code&0x7f][shf_p], WHITE,
BLACK); // 打印它:黑纸白字,清清楚楚
}
/* Ctrl键 */
static void
ctl(void) {
ctl_p ^= 0x1;
}
/* Alt键 */
static void
alt(void) {
alt_p键 ^= 0x1;
}
/* Shift键 */
static void
shf(void) {
shf_p ^= 0x1;
}
/* F1, F2 ~ F12键 */
static void
fun(void) {
}
/* 暂不实现特殊键 */
static void
unp(void) {
}
当有shift键按下时,我们打印大写字母。
void
kb_install(void) {
outb(inb(0x21)&0xfd, 0x21);
}
我们几乎实现完了,最后在中断入口表中添加一项:
05/keyboard/isr.s
.long do_timer, isr0x20, do_kb, isr0x21 # 注意:加入了按键处理项
isrNoError
0x20
isrNoError
0x21 # 这里也加一个宏定义
05/keyboard/include/isr.h
#define VALID_ISR
(32+2) # ISR个数再加1
05/keyboard/init.c
timer_install(100);
kb_install(); /* 安装键盘处理 */
sti();
在Makefile的 KERNEL_OBJS 加入新的模块:
05/keyboard/MakefileKERNEL_OBJS= load.o init.o isr.o
timer.o libcc.o scr.o kprintf.o exceptions.o kb.o
运行make编译一把,再用vmware执行,看看是不是可以处理按键了,退格键也可以了。
你甚至可以再上面写一个hello, world,只是不能编译:)