Pi Pico USB Keyboard/Mouse Input

Hi Everyone...

I would like to read input from a USB keyboard and USB mouse on a Pi Pico. Was going to use the TinyUSB library to do this. I have tried the TinyUSB Host example that retrieves info about the USB device and displays it via a serial terminal and it works perfectly. What I can't seem to find is any information or examples on how to read the input from the USB devices. Can anyone point me towards that information?

My ultimate goal is to make an interface to read USB keyboard/mouse for use with a 6502 microprocessor based system.

Thanks.
-Brian

Have you thought about how you will transfer that information from the Pico to the 6502?

I did this once upon a time with the Pico C SDK. You have to handle the tuh_hid_report_received_cb callback. It'll use tuh_hid_interface_protocol to determine whether the report is HID_ITF_PROTOCOL_KEYBOARD or something else. "Something else" may eventually call the same keyboard report handler that HID_ITF_PROTOCOL_KEYBOARD does, it'll just take a convoluted path to get there (if it does).

Where it really starts to get interesting is handling/generating repeated key presses and LED handling.

I honestly don't remember where I got the basic framework for the keyboard stuff from (it's literally been years). I just remember that it was missing a lot of stuff (repeated key pressed and LED handling, oddly enough!) that I had to add to make a working keyboard handler. I eventually got everything together into a pseudo library format that I could use. I've never had the need to transfer it all over to the Arduino ecosystem though.

I never looked at the mouse end of things though I imagine there's basic similarities.

Good luck, have fun, and don't tear too much of your hair out as you learn waaaay more about the innards of low level USB stuff than you ever wanted to!

My original plan is to implement an 8-bit parallel interface and connect it to a 6522 VIA chip. An alternative is using a 74LS373 latch to link the Pico to the 6502 data bus. Also, recently I have learned about the OneROM project. That project uses the Pico's PIO to create a bus interface that responds quick enough to tie the Pico directly to the 6502 bus. Might take a peek down that road as well. Figured the Pico could do keyboard scancode-to-ASCII conversion too.

Thank you for getting me started with "tuh_hid_report_received_cb", "tuh_hid_interface_protocol" and "HID_ITF_PROTOCOL_KEYBOARD" calls. That is already more information than I was able to find using Google. My search results kept turning up how to turn the Pico into a HID device and not how to read an HID device. Knowing the names of these calls has already helped.

I haven't really thought of the mouse side of things yet, just wanted to keep that as a possibility for later in the project.

Thanks for the replies and help.
-Brian

Feel free to mine this for ideas when you get far enough into things:

.
.
.
// 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) {
   switch( tuh_hid_interface_protocol(dev_addr, instance) ) {

      case HID_ITF_PROTOCOL_KEYBOARD:
         USBkeyboard.processKeyboardReport( dev_addr, instance, (hid_keyboard_report_t const*) report );
         break;

      default:
         USBkeyboard.processGenericReport(dev_addr, instance, report, len);
         break;
   }

   // continue to request to receive report
   tuh_hid_receive_report(dev_addr, instance);
}
.
.
.
void My_USBkeyboard_::processKeyboardReport(uint8_t dev_addr, uint8_t instance, hid_keyboard_report_t const *report) {
   static hid_keyboard_report_t prev_report = { 0, 0, {0} }; // previous report to check key released

   uint8_t physCode = 0;
   for( int i = sizeof(report->keycode) - 1; i >= 0; --i ) {
      if( report->keycode[i] ) {
         physCode = report->keycode[i];
         break;
      }
   }

   // if no key is being pressed but a key repeat timer is running
   // then stop the timer (no more key repeats)
   if( (!physCode || physCode != keyboardState.repeatingKey) && keyboardState.repeatTimer ) {
      cancel_alarm(keyboardState.repeatTimer);
      keyboardState.repeatTimer = 0;
      keyboardState.repeatingKey = 0;
   }

   if( physCode ) {
      for( size_t i = 0; i < sizeof(report->keycode); ++i ) {
         if( physCode == prev_report.keycode[i] ) {
            physCode = 0;
            break;
         }
      }
   }
   prev_report = *report;

   if( physCode ) {
      char *pKeyString = generateKeyEvent(dev_addr, instance, physCode, report->modifier);

      if( keyboardState.repeatTimer ) {
         cancel_alarm(keyboardState.repeatTimer);
         keyboardState.repeatTimer = 0;
      }

      if( pKeyString ) {
         keyboardState.repeatTimer = add_alarm_in_ms(
            REPEAT_FIRST_DELAY, 
            keyTimerHandler, 
            pKeyString, 
            true);
         keyboardState.repeatingKey = physCode;
      }
   }
}

void My_USBkeyboard_::processGenericReport(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len) {
   (void) dev_addr;

   uint8_t const rpt_count = hid_info[instance].report_count;
   tuh_hid_report_info_t* rpt_info_arr = hid_info[instance].report_info;
   tuh_hid_report_info_t* rpt_info = NULL;

   if ( rpt_count == 1 && rpt_info_arr[0].report_id == 0 ) {
      // Simple report without report ID as 1st byte
      rpt_info = &rpt_info_arr[0];
   } else {
      // Composite report, 1st byte is report ID, data starts from 2nd byte
      uint8_t const rpt_id = report[0];

      // Find report id in the arrray
      for( uint8_t i = 0; i < rpt_count; ++i ) {
         if( rpt_id == rpt_info_arr[i].report_id ) {
            rpt_info = &rpt_info_arr[i];
            break;
         }
      }

      report++;
      len--;
   }

   if( rpt_info && rpt_info->usage_page == HID_USAGE_PAGE_DESKTOP ) {
      switch( rpt_info->usage ) {

         case HID_USAGE_DESKTOP_KEYBOARD:
            // Assume keyboard follow boot report layout
            processKeyboardReport(dev_addr, instance, (hid_keyboard_report_t const*)report);
            break;

         default:
            break;
      }
   }
}
.
.
.

Do you really need the PIA or a latch though? The Pico is orders of magnitude faster than the 6502. It could implement its own tristateable 8-bit parallel interface without any external chips (besides addressing logic) as long as you have 9 free pins. The only problem I see is the voltage levels. I don't remember if the Pico is 5V-tolerant.

from rp2040-datasheet GPIOs appear to be NOT 5V tolerant

these days for USB OTG Host and Device I tend to use ESP32-S3 microcontrollers

Thank you for the example code. I will look it over.

Modern 6502s and my project will run on 3.3V.

In the past I have interfaced Pico's with slow 5V logic through a 220-ohm resistor.

Also just for fun a while ago I put some 74LS93 counters directly on a Pico without any voltage translation. Ran it continuously for over a month and the Pico didn't seem to care. Maybe really long term might be a problem but I think TTL logic output is within tolerances of the Pico. (Don't take that as fact though. Use at your own risk.) 5V CMOS parts might be a bit too much as their output gets a lot close to Vdd then TTL does.

Never thought of using en ESP32 for this. I considered STM32 or PSoC 5LP but the Pico was a cheaper alternative. I'll also look into the ESP32.

Thanks.
-Brian

if you wish to experiment with OTG Host and device consider the ESP32-S3-DevKitC-1 which has two USB type C connectors one for programming and Serial IO and the other for USB OTG

example from EspUsbHost
File>Examples>EspUsbHost>EspUsbHostKeyboard

for a ESP32S3 without a USB OTG connector connect to
GPIO20 USB_D-
GPIO21 USB_D+

I actually have that exact ESP32 board in my stash. I'll have to dig it out and play with it again.

Thanks to everyone's help, here is a sample program that allows me to read keyboard or mouse HID devices and outputs the raw data to a serial debug terminal.

Also found a document online called "HID Reporter" by John Park that helped explain some details.

//  TinyUSB Host Test
//  =================
//  This reads keyboard/mouse HID devices and prints the data to a serial terminal.

// Using Adafruit's TinyUSB library for USB Host functions.
#include "Adafruit_TinyUSB.h"

// Assign TinyUSB to a function name.
Adafruit_USBH_Host USBHost;

// Setup (Runs only once.)
void setup()
{
  // Initialize Serial Port 1 for debug output.
  // Serial Port 0 is unavailable due to USB being used as host.
  Serial1.setTX(0);
  Serial1.setRX(1);
  Serial1.begin(115200);
  Serial1.println("\r\nUSB Host Test");

  // Initialize USB Host fucntions using built-in USB interface (0).
  USBHost.begin(0);
}

// Main Loop (Runs continuously.)
void loop()
{
  // TinyUSB's Host Functions
  // Seems that this needs to be polled continuously for USB events to be recognized.
  USBHost.task();
}

// USBHost calls this when HID device is mounted.
void tuh_hid_mount_cb(uint8_t device_address, uint8_t instance, uint8_t const *descriptor_report, uint16_t descriptor_length)
{
  // Print USB device address and instance to debug terminal.
  Serial1.printf("MOUNTED: Device Address = %d, Instance = %d \r\n", device_address, instance);

  // I think this is telling the USBHost that data has been processed and is ready to receive more, but I'm not sure.
  // If this is removed, only one packet of data gets processed.
  if (!tuh_hid_receive_report(device_address, instance))
  {
    Serial1.printf("***ERROR*** : Cannot receive HID report. \r\n");
  }
}

// USBHost calles this when HID device is un-mounted.
void tuh_hid_umount_cb(uint8_t device_address, uint8_t instance)
{
  // Print USB device address and instance to debug terminal.
  Serial1.printf("UN-MOUNTED: Device Address = %d, Instance = %d \r\n", device_address, instance);
}

// USBHost calls this when data packet is received from HID device.
void tuh_hid_report_received_cb(uint8_t device_address, uint8_t instance, uint8_t const *report_data, uint16_t report_length)
{
  // Send received data to debug terminal.
  // Keyboard HID devices send a packet of 8 bytes per transfer.
  // Mouse HID devices seem to send a packet of 3 bytes per transfer.
  Serial1.printf("DATA: ");
  for (uint16_t a = 0; a < report_length; a++)
  {
    Serial1.printf("$%02x ", report_data[a]);
  }
  Serial1.println();

  // I think this is telling the USBHost that data has been processed and is ready to receive more, but I'm not sure.
  // If this is removed, only one packet of data gets processed.
  if (!tuh_hid_receive_report(device_address, instance))
  {
    Serial1.printf("***ERROR*** : Cannot receive HID report.\r\n");
  }
}

I'm still not one-hundred percent sure of why things are the way they are, but I seem to have enough to move forward with my project.

if you wish to use USB OTG host there is link on the bottom of the PCB which needs shorting to provide 5V power to the USB OTG port