鍵盤驅動原理
0 概述
我們將討論 ps/2 鍵盤的驅動。主要討論的內容有,ps/2 鍵盤的硬體,使用鍵盤驅動的應用程式層,鍵盤驅動的初始化,鍵盤驅動如何完成自己的工作,以及一些涉及到的相關內容。需要注意的是,以後我們提到的鍵盤,如果沒有特殊說明,都是指 ps/2 鍵盤。
1 ps/2 鍵盤的硬體
要以寫一個硬體的驅動為目的的話,需要對這個硬體有一定的瞭解,但並不需要太深入,只要對於寫驅動足夠就可以了。關於 ps/2 鍵盤的硬體知識,我們也是對討論鍵盤驅動足夠就可以了,一些對於驅動沒有協助的硬體實現的細節,我們不討論。
1.1 ps/2 鍵盤硬體概述
對於驅動來說,和鍵盤相關的最重要的硬體是兩個晶片。一個是 intel 8042 晶片,位於主板上,CPU 通過 IO 連接埠直接和這個晶片通訊,獲得按鍵的掃描碼或者發送各種鍵盤命令。另一個是 intel 8048 晶片或者其相容晶片,位於鍵盤中,這個晶片主要作用是從鍵盤的硬體中得到被按的鍵所產生的掃描碼,與 i8042 通訊,修飾鍵盤本身。
當鍵盤上有鍵被按下時,i8048 直接獲得鍵盤硬體產生的掃描碼。i8048 也負責鍵盤本身的控制,比如點亮 LED 指示燈,熄滅 LED 指示燈。i8048 通過 ps/2 口和 i8042 通訊,把得到的掃描碼傳送給 i8042。CPU 通過讀寫連接埠,可以直接把 i8042 中的資料讀入到 CPU 的寄存器中,或者把 CPU 寄存器中的資料寫入 i8042 中。ps/2 口一共有 6 個引腳,可以拔下 ps/2 插頭看一下,這 6 個引腳分別為,時鐘,資料,電源地,電源+5V,還有2 個引腳沒有被用到。由於只有一個引腳傳輸資料,所以 ps/2 口上的資料轉送是串列的。
下面幾幅圖是一個鍵盤的內部,可以看到用來產生掃描碼的按鍵矩陣( Key Martix ),可以看到鍵盤中的晶片(這裡不是i8048,是一個相容的其他型號的晶片)。
注意,i8042 並不一定在主板上單獨出現,可能被整合在某一晶片中。
1.2 掃描碼 ,Make Code ,Break Code ,Typematic
當鍵盤上有鍵被按下,鬆開,按住,鍵盤將產生掃描碼( Scan Code ),這些掃描碼將被 i8048 直接得到。掃描碼有兩種,Make Code 和 Break Code。當一個鍵被按下或按住時產生的是 Make Code ,當一個鍵被鬆開產生的是 Break Code。每個鍵被分配了唯一的 Make Code 和 Break Code ,這樣主機通過掃描碼就可以知道是哪一個鍵。簡單的說就是按下鍵,產生一個 Make Code。鬆開鍵,產生一個 Break Code。
而對於按住不放的情況呢。我們可以開啟一個記事本,把 $1$a$1$ 鍵按住不放,可以看到會不停的產生 $1$a$1$ 直到我們鬆開。這是由於,當按住一個鍵不放時,將會 Typematic ,也就是自動打。每隔一定時間,自動產生一個被按住的鍵的 Make Code,直到最後鬆開該鍵。對於 Typematic 有兩個重要的參數,一個是 Typematic Delay ,決定了按下多長時間之後,進入 Typematic,另一個是 Typematic Rate,決定了在進入 Typematic 之後,一秒鐘內能產生多少個 Make Code 。現在我們再開啟記事本,按住 $1$a$1$ 不放,仔細觀察,將看到第一個 $1$a$1$ 和第二個 $1$a$1$ 之間間隔的時間明顯比其他的要長,而之後每個$1$a$1$之間的時間間隔是一樣的。
而對於同步選取多個鍵的情況呢。在一個鍵被按下,產生了 Make Code,而沒有被鬆開,沒有產生 Break Code 的時候,再按下另一個鍵,於是產生了另一個鍵的 Make Code ,就算是這兩個鍵被同步選取。之後,這兩個鍵鬆開時,各自產生各自的 Break Code。更多的鍵的情況也是一樣。比如要按 Ctrl 和 A,下面的情況就算作是同步選取了 Ctrl 和 A。按Ctrl,產生 Ctrl 的 Make Code,然後按A,產生 A 的 Make Code,然後各自鬆開,各自產生各自的 Break Code。
而對於按一個鍵不放的時候,再按另一個鍵的情況呢。我們可以開啟一個記事本,把$1$a$1$鍵按住不放,不要鬆開,然後再按$1$s $1$鍵不放。我們可以看到當按下$1$s$1$時,$1$a$1$鍵並沒有松,但是並沒有$1$a$1$再出現了,而是$1$s$1$開始出現,這時即使鬆開了$1$s$1$,$1$a$1$也不會繼續出現了。
1.3 掃描碼集
到目前為止一共有三套掃描碼集( Scan Code Set ),ps/2 鍵盤預設使用第二套。不過可以設定 i8042,讓 i8042
把得到的 Scan Code 翻譯成 Scan Code Set 1 中的 Scan Code ,這樣鍵盤驅動從 i8042
得到的所有 Scan Code 都是第一套中的 Scan Code(實際中驅動也是這麼做的)。所以我們只討論 Scan Code
Set 1 。需要說明的是 Scan Code 和 ASCII碼完全不相同。
在 Scan
Code Set 1 中,大多數鍵的 Make Code,Break Code 都是一個位元組。他們的 Make Code
的最高位都為0,也就是他們的 Make Code 都小於 0x7F。而他們的 Break Code
為其 Make Code 或運算 80h ,也就是把 Make Code 的低7位不變,最高位設定為1。
還有一些擴充按鍵,他們的 Scan Code 是雙位元組的。他們的第一個位元組都是E0h,表明這是一個擴充鍵。第2個位元組,和單位元組
Scan Code 的情況相同。
還有一個特殊的鍵,Pause/Break 鍵,它的 Make Code 為 E1,1D,45 E1,9D,C5,注意是 E1h
開頭。而且它沒有 Break Code 。
我們按鍵的 Make Code 的值的大小,列出 Scan Code Set 1 中的所有掃描碼
KEY MAKE BREAK
ESC 01 81
1 02 82
2 03 83
3 04 84
4 05 85
5 06 86
6 07 87
7 08 88
8 09 89
9 0A 8A
0 0B 8B
- 0C 8C
= 0D 8D
BKSP 0E 8E
TAB 0F 8F
Q 10 90
W 11 91
E 12 92
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
ENTER 1C 9C
L_CTRL 1D 9D
A 1E 9E
S 1F 9F
D 20 A0
F 21 A1
G 22 A2
H 23 A3
J 24 A4
K 25 A5
L 26 A6
; 27 A7
$1$ 28 A8
` 29 89
L_SHFT 2A AA
\ 2B AB
Z 2C AC
X 2D AD
C 2E AE
V 2F AF
B 30 B0
N 31 B1
M 32 B2
, 33 B3
. 34 B4
/ 35 B5
R_SHFT 36 B6
KP * 37 B7
L_ALT 38 B8
SPACE 39 B9
CAPS 3A BA
F1 3B BB
F2 3C BC
F3 3D BD
F4 3E BE
F5 3F BF
F6 40 C0
F7 41 C1
F8 42 C2
F9 43 C3
F10 44 C4
NUM 45 C5
SCROLL 46 C6
KP 7 47 C7
KP 8 48 C8
KP 9 49 C9
KP - 4A CA
KP 4 4B CB
KP 5 4C CC
KP 6 4D CD
KP + 4E CE
KP 1 4F CF
KP 2 50 D0
KP 3 51 D1
KP 0 52 D2
KP . 53 D3
F11 57 D7
F12 58 D8
KP EN E0,1C E0,9C
R_CTRL E0,1D E0,9D
KP / E0,35 E0,B5
R_ALT E0,38 E0,B8
HOME E0,47 E0,C7
UP ARROW E0,48 E0,C8
PG UP E0,49 E0,C9
L ARROW E0,4B E0,CB
R ARROW E0,4D E0,CD
END E0,4F E0,CF
D ARROW E0,50 E0,D0
PG DN E0,51 E0,D1
INSERT E0,52 E0,D2
DELETE E0,53 E0,D3
L GUI E0,5B E0,DB
R GUI E0,5C E0,DC
APPS E0,5D E0,DD
PRNT SCRN E0,2A, E0,37 E0,B7, E0,AA
PAUSE E1,1D,45 E1,9D,C5 -NONE
這裡說幾句對驅動沒有協助的題外話,記不清是由於先有了關於 Scan Code 的值的猜測,才去按這個順序列 Scan Code
,還是先這樣列 Scan Code ,才有了關於 Scan Code 的值的猜測。總之,用這個
Make Code 的順序,和我們現在鍵盤上鍵的布局做對照,我們大致就能猜到為什麼 A 鍵的 Make Code 值為
0x1e,為什麼 H 鍵的 Make Code 值為 0x23。我們拿其中的一小段舉例子,A 1E,S 1F,D 20,F 21,G
22,H 23,看看鍵盤上 A,S,D,F,G,H 的位置吧。能感覺到些什麼吧,感覺不到就算了,這個和驅動是無關的。從 Scan
Code Set 1,可能還能推測出來最早的鍵盤的樣子。以及發生在鍵盤上的一些變化。我們注意到 F10 和 F11,F12 的
Make Code 不是連在一起的,估計比較早的鍵盤只有10個功能鍵,而不是現在的12個功能鍵。從鍵的 Make Code
來看,有可能曾經使用的一些鍵,現在已經不出現在鍵盤上了。
還有一個值得注意的是,如果有 Make Code 為 0x60 的鍵,那麼它的 Break Code 應該為
0x60+0x80=0xE0。那麼這個鍵的 Break Code 將會和 表示擴充碼的 0xE0 搞混。不過還好,並沒有 Make
Code 為 0x60 的鍵,所以不會發生搞混的情況。
1.4 i8042 鍵盤控制器
鍵盤驅動直接讀寫
i8042 晶片,通過 i8042 間接的向鍵盤中的 i8048 發命令。所以對於驅動來說,直接發生聯絡的只有 i8042
,因此我們只介紹 i8042 ,不介紹 i8048。
象
i8042,i8048 這樣的晶片,本身就是一個小的處理器,它的內部有自己的處理器,有自己的 Ram,有自己的寄存器,等等。
i8042 有 4
個 8 bits 的寄存器,他們是 Status Register(狀態寄存器),Output Buffer(輸出緩衝器),
Input Buffer(輸入緩衝器),Control Register(控制寄存器)。使用兩個 IO 連接埠,60h 和
64h。
Status Register(狀態寄存器)
狀態寄存器是一個8位唯讀寄存器,任何時刻均可被cpu讀取。其各位定義如下
Bit7: PARITY-EVEN(P_E): 從鍵盤獲得的資料同位錯誤
Bit6: RCV-TMOUT(R_T): 接收逾時,置1
Bit5: TRANS_TMOUT(T_T): 發送逾時,置1
Bit4: KYBD_INH(K_I): 為1,鍵盤沒有被禁止。為0,鍵盤被禁止。
Bit3: CMD_DATA(C_D): 為1,輸入緩衝器中的內容為命令,為0,輸入緩衝器中的內容為資料。
Bit2: SYS_FLAG(S_F): 系統標誌,加電啟動置0,自檢通過後置1
Bit1: INPUT_BUF_FULL(I_B_F): 輸入緩衝器滿置1,i8042 取走後置0
BitO: OUT_BUF_FULL(O_B_F): 輸出緩衝器滿置1,CPU讀取後置0
Output Buffer(輸出緩衝器)
輸出緩衝器是一個8位唯讀寄存器。驅動從這個寄存器中讀取資料。這些資料包括,掃描碼,發往 i8042 命令的響應,間接的發往
i8048 命令的響應。
Input Buffer(輸入緩衝器)
輸入緩衝器是一個8位唯寫寄存器。緩衝驅動發來的內容。這些內容包括,發往 i8042 的命令,通過 i8042 間接發往
i8048 的命令,以及作為命令參數的資料。
Control Register(控制寄存器)
也被稱作 Controller Command Byte (控制器命令位元組)。其各位定義如下
Bit7: 保留,應該為0
Bit6: 將第二套掃描碼翻譯為第一套
Bit5: 置1,禁止滑鼠
Bit4: 置1,禁止鍵盤
Bit3: 置1,忽略狀態寄存器中的 Bit4
Bit2: 設定狀態寄存器中的 Bit2
Bit1: 置1,enable 滑鼠中斷
BitO: 置1,enable 鍵盤中斷
2個連接埠 0x60,0x64
驅動中把 0x60 叫資料連接埠
驅動中把 0x64 叫命令連接埠
1.5 命令
驅動可以直接給 i8042 發命令,可以通過 i8042 間接給 i8048 發命令。命令這部分內容直接來自
< 參考資料 [1] >。
1.5.1 發給i8042的命令
驅動對鍵盤控制器發送命令是通過寫連接埠64h實現的,共有12條命令,分別為
20h
準備讀取8042晶片的Command Byte;其行為是將當前8042 Command Byte的內容放置於Output
Register中,下一個從60H連接埠的讀操作將會將其讀取出來。
60h
準備寫入8042晶片的Command Byte;下一個通過60h寫入的位元組將會被放入Command Byte。
A4h
測試一下鍵盤密碼是否被設定;測試結果放置在Output
Register,然後可以通過60h讀取出來。測試結果可以有兩種值:FAh=密碼被設定;F1h=沒有密碼。
A5h
設定鍵盤密碼。其結果被按照順序通過60h連接埠一個一個被放置在Input
Register中。密碼的最後是一個空位元組(內容為0)。
A6h
讓密碼生效。在發布這個命令之前,必須首先使用A5h命令設定密碼。
AAh
自檢。診斷結果放置在Output Register中,可以通過60h讀取。55h=OK。
ADh
禁止鍵盤介面。Command Byte的bit-4被設定。當此命令被發布後,Keyboard將被禁止發送資料到Output
Register。
AEh
開啟鍵盤介面。Command Byte的bit-4被清除。當此命令被發布後,Keyboard將被允許發送資料到Output
Register。
C0h
準備讀取Input Port。Input Port的內容被放置於Output Register中,隨後可以通過60h連接埠讀取。
D0h
準備讀取Outport連接埠。結果被放在Output Register中,隨後通過60h連接埠讀取出來。
D1h
準備寫Output連接埠。隨後通過60h連接埠寫入的位元組,會被放置在Output Port中。
D2h
準備寫資料到Output Register中。隨後通過60h寫入到Input Register的位元組會被放入到Output
Register中,此功能被用來類比來自於Keyboard發送的資料。如果中斷被允許,則會觸發一個中斷。
1.5.2 發給8048的命令
共有10條命令,分別為
EDh
設定LED。Keyboard收到此命令後,一個LED設定會話開始。Keyboard首先回複一個ACK(FAh),然後等待從60h連接埠寫入的LED
設定位元組,如果等到一個,則再次回複一個ACK,然後根據此位元組設定LED。然後接著等待。。。直到等到一個非LED設定位元組(高位被設定),此時LED
設定會話結束。
EEh
診斷Echo。此命令純粹為了檢測Keyboard是否正常,如果正常,當Keyboard收到此命令後,將會回複一個EEh位元組。
F0h
選擇Scan code set。Keyboard系統共可能有3個Scan code
set。當Keyboard收到此命令後,將回複一個ACK,然後等待一個來自於60h連接埠的Scan code
set代碼。系統必須在此命令之後發送給Keyboard一個Scan code
set代碼。當Keyboard收到此代碼後,將再次回複一個ACK,然後將Scan code set設定為收到的Scan code
set代碼所要求的。
F2h
讀取Keyboard
ID。由於8042晶片後不僅僅能夠接Keyboard。此命令是為了讀取8042後所接的裝置ID。裝置ID為2個位元組,Keyboard
ID為83ABh。當鍵盤收到此命令後,會首先回複一個ACK,然後,將2位元組的 Keyboard ID一個一個回複回去。
F3h
設定Typematic
Rate/Delay。當Keyboard收到此命令後,將回複一個ACK。然後等待來自於60h的設定位元組。一旦收到,將回複一個ACK,然後將Keyboard
Rate/Delay設定為相應的值。
F4h
清理鍵盤的Output Buffer。一旦Keyboard收到此命令,將會將Output
buffer清空,然後回複一個ACK。然後繼續接受Keyboard的擊鍵。
F5h
設定預設狀態(w/Disable)。一旦Keyboard收到此命令,將會將Keyboard完全初始化成預設狀態。之前所有對它的設定都將失效——Output
buffer被清空,Typematic Rate/Delay被設定成預設值。然後回複一個
ACK,接著等待下一個命令。需要注意的是,這個命令被執行後,鍵盤的擊鍵接受是禁止的。如果想讓鍵盤接受擊鍵輸入,必須Enable
Keyboard。
F6h
設定預設狀態。和F5命令唯一不同的是,當此命令被執行之後,鍵盤的擊鍵接收是允許的。
FEh
Resend。如果Keyboard收到此命令,則必須將剛才發送到8042 Output Register
中的資料重新發送一遍。當系統檢測到一個來自於Keyboard的錯誤之後,可以使用自命令讓Keyboard重新發送剛才發送的位元組。
FFh
Reset
Keyboard。如果Keyboard收到此命令,則首先回複一個ACK,然後啟動自身的Reset程式,並進行自身基本正確性檢測(BAT-Basic
Assurance Test)。等這一切結束之後,將返回給系統一個單位元組的結束碼(AAh=Success,
FCh=Failed),並將鍵盤的Scan code set 設定為2。
1.5.3 讀到的資料
00h/FFh
當擊鍵或釋放鍵時檢測到錯誤時,則在Output Bufer後放入此位元組,如果Output Buffer已滿,則會將Output
Buffer的最後一個位元組替代為此位元組。使用Scan code set 1時使用00h,Scan code 2和Scan Code
3使用FFh。
AAh
BAT完成代碼。如果鍵盤檢測成功,則會將此位元組發送到8042 Output Register中。
EEh
Echo響應。Keyboard使用EEh響應從60h發來的Echo請求。
F0h
在Scan code set 2和Scan code set 3中,被用作Break Code的首碼。
FAh
ACK。當Keyboard任何時候收到一個來自於60h連接埠的合法命令或合法資料之後,都回複一個FAh。
FCh
BAT失敗代碼。如果鍵盤檢測失敗,則會將此位元組發送到8042 Output Register中。
FEh
Resend。當Keyboard任何時候收到一個來自於60h連接埠的非法命令或非法資料之後,或者資料的奇偶交驗錯誤,都回複一個FEh,要求系統重新發送相關命令或資料。
83ABh
當鍵盤收到一個來自於60h的F2h命令之後,會依次回複83h,ABh。83AB是鍵盤的ID。
Scan code
除了上述那些特殊位元組以外,剩下的都是Scan code。
1.6 連接埠操作
首先介紹一下連接埠的讀寫操作,驅動中使用函數 READ_PORT_UCHAR 進行讀操作, READ_PORT_UCHAR
中使用CPU讀連接埠指令,in。驅動中使用函數 WRITE_PORT_UCHAR 進行寫操作, WRITE_PORT_UCHAR
中使用CPU寫連接埠指令,out。
1.6.1 讀取狀態寄存器
讀取狀態寄存器的方法,對64h連接埠進行讀操作。
1.6.2 讀資料
需要讀取的資料有,i8042從i8048得到的按鍵的掃描碼,i8042命令的ACK,i8042從i8048得到的i8048命令的ACK,需要命令重發的RESEND,一些需要返回結果的命令得到的結果。
當有資料需要被驅動讀走的時候,資料被放入輸出緩衝器,同時將狀態寄存器的bit0(OUTPUT_BUFFER_FULL)置1,引發鍵盤中斷(鍵盤中
斷的IRQ為1)。由於鍵盤中斷,引起由鍵盤驅動提供的鍵盤插斷服務常式被執行。在鍵盤插斷服務常式中,驅動會從i8042讀走資料。一旦資料讀取完成,
狀態寄存器的bit0會被清0。
讀資料的方法,首先,讀取狀態寄存器,判斷bit0,狀態寄存器bit0為1,說明輸出緩衝器中有資料。保證狀態寄存器bit0為1,然後對60h連接埠進行讀操作,讀取資料。
這裡我們要談一點很有用的題外話,前面提到的 IRQ,是 Interrupt Request
line,插斷要求線,是一個硬體線,它和中斷向量是不同的。中斷向量是用來在中斷描述符表(IDT)中尋找插斷服務常式的那個序號。鍵盤的
IRQ 是1,鍵盤插斷服務常式對應的中斷向量可不是1。這點要弄清楚。
1.6.3 向i8042發命令
當命令被發往i8042的時候,命令被放入輸入緩衝器,同時引起狀態寄存器的 Bit1 置1,表示輸入緩衝器滿,同時引起狀態寄存器的
Bit2 置1,表示寫入輸入緩衝器的是一個命令。
向i8042發命令的方法,首先,讀取狀態寄存器,判斷bit1,狀態寄存器bit1為0,說明輸入緩衝器為空白,可以寫入。保證狀態寄存器bit1為0,然後對64h連接埠進行寫操作,寫入命令。
1.6.4 間接向i8048發命令
向i8042發這些命令,i8042會轉寄i8048,命令被放入輸入緩衝器,同時引起狀態寄存器的Bit1
置1,表示輸入緩衝器滿,同時引起狀態寄存器的 Bit2
置1,表示寫入輸入緩衝器的是一個命令。這裡我們要注意,向i8048發命令,是通過寫60h連接埠,而後面發命令的參數,也是寫60h連接埠。i8042如何判斷輸入緩衝器中的內容是命令還是參數呢,我們在後面發命令的參數中一起討論。
向i8048發命令的方法,首先,讀取狀態寄存器,判斷bit1,狀態寄存器bit1為0,說明輸入緩衝器為空白,可以寫入。保證狀態寄存器bit1為0,然後對60h連接埠進行寫操作,寫入命令。
1.6.5 發命令的參數
某些命令需要參數,我們在發送命令之後,發送它的參數,參數被放入輸入緩衝器,同時引起狀態寄存器的Bit1
置1,表示輸入緩衝器滿。這裡我們要注意,向i8048發命令,是通過寫60h連接埠,發命令的參數,也是寫60h連接埠。i8042如何判斷輸入緩衝器中的內容是命令還是參數呢。i8042
是這樣判斷的,如果目前狀態寄存器的Bit3
為1,表示之前已經寫入了一個命令,那麼現在通過寫60h連接埠放入輸入緩衝器中的內容,就被當做之前命令的參數,並且引起狀態寄存器的
Bit3 置0。如果目前狀態寄存器的 Bit3
為0,表示之前沒有寫入命令,那麼現在通過寫60h連接埠放入輸入緩衝器中的內容,就被當做一個間接發往i8048的命令,並且引起狀態寄存器的
Bit3 置1。
向i8048發參數的方法,首先,讀取狀態寄存器,判斷bit1,狀態寄存器bit1為0,說明輸入緩衝器為空白,可以寫入。保證狀態寄存器bit1為0,然後對60h連接埠進行寫操作,寫入參數