今天的目標是賦予 SmallOS 「溝通的能力」。我們不僅要處理更複雜的鍵盤狀態(如 Shift 與 Caps Lock),還要在此基礎上建立一個簡單的命令解釋器(Shell)。此外,我們優化了自動化構建流程,讓 Makefile 能夠根據核心大小自動調整磁碟鏡像結構。
在之前的開發中,我們只能識別單個按鍵。今天我們引入了狀態紀錄,讓內核能夠正確處理以下邏輯:
LSHIFT 或 RSHIFT 的按下的狀態(Make code)與放開(Break code),實現大小寫與特殊符號切換。void keyboard_callback(u32) {
// 讀取掃描碼
u8 scancode = port_byte_in(0x60);
if (scancode == 0x3A)
{
capslock_pressed = capslock_pressed ^ 1;
return;
}
// 檢查是否為 Shift 鍵的掃描碼
// 左 Shift: 0x2A, 右 Shift: 0x36
if (scancode == 0x2A || scancode == 0x36)
{
shift_pressed = 1;
return;
}
// 放開 Shift (掃描碼 = 按下碼 + 0x80)
// 左放開: 0xAA, 右放開: 0xB6
if (scancode == 0xAA || scancode == 0xB6)
{
shift_pressed = 0;
return;
}
// 如果掃描碼最高位是 1 (>= 0x80),代表按鍵放開 (Key Released)
if (scancode & 0x80)
{
// 目前暫不處理放開事件
return;
}
if (scancode == 0x0E)
{ // Backspace
if (buffer_idx > 0)
{
buffer_idx --;
perform_backspace();
}
} else if (scancode == 0x1C)
{ // Enter
// a command is ready
key_buffer[buffer_idx] = '\\0';
command_ready = 1;
buffer_idx = 0;
kprint("\\n");
set_backspace_min();
} else if (scancode <= 0x39)
{
char letter = shift_pressed ? ascii_shift[(int)scancode] : ascii_nomod[(int)scancode];
// for letter, we need to combin with capslock_pressed
if (('a' <= letter && letter <= 'z') || ('A' <= letter && letter <= 'Z'))
{
if (capslock_pressed == 1)
{
letter = (shift_pressed) ? ascii_nomod[(int)scancode] : ascii_shift[(int)scancode];
}else
{
letter = (shift_pressed) ? ascii_shift[(int)scancode] : ascii_nomod[(int)scancode];
}
}
if (letter != 0 && buffer_idx < 255 && command_ready == 0) {
key_buffer[buffer_idx++] = letter;
char str[2] = {letter, '\\0'};
kprint(str);
}
}
}
我們實作了一個簡單的 Shell 循環,它會接收鍵盤緩衝區的輸入,並在按下 Enter 時進行字串比對。目前支持的命令:
help: 顯示可用指令清單。clear: 調用 VGA 驅動清空螢幕。reboot: 透過 8042 鍵盤控制器觸發 CPU 重啟。shutdown: 針對 QEMU 模擬環境的關機指令。#include "string.h"
#include "display.h"
#include "types.h"
#include "shell.h"
#include "io.h"
void shell_print_prompt()
{
kprint("\\nSmallOS > ");
}
/**
* 曾對QEMU的shutdown指令,不一定適合真實機器
*/
void shutdown() {
kprint("Shutting down Small OS...\\n");
// 方法 A: QEMU 專用的 ACPI 關機 (適用於多數 QEMU 預設配置)
// 寫入 0x2000 到 0x604 連接埠
port_word_out(0x604, 0x2000);
// 方法 B: 較舊版本或特定 BIOS 的關機
port_word_out(0xB004, 0x2000);
// 方法 C: 如果是在真實機器上,這通常會觸發「三倍故障 (Triple Fault)」
// 雖然不是優雅的關機,但在沒實作 ACPI 的情況下,這是讓機器停止運行的最後手段
kprint("Shutdown failed. Halting CPU.\\n");
__asm__ __volatile__("cli");
while(1) { __asm__ __volatile__("hlt"); }
}
/**
* 這個是熱啟動,只有CPU被復位
*/
void reboot()
{
kprint("Rebooting system...\\n");
// 1. 關閉所有中斷,防止在重啟過程中被干擾
__asm__ __volatile__("cli");
// 2. 向鍵盤控制器發送重啟命令
u8 temp;
// 等待鍵盤控制器緩衝區清空
do {
temp = port_byte_in(0x64);
} while (temp & 0x02);
port_byte_out(0x64, 0xFE);
// 3. 關鍵:如果硬體重啟慢了,我們絕對不能讓 CPU 執行後面的代碼
// 進入一個死循環,原地等待硬體重置
while(1) {
__asm__ __volatile__("hlt");
}
}
/**
* 執行shell指令
*/
void shell_execute(char *input)
{
if (k_str_len(input) == 0) return;
char *args = k_str_split_first(input, ' ');
input = k_to_lowercase(input);
if (k_str_cmp(input, "help") == 0)
{
kprint("SmallOS Shell. Available commands: help, echo, clear, reboot, shutdown");
}else if (k_str_cmp(input, "echo") == 0)
{
if (args != NULL)
{
kprint(args);
}
}else if (k_str_cmp(input, "clear") == 0)
{
clear_screen();
set_cursor_offset(get_screen_offset(0, 0));
}else if (k_str_cmp(input, "reboot") == 0)
{
reboot();
}else if (k_str_cmp(input, "shutdown") == 0)
{
shutdown();
}
else
{
kprintf("Unknow command: %s\\n", input);
}
}
隨著 Shell 相關代碼的加入,內核體積(kernel.bin)迅速增長。手動修改磁區數量(Sectors)既麻煩又危險。我們更新了 Makefile,讓它自動計算所需的磁區數,確保磁碟鏡像(os-image.bin)永遠結構正確。