guru3.net

A bunch of random stuff...
probably a lot of it outdated

Pico SuperKey Board

The Pico SuperKey Board project contains the code to emulate a keyboard in C++ using the Raspberry Pi Pico. Short the contacts, get a key press on a USB HID keyboard.

Project goals:

Background

Searching on how to use TinyUSB led me to Adafruit's TinyUSB Arduino Core and TinyUSB Arduino libraries. The former library is a framework for adding TinyUSB support to device Arduino cores, and the latter library is the implementation of the TinyUSB devices on top of the Arduino core. A bit more searching revealed the TinyUSB Mouse and Keyboard library, a library sitting on top of the Adafruit TinyUSB libraries providing the same API for mouse and keyboard USB HID as the Arduino API.

My train of thought ran thus: if the libraries could be made to work I would have a familiar interface for emulating a keyboard. The first problem was trying to work out what functions of the code in the Pico SDK example were included within the libraries, and what code I would need to keep from the example. The second problem was that the Arduino libraries above all depend on the Arduino core libraries and certain features of this would need to be re-implemented. The third problem was how to debug and fix any problems along the way.

Coding

To work out what functions were needed from the Pico SDK example code, and how to mesh that with the Arduino libraries, I started with a blank project, included "TinyUSB_Mouse_and_Keyboard/TinyUSB_Mouse_and_Keyboard.h", and worked my way down the include tree until every missing reference was resolved. I learned that the required code from the example was the tusb_config.h file, tusb_init(), and tud_task(). Note that in tusb_config.h, the CLASS section should reflect what devices the code will be emulating. Spoiler alert, tud_task() needs to be called at regular intervals. This can be done by adding a timer using add_repeating_timer_ms() that calls it.

The bulk of the missing elements from the Arduino core libraries were the Print, Stream, and String classes. The first I could not find a C standard library equivalent for and so needed to be copied directly from the library. The second inherits from Print. No new member functions were used and so Stream was replaced with Print. The last can be replaced with std::string. There were additionally some special functions for accessing flash memory that needed to have standard C replacements and delay(), which was replaced sleep_ms(). I'm not entirely sure about the special function replacements, but testing suggests they work. The substitutions were made entirely using C preprocessor #defines in a set of stub .h files in includes/. In the future, a better solution for Arduino compatibility may be what appears to be an unofficial implementation of the Arduino API for Raspberry Pi Pico. At the time of writing, there was a conflict with a Serial object and it seemed a bit overkill for what was needed, so I used the stubs instead.

Some new code was required for the Adafruit TinyUSB Core library, Adafruit_USBD_Device::getSerialDescriptor(). This provides TinyUSB with a unique serial number for the device, which for the Pico can be found from pico_get_unique_board_id(). I just cribbed off the example here as the C SDK doesn't seem to ever explain what the pico_unique_board_id_t struct the function fills is. For reference, the struct member id is a PICO_UNIQUE_BOARD_ID_SIZE_BYTES long array, with each one digit of the ID in each byte. Figuring out the new code needed, as well as how things fit together, was greatly aided by looking at Adafruit Arduino Core for SAMD21 and SAMD51 CPU. Also, note that to get the libraries to play nice USE_TINYUSB needed to be defined via add_definitions(-DUSE_TINYUSB) in the CMakesLists.txt file. To define what the USB device name is, USB_PRODUCT should similarly be defined, e.g., using add_definitions(-DUSB_PRODUCT="Pico Keyboard"). Adafruit_USBD_Device.cpp lists additional defines.

Debugging

Debugging was a slightly annoying task. What was not immediately apparent was that a good chunk of the code in the SDK composite HID example was there to control the Pico's built-in LED based on the state of the USB device and did nothing for actually being the USB device. Particularly, the board_init() function for the example just initialises the built-in LED. The led_blinking_task() is responsible for turning the LED on and off at the interval specified in blink_interval_ms. That variable is altered by the tud_*_cb() callback functions from TinyUSB. Adding this code back in allowed me to have a rough idea of what was going on without the ability to use SWD (software debug) or serial print statements over USB. You may be aware of the Hello World/USB example, which creates a USB serial device that responds to printf(). However, examining stdio_usb.c where this functionality is coded up shows that if we explicitly want to link to TinyUSB, say for creating our own Keyboard device, this functionality is disabled. Thus, debugging was limited to the blinking LED and using well-placed gpio_put() calls to turn the LED on once a point in the code was reached.

The biggest issue I had once the code "should have" worked was that it looked like the Pico was locking up. It would get partway through registering as USB device on my desktop and then time out.

Feb 27 19:18:11 localhost kernel: usb 3-6: new full-speed USB device number 20 using xhci_hcd
Feb 27 19:18:11 localhost kernel: usb 3-6: config index 0 descriptor too short (expected 9, got 0)
Feb 27 19:18:11 localhost kernel: usb 3-6: can't read configurations, error -22

This turned out to be due to the TinyUSB Mouse and Keyboard library waiting in an infinite loop for the device to be recognized. Which it couldn't, because tud_task() was only called later in a while() {} loop as suggested by the composite HID example. Once I recognized that was what was going on, adding the timer resolved this problem and everything worked perfectly. The led_blinking_task() call also needed to be in the timer loop. Debugging, and life in general during development was made a lot easier by being able to reset the Pico by shorting the RUN pin to ground. My clever way to do this is using an unfolded paperclip.

Moving forward

Further work... I don't see why the rest of the Adafruit TinyUSB library wouldn't work on the Pico. You should be able to use this (maybe with some Arduino core library tweaks) to have a CDC serial device along with your keyboard device. Or mass storage. Or midi. For whatever devices you're going to have though, remember that tusb_config.h will need to be updated.

In terms of my application of the project, getting a super (Windows) key to hand, I'm currently working on a 3D printed case, probably based off of Botvinnik's Raspberry Pi Pico Case on Thingiverse.