lyricalmagicalのアイコン画像
lyricalmagical 2026年04月11日作成 © CC BY-NC 4+
製作品 製作品 閲覧数 46
lyricalmagical 2026年04月11日作成 © CC BY-NC 4+ 製作品 製作品 閲覧数 46

Raspberry Pi Pico2でtiny-USBを使わずに単純なUSBデバイスを作ってみる

Raspberry Pi Pico2でpico-SDKベースでtiny-USBを使わずUSBdeviceを作ってみようという試みです。

この記事の読むための前提知識

  • pico-sdkを使ったRaspberry Pi Picoの開発環境の使い方
  • Visual Studio 2022でWindows用コンソールアプリの開発環境の使い方

目標

WindowsからUSB経由(WinUSB)でGPIO入出力したい。
もう少し具体的にいうと、WindowsでPi PicoのGPIOに繋がっているLEDを操作したり、GPIOに繋がっているスイッチを読み取ったりしたいと思います。

必要なもの

  • RP2350開発ボード
    今回はWeACT Studioのものを使用しました。こちら→https://ja.aliexpress.com/item/1005008117237405.html
  • 適当なLED。RP2350に接続できるものが必要です。抵抗内蔵型が便利です。
  • 適当なスイッチ
  • UART→USB変換ブリッジ
    ログ表示のために必要です

USB通信の概要

USB通信は速度が色々ありますが、Pi Picoが対応しているのはUSB1時代の速度(LS:1.5Mbps / FS:12Mbps)のみです。
今回はFSで実装します。
※USB1は正式な呼称ではありませんが概要としてこう記載しました

USBはPC側をhost、接続するもの(USBメモリなど)をdeviceと呼びます。
※両端USB-Cのケーブル等でhost同士を接続したときはどうなっているのか調べてないのでわかりません。USB-CはUSB3.0の範疇のはずで、USB OTGならUSB2です。USB2以降についてはほぼ今回の記事中では触れません。

基本的な原則

USBの通信は基本的に複数のdevice向けの通信が一つのバスに混在しています。概念としては一つのバスに複数のdeviceが並列にぶら下がっているイメージです。
そのため、USB deviceはどの通信が自分に対しての通信かを識別するため、USB deviceはhostからdevice addressを割り当てられ、そのaddressに対してhostから通信が行われた場合に応答します。
※ただし、異なる速度のdeviceが混在している場合、バス使用効率を上げるため、HUBがいい感じにポート別に割り振ってくれたりすることもあります(特にHUB経由で上位が高速、device側が低速の場合など)。

また、転送方向にはIN転送とOUT転送があり、IN方向がdevice→host、OUT方向がhost→deviceです。

初期ネゴ

USB deviceをhostに接続したとき、初期ネゴは以下のような動作を順にします。
※ちなみに間にHUBが挟まっていたとしても、device側から見るとHOSTに接続されたのと(少なくともPi Picoで使う限りは)変わりません。

1.USB hostはdeviceが接続されると、電気的(パケット的ではない)にネゴって速度検出をします。Pi Picoの場合はUSB関連レジスタを設定しておけば自動でやってくれます。

2.hostは新たに接続されたdeviceに対してBUS resetを発行します。
※前述したとおり、基本的にUSBはバス上に複数deviceに対する信号は混在しますが、HUBを経由した場合、BUS resetはdeviceが接続されている末端の1ポートだけに発行します。
BUS resetを検知すると、Pi Picoは割り込みを発行します。
BUS resetを検知したdeviceは、自身のUSB関連情報をすべてリセット(device address=0など)します。

3.SETUPトランザクションが始まります。このトランザクションはdevice address 0に対して行われます。最初はhostがGet Device Descriptor(wLength=64)を発行します。
deviceはwLength=64のため、deviceのDescriptrが64byte超過の場合はDescriptorの先頭64byteだけdeviceアドレス0として応答します。64byte以下の場合はすべて送信します。

4.(ものによっては)hostは再度BUS resetを発行します。

5.hostがSet Addressを発行します。deviceはdevice address 0からZLP(zero length packet)で応答します。応答が終わった後、deviceは自身のアドレスをhostから通知された値に変更します。以降、応答はこのアドレスで行います。

6.hostは各種(device/config/string)のGet Descriptorを発行します。deviceは対応するDescriptorを応答します。
この際、hostからdevice qualifier descriptorを要求されることがありますが、これはUSB 2.0以降のデバイス用パラメータのため、今回対象とするFSデバイスではSTALL応答します。

一通りDescriptorの取得が終わると、USB deviceとして動作できる状態になります。

データ転送

今回は一番簡単(?)なbulk転送を実装します。bulk転送とは主に大量のデータ転送に使われるものです。FIFOに空きがない場合や、エラーが発生した時の再送が自動で行われます。
bulk以外にはinterruptとisochronousがあります。interruptはキーボードなどdevice側から割り込みを発生するもの、isochronousはUSBオーディオなどのリアルタイム性が必要なものに使用しますが、詳細な説明は割愛します。
USB deviceにはEndpoint(以下EPと記載)といわれるデータの送受信の窓口のようなものがあります。
EPは0~7があり、そのうちEP0はSETUPで使われる特殊なものになり、ユーザーデータの転送に使われるのはEP1~7のいずれかになります。どれを使うかは前述の初期ネゴでやりとりされるDescriptorに記載してあります。
また、それぞれのEPはINとOUTがあり、INはdevice→host用、OUTはhost→device用に使います。
基本的にUSBのデータ転送タイミングはすべてhostが管理し、deviceから任意のタイミングでhostにデータを送信することはできません。←ここ重要
なので、「今GPIOに繋がっているスイッチの状態が変化しました」とdeviceからhostに通知することはできません。これはbulk以外も同様です。あくまでhostが知りたいタイミングでdeviceに対してIN要求をしてくる動作になります。

今回はEP1をIN用、EP2をOUT用として使用します。

pico側ソース

基本的には上記の初期ネゴの実装が大半になります。
なるべくHWの動作がわかりやすいようにソースを作ったつもりです(その代わり他用途への流用性は乏しい)。
※ソースの元ネタが記事末の参考資料記載のソフトになります。かなり流用しています。

基本的な動きとしては、割り込みに応じて応答を返していく形です。
また、USBの規格としてわりと高速で応答しないといけない部分があり、printfで都度logを出力してそこでブロッキングされると速度が間に合わなくなるため、UART転送を別coreで行い、printf自体はノンブロッキングとなるようにprintfのラッパー(nb_printf)を作っています。

また、GPIO状態をhostに通知する処理はbulk IN転送を使います。
Pico2のUSB HWは、「事前に転送データをバッファに書き込んでおき、hostがINリクエストを発行してきたときにそのバッファの内容を送る」という動作になります。なので、理論上、INリクエストを受け付けたときのGPIO状態を応答するというのは不可能です。あくまで、事前にバッファに積んだときの状態が送信されるだけです。
出来る限りリアルタイム性を高くするためには、INリクエストを受ける直前にバッファに積む必要がありますが、INリクエストがいつ来るかは予想できません。また、一度バッファに書き込んだ内容をPico2側ソフトから消すことはできないため、常に最新の状態を上書きし続けることもできません。
そのため、今回は、bulk OUTデータを受け取った段階でGPIOを取得し、IN送信用バッファに書き込むという動作にしています。
実運用としては、hostがGPIO状態を取得したいときは、いったんOUT転送をし、その直後にIN要求を発行するという使い方になります。
これはあくまでPico2のHW制限を運用で回避するためのものであり、このような実装はUSBの規格として必須ではありません。
※ちなみに、hostからのbulk IN要求に対して、データを送り返すまでの最大時間は(マイコン視点だと)かなり短く、bulk IN割り込み→GPIO読み出し→そのデータを送り返す、というような実装はPico2を含め似たような構成のマイコンでは速度的には難しいため、現実的ではありません。FPGAなど完全なハードウェアロジックであれば容易に可能です。

また、logの出力はpicoのUART出力を使っています。今回は使用するUART→USBブリッジの都合でTXを反転出力しています。
反転が不要な場合は、main.cppの
gpio_set_outover(0, GPIO_OVERRIDE_INVERT); // TX inverted
をコメントアウトしてください。

build方法

rp2350用にmake環境を作る場合は、buildディレクトリを作成し、buildディレクトリ配下で以下のコマンドを実行します。
$ cmake -DPICO_PLATFORM=rp2350 -DPICO_BOARD=pico2 ..
その後、コンパイルは
$ make
で、uf2ファイルが作成されます。
※pico-SDKのインストールが完了していて、パス設定までは完了している前提です。

配線

GPIO0がUARTのTXです。
GPIO2~9が出力(LED用)です
GPIO10~17が入力(SW用)です

キャプションを入力できます

※今回の実験ではLEDx4、SWx4のみ接続しています。ソースファイルとしては各8個まで接続できるように作ったつもりです。

PC側

WinUSBドライバのインストール

上記で作成したuf2ファイルをpico2に書き込むと、WindowsではUSBデバイスとして認識するようになります。
VID/PIDが適当ですので、当然ですので自動でデバイスドライバは適用されません。
きちんと署名してinfファイルなどを使うと自動インストールもできるようになるはずですが、めんどくさいのでひとまずは手動でインストールします。
インストール方法は、デバイスマネージャの対象デバイス(ほかのデバイス配下のotamesi product)を右クリックして、「ドライバーの更新」を選択します。
※この時点では、対象デバイスに「!」が付いています。
キャプションを入力できます
次に、「ドライバーの検索方法」で、「コンピューターを参照して~」を選択します。
キャプションを入力できます
次に、「コンピューター上のドライバーを参照します。」で「コンピューター上の~」を選択します。
キャプションを入力できます
次に、「次の一覧から~」で、「ユニバーサル シリアル バス デバイス」を選択します。
キャプションを入力できます
次に、「このハードウェアの~」で、製造元で「WinUsbデバイス」を、モデルで「WinUsbデバイス」をそれぞれ選びます。
キャプションを入力できます
次に、動かないかもしれないよ(意訳)という警告が出ますが構わず進めます。
これで、正常にデバイスドライバがインストールされ、「!」表示が消えて「ユニバーサル シリアル バス」配下に「otamesi product」が移動します。

Visual Studio 2022でのコンパイル

Visual Studioの詳細な使い方は触れません。
本当はデバイスドライバのインストール時にinfファイルを作って、そこに独自のGUIDを割り当ててそれを検索するべきですなのですが、上記のデバイスドライバのインストール手順だとGUID割り当てはできないため、GUID_DEVINTERFACE_USB_DEVICEですべて列挙しています。
このような方法で列挙する場合、VID/PIDでフィルタすべきですが、そのあたり複雑になるので実装していません。したがって、PCに複数のWinUSBデバイスが接続されていると正しく動きません。
このサイトの趣旨として、あくまで電子工作(デバイス側)に主眼を置き、PC側ソフトの細かい作法などについては省略したいため、なるべくPC側は簡略化しています。

実験ツールの使い方

上記でVisual Studioで作ったファイルをWindows上から実行します。
例)
 > WinUSB 12345678
コマンドパラメータで送信データを設定します。可変長です。
とはいえ、先頭1byteをGPIO出力するだけなので、2byte目以降はpico2側のlogに受信データとして表示されるだけで特に意味はありません。可変長で送信できるという実験用です。

ソース一式

直接ここに貼れる量ではないため、google driveに格納してあります。
※ソースファイルのライセンスについては同梱のlicence.txtを参照してください。本記事のライセンスとは異なります。
google drive

動作確認

作成物の様子

キャプションを入力できます

キャプションを入力できます

LED点灯

以下のようなコマンドでコマンドに応じてLEDが点灯します
 >WinUSB ff
 >WinUSB 01
 >WinUSB 02
 >WinUSB 04
 >WinUSB 08

キャプションを入力できます

SW入力

C:>WinUSB 00
受信データ(bulk INデータ)が表示されます。
SWが押されている状態に応じて、受信データの1byte目の押したスイッチに対応するbitが0になります。
キャプションを入力できます

log

pico2のTX出力をUART→USB変換などで変換し、PCでターミナルソフトなどで表示すると、USB接続の動作ログが表示されます。
115200bpsです。
下記の例では、初期ネゴの最初の最初が見えています。
キャプションを入力できます

最後に

USBで直接PCとPi Pico間でデータ通信をするお試し第一歩ができました。
PCとのデータ通信でUART(シリアルポート)経由を使う例がよくありますが、このようにするとシリアルブリッジではなくUSBデバイスとして見えるので美しい(?)かもしれませんね。
基本的にはバルク転送デバイス(FT232系やマスストレージなど)はこの形をベースに送受信するデータを作っていけばよいので、ここから発展させると色々作れるかもしれません。
実際にはtiny-USBという非常に便利なライブラリがあるので、このあたりまで自分で実装する必要性は薄いですが、USBの動作を理解する上での実験としては面白いと思います。

参考資料

  • USB 2.0 Specification
    全部隅から隅まで目を通すのは無理なので必要に応じて関係ありそうなところを参照します(本当は良くない)。重要なのはusb_20.pdfです。
    ちなみに今回作るのはUSB FS device(USB 1.1)を目標にしているので、USB 1.1の仕様の範囲内ですが、そのあたりの仕様についてもUSB 2.0 Specに記載されています。

  • me56ps2-emulator-rp2040
    Raspberry Pi Pico(1)でtiny-USBを使わずにUSB deviceを作成している例になります。かなり参考にさせていただきました。

lyricalmagicalのアイコン画像
FPGAとか好きな人
ログインしてコメントを投稿する