自作キーボード「Keyball61」:おれのファームまとめ後編
はじめに
前回の続きです。今回はOLEDをカスタムします。
リポジトリ
今回のコードは前編ブランチから分けた「blog_firm2」ブランチに上げました。
カスタム内容
こんな感じの内容を表示できるようにカスタムします。
- CPI
- スクロール速度
- Lockキー状態
- レイヤー番号
- スクロール状態
完成形はこちらです。ページ切り替えもできるようにしてみました。
ページ切り替えキーで順番に切り替わるようにしてます。
- デフォルトページ(上記内容)
- ステータスページ
- バージョンページ
さらに逆側のOLEDも変えて、ステータス情報を表示してみました。
ファイル作成
custom_oled.c
を作成してこの中にOLED処理を実装します。
keymap.c
に全部記述してもいいのですが、最終的なコードが長くなってしまったのでファイルを分けました。
└─qmk_firmware └─keyboards └─keyball └─keyball61 └─keymaps └─matucon └─custom_oled.c // 新ファイル
既存処理の無効化
既存の表示処理をコメントアウトして新しい処理に切り替えます。
custom_oled.c
に関数を作ってkeymap.c
からこれを呼ぶようにします。
#include QMK_KEYBOARD_H // カスタム内容表示 void keyball_oled_render_mymain(void) { // ここに処理を記述していく }
keymap.c
の既存の表示処理はコメントアウトします。
#include "lib/oledkit/oledkit.h" #include "custom_oled.c" void oledkit_render_info_user(void) { // keyball_oled_render_keyinfo(); // keyball_oled_render_ballinfo(); // keyball_oled_render_layerinfo(); keyball_oled_render_mymain(); }
画面の角度変更
横向き表示を縦向き表示にします。keymap.c
のoled_init_user()
関数を書き換えます。
oled_rotation_t oled_init_user(oled_rotation_t rotation) { if (is_keyboard_master()) { return OLED_ROTATION_270; } return rotation; }
角度の定義は90度単位で指定できるようです。
- OLED_ROTATION_0
- OLED_ROTATION_90
- OLED_ROTATION_180
- OLED_ROTATION_270
CPI、スクロール速度表示
もともと表示されてる内容ですがちょっと簡略化します。
CPIは1/100の値にしスクロール値も数字だけにします。
custom_oled.c
に以下を追記します。
// 数値を文字列に変換します。指定桁数の右寄せでスペースパディングされます。 static const char *itoc(uint8_t number, uint8_t width) { static char str[5]; uint8_t i = 0; width = width > 4 ? 4 : width; do { str[i++] = number % 10 + '0'; number /= 10; } while (number != 0); while (i < width) { str[i++] = ' '; } int len = i; for (int j = 0; j < len / 2; j++) { char temp = str[j]; str[j] = str[len - j - 1]; str[len - j - 1] = temp; } str[i] = '\0'; return str; } // CPI, スクロール情報表示 static void print_cpi_status(void) { oled_write(itoc(keyball_get_cpi(), 0), false); oled_write_P(PSTR(" "), false); oled_set_cursor(4, 2); oled_write_char('0' + keyball_get_scroll_div(), false); } // デフォルトページ表示 static void render_default(void) { print_cpi_status(); } // OLEDメイン処理 void keyball_oled_render_mymain(void) { render_default(); // 追記 }
数値から文字列への変換処理itoc()は以下の理由で独自実装しました。
- 標準ライブラリを使うと容量を食う
- libフォルダの
keymap.c
にも実装されてるが外から呼べなかった - 指定桁数でパディングしたかった(指定桁数で空白文字列の左パディングができるように作りました)
char *itoc(uint8_t number, uint8_t width)
引数 | 内容 |
---|---|
第1引数 | 表示したい値 |
第2引数 | パディングの桁数 |
タイトル表示
文字出力では1行に5文字しか入らないのでタイトルは画像にします。 フリーのドット絵エディタでこんな感じで作りました。OLED用なのでちっちゃいです。
こちらにも上げました。画像は以下ののサイトで配列データに変換します。
- 「1. Select image」で画像を読み込ませると「3. Preview」に画像が表示されます。
- 「4. Output」の「Draw mode」を「Vertical - 1 bit per pixel」にして 方向を変えます。
- 「Generate code」ボタンでコードを出力し
custom_oled.c
にコピペします。
// ヘッダタイトル static const char PROGMEM img_title[] = { 0x3e, 0x63, 0x41, 0x41, 0x22, 0x00, 0x7c, 0x14, 0x08, 0x00, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x7f, 0x49, 0x41, 0x22, 0x1c, 0x00, 0x74, 0x00, 0x38, 0x40, 0x38 };
print_cpi_status()の先頭へ画像表示処理を追加します。
static void print_cpi_status(void) { oled_write_raw_P(img_title, sizeof(img_title)); oled_set_cursor(0, 2); // 以下省略 ]
Lockキー状態表示
NumLockなどのLockキー状態を表示します。
// Lockキー状態表示 static void print_lock_key_status(void) { oled_set_cursor(0, 6); const led_t led_state = host_keyboard_led_state(); oled_write_P(led_state.caps_lock ? PSTR("C ") : PSTR("- "), false); oled_write_P(led_state.num_lock ? PSTR("N ") : PSTR("- "), false); oled_write_P(led_state.scroll_lock ? PSTR("S") : PSTR("-") , false); } static void render_default(void) { print_cpi_status(); print_lock_key_status(); // 追記 }
OFFは「-」、ONはアルファベットで表示してみました。
レイヤーアイコン表示
レイヤー番号は大きめに表示したかったので画像表示にします。タイトル画像と同じように配列化します。
今のところレイヤー4までしか使ってないので0から4まで作りました。
// アラビア数字アイコン static const char PROGMEM img_num0[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xc0, 0xe0, 0x60, 0x70, 0x70, 0xe0, 0xe0, 0xc0, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0xff, 0xff, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x0f, 0xff, 0xfe, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xff, 0xff, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0x0f, 0x1e, 0x1c, 0x1c, 0x1c, 0x1c, 0x0f, 0x07, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; static const char PROGMEM img_num1[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xc0, 0xf0, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0x03, 0x03, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x1f, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }; static const char PROGMEM img_num2[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xc0, 0xe0, 0x60, 0x70, 0x70, 0x60, 0xe0, 0xe0, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x0f, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x80, 0xe0, 0xff, 0xff, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xc0, 0xf0, 0x78, 0x3c, 0x1e, 0x0f, 0x03, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x1e, 0x1f, 0x1f, 0x1d, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }; static const char PROGMEM img_num3[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xe0, 0xe0, 0x60, 0x70, 0x70, 0x60, 0xe0, 0xc0, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x07, 0x07, 0x00, 0x00, 0x80, 0x80, 0xc0, 0xe1, 0x7f, 0x3f, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0xe0, 0xe0, 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x07, 0xfe, 0xfc, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x07, 0x0f, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x1e, 0x0f, 0x07, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }; static const char PROGMEM img_num4[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xf0, 0xf0, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xf0, 0x7c, 0x1f, 0x07, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0xfc, 0xff, 0xe7, 0xe1, 0xe0, 0xe0, 0xe0, 0xff, 0xff, 0xff, 0xe0, 0xe0, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x1f, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
別パターンの画像も作成してみました。こちらでチェックしてみてください。 上記は普通のアラビア数字アイコンです。トップ画像のは肉球バージョンです。
表示処理はこんな感じです。
// レイヤーNo表示 static void print_layer_status(void) { oled_set_cursor(0, 10); switch (get_highest_layer(layer_state)) { case 1: oled_write_raw_P(img_num1, sizeof(img_num1)); break; case 2: oled_write_raw_P(img_num2, sizeof(img_num2)); break; case 3: oled_write_raw_P(img_num3, sizeof(img_num3)); break; case 4: oled_write_raw_P(img_num4, sizeof(img_num4)); break; default: oled_write_raw_P(img_num0, sizeof(img_num0)); break; } } static void render_default(void) { print_cpi_status(); print_lock_key_status(); print_layer_status(); // 追記 }
スクロール状態アイコン表示
レイヤー3以外でもスクロールモードを変えることがあるのでON/OFF状態をこんな感じのアイコンで表示してみます。
スクロールっぽい矢印アイコン(上向き、下向き)と非表示時用の真っ黒アイコンを用意します。
// スクロールアイコン static const char PROGMEM img_scroll_up[] = { 0x00, 0x80, 0x80, 0xc0, 0xc0, 0x60, 0x60, 0x30, 0x30, 0x18, 0x18, 0x0c, 0x0c, 0x86, 0x86, 0xc3, 0xc3, 0x86, 0x86, 0x0c, 0x0c, 0x18, 0x18, 0x30, 0x30, 0x60, 0x60, 0xc0, 0xc0, 0x80, 0x80, 0x00, 0xc3, 0x61, 0x61, 0x30, 0x30, 0x18, 0x18, 0x0c, 0x0c, 0x06, 0x06, 0x03, 0x03, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x03, 0x03, 0x06, 0x06, 0x0c, 0x0c, 0x18, 0x18, 0x30, 0x30, 0x61, 0x61, 0xc3 }; static const char PROGMEM img_scroll_down[] = { 0xc3, 0x86, 0x86, 0x0c, 0x0c, 0x18, 0x18, 0x30, 0x30, 0x60, 0x60, 0xc0, 0xc0, 0x80, 0x80, 0x00, 0x00, 0x80, 0x80, 0xc0, 0xc0, 0x60, 0x60, 0x30, 0x30, 0x18, 0x18, 0x0c, 0x0c, 0x86, 0x86, 0xc3, 0x00, 0x01, 0x01, 0x03, 0x03, 0x06, 0x06, 0x0c, 0x0c, 0x18, 0x18, 0x30, 0x30, 0x61, 0x61, 0xc3, 0xc3, 0x61, 0x61, 0x30, 0x30, 0x18, 0x18, 0x0c, 0x0c, 0x06, 0x06, 0x03, 0x03, 0x01, 0x01, 0x00 }; static const char PROGMEM img_scroll_no[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, };
表示処理はこんな感じです。
// スクロール状態表示 static void print_scroll_status(void) { oled_set_cursor(0, 8); oled_write_raw_P(keyball.scroll_mode ? img_scroll_up : img_scroll_no, sizeof(img_scroll_no)); oled_set_cursor(0, 14); oled_write_raw_P(keyball.scroll_mode ? img_scroll_down : img_scroll_no, sizeof(img_scroll_no)); } static void render_default(void) { print_cpi_status(); print_lock_key_status(); print_layer_status(); print_scroll_status(); // 追記 }
デフォルトページはこれで完成です。
ページング
ページを切り替える処理を追加します。
custom_oled.c
のkeyball_oled_render_mymain()関数を修正しページングできるように加工します。
// ステータス表示 static void render_status(void) { oled_write_ln_P(PSTR("STATE"), false); // 後ほど作成 } // バージョン表示 static void render_version(void) { oled_write_P(PSTR("Ver.\n\n"), false); // 後ほど作成 } static uint8_t page_no = 0; // ページ切り替え void change_page(bool pressed) { if (!pressed) { return; } oled_clear(); page_no ++; } // カスタム内容表示 void keyball_oled_render_mymain(void) { switch(page_no % 3) { case 1: render_status(); break; case 2: render_version(); break; default: render_default(); break; } }
ページ数はそんなに多くないためページを戻る処理は省略して進む処理のみにしました。
keymap.c
にはページング用のキーコードを追加してページ切り替え処理を作ります。
enum my_keyball_keycodes { // 既存の末尾に追加 OLED_IN, // OLED ページ変更 }; // キーマップの任意の場所に「OLED_IN」を追加 // 例: // [3] = LAYOUT_universal( // RGB_TOG , OLED_IN bool process_record_user(uint16_t keycode, keyrecord_t *record) { switch (keycode) { // 既存switch文にcaseを追加 case OLED_IN: change_page(record->event.pressed); return true; default: break; }
これでページ切り替えキーを押すと表示内容が変わるようになります。
ステータス表示ページ
ステータス画面には以下を表示します。
- LED のON/OFF
- レイヤーで色を変える機能のON/OFF
- LED エフェクトのスピード
- LED エフェクトのモード
- LED の色 (hue)
- LED の色 (sat)
- LED の色 (val)
- コンボキーのON/OFF
こんな感じで作ってみました。
static void render_status(void) { oled_write_ln_P(PSTR("STATE"), false); oled_write_P(rgblight_is_enabled() ? PSTR("led o") : PSTR("led -"), false); # ifdef LAYER_LED_ENABLE oled_write_P(layer_led ? PSTR("lay o") : PSTR("lay -"), false); # endif oled_write_P(PSTR("spd "), false); oled_write(itoc(rgblight_get_speed(), 0), false); oled_write_P(PSTR("mo"), false); oled_write(itoc(rgblight_get_mode(), 3), false); oled_set_cursor(0, 7); oled_write_P(PSTR("h "), false); oled_write(itoc(rgblight_get_hue(), 3), false); oled_write_P(PSTR("s "), false); oled_write(itoc(rgblight_get_sat(), 3), false); oled_write_P(PSTR("v "), false); oled_write_ln(itoc(rgblight_get_val(), 3), false); # ifdef COMBO_ENABLE oled_write_P(is_combo_enabled() ? PSTR("cmb o") : PSTR("cmb -"), false); # endif }
バージョン表示ページ
バージョン画面には以下を表示してみます。
キーマップ名はフォルダ名と同じ「matucon」が表示されます。
#include "version.h" static void render_version(void) { oled_write_P(PSTR("Ver.\n\n"), false); oled_write_ln_P(PSTR(QMK_BUILDDATE), false); oled_write_P(PSTR("\n"), false); oled_write_ln_P(PSTR(QMK_KEYMAP), false); oled_write_P(PSTR("\n"), false); oled_write_ln_P(PSTR(QMK_VERSION), false); }
"version.h"を利用する必要があります。 ちなみに「QMK_KEYBOARD」の内容を表示するとキーボード名「keyball/keyball61」が表示されます。
サブ側OLEDの変更
USB接続してないサブ側のOLEDも変えてみます。
こちらはステータス情報を表示するようにします。
custom_oled.c
には以下を追加します。
// サブ画面表示 void keyball_oled_render_mysub(void) { render_status(); }
keymap.c
は以下のようにします。
// 画面は両側回転させるように修正します。 oled_rotation_t oled_init_user(oled_rotation_t rotation) { return OLED_ROTATION_270; } // サブ側OLEDの表示処理 void oledkit_render_logo_user(void) { keyball_oled_render_mysub(); } // メイン、サブの判定 bool oled_task_user(void) { if (is_keyboard_master()) { oledkit_render_info_user(); } else { oledkit_render_logo_user(); } return true; }
サブ側のProMicroにも忘れずに書き込みます。
2つのProMicro間で変数の共有はできないのでコンボキーの状態などは反映できませんでした。 同じ理由でサブ側のページ切り替えも断念しました。
さいごに
OLEDの表示内容を変えてみました。ここまでのカスタムでメモリを97%消費していました。
27856/28672 (97%, 816 bytes free)
リファクタすればもう少し削れるかもです。容量の大きいマイコンが使えるとうれしっすね。