1use std::collections::BTreeMap;
15use std::fmt;
16use std::str::FromStr;
17
18use nucleus_db::{Database, Pin};
19
20use crate::config::Config;
21use crate::model::{self, Bus};
22
23#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum Conflict {
27 PinCollision {
29 pin: Pin,
30 users: Vec<SignalRef>,
32 },
33 AfMismatch {
35 pin: Pin,
36 peripheral: String,
37 signal: String,
38 },
39 InvalidPin {
41 peripheral: String,
42 key: String,
43 value: String,
44 },
45 MissingPin {
47 peripheral: String,
48 key: String,
49 signal: String,
50 },
51 ClockDomainDisabled { peripheral: String, bus: Bus },
53}
54
55#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
57pub struct SignalRef {
58 pub peripheral: String,
59 pub signal: String,
60}
61
62impl fmt::Display for SignalRef {
63 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 write!(f, "{}_{}", self.peripheral, self.signal)
65 }
66}
67
68impl fmt::Display for Conflict {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 match self {
71 Conflict::PinCollision { pin, users } => {
72 let names: Vec<String> = users.iter().map(ToString::to_string).collect();
73 write!(
74 f,
75 "pin collision on {pin}: assigned to {}",
76 names.join(" and ")
77 )
78 }
79 Conflict::AfMismatch {
80 pin,
81 peripheral,
82 signal,
83 } => write!(
84 f,
85 "AF mismatch: {pin} has no alternate function for {peripheral}_{signal} on this MCU"
86 ),
87 Conflict::InvalidPin {
88 peripheral,
89 key,
90 value,
91 } => write!(
92 f,
93 "invalid pin: {peripheral}.{key} = {value:?} is not a valid pin name"
94 ),
95 Conflict::MissingPin {
96 peripheral,
97 key,
98 signal,
99 } => write!(
100 f,
101 "missing required pin: {peripheral} needs a {key} pin ({peripheral}_{signal})"
102 ),
103 Conflict::ClockDomainDisabled { peripheral, bus } => write!(
104 f,
105 "clock domain disabled: {peripheral} is on {} but [clocks].{} = false",
106 bus.name(),
107 bus.name().to_ascii_lowercase()
108 ),
109 }
110 }
111}
112
113pub fn solve(config: &Config, db: &Database) -> Vec<Conflict> {
116 let mut conflicts = Vec::new();
117 let mut pin_users: BTreeMap<Pin, Vec<SignalRef>> = BTreeMap::new();
119
120 for (instance, table) in &config.peripherals {
122 let Some(roles) = model::roles_for(instance) else {
123 continue;
125 };
126 let peripheral = model::peripheral_name(instance);
127
128 if let Some(bus) = model::peripheral_bus(&peripheral) {
130 let enabled = match bus {
131 Bus::Ahb1 => config.clocks.ahb1,
132 Bus::Apb1 => config.clocks.apb1,
133 Bus::Apb2 => config.clocks.apb2,
134 };
135 if !enabled {
136 conflicts.push(Conflict::ClockDomainDisabled {
137 peripheral: peripheral.clone(),
138 bus,
139 });
140 }
141 }
142
143 for role in roles {
144 match table.pin_str(role.key) {
145 None => {
146 if role.required {
147 conflicts.push(Conflict::MissingPin {
148 peripheral: peripheral.clone(),
149 key: role.key.to_string(),
150 signal: role.signal.to_string(),
151 });
152 }
153 }
154 Some(value) => {
155 let Ok(pin) = Pin::from_str(value) else {
156 conflicts.push(Conflict::InvalidPin {
157 peripheral: peripheral.clone(),
158 key: role.key.to_string(),
159 value: value.to_string(),
160 });
161 continue;
162 };
163 if db.find_af(pin, &peripheral, role.signal).is_none() {
165 conflicts.push(Conflict::AfMismatch {
166 pin,
167 peripheral: peripheral.clone(),
168 signal: role.signal.to_string(),
169 });
170 }
171 pin_users.entry(pin).or_default().push(SignalRef {
175 peripheral: peripheral.clone(),
176 signal: role.signal.to_string(),
177 });
178 }
179 }
180 }
181 }
182
183 for (pin, mut users) in pin_users {
186 if users.len() > 1 {
187 users.sort();
188 conflicts.push(Conflict::PinCollision { pin, users });
189 }
190 }
191
192 conflicts
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 use crate::config;
199
200 fn db() -> Database {
201 Database::f446re()
202 }
203
204 fn solve_toml(text: &str) -> Vec<Conflict> {
205 let cfg = config::parse(text).unwrap();
206 solve(&cfg, &db())
207 }
208
209 #[test]
210 fn clean_config_has_no_conflicts() {
211 let conflicts = solve_toml(
212 r#"
213[peripherals.usart2]
214tx = "PA2"
215rx = "PA3"
216
217[peripherals.spi1]
218mosi = "PA7"
219miso = "PA6"
220sck = "PA5"
221nss = "PA4"
222
223[peripherals.i2c1]
224sda = "PB9"
225scl = "PB8"
226"#,
227 );
228 assert_eq!(
229 conflicts,
230 vec![],
231 "expected clean config, got {conflicts:?}"
232 );
233 }
234
235 #[test]
236 fn detects_pin_collision() {
237 let conflicts = solve_toml(
240 r#"
241[peripherals.spi1]
242mosi = "PA7"
243miso = "PA6"
244sck = "PA5"
245
246[peripherals.tim2]
247channel1 = "PA5"
248"#,
249 );
250 let collisions: Vec<_> = conflicts
251 .iter()
252 .filter(|c| matches!(c, Conflict::PinCollision { .. }))
253 .collect();
254 assert_eq!(collisions.len(), 1, "got {conflicts:?}");
255 if let Conflict::PinCollision { pin, users } = collisions[0] {
256 assert_eq!(pin.to_string(), "PA5");
257 assert_eq!(users.len(), 2);
258 }
259 }
260
261 #[test]
262 fn detects_af_mismatch() {
263 let conflicts = solve_toml(
265 r#"
266[peripherals.usart2]
267tx = "PB0"
268rx = "PA3"
269"#,
270 );
271 assert!(
272 conflicts.iter().any(|c| matches!(
273 c,
274 Conflict::AfMismatch { pin, signal, .. }
275 if pin.to_string() == "PB0" && signal == "TX"
276 )),
277 "got {conflicts:?}"
278 );
279 }
280
281 #[test]
282 fn detects_missing_required_pin() {
283 let conflicts = solve_toml(
285 r#"
286[peripherals.spi1]
287miso = "PA6"
288sck = "PA5"
289"#,
290 );
291 assert!(
292 conflicts.iter().any(|c| matches!(
293 c,
294 Conflict::MissingPin { peripheral, signal, .. }
295 if peripheral == "SPI1" && signal == "MOSI"
296 )),
297 "got {conflicts:?}"
298 );
299 }
300
301 #[test]
302 fn missing_optional_pin_is_not_a_conflict() {
303 let conflicts = solve_toml(
305 r#"
306[peripherals.spi1]
307mosi = "PA7"
308miso = "PA6"
309sck = "PA5"
310"#,
311 );
312 assert_eq!(conflicts, vec![]);
313 }
314
315 #[test]
316 fn detects_clock_domain_disabled() {
317 let conflicts = solve_toml(
319 r#"
320[clocks]
321apb2 = false
322
323[peripherals.spi1]
324mosi = "PA7"
325miso = "PA6"
326sck = "PA5"
327"#,
328 );
329 assert!(
330 conflicts.iter().any(|c| matches!(
331 c,
332 Conflict::ClockDomainDisabled { peripheral, bus }
333 if peripheral == "SPI1" && *bus == Bus::Apb2
334 )),
335 "got {conflicts:?}"
336 );
337 }
338
339 #[test]
340 fn invalid_pin_name_reported() {
341 let conflicts = solve_toml(
342 r#"
343[peripherals.usart2]
344tx = "PZ9"
345rx = "PA3"
346"#,
347 );
348 assert!(
349 conflicts
350 .iter()
351 .any(|c| matches!(c, Conflict::InvalidPin { value, .. } if value == "PZ9")),
352 "got {conflicts:?}"
353 );
354 }
355}