Share
Embed
ゲームコントローラやキーボードのファームウェアをSwiftで書く
今日つくるもの
USBデバイスを実装する前に
通信の仕組み
接続の通知
USBデバイスの列挙
入力データの通信
Kishikawa Katsumi
September 12, 2026
Transcript
121 slides
-
本日の目標 操作する対象より一段下の層を理解する ‒ 今日つくるもの ‒ USBデバイスを実装する前に ‒ 通信の仕組み ‒ 接続の通知
‒ USBデバイスの列挙 ‒ 入力データの通信 -
格闘ゲーム用コントローラー ‒ 部品リスト ゲームパッド キーボード ‒ Alpakka 1 Controller https://inputlabs.io/alpakka
‒ CROSSARROW60 ‒ 基板設計・はんだ付け必要 (写真の製品は組み立てのみ) ‒ 基板設計・はんだ付け必要 (写真の製品は組み立てのみ) ‒ ※マイコンはRP2040だが ‣ 筐体(箱であればなんでもいい) 別のボード(Marmota)のため Picoとはピン配置が異なる ‣ 基板設計・はんだ付け不要 ‒ キースイッチ、キーキャップは 別に用意する必要あり ‣ アーケードボタン ‣ 配線ケーブル ‣ Raspberry Pi Pico https://shop.yushakobo.jp/products/10753 -
格闘ゲーム用コントローラー部品リスト ‒ ボタン https://akecon.games/collections/pushbutton ‒ 配線ケーブル https://amzn.to/3V94qK9 ‒ マイコン(Raspberry Pi
Pico) https://akizukidenshi.com/catalog/g/g116132/ ‒ 筐体(箱状のもの) -
章 タイトル 内容 1 概要 この文書の目的と構成、各章の概要についての紹介。 2 用語集 規格書で使われる用語と略語の定義集。Bit Stuf
ng(ビット・スタッフ)などの技術 用語はここで説明されている。 3 バックグラウンド USB規格のゴールや対象となるアプリケーションについての説明。 4 アーキテクチャの全体像 ホスト・ハブ・デバイスからなるバス全体の構成と、データ転送・電源供給につい て。 エンドポイントとパイプという通信の考え方と、4種類の転送方式(コントロール/イ 5 USBのデータ・フロー・モデルの説明 ンタラプト/バルク/アイソクロナス)の説明。 7 電気的仕様 電気的仕様の規定。信号の電圧、NRZI符号化、クロック精度(±0.25%)など。ロゴ 認証で「チャプタ7テスト」と呼ばれる難関の基準。 8 プロトコル・レイヤー パケットの形式とやりとりの手順の規定。トランザクション、ACK/NAK/STALLの応 答、再送の仕組みなど。 9 USBデバイス・フレームワーク デバイスに求められる振る舞いの規定。列挙の状態遷移、標準リクエストへの応答、 ディスクリプタの形式。 10 USBホスト ホスト側のハードウェアとソフトウェアに求められる役割の規定。 11 HUB(ハブ)についての規格 ハブの動作の規定。ポートの管理や速度の異なるデバイスの混在の扱いなど。 fi 6 機械的仕様 コネクタとケーブルの形状・寸法・材質の規定。プラグの形やピン配置はここで決 まっている。 -
章 タイトル 内容 1 概要 この文書の目的と構成、各章の概要についての紹介。 2 用語集 規格書で使われる用語と略語の定義集。Bit Stuf
ng(ビット・スタッフ)などの技術 用語はここで説明されている。 3 バックグラウンド USB規格のゴールや対象となるアプリケーションについての説明。 4 アーキテクチャの全体像 ホスト・ハブ・デバイスからなるバス全体の構成と、データ転送・電源供給につい て。 エンドポイントとパイプという通信の考え方と、4種類の転送方式(コントロール/イ 5 USBのデータ・フロー・モデルの説明 ンタラプト/バルク/アイソクロナス)の説明。 6 機械的仕様 コネクタとケーブルの形状・寸法・材質の規定。プラグの形やピン配置はここで決 まっている。 7 電気的仕様 電気的仕様の規定。信号の電圧、NRZI符号化、クロック精度(±0.25%)など。ロゴ 認証で「チャプタ7テスト」と呼ばれる難関の基準。 8 プロトコル・レイヤー パケットの形式とやりとりの手順の規定。トランザクション、ACK/NAK/STALLの応 全部読む! 答、再送の仕組みなど。 10 USBホスト ホスト側のハードウェアとソフトウェアに求められる役割の規定。 11 HUB(ハブ)についての規格 ハブの動作の規定。ポートの管理や速度の異なるデバイスの混在の扱いなど。 fi 9 USBデバイス・フレームワーク デバイスに求められる振る舞いの規定。列挙の状態遷移、標準リクエストへの応答、 ディスクリプタの形式。 -
章 タイトル 内容 この文書の目的と構成、各章の概要についての紹介。 知らない言葉を調べる 1 概要 2 用語集 規格書で使われる用語と略語の定義集。Bit
Stuf ng(ビット・スタッフ)などの技術 用語はここで説明されている。 3 バックグラウンド USB規格のゴールや対象となるアプリケーションについての説明。 拾い読み 4 アーキテクチャの全体像 ホスト・ハブ・デバイスからなるバス全体の構成と、データ転送・電源供給につい て。 エンドポイントとパイプという通信の考え方と、4種類の転送方式(コントロール/イ 5 USBのデータ・フロー・モデルの説明 ンタラプト/バルク/アイソクロナス)の説明。 コネクタとケーブルの形状・寸法・材質の規定。プラグの形やピン配置はここで決 まっている。 6 機械的仕様 7 電気的仕様 拾い読み 8 プロトコル・レイヤー 電気的仕様の規定。信号の電圧、NRZI符号化、クロック精度(±0.25%)など。ロゴ 認証で「チャプタ7テスト」と呼ばれる難関の基準。 パケットの形式とやりとりの手順の規定。トランザクション、ACK/NAK/STALLの応 全部読む! 答、再送の仕組みなど。 10 USBホスト ホスト側のハードウェアとソフトウェアに求められる役割の規定。 11 HUB(ハブ)についての規格 ハブの動作の規定。ポートの管理や速度の異なるデバイスの混在の扱いなど。 fi 9 USBデバイス・フレームワーク デバイスに求められる振る舞いの規定。列挙の状態遷移、標準リクエストへの応答、 ディスクリプタの形式。 -
Memory-Mapped I/O (MMIO) ソフトウェアからハードウェアを制御する仕組み import _Volatile let outputSet = VolatileMappedRegister<UInt32>(
unsafeBitPattern: 0xD000_0014 // GPIO_OUT_SET ) let outputClear = VolatileMappedRegister<UInt32>( unsafeBitPattern: 0xD000_0018 // GPIO_OUT_CLR ) let ledMask: UInt32 = 1 << 25 while true { outputSet.store(ledMask) // GP25をHighにする delay() outputClear.store(ledMask) } delay() // GP25をLowにする -
Memory-Mapped I/O (MMIO) アドレスで、反応する回路が決まる ‒ CPUは、アドレス・値・読み書き信号を内部バスへ出す ‒ アドレスデコーダーが、アドレスのビットパターンから選択信号を作る ‒ 選択信号が有効になった周辺回路だけがアクセスに反応する
‒ 値の変化をソフトウェアが監視しているのではなく、固定された論理回路によ る動作 -
Memory-Mapped I/O (MMIO) MMIOのアドレスへの読み書きはメモリの特性を前提にできない ‒ アドレスの先はメモリではなく実際の配線や回路に繋がっている ‒ MMIOのアドレスへの読み書きはメモリの特性を前提にできない ‣ 続けて同じアドレスを読めば同じ値が返る
‣ 直前に書いた値を読めば同じ値が返る コンパイラはこの特性を 前提に最適化する -
Memory-Mapped I/O (MMIO) MMIOでは最適化によって処理が省略されると困る import _Volatile struct Register { let
address: UInt32 /// Reads the register value. @inline(__always) func load() -> UInt32 { VolatileMappedRegister<UInt32>(unsafeBitPattern: UInt(address)).load() } } /// Writes a value to the register. @inline(__always) func store(_ value: UInt32) { VolatileMappedRegister<UInt32>(unsafeBitPattern: UInt(address)).store(value) } -
̲Volatile Package SwiftでVolatileなメモリアクセスを実現する import _Volatile struct Register { let address:
UInt32 /// Reads the register value. @inline(__always) func load() -> UInt32 { VolatileMappedRegister<UInt32>(unsafeBitPattern: UInt(address)).load() } } /// Writes a value to the register. @inline(__always) func store(_ value: UInt32) { VolatileMappedRegister<UInt32>(unsafeBitPattern: UInt(address)).store(value) } -
̲Volatile Package SwiftでVolatileなメモリアクセスを実現する extension VolatileMappedRegister where Pointee == UInt32 {
/// Perform a 32-bit volatile load operation from the target pointer. /// /// Do not use for inter-thread synchronization. @_transparent public func load() -> Pointee { UInt32(Builtin.atomicload_monotonic_volatile_Int32(_rawPointer)) } } /// Perform a 32-bit volatile store operation on the target pointer. /// /// Do not use for inter-thread synchronization. @_transparent public func store(_ value: Pointee) { Builtin.atomicstore_monotonic_volatile_Int32(_rawPointer, value._value) } https://github.com/swiftlang/swift/blob/6ee6d5bf4138eb59700463c032e83618d02e9919/stdlib/public/Volatile/Volatile.swift#L74-L90 -
̲Volatile Package コンパイラが直接LLVM IRを生成する Swift UInt32(Builtin.atomicload_monotonic_volatile_Int32(_rawPointer)) LLVM IR load atomic
volatile i32, ptr %address monotonic atomicload 分割されない(atomic) monotonic 順序保証 volatile 省略や統合をしない Int32 32 bit幅で読み込む -
差動通信 2本で1つの状態を表す D- K J D+ K J J K
D+ K K J K K J J J K -
差動通信 0と1はJ/Kの「変化」で表す 1 0 0 0 1 0 1 1
0 0 1 0 1 1 0 K K J K J K J K J J K K K J J -
NRZI (Non Return to Zero Inversion)符号化 1 0 0 0
1 0 1 1 0 0 1 0 1 1 0 K K J K J K J K J J K K K J J -
struct NRZIEncoder { private var line = true mutating func
encode(_ bits: [Bool]) -> [Bool] { bits.map { bit in if !bit { line.toggle() } return line } } } struct NRZIDecoder { private var line = true mutating func decode(_ states: [Bool]) -> [Bool] { states.map { state in let bit = (state == line) // 変わっていない = 1、変わった = 0 } } } line = state return bit -
enum USB { ... static func initialize() { // 1.
Reset to a clean slate before bringing it up (clk_usb assumes // Clocks.initialize has already run). Resets.reset(.usbctrl) Resets.unreset(.usbctrl) // 2. DPRAM holds undefined values right after power-on -> zero it out. DPRAM.clear() // 3. Controller core, wiring to the physical pins, and self-reported power. mainControl.store(controllerEnable) muxing.store(muxToPhySoftcon) power.store(vbusDetectOverride) // The Pico's detect pin isn't wired -> declare "power is present". // 4. Raise a flag on every EP0 completion / address 0 (accept newcomers). sieControl.store(ep0IntPerBuffer) addressEndpoint.store(0) // 5. The one bit that announces us. From here the host's script begins. sieControl.set(pullupEnable) } ... } -
enum USB { ... static func initialize() { // 1.
Reset to a clean slate before bringing it up (clk_usb assumes // Clocks.initialize has already run). Resets.reset(.usbctrl) Resets.unreset(.usbctrl) // 2. DPRAM holds undefined values right after power-on -> zero it out. DPRAM.clear() マルチプレクサーを操作して // 3. Controller core, wiring to the physical pins, and self-reported power. USBコントローラーをPhysical Layerに繋ぐ mainControl.store(controllerEnable) muxing.store(muxToPhySoftcon) power.store(vbusDetectOverride) // The Pico's detect pin isn't wired -> declare "power is present". // 4. Raise a flag on every EP0 completion / address 0 (accept newcomers). sieControl.store(ep0IntPerBuffer) addressEndpoint.store(0) // 5. The one bit that announces us. From here the host's script begins. sieControl.set(pullupEnable) } ... } -
enum USB { ... static func initialize() { // 1.
Reset to a clean slate before bringing it up (clk_usb assumes // Clocks.initialize has already run). Resets.reset(.usbctrl) Resets.unreset(.usbctrl) // 2. DPRAM holds undefined values right after power-on -> zero it out. DPRAM.clear() マルチプレクサーを操作して // 3. Controller core, wiring to the physical pins, and self-reported power. USBコントローラーをPhysical Layerに繋ぐ mainControl.store(controllerEnable) muxing.store(muxToPhySoftcon) power.store(vbusDetectOverride) // The Pico's detect pin isn't wired -> declare "power is present". // 4. Raise a flag on every EP0 completion / address 0 (accept newcomers). sieControl.store(ep0IntPerBuffer) addressEndpoint.store(0) D+をプルアップ // 5. The one bit that announces us. From here the host's script begins. sieControl.set(pullupEnable) } ... } -
enum USB { ... static func initialize() { // 1.
Reset to a clean slate before bringing it up (clk_usb assumes // Clocks.initialize has already run). Resets.reset(.usbctrl) Resets.unreset(.usbctrl) // 2. DPRAM holds undefined values right after power-on -> zero it out. DPRAM.clear() // 3. Controller core, wiring to the physical pins, and self-reported power. mainControl.store(controllerEnable) muxing.store(muxToPhySoftcon) power.store(vbusDetectOverride) // The Pico's detect pin isn't wired -> declare "power is present". // 4. Raise a flag on every EP0 completion / address 0 (accept newcomers). sieControl.store(ep0IntPerBuffer) addressEndpoint.store(0) // 5. The one bit that announces us. From here the host's script begins. sieControl.set(pullupEnable) } ... } -
enum USB { ... デバッガーから実行など、コールドスタートとは static func initialize() { 限らないのでリセット状態に入れてからアンリセットする。
// 1. Reset to a clean slate before bringing it up (clk_usb assumes // Clocks.initialize has already run). コントローラーはまだ動いていない。リセット状態を解除しただけ。 Resets.reset(.usbctrl) Resets.unreset(.usbctrl) // 2. DPRAM holds undefined values right after power-on -> zero it out. DPRAM.clear() // 3. Controller core, wiring to the physical pins, and self-reported power. mainControl.store(controllerEnable) muxing.store(muxToPhySoftcon) power.store(vbusDetectOverride) // The Pico's detect pin isn't wired -> declare "power is present". // 4. Raise a flag on every EP0 completion / address 0 (accept newcomers). sieControl.store(ep0IntPerBuffer) addressEndpoint.store(0) // 5. The one bit that announces us. From here the host's script begins. sieControl.set(pullupEnable) } ... } -
enum USB { ... デバッガーから実行など、コールドスタートとは static func initialize() { 限らないのでリセット状態に入れてからアンリセットする。
// 1. Reset to a clean slate before bringing it up (clk_usb assumes // Clocks.initialize has already run). コントローラーはまだ動いていない。リセット状態を解除しただけ。 Resets.reset(.usbctrl) Resets.unreset(.usbctrl) DPRAMをゼロで初期化 // 2. DPRAM holds undefined values right after power-on -> zero it out. DPRAM.clear() // 3. Controller core, wiring to the physical pins, and self-reported power. mainControl.store(controllerEnable) muxing.store(muxToPhySoftcon) power.store(vbusDetectOverride) // The Pico's detect pin isn't wired -> declare "power is present". // 4. Raise a flag on every EP0 completion / address 0 (accept newcomers). sieControl.store(ep0IntPerBuffer) addressEndpoint.store(0) // 5. The one bit that announces us. From here the host's script begins. sieControl.set(pullupEnable) } ... } -
enum USB { ... デバッガーから実行など、コールドスタートとは static func initialize() { 限らないのでリセット状態に入れてからアンリセットする。
// 1. Reset to a clean slate before bringing it up (clk_usb assumes // Clocks.initialize has already run). コントローラーはまだ動いていない。リセット状態を解除しただけ。 Resets.reset(.usbctrl) Resets.unreset(.usbctrl) DPRAMをゼロで初期化 // 2. DPRAM holds undefined values right after power-on -> zero it out. DPRAM.clear() USBコントローラーを動かす // 3. Controller core, wiring to the physical pins, and self-reported power. mainControl.store(controllerEnable) muxing.store(muxToPhySoftcon) power.store(vbusDetectOverride) // The Pico's detect pin isn't wired -> declare "power is present". // 4. Raise a flag on every EP0 completion / address 0 (accept newcomers). sieControl.store(ep0IntPerBuffer) addressEndpoint.store(0) // 5. The one bit that announces us. From here the host's script begins. sieControl.set(pullupEnable) } ... } -
enum USB { ... デバッガーから実行など、コールドスタートとは static func initialize() { 限らないのでリセット状態に入れてからアンリセットする。
// 1. Reset to a clean slate before bringing it up (clk_usb assumes // Clocks.initialize has already run). コントローラーはまだ動いていない。リセット状態を解除しただけ。 Resets.reset(.usbctrl) Resets.unreset(.usbctrl) DPRAMをゼロで初期化 // 2. DPRAM holds undefined values right after power-on -> zero it out. DPRAM.clear() USBコントローラーを動かす // 3. Controller core, wiring to the physical pins, and self-reported power. mainControl.store(controllerEnable) muxing.store(muxToPhySoftcon) power.store(vbusDetectOverride) // The Pico's detect pin isn't wired -> declare "power is present". セットアップパケットの送受信に使うEP0の準備 // 4. Raise a flag on every EP0 completion / address 0 (accept newcomers). デバイス自身のアドレスをゼロに sieControl.store(ep0IntPerBuffer) addressEndpoint.store(0) // 5. The one bit that announces us. From here the host's script begins. sieControl.set(pullupEnable) } ... } -
enum USB { ... static func initialize() { // 1.
Reset to a clean slate before bringing it up (clk_usb assumes // Clocks.initialize has already run). Resets.reset(.usbctrl) Resets.unreset(.usbctrl) // 2. DPRAM holds undefined values right after power-on -> zero it out. DPRAM.clear() // 3. Controller core, wiring to the physical pins, and self-reported power. mainControl.store(controllerEnable) muxing.store(muxToPhySoftcon) power.store(vbusDetectOverride) // The Pico's detect pin isn't wired -> declare "power is present". // 4. Raise a flag on every EP0 completion / address 0 (accept newcomers). sieControl.store(ep0IntPerBuffer) addressEndpoint.store(0) // 5. The one bit that announces us. From here the host's script begins. sieControl.set(pullupEnable) } ... } -
enum USB { ... static func initialize() { // 1.
Reset to a clean slate before bringing it up (clk_usb assumes // Clocks.initialize has already run). Resets.reset(.usbctrl) Resets.unreset(.usbctrl) // 2. DPRAM holds undefined values right after power-on -> zero it out. DPRAM.clear() // 3. Controller core, wiring to the physical pins, and self-reported power. mainControl.store(controllerEnable) muxing.store(muxToPhySoftcon) power.store(vbusDetectOverride) // The Pico's detect pin isn't wired -> declare "power is present". // 4. Raise a flag on every EP0 completion / address 0 (accept newcomers). sieControl.store(ep0IntPerBuffer) addressEndpoint.store(0) // 5. The one bit that announces us. From here the host's script begins. sieControl.set(pullupEnable) } ... } -
ホストがデバイスとの通信を始める USBコントローラーが信号とパケットを処理する デバイス ホスト(PC) D+をプルアップ(Attatch) SIE 接続を検知 バスリセット USBコントローラーは通信の形式とタイミングを担当 Serial
Interface Engine セットアップトランザクション ソフトウェアは通信の意味と内容を担当 -
ホストがデバイスとの通信を始める USBコントローラーが信号とパケットを処理する デバイス SIE̲STATUS Swift DPRAM D+をプルアップ(Attatch) SIE バスリセット BUS̲RESET
SETUP SETUP̲REC handleReset() ホスト(PC) -
バスリセット 39 ホストがD-/D+を両方ともLowに駆動する ⚡High ⬆ 2 1 ⚡Low ⬇ BOOTSEL
LED USB ⚡Low ⬇ -
Bus ResetとSetup while true { let status = USB.sieStatus.load() //
Bus reset: "return to the initial state" if status & USB.busReset != 0 { USB.handleBusReset() ... } // SETUP: an 8-byte question arrived -> answer it if status & USB.setupReceived != 0 { USB.sieStatus.store(USB.setupReceived) // W1C (acknowledge receipt) let setup = Control.Setup.read() } } Enumeration.handle(setup) ... -
Bus ResetとSetup while true { let status = USB.sieStatus.load() //
Bus reset: "return to the initial state" if status & (1 << 19) != 0 { USB.handleBusReset() ... } // SETUP: an 8-byte question arrived -> answer it if status & (1 << 17) != 0 { USB.sieStatus.store(1 << 17) // W1C (acknowledge receipt) let setup = Control.Setup.read() } } Enumeration.handle(setup) ... -
Bus ResetとSetup while true { let status = USB.sieStatus.load() //
Bus reset: "return to the initial state" if status & (1 << 19) != 0 { USB.handleBusReset() ... } // SETUP: an 8-byte question arrived -> answer it if status & (1 << 17) != 0 { USB.sieStatus.store(1 << 17) // W1C (acknowledge receipt) let setup = Control.Setup.read() } } Enumeration.handle(setup) ... -
〜略〜 SIE_STATUS: 0x00000015 SIE_STATUS: 0x00000015 SIE_STATUS: 0x00000015 SIE_STATUS: 0x00080001 SIE_STATUS:
0x00000001 SIE_STATUS: 0x00000001 SIE_STATUS: 0x00000001 SIE_STATUS: 0x00010005 SIE_STATUS: 0x00010005 SIE_STATUS: 0x00010005 〜略〜 -
31 28 24 20 16 12 8 4 0 〜略〜
SIE_STATUS: 0000 0000 0000 0000 0000 0000 0001 0101 SIE_STATUS: 0000 0000 0000 0000 0000 0000 0001 0101 SIE_STATUS: 0000 0000 0000 0000 0000 0000 0001 0101 SIE_STATUS: 0000 0000 0000 1000 0000 0000 0000 0001 SIE_STATUS: 0000 0000 1000 0000 0000 0000 0000 0001 SIE_STATUS: 0000 0000 1000 0000 0000 0000 0000 0001 SIE_STATUS: 0000 0000 1000 0000 0000 0000 0000 0001 SIE_STATUS: 0000 0000 0000 0001 0000 0000 0000 0101 SIE_STATUS: 0000 0000 0000 0001 0000 0000 0000 0101 SIE_STATUS: 0000 0000 0000 0001 0000 0000 0000 0101 〜略〜 -
31 28 24 20 16 12 8 4 0 〜略〜
SIE_STATUS: 0000 0000 0000 0000 0000 0000 0001 0101 SIE_STATUS: 0000 0000 0000 0000 0000 0000 0001 0101 SIE_STATUS: 0000 0000 0000 0000 0000 0000 0001 0101 SIE_STATUS: 0000 0000 0000 1000 0000 0000 0000 0001 SIE_STATUS: 0000 0000 1000 0000 0000 0000 0000 0001 SIE_STATUS: 0000 0000 1000 0000 0000 0000 0000 0001 SIE_STATUS: 0000 0000 1000 0000 0000 0000 0000 0001 SIE_STATUS: 0000 0000 0000 0001 0000 0000 0000 0101 SIE_STATUS: 0000 0000 0000 0001 0000 0000 0000 0101 SIE_STATUS: 0000 0000 0000 0001 0000 0000 0000 0101 〜略〜 -
31 28 24 20 16 12 8 4 0 〜略〜
SIE_STATUS: 0000 0000 0000 0000 0000 0000 0001 0101 SIE_STATUS: 0000 0000 0000 0000 0000 0000 0001 0101 SIE_STATUS: 0000 0000 0000 0000 0000 0000 0001 0101 SIE_STATUS: 0000 0000 0000 1000 0000 0000 0000 0001 SIE_STATUS: 0000 0000 1000 0000 0000 0000 0000 0001 SIE_STATUS: 0000 0000 1000 0000 0000 0000 0000 0001 SIE_STATUS: 0000 0000 1000 0000 0000 0000 0000 0001 SIE_STATUS: 0000 0000 0000 0001 0000 0000 0000 0101 SIE_STATUS: 0000 0000 0000 0001 0000 0000 0000 0101 SIE_STATUS: 0000 0000 0000 0001 0000 0000 0000 0101 〜略〜 -
Bus Reset リセットをSerial Interface Engineが検知 while true { let status
= USB.sieStatus.load() // Bus reset: "return to the initial state" if status & (1 << 19) != 0 { USB.handleBusReset() InputReport.reset() LedCommand.reset() ... } ... } -
Bus ResetとSetup 内部状態を初期値に戻す while true { let status = USB.sieStatus.load()
// Bus reset: "return to the initial state" if status & (1 << 19) != 0 { USB.handleBusReset() InputReport.reset() LedCommand.reset() ... } ... } -
enum InputReport { nonisolated(unsafe) private static var dataPid = false
nonisolated(unsafe) private static var mailboxEmpty = false static func reset() { dataPid = false mailboxEmpty = true } } 内部状態を保持する変数を初期値に戻すだけ。 ハードウェアの操作はなし。 ... enum LedCommand { nonisolated(unsafe) private static var dataPid = false nonisolated(unsafe) static var playerLampOn = false static func reset() { dataPid = false playerLampOn = false } } ... -
Bus Reset ビットフラグを下ろし、アドレスをゼロに設定する static func handleBusReset() { sieStatus.store(1 << 19)
// W1C: only the flag written with a 1 clears addressEndpoint.store(0) // back to accepting at address 0 } -
Bus Reset ビットフラグを下ろし、アドレスをゼロに設定する デバイス自身のアドレスをゼロ static func handleBusReset() { (規定された初期アドレス)に設定 sieStatus.store(1
<< 19) // W1C: only the flag written with a 1 clears addressEndpoint.store(0) // back to accepting at address 0 } -
Bus Reset ビットフラグを下ろし、アドレスをゼロに設定する static func handleBusReset() { sieStatus.store(1 << 19)
// W1C: only the flag written with a 1 clears addressEndpoint.store(0) // back to accepting at address 0 } -
Bus Reset ビットフラグを下ろし、アドレスをゼロに設定する 19ビットをゼロに戻す static func handleBusReset() { sieStatus.store(1 <<
19) // W1C: only the flag written with a 1 clears addressEndpoint.store(0) // back to accepting at address 0 } -
W1C (Write 1 to Clear) while true { let status
= USB.sieStatus.load() // Bus reset: "return to the initial state" if status & (1 << 19) != 0 { sieStatus.store(1 << 19) // W1C: only the flag written with a 1 clears addressEndpoint.store(0) // back to accepting at address 0 ... } // SETUP: an 8-byte question arrived -> answer it if status & (1 << 17) != 0 { USB.sieStatus.store(1 << 17) // W1C (acknowledge receipt) let setup = Control.Setup.read() } } Enumeration.handle(setup) ... -
W1C (Write 1 to Clear) 1を書いたビットだけをゼロにする while true { let
status = USB.sieStatus.load() // Bus reset: "return to the initial state" W1C: 書いたビットをクリアする if status & (1 << 19) != 0 { sieStatus.store(1 << 19) // W1C: only the flag written with a 1 clears addressEndpoint.store(0) // back to accepting at address 0 ... } // SETUP: an 8-byte question arrived -> answer it if status & (1 << 17) != 0 { USB.sieStatus.store(1 << 17) // W1C (acknowledge receipt) let setup = Control.Setup.read() } } Enumeration.handle(setup) ... -
W1C (Write 1 to Clear) 1を書いたビットだけをゼロにする 0000 0000 0000 1000
0000 0000 0000 0001 -
W1C (Write 1 to Clear) 1を書いたビットだけをゼロにする 0000 0000 0000 1000
0000 0000 0000 0001 0000 0000 0000 1000 0000 0000 0000 0000 -
W1C (Write 1 to Clear) 19ビットにだけゼロを書く? 0000 0000 0000 0000
0000 0000 0000 0001 -
W1C (Write 1 to Clear) 1bitだけを書くということはできない 0000 0000 0000 1000
0000 0000 0000 0001 0000 0000 0000 1000 0000 0000 0000 0000 -
W1C (Write 1 to Clear) 更新が衝突すると上書きしてしまう 0000 0000 0000 1000
0000 0000 0000 0001 0000 0000 0000 0000 0000 0000 0000 0001 -
W1C (Write 1 to Clear) フラグを下ろすのはソフトウェア while true { let
status = USB.sieStatus.load() // Bus reset: "return to the initial state" if status & (1 << 19) != 0 { sieStatus.store(1 << 19) // W1C: only the flag written with a 1 clears addressEndpoint.store(0) // back to accepting at address 0 ... } // SETUP: an 8-byte question arrived -> answer it if status & (1 << 17) != 0 { USB.sieStatus.store(1 << 17) // W1C (acknowledge receipt) let setup = Control.Setup.read() } } Enumeration.handle(setup) ... -
〜略〜 SIE_STATUS: 0x00000015 SIE_STATUS: 0x00000015 SIE_STATUS: 0x00000015 SIE_STATUS: 0x00080001 BUS̲RESET
SIE_STATUS: 0x00000001 SIE_STATUS: 0x00000001 SIE_STATUS: 0x00000001 SIE_STATUS: 0x00010005 SIE_STATUS: 0x00010005 SIE_STATUS: 0x00010005 〜略〜 CONNECTED 0001 0000 0000 0000 0101 -
〜略〜 SIE_STATUS: 0x00010005 SIE_STATUS: 0x00010005 SIE_STATUS: 0x00010005 SIE_STATUS: 0x00010005 SIE_STATUS:
0x00010005 SIE_STATUS: 0x00010005 SIE_STATUS: 0x00010005 SIE_STATUS: 0x00010005 SIE_STATUS: 0x00010005 SETUP SIE_STATUS: 0x00030005 〜略〜 0011 0000 0000 0000 0101 -
Setup while true { let status = USB.sieStatus.load() // Bus
reset: "return to the initial state" if status & (1 << 19) != 0 { sieStatus.store(1 << 19) // W1C: only the flag written with a 1 clears addressEndpoint.store(0) // back to accepting at address 0 ... } // SETUP: an 8-byte question arrived -> answer it if status & (1 << 17) != 0 { USB.sieStatus.store(1 << 17) // W1C (acknowledge receipt) let setup = Control.Setup.read() } } Enumeration.handle(setup) ... -
SETUPリクエストの処理 0 1 2 3 4 5 6 7 方向・種類・宛先
リクエストの種類 引数1 引数2 データの長さ bmRequestType bRequest wValue wIndex wLength 1 byte 1 byte 2 byte 2 byte 2 byte struct Setup { let bmRequestType: UInt8 let bRequest: UInt8 let wValue: UInt16 let wIndex: UInt16 let wLength: UInt16 } -
SETUPリクエストの処理 bmRequestType D7 D6-D5 Data Transfer Direction (データ転送方向) 0: OUT転送(ホスト
→ デバイス) Type (リクエストのタイプ) 00: Device(デバイス全体宛て) 1: IN転送(デバイス → ホスト) 01: Class(HID、オーディオ、CDCなどクラス固有) 10: Vendor(メーカーが独自に定義するコマンド) 11: Reserved(将来の拡張のための予約領域) D4-D0 Recipient (宛先・受信対象) 00000: Device(デバイス全体宛て) 00001: Interface(特定のインターフェース宛て) 00010: Endpoint(特定のエンドポイント宛て) 00011: Other(その他) -
SETUPリクエストの処理 ホストからデバイスへの関数呼び出し(RPC) bmRequestType - データの転送方向(ホスト→デバイス/デバイス→ホスト) - リクエストのタイプ(標準、クラス、ベンダー) - 宛先(デバイス、インターフェース、エンドポイント) bRequest
関数名 具体的に何を実行するかを指定するコード 例: 0x06=GET_DESCRIPTOR, 0x05=SET_ADDRESSなど wValue bRequest に応じた引数 第1引数 ディスクリプタを要求する場合は「ディスクリプタのタイプとインデックス」 wIndex 主にインターフェース番号やエンドポイント番号の指定 wLength 続く「データステージ」で転送されるデータのバイト数を指定。データステー 入力データ・戻り値 ジがない場合は 0。 第2引数 -
SET̲ADDRESS デバイスに個別のアドレスを割り当てる bmRequestType bRequest wValue 00 05 05 00 00
00 00 00 5(新しいアドレス) SET̲ADDRESS ホスト → デバイス -
SET̲ADDRESS(5) デバイスに個別のアドレスを割り当てる SET̲ADDRESS(5) ZLP(Zero-Length Packet)(完了の応答)を返す アドレスを5へ変更 let address = UInt32(setup.wValue)
& 0x7F Control.acknowledge { USB.addressEndpoint.store(address) } -
GET̲DESCRIPTOR(Device) Device Descriptor:基本情報を返す wLength 80 06 00 01 00 00
12 00 18 byte Device Descriptor GET̲DESCRIPTOR デバイス → ホスト -
GET̲DESCRIPTOR(Device) Device Descriptor:基本情報を返す fi ゲームコントローラー(Xboxコントローラーに擬装) USBバージョン 2.00 EP0最大パケットサイズ 64 bytes
Vendor ID 0x045E Product ID 0x028E Deviceバージョン 1.0.0 Con gurationの数 1 Microsoft Corp. Xbox360 Controller -
GET̲DESCRIPTOR(Configuration) Con guration Descriptor:基本情報を返す 80 06 00 02 00 00
31 00 49バイト Con guration Descriptor GET̲DESCRIPTOR fi fi デバイス → ホスト -
SETUP: ゲームコントローラー ホストからのSETUP要求 応答と処理 ZLPで了承し、送信完了後に新しいアドレスを反映 GET̲DESCRIPTORDevice 最大wLengthまで返す 18B/VID・PID:045E:028E GET̲DESCRIPTORCon guration
InterfaceやEndpointを説明するDescriptor群を返す 49B/XUSB Interface/EP1 IN+EP2 OUT GET̲DESCRIPTORReport 使用しないためSTALL SET̲CONFIGURATION(1) 宣言したEndpointを有効にし、ZLPで了承 EP1 INとEP2 OUTを有効化 その他・クラス固有要求 このサンプルではSTALL fi SET̲ADDRESS -
SETUP: キーボード ホストからのSETUP要求 応答と処理 SET̲ADDRESS ZLPで了承し、送信完了後に新しいアドレスを反映 GET̲DESCRIPTORDevice 最大wLengthまで返す 18B/VID・PID:CAFE:0002 InterfaceやEndpointを説明するDescriptor群を返す
34B/HID Keyboard/HID Descriptor+EP1 IN 63BのHID Report Descriptorを返す GET̲DESCRIPTORCon guration GET̲DESCRIPTORReport SET̲REPORT 宣言したEndpointを有効にし、ZLPで了承 EP1 INを有効化 Caps LockなどのLED状態を受信し、ZLPで了承 SET̲IDLESET̲PROTOCOL このサンプルでは内容を利用せず、ZLPで了承 その他の要求 STALL fi SET̲CONFIGURATION(1) -
入力の読み取り方の違い 入力機器 ハードウェアの構成 入力の読み取り方 格闘ゲーム用コントローラー 1個のボタンと1本のGPIOが1対1 各GPIOがHighかLowかを読む。内部プルアッ で対応 プを使うため、Lowなら押下 ゲームパッド
ボタンに加えて、アナログス ボタンはGPIOから読む。スティックはADCで ティックの各軸をADCピンへ接続 電圧を数値に変換し、X軸・Y軸の値として使う 一般的なキーボード 多数のキーを、行と列に並べた キーマトリックスとして接続 行を1本ずつ選択して列を読み、反応した行と列 の交点から押されたキーを特定する -
入力の読み取り方の違い 入力機器 読み取り方 得られる値 格闘コントローラー GPIOを直接読む ボタンのON/OFF ゲームパッド GPIO+ADCを読む ボタン+スティックの軸値
キーボード 行と列の交点を読む 押されているキーの番号 -
USB 3.0ではなく2.0なのはなぜか? USB 3.0を実装するなら2.0も実装しなければならない ‒ USB 3.x対応ホストはUSB 2.0側も持つ USB 2.0機器とSuperSpeed機器は、同じポート上の別のバスで共存する
‒ ユーザー接続型のUSB 3.x機器にもUSB 2.0対応が求められる ただし、機器内部に固定されたSuperSpeed専用デバイスなどには例外がある ‒ 入力機器にSuperSpeedの帯域は必要ない 20バイトのレポートを1msごとに送っても、データ本体は約20kB/s Full-Speedの12Mbpsで十分な余裕がある ‒ USB 2.0は小さく、安価に実装できる マウス、キーボード、ゲームコントローラーなどの低帯域機器に適している -
まとめ ‒ USBデバイス、特に入力機器を作るのは楽しい! ‒ 特定の機器を対応するだけなら500行程度のコードで書ける ‒ アタッチ(プルアップ)さえ済めば意外と普通のプログラミング ‒ 入力機器は応用範囲が広い (ゲームコントローラー、キーボード、マウス
etc.) ‒ DIY用のキットや、表面実装済みの基板を作成してくれるサービスがある -
参考資料 ‒ Sample Code https://github.com/kishikawakatsumi/iosdc-2026-usb-examples ‒ Universal Serial Bus Speci
cation https://www.usb.org/document-library/usb-20-speci cation ‒ RP2040 Datasheet https://pip-assets.raspberrypi.com/categories/814-rp2040/documents/RP-008371-DS-1-rp2040-datasheet.pdf ‒ Xbox Universal Serial Bus Protocol (XUSB) Interface Extension https://learn.microsoft.com/en-us/openspecs/windows̲protocols/ms-xusbi/c79474e7-3968-43d1-8d2f-175d47bef43e fi fi ‒ USB Complete http://janaxelson.com/usbc.htm