An HSTX-native HDMI output library for the RP2350 (Raspberry Pi Pico 2).
2.0 beta notice: the
2.0-betabranch/tag contains the newer opt-in scanout/audio pipeline described below. It is intended for testing and performance-sensitive projects, but it is not yet the recommended stable baseline for every application. If you need the older stable API/behavior, usev0.0.6or thev0.0.6source tree.
PicoHDMI leverages the RP2350's dedicated HSTX (High-Speed Transmit) peripheral with hardware TMDS encoding. No bit-banging, no overclocking required: just near-zero CPU overhead for video output.
pico_hdmi provides a high-performance video and audio output pipeline using the RP2350's HSTX hardware. It is designed to be decoupled from specific application logic, focusing strictly on the generation of stable TMDS signals and Data Island injection (e.g., for audio).
- HSTX Hardware TMDS Encoding: Uses the native TMDS encoder for zero-CPU video serialization.
- Audio Data Islands: Built-in support for TERC4 encoding and scheduled injection of audio samples.
- Data Island Queue: Lock-free queue for asynchronous packet posting from other cores.
- Double-Buffered DMA: Stable video output with minimal jitter.
- True 240p DirectVideo Mode: 320x240 output with HDMI pixel repetition for retro gaming scalers (Morph4K, RetroTINK 4K, OSSC).
- Configurable Audio Sample Rate: Default 48kHz, with runtime support for 32kHz, 44.1kHz, and other standard HDMI rates. ACR N/CTS values follow the HDMI spec (Table 7-1/7-2).
- 720p60 Mode (experimental): Opt-in 1280x720 @ 60Hz (CEA VIC 4) at 372 MHz system clock and 1.3V core voltage. HSTX pins are configured for fast slew and 12mA drive to improve sink margin at 720p. Enable by defining
VIDEO_MODE_1280x720when compiling the library. - 2.0 beta precomposed scanout path (opt-in): Static HDMI active-line templates reduce scanline-ISR work; the ISR patches only the current Data Island payload.
- Native 16-bit pixel scanout (opt-in): A pointer callback can return native RGB565 half-width scanlines for 2x modes; DMA/HSTX performs the horizontal pixel duplication.
- Resync API (opt-in use): Applications can force a full HSTX/DMA restart and track resync count for watchdog-style recovery.
The library selects a mode at compile time via a single define:
| Define | Resolution | Pixel Clock | sys_clk | Notes |
|---|---|---|---|---|
| (none) | 640x480 | 25.2 MHz | 126 MHz | Default — CEA VIC 1, negative sync |
VIDEO_MODE_320x240 |
1280x240* | 25.2 MHz | 126 MHz | True 240p, 4x pixel repetition |
VIDEO_MODE_1280x720 |
1280x720 | 74.4 MHz | 372 MHz | CEA VIC 4, positive sync, needs 1.3V |
The library automatically handles sync polarity and Data Island placement per mode. For modes with a narrow hsync pulse (e.g. 720p60's 40-px hsync), the HDMI Data Island is placed in the back porch rather than inside the hsync pulse. This is transparent to callers.
The runtime backend also exposes two application-selected PC timing descriptors. They are not compile-time demo modes and the application remains responsible for configuring the required HSTX parent clock before init.
| Runtime descriptor | Active raster | Pixel clock | Totals | Sync | Status |
|---|---|---|---|---|---|
video_mode_960x720_p |
960x720 | 56.0 MHz | 1248x748 | H-, V+ | Compile-tested, not hardware-validated |
video_mode_1024x768_p |
1024x768 | 64.8 MHz | 1340x806 | H-, V- | Compile-tested, not hardware-validated |
Runtime mode attributes track horizontal and vertical sync polarity
separately. An application that provides a dedicated PLL_USB clock can call
video_output_set_hstx_source(VIDEO_OUTPUT_HSTX_SOURCE_PLL_USB, source_hz)
before video_output_init(). Audio ACR and pacing then derive the pixel clock
from clk_hstx. The default remains clk_sys, so existing applications do
not need to call this API.
The HSTX output pins are configured for fast slew and 12mA drive. This gives the RP2350 enough edge margin for 720p60 on stricter sinks; weaker default pad settings can produce corruption or sync loss at 720p even when 480p is stable.
By default, the library preserves its original mapping: CLK-/CLK+ on GPIO12/13, followed by D0-/D0+ through D2-/D2+ on GPIO14 through GPIO19. Existing applications do not need any changes.
Boards that permute HSTX lanes or differential polarity can provide an explicit pinout before starting the Core 1 output loop:
#include "pico_hdmi/hstx_pins.h"
static const pico_hdmi_hstx_pinout_t custom_pinout = {
.clock = {.positive_gpio = 14, .negative_gpio = 15},
.data = {
{.positive_gpio = 12, .negative_gpio = 13}, // Logical TMDS D0
{.positive_gpio = 18, .negative_gpio = 19}, // Logical TMDS D1
{.positive_gpio = 16, .negative_gpio = 17}, // Logical TMDS D2
},
};
if (!video_output_set_hstx_pinout(&custom_pinout)) {
panic("Invalid HSTX pinout");
}
multicore_launch_core1(video_output_core1_run);Each GPIO from 12 through 19 must appear exactly once. The pinout is copied by the library and works with both the compile-time and runtime-mode video backends.
GitHub Releases include ready-to-flash bouncing-box firmware for Pico 2:
| Asset | Mode | HDMI path | Notes |
|---|---|---|---|
bouncing_box.uf2 |
640x480 @ 60Hz | Non-RT / compile-time | Default stable VGA/480p demo |
bouncing_box_720p_nonrt.uf2 |
1280x720 @ 60Hz | Non-RT / compile-time | Uses VIDEO_MODE_1280x720; recommended for validating the compile-time 720p path |
bouncing_box_720p_rt.uf2 |
1280x720 @ 60Hz | Runtime-mode (video_output_rt.c) |
Uses the runtime-mode HDMI backend at CEA VIC 4 |
Both 720p builds require the RP2350 to run at 372 MHz with 1.3V core voltage. The demo configures those clocks at startup.
The 2.0-beta branch adds a compatibility-preserving path for applications that are close to the scanline timing limit. The legacy defaults remain enabled unless you opt in with CMake options or API calls.
Use the 2.0 beta path if your app does heavy per-line work, overlays, 320→640 scaling, audio Data Islands, or live capture while HDMI is running. The main benefit is reducing h-blank/ISR pressure and jitter. Simple apps that already fit comfortably in the classic scanline callback do not need it.
| Option | Default | Purpose |
|---|---|---|
PICO_HDMI_PRECOMPOSED_ACTIVE_LINES |
OFF |
Enable precomposed active-line templates and the native 16-bit scanout API. |
PICO_HDMI_HSTX_CLK_DIV |
empty | Override MODE_HSTX_CLK_DIV; empty keeps the mode default. Useful for 252 MHz sysclk + div2 480p. |
PICO_HDMI_LINE_BUFFER_IN_SCRATCH_Y |
OFF |
Place the active-line buffer in scratch Y RAM. |
PICO_HDMI_RUNTIME_MODES |
OFF |
Use video_output_rt.c for runtime mode switching. |
PICO_HDMI_RT_RUNTIME_MODE_ATTRS |
OFF |
Allow runtime modes with different sync polarity/Data Island placement. |
PICO_HDMI_DUMP_COMMAND_LISTS |
OFF |
Print generated command lists at startup for debugging. |
Example 480p beta build using a 252 MHz system clock and HSTX div2:
cmake -S examples/bouncing_box -B build-2beta -G Ninja \
-DPICO_BOARD=pico2 \
-DPICO_PLATFORM=rp2350-arm-s \
-DPICO_HDMI_PRECOMPOSED_ACTIVE_LINES=ON \
-DPICO_HDMI_HSTX_CLK_DIV=2
cmake --build build-2beta -jYour application must set a matching system clock. For example, 640x480p60 can use either the stable default 126 MHz / div1 / CSR div5, or the beta-friendly 252 MHz / div2 / CSR div5; both produce a 25.2 MHz effective pixel clock.
video_output_set_scanline_pointer_callback(my_ptr_cb);
video_output_set_native_pixel_mode(true);
video_output_set_compose_ring(ring, ring_entries);
video_output_compose_service();
video_output_in_vertical_blanking();
video_output_force_resync();For a 2x 480p path, native pixel mode lets the pointer callback return a 320-pixel RGB565 line while HSTX/DMA duplicates each pixel horizontally to 640 output pixels. This is especially useful for emulators, capture pipelines, and OSD composition where the source is already 320-wide.
Minimal pattern:
static video_output_precomposed_line_t compose_ring[64];
video_output_init(320, 240);
video_output_set_compose_ring(compose_ring, 64);
video_output_set_scanline_pointer_callback(my_native_line_cb);
video_output_set_native_pixel_mode(true);
// Run often from Core 1 background context, before/while scanout is active.
video_output_compose_service();The beta path is additive: old fill-buffer callbacks, existing Data Island queue usage, and default compile-time modes still work without changes when PICO_HDMI_PRECOMPOSED_ACTIVE_LINES=OFF.
By default, HDMI audio is configured for 48kHz. To use a different sample rate (44100 Hz for example), call pico_hdmi_set_audio_sample_rate() after video_output_init():
video_output_init(FRAME_WIDTH, FRAME_HEIGHT);
pico_hdmi_set_audio_sample_rate(44100);Supported rates: 32000, 44100, 48000, 88200, 96000, 176400, 192000 Hz.
The scanline callback runs during h-blank with very limited time:
| Clock | H-Blank Window | CPU Cycles | ~Instructions |
|---|---|---|---|
| 126 MHz | ~6 µs | ~800 | 500-700 |
| 252 MHz | ~6 µs | ~1600 | 1000-1400 |
A simple 320→640 pixel copy/double alone takes ~400-600 cycles. This leaves almost no room for additional processing.
Guidelines:
- Do heavy lifting elsewhere (different core or outside the callback)
- Use the callback only to feed pre-computed data into the DMA buffer
- Avoid per-pixel branching; use loop splitting instead
- Process 2 pixels per iteration (32-bit ops)
- Keep callback code in zero-wait-state RAM (
__scratch_x)
The callback exists for flexibility (e.g., upscale from a smaller source buffer on-the-fly) rather than for processing. Pre-render everything, then just copy.
include/pico_hdmi/: Public headers. Use#include <pico_hdmi/...>in your project.src/: Implementation files.CMakeLists.txt: Build configuration.
- Add this directory to your project's
libfolder. - Add
add_subdirectory(path/to/pico_hdmi)to yourCMakeLists.txt. - Link against
pico_hdmi. - Initialize with
video_output_init()and run the output loop on Core 1 withvideo_output_core1_run().
This project uses clang-format and clang-tidy to maintain code quality.
- pre-commit: To automatically run checks before each commit.
On macOS, you can install it via Homebrew:
brew install pre-commitOn Linux:
pip install pre-commitTo activate the git pre-commit hooks, run:
pre-commit installOnce installed, the hooks will automatically format your code and run static analysis whenever you commit.
Note: If a hook fails and modifies your files (e.g.,
clang-format), you will need togit addthose changes and commit again.
To manually run the checks on all files:
pre-commit run --all-filesUnlicense