@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
@@ -18,19 +18,30 @@ export function adcChannelForPin(chip, pin) {
18
18
  }
19
19
  /**
20
20
  * Emit the per-channel ADC setup state. One block per channel in the chip
21
- * descriptor, each guarded by a `static bool __tc_adc<N>_ready` so the first
22
- * read configures it and subsequent reads skip. Called from shimLines when
23
- * the program uses ADC.
21
+ * descriptor that the PROGRAM ACTUALLY READS (`usedPins`) an unread
22
+ * channel's `static` setup function would trip -Wunused-function in the
23
+ * single generated TU. When `usedPins` is omitted (probe paths with no
24
+ * program), every descriptor channel is emitted. Each block is guarded by a
25
+ * `static bool __tc_adc<N>_ready` so the first read configures it and
26
+ * subsequent reads skip. Called from shimLines when the program uses ADC.
24
27
  */
25
- export function adcInitLines(chip) {
28
+ export function adcInitLines(chip, usedPins) {
26
29
  const dev = `DEVICE_DT_GET(DT_NODELABEL(${chip.adc?.nodeLabel ?? 'adc'}))`;
27
30
  const res = chip.adc?.resolution ?? 12;
28
31
  const vref = chip.adc?.vrefMv ?? 3000;
32
+ // Gain/reference are SoC-specific: the nRF SAADC scheme (gain 1/4 against
33
+ // the 0.6V internal ref, vref-mv 3000 = VDD) is the default; the STM32
34
+ // driver requires exactly ADC_GAIN_1 + ADC_REF_INTERNAL (Zephyr maps
35
+ // "internal" to the VREF+ pad) with vref-mv = VDDA. The descriptor carries
36
+ // the SoC's pair so the emitted channel setup validates in the driver.
37
+ const gain = chip.adc?.gain ?? 'ADC_GAIN_1_4';
38
+ const reference = chip.adc?.reference ?? 'ADC_REF_INTERNAL';
39
+ const channels = (chip.adc?.channels ?? []).filter((c) => !usedPins || usedPins.has(c.pin));
29
40
  const lines = ['// CUTTLEFISH_ADC_BEGIN'];
30
41
  lines.push(`static const struct device* __tc_adc_dev = ${dev};`);
31
- for (const c of chip.adc?.channels ?? []) {
42
+ for (const c of channels) {
32
43
  const n = c.channel;
33
- lines.push(`static bool __tc_adc${n}_ready = false;`, `static void __tc_adc${n}_setup(void) {`, ` if (__tc_adc${n}_ready) return;`, ` const struct adc_channel_cfg cfg = {`, ` .gain = ADC_GAIN_1_4,`, ` .reference = ADC_REF_INTERNAL,`, ` .acquisition_time = ADC_ACQ_TIME_DEFAULT,`, ` .channel_id = ${n},`, ` .differential = 0,`, ` };`, ` adc_channel_setup(__tc_adc_dev, &cfg);`, ` __tc_adc${n}_ready = true;`, `}`);
44
+ lines.push(`static bool __tc_adc${n}_ready = false;`, `static void __tc_adc${n}_setup(void) {`, ` if (__tc_adc${n}_ready) return;`, ` const struct adc_channel_cfg cfg = {`, ` .gain = ${gain},`, ` .reference = ${reference},`, ` .acquisition_time = ADC_ACQ_TIME_DEFAULT,`, ` .channel_id = ${n},`, ` .differential = 0,`, ` };`, ` adc_channel_setup(__tc_adc_dev, &cfg);`, ` __tc_adc${n}_ready = true;`, `}`);
34
45
  }
35
46
  lines.push(`#define __TC_ADC_VREF_MV ${vref}`);
36
47
  lines.push(`#define __TC_ADC_RESOLUTION ${res}`);
@@ -45,6 +56,8 @@ export function lowerAdc(op, chip) {
45
56
  const o = op;
46
57
  const res = chip.adc?.resolution ?? 12;
47
58
  const vref = chip.adc?.vrefMv ?? 3000;
59
+ const gain = chip.adc?.gain ?? 'ADC_GAIN_1_4';
60
+ const reference = chip.adc?.reference ?? 'ADC_REF_INTERNAL';
48
61
  switch (op.operation) {
49
62
  case 'adc.read': {
50
63
  const ch = adcChannelForPin(chip, o.pin);
@@ -55,10 +68,11 @@ export function lowerAdc(op, chip) {
55
68
  }
56
69
  case 'adc.read_voltage': {
57
70
  const ch = adcChannelForPin(chip, o.pin);
58
- // Read raw, convert to millivolts via adc_raw_to_millivolts (gain 1/4,
59
- // internal ref). Returns mV as int.
71
+ // Read raw, convert to millivolts via adc_raw_to_millivolts with the
72
+ // descriptor's gain (raw_to_millivolts divides out the gain the channel
73
+ // was set up with). Returns mV as int.
60
74
  return {
61
- 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; })`,
75
+ 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; })`,
62
76
  };
63
77
  }
64
78
  case 'adc.get_resolution':
@@ -66,9 +80,9 @@ export function lowerAdc(op, chip) {
66
80
  case 'adc.set_reference':
67
81
  // Zephyr configures the reference at channel-setup time; runtime switching
68
82
  // would require re-setup. Record the intent as a no-op statement.
69
- return { code: `/* adc.set_reference(${o.reference}): configured at channel setup (ADC_REF_INTERNAL) */` };
83
+ return { code: `/* adc.set_reference(${o.reference}): configured at channel setup (${reference}) */` };
70
84
  case 'adc.get_reference':
71
- return { expression: `0 /* DEFAULT (ADC_REF_INTERNAL) */` };
85
+ return { expression: `0 /* DEFAULT (${reference}) */` };
72
86
  default:
73
87
  throw new Error(`framework-zephyr does not yet support HAL op \`${op.operation}\`. ` +
74
88
  `Open an issue or use rawCpp() to emit it manually.`);
@@ -14,7 +14,7 @@
14
14
  // The manifest validator's probe (which sends {operation, pin:0} with no
15
15
  // port) hits this path, so it must return a lowered result, not undefined.
16
16
  // ---------------------------------------------------------------------------
17
- import { controllerNodelabelForPin } from '../chips/controllers.js';
17
+ import { controllerNodelabelForPin, controllerRawPinForPin } from '../chips/controllers.js';
18
18
  /** The C identifier emitted for a pin's gpio_dt_spec variable. */
19
19
  export function dtSpecVarName(dtSpec) {
20
20
  return `__tc_dt_${dtSpec}`;
@@ -99,19 +99,22 @@ function lowerGpioRaw(op, chip) {
99
99
  const o = op;
100
100
  const pin = o.pin;
101
101
  // Resolve the owning controller by pin range (ESP32-S3 splits GPIO across
102
- // gpio0/gpio1). For single-controller SoCs this is just chip.gpioController.
102
+ // gpio0/gpio1; STM32 across gpioa/gpiob/gpioc). For single-controller SoCs
103
+ // this is just chip.gpioController. The raw API takes the PORT-RELATIVE
104
+ // index (STM32 gpiob is 0-15), not the global HAL pin number.
103
105
  const controller = `DEVICE_DT_GET(DT_NODELABEL(${controllerNodelabelForPin(chip, pin)}))`;
106
+ const rawPin = controllerRawPinForPin(chip, pin);
104
107
  switch (op.operation) {
105
108
  case 'gpio.set_mode': {
106
- return { code: `gpio_pin_configure(${controller}, ${pin}, ${flagsForMode(o.mode)});` };
109
+ return { code: `gpio_pin_configure(${controller}, ${rawPin}, ${flagsForMode(o.mode)});` };
107
110
  }
108
111
  case 'gpio.write': {
109
112
  const v = o.value;
110
113
  const rhs = typeof v === 'string' ? `((${v}) ? 1 : 0)` : v ? 1 : 0;
111
- return { code: `gpio_pin_set_raw(${controller}, ${pin}, ${rhs});` };
114
+ return { code: `gpio_pin_set_raw(${controller}, ${rawPin}, ${rhs});` };
112
115
  }
113
116
  case 'gpio.read':
114
- return { expression: `gpio_pin_get_raw(${controller}, ${pin})` };
117
+ return { expression: `gpio_pin_get_raw(${controller}, ${rawPin})` };
115
118
  case 'gpio.toggle':
116
119
  // Native atomic toggle — never read-modify-write. gpio_pin_get_raw on
117
120
  // a direction-only output reads the input latch, which is undefined on
@@ -119,7 +122,7 @@ function lowerGpioRaw(op, chip) {
119
122
  // gpio_pin_toggle is the driver-level atomic toggle, and for pins
120
123
  // configured without GPIO_ACTIVE_LOW the logical level equals the
121
124
  // physical one, so it matches the get_raw/set_raw used elsewhere.
122
- return { code: `gpio_pin_toggle(${controller}, ${pin});` };
125
+ return { code: `gpio_pin_toggle(${controller}, ${rawPin});` };
123
126
  default:
124
127
  throw new Error(`framework-zephyr does not yet support HAL op \`${op.operation}\`. ` +
125
128
  `Open an issue or use rawCpp() to emit it manually.`);
@@ -290,7 +290,15 @@ export function mqttInitLines() {
290
290
  ` (void)k_thread_create(&__tc_mqtt.poll_thread, __tc_mqtt_stack,`,
291
291
  ` K_THREAD_STACK_SIZEOF(__tc_mqtt_stack),`,
292
292
  ` __tc_mqtt_poll_thread, nullptr, nullptr, nullptr,`,
293
- ` 5, 0, K_NO_WAIT);`,
293
+ ` 5, 0, K_FOREVER);`,
294
+ `#ifdef CONFIG_SMP`,
295
+ ` // On SMP targets park the poll thread on the app core so the network`,
296
+ ` // stack never competes with the main/UI thread for core 0. k_thread_cpu_pin`,
297
+ ` // is SMP-only; the pin happens before k_thread_start (which is why the`,
298
+ ` // thread is created K_FOREVER). Compiles away on !SMP builds.`,
299
+ ` (void)k_thread_cpu_pin(&__tc_mqtt.poll_thread, 1);`,
300
+ `#endif`,
301
+ ` k_thread_start(&__tc_mqtt.poll_thread);`,
294
302
  `}`,
295
303
  ``,
296
304
  `static inline void __tc_mqtt_set_on_message(__tc_mqtt_msg_cb_t fn) {`,
@@ -7,7 +7,7 @@
7
7
  // implementations (no hardware pulse capture on nRF for the HAL surface); they
8
8
  // are correct but not high-precision.
9
9
  // ---------------------------------------------------------------------------
10
- import { controllerNodelabelForPin } from '../chips/controllers.js';
10
+ import { controllerNodelabelForPin, controllerRawPinForPin } from '../chips/controllers.js';
11
11
  /** `DEVICE_DT_GET(DT_NODELABEL(<owning-controller>))` for a HAL pin. */
12
12
  function devForPin(chip, pin) {
13
13
  return `DEVICE_DT_GET(DT_NODELABEL(${controllerNodelabelForPin(chip, pin)}))`;
@@ -23,9 +23,9 @@ export function lowerPulseOrShift(op, chip) {
23
23
  // for the start edge is bounded by the timeout; the measurement of the
24
24
  // pulse itself is intentionally unbounded (that IS the pulse length).
25
25
  // Returns -1 (0) if the start edge never arrives within the timeout.
26
- const pin = o.pin;
26
+ const pin = controllerRawPinForPin(chip, o.pin);
27
27
  const want = o.value;
28
- const dev = devForPin(chip, pin);
28
+ const dev = devForPin(chip, o.pin);
29
29
  const timeout = o.timeout ?? 1_000_000; // default 1s in us
30
30
  return {
31
31
  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; })`,
@@ -36,9 +36,9 @@ export function lowerPulseOrShift(op, chip) {
36
36
  // with NO timeout (the comment claimed "no overflow concern" but the
37
37
  // real risk was hanging the thread on a stuck pin). Apply the same
38
38
  // timeout-bounded start-edge wait as pulse.in (bug Q5).
39
- const pin = o.pin;
39
+ const pin = controllerRawPinForPin(chip, o.pin);
40
40
  const want = o.value;
41
- const dev = devForPin(chip, pin);
41
+ const dev = devForPin(chip, o.pin);
42
42
  const timeout = o.timeout ?? 3_000_000; // default 3s in us (long pulses)
43
43
  return {
44
44
  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; })`,
@@ -57,7 +57,7 @@ export function lowerPulseOrShift(op, chip) {
57
57
  const test = msbFirst ? '(__i >= 0)' : '(__i < 8)';
58
58
  const step = msbFirst ? '__i--' : '__i++';
59
59
  return {
60
- 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); }`,
60
+ 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); }`,
61
61
  };
62
62
  }
63
63
  case 'shift.in': {
@@ -72,7 +72,7 @@ export function lowerPulseOrShift(op, chip) {
72
72
  const step = msbFirst ? '__i--' : '__i++';
73
73
  const accum = msbFirst ? '__v = (__v << 1)' : '__v |= (bit << __i)';
74
74
  return {
75
- 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; })`,
75
+ 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; })`,
76
76
  };
77
77
  }
78
78
  default:
@@ -1,10 +1,28 @@
1
1
  import type { HALOpIR } from '@typecad/cuttlefish/api/shared';
2
- import type { ZephyrChipDescriptor } from '../chips/types.js';
2
+ import type { ZephyrChipDescriptor, ZephyrPwmSpec } from '../chips/types.js';
3
+ /**
4
+ * The DT alias a PWM spec is addressed by. Board-shipped specs carry their
5
+ * alias in `dtSpec`; synthesized specs (controller + channel) get a
6
+ * `tc-pwm<pin>` alias that the overlay generator creates in
7
+ * <board>.overlay — both sides derive the name from the pin so they agree.
8
+ */
9
+ export declare function pwmDtAlias(spec: ZephyrPwmSpec): string;
10
+ /**
11
+ * The C macro token for a spec's alias. Zephyr's devicetree macros replace
12
+ * dashes in alias names with underscores (`pwm-led0` in DTS is
13
+ * DT_ALIAS(pwm_led0) in C) — the dashed spelling is a subtraction
14
+ * expression and fails to compile (caught by the blackpill E2E west build).
15
+ */
16
+ export declare function pwmDtAliasToken(spec: ZephyrPwmSpec): string;
3
17
  /**
4
18
  * Emit the per-channel PWM spec declarations. One per spec in the chip
5
- * descriptor. Called from shimLines when the program uses PWM.
19
+ * descriptor that the PROGRAM ACTUALLY DRIVES (`usedPins`) a spec for an
20
+ * untouched pin is unused code in the emitted TU (and would need a dead DT
21
+ * alias in the overlay). When `usedPins` is omitted (probe paths with no
22
+ * program), every spec is emitted. Called from shimLines when the program
23
+ * uses PWM.
6
24
  */
7
- export declare function pwmInitLines(chip: ZephyrChipDescriptor): string[];
25
+ export declare function pwmInitLines(chip: ZephyrChipDescriptor, usedPins?: ReadonlySet<number>): string[];
8
26
  /**
9
27
  * Resolve a HAL pwm.* op to Zephyr C++.
10
28
  * Returns `{ code }` for statement ops, `{ expression }` for value-returning ops.
@@ -6,22 +6,46 @@
6
6
  // `pwm_set_pulse_dt` / `pwm_set_dt`. Duty is scaled from the Arduino-style
7
7
  // 0–255 (or 0–1023) range to nanoseconds against the spec's period.
8
8
  // ---------------------------------------------------------------------------
9
+ /**
10
+ * The DT alias a PWM spec is addressed by. Board-shipped specs carry their
11
+ * alias in `dtSpec`; synthesized specs (controller + channel) get a
12
+ * `tc-pwm<pin>` alias that the overlay generator creates in
13
+ * <board>.overlay — both sides derive the name from the pin so they agree.
14
+ */
15
+ export function pwmDtAlias(spec) {
16
+ return spec.dtSpec ?? `tc-pwm${spec.pin}`;
17
+ }
18
+ /**
19
+ * The C macro token for a spec's alias. Zephyr's devicetree macros replace
20
+ * dashes in alias names with underscores (`pwm-led0` in DTS is
21
+ * DT_ALIAS(pwm_led0) in C) — the dashed spelling is a subtraction
22
+ * expression and fails to compile (caught by the blackpill E2E west build).
23
+ */
24
+ export function pwmDtAliasToken(spec) {
25
+ return pwmDtAlias(spec).replace(/-/g, '_');
26
+ }
9
27
  /** Look up a PWM spec by HAL pin number. */
10
28
  function findPwmSpec(chip, pin) {
11
29
  return chip.pwm?.specs.find((s) => s.pin === pin);
12
30
  }
13
31
  /** The C variable name emitted for a PWM channel's spec. */
14
32
  function pwmVarName(spec) {
15
- return `__tc_pwm_${spec.dtSpec.replace(/-/g, '_')}`;
33
+ return `__tc_pwm_${pwmDtAliasToken(spec)}`;
16
34
  }
17
35
  /**
18
36
  * Emit the per-channel PWM spec declarations. One per spec in the chip
19
- * descriptor. Called from shimLines when the program uses PWM.
37
+ * descriptor that the PROGRAM ACTUALLY DRIVES (`usedPins`) a spec for an
38
+ * untouched pin is unused code in the emitted TU (and would need a dead DT
39
+ * alias in the overlay). When `usedPins` is omitted (probe paths with no
40
+ * program), every spec is emitted. Called from shimLines when the program
41
+ * uses PWM.
20
42
  */
21
- export function pwmInitLines(chip) {
43
+ export function pwmInitLines(chip, usedPins) {
22
44
  const lines = ['// CUTTLEFISH_PWM_BEGIN'];
23
45
  for (const spec of chip.pwm?.specs ?? []) {
24
- lines.push(`static const struct pwm_dt_spec ${pwmVarName(spec)} = PWM_DT_SPEC_GET(DT_ALIAS(${spec.dtSpec}));`);
46
+ if (usedPins && !usedPins.has(spec.pin))
47
+ continue;
48
+ lines.push(`static const struct pwm_dt_spec ${pwmVarName(spec)} = PWM_DT_SPEC_GET(DT_ALIAS(${pwmDtAliasToken(spec)}));`);
25
49
  }
26
50
  lines.push('// CUTTLEFISH_PWM_END');
27
51
  return lines;
@@ -12,7 +12,7 @@
12
12
  // DEVICE_DT_GET(DT_NODELABEL(spi2)) and call spi_transceive directly.
13
13
  // ---------------------------------------------------------------------------
14
14
  import { parseControllerIndex } from './util.js';
15
- import { controllerNodelabelForPin } from '../chips/controllers.js';
15
+ import { controllerNodelabelForPin, controllerRawPinForPin } from '../chips/controllers.js';
16
16
  /** The C variable prefix for a controller's state. */
17
17
  function prefix(idx) {
18
18
  return `__tc_spi${idx}`;
@@ -103,7 +103,7 @@ export function lowerSpi(op, chip) {
103
103
  const val = op.operation === 'spi.cs_low' ? 0 : 1;
104
104
  const gpioController = controllerNodelabelForPin(chip, o.pin);
105
105
  return {
106
- code: `gpio_pin_set_raw(DEVICE_DT_GET(DT_NODELABEL(${gpioController})), ${o.pin}, ${val});`,
106
+ code: `gpio_pin_set_raw(DEVICE_DT_GET(DT_NODELABEL(${gpioController})), ${controllerRawPinForPin(chip, o.pin)}, ${val});`,
107
107
  };
108
108
  }
109
109
  default:
@@ -11,6 +11,7 @@
11
11
  // matching the synchronous Arduino tone() semantics. For non-blocking tone,
12
12
  // a workqueue would be needed (deferred).
13
13
  // ---------------------------------------------------------------------------
14
+ import { pwmDtAliasToken } from './pwm.js';
14
15
  /**
15
16
  * Resolve a HAL tone.* op to Zephyr C++ via PWM.
16
17
  * Returns `{ code }` for statement ops, `{ expression }` for value-returning ops.
@@ -26,7 +27,7 @@ export function lowerTone(op, chip) {
26
27
  if (!spec) {
27
28
  return { code: `/* tone.play(${o.frequency}): no PWM spec in chip descriptor */` };
28
29
  }
29
- const v = `__tc_pwm_${spec.dtSpec.replace(/-/g, '_')}`;
30
+ const v = `__tc_pwm_${pwmDtAliasToken(spec)}`;
30
31
  const freq = o.frequency;
31
32
  const duration = o.duration;
32
33
  const setTone = `uint32_t __period = (${freq} > 0) ? (1000000000ULL / static_cast<uint64_t>(${freq})) : 0; pwm_set_dt(&${v}, __period, __period / 2);`;
@@ -40,7 +41,7 @@ export function lowerTone(op, chip) {
40
41
  const spec = chip.pwm?.specs[0];
41
42
  if (!spec)
42
43
  return { code: `/* tone.stop: no PWM spec */` };
43
- const v = `__tc_pwm_${spec.dtSpec.replace(/-/g, '_')}`;
44
+ const v = `__tc_pwm_${pwmDtAliasToken(spec)}`;
44
45
  return { code: `pwm_set_pulse_dt(&${v}, 0);` };
45
46
  }
46
47
  default:
@@ -122,6 +122,29 @@ export function wifiInitLines() {
122
122
  ` }`,
123
123
  ` __tc_wifi.inited = true;`,
124
124
  `}`,
125
+ ``,
126
+ `// ── blocking-wait pump ───────────────────────────────────────────────────`,
127
+ `// Blocking wifi waits hold the main thread, which suspends the loop()-driven`,
128
+ `// ui_tick — a 15s association would freeze the display for its whole`,
129
+ `// duration. Each wait slice sleeps 20ms and, in the entry TU of a UI build`,
130
+ `// (the only TU carrying the ui_tick definition), pumps one frame so`,
131
+ `// animations and touch stay live during the wait. Constraint: do NOT call`,
132
+ `// blocking wifi waits from inside UI event handlers — that re-enters`,
133
+ `// ui_tick mid-tick; use wifi.connect_start + wifi.is_connected there.`,
134
+ `#ifdef CUTTLEFISH_ENTRY_UI_TU`,
135
+ `static void ui_tick(uint16_t deltaMs); // defined by the UI runtime header`,
136
+ `#endif`,
137
+ `static void __tc_wifi_wait_slice(uint32_t slice_ms) {`,
138
+ ` k_msleep(slice_ms);`,
139
+ `#ifdef CUTTLEFISH_ENTRY_UI_TU`,
140
+ ` static uint32_t last_ms = 0U;`,
141
+ ` uint32_t now_ms = k_uptime_get_32();`,
142
+ ` uint32_t delta = (last_ms != 0U) ? (now_ms - last_ms) : 0U;`,
143
+ ` if (delta > 250U) delta = 250U; // same clamp as the loop() tick injection`,
144
+ ` last_ms = now_ms;`,
145
+ ` ui_tick(static_cast<uint16_t>(delta));`,
146
+ `#endif`,
147
+ `}`,
125
148
  `// CUTTLEFISH_WIFI_CORE_END`,
126
149
  ``,
127
150
  `// ── connect / disconnect (net_mgmt — conn_mgr monitor supplies L4 events) ─`,
@@ -176,8 +199,9 @@ export function wifiInitLines() {
176
199
  `static void __tc_wifi_connect(const char* ssid, const char* password, int32_t timeout_ms) {`,
177
200
  ` __tc_wifi_connect_start(ssid, password);`,
178
201
  ` // Block until the L4 handler signals connectivity or the deadline passes.`,
202
+ ` // 20ms slices let __tc_wifi_wait_slice pump the UI between polls.`,
179
203
  ` int32_t waited = 0;`,
180
- ` while (!__tc_wifi.connected && waited < timeout_ms) { k_msleep(100); waited += 100; }`,
204
+ ` while (!__tc_wifi.connected && waited < timeout_ms) { __tc_wifi_wait_slice(20); waited += 20; }`,
181
205
  ` printk("tc-wifi: connect %s after %dms\\n", __tc_wifi.connected ? "ok" : "timeout", waited);`,
182
206
  `}`,
183
207
  `// CUTTLEFISH_WIFI_CONNECT_BLOCKING_END`,
@@ -241,7 +265,7 @@ export function wifiInitLines() {
241
265
  ``,
242
266
  `static void __tc_wifi_scan_blocking(void) {`,
243
267
  ` __tc_wifi_scan_start();`,
244
- ` while (__tc_wifi.scanning) { k_msleep(100); }`,
268
+ ` while (__tc_wifi.scanning) { __tc_wifi_wait_slice(20); }`,
245
269
  `}`,
246
270
  ``,
247
271
  `static const char* __tc_wifi_scan_ssid(int32_t i) {`,
@@ -309,13 +333,12 @@ export function wifiInitLines() {
309
333
  `static void __tc_wifi_wait_connected(int32_t timeout_ms) {`,
310
334
  ` __tc_wifi_ensure_init();`,
311
335
  ` int32_t waited = 0;`,
312
- ` while (!__tc_wifi.connected && waited < timeout_ms) { k_msleep(100); waited += 100; }`,
336
+ ` while (!__tc_wifi.connected && waited < timeout_ms) { __tc_wifi_wait_slice(20); waited += 20; }`,
313
337
  `}`,
314
- ``,
315
338
  `static void __tc_wifi_wait_disconnected(void) {`,
316
339
  ` __tc_wifi_ensure_init();`,
317
340
  ` // No timeout in the HAL surface — block until the L4 handler clears the flag.`,
318
- ` while (__tc_wifi.connected) { k_msleep(100); }`,
341
+ ` while (__tc_wifi.connected) { __tc_wifi_wait_slice(20); }`,
319
342
  `}`,
320
343
  ``,
321
344
  `// ── AP mode (NET_REQUEST_WIFI_AP_ENABLE / DISABLE) ──────────────────────`,
package/dist/strategy.js CHANGED
@@ -21,6 +21,58 @@ import { entryHasUI } from '@typecad/cuttlefish/ui-hook';
21
21
  import { chipForTarget, setActiveChip } from './chips/index.js';
22
22
  import { resolveChipFromBoard } from './chips/resolve.js';
23
23
  import { emitGpioDevDispatcher } from './chips/controllers.js';
24
+ /**
25
+ * Deep-walk the program IR and collect the HAL pin numbers the program
26
+ * actually touches for a peripheral family ('adc' | 'pwm') — the same walk
27
+ * profileDiagnostics does. Emit paths gate per-channel state on these sets
28
+ * so nothing unused reaches the single generated TU (-Wunused-function
29
+ * hygiene: every emitted function/variable is referenced). `undefined`
30
+ * (no program — probe paths) means "no information": callers emit every
31
+ * descriptor channel, preserving probe behavior.
32
+ *
33
+ * `pwm` also covers tone.* — the tone lowering drives the descriptor's
34
+ * first PWM spec regardless of pin, so any tone op marks it used.
35
+ */
36
+ function collectUsedPins(program, kind, chip) {
37
+ if (!program)
38
+ return undefined;
39
+ const pins = new Set();
40
+ let usesTone = false;
41
+ const visit = (node) => {
42
+ if (!node || typeof node !== 'object')
43
+ return;
44
+ const n = node;
45
+ const op = n.operation;
46
+ if (op && typeof op === 'object') {
47
+ const o = op;
48
+ const name = o.operation;
49
+ const pin = o.pin;
50
+ if (typeof name === 'string' && typeof pin === 'number') {
51
+ if (kind === 'adc' && (name === 'adc.read' || name === 'adc.read_voltage'))
52
+ pins.add(pin);
53
+ if (kind === 'pwm' && name.startsWith('pwm.'))
54
+ pins.add(pin);
55
+ }
56
+ if (kind === 'pwm' && typeof name === 'string' && name.startsWith('tone.'))
57
+ usesTone = true;
58
+ }
59
+ for (const v of Object.values(n)) {
60
+ if (Array.isArray(v)) {
61
+ for (const item of v)
62
+ visit(item);
63
+ }
64
+ else if (v && typeof v === 'object')
65
+ visit(v);
66
+ }
67
+ };
68
+ visit(program);
69
+ if (kind === 'pwm' && usesTone) {
70
+ const first = chip?.pwm?.specs[0];
71
+ if (first)
72
+ pins.add(first.pin);
73
+ }
74
+ return pins;
75
+ }
24
76
  import { lowerHalOp } from './lowering/index.js';
25
77
  import { buildZephyrWorkerBacking } from './lowering/worker-backing.js';
26
78
  import { adcInitLines } from './lowering/adc.js';
@@ -407,9 +459,9 @@ export class ZephyrStrategy {
407
459
  guardBody.push(...uartInitLines(chip, i));
408
460
  }
409
461
  if (uses('usesADC') && chip.adc)
410
- guardBody.push(...adcInitLines(chip));
462
+ guardBody.push(...adcInitLines(chip, collectUsedPins(program, 'adc')));
411
463
  if (uses('usesPWM') && chip.pwm)
412
- guardBody.push(...pwmInitLines(chip));
464
+ guardBody.push(...pwmInitLines(chip, collectUsedPins(program, 'pwm', chip)));
413
465
  if (uses('usesDAC') && chip.dac)
414
466
  guardBody.push(...dacInitLines(chip));
415
467
  if (uses('usesHwtimer') && chip.hwtimer)
@@ -521,12 +573,12 @@ export class ZephyrStrategy {
521
573
  // still compile-time-resolved per branch, so it is always statically valid.
522
574
  if (this.needsGpioReadShim(program, ctx)) {
523
575
  lines.push(...emitGpioDevDispatcher(chip));
524
- lines.push('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)); }');
576
+ lines.push('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))); }');
525
577
  }
526
578
  // __tc_gpio_write / __tc_delay_us are only referenced via @typecad/safety
527
579
  // lowering, so they stay gated on it.
528
580
  if (program && programUsesSafety(program)) {
529
- lines.push('inline void __tc_gpio_write(uint32_t pin, uint32_t value) { gpio_pin_set_raw(__tc_gpio_dev(pin), pin, value); }', '#ifndef __TC_DELAY_US_DEFINED', '#define __TC_DELAY_US_DEFINED', 'inline void __tc_delay_us(uint32_t us) { k_busy_wait(us); }', '#endif');
581
+ lines.push('inline void __tc_gpio_write(uint32_t pin, uint32_t value) { gpio_pin_set_raw(__tc_gpio_dev(pin), __tc_gpio_pin(pin), value); }', '#ifndef __TC_DELAY_US_DEFINED', '#define __TC_DELAY_US_DEFINED', 'inline void __tc_delay_us(uint32_t us) { k_busy_wait(us); }', '#endif');
530
582
  }
531
583
  return lines;
532
584
  }
@@ -431,7 +431,7 @@ export function writeDebugConfig(o) {
431
431
  * the CLI resolves output.outDir against the ENTRY's directory (cli.ts), so
432
432
  * the emitted app root — and therefore the ELF, build dir, and .cuttlefish/
433
433
  * debug artifacts — always lands at `src/out`. Keep in sync with
434
- * generateProjectConfig in @typecad/cuttlefish create/init-templates.ts.
434
+ * generateProjectConfig in @typecad/cuttlefish create/templates.ts.
435
435
  */
436
436
  const STARTER_SKETCH_REL = 'src/out';
437
437
  /**
@@ -26,7 +26,7 @@ export declare function projectRootFromOptions(o: ToolchainOptions): string;
26
26
  * Exported (pure) so the runner-selection contract is unit-testable without
27
27
  * spawning west.
28
28
  */
29
- export declare function buildFlashArgs(buildDir: string, board: string, userRunner: string | undefined, port: string | undefined): string[];
29
+ export declare function buildFlashArgs(buildDir: string, board: string, userRunner: string | undefined, port: string | undefined, runnerArgs?: readonly string[]): string[];
30
30
  /**
31
31
  * Classify a `west flash` result as success/failure.
32
32
  *