この記事について
OSSのキーボード用ファームウェアである QMK の各機能について、個人的によく使う機能を簡単にまとめた記事となります。
最新の情報は公式ドキュメントを参照ください。
環境構築とファームウェアのビルド
最初に環境構築を行うためには 'Getting started' を参考に、リポジトリのクローンと開発環境構築用スクリプトの実行を行います。
git clone --recurse-submodules https://github.com/qmk/qmk_firmware.git
cd qmk_firmware
util/qmk_install.sh
ファームウェアをコンパイルするには、QMKのルートディレクトリからmake <keyboard>:<default>を実行します。
keyboards/<keyboard>/keymaps/<keymap>を実行します。
下記はHHKBのdefaultキーマップをビルドする例です。
make hhkb:default
ビルドが正常に完了すると、qmk_firmware フォルダ直下に、HEXファイルが作成されます。
VSCode環境でインデント幅が4に固定される
VSCodeにてEditorConfig拡張が有効な環境にあると、 .editorconfig の下記設定によりインデント幅が4に固定されるようです。
https://github.com/qmk/qmk_firmware/commit/068571b9febb46edb64663bf6e84ae821a8219a3#diff-1e70daafb475c0ce3fef7d2728279182
このためインデント幅を4以外に設定するには.editorconfigを編集するか、EditorConfig拡張を無効化する必要があります。
.editorconfigを編集してCソースファイルのみインデントサイズを2にする場合は、下記の設定を追加します。
[{*.h,*.c}]
indent_style = space
indent_size = 2
ファームウェアの書き込み
'Flashing Firmware'を参考に、QMK Toolbox をするのが一番簡単です。
ファームウェアの書き換えをおこなっても、マイコン内のEEPROMに書き込まれた設定情報('default_layer_state'、'MAGIC_SWAP_ALT_GUI'など)が残っていると、新しいファームウェアで期待通りの動作とならない場合があります。
EEPROMの設定を初期化したい場合は、マイコンをブート状態にしてから QMK Toolbox のGUI下方の 'Reset EEPROM' を実行するか、Bootマジックホットキーの 'Clear the EEPROM' 機能でEEPROMを初期化しましょう。
新しいキーマップを作成
キーマップをカスタマイズする場合は、まずはベースとするkeymapのフォルダを複製して、自分のユーザー名で保存しましょう。既存のkeymap.cを直接編集すると、git pullでQMKをバージョンアップする際にコンフリクトが発生して面倒なのでお勧めしません。
QMKの ./util/new_keymap.sh を使うと、下記の様に<keyboard>のdefaultキーマップをベースに指定の<user_name>で複製することができます。
./util/new_keymap.sh <keyboard> <user_name>
キーマップとレイヤー
'Keymap Overview'
QMKではキー配列を定義した行列を、レイヤーとして重ねたデータをkeymapsの3次元配列として定義します。
QMKは様々な機能がありますが、最小構成としてはkeymap.cへkeymaps配列定義さえすればコンパイルすることができます。
下記に HHKB JP キーマップ をベースに作成したキーマップファイルを示します。
# include QMK_KEYBOARD_H
// keymap layer number
enum keymap_layer {
  KL_BASE,
  KL_FN,
};
// custom keycode
# define MO_FN   MO(KL_FN)
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
  [KL_BASE] = LAYOUT_JP(
     KC_ESC,    KC_1,    KC_2,    KC_3,    KC_4,    KC_5,    KC_6,    KC_7,    KC_8,    KC_9,    KC_0, KC_MINS,  KC_EQL, KC_JYEN, KC_BSPC,
     KC_TAB,    KC_Q,    KC_W,    KC_E,    KC_R,    KC_T,    KC_Y,    KC_U,    KC_I,    KC_O,    KC_P, KC_LBRC, KC_RBRC,
    KC_LCTL,    KC_A,    KC_S,    KC_D,    KC_F,    KC_G,    KC_H,    KC_J,    KC_K,    KC_L, KC_SCLN, KC_QUOT, KC_BSLS,  KC_ENT,
    KC_LSFT,    KC_Z,    KC_X,    KC_C,    KC_V,    KC_B,    KC_N,    KC_M, KC_COMM,  KC_DOT, KC_SLSH,   KC_RO,   KC_UP, KC_RSFT,
      MO_FN, KC_ZKHK, KC_LGUI, KC_LALT, KC_MHEN,      KC_SPC     , KC_HENK, KC_KANA, KC_RALT,   MO_FN, KC_LEFT, KC_DOWN, KC_RGHT
  ),
  [KL_FN] = LAYOUT_JP(
     KC_PWR,   KC_F1,   KC_F2,   KC_F3,   KC_F4,   KC_F5,   KC_F6,   KC_F7,   KC_F8,   KC_F9,  KC_F10,  KC_F11,  KC_F12,  KC_INS, KC_DEL,
    KC_CAPS, _______, _______, _______, _______, _______, _______, _______, KC_PSCR, KC_SLCK, KC_PAUS,   KC_UP, _______,
    _______, KC_VOLD, KC_VOLU, KC_MUTE, KC_EJCT, _______, KC_PAST, KC_PSLS, KC_HOME, KC_PGUP, KC_LEFT, KC_RGHT, _______, KC_PENT,
    _______, _______, _______, _______, _______, _______, KC_PPLS, KC_PMNS,  KC_END, KC_PGDN, KC_DOWN, _______, _______, _______,
    _______, _______, _______, _______, _______,     _______     , _______, _______, _______, _______, _______, _______, _______
  ),
};
各レイヤの有効状態はdefault_layer_state および layer_state変数の有効なビットで指定されます。
default_layer_stateはDF(layer)キーコードによって切り替えられ、ベースとして常に有効なレイヤーとなります。
layer_stateはMO(layer)などのキーコードによって切り替えられ、特定のキーを押している間、一時的に有効としたいレイヤーなどを設定します。
上記の例ではFNキー(MO_FN)が押されている間だけKL_FNレイヤーが有効化されます。
レイヤーを切り替えるためのマクロは他にもいくつの種類が用意されています。詳細は公式マニュアルの'Switching and Toggling Layers'を参照してください。
KL_FNに定義されたキーコードで_______は、KC_TRANSの別名で、このキーが押された場合は、レイヤーを透過して、より下位の有効なレイヤーのキーコードが選択されます。この例の場合は、KL_FNの次に有効なレイヤーはKL_BASEのみなので_______の押されたキーはすべてKL_BASEのキーが入力されます。
一つのキーに修飾キーとキーコード入力を割り当てる
'Mod-Tap'機能を使用すると、一つのキーに通常のキーコード入力と、修飾キー機能の両方を割り当てることができます。
この手の機能で定番なカスタムとして、スペースバーにシフト機能も追加したSandS(Space and Shift)と呼ばれる機能を実装してみます。
これは単独でタップする場合はスペース入力、スペースバーを押しながらほかのキーを入力した場合はShift修飾キーとして動作させるキーとなります。
MTを使ってSandSキーを定義した例が下記になります。
# define MT_SS       MT(MOD_LSFT, KC_SPACE)
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
  [KL_BASE] = LAYOUT_JP(
     ...
      MO_FN, KC_ZKHK, KC_LGUI, KC_LALT, KC_MHEN,      MT_SS     , KC_HENK, KC_KANA, KC_RALT,   MO_FN, KC_LEFT, KC_DOWN, KC_RGHT
  ),
};
Mod-Tapは通常にホールドした場合は、指定した修飾キーとして動作しますが、1回タッピングしてすぐに2回目のキー入力をホールドした場合は、修飾キーではなく、キーコード側をホールドした動作となり、キーコードのオートリピートが有効になります。
ただし、TAPPING_FORCE_HOLDが有効な場合は、上記のキーコードのホールドは行われません。
タッピングと判定されるまでのタイムアウト時間はTAPPING_TERMで設定された時間が適用されます。(デフォルトは200ms)
Mod-Tapが有効だとゲームなどでスペースキーを使用する際に不便なため、通常の動作と切り替えて使用したい場合は、SandSキー専用のレイヤー(KL_SANDS)を作ってdefault_layer_stateの切り替え(DF(KL_SANDS))によって使用することもできます。
# include QMK_KEYBOARD_H
// keymap layer number
enum keymap_layer {
  KL_BASE,
  KL_FN,
  KL_SANDS,
};
// custom keycode
# define MO_FN       MO(KL_FN)
# define DF_SAS      DF(KL_SANDS)
# define MT_SS       MT(MOD_LSFT, KC_SPACE)
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
  [KL_BASE] = LAYOUT_JP(
     ...
      MO_FN, KC_ZKHK, KC_LGUI, KC_LALT, KC_MHEN,      KC_SPC     , KC_HENK, KC_KANA, KC_RALT,   MO_FN, KC_LEFT, KC_DOWN, KC_RGHT
  ),
  [KL_FN] = LAYOUT_JP(
     ...
    _______, _______, _______, _______, _______,      DF_SAS     , _______, _______, _______, _______, _______, _______, _______
  ),
  [KL_SANDS] = LAYOUT_JP(
     ...
    _______, _______, _______, _______, _______,       MT_SS     , _______, _______, _______, _______, _______, _______, _______
  ),
};
一つのキーにレイヤー切り替えとキーコード入力を割り当てる
LT(layer, kc)を使用すると、タップした場合はキーコードを入力し、ホールドした場合はレイヤー切り替えを行うキーを設定できます。
例えば、カーソル移動キーを配置したレイヤー(KL_CURSOR)を作成し、「無変換」キーのホールドでレイヤー切り替え、タップでKC_MHENの入力を行うといった動作を実現することができます。
# include QMK_KEYBOARD_H
// keymap layer number
enum keymap_layer {
  KL_BASE,
  KL_CURSOR,
};
// custom keycode
# define LT_MHEN     LT(KL_CURSOR, KC_MHEN)
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
  [KL_BASE] = LAYOUT_JP(
      ...
      MO_FN, KC_ZKHK, KC_LGUI, KC_LALT, LT_MHEN, ...
  ),
  [KL_CURSOR] = LAYOUT_JP(
    ...
    _______, XXXXXXX, XXXXXXX,   KC_UP, XXXXXXX, ...
    _______, XXXXXXX, KC_LEFT, KC_DOWN, KC_RGHT, ...
    _______, _______, _______, _______, _______, ...
  ),
};
LT_MHEN と同じ位置にある移動先レイヤーのキーは、キーを解放した際にちゃんとLTの解放イベントを取得できるように、他のキーを設定せずKC_TRANS(_______)で透過させておきます。
Mod-Tap と同様に、LT(layer, kc)もダブルタップした後にホールドするとレイヤー移動ではなく、キーコードのオートリピートがを行う仕様となります。
しかしながら、通常LT()によるダブルタップのオートリピートは誤操作の原因となりやすいため無効化したいところです。無効化する方法としては下記の二つの方法があります。
一つ目は、TAPPING_FORCE_HOLDを有効化する方法。ただし、この方法では Mod-Tap のオートリピート機能も無効化されます。
二つ目は、LTを使わずに、'process_record_user'にてタッピング判定を行う方法です。下記にこの例を示します。
process_record_user関数を使用すると、特定キーコードの動作をユーザー定義で上書きすることができるため、これを使ってレイヤー切り替えと、タッピング判定を行います。1
# include QMK_KEYBOARD_H
// keymap layer number
enum keymap_layer {
  KL_BASE,
  KL_CURSOR,
};
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
  [KL_BASE] = LAYOUT_JP(
    ...
      MO_FN, KC_ZKHK, KC_LGUI, KC_LALT, KC_MHEN,      KC_SPC     , KC_HENK, KC_KANA, KC_RALT,   MO_FN, KC_LEFT, KC_DOWN, KC_RGHT
  ),
  [KL_CURSOR] = LAYOUT_JP(
    ...
  ),
};
// result of process_record_user
# define PROCESS_OVERRIDE_BEHAVIOR   (false)
# define PROCESS_USUAL_BEHAVIOR      (true)
bool process_record_user(uint16_t keycode, keyrecord_t *record) 
{
  static uint16_t mem_keycode;
  uint16_t prev_keycode = mem_keycode;
  bool is_tapped = ((!record->event.pressed) && (keycode == prev_keycode));
  mem_keycode = keycode;
  switch (keycode) {
    case KC_MHEN: {
      if (record->event.pressed) {
        layer_on(KL_CURSOR);
      }
      else {
        layer_off(KL_CURSOR);
        if (is_tapped) {
          tap_code(keycode);
        }
      }
      return PROCESS_OVERRIDE_BEHAVIOR;
    } break;
    default: {
    } break;
  }
  return PROCESS_USUAL_BEHAVIOR;
}
process_record_userはキーコードが押されたとき、離されたときにそれぞれ呼び出されます。返り値としてfalseを返すと、キー動作を上書きしてデフォルトのキーコード送信をキャンセルします。返り値にtrueを返すと、QMKによる通常のキーコード送信を行います。
このため、それぞれの返り値をPROCESS_OVERRIDE_BEHAVIOR, PROCESS_USUAL_BEHAVIORとしてdefine定義しておきます。
引数のkeycode, record->event.pressedによって関数実行時のキーコードと押下状態を取得できるので、is_tappedにキーが単独でタッピングされたかを判定した結果を格納しておきます。
switch文へkeycodeがKC_MHENだった場合の処理を記述します。
- キーを押下した場合はKL_CURSORレイヤーを有効化(layer_on)、キーを離した場合はKL_CURSORレイヤーを無効化(layer_off)させます。
- キーがタッピングで離された場合は、元のkeycodeをtap_codeで送信します。
キーボードLEDの取得
'LED Control' より5つのLEDを取得できるという仕様ですが実用上は下記三つを使用できるでしょう。
// LED状態が変化時に呼ばれる
void led_set_user(uint8_t usb_led) 
{
  if (IS_HOST_LED_ON(USB_LED_NUM_LOCK)) {...}
  if (IS_HOST_LED_ON(USB_LED_CAPS_LOCK)) {...}
  if (IS_HOST_LED_ON(USB_LED_SCROLL_LOCK)) {...}
}
process_record_userにて空白文字入力を監視して、CapsLockを自動的に解除するということができます。
bool process_record_user(uint16_t keycode, keyrecord_t *record)
{
  if (IS_HOST_LED_ON(USB_LED_CAPS_LOCK)){
    switch(keycode){
    case KC_ENTER:
    case KC_TAB:
    case KC_SPACE:
      //register_mods(MOD_MASK_SHIFT);  // JISキーボードの場合はShiftが必要
      tap_code(KC_CAPS);
      //unregister_mods(MOD_MASK_SHIFT);
      break;
    }
  }
  return true;
}
コンボキー
'Combos' 機能を使うと、キーの同時押しに別のキーコード送信を割り当てることができます。
機能を有効化するにはrules.mkへCOMBO_ENABLE=yesを追加します。
COMBO_ENABLE      = yes
登録するコンボキーの数をCOMBO_COUNTで定義し、key_combos配列の要素数として定義します。
同時押し判定のタイムアウトはTAPPING_TERMがデフォルト値ですが、COMBO_TERMを定義すると、コンボキー専用のタイムアウト値として定義されます。
# define COMBO_COUNT         9
# define COMBO_TERM          500
ファンクションキーF11~F19の入力を、F10+F1~F9のコンビネーションとして割り当てた例を下記に記します。
typedef const uint16_t comb_keys_t[];
static PROGMEM comb_keys_t
  comb_keys_F11 = {KC_F10, KC_F1, COMBO_END}, comb_keys_F12 = {KC_F10, KC_F2, COMBO_END},
  comb_keys_F13 = {KC_F10, KC_F3, COMBO_END}, comb_keys_F14 = {KC_F10, KC_F4, COMBO_END},
  comb_keys_F15 = {KC_F10, KC_F5, COMBO_END}, comb_keys_F16 = {KC_F10, KC_F6, COMBO_END},
  comb_keys_F17 = {KC_F10, KC_F7, COMBO_END}, comb_keys_F18 = {KC_F10, KC_F8, COMBO_END},
  comb_keys_F19 = {KC_F10, KC_F9, COMBO_END};
combo_t key_combos[COMBO_COUNT] = {
  COMBO( comb_keys_F11, KC_F11 ),  COMBO( comb_keys_F12, KC_F12 ),
  COMBO( comb_keys_F13, KC_F13 ),  COMBO( comb_keys_F14, KC_F14 ),
  COMBO( comb_keys_F15, KC_F15 ),  COMBO( comb_keys_F16, KC_F16 ),
  COMBO( comb_keys_F17, KC_F17 ),  COMBO( comb_keys_F18, KC_F18 ),
  COMBO( comb_keys_F19, KC_F19 ),
};
- 
okapies氏のブログ記事を参考にさせていただきました(http://okapies.hateblo.jp/entry/2019/02/02/133953) ↩