@protoboardai/ext-zephyr 0.0.0-stage → 0.2.0
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/LICENSE +202 -0
- package/NOTICE +13 -0
- package/README.md +161 -2
- package/evals/haptic-driver-mode/case.json +44 -0
- package/evals/haptic-driver-mode/seed-firmware.config.ts.txt +13 -0
- package/evals/haptic-driver-mode/solution-firmware.config.ts.txt +15 -0
- package/lib/design.ts +492 -0
- package/lib/drivers.ts +70 -0
- package/lib/dts.ts +166 -0
- package/lib/pins.ts +20 -0
- package/lib/soc.ts +89 -0
- package/ops/firmware.ts +36 -0
- package/package.json +31 -4
- package/protoboard-extension.json +40 -0
- package/skills/firmware-config-zephyr/SKILL.md +134 -0
package/lib/dts.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A structural devicetree check for the generated files, used when `dtc` is
|
|
3
|
+
* not available (and by `check` always): after the C preprocessor would have
|
|
4
|
+
* run, a devicetree source is nodes and properties. This checks what a
|
|
5
|
+
* generator can get wrong: balanced nodes, every property and node closed
|
|
6
|
+
* with `;`, values that are cells, strings, byte strings or references,
|
|
7
|
+
* labels defined once, and every `&label` defined in the files or by the SoC.
|
|
8
|
+
* Macros inside cells (`NRF_PSEL(…)`, `GPIO_ACTIVE_LOW`) are not expanded.
|
|
9
|
+
*/
|
|
10
|
+
export interface DtsProblem {
|
|
11
|
+
file: string;
|
|
12
|
+
line: number;
|
|
13
|
+
message: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface Token {
|
|
17
|
+
t: string;
|
|
18
|
+
line: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function tokenize(text: string, file: string, problems: DtsProblem[]): Token[] {
|
|
22
|
+
const tokens: Token[] = [];
|
|
23
|
+
let line = 1;
|
|
24
|
+
let i = 0;
|
|
25
|
+
const s = text;
|
|
26
|
+
while (i < s.length) {
|
|
27
|
+
const ch = s[i];
|
|
28
|
+
if (ch === "\n") {
|
|
29
|
+
line++;
|
|
30
|
+
i++;
|
|
31
|
+
} else if (/\s/.test(ch)) i++;
|
|
32
|
+
else if (s.startsWith("/*", i)) {
|
|
33
|
+
const end = s.indexOf("*/", i + 2);
|
|
34
|
+
if (end < 0) {
|
|
35
|
+
problems.push({ file, line, message: "unterminated /* comment" });
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
line += s.slice(i, end).split("\n").length - 1;
|
|
39
|
+
i = end + 2;
|
|
40
|
+
} else if (s.startsWith("//", i) || (ch === "#" && /^#(include|define|undef|if|ifdef|ifndef|endif|else)\b/.test(s.slice(i)))) {
|
|
41
|
+
while (i < s.length && s[i] !== "\n") i++;
|
|
42
|
+
} else if (ch === '"') {
|
|
43
|
+
let j = i + 1;
|
|
44
|
+
while (j < s.length && s[j] !== '"' && s[j] !== "\n") j += s[j] === "\\" ? 2 : 1;
|
|
45
|
+
if (s[j] !== '"') problems.push({ file, line, message: "unterminated string" });
|
|
46
|
+
tokens.push({ t: s.slice(i, j + 1), line });
|
|
47
|
+
i = j + 1;
|
|
48
|
+
} else if (ch === "<" || ch === "[") {
|
|
49
|
+
const close = ch === "<" ? ">" : "]";
|
|
50
|
+
let depth = 0;
|
|
51
|
+
let j = i;
|
|
52
|
+
for (; j < s.length; j++) {
|
|
53
|
+
if (s[j] === "(") depth++;
|
|
54
|
+
else if (s[j] === ")") depth--;
|
|
55
|
+
else if (s[j] === close && depth === 0) break;
|
|
56
|
+
else if (s[j] === "\n") line++;
|
|
57
|
+
}
|
|
58
|
+
if (j >= s.length) problems.push({ file, line, message: `unterminated ${ch}…${close}` });
|
|
59
|
+
tokens.push({ t: s.slice(i, j + 1), line });
|
|
60
|
+
i = j + 1;
|
|
61
|
+
} else if ("{};=,".includes(ch)) {
|
|
62
|
+
tokens.push({ t: ch, line });
|
|
63
|
+
i++;
|
|
64
|
+
} else {
|
|
65
|
+
// a comma between word characters belongs to the name (zephyr,code; nordic,nrf-twim)
|
|
66
|
+
let j = i;
|
|
67
|
+
while (j < s.length && (!/[\s{};=,"<[]/.test(s[j]) || (s[j] === "," && j > i && /[\w#-]/.test(s[j + 1] ?? "")))) j++;
|
|
68
|
+
if (j === i) j++;
|
|
69
|
+
tokens.push({ t: s.slice(i, j), line });
|
|
70
|
+
i = j;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return tokens;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const NAME = /^[A-Za-z0-9,._+#?@-]+$/;
|
|
77
|
+
const LABEL = /^[A-Za-z_][A-Za-z0-9_]*:$/;
|
|
78
|
+
const REF = /^&[A-Za-z_][A-Za-z0-9_]*$/;
|
|
79
|
+
|
|
80
|
+
/** Check devicetree sources (name → text). `known` are labels the included SoC devicetree defines. */
|
|
81
|
+
export function checkDts(files: Record<string, string>, known: string[] = []): DtsProblem[] {
|
|
82
|
+
const problems: DtsProblem[] = [];
|
|
83
|
+
const labels = new Map<string, string>();
|
|
84
|
+
const refs: { label: string; file: string; line: number }[] = [];
|
|
85
|
+
for (const [file, text] of Object.entries(files)) {
|
|
86
|
+
const tokens = tokenize(text, file, problems);
|
|
87
|
+
let depth = 0;
|
|
88
|
+
let i = 0;
|
|
89
|
+
const at = (k: number) => tokens[k]?.t;
|
|
90
|
+
const err = (k: number, message: string) => problems.push({ file, line: tokens[Math.min(k, tokens.length - 1)]?.line ?? 1, message });
|
|
91
|
+
while (i < tokens.length) {
|
|
92
|
+
// "/dts-v1/;"
|
|
93
|
+
if (at(i) === "/dts-v1/" && at(i + 1) === ";") {
|
|
94
|
+
i += 2;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (at(i) === "}") {
|
|
98
|
+
if (at(i + 1) !== ";") err(i, "node not closed with };");
|
|
99
|
+
depth--;
|
|
100
|
+
if (depth < 0) {
|
|
101
|
+
err(i, "} without an open node");
|
|
102
|
+
depth = 0;
|
|
103
|
+
}
|
|
104
|
+
i += 2;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
// labels before a node
|
|
108
|
+
while (LABEL.test(at(i) ?? "")) {
|
|
109
|
+
const l = at(i)!.slice(0, -1);
|
|
110
|
+
if (labels.has(l)) err(i, `label ${l} is defined twice (first in ${labels.get(l)})`);
|
|
111
|
+
labels.set(l, file);
|
|
112
|
+
i++;
|
|
113
|
+
}
|
|
114
|
+
const name = at(i);
|
|
115
|
+
if (name === undefined) break;
|
|
116
|
+
if (at(i + 1) === "{") {
|
|
117
|
+
if (REF.test(name)) {
|
|
118
|
+
if (depth !== 0) err(i, `${name} { … } is only allowed at the top level`);
|
|
119
|
+
refs.push({ label: name.slice(1), file, line: tokens[i].line });
|
|
120
|
+
} else if (name !== "/" && !NAME.test(name)) err(i, `bad node name ${name}`);
|
|
121
|
+
depth++;
|
|
122
|
+
i += 2;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (depth === 0) {
|
|
126
|
+
err(i, `unexpected ${name} outside a node`);
|
|
127
|
+
i++;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (!NAME.test(name)) err(i, `bad property name ${name}`);
|
|
131
|
+
if (at(i + 1) === ";") {
|
|
132
|
+
i += 2;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (at(i + 1) !== "=") {
|
|
136
|
+
err(i, `property ${name} needs = or ;`);
|
|
137
|
+
i++;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
// value list: cells, strings, byte strings, references, separated by commas
|
|
141
|
+
let j = i + 2;
|
|
142
|
+
for (;;) {
|
|
143
|
+
const v = at(j);
|
|
144
|
+
if (v === undefined) {
|
|
145
|
+
err(j, `property ${name} is not closed with ;`);
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
if (/^<.*>$/s.test(v)) for (const m of v.matchAll(/&([A-Za-z_][A-Za-z0-9_]*)/g)) refs.push({ label: m[1], file, line: tokens[j].line });
|
|
149
|
+
else if (REF.test(v)) refs.push({ label: v.slice(1), file, line: tokens[j].line });
|
|
150
|
+
else if (!/^".*"$/s.test(v) && !/^\[.*\]$/s.test(v)) err(j, `property ${name}: ${v} is not a cell list, string, byte string or reference`);
|
|
151
|
+
j++;
|
|
152
|
+
if (at(j) === ",") {
|
|
153
|
+
j++;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (at(j) !== ";") err(j, `property ${name} is not closed with ;`);
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
i = j + 1;
|
|
160
|
+
}
|
|
161
|
+
if (depth !== 0) problems.push({ file, line: tokens[tokens.length - 1]?.line ?? 1, message: `${depth} node(s) not closed` });
|
|
162
|
+
}
|
|
163
|
+
const knownSet = new Set([...known, ...labels.keys()]);
|
|
164
|
+
for (const r of refs) if (!knownSet.has(r.label)) problems.push({ file: r.file, line: r.line, message: `&${r.label} is not defined by these files or the SoC devicetree` });
|
|
165
|
+
return problems;
|
|
166
|
+
}
|
package/lib/pins.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Pins named in generated Zephyr files: `NRF_PSEL(<fn>, <port>, <pin>)` and `<&gpio<port> <pin> …>`. */
|
|
2
|
+
import type { ModuleDef } from "@deltarobotics/uhd";
|
|
3
|
+
import type { PinRef } from "@protoboardai/core";
|
|
4
|
+
import { leafAt } from "./soc.ts";
|
|
5
|
+
|
|
6
|
+
export function readZephyrPins(files: Record<string, string>, controller: ModuleDef): PinRef[] {
|
|
7
|
+
const refs: PinRef[] = [];
|
|
8
|
+
for (const [file, text] of Object.entries(files)) {
|
|
9
|
+
if (!/\.(dts|dtsi|overlay)$/.test(file)) continue;
|
|
10
|
+
text.split("\n").forEach((line, i) => {
|
|
11
|
+
const code = line.replace(/\/\*.*?\*\//g, "");
|
|
12
|
+
for (const m of code.matchAll(/NRF_PSEL\(\s*\w+\s*,\s*(\d+)\s*,\s*(\d+)\s*\)|&gpio(\d+)\s+(\d+)\b/g)) {
|
|
13
|
+
const port = Number(m[1] ?? m[3]);
|
|
14
|
+
const pin = Number(m[2] ?? m[4]);
|
|
15
|
+
refs.push({ file, line: i + 1, ref: `P${port}.${String(pin).padStart(2, "0")}`, pin: leafAt(controller, port, pin)?.id });
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return refs;
|
|
20
|
+
}
|
package/lib/soc.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nRF52840 facts the Zephyr provider needs, checked against Zephyr v4.2.0
|
|
3
|
+
* (dts/arm/nordic/nrf52840.dtsi, boards/nordic/nrf52840dongle,
|
|
4
|
+
* drivers/clock_control/Kconfig.nrf). This is the provider's own table,
|
|
5
|
+
* like a PCB tool's land patterns: it is never written into a part.
|
|
6
|
+
*
|
|
7
|
+
* A part is matched to an SoC entry by its stated SoC fact
|
|
8
|
+
* (`domains[electrical].metadata.soc`), which is a UHD fact about the part.
|
|
9
|
+
*/
|
|
10
|
+
import type { InterfaceDef, ModuleDef } from "@deltarobotics/uhd";
|
|
11
|
+
|
|
12
|
+
export const ZEPHYR_VERSION = "4.2.0";
|
|
13
|
+
|
|
14
|
+
export interface ZephyrSoc {
|
|
15
|
+
/** board.yml `socs[].name`. */
|
|
16
|
+
name: string;
|
|
17
|
+
/** Kconfig symbol the board selects (the package variant). */
|
|
18
|
+
kconfig: string;
|
|
19
|
+
/** SoC devicetree include. */
|
|
20
|
+
dtsi: string;
|
|
21
|
+
/** How the part states it: matched against its SoC fact. */
|
|
22
|
+
match: RegExp;
|
|
23
|
+
/** Package the Kconfig symbol stands for, matched against the same fact. */
|
|
24
|
+
packageMatch: RegExp;
|
|
25
|
+
/** Bus instances in the order the provider uses them. */
|
|
26
|
+
i2c: string[];
|
|
27
|
+
spi: string[];
|
|
28
|
+
uart: string[];
|
|
29
|
+
/** Instances that share one peripheral id and cannot both be enabled. */
|
|
30
|
+
sharesId: Record<string, string>;
|
|
31
|
+
/** Highest SPI clock per instance (Hz). */
|
|
32
|
+
spiMaxHz: Record<string, number>;
|
|
33
|
+
/** Labels the SoC devicetree defines (for the structural check of the generated files). */
|
|
34
|
+
labels: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const NRF52840: ZephyrSoc = {
|
|
38
|
+
name: "nrf52840",
|
|
39
|
+
kconfig: "SOC_NRF52840_QIAA",
|
|
40
|
+
dtsi: "nordic/nrf52840_qiaa.dtsi",
|
|
41
|
+
match: /nRF52840/i,
|
|
42
|
+
packageMatch: /aQFN-?73/i,
|
|
43
|
+
// TWIM0/SPIM0 and TWIM1/SPIM1 share peripheral ids (nrf52840.dtsi: i2c0 and spi0 both at 0x40003000)
|
|
44
|
+
i2c: ["i2c0", "i2c1"],
|
|
45
|
+
spi: ["spi3", "spi2", "spi1", "spi0"],
|
|
46
|
+
uart: ["uart0", "uart1"],
|
|
47
|
+
sharesId: { i2c0: "spi0", spi0: "i2c0", i2c1: "spi1", spi1: "i2c1" },
|
|
48
|
+
// SPIM3 runs at 32 MHz, SPIM0-2 at 8 MHz (nRF52840 PS §6.25)
|
|
49
|
+
spiMaxHz: { spi3: 32_000_000, spi2: 8_000_000, spi1: 8_000_000, spi0: 8_000_000 },
|
|
50
|
+
labels: [
|
|
51
|
+
"acl", "adc", "bt_hci_controller", "ccm", "clock", "comp", "cpu0", "cryptocell", "ecb", "egu0", "egu1", "egu2", "egu3", "egu4", "egu5",
|
|
52
|
+
"ficr", "flash0", "flash_controller", "gpio0", "gpio1", "gpiote", "gpiote0", "gpregret1", "gpregret2", "hfxo", "i2c0", "i2c1", "i2s0",
|
|
53
|
+
"ieee802154", "itm", "mwu", "nfct", "nvic", "pdm0", "pinctrl", "power", "ppi", "pwm0", "pwm1", "pwm2", "pwm3", "qdec", "qspi", "radio",
|
|
54
|
+
"reg0", "reg1", "rng", "rtc0", "rtc1", "rtc2", "spi0", "spi1", "spi2", "spi3", "sram0", "systick", "temp", "timer0", "timer1", "timer2",
|
|
55
|
+
"timer3", "timer4", "uart0", "uart1", "uicr", "usbd", "wdt", "wdt0",
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const SOCS = [NRF52840];
|
|
60
|
+
|
|
61
|
+
/** The part's stated SoC fact, if it has one. */
|
|
62
|
+
export function socFact(def: ModuleDef): string | undefined {
|
|
63
|
+
for (const d of def.domains ?? []) {
|
|
64
|
+
const soc = (d as { metadata?: Record<string, unknown> }).metadata?.soc;
|
|
65
|
+
if (typeof soc === "string") return soc;
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function socOf(def: ModuleDef): ZephyrSoc | undefined {
|
|
71
|
+
const fact = socFact(def);
|
|
72
|
+
return fact ? SOCS.find((s) => s.match.test(fact)) : undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** "P0.13" or "P0.00/XL1" → port 0, pin 13. */
|
|
76
|
+
export function portPin(leaf: InterfaceDef | undefined): { port: number; pin: number } | undefined {
|
|
77
|
+
const m = /^P(\d)\.(\d{1,2})\b/.exec(leaf?.name ?? "");
|
|
78
|
+
return m ? { port: Number(m[1]), pin: Number(m[2]) } : undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const pinLabel = (pp: { port: number; pin: number }) => `P${pp.port}.${String(pp.pin).padStart(2, "0")}`;
|
|
82
|
+
|
|
83
|
+
/** The controller leaf named P<port>.<pin>, if the module has it. */
|
|
84
|
+
export function leafAt(def: ModuleDef, port: number, pin: number): InterfaceDef | undefined {
|
|
85
|
+
return def.interfaces.find((i) => {
|
|
86
|
+
const pp = portPin(i);
|
|
87
|
+
return pp?.port === port && pp.pin === pin;
|
|
88
|
+
});
|
|
89
|
+
}
|
package/ops/firmware.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The firmware capability for Zephyr on nRF52840 modules: the shared
|
|
3
|
+
* operations from @protoboardai/core with this provider's design, pin reader
|
|
4
|
+
* and rules (a devicetree structural check of what it generates).
|
|
5
|
+
*/
|
|
6
|
+
import type { CapabilityOperationFn } from "@protoboardai/core";
|
|
7
|
+
import { firmwareOperations, previewFirmware, type FirmwareProviderSpec } from "@protoboardai/core";
|
|
8
|
+
import { designZephyr } from "../lib/design.ts";
|
|
9
|
+
import { checkDts } from "../lib/dts.ts";
|
|
10
|
+
import { readZephyrPins } from "../lib/pins.ts";
|
|
11
|
+
import { NRF52840, ZEPHYR_VERSION, socOf } from "../lib/soc.ts";
|
|
12
|
+
|
|
13
|
+
export const SPEC: FirmwareProviderSpec = {
|
|
14
|
+
provider: "@protoboardai/ext-zephyr",
|
|
15
|
+
framework: "zephyr",
|
|
16
|
+
frameworkVersion: ZEPHYR_VERSION,
|
|
17
|
+
design: (fw, board, controller, ctx) => {
|
|
18
|
+
const d = designZephyr(fw, board, controller, ctx);
|
|
19
|
+
// the generated devicetree must be well formed, whatever the board
|
|
20
|
+
const dts = Object.fromEntries(d.files.filter((f) => /\.dtsi?$/.test(f.path)).map((f) => [f.path, f.lines.map((l) => l.text).join("\n")]));
|
|
21
|
+
for (const p of checkDts(dts, (socOf(controller) ?? NRF52840).labels)) {
|
|
22
|
+
d.findings.push({ code: "FW_DTS_SYNTAX", severity: "error", message: `${p.file}:${p.line}: ${p.message} (provider defect)` });
|
|
23
|
+
}
|
|
24
|
+
return d;
|
|
25
|
+
},
|
|
26
|
+
readPins: readZephyrPins,
|
|
27
|
+
// any nRF52840 GPIO wakes the chip from System OFF through its SENSE level (PS §6.9)
|
|
28
|
+
rules: () => ({ sleepName: "System OFF", i2cAddressRequired: true }),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const ops = firmwareOperations(SPEC);
|
|
32
|
+
export const generate: CapabilityOperationFn = ops.generate;
|
|
33
|
+
export const exportOutputs: CapabilityOperationFn = ops.exportOutputs;
|
|
34
|
+
export const extract: CapabilityOperationFn = ops.extract;
|
|
35
|
+
export const check: CapabilityOperationFn = ops.check;
|
|
36
|
+
export { previewFirmware };
|
package/package.json
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protoboardai/ext-zephyr",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Protoboard firmware provider: Zephyr board definitions (devicetree, pinctrl, Kconfig) for nRF52840 modules, from UHD boards",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "Delta Robotics, Inc.",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Delta-Robotics-Inc/protoboard-stack.git",
|
|
10
|
+
"directory": "packages/ext-zephyr"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"files": [
|
|
14
|
+
"protoboard-extension.json",
|
|
15
|
+
"ops",
|
|
16
|
+
"lib",
|
|
17
|
+
"skills",
|
|
18
|
+
"evals",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE",
|
|
21
|
+
"NOTICE"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=22"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"@protoboardai/core": "^0.2.0",
|
|
31
|
+
"@deltarobotics/uhd": ">=0.3.0"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "protoboard.extension/v0",
|
|
3
|
+
"id": "@protoboardai/ext-zephyr",
|
|
4
|
+
"version": "0.2.0",
|
|
5
|
+
"description": "firmware provider: Zephyr board definitions for nRF52840 modules (devicetree with pinctrl, buses, devices, buttons, display; Kconfig), every setting citing the UHD facts it comes from.",
|
|
6
|
+
"compatibility": {
|
|
7
|
+
"protoboard": "^0.2.0",
|
|
8
|
+
"uhd": ">=0.3.0"
|
|
9
|
+
},
|
|
10
|
+
"skills": [
|
|
11
|
+
{
|
|
12
|
+
"id": "firmware-config-zephyr",
|
|
13
|
+
"summary": "SOP: configure Zephyr for a UHD board with an nRF52840 module (firmware capability): config, generate, check, export, build hand-off; every setting cited",
|
|
14
|
+
"path": "skills/firmware-config-zephyr/SKILL.md"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"capabilities": [
|
|
18
|
+
{
|
|
19
|
+
"id": "firmware",
|
|
20
|
+
"summary": "Zephyr 4.2 board definition (board.yml, Kconfig, defconfig, devicetree, pinctrl) and a Kconfig fragment for an nRF52840 board",
|
|
21
|
+
"native": "zephyr",
|
|
22
|
+
"operations": {
|
|
23
|
+
"generate": { "entry": "./ops/firmware.ts#generate", "summary": "Read the board into a firmware map, apply the rules, write the board definition and firmware.conf under zephyr/ with the line index" },
|
|
24
|
+
"export": { "entry": "./ops/firmware.ts#exportOutputs", "summary": "Copy the generated files to out/config/ and write out/firmware.manifest.json (refuses files edited since generate)" },
|
|
25
|
+
"extract": { "entry": "./ops/firmware.ts#extract", "summary": "Artifact references and the pins the devicetree names" },
|
|
26
|
+
"check": { "entry": "./ops/firmware.ts#check", "summary": "Pin table, single use, bus functions, I2C addresses, strapping, wake sources, devicetree structure, and drift between the files and the board" }
|
|
27
|
+
},
|
|
28
|
+
"constraints": {
|
|
29
|
+
"controller": { "description": "instance path of the controller, when the board has more than one microcontroller" },
|
|
30
|
+
"board_name": { "description": "Zephyr board name (default: the module id with _ for -)" },
|
|
31
|
+
"vendor": { "description": "board vendor folder and compatible prefix (default protoboard)" },
|
|
32
|
+
"console": { "description": "UART bus id or UART device instance for zephyr,console" },
|
|
33
|
+
"i2c_frequency_hz": { "description": "I2C bus clock: 100000 or 400000 (default: the fastest every device allows)" },
|
|
34
|
+
"uart_baud": { "description": "{ <instance>: baud } for UART devices that state no baud_rate" },
|
|
35
|
+
"wake": { "description": "[<button or device instance | net>] inputs that wake the chip from System OFF (written as zephyr,user wakeup-gpios)" },
|
|
36
|
+
"properties": { "description": "{ <instance>: { <devicetree property>: value } } for what a driver binding requires and the model does not state" }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: firmware-config-zephyr
|
|
3
|
+
description: Configure Zephyr for a UHD board whose controller is an nRF52840 module, with the @protoboardai/ext-zephyr firmware provider. Writes a board definition (devicetree, pinctrl, Kconfig) and a Kconfig fragment from the board's nets and part facts, every setting citing its facts, and fixes what firmware:check finds (pins, buses, addresses, wake sources, drift). Use after the board's circuit is modelled, whenever its nets change, and before the release's firmware configuration is frozen.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Firmware configuration: Zephyr (nRF52840)
|
|
7
|
+
|
|
8
|
+
Standard operating procedure (release-package-and-evals.md §4). The
|
|
9
|
+
configuration is a view of the model: which pin is SDA is a net, a sensor's
|
|
10
|
+
address is a part fact. Nothing is typed from memory or edited by hand.
|
|
11
|
+
Reference: the package README.
|
|
12
|
+
|
|
13
|
+
## Purpose
|
|
14
|
+
|
|
15
|
+
Produce a Zephyr 4.2 board definition for one custom board and the standard
|
|
16
|
+
firmware outputs (`out/config/`, `out/firmware.manifest.json`) that the
|
|
17
|
+
release packages. Use it when a board module's controller is an nRF52840
|
|
18
|
+
module (the part's SoC fact says nRF52840), after its nets are modelled, and
|
|
19
|
+
again whenever they change.
|
|
20
|
+
|
|
21
|
+
## Inputs
|
|
22
|
+
|
|
23
|
+
- The board module: `src/boards/<module>/<module>.uhd.ts`, with every
|
|
24
|
+
peripheral pin the firmware drives or reads on a net that reaches a
|
|
25
|
+
controller pin (`netLinks`). Peripherals with a bus state it as a composite
|
|
26
|
+
(`i2c`, `spi`, `uart`) with `i2c_address` and `clock_freq`.
|
|
27
|
+
- The parts: the controller module and each peripheral, from
|
|
28
|
+
`parts/catalog` or `parts/local`, with pin tables and facts.
|
|
29
|
+
- What the model cannot state, decided with the person who owns the
|
|
30
|
+
firmware: which inputs wake the device, and any property a driver binding
|
|
31
|
+
requires (the DRV2605's `actuator-mode`, from the actuator chosen).
|
|
32
|
+
|
|
33
|
+
## Steps
|
|
34
|
+
|
|
35
|
+
### 1. Write the config
|
|
36
|
+
|
|
37
|
+
Next to the board:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// src/boards/<module>/firmware.config.ts
|
|
41
|
+
import { defineCapabilityConfig } from "@protoboardai/core";
|
|
42
|
+
export default defineCapabilityConfig({
|
|
43
|
+
provider: "@protoboardai/ext-zephyr",
|
|
44
|
+
constraints: { board_name: "<board>", wake: ["sw1"], properties: { u5: { "actuator-mode": "LRA" } } },
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Then `npx protoboard capabilities`: the board is listed under `firmware`
|
|
49
|
+
with `✓` and the provider.
|
|
50
|
+
|
|
51
|
+
### 2. Check before generating
|
|
52
|
+
|
|
53
|
+
`npx protoboard check`. Read every
|
|
54
|
+
`ext:@protoboardai/ext-zephyr/firmware:check` finding; errors stop
|
|
55
|
+
generate. `FW_NOT_GENERATED` (info) is expected now.
|
|
56
|
+
|
|
57
|
+
### 3. Generate
|
|
58
|
+
|
|
59
|
+
`npx protoboard run firmware:generate --module <module>`.
|
|
60
|
+
The summary names the files, the number of cited settings, buses and
|
|
61
|
+
GPIOs, and the warnings. Read the warnings (`--format json | jq .data`):
|
|
62
|
+
each names a device without a Zephyr driver or a gap to close.
|
|
63
|
+
|
|
64
|
+
### 4. Read the board devicetree
|
|
65
|
+
|
|
66
|
+
In
|
|
67
|
+
`src/boards/<module>/zephyr/boards/<vendor>/<board>/<board>.dts` and the
|
|
68
|
+
pinctrl file. Compare each comment with the schematic intent: the pin,
|
|
69
|
+
the net, the address, the polarity.
|
|
70
|
+
|
|
71
|
+
### 5. Export
|
|
72
|
+
|
|
73
|
+
`npx protoboard run firmware:export --module <module>`, then
|
|
74
|
+
`npx protoboard check` again: `protoboard/standard-outputs` and the
|
|
75
|
+
provider's check both clean.
|
|
76
|
+
|
|
77
|
+
### 6. Hand off
|
|
78
|
+
|
|
79
|
+
The build command in the README (`BOARD_ROOT`,
|
|
80
|
+
`EXTRA_CONF_FILE`), the warnings, and the bench items in "Verification".
|
|
81
|
+
|
|
82
|
+
## Decision points
|
|
83
|
+
|
|
84
|
+
| Question | Rule |
|
|
85
|
+
| --- | --- |
|
|
86
|
+
| A part has no Zephyr driver (warning) | Leave the generated node (address, label); the application drives it. Do not invent a compatible, and do not edit the installed provider package. If Zephyr has a driver for it, report the missing mapping (part id, compatible, binding file at the pinned release) to the provider's maintainers; meanwhile bind the driver in the application's own overlay (`app.overlay`: `&<label> { compatible = "..."; };`), which the generated files do not touch. |
|
|
87
|
+
| A binding needs a property the model lacks (`FW_PROPERTY_MISSING`) | If it is a fact of the part, add it to the part with its source (part authoring, repair mode). If it is a design choice, put it in `constraints.properties` with a comment saying why. |
|
|
88
|
+
| Which pins should a new bus use | Full-drive pads for I2C, SPI, UART (the module's usage note lists them); low-frequency pads only for buttons and status lines. Change the board's nets, never the generated file. |
|
|
89
|
+
| No crystal on XL1/XL2 (warning) | Acceptable for a prototype (RC oscillator); add the crystal to the board if timing accuracy or sleep current matters. |
|
|
90
|
+
| Two devices share an address | Move one to a second bus (a second set of nets) or choose a part variant with another address. |
|
|
91
|
+
| Something must differ from the generated files | Put it in the application's `prj.conf` or overlay. Never edit `zephyr/`. |
|
|
92
|
+
|
|
93
|
+
## Outputs
|
|
94
|
+
|
|
95
|
+
- `src/boards/<module>/zephyr/`: the board definition, `firmware.conf`,
|
|
96
|
+
`protoboard.lines.json` (native, provider-owned).
|
|
97
|
+
- `src/boards/<module>/out/config/` and `out/firmware.manifest.json`:
|
|
98
|
+
standard outputs, the only files the release and other tools read.
|
|
99
|
+
- The operation records in `operations/` (commit them with the outputs).
|
|
100
|
+
|
|
101
|
+
## Verification
|
|
102
|
+
|
|
103
|
+
- `npx protoboard check`: no error from `protoboard/standard-outputs` or the
|
|
104
|
+
firmware check; `FW_NATIVE_STALE` absent (the files match the model).
|
|
105
|
+
- `out/firmware.manifest.json`: every file listed, each setting line with at
|
|
106
|
+
least one cite (`net:`, `pin:`, `part:`, `constraint:`).
|
|
107
|
+
- First build: `west build -b <board> <app> -- -DBOARD_ROOT=… -DEXTRA_CONF_FILE=…`
|
|
108
|
+
compiles. A devicetree or Kconfig error there is a provider defect: record
|
|
109
|
+
it with the Zephyr version.
|
|
110
|
+
- On the bench (the release's test plan): an I2C scan finds the addresses in
|
|
111
|
+
the devicetree; each button, interrupt and display line works; the device
|
|
112
|
+
wakes from System OFF on each wake source. Record results with
|
|
113
|
+
**test-and-measure**.
|
|
114
|
+
|
|
115
|
+
## Failure handling
|
|
116
|
+
|
|
117
|
+
| Diagnostic | Meaning | Action |
|
|
118
|
+
| --- | --- | --- |
|
|
119
|
+
| `FW_NO_CONTROLLER`, `FW_CONTROLLER_AMBIGUOUS` | No microcontroller, or several | Fix the controller part's category, or set `constraints.controller`. |
|
|
120
|
+
| `FW_SOC_UNSUPPORTED` | The controller's SoC fact is not an nRF52840 | Use the provider for that SoC (ESP32-C3: `@protoboardai/ext-esp-idf`), or fix the part's SoC fact. |
|
|
121
|
+
| `FW_BUS_PROTOCOL` | A bus signal on a pad without that function (a low-frequency pad) | Move the net to a full-drive pad. |
|
|
122
|
+
| `FW_PIN_REUSED`, `FW_BUS_CROSSED` | One pin with two jobs; a peripheral's SDA/SCL swapped, or two SPI devices sharing SCK but not MOSI | Fix the nets. (Devices that agree on the lines they share are one bus.) |
|
|
123
|
+
| `FW_SPI_CS_MISSING` | A device on a shared SPI bus with no chip select | Wire its CS to a controller GPIO. |
|
|
124
|
+
| `FW_I2C_ADDRESS_UNKNOWN` | An I2C part states no address; a devicetree node needs `reg` | Fix the part (`i2c_address` on its I2C interface) through the parts library. |
|
|
125
|
+
| `FW_BUTTON_CODES` (warning) | More than 36 buttons: the extra ones get no `zephyr,code` | Set their key codes in the application overlay. |
|
|
126
|
+
| `FW_BUS_INCOMPLETE` | A peripheral's bus line reaches no controller pin | Add the missing pin to its net. |
|
|
127
|
+
| `FW_I2C_ADDRESS_CONFLICT` | Two devices at one address on one bus | Second bus or another part variant. |
|
|
128
|
+
| `FW_BUS_INSTANCES` | More buses than the nRF52840 has (i2c0/spi0 and i2c1/spi1 share hardware) | Share a bus. |
|
|
129
|
+
| `FW_BUS_SPEED` | A clock the TWIM or a device cannot run | Use 100 or 400 kHz, at most the slowest device's limit. |
|
|
130
|
+
| `FW_WAKE_UNKNOWN` | A wake source that is no input of the board | Name a button, or a device whose interrupt reaches the controller. |
|
|
131
|
+
| `FW_NATIVE_EDITED` | Someone edited `zephyr/` | Regenerate; move the change to the application's files. |
|
|
132
|
+
| `FW_NATIVE_STALE`, `FW_PIN_DRIFT` | The board changed since generate | Regenerate and export. |
|
|
133
|
+
| `FW_PIN_UNKNOWN` | A file names a pin the module does not have | Regenerate; if it persists, it is a provider defect. |
|
|
134
|
+
| `FW_DTS_SYNTAX`, `FW_LINE_UNCITED` | Provider defect | Report it with the board; do not work around it in the output. |
|