この回では割り込み経由でキー入力できるように自作OSを改良する。
※過去回の記事とソースコードは下記から入手できる
https://github.com/Shadow5523/osdev/blob/master/README.md
また、ここで使用するコードはGitHubからダウンロードできる。
https://github.com/Shadow5523/osdev/releases/tag/version0.5.0
前回の記事では、IDTとPICの初期化までは完了した。次に行うのは実際にIDTへキーボードの割り込み信号を受け取ったときの処理を対応付け、今あるキー入力処理を少し変更を加えていく。
大まかな動きを下のような絵にしてみた。(;´・ω・)
まずはidt.cでIDTへキーボードの割り込み信号を受け取ったときの処理をあらかじめ対応付けさせる。
実際にOSを起動させてキー入力をするとキーを押した瞬間にPICへと信号が行き、今実行している処理より優先度が高いとPICはCPUにINT命令 + 割り込み信号を送る。するとCPUは一旦処理を中断させて、受け取った割り込み信号の値をもとにIDTに対応付けしてある処理を呼び出す。
呼び出された処理で現在のレジスタの状態をスタックへ退避させたあとにキー入力処理(バッファに画面に出力するデータを保存させる)を行う。その後画面に文字を出力させる処理は割り込み完了後に行う。(そうしなければ画面出力までキーボードより低い優先度の割り込みを受け取れなくなる)
以上のような流れでキーボード入力+画面出力を行うよう改造していく。
その前に、ヘッダーファイルが増えてきたので、includeフォルダを作ってその中にヘッダーファイルを移動させることにした。そのため、ヘッダーファイルをインクルードしているソースファイルは適宜変更を加えること。
1.interrupt.hではinterrupt.cで使用する関数のプロトタイプ宣言と、IRQ番号を表す定数マクロを宣言する。キーボード割り込みのIRQは0x61。また、interrupt.cではPICに対してinb/outb関数でコマンドを送ったりするのでpic.hをインクルードする。
// include/interrupt.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#ifndef _INETRRUPT_H_ #define _INETRRUPT_H_ #include "pic.h" #include <stdint.h> #include <stddef.h> void interrupt_done(void); //irq void keyboard_interrupt(void); //irqnum #define irq1 0x61 #endif _INETRRUPT_H_ |
2.ここでは、キーボード割り込みがあったらkeyboard_input_int()を呼び出しキー入力処理を行う。また割り込み処理が完了したらPICのマスター/スレーブそれぞれに対してEOIコマンド(0x20)を書き込む。
こうしなければキーボードより優先度の低い割り込み処理を受け取れなくなってしまう。
// interrupt.c
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include "include/interrupt.h" void interrupt_done(void){ outb(MASTER_PIC_CMD_STAT, PIC_EOI); outb(SLAVE_PIC_CMD_STAT, PIC_EOI); } void keyboard_interrupt(void){ outb(MASTER_PIC_CMD_STAT, irq1); keyboard_input_int(getchar()); interrupt_done(); } |
3.callに書かれている関数を呼び出す。実際にここの「as_keyboard_interrupt」ラベルをidtへ対応付けする。call命令の前の処理で割り込み直前のレジスタの状態をいったんスタックへ退避させ、call完了後再度それらをもとに戻しiretl命令でもとの処理へと戻る。
// interrupt.s
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
.global as_keyboard_interrupt .extern keyboard_interrupt as_keyboard_interrupt: push %es push %ds pushal mov %esp, %eax push %eax mov %ss, %ax mov %ax, %ds mov %ax, %es call keyboard_interrupt pop %eax popal pop %ds pop %es sti iretl |
4.interrupt.cの関数を呼び出すためにinterrupt.hをインクルードする。
またアセンブラで作成したinterrupt.sの「as_keyboard_interrupt」ラベルはアセンブラで宣言されているため、externを使用し明示的に外部で宣言されている事を宣言する。
// include/idt.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#ifndef _IDT_H_ #define _IDT_H_ #include <stdint.h> #include <stddef.h> #include "interrupt.h" //追加 #ifdef __cplusplus extern "C" void load_idtr(uint32_t); #else extern void load_idtr(uint32_t); #endif extern as_keyboard_interrupt(void); //追加 ...中略... |
5.アセンブラで作成したinterrupt.sの「as_keyboard_interrupt」ラベルを前回の記事で作成したIDTに登録を行う。
キーボードの割り込み信号を検知するとこのラベルが実行される。
※set_gate_desc()の引数やIRQベクタ番号についてはこちらを参照。
// idt.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include "include/idt.h" void idt_init(void){ idtr idt; terminal_writestring("Initialize IDT..."); for (size_t i = 0; i < IDT_LEN; i++) { set_gate_desc(i, 0, 0, 0); } set_gate_desc(33, (uint32_t)as_keyboard_interrupt, 0x08, 0x8e); //追加 idt.idt_size = IDT_LEN * sizeof(gate_desc) - 1; idt.base = (uint32_t)idt_entries; load_idtr((uint32_t)&(idt)); terminal_writestring(" OK!\n"); } ...中略... |
6.keyboard.hを以下のように変更を加える。key_buf構造体で画面へ出力する文字データを保持する。取り出し方はFIFO型式を利用している。
// include/keyboard.h
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 |
#ifndef _KEYBOARD_H_ #define _KEYBOARD_H_ #include <stdint.h> #include <stddef.h> //command #define SET_TYPEMATIC_RATE 0xF3 //追加 #define ENABLE_KEYBOARD 0xF4 //追加 #define SET_SCANCODESET 0xF0 //追加 #define SCAN_CODE_SET1 0x01 #define SCAN_CODE_SET2 0x02 #define SCAN_CODE_SET3 0x03 //以下4つのマクロで定数を宣言 #define TYPEMATICDELAY_SET1 0x01 #define TYPEMATICDELAY_SET2 0x02 #define PORTMAP_KEYBOARD1 0x60 #define PORTMAP_KEYBOARD2 0x64 //構造体は全面的に変更 typedef struct{ uint8_t pdata[128]; size_t write; size_t read; size_t len; }key_buf; void key_init(void); uint8_t ps2_kerboard_init(void); void keyboard_input_int(uint8_t); uint8_t enable_keyboard(void); uint8_t getscode(void); uint8_t getchar(void); uint8_t getscodeset(void); void change_codeset(uint8_t); void change_trate_delay(uint8_t); //追加 #endif _KEYBOARD_H_ |
7.実際に出力データをバッファに格納する部分。キー配列はUS配列。また、inb/outb関数のポート番号とコマンドを定数マクロに置き換える。
//keyboard.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 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 |
key_buf kb; //追加 void key_init(void){ change_trate_delay(TYPEMATICDELAY_SET2); //追加 if (enable_keyboard() == 0xFA) { terminal_writestring("Keyboard enable OK\n"); } if (ps2_kerboard_init() == 0) { terminal_writestring("PS/2 Keyboard init OK\n"); } } uint8_t ps2_kerboard_init(void){ change_codeset(SCAN_CODE_SET2); //関数名を変更 uint8_t scodeset = getscodeset(); if (scodeset == 0x43) { terminal_writestring("Scan code set 1\n"); } else if (scodeset == 0x41) { terminal_writestring("Scan code set 2\n"); } else if (scodeset == 0x3f) { terminal_writestring("Scan code set 3\n"); } else { terminal_writestring("Unknown scan code set\n"); return 1; } outb(0x60, 0xFA); return 0; } //下の関数を全面的に変更 void keyboard_input_int(uint8_t scan_code){ uint8_t us_keytable_set2[0x80] = { '0', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', '\b', '\t', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\n', '0', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', '\'', '`', '0', '\\', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/', '0', '0', '0', ' ', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0'}; if (scan_code <= 0x80) { if (kb.len < 128) { kb.pdata[kb.write++] = us_keytable_set2[scan_code]; ++kb.len; if (kb.write == 128) { kb.write = 0; } } } } //define定数へ変更 uint8_t enable_keyboard(void){ outb(PORTMAP_KEYBOARD1, ENABLE_KEYBOARD); return getscode(); } //define定数へ変更 uint8_t getscodeset(void){ while (inb(PORTMAP_KEYBOARD2) & 0x02); outb(PORTMAP_KEYBOARD1, SET_SCANCODESET); if (getscode() == 0xFA) { while (inb(PORTMAP_KEYBOARD2) & 0x02); outb(PORTMAP_KEYBOARD1, 0x00); return getscode(); } else { return 0x00; } } //define定数へ変更 uint8_t getscode(void){ uint8_t c = 0; do { if (inb(PORTMAP_KEYBOARD1) != c) { c = inb(PORTMAP_KEYBOARD1); if (c > 0) return c; } } while (1); } //define定数へ変更 uint8_t getchar(void){ return getscode(); } //define定数へ変更 void change_codeset(uint8_t set){ outb(PORTMAP_KEYBOARD1, SET_SCANCODESET); outb(PORTMAP_KEYBOARD1, set); } //追加 void change_trate_delay(uint8_t set){ while (inb(PORTMAP_KEYBOARD2) & 0x02); outb(PORTMAP_KEYBOARD1, SET_TYPEMATIC_RATE); while (inb(PORTMAP_KEYBOARD2) & 0x02); outb(PORTMAP_KEYBOARD1, set); } |
8.kernel.hでは出力文字データを受け取る配列を宣言。
// include/kernel.h
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 |
#if !defined(__cplusplus) #include <stdbool.h> #endif #include <stddef.h> #include <stdint.h> /* コンパイラが正しいターゲットを認識しているかの確認 */ #if defined(__linux__) #error "You are not using a cross-compiler, you will most certainly run into trouble" #endif /* 32-bit x86ターゲット以外のコンパイラを使用するとエラーになる */ #if !defined(__i386__) #error "This tutorial needs to be compiled with a ix86-elf compiler" #endif #include "terminal.h" #include "keyboard.h" #include "inb_outb.h" #include "gdt.h" #include "pic.h" #include "idt.h" uint8_t* c[2]; //追加 size_t strlen(const uint8_t*); #if defined(__cplusplus) us_keytableextern "C" #endif |
9.文字データを取り出し、画面へ出力させる。まずはcli命令で一時的に割り込み命令を受け取らないようにし、バッファーの長さが0以上ならデータを取り出す。その後sti命令で割り込み命令を受け取れるようにした後で画面出力を行う。
//kernel.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 28 29 30 31 32 33 34 35 36 37 38 39 |
#include "include/kernel.h" extern key_buf kb; //追加 void kernel_main(void){ terminal_initialize(); gdt_init(); pic_init(); idt_init(); key_init(); terminal_writestring("Hello, kernel World! \n"); /* 以下から変更 */ kb.len = 0; kb.write = 0; kb.read = 0; for (;;) { asm volatile("cli"); if (kb.len == 0) { asm volatile("sti"); } else { c[0] = kb.pdata[kb.read]; --kb.len; ++kb.read; if (kb.read == 128) { kb.read = 0; } asm volatile("sti"); terminal_writestring(c); } } /* ここまで */ } size_t strlen(const uint8_t* str){ size_t len = 0; while (str[len]) ++len; return len; } |
10.Makefileもそろそろ行が多くごちゃごちゃして見づらくなったので編集し整理する。
因みに、今更ではあるがMakefileの構文はいかのとおり。コマンド行の先頭の空白は必ずTabでなければならない。
ターゲット名 : ファイル名1 ファイル名2
コマンド
また今から改造するMakefileには以下のような内部マクロと呼ばれるものを使用する。
$(マクロ名) | 上で宣言したマクロの中身(文字列)に置き換わる |
$< | ターゲット名の横で宣言した最初のファイル名に置き換わる |
$^ | ターゲット名の横で宣言したすべてのファイル名に置き換わる |
改造したMakefileを以下のように作り直す。
// Makefile
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 |
all: create OBJ = kernel.o terminal.o boot.o inb_outb.o keyboard.o \ gdt.o idt.o pic.o interrupt.o OBJAS = interruptas.o idts.o gdts.o boot.o CC = i686-elf-gcc CCAS = i686-elf-as CFLAGS = -ffreestanding -O2 -Wall INCLUDEDIR = include/ interruptas.o: interrupt.s $(CCAS) $< -o interruptas.o idts.o: idt.s $(CCAS) $< -o idts.o gdts.o: gdt.s $(CCAS) $< -o gdts.o boot.o: boot.s $(CCAS) $< -o boot.o interrupt.o: interrupt.c $(INCLUDEDIR)interrupt.h $(CC) -c $^ -std=gnu99 $(CFLAGS) -Wextra pic.o: pic.c $(INCLUDEDIR)pic.h $(CC) -c $^ -std=gnu99 $(CFLAGS) -Wextra idt.o: idt.c $(INCLUDEDIR)idt.h $(CC) -c $^ -std=gnu99 $(CFLAGS) -Wextra gdt.o: gdt.c $(INCLUDEDIR)gdt.h $(CC) -c $^ -std=gnu99 $(CFLAGS) -Wextra inb_outb.o: $(INCLUDEDIR)inb_outb.h $(CC) -c $^ -std=gnu99 $(CFLAGS) -Wextra keyboard.o: keyboard.c $(INCLUDEDIR)keyboard.h $(CC) -c $^ -std=gnu99 $(CFLAGS) -Wextra terminal.o: terminal.c $(INCLUDEDIR)terminal.h $(INCLUDEDIR)vga.h $(CC) -c $^ -std=gnu99 $(CFLAGS) -Wextra kernel.o: kernel.c $(INCLUDEDIR)kernel.h $(CC) -c $^ -std=gnu99 $(CFLAGS) -Wextra create: $(OBJAS) $(OBJ) $(CC) -T linker.ld -o myos.bin $(CFLAGS) -nostdlib *.o -lgcc grub2-file --is-x86-multiboot myos.bin \cp -f myos.bin isodir/boot/myos.bin grub2-mkrescue -o myos.iso isodir clean: rm -f *.o myos.bin myos.iso $(INCLUDEDIR)*~ ./*~ |
11.あとはmakeコマンドでコンパイルして、エラー等が無ければOK。
次回は英大文字入力等を行えるように改良していく(まだキーボードから抜けられない…)