You are currently viewing 【自キ】ロータリーエンコーダ研究

【自キ】ロータリーエンコーダ研究

この記事はキーボード #1 Advent Calendar 2021の15日目の記事です。 昨日はキムラシンショクバさんの「アクリルをひたすら切った2021年」でした。とても美しい作品がうまれた背景が語られていますので積層好き必読です。
さて、例年アドカレでは非技術的な話を書いてきましたが、今回は技術寄りのネタです。
自分でも意外なほどまじめに書いてしまったので当初のタイトル「ロータリーエンコーダーをぐるぐるしようの巻」を改めています。

Table of Contents

はじめに

キーボードと言えばキーキャップの乗ったプッシュスイッチがずらっと並ぶもの、というPC黎明期、いや、いにしえの端末の時代からの風景にアクセントを加えるのがロータリーエンコーダです。無制限回転入力装置であるロータリーエンコーダはテキスト入力には不向きですが、音量ボリュームのような連続変化を直感的に操作することが出来るという利点があり、とりわけモニタ上に映るボリュームをマウスでくるくる回すような操作に割り当てることが出来れば、身体的にはるかに楽になります。マウスでボリュームいじるの難しくありませんか? わたしは苦手です。

それにです。ロータリーエンコーダってかわいいんです。角張った四角が並ぶ中に丸みがあるととても愛らしく感じます。
というわけで筆者はエンコーダが大好き。付けられそうなキーボードにはついつい乗せてしまいます。

ちなみに筆者が初めてエンコーダを搭載したキーボードはCassette42でした。名刺サイズのキーボードを作ろうという遊舎工房の企画に合わせて設計したネタもの的キーボードとして開発しましが、頂戴した評判に気をよくしてキットとして販売を開始し、いまでも頒布を続けています。最新版はピンクでさらにかわいさアップしてます。

最近ではエンコーダを搭載するキーボードが珍しくなくなりつつあります。しかし一方で、QMK関連のエンコーダに関するドキュメントはそれほど充実しておらず、どんなエンコーダに対応しているのかさえ書かれていません。もっとも、ハード側を合わせればどんなエンコーダにでも対応しているということでもあり、だからこそ子細な仕様についてはあえて触れていないとも言えます。

この記事は、そもそもエンコーダとはどんなものなのか、どんな種類があって、キーボードではどのようなものが使われているのかから始まり、動作原理と絡めてQMKのコアのコードを紹介した上で、最新のユーザーサイドの実装について触れていきたいと思います。

ロータリーエンコーダの分類と動作原理

1. ロータリーエンコーダの分類

ロータリーエンコーダとは、ユーザーによる軸の回転を測定によって読み取ってその結果を出力するものです。

ロータリーエンコーダはおおよそ3つの分類方法によって分けることが出来ます。
1つめは
測定方式による分類です。これには3つの方式があります。

  • 光学式
  • 磁気式
  • 機械式

光学式は光源とフォトダイオードなどの受光器を使うものです。磁気式はN極とS極を並べたものを回転させてホール素子など磁気センサーで読み取ります。機械式はもっと単純で、回転軸にある接点を固定部本体側の接点との接触によりパルスを発生させます。より正確に言うと、パルスを発生させるための接点構造を持った回転式スイッチです。その構造や原理については後ほど扱います。

2つめの分類は信号方式によるものです。こちらは2つに分けられます。

  • アブソリュートエンコーダ
  • インクリメンタルエンコーダ

アブソリュートエンコーダはスイッチの絶対位置を読み取って出力します。たとえば右90度の位置にあるということを出力できます。

一方、インクリメンタルエンコーダは絶対位置を把握しません。その代わり、回転された方向と回転量のヒントとなる2つのパルスを発生し、ユーザーはそれを読み取って回転方向/量を得ます。

 

3つめの分類方法は、デテント、つまり回転時のクリックの有無によるものです。回転量と入力量を明確に一致させたい場合はデテントありを選びます。デテントなしの場合は引っ掛かりなくスムーズに回転するため、入力量を感触で把握することが出来ません。

  • デテントあり
  • デテントなし

自作キーボードで一般的に使われているのは機械式インクリメンタルエンコーダになります。パッケージが小さく、また安価であり、そこそこハンドリングしやすいことが採用の理由でしょう。このほかプッシュスイッチの有無という分け方も出来ますが、エンコーダの主機能からは離れているのでここでは分類には入れていません。

2. 機械式ロータリーエンコーダの動作原理

機械式インクリメンタルロータリーエンコーダの仕組みは、たとえば以下のサイトに図解されています。

https://www.best-microcontroller-projects.com/rotary-encoder.html

このサイト中にパブリックドメインと書かれた構造図がありましたので転載します。

ロータリーエンコーダの底板を一部剥がして覗いた図です。中央に回転軸があり、水車のような形をした部分を含めた円形部が軸とともに回転します。図中の8A、8B、8Cはすべて端子と一体になって固定されているスプリング式の接点です。8Aは常時網点のある部分に接していて、また、8B、8Cの接点が回転方向に対して少しズレて配置されているのが分かります。このズレが肝です。

エンコーダ単体だと動作の説明がしにくいので、実際にキーボード中に使用される回路を例に考えてみましょう。

下図はCassette42の回路図からスイッチとエンコーダ周辺部を抜き出したものです。SW5とSW6がエンコーダとなり、エンコーダの下側にAとCとBの3つの端子が並んでいます(上のS1、S2はプッシュスイッチです)。このうち、A端子はRExA、B端子はRExBに配線されています。RExyはx番のロータリーエンコーダのy端子と言う意味で筆者が付けたラベルです。これらは実際にはCassette42のMCUモジュールとして採用されているProMicroの別々のピンに接続されています。また、C端子はGNDに接続されています。本題からはずれますが、Cassette42はキー数が少ないのでダイオードを使わずに片側はGNDに落とします(マトリクスにおいてこの行が常時選択されているのと同じ状態)。

A端子とB端子はMCUの内部プルアップによって電圧レベルがHiになっています。ここで再び前述のエンコーダ内部の図を参照しましょう。

ちょっとややこしいですが、図の8B=8EがCassette42の(と同時に一般的な)エンコーダのA端子、8C=8FがエンコーダのB端子にあたります。8A=8DがC端子です。図の状態は、8A、8B、8Cのすべてが網点のある部分に接しているように見えます。網点は通電可能な素材、つまり接点ということになります。8A(一般的なエンコーダのC端子)はGNDですので、RExAとRExBはGNDレベル、つまりLoレベルとなります。

いま、エンコーダを時計方向に回転したとします。図は下から眺めたものなので、軸と一体になった回転版(基板)が反時計方向に回ることになります。すると、まず8B(A端子)が先に網点のない部分、つまり絶縁体に入ります。8Bはプルアップされているので、RExAはHiです。一方、8C(B端子)はまだ網点部分なのでRExBはLoです。

さらに回転が進むと8C(B端子)も網点のない絶縁体部分に入るため、RExBもHiになります。

さらにさらに回転が進むと今度は8B(A端子)が次の網点領域に入るので、RExAがまたLoになります。

さらにさらにさらに回転が進むと8C(B端子)が次の網点に入るので、RExBがLoに戻ります。

上記の時計回りの状態遷移を8C:8B(B:A)の順で2ビットで記すと、00→01→11→10→00 となります。

反対方向に回した場合も考えてみましょう。

軸を反時計方向に回すと、図のように裏側から見て時計方向に回転します。すると、まず8C(B端子)が先に絶縁体に入ってRExBがHiとなります。

さらに回転が進むと8B(A端子)も絶縁体に入ってRExAがHiに変化します。

さらにさらに回転が進むと8C(B端子)が接点に入ってRExBがLoに変化します。

さらにさらにさらに回転が進むと8B(A端子)も次の接点に入ってRExAがLoに変化します。

反時計回りの状態遷移を2ビットで記すと、00→10→11→01→00 となります。

これは、時計方向の回転時の 00→01→11→10→00 の矢印の向きを反転させたものと同一です。

QMKにおけるエンコーダ処理

1. QMKコアのコードを読んでみる

QMKでは主にencoder.cというファイルにコア側のエンコーダ処理が書かれています。コア側に対応するのがユーザー側の処理で、こちらは各キーボード各キーマップごとに、エンコーダを操作した際の挙動を書いておきます。まずはコア側から簡単に見ておきましょう。

				
					 bool encoder_read(void) {
    bool changed = false;
    for (uint8_t i = 0; i < NUMBER_OF_ENCODERS; i++) {
        encoder_state[i] <<= 2;
        encoder_state[i] |= (readPin(encoders_pad_a[i]) << 0) | (readPin(encoders_pad_b[i]) << 1);
        changed |= encoder_update(i, encoder_state[i]);
    }
    return changed;
}
				
			

エンコーダからの読み込みはencoder_readという関数によって行われます。

encoders_pad_a[i] i番エンコーダのA端子
encoders_pad_b[i] i番エンコーダのB端子

上の2つの静的変数(配列)には各キーボードもしくは各キーマップ内のconfig.hにあるENCODERS_PAD_A、ENCODERS_PAD_Bマクロの値が代入されています。

まず、読み取り値を格納するencoder_state[i]を2ビット左シフトし、空いた下位2ビット(0~1ビット)にB端子:A端子の順に新しい読み取り値を格納します。これを読み取るごとに繰り返すので、常に前回の読み取り値が上位2ビット(2~3ビット)、今回の読み取り値が下位2ビット(0~1ビット)に収められていることになります。

ちなみにencoder_state[i]は符号なし8ビットで宣言されていますが、下位4ビットしか使われません(4~7ビットにはシフトされたデータが残ってはいます)。

次にencoder_updateを見てみましょう。

				
					static bool encoder_update(uint8_t index, uint8_t state) {
    bool    changed = false;
    uint8_t i       = index;

#ifdef ENCODER_RESOLUTIONS
    uint8_t resolution = encoder_resolutions[i];
#else
    uint8_t resolution = ENCODER_RESOLUTION;
#endif

#ifdef SPLIT_KEYBOARD
    index += thisHand;
#endif
    encoder_pulses[i] += encoder_LUT[state & 0xF];
    if (encoder_pulses[i] >= resolution) {
        encoder_value[index]++;
        changed = true;
        encoder_update_kb(index, ENCODER_COUNTER_CLOCKWISE);
    }
    if (encoder_pulses[i] <= -resolution) {  // direction is arbitrary here, but this clockwise
        encoder_value[index]--;
        changed = true;
        encoder_update_kb(index, ENCODER_CLOCKWISE);
    }
    encoder_pulses[i] %= resolution;
#ifdef ENCODER_DEFAULT_POS
    if ((state & 0x3) == ENCODER_DEFAULT_POS) {
        encoder_pulses[i] = 0;
    }
#endif
    return changed;
}
				
			

注目すべきは以下のラインです。

encoder_pulses[i] += encoder_LUT[state & 0xF];

encoder_readから渡された読み取り値が上位4ビットをカットされてencoder_LUT配列の要素番号になっています。この配列の正体を見てみましょう。

				
					static int8_t encoder_LUT[] = {0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0};

				
			

-1, 0, 1の3つの値がなんとなく規則的に並んでいますが、これがデコードの核心部分。エンコーダの状態遷移を回転方向に変換するテーブルなのです。

例として、上述の時計回りに少しだけ動かした場合を考えてみましょう。動かす前→動かした後は00→01ですから、encoder_readから渡されたstateは0x0001です。これが要素番号になりますので、encoder_LUT[1]、つまり-1がencoder_pulses[i]に加算されます。加算ですが負の数なのでマイナスされます。

時計方向に回しているのにマイナスというのが違和感ありまくりですが、次に来る2つのif文を見ると、プラスの場合は反時計回り、マイナスの場合は時計回りとして修正?されてencoder_update_kbの引数になっていますので、実際には問題ありません(resolutionの中身については後述しますのでここでは正負のみに注目してください)。encoder_value[index]の増減に関しては修正されないので、時計方向に回すと encoder_value[index]がマイナスされるという挙動が気になるものの、いまのところこの変数はどこにも使われていないので問題化していません。【追記:スプリットキーボードの際に前回通信時の差分を取るために使われていました。この場合も符号は正しく処理されていますので問題ありません】本来ならばencoder_LUTのテーブルを修正したいところですが、触らぬ神に祟りなしで黙っておきます。

反時計方向に回した場合についてはご自身で考えてみてください。encoder_LUTは1となるはずです。また、状態遷移のない場合、たとえば0000の場合は0が返ってきます。このほか、0110などAとBの両方が一度に変化するようなあり得ない場合にもエラーとして0が返ってきます。なお、テーブルを使ったこのデコード方法はQMK独自のものというわけではなく、一般によく知られたものです。

話を処理に戻すと、encoder_updateはエンコーダの状態遷移が有効であると判断した場合にはencoder_update_kbを呼び出し、各キーボードや各キーマップごとの処理へと引き継ぎます。つまりこれ以降はユーザー側が指定する処理です。

2. ユーザーサイドの実装

もっともベーシックなユーザーサイドの実装については以下の公式ドキュメントにあります。

https://github.com/qmk/qmk_firmware/blob/master/docs/ja/feature_encoders.md

一般にはkeymap.cに用意された次の関数encoder_update_userで、エンコーダの操作に応じたキー入力をバインドします。

				
					bool encoder_update_user(uint8_t index, bool clockwise) {
    if (index == 0) { /* First encoder */
        if (clockwise) {
            tap_code(KC_PGDN);
        } else {
            tap_code(KC_PGUP);
        }
    } else if (index == 1) { /* Second encoder */
        if (clockwise) {
            tap_code(KC_DOWN);
        } else {
            tap_code(KC_UP);
        }
    }
    return false;
}
				
			

エンコーダ番号(index)、時計回りの回転方向(clockwise)で場合分けをしてそれぞれの処理を書くだけです。この方式はとても簡単ですが、VIAでの割り付け変更に対応出来ないことが欠点です。VIAフレンドリーな処理方法については後述したいと思います。

実際のエンコーダを測定してみる

1. キーボード用途の品種

機械式ロータリーエンコーダを販売しているメーカーは複数あり、品種も様々です。キーボードにはどんなエンコーダだって使うことは出来ます。とはいえ、一般にキーボードに使われるものは、キースイッチのピッチ幅である19mm内に収まるサイズから選ばれることが多いです。例として筆者の手元にあるものを紹介します。

写真の左から品番をあげておきます。すべてデテントあり、プッシュスイッチ付きのものです。
ALPS EC11N1524402
ALPS EC11E18244AU 
BOURNS PEC11R-4220F-S0024
BOURNS PEC12R-4225F-S0024

多くのキーボードキットで指定されているものと同じく、EC11、EC12互換のロータリーエンコーダです。EC11とEC12の違いは金属軸か、絶縁軸(樹脂軸)かです。一般に、金属軸の方が剛性が高く、ぐらつきが少ない傾向があります。

一方、絶縁軸はそのものずばり絶縁特性とともに価格が安いという利点があります。通常、キーボード用途では絶縁性能を求められることはありませんので、主にコスト面から使われます。

上記の中では一番右のものだけが絶縁軸になります。これはCassette42に付属しているものです。アルミ筐体のカスタムキーボードならば金属軸の方がふさわしいですが、カジュアルなCassette42には必要十分な性能を持っています。

ちなみに、ビンテージスイッチでも人気のALPS製は価格が高めなこともあり、さすがの出来です。

このほか、遊舎工房さんがロープロファイル用として販売されているEC12E2440301というものがあります。高さを抑えたいときには便利ですが、プッシュスイッチは内臓されていませんのでその点のみ注意が必要です。

また、出所不明の水平軸エンコーダ(下掲写真)なども自作キーボードに使われている例があります。筆者も念のため?購入してあるものの絶賛放置中です…。

2. パルスを計測する

次に、実際のロータリーエンコーダのパルスをオシロスコープで眺めてみましょう。
下の波形はPEC12R-4225F-S0024を停止状態から時計方向へ数クリック分回転させたときのものです。
1ch(黄)はA端子、4ch(緑)はB端子を計測しています。

停止時はABともHiになっています。さきほどのように2ビット(B:A)で表すと、11→10→00→01→11→10→00→…と続いています。デテント位置の違いにより開始の状態が異なりますが、変化の仕方は動作原理のところで説明した00→01→11→10→00と一致します。

なお、パルスの幅が一定になっていないのは、主にデテントの際にひっかかりがあるためです(人類が回す限りデテントがなくても完全に等速になることはないですが)。

ところで、1デテントだけ回した際にはどれだけのパルスが出るのでしょうか。やってみましょう。使用しているエンコーダは先ほどと同じPEC12R-4225F-S0024です。

やはりデテントのひっかかりによってAとBでは長さが違いますが、位相が90度ズレたパルスが出ています(ALPSの一部品種では60度ズレのものもあります)。この際の状態遷移を先ほどの2ビット(上位B:下位A)で表すと、11→10→00→01→11と4回起こります。

ここでencoder_updateに戻って、さきほど飛ばしたresolutionに注目します。ENCODER_RESOLUTIONが無指定の場合はデフォルトで4となりますので、encoder_pulsesとresolutionを比較する if文が成り立つには、encoder_pulsesが4必要になります。1デテントで4回の状態変化が起るということは、条件を満たし、目論見通りencoder_update_kbが呼び出されることになります。

				
					static bool encoder_update(uint8_t index, uint8_t state) {
    bool    changed = false;
    uint8_t i       = index;

#ifdef ENCODER_RESOLUTIONS
    uint8_t resolution = encoder_resolutions[i];
#else
    uint8_t resolution = ENCODER_RESOLUTION;
#endif

#ifdef SPLIT_KEYBOARD
    index += thisHand;
#endif
    encoder_pulses[i] += encoder_LUT[state & 0xF];
    if (encoder_pulses[i] >= resolution) {
        encoder_value[index]++;
        changed = true;
        encoder_update_kb(index, ENCODER_COUNTER_CLOCKWISE);
    }
    if (encoder_pulses[i] <= -resolution) {  // direction is arbitrary here, but this clockwise
        encoder_value[index]--;
        changed = true;
        encoder_update_kb(index, ENCODER_CLOCKWISE);
    }
    encoder_pulses[i] %= resolution;
#ifdef ENCODER_DEFAULT_POS
    if ((state & 0x3) == ENCODER_DEFAULT_POS) {
        encoder_pulses[i] = 0;
    }
#endif
    return changed;
}
				
			

ENCODER_RESOLUTIONがわざわざ用意されている理由は、すべてのロータリーエンコーダが同じ頻度でパルスを出すわけではないためです。

例としてALPSのEC11N1524402を計測してみましょう。条件は同じく1デテント時計回りです。

なんと立ち下がったままです。このエンコーダをデフォルトのENCODER_RESOLUTIONで使用すると、resolution=4に対して1デテント分回転時の状態遷移は11→10→00の2しかありませんから、encoder_pulsesとresolutionを比較する if文が成立せず、したがってキーボード/キーマップ側で規定された動作は起こりません。続けてもう1デテント分、合計2デテント分動かすと状態遷移が4に達してようやく動作します。操作量に対して1/2の動きです。

このEC11N1524402の仕様に掲載されている出力波形を見ると、デテント停止位置=クリック安定点(下図の点線位置)の間にはA信号、B信号ともに1つずつ状態変化があり、上の観測と一致します。

こうした仕様は波形だけでなく、データシートの別の部分からも読み取れます。下記のクリック数とパルス数を見てください。クリック数とは、360度あたりのデテントの数のことです。クリック数が多いほど小さい角度の回転でカウントされますので、エンコーダ選びの指標のひとつになります。パルス数も同じく1周あたりの数です。パルスは立ち上がりから次の立ち上がり直前までをカウントします。ということは、クリック数とパルス数がイコールの時には、1つの信号に関してデテント間には2つのエッジ(立ち上がりもしくは立ち下がり)があるはずです。しかしこのエンコーダのようにパルス数がクリック数の半分だと、デテント間には1つのエッジしかないことになります(上図の波形参照)。ABの2つの信号ならば4つのエッジがあるところが2つのエッジになってしまうわけです。エッジの数は状態遷移の数でもあります。

EC11N1524402のクリック数(デテント数)とパルス数

ALPS社のEC11にはこのエンコーダのほかにも1デテントで状態遷移が2のものが少なくありません(一部のEC12にもあります)。たとえばEC11E18244AUの仕様を見ると、出力波形は以下のように記載されています。

B信号の波形の立ち上がりと立ち下りがデテントの停止位置にあるように見えますが、注意書きにあるように規定されていません。規定は出来ないというのは、停止位置よりも前に立ち上がりもしくは立ち下りがあるかもしれないし、後かもしれないという意味です。いずれにしても、1デテントでの状態遷移は1~3となります(1周のデテント数は決まっていますから、平均すれば2に収束します)。

このようなエンコーダを使用する際には、config.hで #define ENCODER_RESOLUTION 2 としておけば、encoder_update_kbが正しい頻度で呼ばれるようになります。

ENCODER_RESOLUTIONを可変にしたい

実装するパーツによってENCODER_RESOLUTION(あるいはENCODER_RESOLUTIONS)を変えてビルドすることは、開発者自身にとっては容易なのですが、一方で、キット販売をしている身としてはユーザーにビルドを強要することに抵抗があります。とくに、VIAファームウェアを書き込んであるMCU実装済み基板のキットの場合、ユーザーはファームのことなど気にすることなくブラウザ上のRemapでキー設定を行える時代だというのに、わざわざ面倒なQMKの環境を手元に用意してENCODER_RESOLUTIONを書き換えてビルドして、QMKToolBoxで再度ファームを書き込んでくださいなどと言いたくありません。

というわけでENCODER_RESOLUTIONを可変にするPRを出してみました。

https://github.com/qmk/qmk_firmware/pull/14504

非常に簡単な関数を用意することで、現状の動作を阻害することなく動的にresolutionを変更出来ます。
具体的には、ハードウェアのDIPスイッチやレイヤー深くに割り当てたキープレスなど、どんな形でも対応可能です。

しかし、上記のPRを見ていただけると分かりますが、データドリブンな方向を目指しているメンテナさんたちからは反対されています。こちらとしてはQMKを使わせていただく身ですし、メンテナさんたちの意向を尊重していますが、とはいえそれほど大それたPRとも思えないので必要性をていねいに主張していきました。その甲斐あってか、メンテナさんのお一人が意見を変えてくださってアプルーブをいただきました。ただしマージにはもう1つアプルーブが必要なのでここで止まってしまっています…。QMKが進めているVIA+的なAPIであるXAPの実装時にマージされるかもしれません。

現状での実装例として参考にAleth42の開発ブランチをあげておきます。

https://github.com/monksoffunk/qmk_firmware/commits/feature/aleth42_dev

コア側はPRと同一ですので(コアを改変してありますのでご注意ください)、ここではユーザー側の処理を説明します。下記はrev1.cです。Aleth42のファームウェアはα版用のrev0とリリース版用のrev1に分かれており、リリース版ではrev1.cがキーボード名.cの代わりになります。

				
					/* Copyright 2020 monksoffunk
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "rev1.h"

user_config_t user_config;
//uint8_t encoder_resolution[NUMBER_OF_ENCODERS];

void eeconfig_init_kb(void) {
    user_config.raw      = 0;
    user_config.mac_mode = true;
    uint8_t encoder_resolutions[] = ENCODER_RESOLUTIONS;
    user_config.encoder_resolutions[0] = encoder_resolutions[0];
    user_config.encoder_resolutions[1] = encoder_resolutions[1];
    eeconfig_update_kb(user_config.raw);
    eeconfig_init_user();
}

void keyboard_pre_init_kb(void) {
    // Read the user config from EEPROM
    user_config.raw = eeconfig_read_kb();
    keymap_config.swap_lalt_lgui = keymap_config.swap_ralt_rgui = !user_config.mac_mode;
#ifdef ENCODER_ENABLE
    for (uint8_t i = 0 ; i < 2 ; i++) {
        if ((user_config.encoder_resolutions[i] == 0) || (user_config.encoder_resolutions[i] > 4)) {
            user_config.encoder_resolutions[i] = 4;
            eeconfig_update_kb(user_config.raw);
        }
        encoder_set_resolution(i, user_config.encoder_resolutions[i]);
    }
#endif
    keyboard_pre_init_user();
}

#ifdef ENCODER_ENABLE
void matrix_scan_kb(void) {
    encoder_action_unregister();
    matrix_scan_user();
}

bool encoder_update_kb(uint8_t index, bool clockwise) {
    encoder_action_register(index, clockwise);
    return true;
}
#endif

bool process_record_kb(uint16_t keycode, keyrecord_t* record) {
    switch (keycode) {
        case AG_NORM:
            if (record->event.pressed) {
                if (!user_config.mac_mode) {
                    user_config.mac_mode = true;
                    eeconfig_update_kb(user_config.raw);
                    }
            }
            return true;
            break;
        case AG_SWAP:
            if (record->event.pressed) {
                if (user_config.mac_mode) {
                    user_config.mac_mode = false;
                    eeconfig_update_kb(user_config.raw);
                    }
            }
            return true;
            break;
#ifdef ENCODER_ENABLE
        case CHENCR0:
        case CHENCR1:
            if (record->event.pressed) {
            } else {
                uint8_t index = keycode - CHENCR0;
                user_config.encoder_resolutions[index] = (user_config.encoder_resolutions[index] << 1) & 7;
                if (user_config.encoder_resolutions[index] == 0) { user_config.encoder_resolutions[index] = 1; }
                encoder_set_resolution(index, user_config.encoder_resolutions[index]);
                eeconfig_update_kb(user_config.raw);
            }
            return false;
            break;
#endif
        default:
            break;
    }
    return process_record_user(keycode, record);
}
				
			

DIPスイッチのようにハード的に設定する方法であれば、起動時にスイッチの状態を読み込むことが出来るのですが、ENCODER_RESOLUTIONの値をキープレスなどによってソフト的に調整可能にすると、調整後の値を保存する必要が生じます。保存しておかないと起動ごとに設定し直す必要が出てくるからです。ここではEEPROMに保存することにしました。

22行からのeeconfig_init_kbは、EEPROMに書かれたデータをリセットする際に呼ばれ、config.hにあるENCODER_RESOLUTIONSの値をセットして保存します。

36行以降は、Aleth42の電源がオンになった際に呼ばれ、EEPROMから読み込んだ設定値が正常かどうかをチェックした後にencoder.cの関数にresolutionを渡します。

80行以降がresolutionを変更する部分です。ここでは特定のキープレスに応じてresolutionを変更します。取り得る値は、1、2、4としています。

ほとんどの場合、resolutionは2もしくは4となるはずです。QMKの中を確認してみると1や8としている例もあります。そうした仕様のエンコーダもあるのかもしれませんが、おそらくはデテントなしのエンコーダを使用していて、希望する感度のためにそのような値に設定しているのでしょう。デテントのないタイプのエンコーダではそもそもユーザーが感知し得るクリックのフィードバックがありませんから、パルスと動作の関係に縛られることなく、ENCODER_RESOLUTION(S)を小さくすれば感度を高く、反対に大きくすれば感度を低く調整することが出来ます。resolutionが可変であれば、たとえばレイヤーに応じて感度を変えるようなことも可能になります。

いずれにせよ、ユーザーが好みのスイッチを選ぶのと同じように好きなエンコーダを面倒なく、そしてより便利に使えるよう整備されればなあと願ってやみません。

QMKに寄付をしてみよう

QMKへのPRは上記の例のように思うように進まなかったり、数ヵ月、あるいは半年以上マージされないこともあります。とはいえ、QMKは常時3桁のPRを抱える規模のOSSなので、限られた人数のメンテナさんたちが担う作業量はかなりのものと想像されます。彼らの労をねぎらい、継続的な発展の一助になるためにも、寄付を検討してみてはいかがでしょうか? わたしも少額ながらやってみました。以下のリンクからとても簡単に行うことが出来ます。

https://donorbox.org/qmk

VIA/Remapコンパチブルなファームへ

1. VIA対応化の考え方と実装例

動作原理のところで触れたVIAフレンドリーな実装についても紹介しておきましょう。おそらくこの記事をご覧になる方はVIAをご存じでいらっしゃると勝手に仮定して詳しい説明は省きます。簡単に言えばファームウェアをビルドし直すことなく、アプリ上でキー設定を変更できるものです。現在では、ブラウザ上で同等以上の動作をするRemapという素晴らしいサービスがあります。

VIAやRemapは大変直感的であり、技術的ノウハウを要求しないユーザーフレンドリーなインターフェイスを持っているのですが、残念なことに従来のエンコーダ処理はハードコーディングされているために、VIAコンパチブルではありませんでした。

ところが考える人はいるもので、やがてVIA対応が可能らしいという声が聞こえてきます。自分がはじめて気づいたのはたしかせきごんさんのtweetだったと思います。その書き込みにヒントを得て書いてみたのが次のコードです。

				
					bool encoder_update_kb(uint8_t index, bool clockwise) {
    keypos_t key;
    bool     encoder_layer_locked = false;

    if (index == 0) {
        if (clockwise) {
            key.row = 5;
            key.col = 1;
        } else {
            key.row = 5;
            key.col = 0;
        }
        if (get_highest_layer(layer_state) < _FN) {
        layer_on(encoder_lock_layer);
        encoder_layer_locked = true;
        }
        action_exec((keyevent_t){.key = key, .pressed = true, .time = (timer_read() | 1)});
        action_exec((keyevent_t){.key = key, .pressed = false, .time = (timer_read() | 1)});
        if (encoder_layer_locked) {
            layer_off(encoder_lock_layer);
        }
    }
    return true;
}
				
			

エンコーダが操作された際に呼び出されるencoder_update_kb/userに直接キーコードを指定するのではなく、ハードに近いレイヤーで特定のキー位置をプレス/リリースしたことにする、というものです。こうすることで、たとえばRGB関係の機能をRemap上から自由に割り当てることも可能になります。上記の例では、5行0列と1列にエンコーダを反時計/時計回りに操作した際のキーを割り当てます。当然、LAYOUTマクロにも実際にはマトリクス上にないそのキーを用意する必要があります。

この例では実際のキー数は5×5の25ですが、6行めにK50とK51の2つのキーを加えています。

				
					#define LAYOUT( \
                      K50, K51, \
	K00, K01, K02, K03, K04, \
	K10, K11, K12, K13, K14, \
	K20, K21, K22, K23, K24, \
	K30, K31, K32, K33, K34, \
	K40, K41, K42, K43, K44  \
) { \
	{ K00,   K01,   K02,   K03,   K04 }, \
	{ K10,   K11,   K12,   K13,   K14 }, \
	{ K20,   K21,   K22,   K23,   K24 }, \
	{ K30,   K31,   K32,   K33,   K34 }, \
	{ K40,   K41,   K42,   K43,   K44 }, \
	{ K50,   K51,   0,     0,     0   } \
}
				
			

基本的にはこのコードは問題なく機能します。基本的、と書いたのは、特定のキーコンビネーションを割り付けた場合にアプリによって正常に動作しないことがあったからです。具体的にはAdobeのソフトなのですが、通常のキーコンビネーションの割り付けではうまく動かなくても、VIAやRemapでマクロにすると動くという現象が出ることがあります(現在、Windows11+AdobeCC最新版の環境では問題が出ていません)。

2. Aleth42への実装例

状況から予想するに、上記コードではキープレスとキーリリースのタイミングがほぼないことが原因となっているように思われました。環境によってとはいえ問題が生じるのであればなんとかしたいなあと思っていたある日、エンコーダを使っている様々なキーボードのコードを眺めているとこんなものを見つけました。

https://github.com/qmk/qmk_firmware/blob/3496513865b0b1ac1b4c3771c64581e626428c4d/keyboards/nightly_boards/n40_o/encoder_action.c

これは、キープレスとリリースのタイミングを完全に分割するものです。また、ファイル分割されていてkeymap.cやkeboard.cからごちゃごちゃしたソースをなくすことが出来るのも保守性から大きなメリットです。

というわけで、この方式を採用してみました。実は、前のパートで紹介したAleth42のブランチの最新版では上記のencoder_aciton.cを改造して使っています。改造したポイントはエンコーダレイヤーロック機能の付加です。

VIA対応により、エンコーダはレイヤーごとのキー割り付けが容易になりました。これによって複数のキーコードをエンコーダ操作可能になります。キーコードはアルファベットやモディファイアとの組み合わせにとどまらず、音量操作、RGB LED操作など通常キーに割り付け可能なものすべてです。ただし、レイヤーに収められていると、使うごとにレイヤーキーを押すなどの切替が必要になります。レイヤー自体に移動することも可能ですが、多くの場合はアルファベット部分などキーレイアウトはデフォルトのまま、エンコーダのみ特定のアプリなどに合わせてレイヤー操作なしに使えることが望ましいように思います。

そんなときに活躍するのがエンコーダレイヤーロックです。これは、各レイヤーにあるエンコーダの機能を一時的にデフォルトレイヤーで使えるようにするものです。改造したencoder_action.cを掲載します。

				
					/* Copyright 2020 Neil Brian Ramirez
 * Copyright 2021 monksoffunk
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "encoder_action.h"

#ifdef ENCODERS
static uint8_t encoder_state[ENCODERS] = {0};
static keypos_t encoder_cw[ENCODERS] = ENCODERS_CW_KEY;
static keypos_t encoder_ccw[ENCODERS] = ENCODERS_CCW_KEY;
#endif

void encoder_action_unregister(uint8_t *locklayer) {
#ifdef ENCODERS
    bool layerlocked = false;
    for (int index = 0; index < ENCODERS; ++index) {
        if (encoder_state[index]) {
            if ((get_highest_layer(layer_state) == 0) && (locklayer[index] > 0)) {
                layer_on(locklayer[index]);
                layerlocked = true;
            }
            keyevent_t encoder_event = (keyevent_t) {
                .key = encoder_state[index] >> 1 ? encoder_cw[index] : encoder_ccw[index],
                .pressed = false,
                .time = (timer_read() | 1)
            };
            encoder_state[index] = 0;
            action_exec(encoder_event);
            if (layerlocked) {
                layer_off(locklayer[index]);
            }
        }
    }
#endif
}

void encoder_action_register(uint8_t index, bool clockwise, uint8_t *locklayer) {
#ifdef ENCODERS
    bool layerlocked = false;
    if ((get_highest_layer(layer_state) == 0) && (locklayer[index] > 0)) {
        layer_on(locklayer[index]);
        layerlocked = true;
    }
    keyevent_t encoder_event = (keyevent_t) {
        .key = clockwise ? encoder_cw[index] : encoder_ccw[index],
        .pressed = true,
        .time = (timer_read() | 1)
    };
    encoder_state[index] = (clockwise ^ 1) | (clockwise << 1);
    action_exec(encoder_event);
    if (layerlocked) {
        layer_off(locklayer[index]);
    }
#endif
}
				
			

この関数を使うためにはconfig.hに下記のマクロが必要になります。ENCODERSはエンコーダの数、ENCODERS_CCW_KEYは反時計回りに割り当てるキーのマトリクス位置、ENCODERS_CW_KEYは時計回りに割り当てるキーの位置になります。

				
					/* encoders */
#define ENCODERS 2

#define ENCODERS_CCW_KEY { { 0, 4 },{ 2, 4 } }
#define ENCODERS_CW_KEY  { { 1, 4 },{ 3, 4 } }

				
			

キー位置を表すkeypos_t型のkeyは列、行の順番なので、この例では0番のエンコーダに反時計回り=4行0列、時計回り=4行1列、1番のエンコーダに反時計回り=4行2列、時計回り=4行3列のキーをそれぞれ割り当てていることになります。

これに合わせてLAYOUTマクロにこの4キーを追加してあげます。

				
					#define LAYOUT( \
  k40, k41, k42, k43,\
  k00, k01, k02, k03, k04, k05, k06, k07, k08, k09, k0A, k38,\
  k10, k11, k12, k13, k14, k15, k16, k17, k18, k19, k1A,\
  k20, k21, k22, k23, k24, k25, k26, k27, k28, k29, k2A,\
  k30, k31, k32, k33, k34, k35, k36, k37\
) \
{ \
  { k00, k01, k02, k03, k04, k05, k06, k07, k08, k09, k0A },\
  { k10, k11, k12, k13, k14, k15, k16, k17, k18, k19, k1A },\
  { k20, k21, k22, k23, k24, k25, k26, k27, k28, k29, k2A },\
  { k30, k31, k32, k33, k34, k35, k36, k37, 0,   0,   k38 },\
  { k40, k41, k42, k43, 0,   0,   0,   0,   0,   0,   0   }\
}
				
			

keymap.cのレイアウトにも4キー分を確保します。
ごちゃごちゃするのを嫌ってラッパーを使っています。こうすることでエンコーダ部分だけを一か所に集められます。もっとも実際のキーコード変更にはRemapを使うのでここをきれいにしてもあまり意味はありません。気分の問題です。

				
					#ifdef ENCODER_ENABLE
// keycodes setting for encoder             LEFT_CCW    LEFT_CW   RIGHT_CCW   RIGHT_CW
#    define __________DEFAULT_E__________   KC_PGUP,    KC_PGDN,  KC_PGUP,    KC_PGDN
#    define ___________LOWER_E___________   KC_VOLD,    KC_VOLU,  KC_VOLD,    KC_VOLU
#    define ___________RAISE_E___________   S(KC_TAB),  KC_TAB,   S(KC_TAB),  KC_TAB
#    define __________ADJUST_E___________   RGB_RMOD,   RGB_MOD,  RGB_RMOD,   RGB_MOD
#    define __________ENCADJ_E___________   BL_DEC,     BL_INC,   BL_DEC,     BL_INC
#endif

#define LAYOUT_wrapper(...) LAYOUT(__VA_ARGS__)

#ifdef ENCODER_ENABLE
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
    [_QWERTY] = LAYOUT_wrapper(
        __________DEFAULT_E__________,
        __________DEFAULT_1__________,
        __________DEFAULT_2__________,
        __________DEFAULT_3__________,
        __________DEFAULT_4__________),

    [_LOWER] = LAYOUT_wrapper(
        ___________LOWER_E___________,
        ___________LOWER_1___________,
        ___________LOWER_2___________,
        ___________LOWER_3___________,
        ___________LOWER_4___________),

    [_RAISE] = LAYOUT_wrapper(
        ___________RAISE_E___________,
        ___________RAISE_1___________,
        ___________RAISE_2___________,
        ___________RAISE_3___________,
        ___________RAISE_4___________),

    [_ADJUST] = LAYOUT_wrapper(
        __________ADJUST_E___________,
        __________ADJUST_1___________,
        __________ADJUST_2___________,
        __________ADJUST_3___________,
        __________ADJUST_4___________),

    [_ENCADJ] = LAYOUT_wrapper(
        __________ENCADJ_E___________,
        __________ENCADJ_1___________,
        __________ENCADJ_2___________,
        __________ENCADJ_3___________,
        __________ENCADJ_4___________)
};
#else
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
    [_QWERTY] = LAYOUT_wrapper(
        __________DEFAULT_1__________,
        __________DEFAULT_2__________,
        __________DEFAULT_3__________,
        __________DEFAULT_4__________),

    [_LOWER] = LAYOUT_wrapper(
        ___________LOWER_1___________,
        ___________LOWER_2___________,
        ___________LOWER_3___________,
        ___________LOWER_4___________),

    [_RAISE] = LAYOUT_wrapper(
        ___________RAISE_1___________,
        ___________RAISE_2___________,
        ___________RAISE_3___________,
        ___________RAISE_4___________),

    [_ADJUST] = LAYOUT_wrapper(
        __________ADJUST_1___________,
        __________ADJUST_2___________,
        __________ADJUST_3___________,
        __________ADJUST_4___________),

    [_ENCADJ] = LAYOUT_wrapper(
        __________ENCADJ_1___________,
        __________ENCADJ_2___________,
        __________ENCADJ_3___________,
        __________ENCADJ_4___________)
};
#endif
				
			

あとは、keyboard.cからencoder_action.cの関数を呼び出すだけです。最新版のAleth42のrev1.cをそのまま掲載します。前述のものにロック機能が加わっています。

57行からのencoder_update_kbによって、回転があった際に(割り込みではないので実際にはループの中で判断して)対応するマトリクスのキープレスが行われます。

一方、マトリクススキャンのループの際に53行にあるencoder_action_unregisterが呼び出され、上記によって押されていたキーのリリースが一斉に行われます。

96行からの一連のcaseはロック機能の処理です。ENC_00~04は0番エンコーダのレイヤー0~4番にロック、ENC_10~14は1番エンコーダのレイヤー0~4番にロックすることを意味します。ENC_00などのコードをkeymap.cのレイアウトに割り当てておけばよいでしょう。

				
					/* Copyright 2020 monksoffunk
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "rev1.h"

user_config_t user_config;

#ifdef ENCODER_ENABLE
uint8_t encoderlocklayer[] =  {0, 0};
#endif

void eeconfig_init_kb(void) {
    user_config.raw      = 0;
    user_config.mac_mode = true;
    uint8_t encoder_resolutions[] = ENCODER_RESOLUTIONS;
    user_config.encoder_resolutions[0] = encoder_resolutions[0];
    user_config.encoder_resolutions[1] = encoder_resolutions[1];
    eeconfig_update_kb(user_config.raw);
    eeconfig_init_user();
}

void keyboard_pre_init_kb(void) {
    // Read the user config from EEPROM
    user_config.raw = eeconfig_read_kb();
    keymap_config.swap_lalt_lgui = keymap_config.swap_ralt_rgui = !user_config.mac_mode;
#ifdef ENCODER_ENABLE
    for (uint8_t i = 0 ; i < 2 ; i++) {
        if ((user_config.encoder_resolutions[i] == 0) || (user_config.encoder_resolutions[i] > 4)) {
            user_config.encoder_resolutions[i] = 4;
            eeconfig_update_kb(user_config.raw);
        }
        encoder_set_resolution(i, user_config.encoder_resolutions[i]);
    }
#endif
    keyboard_pre_init_user();
}

#ifdef ENCODER_ENABLE
void matrix_scan_kb(void) {
    encoder_action_unregister(encoderlocklayer);
    matrix_scan_user();
}

bool encoder_update_kb(uint8_t index, bool clockwise) {
    encoder_action_register(index, clockwise, encoderlocklayer);
    return true;
}
#endif

bool process_record_kb(uint16_t keycode, keyrecord_t* record) {
    switch (keycode) {
        case AG_NORM:
            if (record->event.pressed) {
                if (!user_config.mac_mode) {
                    user_config.mac_mode = true;
                    eeconfig_update_kb(user_config.raw);
                    }
            }
            return true;
            break;
        case AG_SWAP:
            if (record->event.pressed) {
                if (user_config.mac_mode) {
                    user_config.mac_mode = false;
                    eeconfig_update_kb(user_config.raw);
                    }
            }
            return true;
            break;
#ifdef ENCODER_ENABLE
        case CHENCR0:
        case CHENCR1:
            if (record->event.pressed) {
            } else {
                uint8_t index = keycode - CHENCR0;
                user_config.encoder_resolutions[index] = (user_config.encoder_resolutions[index] << 1) & 7;
                if (user_config.encoder_resolutions[index] == 0) { user_config.encoder_resolutions[index] = 1; }
                encoder_set_resolution(index, user_config.encoder_resolutions[index]);
                eeconfig_update_kb(user_config.raw);
            }
            return false;
            break;
        case ENC_00:
        case ENC_01:
        case ENC_02:
        case ENC_03:
        case ENC_04:
            if (record->event.pressed) {
                if (encoderlocklayer[0] != keycode - ENC_00) {
                    encoderlocklayer[0] = keycode - ENC_00;
                } else {
                    encoderlocklayer[0] = 0;
                }
            }
            return false;
            break;
        case ENC_10:
        case ENC_11:
        case ENC_12:
        case ENC_13:
        case ENC_14:
            if (record->event.pressed) {
                if (encoderlocklayer[1] != keycode - ENC_10) {
                    encoderlocklayer[1] = keycode - ENC_10;
                } else {
                    encoderlocklayer[1] = 0;
                }
            }
            return false;
            break;
#endif
        default:
            break;
    }
    return process_record_user(keycode, record);
}
				
			

ところで、ENC_00などユーザーが作成したキーコードは、Remapなどでは十六進数として表示され、大変わかりづらいです。このことをRemapのDiscordで質問したところ、KC_FNxxを使えばよいのではないかと教えていただきました。KC_FNxxはQMKの源流であるTMK時代にレイヤー処理などを行うために実装されていたもので、最近ではほぼ使われていません。しかも32個ありますから、ほとんどの用途には充分すぎる数です。

Aleth42ではrev1.hの中で次のようにカスタムキーコードを列挙しています。

				
					enum kb_keycodes {
    ENCADJ = KC_FN0,
    CHENCR0,
    CHENCR1,
    ENC_00,
    ENC_01,
    ENC_02,
    ENC_03,
    ENC_04,
    ENC_10,
    ENC_11,
    ENC_12,
    ENC_13,
    ENC_14,
    USR_SAFE_RANGE,
};
				
			

実際にRemapでの設定画面を見てみましょう。Sepcialタブの下方にあるFUNCという項目がKC_FNxxになります。

ここで接続しているAleth42は左上のキー位置にエンコーダを実装しています。ハードウェアに合わせてRemapのレイアウトオプションでLeft Encoderをオンにすると、左上にエンコーダ設定用の2つのキーが表示されるようになります。

下記の例では反時計回りにPageUp、時計回りにPageDownキーが割り当てられています。一方、ロータリーエンコーダ部分のキーにはFunc3と表示されています。これはKC_FN3を意味しています。上のカスタムキーコードを見ると、KC_FN3に該当するのはENC_00です。0番エンコーダのレイヤー0にロックするという処理になりますので、つまりはデフォルトレイヤーでエンコーダを押し込むとエンコーダレイヤーロックを解除してデフォルトに戻すというわけです。

個人的には各レイヤーにおいてエンコーダを押し込むとエンコーダレイヤーロックがかかるというのが直感的で好みです。その場合は、たとえばレイヤー1にはENC_01に当たるKC_FN4、レイヤー2にはENC_02に当たるKC_FN5といった具合に、それぞれのレイヤーにおけるエンコーダ位置のキーに割り付けていきます。ENC_xxなどのロックキーはトグル動作になっていますので、同じロックキーを押すことでアンロックされます。

3. エンコーダVIA対応ファームを追加する際の困りごと

もうひとつティップス的なことを書いておきます。キーボードを頒布されている開発者向けの内容になりますので、興味のない方は飛ばしてください。

本家QMKにマージされているAleth42のファームウェアはエンコーダのVIA対応前ものです。当然、VIAにもそのファームウェアをベースとしたものがマージされています。Remapでもやはり同じファームで登録していました。

エンコーダのキー割り当てに対応させた新しいファームでは、エンコーダ用に使うキーが加わったことによって、レイアウトが変更になっています。このため、VIAで接続するとレイアウトがズレてしまって使い物になりません。それならば新しいバージョンをPRすればいいのではないかというと、こちらも問題があります。従来のファームウェアで利用しているユーザーがVIAに繋いだ際にレイアウトがズレてしまうのです。販売済みの全数を新ファームに移行出来れば良いのですが、とはいえ、現行ユーザーのみなさんにファームウェア更新を強要することはしたくありません。とくにエンコーダを実装していないユーザーさんにとってはメリットがないのでなおさらです。

解決策のひとつは、新たに別のキーボードとしてPRすることです。ここで言う別のキーボードとは、VIDとPIDの組み合わせが異なることを意味しますから、キーボード名のディレクトリ下にrev2などディレクトリを設け、以前とは異なるVID/PIDが書かれたconfig.hを置くことでめでたく別物となります。しかし、同一のハードウェアに複数のVID/PIDを付けることには躊躇いがあります。

そんなときでも救いの手を差し伸べてくれるのがRemapです。RemapはQMK本家にマージされていないものでもフォークしたリポがありさえすれば受け付けてくれますし(VIAはQMK本家へのマージが必須)、その上、キーボードの機種を識別する際にPIDとVIDの2つのみを用いるVIAとは違って、Remapでは製品名(PRODUCTマクロで定義)も組み合わせるため、異なる製品名を付けることで、初期のAleth42とは別のキーボードとしてエンコーダの割り付けに対応した新ファーム用のjsonを登録することが出来るのです。

最新のAleth42では下記のように場合分けをして製品名を変えています。

				
					#ifdef ENCODER_ENABLE
#define PRODUCT ALETH42 with encoder
#else
#define PRODUCT ALETH42
#endif
				
			

とはいってもレイアウト別に2系統のファームをリリースすることはお勧めしません。上記のようにPRODUCTを場合分けするようなやり方もQMK本家の方針とは相いれないと思います(自分のフォークでやりましょう)。これから新たなキーボードを設計する場合は、最初からVIA/Remapでのエンコーダ割り付けに対応したファームウェアを書くことをおすすめします。

KNOBそれはエンコーダにとってのキーキャップ

勉強的な内容が続いたので、お買い物系の情報も盛り込んでおきましょう。

エンコーダを付けると悩むのがノブです。ノブには操作しやすさという機能面とともに、キーボードにアクセントをもたらすデザイン面にも注目したいところです。スイッチにはめるキーキャップに凝るように、ノブにもこだわってみようというのがここでのテーマになります。

みなさんはノブの入手はどうされていますでしょうか? 遊舎工房さんで販売されているエンコーダにはノブが付属していますので、そちらを使われている人も多そうです。アルミ製のノブが付いて比較的安価に販売されていますので助かります。

そのほかのものを探すとなると自作キーボード界隈の外に求めることになります。良い機会なので、わたしが気に入っているいくつかのノブを紹介したいと思います。

1. Fit-io ニブルス・メキシカン15

筆者が以前に販売していたCassette42のフルキットに付属させていたものです。写真はKalihロープロV1に合うように選んだ低背のALPS EC11N1524402にはめています。ソフトな触り心地の樹脂製で操作性がよく、先細りの形状は込み入ったレイアウトでも指が入りやすいです。Fit-ioはこれ以外にもエンコーダ用のノブを製造されています。

Fit-io

2. DJ Techtools Chroma Caps 

DJ機材用の交換ノブとして人気のシリーズです。エンコーダ用には18.5mmの太めのものと、14.2mmの細めのものの2種類があります。これも非常に操作性の良い樹脂製です。

3. KILO OEDNIシリーズ

上品なローレット加工が施されたアルミ製の美しいノブです。使用しているエンコーダはALPS EC11N1524402です。サイズ違い、軸受け違い、色違いがあり、写真のものはOEDNI-75-3-7という品番になります。なお、固定用のイモネジがレンチでなかなか締まらず、サイズが合わないななどと困っていたところ、ある日なんとはなしにトルクスドライバを突っ込んでみたらしっかり締まりました。AnexのT6-30でぴったりです。

Kilo International

4. Ken Smith Knobs

Ken Smithといえば美しい木材を使ったボディが特徴のアメリカのベースメーカーです。そのこだわりの木材から作られるノブがまた素晴らしい。本来はエレキギター/ベースのポット(可変抵抗)類に付けるためのものなのでインチ規格なのですが、写真のものは3種の異なる木材を合わせて6mm軸用(ロータリーエンコーダの多くは6mmです)に加工した特注品。販売用に仕入れたいものの、常に製造しているものではないようでなかなか難しい状況です。入荷出来そうになりましたらお知らせします。

おわりに

チャタリングの実際とハードウェアによるデバウンスや、割り込みでの入力についての実験についても触れるつもりでしたが、長くなり過ぎましたのでこのあたりで。とりあえず自作キーボード開発に関係するエンコーダの基礎の基礎的情報から、2021年現在のQMKのトレンドに乗った実装についてはおおよそ網羅出来たと思います。少しでもみなさんの参考になりましたら幸いです。

最後に現在開発中(っていくつあるんだという感じですが)のZindoxをチラッと見てやってください。構想2年半(実作業的には半年もないですが)の結果、いまはこんな状態。チャタリング防止のCR付きでエンコーダは2連のものを搭載してみました。使いやすいかどうかは…まあいいじゃないですか、乗せてみたかったんです。コックピットみたいでわくわくするでしょう?

※この記事はAleth54、Aleth42、Attack25rev3を使って、Cassette42で音楽を再生しながら書きました。

Share on twitter
Twitter

コメントを残す