3"""Generate the committed per-package pinout references under ``docs/pinouts/``.
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
14The ball maps are not transcribed; they are parsed out of the primary
15source, section 1.7 "Pin Lists" of each group datasheet:
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).
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.
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.
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.
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
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.
50from __future__
import annotations
54from pathlib
import Path
56sys.path.insert(0, str(Path(__file__).resolve().parent))
58from pinout_model
import (
71from pinout_parse
import (
77from pinout_render
import render_index, render_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.
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
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]
96 if len({r[
"balls"][column]
for r
in mine}) != len(mine):
97 msg = f
"{where}: the same ball appears twice"
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)
104 ports = {r[
"port"]
for r
in mine
if r[
"port"]}
105 if len(ports) != EXPECTED_IO_PORTS[column]:
107 f
"{where}: parsed {len(ports)} I/O port pins, the datasheet's "
108 f
"Function Comparison table says {EXPECTED_IO_PORTS[column]}"
110 raise ParseError(msg)
112 drawn = figures[column]
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)}"
120 raise ParseError(msg)
121 return mine, len(ports)
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}
134 files: dict[str, str] = {}
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))
145 (part, sram_for(part), f
"{group.slug}_{variant_slug(part.package, part.mipi)}.txt")
148 return files, variants, index, by_kind
152 """Parse both datasheets and render every output file in memory."""
153 files: dict[str, str] = {}
155 parts_index: list = []
156 per_group_rows: dict = {}
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
165 files[
"README.md"] = render_index(
167 "variants": variants,
168 "parts": parts_index,
169 "compat": compare_groups(per_group_rows),
172 return {name: scrub(body)
for name, body
in files.items()}
175def _signature(rows: list[dict]) -> dict:
176 """Map every (variant, ball) to the function set printed against it."""
179 for column, ball
in row[
"balls"].items():
181 out[(column, ball)] = tuple(row[f]
for f
in FIELDS)
185def compare_groups(per_group_rows: dict) -> list[str]:
186 """Diff the two groups' pin lists and describe the result.
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.
192 a, b = GROUPS[0], GROUPS[1]
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"
202 if not (only_a
or only_b
or differing):
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."
213 f
"- **{label} products: {len(differing)} ball(s) differ**, "
214 f
"{len(only_a)} only on the {a.name}, {len(only_b)} only on "
217 for column, ball
in (differing + only_a + only_b)[:20]:
219 f
" - `{ball}` (LFBGA {PACKAGES[column[0]].balls}"
220 f
"{'' if column[1] else ' without MIPI'})"
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.",
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"))
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:
247def check(files: dict[str, str]) -> int:
248 """Fail if the committed files are not what a fresh parse produces."""
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)}")
258 f
"unexpected: {extra.relative_to(REPO_ROOT)}"
259 for extra
in sorted(OUT_DIR.iterdir())
260 if extra.name
not in files
263 for problem
in problems:
264 print(f
"gen_pinouts: {problem}", file=sys.stderr)
267 "gen_pinouts: run 'python3 scripts/gen/gen_pinouts.py' and commit the result",
271 print(f
"gen_pinouts: {len(files)} file(s) up to date")
276 """Run the self-test, freshness check, or generator requested by the CLI."""
277 parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
279 "--check", action=
"store_true", help=
"fail if the committed files are stale"
282 "--selftest", action=
"store_true", help=
"assert the parser fires and stays quiet"
284 args = parser.parse_args()
287 return pinout_selftest.run()
291 except ParseError
as exc:
292 print(f
"gen_pinouts: {exc}", file=sys.stderr)
298 print(f
"gen_pinouts: wrote {len(files)} file(s) to {OUT_DIR.relative_to(REPO_ROOT)}/")
302if __name__ ==
"__main__":
void main(void)
The application entry point Reset_Handler hands control to.