@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
package/src/strategy.ts CHANGED
@@ -1,9 +1,12 @@
1
1
  // ---------------------------------------------------------------------------
2
2
  // ZephyrStrategy — Zephyr RTOS target
3
3
  //
4
- // Outputs C++ built with `west` (the Zephyr build tool). Programs use the
5
- // Arduino-style setup()/loop() pair; a generated main() bridges them into
6
- // Zephyr's standard C entry point and yields to the scheduler between loops.
4
+ // Outputs C++ built with `west` (the Zephyr build tool). Zephyr is a standard
5
+ // C main()-based RTOS, so the program's entrypoint IS main(): top-level
6
+ // statements lower straight into it. Worker programs keep the scaffolded
7
+ // top-level `while (true)`; event-driven programs (UI / async / native
8
+ // timers) end with a scheduler loop in main() that ticks the UI runtime and
9
+ // pumps microtasks each frame, yielding to the kernel with k_msleep(1).
7
10
  //
8
11
  // GPIO is lowered through devicetree specs (gpio_pin_*_dt) so an active-low
9
12
  // LED's polarity is honored by the DT flags, not by the generated code. See
@@ -19,6 +22,7 @@ import type {
19
22
  PlatformStrategy,
20
23
  ExpressionIR,
21
24
  ProgramIR,
25
+ StatementIR,
22
26
  Diagnostic,
23
27
  PlatformContext,
24
28
  BoardConstants,
@@ -35,24 +39,335 @@ import type {
35
39
  TouchAdapterCodegen,
36
40
  } from '@typecad/cuttlefish/api/shared';
37
41
  import { DEFAULT_STDLIB_SUPPORT } from '@typecad/cuttlefish/api/shared';
38
- import { buildWorkerRuntimePolyfill } from '@typecad/cuttlefish/api/shared';
39
42
  import { applyStringMethodRewrites } from '@typecad/cuttlefish/api/shared';
40
43
  import { programUsesSafety } from '@typecad/cuttlefish/api';
41
44
  import { entryHasUI } from '@typecad/cuttlefish/ui-hook';
42
- import { chipForTarget, setActiveChip, getActiveChip } from './chips/index.js';
45
+ import { generateBoard, generateBoardModuleFromContract } from './boardgen.js';
46
+ import {
47
+ syncBoardCatalog,
48
+ ensureFreshBoardCatalog,
49
+ type BoardCatalogSyncReport,
50
+ type BoardCatalogEnsureResult,
51
+ } from './sdk/board-catalog-sync.js';
52
+ import { setActiveChip, getActiveChip, NO_BOARD_CHIP } from './chips/index.js';
43
53
  import { resolveChipFromBoard } from './chips/resolve.js';
44
54
  import { emitGpioDevDispatcher } from './chips/controllers.js';
55
+ import type { ZephyrChipDescriptor } from './chips/types.js';
56
+
57
+ /**
58
+ * Deep-walk the program IR and collect the HAL pin numbers the program
59
+ * actually touches for a peripheral family ('adc' | 'pwm') — the same walk
60
+ * profileDiagnostics does. Emit paths gate per-channel state on these sets
61
+ * so nothing unused reaches the single generated TU (-Wunused-function
62
+ * hygiene: every emitted function/variable is referenced). `undefined`
63
+ * (no program — probe paths) means "no information": callers emit every
64
+ * descriptor channel, preserving probe behavior.
65
+ */
66
+ function collectUsedPins(
67
+ program: ProgramIR | undefined,
68
+ kind: 'adc' | 'pwm',
69
+ chip?: ZephyrChipDescriptor,
70
+ ): Set<number> | undefined {
71
+ if (!program) return undefined;
72
+ const pins = new Set<number>();
73
+ const visit = (node: unknown): void => {
74
+ if (!node || typeof node !== 'object') return;
75
+ const n = node as Record<string, unknown>;
76
+ const op = n.operation;
77
+ if (op && typeof op === 'object') {
78
+ const o = op as Record<string, unknown>;
79
+ const name = o.operation;
80
+ const pin = o.pin;
81
+ if (typeof name === 'string' && typeof pin === 'number') {
82
+ if (kind === 'adc' && (name === 'adc.read' || name === 'adc.read_voltage' || name === 'adc.read_raw' || name === 'adc.read_mv')) pins.add(pin);
83
+ if (kind === 'pwm' && name.startsWith('pwm.')) pins.add(pin);
84
+ }
85
+ }
86
+ for (const v of Object.values(n)) {
87
+ if (Array.isArray(v)) { for (const item of v) visit(item); }
88
+ else if (v && typeof v === 'object') visit(v);
89
+ }
90
+ };
91
+ visit(program);
92
+ return pins;
93
+ }
94
+
95
+ /**
96
+ * The inline-override escape hatch: ADC ops carrying construction-time
97
+ * device overrides (hal/adc-pin.ts opts) name DT device labels the manifest
98
+ * may not know — the shim's init block must declare their device handles
99
+ * (the reads reference them) and fire even when the chip has no adc facts
100
+ * at all.
101
+ */
102
+ function collectAdcOverrideDevices(program: ProgramIR | undefined): Set<string> {
103
+ const devices = new Set<string>();
104
+ if (!program) return devices;
105
+ const visit = (node: unknown): void => {
106
+ if (!node || typeof node !== 'object') return;
107
+ const n = node as Record<string, unknown>;
108
+ const op = n.operation;
109
+ if (op && typeof op === 'object') {
110
+ const o = op as Record<string, unknown>;
111
+ if (typeof o.operation === 'string' && o.operation.startsWith('adc.')
112
+ && typeof o.deviceOverride === 'string' && o.deviceOverride !== '') {
113
+ devices.add(o.deviceOverride);
114
+ }
115
+ }
116
+ for (const v of Object.values(n)) {
117
+ if (Array.isArray(v)) { for (const item of v) visit(item); }
118
+ else if (v && typeof v === 'object') visit(v);
119
+ }
120
+ };
121
+ visit(program);
122
+ return devices;
123
+ }
124
+
125
+ /**
126
+ * PWM construction-time overrides (the escape hatch): controller+channel
127
+ * vouched for by the user on pins the manifest may not map. The shim emits
128
+ * their alias vars; the overlay regen's marker merge creates the DT nodes.
129
+ */
130
+ function collectPwmOverrideSpecs(program: ProgramIR | undefined): { pin: number; controller: string; channel: number }[] {
131
+ const specs: { pin: number; controller: string; channel: number }[] = [];
132
+ if (!program) return specs;
133
+ const seen = new Set<number>();
134
+ const visit = (node: unknown): void => {
135
+ if (!node || typeof node !== 'object') return;
136
+ const n = node as Record<string, unknown>;
137
+ const op = n.operation;
138
+ if (op && typeof op === 'object') {
139
+ const o = op as Record<string, unknown>;
140
+ const ctrl = typeof o.controllerOverride === 'string' && o.controllerOverride !== '' ? o.controllerOverride : undefined;
141
+ const ch = typeof o.channelOverride === 'number' && o.channelOverride >= 0 ? o.channelOverride : undefined;
142
+ if (typeof o.operation === 'string' && o.operation.startsWith('pwm.')
143
+ && typeof o.pin === 'number' && (ctrl || ch !== undefined) && !seen.has(o.pin)) {
144
+ seen.add(o.pin);
145
+ specs.push({ pin: o.pin, controller: ctrl ?? 'pwm0', channel: ch ?? 0 });
146
+ }
147
+ }
148
+ for (const v of Object.values(n)) {
149
+ if (Array.isArray(v)) { for (const item of v) visit(item); }
150
+ else if (v && typeof v === 'object') visit(v);
151
+ }
152
+ };
153
+ visit(program);
154
+ return specs;
155
+ }
156
+
157
+ /**
158
+ * Which I2C/SPI/UART controller indexes the program actually drives (parsed
159
+ * from the ops' bus/port id, same as the lowerings' parseControllerIndex).
160
+ * `undefined` (no program — probe paths) means "no information": callers
161
+ * emit every declared controller, preserving probe behavior.
162
+ *
163
+ * The per-controller state blocks are emitted only for these indexes — an
164
+ * unused declared controller's `__tc_<bus>N_dev` would trip -Werror's
165
+ * -Wunused-variable (Zephyr builds with warnings-as-errors), the same
166
+ * hygiene collectUsedPins enforces for per-channel ADC/PWM state.
167
+ */
168
+ function collectUsedBusIndices(program: ProgramIR | undefined): { i2c: Set<number>; spi: Set<number>; uart: Set<number> } | undefined {
169
+ if (!program) return undefined;
170
+ const indices = { i2c: new Set<number>(), spi: new Set<number>(), uart: new Set<number>() };
171
+ const visit = (node: unknown): void => {
172
+ if (!node || typeof node !== 'object') return;
173
+ const n = node as Record<string, unknown>;
174
+ const op = n.operation;
175
+ if (op && typeof op === 'object') {
176
+ const o = op as Record<string, unknown>;
177
+ const name = o.operation;
178
+ if (typeof name === 'string') {
179
+ const m = name.match(/^(i2c|spi|uart)\./);
180
+ if (m) {
181
+ const bus = m[1] as 'i2c' | 'spi' | 'uart';
182
+ const id = typeof o.bus === 'string' ? o.bus : typeof o.port === 'string' ? o.port : undefined;
183
+ // parseControllerIndex() (what the lowering uses to pick the state
184
+ // prefix) maps a digitless alias like 'Wire'/'SPI'/'Serial' to
185
+ // controller 0 — the collector must agree, or the state block for
186
+ // controller 0 is skipped and every __tc_<bus>0_* reference dangles.
187
+ const idx = id?.match(/(\d+)$/);
188
+ indices[bus].add(idx ? parseInt(idx[1], 10) : 0);
189
+ }
190
+ }
191
+ }
192
+ for (const v of Object.values(n)) {
193
+ if (Array.isArray(v)) { for (const item of v) visit(item); }
194
+ else if (v && typeof v === 'object') visit(v);
195
+ }
196
+ };
197
+ visit(program);
198
+ return indices;
199
+ }
200
+
201
+ /**
202
+ * Collect the distinct DT-bound sensors the program's ops reference, as
203
+ * `${part}|${bus}|${address}` keys (same shape as peripheral-usage's
204
+ * sensorPartsUsed). Feeds the per-sensor device-handle state block — only
205
+ * constructed sensors emit state, and only sensors whose ops were emitted
206
+ * get a DT node.
207
+ */
208
+ export function collectSensors(program: ProgramIR | undefined): Map<string, { part: string; bus: string; port: number | string; busKind: string; spiHz: number | string; spiMode: number | string; alertPin: number | string }> | undefined {
209
+ if (!program) return undefined;
210
+ const sensors = new Map<string, { part: string; bus: string; port: number | string; busKind: string; spiHz: number | string; spiMode: number | string; alertPin: number | string }>();
211
+ const visit = (node: unknown): void => {
212
+ if (!node || typeof node !== 'object') return;
213
+ const n = node as Record<string, unknown>;
214
+ const op = n.operation;
215
+ if (op && typeof op === 'object') {
216
+ const o = op as Record<string, unknown>;
217
+ if (o.operation === 'sensor.fetch' || o.operation === 'sensor.get') {
218
+ const part = String(o.part ?? '');
219
+ const bus = String(o.bus ?? '');
220
+ const port = (o.port as number | string) ?? 0;
221
+ const busKind = String(o.busKind ?? 'i2c');
222
+ const spiHz = (o.spiHz as number | string) ?? 0;
223
+ const spiMode = (o.spiMode as number | string) ?? 0;
224
+ const alertPin = (o.alertPin as number | string) ?? -1;
225
+ const key = `${part}|${bus}|${port}|${busKind}`;
226
+ if (!sensors.has(key)) sensors.set(key, { part, bus, port, busKind, spiHz, spiMode, alertPin });
227
+ }
228
+ }
229
+ for (const v of Object.values(n)) {
230
+ if (Array.isArray(v)) { for (const item of v) visit(item); }
231
+ else if (v && typeof v === 'object') visit(v);
232
+ }
233
+ };
234
+ visit(program);
235
+ return sensors;
236
+ }
237
+
238
+ /**
239
+ * Collect the distinct thin SPI targets the program's ops reference
240
+ * (spi.transceive / spi.dev_write), keyed `${bus}|${cs}`. Mirrors
241
+ * collectSensors: only constructed targets whose ops were emitted get a
242
+ * spi_dt_spec state block and a DT child node.
243
+ */
244
+ export function collectSpiTargets(program: ProgramIR | undefined): Map<string, { bus: string; cs: number; hz: number; mode: number }> | undefined {
245
+ if (!program) return undefined;
246
+ const targets = new Map<string, { bus: string; cs: number; hz: number; mode: number }>();
247
+ const visit = (node: unknown): void => {
248
+ if (!node || typeof node !== 'object') return;
249
+ const n = node as Record<string, unknown>;
250
+ const op = n.operation;
251
+ if (op && typeof op === 'object') {
252
+ const o = op as Record<string, unknown>;
253
+ if (o.operation === 'spi.transceive' || o.operation === 'spi.dev_write' || o.operation === 'spi.reg_read') {
254
+ const bus = String(o.bus ?? '');
255
+ const cs = (o.cs as number) ?? 0;
256
+ const key = `${bus}|${cs}`;
257
+ if (!targets.has(key)) {
258
+ targets.set(key, { bus, cs, hz: (o.hz as number) ?? 0, mode: (o.mode as number) ?? 0 });
259
+ }
260
+ }
261
+ }
262
+ for (const v of Object.values(n)) {
263
+ if (Array.isArray(v)) { for (const item of v) visit(item); }
264
+ else if (v && typeof v === 'object') visit(v);
265
+ }
266
+ };
267
+ visit(program);
268
+ return targets;
269
+ }
270
+
271
+ /**
272
+ * Collect the distinct UART RX rings the program reads (uart.rx_* ops),
273
+ * keyed by controller index. Mirrors collectSpiTargets: only ports whose RX
274
+ * ops were emitted get a ring + ISR (an unused ISR is -Wunused-function
275
+ * under Zephyr's -Werror).
276
+ */
277
+ export function collectUartRings(program: ProgramIR | undefined): Map<number, { index: number; ring: number }> | undefined {
278
+ if (!program) return undefined;
279
+ const rings = new Map<number, { index: number; ring: number }>();
280
+ const visit = (node: unknown): void => {
281
+ if (!node || typeof node !== 'object') return;
282
+ const n = node as Record<string, unknown>;
283
+ const op = n.operation;
284
+ if (op && typeof op === 'object') {
285
+ const o = op as Record<string, unknown>;
286
+ if (o.operation === 'uart.rx_arm' || o.operation === 'uart.rx_available'
287
+ || o.operation === 'uart.rx_peek' || o.operation === 'uart.rx_read') {
288
+ const port = String(o.port ?? 'UART0');
289
+ const m = port.match(/(UART|uart)(\d+)/) ?? port.match(/(\d+)/);
290
+ const index = m ? parseInt(m[m.length - 1], 10) : 0;
291
+ if (!rings.has(index)) {
292
+ rings.set(index, { index, ring: (o.ring as number) ?? 64 });
293
+ }
294
+ }
295
+ }
296
+ for (const v of Object.values(n)) {
297
+ if (Array.isArray(v)) { for (const item of v) visit(item); }
298
+ else if (v && typeof v === 'object') visit(v);
299
+ }
300
+ };
301
+ visit(program);
302
+ return rings;
303
+ }
304
+
305
+ /**
306
+ * Collect the distinct threads the program STARTS (thread.start ops), keyed
307
+ * by instance. Only started threads emit state — see shimLines.
308
+ */
309
+ export function collectThreads(program: ProgramIR | undefined): Map<number, { instance: number; stackBytes: number; priority: number }> | undefined {
310
+ if (!program) return undefined;
311
+ const threads = new Map<number, { instance: number; stackBytes: number; priority: number }>();
312
+ const visit = (node: unknown): void => {
313
+ if (!node || typeof node !== 'object') return;
314
+ const n = node as Record<string, unknown>;
315
+ const op = n.operation;
316
+ if (op && typeof op === 'object') {
317
+ const o = op as Record<string, unknown>;
318
+ if (o.operation === 'thread.start') {
319
+ const instance = (o.instance as number) ?? 0;
320
+ if (!threads.has(instance)) {
321
+ threads.set(instance, { instance, stackBytes: (o.stackBytes as number) ?? 2048, priority: (o.priority as number) ?? 5 });
322
+ }
323
+ }
324
+ }
325
+ for (const v of Object.values(n)) {
326
+ if (Array.isArray(v)) { for (const item of v) visit(item); }
327
+ else if (v && typeof v === 'object') visit(v);
328
+ }
329
+ };
330
+ visit(program);
331
+ return threads;
332
+ }
45
333
  import { lowerHalOp } from './lowering/index.js';
46
- import { buildZephyrWorkerBacking } from './lowering/worker-backing.js';
47
- import { adcInitLines } from './lowering/adc.js';
334
+ import { adcInitLines, adcChannelForPin } from './lowering/adc.js';
48
335
  import { pwmInitLines } from './lowering/pwm.js';
49
336
  import { dacInitLines } from './lowering/dac.js';
50
337
  import { fsInitLines } from './lowering/fs.js';
51
338
  import { hwtimerInitLines } from './lowering/hwtimer.js';
52
339
  import { i2cInitLines } from './lowering/i2c.js';
340
+ import { sensorStateLines } from './lowering/sensor.js';
341
+ import { spiTargetStateLines } from './lowering/spi.js';
342
+ import { threadStateLines } from './lowering/thread.js';
343
+ import { uartRingStateLines } from './lowering/uart.js';
53
344
  import { spiInitLines } from './lowering/spi.js';
54
345
  import { uartInitLines } from './lowering/uart.js';
55
- import { interruptInitLines } from './lowering/interrupts.js';
346
+ import { usbInitLines, usbdDeviceLines } from './lowering/usb.js';
347
+
348
+ /**
349
+ * STM32F4 boot-time DBGMCU setup: set DBG_SLEEP|DBG_STOP|DBG_STANDBY
350
+ * (DBGMCU_CR @ 0xE0042004, bits 0–2) so SWD stays attachable while the app
351
+ * sleeps. RCC_APB1ENR (0x40023840) bit 18 clocks the DBGMCU first — F4 gates
352
+ * register access behind it. Raw-register form (not the STM32 LL headers) so
353
+ * the shim stays include-light; AUTOSAR-clean via reinterpret_cast.
354
+ */
355
+ function stm32f4DbgmcuLines(): string[] {
356
+ return [
357
+ '// CUTTLEFISH_STM32_DBGMCU_BEGIN',
358
+ '#include <zephyr/init.h>',
359
+ 'static int __tc_stm32_dbgmcu_keep_swd_alive(void) {',
360
+ ' volatile uint32_t* const rcc_apb1enr = reinterpret_cast<volatile uint32_t*>(0x40023840);',
361
+ ' *rcc_apb1enr = *rcc_apb1enr | (1UL << 18);',
362
+ ' volatile uint32_t* const dbgmcu_cr = reinterpret_cast<volatile uint32_t*>(0xE0042004);',
363
+ ' *dbgmcu_cr = *dbgmcu_cr | 0x7u;',
364
+ ' return 0;',
365
+ '}',
366
+ 'SYS_INIT(__tc_stm32_dbgmcu_keep_swd_alive, PRE_KERNEL_1, 0);',
367
+ '// CUTTLEFISH_STM32_DBGMCU_END',
368
+ ];
369
+ }
370
+ import { interruptInitLines, collectInterruptPins } from './lowering/interrupts.js';
56
371
  import { wdtInitLines } from './lowering/wdt.js';
57
372
  import { bleInitLines } from './lowering/ble.js';
58
373
  import { wifiInitLines } from './lowering/wifi.js';
@@ -62,7 +377,6 @@ import { preferencesInitLines } from './lowering/preferences.js';
62
377
  import { randomInitLines } from './lowering/random.js';
63
378
  import { generateZephyrInitCode, generateZephyrBreakpointCode, generateZephyrLogpointCode } from './debug-codegen.js';
64
379
  import { generateStaticAsyncRuntime } from '@typecad/cuttlefish/api/shared';
65
- import { buildTimerPolyfill } from './async/timer-polyfill.js';
66
380
  import { resolveZephyrDisplayOp, newDisplayState, type DisplayState } from './display/index.js';
67
381
  import { buildDisplayRuntime } from './display/gfx.js';
68
382
  import { ZEPHYR_DISPLAY_PROFILES, BUILT_IN_PROFILES } from './display/profiles.js';
@@ -79,26 +393,44 @@ export class ZephyrStrategy implements PlatformStrategy {
79
393
  * by the methods that need the descriptor (shimLines, resolveHALOperation
80
394
  * via lowerHalOp).
81
395
  *
82
- * Tries to derive the chip descriptor from the board/MCU package's zephyr
83
- * fields (via boardConstants) first. Falls back to the hardcoded
84
- * chipForTarget registry for boards that haven't shipped zephyr config yet.
396
+ * The chip descriptor derives from the board/MCU manifest's zephyr
397
+ * fields (via boardConstants). There is no registry fallback — a board
398
+ * that never generated a manifest stays NO_BOARD_CHIP.
399
+ */
400
+ /**
401
+ * Eagerly resolve + cache the chip for `program`. The emitter calls this
402
+ * before rendering any bodies — hal-op lowering reads the module-global
403
+ * chip cache, and the lazy resolve (inside shimLines) runs later than the
404
+ * first lowered statement.
405
+ */
406
+ prepareChip(program: ProgramIR | undefined, ctx?: PlatformContext): void {
407
+ this.resolveChip(ctx, program);
408
+ }
409
+
410
+ /**
411
+ * Contract-board generation: a custom PCB spec (typecad contract) names
412
+ * its wired pads and bus families; the SoC's bus controller labels come
413
+ * from the installed Zephyr tree's soc dtsi. Same builder as every board.
85
414
  */
415
+ generateContractBoardModule(opts: {
416
+ soc: string;
417
+ zephyrBase: string;
418
+ pinNames: readonly string[];
419
+ padAliases?: readonly { exportName: string; padName: string }[];
420
+ peripherals: { i2c: boolean; spi: boolean; uart: boolean };
421
+ }): { boardTs: string; boardJson: string } {
422
+ return generateBoardModuleFromContract(opts);
423
+ }
424
+
86
425
  private resolveChip(ctx?: PlatformContext, program?: ProgramIR) {
87
- // 1. Try board/MCU package constants (new path)
426
+ // The chip view reconstructs from the project's generated board
427
+ // manifest (boardgen's zephyr.* constants) — the one path, for every
428
+ // board. No curated registry, no target-based fallback: a program with
429
+ // no board data resolves NO_BOARD_CHIP and the lowering reports
430
+ // unsupported per subsystem.
88
431
  const fromBoard = resolveChipFromBoard(program?.boardConstants);
89
- if (fromBoard) {
90
- setActiveChip(fromBoard);
91
- return fromBoard;
92
- }
93
-
94
- // 2. Fall back to frameworkData.buildTarget → hardcoded registry
95
- const fd = ctx?.frameworkData as Record<string, unknown> | undefined;
96
- const target =
97
- (fd?.target as string | undefined) ??
98
- (fd?.buildTarget as string | undefined);
99
- const chip = chipForTarget(target);
100
- setActiveChip(chip);
101
- return chip;
432
+ setActiveChip(fromBoard ?? NO_BOARD_CHIP);
433
+ return fromBoard ?? NO_BOARD_CHIP;
102
434
  }
103
435
 
104
436
  /**
@@ -131,6 +463,7 @@ export class ZephyrStrategy implements PlatformStrategy {
131
463
  // true so nothing is stripped — mirrors framework-esp32's forcedIncludes.
132
464
  const a = (ctx as any)?.analysis;
133
465
  const uses = (f: string): boolean => (a ? !!a[f] : true);
466
+ const chip = this.resolveChip(ctx, _program);
134
467
  // <zephyr/drivers/gpio.h> and <cstdint> stay unconditional: gpio.h is
135
468
  // cross-cutting (gpio/power/interrupt/spi/pulse lowerings + the DT-spec
136
469
  // machinery all reference its API, and no single usesX flag owns it), and
@@ -144,10 +477,11 @@ export class ZephyrStrategy implements PlatformStrategy {
144
477
  // A program touching none of those needs no <cstdio>.
145
478
  const helpers = (a as { usedPolyfillHelpers?: Set<string> } | undefined)?.usedPolyfillHelpers;
146
479
  const needsCstdio = uses('usesCstdio') || uses('usesFS') || uses('usesPreferences')
147
- || uses('usesUart')
480
+ || uses('usesUart') || uses('usesUsb')
148
481
  || !!helpers?.has('__tc_print') || !!helpers?.has('__tc_println');
149
482
  if (needsCstdio) inc.push('<cstdio>');
150
483
  if (uses('usesI2C')) inc.push('<zephyr/drivers/i2c.h>');
484
+ if (uses('usesSensor')) inc.push('<zephyr/drivers/sensor.h>');
151
485
  if (uses('usesSPI')) inc.push('<zephyr/drivers/spi.h>');
152
486
  if (uses('usesUart')) inc.push('<zephyr/drivers/uart.h>');
153
487
  // uart.h is also needed by the printf-mode debug halt shim
@@ -155,6 +489,15 @@ export class ZephyrStrategy implements PlatformStrategy {
155
489
  // program itself does not use the UART HAL. In gdb mode the shim is not
156
490
  // emitted, so skip the include there to avoid pulling in an unused header.
157
491
  if (isPrintf && !inc.includes('<zephyr/drivers/uart.h>')) inc.push('<zephyr/drivers/uart.h>');
492
+ // USB CDC serial: the class instance is a UART device (uart.h); the
493
+ // device context macros + usbd_* API live in the next-stack header.
494
+ if (uses('usesUsb')) {
495
+ if (!inc.includes('<zephyr/drivers/uart.h>')) inc.push('<zephyr/drivers/uart.h>');
496
+ inc.push('<zephyr/usb/usbd.h>');
497
+ // touch-to-reset shim callback reboots via NVIC_SystemReset()
498
+ // (RAM-retaining Cortex-M reset — no CONFIG_REBOOT needed).
499
+ if (chip?.usb?.touchReset) inc.push('<cmsis_core.h>');
500
+ }
158
501
  if (uses('usesADC')) inc.push('<zephyr/drivers/adc.h>');
159
502
  if (uses('usesPWM')) inc.push('<zephyr/drivers/pwm.h>');
160
503
  if (uses('usesDAC')) inc.push('<zephyr/drivers/dac.h>');
@@ -164,8 +507,7 @@ export class ZephyrStrategy implements PlatformStrategy {
164
507
  if (uses('usesFS')) inc.push('<zephyr/fs/fs.h>', '<zephyr/fs/littlefs.h>', '<zephyr/storage/flash_map.h>', '<cstring>');
165
508
  // Hardware timers via the counter driver.
166
509
  if (uses('usesHwtimer')) inc.push('<zephyr/drivers/counter.h>');
167
- if (uses('usesWDT')) inc.push('<zephyr/drivers/watchdog.h>');
168
- if (uses('usesPower')) inc.push('<zephyr/pm/pm.h>', '<zephyr/pm/state.h>', '<zephyr/pm/policy.h>');
510
+ if (uses('usesWDT') || uses('usesWdt')) inc.push('<zephyr/drivers/watchdog.h>');
169
511
  // BLE: the bt_* GATT API + the flat-string headers the shim uses. <string>
170
512
  // is needed because a Utf8 (BleValueType.Utf8) read handler lowers to a
171
513
  // std::string-returning function (the string literal return type), and the
@@ -367,70 +709,203 @@ export class ZephyrStrategy implements PlatformStrategy {
367
709
  'template<typename T, typename U> inline T cuttlefish_nullish(const T& a, U b) { return !cuttlefish_is_nullish(a) ? a : (T)b; }',
368
710
  );
369
711
  }
370
- // millis() backed by the Zephyr uptime counter. uint32_t return matches
371
- // the Arduino API the shared runtime expects (wraps every ~49.7 days).
372
- // Kept when the program reads the clock itself — usesWallClock,
373
- // deliberately WITHOUT the delay() conflation usesMillis carries, because
374
- // Zephyr's delay lowers straight to k_msleep or has a hidden poller:
375
- // async functions / the async runtime, the setInterval/setTimeout
376
- // scheduler, or a mounted UI's per-frame tick.
712
+ // The runtime clock contract: __tc_now_ms() is the monotonic ms clock
713
+ // the shared runtimes (UI per-frame tick, async timers, scheduler)
714
+ // and currentTimeMillis() lower onto. Emitted whenever anything could
715
+ // read the clock: explicit Time calls, async, the interval/timeout
716
+ // scheduler, or a mounted UI. uint32_t wraps every ~49.7 days every
717
+ // consumer compares by subtraction, so the wrap is harmless.
377
718
  if (uses('usesWallClock') || uses('hasAsync') || (!a || a.timerCallCount > 0)
378
719
  || this.programUsesAsyncRuntime(program) || entryHasUI()) {
379
720
  guardBody.push(
380
- 'inline unsigned long millis() { return static_cast<unsigned long>(k_uptime_get_32()); }',
721
+ 'inline uint32_t __tc_now_ms(void) { return k_uptime_get_32(); }',
381
722
  );
382
723
  }
383
- // PROGMEM: only the (Arduino-oriented) UI runtime header can reference it.
384
- if (entryHasUI()) {
724
+ // Num fluent math (Num.abs/min/max) + the free math trio.
725
+ // On Zephyr newlib none of these
726
+ // names are macros, so the underscore-escape rewrite the
727
+ // strategy needs is not. abs(long)/min/max deliberately overload (not
728
+ // shadow) newlib's abs(int) — exact-match int calls still resolve to the
729
+ // libc overload.
730
+ if (uses('usesNum')) {
385
731
  guardBody.push(
386
- '#ifndef PROGMEM', '#define PROGMEM', '#endif',
732
+ 'struct __tc_Num {',
733
+ ' static long abs(long x) { return x < 0 ? -x : x; }',
734
+ ' static long min(long a, long b) { return a < b ? a : b; }',
735
+ ' static long max(long a, long b) { return a > b ? a : b; }',
736
+ // The core emitter escapes abs/min/max member calls to a trailing
737
+ // underscore (C-library collision) — provide the escaped spellings so
738
+ // Num.abs_()/Num.min_()/Num.max_() resolve too.
739
+ ' static long abs_(long x) { return x < 0 ? -x : x; }',
740
+ ' static long min_(long a, long b) { return a < b ? a : b; }',
741
+ ' static long max_(long a, long b) { return a > b ? a : b; }',
742
+ '} Num;',
743
+ 'inline long abs(long v) { return v < 0 ? -v : v; }',
744
+ 'inline long min(long a, long b) { return a < b ? a : b; }',
745
+ 'inline long max(long a, long b) { return a > b ? a : b; }',
746
+ // Escaped free-function spellings — the escape applies to bare call
747
+ // sites on some paths (abs_(-42)); cover both.
748
+ 'inline long abs_(long v) { return v < 0 ? -v : v; }',
749
+ 'inline long min_(long a, long b) { return a < b ? a : b; }',
750
+ 'inline long max_(long a, long b) { return a > b ? a : b; }',
387
751
  );
388
752
  }
389
- // map()/constrain() Arduino-API helpers dead code unless called. The
390
- // setup emitter ORs entryHasUI() into usesConstrain before we see it (the
391
- // UI runtime's progress/range draw calls constrain).
392
- if (uses('usesMap')) {
393
- guardBody.push(
394
- 'inline long map(long x, long in_min, long in_max, long out_min, long out_max) { return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; }',
395
- );
753
+ // Random PRNG shim: the Random namespace lowers to the __tc_rand_* PRNG.
754
+ // Emitted when the program actually uses the random.* ops, so a minimal
755
+ // program's shim stays empty. The dispatcher + PRNG helpers must precede
756
+ // the shims that call them (the guard block emits in push order, and
757
+ // C++ needs the definitions before use).
758
+ if (this.needsGpioReadShim(program, ctx)) {
759
+ guardBody.push(...emitGpioDevDispatcher(chip));
760
+ }
761
+ if (uses('usesRandom')) {
762
+ guardBody.push(...randomInitLines());
396
763
  }
397
- if (uses('usesConstrain')) {
764
+ // PROGMEM note: only some UI runtime headers reference it.
765
+ if (entryHasUI()) {
398
766
  guardBody.push(
399
- 'inline long constrain(long x, long a, long b) { return x < a ? a : (x > b ? b : x); }',
767
+ '#ifndef PROGMEM', '#define PROGMEM', '#endif',
400
768
  );
401
769
  }
402
770
  // Test-runner console helpers: @typecad/expect's Zephyr shim calls these
403
771
  // for protocol output. Overloaded for string (const char*) and numeric
404
772
  // (double) so the same call site works for markers and test values.
405
- // Emitted only when the expect preprocessor actually injected the calls
406
- // (tracked as usedPolyfillHelpers).
407
- if (!a || !!helpers?.has('__tc_print') || !!helpers?.has('__tc_println')) {
773
+ // The fs lowering bakes __tc_println into its error paths too, so the
774
+ // overloads are emitted unconditionally — ctx.analysis can be absent or
775
+ // stale on UI programs, and missing definitions would be a link error.
776
+ //
777
+ // The numeric form formats via INTEGER conversions only: libc float
778
+ // printf is not dependable across SDKs — the 0.17.5 toolchain swapped
779
+ // newlib for picolibc, whose default build silently prints NOTHING for
780
+ // %g (the same trap as newlib-nano's -u _printf_float), which emptied
781
+ // every [TC:EXPECT:...:value:] line. Integer %lld works in every libc
782
+ // configuration, and the host parser accepts plain fixed-point.
783
+ {
408
784
  guardBody.push(
409
785
  'inline void __tc_print(const char* s) { printf("%s", s); }',
410
- 'inline void __tc_print(double v) { printf("%g", v); }',
786
+ [
787
+ 'static inline char* __tc_fmt_num_buf(double v, char* out, size_t cap) {',
788
+ ' if (v != v) { snprintf(out, cap, "nan"); return out; }',
789
+ ' double a = v < 0 ? -v : v;',
790
+ ' long long ip = (long long)a;',
791
+ ' long long fr = (long long)((a - (double)ip) * 1000000.0 + 0.5);',
792
+ ' if (fr >= 1000000LL) { ip += 1LL; fr = 0LL; }',
793
+ ' int used = 0;',
794
+ ' if (v < 0 && (ip != 0LL || fr != 0LL)) { out[used++] = \'-\'; out[used] = \'\\0\'; }',
795
+ ' if (fr == 0LL) { snprintf(out + used, cap - (size_t)used, "%lld", ip); return out; }',
796
+ ' char fbuf[8];',
797
+ ' int len = snprintf(fbuf, sizeof(fbuf), "%06lld", fr);',
798
+ ' while (len > 0 && fbuf[len - 1] == \'0\') { fbuf[--len] = \'\\0\'; }',
799
+ ' snprintf(out + used, cap - (size_t)used, "%lld.%s", ip, fbuf);',
800
+ ' return out;',
801
+ '}',
802
+ 'static void __tc_fmt_num(double v) { char __b[32]; printf("%s", __tc_fmt_num_buf(v, __b, sizeof(__b))); }',
803
+ ].join('\n'),
804
+ 'inline void __tc_print(double v) { __tc_fmt_num(v); }',
411
805
  'inline void __tc_println(const char* s) { printf("%s\\n", s); }',
412
- 'inline void __tc_println(double v) { printf("%g\\n", v); }',
806
+ 'inline void __tc_println(double v) { __tc_fmt_num(v); printf("\\n"); }',
807
+ );
808
+ }
809
+ // Serial-port write helper: writes a scalar to a UART/CDC device a byte at
810
+ // a time. Overloaded on const char* (strings, snprintf buffers) and double
811
+ // (numbers/booleans) so a single __tc_dev_put(dev, value) call site formats
812
+ // any writable scalar — the same overload contract __tc_print uses. Emitted
813
+ // when the program writes a UART or USB port (both include uart.h).
814
+ if (uses('usesUart') || uses('usesUsb')) {
815
+ guardBody.push(
816
+ 'static inline void __tc_dev_put(const struct device* dev, const char* s) {',
817
+ ' for (; *s != \'\\0\'; ++s) { uart_poll_out(dev, *s); }',
818
+ '}',
819
+ 'static inline void __tc_dev_put(const struct device* dev, double v) {',
820
+ ' char __b[32];',
821
+ ' __tc_dev_put(dev, __tc_fmt_num_buf(v, __b, sizeof(__b)));',
822
+ '}',
413
823
  );
414
824
  }
415
825
 
416
826
  // Per-peripheral bus state — gated on the same ctx.analysis.usesX flags as
417
827
  // forcedIncludes, so an unused peripheral emits no state (and its header is
418
- // not included). Mirrors framework-esp32's shimLines espInit block.
828
+ // not included). Mirrors framework-esp32's shimLines espInit block. Within
829
+ // a bus, only the instances the program drives get state — an unused
830
+ // declared controller's __tc_<bus>N_dev trips -Wunused-variable under
831
+ // Zephyr's -Werror (collectUsedBusIndices; probes with no program emit all).
832
+ const usedBuses = collectUsedBusIndices(program);
419
833
  if (uses('usesI2C') && chip.i2c) {
420
- for (let i = 0; i < chip.i2c.controllers.length; i++) guardBody.push(...i2cInitLines(chip, i));
834
+ for (let i = 0; i < chip.i2c.controllers.length; i++) {
835
+ if (usedBuses && !usedBuses.i2c.has(i)) continue;
836
+ guardBody.push(...i2cInitLines(chip, i));
837
+ }
421
838
  }
422
839
  if (uses('usesSPI') && chip.spi) {
423
- for (let i = 0; i < chip.spi.controllers.length; i++) guardBody.push(...spiInitLines(chip, i));
840
+ for (let i = 0; i < chip.spi.controllers.length; i++) {
841
+ if (usedBuses && !usedBuses.spi.has(i)) continue;
842
+ guardBody.push(...spiInitLines(chip, i));
843
+ }
424
844
  }
425
845
  if (uses('usesUart') && chip.uart) {
426
- for (let i = 0; i < chip.uart.controllers.length; i++) guardBody.push(...uartInitLines(chip, i));
846
+ for (let i = 0; i < chip.uart.controllers.length; i++) {
847
+ if (usedBuses && !usedBuses.uart.has(i)) continue;
848
+ guardBody.push(...uartInitLines(chip, i));
849
+ }
850
+ // Thin UART RX rings — one interrupt-drained ring per port the program
851
+ // reads (uart.rx_* ops). Emitted after the device handles it references.
852
+ const rings = collectUartRings(program);
853
+ if (rings) {
854
+ for (const r of rings.values()) {
855
+ guardBody.push(...uartRingStateLines(r.index, r.ring));
856
+ }
857
+ }
858
+ }
859
+ // DT-bound sensors — one state block per constructed part (device handle
860
+ // + sensor_value scratch). The lowering's __tc_sensor_* references and the
861
+ // overlay's DT child nodes derive from the same facts (lowering/sensor.ts).
862
+ if (uses('usesSensor')) {
863
+ const sensors = collectSensors(program);
864
+ if (sensors) {
865
+ for (const s of sensors.values()) {
866
+ guardBody.push(...sensorStateLines(s.part, s.bus, s.port, s.busKind, s.spiHz, s.spiMode, s.alertPin));
867
+ }
868
+ }
869
+ }
870
+ // Thin SPI targets — one spi_dt_spec per constructed target, against the
871
+ // DT child node the overlay emits. Same shared-facts discipline (the
872
+ // tc-spit-cfg comment is the overlay scanner's channel).
873
+ if (uses('usesSPI')) {
874
+ const targets = collectSpiTargets(program);
875
+ if (targets) {
876
+ for (const t of targets.values()) {
877
+ guardBody.push(...spiTargetStateLines(t.bus, t.cs, t.hz, t.mode));
878
+ }
879
+ }
880
+ }
881
+ // Thin Threads — one stack + k_thread + trampoline per started slot.
882
+ // Keyed on thread.start ops only: join() without a prior start() on the
883
+ // same index is a user error that surfaces as the undefined slot symbol.
884
+ {
885
+ const threads = collectThreads(program);
886
+ if (threads) {
887
+ for (const t of threads.values()) {
888
+ guardBody.push(...threadStateLines(t.instance, t.stackBytes));
889
+ }
890
+ }
891
+ }
892
+ if (uses('usesUsb') && chip.usb) {
893
+ guardBody.push(...usbdDeviceLines(chip));
894
+ for (let i = 0; i < chip.usb.cdcInstances; i++) guardBody.push(...usbInitLines(chip, i));
895
+ }
896
+ // PWM init also fires (with alias vars for the override pins) when a
897
+ // program drives ONLY inline-override channels on a chip with no pwm
898
+ // facts — the lowered calls reference those aliases.
899
+ {
900
+ const pwmUserSpecs = collectPwmOverrideSpecs(program);
901
+ if ((uses('usesPWM') && chip.pwm) || pwmUserSpecs.length > 0) {
902
+ guardBody.push(...pwmInitLines(chip, collectUsedPins(program, 'pwm', chip), pwmUserSpecs));
903
+ }
427
904
  }
428
- if (uses('usesADC') && chip.adc) guardBody.push(...adcInitLines(chip));
429
- if (uses('usesPWM') && chip.pwm) guardBody.push(...pwmInitLines(chip));
430
905
  if (uses('usesDAC') && chip.dac) guardBody.push(...dacInitLines(chip));
431
906
  if (uses('usesHwtimer') && chip.hwtimer) guardBody.push(...hwtimerInitLines(chip));
432
- if (uses('usesInterrupts')) guardBody.push(...interruptInitLines(chip));
433
- if (uses('usesWDT') && chip.wdt) guardBody.push(...wdtInitLines(chip));
907
+ if (uses('usesInterrupts')) guardBody.push(...interruptInitLines(chip, program ? collectInterruptPins(program) : undefined));
908
+ if ((uses('usesWDT') || uses('usesWdt')) && chip.wdt) guardBody.push(...wdtInitLines(chip));
434
909
  if (uses('usesBle')) guardBody.push(...bleInitLines());
435
910
  // Display runtime (rect/text renderer): the DIRECT-call display path (user
436
911
  // code calling screen.display.fillRect etc., no @typecad/ui). Emitted only
@@ -454,7 +929,29 @@ export class ZephyrStrategy implements PlatformStrategy {
454
929
  if (uses('usesMqtt')) guardBody.push(...mqttInitLines());
455
930
  if (uses('usesPreferences')) guardBody.push(...preferencesInitLines());
456
931
  if (uses('usesFS')) guardBody.push(...fsInitLines());
457
- if (uses('usesRandom')) guardBody.push(...randomInitLines());
932
+ // The ADC init block also fires (with the override devices' handles)
933
+ // when a program uses ONLY inline-override reads on a chip with no adc
934
+ // facts — otherwise those reads reference undeclared device handles.
935
+ {
936
+ const adcOverrideDevices = collectAdcOverrideDevices(program);
937
+ if ((uses('usesADC') && chip.adc) || adcOverrideDevices.size > 0) {
938
+ guardBody.push(...adcInitLines(chip, collectUsedPins(program, 'adc'), adcOverrideDevices));
939
+ }
940
+ }
941
+ // STM32F4: keep the core debug port alive across WFI sleep. The DBGMCU
942
+ // gates PPB access while the core sleeps unless DBGMCU_CR DBG_SLEEP/
943
+ // DBG_STOP/DBG_STANDBY are set — without them openocd cannot examine or
944
+ // halt the running target ("Failed to read memory at 0xe000ed04", "AP
945
+ // write error, reset will not halt"), and with no RST pad on boards like
946
+ // the Black Pill the only recovery is the BOOT0 bootloader. Zephyr's
947
+ // CONFIG_STM32_ENABLE_DEBUG_SLEEP_STOP sets only DBG_STOP on F4 (the
948
+ // soc_config.c F1/L1 branch is the one that sets DBG_SLEEP), so the bits
949
+ // are set here at boot, unconditionally for dev boards.
950
+ // (soc is empty on board-resolved chips — the SoC name rides in the
951
+ // qualified id's variant segment, e.g. 'blackpill_f411ce/stm32f411xe'.)
952
+ if (chip.soc.startsWith('stm32f4') || /stm32f4\d*/.test(chip.id)) {
953
+ guardBody.push(...stm32f4DbgmcuLines());
954
+ }
458
955
 
459
956
  const lines: string[] = [];
460
957
  if (guardBody.length > 0) {
@@ -516,6 +1013,11 @@ export class ZephyrStrategy implements PlatformStrategy {
516
1013
  'static inline bool __tc_bp_is_disabled(int id) { return id >= 0 && id < 256 && __tc_bp_disabled[id]; }',
517
1014
  // Console input: poll the UART console device. DEVICE_DT_GET(DT_CHOSEN(zephyr_console))
518
1015
  // resolves to the board's console (UART0 USB-CDC on the XIAO nRF52840).
1016
+ // Not every board DTS declares a zephyr,console chosen (STM32MP1 M-side,
1017
+ // display/carrier boards): there is nothing to print a prompt on and
1018
+ // nothing to read a key from, so breakpoints auto-continue instead of
1019
+ // hanging an unattended run.
1020
+ '#if DT_HAS_CHOSEN(zephyr_console)',
519
1021
  'static inline char __tc_debug_wait_for_continue(int id) {',
520
1022
  ' const struct device* __con = DEVICE_DT_GET(DT_CHOSEN(zephyr_console));',
521
1023
  ' unsigned char __c = 0;',
@@ -528,33 +1030,21 @@ export class ZephyrStrategy implements PlatformStrategy {
528
1030
  " if ((__c == 's') || (__c == 'S')) { if (id >= 0 && id < 256) __tc_bp_disabled[id] = true; }",
529
1031
  ' return static_cast<char>(__c);',
530
1032
  '}',
1033
+ '#else',
1034
+ 'static inline char __tc_debug_wait_for_continue(int id) { (void)id; return static_cast<char>(0); }',
1035
+ '#endif // DT_HAS_CHOSEN(zephyr_console)',
531
1036
  '#endif // __TC_BP_DISABLED_DEFINED',
532
1037
  '',
533
1038
  );
534
1039
  }
535
1040
 
536
- // --- Zephyr entrypoint: main() runs setup()/loop() directly ---
537
- // The cuttlefish synthesizer emits setup() and loop() (it keys off
538
- // entrypointFunctionName()="setup" and requiresLoopFunction()=true). Zephyr
539
- // is a standard C main()-based RTOS, so main() bridges the two: it calls
540
- // setup() once, then loops loop() forever, yielding to the scheduler with
541
- // k_msleep(1) each iteration (cheap cooperative yield matches the esp32
542
- // app_main pattern). Declared extern here because setup/loop live in a
543
- // separate translation unit when generateHeaderFile() splits them into the
544
- // header.
545
- lines.push(
546
- 'extern void setup(void);',
547
- 'extern void loop(void);',
548
- '',
549
- 'int main(void) {',
550
- ' setup();',
551
- ' for (;;) {',
552
- ' loop();',
553
- ' k_msleep(1);',
554
- ' }',
555
- ' return 0;',
556
- '}',
557
- );
1041
+ // --- Zephyr entrypoint ---------------------------------------------------
1042
+ // No bridge here: the synthesizer emits main() itself (it keys off
1043
+ // entrypointFunctionName()="main" and requiresLoopFunction()=false), with
1044
+ // the top-level statements inlined. Per-frame driving (ui_tick, microtask
1045
+ // pumping, async task .run()) is appended to main() by the driver
1046
+ // machinery via asyncDriverFunctionName()="main" + hostEventLoop()see
1047
+ // asyncLoopInjection for the scheduler loop it closes over.
558
1048
 
559
1049
  // GPIO read shim: emitted only when something actually reads a pin at
560
1050
  // runtime — user digitalRead() calls, the @typecad/safety voter (calls
@@ -574,17 +1064,19 @@ export class ZephyrStrategy implements PlatformStrategy {
574
1064
  // dispatcher that resolves the owning controller's device per pin;
575
1065
  // single-controller SoCs collapse it to a one-liner. Each DT_NODELABEL is
576
1066
  // still compile-time-resolved per branch, so it is always statically valid.
1067
+ // (The dispatcher itself now lives at the top of the guard block — it must
1068
+ // precede the wiring-ambient shims and the PinGroup factory there; the
1069
+ // __tc_gpio_read body below is what still needs emitting here.)
577
1070
  if (this.needsGpioReadShim(program, ctx)) {
578
- lines.push(...emitGpioDevDispatcher(chip));
579
1071
  lines.push(
580
- 'inline int __tc_gpio_read(int pin) { return gpio_pin_get_raw(__tc_gpio_dev(static_cast<uint32_t>(pin)), static_cast<gpio_pin_t>(pin)); }',
1072
+ 'inline int __tc_gpio_read(int pin) { return gpio_pin_get_raw(__tc_gpio_dev(static_cast<uint32_t>(pin)), __tc_gpio_pin(static_cast<uint32_t>(pin))); }',
581
1073
  );
582
1074
  }
583
1075
  // __tc_gpio_write / __tc_delay_us are only referenced via @typecad/safety
584
1076
  // lowering, so they stay gated on it.
585
1077
  if (program && programUsesSafety(program)) {
586
1078
  lines.push(
587
- 'inline void __tc_gpio_write(uint32_t pin, uint32_t value) { gpio_pin_set_raw(__tc_gpio_dev(pin), pin, value); }',
1079
+ 'inline void __tc_gpio_write(uint32_t pin, uint32_t value) { gpio_pin_set_raw(__tc_gpio_dev(pin), __tc_gpio_pin(pin), value); }',
588
1080
  '#ifndef __TC_DELAY_US_DEFINED',
589
1081
  '#define __TC_DELAY_US_DEFINED',
590
1082
  'inline void __tc_delay_us(uint32_t us) { k_busy_wait(us); }',
@@ -604,11 +1096,16 @@ export class ZephyrStrategy implements PlatformStrategy {
604
1096
  // Collect the pins the program uses for output config, ADC reads, and
605
1097
  // interrupt attaches — deep-walking the IR the same way framework-esp32
606
1098
  // does (its profileDiagnostics walks program to find gpio.set_mode /
607
- // power.deep_sleep_pin / adc.read nodes).
1099
+ // adc.read nodes).
608
1100
  const outputPins = new Set<number>();
609
1101
  const adcReadPins = new Set<number>();
1102
+ const adcOverriddenPins = new Set<number>();
1103
+ const pwmOverriddenPins = new Set<number>();
610
1104
  const interruptPins = new Set<number>();
1105
+ const wdtOps = new Set<string>();
611
1106
  const dacPins = new Set<number>();
1107
+ const pwmPins = new Set<number>();
1108
+ const busInstances = { i2c: new Set<number>(), spi: new Set<number>(), uart: new Set<number>() };
612
1109
  const hwtimerInstances = new Set<number>();
613
1110
  let usesWifiOps = false;
614
1111
  let usesHttpOps = false;
@@ -623,16 +1120,50 @@ export class ZephyrStrategy implements PlatformStrategy {
623
1120
  && typeof op.pin === 'number') {
624
1121
  outputPins.add(op.pin);
625
1122
  }
626
- if ((op.operation === 'adc.read' || op.operation === 'adc.read_voltage')
1123
+ if ((op.operation === 'adc.read' || op.operation === 'adc.read_voltage'
1124
+ || op.operation === 'adc.read_raw' || op.operation === 'adc.read_mv')
627
1125
  && typeof op.pin === 'number') {
628
1126
  adcReadPins.add(op.pin);
1127
+ // Inline routing overrides (construction opts): the user vouches
1128
+ // for the pin — the unavailable-pin diagnostic does not apply.
1129
+ if ((typeof op.channelOverride === 'number' && op.channelOverride >= 0)
1130
+ || (typeof op.deviceOverride === 'string' && op.deviceOverride !== '')) {
1131
+ adcOverriddenPins.add(op.pin);
1132
+ }
629
1133
  }
630
- if (op.operation === 'interrupt.attach' && typeof op.pin === 'number') {
1134
+ if (op.operation === 'interrupt.attach_flags' && typeof op.pin === 'number') {
631
1135
  interruptPins.add(op.pin);
632
1136
  }
633
- if (op.operation === 'dac.write' && typeof op.pin === 'number') {
1137
+ if (typeof op.operation === 'string' && op.operation.startsWith('wdt.')) {
1138
+ wdtOps.add(op.operation);
1139
+ }
1140
+ if ((op.operation === 'dac.write' || op.operation === 'dac.write_value')
1141
+ && typeof op.pin === 'number') {
634
1142
  dacPins.add(op.pin);
635
1143
  }
1144
+ if ((op.operation === 'pwm.set_pulse' || op.operation === 'pwm.set_duty'
1145
+ || op.operation === 'pwm.set_period')
1146
+ && typeof op.pin === 'number') {
1147
+ pwmPins.add(op.pin);
1148
+ if ((typeof op.controllerOverride === 'string' && op.controllerOverride !== '')
1149
+ || (typeof op.channelOverride === 'number' && op.channelOverride >= 0)) {
1150
+ pwmOverriddenPins.add(op.pin);
1151
+ }
1152
+ }
1153
+ // Bus instance usage: which I2C/SPI/UART controller indexes the
1154
+ // program drives (the lowering resolves index N against
1155
+ // chip.<bus>.controllers[N]; an undeclared index emits references
1156
+ // to a state block that is never declared → link error).
1157
+ if (typeof op.operation === 'string') {
1158
+ const busMatch = op.operation.match(/^(i2c|spi|uart)\./);
1159
+ if (busMatch) {
1160
+ const bus = busMatch[1] as 'i2c' | 'spi' | 'uart';
1161
+ const id = typeof op.bus === 'string' ? op.bus
1162
+ : typeof op.port === 'string' ? op.port : undefined;
1163
+ const m = id?.match(/(\d+)$/);
1164
+ if (m) busInstances[bus].add(parseInt(m[1], 10));
1165
+ }
1166
+ }
636
1167
  if (typeof op.operation === 'string' && op.operation.startsWith('hwtimer.')) {
637
1168
  const inst = typeof op.instance === 'number'
638
1169
  ? op.instance
@@ -659,42 +1190,53 @@ export class ZephyrStrategy implements PlatformStrategy {
659
1190
  visit(program);
660
1191
 
661
1192
  // ── ADC pin validity ────────────────────────────────────────────────────
662
- // The SAADC lowering resolves a HAL pin to a channel via the chip
663
- // descriptor's adc.channels map. A pin not in that map resolves to -1,
664
- // which emits __tc_adc-1_setup() — an undefined symbol → link error. Flag
665
- // it at compile time with a clear message instead of an opaque link failure.
1193
+ // The SAADC lowering resolves the adc.read argument to a channel via the
1194
+ // chip descriptor's adc.channels map (pin-first, then channel-number
1195
+ // fallback for the Arduino-compat ADC.read(n) form see
1196
+ // adcChannelForPin). A number resolving to neither emits
1197
+ // __tc_adc-1_setup() — an undefined symbol → link error. Flag it at
1198
+ // compile time with a clear message instead of an opaque link failure.
666
1199
  const adcPins = new Set((chip.adc?.channels ?? []).map((c) => c.pin));
667
1200
  for (const pin of adcReadPins) {
668
- if (!adcPins.has(pin)) {
1201
+ if (adcOverriddenPins.has(pin)) continue;
1202
+ if (adcChannelForPin(chip, pin) < 0) {
669
1203
  const valid = [...adcPins].sort((x, y) => x - y).join(', ');
1204
+ // Cross-peripheral suggestion: the facts know what this pin IS wired
1205
+ // to — a PWM-capable pin misread as analog is the classic mix-up.
1206
+ const pwmOnPin = (chip.pwm?.specs ?? []).find((s) => s.pin === pin);
1207
+ const mixup = pwmOnPin
1208
+ ? ` GPIO ${pin} carries PWM on this board (${pwmOnPin.controller} ch ${pwmOnPin.channel}) — did you mean new PWM(${pin}, …)?`
1209
+ : '';
670
1210
  diags.push({
671
1211
  severity: 'error',
672
1212
  code: 'zephyr-adc-pin-unavailable',
673
1213
  message: `GPIO ${pin} is not a SAADC channel on ${chip.id} and cannot be read with adc.read.`,
674
1214
  hint: valid
675
- ? `Use an analog-capable pin. On ${chip.id} (SAADC): ${valid}.`
676
- : `This target has no ADC channels mapped in its chip descriptor.`,
1215
+ ? `Use an analog-capable pin. On ${chip.id} (SAADC): ${valid}.${mixup}`
1216
+ : `This target has no ADC channels mapped in its chip descriptor.${mixup}`,
677
1217
  source: program.fileName,
678
1218
  });
679
1219
  }
680
1220
  }
681
1221
 
682
1222
  // ── Interrupt pin validity ──────────────────────────────────────────────
683
- // interrupt.attach only wires a real callback for pins listed in the chip
684
- // descriptor's gpio.interruptPins (the lowering needs a DT spec to build
685
- // the gpio_callback struct at init). An attach on an unlisted pin emits
686
- // only a comment silent no-op. Flag it so the user knows the handler
687
- // will never fire.
1223
+ // GPIO.onInterrupt works on every REAL GPIO: pins listed in the chip
1224
+ // descriptor's gpio.interruptPins wire through the DT-spec chain, any
1225
+ // other in-range pin through the raw-controller chain. What cannot work
1226
+ // is a pin number no declared controller range covers (e.g. 99 on a
1227
+ // 34-pin SoC) — that lowering is a comment (silent no-op). Flag only
1228
+ // that case.
688
1229
  const intPins = new Set((chip.gpio.interruptPins ?? []).map((p) => p.pin));
689
1230
  for (const pin of interruptPins) {
690
- if (!intPins.has(pin)) {
1231
+ if (intPins.has(pin)) continue;
1232
+ const inRange = !chip.gpioControllers || chip.gpioControllers.length === 0
1233
+ || chip.gpioControllers.some((r) => pin >= r.minPin && pin <= r.maxPin);
1234
+ if (!inRange) {
691
1235
  diags.push({
692
1236
  severity: 'error',
693
1237
  code: 'zephyr-interrupt-pin-unavailable',
694
- message: `GPIO ${pin} has no interrupt DT spec on ${chip.id}; interrupt.attach is a no-op.`,
695
- hint: intPins.size > 0
696
- ? `Add the pin to the chip descriptor's gpio.interruptPins, or use an interrupt-capable pin: ${[...intPins].sort((x, y) => x - y).join(', ')}.`
697
- : `This target declares no interrupt pins in its chip descriptor; interrupts are not available.`,
1238
+ message: `GPIO ${pin} does not exist on ${chip.id}; onInterrupt is a no-op.`,
1239
+ hint: `Use a real GPIO on this board (controller ranges: ${chip.gpioControllers.map((r) => `${r.nodelabel} ${r.minPin}-${r.maxPin}`).join(', ')}).`,
698
1240
  source: program.fileName,
699
1241
  });
700
1242
  }
@@ -710,13 +1252,82 @@ export class ZephyrStrategy implements PlatformStrategy {
710
1252
  const dacChannels = new Set((chip.dac?.channels ?? []).map((c) => c.pin));
711
1253
  for (const pin of dacPins) {
712
1254
  if (!dacChannels.has(pin)) {
1255
+ const adcCh = (chip.adc?.channels ?? []).find((c) => c.pin === pin);
1256
+ const mixup = adcCh
1257
+ ? ` GPIO ${pin} is an ADC channel (${adcCh.controller ?? chip.adc?.nodeLabel ?? 'adc'} ch ${adcCh.channel}) — did you mean new ADC(${pin})?`
1258
+ : '';
713
1259
  diags.push({
714
1260
  severity: 'error',
715
1261
  code: 'zephyr-dac-pin-unavailable',
716
1262
  message: `GPIO ${pin} is not a DAC channel on ${chip.id} and cannot be driven with dac.write.`,
717
1263
  hint: dacChannels.size > 0
718
- ? `Use a DAC-capable pin. On ${chip.id}: ${[...dacChannels].sort((x, y) => x - y).join(', ')}.`
719
- : `${chip.id} has no DAC. Use an esp32_devkitc target (ESP32 DAC on GPIO25/26).`,
1264
+ ? `Use a DAC-capable pin. On ${chip.id}: ${[...dacChannels].sort((x, y) => x - y).join(', ')}.${mixup}`
1265
+ : `${chip.id} has no DAC. Use an esp32_devkitc target (ESP32 DAC on GPIO25/26).${mixup}`,
1266
+ source: program.fileName,
1267
+ });
1268
+ }
1269
+ }
1270
+ }
1271
+
1272
+ // ── PWM pin validity ────────────────────────────────────────────────────
1273
+ // pwm.write resolves a HAL pin to a DT spec via the chip descriptor's
1274
+ // pwm.specs (static) or pwm.matrix (ESP32 LEDC: any matrix pin synthesizes
1275
+ // a spec at build time). A pin on neither lowers to a comment — the pin
1276
+ // silently never toggles. Flag it so the user knows (warning, not error:
1277
+ // boards legitimately ship partial PWM coverage, e.g. only the aliased LED
1278
+ // channel, and the rest of the program still works).
1279
+ const pwmMatrix = chip.pwm?.matrix;
1280
+ const pwmSpecPins = new Set([
1281
+ ...(chip.pwm?.specs ?? []).map((s) => s.pin),
1282
+ ...(pwmMatrix?.pins ?? []),
1283
+ ]);
1284
+ const pwmValid = [...pwmSpecPins].sort((x, y) => x - y).join(', ');
1285
+ for (const pin of pwmPins) {
1286
+ if (pwmOverriddenPins.has(pin)) continue;
1287
+ if (!pwmSpecPins.has(pin)) {
1288
+ // Cross-peripheral suggestion: an analog pin driven as PWM is the
1289
+ // other classic mix-up.
1290
+ const adcCh = (chip.adc?.channels ?? []).find((c) => c.pin === pin);
1291
+ const mixup = adcCh
1292
+ ? ` GPIO ${pin} is an ADC channel (${adcCh.controller ?? chip.adc?.nodeLabel ?? 'adc'} ch ${adcCh.channel}) — did you mean new ADC(${pin})?`
1293
+ : '';
1294
+ diags.push({
1295
+ severity: 'warning',
1296
+ code: 'zephyr-pwm-pin-unavailable',
1297
+ message: pwmMatrix
1298
+ ? `pwm on GPIO ${pin} lowers to a no-op: the pin is outside ${chip.id}'s PWM-capable set (USB, flash/PSRAM, strapping, and console pads are excluded), so nothing is driven.`
1299
+ : `pwm on GPIO ${pin} lowers to a no-op: the pin has no PWM spec in ${chip.id}'s chip descriptor, so nothing is driven.`,
1300
+ hint: pwmValid
1301
+ ? pwmMatrix
1302
+ ? `PWM-capable pins on ${chip.id} (first ${pwmMatrix.channelCount} driven get channels): ${pwmValid}.${mixup}`
1303
+ : `PWM-capable pins on ${chip.id}: ${pwmValid}.${mixup}`
1304
+ : `${chip.id} maps no PWM channels in its chip descriptor — pwm.* are no-ops on this target.${mixup}`,
1305
+ source: program.fileName,
1306
+ });
1307
+ }
1308
+ }
1309
+ // ── Bus instance validity ───────────────────────────────────────────────
1310
+ // The bus lowerings resolve instance N against chip.<bus>.controllers[N]
1311
+ // and emit `__tc_<bus>N_dev` references; the state block is only declared
1312
+ // for declared controllers, so an out-of-range instance is an undefined
1313
+ // symbol at link time. Flag it here with the board's real controller set
1314
+ // instead (mirrors how the ADC gate replaces an opaque link error).
1315
+ const busLabels: Record<'i2c' | 'spi' | 'uart', { name: string; label: string }> = {
1316
+ i2c: { name: 'I2C', label: 'Wire' },
1317
+ spi: { name: 'SPI', label: 'SPI' },
1318
+ uart: { name: 'UART', label: 'Serial' },
1319
+ };
1320
+ for (const bus of ['i2c', 'spi', 'uart'] as const) {
1321
+ const controllers = chip[bus]?.controllers ?? [];
1322
+ for (const inst of busInstances[bus]) {
1323
+ if (inst < 0 || inst >= controllers.length) {
1324
+ diags.push({
1325
+ severity: 'error',
1326
+ code: 'zephyr-bus-instance-unavailable',
1327
+ message: `${busLabels[bus].name}${inst} is used but ${chip.id} declares only ${controllers.length} ${busLabels[bus].name} controller(s) in its Zephyr chip data — the generated code would not link.`,
1328
+ hint: controllers.length > 0
1329
+ ? `Use ${busLabels[bus].name}0 (the board's default-enabled controller: ${controllers.map((c) => c.nodeLabel).join(', ')}).`
1330
+ : `${chip.id} declares no ${busLabels[bus].name} controllers for Zephyr.`,
720
1331
  source: program.fileName,
721
1332
  });
722
1333
  }
@@ -750,12 +1361,36 @@ export class ZephyrStrategy implements PlatformStrategy {
750
1361
  }
751
1362
  }
752
1363
 
1364
+ // ── Watchdog availability ───────────────────────────────────────────────
1365
+ // wdt.* resolves the device via the chip descriptor's wdt.nodeLabel. A
1366
+ // chip without that entry (e.g. SAM D21 — Zephyr's samd21 dtsi exposes
1367
+ // no watchdog node) lowers to a comment — flag it so the user knows the
1368
+ // watchdog never arms.
1369
+ if (wdtOps.size > 0 && !chip.wdt) {
1370
+ diags.push({
1371
+ severity: 'error',
1372
+ code: 'zephyr-wdt-unavailable',
1373
+ message: `Watchdog ops are used but ${chip.id} exposes no watchdog device.`,
1374
+ hint: `${chip.id} declares no wdt.nodeLabel (Zephyr's devicetree for this SoC has no watchdog node). Use a target with a watchdog, or drop the wdt.* calls.`,
1375
+ source: program.fileName,
1376
+ });
1377
+ }
1378
+
1379
+ // ── Radio presence: family-derived, not board-curated ───────────────────
1380
+ // Espressif ESP32 variants carry a 2.4GHz WiFi radio; every other Zephyr
1381
+ // family in the catalog is radioless (until an Ethernet/board-wifi fact
1382
+ // exists). Derived from the soc name — or, when no board module was
1383
+ // generated, from the raw build target the config carries.
1384
+ const familyTarget = `${chip.soc || ''} ${(ctx?.frameworkData as Record<string, unknown> | undefined)?.target ?? ''} ${(ctx?.frameworkData as Record<string, unknown> | undefined)?.buildTarget ?? ''}`.toLowerCase();
1385
+ const wifiSupported = chip.wifi?.supported
1386
+ ?? familyTarget.split(/[^a-z0-9]+/).some((t) => t.startsWith('esp32'));
1387
+
753
1388
  // ── WiFi target validity ────────────────────────────────────────────────
754
1389
  // WiFi ops require a chip with a WiFi radio. The ESP32-S3 descriptor sets
755
1390
  // wifi.supported; the XIAO nRF52840 omits it (no radio). Flag wifi usage on
756
1391
  // a radioless chip so the user gets a clear "use an ESP32 target" message
757
1392
  // instead of an opaque link/DT failure.
758
- if (usesWifiOps && !chip.wifi?.supported) {
1393
+ if (usesWifiOps && !wifiSupported) {
759
1394
  diags.push({
760
1395
  severity: 'error',
761
1396
  code: 'zephyr-wifi-unavailable-on-target',
@@ -773,7 +1408,7 @@ export class ZephyrStrategy implements PlatformStrategy {
773
1408
  // gets a clear "use an ESP32 target" message instead of an opaque link or
774
1409
  // runtime failure. (HTTP rides over WiFi here; an Ethernet target would
775
1410
  // set wifi.supported via a different transport flag if/when added.)
776
- if (usesHttpOps && !chip.wifi?.supported) {
1411
+ if (usesHttpOps && !wifiSupported) {
777
1412
  diags.push({
778
1413
  severity: 'error',
779
1414
  code: 'zephyr-http-unavailable-on-target',
@@ -786,7 +1421,7 @@ export class ZephyrStrategy implements PlatformStrategy {
786
1421
  // ── MQTT target validity ─────────────────────────────────────────────
787
1422
  // Same constraint as HTTP: MQTT needs a network transport to reach a broker.
788
1423
  // Flag mqtt usage on a radioless chip so the user picks a networked target.
789
- if (usesMqttOps && !chip.wifi?.supported) {
1424
+ if (usesMqttOps && !wifiSupported) {
790
1425
  diags.push({
791
1426
  severity: 'error',
792
1427
  code: 'zephyr-mqtt-unavailable-on-target',
@@ -805,16 +1440,30 @@ export class ZephyrStrategy implements PlatformStrategy {
805
1440
  }
806
1441
 
807
1442
  /**
808
- * `loop` is forward-declared `extern` by the main() bridge shim
809
- * (extern void loop(void);int main(void) {...}), so emitting a
810
- * `static void loop()` definition redeclares it with conflicting linkage,
811
- * which GCC rejects. Exclude it from the static forward-declaration path
812
- * mirrors how ArduinoStrategy excludes `loop` (the Arduino core forward-
813
- * declares it extern). `setup` is already handled because it equals
1443
+ * Strip Arduino library headers that @typecad/hal's class surfaces register
1444
+ * (I2CBus <Wire.h>, SPIBus <SPI.h>). The Zephyr
1445
+ * equivalents are usage-gated forcedIncludes (<zephyr/drivers/i2c.h>, …),
1446
+ * so an unstripped Wire.h reaches the west build and gcc rejects it
1447
+ * ("Wire.h: No such file or directory") this hook is why I2C/SPI class
1448
+ * usage on Zephyr used to fail at compile time.
1449
+ */
1450
+ filterRequiredIncludes(includes: string[]): string[] {
1451
+ // Arduino-core / AVR-only headers the HAL classes declare via __includes.
1452
+ // On Zephyr the watchdog lowers to the Zephyr driver (<zephyr/drivers/
1453
+ // watchdog.h>, forced under usesWDT) — <avr/wdt.h> does not exist there.
1454
+ const arduinoOnlyHeaders = new Set(['<Wire.h>', '<SPI.h>', '<avr/wdt.h>', '<avr/io.h>', '<avr/interrupt.h>', '<avr/sleep.h>', '<avr/power.h>']);
1455
+ return includes.filter((i) => !arduinoOnlyHeaders.has(i));
1456
+ }
1457
+
1458
+ /**
1459
+ * Nothing to exclude: no shim or Zephyr core header forward-declares a user
1460
+ * function extern (the old setup()/loop() bridge was the only consumer of
1461
+ * this hook, excluding `loop`). The entrypoint itself (`main`) is already
1462
+ * excluded from the static forward-declaration path because it equals
814
1463
  * entrypointFunctionName().
815
1464
  */
816
1465
  forwardDeclarationExclusions(): string[] {
817
- return ['loop'];
1466
+ return [];
818
1467
  }
819
1468
 
820
1469
  // ── File shape ──────────────────────────────────────────────────────────
@@ -824,11 +1473,11 @@ export class ZephyrStrategy implements PlatformStrategy {
824
1473
  }
825
1474
 
826
1475
  entrypointFunctionName(): string {
827
- return 'setup';
1476
+ return 'main';
828
1477
  }
829
1478
 
830
1479
  requiresLoopFunction(): boolean {
831
- return true;
1480
+ return false;
832
1481
  }
833
1482
 
834
1483
  overrideBaseName(
@@ -838,17 +1487,17 @@ export class ZephyrStrategy implements PlatformStrategy {
838
1487
  isNpmPackage: boolean,
839
1488
  ): string {
840
1489
  // npm packages are library-style — don't rename. Entry files (non-npm) take
841
- // the out-dir name (mirrors Arduino's .ino-must-match-dir rule). Everything
842
- // else passes through. (The manifest's entrypoint.overrideBaseName field is
843
- // dead — never read in src/ — so this method is the sole name source.)
1490
+ // the out-dir name (mirrors the directory-name-must-match-entry rule).
1491
+ // Everything else passes through. (The manifest's entrypoint.overrideBaseName
1492
+ // field is dead — never read in src/ — so this method is the sole name source.)
844
1493
  if (isNpmPackage) return originalBaseName;
845
1494
  if (isEntryFile) return outDirBaseName;
846
1495
  return originalBaseName;
847
1496
  }
848
1497
 
849
1498
  effectiveEmitMode(requestedMode: string, _isNpmPackage: boolean): string {
850
- // Zephyr always emits .cpp (no .ino equivalent to force away from), so this
851
- // is passthrough regardless of npm/app. The 2-param shape matches the
1499
+ // Zephyr always emits .cpp (no single-file entry equivalent to force away
1500
+ // from), so this is passthrough regardless of npm/app. The 2-param shape
852
1501
  // interface and Arduino; behavior is identical across branches.
853
1502
  return requestedMode;
854
1503
  }
@@ -869,7 +1518,6 @@ export class ZephyrStrategy implements PlatformStrategy {
869
1518
 
870
1519
  mapReturnType(functionName: string, returnType: string): string {
871
1520
  if (functionName === 'main') return 'int';
872
- if (functionName === 'setup' || functionName === 'loop') return 'void';
873
1521
  return this.normalizeCppType(returnType);
874
1522
  }
875
1523
 
@@ -974,28 +1622,6 @@ export class ZephyrStrategy implements PlatformStrategy {
974
1622
  return 'for (;;) { k_msleep(1000); }';
975
1623
  }
976
1624
 
977
- isConsoleCall(callee: string): boolean {
978
- return callee.startsWith('console.');
979
- }
980
-
981
- transformConsoleCall(method: string, renderedArgs: string, forHeader: boolean): string {
982
- const semi = forHeader ? '' : ';';
983
- const empty = !renderedArgs || renderedArgs.trim() === '';
984
- const tag = method === 'error' ? '[ERROR] ' : method === 'warn' ? '[WARN] ' : '';
985
- if (empty) return `printk("%s\\n", "${tag}")${semi}`;
986
- const parts = renderedArgs.split(' << ');
987
- if (parts.length === 1) {
988
- return `printk("%s%s\\n", "${tag}", (${renderedArgs}))${semi}`;
989
- }
990
- const fmt = '%s' + '%s'.repeat(parts.length) + '\\n';
991
- const args = [`"${tag}"`, ...parts].join(', ');
992
- return `printk("${fmt}", ${args})${semi}`;
993
- }
994
-
995
- transformConsoleExpression(_method: string, _renderedArgs: string): string | undefined {
996
- return undefined;
997
- }
998
-
999
1625
  objectFieldInitializer(): string | undefined {
1000
1626
  return undefined;
1001
1627
  }
@@ -1039,18 +1665,6 @@ export class ZephyrStrategy implements PlatformStrategy {
1039
1665
  reason: 'delayMicroseconds() busy-waits the CPU for the full delay, stalling every lower-priority interrupt and the scheduler for its duration',
1040
1666
  severity: 'warning',
1041
1667
  }],
1042
- ['console.log', {
1043
- reason: 'console output lowers to printk(), which is ISR-legal but slow and lock-protected (CONFIG_PRINTK_SYNC) — it adds jitter to every interrupt behind it',
1044
- severity: 'info',
1045
- }],
1046
- ['console.error', {
1047
- reason: 'console output lowers to printk(), which is ISR-legal but slow and lock-protected (CONFIG_PRINTK_SYNC) — it adds jitter to every interrupt behind it',
1048
- severity: 'info',
1049
- }],
1050
- ['console.warn', {
1051
- reason: 'console output lowers to printk(), which is ISR-legal but slow and lock-protected (CONFIG_PRINTK_SYNC) — it adds jitter to every interrupt behind it',
1052
- severity: 'info',
1053
- }],
1054
1668
  ['I2C0', {
1055
1669
  reason: 'I2C transactions may sleep (driver locking + clock stretching) and are not callable from Zephyr interrupt context',
1056
1670
  severity: 'warning',
@@ -1138,20 +1752,18 @@ export class ZephyrStrategy implements PlatformStrategy {
1138
1752
  }
1139
1753
 
1140
1754
  mathHeader(): string {
1141
- // <math.h> is the schema-permitted value (the manifest enum allows none |
1142
- // <math.h> | <Arduino.h>). Zephyr's toolchain provides it; the C++ <cmath>
1143
- // names are available via it as well.
1144
- return '<math.h>';
1755
+ // <cmath>, not <math.h>: the shared lowering emits Math.<fn>() as
1756
+ // std::<fn>() (expression-to-ir), and picolibc's <math.h> declares the
1757
+ // C names in the global namespace only — std::round et al. fail to
1758
+ // compile. Builds run with CONFIG_REQUIRES_FULL_LIBCPP (real libstdc++),
1759
+ // whose <cmath> provides the std:: names.
1760
+ return '<cmath>';
1145
1761
  }
1146
1762
 
1147
1763
  cstringHeader(): string {
1148
1764
  return '<cstring>';
1149
1765
  }
1150
1766
 
1151
- needsVectorOverload(): boolean {
1152
- return true;
1153
- }
1154
-
1155
1767
  needsLargeEnumUnderlying(): boolean {
1156
1768
  return false;
1157
1769
  }
@@ -1167,10 +1779,10 @@ export class ZephyrStrategy implements PlatformStrategy {
1167
1779
  }
1168
1780
 
1169
1781
  // ── Async ───────────────────────────────────────────────────────────────
1170
- // Hybrid: timers are native (k_timer + k_work, see src/async/timer-polyfill.ts);
1171
- // Promises use the heap-free static runtime (generateStaticAsyncRuntime), pumped
1172
- // cooperatively in loop() via cuttlefish_pump_microtasks(). There is no
1173
- // __tc_timer_runtime.run() poll native timers fire from their own expiry path.
1782
+ // Promises use the heap-free static runtime (generateStaticAsyncRuntime),
1783
+ // pumped cooperatively via cuttlefish_pump_microtasks(). There is no
1784
+ // __tc_timer_runtime.run() poll — periodic work is a Thread (k_thread) or a
1785
+ // Counter (hardware timer), never a cooperative timer queue.
1174
1786
 
1175
1787
  getAsyncRuntimeConfig(): AsyncRuntimeConfig {
1176
1788
  return {
@@ -1178,12 +1790,52 @@ export class ZephyrStrategy implements PlatformStrategy {
1178
1790
  scheduler: 'microtask',
1179
1791
  waitForPinEdge: 'stub',
1180
1792
  hasPromiseRuntime: true,
1181
- hasTimers: true,
1793
+ hasTimers: false,
1182
1794
  // Static (heap-free) runtime — no STL headers required.
1183
1795
  requiredIncludes: [],
1184
1796
  };
1185
1797
  }
1186
1798
 
1799
+ /**
1800
+ * Board module generation: join the Zephyr board data pack (extracted
1801
+ * from the pinned tree) with the curated soc descriptors. See
1802
+ * src/boardgen.ts.
1803
+ */
1804
+ generateBoardModule(
1805
+ target: string,
1806
+ opts?: { factsJson?: string; asBuiltJson?: string },
1807
+ ): { boardTs: string; boardJson: string; warnings?: readonly string[] } | undefined {
1808
+ try {
1809
+ const g = generateBoard(target, { factsJson: opts?.factsJson, asBuiltJson: opts?.asBuiltJson });
1810
+ return { boardTs: g.boardTs, boardJson: g.boardJson, ...(g.warnings ? { warnings: g.warnings } : {}) };
1811
+ } catch (err) {
1812
+ // A malformed facts/as-built file is the USER's error — surface it
1813
+ // verbatim instead of the generic "cannot generate" below.
1814
+ if (err instanceof Error && (err.message.includes('cuttlefish.facts.json') || err.message.includes('as-built.json'))) throw err;
1815
+ return undefined;
1816
+ }
1817
+ }
1818
+
1819
+ /**
1820
+ * Regenerate the board catalog overlay from the user's own Zephyr tree —
1821
+ * `cuttlefish board sync`. After a `west update`, this is how new/changed/
1822
+ * removed boards reach projects without a cuttlefish release. See
1823
+ * src/sdk/board-catalog-sync.ts.
1824
+ */
1825
+ syncBoardCatalog(zephyrBase?: string): BoardCatalogSyncReport {
1826
+ return syncBoardCatalog(zephyrBase ? { zephyrBase } : {});
1827
+ }
1828
+
1829
+ /**
1830
+ * Refresh the overlay only when it is stale (provenance no longer matches
1831
+ * the tree) — the pre-step `cuttlefish board regen` runs so a regen after
1832
+ * `west update` picks up the tree's boards automatically. Tree walk only
1833
+ * happens when there is actual refreshing to do.
1834
+ */
1835
+ ensureFreshBoardCatalog(): BoardCatalogEnsureResult {
1836
+ return ensureFreshBoardCatalog();
1837
+ }
1838
+
1187
1839
  asyncLoopInjection(taskVarNames: string[], config: AsyncRuntimeConfig): string[];
1188
1840
  asyncLoopInjection(taskVarNames: string[], hasPromiseRuntime: boolean, hasTimers: boolean): string[];
1189
1841
  asyncLoopInjection(
@@ -1195,25 +1847,61 @@ export class ZephyrStrategy implements PlatformStrategy {
1195
1847
  typeof configOrBool === 'boolean'
1196
1848
  ? { hasPromiseRuntime: configOrBool, hasTimers: _hasTimers ?? false }
1197
1849
  : configOrBool;
1198
- // Drive every async state-machine task once per loop() iteration. The task
1199
- // globals auto-start on their first .run() (constructor sets STATE_0, which
1200
- // runs unconditionally), so this is both the start and the per-frame advance.
1201
- // The state machine no-ops in its terminal/cyclic state, so unconditional
1202
- // .run() is correct (mirrors framework-arduino). No isComplete() gating.
1203
- const lines: string[] = [];
1850
+ // Drive every async state-machine task once per scheduler iteration. The
1851
+ // task globals auto-start on their first .run() (constructor sets STATE_0,
1852
+ // which runs unconditionally), so this is both the start and the per-frame
1853
+ // advance. The state machine no-ops in its terminal/cyclic state, so
1854
+ // unconditional .run() is correct (mirrors framework-arduino). No
1855
+ // isComplete() gating.
1856
+ const work: string[] = [];
1204
1857
  if (cfg.hasPromiseRuntime) {
1205
- lines.push('cuttlefish_pump_microtasks();');
1858
+ work.push('cuttlefish_pump_microtasks();');
1206
1859
  }
1207
1860
  for (const n of taskVarNames) {
1208
- lines.push(`${n}.run();`);
1861
+ work.push(`${n}.run();`);
1209
1862
  }
1210
- // NOTE: no __tc_timer_runtime.run()Zephyr timers are native k_timer
1211
- // (timer-polyfill.ts), not a cooperative poll.
1212
- return lines;
1863
+ // NOTE: no timer-queue poll periodic work is a Thread (k_thread) or a
1864
+ // Counter (hardware timer), never a cooperative poll.
1865
+ //
1866
+ // main() runs once, so the per-frame work must close over its own loop.
1867
+ // Under a mounted UI the emitter's hostEventLoop() already wraps ui_tick +
1868
+ // these lines in `while (...) { ...; k_msleep(1); }` — return flat lines
1869
+ // there. Otherwise (async/timer programs with no UI) self-wrap the same
1870
+ // scheduler loop here. For a program whose top-level `while (true)` never
1871
+ // exits, the loop below it is unreachable — harmless, and identical to the
1872
+ // old bridge behavior (whose for(;;) around loop() was equally dead).
1873
+ if (entryHasUI()) {
1874
+ return work;
1875
+ }
1876
+ return ['for (;;) {', ...work.map((l) => ` ${l}`), ' k_msleep(1);', '}'];
1213
1877
  }
1214
1878
 
1215
1879
  asyncDriverFunctionName(): string {
1216
- return 'loop';
1880
+ return 'main';
1881
+ }
1882
+
1883
+ /**
1884
+ * Zephyr's scheduler loop for event-driven programs (UI mounted). The
1885
+ * emitter wraps the per-frame work — ui_tick plus the asyncLoopInjection
1886
+ * lines — in `while (__tc_zephyr_event_loop) { ...; k_msleep(1); }`: a
1887
+ * 1 ms cooperative yield to the kernel each frame, matching the cadence the
1888
+ * old setup()/loop() bridge gave loop(). The flag is never cleared — the
1889
+ * loop is the program's lifetime — but keeping the bool gives the condition
1890
+ * a used symbol (a bare `while (true)` would leave nothing to name, and an
1891
+ * unused flag would trip -Werror's -Wunused-variable).
1892
+ */
1893
+ hostEventLoop(): {
1894
+ flagName: string;
1895
+ continueCondition: string;
1896
+ preIteration: string;
1897
+ postIteration: string;
1898
+ } | null {
1899
+ return {
1900
+ flagName: '__tc_zephyr_event_loop',
1901
+ continueCondition: '__tc_zephyr_event_loop',
1902
+ preIteration: '',
1903
+ postIteration: 'k_msleep(1);',
1904
+ };
1217
1905
  }
1218
1906
 
1219
1907
  // ── Type aliases ────────────────────────────────────────────────────────
@@ -1229,7 +1917,7 @@ export class ZephyrStrategy implements PlatformStrategy {
1229
1917
  }
1230
1918
 
1231
1919
  currentTimeMillis(): string {
1232
- return 'millis()';
1920
+ return '__tc_now_ms()';
1233
1921
  }
1234
1922
 
1235
1923
  // ── Build configuration ──────────────────────────────────────────────────
@@ -1280,12 +1968,10 @@ export class ZephyrStrategy implements PlatformStrategy {
1280
1968
  // cuttlefish_halt: always (the runtime header may reference it).
1281
1969
  // string_methods / static_array: STL-free array + string helpers a no-STL
1282
1970
  // target needs (mutated/struct array literals + any string method).
1283
- // timer_methods: k_timer/k_work pool for setInterval/setTimeout (gated on
1284
- // timerCallCount at emit time in generateNativePolyfills).
1285
1971
  // async_runtime: heap-free static Promise/microtask runtime (no STL needed).
1286
1972
  return new Set<string>([
1287
1973
  'cuttlefish_halt', 'wiring_compat', 'string_methods', 'static_array',
1288
- 'timer_methods', 'async_runtime',
1974
+ 'async_runtime',
1289
1975
  ]);
1290
1976
  }
1291
1977
 
@@ -1415,23 +2101,6 @@ struct __tc_StaticArray {
1415
2101
  },
1416
2102
  ];
1417
2103
 
1418
- // Worker-offload runtime (Phase 1). Emitted only when the program uses
1419
- // worker.* ops, backed by the Zephyr primitives in worker-backing.ts
1420
- // (k_work system workqueue + k_sem for the completion barrier).
1421
- const usesWorker = !!((ctx as any)?.analysis?.usesWorker);
1422
- if (program && usesWorker) {
1423
- const workerPoly = buildWorkerRuntimePolyfill(program, this, buildZephyrWorkerBacking(), { poolSize: 4 });
1424
- if (workerPoly) polyfills.push(workerPoly);
1425
- }
1426
-
1427
- // timer_methods — k_timer/k_work pool. Gated on observed timer call count;
1428
- // a program with no setInterval/setTimeout emits nothing.
1429
- const analysis = (ctx as { analysis?: { timerCallCount?: number } } | undefined)?.analysis;
1430
- const timerCallCount = analysis?.timerCallCount ?? 0;
1431
- if (timerCallCount > 0) {
1432
- polyfills.push(buildTimerPolyfill(timerCallCount));
1433
- }
1434
-
1435
2104
  // async_runtime — heap-free static Promise/microtask runtime. Emitted when
1436
2105
  // the program declares an async function OR references an async-runtime
1437
2106
  // symbol (Async.sleep/.then from a non-async fn). The static path requires
@@ -1445,10 +2114,10 @@ struct __tc_StaticArray {
1445
2114
  domain: 'embedded',
1446
2115
  requiredIncludes: [],
1447
2116
  // Polyfill definitions emit before shimLines, but the runtime's
1448
- // timer bodies call millis() (defined in shimLines) — declare it
1449
- // first so the polyfill compiles even for programs whose source
2117
+ // timer bodies call __tc_now_ms() (defined in shimLines) — declare
2118
+ // it first so the polyfill compiles even for programs whose source
1450
2119
  // has no explicit timing call.
1451
- forwardDeclarations: ['unsigned long millis();'],
2120
+ forwardDeclarations: ['uint32_t __tc_now_ms(void);'],
1452
2121
  helperStructs: [generateStaticAsyncRuntime(8, this.getAsyncRuntimeConfig().waitForPinEdge)],
1453
2122
  helperFunctions: [],
1454
2123
  shimMacros: [],
@@ -1514,31 +2183,15 @@ struct __tc_StaticArray {
1514
2183
  // ── RTOS ─────────────────────────────────────────────────────────────────
1515
2184
 
1516
2185
  isRtosTarget(): boolean {
1517
- // Zephyr is a preemptive RTOS — delay()/k_msleep inside loop() is the
2186
+ // Zephyr is a preemptive RTOS — delay()/k_msleep inside main() is the
1518
2187
  // expected cooperative yield, not an anti-pattern to warn about.
1519
2188
  return true;
1520
2189
  }
1521
2190
 
1522
- // ── Worker offload backing (Phase 1) ─────────────────────────────────────
1523
- // Delegates to the Zephyr backing (worker-backing.ts): k_work system
1524
- // workqueue + k_sem for completion. k_sem provides the kernel memory barrier
1525
- // the dual-core contract requires (the worker runs on a workqueue thread).
1526
- private _workerBacking = buildZephyrWorkerBacking();
1527
-
1528
2191
  // Display state (mirrors Arduino's _displayCtx). Seeded on display.init; the
1529
2192
  // validator-probe path seeds the default profile lazily.
1530
2193
  private _displayState: DisplayState = newDisplayState();
1531
2194
 
1532
- workerSpawnLines(handleId: number, trampolineName: string, waiterExpr: string): string[] | undefined {
1533
- return this._workerBacking.spawnLines(handleId, trampolineName, waiterExpr);
1534
- }
1535
- workerSignalDoneExpr(handleId: number): string | undefined {
1536
- return this._workerBacking.signalDoneExpr(handleId);
1537
- }
1538
- workerIsDoneExpr(handleId: number): string | undefined {
1539
- return this._workerBacking.isDoneExpr(handleId);
1540
- }
1541
-
1542
2195
  // ── Graphics ──────────────────────────────────────────────────────────────
1543
2196
  // Generic <zephyr/drivers/display.h> + ported GFX primitives (see src/display/).
1544
2197
  // resolveDisplayOp delegates to resolveZephyrDisplayOp with the per-build
@@ -1618,6 +2271,12 @@ struct __tc_StaticArray {
1618
2271
  if (boardId === 'esp32s3_devkitc' || boardId.startsWith('esp32s3')) {
1619
2272
  return 'gdb';
1620
2273
  }
2274
+ // The STM32 Black Pill ships a verified ST-Link probe method in its board
2275
+ // package (openocd runner over SWD, with the reset_config quirk for the
2276
+ // unwired SRST line), so F5 attaches natively out of the box.
2277
+ if (boardId.startsWith('blackpill_')) {
2278
+ return 'gdb';
2279
+ }
1621
2280
  // The plain ESP32 (esp32_devkitc) intentionally stays on 'printf': unlike
1622
2281
  // the S3 it has NO built-in USB-JTAG, so gdb needs an external ESP-PROG
1623
2282
  // probe + a different OpenOCD cfg/toolchain dir (deferred). Falls through.