@typecad/framework-zephyr 1.0.0-alpha.15 → 1.0.0-alpha.18

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.
Files changed (132) hide show
  1. package/LICENSE +202 -21
  2. package/README.md +49 -87
  3. package/dist/as-built.d.ts +2 -2
  4. package/dist/as-built.js +2 -2
  5. package/dist/boardgen.d.ts +1 -9
  6. package/dist/boardgen.js +228 -45
  7. package/dist/chips/types.d.ts +1 -1
  8. package/dist/debug-codegen.js +1 -1
  9. package/dist/display/touch-adapter.js +1 -1
  10. package/dist/display/ui-adapter.js +549 -549
  11. package/dist/doctor.js +4 -4
  12. package/dist/dt-config/custom-board.js +2 -2
  13. package/dist/dt-config/kconfig.js +18 -12
  14. package/dist/dt-config/overlay.d.ts +2 -2
  15. package/dist/dt-config/overlay.js +2 -2
  16. package/dist/framework.manifest.d.ts +3 -3
  17. package/dist/framework.manifest.js +10 -7
  18. package/dist/index.js +5 -5
  19. package/dist/licenses.d.ts +2 -2
  20. package/dist/licenses.js +8 -8
  21. package/dist/lowering/fs.js +1 -1
  22. package/dist/lowering/gpio.js +0 -32
  23. package/dist/lowering/http.js +264 -32
  24. package/dist/lowering/i2c.js +0 -8
  25. package/dist/lowering/interrupts.js +6 -0
  26. package/dist/lowering/mqtt.js +110 -8
  27. package/dist/lowering/usb.js +11 -11
  28. package/dist/lowering/wdt.js +2 -29
  29. package/dist/sdk/board-catalog-sync.d.ts +1 -3
  30. package/dist/sdk/board-catalog-sync.js +4 -10
  31. package/dist/strategy.d.ts +22 -44
  32. package/dist/strategy.js +152 -155
  33. package/dist/tmp-probe.d.ts +2 -0
  34. package/dist/tmp-probe.js +9 -0
  35. package/dist/toolchain/debug-config.d.ts +50 -90
  36. package/dist/toolchain/debug-config.js +241 -502
  37. package/dist/toolchain/env-check.d.ts +1 -3
  38. package/dist/toolchain/env-check.js +2 -7
  39. package/dist/toolchain/index.d.ts +65 -2
  40. package/dist/toolchain/index.js +432 -55
  41. package/dist/toolchain/runners.d.ts +16 -0
  42. package/dist/toolchain/runners.js +75 -0
  43. package/dist/toolchain/scaffold.d.ts +1 -1
  44. package/dist/toolchain/scaffold.js +12 -12
  45. package/dist/toolchain/west-discover.d.ts +6 -0
  46. package/dist/toolchain/west-discover.js +36 -13
  47. package/dist/toolchain/west-spawn.js +8 -2
  48. package/installer/README.md +328 -328
  49. package/installer/install.sh +2 -2
  50. package/installer/templates/project/.typecad/activate-zephyr.ps1 +1 -1
  51. package/installer/templates/project/.typecad/activate-zephyr.sh +1 -1
  52. package/installer/templates/project/.vscode/settings.json +1 -1
  53. package/installer/templates/project/README.md +2 -2
  54. package/package.json +5 -5
  55. package/src/as-built.ts +206 -206
  56. package/src/boardgen.ts +214 -48
  57. package/src/chips/types.ts +567 -567
  58. package/src/display/touch-adapter.ts +204 -204
  59. package/src/display/ui-adapter.ts +781 -781
  60. package/src/doctor.ts +4 -4
  61. package/src/dt-config/custom-board.ts +2 -2
  62. package/src/dt-config/kconfig.ts +18 -12
  63. package/src/dt-config/overlay.ts +1058 -1058
  64. package/src/framework.manifest.ts +538 -535
  65. package/src/index.ts +5 -5
  66. package/src/licenses.ts +425 -425
  67. package/src/lowering/fs.ts +135 -135
  68. package/src/lowering/gpio.ts +0 -33
  69. package/src/lowering/http.ts +264 -32
  70. package/src/lowering/i2c.ts +0 -9
  71. package/src/lowering/interrupts.ts +6 -0
  72. package/src/lowering/mqtt.ts +109 -8
  73. package/src/lowering/usb.ts +221 -221
  74. package/src/lowering/wdt.ts +2 -25
  75. package/src/sdk/board-catalog-sync.ts +4 -25
  76. package/src/strategy.ts +2265 -2309
  77. package/src/toolchain/debug-config.ts +264 -514
  78. package/src/toolchain/env-check.ts +279 -285
  79. package/src/toolchain/index.ts +1703 -1286
  80. package/src/toolchain/runners.ts +80 -0
  81. package/src/toolchain/scaffold.ts +296 -296
  82. package/src/toolchain/west-discover.ts +35 -13
  83. package/src/toolchain/west-spawn.ts +174 -168
  84. package/dist/async/timer-polyfill.d.ts +0 -10
  85. package/dist/async/timer-polyfill.js +0 -95
  86. package/dist/chips/board-overrides.d.ts +0 -7
  87. package/dist/chips/board-overrides.js +0 -11
  88. package/dist/chips/esp32.d.ts +0 -2
  89. package/dist/chips/esp32.js +0 -71
  90. package/dist/chips/esp32s3.d.ts +0 -2
  91. package/dist/chips/esp32s3.js +0 -103
  92. package/dist/chips/soc/.d.ts +0 -2
  93. package/dist/chips/soc/.js +0 -129
  94. package/dist/chips/soc/esp32.d.ts +0 -2
  95. package/dist/chips/soc/esp32.js +0 -120
  96. package/dist/chips/soc/esp32c3.d.ts +0 -2
  97. package/dist/chips/soc/esp32c3.js +0 -90
  98. package/dist/chips/soc/esp32c6.d.ts +0 -2
  99. package/dist/chips/soc/esp32c6.js +0 -109
  100. package/dist/chips/soc/esp32s3.d.ts +0 -2
  101. package/dist/chips/soc/esp32s3.js +0 -189
  102. package/dist/chips/soc/index.d.ts +0 -2
  103. package/dist/chips/soc/index.js +0 -23
  104. package/dist/chips/soc/nrf52840.d.ts +0 -2
  105. package/dist/chips/soc/nrf52840.js +0 -130
  106. package/dist/chips/soc/rp2040.d.ts +0 -2
  107. package/dist/chips/soc/rp2040.js +0 -141
  108. package/dist/chips/soc/rp2350a.d.ts +0 -2
  109. package/dist/chips/soc/rp2350a.js +0 -145
  110. package/dist/chips/soc/samd21g18a.d.ts +0 -2
  111. package/dist/chips/soc/samd21g18a.js +0 -143
  112. package/dist/chips/soc/stm32f411xe.d.ts +0 -2
  113. package/dist/chips/soc/stm32f411xe.js +0 -251
  114. package/dist/chips/xiao-ble.d.ts +0 -2
  115. package/dist/chips/xiao-ble.js +0 -100
  116. package/dist/lowering/power.d.ts +0 -9
  117. package/dist/lowering/power.js +0 -60
  118. package/dist/lowering/pulse.d.ts +0 -7
  119. package/dist/lowering/pulse.js +0 -51
  120. package/dist/lowering/tone.d.ts +0 -10
  121. package/dist/lowering/tone.js +0 -63
  122. package/dist/lowering/worker-backing.d.ts +0 -14
  123. package/dist/lowering/worker-backing.js +0 -79
  124. package/dist/lowering/worker.d.ts +0 -6
  125. package/dist/lowering/worker.js +0 -14
  126. package/dist/sdk/board-data.generated.d.ts +0 -2
  127. package/dist/sdk/board-data.generated.js +0 -4
  128. package/dist/sdk/catalog-walker.d.ts +0 -90
  129. package/dist/sdk/catalog-walker.js +0 -682
  130. package/dist/sdk/dts-reader.d.ts +0 -83
  131. package/dist/sdk/dts-reader.js +0 -596
  132. package/src/debug-codegen.ts +0 -207
@@ -1,781 +1,781 @@
1
- // ---------------------------------------------------------------------------
2
- // Zephyr UI display adapter for the Cuttlefish UI rendering pipeline.
3
- //
4
- // Bridges the in-tree CuttlefishGFX/CuttlefishCanvas16 class (emitted by the
5
- // runtime header's cuttlefish-gfx slice) to an SPI TFT panel. The panel is
6
- // driven DIRECTLY over the SPI controller with GPIO chip-select, DC and reset
7
- // pins — NOT through Zephyr's mipi-dbi-spi bridge: that bridge issues separate
8
- // SPI transactions for the command byte and its parameters (CS deasserts
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.
22
- //
23
- // This is the Zephyr analog of the Adafruit adapters in framework-arduino,
24
- // using the in-tree native GFX class (no #define CuttlefishCanvas16) driven
25
- // through a CuttlefishPanelOps function-pointer vtable.
26
- //
27
- // EMIT BOUNDARY: emitted bytes land in user firmware. Covered by the TypeCAD
28
- // Runtime Exception (RUNTIME_EXCEPTION.md at the repo root).
29
- //
30
- // AGENTS.md rendering guardrails: the line buffer is one row (reused, not
31
- // per-frame allocated); fillRect writes row-by-row. Pixel bursts use 16-bit
32
- // SPI words (MSB-first on the wire = RGB565 big-endian, no byte swap).
33
- // ---------------------------------------------------------------------------
34
-
35
- import type { DisplayAdapterCode, DisplayAdapterGenerator } from "@typecad/cuttlefish/api/shared";
36
- import { ZEPHYR_DISPLAY_PROFILES, panelControllerFor } from "./profiles.js";
37
- import type { ZephyrDisplayProfile } from "./profiles.js";
38
-
39
- /**
40
- * Build the Zephyr UI display adapter for a profile. Emits the full
41
- * display_ / display_target_ / display_canvas_ surface the UI runtime expects,
42
- * driving the panel directly via SPI (CS held across command+data).
43
- */
44
- export interface ZephyrDisplayReadbackOptions {
45
- /** Enable the controller GET_SCANLINE dirty-rectangle experiment. */
46
- scanlineSync?: boolean;
47
- /** MISO/SDO GPIO; readback is disabled when it is not explicitly wired. */
48
- miso?: number;
49
- /** Tearing-effect GPIO (panel TE output). When set, panel updates wait for
50
- * the TE frame pulse instead of GET_SCANLINE readback — no MISO required.
51
- * The overlay adds te-gpios to the display DT node from this. */
52
- tearingEffectPin?: number;
53
- }
54
-
55
- export function zephyrUiDisplayAdapter(
56
- profile: ZephyrDisplayProfile,
57
- readback: ZephyrDisplayReadbackOptions = {},
58
- ): DisplayAdapterCode {
59
- const w = profile.width;
60
- const h = profile.height;
61
- const maxDim = Math.max(w, h);
62
- const dtLabel = profile.dtLabel;
63
- const backlightAlias = profile.backlight;
64
- const bus = profile.busLabel ?? 'spi2';
65
- const bridge = profile.bridgeLabel ?? 'mipi_dbi';
66
- const controller = panelControllerFor(profile);
67
- const isIli9341 = controller === 'ili9341';
68
- // Bytes per pixel on the wire: 3 (18-bit 666) for ST7796S, 2 (16-bit 565)
69
- // for ILI9341. The row/block scratch buffers and pack loops key off this.
70
- const bpp = isIli9341 ? 2 : 3;
71
- const rowBuf = `__tc_display_row${bpp}`;
72
- const blockBuf = `__tc_display_block${bpp}`;
73
- const packFn = isIli9341 ? '__tc_pnl_pack565' : '__tc_pnl_pack666';
74
- const pixelsFn = isIli9341 ? '__tc_pnl_pixels565' : '__tc_pnl_pixels666';
75
- const wireTag = isIli9341 ? '16-bit 565' : '18-bit';
76
- // Never infer readback from a board's default pinmux. SDO/MISO may be left
77
- // floating or shared with another device, and ST7796S modules are known to
78
- // react badly to GSCAN reads. Both an explicit opt-in and an explicit MISO
79
- // pin are required before emitting an active synchronization path.
80
- const scanlineSync = readback.scanlineSync === true && readback.miso !== undefined;
81
- // TE (hardware tearing-effect) sync: strictly opt-in via a configured GPIO.
82
- // Preferred over GET_SCANLINE when wired — no readback traffic, no MISO.
83
- const tePin = typeof readback.tearingEffectPin === 'number' ? readback.tearingEffectPin : undefined;
84
-
85
- const includes = [
86
- `// --- Zephyr UI display adapter (${profile.driver}) ---`,
87
- `// Native CuttlefishGFX path: do NOT #define CuttlefishCanvas16 so the`,
88
- `// runtime header emits the in-tree CuttlefishGFX/CuttlefishCanvas16 class.`,
89
- `#define CuttlefishDisplayTarget CuttlefishGFX`,
90
- `#include <zephyr/kernel.h>`,
91
- `#include <zephyr/drivers/display.h>`,
92
- `#include <zephyr/drivers/spi.h>`,
93
- `#include <zephyr/drivers/gpio.h>`,
94
- ].join("\n");
95
-
96
- // Per-controller scratch-buffer declarations. The ST7796S text is frozen as
97
- // hardware-verified; the ILI9341 variant documents its 565 wire format.
98
- const bufferDecls = isIli9341
99
- ? [
100
- `// One-row scratch buffer in 16-bit (565) wire format: maxDim px x 2 bytes.`,
101
- `// Reused across fillRect/draw calls — never per-frame (AGENTS.md: no`,
102
- `// per-frame heap allocation). The ILI9341 is driven in its native`,
103
- `// 16-bit COLMOD (0x55): RGB565 pixels go out big-endian (MSB-first SPI),`,
104
- `// the Adafruit ILI9341 convention, with no repacking needed.`,
105
- `static uint8_t ${rowBuf}[${maxDim} * 2];`,
106
- `// Multi-row block buffer for solid fills (8 rows of maxDim px in 565).`,
107
- `// fillRect fills this once with the color, then sends the whole rect in a`,
108
- `// few large spi_write chunks instead of one spi_write per row — a full`,
109
- `// 480x320 clear went from ~320 syscalls (~110ms) to ~40 (~15ms). Reused,`,
110
- `// not per-frame (AGENTS.md).`,
111
- `#define __TC_FILL_ROWS 8`,
112
- `static uint8_t ${blockBuf}[${maxDim} * 2 * __TC_FILL_ROWS];`,
113
- ]
114
- : [
115
- `// One-row scratch buffer in 18-bit (666) wire format: maxDim px x 3 bytes.`,
116
- `// Reused across fillRect/draw calls — never per-frame (AGENTS.md: no`,
117
- `// per-frame heap allocation). The panel is driven in 18-bit mode (COLMOD`,
118
- `// 0x66): its 16-bit (565) channel routing is crossed (G/B swap, verified`,
119
- `// with calibration bands), while 18-bit mode routes every channel`,
120
- `// correctly with plain (R,G,B) byte order.`,
121
- `static uint8_t ${rowBuf}[${maxDim} * 3];`,
122
- `// Multi-row block buffer for solid fills (8 rows of maxDim px in 18-bit).`,
123
- `// fillRect fills this once with the color, then sends the whole rect in a`,
124
- `// few large spi_write chunks instead of one spi_write per row — a full`,
125
- `// 480x320 clear went from ~320 syscalls (~110ms) to ~40 (~15ms). Reused,`,
126
- `// not per-frame (AGENTS.md).`,
127
- `#define __TC_FILL_ROWS 8`,
128
- `static uint8_t ${blockBuf}[${maxDim} * 3 * __TC_FILL_ROWS];`,
129
- ];
130
-
131
- const declaration = [
132
- `// CUTTLEFISH_DISPLAY_BEGIN`,
133
- // The display0 DT node carries frequency/dimensions for DT_PROP reads, but
134
- // the panel is driven directly via spi_write — no Zephyr display device is
135
- // needed. Using a constant 1 avoids the ST7796S driver binding (which
136
- // allocates a tearing-effect GPIO interrupt that conflicts with the SPI/I2C
137
- // interrupts — the VECDESC_FL_SHARED assertion crash on the 3rd frame).
138
- `#define __tc_display_dev 1`,
139
- ...bufferDecls,
140
- `// startWrite/endWrite batching depth. When > 0 the panel CS is held asserted`,
141
- `// (low) across multiple primitives — DC still toggles mid-burst, but CS does`,
142
- `// not, matching the Adafruit ST77xx protocol and avoiding one full CS-toggle`,
143
- `// SPI transaction per primitive. The runtime brackets whole canvas pushes`,
144
- `// and per-node repaints in display_startWrite/endWrite pairs.`,
145
- `static uint8_t __tc_pnl_write_depth = 0;`,
146
- `// Scanline readback is deliberately opt-in. A verified SDO/MISO wire and`,
147
- `// controller-specific validation are required; otherwise the display stays`,
148
- `// on the existing retained/composited path with no extra SPI reads.`,
149
- `static const bool __tc_pnl_scanline_sync = ${scanlineSync ? 'true' : 'false'};`,
150
- `// Tearing-effect (TE) hardware sync: the panel pulses its TE line once`,
151
- `// per frame. When te-gpios is present on the display DT node, panel`,
152
- `// updates arm on the TE edge — tear-free writes with no MISO readback.`,
153
- `#if DT_NODE_HAS_PROP(DT_NODELABEL(${dtLabel}), te_gpios)`,
154
- `#define __TC_TE_SYNC 1`,
155
- `static const struct gpio_dt_spec __tc_te =`,
156
- ` GPIO_DT_SPEC_GET(DT_NODELABEL(${dtLabel}), te_gpios);`,
157
- `static struct gpio_callback __tc_te_cb;`,
158
- `static volatile uint32_t __tc_te_count = 0;`,
159
- `static void __tc_te_isr(const struct device* port, struct gpio_callback* cb, uint32_t pins) {`,
160
- ` (void)port; (void)cb; (void)pins;`,
161
- ` __tc_te_count++;`,
162
- `}`,
163
- `#else`,
164
- `#define __TC_TE_SYNC 0`,
165
- `#endif`,
166
- `// Stashed address window from the last setAddrWindow call. The runtime`,
167
- `// calls setAddrWindow + writePixels as a matched pair, so we stash the rect`,
168
- `// here and consume it in writePixels.`,
169
- `static int16_t __tc_aw_x = 0;`,
170
- `static int16_t __tc_aw_y = 0;`,
171
- `static int16_t __tc_aw_w = ${w};`,
172
- `static int16_t __tc_aw_h = ${h};`,
173
- `// CUTTLEFISH_DISPLAY_END`,
174
- ].join("\n");
175
-
176
- // Backlight: drive it as a raw GPIO output. The DT alias points at a
177
- // gpio-leds node whose 'gpios' property is a phandle to the GPIO controller
178
- // + pin. GPIO_DT_SPEC_GET resolves that phandle into a gpio_dt_spec. The
179
- // overlay only emits this alias when a backlight GPIO is configured, so guard
180
- // with DT_HAS_ALIAS (the safe primitive for an alias that may be absent —
181
- // DT_NODE_HAS_STATUS(DT_ALIAS(...)) is version-dependent when the alias is
182
- // missing and can fail the build).
183
- // TE pin: input + rising-edge interrupt (the ST7796 TE pulse), then tell
184
- // the controller to drive the line (TEON 0x35, mode 1 = vertical sync only).
185
- const teInit = tePin !== undefined ? `#if __TC_TE_SYNC
186
- if (device_is_ready(__tc_te.port)) {
187
- gpio_pin_configure_dt(&__tc_te, GPIO_INPUT);
188
- gpio_init_callback(&__tc_te_cb, __tc_te_isr, BIT(__tc_te.pin));
189
- (void)gpio_add_callback(__tc_te.port, &__tc_te_cb);
190
- (void)gpio_pin_interrupt_configure_dt(&__tc_te, GPIO_INT_EDGE_RISING);
191
- __tc_pnl_cmd1(0x35, 0x01); // TEON: TE output = vsync pulse
192
- }
193
- #endif` : '';
194
-
195
- const blInit = backlightAlias
196
- ? `#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`
197
- : '';
198
-
199
- // Per-controller pixel pack/stream helpers. Both share the row-chunked
200
- // transport; only the per-pixel wire encoding differs.
201
- const pixelHelpers = isIli9341
202
- ? `
203
- // Pack count rgb565 pixels into the row2 scratch buffer as big-endian 16-bit
204
- // wire bytes (c>>8, c&0xFF) — the ILI9341's native 565 format under COLMOD
205
- // 0x55. The caller then streams the buffer with the 8-bit config (one row at
206
- // a time; the buffer holds maxDim pixels).
207
- static void ${packFn}(const uint16_t* px, uint32_t count) {
208
- for (uint32_t i = 0; i < count; i++) {
209
- uint16_t c = px[i];
210
- ${rowBuf}[i * 2] = static_cast<uint8_t>(c >> 8);
211
- ${rowBuf}[i * 2 + 1] = static_cast<uint8_t>(c);
212
- }
213
- }
214
-
215
- // Stream count rgb565 pixels to the panel (chunked through the row2 scratch).
216
- // Caller holds the RAMWR burst.
217
- static void ${pixelsFn}(const uint16_t* px, uint32_t count) {
218
- while (count > 0) {
219
- uint32_t __chunk = (count > ${maxDim}) ? ${maxDim} : count;
220
- ${packFn}(px, __chunk);
221
- struct spi_buf __bd = { ${rowBuf}, static_cast<size_t>(__chunk) * 2U };
222
- struct spi_buf_set __sd = { &__bd, 1 };
223
- (void)spi_write(DEVICE_DT_GET(DT_NODELABEL(${bus})), &__tc_pnl_cfg8, &__sd);
224
- px += __chunk;
225
- count -= __chunk;
226
- }
227
- }
228
- `
229
- : `
230
- // Pack count rgb565 pixels into the row3 scratch buffer as 18-bit (666) wire
231
- // format: (r<<3, g<<2, b<<3) — R,G,B byte order, verified correct on this
232
- // panel in 18-bit mode. The caller then streams the buffer with the 8-bit
233
- // config (one row at a time; the buffer holds maxDim pixels).
234
- static void ${packFn}(const uint16_t* px, uint32_t count) {
235
- for (uint32_t i = 0; i < count; i++) {
236
- uint16_t c = px[i];
237
- ${rowBuf}[i * 3] = static_cast<uint8_t>((c >> 8) & 0xF8u);
238
- ${rowBuf}[i * 3 + 1] = static_cast<uint8_t>((c >> 3) & 0xFCu);
239
- ${rowBuf}[i * 3 + 2] = static_cast<uint8_t>((c << 3) & 0xF8u);
240
- }
241
- }
242
-
243
- // Stream count rgb565 pixels to the panel (converted to 18-bit, chunked
244
- // through the row3 scratch). Caller holds the RAMWR burst.
245
- static void ${pixelsFn}(const uint16_t* px, uint32_t count) {
246
- while (count > 0) {
247
- uint32_t __chunk = (count > ${maxDim}) ? ${maxDim} : count;
248
- ${packFn}(px, __chunk);
249
- struct spi_buf __bd = { ${rowBuf}, static_cast<size_t>(__chunk) * 3U };
250
- struct spi_buf_set __sd = { &__bd, 1 };
251
- (void)spi_write(DEVICE_DT_GET(DT_NODELABEL(${bus})), &__tc_pnl_cfg8, &__sd);
252
- px += __chunk;
253
- count -= __chunk;
254
- }
255
- }
256
- `;
257
-
258
- // Per-controller solid-fill. Both tile one built row into the block buffer
259
- // and stream it in multi-row chunks; only the color encoding differs.
260
- const fillRectFn = isIli9341
261
- ? `
262
- static void __tc_op_fillRect(void* /*ctx*/, int16_t x, int16_t y, int16_t rw, int16_t rh, uint16_t c) {
263
- if (rw <= 0 || rh <= 0) return;
264
- uint8_t __b0 = static_cast<uint8_t>(c >> 8);
265
- uint8_t __b1 = static_cast<uint8_t>(c);
266
- // Build one row, then tile it into the block buffer.
267
- for (int16_t i = 0; i < rw; i++) {
268
- ${rowBuf}[i * 2] = __b0;
269
- ${rowBuf}[i * 2 + 1] = __b1;
270
- }
271
- size_t rowBytes = static_cast<size_t>(rw) * 2U;
272
- for (int16_t r = 0; r < __TC_FILL_ROWS; r++) {
273
- memcpy(&${blockBuf}[static_cast<size_t>(r) * rowBytes], ${rowBuf}, rowBytes);
274
- }
275
- __tc_pnl_set_window(x, y, rw, rh);
276
- __tc_pnl_ramwr_begin();
277
- int16_t remaining = rh;
278
- while (remaining > 0) {
279
- int16_t chunk = (remaining > __TC_FILL_ROWS) ? __TC_FILL_ROWS : remaining;
280
- struct spi_buf __bd = { ${blockBuf}, static_cast<size_t>(chunk) * rowBytes };
281
- struct spi_buf_set __sd = { &__bd, 1 };
282
- (void)spi_write(DEVICE_DT_GET(DT_NODELABEL(${bus})), &__tc_pnl_cfg8, &__sd);
283
- remaining -= chunk;
284
- }
285
- __tc_pnl_ramwr_end();
286
- }
287
- `
288
- : `
289
- // Fill a rect row-by-row using the one-row 18-bit scratch buffer. This is the
290
- // hot path for background clears and large fills; building the color into the
291
- // reused buffer and writing each row keeps memory bounded.
292
- // Fill a rect with a solid color. The 18-bit row is tiled into the block buffer
293
- // (__TC_FILL_ROWS rows), then the whole rect is sent in multi-row spi_write
294
- // chunks. A full 480x320 clear is ~40 writes instead of ~320, dropping it from
295
- // ~110ms to ~15ms — the ESP32 SPI driver's per-transaction overhead (not SPI
296
- // bandwidth) is the binding cost, so fewer/larger writes win. Scatter-gather
297
- // descriptor lists tested slower (the driver walks each descriptor), so this
298
- // uses one contiguous buffer per write.
299
- static void __tc_op_fillRect(void* /*ctx*/, int16_t x, int16_t y, int16_t rw, int16_t rh, uint16_t c) {
300
- if (rw <= 0 || rh <= 0) return;
301
- uint8_t __b0 = static_cast<uint8_t>((c >> 8) & 0xF8u);
302
- uint8_t __b1 = static_cast<uint8_t>((c >> 3) & 0xFCu);
303
- uint8_t __b2 = static_cast<uint8_t>((c << 3) & 0xF8u);
304
- // Build one 18-bit row, then tile it into the block buffer.
305
- for (int16_t i = 0; i < rw; i++) {
306
- ${rowBuf}[i * 3] = __b0;
307
- ${rowBuf}[i * 3 + 1] = __b1;
308
- ${rowBuf}[i * 3 + 2] = __b2;
309
- }
310
- size_t rowBytes = static_cast<size_t>(rw) * 3U;
311
- for (int16_t r = 0; r < __TC_FILL_ROWS; r++) {
312
- memcpy(&${blockBuf}[static_cast<size_t>(r) * rowBytes], ${rowBuf}, rowBytes);
313
- }
314
- __tc_pnl_set_window(x, y, rw, rh);
315
- __tc_pnl_ramwr_begin();
316
- int16_t remaining = rh;
317
- while (remaining > 0) {
318
- int16_t chunk = (remaining > __TC_FILL_ROWS) ? __TC_FILL_ROWS : remaining;
319
- struct spi_buf __bd = { ${blockBuf}, static_cast<size_t>(chunk) * rowBytes };
320
- struct spi_buf_set __sd = { &__bd, 1 };
321
- (void)spi_write(DEVICE_DT_GET(DT_NODELABEL(${bus})), &__tc_pnl_cfg8, &__sd);
322
- remaining -= chunk;
323
- }
324
- __tc_pnl_ramwr_end();
325
- }
326
- `;
327
-
328
- // Per-controller init table. Both are byte-for-byte Adafruit sequences
329
- // emitted as (cmd, len, data, delay) records walked in display_init.
330
- const initBlock = isIli9341
331
- ? `
332
- // ── Panel init ──────────────────────────────────────────────────────────
333
- // Adafruit ILI9341 init sequence (initILI9341), byte for byte: SWRESET,
334
- // manufacturer/power/gamma registers, MADCTL 0x28 (MV landscape + BGR, the
335
- // same convention as the ST7796S path), COLMOD 0x55 (16-bit RGB565 — the
336
- // native wire format of the pack565 transport), SLPOUT (150ms), DISPON
337
- // (150ms), INVON. INVON is included because the common ILI9341 SPI modules
338
- // (2.2"/2.4" TFTs) ship with an inverted panel; the in-tree binding's
339
- // display-inversion property mirrors this for the stock driver.
340
- struct __tc_pnl_init_cmd { uint8_t cmd; uint8_t len; const uint8_t* data; uint16_t delay_ms; };
341
- static const uint8_t __tc_pnl_i1[] = {0x03, 0x80, 0x02};
342
- static const uint8_t __tc_pnl_i2[] = {0x00, 0xC1, 0x30};
343
- static const uint8_t __tc_pnl_i3[] = {0x64, 0x03, 0x12, 0x81};
344
- static const uint8_t __tc_pnl_i4[] = {0x85, 0x00, 0x78};
345
- static const uint8_t __tc_pnl_i5[] = {0x39, 0x2C, 0x00, 0x34, 0x02};
346
- static const uint8_t __tc_pnl_i6[] = {0x20};
347
- static const uint8_t __tc_pnl_i7[] = {0x00, 0x00};
348
- static const uint8_t __tc_pnl_i8[] = {0x23};
349
- static const uint8_t __tc_pnl_i9[] = {0x10};
350
- static const uint8_t __tc_pnl_i10[] = {0x3E, 0x28};
351
- static const uint8_t __tc_pnl_i11[] = {0x86};
352
- static const uint8_t __tc_pnl_i12[] = {0x28};
353
- static const uint8_t __tc_pnl_i13[] = {0x55};
354
- static const uint8_t __tc_pnl_i14[] = {0x00, 0x18};
355
- static const uint8_t __tc_pnl_i15[] = {0x08, 0x82, 0x27};
356
- static const uint8_t __tc_pnl_i16[] = {0x00};
357
- static const uint8_t __tc_pnl_i17[] = {0x01};
358
- static const uint8_t __tc_pnl_i18[] = {0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, 0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03, 0x0E, 0x09, 0x00};
359
- static const uint8_t __tc_pnl_i19[] = {0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, 0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C, 0x31, 0x36, 0x0F};
360
- static const struct __tc_pnl_init_cmd __tc_pnl_init_seq[] = {
361
- {0x01, 0, NULL, 150}, // SWRESET
362
- {0xEF, 3, __tc_pnl_i1, 0},
363
- {0xCF, 3, __tc_pnl_i2, 0},
364
- {0xED, 4, __tc_pnl_i3, 0},
365
- {0xE8, 3, __tc_pnl_i4, 0},
366
- {0xCB, 5, __tc_pnl_i5, 0},
367
- {0xF7, 1, __tc_pnl_i6, 0},
368
- {0xEA, 2, __tc_pnl_i7, 0},
369
- {0xC0, 1, __tc_pnl_i8, 0}, // PWCTRL1
370
- {0xC1, 1, __tc_pnl_i9, 0}, // PWCTRL2
371
- {0xC5, 2, __tc_pnl_i10, 0}, // VMCTRL1
372
- {0xC7, 1, __tc_pnl_i11, 0}, // VMCTRL2
373
- {0x36, 1, __tc_pnl_i12, 0}, // MADCTL 0x28: MV (landscape) + BGR=1
374
- {0x3A, 1, __tc_pnl_i13, 0}, // COLMOD 0x55 (16-bit RGB565)
375
- {0xB1, 2, __tc_pnl_i14, 0}, // FRMCTR1
376
- {0xB6, 3, __tc_pnl_i15, 0}, // DISCTRL
377
- {0xF2, 1, __tc_pnl_i16, 0}, // ENABLE3G off
378
- {0x26, 1, __tc_pnl_i17, 0}, // GAMSET gamma curve 1
379
- {0xE0, 15, __tc_pnl_i18, 0}, // PGAMCTRL
380
- {0xE1, 15, __tc_pnl_i19, 0}, // NGAMCTRL
381
- {0x11, 0, NULL, 150}, // SLPOUT (sleep out — 120ms typical)
382
- {0x29, 0, NULL, 150}, // DISPON
383
- {0x21, 0, NULL, 0}, // INVON (common ILI9341 modules ship inverted)
384
- };
385
- `
386
- : `
387
- // ── Panel init ──────────────────────────────────────────────────────────
388
- // Adafruit ST7796S init sequence (demo-st lib fork), byte for byte: hw reset
389
- // pulse, SWRESET, manufacturer unlock, VCOM/MADCTL/COLMOD/porch registers,
390
- // lock, SLPOUT (150ms), DISPON (150ms), INVOFF. MADCTL 0x28 = MV (rotation 1
391
- // landscape) + BGR=1. BGR=1 makes the controller route data R/B to the
392
- // B/R subpixels (verified: red data shows blue with BGR=1), which combined
393
- // with a lossless R/B data swap renders the UI correctly; BGR=0 leaves a
394
- // half-lossy G/B quirk on this clone controller.
395
- struct __tc_pnl_init_cmd { uint8_t cmd; uint8_t len; const uint8_t* data; uint16_t delay_ms; };
396
- static const uint8_t __tc_pnl_i1[] = {0xC3};
397
- static const uint8_t __tc_pnl_i2[] = {0x96};
398
- static const uint8_t __tc_pnl_i3[] = {0x1C};
399
- static const uint8_t __tc_pnl_i4[] = {0x28};
400
- static const uint8_t __tc_pnl_i5[] = {0x66};
401
- static const uint8_t __tc_pnl_i6[] = {0x80};
402
- static const uint8_t __tc_pnl_i7[] = {0x00};
403
- static const uint8_t __tc_pnl_i8[] = {0x80, 0x02, 0x3B};
404
- static const uint8_t __tc_pnl_i9[] = {0xC6};
405
- static const uint8_t __tc_pnl_i10[] = {0x69};
406
- static const uint8_t __tc_pnl_i11[] = {0x3C};
407
- static const struct __tc_pnl_init_cmd __tc_pnl_init_seq[] = {
408
- {0x01, 0, NULL, 150}, // SWRESET
409
- {0xF0, 1, __tc_pnl_i1, 0}, // unlock manufacturer
410
- {0xF0, 1, __tc_pnl_i2, 0},
411
- {0xC5, 1, __tc_pnl_i3, 0}, // VCOM control
412
- {0x36, 1, __tc_pnl_i4, 0}, // MADCTL 0x28: MV (rotation 1) + BGR=1
413
- {0x3A, 1, __tc_pnl_i5, 0}, // COLMOD 0x66 (18-bit, 262K) — clean channel routing
414
- {0xB0, 1, __tc_pnl_i6, 0}, // interface control
415
- {0xB4, 1, __tc_pnl_i7, 0}, // inversion control
416
- {0xB6, 3, __tc_pnl_i8, 0}, // display function control
417
- {0xB7, 1, __tc_pnl_i9, 0}, // entry mode
418
- {0xF0, 1, __tc_pnl_i10, 0}, // lock manufacturer
419
- {0xF0, 1, __tc_pnl_i11, 0},
420
- {0x11, 0, NULL, 150}, // SLPOUT (sleep out — 120ms typical)
421
- {0x29, 0, NULL, 150}, // DISPON
422
- {0x20, 0, NULL, 0}, // INVOFF (non-inverted at power-on)
423
- };
424
- `;
425
-
426
- const functions = `
427
- // ── Direct panel transport ──────────────────────────────────────────────
428
- // GPIOs: CS/DC/RST driven manually; CS stays LOW for the whole command+data
429
- // burst (DC toggles between command byte and parameters/pixels), matching the
430
- // Adafruit ST77xx protocol this panel requires. The SPI config carries no CS
431
- // (cs_is_gpio = false -> the ESP32 driver's hardware CSEL pin is left idle;
432
- // it is not connected to the panel).
433
- static const struct gpio_dt_spec __tc_pnl_cs = GPIO_DT_SPEC_GET(DT_NODELABEL(${bus}), cs_gpios);
434
- static const struct gpio_dt_spec __tc_pnl_dc = GPIO_DT_SPEC_GET(DT_NODELABEL(${bridge}), dc_gpios);
435
- static const struct gpio_dt_spec __tc_pnl_rst = GPIO_DT_SPEC_GET(DT_NODELABEL(${bridge}), reset_gpios);
436
-
437
- // 8-bit frames for commands/parameters and ${isIli9341 ? '16-bit (2 bytes/pixel)' : '18-bit (3 bytes/pixel)'} pixel data.
438
- static struct spi_config __tc_pnl_cfg8 = {
439
- .frequency = DT_PROP(DT_NODELABEL(${dtLabel}), mipi_max_frequency),
440
- .operation = SPI_OP_MODE_MASTER | SPI_WORD_SET(8),
441
- .slave = 0,
442
- };
443
-
444
- // Assert CS (active=low) unless a startWrite/endWrite batch already holds it.
445
- static inline void __tc_pnl_cs_assert(void) {
446
- if (__tc_pnl_write_depth == 0U) { gpio_pin_set_dt(&__tc_pnl_cs, 1); }
447
- }
448
- // Deassert CS unless a startWrite/endWrite batch is still holding it.
449
- static inline void __tc_pnl_cs_release(void) {
450
- if (__tc_pnl_write_depth == 0U) { gpio_pin_set_dt(&__tc_pnl_cs, 0); }
451
- }
452
-
453
- // Write one command byte (DC low) + its parameters (DC high). CS is asserted
454
- // for the burst unless an outer startWrite is already holding it.
455
- static void __tc_pnl_cmd(uint8_t cmd, const uint8_t* data, uint16_t len) {
456
- struct spi_buf __bc = { &cmd, 1 };
457
- struct spi_buf_set __sc = { &__bc, 1 };
458
- __tc_pnl_cs_assert();
459
- gpio_pin_set_dt(&__tc_pnl_dc, 0);
460
- (void)spi_write(DEVICE_DT_GET(DT_NODELABEL(${bus})), &__tc_pnl_cfg8, &__sc);
461
- if (len > 0) {
462
- struct spi_buf __bd = { const_cast<uint8_t*>(data), len };
463
- struct spi_buf_set __sd = { &__bd, 1 };
464
- gpio_pin_set_dt(&__tc_pnl_dc, 1);
465
- (void)spi_write(DEVICE_DT_GET(DT_NODELABEL(${bus})), &__tc_pnl_cfg8, &__sd);
466
- }
467
- __tc_pnl_cs_release();
468
- }
469
-
470
- // Open a RAMWR burst: CS low, RAMWR command, DC high for the pixel data.
471
- static void __tc_pnl_ramwr_begin(void) {
472
- uint8_t __ramwr = 0x2C;
473
- struct spi_buf __bc = { &__ramwr, 1 };
474
- struct spi_buf_set __sc = { &__bc, 1 };
475
- __tc_pnl_cs_assert();
476
- gpio_pin_set_dt(&__tc_pnl_dc, 0);
477
- (void)spi_write(DEVICE_DT_GET(DT_NODELABEL(${bus})), &__tc_pnl_cfg8, &__sc);
478
- gpio_pin_set_dt(&__tc_pnl_dc, 1);
479
- }
480
-
481
- static void __tc_pnl_ramwr_end(void) { __tc_pnl_cs_release(); }
482
-
483
- // Read ST7796S/ILI9341 GET_SCANLINE (0x45). These controllers return one
484
- // dummy byte followed by the 16-bit scanline. Some modules do not implement
485
- // readback correctly; 0xFFFF means "unknown" and disables waiting for that
486
- // update rather than stalling or corrupting the frame.
487
- static uint16_t __tc_pnl_read_scanline(void) {
488
- if (!__tc_pnl_scanline_sync) return 0xFFFFu;
489
- uint8_t __cmd = 0x45;
490
- uint8_t __tx[3] = {0, 0, 0};
491
- uint8_t __rx[3] = {0, 0, 0};
492
- struct spi_buf __bc = { &__cmd, 1 };
493
- struct spi_buf_set __sc = { &__bc, 1 };
494
- struct spi_buf __bt = { __tx, sizeof(__tx) };
495
- struct spi_buf __br = { __rx, sizeof(__rx) };
496
- struct spi_buf_set __st = { &__bt, 1 };
497
- struct spi_buf_set __sr = { &__br, 1 };
498
- __tc_pnl_cs_assert();
499
- gpio_pin_set_dt(&__tc_pnl_dc, 0);
500
- int __cmd_err = spi_write(DEVICE_DT_GET(DT_NODELABEL(${bus})), &__tc_pnl_cfg8, &__sc);
501
- int __read_err = 0;
502
- if (__cmd_err == 0) {
503
- gpio_pin_set_dt(&__tc_pnl_dc, 1);
504
- __read_err = spi_transceive(DEVICE_DT_GET(DT_NODELABEL(${bus})), &__tc_pnl_cfg8, &__st, &__sr);
505
- }
506
- __tc_pnl_cs_release();
507
- if (__cmd_err != 0 || __read_err != 0) return 0xFFFFu;
508
- uint16_t __line = static_cast<uint16_t>((static_cast<uint16_t>(__rx[1]) << 8) | __rx[2]);
509
- return (__line < ${h}) ? __line : 0xFFFFu;
510
- }
511
-
512
- // Wait until the panel has scanned past a dirty rectangle. This is intentionally
513
- // bounded: a broken/floating SDO must never hang the UI loop. Full-height writes
514
- // have no safe post-rectangle interval, so they retain the normal single-burst
515
- // behavior; the caller's framebuffer still prevents intermediate software frames.
516
- static void __tc_pnl_wait_for_safe_rect(int16_t y, int16_t rh) {
517
- #if __TC_TE_SYNC
518
- // TE variant: arm on the next frame pulse, then start the burst — the write
519
- // chases the scan beam from the top of the rect. Bounded so a stuck TE line
520
- // can never hang the UI loop.
521
- if (rh < 8 || y < 0) return;
522
- uint32_t __was = __tc_te_count;
523
- uint32_t __deadline = k_uptime_get_32() + 25U;
524
- while (__tc_te_count == __was) {
525
- if (static_cast<int32_t>(k_uptime_get_32() - __deadline) >= 0) break;
526
- k_msleep(0);
527
- }
528
- return;
529
- #endif
530
- if (!__tc_pnl_scanline_sync || rh < 8 || y < 0) return;
531
- int16_t __last = static_cast<int16_t>(y + rh - 1);
532
- if (__last >= static_cast<int16_t>(${h} - 2)) return;
533
- uint32_t __deadline = k_uptime_get_32() + 20U;
534
- for (;;) {
535
- uint16_t __line = __tc_pnl_read_scanline();
536
- if (__line == 0xFFFFu || (__line > static_cast<uint16_t>(__last + 2) && __line < static_cast<uint16_t>(${h} - 2))) return;
537
- if (static_cast<int32_t>(k_uptime_get_32() - __deadline) >= 0) return;
538
- k_msleep(0);
539
- }
540
- }
541
- ${pixelHelpers}
542
- // Set the address window. Coordinates are in the effective (rotated) UI
543
- // space; the panel's MADCTL (rotation 1: MV) maps them onto the native
544
- // 320x480 raster, so CASET/RASET take the UI x/y ranges directly.
545
- static void __tc_pnl_set_window(int16_t x, int16_t y, int16_t winW, int16_t winH) {
546
- __tc_pnl_wait_for_safe_rect(y, winH);
547
- uint16_t __x0 = static_cast<uint16_t>(x);
548
- uint16_t __x1 = static_cast<uint16_t>(x + winW - 1);
549
- uint16_t __y0 = static_cast<uint16_t>(y);
550
- uint16_t __y1 = static_cast<uint16_t>(y + winH - 1);
551
- uint8_t __ca[4] = { static_cast<uint8_t>(__x0 >> 8), static_cast<uint8_t>(__x0),
552
- static_cast<uint8_t>(__x1 >> 8), static_cast<uint8_t>(__x1) };
553
- uint8_t __ra[4] = { static_cast<uint8_t>(__y0 >> 8), static_cast<uint8_t>(__y0),
554
- static_cast<uint8_t>(__y1 >> 8), static_cast<uint8_t>(__y1) };
555
- __tc_pnl_cmd(0x2A, __ca, 4);
556
- __tc_pnl_cmd(0x2B, __ra, 4);
557
- }
558
-
559
- // ── Panel-ops consumed by CuttlefishGFX ─────────────────────────────────
560
- // startWrite/endWrite batch multiple primitives under one CS-asserted burst
561
- // (Adafruit ST77xx protocol: CS held low across the burst, DC toggles
562
- // mid-burst). A depth counter supports nested startWrite calls — the runtime
563
- // sometimes wraps a canvas push inside an outer transaction. Only the
564
- // outermost startWrite asserts CS and the outermost endWrite releases it;
565
- // inner ones just bump the depth. This collapses per-primitive CS-toggle
566
- // overhead (one transaction per burst instead of one per rect/glyph row).
567
- static void __tc_op_startWrite(void* /*ctx*/) {
568
- if (__tc_pnl_write_depth == 0U) { gpio_pin_set_dt(&__tc_pnl_cs, 1); }
569
- __tc_pnl_write_depth++;
570
- }
571
- static void __tc_op_endWrite(void* /*ctx*/) {
572
- if (__tc_pnl_write_depth > 0U) {
573
- __tc_pnl_write_depth--;
574
- if (__tc_pnl_write_depth == 0U) { gpio_pin_set_dt(&__tc_pnl_cs, 0); }
575
- }
576
- }
577
-
578
- // Stash the target rect. The runtime always follows this with writePixels
579
- // delivering exactly (w*h) pixels for this rect, OR fillRect/writePixel which
580
- // ignore the stash.
581
- static void __tc_op_setAddrWindow(void* /*ctx*/, int16_t x, int16_t y, int16_t winW, int16_t winH) {
582
- __tc_aw_x = x; __tc_aw_y = y; __tc_aw_w = winW; __tc_aw_h = winH;
583
- }
584
-
585
- // Push the stashed rect's worth of rgb565 pixels. Called right after
586
- // setAddrWindow with exactly (aw_w * aw_h) pixels. The caller's buffer is
587
- // never mutated (scroll canvases persist across frames), so pixels are
588
- // converted in chunks through the row${bpp} scratch buffer.
589
- static void __tc_op_writePixels(void* /*ctx*/, const uint16_t* px, uint32_t n) {
590
- if (n == 0U) return;
591
- __tc_pnl_set_window(__tc_aw_x, __tc_aw_y, __tc_aw_w, __tc_aw_h);
592
- __tc_pnl_ramwr_begin();
593
- ${pixelsFn}(px, n);
594
- __tc_pnl_ramwr_end();
595
- }
596
-
597
- // Single pixel: write a 1x1 rect.
598
- static void __tc_op_writePixel(void* /*ctx*/, int16_t x, int16_t y, uint16_t c) {
599
- uint16_t __c = c;
600
- __tc_pnl_set_window(x, y, 1, 1);
601
- __tc_pnl_ramwr_begin();
602
- ${pixelsFn}(&__c, 1);
603
- __tc_pnl_ramwr_end();
604
- }
605
- ${fillRectFn}
606
- static int16_t __tc_op_width(void* /*ctx*/) { return ${w}; }
607
- static int16_t __tc_op_height(void* /*ctx*/) { return ${h}; }
608
-
609
- // flush is nullptr — direct-mode panel (TFT), no backing store to push.
610
- struct CuttlefishPanelOps;
611
- extern const CuttlefishPanelOps __tc_display_ops;
612
-
613
- const CuttlefishPanelOps __tc_display_ops = {
614
- __tc_op_startWrite,
615
- __tc_op_endWrite,
616
- __tc_op_setAddrWindow,
617
- __tc_op_writePixels,
618
- __tc_op_writePixel,
619
- __tc_op_fillRect,
620
- __tc_op_width,
621
- __tc_op_height,
622
- nullptr, // flush — direct-mode panel
623
- };
624
-
625
- // The live display target: a CuttlefishGFX driven by the panel-ops vtable.
626
- CuttlefishGFX __tc_display(&__tc_display_ops, nullptr);
627
- ${initBlock}
628
- // ── display_init (called from setup) ────────────────────────────────────
629
-
630
- static inline void display_init() {
631
- printk("TC_DISPLAY: device ready\\n");
632
- ${teInit}
633
- ${blInit}
634
- gpio_pin_configure_dt(&__tc_pnl_cs, GPIO_OUTPUT);
635
- gpio_pin_configure_dt(&__tc_pnl_dc, GPIO_OUTPUT);
636
- gpio_pin_configure_dt(&__tc_pnl_rst, GPIO_OUTPUT);
637
- // Hardware reset pulse (Adafruit init behavior).
638
- gpio_pin_set_dt(&__tc_pnl_rst, 1);
639
- k_msleep(20);
640
- gpio_pin_set_dt(&__tc_pnl_rst, 0);
641
- k_msleep(20);
642
- // Adafruit init sequence, direct protocol.
643
- for (uint32_t i = 0; i < (sizeof(__tc_pnl_init_seq) / sizeof(__tc_pnl_init_seq[0])); i++) {
644
- __tc_pnl_cmd(__tc_pnl_init_seq[i].cmd, __tc_pnl_init_seq[i].data,
645
- __tc_pnl_init_seq[i].len);
646
- if (__tc_pnl_init_seq[i].delay_ms > 0) k_msleep(__tc_pnl_init_seq[i].delay_ms);
647
- }
648
- __tc_op_fillRect(nullptr, 0, 0, ${w}, ${h}, 0x0000);
649
- printk("TC_DISPLAY: direct init done (${wireTag}, black fill)\\n");
650
- }
651
-
652
- static inline void display_fillScreen(UI_COLOR_T color) {
653
- __tc_op_fillRect(nullptr, 0, 0, ${w}, ${h}, static_cast<uint16_t>(color));
654
- }
655
- static inline CuttlefishDisplayTarget* display_defaultTarget() { return &__tc_display; }
656
- static inline int16_t display_width() { return ${w}; }
657
- static inline int16_t display_height() { return ${h}; }
658
-
659
- static inline void display_startWrite() { __tc_op_startWrite(nullptr); }
660
- static inline void display_endWrite() { __tc_op_endWrite(nullptr); }
661
- static inline void display_setAddrWindow(int16_t x, int16_t y, int16_t winW, int16_t winH) {
662
- __tc_op_setAddrWindow(nullptr, x, y, winW, winH);
663
- }
664
- static inline void display_writePixels(uint16_t* pixels, uint32_t count) {
665
- __tc_op_writePixels(nullptr, pixels, count);
666
- }
667
-
668
- // ── Canvas lifecycle + accessors (offscreen rgb565 compositing) ──────────
669
- // Allocate the canvas object via malloc + placement-new (not operator new).
670
- // Under CONFIG_REQUIRES_FULL_LIBCPP without CONFIG_CPP_EXCEPTIONS, operator new
671
- // throws std::bad_alloc on OOM and the nothrow wrapper's internal catch cannot
672
- // unwind (no EH runtime) → std::terminate → abort. malloc returns NULL on
673
- // failure with no exception path; placement-new then constructs the object in
674
- // place (vtable included). display_deleteCanvas mirrors with an explicit dtor
675
- // + free. The runtime's callers already null-check the return, so an OOM
676
- // degrades gracefully instead of aborting.
677
- static inline CuttlefishCanvas16* display_createCanvas(int16_t cw, int16_t ch) {
678
- void* mem = malloc(sizeof(CuttlefishCanvas16));
679
- if (!mem) return nullptr;
680
- return new (mem) CuttlefishCanvas16(cw, ch);
681
- }
682
- // psramFound() shim: the Arduino ESP32 core provides this, but Zephyr does not.
683
- // Under BOARD_HAS_PSRAM the ESP heap serves PSRAM, so report it as present. The
684
- // runtime's ui_create_canvas_best calls this under #if defined(BOARD_HAS_PSRAM).
685
- #if defined(BOARD_HAS_PSRAM) && !defined(psramFound)
686
- #include <zephyr/multi_heap/shared_multi_heap.h>
687
- static inline bool psramFound() {
688
- // Compile-time truth: if BOARD_HAS_PSRAM is defined, the build targets a
689
- // PSRAM board with CONFIG_ESP_SPIRAM enabled (the framework emits both).
690
- return true;
691
- }
692
- // Allocate from PSRAM via Zephyr's shared multi-heap (the ESP32 SoC code
693
- // registers PSRAM as an SMH_REG_ATTR_EXTERNAL region at boot).
694
- static inline void* ui_psram_malloc(size_t bytes) {
695
- return shared_multi_heap_alloc(SMH_REG_ATTR_EXTERNAL, bytes);
696
- }
697
- #endif
698
- static inline CuttlefishCanvas16* display_createCanvasPsram(int16_t cw, int16_t ch) {
699
- // Allocate the pixel buffer in PSRAM (large: w*h*2 bytes) and the small
700
- // canvas object in SRAM. The canvas takes ownership of the PSRAM buffer and
701
- // frees it via free() in its dtor (the SMH allocator's free is compatible
702
- // with the standard k_free/free path). Returns nullptr if PSRAM isn't
703
- // available or the allocation fails — ui_create_canvas_best falls back.
704
- #if defined(BOARD_HAS_PSRAM)
705
- if ((cw > 0) && (ch > 0)) {
706
- size_t bytes = static_cast<size_t>(cw) * static_cast<size_t>(ch) * sizeof(uint16_t);
707
- uint16_t* psramBuf = static_cast<uint16_t*>(ui_psram_malloc(bytes));
708
- if (psramBuf) {
709
- void* mem = malloc(sizeof(CuttlefishCanvas16));
710
- if (mem) {
711
- return new (mem) CuttlefishCanvas16(cw, ch, psramBuf, 1);
712
- }
713
- free(psramBuf);
714
- }
715
- }
716
- #else
717
- (void)cw; (void)ch;
718
- #endif
719
- return nullptr;
720
- }
721
- static inline void display_deleteCanvas(CuttlefishCanvas16* canvas) {
722
- if (!canvas) return;
723
- canvas->~CuttlefishCanvas16();
724
- free(canvas);
725
- }
726
- static inline int16_t display_canvasWidth(CuttlefishCanvas16* c) { return c->width(); }
727
- static inline int16_t display_canvasHeight(CuttlefishCanvas16* c) { return c->height(); }
728
- static inline uint16_t* display_canvasBuffer(CuttlefishCanvas16* c) { return c->getBuffer(); }
729
- static inline uint16_t display_canvasGetPixel(CuttlefishCanvas16* c, int16_t x, int16_t y) { return c->getPixel(x, y); }
730
- static inline void display_canvasFillScreen(CuttlefishCanvas16* c, UI_COLOR_T color) { c->fillScreen(static_cast<uint16_t>(color)); }
731
- static inline void display_canvasFillRect(CuttlefishCanvas16* c, int16_t x, int16_t y, int16_t cw, int16_t ch, UI_COLOR_T color) {
732
- c->fillRect(x, y, cw, ch, static_cast<uint16_t>(color));
733
- }
734
-
735
- // ── Target-polymorphic draw (panel or canvas via CuttlefishDisplayTarget*) ─
736
- static inline void display_targetDrawPixel(CuttlefishDisplayTarget* t, int16_t x, int16_t y, UI_COLOR_T color) { t->drawPixel(x, y, static_cast<uint16_t>(color)); }
737
- static inline int16_t display_targetWidth(CuttlefishDisplayTarget* t) { return t->width(); }
738
- static inline int16_t display_targetHeight(CuttlefishDisplayTarget* t) { return t->height(); }
739
- static inline void display_targetDrawRGBBitmap(CuttlefishDisplayTarget* t, int16_t x, int16_t y, const uint16_t* bitmap, int16_t bw, int16_t bh) {
740
- t->drawRGBBitmap(x, y, bitmap, bw, bh);
741
- }
742
- static inline void display_targetFillRect(CuttlefishDisplayTarget* t, int16_t x, int16_t y, int16_t bw, int16_t bh, UI_COLOR_T color) { t->fillRect(x, y, bw, bh, static_cast<uint16_t>(color)); }
743
- static inline void display_targetDrawFastHLine(CuttlefishDisplayTarget* t, int16_t x, int16_t y, int16_t bw, UI_COLOR_T color) { t->drawFastHLine(x, y, bw, static_cast<uint16_t>(color)); }
744
- static inline void display_targetDrawFastVLine(CuttlefishDisplayTarget* t, int16_t x, int16_t y, int16_t bh, UI_COLOR_T color) { t->drawFastVLine(x, y, bh, static_cast<uint16_t>(color)); }
745
- static inline void display_targetFillRoundRect(CuttlefishDisplayTarget* t, int16_t x, int16_t y, int16_t bw, int16_t bh, int16_t r, UI_COLOR_T color) { t->fillRoundRect(x, y, bw, bh, r, static_cast<uint16_t>(color)); }
746
- static inline void display_targetDrawRect(CuttlefishDisplayTarget* t, int16_t x, int16_t y, int16_t bw, int16_t bh, UI_COLOR_T color) { t->drawRect(x, y, bw, bh, static_cast<uint16_t>(color)); }
747
- static inline void display_targetDrawRoundRect(CuttlefishDisplayTarget* t, int16_t x, int16_t y, int16_t bw, int16_t bh, int16_t r, UI_COLOR_T color) { t->drawRoundRect(x, y, bw, bh, r, static_cast<uint16_t>(color)); }
748
- static inline void display_targetDrawLine(CuttlefishDisplayTarget* t, int16_t x0, int16_t y0, int16_t x1, int16_t y1, UI_COLOR_T color) { t->drawLine(x0, y0, x1, y1, static_cast<uint16_t>(color)); }
749
- static inline void display_targetFillCircle(CuttlefishDisplayTarget* t, int16_t x, int16_t y, int16_t r, UI_COLOR_T color) { t->fillCircle(x, y, r, static_cast<uint16_t>(color)); }
750
- static inline void display_targetDrawCircle(CuttlefishDisplayTarget* t, int16_t x, int16_t y, int16_t r, UI_COLOR_T color) { t->drawCircle(x, y, r, static_cast<uint16_t>(color)); }
751
- static inline void display_targetSetCursor(CuttlefishDisplayTarget* t, int16_t x, int16_t y) { t->setCursor(x, y); }
752
- static inline void display_targetSetTextColor(CuttlefishDisplayTarget* t, UI_COLOR_T fg) { t->setTextColor(static_cast<uint16_t>(fg)); }
753
- static inline void display_targetSetTextColorBg(CuttlefishDisplayTarget* t, UI_COLOR_T fg, UI_COLOR_T bg) { t->setTextColor(static_cast<uint16_t>(fg), static_cast<uint16_t>(bg)); }
754
- static inline void display_targetSetTextSize(CuttlefishDisplayTarget* t, uint8_t s) { t->setTextSize(s); }
755
- static inline void display_targetSetTextWrap(CuttlefishDisplayTarget* t, bool wp) { t->setTextWrap(wp); }
756
- static inline void display_targetPrint(CuttlefishDisplayTarget* t, const char* s) { t->print(s); }
757
- `;
758
-
759
- return { includes, declaration, functions };
760
- }
761
-
762
- /**
763
- * Strategy-owned display adapter generator. Receives the resolved display and
764
- * looks up the Zephyr DT-binding profile by driver id. Returns undefined for
765
- * unknown drivers so resolveDisplayAdapter can decline and
766
- * generateDisplayAdapter surfaces a clear error.
767
- */
768
- export const zephyrDisplayAdapterGenerator: DisplayAdapterGenerator = (display) => {
769
- const profile = ZEPHYR_DISPLAY_PROFILES[display.driver];
770
- if (!profile) return undefined as unknown as DisplayAdapterCode;
771
- // The UI adapter is RGB565/SPI (TFT) only. Monochrome panels (OLED) use the
772
- // direct display.* GFX runtime (gfx.ts mono branch) — there is no
773
- // CuttlefishGFX UI rendering path for mono. Decline so cuttlefish does not
774
- // emit an incompatible RGB565 adapter for a mono profile.
775
- if (profile.colorFormat === 'mono') return undefined as unknown as DisplayAdapterCode;
776
- return zephyrUiDisplayAdapter(profile, {
777
- scanlineSync: display.scanlineSync,
778
- miso: display.spiPins?.miso,
779
- tearingEffectPin: (display as { tearingEffectPin?: number }).tearingEffectPin,
780
- });
781
- };
1
+ // ---------------------------------------------------------------------------
2
+ // Zephyr UI display adapter for the TypeCAD UI rendering pipeline.
3
+ //
4
+ // Bridges the in-tree CuttlefishGFX/CuttlefishCanvas16 class (emitted by the
5
+ // runtime header's cuttlefish-gfx slice) to an SPI TFT panel. The panel is
6
+ // driven DIRECTLY over the SPI controller with GPIO chip-select, DC and reset
7
+ // pins — NOT through Zephyr's mipi-dbi-spi bridge: that bridge issues separate
8
+ // SPI transactions for the command byte and its parameters (CS deasserts
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.
22
+ //
23
+ // This is the Zephyr analog of the Adafruit adapters in framework-arduino,
24
+ // using the in-tree native GFX class (no #define CuttlefishCanvas16) driven
25
+ // through a CuttlefishPanelOps function-pointer vtable.
26
+ //
27
+ // EMIT BOUNDARY: emitted bytes land in user firmware. Covered by the TypeCAD
28
+ // Runtime Exception (RUNTIME_EXCEPTION.md at the repo root).
29
+ //
30
+ // AGENTS.md rendering guardrails: the line buffer is one row (reused, not
31
+ // per-frame allocated); fillRect writes row-by-row. Pixel bursts use 16-bit
32
+ // SPI words (MSB-first on the wire = RGB565 big-endian, no byte swap).
33
+ // ---------------------------------------------------------------------------
34
+
35
+ import type { DisplayAdapterCode, DisplayAdapterGenerator } from "@typecad/cuttlefish/api/shared";
36
+ import { ZEPHYR_DISPLAY_PROFILES, panelControllerFor } from "./profiles.js";
37
+ import type { ZephyrDisplayProfile } from "./profiles.js";
38
+
39
+ /**
40
+ * Build the Zephyr UI display adapter for a profile. Emits the full
41
+ * display_ / display_target_ / display_canvas_ surface the UI runtime expects,
42
+ * driving the panel directly via SPI (CS held across command+data).
43
+ */
44
+ export interface ZephyrDisplayReadbackOptions {
45
+ /** Enable the controller GET_SCANLINE dirty-rectangle experiment. */
46
+ scanlineSync?: boolean;
47
+ /** MISO/SDO GPIO; readback is disabled when it is not explicitly wired. */
48
+ miso?: number;
49
+ /** Tearing-effect GPIO (panel TE output). When set, panel updates wait for
50
+ * the TE frame pulse instead of GET_SCANLINE readback — no MISO required.
51
+ * The overlay adds te-gpios to the display DT node from this. */
52
+ tearingEffectPin?: number;
53
+ }
54
+
55
+ export function zephyrUiDisplayAdapter(
56
+ profile: ZephyrDisplayProfile,
57
+ readback: ZephyrDisplayReadbackOptions = {},
58
+ ): DisplayAdapterCode {
59
+ const w = profile.width;
60
+ const h = profile.height;
61
+ const maxDim = Math.max(w, h);
62
+ const dtLabel = profile.dtLabel;
63
+ const backlightAlias = profile.backlight;
64
+ const bus = profile.busLabel ?? 'spi2';
65
+ const bridge = profile.bridgeLabel ?? 'mipi_dbi';
66
+ const controller = panelControllerFor(profile);
67
+ const isIli9341 = controller === 'ili9341';
68
+ // Bytes per pixel on the wire: 3 (18-bit 666) for ST7796S, 2 (16-bit 565)
69
+ // for ILI9341. The row/block scratch buffers and pack loops key off this.
70
+ const bpp = isIli9341 ? 2 : 3;
71
+ const rowBuf = `__tc_display_row${bpp}`;
72
+ const blockBuf = `__tc_display_block${bpp}`;
73
+ const packFn = isIli9341 ? '__tc_pnl_pack565' : '__tc_pnl_pack666';
74
+ const pixelsFn = isIli9341 ? '__tc_pnl_pixels565' : '__tc_pnl_pixels666';
75
+ const wireTag = isIli9341 ? '16-bit 565' : '18-bit';
76
+ // Never infer readback from a board's default pinmux. SDO/MISO may be left
77
+ // floating or shared with another device, and ST7796S modules are known to
78
+ // react badly to GSCAN reads. Both an explicit opt-in and an explicit MISO
79
+ // pin are required before emitting an active synchronization path.
80
+ const scanlineSync = readback.scanlineSync === true && readback.miso !== undefined;
81
+ // TE (hardware tearing-effect) sync: strictly opt-in via a configured GPIO.
82
+ // Preferred over GET_SCANLINE when wired — no readback traffic, no MISO.
83
+ const tePin = typeof readback.tearingEffectPin === 'number' ? readback.tearingEffectPin : undefined;
84
+
85
+ const includes = [
86
+ `// --- Zephyr UI display adapter (${profile.driver}) ---`,
87
+ `// Native CuttlefishGFX path: do NOT #define CuttlefishCanvas16 so the`,
88
+ `// runtime header emits the in-tree CuttlefishGFX/CuttlefishCanvas16 class.`,
89
+ `#define CuttlefishDisplayTarget CuttlefishGFX`,
90
+ `#include <zephyr/kernel.h>`,
91
+ `#include <zephyr/drivers/display.h>`,
92
+ `#include <zephyr/drivers/spi.h>`,
93
+ `#include <zephyr/drivers/gpio.h>`,
94
+ ].join("\n");
95
+
96
+ // Per-controller scratch-buffer declarations. The ST7796S text is frozen as
97
+ // hardware-verified; the ILI9341 variant documents its 565 wire format.
98
+ const bufferDecls = isIli9341
99
+ ? [
100
+ `// One-row scratch buffer in 16-bit (565) wire format: maxDim px x 2 bytes.`,
101
+ `// Reused across fillRect/draw calls — never per-frame (AGENTS.md: no`,
102
+ `// per-frame heap allocation). The ILI9341 is driven in its native`,
103
+ `// 16-bit COLMOD (0x55): RGB565 pixels go out big-endian (MSB-first SPI),`,
104
+ `// the Adafruit ILI9341 convention, with no repacking needed.`,
105
+ `static uint8_t ${rowBuf}[${maxDim} * 2];`,
106
+ `// Multi-row block buffer for solid fills (8 rows of maxDim px in 565).`,
107
+ `// fillRect fills this once with the color, then sends the whole rect in a`,
108
+ `// few large spi_write chunks instead of one spi_write per row — a full`,
109
+ `// 480x320 clear went from ~320 syscalls (~110ms) to ~40 (~15ms). Reused,`,
110
+ `// not per-frame (AGENTS.md).`,
111
+ `#define __TC_FILL_ROWS 8`,
112
+ `static uint8_t ${blockBuf}[${maxDim} * 2 * __TC_FILL_ROWS];`,
113
+ ]
114
+ : [
115
+ `// One-row scratch buffer in 18-bit (666) wire format: maxDim px x 3 bytes.`,
116
+ `// Reused across fillRect/draw calls — never per-frame (AGENTS.md: no`,
117
+ `// per-frame heap allocation). The panel is driven in 18-bit mode (COLMOD`,
118
+ `// 0x66): its 16-bit (565) channel routing is crossed (G/B swap, verified`,
119
+ `// with calibration bands), while 18-bit mode routes every channel`,
120
+ `// correctly with plain (R,G,B) byte order.`,
121
+ `static uint8_t ${rowBuf}[${maxDim} * 3];`,
122
+ `// Multi-row block buffer for solid fills (8 rows of maxDim px in 18-bit).`,
123
+ `// fillRect fills this once with the color, then sends the whole rect in a`,
124
+ `// few large spi_write chunks instead of one spi_write per row — a full`,
125
+ `// 480x320 clear went from ~320 syscalls (~110ms) to ~40 (~15ms). Reused,`,
126
+ `// not per-frame (AGENTS.md).`,
127
+ `#define __TC_FILL_ROWS 8`,
128
+ `static uint8_t ${blockBuf}[${maxDim} * 3 * __TC_FILL_ROWS];`,
129
+ ];
130
+
131
+ const declaration = [
132
+ `// CUTTLEFISH_DISPLAY_BEGIN`,
133
+ // The display0 DT node carries frequency/dimensions for DT_PROP reads, but
134
+ // the panel is driven directly via spi_write — no Zephyr display device is
135
+ // needed. Using a constant 1 avoids the ST7796S driver binding (which
136
+ // allocates a tearing-effect GPIO interrupt that conflicts with the SPI/I2C
137
+ // interrupts — the VECDESC_FL_SHARED assertion crash on the 3rd frame).
138
+ `#define __tc_display_dev 1`,
139
+ ...bufferDecls,
140
+ `// startWrite/endWrite batching depth. When > 0 the panel CS is held asserted`,
141
+ `// (low) across multiple primitives — DC still toggles mid-burst, but CS does`,
142
+ `// not, matching the Adafruit ST77xx protocol and avoiding one full CS-toggle`,
143
+ `// SPI transaction per primitive. The runtime brackets whole canvas pushes`,
144
+ `// and per-node repaints in display_startWrite/endWrite pairs.`,
145
+ `static uint8_t __tc_pnl_write_depth = 0;`,
146
+ `// Scanline readback is deliberately opt-in. A verified SDO/MISO wire and`,
147
+ `// controller-specific validation are required; otherwise the display stays`,
148
+ `// on the existing retained/composited path with no extra SPI reads.`,
149
+ `static const bool __tc_pnl_scanline_sync = ${scanlineSync ? 'true' : 'false'};`,
150
+ `// Tearing-effect (TE) hardware sync: the panel pulses its TE line once`,
151
+ `// per frame. When te-gpios is present on the display DT node, panel`,
152
+ `// updates arm on the TE edge — tear-free writes with no MISO readback.`,
153
+ `#if DT_NODE_HAS_PROP(DT_NODELABEL(${dtLabel}), te_gpios)`,
154
+ `#define __TC_TE_SYNC 1`,
155
+ `static const struct gpio_dt_spec __tc_te =`,
156
+ ` GPIO_DT_SPEC_GET(DT_NODELABEL(${dtLabel}), te_gpios);`,
157
+ `static struct gpio_callback __tc_te_cb;`,
158
+ `static volatile uint32_t __tc_te_count = 0;`,
159
+ `static void __tc_te_isr(const struct device* port, struct gpio_callback* cb, uint32_t pins) {`,
160
+ ` (void)port; (void)cb; (void)pins;`,
161
+ ` __tc_te_count++;`,
162
+ `}`,
163
+ `#else`,
164
+ `#define __TC_TE_SYNC 0`,
165
+ `#endif`,
166
+ `// Stashed address window from the last setAddrWindow call. The runtime`,
167
+ `// calls setAddrWindow + writePixels as a matched pair, so we stash the rect`,
168
+ `// here and consume it in writePixels.`,
169
+ `static int16_t __tc_aw_x = 0;`,
170
+ `static int16_t __tc_aw_y = 0;`,
171
+ `static int16_t __tc_aw_w = ${w};`,
172
+ `static int16_t __tc_aw_h = ${h};`,
173
+ `// CUTTLEFISH_DISPLAY_END`,
174
+ ].join("\n");
175
+
176
+ // Backlight: drive it as a raw GPIO output. The DT alias points at a
177
+ // gpio-leds node whose 'gpios' property is a phandle to the GPIO controller
178
+ // + pin. GPIO_DT_SPEC_GET resolves that phandle into a gpio_dt_spec. The
179
+ // overlay only emits this alias when a backlight GPIO is configured, so guard
180
+ // with DT_HAS_ALIAS (the safe primitive for an alias that may be absent —
181
+ // DT_NODE_HAS_STATUS(DT_ALIAS(...)) is version-dependent when the alias is
182
+ // missing and can fail the build).
183
+ // TE pin: input + rising-edge interrupt (the ST7796 TE pulse), then tell
184
+ // the controller to drive the line (TEON 0x35, mode 1 = vertical sync only).
185
+ const teInit = tePin !== undefined ? `#if __TC_TE_SYNC
186
+ if (device_is_ready(__tc_te.port)) {
187
+ gpio_pin_configure_dt(&__tc_te, GPIO_INPUT);
188
+ gpio_init_callback(&__tc_te_cb, __tc_te_isr, BIT(__tc_te.pin));
189
+ (void)gpio_add_callback(__tc_te.port, &__tc_te_cb);
190
+ (void)gpio_pin_interrupt_configure_dt(&__tc_te, GPIO_INT_EDGE_RISING);
191
+ __tc_pnl_cmd1(0x35, 0x01); // TEON: TE output = vsync pulse
192
+ }
193
+ #endif` : '';
194
+
195
+ const blInit = backlightAlias
196
+ ? `#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`
197
+ : '';
198
+
199
+ // Per-controller pixel pack/stream helpers. Both share the row-chunked
200
+ // transport; only the per-pixel wire encoding differs.
201
+ const pixelHelpers = isIli9341
202
+ ? `
203
+ // Pack count rgb565 pixels into the row2 scratch buffer as big-endian 16-bit
204
+ // wire bytes (c>>8, c&0xFF) — the ILI9341's native 565 format under COLMOD
205
+ // 0x55. The caller then streams the buffer with the 8-bit config (one row at
206
+ // a time; the buffer holds maxDim pixels).
207
+ static void ${packFn}(const uint16_t* px, uint32_t count) {
208
+ for (uint32_t i = 0; i < count; i++) {
209
+ uint16_t c = px[i];
210
+ ${rowBuf}[i * 2] = static_cast<uint8_t>(c >> 8);
211
+ ${rowBuf}[i * 2 + 1] = static_cast<uint8_t>(c);
212
+ }
213
+ }
214
+
215
+ // Stream count rgb565 pixels to the panel (chunked through the row2 scratch).
216
+ // Caller holds the RAMWR burst.
217
+ static void ${pixelsFn}(const uint16_t* px, uint32_t count) {
218
+ while (count > 0) {
219
+ uint32_t __chunk = (count > ${maxDim}) ? ${maxDim} : count;
220
+ ${packFn}(px, __chunk);
221
+ struct spi_buf __bd = { ${rowBuf}, static_cast<size_t>(__chunk) * 2U };
222
+ struct spi_buf_set __sd = { &__bd, 1 };
223
+ (void)spi_write(DEVICE_DT_GET(DT_NODELABEL(${bus})), &__tc_pnl_cfg8, &__sd);
224
+ px += __chunk;
225
+ count -= __chunk;
226
+ }
227
+ }
228
+ `
229
+ : `
230
+ // Pack count rgb565 pixels into the row3 scratch buffer as 18-bit (666) wire
231
+ // format: (r<<3, g<<2, b<<3) — R,G,B byte order, verified correct on this
232
+ // panel in 18-bit mode. The caller then streams the buffer with the 8-bit
233
+ // config (one row at a time; the buffer holds maxDim pixels).
234
+ static void ${packFn}(const uint16_t* px, uint32_t count) {
235
+ for (uint32_t i = 0; i < count; i++) {
236
+ uint16_t c = px[i];
237
+ ${rowBuf}[i * 3] = static_cast<uint8_t>((c >> 8) & 0xF8u);
238
+ ${rowBuf}[i * 3 + 1] = static_cast<uint8_t>((c >> 3) & 0xFCu);
239
+ ${rowBuf}[i * 3 + 2] = static_cast<uint8_t>((c << 3) & 0xF8u);
240
+ }
241
+ }
242
+
243
+ // Stream count rgb565 pixels to the panel (converted to 18-bit, chunked
244
+ // through the row3 scratch). Caller holds the RAMWR burst.
245
+ static void ${pixelsFn}(const uint16_t* px, uint32_t count) {
246
+ while (count > 0) {
247
+ uint32_t __chunk = (count > ${maxDim}) ? ${maxDim} : count;
248
+ ${packFn}(px, __chunk);
249
+ struct spi_buf __bd = { ${rowBuf}, static_cast<size_t>(__chunk) * 3U };
250
+ struct spi_buf_set __sd = { &__bd, 1 };
251
+ (void)spi_write(DEVICE_DT_GET(DT_NODELABEL(${bus})), &__tc_pnl_cfg8, &__sd);
252
+ px += __chunk;
253
+ count -= __chunk;
254
+ }
255
+ }
256
+ `;
257
+
258
+ // Per-controller solid-fill. Both tile one built row into the block buffer
259
+ // and stream it in multi-row chunks; only the color encoding differs.
260
+ const fillRectFn = isIli9341
261
+ ? `
262
+ static void __tc_op_fillRect(void* /*ctx*/, int16_t x, int16_t y, int16_t rw, int16_t rh, uint16_t c) {
263
+ if (rw <= 0 || rh <= 0) return;
264
+ uint8_t __b0 = static_cast<uint8_t>(c >> 8);
265
+ uint8_t __b1 = static_cast<uint8_t>(c);
266
+ // Build one row, then tile it into the block buffer.
267
+ for (int16_t i = 0; i < rw; i++) {
268
+ ${rowBuf}[i * 2] = __b0;
269
+ ${rowBuf}[i * 2 + 1] = __b1;
270
+ }
271
+ size_t rowBytes = static_cast<size_t>(rw) * 2U;
272
+ for (int16_t r = 0; r < __TC_FILL_ROWS; r++) {
273
+ memcpy(&${blockBuf}[static_cast<size_t>(r) * rowBytes], ${rowBuf}, rowBytes);
274
+ }
275
+ __tc_pnl_set_window(x, y, rw, rh);
276
+ __tc_pnl_ramwr_begin();
277
+ int16_t remaining = rh;
278
+ while (remaining > 0) {
279
+ int16_t chunk = (remaining > __TC_FILL_ROWS) ? __TC_FILL_ROWS : remaining;
280
+ struct spi_buf __bd = { ${blockBuf}, static_cast<size_t>(chunk) * rowBytes };
281
+ struct spi_buf_set __sd = { &__bd, 1 };
282
+ (void)spi_write(DEVICE_DT_GET(DT_NODELABEL(${bus})), &__tc_pnl_cfg8, &__sd);
283
+ remaining -= chunk;
284
+ }
285
+ __tc_pnl_ramwr_end();
286
+ }
287
+ `
288
+ : `
289
+ // Fill a rect row-by-row using the one-row 18-bit scratch buffer. This is the
290
+ // hot path for background clears and large fills; building the color into the
291
+ // reused buffer and writing each row keeps memory bounded.
292
+ // Fill a rect with a solid color. The 18-bit row is tiled into the block buffer
293
+ // (__TC_FILL_ROWS rows), then the whole rect is sent in multi-row spi_write
294
+ // chunks. A full 480x320 clear is ~40 writes instead of ~320, dropping it from
295
+ // ~110ms to ~15ms — the ESP32 SPI driver's per-transaction overhead (not SPI
296
+ // bandwidth) is the binding cost, so fewer/larger writes win. Scatter-gather
297
+ // descriptor lists tested slower (the driver walks each descriptor), so this
298
+ // uses one contiguous buffer per write.
299
+ static void __tc_op_fillRect(void* /*ctx*/, int16_t x, int16_t y, int16_t rw, int16_t rh, uint16_t c) {
300
+ if (rw <= 0 || rh <= 0) return;
301
+ uint8_t __b0 = static_cast<uint8_t>((c >> 8) & 0xF8u);
302
+ uint8_t __b1 = static_cast<uint8_t>((c >> 3) & 0xFCu);
303
+ uint8_t __b2 = static_cast<uint8_t>((c << 3) & 0xF8u);
304
+ // Build one 18-bit row, then tile it into the block buffer.
305
+ for (int16_t i = 0; i < rw; i++) {
306
+ ${rowBuf}[i * 3] = __b0;
307
+ ${rowBuf}[i * 3 + 1] = __b1;
308
+ ${rowBuf}[i * 3 + 2] = __b2;
309
+ }
310
+ size_t rowBytes = static_cast<size_t>(rw) * 3U;
311
+ for (int16_t r = 0; r < __TC_FILL_ROWS; r++) {
312
+ memcpy(&${blockBuf}[static_cast<size_t>(r) * rowBytes], ${rowBuf}, rowBytes);
313
+ }
314
+ __tc_pnl_set_window(x, y, rw, rh);
315
+ __tc_pnl_ramwr_begin();
316
+ int16_t remaining = rh;
317
+ while (remaining > 0) {
318
+ int16_t chunk = (remaining > __TC_FILL_ROWS) ? __TC_FILL_ROWS : remaining;
319
+ struct spi_buf __bd = { ${blockBuf}, static_cast<size_t>(chunk) * rowBytes };
320
+ struct spi_buf_set __sd = { &__bd, 1 };
321
+ (void)spi_write(DEVICE_DT_GET(DT_NODELABEL(${bus})), &__tc_pnl_cfg8, &__sd);
322
+ remaining -= chunk;
323
+ }
324
+ __tc_pnl_ramwr_end();
325
+ }
326
+ `;
327
+
328
+ // Per-controller init table. Both are byte-for-byte Adafruit sequences
329
+ // emitted as (cmd, len, data, delay) records walked in display_init.
330
+ const initBlock = isIli9341
331
+ ? `
332
+ // ── Panel init ──────────────────────────────────────────────────────────
333
+ // Adafruit ILI9341 init sequence (initILI9341), byte for byte: SWRESET,
334
+ // manufacturer/power/gamma registers, MADCTL 0x28 (MV landscape + BGR, the
335
+ // same convention as the ST7796S path), COLMOD 0x55 (16-bit RGB565 — the
336
+ // native wire format of the pack565 transport), SLPOUT (150ms), DISPON
337
+ // (150ms), INVON. INVON is included because the common ILI9341 SPI modules
338
+ // (2.2"/2.4" TFTs) ship with an inverted panel; the in-tree binding's
339
+ // display-inversion property mirrors this for the stock driver.
340
+ struct __tc_pnl_init_cmd { uint8_t cmd; uint8_t len; const uint8_t* data; uint16_t delay_ms; };
341
+ static const uint8_t __tc_pnl_i1[] = {0x03, 0x80, 0x02};
342
+ static const uint8_t __tc_pnl_i2[] = {0x00, 0xC1, 0x30};
343
+ static const uint8_t __tc_pnl_i3[] = {0x64, 0x03, 0x12, 0x81};
344
+ static const uint8_t __tc_pnl_i4[] = {0x85, 0x00, 0x78};
345
+ static const uint8_t __tc_pnl_i5[] = {0x39, 0x2C, 0x00, 0x34, 0x02};
346
+ static const uint8_t __tc_pnl_i6[] = {0x20};
347
+ static const uint8_t __tc_pnl_i7[] = {0x00, 0x00};
348
+ static const uint8_t __tc_pnl_i8[] = {0x23};
349
+ static const uint8_t __tc_pnl_i9[] = {0x10};
350
+ static const uint8_t __tc_pnl_i10[] = {0x3E, 0x28};
351
+ static const uint8_t __tc_pnl_i11[] = {0x86};
352
+ static const uint8_t __tc_pnl_i12[] = {0x28};
353
+ static const uint8_t __tc_pnl_i13[] = {0x55};
354
+ static const uint8_t __tc_pnl_i14[] = {0x00, 0x18};
355
+ static const uint8_t __tc_pnl_i15[] = {0x08, 0x82, 0x27};
356
+ static const uint8_t __tc_pnl_i16[] = {0x00};
357
+ static const uint8_t __tc_pnl_i17[] = {0x01};
358
+ static const uint8_t __tc_pnl_i18[] = {0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, 0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03, 0x0E, 0x09, 0x00};
359
+ static const uint8_t __tc_pnl_i19[] = {0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, 0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C, 0x31, 0x36, 0x0F};
360
+ static const struct __tc_pnl_init_cmd __tc_pnl_init_seq[] = {
361
+ {0x01, 0, NULL, 150}, // SWRESET
362
+ {0xEF, 3, __tc_pnl_i1, 0},
363
+ {0xCF, 3, __tc_pnl_i2, 0},
364
+ {0xED, 4, __tc_pnl_i3, 0},
365
+ {0xE8, 3, __tc_pnl_i4, 0},
366
+ {0xCB, 5, __tc_pnl_i5, 0},
367
+ {0xF7, 1, __tc_pnl_i6, 0},
368
+ {0xEA, 2, __tc_pnl_i7, 0},
369
+ {0xC0, 1, __tc_pnl_i8, 0}, // PWCTRL1
370
+ {0xC1, 1, __tc_pnl_i9, 0}, // PWCTRL2
371
+ {0xC5, 2, __tc_pnl_i10, 0}, // VMCTRL1
372
+ {0xC7, 1, __tc_pnl_i11, 0}, // VMCTRL2
373
+ {0x36, 1, __tc_pnl_i12, 0}, // MADCTL 0x28: MV (landscape) + BGR=1
374
+ {0x3A, 1, __tc_pnl_i13, 0}, // COLMOD 0x55 (16-bit RGB565)
375
+ {0xB1, 2, __tc_pnl_i14, 0}, // FRMCTR1
376
+ {0xB6, 3, __tc_pnl_i15, 0}, // DISCTRL
377
+ {0xF2, 1, __tc_pnl_i16, 0}, // ENABLE3G off
378
+ {0x26, 1, __tc_pnl_i17, 0}, // GAMSET gamma curve 1
379
+ {0xE0, 15, __tc_pnl_i18, 0}, // PGAMCTRL
380
+ {0xE1, 15, __tc_pnl_i19, 0}, // NGAMCTRL
381
+ {0x11, 0, NULL, 150}, // SLPOUT (sleep out — 120ms typical)
382
+ {0x29, 0, NULL, 150}, // DISPON
383
+ {0x21, 0, NULL, 0}, // INVON (common ILI9341 modules ship inverted)
384
+ };
385
+ `
386
+ : `
387
+ // ── Panel init ──────────────────────────────────────────────────────────
388
+ // Adafruit ST7796S init sequence (demo-st lib fork), byte for byte: hw reset
389
+ // pulse, SWRESET, manufacturer unlock, VCOM/MADCTL/COLMOD/porch registers,
390
+ // lock, SLPOUT (150ms), DISPON (150ms), INVOFF. MADCTL 0x28 = MV (rotation 1
391
+ // landscape) + BGR=1. BGR=1 makes the controller route data R/B to the
392
+ // B/R subpixels (verified: red data shows blue with BGR=1), which combined
393
+ // with a lossless R/B data swap renders the UI correctly; BGR=0 leaves a
394
+ // half-lossy G/B quirk on this clone controller.
395
+ struct __tc_pnl_init_cmd { uint8_t cmd; uint8_t len; const uint8_t* data; uint16_t delay_ms; };
396
+ static const uint8_t __tc_pnl_i1[] = {0xC3};
397
+ static const uint8_t __tc_pnl_i2[] = {0x96};
398
+ static const uint8_t __tc_pnl_i3[] = {0x1C};
399
+ static const uint8_t __tc_pnl_i4[] = {0x28};
400
+ static const uint8_t __tc_pnl_i5[] = {0x66};
401
+ static const uint8_t __tc_pnl_i6[] = {0x80};
402
+ static const uint8_t __tc_pnl_i7[] = {0x00};
403
+ static const uint8_t __tc_pnl_i8[] = {0x80, 0x02, 0x3B};
404
+ static const uint8_t __tc_pnl_i9[] = {0xC6};
405
+ static const uint8_t __tc_pnl_i10[] = {0x69};
406
+ static const uint8_t __tc_pnl_i11[] = {0x3C};
407
+ static const struct __tc_pnl_init_cmd __tc_pnl_init_seq[] = {
408
+ {0x01, 0, NULL, 150}, // SWRESET
409
+ {0xF0, 1, __tc_pnl_i1, 0}, // unlock manufacturer
410
+ {0xF0, 1, __tc_pnl_i2, 0},
411
+ {0xC5, 1, __tc_pnl_i3, 0}, // VCOM control
412
+ {0x36, 1, __tc_pnl_i4, 0}, // MADCTL 0x28: MV (rotation 1) + BGR=1
413
+ {0x3A, 1, __tc_pnl_i5, 0}, // COLMOD 0x66 (18-bit, 262K) — clean channel routing
414
+ {0xB0, 1, __tc_pnl_i6, 0}, // interface control
415
+ {0xB4, 1, __tc_pnl_i7, 0}, // inversion control
416
+ {0xB6, 3, __tc_pnl_i8, 0}, // display function control
417
+ {0xB7, 1, __tc_pnl_i9, 0}, // entry mode
418
+ {0xF0, 1, __tc_pnl_i10, 0}, // lock manufacturer
419
+ {0xF0, 1, __tc_pnl_i11, 0},
420
+ {0x11, 0, NULL, 150}, // SLPOUT (sleep out — 120ms typical)
421
+ {0x29, 0, NULL, 150}, // DISPON
422
+ {0x20, 0, NULL, 0}, // INVOFF (non-inverted at power-on)
423
+ };
424
+ `;
425
+
426
+ const functions = `
427
+ // ── Direct panel transport ──────────────────────────────────────────────
428
+ // GPIOs: CS/DC/RST driven manually; CS stays LOW for the whole command+data
429
+ // burst (DC toggles between command byte and parameters/pixels), matching the
430
+ // Adafruit ST77xx protocol this panel requires. The SPI config carries no CS
431
+ // (cs_is_gpio = false -> the ESP32 driver's hardware CSEL pin is left idle;
432
+ // it is not connected to the panel).
433
+ static const struct gpio_dt_spec __tc_pnl_cs = GPIO_DT_SPEC_GET(DT_NODELABEL(${bus}), cs_gpios);
434
+ static const struct gpio_dt_spec __tc_pnl_dc = GPIO_DT_SPEC_GET(DT_NODELABEL(${bridge}), dc_gpios);
435
+ static const struct gpio_dt_spec __tc_pnl_rst = GPIO_DT_SPEC_GET(DT_NODELABEL(${bridge}), reset_gpios);
436
+
437
+ // 8-bit frames for commands/parameters and ${isIli9341 ? '16-bit (2 bytes/pixel)' : '18-bit (3 bytes/pixel)'} pixel data.
438
+ static struct spi_config __tc_pnl_cfg8 = {
439
+ .frequency = DT_PROP(DT_NODELABEL(${dtLabel}), mipi_max_frequency),
440
+ .operation = SPI_OP_MODE_MASTER | SPI_WORD_SET(8),
441
+ .slave = 0,
442
+ };
443
+
444
+ // Assert CS (active=low) unless a startWrite/endWrite batch already holds it.
445
+ static inline void __tc_pnl_cs_assert(void) {
446
+ if (__tc_pnl_write_depth == 0U) { gpio_pin_set_dt(&__tc_pnl_cs, 1); }
447
+ }
448
+ // Deassert CS unless a startWrite/endWrite batch is still holding it.
449
+ static inline void __tc_pnl_cs_release(void) {
450
+ if (__tc_pnl_write_depth == 0U) { gpio_pin_set_dt(&__tc_pnl_cs, 0); }
451
+ }
452
+
453
+ // Write one command byte (DC low) + its parameters (DC high). CS is asserted
454
+ // for the burst unless an outer startWrite is already holding it.
455
+ static void __tc_pnl_cmd(uint8_t cmd, const uint8_t* data, uint16_t len) {
456
+ struct spi_buf __bc = { &cmd, 1 };
457
+ struct spi_buf_set __sc = { &__bc, 1 };
458
+ __tc_pnl_cs_assert();
459
+ gpio_pin_set_dt(&__tc_pnl_dc, 0);
460
+ (void)spi_write(DEVICE_DT_GET(DT_NODELABEL(${bus})), &__tc_pnl_cfg8, &__sc);
461
+ if (len > 0) {
462
+ struct spi_buf __bd = { const_cast<uint8_t*>(data), len };
463
+ struct spi_buf_set __sd = { &__bd, 1 };
464
+ gpio_pin_set_dt(&__tc_pnl_dc, 1);
465
+ (void)spi_write(DEVICE_DT_GET(DT_NODELABEL(${bus})), &__tc_pnl_cfg8, &__sd);
466
+ }
467
+ __tc_pnl_cs_release();
468
+ }
469
+
470
+ // Open a RAMWR burst: CS low, RAMWR command, DC high for the pixel data.
471
+ static void __tc_pnl_ramwr_begin(void) {
472
+ uint8_t __ramwr = 0x2C;
473
+ struct spi_buf __bc = { &__ramwr, 1 };
474
+ struct spi_buf_set __sc = { &__bc, 1 };
475
+ __tc_pnl_cs_assert();
476
+ gpio_pin_set_dt(&__tc_pnl_dc, 0);
477
+ (void)spi_write(DEVICE_DT_GET(DT_NODELABEL(${bus})), &__tc_pnl_cfg8, &__sc);
478
+ gpio_pin_set_dt(&__tc_pnl_dc, 1);
479
+ }
480
+
481
+ static void __tc_pnl_ramwr_end(void) { __tc_pnl_cs_release(); }
482
+
483
+ // Read ST7796S/ILI9341 GET_SCANLINE (0x45). These controllers return one
484
+ // dummy byte followed by the 16-bit scanline. Some modules do not implement
485
+ // readback correctly; 0xFFFF means "unknown" and disables waiting for that
486
+ // update rather than stalling or corrupting the frame.
487
+ static uint16_t __tc_pnl_read_scanline(void) {
488
+ if (!__tc_pnl_scanline_sync) return 0xFFFFu;
489
+ uint8_t __cmd = 0x45;
490
+ uint8_t __tx[3] = {0, 0, 0};
491
+ uint8_t __rx[3] = {0, 0, 0};
492
+ struct spi_buf __bc = { &__cmd, 1 };
493
+ struct spi_buf_set __sc = { &__bc, 1 };
494
+ struct spi_buf __bt = { __tx, sizeof(__tx) };
495
+ struct spi_buf __br = { __rx, sizeof(__rx) };
496
+ struct spi_buf_set __st = { &__bt, 1 };
497
+ struct spi_buf_set __sr = { &__br, 1 };
498
+ __tc_pnl_cs_assert();
499
+ gpio_pin_set_dt(&__tc_pnl_dc, 0);
500
+ int __cmd_err = spi_write(DEVICE_DT_GET(DT_NODELABEL(${bus})), &__tc_pnl_cfg8, &__sc);
501
+ int __read_err = 0;
502
+ if (__cmd_err == 0) {
503
+ gpio_pin_set_dt(&__tc_pnl_dc, 1);
504
+ __read_err = spi_transceive(DEVICE_DT_GET(DT_NODELABEL(${bus})), &__tc_pnl_cfg8, &__st, &__sr);
505
+ }
506
+ __tc_pnl_cs_release();
507
+ if (__cmd_err != 0 || __read_err != 0) return 0xFFFFu;
508
+ uint16_t __line = static_cast<uint16_t>((static_cast<uint16_t>(__rx[1]) << 8) | __rx[2]);
509
+ return (__line < ${h}) ? __line : 0xFFFFu;
510
+ }
511
+
512
+ // Wait until the panel has scanned past a dirty rectangle. This is intentionally
513
+ // bounded: a broken/floating SDO must never hang the UI loop. Full-height writes
514
+ // have no safe post-rectangle interval, so they retain the normal single-burst
515
+ // behavior; the caller's framebuffer still prevents intermediate software frames.
516
+ static void __tc_pnl_wait_for_safe_rect(int16_t y, int16_t rh) {
517
+ #if __TC_TE_SYNC
518
+ // TE variant: arm on the next frame pulse, then start the burst — the write
519
+ // chases the scan beam from the top of the rect. Bounded so a stuck TE line
520
+ // can never hang the UI loop.
521
+ if (rh < 8 || y < 0) return;
522
+ uint32_t __was = __tc_te_count;
523
+ uint32_t __deadline = k_uptime_get_32() + 25U;
524
+ while (__tc_te_count == __was) {
525
+ if (static_cast<int32_t>(k_uptime_get_32() - __deadline) >= 0) break;
526
+ k_msleep(0);
527
+ }
528
+ return;
529
+ #endif
530
+ if (!__tc_pnl_scanline_sync || rh < 8 || y < 0) return;
531
+ int16_t __last = static_cast<int16_t>(y + rh - 1);
532
+ if (__last >= static_cast<int16_t>(${h} - 2)) return;
533
+ uint32_t __deadline = k_uptime_get_32() + 20U;
534
+ for (;;) {
535
+ uint16_t __line = __tc_pnl_read_scanline();
536
+ if (__line == 0xFFFFu || (__line > static_cast<uint16_t>(__last + 2) && __line < static_cast<uint16_t>(${h} - 2))) return;
537
+ if (static_cast<int32_t>(k_uptime_get_32() - __deadline) >= 0) return;
538
+ k_msleep(0);
539
+ }
540
+ }
541
+ ${pixelHelpers}
542
+ // Set the address window. Coordinates are in the effective (rotated) UI
543
+ // space; the panel's MADCTL (rotation 1: MV) maps them onto the native
544
+ // 320x480 raster, so CASET/RASET take the UI x/y ranges directly.
545
+ static void __tc_pnl_set_window(int16_t x, int16_t y, int16_t winW, int16_t winH) {
546
+ __tc_pnl_wait_for_safe_rect(y, winH);
547
+ uint16_t __x0 = static_cast<uint16_t>(x);
548
+ uint16_t __x1 = static_cast<uint16_t>(x + winW - 1);
549
+ uint16_t __y0 = static_cast<uint16_t>(y);
550
+ uint16_t __y1 = static_cast<uint16_t>(y + winH - 1);
551
+ uint8_t __ca[4] = { static_cast<uint8_t>(__x0 >> 8), static_cast<uint8_t>(__x0),
552
+ static_cast<uint8_t>(__x1 >> 8), static_cast<uint8_t>(__x1) };
553
+ uint8_t __ra[4] = { static_cast<uint8_t>(__y0 >> 8), static_cast<uint8_t>(__y0),
554
+ static_cast<uint8_t>(__y1 >> 8), static_cast<uint8_t>(__y1) };
555
+ __tc_pnl_cmd(0x2A, __ca, 4);
556
+ __tc_pnl_cmd(0x2B, __ra, 4);
557
+ }
558
+
559
+ // ── Panel-ops consumed by CuttlefishGFX ─────────────────────────────────
560
+ // startWrite/endWrite batch multiple primitives under one CS-asserted burst
561
+ // (Adafruit ST77xx protocol: CS held low across the burst, DC toggles
562
+ // mid-burst). A depth counter supports nested startWrite calls — the runtime
563
+ // sometimes wraps a canvas push inside an outer transaction. Only the
564
+ // outermost startWrite asserts CS and the outermost endWrite releases it;
565
+ // inner ones just bump the depth. This collapses per-primitive CS-toggle
566
+ // overhead (one transaction per burst instead of one per rect/glyph row).
567
+ static void __tc_op_startWrite(void* /*ctx*/) {
568
+ if (__tc_pnl_write_depth == 0U) { gpio_pin_set_dt(&__tc_pnl_cs, 1); }
569
+ __tc_pnl_write_depth++;
570
+ }
571
+ static void __tc_op_endWrite(void* /*ctx*/) {
572
+ if (__tc_pnl_write_depth > 0U) {
573
+ __tc_pnl_write_depth--;
574
+ if (__tc_pnl_write_depth == 0U) { gpio_pin_set_dt(&__tc_pnl_cs, 0); }
575
+ }
576
+ }
577
+
578
+ // Stash the target rect. The runtime always follows this with writePixels
579
+ // delivering exactly (w*h) pixels for this rect, OR fillRect/writePixel which
580
+ // ignore the stash.
581
+ static void __tc_op_setAddrWindow(void* /*ctx*/, int16_t x, int16_t y, int16_t winW, int16_t winH) {
582
+ __tc_aw_x = x; __tc_aw_y = y; __tc_aw_w = winW; __tc_aw_h = winH;
583
+ }
584
+
585
+ // Push the stashed rect's worth of rgb565 pixels. Called right after
586
+ // setAddrWindow with exactly (aw_w * aw_h) pixels. The caller's buffer is
587
+ // never mutated (scroll canvases persist across frames), so pixels are
588
+ // converted in chunks through the row${bpp} scratch buffer.
589
+ static void __tc_op_writePixels(void* /*ctx*/, const uint16_t* px, uint32_t n) {
590
+ if (n == 0U) return;
591
+ __tc_pnl_set_window(__tc_aw_x, __tc_aw_y, __tc_aw_w, __tc_aw_h);
592
+ __tc_pnl_ramwr_begin();
593
+ ${pixelsFn}(px, n);
594
+ __tc_pnl_ramwr_end();
595
+ }
596
+
597
+ // Single pixel: write a 1x1 rect.
598
+ static void __tc_op_writePixel(void* /*ctx*/, int16_t x, int16_t y, uint16_t c) {
599
+ uint16_t __c = c;
600
+ __tc_pnl_set_window(x, y, 1, 1);
601
+ __tc_pnl_ramwr_begin();
602
+ ${pixelsFn}(&__c, 1);
603
+ __tc_pnl_ramwr_end();
604
+ }
605
+ ${fillRectFn}
606
+ static int16_t __tc_op_width(void* /*ctx*/) { return ${w}; }
607
+ static int16_t __tc_op_height(void* /*ctx*/) { return ${h}; }
608
+
609
+ // flush is nullptr — direct-mode panel (TFT), no backing store to push.
610
+ struct CuttlefishPanelOps;
611
+ extern const CuttlefishPanelOps __tc_display_ops;
612
+
613
+ const CuttlefishPanelOps __tc_display_ops = {
614
+ __tc_op_startWrite,
615
+ __tc_op_endWrite,
616
+ __tc_op_setAddrWindow,
617
+ __tc_op_writePixels,
618
+ __tc_op_writePixel,
619
+ __tc_op_fillRect,
620
+ __tc_op_width,
621
+ __tc_op_height,
622
+ nullptr, // flush — direct-mode panel
623
+ };
624
+
625
+ // The live display target: a CuttlefishGFX driven by the panel-ops vtable.
626
+ CuttlefishGFX __tc_display(&__tc_display_ops, nullptr);
627
+ ${initBlock}
628
+ // ── display_init (called from setup) ────────────────────────────────────
629
+
630
+ static inline void display_init() {
631
+ printk("TC_DISPLAY: device ready\\n");
632
+ ${teInit}
633
+ ${blInit}
634
+ gpio_pin_configure_dt(&__tc_pnl_cs, GPIO_OUTPUT);
635
+ gpio_pin_configure_dt(&__tc_pnl_dc, GPIO_OUTPUT);
636
+ gpio_pin_configure_dt(&__tc_pnl_rst, GPIO_OUTPUT);
637
+ // Hardware reset pulse (Adafruit init behavior).
638
+ gpio_pin_set_dt(&__tc_pnl_rst, 1);
639
+ k_msleep(20);
640
+ gpio_pin_set_dt(&__tc_pnl_rst, 0);
641
+ k_msleep(20);
642
+ // Adafruit init sequence, direct protocol.
643
+ for (uint32_t i = 0; i < (sizeof(__tc_pnl_init_seq) / sizeof(__tc_pnl_init_seq[0])); i++) {
644
+ __tc_pnl_cmd(__tc_pnl_init_seq[i].cmd, __tc_pnl_init_seq[i].data,
645
+ __tc_pnl_init_seq[i].len);
646
+ if (__tc_pnl_init_seq[i].delay_ms > 0) k_msleep(__tc_pnl_init_seq[i].delay_ms);
647
+ }
648
+ __tc_op_fillRect(nullptr, 0, 0, ${w}, ${h}, 0x0000);
649
+ printk("TC_DISPLAY: direct init done (${wireTag}, black fill)\\n");
650
+ }
651
+
652
+ static inline void display_fillScreen(UI_COLOR_T color) {
653
+ __tc_op_fillRect(nullptr, 0, 0, ${w}, ${h}, static_cast<uint16_t>(color));
654
+ }
655
+ static inline CuttlefishDisplayTarget* display_defaultTarget() { return &__tc_display; }
656
+ static inline int16_t display_width() { return ${w}; }
657
+ static inline int16_t display_height() { return ${h}; }
658
+
659
+ static inline void display_startWrite() { __tc_op_startWrite(nullptr); }
660
+ static inline void display_endWrite() { __tc_op_endWrite(nullptr); }
661
+ static inline void display_setAddrWindow(int16_t x, int16_t y, int16_t winW, int16_t winH) {
662
+ __tc_op_setAddrWindow(nullptr, x, y, winW, winH);
663
+ }
664
+ static inline void display_writePixels(uint16_t* pixels, uint32_t count) {
665
+ __tc_op_writePixels(nullptr, pixels, count);
666
+ }
667
+
668
+ // ── Canvas lifecycle + accessors (offscreen rgb565 compositing) ──────────
669
+ // Allocate the canvas object via malloc + placement-new (not operator new).
670
+ // Under CONFIG_REQUIRES_FULL_LIBCPP without CONFIG_CPP_EXCEPTIONS, operator new
671
+ // throws std::bad_alloc on OOM and the nothrow wrapper's internal catch cannot
672
+ // unwind (no EH runtime) → std::terminate → abort. malloc returns NULL on
673
+ // failure with no exception path; placement-new then constructs the object in
674
+ // place (vtable included). display_deleteCanvas mirrors with an explicit dtor
675
+ // + free. The runtime's callers already null-check the return, so an OOM
676
+ // degrades gracefully instead of aborting.
677
+ static inline CuttlefishCanvas16* display_createCanvas(int16_t cw, int16_t ch) {
678
+ void* mem = malloc(sizeof(CuttlefishCanvas16));
679
+ if (!mem) return nullptr;
680
+ return new (mem) CuttlefishCanvas16(cw, ch);
681
+ }
682
+ // psramFound() shim: the Arduino ESP32 core provides this, but Zephyr does not.
683
+ // Under BOARD_HAS_PSRAM the ESP heap serves PSRAM, so report it as present. The
684
+ // runtime's ui_create_canvas_best calls this under #if defined(BOARD_HAS_PSRAM).
685
+ #if defined(BOARD_HAS_PSRAM) && !defined(psramFound)
686
+ #include <zephyr/multi_heap/shared_multi_heap.h>
687
+ static inline bool psramFound() {
688
+ // Compile-time truth: if BOARD_HAS_PSRAM is defined, the build targets a
689
+ // PSRAM board with CONFIG_ESP_SPIRAM enabled (the framework emits both).
690
+ return true;
691
+ }
692
+ // Allocate from PSRAM via Zephyr's shared multi-heap (the ESP32 SoC code
693
+ // registers PSRAM as an SMH_REG_ATTR_EXTERNAL region at boot).
694
+ static inline void* ui_psram_malloc(size_t bytes) {
695
+ return shared_multi_heap_alloc(SMH_REG_ATTR_EXTERNAL, bytes);
696
+ }
697
+ #endif
698
+ static inline CuttlefishCanvas16* display_createCanvasPsram(int16_t cw, int16_t ch) {
699
+ // Allocate the pixel buffer in PSRAM (large: w*h*2 bytes) and the small
700
+ // canvas object in SRAM. The canvas takes ownership of the PSRAM buffer and
701
+ // frees it via free() in its dtor (the SMH allocator's free is compatible
702
+ // with the standard k_free/free path). Returns nullptr if PSRAM isn't
703
+ // available or the allocation fails — ui_create_canvas_best falls back.
704
+ #if defined(BOARD_HAS_PSRAM)
705
+ if ((cw > 0) && (ch > 0)) {
706
+ size_t bytes = static_cast<size_t>(cw) * static_cast<size_t>(ch) * sizeof(uint16_t);
707
+ uint16_t* psramBuf = static_cast<uint16_t*>(ui_psram_malloc(bytes));
708
+ if (psramBuf) {
709
+ void* mem = malloc(sizeof(CuttlefishCanvas16));
710
+ if (mem) {
711
+ return new (mem) CuttlefishCanvas16(cw, ch, psramBuf, 1);
712
+ }
713
+ free(psramBuf);
714
+ }
715
+ }
716
+ #else
717
+ (void)cw; (void)ch;
718
+ #endif
719
+ return nullptr;
720
+ }
721
+ static inline void display_deleteCanvas(CuttlefishCanvas16* canvas) {
722
+ if (!canvas) return;
723
+ canvas->~CuttlefishCanvas16();
724
+ free(canvas);
725
+ }
726
+ static inline int16_t display_canvasWidth(CuttlefishCanvas16* c) { return c->width(); }
727
+ static inline int16_t display_canvasHeight(CuttlefishCanvas16* c) { return c->height(); }
728
+ static inline uint16_t* display_canvasBuffer(CuttlefishCanvas16* c) { return c->getBuffer(); }
729
+ static inline uint16_t display_canvasGetPixel(CuttlefishCanvas16* c, int16_t x, int16_t y) { return c->getPixel(x, y); }
730
+ static inline void display_canvasFillScreen(CuttlefishCanvas16* c, UI_COLOR_T color) { c->fillScreen(static_cast<uint16_t>(color)); }
731
+ static inline void display_canvasFillRect(CuttlefishCanvas16* c, int16_t x, int16_t y, int16_t cw, int16_t ch, UI_COLOR_T color) {
732
+ c->fillRect(x, y, cw, ch, static_cast<uint16_t>(color));
733
+ }
734
+
735
+ // ── Target-polymorphic draw (panel or canvas via CuttlefishDisplayTarget*) ─
736
+ static inline void display_targetDrawPixel(CuttlefishDisplayTarget* t, int16_t x, int16_t y, UI_COLOR_T color) { t->drawPixel(x, y, static_cast<uint16_t>(color)); }
737
+ static inline int16_t display_targetWidth(CuttlefishDisplayTarget* t) { return t->width(); }
738
+ static inline int16_t display_targetHeight(CuttlefishDisplayTarget* t) { return t->height(); }
739
+ static inline void display_targetDrawRGBBitmap(CuttlefishDisplayTarget* t, int16_t x, int16_t y, const uint16_t* bitmap, int16_t bw, int16_t bh) {
740
+ t->drawRGBBitmap(x, y, bitmap, bw, bh);
741
+ }
742
+ static inline void display_targetFillRect(CuttlefishDisplayTarget* t, int16_t x, int16_t y, int16_t bw, int16_t bh, UI_COLOR_T color) { t->fillRect(x, y, bw, bh, static_cast<uint16_t>(color)); }
743
+ static inline void display_targetDrawFastHLine(CuttlefishDisplayTarget* t, int16_t x, int16_t y, int16_t bw, UI_COLOR_T color) { t->drawFastHLine(x, y, bw, static_cast<uint16_t>(color)); }
744
+ static inline void display_targetDrawFastVLine(CuttlefishDisplayTarget* t, int16_t x, int16_t y, int16_t bh, UI_COLOR_T color) { t->drawFastVLine(x, y, bh, static_cast<uint16_t>(color)); }
745
+ static inline void display_targetFillRoundRect(CuttlefishDisplayTarget* t, int16_t x, int16_t y, int16_t bw, int16_t bh, int16_t r, UI_COLOR_T color) { t->fillRoundRect(x, y, bw, bh, r, static_cast<uint16_t>(color)); }
746
+ static inline void display_targetDrawRect(CuttlefishDisplayTarget* t, int16_t x, int16_t y, int16_t bw, int16_t bh, UI_COLOR_T color) { t->drawRect(x, y, bw, bh, static_cast<uint16_t>(color)); }
747
+ static inline void display_targetDrawRoundRect(CuttlefishDisplayTarget* t, int16_t x, int16_t y, int16_t bw, int16_t bh, int16_t r, UI_COLOR_T color) { t->drawRoundRect(x, y, bw, bh, r, static_cast<uint16_t>(color)); }
748
+ static inline void display_targetDrawLine(CuttlefishDisplayTarget* t, int16_t x0, int16_t y0, int16_t x1, int16_t y1, UI_COLOR_T color) { t->drawLine(x0, y0, x1, y1, static_cast<uint16_t>(color)); }
749
+ static inline void display_targetFillCircle(CuttlefishDisplayTarget* t, int16_t x, int16_t y, int16_t r, UI_COLOR_T color) { t->fillCircle(x, y, r, static_cast<uint16_t>(color)); }
750
+ static inline void display_targetDrawCircle(CuttlefishDisplayTarget* t, int16_t x, int16_t y, int16_t r, UI_COLOR_T color) { t->drawCircle(x, y, r, static_cast<uint16_t>(color)); }
751
+ static inline void display_targetSetCursor(CuttlefishDisplayTarget* t, int16_t x, int16_t y) { t->setCursor(x, y); }
752
+ static inline void display_targetSetTextColor(CuttlefishDisplayTarget* t, UI_COLOR_T fg) { t->setTextColor(static_cast<uint16_t>(fg)); }
753
+ static inline void display_targetSetTextColorBg(CuttlefishDisplayTarget* t, UI_COLOR_T fg, UI_COLOR_T bg) { t->setTextColor(static_cast<uint16_t>(fg), static_cast<uint16_t>(bg)); }
754
+ static inline void display_targetSetTextSize(CuttlefishDisplayTarget* t, uint8_t s) { t->setTextSize(s); }
755
+ static inline void display_targetSetTextWrap(CuttlefishDisplayTarget* t, bool wp) { t->setTextWrap(wp); }
756
+ static inline void display_targetPrint(CuttlefishDisplayTarget* t, const char* s) { t->print(s); }
757
+ `;
758
+
759
+ return { includes, declaration, functions };
760
+ }
761
+
762
+ /**
763
+ * Strategy-owned display adapter generator. Receives the resolved display and
764
+ * looks up the Zephyr DT-binding profile by driver id. Returns undefined for
765
+ * unknown drivers so resolveDisplayAdapter can decline and
766
+ * generateDisplayAdapter surfaces a clear error.
767
+ */
768
+ export const zephyrDisplayAdapterGenerator: DisplayAdapterGenerator = (display) => {
769
+ const profile = ZEPHYR_DISPLAY_PROFILES[display.driver];
770
+ if (!profile) return undefined as unknown as DisplayAdapterCode;
771
+ // The UI adapter is RGB565/SPI (TFT) only. Monochrome panels (OLED) use the
772
+ // direct display.* GFX runtime (gfx.ts mono branch) — there is no
773
+ // CuttlefishGFX UI rendering path for mono. Decline so cuttlefish does not
774
+ // emit an incompatible RGB565 adapter for a mono profile.
775
+ if (profile.colorFormat === 'mono') return undefined as unknown as DisplayAdapterCode;
776
+ return zephyrUiDisplayAdapter(profile, {
777
+ scanlineSync: display.scanlineSync,
778
+ miso: display.spiPins?.miso,
779
+ tearingEffectPin: (display as { tearingEffectPin?: number }).tearingEffectPin,
780
+ });
781
+ };