SNSへはこちら

STM32でUSBをベアメタル(7) - コンフィギュレーションディスクリプタの用意

CDC の勉強をしてから、というのはちょっと無理がありそうなので(後述するように方針転換します)、取り敢えずコピペで動かすことをします。

コピペ

HAL から持ってきました。USB-FS の USB CDC コンフィギュレーションディスクリプタです。

__attribute__((aligned(2)))
const uint8_t USBD_CDC_CfgHSDesc[] =
{
  /*Configuration Descriptor*/
  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 describing the configuration */
  0xC0,   /* bmAttributes: self powered */
  0x32,   /* MaxPower 0 mA */

  /*---------------------------------------------------------------------------*/

  /*Interface Descriptor */
  0x09,   /* bLength: Interface Descriptor size */
  USB_DESC_TYPE_INTERFACE,  /* bDescriptorType: Interface */
  /* Interface descriptor type */
  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: */

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

  /*Call Management Functional Descriptor*/
  0x05,   /* bFunctionLength */
  0x24,   /* bDescriptorType: CS_INTERFACE */
  0x01,   /* bDescriptorSubtype: Call Management Func Desc */
  0x00,   /* bmCapabilities: D0+D1 */
  0x01,   /* bDataInterface: 1 */

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

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

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

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

  /*Endpoint OUT Descriptor*/
  0x07,   /* bLength: Endpoint Descriptor size */
  USB_DESC_TYPE_ENDPOINT,      /* bDescriptorType: Endpoint */
  CDC_OUT_EP,                        /* bEndpointAddress */
  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 IN Descriptor*/
  0x07,   /* bLength: Endpoint Descriptor size */
  USB_DESC_TYPE_ENDPOINT,      /* bDescriptorType: Endpoint */
  CDC_IN_EP,                         /* bEndpointAddress */
  0x02,                              /* bmAttributes: Bulk */
  LOBYTE(CDC_DATA_HS_MAX_PACKET_SIZE),  /* wMaxPacketSize: */
  HIBYTE(CDC_DATA_HS_MAX_PACKET_SIZE),
  0x00                               /* bInterval: ignore for Bulk transfer */
} ;

マクロがありますが、そのへんは適切なヘッダを参照します。ところで、コンフィギュレーションディスクリプタの要求をする次の SETUP パケットをご覧ください。

** SETUP
bmResuestType: 128
bRequest: 6
wValue: 0x200
wIndex: 0x0
wLength: 0x9
IN
STATUS_OUT
** SETUP
bmResuestType: 128
bRequest: 6
wValue: 0x200
wIndex: 0x0
wLength: 0x43

1回目にまず先頭 9bytes を要求し、次にその 9bytes 中に含まれていた .bLength == 67 bytes 分の長さを要求しています。ちょっと問題がありますね。というのは、このエンドポイントはバッファ長が 64bytes なので、1回の通信で収まりません
とすると、ホストは複数回に分けて IN パケットを出すので、それに応答できるようにソフトウェアを組めば良いという訳ですね。既存のコードを変える必要がありそう。やってみましょう。

複数回のデータ送信に対応するコードを書く

本来なら受信も対応したコードを書くべきですが、まあとりあえずやめておきましょう。

構造体にまとめる

ではまず、若干コードの整理を。と同時に、複数回送受信に対応できるようにメンバを追加していきます。

typedef struct {
    uint16_t ptr;
    uint16_t current_pos;
    uint16_t len;
} DataInfo;
typedef struct {
    uint8_t packet_size; // 原則64固定
    PacketBuffer *pb_ptr; // addr_txとかのアレ
    DataInfo xfer_info;
    DataInfo recv_info;
    EP0_fsm fsm; // FSM もまとめる
    USBSetupPacket last_setup; // 最近取得したSETUPパケットデータ
} USBEndpointInfo;

volatile static USBEndpointInfo ep0_info;

この DataInfo 型のメンバを使うことで複数回送受信に対応できるようにします。メンバの機能は以下を意図しています。

  • ptr: データの先頭アドレス。
  • current_pos: 送受信を何処から始めるかを示す。複数回通信毎に位置を移動させていく。
  • len: 送受信すべきデータの全長。複数回通信毎に減らしていく。

FSMを使用して状態遷移を改変する

現状、次のパケット情報は FSM で管理しているので、その遷移関数を変えましょう。

static EP0_fsm next_state_ep0(void) {
    const volatile USBSetupPacket *last_setup = &ep0_info.last_setup;
    const volatile DataInfo *xfer_info = &ep0_info.xfer_info;
//  const volatile DataInfo *recv_info = &ep0_info.recv_info;
    switch(ep0_info.fsm) {
    case SETUP:
        if( last_setup->bmRequestType & 0x80 ) {
            return (last_setup->wLength != 0) ? IN : STATUS_IN;
        }else{
            return (last_setup->wLength != 0) ? OUT : STATUS_IN;
        }
        break;
    case IN:
        if( xfer_info->len > ep0_info.packet_size ) {
            return IN;
        }
        return STATUS_OUT;
        break;
    case OUT:
        return STATUS_IN;
        break;
    case STATUS_IN:
        break;
    case STATUS_OUT:
        break;
    default:
        break;
    }
    return ST_ERROR;
}

IN のところがこれまで無条件で STATUS_OUT 遷移でしたが、今回の実装に伴ってデータ長によって再度 IN に遷移するようにしています。

割り込み関数

割り込み関数の中身も変えます。IN の部分。

}else if( ep0_info.fsm == IN ) {
    iprintf("IN\n");
    ep0_info.fsm = next_state_ep0();
    usb_ep0_handle_in(); // ADD
    usb_ep0_prepare_for_status();
}

usb_ep0_handle_in という関数を追加しました。これは次のトランザクションも IN の時 (=複数回通信の時) に、データポインタ等の操作を行う関数です。
ではこの関数の中身です。

static void usb_ep0_handle_in(void) {
    // Now data is left to be xferred.
    // This means the last IN transaction is completed but the data length is not enough.
    if( ep0_info.fsm != IN ) return; // malfunction of fsm
    const uint16_t packet_size = ep0_info.packet_size;
    const uint16_t len_to_be_xferred = ep0_info.xfer_info.len - packet_size;
//  const uint16_t pos = ep0_info.xfer_info.current_pos;

    if( len_to_be_xferred > 0 && len_to_be_xferred <= packet_size ) { // IN transaction will end this time.
        ep0_info.xfer_info.current_pos += packet_size;
        ep0_info.xfer_info.len -= packet_size;

        usb_ep0_send(ep0_info.xfer_info.current_pos, ep0_info.xfer_info.len);
    }else{
        // unsupported.
    }

}

先程の説明通りのことをしているのみです。

通信させてみる

さて、以上の変更を加えて通信させてみます。以下がログの一部です。

** SETUP
bmResuestType: 128
bRequest: 6
wValue: 0x200
wIndex: 0x0
wLength: 0x9
IN
STATUS_OUT
** SETUP
bmResuestType: 128
bRequest: 6
wValue: 0x200
wIndex: 0x0
wLength: 0x43
IN
IN
STATUS_OUT
** SETUP
bmResuestType: 0
bRequest: 9
wValue: 0x1
wIndex: 0x0
wLength: 0x0
SET_CONF: 0x0001
** SETUP
bmResuestType: 128
bRequest: 8
wValue: 0x0
wIndex: 0x0
wLength: 0x1
IN
STATUS_OUT
** SETUP
bmResuestType: 128
bRequest: 6
wValue: 0x300
wIndex: 0x0
wLength: 0xff
IN
STATUS_OUT

wLength == 0x43 の所を見てください。IN が2回来ているので、確かに通信できていることが分かります。やったね!
続いて、GET_CONFIGURATION と SET_CONFIGURATION が来ていますね。ここらへんはよく分からないので、次回また対応します。取り敢えず通信は続いているのですが、bRequest == 6(GET_DESCRIPTOR) でかつ .wValue == 0x300 になってますね。3 で ストリングディスクリプタを要求されているのは分かりますが、LSBが 0x00 になっています。これは LANGID を返す必要があるようです。

LANGIDの実装

EU とかの面倒くさい地方の影響でしょうか。国の情報を書かねばならないらしいです。必須ではなさそうですが、年のためにやっておきます。
上で最後の通信において wValue: 0x300 となっていますが、wValue の LSB 8bit が0の時、ストリングディスクリプタの何らかの文字情報というより、LANGID を要求されていると言うことになります。

日本を探す

ここは日本であり、日本語という感じなので(適当)、ja-JP を示す LANDID を見つければいいっぽいです。
LANGID は USB.org で公開されていたようですが、今はなんと Microsoft のサイトで公開されているようです。リンクはこちら

そこから PDF をダウンロードして、ja-JP を探します。どうやら 0x411 のようです
以前は String Descriptor の配列で、0番目の値はダミーとして無視していましたが、ちょうどそのダミーの位置に LANGID 情報を記述すれば良いようです。下のコードで一番上の要素がそれになります。

const USBStringDescriptor string_desc[10] = {
        { // Language Information
                .bLength = 2 + 2,
                .bDescriptorType = STRING_DESCRIPTOR_TYPE,
                .wLANGID = 0x411 // ja-JP
        },
        { // Serial Number
                .bLength = 4 * 2 + 2,
                .bDescriptorType = STRING_DESCRIPTOR_TYPE,
                .wLANGID = {'T', 'E', 'S', 'T'}
        },
        { // Manufacturer
                .bLength = 10 * 2 + 2,
                .bDescriptorType = STRING_DESCRIPTOR_TYPE,
                .wLANGID = {'S', 'h', 'i', 't', ' ', 'C', 'o', 'r', 'p', '.'}
        },
        { // Product
                .bLength = 15 * 2 + 2,
                .bDescriptorType = STRING_DESCRIPTOR_TYPE,
                .wLANGID = {'U', 'n', 'c', 'o', 'm', 'm', 'u', 'n', 'i', 'c', 'a', 't', 'i', 'o', 'n'}
        },
};

そうして通信をすると、ja-JP 用のストリングディスクリプタ送出を要求されますが、取り敢えず現状そのままで対応できる構成になっているので、何もしなくても勝手に適切なストリングディスクリプタを送る処理をしてくれます。面倒臭くなくてよかった。

方針変更!!

で、割と重要なお知らせですが、先程 USB CDC 用のディスクリプタ群をコピペしました。しかし、「あれ、CDC って結構闇じゃね?」って思ったので、急ですが方針転換したいと思います

CDC が正直色々情報が多すぎたり、どのディスクリプタを実装すれば良いのかという情報がうまく見つけられなかったりしたので、「やめたい」という気持ちが高まってきました。
一方、マウスやキーボードに代表される USB HID クラスは比較的簡単な構成であるということがなんとなく分かりました。

ということで、一旦 USB CDC の勉強及び実装はやめて、マウスの実装を進めていく方針に転換します。次回以降もよろしくお願いいたします。

上に示すように、方針変更をしましたが、まずは USB HID の勉強をします。

今回実装したコードはこちら