LoginSignup
0
1

More than 1 year has passed since last update.

RXマイコンでTinyUSBのホスト機能を実験

Last updated at Posted at 2022-08-03

IMG_0205s.jpg

TinyUSB について

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

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

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

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

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

RX マイコン用ドライバーは、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 ホストとして、以下の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) のタイミングを作り、呼び出すようにしました。
※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 は「0」にしてあり、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)

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1