@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
@@ -6,11 +6,15 @@
6
6
  // descriptor knows, emit a `&nodelabel { status = "okay"; }` block enabling it.
7
7
  // West merges this over the board's base DT; we never rewrite the base.
8
8
  //
9
- // This is the Zephyr analog of Arduino's library-resolution hooks: the artifact
9
+ // Framework library-resolution hook: the artifact
10
10
  // that brings external capabilities into a build. (Arduino does it by parsing
11
11
  // library headers into .d.ts; Zephyr does it by enabling DT nodes + Kconfig.)
12
12
  // ---------------------------------------------------------------------------
13
13
  import { PANEL_CONTROLLER_DEFAULTS, panelControllerFor } from '../display/profiles.js';
14
+ import { SENSOR_PART_INFO } from '@typecad/hal';
15
+ /** Display panel CS default — shared by the display block and the SPI-sensor
16
+ * cs-gpios merge so both sites write the same entry. */
17
+ const DEFAULT_DISPLAY_CS = 5;
14
18
  export function generateOverlay(chip, usage, display, wiring, touch, diagnostics = []) {
15
19
  // An I2C touch controller with no explicit bus pins: the overlay enables
16
20
  // i2c0 and instantiates the node, but nothing remuxes the controller to
@@ -41,22 +45,186 @@ export function generateOverlay(chip, usage, display, wiring, touch, diagnostics
41
45
  lines.push('};');
42
46
  lines.push('');
43
47
  };
44
- if (usage.usesI2c && chip.i2c) {
45
- for (const c of chip.i2c.controllers)
46
- block(c.nodeLabel);
48
+ // Controllers whose board DT ships no default pinctrl group (rp2xxx uart1)
49
+ // carry synthesis data: emit the group under &pinctrl, then enable the
50
+ // controller referencing it. Controllers with a pinctrlRef wire an existing
51
+ // group instead; `props` adds raw property lines for bindings with
52
+ // required properties the board only sets on its own wired nodes.
53
+ // The pinmux header include is collected and spliced before all blocks
54
+ // (tokens must be defined before use).
55
+ const pinctrlIncludes = new Map();
56
+ const controllerBlock = (c) => {
57
+ const extra = [...(c.props ?? [])];
58
+ if (c.pinctrl) {
59
+ pinctrlIncludes.set(c.pinctrl.include, c.pinctrl.defines ? [...c.pinctrl.defines] : []);
60
+ const group = `${c.nodeLabel}_default`;
61
+ lines.push('&pinctrl {');
62
+ lines.push(` ${group}: ${group} {`);
63
+ lines.push(` group1 {`);
64
+ lines.push(` pinmux = <${c.pinctrl.pinmux.join(', ')}>;`);
65
+ lines.push(` };`);
66
+ if (c.pinctrl.inputPinmux && c.pinctrl.inputPinmux.length > 0) {
67
+ lines.push(` group2 {`);
68
+ lines.push(` pinmux = <${c.pinctrl.inputPinmux.join(', ')}>;`);
69
+ lines.push(` input-enable;`);
70
+ lines.push(` };`);
71
+ }
72
+ lines.push(` };`);
73
+ lines.push('};');
74
+ lines.push('');
75
+ extra.push(`pinctrl-0 = <&${group}>;`, `pinctrl-names = "default";`);
76
+ }
77
+ else if (c.pinctrlRef) {
78
+ extra.push(`pinctrl-0 = <&${c.pinctrlRef}>;`, `pinctrl-names = "default";`);
79
+ }
80
+ block(c.nodeLabel, extra);
81
+ };
82
+ // Sensors ride a bus: a constructed sensor must enable its controller even
83
+ // when the program never calls i2c.*/spi.* directly (the sensor device
84
+ // handle is the only bus user) — merge their instances into the used sets.
85
+ const sensorParts = usage.sensorParts ?? [];
86
+ const i2cSensors = sensorParts.filter((s) => s.busKind !== 'spi');
87
+ const spiSensors = sensorParts.filter((s) => s.busKind === 'spi');
88
+ const spiTargets = usage.spiTargets ?? [];
89
+ const i2cInstances = new Set(usage.i2cUsedInstances ?? []);
90
+ for (const sp of i2cSensors)
91
+ i2cInstances.add(sp.busIndex);
92
+ if ((usage.usesI2c || i2cSensors.length > 0) && chip.i2c) {
93
+ for (const [i, c] of chip.i2c.controllers.entries()) {
94
+ if (usage.i2cUsedInstances && !i2cInstances.has(i))
95
+ continue;
96
+ controllerBlock(c);
97
+ }
98
+ }
99
+ const spiInstances = new Set(usage.spiUsedInstances ?? []);
100
+ for (const sp of spiSensors)
101
+ spiInstances.add(sp.busIndex);
102
+ for (const t of spiTargets)
103
+ spiInstances.add(t.busIndex);
104
+ if ((usage.usesSpi || spiSensors.length > 0 || spiTargets.length > 0) && chip.spi) {
105
+ for (const [i, c] of chip.spi.controllers.entries()) {
106
+ if (usage.spiUsedInstances && !spiInstances.has(i))
107
+ continue;
108
+ controllerBlock(c);
109
+ }
110
+ }
111
+ // One DT child node per constructed I2C sensor — THE enable switch for the
112
+ // driver (its Kconfig is `default y` on this node's presence). The nodelabel
113
+ // is derived identically to the lowering's DEVICE_DT_GET target
114
+ // (lowering/sensor.ts sensorNames) — the two cannot drift.
115
+ for (const sp of i2cSensors) {
116
+ const ctrl = chip.i2c?.controllers[sp.busIndex];
117
+ const info = SENSOR_PART_INFO[sp.part.replace(/^SENSOR\./, '')];
118
+ if (!ctrl || !info)
119
+ continue; // the lowering already threw on unknown parts
120
+ const nodeName = info.compatible.split(',')[1] ?? sp.part;
121
+ const addrHex = sp.port.toString(16);
122
+ lines.push(`&${ctrl.nodeLabel} {`);
123
+ lines.push(` tc_${sp.part.replace(/^SENSOR\./, '')}_i2c${sp.busIndex}_0x${addrHex}: ${nodeName}@${addrHex} {`);
124
+ lines.push(` compatible = "${info.compatible}";`);
125
+ lines.push(` reg = <0x${addrHex}>;`);
126
+ emitAlert(lines, chip, sp.alertPin, info.alert, 8);
127
+ lines.push(' };');
128
+ lines.push('};');
129
+ lines.push('');
47
130
  }
48
131
  if (usage.usesSpi && chip.spi) {
49
- for (const c of chip.spi.controllers)
50
- block(c.nodeLabel);
132
+ for (const [i, c] of chip.spi.controllers.entries()) {
133
+ if (usage.spiUsedInstances && !usage.spiUsedInstances.includes(i))
134
+ continue;
135
+ controllerBlock(c);
136
+ }
51
137
  }
52
138
  if (usage.usesUart && chip.uart) {
53
- for (const c of chip.uart.controllers)
54
- block(c.nodeLabel);
139
+ for (const [i, c] of chip.uart.controllers.entries()) {
140
+ if (usage.uartUsedInstances && !usage.uartUsedInstances.includes(i))
141
+ continue;
142
+ controllerBlock(c);
143
+ }
144
+ }
145
+ // Synthesized pinctrl groups reference pinmux tokens — splice their header
146
+ // includes ahead of every block (same slot the display path uses), with
147
+ // ifndef-guarded #defines for upstream header bugs in front.
148
+ for (const [include, defines] of pinctrlIncludes) {
149
+ const pre = [];
150
+ for (const d of defines) {
151
+ const name = d.trim().split(/\s+/)[0];
152
+ pre.push(`#ifndef ${name}`, `#define ${d}`, `#endif`);
153
+ }
154
+ lines.splice(2, 0, ...pre, `#include <${include}>`, '');
155
+ }
156
+ // Watchdog: the SoC dtsi declares the node disabled (STM32 iwdg ships
157
+ // status = "disabled" until a board enables it) — the wdt.* lowering
158
+ // addresses DEVICE_DT_GET(DT_NODELABEL(<nodeLabel>)), which needs the
159
+ // driver instance built, so enable the node when wdt_* is used.
160
+ if (usage.usesWdt && chip.wdt) {
161
+ block(chip.wdt.nodeLabel);
162
+ }
163
+ // USB CDC-ACM: the "next" USB device stack composes classes as devicetree
164
+ // children of the UDC controller node. Enable the controller and declare one
165
+ // cdc_acm_uart<N> child per instance the chip descriptor declares — the
166
+ // lowering addresses instance N as DT_NODELABEL(cdc_acm_uart<N>), so the
167
+ // nodelabels here and in lowering/usb.ts cannot drift. Endpoints and
168
+ // descriptors are assigned by the class driver at build time.
169
+ if (usage.usesUsb && chip.usb) {
170
+ lines.push(`&${chip.usb.controller} {`);
171
+ lines.push(' status = "okay";');
172
+ for (let i = 0; i < chip.usb.cdcInstances; i++) {
173
+ lines.push(` cdc_acm_uart${i}: cdc-acm-uart${i} {`);
174
+ lines.push(' compatible = "zephyr,cdc-acm-uart";');
175
+ lines.push(' };');
176
+ }
177
+ lines.push('};');
178
+ lines.push('');
179
+ }
180
+ // PWM: synthesized specs (controller + channel, no board-shipped alias) get
181
+ // a pwm-leds consumer node + tc-pwm<pin> alias here — the lowering addresses
182
+ // the channel as PWM_DT_SPEC_GET(DT_ALIAS(tc-pwm<pin>)), so the alias must
183
+ // exist in the merged DT. pwmDtAlias (lowering/pwm.ts) derives the same
184
+ // name from the pin; the two sides cannot drift. Filtered to the pins the
185
+ // program actually drives (pwmUsedPins) so the DT carries no dead channels;
186
+ // an undefined list (prepare-time overlay) emits all specs.
187
+ if (usage.usesPwm && chip.pwm) {
188
+ emitPwmNodes(lines, chip, usage.pwmUsedPins);
189
+ }
190
+ // ADC: enable the ADC device node, and on SoCs whose channels carry a
191
+ // pinctrl label (STM32: the pad stays in GPIO mode unless the ADC node's
192
+ // pinctrl-0 muxes it analog), rewrite pinctrl-0 to exactly the channels the
193
+ // program reads. DT assignment replaces the whole property — the board
194
+ // default (one pad) is deliberately dropped in favor of the used set.
195
+ if (usage.usesAdc && chip.adc) {
196
+ emitAdcNode(lines, chip, usage.adcReadPins);
55
197
  }
56
198
  // DAC: enable the chip's DAC device node when the program uses dac.*. The
57
- // lowering references DEVICE_DT_GET(DT_NODELABEL(<dac.device>)).
199
+ // lowering references DEVICE_DT_GET(DT_NODELABEL(<dac.device>)); the used
200
+ // channels' pinctrl groups route the output to its pad.
58
201
  if (usage.usesDac && chip.dac) {
59
- block(chip.dac.device);
202
+ emitDacNode(lines, chip, usage.dacWritePins);
203
+ }
204
+ // Counters: enable the free counter devices when the program uses hwtimer.
205
+ // Self form (nRF rtc1, STM32 rtc): enable the labeled node. Child form
206
+ // (ESP32 timer0-3): the counter device is an unlabeled `counter {}`
207
+ // child — the overlay defines the tc_counter<N> label the descriptor's
208
+ // nodeLabel references (the same label-less-child move the STM32 pwm
209
+ // path uses).
210
+ if (usage.usesHwtimer && chip.hwtimer) {
211
+ for (const c of chip.hwtimer.controllers) {
212
+ if (c.counterParent) {
213
+ lines.push(`&${c.counterParent} {`);
214
+ lines.push(' status = "okay";');
215
+ lines.push(` ${c.nodeLabel}: counter {`);
216
+ lines.push(' status = "okay";');
217
+ lines.push(' };');
218
+ lines.push('};');
219
+ lines.push('');
220
+ }
221
+ else {
222
+ lines.push(`&${c.nodeLabel} {`);
223
+ lines.push(' status = "okay";');
224
+ lines.push('};');
225
+ lines.push('');
226
+ }
227
+ }
60
228
  }
61
229
  if (display) {
62
230
  // Emit a full display DT node definition. Boards like the ESP32 devkit
@@ -85,18 +253,133 @@ export function generateOverlay(chip, usage, display, wiring, touch, diagnostics
85
253
  // chosen entry makes the lookup explicit and survives boards that name the
86
254
  // partition differently. It only adds a /chosen pointer (never redeclares
87
255
  // the partition node — west errors if a node is multiply-defined).
88
- if (usage.usesPreferences) {
89
- lines.push('/ {');
90
- lines.push(' chosen {');
91
- lines.push(' zephyr,settings-partition = &storage_partition;');
92
- lines.push(' };');
93
- lines.push('};');
94
- lines.push('');
256
+ if (usage.usesPreferences || usage.usesFS) {
257
+ // Boards whose DTS ships no storage_partition (most STM32s define only
258
+ // the MCUboot boot/slot/scratch set) declare a synthesis region in the
259
+ // chip descriptor — emit it as a partition@<offset> child of the flash0
260
+ // partitions node. Adding a NEW child via overlay is legal; redeclaring
261
+ // an existing one is not, so boards that already carry the label
262
+ // (descriptor storage.preexisting — ESP32's AMP layout) skip the
263
+ // synthesis block and only the /chosen pointer lands here.
264
+ if (chip.storage && !chip.storage.preexisting) {
265
+ lines.push('&flash0 {');
266
+ // Explicit compatible + cells: boards whose DTS already carries a
267
+ // partitions node (rp2040, STM32 MCUboot sets) merge identically, and
268
+ // boards without one (rpi_pico2) would otherwise hit dtc's spec
269
+ // default of #address-cells 2 / #size-cells 1 — a 3-cell reg and a
270
+ // "length 8 not divisible by 12" error for the 2-cell reg below.
271
+ lines.push(' partitions {');
272
+ lines.push(' compatible = "fixed-partitions";');
273
+ lines.push(' #address-cells = <1>;');
274
+ lines.push(' #size-cells = <1>;');
275
+ lines.push(` storage_partition: partition@${chip.storage.offset.toString(16)} {`);
276
+ lines.push(' label = "storage";');
277
+ lines.push(` reg = <0x${chip.storage.offset.toString(16).padStart(8, '0')} 0x${chip.storage.size.toString(16).padStart(8, '0')}>;`);
278
+ lines.push(' };');
279
+ lines.push(' };');
280
+ lines.push('};');
281
+ lines.push('');
282
+ }
283
+ if (usage.usesPreferences) {
284
+ lines.push('/ {');
285
+ lines.push(' chosen {');
286
+ lines.push(' zephyr,settings-partition = &storage_partition;');
287
+ lines.push(' };');
288
+ lines.push('};');
289
+ lines.push('');
290
+ }
95
291
  }
96
292
  // PSRAM: enable the psram0 DT node with the correct size. The devkitc board
97
293
  // DT defaults to a no-PSRAM module variant (e.g. wroom_n8); a PSRAM-capable
98
294
  // module (N16R8, N8R8) needs the node enabled + sized so the linker maps
99
295
  // .ext_ram sections into the real PSRAM. OPI on ESP32-S3 = 8MB octal PSRAM.
296
+ // SPI sensors: children whose reg is their cs-gpios INDEX. DT property
297
+ // assignment replaces the whole property, so cs-gpios is written here with
298
+ // every sensor CS on the controller, after the display block — a display
299
+ // panel sharing the bus is a wiring conflict this layer cannot merge (its
300
+ // CS comes from config wiring), so warn instead of silently dropping it.
301
+ // spi-max-frequency defaults to 1 MHz — conservative for sensors whose
302
+ // datasheet max isn't in any binding; the constructor opts override it.
303
+ const SPI_SENSOR_HZ = 1000000;
304
+ const spiByCtrl = new Map();
305
+ for (const sp of spiSensors) {
306
+ const list = spiByCtrl.get(sp.busIndex) ?? [];
307
+ list.push(sp);
308
+ spiByCtrl.set(sp.busIndex, list);
309
+ }
310
+ // Target-only controllers (no sensors) still need the cs-gpios block.
311
+ for (const t of spiTargets) {
312
+ if (!spiByCtrl.has(t.busIndex))
313
+ spiByCtrl.set(t.busIndex, []);
314
+ }
315
+ for (const [idx, sensors] of spiByCtrl) {
316
+ const ctrl = chip.spi?.controllers[idx];
317
+ if (!ctrl)
318
+ continue;
319
+ // A display panel on the same controller already wrote cs-gpios earlier
320
+ // in this overlay (its CS at index 0, an XPT2046 at 1). DT property
321
+ // assignment replaces, so this rewrite carries BOTH: the display entries
322
+ // first — keeping the panel/touch reg indexes stable — then the sensor CS
323
+ // pins. The entry construction mirrors emitDisplayNode's defaults via the
324
+ // shared DEFAULT_DISPLAY_CS/DEFAULT_XPT2046_CS constants.
325
+ const displayEntries = [];
326
+ if (display && (display.busLabel ?? 'spi2') === ctrl.nodeLabel) {
327
+ const displayCs = wiring?.cs ?? DEFAULT_DISPLAY_CS;
328
+ displayEntries.push(`<&gpio${displayCs <= 31 ? '0' : '1'} ${displayCs} GPIO_ACTIVE_LOW>`);
329
+ if (touch?.controller === 'xpt2046') {
330
+ const touchCs = touch.cs ?? DEFAULT_XPT2046_CS;
331
+ displayEntries.push(`<&gpio${touchCs <= 31 ? '0' : '1'} ${touchCs} GPIO_ACTIVE_LOW>`);
332
+ }
333
+ }
334
+ // Thin SPI targets ride this controller too: their CS entries append
335
+ // after the sensor pins, and their child nodes carry no compatible —
336
+ // a raw spi_dt_spec peer, not a driver node.
337
+ const ctrlTargets = spiTargets.filter((t) => t.busIndex === idx);
338
+ lines.push(`&${ctrl.nodeLabel} {`);
339
+ lines.push(` cs-gpios = ${displayEntries.concat(sensors.map((sp) => {
340
+ const gc = chip.gpioControllers?.find((g) => sp.port >= g.minPin && sp.port <= g.maxPin);
341
+ const ctrlLabel = gc?.nodelabel ?? 'gpio0';
342
+ return `<&${ctrlLabel} ${sp.port} GPIO_ACTIVE_LOW>`;
343
+ }), ctrlTargets.map((t) => {
344
+ const gc = chip.gpioControllers?.find((g) => t.cs >= g.minPin && t.cs <= g.maxPin);
345
+ const ctrlLabel = gc?.nodelabel ?? 'gpio0';
346
+ return `<&${ctrlLabel} ${t.cs} GPIO_ACTIVE_LOW>`;
347
+ })).join(', ')};`);
348
+ sensors.forEach((sp, sensorIdx) => {
349
+ const info = SENSOR_PART_INFO[sp.part.replace(/^SENSOR\./, '')];
350
+ if (!info)
351
+ return;
352
+ const nodeName = info.compatible.split(',')[1] ?? sp.part;
353
+ const csIndex = displayEntries.length + sensorIdx;
354
+ lines.push(` tc_${sp.part}_spi${idx}_cs${sp.port}: ${nodeName}@${csIndex} {`);
355
+ lines.push(` compatible = "${info.compatible}";`);
356
+ lines.push(` reg = <${csIndex}>;`);
357
+ lines.push(` spi-max-frequency = <${sp.spiHz && sp.spiHz > 0 ? sp.spiHz : SPI_SENSOR_HZ}>;`);
358
+ // mode bits: 1 = CPHA, 2 = CPOL (presence props, no value).
359
+ if ((sp.spiMode ?? 0) & 2)
360
+ lines.push(' spi-cpol;');
361
+ if ((sp.spiMode ?? 0) & 1)
362
+ lines.push(' spi-cpha;');
363
+ emitAlert(lines, chip, sp.alertPin, info.alert, 8);
364
+ lines.push(' };');
365
+ });
366
+ ctrlTargets.forEach((t, targetIdx) => {
367
+ const csIndex = displayEntries.length + sensors.length + targetIdx;
368
+ lines.push(` tc_spit_spi${idx}_cs${t.cs}: spidev@${csIndex} {`);
369
+ // The app-local cuttlefish,spi-target binding (no driver — it makes
370
+ // gen_defines emit the spi properties SPI_DT_SPEC_GET reads).
371
+ lines.push(` compatible = "cuttlefish,spi-target";`);
372
+ lines.push(` reg = <${csIndex}>;`);
373
+ lines.push(` spi-max-frequency = <${t.hz && t.hz > 0 ? t.hz : SPI_SENSOR_HZ}>;`);
374
+ if ((t.mode ?? 0) & 2)
375
+ lines.push(' spi-cpol;');
376
+ if ((t.mode ?? 0) & 1)
377
+ lines.push(' spi-cpha;');
378
+ lines.push(' };');
379
+ });
380
+ lines.push('};');
381
+ lines.push('');
382
+ }
100
383
  if (usage.psram) {
101
384
  lines.push('&psram0 {');
102
385
  lines.push(' status = "okay";');
@@ -106,6 +389,319 @@ export function generateOverlay(chip, usage, display, wiring, touch, diagnostics
106
389
  }
107
390
  return lines.join('\n');
108
391
  }
392
+ /**
393
+ * Emit the pwm-leds consumer node + aliases for synthesized PWM specs.
394
+ *
395
+ * A board DTS may enable a PWM controller (`pwm4`) without aliasing any
396
+ * channel — the STM32 pattern — while the lowering addresses channels via
397
+ * DT_ALIAS. Each synthesized spec gets a `pwm-leds` child whose `pwms` cell
398
+ * binds the controller+channel (+ period/polarity), and a `tc-pwm<pin>`
399
+ * alias pointing at it. Alias/label names are valid DT identifiers (lowercase
400
+ * alphanumerics + dash/underscore).
401
+ */
402
+ function emitPwmNodes(lines, chip, usedPins) {
403
+ const staticSynth = (chip.pwm?.specs ?? []).filter((s) => s.controller && s.channel != null && !s.dtSpec).filter((s) => !usedPins || usedPins.includes(s.pin));
404
+ // Matrix PWM (ESP32 LEDC): channels are assigned here, over the pins the
405
+ // program actually drives (ascending — the same order lowering/pwm.ts
406
+ // emits the alias vars in; the C++ is channel-blind, so this is the single
407
+ // place a channel number is decided). The espressif,esp32-ledc binding
408
+ // needs three pieces per channel: a pinctrl pinmux token routing the
409
+ // channel to the pad (LEDC_CH<ch>_GPIO<pin> — every channel×pad pair
410
+ // exists in esp32s3-pinctrl.h, already included via the SoC dtsi), a
411
+ // channel child node (the driver's DT_INST_FOREACH_CHILD reads reg +
412
+ // timer from each), and the pwm-leds consumer the alias resolves to.
413
+ const m = chip.pwm?.matrix;
414
+ let matrixSynth = [];
415
+ if (m) {
416
+ const pins = [...new Set(usedPins ? usedPins.filter((p) => m.pins.includes(p)) : m.pins)]
417
+ .sort((a, b) => a - b);
418
+ if (pins.length > m.channelCount) {
419
+ throw new Error(`framework-zephyr: the program drives ${pins.length} PWM pins but ${chip.id}'s ` +
420
+ `${m.controller} exposes only ${m.channelCount} channels — drop ` +
421
+ `${pins.length - m.channelCount} pwm pin(s).`);
422
+ }
423
+ matrixSynth = pins.map((pin, ch) => ({ pin, controller: m.controller, channel: ch }));
424
+ // nRF shape: channels route to pads via psel (pinctrl psel order IS
425
+ // channel order — pwm_nrfx reads PSEL.OUT[ch] back), no channel child
426
+ // nodes, and the overlay carries the nrf pinctrl header for NRF_PSEL.
427
+ // P<port>.<pin> maps from the global number through the nRF controller
428
+ // layout (P0 = 0-31, P1 = 32-63).
429
+ const isNrfMatrix = /^nrf/.test(chip.soc);
430
+ if (isNrfMatrix) {
431
+ const inc = '#include <zephyr/dt-bindings/pinctrl/nrf-pinctrl.h>';
432
+ if (!lines.includes(inc))
433
+ lines.splice(2, 0, inc, '');
434
+ lines.push('&pinctrl {');
435
+ lines.push(` tc_${m.controller}_default: tc-${m.controller}-default {`);
436
+ lines.push(' group1 {');
437
+ lines.push(` psel = ${pins.map((p, ch) => `<NRF_PSEL(PWM_OUT${ch}, ${Math.floor(p / 32)}, ${p % 32})>`).join(', ')};`);
438
+ lines.push(' };');
439
+ lines.push(' };');
440
+ lines.push('};');
441
+ lines.push('');
442
+ lines.push(`&${m.controller} {`);
443
+ lines.push(' status = "okay";');
444
+ lines.push(` pinctrl-0 = <&tc_${m.controller}_default>;`);
445
+ lines.push(' pinctrl-names = "default";');
446
+ lines.push('};');
447
+ lines.push('');
448
+ }
449
+ if (!isNrfMatrix) {
450
+ lines.push('&pinctrl {');
451
+ lines.push(` tc_${m.controller}_default: tc-${m.controller}-default {`);
452
+ lines.push(' group1 {');
453
+ lines.push(` pinmux = ${pins.map((p, ch) => `<LEDC_CH${ch}_GPIO${p}>`).join(', ')};`);
454
+ lines.push(' output-enable;');
455
+ lines.push(' };');
456
+ lines.push(' };');
457
+ lines.push('};');
458
+ lines.push('');
459
+ // The four LEDC timers are round-robined so co-driven channels share a
460
+ // timer only past four (sharing is fine at equal periods; the driver
461
+ // reconfigures on pwm_set_dt when they differ).
462
+ lines.push(`&${m.controller} {`);
463
+ lines.push(' status = "okay";');
464
+ lines.push(` pinctrl-0 = <&tc_${m.controller}_default>;`);
465
+ lines.push(' pinctrl-names = "default";');
466
+ lines.push(' #address-cells = <1>;');
467
+ lines.push(' #size-cells = <0>;');
468
+ for (let ch = 0; ch < pins.length; ch++) {
469
+ lines.push(` channel${ch}@${ch} {`);
470
+ lines.push(` reg = <${ch}>;`);
471
+ lines.push(` timer = <${ch % 4}>;`);
472
+ lines.push(' };');
473
+ }
474
+ lines.push('};');
475
+ lines.push('');
476
+ }
477
+ }
478
+ const synthesized = staticSynth.concat(matrixSynth);
479
+ if (synthesized.length === 0)
480
+ return;
481
+ // The pwms cells name the polarity as a macro (PWM_POLARITY_NORMAL). It is
482
+ // NOT universally available: only dts include chains that already use PWM
483
+ // bindings pull <zephyr/dt-bindings/pwm/pwm.h> in (STM32 does; the
484
+ // ESP32-S3 chain does not), so the overlay carries its own include.
485
+ // Spliced after the header comments, the established pattern (see the
486
+ // esp32s3-pinctrl.h splice above); guarded for the double-emit case.
487
+ if (!lines.includes('#include <zephyr/dt-bindings/pwm/pwm.h>')) {
488
+ lines.splice(2, 0, '#include <zephyr/dt-bindings/pwm/pwm.h>', '');
489
+ }
490
+ // Enable each distinct PWM controller node (idempotent when already okay).
491
+ // 16-bit fit: STM32 timers count period cycles in a 16-bit ARR, and the
492
+ // SoC dtsi default is st,prescaler = <0> (÷1) — a 20 ms servo period at
493
+ // 96 MHz is 1.92M cycles and pwm_stm32 rejects the channel. When the
494
+ // descriptor declares the timer clock, derive the smallest divider that
495
+ // fits the controller's slowest used period and override the prescaler
496
+ // on the timers parent node (the property lives there, not on the pwm
497
+ // child). pwm4 → timers4 is the STM32 nodelabel convention. Matrix
498
+ // controllers are skipped — the block above already enabled them (with
499
+ // their pinctrl + channel children, which this loop doesn't know).
500
+ const clockHz = chip.pwm?.clockHz;
501
+ for (const controller of [...new Set(staticSynth.map((s) => s.controller))]) {
502
+ const timersMatch = controller.match(/^pwm(\d+)$/);
503
+ const specs = staticSynth.filter((s) => s.controller === controller);
504
+ // Macro-form pinctrl tokens (uppercase — DT labels are lowercase by
505
+ // spec): the overlay synthesizes the pad-routing group itself. RP2's
506
+ // macros live in a dt-bindings header the board chain may not pull in
507
+ // (spliced via rp2PinctrlHeader); Kinetis/LPC/GD32 macros come from the
508
+ // part headers the board's own pinctrl dtsi includes, and Zephyr
509
+ // preprocesses overlays together with the base DTS — no splice needed.
510
+ const isMacro = (p) => !!p && /^[A-Z][A-Z0-9_]*$/.test(p);
511
+ if (!timersMatch && specs.some((s) => isMacro(s.pinctrl))) {
512
+ const header = rp2PinctrlHeader(chip.soc);
513
+ if (header) {
514
+ const inc = `#include <zephyr/dt-bindings/pinctrl/${header}>`;
515
+ if (!lines.includes(inc))
516
+ lines.splice(2, 0, inc, '');
517
+ }
518
+ lines.push('&pinctrl {');
519
+ lines.push(' tc_pwm_default: tc-pwm-default {');
520
+ lines.push(' group1 {');
521
+ lines.push(` pinmux = ${specs.filter((s) => s.pinctrl).map((s) => `<${s.pinctrl}>`).join(', ')};`);
522
+ lines.push(' };');
523
+ lines.push(' };');
524
+ lines.push('};');
525
+ lines.push('');
526
+ lines.push(`&${controller} {`);
527
+ lines.push(' status = "okay";');
528
+ // Atmel SAM TCC/TC in PWM mode: the SoC dtsi declares the base timer
529
+ // node (compatible atmel,sam0-tcc / …-tc, with clocks/assigned-clocks/
530
+ // channels/counter-size already present). The overlay overrides the
531
+ // compatible to the PWM binding, sets the required prescaler and the
532
+ // #pwm-cells the binding consts, and reuses the shared pinctrl group.
533
+ const samPwm = controller.match(/^(tcc|tc)(\d+)$/);
534
+ if (samPwm) {
535
+ lines.push(` compatible = "${samPwm[1] === 'tcc' ? 'atmel,sam0-tcc-pwm' : 'atmel,sam0-tc-pwm'}";`);
536
+ lines.push(' prescaler = <1024>;');
537
+ lines.push(' #pwm-cells = <2>;');
538
+ }
539
+ lines.push(' pinctrl-0 = <&tc_pwm_default>;');
540
+ lines.push(' pinctrl-names = "default";');
541
+ lines.push('};');
542
+ lines.push('');
543
+ continue;
544
+ }
545
+ if (timersMatch) {
546
+ // STM32 shape: the SoC dtsi declares a LABEL-LESS pwm child under
547
+ // timers{N} — the overlay defines the label itself (pwmN: pwm), which
548
+ // the synthesized pwms cell below references. One block carries the
549
+ // parent enable, the pad-routing pinctrl groups (harvested silicon
550
+ // routes on the specs — without them the PWM runs but nothing leaves
551
+ // the chip), the prescaler, and the child enable.
552
+ const groups = specs.filter((s) => s.pinctrl).map((s) => `&${s.pinctrl}`);
553
+ const maxPeriodNs = Math.max(...specs.map((s) => s.periodNs ?? 20_000_000));
554
+ // cycles = clockHz * period_s / divider ≤ 65536 (driver allows
555
+ // UINT16_MAX + 1); binding value is divider - 1 (CLK/(prescaler+1)).
556
+ const divider = clockHz
557
+ ? Math.max(1, Math.ceil((clockHz * maxPeriodNs) / 1e9 / 65536))
558
+ : 1;
559
+ lines.push(`&timers${timersMatch[1]} {`);
560
+ lines.push(' status = "okay";');
561
+ if (divider > 1) {
562
+ lines.push(` st,prescaler = <${divider - 1}>;`);
563
+ }
564
+ lines.push(` ${controller}: pwm {`);
565
+ lines.push(' status = "okay";');
566
+ if (groups.length > 0) {
567
+ lines.push(` pinctrl-0 = <${groups.join(' ')}>;`);
568
+ lines.push(' pinctrl-names = "default";');
569
+ }
570
+ lines.push(' };');
571
+ lines.push('};');
572
+ lines.push('');
573
+ continue;
574
+ }
575
+ // Lowercase pinctrl tokens (i.MX `iomuxc_…` node labels from the part
576
+ // dtsi) ride the controller's pinctrl-0 directly.
577
+ const pinRefs = specs.filter((s) => s.pinctrl && !isMacro(s.pinctrl)).map((s) => `&${s.pinctrl}`);
578
+ lines.push(`&${controller} {`);
579
+ lines.push(' status = "okay";');
580
+ if (pinRefs.length > 0) {
581
+ lines.push(` pinctrl-0 = <${pinRefs.join(' ')}>;`);
582
+ lines.push(' pinctrl-names = "default";');
583
+ }
584
+ lines.push('};');
585
+ lines.push('');
586
+ }
587
+ lines.push('/ {');
588
+ lines.push(' tc_pwm_leds: tc-pwm-leds {');
589
+ lines.push(' compatible = "pwm-leds";');
590
+ for (const s of synthesized) {
591
+ // 20 ms / 50 Hz default — the servo convention; the duty scaling in the
592
+ // lowering normalizes 0-255 against whatever period is baked in.
593
+ const period = s.periodNs ?? 20_000_000;
594
+ const polarity = s.polarity ?? 'PWM_POLARITY_NORMAL';
595
+ lines.push(` tc_pwm_${s.pin}: pwm-led-${s.pin} {`);
596
+ lines.push(` pwms = <&${s.controller} ${s.channel} ${period} ${polarity}>;`);
597
+ lines.push(' };');
598
+ }
599
+ lines.push(' };');
600
+ lines.push(' aliases {');
601
+ for (const s of synthesized) {
602
+ lines.push(` tc-pwm${s.pin} = &tc_pwm_${s.pin};`);
603
+ }
604
+ lines.push(' };');
605
+ lines.push('};');
606
+ lines.push('');
607
+ }
608
+ /**
609
+ * Emit the ADC device-node enable (+ optional pinctrl override). The pinctrl
610
+ * override applies only when the descriptor's channels carry pinctrl labels
611
+ * AND the caller knows which pins the program reads (compile-time regen scans
612
+ * the emitted `__tc_adc<N>_setup()` calls); the prepare-time overlay omits it
613
+ * and the compile regen rewrites the file before west runs.
614
+ *
615
+ * Two pinctrl forms by family: NODE labels (STM32's harvested
616
+ * `adc1_in1_pa1` groups — referenced as `&label`) and MACRO tokens (RP2's
617
+ * `ADC_CH0_P26` — DT labels are lowercase by spec and macros uppercase, so
618
+ * the case decides). The macro form synthesizes its own group under
619
+ * &pinctrl and splices the SoC's pinctrl dt-binding header so the token
620
+ * resolves in the overlay's compile.
621
+ *
622
+ * Multi-controller SoCs: channels may name their owning controller (STM32
623
+ * adc3-only routes, ESP32 units). One enable block is emitted per controller
624
+ * that owns used channels — the primary first, others sorted for
625
+ * deterministic output. Single-controller boards emit exactly one block, as
626
+ * before.
627
+ */
628
+ /** The SoC's pinctrl dt-binding header (where the macro tokens are defined)
629
+ * — RP2 shapes; undefined for socs that use node-label pinctrl exclusively. */
630
+ function rp2PinctrlHeader(soc) {
631
+ if (soc === 'rp2040')
632
+ return 'rpi-pico-rp2040-pinctrl.h';
633
+ if (soc.startsWith('rp2350a'))
634
+ return 'rpi-pico-rp2350a-pinctrl.h';
635
+ if (soc.startsWith('rp2350b'))
636
+ return 'rpi-pico-rp2350b-pinctrl.h';
637
+ return undefined;
638
+ }
639
+ function emitAdcNode(lines, chip, readPins) {
640
+ const adc = chip.adc;
641
+ const primary = adc.nodeLabel;
642
+ const controllerOf = (c) => c.controller ?? primary;
643
+ const used = readPins ? adc.channels.filter((c) => readPins.includes(c.pin)) : adc.channels;
644
+ const isMacro = (p) => !!p && /^[A-Z][A-Z0-9_]*$/.test(p);
645
+ const macroChannels = used.filter((c) => isMacro(c.pinctrl));
646
+ if (macroChannels.length > 0) {
647
+ const header = rp2PinctrlHeader(chip.soc);
648
+ if (header) {
649
+ const inc = `#include <zephyr/dt-bindings/pinctrl/${header}>`;
650
+ if (!lines.includes(inc))
651
+ lines.splice(2, 0, inc, '');
652
+ }
653
+ lines.push('&pinctrl {');
654
+ lines.push(' tc_adc_default: tc-adc-default {');
655
+ lines.push(' group1 {');
656
+ lines.push(` pinmux = ${macroChannels.map((c) => `<${c.pinctrl}>`).join(', ')};`);
657
+ lines.push(' };');
658
+ lines.push(' };');
659
+ lines.push('};');
660
+ lines.push('');
661
+ }
662
+ // Every controller that owns a USED channel gets an enable block — a
663
+ // controller left "disabled" in the SoC dtsi has no device instance, so a
664
+ // read against it fails to link. Nothing used: enable the primary only.
665
+ const controllers = used.length > 0
666
+ ? [...new Set(used.map(controllerOf))]
667
+ .sort((a, b) => (a === primary ? -1 : b === primary ? 1 : a.localeCompare(b, undefined, { numeric: true })))
668
+ : [primary];
669
+ for (const label of controllers) {
670
+ const mine = used.filter((c) => controllerOf(c) === label && c.pinctrl);
671
+ lines.push(`&${label} {`);
672
+ lines.push(' status = "okay";');
673
+ if (mine.length > 0) {
674
+ if (mine.every((c) => isMacro(c.pinctrl))) {
675
+ lines.push(' pinctrl-0 = <&tc_adc_default>;');
676
+ }
677
+ else {
678
+ lines.push(` pinctrl-0 = <${mine.map((c) => `&${c.pinctrl}`).join(' ')}>;`);
679
+ }
680
+ lines.push(' pinctrl-names = "default";');
681
+ }
682
+ lines.push('};');
683
+ lines.push('');
684
+ }
685
+ }
686
+ /**
687
+ * Emit the DAC device-node enable + the used output channels' pinctrl
688
+ * groups (the harvested `dac1_out1_pa4`-style nodes — the st,stm32-dac
689
+ * binding includes pinctrl-device, so without the group the channel never
690
+ * reaches its pad). Mirrors emitAdcNode.
691
+ */
692
+ function emitDacNode(lines, chip, writePins) {
693
+ const dac = chip.dac;
694
+ const labeled = dac.channels.filter((c) => c.pinctrl);
695
+ const used = labeled.filter((c) => !writePins || writePins.includes(c.pin));
696
+ lines.push(`&${dac.device} {`);
697
+ lines.push(' status = "okay";');
698
+ if (used.length > 0) {
699
+ lines.push(` pinctrl-0 = <${used.map((c) => `&${c.pinctrl}`).join(' ')}>;`);
700
+ lines.push(' pinctrl-names = "default";');
701
+ }
702
+ lines.push('};');
703
+ lines.push('');
704
+ }
109
705
  /**
110
706
  * Emit a display DT node definition. The node is attached to the profile's SPI
111
707
  * controller (default spi2, the ESP32's first user SPI controller) via a MIPI
@@ -118,13 +714,22 @@ export function generateOverlay(chip, usage, display, wiring, touch, diagnostics
118
714
  * appended as the second cs-gpios entry (the touch node uses reg = <1>) — DT
119
715
  * property assignment replaces, so both entries must be written together.
120
716
  */
717
+ /** Emit alert-gpios for parts whose binding declares it (active high — the
718
+ * bindings describe the sensor's output level, e.g. sht3xd's ALERT). */
719
+ function emitAlert(lines, chip, alertPin, supported, indent) {
720
+ if (alertPin === undefined || alertPin < 0 || !supported)
721
+ return;
722
+ const gc = chip.gpioControllers?.find((g) => alertPin >= g.minPin && alertPin <= g.maxPin);
723
+ const pad = ' '.repeat(indent);
724
+ lines.push(`${pad}alert-gpios = <&${gc?.nodelabel ?? 'gpio0'} ${alertPin} GPIO_ACTIVE_HIGH>;`);
725
+ }
121
726
  function emitDisplayNode(lines, display, wiring, spiTouchCs) {
122
727
  const bus = display.busLabel ?? 'spi2';
123
728
  const controller = panelControllerFor(display);
124
729
  const compatible = display.dtCompatible ?? PANEL_CONTROLLER_DEFAULTS[controller].dtCompatible;
125
730
  const dc = wiring?.dc ?? 17;
126
731
  const rst = wiring?.rst ?? 16;
127
- const cs = wiring?.cs ?? 5;
732
+ const cs = wiring?.cs ?? DEFAULT_DISPLAY_CS;
128
733
  const freq = wiring?.spiFrequency ?? 80000000;
129
734
  // DT node describes the NATIVE panel geometry; the effective (rotated)
130
735
  // dimensions live in the display profile.