To read data from a keyboard in CircuitPython you must use a device which supports USB Host such as the Metro RP2350 or Feather RP2040 USB Host.
Feather RP2040 USB Host Wiring
Simply plug the keyboard into the USB Host port on the Feather RP2040 USB Host
Metro RP2350 Wiring
Connecting to the Metro RP2350 USB Host port requires soldering pins to the broken out USB Host connections as shown in this guide page. Make the following connections between the Metro USB Host pins and CH334F host connection opposite the USB C connector.
- GND to GND with Black or Blue
- D+ to D+ with Green
- D- to D- with White
- 5V to 5V with Red
Note that the data pins are swapped on the Metro compared to the CH334F breakout. On the Metro D- is next to 5V, whereas on the CH334F D- is next to GND.
Be sure to connect D- on the breakout to D- on the Metro, and D+ on the breakout to D+ on the Metro.
USB Host is under active development. As of Pico-PIO-USB version 0.7.1 and TinyUSB version 3.4.4, the wired USB Mouse only works on the Metro RP2350 with Arduino when connected through a USB hub such as the CH334F.
Install the Libraries
You can install the libraries for this project using the Library Manager in the Arduino IDE.
Click the Library Manager icon, search for Adafruit TinyUSB Arduino, and select the Adafruit TinyUSB Library.
If asked about dependencies click "Install All".
Then install the Pico PIO USB library. Click the Library Manager icon menu item again, search for PIO USB, and select the Pico PIO USB library by sekigon-gonnoc.
Code Prep
The code consists of a main usbhost_keyboard_simpletest.ino program file and a separate header file usbh_helper.h. The header file stores configuration for USB host on the RP2040/RP2350. You'll need both of these files to properly compile and run the project.
// SPDX-FileCopyrightText: 2025 Tim Cocks 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 usb host with a standard HID boot keyboard. * - Host depends on MCU: * - rp2040: bit-banging 2 GPIOs with Pico-PIO-USB library (roothub port1) * * Requirements: * - For rp2040: * - Pico-PIO-USB library * - 2 consecutive GPIOs: D+ is defined by PIN_USB_HOST_DP, D- = D+ +1 * - Provide VBus (5v) and GND for peripheral */ // USBHost is defined in usbh_helper.h #include "usbh_helper.h" #include "tusb.h" #include "Adafruit_TinyUSB.h" bool printed_blank = false; void setup() { Serial.begin(115200); // configure pio-usb: defined in usbh_helper.h rp2040_configure_pio_usb(); // run host stack on controller (rhport) 1 // Note: For rp2040 pico-pio-usb, calling USBHost.begin() on core1 will have most of the // host bit-banging processing works done in core1 to free up core0 for other works USBHost.begin(1); delay(3000); Serial.print("USB D+ Pin:"); Serial.println(PIN_USB_HOST_DP); Serial.print("USB 5V Pin:"); Serial.println(PIN_5V_EN); } void loop() { USBHost.task(); Serial.flush(); } //--------------------------------------------------------------------+ // HID Host Callback Functions //--------------------------------------------------------------------+ void tuh_hid_mount_cb(uint8_t dev_addr, uint8_t instance, uint8_t const* desc_report, uint16_t desc_len) { Serial.printf("HID device mounted (address %d, instance %d)\n", dev_addr, instance); // Start receiving HID reports if (!tuh_hid_receive_report(dev_addr, instance)) { Serial.printf("Error: cannot request to receive report\n"); } } void tuh_hid_umount_cb(uint8_t dev_addr, uint8_t instance) { Serial.printf("HID device unmounted (address %d, instance %d)\n", dev_addr, instance); } void tuh_hid_report_received_cb(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len) { if (len > 0){ //debug print report data // Serial.print("Report data: "); // for (int i = 0; i < len; i++) { // Serial.print(report[i], HEX); // Serial.print(" "); // } // Serial.println(); printKeyboardReport(report); } // Continue to receive the next report if (!tuh_hid_receive_report(dev_addr, instance)) { Serial.println("Error: cannot request to receive report"); } } // Helper function to print key names void printKeyName(uint8_t key) { // This is a simplified list. Full HID keyboard has many more key codes switch (key) { case 0x04: Serial.print("A"); break; case 0x05: Serial.print("B"); break; case 0x06: Serial.print("C"); break; case 0x07: Serial.print("D"); break; case 0x08: Serial.print("E"); break; case 0x09: Serial.print("F"); break; case 0x0A: Serial.print("G"); break; case 0x0B: Serial.print("H"); break; case 0x0C: Serial.print("I"); break; case 0x0D: Serial.print("J"); break; case 0x0E: Serial.print("K"); break; case 0x0F: Serial.print("L"); break; case 0x10: Serial.print("M"); break; case 0x11: Serial.print("N"); break; case 0x12: Serial.print("O"); break; case 0x13: Serial.print("P"); break; case 0x14: Serial.print("Q"); break; case 0x15: Serial.print("R"); break; case 0x16: Serial.print("S"); break; case 0x17: Serial.print("T"); break; case 0x18: Serial.print("U"); break; case 0x19: Serial.print("V"); break; case 0x1A: Serial.print("W"); break; case 0x1B: Serial.print("X"); break; case 0x1C: Serial.print("Y"); break; case 0x1D: Serial.print("Z"); break; case 0x1E: Serial.print("1"); break; case 0x1F: Serial.print("2"); break; case 0x20: Serial.print("3"); break; case 0x21: Serial.print("4"); break; case 0x22: Serial.print("5"); break; case 0x23: Serial.print("6"); break; case 0x24: Serial.print("7"); break; case 0x25: Serial.print("8"); break; case 0x26: Serial.print("9"); break; case 0x27: Serial.print("0"); break; case 0x28: Serial.print("ENTER"); break; case 0x29: Serial.print("ESC"); break; case 0x2A: Serial.print("BACKSPACE"); break; case 0x2B: Serial.print("TAB"); break; case 0x2C: Serial.print("SPACE"); break; case 0x2D: Serial.print("MINUS"); break; case 0x2E: Serial.print("EQUAL"); break; case 0x2F: Serial.print("LBRACKET"); break; case 0x30: Serial.print("RBRACKET"); break; case 0x31: Serial.print("BACKSLASH"); break; case 0x33: Serial.print("SEMICOLON"); break; case 0x34: Serial.print("QUOTE"); break; case 0x35: Serial.print("GRAVE"); break; case 0x36: Serial.print("COMMA"); break; case 0x37: Serial.print("PERIOD"); break; case 0x38: Serial.print("SLASH"); break; case 0x39: Serial.print("CAPS_LOCK"); break; case 0x4F: Serial.print("RIGHT_ARROW"); break; case 0x50: Serial.print("LEFT_ARROW"); break; case 0x51: Serial.print("DOWN_ARROW"); break; case 0x52: Serial.print("UP_ARROW"); break; default: if (key >= 0x3A && key <= 0x45) { // F1-F12 Serial.print("F"); Serial.print(key - 0x3A + 1); } else { // For keys not handled above, just print the HID code Serial.print("0x"); Serial.print(key, HEX); } break; } } void printKeyboardReport(uint8_t const* report) { // First byte contains modifier keys uint8_t modifiers = report[0]; // Print modifier keys if pressed if (modifiers > 0) { Serial.print("Modifiers: "); if (modifiers & 0x01) Serial.print("LEFT_CTRL "); if (modifiers & 0x02) Serial.print("LEFT_SHIFT "); if (modifiers & 0x04) Serial.print("LEFT_ALT "); if (modifiers & 0x08) Serial.print("LEFT_GUI "); if (modifiers & 0x10) Serial.print("RIGHT_CTRL "); if (modifiers & 0x20) Serial.print("RIGHT_SHIFT "); if (modifiers & 0x40) Serial.print("RIGHT_ALT "); if (modifiers & 0x80) Serial.print("RIGHT_GUI "); Serial.println(); } // Second byte is reserved (usually 0) // Bytes 2-7 contain up to 6 key codes bool keysPressed = false; for (int i = 2; i < 8; i++) { uint8_t key = report[i]; // Skip if no key or error rollover if (key == 0 || key == 1) { continue; } if (!keysPressed) { Serial.print("Keys: "); keysPressed = true; } // Print key name based on HID Usage Tables for standard keyboard printKeyName(key); Serial.print(" "); } if (keysPressed) { Serial.println(); } else if (modifiers == 0) { Serial.println("No keys pressed"); } }
The character mapping in the code above is for a US 104 key QWERTY keyboard. Other keyboard layouts have different mappings. You can often get the USB value map for other keyboards online or just hook up your keyboard and enter keys to determine which key maps to which character.
// 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 *********************************************************************/ #ifndef USBH_HELPER_H #define USBH_HELPER_H #ifdef ARDUINO_ARCH_RP2040 // pio-usb is required for rp2040 host #include "pio_usb.h" // Pin D+ for host, D- = D+ + 1 #ifndef PIN_USB_HOST_DP #define PIN_USB_HOST_DP 16 #endif // Pin for enabling Host VBUS. comment out if not used #ifndef PIN_5V_EN #define PIN_5V_EN 18 #endif #ifndef PIN_5V_EN_STATE #define PIN_5V_EN_STATE 1 #endif #endif // ARDUINO_ARCH_RP2040 #ifdef ARDUINO_ARCH_RP2350 // pio-usb is required for rp2040 host #include "pio_usb.h" // Pin D+ for host, D- = D+ + 1 #ifndef PIN_USB_HOST_DP #define PIN_USB_HOST_DP 32 #endif // Pin for enabling Host VBUS. comment out if not used #ifndef PIN_5V_EN #define PIN_5V_EN 29 #endif #ifndef PIN_5V_EN_STATE #define PIN_5V_EN_STATE 1 #endif #endif // ARDUINO_ARCH_RP2350 #include "Adafruit_TinyUSB.h" #if defined(CFG_TUH_MAX3421) && CFG_TUH_MAX3421 // USB Host using MAX3421E: SPI, CS, INT #include "SPI.h" #if defined(ARDUINO_METRO_ESP32S2) Adafruit_USBH_Host USBHost(&SPI, 15, 14); #elif defined(ARDUINO_ADAFRUIT_FEATHER_ESP32_V2) Adafruit_USBH_Host USBHost(&SPI, 33, 15); #else // Default CS and INT are pin 10, 9 Adafruit_USBH_Host USBHost(&SPI, 10, 9); #endif #else // Native USB Host such as rp2040 Adafruit_USBH_Host USBHost; #endif //--------------------------------------------------------------------+ // Helper Functions //--------------------------------------------------------------------+ #ifdef ARDUINO_ARCH_RP2040 static void rp2040_configure_pio_usb(void) { //while ( !Serial ) delay(10); // wait for native usb Serial.println("Core1 setup to run TinyUSB host with pio-usb"); #ifdef PIN_5V_EN pinMode(PIN_5V_EN, OUTPUT); digitalWrite(PIN_5V_EN, PIN_5V_EN_STATE); #endif pio_usb_configuration_t pio_cfg = PIO_USB_DEFAULT_CONFIG; pio_cfg.pin_dp = PIN_USB_HOST_DP; #if defined(ARDUINO_RASPBERRY_PI_PICO_W) // For pico-w, PIO is also used to communicate with cyw43 // Therefore we need to alternate the pio-usb configuration // details https://github.com/sekigon-gonnoc/Pico-PIO-USB/issues/46 pio_cfg.sm_tx = 3; pio_cfg.sm_rx = 2; pio_cfg.sm_eop = 3; pio_cfg.pio_rx_num = 0; pio_cfg.pio_tx_num = 1; pio_cfg.tx_ch = 9; #endif USBHost.configure_pio_usb(1, &pio_cfg); } #endif #endif
Upload and Test
Before uploading, you'll need to update some settings in the Boards menu. Select the appropriate board that you are using, either Adafruit Feather RP2040 USB Host, or Adafruit Metro RP2350. Under USB Stack select Adafruit TinyUSB. Then, upload the sketch to your board. You can use the Serial Monitor in the Arduino IDE for debugging any errors.
Serial Output
The code will connect to the USB keyboard and read data coming from it. The keys pressed along with any modifier keys applied will be printed to the serial monitor.
Page last edited April 24, 2025
Text editor powered by tinymce.