自作キーボードの同時押しによる2段階レイヤーシフト

5 min read読了の目安(約4800字

前提

自作キーボードにいろんな意味でハマっています。

自作キーボードには40%と呼ばれるキーの数が少ないものがあります。物理的にキーの数が少ないと入力できる文字種=キーコードが少なくなるので、キーに対応するキーコードをまとめて入れ替えるレイヤーという概念を導入し、さらに特定の操作でレイヤーを切り替えることで十分な数のキーコードを確保します。そうして必要な文字をどのレイヤーのどのキーに置くのか、さらにはレイヤー切り換え自体をどのような操作で行うかの設定をキーマップもしくは単にマッピングと言い、自作キーボードにおける1つの沼となっています。この記事はそんなレイヤー切り換え関係のお話です。

もっともシンプルなレイヤー操作は「あるキーを押している間だけ特定のレイヤーにシフトする」というものです。自作キーボード界隈の代表的なファームウェア(=キーボード自身に内蔵するソフトウェア)であるQMK FirmwareではこれをMO(n)と表現します。次の画像はCorne Keyboardシリーズにおけるキーマップを示したものです。


キーマップの例

最下段中央付近の親指で押しやすそうな位置にMO(1)およびMO(2)が存在します。これらはそれぞれ「押している間だけレイヤー#1にシフトする」「押している間だけレイヤー#2にシフトする」というキーの機能です。なおレイヤーは0から始まる番号で識別し、QMKでは最大16層まで利用できます。

同時押しによる2段階レイヤーシフト

上述の例では左右の親指でそれぞれ別のレイヤーにシフトしていました。しかしこれを「どちらを押してもレイヤー#1に遷移する。しかし両方同時に押した場合はレイヤー#2に遷移する」という方式を考えてみましょう。これがタイトルにある「同時押しによる2段階レイヤーシフト」となります。

さてこの「同時押しによる2段階レイヤーシフト」は一見簡単に実現できそうです。レイヤー#0で両キーをMO(1)にし、レイヤー#1で両キーをMO(2)にすれば良いでしょう。次の画像はそのマッピングを示したモノです。


レイヤー#0のマッピング


レイヤー#1のマッピング

以下では説明の都合上、右のレイヤーシフト用のキーをRと、左のモノをLと表記します。

ここで実際にキーを押した際にどうレイヤーシフトが行われるのかを検討してみましょう。まずRを押すとレイヤー#0のRはMO(1)なのでレイヤー#1にシフトします。続いてレイヤー#1でLはMO(2)ですから、押せばレイヤー#2にシフトします。逆にLから押し始めた場合でも、レイヤー#0のLはMO(1)なのでレイヤー#1にシフトし、レイヤー#1のRはMO(2)なのでレイヤー#2にシフトします。「同時押しによる2段階レイヤーシフト」ができていることがわかるでしょう。

些細な問題

ここまではキーを押してレイヤーをシフトすることを考えていましたが、次にキーを離した際にどうシフトするかを考えましょう。結論を先に言うとこのままではキーを離す順番次第で意図しないレイヤーにシフトしてしまうケースがあります。具体的な再現手順は「Rを押して、Lを押して、Rを離す(Lは押したまま)」で、こうするとレイヤー#2になってしまいます。これは片方しか押してないのだからレイヤー#1であって欲しい人間の気持ちとはズレています。

なぜこうなるのかを知るにはQMKがどのようにアクティブなレイヤーを管理しているかを知る必要があります。具体的にはQMKのlayer_on()とlayer_off()の実装をみると一目瞭然で、各レイヤーに対応したビットを立てたり寝かせたりしているのです。以下のコードはその抜粋です

void layer_on(uint8_t layer) { layer_state_set(layer_state | (1UL << layer)); }

void layer_off(uint8_t layer) { layer_state_set(layer_state & ~(1UL << layer)); }

加えてQMKはデフォルトではレイヤーシフトキーを離した際にどのレイヤーをlayer_off()するのかは、レイヤーシフトキーを押した際にどのレイヤーをlayer_on()したかを覚えておいて決めています。つまりR,Lという順序で押した場合、Rはレイヤー1をLはレイヤー2をオンにしたので、前の例のように先にRだけを離すとレイヤー1がオフになりレイヤー2はオンのままになり、意図とはズレてしまいます。

これをビット表現で書くと以下の表のようになります。QMKではレ押されたキーからキーコードを決定する際にアクティブなレイヤーを最左の高い順に調べ、キーコードが設定されていればそれを使い、設定されていなければ(=KC_TRNSならば)より低いアクティブなレイヤーのものを使うとなっています。

操作 レイヤーのビット表現 備考
(初期状態) 0b000 レイヤー#0としてふるまう
Rを押す 0b010 レイヤー#1としてふるまう
Lを押す 0b110 レイヤー#2としてふるまう
Rを離す 0b100 レイヤー#1ではなくレイヤー#2となる

さてここまで書いてきましたがシンプルな解決法はありません。とはいえ同時押しからRだけ離してレイヤー#1を使うなんて器用なことをすることも、実用上はないでしょうから無視して良い範囲とも言えるでしょう。

些細な問題のおおげさな解決法

この些細な問題の解決法もすでに思いつき実装して機能することを確認しました。ただし解決したい問題に対して手段が複雑すぎるので軽く触れる程度に解説します。

まずレイヤー#2をレイヤー#3に追加・移動します。次にレイヤー#1をレイヤー#2に複製し、Rでシフトした場合とLでシフトした場合で分けます。以下では説明の都合上それぞれレイヤー#1をR用、レイヤー#2をL用にします。レイヤー#0,#1,#2,#4の全部のRはMO(1)にし、Lは同様にすべてMO(2)にします。

そのうえでconfig.hに#define STRICT_LAYER_RELEASEを追加します。このSTRICT_LAYER_RELEASEはレイヤーシフトキーを離した際にオフにするレイヤーをシフト先のレイヤーのキーで決める機能です。レイヤー#1,#2,#3のRにMO(1)を割り当てたのは、Rを離した際には常にレイヤー#1をオフすることを意図しています。

余談ですがドキュメントによってはレイヤーシフトキーはシフト先のレイヤーではKC_TRNSにするべきと書かれています。これはSTRICT_LAYER_RELEASEを意識したもので、KC_TRNSにすることでキーを離す際にオフにするレイヤーを正しく設定できるようにするものと推測されます。逆に考えるとSTRICT_LAYER_RELEASEを使わないのであればこれは必要ありません。それどころか前述した通り、KC_TRNSは「1つ前にいたレイヤー」ではなく「現在のレイヤーよりも低く、アクティブなレイヤーのうち最も高いレイヤー」に委譲する機能なので、レイヤー構成によっては意図通りに機能しない場合があります。

仕上げにレイヤー状態をレイヤー#1と#2が同時にオンになった際にのみレイヤー#3をオンにするコードを追加します。またレイヤー#1か#2のどちらかがオフならば#3もオフにするように、と考えると以下のようなコードになります。

layer_state_t layer_state_set_user(layer_state_t state) {
    if ((state & 0b0110) == 0b0110) {
        state |= 0b1000;
    } else {
        state &= ~0b1000;
    }
    return state;
}

ビット表現をみると次の表のようになります。Lを押した時点でlayer_state_set_user()state引数は0b0110ですが、関数内のifの条件に適合し0b1110に書き換えられます。またRを離した際はstate引数は0b1100ですがelse側で0b0100に書き換えられます。

操作 レイヤーのビット表現 備考
(初期状態) 0b00000 レイヤー#0としてふるまう
Rを押す 0b0010 レイヤー#1としてふるまう
Lを押す 0b1110 レイヤー#3としてふるまう。
Rを離す 0b0100 レイヤー#2=実質レイヤー#1としてふるまう

このコードに必要なヘッダー読み込みを追加してlayer.cと名前を付け保存し、rule.mkに SRC += layer.c としてコンパイルすれば当初目的の「同時押しによる2段階レイヤーシフト」は細部まで正しく実現できます。ただしやりすぎ感は否めません。

今後の課題: よりシンプルな解決策を

ほぼ影響のない不具合のためにココまでするのは大げさすぎるでしょう。特にレイヤーのコピーを持たなければならない点への不満が大きいです。いずれよりシンプルな修正が実現できたらその方法をここに追記します。忘れてなければ。

まとめ

  • 自作キーボードにおける「同時押しによる2段階レイヤーシフト」という機能を紹介しました
  • その実装方法と些細な問題点、およびあまり現実的とは言えない解決法を示しました
  • より良い解決方法は今も模索中です