前情提要
上一節(jié)我們實(shí)現(xiàn)了鎖與信號量,這一節(jié)我們就可以實(shí)現(xiàn)鍵盤驅(qū)動(dòng)了,訪問鍵盤輸入的數(shù)據(jù)也屬于臨界區(qū)資源,所以需要鎖的存在。
一、鍵盤簡介
之前的 ps/2 鍵盤使用的是中斷驅(qū)動(dòng)的,在當(dāng)時(shí),按下鍵盤就會(huì)觸發(fā)中斷,引導(dǎo)操作系統(tǒng)去處理這個(gè)按鍵行文。但是當(dāng)今的usb鍵盤,使用的是輪詢機(jī)制,cpu會(huì)定時(shí)訪問鍵盤看有沒有按下鍵盤。
我個(gè)人認(rèn)為這是cpu技術(shù)的進(jìn)步導(dǎo)致的,在之前,cpu的頻率比較低,使用輪詢可能會(huì)導(dǎo)致漏掉用戶按鍵的行為。但是在今天,cpu的主頻已經(jīng)非常高了,處理一個(gè)按鍵行為就觸發(fā)中斷,這個(gè)開銷太大了,而且輪詢的頻率也上來了,現(xiàn)在每秒訪問幾千次對電腦一點(diǎn)影響都沒有,所以現(xiàn)在大多采用了輪詢機(jī)制。
不過據(jù)說中斷驅(qū)動(dòng)的還是比較快,現(xiàn)在一些電競主板還是支持ps/2的接口,這個(gè)未經(jīng)論證。
1.1、鍵盤的通碼與斷碼
鍵盤的狀態(tài)要么是按下,要么是彈起,因此一個(gè)鍵便有兩個(gè)編碼,按鍵按下時(shí)的編碼叫做通碼,鍵盤上的觸電接通了電路,使硬件產(chǎn)生了一個(gè)編碼,故此通碼叫makecode。按鍵在被按住不松手時(shí)會(huì)持續(xù)產(chǎn)生相同的碼,直到按鍵被松開時(shí)才終止,因此按鍵被松開彈起時(shí)產(chǎn)生的編碼叫斷碼,也就是電路被斷開了,不再持續(xù)產(chǎn)生碼了,故斷碼也稱為breakcode。
無論是按下鍵,或是松開鍵,當(dāng)鍵的狀態(tài)改變后,鍵盤中的8048芯片把按鍵對應(yīng)的掃描碼(通碼或斷碼)發(fā)送到主板上的8042芯片,由8042處理后保存在自己的寄存器中,然后向8259A發(fā)送中斷信號,這樣處理器便去執(zhí)行鍵盤中斷處理程序,將8042處理過的掃描碼從它的寄存器中讀取出來,繼續(xù)進(jìn)行下一步處理。
1.2、鍵盤掃描碼
鍵的掃描碼是由鍵盤中的鍵盤編碼器決定的,不同的編碼方案便是不同的鍵盤掃描碼,也就是說,相同的鍵在不同的編碼方案下產(chǎn)生的通碼和斷碼也是不同的。
根據(jù)不同的編碼方案,鍵盤掃描碼有三套,分別稱為scan code set 1、scan code set 2、scan code set 3。
其中scan code set 1是XT鍵盤用的掃描碼,這個(gè)歷史就比較久遠(yuǎn)了。scan code set 2是AT鍵盤的掃描碼,這個(gè)鍵盤和我們當(dāng)今的鍵盤也不是很一樣,但是已經(jīng)比較接近了。scan code set 3是IBM PS/2系列高端計(jì)算機(jī)所用的鍵盤上,IBM藍(lán)色巨人現(xiàn)在都涼了,這個(gè)鍵盤也就很少看到了。
第二套鍵盤掃描碼幾乎是目前所使用的鍵盤的標(biāo)準(zhǔn),因此大多數(shù)鍵盤向8042發(fā)送的掃描碼都是第二套掃描碼。但是難免有別的鍵盤,所以才會(huì)出現(xiàn)8042這個(gè)芯片,這個(gè)芯片做一個(gè)中間層,為了兼容第一套鍵盤掃描碼對應(yīng)的中斷處理程序,不管鍵盤用的是何種鍵盤掃描碼,當(dāng)鍵盤將掃描碼發(fā)送到8042后,都由8042轉(zhuǎn)換成第一套掃描碼,我們再從8042中讀取掃描碼。
這里我們給出常用鍵位的掃描碼(這里的掃描碼就是通碼,加0x80
就是斷碼)
按鍵 | 掃描碼 | 按鍵 | 掃描碼 | 按鍵 | 掃描碼 |
---|---|---|---|---|---|
Esc | 0x01 | F1 | 0x3B | F2 | 0x3C |
F3 | 0x3D | F4 | 0x3E | F5 | 0x3F |
F6 | 0x40 | F7 | 0x41 | F8 | 0x42 |
F9 | 0x43 | F10 | 0x44 | F11 | 0x57 |
F12 | 0x58 | PrintSc | 0x37 | ScrollLk | 0x46 |
Pause/Brk | 0x45 | ` | 0x29 | 1 | 0x02 |
2 | 0x03 | 3 | 0x04 | 4 | 0x05 |
5 | 0x06 | 6 | 0x07 | 7 | 0x08 |
8 | 0x09 | 9 | 0x0A | 0 | 0x0B |
- | 0x0C | = | 0x0D | Backspace | 0x0E |
Tab | 0x0F | Q | 0x10 | W | 0x11 |
E | 0x12 | R | 0x13 | T | 0x14 |
Y | 0x15 | U | 0x16 | I | 0x17 |
O | 0x18 | P | 0x19 | [ | 0x1A |
] | 0x1B | | | 0x2B | CapsLock | 0x3A |
A | 0x1E | S | 0x1F | D | 0x20 |
F | 0x21 | G | 0x22 | H | 0x23 |
J | 0x24 | K | 0x25 | L | 0x26 |
; | 0x27 | ’ | 0x28 | Enter | 0x1C |
Shift左 | 0x2A | Z | 0x2C | X | 0x2D |
C | 0x2E | V | 0x2F | B | 0x30 |
N | 0x31 | M | 0x32 | , | 0x33 |
. | 0x34 | / | 0x35 | Shift右 | 0x36 |
Ctrl左 | 0x1D | Win左 | 0xE0 | Alt左 | 0x38 |
Space | 0x39 | Alt右 | 0xE038 | Win右 | 0xE0 |
Menu | 0xE0 | Ctrl右 | 0xE01D |
問:為什么會(huì)有通碼和斷碼,通碼不就夠了嘛
**答:**如果按一個(gè)組合鍵的話,比如ctrl+a,是先按下ctrl,再按a,再松開ctrl,再松開a。如果沒有斷碼,我們無法判斷ctrl是否松開。
1.3、鍵盤的芯片
和鍵盤相關(guān)的芯片只有8042和8048,它們都是獨(dú)立的處理器,都有自己的寄存器和內(nèi)存。Intel 8048芯片或兼容芯片位于鍵盤中,它是鍵盤編碼器,Intel 8042芯片或兼容芯片被集成在主板上的南橋芯片中,它是鍵盤控制器,也就是鍵盤的IO接口,因此它是8048的代理,也是前面所得到的處理器和鍵盤的“中間層”。我們只需要學(xué)習(xí)8042就夠了
他的端口如下
寄存器 | 端口 | 讀寫 |
---|---|---|
Output Buffer(輸出緩沖區(qū)) | 0x60 | 讀 |
Input Buffer(輸入緩沖區(qū)) | 0x60 | 寫 |
Status Register(狀態(tài)寄存器) | 0x64 | 讀 |
Control Register(控制寄存器) | 0x64 | 寫 |
狀態(tài)寄存器8位寬度的寄存器,只讀,反映8048和8042的內(nèi)部工作狀態(tài)。各位意義如下。
(1)位0:置1時(shí)表示輸出緩沖區(qū)寄存器已滿,處理器通過in指令讀取后該位自動(dòng)置0。
(2)位1:置1時(shí)表示輸入緩沖區(qū)寄存器已滿,8042將值讀取后該位自動(dòng)置0。
(3)位2:系統(tǒng)標(biāo)志位,最初加電時(shí)為0,自檢通過后置為1。
(4)位3:置1時(shí),表示輸入緩沖區(qū)中的內(nèi)容是命令,置0時(shí),輸入緩沖區(qū)中的內(nèi)容是普通數(shù)據(jù)。
(5)位4:置1時(shí)表示鍵盤啟用,置0時(shí)表示鍵盤禁用。
(6)位5:置1時(shí)表示發(fā)送超時(shí)。
(7)位6:置1時(shí)表示接收超時(shí)。
(8)位7:來自8048的數(shù)據(jù)在奇偶校驗(yàn)時(shí)出錯(cuò)。
8位寬度的寄存器,只寫,用于寫入命令控制字。每個(gè)位都可以設(shè)置一種工作方式,意義如下。
(1)位0:置1時(shí)啟用鍵盤中斷。
(2)位1:置1時(shí)啟用鼠標(biāo)中斷。
(3)位2:設(shè)置狀態(tài)寄存器的位2。
(4)位3:置1時(shí),狀態(tài)寄存器的位4無效。
(5)位4:置1時(shí)禁止鍵盤。
(6)位5:置1時(shí)禁止鼠標(biāo)。
(7)位6:將第二套鍵盤掃描碼轉(zhuǎn)換為第一套鍵盤掃描碼。
(8)位7:保留位,默認(rèn)為0。
二、環(huán)形隊(duì)列
鍵盤中斷的數(shù)據(jù)是放在隊(duì)列中的,等待其他線程的讀取。如果我們之前做過關(guān)于軟件相關(guān)的工作,很容易理解這個(gè)概念,就是buffer,緩沖區(qū)。因?yàn)槲覀兪且恢痹谳斎氲?,所以這里設(shè)計(jì)成了環(huán)形隊(duì)列。
我們看一下環(huán)形隊(duì)列的數(shù)據(jù)結(jié)構(gòu)
#define bufsize 256
/* 環(huán)形隊(duì)列 */
struct ioqueue {
// 生產(chǎn)者消費(fèi)者問題
struct lock lock;
// 生產(chǎn)者,緩沖區(qū)不滿時(shí)就繼續(xù)往里面放數(shù)據(jù)
struct task_struct* producer;
// 消費(fèi)者,緩沖區(qū)不空時(shí)就繼續(xù)從往里面拿數(shù)據(jù)
struct task_struct* consumer;
char buf[bufsize]; // 緩沖區(qū)大小
int32_t head; // 隊(duì)首,數(shù)據(jù)往隊(duì)首處寫入
int32_t tail; // 隊(duì)尾,數(shù)據(jù)從隊(duì)尾處讀出
};
這個(gè)就很明朗了。一個(gè)生產(chǎn)者一個(gè)消費(fèi)者,生產(chǎn)者向buf中添加數(shù)據(jù),消費(fèi)者從buf中取出數(shù)據(jù),為了防止buf中的數(shù)據(jù)出錯(cuò),生產(chǎn)者和消費(fèi)者同時(shí)只能有一個(gè)可以訪問到buf。如果buf中數(shù)據(jù)滿了,生產(chǎn)者就不能放了,此時(shí)阻塞生產(chǎn)者,如果buf中數(shù)據(jù)為空,消費(fèi)者就不能拿了,此時(shí)阻塞消費(fèi)者。
我們看一下具體的實(shí)現(xiàn)
/* 初始化io隊(duì)列ioq */
void ioqueue_init(struct ioqueue* ioq) {
lock_init(&ioq->lock); // 初始化io隊(duì)列的鎖
ioq->producer = ioq->consumer = NULL; // 生產(chǎn)者和消費(fèi)者置空
ioq->head = ioq->tail = 0; // 隊(duì)列的首尾指針指向緩沖區(qū)數(shù)組第0個(gè)位置
}
/* 返回pos在緩沖區(qū)中的下一個(gè)位置值 */
static inline int32_t next_pos(int32_t pos) {
return (pos + 1) % bufsize;
}
/* 判斷隊(duì)列是否已滿 */
bool ioq_full(struct ioqueue* ioq) {
return next_pos(ioq->head) == ioq->tail;
}
/* 判斷隊(duì)列是否已空 */
bool ioq_empty(struct ioqueue* ioq) {
return ioq->head == ioq->tail;
}
/* 使當(dāng)前生產(chǎn)者或消費(fèi)者在此緩沖區(qū)上等待 */
static void ioq_wait(struct task_struct** waiter) {
// 二級指針不為空,指向的pcb指針地址為空
ASSERT(*waiter == NULL && waiter != NULL);
*waiter = running_thread();
thread_block(TASK_BLOCKED);
}
/* 喚醒waiter */
static void wakeup(struct task_struct** waiter) {
// 二級指針指向不為空
ASSERT(*waiter != NULL);
thread_unblock(*waiter);
*waiter = NULL;
}
/* 消費(fèi)者從ioq隊(duì)列中獲取一個(gè)字符 */
char ioq_getchar(struct ioqueue* ioq) {
// 若緩沖區(qū)(隊(duì)列)為空,把消費(fèi)者ioq->consumer記為當(dāng)前線程自己,等待生產(chǎn)者喚醒
while (ioq_empty(ioq)) {
lock_acquire(&ioq->lock);
ioq_wait(&ioq->consumer);
lock_release(&ioq->lock);
}
char byte = ioq->buf[ioq->tail]; // 從緩沖區(qū)中取出
ioq->tail = next_pos(ioq->tail); // 把讀游標(biāo)移到下一位置
if (ioq->producer != NULL) {
wakeup(&ioq->producer); // 喚醒生產(chǎn)者
}
return byte;
}
/* 生產(chǎn)者往ioq隊(duì)列中寫入一個(gè)字符byte */
void ioq_putchar(struct ioqueue* ioq, char byte) {
// 若緩沖區(qū)(隊(duì)列)已經(jīng)滿了,把生產(chǎn)者ioq->producer記為自己,等待消費(fèi)者線程喚醒自己
while (ioq_full(ioq)) {
lock_acquire(&ioq->lock);
ioq_wait(&ioq->producer);
lock_release(&ioq->lock);
}
ioq->buf[ioq->head] = byte; // 把字節(jié)放入緩沖區(qū)中
ioq->head = next_pos(ioq->head); // 把寫游標(biāo)移到下一位置
if (ioq->consumer != NULL) {
wakeup(&ioq->consumer); // 喚醒消費(fèi)者
}
}
我們看一下后面兩個(gè)函數(shù),wait
和wakeup
,這兩個(gè)函數(shù),這兩個(gè)函數(shù)傳入的是一個(gè)pcb指針的地址,所以這里是一個(gè)二級指針。所以無論是阻塞還是解除阻塞都是取這個(gè)二級指針的地址,也就得到了pcb指針。這里對于不熟悉指針的人來說可能會(huì)有點(diǎn)擾。
三、鍵盤驅(qū)動(dòng)
#define KBD_BUF_PORT 0x60 // 鍵盤buffer寄存器端口號為0x60
/* 用轉(zhuǎn)義字符定義部分控制字符 */
#define esc '\033' // 八進(jìn)制表示字符,也可以用十六進(jìn)制'\x1b'
#define backspace '\b'
#define tab '\t'
#define enter '\r'
#define delete '\177' // 八進(jìn)制表示字符,十六進(jìn)制為'\x7f'
/* 以上不可見字符一律定義為0 */
#define char_invisible 0
#define ctrl_l_char char_invisible
#define ctrl_r_char char_invisible
#define shift_l_char char_invisible
#define shift_r_char char_invisible
#define alt_l_char char_invisible
#define alt_r_char char_invisible
#define caps_lock_char char_invisible
/* 定義控制字符的通碼和斷碼 */
#define shift_l_make 0x2a
#define shift_r_make 0x36
#define alt_l_make 0x38
#define alt_r_make 0xe038
#define alt_r_break 0xe0b8
#define ctrl_l_make 0x1d
#define ctrl_r_make 0xe01d
#define ctrl_r_break 0xe09d
#define caps_lock_make 0x3a
struct ioqueue kbd_buf; // 定義鍵盤緩沖區(qū)
/* 定義以下變量記錄相應(yīng)鍵是否按下的狀態(tài),
* ext_scancode用于記錄makecode是否以0xe0開頭 */
static bool ctrl_status, shift_status, alt_status, caps_lock_status, ext_scancode;
/* 以通碼make_code為索引的二維數(shù)組 */
static char keymap[][2] = {
/* 掃描碼 未與shift組合 與shift組合*/
/* ---------------------------------- */
/* 0x00 */ {0, 0},
/* 0x01 */ {esc, esc},
/* 0x02 */ {'1', '!'},
/* 0x03 */ {'2', '@'},
/* 0x04 */ {'3', '#'},
/* 0x05 */ {'4', '$'},
/* 0x06 */ {'5', '%'},
/* 0x07 */ {'6', '^'},
/* 0x08 */ {'7', '&'},
/* 0x09 */ {'8', '*'},
/* 0x0A */ {'9', '('},
/* 0x0B */ {'0', ')'},
/* 0x0C */ {'-', '_'},
/* 0x0D */ {'=', '+'},
/* 0x0E */ {backspace, backspace},
/* 0x0F */ {tab, tab},
/* 0x10 */ {'q', 'Q'},
/* 0x11 */ {'w', 'W'},
/* 0x12 */ {'e', 'E'},
/* 0x13 */ {'r', 'R'},
/* 0x14 */ {'t', 'T'},
/* 0x15 */ {'y', 'Y'},
/* 0x16 */ {'u', 'U'},
/* 0x17 */ {'i', 'I'},
/* 0x18 */ {'o', 'O'},
/* 0x19 */ {'p', 'P'},
/* 0x1A */ {'[', '{'},
/* 0x1B */ {']', '}'},
/* 0x1C */ {enter, enter},
/* 0x1D */ {ctrl_l_char, ctrl_l_char},
/* 0x1E */ {'a', 'A'},
/* 0x1F */ {'s', 'S'},
/* 0x20 */ {'d', 'D'},
/* 0x21 */ {'f', 'F'},
/* 0x22 */ {'g', 'G'},
/* 0x23 */ {'h', 'H'},
/* 0x24 */ {'j', 'J'},
/* 0x25 */ {'k', 'K'},
/* 0x26 */ {'l', 'L'},
/* 0x27 */ {';', ':'},
/* 0x28 */ {'\'', '"'},
/* 0x29 */ {'`', '~'},
/* 0x2A */ {shift_l_char, shift_l_char},
/* 0x2B */ {'\\', '|'},
/* 0x2C */ {'z', 'Z'},
/* 0x2D */ {'x', 'X'},
/* 0x2E */ {'c', 'C'},
/* 0x2F */ {'v', 'V'},
/* 0x30 */ {'b', 'B'},
/* 0x31 */ {'n', 'N'},
/* 0x32 */ {'m', 'M'},
/* 0x33 */ {',', '<'},
/* 0x34 */ {'.', '>'},
/* 0x35 */ {'/', '?'},
/* 0x36 */ {shift_r_char, shift_r_char},
/* 0x37 */ {'*', '*'},
/* 0x38 */ {alt_l_char, alt_l_char},
/* 0x39 */ {' ', ' '},
/* 0x3A */ {caps_lock_char, caps_lock_char}
/*其它按鍵暫不處理*/
};
/* 鍵盤中斷處理程序 */
static void intr_keyboard_handler(void) {
/* 這次中斷發(fā)生前的上一次中斷,以下任意三個(gè)鍵是否有按下 */
bool ctrl_down_last = ctrl_status;
bool shift_down_last = shift_status;
bool caps_lock_last = caps_lock_status;
uint16_t scancode = inb(KBD_BUF_PORT);
// 若掃描碼是e0開頭的, 結(jié)束此次中斷處理函數(shù),等待下一個(gè)掃描碼進(jìn)來
if (scancode == 0xe0) {
ext_scancode = true; // 打開e0標(biāo)記
return;
}
// 如果上次是以0xe0開頭,將掃描碼合并
if (ext_scancode) {
scancode = ((0xe000) | scancode);
ext_scancode = false; // 關(guān)閉e0標(biāo)記
}
// 若是斷碼(按鍵彈起時(shí)產(chǎn)生的掃描碼)
if ((scancode & 0x0080) != 0) {
// 獲得相應(yīng)的通碼
uint16_t make_code = (scancode &= 0xff7f);
// 若是任意以下三個(gè)鍵彈起了,將狀態(tài)置為false
if (make_code == ctrl_l_make || make_code == ctrl_r_make) {
ctrl_status = false;
}
else if (make_code == shift_l_make || make_code == shift_r_make) {
shift_status = false;
}
else if (make_code == alt_l_make || make_code == alt_r_make) {
alt_status = false;
}
// 若是其他非控制鍵位,不需要處理,那些鍵位我們只需要知道通碼
return;
}
// 若是通碼,只處理數(shù)組中定義的鍵以及alt_right和ctrl鍵,全是make_code
else if ((scancode > 0x00 && scancode < 0x3b) || (scancode == alt_r_make) || (scancode == ctrl_r_make)) {
// keymap的二維索引
bool shift = false;
// 按下的鍵不是字母
if ((scancode < 0x0e) || (scancode == 0x29) || \
(scancode == 0x1a) || (scancode == 0x1b) || \
(scancode == 0x2b) || (scancode == 0x27) || \
(scancode == 0x28) || (scancode == 0x33) || \
(scancode == 0x34) || (scancode == 0x35)) {
if (shift_down_last) {
shift = true;
}
}
// 如果按下的鍵是字母,需要和CapsLock配合
else {
if (shift_down_last && caps_lock_last) { // 如果shift和capslock同時(shí)按下
shift = false;
}
else if (shift_down_last || caps_lock_last) { // 如果shift和capslock任意被按下
shift = true;
}
else {
shift = false;
}
}
// 將掃描碼的高字節(jié)置0,主要是針對高字節(jié)是e0的掃描碼.
uint8_t index = (scancode &= 0x00ff);
// 在數(shù)組中找到對應(yīng)的字符
char cur_char = keymap[index][shift];
// 如果cur_char不為0,也就是ascii碼為除'\0'外的字符就加入鍵盤緩沖區(qū)中
if (cur_char) {
// 如果ctrl按下,且輸入的字符為‘l’或者‘u’,那就保存為 cur_char-‘a(chǎn)’,主要是‘a(chǎn)’前面26位沒啥用
if ((ctrl_down_last && cur_char == 'l') || (ctrl_down_last && cur_char == 'u')) {
cur_char -= 'a';
}
// 如果緩沖區(qū)未滿,就將其加入緩沖區(qū)
if (!ioq_full(&kbd_buf)) {
ioq_putchar(&kbd_buf, cur_char);
}
return;
}
// 記錄本次是否按下了下面幾類控制鍵之一,供下次鍵入時(shí)判斷組合鍵
if (scancode == ctrl_l_make || scancode == ctrl_r_make) {
ctrl_status = true;
}
else if (scancode == shift_l_make || scancode == shift_r_make) {
shift_status = true;
}
else if (scancode == alt_l_make || scancode == alt_r_make) {
alt_status = true;
}
// 這里注意,大寫的鎖定鍵是取反
else if (scancode == caps_lock_make) {
caps_lock_status = !caps_lock_status;
}
}
else {
put_str("unknown key\n");
}
}
/* 鍵盤初始化 */
void keyboard_init() {
put_str("keyboard init start\n");
ioqueue_init(&kbd_buf);
register_handler(0x21, intr_keyboard_handler);
put_str("keyboard init done\n");
}
鍵盤驅(qū)動(dòng)就稍顯復(fù)雜一點(diǎn),主要是涉及到了shift
,ctrl
,alt
,caplock
這些個(gè)控制鍵,這些鍵位是否按下所表示的通碼斷碼是不一樣的。這里就是處理字符,相信大家看代碼就可以看明白。
四、仿真
我們創(chuàng)建一個(gè)線程,鍵盤輸入什么,打印什么
結(jié)束語
本節(jié)我們編寫了鍵盤驅(qū)動(dòng)以及其使用的環(huán)形隊(duì)列數(shù)據(jù)結(jié)構(gòu)。下一節(jié)我們將實(shí)現(xiàn)一個(gè)用戶進(jìn)程,即特權(quán)級為3的進(jìn)程。文章來源:http://www.zghlxwxcb.cn/news/detail-858539.html
老規(guī)矩,代碼地址為 https://github.com/lyajpunov/os文章來源地址http://www.zghlxwxcb.cn/news/detail-858539.html
到了這里,關(guān)于手寫簡易操作系統(tǒng)(十七)--編寫鍵盤驅(qū)動(dòng)的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!