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

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 (52) hide show
  1. package/README.md +13 -1
  2. package/dist/chips/controllers.d.ts +28 -8
  3. package/dist/chips/controllers.js +49 -12
  4. package/dist/chips/resolve.js +32 -6
  5. package/dist/chips/types.d.ts +63 -12
  6. package/dist/chips/xiao-ble.js +12 -0
  7. package/dist/dt-config/kconfig.d.ts +11 -0
  8. package/dist/dt-config/kconfig.js +1 -1
  9. package/dist/dt-config/overlay.js +85 -0
  10. package/dist/framework.manifest.d.ts +20 -30
  11. package/dist/framework.manifest.js +12 -3
  12. package/dist/lowering/adc.d.ts +7 -4
  13. package/dist/lowering/adc.js +25 -11
  14. package/dist/lowering/gpio.js +9 -6
  15. package/dist/lowering/mqtt.js +9 -1
  16. package/dist/lowering/pulse.js +7 -7
  17. package/dist/lowering/pwm.d.ts +21 -3
  18. package/dist/lowering/pwm.js +28 -4
  19. package/dist/lowering/spi.js +2 -2
  20. package/dist/lowering/tone.js +3 -2
  21. package/dist/lowering/wifi.js +28 -5
  22. package/dist/strategy.js +56 -4
  23. package/dist/toolchain/debug-config.js +1 -1
  24. package/dist/toolchain/index.d.ts +1 -1
  25. package/dist/toolchain/index.js +83 -8
  26. package/dist/toolchain/scaffold.d.ts +9 -0
  27. package/dist/toolchain/scaffold.js +45 -0
  28. package/dist/toolchain/west-discover.d.ts +4 -1
  29. package/dist/toolchain/west-discover.js +2 -0
  30. package/dist/toolchain/west-spawn.js +17 -5
  31. package/package.json +5 -5
  32. package/src/chips/controllers.ts +61 -12
  33. package/src/chips/resolve.ts +32 -5
  34. package/src/chips/types.ts +63 -12
  35. package/src/chips/xiao-ble.ts +82 -70
  36. package/src/dt-config/kconfig.ts +12 -1
  37. package/src/dt-config/overlay.ts +546 -450
  38. package/src/framework.manifest.ts +12 -3
  39. package/src/lowering/adc.ts +28 -12
  40. package/src/lowering/gpio.ts +9 -6
  41. package/src/lowering/mqtt.ts +9 -1
  42. package/src/lowering/pulse.ts +7 -7
  43. package/src/lowering/pwm.ts +29 -4
  44. package/src/lowering/spi.ts +2 -2
  45. package/src/lowering/tone.ts +3 -3
  46. package/src/lowering/wifi.ts +29 -5
  47. package/src/strategy.ts +52 -4
  48. package/src/toolchain/debug-config.ts +1 -1
  49. package/src/toolchain/index.ts +645 -565
  50. package/src/toolchain/scaffold.ts +43 -0
  51. package/src/toolchain/west-discover.ts +321 -316
  52. package/src/toolchain/west-spawn.ts +17 -5
@@ -50,8 +50,16 @@ export default defineFrameworkManifest({
50
50
  profile: {
51
51
  // Informational list of supported board targets. The manifest validator
52
52
  // never iterates this; chipForTarget (src/chips/index.ts) is the real
53
- // resolver. ESP32-S3 + plain ESP32 added alongside the nRF52840 MVP target.
54
- targets: ['xiao_ble', 'esp32s3_devkitc', 'esp32_devkitc'],
53
+ // resolver. ESP32-S3 + plain ESP32 added alongside the nRF52840 MVP target;
54
+ // rpi_pico / rpi_pico2 / esp32c3 / esp32c6 / blackpill_f411ce resolve via
55
+ // the board packages' zephyr chip data (resolveChipFromBoard), not the
56
+ // hardcoded chip registry.
57
+ targets: [
58
+ 'xiao_ble', 'esp32s3_devkitc', 'esp32_devkitc',
59
+ 'rpi_pico', 'rpi_pico2/rp2350a/m33',
60
+ 'esp32c3_devkitm/esp32c3', 'esp32c6_devkitc/esp32c6/hpcore',
61
+ 'blackpill_f411ce/stm32f411xe',
62
+ ],
55
63
  forcedIncludes: ['<zephyr/kernel.h>', '<zephyr/drivers/gpio.h>', '<cstdint>'],
56
64
  symbolAliases: {},
57
65
  },
@@ -213,7 +221,8 @@ export default defineFrameworkManifest({
213
221
  },
214
222
  },
215
223
  // ── Partial: WiFi (STA connect + scan + config via conn_mgr/net_mgmt) ────
216
- // ESP32-S3 only profileDiagnostics flags wifi usage on radioless chips.
224
+ // ESP32 family only (esp32, esp32s3, esp32c3, esp32c6) profileDiagnostics
225
+ // flags wifi usage on radioless chips (nRF52840, RP2040/RP2350).
217
226
  // AP mode, credential persistence, static IP, and event callbacks deferred.
218
227
  wifi: {
219
228
  supported: true,
@@ -23,25 +23,38 @@ export function adcChannelForPin(chip: ZephyrChipDescriptor, pin: number): numbe
23
23
 
24
24
  /**
25
25
  * Emit the per-channel ADC setup state. One block per channel in the chip
26
- * descriptor, each guarded by a `static bool __tc_adc<N>_ready` so the first
27
- * read configures it and subsequent reads skip. Called from shimLines when
28
- * the program uses ADC.
26
+ * descriptor that the PROGRAM ACTUALLY READS (`usedPins`) an unread
27
+ * channel's `static` setup function would trip -Wunused-function in the
28
+ * single generated TU. When `usedPins` is omitted (probe paths with no
29
+ * program), every descriptor channel is emitted. Each block is guarded by a
30
+ * `static bool __tc_adc<N>_ready` so the first read configures it and
31
+ * subsequent reads skip. Called from shimLines when the program uses ADC.
29
32
  */
30
- export function adcInitLines(chip: ZephyrChipDescriptor): string[] {
33
+ export function adcInitLines(chip: ZephyrChipDescriptor, usedPins?: ReadonlySet<number>): string[] {
31
34
  const dev = `DEVICE_DT_GET(DT_NODELABEL(${chip.adc?.nodeLabel ?? 'adc'}))`;
32
35
  const res = chip.adc?.resolution ?? 12;
33
36
  const vref = chip.adc?.vrefMv ?? 3000;
37
+ // Gain/reference are SoC-specific: the nRF SAADC scheme (gain 1/4 against
38
+ // the 0.6V internal ref, vref-mv 3000 = VDD) is the default; the STM32
39
+ // driver requires exactly ADC_GAIN_1 + ADC_REF_INTERNAL (Zephyr maps
40
+ // "internal" to the VREF+ pad) with vref-mv = VDDA. The descriptor carries
41
+ // the SoC's pair so the emitted channel setup validates in the driver.
42
+ const gain = chip.adc?.gain ?? 'ADC_GAIN_1_4';
43
+ const reference = chip.adc?.reference ?? 'ADC_REF_INTERNAL';
44
+ const channels = (chip.adc?.channels ?? []).filter(
45
+ (c) => !usedPins || usedPins.has(c.pin),
46
+ );
34
47
  const lines: string[] = ['// CUTTLEFISH_ADC_BEGIN'];
35
48
  lines.push(`static const struct device* __tc_adc_dev = ${dev};`);
36
- for (const c of chip.adc?.channels ?? []) {
49
+ for (const c of channels) {
37
50
  const n = c.channel;
38
51
  lines.push(
39
52
  `static bool __tc_adc${n}_ready = false;`,
40
53
  `static void __tc_adc${n}_setup(void) {`,
41
54
  ` if (__tc_adc${n}_ready) return;`,
42
55
  ` const struct adc_channel_cfg cfg = {`,
43
- ` .gain = ADC_GAIN_1_4,`,
44
- ` .reference = ADC_REF_INTERNAL,`,
56
+ ` .gain = ${gain},`,
57
+ ` .reference = ${reference},`,
45
58
  ` .acquisition_time = ADC_ACQ_TIME_DEFAULT,`,
46
59
  ` .channel_id = ${n},`,
47
60
  ` .differential = 0,`,
@@ -68,6 +81,8 @@ export function lowerAdc(
68
81
  const o = op as any;
69
82
  const res = chip.adc?.resolution ?? 12;
70
83
  const vref = chip.adc?.vrefMv ?? 3000;
84
+ const gain = chip.adc?.gain ?? 'ADC_GAIN_1_4';
85
+ const reference = chip.adc?.reference ?? 'ADC_REF_INTERNAL';
71
86
 
72
87
  switch (op.operation) {
73
88
  case 'adc.read': {
@@ -79,10 +94,11 @@ export function lowerAdc(
79
94
  }
80
95
  case 'adc.read_voltage': {
81
96
  const ch = adcChannelForPin(chip, o.pin);
82
- // Read raw, convert to millivolts via adc_raw_to_millivolts (gain 1/4,
83
- // internal ref). Returns mV as int.
97
+ // Read raw, convert to millivolts via adc_raw_to_millivolts with the
98
+ // descriptor's gain (raw_to_millivolts divides out the gain the channel
99
+ // was set up with). Returns mV as int.
84
100
  return {
85
- expression: `({ __tc_adc${ch}_setup(); int16_t __b = 0; struct adc_sequence __s = { .channels = BIT(${ch}), .buffer = &__b, .buffer_size = sizeof(__b), .resolution = ${res} }; adc_read(__tc_adc_dev, &__s); int32_t __v = __b; adc_raw_to_millivolts(${vref}, ADC_GAIN_1_4, ${res}, &__v); __v; })`,
101
+ expression: `({ __tc_adc${ch}_setup(); int16_t __b = 0; struct adc_sequence __s = { .channels = BIT(${ch}), .buffer = &__b, .buffer_size = sizeof(__b), .resolution = ${res} }; adc_read(__tc_adc_dev, &__s); int32_t __v = __b; adc_raw_to_millivolts(${vref}, ${gain}, ${res}, &__v); __v; })`,
86
102
  };
87
103
  }
88
104
  case 'adc.get_resolution':
@@ -90,9 +106,9 @@ export function lowerAdc(
90
106
  case 'adc.set_reference':
91
107
  // Zephyr configures the reference at channel-setup time; runtime switching
92
108
  // would require re-setup. Record the intent as a no-op statement.
93
- return { code: `/* adc.set_reference(${o.reference}): configured at channel setup (ADC_REF_INTERNAL) */` };
109
+ return { code: `/* adc.set_reference(${o.reference}): configured at channel setup (${reference}) */` };
94
110
  case 'adc.get_reference':
95
- return { expression: `0 /* DEFAULT (ADC_REF_INTERNAL) */` };
111
+ return { expression: `0 /* DEFAULT (${reference}) */` };
96
112
  default:
97
113
  throw new Error(
98
114
  `framework-zephyr does not yet support HAL op \`${op.operation}\`. ` +
@@ -17,7 +17,7 @@
17
17
 
18
18
  import type { HALOpIR } from '@typecad/cuttlefish/api/shared';
19
19
  import type { ZephyrChipDescriptor } from '../chips/types.js';
20
- import { controllerNodelabelForPin } from '../chips/controllers.js';
20
+ import { controllerNodelabelForPin, controllerRawPinForPin } from '../chips/controllers.js';
21
21
 
22
22
  /** The C identifier emitted for a pin's gpio_dt_spec variable. */
23
23
  export function dtSpecVarName(dtSpec: string): string {
@@ -121,20 +121,23 @@ function lowerGpioRaw(
121
121
  const o = op as any;
122
122
  const pin: number = o.pin;
123
123
  // Resolve the owning controller by pin range (ESP32-S3 splits GPIO across
124
- // gpio0/gpio1). For single-controller SoCs this is just chip.gpioController.
124
+ // gpio0/gpio1; STM32 across gpioa/gpiob/gpioc). For single-controller SoCs
125
+ // this is just chip.gpioController. The raw API takes the PORT-RELATIVE
126
+ // index (STM32 gpiob is 0-15), not the global HAL pin number.
125
127
  const controller = `DEVICE_DT_GET(DT_NODELABEL(${controllerNodelabelForPin(chip, pin)}))`;
128
+ const rawPin = controllerRawPinForPin(chip, pin);
126
129
 
127
130
  switch (op.operation) {
128
131
  case 'gpio.set_mode': {
129
- return { code: `gpio_pin_configure(${controller}, ${pin}, ${flagsForMode(o.mode)});` };
132
+ return { code: `gpio_pin_configure(${controller}, ${rawPin}, ${flagsForMode(o.mode)});` };
130
133
  }
131
134
  case 'gpio.write': {
132
135
  const v = o.value;
133
136
  const rhs = typeof v === 'string' ? `((${v}) ? 1 : 0)` : v ? 1 : 0;
134
- return { code: `gpio_pin_set_raw(${controller}, ${pin}, ${rhs});` };
137
+ return { code: `gpio_pin_set_raw(${controller}, ${rawPin}, ${rhs});` };
135
138
  }
136
139
  case 'gpio.read':
137
- return { expression: `gpio_pin_get_raw(${controller}, ${pin})` };
140
+ return { expression: `gpio_pin_get_raw(${controller}, ${rawPin})` };
138
141
  case 'gpio.toggle':
139
142
  // Native atomic toggle — never read-modify-write. gpio_pin_get_raw on
140
143
  // a direction-only output reads the input latch, which is undefined on
@@ -142,7 +145,7 @@ function lowerGpioRaw(
142
145
  // gpio_pin_toggle is the driver-level atomic toggle, and for pins
143
146
  // configured without GPIO_ACTIVE_LOW the logical level equals the
144
147
  // physical one, so it matches the get_raw/set_raw used elsewhere.
145
- return { code: `gpio_pin_toggle(${controller}, ${pin});` };
148
+ return { code: `gpio_pin_toggle(${controller}, ${rawPin});` };
146
149
  default:
147
150
  throw new Error(
148
151
  `framework-zephyr does not yet support HAL op \`${op.operation}\`. ` +
@@ -294,7 +294,15 @@ export function mqttInitLines(): string[] {
294
294
  ` (void)k_thread_create(&__tc_mqtt.poll_thread, __tc_mqtt_stack,`,
295
295
  ` K_THREAD_STACK_SIZEOF(__tc_mqtt_stack),`,
296
296
  ` __tc_mqtt_poll_thread, nullptr, nullptr, nullptr,`,
297
- ` 5, 0, K_NO_WAIT);`,
297
+ ` 5, 0, K_FOREVER);`,
298
+ `#ifdef CONFIG_SMP`,
299
+ ` // On SMP targets park the poll thread on the app core so the network`,
300
+ ` // stack never competes with the main/UI thread for core 0. k_thread_cpu_pin`,
301
+ ` // is SMP-only; the pin happens before k_thread_start (which is why the`,
302
+ ` // thread is created K_FOREVER). Compiles away on !SMP builds.`,
303
+ ` (void)k_thread_cpu_pin(&__tc_mqtt.poll_thread, 1);`,
304
+ `#endif`,
305
+ ` k_thread_start(&__tc_mqtt.poll_thread);`,
298
306
  `}`,
299
307
  ``,
300
308
  `static inline void __tc_mqtt_set_on_message(__tc_mqtt_msg_cb_t fn) {`,
@@ -10,7 +10,7 @@
10
10
 
11
11
  import type { HALOpIR } from '@typecad/cuttlefish/api/shared';
12
12
  import type { ZephyrChipDescriptor } from '../chips/types.js';
13
- import { controllerNodelabelForPin } from '../chips/controllers.js';
13
+ import { controllerNodelabelForPin, controllerRawPinForPin } from '../chips/controllers.js';
14
14
 
15
15
  /** `DEVICE_DT_GET(DT_NODELABEL(<owning-controller>))` for a HAL pin. */
16
16
  function devForPin(chip: ZephyrChipDescriptor, pin: number): string {
@@ -32,9 +32,9 @@ export function lowerPulseOrShift(
32
32
  // for the start edge is bounded by the timeout; the measurement of the
33
33
  // pulse itself is intentionally unbounded (that IS the pulse length).
34
34
  // Returns -1 (0) if the start edge never arrives within the timeout.
35
- const pin = o.pin;
35
+ const pin = controllerRawPinForPin(chip, o.pin);
36
36
  const want = o.value;
37
- const dev = devForPin(chip, pin);
37
+ const dev = devForPin(chip, o.pin);
38
38
  const timeout = o.timeout ?? 1_000_000; // default 1s in us
39
39
  return {
40
40
  expression: `({ int64_t __max = static_cast<int64_t>(${timeout} / 1000); int64_t __t0 = k_uptime_get(); bool __ok = true; while (gpio_pin_get_raw(${dev}, ${pin}) != ${want}) { if ((k_uptime_get() - __t0) > __max) { __ok = false; break; } } int32_t __ret = 0; if (__ok) { int64_t __start = k_uptime_get(); while (gpio_pin_get_raw(${dev}, ${pin}) == ${want}) { } __ret = static_cast<int32_t>((k_uptime_get() - __start) * 1000); } __ret; })`,
@@ -45,9 +45,9 @@ export function lowerPulseOrShift(
45
45
  // with NO timeout (the comment claimed "no overflow concern" but the
46
46
  // real risk was hanging the thread on a stuck pin). Apply the same
47
47
  // timeout-bounded start-edge wait as pulse.in (bug Q5).
48
- const pin = o.pin;
48
+ const pin = controllerRawPinForPin(chip, o.pin);
49
49
  const want = o.value;
50
- const dev = devForPin(chip, pin);
50
+ const dev = devForPin(chip, o.pin);
51
51
  const timeout = o.timeout ?? 3_000_000; // default 3s in us (long pulses)
52
52
  return {
53
53
  expression: `({ int64_t __max = static_cast<int64_t>(${timeout} / 1000); int64_t __t0 = k_uptime_get(); bool __ok = true; while (gpio_pin_get_raw(${dev}, ${pin}) != ${want}) { if ((k_uptime_get() - __t0) > __max) { __ok = false; break; } } int32_t __ret = 0; if (__ok) { int64_t __start = k_uptime_get(); while (gpio_pin_get_raw(${dev}, ${pin}) == ${want}) { } __ret = static_cast<int32_t>((k_uptime_get() - __start) * 1000); } __ret; })`,
@@ -66,7 +66,7 @@ export function lowerPulseOrShift(
66
66
  const test = msbFirst ? '(__i >= 0)' : '(__i < 8)';
67
67
  const step = msbFirst ? '__i--' : '__i++';
68
68
  return {
69
- code: `for (int __i = ${init}; ${test}; ${step}) { gpio_pin_set_raw(${dataDev}, ${dataPin}, (${o.value} >> __i) & 1); gpio_pin_set_raw(${clockDev}, ${clockPin}, 1); k_busy_wait(1); gpio_pin_set_raw(${clockDev}, ${clockPin}, 0); }`,
69
+ code: `for (int __i = ${init}; ${test}; ${step}) { gpio_pin_set_raw(${dataDev}, ${controllerRawPinForPin(chip, dataPin)}, (${o.value} >> __i) & 1); gpio_pin_set_raw(${clockDev}, ${controllerRawPinForPin(chip, clockPin)}, 1); k_busy_wait(1); gpio_pin_set_raw(${clockDev}, ${controllerRawPinForPin(chip, clockPin)}, 0); }`,
70
70
  };
71
71
  }
72
72
  case 'shift.in': {
@@ -81,7 +81,7 @@ export function lowerPulseOrShift(
81
81
  const step = msbFirst ? '__i--' : '__i++';
82
82
  const accum = msbFirst ? '__v = (__v << 1)' : '__v |= (bit << __i)';
83
83
  return {
84
- expression: `({ uint8_t __v = 0; for (int __i = ${init}; ${test}; ${step}) { gpio_pin_set_raw(${clockDev}, ${clockPin}, 1); k_busy_wait(1); int bit = gpio_pin_get_raw(${dataDev}, ${dataPin}); gpio_pin_set_raw(${clockDev}, ${clockPin}, 0); ${accum}; } __v; })`,
84
+ expression: `({ uint8_t __v = 0; for (int __i = ${init}; ${test}; ${step}) { gpio_pin_set_raw(${clockDev}, ${controllerRawPinForPin(chip, clockPin)}, 1); k_busy_wait(1); int bit = gpio_pin_get_raw(${dataDev}, ${controllerRawPinForPin(chip, dataPin)}); gpio_pin_set_raw(${clockDev}, ${controllerRawPinForPin(chip, clockPin)}, 0); ${accum}; } __v; })`,
85
85
  };
86
86
  }
87
87
  default:
@@ -10,6 +10,26 @@
10
10
  import type { HALOpIR } from '@typecad/cuttlefish/api/shared';
11
11
  import type { ZephyrChipDescriptor, ZephyrPwmSpec } from '../chips/types.js';
12
12
 
13
+ /**
14
+ * The DT alias a PWM spec is addressed by. Board-shipped specs carry their
15
+ * alias in `dtSpec`; synthesized specs (controller + channel) get a
16
+ * `tc-pwm<pin>` alias that the overlay generator creates in
17
+ * <board>.overlay — both sides derive the name from the pin so they agree.
18
+ */
19
+ export function pwmDtAlias(spec: ZephyrPwmSpec): string {
20
+ return spec.dtSpec ?? `tc-pwm${spec.pin}`;
21
+ }
22
+
23
+ /**
24
+ * The C macro token for a spec's alias. Zephyr's devicetree macros replace
25
+ * dashes in alias names with underscores (`pwm-led0` in DTS is
26
+ * DT_ALIAS(pwm_led0) in C) — the dashed spelling is a subtraction
27
+ * expression and fails to compile (caught by the blackpill E2E west build).
28
+ */
29
+ export function pwmDtAliasToken(spec: ZephyrPwmSpec): string {
30
+ return pwmDtAlias(spec).replace(/-/g, '_');
31
+ }
32
+
13
33
  /** Look up a PWM spec by HAL pin number. */
14
34
  function findPwmSpec(chip: ZephyrChipDescriptor, pin: number): ZephyrPwmSpec | undefined {
15
35
  return chip.pwm?.specs.find((s) => s.pin === pin);
@@ -17,18 +37,23 @@ function findPwmSpec(chip: ZephyrChipDescriptor, pin: number): ZephyrPwmSpec | u
17
37
 
18
38
  /** The C variable name emitted for a PWM channel's spec. */
19
39
  function pwmVarName(spec: ZephyrPwmSpec): string {
20
- return `__tc_pwm_${spec.dtSpec.replace(/-/g, '_')}`;
40
+ return `__tc_pwm_${pwmDtAliasToken(spec)}`;
21
41
  }
22
42
 
23
43
  /**
24
44
  * Emit the per-channel PWM spec declarations. One per spec in the chip
25
- * descriptor. Called from shimLines when the program uses PWM.
45
+ * descriptor that the PROGRAM ACTUALLY DRIVES (`usedPins`) a spec for an
46
+ * untouched pin is unused code in the emitted TU (and would need a dead DT
47
+ * alias in the overlay). When `usedPins` is omitted (probe paths with no
48
+ * program), every spec is emitted. Called from shimLines when the program
49
+ * uses PWM.
26
50
  */
27
- export function pwmInitLines(chip: ZephyrChipDescriptor): string[] {
51
+ export function pwmInitLines(chip: ZephyrChipDescriptor, usedPins?: ReadonlySet<number>): string[] {
28
52
  const lines: string[] = ['// CUTTLEFISH_PWM_BEGIN'];
29
53
  for (const spec of chip.pwm?.specs ?? []) {
54
+ if (usedPins && !usedPins.has(spec.pin)) continue;
30
55
  lines.push(
31
- `static const struct pwm_dt_spec ${pwmVarName(spec)} = PWM_DT_SPEC_GET(DT_ALIAS(${spec.dtSpec}));`,
56
+ `static const struct pwm_dt_spec ${pwmVarName(spec)} = PWM_DT_SPEC_GET(DT_ALIAS(${pwmDtAliasToken(spec)}));`,
32
57
  );
33
58
  }
34
59
  lines.push('// CUTTLEFISH_PWM_END');
@@ -15,7 +15,7 @@
15
15
  import type { HALOpIR } from '@typecad/cuttlefish/api/shared';
16
16
  import type { ZephyrChipDescriptor } from '../chips/types.js';
17
17
  import { parseControllerIndex } from './util.js';
18
- import { controllerNodelabelForPin } from '../chips/controllers.js';
18
+ import { controllerNodelabelForPin, controllerRawPinForPin } from '../chips/controllers.js';
19
19
 
20
20
  /** The C variable prefix for a controller's state. */
21
21
  function prefix(idx: number): string {
@@ -112,7 +112,7 @@ export function lowerSpi(
112
112
  const val = op.operation === 'spi.cs_low' ? 0 : 1;
113
113
  const gpioController = controllerNodelabelForPin(chip, o.pin);
114
114
  return {
115
- code: `gpio_pin_set_raw(DEVICE_DT_GET(DT_NODELABEL(${gpioController})), ${o.pin}, ${val});`,
115
+ code: `gpio_pin_set_raw(DEVICE_DT_GET(DT_NODELABEL(${gpioController})), ${controllerRawPinForPin(chip, o.pin)}, ${val});`,
116
116
  };
117
117
  }
118
118
  default:
@@ -14,7 +14,7 @@
14
14
 
15
15
  import type { HALOpIR } from '@typecad/cuttlefish/api/shared';
16
16
  import type { ZephyrChipDescriptor } from '../chips/types.js';
17
- import { lowerPwm } from './pwm.js';
17
+ import { lowerPwm, pwmDtAliasToken } from './pwm.js';
18
18
 
19
19
  /**
20
20
  * Resolve a HAL tone.* op to Zephyr C++ via PWM.
@@ -35,7 +35,7 @@ export function lowerTone(
35
35
  if (!spec) {
36
36
  return { code: `/* tone.play(${o.frequency}): no PWM spec in chip descriptor */` };
37
37
  }
38
- const v = `__tc_pwm_${spec.dtSpec.replace(/-/g, '_')}`;
38
+ const v = `__tc_pwm_${pwmDtAliasToken(spec)}`;
39
39
  const freq = o.frequency;
40
40
  const duration = o.duration;
41
41
  const setTone = `uint32_t __period = (${freq} > 0) ? (1000000000ULL / static_cast<uint64_t>(${freq})) : 0; pwm_set_dt(&${v}, __period, __period / 2);`;
@@ -48,7 +48,7 @@ export function lowerTone(
48
48
  case 'tone.stop': {
49
49
  const spec = chip.pwm?.specs[0];
50
50
  if (!spec) return { code: `/* tone.stop: no PWM spec */` };
51
- const v = `__tc_pwm_${spec.dtSpec.replace(/-/g, '_')}`;
51
+ const v = `__tc_pwm_${pwmDtAliasToken(spec)}`;
52
52
  return { code: `pwm_set_pulse_dt(&${v}, 0);` };
53
53
  }
54
54
  default:
@@ -126,6 +126,29 @@ export function wifiInitLines(): string[] {
126
126
  ` }`,
127
127
  ` __tc_wifi.inited = true;`,
128
128
  `}`,
129
+ ``,
130
+ `// ── blocking-wait pump ───────────────────────────────────────────────────`,
131
+ `// Blocking wifi waits hold the main thread, which suspends the loop()-driven`,
132
+ `// ui_tick — a 15s association would freeze the display for its whole`,
133
+ `// duration. Each wait slice sleeps 20ms and, in the entry TU of a UI build`,
134
+ `// (the only TU carrying the ui_tick definition), pumps one frame so`,
135
+ `// animations and touch stay live during the wait. Constraint: do NOT call`,
136
+ `// blocking wifi waits from inside UI event handlers — that re-enters`,
137
+ `// ui_tick mid-tick; use wifi.connect_start + wifi.is_connected there.`,
138
+ `#ifdef CUTTLEFISH_ENTRY_UI_TU`,
139
+ `static void ui_tick(uint16_t deltaMs); // defined by the UI runtime header`,
140
+ `#endif`,
141
+ `static void __tc_wifi_wait_slice(uint32_t slice_ms) {`,
142
+ ` k_msleep(slice_ms);`,
143
+ `#ifdef CUTTLEFISH_ENTRY_UI_TU`,
144
+ ` static uint32_t last_ms = 0U;`,
145
+ ` uint32_t now_ms = k_uptime_get_32();`,
146
+ ` uint32_t delta = (last_ms != 0U) ? (now_ms - last_ms) : 0U;`,
147
+ ` if (delta > 250U) delta = 250U; // same clamp as the loop() tick injection`,
148
+ ` last_ms = now_ms;`,
149
+ ` ui_tick(static_cast<uint16_t>(delta));`,
150
+ `#endif`,
151
+ `}`,
129
152
  `// CUTTLEFISH_WIFI_CORE_END`,
130
153
  ``,
131
154
  `// ── connect / disconnect (net_mgmt — conn_mgr monitor supplies L4 events) ─`,
@@ -180,8 +203,9 @@ export function wifiInitLines(): string[] {
180
203
  `static void __tc_wifi_connect(const char* ssid, const char* password, int32_t timeout_ms) {`,
181
204
  ` __tc_wifi_connect_start(ssid, password);`,
182
205
  ` // Block until the L4 handler signals connectivity or the deadline passes.`,
206
+ ` // 20ms slices let __tc_wifi_wait_slice pump the UI between polls.`,
183
207
  ` int32_t waited = 0;`,
184
- ` while (!__tc_wifi.connected && waited < timeout_ms) { k_msleep(100); waited += 100; }`,
208
+ ` while (!__tc_wifi.connected && waited < timeout_ms) { __tc_wifi_wait_slice(20); waited += 20; }`,
185
209
  ` printk("tc-wifi: connect %s after %dms\\n", __tc_wifi.connected ? "ok" : "timeout", waited);`,
186
210
  `}`,
187
211
  `// CUTTLEFISH_WIFI_CONNECT_BLOCKING_END`,
@@ -245,7 +269,7 @@ export function wifiInitLines(): string[] {
245
269
  ``,
246
270
  `static void __tc_wifi_scan_blocking(void) {`,
247
271
  ` __tc_wifi_scan_start();`,
248
- ` while (__tc_wifi.scanning) { k_msleep(100); }`,
272
+ ` while (__tc_wifi.scanning) { __tc_wifi_wait_slice(20); }`,
249
273
  `}`,
250
274
  ``,
251
275
  `static const char* __tc_wifi_scan_ssid(int32_t i) {`,
@@ -313,13 +337,13 @@ export function wifiInitLines(): string[] {
313
337
  `static void __tc_wifi_wait_connected(int32_t timeout_ms) {`,
314
338
  ` __tc_wifi_ensure_init();`,
315
339
  ` int32_t waited = 0;`,
316
- ` while (!__tc_wifi.connected && waited < timeout_ms) { k_msleep(100); waited += 100; }`,
340
+ ` while (!__tc_wifi.connected && waited < timeout_ms) { __tc_wifi_wait_slice(20); waited += 20; }`,
317
341
  `}`,
318
- ``,
342
+
319
343
  `static void __tc_wifi_wait_disconnected(void) {`,
320
344
  ` __tc_wifi_ensure_init();`,
321
345
  ` // No timeout in the HAL surface — block until the L4 handler clears the flag.`,
322
- ` while (__tc_wifi.connected) { k_msleep(100); }`,
346
+ ` while (__tc_wifi.connected) { __tc_wifi_wait_slice(20); }`,
323
347
  `}`,
324
348
  ``,
325
349
  `// ── AP mode (NET_REQUEST_WIFI_AP_ENABLE / DISABLE) ──────────────────────`,
package/src/strategy.ts CHANGED
@@ -42,6 +42,54 @@ import { entryHasUI } from '@typecad/cuttlefish/ui-hook';
42
42
  import { chipForTarget, setActiveChip, getActiveChip } from './chips/index.js';
43
43
  import { resolveChipFromBoard } from './chips/resolve.js';
44
44
  import { emitGpioDevDispatcher } from './chips/controllers.js';
45
+ import type { ZephyrChipDescriptor } from './chips/types.js';
46
+
47
+ /**
48
+ * Deep-walk the program IR and collect the HAL pin numbers the program
49
+ * actually touches for a peripheral family ('adc' | 'pwm') — the same walk
50
+ * profileDiagnostics does. Emit paths gate per-channel state on these sets
51
+ * so nothing unused reaches the single generated TU (-Wunused-function
52
+ * hygiene: every emitted function/variable is referenced). `undefined`
53
+ * (no program — probe paths) means "no information": callers emit every
54
+ * descriptor channel, preserving probe behavior.
55
+ *
56
+ * `pwm` also covers tone.* — the tone lowering drives the descriptor's
57
+ * first PWM spec regardless of pin, so any tone op marks it used.
58
+ */
59
+ function collectUsedPins(
60
+ program: ProgramIR | undefined,
61
+ kind: 'adc' | 'pwm',
62
+ chip?: ZephyrChipDescriptor,
63
+ ): Set<number> | undefined {
64
+ if (!program) return undefined;
65
+ const pins = new Set<number>();
66
+ let usesTone = false;
67
+ const visit = (node: unknown): void => {
68
+ if (!node || typeof node !== 'object') return;
69
+ const n = node as Record<string, unknown>;
70
+ const op = n.operation;
71
+ if (op && typeof op === 'object') {
72
+ const o = op as Record<string, unknown>;
73
+ const name = o.operation;
74
+ const pin = o.pin;
75
+ if (typeof name === 'string' && typeof pin === 'number') {
76
+ if (kind === 'adc' && (name === 'adc.read' || name === 'adc.read_voltage')) pins.add(pin);
77
+ if (kind === 'pwm' && name.startsWith('pwm.')) pins.add(pin);
78
+ }
79
+ if (kind === 'pwm' && typeof name === 'string' && name.startsWith('tone.')) usesTone = true;
80
+ }
81
+ for (const v of Object.values(n)) {
82
+ if (Array.isArray(v)) { for (const item of v) visit(item); }
83
+ else if (v && typeof v === 'object') visit(v);
84
+ }
85
+ };
86
+ visit(program);
87
+ if (kind === 'pwm' && usesTone) {
88
+ const first = chip?.pwm?.specs[0];
89
+ if (first) pins.add(first.pin);
90
+ }
91
+ return pins;
92
+ }
45
93
  import { lowerHalOp } from './lowering/index.js';
46
94
  import { buildZephyrWorkerBacking } from './lowering/worker-backing.js';
47
95
  import { adcInitLines } from './lowering/adc.js';
@@ -425,8 +473,8 @@ export class ZephyrStrategy implements PlatformStrategy {
425
473
  if (uses('usesUart') && chip.uart) {
426
474
  for (let i = 0; i < chip.uart.controllers.length; i++) guardBody.push(...uartInitLines(chip, i));
427
475
  }
428
- if (uses('usesADC') && chip.adc) guardBody.push(...adcInitLines(chip));
429
- if (uses('usesPWM') && chip.pwm) guardBody.push(...pwmInitLines(chip));
476
+ if (uses('usesADC') && chip.adc) guardBody.push(...adcInitLines(chip, collectUsedPins(program, 'adc')));
477
+ if (uses('usesPWM') && chip.pwm) guardBody.push(...pwmInitLines(chip, collectUsedPins(program, 'pwm', chip)));
430
478
  if (uses('usesDAC') && chip.dac) guardBody.push(...dacInitLines(chip));
431
479
  if (uses('usesHwtimer') && chip.hwtimer) guardBody.push(...hwtimerInitLines(chip));
432
480
  if (uses('usesInterrupts')) guardBody.push(...interruptInitLines(chip));
@@ -577,14 +625,14 @@ export class ZephyrStrategy implements PlatformStrategy {
577
625
  if (this.needsGpioReadShim(program, ctx)) {
578
626
  lines.push(...emitGpioDevDispatcher(chip));
579
627
  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)); }',
628
+ '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
629
  );
582
630
  }
583
631
  // __tc_gpio_write / __tc_delay_us are only referenced via @typecad/safety
584
632
  // lowering, so they stay gated on it.
585
633
  if (program && programUsesSafety(program)) {
586
634
  lines.push(
587
- 'inline void __tc_gpio_write(uint32_t pin, uint32_t value) { gpio_pin_set_raw(__tc_gpio_dev(pin), pin, value); }',
635
+ '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
636
  '#ifndef __TC_DELAY_US_DEFINED',
589
637
  '#define __TC_DELAY_US_DEFINED',
590
638
  'inline void __tc_delay_us(uint32_t us) { k_busy_wait(us); }',
@@ -477,7 +477,7 @@ export function writeDebugConfig(o: DebugConfigOptions): void {
477
477
  * the CLI resolves output.outDir against the ENTRY's directory (cli.ts), so
478
478
  * the emitted app root — and therefore the ELF, build dir, and .cuttlefish/
479
479
  * debug artifacts — always lands at `src/out`. Keep in sync with
480
- * generateProjectConfig in @typecad/cuttlefish create/init-templates.ts.
480
+ * generateProjectConfig in @typecad/cuttlefish create/templates.ts.
481
481
  */
482
482
  const STARTER_SKETCH_REL = 'src/out';
483
483