Waveshare ESP32-C6-LCD-1.47
Board-specific firmware knowledge. Most failure modes on this board are silent — the
firmware runs, the panel lights up, and something is subtly wrong — so read the
reference file rather than guessing.
reference/board-hardware.md — the complete board reference: pin table, reset-time
pin states, strapping analysis, free pins, peripheral availability, power budget,
memory and partitions, the display's quirks (Part I), plus a development guide
(Part II: §10 toolchain, §11 sdkconfig, §12 flashing, §13 peripheral cookbook,
§14 symptom → cause → fix table).
reference/recipes.md — copy-paste code: platformio.ini, sdkconfig.defaults,
board.h, LCD bring-up, LEDC backlight, frame presentation, partial redraw, the
WS2812 driver, TF card on the shared bus, ADC, the BOOT button, font generation.
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, the RMT generation table (WS2812 under Wi-Fi load), deep-sleep memory and
ULP/LP-core availability, and 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 also sells the ESP32-C6-Touch-LCD-1.47 — same vendor, same 1.47" 172×320
panel, different board. If the screen responds to a finger, or there is no WS2812 next to
the panel, it is that one: a JD9853 controller instead of the ST7789, an 8 MB C6FH8
instead of the 4 MB C6FH4, a 22-pin header instead of 18, and a pin map in which only
LCD_CS, LCD_DC and TF_CS match this board's. Use the esp32c6-touch-lcd147 skill
and do not port pin numbers between them — the wrong map gives a dark screen, not an
error.
Orientation
|
|
| SoC |
ESP32-C6FH4, RISC-V RV32IMAC @ 160 MHz + LP core @ 20 MHz, QFN32, 22 GPIOs |
| Memory |
4 MB in-package flash (1 MB default app partition), 512 KB HP SRAM (~320 KB linkable), 16 KB LP SRAM |
| Display |
ST7789 172×320 on SPI2 — MOSI GPIO6, SCLK GPIO7, CS GPIO14, DC GPIO15, RST GPIO21, BL GPIO22. Write-only, no MISO |
| Storage |
microSD on the same SPI2 bus, CS GPIO4, MISO GPIO5. SPI 1-bit mode only |
| LED |
one WS2812-family RGB LED on GPIO8 (a strapping pin — safe at factory eFuse settings) |
| Buttons |
BOOT GPIO9, pressed = low · RESET acts on CHIP_PU, not a GPIO |
| USB |
Type-C straight to the SoC's USB-Serial-JTAG (GPIO12/13). No bridge chip, no debug header — console, flashing and JTAG all share it |
| Free pins |
GPIO0, 1, 2, 3, 18, 19, 20, 23. GPIO0–3 are also the only ADC channels and the only deep-sleep wake pins left |
| Radio |
Wi-Fi 6 / BLE 5 / 802.15.4, onboard ceramic antenna, nothing to configure |
| Toolchain |
PlatformIO 6.1.19 + platform-espressif32 7.0.1 + ESP-IDF 6.0.1, board = esp32-c6-devkitc-1 |
Rules that prevent the expensive mistakes
Each of these produces a failure that looks like something else.
CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y in sdkconfig.defaults. Without it the
console defaults to UART0 on GPIO16/17, which is wired to nothing on this board:
the monitor is silent while the firmware runs perfectly, and you debug blind.
board_upload.flash_size = 4MB + CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y. There is
no PlatformIO board definition for this board, so it borrows
esp32-c6-devkitc-1, which claims 8 MB. Both lines are needed — they feed
different tools — or an oversized image links happily and fails to boot.
esp_lcd_panel_invert_color(panel, true). The IPS panel ships inverted.
Without it every colour is a photographic negative, which reads as "my RGB565
packing is broken" and sends you off debugging the wrong thing.
esp_lcd_panel_set_gap(panel, 34, 0). The 172-pixel glass is centred in the
ST7789's 240-column RAM: (240 − 172) / 2 = 34. Without it the image is shifted and
wraps at the edge.
.data_endian = LCD_RGB_DATA_ENDIAN_BIG in the panel config, and byte-swap
RGB565 once, at compile time in the colour macro. Swapping per pixel at runtime
costs more than the effect being drawn.
.miso_io_num = -1 for the panel. It is write-only. A project that also wants
the SD card must initialise the shared bus with miso_io_num = 5 instead.
- The backlight is PWM, never
gpio_set_level(). Waveshare's own warning: above
50 % duty for extended periods the panel overheats and develops permanent dark
shadows. Drive GPIO22 from an LEDC channel and clamp at 50 %. This is a
hardware-damage rule, not a style preference.
- The LCD and the TF card share MOSI (GPIO6) and SCLK (GPIO7) and are separated
only by chip select. Initialise SPI2 once; a second
spi_bus_initialize()
returns ESP_ERR_INVALID_STATE. Adding any SPI device means adding a third CS
from the free pins, not a second bus.
- 40 MHz is the pixel-clock ceiling and it is a wiring fact. The board wires
MOSI/SCLK onto the pads the IO MUX assigns to SCLK/MOSI, so SPI2 is routed 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. 172 × 320 × 2 B = 110,080 B at
40 MHz. If an animation looks slow this is the wall, not the CPU. See below.
- GPIO4–GPIO7 are gone, and they were the interesting ones. They carry all four
JTAG pads (so pad-JTAG is impossible — debug over the Type-C port),
ADC1_CH4-6,
and the fixed pins of LP UART and LP I2C (so neither LP peripheral is
available while the card slot and display exist).
- Never burn
EFUSE_UART_PRINT_CONTROL or EFUSE_JTAG_SEL_ENABLE. Both hand a
boot-time decision to a pin that already has a job here — the RGB LED's idle level
(GPIO8) and the LCD's D/C line (GPIO15). eFuse bits are one-time programmable.
- The WS2812 wire order is GRB, not RGB, and the LED is far brighter than its
numbers suggest — it sits under clear acrylic beside the panel. Divide by ~10.
- Deep-sleep is 7 µA for the SoC, not for the board. The LDO's quiescent draw,
the backlight driver and the LED's standby current all add on top. Do not quote
the datasheet figure for this board.
When the task is display performance
The bus, not the CPU, is the limit: 110,080 bytes at 40 MHz is 22.0 ms on the wire,
and gfx_present() blocks on the transfer-done semaphore so that nothing scribbles
over the framebuffer mid-DMA. That gives a hard ~45 fps for full-screen redraws.
Three moves, in order of payoff:
- Redraw less.
esp_lcd_panel_draw_bitmap() takes a rectangle. A 172×40 status
strip is 2.75 ms instead of 22 ms — an 8× win for the common case where only part
of the UI changed. Coordinates are (x_start, y_start, x_end, y_end) with the end
exclusive, and the source buffer's stride must equal the rectangle width, so
full-width bands are just an offset into the framebuffer.
- Double-buffer, if you have the RAM: another 110 KB out of ~320 KB. It overlaps
CPU and DMA but does not raise the 45 fps ceiling.
- Nothing else — do not go looking for a faster clock (rule 9).
Keep per-pixel loops integer-only. The RISC-V core here has no FPU; float belongs
in one-time init paths (palette and lookup-table generation), never in a frame loop.
When the task is memory
A full framebuffer is 110,080 B — about a third of the ~320 KB the linker actually
hands out. The template's full variant links at 123 KB. A Wi-Fi stack is another
~50 KB, a second framebuffer another 110 KB, LVGL more again. Framebuffer + Wi-Fi +
double-buffering does not fit — pick two.
Starting a new project
Do not hand-assemble one. template/ builds clean; scaffold from it:
~/.claude/skills/esp32c6-lcd147/template/variants/new-project.sh <target-dir> [--full|--minimal]
cd <target-dir> && pio run
--minimal — RGB LED sweep + console heartbeat, no display, no SPI bus. 167,888 B
flash, 11,052 B RAM. Flash this first on a new board: it proves the toolchain,
the flashing route and the console while the display cannot confuse the diagnosis,
and its two outputs (log line, LED) fail independently.
--full (default) — ST7789 effect carousel, 12×24 ASCII + Cyrillic font, LEDC
backlight, RGB LED accent. 245,214 B flash, 123,052 B RAM.
Both build as-is with ESP-IDF 6.0.1 (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.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 appears as CDC-ACM
(/dev/cu.usbmodem*, /dev/ttyACM*, or a COM port).
When firmware has wedged USB, or a console config change broke enumeration: hold
BOOT, tap RESET, release BOOT → Joint Download Boot. ROM code enumerates with no
valid application present, so bad firmware cannot brick this board. If it still
fails, esptool.py -p <port> erase_flash — a 4 MB erase takes 20–60 s, so it is
probably not hung.
There is no debug header. JTAG is the same Type-C port, via the built-in USB
Serial/JTAG controller — but not with the openocd PlatformIO installs for this board.
platform-espressif32's platform.json pins tool-openocd-esp32 to ~2.1100.0
(installed: 2.1100.20220706), and that build predates the ESP32-C6 entirely: it ships
board/esp32c3-builtin.cfg and board/esp32s3-builtin.cfg but no esp32c6-builtin.cfg
and no target/esp32c6.cfg anywhere in the package. pio debug / a bare
openocd -f board/esp32c6-builtin.cfg fails with "Can't find board/esp32c6-builtin.cfg"
on this toolchain, which reads like a typo rather than a missing chip target. Use a
current upstream openocd-esp32 release (Espressif's fork, not PlatformIO's pinned
copy) or an ESP-IDF export'd environment's own idf.py openocd, either of which does
carry ESP32-C6 support.
Reporting
Say what is verified on hardware and what is derived. In this skill the display
path (pin map, 40 MHz clock, gap 34, colour inversion, big-endian RGB565, the 45 fps
ceiling) ran on a real board. The LEDC backlight and the WS2812 driver are
compile-verified and derived from the vendor documents — the original firmware drove
the backlight as a plain GPIO and never touched the RGB LED. Recipes marked
"⚠︎ compile-checked only" in reference/recipes.md (TF card, ADC, BOOT button) are in
the same category. Everything marked ⚠︎ Inference in
reference/board-hardware.md is a conclusion combining the Waveshare wiki with the
Espressif datasheet, not a printed vendor statement — flag it as such rather than
presenting it as fact.
1---2name: esp32c6-lcd1473description: Waveshare ESP32-C6-LCD-1.474---56# Waveshare ESP32-C6-LCD-1.4778Board-specific firmware knowledge. Most failure modes on this board are silent — the9firmware runs, the panel lights up, and something is subtly wrong — so read the10reference file rather than guessing.1112- `reference/board-hardware.md` — the complete board reference: pin table, reset-time13 pin states, strapping analysis, free pins, peripheral availability, power budget,14 memory and partitions, the display's quirks (Part I), **plus** a development guide15 (Part II: §10 toolchain, §11 sdkconfig, §12 flashing, §13 peripheral cookbook,16 §14 symptom → cause → fix table).17- `reference/recipes.md` — copy-paste code: `platformio.ini`, `sdkconfig.defaults`,18 `board.h`, LCD bring-up, LEDC backlight, frame presentation, partial redraw, the19 WS2812 driver, TF card on the shared bus, ADC, the BOOT button, font generation.20- `reference/esp32c6-soc.md` — the ESP32-C6 silicon datasheet digest, for chip-level21 questions: IO MUX and LP IO tables, boot straps, memory, every peripheral's feature22 list, electrical and RF characteristics.23- `reference/esp32-family.md` — the rest of the family, for "should this be a different24 ESP32?" questions: what does and does not port between chips, radio and USB capability25 per chip, the RMT generation table (WS2812 under Wi-Fi load), deep-sleep memory and26 ULP/LP-core availability, and a chip-selection table.27- `template/` — a **project that builds clean**, in two variants, plus a scaffold28 script. See `template/README.md`.2930## Confirm the board first3132Waveshare also sells the **ESP32-C6-Touch-LCD-1.47** — same vendor, same 1.47" 172×32033panel, different board. If the screen responds to a finger, or there is no WS2812 next to34the panel, it is that one: a JD9853 controller instead of the ST7789, an 8 MB C6FH835instead of the 4 MB C6FH4, a 22-pin header instead of 18, and a pin map in which only36`LCD_CS`, `LCD_DC` and `TF_CS` match this board's. Use the `esp32c6-touch-lcd147` skill37and do not port pin numbers between them — the wrong map gives a dark screen, not an38error.3940## Orientation4142| | |43|---|---|44| SoC | ESP32-C6FH4, RISC-V RV32IMAC @ 160 MHz + LP core @ 20 MHz, **QFN32, 22 GPIOs** |45| Memory | **4 MB** in-package flash (1 MB default app partition), 512 KB HP SRAM (~320 KB linkable), 16 KB LP SRAM |46| Display | **ST7789 172×320** on **SPI2** — MOSI **GPIO6**, SCLK **GPIO7**, CS GPIO14, DC GPIO15, RST GPIO21, BL GPIO22. Write-only, no MISO |47| Storage | microSD on the **same** SPI2 bus, CS **GPIO4**, MISO **GPIO5**. SPI 1-bit mode only |48| LED | one WS2812-family RGB LED on **GPIO8** (a strapping pin — safe at factory eFuse settings) |49| Buttons | **BOOT** GPIO9, **pressed = low** · **RESET** acts on `CHIP_PU`, not a GPIO |50| USB | Type-C straight to the SoC's USB-Serial-JTAG (GPIO12/13). **No bridge chip, no debug header** — console, flashing and JTAG all share it |51| Free pins | GPIO0, 1, 2, 3, 18, 19, 20, 23. GPIO0–3 are also the only ADC channels and the only deep-sleep wake pins left |52| Radio | Wi-Fi 6 / BLE 5 / 802.15.4, onboard ceramic antenna, nothing to configure |53| Toolchain | PlatformIO 6.1.19 + `platform-espressif32` 7.0.1 + ESP-IDF 6.0.1, `board = esp32-c6-devkitc-1` |5455## Rules that prevent the expensive mistakes5657Each of these produces a failure that looks like something else.58591. **`CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y`** in `sdkconfig.defaults`. Without it the60 console defaults to UART0 on GPIO16/17, which is wired to nothing on this board:61 the monitor is silent while the firmware runs perfectly, and you debug blind.622. **`board_upload.flash_size = 4MB` + `CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y`.** There is63 no PlatformIO board definition for this board, so it borrows64 `esp32-c6-devkitc-1`, which claims 8 MB. Both lines are needed — they feed65 different tools — or an oversized image links happily and fails to boot.663. **`esp_lcd_panel_invert_color(panel, true)`.** The IPS panel ships inverted.67 Without it every colour is a photographic negative, which reads as "my RGB56568 packing is broken" and sends you off debugging the wrong thing.694. **`esp_lcd_panel_set_gap(panel, 34, 0)`.** The 172-pixel glass is centred in the70 ST7789's 240-column RAM: (240 − 172) / 2 = 34. Without it the image is shifted and71 wraps at the edge.725. **`.data_endian = LCD_RGB_DATA_ENDIAN_BIG`** in the panel config, and byte-swap73 RGB565 **once, at compile time** in the colour macro. Swapping per pixel at runtime74 costs more than the effect being drawn.756. **`.miso_io_num = -1`** for the panel. It is write-only. A project that also wants76 the SD card must initialise the shared bus with `miso_io_num = 5` instead.777. **The backlight is PWM, never `gpio_set_level()`.** Waveshare's own warning: above78 50 % duty for extended periods the panel overheats and develops **permanent dark79 shadows**. Drive GPIO22 from an LEDC channel and clamp at 50 %. This is a80 hardware-damage rule, not a style preference.818. **The LCD and the TF card share MOSI (GPIO6) and SCLK (GPIO7)** and are separated82 only by chip select. Initialise SPI2 **once**; a second `spi_bus_initialize()`83 returns `ESP_ERR_INVALID_STATE`. Adding any SPI device means adding a third CS84 from the free pins, not a second bus.859. **40 MHz is the pixel-clock ceiling and it is a wiring fact.** The board wires86 MOSI/SCLK onto the pads the IO MUX assigns to SCLK/MOSI, so SPI2 is routed through87 the GPIO Matrix at roughly half the 80 MHz IO-MUX ceiling. No software change88 recovers it.8910. **A full-screen flush is 22 ms → ~45 fps, hard.** 172 × 320 × 2 B = 110,080 B at90 40 MHz. If an animation looks slow this is the wall, not the CPU. See below.9111. **GPIO4–GPIO7 are gone, and they were the interesting ones.** They carry all four92 JTAG pads (so pad-JTAG is impossible — debug over the Type-C port), `ADC1_CH4-6`,93 and the fixed pins of **LP UART and LP I2C** (so neither LP peripheral is94 available while the card slot and display exist).9512. **Never burn `EFUSE_UART_PRINT_CONTROL` or `EFUSE_JTAG_SEL_ENABLE`.** Both hand a96 boot-time decision to a pin that already has a job here — the RGB LED's idle level97 (GPIO8) and the LCD's D/C line (GPIO15). eFuse bits are one-time programmable.9813. **The WS2812 wire order is GRB**, not RGB, and the LED is far brighter than its99 numbers suggest — it sits under clear acrylic beside the panel. Divide by ~10.10014. **Deep-sleep is 7 µA for the SoC, not for the board.** The LDO's quiescent draw,101 the backlight driver and the LED's standby current all add on top. Do not quote102 the datasheet figure for this board.103104## When the task is display performance105106The bus, not the CPU, is the limit: 110,080 bytes at 40 MHz is 22.0 ms on the wire,107and `gfx_present()` blocks on the transfer-done semaphore so that nothing scribbles108over the framebuffer mid-DMA. That gives a hard ~45 fps for full-screen redraws.109110Three moves, in order of payoff:1111121. **Redraw less.** `esp_lcd_panel_draw_bitmap()` takes a rectangle. A 172×40 status113 strip is 2.75 ms instead of 22 ms — an 8× win for the common case where only part114 of the UI changed. Coordinates are `(x_start, y_start, x_end, y_end)` with the end115 exclusive, and the source buffer's stride must equal the rectangle width, so116 full-width bands are just an offset into the framebuffer.1172. **Double-buffer**, if you have the RAM: another 110 KB out of ~320 KB. It overlaps118 CPU and DMA but does **not** raise the 45 fps ceiling.1193. Nothing else — do not go looking for a faster clock (rule 9).120121Keep per-pixel loops integer-only. The RISC-V core here has **no FPU**; float belongs122in one-time init paths (palette and lookup-table generation), never in a frame loop.123124## When the task is memory125126A full framebuffer is 110,080 B — about a third of the ~320 KB the linker actually127hands out. The template's full variant links at 123 KB. A Wi-Fi stack is another128~50 KB, a second framebuffer another 110 KB, LVGL more again. **Framebuffer + Wi-Fi +129double-buffering does not fit — pick two.**130131## Starting a new project132133Do not hand-assemble one. `template/` builds clean; scaffold from it:134135```sh136~/.claude/skills/esp32c6-lcd147/template/variants/new-project.sh <target-dir> [--full|--minimal]137cd <target-dir> && pio run138```139140- `--minimal` — RGB LED sweep + console heartbeat, no display, no SPI bus. 167,888 B141 flash, 11,052 B RAM. **Flash this first on a new board**: it proves the toolchain,142 the flashing route and the console while the display cannot confuse the diagnosis,143 and its two outputs (log line, LED) fail independently.144- `--full` (default) — ST7789 effect carousel, 12×24 ASCII + Cyrillic font, LEDC145 backlight, RGB LED accent. 245,214 B flash, 123,052 B RAM.146147Both build as-is with ESP-IDF 6.0.1 (verified). Nothing is generated and no paths are148embedded, so copying the tree by hand works identically. `template/README.md` maps149files to subsystems so a `--full` scaffold can be stripped back cleanly.150151When the user already has a project, prefer bringing it in line with the template's152`platformio.ini`, `sdkconfig.defaults` and `board.h` over rewriting their code.153154## Flashing155156Nothing to press:157158```sh159pio run -t upload -t monitor # or: idf.py -p <port> flash monitor160```161162The USB Serial/JTAG controller supports host-controlled reset and download-mode entry,163so esptool drives the whole cycle over the Type-C cable. The board appears as CDC-ACM164(`/dev/cu.usbmodem*`, `/dev/ttyACM*`, or a COM port).165166When firmware has wedged USB, or a console config change broke enumeration: **hold167BOOT, tap RESET, release BOOT** → Joint Download Boot. ROM code enumerates with no168valid application present, so **bad firmware cannot brick this board**. If it still169fails, `esptool.py -p <port> erase_flash` — a 4 MB erase takes 20–60 s, so it is170probably not hung.171172There is no debug header. JTAG is the same Type-C port, via the built-in USB173Serial/JTAG controller — but not with the openocd PlatformIO installs for this board.174`platform-espressif32`'s `platform.json` pins `tool-openocd-esp32` to `~2.1100.0`175(installed: `2.1100.20220706`), and that build predates the ESP32-C6 entirely: it ships176`board/esp32c3-builtin.cfg` and `board/esp32s3-builtin.cfg` but no `esp32c6-builtin.cfg`177and no `target/esp32c6.cfg` anywhere in the package. `pio debug` / a bare178`openocd -f board/esp32c6-builtin.cfg` fails with "Can't find board/esp32c6-builtin.cfg"179on this toolchain, which reads like a typo rather than a missing chip target. Use a180current upstream `openocd-esp32` release (Espressif's fork, not PlatformIO's pinned181copy) or an ESP-IDF export'd environment's own `idf.py openocd`, either of which does182carry ESP32-C6 support.183184## Reporting185186Say what is verified on hardware and what is derived. In this skill the **display187path** (pin map, 40 MHz clock, gap 34, colour inversion, big-endian RGB565, the 45 fps188ceiling) ran on a real board. The **LEDC backlight** and the **WS2812 driver** are189compile-verified and derived from the vendor documents — the original firmware drove190the backlight as a plain GPIO and never touched the RGB LED. Recipes marked191"⚠︎ compile-checked only" in `reference/recipes.md` (TF card, ADC, BOOT button) are in192the same category. Everything marked **⚠︎ Inference** in193`reference/board-hardware.md` is a conclusion combining the Waveshare wiki with the194Espressif datasheet, not a printed vendor statement — flag it as such rather than195presenting it as fact.