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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +13 -1
  2. package/dist/chips/controllers.d.ts +28 -8
  3. package/dist/chips/controllers.js +49 -12
  4. package/dist/chips/resolve.js +32 -6
  5. package/dist/chips/types.d.ts +63 -12
  6. package/dist/chips/xiao-ble.js +12 -0
  7. package/dist/dt-config/kconfig.d.ts +11 -0
  8. package/dist/dt-config/kconfig.js +1 -1
  9. package/dist/dt-config/overlay.js +85 -0
  10. package/dist/framework.manifest.d.ts +20 -30
  11. package/dist/framework.manifest.js +12 -3
  12. package/dist/lowering/adc.d.ts +7 -4
  13. package/dist/lowering/adc.js +25 -11
  14. package/dist/lowering/gpio.js +9 -6
  15. package/dist/lowering/mqtt.js +9 -1
  16. package/dist/lowering/pulse.js +7 -7
  17. package/dist/lowering/pwm.d.ts +21 -3
  18. package/dist/lowering/pwm.js +28 -4
  19. package/dist/lowering/spi.js +2 -2
  20. package/dist/lowering/tone.js +3 -2
  21. package/dist/lowering/wifi.js +28 -5
  22. package/dist/strategy.js +56 -4
  23. package/dist/toolchain/debug-config.js +1 -1
  24. package/dist/toolchain/index.d.ts +1 -1
  25. package/dist/toolchain/index.js +83 -8
  26. package/dist/toolchain/scaffold.d.ts +9 -0
  27. package/dist/toolchain/scaffold.js +45 -0
  28. package/dist/toolchain/west-discover.d.ts +4 -1
  29. package/dist/toolchain/west-discover.js +2 -0
  30. package/dist/toolchain/west-spawn.js +17 -5
  31. package/package.json +5 -5
  32. package/src/chips/controllers.ts +61 -12
  33. package/src/chips/resolve.ts +32 -5
  34. package/src/chips/types.ts +63 -12
  35. package/src/chips/xiao-ble.ts +82 -70
  36. package/src/dt-config/kconfig.ts +12 -1
  37. package/src/dt-config/overlay.ts +546 -450
  38. package/src/framework.manifest.ts +12 -3
  39. package/src/lowering/adc.ts +28 -12
  40. package/src/lowering/gpio.ts +9 -6
  41. package/src/lowering/mqtt.ts +9 -1
  42. package/src/lowering/pulse.ts +7 -7
  43. package/src/lowering/pwm.ts +29 -4
  44. package/src/lowering/spi.ts +2 -2
  45. package/src/lowering/tone.ts +3 -3
  46. package/src/lowering/wifi.ts +29 -5
  47. package/src/strategy.ts +52 -4
  48. package/src/toolchain/debug-config.ts +1 -1
  49. package/src/toolchain/index.ts +645 -565
  50. package/src/toolchain/scaffold.ts +43 -0
  51. package/src/toolchain/west-discover.ts +321 -316
  52. package/src/toolchain/west-spawn.ts +17 -5
@@ -19,19 +19,79 @@
19
19
  // ---------------------------------------------------------------------------
20
20
  import { spawnSync } from 'node:child_process';
21
21
  import { basename, dirname, join } from 'node:path';
22
- import { readdirSync, readFileSync, mkdirSync, rmSync } from 'node:fs';
22
+ import { readdirSync, readFileSync, mkdirSync, rmSync, existsSync } from 'node:fs';
23
23
  import { parseCompileErrors } from '@typecad/cuttlefish/api/shared';
24
- import { scaffoldZephyrProject, writeIfChanged } from './scaffold.js';
24
+ import { scaffoldZephyrProject, writeIfChanged, appendLibraryOverlayFragments } from './scaffold.js';
25
25
  import { westSpawn, buildEnv } from './west-spawn.js';
26
26
  import { discoverWest } from './west-discover.js';
27
27
  import { writeDebugConfig, resolveDebugLocations } from './debug-config.js';
28
28
  import { ZephyrStrategy } from '../strategy.js';
29
29
  import { generateOverlay } from '../dt-config/overlay.js';
30
30
  import { chipForTarget } from '../chips/index.js';
31
+ import { resolveChipFromBoard } from '../chips/resolve.js';
32
+ import { pwmDtAliasToken } from '../lowering/pwm.js';
31
33
  import { detectZephyrVersion, checkZephyrCompat, resolveBoardTarget } from './compat.js';
32
34
  import { DEFAULT_ZEPHYR_DISPLAY_PROFILE } from '../display/profiles.js';
33
35
  /** Default board target — the framework's MVP canonical board. */
34
36
  const DEFAULT_BOARD = 'xiao_ble';
37
+ /**
38
+ * Resolve the chip for a build the same way the strategy does at emit time —
39
+ * from the board constants the transpile persisted next to the emitted
40
+ * source (`board-constants.json`), falling back to the hardcoded registry.
41
+ * Board-package chips (rpi_pico, esp32c3/c6, blackpill) exist only in their
42
+ * board packages; the registry fallback would silently resolve them to the
43
+ * XIAO default and the overlay generator would emit wrong controller labels
44
+ * (e.g. `&uart0` on an STM32, whose node is `usart1`).
45
+ */
46
+ function chipForBuild(projectRoot, board) {
47
+ try {
48
+ // The transpile writes the constants into the emit outDir, which is
49
+ // <projectRoot>/src for the standard layout (basename 'src' collapsed by
50
+ // projectRootFromOptions); check both locations.
51
+ const bcPath = [join(projectRoot, 'src', 'board-constants.json'), join(projectRoot, 'board-constants.json')]
52
+ .find(p => existsSync(p));
53
+ if (bcPath) {
54
+ const raw = JSON.parse(readFileSync(bcPath, 'utf8'));
55
+ const fromBoard = resolveChipFromBoard(new Map(Object.entries(raw)));
56
+ if (fromBoard)
57
+ return fromBoard;
58
+ }
59
+ }
60
+ catch { /* fall back to the registry below */ }
61
+ return chipForTarget(board);
62
+ }
63
+ /**
64
+ * HAL pins the emitted sources read via adc.* — scanned from the emitted
65
+ * `__tc_adc<N>_setup()` call sites (N = channel index, mapped back to the HAL
66
+ * pin via the chip descriptor). Feeds the overlay's ADC pinctrl rewrite: on
67
+ * SoCs that mux ADC pads via pinctrl (STM32), only the read channels are
68
+ * switched to analog mode.
69
+ */
70
+ function scanAdcReadPins(src, chip) {
71
+ const pins = [];
72
+ // Match CALL SITES only (`__tc_adc<N>_setup()` with empty parens) — the
73
+ // setup definitions emitted by adcInitLines have a `(void)` parameter list
74
+ // and would otherwise mark every descriptor channel as used.
75
+ for (const m of src.matchAll(/__tc_adc(\d+)_setup\(\)/g)) {
76
+ const ch = Number(m[1]);
77
+ const c = chip.adc?.channels.find((x) => x.channel === ch);
78
+ if (c && !pins.includes(c.pin))
79
+ pins.push(c.pin);
80
+ }
81
+ return pins;
82
+ }
83
+ /**
84
+ * HAL pins the emitted sources drive with pwm.* — the emitted source
85
+ * references each used spec as `__tc_pwm_<alias token>` (pwmVarName in
86
+ * lowering/pwm.ts), and the lowering only emits specs for driven pins, so
87
+ * var-presence is the authoritative signal. Feeds the overlay's per-pin
88
+ * pwm-leds gating (no dead DT channels).
89
+ */
90
+ function scanPwmUsedPins(src, chip) {
91
+ return (chip.pwm?.specs ?? [])
92
+ .filter((s) => src.includes(`__tc_pwm_${pwmDtAliasToken(s)}`))
93
+ .map((s) => s.pin);
94
+ }
35
95
  function targetFromOptions(o) {
36
96
  // The cuttlefish CLI populates ToolchainOptions.buildTarget from
37
97
  // config.frameworkData.buildTarget. Accept frameworkData.target as an alias.
@@ -77,7 +137,7 @@ const FLASH_TIMEOUT_MS = 120_000;
77
137
  * Exported (pure) so the runner-selection contract is unit-testable without
78
138
  * spawning west.
79
139
  */
80
- export function buildFlashArgs(buildDir, board, userRunner, port) {
140
+ export function buildFlashArgs(buildDir, board, userRunner, port, runnerArgs) {
81
141
  const args = ['flash', '-d', buildDir];
82
142
  if (userRunner) {
83
143
  args.push('--runner', userRunner);
@@ -85,6 +145,11 @@ export function buildFlashArgs(buildDir, board, userRunner, port) {
85
145
  if (port && board.startsWith('esp32')) {
86
146
  args.push('--esp-device', port);
87
147
  }
148
+ // Extra runner-specific flags, appended verbatim (west's runner parsers
149
+ // accept them after the runner is selected).
150
+ if (runnerArgs && runnerArgs.length > 0) {
151
+ args.push(...runnerArgs);
152
+ }
88
153
  return args;
89
154
  }
90
155
  /**
@@ -182,7 +247,7 @@ export const Toolchain = {
182
247
  // resolution is a pre-build artifact step.
183
248
  const projectRoot = basename(outputDir) === 'src' ? dirname(outputDir) : outputDir;
184
249
  const board = DEFAULT_BOARD;
185
- const chip = chipForTarget(board);
250
+ const chip = chipForBuild(projectRoot, board);
186
251
  // Scan the emitted source for usage tokens (same authoritative signal the
187
252
  // scaffold uses). entryPoint is the path to main.cpp; its dir is src/.
188
253
  const srcDir = dirname(entryPoint);
@@ -216,6 +281,10 @@ export const Toolchain = {
216
281
  usesI2c: uses('i2c_'),
217
282
  usesSpi: uses('spi_'),
218
283
  usesUart: uses('uart_'),
284
+ usesPwm: uses('pwm_'),
285
+ usesAdc: uses('adc_'),
286
+ adcReadPins: scanAdcReadPins(src, chip),
287
+ pwmUsedPins: scanPwmUsedPins(src, chip),
219
288
  usesDisplay,
220
289
  usesTouch: usesTouch || usesXpt,
221
290
  touchController: usesXpt ? 'xpt2046' : 'ft6336u',
@@ -253,7 +322,7 @@ export const Toolchain = {
253
322
  // the <default>.overlay it wrote does not match `west build -b <board>`.
254
323
  // Zephyr auto-detects boards/<board>.overlay under APPLICATION_CONFIG_DIR.
255
324
  try {
256
- const chip = chipForTarget(board);
325
+ const chip = chipForBuild(projectRoot, board);
257
326
  const srcDir = join(projectRoot, 'src');
258
327
  let src = '';
259
328
  try {
@@ -343,6 +412,10 @@ export const Toolchain = {
343
412
  usesI2c: uses('i2c_'),
344
413
  usesSpi: uses('spi_'),
345
414
  usesUart: uses('uart_'),
415
+ usesPwm: uses('pwm_'),
416
+ usesAdc: uses('adc_'),
417
+ adcReadPins: scanAdcReadPins(src, chip),
418
+ pwmUsedPins: scanPwmUsedPins(src, chip),
346
419
  usesDisplay,
347
420
  usesTouch: uses('ft6336u') || uses('touch_') || usesXpt,
348
421
  touchController: usesXpt ? 'xpt2046' : 'ft6336u',
@@ -356,9 +429,10 @@ export const Toolchain = {
356
429
  // Write the board-specific overlay (the one west loads). Zephyr looks for
357
430
  // boards/<board_id>.overlay under APPLICATION_CONFIG_DIR — use the bare
358
431
  // board id (before any hardware-qualifier suffix, e.g. 'esp32_devkitc'
359
- // not the full 'esp32_devkitc/esp32/procpu' target string).
432
+ // not the full 'esp32_devkitc/esp32/procpu' target string). Library
433
+ // packages' overlay fragments are appended by the scaffold helper.
360
434
  const boardId = board.split('/')[0];
361
- writeIfChanged(join(overlayDir, `${boardId}.overlay`), overlay);
435
+ writeIfChanged(join(overlayDir, `${boardId}.overlay`), appendLibraryOverlayFragments(overlay, projectRoot));
362
436
  }
363
437
  catch { /* best-effort overlay regen; the build surfaces DT errors */ }
364
438
  // Use a stable build dir so incremental builds reuse the Ninja graph.
@@ -465,7 +539,8 @@ export const Toolchain = {
465
539
  const board = targetFromOptions(o);
466
540
  const zc = o.zephyrConfig;
467
541
  const runner = zc?.runner;
468
- const args = buildFlashArgs(buildDir, board, runner, o.port);
542
+ const runnerArgs = zc?.runnerArgs;
543
+ const args = buildFlashArgs(buildDir, board, runner, o.port, runnerArgs);
469
544
  const inv = westSpawn(args, {
470
545
  cwd: projectRoot,
471
546
  encoding: 'utf-8',
@@ -1,6 +1,15 @@
1
1
  /** Write a file only if the content differs from the existing file.
2
2
  * Returns true when the file was written (content changed or file was new). */
3
3
  export declare function writeIfChanged(filePath: string, content: string): boolean;
4
+ /**
5
+ * Append cuttlefish library packages' devicetree overlay fragments to the
6
+ * generated overlay. Library entries come from the transpiler's libraries.json
7
+ * sidecar (next to the emitted sources) and are already gated on the
8
+ * library's include token appearing in the emitted sources — no re-detection.
9
+ * Fragments merge after the framework overlay so library nodes (e.g.
10
+ * @typecad/zephyr-esp32s3-rgb's WS2812 node on I2S0) layer over it.
11
+ */
12
+ export declare function appendLibraryOverlayFragments(overlay: string, projectRoot: string): string;
4
13
  /**
5
14
  * Emit the Zephyr application skeleton around the generated src/main.cpp.
6
15
  *
@@ -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) {
@@ -57,6 +58,31 @@ function readEmittedSources(srcDir) {
57
58
  }
58
59
  return out;
59
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';
78
+ }
79
+ }
80
+ catch {
81
+ // best-effort; a missing fragment surfaces as a DT error
82
+ }
83
+ }
84
+ return out;
85
+ }
60
86
  /**
61
87
  * Emit the Zephyr application skeleton around the generated src/main.cpp.
62
88
  *
@@ -202,6 +228,25 @@ export function scaffoldZephyrProject(projectRoot, debug = false, userKconfig, p
202
228
  continue;
203
229
  prjConf.push(`${sym}=${val}`);
204
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
+ }
205
250
  // Emit user-specified Kconfig from cuttlefish.config.ts zephyr.kconfig.
206
251
  // These override any matching auto-detected symbol (skipped above).
207
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.13",
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.13"
41
+ "@typecad/cuttlefish": "1.0.0-alpha.14"
42
42
  },
43
43
  "devDependencies": {
44
- "@typecad/expect": "1.0.0-alpha.13",
45
- "@typecad/board-xiao-nrf52840": "1.0.0-alpha.13",
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