@typecad/framework-zephyr 1.0.0-alpha.13 → 1.0.0-alpha.15
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.
- package/README.md +22 -35
- package/dist/as-built.d.ts +33 -0
- package/dist/as-built.js +188 -0
- package/dist/async/timer-polyfill.js +1 -1
- package/dist/boardgen.d.ts +128 -0
- package/dist/boardgen.js +1366 -0
- package/dist/chips/board-overrides.d.ts +7 -0
- package/dist/chips/board-overrides.js +11 -0
- package/dist/chips/controllers.d.ts +28 -8
- package/dist/chips/controllers.js +49 -12
- package/dist/chips/esp32s3.js +49 -0
- package/dist/chips/index.d.ts +6 -12
- package/dist/chips/index.js +18 -34
- package/dist/chips/resolve.d.ts +16 -2
- package/dist/chips/resolve.js +298 -15
- package/dist/chips/soc/.d.ts +2 -0
- package/dist/chips/soc/.js +129 -0
- package/dist/chips/soc/esp32.d.ts +2 -0
- package/dist/chips/soc/esp32.js +120 -0
- package/dist/chips/soc/esp32c3.d.ts +2 -0
- package/dist/chips/soc/esp32c3.js +90 -0
- package/dist/chips/soc/esp32c6.d.ts +2 -0
- package/dist/chips/soc/esp32c6.js +109 -0
- package/dist/chips/soc/esp32s3.d.ts +2 -0
- package/dist/chips/soc/esp32s3.js +189 -0
- package/dist/chips/soc/index.d.ts +2 -0
- package/dist/chips/soc/index.js +23 -0
- package/dist/chips/soc/nrf52840.d.ts +2 -0
- package/dist/chips/soc/nrf52840.js +130 -0
- package/dist/chips/soc/rp2040.d.ts +2 -0
- package/dist/chips/soc/rp2040.js +141 -0
- package/dist/chips/soc/rp2350a.d.ts +2 -0
- package/dist/chips/soc/rp2350a.js +145 -0
- package/dist/chips/soc/samd21g18a.d.ts +2 -0
- package/dist/chips/soc/samd21g18a.js +143 -0
- package/dist/chips/soc/stm32f411xe.d.ts +2 -0
- package/dist/chips/soc/stm32f411xe.js +251 -0
- package/dist/chips/types.d.ts +382 -12
- package/dist/chips/xiao-ble.js +33 -0
- package/dist/display/profiles.js +1 -1
- package/dist/doctor.js +38 -4
- package/dist/dt-config/custom-board.d.ts +23 -0
- package/dist/dt-config/custom-board.js +227 -0
- package/dist/dt-config/kconfig.d.ts +83 -1
- package/dist/dt-config/kconfig.js +116 -27
- package/dist/dt-config/overlay.js +623 -18
- package/dist/framework.manifest.d.ts +20 -30
- package/dist/framework.manifest.js +124 -178
- package/dist/index.d.ts +4 -1
- package/dist/index.js +8 -2
- package/dist/lowering/adc.d.ts +26 -5
- package/dist/lowering/adc.js +137 -31
- package/dist/lowering/ble.js +31 -28
- package/dist/lowering/dac.js +9 -9
- package/dist/lowering/fs.js +18 -10
- package/dist/lowering/gpio.d.ts +2 -0
- package/dist/lowering/gpio.js +90 -10
- package/dist/lowering/http.js +39 -14
- package/dist/lowering/hwtimer.d.ts +4 -0
- package/dist/lowering/hwtimer.js +35 -10
- package/dist/lowering/i2c.js +46 -54
- package/dist/lowering/index.d.ts +5 -5
- package/dist/lowering/index.js +15 -16
- package/dist/lowering/interrupts.d.ts +22 -5
- package/dist/lowering/interrupts.js +104 -22
- package/dist/lowering/mqtt.js +12 -6
- package/dist/lowering/power.js +0 -4
- package/dist/lowering/preferences.js +49 -77
- package/dist/lowering/pulse.js +5 -36
- package/dist/lowering/pwm.d.ts +32 -3
- package/dist/lowering/pwm.js +105 -17
- package/dist/lowering/random.js +3 -3
- package/dist/lowering/sensor.d.ts +37 -0
- package/dist/lowering/sensor.js +119 -0
- package/dist/lowering/spi.d.ts +12 -0
- package/dist/lowering/spi.js +65 -40
- package/dist/lowering/thread.d.ts +14 -0
- package/dist/lowering/thread.js +55 -0
- package/dist/lowering/timing.js +20 -46
- package/dist/lowering/tone.js +18 -5
- package/dist/lowering/uart.d.ts +13 -0
- package/dist/lowering/uart.js +72 -51
- package/dist/lowering/usb.d.ts +34 -0
- package/dist/lowering/usb.js +206 -0
- package/dist/lowering/wdt.d.ts +1 -1
- package/dist/lowering/wdt.js +17 -7
- package/dist/lowering/wifi.d.ts +3 -2
- package/dist/lowering/wifi.js +120 -111
- package/dist/sdk/board-catalog-sync.d.ts +16 -0
- package/dist/sdk/board-catalog-sync.js +41 -0
- package/dist/sdk/board-data.generated.d.ts +2 -0
- package/dist/sdk/board-data.generated.js +4 -0
- package/dist/sdk/catalog-walker.d.ts +90 -0
- package/dist/sdk/catalog-walker.js +682 -0
- package/dist/sdk/dts-reader.d.ts +83 -0
- package/dist/sdk/dts-reader.js +596 -0
- package/dist/strategy.d.ts +136 -17
- package/dist/strategy.js +881 -212
- package/dist/toolchain/bossac-touch.d.ts +18 -0
- package/dist/toolchain/bossac-touch.js +88 -0
- package/dist/toolchain/compat.d.ts +2 -2
- package/dist/toolchain/compat.js +8 -3
- package/dist/toolchain/debug-config.d.ts +21 -26
- package/dist/toolchain/debug-config.js +212 -72
- package/dist/toolchain/env-check.d.ts +4 -4
- package/dist/toolchain/env-check.js +5 -5
- package/dist/toolchain/index.d.ts +66 -1
- package/dist/toolchain/index.js +713 -58
- package/dist/toolchain/scaffold.d.ts +9 -0
- package/dist/toolchain/scaffold.js +84 -7
- package/dist/toolchain/west-discover.d.ts +8 -1
- package/dist/toolchain/west-discover.js +19 -6
- package/dist/toolchain/west-spawn.js +17 -5
- package/installer/CHANGELOG.md +473 -0
- package/installer/README.md +328 -0
- package/installer/environment.yml +46 -0
- package/installer/etc/conda/activate.d/zephyr.bat +7 -0
- package/installer/etc/conda/activate.d/zephyr.ps1 +12 -0
- package/installer/etc/conda/activate.d/zephyr.sh +20 -0
- package/installer/etc/conda/deactivate.d/zephyr.bat +6 -0
- package/installer/etc/conda/deactivate.d/zephyr.ps1 +6 -0
- package/installer/etc/conda/deactivate.d/zephyr.sh +4 -0
- package/installer/install.mjs +580 -0
- package/installer/install.ps1 +619 -0
- package/installer/install.sh +310 -0
- package/installer/lib/detect-platform.sh +62 -0
- package/installer/lib/fetch-sdk.sh +304 -0
- package/installer/lib/init-workspace.sh +153 -0
- package/installer/lib/write-activation.sh +76 -0
- package/installer/patches/tfm-c-include-separators.patch +19 -0
- package/installer/patches/zephyr-tfm-nordic-zephyr-include.patch +24 -0
- package/installer/templates/project/.typecad/activate-zephyr.ps1 +31 -0
- package/installer/templates/project/.typecad/activate-zephyr.sh +31 -0
- package/installer/templates/project/.vscode/settings.json +30 -0
- package/installer/templates/project/README.md +58 -0
- package/installer/versions.env +110 -0
- package/package.json +16 -16
- package/src/as-built.ts +206 -0
- package/src/boardgen.ts +1428 -0
- package/src/chips/controllers.ts +61 -12
- package/src/chips/index.ts +35 -54
- package/src/chips/resolve.ts +316 -14
- package/src/chips/types.ts +388 -13
- package/src/display/profiles.ts +1 -1
- package/src/doctor.ts +110 -77
- package/src/dt-config/custom-board.ts +325 -0
- package/src/dt-config/kconfig.ts +177 -19
- package/src/dt-config/overlay.ts +1058 -450
- package/src/framework.manifest.ts +125 -178
- package/src/index.ts +57 -41
- package/src/lowering/adc.ts +222 -102
- package/src/lowering/ble.ts +32 -28
- package/src/lowering/dac.ts +82 -82
- package/src/lowering/fs.ts +18 -10
- package/src/lowering/gpio.ts +242 -152
- package/src/lowering/http.ts +512 -488
- package/src/lowering/hwtimer.ts +139 -101
- package/src/lowering/i2c.ts +116 -126
- package/src/lowering/index.ts +91 -93
- package/src/lowering/interrupts.ts +113 -21
- package/src/lowering/mqtt.ts +12 -6
- package/src/lowering/preferences.ts +327 -354
- package/src/lowering/pwm.ts +167 -74
- package/src/lowering/random.ts +3 -3
- package/src/lowering/sensor.ts +155 -0
- package/src/lowering/spi.ts +164 -124
- package/src/lowering/thread.ts +63 -0
- package/src/lowering/timing.ts +46 -72
- package/src/lowering/uart.ts +73 -51
- package/src/lowering/usb.ts +221 -0
- package/src/lowering/wdt.ts +95 -84
- package/src/lowering/wifi.ts +120 -111
- package/src/sdk/board-catalog-sync.ts +62 -0
- package/src/strategy.ts +888 -229
- package/src/toolchain/bossac-touch.ts +112 -0
- package/src/toolchain/compat.ts +8 -3
- package/src/toolchain/debug-config.ts +666 -522
- package/src/toolchain/env-check.ts +9 -9
- package/src/toolchain/index.ts +765 -44
- package/src/toolchain/scaffold.ts +81 -7
- package/src/toolchain/west-discover.ts +334 -316
- package/src/toolchain/west-spawn.ts +17 -5
- package/src/async/timer-polyfill.ts +0 -107
- package/src/chips/esp32.ts +0 -74
- package/src/chips/esp32s3.ts +0 -57
- package/src/chips/xiao-ble.ts +0 -70
- package/src/lowering/power.ts +0 -70
- package/src/lowering/pulse.ts +0 -93
- package/src/lowering/tone.ts +0 -60
- package/src/lowering/worker-backing.ts +0 -94
- package/src/lowering/worker.ts +0 -17
package/dist/boardgen.js
ADDED
|
@@ -0,0 +1,1366 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// boardgen.ts — project-local board module generator.
|
|
3
|
+
//
|
|
4
|
+
// One path for every board: joins a board record — from the machine-local
|
|
5
|
+
// catalog (generated from the installed Zephyr SDK's tree) or from a
|
|
6
|
+
// CONTRACT (custom PCB spec) — and emits the two artifacts a project
|
|
7
|
+
// carries instead of a board package:
|
|
8
|
+
//
|
|
9
|
+
// .cuttlefish/board.ts — typed pin/bus/LED/BUTTON exports (the virtual
|
|
10
|
+
// @typecad/board module points here)
|
|
11
|
+
// .cuttlefish/board.json — the BoardConstants flat map (pins.all.*,
|
|
12
|
+
// peripherals.*, zephyr.*) the transpiler's
|
|
13
|
+
// resolveChipFromBoard reconstructs its chip
|
|
14
|
+
// view from
|
|
15
|
+
//
|
|
16
|
+
// All boards are equal: there is no curated descriptor tier. GPIO
|
|
17
|
+
// controller tables are DERIVED from the controller names present in the
|
|
18
|
+
// board's facts per vendor-family port conventions; the datasheet pin
|
|
19
|
+
// sweep enumerates those derived ranges; bus instances and the USB device
|
|
20
|
+
// controller come from the board's own &i2c/&spi/&uart/&usbd nodes.
|
|
21
|
+
// Silicon facts that never appear in devicetree (per-pin ADC channels, PWM
|
|
22
|
+
// matrices) are absent for every board alike.
|
|
23
|
+
//
|
|
24
|
+
// Pure: returns the file contents; the caller (cuttlefish config-loader via
|
|
25
|
+
// the strategy hook, or `cuttlefish board regen`) writes them.
|
|
26
|
+
// ----------------------------------------------------------------------------
|
|
27
|
+
import { parseAsBuiltJson } from './as-built.js';
|
|
28
|
+
import { loadBoardCatalogOverlay, GENERATOR_REV, boardRecordFingerprint, factsFingerprint, findBoardInCatalog, socBusLabelsFromTree, } from '@typecad/cuttlefish/board-catalog';
|
|
29
|
+
/**
|
|
30
|
+
* The catalog lookups resolve against the local overlay — the user's own
|
|
31
|
+
* Zephyr tree (`cuttlefish board sync`, auto-refreshed when the tree
|
|
32
|
+
* moves). There is no compiled-in database: a machine with no tree and no
|
|
33
|
+
* overlay has no boards, and the error below says exactly that.
|
|
34
|
+
*/
|
|
35
|
+
function activeBoardData() {
|
|
36
|
+
const data = loadBoardCatalogOverlay()?.data;
|
|
37
|
+
if (!data) {
|
|
38
|
+
throw new Error(`No board catalog on this machine. The catalog is generated from your Zephyr tree —\n` +
|
|
39
|
+
`run 'cuttlefish board sync' (or point CUTTLEFISH_BOARD_CATALOG at a catalog file).`);
|
|
40
|
+
}
|
|
41
|
+
return data;
|
|
42
|
+
}
|
|
43
|
+
/** Look up a board variant by qualified target or bare board id. */
|
|
44
|
+
export function findBoardData(target) {
|
|
45
|
+
return findBoardInCatalog(activeBoardData(), target);
|
|
46
|
+
}
|
|
47
|
+
/** soc name from a qualified target ('board/soc/qual' → 'soc'). */
|
|
48
|
+
export function socOfTarget(target) {
|
|
49
|
+
const parts = target.split('/');
|
|
50
|
+
return parts.length >= 2 ? parts[1] : '';
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* nRF SAADC AIN→P0-pad map per SoC — [channel, pad] pairs (see the synthesis
|
|
54
|
+
* block in buildModule for the verification trail). Undefined for SoCs
|
|
55
|
+
* without a verified map (including every /cpunet variant: the nRF5340
|
|
56
|
+
* network core declares no SAADC).
|
|
57
|
+
*/
|
|
58
|
+
export function nrfSaadcAinPads(soc, identifier) {
|
|
59
|
+
if (identifier.split('/').includes('cpunet'))
|
|
60
|
+
return undefined;
|
|
61
|
+
const table = {
|
|
62
|
+
nrf52832: [[0, 2], [1, 3], [2, 4], [3, 5], [4, 28], [5, 29], [6, 30], [7, 31]],
|
|
63
|
+
nrf52840: [[0, 2], [1, 3], [2, 4], [3, 5], [4, 28], [5, 29], [6, 30], [7, 31]],
|
|
64
|
+
nrf5340: [[0, 4], [1, 5], [2, 6], [3, 7], [4, 25], [5, 26]],
|
|
65
|
+
nrf9160: [[1, 14], [2, 15], [3, 16], [4, 17], [5, 18], [6, 19]],
|
|
66
|
+
nrf9161: [[1, 14], [2, 15], [3, 16], [4, 17], [5, 18], [6, 19]],
|
|
67
|
+
nrf9151: [[1, 14], [2, 15], [3, 16], [4, 17], [5, 18], [6, 19]],
|
|
68
|
+
};
|
|
69
|
+
return table[soc];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* ESP32 DAC channel→GPIO pad map per SoC — [channel, pad] pairs, silicon-
|
|
73
|
+
* fixed (no pinctrl group; the DAC outputs through the RTC IO mux on a fixed
|
|
74
|
+
* pad). Only the original ESP32 and the ESP32-S2 carry the 8-bit DAC; the
|
|
75
|
+
* S3/C3/C6/H2 dropped it. Sourced from the HAL's dac_periph.c
|
|
76
|
+
* (`dac_channel_io_num[]`), like the nRF SAADC table is from the PS.
|
|
77
|
+
*/
|
|
78
|
+
export function esp32DacPins(soc) {
|
|
79
|
+
const table = {
|
|
80
|
+
esp32: [[0, 25], [1, 26]],
|
|
81
|
+
esp32s2: [[0, 17], [1, 18]],
|
|
82
|
+
};
|
|
83
|
+
return table[soc];
|
|
84
|
+
}
|
|
85
|
+
function identOf(name) {
|
|
86
|
+
return name.replace(/\./g, '_').replace(/[^A-Za-z0-9_]/g, '_');
|
|
87
|
+
}
|
|
88
|
+
// ── Family analog config parameters ─────────────────────────────────────────
|
|
89
|
+
// The per-SoC channel-setup values the driver validates against. These are the
|
|
90
|
+
// "accompanying config parameters" that travel WITH the board's manifest —
|
|
91
|
+
// never per-SoC code in the lowering, because the driver rejects anything
|
|
92
|
+
// else at runtime. Sourced per family; a soc not listed keeps the lowering's
|
|
93
|
+
// default. (The coverage ledger tracks which families are still on defaults.)
|
|
94
|
+
const PWM_CLOCK_HZ = {
|
|
95
|
+
// STM32F411: 100 MHz sysclk, APB1 50×2 = APB2 100 → every timer 100 MHz.
|
|
96
|
+
stm32f411: 100_000_000,
|
|
97
|
+
// STM32F401: all-84 clock tree.
|
|
98
|
+
stm32f401: 84_000_000,
|
|
99
|
+
};
|
|
100
|
+
/** ADC channel-setup gain/reference pair per soc-family prefix. */
|
|
101
|
+
const ADC_CHANNEL_CFG = {
|
|
102
|
+
// STM32's adc driver rejects any gain but ADC_GAIN_1 ("Invalid channel
|
|
103
|
+
// gain", -EINVAL) and maps ADC_REF_INTERNAL to the VREF+ pad.
|
|
104
|
+
stm32: { gain: 'ADC_GAIN_1', reference: 'ADC_REF_INTERNAL' },
|
|
105
|
+
// adc_rpi_pico.c hard-rejects any gain but 1 ("Gain is not valid") — 12-bit
|
|
106
|
+
// against VDD, hence REF_VDD_1 + the 3300 mV vref below.
|
|
107
|
+
rp2040: { gain: 'ADC_GAIN_1', reference: 'ADC_REF_VDD_1' },
|
|
108
|
+
rp2350: { gain: 'ADC_GAIN_1', reference: 'ADC_REF_VDD_1' },
|
|
109
|
+
};
|
|
110
|
+
function namingConvFor(soc) {
|
|
111
|
+
const s = soc.toLowerCase();
|
|
112
|
+
if (/^nrf5|^nrf9/.test(s))
|
|
113
|
+
return 'nrf-port';
|
|
114
|
+
if (/^rp2/.test(s))
|
|
115
|
+
return 'rp-gpio';
|
|
116
|
+
if (/^esp32/.test(s))
|
|
117
|
+
return 'esp32-gpio';
|
|
118
|
+
return 'family';
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Controller names that are I2C/SPI GPIO expanders, not the SoC's GPIO.
|
|
122
|
+
* Pins behind them ride the devicetree alias path (LED/BUTTON dtSpecs) and
|
|
123
|
+
* cannot be placed on the HAL pin map — exclude them from the derived
|
|
124
|
+
* controller table and from pin placement.
|
|
125
|
+
*/
|
|
126
|
+
function isExpanderController(name) {
|
|
127
|
+
return /^(gpio_exp\d+|sx1509b|mfx|cy8c95xx_port\d+)$/.test(name);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Port-block range for a DTS GPIO controller name, per the vendor family's
|
|
131
|
+
* port convention. Widths are pinned empirically against the board catalog
|
|
132
|
+
* (max observed bit per family): Renesas RA `ioport<N>` ports are 16 bits
|
|
133
|
+
* with letters continuing past ioport9 (a = port 10); Atmel
|
|
134
|
+
* `port<letter>`/`pio<letter>` are 32-bit ports; Cypress `gpio_prt<N>` are
|
|
135
|
+
* 8-bit ports; Ambiq Apollo splits banks into range-encoded `gpio<lo>_<hi>`
|
|
136
|
+
* nodes; bare `gpio<digit>` is the nRF/ESP-style 32-pin controller. The
|
|
137
|
+
* `gpio<letter>` ports are handled by the caller (deriveControllers) —
|
|
138
|
+
* their width is fact-driven, not derivable from the name alone. Returns
|
|
139
|
+
* undefined when the name encodes no derivable range.
|
|
140
|
+
*/
|
|
141
|
+
function controllerRangeFor(name) {
|
|
142
|
+
const A = 'a'.charCodeAt(0);
|
|
143
|
+
const letterIndex = (ch) => ch.charCodeAt(0) - A;
|
|
144
|
+
let m = name.match(/^gpio(\d+)_(\d+)$/); // Ambiq gpio0_31, gpio96_127
|
|
145
|
+
if (m)
|
|
146
|
+
return { nodelabel: name, minPin: parseInt(m[1], 10), maxPin: parseInt(m[2], 10) };
|
|
147
|
+
m = name.match(/^gpio([a-z])_(\d+)_(\d+)$/); // SiFli gpioa_00_31, gpioa_32_44
|
|
148
|
+
if (m)
|
|
149
|
+
return { nodelabel: name, minPin: parseInt(m[2], 10), maxPin: parseInt(m[3], 10) };
|
|
150
|
+
m = name.match(/^gpio(\d+)x(\d+)x$/); // NXP KB combined banks gpio0x1x
|
|
151
|
+
if (m) {
|
|
152
|
+
return { nodelabel: name, minPin: parseInt(m[1], 10) * 32, maxPin: parseInt(m[2], 10) * 32 + 31 };
|
|
153
|
+
}
|
|
154
|
+
m = name.match(/^gpio_prt(\d+)$/); // Cypress/Infineon 8-pin ports
|
|
155
|
+
if (m) {
|
|
156
|
+
const base = parseInt(m[1], 10) * 8;
|
|
157
|
+
return { nodelabel: name, minPin: base, maxPin: base + 7 };
|
|
158
|
+
}
|
|
159
|
+
m = name.match(/^ioport(\d+)$/); // Renesas RA 16-pin ports
|
|
160
|
+
if (m) {
|
|
161
|
+
const base = parseInt(m[1], 10) * 16;
|
|
162
|
+
return { nodelabel: name, minPin: base, maxPin: base + 15 };
|
|
163
|
+
}
|
|
164
|
+
m = name.match(/^ioport([a-z])$/); // RA ports past 9 (a = port 10)
|
|
165
|
+
if (m) {
|
|
166
|
+
const base = (10 + letterIndex(m[1])) * 16;
|
|
167
|
+
return { nodelabel: name, minPin: base, maxPin: base + 15 };
|
|
168
|
+
}
|
|
169
|
+
m = name.match(/^(?:port|pio)([a-z])$/); // Atmel SAM 32-pin ports
|
|
170
|
+
if (m) {
|
|
171
|
+
const base = letterIndex(m[1]) * 32;
|
|
172
|
+
return { nodelabel: name, minPin: base, maxPin: base + 31 };
|
|
173
|
+
}
|
|
174
|
+
m = name.match(/^gpio([a-z])_(l|h)$/); // NXP MCX/S32K low/high register banks
|
|
175
|
+
if (m) {
|
|
176
|
+
const base = letterIndex(m[1]) * 32 + (m[2] === 'h' ? 16 : 0);
|
|
177
|
+
return { nodelabel: name, minPin: base, maxPin: base + 15 };
|
|
178
|
+
}
|
|
179
|
+
m = name.match(/^gpio_([a-z])$/); // NXP MCX gpio_a — 32-pin ports
|
|
180
|
+
if (m) {
|
|
181
|
+
const base = letterIndex(m[1]) * 32;
|
|
182
|
+
return { nodelabel: name, minPin: base, maxPin: base + 31 };
|
|
183
|
+
}
|
|
184
|
+
m = name.match(/^gpioa(\d+)$/); // TI CC32xx gpioa0…
|
|
185
|
+
if (m) {
|
|
186
|
+
const base = parseInt(m[1], 10) * 32;
|
|
187
|
+
return { nodelabel: name, minPin: base, maxPin: base + 31 };
|
|
188
|
+
}
|
|
189
|
+
m = name.match(/^mcu_gpio(\d+)$/); // TI K3 MCU domain — offset past the main domain space
|
|
190
|
+
if (m) {
|
|
191
|
+
const base = 256 + parseInt(m[1], 10) * 32;
|
|
192
|
+
return { nodelabel: name, minPin: base, maxPin: base + 31 };
|
|
193
|
+
}
|
|
194
|
+
m = name.match(/^gpio(\d+)_hi$/); // RP2350B high bank (gpio0_hi = pins 32…)
|
|
195
|
+
if (m) {
|
|
196
|
+
const base = (parseInt(m[1], 10) + 1) * 32;
|
|
197
|
+
return { nodelabel: name, minPin: base, maxPin: base + 31 };
|
|
198
|
+
}
|
|
199
|
+
m = name.match(/^(?:main_)?gpio(\d+)$/); // nRF/ESP/RP + TI main domain
|
|
200
|
+
if (m) {
|
|
201
|
+
const base = parseInt(m[1], 10) * 32;
|
|
202
|
+
return { nodelabel: name, minPin: base, maxPin: base + 31 };
|
|
203
|
+
}
|
|
204
|
+
m = name.match(/^hsgpio(\d+)$/); // RW612
|
|
205
|
+
if (m) {
|
|
206
|
+
const base = parseInt(m[1], 10) * 32;
|
|
207
|
+
return { nodelabel: name, minPin: base, maxPin: base + 31 };
|
|
208
|
+
}
|
|
209
|
+
m = name.match(/^psgpio_bank(\d+)$/); // Zynq PS banks
|
|
210
|
+
if (m) {
|
|
211
|
+
const base = parseInt(m[1], 10) * 32;
|
|
212
|
+
return { nodelabel: name, minPin: base, maxPin: base + 31 };
|
|
213
|
+
}
|
|
214
|
+
if (name === 'gpio') {
|
|
215
|
+
// Flat single-controller boards (musca, neorv32, quicklogic).
|
|
216
|
+
return { nodelabel: name, minPin: 0, maxPin: 31 };
|
|
217
|
+
}
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* GPIO controller table for a board: derived from the controller names
|
|
222
|
+
* present in the board's facts per vendor-family port conventions. Names
|
|
223
|
+
* that encode no derivable range but are real controllers (the MPS2
|
|
224
|
+
* boards' dedicated gpio_led0/gpio_button FPGAIO IPs, the Pi 5's gio_aon
|
|
225
|
+
* bank) get opaque high bases — collision-free by construction, and
|
|
226
|
+
* LED/BUTTON ride the devicetree alias path anyway. Expanders are excluded
|
|
227
|
+
* (isExpanderController). Returns the table plus the OPAQUE controller
|
|
228
|
+
* names (excluded from the datasheet pin sweep).
|
|
229
|
+
*/
|
|
230
|
+
function deriveControllers(entry) {
|
|
231
|
+
// controller → highest bit any fact places on it
|
|
232
|
+
const maxBits = new Map();
|
|
233
|
+
const note = (controller, pin) => {
|
|
234
|
+
if (isExpanderController(controller))
|
|
235
|
+
return;
|
|
236
|
+
maxBits.set(controller, Math.max(maxBits.get(controller) ?? -1, pin));
|
|
237
|
+
};
|
|
238
|
+
if (entry.led)
|
|
239
|
+
note(entry.led.controller, entry.led.pin);
|
|
240
|
+
if (entry.button)
|
|
241
|
+
note(entry.button.controller, entry.button.pin);
|
|
242
|
+
// Silicon routes: the board's own DTS includes its PACKAGE-specific
|
|
243
|
+
// pinctrl file (stm32f411c(c-e)ux-pinctrl.dtsi — the (c-e)ux package), so
|
|
244
|
+
// a harvested route is the board declaring that pad exists — the same
|
|
245
|
+
// board-equal standing as a led fact, not curated data. Without this, a
|
|
246
|
+
// port no led/button/connector happens to sit on exports no PWM/ADC
|
|
247
|
+
// routes (the blackpill lost 12 of 30 PWM routes and PB's ADC channels
|
|
248
|
+
// to exactly this).
|
|
249
|
+
for (const r of [...(entry.pwmPins ?? []), ...(entry.adcPins ?? []), ...(entry.dacPins ?? [])]) {
|
|
250
|
+
note(`gpio${r.port.toLowerCase()}`, r.bit);
|
|
251
|
+
}
|
|
252
|
+
// ESP32 numbered-GPIO family: routes carry global pads (no port letters);
|
|
253
|
+
// the gpio0 convention covers 0-48 on these SoCs.
|
|
254
|
+
for (const r of (entry.espAdc ?? []).map((x) => x.pad).concat(entry.pwmMatrix?.pads ?? [])) {
|
|
255
|
+
note('gpio0', r);
|
|
256
|
+
}
|
|
257
|
+
if (entry.stripLed)
|
|
258
|
+
note(entry.stripLed.controller, entry.stripLed.pin);
|
|
259
|
+
for (const c of entry.extraLeds ?? [])
|
|
260
|
+
note(c.controller, c.pin);
|
|
261
|
+
for (const c of entry.extraButtons ?? [])
|
|
262
|
+
note(c.controller, c.pin);
|
|
263
|
+
for (const conn of entry.connectors ?? []) {
|
|
264
|
+
for (const ref of Object.values(conn.pins))
|
|
265
|
+
note(ref.controller, ref.pin);
|
|
266
|
+
}
|
|
267
|
+
// Overlay io-channel wiring contributes the same standing: the overlay's
|
|
268
|
+
// gpio-map is the board author declaring those pads (Renesas RA boards
|
|
269
|
+
// carry their ENTIRE connector only in board-dir overlays).
|
|
270
|
+
for (const r of entry.connectorAdc ?? [])
|
|
271
|
+
note(r.controller, r.pin);
|
|
272
|
+
// ── SoC gpio-controller inventory (the "every pad" source) ──────────────
|
|
273
|
+
// Seed the table with EVERY controller the SoC dtsi declares, so a port the
|
|
274
|
+
// board's own facts never name still sweeps its full range. The board-fact
|
|
275
|
+
// `note` calls above refine widths (a bit ≥ 16 widens a letter port);
|
|
276
|
+
// `ngpios`, when the dtsi states one, provides the baseline width for
|
|
277
|
+
// families whose port convention isn't derivable from the name alone.
|
|
278
|
+
for (const c of entry.gpioControllers ?? []) {
|
|
279
|
+
if (isExpanderController(c.nodelabel))
|
|
280
|
+
continue;
|
|
281
|
+
if (maxBits.has(c.nodelabel))
|
|
282
|
+
continue;
|
|
283
|
+
maxBits.set(c.nodelabel, c.ngpios !== undefined ? c.ngpios - 1 : 0);
|
|
284
|
+
}
|
|
285
|
+
const derived = [];
|
|
286
|
+
const opaque = new Set();
|
|
287
|
+
// Letter-port family (gpioa…gpiop): STM32-class ports are 16 bits wide,
|
|
288
|
+
// but several vendors reuse the same controller names with 32-bit ports
|
|
289
|
+
// (TI MSPM0 gpiob.22, NXP Kinetis gpioe.31, OpenISA RV32M1 gpioa.31). A
|
|
290
|
+
// hard-coded 16-wide range placed those bits outside every controller
|
|
291
|
+
// range, and the raw-GPIO lowering then fell back to a nonexistent
|
|
292
|
+
// `gpio0` nodelabel (undefined `__device_dts_ord_…` at C++ compile time).
|
|
293
|
+
// Size each port 32 bits when an observed fact uses bit ≥ 16, and hand
|
|
294
|
+
// out bases sequentially so a widened port never overlaps its neighbours
|
|
295
|
+
// (boards whose facts all sit at bits ≤ 15 keep the 16-wide letterIndex
|
|
296
|
+
// numbering; contiguous-from-a boards reproduce it exactly).
|
|
297
|
+
let letterBase = 0;
|
|
298
|
+
for (const name of [...maxBits.keys()].filter((n) => /^gpio[a-p]$/.test(n)).sort()) {
|
|
299
|
+
const width = (maxBits.get(name) ?? 0) >= 16 ? 32 : 16;
|
|
300
|
+
derived.push({ nodelabel: name, minPin: letterBase, maxPin: letterBase + width - 1 });
|
|
301
|
+
letterBase += width;
|
|
302
|
+
}
|
|
303
|
+
for (const name of maxBits.keys()) {
|
|
304
|
+
if (/^gpio[a-p]$/.test(name))
|
|
305
|
+
continue;
|
|
306
|
+
const range = controllerRangeFor(name);
|
|
307
|
+
if (range)
|
|
308
|
+
derived.push(range);
|
|
309
|
+
else
|
|
310
|
+
opaque.add(name);
|
|
311
|
+
}
|
|
312
|
+
// Opaque controllers: deterministic sequential slots far above any
|
|
313
|
+
// derived range so HAL pin numbers never collide.
|
|
314
|
+
[...opaque].sort().forEach((name, i) => {
|
|
315
|
+
derived.push({ nodelabel: name, minPin: 4096 + i * 32, maxPin: 4096 + i * 32 + 31 });
|
|
316
|
+
});
|
|
317
|
+
derived.sort((a, b) => a.minPin - b.minPin);
|
|
318
|
+
return { controllers: derived, opaque };
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Pin name + HAL number for a (controller, bit) fact, consistent with the
|
|
322
|
+
* derived controller table's numbering. Family-aware per the board's
|
|
323
|
+
* naming convention: ioport1.3 → P103, gpio0_31.5 → GPIO5, porta.23 →
|
|
324
|
+
* PA23, gpio_prt5.6 → P5_6, gpio0.26 → GPIO26 (or P0.26 on nRF / GP26 on
|
|
325
|
+
* RP2040 per the soc's convention). Undefined when the controller is an
|
|
326
|
+
* expander or encodes no range and has no opaque slot.
|
|
327
|
+
*/
|
|
328
|
+
function placePin(conv, ref, table) {
|
|
329
|
+
if (isExpanderController(ref.controller))
|
|
330
|
+
return undefined;
|
|
331
|
+
const ctrl = table.find((c) => c.nodelabel === ref.controller);
|
|
332
|
+
if (!ctrl)
|
|
333
|
+
return undefined;
|
|
334
|
+
const placed = (name) => ({ name, ident: identOf(name), halPin: ctrl.minPin + ref.pin });
|
|
335
|
+
// Range-encoded families (Ambiq gpio0_31, SiFli gpioa_00_31): the bit is
|
|
336
|
+
// the global pin offset by the range base.
|
|
337
|
+
if (/^gpio[a-z]?_\d+_\d+$/.test(ref.controller))
|
|
338
|
+
return placed(`GPIO${ctrl.minPin + ref.pin}`);
|
|
339
|
+
if (ref.controller === 'gpio')
|
|
340
|
+
return placed(`GPIO${ref.pin}`);
|
|
341
|
+
let m = ref.controller.match(/^ioport([0-9]|[a-z])$/);
|
|
342
|
+
if (m) {
|
|
343
|
+
const port = /[0-9]/.test(m[1]) ? m[1] : String(10 + m[1].charCodeAt(0) - 'a'.charCodeAt(0));
|
|
344
|
+
return placed(`P${port}${String(ref.pin).padStart(2, '0')}`);
|
|
345
|
+
}
|
|
346
|
+
// Letter-port families (Atmel porta/pioa, NXP gpio_a, STM32 gpioc) and
|
|
347
|
+
// the MCX/S32K low/high register banks (gpioc_h.0 → PC16).
|
|
348
|
+
m = ref.controller.match(/^(?:port|pio)([a-z])$/);
|
|
349
|
+
if (m)
|
|
350
|
+
return placed(`P${m[1].toUpperCase()}${ref.pin}`);
|
|
351
|
+
m = ref.controller.match(/^gpio_?([a-p])$/);
|
|
352
|
+
if (m)
|
|
353
|
+
return placed(`P${m[1].toUpperCase()}${ref.pin}`);
|
|
354
|
+
m = ref.controller.match(/^gpio([a-z])_(l|h)$/);
|
|
355
|
+
if (m)
|
|
356
|
+
return placed(`P${m[1].toUpperCase()}${(m[2] === 'h' ? 16 : 0) + ref.pin}`);
|
|
357
|
+
m = ref.controller.match(/^gpio_prt(\d+)$/);
|
|
358
|
+
if (m)
|
|
359
|
+
return placed(`P${m[1]}_${ref.pin}`);
|
|
360
|
+
m = ref.controller.match(/^gpioa(\d+)$/);
|
|
361
|
+
if (m)
|
|
362
|
+
return placed(`PA${m[1]}${ref.pin}`);
|
|
363
|
+
// Digit controllers (gpio0, gpio0_hi, main_gpio0, gpio0x1x, hsgpio…):
|
|
364
|
+
// the HAL number is the global pin (base + bit), named per the soc's
|
|
365
|
+
// convention — nRF P0.05, RP2040 GP5, Espressif/everything else GPIO5.
|
|
366
|
+
if (/gpio\d/.test(ref.controller)) {
|
|
367
|
+
if (conv === 'nrf-port') {
|
|
368
|
+
const port = (ctrl.minPin / 32) | 0;
|
|
369
|
+
return placed(`P${port}.${String(ref.pin).padStart(2, '0')}`);
|
|
370
|
+
}
|
|
371
|
+
if (conv === 'rp-gpio')
|
|
372
|
+
return placed(`GP${ctrl.minPin + ref.pin}`);
|
|
373
|
+
return placed(`GPIO${ctrl.minPin + ref.pin}`);
|
|
374
|
+
}
|
|
375
|
+
// Opaque controllers (gpio_led0, gio_aon, …): generic label-based name.
|
|
376
|
+
const letters = ref.controller.replace(/[^a-z]/gi, '').toUpperCase().replace('GPIO', '');
|
|
377
|
+
const name = letters.length > 0 ? `P${letters}${ref.pin}` : `${ref.controller.toUpperCase()}${ref.pin}`;
|
|
378
|
+
return placed(name);
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Parse a datasheet pin name into (controller nodelabel, controller-relative
|
|
382
|
+
* bit) per the soc's naming family — the inverse of the sweep naming:
|
|
383
|
+
* nRF P0.28 / P0_28 → gpio0.28
|
|
384
|
+
* RP2040 GP25 → gpio0.25
|
|
385
|
+
* ESP32 GPIO9 → gpio0.9 (GPIO32+ → gpio1.x)
|
|
386
|
+
* letters PA5 / PB6 → gpioa.5 / gpiob.6
|
|
387
|
+
* flat GPIO9 (non-esp32 socs, single flat controller) → gpio.9
|
|
388
|
+
*/
|
|
389
|
+
export function parsePinName(soc, name) {
|
|
390
|
+
const conv = namingConvFor(soc);
|
|
391
|
+
const n = name.trim().toUpperCase();
|
|
392
|
+
let m = n.match(/^P(\d)\.(\d{1,2})$/);
|
|
393
|
+
if (conv === 'nrf-port' && m)
|
|
394
|
+
return { controller: `gpio${m[1]}`, pin: Number(m[2]) };
|
|
395
|
+
m = n.match(/^GP(\d{1,2})$/);
|
|
396
|
+
if (conv === 'rp-gpio' && m)
|
|
397
|
+
return { controller: 'gpio0', pin: Number(m[1]) };
|
|
398
|
+
m = n.match(/^GPIO(\d{1,2})$/);
|
|
399
|
+
if (m) {
|
|
400
|
+
const num = Number(m[1]);
|
|
401
|
+
if (conv === 'esp32-gpio')
|
|
402
|
+
return { controller: num < 32 ? 'gpio0' : 'gpio1', pin: num < 32 ? num : num - 32 };
|
|
403
|
+
return { controller: 'gpio', pin: num };
|
|
404
|
+
}
|
|
405
|
+
m = n.match(/^P([A-Pa-p])(\d{1,2})$/);
|
|
406
|
+
if (m)
|
|
407
|
+
return { controller: `gpio${m[1].toLowerCase()}`, pin: Number(m[2]) };
|
|
408
|
+
return undefined;
|
|
409
|
+
}
|
|
410
|
+
/** Parse + shape-validate the facts file; a clear error names the file. */
|
|
411
|
+
export function parseUserFactsJson(text, source = 'cuttlefish.facts.json') {
|
|
412
|
+
let parsed;
|
|
413
|
+
try {
|
|
414
|
+
parsed = JSON.parse(text);
|
|
415
|
+
}
|
|
416
|
+
catch (err) {
|
|
417
|
+
throw new Error(`${source} is not valid JSON: ${err.message}`);
|
|
418
|
+
}
|
|
419
|
+
const file = parsed;
|
|
420
|
+
if (!file || typeof file !== 'object' || !file.boards || typeof file.boards !== 'object') {
|
|
421
|
+
throw new Error(`${source} must carry a top-level "boards" object keyed by board target.`);
|
|
422
|
+
}
|
|
423
|
+
for (const [key, facts] of Object.entries(file.boards)) {
|
|
424
|
+
if (facts.adc && !Array.isArray(facts.adc.channels)) {
|
|
425
|
+
throw new Error(`${source}: boards['${key}'].adc.channels must be an array.`);
|
|
426
|
+
}
|
|
427
|
+
if (facts.pwm && !Array.isArray(facts.pwm.specs)) {
|
|
428
|
+
throw new Error(`${source}: boards['${key}'].pwm.specs must be an array.`);
|
|
429
|
+
}
|
|
430
|
+
if (facts.dac && (!facts.dac.device || !Array.isArray(facts.dac.channels))) {
|
|
431
|
+
throw new Error(`${source}: boards['${key}'].dac needs a device and a channels array.`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return file;
|
|
435
|
+
}
|
|
436
|
+
/** The section matching a target — the same exact → lowercase → prefix
|
|
437
|
+
* leniency findBoardData applies to catalog keys. */
|
|
438
|
+
export function userFactsForTarget(file, target) {
|
|
439
|
+
if (!file)
|
|
440
|
+
return undefined;
|
|
441
|
+
const keys = Object.keys(file.boards);
|
|
442
|
+
const hit = keys.find((k) => k === target)
|
|
443
|
+
?? keys.find((k) => k.toLowerCase() === target.toLowerCase())
|
|
444
|
+
?? keys.find((k) => target.startsWith(k) || k.startsWith(target));
|
|
445
|
+
return hit ? file.boards[hit] : undefined;
|
|
446
|
+
}
|
|
447
|
+
export function buildModule(entry, userFacts, factsSuffix = '', seedWarnings = []) {
|
|
448
|
+
const soc = socOfTarget(entry.identifier);
|
|
449
|
+
const conv = namingConvFor(soc);
|
|
450
|
+
// ── Controller table + datasheet sweep (the same for every board) ──────
|
|
451
|
+
const { controllers, opaque } = deriveControllers(entry);
|
|
452
|
+
// ── Silicon PWM/analog routes (SoC pinctrl harvest) ────────────────────
|
|
453
|
+
// Raw port/bit routes from the catalog (STM32 vendor-HAL pinctrl dtsi;
|
|
454
|
+
// matrix/arithmetic families — ESP32 LEDC, RP2040 slices — ship no
|
|
455
|
+
// pwmPins and remain family conventions). A route survives only when its
|
|
456
|
+
// pad's controller is in the derived table: a port the board's facts
|
|
457
|
+
// never reference is not exported, silicon route or not — all boards
|
|
458
|
+
// equal. First timer per pad wins (lowest timer number, then channel).
|
|
459
|
+
const globalForPort = (port, bit) => {
|
|
460
|
+
// Letter-port controller naming varies by vendor: STM32/sam0 styles are
|
|
461
|
+
// gpio<letter> (gpioa), Atmel SAM's is port<letter> (porta) — both are
|
|
462
|
+
// the same letter-port convention, matched by whatever the derived
|
|
463
|
+
// table actually contains.
|
|
464
|
+
const lower = port.toLowerCase();
|
|
465
|
+
const ctrl = controllers.find((c) => c.nodelabel === `gpio${lower}` || c.nodelabel === `port${lower}`);
|
|
466
|
+
// The harvested bit is PORT-RELATIVE (PB6 → bit 6), so the guard is the
|
|
467
|
+
// controller's width, not its global range — a `bit >= minPin` check
|
|
468
|
+
// silently dropped every route on any port after the first.
|
|
469
|
+
return ctrl && bit >= 0 && bit <= ctrl.maxPin - ctrl.minPin ? ctrl.minPin + bit : undefined;
|
|
470
|
+
};
|
|
471
|
+
const siliconPwm = [];
|
|
472
|
+
const pwmSeenPad = new Set();
|
|
473
|
+
for (const p of [...(entry.pwmPins ?? [])].sort((a, b) => a.source.localeCompare(b.source, undefined, { numeric: true }) || a.channel - b.channel)) {
|
|
474
|
+
// The route's controller: STM32 spells the pinctrl source `tim{N}` with
|
|
475
|
+
// the DT convention mapping onto the `pwm{N}` child under `timers{N}`;
|
|
476
|
+
// every other family harvested so far (Kinetis `ftmN`, LPC `ctimerN`,
|
|
477
|
+
// GD32 `timerN`, i.MX `flexpwmN_pwmK`) uses the DT controller label
|
|
478
|
+
// VERBATIM as the source.
|
|
479
|
+
const timer = p.source.match(/^tim(\d+)$/);
|
|
480
|
+
const controller = timer ? `pwm${timer[1]}` : p.source;
|
|
481
|
+
if (!controller)
|
|
482
|
+
continue;
|
|
483
|
+
const pin = globalForPort(p.port, p.bit);
|
|
484
|
+
if (pin === undefined || pwmSeenPad.has(pin))
|
|
485
|
+
continue;
|
|
486
|
+
pwmSeenPad.add(pin);
|
|
487
|
+
siliconPwm.push({ pin, controller, channel: p.channel, pinctrl: p.pinctrl });
|
|
488
|
+
}
|
|
489
|
+
// Cross-check: only routes whose source DEVICE the SoC declares (a
|
|
490
|
+
// pinctrl file can carry routes for peripherals the dtsi never defines).
|
|
491
|
+
// EVERY declared controller contributes its routes — STM32 adc1/adc2 (and
|
|
492
|
+
// adc3 on bigger parts) share channel indices per controller, so each
|
|
493
|
+
// route carries its owning controller. The primary (first sorted) source
|
|
494
|
+
// is the descriptor's nodeLabel; its channels omit the controller field
|
|
495
|
+
// so a single-controller manifest is unchanged.
|
|
496
|
+
const analogDevices = new Set(entry.analogDevices ?? []);
|
|
497
|
+
// Digitless single-controller pinctrl spelling (STM32 f0/l0/l1/wl write
|
|
498
|
+
// `adc_in0_pa0`) maps onto the device label the SoC dtsi actually
|
|
499
|
+
// declares (`adc1`), so the cross-check and the nodeLabel stay honest.
|
|
500
|
+
const adcDeviceLabels = [...analogDevices].filter((d) => /^adc/.test(d)).sort();
|
|
501
|
+
const adcRoutes = (entry.adcPins ?? []).map((a) => a.source === 'adc' && adcDeviceLabels.length > 0 ? { ...a, source: adcDeviceLabels[0] } : a);
|
|
502
|
+
const adcSources = [...new Set(adcRoutes.map((a) => a.source))]
|
|
503
|
+
.filter((src) => analogDevices.size === 0 || analogDevices.has(src)).sort();
|
|
504
|
+
const siliconAdc = [];
|
|
505
|
+
const adcSeenPad = new Set();
|
|
506
|
+
for (const a of adcRoutes
|
|
507
|
+
.filter((x) => adcSources.includes(x.source))
|
|
508
|
+
.sort((x, y) => x.source.localeCompare(y.source, undefined, { numeric: true }) || x.channel - y.channel)) {
|
|
509
|
+
const pin = globalForPort(a.port, a.bit);
|
|
510
|
+
if (pin === undefined || adcSeenPad.has(pin))
|
|
511
|
+
continue;
|
|
512
|
+
adcSeenPad.add(pin);
|
|
513
|
+
siliconAdc.push({
|
|
514
|
+
pin,
|
|
515
|
+
channel: a.channel,
|
|
516
|
+
...(a.source !== adcSources[0] ? { controller: a.source } : {}),
|
|
517
|
+
...(a.pinctrl ? { pinctrl: a.pinctrl } : {}),
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
// ESP32 SARADC routes (pad form, no pinctrl groups — the pads are analog
|
|
521
|
+
// by silicon). Same every-declared-unit rule: each unit's routes carry the
|
|
522
|
+
// unit as their controller; the first unit stays the implicit primary.
|
|
523
|
+
const espAdcUnits = [...new Set((entry.espAdc ?? []).map((r) => r.source))]
|
|
524
|
+
.filter((src) => analogDevices.size === 0 || analogDevices.has(src)).sort();
|
|
525
|
+
const espAdcUnit = espAdcUnits[0];
|
|
526
|
+
for (const r of [...(entry.espAdc ?? [])]
|
|
527
|
+
.filter((x) => espAdcUnits.includes(x.source))
|
|
528
|
+
.sort((x, y) => x.source.localeCompare(y.source, undefined, { numeric: true }) || x.channel - y.channel)) {
|
|
529
|
+
const ctrl = controllers.find((c) => c.nodelabel === 'gpio0');
|
|
530
|
+
if (!ctrl || r.pad > ctrl.maxPin - ctrl.minPin)
|
|
531
|
+
continue;
|
|
532
|
+
siliconAdc.push({
|
|
533
|
+
pin: ctrl.minPin + r.pad,
|
|
534
|
+
channel: r.channel,
|
|
535
|
+
...(r.source !== espAdcUnit ? { controller: r.source } : {}),
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
// nRF SAADC: Nordic silicon has no per-pad ADC devicetree — the AIN
|
|
539
|
+
// index IS the driver channel and its pad is silicon-fixed (the XIAO's
|
|
540
|
+
// board DTS carries no ADC node at all). Family tables, same shape as
|
|
541
|
+
// PWM_CLOCK_HZ, all verified against the in-tree DK io-channel-map
|
|
542
|
+
// comments. Entries with no agreeing in-tree source are OMITTED, not
|
|
543
|
+
// guessed — a missing channel just leaves that pad non-analog in the
|
|
544
|
+
// facts:
|
|
545
|
+
// nrf52832/nrf52840: AIN0-7 = P0.02-P0.05, P0.28-P0.31 (nrf52840dk,
|
|
546
|
+
// nrf52dk; AIN0/AIN3 complete the P0.02-P0.05 block per the PS).
|
|
547
|
+
// nrf5340: AIN0-5 = P0.04-P0.07, P0.25-P0.26 (nrf5340dk, nrf7002dk).
|
|
548
|
+
// AIN6/AIN7 pads have no agreeing source — the audio DK's comments
|
|
549
|
+
// conflict with both DKs — omitted until verified. App core only:
|
|
550
|
+
// the netcore dtsi declares no SAADC.
|
|
551
|
+
// nrf9160/nrf9161/nrf9151: AIN1-6 = P0.14-P0.19 (nrf9160dk, nrf9161dk,
|
|
552
|
+
// nrf9151dk — three independent boards agree). AIN0/AIN7 pads are
|
|
553
|
+
// undocumented in-tree — omitted.
|
|
554
|
+
// nRF54L declares no nordic,nrf-saadc node (VND SAADC) — a separate
|
|
555
|
+
// driver/channel shape, follow-up.
|
|
556
|
+
const nrfSaadc = nrfSaadcAinPads(soc, entry.identifier);
|
|
557
|
+
let nrfSaadcSynth = false;
|
|
558
|
+
if (siliconAdc.length === 0 && espAdcUnits.length === 0 && nrfSaadc) {
|
|
559
|
+
const gpio0 = controllers.find((c) => c.nodelabel === 'gpio0');
|
|
560
|
+
for (const [channel, pad] of nrfSaadc) {
|
|
561
|
+
if (gpio0 && pad > gpio0.maxPin - gpio0.minPin)
|
|
562
|
+
continue;
|
|
563
|
+
siliconAdc.push({ pin: (gpio0?.minPin ?? 0) + pad, channel });
|
|
564
|
+
}
|
|
565
|
+
nrfSaadcSynth = siliconAdc.length > 0;
|
|
566
|
+
if (nrfSaadcSynth)
|
|
567
|
+
adcSources.push('adc');
|
|
568
|
+
}
|
|
569
|
+
// RP2 header-matrix routes (rpi-pico-*-pinctrl.h macros): GLOBAL pad
|
|
570
|
+
// numbers (the macro's P<n>), pinmux-macro tokens for the overlay's
|
|
571
|
+
// pinctrl groups (both the ADC and PWM drivers apply pinctrl on RP2). A
|
|
572
|
+
// pad may live on ANY controller — the RP2350B's ADC macros sit on pads
|
|
573
|
+
// 40-43 (gpio1 territory), so a gpio0-only check silently dropped them.
|
|
574
|
+
const padToPin = (pad) => controllers.find((c) => pad >= c.minPin && pad <= c.maxPin) ? pad : undefined;
|
|
575
|
+
for (const r of entry.padAdc ?? []) {
|
|
576
|
+
const pin = padToPin(r.pad);
|
|
577
|
+
if (pin === undefined)
|
|
578
|
+
continue;
|
|
579
|
+
if (adcSeenPad.has(pin))
|
|
580
|
+
continue;
|
|
581
|
+
adcSeenPad.add(pin);
|
|
582
|
+
siliconAdc.push({ pin, channel: r.channel, pinctrl: r.pinctrl });
|
|
583
|
+
if (!adcSources.includes(r.source))
|
|
584
|
+
adcSources.push(r.source);
|
|
585
|
+
}
|
|
586
|
+
for (const r of entry.padPwm ?? []) {
|
|
587
|
+
const pin = padToPin(r.pad);
|
|
588
|
+
if (pin === undefined)
|
|
589
|
+
continue;
|
|
590
|
+
if (pwmSeenPad.has(pin))
|
|
591
|
+
continue;
|
|
592
|
+
pwmSeenPad.add(pin);
|
|
593
|
+
siliconPwm.push({ pin, controller: r.source, channel: r.channel, pinctrl: r.pinctrl });
|
|
594
|
+
}
|
|
595
|
+
// Connector io-channel wiring (the DKs' A0-A5 ↔ AIN maps): the last
|
|
596
|
+
// fallback for boards on SoCs without a family table or a harvested
|
|
597
|
+
// route — the channels the BOARD actually wires, by its own authorship.
|
|
598
|
+
if (siliconAdc.length === 0 && (entry.connectorAdc ?? []).length > 0) {
|
|
599
|
+
for (const r of entry.connectorAdc ?? []) {
|
|
600
|
+
const ctrl = controllers.find((c) => c.nodelabel === r.controller);
|
|
601
|
+
if (!ctrl || r.pin > ctrl.maxPin - ctrl.minPin)
|
|
602
|
+
continue;
|
|
603
|
+
const pin = ctrl.minPin + r.pin;
|
|
604
|
+
if (adcSeenPad.has(pin))
|
|
605
|
+
continue;
|
|
606
|
+
adcSeenPad.add(pin);
|
|
607
|
+
siliconAdc.push({ pin, channel: r.channel });
|
|
608
|
+
if (!adcSources.includes(r.source))
|
|
609
|
+
adcSources.push(r.source);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
// User facts (cuttlefish.facts.json) — the escape hatch. User routes win
|
|
613
|
+
// PER PIN over every harvested source above, and each takeover is warned.
|
|
614
|
+
// Seeded with the pinctrl harvest's own lint results (name↔value
|
|
615
|
+
// disagreements — routes dropped as untrustworthy upstream of here).
|
|
616
|
+
const userWarnings = [...(entry.pinctrlWarnings ?? []), ...seedWarnings];
|
|
617
|
+
// pwm specs: run after ALL pwm sources (STM32 harvest, RP2 header
|
|
618
|
+
// matrices) so every shadow — any family — lands in the warnings. Any
|
|
619
|
+
// user pwm spec also suppresses the auto nRF matrix below (the
|
|
620
|
+
// siliconPwm gate): the user owns pwm on that board.
|
|
621
|
+
if (userFacts?.pwm) {
|
|
622
|
+
for (const u of userFacts.pwm.specs) {
|
|
623
|
+
const shadowed = siliconPwm.findIndex((x) => x.pin === u.pin);
|
|
624
|
+
if (shadowed >= 0) {
|
|
625
|
+
const old = siliconPwm[shadowed];
|
|
626
|
+
userWarnings.push(`pwm pin ${u.pin}: user spec (${u.controller} ch ${u.channel}) shadows the harvested route (${old.controller} ch ${old.channel}).`);
|
|
627
|
+
siliconPwm.splice(shadowed, 1);
|
|
628
|
+
}
|
|
629
|
+
pwmSeenPad.add(u.pin);
|
|
630
|
+
siliconPwm.push({ pin: u.pin, controller: u.controller, channel: u.channel, ...(u.pinctrl ? { pinctrl: u.pinctrl } : {}) });
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
// adc channels: same rule across pinctrl harvest, headers, family tables,
|
|
634
|
+
// connector wiring. The declared device (or the existing primary, or
|
|
635
|
+
// 'adc') joins adcSources so the nodeLabel and the init blocks stay
|
|
636
|
+
// consistent; on a board with NO harvested adc at all the user's device
|
|
637
|
+
// IS the primary.
|
|
638
|
+
if (userFacts?.adc) {
|
|
639
|
+
const device = userFacts.adc.device ?? adcSources[0] ?? 'adc';
|
|
640
|
+
if (!adcSources.includes(device))
|
|
641
|
+
adcSources.push(device);
|
|
642
|
+
for (const u of userFacts.adc.channels) {
|
|
643
|
+
const shadowed = siliconAdc.findIndex((x) => x.pin === u.pin);
|
|
644
|
+
if (shadowed >= 0) {
|
|
645
|
+
const old = siliconAdc[shadowed];
|
|
646
|
+
userWarnings.push(`adc pin ${u.pin}: user channel ${u.channel} shadows the harvested channel ${old.channel}.`);
|
|
647
|
+
siliconAdc.splice(shadowed, 1);
|
|
648
|
+
}
|
|
649
|
+
adcSeenPad.add(u.pin);
|
|
650
|
+
siliconAdc.push({
|
|
651
|
+
pin: u.pin,
|
|
652
|
+
channel: u.channel,
|
|
653
|
+
...(device !== adcSources[0] ? { controller: device } : {}),
|
|
654
|
+
...(u.pinctrl ? { pinctrl: u.pinctrl } : {}),
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
const dacSources = [...new Set((entry.dacPins ?? []).map((d) => d.source))]
|
|
659
|
+
.filter((src) => analogDevices.has(src)).sort();
|
|
660
|
+
const siliconDac = [];
|
|
661
|
+
for (const d of [...(entry.dacPins ?? [])]
|
|
662
|
+
.filter((x) => x.source === dacSources[0])
|
|
663
|
+
.sort((x, y) => x.channel - y.channel)) {
|
|
664
|
+
const pin = globalForPort(d.port, d.bit);
|
|
665
|
+
if (pin === undefined)
|
|
666
|
+
continue;
|
|
667
|
+
siliconDac.push({ pin, channel: d.channel, pinctrl: d.pinctrl });
|
|
668
|
+
}
|
|
669
|
+
// ESP32 DAC: silicon-fixed channel→pad (no pinctrl group, no harvested
|
|
670
|
+
// route). Only when no pinctrl DAC was harvested and the soc has the DAC.
|
|
671
|
+
const esp32Dac = esp32DacPins(soc);
|
|
672
|
+
if (siliconDac.length === 0 && esp32Dac) {
|
|
673
|
+
const gpio0 = controllers.find((c) => c.nodelabel === 'gpio0');
|
|
674
|
+
for (const [channel, pad] of esp32Dac) {
|
|
675
|
+
if (gpio0 && pad > gpio0.maxPin - gpio0.minPin)
|
|
676
|
+
continue;
|
|
677
|
+
siliconDac.push({ pin: (gpio0?.minPin ?? 0) + pad, channel, pinctrl: '' });
|
|
678
|
+
}
|
|
679
|
+
if (siliconDac.length > 0 && !dacSources.includes('dac'))
|
|
680
|
+
dacSources.push('dac');
|
|
681
|
+
}
|
|
682
|
+
// User facts (cuttlefish.facts.json): dac channels win per pin, and the
|
|
683
|
+
// declared device joins the sources (no analogDevices cross-check — the
|
|
684
|
+
// user vouches for it).
|
|
685
|
+
if (userFacts?.dac) {
|
|
686
|
+
if (!dacSources.includes(userFacts.dac.device))
|
|
687
|
+
dacSources.push(userFacts.dac.device);
|
|
688
|
+
for (const u of userFacts.dac.channels) {
|
|
689
|
+
const shadowed = siliconDac.findIndex((x) => x.pin === u.pin);
|
|
690
|
+
if (shadowed >= 0) {
|
|
691
|
+
userWarnings.push(`dac pin ${u.pin}: user channel ${u.channel} shadows the harvested channel ${siliconDac[shadowed].channel}.`);
|
|
692
|
+
siliconDac.splice(shadowed, 1);
|
|
693
|
+
}
|
|
694
|
+
siliconDac.push({ pin: u.pin, channel: u.channel, pinctrl: '' });
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
const siliconPwmPins = new Set(siliconPwm.map((s) => s.pin));
|
|
698
|
+
const siliconAdcPins = new Set(siliconAdc.map((s) => s.pin));
|
|
699
|
+
const siliconDacPins = new Set(siliconDac.map((s) => s.pin));
|
|
700
|
+
// ── Hardware counters (Zephyr counter devices) ─────────────────────────
|
|
701
|
+
// Kernel-claim exclusions — counter nodes the system tick owns are NOT
|
|
702
|
+
// free; a family table, like the letter-port conventions: the ESP32
|
|
703
|
+
// esp_timer (espressif,esp32-rtc-timer) always belongs to the kernel, and
|
|
704
|
+
// nRF's RTC0 is the default system clock (RTC1+ are free). STM32 has no
|
|
705
|
+
// exclusion — Cortex-M SysTick is the tick, so the RTC is free.
|
|
706
|
+
const KERNEL_CLAIMED = [
|
|
707
|
+
{ compatible: 'espressif,esp32-rtc-timer' },
|
|
708
|
+
{ compatible: 'nordic,nrf-rtc', nodeLabel: 'rtc0' },
|
|
709
|
+
];
|
|
710
|
+
const isKernelClaimed = (n) => KERNEL_CLAIMED.some((k) => (k.compatible === undefined || k.compatible === n.compatible)
|
|
711
|
+
&& (k.nodeLabel === undefined || k.nodeLabel === n.nodeLabel));
|
|
712
|
+
const hwtimerControllers = (entry.counterNodes ?? []).filter((n) => !isKernelClaimed(n));
|
|
713
|
+
const pins = [];
|
|
714
|
+
const byIdent = new Map();
|
|
715
|
+
const pushPin = (p) => {
|
|
716
|
+
if (!p)
|
|
717
|
+
return undefined;
|
|
718
|
+
const existing = byIdent.get(p.ident);
|
|
719
|
+
if (existing)
|
|
720
|
+
return existing;
|
|
721
|
+
byIdent.set(p.ident, p);
|
|
722
|
+
pins.push(p);
|
|
723
|
+
return p;
|
|
724
|
+
};
|
|
725
|
+
// Datasheet sweep: enumerate every derived (non-opaque) controller
|
|
726
|
+
// range. Opaque controllers (FPGAIO IPs, AON banks) carry no sweepable
|
|
727
|
+
// silicon layout — their facts still place through placePin below.
|
|
728
|
+
for (const ctrl of controllers) {
|
|
729
|
+
if (opaque.has(ctrl.nodelabel))
|
|
730
|
+
continue;
|
|
731
|
+
const span = ctrl.maxPin - ctrl.minPin + 1;
|
|
732
|
+
for (let bit = 0; bit < span; bit++)
|
|
733
|
+
pushPin(placePin(conv, { controller: ctrl.nodelabel, pin: bit }, controllers));
|
|
734
|
+
}
|
|
735
|
+
// Connector pins from the record (labels like D0/D10, or contract pads).
|
|
736
|
+
// A bit beyond the controller's declared width (the m5stack_fire DTS
|
|
737
|
+
// wires grove p1 to gpio1.21 — a pad classic ESP32 does not have) is
|
|
738
|
+
// unplaceable: skip it rather than mint a pin no lowering can route.
|
|
739
|
+
const connectorExports = [];
|
|
740
|
+
for (const conn of entry.connectors ?? []) {
|
|
741
|
+
for (const [label, ref] of Object.entries(conn.pins)) {
|
|
742
|
+
const ctrl = controllers.find((c) => c.nodelabel === ref.controller);
|
|
743
|
+
if (ctrl && ref.pin <= ctrl.maxPin - ctrl.minPin) {
|
|
744
|
+
const placed = pushPin(placePin(conv, ref, controllers));
|
|
745
|
+
if (placed)
|
|
746
|
+
connectorExports.push({ label, pin: placed });
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
// LED / BUTTON from the record's DTS facts. A board fact is real even
|
|
751
|
+
// when the pad is outside the sweep's family layout — construct it
|
|
752
|
+
// directly so the export exists; the dtSpec path addresses it.
|
|
753
|
+
const placeDts = (ref) => pushPin(placePin(conv, ref, controllers));
|
|
754
|
+
const ledPin = entry.led ? placeDts(entry.led) : undefined;
|
|
755
|
+
// LED fallback when no gpio-leds node: the record's addressable-strip
|
|
756
|
+
// fact (worldsemi,ws2812 — a plain-GPIO LED, no led0 spec).
|
|
757
|
+
const ledPinFinal = ledPin
|
|
758
|
+
?? (entry.stripLed ? placeDts(entry.stripLed) : undefined);
|
|
759
|
+
const buttonPin = entry.button ? placeDts(entry.button) : undefined;
|
|
760
|
+
const extraLedPins = (entry.extraLeds ?? []).map((l) => placeDts(l)).filter((p) => p !== undefined);
|
|
761
|
+
const extraButtonPins = (entry.extraButtons ?? []).map((b) => placeDts(b)).filter((p) => p !== undefined);
|
|
762
|
+
// ── board.ts ────────────────────────────────────────────────────────────
|
|
763
|
+
const hasUsb = entry.usbDevice === 'enabled';
|
|
764
|
+
const wdtNodeLabel = entry.wdtNodeLabel;
|
|
765
|
+
// Aliased pwm-leds (DT_ALIAS-addressable) — the board's own PWM LED
|
|
766
|
+
// channels; specs ride virtual pins from 8192 (above every real range and
|
|
767
|
+
// the opaque controller blocks).
|
|
768
|
+
const pwmLedSpecs = (entry.pwmLeds ?? []).filter((l) => l.alias);
|
|
769
|
+
const buses = entry.buses ?? { i2c: [], spi: [], uart: [] };
|
|
770
|
+
// nRF PWM: any GPIO pad can carry any channel of a PWM peripheral (psel
|
|
771
|
+
// routing — pinctrl psel order IS channel order), so it is a matrix like
|
|
772
|
+
// the ESP32's LEDC: channels are assigned to the driven pads at overlay
|
|
773
|
+
// time. The first declared peripheral (pwm0) is exposed: four concurrent
|
|
774
|
+
// channels, and exceeding them is an explicit overlay error even though
|
|
775
|
+
// further peripherals idle — a documented v1 ceiling.
|
|
776
|
+
const pwmControllers = [...new Set(entry.pwmNodes ?? [])].sort();
|
|
777
|
+
// pwm-leds specs (virtual pins 8192+) coexist with the matrix — a board's
|
|
778
|
+
// own PWM LED channel plus any-pad matrix channels are different pins.
|
|
779
|
+
const nrfPwmMatrix = entry.pwmMatrix || siliconPwm.length > 0 || !/^nrf/.test(soc) || pwmControllers.length === 0
|
|
780
|
+
? undefined
|
|
781
|
+
: (() => {
|
|
782
|
+
const gpio0 = controllers.find((c) => c.nodelabel === 'gpio0');
|
|
783
|
+
const gpio1 = controllers.find((c) => c.nodelabel === 'gpio1');
|
|
784
|
+
if (!gpio0)
|
|
785
|
+
return undefined;
|
|
786
|
+
const pads = [];
|
|
787
|
+
for (let p = gpio0.minPin; p <= gpio0.maxPin; p++)
|
|
788
|
+
pads.push(p);
|
|
789
|
+
if (gpio1)
|
|
790
|
+
for (let p = gpio1.minPin; p <= gpio1.maxPin; p++)
|
|
791
|
+
pads.push(p);
|
|
792
|
+
return { controller: pwmControllers[0], channelCount: 4, pads };
|
|
793
|
+
})();
|
|
794
|
+
const pwmMatrix = entry.pwmMatrix ?? nrfPwmMatrix;
|
|
795
|
+
// Export-name reservation: the generated module must not redeclare. The
|
|
796
|
+
// connector maps reuse labels across headers (the EK-RA8M1's two grove
|
|
797
|
+
// headers both carry IO6/IO7/p7, wired to different pads), and a label
|
|
798
|
+
// can also collide with a datasheet pin ident or a core export (LED,
|
|
799
|
+
// I2C0, …). First connector label wins; later ones keep their
|
|
800
|
+
// datasheet-named pin — always exported above — and the manifest's
|
|
801
|
+
// pins.aliases follow the same rule so module and manifest agree.
|
|
802
|
+
const reservedNames = new Set([
|
|
803
|
+
...pins.map((p) => p.ident),
|
|
804
|
+
'LED',
|
|
805
|
+
'BUTTON',
|
|
806
|
+
// '@typecad/hal' imports — a connector silkscreen label can literally be
|
|
807
|
+
// 'Pin' (phyBOARD-Atlas), and `export const Pin` merges with the import.
|
|
808
|
+
'Pin',
|
|
809
|
+
'I2CBus',
|
|
810
|
+
'SPIBus',
|
|
811
|
+
'UART',
|
|
812
|
+
'USBConsole',
|
|
813
|
+
'PWM',
|
|
814
|
+
]);
|
|
815
|
+
buses.i2c.forEach((_, i) => reservedNames.add(`I2C${i}`));
|
|
816
|
+
buses.spi.forEach((_, i) => reservedNames.add(`SPI${i}`));
|
|
817
|
+
buses.uart.forEach((_, i) => reservedNames.add(`UART${i}`));
|
|
818
|
+
if (hasUsb)
|
|
819
|
+
reservedNames.add('USB0');
|
|
820
|
+
pwmLedSpecs.forEach((_, i) => reservedNames.add(i === 0 ? 'PWMLED' : `PWMLED${i}`));
|
|
821
|
+
const connectorLabelOwner = new Map();
|
|
822
|
+
for (const { label, pin } of connectorExports) {
|
|
823
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(label))
|
|
824
|
+
continue;
|
|
825
|
+
if (reservedNames.has(label) || connectorLabelOwner.has(label))
|
|
826
|
+
continue;
|
|
827
|
+
connectorLabelOwner.set(label, pin);
|
|
828
|
+
}
|
|
829
|
+
const ts = [];
|
|
830
|
+
ts.push(`// GENERATED by cuttlefish boardgen from the Zephyr board catalog —`);
|
|
831
|
+
ts.push(`// ${entry.identifier} (${entry.name}, ${entry.vendor}).`);
|
|
832
|
+
ts.push(`// Regenerate with: npx cuttlefish board regen`);
|
|
833
|
+
ts.push('');
|
|
834
|
+
ts.push(`import { Pin, I2CBus, SPIBus, UART${hasUsb ? ', USBConsole' : ''}${pwmLedSpecs.length > 0 ? ', PWM' : ''} } from '@typecad/hal';`);
|
|
835
|
+
ts.push('');
|
|
836
|
+
// ── Hardware-class gateway ─────────────────────────────────────────────
|
|
837
|
+
// The board module is the NARROWED surface: every hardware class re-export
|
|
838
|
+
// here exists only when this board's facts support it, so importing
|
|
839
|
+
// unavailable hardware fails at module resolution (editor + transpile)
|
|
840
|
+
// instead of at a deep diagnostic. '@typecad/hal' stays the implementation
|
|
841
|
+
// package; user code imports hardware from '@typecad/board'.
|
|
842
|
+
ts.push('// Hardware this board actually has — unavailable hardware is not importable.');
|
|
843
|
+
ts.push(`export { GPIO, Thread, Time, Sensor } from '@typecad/hal';`);
|
|
844
|
+
if (wdtNodeLabel)
|
|
845
|
+
ts.push(`export { Watchdog } from '@typecad/hal';`);
|
|
846
|
+
if (siliconPwm.length > 0 || pwmLedSpecs.length > 0 || pwmMatrix)
|
|
847
|
+
ts.push(`export { PWM } from '@typecad/hal';`);
|
|
848
|
+
if (siliconAdc.length > 0)
|
|
849
|
+
ts.push(`export { ADC } from '@typecad/hal';`);
|
|
850
|
+
if (siliconDac.length > 0)
|
|
851
|
+
ts.push(`export { DAC } from '@typecad/hal';`);
|
|
852
|
+
if (buses.i2c.length > 0)
|
|
853
|
+
ts.push(`export { I2CTarget } from '@typecad/hal';`);
|
|
854
|
+
if (buses.spi.length > 0)
|
|
855
|
+
ts.push(`export { SPITarget } from '@typecad/hal';`);
|
|
856
|
+
if (buses.uart.length > 0)
|
|
857
|
+
ts.push(`export { UART } from '@typecad/hal';`);
|
|
858
|
+
if (hwtimerControllers.length > 0)
|
|
859
|
+
ts.push(`export { Counter } from '@typecad/hal';`);
|
|
860
|
+
if (hasUsb)
|
|
861
|
+
ts.push(`export { USBConsole } from '@typecad/hal';`);
|
|
862
|
+
// Store/File: a persisted backend needs a storage region — either the
|
|
863
|
+
// board's own storage_partition (harvested reg) or a synthesizable one
|
|
864
|
+
// (flash size known, no existing partition to collide with).
|
|
865
|
+
if (entry.storageReg || (entry.flashKb && !entry.hasStoragePartition)) {
|
|
866
|
+
ts.push(`export { Store, File } from '@typecad/hal';`);
|
|
867
|
+
}
|
|
868
|
+
ts.push('');
|
|
869
|
+
if (pins.length > 0) {
|
|
870
|
+
ts.push(`// Datasheet-named pins (derived from the board's devicetree controllers)`);
|
|
871
|
+
for (const p of pins) {
|
|
872
|
+
ts.push(`export const ${p.ident} = Pin.fromPort('${p.name}');`);
|
|
873
|
+
}
|
|
874
|
+
ts.push('');
|
|
875
|
+
}
|
|
876
|
+
for (const [label, pin] of connectorLabelOwner) {
|
|
877
|
+
ts.push(`/** Connector pin ${label} → ${pin.name} */`);
|
|
878
|
+
ts.push(`export const ${label} = ${pin.ident};`);
|
|
879
|
+
}
|
|
880
|
+
if (connectorLabelOwner.size > 0)
|
|
881
|
+
ts.push('');
|
|
882
|
+
// LED/BUTTON aliases reference pins the sweep may not carry (a pad
|
|
883
|
+
// outside the derived controller table's family layout). Emit them
|
|
884
|
+
// explicitly alongside the alias.
|
|
885
|
+
const extraPinDecls = [];
|
|
886
|
+
if (ledPinFinal && !pins.some((p) => p.ident === ledPinFinal.ident))
|
|
887
|
+
extraPinDecls.push(ledPinFinal);
|
|
888
|
+
if (buttonPin && !pins.some((p) => p.ident === buttonPin.ident) && buttonPin.ident !== ledPinFinal?.ident)
|
|
889
|
+
extraPinDecls.push(buttonPin);
|
|
890
|
+
for (const p of [...extraLedPins, ...extraButtonPins]) {
|
|
891
|
+
if (!pins.some((x) => x.ident === p.ident) && !extraPinDecls.some((x) => x.ident === p.ident))
|
|
892
|
+
extraPinDecls.push(p);
|
|
893
|
+
}
|
|
894
|
+
for (const p of extraPinDecls) {
|
|
895
|
+
ts.push(`/** ${p.name} (derived from the devicetree controller label). */`);
|
|
896
|
+
ts.push(`export const ${p.ident} = Pin.fromPort('${p.name}');`);
|
|
897
|
+
}
|
|
898
|
+
if (ledPinFinal) {
|
|
899
|
+
ts.push(`/** On-board LED (${entry.led ? `devicetree ${entry.led.dtSpec}` : 'addressable strip — no gpio-leds node'}). */`);
|
|
900
|
+
ts.push(`export const LED = ${ledPinFinal.ident};`);
|
|
901
|
+
}
|
|
902
|
+
if (buttonPin) {
|
|
903
|
+
ts.push(`/** User button (devicetree ${entry.button?.dtSpec}). */`);
|
|
904
|
+
ts.push(`export const BUTTON = ${buttonPin.ident};`);
|
|
905
|
+
}
|
|
906
|
+
if (ledPinFinal || buttonPin || extraPinDecls.length > 0)
|
|
907
|
+
ts.push('');
|
|
908
|
+
// Bus instance exports from the board's own DTS-wired controllers.
|
|
909
|
+
const busLines = [];
|
|
910
|
+
buses.i2c.forEach((_, i) => busLines.push(`export const I2C${i} = new I2CBus('I2C${i}');`));
|
|
911
|
+
buses.spi.forEach((_, i) => busLines.push(`export const SPI${i} = new SPIBus('SPI${i}');`));
|
|
912
|
+
buses.uart.forEach((_, i) => busLines.push(`export const UART${i} = new UART('UART${i}');`));
|
|
913
|
+
// USB CDC instance (the thin HAL's USBConsole): the board's DTS turns the
|
|
914
|
+
// device controller on (zephyr_udc0 status okay).
|
|
915
|
+
if (hasUsb)
|
|
916
|
+
busLines.push(`export const USB0 = new USBConsole('USB0');`);
|
|
917
|
+
// PWM-driven LEDs (pwm-leds): addressed by the board's own devicetree
|
|
918
|
+
// alias (DT_ALIAS(pwm_led0) in the lowering) — no overlay needed. The
|
|
919
|
+
// construction period comes from the DTS cell when it is a plain number
|
|
920
|
+
// (macro periods like PWM_MSEC(20) are opaque to the reader — 1 ms, the
|
|
921
|
+
// LED-dimming default, stands in).
|
|
922
|
+
pwmLedSpecs.forEach((l, i) => {
|
|
923
|
+
const period = l.periodNs ?? 1_000_000;
|
|
924
|
+
const name = i === 0 ? 'PWMLED' : `PWMLED${i}`;
|
|
925
|
+
busLines.push(`/** Board PWM LED (devicetree ${l.alias}${l.flags?.length ? ', ' + l.flags.join(' ') : ''}). */`);
|
|
926
|
+
busLines.push(`export const ${name} = new PWM(${8192 + i}, { periodNs: ${period} });`);
|
|
927
|
+
});
|
|
928
|
+
if (busLines.length > 0) {
|
|
929
|
+
ts.push('// Bus instance selectors (one per board-wired controller)');
|
|
930
|
+
ts.push(...busLines);
|
|
931
|
+
ts.push('');
|
|
932
|
+
}
|
|
933
|
+
// ── board.json (BoardConstants flat map + manifest) ─────────────────────
|
|
934
|
+
const constants = {};
|
|
935
|
+
constants['id'] = entry.identifier.split('/')[0];
|
|
936
|
+
constants['name'] = entry.name;
|
|
937
|
+
constants['architecture'] = soc;
|
|
938
|
+
constants['build.frameworks.zephyr'] = entry.identifier;
|
|
939
|
+
constants['zephyr.soc'] = soc;
|
|
940
|
+
pins.forEach((p, i) => {
|
|
941
|
+
constants[`pins.all.${i}.number`] = p.halPin;
|
|
942
|
+
constants[`pins.all.${i}.gpio`] = p.halPin;
|
|
943
|
+
constants[`pins.all.${i}.name`] = p.name;
|
|
944
|
+
// digitalIn/out, interrupt, pull-up/down, open-drain are the Zephyr base
|
|
945
|
+
// GPIO driver API (`gpio_pin_configure` flags + `gpio_pin_interrupt_configure`):
|
|
946
|
+
// every GPIO controller implements them, so they are universally true —
|
|
947
|
+
// a platform fact, not a per-pad one. Analog/PWM ride the harvested
|
|
948
|
+
// silicon routes (honest per-pad facts).
|
|
949
|
+
constants[`pins.all.${i}.capabilities.digitalInput`] = true;
|
|
950
|
+
constants[`pins.all.${i}.capabilities.digitalOutput`] = true;
|
|
951
|
+
constants[`pins.all.${i}.capabilities.analogInput`] = siliconAdcPins.has(p.halPin);
|
|
952
|
+
constants[`pins.all.${i}.capabilities.analogOutput`] = siliconDacPins.has(p.halPin);
|
|
953
|
+
constants[`pins.all.${i}.capabilities.pwm`] = siliconPwmPins.has(p.halPin);
|
|
954
|
+
constants[`pins.all.${i}.capabilities.interrupt`] = true;
|
|
955
|
+
constants[`pins.all.${i}.capabilities.pullUp`] = true;
|
|
956
|
+
constants[`pins.all.${i}.capabilities.pullDown`] = true;
|
|
957
|
+
// Capacitive touch has no harvested source yet (ESP32 touch pads live in
|
|
958
|
+
// the pinctrl header matrix) — honestly false until a source lands.
|
|
959
|
+
constants[`pins.all.${i}.capabilities.touch`] = false;
|
|
960
|
+
constants[`pins.all.${i}.capabilities.openDrain`] = true;
|
|
961
|
+
});
|
|
962
|
+
constants['peripherals.i2c.count'] = buses.i2c.length;
|
|
963
|
+
constants['peripherals.spi.count'] = buses.spi.length;
|
|
964
|
+
constants['peripherals.uart.count'] = buses.uart.length;
|
|
965
|
+
buses.i2c.forEach((_, i) => constants[`peripherals.i2c.${i}.instance`] = i);
|
|
966
|
+
buses.spi.forEach((_, i) => constants[`peripherals.spi.${i}.instance`] = i);
|
|
967
|
+
buses.uart.forEach((_, i) => constants[`peripherals.uart.${i}.instance`] = i);
|
|
968
|
+
// Board pwm-led specs (virtual pins 8192+, dtSpec = the board's alias).
|
|
969
|
+
// resolveChipFromBoard merges these into the active chip's pwm specs.
|
|
970
|
+
pwmLedSpecs.forEach((l, i) => {
|
|
971
|
+
constants[`zephyr.pwm.specs.${i}.pin`] = 8192 + i;
|
|
972
|
+
constants[`zephyr.pwm.specs.${i}.dtSpec`] = l.alias;
|
|
973
|
+
});
|
|
974
|
+
// Silicon PWM specs (real pads, pinctrl-routed) continue the index space
|
|
975
|
+
// after the board-alias virtual-pin specs so the two never collide. The
|
|
976
|
+
// overlay enables each referenced controller and derives the STM32 16-bit
|
|
977
|
+
// prescaler from zephyr.pwm.clockHz.
|
|
978
|
+
siliconPwm.forEach((s, i) => {
|
|
979
|
+
const idx = pwmLedSpecs.length + i;
|
|
980
|
+
constants[`zephyr.pwm.specs.${idx}.pin`] = s.pin;
|
|
981
|
+
constants[`zephyr.pwm.specs.${idx}.controller`] = s.controller;
|
|
982
|
+
constants[`zephyr.pwm.specs.${idx}.channel`] = s.channel;
|
|
983
|
+
if (s.pinctrl)
|
|
984
|
+
constants[`zephyr.pwm.specs.${idx}.pinctrl`] = s.pinctrl;
|
|
985
|
+
});
|
|
986
|
+
// Timer input clocks under the family's default clock tree (PWM_CLOCK_HZ
|
|
987
|
+
// at module scope). Omitted when unknown — the overlay then leaves the
|
|
988
|
+
// SoC-default prescaler in place.
|
|
989
|
+
if (siliconPwm.length > 0) {
|
|
990
|
+
const clock = Object.entries(PWM_CLOCK_HZ).find(([k]) => soc.startsWith(k))?.[1];
|
|
991
|
+
if (clock)
|
|
992
|
+
constants['zephyr.pwm.clockHz'] = clock;
|
|
993
|
+
}
|
|
994
|
+
// ADC facts: the first harvested controller's channels with the family
|
|
995
|
+
// resolution/vref (STM32: 12-bit nominal VREF; ESP32 SARADC: 12-bit
|
|
996
|
+
// against the ~1.1 V internal reference). The overlay enables the
|
|
997
|
+
// controller and wires the used channels' pinctrl groups (STM32 only —
|
|
998
|
+
// ESP32 pads are analog by silicon, no groups).
|
|
999
|
+
if (siliconAdc.length > 0) {
|
|
1000
|
+
const isEsp = !!(entry.espAdc ?? []).length;
|
|
1001
|
+
constants['zephyr.adc.nodeLabel'] = isEsp ? (espAdcUnit ?? 'adc0') : adcSources[0];
|
|
1002
|
+
constants['zephyr.adc.resolution'] = 12;
|
|
1003
|
+
// vref: STM32 = VDDA nominal (3300); ESP32 SARADC = the ~1.1 V internal
|
|
1004
|
+
// reference; nRF SAADC = 3000 (the scheme the lowering's gain-1/4 +
|
|
1005
|
+
// internal-reference default assumes — matches the descriptor the XIAO's
|
|
1006
|
+
// hardware suite validated).
|
|
1007
|
+
constants['zephyr.adc.vrefMv'] = isEsp ? 1100 : nrfSaadcSynth ? 3000 : 3300;
|
|
1008
|
+
// The channel-setup gain/reference pair (ADC_CHANNEL_CFG at module
|
|
1009
|
+
// scope). Absent = the lowering's default (the nRF SAADC scheme:
|
|
1010
|
+
// ADC_GAIN_1_4 against ADC_REF_INTERNAL).
|
|
1011
|
+
const adcCfg = !isEsp
|
|
1012
|
+
? Object.entries(ADC_CHANNEL_CFG).find(([k]) => soc.startsWith(k))?.[1]
|
|
1013
|
+
: undefined;
|
|
1014
|
+
if (adcCfg) {
|
|
1015
|
+
constants['zephyr.adc.gain'] = adcCfg.gain;
|
|
1016
|
+
constants['zephyr.adc.reference'] = adcCfg.reference;
|
|
1017
|
+
}
|
|
1018
|
+
siliconAdc.forEach((a, i) => {
|
|
1019
|
+
constants[`zephyr.adc.channels.${i}.pin`] = a.pin;
|
|
1020
|
+
constants[`zephyr.adc.channels.${i}.channel`] = a.channel;
|
|
1021
|
+
if (a.pinctrl)
|
|
1022
|
+
constants[`zephyr.adc.channels.${i}.pinctrl`] = a.pinctrl;
|
|
1023
|
+
if (a.controller)
|
|
1024
|
+
constants[`zephyr.adc.channels.${i}.controller`] = a.controller;
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
// ESP32 LEDC matrix: any listed pad can carry any of channelCount
|
|
1028
|
+
// channels; the overlay assigns channels to the driven pads at build
|
|
1029
|
+
// time. Pins are global pad numbers (gpio0 base 0).
|
|
1030
|
+
if (pwmMatrix) {
|
|
1031
|
+
const ctrl = controllers.find((c) => c.nodelabel === 'gpio0');
|
|
1032
|
+
const base = ctrl ? ctrl.minPin : 0;
|
|
1033
|
+
constants['zephyr.pwm.matrix.controller'] = pwmMatrix.controller;
|
|
1034
|
+
constants['zephyr.pwm.matrix.channelCount'] = pwmMatrix.channelCount;
|
|
1035
|
+
pwmMatrix.pads.forEach((pad, i) => {
|
|
1036
|
+
constants[`zephyr.pwm.matrix.pins.${i}`] = base + pad;
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
// DAC facts: the first harvested controller's output channels (STM32
|
|
1040
|
+
// 12-bit). The pinctrl source IS the DT nodelabel on STM32 (dac1), so it
|
|
1041
|
+
// maps straight to the device spec; the overlay enables the node and
|
|
1042
|
+
// wires the used channels' pinctrl groups.
|
|
1043
|
+
if (siliconDac.length > 0) {
|
|
1044
|
+
constants['zephyr.dac.device'] = dacSources[0];
|
|
1045
|
+
// ESP32's DAC is 8-bit; the STM32 DAC is 12-bit (the default).
|
|
1046
|
+
const dacResolution = esp32Dac ? 8 : 12;
|
|
1047
|
+
siliconDac.forEach((d, i) => {
|
|
1048
|
+
constants[`zephyr.dac.channels.${i}.pin`] = d.pin;
|
|
1049
|
+
constants[`zephyr.dac.channels.${i}.channel`] = d.channel;
|
|
1050
|
+
constants[`zephyr.dac.channels.${i}.resolution`] = dacResolution;
|
|
1051
|
+
constants[`zephyr.dac.channels.${i}.pinctrl`] = d.pinctrl;
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
if (ledPinFinal)
|
|
1055
|
+
constants['pins.aliases.LED'] = ledPinFinal.name;
|
|
1056
|
+
if (buttonPin)
|
|
1057
|
+
constants['pins.aliases.BUTTON'] = buttonPin.name;
|
|
1058
|
+
// Extra LEDs / buttons beyond the canonical first — indexed aliases
|
|
1059
|
+
// INCLUDE the canonical first: LED0 = the first (same as LED), LED1 =
|
|
1060
|
+
// the second, etc. (the dtSpec numbering led0, led1, led2 lines up).
|
|
1061
|
+
if (ledPinFinal)
|
|
1062
|
+
constants['pins.aliases.LED0'] = ledPinFinal.name;
|
|
1063
|
+
if (buttonPin)
|
|
1064
|
+
constants['pins.aliases.BUTTON0'] = buttonPin.name;
|
|
1065
|
+
extraLedPins.forEach((p, i) => {
|
|
1066
|
+
constants[`pins.aliases.LED${i + 1}`] = p.name;
|
|
1067
|
+
});
|
|
1068
|
+
extraButtonPins.forEach((p, i) => {
|
|
1069
|
+
constants[`pins.aliases.BUTTON${i + 1}`] = p.name;
|
|
1070
|
+
});
|
|
1071
|
+
// Connector labels (D0–D20, A0–A10, SDA/SCL/MOSI/MISO/SCK/RX/TX/CS,
|
|
1072
|
+
// GP0–GP29, …) — every uncollided one becomes a pins.aliases.<label>
|
|
1073
|
+
// entry so `resolveHALReceiver` treats them as Pin constants (the same
|
|
1074
|
+
// path LED/BUTTON ride). The board.ts export already exists
|
|
1075
|
+
// (connectorLabelOwner); this makes the manifest agree.
|
|
1076
|
+
for (const [label, pin] of connectorLabelOwner) {
|
|
1077
|
+
constants[`pins.aliases.${label}`] = pin.name;
|
|
1078
|
+
}
|
|
1079
|
+
// Analog silkscreen offset: the lowest-numbered A-label's underlying HAL
|
|
1080
|
+
// pin. The hal-parser's A<N> fallback (no-manifest path) uses this to
|
|
1081
|
+
// map A0 → the board's actual analog base instead of assuming 14.
|
|
1082
|
+
const aLabels = connectorExports
|
|
1083
|
+
.filter(({ label }) => /^A\d+$/.test(label))
|
|
1084
|
+
.sort((a, b) => parseInt(a.label.slice(1), 10) - parseInt(b.label.slice(1), 10));
|
|
1085
|
+
if (aLabels.length > 0) {
|
|
1086
|
+
const a0 = aLabels[0];
|
|
1087
|
+
const idx = pins.indexOf(a0.pin);
|
|
1088
|
+
if (idx >= 0) {
|
|
1089
|
+
constants['pins.analogOffset'] = a0.pin.halPin;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
// ── GPIO controller routing ────────────────────────────────────────────
|
|
1093
|
+
// The strategy reconstructs its controller table from these constants via
|
|
1094
|
+
// resolveChipFromBoard — without them the GPIO lowering would default to
|
|
1095
|
+
// a `gpio0` nodelabel that doesn't exist on most vendors (undefined
|
|
1096
|
+
// `__device_dts_ord_…` at C++ compile time).
|
|
1097
|
+
controllers.forEach((c, i) => {
|
|
1098
|
+
constants[`zephyr.gpioControllers.${i}.nodelabel`] = c.nodelabel;
|
|
1099
|
+
constants[`zephyr.gpioControllers.${i}.minPin`] = c.minPin;
|
|
1100
|
+
constants[`zephyr.gpioControllers.${i}.maxPin`] = c.maxPin;
|
|
1101
|
+
});
|
|
1102
|
+
// Devicetree alias specs for the board's own LEDs/buttons: the lowering
|
|
1103
|
+
// prefers the dtSpec path (gpio_pin_set_dt), which honors the node's
|
|
1104
|
+
// polarity flags from the board DTS. They double as the canonical
|
|
1105
|
+
// interrupt sources (the board's buttons).
|
|
1106
|
+
const dtSpecEntries = [];
|
|
1107
|
+
if (entry.led && ledPin)
|
|
1108
|
+
dtSpecEntries.push({ pin: ledPin.halPin, dtSpec: entry.led.dtSpec });
|
|
1109
|
+
if (entry.button && buttonPin)
|
|
1110
|
+
dtSpecEntries.push({ pin: buttonPin.halPin, dtSpec: entry.button.dtSpec });
|
|
1111
|
+
(entry.extraLeds ?? []).forEach((l, i) => {
|
|
1112
|
+
const p = extraLedPins[i];
|
|
1113
|
+
if (p)
|
|
1114
|
+
dtSpecEntries.push({ pin: p.halPin, dtSpec: l.dtSpec });
|
|
1115
|
+
});
|
|
1116
|
+
(entry.extraButtons ?? []).forEach((b, i) => {
|
|
1117
|
+
const p = extraButtonPins[i];
|
|
1118
|
+
if (p)
|
|
1119
|
+
dtSpecEntries.push({ pin: p.halPin, dtSpec: b.dtSpec });
|
|
1120
|
+
});
|
|
1121
|
+
dtSpecEntries.forEach((s, i) => {
|
|
1122
|
+
constants[`zephyr.gpio.dtSpecs.${i}.pin`] = s.pin;
|
|
1123
|
+
constants[`zephyr.gpio.dtSpecs.${i}.dtSpec`] = s.dtSpec;
|
|
1124
|
+
constants[`zephyr.gpio.interruptPins.${i}.pin`] = s.pin;
|
|
1125
|
+
constants[`zephyr.gpio.interruptPins.${i}.dtSpec`] = s.dtSpec;
|
|
1126
|
+
});
|
|
1127
|
+
// Bus controllers the board's own DTS wires up — resolveChipFromBoard
|
|
1128
|
+
// reconstructs the bus lists from these, and the overlays enable the
|
|
1129
|
+
// nodes the program uses.
|
|
1130
|
+
buses.i2c.forEach((nodelabel, i) => {
|
|
1131
|
+
constants[`zephyr.i2c.controllers.${i}.nodeLabel`] = nodelabel;
|
|
1132
|
+
});
|
|
1133
|
+
buses.spi.forEach((nodelabel, i) => {
|
|
1134
|
+
constants[`zephyr.spi.controllers.${i}.nodeLabel`] = nodelabel;
|
|
1135
|
+
});
|
|
1136
|
+
buses.uart.forEach((nodelabel, i) => {
|
|
1137
|
+
constants[`zephyr.uart.controllers.${i}.nodeLabel`] = nodelabel;
|
|
1138
|
+
});
|
|
1139
|
+
// USB device (CDC-ACM): the board's DTS turned the controller on.
|
|
1140
|
+
if (hasUsb) {
|
|
1141
|
+
constants['zephyr.usb.controller'] = entry.usbController ?? 'zephyr_udc0';
|
|
1142
|
+
constants['zephyr.usb.cdcInstances'] = 1;
|
|
1143
|
+
}
|
|
1144
|
+
// Watchdog: the board's devicetree watchdog0 alias.
|
|
1145
|
+
if (entry.wdtNodeLabel) {
|
|
1146
|
+
constants['zephyr.wdt.nodeLabel'] = entry.wdtNodeLabel;
|
|
1147
|
+
}
|
|
1148
|
+
// Storage partition facts (Store/FS). Boards whose DTS ships a
|
|
1149
|
+
// storage_partition (ESP32's AMP layout) carry its REAL reg — the overlay
|
|
1150
|
+
// then writes only the /chosen pointer, never a redeclaration. Boards
|
|
1151
|
+
// without one get a region synthesized near the TOP of flash (the app
|
|
1152
|
+
// image grows from the bottom, so the end is the conventional safe
|
|
1153
|
+
// region). Region = 1/8 of flash, clamped to [16KB, 128KB], 4KB-aligned.
|
|
1154
|
+
if (entry.storageReg) {
|
|
1155
|
+
constants['zephyr.storage.offset'] = entry.storageReg.offsetBytes;
|
|
1156
|
+
constants['zephyr.storage.size'] = entry.storageReg.sizeBytes;
|
|
1157
|
+
constants['zephyr.storage.preexisting'] = true;
|
|
1158
|
+
}
|
|
1159
|
+
else if (entry.flashKb && !entry.hasStoragePartition) {
|
|
1160
|
+
const regionKb = Math.min(128, Math.max(16, Math.floor(entry.flashKb / 8 / 4) * 4));
|
|
1161
|
+
constants['zephyr.storage.offset'] = (entry.flashKb - regionKb) * 1024;
|
|
1162
|
+
constants['zephyr.storage.size'] = regionKb * 1024;
|
|
1163
|
+
}
|
|
1164
|
+
// Hardware counters: the free-counter list was computed above (with the
|
|
1165
|
+
// kernel-claim exclusions); emit the controller facts. The child form
|
|
1166
|
+
// (ESP32 timers) carries the parent the overlay attaches the label to.
|
|
1167
|
+
hwtimerControllers.forEach((c, i) => {
|
|
1168
|
+
constants[`zephyr.hwtimer.controllers.${i}.nodeLabel`] = c.nodeLabel ?? `tc_counter${i}`;
|
|
1169
|
+
if (c.parentLabel)
|
|
1170
|
+
constants[`zephyr.hwtimer.controllers.${i}.counterParent`] = c.parentLabel;
|
|
1171
|
+
});
|
|
1172
|
+
// Probe methods — the debug-config writer and `west flash` runner choice
|
|
1173
|
+
// read them from the board constants as zephyr.probeMethods.N.*. The
|
|
1174
|
+
// table is the board's own board.cmake runners, so `zephyr.probe`
|
|
1175
|
+
// resolves on every board that declares runners.
|
|
1176
|
+
(entry.probeMethods ?? []).forEach((pm, i) => {
|
|
1177
|
+
constants[`zephyr.probeMethods.${i}.id`] = pm.id;
|
|
1178
|
+
constants[`zephyr.probeMethods.${i}.runner`] = pm.runner;
|
|
1179
|
+
if (pm.args)
|
|
1180
|
+
constants[`zephyr.probeMethods.${i}.args`] = pm.args.join(',');
|
|
1181
|
+
if (pm.description)
|
|
1182
|
+
constants[`zephyr.probeMethods.${i}.description`] = pm.description;
|
|
1183
|
+
if (pm.debug !== undefined)
|
|
1184
|
+
constants[`zephyr.probeMethods.${i}.debug`] = pm.debug;
|
|
1185
|
+
if (pm.debugInterface)
|
|
1186
|
+
constants[`zephyr.probeMethods.${i}.debugInterface`] = pm.debugInterface;
|
|
1187
|
+
if (pm.debugDevice)
|
|
1188
|
+
constants[`zephyr.probeMethods.${i}.debugDevice`] = pm.debugDevice;
|
|
1189
|
+
// cfg lines join on newlines — they legally contain commas (tcl event
|
|
1190
|
+
// blocks), which the comma convention would corrupt.
|
|
1191
|
+
if (pm.debugCfg)
|
|
1192
|
+
constants[`zephyr.probeMethods.${i}.debugCfg`] = pm.debugCfg.join('\n');
|
|
1193
|
+
const src = pm.debugCfgSource;
|
|
1194
|
+
if (src)
|
|
1195
|
+
constants[`zephyr.probeMethods.${i}.debugCfgSource`] = src.join('\n');
|
|
1196
|
+
});
|
|
1197
|
+
const manifest = {
|
|
1198
|
+
version: 1,
|
|
1199
|
+
identifier: entry.identifier,
|
|
1200
|
+
soc,
|
|
1201
|
+
pinNames: pins.map((p) => p.name),
|
|
1202
|
+
constants,
|
|
1203
|
+
// Source fingerprint: covers the record content, the extraction
|
|
1204
|
+
// revision, and the tree provenance (when the record came from a
|
|
1205
|
+
// catalog). `cuttlefish build` recomputes it cheaply and regenerates
|
|
1206
|
+
// this module when it moves — a board change in the config, the catalog
|
|
1207
|
+
// overlay, or the Zephyr tree itself recreates the module.
|
|
1208
|
+
source: {
|
|
1209
|
+
generatorRev: GENERATOR_REV,
|
|
1210
|
+
fingerprint: boardRecordFingerprint(entry, loadBoardCatalogOverlay()) + factsSuffix,
|
|
1211
|
+
},
|
|
1212
|
+
};
|
|
1213
|
+
return {
|
|
1214
|
+
boardTs: ts.join('\n'),
|
|
1215
|
+
boardJson: JSON.stringify(manifest, null, 1),
|
|
1216
|
+
board: entry,
|
|
1217
|
+
...(userWarnings.length > 0 ? { warnings: userWarnings } : {}),
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Generate the board module contents for a qualified Zephyr board target
|
|
1222
|
+
* ('esp32s3_devkitc/esp32s3/procpu'). Bare SoC names are not targets —
|
|
1223
|
+
* every board resolves through the catalog like any other.
|
|
1224
|
+
*/
|
|
1225
|
+
export function generateBoard(target, opts) {
|
|
1226
|
+
const found = findBoardData(target);
|
|
1227
|
+
if (!found) {
|
|
1228
|
+
const hint = loadBoardCatalogOverlay()
|
|
1229
|
+
? `'${target}' is not a board target in the current catalog. ` +
|
|
1230
|
+
`It may be new in your Zephyr tree — run 'cuttlefish board sync' and retry.`
|
|
1231
|
+
: `No board catalog on this machine. Run 'cuttlefish board sync' first.`;
|
|
1232
|
+
throw new Error(hint);
|
|
1233
|
+
}
|
|
1234
|
+
// The as-built snapshot (this project's last successful build's resolved
|
|
1235
|
+
// zephyr.dts): the stable name-grammar routes shadow the walker's harvest
|
|
1236
|
+
// per pin — a broken catalog regex cannot reach the user's board module.
|
|
1237
|
+
// Ignored when absent, unparseable, or written for a different board.
|
|
1238
|
+
let entry = found;
|
|
1239
|
+
const asBuiltWarnings = [];
|
|
1240
|
+
let asBuiltApplied = false;
|
|
1241
|
+
if (opts?.asBuiltJson !== undefined) {
|
|
1242
|
+
try {
|
|
1243
|
+
const asBuilt = parseAsBuiltJson(opts.asBuiltJson);
|
|
1244
|
+
if (asBuilt.board.toLowerCase() === found.identifier.toLowerCase()) {
|
|
1245
|
+
entry = mergeAsBuilt(found, asBuilt, asBuiltWarnings);
|
|
1246
|
+
asBuiltApplied = true;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
catch (err) {
|
|
1250
|
+
asBuiltWarnings.push(`ignoring as-built snapshot: ${err.message}`);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
// The project's cuttlefish.facts.json, when present: the section for THIS
|
|
1254
|
+
// board merges into the manifest (user routes win per pin), and the raw
|
|
1255
|
+
// text hashes into the module's source fingerprint so edits regenerate.
|
|
1256
|
+
let facts;
|
|
1257
|
+
let suffix = '';
|
|
1258
|
+
if (opts?.factsJson !== undefined) {
|
|
1259
|
+
facts = userFactsForTarget(parseUserFactsJson(opts.factsJson), entry.identifier) ?? undefined;
|
|
1260
|
+
suffix = factsFingerprint(opts.factsJson);
|
|
1261
|
+
}
|
|
1262
|
+
// The hash only joins the fingerprint when the snapshot actually applied
|
|
1263
|
+
// (a foreign-board snapshot is ignored and must not force regeneration).
|
|
1264
|
+
if (asBuiltApplied && opts?.asBuiltJson !== undefined)
|
|
1265
|
+
suffix += factsFingerprint(opts.asBuiltJson);
|
|
1266
|
+
return buildModule(entry, facts, suffix, asBuiltWarnings);
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* Merge as-built routes over the walker's harvested routes per PAD
|
|
1270
|
+
* (port+bit). Identical routes pass silently; a disagreement warns and the
|
|
1271
|
+
* as-built route wins — the resolved devicetree is ground truth for
|
|
1272
|
+
* DT-expressible facts. Analog-device cross-checks are not re-applied: the
|
|
1273
|
+
* build already proved the device exists.
|
|
1274
|
+
*/
|
|
1275
|
+
function mergeAsBuilt(entry, asBuilt, warnings) {
|
|
1276
|
+
const merge = (harvested, asBuiltRoutes, kind) => {
|
|
1277
|
+
const out = [];
|
|
1278
|
+
const asByPad = new Map(asBuiltRoutes.map((r) => [`${r.port}${r.bit}`, r]));
|
|
1279
|
+
for (const h of harvested ?? []) {
|
|
1280
|
+
const a = asByPad.get(`${h.port}${h.bit}`);
|
|
1281
|
+
if (!a) {
|
|
1282
|
+
out.push(h);
|
|
1283
|
+
continue;
|
|
1284
|
+
}
|
|
1285
|
+
asByPad.delete(`${h.port}${h.bit}`);
|
|
1286
|
+
if (a.source !== h.source || a.channel !== h.channel) {
|
|
1287
|
+
warnings.push(`as-built ${kind} on ${h.port}${h.bit}: build says ${a.source} ch ${a.channel}, ` +
|
|
1288
|
+
`harvest said ${h.source} ch ${h.channel} — the build wins.`);
|
|
1289
|
+
}
|
|
1290
|
+
out.push({ ...h, source: a.source, channel: a.channel, pinctrl: a.pinctrl });
|
|
1291
|
+
}
|
|
1292
|
+
// As-built pads the harvest never found (a broken regex's blind spot).
|
|
1293
|
+
for (const a of asByPad.values()) {
|
|
1294
|
+
out.push({ source: a.source, channel: a.channel, port: a.port, bit: a.bit, pinctrl: a.pinctrl });
|
|
1295
|
+
}
|
|
1296
|
+
return out;
|
|
1297
|
+
};
|
|
1298
|
+
return {
|
|
1299
|
+
...entry,
|
|
1300
|
+
adcPins: merge(entry.adcPins, asBuilt.routes.adcPins, 'adc route'),
|
|
1301
|
+
pwmPins: merge(entry.pwmPins, asBuilt.routes.pwmPins, 'pwm route'),
|
|
1302
|
+
dacPins: merge(entry.dacPins, asBuilt.routes.dacPins, 'dac route'),
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
/**
|
|
1306
|
+
* Generate the board module for a CONTRACT board — a custom PCB with no
|
|
1307
|
+
* Zephyr board record. The contract (exported from a TypeCAD project)
|
|
1308
|
+
* supplies the wired pads by datasheet name and which bus families the PCB
|
|
1309
|
+
* routes; the SoC's bus controller nodelabels come from the INSTALLED
|
|
1310
|
+
* Zephyr tree's soc dtsi (the SDK is the source of truth — a contract build
|
|
1311
|
+
* compiles against that tree).
|
|
1312
|
+
*
|
|
1313
|
+
* Returns a full board module (the transpiler's pin map + chip resolution
|
|
1314
|
+
* read board.json). The caller layers the narrowed board.ts on top, so the
|
|
1315
|
+
* firmware can only touch pads the PCB actually wired.
|
|
1316
|
+
*/
|
|
1317
|
+
export function generateBoardModuleFromContract(opts) {
|
|
1318
|
+
const pads = [];
|
|
1319
|
+
for (const name of opts.pinNames) {
|
|
1320
|
+
const parsed = parsePinName(opts.soc, name);
|
|
1321
|
+
if (!parsed) {
|
|
1322
|
+
console.error(`contract: pad '${name}' does not match ${opts.soc} naming conventions — skipped`);
|
|
1323
|
+
continue;
|
|
1324
|
+
}
|
|
1325
|
+
pads.push({ name, controller: parsed.controller, pin: parsed.pin });
|
|
1326
|
+
}
|
|
1327
|
+
if (pads.length === 0) {
|
|
1328
|
+
throw new Error(`None of the contract's pads (${opts.pinNames.join(', ')}) match ${opts.soc} naming ` +
|
|
1329
|
+
`conventions — name them by their datasheet form (PA5, P0.28, GP25, GPIO9).`);
|
|
1330
|
+
}
|
|
1331
|
+
// SoC bus labels from the installed tree, gated to the wired families.
|
|
1332
|
+
const treeBuses = socBusLabelsFromTree(opts.zephyrBase, opts.soc);
|
|
1333
|
+
const buses = {
|
|
1334
|
+
i2c: opts.peripherals.i2c ? treeBuses.i2c : [],
|
|
1335
|
+
spi: opts.peripherals.spi ? treeBuses.spi : [],
|
|
1336
|
+
uart: opts.peripherals.uart ? treeBuses.uart : [],
|
|
1337
|
+
};
|
|
1338
|
+
// Synthetic catalog record shaped like a walker record, routed through the
|
|
1339
|
+
// SAME module builder as every board: the wired pads ride in as a
|
|
1340
|
+
// 'contract' connector (labels = datasheet names), and deriveControllers
|
|
1341
|
+
// builds the controller table from them like it does for any board.
|
|
1342
|
+
// Functional aliases (TX = PA9) ride the contract connector under a
|
|
1343
|
+
// second label — buildModule's connectorLabelOwner exports them as
|
|
1344
|
+
// TX = PA9 and stamps pins.aliases.TX.
|
|
1345
|
+
const padEntries = pads.map((p) => [p.name, { controller: p.controller, pin: p.pin, flags: [] }]);
|
|
1346
|
+
for (const a of opts.padAliases ?? []) {
|
|
1347
|
+
const pad = pads.find((p) => p.name === a.padName);
|
|
1348
|
+
if (pad)
|
|
1349
|
+
padEntries.push([a.exportName, { controller: pad.controller, pin: pad.pin, flags: [] }]);
|
|
1350
|
+
}
|
|
1351
|
+
const entry = {
|
|
1352
|
+
identifier: `contract/${opts.soc}`,
|
|
1353
|
+
name: `${opts.soc} custom board (contract)`,
|
|
1354
|
+
vendor: 'typecad',
|
|
1355
|
+
dts: '',
|
|
1356
|
+
buses: Object.values(buses).some((b) => b.length > 0) ? buses : undefined,
|
|
1357
|
+
connectors: padEntries.length > 0
|
|
1358
|
+
? [{
|
|
1359
|
+
nodelabel: 'contract',
|
|
1360
|
+
compatible: 'typecad,contract',
|
|
1361
|
+
pins: Object.fromEntries(padEntries),
|
|
1362
|
+
}]
|
|
1363
|
+
: undefined,
|
|
1364
|
+
};
|
|
1365
|
+
return buildModule(entry);
|
|
1366
|
+
}
|