ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
pinout_model.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Product matrix and pin-list table shape for the RA8D2 and RA8P1 groups.
4
5Everything here is transcribed from the two datasheets' own front matter --
6the part-numbering scheme figure, the product list, the package dimensions
7appendix and the Function Comparison table -- and nothing here parses. It is
8the vocabulary the parser and the renderer share, kept in one file so a
9product-matrix fact has exactly one home.
10
11Used by ``gen_pinouts.py``; see that file for the whole pipeline.
12"""
13
14from __future__ import annotations
15
16import re
17from dataclasses import dataclass
18from pathlib import Path
19
20REPO_ROOT = Path(__file__).resolve().parents[2]
21OUT_DIR = REPO_ROOT / "docs" / "pinouts"
22
23# The em dash the datasheet prints for "this variant has no such pin".
24# Spelled as an escape so this file stays 7-bit ASCII.
25DASH = "\u2014"
26
27
28class ParseError(RuntimeError):
29 """The datasheet did not have the shape this parser requires."""
30
31
32# ---------------------------------------------------------------------------
33# Product matrix, from the "Part numbering scheme" figure of each datasheet
34# (RA8D2 Figure 1.2 p 9; RA8P1 Figure 1.2 p 9 -- the two figures are
35# character-for-character identical apart from the group digits).
36# ---------------------------------------------------------------------------
37
38FEATURE_SETS = {
39 "A": ("single", False), # Single core (CM85 only), no MIPI DSI/CSI
40 "B": ("single", True), # Single core (CM85 only), MIPI DSI/CSI
41 "J": ("dual", False), # Dual core, no MIPI DSI/CSI
42 "K": ("dual", True), # Dual core, MIPI DSI/CSI
43}
44
45MRAM_SIZES = {
46 "D": "512 KB MRAM",
47 "F": "1 MB MRAM",
48 "R": "5 MB (1 MB MRAM + 4 MB flash)",
49 "S": "9 MB (1 MB MRAM + 8 MB flash)",
50}
51
52TEMP_GRADES = {
53 "L": "0 to 95 C",
54 "D": "-40 to 105 C",
55}
56
57
58@dataclass(frozen=True)
59class Package:
60 """One package option from the part-number scheme."""
61
62 code: str # part-number package field, e.g. "AC"
63 balls: int # ball count
64 renesas: str # Renesas package code, e.g. "PLBG0289JA-A"
65 jeita: str # JEITA outline, e.g. "P-LFBGA289-12x12-0.65"
66 body: str # human-readable body/pitch summary
67 sip: bool # True for the MRAM+flash SiP package
68
69
70PACKAGES = {
71 "AB": Package(
72 "AB",
73 224,
74 "PLBG0224JA-A",
75 "P-LFBGA224-11x11-0.65",
76 "11 mm x 11 mm, 0.65 mm pitch",
77 sip=False,
78 ),
79 "AC": Package(
80 "AC",
81 289,
82 "PLBG0289JA-A",
83 "P-LFBGA289-12x12-0.65",
84 "12 mm x 12 mm, 0.65 mm pitch",
85 sip=False,
86 ),
87 "AJ": Package(
88 "AJ",
89 303,
90 "PLBG0303GA-A",
91 "P-LFBGA303-15x15-0.80",
92 "15 mm x 15 mm, 0.80 mm pitch",
93 sip=True,
94 ),
95}
96
97
98@dataclass(frozen=True)
99class Group:
100 """One MCU group and the datasheet that defines its pin lists."""
101
102 name: str # "RA8D2"
103 slug: str # "ra8d2"
104 pdf: Path # committed datasheet
105 doc_id: str # "R01DS0493EJ"
106 std_table: str # table number of the Standard-product pin list
107 sip_table: str # table number of the SiP-product pin list
108 tagline: str # one-line group description
109
110
111GROUPS = (
112 Group(
113 name="RA8D2",
114 slug="ra8d2",
115 pdf=REPO_ROOT / "docs" / "reference" / "ra8d2-datasheet.pdf",
116 doc_id="R01DS0493EJ",
117 std_table="1.16",
118 sip_table="1.17",
119 tagline="Arm Cortex-M85 @ 1 GHz (+ Cortex-M33 @ 250 MHz on dual-core "
120 "feature sets), graphics and Ethernet MCU",
121 ),
122 Group(
123 name="RA8P1",
124 slug="ra8p1",
125 pdf=REPO_ROOT / "docs" / "reference" / "ra8p1-datasheet.pdf",
126 doc_id="R01DS0439EJ",
127 std_table="1.17",
128 sip_table="1.18",
129 tagline="RA8D2 plus an Arm Ethos-U55 NPU; pin-compatible with the RA8D2 in every package",
130 ),
131)
132
133# ---------------------------------------------------------------------------
134# Pin-list table shape
135
136# Field names for the trailing (non-ball) columns, in printed order. Both
137# the Standard and the SiP table carry the same eight; they differ only in
138# how many leading ball columns precede them.
139FIELDS = ("power", "port", "exbus", "irq", "comms", "timer", "analog", "video")
140
141FIELD_HEADINGS = {
142 "port": "I/O port",
143 "power": "Power, system, clock, debug, CAC",
144 "exbus": "External bus / SDRAM",
145 "irq": "External interrupt",
146 "comms": "SCI/IIC/I3C/SPI/CANFD/USBFS/USBHS/OSPI/SSIE/SDHI/MMC/ESWM(GMII,RGMII,MII,RMII)/PDMIF",
147 "timer": "GPT/AGT/ULPT/RTC",
148 "analog": "ADC16H/DAC12/ACMPHS",
149 "video": "MIPI/GLCDC/CEU",
150}
151
152# The order the columns are printed in, which is not the order the
153# datasheet's raw cells arrive in (it prints the port name second).
154COLUMN_ORDER = ("port", "power", "exbus", "irq", "comms", "timer", "analog", "video")
155
156# Leading ball columns of each table, in printed order, as (package code,
157# has-MIPI). The datasheet prints "<pkg>" then "<pkg> without MIPI".
158BALL_COLUMNS = {
159 "standard": (("AC", True), ("AC", False), ("AB", True), ("AB", False)),
160 "sip": (("AJ", True), ("AJ", False)),
161}
162
163# Section 1.6 prints the same information a second way, as one ball-grid
164# figure per variant. Its caption sits BELOW the grid, so a figure's text
165# runs from the previous caption to its own.
166FIGURE_RE = re.compile(r"^\s*Figure 1\.[3-8]\s+Pin assignment for (.+?)\s*$")
167
168# Figure caption -> the variant it draws.
169FIGURE_VARIANTS = {
170 "BGA 289-pin": ("AC", True),
171 "without_MIPI_BGA 289-pin": ("AC", False),
172 "BGA 224-pin": ("AB", True),
173 "without_MIPI_BGA 224-pin": ("AB", False),
174 "BGA 303-pin": ("AJ", True),
175 "without_MIPI_BGA 303-pin": ("AJ", False),
176}
177
178PORT_RE = re.compile(r"\bP(?:[0-9A-D])\d{2}\b")
179
180# I/O port counts per variant, from the "Function Comparison" table
181# (RA8D2 Table 1.14 p 11; RA8P1 Table 1.15 p 11). Used as a parse floor:
182# a run that recovers a different count has mis-parsed the pin list.
183EXPECTED_IO_PORTS = {
184 ("AB", False): 149,
185 ("AB", True): 142,
186 ("AC", False): 208,
187 ("AC", True): 199,
188 ("AJ", False): 195,
189 ("AJ", True): 186,
190}
191
192TABLE_RE = re.compile(
193 r"^Table\s+(\d+\.\d+)\s+Pin list for the (Standard|SiP) product\s+"
194 r"\‍((\d+) of (\d+)\‍)\s*$"
195)
196
197BALL_RE = re.compile(r"^[A-Z]{1,2}\d{1,2}$")
198ROW_START_RE = re.compile(r"^(?:[A-Z]{1,2}\d{1,2}|" + DASH + r")(?:\s|$)")
199
200
201@dataclass(frozen=True)
202class Part:
203 """One orderable part number, decoded from its own characters."""
204
205 number: str
206 group: str
207 feature: str
208 mram: str
209 temp: str
210 quality: str
211 package: str
212
213 @property
214 def cores(self) -> str:
215 """Return the core-count class encoded by this part's feature set."""
216 return FEATURE_SETS[self.feature][0]
217
218 @property
219 def mipi(self) -> bool:
220 """Return whether this part bonds out the MIPI DSI/CSI pins."""
221 return FEATURE_SETS[self.feature][1]
222
223
224def decode_part(number: str) -> Part:
225 """Decode a part number per the datasheet part-numbering scheme."""
226 match = re.fullmatch(
227 r"R7(?P<mem>[KJ])A8(?P<grp>D2|P1)(?P<feat>[ABJK])(?P<mram>[DFRS])"
228 r"(?P<temp>[LD])(?P<qual>[CS])(?P<pkg>A[BCJ])",
229 number,
230 )
231 if not match:
232 msg = f"unparsable part number: {number}"
233 raise ParseError(msg)
234 part = Part(
235 number=number,
236 group=f"RA8{match['grp']}",
237 feature=match["feat"],
238 mram=match["mram"],
239 temp=match["temp"],
240 quality=match["qual"],
241 package=match["pkg"],
242 )
243 # The leading memory letter and the quality grade encode the same fact
244 # from two directions; disagreement means a misread part number.
245 sip = PACKAGES[part.package].sip
246 if (match["mem"] == "J") != sip or (part.quality == "S") != sip:
247 msg = f"inconsistent SiP encoding in {number}"
248 raise ParseError(msg)
249 return part
250
251
252def variant_slug(package: str, mipi: bool) -> str:
253 """Return the stable generated-file slug for one package variant."""
254 pkg = PACKAGES[package]
255 kind = "mipi" if mipi else "nomipi"
256 tail = "_sip" if pkg.sip else ""
257 return f"bga{pkg.balls}{tail}_{kind}"
258
259
260def ball_key(ball: str) -> tuple:
261 """Sort key putting balls in printed order: row letter, then number."""
262 match = re.fullmatch(r"([A-Z]{1,2})(\d{1,2})", ball)
263 row, num = match.group(1), int(match.group(2))
264 return (len(row), row, num)
265
266
267def port_key(port: str) -> tuple:
268 """Return a numeric sort key for a port name, after known port names."""
269 match = re.fullmatch(r"P([0-9A-F])(\d{2})", port)
270 if not match:
271 return (1, port, 0)
272 return (0, match.group(1), int(match.group(2)))
273
274
275def sram_for(part: Part) -> str:
276 """SRAM size per the Function Comparison table: dual core costs 128 KB."""
277 return "1664 KB" if part.cores == "dual" else "1792 KB"