@typecad/framework-zephyr 1.0.0-alpha.14 → 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 -47
- 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/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 +267 -10
- 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 +319 -0
- package/dist/chips/xiao-ble.js +21 -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 +72 -1
- package/dist/dt-config/kconfig.js +115 -26
- package/dist/dt-config/overlay.js +548 -28
- package/dist/framework.manifest.d.ts +3 -3
- package/dist/framework.manifest.js +122 -185
- package/dist/index.d.ts +4 -1
- package/dist/index.js +8 -2
- package/dist/lowering/adc.d.ts +20 -2
- package/dist/lowering/adc.js +121 -29
- 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 +82 -5
- 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 +3 -5
- package/dist/lowering/power.js +0 -4
- package/dist/lowering/preferences.js +49 -77
- package/dist/lowering/pulse.js +0 -31
- package/dist/lowering/pwm.d.ts +12 -1
- package/dist/lowering/pwm.js +78 -14
- 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 +16 -4
- 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 +97 -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 +839 -222
- 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 +211 -71
- 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 +650 -70
- package/dist/toolchain/scaffold.js +39 -7
- package/dist/toolchain/west-discover.d.ts +4 -0
- package/dist/toolchain/west-discover.js +17 -6
- 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 +15 -15
- package/src/as-built.ts +206 -0
- package/src/boardgen.ts +1428 -0
- package/src/chips/index.ts +35 -54
- package/src/chips/resolve.ts +285 -10
- package/src/chips/types.ts +325 -1
- 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 +165 -18
- package/src/dt-config/overlay.ts +537 -25
- package/src/framework.manifest.ts +123 -185
- package/src/index.ts +57 -41
- package/src/lowering/adc.ts +222 -118
- 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 -155
- 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 +3 -5
- package/src/lowering/preferences.ts +327 -354
- package/src/lowering/pwm.ts +167 -99
- 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 +97 -112
- package/src/sdk/board-catalog-sync.ts +62 -0
- package/src/strategy.ts +848 -237
- 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 +1286 -645
- package/src/toolchain/scaffold.ts +38 -7
- package/src/toolchain/west-discover.ts +19 -6
- 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 -82
- 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/toolchain/index.js
CHANGED
|
@@ -19,29 +19,28 @@
|
|
|
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, existsSync } from 'node:fs';
|
|
22
|
+
import { readdirSync, readFileSync, mkdirSync, rmSync, existsSync, writeFileSync } from 'node:fs';
|
|
23
23
|
import { parseCompileErrors } from '@typecad/cuttlefish/api/shared';
|
|
24
24
|
import { scaffoldZephyrProject, writeIfChanged, appendLibraryOverlayFragments } from './scaffold.js';
|
|
25
|
+
import { parseZephyrDts, asBuiltJson } from '../as-built.js';
|
|
25
26
|
import { westSpawn, buildEnv } from './west-spawn.js';
|
|
26
27
|
import { discoverWest } from './west-discover.js';
|
|
27
28
|
import { writeDebugConfig, resolveDebugLocations } from './debug-config.js';
|
|
29
|
+
import { bossacTouchReset } from './bossac-touch.js';
|
|
28
30
|
import { ZephyrStrategy } from '../strategy.js';
|
|
29
31
|
import { generateOverlay } from '../dt-config/overlay.js';
|
|
30
|
-
import {
|
|
32
|
+
import { generateCustomBoard } from '../dt-config/custom-board.js';
|
|
33
|
+
import { NO_BOARD_CHIP } from '../chips/index.js';
|
|
31
34
|
import { resolveChipFromBoard } from '../chips/resolve.js';
|
|
32
35
|
import { pwmDtAliasToken } from '../lowering/pwm.js';
|
|
33
36
|
import { detectZephyrVersion, checkZephyrCompat, resolveBoardTarget } from './compat.js';
|
|
34
37
|
import { DEFAULT_ZEPHYR_DISPLAY_PROFILE } from '../display/profiles.js';
|
|
35
|
-
/** Default board target — the framework's MVP canonical board. */
|
|
36
|
-
const DEFAULT_BOARD = 'xiao_ble';
|
|
37
38
|
/**
|
|
38
39
|
* Resolve the chip for a build the same way the strategy does at emit time —
|
|
39
40
|
* from the board constants the transpile persisted next to the emitted
|
|
40
|
-
* source (`board-constants.json`)
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* XIAO default and the overlay generator would emit wrong controller labels
|
|
44
|
-
* (e.g. `&uart0` on an STM32, whose node is `usart1`).
|
|
41
|
+
* source (`board-constants.json`). There is no registry fallback: a build
|
|
42
|
+
* whose constants did not persist stays NO_BOARD_CHIP, exactly like the
|
|
43
|
+
* emit-time path.
|
|
45
44
|
*/
|
|
46
45
|
function chipForBuild(projectRoot, board) {
|
|
47
46
|
try {
|
|
@@ -52,13 +51,14 @@ function chipForBuild(projectRoot, board) {
|
|
|
52
51
|
.find(p => existsSync(p));
|
|
53
52
|
if (bcPath) {
|
|
54
53
|
const raw = JSON.parse(readFileSync(bcPath, 'utf8'));
|
|
55
|
-
const
|
|
54
|
+
const bc = new Map(Object.entries(raw));
|
|
55
|
+
const fromBoard = resolveChipFromBoard(bc);
|
|
56
56
|
if (fromBoard)
|
|
57
57
|
return fromBoard;
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
-
catch { /*
|
|
61
|
-
return
|
|
60
|
+
catch { /* constants unreadable — no board resolved */ }
|
|
61
|
+
return NO_BOARD_CHIP;
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
64
64
|
* HAL pins the emitted sources read via adc.* — scanned from the emitted
|
|
@@ -69,15 +69,42 @@ function chipForBuild(projectRoot, board) {
|
|
|
69
69
|
*/
|
|
70
70
|
function scanAdcReadPins(src, chip) {
|
|
71
71
|
const pins = [];
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
72
|
+
// Two used-signal forms:
|
|
73
|
+
// - `__tc_adc<N>_setup()` / `__tc_adc_<ctrl>_<N>_setup()` CALL SITES
|
|
74
|
+
// (empty parens — the adcInitLines definitions have `(void)` and would
|
|
75
|
+
// otherwise mark every descriptor channel as used).
|
|
76
|
+
// - `__tc_adct<pin>_done` lazy-guard vars — the thin-ADC read lowering's
|
|
77
|
+
// inline setup (families without pinctrl groups, e.g. ESP32 SARADC,
|
|
78
|
+
// never emit the setup-function form at all).
|
|
79
|
+
// Channel indices are unique per CONTROLLER, so the labeled form resolves
|
|
80
|
+
// (controller, channel) before mapping back to the HAL pin.
|
|
81
|
+
for (const m of src.matchAll(/__tc_adc(?:(\w+?)_)?(\d+)_setup\(\)/g)) {
|
|
82
|
+
const ch = Number(m[2]);
|
|
83
|
+
const label = m[1];
|
|
84
|
+
const c = chip.adc?.channels.find((x) => x.channel === ch && (x.controller ?? chip.adc?.nodeLabel) === (label ?? chip.adc?.nodeLabel));
|
|
78
85
|
if (c && !pins.includes(c.pin))
|
|
79
86
|
pins.push(c.pin);
|
|
80
87
|
}
|
|
88
|
+
for (const m of src.matchAll(/__tc_adct(\d+)_done/g)) {
|
|
89
|
+
const pin = Number(m[1]);
|
|
90
|
+
if (chip.adc?.channels.some((x) => x.pin === pin) && !pins.includes(pin))
|
|
91
|
+
pins.push(pin);
|
|
92
|
+
}
|
|
93
|
+
return pins;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* HAL pins the emitted sources drive with dac.* — the DAC lowering's lazy
|
|
97
|
+
* per-pin setup guard is `__tc_dact<pin>_done` (lowering/dac.ts), so
|
|
98
|
+
* var-presence is the authoritative used-signal. Feeds the overlay's DAC
|
|
99
|
+
* pinctrl gating (same pattern as scanAdcReadPins).
|
|
100
|
+
*/
|
|
101
|
+
function scanDacWritePins(src) {
|
|
102
|
+
const pins = [];
|
|
103
|
+
for (const m of src.matchAll(/__tc_dact(\d+)_done/g)) {
|
|
104
|
+
const pin = Number(m[1]);
|
|
105
|
+
if (!pins.includes(pin))
|
|
106
|
+
pins.push(pin);
|
|
107
|
+
}
|
|
81
108
|
return pins;
|
|
82
109
|
}
|
|
83
110
|
/**
|
|
@@ -87,16 +114,150 @@ function scanAdcReadPins(src, chip) {
|
|
|
87
114
|
* var-presence is the authoritative signal. Feeds the overlay's per-pin
|
|
88
115
|
* pwm-leds gating (no dead DT channels).
|
|
89
116
|
*/
|
|
117
|
+
/**
|
|
118
|
+
* Inline-override markers (the escape hatch): the adc/pwm lowerings emit
|
|
119
|
+
* `/* cuttlefish-user-facts: <kind> pin=N [device=X] [pinctrl=P] channel=C */`
|
|
120
|
+
* comments when a construction carries routing overrides. Merged into the
|
|
121
|
+
* chip so the overlay synthesis + used-pin scans treat them as facts —
|
|
122
|
+
* the transpiler cannot create DT nodes, but this regen can.
|
|
123
|
+
*/
|
|
124
|
+
function applyUserFactMarkers(chip, src) {
|
|
125
|
+
const adcAdds = [];
|
|
126
|
+
const pwmAdds = [];
|
|
127
|
+
for (const m of src.matchAll(/\/\* cuttlefish-user-facts: (adc|pwm) ([^*]*?) \*\//g)) {
|
|
128
|
+
const kind = m[1];
|
|
129
|
+
const fields = new Map();
|
|
130
|
+
for (const kv of m[2].split(/\s+/).filter(Boolean)) {
|
|
131
|
+
const eq = kv.indexOf('=');
|
|
132
|
+
if (eq > 0)
|
|
133
|
+
fields.set(kv.slice(0, eq), kv.slice(eq + 1));
|
|
134
|
+
}
|
|
135
|
+
const pin = Number(fields.get('pin'));
|
|
136
|
+
const channel = Number(fields.get('channel'));
|
|
137
|
+
if (!Number.isFinite(pin) || !Number.isFinite(channel))
|
|
138
|
+
continue;
|
|
139
|
+
if (kind === 'adc') {
|
|
140
|
+
adcAdds.push({
|
|
141
|
+
pin,
|
|
142
|
+
channel,
|
|
143
|
+
...(fields.get('device') ? { controller: fields.get('device') } : {}),
|
|
144
|
+
...(fields.get('pinctrl') ? { pinctrl: fields.get('pinctrl') } : {}),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
else if (fields.get('controller')) {
|
|
148
|
+
pwmAdds.push({ pin, channel, controller: fields.get('controller') });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (adcAdds.length === 0 && pwmAdds.length === 0)
|
|
152
|
+
return chip;
|
|
153
|
+
const adc = chip.adc
|
|
154
|
+
? chip.adc
|
|
155
|
+
: { nodeLabel: adcAdds.find((a) => !a.controller)?.controller ?? 'adc', resolution: 12, vrefMv: 3000, channels: [] };
|
|
156
|
+
const adcChannels = [...adc.channels];
|
|
157
|
+
for (const a of adcAdds) {
|
|
158
|
+
const existing = adcChannels.findIndex((c) => c.pin === a.pin);
|
|
159
|
+
if (existing >= 0)
|
|
160
|
+
adcChannels.splice(existing, 1);
|
|
161
|
+
adcChannels.push({ pin: a.pin, channel: a.channel, ...(a.controller ? { controller: a.controller } : {}), ...(a.pinctrl ? { pinctrl: a.pinctrl } : {}) });
|
|
162
|
+
}
|
|
163
|
+
const pwmSpecs = [...(chip.pwm?.specs ?? [])];
|
|
164
|
+
for (const p of pwmAdds) {
|
|
165
|
+
const existing = pwmSpecs.findIndex((s) => s.pin === p.pin);
|
|
166
|
+
if (existing >= 0)
|
|
167
|
+
pwmSpecs.splice(existing, 1);
|
|
168
|
+
pwmSpecs.push({ pin: p.pin, controller: p.controller, channel: p.channel });
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
...chip,
|
|
172
|
+
adc: { ...adc, channels: adcChannels },
|
|
173
|
+
pwm: { ...(chip.pwm ?? { specs: [] }), specs: pwmSpecs },
|
|
174
|
+
};
|
|
175
|
+
}
|
|
90
176
|
function scanPwmUsedPins(src, chip) {
|
|
91
|
-
|
|
177
|
+
const pins = (chip.pwm?.specs ?? [])
|
|
92
178
|
.filter((s) => src.includes(`__tc_pwm_${pwmDtAliasToken(s)}`))
|
|
93
179
|
.map((s) => s.pin);
|
|
180
|
+
// Matrix pins (ESP32 LEDC) have no static specs — recover the driven pins
|
|
181
|
+
// from the same alias-var presence signal (`__tc_pwm_tc_pwm<N>`), keeping
|
|
182
|
+
// only pins the descriptor's matrix allows (the regex grabs the full
|
|
183
|
+
// number, so pin 4 never matches a reference to pin 45).
|
|
184
|
+
const matrix = chip.pwm?.matrix;
|
|
185
|
+
if (matrix) {
|
|
186
|
+
const present = new Set();
|
|
187
|
+
for (const match of src.matchAll(/__tc_pwm_tc_pwm(\d+)/g)) {
|
|
188
|
+
present.add(Number(match[1]));
|
|
189
|
+
}
|
|
190
|
+
for (const pin of matrix.pins) {
|
|
191
|
+
if (present.has(pin))
|
|
192
|
+
pins.push(pin);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return pins;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Bus controller indexes the emitted sources actually reference — the shim
|
|
199
|
+
* declares one `__tc_<bus><N>_dev` state block per used instance (gated by
|
|
200
|
+
* collectUsedBusIndices at transpile time), so var-presence is the
|
|
201
|
+
* authoritative signal. The overlay enables only these controllers: an
|
|
202
|
+
* enabled-but-unused one claims its default pins (i2c0's GP4/GP5 on the
|
|
203
|
+
* Pico) which a program driving the OTHER controller may want as GPIO.
|
|
204
|
+
* Empty list (no state blocks — e.g. display/touch composites that use the
|
|
205
|
+
* driver API directly) means "no signal"; the caller then passes undefined
|
|
206
|
+
* so the overlay enables every declared controller, preserving old behavior.
|
|
207
|
+
*/
|
|
208
|
+
function scanUsedBusInstances(src, controllers, bus) {
|
|
209
|
+
if (!controllers)
|
|
210
|
+
return undefined;
|
|
211
|
+
const used = [];
|
|
212
|
+
for (let i = 0; i < controllers.length; i++) {
|
|
213
|
+
if (src.includes(`__tc_${bus}${i}_dev`))
|
|
214
|
+
used.push(i);
|
|
215
|
+
}
|
|
216
|
+
return used.length > 0 ? used : undefined;
|
|
217
|
+
}
|
|
218
|
+
/** Scan the emitted source for thin SPI targets (hal/spi-target.ts): the
|
|
219
|
+
* spi_dt_spec state block's tc-spit-cfg comment carries the construction
|
|
220
|
+
* facts, the same channel tc-sensor-cfg uses. */
|
|
221
|
+
export function scanSpiTargets(src) {
|
|
222
|
+
const out = new Map();
|
|
223
|
+
for (const m of src.matchAll(/tc-spit-cfg: tc_spit_spi(\d+)_cs(\d+) hz=(\d+) mode=(\d+)/g)) {
|
|
224
|
+
const ref = { busIndex: parseInt(m[1], 10), cs: parseInt(m[2], 10), hz: parseInt(m[3], 10), mode: parseInt(m[4], 10) };
|
|
225
|
+
out.set(`${ref.busIndex}|${ref.cs}`, ref);
|
|
226
|
+
}
|
|
227
|
+
return [...out.values()];
|
|
228
|
+
}
|
|
229
|
+
export function scanSensorParts(src) {
|
|
230
|
+
const out = new Map();
|
|
231
|
+
for (const m of src.matchAll(/__tc_sensor_([a-z0-9_]+)_(i2c|spi)(\d+)_(0x[0-9a-f]+|cs[0-9]+)_dev\b/g)) {
|
|
232
|
+
const port = m[4].startsWith('0x') ? parseInt(m[4], 16) : parseInt(m[4].slice(2), 10);
|
|
233
|
+
const ref = { part: m[1], busIndex: parseInt(m[3], 10), port, busKind: m[2], spiHz: 0, spiMode: 0, alertPin: -1 };
|
|
234
|
+
out.set(`${ref.part}|${ref.busKind}${ref.busIndex}|${ref.port}`, ref);
|
|
235
|
+
}
|
|
236
|
+
// Construction facts ride the state block's config comment — merge by
|
|
237
|
+
// nodelabel so the scanner stays the single source for the overlay.
|
|
238
|
+
for (const m of src.matchAll(/tc-sensor-cfg: tc_([a-z0-9_]+)_(i2c|spi)(\d+)_(0x[0-9a-f]+|cs[0-9]+) hz=(\d+) mode=(\d+) alert=(-?\d+)/g)) {
|
|
239
|
+
const port = m[4].startsWith('0x') ? parseInt(m[4], 16) : parseInt(m[4].slice(2), 10);
|
|
240
|
+
const key = `${m[1]}|${m[2]}${m[3]}|${port}`;
|
|
241
|
+
const existing = out.get(key);
|
|
242
|
+
if (existing) {
|
|
243
|
+
existing.spiHz = parseInt(m[5], 10);
|
|
244
|
+
existing.spiMode = parseInt(m[6], 10);
|
|
245
|
+
existing.alertPin = parseInt(m[7], 10);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return [...out.values()];
|
|
94
249
|
}
|
|
95
250
|
function targetFromOptions(o) {
|
|
96
|
-
// The cuttlefish CLI populates ToolchainOptions.buildTarget from
|
|
97
|
-
// config
|
|
251
|
+
// The cuttlefish CLI populates ToolchainOptions.buildTarget from the
|
|
252
|
+
// config's board: (frameworkData.buildTarget for board-less projects).
|
|
253
|
+
// Accept frameworkData.target as an alias. No silent default: building for
|
|
254
|
+
// a wrong hard-coded board is the split-brain trap.
|
|
98
255
|
const fcTarget = o.frameworkConfig?.target;
|
|
99
|
-
|
|
256
|
+
const board = o.buildTarget ?? fcTarget;
|
|
257
|
+
if (!board) {
|
|
258
|
+
throw new Error('No build target: set board: in cuttlefish.config.ts (or frameworkData.buildTarget for custom-board projects).');
|
|
259
|
+
}
|
|
260
|
+
return board;
|
|
100
261
|
}
|
|
101
262
|
/**
|
|
102
263
|
* Derive the Zephyr project root from the cuttlefish-emitted source path.
|
|
@@ -119,32 +280,188 @@ export function projectRootFromOptions(o) {
|
|
|
119
280
|
*/
|
|
120
281
|
const BUILD_TIMEOUT_MS = 600_000;
|
|
121
282
|
const FLASH_TIMEOUT_MS = 120_000;
|
|
283
|
+
export function resolveProbeMethod(zc, chip, purpose = 'flash') {
|
|
284
|
+
const probe = zc?.probe;
|
|
285
|
+
const runner = zc?.runner;
|
|
286
|
+
const userArgs = zc?.runnerArgs ?? [];
|
|
287
|
+
if (probe && runner) {
|
|
288
|
+
return {
|
|
289
|
+
ok: false,
|
|
290
|
+
error: `cuttlefish.config.ts sets both zephyr.probe ('${probe}') and zephyr.runner ('${runner}'). ` +
|
|
291
|
+
`They are two ways to choose the probe method — remove one.`,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
if (probe) {
|
|
295
|
+
const methods = chip.probeMethods ?? [];
|
|
296
|
+
const method = methods.find((m) => m.id === probe);
|
|
297
|
+
if (!method) {
|
|
298
|
+
const listAll = methods
|
|
299
|
+
.map((m) => `${m.id} (${m.runner}${m.description ? ` — ${m.description}` : ''})`)
|
|
300
|
+
.join('; ');
|
|
301
|
+
return {
|
|
302
|
+
ok: false,
|
|
303
|
+
error: methods.length > 0
|
|
304
|
+
? `Unknown probe method '${probe}' for ${chip.id}. Supported: ${listAll}.`
|
|
305
|
+
: `This board (${chip.id}) ships no probe-method table, so 'zephyr.probe' cannot resolve '${probe}'. ` +
|
|
306
|
+
`Use the raw 'zephyr.runner' field instead (run 'west flash --context' in the build dir for options).`,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
if (purpose === 'debug' && method.debug === false) {
|
|
310
|
+
const debuggable = methods.filter((m) => m.debug !== false).map((m) => m.id).join(', ');
|
|
311
|
+
return {
|
|
312
|
+
ok: false,
|
|
313
|
+
error: `The '${probe}' method cannot debug ${chip.id} — a bootloader is not a debugger. ` +
|
|
314
|
+
`Debug-capable methods: ${debuggable || '(none — this board needs an external probe)'}.`,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
ok: true,
|
|
319
|
+
runner: method.runner,
|
|
320
|
+
args: [...(method.args ?? []), ...userArgs],
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
return { ok: true, runner, args: userArgs };
|
|
324
|
+
}
|
|
122
325
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
326
|
+
* Run an openocd session against the board's probe config — the shared
|
|
327
|
+
* engine for the pre-flash quiesce and the post-flash SYSRESETREQ (see the
|
|
328
|
+
* call sites in upload()). The config is the probe method's verbatim
|
|
329
|
+
* debugCfg from the board catalog (the same lines `west debug` uses);
|
|
330
|
+
* `commands` are appended after `-f <cfg> -c init`. Returns a flash note on
|
|
331
|
+
* success, undefined when skipped or failed (best-effort by design).
|
|
332
|
+
*/
|
|
333
|
+
function openocdProbeSession(buildDir, zc, chip, commands) {
|
|
334
|
+
// Config resolution — two sources, in order:
|
|
335
|
+
// 1. The named probe method's verbatim debugCfg from the board catalog
|
|
336
|
+
// (written to a temp cfg), when zephyr.probe names a method that has
|
|
337
|
+
// one.
|
|
338
|
+
// 2. The board's own support/openocd.cfg in the Zephyr tree — the exact
|
|
339
|
+
// config `west flash` resolves for the openocd runner. This covers
|
|
340
|
+
// raw `zephyr.runner: 'openocd'` (no named probe) and probe methods
|
|
341
|
+
// that ship no debugCfg of their own.
|
|
342
|
+
const probeId = zc?.probe;
|
|
343
|
+
const method = chip.probeMethods?.find((m) => m.id === probeId);
|
|
344
|
+
const cfgLines = method?.debugCfg;
|
|
345
|
+
// Session reset policy: `reset_config none`. The session's resets are
|
|
346
|
+
// core-domain by design — vector-catch halt before the flash write,
|
|
347
|
+
// SYSRESETREQ to boot — so they must not depend on the SRST pin. Boards
|
|
348
|
+
// like the WeAct Black Pill don't break NRST out at all: under the board
|
|
349
|
+
// cfg's `srst_only`, every `reset` asserts a pin that reaches nothing
|
|
350
|
+
// (the target never resets, `reset halt` catches the core mid-app in
|
|
351
|
+
// dirty state and the flash algorithm times out) while the probe's
|
|
352
|
+
// floating SRST sense reports phantom "external reset detected" events
|
|
353
|
+
// that leave the session's halt state inconsistent. Method-declared west
|
|
354
|
+
// quirks (`--cmd-pre-init=…`) are appended after and override the
|
|
355
|
+
// default for boards whose facts carry one.
|
|
356
|
+
const preInit = [
|
|
357
|
+
'reset_config none',
|
|
358
|
+
...(method?.args ?? [])
|
|
359
|
+
.filter((a) => a.startsWith('--cmd-pre-init='))
|
|
360
|
+
.map((a) => a.slice('--cmd-pre-init='.length)),
|
|
361
|
+
];
|
|
362
|
+
const install = discoverWest();
|
|
363
|
+
const sdkRoot = process.env.ZEPHYR_SDK_INSTALL_DIR || install?.sdkInstallDir;
|
|
364
|
+
if (!sdkRoot)
|
|
365
|
+
return undefined;
|
|
366
|
+
const openocdExe = join(sdkRoot, 'hosttools', 'openocd', 'bin', process.platform === 'win32' ? 'openocd.exe' : 'openocd');
|
|
367
|
+
if (!existsSync(openocdExe))
|
|
368
|
+
return undefined;
|
|
369
|
+
// Script search path: the SDK layouts differ across versions — prefer the
|
|
370
|
+
// share/ form west's own runner uses, fall back to the scripts/ form.
|
|
371
|
+
const shareScripts = join(sdkRoot, 'hosttools', 'openocd', 'share', 'openocd', 'scripts');
|
|
372
|
+
const binScripts = join(sdkRoot, 'hosttools', 'openocd', 'scripts');
|
|
373
|
+
const searchDir = existsSync(shareScripts) ? shareScripts : binScripts;
|
|
374
|
+
let cfgArgs;
|
|
375
|
+
let sessionCfg;
|
|
376
|
+
if (cfgLines && cfgLines.length > 0) {
|
|
377
|
+
sessionCfg = join(buildDir, 'cuttlefish-probe.cfg');
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
// The board target's qualifier ('blackpill_f411ce/stm32f411xe' →
|
|
381
|
+
// 'blackpill_f411ce') identifies the board dir; the vendor segment is
|
|
382
|
+
// not part of the target, so probe the boards/ tree for it.
|
|
383
|
+
const zephyrBase = process.env.ZEPHYR_BASE || install?.zephyrBase;
|
|
384
|
+
const boardDir = (chip.id ?? '').split('/')[0];
|
|
385
|
+
if (!zephyrBase || !boardDir)
|
|
386
|
+
return undefined;
|
|
387
|
+
const boardsRoot = join(zephyrBase, 'boards');
|
|
388
|
+
let supportCfg;
|
|
389
|
+
try {
|
|
390
|
+
for (const vendor of readdirSync(boardsRoot)) {
|
|
391
|
+
const candidate = join(boardsRoot, vendor, boardDir, 'support', 'openocd.cfg');
|
|
392
|
+
if (existsSync(candidate)) {
|
|
393
|
+
supportCfg = candidate;
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
return undefined;
|
|
400
|
+
}
|
|
401
|
+
if (!supportCfg)
|
|
402
|
+
return undefined;
|
|
403
|
+
cfgArgs = ['-s', dirname(supportCfg), '-f', supportCfg];
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
mkdirSync(buildDir, { recursive: true });
|
|
407
|
+
if (sessionCfg) {
|
|
408
|
+
writeFileSync(sessionCfg, cfgLines.join('\n') + '\n', 'utf-8');
|
|
409
|
+
cfgArgs = ['-f', sessionCfg];
|
|
410
|
+
}
|
|
411
|
+
const res = spawnSync(openocdExe, [
|
|
412
|
+
'-s', searchDir, ...cfgArgs,
|
|
413
|
+
// Pre-init TCL AFTER the cfg (overrides its reset_config) and BEFORE
|
|
414
|
+
// init — the same position west gives --cmd-pre-init.
|
|
415
|
+
...preInit.map((c) => ['-c', c]).flat(),
|
|
416
|
+
'-c', 'init',
|
|
417
|
+
...commands.map((c) => ['-c', c]).flat(),
|
|
418
|
+
'-c', 'shutdown',
|
|
419
|
+
], {
|
|
420
|
+
cwd: buildDir,
|
|
421
|
+
encoding: 'utf-8',
|
|
422
|
+
timeout: 20_000,
|
|
423
|
+
});
|
|
424
|
+
return res.status === 0 ? `-- probe session ok: ${commands.join('; ')}` : undefined;
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
return undefined;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Whether a flash runner carries the upload over a serial port. Runner-gated,
|
|
432
|
+
* never board-name-gated: esptool and bossac are the only runners
|
|
433
|
+
* `buildFlashArgs` forwards `--port` to, so they are the only ones that
|
|
434
|
+
* cannot flash without one. Probe runners (openocd, jlink) and USB flows
|
|
435
|
+
* (dfu-util, uf2 mass storage) need no port — a missing `--port` must not
|
|
436
|
+
* block them.
|
|
136
437
|
*
|
|
137
|
-
* Exported (pure) so the
|
|
438
|
+
* Exported (pure) so the port-requirement contract is unit-testable without
|
|
138
439
|
* spawning west.
|
|
139
440
|
*/
|
|
140
|
-
export function
|
|
441
|
+
export function uploadRequiresPort(runner) {
|
|
442
|
+
return runner === 'esptool' || runner === 'bossac';
|
|
443
|
+
}
|
|
444
|
+
export function buildFlashArgs(buildDir, userRunner, port, flashRunner, runnerArgs) {
|
|
445
|
+
// flashRunner is the runner the flash will actually use — the explicit
|
|
446
|
+
// zephyr.runner when set, else the board's declared default from its probe
|
|
447
|
+
// table. It gates the esptool port forwarding below; only an EXPLICIT
|
|
448
|
+
// userRunner forces west's --runner (board defaults stay board.cmake's
|
|
449
|
+
// choice). Runner-gated, never board-name-gated: any board whose flash
|
|
450
|
+
// runs esptool gets the same forwarding.
|
|
141
451
|
const args = ['flash', '-d', buildDir];
|
|
142
452
|
if (userRunner) {
|
|
143
453
|
args.push('--runner', userRunner);
|
|
144
454
|
}
|
|
145
|
-
if (port &&
|
|
455
|
+
if (port && flashRunner === 'esptool') {
|
|
146
456
|
args.push('--esp-device', port);
|
|
147
457
|
}
|
|
458
|
+
// The bossac runner defaults its port to /dev/ttyACM0 — on Windows that
|
|
459
|
+
// never matches, so the port MUST be forwarded or bossac fails with
|
|
460
|
+
// "No device found on /dev/ttyACM0" (same class of port-forwarding
|
|
461
|
+
// problem as the esptool --esp-device above).
|
|
462
|
+
if (port && flashRunner === 'bossac') {
|
|
463
|
+
args.push('--bossac-port', port);
|
|
464
|
+
}
|
|
148
465
|
// Extra runner-specific flags, appended verbatim (west's runner parsers
|
|
149
466
|
// accept them after the runner is selected).
|
|
150
467
|
if (runnerArgs && runnerArgs.length > 0) {
|
|
@@ -246,8 +563,6 @@ export const Toolchain = {
|
|
|
246
563
|
// logic in scaffold via the usage scan. Mirrors how Arduino's library
|
|
247
564
|
// resolution is a pre-build artifact step.
|
|
248
565
|
const projectRoot = basename(outputDir) === 'src' ? dirname(outputDir) : outputDir;
|
|
249
|
-
const board = DEFAULT_BOARD;
|
|
250
|
-
const chip = chipForBuild(projectRoot, board);
|
|
251
566
|
// Scan the emitted source for usage tokens (same authoritative signal the
|
|
252
567
|
// scaffold uses). entryPoint is the path to main.cpp; its dir is src/.
|
|
253
568
|
const srcDir = dirname(entryPoint);
|
|
@@ -261,6 +576,12 @@ export const Toolchain = {
|
|
|
261
576
|
}
|
|
262
577
|
catch { /* src may not exist yet on first prepare */ }
|
|
263
578
|
const uses = (t) => src.includes(t);
|
|
579
|
+
// Inline-override markers (the escape hatch): the lowerings emit
|
|
580
|
+
// `cuttlefish-user-facts` comments carrying routing the transpiler
|
|
581
|
+
// cannot synthesize (adc device/pinctrl, pwm controller/channel).
|
|
582
|
+
// Merged into the chip BEFORE the scans + overlay generation, so the DT
|
|
583
|
+
// nodes, pinctrl groups, and used-pin recovery treat them as facts.
|
|
584
|
+
const chip = applyUserFactMarkers(chipForBuild(projectRoot, ''), src);
|
|
264
585
|
// Display usage tokens: the minimal GFX runtime (display_write/_fill_rect)
|
|
265
586
|
// and the UI display adapter (display_init / __tc_display_dev /
|
|
266
587
|
// DEVICE_DT_GET on the display nodelabel). Both paths need the DT overlay
|
|
@@ -277,21 +598,65 @@ export const Toolchain = {
|
|
|
277
598
|
// references (FT6336U on I2C, XPT2046 on the display's SPI bus).
|
|
278
599
|
const usesTouch = uses('ft6336u') || uses('touch_');
|
|
279
600
|
const usesXpt = uses('xpt2046');
|
|
601
|
+
const sensorParts = scanSensorParts(src);
|
|
602
|
+
const spiTargetParts = scanSpiTargets(src);
|
|
280
603
|
const overlay = generateOverlay(chip, {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
604
|
+
// __tc_<bus> matches the shim state block — a begin()-only program
|
|
605
|
+
// emits no driver API call but still declares the DT device. A
|
|
606
|
+
// constructed sensor is also a bus user (its device handle is the
|
|
607
|
+
// only i2c reference a sensor-only program carries).
|
|
608
|
+
usesI2c: uses('i2c_') || uses('__tc_i2c') || sensorParts.length > 0,
|
|
609
|
+
usesSensor: uses('sensor_') || sensorParts.length > 0,
|
|
610
|
+
usesFloatFormat: /%[-0-9.]*[eEfFgG]/.test(src),
|
|
611
|
+
sensorParts,
|
|
612
|
+
spiTargets: spiTargetParts,
|
|
613
|
+
usesSpi: uses('spi_') || uses('__tc_spi') || spiTargetParts.length > 0,
|
|
614
|
+
usesUart: uses('__tc_uart'),
|
|
615
|
+
usesUsb: uses('__tc_usb'),
|
|
284
616
|
usesPwm: uses('pwm_'),
|
|
285
617
|
usesAdc: uses('adc_'),
|
|
286
618
|
adcReadPins: scanAdcReadPins(src, chip),
|
|
619
|
+
dacWritePins: scanDacWritePins(src),
|
|
287
620
|
pwmUsedPins: scanPwmUsedPins(src, chip),
|
|
621
|
+
i2cUsedInstances: scanUsedBusInstances(src, chip.i2c?.controllers, 'i2c'),
|
|
622
|
+
spiUsedInstances: scanUsedBusInstances(src, chip.spi?.controllers, 'spi'),
|
|
623
|
+
uartUsedInstances: scanUsedBusInstances(src, chip.uart?.controllers, 'uart'),
|
|
624
|
+
// Preferences/FS — same tokens scaffoldZephyrProject scans (the ZMS
|
|
625
|
+
// settings_* API + __tc_prefs shim, the __tc_fs mount shim); drive the
|
|
626
|
+
// storage-partition synthesis + /chosen settings pointer.
|
|
627
|
+
usesPreferences: uses('settings_') || uses('__tc_prefs'),
|
|
628
|
+
usesFS: uses('__tc_fs'),
|
|
629
|
+
usesWdt: uses('wdt_'),
|
|
630
|
+
usesHwtimer: uses('counter_') || uses('__tc_hw'),
|
|
288
631
|
usesDisplay,
|
|
289
632
|
usesTouch: usesTouch || usesXpt,
|
|
290
633
|
touchController: usesXpt ? 'xpt2046' : 'ft6336u',
|
|
291
634
|
}, displayProfile);
|
|
292
635
|
const overlayDir = join(projectRoot, 'boards');
|
|
293
636
|
mkdirSync(overlayDir, { recursive: true });
|
|
294
|
-
|
|
637
|
+
// prepare() runs before the real target is known — the placeholder name
|
|
638
|
+
// never matches `west build -b <board>` (compile rewrites the overlay
|
|
639
|
+
// under the actual board's name below).
|
|
640
|
+
writeIfChanged(join(overlayDir, 'board.overlay'), overlay);
|
|
641
|
+
// Thin SPI targets need an app-local binding: a compatible-less DT node
|
|
642
|
+
// generates NO property macros, so SPI_DT_SPEC_GET's spi-max-frequency
|
|
643
|
+
// lookup would not exist. The binding has no driver — it exists so
|
|
644
|
+
// gen_defines emits the spi properties for the target nodes.
|
|
645
|
+
if (spiTargetParts.length > 0) {
|
|
646
|
+
const bindingsDir = join(projectRoot, 'dts', 'bindings');
|
|
647
|
+
mkdirSync(bindingsDir, { recursive: true });
|
|
648
|
+
writeIfChanged(join(bindingsDir, 'cuttlefish,spi-target.yaml'), [
|
|
649
|
+
'description: |',
|
|
650
|
+
' Cuttlefish thin SPITarget (hal/spi-target.ts) — a raw spi_dt_spec',
|
|
651
|
+
' peer. No driver binds this compatible; it exists so devicetree',
|
|
652
|
+
' generation emits the spi properties (spi-max-frequency,',
|
|
653
|
+
' spi-cpol/spi-cpha, reg = the cs-gpios index) that SPI_DT_SPEC_GET',
|
|
654
|
+
' consumes from the generated C++.',
|
|
655
|
+
'compatible: "cuttlefish,spi-target"',
|
|
656
|
+
'include: spi-device.yaml',
|
|
657
|
+
'',
|
|
658
|
+
].join('\n'));
|
|
659
|
+
}
|
|
295
660
|
},
|
|
296
661
|
compile(o) {
|
|
297
662
|
const projectRoot = projectRootFromOptions(o);
|
|
@@ -323,6 +688,19 @@ export const Toolchain = {
|
|
|
323
688
|
// Zephyr auto-detects boards/<board>.overlay under APPLICATION_CONFIG_DIR.
|
|
324
689
|
try {
|
|
325
690
|
const chip = chipForBuild(projectRoot, board);
|
|
691
|
+
// Custom-board generation: an MCU-only target (no board package) has no
|
|
692
|
+
// upstream Zephyr board — generate one under boards/typecad/<name>/ from
|
|
693
|
+
// the chip's silicon data. Opt-in via `zephyr.customBoard: true` in
|
|
694
|
+
// cuttlefish.config.ts; the board takes its name from the build target.
|
|
695
|
+
// Idempotent — regenerated on every compile, before the overlay pass.
|
|
696
|
+
if (zc?.customBoard === true) {
|
|
697
|
+
const generated = generateCustomBoard(projectRoot, chip, board.split('/')[0]);
|
|
698
|
+
if (!generated) {
|
|
699
|
+
throw new Error(`zephyr.customBoard is set, but the resolved chip ('${chip.id}') carries no ` +
|
|
700
|
+
`silicon board data. Custom-board generation requires an MCU-only config ` +
|
|
701
|
+
`(mcu set, board absent) whose MCU package ships a zephyr block.`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
326
704
|
const srcDir = join(projectRoot, 'src');
|
|
327
705
|
let src = '';
|
|
328
706
|
try {
|
|
@@ -408,14 +786,35 @@ export const Toolchain = {
|
|
|
408
786
|
touchWiring = { controller: 'xpt2046', ...(touchWiring ?? {}) };
|
|
409
787
|
}
|
|
410
788
|
const overlayDiagnostics = [];
|
|
789
|
+
const sensorParts = scanSensorParts(src);
|
|
790
|
+
const spiTargetParts = scanSpiTargets(src);
|
|
411
791
|
const overlay = generateOverlay(chip, {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
792
|
+
// __tc_<bus> matches the shim state block — a begin()-only program
|
|
793
|
+
// emits no driver API call but still declares the DT device. A
|
|
794
|
+
// constructed sensor is also a bus user (see scanSensorParts).
|
|
795
|
+
usesI2c: uses('i2c_') || uses('__tc_i2c') || sensorParts.length > 0,
|
|
796
|
+
usesSensor: uses('sensor_') || sensorParts.length > 0,
|
|
797
|
+
usesFloatFormat: /%[-0-9.]*[eEfFgG]/.test(src),
|
|
798
|
+
sensorParts,
|
|
799
|
+
spiTargets: spiTargetParts,
|
|
800
|
+
usesSpi: uses('spi_') || uses('__tc_spi') || spiTargetParts.length > 0,
|
|
801
|
+
usesUart: uses('__tc_uart'),
|
|
802
|
+
usesUsb: uses('__tc_usb'),
|
|
415
803
|
usesPwm: uses('pwm_'),
|
|
416
804
|
usesAdc: uses('adc_'),
|
|
417
805
|
adcReadPins: scanAdcReadPins(src, chip),
|
|
806
|
+
usesDac: uses('dac_') || uses('__tc_dac'),
|
|
807
|
+
dacWritePins: scanDacWritePins(src),
|
|
418
808
|
pwmUsedPins: scanPwmUsedPins(src, chip),
|
|
809
|
+
i2cUsedInstances: scanUsedBusInstances(src, chip.i2c?.controllers, 'i2c'),
|
|
810
|
+
spiUsedInstances: scanUsedBusInstances(src, chip.spi?.controllers, 'spi'),
|
|
811
|
+
uartUsedInstances: scanUsedBusInstances(src, chip.uart?.controllers, 'uart'),
|
|
812
|
+
// Preferences/FS — same tokens scaffoldZephyrProject scans; drive the
|
|
813
|
+
// storage-partition synthesis + /chosen settings pointer.
|
|
814
|
+
usesPreferences: uses('settings_') || uses('__tc_prefs'),
|
|
815
|
+
usesWdt: uses('wdt_'),
|
|
816
|
+
usesHwtimer: uses('counter_') || uses('__tc_hw'),
|
|
817
|
+
usesFS: uses('__tc_fs'),
|
|
419
818
|
usesDisplay,
|
|
420
819
|
usesTouch: uses('ft6336u') || uses('touch_') || usesXpt,
|
|
421
820
|
touchController: usesXpt ? 'xpt2046' : 'ft6336u',
|
|
@@ -433,6 +832,24 @@ export const Toolchain = {
|
|
|
433
832
|
// packages' overlay fragments are appended by the scaffold helper.
|
|
434
833
|
const boardId = board.split('/')[0];
|
|
435
834
|
writeIfChanged(join(overlayDir, `${boardId}.overlay`), appendLibraryOverlayFragments(overlay, projectRoot));
|
|
835
|
+
// Thin SPI targets need the app-local binding (see the transpile-side
|
|
836
|
+
// write for the rationale): no compatible → no generated spi props →
|
|
837
|
+
// SPI_DT_SPEC_GET's spi-max-frequency lookup does not exist.
|
|
838
|
+
if (spiTargetParts.length > 0) {
|
|
839
|
+
const bindingsDir = join(projectRoot, 'dts', 'bindings');
|
|
840
|
+
mkdirSync(bindingsDir, { recursive: true });
|
|
841
|
+
writeIfChanged(join(bindingsDir, 'cuttlefish,spi-target.yaml'), [
|
|
842
|
+
'description: |',
|
|
843
|
+
' Cuttlefish thin SPITarget (hal/spi-target.ts) — a raw spi_dt_spec',
|
|
844
|
+
' peer. No driver binds this compatible; it exists so devicetree',
|
|
845
|
+
' generation emits the spi properties (spi-max-frequency,',
|
|
846
|
+
' spi-cpol/spi-cpha, reg = the cs-gpios index) that SPI_DT_SPEC_GET',
|
|
847
|
+
' consumes from the generated C++.',
|
|
848
|
+
'compatible: "cuttlefish,spi-target"',
|
|
849
|
+
'include: spi-device.yaml',
|
|
850
|
+
'',
|
|
851
|
+
].join('\n'));
|
|
852
|
+
}
|
|
436
853
|
}
|
|
437
854
|
catch { /* best-effort overlay regen; the build surfaces DT errors */ }
|
|
438
855
|
// Use a stable build dir so incremental builds reuse the Ninja graph.
|
|
@@ -513,11 +930,11 @@ export const Toolchain = {
|
|
|
513
930
|
// toolchain compile() debug-config wiring.
|
|
514
931
|
if (result.status === 0 && isGdbDebug) {
|
|
515
932
|
try {
|
|
516
|
-
const { workspaceRoot,
|
|
933
|
+
const { workspaceRoot, appRel } = resolveDebugLocations(projectRoot);
|
|
517
934
|
writeDebugConfig({
|
|
518
935
|
projectRoot,
|
|
519
936
|
workspaceRoot,
|
|
520
|
-
|
|
937
|
+
appRel,
|
|
521
938
|
target: board,
|
|
522
939
|
buildDir,
|
|
523
940
|
sourceMapPath: join(dirname(o.sourcePath), `${basename(o.sourcePath)}.thcppmap.json`),
|
|
@@ -527,6 +944,41 @@ export const Toolchain = {
|
|
|
527
944
|
console.warn(`[cuttlefish] gdb debug config generation failed: ${e.message}`);
|
|
528
945
|
}
|
|
529
946
|
}
|
|
947
|
+
// As-built snapshot: after a successful build, the resolved devicetree
|
|
948
|
+
// at <buildDir>/zephyr/zephyr.dts carries the board's pinctrl labels —
|
|
949
|
+
// the STABLE name grammar, immune to vendor macro churn. Harvest its
|
|
950
|
+
// routes into .cuttlefish/as-built.json; the next build's board-module
|
|
951
|
+
// generation merges them per-pin over the catalog harvest (build wins,
|
|
952
|
+
// silently when they agree). One-build freshness lag on first setup,
|
|
953
|
+
// self-maintaining after. Best-effort — a missing/unparseable artifact
|
|
954
|
+
// never fails the build.
|
|
955
|
+
if (result.status === 0) {
|
|
956
|
+
try {
|
|
957
|
+
const dtsPath = join(buildDir, 'zephyr', 'zephyr.dts');
|
|
958
|
+
const dtsText = readFileSync(dtsPath, 'utf8');
|
|
959
|
+
const facts = parseZephyrDts(dtsText);
|
|
960
|
+
const total = facts.adc.length + facts.pwm.length + facts.dac.length;
|
|
961
|
+
if (total > 0) {
|
|
962
|
+
// Write beside the project's board module — the .cuttlefish dir the
|
|
963
|
+
// config loader reads from, discovered by walking up to the
|
|
964
|
+
// generated board.json (the scaffold root and the config root are
|
|
965
|
+
// different dirs in the standard layout: src/out vs project root).
|
|
966
|
+
let cfDir = join(projectRoot, '.cuttlefish');
|
|
967
|
+
for (let dir = projectRoot;; dir = dirname(dir)) {
|
|
968
|
+
if (existsSync(join(dir, '.cuttlefish', 'board.json'))) {
|
|
969
|
+
cfDir = join(dir, '.cuttlefish');
|
|
970
|
+
break;
|
|
971
|
+
}
|
|
972
|
+
const parent = dirname(dir);
|
|
973
|
+
if (parent === dir)
|
|
974
|
+
break;
|
|
975
|
+
}
|
|
976
|
+
mkdirSync(cfDir, { recursive: true });
|
|
977
|
+
writeIfChanged(join(cfDir, 'as-built.json'), asBuiltJson(board, facts));
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
catch { /* best-effort snapshot — nothing to harvest or unreadable */ }
|
|
981
|
+
}
|
|
530
982
|
return {
|
|
531
983
|
success: result.status === 0,
|
|
532
984
|
output: header + output,
|
|
@@ -538,21 +990,137 @@ export const Toolchain = {
|
|
|
538
990
|
const buildDir = join(projectRoot, 'build');
|
|
539
991
|
const board = targetFromOptions(o);
|
|
540
992
|
const zc = o.zephyrConfig;
|
|
541
|
-
const
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
const
|
|
552
|
-
|
|
993
|
+
const chip = chipForBuild(projectRoot, board);
|
|
994
|
+
const probe = resolveProbeMethod(zc, chip, 'flash');
|
|
995
|
+
if (!probe.ok) {
|
|
996
|
+
return { success: false, output: `-- west flash: ${probe.error}` };
|
|
997
|
+
}
|
|
998
|
+
// BOSSA bootloader boards with touch-reset data: open the app's console
|
|
999
|
+
// port at 1200 baud (the firmware's USB shim reboots into the
|
|
1000
|
+
// bootloader), wait for the bootloader identity, and flash THAT port.
|
|
1001
|
+
// Falls back to the configured port (manual double-tap) on any failure.
|
|
1002
|
+
let flashPort = o.port;
|
|
1003
|
+
const flashNotes = [];
|
|
1004
|
+
// The runner this flash will actually use: the explicit choice, else the
|
|
1005
|
+
// board's declared default (first probe-method entry). Gates port
|
|
1006
|
+
// forwarding and the bossac touch below — board.cmake still resolves
|
|
1007
|
+
// the default runner itself.
|
|
1008
|
+
const flashRunner = probe.runner ?? chip.probeMethods?.[0]?.runner;
|
|
1009
|
+
// Serial-port runners cannot flash without a port; every other runner
|
|
1010
|
+
// (probe or USB) proceeds — whether a port is required is the runner's
|
|
1011
|
+
// call, not the CLI's blanket gate.
|
|
1012
|
+
if (uploadRequiresPort(flashRunner) && !flashPort) {
|
|
1013
|
+
return {
|
|
1014
|
+
success: false,
|
|
1015
|
+
output: `-- upload requires a port for ${flashRunner} flashing. Set --port <port> on the command line (or the CUTTLEFISH_PORT env var).`,
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
if (flashRunner === 'bossac' && flashPort && chip.usb?.touchReset) {
|
|
1019
|
+
const touch = bossacTouchReset(flashPort, chip.usb.touchReset);
|
|
1020
|
+
flashNotes.push(`-- ${touch.note}`);
|
|
1021
|
+
if (touch.port)
|
|
1022
|
+
flashPort = touch.port;
|
|
1023
|
+
}
|
|
1024
|
+
const args = buildFlashArgs(buildDir, probe.runner, flashPort, flashRunner, probe.args);
|
|
1025
|
+
// openocd flashes go through a dedicated session instead of `west flash`.
|
|
1026
|
+
// west's flow has three sequential races that each strand the board: the
|
|
1027
|
+
// connect happens against a running (often USB-active) application, the
|
|
1028
|
+
// erase precedes the write so the vector table is 0xFFFFFFFF while the
|
|
1029
|
+
// RAM algorithm runs (any exception → core LOCKUP at 0xFFFFFFFE, "timeout
|
|
1030
|
+
// waiting for algorithm"), and the trailing `reset run` frequently does
|
|
1031
|
+
// not reach the core. This session is deterministic end to end: halt at
|
|
1032
|
+
// the reset vector (static target for the DAP), mask interrupts for the
|
|
1033
|
+
// algorithm (exceptions cannot vector through erased flash), unmask
|
|
1034
|
+
// after, and boot the flashed app with a direct SYSRESETREQ. Falls back
|
|
1035
|
+
// to `west flash` when the image or session is unavailable.
|
|
1036
|
+
let westFallback = true;
|
|
1037
|
+
if (probe.runner === 'openocd') {
|
|
1038
|
+
const hex = join(buildDir, 'zephyr', 'zephyr.hex');
|
|
1039
|
+
if (existsSync(hex)) {
|
|
1040
|
+
// Forward slashes + TCL quoting so project paths with spaces work.
|
|
1041
|
+
const hexArg = `"${hex.replace(/\\/g, '/')}"`;
|
|
1042
|
+
// Target addressing: `cortex_m` is a PER-TARGET subcommand — a bare
|
|
1043
|
+
// `cortex_m maskisr on` is an unknown command (a silent no-op inside
|
|
1044
|
+
// catch). Resolve the session's target object once and address it.
|
|
1045
|
+
// Cortex-M-only, self-gating: on other cores the cortex_m method
|
|
1046
|
+
// errors and catch contains it (the plain flash path is safe there
|
|
1047
|
+
// without masking — the lockup class is Cortex-M vectoring).
|
|
1048
|
+
const flashed = openocdProbeSession(buildDir, zc, chip, [
|
|
1049
|
+
'set _tgt [lindex [target names] 0]',
|
|
1050
|
+
'reset halt',
|
|
1051
|
+
'catch { $_tgt cortex_m maskisr on }',
|
|
1052
|
+
`flash write_image erase ${hexArg}`,
|
|
1053
|
+
'catch { $_tgt cortex_m maskisr off }',
|
|
1054
|
+
// Boot the flashed app. Cortex-M: a direct SYSRESETREQ via AIRCR —
|
|
1055
|
+
// pin-independent, always reaches the core, and clears PRIMASK
|
|
1056
|
+
// (so the masked algorithm leaves nothing behind). Other cores:
|
|
1057
|
+
// openocd's generic reset run. A halted core STAYS halted across
|
|
1058
|
+
// a core-initiated reset (debug halt state survives — that is how
|
|
1059
|
+
// reset halt works), so resume it; on a running core resume errors
|
|
1060
|
+
// and the catch swallows it.
|
|
1061
|
+
'if {[catch {$_tgt cortex_m maskisr on}] == 0} { $_tgt cortex_m maskisr off; mww 0xE000ED0C 0x05FA0004 } else { reset run }',
|
|
1062
|
+
'sleep 100',
|
|
1063
|
+
'catch { resume }',
|
|
1064
|
+
'sleep 200',
|
|
1065
|
+
]);
|
|
1066
|
+
if (flashed) {
|
|
1067
|
+
westFallback = false;
|
|
1068
|
+
flashNotes.push('-- probe flash ok: halt → masked write → SYSRESETREQ');
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
let result;
|
|
1073
|
+
let raw = '';
|
|
1074
|
+
let ok = false;
|
|
1075
|
+
if (westFallback) {
|
|
1076
|
+
const inv = westSpawn(args, {
|
|
1077
|
+
cwd: projectRoot,
|
|
1078
|
+
encoding: 'utf-8',
|
|
1079
|
+
timeout: FLASH_TIMEOUT_MS,
|
|
1080
|
+
});
|
|
1081
|
+
result = spawnSync(inv.command, inv.args, inv.options);
|
|
1082
|
+
const fstdout = typeof result.stdout === 'string' ? result.stdout : (result.stdout?.toString() ?? '');
|
|
1083
|
+
const fstderr = typeof result.stderr === 'string' ? result.stderr : (result.stderr?.toString() ?? '');
|
|
1084
|
+
raw = fstdout + fstderr;
|
|
1085
|
+
ok = classifyUploadResult(probe.runner, result.status, raw);
|
|
1086
|
+
// A SUCCESSFUL west openocd flash can still leave the core HALTED:
|
|
1087
|
+
// west's trailing `reset run` does not reach a core behind an unwired
|
|
1088
|
+
// SRST, and the user's only recourse is the NRST button. Boot it from
|
|
1089
|
+
// a probe session instead — one core reset (a halted core re-halts at
|
|
1090
|
+
// the reset vector; a running core restarts the just-flashed app)
|
|
1091
|
+
// plus a resume.
|
|
1092
|
+
if (ok && probe.runner === 'openocd') {
|
|
1093
|
+
const booted = openocdProbeSession(buildDir, zc, chip, [
|
|
1094
|
+
'set _tgt [lindex [target names] 0]',
|
|
1095
|
+
'if {[catch {$_tgt cortex_m maskisr on}] == 0} { $_tgt cortex_m maskisr off; mww 0xE000ED0C 0x05FA0004 } else { reset run }',
|
|
1096
|
+
'sleep 100',
|
|
1097
|
+
'catch { resume }',
|
|
1098
|
+
'sleep 200',
|
|
1099
|
+
]);
|
|
1100
|
+
if (booted)
|
|
1101
|
+
flashNotes.push('-- probe boot ok: SYSRESETREQ → resume');
|
|
1102
|
+
}
|
|
1103
|
+
// Fallback-path recovery: a failed west flash leaves the core in
|
|
1104
|
+
// lockup; a direct SYSRESETREQ clears it so the caller's retry (or a
|
|
1105
|
+
// later flash) starts from a clean chip.
|
|
1106
|
+
if (!ok && probe.runner === 'openocd') {
|
|
1107
|
+
const revived = openocdProbeSession(buildDir, zc, chip, [
|
|
1108
|
+
'init',
|
|
1109
|
+
'set _tgt [lindex [target names] 0]',
|
|
1110
|
+
'if {[catch {$_tgt cortex_m maskisr on}] == 0} { $_tgt cortex_m maskisr off; mww 0xE000ED0C 0x05FA0004 } else { reset run }',
|
|
1111
|
+
'sleep 300',
|
|
1112
|
+
]);
|
|
1113
|
+
if (revived)
|
|
1114
|
+
flashNotes.push(revived);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
else {
|
|
1118
|
+
ok = true;
|
|
1119
|
+
}
|
|
553
1120
|
return {
|
|
554
|
-
success:
|
|
555
|
-
output: cleanseUploadOutput(runner, result.status, raw)
|
|
1121
|
+
success: ok,
|
|
1122
|
+
output: [...flashNotes, cleanseUploadOutput(probe.runner, westFallback ? result.status : 0, raw)]
|
|
1123
|
+
.filter(Boolean).join('\n'),
|
|
556
1124
|
};
|
|
557
1125
|
},
|
|
558
1126
|
monitor(o) {
|
|
@@ -567,7 +1135,7 @@ export const Toolchain = {
|
|
|
567
1135
|
}
|
|
568
1136
|
// ESP32 USB-CDC console runs at 115200 (the Zephyr ESP32 board default).
|
|
569
1137
|
// The CLI's generic default of 9600 is wrong for this target; honor an
|
|
570
|
-
// explicit --baud
|
|
1138
|
+
// explicit --baud when given, else 115200.
|
|
571
1139
|
const baud = o.baud ?? 115200;
|
|
572
1140
|
const install = discoverWest();
|
|
573
1141
|
const py = install?.pythonExecutable ?? process.env.PYTHON ?? 'python';
|
|
@@ -582,15 +1150,27 @@ export const Toolchain = {
|
|
|
582
1150
|
});
|
|
583
1151
|
},
|
|
584
1152
|
debug(o) {
|
|
585
|
-
// Launch an interactive GDB session for the last build.
|
|
586
|
-
//
|
|
587
|
-
//
|
|
1153
|
+
// Launch an interactive GDB session for the last build. The probe method
|
|
1154
|
+
// resolves exactly like flashing (zephyr.probe / zephyr.runner — the same
|
|
1155
|
+
// attach session, so the same quirks apply); debug-incapable methods
|
|
1156
|
+
// (bootloaders) are rejected with the debug-capable list. west debug
|
|
1157
|
+
// resolves the GDB binary from the build dir's CMakeCache — no
|
|
588
1158
|
// hand-authored gdbinit needed. Inherits stdio so GDB runs interactively.
|
|
589
|
-
// (Not invoked by the standard build/compile flow; powers an explicit
|
|
590
|
-
// debug-attach entry point for terminal-driven debugging without VS Code.)
|
|
591
1159
|
const projectRoot = projectRootFromOptions(o);
|
|
592
1160
|
const buildDir = join(projectRoot, 'build');
|
|
593
|
-
const
|
|
1161
|
+
const board = targetFromOptions(o);
|
|
1162
|
+
const zc = o.zephyrConfig;
|
|
1163
|
+
const probe = resolveProbeMethod(zc, chipForBuild(projectRoot, board), 'debug');
|
|
1164
|
+
if (!probe.ok) {
|
|
1165
|
+
console.error(`-- west debug: ${probe.error}`);
|
|
1166
|
+
process.exitCode = 1;
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
const debugArgs = ['debug', '-d', buildDir];
|
|
1170
|
+
if (probe.runner)
|
|
1171
|
+
debugArgs.push('--runner', probe.runner);
|
|
1172
|
+
debugArgs.push(...probe.args);
|
|
1173
|
+
const inv = westSpawn(debugArgs, {
|
|
594
1174
|
cwd: projectRoot,
|
|
595
1175
|
encoding: 'utf-8',
|
|
596
1176
|
stdio: 'inherit',
|