QMK Firmware: Understanding Split Keyboards

QMK Firmware での分割キーボードの仕組み

皆さんお久しぶりです。気が付いたら2年以上もブログを放置していました。X(Twitter)では、多少呟いてたりはしてたのですが。

キーボードも細々と作っていまして、先日はキー部7%に作りかけのキーボードを持ち込んでみたりしました。

youtu.be

(Daifuku Keyboard さんのアーカイブです。)

 

パームレストに仕込んだタッチセンサで、キーボードモードとトラックボールモードを切り替える点が意外に評判良くて、ちょっと気を良くしました!が、他の方の作品は、どれも見た目も美しくて、私にはとても無理なので落ち込んだり。

まぁ、何れにせよ刺激になりました。スタッフの皆さん、参加された皆さん、ありがとうございました。

 

さて、この作りかけのキーボードでは、上述のとおりマスタ側である右側のキーボードにタッチセンサを組み込んであるんですが、どうせななら、スレーブ(左)側にもタッチセンサを組み込みたい、と思ったのですが、そもそもQMKでの分割キーボードの仕組みが良く分かっていない。仕方がないので、QMKの関連しそうなソースをちょっと探ってみました。

 

QMKの処理の流れ

まず、QMKの処理の大きな流れは、Understanding QMKに書いてありまして、ざっくり言うと、メインループから、protocol_task()を呼び、そこから、keyboard_task()を呼び、keyboard_task()がマトリクスのスキャニング、マウスのハンドリング、LEDなどのキーボードステータスの処理を行う、とあります。より具体的な関数の呼び出し階層なんかも書いてますが、これはマスタ側の処理で、スレーブ側ではどういう処理をしているのかまるで書いていません。しかも、どういうデータが通信されるのかも書いていません。

で、その辺どうなってるの?というのがこの記事の主旨です。

 

分割キーボードのデータ構造

まず、QMK内部で分割キーボードの各種データはどのような構造になっているか見てみます。

qmk_firmware/quantum/split_common/transport.h

に _split_shared_memory_t という構造体が定義されていて、

typedef struct _split_shared_memory_t {
#ifdef USE_I2C
    int8_t transaction_id;
#endif // USE_I2C

    split_slave_matrix_sync_t smatrix;

#ifdef SPLIT_TRANSPORT_MIRROR
    split_master_matrix_sync_t mmatrix;
#endif // SPLIT_TRANSPORT_MIRROR

(以下略)

といった感じになっています。ここで、smatrixがスレーブ側のキーボードマトリクスの値、mmatrixがマスタ側のマトリクスの値になります。

他のLEDの情報やポインティングデバイス等の情報もマスタ、スレーブそれぞれのデータ構造が定義されています。

マスタ・スレーブそれぞれはこのshared memoryの自信の担当領域にマトリクスのスキャン結果などを書き込んでいき、(必要に応じて)特定のタイミングで他方のマトリクススキャン結果を要求して、送られてきた通信結果をshared memoryに書き込み、その後の処理を行うという流れになります。

マトリクスに関しては、これらのshared memoryのデータは、matrix.c等で用いられる、

extern matrix_row_t matrix[MATRIX_ROWS]; 

に(左、右の順で)マップされていて、マスタ・スレーブのどちらのマトリクスであるか意識することなく、読み出して、それをもとに処理(例えばLED処理)できるようになっています。

マトリクスの値を変更したい場合も、このmatrixのマスタ・スレーブそれぞれの担当の領域に書き込みすれば、他方が読み出した時に、転送されるようになっています。(自身の担当外の領域は、読み出し専用で、書き込んでも、相手側のデータは反映されないようです。)

 

スレーブ側のマトリクススキャン処理の流れ

マスタ側は先に見たようにUnderstanding QMKのとおりなのですが、スレーブ側も基本的な流れは同様のようです。(#defineされた機能の定義によりますが)

マスタと同様、matrix.c の matrix_scan()が呼ばれ、マトリクスをスキャンします。この中で、分割キーボードの場合は、matrix_common.c の matrix_post_scan()が呼ばれ、#defineされた内容に応じて、必要なマスタ側のデータを取得したのち、matrix_slave_scan_kb() が呼ばれ、さらに matrix_slave_scan_user() が呼ばれます。

matrix_slave_scan_user()は

__attribute__((weak)) void matrix_slave_scan_user(void) {}

と定義されているので、ユーザは、このmatrix_slave_scan_user()を(keymap.cの中などで)オーバーライドして、そこでマトリクスに読み書きすることができます。例えば、(マトリクススキャンされない)タッチキーのオン・オフデータをマトリクスに反映させるためには、このmatrix_slave_scan_user()中で、matrixの所望の位置のビットを操作すればよいことになります。

matrix[]のデータの持ち方は、キーボードによりますが、テンプレートから生成されたものの場合、左側キーボードは奥がmatrix[0]で、手前に向かって引数が1ずつ増加、LSBが左端に対応、右側キーボードは左側の次の引数から手前に向かって引数が1ずつ増加、LSBが右端に対応かと思います(たぶん。これはキーボードによるので、適宜確認してください。)

私の場合、この操作でスレーブ側のタッチキーのオン・オフをマトリクスに反映し、マトリクス側のキー操作に対応することができました。

 

マトリクスについては以上ですが、LED等の他の機能についても、QMKは一体型のキーボードを基本としているようで、各機能毎にそれぞれ分割型の処理がそれぞれ実装されており、上のレイヤでは、分割型・一体型の違いを意識させずにプログラミングできるようになっているようです。逆にいうと、スレーブ側でちょっと変わったことをしようとすると、いろいろ調べて自前で実装しないといけないようです。分散型ではなく中央集権的、といった感じでしょうか。(これはこれである意味わかりやすいのですが、個人的な趣味とは違うというか。)