「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 の表示 | 主なドライバ | 確認コマンド / 画面 |
|---|---|---|---|
| Windows | COM3 等(デバイス マネージャ → ポート) | 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_acm | dmesg | 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)を使う最短レシピ
手順
- ドライバを導入(Windows はデバイスに応じて usbser/FTDI/CH340/CP210x 等。macOS/Linux は多くが標準対応)。
- ポート名を確認(Windows: COM 番号、Linux:
/dev/ttyUSB*or/dev/ttyACM*、macOS:/dev/tty.usb*//dev/cu.usb*)。 - ボーレート・データビット・パリティ・ストップビット・フロー制御を合わせる。デバイス側仕様に従う。
- 必要であれば 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 > 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 += (_, __) =>
{
try
{
// バイナリも扱うなら Read で受ける
int n = _sp.BytesToRead;
if (n > 0)
{
var buf = new byte[n];
_sp.Read(buf, 0, n);
Console.WriteLine($"RAW({n}): {BitConverter.ToString(buf)}");
}
// 行単位で読みたい場合
while (_sp.BytesToRead > 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, &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, &dcb);
COMMTIMEOUTS to = { .ReadIntervalTimeout = 50, .ReadTotalTimeoutConstant = 200 };
SetCommTimeouts(h, &to);
char buf[512]; DWORD got;
while (ReadFile(h, buf, sizeof(buf), &got, NULL) && got > 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 / Feature | OS 標準 API でアクセス。レポート ID とレポート長を理解(1 バイト目に Report ID が入る設計が多い)。 |
| CDC‑ACM | Bulk 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 > 0) {
printf("got %d bytes: ", n);
for (int i=0;i<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), &transferred, 1000);
if (r == 0 && transferred > 0) {
printf("IN %d bytes: ", transferred);
for (int i=0;i<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, &GUID_DEVINTERFACE_MyUsb, 0, &ifdata)) return 1;
DWORD needed = 0;
SetupDiGetDeviceInterfaceDetail(info, &ifdata, NULL, 0, &needed, NULL);
PSP_DEVICE_INTERFACE_DETAIL_DATA detail = (PSP_DEVICE_INTERFACE_DETAIL_DATA)malloc(needed);
detail->cbSize = sizeof(*detail);
SetupDiGetDeviceInterfaceDetail(info, &ifdata, detail, needed, NULL, NULL);
HANDLE hFile = CreateFile(detail->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, &hUsb)) return 1;
UCHAR pipeIn = 0x81; // 実機に合わせる
UCHAR buf[512]; ULONG read = 0;
while (WinUsb_ReadPipe(hUsb, pipeIn, buf, sizeof(buf), &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 分でやることチェックリスト
- Windows ならデバイス マネージャで「ポート (COM と LPT)」に現れるか確認。現れたら ケース A 確定。
- Linux/macOS なら
ls /dev/tty*、dmesgでttyUSB*/ttyACM*/usbserial*の有無を確認。 - 上記が無ければ、USB デバイスの クラス/HID/ベンダ固有 を把握(デスクリプタや OS のデバイス情報から)。
- HID なら hidapi、ベンダ固有なら WinUSB/libusb を選択。エンドポイント(例:0x81=IN, 0x02=OUT)とパケット長を確定。
- プロトコル不明なら、既存ユーティリティで動作する瞬間の 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<byte[]> 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 > 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 > 0 && sw.ElapsedMilliseconds >= 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, &cfg);
for (int i=0; i<cfg->interface[0].num_altsetting; ++i) {
id = &cfg->interface[0].altsetting[i];
for (int e=0; e<id->bNumEndpoints; ++e) {
const struct libusb_endpoint_descriptor *ep = &id->endpoint[e];
if ((ep->bmAttributes & LIBUSB_TRANSFER_TYPE_MASK) == LIBUSB_TRANSFER_TYPE_BULK) {
if (ep->bEndpointAddress & 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 でもシリアルと同じ“感覚”で安定運用できます。

コメント