弊社では業務PCとしてノートPCが支給されますが、外付けキーボードやマウスを利用したい方は追加支給してもらうこともできます。ですが自分好みのキーボード・マウスを利用したい方は、各人の責任で持ち込んで利用することも認められています。
その流れで私は自作したキーボードを業務PCに接続して使っていますが、そのキーボードを作成した際の知見を少しまとめました。
ロータリーエンコーダーとは
回転する軸の角度の変化を入力として扱うデバイスです。
回転軸を扱うものとしては他に
- 固定した範囲の中で角度をアナログ値として扱うボリューム
- いくつかに区切った角度をDIPスイッチのように扱うロータリースイッチ
などがありますが、ロータリーエンコーダーは無限角度、相対変化と言う特徴があります。
PC関連では古くは機械式マウスや、最近ではマウスホイールなどで用いられています。
ロータリーエンコーダーの特性
電気的な接点を使うものでは、キーボードのキースイッチと同じようにバウンス(チャタリング)が発生します。デバウンスの影響でA相B相の推移順が崩れてしまうと一瞬止まったようになったり逆行してしまったりします。
私は自作キーボードでqmk_firmware(Quantum Mechanical Keyboard)というオープンソースのファームウェアとその派生物を使っていますが、キースイッチのマトリクスを処理する場面ではソフト的に変化を測定して対処する処理が quantum/debounce/ の中に記述されています。
これに対してロータリーエンコーダーの処理は drivers/encoder/encoder_quadrature.c に encoder_wait_pullup_charge() と言う関数が記述されていることなどから、ハードウェア側でコンデンサーを使った対応を想定していることが分かります。このため、エンコーダーのA相B相で2本のGPIOが必要でコンデンサーのチャージを考慮して可能な限り専有させておく必要があります。
また、キースイッチのようにマトリクスを考慮するならチャージ時間より切替サイクルを長くとる必要があるのではないでしょうか。
このように、qmkではロータリーエンコーダーのドライバはデバウンス処理済のデータを出力する必要があります。
キーマトリクスへの組み込み
このような背景があるロータリーエンコーダーですが、キーボードを設計し配線していると、どうしても配線が使うスペースが無駄に広い印象を持ってしまい、どうせスイッチなのだからキーマトリクスと共用できないのか?と試してみました。
(前提としまして使っているマイコンはRP2040のため、処理速度が比較的速いためうまくいっている可能性がありますので、他のマイコンをご利用される際には予めご自身での検証をお願いします。)
普通のキースイッチとロータリーエンコーダーが違うところは電気接点として見るとA相B相のGNDが共有されている点だけとなります。私が日頃使うCOL2ROWではGND端子をCOL側に接続してA,Bの端子の先にダイオードをつけて異なるROWに接続すると問題が無さそうです。

マトリクスのキーの状態は quantum/keyboard.c から呼び出された matrix_common.c, matrix.c の中でスキャンされてデバウンスした後、matrix[]に保存されています。このデバウンスされた入力結果をロータリーエンコーダーの処理に引き渡せばいいのではないでしょうか?
と言うことで、ロータリーエンコーダーのドライバーを上書きします。
まずはマトリクススキャン時に必要な接点の情報を保存します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// エンコーダーの総数を定義 #define NUM_ENCODERS 2 // エンコーダーのキー位置を配列で定義 static const uint8_t encoder_a_rows[NUM_ENCODERS] = {1,1}; static const uint8_t encoder_a_cols[NUM_ENCODERS] = {7,6}; static const uint8_t encoder_b_rows[NUM_ENCODERS] = {0,0}; static const uint8_t encoder_b_cols[NUM_ENCODERS] = {7,6}; // スキャン結果を保存するバッファ bool encoder_a_state[NUM_ENCODERS] = {0}; bool encoder_b_state[NUM_ENCODERS] = {0}; // スキャン生データを取得 extern matrix_row_t raw_matrix[MATRIX_ROWS]; inline bool raw_matrix_is_on(uint8_t row, uint8_t col) { return (raw_matrix[row] & ((matrix_row_t)1 << col)); } // キーマトリクスのスキャン結果から必要分を退避 void matrix_scan_user(void) { // エンコーダーのピンの状態を記録 for (uint8_t i = 0; i < NUM_ENCODERS; i++) { encoder_a_state[i] = raw_matrix_is_on(encoder_a_rows[i], encoder_a_cols[i]); encoder_b_state[i] = raw_matrix_is_on(encoder_b_rows[i], encoder_b_cols[i]); } } |
また、ロータリーエンコーダーのドライバが値を読み取る関数が(weak)定義なので、これを上書きします。
回りくどくドライバ関数を上書きしているように見えるかもしれませんが、encoder_quadrature_read_pin() の結果はencoderの処理のなかでソフトウェア的なキューとデバウンスの処理をしているように見えたので、できるだけ通常の処理に合わせるためにこのようにしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// お勧めのコンデンサチャージ時間を待機してくれる便利関数 // とりあえずつぶしておく void encoder_wait_pullup_charge(void){ } // 固定ピンのためにinput highにしてしまうからつぶしておく void encoder_quadrature_init_pin(uint8_t index, bool pad_b){ } // エンコーダーのピン状態を記録したテーブルを読み込んで 0 か 1 を返す uint8_t encoder_quadrature_read_pin(uint8_t index, bool pad_b){ uint8_t ret; if(pad_b){ ret = encoder_b_state[index]; } else { ret = encoder_a_state[index]; } return ret; } |
マトリクスの中のスイッチがONになると設定されたキーコードが発行されてしまうのですが、これを避けるための手段は2つあり、一つは process_record_user() を定義して目的のキーコードの場合にfalseを返して以後の処理をスキップする方法、
もう一つは matrix_mask を定義してスキップしたい接点のビットをオフにする方法です。
対象のキーボードですでにどちらかの手段を利用している場合はその中に組み込むのが効率が良いと思います。どちらか迷った場合は matrix_mask の方が設定が少し複雑ですが処理するための計算量が少ないのではないでしょうか。
また、quantum/ 側の encoder_update_user() でfalseを返すと通常の処理をスキップできるため、この中で目的の処理を行います。
1 2 3 4 5 6 7 8 |
// エンコーダーのイベント処理(デフォルトを有効にする場合 encoder_update_userを実装してindexを読む) bool encoder_update_user(uint8_t index, bool clockwise) { int8_t row = clockwise ? encoder_a_rows[index] : encoder_b_rows[index]; int8_t col = clockwise ? encoder_a_cols[index] : encoder_b_cols[index]; tap_code_delay(matrix_to_keycode(row, col), 10); return false; // tap_code_delay() でスイッチとして処理しているためencoderで処理させない } |

エンコーダーを操作した結果の入力については、私は設定と変更の容易さからA相B相に割り当てた位置のキーコードを発行しました。
おわりに
自作キーボードのファームウェアはソフトウェアと電子回路、機械的な事柄まで幅広い知識が身に着けられるのでソフトウェアエンジニアとして幅を広げるにはいい課題ではないでしょうか。
キーボードと言う毎日使うデバイスですので、活動の結果がQOLに直結する点も素晴らしいと思います。
この記事で興味を持って一人でも多くの仲間が現れることを期待しています。