SNSへはこちら

ラズピコでTinyUSBやってみよう(2) - USB HIDの構造

続いて USB HID を簡易的に実装してみます。本記事も、実際の動作に漕ぎ着けるところまでは行かずに、TinyUSB の構造や動作の仕組みを理解するものになります。本格的な実装はもうちょっと待ってね。

ディスクリプタの準備

前回の記事をベースにしていきます。

設定をする必要があるのは HID レポートディスクリプタコンフィグレーションディスクリプタなのですが、ここで超絶便利なマクロを見つけましたので、ちゃっかりと使用します。

const uint8_t hid_report_desc[] = {
    TUD_HID_REPORT_DESC_GENERIC_INOUT(1)
};

const uint8_t conf_desc[] = {
    TUD_CONFIG_DESCRIPTOR(1, 1, 0, TUD_CONFIG_DESC_LEN + TUD_HID_INOUT_DESC_LEN, 0, 0x0F),
    TUD_HID_INOUT_DESCRIPTOR(1, 0, 0, sizeof(hid_report_desc), 0x01, 0x81, 64, 0x0F)
};

すごいですよね。ありがたい。これだけ見ても「はぁ?」だと思いますので、記述されていたファイル名と定義を載せておきます。

TUD_CONFIG_DESCRIPTOR: tinyusb/src/class/hid/hid_device.h

// HID Generic Input & Output
// - 1st parameter is report size (mandatory)
// - 2nd parameter is report id HID_REPORT_ID(n) (optional)
#define TUD_HID_REPORT_DESC_GENERIC_INOUT(report_size, ...)


TUD_CONFIG_DESCRIPTOR: tinyusb/src/device/usbd.h

// Config number, interface count, string index, total length, attribute, power in mA
#define TUD_CONFIG_DESCRIPTOR(config_num, _itfcount, _stridx, _total_len, _attribute, _power_ma)


TUD_HID_INOUT_DESCRIPTOR: tinyusb/src/device/usbd.h

// HID Input & Output descriptor
// Interface number, string index, protocol, report descriptor len, EP OUT & IN address, size & polling interval
#define TUD_HID_INOUT_DESCRIPTOR(_itfnum, _stridx, _boot_protocol, _report_desc_len, _epout, _epin, _epsize, _ep_interval)

tusb_config.hの編集

HID 関連の機能を有効化するために、96行目付近のコンフィグを有効化します。

//------------- CLASS -------------//
#define CFG_TUD_CDC               0
#define CFG_TUD_MSC               0
#define CFG_TUD_HID               1
#define CFG_TUD_MIDI              0
#define CFG_TUD_VENDOR            0

コールバック関数の用意(取り敢えず)

これによって追加のコールバック関数が必要になりました。例によって関数の型が分からないのですが、ソースコードを見た上で追記してあげましょう。

uint8_t const *tud_hid_descriptor_report_cb(void) {
    return hid_report_desc;
}
uint16_t tud_hid_get_report_cb(uint8_t report_id, hid_report_type_t report_type, uint8_t* buffer, uint16_t reqlen) {
    return 0;
}
void tud_hid_set_report_cb(uint8_t report_id, hid_report_type_t report_type, uint8_t const* buffer, uint16_t bufsize) {
    return;
}

はい、前回同様何もしない感じです。取り敢えず認識されるかチェックしたいじゃないですか。

main関数

これまたシンプル。雛形通りでOK。これだけで動き出します。データの受け渡しとか一切書いていないですが、

int main() {
    board_init();
    tusb_init();

    while(1) {
        tud_task();
    }
    return 0;
}

USB Prober の認識は以下のような感じ。出来てそうですね。

USBの初期化について

ここで疑問があると思います。それは「HID のパイプ通信で使うエンドポイントってどうなってるの??」という疑問。
上の初期化コードでは読み書きともに EP1 を使用することにしたのですが、これ何処で決まっているんでしょう。結論から言うと、TinyUSB 側で、記述したディスクリプタ(先程決め打ちした EP1 IN/OUT)の情報を元に、いい感じにエンドポイントの設定をしてくれます

この疑問を晴らすために、ソースコードを読みます。なお階層的構造がかなり深く、分かりづらいですので飛ばしていただいても結構です。というか、記事の残りはこの説明のみです。

tud_taskと関数ポインタテーブル

while ループ内で呼んでいる関数です。tinyusb/src/device/usbd.c にあるこの中身を見ると、process_control_request を呼んでいて、更にその中で process_set_config を呼んでいます。その中で、次のような記述があります。

      uint16_t const drv_len = driver->open(rhport, desc_itf, remaining_len);

この driver->open がキモです。 driver は以下の関数ポインタテーブルから代入されるものです。

// Built-in class drivers
static usbd_class_driver_t const _usbd_driver[] =
{
...
  #if CFG_TUD_HID
  {
      DRIVER_NAME("HID")
      .init             = hidd_init,
      .reset            = hidd_reset,
      .open             = hidd_open,
      .control_request  = hidd_control_request,
      .control_complete = hidd_control_complete,
      .xfer_cb          = hidd_xfer_cb,
      .sof              = NULL
  },
  #endif
...
};

この open というメンバが呼ばれているわけですね。実態は hidd_open のようですよ。

hidd_openとその中身の奥深く

この関数は tinyusb/src/class/hid/hid_device.c にあって、一部抜粋して示します。

uint16_t hidd_open(uint8_t rhport, tusb_desc_interface_t const * desc_itf, uint16_t max_len)
{
...
  uint8_t const *p_desc = (uint8_t const *) desc_itf;

  //------------- HID descriptor -------------//
  p_desc = tu_desc_next(p_desc);
  p_hid->hid_descriptor = (tusb_hid_descriptor_hid_t const *) p_desc;
  TU_ASSERT(HID_DESC_TYPE_HID == p_hid->hid_descriptor->bDescriptorType, 0);

  //------------- Endpoint Descriptor -------------//
  p_desc = tu_desc_next(p_desc);
  TU_ASSERT(usbd_open_edpt_pair(rhport, p_desc, desc_itf->bNumEndpoints, TUSB_XFER_INTERRUPT, &p_hid->ep_out, &p_hid->ep_in), 0);
...
}

何やら色々していますが、第2引数の型にご注目ください。usb_desc_interface_t * ですね。これはその名の通り、インターフェイスディスクリプタを差しています。そしてここからポインタを先に進めていくことで、その中身の解釈に移ります。

tu_desc_next と言う関数は正直良くわかっていませんが、おそらく bLength を見てその分ポインタをインクリメントする関数だと考えられます。そうすると一連のコンフィギュレーションディスクリプタで、各種従属するディスクリプタごとに一律に見ることが出来ますね。

ここでポイントとなるのはエンドポイントディスクリプタです。このブロックでは tu_desc_next した後に usbd_open_edpt_pair を呼んでいます。そしてその戻り値でアサートしているようです。ここでもポイントは引数の p_desc です。エンドポイントディスクリプタのポインタを渡しています。


お次は usbd_open_edpt_pair。こちらは tinyusb/src/device/usbd.c 内にあって、更に usbd_edpt_open を呼び、更に dcd_edpt_open をほぼそのまま呼び出しています。

dcd_edpt_open は tinyusb/src/portable/raspberrypi/rp2040/dcd_rp2040.c にあって、こちらも hw_endpoint_init を呼び、_hw_endpoint_init を呼んでいます。そして _hw_endpoint_init を呼ぶタイミングで、エンドポイントディスクリプタの記述した bmAttributes を最後の引数として渡しています。つまりここで、記述したエンドポイントディスクリプタのエンドポイント番号と転送モードが渡されます

なんかゴチャゴチャしてきましたね。でも単純に関数を呼びまくっているだけです。条件分岐とかは有りません。

やっとゴールが見えてきました。 _hw_endpoint_init 内部では ep という構造体ポインタに先程の bmAttributes を書き込んだ上で、_hw_endpoint_alloc を呼びます。ここでやっと USB ペリフェラルのレジスタへ書き込みをします

static void _hw_endpoint_alloc(struct hw_endpoint *ep)
{
...
    // Fill in endpoint control register with buffer offset
    uint32_t reg =  EP_CTRL_ENABLE_BITS
                  | EP_CTRL_INTERRUPT_PER_BUFFER
                  | (ep->transfer_type << EP_CTRL_BUFFER_TYPE_LSB)
                  | dpram_offset;

    *ep->endpoint_control = reg;
}

実は内部で色々していますが、ここでレジスタへ書き込みです。ep->transfer_type に関しては bmAttributes で、これにより値が反映されていることがわかります。

ということで、結果として TinyUSB はエンドポイントディスクリプタに記述したエンドポイント番号と転送モードをいい感じに読み取って、勝手に設定してくれるという結論でした。

コメント

  1. コロンビアからあいさつ。
    Saludos desde Colombia.
    Your article is the only one about the topic of the title.
    すごいですね。
    ありがとうございます。

    • shima-529 より:

      Thank you very much for your compliments!!

      I am curious about serching into the low layer. However no article could be found on the title... so I stepped in its source codes.
      This is why I posted this article!