WindowsアプリでUSB機器を扱う方法 ── 仮想COM・HID・WinUSB・専用SDKの選び方

· · USB, HID, WinUSB, シリアル通信, 装置連携, Windows開発, C#, デバイスドライバー

「この装置、USBでつながるのでアプリから叩けますよね?」── 装置連携の相談で最初に出てくる質問です。答えは「つなぎ方によります」で、この一言に開発工数の数十倍の差が隠れています。同じ「USB接続の機器」でも、COMポートとして見えるのか、HIDとして見えるのか、専用ドライバーが要るのかで、書くコードも配布方法も、現場で起きるトラブルの種類もまるで別物になります。

厄介なのは、この判断がアプリを書き始める前に必要なことです。「とりあえずSDKを入れて動いた」で進めると、後になって「64bitビルドができない」「装置を2台つないだら識別できない」「客先のPCでドライバーが入らない」という形で跳ね返ってきます。

この記事では、WindowsアプリからUSB機器を扱う4つの方式 ── 仮想COMポート・HID・WinUSB・ベンダー提供SDK ── を、選定基準と実装上の勘所、そして4方式に共通して必要になる設計まで含めて整理します。

1. まず結論

  • 最初に確認するのは「デバイスマネージャーでどこに、何として見えているか」です。ポート(COMとLPT)、ヒューマンインターフェイスデバイス、ユニバーサルシリアルバスデバイス、独自のカテゴリ ── ここで方式はほぼ決まります(2章)。
  • 標準クラスに属する機器はドライバー不要です。Windowsは音声・CDC・HID・マスストレージ・印刷などのクラスドライバーを標準搭載しており、該当する機器は自動で動きます。ベンダーが標準クラス向けにドライバーを書くのは非推奨です。1
  • 公式の選定順序は「単純なものから」です。(1)標準クラスドライバーが使えるなら書かない、(2)使えず単一アプリからのアクセスならWinUSB、(3)複数アプリが同時にアクセスするならUMDFドライバー、(4)それも無理ならKMDFドライバー ── この順で検討します。2
  • 仮想COMは移植と流用が最強、識別が最弱です。CDC-ACM機器はWindows 10以降ならusbser.sysが自動で載り、SerialPortだけで書けます。ただしCOM番号は機器のIDではありません。VID/PID・シリアル番号から実行時に引き当てる実装が必須です(3章)。3
  • HIDはドライバー配布ゼロで双方向通信できる隠れた本命です。ただしマウス・キーボード・タッチ・ペン相当のコレクションはOSが排他で開くため触れません。速度も割り込み転送の帯域に縛られます(4章)。4
  • WinUSBは「バルク転送で速度が要る」「独自プロトコル」向けです。INFなしで自動インストールできるのは、ファームウェアがMicrosoft OS記述子で互換IDWINUSBを報告する機器を、Windows 8以降で使う場合だけ。既存機器やWindows 7以前が対象なら基本的にカスタムINFが要ります(5章)。5
  • ベンダーSDKは「選ぶ」のではなく「引き受ける」ものです。bitness・スレッドモデル・寿命・再頒布条件が全部他人の都合で決まるので、SDKの制約をアプリ設計の前提として最初に洗い出します(6章)。
  • どの方式でも、機器の一意識別・抜き差し追従・タイムアウト・電源管理の4点は自分で設計します。ここを省いたアプリは、必ず「たまに動かない」になります(8章)。
  • カーネルモードドライバーを自作するなら、Windows 10 1607以降はMicrosoftによる署名が必須です。Partner Centerのアカウント開設にEV証明書が要る点も含め、配布コストとして事前に見積もってください(10章)。6

2. 大前提 ── Windowsから見たUSB機器は「どのドライバーが載ったか」がすべて

USBケーブルの先に何がつながっていても、アプリから見えるのはその機器の上に載ったドライバーが公開しているインターフェースだけです。ここを押さえないと議論が噛み合いません。

機器を挿すと、Windowsは機器が申告するディスクリプタを読み、クラスコードとVID/PIDから載せるドライバーを決めます。標準クラスに該当すれば、Windows同梱のクラスドライバーが自動で載ります。1

USB-IFクラスコード Windows標準ドライバー アプリから見える形
Audio (01h) Usbaudio.sys オーディオデバイス
CDC (02h, サブクラス02h) Usbser.sys COMポート
HID (03h) Hidclass.sys / Hidusb.sys HIDコレクション
Image (06h) Usbscan.sys WIAデバイス
Printer (07h) Usbprint.sys プリンター
Mass Storage (08h) Usbstor.sys ドライブ
Video (0Eh) Usbvideo.sys カメラ(UVC)
Vendor Specific (FFh) (なし) WinUSB推奨

最後の行が重要です。ベンダー独自の機器はFFh(Vendor Specific)を名乗ることが多く、その場合Microsoftの推奨はWinUSBです。1

もうひとつ知っておくべきなのが複合デバイス(composite device)です。1本のUSBケーブルの先で複数の機能を持つ機器は、Usbccgp.sysが機能ごとに別々のデバイスとして展開します。「1台の装置なのにデバイスマネージャーに3つ出てくる」のはこれで、たとえば「制御はCDC(COMポート)、ステータス通知はHID」という構成の機器も珍しくありません。方式は機器単位ではなく、機能(インターフェース)単位で決まります。

最初にやること: 現物をデバイスマネージャーで見る

議論の前に、実機を挿して以下を確認してください。5分で終わり、その後の判断が全部変わります。

  1. デバイスマネージャーのどのカテゴリに、何という名前で出るか
  2. プロパティ → 詳細タブ → ハードウェアID (USB\VID_xxxx&PID_yyyy&...)
  3. 同 → 互換ID (USB\Class_02&SubClass_02USB\MS_COMP_WINUSB が見えるか)
  4. 同 → デバイスインスタンスパスの末尾(シリアル番号が入っているか、&を含む生成値か)
  5. ドライバータブ → プロバイダーとドライバーファイル(Microsoft製か、ベンダー製か)

3のところにUSB\MS_COMP_WINUSBがあれば、その機器はWinUSBデバイスとして設計されています。5 4は8.1節で使う、機器の一意識別の可否を判断する材料です。

3. 方式A: 仮想COMポート ── いちばん楽で、いちばん取り違えやすい

3.1 何が起きているか

USBのCDC(Communications and CDC Control)クラス、サブクラス02h(ACM)を名乗る機器には、Windows標準のUsbser.sysINFの配布なしで自動的に載ります。デバイスディスクリプタでクラス02・サブクラス02を設定するだけで、USB\Class_02&SubClass_02という互換IDにより標準のUsbser.infがマッチする仕組みです。3

ただし、この自動ロードはWindows 10以降の挙動です。1 Windows 8.1以前も対象に含めるなら、ディスクリプタだけでは足りず、標準ドライバーを参照するINF(mdmcpq.infを参照するカスタムINFなど)を用意して配布する必要があります。「Windows 10では何もせず動いたのに、客先のWindows 7機では不明なデバイスになる」はここが原因です。

もうひとつの経路が、FTDI・Silicon Labs・ProlificといったUSB-シリアル変換チップのベンダーが提供するVCPドライバーです。こちらはドライバーの導入が必要ですが、チップベンダーが署名済みドライバーをWindows Updateにも載せているため、実務上はほぼ「挿せば入る」状態になります。

いずれの場合も、アプリから見えるのはただのCOMポートです。ここが最大の利点で、RS-232時代の資産・ノウハウ・テスト用ターミナルソフトがそのまま使えます。

3.2 実装はSerialPortだけ、ただし落とし穴も継承する

.NETならSystem.IO.Ports.SerialPortです(.NET 5以降はSystem.IO.Portsパッケージの参照が必要)。実装上の注意点はUSB特有ではなくシリアル通信一般のもので、フレーミング・タイムアウト・再接続・ログ設計まで含めて「シリアル通信アプリの落とし穴」に整理してあります。とくに、Read(buffer, 0, 16)で16バイトちょうど読めるとは限らないという点は、USB経由でも変わりません。バイトストリームとして受け、バッファに溜めてからparserでフレームを切り出す構成にしてください。

3.3 COM番号を設定ファイルに書かない

仮想COM方式で現場を壊す原因の第1位が、これです。

  • COM番号はWindowsがそのPCで割り当てた番号にすぎず、機器の識別子ではありません
  • 挿すUSBポートを変えると番号が変わることがあります
  • 同型機を2台つなぐと、どちらがどちらか番号だけでは分かりません
  • 「COM3が使用中」で欠番が進み、COM13やCOM27になることも普通に起きます

正しい実装は、VID/PID(と可能ならシリアル番号)からCOM番号を実行時に引き当てることです。PnPの列挙から取得できます。

# COMポートを、ハードウェアIDつきで全部出す(まずは絞り込まずに見るのが安全)
Get-CimInstance Win32_PnPEntity |
  Where-Object { $_.PNPClass -eq 'Ports' } |
  Select-Object Name, PNPDeviceID |
  Format-List

# 出力例:
# Name        : USB シリアル デバイス (COM5)          ← CDC-ACM(usbser.sys)
# PNPDeviceID : USB\VID_2341&PID_0043\85436323631351D0E1C1
#                    ^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^
#                    VID/PID            デバイスインスタンスID
#
# Name        : USB Serial Port (COM7)               ← FTDIのVCPドライバー
# PNPDeviceID : FTDIBUS\VID_0403+PID_6001+A5XK3RJTA\0000
#               ^^^^^^^ 列挙子が USB ではなく FTDIBUS

ここでPNPDeviceID -like 'USB\*'のように絞り込むと事故ります。上の例のとおり、FTDIのVCPドライバーが作るCOMポートはFTDIBUS\で始まり、USB\のフィルターには1件も引っかかりません。Silicon Labsなど他のチップベンダーも独自の列挙子を持つことがあります。

安全な実装は次のどちらかです。

  • Portsクラスを全件取得してから、PNPDeviceIDに含まれるVID_xxxx/PID_yyyy(あるいはVID_xxxx+PID_yyyy)を正規表現で拾う ── 列挙子名に依存しないので堅い
  • 列挙子名を明示的に許可リストにする(USB\FTDIBUS\ など) ── 対象機器が固定なら十分

いずれにせよ、自分の対象機器を実際に挿してこのコマンドを流し、どんなPNPDeviceIDで出るかを目視で確認してからフィルターを書いてください。列挙子名は機器とドライバーの組み合わせで決まるので、机上で決め打ちできません。

C#からはSystem.ManagementManagementObjectSearcherで同じクエリを投げるか、Windows.Devices.SerialCommunication.SerialDevice.GetDeviceSelectorFromUsbVidPid(vid, pid)でAQSセレクターを作ってDeviceInformation.FindAllAsyncする方法があります(GetDeviceSelectorのほうはVID/PIDを取らず、引数なしかポート名を渡す形なので取り違えに注意)。前者のほうが依存が少なく、デスクトップアプリでは扱いやすいことが多いです。

Nameの末尾の(COM5)から番号を切り出すのは行儀が悪く見えますが、実務では最も確実に動く手です。厳密にやるならデバイスのレジストリキー配下のPortName値を読みます。

3.4 抜き差しでハンドルが腐る

USB-シリアルはケーブルを抜くとポートそのものが消えます。SerialPortを開いたまま抜くと、内部の受信スレッドから例外が飛んでアプリごと落ちる、という事故が古典的に知られています。PnPの取り外し通知を受けたら、まずポートをCloseするという順序を作っておくのが安全です(8.2節)。

再接続は「Open()をやり直す」では足りません。旧セッションの無効化、処理中リクエストの失敗確定、リーダー/ライターの停止、バックオフ後の再オープン、装置初期化シーケンスの再実行まで含めたセッション再生成として設計してください。

3.5 向き・不向き

向いている 向いていない
既存のシリアルプロトコル資産がある USB-UART変換チップ経由での高スループット
テキストコマンド応答型の装置・計測器 低レイテンシの厳しい制御
現場でターミナルソフトによる切り分けをしたい 同型機の多数同時接続(識別の実装が重くなる)
開発者にドライバーの知識がない プロトコル設計をこちらで決められる新規開発

スループットについては、「仮想COMだから遅い」と一括りにしないでください。FTDIなどのUSB-UART変換チップを挟む構成では、変換先のUARTのボーレートが天井になります(921.6kbpsなら約92KB/s)。一方、マイコンが直接CDC-ACMを実装したネイティブUSB機器では、データインターフェースはUSBのバルク転送なのでUARTの制約がなく、ハイスピード接続なら数MB/s級が出ることもあります。ただしその領域ではUsbser.sysSerialPort層のオーバーヘッドが効いてくるため、必要帯域が数百KB/sを超えるなら実機で実測してから方式を決めるのが正解です。測っていない段階で「速度が要るからWinUSB」と決め打ちすると、不要なドライバー配布を背負い込むことになります。

4. 方式B: HID ── ドライバー配布ゼロで双方向通信できる

4.1 HIDは入力機器のためだけのものではない

HIDというとマウスとキーボードを連想しますが、規格上は任意のバイト列(レポート)を双方向にやり取りできる汎用プロトコルです。バーコードリーダー、カードリーダー、電子錠、計測ユニット、UPS、独自のI/Oボックス ── 「ドライバーを配りたくないが独自データをやり取りしたい」機器がHIDを名乗るのは、WindowsにHidclass.sysHidusb.sysが標準搭載されていて、INFもドライバーも一切配布せずに動くからです。1

Windowsから見たHIDの単位はトップレベルコレクション(TLC)です。1つの物理デバイスが複数のTLCを持つことがあり、その場合はそれぞれ別のデバイスインターフェースとして見えます。4

4.2 触れるHIDと触れないHID

ここが最重要の制約です。Windowsは一部のTLCを排他モードで開きます。他のアプリが全体の入力状態を横取りできないようにするためで、Raw Input Manager(RIM)がそれらのデバイスを排他で開きます。4

Usage Page / Usage 用途 アクセスモード
0x0001 / 0x0001-0x0002 マウス 排他
0x0001 / 0x0004-0x0005 ゲームコントローラー 共有
0x0001 / 0x0006-0x0007 キーボード・キーパッド 排他
0x000C / 0x0001 コンシューマーコントロール 共有
0x000D / 0x0001-0x0002 ペン 排他
0x000D / 0x0004-0x0005 タッチスクリーン・高精度タッチパッド 排他
0x0020 / 各種 センサー 共有
0x008C / 0x0002 バーコードスキャナー 共有(復号データの取得は排他)

つまり、キーボードエミュレーション型のバーコードリーダーからHID APIで直接データを取ろうとしても取れません。それはキーボードとして排他で開かれています。この手の機器を「キー入力ではなくデータとして受け取りたい」場合は、機器側の設定でHIDのベンダー定義TLCモードやCDCモードに切り替えるのが正攻法です。

なお、排他で開かれているデバイスでも、読み書き権限を要求せずにハンドルを開けば HidD_GetXxx系で属性や文字列は取得できます。4 「機器がつながっているかだけ確認したい」用途はこれで足ります。

4.3 実装 ── レポート長を間違えると必ず失敗する

ユーザーモードアプリの手順は決まっています。SetupDi*でHIDコレクションを探し、CreateFileで開き、HidD_*で情報を取り、ReadFile/WriteFileでレポートを読み書きし、HidP_*でレポートを解釈する ── これだけです。7

// HIDデバイスの列挙と、レポート長の取得(P/Invoke宣言は抜粋)
[DllImport("hid.dll")]
static extern void HidD_GetHidGuid(out Guid hidGuid);

[DllImport("hid.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.U1)]
static extern bool HidD_GetAttributes(SafeFileHandle device, ref HIDD_ATTRIBUTES attributes);

[DllImport("hid.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.U1)]
static extern bool HidD_GetPreparsedData(SafeFileHandle device, out IntPtr preparsedData);

[DllImport("hid.dll")]
static extern int HidP_GetCaps(IntPtr preparsedData, out HIDP_CAPS capabilities);

// GetPreparsedData が返したバッファは必ず解放する。忘れるとネイティブ側で漏れる
[DllImport("hid.dll")]
[return: MarshalAs(UnmanagedType.U1)]
static extern bool HidD_FreePreparsedData(IntPtr preparsedData);

[StructLayout(LayoutKind.Sequential)]
struct HIDD_ATTRIBUTES
{
    public int Size;              // sizeof(HIDD_ATTRIBUTES) を必ず設定する
    public ushort VendorID;
    public ushort ProductID;
    public ushort VersionNumber;
}

// HIDP_CAPS は「使うフィールドだけ」の宣言にしてはいけない。
// HidP_GetCaps はネイティブ定義の全長(USHORT×32 = 64バイト)を書き込むため、
// 途中で切った構造体を渡すとその先のスタックを破壊する
[StructLayout(LayoutKind.Sequential)]
struct HIDP_CAPS
{
    public ushort Usage;
    public ushort UsagePage;
    public ushort InputReportByteLength;    // ReadFile に渡すバッファ長
    public ushort OutputReportByteLength;   // WriteFile に渡すバッファ長
    public ushort FeatureReportByteLength;

    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 17)]
    public ushort[] Reserved;               // 予約領域。省略不可

    public ushort NumberLinkCollectionNodes;
    public ushort NumberInputButtonCaps;
    public ushort NumberInputValueCaps;
    public ushort NumberInputDataIndices;
    public ushort NumberOutputButtonCaps;
    public ushort NumberOutputValueCaps;
    public ushort NumberOutputDataIndices;
    public ushort NumberFeatureButtonCaps;
    public ushort NumberFeatureValueCaps;
    public ushort NumberFeatureDataIndices;
}

構造体の宣言でよくやる事故が、「使うフィールドだけ書いて残りをコメントで済ませる」ことです。HidP_GetCapsはネイティブ定義どおりの全長(USHORT×32 = 64バイト)を書き込むので、先頭5フィールド(10バイト)だけの構造体を渡すと、マーシャラーが確保した領域を54バイト分はみ出して書き込みます。運が良ければAccessViolationException、悪ければ他の変数を静かに壊します。P/Invokeの構造体は、使わないフィールドまで含めてネイティブと同じサイズ・同じ並びで宣言してください。8

HidD_GetPreparsedDataが返すバッファはネイティブ側で確保されたものなので、使い終わったら必ずHidD_FreePreparsedDataで解放します。抜き差しのたびに全HIDデバイスを列挙し直すアプリでこれを忘れると、静かにメモリを食い続けます。try/finallyで囲むか、SafeHandle派生クラスでラップして解放漏れを構造的に防いでください。

const int HIDP_STATUS_SUCCESS = 0x00110000;

// 戻り値を必ず見る。列挙した直後に抜かれていれば FALSE が返る
if (!HidD_GetPreparsedData(handle, out IntPtr preparsed))
{
    return null;   // この機器はスキップ。preparsed は無効なので触らない
}

try
{
    if (HidP_GetCaps(preparsed, out HIDP_CAPS caps) != HIDP_STATUS_SUCCESS)
    {
        return null;
    }
    // ここで初めて caps.InputReportByteLength などが有効
}
finally
{
    HidD_FreePreparsedData(preparsed);   // 取得に成功した場合だけ解放する
}

HidD_GetPreparsedDataの戻り値を無視しないでください。列挙してからハンドルを開くまでのあいだに機器が抜かれるとFALSEが返り、preparsedは有効なポインターになりません。それをHidP_GetCapsHidD_FreePreparsedDataに渡したうえで、中身の分からないcapsでレポート長を決めることになります。抜き差しの多い現場ほど踏む競合なので、tryに入るのは取得に成功したときだけにし、HidP_GetCapsの戻り値(HIDP_STATUS_SUCCESSかどうか)も確認します。

実装で最も多い失敗がレポート長の扱いです。

  • ReadFileに渡すバッファはInputReportByteLengthちょうどにします。短いと失敗し、長くても正しく扱われません
  • バッファの先頭1バイトはレポートIDです。機器がレポートIDを使わない設計なら0が入ります。実データはバイト1からです
  • 同様にWriteFileのバッファはOutputReportByteLengthちょうどで、先頭にレポートIDを置きます

「送ったのに機器が反応しない」の9割は、レポートIDの1バイトぶんデータがずれているか、バッファ長が合っていないかです。機器のドキュメントに「コマンドは8バイト」と書いてあるとき、OutputReportByteLengthが9ならレポートIDを含めて9バイトという意味です。

出力レポートの送信経路にはWriteFileのほかにHidD_SetOutputReportもあり、使い分けが公式に決まっています。9

用途 使うもの
出力レポートを継続的に送る WriteFile(こちらが基本)
コレクションの現在状態を設定する HidD_SetOutputReport
機能(Feature)レポートを送る HidD_SetFeature

注意が必要なのは、公式ドキュメントが「一部のデバイスはHidD_SetOutputReportをサポートせず、使うと応答しなくなることがある」と警告している点です。9 つまり「WriteFileが効かないからHidD_SetOutputReportに替える」という切り替えは、無条件に安全な代替ではありません。機器の仕様書とベンダーのサンプルがどちらを使っているかを確認したうえで選び、切り替える場合は実機で応答が止まらないことまで確認してください。

デバイスの列挙だけが目的なら、CreateFiledwDesiredAccessを0にして開いてください。排他で開かれているデバイスも列挙でき、HidD_GetAttributesでVID/PIDを、HidD_GetSerialNumberStringでシリアル番号を取得できます。

C#から生のP/Invokeを書きたくない場合、HidSharpのようなライブラリを使う選択肢もあります。ただしレポート長とレポートIDの扱いは結局理解する必要があるので、最初の1回は上の形で通しておくと後の調査が速くなります。パッケージ化されたアプリならWindows.Devices.HumanInterfaceDevice.HidDeviceも使えますが、マニフェストへのDeviceCapability宣言が必要です。10

4.4 速度の天井

HIDは割り込み転送を使います。USB 2.0のフルスピード(12Mbps)機器では、割り込みエンドポイントの最大パケット長は64バイト、ポーリング間隔は1〜255msの範囲でファームウェアが申告した値になります。ハイスピード(480Mbps)なら最大1024バイト、間隔は125µs単位です。11

つまり、フルスピードのHID機器で1msポーリング・64バイトなら理論値でも64KB/s程度です。ここに収まらない用途 ── 画像、波形、ログの一括吸い上げ ── にHIDを選ぶと、後から取り返しがつきません。逆に、数十バイトのコマンド応答や状態通知なら十分すぎる帯域です。

5. 方式C: WinUSB ── 独自プロトコルを素で叩く

5.1 位置づけ

Winusb.sysはMicrosoft提供の汎用USBドライバーで、これを機能ドライバーとして載せると、ユーザーモードのWinusb.dllが公開する関数からエンドポイントに直接読み書きできます。ドライバーを書かずに独自プロトコルを扱うための仕組みです。2

公式が挙げるWinUSB採用の条件は明快です。2

  • 機器にアクセスするのが単一のアプリであること
  • バルク・割り込み・アイソクロナスのエンドポイントを持つこと(アイソクロナスはWindows 8.1以降)
  • Windows XP SP2以降を対象とすること

逆に、複数のアプリから同時にアクセスする必要がある機器にWinUSBは使えません。そこはUMDFドライバーの領域です。

機能 WinUSB UMDF KMDF
複数アプリの同時アクセス 不可
バルク・割り込み・制御転送
アイソクロナス転送 可(8.1以降) 不可
フィルタードライバーの積み上げ 不可 不可
セレクティブサスペンド

5.2 「INF不要」が成立する条件

WinUSBの説明でいちばん誤解されるのがここです。INFなしで自動的にWinusb.sysが載るのは、機器のファームウェアがMicrosoft OS記述子を持ち、互換IDとしてWINUSBを報告する場合だけです。5

しかも、この自動マッチが効くのはWindows 8以降です。標準搭載のWinusb.infが互換IDUSB\MS_COMP_WINUSBに対応したのがWindows 8で、それ以前はハードウェアIDを指定したカスタムINFが必須でした。5 5.1節のとおりWinUSB自体はWindows XP SP2以降で動きますが、「XPから動く」と「INFなしで入る」は別の話です。Windows 7以前も対象なら、OS記述子を実装していてもINFを配布する前提で計画してください(Windows 7以前でも、更新版Winusb.infがWindows Update経由で入っていればマッチしますが、それを配布計画の前提にはできません)。

具体的には、機器側に次の実装が必要です。バージョン1.0(WCID)と2.0の2系統がある点に注意してください。

  • Microsoft OS 1.0記述子(全バージョン共通)
    1. 文字列インデックス0xEEにOS文字列ディスクリプタを持ち、ベンダーコードを返す
    2. 拡張互換ID OS機能記述子でcompatibleIDWINUSBを設定する(複合デバイスなら機能ごとに)
  • Microsoft OS 2.0記述子(Windows 8.1以降)
    1. BOSディスクリプタのプラットフォーム機能ディスクリプタで記述子セットの所在を通知する。0xEEの文字列ディスクリプタは使いません
    2. その記述子セットの中に互換ID機能記述子を置き、WINUSBを報告する。BOSで「セットがある」と知らせるだけではWinusb.sysは選ばれません。バインドを決めているのは、1.0と同じく互換IDのほうです

    1.0の制約と信頼性の問題を解消するために策定されたもので、新規設計のファームウェアならこちらが第一候補です12

デバイスインターフェースGUIDの登録は、上記とは役割が違います。これはアプリが機器を見つけるためのGUIDであって、Winusb.sysがバインドできるかどうかを決めているのは互換IDのほうです。GUIDは発見(discovery)の側。とはいえ独自GUIDが登録されていなければアプリ側の機器探索が組み立てにくいので、実務上はセットで実装します。

ここで注意すべきなのが、レジストリ上のプロパティ名が単数形と複数形の2種類あることです。13

名前 使われる場面
DeviceInterfaceGUID 文字列(REG_SZ) Microsoft OS 1.0 の拡張プロパティ記述子はこの名前をwPropertyNameLength 40バイトで指定する5
DeviceInterfaceGUIDs 複数文字列(REG_MULTI_SZ) カスタムINFの標準形。Microsoftのサンプルも HKR,,DeviceInterfaceGUIDs,0x10000,"{...}" と複数形を使う13

公式ドキュメントはWinusb.sysの挙動を説明するとき 「レジストリのDeviceInterfaceGUIDsキーを読み、そこに指定されたGUIDでデバイスインターフェースを登録する」 と複数形で書いています。13 手動でレジストリに追加する手順でも、文字列のDeviceInterfaceGUIDか複数文字列のDeviceInterfaceGUIDsのどちらかDevice Parameters配下に置く、と両方が案内されています。13

OS 2.0のレジストリプロパティ機能記述子を使う場合は、プロパティ名とデータ型を仕様書で確認してください(実装では複数形+REG_MULTI_SZが一般的です)。1.0の単数形をそのまま2.0の経路に持ち込むと、Windowsが参照しない場所に書き込んでいて「バインドはできているのにアプリから見つからない」という状態になり得ます。

確実な検証法は、機器を挿してからレジストリを見ることです。HKLM\SYSTEM\CurrentControlSet\Enum\USB\<ハードウェアID>\<インスタンスID>\Device Parametersに、意図した名前・型・値で入っているかを確認すれば、記述子の解釈がどうであれ事実が分かります。

さらに、INFを書く場合はセットアップクラスにUSBDevice({88BAE032-5A81-49f0-BC3D-A4FF138216D6})を使います。USBクラスはホストコントローラーとハブ、複合デバイス専用で、独自機器に使うと信頼性と性能の問題を招くと明記されています。5

既存機器のファームウェアに手が入らない場合は、ハードウェアIDを指定したカスタムINFを自分で用意して配布することになります。この時点で「インストーラーがドライバーをインストールする」構成になり、配布の話(10章)が発生します。独自の.sysを1バイトも書かず、Microsoftのwinusb.sysを参照するだけのINFでも、署名済みカタログを付けなければ実運用のWindowsには入りません。「INFを1枚書けば済む」ではないので、10章を読んでから工数を見積もってください。

開発中にZadigなどのツールでドライバーをWinUSBに差し替えて検証するのは有効な手ですが、これはベンダーのドライバーを外す操作です。本番の配布手段にはしないでください。他のアプリが同じ機器を使えなくなります。

5.3 実装の勘所

// WinUSB の初期化(エラー処理は省略)
HANDLE h = CreateFile(devicePath,
                      GENERIC_READ | GENERIC_WRITE,
                      FILE_SHARE_READ | FILE_SHARE_WRITE,
                      NULL, OPEN_EXISTING,
                      FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, // 非同期は必須級
                      NULL);

WINUSB_INTERFACE_HANDLE usb;
WinUsb_Initialize(h, &usb);

// 読み取りに必ずタイムアウトを設定する(既定は無期限待ち)
ULONG timeoutMs = 1000;
WinUsb_SetPipePolicy(usb, bulkInPipeId, PIPE_TRANSFER_TIMEOUT,
                     sizeof(timeoutMs), &timeoutMs);

// 非同期のときは LengthTransferred に NULL を渡し、完了後に転送長を取る
WinUsb_ReadPipe(usb, bulkInPipeId, buffer, bufferLength, NULL, &overlapped);
// → 戻り値 FALSE / GetLastError() == ERROR_IO_PENDING なら実行中

ULONG transferred = 0;
WinUsb_GetOverlappedResult(usb, &overlapped, &transferred, TRUE); // ここで初めて有効な値

// --- 後始末。抜き差しのたびにここを通るので、漏らすと再接続のたびに蓄積する ---
CancelIoEx(h, NULL);                                  // 処理中のI/Oを止め、
WinUsb_GetOverlappedResult(usb, &overlapped, &transferred, TRUE); // 完了を回収してから
WinUsb_Free(usb);                                     // インターフェースハンドルを解放し
CloseHandle(h);                                       // ファイルハンドルを閉じる

現場で効くポイントは4つです。

  • FILE_FLAG_OVERLAPPEDで開いて非同期で回す。同期I/Oにすると、機器が黙ったときにスレッドごと固まります
  • PIPE_TRANSFER_TIMEOUTを必ず設定する。既定では読み取りが返ってきません
  • 非同期ではLengthTransferredにポインターを渡さない。公式ドキュメントは「Overlappedが非NULLならLengthTransferredはNULLでよい」「非NULLを渡した場合、WinUsb_ReadPipeから戻った時点の値は操作が完了するまで無意味」と明記しています。転送長はWinUsb_GetOverlappedResultで取得します。14 ローカル変数のアドレスを渡すのは、値が無意味なだけでなく、その変数がスコープを抜ける設計だとダングリングポインターになります
  • WinUsb_FreeCloseHandleを必ず対にする。WinUsb_Initializeが成功するたびにインターフェースハンドルが確保されます。8.2節のとおり抜き差しのたびにセッションを作り直す設計にすると、解放を書き忘れたぶんが再接続のたびに積み上がります。成功経路だけでなく、初期化途中で失敗した経路(WinUsb_Initializeは成功したがパイプ設定で失敗した、など)でも必ず通るように、後始末は1箇所にまとめてください。順序は「処理中のI/OをCancelIoExで止める → WinUsb_GetOverlappedResultで完了を回収する → WinUsb_FreeCloseHandle」です。完了を回収する前にハンドルを閉じると、カーネルがまだ触っているバッファを解放してしまいます
  • アプリの多重起動を防ぐ。WinUSBは複数アプリの同時アクセスをサポートしないので、二重起動防止(名前付きミューテックスなど)を仕様に入れておきます

C#から使うなら、libusbのWindowsバックエンドはWinUSBの上に実装されているため、LibUsbDotNetのようなラッパーが選択肢になります。パッケージ化されたアプリではWindows.Devices.Usb.UsbDeviceも使えますが、Audio・HID・Image・Printer・Mass Storage・Smart Card・Audio/Video・Wireless Controllerの各デバイスクラスにはアクセスできないという明示的な制限があります。15

6. 方式D: ベンダー提供SDK・専用ドライバー ── 選ぶのではなく引き受ける

産業用カメラ、計測器、POS周辺機器、指紋・静脈認証、専用I/Oボード ── これらはベンダーがドライバーとSDKをセットで提供し、それ以外の使い方が事実上できません。方式Dは選択肢というより、機器を選んだ時点で決まっている前提条件です。

だからこそ、機器選定の段階でSDKの制約を洗い出すことがそのまま設計になります。確認すべき項目を挙げます。

確認項目 見落としたときに起きること
32bit/64bit両対応か 64bitアプリから32bit専用DLLが呼べず、プロセス分離が必要になる
APIの形態(C DLL / COM / .NET) 呼び出し方とマーシャリング設計が変わる。COMならスレッドモデルの制約が付く
スレッド制約(STA必須、コールバックのスレッド) UIスレッドを塞ぐ、あるいはデッドロックする
再頒布可能物と配布条件 インストーラーに同梱できず、客先で手動インストールが必要になる
同梱ドライバーの署名状態 Windows 11の新しいビルドや装置PCでインストールできない
対応OSと保守期限 OS更改でアプリごと作り直しになる
複数台同時接続の可否と識別方法 2台目をつないだ時点で破綻する
デモアプリのソース有無 仕様不明な挙動の調査コストが跳ね上がる

実装面では、SDKを直接アプリ全体にばらまかないことが最大の防御です。SDK呼び出しを1つの薄い抽象層(インターフェース)の裏に閉じ込め、アプリ本体はその抽象に対して書きます。こうしておくと、機器の型番変更・SDKのメジャーバージョンアップ・ベンダー乗り換えの影響が1箇所で済み、機器なしでの単体テストも書けるようになります。

32bit専用SDKを64bitアプリから使う必要が出た場合は、別プロセスに追い出してプロセス間通信でつなぐのが定石です。COM経由なら「32bitアプリから64bit DLLを呼ぶCOMブリッジ実例」が逆方向の同じ考え方で、ネイティブDLLの呼び出し方そのものは「C#からネイティブDLLを呼ぶ:C++/CLIラッパー vs P/Invoke」に整理してあります。子プロセスの生存管理は「子プロセスの安全な扱い」を参照してください。

7. 4方式の判断表

観点 仮想COM HID WinUSB ベンダーSDK
ドライバー配布 不要(CDC)/ 標準的(VCP) 不要 条件付きで不要、多くはINF必要 必要
実装難易度 中〜高 SDK次第(ピンキリ)
スループット 低〜中(機器依存・要実測)
レイテンシ 中(ポーリング間隔依存)
複数アプリの同時利用 不可(ポート排他) 可(共有TLCなら) 不可 SDK次第
機器の一意識別 実装が必要(COM番号は不可) VID/PID/シリアルで可 デバイスパスで可 SDK次第
現場での切り分けやすさ (ターミナルソフト)
機器ファームへの依存 (OS記述子) 全部
向く用途 コマンド応答型の装置・計測器 状態通知・小さなコマンド 大量データ・独自プロトコル カメラ・計測器・専用機

判断の順序はこうなります。

  1. 機器がすでにCOMポート/HID/標準クラスとして見えているか → 見えているならそれを使う
  2. 見えていないが、ファームウェアに手が入る → HID(小データ)かWinUSB(大データ)かを帯域で決める
  3. ファームに手が入らず、ベンダーSDKがある → SDKを使う。ただし6章のチェックを先にやる
  4. どれも該当せず、複数アプリからの同時アクセスが要る → UMDFドライバーの開発を検討する2

8. どの方式でも自分で設計する4つのこと

方式が決まっても、以下の4つは自分で作る必要があります。「たまに動かない」装置連携アプリは、ほぼ確実にこのどれかが欠けています。

8.1 機器の一意識別 ── 番号ではなくIDで掴む

設定に書いてよいのはVID/PID + シリアル番号、あるいはデバイスインターフェースパスです。COM番号やデバイスマネージャー上の並び順は識別子ではありません。

シリアル番号の有無はデバイスインスタンスパスの最後の要素で分かります。

USB\VID_2341&PID_0043\85436323631351D0E1C1   ← 機器がシリアル番号を報告している(移動しても不変)
USB\VID_0403&PID_6001\5&1a2b3c4d&0&2         ← 報告していない(Windowsが接続位置から生成した値)

複合デバイスでは、VID/PID + シリアル番号だけでは足りません。2章のとおり1台の機器が複数の機能を持つことがあり、その場合は各機能が同じVID・PID・シリアル番号を共有します。「制御用と保守用でCDCが2本」「HIDのトップレベルコレクションが2つ」という機器では、この3点セットが同じ値になる相手が複数見つかり、どちらを開くかは運任せになります。機能を区別する要素まで含めて鍵にしてください。

USB\VID_1234&PID_5678&MI_00\7&2a3b4c5d&0&0000   ← 機能0(たとえば制御用CDC)
USB\VID_1234&PID_5678&MI_02\7&2a3b4c5d&0&0002   ← 機能2(たとえば保守用CDC)
       同じVID/PID・同じ親デバイス ↑ MI_xx(USBインターフェース番号)だけが違う

実務での鍵の作り方は次のいずれかです。

  • USBインターフェース番号(MI_xx)を含める ── 複合デバイスのCDC/WinUSB機能を区別する標準的な方法
  • HIDならUsage Page + Usageを併用する ── 同一デバイスの複数トップレベルコレクションはこれで判別します(HidP_GetCapsUsagePage/Usage)
  • デバイスインターフェースパスをそのまま保存する ── 列挙で得られる文字列は機能単位で一意なので、これを鍵にするのが最も確実です

自社で機器仕様を決められる立場なら、機能ごとに異なるインターフェース番号を割り当て、シリアル番号を必ず報告するファームウェア仕様にしておくと、ソフト側の識別ロジックが劇的に単純になります。

後者は&を含み、挿すポートが変わると値も変わります

ただしこの判定を複合デバイスの子ノードに当てはめてはいけません。複合デバイスでは、CDC機能やHIDコレクションとして列挙される子PDOのインスタンスID末尾はUsbccgp.sysが生成した値になり、物理デバイスがシリアル番号を報告していても&を含みます。子のパスだけを見て「シリアル番号なし」と判定すると誤りです。シリアル番号は親のUSBデバイスノード側にあるので、CM_Get_Parent(またはDEVPKEY_Device_Parent)で親までたどってから、そのインスタンスIDを見てください。

USB\VID_1234&PID_5678\SN0001234           ← 親(物理デバイス)。ここにシリアル番号がある
 └ USB\VID_1234&PID_5678&MI_00\7&2a3b…    ← 子。Usbccgpの生成値なので & を含む(判定に使わない)

同型機を複数台つなぐ運用でシリアル番号のない機器を選んでしまうと、識別手段が「どのUSBポートに挿すか」しか残りません。その場合はハブのポートを固定し、運用手順としてラベルを貼るところまで含めて設計します ── これは技術で解決できない部分なので、機器選定の段階で気づく必要があります。

8.2 抜き差しへの追従 ── ポーリングしない

Timerで1秒ごとに列挙し直す実装をよく見かけますが、Windowsには通知の仕組みがあります。

  • Windows 8以降: CM_Register_NotificationCM_NOTIFY_FILTER_TYPE_DEVICEINTERFACE(到着・削除の検知)とCM_NOTIFY_FILTER_TYPE_DEVICEHANDLE(開いているハンドルの機器が消えたことの検知)を登録します16
  • Windows 7以前も対象: RegisterDeviceNotificationDBT_DEVTYP_DEVICEINTERFACEを登録し、WM_DEVICECHANGEを処理します17

実装で外せない注意点が2つあります。16

  • CM_Register_Notificationは「登録時点で既に存在するインターフェース」を通知しません。先に登録し、その後でCM_Get_Device_Interface_Listにより既存分を列挙します。順序を逆にすると、その隙間に挿された機器を取りこぼします
  • その代わり、重複が出ることを前提に組みます。登録後・列挙前に有効化されたインターフェースは、到着通知と一覧の両方に現れます。両方をそのまま到着処理へ流すと、同じ機器のセッションを二重に生成し、2回目の排他オープンが失敗したり、確立済みの状態を上書きしたりします。デバイスインターフェースパスをキーにした集合を持ち、既知のパスは無視するという重複排除を必ず入れてください(この集合は取り外し時に消します)
  • コールバック内でブロックし得る処理をしない。I/Oを伴う処理は別スレッドに投げます。ここで待つとPnPイベントの処理全体が詰まります

パッケージ化されたアプリやWinRT APIを使える構成なら、DeviceWatcherが同じことを簡潔に書けます。

// 特定のVID/PIDのシリアルデバイスを監視する(WinRT)
string selector = SerialDevice.GetDeviceSelectorFromUsbVidPid(0x2341, 0x0043);
DeviceWatcher watcher = DeviceInformation.CreateWatcher(selector);

watcher.Added   += (s, info) => OnDeviceArrived(info.Id);
watcher.Removed += (s, info) => OnDeviceRemoved(info.Id);
watcher.Start();

そして重要なのは、PnP通知を「唯一の入口」にしないことです。ケーブルが抜かれたとき、処理中だったI/Oは通知より先に、あるいは同時に、削除・キャンセル系のエラーで完了し得ます。「通知を受けたら真っ先にハンドルを閉じる」という順序は通知が先に来たときにしか成り立たないので、それだけに頼った実装は3.4節の「抜いたら落ちる」を残したままになります。

正しい形は、セッション終了の入口を2つ持つことです。

  • すべての読み書きの完了パスで、削除系のエラー(ERROR_DEVICE_NOT_CONNECTED / ERROR_DEVICE_REMOVED / ERROR_GEN_FAILURE、.NETなら該当するIOException)を「機器が消えた」として扱い、そこからセッション破棄に入る
  • PnP通知は補助の信号として扱う。I/Oが動いていない待機中に抜かれたケースを拾うために必要ですが、これ単独では足りません

ここで必ず区別しなければならないのが「自分でキャンセルした」ケースです。応答タイムアウトでの打ち切り、アプリ終了時の後始末、ユーザー操作による中断 ── これらでCancelIoExCancellationTokenDisposeを使うと、正常動作なのにERROR_OPERATION_ABORTEDOperationCanceledExceptionObjectDisposedExceptionが出ます。これらを無条件に「機器が消えた」と判定すると、機器はつながったままなのにタイムアウトのたびにセッションを捨てて再接続するという、性質の悪いループができあがります。

// 「自分で止めたのか、機器が消えたのか」を状態で判別する
catch (OperationCanceledException) when (_shutdown.IsCancellationRequested)
{
    // 自己キャンセル。セッションは壊れていないので破棄しない
}
catch (Exception ex) when (IsDeviceGone(ex))
{
    TearDownSession();   // 冪等。PnP通知から呼ばれても二重に走らない
}

判定の基準は例外の型やエラーコードだけでは足りません。「いま自分がキャンセルを要求している最中か」という状態(キャンセルトークン、シャットダウンフラグ)と突き合わせて初めて切り分けられます。逆に言うと、自己キャンセルの経路を持つなら、その事実をI/O層から見える場所に置いておく必要があります。

どちらの経路から入っても同じ後始末になるよう、セッション破棄は冪等な1つの処理にまとめ、二重呼び出しで壊れないようにしておきます(Interlocked.Exchangeでフラグを立てて先着1回だけ実行する、など)。腐ったハンドルに対するI/Oが投げる例外は、しばしばキャッチしにくい場所から飛んできます ── だからこそ、例外の発生源側で握って状態遷移につなげる設計が要ります。

8.3 タイムアウトと再接続 ── 「1個のタイムアウト」では足りない

USB機器のI/Oは、抜けた・電源が落ちた・ファームがハングした、のいずれでも「返ってこない」という同じ症状になります。タイムアウトは意味ごとに分けて持ちます。

タイムアウト 対象 目安
オープンタイムアウト 機器を開くまで 秒オーダー
応答タイムアウト コマンド発行から応答完了まで 機器仕様の最悪値 × 安全率
バイト間タイムアウト フレーム途中で続きが来ない 通信速度から算出
再接続バックオフ 再オープンの待機間隔 指数バックオフ + 上限

そしてタイムアウトは「遅いときの保険」ではなく「状態遷移を進めるルール」として扱ってください。タイムアウトしたときにどの状態に移るのか、処理中のリクエストをどう失敗させるのか、UIに何を出すのかまで決めて初めて設計になります。UIへの出し方は「外部機器の状態の確認と表示のベストプラクティス」で扱っています ── 「接続中」の一言で済ませないでください。

8.4 電源管理 ── 「抜いてないのに反応が遅い」の正体

USBのセレクティブサスペンドは、アイドル状態の機器を低消費電力状態に入れる仕組みです。復帰に時間がかかるため、「最初の1回だけ応答が遅い」「しばらく放置すると1回目のコマンドを取りこぼす」という症状の犯人になります。

  • Usbser.sys(仮想COM)では既定で無効で、レジストリのIdleUsbSelectiveSuspendPolicyで有効化・設定します3
  • WinUSBでは、拡張プロパティOS機能記述子(またはINF)のDeviceIdleEnabledDefaultIdleTimeoutUserSetDeviceIdleEnabledなどで制御します5

現場でまず確認するのは、デバイスマネージャーの当該デバイス(およびUSBルートハブ)のプロパティにある「電力の節約のために、コンピューターでこのデバイスの電源をオフにできるようにする」チェックボックスです。装置PCでは、ここを外すだけで直る不具合が実際にあります。ノートPCの省電力設定込みで、検証は本番と同じ電源プランで行ってください。

9. 性能とレイテンシの見積もり

方式選定の段階で、必要な帯域とレイテンシを数字にしておくと後戻りがなくなります。

転送タイプ 使う方式 特徴
制御転送 全方式(内部で使用) 設定・小さなコマンド向け。帯域保証なし
割り込み転送 HID、WinUSB 定期ポーリング。低レイテンシだが小容量
バルク転送 WinUSB、マスストレージ 大容量向け。帯域の保証はなく、空きを使う
アイソクロナス転送 UVC(カメラ)、UAC(音声)、WinUSB(8.1以降) 帯域保証あり、再送なし

USB 2.0の割り込みエンドポイントは、フルスピードで最大64バイト/パケット・1〜255msのポーリング間隔、ハイスピードで最大1024バイト・125µs単位の間隔です。11 HIDを選ぶなら、この上限に対して必要帯域が1桁以上余っているかを確認してください。

もうひとつ、Windowsは汎用OSなのでレイテンシに保証がありません。「10ms周期で必ず応答する」といった要件をアプリ層で満たすのは無理があります。周期制御が本質的に必要なら、機器側のマイコンに閉じ込めてPCは指令と監視に徹する設計にしてください。この線引きは「普通のWindowsでソフトリアルタイムをできるだけ実現するための実践ガイド」で詳しく扱っています。

10. 配布と運用 ── ドライバーを配る瞬間にコストが変わる

「ドライバー不要」の方式(標準クラス・HID・WinUSBデバイス)と、「ドライバーを配る」方式のあいだには、開発コストではなく配布・保守コストの断層があります。

  • 「自分で.sysを書かないから署名は不要」は誤りです。PnPのデバイスインストールでは、ドライバーパッケージのカタログファイルに署名がなければDriver Storeにステージされません。18 これはパッケージの中身によらない要件なので、5.2節のようにMicrosoftのwinusb.sysを参照するだけのINFでも、カタログ(.cat)を生成して署名する工程が必要です。「INFを1枚書いて配れば動く」と見積もると、客先で「このデバイスのドライバーは署名されていません」と拒否されてから気づくことになります。カタログの署名は、WHQLリリース署名か、サードパーティのリリース証明書(SPC)による署名です。18
  • カーネルモードドライバーの署名。Windows 10 バージョン1607以降、新規のカーネルモードドライバーはDev Portal(Partner Center)経由でMicrosoftに署名してもらわないとロードされません。Partner Centerのアカウント開設にはEV コード署名証明書が必要です。6 上のカタログ署名とは別レイヤーの要件で、カーネルモードのバイナリを含むなら両方満たす必要があります。
  • 署名の経路は2つあり、適用範囲が違います。HLKテストに通したHLK tested / dashboard signedは、Windows VistaからWindows Serverまで含めて有効で、Microsoftはこちらを推奨経路としています。もう一方のattestation signingはHLKテストが不要な代わりに、Windows 10デスクトップ以降でしか有効になりません(Windows 7/8.1やWindows Server 2016以降では受け付けられない)。加えて、一般ユーザー向けにWindows Updateで配信することはできず、Windows認定(Windows Certified)にもなりません。Microsoftのドキュメントも位置づけを「テスト目的」としています。19 自社インストーラーで配る自作ドライバーにattestation signingを使う運用は現に広く行われていますが、対象OSがWindows 10/11デスクトップに限られることを、対応OS表に落とし込んでから採用してください。装置PCがWindows Serverや古いLTSCなら、この経路は最初から選べません。
  • 例外条件は当てにしない。セキュアブートが無効、または2015年7月29日より前に発行された証明書で署名されている等の場合はクロス署名ドライバーも動きますが、これを前提にした配布計画は数年で破綻します。6
  • インストーラーの設計。ドライバーを含むインストーラーは管理者権限が要り、サイレントインストールの検証も必要になります。配布方式そのものの選び方は「Windowsアプリ配布方式の選び方」に、管理者権限が要る条件の見分け方は「Windowsの管理者特権が必要になるのはいつなのか」にまとめています。
  • 装置PCではドライバーとOS更新が競合します。LTSC構成の装置PCにベンダードライバーを入れる場合、OSのビルド固定とドライバーの更新方針をセットで決めてください。「産業用PCにはどのWindowsを入れるべきか」が参考になります。

設計判断としては、「ドライバーを配らずに済む方式があるなら、多少実装が面倒でもそちらを選ぶ」がほぼ常に正解です。HIDが地味に強いのはこの一点に尽きます。

11. よくある失敗と対処

症状 ありがちな原因 対処
開発機で動くが客先で動かない COM番号を設定にハードコードしている VID/PID・シリアルから実行時に解決する(8.1)
2台目をつないだら誤動作 機器がシリアル番号を報告していない 機器選定を見直す。無理ならポート固定+ラベル運用
1台の機器なのに掴む相手が毎回変わる 複合デバイスで、機能を区別せず鍵にしている MI_xxやHIDのUsage、デバイスインターフェースパスまで含めて鍵にする(8.1)
ケーブルを抜くとアプリが落ちる 腐ったハンドルへのI/O、PnP通知だけに頼った後始末 I/O完了パスでも削除系エラーをセッション終了として扱う(8.2)
最初の1回だけ応答が遅い/取りこぼす セレクティブサスペンドからの復帰 電源管理の設定を確認・無効化(8.4)
HIDで送信しても機器が無反応 レポートIDのぶんデータがずれている バッファ長はOutputReportByteLengthちょうど、先頭はレポートID(4.3)
HIDでデータが1件も読めない 対象がOSに排他で開かれるTLC(キーボード等) 機器のモードを切り替える。列挙だけならアクセス権0で開く(4.2)
WinUSBのReadが返ってこない PIPE_TRANSFER_TIMEOUT未設定 パイプポリシーでタイムアウトを設定(5.3)
タイムアウトのたびに再接続してしまう 自己キャンセルを機器切断と誤判定 キャンセルトークン等の状態と突き合わせて切り分ける(8.2)
抜き差しを繰り返すと徐々に重くなる WinUsb_Free/CloseHandleの漏れ 後始末を1箇所にまとめ、失敗経路でも必ず通す(5.3)
P/Invoke呼び出しの後で無関係な変数が壊れる 構造体を途中まで切って宣言している ネイティブと同じサイズ・並びで全フィールド宣言(4.3)
客先でドライバーが入らない カタログ未署名(自作.sysの有無は無関係) .catの生成と署名を配布計画に含める(10章)
アプリを2つ起動すると片方が失敗 WinUSBは同時アクセス不可 二重起動防止、または常駐サービス経由に集約する
64bitビルドでSDKが読めない 32bit専用DLL 別プロセスに分離してIPCでつなぐ(6章)
通信内容が本当に届いているか分からない 観察手段がない USBプロトコルアナライザー、usbmon相当のトレース、通信ログの実装

最後の行は軽視されがちですが重要です。「どちらが悪いか(アプリか機器か)」を切り分けられる手段を最初から持っておくと、原因不明の期間が劇的に短くなります。仮想COM方式が現場で強いのは、ターミナルソフトという万人が使える切り分けツールがあるからです。HIDやWinUSBを選ぶなら、その代わりになるログとテスト用CLIを自分で作っておいてください。

12. まとめ

  • USB機器の扱い方は、機器そのものではなくその上にどのドライバーが載ったかで決まります。デバイスマネージャーでハードウェアID・互換ID・デバイスインスタンスパスを見るところから始めます。
  • 公式の選定順序は「単純なものから」です。標準クラスドライバー → WinUSB(単一アプリ) → UMDF(複数アプリ) → KMDF。自作ドライバーは最後の手段です。
  • 仮想COMは実装と現場切り分けが楽ですが、COM番号は識別子ではありません。VID/PID・シリアル番号から実行時に解決してください。スループットはUSB-UART変換かネイティブCDCかで桁が変わるので、決め打ちせず実測します。
  • HIDはドライバー配布ゼロで双方向通信できる強い選択肢ですが、マウス・キーボード・タッチ・ペン相当のコレクションはOSが排他で開くため触れず、割り込み転送の帯域が上限になります。
  • WinUSBは大量データと独自プロトコル向け。ただしINF不要が成立するのは、OS記述子を持つ機器をWindows 8以降で使う場合だけで、複数アプリの同時アクセスはできません。新規ファームならOS 2.0記述子が第一候補です。
  • ベンダーSDKは選択肢ではなく前提条件です。bitness・スレッド制約・再頒布条件・保守期限を機器選定の段階で洗い出し、アプリ側は薄い抽象層でSDKを包みます。
  • 方式が何であれ、一意識別・抜き差し追従・多層のタイムアウト・電源管理の4点は自分で設計します。ここが「たまに動かない」の発生源です。複合デバイスでは機能単位まで識別を降ろし、切断はPnP通知とI/Oエラーの両方から拾います。
  • ドライバーパッケージを配るなら、自作の.sysがなくてもカタログの署名が必要です。カーネルモードのバイナリを含むなら、さらに1607以降のMicrosoft署名(とEV証明書)が要ります。attestation signingはHLK不要な代わりにWindows 10デスクトップ以降限定なので、対応OS表と突き合わせてから選びます。ドライバーを配らずに済む方式があるなら、それを選ぶのが実務上ほぼ常に正解です。

関連記事

関連する相談領域

合同会社小村ソフトでは、USB接続の装置・計測器とWindowsアプリの連携設計、既存SDKのラッピングと64bit化、抜き差しや再接続で不安定になる装置連携アプリの原因調査と改修を扱っています。

参考リンク

  1. Microsoft Learn, USB device class drivers included in Windows. Windowsが標準で同梱するUSBクラスドライバーの一覧(Usbaudio.sys / Usbser.sys / Hidclass.sys・Hidusb.sys / Usbscan.sys / Usbprint.sys / Usbstor.sys / Usbvideo.sys など)、サポート対象のデバイスクラスにはベンダーがドライバーを書くべきでないこと、Vendor Specific(FFh)を含む未分類のクラスではWinUSB(Winusb.sys)が推奨されること、複合デバイスではUsbccgp.sysが機能ごとにPDOを生成すること、セットアップクラスUSBDevice({88BAE032-5A81-49f0-BC3D-A4FF138216D6})とUSBクラスの使い分けについて。CDC(02h)の行にある「Windows 10では、Usbser.infがUsbser.sysを機能ドライバーとして自動的にロードする」という記述、およびサブクラス02h(ACM)をmdmcpq.infを参照するカスタムINFで扱う経路についても同ページによる。  2 3 4 5

  2. Microsoft Learn, Choose a driver model for developing a USB client driver. 「最も単純な方法から始める」という選定順序(標準クラスドライバー → WinUSB → UMDF → KMDF)、WinUSBが適するのは単一アプリからのアクセス・バルク/割り込み/アイソクロナスエンドポイント・Windows XP SP2以降を対象とする場合であること、複数アプリの同時アクセスにはWinUSBが使えないこと、WinUSB / UMDF / KMDFの機能比較表(アイソクロナス転送はWindows 8.1以降でWinUSBがサポート、UMDFは非対応)について。  2 3 4

  3. Microsoft Learn, USB serial driver (Usbser.sys). デバイスディスクリプタでクラス02・サブクラス02を設定すると互換ID(USB\Class_02&SubClass_02)により標準のUsbser.infがマッチし、独自INFの配布なしにUsbser.sysが自動でロードされること、サブクラスが02以外だと自動ロードされないこと、Windows.Devices.SerialCommunication名前空間からCDCデバイスと通信できること、セレクティブサスペンドが既定で無効でありレジストリのIdleUsbSelectiveSuspendPolicyで設定できることについて。  2 3

  4. Microsoft Learn, HID Architecture. HIDクラスドライバー(hidclass.sys)がHIDクライアントとトランスポートの間を抽象化すること、Windowsがサポートするトップレベルコレクションの一覧とアクセスモード(マウス・キーボード・ペン・タッチスクリーン・高精度タッチパッドは排他、ゲームコントローラー・センサー・バーコードスキャナーなどは共有)、セキュリティ上の理由からRaw Input Manager(RIM)がそれらのデバイスを排他で開くこと、排他で開かれていても読み書き権限を要求せずにハンドルを開けばHidD_GetXxxで情報取得が可能であることについて。  2 3 4

  5. Microsoft Learn, WinUSB Device. WinUSBデバイスとはファームウェアがMicrosoft OS機能記述子で互換IDとしてWINUSBを報告するUSBデバイスであり、カスタムINFなしにWinusb.sysがロードされること、Windows 8より前は互換IDによる自動マッチが存在せずカスタムINFが必須だったこと(Windows 8で標準搭載のWinusb.infがUSB\MS_COMP_WINUSBに対応し、それ以前のバージョン向けには更新版INFがWindows Update経由で提供されること)、文字列インデックス0xEEのOS文字列ディスクリプタとベンダーコードの仕組み、拡張互換ID記述子でcompatibleIDにWINUSBを設定すること、拡張プロパティ記述子でDeviceInterfaceGUIDを登録するとアプリが機器を発見・操作できるようになること、セットアップクラスにUSBDevice({88BAE032-5A81-49f0-BC3D-A4FF138216D6})を使いUSBクラスは使わないこと、DeviceIdleEnabled / DefaultIdleTimeout / UserSetDeviceIdleEnabled / SystemWakeEnabled による電源管理設定について。  2 3 4 5 6 7

  6. Microsoft Learn, Driver Signing Policy. Windows 10 バージョン1607以降、Dev Portalで署名されていない新規のカーネルモードドライバーはロードされないこと、Windows Hardware Dev Centerプログラムへの登録にEVコード署名証明書が必要であること、クロス署名ドライバーが許容される例外条件(1607へのアップグレード、セキュアブート無効、2015年7月29日より前に発行された終端証明書)について。  2 3

  7. Microsoft Learn, Opening HID collections. ユーザーモードアプリがSetupDi*関数でHIDコレクションを特定し、CreateFileで開き、HidD_Xxxでプリパーズドデータと情報を取得し、ReadFileで入力レポートを読みWriteFileで出力レポートを送り、HidP_Xxxでレポートを解釈するという一連の手順について。 

  8. Microsoft Learn, HIDP_CAPS structure (hidpi.h). 構造体の完全な定義(Usage / UsagePage / InputReportByteLength / OutputReportByteLength / FeatureReportByteLength / Reserved[17] / NumberLinkCollectionNodes 以下10個のNumber系メンバー、合計USHORT×32)、および各レポート長がレポートIDの1バイトを含む値であることについて。 

  9. Microsoft Learn, Sending HID Reports. ユーザーモードアプリが出力レポートを継続的に送るにはWriteFileを使うこと、HidD_SetXxx系ルーチン(HidD_SetOutputReport / HidD_SetFeature)でも出力レポートや機能レポートを送れるが、HidD_SetXxxはコレクションの現在状態を設定する用途に限って使うべきであること、「一部のデバイスはHidD_SetOutputReportをサポートせず、このルーチンを使うと応答しなくなることがある」と警告されていることについて。あわせてHidD_SetOutputReport functionの、ReportBufferLengthがHIDP_CAPSのOutputReportByteLengthで決まること、レポートIDを使わない場合は先頭バイトを0にすることについて。  2

  10. Microsoft Learn, HidDevice Class (Windows.Devices.HumanInterfaceDevice). HidDeviceがトップレベルコレクションと対応するデバイスを表すこと、GetDeviceSelectorでusagePage / usageId / vendorId / productIdからAQSセレクターを作りFromIdAsyncで開く流れ、このクラスでHIDデバイスにアクセスするアプリはマニフェストのCapabilitiesノードに固有のDeviceCapabilityデータを含める必要があることについて。 

  11. USB Implementers Forum, Universal Serial Bus Specification Revision 2.0. 割り込みエンドポイントの最大パケット長がフルスピードで64バイト・ハイスピードで1024バイトであること、ポーリング間隔(bInterval)がフルスピードでは1〜255ミリ秒、ハイスピードでは125マイクロ秒を単位とする2^(bInterval-1)で表されること(セクション9.6.6 Endpoint)、および制御・バルク・割り込み・アイソクロナスの各転送タイプの帯域特性について。  2

  12. Microsoft Learn, Microsoft OS 2.0 Descriptors Specification. Microsoft OS記述子のバージョン2.0がバージョン1.0の制約と信頼性の問題を解消するために策定されたものであること、対象OSがWindows 10およびWindows 8.1 Previewであることについて。 

  13. Microsoft Learn, WinUSB (Winusb.sys) Installation for Developers. デバイスのDevice Parametersキー配下に文字列エントリDeviceInterfaceGUIDまたは複数文字列エントリDeviceInterfaceGUIDsを追加してGUIDを設定すること、カスタムINFのAddRegではHKR,,DeviceInterfaceGUIDs,0x10000,"{...}"(0x10000 = REG_MULTI_SZ)と記述すること、「Winusb.sysが機能ドライバーとしてロードされるとレジストリ値DeviceInterfaceGUIDsキーを読み、指定されたGUIDでデバイスインターフェースを表す」「Winusb.sysはロードのたびにDeviceInterfaceGUIDsキー配下に指定されたデバイスインターフェースクラスでデバイスインターフェースを登録する」こと、ユーザーモード側はSetupDiGetClassDevsで登録済みインターフェースを列挙してからWinUsb_Initializeに渡すこと、およびドライバーパッケージには署名済みカタログファイルが必要であることについて。  2 3 4

  14. Microsoft Learn, WinUsb_ReadPipe function (winusb.h). Overlappedを指定すると関数が即座に戻り操作が非同期に実行されること、その場合GetLastErrorがERROR_IO_PENDINGを返しWinUsb_GetOverlappedResultで成否を確認すること、非同期(Overlappedが非NULL)ではLengthTransferredにNULLを設定してよいこと、LengthTransferredに非NULLを渡した場合でも関数から戻った時点の値はオーバーラップ操作が完了するまで無意味(meaningless)であり、実際の読み取りバイト数はWinUsb_GetOverlappedResultで取得する必要があること、同期呼び出し(OverlappedがNULL)ではLengthTransferredを非NULLにしなければならないことについて。 

  15. Microsoft Learn, Windows.Devices.Usb Namespace. この名前空間が対象とするのは標準搭載のwinusb.sysが扱うWinUSBデバイス(互換ID USB\MS_COMP_WINUSB)であること、Audio(0x01) / HID(0x03) / Image(0x06) / Printer(0x07) / Mass Storage(0x08) / Smart Card(0x0B) / Audio/Video(0x10) / Wireless Controller(0xE0)の各デバイスクラスにはアクセスできないこと、マニフェストへのusbデバイス機能の宣言が必要でWindows 10 バージョン1809以降はVendorId/ProductIdの指定が不要になったこと、上位/下位フィルタードライバーを含むデバイススタックは一般にアクセスできないことについて。 

  16. Microsoft Learn, CM_Register_Notification function (cfgmgr32.h). Windows 8以降で利用可能でありWindows 7以前を対象とする場合はRegisterDeviceNotificationを使うこと、CM_NOTIFY_FILTER_TYPE_DEVICEINTERFACE / DEVICEHANDLE / DEVICEINSTANCEの各フィルタータイプ、PnPイベントはできるだけ速く処理しI/Oなどブロックし得る処理は別スレッドで非同期に行うべきこと、本関数は既存のデバイスインターフェースを通知しないため登録後にCM_Get_Device_Interface_Listを呼ぶ必要があり、その間に有効化されたインターフェースは通知と一覧の双方に現れることについて。  2

  17. Microsoft Learn, RegisterDeviceNotificationW function (winuser.h). アプリケーションがデバイス通知を受け取るための登録関数であること、成功時にデバイス通知ハンドルを返し失敗時はNULLを返すことについて。あわせてWM_DEVICECHANGE messageおよびDBT_DEVICEARRIVAL eventの、デバイスやメディアが挿入されて利用可能になったときにwParamをDBT_DEVICEARRIVALとしてWM_DEVICECHANGEがブロードキャストされることについて。 

  18. Microsoft Learn, PnP Device Installation Signing Requirements. ドライバーパッケージをDriver Storeにステージするには署名要件を満たす必要があること、PnPのデバイスインストールで「署名済み」と見なされるにはドライバーパッケージのカタログファイルがWHQLまたはサードパーティのリリース証明書(SPC・商用リリース証明書)で署名されている必要があること、カーネルモードドライバーのバイナリをロードするための署名要件はこれとは別に課されること、64bit版Windowsではカーネルモードコード署名ポリシーによりWHQLまたはSPCによる署名が要求されること、Windows 10 in S modeなど一部のエディションではWHQL署名のカタログしか受け付けないことについて。  2

  19. Microsoft Learn, Driver Signing Options. HLKテストに合格したdashboard署名ドライバーがWindows VistaおよびWindows Serverエディションを含む以降のOSで動作し、全OSバージョン向けに署名できるため推奨される方法であること、attestation signingが「テスト目的(for testing purposes only)」と位置づけられHLKテストを必要としないこと、attestation署名ドライバーは一般ユーザー向けにWindows Updateへ公開できないこと、Windows 10デスクトップ以降でのみ有効であること、それ以前のWindowsを対象とする場合はHLK/HCKテストログの提出が必要であること、Windows Server 2016以降がattestation署名の提出を受け付けずHLK合格ドライバーのみをロードすること、attestation署名はEV証明書を必要とし、署名を受けてもWindows Certifiedにはならないことについて。 

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

よくある質問

この記事のテーマについて、相談時によくある質問をまとめています。

USB機器をアプリから使いたいのですが、ドライバーは自分で書く必要がありますか?
ほとんどの場合、必要ありません。Microsoftの公式ガイドラインも「最も単純な方法から始めて、必要になったときだけ複雑な方法へ進む」と明示しています。機器がUSBの標準クラス(CDC・HID・マスストレージなど)に属していればWindows標準のクラスドライバーが自動で載るのでドライバーは不要です。標準クラスに属さず、かつ1つのアプリからだけアクセスするならWinUSB(winusb.sys)をそのまま機能ドライバーとして使えます。複数のアプリが同時にアクセスする必要が出て初めてUMDFドライバー、それも無理ならKMDFドライバー、という順序になります。自社でドライバーを書くのは最後の手段です。
仮想COMポート(USBシリアル)方式のいちばんの弱点は何ですか?
COMポート番号が機器のIDではないことです。同じ機器でも挿すUSBポートを変えればCOM番号は変わり得ますし、複数台つなげばどれがどれか番号だけでは判別できません。設定ファイルに「COM3」と書く運用は、現場で必ず壊れます。実務では、Win32_PnPEntityなどからVID/PID・シリアル番号とCOM番号の対応を実行時に引き当てて開くのが正解です。加えて、シリアル通信はバイトストリームなのでメッセージ境界が保証されず、受信バッファに蓄積してからフレームを切り出すparserが別途必要になります。
HID方式はドライバー不要で手軽と聞きますが、何に注意すべきですか?
3点あります。1つめは速度で、HIDは割り込み転送を使うため、大量データの連続転送には向きません。2つめはレポート長で、ReadFileに渡すバッファはHidP_GetCapsが返すInputReportByteLengthちょうどである必要があり、先頭1バイトはレポートIDです。ここを間違えると読めない・落ちるという定番の不具合になります。3つめは排他制御で、マウス・キーボード・タッチスクリーン・ペンに相当するトップレベルコレクションはWindowsのRaw Input Managerが排他で開くため、アプリからは読み書きできません。ただし読み書き権限を要求せずにハンドルを開けば、HidD_GetXxx系での情報取得は可能です。
WinUSBを使いたいのですが、既存の機器にそのまま適用できますか?
できないことが多いです。INFファイルなしでwinusb.sysが自動で載るのは、機器のファームウェアがMicrosoft OS記述子を持ち、互換IDとしてWINUSBを報告する「WinUSBデバイス」を、Windows 8以降で使う場合だけです。標準搭載のWinusb.infが互換IDに対応したのがWindows 8なので、Windows 7以前も対象ならINFの配布が前提になります。既存機器がWinUSBデバイスに該当しない場合も、ハードウェアIDを指定したカスタムINFを自分で用意して配布・インストールする必要があります。開発中にZadigのようなツールでドライバーを差し替えるのは検証としては有効ですが、ベンダーのドライバーを外す行為なので本番配布の手段にはしないでください。ファームウェアに手が入るなら、OS記述子を追加してもらうのが最もきれいな解決で、新規設計ならBOS経由で通知するOS 2.0記述子(Windows 8.1以降)が第一候補になります。
USB機器の抜き差しにアプリを追従させるには、どうすればよいですか?
ポーリングではなくPnP通知を購読します。Windows 8以降のデスクトップアプリならCM_Register_Notificationにデバイスインターフェースのフィルターを指定するのが標準的で、Windows 7以前も対象ならRegisterDeviceNotificationとWM_DEVICECHANGEを使います。注意点として、CM_Register_Notificationは登録時点で既に存在するインターフェースを通知しないため、登録してからCM_Get_Device_Interface_Listで既存分を列挙する順序にします(逆にすると取りこぼします)。また、コールバック内でI/Oなどブロックし得る処理を行うと危険なので、別スレッドへ渡してください。そのうえで重要なのは、PnP通知を唯一の入口にしないことです。処理中のI/Oは通知より先に、あるいは同時に、削除やキャンセルのエラーで完了し得ます。すべての読み書きの完了パスで削除系エラーをセッション終了として扱い、PnP通知は待機中の切断を拾う補助の信号として組み合わせてください。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る