consortium_data/chipmap.rs
1// Copyright 2026 Ethan Wu
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//
15// SPDX-License-Identifier: Apache-2.0
16
17//! Per-core peripheral and interrupt maps, embedded at compile time from the
18//! generated chip dumps (`database/chip/*.json`).
19//!
20//! Each JSON file is a single core's view of a chip: the peripheral MMIO bases
21//! and the NVIC interrupt numbers as extracted from the vendor CMSIS headers.
22//! Because there is one file per core, the set of peripherals in a core's file
23//! *is* the set that core can reach — this is the filter used when a
24//! `[peripheral.<name>]` is assigned to a coprocessor.
25
26use std::collections::HashMap;
27
28use crate::error::DataError;
29
30/// A chip dump bundled into the binary, paired with the family prefix and core
31/// name that select it.
32struct Embedded {
33 /// Lowercase part-number prefix that selects this family (e.g. `stm32mp25`).
34 family: &'static str,
35 /// Lowercase core name this dump describes (e.g. `cm33`, `cm7`, `cm0p`).
36 core: &'static str,
37 /// Whether the chip PAC's shipped `Interrupt` table is generated from *this*
38 /// core's dump (see `scripts/pac/gen_interrupts.py`). A PAC carries one
39 /// table per chip, so only one core per family is covered; code generation
40 /// must not spell `Interrupt::<name>` for any other core, because the
41 /// variant exists but carries the covered core's number.
42 pac: bool,
43 /// The raw JSON dump.
44 json: &'static str,
45}
46
47/// All bundled per-core chip dumps.
48///
49/// Ordered most-specific family prefix first, mirroring the chip-family
50/// database resolution.
51const CHIPS: &[Embedded] = &[
52 Embedded {
53 family: "stm32mp25",
54 core: "cm33",
55 pac: true,
56 json: include_str!("../database/chip/stm32mp25_cm33.json"),
57 },
58 Embedded {
59 family: "stm32mp25",
60 core: "cm0p",
61 // The CM0+ has its own, differently numbered NVIC table, and the PAC
62 // ships the CM33 one.
63 pac: false,
64 json: include_str!("../database/chip/stm32mp25_cm0p.json"),
65 },
66 Embedded {
67 family: "stm32mp23",
68 core: "cm33",
69 // `stm32mp23x` selects the PAC's `stm32mp257` table, whose CM33 lines
70 // are numbered identically.
71 pac: true,
72 json: include_str!("../database/chip/stm32mp23_cm33.json"),
73 },
74 Embedded {
75 family: "stm32mp21",
76 core: "cm33",
77 pac: true,
78 json: include_str!("../database/chip/stm32mp21_cm33.json"),
79 },
80 Embedded {
81 family: "mimx95",
82 core: "cm7",
83 pac: true,
84 json: include_str!("../database/chip/mimx95_cm7.json"),
85 },
86 Embedded {
87 family: "mimx93",
88 core: "cm33",
89 pac: true,
90 json: include_str!("../database/chip/mimx93_cm33.json"),
91 },
92];
93
94/// Raw JSON shape of a chip dump (only the fields we consume).
95#[derive(serde::Deserialize)]
96struct RawChip {
97 irqs: Vec<RawIrq>,
98 peripherals: Vec<RawPeripheral>,
99}
100
101#[derive(serde::Deserialize)]
102struct RawIrq {
103 name: String,
104 value: u16,
105}
106
107#[derive(serde::Deserialize)]
108struct RawPeripheral {
109 name: String,
110 base_address: String,
111 block: String,
112}
113
114/// A resolved peripheral: its MMIO base and register-block reference.
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct PeripheralEntry {
117 /// Instance name exactly as the dump spells it, e.g. `LPI2C1`. Lookups are
118 /// case-insensitive, so this is what code generation must use when it needs
119 /// to name the matching HAL peripheral singleton.
120 pub name: String,
121 /// Physical base address as seen by the owning core.
122 pub base: u32,
123 /// Register-block reference from the dump, e.g. `lpi2c/LPI2C` or `gpio/GPIO`.
124 pub block: String,
125}
126
127impl PeripheralEntry {
128 /// The block *kind* — the part before the `/`, lowercased (e.g. `lpi2c`).
129 pub fn kind(&self) -> &str {
130 self.block.split('/').next().unwrap_or(&self.block)
131 }
132}
133
134/// A resolved interrupt: its NVIC number and canonical name.
135#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct IrqEntry {
137 /// External interrupt number (exception number minus 16).
138 pub number: u16,
139 /// Canonical interrupt name from the dump (e.g. `LPI2C1`). The PAC's
140 /// `Interrupt` variants are generated from these same names, so this is what
141 /// a `#[interrupt] fn <name>()` vector is called.
142 pub name: String,
143}
144
145/// A single core's peripheral and interrupt map.
146#[derive(Debug, Clone)]
147pub struct ChipMap {
148 /// Peripheral entries keyed by lowercased instance name.
149 peripherals: HashMap<String, PeripheralEntry>,
150 /// Interrupt entries keyed by lowercased interrupt name.
151 irqs: HashMap<String, IrqEntry>,
152 /// Whether the chip PAC's `Interrupt` enum describes *this* core.
153 pac_interrupts: bool,
154}
155
156impl ChipMap {
157 /// Look up a peripheral instance by name (case-insensitive).
158 pub fn peripheral(&self, name: &str) -> Option<&PeripheralEntry> {
159 self.peripherals.get(&name.to_ascii_lowercase())
160 }
161
162 /// Whether this core can reach the named peripheral instance.
163 pub fn contains_peripheral(&self, name: &str) -> bool {
164 self.peripherals.contains_key(&name.to_ascii_lowercase())
165 }
166
167 /// Every peripheral instance name this core can reach, as the dump spells
168 /// them. Order is unspecified; callers that emit code must sort.
169 pub fn peripheral_names(&self) -> Vec<String> {
170 let mut names: Vec<String> = self.peripherals.values().map(|p| p.name.clone()).collect();
171 names.sort();
172 names
173 }
174
175 /// Look up an interrupt by name (case-insensitive).
176 pub fn irq(&self, name: &str) -> Option<&IrqEntry> {
177 self.irqs.get(&name.to_ascii_lowercase())
178 }
179
180 /// Look up an interrupt by its external interrupt number.
181 ///
182 /// The hardware doorbell database records only the raw NVIC position for a
183 /// party, so this is how code generation recovers the line's canonical name.
184 /// Numbers are unique within a dump; if one ever repeats, the entry with the
185 /// lowest name wins so the result stays deterministic.
186 pub fn irq_by_number(&self, number: u16) -> Option<&IrqEntry> {
187 self.irqs
188 .values()
189 .filter(|entry| entry.number == number)
190 .min_by(|a, b| a.name.cmp(&b.name))
191 }
192
193 /// Whether the chip PAC's `Interrupt` enum is generated from this core's
194 /// dump, and therefore whether generated code may name a line as
195 /// `Interrupt::<name>` instead of by number.
196 ///
197 /// A PAC ships one interrupt table per chip (see
198 /// `scripts/pac/gen_interrupts.py`), taken from the primary controller core.
199 /// On a secondary core such as STM32MP2's CM0+, a shared line name like
200 /// `IPCC2_RX` exists in both tables under *different* numbers, so spelling
201 /// the variant there would enable the wrong vector.
202 pub fn has_pac_interrupts(&self) -> bool {
203 self.pac_interrupts
204 }
205}
206
207/// Resolve and parse the per-core chip map for a chip part number and core name.
208///
209/// `chip` is the `[profile] chip` part number; `core` is the coprocessor name
210/// (e.g. `cm7`). The lookup is a case-insensitive family-prefix match, with the
211/// `imx*` aliases rewritten to the `mimx*` prefixes, mirroring the chip-family
212/// database.
213///
214/// # Errors
215///
216/// Returns [`DataError::ChipMapUnavailable`] when no bundled dump matches, or
217/// [`DataError::ChipMapParse`] when the dump fails to deserialize.
218pub fn load(chip: &str, core: &str) -> Result<ChipMap, DataError> {
219 let needle = normalize(chip);
220 let core_key = core.to_ascii_lowercase();
221 let entry = CHIPS
222 .iter()
223 .find(|entry| needle.starts_with(entry.family) && entry.core == core_key)
224 .ok_or_else(|| DataError::ChipMapUnavailable {
225 chip: chip.to_string(),
226 core: core.to_string(),
227 })?;
228
229 let raw: RawChip = serde_json::from_str(entry.json).map_err(|err| DataError::ChipMapParse {
230 chip: chip.to_string(),
231 core: core.to_string(),
232 detail: err.to_string(),
233 })?;
234
235 let mut peripherals = HashMap::with_capacity(raw.peripherals.len());
236 for p in raw.peripherals {
237 let base = parse_hex(&p.base_address).ok_or_else(|| DataError::ChipMapParse {
238 chip: chip.to_string(),
239 core: core.to_string(),
240 detail: format!(
241 "peripheral '{}' has invalid base '{}'",
242 p.name, p.base_address
243 ),
244 })?;
245 peripherals.insert(
246 p.name.to_ascii_lowercase(),
247 PeripheralEntry {
248 name: p.name,
249 base,
250 block: p.block,
251 },
252 );
253 }
254
255 let irqs = raw
256 .irqs
257 .into_iter()
258 .map(|irq| {
259 (
260 irq.name.to_ascii_lowercase(),
261 IrqEntry {
262 number: irq.value,
263 name: irq.name,
264 },
265 )
266 })
267 .collect();
268
269 Ok(ChipMap {
270 peripherals,
271 irqs,
272 pac_interrupts: entry.pac,
273 })
274}
275
276/// Parse a `0x`-prefixed (or bare) hexadecimal address.
277fn parse_hex(value: &str) -> Option<u32> {
278 let trimmed = value.trim();
279 let digits = trimmed
280 .strip_prefix("0x")
281 .or_else(|| trimmed.strip_prefix("0X"))
282 .unwrap_or(trimmed);
283 u32::from_str_radix(digits, 16).ok()
284}
285
286/// Lowercases the part number and rewrites `imx` aliases to the `mimx` prefix.
287fn normalize(chip: &str) -> String {
288 let lowered = chip.to_ascii_lowercase();
289 match lowered.strip_prefix("imx") {
290 Some(rest) => format!("mimx{rest}"),
291 None => lowered,
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 #[test]
300 fn resolves_imx95_cm7_lpi2c_and_irq() {
301 let map = load("imx95", "cm7").expect("imx95 cm7 map should resolve");
302 let lpi2c1 = map.peripheral("LPI2C1").expect("LPI2C1 should exist");
303 assert_ne!(lpi2c1.base, 0);
304 assert_eq!(lpi2c1.kind(), "lpi2c");
305 // LPI2C instance name maps 1:1 to its interrupt name.
306 assert!(map.irq("LPI2C1").is_some());
307 }
308
309 #[test]
310 fn resolves_stm32mp25_cm33_gpio() {
311 let map = load("stm32mp257fak3", "cm33").expect("stm32mp25 cm33 map should resolve");
312 let gpioa = map.peripheral("GPIOA").expect("GPIOA should exist");
313 assert_eq!(gpioa.kind(), "gpio");
314 assert!(map.contains_peripheral("i2c1"));
315 }
316
317 #[test]
318 fn rejects_unknown_core() {
319 assert!(matches!(
320 load("stm32mp257fak3", "cm4"),
321 Err(DataError::ChipMapUnavailable { .. })
322 ));
323 }
324
325 /// The STM32MP2 CM0+ must never claim PAC interrupt coverage. The PAC ships
326 /// the CM33 table for both, and the two cores number the same line names
327 /// differently — so code generation naming `Interrupt::<NAME>` on the CM0+
328 /// would install a vector in the CM33's slot while enabling the CM0+ line,
329 /// and the interrupt would be enabled but never delivered.
330 #[test]
331 fn cm0p_does_not_claim_pac_interrupts() {
332 let cm0p = load("stm32mp257fak3", "cm0p").expect("stm32mp25 cm0p map should resolve");
333 let cm33 = load("stm32mp257fak3", "cm33").expect("stm32mp25 cm33 map should resolve");
334
335 assert!(!cm0p.has_pac_interrupts());
336 assert!(cm33.has_pac_interrupts());
337
338 // The divergence this guards against, spelled out: same name, different
339 // line. If these ever agree the flag is still correct, but the reason
340 // recorded here would no longer be the reason.
341 let a = cm0p.irq("IPCC2_RX").expect("cm0p declares IPCC2_RX");
342 let b = cm33.irq("IPCC2_RX").expect("cm33 declares IPCC2_RX");
343 assert_ne!(a.number, b.number);
344 }
345
346 /// `stm32mp23x` and `stm32mp25x` both select the PAC's `stm32mp257`
347 /// interrupt table, so both dumps claim coverage. That is only sound while
348 /// they agree on every line name they share — one `Interrupt` enum cannot be
349 /// right for both otherwise.
350 #[test]
351 fn dumps_sharing_a_pac_table_agree_on_interrupt_numbers() {
352 let mp25 = load("stm32mp257fak3", "cm33").expect("stm32mp25 cm33 map should resolve");
353 let mp23 = load("stm32mp235fak3", "cm33").expect("stm32mp23 cm33 map should resolve");
354 assert!(mp25.has_pac_interrupts() && mp23.has_pac_interrupts());
355
356 let mut shared = 0usize;
357 for (name, entry) in &mp23.irqs {
358 if let Some(other) = mp25.irqs.get(name) {
359 assert_eq!(
360 entry.number, other.number,
361 "{} is {} on stm32mp23 but {} on stm32mp25, yet both use the PAC's \
362 stm32mp257 interrupt table",
363 entry.name, entry.number, other.number,
364 );
365 shared += 1;
366 }
367 }
368 assert!(shared > 100, "expected substantial overlap, saw {shared}");
369 }
370}