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 PeripheralUnavailable { peripheral: String, family: String },
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
59pub struct SignalRef {
60 pub peripheral: String,
61 pub signal: String,
62}
63
64impl fmt::Display for SignalRef {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 write!(f, "{}_{}", self.peripheral, self.signal)
67 }
68}
69
70impl fmt::Display for Conflict {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 match self {
73 Conflict::PinCollision { pin, users } => {
74 let names: Vec<String> = users.iter().map(ToString::to_string).collect();
75 write!(
76 f,
77 "pin collision on {pin}: assigned to {}",
78 names.join(" and ")
79 )
80 }
81 Conflict::AfMismatch {
82 pin,
83 peripheral,
84 signal,
85 } => write!(
86 f,
87 "AF mismatch: {pin} has no alternate function for {peripheral}_{signal} on this MCU"
88 ),
89 Conflict::InvalidPin {
90 peripheral,
91 key,
92 value,
93 } => write!(
94 f,
95 "invalid pin: {peripheral}.{key} = {value:?} is not a valid pin name"
96 ),
97 Conflict::MissingPin {
98 peripheral,
99 key,
100 signal,
101 } => write!(
102 f,
103 "missing required pin: {peripheral} needs a {key} pin ({peripheral}_{signal})"
104 ),
105 Conflict::ClockDomainDisabled { peripheral, bus } => write!(
106 f,
107 "clock domain disabled: {peripheral} is on {} but [clocks].{} = false",
108 bus.name(),
109 bus.name().to_ascii_lowercase()
110 ),
111 Conflict::PeripheralUnavailable { peripheral, family } => {
112 write!(f, "peripheral {peripheral} is not available on {family}")
113 }
114 }
115 }
116}
117
118pub fn solve(config: &Config, db: &Database) -> Vec<Conflict> {
121 let mut conflicts = Vec::new();
122 let mut pin_users: BTreeMap<Pin, Vec<SignalRef>> = BTreeMap::new();
124
125 for (instance, table) in &config.peripherals {
127 let Some(roles) = model::roles_for(instance) else {
128 continue;
130 };
131 let peripheral = model::peripheral_name(instance);
132
133 if !db.has_peripheral(&peripheral) {
137 conflicts.push(Conflict::PeripheralUnavailable {
138 peripheral: peripheral.clone(),
139 family: config.device.family.clone(),
140 });
141 continue;
142 }
143
144 if let Some(bus) = model::peripheral_bus(&peripheral) {
146 let enabled = match bus {
147 Bus::Ahb1 => config.clocks.ahb1,
148 Bus::Apb1 => config.clocks.apb1,
149 Bus::Apb2 => config.clocks.apb2,
150 };
151 if !enabled {
152 conflicts.push(Conflict::ClockDomainDisabled {
153 peripheral: peripheral.clone(),
154 bus,
155 });
156 }
157 }
158
159 for role in roles {
160 match table.pin_str(role.key) {
161 None => {
162 if role.required {
163 conflicts.push(Conflict::MissingPin {
164 peripheral: peripheral.clone(),
165 key: role.key.to_string(),
166 signal: role.signal.to_string(),
167 });
168 }
169 }
170 Some(value) => {
171 let Ok(pin) = Pin::from_str(value) else {
172 conflicts.push(Conflict::InvalidPin {
173 peripheral: peripheral.clone(),
174 key: role.key.to_string(),
175 value: value.to_string(),
176 });
177 continue;
178 };
179 if db.find_af(pin, &peripheral, role.signal).is_none() {
181 conflicts.push(Conflict::AfMismatch {
182 pin,
183 peripheral: peripheral.clone(),
184 signal: role.signal.to_string(),
185 });
186 }
187 pin_users.entry(pin).or_default().push(SignalRef {
191 peripheral: peripheral.clone(),
192 signal: role.signal.to_string(),
193 });
194 }
195 }
196 }
197 }
198
199 for (pin, mut users) in pin_users {
202 if users.len() > 1 {
203 users.sort();
204 conflicts.push(Conflict::PinCollision { pin, users });
205 }
206 }
207
208 conflicts
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214 use crate::config;
215
216 fn db() -> Database {
217 Database::f446re()
218 }
219
220 fn solve_toml(text: &str) -> Vec<Conflict> {
221 let cfg = config::parse(text).unwrap();
222 solve(&cfg, &db())
223 }
224
225 #[test]
226 fn clean_config_has_no_conflicts() {
227 let conflicts = solve_toml(
228 r#"
229[peripherals.usart2]
230tx = "PA2"
231rx = "PA3"
232
233[peripherals.spi1]
234mosi = "PA7"
235miso = "PA6"
236sck = "PA5"
237nss = "PA4"
238
239[peripherals.i2c1]
240sda = "PB9"
241scl = "PB8"
242"#,
243 );
244 assert_eq!(
245 conflicts,
246 vec![],
247 "expected clean config, got {conflicts:?}"
248 );
249 }
250
251 #[test]
252 fn detects_pin_collision() {
253 let conflicts = solve_toml(
256 r#"
257[peripherals.spi1]
258mosi = "PA7"
259miso = "PA6"
260sck = "PA5"
261
262[peripherals.tim2]
263channel1 = "PA5"
264"#,
265 );
266 let collisions: Vec<_> = conflicts
267 .iter()
268 .filter(|c| matches!(c, Conflict::PinCollision { .. }))
269 .collect();
270 assert_eq!(collisions.len(), 1, "got {conflicts:?}");
271 if let Conflict::PinCollision { pin, users } = collisions[0] {
272 assert_eq!(pin.to_string(), "PA5");
273 assert_eq!(users.len(), 2);
274 }
275 }
276
277 #[test]
278 fn detects_af_mismatch() {
279 let conflicts = solve_toml(
281 r#"
282[peripherals.usart2]
283tx = "PB0"
284rx = "PA3"
285"#,
286 );
287 assert!(
288 conflicts.iter().any(|c| matches!(
289 c,
290 Conflict::AfMismatch { pin, signal, .. }
291 if pin.to_string() == "PB0" && signal == "TX"
292 )),
293 "got {conflicts:?}"
294 );
295 }
296
297 #[test]
298 fn detects_missing_required_pin() {
299 let conflicts = solve_toml(
301 r#"
302[peripherals.spi1]
303miso = "PA6"
304sck = "PA5"
305"#,
306 );
307 assert!(
308 conflicts.iter().any(|c| matches!(
309 c,
310 Conflict::MissingPin { peripheral, signal, .. }
311 if peripheral == "SPI1" && signal == "MOSI"
312 )),
313 "got {conflicts:?}"
314 );
315 }
316
317 #[test]
318 fn missing_optional_pin_is_not_a_conflict() {
319 let conflicts = solve_toml(
321 r#"
322[peripherals.spi1]
323mosi = "PA7"
324miso = "PA6"
325sck = "PA5"
326"#,
327 );
328 assert_eq!(conflicts, vec![]);
329 }
330
331 #[test]
332 fn detects_clock_domain_disabled() {
333 let conflicts = solve_toml(
335 r#"
336[clocks]
337apb2 = false
338
339[peripherals.spi1]
340mosi = "PA7"
341miso = "PA6"
342sck = "PA5"
343"#,
344 );
345 assert!(
346 conflicts.iter().any(|c| matches!(
347 c,
348 Conflict::ClockDomainDisabled { peripheral, bus }
349 if peripheral == "SPI1" && *bus == Bus::Apb2
350 )),
351 "got {conflicts:?}"
352 );
353 }
354
355 #[test]
356 fn detects_peripheral_unavailable_on_family() {
357 let cfg = config::parse(
361 "[device]\nfamily = \"STM32F411RE\"\n\n[peripherals.uart4]\ntx = \"PA0\"\nrx = \"PA1\"\n",
362 )
363 .unwrap();
364 let conflicts = solve(&cfg, &Database::f411re());
365 assert_eq!(
366 conflicts,
367 vec![Conflict::PeripheralUnavailable {
368 peripheral: "UART4".to_string(),
369 family: "STM32F411RE".to_string(),
370 }],
371 "got {conflicts:?}"
372 );
373 }
374
375 #[test]
376 fn invalid_pin_name_reported() {
377 let conflicts = solve_toml(
378 r#"
379[peripherals.usart2]
380tx = "PZ9"
381rx = "PA3"
382"#,
383 );
384 assert!(
385 conflicts
386 .iter()
387 .any(|c| matches!(c, Conflict::InvalidPin { value, .. } if value == "PZ9")),
388 "got {conflicts:?}"
389 );
390 }
391}