@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
@@ -1,70 +1,82 @@
1
- // ---------------------------------------------------------------------------
2
- // Seeed Studio XIAO nRF52840 — Zephyr board descriptor
3
- //
4
- // Board target: `xiao_ble` (mainline Zephyr, boards/seeed/xiao_ble).
5
- // The onboard user LED is active-low and exposed as the DT alias `led0`.
6
- // ---------------------------------------------------------------------------
7
-
8
- import type { ZephyrChipDescriptor } from './types.js';
9
-
10
- export const XIAO_BLE: ZephyrChipDescriptor = {
11
- id: 'xiao_ble',
12
- soc: 'nrf52840',
13
- gpioController: 'gpio0',
14
- gpio: {
15
- dtSpecs: [
16
- // Onboard RGB LEDs active-low (GPIO_ACTIVE_LOW in xiao_ble_common.dtsi).
17
- // Verified against the board's `aliases { led0 = &led0; led1 = &led1; led2 = &led2; }`.
18
- { pin: 26, dtSpec: 'led0' }, // Red (P0.26)
19
- { pin: 30, dtSpec: 'led1' }, // Green (P0.30)
20
- { pin: 6, dtSpec: 'led2' }, // Blue (P0.06)
21
- ],
22
- // The XIAO nRF52840 has a user button on P0.04, but mainline Zephyr's
23
- // xiao_ble board (verified against Zephyr 4.3.99) does NOT expose it as a
24
- // DT `sw0` alias there is no gpio-keys node in xiao_ble.dts. Emitting
25
- // GPIO_DT_SPEC_GET(DT_ALIAS(sw0), gpios) therefore fails to compile
26
- // ('DT_N_ALIAS_sw0_... was not declared'). Until a button node + alias is
27
- // added (via an overlay or an upstream board update), interruptPins stays
28
- // empty: attachInterrupt on this board lowers to the no-DT-spec comment
29
- // fallback rather than a hard compile error.
30
- interruptPins: [],
31
- },
32
- // The XIAO connector wiring (from seeed_xiao_connector.dtsi + xiao_ble-pinctrl.dtsi):
33
- // xiao_i2c → i2c1 (SDA P0.04/D4, SCL P0.05/D5)
34
- // xiao_spi → spi2 (SCK P1.13/D8, MOSI P1.15/D10, MISO P1.14/D9)
35
- // xiao_serial → uart0 (TX P1.11/D6, RX P1.12/D7)
36
- // The board DTS leaves i2c0/spi0/spi1 disabled (shared instances).
37
- i2c: {
38
- controllers: [{ nodeLabel: 'i2c1' }],
39
- },
40
- spi: {
41
- controllers: [{ nodeLabel: 'spi2' }],
42
- },
43
- uart: {
44
- controllers: [{ nodeLabel: 'uart0' }],
45
- },
46
- pwm: {
47
- // pwm-led0 drives the board PWM LED (PWM_OUT0 on P0.17, inverted).
48
- specs: [{ pin: 17, dtSpec: 'pwm-led0' }],
49
- },
50
- adc: {
51
- // SAADC node is `adc`; no pre-declared channels. XIAO D0–D3 = AIN0–AIN3
52
- // (P0.02/P0.03/P0.28/P0.29). Internal VREF ~0.6V with VDD/4 gain ⇒ 3000mV.
53
- nodeLabel: 'adc',
54
- resolution: 12,
55
- vrefMv: 3000,
56
- channels: [
57
- { pin: 2, channel: 0 }, // P0.02 / D0 / AIN0
58
- { pin: 3, channel: 1 }, // P0.03 / D1 / AIN1
59
- { pin: 28, channel: 2 }, // P0.28 / D2 / AIN2
60
- { pin: 29, channel: 3 }, // P0.29 / D3 / AIN3
61
- ],
62
- },
63
- wdt: { nodeLabel: 'wdt0' },
64
- // Hardware timer: nRF RTC1 is the free counter (RTC0 is kernel-owned by the
65
- // softdevice/clock driver). The hwtimer lowering drives it as a Zephyr
66
- // counter device (counter_start/stop + a top-value alarm for set_frequency).
67
- // Verified against the nRF52840 SoC dtsi (rtc0/rtc1 nodes). The kernel uses
68
- // RTC0 for the system tick; RTC1 is available for application use.
69
- hwtimer: { controllers: [{ nodeLabel: 'rtc1' }] },
70
- };
1
+ // ---------------------------------------------------------------------------
2
+ // Seeed Studio XIAO nRF52840 — Zephyr board descriptor
3
+ //
4
+ // Board target: `xiao_ble` (mainline Zephyr, boards/seeed/xiao_ble).
5
+ // The onboard user LED is active-low and exposed as the DT alias `led0`.
6
+ // ---------------------------------------------------------------------------
7
+
8
+ import type { ZephyrChipDescriptor } from './types.js';
9
+
10
+ export const XIAO_BLE: ZephyrChipDescriptor = {
11
+ id: 'xiao_ble',
12
+ soc: 'nrf52840',
13
+ gpioController: 'gpio0',
14
+ // GPIO is split across two devicetree controllers: gpio0 (P0.00–P0.31)
15
+ // and gpio1 (P1.00–P1.15, HAL pins 32–47). Declaring the split makes the
16
+ // raw path emit the port-relative index against gpio1 (P1.11 = raw 11),
17
+ // which NRF_GPIO_PIN_MAP(1, 11) resolves to absolute pin 43 the same
18
+ // physical pin the old single-controller form reached only by accident:
19
+ // gpio0 + global 43 also maps to 43 (MAP(0, 43) = 43), but that form
20
+ // trips the generic layer's port_pin_mask __ASSERT ("Unsupported pin",
21
+ // gpio0's mask covers 0–31) on any assert-enabled build.
22
+ gpioControllers: [
23
+ { nodelabel: 'gpio0', minPin: 0, maxPin: 31 },
24
+ { nodelabel: 'gpio1', minPin: 32, maxPin: 47 },
25
+ ],
26
+ gpio: {
27
+ dtSpecs: [
28
+ // Onboard RGB LEDs active-low (GPIO_ACTIVE_LOW in xiao_ble_common.dtsi).
29
+ // Verified against the board's `aliases { led0 = &led0; led1 = &led1; led2 = &led2; }`.
30
+ { pin: 26, dtSpec: 'led0' }, // Red (P0.26)
31
+ { pin: 30, dtSpec: 'led1' }, // Green (P0.30)
32
+ { pin: 6, dtSpec: 'led2' }, // Blue (P0.06)
33
+ ],
34
+ // The XIAO nRF52840 has a user button on P0.04, but mainline Zephyr's
35
+ // xiao_ble board (verified against Zephyr 4.3.99) does NOT expose it as a
36
+ // DT `sw0` alias there is no gpio-keys node in xiao_ble.dts. Emitting
37
+ // GPIO_DT_SPEC_GET(DT_ALIAS(sw0), gpios) therefore fails to compile
38
+ // ('DT_N_ALIAS_sw0_... was not declared'). Until a button node + alias is
39
+ // added (via an overlay or an upstream board update), interruptPins stays
40
+ // empty: attachInterrupt on this board lowers to the no-DT-spec comment
41
+ // fallback rather than a hard compile error.
42
+ interruptPins: [],
43
+ },
44
+ // The XIAO connector wiring (from seeed_xiao_connector.dtsi + xiao_ble-pinctrl.dtsi):
45
+ // xiao_i2c → i2c1 (SDA P0.04/D4, SCL P0.05/D5)
46
+ // xiao_spi → spi2 (SCK P1.13/D8, MOSI P1.15/D10, MISO P1.14/D9)
47
+ // xiao_serial → uart0 (TX P1.11/D6, RX P1.12/D7)
48
+ // The board DTS leaves i2c0/spi0/spi1 disabled (shared instances).
49
+ i2c: {
50
+ controllers: [{ nodeLabel: 'i2c1' }],
51
+ },
52
+ spi: {
53
+ controllers: [{ nodeLabel: 'spi2' }],
54
+ },
55
+ uart: {
56
+ controllers: [{ nodeLabel: 'uart0' }],
57
+ },
58
+ pwm: {
59
+ // pwm-led0 drives the board PWM LED (PWM_OUT0 on P0.17, inverted).
60
+ specs: [{ pin: 17, dtSpec: 'pwm-led0' }],
61
+ },
62
+ adc: {
63
+ // SAADC node is `adc`; no pre-declared channels. XIAO D0–D3 = AIN0–AIN3
64
+ // (P0.02/P0.03/P0.28/P0.29). Internal VREF ~0.6V with VDD/4 gain 3000mV.
65
+ nodeLabel: 'adc',
66
+ resolution: 12,
67
+ vrefMv: 3000,
68
+ channels: [
69
+ { pin: 2, channel: 0 }, // P0.02 / D0 / AIN0
70
+ { pin: 3, channel: 1 }, // P0.03 / D1 / AIN1
71
+ { pin: 28, channel: 2 }, // P0.28 / D2 / AIN2
72
+ { pin: 29, channel: 3 }, // P0.29 / D3 / AIN3
73
+ ],
74
+ },
75
+ wdt: { nodeLabel: 'wdt0' },
76
+ // Hardware timer: nRF RTC1 is the free counter (RTC0 is kernel-owned by the
77
+ // softdevice/clock driver). The hwtimer lowering drives it as a Zephyr
78
+ // counter device (counter_start/stop + a top-value alarm for set_frequency).
79
+ // Verified against the nRF52840 SoC dtsi (rtc0/rtc1 nodes). The kernel uses
80
+ // RTC0 for the system tick; RTC1 is available for application use.
81
+ hwtimer: { controllers: [{ nodeLabel: 'rtc1' }] },
82
+ };
@@ -18,7 +18,7 @@ import { DEFAULT_ZEPHYR_DISPLAY_PROFILE, type ZephyrDisplayProfile } from './pro
18
18
  // and consumers can reach them from the package barrel.
19
19
  export { zephyrUiDisplayAdapter, zephyrDisplayAdapterGenerator } from './ui-adapter.js';
20
20
  export { zephyrTouchAdapter } from './touch-adapter.js';
21
- export { ZEPHYR_DISPLAY_PROFILES, DEFAULT_ZEPHYR_DISPLAY_PROFILE } from './profiles.js';
21
+ export { ZEPHYR_DISPLAY_PROFILES, DEFAULT_ZEPHYR_DISPLAY_PROFILE, BUILT_IN_PROFILES } from './profiles.js';
22
22
  export type { ZephyrDisplayProfile } from './profiles.js';
23
23
 
24
24
  export interface DisplayState {
@@ -114,3 +114,26 @@ export const ZEPHYR_DISPLAY_PROFILES: Record<string, ZephyrDisplayProfile> = {
114
114
  /** The default profile used when resolveDisplayOp is probed without a display.init. */
115
115
  export const DEFAULT_ZEPHYR_DISPLAY_PROFILE: ZephyrDisplayProfile =
116
116
  ZEPHYR_DISPLAY_PROFILES['ili9341-zephyr'];
117
+
118
+ /**
119
+ * The Zephyr profiles mapped to the shared DisplayProfile shape — the single
120
+ * mapping, so no consumer needs to know the DT-binding descriptor layout.
121
+ * The strategy's getProfileRegistry() and the preview's profile-registry
122
+ * loader both consume this (the same role `BUILT_IN_PROFILES` plays in
123
+ * framework-arduino's displays modules).
124
+ */
125
+ export const BUILT_IN_PROFILES: Record<string, import('@typecad/cuttlefish/api/shared').DisplayProfile> =
126
+ Object.fromEntries(
127
+ Object.entries(ZEPHYR_DISPLAY_PROFILES).map(([name, p]) => [
128
+ name,
129
+ {
130
+ driver: p.driver,
131
+ width: p.width,
132
+ height: p.height,
133
+ nativeWidth: p.nativeWidth,
134
+ nativeHeight: p.nativeHeight,
135
+ colorFormat: p.colorFormat,
136
+ rotation: p.rotation ?? 1,
137
+ },
138
+ ]),
139
+ );
@@ -46,6 +46,10 @@ export interface ZephyrDisplayReadbackOptions {
46
46
  scanlineSync?: boolean;
47
47
  /** MISO/SDO GPIO; readback is disabled when it is not explicitly wired. */
48
48
  miso?: number;
49
+ /** Tearing-effect GPIO (panel TE output). When set, panel updates wait for
50
+ * the TE frame pulse instead of GET_SCANLINE readback — no MISO required.
51
+ * The overlay adds te-gpios to the display DT node from this. */
52
+ tearingEffectPin?: number;
49
53
  }
50
54
 
51
55
  export function zephyrUiDisplayAdapter(
@@ -74,6 +78,9 @@ export function zephyrUiDisplayAdapter(
74
78
  // react badly to GSCAN reads. Both an explicit opt-in and an explicit MISO
75
79
  // pin are required before emitting an active synchronization path.
76
80
  const scanlineSync = readback.scanlineSync === true && readback.miso !== undefined;
81
+ // TE (hardware tearing-effect) sync: strictly opt-in via a configured GPIO.
82
+ // Preferred over GET_SCANLINE when wired — no readback traffic, no MISO.
83
+ const tePin = typeof readback.tearingEffectPin === 'number' ? readback.tearingEffectPin : undefined;
77
84
 
78
85
  const includes = [
79
86
  `// --- Zephyr UI display adapter (${profile.driver}) ---`,
@@ -140,6 +147,22 @@ export function zephyrUiDisplayAdapter(
140
147
  `// controller-specific validation are required; otherwise the display stays`,
141
148
  `// on the existing retained/composited path with no extra SPI reads.`,
142
149
  `static const bool __tc_pnl_scanline_sync = ${scanlineSync ? 'true' : 'false'};`,
150
+ `// Tearing-effect (TE) hardware sync: the panel pulses its TE line once`,
151
+ `// per frame. When te-gpios is present on the display DT node, panel`,
152
+ `// updates arm on the TE edge — tear-free writes with no MISO readback.`,
153
+ `#if DT_NODE_HAS_PROP(DT_NODELABEL(${dtLabel}), te_gpios)`,
154
+ `#define __TC_TE_SYNC 1`,
155
+ `static const struct gpio_dt_spec __tc_te =`,
156
+ ` GPIO_DT_SPEC_GET(DT_NODELABEL(${dtLabel}), te_gpios);`,
157
+ `static struct gpio_callback __tc_te_cb;`,
158
+ `static volatile uint32_t __tc_te_count = 0;`,
159
+ `static void __tc_te_isr(const struct device* port, struct gpio_callback* cb, uint32_t pins) {`,
160
+ ` (void)port; (void)cb; (void)pins;`,
161
+ ` __tc_te_count++;`,
162
+ `}`,
163
+ `#else`,
164
+ `#define __TC_TE_SYNC 0`,
165
+ `#endif`,
143
166
  `// Stashed address window from the last setAddrWindow call. The runtime`,
144
167
  `// calls setAddrWindow + writePixels as a matched pair, so we stash the rect`,
145
168
  `// here and consume it in writePixels.`,
@@ -157,6 +180,18 @@ export function zephyrUiDisplayAdapter(
157
180
  // with DT_HAS_ALIAS (the safe primitive for an alias that may be absent —
158
181
  // DT_NODE_HAS_STATUS(DT_ALIAS(...)) is version-dependent when the alias is
159
182
  // missing and can fail the build).
183
+ // TE pin: input + rising-edge interrupt (the ST7796 TE pulse), then tell
184
+ // the controller to drive the line (TEON 0x35, mode 1 = vertical sync only).
185
+ const teInit = tePin !== undefined ? `#if __TC_TE_SYNC
186
+ if (device_is_ready(__tc_te.port)) {
187
+ gpio_pin_configure_dt(&__tc_te, GPIO_INPUT);
188
+ gpio_init_callback(&__tc_te_cb, __tc_te_isr, BIT(__tc_te.pin));
189
+ (void)gpio_add_callback(__tc_te.port, &__tc_te_cb);
190
+ (void)gpio_pin_interrupt_configure_dt(&__tc_te, GPIO_INT_EDGE_RISING);
191
+ __tc_pnl_cmd1(0x35, 0x01); // TEON: TE output = vsync pulse
192
+ }
193
+ #endif` : '';
194
+
160
195
  const blInit = backlightAlias
161
196
  ? `#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`
162
197
  : '';
@@ -479,6 +514,19 @@ static uint16_t __tc_pnl_read_scanline(void) {
479
514
  // have no safe post-rectangle interval, so they retain the normal single-burst
480
515
  // behavior; the caller's framebuffer still prevents intermediate software frames.
481
516
  static void __tc_pnl_wait_for_safe_rect(int16_t y, int16_t rh) {
517
+ #if __TC_TE_SYNC
518
+ // TE variant: arm on the next frame pulse, then start the burst — the write
519
+ // chases the scan beam from the top of the rect. Bounded so a stuck TE line
520
+ // can never hang the UI loop.
521
+ if (rh < 8 || y < 0) return;
522
+ uint32_t __was = __tc_te_count;
523
+ uint32_t __deadline = k_uptime_get_32() + 25U;
524
+ while (__tc_te_count == __was) {
525
+ if (static_cast<int32_t>(k_uptime_get_32() - __deadline) >= 0) break;
526
+ k_msleep(0);
527
+ }
528
+ return;
529
+ #endif
482
530
  if (!__tc_pnl_scanline_sync || rh < 8 || y < 0) return;
483
531
  int16_t __last = static_cast<int16_t>(y + rh - 1);
484
532
  if (__last >= static_cast<int16_t>(${h} - 2)) return;
@@ -578,8 +626,10 @@ const CuttlefishPanelOps __tc_display_ops = {
578
626
  CuttlefishGFX __tc_display(&__tc_display_ops, nullptr);
579
627
  ${initBlock}
580
628
  // ── display_init (called from setup) ────────────────────────────────────
629
+
581
630
  static inline void display_init() {
582
631
  printk("TC_DISPLAY: device ready\\n");
632
+ ${teInit}
583
633
  ${blInit}
584
634
  gpio_pin_configure_dt(&__tc_pnl_cs, GPIO_OUTPUT);
585
635
  gpio_pin_configure_dt(&__tc_pnl_dc, GPIO_OUTPUT);
@@ -726,5 +776,6 @@ export const zephyrDisplayAdapterGenerator: DisplayAdapterGenerator = (display)
726
776
  return zephyrUiDisplayAdapter(profile, {
727
777
  scanlineSync: display.scanlineSync,
728
778
  miso: display.spiPins?.miso,
779
+ tearingEffectPin: (display as { tearingEffectPin?: number }).tearingEffectPin,
729
780
  });
730
781
  };
@@ -35,6 +35,17 @@ export interface KconfigUsage {
35
35
  /** PSRAM type ('opi' | 'quad') when the target board has PSRAM. Emits the
36
36
  * CONFIG_SPIRAM symbols so the ESP heap serves PSRAM for canvas allocations. */
37
37
  psram?: 'opi' | 'quad';
38
+ /** HAL pin numbers the program reads with adc.* — scanned from the emitted
39
+ * `__tc_adc<N>_setup()` calls at compile time. Only the overlay generator
40
+ * consumes this (to rewrite the ADC node's pinctrl-0 to the used channels
41
+ * on SoCs that need pad muxing, e.g. STM32); prj.conf ignores it. */
42
+ adcReadPins?: readonly number[];
43
+ /** HAL pin numbers the program drives with pwm.* — scanned from the
44
+ * emitted `__tc_pwm_*` spec references at compile time. Only the overlay
45
+ * generator consumes this (synthesized pwm-leds consumers + aliases are
46
+ * emitted per used pin, so the DT carries no dead channels); prj.conf
47
+ * ignores it. */
48
+ pwmUsedPins?: readonly number[];
38
49
  }
39
50
 
40
51
  /**
@@ -123,7 +134,7 @@ export function resolveKconfigFragments(
123
134
  // (without it, Kconfig silently forces them all to n).
124
135
  m.set('CONFIG_NETWORKING', 'y');
125
136
  m.set('CONFIG_WIFI', 'y');
126
- m.set('CONFIG_WIFI_ESP32', 'y'); // ESP32-specific driver (sole WiFi target)
137
+ m.set('CONFIG_WIFI_ESP32', 'y'); // family-wide ESP32 driver (esp32/s3/c3/c6)
127
138
  m.set('CONFIG_NET_L2_ETHERNET', 'y');
128
139
  m.set('CONFIG_NET_IPV4', 'y');
129
140
  m.set('CONFIG_NET_UDP', 'y'); // transitive dep of NET_DHCPV4
@@ -25,6 +25,9 @@ export interface DisplayWiring {
25
25
  cs?: number;
26
26
  dc?: number;
27
27
  rst?: number;
28
+ /** Tearing-effect (TE) GPIO from display.tearingEffectPin — emitted as
29
+ * te-gpios on the display DT node. Opt-in; most boards don't wire TE. */
30
+ tearingEffectPin?: number;
28
31
  spiFrequency?: number;
29
32
  /** SPI bus pins. When present, the overlay remuxes the SPI controller's
30
33
  * pinctrl to these pins (the board defaults rarely match a breakout's
@@ -61,13 +64,30 @@ export interface TouchWiring {
61
64
  minPressure?: number;
62
65
  }
63
66
 
67
+ export interface OverlayDiagnostic {
68
+ severity: "warning" | "error";
69
+ message: string;
70
+ }
71
+
64
72
  export function generateOverlay(
65
73
  chip: ZephyrChipDescriptor,
66
74
  usage: KconfigUsage,
67
75
  display: ZephyrDisplayProfile | undefined,
68
76
  wiring?: DisplayWiring,
69
77
  touch?: TouchWiring,
78
+ diagnostics: OverlayDiagnostic[] = [],
70
79
  ): string {
80
+ // An I2C touch controller with no explicit bus pins: the overlay enables
81
+ // i2c0 and instantiates the node, but nothing remuxes the controller to
82
+ // the wired SDA/SCL (the board's default I2C pins rarely match a
83
+ // breakout). Every I2C read then fails and touch silently does nothing —
84
+ // surface it at build time instead of leaving it to a multimeter.
85
+ if (touch && touch.controller !== 'xpt2046' && (touch.sda === undefined || touch.scl === undefined)) {
86
+ diagnostics.push({
87
+ severity: "warning",
88
+ message: `touch: I2C controller '${touch.controller}' has no sda/scl pins in cuttlefish.config.ts — the overlay enables the bus without a pin assignment, so the controller may never answer. Add touch.sda and touch.scl (the board's default I2C pins are rarely the wired ones).`,
89
+ });
90
+ }
71
91
  const lines: string[] = [
72
92
  '/* Auto-generated by @typecad/framework-zephyr from cuttlefish.config.ts. */',
73
93
  '/* Enables peripherals the program uses. West merges this over the board DT. */',
@@ -97,6 +117,24 @@ export function generateOverlay(
97
117
  if (usage.usesUart && chip.uart) {
98
118
  for (const c of chip.uart.controllers) block(c.nodeLabel);
99
119
  }
120
+ // PWM: synthesized specs (controller + channel, no board-shipped alias) get
121
+ // a pwm-leds consumer node + tc-pwm<pin> alias here — the lowering addresses
122
+ // the channel as PWM_DT_SPEC_GET(DT_ALIAS(tc-pwm<pin>)), so the alias must
123
+ // exist in the merged DT. pwmDtAlias (lowering/pwm.ts) derives the same
124
+ // name from the pin; the two sides cannot drift. Filtered to the pins the
125
+ // program actually drives (pwmUsedPins) so the DT carries no dead channels;
126
+ // an undefined list (prepare-time overlay) emits all specs.
127
+ if (usage.usesPwm && chip.pwm) {
128
+ emitPwmNodes(lines, chip, usage.pwmUsedPins);
129
+ }
130
+ // ADC: enable the ADC device node, and on SoCs whose channels carry a
131
+ // pinctrl label (STM32: the pad stays in GPIO mode unless the ADC node's
132
+ // pinctrl-0 muxes it analog), rewrite pinctrl-0 to exactly the channels the
133
+ // program reads. DT assignment replaces the whole property — the board
134
+ // default (one pad) is deliberately dropped in favor of the used set.
135
+ if (usage.usesAdc && chip.adc) {
136
+ emitAdcNode(lines, chip, usage.adcReadPins);
137
+ }
100
138
  // DAC: enable the chip's DAC device node when the program uses dac.*. The
101
139
  // lowering references DEVICE_DT_GET(DT_NODELABEL(<dac.device>)).
102
140
  if (usage.usesDac && chip.dac) {
@@ -159,6 +197,84 @@ export function generateOverlay(
159
197
  return lines.join('\n');
160
198
  }
161
199
 
200
+ /**
201
+ * Emit the pwm-leds consumer node + aliases for synthesized PWM specs.
202
+ *
203
+ * A board DTS may enable a PWM controller (`pwm4`) without aliasing any
204
+ * channel — the STM32 pattern — while the lowering addresses channels via
205
+ * DT_ALIAS. Each synthesized spec gets a `pwm-leds` child whose `pwms` cell
206
+ * binds the controller+channel (+ period/polarity), and a `tc-pwm<pin>`
207
+ * alias pointing at it. Alias/label names are valid DT identifiers (lowercase
208
+ * alphanumerics + dash/underscore).
209
+ */
210
+ function emitPwmNodes(
211
+ lines: string[],
212
+ chip: ZephyrChipDescriptor,
213
+ usedPins?: readonly number[],
214
+ ): void {
215
+ const synthesized = (chip.pwm?.specs ?? []).filter(
216
+ (s) => s.controller && s.channel != null && !s.dtSpec,
217
+ ).filter((s) => !usedPins || usedPins.includes(s.pin));
218
+ if (synthesized.length === 0) return;
219
+ // Enable each distinct PWM controller node (idempotent when already okay).
220
+ for (const controller of [...new Set(synthesized.map((s) => s.controller!))]) {
221
+ lines.push(`&${controller} {`);
222
+ lines.push(' status = "okay";');
223
+ lines.push('};');
224
+ lines.push('');
225
+ }
226
+ lines.push('/ {');
227
+ lines.push(' tc_pwm_leds: tc-pwm-leds {');
228
+ lines.push(' compatible = "pwm-leds";');
229
+ for (const s of synthesized) {
230
+ // 20 ms / 50 Hz default — the servo convention; the duty scaling in the
231
+ // lowering normalizes 0-255 against whatever period is baked in.
232
+ const period = s.periodNs ?? 20_000_000;
233
+ const polarity = s.polarity ?? 'PWM_POLARITY_NORMAL';
234
+ lines.push(` tc_pwm_${s.pin}: pwm-led-${s.pin} {`);
235
+ lines.push(` pwms = <&${s.controller} ${s.channel} ${period} ${polarity}>;`);
236
+ lines.push(' };');
237
+ }
238
+ lines.push(' };');
239
+ lines.push(' aliases {');
240
+ for (const s of synthesized) {
241
+ lines.push(` tc-pwm${s.pin} = &tc_pwm_${s.pin};`);
242
+ }
243
+ lines.push(' };');
244
+ lines.push('};');
245
+ lines.push('');
246
+ }
247
+
248
+ /**
249
+ * Emit the ADC device-node enable (+ optional pinctrl override). The pinctrl
250
+ * override applies only when the descriptor's channels carry pinctrl labels
251
+ * AND the caller knows which pins the program reads (compile-time regen scans
252
+ * the emitted `__tc_adc<N>_setup()` calls); the prepare-time overlay omits it
253
+ * and the compile regen rewrites the file before west runs.
254
+ */
255
+ function emitAdcNode(
256
+ lines: string[],
257
+ chip: ZephyrChipDescriptor,
258
+ readPins?: readonly number[],
259
+ ): void {
260
+ const adc = chip.adc!;
261
+ const labeled = adc.channels.filter((c) => c.pinctrl);
262
+ const used = labeled.filter((c) => !readPins || readPins.includes(c.pin));
263
+ if (labeled.length > 0 && used.length > 0) {
264
+ lines.push(`&${adc.nodeLabel} {`);
265
+ lines.push(' status = "okay";');
266
+ lines.push(` pinctrl-0 = <${used.map((c) => `&${c.pinctrl}`).join(' ')}>;`);
267
+ lines.push(' pinctrl-names = "default";');
268
+ lines.push('};');
269
+ lines.push('');
270
+ return;
271
+ }
272
+ lines.push(`&${adc.nodeLabel} {`);
273
+ lines.push(' status = "okay";');
274
+ lines.push('};');
275
+ lines.push('');
276
+ }
277
+
162
278
  /**
163
279
  * Emit a display DT node definition. The node is attached to the profile's SPI
164
280
  * controller (default spi2, the ESP32's first user SPI controller) via a MIPI
@@ -246,6 +362,13 @@ function emitDisplayNode(
246
362
  lines.push(` ${display.dtLabel}: display@0 {`);
247
363
  lines.push(` compatible = "${compatible}";`);
248
364
  lines.push(' reg = <0>;');
365
+ if (wiring?.tearingEffectPin !== undefined) {
366
+ const tePin = wiring.tearingEffectPin!;
367
+ // Tearing-effect input on the display node: GPIO_DT_SPEC_GET(
368
+ // DT_NODELABEL(display0), te_gpios) in the adapter. Opt-in —
369
+ // most modules don't break the TE pad out.
370
+ lines.push(` te-gpios = <&${gpioController(tePin)} ${tePin} GPIO_ACTIVE_HIGH>;`);
371
+ }
249
372
  lines.push(` mipi-max-frequency = <${freq}>;`);
250
373
  lines.push(' mipi-mode = "MIPI_DBI_MODE_SPI_4WIRE";');
251
374
  // Required by the lcd-controller binding (Zephyr 4.x): 0 = RGB565,
@@ -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,
package/src/index.ts CHANGED
@@ -10,6 +10,12 @@ export { ZephyrStrategy as FrameworkStrategy } from './strategy.js';
10
10
  export { ZephyrStrategy } from './strategy.js';
11
11
  export { Toolchain } from './toolchain/index.js';
12
12
 
13
+ // Create-time starter debug artifacts. The `cuttlefish create` flow reads this
14
+ // optional named export off the loaded framework module (same loader pattern
15
+ // as doctor/licenses) and calls it for freshly scaffolded projects, so F5 in
16
+ // VS Code works before the first build. No-ops for non-GDB targets.
17
+ export { writeProjectDebugArtifacts } from './toolchain/debug-config.js';
18
+
13
19
  // `cuttlefish doctor` — verify the installed Zephyr is reachable + inside the
14
20
  // declared compat range, and preview board-target normalization. Re-exported
15
21
  // under the dispatcher-facing alias `doctor` so the loader picks it up as
@@ -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}\`. ` +