uchan; USB CDC機器のパケットキャプチャを解析 - uchan-nos/os-from-zero GitHub Wiki
秋月のPIC18F14K50使用USB対応超小型マイコンボード に、Microchip 提供の CDC サンプルをそのまま書き込んだ機器の挙動を観察し、CDC ACM の仕様の理解を試みます。
使用した Microchip のサンプルプログラムは mla\v2018_11_26\apps\usb\device\cdc_basic\firmware\low_pin_count_usb_development_kit_pic18f14k50.x です。このプログラムを書き込んだマイコンを USB で PC につなぎ、USB ディスクリプタの内容を確認します。
まず、接続した機器のバス番号とデバイス番号を調べます。
$ lsusb
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 001 Device 002: ID 04f2:b683 Chicony Electronics Co., Ltd
Bus 001 Device 003: ID 8087:0aaa Intel Corp.
Bus 001 Device 021: ID 04d8:000a Microchip Technology, Inc. CDC RS-232 Emulation Demo
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
バス 1、デバイス 21 ということが分かりました。再度 lsusb コマンドを使ってディスクリプタを取得します。
$ lsusb -v -s 1:21
Bus 001 Device 021: ID 04d8:000a Microchip Technology, Inc. CDC RS-232 Emulation Demo
Couldn't open device, some information will be missing
Device Descriptor:
bLength 18
bDescriptorType 1
bcdUSB 2.00
bDeviceClass 2 Communications
bDeviceSubClass 0
bDeviceProtocol 0
bMaxPacketSize0 8
idVendor 0x04d8 Microchip Technology, Inc.
idProduct 0x000a CDC RS-232 Emulation Demo
bcdDevice 1.00
iManufacturer 1
iProduct 2
iSerial 0
bNumConfigurations 1
Configuration Descriptor:
bLength 9
bDescriptorType 2
wTotalLength 67
bNumInterfaces 2
bConfigurationValue 1
iConfiguration 0
bmAttributes 0xc0
Self Powered
MaxPower 100mA
Interface Descriptor:
bLength 9
bDescriptorType 4
bInterfaceNumber 0
bAlternateSetting 0
bNumEndpoints 1
bInterfaceClass 2 Communications
bInterfaceSubClass 2 Abstract (modem)
bInterfaceProtocol 1 AT-commands (v.25ter)
iInterface 0
CDC Header:
bcdCDC 1.10
CDC ACM:
bmCapabilities 0x02
line coding and serial state
CDC Union:
bMasterInterface 0
bSlaveInterface 1
CDC Call Management:
bmCapabilities 0x00
bDataInterface 1
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x81 EP 1 IN
bmAttributes 3
Transfer Type Interrupt
Synch Type None
Usage Type Data
wMaxPacketSize 0x000a 1x 10 bytes
bInterval 2
Interface Descriptor:
bLength 9
bDescriptorType 4
bInterfaceNumber 1
bAlternateSetting 0
bNumEndpoints 2
bInterfaceClass 10 CDC Data
bInterfaceSubClass 0 Unused
bInterfaceProtocol 0
iInterface 0
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x02 EP 2 OUT
bmAttributes 2
Transfer Type Bulk
Synch Type None
Usage Type Data
wMaxPacketSize 0x0040 1x 64 bytes
bInterval 0
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x82 EP 2 IN
bmAttributes 2
Transfer Type Bulk
Synch Type None
Usage Type Data
wMaxPacketSize 0x0040 1x 64 bytes
bInterval 0
CDC の仕様書「USB Class Definitions for Communications Devices」によると、各ディスクリプタの値は次のように決まっています。
- デバイスディスクリプタ
- bDeviceClass: 2
- bDeviceSubClass, bDeviceProtocol: 0
- Communications Class のインタフェースディスクリプタ
- bInterfaceClass: 2
- bInterfaceSubClass: サブクラスコード。ACM なら 2。
- bInterfaceProtocol: サポートするコマンド体系を表すコード。実験機器での値は 1(AT-commands V.250)
- Data Class のインタフェースディスクリプタ
- bInterfaceClass: 10
- bInterfaceSubClass: 0
- bInterfaceProtocol: データフォーマットを表す(?)コード。実験機器での値は 0(規定しない)
CDC では、インタフェースディスクリプタの後ろにファンクショナルディスクリプタ(Functional Descriptors)が並ぶ構造になっています。仕様書には「Header Functional Descriptor」や「Union Functional Descriptor」などが記載されています。lsusb の出力にある「CDC Header」や「CDC ACM」、「CDC Union」等がファンクショナルディスクリプタのようですね。ACM 独自のファンクショナルディスクリプタについては「USB Communications Class Subclass Specification for PSTN Devices」の方に規定されています。
実験機器の情報から、特に重要そうな項目に関して仕様を確認してみます。今回調べる項目を抜き出して再掲します。
Interface Descriptor:
……
bInterfaceClass 2 Communications
bInterfaceSubClass 2 Abstract (modem)
bInterfaceProtocol 1 AT-commands (v.25ter)
……
CDC Header:
bcdCDC 1.10
CDC ACM:
bmCapabilities 0x02
line coding and serial state
CDC Union:
bMasterInterface 0
bSlaveInterface 1
CDC Call Management:
bmCapabilities 0x00
bDataInterface 1
Endpoint Descriptor:
……
bEndpointAddress 0x81 EP 1 IN
bmAttributes 3
Transfer Type Interrupt
Synch Type None
Usage Type Data
……
Interface Descriptor:
……
bInterfaceClass 10 CDC Data
bInterfaceSubClass 0 Unused
bInterfaceProtocol 0
……
Endpoint Descriptor:
……
bEndpointAddress 0x02 EP 2 OUT
bmAttributes 2
Transfer Type Bulk
Synch Type None
Usage Type Data
……
Endpoint Descriptor:
……
bEndpointAddress 0x82 EP 2 IN
bmAttributes 2
Transfer Type Bulk
Synch Type None
Usage Type Data
……
実験機器には 2 つのインタフェースディスクリプタがあります。「コミュニケーションクラス」と「データクラス」です。まずはコミュニケーションクラスから調査します。
コミュニケーションクラスのインタフェースディスクリプタには、サブクラスが ACM、プロトコルが AT Commands V.250 だということが記されています(lsusb の出力ではプロトコルが「v.25ter」となっていますが、CDC 仕様書にはプロトコル 1 は「AT Commands: V.250 etc」と定められています)。
AT コマンドは知らなかったのですが、どうやらモデムを制御するコマンド体系だそうです。コマンド名が「AT」から始まるので「AT コマンド」というそうです。AT コマンドについては AT コマンドの概要 - SORACOM で学ぶ AT コマンド入門 などが詳しそうな感じがします。RS-232C はもともとモデム(アナログ電話回線に通信データを流すための装置)を制御することに多用されていたので AT コマンドが登場するのかな?と思っていますが、真偽は不明です。
さて、インタフェースディスクリプタの直後から続くファンクショナルディスクリプタを調べていきます。
CDC Header は、この CDC 機器が対応している CDC リビジョン番号を表します。実験機器では 1.10 ですから、1998 年に発行された規格に準拠しますよ、という意味だと思います。2007 年に 1.2 が発行されているので、ちょっと古い規格に準拠しているな、ということが分かりました。
CDC ACM は、この ACM 機器(正確には、このコンフィギュレーション)がサポートする機能を表します。ビットマップになっており、それぞれのビットが特定の機能を表します。実験機器では、ビット 1 だけが ON になっています。このビットが ON になっているので、機器は Set_Line_Coding, Set_Control_Line_State, Get_Line_Coding というリクエストと Serial_State という通知をサポートしていることが分かります。それぞれがどんな意味かは、後で調べようと思います。
CDC Union は複数のインタフェース間の関係性を記述するものです。複数のインタフェースのうち 1 つは、全体の制御専用インタフェースです。lsusb の出力では「bMasterInterface」となっていますが、最新の CDC 仕様では「bControlInterface」という名前になっているようです。残りのインタフェースは、制御専用インタフェースと組み合わさって 1 つの機能を構成するものです。lsusb では「bSlaveInterface」ですが、仕様では「bSubordinateInterface0」となっています。ここに書かれているインタフェース番号は、このコンフィギュレーション内での 0 始まりのインデックスだそうです。
実験機器では、bMasterInterface は 1 つ目のインタフェース(bInterfaceClass が 2 Communications となっているもの)を指していて、bSlaveInterface は 2 つ目のインタフェース(bInterfaceClass が 10 CDC Data となっているもの)を指しているということが分かりますね。
CDC 仕様の「3.5.1 Communications Class Endpoint Requirements」に、機器に対するリクエストは「the default endpoint」を使う、とあります。これはエンドポイント 0(デフォルトコントロールパイプ)のことだろうと思います。さらに、CDC 機器は通知のために interrupt エンドポイントを使うこともある、ということが記されています。このエンドポイントはまさに、実験機器の 1 つ目のインタフェースに含まれるエンドポイント(bEndpointAddress が 0x81 EP 1 IN となっているもの)のことですね。
CDC Call Management はコールの処理に関するものだそうです。「コール」の意味が筆者はよく分かっていませんが、CDC ACM がモデム用の仕様だということを考えると、電話をかけたり、受けたりすることをコールと呼ぶのかなと想像します。
bmCapabilities のビット 0 は、この機器がコールに対応するかどうかを表しています。実験機器では 0 なので、そもそもコールに対応する能力が無いということですね。パソコンと通信することだけが目的の機器なので、まあ当然かなと思います。bDataInterface はコール処理に使うインタフェース番号が記入されています。といっても、そもそもコール処理には対応しないと言っているので、この番号が使われることは無いんだろうと思いますが。
ここまでで、コミュニケーションクラス専用のディスクリプタたちを説明しました。データクラスにはクラス専用ディスクリプタは(今のところは)規定されていません。実際、実験機器にもデータクラスのインタフェースには標準ディスクリプタしか含まれませんね。
ここでは CDC ACM の bmCapabilities に設定された値を詳しく調査していきます。lsusb の出力には bmCapabilities=0x02 とあり、このコンフィギュレーションが Set_Line_Coding, Set_Control_Line_State, Get_Line_Coding というリクエストと Serial_State という通知をサポートしている、ということが分かります。それぞれ、どういう意味でしょうか。
前者 3 つのリクエストは CDC ACM 仕様の「6.3 PSTN Subclass Specific Requests」に載っています。順番に見ていきます。
Set_Line_Coding、Get_Line_Coding は UART の通信パラメタを設定、取得するリクエストです。ビットレート(9600bps とか)、ストップビット数、パリティの種類、データビット幅を設定、取得できます。
Set_Control_Line_State は制御信号を設定します。制御信号をビットマップで指定します。ビット 1 は RTS、ビット 0 は DTR に対応します。RTS は Request To Send の意味で、1 にすると相手側機器(ここでは、実験機器そのもの)に送信を要求するという意味になります。DTR は Data Terminal Ready の意味で、1 にすると自身(ここでは、実験機器に繋がったパソコン)が存在することを表明します。これらと対になる 2 つの制御信号 CTS と DSR は、実験機器からパソコンへの方向の信号であるため、パソコンからのリクエストで変化させるのではなく、機器自身が変化させることになります。
残るは Serial_State という通知についてです。これは CDC ACM 仕様の「6.5 PSTN Subclass Specific Notifications」に記載されています。通知のデータはビットマップになっていて、この中に DSR と DCD が含まれます。あれ?CTS はありませんね。CTS を機器からパソコンに送りたい場合、どうするんでしょうね?
ここまででディスクリプタの仕様があらかた分かりました。MikanOS 向けの CDC ACM ドライバを開発しようと思ったら、ディスクリプタが分かるだけでは不十分です。エンドポイントを流れるパケットがどのようなものなのかを知らなければなりません。
パケットを知るには実験機器のソースコードを読むか、実際のパケットを見る方法がありそうです。ソースコードを少し読もうとしたところ、サンプルコードはかなり複雑な作りになっており、実際にどのようなデータが送受信されるのかが非常に分かりにくい構造になっていました。そこで、まずは USB パケットのキャプチャを試みます。
軽く調べたところ、Wireshark で USB パケットをキャプチャできるようです。ネットワークだけかと思っていたので驚きです。Wireshark のターミナル版である tshak のインストールから実際にパケットをキャプチャするまでの手順を簡単にメモしておきます。Wireshark のセットアップ手順 https://wiki.wireshark.org/CaptureSetup/USB を参考にしました。
- sudo apt install tshark: tshark をインストールします。
- 途中で非特権ユーザで dumpcap を使えるようにするか、というようなダイアログが出ます。先に示した Wireshark のセットアップ手順では「はい」を選ぶことになっています。
- sudo adduser $USER wireshark: ユーザを wireshark グループに追加します。
- sudo setfacl -m u:$USER:r /dev/usbmon*
- /dev/usbmon* を現在のユーザが読めるようにします。setfacl コマンドを使うと UNIX の古典的なアクセス制御(所有者、グループ、その他)よりも細かくアクセス権を設定できるようです。所有者は root でありながらも $USER に対してのみ Read 権限を付与する、ということをやりたいわけです。
- sudo chmod o+x /usr/bin/dumpcap
- これは先の手順には書いてありませんが、自分の環境ではこの手順をしないと一般ユーザで usbmon のキャプチャができませんでした。
- sudo modprobe usbmon: usbmon モジュールをロードします。
- モジュールをロードすると /sys/kernel/debug/usb/usbmon が生成されるはずです。
これで準備完了です。後はキャプチャしたい USB 機器のバス番号を調べ、tshark -i usbmon<バス番号>
でキャプチャをスタートします。
パケットキャプチャを動作させた状態で、GtkTerm というシリアル通信のためのアプリを用いて、実験機器(PIC18F14K50)と通信してみました。GtkTerm を起動し、「abc」を送り、「bcd」がエコーバックされ、接続を切るまでの様子を示します。
$ tshark -i usbmon1
Capturing on 'usbmon1'
1 0.000000 host → 1.1.0 USBHUB 64 GET_STATUS Request [Port 1]
2 0.000046 1.1.0 → host USBHUB 68 GET_STATUS Response [Port 1]
3 0.000051 host → 1.1.0 USBHUB 64 GET_STATUS Request [Port 2]
4 0.000068 1.1.0 → host USBHUB 68 GET_STATUS Response [Port 2]
5 0.000071 host → 1.1.0 USBHUB 64 GET_STATUS Request [Port 3]
6 0.000078 1.1.0 → host USBHUB 68 GET_STATUS Response [Port 3]
7 0.000080 host → 1.1.0 USBHUB 64 GET_STATUS Request [Port 4]
8 0.000088 1.1.0 → host USBHUB 68 GET_STATUS Response [Port 4]
9 0.000091 host → 1.1.0 USBHUB 64 GET_STATUS Request [Port 5]
10 0.000099 1.1.0 → host USBHUB 68 GET_STATUS Response [Port 5]
11 0.000101 host → 1.1.0 USBHUB 64 GET_STATUS Request [Port 6]
12 0.000113 1.1.0 → host USBHUB 68 GET_STATUS Response [Port 6]
13 0.000116 host → 1.1.0 USBHUB 64 GET_STATUS Request [Port 7]
14 0.000133 1.1.0 → host USBHUB 68 GET_STATUS Response [Port 7]
15 0.000135 host → 1.1.0 USBHUB 64 GET_STATUS Request [Port 8]
16 0.000143 1.1.0 → host USBHUB 68 GET_STATUS Response [Port 8]
17 0.000145 host → 1.1.0 USBHUB 64 GET_STATUS Request [Port 9]
18 0.000153 1.1.0 → host USBHUB 68 GET_STATUS Response [Port 9]
19 0.000155 host → 1.1.0 USBHUB 64 GET_STATUS Request [Port 10]
20 0.000163 1.1.0 → host USBHUB 68 GET_STATUS Response [Port 10]
21 0.000165 host → 1.1.0 USBHUB 64 GET_STATUS Request [Port 11]
22 0.000189 1.1.0 → host USBHUB 68 GET_STATUS Response [Port 11]
23 0.000191 host → 1.1.0 USBHUB 64 GET_STATUS Request [Port 12]
24 0.000208 1.1.0 → host USBHUB 68 GET_STATUS Response [Port 12]
25 0.000210 host → 1.1.1 USB 64 URB_INTERRUPT in
26 0.000219 host → 1.1.0 USBHUB 64 GET_STATUS Request [Port 1]
27 0.000237 1.1.0 → host USBHUB 68 GET_STATUS Response [Port 1]
28 0.000239 host → 1.1.0 USBHUB 64 CLEAR_FEATURE Request [Port 1: PORT_SUSPEND]
29 0.048176 1.1.0 → host USBHUB 64 CLEAR_FEATURE Response [Port 1: PORT_SUSPEND]
30 0.048204 1.1.1 → host USB 66 URB_INTERRUPT in
31 0.048209 host → 1.1.1 USB 64 URB_INTERRUPT in
32 0.096123 host → 1.1.0 USBHUB 64 GET_STATUS Request [Port 1]
33 0.096145 1.1.0 → host USBHUB 68 GET_STATUS Response [Port 1]
34 0.116122 host → 1.1.0 USBHUB 64 CLEAR_FEATURE Request [Port 1: C_PORT_SUSPEND]
35 0.116145 1.1.0 → host USBHUB 64 CLEAR_FEATURE Response [Port 1: C_PORT_SUSPEND]
36 0.116150 host → 1.22.0 USB 64 GET STATUS Request
37 0.116287 1.22.0 → host USB 66 GET STATUS Response
38 0.116333 host → 1.1.0 USBHUB 64 GET_STATUS Request [Port 1]
39 0.116338 host → 1.22.1 USB 64 URB_INTERRUPT in
40 0.116351 host → 1.22.2 USB 64 URB_BULK in
41 0.116353 1.1.0 → host USBHUB 68 GET_STATUS Response [Port 1]
42 0.116353 host → 1.22.2 USB 64 URB_BULK in
43 0.116354 host → 1.22.2 USB 64 URB_BULK in
44 0.116355 host → 1.22.2 USB 64 URB_BULK in
45 0.116355 host → 1.22.2 USB 64 URB_BULK in
46 0.116356 host → 1.22.2 USB 64 URB_BULK in
47 0.116356 host → 1.22.2 USB 64 URB_BULK in
48 0.116357 host → 1.22.2 USB 64 URB_BULK in
49 0.116358 host → 1.22.2 USB 64 URB_BULK in
50 0.116358 host → 1.22.2 USB 64 URB_BULK in
51 0.116359 host → 1.22.2 USB 64 URB_BULK in
52 0.116359 host → 1.22.2 USB 64 URB_BULK in
53 0.116360 host → 1.22.2 USB 64 URB_BULK in
54 0.116360 host → 1.22.2 USB 64 URB_BULK in
55 0.116361 host → 1.22.2 USB 64 URB_BULK in
56 0.116361 host → 1.22.2 USB 64 URB_BULK in
57 0.116369 host → 1.22.0 USB 64 URB_CONTROL out
58 0.116607 1.22.0 → host USB 64 URB_CONTROL out
59 0.116664 host → 1.22.0 USB 71 URB_CONTROL out
60 0.116815 1.22.0 → host USB 64 URB_CONTROL out
<ここで tshark の出力が一旦止まる。CDC の初期化プロトコルが一段落した感じ。この後「abc」を入力。>
61 4.011489 host → 1.22.2 USB 65 URB_BULK out
62 4.011555 1.22.2 → host USB 64 URB_BULK out
63 4.011615 1.22.2 → host USB 65 URB_BULK in
64 4.011645 host → 1.22.2 USB 64 URB_BULK in
65 4.283069 host → 1.22.2 USB 65 URB_BULK out
66 4.283203 1.22.2 → host USB 64 URB_BULK out
67 4.283249 1.22.2 → host USB 65 URB_BULK in
68 4.283294 host → 1.22.2 USB 64 URB_BULK in
69 4.479766 host → 1.22.2 USB 65 URB_BULK out
70 4.479852 1.22.2 → host USB 64 URB_BULK out
71 4.479898 1.22.2 → host USB 65 URB_BULK in
72 4.479910 host → 1.22.2 USB 64 URB_BULK in
<ここで再度 tshark の出力が一旦止まる。「abc」の入力と「bcd」のエコーバックが完了したのだろう。>
73 9.500228 host → 1.22.0 USB 71 URB_CONTROL out
74 9.500398 1.22.0 → host USB 64 URB_CONTROL out
75 9.500486 host → 1.22.0 USB 64 URB_CONTROL out
76 9.500651 1.22.0 → host USB 64 URB_CONTROL out
77 9.500741 1.22.1 → host USB 64 URB_INTERRUPT in
78 9.500771 1.22.2 → host USB 64 URB_BULK in
79 9.500818 1.22.2 → host USB 64 URB_BULK in
80 9.500873 1.22.2 → host USB 64 URB_BULK in
81 9.500915 1.22.2 → host USB 64 URB_BULK in
82 9.500953 1.22.2 → host USB 64 URB_BULK in
83 9.500994 1.22.2 → host USB 64 URB_BULK in
84 9.501032 1.22.2 → host USB 64 URB_BULK in
85 9.501074 1.22.2 → host USB 64 URB_BULK in
86 9.501113 1.22.2 → host USB 64 URB_BULK in
87 9.501154 1.22.2 → host USB 64 URB_BULK in
88 9.501192 1.22.2 → host USB 64 URB_BULK in
89 9.501234 1.22.2 → host USB 64 URB_BULK in
90 9.501272 1.22.2 → host USB 64 URB_BULK in
91 9.501314 1.22.2 → host USB 64 URB_BULK in
92 9.501352 1.22.2 → host USB 64 URB_BULK in
93 9.501394 1.22.2 → host USB 64 URB_BULK in
94 11.864744 host → 1.1.0 USBHUB 64 SET_FEATURE Request [Port 1: PORT_SUSPEND]
95 11.884237 1.1.0 → host USBHUB 64 SET_FEATURE Response [Port 1: PORT_SUSPEND]
96 11.904357 1.1.1 → host USB 64 URB_INTERRUPT in
<ここで再度 tshark の出力が一旦止まる。GtkTerm の終了処理が完了したのだろう。>
このログを更に詳細に解析してみようと思います。RTS や DSR などの制御信号や、「abc」や「bcd」の送受信の様子が分かるのが目標です。
まず一番簡単そうな「abc」を送信して「bcd」が受信される様子を解析します。なぜ簡単だと思ったかというと、送受信しているデータが既知であること、フレーム数が少ないことがあります。
解析に必要なデータとして tshark に -V -x
を付与して得た詳細なキャプチャを USB CDC機器のパケットキャプチャ出力例(PIC18F14K50サンプル) に載せました。ここから必要な表示を抜粋して解析していきます。
実験機器は、入力された文字の ASCII コードに 1 を加えた文字をエコーバックするという動作を繰り返します。すなわち、「a」を入力すると直後に「b」がエコーバックされ、「b」を入力すると「c」がエコーバックされる、ということです。ですので、「abc」を入力すると「bcd」がエコーバックされてきます。
という知識を前提にパケットの流れを見ると、どうやら 1 文字送受信するたびに 4 フレームが生成されているようです。「a」の送信と「b」の受信が Frame 61 から Frame 64 に相当するだろうと仮定をおいて、それら 4 フレームを詳しく見てみます。
Frame 61: 65 bytes on wire (520 bits), 65 bytes captured (520 bits) on interface 0
<中略>
0000 00 0c 32 98 ba 93 ff ff 53 03 02 16 01 00 2d 00 ..2.....S.....-.
0010 f2 b6 51 60 00 00 00 00 4f 89 08 00 8d ff ff ff ..Q`....O.......
0020 01 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 ................
0030 00 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 ................
0040 61 a
Frame 62: 64 bytes on wire (512 bits), 64 bytes captured (512 bits) on interface 0
<中略>
0000 00 0c 32 98 ba 93 ff ff 43 03 02 16 01 00 2d 3e ..2.....C.....->
0010 f2 b6 51 60 00 00 00 00 a5 89 08 00 00 00 00 00 ..Q`............
0020 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0030 00 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 ................
Frame 63: 65 bytes on wire (520 bits), 65 bytes captured (520 bits) on interface 0
<中略>
0000 40 fb 92 65 ba 93 ff ff 43 03 82 16 01 00 2d 00 @..e....C.....-.
0010 f2 b6 51 60 00 00 00 00 6b 8a 08 00 00 00 00 00 ..Q`....k.......
0020 01 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 ................
0030 00 00 00 00 00 00 00 00 04 02 00 00 00 00 00 00 ................
0040 62 b
Frame 64: 64 bytes on wire (512 bits), 64 bytes captured (512 bits) on interface 0
<中略>
0000 40 fb 92 65 ba 93 ff ff 53 03 82 16 01 00 2d 3c @..e....S.....-<
0010 f2 b6 51 60 00 00 00 00 7e 8a 08 00 8d ff ff ff ..Q`....~.......
0020 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0030 00 00 00 00 00 00 00 00 04 02 00 00 00 00 00 00 ................
Frame 61 と Frame 63 の 16 進数ダンプの最後に注目すると、こんな感じになっています。
- Frame 61: 0x61 (a)
- Frame 63: 0x62 (b)
おそらく、これが送受信した文字だろうと思います。そういう思いで Frame 65 から Frame 72 を見てみると、
- Frame 65: 0x62 (b)
- Frame 67: 0x63 (c)
- Frame 69: 0x63 (c)
- Frame 71: 0x64 (d)
となっています。うん、まさしくそうですね。
送受信されるデータには、この 1 バイトの他に 64 バイトもの余分なバイト列が付与されています。これは多分、usbmon のバイナリ形式 API で使われる、1 つのイベントを表す構造体だと思います。構造体のフォーマットは usbmon のドキュメント の「struct usbmon_packet」に書いてありました。構造体がちょうど 64 バイトなので、きっとこれでしょう。
次は Frame 1 から Frame 60 の内容を調べてみたいと思います。この部分は GtkTerm で /dev/ttyACM0 を開き、文字列を入力する直前までに該当します。USB 機器としての最低限の初期化は、USB 機器を USB ポートに接続した時点で完了しており、Frame 1 の時点では USB 機器としては初期化済みである点に注意します。
tshark のログ(概要表示の方)を見ると「host → 1.1.0」というような表示があります。これはホストマシンから、USB バス 1 に接続されたデバイス 1 番のエンドポイント 0 に向けた通信という意味です。筆者が使っているパソコンの USB バス 1 のデバイス 1 は「Linux Foundation 2.0 root hub」です。ルートハブとパソコン間で何やら多くのやりとりがありますね。
Frame 1 から Frame 24 までの通信は、どうやら Port 1 から Port 12 までの状態を取得しているようです。各 USB ポートにデバイスが接続されているか、接続されているなら通信速度(Low、Full、High)はどれか、というような状態を取得していることが、詳細な方のログを見ると分かります。これらの処理は CDC ACM の処理とは関係ないように思えます。Linux の USB ドライバは、ことあるごとに各ポートの状態を取得するような実装になっているのでしょうかね。
Frame 24 までのやりとりで、Port 1、6、10 に何らかのデバイスが接続されているということが判明します。Frame 25 から Frame 35 にかけては、Port 1 に対してステータス取得とステータス変更を行っているように見えます。PORT_SUSPEND フラグを 1→0 に変化させた後、C_PORT_SUSPEND フラグをクリアするような動作をしているので、ポートをサスペンド状態から動作状態に復帰させているように推測しました。
ホストとルートハブのやりとりが、あたかも USB バス上の通信かのように表示されているのは変な感じがします。ホストとルートハブは PCI バスで接続されているのであって、USB バスで繋がってはいないからです。詳細ログの方で詳しい値を追ってみても、どうやら USB の仕様書に書いてない値が登場しています。筆者の推測ですが、tshark(が利用する usbmon)が、USB バス上の通信と同一のフォーマットでホスト・ルートハブ間の通信を観察できるように工夫しているのかな、と思っています。
さて、Frame 35 までで、Port 1 にデバイスが接続されていることの確認、およびサスペンド状態からの復帰が完了しました。Frame 36 からは、いよいよホストと実験機器(デバイス 22)の通信が始まります。なお、Frame 36 から Frame 60 までの通信順序は、概要ログと詳細ログで順番が若干異なるようです(tshark を 2 回実行して得たログなので、順番がずれるのは仕方ありません)。照らし合わせるときに注意が必要です。
まず、概要ログの Frame 36 から 41 に着目します。
36 0.116150 host → 1.22.0 USB 64 GET STATUS Request -----.
37 0.116287 1.22.0 → host USB 66 GET STATUS Response <---'
38 0.116333 host → 1.1.0 USBHUB 64 GET_STATUS Request [Port 1] ----.
39 0.116338 host → 1.22.1 USB 64 URB_INTERRUPT in |
40 0.116351 host → 1.22.2 USB 64 URB_BULK in |
41 0.116353 1.1.0 → host USBHUB 68 GET_STATUS Response [Port 1] <---'
対応するリクエストとレスポンスが分かるように、矢印を記入してみました。まず Frame 36 と 37 の組を調べます。このリクエストは実験機器のデフォルトコントロールパイプに対する GET_STATUS リクエストです。それに対するレスポンスのダンプは次のようになっています。
0000 00 9c 77 22 b9 93 ff ff 43 02 80 16 01 00 2d 00 ..w"....C.....-.
0010 ed b6 51 60 00 00 00 00 cd 76 09 00 00 00 00 00 ..Q`.....v......
0020 02 00 00 00 02 00 00 00 00 00 00 00 00 00 00 00 ................
0030 00 00 00 00 00 00 00 00 00 02 00 00 00 00 00 00 ................
0040 00 00 ..
ダンプのうち先頭 64 バイトは URB 構造体ですので、実質的に実験機器から送られてきたデータは末尾 2 バイトとなるはずです。USB 2.0 の仕様でも GET_STATUS リクエストの返却値は 2 バイトと定められている(USB 2.0 仕様書の Figure 9-4)ので、これは正しいです。この 2 バイトのうち、下位 2 ビット以外は予約領域です。ビット 0 は「Self Powered」、ビット 1 は「Remote Wakeup」を表します。実験機器はバスパワー動作で、特にスリープしているわけではないので、どちらも値 0 となっていますね。
概要ログの Frame 38 と 41 の組は、詳細ログの方では Frame 56 と 57 に移動しているようです。さらに、概要ログの Frame 57 は詳細ログの Frame 55 に移動しています。lこんな気軽に移動されるということは、概要ログの Frame 38/41 の組は、実験機器の動作を解析するにあたりそれほど大切ではないと思います。無視します。
概要ログの Frame 39 は Interrupt In 転送を要求しています。Interrupt In 転送はデバイス→ホストの通信なのですが、通信開始のきっかけはホストが与えます。ホスト側から Interrupt In 転送リクエストを出しておいて、デバイス側に送信したいデータが発生した瞬間にレスポンスが返ってくる、という仕組みです。Frame 39 で出したリクエストに対するレスポンスは、概要ログの Frame 77 ですね。Frame 77 は「abc」と「bcd」の送受信が終わった後の通信ですので、とりあえず今は無視しても大丈夫そうです。
すると、残るは Frame 56 まで続く大量の Bulk In 転送と、最後に 4 回ある Control Out 転送ですね。
Bulk In 転送は合計で 16 回発行されています。概要ログで見る限り、同じ転送に見えるのですが、何か差があるのでしょうか。詳細ログから、2 つの Bulk In 転送のログを抜き出して、差分を確認してみます。USB URB の行から上は、そのフレームの番号とか、フレームが送信された時刻(前回のフレームからの経過秒数)とかの、フレームの内容に無関係の情報が出ていますので省略しました。
Frame 39: 64 bytes on wire (512 bits), 64 bytes captured (512 bits) on interface 0
<中略>
USB URB
[Source: host]
[Destination: 1.22.2]
URB id: 0xffff93ba6592fb40
URB type: URB_SUBMIT ('S')
URB transfer type: URB_BULK (0x03)
Endpoint: 0x82, Direction: IN
1... .... = Direction: IN (1)
.... 0010 = Endpoint number: 2
Device: 22
URB bus id: 1
Device setup request: not relevant ('-')
Data: not present ('<')
URB sec: 1615967981
URB usec: 620298
URB status: Operation now in progress (-EINPROGRESS) (-115)
URB length [bytes]: 128
Data length [bytes]: 0
[bInterfaceClass: Unknown (0xffff)]
Unused Setup Header
Interval: 0
Start frame: 0
Copy of Transfer Flags: 0x00000204
Number of ISO descriptors: 0
Frame 40 の差分は、次の 2 行だけでした。
URB id: 0xffff93ba6592f240
URB usec: 620300
ID や時間が変わるのは当然だと思いますから、実質的に差が無いと言えると思います。では、Bulk In は何のために 16 回も発行されているのでしょうか。これは全然分からなかったので、とりあえず放置することにしましょう。Linux 側のドライバが謎の受信をしているだけかもしれません。
最後に 4 回発生している URB_CONTROL について見ていきます。まず概要ログの Frame 57(詳細ログの 55)を見てみると、デフォルトコントロールパイプに対する Control Out 転送で、値は次の通りです。
bmRequestType: 0x21
0... .... = Direction: Host-to-device
.01. .... = Type: Class (0x1)
...0 0001 = Recipient: Interface (0x01)
bRequest: 34
wValue: 0x0003
wIndex: 0 (0x0000)
wLength: 0
bmRequestType = 0x21 かつ bRequest = 34 = 0x22 に対応するメッセージを CDC 仕様書から探すと、これは Set_Control_Line_State リクエストであることが分かります。wValue = 3 なので、DTR = 1 かつ RTS = 1 ということですね。ホスト側は準備完了しているのでデータを送ってください、という意味になります。
続く URB_CONTROL は先ほどのリクエストを無事に受理したよ、という返答です。
次に概要ログの Frame 59(詳細ログでも 59)を見ます。kろえも Control Out 転送で、値は次の通りです。
bmRequestType: 0x21
0... .... = Direction: Host-to-device
.01. .... = Type: Class (0x1)
...0 0001 = Recipient: Interface (0x01)
bRequest: 32
wValue: 0x0000
wIndex: 0 (0x0000)
wLength: 7
Data Fragment: 80250000000008
bmRequestType = 0x21 かつ bRequest = 32 = 0x20 は Set_Line_Coding リクエストです。このリクエストは仕様で 7 バイトのデータが伴うことになっていまして、実際のリクエストにも 7 バイトのデータ(80250000000008)が付いてますね。
このデータと CDC の仕様を照らし合わせると、次のように解釈できます。一般的な設定をしていることが分かりますね。
Offset | Size | 実際の値 | 意味 |
---|---|---|---|
0 | 4 | 0x2580 = 9600 | 転送速度(bits/sec) |
4 | 1 | 0 | ストップビット数(0 = 1、1 = 1.5、2 = 2) |
5 | 1 | 0 | パリティ(0 = 無し、1 = 奇数、2 = 偶数、3 = mark、4 = space) |
6 | 1 | 8 | データビット数(5, 6, 7, 8, 16) |
続く URB_CONTROL は先ほどのリクエストを無事に受理したよ、という返答です。
Frame 73 以降が接続終了時の通信です。先頭から見ていきます。
最初はおなじみ Control Out 転送で、値は次の通りです。
bmRequestType: 0x21
0... .... = Direction: Host-to-device
.01. .... = Type: Class (0x1)
...0 0001 = Recipient: Interface (0x01)
bRequest: 32
wValue: 0x0000
wIndex: 0 (0x0000)
wLength: 7
Data Fragment: 00c20100000008
これは Set_Line_Coding リクエストです。値を解釈すると次の通りになります。
Offset | Size | 実際の値 | 意味 |
---|---|---|---|
0 | 4 | 0x1c200 = 115200 | 転送速度(bits/sec) |
4 | 1 | 0 | ストップビット数(0 = 1、1 = 1.5、2 = 2) |
5 | 1 | 0 | パリティ(0 = 無し、1 = 奇数、2 = 偶数、3 = mark、4 = space) |
6 | 1 | 8 | データビット数(5, 6, 7, 8, 16) |
なぜ、接続を切断するときに 115,200bps に設定し直しているのかよく分かりませんね。続く Frame 74 はもちろんリクエストに対する返答です。
Frame 75 も Control Out 転送で、値は次の通りです。
bmRequestType: 0x21
0... .... = Direction: Host-to-device
.01. .... = Type: Class (0x1)
...0 0001 = Recipient: Interface (0x01)
bRequest: 34
wValue: 0x0000
wIndex: 0 (0x0000)
wLength: 0
これは Set_Control_Line_State リクエストです。wValue = 0 なので、DTR = 0 かつ RTS = 0 ということですね。ホスト側がデータ受信できない状態になったことを意味します。まあ、これから切断しようとしているのですから、自然な設定ですね。続く Frame 76 はリクエストに対する返答です。
Frame 77 は Interrupt In 転送です。通信方向が実験機器→ホストなので、これは Frame 39 の Interrupt In リクエストに対する応答ぽいです。詳細ログから URB id を確認すると、どちらも同じなので応答ということで確定です。インタラプト転送は、ホスト側から Interrupt In リクエストをかけておいて、デバイス側がデータを送りたいタイミングでリクエストに対する返答が来る、という通信のやり方です。Frame 39 以降、送るデータが無く、Frame 77 ではじめて応答が返ってきたというわけです。応答の中身を見るとデータは何も無いので、Interrupt In リクエストを仕掛けたものの、最後まで転送すべきデータは発生しなかった、ということでしょう。
Frame 78 から 16 個の Bulk In が並んでいます。これはおそらく、準備段階にある 16 個の Bulk In と関連がある気がします。準備段階での Bulk In 転送と通信方向が逆なので、もしかしたら 16 個の Bulk In に対する応答でしょうか。詳細ログで URB id を追いかけてみます。
Bulk In の URB id の時系列:
- host→1.22.2 Bulk In 要求 0xffff93ba6592fb40 = X
- host→1.22.2 Bulk In 要求 0xffff93ba6592f240 = Y
- host→1.22.2 Bulk In 要求 0xffff93ba6592f300 = Z
- host→1.22.2 Bulk In 要求 0xffff93ba6592fc00
- host→1.22.2 Bulk In 要求 0xffff93ba6592f000
- host→1.22.2 Bulk In 要求 0xffff93ba6592fd80
- host→1.22.2 Bulk In 要求 0xffff93ba6592f3c0
- host→1.22.2 Bulk In 要求 0xffff93ba6592fcc0
- host→1.22.2 Bulk In 要求 0xffff93ba6592f180
- host→1.22.2 Bulk In 要求 0xffff93ba6592f480
- host→1.22.2 Bulk In 要求 0xffff93ba6592f540
- host→1.22.2 Bulk In 要求 0xffff93ba973f2e40
- host→1.22.2 Bulk In 要求 0xffff93b8c6a0d540
- host→1.22.2 Bulk In 要求 0xffff93b8c6a0d000
- host→1.22.2 Bulk In 要求 0xffff93ba97185e40
- host→1.22.2 Bulk In 要求 0xffff93ba97185900
- host→1.22.2 Bulk Out 要求 0xffff93ba98320c00 = O(「a」の送信)
- 1.22.2→host Bulk Out 応答 0xffff93ba98320c00 = O
- 1.22.2→host Bulk In 応答 0xffff93ba6592fb40 = X(「b」の受信)
- host→1.22.2 Bulk In 要求 0xffff93ba6592fb40 = X
- host→1.22.2 Bulk Out 要求 0xffff93ba98320c00 = O(「b」の送信)
- 1.22.2→host Bulk Out 応答 0xffff93ba98320c00 = O
- 1.22.2→host Bulk In 応答 0xffff93ba6592f240 = Y(「c」の受信)
- host→1.22.2 Bulk In 要求 0xffff93ba6592f240 = Y
- host→1.22.2 Bulk Out 要求 0xffff93ba98320c00 = O(「c」の送信)
- 1.22.2→host Bulk Out 応答 0xffff93ba98320c00 = O
- 1.22.2→host Bulk In 応答 0xffff93ba6592f300 = Z(「d」の受信)
- host→1.22.2 Bulk In 要求 0xffff93ba6592f300 = Z
- 1.22.2→host Bulk In 応答 0xffff93ba6592fb40 = X
- 1.22.2→host Bulk In 応答 0xffff93ba6592f240 = Y
- 1.22.2→host Bulk In 応答 0xffff93ba6592f300 = Z
- 1.22.2→host Bulk In 応答 0xffff93ba6592fc00
- 1.22.2→host Bulk In 応答 0xffff93ba6592f000
- 1.22.2→host Bulk In 応答 0xffff93ba6592fd80
- 1.22.2→host Bulk In 応答 0xffff93ba6592f3c0
- 1.22.2→host Bulk In 応答 0xffff93ba6592fcc0
- 1.22.2→host Bulk In 応答 0xffff93ba6592f180
- 1.22.2→host Bulk In 応答 0xffff93ba6592f480
- 1.22.2→host Bulk In 応答 0xffff93ba6592f540
- 1.22.2→host Bulk In 応答 0xffff93ba973f2e40
- 1.22.2→host Bulk In 応答 0xffff93b8c6a0d540
- 1.22.2→host Bulk In 応答 0xffff93b8c6a0d000
- 1.22.2→host Bulk In 応答 0xffff93ba97185e40
- 1.22.2→host Bulk In 応答 0xffff93ba97185900
URB id は長くて見にくいので、いくつの URB id に別名(X、Y、Z、O)を付けてみました。
筆者が注目したのが「b」の受信(X)です。これは Bulk In 応答として実現されているのですが、もちろん「応答」なので、対応する「要求」が前にあるはずです。どこにあるかというと、準備段階に 16 個連続で送られた Bulk In 要求の先頭ですね。
これを見ると、Bulk In も Interrupt In と同様に、まずホストが Bulk In 要求をかけておいて、デバイス側に送信待機データが発生したときに応答が返ってくる、という仕組みだと分かります。ホストは応答を受信した直後、改めて Bulk In 要求を発行しなおしていますね。
次に「c」の受信(Y)を見てみます。これも同様に、準備段階で発行された Bulk In 要求に対する応答です。ホストは、この応答を受信した直後に Bulk In 要求を発行しています。
ここから分かるのは、Linux のデバイスドライバは、Bulk In 要求が常に 16 個になるように調整しているということです。最初に 16 個の Bulk In 要求を出した段階では、デバイスに 16 個の使用可能な Bulk In が貯まっている状態になっています。デバイスが何らかのデータを送信したくなり、1 つの Bulk In を選んで応答します。このとき、デバイスが利用可能な Bulk In 要求は 15 個に減ります。Bulk In 応答を受信した Linux デバイスドライバは、すかさず 1 つの Bulk In 要求を出し、デバイスが使用可能な Bulk In 要求を補充します。
事前に要求を出しておいて、デバイスの好きなタイミングで応答を返す、というのは Interrupt In と同じです。しかし、バルク転送は大量データ送信向けの転送モードですので、複数の Bulk In 要求を出しておく、という違いがあるようです。16 個という数が仕様で決まっているのか、Linux のデバイスドライバがたまたまその数なのかは分かりませんが、なんとなくバルク転送のやり方が分かりました!
通信終了時に並ぶ 16 個の Bulk In 応答は、中身を見ると送信データが空です。これは、おそらくデバイスにたまった 16 個の Bulk In 要求に、空の応答を返しているだけだと思います。malloc で確保したメモリを free で片付ける、というのと同じような、単なるリソース解放処理だろうと思います。