Minimal standalone driver for the Pimoroni Inky Impression 13.3 (EL133UF1 / Spectra 6) on a Pico Plus 2 W: panel bring-up, test patterns, and on-device JPEG decode + dither.
C
23
5 commits
updated Jun 22, 2026
Minimal standalone driver for the Pimoroni Inky Impression 13.3" (EL133UF1 / Spectra 6, 1600x1200, 6-colour) on a Pimoroni Pico Plus 2 W (RP2350B). It brings the panel up, draws a set of test patterns and an embedded sample image, and advances one screen per press of the front button. No WiFi, MQTT, deep sleep, or config — just the panel.
Extracted from the tesserae-device-pico-bin firmware as a clean reference /
bring-up tool for this panel.
Pico Plus 2 W on a Hard Stuff "Pico to Pi Hat" adapter, driving the Inky 13.3". The adapter is a 1:1 Pi-BCM-to-Pico-GP passthrough except it swaps clock and data (meter-verified; the vendor doc is wrong).
| Signal | Pico GP |
|---|---|
| SCLK | GP10 (SPI1 SCK) |
| MOSI | GP11 (SPI1 TX) |
| DC | GP22 |
| RST | GP27 |
| BUSY | GP17 (active low) |
| CS_M (left half, cols 0..599) | GP26 |
| CS_S (right half, cols 600..1199) | GP16 |
| Button A | GP15 (active low) |
The panel is landscape-native 1600x1200, but its two controllers scan a 1200x1600 portrait frame split at column 600. The driver rotates a landscape frame 90 deg CW and splits it; patterns are drawn directly in portrait. Spectra 6 panels need a ~300 ms per-command D/C setup or they drop commands, and a full refresh takes ~35 s.
PlatformIO core: ~/.platformio/penv/bin/pio (adjust if yours is on PATH).
pio run # build -> .pio/build/pimoroni_pico_plus_2w/firmware.uf2
pio device monitor # 115200 baud
Flashing (no pio run -t upload — the bundled picotool can't force this RP2350
into BOOTSEL): double-tap RESET to mount the RP2350 drive, then copy
firmware.uf2 onto it. (A 1200-baud touch on the USB CDC port also triggers
BOOTSEL.)
On boot it paints the vertical-stripe test pattern, then each button A press advances to the next screen:
sample_card)The two image screens come right after boot so they're reachable in one or two presses — the on-device JPEG is the slow one (decode + ~35 s refresh) and was tedious to reach when it sat last. Each paint includes the ~35 s refresh; wait for the panel to settle before pressing A again. Serial logs each step.
There are two image paths: a frame packed on the host, and a JPEG converted on the device itself.
.bin (host-converted)src/sample_card.bin is a landscape 1600x1200 Spectra-6 packed-4bpp frame
(960000 bytes), embedded into flash via src/sample_card.S (.incbin) and shown
directly by el133_show_frame() (no decode, no PSRAM).
python3 tools/gen_sample.pypython3 tools/png2frame.py photo.png
— it cover-fits to 1600x1200, quantises to the Spectra 6 palette, and
overwrites src/sample_card.bin. Rebuild and reflash.Frame format: packed 4bpp, 2 px/byte, high nibble = even column, 800 bytes/row, 1200 rows. Colour codes: black 0x0, white 0x1, yellow 0x2, red 0x3, blue 0x5, green 0x6.
src/sample.jpg is a 1600x1200 baseline JPEG embedded the same way
(src/sample_jpg.S). At paint time img_decode_jpeg() (src/img_decode.c)
decodes it with the vendored picojpeg into a full RGB888 buffer in PSRAM,
then Floyd-Steinberg dithers it to the six panel colours, packing the result into
a second PSRAM buffer that el133_show_frame() paints. This mirrors what a
networked client does with a server-delivered photo, no host conversion needed.
The dither boosts saturation ~1.4x and snaps to the nominal sRGB primaries (pure red/green/blue, not muted "as it renders" targets). Muted targets — a navy-ish blue especially — sit close to dusty-pink/mauve shadow tones, so shadows snap to blue and the dither smears them into a violet haze; pure primaries keep the colour decisions clean and let the panel do its own gamut projection.
NOTICES.md). Fetch and
crop it reproducibly with python3 tools/fetch_sample_jpg.py, or point it at
your own file: python3 tools/fetch_sample_jpg.py --src photo.jpg.numpy optional, just faster):
python3 tools/make_sample_jpg.py.src/sample.jpg and rebuild — the decoder does not resize, so it clips a larger
image and white-pads a smaller one.include/epd_io.h / src/epd_io.c — SPI1 + D/C/RST/BUSY transport, command +
data-stream helpers.include/el133.h / src/el133.c — the EL133UF1 driver: reset, the verified
Pimoroni init sequence, the rotate/split paint, and refresh.
el133_show_pattern() (per-row generator) and el133_show_frame() (landscape
buffer).include/patterns.h / src/patterns.c — the procedural test patterns.include/images.h / src/sample_card.S / src/sample_card.bin — the embedded
pre-packed image; src/sample_jpg.S / src/sample.jpg — the embedded JPEG.include/img_decode.h / src/img_decode.c — on-device JPEG decode +
Floyd-Steinberg dither to a Spectra-6 frame in PSRAM.include/psram.h / src/psram.c — RP2350 QMI bring-up for the 8 MB PSRAM.include/picojpeg.h / src/vendor/picojpeg.c — vendored picojpeg (public
domain) baseline JPEG decoder.src/main.c — boot, PSRAM init, button-A advance loop.tools/gen_sample.py, tools/png2frame.py — pre-packed .bin tooling;
tools/make_sample_jpg.py (synthetic gradient) and tools/fetch_sample_jpg.py
(fetch/crop a real photo) — embedded-JPEG tooling.AGPL-3.0-or-later (LICENSE). The vendored picojpeg decoder
(src/vendor/picojpeg.c) is public domain, and the bundled sample photo
(src/sample.jpg) is CC BY-SA 3.0; see NOTICES.md for both.
5 commits
C
82.3%
Python
15.3%
Assembly
2.4%
Minimal standalone driver for the Pimoroni Inky Impression 13.3 (EL133UF1 / Spectra 6) on a Pico Plus 2 W: panel bring-up, test patterns, and on-device JPEG decode + dither.
C
23
5 commits
updated Jun 22, 2026
Minimal standalone driver for the Pimoroni Inky Impression 13.3" (EL133UF1 / Spectra 6, 1600x1200, 6-colour) on a Pimoroni Pico Plus 2 W (RP2350B). It brings the panel up, draws a set of test patterns and an embedded sample image, and advances one screen per press of the front button. No WiFi, MQTT, deep sleep, or config — just the panel.
Extracted from the tesserae-device-pico-bin firmware as a clean reference /
bring-up tool for this panel.
Pico Plus 2 W on a Hard Stuff "Pico to Pi Hat" adapter, driving the Inky 13.3". The adapter is a 1:1 Pi-BCM-to-Pico-GP passthrough except it swaps clock and data (meter-verified; the vendor doc is wrong).
| Signal | Pico GP |
|---|---|
| SCLK | GP10 (SPI1 SCK) |
| MOSI | GP11 (SPI1 TX) |
| DC | GP22 |
| RST | GP27 |
| BUSY | GP17 (active low) |
| CS_M (left half, cols 0..599) | GP26 |
| CS_S (right half, cols 600..1199) | GP16 |
| Button A | GP15 (active low) |
The panel is landscape-native 1600x1200, but its two controllers scan a 1200x1600 portrait frame split at column 600. The driver rotates a landscape frame 90 deg CW and splits it; patterns are drawn directly in portrait. Spectra 6 panels need a ~300 ms per-command D/C setup or they drop commands, and a full refresh takes ~35 s.
PlatformIO core: ~/.platformio/penv/bin/pio (adjust if yours is on PATH).
pio run # build -> .pio/build/pimoroni_pico_plus_2w/firmware.uf2
pio device monitor # 115200 baud
Flashing (no pio run -t upload — the bundled picotool can't force this RP2350
into BOOTSEL): double-tap RESET to mount the RP2350 drive, then copy
firmware.uf2 onto it. (A 1200-baud touch on the USB CDC port also triggers
BOOTSEL.)
On boot it paints the vertical-stripe test pattern, then each button A press advances to the next screen:
sample_card)The two image screens come right after boot so they're reachable in one or two presses — the on-device JPEG is the slow one (decode + ~35 s refresh) and was tedious to reach when it sat last. Each paint includes the ~35 s refresh; wait for the panel to settle before pressing A again. Serial logs each step.
There are two image paths: a frame packed on the host, and a JPEG converted on the device itself.
.bin (host-converted)src/sample_card.bin is a landscape 1600x1200 Spectra-6 packed-4bpp frame
(960000 bytes), embedded into flash via src/sample_card.S (.incbin) and shown
directly by el133_show_frame() (no decode, no PSRAM).
python3 tools/gen_sample.pypython3 tools/png2frame.py photo.png
— it cover-fits to 1600x1200, quantises to the Spectra 6 palette, and
overwrites src/sample_card.bin. Rebuild and reflash.Frame format: packed 4bpp, 2 px/byte, high nibble = even column, 800 bytes/row, 1200 rows. Colour codes: black 0x0, white 0x1, yellow 0x2, red 0x3, blue 0x5, green 0x6.
src/sample.jpg is a 1600x1200 baseline JPEG embedded the same way
(src/sample_jpg.S). At paint time img_decode_jpeg() (src/img_decode.c)
decodes it with the vendored picojpeg into a full RGB888 buffer in PSRAM,
then Floyd-Steinberg dithers it to the six panel colours, packing the result into
a second PSRAM buffer that el133_show_frame() paints. This mirrors what a
networked client does with a server-delivered photo, no host conversion needed.
The dither boosts saturation ~1.4x and snaps to the nominal sRGB primaries (pure red/green/blue, not muted "as it renders" targets). Muted targets — a navy-ish blue especially — sit close to dusty-pink/mauve shadow tones, so shadows snap to blue and the dither smears them into a violet haze; pure primaries keep the colour decisions clean and let the panel do its own gamut projection.
NOTICES.md). Fetch and
crop it reproducibly with python3 tools/fetch_sample_jpg.py, or point it at
your own file: python3 tools/fetch_sample_jpg.py --src photo.jpg.numpy optional, just faster):
python3 tools/make_sample_jpg.py.src/sample.jpg and rebuild — the decoder does not resize, so it clips a larger
image and white-pads a smaller one.include/epd_io.h / src/epd_io.c — SPI1 + D/C/RST/BUSY transport, command +
data-stream helpers.include/el133.h / src/el133.c — the EL133UF1 driver: reset, the verified
Pimoroni init sequence, the rotate/split paint, and refresh.
el133_show_pattern() (per-row generator) and el133_show_frame() (landscape
buffer).include/patterns.h / src/patterns.c — the procedural test patterns.include/images.h / src/sample_card.S / src/sample_card.bin — the embedded
pre-packed image; src/sample_jpg.S / src/sample.jpg — the embedded JPEG.include/img_decode.h / src/img_decode.c — on-device JPEG decode +
Floyd-Steinberg dither to a Spectra-6 frame in PSRAM.include/psram.h / src/psram.c — RP2350 QMI bring-up for the 8 MB PSRAM.include/picojpeg.h / src/vendor/picojpeg.c — vendored picojpeg (public
domain) baseline JPEG decoder.src/main.c — boot, PSRAM init, button-A advance loop.tools/gen_sample.py, tools/png2frame.py — pre-packed .bin tooling;
tools/make_sample_jpg.py (synthetic gradient) and tools/fetch_sample_jpg.py
(fetch/crop a real photo) — embedded-JPEG tooling.AGPL-3.0-or-later (LICENSE). The vendored picojpeg decoder
(src/vendor/picojpeg.c) is public domain, and the bundled sample photo
(src/sample.jpg) is CC BY-SA 3.0; see NOTICES.md for both.
5 commits
C
82.3%
Python
15.3%
Assembly
2.4%