

Tiny tft carrier
A repo for housing all the work related to the ESP32-C6-LCD-1.47H display device.
- Size
- 36.5 × 23 mm
- Copper layers
- 2
- Thickness
- 1.6 mm
- Parts
- 3
- Nets
- 15
- Pads
- 38
- Tracks, vias
- 25, 0
- Routed
- Yes
- KiCad
- KiCad 8.0 (file 20240108)
- Created
- 2 Aug 2026
- Updated
- 31 Oct 2026
Overview
Tiny tft carrier is an open-source printed circuit board by Djspaceg, from the Djspaceg/esp32-display repository on GitHub, published under the MIT licence. It is a 2-layer board of 36.5 × 23 mm with 3 parts on 15 nets, routed with 25 tracks and 0 vias.
Its parts include 1 IC and 1 connector. It belongs with the lEDs and displays boards and the ESP32 boards of this library.
README
README.mdesp32-display
Turn supported ESP32 display boards into small wireless displays driven by a native macOS app. The repository contains firmware, wire protocols, a signed-app build, USB onboarding, OTA updates, and canonical precompiled releases.
What it does
- Streams dirty RGB565 bands or tiles over UDP.
- Uses packed/RLE bands on smaller displays, tile codecs on the 466×466 round AMOLED, and capability-gated ETL1 large tiles on 720×720 P4.
- Discovers panels through
_espdisp._udpmDNS records. - Supports brightness, power, rotation, identify, restart, touch gestures, battery telemetry, status screens, and password-protected OTA.
- Onboards blank boards over USB with explicit identity checks.
- Packages current firmware as one artifact per chip family.
Firmware families
Release families are exactly c3, c6, s3, and p4. Every family is universal
within its currently supported chip-family profiles. Product names, panel names,
and internal build-target keys are not release names.
| Family | Chip | Runtime profiles | Canonical partition |
|---|---|---|---|
c6 | esp32c6 | st7789, jd9853 | default-8m |
s3 | esp32s3 | st7735-042, gc9107, st7789-130, st7789-154, st7789-190, co5300, st77916, gc9a01-knob-128, st7789-twatch | universal-8m-doom-saves-ota |
p4 | esp32p4 | st7703-4b, jd9365-4c, jd9365-34c | p4-32m-saves-ota |
The S3 image uses an 8 MiB common-denominator dual-OTA layout and contains all
supported S3 panel, touch, power, and peripheral paths, including the
runtime-gated Doom easter egg for the CO5300 (co5300) profile. Doom's heavy
renderer state and mutable engine tables allocate from PSRAM only when it
starts, so normal streaming keeps the internal RAM its SPI DMA rotation repaint
needs. The canonical artifact and app resources carry Doom code but no WAD; the
WAD stays a developer-installed payload read from a raw flash region on the
16/32 MiB CO5300 carrier.
P4 keeps platform, internal build-target, panel, and carrier configuration
separate. The P4 platform owns chip/toolchain/memory/network facts; the internal
composition owns its family selector and partition source; the ST7703 and
JD9365 panel profiles own their MIPI timing; and each carrier profile owns its
GPIOs, GT911, and backlight wiring. The released artifact remains named only
p4.
See firmware family architecture.
Supported boards
Every board this firmware runs on, and what each one supports. "Square" is a functional property, not cosmetic: it decides whether the IMU can drive all four orientations or only a 180-degree flip.
| Runtime profile | Board | Chip | Panel | Resolution | Diagonal | Square | IMU | Automatic orientation | Orientations in the app | Audio |
|---|---|---|---|---|---|---|---|---|---|---|
st7789 | ESP32-C6-LCD-1.47 | esp32c6 | ST7789 | 172x320 | 1.47" | No | No | None | 0/180 | None |
jd9853 | ESP32-C6-Touch-LCD-1.47 | esp32c6 | JD9853 | 172x320 | 1.47" | No | Yes | Flip only (0/180) | 0/180 | None |
st7735-042 | Seeed XIAO ESP32-S3 + Questwise 0.42" 96x54 TFT | esp32s3 | ST7735 | 96x54 | 0.42" | No | No | None | 0/90/180/270 | None |
gc9107 | ESP32-S3-LCD-0.85 | esp32s3 | GC9107 | 128x128 | 0.85" | Yes | No | None | 0/90/180/270 | Speaker + mic |
st7789-130 | ESP32-S3-LCD-1.3 | esp32s3 | ST7789V2 | 240x240 | 1.3" | Yes | Yes | Four-way | 0/90/180/270 | None |
st7789-154 | ESP32-S3-LCD-1.54 | esp32s3 | ST7789 | 240x240 | 1.54" | Yes | Yes | Four-way | 0/90/180/270 | Speaker + 2 mics |
st7789-190 | ESP32-S3-LCD-1.9 | esp32s3 | ST7789V2 | 170x320 | 1.9" | No | Yes | Flip only (0/180) | 0/180 | None |
co5300 | ESP32-S3-Touch-AMOLED-1.75C | esp32s3 | CO5300 (AMOLED) | 466x466 | 1.75" | Yes | Yes | Four-way | 0/90/180/270 | Speaker + 2 mics |
st77916 | ESP32-S3-Touch-LCD-1.85C | esp32s3 | ST77916 (round) | 360x360 | 1.85" | Yes | No | None | 0/90/180/270 | Speaker + 2 mics (V2; V1 stays off) |
gc9a01-knob-128 | ELECROW CrowPanel 1.28" Rotary Display | esp32s3 | GC9A01 (round) | 240x240 | 1.28" | Yes | No | None | 0/90/180/270 | None |
st7789-twatch | LilyGO T-Watch-S3 | esp32s3 | ST7789V3 | 240x240 | 1.54" | Yes | BMA4xx, no driver here | None | 0/90/180/270 | Speaker + PDM mic, not driven here |
st7703-4b | ESP32-P4-WIFI6-Touch-LCD-4B | esp32p4 | ST7703 (MIPI-DSI) | 720x720 | 4" | Yes | No | None | 0/90/180/270 | Speaker + 2 mics |
jd9365-4c | ESP32-P4-WIFI6-Touch-LCD-4C | esp32p4 | JD9365 (MIPI-DSI, round) | 720x720 | 4" | Yes | No | None | 0/90/180/270 | Speaker + 2 mics |
jd9365-34c | ESP32-P4-WIFI6-Touch-LCD-3.4C | esp32p4 | JD9365 (MIPI-DSI, round) | 800x800 | 3.4" | Yes | No | None | 0/90/180/270 | Speaker + 2 mics |
The Audio column is the vendor hardware, sourced in each board's descriptor. Every audio board uses ES8311 (speaker, via an NS4150B amplifier) and ES7210 (microphones), and firmware turns audio on only after both chips answer with their chip IDs over I2C. On the 1.85C that is what separates V2 from V1, whose different audio parts stay off. The C3 board has no audio.
Orientation rules behind the Square, IMU and orientation columns:
- A square panel with an IMU auto-rotates through all four cardinal orientations.
- A non-square panel with an IMU auto-flips between 0 and 180 only. A quarter-turn is never applied automatically, because the quarter-turn framebuffer geometry for non-square panels is intentionally unsupported. Side-pointing gravity holds the last stable 0 or 180 rather than guessing a nearest edge, and only a settled up/down reading commits a change.
- A board with no IMU never rotates on its own, but its orientation is still selectable from the macOS app.
- A manual choice is never overwritten. Effective rotation is
(manual + automatic) mod 4: the app writes the saved rotation, gravity moves only a volatile correction on top of it.
Axis calibration is not uniform. Two profiles are field-verified, each checked in
all four cable positions on real hardware: co5300 (panel X = raw Y, panel Y =
raw X, neither negated) and st7789-154 (the vendor's identity axes, confirmed
correct as shipped rather than assumed). st7789-130, st7789-190 and jd9853 still use the
vendor example's identity axes with no calibration run behind them, so their
automatic orientation may be correctly shaped but wrongly signed until one is
done. Two positions is not a calibration: a single inverted sign leaves one
opposite pair correct and only the other pair wrong, which is how a wrong
mapping survived several confirmations here.
Board identity, pin maps, panel geometry, and motion mappings are declared in
boards/*.toml; those descriptors are the source of truth if this
table or generated firmware data drifts.
Runtime profile selection
Profile selection occurs before panel GPIO initialization.
- C6 probes the shared GPIO18/19 I2C bus. A responding touch/motion path selects
jd9853; a silent bus selectsst7789. An inconclusive C6 probe uses the electrically safer profile. - On 8 MiB S3 parts the probe is the 0.85's codec bus (ES8311
0x18, ES72100x40on GPIO42/41): an answer selectsgc9107, silencest7735-042(the XIAO has nothing on those pads and its screen cannot be read back). S3 then probes distinct I2C buses on larger-flash carriers. Thest7789-130profile uses the QMI8658A at0x6Bon GPIO47/48 and routes CFG commands through its CH343 UART bridge; existing S3 profiles retain native USB CDC. Thest7789-190carrier answers identically on GPIO47/48, so both profiles also read GPIO4 with the pull-down enabled: the 1.9's system-rail divider reads at least 250 mV and the 1.3's bare header pin at most 150 mV. Thegc9a01-knob-128profile powers its GPIO1-switched panel rail for the probe and looks for its CST816D at0x15on GPIO6/7. Thest7789-twatchwatch answers on its always-powered main bus, SDA GPIO10 / SCL GPIO11 (AXP21010x34, BMA4xx0x19, PCF85630x51), probed before the 1.75C's and the knob's buses. Its panel, backlight and touch run from AXP2101 ALDO2/ALDO3, which panel bring-up sets to LilyGO's 3300 mV and switches on. Exactly one compatible candidate is required. - P4's carriers wire identical I2C buses, so P4 needs signals that agree
before choosing. It reads GPIO52 with the pull-down enabled (the 4B's
Ethernet PHY pulls that MDIO line up with 1.5K; on the XC carrier of the 4C
and 3.4C it is a floating header pin), the resolution stored in the GT9271
touch controller's config (register
0x8048, read-only), and its MIPI-DSI panel's controller ID (DCS0x04, with a one-second deadline). The 4B needs its ST7703's38 21 1Fand a high GPIO52. The 4C needs its JD9365's93 65 04, a low GPIO52 and a 720x720 touch config; the 3.4C the same with an 800x800 one. Any disagreement, or a failed read, resolves neither, like an unresolved S3, and the backlight is switched off. An XC board with something on its header that pulls GPIO52 high is therefore Unknown; sendCFGBOARD jd9365-4c(orjd9365-34c) over serial to pin it. The 3.4C profile comes from Waveshare's BSP and has not been run on 3.4C hardware. The internal family selector remains a build implementation detail and the artifact isp4.
Zero or multiple S3 or P4 candidates leave display, networking, and streaming
disabled. Serial remains active so CFGBOARD <profile> can provide an explicit
recovery override. CFGBOARD auto clears the override.
Current firmware reports independent identity evidence:
target=c3|c6|s3|p4
chip=esp32c3|esp32c6|esp32s3|esp32p4
profile=<runtime profile>
partition=<compatibility token>The Mac app requires all four fields to auto-select embedded firmware. Missing, unknown, conflicting, or stale evidence fails closed.
Canonical releases
Canonical firmware is committed under firmware-releases/:
firmware-releases/manifest.json
firmware-releases/c6/espdisp-c6-<version>.espdispfw
firmware-releases/s3/espdisp-s3-<version>.espdispfw
firmware-releases/p4/espdisp-p4-<version>.espdispfwThe catalog lists each latest SemVer, relative path, SHA-256, byte size, chip, profiles, hardware mappings, flash capacities, partition compatibility, flash addresses, and required identity fields. Catalog and bundle readers reject duplicate JSON member names.
A current .espdispfw contains one family only. Historical multi-target bundles
remain readable but current writers never produce one.
Generate all three build-numbered development artifacts and their local catalog:
python3 tools/espdisp.py releaseThis writes under the ignored firmware-dev/ root. Build-numbered firmware is
never allowed under firmware-releases/, so a development run cannot replace
the committed shipping catalog with paths to ignored artifacts. Shipping cuts
use bare versions under firmware-releases/ after the user bumps FW_VERSION;
the current build-numbered writer is development-only. An intentional rebuild
of an existing shipping version must confirm that exact version:
python3 tools/espdisp.py release --shipping --regenerate-existing 1.5.0Without that confirmation, release --shipping refuses to replace committed
shipping artifacts.
Inspect each artifact independently:
python3 tools/espdisp.py bundle-info firmware-releases/c6/espdisp-c6-1.5.0.espdispfw
python3 tools/espdisp.py bundle-info firmware-releases/s3/espdisp-s3-2.1.0.espdispfw
python3 tools/espdisp.py bundle-info firmware-releases/p4/espdisp-p4-1.5.0.espdispfwbundle-info reports release-note availability or count, never the prose.
release-notes.md at the repository root is the sole authored source for that
prose.
Build firmware
Install arduino-cli and the Espressif Arduino core, then build by family:
python3 tools/espdisp.py compile --family c6
python3 tools/espdisp.py compile --family s3
python3 tools/espdisp.py compile --family p4The CLI stages custom partition files in private temporary sketch copies. It
does not mutate firmware/display_stream/partitions.csv.
Flash over USB
List connected ports and optionally probe chip identity:
python3 tools/espdisp.py list --probeFlash one family. Supplying --port is required when more than one candidate is
connected:
python3 tools/espdisp.py flash --family s3 --port /dev/cu.usbmodem1101
python3 tools/espdisp.py flash --family p4 --profile st7703-4b \
--port /dev/cu.usbmodem5B901669681flash verifies firmware-releases/manifest.json and writes the selected
committed family artifact. It does not compile mutable local source; use
compile or bundle explicitly for development builds.
The normal flash path writes only the bootloader, partition table, OTA initializer, and application segments. It does not issue a whole-chip erase, change fuses/security settings, or rewrite calibration data.
Rescue a board that drops off USB
If firmware drives a board's own USB pins, the board shows up on USB for a
moment after each plug-in and then disappears, too briefly for flash. Unplug
it and run:
python3 tools/espdisp.py rescue --family s3
python3 tools/espdisp.py rescue --family p4 --profile jd9365-4cAs with flash, P4 needs --profile naming the carrier (st7703-4b,
jd9365-4c or jd9365-34c), because its image drives the display pins of any
P4 it lands on. The app's Repair flow asks for the board for the same reason.
Wait for "Plug the board in now", then plug it in; replug whenever it says
"Missed it". Only a port that appears after the command starts is touched. The
board then runs the rescue image from firmware-rescue/<family>/: it keeps USB
alive, never drives the USB pins, and shows RESCUE with the detected board name
on the panel. The ESP32-S3-LCD-1.3's backlight is on a USB pin, so its panel
stays dark. Follow up with flash. Rescue writes the bootloader, the release
partition table, the OTA selector and its own app, and nothing else, so NVS
settings and the Doom WAD survive. Rebuild a family's rescue image with
rescue-build --family <family> after changing the rescue sketch, board
detection or a panel profile.
A profile forced earlier with CFGBOARD is stored in NVS and would be forced
again by the real firmware. The rescue screen then shows it in red, and
rescue warns about it. Clear it before flash with
python3 tools/espdisp.py config --port <port> CFGBOARD auto, which the rescue
image answers by removing only that setting.
Serial configuration
Send one CFG* command with the stdlib-only CLI:
python3 tools/espdisp.py config --port /dev/cu.usbmodem1101 CFGSHOW
python3 tools/espdisp.py config --port /dev/cu.usbmodem1101 CFGBOARD co5300
python3 tools/espdisp.py config --port /dev/cu.usbmodem1101 CFGPOWER 0
python3 tools/espdisp.py config --port /dev/cu.usbmodem1101 CFGROT 2
python3 tools/espdisp.py config --port /dev/cu.usbmodem1101 CFGAUTOROT 1
python3 tools/espdisp.py config --port /dev/cu.usbmodem1101 CFGMIRRORX 1
python3 tools/espdisp.py config --port /dev/cu.usbmodem1101 \
CFGBRIGHTLEVELS 24 128 10 255
python3 tools/espdisp.py config --port /dev/cu.usbmodem1101 CFGFIXEDBL 255
python3 tools/espdisp.py config --port /dev/cu.usbmodem1101 CFGGAME QUAKE
python3 tools/espdisp.py config --port /dev/cu.usbmodem1101 CFGQUIET 1CFGAUTOROT 0|1 persists motion-based orientation; CFGSHOW reports both
auto= (the current motion correction) and effective= (the rotation actually
applied after composing it with rot=). CFGBRIGHTLEVELS low high idle survey
persists the four runtime brightness presets, each from 1 through 255, with
low < high.
CFGMIRRORX persists a left/right reflection for prism installations.
CFGFIXEDBL 1..255 locks every lit state at that level and stops advertising
brightness controls; CFGFIXEDBL 0 restores normal brightness behavior.
CFGGAME <label> starts a boot-menu row by its label (DOOM, QUAKE, a stored
WAD's name such as FREEDOOM1, or STREAMING), as choosing it on the menu
does. A game restarts the board into it, so it sends no reply; otherwise it
answers CFGOK game=STREAMING, CFGERR the boot menu offers no <label>, or
CFGERR could not save the <label> request. See docs/game-store.md.
CFGQUIET 1 is quiet mode for benchmark and demo runs (timedemo demo1):
Doom and Quake start with no sound at all, effects or music, on every family
that runs them. It is persisted, so it lasts across restarts until
CFGQUIET 0, applies from the next game boot, and replies
CFGOK quiet=1 (saved; Doom and Quake start with no sound). CFGSHOW reports
it as quiet=. A quiet game logs sound: off, quiet mode on serial and never
powers the codec or amplifier. Audio streamed from the Mac is not affected.
WiFi and names use base64-safe helper flows in the app. OTA stays disabled until a password is stored:
python3 tools/espdisp.py set-password --port /dev/cu.usbmodem1101OTA
OTA requires live mDNS evidence for matching family, chip, profile, and partition. Discovery cannot be disabled for a family-safe push.
export ESPDISP_OTA_PASSWORD='<password>'
python3 tools/espdisp.py ota panel.local --family s3OTA writes the inactive application slot. It does not replace bootloader or partition metadata. USB remains the recovery path.
The app updates a board that advertises CAP_STREAM_UPDATE over its existing
stream connection instead (stream_update.h, StreamUpdateProtocol.swift):
the same password and PBKDF2 challenge, a window of CRC-checked chunks with
selective acks, and the image's SHA-256 checked before Update.end() switches
the boot slot. The board never connects back to the Mac, so a Mac firewall that
drops connections from the LAN does not block it. Streaming to that board
pauses for the transfer. Boards without the bit, and the CLI, keep ArduinoOTA.
macOS app
Build and install to /Applications/ESPDisplay.app:
mac/make-app.shThe Xcode build phase validates firmware-releases/manifest.json and copies
exactly its latest c3/c6/s3/p4 artifacts directly into the app before signing. No
second repository copy exists under mac/.
A development fallback can build without firmware resources:
ESPDISP_SKIP_FIRMWARE=1 mac/make-app.shFor a discovered panel, the update sheet auto-selects only from complete runtime identity. For a blank/recovery board, the Add Display sheet requires an explicit hardware-profile choice, then rechecks chip and MAC with esptool before writing. Wrong family/chip/profile/partition, absent identity, ambiguous resources, and catalog size/hash drift are refused.
A P4 or S3 display's Firmware section also has a Games data row. Its
Install Game Data… sheet adds Doom WADs and Quake paks to the board's game
store over USB: choose your own files or download Quake's shareware pak0.pak,
see which are already on the board and how much room is left, and write the
checked ones. See docs/game-store.md.
Release notes are rendered as literal text. Legacy bundles receive accessible fallback text, and note metadata never affects transfer or update-safety logic.
Protocols
Band stream
The classic six-byte header contains frame ID, band index/orientation flags, and dirty count. Packed datagrams may carry multiple raw or RLE565 bands.
Tile stream
The CO5300 profile uses 16×16 dirty tiles with raw, RLE565, BC1, and half-resolution BC1 codecs. Visible-span records avoid transmitting hidden pixels outside round glass.
ETL1 large tiles
The P4 profile advertises CAP_LARGE_TILE_STREAM. ETL1 uses 16-bit tile indices
for grids too large for tile v1. Records are validated and decoded before
completion state is committed, so malformed records can be retried without
poisoning frame state.
P4 measured state
Attached revision-v1.3 hardware identified as ESP32-P4, base MAC
e8:f6:0a:e0:b5:5a, with 32 MiB flash. Display diagnostics measured correct
RGB primaries, edges/corners, full and partial draws, four software transforms,
GT911 portrait markers/releases, BOOT rotation, and backlight controls with no
draw errors.
Hosted networking remains blocked at Req_GetCoprocessorFwVersion with the
factory C6 image. The C6 coprocessor has not been modified and is not
electrically accessible through the current authorized development path.
Reachable mDNS, ETL1 over WiFi, reconnect, OTA, motion tearing, and touch corners
in odd orientations remain unverified.
Repository layout
| Path | Purpose |
|---|---|
firmware/display_stream/ | Main universal-family firmware |
firmware/libraries/espdisp_board/ | Platform/panel/carrier composition and display facade |
firmware/test/ | Hardware-free ASan/UBSan host tests |
tools/espdisp.py | Build, release, inspect, flash, OTA, and serial CLI |
tools/test_espdisp.py | Python source/parser/writer/catalog tests |
firmware-dev/ | Ignored build-numbered artifacts and local catalog |
firmware-releases/ | Canonical committed family artifacts and catalog |
firmware-rescue/ | Prebuilt rescue images, one per family (firmware/rescue/) |
mac/ESPDisplaySender/ | Native app, protocol readers, and Swift tests |
docs/firmware-target-architecture.md | Family selection and extension rules |
docs/code-structure.md | Module ownership |
Validation
Run the complete source gate before release:
bash firmware/test/run_tests.sh
python3 tools/test_espdisp.py
python3 tools/espdisp.py compile --family c6
python3 tools/espdisp.py compile --family s3
python3 tools/espdisp.py compile --family p4From mac/ESPDisplaySender, run swift test. After generating canonical
artifacts, run bundle-info on all three, cross-read them with Swift tests,
build/install the signed app, and compare every embedded size/hash to the
catalog.
Parts
3| Qty | Value | Footprint | References |
|---|---|---|---|
| 1 | Questwise_TFT_042_96x54 | tiny-tft-carrier:Questwise_TFT_042_96x54_2x4 | J1 |
| 1 | SolderJumper_2_Open | tiny-tft-carrier:SolderJumper_2_Open | JP1 |
| 1 | XIAO_ESP32_DIP14 | tiny-tft-carrier:XIAO_ESP32_DIP14 | U1 |
Questions
How big is the Tiny tft carrier PCB?
It measures 36.5 × 23 mm and has 2 copper layers, 1.6 mm thick.
What parts does the Tiny tft carrier use?
3 parts. The full list of its parts is on this page, with links to each part's symbol and footprint where PCBKit's library has them.
Where can I download the Tiny tft carrier KiCad files?
From the Djspaceg/esp32-display repository on GitHub, at the commit this page was made from: the board is hardware/tiny-tft-carrier/tiny-tft-carrier.kicad_pcb and its schematic hardware/tiny-tft-carrier/tiny-tft-carrier.kicad_sch. PCBKit opens the board in the browser, with nothing to install.
Can I use the Tiny tft carrier design in my own project?
Its repository publishes it under the MIT licence: you may reuse it on that licence's terms (keep the author's notice, and for a copyleft licence share your changes alike).
Similar boards
- 2 layers40 × 30 mm

- 2 layers51.9 × 16 mm

Passive2-rpi non standard adapter
hzeller/rpi-rgb-led-matrix
Controlling up to three chains of 64x64, 32x32, 16x32 or similar RGB LED displays using Raspberry Pi GPIO
3 parts4.4kGPL-2.0 - 2 layers16.3 × 16.3 mm

- 2 layers

Sub
atopile/atopile
Design circuit boards with code! ✨ Get software-like design reuse 🚀, validation, version control and collaboration in hardware; starting with electronics ⚡️
3 parts4kMIT - 2 layers85 × 30 mm

- 2 layers124.5 × 33.7 mm

- 2 layers10.5 × 83 mm

- 2 layers21.2 × 51.3 mm

Pico reset
hackclub/sprig
🍃 Learn to code by making games in a JavaScript web-based game editor.
3 parts1.1kMIT