arduino-esp32 icon indicating copy to clipboard operation
arduino-esp32 copied to clipboard

Add support for TinyUSB Host

Open marchingband opened this issue 9 months ago • 20 comments

Related area

USB

Hardware specification

ESP32-S3

Is your feature request related to a problem?

I would like to use USB Host on S3.

Describe the solution you'd like

I hear that USB Host support is ready from tinyusb, which is very exciting. https://github.com/hathach/tinyusb/issues/2634#issuecomment-2646978422

Would it be possible to confirm this? Will we see some example code at some point? I am particularly interested in MIDI USB Host. thank you!

Describe alternatives you've considered

Currently I use esp-idf for USB MIDI Host, but I would like to use Arduino, because I produce development boards, and my users prefer Arduino.

Additional context

No response

I have checked existing list of Feature requests and the Contribution Guide

  • [x] I confirm I have checked existing list of Feature requests and Contribution Guide.

marchingband avatar Feb 18 '25 08:02 marchingband

we have not added host support to Arduino yet, but we will look into it in near future

me-no-dev avatar Feb 18 '25 08:02 me-no-dev

That's amazing thank you. Can we leave this issue open to track updates or is there a better place?

marchingband avatar Feb 18 '25 16:02 marchingband

Sure we can. I updated the title

me-no-dev avatar Feb 18 '25 17:02 me-no-dev

I just wanted to add that I am also looking for MIDI capabilities in Arduino environments with an S3. Would be great to see this working in a similar way as with the Teensy devices with MIDI and other USB device type support. Thanks in advance.

JorenSix avatar Feb 25 '25 19:02 JorenSix

I am looking for this feature as well. Spent three days tring to implement and it's hell.

Would be great for this to be in the arduino esp core. Using an m5stack Core S3 (ESP32-S3)

polygonfuture avatar May 07 '25 07:05 polygonfuture

@me-no-dev any updates? Is this still on the road map? Thanks!

marchingband avatar May 07 '25 07:05 marchingband

It is on the roadmap. Can not give time estimate yet

me-no-dev avatar May 07 '25 08:05 me-no-dev

It is on the roadmap. Can not give time estimate yet

@me-no-dev I understand the "no date" commitment. If this is something you can communicate, would you able to indicate whether that's a low or middle priority task at the moment ?

davidandreoletti avatar May 13 '25 08:05 davidandreoletti

It's low to middle priority @davidandreoletti we have issues to fix with currently supported peripherals (including USB device)

me-no-dev avatar May 13 '25 09:05 me-no-dev

Hello, @me-no-dev , is it getting any closer?

copych avatar Aug 06 '25 18:08 copych

Havnt tested this but it looks promising, not sure how they pulled it off.

https://github.com/enudenki/esp32-usb-host-midi-library

marchingband avatar Aug 21 '25 01:08 marchingband

This is cool but I'd still like a native USB Host functionality added to the core TinyUSB library for Arduino that supports ESP32-S3.

polygonfuture avatar Oct 12 '25 01:10 polygonfuture

It's low to middle priority @davidandreoletti we have issues to fix with currently supported peripherals (including USB device)

@me-no-dev Over the last 5-6months, how has the priority changed ?

davidandreoletti avatar Oct 15 '25 10:10 davidandreoletti

it's being worked on. Current core 3.3.3 already supports TinyUSB host, but still needs Arduino abstraction for it.

me-no-dev avatar Nov 05 '25 23:11 me-no-dev

@me-no-dev

it's being worked on. Current core 3.3.3 already supports TinyUSB host, but still needs Arduino abstraction for it.

Is there any MCVE to test it ?

When I build and execute the Host example that comes with Arduino TinyUSB on ESP32-P4 DevKit @ Core 3.3.3 https://github.com/adafruit/Adafruit_TinyUSB_Arduino/blob/master/examples/Host/Simple/host_device_info/host_device_info.ino it gives the following boot message and that's it

TinyUSB Host: Device Info Example

the sketch does neither detect nor display devices plugged in the DevKit's HS USB port.

lyusupov avatar Nov 11 '25 05:11 lyusupov

/*
 * TinyUSB Host Device Info Example - Arduino Port
 * Based on tinyusb/examples/host/device_info
 * 
 * This example enumerates attached USB devices and prints their descriptors
*/

#if CONFIG_IDF_TARGET_ESP32S3
// Hardware pin definitions (OTG board specific)
#define BSP_USB_POS           (GPIO_NUM_20)
#define BSP_USB_NEG           (GPIO_NUM_19)
#define BSP_USB_MODE_SEL      (GPIO_NUM_18) // Select Host (high level) or Device (low level, default) mode
#define BSP_USB_HOST_VOLTAGE  (GPIO_NUM_1)  // Voltage at this pin = (V_BUS / 3.7), ADC1 channel 0
#define BSP_USB_HOST_VOLTAGE_DIV (3.7f)
#define BSP_USB_LIMIT_EN      (GPIO_NUM_17) // Active high (pulled low)
#define BSP_USB_DEV_VBUS_EN   (GPIO_NUM_12) // Active high (pulled low)

#define BSP_BATTERY_VOLTAGE   (GPIO_NUM_2)  // Voltage at this pin = (V_BAT / 2), ADC1 channel 1
#define BSP_BATTERY_VOLTAGE_DIV (2)
#define BSP_BATTERY_BOOST_EN  (GPIO_NUM_13) // 3.3->5V for USB device power from battery. Active high (pulled low)

#elif CONFIG_IDF_TARGET_ESP32P4
#define BSP_USB_POS           (GPIO_NUM_20)
#define BSP_USB_NEG           (GPIO_NUM_19)
#endif
// Include ESP-IDF headers for USB hardware initialization
#ifdef ESP_PLATFORM
  #include "esp_err.h"  // For esp_err_to_name()
  #if __has_include("esp_private/usb_phy.h")
    #include "esp_private/usb_phy.h"
    #define USB_PHY_AVAILABLE
  #elif __has_include("hal/usb_hal.h")
    #include "hal/usb_hal.h"
    #define USB_HAL_AVAILABLE
  #endif
#endif

// Include TinyUSB header
// The arduino_tinyusb component's tusb_config.h should already have
// CFG_TUH_ENABLED=1 if built from PR #314
#include "tusb.h"

// USB PHY handle for host mode
#ifdef USB_PHY_AVAILABLE
static usb_phy_handle_t usb_phy_handle = NULL;
#endif

// English language ID
#define LANGUAGE_ID 0x0409

// Declare buffers for USB transfers (must be in USB/DMA section)
CFG_TUH_MEM_SECTION struct {
  TUH_EPBUF_TYPE_DEF(tusb_desc_device_t, device);
  TUH_EPBUF_DEF(serial, 64*sizeof(uint16_t));
  TUH_EPBUF_DEF(buf, 128*sizeof(uint16_t));
} desc;

//--------------------------------------------------------------------+
// String Descriptor Helper
//--------------------------------------------------------------------+

static void _convert_utf16le_to_utf8(const uint16_t* utf16, size_t utf16_len, uint8_t* utf8, size_t utf8_len) {
  (void) utf8_len;
  for (size_t i = 0; i < utf16_len; i++) {
    uint16_t chr = utf16[i];
    if (chr < 0x80) {
      *utf8++ = chr & 0xffu;
    } else if (chr < 0x800) {
      *utf8++ = (uint8_t) (0xC0 | (chr >> 6 & 0x1F));
      *utf8++ = (uint8_t) (0x80 | (chr >> 0 & 0x3F));
    } else {
      *utf8++ = (uint8_t) (0xE0 | (chr >> 12 & 0x0F));
      *utf8++ = (uint8_t) (0x80 | (chr >> 6 & 0x3F));
      *utf8++ = (uint8_t) (0x80 | (chr >> 0 & 0x3F));
    }
  }
}

// Count how many bytes a utf-16-le encoded string will take in utf-8.
static int _count_utf8_bytes(const uint16_t* buf, size_t len) {
  size_t total_bytes = 0;
  for (size_t i = 0; i < len; i++) {
    uint16_t chr = buf[i];
    if (chr < 0x80) {
      total_bytes += 1;
    } else if (chr < 0x800) {
      total_bytes += 2;
    } else {
      total_bytes += 3;
    }
  }
  return (int) total_bytes;
}

static void print_utf16(uint16_t* temp_buf, size_t buf_len) {
  if ((temp_buf[0] & 0xff) == 0) return;  // empty
  size_t utf16_len = ((temp_buf[0] & 0xff) - 2) / sizeof(uint16_t);
  size_t utf8_len = (size_t) _count_utf8_bytes(temp_buf + 1, utf16_len);
  _convert_utf16le_to_utf8(temp_buf + 1, utf16_len, (uint8_t*) temp_buf, sizeof(uint16_t) * buf_len);
  ((uint8_t*) temp_buf)[utf8_len] = '\0';
  Serial.printf("%s", (char*) temp_buf);
}

//--------------------------------------------------------------------+
// TinyUSB Host Callbacks
//--------------------------------------------------------------------+

// Invoked when device is mounted (configured)
void tuh_mount_cb(uint8_t daddr) {
  Serial.printf("\r\n=== Device %u MOUNTED ===\r\n", daddr);

  // Get Device Descriptor
  tusb_xfer_result_t xfer_result = tuh_descriptor_get_device_sync(daddr, &desc.device, 18);
  if (XFER_RESULT_SUCCESS != xfer_result) {
    Serial.println("Failed to get device descriptor");
    return;
  }

  Serial.printf("Device %u: ID %04x:%04x SN ", daddr, desc.device.idVendor, desc.device.idProduct);

  xfer_result = (tusb_xfer_result_t)XFER_RESULT_FAILED;
  if (desc.device.iSerialNumber != 0) {
    xfer_result = tuh_descriptor_get_serial_string_sync(daddr, LANGUAGE_ID, desc.serial, sizeof(desc.serial));
  }
  if (XFER_RESULT_SUCCESS != xfer_result) {
    uint16_t* serial = (uint16_t*)(uintptr_t) desc.serial;
    serial[0] = (uint16_t) ((TUSB_DESC_STRING << 8) | (2 * 3 + 2));
    serial[1] = 'n';
    serial[2] = '/';
    serial[3] = 'a';
    serial[4] = 0;
  }
  print_utf16((uint16_t*)(uintptr_t) desc.serial, sizeof(desc.serial)/2);
  Serial.println();

  Serial.println("Device Descriptor:");
  Serial.printf("  bLength             %u\r\n", desc.device.bLength);
  Serial.printf("  bDescriptorType     %u\r\n", desc.device.bDescriptorType);
  Serial.printf("  bcdUSB              %04x\r\n", desc.device.bcdUSB);
  Serial.printf("  bDeviceClass        %u\r\n", desc.device.bDeviceClass);
  Serial.printf("  bDeviceSubClass     %u\r\n", desc.device.bDeviceSubClass);
  Serial.printf("  bDeviceProtocol     %u\r\n", desc.device.bDeviceProtocol);
  Serial.printf("  bMaxPacketSize0     %u\r\n", desc.device.bMaxPacketSize0);
  Serial.printf("  idVendor            0x%04x\r\n", desc.device.idVendor);
  Serial.printf("  idProduct           0x%04x\r\n", desc.device.idProduct);
  Serial.printf("  bcdDevice           %04x\r\n", desc.device.bcdDevice);

  // Get String descriptors
  Serial.printf("  iManufacturer       %u     ", desc.device.iManufacturer);
  if (desc.device.iManufacturer != 0) {
    xfer_result = tuh_descriptor_get_manufacturer_string_sync(daddr, LANGUAGE_ID, desc.buf, sizeof(desc.buf));
    if (XFER_RESULT_SUCCESS == xfer_result) {
      print_utf16((uint16_t*)(uintptr_t) desc.buf, sizeof(desc.buf)/2);
    }
  }
  Serial.println();

  Serial.printf("  iProduct            %u     ", desc.device.iProduct);
  if (desc.device.iProduct != 0) {
    xfer_result = tuh_descriptor_get_product_string_sync(daddr, LANGUAGE_ID, desc.buf, sizeof(desc.buf));
    if (XFER_RESULT_SUCCESS == xfer_result) {
      print_utf16((uint16_t*)(uintptr_t) desc.buf, sizeof(desc.buf)/2);
    }
  }
  Serial.println();

  Serial.printf("  iSerialNumber       %u     ", desc.device.iSerialNumber);
  Serial.printf("%s\r\n", (char*)desc.serial); // serial is already converted to UTF-8
  Serial.printf("  bNumConfigurations  %u\r\n", desc.device.bNumConfigurations);
}

// Invoked when device is unmounted (bus reset/unplugged)
void tuh_umount_cb(uint8_t daddr) {
  Serial.printf("Device removed, address = %d\r\n", daddr);
}

//--------------------------------------------------------------------+
// Host Class Driver Callbacks (stubs for device enumeration example)
// These are required by the linker since host class drivers are compiled in
//--------------------------------------------------------------------+

// HID callbacks
#if CFG_TUH_HID
void tuh_hid_mount_cb(uint8_t dev_addr, uint8_t idx, uint8_t const* report_desc, uint16_t desc_len) {
  // Stub - not used in this enumeration example
  (void) dev_addr;
  (void) idx;
  (void) report_desc;
  (void) desc_len;
}

void tuh_hid_umount_cb(uint8_t dev_addr, uint8_t idx) {
  // Stub - not used in this enumeration example
  (void) dev_addr;
  (void) idx;
}

void tuh_hid_report_received_cb(uint8_t dev_addr, uint8_t idx, uint8_t const* report, uint16_t len) {
  // Stub - not used in this enumeration example
  (void) dev_addr;
  (void) idx;
  (void) report;
  (void) len;
}

void tuh_hid_report_sent_cb(uint8_t dev_addr, uint8_t idx, uint8_t const* report, uint16_t len) {
  // Stub - not used in this enumeration example
  (void) dev_addr;
  (void) idx;
  (void) report;
  (void) len;
}

void tuh_hid_get_report_complete_cb(uint8_t dev_addr, uint8_t idx, uint8_t report_id, uint8_t report_type, uint16_t len) {
  // Stub - not used in this enumeration example
  (void) dev_addr;
  (void) idx;
  (void) report_id;
  (void) report_type;
  (void) len;
}

void tuh_hid_set_report_complete_cb(uint8_t dev_addr, uint8_t idx, uint8_t report_id, uint8_t report_type, uint16_t len) {
  // Stub - not used in this enumeration example
  (void) dev_addr;
  (void) idx;
  (void) report_id;
  (void) report_type;
  (void) len;
}

void tuh_hid_set_protocol_complete_cb(uint8_t dev_addr, uint8_t idx, uint8_t protocol) {
  // Stub - not used in this enumeration example
  (void) dev_addr;
  (void) idx;
  (void) protocol;
}
#endif

// CDC callbacks
#if CFG_TUH_CDC
void tuh_cdc_mount_cb(uint8_t idx) {
  // Stub - not used in this enumeration example
  (void) idx;
}

void tuh_cdc_umount_cb(uint8_t idx) {
  // Stub - not used in this enumeration example
  (void) idx;
}

void tuh_cdc_rx_cb(uint8_t idx) {
  // Stub - not used in this enumeration example
  (void) idx;
}

void tuh_cdc_tx_complete_cb(uint8_t idx) {
  // Stub - not used in this enumeration example
  (void) idx;
}
#endif

// MSC callbacks
#if CFG_TUH_MSC
void tuh_msc_mount_cb(uint8_t dev_addr) {
  // Stub - not used in this enumeration example
  (void) dev_addr;
}

void tuh_msc_umount_cb(uint8_t dev_addr) {
  // Stub - not used in this enumeration example
  (void) dev_addr;
}
#endif

//--------------------------------------------------------------------+
// Setup and Loop
//--------------------------------------------------------------------+

void setup() {
  Serial.begin(115200);
  while (!Serial && millis() < 5000) {
    delay(10);
  }
  
  Serial.println("TinyUSB Device Info Example");
  
  #if CONFIG_IDF_TARGET_ESP32S3
  // Configure USB mode pin for host mode (high level)
  pinMode(BSP_USB_MODE_SEL, OUTPUT);
  digitalWrite(BSP_USB_MODE_SEL, HIGH);
  delay(10);  // Allow mode to settle
  
  // Initialize USB power control pins
  pinMode(BSP_USB_LIMIT_EN, OUTPUT);
  pinMode(BSP_USB_DEV_VBUS_EN, OUTPUT);
  pinMode(BSP_BATTERY_BOOST_EN, OUTPUT);
  
  // Start with all power disabled
  digitalWrite(BSP_USB_LIMIT_EN, LOW);
  digitalWrite(BSP_USB_DEV_VBUS_EN, LOW);
  digitalWrite(BSP_BATTERY_BOOST_EN, LOW);
  delay(10);
  
  // Enable USB host power
  // Option 1: Use VBUS power (if board is powered via USB)
  // Option 2: Use battery boost (if powered by battery)
  // For now, enable VBUS power - change to battery if needed
  digitalWrite(BSP_USB_DEV_VBUS_EN, HIGH);  // Enable VBUS power
  delay(10);  // Allow power to stabilize
  digitalWrite(BSP_USB_LIMIT_EN, HIGH);     // Enable current limiting
  Serial.println("USB host power enabled (VBUS mode)");

  #endif
  
  // Alternative: If using battery power, uncomment this instead:
  // digitalWrite(BSP_BATTERY_BOOST_EN, HIGH);  // Enable battery boost
  // delay(10);
  // digitalWrite(BSP_USB_LIMIT_EN, HIGH);      // Enable current limiting
  // Serial.println("USB host power enabled (battery mode)");
  
  // Initialize USB PHY for host mode (required before tusb_init)
  #ifdef USB_PHY_AVAILABLE
    // Set PHY target and speed based on chip
    #if CONFIG_IDF_TARGET_ESP32P4
      usb_phy_target_t phy_target = USB_PHY_TARGET_UTMI;  // ESP32-P4 uses UTMI PHY
      usb_phy_speed_t phy_speed = USB_PHY_SPEED_HIGH;     // ESP32-P4 supports High Speed
    #else
      usb_phy_target_t phy_target = USB_PHY_TARGET_INT;   // Internal PHY for ESP32-S3/S2
      usb_phy_speed_t phy_speed = USB_PHY_SPEED_UNDEFINED; // Speed determined by connected device
    #endif
    
    usb_phy_config_t phy_config = {
      .controller = USB_PHY_CTRL_OTG,
      .target = phy_target,
      .otg_mode = USB_OTG_MODE_HOST,  // HOST mode, not DEVICE
      .otg_speed = phy_speed,
      .ext_io_conf = NULL,
      .otg_io_conf = NULL,
    };
    
    esp_err_t phy_ret = usb_new_phy(&phy_config, &usb_phy_handle);
    if (phy_ret != ESP_OK) {
      Serial.printf("Failed to initialize USB PHY for host mode: %s\r\n", esp_err_to_name(phy_ret));
      while(1) delay(1000);
    }
    Serial.printf("USB PHY initialized for host mode (PHY: %s)\r\n", 
                  #if CONFIG_IDF_TARGET_ESP32P4
                    "UTMI"
                  #else
                    "INT"
                  #endif
                  );
    
    // Give PHY time to stabilize
    delay(50);
  #elif defined(USB_HAL_AVAILABLE)
    // Alternative: use USB HAL if PHY API not available
    usb_hal_context_t hal = {.use_external_phy = false};
    usb_hal_init(&hal);
    Serial.println("USB HAL initialized");
    delay(50);
  #endif
  
  // Initialize TinyUSB host stack
  tusb_rhport_init_t host_init = {
    .role = TUSB_ROLE_HOST,
    #if CONFIG_IDF_TARGET_ESP32P4
      .speed = TUSB_SPEED_HIGH,  // ESP32-P4 supports High Speed
    #else
      .speed = TUSB_SPEED_AUTO,  // Auto-detect for ESP32-S3/S2
    #endif
  };
  
  // Determine rhport - ESP32-P4 has two controllers
  #if CONFIG_IDF_TARGET_ESP32P4
    // ESP32-P4 has two DWC2 controllers:
    // - rhport 0: FS controller (0x50040000) - for Full Speed
    // - rhport 1: HS controller (0x50000000) - for High Speed (UTMI PHY)
    // For UTMI PHY with High Speed, we MUST use rhport 1 (HS controller)
    uint8_t rhport = 1;
    Serial.println("Initializing TinyUSB host on rhport 1 (HS controller for UTMI PHY)");
  #else
    // ESP32-S3/S2 use rhport 0
    uint8_t rhport = 0;
    Serial.println("Initializing TinyUSB host on rhport 0");
  #endif
  
  Serial.printf("TinyUSB init params: role=HOST, speed=%s, rhport=%u\r\n",
                #if CONFIG_IDF_TARGET_ESP32P4
                  "HIGH",
                #else
                  "AUTO",
                #endif
                rhport);
  
  if (!tusb_init(rhport, &host_init)) {
    Serial.printf("Failed to initialize TinyUSB host stack (rhport=%u)\r\n", rhport);
    #ifdef USB_PHY_AVAILABLE
      if (usb_phy_handle) {
        usb_del_phy(usb_phy_handle);
        usb_phy_handle = NULL;
      }
    #endif
    while(1) delay(1000);
  }
  
  Serial.println("TinyUSB host initialized");
  Serial.println("Waiting for USB device...");
  
  // Diagnostic: Check if TinyUSB is properly initialized
  if (!tusb_inited()) {
    Serial.println("WARNING: tusb_inited() returned false!");
  } else {
    Serial.println("TinyUSB initialization verified");
  }
}

void loop() {
  tuh_task();
  delay(1);  // Small delay to prevent watchdog issues
}

me-no-dev avatar Nov 11 '25 05:11 me-no-dev

@me-no-dev

Thank you! The MCVE works fine to me -

TinyUSB Device Info Example
USB PHY initialized for host mode (PHY: UTMI)
Initializing TinyUSB host on rhport 1 (HS controller for UTMI PHY)
TinyUSB init params: role=HOST, speed=HIGH, rhport=1
TinyUSB host initialized
Waiting for USB device...
TinyUSB initialization verified

=== Device 1 MOUNTED ===
Device 1: ID 0bda:2832 SN 77771111153705700
Device Descriptor:
  bLength             18
  bDescriptorType     1
  bcdUSB              0200
  bDeviceClass        0
  bDeviceSubClass     0
  bDeviceProtocol     0
  bMaxPacketSize0     64
  idVendor            0x0bda
  idProduct           0x2832
  bcdDevice           0100
  iManufacturer       1     
  iProduct            2     
  iSerialNumber       3     77771111153705700
  bNumConfigurations  1

lyusupov avatar Nov 11 '25 07:11 lyusupov

@me-no-dev

This commit made by @tanakamasayuki https://github.com/espressif/esp32-arduino-lib-builder/commit/fffa99e734a4505fd36d4caf94fbf5cb5f192c5c

has negative effect on USB bulk transfers with Espressif USB Host Library.

  1. These lines went into defconfig.common
CONFIG_USB_HOST_HUBS_SUPPORTED=y
CONFIG_USB_HOST_HUB_MULTI_LEVEL=y
CONFIG_USB_HOST_HW_BUFFER_BIAS_PERIODIC_OUT=y

This is wrong because native USB HOST feature is applicable for S2 , S3 and P4 configurations only.

  1. CONFIG_USB_HOST_HW_BUFFER_BIAS_PERIODIC_OUT=y setting (may) have positive effect for S2 and S3 targets only.

With ESP32-P4 is causes a bulk transfer failure issue explained in this esp-idf thread https://github.com/espressif/esp-idf/issues/17550

Suggestion is to

  • remove all three lines from defconfig.common
  • add the following 2 lines into defconfig.esp32s2 , defconfig.esp32s3 and defconfig.esp32p4
CONFIG_USB_HOST_HUBS_SUPPORTED=y
CONFIG_USB_HOST_HUB_MULTI_LEVEL=y
  • add CONFIG_USB_HOST_HW_BUFFER_BIAS_PERIODIC_OUT=y into defconfig.esp32s2 and defconfig.esp32s3
  • add CONFIG_USB_HOST_HW_BUFFER_BIAS_BALANCED=y into defconfig.esp32p4

lyusupov avatar Nov 11 '25 18:11 lyusupov

@lyusupov we have yet to look into that host library. feel free to make a PR with the needed changes

me-no-dev avatar Nov 11 '25 20:11 me-no-dev

@me-no-dev

feel free to make a PR with the needed changes

Filed as PR https://github.com/espressif/esp32-arduino-lib-builder/pull/330

lyusupov avatar Nov 11 '25 21:11 lyusupov