見出し画像

自作キーボードとは、己のマトリックスを築き、自らスキャンすること也

前回の続きです。

キーボードにたくさんあるスイッチ。マイコンにスイッチの数だけのIOポートはないので、マトリックスを組んでIOピンを減らすという話。電子工作の定番ネタでもあります。

Pro Microを見ると、デジタル入出力に使えるピンが18本。
12本と書いてあるところも見かけますが、公式のチュートリアルを見ると "The Pro Micro's I/O pins -- 18 in all -- are multi-talented." とのことなので、ここは18本と信じましょう。
(基板上のシルク印刷だけを見ると12本に見えますね)

そのうちBluetooth対応のためにAdafruit Bluefruit LE SPI FriendとSPIで通信しないといけないのでピンが3本埋まってしまいます。残りは15本。

バックライトのコントロール用に1本。残り14本。
14本なら、7×7=49個のスイッチのON/OFFがスキャンできるはず。

今回目標にしているキーボードは以下のようなもの

自作キーボードのイメージ

キーの数は48個。
おっ、え、まさか、奇跡的にいけるやん!


そんな装備で大丈夫か?

「大丈夫だ。問題ない」
『El Shaddai - エルシャダイ -』の中で、ルシフェルに向かってイーノックはそう発します。
そして「神は言っている、ここで死ぬ運命ではないと・・・」
時は巻き戻り、再び「そんな装備で大丈夫か?」と問いかけるルシフェルに、イーノックは「一番いいのを頼む」と返すのです。

まだ実際のチップもボードも触っていない状態で、IOピンが一つも残っていないのはちょっとまずい気がします。
こんな装備で大丈夫なのでしょうか?
それに、よく考えると、筆者がこれから作ろうとしているのは40%(+α)キーボードです。もっとキー数の多いキーボードを作っている人たちはどうしているのでしょう?
どうも、このまま「大丈夫だ。問題ない」といいながら進んでいくのはちょっと違う気がしてきました。

あまり先のことは気にせずに、とにかく手を動かしてみるというのも手段の一つではありますが、ここはもう少し、キー数が増えたとき、みんなどう対処しているの?というところまで探ってみたいと思います。

手段は2つ

一つ目。わかりやすい解決策として、分割キーボードにするという手がありそうです。
QMK Firmwareは分割キーボードに対応しています。

外観上は分離していなくても、Pro Microを2つ使えば、内部でキーマトリクスを分離することができます。
ただし、2つのPro Micro同士が通信する必要があり、このため双方のIOピンが最低でも1ピンずつ埋まります。I2C通信をしようとすれば2ピンずつ埋まってしまいます。
I2Cでどんどんつなげて行ければ面白いのですが、どうもそういうわけではなさそうです。

二つ目に、IOピンだけを増やす方法として、IOエキスパンダーがあります。

例えば、このPCF8575を使うとすると、Pro Micro側は、I2C通信のために2ピン塞がりますが、IOピンが16本増えます。
このIOエキスパンダーは、アドレスを変更すれば同じI2Cピンを使って8枚接続可能なので、I2C通信のために2ピン失うだけで、16×8=128ピン増設することができます。つまり、128÷2=64ですから、理屈の上では64×64=4096個のキーをスキャンできることになります。
こいつは、いわば自作キーボード界のヘカトンケイルシステム(※)。
4本の腕を同時に操作したり、空母を丸ごと制御することはできそうにないですが、面白そうなので、IOエキスパンダーを使う方向で検討したいと思います。

※参考:アップルシード - Wikipedia

IOエキスパンダー

PCF8575について調べてみました。

PDFのデーターシートを見ると、冒頭、3 Description のところに、"Each quasi-bidirectional I/O can be used as an input or output without the use of a data-direction control signal." と書かれています。

"without the use of a data-direction control signal"(データ方向制御信号を使用することなく)って? 各IOピンの入力or出力を指定する手順が書かれていることを期待して読み始めたので、いきなり「え、いらんの?」って、なりました。
はじめにI2C通信で初期化する必要があるのだろうと思い、初期化手順でも確認しておこうかなと思ったのですが、そういうものではないようです。

書き出せば出力ピン、読み込めば入力ピン

どうやら、とくに入力・出力を切り替える設定はなく、基本的にはデータを書き出せば出力ピン、読み込めば入力ピンとして動作するようです。

PCF8575の構成
https://www.ti.com/lit/ds/symlink/pcf8575.pdf

データシートで回路構成を見ると、I2Cバスコントローラーがシフトレジスタにつながっていて、シフトレジスタはそのままI/Oポートにつながっています。

I2C通信で16ビット分のデータを送れば、シリ・パラ変換されてそのまま各ピンの状態として書き出され、I2C通信で16ビット分のデータを読み込めば、そのときのピンの状態がパラ・シリ変換されて返ってくるという仕組みです。わかりやすい!

ただし、データシートの説明では、ピンがLowになっている状態で外からHighを書き込むと、GNDに向けて大電流が流れてしまうので、入力に使うピンはHighにしろと書かれています。

I/Oポート周辺の回路構成
https://www.ti.com/lit/ds/symlink/pcf8575.pdf

IOピン周辺の回路を見ると、確かにLow側のトランジスタにはソース側にもドレイン側にもプルダウン抵抗にあたるものが付いていません。
したがって、入力として使うときは、Highにプルアップされているもの(アクティブ・ロー)として扱わなくてはいけないということですね。

同様に、使わないピンもHighにするようにと書かれていますので、このあたりはソフトウェアを考える時に意識しておく必要がありそうです。

なんとなくIOエキスパンダーの使い方はわかってような気がしてきましたが……
で、これ、QMK Firmwareの中で扱うにはどうすればいいのでしょう?

QMKのコードについて理解を試みる

ヒントは公式ドキュメントにちゃんと書いてありました。

QMK のコードの理解』というページの『メインループ』の項目を読むと、下記のように書いてあります。

ここで、全てのキーボードの固有の機能が実行されます。keyboard_task() のソースコードは tmk_core/common/keyboard.c にあり、マトリックスの変化を検知し、LED の状態をオンオフする責任があります。

QMK のコードの理解

更に、keyboard_task()で処理される内容として、下記の5つが挙げられています。

  • マトリックスのスキャン

  • マウスの処理

  • シリアルリンク

  • ビジュアライザ

  • キーボードの状態の LED (Caps Lock, Num Lock, Scroll Lock)

出てきましたね、マトリックスのスキャン。
実際にマトリックスのスキャンで行っている内容にも触れられています。

マトリックスの現在の状態を求めると、以下のようなデータ構造を取得します:

マトリックスのスキャン
{
    {0,0,0,0},
    {0,0,0,0},
    {0,0,0,0},
    {0,0,0,0},
    {0,0,0,0}
}

これは 4行x5列のテンキー(訳注: 5行x4列の間違いと思われます)のマトリックスを表す直接的な表現のデータ構造です。キーが押されると、マトリックス内のそのキーの位置が、 0 ではなく 1 として返されます。

マトリックスのスキャン

状態変更の検知』の項でも少し触れられていますが、ここで重要な情報が得られました。スキャンの結果は、一旦、上記のような0と1の2次元的な配列の形で保存されることがわかりました。

これをイメージしながら、tmk_core/common/keyboard.cの該当箇所を見てみます。

void keyboard_task(void)から下にコードを追っていくと、matrix_scan()という関数を呼んでいます。
おっ、"matrix_scan"!!臭いですね。

GitHub上でmatrix_scan()関数をクリックすると、右側にmatrix_scan()に関連する箇所のリストがずらりと表示されます。

matrix_scan()関数

どうやら、多くのキーボードで専用のmatrix_scan()関数を作っているようですね。

カスタムマトリックス

なるほど、独自のmatrix_scan()関数を作ってしまえばいいのか。
そんなことを考えながら、再度、公式ドキュメントを眺めていると『カスタムマトリックス』という項目に、しっかりmatrix_scan()関数の置き換え方が載っていました。
なるほど、カスタムマトリックスって、そういうことだったのですね。

rules.mkに、"CUSTOM_MATRIX = yes"を指定して、matrix.cファイルに下記のようなコードを書けばよいとのこと。

matrix_row_t matrix_get_row(uint8_t row) {
    // TODO: 要求された行データを返します
}

void matrix_print(void) {
    // TODO: printf() を使って現在のマトリックスの状態をコンソールにダンプします
}

void matrix_init(void) {
    // TODO: ここでハードウェアとグローバルマトリックスの状態を初期化します

    // ハードウェアによるデバウンスがない場合 - 設定されているデバウンスルーチンを初期化します
    debounce_init(MATRIX_ROWS);

    // 正しいキーボード動作のためにこれを呼び出す*必要があります*
    matrix_init_kb();
}

uint8_t matrix_scan(void) {
    bool changed = false;

    // TODO: ここにマトリックススキャンルーチンを追加します

    // ハードウェアによるデバウンスがない場合 - 設定されているデバウンスルーチンを使用します
    changed = debounce(raw_matrix, matrix, MATRIX_ROWS, changed);

    // 正しいキーボード動作のためにこれを呼び出す*必要があります*
    matrix_scan_kb();

    return changed;
}

なるほどなるほど。
皆さんこれに沿ってmatrix_scan()関数を書き直しているのですね。

なんとなく流れはわかってきました。

(続く)

頂いたサポートは今後の記事作成のために活用させて頂きます。