一个操作系统的实现(6)键盘及控制台

tech2022-08-06  147

主要内容

实现简单的I/O 实现多控制台

1.添加一个简单的键盘中断处理程序

PUBLIC void keyboard_handler(int irq) { disp_str("*"); }

2.打开键盘中断 8259A的IRQ1对应的就是键盘

PUBLIC void init_keyboard() { put_irq_handler(KEYBOARD_IRQ, keyboard_handler);/*设定键盘中断处理程序*/ enable_irq(KEYBOARD_IRQ); /*开键盘中断*/ }

3.运行后发现只能输入一次,下面解决只能单次输入问题

键盘的敲击 键盘中存在一枚叫做键盘编码器的芯片,作为键盘输入的监视,并把适当的数据传送到计算机中。计算机主板上还有一个键盘控制器,用来接受和解码来自键盘的数据,并于8259A进行通信。 键盘的敲击可细分为两方面:动作和内容,动作可以分解为按下,保持,放开,内容则是键盘对应的数值。 敲击键盘产生的编码被称为扫描码,分为Make Code和Break Code,当一个键被按下会产生Make Code,弹起时产生Break Code。 当键盘编码器检测到一个键的动作后会把相应的扫描码发送给键盘控制器,然后控制器把他转化为对套的扫描码(Scan code set 1),放在输入缓冲区中并通知8259A产生中断。(在缓冲区被清空前,不会收到其他扫描码)

1.所以我们要先读取扫描码才能继续输入

对于输入和输出缓冲区,通过in 和 out指令来进行相应的读取

in_byte(0x60);

2.建立数组将扫描码与相应键对应

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 };

3.设置键盘缓冲区

防止扫描码不止一个字符时,8042的输入缓冲区大小不够。 定义

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

对缓冲区的使用

PRIVATE KB_INPUT kb_in; 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++; } }

初始化kb_in

PUBLIC void init_keyboard() { kb_in.count = 0; kb_in.p_head = kb_in.p_tail = kb_in.buf; put_irq_handler(KEYBOARD_IRQ, keyboard_handler);/*设定键盘中断处理程序*/ enable_irq(KEYBOARD_IRQ); /*开键盘中断*/ }

判断缓冲区中是否有扫描码,如果有就开始读 (在读的时候关闭中断)

PUBLIC void keyboard_read() { u8 scan_code; if(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(); disp_int(scan_code); } }

4.解析扫描码

对于0xE0和0xE1单独处理,对其余判断 将Make Code打印出来

PUBLIC void keyboard_read() { u8 scan_code; char output[2]; int make; /* TRUE: make; FALSE: break. */ memset(output, 0, 2); if(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(); /* 下面开始解析扫描码 */ if (scan_code == 0xE1) { /* 暂时不做任何操作 */ } else if (scan_code == 0xE0) { /* 暂时不做任何操作 */ } else { /* 下面处理可打印字符 */ /* 首先判断Make Code 还是 Break Code */ make = (scan_code & FLAG_BREAK ? FALSE : TRUE); /* 如果是Make Code 就打印,是 Break Code 则不做处理 */ if(make) { output[0] = keymap[(scan_code&0x7F)*MAP_COLS]; disp_str(output); } } /* disp_int(scan_code); */ } }

处理特殊按键

PRIVATE int code_with_E0 = 0; PRIVATE int shift_l; /* l shift state */ PRIVATE int shift_r; /* r shift state */ PRIVATE int alt_l; /* l alt state */ PRIVATE int alt_r; /* r left state */ PRIVATE int ctrl_l; /* l ctrl state */ PRIVATE int ctrl_r; /* l ctrl state */ PRIVATE int caps_lock; /* Caps Lock */ PRIVATE int num_lock; /* Num Lock */ PRIVATE int scroll_lock; /* Scroll Lock */ PRIVATE int column; PUBLIC void keyboard_read() { u8 scan_code; char output[2]; int make; /* TRUE: make; FALSE: break. */ u32 key = 0;/* 用一个整型来表示一个键。比如,如果 Home 被按下, * 则 key 值将为定义在 keyboard.h 中的 'HOME'。 */ u32* keyrow; /* 指向 keymap[] 的某一行 */ /* memset(output, 0, 2); */ if(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(); /* 下面开始解析扫描码 */ if (scan_code == 0xE1) { /* 暂时不做任何操作 */ } else if (scan_code == 0xE0) { code_with_E0 = 1; } else { /* 下面处理可打印字符 */ /* 首先判断Make Code 还是 Break Code */ make = (scan_code & FLAG_BREAK ? 0 : 1); /* 先定位到 keymap 中的行 */ keyrow = &keymap[(scan_code & 0x7F) * MAP_COLS]; column = 0; if (shift_l || shift_r) { column = 1; } if (code_with_E0) { column = 2; code_with_E0 = 0; } key = keyrow[column]; switch(key) { case SHIFT_L: shift_l = make; key = 0; break; case SHIFT_R: shift_r = make; key = 0; break; case CTRL_L: ctrl_l = make; key = 0; break; case CTRL_R: ctrl_r = make; key = 0; break; case ALT_L: alt_l = make; key = 0; break; case ALT_R: alt_l = make; key = 0; break; default: if (!make) { /* 如果是 Break Code */ key = 0; /* 忽略之 */ } break; } /* 如果 Key 不为0说明是可打印字符,否则不做处理 */ if(key){ output[0] = key; disp_str(output); } } } }

对于特殊按键设立相应的变量来记录,通过if判断让column的值为1,取keymap数组中第二列的值。 完善对于类似F1,F2等功能键的处理 将打印的功能用另一个函数来处理

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; if (shift_l || shift_r) { column = 1; } if (code_with_E0) { column = 2; code_with_E0 = 0; } 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; default: break; } if (make) { /* 忽略 Break Code */ 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; in_process(key); }

处理打印函数

PUBLIC void in_process(u32 key) { char output[2] = {'\0', '\0'}; if (!(key & FLAG_EXT)) { output[0] = key & 0xFF; disp_str(output); } }

TTY:终端,在Linux或UNIX中通过Alt+F1,ALT+F2等组合键切换,不同终端可以有不同的输入输出,相互之间不影响。 不同的TTY对应着同一个输入键盘,但输出却不同,这是通过显示显存的不同位置来实现的。 通过设置相应寄存器来实现让系统显示指定位置的内容 对于这么多寄存器,只有一个端口,需要用到Address Register,要想访问某个寄存器,需要先向Address Register写入对于的索引值,然后再通过端口进行操作。 设置光标的位置

out_byte(CRTC_ADDR_REG, CURSOR_H); out_byte(CRTC_DATA_REG, ((disp_pos/2)>>8)&0xFF); out_byte(CRTC_ADDR_REG, CURSOR_L); out_byte(CRTC_DATA_REG, (disp_pos/2)&0xFF); enable_int();

相应端口的宏

#define CRTC_ADDR_REG 0x3D4 /* CRT Controller Registers - Addr Register */ #define CRTC_DATA_REG 0x3D5 /* CRT Controller Registers - Data Register */ #define START_ADDR_H 0xC /* reg index of video mem start addr (MSB) */ #define START_ADDR_L 0xD /* reg index of video mem start addr (LSB) */ #define CURSOR_H 0xE /* reg index of cursor position (MSB) */ #define CURSOR_L 0xF /* reg index of cursor position (LSB) */ #define V_MEM_BASE 0xB8000 /* base of color video memory */ #define V_MEM_SIZE 0x8000 /* 32K: B8000H -> BFFFFH */

TTY任务 建立多个任务,让一个循环轮询每个TTY,当找到对应的TTY时,进行键盘缓冲区的读取及字符的显示。 在读取键盘函数中通过添加一个指向当前TTY的指针来让他知道被那个TTY调用,而每个TTY都应有一个成员来记录相应输出信息。 1.新建TTY和CONSOLE结构体(显示情况)

typedef struct s_tty { u32 in_buf[TTY_IN_BYTES]; /* TTY 输入缓冲区 */ u32* p_inbuf_head; /* 指向缓冲区中下一个空闲位置 */ u32* p_inbuf_tail; /* 指向键盘任务应处理的键值 */ int inbuf_count; /* 缓冲区中已经填充了多少 */ struct s_console * p_console; }TTY; typedef struct s_console { unsigned int current_start_addr; /* 当前显示到了什么位置 */ unsigned int original_addr; /* 当前控制台对应显存位置 */ unsigned int v_mem_limit; /* 当前控制台占的显存大小 */ unsigned int cursor; /* 当前光标位置 */ }CONSOLE; %define NR_CONSOLES 3 //设置三个终端

任务的框架

PUBLIC void task_tty() { TTY* p_tty; init_keyboard(); for (p_tty=TTY_FIRST;p_tty<TTY_END;p_tty++) { init_tty(p_tty); } nr_current_console = 0; while (1) { for (p_tty=TTY_FIRST;p_tty<TTY_END;p_tty++) { tty_do_read(p_tty); tty_do_write(p_tty); } } }

初始化TTY

PRIVATE void init_tty(TTY* p_tty) { p_tty->inbuf_count = 0; p_tty->p_inbuf_head = p_tty->p_inbuf_tail = p_tty->in_buf; int nr_tty = p_tty - tty_table; p_tty->p_console = console_table + nr_tty; }

判断是否是当前控制台

PUBLIC int is_current_console(CONSOLE* p_con) { return (p_con == &console_table[nr_current_console]); }

用tty_do_read()来进行TTY的读取

PRIVATE void tty_do_read(TTY* p_tty) { if (is_current_console(p_tty->p_console)) { keyboard_read(p_tty); } }

tty_do_write()进行写操作

PRIVATE void tty_do_write(TTY* p_tty) { if (p_tty->inbuf_count) { char ch = *(p_tty->p_inbuf_tail); p_tty->p_inbuf_tail++; if (p_tty->p_inbuf_tail == p_tty->in_buf + TTY_IN_BYTES) { p_tty->p_inbuf_tail = p_tty->in_buf; } p_tty->inbuf_count--; out_char(p_tty->p_console, ch); } }

out_char

PUBLIC void out_char(CONSOLE* p_con, char ch) { u8* p_vmem = (u8*)(V_MEM_BASE + disp_pos); *p_vmem++ = ch; *p_vmem++ = DEFAULT_CHAR_COLOR; disp_pos += 2; set_cursor(disp_pos/2); } /*======================================================================* set_cursor *======================================================================*/ PRIVATE void set_cursor(unsigned int position) { disable_int(); out_byte(CRTC_ADDR_REG, CURSOR_H); out_byte(CRTC_DATA_REG, (position >> 8) & 0xFF); out_byte(CRTC_ADDR_REG, CURSOR_L); out_byte(CRTC_DATA_REG, position & 0xFF); enable_int(); }

对于每一个TTY,执行tty_do_read,调用keyboard_read()将读入的字符交给in_process处理,如果是需要输出的字符,会被放入缓冲区,由tty_do_write处理,out_char显示

实现多控制台

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); }

修改

PRIVATE void init_tty(TTY* p_tty) { p_tty->inbuf_count = 0; p_tty->p_inbuf_head = p_tty->p_inbuf_tail = p_tty->in_buf; init_screen(p_tty); }

切换控制台

PUBLIC void select_console(int nr_console) /* 0 ~ (NR_CONSOLES - 1) */ { if ((nr_console < 0) || (nr_console >= NR_CONSOLES)) { return; } nr_current_console = nr_console; set_cursor(console_table[nr_console].cursor); set_video_start_addr(console_table[nr_console].current_start_addr); } PRIVATE void set_video_start_addr(u32 addr) { disable_int(); out_byte(CRTC_ADDR_REG, START_ADDR_H); out_byte(CRTC_DATA_REG, (addr >> 8) & 0xFF); out_byte(CRTC_ADDR_REG, START_ADDR_L); out_byte(CRTC_DATA_REG, addr & 0xFF); enable_int(); }

修改,将原来tty_task()中直接选择控制台的语句换成对控制台切换的调用

PUBLIC void task_tty() { TTY* p_tty; init_keyboard(); for (p_tty=TTY_FIRST;p_tty<TTY_END;p_tty++) { init_tty(p_tty); } select_console(0); while (1) { for (p_tty=TTY_FIRST;p_tty<TTY_END;p_tty++) { tty_do_read(p_tty); tty_do_write(p_tty); } } }

完善键盘处理

对于回车键和退格键,往TTY缓冲区写入“\n’ ,’\b’,然后在out_char中处理, 思路是回车键:直接把光标移动到下一行开头,退格键:把光标移动到上一个字符,并在那里写一个空格。 对于Caps Lock,Num Lock,Scroll Lock的处理:利用三个全局变量代表这三个键对应的小灯的状态,在键盘初始化时赋想要的初值,并设置灯的相应状态。 1.通过相应端口操作来设置LDT

PRIVATE void kb_wait() /* 等待 8042 的输入缓冲区空 */ { u8 kb_stat; do { kb_stat = in_byte(KB_CMD); } while (kb_stat & 0x02); } /*======================================================================* kb_ack *======================================================================*/ PRIVATE void kb_ack() { u8 kb_read; do { kb_read = in_byte(KB_DATA); } while (kb_read =! KB_ACK); } /*======================================================================* set_leds *======================================================================*/ PRIVATE void set_leds() { u8 leds = (caps_lock << 2) | (num_lock << 1) | scroll_lock; kb_wait(); out_byte(KB_DATA, LED_CODE); kb_ack(); kb_wait(); out_byte(KB_DATA, leds); kb_ack(); }

2.LED初始化

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); /*开键盘中断*/ }

3.修改读键盘函数,增加相应的处理

最新回复(0)