use std::collections::BTreeMap;
use std::fmt;
use std::str::FromStr;
use nucleus_db::{Database, Pin};
use crate::config::Config;
use crate::model::{self, Bus};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Conflict {
PinCollision {
pin: Pin,
users: Vec<SignalRef>,
},
AfMismatch {
pin: Pin,
peripheral: String,
signal: String,
},
InvalidPin {
peripheral: String,
key: String,
value: String,
},
MissingPin {
peripheral: String,
key: String,
signal: String,
},
ClockDomainDisabled { peripheral: String, bus: Bus },
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct SignalRef {
pub peripheral: String,
pub signal: String,
}
impl fmt::Display for SignalRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}_{}", self.peripheral, self.signal)
}
}
impl fmt::Display for Conflict {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Conflict::PinCollision { pin, users } => {
let names: Vec<String> = users.iter().map(ToString::to_string).collect();
write!(
f,
"pin collision on {pin}: assigned to {}",
names.join(" and ")
)
}
Conflict::AfMismatch {
pin,
peripheral,
signal,
} => write!(
f,
"AF mismatch: {pin} has no alternate function for {peripheral}_{signal} on this MCU"
),
Conflict::InvalidPin {
peripheral,
key,
value,
} => write!(
f,
"invalid pin: {peripheral}.{key} = {value:?} is not a valid pin name"
),
Conflict::MissingPin {
peripheral,
key,
signal,
} => write!(
f,
"missing required pin: {peripheral} needs a {key} pin ({peripheral}_{signal})"
),
Conflict::ClockDomainDisabled { peripheral, bus } => write!(
f,
"clock domain disabled: {peripheral} is on {} but [clocks].{} = false",
bus.name(),
bus.name().to_ascii_lowercase()
),
}
}
}
pub fn solve(config: &Config, db: &Database) -> Vec<Conflict> {
let mut conflicts = Vec::new();
let mut pin_users: BTreeMap<Pin, Vec<SignalRef>> = BTreeMap::new();
for (instance, table) in &config.peripherals {
let Some(roles) = model::roles_for(instance) else {
continue;
};
let peripheral = model::peripheral_name(instance);
if let Some(bus) = model::peripheral_bus(&peripheral) {
let enabled = match bus {
Bus::Ahb1 => config.clocks.ahb1,
Bus::Apb1 => config.clocks.apb1,
Bus::Apb2 => config.clocks.apb2,
};
if !enabled {
conflicts.push(Conflict::ClockDomainDisabled {
peripheral: peripheral.clone(),
bus,
});
}
}
for role in roles {
match table.pin_str(role.key) {
None => {
if role.required {
conflicts.push(Conflict::MissingPin {
peripheral: peripheral.clone(),
key: role.key.to_string(),
signal: role.signal.to_string(),
});
}
}
Some(value) => {
let Ok(pin) = Pin::from_str(value) else {
conflicts.push(Conflict::InvalidPin {
peripheral: peripheral.clone(),
key: role.key.to_string(),
value: value.to_string(),
});
continue;
};
if db.find_af(pin, &peripheral, role.signal).is_none() {
conflicts.push(Conflict::AfMismatch {
pin,
peripheral: peripheral.clone(),
signal: role.signal.to_string(),
});
}
pin_users.entry(pin).or_default().push(SignalRef {
peripheral: peripheral.clone(),
signal: role.signal.to_string(),
});
}
}
}
}
for (pin, mut users) in pin_users {
if users.len() > 1 {
users.sort();
conflicts.push(Conflict::PinCollision { pin, users });
}
}
conflicts
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config;
fn db() -> Database {
Database::f446re()
}
fn solve_toml(text: &str) -> Vec<Conflict> {
let cfg = config::parse(text).unwrap();
solve(&cfg, &db())
}
#[test]
fn clean_config_has_no_conflicts() {
let conflicts = solve_toml(
r#"
[peripherals.usart2]
tx = "PA2"
rx = "PA3"
[peripherals.spi1]
mosi = "PA7"
miso = "PA6"
sck = "PA5"
nss = "PA4"
[peripherals.i2c1]
sda = "PB9"
scl = "PB8"
"#,
);
assert_eq!(
conflicts,
vec![],
"expected clean config, got {conflicts:?}"
);
}
#[test]
fn detects_pin_collision() {
let conflicts = solve_toml(
r#"
[peripherals.spi1]
mosi = "PA7"
miso = "PA6"
sck = "PA5"
[peripherals.tim2]
channel1 = "PA5"
"#,
);
let collisions: Vec<_> = conflicts
.iter()
.filter(|c| matches!(c, Conflict::PinCollision { .. }))
.collect();
assert_eq!(collisions.len(), 1, "got {conflicts:?}");
if let Conflict::PinCollision { pin, users } = collisions[0] {
assert_eq!(pin.to_string(), "PA5");
assert_eq!(users.len(), 2);
}
}
#[test]
fn detects_af_mismatch() {
let conflicts = solve_toml(
r#"
[peripherals.usart2]
tx = "PB0"
rx = "PA3"
"#,
);
assert!(
conflicts.iter().any(|c| matches!(
c,
Conflict::AfMismatch { pin, signal, .. }
if pin.to_string() == "PB0" && signal == "TX"
)),
"got {conflicts:?}"
);
}
#[test]
fn detects_missing_required_pin() {
let conflicts = solve_toml(
r#"
[peripherals.spi1]
miso = "PA6"
sck = "PA5"
"#,
);
assert!(
conflicts.iter().any(|c| matches!(
c,
Conflict::MissingPin { peripheral, signal, .. }
if peripheral == "SPI1" && signal == "MOSI"
)),
"got {conflicts:?}"
);
}
#[test]
fn missing_optional_pin_is_not_a_conflict() {
let conflicts = solve_toml(
r#"
[peripherals.spi1]
mosi = "PA7"
miso = "PA6"
sck = "PA5"
"#,
);
assert_eq!(conflicts, vec![]);
}
#[test]
fn detects_clock_domain_disabled() {
let conflicts = solve_toml(
r#"
[clocks]
apb2 = false
[peripherals.spi1]
mosi = "PA7"
miso = "PA6"
sck = "PA5"
"#,
);
assert!(
conflicts.iter().any(|c| matches!(
c,
Conflict::ClockDomainDisabled { peripheral, bus }
if peripheral == "SPI1" && *bus == Bus::Apb2
)),
"got {conflicts:?}"
);
}
#[test]
fn invalid_pin_name_reported() {
let conflicts = solve_toml(
r#"
[peripherals.usart2]
tx = "PZ9"
rx = "PA3"
"#,
);
assert!(
conflicts
.iter()
.any(|c| matches!(c, Conflict::InvalidPin { value, .. } if value == "PZ9")),
"got {conflicts:?}"
);
}
}