QMK Firmware で DIP スイッチを使う方法

前置き

現在設計中のキーボードに DIP スイッチでデフォルトレイヤーを切り替える機能を登載するのですが、公式リファレンスの説明だけではつまずいてしまいそうなポイントもあったため、備忘録として使い方をまとめます。

環境

QMK Firmware

1❯ qmk --version
21.1.5

マイコン

RP2040 を登載しているボードを使います。なお、テストに使ったマイコンボードは以下の3つです。

実装

ここから実装方法を書いていきますが、昨今の QMK Firmware の Data Driven Configuration | QMK Firmware の考え方に従って、可能な限り (keyboard_name).json に設定を記載する方法で進めていきます。なお、毎回 (keyboard_name).json とカッコ付きで書くのは面倒なので、以下では keyboard.json と書いていきます。

keyboard.json

DIP スイッチを使う場合、keyboard.json に次の設定を追加します。ちなみに、DIP スイッチをキーマトリックスの中に組み込む方法もあるようですが、今回は試していません。

1"dip_switch": {
2    "enabled": true,
3    "pins": [ "GP26", "GP27" ]
4},

"enabled: true" が DIP スイッチの機能をオンにする設定で、"pins: []" が DIP スイッチを接続するピンを指定する設定です。ここではスイッチが2つある DIP スイッチを使うため、ピンを2つ指定しています。

なお、従来どおりの rules.mkconfig.h で設定する場合、以下のとおり設定します。

1# rules.mk
2DIP_SWITCH_ENABLE = yes
1// config.h
2#define DIP_SWITCH_PINS { GP26, GP27 }

ハードウェアの設定

DIPスイッチの片側を keyboard.json に設定したピンに接続し、もう片方を GND に接続します。

DIP スイッチの配線

DIP スイッチ自体はただのスイッチなので、DIP スイッチのどちらのピンを config.h に設定したピンに接続してもOKです。

QMK Firmware の設定

これで DIP スイッチを使う準備ができましたので、DIP スイッチを切り替えた時の動作を設定します。

DIP スイッチを切り替えた時の動作については、キーコードを送信するだけで足りる場合と、それだけでは足りない場合で設定が変わります。まず、キーコードを送信するだけで足りる場合の設定を解説します。

キーコードを送信するだけで足りる場合

キーボードの keymaps/(keymap)/rules.mk に以下の設定を追加します。

1DIP_SWITCH_MAP_ENABLE = yes

それから keymap.c に以下の設定を追加します。ここでは、以下の2つの設定を行っています。

  • 1つ目の DIP スイッチをオンにした時に DF(1) を送信してレイヤー1をデフォルトレイヤーにし、オフにした時に DF(0) を送信してレイヤー0をデフォルトレイヤーにする。
  • 2つ目の DIP スイッチをオンにした時に AG_LSWP を送信して LALTLWIN を入れ替えて、オフにした時に AG_LNRM を送信して入れ替えを元に戻す。
1#if defined(DIP_SWITCH_MAP_ENABLE)
2const uint16_t PROGMEM dip_switch_map[NUM_DIP_SWITCHES][NUM_DIP_STATES] = {
3    DIP_SWITCH_OFF_ON(DF(0), DF(1)),
4    DIP_SWITCH_OFF_ON(AG_LNRM, AG_LSWP),
5};
6#endif

複数の処理を実行する場合

スイッチを切り替えたら複数の処理を実行する、スイッチを切り替えたら OLED の表示を変更する、といった対応するキーコードが無い処理については、スイッチ切り替えに合わせてコールバック処理を実行することで対応します。コールバック処理を行う場合、各スイッチの状態を個別に管理して条件分岐する方法と、スイッチの状態をまとめて管理して条件分岐する方法の2通りの方法がありますので、それぞれ説明します。

なお、いずれの場合であっても keymaps/(keymap)/rules.mkDIP_SWITCH_MAP_ENABLE = yes を追加しているとコールバック処理が呼ばれませんので、この設定は削除してください。この点は本記事執筆時点の公式リファレンスでは明記されていませんので、注意してください。私はこの点になかなか気付かず苦労しました。

各スイッチの状態を個別に管理する方法

keymap.cdip_switch_update_user(uint8_t index, bool active) 関数を追加します。

引数の index はスイッチの順番で、keyboard.jsondip_switch.pins で指定した順番になります。config.h で指定している場合は #define DIP_SWITCH_PINS {} で指定した順番になります。active はスイッチがオンなら true になります。

上で紹介した処理をコールバック処理で実装した場合のコードは次のとおりとなります。なお、ここではデフォルトレイヤーを切り替える関数の set_single_persistent_default_layer と、LALT と LWIN を入れ替えるフラグの keymap_config.swap_lalt_lgui を利用しています。

 1bool dip_switch_update_user(uint8_t index, bool active) {
 2    switch (index) {
 3        # 1つ目のDIPスイッチのオン・オフでデフォルトレイヤーを切り替え
 4        case 0:
 5            if (active) {
 6                set_single_persistent_default_layer(1);
 7            } else {
 8                set_single_persistent_default_layer(0);
 9            }
10            break;
11        # 2つ目のDIPスイッチのオン・オフで LALT と LWIN を入れ替え
12        case 1:
13            keymap_config.raw = eeconfig_read_keymap();
14            if (active) {
15                keymap_config.swap_lalt_lgui = true;
16            } else {
17                keymap_config.swap_lalt_lgui = false;
18            }
19    }
20    return true;
21}
スイッチの状態をまとめて取り扱う方法

bool dip_switch_update_mask_user(uint32_t state) を使うと、各スイッチの状態をビット列とみなした上で、そのビット列が表す値が引数の state に格納されます。例えば、スイッチの状態が次表の場合の state1 となります。

スイッチ番号スイッチの状態ビット値
スイッチ1ON1
スイッチ2OFF0

また、スイッチの状態が次表の場合 state3 となります。

スイッチ番号スイッチの状態ビット値
スイッチ1ON1
スイッチ2ON1

このように state の値で各スイッチの状態を管理できますので、あとは switch 文で条件分岐して処理を指定します。

 1bool dip_switch_update_mask_user(uint32_t state) {
 2  switch (state) {
 3    case 0:
 4      // 
 5      break;
 6    case 1:
 7      // 
 8      break;
 9    case 2:
10      // 
11      break;
12  // ...
13  }
14  return true;
15}

なお、state の値を利用せずにスイッチの状態を直接ビット演算を使って管理する方法もあり、公式リファレンスで説明されています。

 1bool dip_switch_update_mask_user(uint32_t state) { 
 2    if (state & (1UL<<0) && state & (1UL<<1)) {
 3        layer_on(_ADJUST); // C on esc
 4    } else {
 5        layer_off(_ADJUST);
 6    }
 7    if (state & (1UL<<0)) {
 8        layer_on(_TEST_A); // A on ESC
 9    } else {
10        layer_off(_TEST_A);
11    }
12    if (state & (1UL<<1)) {
13        layer_on(_TEST_B); // B on esc
14    } else {
15        layer_off(_TEST_B);
16    }
17    return true;
18}

補足情報

分割型キーボードの右手側の DIP スイッチの管理

公式リファレンスでは分割型キーボードの右手側にある DIP スイッチを個別に管理する場合の設定も紹介されていますが、私が試したところ、右手側のスイッチを切り替えても dip_switch_update_user()dip_switch_update_mask_user() 関数が実行されませんでした。

試行錯誤の過程でマイコンボードを ProMicro に変更したり、左右判定の設定を変更したりしましたが、原因は突き止められませんでした。ただ、DIP スイッチは片方にあれば足りるので、左右に DIP スイッチを配置するのはあきらめました。

デフォルトレイヤーを切り替える方法

デフォルトレイヤーの切り替えは DF(layer) キーを送信すれば可能ですが、このキーは tap_code() では送信できません。

そのため、上記の dip_switch_update_user() を使う処理でのデフォルトレイヤーの切り替えは set_single_persistent_default_layer() 関数で処理しています。

LALT と LWIN キーを入れ替える方法

LALT と LWIN キーは QK_MAGIC_SWAP_LALT_LGUIAG_LSWP キーで入れ替えできます。また、QK_MAGIC_UNSWAP_LALT_LGUIAG_LNRM キーで元に戻せますが、このキーも tap_code() では送信できません。

そこでキーの送信以外で入れ替えを実現できないか調べたところ、QK_MAGIC_SWAP_LALT_LGUIkeymap_config.swap_lalt_lgui 変数を true/false にすることでキーの入れ替えを実現していることが判明しましたので、上記のコードでは keymap_config.swap_lalt_lgui 変数を true または false にして LALT と LWIN キーの入れ替えを実現しています。

なお、QK_MAGIC_SWAP_LALT_LGUIquantum/process_keycode/process_magic.c で以下のとおり定義されています。

 1// quantum/process_keycode/process_magic.c
 2// 関係するコードのみ抜粋しています
 3
 4/**
 5 * MAGIC actions (BOOTMAGIC without the boot)
 6 */
 7bool process_magic(uint16_t keycode, keyrecord_t *record) {
 8    // skip anything that isn't a keyup
 9    if (record->event.pressed) {
10        if (IS_MAGIC_KEYCODE(keycode)) {
11            /* keymap config */
12            keymap_config.raw = eeconfig_read_keymap();
13            switch (keycode) {
14                case QK_MAGIC_SWAP_LALT_LGUI:
15                    keymap_config.swap_lalt_lgui = true;
16                    break;
17                case QK_MAGIC_UNSWAP_LALT_LGUI:
18                    keymap_config.swap_lalt_lgui = false;
19                    break;

デフォルトレイヤーの確認方法

例えば、DIP スイッチのオン・オフでデフォルトレイヤーを切り替える(QWERTY 配列レイヤー ⇔ COLEMAK 配列レイヤー)場合、QMK の機能でデフォルトレイヤーのレイヤー番号が取得できると LED インジケータの変更などに利用できそうです。

しかし、MO(layer) などによる一時的なレイヤーの切り替えでは get_highest_layer(layer_state) 関数で使用中のレイヤー番号を取得できますが、デフォルトレイヤーのレイヤー番号は常に 0 になるため、どのレイヤーをデフォルトレイヤーにしているか調べる方法は無い模様です。

そのため、デフォルトレイヤーの切り替えで LED インジケータなどを変更する場合、デフォルトレイヤーの切り替え時に「どのレイヤーをデフォルトレイヤーに設定したか」という情報を保存し、インジケータの更新処理などでその変数を参照して対処するしかなさそうです。

なお、デフォルトレイヤーのレイヤー番号が常に 0 になるという点については、QMK のコントリビュータの Drashna 氏が以下の Issues で発言しています。私の方でも QMK のデバッグ機能でデフォルトレイヤー切り替え時に get_highest_layer(layer_state) 関数が返す値を確認したところ、常に 0 になるのを確認しています。

# [Bug] default_layer_state is 0 when the keyboard is powered up after flashing.