自作OS、始まります! #8
キーボード入力の改善
バッファリング対応
さて、#7のハンドラはグローバル変数に格納しており、これはよくないという話をしました。というのも、CPUの処理によっては入力された値を表示する前に次の割り込みが発生し、全ての入力を処理しきれない可能性があるからです。これを解決するために今回はリングバッファを用います。
スタックとキューとリングバッファ
誰もが認めるであろう難しいけど便利なものとしてC++のテンプレートがあげられると思います。実際、自分はほとんど理解出来ていないですが、そんな人でも簡単なデータ構造程度であればテンプレートを用いて作成できます。そしてこれが非常に便利なのです。
折角C++でOSを作っているわけですし、これを使わない手はありません。しかも、テンプレートで任意の型のデータ構造に対応させつつ、要素数は固定させるといったことも可能です。そのため、メモリ管理を使えないOS開発の初期段階においても存分に力を発揮してくれます。
まず、この記事での(というよりは要素数が固定の?)キューとリングバッファはほとんど同じものです。ただし、キューは要素数がいっぱいになった場合は何もしないけど、リングバッファは上書きをするという差を持たせています。スタックは普通のFILOです。
template <typename T, uint32_t N> class RingBuffer { public: RingBuffer() { write_pos = 0; read_pos = 0; count = 0; } void push(T push_data) { data[write_pos] = push_data; ++write_pos; ++count; if (write_pos == N) write_pos = 0; } void pop(void) { if (count == 0) return; ++read_pos; --count; if (read_pos == N) read_pos = 0; } T front(void) const { return data[read_pos]; } uint32_t size(void) const { return count; } bool empty(void) const { return !count; } private: T data[N]; uint32_t write_pos, read_pos, count; };
とりあえず使用しているリングバッファのコードのみ記載しておきます。といってもキューはpush時に要素数と最大値の比較をし、同じ場合はreturnをするif文を記述するだけです。スタックは積むだけなのでさらに簡単だと思いますのでいらないかと・・・
注意する点として、テンプレートを使う場合ヘッダファイルとソースファイルに分割するとコンパイルエラーが出てしまいます。使用したい型を明示的に記述すると一応解決できるのですが、それではあまりテンプレートを使う意味がないように思えます。ヘッダファイルに実装まで書いてしまえばこの問題は解決するので素直に記述しましょう。
リングバッファを使うように変更する
使用したいデータの型(intやcharなど)をTとして、確保したい要素数をNとして以下のように宣言すればこのリングバッファを使えるようになります。
RingBuffer<char, 100> key_buf; // char型で100個の要素を持ったリングバッファ /* 値の追加 */ key_buf.push(keycode); /* 値の取り出し */ char keycode = key_buf.front(); key_buf.pop();
そして上記のkey_bufを使ってハンドラを書き換えてみましょう。
void do_kbc_handler() { io_write_8(PIC0_OCW2_ADDR, 0x61); if (!(io_read_8(KBC_STATUS_ADDR) & KBC_STATUS_OBF)) { return; } char keycode = io_read_8(KBC_DATA_ADDR); key_buf.push(keycode); return; }
前回から少し変えてkeycodeの値自体を追加するようにしています。これは後の大文字小文字対応のためです。
関数的にも結構処理が短くできているように思われます。また、これによって100個までは値を取りこぼす心配がありません。どんな連打でもさすがに100個あれば足りるでしょう、試してはいませんが・・・もし足りない場合は要素数を増やせばいいので簡単ですね。
割り込み禁止は短く
リングバッファを使うことになったため、値を取り出すのは割り込み処理外となります。しかし、値を取り出そうとした瞬間に次の割り込みが発生しては大変困ります。そこで、cliという割り込み禁止命令を使って値を取り出す瞬間だけ割り込みが発生しないようにします。sti命令と同様にこれもアセンブリの関数です。
io_stihlt(); io_cli(); if(key_buf.empty()) continue; char keycode = key_buf.front(); key_buf.pop(); io_sti(); /* 取り出したkeycodeの処理 */
io_stihlt関数(sti命令で割り込みを許可した後にhlt命令でCPUを休ませてあげる関数)から処理が戻る時は、すなわち割り込みが発生した時です。そのため、一旦cli命令で割り込みを禁止し、確実にkeycodeの取得と削除を行ってからsti命令で割り込みを許可し、keycodeの処理に移るようにします。
これでずいぶん割り込みが短く、それでいて連続で発生しても対処できるようになりました。あとは大文字小文字だけです。
シフトの押下判定を取る
本来であればコントロールキー等もすべて対処したいところではありますが、現状コントロールキー使わないので・・・
キーボードを離した時にKBCから送られてくるキーコードは押した時の値+0x80です。そこで、シフトキーが押されたときにまずシフトが押されているかどうか(is_Shift)をtrueにし、離されたときにfalseにすればうまいことシフトキーの押下判定を取ることができそうです。
/* 取り出したkeycodeの処理 */ if(keycode & KBC_DATA_PRESSED){ keycode ^= KBC_DATA_PRESSED; if(keycode == Key_Shift_L || keycode == Key_Shift_R) is_Shift = false; continue; } if(keycode == Key_Shift_L || keycode == Key_Shift_R){ is_Shift = true; continue; } char write_data = is_Shift ? keymap_shift[keycode] : keymap[keycode]; putc(frame_buffer, write_data);
KBC_DATA_PRESSEDは0x80の値を持っていて、keycodeと&を取ると離された時かどうかがわかります。(&を取ってtrue、つまり1の場合keycodeも0x80のビットが立っているため、離されたときです。)そして、^、つまりxorを用いて0x80のビットをなくしてからシフトかどうかを確認します。このあたりの処理は特に何でもいい気がします、0x80を足したシフトのキーコードとの比較とかでも。キーコードがシフトの場合はis_Shiftをfalseに、そうでない場合は何らかのキーが離されただけなので何もしません。
次に、キーが押された場合の処理ですが、これは単純に押されたキーがシフトかどうかを見るだけです。シフトの場合はis_Shiftをtrueにし、そうでない場合はkeycodeに対応した値を取得、表示させます。このkeymapは頑張って作りました。シフトが押されている時または押されていない時に出したい値を羅列しただけです。
これで大文字も小文字も!なんかも出せるようになったはずです。試してみましょう。
ちゃんとシフトを押している間だけ大文字になってくれています。しかも!なんかもふつうに入力できています。
ちなみに、押したキーによっては変な値が出たりしますが・・・とりあえず気にしないでおきましょう。
次回以降の目標
割とサクッとやりたかった部分が実装出来てウキウキで記事書いてました。次回以降、どうしましょうか・・・描画をsheet構造体に対応させたいですが、そのためにはメモリ管理が必須です。しかし、メモリ管理は現状OSの講義で知識として習っただけのページングが必須。まずは勉強からになりそうですね。