TPS43 で円周スクロール(クルクル)を実装した #
実装だけじゃなく、ブログ記事まで Claude に手伝ってもらったよ。
人間が書いた部分 #
Let’s Note のタッチパッドのクルクルいいよねぇという X の投稿を見て、完全同意したので、TPS43 で実装の練習したら、2本指スクロールよりよっぽど使いやすかったので即採用。
ソースコードは GitHub においてる
ので興味がある方はそちらを見ていただくとして、自分用メモとして、Claude に記事を書いてもらったよ。
調査、実装、ソースコードの精査・コメント、生成 AI がなかったら、途中でいやになってるよ、きっと。
説明用ブログ記事も、ワシが書くより読みやすい記事書いてくれそうなので、以下、全部 Claude です。
Kewbie36tp に載せている角型トラックパッド TPS43 で、指で円を描くとスクロールする「円周スクロール」を実装した。iPod のクリックホイールみたいに、クルクル回すとページが流れていくアレ。
思った以上に快適で、今ではすっかり手放せなくなっている。
きっかけ #
最初は丸いタッチパッドが欲しかった。Cirque の円形トラックパッド(TM040040 など)は QMK が標準で円周スクロールに対応していて、外周をなぞると縦・横スクロールになる。
ただ、国内だと入手しづらい。Mouser では I2C 版の取り扱いがなく、SPI 版を買って抵抗を外す必要がある。送料も考えると、ちょっと試すには敷居が高い。
そこで発想を変えて、手持ちの TPS43 で同じことをやることにした。サイズは 43×40mm で、短辺いっぱいに円を取れば Cirque 40mm とほぼ同じ感覚になるはず。
仕組み #
なぜ絶対座標が要るのか #
普段のカーソル移動に使っているのは相対座標、つまり「前回からどっちにどれだけ動いたか」だけ。これだけでは円周スクロールはできない。
たとえば「指が右に動いた」という情報だけだと、
- 円の上側で右に動いたなら時計回り
- 円の下側で右に動いたなら反時計回り
と、指がパッドのどこにいるかで意味が逆になる。回転の向きを決めるには、「中心から見て指がどの方向にいるか」、つまり絶対座標が必要になる。
相対座標を足し合わせて位置を推測する手もなくはないけど、触り始めた位置がわからないので、どこが中心なのか決められない。外周から触り始めたら円周スクロールにする TP_RING モードも、触った場所そのものがわからないと判定できない。
QMK の Azoteq ドライバは絶対座標を捨てている #
TPS43 に載っている IQS572 は、指の絶対座標をレジスタに持っている。ところが QMK 標準の azoteq_iqs5xx ドライバは、0x000C から 10 バイトだけ読んで通信を終えていて、相対移動量しか使っていない。
| アドレス | 中身 |
|---|---|
| 0x000C | 前回の処理時間 |
| 0x000D–0E | ジェスチャー |
| 0x000F–10 | システム情報 |
| 0x0011 | 指の本数 |
| 0x0012–15 | 相対 X / Y |
| 0x0016–19 | 1本目の指の絶対 X / Y |
絶対座標はそのすぐ後ろにある。これは Linux カーネルの drivers/input/touchscreen/iqs5xx.c で確認した。Linux のドライバは 0x000F から構造体ひとつぶんをまとめて読んでいて、その構造体の並び(__packed なので隙間なし)を先頭から数えると、1本目の指の絶対座標が 0x0016–19 にあるとわかる。QMK が読んでいる 0x000F–15 の部分(指の本数・相対移動量)は、Linux の構造体とも一致する。
じゃあ 0x0016 を別に読めばいいかというと、そうもいかない。IQS5xx は計測が終わるたびに通信を受け付けて、0xEEEE に書き込むと通信を閉じて次の計測に入る。QMK のドライバは 10 バイト読んだ直後に自分で通信を閉じてしまうので、同じ計測のデータを追加で読む隙がない。次の周期に 0x0016 から読むことはできるけど、それだと相対移動量と絶対座標が別の計測のデータになってしまうし、1回ぶんのデータをそろえるのに2周期かかる。
なので、QMK の読み始め(0x000C)から 0x0019 まで、14 バイトを1回の通信でまとめて読むのが正解。ところが QMK のドライバは読み出しが 10 バイト固定で、設定では変えられない。QMK 本体を書き換えずに済ませるため、読み出し部分を自前で書くことにした。
自前ドライバにした #
最初は QMK の Azoteq ドライバの関数を呼んで初期化だけ任せようとしたけど、vial-qmk のバージョンによって関数の戻り値やマクロの有無が違っていて、リンクエラーやコンパイルエラーが続いた。最終的に、初期化も含めて POINTING_DEVICE_DRIVER = custom の自前ドライバにした。中身は QMK のドライバの手順を移植したもので、AZOTEQ_IQS5XX_* の設定はそのまま使える。
# rules.mk
POINTING_DEVICE_ENABLE = yes
POINTING_DEVICE_DRIVER = custom
I2C_DRIVER_REQUIRED = yes
SRC += tps43_driver.c tp_circular.c
角度の変化をスクロール量にする #
やっていることは単純で、パッドの中心からの角度を毎フレーム求めて、前回との差 × 半径(=弧の長さ)をスクロール量にしているだけ。
// 画面座標系(Y が下向き)では、atan2 の増加方向が時計回りになる
const float angle = atan2f(dy, dx);
float d = angle - g_prev_angle;
if (d > (float)M_PI) d -= 2.0f * (float)M_PI; // ±180度をまたいだとき
if (d < -(float)M_PI) d += 2.0f * (float)M_PI;
g_prev_angle = angle;
// 弧の長さ → チップの2本指スクロールと同じ単位にそろえる
const float arc = d * r * g_res_x / PAD_W_001MM + g_remainder;
ポイントはいくつか。
- X と Y で解像度が違う(2048×1792)ので、いったん mm 単位にそろえてから角度と半径を計算する
- 中心付近は無視する。中心に近いと少し動いただけで角度が大きく変わって暴れるため
- 端数は繰り越す。ゆっくり回したときに、スクロールが出ずに消えてしまうのを防ぐ
- 単位をチップの2本指スクロールにそろえたので、既存のスクロール速度設定や慣性スクロールがそのまま効く。回してから指を離すと、惰性でしばらく回り続ける
判定ロジックは QMK に依存しない形で tp_circular.c に切り出してあって、PC 上の gcc で単体テストしながら作った。絶対座標が取れるパッドなら、TPS43 以外でもそのまま使えるはず。
モードを Vial で切り替える #
使っているうちに欲が出て、モードを3つ用意した。
| キーコード | 動き |
|---|---|
TP_RING |
外周から触り始めたら円周スクロール、内側はカーソル |
TP_CIRC |
どこから触っても円周スクロール |
TP_DRAG |
動かした方向に縦横スクロール |
切り替えは Vial 上でタッチパッドを1つのキーとして扱う形にした。マトリクスの空き位置をタッチパッドの「仮想キー」にして、レイヤーごとにそこへモードのキーコードを置く。
uint8_t tp_requested_mode(void) {
if (g_mode_key_held != KC_NO) { // 実キーに置いた場合は押している間
return mode_from_keycode(g_mode_key_held);
}
const keypos_t pos = {.row = TP_VKEY_ROW, .col = TP_VKEY_COL};
const uint8_t layer = layer_switch_get_layer(pos); // KC_TRNS も解決してくれる
return mode_from_keycode(keymap_key_to_keycode(layer, pos));
}
今の設定はこんな感じ。
- レイヤー0:カーソル(または
TP_RING) - 左親指のレイヤー1:
TP_CIRC - 右親指のレイヤー2:
TP_DRAG
親指キーを押しながらパッドに触れると、そのモードで動く。一度始まったら、キーを離しても指を離すまで続くようにしたのが地味に効いていて、回し始めたら親指は自由にしておける。
つまずいたところ #
2本指で回す方式は失敗 #
カーソル移動と円周スクロールの誤爆を減らそうと、「1本指はカーソル、2本指をそろえて回すとスクロール」も試した。2本目の指の座標も読んで、中間点の角度を見る方式。
これは失敗だった。43×40mm のパッドでは2本指で回すには狭すぎて、スクロールに入らなかったり、途中で片方の指が浮いて2本指タップ(右クリック)と誤認識されたりした。結局、キーとの組み合わせで切り替える今の形に落ち着いた。
横スクロールの向き #
縦横スクロール(TP_DRAG)を作ったら、縦と横で向きの感覚が逆になった。HID のホイールは「縦は正が上、横は正が右」なので、指の移動量をそのまま入れると、縦は指にコンテンツがついてくる向き、横は逆向きになる。横の符号を反転してそろえた。
RDY ピンを見て読むようにした #
IQS572 は 10ms ごとに計測していて、計測中に話しかけると、終わるまで I2C のクロックを止めてマイコンを待たせる(クロックストレッチ)。QMK は1本の処理ループでキーの読み取りもしているので、その間はキーの読み取りも止まってしまう。
TPS43 の RDY ピン(計測が終わると High になる)を見て、High のときだけ読むようにした。待たされることがなくなるので、キー入力への影響を小さくできる。
おまけ:vial-qmk を久々に更新したら Tap-Hold が変わった #
今回、vial-qmk を長いこと更新していなかったので、ついでに本家に追従した。すると、LT(1, KC_SPC) や LGUI_T(KC_TAB) のようなタップとホールドを兼ねるキーで、Cmd+C が Tab → C に、Shift+A が z → a になる誤判定が増えた。
最初はトラックパッドの通信を疑って色々試したけど、結論としては Tap-Hold の設定だった。
- 長期間の更新で、QMK の Tap-Hold 判定の中身がかなり変わっていた
- Vial は新しいファームを書き込むたびに、キーマップと QMK Settings を既定値に戻す(ビルドごとにランダムな ID が埋め込まれていて、それが変わると EEPROM を初期化する)
2つ目を知らずに、テスト用のファームを書き込むたびに Permissive Hold が無効に戻っていて、しばらく原因を見誤っていた。今は、EEPROM が初期化されたときに Permissive Hold を有効にするようにファームに仕込んである。
Vial でキーマップをいじっている人は、書き込む前に「File → Save current layout」で .vil に保存しておくのがおすすめ。
コード #
ファイルごとの役割はこんな感じ。上の2つがトラックパッドまわりの中心で、下の2つはキーボード固有の部分。
tps43_driver.c … TPS43 と話す(読む・初期化する・モードに振り分ける)
tp_circular.c/h … 座標から円周スクロール量を計算する(ハード非依存)
config.h … ピン配置と、各種しきい値の設定
kewbie36tp.c … モードを決めるキーコードと、慣性スクロールなど
tps43_driver.c:TPS43 のドライバ #
QMK の POINTING_DEVICE_DRIVER = custom として動く、TPS43 専用のドライバ。
- 初期化:リセット、計測周期(10ms)、ジェスチャー(タップ・2本指タップなど)、回転の設定。QMK の Azoteq ドライバの手順を移植したもので、
AZOTEQ_IQS5XX_*の設定がそのまま効く - 読み出し:RDY ピンが High のときだけ、
0x000Cから 14 バイトを1回で読む。相対移動量と絶対座標を同時に取る - 振り分け:今のモードを見て、カーソル移動・円周スクロール・縦横スクロールのどれとして HID レポートを作るか決める
ハードに依存する処理は全部ここに集めてある。別のトラックパッドに移植するなら、書き直すのはこのファイルだけ。
tp_circular.c / tp_circular.h:円周スクロールの計算 #
指の本数と絶対座標を受け取って、「中心の周りをどれだけ回ったか」をスクロール量として返す。QMK には依存していないので、PC の gcc でそのまま単体テストできる。
- 絶対座標を mm に換算して、中心からの距離と角度を求める
- 前回との角度差 × 半径(弧の長さ)をスクロール量にする。端数は次回に繰り越す
- 開始の条件(外周から触り始めたとき/どこからでも)を呼び出し側が指定できる
- 一度始まったら、指を全部離すまで続く
tp_circular.h には、パッドの寸法や判定のしきい値の既定値と説明をまとめてある。
config.h:設定 #
キーボード側で変えたい値はここに置く。円周スクロールに関係するのはこのあたり。
#define TPS43_RDY_PIN GP21 // RDY を見て読む(外すと従来の固定間隔で読む)
#define POINTING_DEVICE_TASK_THROTTLE_MS 1 // RDY を 1ms ごとに確認
#define TP_CIRC_DIAMETER_MM 38 // 回転の基準円の直径
#define TP_CIRC_RING_PCT 70 // TP_RING:半径の 70% より外で触り始めたら円周スクロール
/* #define TP_CIRC_INVERT 1 */ // 回転方向を逆にする
/* #define TP_CIRC_DEBUG */ // 座標をコンソールに出す
外周リングの幅や回転方向は、実機で触りながらここで調整する。
kewbie36tp.c:キーボード側の処理 #
TP_CIRC/TP_DRAG/TP_RINGのキーコード処理と、タッチパッドの仮想キーからモードを決めるtp_requested_mode()- スクロール速度・カーソル速度・慣性スクロールなど、以前から入れていた処理(円周スクロールにもそのまま効く)
- EEPROM 初期化時に Permissive Hold を有効にする処理
- 起動時に TPS43 の RST(GP20)へリセットパルスを入れる処理。マイコンだけがリセットされたとき(書き込み直後など)に、TPS43 が I2C の途中状態で固まっているのを確実に戻す
初期化部分は QMK の Azoteq ドライバを移植しているので、ライセンスは GPL-2.0-or-later。
次にやりたいこと #
クルクルに合わせて「カチッ」と振動が返ってきたら、もっと気持ちいいはず。LRA(リニア振動モーター)と DRV2605L を使えば QMK から鳴らせるので、次に基板を作り直すときに載せてみたい。厚みが増えるのが悩みどころ。