ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
gen_pinouts.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Generate the committed per-package pinout references under ``docs/pinouts/``.
4
5Every orderable RA8D2 and RA8P1 part number is a point in a four-axis
6product matrix -- feature set, code-MRAM size, junction-temperature grade
7and package -- but only two of those axes move a ball: the **package**
8(LFBGA 224 / 289 / 303) and whether the **feature set** bonds out the MIPI
9DSI/CSI pins (``B`` and ``K`` do, ``A`` and ``J`` do not). The 32 part
10numbers per group therefore collapse onto six distinct ball maps, and this
11script emits one file per map plus an index that resolves any part number
12to its map.
13
14The ball maps are not transcribed; they are parsed out of the primary
15source, section 1.7 "Pin Lists" of each group datasheet:
16
17* RA8D2 -- ``docs/reference/ra8d2-datasheet.pdf`` (R01DS0493EJ), Table 1.16
18 (Standard product) and Table 1.17 (SiP product).
19* RA8P1 -- ``docs/reference/ra8p1-datasheet.pdf`` (R01DS0439EJ), Table 1.17
20 (Standard product) and Table 1.18 (SiP product).
21
22Each of those tables is one row per *signal position* with a leading column
23per package variant giving that variant's ball coordinate, or an em dash
24where the variant does not bond the position out. Selecting a variant is
25therefore selecting one leading column and dropping its dashed rows.
26
27The parse is not trusted on its own. ``validate_variant()`` holds every
28variant to four independent facts the datasheet states elsewhere: the ball
29count its package name implies, the I/O-port count the "Function Comparison"
30table prints, no ball claimed twice, and -- the load-bearing one -- the exact
31port-pin set drawn by that variant's section 1.6 ball-grid FIGURE, which is a
32wholly separate rendering of the same information. A mis-sliced column fails
33the run instead of quietly emitting a thinner table.
34
35The work is split across sibling modules: ``pinout_model`` (the product
36matrix and table shape), ``pinout_parse`` (PDF -> rows), ``pinout_render``
37(rows -> files) and ``pinout_selftest``. This file owns validation, the
38group-vs-group diff, and the CLI.
39
40Usage::
41
42 python3 scripts/gen/gen_pinouts.py # write docs/pinouts/
43 python3 scripts/gen/gen_pinouts.py --check # fail if they are stale
44 python3 scripts/gen/gen_pinouts.py --selftest # assert both directions
45
46``--check`` is what the ``pinout-freshness`` gate runs, so a datasheet
47revision that moves a ball cannot land without the reference moving with it.
48"""
49
50from __future__ import annotations
51
52import argparse
53import sys
54from pathlib import Path
55
56sys.path.insert(0, str(Path(__file__).resolve().parent))
57import pinout_selftest
58from pinout_model import (
59 BALL_COLUMNS,
60 EXPECTED_IO_PORTS,
61 FIELDS,
62 GROUPS,
63 OUT_DIR,
64 PACKAGES,
65 REPO_ROOT,
66 Group,
67 ParseError,
68 sram_for,
69 variant_slug,
70)
71from pinout_parse import (
72 extract_parts,
73 figure_port_sets,
74 parse_pin_list,
75 pdf_text,
76)
77from pinout_render import render_index, render_variant
78
79PARTS_PER_GROUP = 32
80
81
82def validate_variant(
83 group: Group, column: tuple, rows: list[dict], figures: dict
84) -> tuple[list[dict], int]:
85 """Hold one variant to every count the datasheet states elsewhere.
86
87 Returns its balls and I/O-port count. Raises rather than returning a
88 verdict: a generator that emitted a table it could not corroborate would
89 be worse than one that emitted nothing, because the output looks the
90 same either way.
91 """
92 package, mipi = column
93 where = f"{group.name} {package} mipi={mipi}"
94 mine = [r for r in rows if r["balls"][column] is not None]
95
96 if len({r["balls"][column] for r in mine}) != len(mine):
97 msg = f"{where}: the same ball appears twice"
98 raise ParseError(msg)
99
100 if len(mine) != PACKAGES[package].balls:
101 msg = f"{where}: parsed {len(mine)} balls, the package has {PACKAGES[package].balls}"
102 raise ParseError(msg)
103
104 ports = {r["port"] for r in mine if r["port"]}
105 if len(ports) != EXPECTED_IO_PORTS[column]:
106 msg = (
107 f"{where}: parsed {len(ports)} I/O port pins, the datasheet's "
108 f"Function Comparison table says {EXPECTED_IO_PORTS[column]}"
109 )
110 raise ParseError(msg)
111
112 drawn = figures[column]
113 if ports != drawn:
114 msg = (
115 f"{where}: the section 1.7 pin list and the section 1.6 "
116 f"ball-grid figure disagree -- only in the list "
117 f"{sorted(ports - drawn)}, only in the figure "
118 f"{sorted(drawn - ports)}"
119 )
120 raise ParseError(msg)
121 return mine, len(ports)
122
123
124def build_group(group: Group) -> tuple[dict, list, list, dict]:
125 """Parse, validate and render one MCU group."""
126 text = pdf_text(group.pdf)
127 parts = extract_parts(text, group)
128 if len(parts) != PARTS_PER_GROUP:
129 msg = f"{group.name}: expected {PARTS_PER_GROUP} part numbers, found {len(parts)}"
130 raise ParseError(msg)
131 figures = figure_port_sets(text)
132 by_kind = {kind: parse_pin_list(text, group, kind) for kind in BALL_COLUMNS}
133
134 files: dict[str, str] = {}
135 variants = []
136 for kind, rows in by_kind.items():
137 for column in BALL_COLUMNS[kind]:
138 package, mipi = column
139 mine, io_pins = validate_variant(group, column, rows, figures)
140 filename = f"{group.slug}_{variant_slug(package, mipi)}.txt"
141 files[filename] = render_variant(group, package, mipi, rows, parts)
142 variants.append((group.name, package, mipi, filename, len(mine), io_pins, kind))
143
144 index = [
145 (part, sram_for(part), f"{group.slug}_{variant_slug(part.package, part.mipi)}.txt")
146 for part in parts
147 ]
148 return files, variants, index, by_kind
149
150
151def build() -> dict:
152 """Parse both datasheets and render every output file in memory."""
153 files: dict[str, str] = {}
154 variants: list = []
155 parts_index: list = []
156 per_group_rows: dict = {}
157
158 for group in GROUPS:
159 group_files, group_variants, group_parts, rows = build_group(group)
160 files.update(group_files)
161 variants += group_variants
162 parts_index += group_parts
163 per_group_rows[group.name] = rows
164
165 files["README.md"] = render_index(
166 {
167 "variants": variants,
168 "parts": parts_index,
169 "compat": compare_groups(per_group_rows),
170 }
171 )
172 return {name: scrub(body) for name, body in files.items()}
173
174
175def _signature(rows: list[dict]) -> dict:
176 """Map every (variant, ball) to the function set printed against it."""
177 out = {}
178 for row in rows:
179 for column, ball in row["balls"].items():
180 if ball is not None:
181 out[(column, ball)] = tuple(row[f] for f in FIELDS)
182 return out
183
184
185def compare_groups(per_group_rows: dict) -> list[str]:
186 """Diff the two groups' pin lists and describe the result.
187
188 "The RA8P1 is pin-compatible with the RA8D2" was an assertion carried in
189 prose. Diffing the two parsed pin lists turns it into a measurement that
190 is re-taken on every run, so a future revision that breaks it says so.
191 """
192 a, b = GROUPS[0], GROUPS[1]
193 notes = []
194 for kind in BALL_COLUMNS:
195 sig_a = _signature(per_group_rows[a.name][kind])
196 sig_b = _signature(per_group_rows[b.name][kind])
197 only_a = sorted(set(sig_a) - set(sig_b))
198 only_b = sorted(set(sig_b) - set(sig_a))
199 differing = sorted(k for k in set(sig_a) & set(sig_b) if sig_a[k] != sig_b[k])
200 label = "Standard" if kind == "standard" else "SiP"
201
202 if not (only_a or only_b or differing):
203 notes.append(
204 f"- **{label} products: identical.** Every ball of every "
205 f"{label} package carries the same function set on the "
206 f"{a.name} and the {b.name} -- {len(sig_a)} (variant, ball) "
207 f"pairs compared, established by diffing the two parsed pin "
208 f"lists rather than by assertion."
209 )
210 continue
211
212 notes.append(
213 f"- **{label} products: {len(differing)} ball(s) differ**, "
214 f"{len(only_a)} only on the {a.name}, {len(only_b)} only on "
215 f"the {b.name}."
216 )
217 for column, ball in (differing + only_a + only_b)[:20]:
218 notes.append(
219 f" - `{ball}` (LFBGA {PACKAGES[column[0]].balls}"
220 f"{'' if column[1] else ' without MIPI'})"
221 )
222
223 return [
224 *notes,
225 "",
226 "The one function the two groups do not share (the RA8P1's Ethos-U55",
227 "NPU) is not pinned out, so pin compatibility is what the diff above",
228 "shows. See `docs/reference/ra8p1_vs_ra8d2.md` for the register-level delta.",
229 ]
230
231
232def scrub(content: str) -> str:
233 """Strip trailing whitespace; the format gates reject it repo-wide."""
234 return "\n".join(line.rstrip() for line in content.split("\n"))
235
236
237def write(files: dict[str, str]) -> None:
238 """Write every rendered file, removing any output that is no longer ours."""
239 OUT_DIR.mkdir(parents=True, exist_ok=True)
240 for name, content in sorted(files.items()):
241 (OUT_DIR / name).write_text(content, encoding="ascii")
242 for stale in sorted(OUT_DIR.iterdir()):
243 if stale.name not in files:
244 stale.unlink()
245
246
247def check(files: dict[str, str]) -> int:
248 """Fail if the committed files are not what a fresh parse produces."""
249 problems = []
250 for name, content in sorted(files.items()):
251 path = OUT_DIR / name
252 if not path.is_file():
253 problems.append(f"missing: {path.relative_to(REPO_ROOT)}")
254 elif path.read_text(encoding="utf-8") != content:
255 problems.append(f"stale: {path.relative_to(REPO_ROOT)}")
256 if OUT_DIR.is_dir():
257 problems.extend(
258 f"unexpected: {extra.relative_to(REPO_ROOT)}"
259 for extra in sorted(OUT_DIR.iterdir())
260 if extra.name not in files
261 )
262
263 for problem in problems:
264 print(f"gen_pinouts: {problem}", file=sys.stderr)
265 if problems:
266 print(
267 "gen_pinouts: run 'python3 scripts/gen/gen_pinouts.py' and commit the result",
268 file=sys.stderr,
269 )
270 return 1
271 print(f"gen_pinouts: {len(files)} file(s) up to date")
272 return 0
273
274
275def main() -> int:
276 """Run the self-test, freshness check, or generator requested by the CLI."""
277 parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
278 parser.add_argument(
279 "--check", action="store_true", help="fail if the committed files are stale"
280 )
281 parser.add_argument(
282 "--selftest", action="store_true", help="assert the parser fires and stays quiet"
283 )
284 args = parser.parse_args()
285
286 if args.selftest:
287 return pinout_selftest.run()
288
289 try:
290 files = build()
291 except ParseError as exc:
292 print(f"gen_pinouts: {exc}", file=sys.stderr)
293 return 1
294
295 if args.check:
296 return check(files)
297 write(files)
298 print(f"gen_pinouts: wrote {len(files)} file(s) to {OUT_DIR.relative_to(REPO_ROOT)}/")
299 return 0
300
301
302if __name__ == "__main__":
303 sys.exit(main())
-copyright
-proof
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298