「30天自制操作系统」Day17 - 命令行窗口

闲置任务

为了防止由于没有任务运行而使程序出现异常,在所有 LEVEL 中都没有任务存在的时候,我们就需要 HTL 了。借鉴之前我们处理链表时的「哨兵」思想,我们创建一个闲置的任务 idle,并把它一直放在最下层 LEVEL 中,该任务的功能只是执行 HTL。这样即便任务 A 进入休眠状态,系统也会自动切换到 idle 上。而当 FIFO 中有数据写入的时候,任务 A 就会被唤醒,系统会自动切换回任务 A 进行工作。

创建命令行窗口

我们将命令行窗口单独做成一个新的任务,该任务的实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void console_task(struct SHEET *sheet)
{
struct FIFO32 fifo;
struct TIMER *timer;
struct TASK *task = task_now();

int i, fifobuf[128], cursor_x = 8, cursor_c = COL8_000000;
fifo32_init(&fifo, 128, fifobuf, task);

timer = timer_alloc();
timer_init(timer, &fifo, 1);
timer_settime(timer, 50);

for (;;) {
io_cli();
if (fifo32_status(&fifo) == 0) {
task_sleep(task);
io_sti();
} else {
i = fifo32_get(&fifo);
io_sti();
if (i <= 1) {
if (i != 0) {
timer_init(timer, &fifo, 0);
cursor_c = COL8_FFFFFF;
} else {
timer_init(timer, &fifo, 1);
cursor_c = COL8_000000;
}
timer_settime(timer, 50);
boxfill8(sheet->buf, sheet->bxsize, cursor_c, cursor_x, 28, cursor_x + 7, 43);
sheet_refresh(sheet, cursor_x, 28, cursor_x + 8, 44);
}
}
}
}

这里我们通过 task_now 函数获得了本身 TASK 所在的内存地址,在之后的 task_sleep 函数中我们会用到。看一下现在的效果:

切换输入窗口

命令行已经有了,如何往里输入字符呢?我们现在要实现这样一个功能:在按下 tab 键的时候,将输入窗口切换到命令行窗口上去。现在我们在键盘中断中加上对 tab 键的处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
if (i == 256 + 0x0f) { /* Tab */
if (key_to == 0) {
key_to = 1;
make_wtitle8(buf_win, sht_win->bxsize, "task_a", 0);
make_wtitle8(buf_cons, sht_cons->bxsize, "console", 1);
} else {
key_to = 0;
make_wtitle8(buf_win, sht_win->bxsize, "task_a", 1);
make_wtitle8(buf_cons, sht_cons->bxsize, "console", 0);
}
sheet_refresh(sht_win, 0, 0, sht_win->bxsize, 21);
sheet_refresh(sht_cons, 0, 0, sht_cons->bxsize, 21);
}

key_to 这个变量用于记录键盘输入应该发送到哪里,每次按下 tab 的时候都要将 key_to 取反。看一下效果:

现在我们能通过改变窗口颜色来显示当前窗口了,不过还无法实现字符输入。要实现字符输入,只要在键盘被按下的时候向 console_task 的 FIFO 发送数据即可。这就要求我们需要知道当前 TASK 对应的 FIFO 的内存地址,为了方便起见,我们修改 TASK 结构体如下:

1
2
3
4
5
6
struct TASK {
int sel, flags;
int level, priority;
struct FIFO32 fifo;
struct TSS32 tss;
};

我们修改一下 HariMain,部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if (i < 0x54 + 256 && keytable[i - 256] != 0) { /* 一般字符 */
if (key_to == 0) { /* 发送给任务 A */
if (cursor_x < 128) {
s[0] = keytable[i - 256];
s[1] = 0;
putfonts8_asc_sht(sht_win, cursor_x, 28, COL8_000000, COL8_FFFFFF, s, 1);
cursor_x += 8;
}
} else { /* 发送给命令行窗口 */
fifo32_put(&task_cons->fifo, keytable[i - 256] + 256);
}
}
if (i == 256 + 0x0e) { /* 退格键 */
if (key_to == 0) { /* 发送给任务 A */
if (cursor_x > 8) {
putfonts8_asc_sht(sht_win, cursor_x, 28, COL8_000000, COL8_FFFFFF, " ", 1);
cursor_x -= 8;
}
} else { /* 发送给命令行窗口 */
fifo32_put(&task_cons->fifo, 8 + 256);
}
}

这样当 key_to 不为 0 时,系统就可以向命令行窗口任务发送键盘数据了。现在我们修改一下 console_task,让它能够接收并处理键盘数据就好了,运行一下看看效果吧:

符号的输入

现在我们还无法输入符号,要想输入符号,我们必须要处理 shift 键,因此我们可以准备一个 key_shift 变量,当左 shift 按下时置为 1,右 shift 按下时置为 2,两个都不按时置为 0,两个都按下时置为 3。这样我们根据 key_shift 的状态,设置两个映射表 keytable0[]、keytable1[] 就可以了。看一下效果吧:

这里由于和作者的键盘键位不同……要打出这个表达式还得按照 keytable1[] 对应一下才打的出来。但可以看到我们已经可以输入符号了。

大写字母与小写字母

输入大小写字母的原理也很简单,就是判断 CapsLock 和 shift 的状态组合即可,CapsLock 状态的获取在 binfo->leds 的第 6 位中。代码实现如下:

1
2
3
4
5
6
if ('A' <= s[0] && s[0] <= 'Z') {
if (((key_leds & 4) == 0 && key_shift == 0) ||
((key_leds & 4) != 0 && key_shift != 0)) {
s[0] += 0x20; /* 将大写字母转化为小写字母 */
}
}

我们运行一下看看效果:

对各种锁定键的支持

刚刚我们已经实现了根据 CapsLock 的状态来切换大小写字母的输入,现在我们来实现在按下 CapsLock 键的时候切换 CapsLock 的状态。

除 CapsLock 之外,常用的锁定键还有 NumLock、ScrollLock,对应编码分别为 0x3a、0x45、0x46。当我们接收到这些编码时,只要将 binfo->leds 中对应位置改写就可以了。

除此之外,如果我们要向实现在按下的同时点亮/熄灭键盘上的指示灯,可采用以下方法:

  • 读取状态寄存器,等待 bit 1 的值变为 0。
  • 向数据输出写入要发送的 1 个字节数据。
  • 等待键盘返回 1 个字节的信息,返回的信息如果为 0xfa,表明发送成功;如果为 0xfe,表明发送失败。
  • 要控制 LED 的状态,需要按上述方法执行两次,向键盘发送 EDxx 数据,其中 xx 的 bit 0 代表 ScrollLock,bit 1 代表 NumLock,bit 2 代表 CapsLock,其他为保留位。

我们修改 HariMain 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
if (fifo32_status(&keycmd) > 0 && keycmd_wait < 0) {
/* 如果存在向键盘控制器发送的数据则发送 */
keycmd_wait = fifo32_get(&keycmd);
wait_KBC_sendready();
io_out8(PORT_KEYDAT, keycmd_wait);
}
...
else {
if (i == 256 + 0x3a) { /* CapsLock */
key_leds ^= 4;
fifo32_put(&keycmd, KEYCMD_LED);
fifo32_put(&keycmd, key_leds);
}
if (i == 256 + 0x45) { /* NumLock */
key_leds ^= 2;
fifo32_put(&keycmd, KEYCMD_LED);
fifo32_put(&keycmd, key_leds);
}
if (i == 256 + 0x46) { /* ScrollLock */
key_leds ^= 1;
fifo32_put(&keycmd, KEYCMD_LED);
fifo32_put(&keycmd, key_leds);
}
if (i == 256 + 0xfa) { /* 成功接收到数据 */
keycmd_wait = -1;
}
if (i == 256 + 0xfe) { /* 没有成功接收到数据 */
wait_KBC_sendready();
io_out8(PORT_KEYDAT, keycmd_wait);
}
}

这里我们创建了一个叫 keycmd 的 FIFO 缓冲区,用来管理由任务 A 向键盘控制器发送数据的顺序,如果有数据要发送到键盘控制器,首先会在这个 keycmd 中累积起来。

keycmd_wait 变量,用来表示向键盘控制器发送数据的状态。当 keycmd_wait 的值为 -1 时可以发送指令;当值不为 -1 时,表示键盘控制器正在等待发送的数据,这时要发送的数据被保存在 keycmd_wait 变量中。

在 for 循环的开头,当 keycmd 中有数据,且 keycmd_wait 为 -1 时,向键盘发送 1 个字节的数据,在开始发送数据的同时,keycmd_wait 变为非 -1 的值。随后,当从键盘接收到 0xfa 返回信息时,keycmd_wait 恢复为 -1,继续发送下一个数据。当从键盘接收到的返回信息为 0xfe 时,则重新发送刚才的数据。到此为止,我们就实现点亮指示灯的功能了。