@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
@@ -9,6 +9,7 @@
9
9
  import { writeFileSync, existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
10
10
  import { join } from 'node:path';
11
11
  import { resolveKconfigFragments } from '../dt-config/kconfig.js';
12
+ import { readCuttlefishLibrarySidecar } from '@typecad/cuttlefish/library-packages';
12
13
  /** Write a file only if the content differs from the existing file.
13
14
  * Returns true when the file was written (content changed or file was new). */
14
15
  export function writeIfChanged(filePath, content) {
@@ -24,27 +25,61 @@ export function writeIfChanged(filePath, content) {
24
25
  writeFileSync(filePath, content);
25
26
  return true;
26
27
  }
28
+ /**
29
+ * Names of the cuttlefish-emitted C/C++ sources under src/ (top level only,
30
+ * matching the old `src/*.cpp src/*.c` glob; sorted so the generated
31
+ * CMakeLists.txt is stable across readdir orderings). Empty when no sources
32
+ * exist yet (first prepare call).
33
+ */
34
+ function listEmittedSources(srcDir) {
35
+ if (!existsSync(srcDir))
36
+ return [];
37
+ const names = readdirSync(srcDir).filter((name) => name.endsWith('.cpp') || name.endsWith('.c'));
38
+ names.sort();
39
+ return names;
40
+ }
27
41
  /**
28
42
  * Concatenate all emitted source under src/ so the scaffold can detect which
29
43
  * peripherals the program actually uses. The cuttlefish lowering emits
30
44
  * well-known driver API tokens (adc_read, spi_transceive, bt_*, …), so scanning
31
- * the post-transpile source is an authoritative usage signal — and it keeps the
32
- * scaffold self-contained (no need to thread analysis through the toolchain
45
+ * the post-transpile source is an authoritative usage signal — and it keeps
46
+ * the scaffold self-contained (no need to thread analysis through the toolchain
33
47
  * contract). Returns '' when no sources exist yet (first prepare call).
34
48
  */
35
49
  function readEmittedSources(srcDir) {
36
- if (!existsSync(srcDir))
37
- return '';
38
50
  let out = '';
39
- for (const name of readdirSync(srcDir)) {
40
- if (name.endsWith('.cpp') || name.endsWith('.c')) {
41
- try {
42
- out += readFileSync(join(srcDir, name), 'utf8');
43
- }
44
- catch {
45
- // ignore unreadable files
51
+ for (const name of listEmittedSources(srcDir)) {
52
+ try {
53
+ out += readFileSync(join(srcDir, name), 'utf8');
54
+ }
55
+ catch {
56
+ // ignore unreadable files
57
+ }
58
+ }
59
+ return out;
60
+ }
61
+ /**
62
+ * Append cuttlefish library packages' devicetree overlay fragments to the
63
+ * generated overlay. Library entries come from the transpiler's libraries.json
64
+ * sidecar (next to the emitted sources) and are already gated on the
65
+ * library's include token appearing in the emitted sources — no re-detection.
66
+ * Fragments merge after the framework overlay so library nodes (e.g.
67
+ * @typecad/zephyr-esp32s3-rgb's WS2812 node on I2S0) layer over it.
68
+ */
69
+ export function appendLibraryOverlayFragments(overlay, projectRoot) {
70
+ let out = overlay;
71
+ for (const entry of readCuttlefishLibrarySidecar(join(projectRoot, 'src'))) {
72
+ if (!entry.overlay)
73
+ continue;
74
+ try {
75
+ const fragment = readFileSync(entry.overlay, 'utf8').trim();
76
+ if (fragment.length > 0) {
77
+ out += (out.endsWith('\n') ? '' : '\n') + '\n' + fragment + '\n';
46
78
  }
47
79
  }
80
+ catch {
81
+ // best-effort; a missing fragment surfaces as a DT error
82
+ }
48
83
  }
49
84
  return out;
50
85
  }
@@ -118,8 +153,13 @@ export function scaffoldZephyrProject(projectRoot, debug = false, userKconfig, p
118
153
  };
119
154
  let changed = false;
120
155
  // ── Root CMakeLists.txt ─────────────────────────────────────────────────
121
- // The canonical Zephyr CMake application. GLOB src/*.cpp so future multi-
122
- // file emits are picked up automatically; cuttlefish owns the file contents.
156
+ // The canonical Zephyr CMake application. The emitted source list is
157
+ // explicit (no file(GLOB CONFIGURE_DEPENDS ...)): CONFIGURE_DEPENDS puts a
158
+ // cmake.verify_globs step in the ninja graph that spawns CMake to re-check
159
+ // the glob on every build, and the scaffold already rewrites this file via
160
+ // writeIfChanged whenever the emitted file set changes — which flips
161
+ // configChanged and reconfigures with the new list baked in.
162
+ const sourceFiles = listEmittedSources(srcDir);
123
163
  const cmakeLists = [
124
164
  '# Auto-generated by @typecad/framework-zephyr from cuttlefish.config.ts.',
125
165
  'cmake_minimum_required(VERSION 3.20.0)',
@@ -128,12 +168,18 @@ export function scaffoldZephyrProject(projectRoot, debug = false, userKconfig, p
128
168
  '',
129
169
  'project(zephyr_app)',
130
170
  '',
131
- '# Collect cuttlefish-emitted sources. CONFIGURE_DEPENDS makes CMake re-',
132
- '# check the glob when the source set changes (e.g. the transpiler removes',
133
- '# a stale entry), instead of linking a file list from the last configure.',
134
- 'file(GLOB app_sources CONFIGURE_DEPENDS src/*.cpp src/*.c)',
135
- '',
136
- 'target_sources(app PRIVATE ${app_sources})',
171
+ ...(sourceFiles.length > 0
172
+ ? [
173
+ '# Cuttlefish-emitted sources. This list is regenerated whenever the',
174
+ '# emitted file set changes (the scaffold rewrites CMakeLists.txt).',
175
+ 'target_sources(app PRIVATE',
176
+ ...sourceFiles.map((name) => ` src/${name}`),
177
+ ')',
178
+ ]
179
+ : [
180
+ '# No emitted sources yet — the scaffold regenerates this list on the',
181
+ '# next compile once src/ contains .cpp/.c files.',
182
+ ]),
137
183
  // When PSRAM is configured, define BOARD_HAS_PSRAM so the UI runtime's
138
184
  // PSRAM canvas allocator (ui_create_canvas_best) is compiled in.
139
185
  ...(psram ? ['', '# PSRAM enabled: activate the runtime PSRAM canvas paths.', 'target_compile_definitions(app PRIVATE BOARD_HAS_PSRAM)', ''] : ['']),
@@ -182,6 +228,25 @@ export function scaffoldZephyrProject(projectRoot, debug = false, userKconfig, p
182
228
  continue;
183
229
  prjConf.push(`${sym}=${val}`);
184
230
  }
231
+ // ── Cuttlefish library packages ─────────────────────────────────────────
232
+ // Libraries the program imports (recorded in the transpiler's libraries.json
233
+ // sidecar, next to the emitted sources) contribute their manifest's kconfig
234
+ // lines — e.g. @typecad/zephyr-esp32s3-rgb contributes CONFIG_LED_STRIP.
235
+ // Sidecar entries are already gated on the library's include token
236
+ // appearing in the emitted sources, so no re-detection here. User
237
+ // zephyr.kconfig overrides still win.
238
+ const libraryEntries = readCuttlefishLibrarySidecar(srcDir);
239
+ if (libraryEntries.length > 0) {
240
+ prjConf.push('', '# Library packages (cuttlefish.library.json contributions).');
241
+ for (const entry of libraryEntries) {
242
+ for (const line of entry.kconfig) {
243
+ const sym = line.split('=')[0];
244
+ if (userKconfig && sym !== undefined && userKconfig.hasOwnProperty(sym))
245
+ continue;
246
+ prjConf.push(line);
247
+ }
248
+ }
249
+ }
185
250
  // Emit user-specified Kconfig from cuttlefish.config.ts zephyr.kconfig.
186
251
  // These override any matching auto-detected symbol (skipped above).
187
252
  if (userKconfig) {
@@ -9,8 +9,11 @@ export interface WestInstall {
9
9
  westExecutable?: string;
10
10
  /** Absolute path to a Python interpreter with west installed (mode 'module'). */
11
11
  pythonExecutable?: string;
12
- /** Absolute path to the Zephyr SDK root (for $ZEPHYR_BASE), if found. */
12
+ /** Absolute path to the Zephyr workspace (for $ZEPHYR_BASE), if found. */
13
13
  zephyrBase?: string;
14
+ /** Absolute path to the Zephyr SDK install dir (hosttools/openocd lives
15
+ * there), if found — used to put the SDK's openocd on the flash PATH. */
16
+ sdkInstallDir?: string;
14
17
  /** mode 'micromamba': path to the micromamba binary (for `micromamba run -n …`). */
15
18
  micromambaExe?: string;
16
19
  /** mode 'micromamba': the conda env name (default 'zephyr'). */
@@ -172,12 +172,14 @@ export function discoverFromMicromamba(envName = process.env.TYPECAD_ZEPHYR_ENV
172
172
  // anything else in the cuttlefish process) can detect the Zephyr version
173
173
  // WITHOUT activation — micromamba run sets it only inside the west subprocess.
174
174
  const zb = readMicromambaEnvVar(envDir, 'TYPECAD_ZEPHYR_BASE');
175
+ const sdk = readMicromambaEnvVar(envDir, 'TYPECAD_ZEPHYR_SDK_INSTALL_DIR');
175
176
  return {
176
177
  mode: 'micromamba',
177
178
  micromambaExe: mm.exe,
178
179
  envName,
179
180
  mambaRootPrefix: mm.rootPrefix,
180
181
  zephyrBase: zb && isZephyrBase(zb) ? zb : undefined,
182
+ sdkInstallDir: sdk && existsSync(sdk) ? sdk : undefined,
181
183
  source: 'micromamba',
182
184
  };
183
185
  }
@@ -14,7 +14,8 @@
14
14
  // (find_package(Zephyr) needs it). west's prj.conf/CMakeLists are found via
15
15
  // the project dir regardless.
16
16
  // ---------------------------------------------------------------------------
17
- import { dirname } from 'node:path';
17
+ import { dirname, join } from 'node:path';
18
+ import { existsSync } from 'node:fs';
18
19
  import { discoverWest } from './west-discover.js';
19
20
  /**
20
21
  * The Scripts/ (Windows) or bin/ (POSIX) directory of the venv the discovered
@@ -41,8 +42,19 @@ export function buildEnv(install) {
41
42
  if (install.zephyrBase && !env.ZEPHYR_BASE) {
42
43
  env.ZEPHYR_BASE = install.zephyrBase;
43
44
  }
44
- const bin = venvBinDir(install);
45
- if (bin) {
45
+ // SWD flashing (`zephyr.runner: 'openocd'`): west's openocd runner resolves
46
+ // a bare `openocd` from PATH. The Zephyr SDK ships it under
47
+ // hosttools/openocd/bin (the SDK's own setup.cmd puts that dir on PATH for
48
+ // activated terminals) — do the same for spawned west processes so ST-Link
49
+ // flashing works without activation. $ZEPHYR_SDK_INSTALL_DIR (set by an
50
+ // activated env) wins over the discovered install dir.
51
+ const sdkRoot = env.ZEPHYR_SDK_INSTALL_DIR || install.sdkInstallDir;
52
+ const openocdBin = sdkRoot ? join(sdkRoot, 'hosttools', 'openocd', 'bin') : undefined;
53
+ const prepend = [
54
+ openocdBin !== undefined && existsSync(openocdBin) ? openocdBin : undefined,
55
+ venvBinDir(install),
56
+ ].filter((d) => d !== undefined);
57
+ if (prepend.length > 0) {
46
58
  const sep = process.platform === 'win32' ? ';' : ':';
47
59
  // On Windows the PATH environment variable may be cased as `Path` (the
48
60
  // registry-native form, the only one populated when node is launched from
@@ -50,9 +62,9 @@ export function buildEnv(install) {
50
62
  // casing can leave the other stale/empty, which under PowerShell would drop
51
63
  // the user's real PATH (cmake, ninja, …) — breaking `west` configure. Read
52
64
  // whichever casing is populated and write that same casing back, preserving
53
- // the full existing value with the venv dir prepended.
65
+ // the full existing value with the discovered dirs prepended.
54
66
  const existing = env.Path ?? env.PATH ?? '';
55
- const updated = bin + sep + existing;
67
+ const updated = prepend.join(sep) + sep + existing;
56
68
  if (env.Path !== undefined || (env.PATH === undefined && process.platform === 'win32')) {
57
69
  env.Path = updated;
58
70
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typecad/framework-zephyr",
3
- "version": "1.0.0-alpha.12",
3
+ "version": "1.0.0-alpha.14",
4
4
  "description": "TypeCAD framework package for the Zephyr RTOS — west/CMake build, devicetree-driven GPIO",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -38,11 +38,11 @@
38
38
  "test:hw:mqtt": "cd ../../tests/hardware && npm exec -- cuttlefish-test mqtt-client.test.ts"
39
39
  },
40
40
  "dependencies": {
41
- "@typecad/cuttlefish": "1.0.0-alpha.12"
41
+ "@typecad/cuttlefish": "1.0.0-alpha.14"
42
42
  },
43
43
  "devDependencies": {
44
- "@typecad/expect": "1.0.0-alpha.12",
45
- "@typecad/board-xiao-nrf52840": "1.0.0-alpha.12",
44
+ "@typecad/expect": "1.0.0-alpha.14",
45
+ "@typecad/board-xiao-nrf52840": "1.0.0-alpha.14",
46
46
  "typescript": "^5.7.3"
47
47
  },
48
48
  "license": "MIT",
@@ -75,7 +75,7 @@
75
75
  "zephyr"
76
76
  ],
77
77
  "engines": {
78
- "node": ">=18"
78
+ "node": ">=22.11.0"
79
79
  },
80
80
  "author": "typecad0",
81
81
  "sideEffects": false
@@ -19,7 +19,7 @@
19
19
  // collapse to a one-liner.
20
20
  // ---------------------------------------------------------------------------
21
21
 
22
- import type { ZephyrChipDescriptor } from './types.js';
22
+ import type { ZephyrChipDescriptor, ZephyrGpioController } from './types.js';
23
23
 
24
24
  /**
25
25
  * Resolve the devicetree nodelabel of the GPIO controller that owns `pin`.
@@ -30,23 +30,53 @@ import type { ZephyrChipDescriptor } from './types.js';
30
30
  * for out-of-range pins (e.g. the manifest probe's synthetic pin 0).
31
31
  */
32
32
  export function controllerNodelabelForPin(chip: ZephyrChipDescriptor, pin: number): string {
33
+ const hit = controllerRangeForPin(chip, pin);
34
+ return hit ? hit.nodelabel : chip.gpioController;
35
+ }
36
+
37
+ /**
38
+ * The owning controller range for a HAL pin, if the chip declares a split.
39
+ */
40
+ export function controllerRangeForPin(
41
+ chip: ZephyrChipDescriptor,
42
+ pin: number,
43
+ ): ZephyrGpioController | undefined {
33
44
  const ranges = chip.gpioControllers;
34
45
  if (ranges && ranges.length > 0) {
35
- const hit = ranges.find((r) => pin >= r.minPin && pin <= r.maxPin);
36
- if (hit) return hit.nodelabel;
46
+ return ranges.find((r) => pin >= r.minPin && pin <= r.maxPin);
37
47
  }
38
- return chip.gpioController;
48
+ return undefined;
39
49
  }
40
50
 
41
51
  /**
42
- * Emit the C++ source for a runtime pin GPIO-device dispatcher.
52
+ * The PORT-RELATIVE pin index for the raw gpio_pin_*_raw() API the Zephyr
53
+ * raw calls address the index WITHIN the controller, not the global HAL pin
54
+ * number. For split SoCs each controller's `minPin` is its base (STM32:
55
+ * gpiob minPin 16, so PB12 = pin 28 → raw 12); single-controller SoCs have
56
+ * no offset. Emitting the global number against a port driver would address
57
+ * a nonexistent port bit (STM32 gpiob is 0–15) and fail at runtime.
58
+ */
59
+ export function controllerRawPinForPin(chip: ZephyrChipDescriptor, pin: number): number {
60
+ const hit = controllerRangeForPin(chip, pin);
61
+ return pin - (hit?.minPin ?? 0);
62
+ }
63
+
64
+ /**
65
+ * Emit the C++ source for runtime pin → GPIO-device / port-relative-index
66
+ * dispatchers.
67
+ *
68
+ * Returns lines defining:
69
+ * - `static inline const struct device* __tc_gpio_dev(uint32_t pin)` — the
70
+ * owning controller's device, resolved via DEVICE_DT_GET(DT_NODELABEL(...))
71
+ * at compile time (the macro is evaluated per branch, so it is always
72
+ * statically valid); only `pin` is runtime.
73
+ * - `static inline gpio_pin_t __tc_gpio_pin(uint32_t pin)` — the
74
+ * port-relative index for gpio_pin_*_raw() (see controllerRawPinForPin).
75
+ * The runtime shim paths (UI pin-watch, safety voters) take a runtime pin,
76
+ * so they cannot bake the offset in at emit time.
43
77
  *
44
- * Returns lines defining `static inline const struct device* __tc_gpio_dev(uint32_t pin)`.
45
- * Each branch resolves its controller via `DEVICE_DT_GET(DT_NODELABEL(...))` at
46
- * compile time (the macro is evaluated per branch, so it is always statically
47
- * valid); only `pin` is runtime. For a single-controller SoC this collapses to
48
- * a one-liner returning that controller, so the existing XIAO nRF52840 behavior
49
- * is byte-for-byte unchanged.
78
+ * For a single-controller SoC both collapse to one-liners, so the existing
79
+ * XIAO nRF52840 behavior is byte-for-byte unchanged.
50
80
  */
51
81
  export function emitGpioDevDispatcher(chip: ZephyrChipDescriptor): string[] {
52
82
  const ranges = chip.gpioControllers;
@@ -56,19 +86,38 @@ export function emitGpioDevDispatcher(chip: ZephyrChipDescriptor): string[] {
56
86
  ` (void)pin;`,
57
87
  ` return DEVICE_DT_GET(DT_NODELABEL(${chip.gpioController}));`,
58
88
  '}',
89
+ 'static inline gpio_pin_t __tc_gpio_pin(uint32_t pin) {',
90
+ ` return (gpio_pin_t)pin;`,
91
+ '}',
59
92
  ];
60
93
  }
61
94
  const lines: string[] = [
62
95
  'static inline const struct device* __tc_gpio_dev(uint32_t pin) {',
63
96
  ];
97
+ const pinLines: string[] = [
98
+ 'static inline gpio_pin_t __tc_gpio_pin(uint32_t pin) {',
99
+ ];
64
100
  for (const r of ranges) {
65
101
  lines.push(
66
102
  ` if (pin >= ${r.minPin} && pin <= ${r.maxPin}) { return DEVICE_DT_GET(DT_NODELABEL(${r.nodelabel})); }`,
67
103
  );
104
+ if (r.minPin === 0) {
105
+ pinLines.push(
106
+ ` if (pin >= ${r.minPin} && pin <= ${r.maxPin}) { return (gpio_pin_t)pin; }`,
107
+ );
108
+ } else {
109
+ pinLines.push(
110
+ ` if (pin >= ${r.minPin} && pin <= ${r.maxPin}) { return (gpio_pin_t)(pin - ${r.minPin}); }`,
111
+ );
112
+ }
68
113
  }
69
114
  lines.push(
70
115
  ` return DEVICE_DT_GET(DT_NODELABEL(${chip.gpioController}));`,
71
116
  '}',
72
117
  );
73
- return lines;
118
+ pinLines.push(
119
+ ` return (gpio_pin_t)pin;`,
120
+ '}',
121
+ );
122
+ return [...lines, ...pinLines];
74
123
  }
@@ -102,18 +102,36 @@ export function resolveChipFromBoard(
102
102
 
103
103
  const pwmSpecs = collectIndexed<ZephyrPwmSpec>(bc, 'zephyr.pwm.specs', (m, i) => {
104
104
  const pin = m.get(`zephyr.pwm.specs.${i}.pin`) as number;
105
- const dtSpec = m.get(`zephyr.pwm.specs.${i}.dtSpec`) as string;
106
- if (pin != null && dtSpec) return { pin, dtSpec };
107
- return null;
105
+ const dtSpec = m.get(`zephyr.pwm.specs.${i}.dtSpec`) as string | undefined;
106
+ const controller = m.get(`zephyr.pwm.specs.${i}.controller`) as string | undefined;
107
+ const channel = m.get(`zephyr.pwm.specs.${i}.channel`) as number | undefined;
108
+ const periodNs = m.get(`zephyr.pwm.specs.${i}.periodNs`) as number | undefined;
109
+ const polarity = m.get(`zephyr.pwm.specs.${i}.polarity`) as string | undefined;
110
+ // Board-shipped alias form (dtSpec) or synthesized form (controller +
111
+ // channel → overlay-generated alias) — at least one, else drop the entry.
112
+ if (pin == null || !(dtSpec || (controller && channel != null))) return null;
113
+ return {
114
+ pin,
115
+ ...(dtSpec ? { dtSpec } : {}),
116
+ ...(controller ? { controller } : {}),
117
+ ...(channel != null ? { channel } : {}),
118
+ ...(periodNs != null ? { periodNs } : {}),
119
+ ...(polarity ? { polarity } : {}),
120
+ };
108
121
  });
109
122
 
110
123
  const adcNodeLabel = bc.get('zephyr.adc.nodeLabel') as string | undefined;
111
124
  const adcResolution = bc.get('zephyr.adc.resolution') as number | undefined;
112
125
  const adcVref = bc.get('zephyr.adc.vrefMv') as number | undefined;
126
+ const adcGain = bc.get('zephyr.adc.gain') as string | undefined;
127
+ const adcReference = bc.get('zephyr.adc.reference') as string | undefined;
113
128
  const adcChannels = collectIndexed<ZephyrAdcChannel>(bc, 'zephyr.adc.channels', (m, i) => {
114
129
  const pin = m.get(`zephyr.adc.channels.${i}.pin`) as number;
115
130
  const channel = m.get(`zephyr.adc.channels.${i}.channel`) as number;
116
- if (pin != null && channel != null) return { pin, channel };
131
+ const pinctrl = m.get(`zephyr.adc.channels.${i}.pinctrl`) as string | undefined;
132
+ if (pin != null && channel != null) {
133
+ return { pin, channel, ...(pinctrl ? { pinctrl } : {}) };
134
+ }
117
135
  return null;
118
136
  });
119
137
 
@@ -138,7 +156,16 @@ export function resolveChipFromBoard(
138
156
  ...(uartControllers.length > 0 ? { uart: { controllers: uartControllers } } : {}),
139
157
  ...(pwmSpecs.length > 0 ? { pwm: { specs: pwmSpecs } } : {}),
140
158
  ...(adcNodeLabel || adcResolution != null || adcVref != null || adcChannels.length > 0
141
- ? { adc: { nodeLabel: adcNodeLabel ?? 'adc', resolution: adcResolution ?? 12, vrefMv: adcVref ?? 3000, channels: adcChannels } }
159
+ ? {
160
+ adc: {
161
+ nodeLabel: adcNodeLabel ?? 'adc',
162
+ resolution: adcResolution ?? 12,
163
+ vrefMv: adcVref ?? 3000,
164
+ channels: adcChannels,
165
+ ...(adcGain ? { gain: adcGain } : {}),
166
+ ...(adcReference ? { reference: adcReference } : {}),
167
+ },
168
+ }
142
169
  : {}),
143
170
  ...(wdtNodeLabel ? { wdt: { nodeLabel: wdtNodeLabel } } : {}),
144
171
  ...(wifiSupported ? { wifi: { supported: true as const } } : {}),
@@ -64,14 +64,37 @@ export interface ZephyrBusController {
64
64
  /**
65
65
  * A PWM channel described as a devicetree spec.
66
66
  *
67
- * Emitted as `struct pwm_dt_spec __tc_pwm<N> = PWM_DT_SPEC_GET(DT_ALIAS(<dtSpec>))`.
68
- * `pwm_set_pulse_dt(&__tc_pwm<N>, pulse_ns)` honors the spec's period/polarity.
67
+ * Two forms, mutually exclusive:
68
+ * - **Board-shipped alias:** `dtSpec` names a DT alias the board's own DTS
69
+ * already defines (e.g. the XIAO's `pwm-led0`). Emitted as
70
+ * `PWM_DT_SPEC_GET(DT_ALIAS(<dtSpec>))`.
71
+ * - **Synthesized** (controller + channel): the board DTS enables a PWM
72
+ * controller node (e.g. `pwm4`) but defines no alias for it. The overlay
73
+ * generator synthesizes a `pwm-leds` consumer node + a `tc-pwm<pin>` alias
74
+ * in `<board>.overlay`; the lowering emits
75
+ * `PWM_DT_SPEC_GET(DT_ALIAS(tc-pwm<pin>))`. Both sides derive the alias
76
+ * name from the pin, so they always agree.
77
+ *
78
+ * `pwm_set_pulse_dt(&spec, pulse_ns)` honors the spec's period/polarity.
69
79
  */
70
80
  export interface ZephyrPwmSpec {
71
81
  /** GPIO number (matches the HAL op `pin` field). */
72
82
  readonly pin: number;
73
- /** Devicetree alias, e.g. 'pwm-led0'. */
74
- readonly dtSpec: string;
83
+ /** Board-shipped DT alias, e.g. 'pwm-led0'. Omit when using the
84
+ * synthesized form (controller + channel). */
85
+ readonly dtSpec?: string;
86
+ /** Synthesized form: PWM controller DT nodelabel, e.g. 'pwm4' (the STM32
87
+ * timer's pwm child node). The overlay's pwm-leds node consumes it. */
88
+ readonly controller?: string;
89
+ /** Synthesized form: channel index within the controller (1-based timer
90
+ * channel, matching the `pwms` binding's channel cell). */
91
+ readonly channel?: number;
92
+ /** Synthesized form: period in nanoseconds, baked into the DT spec. The
93
+ * lowering scales duty against `spec.period`. Default 20 000 000 (20 ms /
94
+ * 50 Hz — the servo convention; harmless for LED dimming). */
95
+ readonly periodNs?: number;
96
+ /** Synthesized form: PWM polarity flag. Default PWM_POLARITY_NORMAL. */
97
+ readonly polarity?: string;
75
98
  }
76
99
 
77
100
  /**
@@ -88,7 +111,7 @@ export interface ZephyrInterruptPin {
88
111
  }
89
112
 
90
113
  /**
91
- * An ADC channel: which SAADC input a given HAL pin maps to.
114
+ * An ADC channel: which ADC input a given HAL pin maps to.
92
115
  *
93
116
  * The XIAO nRF52840 has no pre-declared ADC channel nodes in devicetree, so the
94
117
  * lowering emits `adc_channel_setup` against `DEVICE_DT_GET(DT_NODELABEL(adc))`
@@ -97,8 +120,16 @@ export interface ZephyrInterruptPin {
97
120
  export interface ZephyrAdcChannel {
98
121
  /** GPIO number (matches the HAL op `pin` field). */
99
122
  readonly pin: number;
100
- /** SAADC channel index (AIN0–AIN7). */
123
+ /** ADC channel index (nRF SAADC AIN0–AIN7; STM32 ADC1_IN0–IN9). */
101
124
  readonly channel: number;
125
+ /**
126
+ * Pinctrl node label that muxes this pin to analog mode, e.g.
127
+ * 'adc1_in0_pa0' (STM32). When present, the overlay generator rewrites the
128
+ * ADC node's pinctrl-0 to the channels the program actually reads — SoCs
129
+ * like STM32 leave the pad in GPIO mode otherwise and reads float.
130
+ * Omit on SoCs whose ADC needs no pad muxing (nRF SAADC, RP2040).
131
+ */
132
+ readonly pinctrl?: string;
102
133
  }
103
134
 
104
135
  /**
@@ -134,11 +165,18 @@ export interface ZephyrChipDescriptor {
134
165
  readonly gpioController: string;
135
166
  /**
136
167
  * Per-range GPIO controllers for SoCs that split GPIO across multiple
137
- * devicetree nodes (ESP32-S3: `gpio0` 0–31, `gpio1` 32–48). When present, the
138
- * lowering routes a HAL pin to its owning controller at runtime via the
139
- * emitted `__tc_gpio_dev(pin)` dispatcher; `gpioController` is the fallback.
140
- * Omit on single-controller SoCs (nRF52840, RP2040, …) every pin is on the
141
- * one controller described by `gpioController`.
168
+ * devicetree nodes (ESP32-S3: `gpio0` 0–31, `gpio1` 32–48; STM32: one
169
+ * controller per port `gpioa` 0–15, `gpiob` 16–31, `gpioc` 32–47).
170
+ * When present, the lowering routes a HAL pin to its owning controller at
171
+ * runtime via the emitted `__tc_gpio_dev(pin)` dispatcher; `gpioController`
172
+ * is the fallback. Omit on single-controller SoCs (RP2040, …) — every pin
173
+ * is on the one controller described by `gpioController`.
174
+ *
175
+ * NUMBERING RULE (load-bearing): `minPin` must equal the controller's port
176
+ * base so the port-relative raw index is `pin - minPin` (STM32 PB12 = pin
177
+ * 28 → raw 12 — the Zephyr raw API addresses the index WITHIN the
178
+ * controller). Number pins by port blocks and never contiguously across
179
+ * unbonded pins.
142
180
  */
143
181
  readonly gpioControllers?: readonly ZephyrGpioController[];
144
182
  /** GPIO pins with devicetree specs (LEDs, buttons, board-defined pins). */
@@ -155,7 +193,7 @@ export interface ZephyrChipDescriptor {
155
193
  readonly uart?: { readonly controllers: readonly ZephyrBusController[] };
156
194
  /** PWM channels with DT specs. */
157
195
  readonly pwm?: { readonly specs: readonly ZephyrPwmSpec[] };
158
- /** ADC: the SAADC node label + the pin→channel map. */
196
+ /** ADC: the ADC device node label + the pin→channel map. */
159
197
  readonly adc?: {
160
198
  readonly nodeLabel: string;
161
199
  readonly channels: readonly ZephyrAdcChannel[];
@@ -163,6 +201,19 @@ export interface ZephyrChipDescriptor {
163
201
  readonly vrefMv: number;
164
202
  /** ADC resolution in bits. */
165
203
  readonly resolution: number;
204
+ /**
205
+ * Zephyr `enum adc_gain` macro for the channel setup, e.g.
206
+ * 'ADC_GAIN_1_4' (nRF SAADC default) or 'ADC_GAIN_1' (STM32 driver
207
+ * requires exactly this). Defaults to 'ADC_GAIN_1_4'.
208
+ */
209
+ readonly gain?: string;
210
+ /**
211
+ * Zephyr `enum adc_reference` macro, e.g. 'ADC_REF_INTERNAL'. Defaults to
212
+ * 'ADC_REF_INTERNAL' — on nRF that is the 0.6 V internal ref measured
213
+ * through the gain divider; the STM32 driver ALSO requires
214
+ * ADC_REF_INTERNAL (Zephyr maps it to the VREF+ pad) with vrefMv = VDDA.
215
+ */
216
+ readonly reference?: string;
166
217
  };
167
218
  /**
168
219
  * DAC: the DAC device node label + the pin→channel map. Present only on chips