[STM32メモ] USBでCDCを2つ持つ複合デバイスを作る ディスクリプタ編(IADとか)

9年ほど前に、STM32のUSBライブラリとかCDCの仕様を調べていたことがあったが、結局あまり触れずじまいだった。最近とある事情でCDCを2つ持つ(VCP: Virtual COM Portでつながる)USBデバイスを作りたくなり、資料を読み返している。

中でも難解だったのが、複合デバイス(Composite Device)を作る際に必須となるIAD(Interface Association Descriptor)。もう色々な人が解説をしているだろうから需要はないかもしれないが、自分の理解のために書き残す。(ちゃんとした文書に準拠した用語を使えていない箇所があるかもしれないので、仕様書を確認のこと)

# サンプル
ディスクリプタを書いてCDCが2つある複合デバイスとして認識できるようにしたもの。一方のCOMポートは動作しない。STM32F072RBT6を搭載した、NUCLEO-F072RBボードで動作する。
https://github.com/stm32p103/stm32practice/releases/tag/0.0.1
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/3267e2d4-e295-cfde-c6bd-4822d5c36e06.png)

# 参考文献
* USB(2.0)仕様書: https://www.usb.org/document-library/usb-20-specification
 * usb_20.pdf
* CDC仕様書: https://www.usb.org/document-library/class-definitions-communication-devices-12
 * CDC120-20101103-track.pdf
 * PSTN120.pdf
* IAD参考資料: https://www.usb.org/sites/default/files/iadclasscode_r10.pdf
* 昔々書いたもの: https://sites.google.com/site/toriaezunomemo/home

# CDC: Communication Device Class
お題としてCDCを2つ持つ複合デバイスを作るにあたり、まずはCDCのサブクラスである、ACMのディスクリプタがどうなるかを把握する。

## CDCとは
厳密な定義はCDCの仕様書に任せるとして、CDCは「USBで通信をする類のデバイスの総称である」くらいの理解で十分かと思う。「[CDC仕様書: 3.4    Interface Definitions](https://www.usb.org/document-library/class-definitions-communication-devices-12)」によると、CDCは以下の2種類のインターフェースにより構成される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/f4d92395-b537-12b0-f14f-78ae3e6d8d36.png)


* Communications Class Interface
 * OUTエンドポイント: ホストからデバイスを管理するためのエンドポイント。(特別必要でなければEP0上でやり取りするので、必須ではない)
 * INエンドポイント: デバイスからホストへ通知するためのエンドポイント。
 * どちらも大抵はインタラプト転送を使う。CDCサブクラスの種類によって機能が異なる。デバイスにどんな機能があるかは、CDCクラス固有のDescriptor(Class-specific Descriptor)を使ってホストに伝える。 
* Data Class Interface
 * OUTエンドポイント: ホストからデバイスへのデータを送るエンドポイント。
 * INエンドポイント: デバイスからホストへデータを送るエンドポイント。
 * 「CDC仕様書: 3.5.2 Data Class Endpoint Requirement」によると、バルク転送またはアイソクロナス転送のペアであることが期待される。CDCの種類によっては使わない場合もある。

### ACM: Abstract Control Model とは
シリアル通信を模擬してVCPをつかって通信したい場合は、CDCのサブクラスであるPSTN(Public Switched Telephone Network)の「Abstract Control Model」を使用する([PSTN仕様書: 3.2.2.1 Abstract Control Model Serial Emulation参照](https://www.usb.org/document-library/class-definitions-communication-devices-12))。

ACMは、CDCの一般的な構成の通り、Communications Class InterfaceとData Class Interfaceから構成される。(以下PSTN仕様書の図参照)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/3a00d6e9-28ac-a0fc-8ecb-1a4856ce66a1.png)

## CDCのディスクリプタ
CDCのデバイスと、その構成を説明するディスクリプタは下図のように対比することができる。ここで、ただ`Interface Descriptor`を並べただけだと、`Interface Descriptor[0]`と`Interface Descriptor[1]`のどちらが`Communication Class Interface`と`Data Class Interface`に対応するのかが分からず、ホストはどちらにデータを送ってよいのか分からず困ってしまう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/24960ee3-36d9-fdb3-9fa7-4c7a03c1a23a.png)
> デバイスと対比するためのイメージのためにディスクリプタを包含関係があるように描いているが、実際のディスクリプタは1つ1つ分かれているので注意する。

そこで、CDCを構成するインターフェースの役割を説明するのが、図中の赤で示した`Functional Descriptor`である。`Functional Descriptor`には何種類かあり、中でもインターフェースの役割を説明するのに使われるのが`Union Functional Descriptor`である。他にも、機能の有無を説明する`ACM(Abstract Control Model) Functional Descriptor`がある。どういった`Functional Descriptor`があるかは、CDCやそのサブクラスの仕様書を参照すること。

`Union Functional Descriptor`は、CDC仕様書: 5.2.3.2 Union Functional Descriptor で説明のあるように、以下のフォーマットになっている。これにより、ディスクリプタ全体に含まれるインターフェースの何番目が`Communication Class Interface`や`Data Class Interface`に対応するかを説明できるようになっている。(表のSubordinate interfaceというのが`Data Class Interface`になるインターフェースを指す)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/6eb6f14e-147f-2fdb-b8d3-d8c83935b1ee.png)

### CDCのサンプルコードのディスクリプタ
STM32CubeIDEでCDC(VCP)のコードを生成することができる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/c033a114-164d-26c7-e2a7-1ccf8a9b32c8.png)

コード生成を実行すると、`Device Descriptor`はusbd_desc.cに、`Configuration Descriptor`以降はusbd_cdc.cに配列が生成される。`Configuration Descriptor`以降を見てみると、以下のようになっている(コメントは編集している)。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/322bbf0c-262f-838c-837a-6fba959930de.png)


```C:usbd_cdc.c抜粋
/* USB CDC device Configuration Descriptor */
__ALIGN_BEGIN uint8_t USBD_CDC_CfgHSDesc[USB_CDC_CONFIG_DESC_SIZ] __ALIGN_END =
{
  /* Configuration Descriptor[0] -------------------------------------------- */
  0x09,                         /* bLength: Configuration Descriptor size     */
  USB_DESC_TYPE_CONFIGURATION,  /* bDescriptorType: Configuration             */
  USB_CDC_CONFIG_DESC_SIZ,      /* wTotalLength:no of returned bytes          */
  0x00,
  0x02,                         /* bNumInterfaces: 2 interface                */
  0x01,                         /* bConfigurationValue: Configuration value   */
  0x00,                         /* iConfiguration: Index of string descriptor */
  0xC0,                         /* bmAttributes: self powered                 */
  0x32,                         /* MaxPower 0 mA                              */

  /* Interface Descriptor[1] ------------------------------------------------ */
  /* Communication Class Interface                                            */
  0x09,                     /* bLength: Interface Descriptor size             */
  USB_DESC_TYPE_INTERFACE,  /* bDescriptorType: Interface                     */
  0x00,                     /* bInterfaceNumber: Number of Interface          */
  0x00,                     /* bAlternateSetting: Alternate setting           */
  0x01,                     /* bNumEndpoints: One endpoints used              */
  0x02,                     /* bInterfaceClass: Communication Interface Class */
  0x02,                     /* bInterfaceSubClass: Abstract Control Model     */
  0x01,                     /* bInterfaceProtocol: Common AT commands         */
  0x00,                     /* iInterface: -                                  */

  /* Functional Descriptor[0] ----------------------------------------------- */
  /* Header Functional Descriptor                                             */
  0x05,   /* bLength: Endpoint Descriptor size                                */
  0x24,   /* bDescriptorType: CS_INTERFACE                                    */
  0x00,   /* bDescriptorSubtype: Header Func Desc                             */
  0x10,   /* bcdCDC: spec release number                                      */
  0x01,

  /* Functional Descriptor[1] ----------------------------------------------- */
  /* Call Management Functional Descriptor                                    */
  0x05,   /* bFunctionLength                                                  */
  0x24,   /* bDescriptorType: CS_INTERFACE                                    */
  0x01,   /* bDescriptorSubtype: Call Management Func Desc                    */
  0x00,   /* bmCapabilities: D0+D1 (no call management supported)             */
  0x01,   /* bDataInterface: Interface[1]                                     */

  /* Functional Descriptor[2] ----------------------------------------------- */
  /* ACM Functional Descriptor                                                */
  0x04,   /* bFunctionLength                                                  */
  0x24,   /* bDescriptorType: CS_INTERFACE                                    */
  0x02,   /* bDescriptorSubtype: Abstract Control Management desc             */
  0x02,   /* bmCapabilities: *                                                */

  /* Functional Descriptor[3] ----------------------------------------------- */
  /* Union Functional Descriptor                                              */
  0x05,   /* bFunctionLength                                                  */
  0x24,   /* bDescriptorType: CS_INTERFACE                                    */
  0x06,   /* bDescriptorSubtype: Union func desc                              */
  0x00,   /* bMasterInterface: Communication class interface = Interface[0]   */
  0x01,   /* bSlaveInterface0: Data Class Interface = Interface[1]            */

  /* Endpoint Descriptor[0] ------------------------------------------------- */
  0x07,                                /* bLength: Endpoint Descriptor size   */
  USB_DESC_TYPE_ENDPOINT,              /* bDescriptorType: Endpoint           */
  CDC_CMD_EP,                          /* bEndpointAddress: IN 2              */
  0x03,                                /* bmAttributes: Interrupt             */
  LOBYTE(CDC_CMD_PACKET_SIZE),         /* wMaxPacketSize:                     */
  HIBYTE(CDC_CMD_PACKET_SIZE),
  CDC_HS_BINTERVAL,                    /* bInterval:                          */

  /* Interface Descriptor[1] ------------------------------------------------ */
  /* Data class interface descriptor                                          */
  0x09,                              /* bLength: Endpoint Descriptor size     */
  USB_DESC_TYPE_INTERFACE,           /* bDescriptorType: Endpoint             */
  0x01,                              /* bInterfaceNumber: Number of Interface */
  0x00,                              /* bAlternateSetting: Alternate setting  */
  0x02,                              /* bNumEndpoints: Two endpoints used     */
  0x0A,                              /* bInterfaceClass: CDC                  */
  0x00,                              /* bInterfaceSubClass: -                 */
  0x00,                              /* bInterfaceProtocol: -                 */
  0x00,                              /* iInterface: -                         */

  /* Endpoint Descriptor[1] ------------------------------------------------- */
  0x07,                                /* bLength: Endpoint Descriptor size   */
  USB_DESC_TYPE_ENDPOINT,              /* bDescriptorType: Endpoint           */
  CDC_OUT_EP,                          /* bEndpointAddress: OUT 1             */
  0x02,                                /* bmAttributes: Bulk                  */
  LOBYTE(CDC_DATA_HS_MAX_PACKET_SIZE), /* wMaxPacketSize:                     */
  HIBYTE(CDC_DATA_HS_MAX_PACKET_SIZE),
  0x00,                                /* bInterval: ignore for Bulk transfer */

  /* Endpoint Descriptor[2] ------------------------------------------------- */
  0x07,                                /* bLength: Endpoint Descriptor size   */
  USB_DESC_TYPE_ENDPOINT,              /* bDescriptorType: Endpoint           */
  CDC_IN_EP,                           /* bEndpointAddress: IN 1              */
  0x02,                                /* bmAttributes: Bulk                  */
  LOBYTE(CDC_DATA_HS_MAX_PACKET_SIZE), /* wMaxPacketSize:                     */
  HIBYTE(CDC_DATA_HS_MAX_PACKET_SIZE),
  0x00                                 /* bInterval: ignore for Bulk transfer */
};
```

# 複合デバイス
前置きがだいぶ長くなったが、USBで定義されたCDCやMSD(Mass Storage Device)、HID(Human Interface Device)を複数備えるデバイスのディスクリプタをどう定義すればよいかをまとめる。

## Device Descriptor
複合デバイスはその名の通り「複合デバイス」というデバイスクラスであるため、`Device Descriptor`は以下のようになる。デバイスクラスは0xEF: Miscellaneous Device(雑多なデバイス?)である。([IAD参考資料参照](https://www.usb.org/sites/default/files/iadclasscode_r10.pdf))
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/a7184a5b-7103-fe75-b2a8-6bb418829131.png)

## Configuration Descriptor
通常のデバイスと同様なので、割愛する。

## IAD: Interface Association Descriptor
複合デバイスでは、コンフィギュレーションに含まれる複数のインターフェースが連携してデバイスに相当する機能を実現する。例えばCDCの場合、`Communication Class Interface`と`Data Class Interface`合わせて2つのインターフェースが連携して機能を実現する。CDCを2つ持つ複合デバイスであれば4つのインターフェースの2つずつが連携して、それぞれCDCの機能を実現する。

複合デバイスでは、コンフィギュレーションに含まれる複数のインターフェスに関して、「どれが連携してどんな機能を定義するか」を説明するために、IAD(Interface Associate Descriptor)というディスクリプタを用いる。これは、CDCでUnion Functional Descriptorを用いてインターフェースの役割を説明したのに似ている。そのイメージは以下のようになる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/30b9047e-41cc-a480-634d-f2c0a54c8861.png)


IADの構成は以下のようになっている。連携するインターフェースは`bFirstInterface`と`bInterfaceCount`を用いて説明する。例えば0番ねのインターフェースから2つのインターフェースが連携するなら、それぞれ`0x00`,`0x02`となる。インターフェースが連携してどんな機能を実現するかは、`bFunctionClass`、`bFunctionSubClass`、`bFunctionProtocol`を用いて説明する。これはデバイスのクラス、サブクラス・プロトコルに相当するもので、例えばCDC(VCP)であればそれぞれ`0x02`,`0x02`,`0x00`になる。([IAD参考資料参照](https://www.usb.org/sites/default/files/iadclasscode_r10.pdf))
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/31132122-19e2-8b4e-5268-4ee4ee96ddd5.png)

## 以降のディスクリプタ
複合デバイスではIADというディスクリプタを用いること以外は特別なことはなく、`Interface Descriptor`や`Endpoint Descriptor`は通常のデバイスクラスの時と同様に記述すればよい。

# 試しに複合デバイスのディスクリプタを書く
STM32のCDCサンプルコードを少し改造するだけで、なんちゃって複合デバイスにすることができる。CDCを2つ搭載させるには、ディスクリプタだけでなく色々な所に手を加えないといけないので、ここではディスクリプタの説明にとどめる。

## CDCが1つしかない複合デバイスにする
簡単な例としてCDCサンプルコードを改造し、以下のようなイメージでCDCが1つしかない複合デバイスを作ってみる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/7ba5e6ca-0f00-2ca9-c5f6-03a9174966d5.png)

### Device Descriptorを変更する
まず、`usbd_desc.c`で定義されている`Device Descriptor`を、CDCから複合デバイスに書き換える。以下の★のところがCDCサンプルコードから変えた個所となる。

```C:usbd_desc.c
/** USB standard device descriptor. */
__ALIGN_BEGIN uint8_t USBD_FS_DeviceDesc[USB_LEN_DEV_DESC] __ALIGN_END =
{
  0x12,                       /*bLength */
  USB_DESC_TYPE_DEVICE,       /*bDescriptorType*/
  0x00,                       /*bcdUSB */
  0x02,
  0xEF,                       /*bDeviceClass: Miscellaneous Device Class ★*/
  0x02,                       /*bDeviceSubClass: Common Class ★*/
  0x01,                       /*bDeviceProtocol: Interface Association Descriptor ★*/
  USB_MAX_EP0_SIZE,           /*bMaxPacketSize*/
  LOBYTE(USBD_VID),           /*idVendor*/
  HIBYTE(USBD_VID),           /*idVendor*/
  LOBYTE(USBD_PID_FS),        /*idProduct*/
  HIBYTE(USBD_PID_FS),        /*idProduct*/
  0x00,                       /*bcdDevice rel. 2.00*/
  0x02,
  USBD_IDX_MFC_STR,           /*Index of manufacturer  string*/
  USBD_IDX_PRODUCT_STR,       /*Index of product string*/
  USBD_IDX_SERIAL_STR,        /*Index of serial number string*/
  USBD_MAX_NUM_CONFIGURATION  /*bNumConfigurations*/
};
```

### Configuration Descriptorを変更する
次に、`usbd_cdc.c`で定義されている`Configuration Descriptor`を書き換える。(実際には順番に制約はないらしいのだが)ここでは、CDCサンプルのディスクリプタに対して、IADを`Configuration Descriptor`と`Interface Descriptor[0]`の間に追加する。配列サイズを決める`USB_CDC_CONFIG_DESC_SIZ`は、IADで8バイト増加することから`67U`から`75U`に書き換える。USBの速度(FS/HS/その他)ごとに`Configuratgion Descriptor`があるので、全部変えるように気を付ける。

```C:usbd_cdc.c
/* USB CDC device Configuration Descriptor */
__ALIGN_BEGIN uint8_t USBD_CDC_CfgFSDesc[USB_CDC_CONFIG_DESC_SIZ] __ALIGN_END =
{
    /* Configuration Descriptor[0] -------------------------------------------- */
    0x09,                         /* bLength: Configuration Descriptor size     */
    USB_DESC_TYPE_CONFIGURATION,  /* bDescriptorType: Configuration             */
    USB_CDC_CONFIG_DESC_SIZ,      /* wTotalLength:no of returned bytes          */
    0x00,
    0x02,                         /* bNumInterfaces: 2 interface                */
    0x01,                         /* bConfigurationValue: Configuration value   */
    0x00,                         /* iConfiguration: Index of string descriptor */
    0xC0,                         /* bmAttributes: self powered                 */
    0x32,                         /* MaxPower 0 mA                              */

    /* IAD: Interface Association Descriptor[0] ------------------------------- */
    0x08,                         /* bLength: IAD size                          */
    0x0B,                         /* bDescriptorType: IAD                       */
    0x00,                         /* bFirstInterface: Interface[0]              */
    0x02,                         /* bInterfaceCount: 2 interfaces              */
    0x02,                         /* bFunctionClass: CDC Class                  */
    0x02,                         /* bFunctionSubClass: ACM                     */
    0x00,                         /* bFunctionProtocol: -                       */
    0x00,                         /* iFunction: -                               */

    /* Interface Descriptor[1] ------------------------------------------------ */
    /* Communication Class Interface                                            */
    0x09,                     /* bLength: Interface Descriptor size             */
    USB_DESC_TYPE_INTERFACE,  /* bDescriptorType: Interface                     */
    0x00,                     /* bInterfaceNumber: Number of Interface          */
    0x00,                     /* bAlternateSetting: Alternate setting           */
    0x01,                     /* bNumEndpoints: One endpoints used              */
    0x02,                     /* bInterfaceClass: Communication Interface Class */
    0x02,                     /* bInterfaceSubClass: Abstract Control Model     */
    0x01,                     /* bInterfaceProtocol: Common AT commands         */
    0x00,                     /* iInterface: -                                  */

    /* Functional Descriptor[0] ----------------------------------------------- */
    /* Header Functional Descriptor                                             */
    0x05,   /* bLength: Endpoint Descriptor size                                */
    0x24,   /* bDescriptorType: CS_INTERFACE                                    */
    0x00,   /* bDescriptorSubtype: Header Func Desc                             */
    0x10,   /* bcdCDC: spec release number                                      */
    0x01,

    /* Functional Descriptor[1] ----------------------------------------------- */
    /* Call Management Functional Descriptor                                    */
    0x05,   /* bFunctionLength                                                  */
    0x24,   /* bDescriptorType: CS_INTERFACE                                    */
    0x01,   /* bDescriptorSubtype: Call Management Func Desc                    */
    0x00,   /* bmCapabilities: D0+D1 (no call management supported)             */
    0x01,   /* bDataInterface: Interface[1]                                     */

    /* Functional Descriptor[2] ----------------------------------------------- */
    /* ACM Functional Descriptor                                                */
    0x04,   /* bFunctionLength                                                  */
    0x24,   /* bDescriptorType: CS_INTERFACE                                    */
    0x02,   /* bDescriptorSubtype: Abstract Control Management desc             */
    0x02,   /* bmCapabilities: *                                                */

    /* Functional Descriptor[3] ----------------------------------------------- */
    /* Union Functional Descriptor                                              */
    0x05,   /* bFunctionLength                                                  */
    0x24,   /* bDescriptorType: CS_INTERFACE                                    */
    0x06,   /* bDescriptorSubtype: Union func desc                              */
    0x00,   /* bMasterInterface: Communication class interface = Interface[0]   */
    0x01,   /* bSlaveInterface0: Data Class Interface = Interface[1]            */

    /* Endpoint Descriptor[0] ------------------------------------------------- */
    0x07,                                /* bLength: Endpoint Descriptor size   */
    USB_DESC_TYPE_ENDPOINT,              /* bDescriptorType: Endpoint           */
    CDC_CMD_EP,                          /* bEndpointAddress: IN 2              */
    0x03,                                /* bmAttributes: Interrupt             */
    LOBYTE(CDC_CMD_PACKET_SIZE),         /* wMaxPacketSize:                     */
    HIBYTE(CDC_CMD_PACKET_SIZE),
    CDC_HS_BINTERVAL,                    /* bInterval:                          */

    /* Interface Descriptor[1] ------------------------------------------------ */
    /* Data class interface descriptor                                          */
    0x09,                              /* bLength: Endpoint Descriptor size     */
    USB_DESC_TYPE_INTERFACE,           /* bDescriptorType: Endpoint             */
    0x01,                              /* bInterfaceNumber: Number of Interface */
    0x00,                              /* bAlternateSetting: Alternate setting  */
    0x02,                              /* bNumEndpoints: Two endpoints used     */
    0x0A,                              /* bInterfaceClass: CDC                  */
    0x00,                              /* bInterfaceSubClass: -                 */
    0x00,                              /* bInterfaceProtocol: -                 */
    0x00,                              /* iInterface: -                         */

    /* Endpoint Descriptor[1] ------------------------------------------------- */
    0x07,                                /* bLength: Endpoint Descriptor size   */
    USB_DESC_TYPE_ENDPOINT,              /* bDescriptorType: Endpoint           */
    CDC_OUT_EP,                          /* bEndpointAddress: OUT 1             */
    0x02,                                /* bmAttributes: Bulk                  */
    LOBYTE(CDC_DATA_HS_MAX_PACKET_SIZE), /* wMaxPacketSize:                     */
    HIBYTE(CDC_DATA_HS_MAX_PACKET_SIZE),
    0x00,                                /* bInterval: ignore for Bulk transfer */

    /* Endpoint Descriptor[2] ------------------------------------------------- */
    0x07,                                /* bLength: Endpoint Descriptor size   */
    USB_DESC_TYPE_ENDPOINT,              /* bDescriptorType: Endpoint           */
    CDC_IN_EP,                           /* bEndpointAddress: IN 1              */
    0x02,                                /* bmAttributes: Bulk                  */
    LOBYTE(CDC_DATA_HS_MAX_PACKET_SIZE), /* wMaxPacketSize:                     */
    HIBYTE(CDC_DATA_HS_MAX_PACKET_SIZE),
    0x00                                 /* bInterval: ignore for Bulk transfer */
};
```

### 実行してみる
上記のようにディスクリプタを修正してソフトを書きこみ実行すると、作成したUSBデバイスが複合デバイスとして認識されることが分かる。(ベンダID:1155 = 0x0483から見つけられる)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/afced3ec-f9cb-5698-0d37-b27493476975.png)

## CDCが2つある複合デバイスにする
実際に動作させるにはコーディングが必要だが、ひとまずCDCが2つあると認識させるようディスクリプタを書き換える。`Device Descriptor`は前の例のままで、`Configuration Descriptor`を変更する。

### Configuration Descriptorを変更する
CDCが1つある複合デバイスの`Configuration Descriptor`に、もう一つCDCを追加する。以下コードの`IAD: Interface Association Descriptor[1]`からが追加したCDCのディスクリプタとなる。`Configuration Descriptor`全体として、インターフェースの総数(2→4)やディスクリプタ長(75→141)などが変わっているので不整合が無いように注意して編集する。

```C:usbd_cdc.c
/* USB CDC device Configuration Descriptor */
__ALIGN_BEGIN uint8_t USBD_CDC_CfgFSDesc[USB_CDC_CONFIG_DESC_SIZ] __ALIGN_END =
{
    /* Configuration Descriptor[0] -------------------------------------------- */
    0x09,                         /* bLength: Configuration Descriptor size     */
    USB_DESC_TYPE_CONFIGURATION,  /* bDescriptorType: Configuration             */
    USB_CDC_CONFIG_DESC_SIZ,      /* wTotalLength:no of returned bytes          */
    0x00,
    0x04,                         /* bNumInterfaces: 4 interfaces               */
    0x01,                         /* bConfigurationValue: Configuration value   */
    0x00,                         /* iConfiguration: Index of string descriptor */
    0xC0,                         /* bmAttributes: self powered                 */
    0x32,                         /* MaxPower 0 mA                              */

    /* IAD: Interface Association Descriptor[0] ------------------------------- */
    0x08,                         /* bLength: IAD size                          */
    0x0B,                         /* bDescriptorType: IAD                       */
    0x00,                         /* bFirstInterface: Interface[0]              */
    0x02,                         /* bInterfaceCount: 2 interfaces              */
    0x02,                         /* bFunctionClass: CDC Class                  */
    0x02,                         /* bFunctionSubClass: ACM                     */
    0x00,                         /* bFunctionProtocol: -                       */
    0x00,                         /* iFunction: -                               */

    /* Interface Descriptor[1] ------------------------------------------------ */
    /* Communication Class Interface                                            */
    0x09,                     /* bLength: Interface Descriptor size             */
    USB_DESC_TYPE_INTERFACE,  /* bDescriptorType: Interface                     */
    0x00,                     /* bInterfaceNumber: Number of Interface          */
    0x00,                     /* bAlternateSetting: Alternate setting           */
    0x01,                     /* bNumEndpoints: One endpoints used              */
    0x02,                     /* bInterfaceClass: Communication Interface Class */
    0x02,                     /* bInterfaceSubClass: Abstract Control Model     */
    0x01,                     /* bInterfaceProtocol: Common AT commands         */
    0x00,                     /* iInterface: -                                  */

    /* Functional Descriptor[0] ----------------------------------------------- */
    /* Header Functional Descriptor                                             */
    0x05,   /* bLength: Endpoint Descriptor size                                */
    0x24,   /* bDescriptorType: CS_INTERFACE                                    */
    0x00,   /* bDescriptorSubtype: Header Func Desc                             */
    0x10,   /* bcdCDC: spec release number                                      */
    0x01,

    /* Functional Descriptor[1] ----------------------------------------------- */
    /* Call Management Functional Descriptor                                    */
    0x05,   /* bFunctionLength                                                  */
    0x24,   /* bDescriptorType: CS_INTERFACE                                    */
    0x01,   /* bDescriptorSubtype: Call Management Func Desc                    */
    0x00,   /* bmCapabilities: D0+D1 (no call management supported)             */
    0x01,   /* bDataInterface: Interface[1]                                     */

    /* Functional Descriptor[2] ----------------------------------------------- */
    /* ACM Functional Descriptor                                                */
    0x04,   /* bFunctionLength                                                  */
    0x24,   /* bDescriptorType: CS_INTERFACE                                    */
    0x02,   /* bDescriptorSubtype: Abstract Control Management desc             */
    0x02,   /* bmCapabilities: *                                                */

    /* Functional Descriptor[3] ----------------------------------------------- */
    /* Union Functional Descriptor                                              */
    0x05,   /* bFunctionLength                                                  */
    0x24,   /* bDescriptorType: CS_INTERFACE                                    */
    0x06,   /* bDescriptorSubtype: Union func desc                              */
    0x00,   /* bMasterInterface: Communication class interface = Interface[0]   */
    0x01,   /* bSlaveInterface0: Data Class Interface = Interface[1]            */

    /* Endpoint Descriptor[0] ------------------------------------------------- */
    0x07,                                /* bLength: Endpoint Descriptor size   */
    USB_DESC_TYPE_ENDPOINT,              /* bDescriptorType: Endpoint           */
    CDC_CMD_EP,                          /* bEndpointAddress: OUT 2             */
    0x03,                                /* bmAttributes: Interrupt             */
    LOBYTE(CDC_CMD_PACKET_SIZE),         /* wMaxPacketSize:                     */
    HIBYTE(CDC_CMD_PACKET_SIZE),
    CDC_HS_BINTERVAL,                    /* bInterval:                          */

    /* Interface Descriptor[1] ------------------------------------------------ */
    /* Data class interface descriptor                                          */
    0x09,                              /* bLength: Endpoint Descriptor size     */
    USB_DESC_TYPE_INTERFACE,           /* bDescriptorType: Endpoint             */
    0x01,                              /* bInterfaceNumber: Number of Interface */
    0x00,                              /* bAlternateSetting: Alternate setting  */
    0x02,                              /* bNumEndpoints: Two endpoints used     */
    0x0A,                              /* bInterfaceClass: CDC                  */
    0x00,                              /* bInterfaceSubClass: -                 */
    0x00,                              /* bInterfaceProtocol: -                 */
    0x00,                              /* iInterface: -                         */

    /* Endpoint Descriptor[1] ------------------------------------------------- */
    0x07,                                /* bLength: Endpoint Descriptor size   */
    USB_DESC_TYPE_ENDPOINT,              /* bDescriptorType: Endpoint           */
    CDC_OUT_EP,                          /* bEndpointAddress: OUT 1             */
    0x02,                                /* bmAttributes: Bulk                  */
    LOBYTE(CDC_DATA_HS_MAX_PACKET_SIZE), /* wMaxPacketSize:                     */
    HIBYTE(CDC_DATA_HS_MAX_PACKET_SIZE),
    0x00,                                /* bInterval: ignore for Bulk transfer */

    /* Endpoint Descriptor[2] ------------------------------------------------- */
    0x07,                                /* bLength: Endpoint Descriptor size   */
    USB_DESC_TYPE_ENDPOINT,              /* bDescriptorType: Endpoint           */
    CDC_IN_EP,                           /* bEndpointAddress: IN 1              */
    0x02,                                /* bmAttributes: Bulk                  */
    LOBYTE(CDC_DATA_HS_MAX_PACKET_SIZE), /* wMaxPacketSize:                     */
    HIBYTE(CDC_DATA_HS_MAX_PACKET_SIZE),
    0x00,                                /* bInterval: ignore for Bulk transfer */

    /* IAD: Interface Association Descriptor[1] ------------------------------- */
    0x08,                         /* bLength: IAD size                          */
    0x0B,                         /* bDescriptorType: IAD                       */
    0x02,                         /* bFirstInterface: Interface[2]              */
    0x02,                         /* bInterfaceCount: 2 interfaces              */
    0x02,                         /* bFunctionClass: CDC Class                  */
    0x02,                         /* bFunctionSubClass: ACM                     */
    0x00,                         /* bFunctionProtocol: -                       */
    0x00,                         /* iFunction: -                               */

    /* Interface Descriptor[2] ------------------------------------------------ */
    /* Communication Class Interface                                            */
    0x09,                     /* bLength: Interface Descriptor size             */
    USB_DESC_TYPE_INTERFACE,  /* bDescriptorType: Interface                     */
    0x02,                     /* bInterfaceNumber: Number of Interface          */
    0x00,                     /* bAlternateSetting: Alternate setting           */
    0x01,                     /* bNumEndpoints: One endpoints used              */
    0x02,                     /* bInterfaceClass: Communication Interface Class */
    0x02,                     /* bInterfaceSubClass: Abstract Control Model     */
    0x01,                     /* bInterfaceProtocol: Common AT commands         */
    0x00,                     /* iInterface: -                                  */

    /* Functional Descriptor[4] ----------------------------------------------- */
    /* Header Functional Descriptor                                             */
    0x05,   /* bLength: Endpoint Descriptor size                                */
    0x24,   /* bDescriptorType: CS_INTERFACE                                    */
    0x00,   /* bDescriptorSubtype: Header Func Desc                             */
    0x10,   /* bcdCDC: spec release number                                      */
    0x01,

    /* Functional Descriptor[5] ----------------------------------------------- */
    /* Call Management Functional Descriptor                                    */
    0x05,   /* bFunctionLength                                                  */
    0x24,   /* bDescriptorType: CS_INTERFACE                                    */
    0x01,   /* bDescriptorSubtype: Call Management Func Desc                    */
    0x00,   /* bmCapabilities: D0+D1 (no call management supported)             */
    0x03,   /* bDataInterface: Interface[3]                                     */

    /* Functional Descriptor[6] ----------------------------------------------- */
    /* ACM Functional Descriptor                                                */
    0x04,   /* bFunctionLength                                                  */
    0x24,   /* bDescriptorType: CS_INTERFACE                                    */
    0x02,   /* bDescriptorSubtype: Abstract Control Management desc             */
    0x02,   /* bmCapabilities: *                                                */

    /* Functional Descriptor[7] ----------------------------------------------- */
    /* Union Functional Descriptor                                              */
    0x05,   /* bFunctionLength                                                  */
    0x24,   /* bDescriptorType: CS_INTERFACE                                    */
    0x06,   /* bDescriptorSubtype: Union func desc                              */
    0x02,   /* bMasterInterface: Communication class interface = Interface[2]   */
    0x03,   /* bSlaveInterface0: Data Class Interface = Interface[3]            */

    /* Endpoint Descriptor[3] ------------------------------------------------- */
    0x07,                                /* bLength: Endpoint Descriptor size   */
    USB_DESC_TYPE_ENDPOINT,              /* bDescriptorType: Endpoint           */
    0x84,                                /* bEndpointAddress: IN 4              */
    0x03,                                /* bmAttributes: Interrupt             */
    LOBYTE(CDC_CMD_PACKET_SIZE),         /* wMaxPacketSize:                     */
    HIBYTE(CDC_CMD_PACKET_SIZE),
    CDC_HS_BINTERVAL,                    /* bInterval:                          */

    /* Interface Descriptor[3] ------------------------------------------------ */
    /* Data class interface descriptor                                          */
    0x09,                              /* bLength: Endpoint Descriptor size     */
    USB_DESC_TYPE_INTERFACE,           /* bDescriptorType: Endpoint             */
    0x03,                              /* bInterfaceNumber: Number of Interface */
    0x00,                              /* bAlternateSetting: Alternate setting  */
    0x02,                              /* bNumEndpoints: Two endpoints used     */
    0x0A,                              /* bInterfaceClass: CDC                  */
    0x00,                              /* bInterfaceSubClass: -                 */
    0x00,                              /* bInterfaceProtocol: -                 */
    0x00,                              /* iInterface: -                         */

    /* Endpoint Descriptor[4] ------------------------------------------------- */
    0x07,                                /* bLength: Endpoint Descriptor size   */
    USB_DESC_TYPE_ENDPOINT,              /* bDescriptorType: Endpoint           */
    0x03,                                /* bEndpointAddress: OUT 3             */
    0x02,                                /* bmAttributes: Bulk                  */
    LOBYTE(CDC_DATA_HS_MAX_PACKET_SIZE), /* wMaxPacketSize:                     */
    HIBYTE(CDC_DATA_HS_MAX_PACKET_SIZE),
    0x00,                                /* bInterval: ignore for Bulk transfer */

    /* Endpoint Descriptor[5] ------------------------------------------------- */
    0x07,                                /* bLength: Endpoint Descriptor size   */
    USB_DESC_TYPE_ENDPOINT,              /* bDescriptorType: Endpoint           */
    0x83,                                /* bEndpointAddress: IN 3              */
    0x02,                                /* bmAttributes: Bulk                  */
    LOBYTE(CDC_DATA_HS_MAX_PACKET_SIZE), /* wMaxPacketSize:                     */
    HIBYTE(CDC_DATA_HS_MAX_PACKET_SIZE),
    0x00                                 /* bInterval: ignore for Bulk transfer */
};
```

### 実行してみる
マイコンにプログラムを着込んで実行すると、以下のようにデバイスマネージャから複合デバイスとして認識されることが確認できる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/950f0054-822f-8457-228e-7551205a0f5b.png)

また、USBコネクタを抜き差しすると、COM3とCOM9が追加・削除されることが確認できる。(5はST-LINK内蔵、COM1は元々ついている別のデバイスのCOMポート)。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/1922d2a2-3a10-63a1-b1b1-9a5da2eaf421.png)⇔![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/3267e2d4-e295-cfde-c6bd-4822d5c36e06.png)

なお、この時点ではディスクリプタで「こんなデバイスがあります」とPCに認識させただけに過ぎないため、一方のCOMポートは動作しない。この後、ちゃんとコーディングしていくことで使えるようにしていくが、それはまた別の機会に...。

#まとめ
USBの複合デバイスのディスクリプタの作り方をまとめた。複合デバイスを作るにはまず`Device Descriptor`を複合デバイスのクラス(0xEF)にする。そして、`Configuration Descriptor`以下で、インターフェースのまとまりごとに、IADを用いて「どのインターフェースが連携してどんな機能を実現するか」を記述すればよい。実際に動作させるにはコーディングが必要だが、ディスクリプタを正しく書ければひとまず認識させることはできる。

#備考
原文をみると、`Communications Class Interface`と書かれていたのだが、本文や図で間違って`Communication Class Interface`と書いてしまっているので注意。