SNSへはこちら

STM32でUSBをベアメタル(6) - 各種ディスクリプタを整える

本記事の参考文献は(だいたい全て) USB の規格書です。

ディスクリプタ

今回は取り敢えず(この単語何回使った?)、String Descriptor を整えていきたいと思います。
ひとまずまとめとして、これまで雑にしか掲載していなかったディスクリプタの定義や宣言も詳細にしていきたいと思います。

デバイスディスクリプタ

苦労して、ホストに読ませることが成功したデバイスディスクリプタですが、以下の構造体定義でやってます。

typedef struct {
    uint8_t bLength;
    uint8_t bDescriptorType;
    uint16_t bcdUSB;
    uint8_t bDeviceClass;
    uint8_t bDeviceSubClass;
    uint8_t bDeviceProtocol;
    uint8_t bMaxPacketSize0;
    uint16_t idVendor;
    uint16_t idProduct;
    uint16_t bcdDevice;
    uint8_t iManufacturer;
    uint8_t iProduct;
    uint8_t iSerialNumber;
    uint8_t bNumConfigurations;
} USBDeviceDescriptor;
enum {
    DEVICE_DESCRIPTOR_LENGTH = 18,
    DEVICE_DESCRIPTOR_TYPE = 1
};

下の enumbLengthbDescriptorType 用の定数です。マクロとして使う感じ。
そして、この実体は1つのみです。

// For USB CDC
const USBDeviceDescriptor dev_desc = {
        .bLength = DEVICE_DESCRIPTOR_LENGTH,
        .bDescriptorType = DEVICE_DESCRIPTOR_TYPE,
        .bcdUSB = 0x200, // USB2.0
        .bDeviceClass = 2, // Communication
        .bDeviceSubClass = 0, // tekitou
        .bDeviceProtocol = 0,
        .bMaxPacketSize0 = 64,
        .idVendor = 0xdead,
        .idProduct = 0xbeef,
        .bcdDevice = 0x100,
        .iManufacturer = 2,
        .iProduct = 3,
        .iSerialNumber = 1,
        .bNumConfigurations = 1
};

コメントで tekitou ともあるとおり、ここらへんの値は今後も随時変更していくかも知れません。前回以前より iManufacturer, iProduct, iSerialNumber の値を変えています。
このように i のみが先頭につくフィールドは、(おそらく)ストリングディスクリプタのインデックスを表していると思われます。このインデックスは、ストリングディスクリプタそのものに記載されるということはなく、あくまでホストとデバイス間での対応関係を表すものに過ぎません。 また、0 を指定すると対応するストリングディスクリプタはないという意味になるので、避けました。

ストリングディスクリプタ

i で始まるフィールドのための文字列を記載したディスクリプタです。まあ言語関係のことを表すこともあるらしいのですが、よくわかりません(暴論)。
構造体定義はこちら。要素数は適当です。

typedef struct {
    uint8_t bLength;
    uint8_t bDescriptorType;
    uint16_t wLANGID[20];
} USBStringDescriptor;
enum {
    STRING_DESCRIPTOR_TYPE = 3
};

bLength は実際の文字数に依存するので可変長となっています。wLANGID は(面倒なので)名前をそのままにしていますが、ここに文字を入れます。型が uint16_t なのですが、これは1文字を 16bits で表現する UCS-2 で表現するためとなります。C++ ならここらへんをいい感じにできるでしょうが、C言語は世界の「文字とは」を理解しない人たちが作った言語規格ですので、char 固定になってしまっていますよね。でも実体は異なるので、文字リテラルで表すことは出来ません。悲しきや。
実体はこんな感じで定義します。index が 0 のものは USB 規格上存在しないことになっているので、便宜上ダミーのインスタンスとしました。このダミーは使用しません。

const USBStringDescriptor string_desc[10] = {
        {}, // dummy
        { // 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'}
        },
};

例によって文字が汚いですね。普段からこういう汚い表現を使っていることがバレてしまいます。例えばテストで作ってみるファイル名が fuck.c とかね。許して

プログラムの改変

ということで、複数のストリングディスクリプタ等が出来始めたので、送受信を行うプログラム部分の改変を行いたいと思います。

構造体

まずは最近良く使っている PMA アドレスオフセットを記録している構造体。今回追加部分を記述します。

// Vars for PMA buffer
typedef struct {
    uint16_t setup_packet;
    uint16_t device_desc;
    uint16_t config_desc;
    uint16_t string_desc[10];
} PMAOffsets;
typedef struct {
    PMAOffsets offsets;
    uint16_t sp;
} PMAInfo;
volatile static PMAInfo pma_info;

string_desc は今後いちいち調整をする手間が増えないように、10 くらいに設定しておきました。

関数

対応するディスクリプタへのポインタを送信バッファへセットする関数の中身です。CONFIGURATION は適当に、STRING はしっかりと対応しています。

static void search_descriptor(uint16_t wValue) {
    const uint8_t desc_type = (wValue >> 8) & 0xFF;
    const uint8_t desc_no = wValue & 0xFF;
    const uint16_t wLength = g_last_setup.wLength;
    int len = string_desc[desc_no].bLength;
    volatile PMAOffsets *offsets = &pma_info.offsets;

    switch( desc_type ) {
    case DEVICE: // Device Descriptor
        if( offsets->device_desc == 0 ) {
            offsets->device_desc = alloc(sizeof(USBDeviceDescriptor));
            pma_in(offsets->device_desc, &dev_desc, sizeof(USBDeviceDescriptor));
        }
        next_xfer.ptr = offsets->device_desc;
        next_xfer.len = MIN(wLength, sizeof(USBDeviceDescriptor));
        break;
    case CONFIGURATION:
        if( offsets->config_desc == 0 ) {
            offsets->config_desc = alloc(sizeof(USBConfigurationDescriptor));
            pma_in(offsets->config_desc, &conf_desc, sizeof(USBConfigurationDescriptor));
        }
        next_xfer.ptr = offsets->config_desc;
        next_xfer.len = MIN(wLength, sizeof(USBConfigurationDescriptor));
        break;
    case STRING:
        if( offsets->string_desc[desc_no] == 0 ) {
            offsets->string_desc[desc_no] = alloc(len);
            pma_in(offsets->string_desc[desc_no], &string_desc[desc_no], len);
        }
        next_xfer.ptr = offsets->string_desc[desc_no];
        next_xfer.len = MIN(wLength, len);
        GPIOB->ODR |= 1 << 1;
        break;
//  case INTERFACE:
//      break;
//  case ENDPOINT:
//      break;
    default:
        break;
    }
}

結果

それでは通信してみましょう。メイン部分のソースコードはこちらです

今回から、Mac の Xcode に付属していた USB Prober にて確認していきたいと思います。これを使うと接続状況やトポロジー、ディスクリプタ情報がわかる代物です。
書き込んでリセットすると...

はい。こんな感じでストリングディスクリプタが反映されていることが分かると思います。良かったです。

残るはコンフィギュレーションディスクリプタですが、これは USB CDC のお勉強を終えた後に実装したいと思います。ありがとうございました。