続いて 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 はエンドポイントディスクリプタに記述したエンドポイント番号と転送モードをいい感じに読み取って、勝手に設定してくれるという結論でした。
コメント
コロンビアからあいさつ。
Saludos desde Colombia.
Your article is the only one about the topic of the title.
すごいですね。
ありがとうございます。
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!