在上一篇,我講了鍵盤操作會產生掃描碼以及如何解析Pause
鍵和Print Screen
鍵的掃描碼。
在這一篇,我會說清楚”鍵盤上的輸入為什么會出現在顯示器上“。
極簡版
- 我們敲擊鍵盤,產生掃描碼。
- 操作系統獲取掃描碼,把掃描碼解析成ASCII碼。
- 操作系統把ASCII碼寫入顯存,顯示器上就會打印出顯存中的可打印字符。
解析掃描碼
在上一篇,我建立了一個Make Code和按鍵ASCII碼(部分按鍵如Enter是我自己設置的值)的映射數組keyboardMap
。
按鍵不是Pause
,也不是Print Screen
,就進入下面的解析流程。
默認鍵值
- 當接收到的掃描碼只有一個,
- 掃描碼的類型是Make Code,
- 這個Make Code的值是
V
, - 那么鍵值是
keyboardMap[V *3]
。
另一個鍵值
- 接收到的第一個掃描碼是Make Code,值是
V
。 - 從
keyboardMap
中查詢出被按下的鍵是left_shift
或right_shift
。 - 設置
column
的值是1。 - 接收到第二個掃描碼是Make Code,值是
N
。 - 從映射數組中查詢這個掃描碼對應的鍵值是:
keyboardMap[N * 3 + column]
。
注意,查詢到的鍵值是keyboardMap[N * 3 + column]
。這就是鍵盤上的1
、A
這類按鍵與shift
鍵組合時的值,即默認值外的另一個值。
提問
請想一想,點擊shift + A
鍵,讀取掃描碼的過程是什么樣的?
Enter
Enter鍵和Backspace鍵很容易解析。獲取掃描碼(Make Code)S,獲取鍵值keyboardMap[N * 3]
。
根據鍵值識別出是Enter鍵后就把光標設置到下一行。
識別出是Backspace就這樣處理最新的兩個字節:將高字節設置成0Fh
,將低字節設置成空字符的ASCII碼;另外,將光標的位置后退兩個字符。
Caps Lock等鍵
沒有弄明白。
清屏
使用linux的命令行、顯示器上打印滿了字符,我們可以使用clear
讓命令行終端的字符全部消失。讓屏幕上的字符全部消失,這就是清屏。
clear
命令只是移動了光標的位置,並沒有清除屏幕上的字符。按下方向鍵中的Up
能定位到已經消失的字符。
我們的清屏,是讓字符徹底從屏幕上消失,無論怎么按Up
鍵都不會再看到字符。
清屏其實很簡單,就是往寫滿字符的顯存區域寫入空字符。
tty
什么是tty
我覺得tty
是一個比較過時的功能,我幾乎沒用過。
想體驗一番tty
是什么的同學,可以在安裝了linux系統的電腦或虛擬機上按下shift + alt + f1
或shift + alt + f2
鍵在不同的tty之間切換。下面是我在虛擬機上切換tty的效果,一個是圖形界面,一個是純命令行界面。
VGA
我只講述VGA
模式下的tty。
如上圖所示,不同的tty窗口展示的內容完全不相同,在tty0窗口敲擊鍵盤,數據只會出現在tty0的顯示器;切換到tty1后,顯示器上不會出現剛剛敲擊鍵盤的數據;反過來也是一樣。
不同tty之間,是完全隔離的,只是公用一個鍵盤。
VGA
是什么呢?我覺得弄清楚這個概念的方方面面沒有更多的作用。對VGA
,了解有限的下面這些有限的知識就夠了。
字符顯示
在這種模式下,顯示器一共能顯示25行字符,每行80個字符。
每個字符占用2個字節,高字節是字符的顏色,低字節是字符的ASCII碼。例如
mov ah, 0Ah
mov al, 'A'
mov ah, 0Ah
中的 0Ah
的高4位和低4位分別是: 0000b
和 1010b
。它們分別設置字符的背景色和前景色。
mov al, 'A'
中的 A
是要打印的字符。
寄存器
VGA
模式下的顯示器是一個硬件,提供了多組寄存器。
讀寫這些寄存器,能設置光標的位置、點擊Up
等方向鍵能滾屏。
讀寫這類寄存器,需先往一類寄存器中寫入另一類寄存器的索引,即往另一類寄存器的第N個寄存器中寫入數據;然后往第二類寄存器寫入數據,不需要再指定具體是哪個寄存器。
tty的實現
顯示器上的內容是顯存中的數據的映射。要讓不同的tty窗口打印不同的內容,只需把顯存分割成若干塊,每個tty分配一塊。
顯示器一滿屏所需空間是80 * 25 * 2 = 4000
個字節。顯存的內存地址是0xB8000~~~0xBFFFF
,總計0xBFFFF - 0xB8000 + 1 = 0x8000 = 32768
字節。如果實現3個tty,每個tty對應的顯存大小大概是32768 / 3 = 10922
個字節,能存儲兩”滿屏“數據。
偽代碼
每個tty設計一個緩沖區C1,數據從鍵盤緩沖區C2到這個緩沖區,然后再從C1讀取數據寫入對應tty的顯存區域。
tty對應的顯存區域,也設計一個結構來存儲,叫Console
。
直接看偽代碼吧。
typedef struct{
// tty使用的顯存的開始位置
int original_address;
// tty使用的顯存的大小
int limit;
// tty使用顯存的當前位置
int current_address;
// tty的光標位置
int cursor_address;
}Console;
typedef struct{
// 緩沖區下一個要處理的字符的
int tail;
// 緩沖區下一個空閑位置
int head;
// 緩沖區存儲的數據的數量
int count;
// 存儲數據的數組
int buff[256];
Console * console;
}TTY;
全局視角下的流程
- tty任務和其他用戶進程(TestA等)一起初始化,並使用
restart
運行tty任務。 - tty任務的流程如下:
- tty任務遍歷所有的tty窗口。
- 如果被遍歷到的tty窗口是當前tty,從C1緩沖區讀取數據D。
- 把D轉換成ASCII碼,然后交給寫顯存模塊打印到顯示器。
- 發生時鍾中斷,給tty任務建立快照,調度模塊讓用戶進程上CPU運行。
- 用戶敲擊鍵盤,8048監測到鍵盤操作,讀取掃描碼(第2套),傳輸給8042。
- 8042把掃描碼(第2套)轉換成第1套,放入鍵盤緩沖區C2,通知8259A發生鍵盤中斷。
- 在C2中的數據被取走前,8042不再接收新數據。
- 鍵盤中斷例程
save
當前進程,從C2讀取數據,然后放入C1。 - tty任務在某個時刻獲得CPU控制權,執行第2步的流程。