Pico_USB_Disp - Library Usage

For simple usage instructions, please see the Quick Start section on the Home Page.

This library turns a USB display adapter (DisplayLink DL-1xx, etc.) into an external display that you can drive from an MCU. The default color format is RGB565 (16-bit); 24-bit RGB888 is also supported.

Platform USB host Supported chips
Raspberry Pi Pico / Pico 2 (RP2040 / RP2350) Implemented in PIO (Full-Speed) DisplayLink DL-1xx
ESP32-S2 / S3 USB-OTG (Full-Speed) DisplayLink DL-1xx
ESP32-P4 USB-OTG (High-Speed) DL-1xx + MCT Trigger 6 + MS91xx (T6 / MS91xx need the board option PSRAM: Enabled)
Teensy 4.0 / 4.1 Built-in EHCI (High-Speed) DisplayLink DL-1xx
PC (Windows / Linux / macOS) libusb DL-1xx + MCT Trigger 6 + MS91xx

1. Installation (Arduino)

Search for Pico_USB_Disp in the Library Manager and install it. Use the following board cores:

2. Wiring

Raspberry Pi Pico / Pico 2 - Add an extra USB connector (e.g. a board-mount USB-A female connector) and plug the adapter into it:

Adapter side Pico side
D+ GPIO16 (configurable)
D- GPIO17 (must be the GPIO adjacent to D+; reversed order is also OK)
VBUS (5V) VBUS (5V). Watch the current draw of bus-powered adapters
GND GND

If you are using a board equipped with a USB connector, simply connect the adapter.

ESP32-S2 / S3 / P4 - Just plug the adapter into the board’s USB-OTG connector (an OTG adapter cable may be required). No GPIO configuration is needed.

Teensy 4.1 - Use the 5-pin USB host header in the middle of the board (solder pins and use a USB host cable, e.g. PJRC’s). The 5V and GND pins of the header power the adapter. No GPIO configuration is needed.

Teensy 4.0 - The host D+/D- pads are on the bottom side of the board; wire them to a USB-A female connector together with 5V (VUSB) and GND.

3. Minimal code

#include <usb_disp.h>

usb_disp_t *disp;

void setup() {
    usb_disp_init();                    // 1. Initialize
    disp = usb_disp_add(0, 16, 17);     // 2. Register a port (D+=GPIO16, D-=GPIO17)
    usb_disp_start();                   // 3. Start the host
}

void loop() {
    usb_disp_poll(disp);                // 4. Call periodically (handles connection)
    if (usb_disp_ready(disp)) {
        // 5. Once connected, draw whenever you like
        usb_disp_fill(disp, 100, 100, 200, 150, 0xF800);  // red rectangle
        usb_disp_flush(disp, 100);
    }
}

The two GPIO numbers are only meaningful on the Pico. On ESP32-S2/S3/P4, Teensy 4.x and PC the host pins are fixed, so you can drop them entirely:

disp = usb_disp_add(0);             // ESP32 / Teensy / PC: port number only

Passing pins on those platforms is harmless (they are ignored), so the Pico form above still builds everywhere - handy when you want one sketch for several boards.

That is all it takes. No clock setup, no logging setup, no resolution setup (the library handles everything automatically; at connection time it picks the largest resolution the display advertises in its EDID that the adapter chip can drive - see edid_policy in the API reference if you would rather have the display’s native/preferred timing).

Drawing model

  • The default color format is RGB565 (16-bit). usb_disp_update() transfers a pixel array to an arbitrary rectangle, and usb_disp_fill() fills a rectangle with a solid color.
  • 24-bit color (RGB888) is also supported: use usb_disp_update_888(), or switch the display depth itself with usb_disp_set_depth(). See 24-bit color in the API Reference for details.
  • Call usb_disp_flush() at the end of each batch of drawing (it pushes out any buffered data).
  • The frame buffer lives inside the adapter, so anything you have drawn stays on screen without being resent. Updating only the parts that change is sufficient.

Connect / Disconnect events

usb_disp_poll() returns true at the moment the display becomes READY and at the moment it is disconnected:

if (usb_disp_poll(disp)) {
    if (usb_disp_ready(disp)) {
        // Connected: the resolution is now known. Draw the initial screen.
    } else {
        // Unplugged: it will re-enumerate automatically when reconnected.
    }
}

4. Using GUI libraries

LovyanGFX

Install LovyanGFX and include it; the LGFX_USB_Disp class becomes available automatically (no extra include needed):

#include <LovyanGFX.hpp>
#include <usb_disp.h>

usb_disp_t *disp;
LGFX_USB_Disp lcd;

// Once, after READY:
lcd.setUsbDisp(disp);
lcd.init();
lcd.fillScreen(TFT_NAVY);
lcd.drawCircle(960, 540, 100, TFT_CYAN);

Limitations: no readback / rotation fixed to 0. Color depth is 16-bit by default; 24-bit is available via lcd.setColorDepth(24). Sprites work with full functionality. -> See the LovyanGFX_Demo example.

Note: Teensy 4.x requires LovyanGFX 1.2.27 or later - earlier releases do not support the i.MX RT platform (their platform layer fails to compile).

LVGL (v9)

Install lvgl, then copy the lv_conf.h bundled with the LVGL_Demo example directly into your libraries folder (next to the lvgl folder):

  • Windows: C:\Users\<username>\Documents\Arduino\libraries\lv_conf.h
  • macOS: /Users/<username>/Documents/Arduino/libraries/lv_conf.h
  • Linux: /home/<username>/Arduino/libraries/lv_conf.h

The code is a single line after READY:

#include <lvgl.h>
#include <usb_disp.h>

lv_display_t *lvd = usb_disp_lvgl_create(disp);   // the LVGL display is ready
// From here it is normal LVGL. Call lv_timer_handler() in loop().

-> See the LVGL_Demo example.

5. Pico-specific topics

About core1

On the Pico, the USB host runs permanently on core1 (started by usb_disp_start()). If your sketch does not use setup1()/loop1(), you do not need to think about it.

If your sketch does use setup1()/loop1(), core1 belongs to the sketch, so the library automatically switches to “manual mode”. In that case, keep calling the service function from loop1:

void setup1() {}
void loop1() { usb_disp_task(); }   // Just this. Do not mix in other work.

-> See the TaskMode example.

FreeRTOS mode (the “FreeRTOS SMP” menu option) also switches to manual mode. In that case add a delay - loop1(){ usb_disp_task(); delay(1); } - so that other tasks get CPU time.

Clock

PIO USB requires sys_clk to be a multiple of 12 MHz. If it is not, the library automatically adjusts it downward (default 150 MHz -> 144 MHz). The core voltage (vreg) is never changed. If you want a higher clock, set it yourself at the top of setup() (e.g. 240 MHz is a multiple of 12, so it is used as-is).

6. Troubleshooting

Symptom Fix
Nothing is displayed Call usb_disp_set_log(true) and watch the log. It shows enumeration progress
ESP32-S3 / P4: no device is ever detected, and/or the log spams E HUB: Failed to issue root port reset / E HUB: Root port reset failed / E ENUM: ... FAILED / E USB HOST: usbh_devs_open error: ESP_ERR_INVALID_STATE Two known causes. (1) arduino-esp32 core 3.3.11 enables the usb_host enumeration-filter feature in its prebuilt libraries, and enumeration silently hangs when no filter callback is set - the current version of this library sets the callback and works on 3.3.11; if you hit this with an older copy of the library, update it (or use core 3.3.10). (2) After re-flashing the board, a bus-powered adapter (DL, T6 and MS91xx alike - confirmed on a T6 mid-stream) can be left wedged because the host vanished mid-operation - dev-board VBUS is hardwired, so software cannot power-cycle it: physically replug the adapter
ESP32-P4: a T6 / MS91xx adapter attaches (protocol log looks fine) but nothing is displayed, log shows [T6] FB alloc failed / [MS91xx] FB alloc failed These protocols need a full-frame buffer (several MB), which only fits in PSRAM. Build with the board option PSRAM: Enabled (Tools > PSRAM). The library logs a PSRAM not available warning at attach time when this is the case
The screen suddenly went black Unplugging/replugging on the monitor side can drop the output -> re-run usb_disp_set_auto_mode() (or set_mode) and redraw (this applies to both DL-1xx and MS913x)
Black screen at an unsupported resolution Switch back to a resolution the monitor supports with usb_disp_set_mode()
The resolution collapses to something tiny (e.g. 720x480) behind an HDMI analyzer / splitter / capture box / KVM Such devices commonly list only 480p in their EDID base block and put 720p/1080p in the CEA extension block. The library reads extension blocks, so ask it with usb_disp_edid_supports(d, w, h) before demoting a mode yourself. If the device under-reports even there, force it: usb_disp_add(port, dp, dm, w, h, true) or usb_disp_set_ignore_edid()
Nothing works at all on Pico Check the D+/D- wiring (adjacent GPIOs) and the 5V VBUS supply
Nothing works at all on Teensy Use the USB host port (4.1 = the 5-pin header / 4.0 = the bottom pads), not the micro-USB device port
No image when using setup1/loop1 Make sure you are calling loop1(){ usb_disp_task(); }

7. Examples

Example Description
ColorBars Minimal use of the raw API
DisplayInfo Showing MCU / chip / resolution / EDID / FPS on screen (the Web Flasher firmware; one photo of this screen makes a compatibility report)
FixedResolution Fixing the resolution (including ignore_edid) and changing it at runtime
LovyanGFX_Demo Using the library with LovyanGFX (Teensy requires LovyanGFX 1.2.27 or later)
LVGL_Demo Using the library with LVGL v9
MultiDisplay Driving two displays at once (Pico / Pico 2)
RGB888_Demo 24-bit color (RGB888): side-by-side gradients to compare banding against 16-bit, with runtime depth switching
ShadowFB Reducing transfer volume with differential updates (DL-1xx adapters only; ESP32 / Teensy 4.1 with PSRAM / PC)
TaskMode Coexisting with setup1()/loop1() (Pico / Pico 2)