Skip to content

USB Device

Pico SDK is shipped with TinyUSB, a cross-platform USB stack. Thanks to this library, you can use the USB peripheral on the RP2040 to create a custom USB device, such as a keyboard, mouse, or mass storage device. However, it requires a good understanding of the USB protocol especially about USB descriptors. Device descriptors, configuration descriptors, interface descriptors, endpoint descriptors, and string descriptors... Although thse descriptors are essential for USB communication, they can be quite complex to implement correctly.

pico-jxglib provides a higher-level API that abstracts away the complexities of USB descriptors, making it easier to create custom USB devices. With this library, you can define your USB device's functionality using simple C++ code, without needing to manually craft USB descriptors.

Example: CDC EchoBack

To see how to use pico-jxglib to create a custom USB device, let's look at an example of a Communications Device Class that echos back data.

Below is the additional code to your CMakeLists.txt file:

CMakeLists.txt
1
2
3
4
target_link_libraries(usbdev-cdc
    jxglib_USBDevice)
add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../pico-jxglib pico-jxglib)
jxglib_configure_USBDevice(usbdev-cdc CFG_TUD_CDC 1)
  • Line 2 jxglib_USBDevice is the library that handles various USB descriptors.
  • Line 3 jxglib_configure_USBDevice() generates a configuration file, tusb_config.h, that is required by TinyUSB. Following arguments specify the number of USB interfaces used by the device:
    • CFG_TUD_CDC: CDC interface
    • CFG_TUD_HID: Human Interface Device interface
    • CFG_TUD_MSC: Mass Storage Class interface
    • CFG_TUD_VIDEO: Video Class interface

Below is the complete code for usbdev-cdc.cpp:

usbdev-cdc.cpp
#include "pico/stdlib.h"
#include "jxglib/USBDevice/CDC.h"

using namespace jxglib;

int main(void)
{
    ::stdio_init_all(); 
    USBDevice::Controller deviceController({
            bcdUSB:             0x0200,
            bDeviceClass:       TUSB_CLASS_MISC,
            bDeviceSubClass:    MISC_SUBCLASS_COMMON,
            bDeviceProtocol:    MISC_PROTOCOL_IAD,
            bMaxPacketSize0:    CFG_TUD_ENDPOINT0_SIZE,
            idVendor:           0xcafe,
            idProduct:          USBDevice::GenerateSpecificProductId(0x4000),
            bcdDevice:          0x0100,
        },
        0x0409,
        "pico-jxglib sample",
        "CDC Echo Back",
        "01234567"
    );
    USBDevice::CDC cdc(deviceController, "EchoBack Normal", 0x81, 8, 0x02, 0x82, 64);
    deviceController.Initialize();
    cdc.Initialize();
    for (;;) {
        if (cdc.cdc_available()) {
            char buff[64];
            int bytes = cdc.cdc_read(buff, sizeof(buff));
            cdc.cdc_write(buff, bytes);
            cdc.cdc_write_flush();
        }
        Tickable::Tick();
    }
}
  • Line 9: USBDevice::Controller is the main class that manages the USB descriptor.
  • Line 10-17: These fields are used to define the USB Device descriptor.
  • Line 19: Language Id used in string descriptors. 0x0409 is the language ID for English (United States).
  • Line 20-22: Specify the manufacture, product, and serial number string descriptors.
  • Line 24: Creates an instance of USBDevice::CDC, one of the USB interfaces. Although each interface class has different arguments, it generally requires a string for the interface name and addresses for IN and OUT endpoints. You can create multiple instances of interface classes by assigning different endpoint addresses.