目錄
所以,鍵盤是如何工作的
說一說我們的8042
輸出緩沖區寄存器
狀態寄存器
控制寄存器
動手!
注冊中斷
簡單整個鍵盤驅動
Reference
ScanCode Table
我們下一步就是準備進一步完善我們系統的交互性。基于這個,我們想到的第一個可以用來進行輸入的設備,就是鍵盤!這個不假。我們這個章節的核心內容,就是構建一個基于鍵盤的輸入子系統。給我們之后的系統更多功能添磚加瓦!
所以,鍵盤是如何工作的
雖然現在的鍵盤可以說是日新月異,但是基本的工作原理不會發生很大的改變。我們的鍵盤是一個獨立的設備,需要介入總線跟我們的主板系統溝通。不管怎么說,我們的主板上,存在一個Intel 8048 或兼容芯片,它的作用是:每當鍵盤上發生按鍵操作,它就向鍵盤控制器報告哪個鍵被按下,按鍵是否彈起。
上圖的8048 是鍵盤上的芯片,其主要責任就是監控哪個鍵被按下。當鍵盤上發生按鍵操作時,8048 當然知道是哪個鍵被按下。但光它自己知道還不 行,它畢竟要將按鍵信息傳給8042,必須得讓8042 知道到底是按下了哪個鍵,為此8048 必然要和8042達成一個協議,這個協議規定了鍵盤上的每個物理鍵對應的唯一數值,說白了就是對鍵盤上所有的按鍵進行編碼,為每個按鍵分配唯一的數字,這樣雙方都知道了每個數值代表哪個鍵。當某個鍵被按下時,8048 把這個鍵對應的數值發送給8042,8042根據這個數值便知道是哪個鍵被按下了。
現在,筆者就在使用鍵盤敲擊一些字符,我松開手,按鍵被彈起來了,輸入顯然完成了,8048就會通知在主板上的8042何時按鍵被彈起,也就是擊鍵操作何時結束,這樣8042才知道用戶在一次持續按鍵操作中到底輸入了多少個相同的字符。因此,鍵盤掃描碼中不僅僅要記錄按鍵被按下時對應的編碼,還要記錄按鍵被松開(彈起)時的編碼。這下事情變得顯然了,我們不得不請出兩個碼——按鍵被按下時的編碼叫通碼,也就是表示按鍵上的觸點接通了內部電路,使硬件產生了一個碼,故通碼也稱為 makecode。按鍵在被按住不松手時會持續產生相同的碼,直到按鍵被松開時才終止,因此按鍵被松開彈起時產生的編碼叫斷碼,也就是電路被斷開了,不再持續產生碼了,故斷碼也稱為breakcode。一個鍵的掃描碼是由通碼和斷碼組的。
鄭剛老師在《操作系統真相還原》中介紹了三種掃描碼,這里只介紹第二種,因為余下的已經幾乎沒人使用了,我們(程序員)不是不知道鍵盤用的是哪種掃描碼嗎,那好,只要 8042 知道就行。為了兼容第一套 鍵盤掃描碼對應的中斷處理程序,不管鍵盤用的是何種鍵盤掃描碼,當鍵盤將掃描碼發送到 8042 后,都 由 8042 轉換成第一套掃描碼,這就是我們上一節中所說的 8042 的“處理”。 因此,我們在鍵盤的中斷處理程序中只處理第一套鍵盤掃描碼就可以了。關于整張表,參考筆者的附錄即可。
看完這個表,你仔細觀察一下,大多數情況下第一套掃描碼中的通碼和斷碼都是 1 字節大小。而且不難發現:斷碼 = 0x80 + 通碼。
所以回過頭來,我們在分析一下:完整的擊鍵操作包括兩個過程,先是被按下,也許是被按下一瞬間,也許是持續保持被按下,然后是被松開,總之,按下的動作是先于松開發生的,因此每次按鍵時會先產生通碼,再產生斷碼。比如我們按下字符 a 時,按照第一套鍵盤掃描碼來說,先是產生通碼 0x1e,后是產生斷碼 0x9e。
另一些我們注意到:一些按鍵的通碼和斷碼都以0xe0 開頭,它們占2 字節,甚至Pause 鍵以0xe1 開頭, 占6字節。原因是這樣的,并不是一種鍵盤就要用一套鍵盤掃描碼,最初第一套鍵盤掃描碼是由XT 鍵盤所使用的,它后來也被一些更新的鍵盤所使用。XT 鍵盤上的鍵很少,比如右邊回車鍵附近就沒有alt 和ctrl 鍵,這是在后來的鍵盤中才加進去的,因此表示擴展 extend,所以在掃描碼前面加了 0xe0 作為前綴。比如在 XT 鍵盤上,左邊有alt 鍵,其通碼為0x38,斷碼為0xb8。右邊的alt 鍵是后來在新的鍵盤上加進去的,因此,一方面為了表示都是同樣功能的alt 鍵,另一方面表示不是左邊那個alt,而是右邊的alt,于是這個擴展的alt 鍵的掃描碼便為“0xe0 和原來左邊alt 的掃描碼”。因此,右邊alt 鍵的通碼便為“0xe0,0x38”,斷碼為“0xe0,0xb8”。
那組合鍵呢?比如說我們嗯下Ctrl + C鍵,想要復制鄭剛老師的教授內容,自己偷個懶的時候,這個是如何處理的呢?
現在你自己慢慢做一次Ctrl + C試一下:
-
你先摁下Ctrl鍵,畢竟你先按C就會打印出字符C了。
-
你保持Ctrl鍵不松手
-
一個手指按下C
-
然后隨意的松開,比如說可能松開ctrl
-
松開C,然后感覺自己像是一個笨蛋一樣(笑)
不開玩笑了,當我們做步驟一的時候,8048向8042發送了<L-ctrl>
鍵的通碼0x14,當然,這顯然是第二套掃描碼8042收到0x14后將其轉換為第一套鍵盤掃描碼0x1d,并將其保存到自己的輸出緩沖區寄存器中。接著,8042向中斷代理發送中斷信號,處理器隨后執行鍵盤中斷處理程序。鍵盤中斷處理程序從8042的輸出緩沖區寄存器中獲取掃描碼0x1d,并判斷這次按下的是<L-ctrl>
鍵(實際上無論是<L-ctrl>
還是<R-ctrl>
,通常都被視為Ctrl鍵,因為它們只是位置不同,功能相同)。
我們的鍵盤處理程序在某個全局變量中記錄Ctrl鍵已被按下。這個,跟大部分的鍵盤處理程序是一樣的。
第二步的時候,我們的<L-ctrl>
鍵持續按住不松手因此8048會持續向8042發送0x14。8042每次都會將其轉換為第一套鍵盤掃描碼0x1d,并向中斷代理發送中斷信號。每次鍵盤中斷處理程序都會從8042中獲取到0x1d。與步驟一相同,鍵盤處理程序判斷這是<L-ctrl>
的通碼,并在全局變量中記錄Ctrl鍵被按下。盡管Ctrl鍵已經被按下,鍵盤處理程序可能只記錄最后一次按下的鍵,而不關心之前按下了多少次相同的鍵。(我們好像沒必要記著,對吧)
第三步,我們終于準備發送c的第二套鍵盤掃描碼0x21,8042將其轉換為第一套鍵盤掃描碼0x2e,并保存到輸出緩沖區寄存器中,隨后向中斷代理發送中斷信號。
鍵盤中斷處理程序開始執行,從8042的輸出緩沖區寄存器中獲取0x2e。鍵盤處理程序判斷這次按下的是c鍵,并檢查之前Ctrl鍵已經被按下(全局變量中有記錄),因此判斷用戶按下的是“Ctrl+c”組合鍵。Ctrl、Alt、Shift等控制鍵通常與后續按下的鍵組合使用,這是基于微軟的操作習慣,即控制鍵先按下,普通鍵后按下。由于這次按下的不是控制鍵,鍵盤處理程序將記錄Ctrl鍵是否按下的全局變量清空,并將“Ctrl+C”這一消息上報給上層模塊。我們的上層接受到后,就會做對應的Hook操作。
我們在步驟四種假設你是,,<L-ctrl>
鍵被松開,8048向8042發送它的第二套鍵盤掃描碼0xf0和0x14(斷碼)。第二套鍵盤掃描碼的斷碼通常由固定的前綴0xf0和其通碼組成。8042將這兩個字節轉換為第一套鍵盤掃描碼0x9d(斷碼),隨后發送中斷信號。鍵盤中斷處理程序發現最高位為1,表示這是斷碼,意味著鍵被松開。無論松開的是什么鍵,鍵盤處理程序都會忽略,不做任何處理。
在步驟五中,a鍵被松開,8048向8042發送它的第二套鍵盤掃描碼0xf0和0x21(斷碼)。8042將其轉換為0xae并保存到輸出緩沖區寄存器中,隨后發送中斷信號。鍵盤中斷處理程序讀取該掃描碼,發現是鍵被彈起,因此忽略該事件。
說一說我們的8042
Intel 8042 芯片或兼容芯片被集成在主板上的南橋芯片中,它是鍵盤控制器,也就是鍵盤的 IO 接口, 因此它是8048的代理,也是前面所得到的處理器和鍵盤的“中間層”。8048通過PS/2、USB 等接口與8042通信,處理器通過端口與8042通信(IO 接就是外部硬件的代理,它和處理器都位于主機內部,因此處理器與 IO 接口可以通過端口直接通信)。
我們來看看IO口:
寄存器 | 端口 | 讀/寫 |
---|---|---|
Output Buffer(輸出緩沖區) | 0x60 | 讀 |
Input Buffer(輸入緩沖區) | 0x60 | 寫 |
Status Register(狀態寄存器) | 0x64 | 讀 |
Control Register(控制寄存器) | 0x64 | 寫 |
8042 是連接 8048 和處理器的橋梁,8042 存在的目的是:為了處理器可以通過它控制 8048 的工作方式,然后讓8048 的工作成果通過8042 回傳給處理器。此時8042 就相當于數據的緩沖區、中轉站,根據數據被發送的方向,8042 的作用分別是輸入和輸出。
處理器把對 8048 的控制命令臨時放在 8042 的寄存器中,讓 8042 把控制命令發送給 8048,此時 8042 充當了 8048 的參數輸入緩沖區。 8048 把工作成果臨時提交到8042 的寄存器中,好讓處理器能從 8042 的寄存器中獲取它(8048)的工作成果,此時 8042 充當了 8048 的結果輸出緩沖區。
當需要把數據從處理器發到8042 時(數據傳送尚未發生時),0x60 端口的作用是輸入緩沖區,此時應該用out 指令寫入0x60 端口。
當數據已從 8048 發到 8042 時,0x60 端口的作用是輸出緩沖 區,此時應該用in指令從8042 的0x60 端口(輸出緩沖區寄存器)讀 取8048 的輸出結果。
最后,出于編程目的,還差寄存器說明:
寄存器 | 寬度 | 讀寫屬性 | 描述 |
---|---|---|---|
輸入緩沖區寄存器 | 8 位 | 只寫 | 鍵盤驅動程序通過 out 指令向此寄存器寫入對 8048 的控制命令、參數等。對于 8042 本身的控制命令也是寫入此寄存器。 |
狀態寄存器 | 8 位 | 只讀 | 反映 8048 和 8042 的內部工作狀態。各位意義詳見描述。 |
控制寄存器 | 8 位 | 只寫 | 用于寫入命令控制字。每個位都可以設置一種工作方式,意義詳見描述。 |
輸出緩沖區寄存器
8042的輸出緩沖區寄存器是一個8位寬度的寄存器,只讀,鍵盤驅動程序從此寄存器中通過in指令讀取來自8048 的掃描碼、來自8048 的命令應答以及對8042 本身設置時,8042 自身的應答也從該寄存器中獲取。
注意,輸出緩沖區寄存器中的掃描碼是給處理器準備的,在處理器未讀取之前,8042 不會再往此寄 存器中存入新的掃描碼。
8042 是怎樣知道輸出緩沖區寄存器中的值是否被讀取了呢?這個簡單,8042 也有個 智能芯片,它為處理器提供服務,當處理器通過端口跟它要數據的時候它當然知道了,因此,每當有 in 指令來讀取此寄存器時,8042 就將狀態寄存器中的第0位置成0,這就表示寄存器中的掃描碼數據已經被取走,可以繼續處理下一個掃描碼了。當再次往輸出緩沖寄存器存入新的掃描碼時,8042 就將狀態寄存器中的第 0 位置為 1,這表示輸出緩沖寄存器已滿,可以讀取了。
狀態寄存器
位 | 描述 |
---|---|
位 0 | 置 1 時表示輸出緩沖區寄存器已滿,處理器通過 in 指令讀取后該位自動置 0。 |
位 1 | 置 1 時表示輸入緩沖區寄存器已滿,8042 將值讀取后該位自動置 0。 |
位 2 | 系統標志位,最初加電時為 0,自檢通過后置為 1。 |
位 3 | 置 1 時,表示輸入緩沖區中的內容是命令,置 0 時,輸入緩沖區中的內容是普通數據。 |
位 4 | 置 1 時表示鍵盤啟用,置 0 時表示鍵盤禁用。 |
位 5 | 置 1 時表示發送超時。 |
位 6 | 置 1 時表示接收超時。 |
位 7 | 來自 8048 的數據在奇偶校驗時出錯。 |
控制寄存器
位 | 描述 |
---|---|
位 0 | 置 1 時啟用鍵盤中斷。 |
位 1 | 置 1 時啟用鼠標中斷。 |
位 2 | 設置狀態寄存器的位 2。 |
位 3 | 置 1 時,狀態寄存器的位 4 無效。 |
位 4 | 置 1 時禁止鍵盤。 |
位 5 | 置 1 時禁止鼠標。 |
位 6 | 將第二套鍵盤掃描碼轉換為第一套鍵盤掃描碼。 |
位 7 | 保留位,默認為 0。 |
動手!
注冊中斷
這里我們先把中斷一次性注冊了,省事
; -------------------------------------------------------------------------
; ? Part 2 Table Page for the interrupt for kernel
; -------------------------------------------------------------------------
INTR_VECTOR 0x20, PUSH_ZERO ?; Entry for the timer interrupt.
INTR_VECTOR 0x21, PUSH_ZERO ?; Entry for the keyboard interrupt.
INTR_VECTOR 0x22, PUSH_ZERO ?; Cascade interrupt.
INTR_VECTOR 0x23, PUSH_ZERO ?; Entry for serial port 2.
INTR_VECTOR 0x24, PUSH_ZERO ?; Entry for serial port 1.
INTR_VECTOR 0x25, PUSH_ZERO ?; Entry for parallel port 2.
INTR_VECTOR 0x26, PUSH_ZERO ?; Entry for the floppy disk.
INTR_VECTOR 0x27, PUSH_ZERO ?; Entry for parallel port 1.
INTR_VECTOR 0x28, PUSH_ZERO ?; Entry for the real-time clock.
INTR_VECTOR 0x29, PUSH_ZERO ?; Redirect.
INTR_VECTOR 0x2a, PUSH_ZERO ?; Reserved.
INTR_VECTOR 0x2b, PUSH_ZERO ?; Reserved.
INTR_VECTOR 0x2c, PUSH_ZERO ?; PS/2 mouse.
INTR_VECTOR 0x2d, PUSH_ZERO ?; FPU floating-point unit exception.
INTR_VECTOR 0x2e, PUSH_ZERO ?; Hard disk.
INTR_VECTOR 0x2f, PUSH_ZERO ?; Reserved.
記得修改一下支持的中斷數
#define IDT_DESC_CNT (0x30) // The number of interrupt descriptors in the IDT
然后在pci.c種,記得只打開鍵盤的中斷
? ?// Mask interrupts to disable all IRQsoutb(PCI_MASTER_DATA_PORT, 0xfd); ? ? ?// Mask all IRQs on the master PIC (set bit 0)outb(PCI_SLAVE_DATA_PORT, 0xff); ? ? ? // Mask all IRQs on the slave PIC (set all bits)
簡單整個鍵盤驅動
實際上就是直接讀緩存端口就好了哈哈
#include "include/device/keyboard.h"
#include "include/library/ccos_print.h"
#include "include/kernel/interrupt.h"
#include "include/device/configs/keyboard_ascii.h"
#include "include/device/configs/keyboard_mappings.h"
#include "include/io/io.h"
?
static void keyboard_intr_handler(void)
{// hey don't use puts here, gs is not switched, else we will visit// wrong place__ccos_putchar('C');// uint8_t scancode = inb(KEYBOARD_BUF_PORT);// __ccos_display_int(scancode);return;
}
?
// register the interrupt here
void init_basic_input_subsystem(void)
{ccos_puts("initing subsystem of input: from keyboard!\n");register_intr_handler(KEYBOARD_INTERRUPT_N, keyboard_intr_handler);ccos_puts("init subsystem of input: from keyboard done!\n");
}
這里呢,#include "include/device/configs/keyboard_ascii.h" #include "include/device/configs/keyboard_mappings.h"
兩個文件就具體到筆者的代碼種看先。
嘿!我們上電試試看,不用擔心線程的事情,我們把時鐘中斷關閉了。
可以看到我們摁下摁鍵的時候,這些字符就會蹦出來了!注意,嗯下一次,彈起一次。這就說明了通碼和斷碼的存在了。
小小的修改一下代碼哈:
static void keyboard_intr_handler(void)
{// hey don't use puts here, gs is not switched, else we will visit// wrong place// __ccos_putchar('C');uint8_t scancode = inb(KEYBOARD_BUF_PORT);__ccos_display_int(scancode);__ccos_putchar(' ');return;
}
看看!現在外面收到了scancode了!
代碼
CCOperateSystem/Documentations/9_Boost_BasicInputSubsystem/9.2_finish_input_subsystem_1_code at main · Charliechen114514/CCOperateSystemhttps://github.com/Charliechen114514/CCOperateSystem/tree/main/Documentations/9_Boost_BasicInputSubsystem/9.2_finish_input_subsystem_1_code
下一篇
nullhttps://blog.csdn.net/charlie114514191/article/details/146105521
Reference
ScanCode Table
鍵 | 通碼 | 斷碼 | 鍵 | 通碼 | 斷碼 |
---|---|---|---|---|---|
<esc> | 01 | 81 | <caps lock> | 3a | ba |
F1 | 3b | bb | a | 1e | 9e |
F2 | 3c | bc | s | 1f | 9f |
F3 | 3d | bd | d | 20 | a0 |
F4 | 3e | be | f | 21 | a1 |
F5 | 3f | bf | g | 22 | a2 |
F6 | 40 | c0 | h | 23 | a3 |
F7 | 41 | c1 | j | 24 | a4 |
F8 | 42 | c2 | k | 25 | a5 |
F9 | 43 | c3 | l | 26 | a6 |
F10 | 44 | c4 | :; | 27 | a7 |
F11 | 57 | d7 | "' | 28 | a8 |
F12 | 58 | d8 | <enter> | 1c | 9c |
~· | 29 | a9 | <L-Shift> | 2a | aa |
!1 | 02 | 82 | z | 2c | ac |
@2 | 03 | 83 | x | 2d | ad |
#3 | 04 | 84 | c | 2e | ae |
$4 | 05 | 85 | v | 2f | af |
%5 | 06 | 86 | b | 30 | b0 |
^6 | 07 | 87 | n | 31 | b1 |
&7 | 08 | 88 | m | 32 | b2 |
*8 | 09 | 89 | <, | 33 | b3 |
(9 | 0a | 8a | >. | 34 | b4 |
)0 | 0b | 8b | ?/ | 35 | b5 |
_- | 0c | 8c | <R-shift> | 36 | b6 |
+= | 0d | 8d | <L-ctrl> | 1d | 9d |
<backspace> | 0e | 8e | <L-alt> | 38 | b8 |
<tab> | 0f | 8f | <space> | 39 | b9 |
q | 10 | 90 | <R-alt> | e0,38 | e0,b8 |
w | 11 | 91 | <R-ctrl> | e0,1d | e0,9d |
e | 12 | 12 | |||
r | 13 | 93 | |||
t | 14 | 94 | |||
y | 15 | 95 | |||
u | 16 | 96 | |||
i | 17 | 97 | |||
o | 18 | 98 | |||
p | 19 | 99 | |||
{[ | 1a | 9a | |||
}] | 1b | 9b | |||
|\ | 2b | ab |
鍵 | 通碼 | 斷碼 | 鍵 | 通碼 | 斷碼 |
---|---|---|---|---|---|
PrintScreen SysRq | e0,2a,e0,37 | e0,b7,e0,aa | NumLock | 45 | c5 |
Scroll Lock | 46 | c6 | / | e0,35 | e0,b5 |
Pause Break | e1,1d,45 | e1,9d,c5 | * | 37 | b7 |
Insert | e0,52 | e0,d2 | - | 4a | ca |
Home | e0,47 | e0,c7 | 7Home | 47 | c7 |
Page Up | e0,49 | e0,c9 | 8Up | 48 | c8 |
Delete | e0,53 | e0,d3 | 9PgUp | 49 | c9 |
End | e0,4f | e0,cf | 4Left | 4b | cb |
Page Down | e0,51 | e0,d1 | 5 | 4c | cc |
← | e0,46 | e0,c6 | 6Right | 4d | cd |
→ | e0,4d | e0,cd | 1End | 4f | cf |
↑ | e0,48 | e0,c8 | 2Down | 50 | d0 |
↓ | e0,50 | e0,d0 | 3PgDn | 51 | d1 |
0Ins | 52 | d2 | .Del | 53 | d3 |
+ | 4e | ce | Enter | e0,1c | e0,9c |