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

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 (191) hide show
  1. package/README.md +22 -35
  2. package/dist/as-built.d.ts +33 -0
  3. package/dist/as-built.js +188 -0
  4. package/dist/async/timer-polyfill.js +1 -1
  5. package/dist/boardgen.d.ts +128 -0
  6. package/dist/boardgen.js +1366 -0
  7. package/dist/chips/board-overrides.d.ts +7 -0
  8. package/dist/chips/board-overrides.js +11 -0
  9. package/dist/chips/controllers.d.ts +28 -8
  10. package/dist/chips/controllers.js +49 -12
  11. package/dist/chips/esp32s3.js +49 -0
  12. package/dist/chips/index.d.ts +6 -12
  13. package/dist/chips/index.js +18 -34
  14. package/dist/chips/resolve.d.ts +16 -2
  15. package/dist/chips/resolve.js +298 -15
  16. package/dist/chips/soc/.d.ts +2 -0
  17. package/dist/chips/soc/.js +129 -0
  18. package/dist/chips/soc/esp32.d.ts +2 -0
  19. package/dist/chips/soc/esp32.js +120 -0
  20. package/dist/chips/soc/esp32c3.d.ts +2 -0
  21. package/dist/chips/soc/esp32c3.js +90 -0
  22. package/dist/chips/soc/esp32c6.d.ts +2 -0
  23. package/dist/chips/soc/esp32c6.js +109 -0
  24. package/dist/chips/soc/esp32s3.d.ts +2 -0
  25. package/dist/chips/soc/esp32s3.js +189 -0
  26. package/dist/chips/soc/index.d.ts +2 -0
  27. package/dist/chips/soc/index.js +23 -0
  28. package/dist/chips/soc/nrf52840.d.ts +2 -0
  29. package/dist/chips/soc/nrf52840.js +130 -0
  30. package/dist/chips/soc/rp2040.d.ts +2 -0
  31. package/dist/chips/soc/rp2040.js +141 -0
  32. package/dist/chips/soc/rp2350a.d.ts +2 -0
  33. package/dist/chips/soc/rp2350a.js +145 -0
  34. package/dist/chips/soc/samd21g18a.d.ts +2 -0
  35. package/dist/chips/soc/samd21g18a.js +143 -0
  36. package/dist/chips/soc/stm32f411xe.d.ts +2 -0
  37. package/dist/chips/soc/stm32f411xe.js +251 -0
  38. package/dist/chips/types.d.ts +382 -12
  39. package/dist/chips/xiao-ble.js +33 -0
  40. package/dist/display/profiles.js +1 -1
  41. package/dist/doctor.js +38 -4
  42. package/dist/dt-config/custom-board.d.ts +23 -0
  43. package/dist/dt-config/custom-board.js +227 -0
  44. package/dist/dt-config/kconfig.d.ts +83 -1
  45. package/dist/dt-config/kconfig.js +116 -27
  46. package/dist/dt-config/overlay.js +623 -18
  47. package/dist/framework.manifest.d.ts +20 -30
  48. package/dist/framework.manifest.js +124 -178
  49. package/dist/index.d.ts +4 -1
  50. package/dist/index.js +8 -2
  51. package/dist/lowering/adc.d.ts +26 -5
  52. package/dist/lowering/adc.js +137 -31
  53. package/dist/lowering/ble.js +31 -28
  54. package/dist/lowering/dac.js +9 -9
  55. package/dist/lowering/fs.js +18 -10
  56. package/dist/lowering/gpio.d.ts +2 -0
  57. package/dist/lowering/gpio.js +90 -10
  58. package/dist/lowering/http.js +39 -14
  59. package/dist/lowering/hwtimer.d.ts +4 -0
  60. package/dist/lowering/hwtimer.js +35 -10
  61. package/dist/lowering/i2c.js +46 -54
  62. package/dist/lowering/index.d.ts +5 -5
  63. package/dist/lowering/index.js +15 -16
  64. package/dist/lowering/interrupts.d.ts +22 -5
  65. package/dist/lowering/interrupts.js +104 -22
  66. package/dist/lowering/mqtt.js +12 -6
  67. package/dist/lowering/power.js +0 -4
  68. package/dist/lowering/preferences.js +49 -77
  69. package/dist/lowering/pulse.js +5 -36
  70. package/dist/lowering/pwm.d.ts +32 -3
  71. package/dist/lowering/pwm.js +105 -17
  72. package/dist/lowering/random.js +3 -3
  73. package/dist/lowering/sensor.d.ts +37 -0
  74. package/dist/lowering/sensor.js +119 -0
  75. package/dist/lowering/spi.d.ts +12 -0
  76. package/dist/lowering/spi.js +65 -40
  77. package/dist/lowering/thread.d.ts +14 -0
  78. package/dist/lowering/thread.js +55 -0
  79. package/dist/lowering/timing.js +20 -46
  80. package/dist/lowering/tone.js +18 -5
  81. package/dist/lowering/uart.d.ts +13 -0
  82. package/dist/lowering/uart.js +72 -51
  83. package/dist/lowering/usb.d.ts +34 -0
  84. package/dist/lowering/usb.js +206 -0
  85. package/dist/lowering/wdt.d.ts +1 -1
  86. package/dist/lowering/wdt.js +17 -7
  87. package/dist/lowering/wifi.d.ts +3 -2
  88. package/dist/lowering/wifi.js +120 -111
  89. package/dist/sdk/board-catalog-sync.d.ts +16 -0
  90. package/dist/sdk/board-catalog-sync.js +41 -0
  91. package/dist/sdk/board-data.generated.d.ts +2 -0
  92. package/dist/sdk/board-data.generated.js +4 -0
  93. package/dist/sdk/catalog-walker.d.ts +90 -0
  94. package/dist/sdk/catalog-walker.js +682 -0
  95. package/dist/sdk/dts-reader.d.ts +83 -0
  96. package/dist/sdk/dts-reader.js +596 -0
  97. package/dist/strategy.d.ts +136 -17
  98. package/dist/strategy.js +881 -212
  99. package/dist/toolchain/bossac-touch.d.ts +18 -0
  100. package/dist/toolchain/bossac-touch.js +88 -0
  101. package/dist/toolchain/compat.d.ts +2 -2
  102. package/dist/toolchain/compat.js +8 -3
  103. package/dist/toolchain/debug-config.d.ts +21 -26
  104. package/dist/toolchain/debug-config.js +212 -72
  105. package/dist/toolchain/env-check.d.ts +4 -4
  106. package/dist/toolchain/env-check.js +5 -5
  107. package/dist/toolchain/index.d.ts +66 -1
  108. package/dist/toolchain/index.js +713 -58
  109. package/dist/toolchain/scaffold.d.ts +9 -0
  110. package/dist/toolchain/scaffold.js +84 -7
  111. package/dist/toolchain/west-discover.d.ts +8 -1
  112. package/dist/toolchain/west-discover.js +19 -6
  113. package/dist/toolchain/west-spawn.js +17 -5
  114. package/installer/CHANGELOG.md +473 -0
  115. package/installer/README.md +328 -0
  116. package/installer/environment.yml +46 -0
  117. package/installer/etc/conda/activate.d/zephyr.bat +7 -0
  118. package/installer/etc/conda/activate.d/zephyr.ps1 +12 -0
  119. package/installer/etc/conda/activate.d/zephyr.sh +20 -0
  120. package/installer/etc/conda/deactivate.d/zephyr.bat +6 -0
  121. package/installer/etc/conda/deactivate.d/zephyr.ps1 +6 -0
  122. package/installer/etc/conda/deactivate.d/zephyr.sh +4 -0
  123. package/installer/install.mjs +580 -0
  124. package/installer/install.ps1 +619 -0
  125. package/installer/install.sh +310 -0
  126. package/installer/lib/detect-platform.sh +62 -0
  127. package/installer/lib/fetch-sdk.sh +304 -0
  128. package/installer/lib/init-workspace.sh +153 -0
  129. package/installer/lib/write-activation.sh +76 -0
  130. package/installer/patches/tfm-c-include-separators.patch +19 -0
  131. package/installer/patches/zephyr-tfm-nordic-zephyr-include.patch +24 -0
  132. package/installer/templates/project/.typecad/activate-zephyr.ps1 +31 -0
  133. package/installer/templates/project/.typecad/activate-zephyr.sh +31 -0
  134. package/installer/templates/project/.vscode/settings.json +30 -0
  135. package/installer/templates/project/README.md +58 -0
  136. package/installer/versions.env +110 -0
  137. package/package.json +16 -16
  138. package/src/as-built.ts +206 -0
  139. package/src/boardgen.ts +1428 -0
  140. package/src/chips/controllers.ts +61 -12
  141. package/src/chips/index.ts +35 -54
  142. package/src/chips/resolve.ts +316 -14
  143. package/src/chips/types.ts +388 -13
  144. package/src/display/profiles.ts +1 -1
  145. package/src/doctor.ts +110 -77
  146. package/src/dt-config/custom-board.ts +325 -0
  147. package/src/dt-config/kconfig.ts +177 -19
  148. package/src/dt-config/overlay.ts +1058 -450
  149. package/src/framework.manifest.ts +125 -178
  150. package/src/index.ts +57 -41
  151. package/src/lowering/adc.ts +222 -102
  152. package/src/lowering/ble.ts +32 -28
  153. package/src/lowering/dac.ts +82 -82
  154. package/src/lowering/fs.ts +18 -10
  155. package/src/lowering/gpio.ts +242 -152
  156. package/src/lowering/http.ts +512 -488
  157. package/src/lowering/hwtimer.ts +139 -101
  158. package/src/lowering/i2c.ts +116 -126
  159. package/src/lowering/index.ts +91 -93
  160. package/src/lowering/interrupts.ts +113 -21
  161. package/src/lowering/mqtt.ts +12 -6
  162. package/src/lowering/preferences.ts +327 -354
  163. package/src/lowering/pwm.ts +167 -74
  164. package/src/lowering/random.ts +3 -3
  165. package/src/lowering/sensor.ts +155 -0
  166. package/src/lowering/spi.ts +164 -124
  167. package/src/lowering/thread.ts +63 -0
  168. package/src/lowering/timing.ts +46 -72
  169. package/src/lowering/uart.ts +73 -51
  170. package/src/lowering/usb.ts +221 -0
  171. package/src/lowering/wdt.ts +95 -84
  172. package/src/lowering/wifi.ts +120 -111
  173. package/src/sdk/board-catalog-sync.ts +62 -0
  174. package/src/strategy.ts +888 -229
  175. package/src/toolchain/bossac-touch.ts +112 -0
  176. package/src/toolchain/compat.ts +8 -3
  177. package/src/toolchain/debug-config.ts +666 -522
  178. package/src/toolchain/env-check.ts +9 -9
  179. package/src/toolchain/index.ts +765 -44
  180. package/src/toolchain/scaffold.ts +81 -7
  181. package/src/toolchain/west-discover.ts +334 -316
  182. package/src/toolchain/west-spawn.ts +17 -5
  183. package/src/async/timer-polyfill.ts +0 -107
  184. package/src/chips/esp32.ts +0 -74
  185. package/src/chips/esp32s3.ts +0 -57
  186. package/src/chips/xiao-ble.ts +0 -70
  187. package/src/lowering/power.ts +0 -70
  188. package/src/lowering/pulse.ts +0 -93
  189. package/src/lowering/tone.ts +0 -60
  190. package/src/lowering/worker-backing.ts +0 -94
  191. package/src/lowering/worker.ts +0 -17
@@ -1,450 +1,1058 @@
1
- // ---------------------------------------------------------------------------
2
- // Devicetree overlay generator
3
- //
4
- // Generates <project>/app/boards/<board>.overlay — the standard Zephyr overlay
5
- // path. Content is additive: for each used peripheral with a DT binding the chip
6
- // descriptor knows, emit a `&nodelabel { status = "okay"; }` block enabling it.
7
- // West merges this over the board's base DT; we never rewrite the base.
8
- //
9
- // This is the Zephyr analog of Arduino's library-resolution hooks: the artifact
10
- // that brings external capabilities into a build. (Arduino does it by parsing
11
- // library headers into .d.ts; Zephyr does it by enabling DT nodes + Kconfig.)
12
- // ---------------------------------------------------------------------------
13
-
14
- import type { ZephyrChipDescriptor } from '../chips/types.js';
15
- import type { ZephyrDisplayProfile } from '../display/profiles.js';
16
- import { PANEL_CONTROLLER_DEFAULTS, panelControllerFor } from '../display/profiles.js';
17
- import type { KconfigUsage } from './kconfig.js';
18
-
19
- /**
20
- * Generate the overlay source for a board + usage. Returns the overlay text
21
- * (caller writes it to app/boards/<board>.overlay via writeIfChanged).
22
- */
23
- /** Display wiring from cuttlefish.config.ts (cs/dc/rst/spiFrequency/spiPins). */
24
- export interface DisplayWiring {
25
- cs?: number;
26
- dc?: number;
27
- rst?: number;
28
- /** Tearing-effect (TE) GPIO from display.tearingEffectPin emitted as
29
- * te-gpios on the display DT node. Opt-in; most boards don't wire TE. */
30
- tearingEffectPin?: number;
31
- spiFrequency?: number;
32
- /** SPI bus pins. When present, the overlay remuxes the SPI controller's
33
- * pinctrl to these pins (the board defaults rarely match a breakout's
34
- * wiring e.g. demo-st's panel is on SCK=18/MOSI=23, not the devkitc
35
- * default 12/11). */
36
- sck?: number;
37
- mosi?: number;
38
- miso?: number;
39
- /** Optional GPIO driving the panel backlight. When set, the overlay emits a
40
- * `gpio-leds` node + DT alias (named by the profile's `backlight`) so the
41
- * display adapter can drive it high at init. When absent (e.g. the panel's
42
- * backlight is hardwired to 3.3V), no node or alias is emitted — the C++
43
- * backlight init is guarded by `DT_HAS_ALIAS` and compiles away. */
44
- backlightPin?: number;
45
- }
46
-
47
- /** Touch wiring from cuttlefish.config.ts (irq/resetPin/sda/scl; cs for SPI
48
- * resistive controllers, calibration for the XPT2046 DT binding). */
49
- export interface TouchWiring {
50
- /** Touch controller kind — selects the DT node shape (FT6336U node on I2C0
51
- * vs XPT2046 node on the display's SPI bus). Default 'ft6336u'. */
52
- controller?: 'ft6336u' | 'xpt2046';
53
- irq?: number;
54
- resetPin?: number;
55
- sda?: number;
56
- scl?: number;
57
- /** XPT2046 only: SPI CS pin (second cs-gpios entry on the panel's bus). */
58
- cs?: number;
59
- /** XPT2046 only: raw ADC calibration — feeds the binding's min-x/max-x/
60
- * min-y/max-y (required props). Defaults span the full 12-bit range. */
61
- calibration?: { xMin: number; xMax: number; yMin: number; yMax: number };
62
- /** XPT2046 only: pen-detect Z1 threshold (binding's z-threshold). Resistive
63
- * panels need a few hundred 12-bit counts; default 400. */
64
- minPressure?: number;
65
- }
66
-
67
- export interface OverlayDiagnostic {
68
- severity: "warning" | "error";
69
- message: string;
70
- }
71
-
72
- export function generateOverlay(
73
- chip: ZephyrChipDescriptor,
74
- usage: KconfigUsage,
75
- display: ZephyrDisplayProfile | undefined,
76
- wiring?: DisplayWiring,
77
- touch?: TouchWiring,
78
- diagnostics: OverlayDiagnostic[] = [],
79
- ): string {
80
- // An I2C touch controller with no explicit bus pins: the overlay enables
81
- // i2c0 and instantiates the node, but nothing remuxes the controller to
82
- // the wired SDA/SCL (the board's default I2C pins rarely match a
83
- // breakout). Every I2C read then fails and touch silently does nothing —
84
- // surface it at build time instead of leaving it to a multimeter.
85
- if (touch && touch.controller !== 'xpt2046' && (touch.sda === undefined || touch.scl === undefined)) {
86
- diagnostics.push({
87
- severity: "warning",
88
- message: `touch: I2C controller '${touch.controller}' has no sda/scl pins in cuttlefish.config.ts — the overlay enables the bus without a pin assignment, so the controller may never answer. Add touch.sda and touch.scl (the board's default I2C pins are rarely the wired ones).`,
89
- });
90
- }
91
- const lines: string[] = [
92
- '/* Auto-generated by @typecad/framework-zephyr from cuttlefish.config.ts. */',
93
- '/* Enables peripherals the program uses. West merges this over the board DT. */',
94
- '',
95
- ];
96
-
97
- // Pinctrl override blocks reference the esp32s3 pinmux tokens; the include
98
- // must precede them. Emitted when the display SPI pins are overridden.
99
- if (wiring?.sck !== undefined && wiring?.mosi !== undefined) {
100
- lines.splice(2, 0, '#include <zephyr/dt-bindings/pinctrl/esp32s3-pinctrl.h>', '');
101
- }
102
-
103
- const block = (label: string, extra: string[] = []): void => {
104
- lines.push(`&${label} {`);
105
- lines.push(` status = "okay";`);
106
- for (const e of extra) lines.push(` ${e}`);
107
- lines.push('};');
108
- lines.push('');
109
- };
110
-
111
- if (usage.usesI2c && chip.i2c) {
112
- for (const c of chip.i2c.controllers) block(c.nodeLabel);
113
- }
114
- if (usage.usesSpi && chip.spi) {
115
- for (const c of chip.spi.controllers) block(c.nodeLabel);
116
- }
117
- if (usage.usesUart && chip.uart) {
118
- for (const c of chip.uart.controllers) block(c.nodeLabel);
119
- }
120
- // DAC: enable the chip's DAC device node when the program uses dac.*. The
121
- // lowering references DEVICE_DT_GET(DT_NODELABEL(<dac.device>)).
122
- if (usage.usesDac && chip.dac) {
123
- block(chip.dac.device);
124
- }
125
- if (display) {
126
- // Emit a full display DT node definition. Boards like the ESP32 devkit
127
- // have no display node in their base DT, so a bare `&display0 { status }`
128
- // fails (the nodelabel doesn't exist). Define the node attached to a SPI
129
- // controller with the panel's compatible string + dimensions. The pin
130
- // wiring (cs/dc/rst) uses ESP32 GPIO defaults from the demo config; a
131
- // real board overlay would carry its own binding.
132
- // An XPT2046 on the same bus needs its CS as the second cs-gpios entry,
133
- // so thread it into the display block (DT assignment replaces the whole
134
- // property both entries must be written together).
135
- emitDisplayNode(
136
- lines,
137
- display,
138
- wiring,
139
- touch?.controller === 'xpt2046' ? (touch?.cs ?? DEFAULT_XPT2046_CS) : undefined,
140
- );
141
- }
142
- // Touch — FT6336U capacitive on I2C (references DT_NODELABEL(ft6336u)) or
143
- // XPT2046 resistive on the display's SPI bus (references
144
- // DT_NODELABEL(xpt2046)). Same rationale as the display node: the bare
145
- // devkit has no such node. Gated on usesTouch (not usesI2c/chip.i2c) so it
146
- // emits even when the chip descriptor doesn't declare bus controllers.
147
- if (usage.usesTouch) {
148
- emitTouchNode(lines, touch, display);
149
- }
150
-
151
- // Preferences (ZMS settings backend): point the settings subsystem at the
152
- // board's storage_partition. The backend looks for /chosen
153
- // zephyr,settings-partition first, then falls back to a fixed-partition
154
- // labeled storage_partition most Zephyr boards define that label, so this
155
- // chosen entry makes the lookup explicit and survives boards that name the
156
- // partition differently. It only adds a /chosen pointer (never redeclares
157
- // the partition node west errors if a node is multiply-defined).
158
- if (usage.usesPreferences) {
159
- lines.push('/ {');
160
- lines.push(' chosen {');
161
- lines.push(' zephyr,settings-partition = &storage_partition;');
162
- lines.push(' };');
163
- lines.push('};');
164
- lines.push('');
165
- }
166
-
167
- // PSRAM: enable the psram0 DT node with the correct size. The devkitc board
168
- // DT defaults to a no-PSRAM module variant (e.g. wroom_n8); a PSRAM-capable
169
- // module (N16R8, N8R8) needs the node enabled + sized so the linker maps
170
- // .ext_ram sections into the real PSRAM. OPI on ESP32-S3 = 8MB octal PSRAM.
171
- if (usage.psram) {
172
- lines.push('&psram0 {');
173
- lines.push(' status = "okay";');
174
- lines.push(' size = <(DT_SIZE_M(8))>;');
175
- lines.push('};');
176
- lines.push('');
177
- }
178
-
179
- return lines.join('\n');
180
- }
181
-
182
- /**
183
- * Emit a display DT node definition. The node is attached to the profile's SPI
184
- * controller (default spi2, the ESP32's first user SPI controller) via a MIPI
185
- * DBI SPI bridge. Pin wiring comes from the display config (cs/dc/rst);
186
- * defaults match the demo-st wiring if absent. The compatible string + node
187
- * props come from the profile's panel controller (st7796s carries the required
188
- * pgc/ngc gamma + madctl; ili9341's binding defaults everything else).
189
- *
190
- * spiTouchCs: when an XPT2046 SPI touch controller shares the bus, its CS is
191
- * appended as the second cs-gpios entry (the touch node uses reg = <1>) — DT
192
- * property assignment replaces, so both entries must be written together.
193
- */
194
- function emitDisplayNode(
195
- lines: string[],
196
- display: ZephyrDisplayProfile,
197
- wiring?: DisplayWiring,
198
- spiTouchCs?: number,
199
- ): void {
200
- const bus = display.busLabel ?? 'spi2';
201
- const controller = panelControllerFor(display);
202
- const compatible = display.dtCompatible ?? PANEL_CONTROLLER_DEFAULTS[controller].dtCompatible;
203
- const dc = wiring?.dc ?? 17;
204
- const rst = wiring?.rst ?? 16;
205
- const cs = wiring?.cs ?? 5;
206
- const freq = wiring?.spiFrequency ?? 80000000;
207
- // DT node describes the NATIVE panel geometry; the effective (rotated)
208
- // dimensions live in the display profile.
209
- const nativeW = display.nativeWidth ?? display.width;
210
- const nativeH = display.nativeHeight ?? display.height;
211
- const sck = wiring?.sck;
212
- const mosi = wiring?.mosi;
213
- const miso = wiring?.miso;
214
- // ESP32-S3 GPIOs 0-31 are on gpio0, 32-48 on gpio1.
215
- const gpioController = (pin: number) => pin <= 31 ? 'gpio0' : 'gpio1';
216
- // The board's spim2_default pinctrl group usually targets the devkitc's
217
- // default SPI pins (SCLK=12/MOSI=11), which rarely match the display's
218
- // wiring. When the config declares spiPins, override the pinctrl groups to
219
- // remux the controller onto the panel's actual pins. Raw ESP32_PINMUX()
220
- // macros are used instead of the named SPIM2_*_GPIOxx tokens because the
221
- // bindings header omits GPIOs 22-25 from those lists.
222
- if (sck !== undefined && mosi !== undefined) {
223
- // 'spi2' pinctrl group 'spim2_default' (ESP32 SPI-master naming).
224
- const pinctrlGroup = bus.replace(/^spi(\d)$/, 'spim$1') + '_default';
225
- lines.push(`&${pinctrlGroup} {`);
226
- lines.push(' group1 {');
227
- lines.push(` pinmux = <ESP32_PINMUX(${miso ?? 19}, ESP_FSPIQ_IN, ESP_NOSIG)>,`);
228
- lines.push(` <ESP32_PINMUX(${sck}, ESP_NOSIG, ESP_FSPICLK_OUT)>,`);
229
- lines.push(' <ESP32_PINMUX(10, ESP_NOSIG, ESP_FSPICS0_OUT)>;');
230
- lines.push(' };');
231
- lines.push(' group2 {');
232
- lines.push(` pinmux = <ESP32_PINMUX(${mosi}, ESP_NOSIG, ESP_FSPID_OUT)>;`);
233
- lines.push(' output-low;');
234
- lines.push(' };');
235
- lines.push('};');
236
- lines.push('');
237
- }
238
- lines.push('&dma {');
239
- lines.push(' status = "okay";');
240
- lines.push('};');
241
- lines.push('');
242
- lines.push(`&${bus} {`);
243
- lines.push(' status = "okay";');
244
- lines.push(` cs-gpios = <&${gpioController(cs)} ${cs} GPIO_ACTIVE_LOW>${spiTouchCs !== undefined ? `, <&${gpioController(spiTouchCs)} ${spiTouchCs} GPIO_ACTIVE_LOW>` : ''};`);
245
- // Enable GDMA for the SPI2 host. The ESP32 SPI driver uses DMA only when
246
- // dma-enabled is set AND dmas wires tx/rx channels to the GDMA controller;
247
- // without it, transfers run PIO through the 64-byte FIFO (~4MHz effective at
248
- // 80MHz requested). The GDMA driver allocates channels per peripheral based
249
- // on spi2's dma-host=<0> (set in the SoC dtsi), so the channel cells are the
250
- // GDMA instance's rx/tx slot ids (2 here = an SPI2-dedicated pair; the GDMA
251
- // binding's #dma-cells = <1> carries the channel).
252
- lines.push(' dma-enabled;');
253
- lines.push(' dmas = <&dma 0>, <&dma 1>;');
254
- lines.push(' dma-names = "rx", "tx";');
255
- lines.push('};');
256
- lines.push('');
257
- lines.push('/ {');
258
- lines.push(' mipi_dbi: mipi-dbi {');
259
- lines.push(' compatible = "zephyr,mipi-dbi-spi";');
260
- lines.push(` spi-dev = <&${bus}>;`);
261
- lines.push(` dc-gpios = <&${gpioController(dc)} ${dc} GPIO_ACTIVE_HIGH>;`);
262
- lines.push(` reset-gpios = <&${gpioController(rst)} ${rst} GPIO_ACTIVE_LOW>;`);
263
- lines.push(' write-only;');
264
- lines.push(' #address-cells = <1>;');
265
- lines.push(' #size-cells = <0>;');
266
- lines.push(` ${display.dtLabel}: display@0 {`);
267
- lines.push(` compatible = "${compatible}";`);
268
- lines.push(' reg = <0>;');
269
- if (wiring?.tearingEffectPin !== undefined) {
270
- const tePin = wiring.tearingEffectPin!;
271
- // Tearing-effect input on the display node: GPIO_DT_SPEC_GET(
272
- // DT_NODELABEL(display0), te_gpios) in the adapter. Opt-in —
273
- // most modules don't break the TE pad out.
274
- lines.push(` te-gpios = <&${gpioController(tePin)} ${tePin} GPIO_ACTIVE_HIGH>;`);
275
- }
276
- lines.push(` mipi-max-frequency = <${freq}>;`);
277
- lines.push(' mipi-mode = "MIPI_DBI_MODE_SPI_4WIRE";');
278
- // Required by the lcd-controller binding (Zephyr 4.x): 0 = RGB565,
279
- // matching upstream ILI9341 boards (esp_wrover_kit) and the C++
280
- // runtime, which drives these SPI TFTs as RGB565.
281
- lines.push(' pixel-format = <0>;');
282
- lines.push(` width = <${nativeW}>;`);
283
- lines.push(` height = <${nativeH}>;`);
284
- if (controller === 'st7796s') {
285
- // MADCTL: rotation 1 (landscape, MV=1) + BGR bit, matching the adapter's
286
- // direct-drive init (0x28). The DT copy keeps the stock driver's init
287
- // consistent if it is ever exercised.
288
- lines.push(' madctl = <0x28>;');
289
- lines.push(' pgc = [f0 09 0b 06 04 2e 46 46 39 13 15 12 15 12];');
290
- lines.push(' ngc = [f0 09 0b 06 04 2e 46 46 39 13 15 12 15 12];');
291
- } else {
292
- // ILI9341: the ilitek,ili9341 binding carries defaults for every register
293
- // (gamma, power, porch) and expresses orientation via `rotation` (degrees)
294
- // instead of a raw MADCTL no panel-specific props are required.
295
- lines.push(` rotation = <${display.rotation ?? 0}>;`);
296
- }
297
- lines.push(' };');
298
- lines.push(' };');
299
- lines.push('};');
300
- lines.push('');
301
- // Backlight node: emit only when the config provides a backlight GPIO. The
302
- // pin was previously hardcoded to 4, which collided with the FT6336U
303
- // reset-gpios on the same pin (demo-st's backlight is hardwired to 3.3V, so
304
- // no backlight node is correct there). When present, use the configured pin
305
- // and the profile's alias name so the display adapter can drive it.
306
- const blPin = wiring?.backlightPin;
307
- if (display.backlight && blPin !== undefined) {
308
- lines.push('/ {');
309
- lines.push(' aliases {');
310
- lines.push(` ${display.backlight} = &bl_led;`);
311
- lines.push(' };');
312
- lines.push(' bl_gpio: bl-gpio-leds {');
313
- lines.push(' compatible = "gpio-leds";');
314
- lines.push(' bl_led: led {');
315
- lines.push(` gpios = <&${gpioController(blPin)} ${blPin} GPIO_ACTIVE_HIGH>;`);
316
- lines.push(' };');
317
- lines.push(' };');
318
- lines.push('};');
319
- lines.push('');
320
- }
321
- }
322
-
323
- /** Default XPT2046 CS/IRQ pins (ESP32-S3 GPIOs clear of the demo-st display
324
- * wiring: 5/17/16/15 and the remuxed SPI pins). Config values override. */
325
- const DEFAULT_XPT2046_CS = 6;
326
- const DEFAULT_XPT2046_IRQ = 7;
327
-
328
- /**
329
- * Emit the touch DT node for the configured controller.
330
- *
331
- * FT6336U (capacitive, I2C): node on i2c0 at the FT6336U default address
332
- * (0x38); the C++ touch adapter reads it via i2c_write_read_dt.
333
- *
334
- * XPT2046 (resistive, SPI): node on the display's SPI bus as CS index 1. The
335
- * in-tree xptek,xpt2046 binding (drivers/input) is register-matched for the
336
- * raw SPI access — CONFIG_INPUT stays off, so the in-tree input driver does
337
- * not build and the adapter owns the chip (same pattern as FT6336U reusing
338
- * the ft5336 binding). The binding requires int-gpios, touchscreen-size-*,
339
- * and min/max calibration props, so defaults are filled for anything the
340
- * config omits.
341
- */
342
- function emitTouchNode(
343
- lines: string[],
344
- touch: TouchWiring | undefined,
345
- display: ZephyrDisplayProfile | undefined,
346
- ): void {
347
- if (touch?.controller === 'xpt2046') {
348
- emitXpt2046Node(lines, touch, display);
349
- return;
350
- }
351
- emitFt6336uNode(lines, touch);
352
- }
353
-
354
- /** FT6336U capacitive touch node on the first I2C controller. */
355
- function emitFt6336uNode(lines: string[], touch?: TouchWiring): void {
356
- const irq = touch?.irq ?? 15;
357
- const resetPin = touch?.resetPin;
358
- const sda = touch?.sda;
359
- const scl = touch?.scl;
360
- // ESP32-S3 GPIOs 0-31 are on gpio0, 32-48 on gpio1.
361
- const gpioController = (pin: number) => pin <= 31 ? 'gpio0' : 'gpio1';
362
- // Zephyr has no ft6336 binding the FT6336U is register-compatible with the
363
- // ft5336 binding (same Focaltech register map: TD_STATUS at 0x02, coordinate
364
- // registers 0x03-0x06). The C++ touch adapter reads these directly via I2C,
365
- // so the binding just needs to exist for the DT node to resolve.
366
- // When SDA/SCL are provided, remux the I2C bus to those pins via pinctrl
367
- // (the board's default I2C pins rarely match a breakout's wiring).
368
- if (sda !== undefined && scl !== undefined) {
369
- lines.push('&pinctrl {');
370
- lines.push(' i2c0_touch: i2c0_touch {');
371
- lines.push(' group1 {');
372
- lines.push(` pinmux = <I2C0_SDA_GPIO${sda}>, <I2C0_SCL_GPIO${scl}>;`);
373
- lines.push(' bias-pull-up;');
374
- lines.push(' drive-open-drain;');
375
- lines.push(' };');
376
- lines.push(' };');
377
- lines.push('};');
378
- lines.push('');
379
- lines.push('&i2c0 {');
380
- lines.push(' status = "okay";');
381
- lines.push(' pinctrl-0 = <&i2c0_touch>;');
382
- lines.push(' pinctrl-names = "default";');
383
- } else {
384
- lines.push('&i2c0 {');
385
- lines.push(' status = "okay";');
386
- }
387
- lines.push(' ft6336u: ft6336u@38 {');
388
- lines.push(' compatible = "focaltech,ft5336";');
389
- lines.push(' reg = <0x38>;');
390
- // NOTE: int-gpios is intentionally omitted. The ft5336 Zephyr driver
391
- // registers a GPIO interrupt on int-gpios, which triggers an assertion
392
- // failure in the ESP32 interrupt controller (VECDESC_FL_SHARED conflict).
393
- // The cuttlefish touch adapter polls touch_isTouched() via I2C every frame
394
- // it never uses the IRQ pin, so the interrupt registration is unnecessary.
395
- if (resetPin !== undefined) {
396
- lines.push(` reset-gpios = <&${gpioController(resetPin)} ${resetPin} GPIO_ACTIVE_LOW>;`);
397
- }
398
- lines.push(' };');
399
- lines.push('};');
400
- lines.push('');
401
- }
402
-
403
- /** XPT2046 resistive touch node on the display's SPI bus (CS index 1). */
404
- function emitXpt2046Node(
405
- lines: string[],
406
- touch: TouchWiring,
407
- display: ZephyrDisplayProfile | undefined,
408
- ): void {
409
- const bus = display?.busLabel ?? 'spi2';
410
- const cs = touch.cs ?? DEFAULT_XPT2046_CS;
411
- const irq = touch.irq ?? DEFAULT_XPT2046_IRQ;
412
- const cal = touch.calibration;
413
- // ESP32-S3 GPIOs 0-31 are on gpio0, 32-48 on gpio1.
414
- const gpioController = (pin: number) => pin <= 31 ? 'gpio0' : 'gpio1';
415
- const zThreshold = touch.minPressure ?? 400;
416
- // touchscreen-size-* describe the panel the touch layer sits on (the display
417
- // profile's effective size); a touch-only build falls back to the 12-bit
418
- // full-scale range so the binding's required props still resolve.
419
- const sizeX = display?.width ?? cal?.xMax ?? 320;
420
- const sizeY = display?.height ?? cal?.yMax ?? 240;
421
- // The display block already wrote cs-gpios with both entries (its CS at
422
- // index 0, the touch CS at index 1). When there is no display block, enable
423
- // the bus here with the touch CS as the only entry.
424
- if (!display) {
425
- lines.push(`&${bus} {`);
426
- lines.push(' status = "okay";');
427
- lines.push(` cs-gpios = <&${gpioController(cs)} ${cs} GPIO_ACTIVE_LOW>;`);
428
- lines.push('};');
429
- lines.push('');
430
- }
431
- lines.push(`&${bus} {`);
432
- lines.push(' xpt2046: xpt2046@1 {');
433
- lines.push(' compatible = "xptek,xpt2046";');
434
- lines.push(' reg = <1>;');
435
- // The XPT2046 datasheet max SPI clock is 2.5MHz — the panel bus may run at
436
- // 80MHz, but this node's spi-max-frequency gates only its own transactions
437
- // (the adapter's SPI_DT_SPEC picks it up).
438
- lines.push(' spi-max-frequency = <2500000>;');
439
- lines.push(` int-gpios = <&${gpioController(irq)} ${irq} GPIO_ACTIVE_LOW>;`);
440
- lines.push(` touchscreen-size-x = <${sizeX}>;`);
441
- lines.push(` touchscreen-size-y = <${sizeY}>;`);
442
- lines.push(` min-x = <${cal?.xMin ?? 0}>;`);
443
- lines.push(` max-x = <${cal?.xMax ?? 4095}>;`);
444
- lines.push(` min-y = <${cal?.yMin ?? 0}>;`);
445
- lines.push(` max-y = <${cal?.yMax ?? 4095}>;`);
446
- lines.push(` z-threshold = <${zThreshold}>;`);
447
- lines.push(' };');
448
- lines.push('};');
449
- lines.push('');
450
- }
1
+ // ---------------------------------------------------------------------------
2
+ // Devicetree overlay generator
3
+ //
4
+ // Generates <project>/app/boards/<board>.overlay — the standard Zephyr overlay
5
+ // path. Content is additive: for each used peripheral with a DT binding the chip
6
+ // descriptor knows, emit a `&nodelabel { status = "okay"; }` block enabling it.
7
+ // West merges this over the board's base DT; we never rewrite the base.
8
+ //
9
+ // Framework library-resolution hook: the artifact
10
+ // that brings external capabilities into a build. (Arduino does it by parsing
11
+ // library headers into .d.ts; Zephyr does it by enabling DT nodes + Kconfig.)
12
+ // ---------------------------------------------------------------------------
13
+
14
+ import type { ZephyrChipDescriptor } from '../chips/types.js';
15
+ import type { ZephyrDisplayProfile } from '../display/profiles.js';
16
+ import { PANEL_CONTROLLER_DEFAULTS, panelControllerFor } from '../display/profiles.js';
17
+ import { SENSOR_PART_INFO } from '@typecad/hal';
18
+ import type { KconfigUsage } from './kconfig.js';
19
+
20
+ /** Display panel CS default shared by the display block and the SPI-sensor
21
+ * cs-gpios merge so both sites write the same entry. */
22
+ const DEFAULT_DISPLAY_CS = 5;
23
+
24
+ /**
25
+ * Generate the overlay source for a board + usage. Returns the overlay text
26
+ * (caller writes it to app/boards/<board>.overlay via writeIfChanged).
27
+ */
28
+ /** Display wiring from cuttlefish.config.ts (cs/dc/rst/spiFrequency/spiPins). */
29
+ export interface DisplayWiring {
30
+ cs?: number;
31
+ dc?: number;
32
+ rst?: number;
33
+ /** Tearing-effect (TE) GPIO from display.tearingEffectPin emitted as
34
+ * te-gpios on the display DT node. Opt-in; most boards don't wire TE. */
35
+ tearingEffectPin?: number;
36
+ spiFrequency?: number;
37
+ /** SPI bus pins. When present, the overlay remuxes the SPI controller's
38
+ * pinctrl to these pins (the board defaults rarely match a breakout's
39
+ * wiring e.g. demo-st's panel is on SCK=18/MOSI=23, not the devkitc
40
+ * default 12/11). */
41
+ sck?: number;
42
+ mosi?: number;
43
+ miso?: number;
44
+ /** Optional GPIO driving the panel backlight. When set, the overlay emits a
45
+ * `gpio-leds` node + DT alias (named by the profile's `backlight`) so the
46
+ * display adapter can drive it high at init. When absent (e.g. the panel's
47
+ * backlight is hardwired to 3.3V), no node or alias is emitted — the C++
48
+ * backlight init is guarded by `DT_HAS_ALIAS` and compiles away. */
49
+ backlightPin?: number;
50
+ }
51
+
52
+ /** Touch wiring from cuttlefish.config.ts (irq/resetPin/sda/scl; cs for SPI
53
+ * resistive controllers, calibration for the XPT2046 DT binding). */
54
+ export interface TouchWiring {
55
+ /** Touch controller kind — selects the DT node shape (FT6336U node on I2C0
56
+ * vs XPT2046 node on the display's SPI bus). Default 'ft6336u'. */
57
+ controller?: 'ft6336u' | 'xpt2046';
58
+ irq?: number;
59
+ resetPin?: number;
60
+ sda?: number;
61
+ scl?: number;
62
+ /** XPT2046 only: SPI CS pin (second cs-gpios entry on the panel's bus). */
63
+ cs?: number;
64
+ /** XPT2046 only: raw ADC calibration — feeds the binding's min-x/max-x/
65
+ * min-y/max-y (required props). Defaults span the full 12-bit range. */
66
+ calibration?: { xMin: number; xMax: number; yMin: number; yMax: number };
67
+ /** XPT2046 only: pen-detect Z1 threshold (binding's z-threshold). Resistive
68
+ * panels need a few hundred 12-bit counts; default 400. */
69
+ minPressure?: number;
70
+ }
71
+
72
+ export interface OverlayDiagnostic {
73
+ severity: "warning" | "error";
74
+ message: string;
75
+ }
76
+
77
+ export function generateOverlay(
78
+ chip: ZephyrChipDescriptor,
79
+ usage: KconfigUsage,
80
+ display: ZephyrDisplayProfile | undefined,
81
+ wiring?: DisplayWiring,
82
+ touch?: TouchWiring,
83
+ diagnostics: OverlayDiagnostic[] = [],
84
+ ): string {
85
+ // An I2C touch controller with no explicit bus pins: the overlay enables
86
+ // i2c0 and instantiates the node, but nothing remuxes the controller to
87
+ // the wired SDA/SCL (the board's default I2C pins rarely match a
88
+ // breakout). Every I2C read then fails and touch silently does nothing
89
+ // surface it at build time instead of leaving it to a multimeter.
90
+ if (touch && touch.controller !== 'xpt2046' && (touch.sda === undefined || touch.scl === undefined)) {
91
+ diagnostics.push({
92
+ severity: "warning",
93
+ message: `touch: I2C controller '${touch.controller}' has no sda/scl pins in cuttlefish.config.ts — the overlay enables the bus without a pin assignment, so the controller may never answer. Add touch.sda and touch.scl (the board's default I2C pins are rarely the wired ones).`,
94
+ });
95
+ }
96
+ const lines: string[] = [
97
+ '/* Auto-generated by @typecad/framework-zephyr from cuttlefish.config.ts. */',
98
+ '/* Enables peripherals the program uses. West merges this over the board DT. */',
99
+ '',
100
+ ];
101
+
102
+ // Pinctrl override blocks reference the esp32s3 pinmux tokens; the include
103
+ // must precede them. Emitted when the display SPI pins are overridden.
104
+ if (wiring?.sck !== undefined && wiring?.mosi !== undefined) {
105
+ lines.splice(2, 0, '#include <zephyr/dt-bindings/pinctrl/esp32s3-pinctrl.h>', '');
106
+ }
107
+
108
+ const block = (label: string, extra: string[] = []): void => {
109
+ lines.push(`&${label} {`);
110
+ lines.push(` status = "okay";`);
111
+ for (const e of extra) lines.push(` ${e}`);
112
+ lines.push('};');
113
+ lines.push('');
114
+ };
115
+
116
+ // Controllers whose board DT ships no default pinctrl group (rp2xxx uart1)
117
+ // carry synthesis data: emit the group under &pinctrl, then enable the
118
+ // controller referencing it. Controllers with a pinctrlRef wire an existing
119
+ // group instead; `props` adds raw property lines for bindings with
120
+ // required properties the board only sets on its own wired nodes.
121
+ // The pinmux header include is collected and spliced before all blocks
122
+ // (tokens must be defined before use).
123
+ const pinctrlIncludes = new Map<string, string[]>();
124
+ const controllerBlock = (c: { nodeLabel: string; pinctrlRef?: string; props?: readonly string[]; pinctrl?: { include: string; pinmux: readonly string[]; inputPinmux?: readonly string[]; defines?: readonly string[] } }): void => {
125
+ const extra: string[] = [...(c.props ?? [])];
126
+ if (c.pinctrl) {
127
+ pinctrlIncludes.set(c.pinctrl.include, c.pinctrl.defines ? [...c.pinctrl.defines] : []);
128
+ const group = `${c.nodeLabel}_default`;
129
+ lines.push('&pinctrl {');
130
+ lines.push(` ${group}: ${group} {`);
131
+ lines.push(` group1 {`);
132
+ lines.push(` pinmux = <${c.pinctrl.pinmux.join(', ')}>;`);
133
+ lines.push(` };`);
134
+ if (c.pinctrl.inputPinmux && c.pinctrl.inputPinmux.length > 0) {
135
+ lines.push(` group2 {`);
136
+ lines.push(` pinmux = <${c.pinctrl.inputPinmux.join(', ')}>;`);
137
+ lines.push(` input-enable;`);
138
+ lines.push(` };`);
139
+ }
140
+ lines.push(` };`);
141
+ lines.push('};');
142
+ lines.push('');
143
+ extra.push(`pinctrl-0 = <&${group}>;`, `pinctrl-names = "default";`);
144
+ } else if (c.pinctrlRef) {
145
+ extra.push(`pinctrl-0 = <&${c.pinctrlRef}>;`, `pinctrl-names = "default";`);
146
+ }
147
+ block(c.nodeLabel, extra);
148
+ };
149
+
150
+ // Sensors ride a bus: a constructed sensor must enable its controller even
151
+ // when the program never calls i2c.*/spi.* directly (the sensor device
152
+ // handle is the only bus user) — merge their instances into the used sets.
153
+ const sensorParts = usage.sensorParts ?? [];
154
+ const i2cSensors = sensorParts.filter((s) => s.busKind !== 'spi');
155
+ const spiSensors = sensorParts.filter((s) => s.busKind === 'spi');
156
+ const spiTargets = usage.spiTargets ?? [];
157
+ const i2cInstances = new Set(usage.i2cUsedInstances ?? []);
158
+ for (const sp of i2cSensors) i2cInstances.add(sp.busIndex);
159
+ if ((usage.usesI2c || i2cSensors.length > 0) && chip.i2c) {
160
+ for (const [i, c] of chip.i2c.controllers.entries()) {
161
+ if (usage.i2cUsedInstances && !i2cInstances.has(i)) continue;
162
+ controllerBlock(c);
163
+ }
164
+ }
165
+ const spiInstances = new Set(usage.spiUsedInstances ?? []);
166
+ for (const sp of spiSensors) spiInstances.add(sp.busIndex);
167
+ for (const t of spiTargets) spiInstances.add(t.busIndex);
168
+ if ((usage.usesSpi || spiSensors.length > 0 || spiTargets.length > 0) && chip.spi) {
169
+ for (const [i, c] of chip.spi.controllers.entries()) {
170
+ if (usage.spiUsedInstances && !spiInstances.has(i)) continue;
171
+ controllerBlock(c);
172
+ }
173
+ }
174
+ // One DT child node per constructed I2C sensor — THE enable switch for the
175
+ // driver (its Kconfig is `default y` on this node's presence). The nodelabel
176
+ // is derived identically to the lowering's DEVICE_DT_GET target
177
+ // (lowering/sensor.ts sensorNames) — the two cannot drift.
178
+ for (const sp of i2cSensors) {
179
+ const ctrl = chip.i2c?.controllers[sp.busIndex];
180
+ const info = SENSOR_PART_INFO[sp.part.replace(/^SENSOR\./, '')];
181
+ if (!ctrl || !info) continue; // the lowering already threw on unknown parts
182
+ const nodeName = info.compatible.split(',')[1] ?? sp.part;
183
+ const addrHex = sp.port.toString(16);
184
+ lines.push(`&${ctrl.nodeLabel} {`);
185
+ lines.push(` tc_${sp.part.replace(/^SENSOR\./, '')}_i2c${sp.busIndex}_0x${addrHex}: ${nodeName}@${addrHex} {`);
186
+ lines.push(` compatible = "${info.compatible}";`);
187
+ lines.push(` reg = <0x${addrHex}>;`);
188
+ emitAlert(lines, chip, sp.alertPin, info.alert, 8);
189
+ lines.push(' };');
190
+ lines.push('};');
191
+ lines.push('');
192
+ }
193
+ if (usage.usesSpi && chip.spi) {
194
+ for (const [i, c] of chip.spi.controllers.entries()) {
195
+ if (usage.spiUsedInstances && !usage.spiUsedInstances.includes(i)) continue;
196
+ controllerBlock(c);
197
+ }
198
+ }
199
+ if (usage.usesUart && chip.uart) {
200
+ for (const [i, c] of chip.uart.controllers.entries()) {
201
+ if (usage.uartUsedInstances && !usage.uartUsedInstances.includes(i)) continue;
202
+ controllerBlock(c);
203
+ }
204
+ }
205
+ // Synthesized pinctrl groups reference pinmux tokens — splice their header
206
+ // includes ahead of every block (same slot the display path uses), with
207
+ // ifndef-guarded #defines for upstream header bugs in front.
208
+ for (const [include, defines] of pinctrlIncludes) {
209
+ const pre: string[] = [];
210
+ for (const d of defines) {
211
+ const name = d.trim().split(/\s+/)[0];
212
+ pre.push(`#ifndef ${name}`, `#define ${d}`, `#endif`);
213
+ }
214
+ lines.splice(2, 0, ...pre, `#include <${include}>`, '');
215
+ }
216
+ // Watchdog: the SoC dtsi declares the node disabled (STM32 iwdg ships
217
+ // status = "disabled" until a board enables it) — the wdt.* lowering
218
+ // addresses DEVICE_DT_GET(DT_NODELABEL(<nodeLabel>)), which needs the
219
+ // driver instance built, so enable the node when wdt_* is used.
220
+ if (usage.usesWdt && chip.wdt) {
221
+ block(chip.wdt.nodeLabel);
222
+ }
223
+ // USB CDC-ACM: the "next" USB device stack composes classes as devicetree
224
+ // children of the UDC controller node. Enable the controller and declare one
225
+ // cdc_acm_uart<N> child per instance the chip descriptor declares — the
226
+ // lowering addresses instance N as DT_NODELABEL(cdc_acm_uart<N>), so the
227
+ // nodelabels here and in lowering/usb.ts cannot drift. Endpoints and
228
+ // descriptors are assigned by the class driver at build time.
229
+ if (usage.usesUsb && chip.usb) {
230
+ lines.push(`&${chip.usb.controller} {`);
231
+ lines.push(' status = "okay";');
232
+ for (let i = 0; i < chip.usb.cdcInstances; i++) {
233
+ lines.push(` cdc_acm_uart${i}: cdc-acm-uart${i} {`);
234
+ lines.push(' compatible = "zephyr,cdc-acm-uart";');
235
+ lines.push(' };');
236
+ }
237
+ lines.push('};');
238
+ lines.push('');
239
+ }
240
+ // PWM: synthesized specs (controller + channel, no board-shipped alias) get
241
+ // a pwm-leds consumer node + tc-pwm<pin> alias here — the lowering addresses
242
+ // the channel as PWM_DT_SPEC_GET(DT_ALIAS(tc-pwm<pin>)), so the alias must
243
+ // exist in the merged DT. pwmDtAlias (lowering/pwm.ts) derives the same
244
+ // name from the pin; the two sides cannot drift. Filtered to the pins the
245
+ // program actually drives (pwmUsedPins) so the DT carries no dead channels;
246
+ // an undefined list (prepare-time overlay) emits all specs.
247
+ if (usage.usesPwm && chip.pwm) {
248
+ emitPwmNodes(lines, chip, usage.pwmUsedPins);
249
+ }
250
+ // ADC: enable the ADC device node, and on SoCs whose channels carry a
251
+ // pinctrl label (STM32: the pad stays in GPIO mode unless the ADC node's
252
+ // pinctrl-0 muxes it analog), rewrite pinctrl-0 to exactly the channels the
253
+ // program reads. DT assignment replaces the whole property — the board
254
+ // default (one pad) is deliberately dropped in favor of the used set.
255
+ if (usage.usesAdc && chip.adc) {
256
+ emitAdcNode(lines, chip, usage.adcReadPins);
257
+ }
258
+ // DAC: enable the chip's DAC device node when the program uses dac.*. The
259
+ // lowering references DEVICE_DT_GET(DT_NODELABEL(<dac.device>)); the used
260
+ // channels' pinctrl groups route the output to its pad.
261
+ if (usage.usesDac && chip.dac) {
262
+ emitDacNode(lines, chip, usage.dacWritePins);
263
+ }
264
+ // Counters: enable the free counter devices when the program uses hwtimer.
265
+ // Self form (nRF rtc1, STM32 rtc): enable the labeled node. Child form
266
+ // (ESP32 timer0-3): the counter device is an unlabeled `counter {}`
267
+ // child — the overlay defines the tc_counter<N> label the descriptor's
268
+ // nodeLabel references (the same label-less-child move the STM32 pwm
269
+ // path uses).
270
+ if (usage.usesHwtimer && chip.hwtimer) {
271
+ for (const c of chip.hwtimer.controllers) {
272
+ if (c.counterParent) {
273
+ lines.push(`&${c.counterParent} {`);
274
+ lines.push(' status = "okay";');
275
+ lines.push(` ${c.nodeLabel}: counter {`);
276
+ lines.push(' status = "okay";');
277
+ lines.push(' };');
278
+ lines.push('};');
279
+ lines.push('');
280
+ } else {
281
+ lines.push(`&${c.nodeLabel} {`);
282
+ lines.push(' status = "okay";');
283
+ lines.push('};');
284
+ lines.push('');
285
+ }
286
+ }
287
+ }
288
+ if (display) {
289
+ // Emit a full display DT node definition. Boards like the ESP32 devkit
290
+ // have no display node in their base DT, so a bare `&display0 { status }`
291
+ // fails (the nodelabel doesn't exist). Define the node attached to a SPI
292
+ // controller with the panel's compatible string + dimensions. The pin
293
+ // wiring (cs/dc/rst) uses ESP32 GPIO defaults from the demo config; a
294
+ // real board overlay would carry its own binding.
295
+ // An XPT2046 on the same bus needs its CS as the second cs-gpios entry,
296
+ // so thread it into the display block (DT assignment replaces the whole
297
+ // property — both entries must be written together).
298
+ emitDisplayNode(
299
+ lines,
300
+ display,
301
+ wiring,
302
+ touch?.controller === 'xpt2046' ? (touch?.cs ?? DEFAULT_XPT2046_CS) : undefined,
303
+ );
304
+ }
305
+ // Touch FT6336U capacitive on I2C (references DT_NODELABEL(ft6336u)) or
306
+ // XPT2046 resistive on the display's SPI bus (references
307
+ // DT_NODELABEL(xpt2046)). Same rationale as the display node: the bare
308
+ // devkit has no such node. Gated on usesTouch (not usesI2c/chip.i2c) so it
309
+ // emits even when the chip descriptor doesn't declare bus controllers.
310
+ if (usage.usesTouch) {
311
+ emitTouchNode(lines, touch, display);
312
+ }
313
+
314
+ // Preferences (ZMS settings backend): point the settings subsystem at the
315
+ // board's storage_partition. The backend looks for /chosen
316
+ // zephyr,settings-partition first, then falls back to a fixed-partition
317
+ // labeled storage_partition — most Zephyr boards define that label, so this
318
+ // chosen entry makes the lookup explicit and survives boards that name the
319
+ // partition differently. It only adds a /chosen pointer (never redeclares
320
+ // the partition node — west errors if a node is multiply-defined).
321
+ if (usage.usesPreferences || usage.usesFS) {
322
+ // Boards whose DTS ships no storage_partition (most STM32s define only
323
+ // the MCUboot boot/slot/scratch set) declare a synthesis region in the
324
+ // chip descriptor emit it as a partition@<offset> child of the flash0
325
+ // partitions node. Adding a NEW child via overlay is legal; redeclaring
326
+ // an existing one is not, so boards that already carry the label
327
+ // (descriptor storage.preexisting — ESP32's AMP layout) skip the
328
+ // synthesis block and only the /chosen pointer lands here.
329
+ if (chip.storage && !chip.storage.preexisting) {
330
+ lines.push('&flash0 {');
331
+ // Explicit compatible + cells: boards whose DTS already carries a
332
+ // partitions node (rp2040, STM32 MCUboot sets) merge identically, and
333
+ // boards without one (rpi_pico2) would otherwise hit dtc's spec
334
+ // default of #address-cells 2 / #size-cells 1 a 3-cell reg and a
335
+ // "length 8 not divisible by 12" error for the 2-cell reg below.
336
+ lines.push(' partitions {');
337
+ lines.push(' compatible = "fixed-partitions";');
338
+ lines.push(' #address-cells = <1>;');
339
+ lines.push(' #size-cells = <1>;');
340
+ lines.push(` storage_partition: partition@${chip.storage.offset.toString(16)} {`);
341
+ lines.push(' label = "storage";');
342
+ lines.push(` reg = <0x${chip.storage.offset.toString(16).padStart(8, '0')} 0x${chip.storage.size.toString(16).padStart(8, '0')}>;`);
343
+ lines.push(' };');
344
+ lines.push(' };');
345
+ lines.push('};');
346
+ lines.push('');
347
+ }
348
+ if (usage.usesPreferences) {
349
+ lines.push('/ {');
350
+ lines.push(' chosen {');
351
+ lines.push(' zephyr,settings-partition = &storage_partition;');
352
+ lines.push(' };');
353
+ lines.push('};');
354
+ lines.push('');
355
+ }
356
+ }
357
+
358
+ // PSRAM: enable the psram0 DT node with the correct size. The devkitc board
359
+ // DT defaults to a no-PSRAM module variant (e.g. wroom_n8); a PSRAM-capable
360
+ // module (N16R8, N8R8) needs the node enabled + sized so the linker maps
361
+ // .ext_ram sections into the real PSRAM. OPI on ESP32-S3 = 8MB octal PSRAM.
362
+ // SPI sensors: children whose reg is their cs-gpios INDEX. DT property
363
+ // assignment replaces the whole property, so cs-gpios is written here with
364
+ // every sensor CS on the controller, after the display block — a display
365
+ // panel sharing the bus is a wiring conflict this layer cannot merge (its
366
+ // CS comes from config wiring), so warn instead of silently dropping it.
367
+ // spi-max-frequency defaults to 1 MHz conservative for sensors whose
368
+ // datasheet max isn't in any binding; the constructor opts override it.
369
+ const SPI_SENSOR_HZ = 1000000;
370
+ const spiByCtrl = new Map<number, typeof spiSensors>();
371
+ for (const sp of spiSensors) {
372
+ const list = spiByCtrl.get(sp.busIndex) ?? [];
373
+ list.push(sp);
374
+ spiByCtrl.set(sp.busIndex, list);
375
+ }
376
+ // Target-only controllers (no sensors) still need the cs-gpios block.
377
+ for (const t of spiTargets) {
378
+ if (!spiByCtrl.has(t.busIndex)) spiByCtrl.set(t.busIndex, []);
379
+ }
380
+ for (const [idx, sensors] of spiByCtrl) {
381
+ const ctrl = chip.spi?.controllers[idx];
382
+ if (!ctrl) continue;
383
+ // A display panel on the same controller already wrote cs-gpios earlier
384
+ // in this overlay (its CS at index 0, an XPT2046 at 1). DT property
385
+ // assignment replaces, so this rewrite carries BOTH: the display entries
386
+ // first — keeping the panel/touch reg indexes stable — then the sensor CS
387
+ // pins. The entry construction mirrors emitDisplayNode's defaults via the
388
+ // shared DEFAULT_DISPLAY_CS/DEFAULT_XPT2046_CS constants.
389
+ const displayEntries: string[] = [];
390
+ if (display && (display.busLabel ?? 'spi2') === ctrl.nodeLabel) {
391
+ const displayCs = wiring?.cs ?? DEFAULT_DISPLAY_CS;
392
+ displayEntries.push(`<&gpio${displayCs <= 31 ? '0' : '1'} ${displayCs} GPIO_ACTIVE_LOW>`);
393
+ if (touch?.controller === 'xpt2046') {
394
+ const touchCs = touch.cs ?? DEFAULT_XPT2046_CS;
395
+ displayEntries.push(`<&gpio${touchCs <= 31 ? '0' : '1'} ${touchCs} GPIO_ACTIVE_LOW>`);
396
+ }
397
+ }
398
+ // Thin SPI targets ride this controller too: their CS entries append
399
+ // after the sensor pins, and their child nodes carry no compatible —
400
+ // a raw spi_dt_spec peer, not a driver node.
401
+ const ctrlTargets = spiTargets.filter((t) => t.busIndex === idx);
402
+ lines.push(`&${ctrl.nodeLabel} {`);
403
+ lines.push(` cs-gpios = ${displayEntries.concat(sensors.map((sp) => {
404
+ const gc = chip.gpioControllers?.find((g) => sp.port >= g.minPin && sp.port <= g.maxPin);
405
+ const ctrlLabel = gc?.nodelabel ?? 'gpio0';
406
+ return `<&${ctrlLabel} ${sp.port} GPIO_ACTIVE_LOW>`;
407
+ }), ctrlTargets.map((t) => {
408
+ const gc = chip.gpioControllers?.find((g) => t.cs >= g.minPin && t.cs <= g.maxPin);
409
+ const ctrlLabel = gc?.nodelabel ?? 'gpio0';
410
+ return `<&${ctrlLabel} ${t.cs} GPIO_ACTIVE_LOW>`;
411
+ })).join(', ')};`);
412
+ sensors.forEach((sp, sensorIdx) => {
413
+ const info = SENSOR_PART_INFO[sp.part.replace(/^SENSOR\./, '')];
414
+ if (!info) return;
415
+ const nodeName = info.compatible.split(',')[1] ?? sp.part;
416
+ const csIndex = displayEntries.length + sensorIdx;
417
+ lines.push(` tc_${sp.part}_spi${idx}_cs${sp.port}: ${nodeName}@${csIndex} {`);
418
+ lines.push(` compatible = "${info.compatible}";`);
419
+ lines.push(` reg = <${csIndex}>;`);
420
+ lines.push(` spi-max-frequency = <${sp.spiHz && sp.spiHz > 0 ? sp.spiHz : SPI_SENSOR_HZ}>;`);
421
+ // mode bits: 1 = CPHA, 2 = CPOL (presence props, no value).
422
+ if ((sp.spiMode ?? 0) & 2) lines.push(' spi-cpol;');
423
+ if ((sp.spiMode ?? 0) & 1) lines.push(' spi-cpha;');
424
+ emitAlert(lines, chip, sp.alertPin, info.alert, 8);
425
+ lines.push(' };');
426
+ });
427
+ ctrlTargets.forEach((t, targetIdx) => {
428
+ const csIndex = displayEntries.length + sensors.length + targetIdx;
429
+ lines.push(` tc_spit_spi${idx}_cs${t.cs}: spidev@${csIndex} {`);
430
+ // The app-local cuttlefish,spi-target binding (no driver — it makes
431
+ // gen_defines emit the spi properties SPI_DT_SPEC_GET reads).
432
+ lines.push(` compatible = "cuttlefish,spi-target";`);
433
+ lines.push(` reg = <${csIndex}>;`);
434
+ lines.push(` spi-max-frequency = <${t.hz && t.hz > 0 ? t.hz : SPI_SENSOR_HZ}>;`);
435
+ if ((t.mode ?? 0) & 2) lines.push(' spi-cpol;');
436
+ if ((t.mode ?? 0) & 1) lines.push(' spi-cpha;');
437
+ lines.push(' };');
438
+ });
439
+ lines.push('};');
440
+ lines.push('');
441
+ }
442
+
443
+ if (usage.psram) {
444
+ lines.push('&psram0 {');
445
+ lines.push(' status = "okay";');
446
+ lines.push(' size = <(DT_SIZE_M(8))>;');
447
+ lines.push('};');
448
+ lines.push('');
449
+ }
450
+
451
+ return lines.join('\n');
452
+ }
453
+
454
+ /**
455
+ * Emit the pwm-leds consumer node + aliases for synthesized PWM specs.
456
+ *
457
+ * A board DTS may enable a PWM controller (`pwm4`) without aliasing any
458
+ * channel — the STM32 pattern — while the lowering addresses channels via
459
+ * DT_ALIAS. Each synthesized spec gets a `pwm-leds` child whose `pwms` cell
460
+ * binds the controller+channel (+ period/polarity), and a `tc-pwm<pin>`
461
+ * alias pointing at it. Alias/label names are valid DT identifiers (lowercase
462
+ * alphanumerics + dash/underscore).
463
+ */
464
+ function emitPwmNodes(
465
+ lines: string[],
466
+ chip: ZephyrChipDescriptor,
467
+ usedPins?: readonly number[],
468
+ ): void {
469
+ const staticSynth = (chip.pwm?.specs ?? []).filter(
470
+ (s) => s.controller && s.channel != null && !s.dtSpec,
471
+ ).filter((s) => !usedPins || usedPins.includes(s.pin));
472
+
473
+ // Matrix PWM (ESP32 LEDC): channels are assigned here, over the pins the
474
+ // program actually drives (ascending — the same order lowering/pwm.ts
475
+ // emits the alias vars in; the C++ is channel-blind, so this is the single
476
+ // place a channel number is decided). The espressif,esp32-ledc binding
477
+ // needs three pieces per channel: a pinctrl pinmux token routing the
478
+ // channel to the pad (LEDC_CH<ch>_GPIO<pin> — every channel×pad pair
479
+ // exists in esp32s3-pinctrl.h, already included via the SoC dtsi), a
480
+ // channel child node (the driver's DT_INST_FOREACH_CHILD reads reg +
481
+ // timer from each), and the pwm-leds consumer the alias resolves to.
482
+ const m = chip.pwm?.matrix;
483
+ let matrixSynth: { pin: number; controller: string; channel: number }[] = [];
484
+ if (m) {
485
+ const pins = [...new Set(usedPins ? usedPins.filter((p) => m.pins.includes(p)) : m.pins)]
486
+ .sort((a, b) => a - b);
487
+ if (pins.length > m.channelCount) {
488
+ throw new Error(
489
+ `framework-zephyr: the program drives ${pins.length} PWM pins but ${chip.id}'s ` +
490
+ `${m.controller} exposes only ${m.channelCount} channels — drop ` +
491
+ `${pins.length - m.channelCount} pwm pin(s).`,
492
+ );
493
+ }
494
+ matrixSynth = pins.map((pin, ch) => ({ pin, controller: m.controller, channel: ch }));
495
+ // nRF shape: channels route to pads via psel (pinctrl psel order IS
496
+ // channel order — pwm_nrfx reads PSEL.OUT[ch] back), no channel child
497
+ // nodes, and the overlay carries the nrf pinctrl header for NRF_PSEL.
498
+ // P<port>.<pin> maps from the global number through the nRF controller
499
+ // layout (P0 = 0-31, P1 = 32-63).
500
+ const isNrfMatrix = /^nrf/.test(chip.soc);
501
+ if (isNrfMatrix) {
502
+ const inc = '#include <zephyr/dt-bindings/pinctrl/nrf-pinctrl.h>';
503
+ if (!lines.includes(inc)) lines.splice(2, 0, inc, '');
504
+ lines.push('&pinctrl {');
505
+ lines.push(` tc_${m.controller}_default: tc-${m.controller}-default {`);
506
+ lines.push(' group1 {');
507
+ lines.push(` psel = ${pins.map((p, ch) => `<NRF_PSEL(PWM_OUT${ch}, ${Math.floor(p / 32)}, ${p % 32})>`).join(', ')};`);
508
+ lines.push(' };');
509
+ lines.push(' };');
510
+ lines.push('};');
511
+ lines.push('');
512
+ lines.push(`&${m.controller} {`);
513
+ lines.push(' status = "okay";');
514
+ lines.push(` pinctrl-0 = <&tc_${m.controller}_default>;`);
515
+ lines.push(' pinctrl-names = "default";');
516
+ lines.push('};');
517
+ lines.push('');
518
+ }
519
+ if (!isNrfMatrix) {
520
+ lines.push('&pinctrl {');
521
+ lines.push(` tc_${m.controller}_default: tc-${m.controller}-default {`);
522
+ lines.push(' group1 {');
523
+ lines.push(` pinmux = ${pins.map((p, ch) => `<LEDC_CH${ch}_GPIO${p}>`).join(', ')};`);
524
+ lines.push(' output-enable;');
525
+ lines.push(' };');
526
+ lines.push(' };');
527
+ lines.push('};');
528
+ lines.push('');
529
+ // The four LEDC timers are round-robined so co-driven channels share a
530
+ // timer only past four (sharing is fine at equal periods; the driver
531
+ // reconfigures on pwm_set_dt when they differ).
532
+ lines.push(`&${m.controller} {`);
533
+ lines.push(' status = "okay";');
534
+ lines.push(` pinctrl-0 = <&tc_${m.controller}_default>;`);
535
+ lines.push(' pinctrl-names = "default";');
536
+ lines.push(' #address-cells = <1>;');
537
+ lines.push(' #size-cells = <0>;');
538
+ for (let ch = 0; ch < pins.length; ch++) {
539
+ lines.push(` channel${ch}@${ch} {`);
540
+ lines.push(` reg = <${ch}>;`);
541
+ lines.push(` timer = <${ch % 4}>;`);
542
+ lines.push(' };');
543
+ }
544
+ lines.push('};');
545
+ lines.push('');
546
+ }
547
+ }
548
+
549
+ const synthesized = staticSynth.concat(matrixSynth);
550
+ if (synthesized.length === 0) return;
551
+ // The pwms cells name the polarity as a macro (PWM_POLARITY_NORMAL). It is
552
+ // NOT universally available: only dts include chains that already use PWM
553
+ // bindings pull <zephyr/dt-bindings/pwm/pwm.h> in (STM32 does; the
554
+ // ESP32-S3 chain does not), so the overlay carries its own include.
555
+ // Spliced after the header comments, the established pattern (see the
556
+ // esp32s3-pinctrl.h splice above); guarded for the double-emit case.
557
+ if (!lines.includes('#include <zephyr/dt-bindings/pwm/pwm.h>')) {
558
+ lines.splice(2, 0, '#include <zephyr/dt-bindings/pwm/pwm.h>', '');
559
+ }
560
+ // Enable each distinct PWM controller node (idempotent when already okay).
561
+ // 16-bit fit: STM32 timers count period cycles in a 16-bit ARR, and the
562
+ // SoC dtsi default is st,prescaler = <0> (÷1) — a 20 ms servo period at
563
+ // 96 MHz is 1.92M cycles and pwm_stm32 rejects the channel. When the
564
+ // descriptor declares the timer clock, derive the smallest divider that
565
+ // fits the controller's slowest used period and override the prescaler
566
+ // on the timers parent node (the property lives there, not on the pwm
567
+ // child). pwm4 → timers4 is the STM32 nodelabel convention. Matrix
568
+ // controllers are skipped — the block above already enabled them (with
569
+ // their pinctrl + channel children, which this loop doesn't know).
570
+ const clockHz = chip.pwm?.clockHz;
571
+ for (const controller of [...new Set(staticSynth.map((s) => s.controller!))]) {
572
+ const timersMatch = controller.match(/^pwm(\d+)$/);
573
+ const specs = staticSynth.filter((s) => s.controller === controller);
574
+ // Macro-form pinctrl tokens (uppercase — DT labels are lowercase by
575
+ // spec): the overlay synthesizes the pad-routing group itself. RP2's
576
+ // macros live in a dt-bindings header the board chain may not pull in
577
+ // (spliced via rp2PinctrlHeader); Kinetis/LPC/GD32 macros come from the
578
+ // part headers the board's own pinctrl dtsi includes, and Zephyr
579
+ // preprocesses overlays together with the base DTS — no splice needed.
580
+ const isMacro = (p: string | undefined): boolean => !!p && /^[A-Z][A-Z0-9_]*$/.test(p);
581
+ if (!timersMatch && specs.some((s) => isMacro(s.pinctrl))) {
582
+ const header = rp2PinctrlHeader(chip.soc);
583
+ if (header) {
584
+ const inc = `#include <zephyr/dt-bindings/pinctrl/${header}>`;
585
+ if (!lines.includes(inc)) lines.splice(2, 0, inc, '');
586
+ }
587
+ lines.push('&pinctrl {');
588
+ lines.push(' tc_pwm_default: tc-pwm-default {');
589
+ lines.push(' group1 {');
590
+ lines.push(` pinmux = ${specs.filter((s) => s.pinctrl).map((s) => `<${s.pinctrl}>`).join(', ')};`);
591
+ lines.push(' };');
592
+ lines.push(' };');
593
+ lines.push('};');
594
+ lines.push('');
595
+ lines.push(`&${controller} {`);
596
+ lines.push(' status = "okay";');
597
+ // Atmel SAM TCC/TC in PWM mode: the SoC dtsi declares the base timer
598
+ // node (compatible atmel,sam0-tcc / …-tc, with clocks/assigned-clocks/
599
+ // channels/counter-size already present). The overlay overrides the
600
+ // compatible to the PWM binding, sets the required prescaler and the
601
+ // #pwm-cells the binding consts, and reuses the shared pinctrl group.
602
+ const samPwm = controller.match(/^(tcc|tc)(\d+)$/);
603
+ if (samPwm) {
604
+ lines.push(` compatible = "${samPwm[1] === 'tcc' ? 'atmel,sam0-tcc-pwm' : 'atmel,sam0-tc-pwm'}";`);
605
+ lines.push(' prescaler = <1024>;');
606
+ lines.push(' #pwm-cells = <2>;');
607
+ }
608
+ lines.push(' pinctrl-0 = <&tc_pwm_default>;');
609
+ lines.push(' pinctrl-names = "default";');
610
+ lines.push('};');
611
+ lines.push('');
612
+ continue;
613
+ }
614
+ if (timersMatch) {
615
+ // STM32 shape: the SoC dtsi declares a LABEL-LESS pwm child under
616
+ // timers{N} — the overlay defines the label itself (pwmN: pwm), which
617
+ // the synthesized pwms cell below references. One block carries the
618
+ // parent enable, the pad-routing pinctrl groups (harvested silicon
619
+ // routes on the specs — without them the PWM runs but nothing leaves
620
+ // the chip), the prescaler, and the child enable.
621
+ const groups = specs.filter((s) => s.pinctrl).map((s) => `&${s.pinctrl}`);
622
+ const maxPeriodNs = Math.max(...specs.map((s) => s.periodNs ?? 20_000_000));
623
+ // cycles = clockHz * period_s / divider ≤ 65536 (driver allows
624
+ // UINT16_MAX + 1); binding value is divider - 1 (CLK/(prescaler+1)).
625
+ const divider = clockHz
626
+ ? Math.max(1, Math.ceil((clockHz * maxPeriodNs) / 1e9 / 65536))
627
+ : 1;
628
+ lines.push(`&timers${timersMatch[1]} {`);
629
+ lines.push(' status = "okay";');
630
+ if (divider > 1) {
631
+ lines.push(` st,prescaler = <${divider - 1}>;`);
632
+ }
633
+ lines.push(` ${controller}: pwm {`);
634
+ lines.push(' status = "okay";');
635
+ if (groups.length > 0) {
636
+ lines.push(` pinctrl-0 = <${groups.join(' ')}>;`);
637
+ lines.push(' pinctrl-names = "default";');
638
+ }
639
+ lines.push(' };');
640
+ lines.push('};');
641
+ lines.push('');
642
+ continue;
643
+ }
644
+ // Lowercase pinctrl tokens (i.MX `iomuxc_…` node labels from the part
645
+ // dtsi) ride the controller's pinctrl-0 directly.
646
+ const pinRefs = specs.filter((s) => s.pinctrl && !isMacro(s.pinctrl)).map((s) => `&${s.pinctrl}`);
647
+ lines.push(`&${controller} {`);
648
+ lines.push(' status = "okay";');
649
+ if (pinRefs.length > 0) {
650
+ lines.push(` pinctrl-0 = <${pinRefs.join(' ')}>;`);
651
+ lines.push(' pinctrl-names = "default";');
652
+ }
653
+ lines.push('};');
654
+ lines.push('');
655
+ }
656
+ lines.push('/ {');
657
+ lines.push(' tc_pwm_leds: tc-pwm-leds {');
658
+ lines.push(' compatible = "pwm-leds";');
659
+ for (const s of synthesized) {
660
+ // 20 ms / 50 Hz default — the servo convention; the duty scaling in the
661
+ // lowering normalizes 0-255 against whatever period is baked in.
662
+ const period = s.periodNs ?? 20_000_000;
663
+ const polarity = s.polarity ?? 'PWM_POLARITY_NORMAL';
664
+ lines.push(` tc_pwm_${s.pin}: pwm-led-${s.pin} {`);
665
+ lines.push(` pwms = <&${s.controller} ${s.channel} ${period} ${polarity}>;`);
666
+ lines.push(' };');
667
+ }
668
+ lines.push(' };');
669
+ lines.push(' aliases {');
670
+ for (const s of synthesized) {
671
+ lines.push(` tc-pwm${s.pin} = &tc_pwm_${s.pin};`);
672
+ }
673
+ lines.push(' };');
674
+ lines.push('};');
675
+ lines.push('');
676
+ }
677
+
678
+ /**
679
+ * Emit the ADC device-node enable (+ optional pinctrl override). The pinctrl
680
+ * override applies only when the descriptor's channels carry pinctrl labels
681
+ * AND the caller knows which pins the program reads (compile-time regen scans
682
+ * the emitted `__tc_adc<N>_setup()` calls); the prepare-time overlay omits it
683
+ * and the compile regen rewrites the file before west runs.
684
+ *
685
+ * Two pinctrl forms by family: NODE labels (STM32's harvested
686
+ * `adc1_in1_pa1` groups — referenced as `&label`) and MACRO tokens (RP2's
687
+ * `ADC_CH0_P26` — DT labels are lowercase by spec and macros uppercase, so
688
+ * the case decides). The macro form synthesizes its own group under
689
+ * &pinctrl and splices the SoC's pinctrl dt-binding header so the token
690
+ * resolves in the overlay's compile.
691
+ *
692
+ * Multi-controller SoCs: channels may name their owning controller (STM32
693
+ * adc3-only routes, ESP32 units). One enable block is emitted per controller
694
+ * that owns used channels — the primary first, others sorted for
695
+ * deterministic output. Single-controller boards emit exactly one block, as
696
+ * before.
697
+ */
698
+ /** The SoC's pinctrl dt-binding header (where the macro tokens are defined)
699
+ * — RP2 shapes; undefined for socs that use node-label pinctrl exclusively. */
700
+ function rp2PinctrlHeader(soc: string): string | undefined {
701
+ if (soc === 'rp2040') return 'rpi-pico-rp2040-pinctrl.h';
702
+ if (soc.startsWith('rp2350a')) return 'rpi-pico-rp2350a-pinctrl.h';
703
+ if (soc.startsWith('rp2350b')) return 'rpi-pico-rp2350b-pinctrl.h';
704
+ return undefined;
705
+ }
706
+
707
+ function emitAdcNode(
708
+ lines: string[],
709
+ chip: ZephyrChipDescriptor,
710
+ readPins?: readonly number[],
711
+ ): void {
712
+ const adc = chip.adc!;
713
+ const primary = adc.nodeLabel;
714
+ const controllerOf = (c: { controller?: string }) => c.controller ?? primary;
715
+ const used = readPins ? adc.channels.filter((c) => readPins.includes(c.pin)) : adc.channels;
716
+ const isMacro = (p: string | undefined): boolean => !!p && /^[A-Z][A-Z0-9_]*$/.test(p);
717
+ const macroChannels = used.filter((c) => isMacro(c.pinctrl));
718
+ if (macroChannels.length > 0) {
719
+ const header = rp2PinctrlHeader(chip.soc);
720
+ if (header) {
721
+ const inc = `#include <zephyr/dt-bindings/pinctrl/${header}>`;
722
+ if (!lines.includes(inc)) lines.splice(2, 0, inc, '');
723
+ }
724
+ lines.push('&pinctrl {');
725
+ lines.push(' tc_adc_default: tc-adc-default {');
726
+ lines.push(' group1 {');
727
+ lines.push(` pinmux = ${macroChannels.map((c) => `<${c.pinctrl}>`).join(', ')};`);
728
+ lines.push(' };');
729
+ lines.push(' };');
730
+ lines.push('};');
731
+ lines.push('');
732
+ }
733
+ // Every controller that owns a USED channel gets an enable block — a
734
+ // controller left "disabled" in the SoC dtsi has no device instance, so a
735
+ // read against it fails to link. Nothing used: enable the primary only.
736
+ const controllers = used.length > 0
737
+ ? [...new Set(used.map(controllerOf))]
738
+ .sort((a, b) => (a === primary ? -1 : b === primary ? 1 : a.localeCompare(b, undefined, { numeric: true })))
739
+ : [primary];
740
+ for (const label of controllers) {
741
+ const mine = used.filter((c) => controllerOf(c) === label && c.pinctrl);
742
+ lines.push(`&${label} {`);
743
+ lines.push(' status = "okay";');
744
+ if (mine.length > 0) {
745
+ if (mine.every((c) => isMacro(c.pinctrl))) {
746
+ lines.push(' pinctrl-0 = <&tc_adc_default>;');
747
+ } else {
748
+ lines.push(` pinctrl-0 = <${mine.map((c) => `&${c.pinctrl}`).join(' ')}>;`);
749
+ }
750
+ lines.push(' pinctrl-names = "default";');
751
+ }
752
+ lines.push('};');
753
+ lines.push('');
754
+ }
755
+ }
756
+
757
+ /**
758
+ * Emit the DAC device-node enable + the used output channels' pinctrl
759
+ * groups (the harvested `dac1_out1_pa4`-style nodes — the st,stm32-dac
760
+ * binding includes pinctrl-device, so without the group the channel never
761
+ * reaches its pad). Mirrors emitAdcNode.
762
+ */
763
+ function emitDacNode(
764
+ lines: string[],
765
+ chip: ZephyrChipDescriptor,
766
+ writePins?: readonly number[],
767
+ ): void {
768
+ const dac = chip.dac!;
769
+ const labeled = dac.channels.filter((c) => c.pinctrl);
770
+ const used = labeled.filter((c) => !writePins || writePins.includes(c.pin));
771
+ lines.push(`&${dac.device} {`);
772
+ lines.push(' status = "okay";');
773
+ if (used.length > 0) {
774
+ lines.push(` pinctrl-0 = <${used.map((c) => `&${c.pinctrl}`).join(' ')}>;`);
775
+ lines.push(' pinctrl-names = "default";');
776
+ }
777
+ lines.push('};');
778
+ lines.push('');
779
+ }
780
+
781
+ /**
782
+ * Emit a display DT node definition. The node is attached to the profile's SPI
783
+ * controller (default spi2, the ESP32's first user SPI controller) via a MIPI
784
+ * DBI SPI bridge. Pin wiring comes from the display config (cs/dc/rst);
785
+ * defaults match the demo-st wiring if absent. The compatible string + node
786
+ * props come from the profile's panel controller (st7796s carries the required
787
+ * pgc/ngc gamma + madctl; ili9341's binding defaults everything else).
788
+ *
789
+ * spiTouchCs: when an XPT2046 SPI touch controller shares the bus, its CS is
790
+ * appended as the second cs-gpios entry (the touch node uses reg = <1>) — DT
791
+ * property assignment replaces, so both entries must be written together.
792
+ */
793
+ /** Emit alert-gpios for parts whose binding declares it (active high — the
794
+ * bindings describe the sensor's output level, e.g. sht3xd's ALERT). */
795
+ function emitAlert(lines: string[], chip: ZephyrChipDescriptor, alertPin: number | undefined, supported: boolean, indent: number): void {
796
+ if (alertPin === undefined || alertPin < 0 || !supported) return;
797
+ const gc = chip.gpioControllers?.find((g) => alertPin >= g.minPin && alertPin <= g.maxPin);
798
+ const pad = ' '.repeat(indent);
799
+ lines.push(`${pad}alert-gpios = <&${gc?.nodelabel ?? 'gpio0'} ${alertPin} GPIO_ACTIVE_HIGH>;`);
800
+ }
801
+
802
+ function emitDisplayNode(
803
+ lines: string[],
804
+ display: ZephyrDisplayProfile,
805
+ wiring?: DisplayWiring,
806
+ spiTouchCs?: number,
807
+ ): void {
808
+ const bus = display.busLabel ?? 'spi2';
809
+ const controller = panelControllerFor(display);
810
+ const compatible = display.dtCompatible ?? PANEL_CONTROLLER_DEFAULTS[controller].dtCompatible;
811
+ const dc = wiring?.dc ?? 17;
812
+ const rst = wiring?.rst ?? 16;
813
+ const cs = wiring?.cs ?? DEFAULT_DISPLAY_CS;
814
+ const freq = wiring?.spiFrequency ?? 80000000;
815
+ // DT node describes the NATIVE panel geometry; the effective (rotated)
816
+ // dimensions live in the display profile.
817
+ const nativeW = display.nativeWidth ?? display.width;
818
+ const nativeH = display.nativeHeight ?? display.height;
819
+ const sck = wiring?.sck;
820
+ const mosi = wiring?.mosi;
821
+ const miso = wiring?.miso;
822
+ // ESP32-S3 GPIOs 0-31 are on gpio0, 32-48 on gpio1.
823
+ const gpioController = (pin: number) => pin <= 31 ? 'gpio0' : 'gpio1';
824
+ // The board's spim2_default pinctrl group usually targets the devkitc's
825
+ // default SPI pins (SCLK=12/MOSI=11), which rarely match the display's
826
+ // wiring. When the config declares spiPins, override the pinctrl groups to
827
+ // remux the controller onto the panel's actual pins. Raw ESP32_PINMUX()
828
+ // macros are used instead of the named SPIM2_*_GPIOxx tokens because the
829
+ // bindings header omits GPIOs 22-25 from those lists.
830
+ if (sck !== undefined && mosi !== undefined) {
831
+ // 'spi2' → pinctrl group 'spim2_default' (ESP32 SPI-master naming).
832
+ const pinctrlGroup = bus.replace(/^spi(\d)$/, 'spim$1') + '_default';
833
+ lines.push(`&${pinctrlGroup} {`);
834
+ lines.push(' group1 {');
835
+ lines.push(` pinmux = <ESP32_PINMUX(${miso ?? 19}, ESP_FSPIQ_IN, ESP_NOSIG)>,`);
836
+ lines.push(` <ESP32_PINMUX(${sck}, ESP_NOSIG, ESP_FSPICLK_OUT)>,`);
837
+ lines.push(' <ESP32_PINMUX(10, ESP_NOSIG, ESP_FSPICS0_OUT)>;');
838
+ lines.push(' };');
839
+ lines.push(' group2 {');
840
+ lines.push(` pinmux = <ESP32_PINMUX(${mosi}, ESP_NOSIG, ESP_FSPID_OUT)>;`);
841
+ lines.push(' output-low;');
842
+ lines.push(' };');
843
+ lines.push('};');
844
+ lines.push('');
845
+ }
846
+ lines.push('&dma {');
847
+ lines.push(' status = "okay";');
848
+ lines.push('};');
849
+ lines.push('');
850
+ lines.push(`&${bus} {`);
851
+ lines.push(' status = "okay";');
852
+ lines.push(` cs-gpios = <&${gpioController(cs)} ${cs} GPIO_ACTIVE_LOW>${spiTouchCs !== undefined ? `, <&${gpioController(spiTouchCs)} ${spiTouchCs} GPIO_ACTIVE_LOW>` : ''};`);
853
+ // Enable GDMA for the SPI2 host. The ESP32 SPI driver uses DMA only when
854
+ // dma-enabled is set AND dmas wires tx/rx channels to the GDMA controller;
855
+ // without it, transfers run PIO through the 64-byte FIFO (~4MHz effective at
856
+ // 80MHz requested). The GDMA driver allocates channels per peripheral based
857
+ // on spi2's dma-host=<0> (set in the SoC dtsi), so the channel cells are the
858
+ // GDMA instance's rx/tx slot ids (2 here = an SPI2-dedicated pair; the GDMA
859
+ // binding's #dma-cells = <1> carries the channel).
860
+ lines.push(' dma-enabled;');
861
+ lines.push(' dmas = <&dma 0>, <&dma 1>;');
862
+ lines.push(' dma-names = "rx", "tx";');
863
+ lines.push('};');
864
+ lines.push('');
865
+ lines.push('/ {');
866
+ lines.push(' mipi_dbi: mipi-dbi {');
867
+ lines.push(' compatible = "zephyr,mipi-dbi-spi";');
868
+ lines.push(` spi-dev = <&${bus}>;`);
869
+ lines.push(` dc-gpios = <&${gpioController(dc)} ${dc} GPIO_ACTIVE_HIGH>;`);
870
+ lines.push(` reset-gpios = <&${gpioController(rst)} ${rst} GPIO_ACTIVE_LOW>;`);
871
+ lines.push(' write-only;');
872
+ lines.push(' #address-cells = <1>;');
873
+ lines.push(' #size-cells = <0>;');
874
+ lines.push(` ${display.dtLabel}: display@0 {`);
875
+ lines.push(` compatible = "${compatible}";`);
876
+ lines.push(' reg = <0>;');
877
+ if (wiring?.tearingEffectPin !== undefined) {
878
+ const tePin = wiring.tearingEffectPin!;
879
+ // Tearing-effect input on the display node: GPIO_DT_SPEC_GET(
880
+ // DT_NODELABEL(display0), te_gpios) in the adapter. Opt-in —
881
+ // most modules don't break the TE pad out.
882
+ lines.push(` te-gpios = <&${gpioController(tePin)} ${tePin} GPIO_ACTIVE_HIGH>;`);
883
+ }
884
+ lines.push(` mipi-max-frequency = <${freq}>;`);
885
+ lines.push(' mipi-mode = "MIPI_DBI_MODE_SPI_4WIRE";');
886
+ // Required by the lcd-controller binding (Zephyr 4.x): 0 = RGB565,
887
+ // matching upstream ILI9341 boards (esp_wrover_kit) and the C++
888
+ // runtime, which drives these SPI TFTs as RGB565.
889
+ lines.push(' pixel-format = <0>;');
890
+ lines.push(` width = <${nativeW}>;`);
891
+ lines.push(` height = <${nativeH}>;`);
892
+ if (controller === 'st7796s') {
893
+ // MADCTL: rotation 1 (landscape, MV=1) + BGR bit, matching the adapter's
894
+ // direct-drive init (0x28). The DT copy keeps the stock driver's init
895
+ // consistent if it is ever exercised.
896
+ lines.push(' madctl = <0x28>;');
897
+ lines.push(' pgc = [f0 09 0b 06 04 2e 46 46 39 13 15 12 15 12];');
898
+ lines.push(' ngc = [f0 09 0b 06 04 2e 46 46 39 13 15 12 15 12];');
899
+ } else {
900
+ // ILI9341: the ilitek,ili9341 binding carries defaults for every register
901
+ // (gamma, power, porch) and expresses orientation via `rotation` (degrees)
902
+ // instead of a raw MADCTL — no panel-specific props are required.
903
+ lines.push(` rotation = <${display.rotation ?? 0}>;`);
904
+ }
905
+ lines.push(' };');
906
+ lines.push(' };');
907
+ lines.push('};');
908
+ lines.push('');
909
+ // Backlight node: emit only when the config provides a backlight GPIO. The
910
+ // pin was previously hardcoded to 4, which collided with the FT6336U
911
+ // reset-gpios on the same pin (demo-st's backlight is hardwired to 3.3V, so
912
+ // no backlight node is correct there). When present, use the configured pin
913
+ // and the profile's alias name so the display adapter can drive it.
914
+ const blPin = wiring?.backlightPin;
915
+ if (display.backlight && blPin !== undefined) {
916
+ lines.push('/ {');
917
+ lines.push(' aliases {');
918
+ lines.push(` ${display.backlight} = &bl_led;`);
919
+ lines.push(' };');
920
+ lines.push(' bl_gpio: bl-gpio-leds {');
921
+ lines.push(' compatible = "gpio-leds";');
922
+ lines.push(' bl_led: led {');
923
+ lines.push(` gpios = <&${gpioController(blPin)} ${blPin} GPIO_ACTIVE_HIGH>;`);
924
+ lines.push(' };');
925
+ lines.push(' };');
926
+ lines.push('};');
927
+ lines.push('');
928
+ }
929
+ }
930
+
931
+ /** Default XPT2046 CS/IRQ pins (ESP32-S3 GPIOs clear of the demo-st display
932
+ * wiring: 5/17/16/15 and the remuxed SPI pins). Config values override. */
933
+ const DEFAULT_XPT2046_CS = 6;
934
+ const DEFAULT_XPT2046_IRQ = 7;
935
+
936
+ /**
937
+ * Emit the touch DT node for the configured controller.
938
+ *
939
+ * FT6336U (capacitive, I2C): node on i2c0 at the FT6336U default address
940
+ * (0x38); the C++ touch adapter reads it via i2c_write_read_dt.
941
+ *
942
+ * XPT2046 (resistive, SPI): node on the display's SPI bus as CS index 1. The
943
+ * in-tree xptek,xpt2046 binding (drivers/input) is register-matched for the
944
+ * raw SPI access — CONFIG_INPUT stays off, so the in-tree input driver does
945
+ * not build and the adapter owns the chip (same pattern as FT6336U reusing
946
+ * the ft5336 binding). The binding requires int-gpios, touchscreen-size-*,
947
+ * and min/max calibration props, so defaults are filled for anything the
948
+ * config omits.
949
+ */
950
+ function emitTouchNode(
951
+ lines: string[],
952
+ touch: TouchWiring | undefined,
953
+ display: ZephyrDisplayProfile | undefined,
954
+ ): void {
955
+ if (touch?.controller === 'xpt2046') {
956
+ emitXpt2046Node(lines, touch, display);
957
+ return;
958
+ }
959
+ emitFt6336uNode(lines, touch);
960
+ }
961
+
962
+ /** FT6336U capacitive touch node on the first I2C controller. */
963
+ function emitFt6336uNode(lines: string[], touch?: TouchWiring): void {
964
+ const irq = touch?.irq ?? 15;
965
+ const resetPin = touch?.resetPin;
966
+ const sda = touch?.sda;
967
+ const scl = touch?.scl;
968
+ // ESP32-S3 GPIOs 0-31 are on gpio0, 32-48 on gpio1.
969
+ const gpioController = (pin: number) => pin <= 31 ? 'gpio0' : 'gpio1';
970
+ // Zephyr has no ft6336 binding — the FT6336U is register-compatible with the
971
+ // ft5336 binding (same Focaltech register map: TD_STATUS at 0x02, coordinate
972
+ // registers 0x03-0x06). The C++ touch adapter reads these directly via I2C,
973
+ // so the binding just needs to exist for the DT node to resolve.
974
+ // When SDA/SCL are provided, remux the I2C bus to those pins via pinctrl
975
+ // (the board's default I2C pins rarely match a breakout's wiring).
976
+ if (sda !== undefined && scl !== undefined) {
977
+ lines.push('&pinctrl {');
978
+ lines.push(' i2c0_touch: i2c0_touch {');
979
+ lines.push(' group1 {');
980
+ lines.push(` pinmux = <I2C0_SDA_GPIO${sda}>, <I2C0_SCL_GPIO${scl}>;`);
981
+ lines.push(' bias-pull-up;');
982
+ lines.push(' drive-open-drain;');
983
+ lines.push(' };');
984
+ lines.push(' };');
985
+ lines.push('};');
986
+ lines.push('');
987
+ lines.push('&i2c0 {');
988
+ lines.push(' status = "okay";');
989
+ lines.push(' pinctrl-0 = <&i2c0_touch>;');
990
+ lines.push(' pinctrl-names = "default";');
991
+ } else {
992
+ lines.push('&i2c0 {');
993
+ lines.push(' status = "okay";');
994
+ }
995
+ lines.push(' ft6336u: ft6336u@38 {');
996
+ lines.push(' compatible = "focaltech,ft5336";');
997
+ lines.push(' reg = <0x38>;');
998
+ // NOTE: int-gpios is intentionally omitted. The ft5336 Zephyr driver
999
+ // registers a GPIO interrupt on int-gpios, which triggers an assertion
1000
+ // failure in the ESP32 interrupt controller (VECDESC_FL_SHARED conflict).
1001
+ // The cuttlefish touch adapter polls touch_isTouched() via I2C every frame
1002
+ // — it never uses the IRQ pin, so the interrupt registration is unnecessary.
1003
+ if (resetPin !== undefined) {
1004
+ lines.push(` reset-gpios = <&${gpioController(resetPin)} ${resetPin} GPIO_ACTIVE_LOW>;`);
1005
+ }
1006
+ lines.push(' };');
1007
+ lines.push('};');
1008
+ lines.push('');
1009
+ }
1010
+
1011
+ /** XPT2046 resistive touch node on the display's SPI bus (CS index 1). */
1012
+ function emitXpt2046Node(
1013
+ lines: string[],
1014
+ touch: TouchWiring,
1015
+ display: ZephyrDisplayProfile | undefined,
1016
+ ): void {
1017
+ const bus = display?.busLabel ?? 'spi2';
1018
+ const cs = touch.cs ?? DEFAULT_XPT2046_CS;
1019
+ const irq = touch.irq ?? DEFAULT_XPT2046_IRQ;
1020
+ const cal = touch.calibration;
1021
+ // ESP32-S3 GPIOs 0-31 are on gpio0, 32-48 on gpio1.
1022
+ const gpioController = (pin: number) => pin <= 31 ? 'gpio0' : 'gpio1';
1023
+ const zThreshold = touch.minPressure ?? 400;
1024
+ // touchscreen-size-* describe the panel the touch layer sits on (the display
1025
+ // profile's effective size); a touch-only build falls back to the 12-bit
1026
+ // full-scale range so the binding's required props still resolve.
1027
+ const sizeX = display?.width ?? cal?.xMax ?? 320;
1028
+ const sizeY = display?.height ?? cal?.yMax ?? 240;
1029
+ // The display block already wrote cs-gpios with both entries (its CS at
1030
+ // index 0, the touch CS at index 1). When there is no display block, enable
1031
+ // the bus here with the touch CS as the only entry.
1032
+ if (!display) {
1033
+ lines.push(`&${bus} {`);
1034
+ lines.push(' status = "okay";');
1035
+ lines.push(` cs-gpios = <&${gpioController(cs)} ${cs} GPIO_ACTIVE_LOW>;`);
1036
+ lines.push('};');
1037
+ lines.push('');
1038
+ }
1039
+ lines.push(`&${bus} {`);
1040
+ lines.push(' xpt2046: xpt2046@1 {');
1041
+ lines.push(' compatible = "xptek,xpt2046";');
1042
+ lines.push(' reg = <1>;');
1043
+ // The XPT2046 datasheet max SPI clock is 2.5MHz — the panel bus may run at
1044
+ // 80MHz, but this node's spi-max-frequency gates only its own transactions
1045
+ // (the adapter's SPI_DT_SPEC picks it up).
1046
+ lines.push(' spi-max-frequency = <2500000>;');
1047
+ lines.push(` int-gpios = <&${gpioController(irq)} ${irq} GPIO_ACTIVE_LOW>;`);
1048
+ lines.push(` touchscreen-size-x = <${sizeX}>;`);
1049
+ lines.push(` touchscreen-size-y = <${sizeY}>;`);
1050
+ lines.push(` min-x = <${cal?.xMin ?? 0}>;`);
1051
+ lines.push(` max-x = <${cal?.xMax ?? 4095}>;`);
1052
+ lines.push(` min-y = <${cal?.yMin ?? 0}>;`);
1053
+ lines.push(` max-y = <${cal?.yMax ?? 4095}>;`);
1054
+ lines.push(` z-threshold = <${zThreshold}>;`);
1055
+ lines.push(' };');
1056
+ lines.push('};');
1057
+ lines.push('');
1058
+ }