TPS43 で円周スクロール(クルクル)を実装した

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 から鳴らせるので、次に基板を作り直すときに載せてみたい。厚みが増えるのが悩みどころ。

Copyright © 髭。/ Hugo + hugo-book