@typecad/framework-zephyr 1.0.0-alpha.12 → 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 (67) 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/display/index.d.ts +1 -1
  8. package/dist/display/index.js +1 -1
  9. package/dist/display/profiles.d.ts +8 -0
  10. package/dist/display/profiles.js +19 -0
  11. package/dist/display/ui-adapter.d.ts +4 -0
  12. package/dist/display/ui-adapter.js +46 -0
  13. package/dist/dt-config/kconfig.d.ts +11 -0
  14. package/dist/dt-config/kconfig.js +1 -1
  15. package/dist/dt-config/overlay.d.ts +8 -1
  16. package/dist/dt-config/overlay.js +104 -1
  17. package/dist/framework.manifest.d.ts +20 -30
  18. package/dist/framework.manifest.js +12 -3
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.js +5 -0
  21. package/dist/lowering/adc.d.ts +7 -4
  22. package/dist/lowering/adc.js +25 -11
  23. package/dist/lowering/gpio.js +15 -8
  24. package/dist/lowering/mqtt.js +9 -1
  25. package/dist/lowering/pulse.js +7 -7
  26. package/dist/lowering/pwm.d.ts +21 -3
  27. package/dist/lowering/pwm.js +28 -4
  28. package/dist/lowering/spi.js +2 -2
  29. package/dist/lowering/tone.js +3 -2
  30. package/dist/lowering/wifi.js +28 -5
  31. package/dist/strategy.d.ts +20 -0
  32. package/dist/strategy.js +295 -119
  33. package/dist/toolchain/debug-config.d.ts +43 -2
  34. package/dist/toolchain/debug-config.js +129 -17
  35. package/dist/toolchain/index.d.ts +14 -1
  36. package/dist/toolchain/index.js +146 -20
  37. package/dist/toolchain/scaffold.d.ts +9 -0
  38. package/dist/toolchain/scaffold.js +84 -19
  39. package/dist/toolchain/west-discover.d.ts +4 -1
  40. package/dist/toolchain/west-discover.js +2 -0
  41. package/dist/toolchain/west-spawn.js +17 -5
  42. package/package.json +5 -5
  43. package/src/chips/controllers.ts +61 -12
  44. package/src/chips/resolve.ts +32 -5
  45. package/src/chips/types.ts +63 -12
  46. package/src/chips/xiao-ble.ts +82 -70
  47. package/src/display/index.ts +1 -1
  48. package/src/display/profiles.ts +23 -0
  49. package/src/display/ui-adapter.ts +51 -0
  50. package/src/dt-config/kconfig.ts +12 -1
  51. package/src/dt-config/overlay.ts +123 -0
  52. package/src/framework.manifest.ts +12 -3
  53. package/src/index.ts +6 -0
  54. package/src/lowering/adc.ts +28 -12
  55. package/src/lowering/gpio.ts +15 -8
  56. package/src/lowering/mqtt.ts +9 -1
  57. package/src/lowering/pulse.ts +7 -7
  58. package/src/lowering/pwm.ts +29 -4
  59. package/src/lowering/spi.ts +2 -2
  60. package/src/lowering/tone.ts +3 -3
  61. package/src/lowering/wifi.ts +29 -5
  62. package/src/strategy.ts +320 -123
  63. package/src/toolchain/debug-config.ts +137 -14
  64. package/src/toolchain/index.ts +645 -513
  65. package/src/toolchain/scaffold.ts +81 -17
  66. package/src/toolchain/west-discover.ts +321 -316
  67. package/src/toolchain/west-spawn.ts +17 -5
package/README.md CHANGED
@@ -35,7 +35,19 @@ fix (`pip install west`, set `ZEPHYR_BASE`, or activate the venv).
35
35
  symbols for the lowered peripherals). Both are regenerated idempotently —
36
36
  only rewritten when their content changes, so Ninja's incremental build is
37
37
  preserved.
38
- - The MVP target is the **Seeed Studio XIAO nRF52840** (`xiao_ble` board).
38
+ - Supported board targets: **Seeed Studio XIAO nRF52840** (`xiao_ble`),
39
+ **ESP32 DevKit** (`esp32_devkitc`), **ESP32-S3 DevKit** (`esp32s3_devkitc`),
40
+ **ESP32-C3 DevKitM** (`esp32c3_devkitm/esp32c3`), **ESP32-C6 DevKitC**
41
+ (`esp32c6_devkitc/esp32c6/hpcore` — the qualified form is required, the
42
+ board also ships an lpcore variant), **Raspberry Pi Pico** (`rpi_pico`),
43
+ **Pico 2** (`rpi_pico2/rp2350a/m33`), and **WeAct Black Pill V2.0**
44
+ (`blackpill_f411ce/stm32f411xe` — the first STM32 target; flashed over USB
45
+ via the ROM DFU bootloader). The rpi_pico, esp32c3, esp32c6, and blackpill
46
+ boards resolve their chip data from the board packages' `zephyr` field (see
47
+ `resolveChipFromBoard`) rather than the hardcoded chip registry; the
48
+ qualified Pico 2 target selects the Cortex-M33 cpucluster (Zephyr 4.3+
49
+ rejects the bare `rpi_pico2` because the board also ships a Hazard3 variant
50
+ with no default).
39
51
 
40
52
  ## Installation
41
53
 
@@ -1,4 +1,4 @@
1
- import type { ZephyrChipDescriptor } from './types.js';
1
+ import type { ZephyrChipDescriptor, ZephyrGpioController } from './types.js';
2
2
  /**
3
3
  * Resolve the devicetree nodelabel of the GPIO controller that owns `pin`.
4
4
  *
@@ -9,13 +9,33 @@ import type { ZephyrChipDescriptor } from './types.js';
9
9
  */
10
10
  export declare function controllerNodelabelForPin(chip: ZephyrChipDescriptor, pin: number): string;
11
11
  /**
12
- * Emit the C++ source for a runtime pin GPIO-device dispatcher.
12
+ * The owning controller range for a HAL pin, if the chip declares a split.
13
+ */
14
+ export declare function controllerRangeForPin(chip: ZephyrChipDescriptor, pin: number): ZephyrGpioController | undefined;
15
+ /**
16
+ * The PORT-RELATIVE pin index for the raw gpio_pin_*_raw() API — the Zephyr
17
+ * raw calls address the index WITHIN the controller, not the global HAL pin
18
+ * number. For split SoCs each controller's `minPin` is its base (STM32:
19
+ * gpiob minPin 16, so PB12 = pin 28 → raw 12); single-controller SoCs have
20
+ * no offset. Emitting the global number against a port driver would address
21
+ * a nonexistent port bit (STM32 gpiob is 0–15) and fail at runtime.
22
+ */
23
+ export declare function controllerRawPinForPin(chip: ZephyrChipDescriptor, pin: number): number;
24
+ /**
25
+ * Emit the C++ source for runtime pin → GPIO-device / port-relative-index
26
+ * dispatchers.
27
+ *
28
+ * Returns lines defining:
29
+ * - `static inline const struct device* __tc_gpio_dev(uint32_t pin)` — the
30
+ * owning controller's device, resolved via DEVICE_DT_GET(DT_NODELABEL(...))
31
+ * at compile time (the macro is evaluated per branch, so it is always
32
+ * statically valid); only `pin` is runtime.
33
+ * - `static inline gpio_pin_t __tc_gpio_pin(uint32_t pin)` — the
34
+ * port-relative index for gpio_pin_*_raw() (see controllerRawPinForPin).
35
+ * The runtime shim paths (UI pin-watch, safety voters) take a runtime pin,
36
+ * so they cannot bake the offset in at emit time.
13
37
  *
14
- * Returns lines defining `static inline const struct device* __tc_gpio_dev(uint32_t pin)`.
15
- * Each branch resolves its controller via `DEVICE_DT_GET(DT_NODELABEL(...))` at
16
- * compile time (the macro is evaluated per branch, so it is always statically
17
- * valid); only `pin` is runtime. For a single-controller SoC this collapses to
18
- * a one-liner returning that controller, so the existing XIAO nRF52840 behavior
19
- * is byte-for-byte unchanged.
38
+ * For a single-controller SoC both collapse to one-liners, so the existing
39
+ * XIAO nRF52840 behavior is byte-for-byte unchanged.
20
40
  */
21
41
  export declare function emitGpioDevDispatcher(chip: ZephyrChipDescriptor): string[];
@@ -27,23 +27,47 @@
27
27
  * for out-of-range pins (e.g. the manifest probe's synthetic pin 0).
28
28
  */
29
29
  export function controllerNodelabelForPin(chip, pin) {
30
+ const hit = controllerRangeForPin(chip, pin);
31
+ return hit ? hit.nodelabel : chip.gpioController;
32
+ }
33
+ /**
34
+ * The owning controller range for a HAL pin, if the chip declares a split.
35
+ */
36
+ export function controllerRangeForPin(chip, pin) {
30
37
  const ranges = chip.gpioControllers;
31
38
  if (ranges && ranges.length > 0) {
32
- const hit = ranges.find((r) => pin >= r.minPin && pin <= r.maxPin);
33
- if (hit)
34
- return hit.nodelabel;
39
+ return ranges.find((r) => pin >= r.minPin && pin <= r.maxPin);
35
40
  }
36
- return chip.gpioController;
41
+ return undefined;
37
42
  }
38
43
  /**
39
- * Emit the C++ source for a runtime pin GPIO-device dispatcher.
44
+ * The PORT-RELATIVE pin index for the raw gpio_pin_*_raw() API the Zephyr
45
+ * raw calls address the index WITHIN the controller, not the global HAL pin
46
+ * number. For split SoCs each controller's `minPin` is its base (STM32:
47
+ * gpiob minPin 16, so PB12 = pin 28 → raw 12); single-controller SoCs have
48
+ * no offset. Emitting the global number against a port driver would address
49
+ * a nonexistent port bit (STM32 gpiob is 0–15) and fail at runtime.
50
+ */
51
+ export function controllerRawPinForPin(chip, pin) {
52
+ const hit = controllerRangeForPin(chip, pin);
53
+ return pin - (hit?.minPin ?? 0);
54
+ }
55
+ /**
56
+ * Emit the C++ source for runtime pin → GPIO-device / port-relative-index
57
+ * dispatchers.
58
+ *
59
+ * Returns lines defining:
60
+ * - `static inline const struct device* __tc_gpio_dev(uint32_t pin)` — the
61
+ * owning controller's device, resolved via DEVICE_DT_GET(DT_NODELABEL(...))
62
+ * at compile time (the macro is evaluated per branch, so it is always
63
+ * statically valid); only `pin` is runtime.
64
+ * - `static inline gpio_pin_t __tc_gpio_pin(uint32_t pin)` — the
65
+ * port-relative index for gpio_pin_*_raw() (see controllerRawPinForPin).
66
+ * The runtime shim paths (UI pin-watch, safety voters) take a runtime pin,
67
+ * so they cannot bake the offset in at emit time.
40
68
  *
41
- * Returns lines defining `static inline const struct device* __tc_gpio_dev(uint32_t pin)`.
42
- * Each branch resolves its controller via `DEVICE_DT_GET(DT_NODELABEL(...))` at
43
- * compile time (the macro is evaluated per branch, so it is always statically
44
- * valid); only `pin` is runtime. For a single-controller SoC this collapses to
45
- * a one-liner returning that controller, so the existing XIAO nRF52840 behavior
46
- * is byte-for-byte unchanged.
69
+ * For a single-controller SoC both collapse to one-liners, so the existing
70
+ * XIAO nRF52840 behavior is byte-for-byte unchanged.
47
71
  */
48
72
  export function emitGpioDevDispatcher(chip) {
49
73
  const ranges = chip.gpioControllers;
@@ -53,14 +77,27 @@ export function emitGpioDevDispatcher(chip) {
53
77
  ` (void)pin;`,
54
78
  ` return DEVICE_DT_GET(DT_NODELABEL(${chip.gpioController}));`,
55
79
  '}',
80
+ 'static inline gpio_pin_t __tc_gpio_pin(uint32_t pin) {',
81
+ ` return (gpio_pin_t)pin;`,
82
+ '}',
56
83
  ];
57
84
  }
58
85
  const lines = [
59
86
  'static inline const struct device* __tc_gpio_dev(uint32_t pin) {',
60
87
  ];
88
+ const pinLines = [
89
+ 'static inline gpio_pin_t __tc_gpio_pin(uint32_t pin) {',
90
+ ];
61
91
  for (const r of ranges) {
62
92
  lines.push(` if (pin >= ${r.minPin} && pin <= ${r.maxPin}) { return DEVICE_DT_GET(DT_NODELABEL(${r.nodelabel})); }`);
93
+ if (r.minPin === 0) {
94
+ pinLines.push(` if (pin >= ${r.minPin} && pin <= ${r.maxPin}) { return (gpio_pin_t)pin; }`);
95
+ }
96
+ else {
97
+ pinLines.push(` if (pin >= ${r.minPin} && pin <= ${r.maxPin}) { return (gpio_pin_t)(pin - ${r.minPin}); }`);
98
+ }
63
99
  }
64
100
  lines.push(` return DEVICE_DT_GET(DT_NODELABEL(${chip.gpioController}));`, '}');
65
- return lines;
101
+ pinLines.push(` return (gpio_pin_t)pin;`, '}');
102
+ return [...lines, ...pinLines];
66
103
  }
@@ -81,18 +81,35 @@ export function resolveChipFromBoard(bc) {
81
81
  const pwmSpecs = collectIndexed(bc, 'zephyr.pwm.specs', (m, i) => {
82
82
  const pin = m.get(`zephyr.pwm.specs.${i}.pin`);
83
83
  const dtSpec = m.get(`zephyr.pwm.specs.${i}.dtSpec`);
84
- if (pin != null && dtSpec)
85
- return { pin, dtSpec };
86
- return null;
84
+ const controller = m.get(`zephyr.pwm.specs.${i}.controller`);
85
+ const channel = m.get(`zephyr.pwm.specs.${i}.channel`);
86
+ const periodNs = m.get(`zephyr.pwm.specs.${i}.periodNs`);
87
+ const polarity = m.get(`zephyr.pwm.specs.${i}.polarity`);
88
+ // Board-shipped alias form (dtSpec) or synthesized form (controller +
89
+ // channel → overlay-generated alias) — at least one, else drop the entry.
90
+ if (pin == null || !(dtSpec || (controller && channel != null)))
91
+ return null;
92
+ return {
93
+ pin,
94
+ ...(dtSpec ? { dtSpec } : {}),
95
+ ...(controller ? { controller } : {}),
96
+ ...(channel != null ? { channel } : {}),
97
+ ...(periodNs != null ? { periodNs } : {}),
98
+ ...(polarity ? { polarity } : {}),
99
+ };
87
100
  });
88
101
  const adcNodeLabel = bc.get('zephyr.adc.nodeLabel');
89
102
  const adcResolution = bc.get('zephyr.adc.resolution');
90
103
  const adcVref = bc.get('zephyr.adc.vrefMv');
104
+ const adcGain = bc.get('zephyr.adc.gain');
105
+ const adcReference = bc.get('zephyr.adc.reference');
91
106
  const adcChannels = collectIndexed(bc, 'zephyr.adc.channels', (m, i) => {
92
107
  const pin = m.get(`zephyr.adc.channels.${i}.pin`);
93
108
  const channel = m.get(`zephyr.adc.channels.${i}.channel`);
94
- if (pin != null && channel != null)
95
- return { pin, channel };
109
+ const pinctrl = m.get(`zephyr.adc.channels.${i}.pinctrl`);
110
+ if (pin != null && channel != null) {
111
+ return { pin, channel, ...(pinctrl ? { pinctrl } : {}) };
112
+ }
96
113
  return null;
97
114
  });
98
115
  const wdtNodeLabel = bc.get('zephyr.wdt.nodeLabel');
@@ -113,7 +130,16 @@ export function resolveChipFromBoard(bc) {
113
130
  ...(uartControllers.length > 0 ? { uart: { controllers: uartControllers } } : {}),
114
131
  ...(pwmSpecs.length > 0 ? { pwm: { specs: pwmSpecs } } : {}),
115
132
  ...(adcNodeLabel || adcResolution != null || adcVref != null || adcChannels.length > 0
116
- ? { adc: { nodeLabel: adcNodeLabel ?? 'adc', resolution: adcResolution ?? 12, vrefMv: adcVref ?? 3000, channels: adcChannels } }
133
+ ? {
134
+ adc: {
135
+ nodeLabel: adcNodeLabel ?? 'adc',
136
+ resolution: adcResolution ?? 12,
137
+ vrefMv: adcVref ?? 3000,
138
+ channels: adcChannels,
139
+ ...(adcGain ? { gain: adcGain } : {}),
140
+ ...(adcReference ? { reference: adcReference } : {}),
141
+ },
142
+ }
117
143
  : {}),
118
144
  ...(wdtNodeLabel ? { wdt: { nodeLabel: wdtNodeLabel } } : {}),
119
145
  ...(wifiSupported ? { wifi: { supported: true } } : {}),
@@ -47,14 +47,37 @@ export interface ZephyrBusController {
47
47
  /**
48
48
  * A PWM channel described as a devicetree spec.
49
49
  *
50
- * Emitted as `struct pwm_dt_spec __tc_pwm<N> = PWM_DT_SPEC_GET(DT_ALIAS(<dtSpec>))`.
51
- * `pwm_set_pulse_dt(&__tc_pwm<N>, pulse_ns)` honors the spec's period/polarity.
50
+ * Two forms, mutually exclusive:
51
+ * - **Board-shipped alias:** `dtSpec` names a DT alias the board's own DTS
52
+ * already defines (e.g. the XIAO's `pwm-led0`). Emitted as
53
+ * `PWM_DT_SPEC_GET(DT_ALIAS(<dtSpec>))`.
54
+ * - **Synthesized** (controller + channel): the board DTS enables a PWM
55
+ * controller node (e.g. `pwm4`) but defines no alias for it. The overlay
56
+ * generator synthesizes a `pwm-leds` consumer node + a `tc-pwm<pin>` alias
57
+ * in `<board>.overlay`; the lowering emits
58
+ * `PWM_DT_SPEC_GET(DT_ALIAS(tc-pwm<pin>))`. Both sides derive the alias
59
+ * name from the pin, so they always agree.
60
+ *
61
+ * `pwm_set_pulse_dt(&spec, pulse_ns)` honors the spec's period/polarity.
52
62
  */
53
63
  export interface ZephyrPwmSpec {
54
64
  /** GPIO number (matches the HAL op `pin` field). */
55
65
  readonly pin: number;
56
- /** Devicetree alias, e.g. 'pwm-led0'. */
57
- readonly dtSpec: string;
66
+ /** Board-shipped DT alias, e.g. 'pwm-led0'. Omit when using the
67
+ * synthesized form (controller + channel). */
68
+ readonly dtSpec?: string;
69
+ /** Synthesized form: PWM controller DT nodelabel, e.g. 'pwm4' (the STM32
70
+ * timer's pwm child node). The overlay's pwm-leds node consumes it. */
71
+ readonly controller?: string;
72
+ /** Synthesized form: channel index within the controller (1-based timer
73
+ * channel, matching the `pwms` binding's channel cell). */
74
+ readonly channel?: number;
75
+ /** Synthesized form: period in nanoseconds, baked into the DT spec. The
76
+ * lowering scales duty against `spec.period`. Default 20 000 000 (20 ms /
77
+ * 50 Hz — the servo convention; harmless for LED dimming). */
78
+ readonly periodNs?: number;
79
+ /** Synthesized form: PWM polarity flag. Default PWM_POLARITY_NORMAL. */
80
+ readonly polarity?: string;
58
81
  }
59
82
  /**
60
83
  * A GPIO pin usable as an interrupt source.
@@ -69,7 +92,7 @@ export interface ZephyrInterruptPin {
69
92
  readonly dtSpec: string;
70
93
  }
71
94
  /**
72
- * An ADC channel: which SAADC input a given HAL pin maps to.
95
+ * An ADC channel: which ADC input a given HAL pin maps to.
73
96
  *
74
97
  * The XIAO nRF52840 has no pre-declared ADC channel nodes in devicetree, so the
75
98
  * lowering emits `adc_channel_setup` against `DEVICE_DT_GET(DT_NODELABEL(adc))`
@@ -78,8 +101,16 @@ export interface ZephyrInterruptPin {
78
101
  export interface ZephyrAdcChannel {
79
102
  /** GPIO number (matches the HAL op `pin` field). */
80
103
  readonly pin: number;
81
- /** SAADC channel index (AIN0–AIN7). */
104
+ /** ADC channel index (nRF SAADC AIN0–AIN7; STM32 ADC1_IN0–IN9). */
82
105
  readonly channel: number;
106
+ /**
107
+ * Pinctrl node label that muxes this pin to analog mode, e.g.
108
+ * 'adc1_in0_pa0' (STM32). When present, the overlay generator rewrites the
109
+ * ADC node's pinctrl-0 to the channels the program actually reads — SoCs
110
+ * like STM32 leave the pad in GPIO mode otherwise and reads float.
111
+ * Omit on SoCs whose ADC needs no pad muxing (nRF SAADC, RP2040).
112
+ */
113
+ readonly pinctrl?: string;
83
114
  }
84
115
  /**
85
116
  * A DAC channel: which DAC output a given HAL pin maps to. The lowering emits
@@ -113,11 +144,18 @@ export interface ZephyrChipDescriptor {
113
144
  readonly gpioController: string;
114
145
  /**
115
146
  * Per-range GPIO controllers for SoCs that split GPIO across multiple
116
- * devicetree nodes (ESP32-S3: `gpio0` 0–31, `gpio1` 32–48). When present, the
117
- * lowering routes a HAL pin to its owning controller at runtime via the
118
- * emitted `__tc_gpio_dev(pin)` dispatcher; `gpioController` is the fallback.
119
- * Omit on single-controller SoCs (nRF52840, RP2040, …) every pin is on the
120
- * one controller described by `gpioController`.
147
+ * devicetree nodes (ESP32-S3: `gpio0` 0–31, `gpio1` 32–48; STM32: one
148
+ * controller per port `gpioa` 0–15, `gpiob` 16–31, `gpioc` 32–47).
149
+ * When present, the lowering routes a HAL pin to its owning controller at
150
+ * runtime via the emitted `__tc_gpio_dev(pin)` dispatcher; `gpioController`
151
+ * is the fallback. Omit on single-controller SoCs (RP2040, …) — every pin
152
+ * is on the one controller described by `gpioController`.
153
+ *
154
+ * NUMBERING RULE (load-bearing): `minPin` must equal the controller's port
155
+ * base so the port-relative raw index is `pin - minPin` (STM32 PB12 = pin
156
+ * 28 → raw 12 — the Zephyr raw API addresses the index WITHIN the
157
+ * controller). Number pins by port blocks and never contiguously across
158
+ * unbonded pins.
121
159
  */
122
160
  readonly gpioControllers?: readonly ZephyrGpioController[];
123
161
  /** GPIO pins with devicetree specs (LEDs, buttons, board-defined pins). */
@@ -142,7 +180,7 @@ export interface ZephyrChipDescriptor {
142
180
  readonly pwm?: {
143
181
  readonly specs: readonly ZephyrPwmSpec[];
144
182
  };
145
- /** ADC: the SAADC node label + the pin→channel map. */
183
+ /** ADC: the ADC device node label + the pin→channel map. */
146
184
  readonly adc?: {
147
185
  readonly nodeLabel: string;
148
186
  readonly channels: readonly ZephyrAdcChannel[];
@@ -150,6 +188,19 @@ export interface ZephyrChipDescriptor {
150
188
  readonly vrefMv: number;
151
189
  /** ADC resolution in bits. */
152
190
  readonly resolution: number;
191
+ /**
192
+ * Zephyr `enum adc_gain` macro for the channel setup, e.g.
193
+ * 'ADC_GAIN_1_4' (nRF SAADC default) or 'ADC_GAIN_1' (STM32 driver
194
+ * requires exactly this). Defaults to 'ADC_GAIN_1_4'.
195
+ */
196
+ readonly gain?: string;
197
+ /**
198
+ * Zephyr `enum adc_reference` macro, e.g. 'ADC_REF_INTERNAL'. Defaults to
199
+ * 'ADC_REF_INTERNAL' — on nRF that is the 0.6 V internal ref measured
200
+ * through the gain divider; the STM32 driver ALSO requires
201
+ * ADC_REF_INTERNAL (Zephyr maps it to the VREF+ pad) with vrefMv = VDDA.
202
+ */
203
+ readonly reference?: string;
153
204
  };
154
205
  /**
155
206
  * DAC: the DAC device node label + the pin→channel map. Present only on chips
@@ -8,6 +8,18 @@ export const XIAO_BLE = {
8
8
  id: 'xiao_ble',
9
9
  soc: 'nrf52840',
10
10
  gpioController: 'gpio0',
11
+ // GPIO is split across two devicetree controllers: gpio0 (P0.00–P0.31)
12
+ // and gpio1 (P1.00–P1.15, HAL pins 32–47). Declaring the split makes the
13
+ // raw path emit the port-relative index against gpio1 (P1.11 = raw 11),
14
+ // which NRF_GPIO_PIN_MAP(1, 11) resolves to absolute pin 43 — the same
15
+ // physical pin the old single-controller form reached only by accident:
16
+ // gpio0 + global 43 also maps to 43 (MAP(0, 43) = 43), but that form
17
+ // trips the generic layer's port_pin_mask __ASSERT ("Unsupported pin",
18
+ // gpio0's mask covers 0–31) on any assert-enabled build.
19
+ gpioControllers: [
20
+ { nodelabel: 'gpio0', minPin: 0, maxPin: 31 },
21
+ { nodelabel: 'gpio1', minPin: 32, maxPin: 47 },
22
+ ],
11
23
  gpio: {
12
24
  dtSpecs: [
13
25
  // Onboard RGB LEDs — active-low (GPIO_ACTIVE_LOW in xiao_ble_common.dtsi).
@@ -2,7 +2,7 @@ import type { DisplayHALOp } from '@typecad/cuttlefish/api/shared';
2
2
  import { type ZephyrDisplayProfile } from './profiles.js';
3
3
  export { zephyrUiDisplayAdapter, zephyrDisplayAdapterGenerator } from './ui-adapter.js';
4
4
  export { zephyrTouchAdapter } from './touch-adapter.js';
5
- export { ZEPHYR_DISPLAY_PROFILES, DEFAULT_ZEPHYR_DISPLAY_PROFILE } from './profiles.js';
5
+ export { ZEPHYR_DISPLAY_PROFILES, DEFAULT_ZEPHYR_DISPLAY_PROFILE, BUILT_IN_PROFILES } from './profiles.js';
6
6
  export type { ZephyrDisplayProfile } from './profiles.js';
7
7
  export interface DisplayState {
8
8
  initialized: boolean;
@@ -15,7 +15,7 @@ import { DEFAULT_ZEPHYR_DISPLAY_PROFILE } from './profiles.js';
15
15
  // and consumers can reach them from the package barrel.
16
16
  export { zephyrUiDisplayAdapter, zephyrDisplayAdapterGenerator } from './ui-adapter.js';
17
17
  export { zephyrTouchAdapter } from './touch-adapter.js';
18
- export { ZEPHYR_DISPLAY_PROFILES, DEFAULT_ZEPHYR_DISPLAY_PROFILE } from './profiles.js';
18
+ export { ZEPHYR_DISPLAY_PROFILES, DEFAULT_ZEPHYR_DISPLAY_PROFILE, BUILT_IN_PROFILES } from './profiles.js';
19
19
  /** Fresh display state (used by the strategy per-build). */
20
20
  export function newDisplayState() {
21
21
  return { initialized: false, profile: DEFAULT_ZEPHYR_DISPLAY_PROFILE };
@@ -48,3 +48,11 @@ export declare function panelControllerFor(profile: Pick<ZephyrDisplayProfile, '
48
48
  export declare const ZEPHYR_DISPLAY_PROFILES: Record<string, ZephyrDisplayProfile>;
49
49
  /** The default profile used when resolveDisplayOp is probed without a display.init. */
50
50
  export declare const DEFAULT_ZEPHYR_DISPLAY_PROFILE: ZephyrDisplayProfile;
51
+ /**
52
+ * The Zephyr profiles mapped to the shared DisplayProfile shape — the single
53
+ * mapping, so no consumer needs to know the DT-binding descriptor layout.
54
+ * The strategy's getProfileRegistry() and the preview's profile-registry
55
+ * loader both consume this (the same role `BUILT_IN_PROFILES` plays in
56
+ * framework-arduino's displays modules).
57
+ */
58
+ export declare const BUILT_IN_PROFILES: Record<string, import('@typecad/cuttlefish/api/shared').DisplayProfile>;
@@ -69,3 +69,22 @@ export const ZEPHYR_DISPLAY_PROFILES = {
69
69
  };
70
70
  /** The default profile used when resolveDisplayOp is probed without a display.init. */
71
71
  export const DEFAULT_ZEPHYR_DISPLAY_PROFILE = ZEPHYR_DISPLAY_PROFILES['ili9341-zephyr'];
72
+ /**
73
+ * The Zephyr profiles mapped to the shared DisplayProfile shape — the single
74
+ * mapping, so no consumer needs to know the DT-binding descriptor layout.
75
+ * The strategy's getProfileRegistry() and the preview's profile-registry
76
+ * loader both consume this (the same role `BUILT_IN_PROFILES` plays in
77
+ * framework-arduino's displays modules).
78
+ */
79
+ export const BUILT_IN_PROFILES = Object.fromEntries(Object.entries(ZEPHYR_DISPLAY_PROFILES).map(([name, p]) => [
80
+ name,
81
+ {
82
+ driver: p.driver,
83
+ width: p.width,
84
+ height: p.height,
85
+ nativeWidth: p.nativeWidth,
86
+ nativeHeight: p.nativeHeight,
87
+ colorFormat: p.colorFormat,
88
+ rotation: p.rotation ?? 1,
89
+ },
90
+ ]));
@@ -10,6 +10,10 @@ export interface ZephyrDisplayReadbackOptions {
10
10
  scanlineSync?: boolean;
11
11
  /** MISO/SDO GPIO; readback is disabled when it is not explicitly wired. */
12
12
  miso?: number;
13
+ /** Tearing-effect GPIO (panel TE output). When set, panel updates wait for
14
+ * the TE frame pulse instead of GET_SCANLINE readback — no MISO required.
15
+ * The overlay adds te-gpios to the display DT node from this. */
16
+ tearingEffectPin?: number;
13
17
  }
14
18
  export declare function zephyrUiDisplayAdapter(profile: ZephyrDisplayProfile, readback?: ZephyrDisplayReadbackOptions): DisplayAdapterCode;
15
19
  /**
@@ -55,6 +55,9 @@ export function zephyrUiDisplayAdapter(profile, readback = {}) {
55
55
  // react badly to GSCAN reads. Both an explicit opt-in and an explicit MISO
56
56
  // pin are required before emitting an active synchronization path.
57
57
  const scanlineSync = readback.scanlineSync === true && readback.miso !== undefined;
58
+ // TE (hardware tearing-effect) sync: strictly opt-in via a configured GPIO.
59
+ // Preferred over GET_SCANLINE when wired — no readback traffic, no MISO.
60
+ const tePin = typeof readback.tearingEffectPin === 'number' ? readback.tearingEffectPin : undefined;
58
61
  const includes = [
59
62
  `// --- Zephyr UI display adapter (${profile.driver}) ---`,
60
63
  `// Native CuttlefishGFX path: do NOT #define CuttlefishCanvas16 so the`,
@@ -118,6 +121,22 @@ export function zephyrUiDisplayAdapter(profile, readback = {}) {
118
121
  `// controller-specific validation are required; otherwise the display stays`,
119
122
  `// on the existing retained/composited path with no extra SPI reads.`,
120
123
  `static const bool __tc_pnl_scanline_sync = ${scanlineSync ? 'true' : 'false'};`,
124
+ `// Tearing-effect (TE) hardware sync: the panel pulses its TE line once`,
125
+ `// per frame. When te-gpios is present on the display DT node, panel`,
126
+ `// updates arm on the TE edge — tear-free writes with no MISO readback.`,
127
+ `#if DT_NODE_HAS_PROP(DT_NODELABEL(${dtLabel}), te_gpios)`,
128
+ `#define __TC_TE_SYNC 1`,
129
+ `static const struct gpio_dt_spec __tc_te =`,
130
+ ` GPIO_DT_SPEC_GET(DT_NODELABEL(${dtLabel}), te_gpios);`,
131
+ `static struct gpio_callback __tc_te_cb;`,
132
+ `static volatile uint32_t __tc_te_count = 0;`,
133
+ `static void __tc_te_isr(const struct device* port, struct gpio_callback* cb, uint32_t pins) {`,
134
+ ` (void)port; (void)cb; (void)pins;`,
135
+ ` __tc_te_count++;`,
136
+ `}`,
137
+ `#else`,
138
+ `#define __TC_TE_SYNC 0`,
139
+ `#endif`,
121
140
  `// Stashed address window from the last setAddrWindow call. The runtime`,
122
141
  `// calls setAddrWindow + writePixels as a matched pair, so we stash the rect`,
123
142
  `// here and consume it in writePixels.`,
@@ -134,6 +153,17 @@ export function zephyrUiDisplayAdapter(profile, readback = {}) {
134
153
  // with DT_HAS_ALIAS (the safe primitive for an alias that may be absent —
135
154
  // DT_NODE_HAS_STATUS(DT_ALIAS(...)) is version-dependent when the alias is
136
155
  // missing and can fail the build).
156
+ // TE pin: input + rising-edge interrupt (the ST7796 TE pulse), then tell
157
+ // the controller to drive the line (TEON 0x35, mode 1 = vertical sync only).
158
+ const teInit = tePin !== undefined ? `#if __TC_TE_SYNC
159
+ if (device_is_ready(__tc_te.port)) {
160
+ gpio_pin_configure_dt(&__tc_te, GPIO_INPUT);
161
+ gpio_init_callback(&__tc_te_cb, __tc_te_isr, BIT(__tc_te.pin));
162
+ (void)gpio_add_callback(__tc_te.port, &__tc_te_cb);
163
+ (void)gpio_pin_interrupt_configure_dt(&__tc_te, GPIO_INT_EDGE_RISING);
164
+ __tc_pnl_cmd1(0x35, 0x01); // TEON: TE output = vsync pulse
165
+ }
166
+ #endif` : '';
137
167
  const blInit = backlightAlias
138
168
  ? `#if DT_HAS_ALIAS(${backlightAlias})\n const struct gpio_dt_spec __bl = GPIO_DT_SPEC_GET(DT_ALIAS(${backlightAlias}), gpios);\n if (device_is_ready(__bl.port)) { gpio_pin_configure_dt(&__bl, GPIO_OUTPUT_ACTIVE); }\n#endif`
139
169
  : '';
@@ -452,6 +482,19 @@ static uint16_t __tc_pnl_read_scanline(void) {
452
482
  // have no safe post-rectangle interval, so they retain the normal single-burst
453
483
  // behavior; the caller's framebuffer still prevents intermediate software frames.
454
484
  static void __tc_pnl_wait_for_safe_rect(int16_t y, int16_t rh) {
485
+ #if __TC_TE_SYNC
486
+ // TE variant: arm on the next frame pulse, then start the burst — the write
487
+ // chases the scan beam from the top of the rect. Bounded so a stuck TE line
488
+ // can never hang the UI loop.
489
+ if (rh < 8 || y < 0) return;
490
+ uint32_t __was = __tc_te_count;
491
+ uint32_t __deadline = k_uptime_get_32() + 25U;
492
+ while (__tc_te_count == __was) {
493
+ if (static_cast<int32_t>(k_uptime_get_32() - __deadline) >= 0) break;
494
+ k_msleep(0);
495
+ }
496
+ return;
497
+ #endif
455
498
  if (!__tc_pnl_scanline_sync || rh < 8 || y < 0) return;
456
499
  int16_t __last = static_cast<int16_t>(y + rh - 1);
457
500
  if (__last >= static_cast<int16_t>(${h} - 2)) return;
@@ -551,8 +594,10 @@ const CuttlefishPanelOps __tc_display_ops = {
551
594
  CuttlefishGFX __tc_display(&__tc_display_ops, nullptr);
552
595
  ${initBlock}
553
596
  // ── display_init (called from setup) ────────────────────────────────────
597
+
554
598
  static inline void display_init() {
555
599
  printk("TC_DISPLAY: device ready\\n");
600
+ ${teInit}
556
601
  ${blInit}
557
602
  gpio_pin_configure_dt(&__tc_pnl_cs, GPIO_OUTPUT);
558
603
  gpio_pin_configure_dt(&__tc_pnl_dc, GPIO_OUTPUT);
@@ -699,5 +744,6 @@ export const zephyrDisplayAdapterGenerator = (display) => {
699
744
  return zephyrUiDisplayAdapter(profile, {
700
745
  scanlineSync: display.scanlineSync,
701
746
  miso: display.spiPins?.miso,
747
+ tearingEffectPin: display.tearingEffectPin,
702
748
  });
703
749
  };
@@ -25,6 +25,17 @@ export interface KconfigUsage {
25
25
  /** PSRAM type ('opi' | 'quad') when the target board has PSRAM. Emits the
26
26
  * CONFIG_SPIRAM symbols so the ESP heap serves PSRAM for canvas allocations. */
27
27
  psram?: 'opi' | 'quad';
28
+ /** HAL pin numbers the program reads with adc.* — scanned from the emitted
29
+ * `__tc_adc<N>_setup()` calls at compile time. Only the overlay generator
30
+ * consumes this (to rewrite the ADC node's pinctrl-0 to the used channels
31
+ * on SoCs that need pad muxing, e.g. STM32); prj.conf ignores it. */
32
+ adcReadPins?: readonly number[];
33
+ /** HAL pin numbers the program drives with pwm.* — scanned from the
34
+ * emitted `__tc_pwm_*` spec references at compile time. Only the overlay
35
+ * generator consumes this (synthesized pwm-leds consumers + aliases are
36
+ * emitted per used pin, so the DT carries no dead channels); prj.conf
37
+ * ignores it. */
38
+ pwmUsedPins?: readonly number[];
28
39
  }
29
40
  /**
30
41
  * Resolve the Kconfig symbol→value map for a prj.conf. Returns a Map preserving
@@ -97,7 +97,7 @@ export function resolveKconfigFragments(usage, debug) {
97
97
  // (without it, Kconfig silently forces them all to n).
98
98
  m.set('CONFIG_NETWORKING', 'y');
99
99
  m.set('CONFIG_WIFI', 'y');
100
- m.set('CONFIG_WIFI_ESP32', 'y'); // ESP32-specific driver (sole WiFi target)
100
+ m.set('CONFIG_WIFI_ESP32', 'y'); // family-wide ESP32 driver (esp32/s3/c3/c6)
101
101
  m.set('CONFIG_NET_L2_ETHERNET', 'y');
102
102
  m.set('CONFIG_NET_IPV4', 'y');
103
103
  m.set('CONFIG_NET_UDP', 'y'); // transitive dep of NET_DHCPV4
@@ -10,6 +10,9 @@ export interface DisplayWiring {
10
10
  cs?: number;
11
11
  dc?: number;
12
12
  rst?: number;
13
+ /** Tearing-effect (TE) GPIO from display.tearingEffectPin — emitted as
14
+ * te-gpios on the display DT node. Opt-in; most boards don't wire TE. */
15
+ tearingEffectPin?: number;
13
16
  spiFrequency?: number;
14
17
  /** SPI bus pins. When present, the overlay remuxes the SPI controller's
15
18
  * pinctrl to these pins (the board defaults rarely match a breakout's
@@ -49,4 +52,8 @@ export interface TouchWiring {
49
52
  * panels need a few hundred 12-bit counts; default 400. */
50
53
  minPressure?: number;
51
54
  }
52
- export declare function generateOverlay(chip: ZephyrChipDescriptor, usage: KconfigUsage, display: ZephyrDisplayProfile | undefined, wiring?: DisplayWiring, touch?: TouchWiring): string;
55
+ export interface OverlayDiagnostic {
56
+ severity: "warning" | "error";
57
+ message: string;
58
+ }
59
+ export declare function generateOverlay(chip: ZephyrChipDescriptor, usage: KconfigUsage, display: ZephyrDisplayProfile | undefined, wiring?: DisplayWiring, touch?: TouchWiring, diagnostics?: OverlayDiagnostic[]): string;