matuconとマイコン

ITガジェットが好物です

自作キーボード「Keyball61」:おれのファームまとめ後編

はじめに

前回の続きです。今回はOLEDをカスタムします。

リポジトリ

今回のコードは前編ブランチから分けた「blog_firm2」ブランチに上げました。

github.com

カスタム内容

こんな感じの内容を表示できるようにカスタムします。

  • 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.coled_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用なのでちっちゃいです。

こちらにも上げました。画像は以下ののサイトで配列データに変換します。

javl.github.io

  • 「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)

リファクタすればもう少し削れるかもです。容量の大きいマイコンが使えるとうれしっすね。