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;
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    /// The raw JSON dump.
38    json: &'static str,
39}
40
41/// All bundled per-core chip dumps.
42///
43/// Ordered most-specific family prefix first, mirroring the chip-family
44/// database resolution.
45const CHIPS: &[Embedded] = &[
46    Embedded {
47        family: "stm32mp25",
48        core: "cm33",
49        json: include_str!("../database/chip/stm32mp25_cm33.json"),
50    },
51    Embedded {
52        family: "stm32mp25",
53        core: "cm0p",
54        json: include_str!("../database/chip/stm32mp25_cm0p.json"),
55    },
56    Embedded {
57        family: "stm32mp23",
58        core: "cm33",
59        json: include_str!("../database/chip/stm32mp23_cm33.json"),
60    },
61    Embedded {
62        family: "stm32mp21",
63        core: "cm33",
64        json: include_str!("../database/chip/stm32mp21_cm33.json"),
65    },
66    Embedded {
67        family: "mimx95",
68        core: "cm7",
69        json: include_str!("../database/chip/mimx95_cm7.json"),
70    },
71    Embedded {
72        family: "mimx93",
73        core: "cm33",
74        json: include_str!("../database/chip/mimx93_cm33.json"),
75    },
76];
77
78/// Raw JSON shape of a chip dump (only the fields we consume).
79#[derive(serde::Deserialize)]
80struct RawChip {
81    irqs: Vec<RawIrq>,
82    peripherals: Vec<RawPeripheral>,
83}
84
85#[derive(serde::Deserialize)]
86struct RawIrq {
87    name: String,
88    value: u16,
89}
90
91#[derive(serde::Deserialize)]
92struct RawPeripheral {
93    name: String,
94    base_address: String,
95    block: String,
96}
97
98/// A resolved peripheral: its MMIO base and register-block reference.
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct PeripheralEntry {
101    /// Instance name exactly as the dump spells it, e.g. `LPI2C1`. Lookups are
102    /// case-insensitive, so this is what code generation must use when it needs
103    /// to name the matching HAL peripheral singleton.
104    pub name: String,
105    /// Physical base address as seen by the owning core.
106    pub base: u32,
107    /// Register-block reference from the dump, e.g. `lpi2c/LPI2C` or `gpio/GPIO`.
108    pub block: String,
109}
110
111impl PeripheralEntry {
112    /// The block *kind* — the part before the `/`, lowercased (e.g. `lpi2c`).
113    pub fn kind(&self) -> &str {
114        self.block.split('/').next().unwrap_or(&self.block)
115    }
116}
117
118/// A resolved interrupt: its NVIC number and canonical name.
119#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct IrqEntry {
121    /// External interrupt number (exception number minus 16).
122    pub number: u16,
123    /// Canonical interrupt name from the dump (e.g. `LPI2C1`), used to form the
124    /// `<name>_IRQHandler` vector-table symbol.
125    pub name: String,
126}
127
128/// A single core's peripheral and interrupt map.
129#[derive(Debug, Clone)]
130pub struct ChipMap {
131    /// Peripheral entries keyed by lowercased instance name.
132    peripherals: HashMap<String, PeripheralEntry>,
133    /// Interrupt entries keyed by lowercased interrupt name.
134    irqs: HashMap<String, IrqEntry>,
135}
136
137impl ChipMap {
138    /// Look up a peripheral instance by name (case-insensitive).
139    pub fn peripheral(&self, name: &str) -> Option<&PeripheralEntry> {
140        self.peripherals.get(&name.to_ascii_lowercase())
141    }
142
143    /// Whether this core can reach the named peripheral instance.
144    pub fn contains_peripheral(&self, name: &str) -> bool {
145        self.peripherals.contains_key(&name.to_ascii_lowercase())
146    }
147
148    /// Every peripheral instance name this core can reach, as the dump spells
149    /// them. Order is unspecified; callers that emit code must sort.
150    pub fn peripheral_names(&self) -> Vec<String> {
151        let mut names: Vec<String> = self.peripherals.values().map(|p| p.name.clone()).collect();
152        names.sort();
153        names
154    }
155
156    /// Look up an interrupt by name (case-insensitive).
157    pub fn irq(&self, name: &str) -> Option<&IrqEntry> {
158        self.irqs.get(&name.to_ascii_lowercase())
159    }
160}
161
162/// Resolve and parse the per-core chip map for a chip part number and core name.
163///
164/// `chip` is the `[profile] chip` part number; `core` is the coprocessor name
165/// (e.g. `cm7`). The lookup is a case-insensitive family-prefix match, with the
166/// `imx*` aliases rewritten to the `mimx*` prefixes, mirroring the chip-family
167/// database.
168///
169/// # Errors
170///
171/// Returns [`DataError::ChipMapUnavailable`] when no bundled dump matches, or
172/// [`DataError::ChipMapParse`] when the dump fails to deserialize.
173pub fn load(chip: &str, core: &str) -> Result<ChipMap, DataError> {
174    let needle = normalize(chip);
175    let core_key = core.to_ascii_lowercase();
176    let entry = CHIPS
177        .iter()
178        .find(|entry| needle.starts_with(entry.family) && entry.core == core_key)
179        .ok_or_else(|| DataError::ChipMapUnavailable {
180            chip: chip.to_string(),
181            core: core.to_string(),
182        })?;
183
184    let raw: RawChip = serde_json::from_str(entry.json).map_err(|err| DataError::ChipMapParse {
185        chip: chip.to_string(),
186        core: core.to_string(),
187        detail: err.to_string(),
188    })?;
189
190    let mut peripherals = HashMap::with_capacity(raw.peripherals.len());
191    for p in raw.peripherals {
192        let base = parse_hex(&p.base_address).ok_or_else(|| DataError::ChipMapParse {
193            chip: chip.to_string(),
194            core: core.to_string(),
195            detail: format!(
196                "peripheral '{}' has invalid base '{}'",
197                p.name, p.base_address
198            ),
199        })?;
200        peripherals.insert(
201            p.name.to_ascii_lowercase(),
202            PeripheralEntry {
203                name: p.name,
204                base,
205                block: p.block,
206            },
207        );
208    }
209
210    let irqs = raw
211        .irqs
212        .into_iter()
213        .map(|irq| {
214            (
215                irq.name.to_ascii_lowercase(),
216                IrqEntry {
217                    number: irq.value,
218                    name: irq.name,
219                },
220            )
221        })
222        .collect();
223
224    Ok(ChipMap { peripherals, irqs })
225}
226
227/// Parse a `0x`-prefixed (or bare) hexadecimal address.
228fn parse_hex(value: &str) -> Option<u32> {
229    let trimmed = value.trim();
230    let digits = trimmed
231        .strip_prefix("0x")
232        .or_else(|| trimmed.strip_prefix("0X"))
233        .unwrap_or(trimmed);
234    u32::from_str_radix(digits, 16).ok()
235}
236
237/// Lowercases the part number and rewrites `imx` aliases to the `mimx` prefix.
238fn normalize(chip: &str) -> String {
239    let lowered = chip.to_ascii_lowercase();
240    match lowered.strip_prefix("imx") {
241        Some(rest) => format!("mimx{rest}"),
242        None => lowered,
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    #[test]
251    fn resolves_imx95_cm7_lpi2c_and_irq() {
252        let map = load("imx95", "cm7").expect("imx95 cm7 map should resolve");
253        let lpi2c1 = map.peripheral("LPI2C1").expect("LPI2C1 should exist");
254        assert_ne!(lpi2c1.base, 0);
255        assert_eq!(lpi2c1.kind(), "lpi2c");
256        // LPI2C instance name maps 1:1 to its interrupt name.
257        assert!(map.irq("LPI2C1").is_some());
258    }
259
260    #[test]
261    fn resolves_stm32mp25_cm33_gpio() {
262        let map = load("stm32mp257fak3", "cm33").expect("stm32mp25 cm33 map should resolve");
263        let gpioa = map.peripheral("GPIOA").expect("GPIOA should exist");
264        assert_eq!(gpioa.kind(), "gpio");
265        assert!(map.contains_peripheral("i2c1"));
266    }
267
268    #[test]
269    fn rejects_unknown_core() {
270        assert!(matches!(
271            load("stm32mp257fak3", "cm4"),
272            Err(DataError::ChipMapUnavailable { .. })
273        ));
274    }
275}