TinyUSB について
最近、USB のスタックで評判の TinyUSB を実験してみた。
現状の実装では、主に以下のような感じとなっている。
- オープンソースである。
- 非常に多くのマイコンに対応している。
- RX マイコンも、デバイス(クライアント)、ホストのドライバーが用意されている。
- 主にデバイスの機能が充実している。(PC に繋ぐ、USB 機器を作る場合)
- USB ホストは実装が始まったばかりで、あまり枯れていないよう。
- 全体の構成が判りやすく、サンプルやドキュメントが充実している。
- 機能実装、バグ修正などが GitHub で行われており、将来性が有望。
- 「Tiny」となっているが、プロジェクトはかなり大きい。
- API の仕様が明確で適切なので、マイコンの種別に関係無く、ソースコードの再利用が出来る。(ここが大きい!)
など、かなり魅力的なプロジェクトとなっている。
RX マイコン用のドライバーもあるので、自分の環境で実験してみた。
※当初、ホストのドライバーが無かったが、現在は、コミットされている。
RX マイコン用ドライバーは、Koji KITAYAMA さんが実装しているようで、御礼申し上げます。
自分のシステムに取り込む場合
自分の C++ フレームワークに取り込むには、多少の改造が必要なので、その過程を記す。
- TinyUSB には、サンプルプログラムが沢山あるが、RX マイコンで使う場合、主に CC-RX 環境に依存しているので、gcc で使うには問題がある。
- そこで、最小限の改造で、使うようにする為、自分のシステムに必要な部分だけ取り込んで実験した。
- とりあえず、ホスト機能を使い、ゲームパッドやキーボードを利用する事が目的となっている。
- 非常に機能が豊富なので、他も試していきたい。
RX マイコンのオフィシャルな対応としては、RX63N、RX65N、RX72N となっているが、基本、USB ペリフェラルは、ほぼ同じ構成なので、RX64M、RX71M、RX66T、RX72T など多くのデバイスにも使えると思われる。
※基本 USB2.0 系で、LowSpeed(1.5Mbps)、FullSpeed(12Mbps)に対応していれば良いようである。
※ RX631/RX63N はマイコン側が LowSpeed に対応しない。
※ RX64M/RX71M の HighSpeed(480Mbps)には対応していない。
USB0 のクロック設定
USB0 ペリフェラルのクロック源には 48MHz が必要なので、クロックジェネレーターの設定を調整する。
RX72N では、内部 PLL を 240MHz にすれば、48MHz は 1/5 にすれば良い。
自分のフレームワークでは、clock_profile.hpp で、以下の設定をして、boost_master_clock() を呼べば、あとは自動で設定してくれる。
class clock_profile {
public:
static constexpr bool TURN_USB = true; ///< USB を使う場合「true」
static constexpr uint32_t BASE = 16'000'000; ///< 外部接続クリスタル
static constexpr uint32_t PLL_BASE = 240'000'000; ///< PLL ベースクロック(最大240MHz)
static constexpr uint32_t ICLK = 240'000'000; ///< ICLK 周波数(最大240MHz)
static constexpr uint32_t PCLKA = 120'000'000; ///< PCLKA 周波数(最大120MHz)
static constexpr uint32_t PCLKB = 60'000'000; ///< PCLKB 周波数(最大60MHz)
static constexpr uint32_t PCLKC = 60'000'000; ///< PCLKC 周波数(最大60MHz)
static constexpr uint32_t PCLKD = 60'000'000; ///< PCLKD 周波数(最大60MHz)
static constexpr uint32_t FCLK = 60'000'000; ///< FCLK 周波数(最大60MHz)
static constexpr uint32_t BCLK = 120'000'000; ///< BCLK 周波数(最大120MHz)
};
※48MHz が作れない設定を行うと、コンパイルを失敗する。
static constexpr bool TURN_USB = true; ///< USB を使う場合「true」
static constexpr uint32_t BASE = 16'000'000; ///< 外部接続クリスタル
static constexpr uint32_t PLL_BASE = 200'000'000; ///< PLL ベースクロック(最大240MHz)
static constexpr uint32_t ICLK = 200'000'000; ///< ICLK 周波数(最大240MHz)
static constexpr uint32_t PCLKA = 100'000'000; ///< PCLKA 周波数(最大120MHz)
static constexpr uint32_t PCLKB = 50'000'000; ///< PCLKB 周波数(最大60MHz)
static constexpr uint32_t PCLKC = 50'000'000; ///< PCLKC 周波数(最大60MHz)
static constexpr uint32_t PCLKD = 50'000'000; ///< PCLKD 周波数(最大60MHz)
static constexpr uint32_t FCLK = 50'000'000; ///< FCLK 周波数(最大60MHz)
static constexpr uint32_t BCLK = 100'000'000; ///< BCLK 周波数(最大120MHz)
---
../RX600/system_io.hpp:228:34: error: static assertion failed: USB Clock can't divided.
static_assert(usb_div_() >= 2 && usb_div_() <= 5, "USB Clock can't divided.");
~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~
../RX600/system_io.hpp:230:44: error: conversion from 'long unsigned int' to 'device::rw16_t<524324>::value_type' {aka 'short unsigned int'} changes value from '4294967295' to '65535' [-Werror=overflow]
device::SYSTEM::SCKCR2.UCK = usb_div_() - 1;
~~~~~~~~~~~^~~
cc1plus.exe: all warnings being treated as errors
make: *** [Makefile:193: release/main.o] エラー 1
USB0 のポート設定
RX72N Envision Kit では、USB の関係ポートとして、以下の2ポートを利用している。
- USB0_VBUSEN(P16) USB 機器の電源制御ポート
- USB0_OVRCURB(P14) 外部接続機器の電流制限がオーバーした場合を通知するポート
現在の実装では「オーバーカレント」は観ていないようなので、USB0_VBUSEN のみ設定を行う。
ポート設定は、現在は、ポートマップのオーダーを「SECOND」にする事で、RX72N Envision Kit に対応したもになるようにしてある。
とりあえず、tinyusb_mng クラスを用意して、対応している。
//-----------------------------------------------------------------//
/*!
@brief 開始
@param[in] ilvl 割り込みレベル
@return 成功なら「true」
*/
//-----------------------------------------------------------------//
bool start(uint8_t ilvl) noexcept
{
if(ilvl == 0) { // 割り込み無しはエラー
return false;
}
power_mgr::turn(USB_CH::PERIPHERAL);
// VBUSEN, OVERCURA ピンの設定
if(!port_map::turn(USB_CH::PERIPHERAL, true, PSEL)) {
return false;
}
ivec_ = icu_mgr::set_interrupt(USB_CH::I_VEC, i_task_, ilvl);
// utils::format("USB clock divider: 0b%04b\n") % static_cast<uint16_t>(device::SYSTEM::SCKCR2.UCK());
// utils::format("USB0 interrupt vector: %u\n") % static_cast<uint16_t>(ivec_);
tuh_init(BOARD_TUH_RHPORT);
return true;
}
---
// RX72N Envision Kit
typedef device::tinyusb_mng<device::USB0, device::port_map::ORDER::SECOND> TINYUSB;
TINYUSB tinyusb_;
USB0 の省電力設定
USB0 の省電力設定解除は、ドライバーでされているが、自分のシステムで行いたいので、コメントアウトした。
tinyusb/src/portable/renesas/usba/hcd_usba.c
bool hcd_init(uint8_t rhport)
{
(void)rhport;
/* Enable USB0 */
// uint32_t pswi = disable_interrupt();
// SYSTEM.PRCR.WORD = SYSTEM_PRCR_PRKEY | SYSTEM_PRCR_PRC1;
// MSTP(USB0) = 0;
// SYSTEM.PRCR.WORD = SYSTEM_PRCR_PRKEY;
// enable_interrupt(pswi);
USB0 の割り込み設定
RX72N では、USB0/USBI は選択型割り込みとなっているので、割り込みベクターを独自に設定して、TinyUSB のハンドラを呼ぶようにしている。
static INTERRUPT_FUNC void i_task_()
{
#if CFG_TUH_ENABLED
tuh_int_handler(0);
#endif
#if CFG_TUD_ENABLED
tud_int_handler(0);
#endif
}
...
ivec_ = icu_mgr::set_interrupt(USB_CH::I_VEC, i_task_, ilvl);
TinyUSB の RX マイコンドライバーでは、マイコンとして、RX72N を選ぶと、選択型割り込みBは、185番がハードコートされている。
自分のシステムでは、選択型割り込みBの割り込みベクターは、内部で管理されていて、設定順により優先度が決定される。
USB0 の初期化は最初に呼ぶので、128番が設定される。
そこで、ドライバーのソースを修正して、128番を使うように修正を行った。
tinyusb/src/portable/renesas/usba/hcd_usba.c
// hcd_init 内
IR(PERIB, INTB128) = 0; // IR(PERIB, INTB185) = 0;
void hcd_int_enable(uint8_t rhport)
{
(void)rhport;
#if ( CFG_TUSB_MCU == OPT_MCU_RX72N )
IEN(PERIB, INTB128) = 1; // IEN(PERIB, INTB185) = 1;
#else
IEN(USB0, USBI0) = 1;
#endif
}
void hcd_int_disable(uint8_t rhport)
{
(void)rhport;
#if ( CFG_TUSB_MCU == OPT_MCU_RX72N )
IEN(PERIB, INTB128) = 0; // IEN(PERIB, INTB185) = 0;
#else
IEN(USB0, USBI0) = 0;
#endif
}
これで、TinyUSB を自分のシステムで利用する準備が整った。
TinyUSB のコア部分
TinyUSB のコアでは、「tuh_task()」をサービスすれば良いようで、通常無限ループにする。
しかし、それだと、他に何も出来ないので、とりあえず、CMT で 1000Hz(1ms) のタイミングを作り、呼び出すようにした。
uint16_t cnt = 0;
uint16_t cnt_max = 50; // by 100Hz
while(1) {
cnt_max = 500;
cmt_.sync();
tuh_task();
#if CFG_TUH_CDC
cdc_task();
#endif
#if CFG_TUH_HID
hid_app_task();
#endif
++cnt;
if(cnt >= cnt_max) {
cnt = 0;
}
if(cnt < (cnt_max / 2)) {
LED::P = 0;
} else {
LED::P = 1;
}
}
TinyUSB のソースをコンパイルする。
iodefine.h は、e2studio/gcc で生成した物をアプリケーションのルートに置いて利用した。
TinyUSB は、tusb_config.h をアプリケーションに合わせて設定する事で、必要なソースをコンパイルするようになっている。
※アプリケーションのルートに置いてある。
ホスト機能を提供する場合、基本的な設定は以下のようになっている。
#define CFG_TUH_HUB 0
#define CFG_TUH_CDC 0
#define CFG_TUH_HID 4 // typical keyboard + mouse device can have 3-4 HID interfaces
#define CFG_TUH_MSC 0 // Mass Storage Device
#define CFG_TUH_VENDOR 0
とりあえず、HUB は「0」にしてある。
必要なソースを Makefile に追加して、パス、外部変数などを設定する。
CSOURCES = common/init.c \
common/vect.c \
common/syscalls.c \
tinyusb/src/tusb.c \
tinyusb/src/common/tusb_fifo.c \
tinyusb/src/device/usbd.c \
tinyusb/src/device/usbd_control.c \
tinyusb/src/class/audio/audio_device.c \
tinyusb/src/class/cdc/cdc_device.c \
tinyusb/src/class/dfu/dfu_device.c \
tinyusb/src/class/dfu/dfu_rt_device.c \
tinyusb/src/class/hid/hid_device.c \
tinyusb/src/class/midi/midi_device.c \
tinyusb/src/class/msc/msc_device.c \
tinyusb/src/class/net/ecm_rndis_device.c \
tinyusb/src/class/net/ncm_device.c \
tinyusb/src/class/usbtmc/usbtmc_device.c \
tinyusb/src/class/video/video_device.c \
tinyusb/src/class/vendor/vendor_device.c \
tinyusb/src/class/cdc/cdc_host.c \
tinyusb/src/class/hid/hid_host.c \
tinyusb/src/class/msc/msc_host.c \
tinyusb/src/host/hub.c \
tinyusb/src/host/usbh.c \
tinyusb/src/portable/ohci/ohci.c \
tinyusb/src/portable/renesas/usba/hcd_usba.c
...
USER_DEFS = SIG_RX72N CFG_TUSB_MCU=OPT_MCU_RX72N
...
# インクルードパス
INC_APP = . ../ \
../RX600/drw2d/inc/tes \
../tinyusb/src
# C コンパイル時の警告設定
CP_OPT = -Wall -Werror \
-Wno-unused-variable \
-Wno-unused-function \
-Wno-stringop-truncation \
-fno-exceptions
これで、TinyUSB 関係ソースをコンパイル出来た。
HID 関係のサービスを実装する
HID 関係の実装は、サンプルがあるので、それを参考にした。
基本、接続時、解放時、データ転送などでコールバックが呼ばれるので、それに対応するコードを実装する。
extern "C" {
void tuh_hid_mount_cb(uint8_t dev_addr, uint8_t instance, uint8_t const* desc_report, uint16_t desc_len)
{
device::tinyusb_base::hid_mount_cb(dev_addr, instance, desc_report, desc_len);
}
void tuh_hid_umount_cb(uint8_t dev_addr, uint8_t instance)
{
device::tinyusb_base::hid_umount_cb(dev_addr, instance);
}
void hid_app_task(void)
{
device::tinyusb_base::hid_app_task();
}
void tuh_hid_report_received_cb(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len)
{
device::tinyusb_base::hid_report_received_cb(dev_addr, instance, report, len);
}
#if 0
void tuh_cdc_xfer_isr(uint8_t dev_addr, xfer_result_t event, cdc_pipeid_t pipe_id, uint32_t xferred_bytes)
{
}
#endif
void cdc_task(void)
{
}
}
とりあえず、C++ で実装して、TinyUSB に繋いだ・・
struct tinyusb_base {
static uint8_t dev_addr_;
static uint8_t instance_;
static bool send_;
static uint8_t leds_;
static void hid_mount_cb(uint8_t dev_addr, uint8_t instance, uint8_t const* desc_report, uint16_t desc_len)
{
uint16_t vid, pid;
tuh_vid_pid_get(dev_addr, &vid, &pid);
utils::format("HID device address = %d, instance = %d is mounted\r\n")
% static_cast<uint16_t>(dev_addr) % static_cast<uint16_t>(instance);
utils::format("VID = %04x, PID = %04x\r\n") % vid % pid;
auto detect = false;
auto itf_protocol = tuh_hid_interface_protocol(dev_addr, instance);
switch (itf_protocol) {
case HID_ITF_PROTOCOL_KEYBOARD:
// process_kbd_report( (hid_keyboard_report_t const*) report );
utils::format("Detected KEYBOARD\n");
detect = true;
break;
case HID_ITF_PROTOCOL_MOUSE:
// process_mouse_report( (hid_mouse_report_t const*) report );
utils::format("Detected MOUSE\n");
detect = true;
break;
default:
// Generic report requires matching ReportID and contents with previous parsed report info
// process_generic_report(dev_addr, instance, report, len);
utils::format("Detected GENERIC\n");
detect = true;
break;
}
if (detect) {
if ( !tuh_hid_receive_report(dev_addr, instance) ) {
utils::format("Error: cannot request to receive report\r\n");
}
}
}
static void hid_umount_cb(uint8_t dev_addr, uint8_t instance)
{
utils::format("HID device address = %d, instance = %d is unmounted\r\n")
% static_cast<uint16_t>(dev_addr) % static_cast<uint16_t>(instance);
}
static void hid_app_task()
{
if(send_) {
if(!tuh_hid_set_report(dev_addr_, instance_, 0, HID_REPORT_TYPE_OUTPUT, &leds_, sizeof(leds_))) {
utils::format("set report fail...\n");
}
send_ = false;
}
}
static void hid_report_received_cb(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len)
{
static uint8_t cnt = 0;
auto itf_protocol = tuh_hid_interface_protocol(dev_addr, instance);
switch (itf_protocol) {
case HID_ITF_PROTOCOL_KEYBOARD:
{
uint8_t tmp[8];
memcpy(tmp, report, len);
utils::format("%d, %d, %d, %d\n") % static_cast<uint16_t>(tmp[0]) % static_cast<uint16_t>(tmp[1]) % static_cast<uint16_t>(tmp[2]) % static_cast<uint16_t>(tmp[3]);
if(tmp[2] == 83 || tmp[2] == 57) {
++cnt;
if(cnt & 1) {
leds_ |= KEYBOARD_LED_NUMLOCK | KEYBOARD_LED_CAPSLOCK;
} else {
leds_ = 0;
}
dev_addr_ = dev_addr;
instance_ = instance;
send_ = true;
}
}
break;
case HID_ITF_PROTOCOL_MOUSE:
break;
default:
break;
}
// continue to request to receive report
if ( !tuh_hid_receive_report(dev_addr, instance) ) {
utils::format("Error: cannot request to receive report\r\n");
}
}
};
実験してみるが・・・
とりあえず、コンパイルが通ったので、RX72N Envision Kit にキーボードを繋いで実験してみた。
Start 'USB0' test for 'RX72N' 240[MHz]
SCI Baud rate (set): 115200
SCI Baud rate (real): 115384 (0.16 [%])
CMT rate (set): 1000 [Hz]
CMT rate (real): 1000 [Hz] (0.00 [%])
Start USB: OK!
HID device address = 1, instance = 0 is mounted
VID = 1c4f, PID = 0027
Detected KEYBOARD
0, 0, 25, 0
0, 0, 0, 0
0, 0, 10, 0
0, 0, 0, 0
0, 0, 9, 0
0, 0, 0, 0
0, 0, 23, 0
0, 0, 0, 0
USB キーボードを繋いで、キーを押してみた。
反応が返る!
問題点
ネットで、TinyUSB/Host でキーボード接続時、LED を点灯する仕組みを探したら、「tuh_hid_set_report」API を使い、LED ビットに対応するバイトデータを送れば良い事が判り、実装するものの、上記 API は「false」を返して失敗するようだ・・・
色々調べたが、キーボード接続時に、IDLE 設定の転送を行い、それが終了しない為、失敗しているようだが、根本的な原因が判らない・・
他にも、GENRIC デバイス(キーボードを外して、ゲームパッドを接続するとハングアップする)の動作が微妙とか。
最初にゲームパッドを接続して、次にキーボードを接続すると認識しないとか(電源を切るまで認識しなくなる)
色々問題があるようだ・・・
ただ、それが、TinyUSB の問題なのか、RX マイコンのドライバーなのか、切り分けが出来ていない。
まとめ
まだまだ、これからのようだが、将来性を考えると、TinyUSB は良い選択となるものと思える。
今後、色々と環境を整えていきたい。