通过上两篇文章的记录,对键盘和显示器的操作我们都已经了解了,那么这篇文章记录如何实现多个TTY。
TTY 任务
我们可以让TTY任务以下图的形式运行。
在TTY任务中执行一个循环,这个循环将轮询每一个TTY,处理它的事件,包括从键盘缓冲区读取数据、显示字符等内容。
需要说明如下几点:
- 并非每轮轮询到某个TTY时,箭头所对应的全部事件都会发生,只有当某个TTY对应的控制台是当前控制台时,它才可以读取键盘缓冲区(所以图中读取过程使用了虚线)。
- TTY可以对输入的数据做更多处理,但在这里,我们只把它简化为“显示”一项。
- 我们应该把键盘和显示器算做每一个TTY的一部分,它们是公用的。
运行的过程已经清楚了,其实轮询到每一个TTY时不外乎做两件事:
- 处理输入——查看是不是当前TTY,如果是则从键盘缓冲区读取数据。
- 处理输出——如果有要显示的内容则显示它。
在前面的程序中,TTY任务很简单。如下图所示,箭头指的是函数间的调用关系。task_tty()是一个循环,它不断调用keyboard_read(),而keyboard_read()从键盘缓冲区得到数据后会调用in_process(),将字符直接显示出来。
我们下面要做的工作不能再这么简单了。它与原先程序实现的区别主要表现在以下几个方面:
- 每一个TTY都应该有自己的读和写动作。所以在keyboard_read()内部,函数需要了解自己是被哪一个TTY调用。我们通过函数传入一个参数来做到这一点,这个参数是指向当前TTY的指针。
- 为了让输入和输出分离,被keyboard_read()调用的in_process()不应该再直接回显字符,而应该将回显的任务交给TTY来完成,这样,我们就需要为TTY建立一块缓冲区,用以放置被回显的字符。
- 每个TTY回显字符时操作的CONSOLE是不同的,所以每个TTY都应该有一个成员来记载其对应的CONSOLE信息。
TTY 任务框架搭建
基于上面的考虑,我们新建两个结构体,分别表示TTY和CONSOLE,代码如下所示。
代码 include/tty.h,TTY结构。
#define TTY_IN_BYTES 256 /* tty input queue size */
struct s_console;
/* TTY */
typedef struct s_tty
{
u32 in_buf[TTY_IN_BYTES]; /* TTY 输入缓冲区 */
u32* p_inbuf_head; /* 指向缓冲区中下一个空闲位置 */
u32* p_inbuf_tail; /* 指向键盘任务应处理的键值 */
int inbuf_count; /* 缓冲区中已经填充了多少 */
struct s_console * p_console;
}TTY;
代码 include/console.h,CONSOLE结构。
typedef struct s_console
{
unsigned int current_start_addr; /* 当前显示到了什么位置 */
unsigned int original_addr; /* 当前控制台对应显存位置 */
unsigned int v_mem_limit; /* 当前控制台占的显存大小 */
unsigned int cursor; /* 当前光标位置 */
}CONSOLE;
由于下面要添加的内容有一点多,我们需要先来看一下整个程序的流程,如下图所示。在task_tty()中,通过循环来处理每一个TTY的读和写操作,读写操作都放在了tty_do_read()和tty_do_write()两个函数中,这样就让task_tty()很简洁,而且逻辑清晰。读操作会调用keyboard_read(),当然此时已经多了一个参数;写操作会调用out_char(),它会将字符写入指定的CONSOLE。
对照上图,具体实现就变得容易多了。之前分析过,32KB的显存同时存在3个控制台是允许的,那么我们就先声明3个TTY以及对应的3个CONSOLE。
代码 include/const.h,控制台个数(也是终端个数)。
/* TTY */
#define NR_CONSOLES 3 /* consoles */
代码 kernel/global.c,TTY和CONSOLE。
PUBLIC TTY tty_table[NR_CONSOLES];
PUBLIC CONSOLE console_table[NR_CONSOLES];
下面来看一下框架性的task_tty()。
代码 kernel/tty.c,task_tty。
#define TTY_FIRST (tty_table)
#define TTY_END (tty_table + NR_CONSOLES)
...
PUBLIC void task_tty()
{
TTY *p_tty;
init_keyboard();
for (p_tty = TTY_FIRST; p_tty < TTY_END; p_tty++) {
init_tty(p_tty);
}
nr_current_console = 0;
while (1) {
for (p_tty = TTY_FIRST; p_tty < TTY_END; p_tty++) {
tty_do_read(p_tty);
tty_do_write(p_tty);
}
}
}
在主循环之前,做了一些初始化工作。由于键盘应被看做是TTY的一部分,所以init_keyboard()的调用也挪到了这里。函数init_tty()的代码如下所示。
代码 kernel/tty.c,init_tty。
PUBLIC void init_tty(TTY* p_tty)
{
p_tty->inbuf_count = 0;
p_tty->p_inbuf_head = p_tty->p_inbuf_tail = p_tty->in_buf;
int nr_tty = p_tty - tty_table;
p_tty->p_console = console_table + nr_tty;
}
可以看到,之所以要初始化TTY的工作,既因为其中的缓冲区需要设置初值,也因为要为每个TTY指定对应的CONSOLE。
另外,在代码task_tty中还有一句初始化nr_current_console的语句,这是一个全局变量,定义在global.h中:
EXTERN int nr_current_console;
从字面意思上可以知道,这个变量用来记录当前的控制台是哪一个,只有当某个TTY对应的控制台是当前控制台时,它才可以读取键盘缓冲区。所以,在tty_do_read()中要判断这个变量的值,进行控制台切换时也要记得改变它。
判断是否为当前控制台的代码如下所示。源文件console.c是新建立的。
代码 kernel/console.c,判断是否为当前的控制台。
PUBLIC int is_current_console(CONSOLE* p_con)
{
return (p_con == &console_table[nr_current_console]);
}
这样,tty_do_read()就容易写了。
代码 kernel/tty.c,tty_do_read。
PUBLIC void tty_do_read(TTY* p_tty)
{
if (is_current_console(p_tty->p_console)) {
keyboard_read(p_tty);
}
}
注意,keyboard_read()发生了改变,要对其函数体做相应修改,同时,in_process()也要增加一个参数。
代码 kernel/tty.c,in_process。
PUBLIC void in_process(TTY* p_tty, u32 key)
{
char output[2] = {'\0', '\0'};
if (!(key & FLAG_EXT)) {
if (p_tty->inbuf_count < TTY_IN_BYTES) {
*(p_tty->p_inbuf_head) = key;
p_tty->p_inbuf_head++;
if (p_tty->p_inbuf_head == p_tty->in_buf + TTY_IN_BYTES) {
p_tty->p_inbuf_head = p_tty->in_buf;
}
p_tty->inbuf_count++;
}
} else {
/* 暂时忽略 */
}
}
往TTY缓冲区中写入数据的代码与keyboard_handler()中的代码差不多。我们只把需要输出的字符写入缓冲区,如果遇到诸如Alt+Fn这样的切换控制台的操作,就让它在in_process()中处理掉。
在写入TTY缓冲区之后,读操作就算结束了。我们再来看一下写操作。
代码 kernel/tty.c,tty_do_write。
PRIVATE void tty_do_write(TTY* p_tty)
{
if (p_tty->inbuf_count) {
char ch = *(p_tty->p_inbuf_tail);
p_tty->p_inbuf_tail++;
if (p_tty->p_inbuf_tail == p_tty->in_buf + TTY_IN_BYTES) {
p_tty->p_inbuf_tail = p_tty->in_buf;
}
p_tty->inbuf_count--;
out_char(p_tty->p_console, ch);
}
}
这段代码从TTY缓冲区中取出键值,然后用out_char()显示在对应的CONSOLE中。
我们暂时这样实现out_char(),代码如下所示。
代码 kernel/console.c,往控制台输出字符。
PUBLIC void out_char(CONSOLE* p_con, char ch)
{
u8* p_vmem = (u8*)(V_MEM_BASE + disp_pos);
*p_vmem++ = ch;
*p_vmem++ = DEFAULT_CHAR_COLOR;
disp_pos += 2;
set_cursor(disp_pos / 2);
}
PRIVATE void set_cursor(unsigned int position)
{
disable_int();
out_byte(CRTC_ADDR_REG, CURSOR_H);
out_byte(CRTC_DATA_REG, (position >> 8) & 0xFF);
out_byte(CRTC_ADDR_REG, CURSOR_L);
out_byte(CRTC_DATA_REG, position & 0xFF);
enable_int();
}
V_MEM_BASE在const.h被定义为0xB8000,所以V_MEM_BASE+disp_pos就变成当前显示位置的地址。在这里,我们不再使用disp_str()来显示字符,而直接写字符到特定地址,这样做的前提是当前的ds指向的段的基址为0。
回顾一下我们上面所做的这些工作。当TTY任务开始运行时,所有TTY都将被初始化,并且全局变量nr_current_console会被赋值为0。然后循环开始并一直进行下去。对于每一个TTY,首先执行tty_do_read(),它将调用keyboard_read()并将读入的字符交给函数in_process()来处理,如果是需要输出的字符,会被in_process()放入当前接受处理的TTY的缓冲区中。然后tty_do_write()会接着执行,如果缓冲区中有数据,就被送入out_char显示出来。
由于nr_current_console初始之后再没改变过,所以只是初始TTY在接受处理。其它TTY在做is_current_console(p_tty->p_console)这个判断后就被忽略掉了。所以,尽管在out_char()中将所有字符不加区分地顺序显示出来,但这是没有关系的。make并运行,效果如下图所示。
运行正常,一切良好。不过,框架虽然搭建起来了,但是我们仍然只是在使用一个CONSOLE,下面我们就来实现多个CONSOLE。
多控制台
其实在写TTY和CONSOLE两个结构时已经为多控制台留下了足够的接口,只是我们还没实现它们而已,比如,CONSOLE这个结构中的成员其实根本没有用到,具体如下图所示。
上图表示了某时刻显存的使用情况。其中灰色框表示当前屏幕,黑色小方格表示显存中已写入的字符。可以看出,original_addr和v_mem_limit用做定义控制台所占显存的总体情况,它们是静态的,一经初始化就不再改变;current_start_addr将随着屏幕卷动而变化,cursor变化更频繁,每输出一个字符就更新一次。
下面我们先来为这些成员设置初值。
代码 kernel/console.c。
PUBLIC void init_screen(TTY* p_tty)
{
int nr_tty = p_tty - tty_table;
p_tty->p_console = console_table + nr_tty;
int v_mem_size = V_MEM_SIZE >> 1; /* 显存总大小 (in WORD) */
int con_v_mem_size = v_mem_size / NR_CONSOLES;
p_tty->p_console->original_addr = nr_tty * con_v_mem_size;
p_tty->p_console->v_mem_limit = con_v_mem_size;
p_tty->p_console->current_start_addr = p_tty->p_console->original_addr;
/* 默认光标位置在最开始处 */
p_tty->p_console->cursor = p_tty->p_console->original_addr;
if (nr_tty == 0) {
/* 第一个控制台沿用原来的光标位置 */
p_tty->p_console->cursor = disp_pos / 2;
disp_pos = 0;
} else {
out_char(p_tty->p_console, nr_tty + '0');
out_char(p_tty->p_console, '#');
}
set_cursor(p_tty->p_console->cursor);
}
值得注意的有几下几点:
- 结构CONSOLE的成员都是以WORD(双字节)计的,这符合对VGA寄存器操作的习惯。
- 这段代码在init_tty()中调用,而且为TTY指定对应CONSOLE的代码也挪到了这里。
- 第一个控制台沿用原来光标的位置,其它控制台光标都在屏幕左上角,并且将显示控制台号和一个字符“#”(看起来好像一个特殊的Shell)。
修改后的init_tty()代码如下所示。
代码 kernel/tty.c,修改后的初始化TTY。
PUBLIC void init_tty(TTY* p_tty)
{
p_tty->inbuf_count = 0;
p_tty->p_inbuf_head = p_tty->p_inbuf_tail = p_tty->in_buf;
init_screen(p_tty);
}
原来的函数out_char()尚未考虑多控制台的情况,如今要改变一下了。
代码 kernel/console.c,修改后的out_char。
PUBLIC void out_char(CONSOLE* p_con, char ch)
{
u8* p_vmem = (u8*)(V_MEM_BASE + p_con->cursor * 2);
*p_vmem++ = ch;
*p_vmem++ = DEFAULT_CHAR_COLOR;
p_con->cursor++;
set_cursor(p_con->cursor);
}
为了能够看到效果,我们还需要一个切换控制台的函数。
代码 kernel/console.c,切换控制台的代码。
PUBLIC void select_console(int nr_console) /* 0 ~ (NR_CONSOLES - 1) */
{
if ((nr_console < 0) || (nr_console >= NR_CONSOLES)) {
return;
}
nr_current_console = nr_console;
set_cursor(console_table[nr_console].cursor);
set_video_start_addr(console_table[nr_console].current_start_addr);
}
其中,函数set_video_start_addr()我们已经很熟悉了。
代码 include/console.c,切换控制台。
PRIVATE void set_video_start_addr(u32 addr)
{
disable_int();
out_byte(CRTC_ADDR_REG, START_ADDR_H);
out_byte(CRTC_DATA_REG, (addr >> 8) & 0xFF);
out_byte(CRTC_ADDR_REG, START_ADDR_L);
out_byte(CRTC_DATA_REG, addr & 0xFF);
enable_int();
}
按照惯例,我们应该在按下Alt+Fn时做切换工作。
代码 kernel/tty.c,处理Alt+Fn。
PUBLIC void in_process(TTY* p_tty, u32 key)
{
...
else {
int raw_code = key & MASK_RAW;
switch(raw_code) {
case UP:
if ((key & FLAG_SHIFT_L) || (key & FLAG_SHIFT_R)) {
disable_int();
out_byte(CRTC_ADDR_REG, START_ADDR_H);
out_byte(CRTC_DATA_REG, ((80 * 15) >> 8) & 0xFF);
out_byte(CRTC_ADDR_REG, START_ADDR_L);
out_byte(CRTC_DATA_REG, (80 * 15) & 0xFF);
enable_int();
}
break;
case DOWN:
if ((key & FLAG_SHIFT_L) || (key & FLAG_SHIFT_R)) {
/* Shift+Down, do nothing */
}
break;
case F1:
case F2:
case F3:
case F4:
case F5:
case F6:
case F7:
case F8:
case F9:
case F10:
case F11:
case F12:
/* Alt + F1~F12 */
if ((key & FLAG_ALT_L) || (key & FLAG_ALT_R)) {
select_console(raw_code - F1);
}
default:
break;
}
}
}
这样,我们就可以把原来tty_task()中直接将nr_current_console赋值为0的语句换成对select_console()的调用了。
代码 kernel/tty.c,选择第0个console。
PUBLIC void task_tty()
{
...
select_console(0);
...
}
好了,现在我们可以运行一下看看了,效果如下图所示。
可以看到,多个控制台之间可以通过Alt+Fn进行切换,而且输入也是在各自的控制台之上。怎么样,是不是开始觉得我们的OS越来越好玩了呢?
不过我们现在可以看到控制台0的屏幕快满了。我们现在就来添加屏幕滚动的代码。
代码 kernel/console.c,scroll_screen。
PUBLIC void scroll_screen(CONSOLE* p_con, int direction)
{
if (direction == SCR_UP) {
if (p_con->current_start_addr > p_con->original_addr) {
p_con->current_start_addr -= SCREEN_WIDTH;
}
} else if (direction == SCR_DN) {
if (p_con->current_start_addr + SCREEN_SIZE < p_con->original_addr + p_con->v_mem_limit) {
p_con->current_start_addr += SCREEN_WIDTH;
}
}
set_video_start_addr(p_con->current_start_addr);
set_cursor(p_con->cursor);
}
为了简化程序,当屏幕滚动到最下端后,再试图向下滚动时按键将不再响应,最上端时也是这样。下面来看响应Shift+↑和Shift+↓的代码。
代码 kernel/tty.c,响应Shift+↑和Shift+↓。
PUBLIC void in_process(TTY* p_tty, u32 key)
{
...
else {
int raw_code = key & MASK_RAW;
switch(raw_code) {
case UP:
if ((key & FLAG_SHIFT_L) || (key & FLAG_SHIFT_R)) {
scroll_screen(p_tty->p_console, SCR_DN);
}
break;
case DOWN:
if ((key & FLAG_SHIFT_L) || (key & FLAG_SHIFT_R)) {
scroll_screen(p_tty->p_console, SCR_UP);
}
break;
...
}
好了,我们现在运行一下,在控制台0按Shift+↑和Shift+↓数次,会呈现如下图所示的情形。
到现在为止,多控制台已经被我们实现了。
完善键盘处理
在上面的运行过程中你可能已经发现了,现在我们的系统对键盘的支持是很差的,比如你不能使用CapsLock,更不用说BackSpace、小键盘了。之所以一直没有加入这些内容,是为了让我们的TTY任务最简单。如今,任务的框架已经搭建起来了,现在就可以添加处理其它按键的代码了。
回车键和退格键
当敲击回车键和退格键时,我们往TTY缓冲区中写入’\n’和’\b’,然后在out_char中做相应处理。
代码 kernel/tty.c,响应回车键和退格键。
PUBLIC void in_process(TTY* p_tty, u32 key)
{
char output[2] = {'\0', '\0'};
if (!(key & FLAG_EXT)) {
put_key(p_tty, key);
} else {
int raw_code = key & MASK_RAW;
switch(raw_code) {
...
case ENTER:
put_key(p_tty, '\n');
break;
case BACKSPACE:
put_key(p_tty, '\b');
break;
...
}
...
PRIVATE void put_key(TTY* p_tty, u32 key)
{
if (p_tty->inbuf_count < TTY_IN_BYTES) {
*(p_tty->p_inbuf_head) = key;
p_tty->p_inbuf_head++;
if (p_tty->p_inbuf_head == p_tty->in_buf + TTY_IN_BYTES) {
p_tty->p_inbuf_head = p_tty->in_buf;
}
p_tty->inbuf_count++;
}
}
然后修改out_char()。
代码 kernel/console.c,修改out_char。
PUBLIC void out_char(CONSOLE* p_con, char ch)
{
u8* p_vmem = (u8*)(V_MEM_BASE + p_con->cursor * 2);
switch(ch) {
case '\n':
if (p_con->cursor < p_con->original_addr + p_con->v_mem_limit - SCREEN_WIDTH) {
p_con->cursor = p_con->original_addr + SCREEN_WIDTH * ((p_con->cursor - p_con->original_addr) / SCREEN_WIDTH + 1);
}
break;
case '\b':
if (p_con->cursor > p_con->original_addr) {
p_con->cursor--;
*(p_vmem - 2) = ' ';
*(p_vmem - 1) = DEFAULT_CHAR_COLOR;
}
break;
default:
if (p_con->cursor < p_con->original_addr + p_con->v_mem_limit - 1) {
*p_vmem++ = ch;
*p_vmem++ = DEFAULT_CHAR_COLOR;
p_con->cursor++;
}
}
while (p_con->cursor >= p_con->current_start_addr + SCREEN_SIZE) {
scroll_screen(p_con, SCR_DN);
}
flush(p_con);
}
...
PRIVATE void flush(CONSOLE* p_con)
{
set_cursor(p_con->cursor);
set_video_start_addr(p_con->current_start_addr);
}
可以看到,回车键直接把光标移动到下一行的开头,而退格键则把光标挪到上一个字符的位置,并在那里写入一个空格,以便清除原来的字符。
另外,输出任何类型的字符时,都做了边界检验,以防止影响到别的控制台,甚至试图写到显存之外的内存。
现在我们再运行一下,效果如下图所示。可以看到回车键和退格键都已经生效了。
Caps Lock、Num Lock、Scroll Lock
键盘上这3个键有一点特殊,因为每一个都对应一个小灯(LED)。实际上,不但通过敲击键盘可以控制这些灯的亮灭,通过写入8042的输入缓冲区也可以做到这一点。这样,我们可以维持3个全局变量,用以表示3个灯的状态,在键盘初始化的时候给它们任意付我们想要的初值,并同时设置灯的相应状态。
先来看看如何通过端口操作它们。从下表可以看出(这张表在记录键盘操作的文章中也有,这里再展示是为了说明清楚),输入缓冲区和控制寄存器都是可以写入的,但它们的作用是不同的,写入输入缓冲区用来往8048发送命令,而写入控制寄存器是往8042本身发送命令。
寄存器名称 | 寄存器大小 | 端口 | R/W | 用法 |
输出缓冲区 | 1 BYTE | 0x60 | Read | 读输出缓冲区 |
输入缓冲区 | 1 BYTE | 0x60 | Write | 写输入缓冲区 |
状态寄存器 | 1 BYTE | 0x64 | Read | 读状态寄存器 |
控制寄存器 | 1 BYTE | 0x64 | Write | 发送命令 |
我们的目的是往8048发送命令,使用端口0x60。设置LED的命令是0xED。当键盘接收到这个命令后,会回复一个ACK(0xFA),然后等待从端口0x60写入的LED参数字节,这个参数字节定义如下图所示。
当键盘接收到参数字节后,会再回复一个ACK,并根据参数字节的值来设置LED。需要注意的是,在向8042输入缓冲区写入数据时,要先判断一下输入缓冲区是否为空,方法是通过端口0x64读取状态寄存器。状态寄存器的第一位如果为0,表示输入缓冲区是空的,可以向其写入数据。
现在可以动手写代码了。
代码 kernel/keyboard.c,设置LED。
PRIVATE void kb_wait() /* 等待 8042 的输入缓冲区空 */
{
u8 kb_stat;
do {
kb_stat = in_byte(KB_CMD);
} while (kb_stat & 0x02);
}
PRIVATE void kb_ack()
{
u8 kb_read;
do {
kb_read = in_byte(KB_DATA);
} while (kb_read =! KB_ACK);
}
PRIVATE void set_leds()
{
u8 leds = (caps_lock << 2) | (num_lock << 1) | scroll_lock;
kb_wait();
out_byte(KB_DATA, LED_CODE);
kb_ack();
kb_wait();
out_byte(KB_DATA, leds);
kb_ack();
}
其中LED_CODE和KB_ACK分别被定义成0xED和0xFA。
代码 include/const.h,LED_CODE和KB_ACK。
#define LED_CODE 0xED
#define KB_ACK 0xFA
而caps_lock,num_lock和scroll_lock的声明和初始化如下代码所示。
代码 kernel/keyboard.c,初始化LEDs的状态。
PRIVATE int caps_lock; /* Caps Lock */
PRIVATE int num_lock; /* Num Lock */
PRIVATE int scroll_lock; /* Scroll Lock */
...
PUBLIC void init_keyboard()
{
kb_in.count = 0;
kb_in.p_head = kb_in.p_tail = kb_in.buf;
shift_l = shift_r = 0;
alt_l = alt_r = 0;
ctrl_l = ctrl_r = 0;
caps_lock = 0;
num_lock = 1;
scroll_lock = 0;
set_leds();
put_irq_handler(KEYBOARD_IRQ, keyboard_handler); /* 设定键盘中断处理程序 */
enable_irq(KEYBOARD_IRQ); /* 开启键盘中断 */
}
之所以把num_lock的初值设为1,是因为大多数人使用小键盘的时候都是使用其数字功能而非箭头等功能。
现在运行程序,你会发现系统启动之后NumLock被点亮。
虽然灯亮了,但却还未起到作用,我们现在就来修改keyboard_read()。
代码 kernel/keyboard.c,使用CapsLock和小键盘。
PUBLIC void keyboard_read(TTY* p_tty)
{
...
if ((key != PAUSEBREAK) && (key != PRINTSCREEN)) {
/* 首先判断是 Make Code 还是 Break Code */
make = (scan_code & FLAG_BREAK ? FALSE : TRUE);
/* 先定位到 keymap 中的行 */
keyrow = &keymap[(scan_code & 0x7F) * MAP_COLS];
column = 0;
int caps = shift_l || shift_r;
if (caps_lock) {
if ((keyrow[0] >= 'a') && (keyrow[0] <= 'z')) {
caps = !caps;
}
}
if (caps) {
column = 1;
}
if (code_with_E0) {
column = 2;
}
key = keyrow[column];
switch(key) {
case SHIFT_L:
shift_l = make;
break;
case SHIFT_R:
shift_r = make;
break;
case CTRL_L:
ctrl_l = make;
break;
case CTRL_R:
ctrl_r = make;
break;
case ALT_L:
alt_l = make;
break;
case ALT_R:
alt_r = make;
break;
case CAPS_LOCK:
if (make) {
caps_lock = !caps_lock;
set_leds();
}
break;
case NUM_LOCK:
if (make) {
num_lock = !num_lock;
set_leds();
}
break;
case SCROLL_LOCK:
if (make) {
scroll_lock = !scroll_lock;
set_leds();
}
break;
default:
break;
}
if (make) { /* 忽略 Break Code */
int pad = 0;
/* 首先处理小键盘 */
if ((key >= PAD_SLASH) && (key <= PAD_9)) {
pad = 1;
switch(key) {
case PAD_SLASH:
key = '/';
break;
case PAD_STAR:
key = '*';
break;
case PAD_MINUS:
key = '-';
break;
case PAD_PLUS:
key = '+';
break;
case PAD_ENTER:
key = ENTER;
break;
default:
if (num_lock && (key >= PAD_0) && (key <= PAD_9)) {
key = key - PAD_0 + '0';
} else if (num_lock && (key == PAD_DOT)) {
key = '.';
} else {
switch(key) {
case PAD_HOME:
key = HOME;
break;
case PAD_END:
key = END;
break;
case PAD_PAGEUP:
key = PAGEUP;
break;
case PAD_PAGEDOWN:
key = PAGEDOWN;
break;
case PAD_INS:
key = INSERT;
break;
case PAD_UP:
key = UP;
break;
case PAD_DOWN:
key = DOWN;
break;
case PAD_LEFT:
key = LEFT;
break;
case PAD_RIGHT:
key = RIGHT;
break;
case PAD_DOT:
key = DELETE;
break;
default:
break;
}
}
break;
}
}
key |= shift_l ? FLAG_SHIFT_L : 0;
key |= shift_r ? FLAG_SHIFT_R : 0;
key |= ctrl_l ? FLAG_CTRL_L : 0;
key |= ctrl_r ? FLAG_CTRL_R : 0;
key |= alt_l ? FLAG_ALT_L : 0;
key |= alt_r ? FLAG_ALT_R : 0;
in_process(p_tty, key);
}
}
}
}
代码虽长,但是理解起来并不难。唯一要说的是,这里增加了一个pad变量,并在key中设置了一个相应的位。这样做是考虑将来可能需要区分普通的数字键和小键盘上的数字键。
好了,我们现在可以运行了,你会发现CapsLock已经开始起作用了,小键盘也能用了。效果如下图所示。
图中的字符是在点亮CapsLock之后键入的,数字键以及/、*等是用小键盘输入的。
TTY 任务总结
至此,我们的TTY任务可以暂时告一段落了。它的优点是足够小巧,而且结构很清晰,很易懂,加上我们上面对于细节的介绍以及辅助的图表,理解起来应该是很容易的。
公众号