1use std::collections::HashMap;
20
21use consortium_cfg_common::config::{Config, MemoryRegion, Party, VisibleCore};
22use consortium_cfg_common::database::{ChipDatabase, HardwareDoorbellPeripheral};
23
24use consortium_data::DataError;
25
26use crate::{database, error::CfgError};
27
28#[derive(Debug, Clone)]
30pub struct Session {
31 pub config: Config,
33 pub database: ChipDatabase,
35}
36
37impl Session {
38 pub fn from_toml(content: &str) -> Result<Self, CfgError> {
49 consortium_log::trace!("parsing Consortium.toml ({} bytes)", content.len());
50 let config: Config = toml::from_str(content).map_err(|err| {
51 consortium_log::trace!("manifest TOML parse failed: {}", err);
52 CfgError::Manifest(err.to_string())
53 })?;
54 consortium_log::trace!("resolving chip database for '{}'", config.profile.chip);
55 let database = database::load(&config.profile.chip)?;
56
57 let session = Self { config, database };
58 session.validate()?;
59 Ok(session)
60 }
61
62 pub fn validate(&self) -> Result<(), CfgError> {
66 let chip = &self.config.profile.chip;
67 consortium_log::trace!("validating manifest against chip '{}'", chip);
68
69 self.validate_memory()?;
70 self.validate_ipc()?;
71 self.validate_dbg()?;
72 self.validate_memory_access()?;
75 self.validate_remoteproc()?;
76
77 for (name, peripheral) in &self.config.peripheral {
78 consortium_log::trace!(
79 "validating peripheral '{}' owned by '{}'",
80 name,
81 peripheral.owner
82 );
83 if !self.is_core(peripheral.owner) {
84 consortium_log::trace!("peripheral '{}' owner not visible on chip", name);
85 return Err(CfgError::OwnerNotVisible(
86 name.clone(),
87 peripheral.owner.to_string(),
88 chip.clone(),
89 ));
90 }
91
92 if let VisibleCore::Mcore(_) = peripheral.owner {
100 let core = peripheral.owner.to_string();
101 match crate::chipmap::load(chip, &core) {
102 Ok(map) => {
103 if !map.contains_peripheral(name) {
104 consortium_log::trace!(
105 "peripheral '{}' absent from chip map for core '{}'",
106 name,
107 core
108 );
109 return Err(CfgError::PeripheralNotOnCore {
110 name: name.clone(),
111 core,
112 chip: chip.clone(),
113 });
114 }
115 }
116 Err(DataError::ChipMapUnavailable { .. }) => {
117 consortium_log::trace!(
118 "no chip map bundled for core '{}'; skipping peripheral accessibility check",
119 core
120 );
121 }
122 Err(err) => {
123 consortium_log::trace!("chip map load failed for core '{}': {}", core, err);
124 return Err(err.into());
125 }
126 }
127 }
128 }
129
130 self.validate_pins()
131 }
132
133 fn validate_pins(&self) -> Result<(), CfgError> {
141 let chip = &self.config.profile.chip;
142 if self.config.peripheral.values().all(|p| p.pin.is_empty()) {
143 return Ok(());
144 }
145 let map = match crate::signal::load(chip) {
146 Ok(map) => map,
147 Err(DataError::SignalMapUnavailable(_)) => {
148 consortium_log::trace!("no signal map bundled for '{}'; pins unchecked", chip);
149 return Ok(());
150 }
151 Err(err) => return Err(err.into()),
152 };
153 let package = map.package_of(chip);
154
155 let mut names: Vec<&String> = self.config.peripheral.keys().collect();
156 names.sort();
157 let mut taken: HashMap<String, String> = HashMap::new();
158 for name in names {
159 let mut pins: Vec<(&String, &String)> =
160 self.config.peripheral[name].pin.iter().collect();
161 pins.sort();
162 for (signal, pad) in pins {
163 let function = format!("{name}_{signal}").to_ascii_uppercase();
164 let routes = map.signal_routes(name, signal);
165 if routes.is_empty() {
166 return Err(CfgError::PinSignalUnknown {
167 peripheral: name.clone(),
168 signal: signal.clone(),
169 function,
170 chip: chip.clone(),
171 });
172 }
173 if !routes.iter().any(|r| r.pad.eq_ignore_ascii_case(pad)) {
174 let mut allowed: Vec<&str> = routes.iter().map(|r| r.pad.as_str()).collect();
175 allowed.sort_unstable();
176 allowed.dedup();
177 return Err(CfgError::PinCannotCarry {
178 peripheral: name.clone(),
179 signal: signal.clone(),
180 pad: pad.clone(),
181 function,
182 allowed: allowed.join(", "),
183 });
184 }
185 if let Some(package) = package {
186 let bonded = map
187 .pad(pad)
188 .and_then(|p| p.packages.as_ref())
189 .is_none_or(|packages| packages.contains(&package.name));
190 if !bonded {
191 return Err(CfgError::PinNotInPackage {
192 peripheral: name.clone(),
193 signal: signal.clone(),
194 pad: pad.clone(),
195 package: package.name.clone(),
196 chip: chip.clone(),
197 });
198 }
199 }
200 let owner = format!("{name}.{signal}");
201 if let Some(first) = taken.insert(pad.to_ascii_uppercase(), owner.clone()) {
202 return Err(CfgError::PinConflict {
203 pad: pad.clone(),
204 first,
205 second: owner,
206 });
207 }
208 }
209 }
210 Ok(())
211 }
212
213 fn validate_remoteproc(&self) -> Result<(), CfgError> {
214 let layouts = self.lower_remoteproc()?;
215 if !layouts.is_empty() && self.config.dts.mode.is_overlay() {
216 return Err(CfgError::Manifest(
217 "remoteproc deployment requires `[dts] mode = \"merge\"`".into(),
218 ));
219 }
220 let shared =
221 self.config
222 .dbg
223 .cores
224 .iter()
225 .map(|(core, entry)| (format!("dbg.{core}"), entry.memory))
226 .chain(self.config.ipc.channels.iter().enumerate().filter_map(
227 |(index, channel)| {
228 channel
229 .memory()
230 .map(|memory| (format!("ipc.channel[{index}].config.memory"), memory))
231 },
232 ));
233
234 for layout in layouts.values() {
235 let windows = [
236 ("vector".to_string(), layout.vector.window),
237 ("flash".to_string(), layout.flash),
238 ("shmem".to_string(), layout.shmem.window),
239 ("ram".to_string(), layout.ram),
240 ]
241 .into_iter()
242 .chain(
243 layout
244 .regions
245 .iter()
246 .map(|region| (format!("regions.{}", region.name), region.window)),
247 );
248 for (window_name, window) in windows {
249 if !window.reserve {
250 continue;
251 }
252 let window_end = window.end().expect("layout is validated");
253 for (name, region) in shared.clone() {
254 let start = u64::from(region.base);
255 let end = start + u64::from(region.length);
256 if window.origin < end && start < window_end {
257 return Err(CfgError::Manifest(format!(
258 "[remoteproc.{}].{window_name} overlaps {name} in memory",
259 layout.core
260 )));
261 }
262 }
263 }
264 }
265 Ok(())
266 }
267
268 fn validate_ipc(&self) -> Result<(), CfgError> {
270 let chip = &self.config.profile.chip;
271 let mut used_channels: HashMap<(String, u32), Party> = HashMap::new();
272
273 for channel in &self.config.ipc.channels {
274 consortium_log::trace!(
275 "validating ipc channel '{}' on doorbell '{}'",
276 channel.name,
277 channel.doorbell
278 );
279
280 if let Some(memory) = channel.memory() {
281 let required =
282 consortium_ipc_transport_memory::required_region_len(2, channel.mtu as usize);
283 if (memory.length as usize) < required {
284 consortium_log::trace!(
285 "ipc channel '{}' region length {:#x} is smaller than aligned layout {:#x}",
286 channel.name,
287 memory.length,
288 required
289 );
290 return Err(CfgError::IpcRegionTooSmall {
291 channel: channel.name.clone(),
292 length: memory.length,
293 mtu: channel.mtu,
294 required,
295 });
296 }
297 }
298
299 if channel.cache() {
300 let controller = [channel.secondary, channel.primary]
301 .into_iter()
302 .map(Party::core)
303 .find(|core| *core != VisibleCore::Ap);
304 if controller
305 != Some(VisibleCore::Mcore(
306 consortium_cfg_common::config::McoreName::Cm7,
307 ))
308 {
309 let core = controller
310 .map_or_else(|| VisibleCore::Ap.to_string(), |core| core.to_string());
311 consortium_log::trace!(
312 "ipc channel '{}' enables caching on unsupported core '{}'",
313 channel.name,
314 core
315 );
316 return Err(CfgError::IpcCacheUnsupported {
317 channel: channel.name.clone(),
318 core,
319 });
320 }
321 }
322 let doorbell = self.doorbell(&channel.doorbell).ok_or_else(|| {
323 consortium_log::trace!("unknown doorbell '{}'", channel.doorbell);
324 self.unknown_doorbell(&channel.doorbell)
325 })?;
326
327 for (party, endpoint) in &channel.endpoints {
328 if !self.is_party(*party) {
329 consortium_log::trace!("ipc endpoint party '{}' not visible on chip", party);
330 return Err(CfgError::IpcEndpointNotVisible(
331 channel.doorbell.clone(),
332 party.to_string(),
333 chip.clone(),
334 ));
335 }
336
337 if !doorbell.parties.iter().any(|entry| entry.core == *party) {
338 consortium_log::trace!("doorbell '{}' has no party '{}'", doorbell.name, party);
339 return Err(self.doorbell_party_unavailable(doorbell, *party));
340 }
341
342 match (channel.ack(), endpoint.ack_chan) {
346 (true, None) => {
347 consortium_log::trace!(
348 "ipc channel '{}' party '{}' sets config.ack without an ack_chan",
349 channel.name,
350 party
351 );
352 return Err(CfgError::IpcAckChannelMissing {
353 channel: channel.name.clone(),
354 party: party.to_string(),
355 });
356 }
357 (false, Some(_)) => {
358 consortium_log::trace!(
359 "ipc channel '{}' party '{}' declares ack_chan without config.ack",
360 channel.name,
361 party
362 );
363 return Err(CfgError::IpcAckChannelUnexpected {
364 channel: channel.name.clone(),
365 party: party.to_string(),
366 });
367 }
368 _ => {}
369 }
370
371 for chan in [Some(endpoint.chan), endpoint.ack_chan]
372 .into_iter()
373 .flatten()
374 {
375 if chan >= doorbell.channels {
376 consortium_log::trace!(
377 "doorbell '{}' channel {} out of bounds ({} channels)",
378 doorbell.name,
379 chan,
380 doorbell.channels
381 );
382 return Err(CfgError::DoorbellChannelUnavailable {
383 doorbell: doorbell.name.clone(),
384 channel: chan,
385 channels: doorbell.channels,
386 chip: chip.clone(),
387 trm: self.database.trm.clone(),
388 page: doorbell.channel_page,
389 });
390 }
391
392 let key = (doorbell.name.to_ascii_lowercase(), chan);
393 if let Some(first_party) = used_channels.insert(key, *party) {
394 consortium_log::trace!(
395 "doorbell '{}' channel {} claimed by both '{}' and '{}'",
396 doorbell.name,
397 chan,
398 first_party,
399 party
400 );
401 return Err(CfgError::DoorbellChannelDuplicate {
402 doorbell: doorbell.name.clone(),
403 channel: chan,
404 first_party: first_party.to_string(),
405 second_party: party.to_string(),
406 });
407 }
408 }
409 }
410 }
411
412 Ok(())
413 }
414
415 fn validate_dbg(&self) -> Result<(), CfgError> {
419 let chip = &self.config.profile.chip;
420
421 for core in self.config.dbg.cores.keys() {
422 consortium_log::trace!("validating dbg logger on core '{}'", core);
423 if *core == VisibleCore::Ap {
424 consortium_log::trace!("dbg core is the AP host, which cannot be a logger");
425 return Err(CfgError::DbgHostNotLoggable);
426 }
427
428 if !self.is_core(*core) {
429 consortium_log::trace!("dbg core '{}' not visible on chip", core);
430 return Err(CfgError::DbgCoreNotVisible {
431 core: core.to_string(),
432 chip: chip.clone(),
433 });
434 }
435 }
436
437 Ok(())
438 }
439
440 fn shared_windows(&self) -> Vec<(String, MemoryRegion, bool, Vec<VisibleCore>)> {
443 let mut regions = Vec::new();
444
445 for (core, entry) in &self.config.dbg.cores {
447 regions.push((
448 format!("dbg.{core}"),
449 entry.memory,
450 false,
451 vec![*core, VisibleCore::Ap],
452 ));
453 }
454
455 for (index, channel) in self.config.ipc.channels.iter().enumerate() {
456 if let Some(memory) = channel.memory() {
457 regions.push((
458 format!("ipc.channel[{index}].config.memory"),
459 memory,
460 channel.cache(),
461 vec![channel.primary.core(), channel.secondary.core()],
462 ));
463 }
464 }
465 regions
466 }
467
468 fn validate_memory(&self) -> Result<(), CfgError> {
471 let regions = self.shared_windows();
472
473 for (name, region, cache, _) in ®ions {
474 self.validate_memory_region(name, *region, *cache)?;
475 }
476
477 for (left_index, (left_name, left, _, _)) in regions.iter().enumerate() {
478 let left_end = left.base + left.length;
479 for (right_name, right, _, _) in regions.iter().skip(left_index + 1) {
480 let right_end = right.base + right.length;
481 if left.base < right_end && right.base < left_end {
482 consortium_log::trace!(
483 "shared-memory regions '{}' and '{}' overlap",
484 left_name,
485 right_name
486 );
487 return Err(CfgError::RegionOverlap {
488 a: left_name.clone(),
489 a_base: left.base,
490 a_end: left_end,
491 b: right_name.clone(),
492 b_base: right.base,
493 b_end: right_end,
494 });
495 }
496 }
497 }
498
499 Ok(())
500 }
501
502 fn validate_memory_region(
503 &self,
504 name: &str,
505 region: MemoryRegion,
506 cache: bool,
507 ) -> Result<(), CfgError> {
508 consortium_log::trace!(
509 "validating region '{}' base {:#x} length {:#x}",
510 name,
511 region.base,
512 region.length
513 );
514 let Some(end) = region.base.checked_add(region.length) else {
517 consortium_log::trace!("region '{}' end address overflows", name);
518 return Err(self.region_out_of_bounds(name, region.base, region.base));
519 };
520
521 if region.length == 0 {
522 consortium_log::trace!("region '{}' has zero length", name);
523 return Err(CfgError::RegionEmpty {
524 region: name.to_string(),
525 });
526 }
527
528 if cache {
529 let requirement = if !region.base.is_multiple_of(64) {
533 Some("base must be aligned to 64 bytes")
534 } else if !region.length.is_multiple_of(64) {
535 Some("size must be a multiple of 64 bytes")
536 } else {
537 None
538 };
539 if let Some(requirement) = requirement {
540 consortium_log::trace!("region '{}' not cache-line aligned: {}", name, requirement);
541 return Err(CfgError::RegionNotCacheAligned {
542 region: name.to_string(),
543 base: region.base,
544 length: region.length,
545 requirement,
546 });
547 }
548 } else {
549 let mpu_requirement = if !region.length.is_power_of_two() {
553 Some("size must be a power of two")
554 } else if region.length < 32 {
555 Some("size must be at least 32 bytes")
556 } else if !region.base.is_multiple_of(region.length) {
557 Some("base must be aligned to the region size")
558 } else {
559 None
560 };
561 if let Some(requirement) = mpu_requirement {
562 consortium_log::trace!("region '{}' not MPU-aligned: {}", name, requirement);
563 return Err(CfgError::RegionNotMpuAligned {
564 region: name.to_string(),
565 base: region.base,
566 length: region.length,
567 requirement,
568 });
569 }
570 }
571
572 let contained = self
573 .database
574 .memory
575 .regions
576 .iter()
577 .any(|window| window.contains(region.base, region.length));
578 if !contained {
579 consortium_log::trace!(
580 "region '{}' ({:#x}..{:#x}) not contained in any memory window",
581 name,
582 region.base,
583 end
584 );
585 return Err(self.region_out_of_bounds(name, region.base, end));
586 }
587
588 Ok(())
589 }
590
591 fn validate_memory_access(&self) -> Result<(), CfgError> {
596 for (name, region, _, cores) in self.shared_windows() {
597 let Some(window) = self
598 .database
599 .memory
600 .regions
601 .iter()
602 .find(|window| window.contains(region.base, region.length))
603 else {
604 continue;
606 };
607 self.validate_window_access(&name, window, &cores)?;
608 }
609 Ok(())
610 }
611
612 fn validate_window_access(
613 &self,
614 name: &str,
615 window: &consortium_cfg_common::database::MemoryRegion,
616 cores: &[VisibleCore],
617 ) -> Result<(), CfgError> {
618 if let Some(core) = cores.iter().find(|core| !window.visible.contains(core)) {
619 consortium_log::trace!(
620 "region '{}' lies in '{}', which core '{}' cannot access",
621 name,
622 window.name,
623 core
624 );
625 return Err(CfgError::RegionNotVisible {
626 region: name.to_string(),
627 window: window.name.clone(),
628 core: core.to_string(),
629 visible: window
630 .visible
631 .iter()
632 .map(ToString::to_string)
633 .collect::<Vec<_>>()
634 .join(", "),
635 chip: self.config.profile.chip.clone(),
636 });
637 }
638
639 Ok(())
640 }
641
642 fn region_out_of_bounds(&self, region: &str, base: u32, end: u32) -> CfgError {
645 let pages = self
646 .database
647 .memory
648 .trm_page
649 .iter()
650 .map(u32::to_string)
651 .collect::<Vec<_>>()
652 .join(", ");
653
654 CfgError::RegionOutOfBounds {
655 region: region.to_string(),
656 base,
657 end,
658 chip: self.config.profile.chip.clone(),
659 trm: self.database.trm.clone(),
660 pages,
661 }
662 }
663
664 fn unknown_doorbell(&self, name: &str) -> CfgError {
667 let available = self
668 .database
669 .peripheral
670 .doorbells
671 .iter()
672 .map(|entry| format!("{} (TRM p. {})", entry.name, entry.page))
673 .collect::<Vec<_>>()
674 .join(", ");
675
676 CfgError::UnknownDoorbell {
677 name: name.to_string(),
678 chip: self.config.profile.chip.clone(),
679 trm: self.database.trm.clone(),
680 available,
681 }
682 }
683
684 pub(crate) fn doorbell(&self, name: &str) -> Option<&HardwareDoorbellPeripheral> {
686 self.database
687 .peripheral
688 .doorbells
689 .iter()
690 .find(|entry| entry.name.eq_ignore_ascii_case(name))
691 }
692
693 fn doorbell_party_unavailable(
696 &self,
697 doorbell: &HardwareDoorbellPeripheral,
698 party: Party,
699 ) -> CfgError {
700 let available = doorbell
701 .parties
702 .iter()
703 .map(|entry| {
704 format!(
705 "{} (TRM memory p. {}, IRQ p. {})",
706 entry.core, entry.page_memory, entry.page_irq
707 )
708 })
709 .collect::<Vec<_>>()
710 .join(", ");
711
712 CfgError::DoorbellPartyUnavailable {
713 doorbell: doorbell.name.clone(),
714 party: party.to_string(),
715 chip: self.config.profile.chip.clone(),
716 available,
717 trm: self.database.trm.clone(),
718 page: doorbell.party_page,
719 }
720 }
721
722 fn is_core(&self, core: VisibleCore) -> bool {
725 match core {
726 VisibleCore::Ap => true,
727 VisibleCore::Mcore(name) => self
728 .database
729 .cp
730 .iter()
731 .any(|cp| cp.name.eq_ignore_ascii_case(name.as_str())),
732 VisibleCore::Rcore(name) => self
733 .database
734 .cp
735 .iter()
736 .any(|cp| cp.name.eq_ignore_ascii_case(name.as_str())),
737 VisibleCore::Rvcore(name) => self
738 .database
739 .cp
740 .iter()
741 .any(|cp| cp.name.eq_ignore_ascii_case(&name.to_string())),
742 }
743 }
744
745 fn is_party(&self, party: Party) -> bool {
747 self.is_core(party.core())
748 }
749}
750
751#[cfg(test)]
752mod tests {
753 use super::*;
754
755 const MANIFEST: &str = include_str!("../Consortium.example.stm32mp257.toml");
756
757 fn cached_manifest(
758 chip: &str,
759 secondary: &str,
760 doorbell: &str,
761 base: u32,
762 length: u32,
763 ) -> String {
764 format!(
765 r#"
766 [profile]
767 chip = "{chip}"
768 shared = "./shared"
769 deploy_root = "/opt/consortium"
770
771 [[ipc.channel]]
772 name = "probe"
773 doorbell = "{doorbell}"
774 pattern = "memory"
775 primary = "linux"
776 secondary = "{secondary}"
777 party.linux = {{ chan = 0, type = "Ping" }}
778 party.{secondary} = {{ chan = 1, type = "Pong" }}
779 config.memory = {{ base = {base}, length = {length} }}
780 config.cache = true
781 mtu = 128
782 "#
783 )
784 }
785
786 #[test]
787 fn loads_example_manifest() {
788 let session = Session::from_toml(MANIFEST).expect("example manifest should load");
789
790 assert_eq!(session.config.profile.chip, "stm32mp257fak3");
791 assert_eq!(session.database.name, "STM32MP25");
792 assert_eq!(session.config.ipc.channels.len(), 2);
793 assert_eq!(session.config.ipc.channels[0].doorbell, "ipcc1");
794 assert_eq!(
795 session.config.ipc.channels[0].endpoints[&Party::AcoreNonSecure].chan,
796 0
797 );
798 assert_eq!(
799 session.config.ipc.channels[0].endpoints[&"cm33".parse::<Party>().unwrap()].r#type,
800 "MotionControl"
801 );
802 assert_eq!(
803 session.config.ipc.channels[0].primary,
804 Party::AcoreNonSecure
805 );
806 assert_eq!(
807 session.config.ipc.channels[0].secondary,
808 "cm33".parse::<Party>().unwrap()
809 );
810 assert_eq!(session.config.endpoints[&Party::AcoreNonSecure], "./app");
811 assert_eq!(
812 session
813 .config
814 .tee
815 .as_ref()
816 .unwrap()
817 .uuid
818 .to_string()
819 .to_uppercase(),
820 "48551DD5-74B2-4D24-BF9D-A09F02F5C56F"
821 );
822
823 assert_eq!(
825 session.config.dbg.mode,
826 consortium_cfg_common::config::DbgMode::Auto
827 );
828 let cm33_dbg = session.config.dbg.cores[&"cm33".parse::<VisibleCore>().unwrap()];
829 assert_eq!(cm33_dbg.mtu, 64);
830 assert_eq!(cm33_dbg.memory.length, 0x2000);
831
832 let i2c1 = &session.config.peripheral["i2c1"];
833 assert_eq!(i2c1.owner, VisibleCore::Ap);
834 assert_eq!(i2c1.pin["sda"], "PA2");
835
836 let spi1 = &session.config.peripheral["spi1"];
837 assert_eq!(spi1.owner, "cm33".parse().unwrap());
838 }
839
840 #[test]
841 fn rejects_unknown_doorbell() {
842 let manifest = r#"
843 [profile]
844 chip = "stm32mp257fak3"
845 shared = "./shared"
846 deploy_root = "/opt/consortium"
847
848 [[ipc.channel]]
849 name = "probe"
850 doorbell = "nope"
851 pattern = "memory"
852 primary = "linux"
853 secondary = "cm33"
854 party.linux = { chan = 0, type = "Ping" }
855 party.cm33 = { chan = 1, type = "Pong" }
856 config.memory = { base = 0x00040000, length = 0x00008000 }
857 mtu = 128
858 "#;
859
860 let err = Session::from_toml(manifest).unwrap_err();
861 assert!(matches!(err, CfgError::UnknownDoorbell { .. }));
862 assert!(err.to_string().contains("HSEM (TRM p. 1306)"));
864 }
865
866 #[test]
867 fn rejects_doorbell_party_unavailable() {
868 let manifest = r#"
869 [profile]
870 chip = "stm32mp257fak3"
871 shared = "./shared"
872 deploy_root = "/opt/consortium"
873
874 [[ipc.channel]]
875 name = "probe"
876 doorbell = "ipcc1"
877 pattern = "memory"
878 primary = "linux"
879 secondary = "cm0p"
880 party.cm0p = { chan = 0, type = "PowerMode" }
881 config.memory = { base = 0x00040000, length = 0x00008000 }
882 mtu = 128
883 "#;
884
885 let err = Session::from_toml(manifest).unwrap_err();
886 assert!(matches!(err, CfgError::DoorbellPartyUnavailable { .. }));
887 assert!(err.to_string().contains("Configured parties: linux"));
888 assert!(err.to_string().contains("TRM"));
889 }
890
891 #[test]
892 fn rejects_unavailable_doorbell_channel() {
893 let manifest = r#"
894 [profile]
895 chip = "stm32mp257fak3"
896 shared = "./shared"
897 deploy_root = "/opt/consortium"
898
899 [[ipc.channel]]
900 name = "probe"
901 doorbell = "ipcc1"
902 pattern = "memory"
903 primary = "linux"
904 secondary = "cm33"
905 party.linux = { chan = 32, type = "Ping" }
906 config.memory = { base = 0x00040000, length = 0x00008000 }
907 mtu = 128
908 "#;
909
910 assert!(matches!(
912 Session::from_toml(manifest),
913 Err(CfgError::DoorbellChannelUnavailable { .. })
914 ));
915 }
916
917 #[test]
918 fn rejects_duplicate_doorbell_channel() {
919 let manifest = r#"
920 [profile]
921 chip = "stm32mp257fak3"
922 shared = "./shared"
923 deploy_root = "/opt/consortium"
924
925 [[ipc.channel]]
926 name = "probe"
927 doorbell = "ipcc1"
928 pattern = "memory"
929 primary = "linux"
930 secondary = "cm33"
931 party.linux = { chan = 0, type = "Ping" }
932 party.cm33 = { chan = 0, type = "Pong" }
933 config.memory = { base = 0x00040000, length = 0x00008000 }
934 mtu = 128
935 "#;
936
937 assert!(matches!(
938 Session::from_toml(manifest),
939 Err(CfgError::DoorbellChannelDuplicate { .. })
940 ));
941 }
942
943 #[test]
947 fn rejects_region_outside_sram() {
948 let manifest = r#"
949 [profile]
950 chip = "stm32mp257fak3"
951 shared = "./shared"
952 deploy_root = "/opt/consortium"
953
954 [[ipc.channel]]
955 name = "probe"
956 doorbell = "ipcc1"
957 pattern = "memory"
958 primary = "linux"
959 secondary = "cm33"
960 party.linux = { chan = 0, type = "Ping" }
961 config.memory = { base = 0x00400000, length = 0x00080000 }
962 mtu = 128
963 "#;
964
965 assert!(matches!(
966 Session::from_toml(manifest),
967 Err(CfgError::RegionOutOfBounds { .. })
968 ));
969 }
970
971 #[test]
973 fn rejects_non_power_of_two_region() {
974 let manifest = r#"
975 [profile]
976 chip = "stm32mp257fak3"
977 shared = "./shared"
978 deploy_root = "/opt/consortium"
979
980 [[ipc.channel]]
981 name = "probe"
982 doorbell = "ipcc1"
983 pattern = "memory"
984 primary = "linux"
985 secondary = "cm33"
986 party.linux = { chan = 0, type = "Ping" }
987 config.memory = { base = 0x00400000, length = 0x00006000 }
988 mtu = 128
989 "#;
990
991 assert!(matches!(
992 Session::from_toml(manifest),
993 Err(CfgError::RegionNotMpuAligned { .. })
994 ));
995 }
996
997 fn imx95_ack_manifest(linux_ack: &str, cm7_ack: &str, ack: bool) -> String {
1000 format!(
1001 r#"
1002 [profile]
1003 chip = "mimx9596xvt"
1004 shared = "./shared"
1005 deploy_root = "/opt/consortium"
1006
1007 [[ipc.channel]]
1008 name = "sensor"
1009 doorbell = "mu7"
1010 pattern = "memory"
1011 primary = "linux"
1012 secondary = "cm7"
1013 party.linux = {{ chan = 6, {linux_ack} type = "SensorData" }}
1014 party.cm7 = {{ chan = 7, {cm7_ack} type = "MotionControl" }}
1015 config.memory = {{ base = 0x88300000, length = 0x4000 }}
1016 config.ack = {ack}
1017 mtu = 128
1018 "#
1019 )
1020 }
1021
1022 #[test]
1023 fn accepts_ack_channels_alongside_the_data_lines() {
1024 let manifest = imx95_ack_manifest("ack_chan = 0,", "ack_chan = 1,", true);
1025 Session::from_toml(&manifest).expect("distinct ack lines should be valid");
1026 }
1027
1028 #[test]
1031 fn rejects_ack_without_an_ack_channel() {
1032 let manifest = imx95_ack_manifest("", "ack_chan = 1,", true);
1033 let err = Session::from_toml(&manifest).unwrap_err();
1034 assert!(matches!(err, CfgError::IpcAckChannelMissing { .. }));
1035 assert!(err.to_string().contains("ack_chan"));
1036 }
1037
1038 #[test]
1041 fn rejects_an_ack_channel_without_the_capability() {
1042 let manifest = imx95_ack_manifest("ack_chan = 0,", "ack_chan = 1,", false);
1043 assert!(matches!(
1044 Session::from_toml(&manifest),
1045 Err(CfgError::IpcAckChannelUnexpected { .. })
1046 ));
1047 }
1048
1049 #[test]
1053 fn rejects_an_ack_channel_that_aliases_a_data_line() {
1054 let manifest = imx95_ack_manifest("ack_chan = 7,", "ack_chan = 1,", true);
1055 assert!(matches!(
1056 Session::from_toml(&manifest),
1057 Err(CfgError::DoorbellChannelDuplicate { channel: 7, .. })
1058 ));
1059 }
1060
1061 #[test]
1063 fn rejects_an_out_of_range_ack_channel() {
1064 let manifest = imx95_ack_manifest("ack_chan = 0,", "ack_chan = 64,", true);
1065 assert!(matches!(
1066 Session::from_toml(&manifest),
1067 Err(CfgError::DoorbellChannelUnavailable { channel: 64, .. })
1068 ));
1069 }
1070
1071 #[test]
1072 fn accepts_cached_non_power_of_two_region() {
1073 let manifest = cached_manifest("mimx9596xvt", "cm7", "mu7", 0x8830_0000, 0x6000);
1074 Session::from_toml(&manifest).expect("cache-line-aligned cached region should be valid");
1075 }
1076
1077 #[test]
1078 fn rejects_region_too_small_for_aligned_slot_pair() {
1079 let manifest = cached_manifest("mimx9596xvt", "cm7", "mu7", 0x8830_0000, 0x200);
1080 let err = Session::from_toml(&manifest).unwrap_err();
1081 assert!(matches!(
1082 err,
1083 CfgError::IpcRegionTooSmall {
1084 length: 0x200,
1085 mtu: 128,
1086 required: 0x24c,
1087 ..
1088 }
1089 ));
1090 assert!(err.to_string().contains("bidirectional slot pair"));
1091 }
1092
1093 #[test]
1094 fn accepts_smallest_cache_aligned_region_for_slot_pair() {
1095 let manifest = cached_manifest("mimx9596xvt", "cm7", "mu7", 0x8830_0000, 0x280);
1096 Session::from_toml(&manifest)
1097 .expect("0x280 bytes contains both aligned 128-byte-MTU slots");
1098 }
1099
1100 #[test]
1101 fn rejects_cached_region_with_partial_cache_line() {
1102 for (base, length) in [(0x8830_0000, 0x4020), (0x8830_0020, 0x4000)] {
1103 let manifest = cached_manifest("mimx9596xvt", "cm7", "mu7", base, length);
1104 assert!(matches!(
1105 Session::from_toml(&manifest),
1106 Err(CfgError::RegionNotCacheAligned { .. })
1107 ));
1108 }
1109 }
1110
1111 #[test]
1112 fn rejects_cached_region_on_cacheless_controller() {
1113 let manifest = cached_manifest("mimx9352", "cm33", "mu1", 0x0048_8000, 0x4000);
1114 let err = Session::from_toml(&manifest).unwrap_err();
1115 assert!(matches!(err, CfgError::IpcCacheUnsupported { .. }));
1116 assert!(err.to_string().contains("cm33"));
1117 }
1118
1119 #[test]
1122 fn rejects_a_region_the_controller_cannot_access() {
1123 let manifest = r#"
1124 [profile]
1125 chip = "mimx9596xvt"
1126 shared = "./shared"
1127 deploy_root = "/opt/consortium"
1128
1129 [[ipc.channel]]
1130 name = "probe"
1131 doorbell = "mu7"
1132 pattern = "memory"
1133 primary = "linux"
1134 secondary = "cm7"
1135 party.linux = { chan = 0, type = "Ping" }
1136 party.cm7 = { chan = 1, type = "Pong" }
1137 config.memory = { base = 0x004C0000, length = 0x4000 }
1138 mtu = 128
1139 "#;
1140 let err = Session::from_toml(manifest).unwrap_err();
1141 assert!(
1142 matches!(&err, CfgError::RegionNotVisible { window, core, .. }
1143 if window == "OCRAM" && core == "cm7"),
1144 "{err}"
1145 );
1146 }
1147
1148 #[test]
1151 fn rejects_a_dbg_ring_the_ap_cannot_drain() {
1152 let manifest = r#"
1153 [profile]
1154 chip = "mimx9596xvt"
1155 shared = "./shared"
1156 deploy_root = "/opt/consortium"
1157
1158 [dbg.cm7]
1159 memory = { base = 0x004C0000, length = 0x2000 }
1160 mtu = 64
1161 "#;
1162 assert!(matches!(
1163 Session::from_toml(manifest),
1164 Err(CfgError::RegionNotVisible { .. })
1165 ));
1166 }
1167
1168 #[test]
1171 fn rejects_misaligned_region() {
1172 let manifest = r#"
1174 [profile]
1175 chip = "stm32mp257fak3"
1176 shared = "./shared"
1177 deploy_root = "/opt/consortium"
1178
1179 [[ipc.channel]]
1180 name = "probe"
1181 doorbell = "ipcc1"
1182 pattern = "memory"
1183 primary = "linux"
1184 secondary = "cm33"
1185 party.linux = { chan = 0, type = "Ping" }
1186 config.memory = { base = 0x00402000, length = 0x00004000 }
1187 mtu = 128
1188 "#;
1189
1190 let err = Session::from_toml(manifest).unwrap_err();
1191 assert!(matches!(err, CfgError::RegionNotMpuAligned { .. }));
1192 assert!(err.to_string().contains("aligned"));
1193 }
1194
1195 #[test]
1197 fn rejects_subminimal_region() {
1198 let manifest = r#"
1199 [profile]
1200 chip = "stm32mp257fak3"
1201 shared = "./shared"
1202 deploy_root = "/opt/consortium"
1203
1204 [[ipc.channel]]
1205 name = "probe"
1206 doorbell = "ipcc1"
1207 pattern = "memory"
1208 primary = "linux"
1209 secondary = "cm33"
1210 party.linux = { chan = 0, type = "Ping" }
1211 config.memory = { base = 0x00400000, length = 0x10 }
1212 mtu = 128
1213 "#;
1214
1215 assert!(matches!(
1216 Session::from_toml(manifest),
1217 Err(CfgError::RegionNotMpuAligned { .. })
1218 ));
1219 }
1220
1221 #[test]
1223 fn rejects_dbg_on_application_processor() {
1224 let manifest = r#"
1225 [profile]
1226 chip = "stm32mp257fak3"
1227 shared = "./shared"
1228 deploy_root = "/opt/consortium"
1229
1230 [dbg.ap]
1231 memory = { base = 0x00044000, length = 0x00002000 }
1232 mtu = 64
1233 "#;
1234
1235 assert!(matches!(
1236 Session::from_toml(manifest),
1237 Err(CfgError::DbgHostNotLoggable)
1238 ));
1239 }
1240
1241 #[test]
1243 fn rejects_dbg_on_unknown_core() {
1244 let manifest = r#"
1245 [profile]
1246 chip = "stm32mp257fak3"
1247 shared = "./shared"
1248 deploy_root = "/opt/consortium"
1249
1250 [dbg.cm7]
1251 memory = { base = 0x00044000, length = 0x00002000 }
1252 mtu = 64
1253 "#;
1254
1255 assert!(matches!(
1256 Session::from_toml(manifest),
1257 Err(CfgError::DbgCoreNotVisible { .. })
1258 ));
1259 }
1260
1261 #[test]
1262 fn rejects_overlapping_regions() {
1263 let manifest = r#"
1264 [profile]
1265 chip = "stm32mp257fak3"
1266 shared = "./shared"
1267 deploy_root = "/opt/consortium"
1268
1269 [dbg.cm33]
1270 memory = { base = 0x00044000, length = 0x00002000 }
1271 mtu = 64
1272
1273 [[ipc.channel]]
1274 name = "probe"
1275 doorbell = "ipcc1"
1276 pattern = "memory"
1277 primary = "linux"
1278 secondary = "cm33"
1279 party.linux = { chan = 0, type = "Ping" }
1280 config.memory = { base = 0x00040000, length = 0x00008000 }
1281 mtu = 128
1282 "#;
1283
1284 assert!(matches!(
1285 Session::from_toml(manifest),
1286 Err(CfgError::RegionOverlap { .. })
1287 ));
1288 }
1289
1290 #[test]
1292 fn accepts_peripheral_present_on_core() {
1293 let manifest = r#"
1294 [profile]
1295 chip = "stm32mp257fak3"
1296 shared = "./shared"
1297 deploy_root = "/opt/consortium"
1298
1299 [[ipc.channel]]
1300 name = "probe"
1301 doorbell = "ipcc1"
1302 pattern = "memory"
1303 primary = "linux"
1304 secondary = "cm33"
1305 party.linux = { chan = 0, type = "Ping" }
1306 config.memory = { base = 0x00040000, length = 0x00002000 }
1307 mtu = 128
1308
1309 [peripheral.i2c1]
1310 owner = "cm33"
1311 clock = 64000000
1312 frequency = 100000
1313 "#;
1314
1315 assert!(Session::from_toml(manifest).is_ok());
1316 }
1317
1318 #[test]
1321 fn rejects_peripheral_absent_on_core() {
1322 let manifest = r#"
1323 [profile]
1324 chip = "stm32mp257fak3"
1325 shared = "./shared"
1326 deploy_root = "/opt/consortium"
1327
1328 [[ipc.channel]]
1329 name = "probe"
1330 doorbell = "ipcc1"
1331 pattern = "memory"
1332 primary = "linux"
1333 secondary = "cm33"
1334 party.linux = { chan = 0, type = "Ping" }
1335 config.memory = { base = 0x00040000, length = 0x00002000 }
1336 mtu = 128
1337
1338 [peripheral.nonexistent99]
1339 owner = "cm33"
1340 "#;
1341
1342 assert!(matches!(
1343 Session::from_toml(manifest),
1344 Err(CfgError::PeripheralNotOnCore { .. })
1345 ));
1346 }
1347
1348 const IMX952_MANIFEST: &str = include_str!("../Consortium.example.mimx952.toml");
1349
1350 #[test]
1354 fn loads_imx952_example_manifest() {
1355 let session = Session::from_toml(IMX952_MANIFEST).expect("i.MX 952 example should load");
1356 assert_eq!(session.database.name, "MIMX952");
1357 assert!(session.config.ipc.channels.is_empty());
1358 }
1359
1360 #[test]
1365 fn imx952_rejects_ipc_until_the_reference_manual() {
1366 let manifest = r#"
1367 [profile]
1368 chip = "mimx9529avzxn"
1369 shared = "./shared"
1370 deploy_root = "/opt/consortium"
1371
1372 [[ipc.channel]]
1373 name = "probe"
1374 doorbell = "mu7"
1375 pattern = "memory"
1376 primary = "linux"
1377 secondary = "cm7"
1378 party.linux = { chan = 0, type = "Ping" }
1379 party.cm7 = { chan = 1, type = "Pong" }
1380 config.memory = { base = 0x00480000, length = 0x4000 }
1381 mtu = 128
1382 "#;
1383 assert!(matches!(
1384 Session::from_toml(manifest),
1385 Err(CfgError::RegionOutOfBounds { .. } | CfgError::UnknownDoorbell { .. })
1386 ));
1387 }
1388
1389 #[test]
1391 fn imx952_rejects_a_dbg_ring_without_sram() {
1392 let manifest = r#"
1393 [profile]
1394 chip = "mimx9529avzxn"
1395 shared = "./shared"
1396 deploy_root = "/opt/consortium"
1397
1398 [dbg.cm7]
1399 memory = { base = 0x00480000, length = 0x2000 }
1400 mtu = 64
1401 "#;
1402 assert!(matches!(
1403 Session::from_toml(manifest),
1404 Err(CfgError::RegionOutOfBounds { .. })
1405 ));
1406 }
1407
1408 const MP235_MANIFEST: &str = include_str!("../Consortium.example.stm32mp235.toml");
1409
1410 fn mp235_manifest(doorbell: &str, secondary: &str, base: u32, extra: &str) -> String {
1412 format!(
1413 r#"
1414 [profile]
1415 chip = "stm32mp235fak3"
1416 shared = "./shared"
1417 deploy_root = "/opt/consortium"
1418
1419 [[ipc.channel]]
1420 name = "probe"
1421 doorbell = "{doorbell}"
1422 pattern = "memory"
1423 primary = "linux"
1424 secondary = "{secondary}"
1425 party.linux = {{ chan = 0, type = "Ping" }}
1426 party.{secondary} = {{ chan = 1, type = "Pong" }}
1427 config.memory = {{ base = {base}, length = 0x4000 }}
1428 mtu = 128
1429
1430 {extra}
1431 "#
1432 )
1433 }
1434
1435 #[test]
1436 fn loads_mp235_example_manifest() {
1437 let session = Session::from_toml(MP235_MANIFEST).expect("MP235 example should load");
1438 assert_eq!(session.database.name, "STM32MP23");
1439 assert_eq!(session.config.ipc.channels.len(), 1);
1440 }
1441
1442 #[test]
1444 fn mp235_rejects_ipcc2() {
1445 let manifest = mp235_manifest("ipcc2", "cm33", 0x0006_4000, "");
1446 assert!(matches!(
1447 Session::from_toml(&manifest),
1448 Err(CfgError::UnknownDoorbell { .. })
1449 ));
1450 }
1451
1452 #[test]
1453 fn mp235_rejects_a_cm0p_endpoint() {
1454 let manifest = mp235_manifest("ipcc1", "cm0p", 0x0006_4000, "");
1455 assert!(Session::from_toml(&manifest).is_err());
1456 }
1457
1458 #[test]
1460 fn mp235_rejects_an_mp25_only_peripheral() {
1461 let i2c = |name: &str| format!("[peripheral.{name}]\nowner = \"cm33\"");
1462 let absent = mp235_manifest("ipcc1", "cm33", 0x0006_4000, &i2c("i2c3"));
1463 assert!(matches!(
1464 Session::from_toml(&absent),
1465 Err(CfgError::PeripheralNotOnCore { .. })
1466 ));
1467 let present = mp235_manifest("ipcc1", "cm33", 0x0006_4000, &i2c("i2c8"));
1468 assert!(Session::from_toml(&present).is_ok());
1469 }
1470
1471 #[test]
1473 fn mp235_rejects_an_lpsram_window() {
1474 let manifest = mp235_manifest("ipcc1", "cm33", 0x000C_4000, "");
1475 assert!(Session::from_toml(&manifest).is_err());
1476 }
1477
1478 fn pins_manifest(chip: &str, peripherals: &str) -> String {
1480 format!(
1481 r#"
1482 [profile]
1483 chip = "{chip}"
1484 shared = "./shared"
1485 deploy_root = "/opt/consortium"
1486
1487 {peripherals}
1488 "#
1489 )
1490 }
1491
1492 #[test]
1495 fn accepts_pins_the_pad_can_carry() {
1496 let manifest = pins_manifest(
1497 "stm32mp257fak3",
1498 "[peripheral.i2c1]\nowner = \"ap\"\npin.sda = \"PA2\"\npin.scl = \"pa3\"",
1499 );
1500 assert!(Session::from_toml(&manifest).is_ok());
1501 }
1502
1503 #[test]
1504 fn rejects_a_pad_that_cannot_carry_the_signal() {
1505 let manifest = pins_manifest(
1506 "stm32mp257fak3",
1507 "[peripheral.i2c1]\nowner = \"ap\"\npin.sda = \"PB0\"",
1508 );
1509 match Session::from_toml(&manifest) {
1510 Err(CfgError::PinCannotCarry { pad, allowed, .. }) => {
1511 assert_eq!(pad, "PB0");
1512 assert_eq!(allowed, "PA2, PI1, PJ10");
1513 }
1514 other => panic!("expected PinCannotCarry, got {other:?}"),
1515 }
1516 }
1517
1518 #[test]
1521 fn rejects_a_signal_the_peripheral_does_not_have() {
1522 let manifest = pins_manifest(
1523 "mimx9596xvt",
1524 "[peripheral.lpspi2]\nowner = \"ap\"\npin.miso = \"UART1_RXD\"",
1525 );
1526 assert!(matches!(
1527 Session::from_toml(&manifest),
1528 Err(CfgError::PinSignalUnknown { function, .. }) if function == "LPSPI2_MISO"
1529 ));
1530 let manifest = pins_manifest(
1531 "mimx9596xvt",
1532 "[peripheral.lpspi2]\nowner = \"ap\"\npin.sin = \"UART1_RXD\"",
1533 );
1534 assert!(Session::from_toml(&manifest).is_ok());
1535 }
1536
1537 #[test]
1540 fn rejects_a_pad_the_package_does_not_bond_out() {
1541 let peripheral = "[peripheral.i2c1]\nowner = \"ap\"\npin.sda = \"PJ10\"";
1542 match Session::from_toml(&pins_manifest("stm32mp257fak3", peripheral)) {
1543 Err(CfgError::PinNotInPackage { package, .. }) => assert_eq!(package, "VFBGA424"),
1544 other => panic!("expected PinNotInPackage, got {other:?}"),
1545 }
1546 assert!(Session::from_toml(&pins_manifest("stm32mp257fai3", peripheral)).is_ok());
1547 assert!(Session::from_toml(&pins_manifest("stm32mp257", peripheral)).is_ok());
1548 }
1549
1550 #[test]
1552 fn rejects_a_pad_assigned_twice() {
1553 let manifest = pins_manifest(
1554 "stm32mp257fak3",
1555 "[peripheral.spi1]\nowner = \"ap\"\npin.mosi = \"PE4\"\n\n\
1556 [peripheral.fdcan1]\nowner = \"ap\"\npin.tx = \"PE4\"",
1557 );
1558 match Session::from_toml(&manifest) {
1559 Err(CfgError::PinConflict { first, second, .. }) => {
1560 assert_eq!(
1561 (first.as_str(), second.as_str()),
1562 ("fdcan1.tx", "spi1.mosi")
1563 );
1564 }
1565 other => panic!("expected PinConflict, got {other:?}"),
1566 }
1567 }
1568}