Skip to the content
PCBKitNew board
2 layers36.5 × 23 mmTiny tft carrier, the printed circuit board in 3D

Tiny tft carrier

Djspaceg/esp32-display

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.md

esp32-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._udp mDNS 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.

FamilyChipRuntime profilesCanonical partition
c6esp32c6st7789, jd9853default-8m
s3esp32s3st7735-042, gc9107, st7789-130, st7789-154, st7789-190, co5300, st77916, gc9a01-knob-128, st7789-twatchuniversal-8m-doom-saves-ota
p4esp32p4st7703-4b, jd9365-4c, jd9365-34cp4-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 profileBoardChipPanelResolutionDiagonalSquareIMUAutomatic orientationOrientations in the appAudio
st7789ESP32-C6-LCD-1.47esp32c6ST7789172x3201.47"NoNoNone0/180None
jd9853ESP32-C6-Touch-LCD-1.47esp32c6JD9853172x3201.47"NoYesFlip only (0/180)0/180None
st7735-042Seeed XIAO ESP32-S3 + Questwise 0.42" 96x54 TFTesp32s3ST773596x540.42"NoNoNone0/90/180/270None
gc9107ESP32-S3-LCD-0.85esp32s3GC9107128x1280.85"YesNoNone0/90/180/270Speaker + mic
st7789-130ESP32-S3-LCD-1.3esp32s3ST7789V2240x2401.3"YesYesFour-way0/90/180/270None
st7789-154ESP32-S3-LCD-1.54esp32s3ST7789240x2401.54"YesYesFour-way0/90/180/270Speaker + 2 mics
st7789-190ESP32-S3-LCD-1.9esp32s3ST7789V2170x3201.9"NoYesFlip only (0/180)0/180None
co5300ESP32-S3-Touch-AMOLED-1.75Cesp32s3CO5300 (AMOLED)466x4661.75"YesYesFour-way0/90/180/270Speaker + 2 mics
st77916ESP32-S3-Touch-LCD-1.85Cesp32s3ST77916 (round)360x3601.85"YesNoNone0/90/180/270Speaker + 2 mics (V2; V1 stays off)
gc9a01-knob-128ELECROW CrowPanel 1.28" Rotary Displayesp32s3GC9A01 (round)240x2401.28"YesNoNone0/90/180/270None
st7789-twatchLilyGO T-Watch-S3esp32s3ST7789V3240x2401.54"YesBMA4xx, no driver hereNone0/90/180/270Speaker + PDM mic, not driven here
st7703-4bESP32-P4-WIFI6-Touch-LCD-4Besp32p4ST7703 (MIPI-DSI)720x7204"YesNoNone0/90/180/270Speaker + 2 mics
jd9365-4cESP32-P4-WIFI6-Touch-LCD-4Cesp32p4JD9365 (MIPI-DSI, round)720x7204"YesNoNone0/90/180/270Speaker + 2 mics
jd9365-34cESP32-P4-WIFI6-Touch-LCD-3.4Cesp32p4JD9365 (MIPI-DSI, round)800x8003.4"YesNoNone0/90/180/270Speaker + 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 selects st7789. 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, ES7210 0x40 on GPIO42/41): an answer selects gc9107, silence st7735-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. The st7789-130 profile uses the QMI8658A at 0x6B on GPIO47/48 and routes CFG commands through its CH343 UART bridge; existing S3 profiles retain native USB CDC. The st7789-190 carrier 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. The gc9a01-knob-128 profile powers its GPIO1-switched panel rail for the probe and looks for its CST816D at 0x15 on GPIO6/7. The st7789-twatch watch answers on its always-powered main bus, SDA GPIO10 / SCL GPIO11 (AXP2101 0x34, BMA4xx 0x19, PCF8563 0x51), 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 (DCS 0x04, with a one-second deadline). The 4B needs its ST7703's 38 21 1F and a high GPIO52. The 4C needs its JD9365's 93 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; send CFGBOARD jd9365-4c (or jd9365-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 is p4.

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:

text
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/:

text
firmware-releases/manifest.json
firmware-releases/c6/espdisp-c6-<version>.espdispfw
firmware-releases/s3/espdisp-s3-<version>.espdispfw
firmware-releases/p4/espdisp-p4-<version>.espdispfw

The 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:

sh
python3 tools/espdisp.py release

This 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:

sh
python3 tools/espdisp.py release --shipping --regenerate-existing 1.5.0

Without that confirmation, release --shipping refuses to replace committed shipping artifacts.

Inspect each artifact independently:

sh
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.espdispfw

bundle-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:

sh
python3 tools/espdisp.py compile --family c6
python3 tools/espdisp.py compile --family s3
python3 tools/espdisp.py compile --family p4

The 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:

sh
python3 tools/espdisp.py list --probe

Flash one family. Supplying --port is required when more than one candidate is connected:

sh
python3 tools/espdisp.py flash --family s3 --port /dev/cu.usbmodem1101
python3 tools/espdisp.py flash --family p4 --profile st7703-4b \
  --port /dev/cu.usbmodem5B901669681

flash 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:

sh
python3 tools/espdisp.py rescue --family s3
python3 tools/espdisp.py rescue --family p4 --profile jd9365-4c

As 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:

sh
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 1

CFGAUTOROT 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:

sh
python3 tools/espdisp.py set-password --port /dev/cu.usbmodem1101

OTA

OTA requires live mDNS evidence for matching family, chip, profile, and partition. Discovery cannot be disabled for a family-safe push.

sh
export ESPDISP_OTA_PASSWORD='<password>'
python3 tools/espdisp.py ota panel.local --family s3

OTA 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:

sh
mac/make-app.sh

The 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:

sh
ESPDISP_SKIP_FIRMWARE=1 mac/make-app.sh

For 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

PathPurpose
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.pyBuild, release, inspect, flash, OTA, and serial CLI
tools/test_espdisp.pyPython 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.mdFamily selection and extension rules
docs/code-structure.mdModule ownership

Validation

Run the complete source gate before release:

sh
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 p4

From 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
QtyValueFootprintReferences
1Questwise_TFT_042_96x54tiny-tft-carrier:Questwise_TFT_042_96x54_2x4J1
1SolderJumper_2_Opentiny-tft-carrier:SolderJumper_2_OpenJP1
1XIAO_ESP32_DIP14tiny-tft-carrier:XIAO_ESP32_DIP14U1

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 mmBlinky, the printed circuit board in 3D

    Blinky

    earthtojake/text-to-cad

    Design physical products with AI

    3 parts26kMIT
  • 2 layers51.9 × 16 mmPassive2-rpi non standard adapter, the printed circuit board in 3D

    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 mmSensor, the printed circuit board in 3D

    Sensor

    scottbez1/splitflap

    DIY split-flap display

    3 parts4.2k
  • 2 layersSub, the printed circuit board in 3D

    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 mmDual stomp, the printed circuit board in 3D

    Dual stomp

    torvalds/GuitarPedal

    Linus learns analog circuits

    3 parts2.5kGPL-2.0
  • 2 layers124.5 × 33.7 mm172319-1 connector, the printed circuit board in 3D

    172319-1 connector

    rusefi/rusefi

    rusefi - GPL internal combustion engine control unit

    3 parts1.2k
  • 2 layers10.5 × 83 mmProgrammingAdapter, the printed circuit board in 3D

    ProgrammingAdapter

    stuartpittaway/diyBMSv4

    Version 4 of the diyBMS

    3 parts1.1k
  • 2 layers21.2 × 51.3 mmPico reset, the printed circuit board in 3D

    Pico reset

    hackclub/sprig

    🍃 Learn to code by making games in a JavaScript web-based game editor.

    3 parts1.1kMIT