@typecad/framework-zephyr 1.0.0-alpha.11 → 1.0.0-alpha.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,14 +2,23 @@
2
2
  // Zephyr UI display adapter for the Cuttlefish UI rendering pipeline.
3
3
  //
4
4
  // Bridges the in-tree CuttlefishGFX/CuttlefishCanvas16 class (emitted by the
5
- // runtime header's cuttlefish-gfx slice) to the ST7796S panel. The panel is
5
+ // runtime header's cuttlefish-gfx slice) to an SPI TFT panel. The panel is
6
6
  // driven DIRECTLY over the SPI controller with GPIO chip-select, DC and reset
7
7
  // pins — NOT through Zephyr's mipi-dbi-spi bridge: that bridge issues separate
8
8
  // SPI transactions for the command byte and its parameters (CS deasserts
9
- // between them), which scrambles this panel's command decoder and leaves it
10
- // white. Verified on hardware: the direct protocol (CS held low across the
11
- // command+data burst, DC toggled mid-burst — the Adafruit ST77xx protocol)
12
- // initializes the panel and renders pixels correctly.
9
+ // between them), which scrambles this panel family's command decoder and
10
+ // leaves it white. Verified on hardware (ST7796S): the direct protocol (CS
11
+ // held low across the command+data burst, DC toggled mid-burst — the Adafruit
12
+ // ST77xx protocol) initializes the panel and renders pixels correctly.
13
+ //
14
+ // Per-controller support lives in two places, keyed off the display profile's
15
+ // `controller` field (see profiles.ts):
16
+ // - the init command table (byte-for-byte Adafruit sequences)
17
+ // - the pixel wire format: ST7796S is driven in 18-bit (666) mode (its
18
+ // 16-bit channel routing is crossed on the verified clone panel), ILI9341
19
+ // in native 16-bit (565) big-endian. The ST7796S path below is frozen
20
+ // exactly as hardware-verified; the ILI9341 path mirrors it with the
21
+ // Adafruit ILI9341 init + 565 packing and is not yet hardware-tuned.
13
22
  //
14
23
  // This is the Zephyr analog of the Adafruit adapters in framework-arduino,
15
24
  // using the in-tree native GFX class (no #define CuttlefishCanvas16) driven
@@ -24,7 +33,7 @@
24
33
  // ---------------------------------------------------------------------------
25
34
 
26
35
  import type { DisplayAdapterCode, DisplayAdapterGenerator } from "@typecad/cuttlefish/api/shared";
27
- import { ZEPHYR_DISPLAY_PROFILES } from "./profiles.js";
36
+ import { ZEPHYR_DISPLAY_PROFILES, panelControllerFor } from "./profiles.js";
28
37
  import type { ZephyrDisplayProfile } from "./profiles.js";
29
38
 
30
39
  /**
@@ -48,6 +57,18 @@ export function zephyrUiDisplayAdapter(
48
57
  const maxDim = Math.max(w, h);
49
58
  const dtLabel = profile.dtLabel;
50
59
  const backlightAlias = profile.backlight;
60
+ const bus = profile.busLabel ?? 'spi2';
61
+ const bridge = profile.bridgeLabel ?? 'mipi_dbi';
62
+ const controller = panelControllerFor(profile);
63
+ const isIli9341 = controller === 'ili9341';
64
+ // Bytes per pixel on the wire: 3 (18-bit 666) for ST7796S, 2 (16-bit 565)
65
+ // for ILI9341. The row/block scratch buffers and pack loops key off this.
66
+ const bpp = isIli9341 ? 2 : 3;
67
+ const rowBuf = `__tc_display_row${bpp}`;
68
+ const blockBuf = `__tc_display_block${bpp}`;
69
+ const packFn = isIli9341 ? '__tc_pnl_pack565' : '__tc_pnl_pack666';
70
+ const pixelsFn = isIli9341 ? '__tc_pnl_pixels565' : '__tc_pnl_pixels666';
71
+ const wireTag = isIli9341 ? '16-bit 565' : '18-bit';
51
72
  // Never infer readback from a board's default pinmux. SDO/MISO may be left
52
73
  // floating or shared with another device, and ST7796S modules are known to
53
74
  // react badly to GSCAN reads. Both an explicit opt-in and an explicit MISO
@@ -65,6 +86,41 @@ export function zephyrUiDisplayAdapter(
65
86
  `#include <zephyr/drivers/gpio.h>`,
66
87
  ].join("\n");
67
88
 
89
+ // Per-controller scratch-buffer declarations. The ST7796S text is frozen as
90
+ // hardware-verified; the ILI9341 variant documents its 565 wire format.
91
+ const bufferDecls = isIli9341
92
+ ? [
93
+ `// One-row scratch buffer in 16-bit (565) wire format: maxDim px x 2 bytes.`,
94
+ `// Reused across fillRect/draw calls — never per-frame (AGENTS.md: no`,
95
+ `// per-frame heap allocation). The ILI9341 is driven in its native`,
96
+ `// 16-bit COLMOD (0x55): RGB565 pixels go out big-endian (MSB-first SPI),`,
97
+ `// the Adafruit ILI9341 convention, with no repacking needed.`,
98
+ `static uint8_t ${rowBuf}[${maxDim} * 2];`,
99
+ `// Multi-row block buffer for solid fills (8 rows of maxDim px in 565).`,
100
+ `// fillRect fills this once with the color, then sends the whole rect in a`,
101
+ `// few large spi_write chunks instead of one spi_write per row — a full`,
102
+ `// 480x320 clear went from ~320 syscalls (~110ms) to ~40 (~15ms). Reused,`,
103
+ `// not per-frame (AGENTS.md).`,
104
+ `#define __TC_FILL_ROWS 8`,
105
+ `static uint8_t ${blockBuf}[${maxDim} * 2 * __TC_FILL_ROWS];`,
106
+ ]
107
+ : [
108
+ `// One-row scratch buffer in 18-bit (666) wire format: maxDim px x 3 bytes.`,
109
+ `// Reused across fillRect/draw calls — never per-frame (AGENTS.md: no`,
110
+ `// per-frame heap allocation). The panel is driven in 18-bit mode (COLMOD`,
111
+ `// 0x66): its 16-bit (565) channel routing is crossed (G/B swap, verified`,
112
+ `// with calibration bands), while 18-bit mode routes every channel`,
113
+ `// correctly with plain (R,G,B) byte order.`,
114
+ `static uint8_t ${rowBuf}[${maxDim} * 3];`,
115
+ `// Multi-row block buffer for solid fills (8 rows of maxDim px in 18-bit).`,
116
+ `// fillRect fills this once with the color, then sends the whole rect in a`,
117
+ `// few large spi_write chunks instead of one spi_write per row — a full`,
118
+ `// 480x320 clear went from ~320 syscalls (~110ms) to ~40 (~15ms). Reused,`,
119
+ `// not per-frame (AGENTS.md).`,
120
+ `#define __TC_FILL_ROWS 8`,
121
+ `static uint8_t ${blockBuf}[${maxDim} * 3 * __TC_FILL_ROWS];`,
122
+ ];
123
+
68
124
  const declaration = [
69
125
  `// CUTTLEFISH_DISPLAY_BEGIN`,
70
126
  // The display0 DT node carries frequency/dimensions for DT_PROP reads, but
@@ -73,20 +129,7 @@ export function zephyrUiDisplayAdapter(
73
129
  // allocates a tearing-effect GPIO interrupt that conflicts with the SPI/I2C
74
130
  // interrupts — the VECDESC_FL_SHARED assertion crash on the 3rd frame).
75
131
  `#define __tc_display_dev 1`,
76
- `// One-row scratch buffer in 18-bit (666) wire format: maxDim px x 3 bytes.`,
77
- `// Reused across fillRect/draw calls — never per-frame (AGENTS.md: no`,
78
- `// per-frame heap allocation). The panel is driven in 18-bit mode (COLMOD`,
79
- `// 0x66): its 16-bit (565) channel routing is crossed (G/B swap, verified`,
80
- `// with calibration bands), while 18-bit mode routes every channel`,
81
- `// correctly with plain (R,G,B) byte order.`,
82
- `static uint8_t __tc_display_row3[${maxDim} * 3];`,
83
- `// Multi-row block buffer for solid fills (8 rows of maxDim px in 18-bit).`,
84
- `// fillRect fills this once with the color, then sends the whole rect in a`,
85
- `// few large spi_write chunks instead of one spi_write per row — a full`,
86
- `// 480x320 clear went from ~320 syscalls (~110ms) to ~40 (~15ms). Reused,`,
87
- `// not per-frame (AGENTS.md).`,
88
- `#define __TC_FILL_ROWS 8`,
89
- `static uint8_t __tc_display_block3[${maxDim} * 3 * __TC_FILL_ROWS];`,
132
+ ...bufferDecls,
90
133
  `// startWrite/endWrite batching depth. When > 0 the panel CS is held asserted`,
91
134
  `// (low) across multiple primitives — DC still toggles mid-burst, but CS does`,
92
135
  `// not, matching the Adafruit ST77xx protocol and avoiding one full CS-toggle`,
@@ -118,6 +161,233 @@ export function zephyrUiDisplayAdapter(
118
161
  ? `#if DT_HAS_ALIAS(${backlightAlias})\n const struct gpio_dt_spec __bl = GPIO_DT_SPEC_GET(DT_ALIAS(${backlightAlias}), gpios);\n if (device_is_ready(__bl.port)) { gpio_pin_configure_dt(&__bl, GPIO_OUTPUT_ACTIVE); }\n#endif`
119
162
  : '';
120
163
 
164
+ // Per-controller pixel pack/stream helpers. Both share the row-chunked
165
+ // transport; only the per-pixel wire encoding differs.
166
+ const pixelHelpers = isIli9341
167
+ ? `
168
+ // Pack count rgb565 pixels into the row2 scratch buffer as big-endian 16-bit
169
+ // wire bytes (c>>8, c&0xFF) — the ILI9341's native 565 format under COLMOD
170
+ // 0x55. The caller then streams the buffer with the 8-bit config (one row at
171
+ // a time; the buffer holds maxDim pixels).
172
+ static void ${packFn}(const uint16_t* px, uint32_t count) {
173
+ for (uint32_t i = 0; i < count; i++) {
174
+ uint16_t c = px[i];
175
+ ${rowBuf}[i * 2] = static_cast<uint8_t>(c >> 8);
176
+ ${rowBuf}[i * 2 + 1] = static_cast<uint8_t>(c);
177
+ }
178
+ }
179
+
180
+ // Stream count rgb565 pixels to the panel (chunked through the row2 scratch).
181
+ // Caller holds the RAMWR burst.
182
+ static void ${pixelsFn}(const uint16_t* px, uint32_t count) {
183
+ while (count > 0) {
184
+ uint32_t __chunk = (count > ${maxDim}) ? ${maxDim} : count;
185
+ ${packFn}(px, __chunk);
186
+ struct spi_buf __bd = { ${rowBuf}, static_cast<size_t>(__chunk) * 2U };
187
+ struct spi_buf_set __sd = { &__bd, 1 };
188
+ (void)spi_write(DEVICE_DT_GET(DT_NODELABEL(${bus})), &__tc_pnl_cfg8, &__sd);
189
+ px += __chunk;
190
+ count -= __chunk;
191
+ }
192
+ }
193
+ `
194
+ : `
195
+ // Pack count rgb565 pixels into the row3 scratch buffer as 18-bit (666) wire
196
+ // format: (r<<3, g<<2, b<<3) — R,G,B byte order, verified correct on this
197
+ // panel in 18-bit mode. The caller then streams the buffer with the 8-bit
198
+ // config (one row at a time; the buffer holds maxDim pixels).
199
+ static void ${packFn}(const uint16_t* px, uint32_t count) {
200
+ for (uint32_t i = 0; i < count; i++) {
201
+ uint16_t c = px[i];
202
+ ${rowBuf}[i * 3] = static_cast<uint8_t>((c >> 8) & 0xF8u);
203
+ ${rowBuf}[i * 3 + 1] = static_cast<uint8_t>((c >> 3) & 0xFCu);
204
+ ${rowBuf}[i * 3 + 2] = static_cast<uint8_t>((c << 3) & 0xF8u);
205
+ }
206
+ }
207
+
208
+ // Stream count rgb565 pixels to the panel (converted to 18-bit, chunked
209
+ // through the row3 scratch). Caller holds the RAMWR burst.
210
+ static void ${pixelsFn}(const uint16_t* px, uint32_t count) {
211
+ while (count > 0) {
212
+ uint32_t __chunk = (count > ${maxDim}) ? ${maxDim} : count;
213
+ ${packFn}(px, __chunk);
214
+ struct spi_buf __bd = { ${rowBuf}, static_cast<size_t>(__chunk) * 3U };
215
+ struct spi_buf_set __sd = { &__bd, 1 };
216
+ (void)spi_write(DEVICE_DT_GET(DT_NODELABEL(${bus})), &__tc_pnl_cfg8, &__sd);
217
+ px += __chunk;
218
+ count -= __chunk;
219
+ }
220
+ }
221
+ `;
222
+
223
+ // Per-controller solid-fill. Both tile one built row into the block buffer
224
+ // and stream it in multi-row chunks; only the color encoding differs.
225
+ const fillRectFn = isIli9341
226
+ ? `
227
+ static void __tc_op_fillRect(void* /*ctx*/, int16_t x, int16_t y, int16_t rw, int16_t rh, uint16_t c) {
228
+ if (rw <= 0 || rh <= 0) return;
229
+ uint8_t __b0 = static_cast<uint8_t>(c >> 8);
230
+ uint8_t __b1 = static_cast<uint8_t>(c);
231
+ // Build one row, then tile it into the block buffer.
232
+ for (int16_t i = 0; i < rw; i++) {
233
+ ${rowBuf}[i * 2] = __b0;
234
+ ${rowBuf}[i * 2 + 1] = __b1;
235
+ }
236
+ size_t rowBytes = static_cast<size_t>(rw) * 2U;
237
+ for (int16_t r = 0; r < __TC_FILL_ROWS; r++) {
238
+ memcpy(&${blockBuf}[static_cast<size_t>(r) * rowBytes], ${rowBuf}, rowBytes);
239
+ }
240
+ __tc_pnl_set_window(x, y, rw, rh);
241
+ __tc_pnl_ramwr_begin();
242
+ int16_t remaining = rh;
243
+ while (remaining > 0) {
244
+ int16_t chunk = (remaining > __TC_FILL_ROWS) ? __TC_FILL_ROWS : remaining;
245
+ struct spi_buf __bd = { ${blockBuf}, static_cast<size_t>(chunk) * rowBytes };
246
+ struct spi_buf_set __sd = { &__bd, 1 };
247
+ (void)spi_write(DEVICE_DT_GET(DT_NODELABEL(${bus})), &__tc_pnl_cfg8, &__sd);
248
+ remaining -= chunk;
249
+ }
250
+ __tc_pnl_ramwr_end();
251
+ }
252
+ `
253
+ : `
254
+ // Fill a rect row-by-row using the one-row 18-bit scratch buffer. This is the
255
+ // hot path for background clears and large fills; building the color into the
256
+ // reused buffer and writing each row keeps memory bounded.
257
+ // Fill a rect with a solid color. The 18-bit row is tiled into the block buffer
258
+ // (__TC_FILL_ROWS rows), then the whole rect is sent in multi-row spi_write
259
+ // chunks. A full 480x320 clear is ~40 writes instead of ~320, dropping it from
260
+ // ~110ms to ~15ms — the ESP32 SPI driver's per-transaction overhead (not SPI
261
+ // bandwidth) is the binding cost, so fewer/larger writes win. Scatter-gather
262
+ // descriptor lists tested slower (the driver walks each descriptor), so this
263
+ // uses one contiguous buffer per write.
264
+ static void __tc_op_fillRect(void* /*ctx*/, int16_t x, int16_t y, int16_t rw, int16_t rh, uint16_t c) {
265
+ if (rw <= 0 || rh <= 0) return;
266
+ uint8_t __b0 = static_cast<uint8_t>((c >> 8) & 0xF8u);
267
+ uint8_t __b1 = static_cast<uint8_t>((c >> 3) & 0xFCu);
268
+ uint8_t __b2 = static_cast<uint8_t>((c << 3) & 0xF8u);
269
+ // Build one 18-bit row, then tile it into the block buffer.
270
+ for (int16_t i = 0; i < rw; i++) {
271
+ ${rowBuf}[i * 3] = __b0;
272
+ ${rowBuf}[i * 3 + 1] = __b1;
273
+ ${rowBuf}[i * 3 + 2] = __b2;
274
+ }
275
+ size_t rowBytes = static_cast<size_t>(rw) * 3U;
276
+ for (int16_t r = 0; r < __TC_FILL_ROWS; r++) {
277
+ memcpy(&${blockBuf}[static_cast<size_t>(r) * rowBytes], ${rowBuf}, rowBytes);
278
+ }
279
+ __tc_pnl_set_window(x, y, rw, rh);
280
+ __tc_pnl_ramwr_begin();
281
+ int16_t remaining = rh;
282
+ while (remaining > 0) {
283
+ int16_t chunk = (remaining > __TC_FILL_ROWS) ? __TC_FILL_ROWS : remaining;
284
+ struct spi_buf __bd = { ${blockBuf}, static_cast<size_t>(chunk) * rowBytes };
285
+ struct spi_buf_set __sd = { &__bd, 1 };
286
+ (void)spi_write(DEVICE_DT_GET(DT_NODELABEL(${bus})), &__tc_pnl_cfg8, &__sd);
287
+ remaining -= chunk;
288
+ }
289
+ __tc_pnl_ramwr_end();
290
+ }
291
+ `;
292
+
293
+ // Per-controller init table. Both are byte-for-byte Adafruit sequences
294
+ // emitted as (cmd, len, data, delay) records walked in display_init.
295
+ const initBlock = isIli9341
296
+ ? `
297
+ // ── Panel init ──────────────────────────────────────────────────────────
298
+ // Adafruit ILI9341 init sequence (initILI9341), byte for byte: SWRESET,
299
+ // manufacturer/power/gamma registers, MADCTL 0x28 (MV landscape + BGR, the
300
+ // same convention as the ST7796S path), COLMOD 0x55 (16-bit RGB565 — the
301
+ // native wire format of the pack565 transport), SLPOUT (150ms), DISPON
302
+ // (150ms), INVON. INVON is included because the common ILI9341 SPI modules
303
+ // (2.2"/2.4" TFTs) ship with an inverted panel; the in-tree binding's
304
+ // display-inversion property mirrors this for the stock driver.
305
+ struct __tc_pnl_init_cmd { uint8_t cmd; uint8_t len; const uint8_t* data; uint16_t delay_ms; };
306
+ static const uint8_t __tc_pnl_i1[] = {0x03, 0x80, 0x02};
307
+ static const uint8_t __tc_pnl_i2[] = {0x00, 0xC1, 0x30};
308
+ static const uint8_t __tc_pnl_i3[] = {0x64, 0x03, 0x12, 0x81};
309
+ static const uint8_t __tc_pnl_i4[] = {0x85, 0x00, 0x78};
310
+ static const uint8_t __tc_pnl_i5[] = {0x39, 0x2C, 0x00, 0x34, 0x02};
311
+ static const uint8_t __tc_pnl_i6[] = {0x20};
312
+ static const uint8_t __tc_pnl_i7[] = {0x00, 0x00};
313
+ static const uint8_t __tc_pnl_i8[] = {0x23};
314
+ static const uint8_t __tc_pnl_i9[] = {0x10};
315
+ static const uint8_t __tc_pnl_i10[] = {0x3E, 0x28};
316
+ static const uint8_t __tc_pnl_i11[] = {0x86};
317
+ static const uint8_t __tc_pnl_i12[] = {0x28};
318
+ static const uint8_t __tc_pnl_i13[] = {0x55};
319
+ static const uint8_t __tc_pnl_i14[] = {0x00, 0x18};
320
+ static const uint8_t __tc_pnl_i15[] = {0x08, 0x82, 0x27};
321
+ static const uint8_t __tc_pnl_i16[] = {0x00};
322
+ static const uint8_t __tc_pnl_i17[] = {0x01};
323
+ static const uint8_t __tc_pnl_i18[] = {0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, 0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03, 0x0E, 0x09, 0x00};
324
+ static const uint8_t __tc_pnl_i19[] = {0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, 0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C, 0x31, 0x36, 0x0F};
325
+ static const struct __tc_pnl_init_cmd __tc_pnl_init_seq[] = {
326
+ {0x01, 0, NULL, 150}, // SWRESET
327
+ {0xEF, 3, __tc_pnl_i1, 0},
328
+ {0xCF, 3, __tc_pnl_i2, 0},
329
+ {0xED, 4, __tc_pnl_i3, 0},
330
+ {0xE8, 3, __tc_pnl_i4, 0},
331
+ {0xCB, 5, __tc_pnl_i5, 0},
332
+ {0xF7, 1, __tc_pnl_i6, 0},
333
+ {0xEA, 2, __tc_pnl_i7, 0},
334
+ {0xC0, 1, __tc_pnl_i8, 0}, // PWCTRL1
335
+ {0xC1, 1, __tc_pnl_i9, 0}, // PWCTRL2
336
+ {0xC5, 2, __tc_pnl_i10, 0}, // VMCTRL1
337
+ {0xC7, 1, __tc_pnl_i11, 0}, // VMCTRL2
338
+ {0x36, 1, __tc_pnl_i12, 0}, // MADCTL 0x28: MV (landscape) + BGR=1
339
+ {0x3A, 1, __tc_pnl_i13, 0}, // COLMOD 0x55 (16-bit RGB565)
340
+ {0xB1, 2, __tc_pnl_i14, 0}, // FRMCTR1
341
+ {0xB6, 3, __tc_pnl_i15, 0}, // DISCTRL
342
+ {0xF2, 1, __tc_pnl_i16, 0}, // ENABLE3G off
343
+ {0x26, 1, __tc_pnl_i17, 0}, // GAMSET gamma curve 1
344
+ {0xE0, 15, __tc_pnl_i18, 0}, // PGAMCTRL
345
+ {0xE1, 15, __tc_pnl_i19, 0}, // NGAMCTRL
346
+ {0x11, 0, NULL, 150}, // SLPOUT (sleep out — 120ms typical)
347
+ {0x29, 0, NULL, 150}, // DISPON
348
+ {0x21, 0, NULL, 0}, // INVON (common ILI9341 modules ship inverted)
349
+ };
350
+ `
351
+ : `
352
+ // ── Panel init ──────────────────────────────────────────────────────────
353
+ // Adafruit ST7796S init sequence (demo-st lib fork), byte for byte: hw reset
354
+ // pulse, SWRESET, manufacturer unlock, VCOM/MADCTL/COLMOD/porch registers,
355
+ // lock, SLPOUT (150ms), DISPON (150ms), INVOFF. MADCTL 0x28 = MV (rotation 1
356
+ // landscape) + BGR=1. BGR=1 makes the controller route data R/B to the
357
+ // B/R subpixels (verified: red data shows blue with BGR=1), which combined
358
+ // with a lossless R/B data swap renders the UI correctly; BGR=0 leaves a
359
+ // half-lossy G/B quirk on this clone controller.
360
+ struct __tc_pnl_init_cmd { uint8_t cmd; uint8_t len; const uint8_t* data; uint16_t delay_ms; };
361
+ static const uint8_t __tc_pnl_i1[] = {0xC3};
362
+ static const uint8_t __tc_pnl_i2[] = {0x96};
363
+ static const uint8_t __tc_pnl_i3[] = {0x1C};
364
+ static const uint8_t __tc_pnl_i4[] = {0x28};
365
+ static const uint8_t __tc_pnl_i5[] = {0x66};
366
+ static const uint8_t __tc_pnl_i6[] = {0x80};
367
+ static const uint8_t __tc_pnl_i7[] = {0x00};
368
+ static const uint8_t __tc_pnl_i8[] = {0x80, 0x02, 0x3B};
369
+ static const uint8_t __tc_pnl_i9[] = {0xC6};
370
+ static const uint8_t __tc_pnl_i10[] = {0x69};
371
+ static const uint8_t __tc_pnl_i11[] = {0x3C};
372
+ static const struct __tc_pnl_init_cmd __tc_pnl_init_seq[] = {
373
+ {0x01, 0, NULL, 150}, // SWRESET
374
+ {0xF0, 1, __tc_pnl_i1, 0}, // unlock manufacturer
375
+ {0xF0, 1, __tc_pnl_i2, 0},
376
+ {0xC5, 1, __tc_pnl_i3, 0}, // VCOM control
377
+ {0x36, 1, __tc_pnl_i4, 0}, // MADCTL 0x28: MV (rotation 1) + BGR=1
378
+ {0x3A, 1, __tc_pnl_i5, 0}, // COLMOD 0x66 (18-bit, 262K) — clean channel routing
379
+ {0xB0, 1, __tc_pnl_i6, 0}, // interface control
380
+ {0xB4, 1, __tc_pnl_i7, 0}, // inversion control
381
+ {0xB6, 3, __tc_pnl_i8, 0}, // display function control
382
+ {0xB7, 1, __tc_pnl_i9, 0}, // entry mode
383
+ {0xF0, 1, __tc_pnl_i10, 0}, // lock manufacturer
384
+ {0xF0, 1, __tc_pnl_i11, 0},
385
+ {0x11, 0, NULL, 150}, // SLPOUT (sleep out — 120ms typical)
386
+ {0x29, 0, NULL, 150}, // DISPON
387
+ {0x20, 0, NULL, 0}, // INVOFF (non-inverted at power-on)
388
+ };
389
+ `;
390
+
121
391
  const functions = `
122
392
  // ── Direct panel transport ──────────────────────────────────────────────
123
393
  // GPIOs: CS/DC/RST driven manually; CS stays LOW for the whole command+data
@@ -125,11 +395,11 @@ export function zephyrUiDisplayAdapter(
125
395
  // Adafruit ST77xx protocol this panel requires. The SPI config carries no CS
126
396
  // (cs_is_gpio = false -> the ESP32 driver's hardware CSEL pin is left idle;
127
397
  // it is not connected to the panel).
128
- static const struct gpio_dt_spec __tc_pnl_cs = GPIO_DT_SPEC_GET(DT_NODELABEL(spi2), cs_gpios);
129
- static const struct gpio_dt_spec __tc_pnl_dc = GPIO_DT_SPEC_GET(DT_NODELABEL(mipi_dbi), dc_gpios);
130
- static const struct gpio_dt_spec __tc_pnl_rst = GPIO_DT_SPEC_GET(DT_NODELABEL(mipi_dbi), reset_gpios);
398
+ static const struct gpio_dt_spec __tc_pnl_cs = GPIO_DT_SPEC_GET(DT_NODELABEL(${bus}), cs_gpios);
399
+ static const struct gpio_dt_spec __tc_pnl_dc = GPIO_DT_SPEC_GET(DT_NODELABEL(${bridge}), dc_gpios);
400
+ static const struct gpio_dt_spec __tc_pnl_rst = GPIO_DT_SPEC_GET(DT_NODELABEL(${bridge}), reset_gpios);
131
401
 
132
- // 8-bit frames for commands/parameters and 18-bit (3 bytes/pixel) pixel data.
402
+ // 8-bit frames for commands/parameters and ${isIli9341 ? '16-bit (2 bytes/pixel)' : '18-bit (3 bytes/pixel)'} pixel data.
133
403
  static struct spi_config __tc_pnl_cfg8 = {
134
404
  .frequency = DT_PROP(DT_NODELABEL(${dtLabel}), mipi_max_frequency),
135
405
  .operation = SPI_OP_MODE_MASTER | SPI_WORD_SET(8),
@@ -152,12 +422,12 @@ static void __tc_pnl_cmd(uint8_t cmd, const uint8_t* data, uint16_t len) {
152
422
  struct spi_buf_set __sc = { &__bc, 1 };
153
423
  __tc_pnl_cs_assert();
154
424
  gpio_pin_set_dt(&__tc_pnl_dc, 0);
155
- (void)spi_write(DEVICE_DT_GET(DT_NODELABEL(spi2)), &__tc_pnl_cfg8, &__sc);
425
+ (void)spi_write(DEVICE_DT_GET(DT_NODELABEL(${bus})), &__tc_pnl_cfg8, &__sc);
156
426
  if (len > 0) {
157
427
  struct spi_buf __bd = { const_cast<uint8_t*>(data), len };
158
428
  struct spi_buf_set __sd = { &__bd, 1 };
159
429
  gpio_pin_set_dt(&__tc_pnl_dc, 1);
160
- (void)spi_write(DEVICE_DT_GET(DT_NODELABEL(spi2)), &__tc_pnl_cfg8, &__sd);
430
+ (void)spi_write(DEVICE_DT_GET(DT_NODELABEL(${bus})), &__tc_pnl_cfg8, &__sd);
161
431
  }
162
432
  __tc_pnl_cs_release();
163
433
  }
@@ -169,7 +439,7 @@ static void __tc_pnl_ramwr_begin(void) {
169
439
  struct spi_buf_set __sc = { &__bc, 1 };
170
440
  __tc_pnl_cs_assert();
171
441
  gpio_pin_set_dt(&__tc_pnl_dc, 0);
172
- (void)spi_write(DEVICE_DT_GET(DT_NODELABEL(spi2)), &__tc_pnl_cfg8, &__sc);
442
+ (void)spi_write(DEVICE_DT_GET(DT_NODELABEL(${bus})), &__tc_pnl_cfg8, &__sc);
173
443
  gpio_pin_set_dt(&__tc_pnl_dc, 1);
174
444
  }
175
445
 
@@ -192,11 +462,11 @@ static uint16_t __tc_pnl_read_scanline(void) {
192
462
  struct spi_buf_set __sr = { &__br, 1 };
193
463
  __tc_pnl_cs_assert();
194
464
  gpio_pin_set_dt(&__tc_pnl_dc, 0);
195
- int __cmd_err = spi_write(DEVICE_DT_GET(DT_NODELABEL(spi2)), &__tc_pnl_cfg8, &__sc);
465
+ int __cmd_err = spi_write(DEVICE_DT_GET(DT_NODELABEL(${bus})), &__tc_pnl_cfg8, &__sc);
196
466
  int __read_err = 0;
197
467
  if (__cmd_err == 0) {
198
468
  gpio_pin_set_dt(&__tc_pnl_dc, 1);
199
- __read_err = spi_transceive(DEVICE_DT_GET(DT_NODELABEL(spi2)), &__tc_pnl_cfg8, &__st, &__sr);
469
+ __read_err = spi_transceive(DEVICE_DT_GET(DT_NODELABEL(${bus})), &__tc_pnl_cfg8, &__st, &__sr);
200
470
  }
201
471
  __tc_pnl_cs_release();
202
472
  if (__cmd_err != 0 || __read_err != 0) return 0xFFFFu;
@@ -220,34 +490,7 @@ static void __tc_pnl_wait_for_safe_rect(int16_t y, int16_t rh) {
220
490
  k_msleep(0);
221
491
  }
222
492
  }
223
-
224
- // Pack count rgb565 pixels into the row3 scratch buffer as 18-bit (666) wire
225
- // format: (r<<3, g<<2, b<<3) — R,G,B byte order, verified correct on this
226
- // panel in 18-bit mode. The caller then streams the buffer with the 8-bit
227
- // config (one row at a time; the buffer holds maxDim pixels).
228
- static void __tc_pnl_pack666(const uint16_t* px, uint32_t count) {
229
- for (uint32_t i = 0; i < count; i++) {
230
- uint16_t c = px[i];
231
- __tc_display_row3[i * 3] = static_cast<uint8_t>((c >> 8) & 0xF8u);
232
- __tc_display_row3[i * 3 + 1] = static_cast<uint8_t>((c >> 3) & 0xFCu);
233
- __tc_display_row3[i * 3 + 2] = static_cast<uint8_t>((c << 3) & 0xF8u);
234
- }
235
- }
236
-
237
- // Stream count rgb565 pixels to the panel (converted to 18-bit, chunked
238
- // through the row3 scratch). Caller holds the RAMWR burst.
239
- static void __tc_pnl_pixels666(const uint16_t* px, uint32_t count) {
240
- while (count > 0) {
241
- uint32_t __chunk = (count > ${maxDim}) ? ${maxDim} : count;
242
- __tc_pnl_pack666(px, __chunk);
243
- struct spi_buf __bd = { __tc_display_row3, static_cast<size_t>(__chunk) * 3U };
244
- struct spi_buf_set __sd = { &__bd, 1 };
245
- (void)spi_write(DEVICE_DT_GET(DT_NODELABEL(spi2)), &__tc_pnl_cfg8, &__sd);
246
- px += __chunk;
247
- count -= __chunk;
248
- }
249
- }
250
-
493
+ ${pixelHelpers}
251
494
  // Set the address window. Coordinates are in the effective (rotated) UI
252
495
  // space; the panel's MADCTL (rotation 1: MV) maps them onto the native
253
496
  // 320x480 raster, so CASET/RASET take the UI x/y ranges directly.
@@ -294,12 +537,12 @@ static void __tc_op_setAddrWindow(void* /*ctx*/, int16_t x, int16_t y, int16_t w
294
537
  // Push the stashed rect's worth of rgb565 pixels. Called right after
295
538
  // setAddrWindow with exactly (aw_w * aw_h) pixels. The caller's buffer is
296
539
  // never mutated (scroll canvases persist across frames), so pixels are
297
- // converted in chunks through the row3 scratch buffer.
540
+ // converted in chunks through the row${bpp} scratch buffer.
298
541
  static void __tc_op_writePixels(void* /*ctx*/, const uint16_t* px, uint32_t n) {
299
542
  if (n == 0U) return;
300
543
  __tc_pnl_set_window(__tc_aw_x, __tc_aw_y, __tc_aw_w, __tc_aw_h);
301
544
  __tc_pnl_ramwr_begin();
302
- __tc_pnl_pixels666(px, n);
545
+ ${pixelsFn}(px, n);
303
546
  __tc_pnl_ramwr_end();
304
547
  }
305
548
 
@@ -308,48 +551,10 @@ static void __tc_op_writePixel(void* /*ctx*/, int16_t x, int16_t y, uint16_t c)
308
551
  uint16_t __c = c;
309
552
  __tc_pnl_set_window(x, y, 1, 1);
310
553
  __tc_pnl_ramwr_begin();
311
- __tc_pnl_pixels666(&__c, 1);
312
- __tc_pnl_ramwr_end();
313
- }
314
-
315
- // Fill a rect row-by-row using the one-row 18-bit scratch buffer. This is the
316
- // hot path for background clears and large fills; building the color into the
317
- // reused buffer and writing each row keeps memory bounded.
318
- // Fill a rect with a solid color. The 18-bit row is tiled into the block buffer
319
- // (__TC_FILL_ROWS rows), then the whole rect is sent in multi-row spi_write
320
- // chunks. A full 480x320 clear is ~40 writes instead of ~320, dropping it from
321
- // ~110ms to ~15ms — the ESP32 SPI driver's per-transaction overhead (not SPI
322
- // bandwidth) is the binding cost, so fewer/larger writes win. Scatter-gather
323
- // descriptor lists tested slower (the driver walks each descriptor), so this
324
- // uses one contiguous buffer per write.
325
- static void __tc_op_fillRect(void* /*ctx*/, int16_t x, int16_t y, int16_t rw, int16_t rh, uint16_t c) {
326
- if (rw <= 0 || rh <= 0) return;
327
- uint8_t __b0 = static_cast<uint8_t>((c >> 8) & 0xF8u);
328
- uint8_t __b1 = static_cast<uint8_t>((c >> 3) & 0xFCu);
329
- uint8_t __b2 = static_cast<uint8_t>((c << 3) & 0xF8u);
330
- // Build one 18-bit row, then tile it into the block buffer.
331
- for (int16_t i = 0; i < rw; i++) {
332
- __tc_display_row3[i * 3] = __b0;
333
- __tc_display_row3[i * 3 + 1] = __b1;
334
- __tc_display_row3[i * 3 + 2] = __b2;
335
- }
336
- size_t rowBytes = static_cast<size_t>(rw) * 3U;
337
- for (int16_t r = 0; r < __TC_FILL_ROWS; r++) {
338
- memcpy(&__tc_display_block3[static_cast<size_t>(r) * rowBytes], __tc_display_row3, rowBytes);
339
- }
340
- __tc_pnl_set_window(x, y, rw, rh);
341
- __tc_pnl_ramwr_begin();
342
- int16_t remaining = rh;
343
- while (remaining > 0) {
344
- int16_t chunk = (remaining > __TC_FILL_ROWS) ? __TC_FILL_ROWS : remaining;
345
- struct spi_buf __bd = { __tc_display_block3, static_cast<size_t>(chunk) * rowBytes };
346
- struct spi_buf_set __sd = { &__bd, 1 };
347
- (void)spi_write(DEVICE_DT_GET(DT_NODELABEL(spi2)), &__tc_pnl_cfg8, &__sd);
348
- remaining -= chunk;
349
- }
554
+ ${pixelsFn}(&__c, 1);
350
555
  __tc_pnl_ramwr_end();
351
556
  }
352
-
557
+ ${fillRectFn}
353
558
  static int16_t __tc_op_width(void* /*ctx*/) { return ${w}; }
354
559
  static int16_t __tc_op_height(void* /*ctx*/) { return ${h}; }
355
560
 
@@ -371,45 +576,7 @@ const CuttlefishPanelOps __tc_display_ops = {
371
576
 
372
577
  // The live display target: a CuttlefishGFX driven by the panel-ops vtable.
373
578
  CuttlefishGFX __tc_display(&__tc_display_ops, nullptr);
374
-
375
- // ── Panel init ──────────────────────────────────────────────────────────
376
- // Adafruit ST7796S init sequence (demo-st lib fork), byte for byte: hw reset
377
- // pulse, SWRESET, manufacturer unlock, VCOM/MADCTL/COLMOD/porch registers,
378
- // lock, SLPOUT (150ms), DISPON (150ms), INVOFF. MADCTL 0x28 = MV (rotation 1
379
- // landscape) + BGR=1. BGR=1 makes the controller route data R/B to the
380
- // B/R subpixels (verified: red data shows blue with BGR=1), which combined
381
- // with a lossless R/B data swap renders the UI correctly; BGR=0 leaves a
382
- // half-lossy G/B quirk on this clone controller.
383
- struct __tc_pnl_init_cmd { uint8_t cmd; uint8_t len; const uint8_t* data; uint16_t delay_ms; };
384
- static const uint8_t __tc_pnl_i1[] = {0xC3};
385
- static const uint8_t __tc_pnl_i2[] = {0x96};
386
- static const uint8_t __tc_pnl_i3[] = {0x1C};
387
- static const uint8_t __tc_pnl_i4[] = {0x28};
388
- static const uint8_t __tc_pnl_i5[] = {0x66};
389
- static const uint8_t __tc_pnl_i6[] = {0x80};
390
- static const uint8_t __tc_pnl_i7[] = {0x00};
391
- static const uint8_t __tc_pnl_i8[] = {0x80, 0x02, 0x3B};
392
- static const uint8_t __tc_pnl_i9[] = {0xC6};
393
- static const uint8_t __tc_pnl_i10[] = {0x69};
394
- static const uint8_t __tc_pnl_i11[] = {0x3C};
395
- static const struct __tc_pnl_init_cmd __tc_pnl_init_seq[] = {
396
- {0x01, 0, NULL, 150}, // SWRESET
397
- {0xF0, 1, __tc_pnl_i1, 0}, // unlock manufacturer
398
- {0xF0, 1, __tc_pnl_i2, 0},
399
- {0xC5, 1, __tc_pnl_i3, 0}, // VCOM control
400
- {0x36, 1, __tc_pnl_i4, 0}, // MADCTL 0x28: MV (rotation 1) + BGR=1
401
- {0x3A, 1, __tc_pnl_i5, 0}, // COLMOD 0x66 (18-bit, 262K) — clean channel routing
402
- {0xB0, 1, __tc_pnl_i6, 0}, // interface control
403
- {0xB4, 1, __tc_pnl_i7, 0}, // inversion control
404
- {0xB6, 3, __tc_pnl_i8, 0}, // display function control
405
- {0xB7, 1, __tc_pnl_i9, 0}, // entry mode
406
- {0xF0, 1, __tc_pnl_i10, 0}, // lock manufacturer
407
- {0xF0, 1, __tc_pnl_i11, 0},
408
- {0x11, 0, NULL, 150}, // SLPOUT (sleep out — 120ms typical)
409
- {0x29, 0, NULL, 150}, // DISPON
410
- {0x20, 0, NULL, 0}, // INVOFF (non-inverted at power-on)
411
- };
412
-
579
+ ${initBlock}
413
580
  // ── display_init (called from setup) ────────────────────────────────────
414
581
  static inline void display_init() {
415
582
  printk("TC_DISPLAY: device ready\\n");
@@ -429,7 +596,7 @@ ${blInit}
429
596
  if (__tc_pnl_init_seq[i].delay_ms > 0) k_msleep(__tc_pnl_init_seq[i].delay_ms);
430
597
  }
431
598
  __tc_op_fillRect(nullptr, 0, 0, ${w}, ${h}, 0x0000);
432
- printk("TC_DISPLAY: direct init done (18-bit, black fill)\\n");
599
+ printk("TC_DISPLAY: direct init done (${wireTag}, black fill)\\n");
433
600
  }
434
601
 
435
602
  static inline void display_fillScreen(UI_COLOR_T color) {
@@ -26,8 +26,12 @@ export interface KconfigUsage {
26
26
  usesMqtt?: boolean;
27
27
  usesPreferences?: boolean;
28
28
  usesRandom?: boolean;
29
- /** Touch controller referenced (UI touch adapter emits DT_NODELABEL(ft6336u)). */
29
+ /** Touch controller referenced (UI touch adapter emits DT_NODELABEL(ft6336u)
30
+ * or DT_NODELABEL(xpt2046)). Selects the bus driver the node needs. */
30
31
  usesTouch?: boolean;
32
+ /** Which touch controller the program uses — FT6336U rides I2C, XPT2046
33
+ * rides the display's SPI bus. Only meaningful with usesTouch. */
34
+ touchController?: 'ft6336u' | 'xpt2046';
31
35
  /** PSRAM type ('opi' | 'quad') when the target board has PSRAM. Emits the
32
36
  * CONFIG_SPIRAM symbols so the ESP heap serves PSRAM for canvas allocations. */
33
37
  psram?: 'opi' | 'quad';
@@ -68,15 +72,31 @@ export function resolveKconfigFragments(
68
72
  // configured SPI clock (~80MHz) and drops into the low tens of ms. The
69
73
  // display overlay pairs this with dma-enabled + dmas on the spi2 node.
70
74
  m.set('CONFIG_DMA', 'y');
71
- // Disable the MIPI DBI SPI bridge + ST7796S drivers. The display adapter
72
- // drives the panel directly via spi_write. Binding these drivers would
73
- // allocate a tearing-effect GPIO interrupt that conflicts with the SPI/I2C
74
- // driver interrupts the VECDESC_FL_SHARED assertion crashes on touch.
75
+ // Disable the MIPI DBI SPI bridge + in-tree panel drivers (ILI9341,
76
+ // ST7796S). The display adapter drives the panel directly via spi_write.
77
+ // Binding these drivers would allocate a tearing-effect GPIO interrupt
78
+ // that conflicts with the SPI/I2C driver interrupts the
79
+ // VECDESC_FL_SHARED assertion crashes on touch. ILI9341 matters as much
80
+ // as the bridge: the driver auto-defaults on from the overlay's
81
+ // ilitek,ili9341 node and references the (disabled) mipi-dbi-spi
82
+ // controller's device struct, failing at link time with
83
+ // "undefined reference to __device_dts_ord_N". (Assign the prompted
84
+ // ILI9341, not the hidden ILI9XXX — promptless symbols reject prj.conf
85
+ // assignments.)
75
86
  m.set('CONFIG_MIPI_DBI_SPI', 'n');
87
+ m.set('CONFIG_ILI9341', 'n');
76
88
  m.set('CONFIG_ST7796S', 'n');
77
89
  }
78
90
  if (usage.usesTouch) {
79
- m.set('CONFIG_I2C', 'y'); // FT6336U touch on I2C
91
+ // FT6336U touch is on I2C; the XPT2046 shares the display's SPI bus.
92
+ // CONFIG_INPUT stays off either way: the adapters drive the controllers
93
+ // directly, and enabling it would build the in-tree input drivers
94
+ // (ft5336 / xpt2046) against nodes these adapters already own.
95
+ if (usage.touchController === 'xpt2046') {
96
+ m.set('CONFIG_SPI', 'y');
97
+ } else {
98
+ m.set('CONFIG_I2C', 'y');
99
+ }
80
100
  }
81
101
  // PSRAM: enable the ESP SPIRAM driver + route malloc/heap to external RAM so
82
102
  // large canvas allocations (scroll viewports, lists) can use PSRAM instead of