Skip to main content

nucleus_compiler/
model.rs

1//! The static peripheral model: how `stm32.toml` keys map to database signals,
2//! which pins a peripheral requires, and which bus clock domain feeds it.
3//!
4//! This is intentionally a small hand-maintained table rather than something
5//! derived from the pack data: the pack knows pin↔signal wiring, but not the
6//! *ergonomic* config-key names (`tx`, `mosi`, `sda`) nor the required-vs-optional
7//! distinction, which are Nucleus product decisions.
8
9/// A device bus clock domain. Phase 2 does only "is this bus enabled?" checking
10/// (per the scope rules — no full clock-tree solving).
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum Bus {
13    Ahb1,
14    Apb1,
15    Apb2,
16}
17
18impl Bus {
19    pub fn name(self) -> &'static str {
20        match self {
21            Bus::Ahb1 => "AHB1",
22            Bus::Apb1 => "APB1",
23            Bus::Apb2 => "APB2",
24        }
25    }
26}
27
28/// One configurable pin on a peripheral: the `stm32.toml` key, the database
29/// signal name it resolves to, and whether the peripheral is unusable without it.
30#[derive(Debug, Clone, Copy)]
31pub struct Role {
32    /// The key as written in the config, e.g. `"tx"`.
33    pub key: &'static str,
34    /// The database signal name, e.g. `"TX"`.
35    pub signal: &'static str,
36    /// Whether omitting this pin is an error (vs. a legitimately optional line).
37    pub required: bool,
38}
39
40const fn req(key: &'static str, signal: &'static str) -> Role {
41    Role {
42        key,
43        signal,
44        required: true,
45    }
46}
47
48const fn opt(key: &'static str, signal: &'static str) -> Role {
49    Role {
50        key,
51        signal,
52        required: false,
53    }
54}
55
56/// The pin roles for a peripheral kind, in canonical order.
57struct Kind {
58    roles: &'static [Role],
59}
60
61const USART: Kind = Kind {
62    roles: &[
63        req("tx", "TX"),
64        req("rx", "RX"),
65        opt("cts", "CTS"),
66        opt("rts", "RTS"),
67        opt("ck", "CK"),
68    ],
69};
70
71const SPI: Kind = Kind {
72    roles: &[
73        req("mosi", "MOSI"),
74        req("miso", "MISO"),
75        req("sck", "SCK"),
76        opt("nss", "NSS"),
77    ],
78};
79
80const I2C: Kind = Kind {
81    roles: &[req("sda", "SDA"), req("scl", "SCL"), opt("smba", "SMBA")],
82};
83
84const TIM: Kind = Kind {
85    roles: &[
86        opt("channel1", "CH1"),
87        opt("channel2", "CH2"),
88        opt("channel3", "CH3"),
89        opt("channel4", "CH4"),
90    ],
91};
92
93/// The pin roles for the peripheral instance named `instance` (e.g. `"usart2"`),
94/// or `None` if the kind is not modelled. The match is on the alphabetic prefix,
95/// so `usart2` and `usart3` share one role table.
96pub fn roles_for(instance: &str) -> Option<&'static [Role]> {
97    let kind = instance.trim_end_matches(|c: char| c.is_ascii_digit());
98    let kind = match kind {
99        "usart" | "uart" => &USART,
100        "spi" => &SPI,
101        "i2c" | "fmpi2c" => &I2C,
102        "tim" => &TIM,
103        _ => return None,
104    };
105    Some(kind.roles)
106}
107
108/// The database peripheral name for a config instance name, e.g.
109/// `"usart2"` → `"USART2"`. Nucleus simply upper-cases the instance name; the
110/// database uses the same convention as ST's pin data.
111pub fn peripheral_name(instance: &str) -> String {
112    instance.to_ascii_uppercase()
113}
114
115/// The bus clock domain that feeds `peripheral` on the STM32F446RE, or `None`
116/// if unknown (in which case the clock check is skipped — never a false error).
117///
118/// Source: STM32F446xx reference manual (RM0390) RCC peripheral-clock-enable
119/// registers. Only the peripherals Nucleus models in Phase 2 need be exact;
120/// the table errs toward `None` rather than guessing.
121pub fn peripheral_bus(peripheral: &str) -> Option<Bus> {
122    let p = peripheral;
123    let bus = match p {
124        // APB2
125        "USART1" | "USART6" | "SPI1" | "SPI4" | "SDIO" | "ADC1" | "ADC2" | "ADC3" | "SAI1"
126        | "SAI2" | "TIM1" | "TIM8" | "TIM9" | "TIM10" | "TIM11" => Bus::Apb2,
127        // APB1
128        "USART2" | "USART3" | "UART4" | "UART5" | "SPI2" | "SPI3" | "I2C1" | "I2C2" | "I2C3"
129        | "FMPI2C1" | "CAN1" | "CAN2" | "SPDIFRX" | "CEC" | "TIM2" | "TIM3" | "TIM4" | "TIM5"
130        | "TIM6" | "TIM7" | "TIM12" | "TIM13" | "TIM14" => Bus::Apb1,
131        // AHB1 / OTG
132        "DCMI" | "QUADSPI" | "FMC" => Bus::Ahb1,
133        _ => return None,
134    };
135    Some(bus)
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn role_tables_resolve_by_prefix() {
144        assert!(roles_for("usart2").is_some());
145        assert!(roles_for("spi1").is_some());
146        assert!(roles_for("i2c3").is_some());
147        assert!(roles_for("tim2").is_some());
148        assert!(roles_for("wibble9").is_none());
149    }
150
151    #[test]
152    fn spi_required_and_optional() {
153        let roles = roles_for("spi1").unwrap();
154        let nss = roles.iter().find(|r| r.key == "nss").unwrap();
155        let sck = roles.iter().find(|r| r.key == "sck").unwrap();
156        assert!(!nss.required);
157        assert!(sck.required);
158    }
159
160    #[test]
161    fn buses_match_f446() {
162        assert_eq!(peripheral_bus("SPI1"), Some(Bus::Apb2));
163        assert_eq!(peripheral_bus("USART2"), Some(Bus::Apb1));
164        assert_eq!(peripheral_bus("I2C1"), Some(Bus::Apb1));
165        assert_eq!(peripheral_bus("MADEUP"), None);
166    }
167}