Waveshare ESP32-C6-Touch-LCD-1.47
Board-specific firmware knowledge. Most failure modes here are silent — the firmware
runs, the panel lights up, the touch controller ACKs, and something is subtly wrong — so
read the reference file rather than guessing.
reference/board-hardware.md — the complete board reference: master pin table, the
22-pin header, reset-time pin states, strapping analysis, what few pins are free,
peripheral availability, power and battery, memory and partitions, the display's
quirks (Part I), plus a development guide (Part II: §10 toolchain, §11 sdkconfig,
§12 flashing and recovery, §13 cookbook index, §14 symptom → cause → fix, §15 a
line-by-line diff against the non-touch board).
reference/touch-axs5106l.md — the touch controller in full: register map, the four
protocol rules, coordinate mapping per rotation, the three-point calibration, polling
vs. interrupt, LVGL wiring, and which registry components to avoid.
reference/recipes.md — copy-paste code: platformio.ini, sdkconfig.defaults,
board_pins.h, SPI + JD9853 bring-up, safe rectangle flushing, backlight PWM, I²C
scan, touch, calibration in NVS, BOOT button, battery voltage, the IMU, TF card.
reference/esp32c6-soc.md — the ESP32-C6 silicon datasheet digest, for chip-level
questions: IO MUX and LP IO tables, boot straps, memory, every peripheral's feature
list, electrical and RF characteristics.
reference/esp32-family.md — the rest of the family, for "should this be a different
ESP32?" questions: what does and does not port between chips, radio and USB capability
per chip, deep-sleep memory and ULP/LP-core availability, a chip-selection table.
template/ — a project that builds clean, in two variants, plus a scaffold script.
See template/README.md.
Confirm the board first
Waveshare sells two 1.47" ESP32-C6 boards with nearly the same name. Getting this wrong
costs a full debugging session, because the wrong pin map produces a dark screen, not an
error.
|
ESP32-C6-LCD-1.47 |
ESP32-C6-Touch-LCD-1.47 (this skill) |
| Screen responds to a finger |
no |
yes |
| Visible extras |
WS2812 RGB LED next to the panel |
no LED; two status LEDs by the USB port |
| SKU |
28563 / 30381 |
31203 / 31201 |
| Header |
18-pin (-M variant) |
22-pin |
| Battery pad |
none |
VBAT |
| Panel controller |
ST7789 |
JD9853 |
| Flash |
4 MB (C6FH4) |
8 MB (C6FH8) |
For the other one use the esp32c6-lcd147 skill. Do not port pin numbers between
them — reference/board-hardware.md §15 lists every difference.
Orientation
|
|
| SoC |
ESP32-C6FH8, RISC-V RV32IMAC @ 160 MHz + LP core @ 20 MHz, QFN32, 22 GPIOs |
| Memory |
8 MB in-package flash (1 MB default app partition), 512 KB HP SRAM (327,680 B linkable), 16 KB LP SRAM. No PSRAM |
| Display |
JD9853 172×320 on SPI2 — SCLK GPIO1, MOSI GPIO2, CS GPIO14, DC GPIO15, RST GPIO22. Write-only, no MISO |
| Backlight |
GPIO23, active high, into an SS8050 NPN base through 1 kΩ. LEDC PWM |
| Touch |
AXS5106L, I²C 0x63 — SDA GPIO18, SCL GPIO19, RST GPIO20, INT GPIO21 (active low) |
| IMU |
QMI8658A, same I²C bus, 0x6B (SA0 grounded). INT1 GPIO5, INT2 GPIO6 |
| Storage |
microSD on the same SPI2 bus, CS GPIO4, MISO GPIO3. SPI 1-bit only, FAT32 |
| Buttons |
BOOT GPIO9, pressed = low (not GPIO8 — the wiki is wrong) · RESET acts on CHIP_PU |
| Battery |
ETA6098 charger, VBAT pad; sense on GPIO0 through 200 k/100 k → VBAT = 3 × V(GPIO0) |
| USB |
Type-C straight to the SoC's USB-Serial-JTAG (GPIO12/13). No bridge chip, no debug header |
| Free pins |
GPIO7 and GPIO8 only. No ADC channel is free; GPIO7 is the only LP-IO/wake pin left |
| Radio |
Wi-Fi 6 / BLE 5 / 802.15.4, onboard ceramic antenna |
| Toolchain |
PlatformIO 6.1.19 + platform-espressif32 7.1.0 + ESP-IDF 6.1.0, board = esp32-c6-devkitc-1 |
Rules that prevent the expensive mistakes
Each of these produces a failure that looks like something else.
- The BOOT button is GPIO9, not GPIO8. The vendor pinout table says GPIO8; the
schematic routes Key2 to QFN32 pin 15, which is GPIO9. GPIO8 reaches only header
pin 20. Firmware that polls GPIO8 for "is BOOT held" reads a floating pin and sees
not pressed forever — the feature simply never fires, with no error.
CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y in sdkconfig.defaults. Without it the
console defaults to UART0 on GPIO16/17, which only reach the header: the monitor is
silent while the firmware runs perfectly, and you debug blind.
- Pin
monitor_port and upload_port to /dev/cu.usbmodem*. Unpinned, PlatformIO
takes the first /dev/cu.* it finds — on macOS a Bluetooth or AirPlay pseudo-port —
and attaches to nothing. Same silent-monitor symptom as rule 2, different cause.
- Write your own JD9853 driver, or use
template/components/jd9853/. ESP-IDF has no
JD9853. The registry component mydazy/esp_lcd_jd9853 does not compile on IDF 6.x
(rgb_endian → rgb_ele_order) and its init sequence targets a 240×284 BOE panel.
- Send the vendor init batch verbatim, and nothing else.
0xDF 98 53 unlocks the
vendor registers, 0xDE selects the register page, 0xC8 is 32 bytes of gamma. It
already contains SLPOUT, COLMOD, MADCTL and ends with INVON (0x21) —
this panel is inverted. Calling esp_lcd_panel_invert_color() or resending
COLMOD/MADCTL on top upsets the register paging.
esp_lcd_panel_set_gap(panel, 34, 0). The 172-pixel glass is centred in the
controller's 240-column RAM: (240 − 172) / 2 = 34. Without it the image is shifted
sideways and wraps at the edge.
esp_lcd_panel_draw_bitmap() is asynchronous. It queues a DMA transfer and
returns. Compose the next tile into the same buffer and the panel gets half-old,
half-new bytes — the screen fills with coloured garbage, intermittently, which
reads as a flaky panel or a bad ribbon. Register on_color_trans_done, give a
semaphore from it, take it after every draw_bitmap. This one costs a day.
- Byte-swap RGB565 once, at compile time, in the colour macro. The panel is
big-endian and the CPU is not. Swapping per pixel in a loop costs more than whatever
you are drawing.
- Park TF
CS (GPIO4) high before opening the SPI bus. The card and the panel share
SCLK (GPIO1) and MOSI (GPIO2) and are separated only by chip select. An unparked card
corrupts the display's traffic while doing nothing itself.
- Initialise SPI2 once. A second
spi_bus_initialize() returns
ESP_ERR_INVALID_STATE. Adding a device means a third CS — and GPIO7 and GPIO8 are
the only pins available for it.
- Claim GPIO23 with LEDC at 0 % duty before touching the panel. GPIO23 has the
SoC's internal weak pull-up at reset, which is enough base drive to switch the
backlight transistor on: the panel is lit from power-on, showing whatever its RAM
held. Raise the duty only after the first full flush.
- 40 MHz is the pixel-clock ceiling and it is a wiring fact. The board puts SCLK on
GPIO1 — which has no FSPI IO-MUX function — and MOSI on GPIO2, whose IO-MUX slot is
MISO. Both go through the GPIO Matrix at roughly half the 80 MHz IO-MUX ceiling. No
software change recovers it. A full-screen flush is 22 ms → ~45 fps, hard.
- 8 MB of flash is still a 1 MB app partition.
CONFIG_PARTITION_TABLE_SINGLE_APP
is the default and leaves 7 MB unpartitioned. Nothing warns you until the image
overflows; add a custom partitions.csv before planning on the space.
- The AXS5106L needs the whole 14-byte frame, in two transactions. Short reads leave
it misaligned and every following frame comes back shifted; a repeated START
(
i2c_master_transmit_receive()) fails every read while the address still ACKs.
Detect presence by address ACK, never by chip ID — some revisions report all
zeros while working. See reference/touch-axs5106l.md.
TP_RST low for 200 ms, then wait 300 ms. Shorter pulses look fine — the chip
ACKs — and then it reports nonsense coordinates.
- The board has two free GPIOs and no free ADC channel. All seven ADC channels
(GPIO0–GPIO6) are used, and neither free pin has an analog function. Before promising
a user an analog input, decide what they are giving up: the IMU interrupts (GPIO5/6)
or the card slot (GPIO3/4).
- Never pull GPIO8 low across a reset, and never burn
EFUSE_UART_PRINT_CONTROL or
EFUSE_JTAG_SEL_ENABLE. GPIO8 floats here and is the boot-mode strap; the eFuses
hand boot-time decisions to a floating pin and to the LCD's D/C line, permanently.
When the task is touch
Read reference/touch-axs5106l.md first — it is 190 lines and every one of them was a
failure at some point.
The mapping for this panel at rotation 0 is mirror X, leave Y alone:
screen_x = 171 - raw_x
screen_y = raw_y
If the dot is mirrored, you skipped the mirror. If it is in the opposite corner, both
axes got flipped. If it moves along the wrong axis, X and Y got swapped. If it tracks but
compressed, the raw full scale is not the panel resolution — scale, do not clamp.
Do not hard-code a guess for a panel that misbehaves. The template derives the mapping
from three taps (origin, displaced along screen X, displaced along screen Y) and stores
it in NVS; that is exactly enough information for all eight orientations plus a different
raw range. Hold BOOT (GPIO9) while pressing RESET to run it.
When the task is display performance or memory
The bus is the limit, not the CPU: 110,080 bytes at 40 MHz is 22.0 ms on the wire, so
full-screen redraws cap at ~45 fps however cheap the drawing is.
The template does not keep a framebuffer. It renders straight from the scene into a
172 × 32 DMA staging buffer (11,008 B) band by band, which is why the full variant links
at 12.9 KB of static RAM instead of 123 KB. The trade is CPU for RAM, and it is the right
one here: 110 KB of DRAM buys nothing when the bus caps you at 45 fps anyway.
What actually buys speed, in order of payoff:
- Redraw less.
esp_lcd_panel_draw_bitmap() takes a rectangle; the end coordinates
are exclusive and the source stride must equal the rectangle width. A 172 × 40 status
strip is 2.75 ms instead of 22 ms.
- Redraw in fewer calls. Pixel arithmetic is only half the cost: each
draw_bitmap() pays about 450 µs of address commands and DMA round trip on top of
its pixels, so a 14 × 14 tile costs 0.53 ms of which 78 µs is the wire. The toll is
worth about six such tiles, so merging two nearby rectangles into one beats splitting
them. Once you are pushing dozens of small rectangles a frame, this — not the wire — is
what you are fighting. board-hardware.md §9.5 has the table and the frame-budget rule.
- Double-buffer, if you are keeping framebuffers at all — 110 KB each out of
327,680 B. It overlaps CPU and DMA but does not raise the 45 fps ceiling.
- Nothing else. Do not go looking for a faster clock (rule 12).
Above 45 fps, budget the frame rather than repainting on demand. A full 172 × 320
repaint never fits in a 16.7 ms frame, so spend a fixed allowance per frame — counted in
tile-equivalents, with each draw_bitmap() charged its ~6 tiles — and leave the rest
dirty for the next frame, resuming where you stopped. ✅ A 60 fps game with a 10 × 20
matrix of 14 px cells holds a steady 60 with a worst frame of 15 ms this way; the same
code repainting on demand dropped to 59 with 19 ms spikes.
If you pace frames, set CONFIG_FREERTOS_HZ=1000. The default is 100, so the shortest
vTaskDelay() is 10 ms and a 16.7 ms frame period cannot be held — the rate quietly lands
wherever the rounding puts it. Pace against esp_timer_get_time(), sleep on the tick.
Keep per-pixel loops integer-only: this RISC-V core has no FPU. Float belongs in
one-time init paths, never in a redraw.
For LVGL: two full framebuffers plus LVGL plus a Wi-Fi stack (~50 KB) does not fit in
327,680 B. Use partial buffers of a few dozen lines — the same shape as the template's
tile — and budget before writing code.
Starting a new project
Do not hand-assemble one. template/ builds clean; scaffold from it:
~/.claude/skills/esp32c6-touch-lcd147/template/variants/new-project.sh <target-dir> [--full|--minimal]
cd <target-dir> && pio run
--minimal — console heartbeat, backlight PWM breathing and an I²C bus scan. No
display, no SPI bus. 192,320 B flash, 12,040 B static RAM. Flash this first on a
new board: it proves the toolchain, the flashing route, the console and the I²C bus
while the display cannot confuse the diagnosis, and its three outputs (log line,
backlight, scan result) fail independently. A healthy board answers at 0x63 and 0x6B.
--full (default) — JD9853 display, AXS5106L touch with a live dot and coordinates,
three-point calibration in NVS, 5 × 7 font, tile renderer. 271,242 B flash
(25.9 % of the 1 MB default app partition), 12,892 B static RAM plus an 11,008 B
DMA tile buffer on the heap.
Both build as-is with ESP-IDF 6.1.0 (verified). Nothing is generated and no paths are
embedded, so copying the tree by hand works identically. template/README.md maps files
to subsystems so a --full scaffold can be stripped back cleanly.
When the user already has a project, prefer bringing it in line with the template's
platformio.ini, sdkconfig.defaults and board_pins.h over rewriting their code.
Flashing
Nothing to press:
pio run -t upload -t monitor # or: idf.py -p <port> flash monitor
The USB Serial/JTAG controller supports host-controlled reset and download-mode entry, so
esptool drives the whole cycle over the Type-C cable. The board enumerates as CDC-ACM
(/dev/cu.usbmodem*, /dev/ttyACM*, or a COM port). Close the monitor before
uploading — Could not open … port is busy means one is still holding it.
Recovery, in order:
- Nothing enumerates at all → almost always a charge-only cable. The ROM creates the USB
device on a blank chip, so bad firmware cannot brick this board.
- Firmware wedged USB, or a console change broke enumeration → hold BOOT, tap RESET,
release BOOT.
- Still nothing → jumper header pin 20 (GPIO8) to 3V3 (pin 8) and repeat. Joint
Download Boot needs GPIO8 = 1 while GPIO9 = 0, and GPIO8 floats on this board. This is
what Waveshare's FAQ means by "temporarily connect GPIO8 to 3V3".
- Still stuck →
esptool.py -p <port> erase_flash. An 8 MB erase takes a while; it is
probably not hung.
There is no debug header. JTAG is the same Type-C port via the built-in controller — but
not with the openocd PlatformIO installs here: platform-espressif32 pins
tool-openocd-esp32 to ~2.1100.0, which predates the ESP32-C6 and ships no
board/esp32c6-builtin.cfg, so pio debug fails with what looks like a typo. Use a
current upstream openocd-esp32 from Espressif, or an ESP-IDF export'd environment's
idf.py openocd.
Reporting
Say what is verified on hardware and what is derived.
✅ Ran on a real board: the display path (pin map, JD9853 vendor batch, 40 MHz clock,
set_gap(34, 0), INVON, big-endian RGB565, the async-draw_bitmap semaphore, the tile
renderer), the AXS5106L touch path (14-byte frame, no repeated START, 200/300 ms reset,
presence by ACK, the mirror-X mapping) and the three-point calibration, the LEDC
backlight at 90 %, the USB console, and both template variants building on ESP-IDF 6.1.0.
Also on a real board, from a 60 fps game built on this template: a per-frame redraw
budget holds 60 fps where repainting on demand does not (§9.5 has the numbers), the
git describe / PROJECT_VER build failure and its fix, and the CONFIG_FREERTOS_HZ
sleep quantum. ⚠︎ Within that, the 450 µs per draw_bitmap figure is derived from
whole-frame timings rather than from timing a call in isolation — the fixed cost is
certainly there and certainly dominates small rectangles, but quote the number as an
order of magnitude, not a measurement.
⚠︎ Schematic- or datasheet-derived, never exercised: the BOOT = GPIO9 correction
(netlist, unambiguous, but the template's calibration hatch has not been re-tested on
hardware since the fix), the backlight-lit-at-reset conclusion, the QMI8658A address
0x6B, the battery-voltage recipe, the TF-card recipe, the header's physical row layout,
and the 40 MHz GPIO-Matrix ceiling (the number is conventional, not measured here).
Everything marked ⚠︎ Inference in reference/board-hardware.md is in this category —
flag it as such rather than presenting it as fact.
Not covered at all: Wi-Fi/BLE/802.15.4 bring-up beyond memory budgeting, LVGL beyond
the wiring sketch in reference/touch-axs5106l.md §9, and deep sleep.
1---2name: esp32c6-touch-lcd1473description: Waveshare ESP32-C6-Touch-LCD-1.474---56# Waveshare ESP32-C6-Touch-LCD-1.4778Board-specific firmware knowledge. Most failure modes here are silent — the firmware9runs, the panel lights up, the touch controller ACKs, and something is subtly wrong — so10read the reference file rather than guessing.1112- `reference/board-hardware.md` — the complete board reference: master pin table, the13 22-pin header, reset-time pin states, strapping analysis, what few pins are free,14 peripheral availability, power and battery, memory and partitions, the display's15 quirks (Part I), **plus** a development guide (Part II: §10 toolchain, §11 sdkconfig,16 §12 flashing and recovery, §13 cookbook index, §14 symptom → cause → fix, §15 a17 line-by-line diff against the non-touch board).18- `reference/touch-axs5106l.md` — the touch controller in full: register map, the four19 protocol rules, coordinate mapping per rotation, the three-point calibration, polling20 vs. interrupt, LVGL wiring, and which registry components to avoid.21- `reference/recipes.md` — copy-paste code: `platformio.ini`, `sdkconfig.defaults`,22 `board_pins.h`, SPI + JD9853 bring-up, safe rectangle flushing, backlight PWM, I²C23 scan, touch, calibration in NVS, BOOT button, battery voltage, the IMU, TF card.24- `reference/esp32c6-soc.md` — the ESP32-C6 silicon datasheet digest, for chip-level25 questions: IO MUX and LP IO tables, boot straps, memory, every peripheral's feature26 list, electrical and RF characteristics.27- `reference/esp32-family.md` — the rest of the family, for "should this be a different28 ESP32?" questions: what does and does not port between chips, radio and USB capability29 per chip, deep-sleep memory and ULP/LP-core availability, a chip-selection table.30- `template/` — a **project that builds clean**, in two variants, plus a scaffold script.31 See `template/README.md`.3233## Confirm the board first3435Waveshare sells two 1.47" ESP32-C6 boards with nearly the same name. Getting this wrong36costs a full debugging session, because the wrong pin map produces a dark screen, not an37error.3839| | ESP32-C6-**LCD**-1.47 | ESP32-C6-**Touch**-LCD-1.47 (this skill) |40|---|---|---|41| Screen responds to a finger | no | **yes** |42| Visible extras | WS2812 RGB LED next to the panel | **no LED**; two status LEDs by the USB port |43| SKU | 28563 / 30381 | **31203 / 31201** |44| Header | 18-pin (`-M` variant) | **22-pin** |45| Battery pad | none | **`VBAT`** |46| Panel controller | ST7789 | **JD9853** |47| Flash | 4 MB (C6FH4) | **8 MB (C6FH8)** |4849For the other one use the `esp32c6-lcd147` skill. Do **not** port pin numbers between50them — `reference/board-hardware.md` §15 lists every difference.5152## Orientation5354| | |55|---|---|56| SoC | ESP32-C6FH8, RISC-V RV32IMAC @ 160 MHz + LP core @ 20 MHz, **QFN32, 22 GPIOs** |57| Memory | **8 MB** in-package flash (**1 MB default app partition**), 512 KB HP SRAM (327,680 B linkable), 16 KB LP SRAM. **No PSRAM** |58| Display | **JD9853 172×320** on **SPI2** — SCLK **GPIO1**, MOSI **GPIO2**, CS GPIO14, DC GPIO15, RST GPIO22. Write-only, no MISO |59| Backlight | **GPIO23**, active high, into an SS8050 NPN base through 1 kΩ. LEDC PWM |60| Touch | **AXS5106L**, I²C **0x63** — SDA **GPIO18**, SCL **GPIO19**, RST GPIO20, INT GPIO21 (active low) |61| IMU | **QMI8658A**, same I²C bus, **0x6B** (SA0 grounded). INT1 GPIO5, INT2 GPIO6 |62| Storage | microSD on the **same SPI2 bus**, CS **GPIO4**, MISO **GPIO3**. SPI 1-bit only, FAT32 |63| Buttons | **BOOT GPIO9, pressed = low** (*not* GPIO8 — the wiki is wrong) · **RESET** acts on `CHIP_PU` |64| Battery | ETA6098 charger, `VBAT` pad; sense on **GPIO0** through 200 k/100 k → `VBAT = 3 × V(GPIO0)` |65| USB | Type-C straight to the SoC's USB-Serial-JTAG (GPIO12/13). **No bridge chip, no debug header** |66| Free pins | **GPIO7 and GPIO8 only.** No ADC channel is free; GPIO7 is the only LP-IO/wake pin left |67| Radio | Wi-Fi 6 / BLE 5 / 802.15.4, onboard ceramic antenna |68| Toolchain | PlatformIO 6.1.19 + `platform-espressif32` **7.1.0** + ESP-IDF **6.1.0**, `board = esp32-c6-devkitc-1` |6970## Rules that prevent the expensive mistakes7172Each of these produces a failure that looks like something else.73741. **The BOOT button is GPIO9, not GPIO8.** The vendor pinout table says GPIO8; the75 schematic routes Key2 to QFN32 pin 15, which is GPIO9. GPIO8 reaches only header76 pin 20. Firmware that polls GPIO8 for "is BOOT held" reads a floating pin and sees77 *not pressed* forever — the feature simply never fires, with no error.782. **`CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y`** in `sdkconfig.defaults`. Without it the79 console defaults to UART0 on GPIO16/17, which only reach the header: the monitor is80 silent while the firmware runs perfectly, and you debug blind.813. **Pin `monitor_port` and `upload_port` to `/dev/cu.usbmodem*`.** Unpinned, PlatformIO82 takes the first `/dev/cu.*` it finds — on macOS a Bluetooth or AirPlay pseudo-port —83 and attaches to nothing. Same silent-monitor symptom as rule 2, different cause.844. **Write your own JD9853 driver, or use `template/components/jd9853/`.** ESP-IDF has no85 JD9853. The registry component `mydazy/esp_lcd_jd9853` does not compile on IDF 6.x86 (`rgb_endian` → `rgb_ele_order`) and its init sequence targets a 240×284 BOE panel.875. **Send the vendor init batch verbatim, and nothing else.** `0xDF 98 53` unlocks the88 vendor registers, `0xDE` selects the register page, `0xC8` is 32 bytes of gamma. It89 already contains `SLPOUT`, `COLMOD`, `MADCTL` and ends with **`INVON` (0x21)** —90 this panel is inverted. Calling `esp_lcd_panel_invert_color()` or resending91 `COLMOD`/`MADCTL` on top upsets the register paging.926. **`esp_lcd_panel_set_gap(panel, 34, 0)`.** The 172-pixel glass is centred in the93 controller's 240-column RAM: (240 − 172) / 2 = 34. Without it the image is shifted94 sideways and wraps at the edge.957. **`esp_lcd_panel_draw_bitmap()` is asynchronous.** It queues a DMA transfer and96 returns. Compose the next tile into the same buffer and the panel gets half-old,97 half-new bytes — the screen fills with coloured garbage, *intermittently*, which98 reads as a flaky panel or a bad ribbon. Register `on_color_trans_done`, give a99 semaphore from it, take it after every `draw_bitmap`. This one costs a day.1008. **Byte-swap RGB565 once, at compile time, in the colour macro.** The panel is101 big-endian and the CPU is not. Swapping per pixel in a loop costs more than whatever102 you are drawing.1039. **Park TF `CS` (GPIO4) high before opening the SPI bus.** The card and the panel share104 SCLK (GPIO1) and MOSI (GPIO2) and are separated only by chip select. An unparked card105 corrupts the display's traffic while doing nothing itself.10610. **Initialise SPI2 once.** A second `spi_bus_initialize()` returns107 `ESP_ERR_INVALID_STATE`. Adding a device means a third CS — and GPIO7 and GPIO8 are108 the only pins available for it.10911. **Claim GPIO23 with LEDC at 0 % duty before touching the panel.** GPIO23 has the110 SoC's internal weak pull-up at reset, which is enough base drive to switch the111 backlight transistor on: the panel is lit from power-on, showing whatever its RAM112 held. Raise the duty only after the first full flush.11312. **40 MHz is the pixel-clock ceiling and it is a wiring fact.** The board puts SCLK on114 GPIO1 — which has *no* FSPI IO-MUX function — and MOSI on GPIO2, whose IO-MUX slot is115 MISO. Both go through the GPIO Matrix at roughly half the 80 MHz IO-MUX ceiling. No116 software change recovers it. A full-screen flush is 22 ms → **~45 fps, hard**.11713. **8 MB of flash is still a 1 MB app partition.** `CONFIG_PARTITION_TABLE_SINGLE_APP`118 is the default and leaves 7 MB unpartitioned. Nothing warns you until the image119 overflows; add a custom `partitions.csv` before planning on the space.12014. **The AXS5106L needs the whole 14-byte frame, in two transactions.** Short reads leave121 it misaligned and every following frame comes back shifted; a repeated START122 (`i2c_master_transmit_receive()`) fails every read while the address still ACKs.123 Detect presence by **address ACK**, never by chip ID — some revisions report all124 zeros while working. See `reference/touch-axs5106l.md`.12515. **`TP_RST` low for 200 ms, then wait 300 ms.** Shorter pulses look fine — the chip126 ACKs — and then it reports nonsense coordinates.12716. **The board has two free GPIOs and no free ADC channel.** All seven ADC channels128 (GPIO0–GPIO6) are used, and neither free pin has an analog function. Before promising129 a user an analog input, decide what they are giving up: the IMU interrupts (GPIO5/6)130 or the card slot (GPIO3/4).13117. **Never pull GPIO8 low across a reset, and never burn `EFUSE_UART_PRINT_CONTROL` or132 `EFUSE_JTAG_SEL_ENABLE`.** GPIO8 floats here and is the boot-mode strap; the eFuses133 hand boot-time decisions to a floating pin and to the LCD's D/C line, permanently.134135## When the task is touch136137Read `reference/touch-axs5106l.md` first — it is 190 lines and every one of them was a138failure at some point.139140The mapping for this panel at rotation 0 is **mirror X, leave Y alone**:141142```143screen_x = 171 - raw_x144screen_y = raw_y145```146147If the dot is mirrored, you skipped the mirror. If it is in the opposite corner, both148axes got flipped. If it moves along the wrong axis, X and Y got swapped. If it tracks but149compressed, the raw full scale is not the panel resolution — scale, do not clamp.150151Do not hard-code a guess for a panel that misbehaves. The template derives the mapping152from three taps (origin, displaced along screen X, displaced along screen Y) and stores153it in NVS; that is exactly enough information for all eight orientations plus a different154raw range. Hold **BOOT (GPIO9)** while pressing RESET to run it.155156## When the task is display performance or memory157158The bus is the limit, not the CPU: 110,080 bytes at 40 MHz is 22.0 ms on the wire, so159full-screen redraws cap at ~45 fps however cheap the drawing is.160161The template does **not** keep a framebuffer. It renders straight from the scene into a162172 × 32 DMA staging buffer (11,008 B) band by band, which is why the full variant links163at 12.9 KB of static RAM instead of 123 KB. The trade is CPU for RAM, and it is the right164one here: 110 KB of DRAM buys nothing when the bus caps you at 45 fps anyway.165166What actually buys speed, in order of payoff:1671681. **Redraw less.** `esp_lcd_panel_draw_bitmap()` takes a rectangle; the end coordinates169 are exclusive and the source stride must equal the rectangle width. A 172 × 40 status170 strip is 2.75 ms instead of 22 ms.1712. **Redraw in fewer calls.** Pixel arithmetic is only half the cost: each172 `draw_bitmap()` pays about **450 µs** of address commands and DMA round trip on top of173 its pixels, so a 14 × 14 tile costs 0.53 ms of which 78 µs is the wire. The toll is174 worth about six such tiles, so merging two nearby rectangles into one beats splitting175 them. Once you are pushing dozens of small rectangles a frame, this — not the wire — is176 what you are fighting. `board-hardware.md` §9.5 has the table and the frame-budget rule.1773. **Double-buffer**, if you are keeping framebuffers at all — 110 KB each out of178 327,680 B. It overlaps CPU and DMA but does not raise the 45 fps ceiling.1794. Nothing else. Do not go looking for a faster clock (rule 12).180181**Above 45 fps, budget the frame rather than repainting on demand.** A full 172 × 320182repaint never fits in a 16.7 ms frame, so spend a fixed allowance per frame — counted in183tile-equivalents, with each `draw_bitmap()` charged its ~6 tiles — and leave the rest184dirty for the next frame, resuming where you stopped. ✅ A 60 fps game with a 10 × 20185matrix of 14 px cells holds a steady 60 with a worst frame of 15 ms this way; the same186code repainting on demand dropped to 59 with 19 ms spikes.187188**If you pace frames, set `CONFIG_FREERTOS_HZ=1000`.** The default is 100, so the shortest189`vTaskDelay()` is 10 ms and a 16.7 ms frame period cannot be held — the rate quietly lands190wherever the rounding puts it. Pace against `esp_timer_get_time()`, sleep on the tick.191192Keep per-pixel loops integer-only: this RISC-V core has **no FPU**. Float belongs in193one-time init paths, never in a redraw.194195For LVGL: two full framebuffers plus LVGL plus a Wi-Fi stack (~50 KB) does not fit in196327,680 B. Use partial buffers of a few dozen lines — the same shape as the template's197tile — and budget before writing code.198199## Starting a new project200201Do not hand-assemble one. `template/` builds clean; scaffold from it:202203```sh204~/.claude/skills/esp32c6-touch-lcd147/template/variants/new-project.sh <target-dir> [--full|--minimal]205cd <target-dir> && pio run206```207208- `--minimal` — console heartbeat, backlight PWM breathing and an I²C bus scan. No209 display, no SPI bus. **192,320 B flash, 12,040 B static RAM.** Flash this first on a210 new board: it proves the toolchain, the flashing route, the console and the I²C bus211 while the display cannot confuse the diagnosis, and its three outputs (log line,212 backlight, scan result) fail independently. A healthy board answers at 0x63 and 0x6B.213- `--full` (default) — JD9853 display, AXS5106L touch with a live dot and coordinates,214 three-point calibration in NVS, 5 × 7 font, tile renderer. **271,242 B flash215 (25.9 % of the 1 MB default app partition), 12,892 B static RAM** plus an 11,008 B216 DMA tile buffer on the heap.217218Both build as-is with ESP-IDF 6.1.0 (verified). Nothing is generated and no paths are219embedded, so copying the tree by hand works identically. `template/README.md` maps files220to subsystems so a `--full` scaffold can be stripped back cleanly.221222When the user already has a project, prefer bringing it in line with the template's223`platformio.ini`, `sdkconfig.defaults` and `board_pins.h` over rewriting their code.224225## Flashing226227Nothing to press:228229```sh230pio run -t upload -t monitor # or: idf.py -p <port> flash monitor231```232233The USB Serial/JTAG controller supports host-controlled reset and download-mode entry, so234esptool drives the whole cycle over the Type-C cable. The board enumerates as CDC-ACM235(`/dev/cu.usbmodem*`, `/dev/ttyACM*`, or a COM port). **Close the monitor before236uploading** — `Could not open … port is busy` means one is still holding it.237238Recovery, in order:2392401. Nothing enumerates at all → almost always a charge-only cable. The ROM creates the USB241 device on a blank chip, so **bad firmware cannot brick this board**.2422. Firmware wedged USB, or a console change broke enumeration → **hold BOOT, tap RESET,243 release BOOT**.2443. Still nothing → **jumper header pin 20 (GPIO8) to 3V3 (pin 8) and repeat.** Joint245 Download Boot needs GPIO8 = 1 while GPIO9 = 0, and GPIO8 floats on this board. This is246 what Waveshare's FAQ means by "temporarily connect GPIO8 to 3V3".2474. Still stuck → `esptool.py -p <port> erase_flash`. An 8 MB erase takes a while; it is248 probably not hung.249250There is no debug header. JTAG is the same Type-C port via the built-in controller — but251not with the openocd PlatformIO installs here: `platform-espressif32` pins252`tool-openocd-esp32` to `~2.1100.0`, which predates the ESP32-C6 and ships no253`board/esp32c6-builtin.cfg`, so `pio debug` fails with what looks like a typo. Use a254current upstream `openocd-esp32` from Espressif, or an ESP-IDF export'd environment's255`idf.py openocd`.256257## Reporting258259Say what is verified on hardware and what is derived.260261**✅ Ran on a real board:** the display path (pin map, JD9853 vendor batch, 40 MHz clock,262`set_gap(34, 0)`, `INVON`, big-endian RGB565, the async-`draw_bitmap` semaphore, the tile263renderer), the AXS5106L touch path (14-byte frame, no repeated START, 200/300 ms reset,264presence by ACK, the mirror-X mapping) and the three-point calibration, the LEDC265backlight at 90 %, the USB console, and both template variants building on ESP-IDF 6.1.0.266267Also on a real board, from a 60 fps game built on this template: **a per-frame redraw268budget holds 60 fps** where repainting on demand does not (§9.5 has the numbers), the269`git describe` / `PROJECT_VER` build failure and its fix, and the `CONFIG_FREERTOS_HZ`270sleep quantum. ⚠︎ Within that, the **450 µs per `draw_bitmap`** figure is derived from271whole-frame timings rather than from timing a call in isolation — the fixed cost is272certainly there and certainly dominates small rectangles, but quote the number as an273order of magnitude, not a measurement.274275**⚠︎ Schematic- or datasheet-derived, never exercised:** the **BOOT = GPIO9** correction276(netlist, unambiguous, but the template's calibration hatch has not been re-tested on277hardware since the fix), the backlight-lit-at-reset conclusion, the QMI8658A address2780x6B, the battery-voltage recipe, the TF-card recipe, the header's physical row layout,279and the 40 MHz GPIO-Matrix ceiling (the number is conventional, not measured here).280Everything marked **⚠︎ Inference** in `reference/board-hardware.md` is in this category —281flag it as such rather than presenting it as fact.282283**Not covered at all:** Wi-Fi/BLE/802.15.4 bring-up beyond memory budgeting, LVGL beyond284the wiring sketch in `reference/touch-axs5106l.md` §9, and deep sleep.