Skip to main content

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