Overview
Is your QT Py all alone, lacking a friend to travel the wide world with? When you were a kid, you may have learned about the "buddy" system. Well, this product is kind of like that! It is a board that will watch your QT Py's back and add a USB Host port.
That means that your tiny microcontroller project can have a keyboard, mouse, or disk drive plugged into it, opening up a huge ecosystem of common off-the-shelf devices that you can now integrate.
The Adafruit USB Host BFF makes it easy to add USB Host support, especially now that TinyUSB supports it in the Arduino library as a 'native' interface for host support.
This BFF uses the MAX3421E - a tried and true USB Host chip. It uses SPI plus an IRQ pin to send data to just about any USB device.
Note that because the chip is older and you're limited to the SPI port speed, you won't get blazing 480Mbps high-speed data transfer. But for basic HID interfacing or even reading/writing to a Mass Storage device, it does work quite well.
There's a famous USB Host Library that can be used, and its specialty is AVR support, but it also seems to support nRF52 and ESP32.
We personally recommend using the TinyUSB Arduino library - however, the trade-off is that the chip must have TinyUSB support already, which means it's great for RP2040, ESP32-S2 or S3, nRF52840, SAMD21/51 chips. Make sure your desired QT Py mainboard is supported between the two libraries before purchasing!
To keep the BFF very compact, we use a micro-B USB "OTG" connector - a full sized USB Type A wouldn't fit! You'll need to use a basic OTG adapter (such as those sold for use with the Pi Zero) to convert to type A. Unlike our USB Host 'Wing we don't include a 5V power booster, so you will need to power the QT Py + BFF over USB or in some other way provide 5V on the 5V power line.
However, to make it easier to power-cycle the peripheral, we have wired up a P-FET for switching the 5V host power on and off, you can either short the pin to A0 or you can use the MAX3421E's GPIO pin.
We include some header that you can solder to your QT Py. You can also pick up an Itsy Bitsy short female header kit to make it removable but compact; you'll need to trim the headers to 7 pins long.
Remember, you need driver support for the MAX3421E (see above for chips that are known to work). Unless you're using a generic mouse, keyboard, CDC serial, or USB mass storage device, you will also need a USB driver that knows how to talk to the device—and writing a driver is non-trivial.
QT Py or Xiao is not included!
Page last edited May 06, 2024
Text editor powered by tinymce.
Pinouts
Micro-B USB Port
On the bottom edge of the board is the micro-B USB port. This port can be used for sending or receiving data from an attached USB device with the MAX3421E. Since it's an "OTG" connector, you'll need a basic OTG adapter (such as those sold for use with the Pi Zero) to convert to type A.
MAX3421E Control Pins
The MAX3421E USB Host chip on the BFF uses SPI plus an IRQ pin to send data to just about any USB device.
- MOSI - This is the SPI MOSI (Microcontroller Out / Serial In) pin.
- MISO - This is the SPI MISO (Microcontroller In Serial Out) pin.
- SCK - This is the SPI clock input pin.
- CS - This is the chip select pin. It is connected to pin A1 on the BFF. It is also broken out directly next to pin A1.
- IRQ - This is the interrupt pin for the MAX3421E. It is connected to pin A2 on the BFF and is broken out directly next to pin A2.
5V Enable Pin and Jumper
- 5VEN - Directly next to pin A0 on the BFF is a pin labeled 5VEN. This is the 5V enable output from the 5V selection circuitry. If you tie this pin to ground, it can disable 5V power to the micro-B USB port, effectively turning off your connected USB device.
- 5V Enable Jumper - A0 - On the back of the BFF, to the left of the 5VEN pin, is an open jumper pad surrounding pin A0. If you solder this jumper closed you will connect the 5V enable pin to A0 on the BFF. This means you can then enable or disable 5V to the micro-B USB port with this GPIO pin.
- 5V Enable Jumper - GPOUT0 - On the back of the BFF, to the right of the 5VEN pin, is an open jumper. It is outlined in white on the board silk. If you solder this jumper closed you will connect the 5V enable pin to GPOUT0 (general purpose output 0) on the MAX3421E. This output is controllable over SPI, which means you can then enable or disable 5V to the micro-B USB port in software. GPOUT0 is only accessible with Arduino.
Page last edited May 06, 2024
Text editor powered by tinymce.
CircuitPython
It's easy to use the USB Host BFF with CircuitPython and the max3421e core module. This module allows you to easily write Python code that lets you utilize the MAX3421E USB Host chip. CircuitPython support is available for these QT Py and Xiao form factor boards:
This support is currently available in the 9.1 beta builds of CircuitPython. On CircuitPython.org, search for and select your board via the Downloads tab. On the board page, scroll down to the 9.1 beta and select your UF2 and/or BIN file.
Plug a USB Host BFF into your QT Py or Xiao form factor board. Then, plug an OTG adapter into the BFF. Here's an example connecting a QT Py ESP32-S3 to the BFF with a micro-B USB OTG cable plugged into the micro-B USB port on the BFF.
CircuitPython Usage
To use with CircuitPython, you need to update code.py with the example script.
Thankfully, we can do this in one go. In the example below, click the Download Project Bundle button below to download the code.py file in a zip file. Extract the contents of the zip file and copy the code.py file to your CIRCUITPY drive.
Device Info Example Code
Once everything is saved to the CIRCUITPY drive, connect to the serial console to see the data printed out!
# SPDX-FileCopyrightText: 2024 Scott Shawcroft for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""USB Host BFF Device Info CircuitPython Example"""
import time
import board
import max3421e
import usb
spi = board.SPI()
cs = board.A1
irq = board.A2
host_chip = max3421e.Max3421E(spi, chip_select=cs, irq=irq)
while True:
print("Finding devices:")
for device in usb.core.find(find_all=True):
# pylint: disable=line-too-long
print(f"{device.idVendor:04x}:{device.idProduct:04x}: {device.manufacturer} {device.product}")
time.sleep(5)
First, the MAX3421E is instantiated over SPI. In the loop, a scan is performed every 5 seconds on the USB host port for a connected USB device. If a device is detected, its VID, PID, manufacturer name and product name are printed to the serial monitor. You can plug and unplug devices to experiment with this.
HID Device Report Example
Update the code.py file on your CIRCUITPY drive with the code below. You'll need a USB HID keyboard to read device reports from.
# SPDX-FileCopyrightText: 2024 Liz Clark for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""USB Host BFF CircuitPython Example
Read key report from attached keyboard"""
import time
import array
import board
import max3421e
import usb
spi = board.SPI()
cs = board.A1
irq = board.A2
host_chip = max3421e.Max3421E(spi, chip_select=cs, irq=irq)
device = None
vid = None
pid = None
while device is None:
for d in usb.core.find(find_all=True):
vid = d.idVendor
pid = d.idProduct
device = usb.core.find(idVendor=vid, idProduct=pid)
time.sleep(1)
device.set_configuration()
print(f"{device.idVendor:04x}:{device.idProduct:04x}: {device.manufacturer} {device.product}")
# Test to see if the kernel is using the device and detach it.
if device.is_kernel_driver_active(0):
device.detach_kernel_driver(0)
# Boot keyboards have 8 byte reports
buf = array.array("B", [0] * 8)
while True:
try:
count = device.read(0x81, buf)
# pylint: disable=broad-except
except Exception as e:
continue
for i in range(0, 8):
print(buf[i], end=" ")
print()
In this example, the HID input report from an HID keyboard are logged to the serial monitor. The code begins by instantiating the MAX3421E over SPI. Then, a scan is performed to find a USB device. When the HID device is found, its VID and PID are used to instantiate the device.
In the loop, the read() function is used to read the incoming report from the device. When a report is received, the buffer is printed to the serial monitor. The buffer is 8 bytes long. The first index contains any modifier keys and the third index contains the key number. For example, the letter A is 0x04 or 4. When all keys are released, the buffer is filled with a 0 byte in each index.
If you plug in a USB HID keyboard to the USB Host BFF and press keys on it, you'll see those keycodes print to the serial monitor as keys are pressed and released.
Page last edited May 06, 2024
Text editor powered by tinymce.
Arduino
Using the USB Host BFF with Arduino involves connecting your BFF to your Arduino-compatible QT Py or Xiao form factor board that has TinyUSB support (ESP32, ESP32-S2 or ESP32-S3, nRF52840, SAMD21/51, RP2040 chips), plugging a USB device into the USB OTG port on the USB Host BFF, installing the Adafruit_TinyUSB_Arduino library, and running the provided example code.
Plug a USB Host BFF into your QT Py or Xiao form factor board. Then, plug an OTG adapter into the BFF. Here's an example connecting a QT Py ESP32-S3 to the BFF with a micro-B USB OTG cable plugged into the micro-B USB port on the BFF.
Library Installation
You can install the Adafruit_TinyUSB_Arduino library using the Library Manager in the Arduino IDE.
Click the Manage Libraries ... menu item, search for Adafruit_TinyUSB, and select the Adafruit TinyUSB library:
If asked about dependencies, click "Install all".
If the "Dependencies" window does not come up, then you already have the dependencies installed.
// SPDX-FileCopyrightText: 2024 Ha Thach for Adafruit Industries
//
// SPDX-License-Identifier: MIT
/*********************************************************************
Adafruit invests time and resources providing this open source code,
please support Adafruit and open-source hardware by purchasing
products from Adafruit!
MIT license, check LICENSE for more information
Copyright (c) 2019 Ha Thach for Adafruit Industries
All text above, and the splash screen below must be included in
any redistribution
*********************************************************************/
/* This example demonstrates use of both device and host, where
* - Device run on native usb controller (roothub port0)
* - Host run on MAX3421E controller (roothub port1) tested with:
* - SAMD21, SAMD51, nRF52840, ESP32S2, ESP32S3, ESP32
* - RP2040: "pio_usb.h" must not be included, otherwise pio-usb will be used as host controller
*
* Requirements:
* - SPI instance, CS pin, INT pin are correctly configured
*/
/* Host example will get device descriptors of attached devices and print it out:
* Device 1: ID 046d:c52f
Device Descriptor:
bLength 18
bDescriptorType 1
bcdUSB 0200
bDeviceClass 0
bDeviceSubClass 0
bDeviceProtocol 0
bMaxPacketSize0 8
idVendor 0x046d
idProduct 0xc52f
bcdDevice 2200
iManufacturer 1 Logitech
iProduct 2 USB Receiver
iSerialNumber 0
bNumConfigurations 1
*
*/
#include "Adafruit_TinyUSB.h"
#include "SPI.h"
// USB Host with MAX3421E
Adafruit_USBH_Host USBHost(&SPI, A1, A2);
// Language ID: English
#define LANGUAGE_ID 0x0409
typedef struct {
tusb_desc_device_t desc_device;
uint16_t manufacturer[32];
uint16_t product[48];
uint16_t serial[16];
bool mounted;
} dev_info_t;
// CFG_TUH_DEVICE_MAX is defined by tusb_config header
dev_info_t dev_info[CFG_TUH_DEVICE_MAX] = { 0 };
//--------------------------------------------------------------------+
// setup() & loop()
//--------------------------------------------------------------------+
void setup() {
Serial.begin(115200);
while ( !Serial ) delay(10); // wait for native usb
Serial.println("starting usb..");
// init host stack on controller (rhport) 1
USBHost.begin(1);
Serial.println("usb started");
Serial.println("TinyUSB Dual: Device Info Example with MAX3421E");
}
void loop() {
USBHost.task();
Serial.flush();
}
//--------------------------------------------------------------------+
// TinyUSB Host callbacks
//--------------------------------------------------------------------+
void print_device_descriptor(tuh_xfer_t *xfer);
void utf16_to_utf8(uint16_t *temp_buf, size_t buf_len);
void print_lsusb(void) {
bool no_device = true;
for (uint8_t daddr = 1; daddr < CFG_TUH_DEVICE_MAX + 1; daddr++) {
// TODO can use tuh_mounted(daddr), but tinyusb has an bug
// use local connected flag instead
dev_info_t *dev = &dev_info[daddr - 1];
if (dev->mounted) {
Serial.printf("Device %u: ID %04x:%04x %s %s\r\n", daddr,
dev->desc_device.idVendor, dev->desc_device.idProduct,
(char *) dev->manufacturer, (char *) dev->product);
no_device = false;
}
}
if (no_device) {
Serial.println("No device connected (except hub)");
}
}
// Invoked when device is mounted (configured)
void tuh_mount_cb(uint8_t daddr) {
Serial.printf("Device attached, address = %d\r\n", daddr);
dev_info_t *dev = &dev_info[daddr - 1];
dev->mounted = true;
// Get Device Descriptor
tuh_descriptor_get_device(daddr, &dev->desc_device, 18, print_device_descriptor, 0);
}
/// Invoked when device is unmounted (bus reset/unplugged)
void tuh_umount_cb(uint8_t daddr) {
Serial.printf("Device removed, address = %d\r\n", daddr);
dev_info_t *dev = &dev_info[daddr - 1];
dev->mounted = false;
// print device summary
print_lsusb();
}
void print_device_descriptor(tuh_xfer_t *xfer) {
if (XFER_RESULT_SUCCESS != xfer->result) {
Serial.printf("Failed to get device descriptor\r\n");
return;
}
uint8_t const daddr = xfer->daddr;
dev_info_t *dev = &dev_info[daddr - 1];
tusb_desc_device_t *desc = &dev->desc_device;
Serial.printf("Device %u: ID %04x:%04x\r\n", daddr, desc->idVendor, desc->idProduct);
Serial.printf("Device Descriptor:\r\n");
Serial.printf(" bLength %u\r\n" , desc->bLength);
Serial.printf(" bDescriptorType %u\r\n" , desc->bDescriptorType);
Serial.printf(" bcdUSB %04x\r\n" , desc->bcdUSB);
Serial.printf(" bDeviceClass %u\r\n" , desc->bDeviceClass);
Serial.printf(" bDeviceSubClass %u\r\n" , desc->bDeviceSubClass);
Serial.printf(" bDeviceProtocol %u\r\n" , desc->bDeviceProtocol);
Serial.printf(" bMaxPacketSize0 %u\r\n" , desc->bMaxPacketSize0);
Serial.printf(" idVendor 0x%04x\r\n" , desc->idVendor);
Serial.printf(" idProduct 0x%04x\r\n" , desc->idProduct);
Serial.printf(" bcdDevice %04x\r\n" , desc->bcdDevice);
// Get String descriptor using Sync API
Serial.printf(" iManufacturer %u ", desc->iManufacturer);
if (XFER_RESULT_SUCCESS ==
tuh_descriptor_get_manufacturer_string_sync(daddr, LANGUAGE_ID, dev->manufacturer, sizeof(dev->manufacturer))) {
utf16_to_utf8(dev->manufacturer, sizeof(dev->manufacturer));
Serial.printf((char *) dev->manufacturer);
}
Serial.printf("\r\n");
Serial.printf(" iProduct %u ", desc->iProduct);
if (XFER_RESULT_SUCCESS ==
tuh_descriptor_get_product_string_sync(daddr, LANGUAGE_ID, dev->product, sizeof(dev->product))) {
utf16_to_utf8(dev->product, sizeof(dev->product));
Serial.printf((char *) dev->product);
}
Serial.printf("\r\n");
Serial.printf(" iSerialNumber %u ", desc->iSerialNumber);
if (XFER_RESULT_SUCCESS ==
tuh_descriptor_get_serial_string_sync(daddr, LANGUAGE_ID, dev->serial, sizeof(dev->serial))) {
utf16_to_utf8(dev->serial, sizeof(dev->serial));
Serial.printf((char *) dev->serial);
}
Serial.printf("\r\n");
Serial.printf(" bNumConfigurations %u\r\n", desc->bNumConfigurations);
// print device summary
print_lsusb();
}
//--------------------------------------------------------------------+
// String Descriptor Helper
//--------------------------------------------------------------------+
static void _convert_utf16le_to_utf8(const uint16_t *utf16, size_t utf16_len, uint8_t *utf8, size_t utf8_len) {
// TODO: Check for runover.
(void) utf8_len;
// Get the UTF-16 length out of the data itself.
for (size_t i = 0; i < utf16_len; i++) {
uint16_t chr = utf16[i];
if (chr < 0x80) {
*utf8++ = chr & 0xff;
} else if (chr < 0x800) {
*utf8++ = (uint8_t) (0xC0 | (chr >> 6 & 0x1F));
*utf8++ = (uint8_t) (0x80 | (chr >> 0 & 0x3F));
} else {
// TODO: Verify surrogate.
*utf8++ = (uint8_t) (0xE0 | (chr >> 12 & 0x0F));
*utf8++ = (uint8_t) (0x80 | (chr >> 6 & 0x3F));
*utf8++ = (uint8_t) (0x80 | (chr >> 0 & 0x3F));
}
// TODO: Handle UTF-16 code points that take two entries.
}
}
// 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;
}
// TODO: Handle UTF-16 code points that take two entries.
}
return total_bytes;
}
void utf16_to_utf8(uint16_t *temp_buf, size_t buf_len) {
size_t utf16_len = ((temp_buf[0] & 0xff) - 2) / sizeof(uint16_t);
size_t utf8_len = _count_utf8_bytes(temp_buf + 1, utf16_len);
_convert_utf16le_to_utf8(temp_buf + 1, utf16_len, (uint8_t *) temp_buf, buf_len);
((uint8_t *) temp_buf)[utf8_len] = '\0';
}
Upload the sketch to your board and open up the Serial Monitor (Tools -> Serial Monitor) at 115200 baud. As you plug and unplug your USB device to the USB Host BFF, you will see the device info print out to the Serial Monitor.
// SPDX-FileCopyrightText: 2024 Ha Thach for Adafruit Industries
//
// SPDX-License-Identifier: MIT
/*********************************************************************
Adafruit invests time and resources providing this open source code,
please support Adafruit and open-source hardware by purchasing
products from Adafruit!
MIT license, check LICENSE for more information
Copyright (c) 2019 Ha Thach for Adafruit Industries
All text above, and the splash screen below must be included in
any redistribution
*********************************************************************/
#include "Adafruit_TinyUSB.h"
#include "SPI.h"
Adafruit_USBH_Host USBHost(&SPI, A1, A2);
void setup() {
Serial.begin(115200);
// init host stack on controller (rhport) 1
USBHost.begin(1);
// while ( !Serial ) delay(10); // wait for native usb
Serial.println("TinyUSB Dual: HID Device Report Example");
}
void loop() {
USBHost.task();
Serial.flush();
}
extern "C" {
// Invoked when device with hid interface is mounted
// Report descriptor is also available for use.
// tuh_hid_parse_report_descriptor() can be used to parse common/simple enough
// descriptor. Note: if report descriptor length > CFG_TUH_ENUMERATION_BUFSIZE,
// it will be skipped therefore report_desc = NULL, desc_len = 0
void tuh_hid_mount_cb(uint8_t dev_addr, uint8_t instance, uint8_t const *desc_report, uint16_t desc_len) {
(void) desc_report;
(void) desc_len;
uint16_t vid, pid;
tuh_vid_pid_get(dev_addr, &vid, &pid);
Serial.printf("HID device address = %d, instance = %d is mounted\r\n", dev_addr, instance);
Serial.printf("VID = %04x, PID = %04x\r\n", vid, pid);
if (!tuh_hid_receive_report(dev_addr, instance)) {
Serial.printf("Error: cannot request to receive report\r\n");
}
}
// Invoked when device with hid interface is un-mounted
void tuh_hid_umount_cb(uint8_t dev_addr, uint8_t instance) {
Serial.printf("HID device address = %d, instance = %d is unmounted\r\n", dev_addr, instance);
}
// Invoked when received report from device via interrupt endpoint
void tuh_hid_report_received_cb(uint8_t dev_addr, uint8_t instance, uint8_t const *report, uint16_t len) {
Serial.printf("HIDreport : ");
for (uint16_t i = 0; i < len; i++) {
Serial.printf("0x%02X ", report[i]);
}
Serial.println();
// continue to request to receive report
if (!tuh_hid_receive_report(dev_addr, instance)) {
Serial.printf("Error: cannot request to receive report\r\n");
}
}
} // extern C
Upload the sketch to your board and open up the Serial Monitor (Tools -> Serial Monitor) at 115200 baud. Plug in a USB HID keyboard to the USB Host BFF. As you press keys, you'll see the report print to the Serial Monitor as keys are pressed and released.
Page last edited May 06, 2024
Text editor powered by tinymce.
Downloads
Files
Page last edited May 06, 2024
Text editor powered by tinymce.