Pico_USB_Disp - API Reference
All functions use C linkage and can be called from both C and C++.
The handle usb_disp_t * refers to one display managed by the library.
Lifecycle
void usb_disp_init(void)
Initializes the subsystem. Call once, first.
usb_disp_t *usb_disp_add(uint8_t port, uint8_t pin_dp, uint8_t pin_dm, uint16_t width = 0, uint16_t height = 0, bool ignore_edid = false)
Registers a display port (call before usb_disp_start()).
port- zero-based host controller number- Pico: PIO block number (RP2040 = 0..1 / RP2350 = 0..2; one port occupies one block)
- ESP32: 0 only (the P4’s second controller - USB 1.1 OTG FS - is not supported yet; support is planned)
- Teensy 4.x: 0 only (the built-in USB host port)
- PC: device number in discovery order
pin_dp/pin_dm- GPIO numbers for D+ / D-. Pico only; the host pins are fixed on every other platform.- Pico: both must be given explicitly and must be adjacent GPIOs (reversed order is fine = swapped wiring is supported). Non-adjacent pins are a registration error and return NULL.
- ESP32 / Teensy / PC: ignored, and omittable in C++ -
usb_disp_add(0)is enough. Passing pins anyway is harmless, so a sketch written for the Pico still builds unchanged on these platforms.
width/height- resolution applied at connection time. 0, 0 (omitted) = automatic selection (by default, the largest resolution the sink advertises in its EDID that fits the chip -> if the EDID is unreadable, the largest entry from the built-in list). Seeedid_policybelow to select the sink’s native/preferred timing instead. If the requested mode cannot be set, the library also falls back to automatic selection, so check the actual resolution withusb_disp_width/height()after READY.ignore_edid-true= never read EDID; usewidth/heightonly. Same asusb_disp_config_t.ignore_edid(see below).- Returns: the handle (NULL if the limit is reached or the pins are invalid)
usb_disp_add(0, 16, 17); // automatic resolution
usb_disp_add(1, 19, 18, 800, 600); // swapped wiring, fixed 800x600
usb_disp_add(0, 16, 17, 1920, 1080, true); // force 1080p, ignore EDID
// ESP32 / Teensy / PC - the pin arguments can be dropped
usb_disp_add(0); // automatic resolution
usb_disp_add(0, 0, 0, 800, 600); // fixed 800x600
Resolution is the 4th and 5th argument.
usb_disp_add(0, 800, 600)puts 800 and 600 into the pin arguments (where they are ignored), silently leaving the resolution automatic - pad the pins with0, 0or useusb_disp_add_cfg().
C callers must pass all six arguments (the defaults are C++ only). Omitting the pins in a Pico build is a compile error (
too few arguments), so the short form can never silently reach a target that actually needs the pins.
usb_disp_t *usb_disp_add_cfg(const usb_disp_config_t *cfg)
Port registration via a struct. Use it for settings usb_disp_add() does not cover:
typedef struct {
uint8_t port; // host controller number (same as add)
uint8_t pin_dp; // [Pico] GPIO for D+
uint8_t pin_dm; // [Pico] GPIO for D- (must be adjacent)
uint16_t width; // 0 = automatic selection
uint16_t height;
uint8_t refresh_hz; // 0 = 60Hz
bool no_auto_mode; // true: skip automatic mode setup on connect
// (the app calls the set_mode family itself)
bool shadow_fb; // true: enable the shadow FB (differential updates) on connect
bool depth24; // true: run in 24-bit color (RGB888)
bool ignore_edid; // true: never read EDID; use width/height only
usb_disp_edid_policy_t edid_policy; // how automatic selection uses EDID
} usb_disp_config_t;
edid_policy - how automatic selection reads EDID
typedef enum {
USB_DISP_EDID_POLICY_MAX = 0, // default
USB_DISP_EDID_POLICY_PREFERRED,
} usb_disp_edid_policy_t;
| Policy | Behaviour |
|---|---|
MAX (default) |
Of everything the EDID advertises - base DTDs, established and standard timings, and the CEA extension’s VICs and DTDs - pick the largest that fits the chip’s pixel limit. Anything beyond the adapter’s max_area or USB_DISP_MAX_WIDTH is filtered out first, so a 4K TV lands on the largest mode the adapter can actually drive (usually 1080p). For DL adapters whose exact SKU cannot be identified, automatic selection additionally caps itself at the smallest SKU of the generation (unidentified DL-1x0: 1,470,000 px = DL-120 class / unidentified DL-1x5: 1,310,720 px = DL-115 class) to avoid picking a mode the chip cannot drive; an explicit usb_disp_set_mode() is not affected by this cap. |
PREFERRED |
Use the base block’s preferred timing (the sink’s declared native resolution) and ignore extension blocks. |
MAX is the default because it keeps application layout simple: you can place widgets at fixed coordinates for a known resolution and let the display scale, instead of querying the size and re-laying-out. PREFERRED is there for the opposite case - driving a panel at exactly its native resolution so nothing is rescaled.
Both policies fall back to the built-in mode list (descending area, EDID-listed entries first) when the EDID is unreadable or nothing fits.
usb_disp_set_edid_policy(d, USB_DISP_EDID_POLICY_PREFERRED);
usb_disp_set_auto_mode(d); // apply it now (the setter alone does not re-select)
| Function | Description |
|---|---|
void usb_disp_set_edid_policy(d, policy) |
Change the policy (before or after connection) |
usb_disp_edid_policy_t usb_disp_edid_policy(d) |
Current policy |
ignore_edid - fixed resolution without EDID
By default the library reads the monitor’s EDID and lets it influence the mode: the preferred timing is used for automatic selection, the built-in fallback list prefers modes the EDID lists, and a late-arriving EDID promotes the mode after READY.
That is the right behaviour for a real monitor, but it can still backfire behind a relay device - an HDMI analyzer, splitter, capture box, or KVM - whose EDID under-reports what it can display. (The library reads CEA extension blocks too, which covers most such devices; see EDID extension blocks below. ignore_edid is the escape hatch for the rest.)
Setting ignore_edid = true makes the library perform no EDID access at all: the mode comes from width/height (or, if those are 0, from the built-in list in descending area order), and none of the EDID-driven promotion/demotion paths run. An explicit usb_disp_read_edid() from your own code still works - the flag only stops the library from acting on EDID by itself.
// via the struct...
usb_disp_config_t cfg = {};
cfg.port = 0; cfg.pin_dp = 16; cfg.pin_dm = 17;
cfg.width = 1920; cfg.height = 1080;
cfg.ignore_edid = true; // force 1080p regardless of what EDID says
usb_disp_t *d = usb_disp_add_cfg(&cfg);
// ...or directly as the 6th argument of usb_disp_add()
usb_disp_t *d = usb_disp_add(0, 16, 17, 1920, 1080, true);
It can also be toggled at runtime:
| Function | Description |
|---|---|
void usb_disp_set_ignore_edid(d, bool on) |
Same as the config field; may be called before or after connection. Enabling it does not change an already-selected mode - call usb_disp_set_mode() if you want to re-force one |
bool usb_disp_ignore_edid(d) |
Current setting |
void usb_disp_start(void)
Starts the host for all registered ports (call once, after the add calls).
On the Pico this takes up residence on core1. When the sketch defines setup1()/loop1() on arduino-pico, or a FreeRTOS menu option is selected, the library automatically switches to manual mode (below).
void usb_disp_start_manual(void) / void usb_disp_task(void)
Manual service mode (only meaningful on the Pico). Call start_manual() instead of usb_disp_start(), then keep calling usb_disp_task() continuously from a context you can dedicate to it (arduino-pico’s loop1() is the usual choice).
On ESP32 / Teensy / PC, start_manual = start and task does nothing beyond housekeeping (empty implementations so shared code runs unchanged).
bool usb_disp_poll(usb_disp_t *d)
Call periodically (every loop iteration is fine). Handles connection, disconnection, enumeration, and automatic mode setup. Returns true on the transition to READY, on disconnect, or when a late-arriving EDID switches the mode after READY (use it as a redraw trigger).
Querying state
| Function | Description |
|---|---|
bool usb_disp_ready(d) |
Whether the display is connected and drawable |
uint16_t usb_disp_width(d) / usb_disp_height(d) |
Current resolution (valid after READY) |
usb_disp_chip_t usb_disp_chip(d) |
Detected chip (e.g. USB_DISP_CHIP_DL165) |
const char *usb_disp_chip_name(d) |
Chip name string (e.g. “DL-165”) |
const char *usb_disp_model(d) |
Model string for known products (e.g. “BUFFALO GX-DVI/U2”). NULL if not in the list (src/usb_disp_model.h) |
const char *usb_disp_model_name(vid, pid) |
Same, but by VID/PID directly (no connection needed) |
uint32_t usb_disp_max_area(d) |
The chip’s pixel-count limit (0 = unknown) |
uint16_t usb_disp_vid(d) / usb_disp_pid(d) |
VID / PID of the connected device |
uint64_t usb_disp_stat_bytes(d) |
Cumulative bytes sent (for performance checks) |
usb_disp_udh_host_t *usb_disp_get_host(d) |
Pico only: access to the PIO USB host’s statistics (declared in src/usb_disp_udh_host.h; used by examples/DisplayInfo) |
uint8_t usb_disp_count(void) |
Number of registered ports |
usb_disp_t *usb_disp_at(uint8_t idx) |
Handle at index idx |
uint8_t usb_disp_index(d) |
Handle -> index |
usb_disp_chip_t members (prefix USB_DISP_CHIP_): UNKNOWN, DL120, DL160, DL1X0 (Gen 1.0, SKU unidentified), DL115, DL125, DL165, DL195, DL1X5 (Gen 1.5, SKU unidentified), T6, MS912X, MS913X.
Drawing (after READY)
For update and fill, coordinates that extend beyond the screen are clipped at the right and bottom edges (a fully off-screen rectangle draws nothing and returns true). usb_disp_copy does not clip - a rectangle that goes out of range is rejected and returns false.
Rectangle updates (3 forms; use whichever you like)
// Generic form: fmt selects the pixel format (in C++ fmt may be omitted = RGB565)
bool usb_disp_update(d, x, y, w, h, const void *px, stride_px,
usb_disp_fmt_t fmt);
// Fixed-format versions
bool usb_disp_update_565(d, x, y, w, h, const uint16_t *rgb565, stride_px);
bool usb_disp_update_888(d, x, y, w, h, const uint8_t *px, stride_px);
fmtisUSB_DISP_FMT_RGB565(2 B/px) orUSB_DISP_FMT_RGB888(3 B/px, memory order B,G,R = same as LVGL’s RGB888).stride_pxis the number of pixels per row of the source array (wif it equals the rectangle width; 0 = repeat the same row). This shape can be called directly from an LVGL flush_cb.- The input format is independent of the display depth (usb_disp_set_depth); the library converts when they differ.
bool usb_disp_fill(d, x, y, w, h, color)
Solid fill.
bool usb_disp_copy(d, sx, sy, dx, dy, w, h)
Rectangle copy within the screen. Useful for scrolling. Overlap is handled automatically. Readback is not possible. On DL chips this maps to a native COPY command with no pixel retransmission; on T6 / MS912x / MS913x the copy happens in the internal frame buffer and the full frame is re-sent on the next transfer.
bool usb_disp_flush(d, timeout_ms)
Pushes out buffered data. Always call at the end of a batch of drawing (the chip holds the last command until more data arrives).
bool usb_disp_blank(d, bool on)
Blank / unblank the screen.
Changing the resolution (mode)
All of these reject modes exceeding the chip’s pixel-count limit and return false. Additional per-chip constraints: the T6 only accepts modes present in the device’s own mode table (width/height/refresh must match an entry), and MS912x/MS913x only support their known modes - 1920x1080 and 1280x720. Screen contents are undefined after a mode change, so redraw everything.
| Function | Description |
|---|---|
bool usb_disp_set_mode(d, w, h) |
Set a mode at 60Hz (built-in list first -> CVT-RB computation otherwise) |
bool usb_disp_set_mode_hz(d, w, h, hz) |
Same, with an explicit refresh rate |
bool usb_disp_set_mode_ex(d, const usb_disp_mode_t *m) |
Fully specified timing (advanced) |
bool usb_disp_set_auto_mode(d) |
Redo automatic selection (EDID first) |
uint8_t usb_disp_builtin_mode_count(void) |
Number of built-in modes |
const usb_disp_mode_t *usb_disp_builtin_mode(uint8_t idx) |
Get a built-in mode (largest area first) |
bool usb_disp_current_mode(d, usb_disp_mode_t *out) |
Get the current mode including timing. false if not set |
uint16_t usb_disp_refresh_hz(d) |
Output refresh rate [Hz] (computed from the timing; independent of the MCU’s drawing rate) |
typedef struct {
uint16_t width, height; // active resolution
uint32_t pclk_khz; // pixel clock [kHz]
uint16_t hfp, hsync, hbp; // horizontal timing
uint16_t vfp, vsync, vbp; // vertical timing
} usb_disp_mode_t;
24-bit color (RGB888)
The default is 16-bit (RGB565). Switching to 24-bit displays gradients in photos etc. without banding (1.5x the bandwidth - suited to slow signage).
| Support | Chip | Method |
|---|---|---|
| ○ | DL-1x0 / DL-1x5 | 16+8 dual plane (verified on real DL-165 hardware) |
| ○ | MS913x | RGB24 sent directly (no conversion) |
| ○ | MS912x | The UYVY conversion source becomes 888 (better luma precision) |
| ○ | T6 | JPEG encoder with 888 input |
| Function | Description |
|---|---|
usb_disp_config_t.depth24 = true |
Run in 24-bit from connection time |
bool usb_disp_set_depth(d, bits) |
Switch between 16 / 24 (while READY this re-sets the mode = clears the screen; redraw required) |
uint8_t usb_disp_depth(d) |
Effective depth (requesting 24 on unsupported setups yields 16) |
bool usb_disp_update_888(d, x, y, w, h, px, stride_px) |
Rectangle update. px = 3 B/px, memory order B,G,R (same as LVGL’s RGB888) |
In either mode, both usb_disp_update (565) and usb_disp_update_888 may be used (the library converts).
Mixing is allowed - e.g. “GUI in 565, photo areas in 888”.
The shadow FB (differential updates) also works in 24-bit (allocated at 3 B/px).
From GUI libraries:
- LVGL:
usb_disp_lvgl_create(d, 16, LV_COLOR_FORMAT_RGB888) - LovyanGFX:
lcd.setColorDepth(24)(before or after init)
Shadow frame buffer (differential updates)
When enabled, update/fill compare against the previous screen contents and send only the changed parts.
On screens with a lot of static content the transfer volume drops dramatically.
The shadow FB applies to the span-update protocol (DisplayLink DL-1xx) only - full-frame protocols (T6 / MS912x / MS913x) always send whole frames, so it never activates there: usb_disp_set_shadow() returns false when called while READY, and a request made before connect is dropped at connection time with a “shadow FB not applicable” log.
Memory is allocated from PSRAM on ESP32, from the add-on PSRAM (extmem_malloc) on Teensy 4.1, and from the heap on PC (not supported on Pico = always false; on a Teensy without PSRAM fitted it also stays disabled).
| Function | Description |
|---|---|
bool usb_disp_set_shadow(d, bool on) |
Enable/disable (right after enabling, the screen is synchronized to black) |
bool usb_disp_shadow_active(d) |
Whether it is currently enabled |
const uint16_t *usb_disp_shadow_row(d, row) |
Debug: pointer to a shadow row (NULL when disabled, and always NULL while running in 24-bit - the shadow is then stored at 3 B/px, which does not fit the uint16_t view) |
usb_disp_config_t.shadow_fb = true enables it automatically at connection time.
Miscellaneous
| Function | Description |
|---|---|
uint16_t usb_disp_read_edid(d, buf, len) |
Read the monitor’s EDID from offset 0 (returns bytes read, 0 = failed) |
uint16_t usb_disp_read_edid_at(d, off, buf, len) |
Same, from a byte offset - use it to fetch extension blocks |
uint16_t usb_disp_edid_size(d) |
Total EDID size including extension blocks (0 = unreadable / ignore_edid) |
bool usb_disp_edid_supports(d, w, h) |
Whether the sink advertises this resolution anywhere in its EDID (see below) |
bool usb_disp_edid_max_mode(d, &w, &h) |
Largest advertised resolution that fits the chip - what EDID_POLICY_MAX selects |
void usb_disp_force_reenum(d) |
Force re-enumeration (virtual replug) |
bool usb_disp_supported_device(vid, pid) |
Whether this VID/PID is supported |
EDID extension blocks
EDID is a 128-byte base block plus optional 128-byte extension blocks (the count is in byte 126). The library reads them up to USB_DISP_EDID_MAX_BLOCKS (default 2 blocks total = base + one extension; see Limits and tuning below) and, for CEA-861 extensions (tag 0x02), parses both the detailed timings and the Video Data Block’s short video descriptors (VIC codes).
This matters because a relay device - an HDMI analyzer, splitter, capture box, or KVM - commonly puts only 480p in its base block and everything else in the CEA extension. A real measured example (an analyzer reporting monitor name VA-1838):
base block : preferred timing 720x480, established 640x480 only
CEA ext : VIC 2* 1 3 4 5 16 17 18 19 20 31 6 7 21 22
(VIC 4 = 1280x720p60, VIC 16 = 1920x1080p60)
ext DTDs: 720x480, 1280x720, 1920x540(=1080i)
Reading only the base block makes such a device look like a 480p sink. usb_disp_edid_supports(d, 1920, 1080) returns true here because it sees VIC 16.
Use usb_disp_edid_supports() when your app wants to sanity-check a resolution before forcing it, instead of comparing against the base block’s preferred timing. usb_disp_edid_max_mode() answers the softer question “will the sink cope with this?” - a 1080p monitor usually does not list 1280x720 anywhere in its EDID, yet it displays it fine, so compare against the advertised maximum rather than requiring an exact listing. Both may trigger a full EDID read (hundreds of control transfers on DL-1xx, which reads one byte per transfer), so call them on connect - not every frame.
Limits and tuning:
USB_DISP_EDID_MAX_BLOCKS(default 2 = base + 1 extension) caps how many blocks are read and sizes the internal buffer (128 * Nbytes). Raise it with-DUSB_DISP_EDID_MAX_BLOCKS=nif you have a sink with more than one extension block.- DL-1x0/1x5 can only read the first 256 bytes - the chip’s EDID register takes an 8-bit byte offset, so base + one extension is the hardware limit regardless of the setting. T6 and MS912x/MS913x have no such limit.
- Extension blocks are checksum-verified before use; a corrupted one is logged and ignored rather than parsed.
Extension data feeds the default EDID_POLICY_MAX selection, the fallback search, usb_disp_edid_supports() and usb_disp_edid_max_mode(). Note that interlaced detailed timings are skipped - CEA extensions often carry 1080i as a 1920x540 DTD, which would otherwise be programmed as a 540-line mode.
Logging
By default nothing is printed.
| Function | Description |
|---|---|
void usb_disp_set_log(bool on) |
Arduino: true sends the log to Serial |
void usb_disp_log(const char *fmt, ...) |
Weak function. Define it in your app to completely replace the log destination (all platforms; set_log is then irrelevant) |
// Example of replacing the log destination (just define it)
void usb_disp_log(const char *fmt, ...) {
va_list ap; va_start(ap, fmt);
vfprintf(stderr, fmt, ap); // e.g. to stderr
va_end(ap); fprintf(stderr, "\n");
}
Arduino auto-glue (no extra include)
<usb_disp.h> detects when your sketch includes the following libraries and enables the integration automatically:
LovyanGFX -> the LGFX_USB_Disp class
#include <LovyanGFX.hpp>
#include <usb_disp.h>
LGFX_USB_Disp lcd;
// After READY: lcd.setUsbDisp(d); lcd.init(); then normal LovyanGFX
Limitations: no readback / rotation fixed to 0. Color depth is 16 (default) or 24, selectable via lcd.setColorDepth(24).
Teensy 4.x requires LovyanGFX 1.2.27 or later (earlier releases do not support the i.MX RT).
LVGL v9 -> usb_disp_lvgl_create()
#include <lvgl.h>
#include <usb_disp.h>
// After READY:
lv_display_t *lvd = usb_disp_lvgl_create(d /*, buf_lines = 16 */);
// Returns with lv_init / tick / draw buffers / flush_cb already set up.
// Then just call lv_timer_handler() in loop().
Compile-time configuration (advanced)
| Macro | Default | Description |
|---|---|---|
USB_DISP_MAX_WIDTH |
2048 | Maximum supported horizontal resolution (line buffer length; can be reduced on RAM-constrained boards) |
USB_DISP_MAX |
Pico = number of PIOs / PC = 4 / ESP32 = 1 / Teensy = 1 | Maximum number of simultaneous displays. Only tunable with -D on PC (libusb); fixed on ESP32/Teensy, and derived from USB_DISP_UDH_MAX_PORTS on the Pico |
USB_DISP_UDH_MAX_PORTS |
RP2040 = 2 / RP2350 = 3 | Pico only: number of PIO USB host ports to provision. Each port reserves a 16 KB ring in .bss, so lowering it saves RAM on the RP2040 |
USB_DISP_EDID_MAX_BLOCKS |
2 | How many 128-byte EDID blocks are read (base + extensions) and the size of the internal buffer (128 x N bytes). See EDID extension blocks above |
USB_DISP_PSRAM_LIMIT_KB |
0 (unlimited) | ESP32 only: cap on the total PSRAM the library may allocate for frame buffers (shadow FB etc.) |
USB_DISP_TEENSY_HUBS |
2 | Teensy only: number of USBHub instances for adapters behind a hub / dock (0 saves RAM) |
USB_DISP_TEENSY_SETTLE_MS |
1500 | Teensy only: delay [ms] from connect detection to enumeration start. DL chips need 1-2 s of internal startup; enumerating earlier (USBHost_t36’s default is 100 ms) wedges their EP0 (verified on DL-165) |
USB_DISP_ESP32_SETTLE_MS |
200 | ESP32 only: disconnected period [ms] before the USB host stack starts (lets a DL chip left “warm” by an MCU reboot start fresh) |
USB_DISP_UDH_HS_DIAG |
0 | Pico only, debug: classify and count handshake results in the PIO host (used by examples/DisplayInfo to distinguish “device not answering” from “receiving our own transmission”) |
The platform-detection macros USB_DISP_PORT_PICO / USB_DISP_PORT_ESP32 / USB_DISP_PORT_TEENSY / USB_DISP_PORT_LIBUSB are defined to 0/1 by usb_disp.h and can be used for conditional code in sketches that target several platforms (examples/DisplayInfo does this).