ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
pinout_render.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Render parsed RA8 pin lists into the files under ``docs/pinouts/``.
4
5Two shapes are produced. A per-variant plain-text reference carries three
6views of one ball map -- the physical grid as the package is drawn, the full
7alternate-function table per ball, and a port-name index for the common
8"which ball is P409" lookup. A Markdown index resolves any of the 64 part
9numbers to its variant file.
10
11Nothing here reads a PDF or decides a fact; it is handed parsed rows and
12formats them. Used by ``gen_pinouts.py``; see that file for the whole
13pipeline.
14"""
15
16from __future__ import annotations
17
18import sys
19import textwrap
20from pathlib import Path
21
22sys.path.insert(0, str(Path(__file__).resolve().parent))
23from pinout_model import (
24 COLUMN_ORDER,
25 FIELD_HEADINGS,
26 GROUPS,
27 MRAM_SIZES,
28 PACKAGES,
29 REPO_ROOT,
30 TEMP_GRADES,
31 Group,
32 Part,
33 ball_key,
34 port_key,
35)
36
37RULE = "-" * 80
38
39# Column widths of the per-ball function table. COMMS is by far the widest
40# field in the source (a single ball can offer a dozen alternates), so it
41# gets the slack; anything longer wraps within its own column.
42TABLE_WIDTHS = {
43 "ball": 5,
44 "port": 6,
45 "power": 12,
46 "exbus": 9,
47 "irq": 11,
48 "comms": 44,
49 "timer": 24,
50 "analog": 11,
51 "video": 18,
52}
53
54PORTS_PER_LINE = 6
55
56
57def wrap_cell(value: str, width: int) -> list[str]:
58 """Wrap a slash-separated function list to ``width``, breaking on '/'.
59
60 Alternate-function lists are slash-separated, so a break after a slash
61 reads naturally and never splits a signal name unless one name alone
62 exceeds the column -- in which case it is split rather than allowed to
63 push the table out of alignment.
64 """
65 if not value:
66 return ["-"]
67 pieces = [p + "/" for p in value.split("/")]
68 pieces[-1] = pieces[-1][:-1]
69 out: list[str] = []
70 line = ""
71 for raw_piece in pieces:
72 piece = raw_piece
73 if line and len(line) + len(piece) > width:
74 out.append(line)
75 line = ""
76 while len(piece) > width:
77 if line:
78 out.append(line)
79 line = ""
80 out.append(piece[:width])
81 piece = piece[width:]
82 line += piece
83 if line:
84 out.append(line)
85 return out
86
87
88def render_grid(rows: list[dict], column: tuple) -> list[str]:
89 """Render the physical ball grid, top view, one cell per ball."""
90 placed = {}
91 for row in rows:
92 ball = row["balls"][column]
93 if ball is not None:
94 placed[ball] = row["port"] or row["power"] or "-"
95
96 letters = sorted({b.rstrip("0123456789") for b in placed}, key=lambda r: (len(r), r))
97 numbers = sorted({int(b.lstrip("ABCDEFGHIJKLMNOPQRSTUVWXYZ")) for b in placed})
98 width = max(*(len(v) for v in placed.values()), *(len(str(n)) for n in numbers))
99 pad = max(len(letter) for letter in letters)
100
101 out = [" " * pad + " " + " ".join(str(n).center(width) for n in numbers)]
102 for letter in letters:
103 cells = [placed.get(f"{letter}{num}", "").center(width) for num in numbers]
104 out.append(f"{letter:<{pad}} " + " ".join(cells).rstrip())
105 return out
106
107
108def _variant_header(group: Group, package: str, mipi: bool, balls: int, io_pins: int) -> list[str]:
109 """The provenance block every variant file opens with."""
110 pkg = PACKAGES[package]
111 table = group.sip_table if pkg.sip else group.std_table
112 kind = "SiP" if pkg.sip else "Standard"
113 title = (
114 f"{group.name} group pinout -- LFBGA {pkg.balls}-pin, "
115 f"{'with' if mipi else 'without'} MIPI DSI/CSI"
116 )
117
118 lines = [
119 "=" * 80,
120 title,
121 "=" * 80,
122 "",
123 "GENERATED FILE -- DO NOT EDIT BY HAND.",
124 " Regenerate with: python3 scripts/gen/gen_pinouts.py",
125 "",
126 f"Source : {group.name} Group Datasheet {group.doc_id}, section 1.7",
127 f' "Pin Lists", Table {table} "Pin list for the {kind} product",',
128 f' column "BGA{pkg.balls}{"" if mipi else " without MIPI"}".',
129 "",
130 f"Package : {pkg.renesas} ({pkg.jeita})",
131 f" LFBGA {pkg.balls}-pin, {pkg.body}",
132 f"Balls : {balls}",
133 f"I/O port pins : {io_pins}",
134 f"MIPI DSI/CSI : {'available' if mipi else 'not available'}",
135 ]
136 lines += textwrap.wrap(
137 group.tagline,
138 width=64,
139 initial_indent="Group : ",
140 subsequent_indent=" ",
141 )
142 return [*lines, ""]
143
144
145def _variant_parts(parts: list[Part], package: str, mipi: bool) -> list[str]:
146 """The list of part numbers that share this ball map."""
147 matching = [p for p in parts if p.package == package and p.mipi == mipi]
148 lines = [
149 f"Part numbers using this ball map ({len(matching)}):",
150 "",
151 f" {'Part number':<16} {'Cores':<7} {'Code memory':<32} {'Junction temp':<14}".rstrip(),
152 f" {'-' * 16} {'-' * 7} {'-' * 32} {'-' * 14}",
153 ]
154 lines.extend(
155 (
156 f" {part.number:<16} {part.cores:<7} "
157 f"{MRAM_SIZES[part.mram]:<32} {TEMP_GRADES[part.temp]:<14}"
158 )
159 for part in matching
160 )
161 return [
162 *lines,
163 "",
164 "Every part number above has an identical ball map; they",
165 "differ only in core count, memory size and temperature",
166 "grade. See docs/pinouts/README.md for the full matrix.",
167 "",
168 ]
169
170
171def _function_table(rows: list[dict], column: tuple) -> list[str]:
172 """Section 2: every ball with every alternate function it offers."""
173 lines = [RULE, "2. Alternate functions per ball", RULE, ""]
174 lines += [f" {key.upper():<7}{FIELD_HEADINGS[key]}" for key in COLUMN_ORDER]
175 lines += [
176 "",
177 ' A field reads "-" when the ball offers nothing in that',
178 " category. Suffixes _A/_B/_C are the datasheet's pin-",
179 ' candidate variants; "-DS" marks a deep-standby-capable',
180 " interrupt input.",
181 "",
182 ]
183
184 header = f"{'BALL':<{TABLE_WIDTHS['ball']}}" + "".join(
185 f"{k.upper():<{TABLE_WIDTHS[k] + 1}}" for k in COLUMN_ORDER
186 )
187 lines += [header.rstrip(), "-" * len(header.rstrip())]
188
189 for row in rows:
190 cells = {k: wrap_cell(row[k], TABLE_WIDTHS[k]) for k in COLUMN_ORDER}
191 for i in range(max(len(v) for v in cells.values())):
192 ball = row["balls"][column] if i == 0 else ""
193 text = f"{ball:<{TABLE_WIDTHS['ball']}}"
194 for key in COLUMN_ORDER:
195 piece = cells[key][i] if i < len(cells[key]) else ""
196 text += f"{piece:<{TABLE_WIDTHS[key] + 1}}"
197 lines.append(text.rstrip())
198 return [*lines, ""]
199
200
201def _port_index(io_pins: list[dict], column: tuple) -> list[str]:
202 """Section 3: port name -> ball, the lookup people actually run."""
203 lines = [
204 RULE,
205 "3. I/O port pin index",
206 RULE,
207 "",
208 f"{len(io_pins)} port pins, in port order.",
209 "",
210 ]
211 by_port = sorted(io_pins, key=lambda r: port_key(r["port"]))
212 for i in range(0, len(by_port), PORTS_PER_LINE):
213 chunk = by_port[i : i + PORTS_PER_LINE]
214 lines.append(
215 " " + " ".join(f"{r['port']:<5} {r['balls'][column]:<4}" for r in chunk).rstrip()
216 )
217 return [*lines, ""]
218
219
220def render_variant(
221 group: Group, package: str, mipi: bool, rows: list[dict], parts: list[Part]
222) -> str:
223 """Render one variant's whole reference file."""
224 column = (package, mipi)
225 mine = sorted(
226 (r for r in rows if r["balls"][column] is not None),
227 key=lambda r: ball_key(r["balls"][column]),
228 )
229 io_pins = [r for r in mine if r["port"]]
230
231 lines = _variant_header(group, package, mipi, len(mine), len(io_pins))
232 lines += _variant_parts(parts, package, mipi)
233 lines += [
234 RULE,
235 "1. Ball grid (top view)",
236 RULE,
237 "",
238 "Each cell names the I/O port pin, or the power/system",
239 "function for balls that are not port pins. Blank = no ball.",
240 "",
241 ]
242 lines += render_grid(rows, column)
243 lines.append("")
244 lines += _function_table(mine, column)
245 lines += _port_index(io_pins, column)
246 return "\n".join(lines) + "\n"
247
248
249def _index_intro() -> list[str]:
250 return [
251 "# RA8 pinout reference",
252 "",
253 "Ball maps for every orderable RA8D2 and RA8P1 part number, parsed",
254 'out of section 1.7 "Pin Lists" of the two group datasheets by',
255 "`scripts/gen/gen_pinouts.py`. **These files are generated -- edit the",
256 "generator, not the output.** `gen_pinouts.py --check` runs in CI, so",
257 "a datasheet revision that moves a ball cannot land without the",
258 "reference moving with it.",
259 "",
260 "## Which file do I want?",
261 "",
262 "A part number's ball map is fixed by exactly two of its fields: the",
263 "**package** and whether its **feature set** bonds out MIPI DSI/CSI",
264 "(`B` and `K` do; `A` and `J` do not). Memory size, core count and",
265 "temperature grade never move a ball, so the 64 part numbers below",
266 "collapse onto 12 ball maps.",
267 "",
268 ]
269
270
271def _index_variant_table(variants: list) -> list[str]:
272 lines = [
273 "| Group | Package | MIPI DSI/CSI | Balls | I/O | Pinout file |",
274 "|---|---|---|---|---|---|",
275 ]
276 for group, package, mipi, filename, balls, io_pins, _ in variants:
277 pkg = PACKAGES[package]
278 lines.append(
279 f"| {group} | LFBGA {pkg.balls}{' (SiP)' if pkg.sip else ''} | "
280 f"{'yes' if mipi else 'no'} | {balls} | {io_pins} | "
281 f"[`{filename}`]({filename}) |"
282 )
283 return [*lines, ""]
284
285
286def _index_part_table(parts: list) -> list[str]:
287 lines = [
288 "## Part number -> ball map",
289 "",
290 "Decoded from the part-numbering scheme (Figure 1.2 of either",
291 "datasheet), cross-checked against the printed product list.",
292 "",
293 "| Part number | Group | Cores | MIPI | Code memory | SRAM[^sram] "
294 "| Junction temp | Package | Pinout file |",
295 "|---|---|---|---|---|---|---|---|---|",
296 ]
297 for part, sram, filename in parts:
298 pkg = PACKAGES[part.package]
299 lines.append(
300 f"| `{part.number}` | {part.group} | {part.cores} | "
301 f"{'yes' if part.mipi else 'no'} | {MRAM_SIZES[part.mram]} | "
302 f"{sram} | {TEMP_GRADES[part.temp]} | "
303 f"LFBGA {pkg.balls}{' SiP' if pkg.sip else ''} | "
304 f"[`{filename}`]({filename}) |"
305 )
306 return [
307 *lines,
308 "",
309 "[^sram]: SRAM is the one column here that is not pinout data and not read",
310 " per-part: the Function Comparison table merges it across columns, giving",
311 " 1792 KB for the single-core feature sets (`A`, `B`) and 1664 KB for the",
312 " dual-core ones (`J`, `K`), the latter spending 128 KB on the CM33 TCM.",
313 " Every SiP part is dual-core. Take it as orientation and confirm against",
314 " the datasheet before sizing anything against it.",
315 "",
316 ]
317
318
319def _index_scheme() -> list[str]:
320 return [
321 "## Reading a part number",
322 "",
323 "```",
324 "R 7 K A 8 D 2 A D L C AB",
325 " | | | | | | +-- package: AB=LFBGA224 AC=LFBGA289 AJ=LFBGA303",
326 " | | | | | +----- quality grade: C=standard S=SiP",
327 " | | | | +------- junction temp: L=0..95C D=-40..105C",
328 " | | | +--------- code memory: D=512KB F=1MB R=1MB+4MB S=1MB+8MB",
329 " | | +----------- feature set: A/B single core, J/K dual core;",
330 " | | B/K bond out MIPI DSI/CSI, A/J do not",
331 " +-+------------- group: D2=RA8D2, P1=RA8P1",
332 "```",
333 "",
334 "The leading `R7K`/`R7J` also encodes the memory technology (`K`=MRAM,",
335 "`J`=MRAM+flash SiP) and so tracks the quality-grade and package",
336 "fields; the generator rejects a part number where the three",
337 "disagree.",
338 "",
339 ]
340
341
342def _index_sources() -> list[str]:
343 lines = ["## Sources", "", "| Group | Datasheet | Committed as |", "|---|---|---|"]
344 lines.extend(
345 f"| {group.name} | {group.doc_id} | `{group.pdf.relative_to(REPO_ROOT)}` |"
346 for group in GROUPS
347 )
348 return [
349 *lines,
350 "",
351 "The Hardware User's Manual, not the datasheet, is the authority on",
352 "*register* programming for any of these pins -- see",
353 "`docs/reference/README.md`. The datasheet is the authority on which",
354 "ball carries which function, which is what these files record.",
355 "",
356 ]
357
358
359def render_index(data: dict) -> str:
360 """Render `docs/pinouts/README.md`, the part-number -> file resolver."""
361 lines = _index_intro()
362 lines += _index_variant_table(data["variants"])
363 lines += _index_part_table(data["parts"])
364 lines += _index_scheme()
365 lines += ["## Pin compatibility between the two groups", ""]
366 lines += data["compat"]
367 lines.append("")
368 lines += _index_sources()
369 return "\n".join(lines) + "\n"