@typecad/framework-zephyr 1.0.0-alpha.16 → 1.0.0-alpha.19

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