Tech Notes

ドライバインストールなしで独自USBデバイスを認識させる

Zephyrとラズパイpicoで独自USBデバイスを作るの続編記事である。

Windowsではドライバのない独自USBデバイスは上手く認識されずに弾かれてしまう。キーボード、マウス、USBメモリのような既定のデバイスであれば良いのだが、自作の独自デバイスではダメだ。

独自デバイスだったらどうせ対応するドライバも自作が必須になるのだから仕方ないことではないか、と思うかもしれないがそうではない。独自ドライバの設定もインストールもなしに挿すだけでデバイスを認識させる方法がある。

WinUSBについて

デバイスの使用にはデバイスドライバが必要になる。普段様々なUSBデバイスを活用できるのはOSにそのデバイス用のドライバが入っているからだ。

キーボードやマウスのような一般的デバイスはOS標準のドライバがある。だからどんな企業の製品でもそのまま接続して使える。また企業が独自に開発したデバイスでは大抵デバイスドライバも同時に配布されている。これをインストールするからデバイスが使えるのである。

ところで、WindowsにはWinUSBという何にでも使える汎用デバイスドライバがある。これはWindows標準装備の機能だ。

これさえ利用できれば独自デバイス開発でもドライバ開発の必要は無い。専用デバイスドライバの無い独自デバイスでも、WinUSBをドライバに使って使用できる。管理者権限のないユーザーモードのアプリ開発だけで独自デバイスとやり取りできるのである。これは開発のハードルが低くてかなり嬉しい。

ところがデバイスの「認識」は話が別だ。Windowsはデバイス接続時にデバイスを識別し、適合するドライバを割り当てるように出来ている。マウスならマウスの、キーボードならキーボードのドライバを割り当てる。

しかし、ただの独自デバイスでは何のヒントもない。

従ってWindowsはWinUSBを割り当てることができない。WinUSBデバイスとして認識されればWinUSBが使えるのに、そのままではWinUSBデバイスとして認識されないのである。

Zadigを使う方法とOS Descriptorを使う方法

Zadigというフリーソフトがある。これを使うと独自デバイスに対してWinUSBのドライバを割り当てることができる。そのためZadigを使えばドライバ開発の手間なしに独自デバイスを使える。

Zadig(リンク)

ただし使用に管理者権限は要るし、設定の手間がある。

一方実はデバイス側で工夫することで、設定作業無しに挿すだけでWinUSBデバイスとしてWindowsに認識させられる方法がある。それがOS Descriptorと呼ばれるもので、デバイス側でこれを提示すると設定も何も要らずにWindowsが自動で認識してくれる。この記事ではこれについて解説していく。

長い前置きだった。

こんな長い記事読んでられねえよという方へ

実はZephyr付属のサンプルコードが非常に参考になる。

Zephyrで手っ取り早く動かしたい、あるいは長々しい解説よりコードを読む方が分かりやすいという人はこれを読めばよいと思う。

webusbサンプル(リンク)

このコードの本質的な箇所は以下の通りだ。

  • usbd_device_set_bcd_usbでUSBバージョン2.1以上(0x0201)を指定している。
  • usbd_add_descriptorでBOSデスクリプタbos_vreq_msosv2を指定している。
  • bos_vreq_msosv2の定義においてmsosv2_to_host_cbコールバックを指定し、その中でOSデスクリプタmsosv2_descのデータを送信している。
  • WEBUSB_DEVICE_INTERFACE_GUIDの値はGUIDの形であれば自由に変えて良い(というか変えるべき)。

以降の記事の内容は言ってしまえば、このwebusbサンプルのコードをちゃんと理解するための説明になる。

ちなみに筆者はTL;DRというカスの略語が嫌いだ。

USBデスクリプタについて

USBデスクリプタについて改めて説明すると、これはUSBデバイスを接続した際にデバイスが提示する「自己紹介」のようななものである。

通常はデバイス名、必要電力、通信方法といった情報をやり取りする。これによってWindowsやMacといったOSはUSBデバイスを認識することができる。

しかしところで、このUSBデスクリプタではBOS Descriptorと言ってベンダ定義の特殊なデータも提示することができる。Microsoft OS Descriptorはこの枠組みに乗ったものであり、Microsoftの決める所定の形式のBOSデスクリプタを出すことによってWindowsから認識される。

これから説明するのは、速い話がMicrosoftの独自規格だ。

Microsoft OS 1.0 Descriptor

Microsoft OS Descriptorには1.0と2.0の2つのバージョンがある。

1.0はやや無茶な方法で実現されており、ストリングデスクリプタ(文字列テーブル)の0xEE番に"MSFT100"という文字列を仕込むというものである。このデータがあれば未知のデバイスでもWindowsは認識してくれるという。自由に使えてしかるべきデータ空間で特定のインデックスを専有するのはどうかと思うが、そういう風にしてしまったようだ。

この仕組みは案の定問題を引き起こしていた。Microsoftの独自仕様など知らない可哀そうな無辜のUSBデバイスが、0xEE番などという存在しないデータ番地を参照され、失意の内にハングアップしていったという。

それからというものMicrosoftは反省し、Microsoft OS 2.0 Descriptorなる新しい規格を作り出したそうな。

Microsoft OS 2.0 Descriptor

より複雑になっているぞクソったれ。

情報源は以下の公式情報に基づく。

USBバージョン

Microsoft OS 2.0 Descriptorを実装していると認識される前提として、USBバージョンがUSB2.1以上でなければならない。なぜなら後述するBOS DescriptorはUSB2.1で定義されるデスクリプタだからだ。

具体的にはデバイスデスクリプタのbcdUSBプロパティについて0x201以上の値を示す。これをしないとWindowsは確認すらしてくれない。

BOS(Binary device Object Store) Descriptorについて

これはUSB標準に定義されているUSBデスクリプタの一種である。これ自体はMicrosoftの独自規格ではない。

さまざまな拡張情報が入れられるデスクリプタで、Descriptor Typeは15(0x0F)が割り当てられている。Microsoft OS Descriptorはこの仕組みを用いて定義されている。

このBOS Descriptorがまずややこしい。

USB2.1以降で定義されるデスクリプタなので、USB2.0の仕様書を見ても載っていない。USB2.1の仕様書というものはなぜか存在せず、USB 3.2の仕様書を見に行くと載っている。

BOS Descriptorの構造

構造は以下のようになっている。

フィールド名バイト数概要
bLength1BOS Descriptorのバイト数
bDescriptorType1デスクリプタ種別(定数:0x0F)
wTotalLength2サブデスクリプタ含めたBOS Descriptorのバイト数
bNumDeviceCaps1Device Capability Descriptorの数

USBデスクリプタは階層構造を持つのだが、BOS Descriptorは任意個のDevice Capability Descriptorをサブデスクリプタとして抱える。立ち位置としてはそのDevice Capability Descriptorの方がデータ本体であり、BOS Descriptor自体は取りまとめ役に過ぎないので上記のように単純だ。

Device Capability Descriptorの構造

BOS Descriptorの下にぶら下がるDevice Capability Descriptorは以下のような構造になっている。デスクリプタ種別IDは16(0x10)が割り当てられている。

フィールド名バイト数概要
bLength1Device Capability Descriptorの長さ
bDescriptorType1デスクリプタ種別(定数:0x10)
bDevCapabilityType1Device Capabilityの種別(後述)
(Device Capability)不定種別ごとに異なるデータ

Device Capability Descriptorは可変長かつ種類があり、その種類一覧は以下に定義されている。

https://www.usb.org/bos-descriptor-types

リンク先を見れば分かるようにBOS Descriptorの上ではかなり色々な情報が提示できるのだが、Microsoft OS Descriptorを定義するにあたっては、bDevCapabilityType0x05(プラットフォーム/OS依存の情報)を指定する。Windowsというプラットフォームに依存する情報だからちゃんとそういう枠組みを利用している訳だ。偉いぞ。

Device Capability Descriptor 0x05(プラットフォーム依存情報)

フィールド名バイト数概要
bLength1Device Capability Descriptorの長さ
bDescriptorType1デスクリプタ種別(定数:0x10)
bDevCapabilityType1Device Capabilityの種別(定数:0x05)
bReserved1定数:0x00
PlatformCapabilityUUID16プラットフォーム依存データを示すUUID
CapabilityData不定プラットフォーム依存データ

PlatformCapabilityUUIDに情報の種類を示すUUIDを置く。これのおかげで色々な事業者が好き勝手に独自のデータ規格を定義しても衝突せず、また各社は自分の定義したデータ規格を見分けられるという建て付けなのだろう。

Microsoft OS 2.0 Descriptorを示すUUIDはD8DD60DF-4589-4CC7-9CD2-659D9E648A9Fだ。しかしここも注意が必要で、Microsoftの慣習により前の3グループだけリトルエンディアンで後ろの2グループだけビッグエンディアンらしい。

つまりバイナリとしては

(誤) D8 DD 60 DF | 45 89 | 4C C7 | 9C D2 | 65 9D 9E 64 8A 9F

ではなく

(正) DF 60 DD D8 | 89 45 | C7 4C | 9C D2 | 65 9D 9E 64 8A 9F

という形になる。Microsoftは頭がイカレている。

Microsoft OS 2.0 platform capability descriptor

CapabilityDataの中身はこのように書くとされている。

フィールド名バイト数概要
dwWindowsVersion4サポートするWindowsの最低バージョン
wMSOSDescriptorSetTotalLength2Microsoft OS Descriptor本体の長さ
bMS_VendorCode1後述
bAltEnumCode1割愛(0x00で問題無し)

dwWindowsVersionの値はWindows APIのsdkddkver.hに定義されるNTDDI_xマクロの数値を用いる。例としてWindows10(NTDDI_WIN10)は0x0A000000という値になっている。

bMS_VendorCodeの値は後述する。適切な値を出す必要がある。

bAltEnumCodeについてはこの記事では解説しない。基本的に0x00で良い。

このデスクリプタを受け取ったあと、Windowsはコントロール転送でデバイスにMicrosoft OS Descriptorのデータ本体を要求する

ここまでの内容はWindowsがOS Descriptorのデータ本体をデバイスに"問い合わせるための情報"で、まだ本体ではない。

WindowsによるOS Descriptorの問い合わせ

BOSデスクリプタを読んだWindowsはこんな感じのsetupパケットを送るので、デバイスはそれにOS Descriptorの内容を返す。

フィールド名バイト数内容
bmRequestType10b11000000
bRequest1bMS_VendorCodeの値
wValue20x00
wIndex2MS_OS_20_DESCRIPTOR_INDEX(=0x07)
wLength2長さ

bRequestの値として、デバイスの提示したbMS_VendorCodeの値が使われる。ここが要注意な仕様だ。

bMS_VendorCodeに変な値を選ぶとGET_DESCRIPTORなどの標準リクエストとかち合う可能性があるので、使われていない数値を使用することに注意する。Zephyrのサンプルプログラムでは0x02になっていた。

流れとしては以下のようになっている。

Microsoft OS 2.0 Descriptor本体

ようやくOS Descriptor本体。デバイスはWindowsから問い合わせがあったときにコントロール転送でこれを返す。

OS Descriptorの基本単位

Microsoft OS 2.0 Descriptorは以下のような基本構造をもつ。

フィールド名バイト数概要
wLength2バイト数
wDescriptorType2デスクリプタ種別
---wLengthデスクリプタ内容

このような構造を単位とし、それを並べることになっている。

wDescriptorTypeの値は全部で9種が規定されている。

名前wDescriptorTypeの値
MS_OS_20_SET_HEADER_DESCRIPTOR0x00
MS_OS_20_SUBSET_HEADER_CONFIGURATION0x01
MS_OS_20_SUBSET_HEADER_FUNCTION0x02
MS_OS_20_FEATURE_COMPATBLE_ID0x03
MS_OS_20_FEATURE_REG_PROPERTY0x04
MS_OS_20_FEATURE_MIN_RESUME_TIME0x05
MS_OS_20_FEATURE_MODEL_ID0x06
MS_OS_20_FEATURE_CCGP_DEVICE0x07
MS_OS_20_FEATURE_VENDOR_REVISION0x08

このような基本単位が並んだ上で、後述するように大きい全体構造がある。

OS Descriptorの全体構造

全体としてはこんな感じの構造になっているらしい。

  • MS OS 2.0 Descriptor Set Header
  • Feature Descriptor × n個
  • Configuration Subset × n個

ヘッダがあり、Feature Descriptorが並び、Configuration Subsetが並んでいるという構造だ。基本的にはFeature Descriptorがメインとなる種々のデータで、ヘッダなどはそれに構造を与える役割である。

各Configuration Subsetは以下のような構造を持つ。

  • Configuration Subset Header
  • Feature Descriptor × n個
  • Function Subset × n個

各Function Subsetは以下のような構造を持つ。

  • Function Subset Header
  • Feature Descriptor × n個

要するに、

  • ヘッダー
  • 任意個のFeature Descriptor
  • 任意個の下位サブセット

という纏まりがあって、それが3階層の階層構造を形成している。

  • OS Descriptor
    • Configuration Subset
      • Function Subset

下位サブセットは0個でも良い。従って最も簡単なOS Descriptor 2.0は以下のようになる。

  • MS OS 2.0 Descriptor Set Header
  • Feature Descriptor

というかこれをやればよい。3階層とかやる必要はない。

ということでMS OS 2.0 Descriptor Set HeaderとFeature Descriptorについて順に見ていく。

MS OS 2.0 Descriptor Set Header

データ全体を統べるヘッダ(MS_OS_20_SET_HEADER_DESCRIPTOR)。一番最初に置くのがこれ。

フィールド名バイト数概要
wLength2バイト数(定数:10)
wDescriptorType2デスクリプタ種別(定数:0x00)
dwWindowsVersion4Windowsのバージョン
wTotalLength2OS 2.0 Descriptor全体のバイト数

Windowsのバージョンと全体の長さくらいしかないのでシンプルだ。

Feature Descriptorについて

MS OS 2.0 Descriptor Set Headerの直後、もしくは各Subsetの中にこれがある。

WinUSBで認識されるのに必須なのはMS_OS_20_FEATURE_COMPATBLE_IDMS_OS_20_FEATURE_REG_PROPERTYの2つなので、それに絞って解説する。

MS_OS_20_FEATURE_COMPATBLE_ID

これは何かというと、OS Descriptor 1.0で規定されるextended configuration descriptorと同等のものだと書いてある。しかし1.0のドキュメントを引いてもExtended Configuration Descriptorなどという言葉は出てこない。おそらくExtended Compatible ID Descriptorのこととと思われる。

フィールド名バイト数概要
wLength2バイト数(定数:20)
wDescriptorType2デスクリプタ種別(定数:0x03)
CompatibleID8Compatible ID文字列
SubCompatibleID8Sub Compatible ID文字列

Compatible IDとはなんぞやというところだが、これはWindowsがデバイスドライバを自動インストールするために使用されるヒント文字列である

これは本当に頭がおかしいとしか思えないのだが、USB標準にはVID/PIDやらデバイスクラスやらといったデバイス識別のための仕組みが豊富に整備されているというのに、Windowsは独自規格のデータであるCompatible ID/Sub Compatible IDを認識するとそちらを元にデバイスドライバをインストールするようになっている。デバイスクラスなどはガン無視する。気が狂っている。

例えばCompatibleIDの中にWINUSBなどという文字列を入れているとWinUSBデバイスとして認識されるのだ。8バイトに満たない部分は'\0'(0x00)で埋める。

ということでWinUSBで認識されたかったらCompatibleIDWINUSB\0\0とし、SubCompatibleID\0で埋めればよい。

とにかく「こういう文字列を指定すればそれを認識したWindowsは対応するデバイスドライバを入れるよ」というのがいくつか決まっている。それにも関わらず、その使える文字列の完全な一覧はない。本当にない。

MS_OS_20_FEATURE_REG_PROPERTY

これはデバイスに対応したレジストリデータを指定するプロパティである。デバイスがWindows PCに接続されると、これを元にレジストリデータが更新される。

フィールド名バイト数概要
wLength2バイト数
wDescriptorType2デスクリプタ種別(定数:0x04)
wPropertyDataType2レジストリ設定種別
wPropertyNameLength2レジストリプロパティ名の長さ
PropertyName不定レジストリプロパティ名(UTF16-LE)
wPropertyDataLength2レジストリプロパティデータの長さ
PropertyData不定レジストリプロパティデータ

更新されるのは以下のレジストリパスのデータである。ここに任意の名前のプロパティを入れられる。

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\USB\<VID_vvvv&PID_pppp>\<シリアルナンバー>\Device Parameters\<PropertyName>

ここでDeviceInterfaceGUIDsという名前のプロパティを作り、{nnnnnnnn-nnnn-nnnn-nnnn-nnnnnnnnnnnn}というUTF16-LE文字列の形式でGUIDを入れる。GUIDは名目としてはデバイスクラス識別用であり、デバイスは品種ごとに異なるランダムなGUIDを名乗ることが想定されているようだ。デバイスを量産するならそれぞれ同じGUIDを入れ、異なるデバイスを作るなら異なるGUIDを入れる。

デバイス品種識別のためだとは思うのだが、そんなのUSB機器ならVID/PID/bcdDeviceでやるべきだろと思う。でも必須なのだから仕方ない。このFeature Descriptorを省略してみたらデバイスは認識されなかった。

まとめ

Microsoft OS 2.0 Descriptorというのを実装すると挿すだけで認識されるUSB機器を作れるという話だった。

必須なのは

  • USB2.1以上のバージョン提示
  • 適切なBOS Descriptorの提示
  • Windowsからの問い合わせへの応答としてMS OS 2.0 Descriptorの提示
  • 最小構成としてヘッダと2つのFeature Descriptor

ここを押さえれば動作すると思う。

この記事ではOS Descriptorの中でConfiguration Subsetなどは使わなかったが、複数インタフェースを持つ複合型USBデバイスとかを作る場合は必要になるのだと思う。そちらについては検証していないので、やってみることがあったらまた記事にしたい。

コメント