gamepad-over-920MHz
PublicLoading…
gamepad-over-920MHz
USB ゲームパッドを 920MHz 帯(IM920sL)で無線化するファームウェアです。 Bluetooth より遠くまで届く無線リンクに載せ替えつつ、PC からは普通のゲームパッドのままに見せます。 ロボットの遠隔操縦を想定しています。
[USBゲームパッド] ─USB(PIO-USB)→ [送信機 XIAO RP2040] ─UART→ [IM920sL]
│ 920MHz
[PC] ←USB HID + CDC─ [受信機 XIAO RP2040] ←UART─ [IM920sL] ←─────┘
特徴
- 軸番号・ボタン番号が元のパッドと一致する。 送信機がパッドの HID レポート
ディスクリプタを解析して軸構成を無線で送り、受信機がそれに合わせて HID
ディスクリプタを実行時に組み立てます。受信機側の設定は不要で、
/dev/input/js0の番号が直挿しと同じになるので、既存のソフトをそのまま使えます。 - フェイルセーフ。 有効なパケットが 300ms 途絶えると、受信機は全軸中立・ 全ボタン OFF を出力します(実測で約 300ms、ログにも残ります)。
- リンクの生死が送信機側でも分かる。 受信機から 2 秒ごとにハートビートを返し、 「受信機が前方向を受信できているか」まで含めて LED に反映します。
- チャンネル自動選定。 起動時に 31〜45 の雑音を測って空いているチャンネルへ移動します。 複数ペアを同時に運用しても電波を取り合いません。通信が途絶えたら待ち合わせ用の ch31 へ自動で戻ります。
- 暗号化(AES-GCM 256bit)。 同じグループ番号さえ合えば誰でも操作できてしまう 状態を防げます。
- USB CDC コンソール。 設定・ペアリング・電波品質の確認・配線の切り分けができます。
Linux と Windows の両方で動作を確認しています(macOS は未確認ですが、標準の HID と CDC しか使っていないため動く見込みです)。
必要なもの
| 品目 | 備考 | |
|---|---|---|
| 送信機 | Seeed XIAO RP2040 ×1 | CPU 240MHz で動かす(PIO-USB の要件) |
| 送信機 | IM920sL + IM920sL-ADP ×1 | 変換アダプタ経由で 2.54mm に |
| 送信機 | USB Type-A メスコネクタ | ゲームパッド接続用 |
| 送信機 | モバイルバッテリー | 実測の消費電流は約 60mA |
| 受信機 | Seeed XIAO RP2040 ×1 | PC に USB-C で接続 |
| 受信機 | IM920sL + IM920sL-ADP ×1 |
配線は docs/schematic/gamepad_wireless_schematic.svg(Rev.3)、 部品表は docs/gamepad_wireless_BOM.xlsx を参照してください。
基板(送信機・受信機で共通、100×62mm 2層)の KiCad データと発注用ガーバーは hardware/ にあります。ブレッドボードやユニバーサル基板で組む場合は docs/netlist.md にネットリストとレイアウト制約があります。
IM920sL 本体と変換アダプタの取扱説明書は、メーカー(インタープラン)の配布物なので リポジトリには含めていません。https://www.interplan.co.jp/ から入手し、
docs/IM920sL_manual.pdf/docs/IM920sL_ADP_manual.pdfとして置くと、 本 README 中の参照(コマンド仕様・端子表・チャンネル表)と対応します。
最短の始め方
# 1. ビルド環境(arduino-cli)
arduino-cli config add board_manager.additional_urls \
https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json
arduino-cli core update-index
arduino-cli core install rp2040:rp2040
arduino-cli lib install "Pico PIO USB" "Adafruit NeoPixel"
# 2. 書込み
./build.sh rx upload /dev/ttyACM0 # 受信機
./build.sh tx upload /dev/ttyACM1 # 送信機
# 3. ペアリング(初回だけ。両機を 50cm 以内に置く)
# 起動後 5 秒以内(LED が水色の速い点滅)か、赤点滅中に
# 受信機 → 送信機 の順に BOOTSEL を 3 秒以上長押し
# 4. 確認
tools/jsinfo.py 10 # /dev/input/js0 の軸数・ボタン数とライブの値
うまくいくと両機とも緑点灯になり、PC に irlab Wireless Gamepad として
ゲームパッドが現れます。
詳しい操作は操作(BOOTSEL ボタン / CDC コンソール)、 うまくいかないときは診断に使えるコマンドを参照してください。
免責
**個人が趣味・研究用に作った原理試作です。**動作・性能・安全性のいずれも保証しません。
- 無線の到達距離や通信の安定性は、設置場所の電波環境に大きく左右されます。 同じ構成でも結果は異なります。
- ロボットなど**物理的に動くものを制御する用途では、本装置とは独立した非常停止手段を 必ず用意してください。**受信側に 300ms のフェイルセーフを実装してありますが、 これは「通信が途絶えたら中立を出力する」だけのもので、機器の安全を保証するものでは ありません。
- 本リポジトリの成果物(ファームウェア、回路図、基板データ、資料)を使用した結果 生じたいかなる損害についても、作者は責任を負いません。
- 920MHz 帯の利用は国と地域の法令に従ってください。IM920sL は日本国内の電波法認証を 取得済みの製品ですが、モジュールを改造したり国外で使用したりする場合は、 使用者の責任において適法性を確認してください。
不具合や改善点を見つけたら Issue や Pull Request で教えていただけると助かります。
ライセンス
MIT License(LICENSE)。無保証です(上記「免責」および LICENSE の "AS IS" 条項を参照)。
以下は実装と実機検証の詳細です。立ち上げで踏んだ問題(PIO-USB の 240MHz 要件、 TxD/RxD 逆接続の切り分け、コア1 だけが死ぬ故障モードなど)も記録してあります。
ビルドと書込み
./build.sh tx # 送信機をビルド
./build.sh rx # 受信機をビルド
./build.sh tx upload /dev/ttyACM0 # ビルドして書込み
必要な環境:
- arduino-cli +
rp2040:rp2040(earlephilhower コア) - ライブラリ
Pico PIO USB、Adafruit NeoPixel - FQBN
- TX:
rp2040:rp2040:seeed_xiao_rp2040:usbstack=tinyusb,freq=240(PIO-USB は 12MHz の倍数が必要。120MHz ではタイミング余裕が足りず列挙に失敗したので 240MHz。WG_FREQ=120 ./build.sh txで変更可) - RX:
rp2040:rp2040:seeed_xiao_rp2040:usbstack=tinyusb
- TX:
書込みは 1200bps タッチで自動的にブートローダへ入る。入らないときは BOOTSEL を押しながら
USB を挿し、RPI-RP2 に build/<role>/*.uf2 をコピーする。
構成
firmware/common/ TX・RX 共通(arduino-cli には --library で渡す)
packet.h 無線パケット(State 12 バイト / Info 10 バイト)
im920sl_config.h IM920sL の設定・送受信(元からあったもの+追記)
im920_diag.h 配線切り分け用の diag / raw / scan
hid_gamepad_map.h パッドの HID ディスクリプタを解析して状態を取り出す(TX)
hid_desc_builder.h HID ディスクリプタを実行時に組み立てる(RX)
status_led.h オンボード LED の状態表示
firmware/tx/tx.ino 送信機
firmware/rx/rx.ino 受信機
tools/console.py CDC コンソールにコマンドを送る補助ツール
状態表示 LED
XIAO RP2040 のオンボード NeoPixel(GPIO12/電源 GPIO11)と三色 LED(R17/G16/B25)を同時に駆動する。
| 色 | 状態 |
|---|---|
| 赤の点滅 | 接続なし(TX: パッド未接続か送信が通らない/RX: フェイルセーフ中) |
| 黄の点滅 | ペアリング中 |
| 緑の点灯 | 接続中(送信機は受信機からの応答が届いているときだけ) |
| 青の点灯 | 設定ブリッジ中 |
| 白の点滅 | 起動処理中 |
| 水色の速い点滅 | BOOTSEL ボタン受付中(起動直後 5 秒) |
| 赤紫の速い点滅 | IM920sL の設定に失敗 |
操作(BOOTSEL ボタン / CDC コンソール)
SW1 は未実装なので BOOTSEL ボタンを使う。電源投入時に押したままだと書込みモードに 入ってしまうため、電源を入れてから押す必要がある。押せるタイミングは 2 つ:
1. 起動直後の受付ウィンドウ(LED が水色の速い点滅、5 秒間)
電源を入れると 白点滅(起動処理)→ 水色の速い点滅(受付中) と変わる。 水色の間に押す。
2. 運用中、リンクが確立していないとき(LED が赤点滅)
組み立て直後でまだペアリングしていない状態は赤点滅になるので、そのまま押せばよい。 緑点灯(正常動作中)のときはボタンを一切読まないので、運用中の通信を乱すことはない。
- 短押し(3 秒未満)→ 設定ブリッジ
- 3 秒以上の長押し → ペアリング(TX=子機、RX=親機。両機を 50cm 以内に置く)
押している間は LED が黄点滅になるので、3 秒の長押しはそれを見ながら数えられる。
BOOTSEL の読み取りは XIP(フラッシュ実行)を一瞬止めてもう一方のコアを idle にするため、 PIO-USB が動作中は避けたい。そのため送信機では、パッドが列挙済みになるか起動から 15 秒経つまでは運用中のポーリングを行わない。
組み立て後の初回手順
- 受信機を PC に、送信機にゲームパッドとモバイルバッテリーを繋ぐ
- 両方とも赤点滅になる(=まだペアリングしていない)
- 両機を 50cm 以内に置く
- 受信機の BOOTSEL を 3 秒以上長押し(親機として登録パケットを送る)
- すぐに送信機の BOOTSEL も 3 秒以上長押し(子機として受け取る)
- 成功すると両方とも緑点灯になる
ペアリングは Flash に保存されるので初回だけでよい(ペアリングは初回だけ 参照)。
USB-C の CDC コンソール(tools/console.py か screen /dev/ttyACM0 115200):
| コマンド | 内容 |
|---|---|
help | コマンド一覧 |
status | IM920sL の設定値、受信状況(RSSI・seq 欠番)、パッドの状態 |
config | AutoConfig(目標値と違う項目だけ ENWR で書込み) |
pair | ペアリング |
bridge | USB CDC ↔ IM920sL の透過ブリッジ |
dump | (TX)HID 生レポートの表示 |
map | (TX)解析した HID マップ |
profile / reset | (RX)HID 構成の表示/既定値に戻して再起動 |
diag / raw <cmd> / scan | IM920sL の配線・応答の切り分け |
ペアリングは初回だけ
グループ番号は ENWR でモジュールの Flash に書かれる不揮発設定なので、電源を切っても残る。
実測(両機を再起動して確認):
再起動前: TX RDGN=0001D9D8 / RX RDGN=0001D9D8
再起動後: TX RDGN=0001D9D8 / RX RDGN=0001D9D8 ← 保持
link: UP RSSI=-19dBm packets=116 bad=0 ← 自動で復帰
ペアリングが要るのは 初回と、どちらかのモジュールを交換したとき
(グループ番号は親機の固有 ID から決まる)、あるいは RDGN が FFFFFFFF に
戻ってしまったときだけ。ファームを書き直しても不要(im920AutoConfig() は
STNN/STRT/STCH/STPO/STTR しか触らない)。
⚠ Flash の書換えは全項目合計で 10 万回まで。ペアリングを不必要に繰り返さないこと。
im920AutoConfig() も「目標値と違う項目だけ」書くので、設定が合っていれば書き込まない。
軸番号・ボタン番号を元のパッドに合わせる仕組み
既存の操縦ソフトが /dev/input/js0 を直接読んでいる場合、軸とボタンの並びが
元のパッドと一致していないと、受信機に差し替えた途端に割り当てがずれる。
joydev は ABS_* コードの昇順に軸番号を振る
(X=0, Y=1, Z=2, Rx=3, Ry=4, Rz=5, Hat=6,7)ので、元のパッドに在る軸の集合が
一致していれば番号も一致する。そのため、
- TX は VID:PID の決め打ち表ではなく、HID レポートディスクリプタを解析して Usage(X, Y, Z, Rx, Ry, Rz, Hat switch, Button 1..n)の位置を求める。 → どのパッドでも Linux の hid-generic と同じ割り当てになる。
- TX は軸の種類(axisMask)とボタン数を Info パケットで 2 秒ごとに送る。
- RX は受け取った構成を EEPROM に保存し、HID ディスクリプタを実行時に組み立てる。 構成が変わったときは保存して自動で再起動し、新しいディスクリプタで列挙し直す。
つまり 受信機側の設定は不要。送信機にパッドを繋げば、受信機が自動的に同じ形の ゲームパッドとして PC に見えるようになる(初回は 1 回だけ自動再起動する)。
dump で生レポート、map で解析結果、RX の profile で現在の構成を確認できる。
Linux 以外の OS で使えるか
受信機は標準的な USB HID ゲームパッド+CDC の複合デバイスなので、Windows / macOS でも 追加ドライバなしで動く。実際のディスクリプタ:
bDeviceClass 239 / bDeviceSubClass 2 / bDeviceProtocol 1 ← IAD 複合デバイス宣言
インタフェース 0,1: CDC(Communications + CDC Data)
インタフェース 2 : HID(Usage Page = Generic Desktop, Usage = Game Pad)
- HID: Windows 標準の HID ドライバで動く。
joy.cpl・DirectInput・RawInput から見える。macOS も標準対応 - CDC: Windows 10/11 は
usbser.inf内蔵で COM ポートとして見える。macOS は/dev/cu.usbmodem* - IAD 宣言がないと Windows が複合デバイスを正しく分解できないが、TinyUSB が付けている
注意点
- XInput ではない。Windows で XInput 専用のゲームからは見えない。 これは F310 を D モードで使うのと同じ制約。 DirectInput / RawInput 対応のソフトなら問題ない。
- 軸・ボタン番号は、Linux の joydev が ABS コード順、Windows の DirectInput が HID Usage 順で決まるので、同じ Usage を出している限り一致するはず。Windows 実機は未確認。
- VID:PID は Adafruit の既定値
239a:cafeのまま。機能上は問題ないが、 外部に配布するなら自前の PID を用意するのが望ましい。
Windows 向けに入れてある対策
- シリアル番号の衝突回避 — 手元の 2 枚は同じチップ ID(
4250305539333104)を返す。 Windows は VID/PID/シリアルでインスタンスを識別するため、同じ PC に両方挿すと衝突しうる。 役割の接頭辞を付けている(WGTX-.../WGRX-...)。 - HID ディスクリプタのキャッシュ対策 — Windows はレポートディスクリプタを
VID/PID/REV 単位でキャッシュする。受信機はプロファイル学習でディスクリプタを
作り直すので、構成から
bcdDeviceを生成して別リビジョンとして見せている (例: axisMask 0x27・ボタン 12・ハットあり →bcdDevice = 27.19)。
取扱説明書との照合(docs/IM920sL_manual.pdf Rev.1.5)
公式資料と全コマンド・全設定値を突き合わせ済み。仕様違反は無かった。
-
ch31+高速モード(STRT 1)は仕様上有効。
STCHは「01〜29、31〜45」とだけ規定され、 通信モードによる制限は無い(モードが決めるのは既定値のみ)。 4s モード(31〜45)は「1 時間あたりの送信時間の総和 制限なし」と明記。 電波法上の処理はモジュール内部で自動なので、ファーム側で duty を管理する義務は無い。 -
発行している 14 コマンドの名称・引数書式・応答・前提条件・順序依存はすべて資料どおり (
STNN 0001に ENWR 必須、STRT→STCHの順、ペアリング後は RESET 端子必須)。 -
受信行
aa,bbbb,dd:、RSSI は符号付き(9Ch=−100dBm)、TXDA は 1〜32 バイト、 送信時間4.56ms + 80µs × バイト数(12 バイトで 5.52ms)— すべて一致。 -
触っている設定は次の 5 つだけ(それ以外は初期値のまま)。
コマンド 内容 設定値 STNNノード番号 受信機(親機)= 0001、送信機 = 0002 STRT通信モード 1(高速 100kbps) STCHチャネル 31(起動後に空きチャネルへ移動) STPO送信出力 2(10mW) STTRキャリアセンス NG 時の自動リトライ 00(再送はファーム側で判断する) -
ADP の J1/J2 対応も回路図で確認。
J1-n = 本体 pin(2n−1)、J2-n = pin(2n)。 本体 pin1 は資料によりIO1/BUSY、ADP 回路図ではIO1/RTSと表記が違うが同じ端子。
ユニキャストと ACK について
TXDU(ユニキャスト)は実在し「OK は相手に届いたことを示す」と明記されている。
ただしルート探索を行い、失敗時は自動で最大 10 回リトライするため遅延が大きくばらつく
(資料も「リアルタイム性が要求される用途では十分ご注意ください」と注意喚起)。
30ms 周期の操縦データには不向きなので採用していない。
各パケットは差分ではなく現在の全状態を持つので、落ちても次のパケットが完全に 上書きする。再送は「古い状態を届けたうえに新しい状態を遅らせる」ため逆効果。 取りこぼしへの対処は受信側の 300ms フェイルセーフで行う。
なお TXSB(センドバック送信)はルート探索なしで送信元へ返せるので、
ハートビートをブロードキャストからユニキャストに変えたい場合の選択肢になる(未採用)。
4s モードの送信休止規定(2026-09-19 に再確認)
取扱説明書 表1・§7-3 より、ch31〜45(4s モード)の規定は次のとおり。
| 項目 | 4s モード(ch31〜45) | 360s モード(ch01〜29) |
|---|---|---|
| キャリアセンス時間 | 約 6ms | 128µs〜1ms |
| 送信休止時間 | 52ms 以上 | 2ms 以上 |
| 1 時間あたりの送信時間の総和 | なし | 360 秒以内 |
| 送信時間制限 | 3.9 秒以下 | 400ms 以下 |
「1 時間 360 秒」の総和制限は ch01〜29 だけの話で、本プロジェクトの ch31〜45 には無い。
代わりに効くのが送信休止 52ms で、挙動は「最初の送信から 3.9 秒を超えたら 52ms 休止」 「4 秒経過前に 52ms 以上空けば窓がリセットされる」。 1 回の占有はキャリアセンス約 6ms + 送信 6ms(12 バイト時 5.52ms を切り上げ)= 約 12ms なので、計算上は 64ms 間隔あれば窓がリセットされ続けるはず。
ただし実測はこの計算と合わない。 ブリッジ経由でファームを介さず TXDA を打った結果:
| 送信間隔 | 55ms | 65ms | 75ms | 90ms | 120ms | 150ms |
|---|---|---|---|---|---|---|
| NG 率 | 20.0% | 17.5% | 12.5% | 8.0% | 4.0% | 0% |
64ms を超えても NG は残り、0 になるのは 150ms 以上。機構は特定できていない。
なお NG は一度も連続しなかった(90ms で 12 回、120ms で 6 回、すべて単発)。 欠落は最悪 2 周期分なので、受信側フェイルセーフ 300ms には届かない。
⚠ 暗号化を有効にすると 1 パケットが 24 バイト増える(§7-4)ので、 12 バイトのパケットは 36 バイト相当(7.44ms)になる。上の測定は暗号化なしの値。
UART の高速化(115200bps)
12 バイト送信の UART 時間は 19200bps で約 16ms あり、電波の送信時間 5.5ms の 3 倍を
食っていた。SBRT 7 で 115200bps にすると約 2.7ms になる。
SBRT は資料に「Flash メモリに記憶します」の記載が無く揮発性とみられるため、
毎起動で設定しても Flash 書換え回数を消費しない。起動時に im920SetupBaud() が
RDVR の応答でモジュールの現在のボーレートを判別し、必要なら 115200 に上げる。
判別に失敗したときは元のボーレートのまま動作を続ける。
パケット暗号化+送信元認証
AES-GCM 256bit。同じグループ番号さえ合えば誰でもロボットを操作できる状態を防ぐ。
enc <16進64桁> キーを設定して有効化(両機に同じキーを入れること)
enc off 無効化(キーは残る)
STKYは ENWR 中のみ実行可能で Flash に記憶される(読出しコマンドは無い)ため、 明示的なコマンドのときだけ設定する- 通信時間が 24 バイト分(高速モードで約 1.9ms)増える
- ペアリング(STGN)時は常に非暗号化パケットなので、順序はどちらでもよい
- 実測: 暗号化オンで
ng=0%、bad=0、lostEvents=0
無線パケット
State(12 バイト。30〜100ms ごと)
| オフセット | 内容 |
|---|---|
| 0 | seq |
| 1–2 | buttons(u16 LE、bit0 = ボタン 1) |
| 3 | hat(0〜7 が方向(0=上、時計回り)、8 = 中立) |
| 4–9 | 軸 6 個 = X, Y, Z, Rx, Ry, Rz(0x80 が中立。スティック/トリガではなく HID Usage 順で並べている。理由は「軸番号・ボタン番号を元のパッドに合わせる仕組み」を参照) |
| 10 | flags(bit0: パッド接続中) |
| 11 | checksum(byte0〜10 の XOR) |
Heartbeat(4 バイト。受信機 → 送信機、2 秒ごと)
0xC3, seq, 受信機側の RSSI(i8), XOR。
TXDA はブロードキャストで ACK がないため、送信機は自分の電波が届いたかを知る手段がない。
そのままだと送信機の緑は「モジュールが送信を受理した」ことしか意味せず、
受信機の電源が落ちていても緑になってしまう。そこで受信機から定期的に投げ返し、
送信機はこれが 1.5 秒途切れたら赤点滅にする。受信機側の RSSI も載せているので、
送信機のコンソールで両方向の電波強度が見える(距離試験に便利)。
⚠ 周期を短くすると前方向のパケットが落ちる。 半二重なので、受信機が ハートビートを送っている間はパッドのパケットを受け取れない。実測:
| ハートビート周期 | 受信パケット | seqDrop | 300ms 途絶の回数 |
|---|---|---|---|
| 500ms | 11502 | 901(7.8%) | 870 |
| 2000ms | 203 | 0(0%) | 0 |
送信機の NG 率(キャリアセンス)は 2Hz でも +1% 程度しか増えないので、
一見すると安く見えるが、効いてくるのは受信側の取りこぼしのほう。
既定は 2 秒、送信機のリンク判定タイムアウトは 5 秒。
受信機は応答を待たずに投げる(im920SendNoWait)ので HID の処理は止まらない。
Info(10 バイト。2 秒ごと) 0xA5, version, axisMask, buttonCount, caps(bit0=hat), VID, PID, XOR。
長さでどちらのパケットか判別する。
送信は「状態が変わったら即送信+無変化でも 100ms ごと」「最短間隔 30ms」。TXDA が NG でも
再送はせず次の周期に任せる。
動作中に突然リンク断になる問題(2026-09-19 修正)
送信機のモジュールは半二重で、自分が送信している間は受信できない。 そのため送信間隔によって、受信機からのハートビートの取りこぼし率が大きく変わる。
| 送信間隔 | ハートビート欠落 |
|---|---|
| 60ms(パッド静止) | 6.6% |
| 30ms(操作中) | 29.5% |
ハートビートは 2 秒周期なので、旧設定の HB_TIMEOUT_MS = 5000 では
2 回連続で落ちるだけ(操作中は 8.7%/2 秒ごと)でリンク断と誤判定していた。
平均 20〜30 秒に 1 回、動作中に突然赤点滅する原因。
さらに CH_REVERT_MS = 12000 も同じハートビート頼りで、6 回連続の欠落で
ch31 に戻って再走査に入る。受信機が取り残されるので本当の通信断になっていた。
| 定数 | 変更前 | 変更後 | 必要な連続欠落 |
|---|---|---|---|
HB_TIMEOUT_MS | 5000 | 15000 | 2 回 → 7 回 |
CH_REVERT_MS | 12000 | 30000 | 6 回 → 15 回 |
ロボットの保護は受信側の 300ms フェイルセーフが行うので、送信側のリンク判定を 鈍くしても安全性は落ちない(送信機の LED は状態表示にすぎない)。
修正後、操作中相当(30ms 連続送信)で 225 秒間、link: DOWN・再走査とも 0 回。
またハートビートの欠落自体も 29.5% → 約 7% に下がった。誤判定による再走査が
さらにハートビートを失わせる悪循環になっていたとみられる。
半二重による取りこぼし自体は解消していない。 しきい値を実測に合わせた修正である。
pad: reports が増えないのは異常ではない
送信機の status が出す reports は、パッドから HID レポートが届いた回数。
多くのゲームパッド(Logitech F310 など)は状態が変化したときだけレポートを返すので、
パッドを触っていなければ reports=1 のままになる。これは正常。
実際に操作すると増える(実測で 2512 まで上がることを確認済み)。
PIO-USB が死んでいるかどうかは usb コマンドの
epErr / epStall / コア1: loop1 通算 で判断すること。
動作中の突然の再起動=core1(PIO-USB)の停止
送信機に再起動しても消えないブラックボックス記録(__uninitialized_ram)を入れて
現行犯で捕らえた結果、原因が確定した。
ブラックボックス: ソフト再起動 1 回 / 赤点滅 1 回
赤点滅の内訳: パッド切断=1 USBホスト停止=0 ハートビート途絶=0 受信機側=0
直近の自動再起動: コア1(PIO-USB)の停止
(稼働 265717ms, reports=1 ok=2764 ng=309, ハートビート 223ms 前)
- 無線は無関係。 停止時点でハートビートは 223ms 前に届いており、リンクは健全だった
- 赤点滅の原因はパッド切断で、その 9 秒後に監視機構が再起動をかけている
loop1()はg_hostTicks++が先頭なので、止まっているのはUSBHost.task()の中
受信機側のログで実際の通信断も測れた:
途絶 6 回 / 継続時間: [0, 1, 10, 14, 2988, 20930] ms
20.9 秒の内訳は「core1 停止の検出待ち 10 秒 + 再起動 + チャンネル再走査」。 再起動後は ch31 から始まるため、受信機が待ち合わせに戻るまで待つことになっていた。
対策(根本原因ではなく復帰時間)
USBHost.task() が固まる原因はライブラリ内部で手が出ないため、復帰を速くする方針。
| 項目 | 変更前 | 変更後 |
|---|---|---|
HOST_REBOOT_MS(停止と判断するまで) | 10000 | 3000 |
| 再起動後のチャンネル | ch31 から再会合 | 停止直前の ch へ直接復帰(再走査なし) |
core1 は常時回っているので 3 秒止まれば確実に死んでいる。 またチャンネルをブラックボックスに保存しておけば、受信機はそこに残っているので 再会合が不要になる。合わせて通信断は 21 秒 → 数秒に縮む見込み。
根本原因(USBHost.task() が固まる理由)は未解明のまま。
ネイティブ USB ホストへの移行を検討し、断念した(2026-09-19)
PIO-USB をやめて RP2040 のネイティブ USB コントローラをホストにすれば、 core1 も PIO も USB に関与しなくなり、この停止は構造的に消える。 XIAO の USB-C に OTG 変換でパッドを繋ぎ、給電は J1 から入れる構成。
- arduino-pico は
usbstack=tinyusb_host(Adafruit TinyUSB Host (native))に対応 - 5V ピンから USB-C の VBUS へ逆流することは実機で確認済み(J1 給電で動作した)
最小構成(firmware/hosttest/)で検証した結果:
| 段階 | 結果 |
|---|---|
USBHost.begin(0) | ✅ 通る |
USB 機器の検出(tuh_mount_cb) | ✅ 呼ばれる |
HID として列挙(tuh_hid_mount_cb) | ✅ 呼ばれる。VID:PID も取得できる |
tuh_hid_receive_report() | ✅ 成功を返す |
| レポートの受信 | ❌ 一度も届かない |
rearm=0 rep=0 が延々と続く(rearm=0 は「転送が保留中」を意味し正常)。
つまり転送は armed のまま完了しない。ゲームパッドでもマウスでも同じ。
同じ TinyUSB の HID クラスドライバで PIO-USB 版では動くので、
差分はネイティブ HCD の割り込み IN 転送にある。
hcd_rp2040.c を読んだ限り実装は構造的に正しく見え(割り込みエンドポイントは
15 個のプールから確保、bInterval 設定済み、SOF も有効化)、
設定漏れのような分かりやすい原因は見当たらなかった。
これ以上はライブラリ内部のデバッグになるため打ち切った。 検証用スケッチは最小再現コードとして残してある。
NeoPixel との干渉を疑って調べたこと
Adafruit_NeoPixel::show() は RP2040 で PIO を使う経路でも、実行中ずっと
noInterrupts() で割り込みを止める(Adafruit_NeoPixel.cpp:459 と :3336)。
ただし arduino-pico の noInterrupts() は get_core_num() でコア別に管理されており
(wiring_private.cpp:81)、core0 で止めても core1 の割り込みは止まらない。
show() は core0、PIO-USB は core1 なので、割り込み阻害が直接の原因という線は薄い。
とはいえ NeoPixel と PIO-USB は PIO ハードウェアを共有しており
(そのため enableNeoPixel() は USBHost.begin() の後に呼ぶ必要がある)、
静的解析だけでは否定しきれない。切り分けのため neo on|off コマンドを用意した。
off にすると NeoPixel の PIO を確保せず、三色 LED(R=17/G=16/B=25)だけで
状態表示する。設定は EEPROM に保存され、再起動しても残る。
まだ結論は出ていない。 core1 停止は数分〜数十分に 1 回の頻度なので、
neo off で長時間動かして ブラックボックス の再起動回数を比較する必要がある。
未解明:送信後に消える約 15% のパケット
TXDA が OK を返したのに受信機に届かないパケットが約 15% ある。
実害は確認できていないが、原因も特定できていないので記録しておく。
切り分けで否定できたもの:
| 疑い | 検証方法 | 結果 |
|---|---|---|
| 電波干渉 | ch31〜45 の RSSI を実測 | −105〜−110dBm。キャリアセンス閾値 −80dBm 超過は 0 |
| 受信機の取りこぼし | Serial1.overflow()、行の解析結果を計測 | FIFO 溢れ 0、解釈不能 0、他ノード 0 |
| 受信機の処理落ち | loop() の最大間隔を計測 | 最大 22ms(FIFO 溢れの閾値未満) |
| パケット破損 | チェックサム失敗を計測 | bad=0 |
| ハートビートとの衝突 | hb off で比較 | 18.7% / 21.1%。ほぼ差なし |
| 至近距離による受信機の飽和 | 送信出力 10mW / 1mW で比較 | RSSI −12dBm で 21.1%、−21dBm で 22.9%。変わらず |
| 送信間隔 | 60ms / 90ms で比較 | 14.6% / 19.8%。改善せず |
欠落は必ず単発(1 個が大半、2 個が数回、3 個以上は 0)。 最悪の空きは約 90〜180ms で、受信側フェイルセーフ 300ms には届かない。 実効更新レートが 2 割ほど落ちるだけで、操作が途切れる現象には繋がっていない。
モジュール内部で CRC を通らなかったパケットが黙って捨てられている可能性が高いが、 モジュールから受信エラー数を読む手段が無いため確認できていない。
フェイルセーフ
- 有効なパケットが 300ms 届かない、または flags.bit0 = 0 のとき、RX は全ボタン OFF・ 軸中立・ハット中立の HID レポートを出す。
- 途絶の開始と復帰を CDC にログ出力し、LED を赤点滅にする。
- TX 側もパッドが外れたら中立+flags.bit0 = 0 を送る。
実機の状況(2026-09-18 時点)— 通しで動作
送信機・受信機とも動作し、パッド → 920MHz → PC の経路が通った。
送信機 TX: pad connected VID:PID=046D:C216 axisMask=0x27 buttons=12 hat=1
tx: ok=143 ng=0 (0%) 送信間隔 30ms
受信機 RX: link UP RSSI=-12〜-33dBm packets=1334 bad=0
profile: axisMask=0x27 (X Y Z Rz) buttons=12 hat=1 src=046D:C216
PC: /dev/input/js0 "irlab Wireless Gamepad" 軸 6 個 / ボタン 12 個
軸・ボタンの数が元の F310(D モード)と一致している。受信機は送信機から届いた
Info パケットでパッド構成を自動学習し、EEPROM に保存して HID ディスクリプタを
作り直した(ログ: パッド構成が変わりました: axisMask 0x3F->0x27 buttons 16->12)。
立ち上げでつまずいた点(記録)
1. IM920sL が無応答 → TxD/RxD の逆接続(両基板とも)
swap コマンド(ソフト UART で GPIO1 送信/GPIO0 受信)で確定した:
TxD/RxD 逆接続テスト(GPIO1 から送信し、GPIO0 で受信する)
リセット後の受信: 19 バイト "IM920sL Ver.01.12"
RDVR の応答: 19 バイト "IM920sL Ver.01.12"
D6/D7 を入れ替えて解決。1 枚目はさらに芋半田の手直しも必要だった。 判別の決め手は BUSY ピンで、生きているモジュールは BUSY を L に駆動し、 リセットで変化する(死んでいる側は H に固着したままだった)。
2. USB ゲームパッドが列挙されない → CPU クロックと XInput
- PIO-USB はビットバンギングのため 120MHz ではタイミング余裕が足りなかった。 アドレス 0 での通信と SET_ADDRESS までは成功し、アドレス 1 の転送が全滅する、 という症状。240MHz にして解決(build.sh の既定値)。
- 繋いでいた F310 が XInput モード(046D:C21D, class 255/subclass 0x5D)で、 HID ではないため HID ドライバが掴めなかった。背面スイッチを D にして解決。
3. Adafruit_NeoPixel が PIO を使う
RP2040 版の NeoPixel は pio_claim_free_sm_and_add_program で PIO を掴む。
PIO-USB は TX プログラムを PIO0 のオフセット 0 に置く必要があるため、
送信機では起動直後は三色 LED だけで表示し、USBHost.begin() の後に
enableNeoPixel() を呼ぶ順序にしてある。
確認できた設計上の疑問
-
高速モード(STRT 1)+ ch31 は受け付けられた(
RDCH=31が読める)。 時間総和の制限がない 4s モードが使えるので、送信頻度を落とす必要はない。 -
ペアリング(STGN → RESET 端子で再起動)は成功し、親機・子機の
RDGNが0001D9D8で一致した。子機はGRNOREGDを出力した。 -
NG 率(干渉のない状態、
rate N/hb Nコマンドで測定):条件 NG 率 送信間隔 30ms・ハートビートなし 1% 送信間隔 30ms・ハートビート 2Hz(既定) 2% 送信間隔 30ms・ハートビート 4Hz 3% 送信間隔 20ms(50Hz) 1% 内訳はすべて「モジュールが NG」=キャリアセンス失敗で、BUSY 待ちや無応答は 0。
外部干渉とチャンネル自動選定
測定中、ch31 の NG 率が一時的に 26〜41% まで悪化した。送信間隔を 30→60→100ms と 広げても NG 率が変わらなかった(26%→30%→34%)ことから、自分の送信が原因ではなく 外部の電波でチャンネルが塞がれていると判断できる。干渉は時間とともに移動し、 別の時点では ch35 が 63% に悪化した。固定のチャンネル割り当てでは追随できない。
そこで ch31 を待ち合わせ専用にして、送信機が起動時に空きチャンネルを選ぶ方式にした。
1. 起動時は両機とも ch31(AutoConfig が Flash の既定値に戻すので必ずここに来る)
2. ch31 でリンク確立(送信機は受信機のハートビートを待つ)
3. 送信機が 31〜45 を STCH で切り替えながら RDRS で雑音を測り、最も静かな ch を選ぶ
4. チャンネル変更通知(3 バイト)を 6 回送ってから、自分も移動
5. 受信機は通知を受けて移動
6. 双方とも「12 秒間 相手から受信が無ければ ch31 へ戻る」(片方だけ移動した場合の復帰)
Flash を消費しないのが利点。STCH は取扱説明書に「ENWR コマンド実行時は Flash
メモリに記憶します」の記載が無く揮発的なので、ENWR なしで発行すれば書換え回数
(全項目合計 10 万回)を使わない。EEPROM も使わない(送信機では EEPROM.commit() が
rp2040.idleOtherCore() を使うため、PIO-USB 稼働中の書込みはデッドロックの危険がある)。
実測:
起動 → 走査 → ch42 を選択・移動 tx ng=8%, bad=0, lostEvents=0
[復帰試験]受信機だけ ch38 に強制
→ リンク断(受信機は中立を出力)
→ 約12秒で両機とも ch31 へ復帰
→ 送信機が再走査して ch40 を選択・通知 → 両機 ch40 で確立、以降安定
複数ペアを同時に運用するとき
グループ番号で論理的には分離される(取扱説明書「グループ番号が一致するモジュールのみ 通信」)ので、他のペアのデータが混ざることはない。ただし同じチャンネルでは電波を取り合う (キャリアセンスは「その通信チャンネルの RSSI 値が -80dBm 以上のときは NG」)。 手元で -21dBm なので、同じ部屋の別ペアは確実にこの閾値を超える。
上記の自動選定により、ペアごとに別チャンネルへ分かれる。
⚠ 複数ペアが同時に起動すると一時的に全機が ch31 に集まる。走査と移動は数秒で終わるが、 起動タイミングをずらせるならずらしたほうが確実。
ch N(31〜45)で手動指定もできる。送信機で実行すると受信機にも通知が飛ぶ。
どちらも揮発的なので、再起動すると ch31 に戻って改めて走査する。
ttr N でキャリアセンス NG 時の自動リトライ回数も変えられる(既定 00=リトライなし)。
遅延と引き換えに NG を減らせるので、干渉が避けられない環境では選択肢になる。
診断に使えるコマンド
tools/console.py -p /dev/ttyACM0 -m 12 diag # IM920sL の配線切り分け
tools/console.py -p /dev/ttyACM0 -m 12 swap # TxD/RxD 逆接続の判定(決め手)
tools/console.py -p /dev/ttyACM0 -m 20 rscan # リセットしながらボーレート総当たり
tools/console.py -p /dev/ttyACM0 -m 5 usb # USB ホストの状態・線の静電容量
tools/console.py -p /dev/ttyACM0 "rate 20" # 送信間隔を変えて NG 率を測る
tools/console.py -p /dev/ttyACM0 "ch 35" # チャネル変更(測定用。再起動で戻る)
tools/console.py -p /dev/ttyACM0 "ttr 03" # キャリアセンス NG 時のリトライ回数
tools/console.py -p /dev/ttyACM1 "hb 1000" # ハートビート周期(off で停止)
tools/console.py -p /dev/ttyACM0 "enc <16進64桁>" # 暗号化キー設定(両機に同じキー)
tools/jsinfo.py 10 # js0 の軸数・ボタン数とライブの値
TinyUSB 本体のログを取りたいときは README 末尾の wg_dbg.h の項を参照。
残っている実機確認
- IM920sL が応答する
-
STRT 1の後のSTCH 31が OK になる - ペアリングと
RDGNの一致 - PIO-USB で元パッドが列挙され、
mapで解析できる - 送信頻度ごとの NG 率。初期の「30ms で 0%」は短時間の測定で、 長時間動かすと 30ms で約 22%、60ms で約 8% になる(2026-09-19 再測定)。 NG は必ず単発で、次の周期で回復するため実害は無い。遅延の実測はこれから
-
/dev/input/js0として認識され、元のパッド向けに書かれたソフトが そのまま動くか(js0 としての認識と軸/ボタン数の一致までは確認済み。 実アプリでの動作確認が残り) - フェイルセーフ(TX 電源断・アンテナ遮蔽)
- モバイルバッテリーが低電流で自動 OFF しないか
No comments yet. Be the first to ask about this board.