ＲＸマイコンでＴｉｎｙＵＳＢのホスト機能を実験

![IMG_0205s.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58667/781d6cbc-163c-2a2d-18d3-a52ec23b4f73.jpeg)


## TinyUSB について

最近、USB のスタックで評判の TinyUSB を実験してみました。

現状の実装では、主に以下のような感じとなっています。

- オープンソースです。（MIT ライセンス）
- 非常に多くのマイコンに対応しています。
- RX マイコンも、デバイス（クライアント）、ホストのドライバーが用意されています。
- 主にデバイス（クライアント）の機能が充実しています。（PC に繋ぐ、USB 機器を作る場合に有用）
- USB ホストは実装が始まったばかりで、あまり枯れていないようです。
- 全体の構成が判りやすく、サンプルやドキュメントが充実しています。
- 機能実装、バグ修正などが GitHub で行われており、将来性が有望です。
- 「Tiny」となっていますが、プロジェクトはかなり大きいです。
- 実行バイナリーがコンパクトになるものと思います。
- API の仕様が明確で適切なので、マイコンの種別に関係無く、ソースコードの再利用が出来ると思われます。（ここが大きい！）

など、かなり魅力的なプロジェクトとなっています。

RX マイコン用のドライバーもあるので、自分の環境で実験してみました。
※当初、ホストのドライバーが無かったのですが、現在は、コミットされています。

RX マイコン用ドライバーは、[Koji KITAYAMA](https://github.com/kkitayam "Koji KITAYAMA") さんが実装しているようで、御礼申し上げます。

---

## 自分のシステムに取り込む場合

自分の C++ フレームワークに取り込むには、多少の改造が必要なので、その過程を記しておきます。

- TinyUSB には、サンプルプログラムが沢山ありますが、RX マイコンで使う場合、主に CC-RX 環境に依存しているので、gcc で使うには問題があります。
- ただ、元が、基本 Makefile でのビルドなので、自分のシステムとは親和性があります。
- そこで、最小限の改造で、使うようにする為、自分のシステムに必要な部分だけ取り込んで実験してみました。
- とりあえず、ホスト機能を使い、ゲームパッドやキーボードを利用する事が主目的となっています。
- 非常に機能が豊富なので、他も試していきたいと思っています。

---

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 ホストとして、以下の２ポートを利用しています。

- 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 を選ぶと、選択型割り込みＢは、１８５番がハードコートされています。

自分のシステムでは、選択型割り込みＢの割り込みベクターは、内部で管理されていて、設定順により優先度が決定されます。
USB0 の初期化は最初に呼ぶので、１２８番が設定されます。

そこで、ドライバーのソースを修正して、１２８番を使うように修正を行いました。

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） のタイミングを作り、呼び出すようにしました。
※tuh_task() などをラップした C++ クラス「tinyusb_mng.hpp」を用意してあります。

```
	{
		uint8_t intr = 5;
		if(tinyusb_.start(intr)) {
			utils::format("Start USB: OK!\n");
		} else {
			utils::format("Start USB: fail...\n");
		}
	}

	uint16_t cnt = 0;
	while(1) {

		cmt_.sync();

		tinyusb_.service();

		auto& k = tinyusb_.at_keyboard();
		if(k.get_num() > 0) {
			auto t = k.get_key();
			if(t.code == 0x0d) {
				utils::format("\n");
			} else {
				utils::format("%c") % t.code;
				utils::format::flush();
			}
		}

		++cnt;
		if(cnt >= 500) {
			cnt = 0;
		}
		if(cnt < 250) {
			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 は「０」にしてあり、HID のみを使う設定です。

必要なソースを 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 にキーボードを繋いで実験してみました。

キーを押すと、コードが返るようにしてあります、同時に、キーマップの型を戻しています。
※「usb/usb_keyboard.hpp」を参照

```
Start 'USB0' test for 'RX72N Envision Kit' 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
lkdsjfiore958j&^jnfr9
kdsjfKJKLJ
```

RX65N Envision Kit:
```
Start TinyUSB/Host sample for 'RX65N Envision Kit' 120[MHz]
SCI PCLK: 60000000
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
kjyhDFTYgh65
```

---

### 問題点

ネットで、TinyUSB/Host でキーボード接続時、LED を点灯する仕組みを探したら、「tuh_hid_set_report」API を使い、LED ビットに対応するバイトデータを送れば良い事が判り、実装するものの、上記 API は「false」を返して失敗するようです・・・

色々調べましたが、キーボードマウント時に、IDLE 設定の転送を行い、それが終了しない為、失敗しているようですが、根本的な原因が判らないです・・
※ネットの情報では、他のマイコンで、LED の点灯を実現しているようです。

他にも、GENRIC デバイス（キーボードを外して、ゲームパッドを接続するとハングアップするなど）の動作が微妙とか。
最初にゲームパッドを接続して、次にキーボードを接続すると認識しないとか（電源を切るまで認識しなくなる）
色々問題があるようです・・・
ただ、それが、TinyUSB の問題なのか、RX マイコンのドライバーなのか、切り分けが出来ていません。

- FILCO FKBN87M/EB キーボードを接続してみましたが、認識しませんでした。
- SANWA SKB-E3U キーボードを認識しました。（LowSpeed)
- ELECOM JC-U4013S ゲームパッドを認識しました。（FullSpeed)

---

## まとめ

まだまだ、これからのようですが、将来性を考えると、TinyUSB は良い選択となるものと思えます。

とりあえず、最初に接続すれば使えると思うので、デバイスの応答など色々実装して行きたいと思います。

又、他のデバイス（CDC など）も試してみたいと思います。

---
RX65N Envision Kit, RX72N Envision Kit で動作確認をしました。

ソースコード：　[TUSB_HOST_sample (Github)](https://github.com/hirakuni45/RX/tree/master/TUSB_HOST_sample)