@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/design.ts
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Zephyr board definition (hardware model v2) for a UHD board whose
|
|
3
|
+
* controller is an nRF52840 module, from the board's firmware map
|
|
4
|
+
* (`boardFirmware` in @protoboardai/core). Checked against Zephyr v4.2.0.
|
|
5
|
+
*
|
|
6
|
+
* boards/<vendor>/<board>/board.yml board and SoC
|
|
7
|
+
* boards/<vendor>/<board>/Kconfig.<board> selects the SoC variant
|
|
8
|
+
* boards/<vendor>/<board>/<board>_defconfig MPU, GPIO, 32 kHz clock source
|
|
9
|
+
* boards/<vendor>/<board>/<board>.dts buses, devices, buttons, display, regulators
|
|
10
|
+
* boards/<vendor>/<board>/<board>-pinctrl.dtsi pin assignment of every bus
|
|
11
|
+
* firmware.conf the subsystems the board's drivers need
|
|
12
|
+
*
|
|
13
|
+
* Point BOARD_ROOT at the folder holding boards/ and build with
|
|
14
|
+
* `west build -b <board>`; add firmware.conf with EXTRA_CONF_FILE.
|
|
15
|
+
*
|
|
16
|
+
* Every setting line cites the UHD facts it comes from. What the model
|
|
17
|
+
* cannot state (a driver's required property) comes from the module's
|
|
18
|
+
* firmware.config.ts and cites it.
|
|
19
|
+
*/
|
|
20
|
+
import type { InterfaceDef, ModuleDef } from "@deltarobotics/uhd";
|
|
21
|
+
import type { BoardFirmware, CapabilityContext, CitedLine, FirmwareBus, FirmwareCite, FirmwareDesign, FirmwareDevice, FirmwareFile, FirmwareFinding, FirmwareGpio } from "@protoboardai/core";
|
|
22
|
+
import { wakeSources } from "@protoboardai/core";
|
|
23
|
+
import { DRIVERS } from "./drivers.ts";
|
|
24
|
+
import { NRF52840, ZEPHYR_VERSION, pinLabel, portPin, socFact, socOf, type ZephyrSoc } from "./soc.ts";
|
|
25
|
+
|
|
26
|
+
type Ctx = Pick<CapabilityContext, "constraints" | "module" | "project">;
|
|
27
|
+
|
|
28
|
+
const PSEL: Record<string, string> = { sda: "TWIM_SDA", scl: "TWIM_SCL", sck: "SPIM_SCK", mosi: "SPIM_MOSI", miso: "SPIM_MISO", tx: "UART_TX", rx: "UART_RX", rts: "UART_RTS", cts: "UART_CTS" };
|
|
29
|
+
const BUS_KCONFIG: Record<string, string> = { i2c: "I2C", spi: "SPI", uart: "SERIAL", onewire: "W1" };
|
|
30
|
+
|
|
31
|
+
/** Devicetree-safe names: lowercase, `_` in labels, `-` in node and property names. */
|
|
32
|
+
export const dtLabel = (s: string) => s.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
33
|
+
const dtName = (s: string) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
34
|
+
const hex = (n: number) => `0x${n.toString(16)}`;
|
|
35
|
+
const dtString = (s: string) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
36
|
+
|
|
37
|
+
/** A devicetree value from a config property: string, number, boolean or number array. */
|
|
38
|
+
function dtValue(v: unknown): string | undefined {
|
|
39
|
+
if (typeof v === "string") return dtString(v);
|
|
40
|
+
if (typeof v === "number" && Number.isInteger(v)) return `<${v}>`;
|
|
41
|
+
if (Array.isArray(v) && v.every((x) => Number.isInteger(x))) return `<${v.join(" ")}>`;
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Zephyr board name from the module id, unless the config names one. */
|
|
46
|
+
export function boardName(board: ModuleDef, constraints: Record<string, unknown>): string {
|
|
47
|
+
return typeof constraints.board_name === "string" ? constraints.board_name : dtLabel(board.id);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function designZephyr(fw: BoardFirmware, board: ModuleDef, controller: ModuleDef, ctx: Ctx): FirmwareDesign {
|
|
51
|
+
const findings: FirmwareFinding[] = [];
|
|
52
|
+
const warnings: string[] = [];
|
|
53
|
+
const c = ctx.constraints;
|
|
54
|
+
const soc = socOf(controller);
|
|
55
|
+
const fact = socFact(controller);
|
|
56
|
+
if (!soc) {
|
|
57
|
+
findings.push({ code: "FW_SOC_UNSUPPORTED", severity: "error", instance: fw.controller!.instance, message: `${controller.id} states SoC "${fact ?? "nothing"}"; this provider knows ${NRF52840.name} (domains[electrical].metadata.soc)` });
|
|
58
|
+
return { files: [], warnings, findings };
|
|
59
|
+
}
|
|
60
|
+
const ctl = fw.controller!.instance;
|
|
61
|
+
const socCite: FirmwareCite = `part:${controller.id}:domains.electrical.metadata.soc`;
|
|
62
|
+
const name = boardName(board, c);
|
|
63
|
+
const vendor = typeof c.vendor === "string" ? c.vendor : "protoboard";
|
|
64
|
+
const dir = `boards/${vendor}/${name}`;
|
|
65
|
+
const leaf = (pin: string) => controller.interfaces.find((i) => i.id === pin);
|
|
66
|
+
const pp = (pin: string) => portPin(leaf(pin))!;
|
|
67
|
+
const where = (pin: string) => `${ctl}:${pin} ${leaf(pin)?.name ?? pin}`;
|
|
68
|
+
const gpioCell = (pin: string, flags: string[]) => {
|
|
69
|
+
const p = pp(pin);
|
|
70
|
+
return `<&gpio${p.port} ${p.pin} ${flags.length > 1 ? `(${flags.join(" | ")})` : flags[0]}>`;
|
|
71
|
+
};
|
|
72
|
+
const flagsOf = (g: FirmwareGpio, extra: string[] = []) => [...(g.pull === "up" ? ["GPIO_PULL_UP"] : g.pull === "down" ? ["GPIO_PULL_DOWN"] : []), ...extra, g.activeLow ? "GPIO_ACTIVE_LOW" : "GPIO_ACTIVE_HIGH"];
|
|
73
|
+
const gpioWhy = (g: FirmwareGpio) => `${g.label}: ${where(g.pin)}${g.externalPull ? `, pulled ${g.externalPull.to} by ${g.externalPull.instance}` : ""}`;
|
|
74
|
+
|
|
75
|
+
for (const [pin] of Object.entries(fw.uses)) {
|
|
76
|
+
if (leaf(pin) && !portPin(leaf(pin)) && !fw.usb?.pins.includes(pin)) {
|
|
77
|
+
findings.push({ code: "FW_PIN_UNKNOWN", severity: "error", instance: ctl, message: `${where(pin)} has no nRF port/pin name (P<port>.<pin>), so it cannot be configured`, cites: [`pin:${ctl}:${pin}`] });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (findings.length) return { files: [], warnings, findings };
|
|
81
|
+
|
|
82
|
+
// ---- bus instances
|
|
83
|
+
const used = new Set<string>();
|
|
84
|
+
const instanceOf = new Map<FirmwareBus, string>();
|
|
85
|
+
for (const bus of fw.buses) {
|
|
86
|
+
if (bus.protocol === "onewire") continue;
|
|
87
|
+
const pool = soc[bus.protocol as "i2c" | "spi" | "uart"];
|
|
88
|
+
const free = pool.find((x) => !used.has(x) && !(soc.sharesId[x] && used.has(soc.sharesId[x])));
|
|
89
|
+
if (!free) {
|
|
90
|
+
findings.push({ code: "FW_BUS_INSTANCES", severity: "error", instance: ctl, message: `${bus.id}: no ${bus.protocol.toUpperCase()} instance left on the ${soc.name} (${pool.join(", ")}; i2c0/spi0 and i2c1/spi1 share hardware)`, cites: bus.signals.flatMap((s) => s.cites) });
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
used.add(free);
|
|
94
|
+
instanceOf.set(bus, free);
|
|
95
|
+
}
|
|
96
|
+
if (findings.length) return { files: [], warnings, findings };
|
|
97
|
+
|
|
98
|
+
const kconfig = new Map<string, FirmwareCite[]>();
|
|
99
|
+
const needs = (sym: string, cites: FirmwareCite[]) => kconfig.set(sym, [...(kconfig.get(sym) ?? []), ...cites]);
|
|
100
|
+
const zephyrUser: CitedLine[] = [];
|
|
101
|
+
const userGpio = (g: FirmwareGpio) => {
|
|
102
|
+
zephyrUser.push({ text: `\t\t${dtName(`${g.instance}-${leafName(g)}`)}-gpios = ${gpioCell(g.pin, flagsOf(g))};`, cites: g.cites, why: gpioWhy(g) });
|
|
103
|
+
};
|
|
104
|
+
const leafName = (g: FirmwareGpio) => {
|
|
105
|
+
const part = ctx.project.lookup(g.part);
|
|
106
|
+
return part?.interfaces.find((i: InterfaceDef) => i.id === g.peripheralPin)?.name ?? g.use;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// ---- pinctrl
|
|
110
|
+
const pinctrl: CitedLine[] = [{ text: "&pinctrl {" }];
|
|
111
|
+
for (const [bus, inst] of instanceOf) {
|
|
112
|
+
const sigs = [...bus.signals].sort((a, b) => Object.keys(PSEL).indexOf(a.role) - Object.keys(PSEL).indexOf(b.role));
|
|
113
|
+
const cites = sigs.flatMap((s) => s.cites);
|
|
114
|
+
const psel = (s: (typeof sigs)[number], i: number, all: typeof sigs) => {
|
|
115
|
+
const p = pp(s.pin);
|
|
116
|
+
const head = i === 0 ? "\t\t\tpsels = " : "\t\t\t\t";
|
|
117
|
+
return { text: `${head}<NRF_PSEL(${PSEL[s.role]}, ${p.port}, ${p.pin})>${i === all.length - 1 ? ";" : ","}`, cites: s.cites, why: `${s.role.toUpperCase()} on net ${s.net}: ${where(s.pin)}` };
|
|
118
|
+
};
|
|
119
|
+
pinctrl.push({ text: `\t${inst}_default: ${inst}_default {` });
|
|
120
|
+
if (bus.protocol === "uart") {
|
|
121
|
+
// receive lines get a pull-up so an idle, unplugged line reads as idle (Zephyr's nRF boards do the same)
|
|
122
|
+
const outs = sigs.filter((s) => s.role === "tx" || s.role === "rts");
|
|
123
|
+
const ins = sigs.filter((s) => s.role === "rx" || s.role === "cts");
|
|
124
|
+
if (outs.length) pinctrl.push({ text: "\t\tgroup1 {" }, ...outs.map(psel), { text: "\t\t};" });
|
|
125
|
+
if (ins.length) pinctrl.push({ text: `\t\tgroup${outs.length ? 2 : 1} {` }, ...ins.map(psel), { text: "\t\t\tbias-pull-up;", cites: ins.flatMap((s) => s.cites), why: "idle-high receive line" }, { text: "\t\t};" });
|
|
126
|
+
} else pinctrl.push({ text: "\t\tgroup1 {" }, ...sigs.map(psel), { text: "\t\t};" });
|
|
127
|
+
pinctrl.push({ text: "\t};" }, { text: "" });
|
|
128
|
+
pinctrl.push({ text: `\t${inst}_sleep: ${inst}_sleep {` }, { text: "\t\tgroup1 {" }, ...sigs.map(psel), { text: "\t\t\tlow-power-enable;", cites, why: "pins released in sleep" }, { text: "\t\t};" }, { text: "\t};" }, { text: "" });
|
|
129
|
+
}
|
|
130
|
+
if (pinctrl[pinctrl.length - 1].text === "") pinctrl.pop();
|
|
131
|
+
pinctrl.push({ text: "};" });
|
|
132
|
+
|
|
133
|
+
// ---- devicetree
|
|
134
|
+
const root: CitedLine[] = [];
|
|
135
|
+
const after: CitedLine[] = [];
|
|
136
|
+
const includes: CitedLine[] = [
|
|
137
|
+
{ text: "/dts-v1/;" },
|
|
138
|
+
{ text: `#include <${soc.dtsi}>`, cites: [socCite], why: `${controller.id}: ${fact}` },
|
|
139
|
+
{ text: `#include "${name}-pinctrl.dtsi"` },
|
|
140
|
+
];
|
|
141
|
+
const memCite: FirmwareCite = `part:${controller.id}:domains.electrical.metadata.memory`;
|
|
142
|
+
root.push(
|
|
143
|
+
{ text: "/ {" },
|
|
144
|
+
{ text: `\tmodel = ${dtString(board.name)};`, cites: ["module:name"] },
|
|
145
|
+
{ text: `\tcompatible = ${dtString(`${vendor},${dtName(name)}`)};`, cites: ["module:id", ...(typeof c.vendor === "string" ? ["constraint:vendor" as const] : [])] },
|
|
146
|
+
{ text: "" },
|
|
147
|
+
{ text: "\tchosen {" },
|
|
148
|
+
{ text: "\t\tzephyr,sram = &sram0;", cites: [memCite], why: "on-chip RAM" },
|
|
149
|
+
{ text: "\t\tzephyr,flash = &flash0;", cites: [memCite], why: "on-chip flash" },
|
|
150
|
+
);
|
|
151
|
+
const console = typeof c.console === "string" ? c.console : undefined;
|
|
152
|
+
if (console) {
|
|
153
|
+
const bus = [...instanceOf].find(([b]) => b.id === console || b.devices.some((d) => d.instance === console));
|
|
154
|
+
if (bus && bus[0].protocol === "uart") root.push({ text: `\t\tzephyr,console = &${bus[1]};`, cites: ["constraint:console", ...bus[0].signals.flatMap((s) => s.cites)] });
|
|
155
|
+
else findings.push({ code: "FW_CONSOLE_UNKNOWN", severity: "error", message: `console ${console} is no UART bus or UART device of the board`, cites: ["constraint:console"] });
|
|
156
|
+
}
|
|
157
|
+
root.push({ text: "\t};" });
|
|
158
|
+
|
|
159
|
+
// buttons
|
|
160
|
+
if (fw.gpios.some((g) => g.use === "button")) {
|
|
161
|
+
includes.push({ text: "#include <zephyr/dt-bindings/input/input-event-codes.h>", cites: fw.gpios.filter((g) => g.use === "button").flatMap((g) => g.cites.slice(0, 1)) });
|
|
162
|
+
root.push({ text: "" }, { text: "\tbuttons {" }, { text: '\t\tcompatible = "gpio-keys";', cites: fw.gpios.filter((g) => g.use === "button").flatMap((g) => g.cites.slice(0, 1)) });
|
|
163
|
+
fw.gpios
|
|
164
|
+
.filter((g) => g.use === "button")
|
|
165
|
+
.forEach((g, i) => {
|
|
166
|
+
root.push(
|
|
167
|
+
{ text: `\t\tbutton_${dtLabel(g.instance)}: button_${i} {` },
|
|
168
|
+
{ text: `\t\t\tgpios = ${gpioCell(g.pin, flagsOf(g))};`, cites: g.cites, why: gpioWhy(g) },
|
|
169
|
+
{ text: `\t\t\tlabel = ${dtString(g.label)};`, cites: [`pin:${g.instance}:${ctx.project.lookup(g.part)?.interfaces[0]?.id ?? "pin_1"}`] },
|
|
170
|
+
...(buttonCode(i) ? [{ text: `\t\t\tzephyr,code = <${buttonCode(i)}>;`, cites: g.cites.slice(0, 1) }] : []),
|
|
171
|
+
{ text: "\t\t};" },
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
const buttons = fw.gpios.filter((g) => g.use === "button");
|
|
175
|
+
if (buttons.length > BUTTON_CODES) {
|
|
176
|
+
// zephyr,code is optional in gpio-keys.yaml: the extra buttons get none rather than a code that is not defined
|
|
177
|
+
findings.push({ code: "FW_BUTTON_CODES", severity: "warning", message: `${buttons.length} buttons: this provider gives them INPUT_KEY_0..9 and INPUT_KEY_A..Z (${BUTTON_CODES}); ${buttons.slice(BUTTON_CODES).map((g) => g.instance).join(", ")} ${buttons.length - BUTTON_CODES === 1 ? "gets" : "get"} no zephyr,code: set it in the application overlay`, cites: buttons.slice(BUTTON_CODES).flatMap((g) => g.cites.slice(0, 1)) });
|
|
178
|
+
}
|
|
179
|
+
root.push({ text: "\t};" });
|
|
180
|
+
needs("INPUT", fw.gpios.filter((g) => g.use === "button").flatMap((g) => g.cites.slice(0, 1)));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// devices on buses
|
|
184
|
+
const busNodes: CitedLine[] = [];
|
|
185
|
+
for (const [bus, inst] of instanceOf) {
|
|
186
|
+
const sigCites = bus.signals.flatMap((s) => s.cites);
|
|
187
|
+
needs(BUS_KCONFIG[bus.protocol], sigCites);
|
|
188
|
+
busNodes.push({ text: "" }, { text: `&${inst} {` });
|
|
189
|
+
if (bus.protocol === "i2c") busNodes.push({ text: '\tcompatible = "nordic,nrf-twim";', cites: sigCites, why: "EasyDMA TWI master" });
|
|
190
|
+
if (bus.protocol === "spi") busNodes.push({ text: '\tcompatible = "nordic,nrf-spim";', cites: sigCites, why: "EasyDMA SPI master" });
|
|
191
|
+
if (bus.protocol === "uart") busNodes.push({ text: '\tcompatible = "nordic,nrf-uarte";', cites: sigCites, why: "EasyDMA UART" });
|
|
192
|
+
busNodes.push(
|
|
193
|
+
{ text: '\tstatus = "okay";', cites: sigCites },
|
|
194
|
+
{ text: `\tpinctrl-0 = <&${inst}_default>;`, cites: sigCites },
|
|
195
|
+
{ text: `\tpinctrl-1 = <&${inst}_sleep>;`, cites: sigCites },
|
|
196
|
+
{ text: '\tpinctrl-names = "default", "sleep";', cites: sigCites },
|
|
197
|
+
);
|
|
198
|
+
if (bus.protocol === "i2c") {
|
|
199
|
+
const asked = c.i2c_frequency_hz;
|
|
200
|
+
const slowest = Math.min(...bus.devices.map((d) => d.maxClockHz ?? 400_000), 400_000);
|
|
201
|
+
const hz = typeof asked === "number" ? asked : slowest >= 400_000 ? 400_000 : 100_000;
|
|
202
|
+
const macro = hz === 400_000 ? "I2C_BITRATE_FAST" : hz === 100_000 ? "I2C_BITRATE_STANDARD" : undefined;
|
|
203
|
+
if (!macro) findings.push({ code: "FW_BUS_SPEED", severity: "error", message: `i2c_frequency_hz ${hz}: the nRF52840 TWIM runs at 100 kHz or 400 kHz (part fact bus_limits.twim_kbps)`, cites: ["constraint:i2c_frequency_hz"] });
|
|
204
|
+
else {
|
|
205
|
+
busNodes.push({
|
|
206
|
+
text: `\tclock-frequency = <${macro}>;`,
|
|
207
|
+
cites: [...(typeof asked === "number" ? ["constraint:i2c_frequency_hz" as const] : []), `part:${controller.id}:domains.electrical.metadata.bus_limits.twim_kbps`, ...bus.devices.flatMap((d) => d.cites.filter((x) => x.endsWith(".clock_freq")))],
|
|
208
|
+
why: `${hz / 1000} kHz: ${typeof asked === "number" ? "set in firmware.config.ts" : `the slowest device allows ${slowest / 1000} kHz`}`,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// SPI: reg is the device's index in cs-gpios, so every device on a shared bus needs its own chip select
|
|
213
|
+
const csIndex = new Map<FirmwareDevice, number>();
|
|
214
|
+
if (bus.protocol === "spi") {
|
|
215
|
+
const cs = bus.devices.filter((d) => d.cs);
|
|
216
|
+
cs.forEach((d, i) => csIndex.set(d, i));
|
|
217
|
+
if (cs.length) busNodes.push({ text: `\tcs-gpios = ${cs.map((d) => gpioCell(d.cs!.pin, flagsOf(d.cs!))).join(", ")};`, cites: cs.flatMap((d) => d.cs!.cites), why: cs.map((d) => `${d.instance} CS ${where(d.cs!.pin)}`).join("; ") });
|
|
218
|
+
if (bus.devices.length > 1) {
|
|
219
|
+
for (const d of bus.devices.filter((d) => !d.cs)) {
|
|
220
|
+
findings.push({ code: "FW_SPI_CS_MISSING", severity: "error", instance: d.instance, message: `${d.instance} (${d.part}) shares ${bus.id} with ${bus.devices.filter((x) => x !== d).map((x) => x.instance).join(", ")} but has no chip select wired to the controller`, cites: d.cites });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (bus.protocol === "uart") {
|
|
225
|
+
const d = bus.devices[0];
|
|
226
|
+
const asked = (c.uart_baud as Record<string, unknown> | undefined)?.[d?.instance ?? ""];
|
|
227
|
+
const baud = typeof asked === "number" ? asked : d?.baud;
|
|
228
|
+
if (baud) busNodes.push({ text: `\tcurrent-speed = <${baud}>;`, cites: typeof asked === "number" ? ["constraint:uart_baud"] : [`part:${d.part}:${d.interface}.baud_rate`], why: `${d.instance} (${d.name})` });
|
|
229
|
+
else findings.push({ code: "FW_UART_BAUD_UNKNOWN", severity: "error", instance: d?.instance, message: `${d?.instance} states no baud_rate; set uart_baud: { ${d?.instance}: <baud> } in firmware.config.ts`, cites: d?.cites });
|
|
230
|
+
}
|
|
231
|
+
bus.devices.forEach((d, i) => {
|
|
232
|
+
// an I2C node needs reg (i2c-device.yaml); a device with no address is an error from the shared rules and gets no node
|
|
233
|
+
if (bus.protocol === "i2c" && d.address !== undefined) busNodes.push(...deviceNode(d, `@${d.address.toString(16)}`, `<${hex(d.address)}>`));
|
|
234
|
+
const reg = csIndex.get(d) ?? 0;
|
|
235
|
+
if (bus.protocol === "spi" && !isDisplay(d)) busNodes.push(...deviceNode(d, `@${reg}`, `<${reg}>`, inst));
|
|
236
|
+
if (bus.protocol === "spi" && isDisplay(d)) root.push(...displayNode(d, inst, reg));
|
|
237
|
+
if (bus.protocol === "uart") {
|
|
238
|
+
warnings.push(`${d.instance} (${d.part}) on ${inst}: no Zephyr driver in this provider's table; the application talks to ${inst} directly`);
|
|
239
|
+
for (const g of d.controls) userGpio(g);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
busNodes.push({ text: "};" });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 1-Wire
|
|
246
|
+
for (const bus of fw.buses.filter((b) => b.protocol === "onewire")) {
|
|
247
|
+
const s = bus.signals[0];
|
|
248
|
+
const ext = fw.netlist.nets.find((n) => n.id === s.net);
|
|
249
|
+
const pulled = ext?.pins.some((p) => fw.netlist.components.find((x) => x.instance === p.split(":")[0])?.kind === "resistor");
|
|
250
|
+
needs("W1", s.cites);
|
|
251
|
+
root.push(
|
|
252
|
+
{ text: "" },
|
|
253
|
+
{ text: `\tw1_${dtLabel(bus.id)}: w1-${dtName(bus.id)} {` },
|
|
254
|
+
{ text: '\t\tcompatible = "zephyr,w1-gpio";', cites: s.cites },
|
|
255
|
+
{ text: `\t\tgpios = ${gpioCell(s.pin, [...(pulled ? [] : ["GPIO_PULL_UP"]), "GPIO_OPEN_DRAIN", "GPIO_ACTIVE_HIGH"])};`, cites: s.cites, why: `1-Wire DQ on net ${s.net}: ${where(s.pin)}${pulled ? " (pull-up resistor on the board)" : ""}` },
|
|
256
|
+
);
|
|
257
|
+
for (const d of bus.devices) {
|
|
258
|
+
const drv = DRIVERS[d.part];
|
|
259
|
+
if (!drv) {
|
|
260
|
+
warnings.push(`${d.instance} (${d.part}) on 1-Wire: no Zephyr driver in this provider's table`);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
drv.kconfig.forEach((k) => needs(k, d.cites));
|
|
264
|
+
root.push({ text: `\t\t${dtLabel(d.instance)}: ${drv.node} {` }, { text: `\t\t\tcompatible = ${dtString(drv.compatible)};`, cites: d.cites, why: d.name });
|
|
265
|
+
root.push(...fromPart(d, "\t\t\t"));
|
|
266
|
+
root.push({ text: "\t\t};" });
|
|
267
|
+
}
|
|
268
|
+
root.push({ text: "\t};" });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// controls without a driver property, and wake sources
|
|
272
|
+
for (const g of fw.gpios.filter((g) => g.use !== "button")) userGpio(g);
|
|
273
|
+
const { sources } = wakeSources(fw, c.wake);
|
|
274
|
+
if (sources.length) {
|
|
275
|
+
zephyrUser.push({
|
|
276
|
+
text: `\t\twakeup-gpios = ${sources.map((w) => gpioCell(w.pin, [w.activeLow ? "GPIO_ACTIVE_LOW" : "GPIO_ACTIVE_HIGH"])).join(", ")};`,
|
|
277
|
+
cites: sources.flatMap((w) => w.cites),
|
|
278
|
+
why: `wake from System OFF: ${sources.map((w) => w.source).join(", ")} (any nRF52840 GPIO can, through its SENSE level)`,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
if (zephyrUser.length) root.push({ text: "" }, { text: "\tzephyr,user {" }, ...zephyrUser, { text: "\t};" });
|
|
282
|
+
root.push({ text: "};" });
|
|
283
|
+
|
|
284
|
+
// regulators, GPIO ports, UICR, USB
|
|
285
|
+
const internal = metadata(controller, "internal");
|
|
286
|
+
if (typeof internal === "string" && /REG1 DC\/DC inductor/i.test(internal)) {
|
|
287
|
+
after.push({ text: "" }, { text: "®1 {" }, { text: "\tregulator-initial-mode = <NRF5X_REG_MODE_DCDC>;", cites: [`part:${controller.id}:domains.electrical.metadata.internal`], why: "REG1 DC/DC inductor is inside the module" }, { text: "};" });
|
|
288
|
+
}
|
|
289
|
+
after.push({ text: "" }, { text: "&gpiote {" }, { text: '\tstatus = "okay";', cites: Object.keys(fw.uses).slice(0, 1).map((p) => `pin:${ctl}:${p}` as const), why: "GPIO interrupts" }, { text: "};" });
|
|
290
|
+
for (const port of [0, 1]) {
|
|
291
|
+
const pins = Object.keys(fw.uses)
|
|
292
|
+
.filter((p) => portPin(leaf(p))?.port === port && !fw.usb?.pins.includes(p))
|
|
293
|
+
.sort((a, b) => pp(a).pin - pp(b).pin);
|
|
294
|
+
if (pins.length) after.push({ text: "" }, { text: `&gpio${port} {` }, { text: '\tstatus = "okay";', cites: pins.map((p) => `pin:${ctl}:${p}` as const), why: `used: ${pins.map((p) => pinLabel(pp(p))).join(", ")}` }, { text: "};" });
|
|
295
|
+
}
|
|
296
|
+
const nfc = Object.keys(fw.uses).filter((p) => (leaf(p)?.capabilities ?? []).some((x) => x === "nfc1" || x === "nfc2"));
|
|
297
|
+
if (nfc.length) after.push({ text: "" }, { text: "&uicr {" }, { text: "\tnfct-pins-as-gpios;", cites: nfc.map((p) => `pin:${ctl}:${p}` as const), why: `${nfc.map((p) => leaf(p)?.name).join(", ")} used as GPIO` }, { text: "};" });
|
|
298
|
+
if (fw.usb) {
|
|
299
|
+
after.push({ text: "" }, { text: "zephyr_udc0: &usbd {" }, { text: '\tstatus = "okay";', cites: fw.usb.cites, why: `USB D+/D- wired to ${fw.usb.peers.join(", ")}` }, { text: "};" });
|
|
300
|
+
needs("USB_DEVICE_STACK_NEXT", fw.usb.cites);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const dts: FirmwareFile = {
|
|
304
|
+
path: `${dir}/${name}.dts`,
|
|
305
|
+
role: "board devicetree",
|
|
306
|
+
style: "c-block",
|
|
307
|
+
warnings: true,
|
|
308
|
+
header: [`${board.name}: Zephyr ${ZEPHYR_VERSION} board devicetree, generated from the UHD model by @protoboardai/ext-zephyr. Do not edit.`],
|
|
309
|
+
lines: [...includes, { text: "" }, ...root, ...busNodes, ...after],
|
|
310
|
+
};
|
|
311
|
+
const pinctrlFile: FirmwareFile = { path: `${dir}/${name}-pinctrl.dtsi`, role: "pin assignment", style: "c-block", header: [`${board.name}: pin assignment, generated from the board's nets. Do not edit.`], lines: pinctrl };
|
|
312
|
+
|
|
313
|
+
// ---- Kconfig and board metadata
|
|
314
|
+
const xtal = fw.crystals.find((x) => x.pins.some((p) => (leaf(p)?.capabilities ?? []).some((k) => k.startsWith("lfxo_"))));
|
|
315
|
+
const defconfig: CitedLine[] = [
|
|
316
|
+
{ text: "CONFIG_ARM_MPU=y", cites: [`part:${controller.id}:domains.electrical.metadata.cpu`], why: `${metadata(controller, "cpu")}: memory protection unit` },
|
|
317
|
+
];
|
|
318
|
+
if (Object.keys(fw.uses).length) defconfig.push({ text: "CONFIG_GPIO=y", cites: Object.keys(fw.uses).slice(0, 3).map((p) => `pin:${ctl}:${p}` as const), why: "the board uses GPIOs" });
|
|
319
|
+
if (xtal) {
|
|
320
|
+
defconfig.push({ text: "CONFIG_CLOCK_CONTROL_NRF_K32SRC_XTAL=y", cites: xtal.cites, why: `32.768 kHz crystal ${xtal.instance} (${xtal.part}) on ${xtal.pins.map((p) => leaf(p)?.name).join(", ")}` });
|
|
321
|
+
} else {
|
|
322
|
+
defconfig.push({ text: "CONFIG_CLOCK_CONTROL_NRF_K32SRC_RC=y", cites: [`part:${controller.id}:traits.usage_note`], why: "no crystal on XL1/XL2: the internal RC oscillator (the module's usage note: omit the crystal with the RC oscillator)" });
|
|
323
|
+
warnings.push("no 32.768 kHz crystal on XL1/XL2: the low-frequency clock runs from the RC oscillator (±500 ppm after calibration); BLE timing allows it, but sleep current rises with calibration");
|
|
324
|
+
}
|
|
325
|
+
const files: FirmwareFile[] = [
|
|
326
|
+
{
|
|
327
|
+
path: `${dir}/board.yml`,
|
|
328
|
+
role: "board metadata",
|
|
329
|
+
style: "yaml",
|
|
330
|
+
header: ["Generated from the UHD model by @protoboardai/ext-zephyr. Do not edit."],
|
|
331
|
+
lines: [
|
|
332
|
+
{ text: "board:" },
|
|
333
|
+
{ text: ` name: ${name}`, cites: [typeof c.board_name === "string" ? "constraint:board_name" : "module:id"] },
|
|
334
|
+
{ text: ` full_name: ${board.name}`, cites: ["module:name"] },
|
|
335
|
+
{ text: ` vendor: ${vendor}`, cites: [typeof c.vendor === "string" ? "constraint:vendor" : "module:id"] },
|
|
336
|
+
{ text: " socs:" },
|
|
337
|
+
{ text: ` - name: ${soc.name}`, cites: [socCite], why: fact },
|
|
338
|
+
],
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
path: `${dir}/Kconfig.${name}`,
|
|
342
|
+
role: "board Kconfig",
|
|
343
|
+
style: "kconfig",
|
|
344
|
+
header: ["Generated from the UHD model by @protoboardai/ext-zephyr. Do not edit."],
|
|
345
|
+
lines: [
|
|
346
|
+
{ text: `config BOARD_${name.toUpperCase()}`, cites: ["module:id"] },
|
|
347
|
+
{ text: `\tselect ${soc.kconfig}`, cites: [socCite], why: `${fact} (${soc.kconfig}: the ${soc.packageMatch.source.replace(/[\\?-]/g, "")} package)` },
|
|
348
|
+
],
|
|
349
|
+
},
|
|
350
|
+
{ path: `${dir}/${name}_defconfig`, role: "board defconfig", style: "conf", header: ["Generated from the UHD model by @protoboardai/ext-zephyr. Do not edit."], lines: defconfig },
|
|
351
|
+
dts,
|
|
352
|
+
pinctrlFile,
|
|
353
|
+
{
|
|
354
|
+
path: "firmware.conf",
|
|
355
|
+
role: "Kconfig fragment: subsystems for the board's devices",
|
|
356
|
+
style: "conf",
|
|
357
|
+
header: [`Subsystems the board's devices need (Zephyr ${ZEPHYR_VERSION}); each driver turns itself on when its devicetree node is enabled.`, "Add with -DEXTRA_CONF_FILE=firmware.conf. Generated by @protoboardai/ext-zephyr. Do not edit."],
|
|
358
|
+
warnings: true,
|
|
359
|
+
lines: [...kconfig].map(([sym, cites]) => ({ text: `CONFIG_${sym}=y`, cites: [...new Set(cites)].slice(0, 6), why: KCONFIG_WHY[sym] ?? sym })),
|
|
360
|
+
},
|
|
361
|
+
];
|
|
362
|
+
if (!soc.packageMatch.test(fact ?? "")) warnings.push(`${controller.id}: SoC fact "${fact}" does not name the ${soc.kconfig} package; check the Kconfig select`);
|
|
363
|
+
return { files, warnings, findings };
|
|
364
|
+
|
|
365
|
+
// ---- helpers that close over the context
|
|
366
|
+
function isDisplay(d: FirmwareDevice) {
|
|
367
|
+
// the UHD taxonomy's component.display, or a bare display category from before it
|
|
368
|
+
return d.categories.some((x) => /^(component\.)?display(\.|$)/.test(x));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function fromPart(d: FirmwareDevice, indent: string): CitedLine[] {
|
|
372
|
+
const drv = DRIVERS[d.part];
|
|
373
|
+
const out: CitedLine[] = [];
|
|
374
|
+
const part = ctx.project.lookup(d.part);
|
|
375
|
+
for (const f of drv?.fromPart ?? []) {
|
|
376
|
+
const t = part?.traits?.find((x) => x.type === "performance" && (x.params as Record<string, unknown> | undefined)?.[f.fact] !== undefined);
|
|
377
|
+
const v = (t?.params as Record<string, unknown> | undefined)?.[f.fact];
|
|
378
|
+
if (v !== undefined) out.push({ text: `${indent}${f.property} = ${f.format === "int" ? `<${v}>` : dtString(String(v))};`, cites: [`part:${d.part}:traits.performance.${f.fact}`] });
|
|
379
|
+
}
|
|
380
|
+
const props = (c.properties as Record<string, Record<string, unknown>> | undefined)?.[d.instance] ?? {};
|
|
381
|
+
for (const [k, v] of Object.entries(props)) {
|
|
382
|
+
const val = v === true ? undefined : dtValue(v);
|
|
383
|
+
if (v === true) out.push({ text: `${indent}${k};`, cites: ["constraint:properties"] });
|
|
384
|
+
else if (val) out.push({ text: `${indent}${k} = ${val};`, cites: ["constraint:properties"], why: "firmware.config.ts" });
|
|
385
|
+
else findings.push({ code: "FW_PROPERTY_INVALID", severity: "error", instance: d.instance, message: `properties.${d.instance}.${k}: use a string, an integer, an integer array or true`, cites: ["constraint:properties"] });
|
|
386
|
+
}
|
|
387
|
+
for (const r of drv?.required ?? []) {
|
|
388
|
+
if (props[r] === undefined) findings.push({ code: "FW_PROPERTY_MISSING", severity: "error", instance: d.instance, message: `${drv!.compatible} requires ${r}, which the model does not state; set properties: { ${d.instance}: { "${r}": … } } in firmware.config.ts`, cites: d.cites });
|
|
389
|
+
}
|
|
390
|
+
return out;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function controlLines(d: FirmwareDevice, indent: string): CitedLine[] {
|
|
394
|
+
const drv = DRIVERS[d.part];
|
|
395
|
+
const out: CitedLine[] = [];
|
|
396
|
+
for (const g of d.controls) {
|
|
397
|
+
const prop = drv?.controls?.[g.use];
|
|
398
|
+
if (prop) out.push({ text: `${indent}${prop} = ${gpioCell(g.pin, flagsOf(g))};`, cites: g.cites, why: gpioWhy(g) });
|
|
399
|
+
else userGpio(g);
|
|
400
|
+
}
|
|
401
|
+
return out;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function deviceNode(d: FirmwareDevice, unit: string, reg: string | undefined, spiInst?: string): CitedLine[] {
|
|
405
|
+
const drv = DRIVERS[d.part];
|
|
406
|
+
const out: CitedLine[] = [{ text: "" }, { text: `\t${dtLabel(d.instance)}: ${drv?.node ?? dtName(d.categories[d.categories.length - 1] ?? "device")}${unit} {` }];
|
|
407
|
+
if (drv) {
|
|
408
|
+
out.push({ text: `\t\tcompatible = ${dtString(drv.compatible)};`, cites: [`pin:${d.instance}:${d.interface}`], why: `${d.name} (${d.part})` });
|
|
409
|
+
drv.kconfig.forEach((k) => needs(k, [`pin:${d.instance}:${d.interface}`]));
|
|
410
|
+
if (drv.childrenNote) warnings.push(`${d.instance} (${drv.compatible}): ${drv.childrenNote}`);
|
|
411
|
+
} else {
|
|
412
|
+
warnings.push(`${d.instance} (${d.part}): no Zephyr driver in this provider's table; its node has no compatible, so the application finds it by the label ${dtLabel(d.instance)} and drives it itself`);
|
|
413
|
+
}
|
|
414
|
+
if (reg) out.push({ text: `\t\treg = ${reg};`, cites: d.cites, why: d.address !== undefined ? `I2C address ${hex(d.address)} (${d.part})` : `chip select ${reg}` });
|
|
415
|
+
if (spiInst && d.maxClockHz) {
|
|
416
|
+
const hz = Math.min(d.maxClockHz, NRF52840.spiMaxHz[spiInst]);
|
|
417
|
+
out.push({ text: `\t\tspi-max-frequency = <${hz}>;`, cites: [...d.cites, `part:${controller.id}:domains.electrical.metadata.bus_limits.spim_Mbps`] });
|
|
418
|
+
}
|
|
419
|
+
if (drv) out.push(...fromPart(d, "\t\t"));
|
|
420
|
+
out.push(...controlLines(d, "\t\t"));
|
|
421
|
+
out.push({ text: "\t};" });
|
|
422
|
+
return out;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function displayNode(d: FirmwareDevice, spiInst: string, index: number): CitedLine[] {
|
|
426
|
+
const drv = DRIVERS[d.part];
|
|
427
|
+
const part = ctx.project.lookup(d.part);
|
|
428
|
+
const perf = part?.traits?.find((t) => t.type === "performance" && Array.isArray((t.params as Record<string, unknown> | undefined)?.resolution))?.params as { resolution?: number[]; controller?: string } | undefined;
|
|
429
|
+
const out: CitedLine[] = [{ text: "" }, { text: `\tmipi_dbi_${dtLabel(d.instance)}: mipi-dbi-${dtName(d.instance)} {` }];
|
|
430
|
+
out.push({ text: '\t\tcompatible = "zephyr,mipi-dbi-spi";', cites: [`pin:${d.instance}:${d.interface}`], why: "display on SPI with D/C (MIPI DBI type C, 4-wire)" });
|
|
431
|
+
out.push({ text: `\t\tspi-dev = <&${spiInst}>;`, cites: [`pin:${d.instance}:${d.interface}`] });
|
|
432
|
+
for (const g of d.controls.filter((g) => drv?.controls?.[g.use])) out.push({ text: `\t\t${drv!.controls![g.use]} = ${gpioCell(g.pin, flagsOf(g))};`, cites: g.cites, why: gpioWhy(g) });
|
|
433
|
+
for (const g of d.controls.filter((g) => !drv?.controls?.[g.use])) userGpio(g);
|
|
434
|
+
if (!d.controls.some((g) => g.use === "dc")) findings.push({ code: "FW_DISPLAY_DC_MISSING", severity: "error", instance: d.instance, message: `${d.instance} (${d.part}) has no D/C line wired to the controller; a 4-wire SPI display needs one`, cites: d.cites });
|
|
435
|
+
// write-only is the panel's property (it drives no MISO), whatever else shares the bus
|
|
436
|
+
if (!d.roles?.includes("miso")) out.push({ text: "\t\twrite-only;", cites: [`pin:${d.instance}:${d.interface}`], why: "the panel has no MISO: it is written, never read" });
|
|
437
|
+
out.push({ text: "\t\t#address-cells = <1>;", cites: [`pin:${d.instance}:${d.interface}`] }, { text: "\t\t#size-cells = <0>;", cites: [`pin:${d.instance}:${d.interface}`] }, { text: "" });
|
|
438
|
+
if (!drv) {
|
|
439
|
+
warnings.push(`${d.instance} (${d.part}): no Zephyr display driver in this provider's table`);
|
|
440
|
+
out.push({ text: "\t};" });
|
|
441
|
+
return out;
|
|
442
|
+
}
|
|
443
|
+
drv.kconfig.forEach((k) => needs(k, [`pin:${d.instance}:${d.interface}`]));
|
|
444
|
+
const hz = Math.min(d.maxClockHz ?? NRF52840.spiMaxHz[spiInst], NRF52840.spiMaxHz[spiInst]);
|
|
445
|
+
out.push(
|
|
446
|
+
{ text: `\t\t${dtLabel(d.instance)}: ${drv.node}@${index} {` },
|
|
447
|
+
{ text: `\t\t\tcompatible = ${dtString(drv.compatible)};`, cites: [`part:${d.part}:traits.performance.controller`], why: `${perf?.controller ?? "controller"} (${d.part})` },
|
|
448
|
+
{ text: `\t\t\treg = <${index}>;`, cites: d.cs ? d.cs.cites : d.cites, why: `chip select ${index} of ${spiInst}` },
|
|
449
|
+
{ text: `\t\t\tmipi-max-frequency = <${hz}>;`, cites: [...d.cites.filter((x) => x.endsWith(".clock_freq")), `part:${controller.id}:domains.electrical.metadata.bus_limits.spim_Mbps`], why: `${hz / 1e6} MHz: the slower of the panel and ${spiInst}` },
|
|
450
|
+
{ text: "\t\t\tpixel-format = <PANEL_PIXEL_FORMAT_RGB_565>;", cites: [`part:${d.part}:traits.performance.colours`], why: "65K colours" },
|
|
451
|
+
);
|
|
452
|
+
includes.push({ text: "#include <zephyr/dt-bindings/display/panel.h>", cites: [`pin:${d.instance}:${d.interface}`] });
|
|
453
|
+
if (perf?.resolution?.length === 2) {
|
|
454
|
+
out.push({ text: `\t\t\twidth = <${perf.resolution[0]}>;`, cites: [`part:${d.part}:traits.performance.resolution`] }, { text: `\t\t\theight = <${perf.resolution[1]}>;`, cites: [`part:${d.part}:traits.performance.resolution`] });
|
|
455
|
+
} else findings.push({ code: "FW_PROPERTY_MISSING", severity: "error", instance: d.instance, message: `${d.part} states no resolution (traits.performance.resolution); the display binding requires width and height`, cites: d.cites });
|
|
456
|
+
out.push(...fromPart(d, "\t\t\t"));
|
|
457
|
+
out.push({ text: "\t\t};" }, { text: "\t};" });
|
|
458
|
+
return out;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** gpio-keys codes by button index: the digit keys, then the letter keys (zephyr/dt-bindings/input/input-event-codes.h). */
|
|
463
|
+
const BUTTON_CODES = 36;
|
|
464
|
+
export function buttonCode(i: number): string | undefined {
|
|
465
|
+
if (i < 10) return `INPUT_KEY_${i}`;
|
|
466
|
+
if (i < BUTTON_CODES) return `INPUT_KEY_${String.fromCharCode(65 + i - 10)}`;
|
|
467
|
+
return undefined;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const KCONFIG_WHY: Record<string, string> = {
|
|
471
|
+
I2C: "I2C buses",
|
|
472
|
+
SPI: "SPI buses",
|
|
473
|
+
SERIAL: "UARTs",
|
|
474
|
+
W1: "1-Wire bus",
|
|
475
|
+
INPUT: "buttons (gpio-keys)",
|
|
476
|
+
FUEL_GAUGE: "fuel gauge driver",
|
|
477
|
+
HAPTICS: "haptic driver",
|
|
478
|
+
LED: "LED controller driver",
|
|
479
|
+
DISPLAY: "display driver",
|
|
480
|
+
SENSOR: "sensor drivers",
|
|
481
|
+
USB_DEVICE_STACK_NEXT: "USB device stack",
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
function metadata(def: ModuleDef, key: string): unknown {
|
|
485
|
+
for (const d of def.domains ?? []) {
|
|
486
|
+
const m = (d as { metadata?: Record<string, unknown> }).metadata;
|
|
487
|
+
if (m && key in m) return m[key];
|
|
488
|
+
}
|
|
489
|
+
return undefined;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
export type { ZephyrSoc };
|
package/lib/drivers.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which Zephyr driver serves a part: the provider's own table, keyed by part
|
|
3
|
+
* id (tool-integrations.md §4: mapping data from part facts to a tool lives
|
|
4
|
+
* with the provider, never in the part). Every entry was checked against the
|
|
5
|
+
* binding in Zephyr v4.2.0 (`binding` names the file); `kconfig` is the
|
|
6
|
+
* subsystem the driver lives under (the driver itself defaults to y when its
|
|
7
|
+
* compatible is enabled in the devicetree).
|
|
8
|
+
*
|
|
9
|
+
* A part with no entry still gets a devicetree node on its bus, with its
|
|
10
|
+
* address and no compatible: the application finds it by label and drives it
|
|
11
|
+
* itself. The generator says so in a warning.
|
|
12
|
+
*/
|
|
13
|
+
import type { FirmwareGpioUse } from "@protoboardai/core";
|
|
14
|
+
|
|
15
|
+
export interface ZephyrDriver {
|
|
16
|
+
compatible: string;
|
|
17
|
+
/** Generic node name (devicetree spec §2.2.2). */
|
|
18
|
+
node: string;
|
|
19
|
+
/** The binding file in Zephyr v4.2.0. */
|
|
20
|
+
binding: string;
|
|
21
|
+
/** Subsystem Kconfig symbols to enable (without CONFIG_). */
|
|
22
|
+
kconfig: string[];
|
|
23
|
+
/** Device controls the binding takes as `*-gpios` properties. Others go to `zephyr,user`. */
|
|
24
|
+
controls?: Partial<Record<FirmwareGpioUse, string>>;
|
|
25
|
+
/** Properties the binding requires that the model cannot state: they come from the config's `properties`. */
|
|
26
|
+
required?: string[];
|
|
27
|
+
/** Fixed properties with the part fact (a `traits.performance` key) they come from. */
|
|
28
|
+
fromPart?: { property: string; fact: string; format: "int" | "string" }[];
|
|
29
|
+
/** Child nodes the binding needs that the generator does not write. */
|
|
30
|
+
childrenNote?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const DRIVERS: Record<string, ZephyrDriver> = {
|
|
34
|
+
"adi-max17048g-t10": {
|
|
35
|
+
compatible: "maxim,max17048",
|
|
36
|
+
node: "fuel-gauge",
|
|
37
|
+
binding: "dts/bindings/fuel-gauge/maxim,max17048.yaml",
|
|
38
|
+
kconfig: ["FUEL_GAUGE"],
|
|
39
|
+
},
|
|
40
|
+
"ti-drv2605ldgs": {
|
|
41
|
+
compatible: "ti,drv2605",
|
|
42
|
+
node: "haptics",
|
|
43
|
+
binding: "dts/bindings/haptics/ti,drv2605.yaml",
|
|
44
|
+
kconfig: ["HAPTICS"],
|
|
45
|
+
controls: { enable: "en-gpios", trigger: "in-trig-gpios" },
|
|
46
|
+
required: ["actuator-mode"],
|
|
47
|
+
},
|
|
48
|
+
"ti-lp5024rsm": {
|
|
49
|
+
compatible: "ti,lp5024",
|
|
50
|
+
node: "led-controller",
|
|
51
|
+
binding: "dts/bindings/led/ti,lp5024.yaml",
|
|
52
|
+
kconfig: ["LED"],
|
|
53
|
+
controls: { enable: "enable-gpios" },
|
|
54
|
+
childrenNote: "add one child node per LED module (label, index, color-mapping): the model does not say which colour each output drives",
|
|
55
|
+
},
|
|
56
|
+
"eastrising-er-tft128-1": {
|
|
57
|
+
compatible: "galaxycore,gc9x01x",
|
|
58
|
+
node: "gc9x01x",
|
|
59
|
+
binding: "dts/bindings/display/galaxycore,gc9x01x.yaml (on zephyr,mipi-dbi-spi)",
|
|
60
|
+
kconfig: ["DISPLAY"],
|
|
61
|
+
controls: { dc: "dc-gpios", reset: "reset-gpios" },
|
|
62
|
+
},
|
|
63
|
+
"adafruit-381-ds18b20-probe": {
|
|
64
|
+
compatible: "maxim,ds18b20",
|
|
65
|
+
node: "ds18b20",
|
|
66
|
+
binding: "dts/bindings/sensor/maxim,ds18b20.yaml (on zephyr,w1-gpio)",
|
|
67
|
+
kconfig: ["W1", "SENSOR"],
|
|
68
|
+
fromPart: [{ property: "resolution", fact: "default_resolution_bits", format: "int" }],
|
|
69
|
+
},
|
|
70
|
+
};
|