USBデバイスをシリアルポートのように読み取る完全ガイド|仮想COM・CDC‑ACM・WinUSB・HID・libusbとC/C++/C#実装

「USB でもシリアルみたいに生のバイト列を取りたい」――開発現場でよく出るこの要望は、デバイスの種類(仮想 COM を出すか、独自 USB か)を見極めればシンプルに解けます。本記事では最短で判断・実装できるよう、ケース別の手順、必要ドライバ、C/C++/C# の実装例、OS ごとの確認ポイント、そして詰まりやすい落とし穴までを一気通貫で解説します。

目次

USB を「シリアルポートと同じ感覚」で読むための全体像

USB デバイスからの読み取りは、大きく次の二択で戦略が変わります。

ケース必要なもの・ポイント概要 / 実装方針
A. デバイスに USB‑シリアル変換(CH340/FT232/CP210x/CDC‑ACM 等)が内蔵OS に対応ドライバを導入(多くは標準 or ベンダ提供)。 仮想 COM ポート番号(Windows)/ デバイスノード(macOS/Linux)を確認。既存のシリアル通信コードをそのまま使える。C# は System.IO.Ports.SerialPort、C/C++ は Win32 API / POSIX open() で読み取り。
B. 変換チップなし(独自 USB)USB の通信クラスを特定(HID / CDC‑ACM / ベンダ固有)。 HID なら OS 標準 API(hidraw / HID API)。 CDC‑ACM なら実質ケース A と同様(仮想 COM)。 ベンダ固有なら WinUSB / libusb 等でエンドポイントへ I/O。パケット構造・エンドポイントを理解し、プロトコルを自実装。C/C++ は libusb が汎用、C# は Windows.Devices.Usb(UWP/WinRT)など。

OS 別:仮想 COM が現れる場所とドライバ

OS仮想 COM の表示主なドライバ確認コマンド / 画面
WindowsCOM3 等(デバイス マネージャ → ポート)usbser(CDC‑ACM)、FTDI、CH340/CH341、CP210x などdevmgmt.msc、または PowerShell で Get-PnpDevice -Class 'Ports'
Linux/dev/ttyUSB*(USB‑Serial)、/dev/ttyACM*(CDC‑ACM)ftdi_sio、ch341、cp210x、cdc_acmdmesg | grep tty、ls -l /dev/serial/by-id/
macOS/dev/tty.usbserial* / /dev/cu.usbserial*、/dev/tty.usbmodem*(CDC)FTDI、CP210x、CH34x(OS 版により標準/ベンダドライバ)ls /dev/tty.usb*、ls /dev/cu.usb*

備考:Linux ではユーザを dialout グループ等に追加しないとアクセスできない場合があります(例:sudo usermod -aG dialout <user> 後に再ログイン)。

ケース A:USB‑シリアル(仮想 COM)を使う最短レシピ

手順

  1. ドライバを導入(Windows はデバイスに応じて usbser/FTDI/CH340/CP210x 等。macOS/Linux は多くが標準対応)。
  2. ポート名を確認(Windows: COM 番号、Linux: /dev/ttyUSB* or /dev/ttyACM*、macOS: /dev/tty.usb*//dev/cu.usb*)。
  3. ボーレート・データビット・パリティ・ストップビット・フロー制御を合わせる。デバイス側仕様に従う。
  4. 必要であれば DTR/RTS を制御(多くの MCU ボードは DTR の遷移でリセット・ストリーム開始)。

C#(System.IO.Ports.SerialPort)の堅牢な例

using System;
using System.IO.Ports;
using System.Text;

class Program
{
static SerialPort _sp;
static void Main(string[] args)
{
    var portName = args.Length &gt; 0 ? args[0] : "COM3";
    _sp = new SerialPort(portName, 115200, Parity.None, 8, StopBits.One)
    {
        Encoding = Encoding.UTF8,
        ReadTimeout = 500,   // 無限待ちはハングの原因
        WriteTimeout = 500,
        NewLine = "\n",
        DtrEnable = true,    // 必要に応じて有効化
        RtsEnable = false
    };

    _sp.DataReceived += (_, __) =&gt;
    {
        try
        {
            // バイナリも扱うなら Read で受ける
            int n = _sp.BytesToRead;
            if (n &gt; 0)
            {
                var buf = new byte[n];
                _sp.Read(buf, 0, n);
                Console.WriteLine($"RAW({n}): {BitConverter.ToString(buf)}");
            }

            // 行単位で読みたい場合
            while (_sp.BytesToRead &gt; 0)
            {
                string line = _sp.ReadLine();
                Console.WriteLine($"LINE: {line}");
            }
        }
        catch (TimeoutException) { }
    };

    _sp.Open();
    Console.WriteLine($"{portName} open. Press ENTER to quit.");
    Console.ReadLine();
    _sp?.Close();
}

} 

Windows C(Win32 API)最小例

#include <windows.h>
#include <stdio.h>

int main(void) {
HANDLE h = CreateFileA("\\.\COM3", GENERIC_READ|GENERIC_WRITE, 0, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (h == INVALID_HANDLE_VALUE) { fprintf(stderr, "open failed\n"); return 1; }
DCB dcb = {0}; dcb.DCBlength = sizeof(dcb);
GetCommState(h, &amp;dcb);
dcb.BaudRate = CBR_115200; dcb.ByteSize = 8; dcb.Parity = NOPARITY; dcb.StopBits = ONESTOPBIT;
dcb.fOutxCtsFlow = FALSE; dcb.fOutxDsrFlow = FALSE; dcb.fOutX = dcb.fInX = FALSE;
SetCommState(h, &amp;dcb);

COMMTIMEOUTS to = { .ReadIntervalTimeout = 50, .ReadTotalTimeoutConstant = 200 };
SetCommTimeouts(h, &amp;to);

char buf[512]; DWORD got;
while (ReadFile(h, buf, sizeof(buf), &amp;got, NULL) &amp;&amp; got &gt; 0) {
    fwrite(buf, 1, got, stdout);
}
CloseHandle(h);
return 0;

} 

POSIX C(Linux/macOS)最小例

#include <termios.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int set_attr(int fd, speed_t baud) {
struct termios tio;
if (tcgetattr(fd, &tio) != 0) return -1;
cfmakeraw(&tio);
cfsetispeed(&tio, baud);
cfsetospeed(&tio, baud);
tio.c_cflag |= (CLOCAL | CREAD);
tio.c_cflag &= ~PARENB; // 8N1
tio.c_cflag &= ~CSTOPB;
tio.c_cflag &= ~CSIZE;
tio.c_cflag |= CS8;
tio.c_cc[VMIN]  = 0;  // 非ブロッキング寄り
tio.c_cc[VTIME] = 2;  // 0.2 秒
return tcsetattr(fd, TCSANOW, &tio);
}
int main(int argc, char** argv) {
const char* dev = argc > 1 ? argv[1] : "/dev/ttyUSB0";
int fd = open(dev, O_RDONLY | O_NOCTTY);
if (fd < 0) { perror("open"); return 1; }
if (set_attr(fd, B115200) != 0) { perror("termios"); return 1; }
char buf[512]; ssize_t n;
while ((n = read(fd, buf, sizeof(buf))) > 0) fwrite(buf, 1, n, stdout);
close(fd); return 0;
} 

よくある詰まりポイント

  • ポートが現れない:充電専用ケーブルの可能性/ドライバ未導入/ポート電力不足。別ケーブル・別ポートで切り分け。
  • Open で例外:別プロセスが占有、ポート番号違い、権限不足(Linux は dialout グループへ)。
  • 文字化け・バイナリ崩れ:通信設定(ボーレート等)と文字コードの不一致。ラインエンディング(CR/LF)も確認。
  • DTR 必須:一部の MCU/USB‑UART は DTR を ON にしないとストリーミングを始めません。

ケース B:独自 USB デバイスを直接読む(HID / WinUSB / libusb)

まずはクラスを特定する

クラス典型的なエンドポイント構成実装の要点
HID(Human Interface Device)Interrupt IN(必須) + 必要に応じて OUT / FeatureOS 標準 API でアクセス。レポート ID とレポート長を理解(1 バイト目に Report ID が入る設計が多い)。
CDC‑ACMBulk IN/OUT + Interrupt(通知)仮想 COM として扱える(ケース A に準ずる)。
ベンダ固有(0xFF)Bulk IN/OUT(任意個)WinUSB / libusb でパイプに直接 I/O。インターフェース番号・エンドポイント番号を把握する。

HID:C / hidapi の最小例

HID はドライバ導入不要で扱えるのが強みです(キーボード/マウス以外の専用機も多数)。

#include "hidapi.h"
#include <stdio.h>

int main(void) {
if (hid_init() != 0) return 1;
// 例: VID=0x1234, PID=0x5678 を置き換え
hid_device* dev = hid_open(0x1234, 0x5678, NULL);
if (!dev) { puts("open failed"); return 1; }
unsigned char buf[65]; // 先頭1バイトがReport IDの設計が多い
while (1) {
    int n = hid_read(dev, buf, sizeof(buf));
    if (n &gt; 0) {
        printf("got %d bytes: ", n);
        for (int i=0;i&lt;n;i++) printf("%02X ", buf[i]);
        puts("");
    }
}
hid_close(dev);
hid_exit();
return 0;

} 

ベンダ固有:C / libusb の最小例(Bulk IN を読む)

#include <libusb-1.0/libusb.h>
#include <stdio.h>
#include <string.h>

int main(void) {
libusb_context* ctx = NULL;
libusb_device_handle* h = NULL;
int r = libusb_init(&ctx);
if (r) return 1;
uint16_t vid = 0x1234, pid = 0x5678; // 実機に合わせる
h = libusb_open_device_with_vid_pid(ctx, vid, pid);
if (!h) { puts("open failed"); goto EXIT; }

int ifnum = 0; // 実機のインターフェース番号
libusb_claim_interface(h, ifnum);

// 例: Bulk IN エンドポイント 0x81(EP1 IN)
unsigned char ep_in = 0x81;
unsigned char buf[512];
int transferred = 0;
while (1) {
    r = libusb_bulk_transfer(h, ep_in, buf, sizeof(buf), &amp;transferred, 1000);
    if (r == 0 &amp;&amp; transferred &gt; 0) {
        printf("IN %d bytes: ", transferred);
        for (int i=0;i&lt;transferred;i++) printf("%02X ", buf[i]);
        puts("");
    }
}

libusb_release_interface(h, ifnum);

EXIT:
if (h) libusb_close(h);
libusb_exit(ctx);
return 0;
} 

補足:Bulk IN/OUT の番地(0x81, 0x02 等)はデバイスごとに異なります。正確な番号はデスクリプタや USB トレースで確認し、必要なら libusb_get_config_descriptor() で列挙して自動検出してください。Linux でカーネルドライバが掴んでいる場合は libusb_detach_kernel_driver() が必要です。

Windows(WinUSB)でベンダ固有デバイスを読む:C++ の骨格

WinUSB はユーザ空間から USB パイプに直接アクセスするための標準 API です。デバイスには WinUSB ドライバを割り当てておきます(ツール等で切替)。

#include <windows.h>
#include <setupapi.h>
#include <winusb.h>
#include <initguid.h>
#include <stdio.h>

// デバイス固有のデバイスインターフェース GUID を指定
// 例: DEFINE_GUID(GUID_DEVINTERFACE_MyUsb, ...);

int main() {
HDEVINFO info = SetupDiGetClassDevs(&GUID_DEVINTERFACE_MyUsb, NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
if (info == INVALID_HANDLE_VALUE) return 1;
SP_DEVICE_INTERFACE_DATA ifdata = {0}; ifdata.cbSize = sizeof(ifdata);
if (!SetupDiEnumDeviceInterfaces(info, NULL, &amp;GUID_DEVINTERFACE_MyUsb, 0, &amp;ifdata)) return 1;

DWORD needed = 0;
SetupDiGetDeviceInterfaceDetail(info, &amp;ifdata, NULL, 0, &amp;needed, NULL);
PSP_DEVICE_INTERFACE_DETAIL_DATA detail = (PSP_DEVICE_INTERFACE_DETAIL_DATA)malloc(needed);
detail-&gt;cbSize = sizeof(*detail);
SetupDiGetDeviceInterfaceDetail(info, &amp;ifdata, detail, needed, NULL, NULL);

HANDLE hFile = CreateFile(detail-&gt;DevicePath, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE,
                          NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL|FILE_FLAG_OVERLAPPED, NULL);

WINUSB_INTERFACE_HANDLE hUsb;
if (!WinUsb_Initialize(hFile, &amp;hUsb)) return 1;

UCHAR pipeIn = 0x81; // 実機に合わせる
UCHAR buf[512]; ULONG read = 0;
while (WinUsb_ReadPipe(hUsb, pipeIn, buf, sizeof(buf), &amp;read, NULL)) {
    fwrite(buf, 1, read, stdout);
}
WinUsb_Free(hUsb);
CloseHandle(hFile);
free(detail);
SetupDiDestroyDeviceInfoList(info);
return 0;

} 

補足:GUID は自作デバイスの INF/ドライバ側で公開したものに合わせてください。WinUSB では「パイプ=エンドポイント」に対して Read/Write を行います。

C#(UWP/WinRT):Windows.Devices.Usb で Bulk IN を読む

using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.Devices.Usb;
using Windows.Devices.Enumeration;

public async Task ReadUsbAsync(ushort vid, ushort pid) {
string selector = UsbDevice.GetDeviceSelector(vid, pid);
var dis = await DeviceInformation.FindAllAsync(selector);
if (dis.Count == 0) return;
using (UsbDevice dev = await UsbDevice.FromIdAsync(dis[0].Id)) {
var iface = dev.Configuration.UsbInterfaces[0];
var bulkIn = iface.BulkInPipes.First();
var reader = new Windows.Storage.Streams.DataReader(bulkIn.InputStream);
reader.InputStreamOptions = Windows.Storage.Streams.InputStreamOptions.Partial;
while (true) {
uint n = await reader.LoadAsync(512);
byte[] buf = new byte[n];
reader.ReadBytes(buf);
System.Diagnostics.Debug.WriteLine(BitConverter.ToString(buf));
}
}
} 

デバイスの見極め:最初の 5 分でやることチェックリスト

  1. Windows ならデバイス マネージャで「ポート (COM と LPT)」に現れるか確認。現れたら ケース A 確定。
  2. Linux/macOS なら ls /dev/tty*、dmesg で ttyUSB* / ttyACM* / usbserial* の有無を確認。
  3. 上記が無ければ、USB デバイスの クラス/HID/ベンダ固有 を把握(デスクリプタや OS のデバイス情報から)。
  4. HID なら hidapi、ベンダ固有なら WinUSB/libusb を選択。エンドポイント(例:0x81=IN, 0x02=OUT)とパケット長を確定。
  5. プロトコル不明なら、既存ユーティリティで動作する瞬間の USB パケットをキャプチャし、固定ヘッダ・長さ・チェックサム等の規則を抽出。

シリアル“らしさ”を再現する実装のコツ

  • 行単位の取り回し:USB Bulk/HID の世界には「行」の概念がないため、\n やデリミタでアプリ側がフレーミングする。
  • バッファとタイムアウト:PC 側では「受信無音 >= X ms で 1 フレーム確定」といったタイムアウト切り上げを実装すると扱いやすい。
  • フロー制御:仮想 COM では RTS/CTS/DTR/DSR を使えるが、WinUSB/libusb/HID には無い。かわりにアプリ層の ACK/NAK/シーケンス番号で制御する。
  • エンコーディング:テキストなら UTF‑8 に統一。バイナリは HEX/CBOR/MessagePack 等の自前プロトコルで混在を避ける。
  • 再接続:USB の抜き差しを常に想定。デバイスノード/パスの再探索を周期実行し、I/O 例外時に自動復帰を入れる。

トラブルシューティングを深掘り

ポートが出ない/頻繁に切れる

  • 給電不足(バスパワー)やノイズに弱いケーブル。セルフパワー HUB・短いケーブルで改善。
  • ドライバの競合(Windows で CDC が別ドライバに捕捉)。デバイスのモードを固定する/適切なドライバを割り当てる。
  • macOS のセキュリティポリシーで kext が無効化。必要に応じて許可設定。

ケース A での通信不良

  • MCU 側が TTL レベル なのに PC 側が RS‑232 レベルの変換器を使っているとレベル不一致で通信不能。3.3V/5V の電圧仕様を確認。
  • DTR/RTS の初期値に依存する機器(ログ開始のトリガ等)。接続直後に既定値へ明示的に設定。
  • Linux の権限問題:udev ルールで特定 VID/PID にシンボリックリンクを張り、パーミッションを設定する例: # /etc/udev/rules.d/99-myserial.rules SUBSYSTEM=="tty", ATTRS{idVendor}=="1234", ATTRS{idProduct}=="5678", \ SYMLINK+="mydevice", MODE="0666"

ケース B(libusb/WinUSB/HID)での典型的なハマり

  • エンドポイント番号違い:0x81 は「EP1 IN」を意味する慣例だが、実機が別番地のことも多い。必ずデスクリプタで確認。
  • カーネルドライバの占有:Linux で HID/CDC が既にバインドされている場合、libusb_claim_interface が失敗。必要に応じてデタッチ。
  • HID のレポート ID:1 バイト目が Report ID の設計では、hid_read() で返る先頭が ID になる。固定長で 1 バイト余るのは仕様。
  • Windows のデバイスパス探索:WinUSB では INF/デバイスインターフェース GUID を統一し、SetupAPI → DevicePath → WinUsb_Initialize の順で開く。

セキュリティ・配布の観点

  • 署名付きドライバ:Windows ではテスト署名/本番署名の違いに注意。配布時は必ず本番署名のパッケージに。
  • 権限と多重起動:同時オープンが不可の設計が多い。アプリ側はミューテックス・排他制御を実装。
  • 自動ドライバ割当:WinUSB を前提とする機器は「WCID 互換」記述子を用意すると、ユーザが手動でドライバを選ぶ手間を減らせます。

よくある質問(FAQ)

USB でも SerialPort クラスで読めますか? 仮想 COM(CDC‑ACM/USB‑UART)になっていれば読めます。HID/WinUSB 等の独自デバイスは SerialPort では読めません。

<dt>読めない場合、何が必要ですか?</dt>
<dd>HIDなら OS 標準の HID API、ベンダ固有なら WinUSB/libusb 等を使い、正しいエンドポイントに対して I/O します。ドライバの割当(Windows)や権限(Linux)も整えます。</dd>

<dt>C/C++ の実装例は?</dt>
<dd>本記事に Win32 API(COM ポート)、POSIX <code>termios</code>、hidapi、libusb、WinUSB の最小例を掲載しています。目的に応じて選択してください。</dd>

<dt>CDC‑ACM と USB‑シリアル(UART ブリッジ)の違いは?</dt>
<dd>どちらも PC 側からは「仮想 COM」として見えます。CDC‑ACM は USB クラス規格、USB‑UART ブリッジはベンダ固有ドライバの場合があります。</dd>

<dt>テキストとバイナリが混在します。安全に区別するには?</dt>
<dd>アプリ層でフレーム化(ヘッダ・長さ・CRC)を行い、テキストは UTF‑8、バイナリは Hex 表示等に分けて扱うとデバッグが容易です。</dd>

実装テンプレ集(コピペしてすぐ動かす)

行フレームを組み立てる受信ループ(C#)

private static IEnumerable&lt;byte[]&gt; ReadFrames(SerialPort sp, byte delimiter = (byte)'\n', int idleMs = 50) {
    var ms = new System.IO.MemoryStream();
    var sw = System.Diagnostics.Stopwatch.StartNew();
    while (sp.IsOpen) {
        int n = sp.BytesToRead;
        if (n &gt; 0) {
            var buf = new byte[n]; sp.Read(buf, 0, n); sw.Restart();
            foreach (var b in buf) {
                if (b == delimiter) { yield return ms.ToArray(); ms.SetLength(0); }
                else ms.WriteByte(b);
            }
        } else if (ms.Length &gt; 0 &amp;&amp; sw.ElapsedMilliseconds &gt;= idleMs) {
            // 無音でフレーム確定
            yield return ms.ToArray(); ms.SetLength(0);
        } else {
            System.Threading.Thread.Sleep(5);
        }
    }
}

libusb:エンドポイント自動検出のヒント(断片)

const struct libusb_interface_descriptor *id;
const struct libusb_config_descriptor *cfg;
libusb_get_config_descriptor(libusb_get_device(h), 0, &amp;cfg);
for (int i=0; i&lt;cfg-&gt;interface[0].num_altsetting; ++i) {
    id = &amp;cfg-&gt;interface[0].altsetting[i];
    for (int e=0; e&lt;id-&gt;bNumEndpoints; ++e) {
        const struct libusb_endpoint_descriptor *ep = &amp;id-&gt;endpoint[e];
        if ((ep-&gt;bmAttributes &amp; LIBUSB_TRANSFER_TYPE_MASK) == LIBUSB_TRANSFER_TYPE_BULK) {
            if (ep-&gt;bEndpointAddress &amp; 0x80) {/* IN */}
            else {/* OUT */}
        }
    }
}
libusb_free_config_descriptor(cfg);

最後に:判断の指針(これだけ覚えれば OK)

  • まず「仮想 COM が出るか」を見る。出るなら SerialPort で完了。
  • 仮想 COM が出ないならクラスを特定。HID → HID API、ベンダ固有 → WinUSB / libusb。
  • どの方法でも「フレーミング」「タイムアウト」「再接続」の 3 点を実装しておけば現場は安定します。

付録:チェックシート(貼って使える)

項目OK?メモ
仮想 COM の有無を確認した□Windows: COM? / Linux: ttyUSB/ttyACM / macOS: tty.usb*
通信パラメータ(ボーレート等)を一致□8N1 / フロー制御 / DTR/RTS
HID/WinUSB/libusb 用のクラス・EP を把握□IN/OUT 番地、パケット長、レポート ID
権限/ドライバの割当を確認□Linux のグループ、Windows のドライバ、macOS の許可
再接続・例外時リトライを実装□抜け差し/サスペンド復帰

まとめ

USB デバイスの読み取りは「仮想 COM があるか」の見極めが 80%。あれば既存のシリアル資産がそのまま使え、なければクラス(HID/CDC/ベンダ固有)に応じて WinUSB・libusb・HID API を選ぶだけです。本記事のコード断片をベースに、エンドポイントとフレーミングを整えれば、USB でもシリアルと同じ“感覚”で安定運用できます。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次