讀取鍵盤輸入

  1. 首先要初始化鍵盤,設定鍵盤中斷处理程序,開鍵盤中斷等

    1
    2
    3
    4
    PUBLIC void init_keyboard(){
    put_irq_handler(KEYBOARD_IRQ, keyboard_handler);
    enable_irq(KEYBOARD_IRQ);
    }

    第一條設定中斷处理程序交由8259A芯片处理

    1
    2
    3
    4
    5
    PUBLIC void put_irq_handler(int irq, irq_handler handler)
    {
    disable_irq(irq);
    irq_table[irq] = handler;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    disable_irq:
    mov ecx, [esp + 4] ; irq
    pushf
    cli
    mov ah, 1
    rol ah, cl ; ah = (1 << (irq % 8))
    cmp cl, 8
    jae disable_8 ; disable irq >= 8 at the slave 8259
    disable_0:
    in al, INT_M_CTLMASK
    test al, ah
    jnz dis_already ; already disabled?
    or al, ah
    out INT_M_CTLMASK, al ; set bit at master 8259
    popf
    mov eax, 1 ; disabled by this function
    ret
    disable_8:
    in al, INT_S_CTLMASK
    test al, ah
    jnz dis_already ; already disabled?
    or al, ah
    out INT_S_CTLMASK, al ; set bit at slave 8259
    popf
    mov eax, 1 ; disabled by this function
    ret
    dis_already:
    popf
    xor eax, eax ; already disabled
    ret

    關中斷后,在中斷处理表對应位置放上鍵盤的处理程序
    第二條開启鍵盤中斷

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    enable_irq:
    mov ecx, [esp + 4] ; irq
    pushf
    cli
    mov ah, ~1
    rol ah, cl ; ah = ~(1 << (irq % 8))
    cmp cl, 8
    jae enable_8 ; enable irq >= 8 at the slave 8259
    enable_0:
    in al, INT_M_CTLMASK
    and al, ah
    out INT_M_CTLMASK, al ; clear bit at master 8259
    popf
    ret
    enable_8:
    in al, INT_S_CTLMASK
    and al, ah
    out INT_S_CTLMASK, al ; clear bit at slave 8259
    popf
    ret

    鍵盤處理程序讀數據

    鍵盤上有鍵盤編碼器Intel 8048,主板上有鍵盤控制器Intel 8042。敲鍵盤產生的編碼叫掃描碼,分為Make Code和Break Code兩類,当8048檢測到一個鍵的動作,會把相應的掃描碼發给8042,8042經轉換后將其放置在輸入緩冲區,然后告訴8259A說:產生鍵盤中斷啦!然后等待处理。(PS.如果又按新的鍵,緩冲区沒清空,8042是不會在接收新的掃描碼的)
    看一看8042的寄存器,因為它處於8048和系統中間,輸出和輸入是相对的,可以共用一個端口。就是說,一个in al, 0x60指令就能讀取掃描碼了。

  2. 寫一个讀取並打印的語句試試:

    1
    2
    3
    4
    PUBLIC void keyboard_handler(int irq){
    u8 scan_code = in_byte(KB_DATA);
    disp_int(scan_code); //disp是在kliba.asm里的打印方法
    }

    如果按下”a”和”9”鍵,將會出現:0x1E、0x9E、0xA、0x8A,看表可見对应的就是那兩字母的Make Code和Break Code。Break Code是Make Code與0x80進行或操作的結果。

  3. Make Code似乎找不到什么規律,干脆寫成解析數組好了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    u32 keymap[NR_SCAN_CODES * MAP_COLS] = {

    /* scan-code !Shift Shift E0 XX */
    /* ==================================================================== */
    /* 0x00 - none */ 0, 0, 0,
    /* 0x01 - ESC */ ESC, ESC, 0,
    /* 0x02 - '1' */ '1', '!', 0,
    /* 0x03 - '2' */ '2', '@', 0,
    /* 0x04 - '3' */ '3', '#', 0,
    /* 0x05 - '4' */ '4', '$', 0,
    /* 0x06 - '5' */ '5', '%', 0,
    /* 0x07 - '6' */ '6', '^', 0,
    /* 0x08 - '7' */ '7', '&', 0,
    /* 0x09 - '8' */ '8', '*', 0,
    /* 0x0A - '9' */ '9', '(', 0,
    /* 0x0B - '0' */ '0', ')', 0,
    /* 0x0C - '-' */ '-', '_', 0,
    /* 0x0D - '=' */ '=', '+', 0,
    /* 0x0E - BS */ BACKSPACE, BACKSPACE, 0,
    /* 0x0F - TAB */ TAB, TAB, 0,
    /* 0x10 - 'q' */ 'q', 'Q', 0,
    /* 0x11 - 'w' */ 'w', 'W', 0,
    /* 0x12 - 'e' */ 'e', 'E', 0,
    /* 0x13 - 'r' */ 'r', 'R', 0,
    /* 0x14 - 't' */ 't', 'T', 0,
    /* 0x15 - 'y' */ 'y', 'Y', 0,
    /* 0x16 - 'u' */ 'u', 'U', 0,
    /* 0x17 - 'i' */ 'i', 'I', 0,
    /* 0x18 - 'o' */ 'o', 'O', 0,
    /* 0x19 - 'p' */ 'p', 'P', 0,
    /* 0x1A - '[' */ '[', '{', 0,
    /* 0x1B - ']' */ ']', '}', 0,
    /* 0x1C - CR/LF */ ENTER, ENTER, PAD_ENTER,
    /* 0x1D - l. Ctrl */ CTRL_L, CTRL_L, CTRL_R,
    /* 0x1E - 'a' */ 'a', 'A', 0,
    /* 0x1F - 's' */ 's', 'S', 0,
    /* 0x20 - 'd' */ 'd', 'D', 0,
    /* 0x21 - 'f' */ 'f', 'F', 0,
    /* 0x22 - 'g' */ 'g', 'G', 0,
    /* 0x23 - 'h' */ 'h', 'H', 0,
    /* 0x24 - 'j' */ 'j', 'J', 0,
    /* 0x25 - 'k' */ 'k', 'K', 0,
    /* 0x26 - 'l' */ 'l', 'L', 0,
    /* 0x27 - ';' */ ';', ':', 0,
    /* 0x28 - '\'' */ '\'', '"', 0,
    /* 0x29 - '`' */ '`', '~', 0,
    /* 0x2A - l. SHIFT */ SHIFT_L, SHIFT_L, 0,
    /* 0x2B - '\' */ '\\', '|', 0,
    /* 0x2C - 'z' */ 'z', 'Z', 0,
    /* 0x2D - 'x' */ 'x', 'X', 0,
    /* 0x2E - 'c' */ 'c', 'C', 0,
    /* 0x2F - 'v' */ 'v', 'V', 0,
    /* 0x30 - 'b' */ 'b', 'B', 0,
    /* 0x31 - 'n' */ 'n', 'N', 0,
    /* 0x32 - 'm' */ 'm', 'M', 0,
    /* 0x33 - ',' */ ',', '<', 0,
    /* 0x34 - '.' */ '.', '>', 0,
    /* 0x35 - '/' */ '/', '?', PAD_SLASH,
    /* 0x36 - r. SHIFT */ SHIFT_R, SHIFT_R, 0,
    /* 0x37 - '*' */ '*', '*', 0,
    /* 0x38 - ALT */ ALT_L, ALT_L, ALT_R,
    /* 0x39 - ' ' */ ' ', ' ', 0,
    /* 0x3A - CapsLock */ CAPS_LOCK, CAPS_LOCK, 0,
    /* 0x3B - F1 */ F1, F1, 0,
    /* 0x3C - F2 */ F2, F2, 0,
    /* 0x3D - F3 */ F3, F3, 0,
    /* 0x3E - F4 */ F4, F4, 0,
    /* 0x3F - F5 */ F5, F5, 0,
    /* 0x40 - F6 */ F6, F6, 0,
    /* 0x41 - F7 */ F7, F7, 0,
    /* 0x42 - F8 */ F8, F8, 0,
    /* 0x43 - F9 */ F9, F9, 0,
    /* 0x44 - F10 */ F10, F10, 0,
    /* 0x45 - NumLock */ NUM_LOCK, NUM_LOCK, 0,
    /* 0x46 - ScrLock */ SCROLL_LOCK, SCROLL_LOCK, 0,
    /* 0x47 - Home */ PAD_HOME, '7', HOME,
    /* 0x48 - CurUp */ PAD_UP, '8', UP,
    /* 0x49 - PgUp */ PAD_PAGEUP, '9', PAGEUP,
    /* 0x4A - '-' */ PAD_MINUS, '-', 0,
    /* 0x4B - Left */ PAD_LEFT, '4', LEFT,
    /* 0x4C - MID */ PAD_MID, '5', 0,
    /* 0x4D - Right */ PAD_RIGHT, '6', RIGHT,
    /* 0x4E - '+' */ PAD_PLUS, '+', 0,
    /* 0x4F - End */ PAD_END, '1', END,
    /* 0x50 - Down */ PAD_DOWN, '2', DOWN,
    /* 0x51 - PgDown */ PAD_PAGEDOWN, '3', PAGEDOWN,
    /* 0x52 - Insert */ PAD_INS, '0', INSERT,
    /* 0x53 - Delete */ PAD_DOT, '.', DELETE,
    /* 0x54 - Enter */ 0, 0, 0,
    /* 0x55 - ??? */ 0, 0, 0,
    /* 0x56 - ??? */ 0, 0, 0,
    /* 0x57 - F11 */ F11, F11, 0,
    /* 0x58 - F12 */ F12, F12, 0,
    /* 0x59 - ??? */ 0, 0, 0,
    /* 0x5A - ??? */ 0, 0, 0,
    /* 0x5B - ??? */ 0, 0, GUI_L,
    /* 0x5C - ??? */ 0, 0, GUI_R,
    /* 0x5D - ??? */ 0, 0, APPS,
    /* 0x5E - ??? */ 0, 0, 0,
    /* 0x5F - ??? */ 0, 0, 0,
    /* 0x60 - ??? */ 0, 0, 0,
    /* 0x61 - ??? */ 0, 0, 0,
    /* 0x62 - ??? */ 0, 0, 0,
    /* 0x63 - ??? */ 0, 0, 0,
    /* 0x64 - ??? */ 0, 0, 0,
    /* 0x65 - ??? */ 0, 0, 0,
    /* 0x66 - ??? */ 0, 0, 0,
    /* 0x67 - ??? */ 0, 0, 0,
    /* 0x68 - ??? */ 0, 0, 0,
    /* 0x69 - ??? */ 0, 0, 0,
    /* 0x6A - ??? */ 0, 0, 0,
    /* 0x6B - ??? */ 0, 0, 0,
    /* 0x6C - ??? */ 0, 0, 0,
    /* 0x6D - ??? */ 0, 0, 0,
    /* 0x6E - ??? */ 0, 0, 0,
    /* 0x6F - ??? */ 0, 0, 0,
    /* 0x70 - ??? */ 0, 0, 0,
    /* 0x71 - ??? */ 0, 0, 0,
    /* 0x72 - ??? */ 0, 0, 0,
    /* 0x73 - ??? */ 0, 0, 0,
    /* 0x74 - ??? */ 0, 0, 0,
    /* 0x75 - ??? */ 0, 0, 0,
    /* 0x76 - ??? */ 0, 0, 0,
    /* 0x77 - ??? */ 0, 0, 0,
    /* 0x78 - ??? */ 0, 0, 0,
    /* 0x78 - ??? */ 0, 0, 0,
    /* 0x7A - ??? */ 0, 0, 0,
    /* 0x7B - ??? */ 0, 0, 0,
    /* 0x7C - ??? */ 0, 0, 0,
    /* 0x7D - ??? */ 0, 0, 0,
    /* 0x7E - ??? */ 0, 0, 0,
    /* 0x7F - ??? */ 0, 0, 0
    };

    MAP_COLS為三,代表按鍵組合的三種情況,如果獲得掃描碼0x1F,就应該用keymap[0x1FMAP_COLS]來表示,即s。如果獲得的掃描碼是0x2A0x1E,說明左shift和a鍵是一起按的,因此应該用keymap[0x1EMAP_COLS+1]來表示这個結果,即A。
    然而,輸入緩冲区大小只有一個字節,按下Shift+A,实際就產生了四次中斷接收,这给程序實現帶來了困難,無法準備知道用戶做了什么,于是当接收到类似的值可以專門建個任務處理他們。

  4. 建立鍵盤輸入緩冲區,存掃描碼,如果到达末尾則把指針移到開頭:

    1
    2
    3
    4
    5
    6
    typedef struct s_kb {
    char* p_head; /* 指向缓冲区中下一个空闲位置 */
    char* p_tail; /* 指向键盘任务应处理的字节 */
    int count; /* 缓冲区中共有多少字节 */
    char buf[KB_IN_BYTES]; /* 缓冲区 */
    }KB_INPUT;

    修改处理程序,掃描碼放入緩冲区:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    PUBLIC void keyboard_handler(int irq)
    {
    u8 scan_code = in_byte(KB_DATA);

    if (kb_in.count < KB_IN_BYTES) {
    *(kb_in.p_head) = scan_code;
    kb_in.p_head++;
    if (kb_in.p_head == kb_in.buf + KB_IN_BYTES) {//看到尾沒
    kb_in.p_head = kb_in.buf;
    }
    kb_in.count++;
    }
    }

    修改init_keyboard,使中斷發生時初始化緩冲区狀態、按鍵狀態:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    PUBLIC void init_keyboard()
    {
    kb_in.count = 0;
    kb_in.p_head = kb_in.p_tail = kb_in.buf;

    shift_l = shift_r = 0;
    alt_l = alt_r = 0;
    ctrl_l = ctrl_r = 0;

    caps_lock = 0;
    num_lock = 1;
    scroll_lock = 0;

    set_leds();

    put_irq_handler(KEYBOARD_IRQ, keyboard_handler);/*设定键盘中断处理程序*/
    enable_irq(KEYBOARD_IRQ); /*开键盘中断*/
    }
  5. 添加终端處理任務,新建文件tty.c,開始只是不停調用keyboard.c中的keyboard_red()讀鍵盤碼。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    PUBLIC void keyboard_read(TTY* p_tty)
    {
    u8 scan_code;
    char output[2];
    int make; /* 1: make; 0: break. */

    u32 key = 0;/* 用一个整型来表示一个键。比如,如果 Home 被按下,
    * 则 key 值将为定义在 keyboard.h 中的 'HOME'。
    */
    u32* keyrow; /* 指向 keymap[] 的某一行 */

    if(kb_in.count > 0){
    code_with_E0 = 0;

    scan_code = get_byte_from_kbuf();

    /* 下面开始解析扫描码 */
    if (scan_code == 0xE1) {
    int i;
    u8 pausebrk_scode[] = {0xE1, 0x1D, 0x45,
    0xE1, 0x9D, 0xC5};
    int is_pausebreak = 1;
    for(i=1;i<6;i++){
    if (get_byte_from_kbuf() != pausebrk_scode[i]) {
    is_pausebreak = 0;
    break;
    }
    }
    if (is_pausebreak) {
    key = PAUSEBREAK;
    }
    }
    else if (scan_code == 0xE0) {
    scan_code = get_byte_from_kbuf();

    /* PrintScreen 被按下 */
    if (scan_code == 0x2A) {
    if (get_byte_from_kbuf() == 0xE0) {
    if (get_byte_from_kbuf() == 0x37) {
    key = PRINTSCREEN;
    make = 1;
    }
    }
    }
    /* PrintScreen 被释放 */
    if (scan_code == 0xB7) {
    if (get_byte_from_kbuf() == 0xE0) {
    if (get_byte_from_kbuf() == 0xAA) {
    key = PRINTSCREEN;
    make = 0;
    }
    }
    }
    /* 不是PrintScreen, 此时scan_code为0xE0紧跟的那个值. */
    if (key == 0) {
    code_with_E0 = 1;
    }
    }
    if ((key != PAUSEBREAK) && (key != PRINTSCREEN)) {
    /* 首先判断Make Code 还是 Break Code */
    make = (scan_code & FLAG_BREAK ? 0 : 1);

    /* 先定位到 keymap 中的行 */
    keyrow = &keymap[(scan_code & 0x7F) * MAP_COLS];

    column = 0;

    int caps = shift_l || shift_r;
    if (caps_lock) {
    if ((keyrow[0] >= 'a') && (keyrow[0] <= 'z')){
    caps = !caps;
    }
    }
    if (caps) {
    column = 1;
    }

    if (code_with_E0) {
    column = 2;
    }

    key = keyrow[column];

    switch(key) {
    case SHIFT_L:
    shift_l = make;
    break;
    case SHIFT_R:
    shift_r = make;
    break;
    case CTRL_L:
    ctrl_l = make;
    break;
    case CTRL_R:
    ctrl_r = make;
    break;
    case ALT_L:
    alt_l = make;
    break;
    case ALT_R:
    alt_l = make;
    break;
    case CAPS_LOCK:
    if (make) {
    caps_lock = !caps_lock;
    set_leds();
    }
    break;
    case NUM_LOCK:
    if (make) {
    num_lock = !num_lock;
    set_leds();
    }
    break;
    case SCROLL_LOCK:
    if (make) {
    scroll_lock = !scroll_lock;
    set_leds();
    }
    break;
    default:
    break;
    }

    if (make) { /* 忽略 Break Code */
    int pad = 0;

    /* 首先处理小键盘 */
    if ((key >= PAD_SLASH) && (key <= PAD_9)) {
    pad = 1;
    switch(key) {
    case PAD_SLASH:
    key = '/';
    break;
    case PAD_STAR:
    key = '*';
    break;
    case PAD_MINUS:
    key = '-';
    break;
    case PAD_PLUS:
    key = '+';
    break;
    case PAD_ENTER:
    key = ENTER;
    break;
    default:
    if (num_lock &&
    (key >= PAD_0) &&
    (key <= PAD_9)) {
    key = key - PAD_0 + '0';
    }
    else if (num_lock &&
    (key == PAD_DOT)) {
    key = '.';
    }
    else{
    switch(key) {
    case PAD_HOME:
    key = HOME;
    break;
    case PAD_END:
    key = END;
    break;
    case PAD_PAGEUP:
    key = PAGEUP;
    break;
    case PAD_PAGEDOWN:
    key = PAGEDOWN;
    break;
    case PAD_INS:
    key = INSERT;
    break;
    case PAD_UP:
    key = UP;
    break;
    case PAD_DOWN:
    key = DOWN;
    break;
    case PAD_LEFT:
    key = LEFT;
    break;
    case PAD_RIGHT:
    key = RIGHT;
    break;
    case PAD_DOT:
    key = DELETE;
    break;
    default:
    break;
    }
    }
    break;
    }
    }

    key |= shift_l ? FLAG_SHIFT_L : 0;
    key |= shift_r ? FLAG_SHIFT_R : 0;
    key |= ctrl_l ? FLAG_CTRL_L : 0;
    key |= ctrl_r ? FLAG_CTRL_R : 0;
    key |= alt_l ? FLAG_ALT_L : 0;
    key |= alt_r ? FLAG_ALT_R : 0;
    key |= pad ? FLAG_PAD : 0;

    in_process(p_tty, key);
    }
    }
    }
    }
    PRIVATE u8 get_byte_from_kbuf() /* 从键盘缓冲区中读取下一个字节 */
    {
    u8 scan_code;

    while (kb_in.count <= 0) {} /* 等待下一个字节到来 */

    disable_int();
    scan_code = *(kb_in.p_tail);
    kb_in.p_tail++;
    if (kb_in.p_tail == kb_in.buf + KB_IN_BYTES) {
    kb_in.p_tail = kb_in.buf;
    }
    kb_in.count--;
    enable_int();

    return scan_code;
    }

    函數首先判斷kb_in.count是否為0,不為零就開始讀,讀時關中斷,讀完才打開,然后解析掃描碼,要判斷的東西很多,最后要能輸出所有可輸出字符,按shift能輸出大寫。

    顯示器輸出

    之前一直都是調用系統的中斷來輸出字符,現在要自食其力了。
    我們來討論8025文本模式,在此模式下,顥存大小為32KB,佔用范圍為0xB8000~0xBFFFF。每兩個字符代表一個字符,低字節表示ASCII碼,高字節表示字符属性(顏色,分前景和背景兩部分)

    所以一個屏幕映射到顯存中所佔的空間為80
    25*2=4000Byte,32MB顯存可以放下8個屏幕。通过端口操作可以对顯示器寄存器進行讀寫操作。

  6. 新建console.c以用來寫顯示相關程序,首圥初始化一個屏幕,設置光標等信息:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    PUBLIC void init_screen(TTY* p_tty)
    {
    int nr_tty = p_tty - tty_table;
    p_tty->p_console = console_table + nr_tty;

    int v_mem_size = V_MEM_SIZE >> 1; /* 显存总大小 (in WORD) */

    int con_v_mem_size = v_mem_size / NR_CONSOLES;
    p_tty->p_console->original_addr = nr_tty * con_v_mem_size;
    p_tty->p_console->v_mem_limit = con_v_mem_size;
    p_tty->p_console->current_start_addr = p_tty->p_console->original_addr;

    /* 默认光标位置在最开始处 */
    p_tty->p_console->cursor = p_tty->p_console->original_addr;

    if (nr_tty == 0) {
    /* 第一个控制台沿用原来的光标位置 */
    p_tty->p_console->cursor = disp_pos / 2;
    disp_pos = 0;
    }
    else {
    out_char(p_tty->p_console, nr_tty + '0');
    out_char(p_tty->p_console, '#');
    }

    set_cursor(p_tty->p_console->cursor);
    }
  7. 我們可以通过設置Start Address High Register和Start Address Low Register來重新設置顯示開始地址,從而實現滾屏功能。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    PUBLIC void in_process(TTY* p_tty, u32 key)
    {
    char output[2] = {'\0', '\0'};

    if (!(key & FLAG_EXT)) {
    put_key(p_tty, key);
    }
    else {
    int raw_code = key & MASK_RAW;
    switch(raw_code) {
    case ENTER:
    put_key(p_tty, '\n');
    break;
    case BACKSPACE:
    put_key(p_tty, '\b');
    break;
    case UP:
    if ((key & FLAG_SHIFT_L) || (key & FLAG_SHIFT_R)) {
    scroll_screen(p_tty->p_console, SCR_DN);
    }
    break;
    case DOWN:
    if ((key & FLAG_SHIFT_L) || (key & FLAG_SHIFT_R)) {
    scroll_screen(p_tty->p_console, SCR_UP);
    }
    break;
    case F1:
    case F2:
    case F3:
    case F4:
    case F5:
    case F6:
    case F7:
    case F8:
    case F9:
    case F10:
    case F11:
    case F12:
    /* Alt + F1~F12 */
    if ((key & FLAG_ALT_L) || (key & FLAG_ALT_R)) {
    select_console(raw_code - F1);
    }
    break;
    default:
    break;
    }
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    PUBLIC void scroll_screen(CONSOLE* p_con, int direction)
    {
    if (direction == SCR_UP) {
    if (p_con->current_start_addr > p_con->original_addr) {
    p_con->current_start_addr -= SCREEN_WIDTH;
    }
    }
    else if (direction == SCR_DN) {
    if (p_con->current_start_addr + SCREEN_SIZE <
    p_con->original_addr + p_con->v_mem_limit) {
    p_con->current_start_addr += SCREEN_WIDTH;
    }
    }
    else{
    }

    set_video_start_addr(p_con->current_start_addr);
    set_cursor(p_con->cursor);
    }

    現在可以通過Shift+上下箭頭實現滾屏了

    單個終端的輸出實現後,多個終端不過就是多了一個選擇的步驟而已,只有当某個TTY对應的控制台是當前控制台,它才可以讀取鍵盤緩冲區。

    完善鍵盤功能

    1 回車,退格等鍵的处理

    首先在tty.c终端in_process里識別到对应的按鍵,用0x01FF和特殊按鍵key相與得出raw_code,然后控制台作对应的處理。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    switch(raw_code) {
    case ENTER:
    if(mode == 0){
    put_key(p_tty, '\n');
    }else{
    mode = 2;
    }

    break;
    case BACKSPACE:
    put_key(p_tty, '\b');
    break;
    case UP:
    if ((key & FLAG_SHIFT_L) || (key & FLAG_SHIFT_R)) {
    scroll_screen(p_tty->p_console, SCR_DN);}
    break;
    case DOWN:
    if ((key & FLAG_SHIFT_L) || (key & FLAG_SHIFT_R)) {
    scroll_screen(p_tty->p_console, SCR_UP);
    }
    break;
    case TAB:
    put_key(p_tty,'\t');
    break;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    u8* p_vmem = (u8*)(V_MEM_BASE + p_con->cursor * 2);

    switch(ch) {
    case '\n':
    if (p_con->cursor < p_con->original_addr +
    p_con->v_mem_limit - SCREEN_WIDTH) {
    push_STA(p_con,p_con->cursor);//輸出的話,先存儲光標位置
    p_con->cursor = p_con->original_addr + SCREEN_WIDTH *
    ((p_con->cursor - p_con->original_addr) /
    SCREEN_WIDTH + 1);
    }
    break;
    //刪除的話pop出之前光標位置,將現在的光標和之前光標的位置間的內容清空
    case '\b':
    if (p_con->cursor > p_con->original_addr) {
    int tmp =p_con->cursor;

    p_con->cursor = pop_STA(p_con);
    for(int i = 0; i < tmp - p_con->cursor; i++) {
    *(p_vmem - 2 - 2 * i) = ' ';
    *(p_vmem - 1 - 2 * i) = DEFAULT_CHAR_COLOR;
    }
    }
    break;
    //tab:要判斷是否會越界,把位置進棧,移動光標
    case '\t':
    if(p_con->cursor < p_con->original_addr + p_con->v_mem_limit-TAB_WIDTH){
    push_STA(p_con,p_con->cursor);
    p_con->cursor += TAB_WIDTH;
    }
    break;
    default:
    if (p_con->cursor <
    p_con->original_addr + p_con->v_mem_limit - 1) {
    //普通輸入
    push_STA(p_con, p_con->cursor);
    *p_vmem++ = ch;
    if(mode == 0){
    if(*p_vmem == ' '){
    *p_vmem++ = DEFAULT_CHAR_COLOR;
    }else{
    *p_vmem++ = DEFAULT_CHAR_COLOR;
    }
    }else{
    if(*p_vmem == ' '){
    *p_vmem++ = DEFAULT_CHAR_COLOR;
    }else{
    *p_vmem++ = RED_CHAR_COLOR;
    }
    }
    p_con->cursor++;
    }
    break;
    }

    首先顯存定位到當前控制台顯存基地址+(光標位置*2 Byte)的地方,

  • 如果不是特殊字符就正常輸出,光標位置沒有超出最大顯存位置的話,就先把光標位置入棧,在當前顯存位置輸出該字符,如果是普通模式,下一字節顏色控制就設為默認,否則設為紅色。
  • 如果是’\n’就是換行,光標位置就要少于最大位置減去屏幕寬度才行,一样先存儲位置,再把光標移到下一行開頭
  • 如果是’\t’,如果沒越界,就位置入棧,光標往后移四格
  • 如果是’\b’退格,前一個是普通輸入的話,回到前一格就好,但前一個是換行的話就要回到上一行的位置,是TAB也要刪掉連續的四格,所以入棧的光標位置就派上了用場。如果光標位置大于開始的位置,就先保存当前位置,把光標位置設為出棧的那個位置,然后把兩個位置中間的數據全設為空和默認顏色

    2 隔一段時間清空屏幕

  1. 可以用時鐘中斷來做,但事先要增加一個循环清空屏幕的任務。在proc.h里新增並定義这個任務:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /* Number of tasks & procs */
    #define NR_TASKS 2 //增加一個任務
    #define NR_PROCS 3

    /* stacks of tasks */
    #define STACK_SIZE_TTY 0x8000
    #define STACK_SIZE_FRESHER 0x8000 //任務名和栈大小
    #define STACK_SIZE_TESTA 0x8000
    #define STACK_SIZE_TESTB 0x8000
    #define STACK_SIZE_TESTC 0x8000

    #define STACK_SIZE_TOTAL (STACK_SIZE_TTY + \
    STACK_SIZE_FRESHER + \ //加入棧
    STACK_SIZE_TESTA + \
    STACK_SIZE_TESTB + \
    STACK_SIZE_TESTC)

    在global.c里把任務加進任務列表

  2. tty.c里寫这個任務,當開始時就設一個20秒的時鐘,發生中斷后就刷新所有控制台。井且把鼠標定位到開頭。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //初始代所有屏幕
    PUBLIC void taskFresher(){
    while(1){
    milli_delay(200000);
    TTY* p_tty; //tty指針;
    for(p_tty = TTY_FIRST; p_tty < TTY_END; p_tty++){
    refresh(p_tty->p_console);
    }
    }

    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    PUBLIC void refresh(CONSOLE* p_con){
    u8* p_vmem = (u8*)(V_MEM_BASE);
    for(int i = p_con->original_addr;i<p_con->cursor;i++){
    *p_vmem++= ' ';
    *p_vmem++ = DEFAULT_CHAR_COLOR;
    }
    p_con->cursor = p_con->current_start_addr = p_con->original_addr;
    flush(p_con);
    }

    同時也可以改一下原本的初始化控制台的函數,使其初始化最后再清空一下屏幕

  3. 最后在global.c里的任務表里添加上这個任務,然后每二十秒就會自動清空屏幕辣!

    1
    2
    3
    4
    PUBLIC TASK task_table[NR_TASKS] = {
    {task_tty, STACK_SIZE_TTY, "tty"},
    {taskFresher, STACK_SIZE_FRESHER, "fresher"}
    };

    3 搜索模式

    目標是按下ESC進入搜索模式,停止清屏,輸入红色的關鍵字,按下Enter進行搜索,封鎖除了Enter外的鍵,搜到的字全標红,再按一下ESC退去搜索模式並清空關鍵字和標红。

    1 新增模式

    在global.c里寫個变量PUBLIC int mode;用來表示当前模式。
    在console.h里新增变量記錄按下esc時光標的位置,方便退出時回去。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    typedef struct s_console
    {
    unsigned int current_start_addr; /* 当前显示到了什么位置 */
    unsigned int original_addr; /* 当前控制台对应显存位置 */
    unsigned int v_mem_limit; /* 当前控制台占的显存大小 */
    unsigned int cursor; /* 当前光标位置 */
    unsigned int start_search_cursor; //按下esc時光標的位置
    STA pos_stk; //棧
    }CONSOLE;

    2 模式切換

    在tty.c判斷特殊按鍵里加上ESC

    1
    2
    3
    4
    5
    6
    7
    8
    case ESC:
    if(mode == 0){
    mode = 1;
    p_tty->p_console->start_search_cursor = p_tty->p_console->cursor;
    p_tty->p_console->pos_stk.start_search_cursor = p_tty->p_console->pos_stk.ptr;
    }else{
    mode = 0;
    }

    開头也添上當mode=2就只有ESC退出可用,ENTER鍵增加當模式非0進行搜索的功能

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    if (mode == 2){
    if((key & MASK_RAW) == ESC){
    mode = 0;//查找模式按回車后只有ESC可用,按下后恢复正常模式
    exitSearch(p_tty->p_console);
    }
    return;
    }
    ..................................
    case ENTER:
    if(mode == 0){
    put_key(p_tty, '\n');
    }else{
    searchWord(p_tty->p_console);
    mode = 2;
    }

    計時刷新屏幕那里也加上一個判斷mode=0才刷新的條件,console.c里輸出字符時若mode為2則輸出红色

    3 搜索

    獲取搜索關鍵字的長度,遍历控制台每個字符,給當前字符指定開始和結束指針,每匹配上一個字符,結束指針就往后移,全部匹配就代表搜索到了,標红。不匹配就結束,從下一個字符繼續匹配,直到找到所有匹配的字符串并標红

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    PUBLIC void searchWord(CONSOLE *p_con){
    int wordLen = p_con->cursor * 2 - p_con->start_search_cursor *2;
    for( int i = 0; i < p_con->start_search_cursor * 2; i += 2) { //从头到尾一個個字找
    int begin = i;
    int end = i;
    int found = 1;

    for(int j = 0; j< wordLen ; j += 2){
    char c = *((u8 *) (V_MEM_BASE + end));
    int place = j + p_con->start_search_cursor * 2;
    char target = *((u8 *)(V_MEM_BASE + place));
    if( c == target){
    end += 2;
    }else {
    found = 0;
    break;
    }
    }

    if(found == 1){
    for(int m = begin; m < end; m += 2){
    *(u8 *) (V_MEM_BASE + m + 1) = RED_CHAR_COLOR;
    }
    }
    }
    }

    退出搜索模式則是把搜索關鍵詞清空,標红恢复,並把光標移動到搜索前的位置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    PUBLIC void exitSearch(CONSOLE *p_con){
    u8 * p_vmem = (u8 *)(V_MEM_BASE + p_con->cursor * 2);
    int len = p_con->cursor - p_con->start_search_cursor;
    for (int i = 0; i < len ; i ++){
    *(p_vmem - 2 - 2 * i) = ' ';
    *(p_vmem - 1 - 2 * i) = DEFAULT_CHAR_COLOR;
    }

    for( int i = 0; i < p_con->start_search_cursor* 2; i += 2){
    *(u8 *)(V_MEM_BASE + i + 1) = DEFAULT_CHAR_COLOR;
    }

    p_con->cursor = p_con->start_search_cursor;
    p_con->pos_stk.ptr = p_con->pos_stk.start_search_cursor;
    flush(p_con);
    }

    4 ctrl+z撤銷

    咋一想这個功能似乎很麻煩,要記錄下每一次的輸入操作,並且一步步地反向操作回去,工作量好像很大。但其實我們每一步操作其實都是個字符,完全可以建一個操作的棧,但是每次操作時錄入的是該操作对应的逆操作,然后每次按ctrl+z就出桟一個字符並當成正能輸入out_put出去,做相应的处理。

    1 識別ctrl+z操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
         if (!(key & FLAG_EXT)) {
    if((key & FLAG_CTRL_L) ||(key & FLAG_CTRL_R)){
    output[0] = key;
    if (output[0] == 'z' || output[0] == 'Z'){
    rollback(p_tty->p_console);
    }
    }else{
    put_key(p_tty, key);
    }
    }else{

    2 建立操作棧,此操作不是撤銷操作的話才入棧

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    PRIVATE int optionsTop;
    PRIVATE char options[SCREEN_SIZE];
    ...............................................
    if(!p_con->rolling){
    options[++optionsTop] = '\b';//換行的話,对应逆操作是刪除
    }
    ................................................
    char c; //被退格的字符
    if(p_con->cursor - tmp == 4){
    c = '\t';
    }else if(p_con->cursor/SCREEN_WIDTH - tmp /SCREEN_WIDTH == 1){
    c = '\n';
    }else{
    c = *(p_vmem-2);
    }
    if(!p_con->rolling){
    options[++optionsTop] = c; //退格的話,如果前一個鼠標位置和当前位
    //置差四的話被退格就是"\t",差一行就是"\n",其余的就是顯存位置的該字符
    }
    ................................................
    if(!p_con->rolling){
    options[++optionsTop] = '\b'; //"\t"和普通字符的逆操作都是"\b"
    }

    3 撤銷操作

    1
    2
    3
    4
    5
    6
    7
    PUBLIC void rollback(CONSOLE* p_con) {
    p_con->rolling = 1;
    if(optionsTop >= 0){
    out_char(p_con, options[optionsTop--]);
    }
    p_con->rolling = 0;
    }

    5 一些問題

    1 解釋中斷向量

    中斷向量就是中斷服務程序的首地址,包含偏移地址IP和段地址CS,占四個字節。

    2 解释中断类型码

    中断类型码为8位二进制数。是中斷服務程序的編號,所有的中断服务程序入口地址放在中断向量表中,由中断类型码×4,就是該程序入口地址在中断向量表的位置。

    3 解釋中斷向量表

    存放所有中斷向量的地址空間(8086是在低1KB的空間),

    4 實模式下中斷程序地址如何得到?

    根據中斷類型碼n,從中斷向量表中取得中斷程序地址,段地址存入CS,偏移量存入IP,使CPU轉入中斷處理程序運行。

    5 保護模式下中斷程序地址如何得到?

    以中断描述符表寄存器(IDTR)指定中斷描述符表的基地址為起始地址,用調用號N*8算出偏移量,即為N號中斷門描述符的首地址,根據中斷門中的選擇子和偏移量得到中斷處理程序入口。

    6 中斷向量的地址如何得到?

    調用號N*4。

    7 實模式下如何根據中斷向量的地址得到中斷程序地址?

    段地址存入CS,偏移量存入IP。

    8 解釋中斷描述符

    IDT,除了含有中斷处理程序地址信息外,還包括許多属性和類型位,每個中斷描述符占用連續的8個字節。

    9 保護模式下中斷描述符表如何得到?

    由IDTR指定中斷描述符表的基地址

    10 保護模式下中斷門如何得到?

    由IDTR指定中斷描述符表的基地址,用調用號N*8算出偏移量,即為N號中斷門的首地址。

    11 保護模式下如何根據中斷門得到中斷处理程序地址?

    从中斷門中獲得中斷处理程序代碼段的选择子和偏移量,

  4. 如果是GDT方式:

  • 则从GDTR获取GDT首地址,用段选择子中的13位做偏移,拿到

GDT中的段描述符

  • 用描述符中的段首地址加上偏移量找到中斷處理程序入口的物理地址。寻址结束。
  1. 如果是LDT方式:
  • 从GDTR获取GDT首地址,用LDTR中的偏移量做偏移,拿到GDT中的描述符1
  • 从描述符1中获取LDT首地址,用中斷門中段选择子中的13位做偏移,拿到LDT中的描述符2
  • 用描述符2中的段首地址加上中斷門中的偏移量找到中斷處理程序入口,寻址结束。

    12 中斷的分类,舉例不同類型的中斷?

    从中斷可分為內部中斷和外部中斷,內部中斷的类型有軟中斷(程序中執行的中斷命令,如int)和異常中斷(計算机硬件異常或故障引起),外部中斷有I/O中斷(外部設備請求引起)和人工干预的中斷(手動結束進程),分為可屏蔽和不可屏蔽中斷(時鐘中斷不可屏蔽。

    13 中斷與異常的区別?

    中斷指由CPU以外的事件引起的中斷(如I/O、時鐘、控制台中斷),異常指來自CPU的內部事件或程序執行中的事件引起的過程(如CPU本身故障、程序故障和請求系統服務的指令引起的中斷),中斷是異步的,異常是同步的。

    14 實模式和保護模式下的中斷處理差別

    最大的區別在於尋找中斷處理程序入口的方式。

  • 實模式下,中斷处理程序的入口地址稱為”中斷向量”,所有的”中斷向量”存儲在一個”中斷向量表中”

  • 保護模式下,為每個中斷和異常定義了一個中斷描述符,來說明中斷和異常服務程序的入口地址的属性,由中斷描述符表取代實地址模式下的中斷向量表

    15 如何識別鍵血組合鍵(如Shift+a)是否还有其他解決方案?

    在orange’s中,是建立了掃描碼的解析數組記錄一個鍵在組合及非組合狀態下的實際值,並設置緩冲區,聲明了組合鍵中前者(crtl,shift等)的相应變量,若出現其make code或break code,設置其標誌位以便與其他非組合鍵組合在解析數組中找到相應實際值。

    16 IDT是什么,有什么作用?

    IDT(Interrupt Descriptor Table),中斷描述符表,作用是將每一個中斷向量和一個描述符對應起來。

    17 IDT中有哪几種描述符?

    中斷門描述符、陷阱門描述符、任務門描述符。

    18 異常的分類?

  • Fault(缺陷): 是一種可被更正的異常,而且一旦更正了,程序可以不失連續性地繼續執行。返回地址是產生fault的指令。

  • Trap(陷入): 一種在發生trap的指令執行之後立即被報告的異常,它也允許程序或任務不失連續性地繼續執行,返回地址是產生trap指令之後的那條指令。
  • Abort(中止): 不總是報告精确異常發生位置的異常,不允許程序或任務繼續執行,是用來報告嚴重錯誤的。

    19 用戶態和內核態的特权級分別是多少?

    用戶態: 3級。
    內核態: 0級。

    20 中斷向量表中,每個中斷有几個字節?里面的結構是什么?

    4個Bytes,低地址兩個Byte放偏移,高地址兩個Byte放段描述符。

    21 中斷異常共同點和不同點?

    相同點:

  • 都是程序執行過程中的強制性轉移,轉移到相應的處理程序。

  • 都是軟件或者硬件發生了某種情形而通知處理器的行為。

不同點:

  • 中斷,是CPU所具備的功能。通常因為”硬件而隨機發生。異常,是”軟件”運行過程中的一種開發過程中沒有考慮到的程序錯誤。
  • 中斷是CPU暫停當前工作,有計劃地去處理其他的事情。中斷的發生一航是可以預知的,處理的過程也是事圥制定好的。處理中斷時程序是正常運行的。異常是CPU遇到了無法響應的工作,而後進入一種非正常狀態。異常的出現表明程序有缺陷。
  • 中斷是異步的,異常是同步的。
  • 良性的中斷和trap,只是在正常的工作流之外執行額外的操作,然後繼續沒完成的任務,因此處理程序完了后會返回到原指令流的下一條指令繼續執行。而惡性的fault和abort異常,修复可修复的fault后,會重新執行該指令,而不可修复的fault和abort則不會再返回。
  • 中斷是由於由前程序無關的中斷信號觸發的,CPU對中斷的響應是被動的,且與CPU模式無關。異常是由CPU控制單元產生的,大部分發生在用戶態。