自作キーボードにゲームを作って入れた話
先日こんなのを作りました。
ソースコードは以下の場所に置いておきました。128x32 の OLED なら頑張れば組み込めるはずです。
キーボードについて
ツイートにも書いてる通り、このキーボードは自作キーボード勢の人に貸してもらったものです。
このキーボードは Helix キーボードキット から作ったものらしく、QMK Firmware の Helix のキーマップを弄ることでキーの設定が変えられました。自分は JIS 配列マンなのでいろいろ弄らないとつらい。
Helix のキーマップの変更については 日本語のドキュメント もあってとても楽でした。
なぜこんなのを作ったのか
「これって何か表示できるんですか?」「表示できるよ、ほら(その人の作った別の自作キーボードの OLED にテキストが表示されているのを見せてくれる)」
と言われつつ フォントデータ があり、このフォント番号を指定することでテキストを表示が出来るというのを教えてもらいました。
「ほんとだ。ところでこれってテキストじゃないと表示できないんですか?」「どうなんだろう、ちょっと分からないな」
ということでソースを追いかけていくと、フォントデータを表示している部分 を見つけました。
for (uint8_t row = 0; row < MatrixRows; ++row) {
for (uint8_t col = 0; col < MatrixCols; ++col) {
const uint8_t *glyph = font + (matrix->display[row][col] * FontWidth);for (uint8_t glyphCol = 0; glyphCol < FontWidth; ++glyphCol) {
uint8_t colBits = pgm_read_byte(glyph + glyphCol);
i2c_master_write(colBits);
}// 1 column of space between chars (it's not included in the glyph)
//i2c_master_write(0);
}
}
i2c_master_write
という関数でデータを転送しているだけというのが分かります。
もう少し細かく調べると、8 ドットのデータを 128 回書き込むと1行分のデータになり、それを4回繰り返しているだけだというのが分かりました。つまり 128x32 のディスプレイが、以下の順番で描画されるということです。
8ドット 8ドット 8ドット 8ドット -> 32 ドット
-----------------------
| 127 | 255 | 383 | 511 |
| 126 | 254 | 382 | 510 |
| 125 | 253 | 381 | 509 |
.......... 中略 .........
| 2 | 130 | 258 | 386 |
| 1 | 129 | 257 | 385 |
| 0 | 128 | 256 | 384 |
-----------------------
これさえ分かっていれば任意の表示ができそうです。
また、もう少し調べると timer_read()
関数で経過時間(ミリ秒)が16ビットで取れて、特定の関数が秒間数百回ぐらいのペースで呼ばれることが分かりました。
任意の表示、タイマー、アップデート関数、これだけあればゲームが作れそうだと思ったので作り始めました。
大変だったところ
ゲームを作るのは難しくないのですが、それ以外のところが大変でした。難しかったところだけ説明していきます。
任意の表示をするのが大変
任意の表示というのは、予め決まったパターンのフォントデータのみだけではなく、コードさえ書けば任意のドットパターンを表示できるようするということです。これを用意してあげるのが大変でした。
任意の表示をするためには、仮想的なスクリーンを表す情報があると良いでしょう。
今回は uint8_t screen[128][4]
という仮想スクリーンのデータを用意しました。左上が原点として、8ドットが4つに、縦が128ドット分です。 uint32_t screen[128]
でやろうと思っていたのですが、この環境は sizeof(int)
が 2 を返し、32ビットの計算も出来ないため不可能でした。
反映したい情報を仮想スクリーンに書き込み、これを元に i2c_master_write()
関数で実スクリーン書き込んでいくことになります。
まずは仮想スクリーンに任意の矩形を書き込めるようにすることを考えます。例えば、左上を原点として X=5, Y=10 の位置に 幅=5, 高さ=5 の矩形を描画しようと思うと
screen[0][5] = 0x7; screen[1][5] = 0xC0;
screen[0][6] = 0x7; screen[1][6] = 0xC0;
screen[0][7] = 0x7; screen[1][7] = 0xC0;
screen[0][8] = 0x7; screen[1][8] = 0xC0;
screen[0][9] = 0x7; screen[1][9] = 0xC0;
こんな感じに情報を書き込むことになります。矩形が8ビットの境界を超えると複数の場所に書き込む必要があることに注意する必要があります。
汎用的な仮想スクリーンへの描画はこんな感じのコードで実現できます。
これが出来たら、あとは i2c_master_write()
で実スクリーンに反映するだけです。実スクリーンは左下から始まることと、ビット順序が期待する順序と逆である(0000 0001, 0000 0010 と増えるのではなく、1000 0000, 0100 0000 と増えていく)ことに注意する必要があります。こんな感じになります。
ここら辺を作っていくのが大変でしたが、これだけ出来たら、あとはロジックを動かして仮想スクリーンに描画していくだけの普通のコードになるので、何も難しいことは無いです。
デバッグ表示が出来なくて大変
これは自分のやり方がまずかったのですが、フォントデータや、フォントの表示機能は一番最初に消したので、デバッグ用に表示する機能が一切なくて大変でした。
最初の方は、実スクリーンの最初の8ビットに値を転送して、何ドット光っているかでエラーの原因を探ったりしてました。
後になって 数字を表示する機能 を用意したので、それからは大分開発しやすかったです。ただし今でもアルファベットを表示する機能はありません。
謎のフリーズがあって大変
ドットの点灯する場所が多くなると、表示がずれたりした後に OLED が固まる、という現象に遭遇しました。つまり screen_draw_rect(screen, 0, 0, 32, 128)
とかで全体を点灯するとフリーズするということです。
これは結局解決できなくて、1ブロック 3x3 で作って隙間が無かったのを 1ブロック 2x2 にして隙間を作って点灯する量を減らすことで解決しました。9ドット表示だったのが4ドット表示になったので半分以下の点灯量になってます。
その後他の人の OLED に入れてもらって 3x3 でやってみたら普通に動いたので、自分の借りた OLED が何かおかしいだけなんだろうと思います。何か分かったら教えて下さい。
感想
楽しかったです。
あと CPU が高速でビックリしました。効率めっちゃ悪いと思いながらコード書いてる部分があったりしたのですが、特にネックになることなく普通に動いたのには驚きです。
こういう小さいゲームをチマチマ作るのは短時間で達成感が得られるのでとても良いですね。研修とかに使うのに向いてそうな気がします。
今後は、気が向いたらリポジトリに他のゲームとか追加するかもしれません。何かゲームを作った時にはぜひプルリクエスト下さい。