Skip to main content

consortium_data/
signal.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-family pin-mux and DMA-request maps, embedded at compile time from the
18//! generated signal databases (`database/signal/<family>.json`).
19//!
20//! A chip map says which peripherals one *core* can reach; a signal map says
21//! which *pads* a peripheral's signals can be routed to, how each pad is muxed,
22//! which packages bond the pad out, and which DMA request number each
23//! peripheral signal raises. Pads and DMA requests belong to the die, so there is
24//! one map per family rather than one per core.
25//!
26//! Both vendors share one shape: ST pads carry an AF number (or need no AF, or
27//! are analog inputs), i.MX pads carry their IOMUXC registers. ST sources say
28//! which packages bond each pad out; i.MX sources do not, so an i.MX
29//! [`Pad::packages`] is `None`.
30//!
31//! The data is generated by `scripts/pac/parse_signals_*.py` from
32//! `STM32_open_pin_data` (BSD-3-Clause, © STMicroelectronics), STM32CubeMP2, and
33//! MCUX `mcux-devices-imx` (BSD-3-Clause, © NXP); see this crate's `NOTICE`.
34
35use std::collections::HashMap;
36
37use crate::error::DataError;
38use crate::family::{matches, normalize};
39
40/// A signal database bundled into the binary, with the family prefix that
41/// selects it.
42struct Embedded {
43    /// Lowercase part-number prefix that selects this family (e.g. `stm32mp25`).
44    family: &'static str,
45    /// The raw JSON database.
46    json: &'static str,
47}
48
49const FAMILIES: &[Embedded] = &[
50    Embedded {
51        family: "stm32mp25",
52        json: include_str!("../database/signal/stm32mp25.json"),
53    },
54    Embedded {
55        family: "stm32mp23",
56        json: include_str!("../database/signal/stm32mp23.json"),
57    },
58    Embedded {
59        family: "stm32mp21",
60        json: include_str!("../database/signal/stm32mp21.json"),
61    },
62    Embedded {
63        family: "mimx952",
64        json: include_str!("../database/signal/mimx952.json"),
65    },
66    Embedded {
67        family: "mimx95",
68        json: include_str!("../database/signal/mimx95.json"),
69    },
70    Embedded {
71        family: "mimx93",
72        json: include_str!("../database/signal/mimx93.json"),
73    },
74];
75
76#[derive(serde::Deserialize)]
77struct RawMap {
78    packages: Vec<RawPackage>,
79    pads: Vec<RawPad>,
80    signals: Vec<RawSignal>,
81    dma: Vec<DmaRequest>,
82}
83
84#[derive(serde::Deserialize)]
85struct RawPackage {
86    name: String,
87    parts: Vec<String>,
88}
89
90#[derive(serde::Deserialize)]
91struct RawPad {
92    name: String,
93    packages: Option<Vec<String>>,
94}
95
96#[derive(serde::Deserialize)]
97struct RawSignal {
98    pad: String,
99    function: String,
100    peripheral: Option<String>,
101    signal: Option<String>,
102    mux: RawMux,
103}
104
105#[derive(serde::Deserialize)]
106#[serde(tag = "kind", rename_all = "snake_case")]
107enum RawMux {
108    Af {
109        af: u8,
110    },
111    Additional,
112    Analog,
113    Iomuxc {
114        mux_register: String,
115        mux_mode: u8,
116        input_register: Option<String>,
117        input_daisy: Option<u8>,
118        config_register: String,
119    },
120}
121
122/// How a pad is switched to a signal.
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub enum Mux {
125    /// STM32 alternate function `AFn` in the port's `AFRL`/`AFRH`.
126    Af(u8),
127    /// An STM32 pad function that needs no AF (`RTC_OUT1`, `PWR_WKUP1`,
128    /// `TAMP_IN1`, `ADCx_EXTIn`): the owning block takes the pad over.
129    Additional,
130    /// An STM32 ADC input: the pad in analog mode.
131    Analog,
132    /// An i.MX IOMUXC routing.
133    Iomuxc {
134        /// `SW_MUX_CTL_PAD_*` register address.
135        mux_register: u32,
136        /// `MUX_MODE` value selecting this function.
137        mux_mode: u8,
138        /// Input-select (daisy) register address and the value routing this
139        /// pad to it, for signals that have one.
140        input: Option<(u32, u8)>,
141        /// `SW_PAD_CTL_PAD_*` register address.
142        config_register: u32,
143    },
144}
145
146/// A package and the part numbers that come in it.
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct Package {
149    /// Package name, e.g. `TFBGA436`.
150    pub name: String,
151    /// CubeMX part references, e.g. `STM32MP257FAKx` (the trailing `x` stands
152    /// for the temperature-range digit).
153    pub parts: Vec<String>,
154}
155
156/// A muxable pad.
157#[derive(Debug, Clone, PartialEq, Eq)]
158pub struct Pad {
159    /// Pad name as the vendor spells it: `PA4` on STM32, `UART1_TXD` on i.MX.
160    pub name: String,
161    /// Packages that bond this pad out, or `None` when the source does not say.
162    pub packages: Option<Vec<String>>,
163}
164
165/// One signal a pad can carry.
166#[derive(Debug, Clone, PartialEq, Eq)]
167pub struct Signal {
168    /// Pad name.
169    pub pad: String,
170    /// Vendor function name, verbatim: `USART2_TX`, `LPUART1_TX`.
171    pub function: String,
172    /// Chip-dump peripheral the function belongs to, when any core can reach
173    /// it (`None` for A-core-only blocks such as i.MX `ENET1`).
174    pub peripheral: Option<String>,
175    /// The rest of `function` after the peripheral, e.g. `TX`.
176    pub signal: Option<String>,
177    /// How the pad is switched to this function.
178    pub mux: Mux,
179}
180
181/// One DMA request line.
182#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
183pub struct DmaRequest {
184    /// Request-routing controller: `HPDMA`/`LPDMA` on STM32MP2, `DMA3`/`DMA4`/
185    /// `DMA5` on i.MX 9.
186    pub controller: String,
187    /// Request number to program into the controller.
188    pub request: u16,
189    /// Vendor function name, e.g. `USART1_TX` or `TPM1_OVERFLOW_REQUEST`.
190    pub function: String,
191    /// Chip-dump peripheral, as for [`Signal::peripheral`].
192    pub peripheral: Option<String>,
193    /// The rest of `function` after the peripheral.
194    pub signal: Option<String>,
195}
196
197/// A family's pads, signals, packages, and DMA requests.
198#[derive(Debug, Clone)]
199pub struct SignalMap {
200    packages: Vec<Package>,
201    /// Pads keyed by uppercased name.
202    pads: HashMap<String, Pad>,
203    signals: Vec<Signal>,
204    dma: Vec<DmaRequest>,
205}
206
207impl SignalMap {
208    /// Every package of the family.
209    pub fn packages(&self) -> &[Package] {
210        &self.packages
211    }
212
213    /// The package a part number comes in, if the family lists packages and
214    /// `chip` is specific enough to pick one.
215    ///
216    /// CubeMX writes a part as `STM32MP257FAKx`, where the `x` is the
217    /// temperature-range digit, so `stm32mp257fak3` matches it and
218    /// `stm32mp257` (no package letters) matches nothing.
219    pub fn package_of(&self, chip: &str) -> Option<&Package> {
220        let chip = chip.to_ascii_uppercase();
221        self.packages.iter().find(|package| {
222            package.parts.iter().any(|part| {
223                let stem = part.strip_suffix(['x', 'X']).unwrap_or(part);
224                chip.starts_with(&stem.to_ascii_uppercase())
225            })
226        })
227    }
228
229    /// Look up a pad by name (case-insensitive).
230    pub fn pad(&self, name: &str) -> Option<&Pad> {
231        self.pads.get(&name.to_ascii_uppercase())
232    }
233
234    /// Every pad signal of the family.
235    pub fn signals(&self) -> &[Signal] {
236        &self.signals
237    }
238
239    /// Every pad that can carry `<peripheral>_<signal>` (case-insensitive),
240    /// e.g. `("usart2", "tx")`.
241    ///
242    /// Matching is on the vendor's function name, so it also works for blocks
243    /// no controller core can reach (whose [`Signal::peripheral`] is `None`).
244    pub fn signal_routes(&self, peripheral: &str, signal: &str) -> Vec<&Signal> {
245        let function = format!("{peripheral}_{signal}").to_ascii_uppercase();
246        self.signals
247            .iter()
248            .filter(|s| s.function == function)
249            .collect()
250    }
251
252    /// Every DMA request line of the family.
253    pub fn dma_requests(&self) -> &[DmaRequest] {
254        &self.dma
255    }
256
257    /// The DMA request lines `peripheral` raises (case-insensitive).
258    pub fn dma_requests_for(&self, peripheral: &str) -> Vec<&DmaRequest> {
259        let prefix = format!("{}_", peripheral.to_ascii_uppercase());
260        self.dma
261            .iter()
262            .filter(|d| {
263                d.peripheral
264                    .as_deref()
265                    .is_some_and(|p| p.eq_ignore_ascii_case(peripheral))
266                    || d.function.starts_with(&prefix)
267            })
268            .collect()
269    }
270}
271
272/// Resolve and parse the signal map for a chip part number.
273///
274/// The lookup is the same case-insensitive family match as
275/// [`crate::chipmap::load`], with `imx*` rewritten to `mimx*`.
276///
277/// # Errors
278///
279/// Returns [`DataError::SignalMapUnavailable`] when no bundled database
280/// matches, or [`DataError::SignalMapParse`] when it fails to deserialize.
281pub fn load(chip: &str) -> Result<SignalMap, DataError> {
282    let needle = normalize(chip);
283    let entry = FAMILIES
284        .iter()
285        .find(|entry| matches(&needle, entry.family))
286        .ok_or_else(|| DataError::SignalMapUnavailable(chip.to_string()))?;
287    let parse_err = |detail: String| DataError::SignalMapParse {
288        chip: chip.to_string(),
289        detail,
290    };
291
292    let raw: RawMap = serde_json::from_str(entry.json).map_err(|err| parse_err(err.to_string()))?;
293
294    let mut signals = Vec::with_capacity(raw.signals.len());
295    for s in raw.signals {
296        let register = |value: &str| {
297            parse_hex(value).ok_or_else(|| {
298                parse_err(format!(
299                    "{} on {}: bad register '{value}'",
300                    s.function, s.pad
301                ))
302            })
303        };
304        let mux = match &s.mux {
305            RawMux::Af { af } => Mux::Af(*af),
306            RawMux::Additional => Mux::Additional,
307            RawMux::Analog => Mux::Analog,
308            RawMux::Iomuxc {
309                mux_register,
310                mux_mode,
311                input_register,
312                input_daisy,
313                config_register,
314            } => Mux::Iomuxc {
315                mux_register: register(mux_register)?,
316                mux_mode: *mux_mode,
317                input: match (input_register, input_daisy) {
318                    (Some(reg), Some(daisy)) => Some((register(reg)?, *daisy)),
319                    (None, None) => None,
320                    _ => {
321                        return Err(parse_err(format!(
322                            "{} on {}: input register and daisy disagree",
323                            s.function, s.pad
324                        )));
325                    }
326                },
327                config_register: register(config_register)?,
328            },
329        };
330        signals.push(Signal {
331            pad: s.pad,
332            function: s.function,
333            peripheral: s.peripheral,
334            signal: s.signal,
335            mux,
336        });
337    }
338
339    Ok(SignalMap {
340        packages: raw
341            .packages
342            .into_iter()
343            .map(|p| Package {
344                name: p.name,
345                parts: p.parts,
346            })
347            .collect(),
348        pads: raw
349            .pads
350            .into_iter()
351            .map(|p| {
352                (
353                    p.name.to_ascii_uppercase(),
354                    Pad {
355                        name: p.name,
356                        packages: p.packages,
357                    },
358                )
359            })
360            .collect(),
361        signals,
362        dma: raw.dma,
363    })
364}
365
366fn parse_hex(value: &str) -> Option<u32> {
367    u32::from_str_radix(value.strip_prefix("0x")?, 16).ok()
368}
369
370#[cfg(test)]
371mod tests {
372    use super::*;
373
374    /// Every bundled family parses, so a malformed regeneration fails here
375    /// rather than at a user's `csti build`.
376    #[test]
377    fn every_family_parses() {
378        for chip in [
379            "stm32mp257fak3",
380            "stm32mp235fak3",
381            "stm32mp215fan3",
382            "mimx9596xvt",
383            "mimx9529avzxn",
384            "mimx9352",
385        ] {
386            let map = load(chip).unwrap_or_else(|err| panic!("{chip}: {err}"));
387            assert!(!map.signals().is_empty(), "{chip} has no signals");
388            assert!(!map.dma_requests().is_empty(), "{chip} has no DMA requests");
389        }
390    }
391
392    #[test]
393    fn rejects_other_dies_sharing_an_imx_prefix() {
394        for chip in ["mimx9375", "mimx9371xxvtx"] {
395            assert!(
396                matches!(load(chip), Err(DataError::SignalMapUnavailable(_))),
397                "{chip} should not resolve"
398            );
399        }
400    }
401
402    #[test]
403    fn stm32mp25_i2c1_sda_routes_and_package() {
404        let map = load("stm32mp257fak3").unwrap();
405        let mut routes: Vec<(String, Mux)> = map
406            .signal_routes("i2c1", "sda")
407            .into_iter()
408            .map(|s| (s.pad.clone(), s.mux))
409            .collect();
410        routes.sort_by(|a, b| a.0.cmp(&b.0));
411        assert_eq!(
412            routes,
413            [
414                ("PA2".into(), Mux::Af(10)),
415                ("PI1".into(), Mux::Af(9)),
416                ("PJ10".into(), Mux::Af(9)),
417            ]
418        );
419
420        let package = map
421            .package_of("stm32mp257fak3")
422            .expect("FAK is a known part");
423        assert_eq!(package.name, "VFBGA424");
424        assert!(map.package_of("stm32mp257").is_none());
425        let pa0 = map.pad("pa0").expect("PA0 exists");
426        assert!(pa0.packages.as_ref().unwrap().contains(&package.name));
427    }
428
429    #[test]
430    fn mimx93_lpuart1_tx_iomuxc_and_dma() {
431        let map = load("imx93").unwrap();
432        let routes = map.signal_routes("LPUART1", "TX");
433        assert_eq!(routes.len(), 1);
434        assert_eq!(routes[0].pad, "UART1_TXD");
435        assert_eq!(
436            routes[0].mux,
437            Mux::Iomuxc {
438                mux_register: 0x543c_0184,
439                mux_mode: 0,
440                input: None,
441                config_register: 0x543c_0334,
442            }
443        );
444        assert!(map.package_of("mimx9352").is_none());
445        assert_eq!(map.pad("UART1_TXD").unwrap().packages, None);
446
447        let dma: Vec<(&str, u16)> = map
448            .dma_requests_for("lpuart1")
449            .into_iter()
450            .map(|d| (d.function.as_str(), d.request))
451            .collect();
452        assert_eq!(dma, [("LPUART1_TX", 16), ("LPUART1_RX", 17)]);
453    }
454
455    #[test]
456    fn rejects_unknown_family() {
457        assert!(matches!(
458            load("stm32h747"),
459            Err(DataError::SignalMapUnavailable(_))
460        ));
461    }
462}