Slide 1

Slide 1 text

ゲームコントローラやキーボードの ファームウェアをSwiftで書く kishikawa katsumi [email protected] kishikawakatsumi

Slide 2

Slide 2 text

目標:操作する対象より 一段下の層を理解する

Slide 3

Slide 3 text

本日の目標 操作する対象より一段下の層を理解する ‒ 今日つくるもの ‒ USBデバイスを実装する前に ‒ 通信の仕組み ‒ 接続の通知 ‒ USBデバイスの列挙 ‒ 入力データの通信

Slide 4

Slide 4 text

今回つくる3つのUSBデバイス

Slide 5

Slide 5 text

格闘ゲーム用コントローラー

Slide 6

Slide 6 text

ー ゲームパッド

Slide 7

Slide 7 text

キーボード

Slide 8

Slide 8 text

格闘ゲーム用コントローラー ‒ 部品リスト ゲームパッド キーボード ‒ Alpakka 1 Controller https://inputlabs.io/alpakka ‒ CROSSARROW60 ‒ 基板設計・はんだ付け必要 (写真の製品は組み立てのみ) ‒ 基板設計・はんだ付け必要 (写真の製品は組み立てのみ) ‒ ※マイコンはRP2040だが ‣ 筐体(箱であればなんでもいい) 別のボード(Marmota)のため Picoとはピン配置が異なる ‣ 基板設計・はんだ付け不要 ‒ キースイッチ、キーキャップは 別に用意する必要あり ‣ アーケードボタン ‣ 配線ケーブル ‣ Raspberry Pi Pico https://shop.yushakobo.jp/products/10753

Slide 9

Slide 9 text

RP2040 Microcontroller/マイコン

Slide 10

Slide 10 text

RP2040 CPU、メモリ、入出力機能を1つのチップに収めたもの

Slide 11

Slide 11 text

Raspberry Pi Pico RP2040を載せた基板

Slide 12

Slide 12 text

GNDに繋ぐ GPIOに繋ぐ

Slide 13

Slide 13 text

GP3 GND GNDに繋ぐ GPIOに繋ぐ

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

GND GP4 GP5 GP3

Slide 16

Slide 16 text

No content

Slide 17

Slide 17 text

No content

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

格闘ゲーム用コントローラー部品リスト ‒ ボタン https://akecon.games/collections/pushbutton ‒ 配線ケーブル https://amzn.to/3V94qK9 ‒ マイコン(Raspberry Pi Pico) https://akizukidenshi.com/catalog/g/g116132/ ‒ 筐体(箱状のもの)

Slide 20

Slide 20 text

USBデバイスを実装する前に 規格書を読む

Slide 21

Slide 21 text

Universal Serial Bus Specification USB-IF (USB Implementers Forum) fi https://www.usb.org/document-library/usb-20-speci cation

Slide 22

Slide 22 text

章 タイトル 内容 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 機械的仕様 コネクタとケーブルの形状・寸法・材質の規定。プラグの形やピン配置はここで決 まっている。

Slide 23

Slide 23 text

章 タイトル 内容 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デバイス・フレームワーク デバイスに求められる振る舞いの規定。列挙の状態遷移、標準リクエストへの応答、 ディスクリプタの形式。

Slide 24

Slide 24 text

章 タイトル 内容 この文書の目的と構成、各章の概要についての紹介。 知らない言葉を調べる 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デバイス・フレームワーク デバイスに求められる振る舞いの規定。列挙の状態遷移、標準リクエストへの応答、 ディスクリプタの形式。

Slide 25

Slide 25 text

規格書は「何を実現するか」 データシートは「どう実現するか」 ‒ USB規格書は、電気信号など、外から見える振る舞いを定める ‒ ハードウェアとソフトウェアの役割分担は、マイコンごとに異なる ‒ 規格書だけでは、操作するレジスターやバッファまでは分からない ‒ データシートを読み、チップでの実現方法を確認する

Slide 26

Slide 26 text

RP2040 Datasheet 4.1 USB https://pip-assets.raspberrypi.com/categories/814-rp2040/documents/RP-008371-DS-1-rp2040-datasheet.pdf

Slide 27

Slide 27 text

RP2040 Datasheet 4.1.3.2 Standalone device example https://pip-assets.raspberrypi.com/categories/814-rp2040/documents/RP-008371-DS-1-rp2040-datasheet.pdf

Slide 28

Slide 28 text

No content

Slide 29

Slide 29 text

USBデバイスを実装する前に ハードウェアを操作する

Slide 30

Slide 30 text

Memory-Mapped I/O (MMIO) ソフトウェアからハードウェアを制御する仕組み import _Volatile let outputSet = VolatileMappedRegister( unsafeBitPattern: 0xD000_0014 // GPIO_OUT_SET ) let outputClear = VolatileMappedRegister( 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にする

Slide 31

Slide 31 text

Memory-Mapped I/O (MMIO) ハードウェアにマッピングしたメモリアドレスを操作する ‒ CPUがもともと持っている命令(load/store)を流用できる ‒ メモリアドレス空間が余っている

Slide 32

Slide 32 text

Memory-Mapped I/O (MMIO) MMIOのアドレスの先はメモリではない ‒ アドレスの先はメモリではなく実際の配線や回路に繋がっている ‒ MMIOのアドレスへの読み書きはメモリの特性を前提にできない

Slide 33

Slide 33 text

Memory-Mapped I/O (MMIO) アドレスで、反応する回路が決まる ‒ CPUは、アドレス・値・読み書き信号を内部バスへ出す ‒ アドレスデコーダーが、アドレスのビットパターンから選択信号を作る ‒ 選択信号が有効になった周辺回路だけがアクセスに反応する ‒ 値の変化をソフトウェアが監視しているのではなく、固定された論理回路によ る動作

Slide 34

Slide 34 text

Memory-Mapped I/O (MMIO) MMIOのアドレスへの読み書きはメモリの特性を前提にできない ‒ アドレスの先はメモリではなく実際の配線や回路に繋がっている ‒ MMIOのアドレスへの読み書きはメモリの特性を前提にできない ‣ 続けて同じアドレスを読めば同じ値が返る ‣ 直前に書いた値を読めば同じ値が返る

Slide 35

Slide 35 text

Memory-Mapped I/O (MMIO) MMIOのアドレスへの読み書きはメモリの特性を前提にできない ‒ アドレスの先はメモリではなく実際の配線や回路に繋がっている ‒ MMIOのアドレスへの読み書きはメモリの特性を前提にできない ‣ 続けて同じアドレスを読めば同じ値が返る ‣ 直前に書いた値を読めば同じ値が返る コンパイラはこの特性を 前提に最適化する

Slide 36

Slide 36 text

Memory-Mapped I/O (MMIO) MMIOでは最適化によって処理が省略されると困る import _Volatile struct Register { let address: UInt32 /// Reads the register value. @inline(__always) func load() -> UInt32 { VolatileMappedRegister(unsafeBitPattern: UInt(address)).load() } } /// Writes a value to the register. @inline(__always) func store(_ value: UInt32) { VolatileMappedRegister(unsafeBitPattern: UInt(address)).store(value) }

Slide 37

Slide 37 text

̲Volatile Package SwiftでVolatileなメモリアクセスを実現する import _Volatile struct Register { let address: UInt32 /// Reads the register value. @inline(__always) func load() -> UInt32 { VolatileMappedRegister(unsafeBitPattern: UInt(address)).load() } } /// Writes a value to the register. @inline(__always) func store(_ value: UInt32) { VolatileMappedRegister(unsafeBitPattern: UInt(address)).store(value) }

Slide 38

Slide 38 text

̲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

Slide 39

Slide 39 text

̲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幅で読み込む

Slide 40

Slide 40 text

USBデバイスを実装する前に 動作を観測する (デバッグ環境の構築)

Slide 41

Slide 41 text

デバッグ環境の構築 Debug Probe https://www.raspberrypi.com/documentation/microcontrollers/debug-probe.html

Slide 42

Slide 42 text

UART Universal Asynchronous Receiver Transmitter SWDポート Serial Wire Debug

Slide 43

Slide 43 text

通信の仕組み

Slide 44

Slide 44 text

USB 2.0 Wiring Diagram

Slide 45

Slide 45 text

USB 2.0 Wiring Diagram VBUS (5V) GND

Slide 46

Slide 46 text

USB 2.0 Wiring Diagram VBUS (5V) D- D+ GND

Slide 47

Slide 47 text

差動通信 D⚡High ⚡Low ⚡High ⚡Low D+

Slide 48

Slide 48 text

差動通信 D⚡High ⚡Low ⚡High ⚡Low D+

Slide 49

Slide 49 text

差動通信 2本で1つの状態を表す D- K J D+ K J J K D+ K K J K K J J J K

Slide 50

Slide 50 text

差動通信 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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

• 差動通信 信号のノイズを打ち消し合うのでノイズに強い • NRZI符号化 同期ずれによる誤差の蓄積を定期的に解消できる

Slide 54

Slide 54 text

• 差動通信 信号のノイズを打ち消し合うのでノイズに強い RP2040の「USBコントローラー」が担当するので • NRZI符号化 ソフトウェア側で意識する必要はない 同期ずれによる誤差の蓄積を定期的に解消できる

Slide 55

Slide 55 text

接続を通知する

Slide 56

Slide 56 text

USBデバイスをホスト(PCなど)に接続する USBから電力が供給され、RP2040が起動する 2 1 GND BOOTSEL LED USB 39 VBUS (5V)

Slide 57

Slide 57 text

USBデバイスをバス上に公開する 2 1 ⚡High ⬆ BOOTSEL LED USB 39 D+をプルアップして接続を知らせる

Slide 58

Slide 58 text

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) } ... }

Slide 59

Slide 59 text

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) } ... }

Slide 60

Slide 60 text

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) } ... }

Slide 61

Slide 61 text

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) } ... }

Slide 62

Slide 62 text

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) } ... }

Slide 63

Slide 63 text

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) } ... }

Slide 64

Slide 64 text

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) } ... }

Slide 65

Slide 65 text

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) } ... }

Slide 66

Slide 66 text

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) } ... }

Slide 67

Slide 67 text

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) } ... }

Slide 68

Slide 68 text

USBデバイスの列挙

Slide 69

Slide 69 text

ホストがデバイスとの通信を始める USBコントローラーが信号とパケットを処理する デバイス ホスト(PC) D+をプルアップ(Attatch) バスリセット セットアップトランザクション 接続を検知

Slide 70

Slide 70 text

ホストがデバイスとの通信を始める USBコントローラーが信号とパケットを処理する デバイス ホスト(PC) D+をプルアップ(Attatch) SIE バスリセット Serial Interface Engine セットアップトランザクション 接続を検知

Slide 71

Slide 71 text

ホストがデバイスとの通信を始める USBコントローラーが信号とパケットを処理する デバイス ホスト(PC) D+をプルアップ(Attatch) SIE バスリセット Serial Interface Engine セットアップトランザクション 接続を検知

Slide 72

Slide 72 text

ホストがデバイスとの通信を始める USBコントローラーが信号とパケットを処理する デバイス ホスト(PC) D+をプルアップ(Attatch) SIE 接続を検知 バスリセット USBコントローラーは通信の形式とタイミングを担当 Serial Interface Engine セットアップトランザクション ソフトウェアは通信の意味と内容を担当

Slide 73

Slide 73 text

ホストがデバイスとの通信を始める USBコントローラーが信号とパケットを処理する デバイス SIE̲STATUS Swift DPRAM D+をプルアップ(Attatch) SIE バスリセット BUS̲RESET SETUP SETUP̲REC handleReset() ホスト(PC)

Slide 74

Slide 74 text

ホストがデバイスとの通信を始める USBコントローラーが信号とパケットを処理する Swift DPRAM SIE̲STATUS SIE 📝 👀/📝 👀 👀/📝

Slide 75

Slide 75 text

バスリセット 39 ホストがD-/D+を両方ともLowに駆動する 2 1 ⚡High ⬆ BOOTSEL LED USB Attach

Slide 76

Slide 76 text

バスリセット 39 ホストがD-/D+を両方ともLowに駆動する ⚡High ⬆ 2 1 ⚡Low ⬇ BOOTSEL LED USB ⚡Low ⬇

Slide 77

Slide 77 text

バスリセット リセットをSerial Interface Engineが検知 Swift DPRAM SIE̲STATUS SIE BUS̲RESET

Slide 78

Slide 78 text

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) ...

Slide 79

Slide 79 text

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) ...

Slide 80

Slide 80 text

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) ...

Slide 81

Slide 81 text

〜略〜 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 〜略〜

Slide 82

Slide 82 text

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 〜略〜

Slide 83

Slide 83 text

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 〜略〜

Slide 84

Slide 84 text

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 〜略〜

Slide 85

Slide 85 text

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() ... } ... }

Slide 86

Slide 86 text

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() ... } ... }

Slide 87

Slide 87 text

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 } } ...

Slide 88

Slide 88 text

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 }

Slide 89

Slide 89 text

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 }

Slide 90

Slide 90 text

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 }

Slide 91

Slide 91 text

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 }

Slide 92

Slide 92 text

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) ...

Slide 93

Slide 93 text

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) ...

Slide 94

Slide 94 text

W1C (Write 1 to Clear) 1を書いたビットだけをゼロにする 0000 0000 0000 1000 0000 0000 0000 0001

Slide 95

Slide 95 text

W1C (Write 1 to Clear) 1を書いたビットだけをゼロにする 0000 0000 0000 1000 0000 0000 0000 0001 0000 0000 0000 1000 0000 0000 0000 0000

Slide 96

Slide 96 text

W1C (Write 1 to Clear) 19ビットにだけゼロを書く? 0000 0000 0000 0000 0000 0000 0000 0001

Slide 97

Slide 97 text

W1C (Write 1 to Clear) 1bitだけを書くということはできない 0000 0000 0000 1000 0000 0000 0000 0001 0000 0000 0000 1000 0000 0000 0000 0000

Slide 98

Slide 98 text

W1C (Write 1 to Clear) 更新が衝突すると上書きしてしまう 0000 0000 0000 1000 0000 0000 0000 0001 0000 0000 0000 0000 0000 0000 0000 0001

Slide 99

Slide 99 text

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) ...

Slide 100

Slide 100 text

〜略〜 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

Slide 101

Slide 101 text

〜略〜 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

Slide 102

Slide 102 text

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) ...

Slide 103

Slide 103 text

USBデバイスプログラミングの構造 USBコントローラーの状態を確認 BUS̲RESET ソフトウェアの状態を初期化 SETUP̲REC リクエストの種類ごとに応答 IN転送(送信)完了 次の送信データを用意 OUT転送(受信)完了 受信データを処理し、次の受け皿を用意

Slide 104

Slide 104 text

SETUPリクエストの処理 SIE ホスト(PC) SETUP DPRAM SIE̲STATUS パケットを保存 SETUP̲REC→1

Slide 105

Slide 105 text

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 }

Slide 106

Slide 106 text

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(その他)

Slide 107

Slide 107 text

SETUPリクエストの処理 ホストからデバイスへの関数呼び出し(RPC) bmRequestType - データの転送方向(ホスト→デバイス/デバイス→ホスト) - リクエストのタイプ(標準、クラス、ベンダー) - 宛先(デバイス、インターフェース、エンドポイント) bRequest 関数名 具体的に何を実行するかを指定するコード 例: 0x06=GET_DESCRIPTOR, 0x05=SET_ADDRESSなど wValue bRequest に応じた引数 第1引数 ディスクリプタを要求する場合は「ディスクリプタのタイプとインデックス」 wIndex 主にインターフェース番号やエンドポイント番号の指定 wLength 続く「データステージ」で転送されるデータのバイト数を指定。データステー 入力データ・戻り値 ジがない場合は 0。 第2引数

Slide 108

Slide 108 text

SET̲ADDRESS デバイスに個別のアドレスを割り当てる bmRequestType bRequest wValue 00 05 05 00 00 00 00 00 5(新しいアドレス) SET̲ADDRESS ホスト → デバイス

Slide 109

Slide 109 text

SET̲ADDRESS(5) デバイスに個別のアドレスを割り当てる SET̲ADDRESS(5) ZLP(Zero-Length Packet)(完了の応答)を返す アドレスを5へ変更 let address = UInt32(setup.wValue) & 0x7F Control.acknowledge { USB.addressEndpoint.store(address) }

Slide 110

Slide 110 text

GET̲DESCRIPTOR(Device) Device Descriptor:基本情報を返す wLength 80 06 00 01 00 00 12 00 18 byte Device Descriptor GET̲DESCRIPTOR デバイス → ホスト

Slide 111

Slide 111 text

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

Slide 112

Slide 112 text

GET̲DESCRIPTOR(Configuration) Con guration Descriptor:基本情報を返す 80 06 00 02 00 00 31 00 49バイト Con guration Descriptor GET̲DESCRIPTOR fi fi デバイス → ホスト

Slide 113

Slide 113 text

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

Slide 114

Slide 114 text

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)

Slide 115

Slide 115 text

入力の読み取り方の違い 入力機器 ハードウェアの構成 入力の読み取り方 格闘ゲーム用コントローラー 1個のボタンと1本のGPIOが1対1 各GPIOがHighかLowかを読む。内部プルアッ で対応 プを使うため、Lowなら押下 ゲームパッド ボタンに加えて、アナログス ボタンはGPIOから読む。スティックはADCで ティックの各軸をADCピンへ接続 電圧を数値に変換し、X軸・Y軸の値として使う 一般的なキーボード 多数のキーを、行と列に並べた キーマトリックスとして接続 行を1本ずつ選択して列を読み、反応した行と列 の交点から押されたキーを特定する

Slide 116

Slide 116 text

入力の読み取り方の違い 入力機器 読み取り方 得られる値 格闘コントローラー GPIOを直接読む ボタンのON/OFF ゲームパッド GPIO+ADCを読む ボタン+スティックの軸値 キーボード 行と列の交点を読む 押されているキーの番号

Slide 117

Slide 117 text

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は小さく、安価に実装できる マウス、キーボード、ゲームコントローラーなどの低帯域機器に適している

Slide 118

Slide 118 text

デモ ファームウェアを入れ替えるだけでゲームパッドがキーボードになる

Slide 119

Slide 119 text

まとめ ‒ USBデバイス、特に入力機器を作るのは楽しい! ‒ 特定の機器を対応するだけなら500行程度のコードで書ける ‒ アタッチ(プルアップ)さえ済めば意外と普通のプログラミング ‒ 入力機器は応用範囲が広い (ゲームコントローラー、キーボード、マウス etc.) ‒ DIY用のキットや、表面実装済みの基板を作成してくれるサービスがある

Slide 120

Slide 120 text

サンプルコード ‒ https://github.com/kishikawakatsumi/iosdc-2026-usb-examples

Slide 121

Slide 121 text

参考資料 ‒ 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