Skip to main content

consortium_cfg/
bridge.rs

1// Copyright 2026 Ethan Wu
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//
15// SPDX-License-Identifier: Apache-2.0
16
17//! Lowers a validated [`Session`] into the `components` IR.
18//!
19//! The IR types in [`crate::components`] each own their own code generation
20//! ([`ShmNode::initialize`](crate::components::ipc_shm::ShmNode::initialize),
21//! [`BellA::nvic_enable`](crate::components::bella::BellA::nvic_enable),
22//! [`ToUio::to_uio`]). This module builds those structs from the parsed
23//! `Consortium.toml` plus its resolved chip database, so the builder can drive
24//! codegen without hand-assembling addresses, doorbell metadata, and UIO ids.
25//!
26//! A channel names its transport primary and secondary parties explicitly
27//! (`primary = ...` / `secondary = ...`); each must have a declared endpoint.
28//!
29//! The AP-side address layout is fixed here so the firmware-, NVIC-, and
30//! Linux-DTS outputs agree on UIO ids and mmap indices:
31//!
32//! - Every device — the aggregate IPC shared-memory device, one device per
33//!   distinct doorbell, and the aggregate dbg-ring device — is assigned its
34//!   `uioN` id by ascending mapped base, so the ids follow the memory map the
35//!   kernel enumerates.
36//! - Within the IPC device, a region's `mmap_id` is its index in
37//!   channel-declaration order; a doorbell always exposes its register window as
38//!   `mmap_id` 0.
39
40use std::collections::HashMap;
41
42use consortium_cfg_common::config::{McoreName, Party, VisibleCore};
43use consortium_cfg_common::database::{
44    ChipDatabase, DoorbellWindow, HardwareDoorbellPeripheral, MemoryRegionType,
45};
46
47use crate::components::address::{Address, ApAddress};
48use crate::components::bella::{BellA, HardwareDoorbell};
49use crate::components::dbg::{DebugLogs, DebugRing, ElfLocation};
50use crate::components::ipc_shm::{Channel, DoorbellMeta, DoorbellRingerSide, ShmNode, Transceiver};
51use crate::components::irq::InterruptLine;
52use crate::components::memory_to_uio::SharedWindow;
53use crate::components::peripheral::{PeripheralInstance, PeripheralKind, Peripherals};
54use crate::components::region::Region;
55use crate::components::remoteproc::{
56    LinkerRegionAttributes, LinkerSectionBinding, RemoteprocMemoryLayout, RemoteprocMemoryWindow,
57    RemoteprocNamedRegion,
58};
59use crate::components::uio::{ToUio, UioBacking, UioDevice};
60use consortium_data::DataError;
61
62use crate::{Session, error::CfgError};
63
64/// The `components` IR lowered from a [`Session`].
65///
66/// Built once per session and shared across target cores; the codegen entry
67/// points ([`ShmNode::initialize`], [`BellA::nvic_enable`]) select the relevant
68/// core at emit time.
69#[derive(Debug, Clone)]
70pub struct LoweredIr {
71    /// Every IPC transceiver, one per `[[ipc.channel]]`.
72    pub ipc_shm: ShmNode,
73    /// Every distinct hardware doorbell, carrying per-core IRQ maps.
74    pub bella: BellA,
75    /// Linux UIO devices, named for their DTS node. Index 0 is the aggregate
76    /// shared-memory device (`ipc-shm`); the rest are one per doorbell.
77    pub uio: Vec<(String, UioDevice)>,
78    /// Per-coprocessor debug log rings: each logging core's generated `init()`
79    /// passes its ring to the logger, and the AP's maps every ring to read it.
80    pub dbg: DebugLogs,
81    /// Owned peripherals (I2C, GPIO) to bring up per controller core.
82    pub peripherals: Peripherals,
83    /// Peripheral instance names each controller core's chip map lists, keyed by
84    /// core. Code generation can only name a HAL peripheral singleton for an
85    /// instance that appears here; cores with no bundled map have no entry, and
86    /// fall back to address-based construction.
87    pub core_peripherals: HashMap<VisibleCore, Vec<String>>,
88    /// Per-remote-core linker/ELF/DTS memory contracts.
89    pub remoteproc: HashMap<VisibleCore, RemoteprocMemoryLayout>,
90    /// Whether controller cores get the sealed Embassy time driver, timer
91    /// vector, and `defmt::timestamp!` (mirrors `[profile] time_driver`).
92    pub time_driver: bool,
93}
94
95impl LoweredIr {
96    /// A minimal IR carrying only a UIO device list, for tests that exercise the
97    /// device-tree rendering (which reads nothing else).
98    ///
99    /// Test-only; hidden from the public API surface but reachable from other
100    /// crates' tests (e.g. `consortium-builder`).
101    #[doc(hidden)]
102    pub fn empty_with_uio(uio: Vec<(String, UioDevice)>) -> Self {
103        use crate::components::bella::BellA;
104        use crate::components::dbg::DebugLogs;
105        use crate::components::ipc_shm::ShmNode;
106        use crate::components::peripheral::Peripherals;
107        use consortium_cfg_common::config::DbgMode;
108        use std::collections::HashMap;
109
110        Self {
111            ipc_shm: ShmNode::new(Vec::new()),
112            bella: BellA { bells: Vec::new() },
113            uio,
114            dbg: DebugLogs::new(HashMap::new(), DbgMode::default()),
115            peripherals: Peripherals {
116                hal_crate: String::new(),
117                instances: Vec::new(),
118            },
119            core_peripherals: HashMap::new(),
120            remoteproc: HashMap::new(),
121            time_driver: true,
122        }
123    }
124
125    /// Generates the per-core IPC initialization module.
126    ///
127    /// Every core gets channel markers, instance bindings, and a transport
128    /// installer restricted to the transceivers it participates in.
129    ///
130    /// The AP additionally emits `Config`, platform `Peripherals`, and a
131    /// fallible synchronous `init` that opens the declared [`UioDevice`]s.
132    ///
133    /// [`UioDevice`]: crate::components::uio::UioDevice
134    pub fn initialize(&self, core: VisibleCore, ty_crate: &syn::Path) -> proc_macro2::TokenStream {
135        use quote::{format_ident, quote};
136
137        let uio_ident = format_ident!("uio");
138
139        // A core only constructs the transceivers it participates in.
140        let participating: Vec<_> = self
141            .ipc_shm
142            .transceivers()
143            .iter()
144            .filter(|t| t.channel_primary.0 == core || t.channel_secondary.0 == core)
145            .cloned()
146            .collect();
147        // Peripheral singletons only exist for cores with a bundled chip map;
148        // the doorbell falls back to its mapped-base constructor elsewhere.
149        let available = self
150            .core_peripherals
151            .get(&core)
152            .map(Vec::as_slice)
153            .unwrap_or_default();
154        let endpoints = crate::components::ipc_shm::initialize_instances(
155            &ShmNode::new(participating.clone()),
156            core,
157            &uio_ident,
158            ty_crate,
159            &self.peripherals.hal_crate,
160            available,
161        );
162
163        match core {
164            // The AP falls through to its UIO/`init` bring-up below.
165            VisibleCore::Ap => {}
166            // Cortex-M controllers additionally get an `init` entry point that
167            // programs the MPU and NVIC before constructing the endpoints.
168            VisibleCore::Mcore(name) => {
169                let doorbells =
170                    ShmNode::new(participating.clone()).doorbell_singletons(core, available);
171                return self.controller_init(core, name, &participating, endpoints, doorbells);
172            }
173            // Other controller families (Cortex-R, RISC-V) have no Cortex-M
174            // MPU/NVIC weave yet; emit just the endpoint module.
175            _ => return endpoints,
176        }
177
178        // `UioDevice::open_named(name, base, enable_irq)` for each declared
179        // device, in `self.uio` order — the order the generated `uio[i]` indices
180        // refer to. The kernel's `uioN` numbering is probe order and differs
181        // between device trees, so each device is looked up by the
182        // `linux,uio-name` its DTS node carries, with its first window's
183        // physical base as the fallback key. Opened handles are Arc-owned by
184        // platform resources and cloned into each transport that uses one of
185        // their mappings.
186        let opens = self.uio.iter().map(|(name, device)| {
187            let enable_irq = device.irq.is_some();
188            let base = u64::from(device.mmap.first().map_or(0, |(base, _)| *base));
189            quote! {
190                ::std::sync::Arc::new(
191                    ::consortium_runtime_app::uio::UioDevice::open_named(
192                        #name,
193                        #base,
194                        #enable_irq && config.uio_irqs,
195                    )?
196                )
197            }
198        });
199        // Literal device count for the generated `init` trace line.
200        let uio_count = self.uio.len();
201
202        // The AP host console for the coprocessor debug rings, if any. Its field
203        // is declared *before* `uio` so its threads/handles drop (and stop
204        // touching the mappings) before the `UioDevice`s unmap them.
205        let host = self.dbg.initialize_host(&uio_ident);
206        let (dbg_field, dbg_field_name, dbg_init, dbg_error_arm) = match &host {
207            Some(host) => {
208                let field = &host.field;
209                let name = &host.field_name;
210                let init = &host.init;
211                (
212                    quote! { #field, },
213                    quote! { #name, },
214                    quote! { #init },
215                    quote! {
216                        Dbg(::consortium_runtime_app::dbg_console::DbgConsoleError),
217                    },
218                )
219            }
220            None => (quote! {}, quote! {}, quote! {}, quote! {}),
221        };
222        let dbg_from = if host.is_some() {
223            quote! {
224                impl ::core::convert::From<::consortium_runtime_app::dbg_console::DbgConsoleError>
225                    for ConsortiumInitError
226                {
227                    fn from(e: ::consortium_runtime_app::dbg_console::DbgConsoleError) -> Self {
228                        ConsortiumInitError::Dbg(e)
229                    }
230                }
231            }
232        } else {
233            quote! {}
234        };
235
236        quote! {
237            #endpoints
238
239            /// Aggregate bring-up error for [`init`].
240            ///
241            /// The variant payloads are surfaced through `Debug` in the
242            /// consuming binary (a downstream crate this module is `include!`d
243            /// into), which dead-code analysis cannot see, so allow the
244            /// otherwise-"never read" fields.
245            #[derive(Debug)]
246            #[allow(dead_code)]
247            enum ConsortiumInitError {
248                /// Opening or mapping a UIO device failed.
249                Uio(::consortium_runtime_app::uio::UioDeviceError),
250                #dbg_error_arm
251            }
252
253            impl ::core::convert::From<::consortium_runtime_app::uio::UioDeviceError>
254                for ConsortiumInitError
255            {
256                fn from(e: ::consortium_runtime_app::uio::UioDeviceError) -> Self {
257                    ConsortiumInitError::Uio(e)
258                }
259            }
260
261            #dbg_from
262
263            /// Runtime bring-up policy. Every enabled manifest step defaults on.
264            #[non_exhaustive]
265            pub struct Config {
266                pub tracing: bool,
267                pub uio_irqs: bool,
268                pub dbg_console: bool,
269            }
270
271            impl ::core::default::Default for Config {
272                fn default() -> Self {
273                    Self { tracing: true, uio_irqs: true, dbg_console: true }
274                }
275            }
276
277            /// Platform resources owned by the application endpoint.
278            ///
279            /// This type deliberately has no `Drop` implementation so fields can
280            /// be moved independently. The debug field is declared first so it
281            /// stops touching mappings before the UIO devices unmap.
282            pub struct Peripherals {
283                #dbg_field
284                pub #uio_ident: ::std::vec::Vec<
285                    ::std::sync::Arc<::consortium_runtime_app::uio::UioDevice>
286                >,
287            }
288
289            fn init(
290                config: Config,
291            ) -> ::core::result::Result<(Peripherals, Channels), ConsortiumInitError> {
292                if config.tracing {
293                    ::consortium_runtime_app::init_tracing();
294                }
295                ::consortium_runtime_app::log::trace!(
296                    "consortium init: opening {} uio device(s)", #uio_count
297                );
298                let #uio_ident = ::std::vec![ #(#opens),* ];
299                ::consortium_runtime_app::log::trace!(
300                    "consortium init: installing ipc endpoints"
301                );
302                __consortium_install_channels(&#uio_ident)?;
303                let channels = Channels::take();
304                #dbg_init
305                ::consortium_runtime_app::log::trace!("consortium init: resources ready");
306                ::core::result::Result::Ok((
307                    Peripherals { #dbg_field_name #uio_ident },
308                    channels,
309                ))
310            }
311
312            /// Error union suitable for `#[consortium_runtime_app::main]`.
313            ///
314            /// `MainOutcome` reports the payload through derived `Debug`, which
315            /// dead-code analysis intentionally does not count as a field read.
316            #[derive(Debug)]
317            #[allow(dead_code)]
318            enum ConsortiumError {
319                Init(ConsortiumInitError),
320                Connect(::consortium_ipc_transport_memory::ConnectError),
321            }
322
323            impl ::core::convert::From<ConsortiumInitError> for ConsortiumError {
324                fn from(e: ConsortiumInitError) -> Self { Self::Init(e) }
325            }
326
327            impl ::core::convert::From<::consortium_ipc_transport_memory::ConnectError>
328                for ConsortiumError
329            {
330                fn from(e: ::consortium_ipc_transport_memory::ConnectError) -> Self {
331                    Self::Connect(e)
332                }
333            }
334        }
335    }
336
337    /// Wraps a Cortex-M endpoint module with its Embassy-shaped resource types
338    /// and synchronous `init`.
339    ///
340    /// The generated `init`:
341    ///
342    /// - resets the CDBG ring via `ConsortiumLogger::init(base, len)` when a
343    ///   `[dbg.<core>]` logger is present, so the first `defmt` frame lands in a
344    ///   fresh descriptor;
345    /// - programs one Normal, shareable, non-cacheable MPU carveout per
346    ///   distinct uncached shared IPC region (cacheable windows retain the
347    ///   default write-back attributes and use explicit maintenance), for the
348    ///   Cortex-M families that ship a [`consortium_runtime_mcu::mpu`] helper
349    ///   (ARMv7-M/ARMv8-M). ARMv6-M (`cm0p`) has no data cache and no helper, so
350    ///   it gets NVIC setup only;
351    /// - invalidates and enables the Cortex-M7 data cache when at least one IPC
352    ///   window uses `config.cache = true`;
353    /// - unmasks every doorbell NVIC line visible to this core (via
354    ///   [`BellA::nvic_enable`](crate::components::bella::BellA::nvic_enable));
355    /// - brings up the sealed Embassy time driver (its timer vector and
356    ///   `defmt::timestamp!` are emitted alongside) unless `[profile]
357    ///   time_driver = false` or this core's chip map lacks the driver's timer
358    ///   (the STM32MP25 CM0+ has no TIM2); and
359    /// - installs each unsplit IPC transport behind its channel instance; and
360    /// - returns the peripheral and channel singleton allowlists.
361    ///
362    /// The platform resource surface is gated to `target_arch = "arm"` so host
363    /// trybuild can compile the portable instance declarations.
364    fn controller_init(
365        &self,
366        core: VisibleCore,
367        name: McoreName,
368        participating: &[Transceiver],
369        endpoints: proc_macro2::TokenStream,
370        doorbell_arguments: Vec<syn::Ident>,
371    ) -> proc_macro2::TokenStream {
372        use quote::quote;
373
374        // One non-cacheable carveout per distinct shared-region base this core
375        // maps. ARMv6-M has no cache to disable, so it configures no regions.
376        let mut mpu_stmts = Vec::new();
377        if mcore_has_mpu_helper(name) {
378            let mut seen = std::collections::BTreeSet::new();
379            for transceiver in participating {
380                // A cached window keeps the core's default write-back attributes;
381                // `CachedRegion` does the maintenance instead. Skipping before the
382                // dedup insert keeps MPU region numbering gap-free.
383                if transceiver.region.cache {
384                    continue;
385                }
386                let Some(base) = transceiver.region.address.cp.get(&core).copied() else {
387                    continue;
388                };
389                if !seen.insert(base) {
390                    continue;
391                }
392                let size = transceiver.region.length;
393                let region = seen.len() as u8 - 1;
394                mpu_stmts.push(quote! {
395                    /// SAFETY: `init` holds exclusive privileged MPU access during
396                    /// bring-up; `base`/`size` are this core's validated shared
397                    /// region from the Consortium hardware layout.
398                    unsafe {
399                        ::consortium_runtime_mcu::mpu::disable_cache(
400                            &mut __core.MPU,
401                            #region,
402                            #base,
403                            #size,
404                        );
405                        ::consortium_runtime_mcu::log::trace!(
406                            "consortium controller init: MPU carveout {} base={:#010x} size={:#010x}",
407                            #region, #base, #size
408                        );
409                    }
410                });
411            }
412        }
413
414        let enables_dcache = name == McoreName::Cm7
415            && participating
416                .iter()
417                .any(|transceiver| transceiver.region.cache);
418        let enable_dcache = if enables_dcache {
419            quote! {
420                // `enable_dcache` is idempotent and invalidates all cache lines
421                // before setting SCB.CCR.DC, so a reset-started CM7 cannot retain
422                // stale data while switching to cached shared-window access.
423                __core.SCB.enable_dcache(&mut __core.CPUID);
424                ::consortium_runtime_mcu::log::trace!(
425                    "consortium controller init: Cortex-M7 data cache enabled"
426                );
427            }
428        } else {
429            quote! {}
430        };
431
432        // MPU carveouts need `MPU`; D-cache bring-up needs `SCB` and `CPUID`.
433        // NVIC unmasking is a free function, so a core needing neither skips the
434        // steal entirely.
435        let steal = if mpu_stmts.is_empty() && !enables_dcache {
436            quote! {}
437        } else {
438            quote! {
439                /// SAFETY: `init` runs once during controller bring-up, in
440                /// privileged mode, before any other code touches the MPU or
441                /// cache-control peripherals.
442                let mut __core = unsafe { ::cortex_m::Peripherals::steal() };
443            }
444        };
445
446        // The doorbell NVIC priority/unpend/enable statements for this core.
447        let nvic = self.bella.nvic_enable(&core, &self.peripherals.hal_crate);
448
449        // Peripheral singleton allowlist and interrupt vectors.
450        let peripherals = self.peripherals.initialize(core);
451        let peripheral_vectors = self.peripherals.vectors(core);
452        let peripheral_arguments = self.peripherals.arguments(core);
453        let has_peripherals = !peripheral_arguments.is_empty();
454        let peripheral_nvic = self.peripherals.nvic(core);
455        let peripheral_value = if has_peripherals {
456            quote! { Peripherals { #(#peripheral_arguments: __p.#peripheral_arguments),* } }
457        } else {
458            quote! { Peripherals {} }
459        };
460
461        // A `[dbg.<core>]` logger means `defmt` is linked: reset the CDBG ring
462        // first thing (before any trace) and emit a `defmt::timestamp!` source.
463        let dbg_ring = self.dbg.ring(core);
464        let dbg_present = dbg_ring.is_some();
465        // Reset the CDBG ring on every boot so a warm reset does not leave a
466        // stale, valid-looking descriptor behind. Must run before the first
467        // `defmt` frame below.
468        let logger_init = dbg_ring
469            .map(|ring| ring.initialize_controller())
470            .unwrap_or_default();
471
472        // Sealed Embassy time driver: `bringup` statement (into `init`), the
473        // timer vector, and the `defmt::timestamp!` source. Empty when
474        // `[profile] time_driver = false`, the chip family has no driver yet, or
475        // this core's chip map does not list the driver's timer.
476        let time = crate::components::time::time_driver_codegen(
477            &self.peripherals.hal_crate,
478            self.time_driver,
479            dbg_present,
480            self.core_peripherals.get(&core).map(Vec::as_slice),
481        );
482        let crate::components::time::TimeDriverCodegen {
483            timestamp,
484            isr: time_isr,
485            bringup: time_bringup,
486        } = time;
487
488        // The chip HAL's singletons are taken once and then moved out field by
489        // field: to the doorbells, the time driver, and `peripherals()`. Cores
490        // that need none of them skip the call, so `init` stays free of an
491        // unused binding (and of the HAL's once-only panic).
492        let hal: syn::Path =
493            syn::parse_str(&self.peripherals.hal_crate).expect("hal_crate must be a valid path");
494        let hal_peripherals =
495            if has_peripherals || !doorbell_arguments.is_empty() || !time_bringup.is_empty() {
496                quote! {
497                    ::consortium_runtime_mcu::log::trace!("before chip init");
498                    let __p = #hal::init();
499                    ::consortium_runtime_mcu::log::trace!("after chip init");
500                }
501            } else {
502                quote! {}
503            };
504
505        // Every vector this core owns goes in one module. cortex-m-rt's
506        // `#[interrupt]` expands to a `interrupt::<NAME>;` existence check, so
507        // the attribute needs an `interrupt` path in scope — importing it here
508        // rather than at the include site keeps the generated file from
509        // depending on (or colliding with) the firmware crate's own `use`.
510        // Vectors are bare-metal only: the host trybuild harness never links the
511        // chip `rt` vector table, matching the `init` gate.
512        let mut vectors = self.bella.vectors(&core, &self.peripherals.hal_crate);
513        vectors.extend(peripheral_vectors);
514        if !time_isr.is_empty() {
515            vectors.insert(0, time_isr);
516        }
517        let vectors = if vectors.is_empty() {
518            quote! {}
519        } else {
520            quote! {
521                /// Interrupt vectors for this endpoint's generated bring-up.
522                #[cfg(target_arch = "arm")]
523                mod __consortium_vectors {
524                    use #hal::interrupt;
525
526                    #(#vectors)*
527                }
528            }
529        };
530
531        quote! {
532            #endpoints
533
534            #timestamp
535            #vectors
536
537            /// Runtime bring-up policy. Enabled manifest steps default on.
538            #[cfg(target_arch = "arm")]
539            #[non_exhaustive]
540            pub struct Config {
541                pub logger: bool,
542                pub mpu: bool,
543                pub nvic: bool,
544                pub time_driver: bool,
545            }
546
547            #[cfg(target_arch = "arm")]
548            impl ::core::default::Default for Config {
549                fn default() -> Self {
550                    Self { logger: true, mpu: true, nvic: true, time_driver: true }
551                }
552            }
553
554            /// Brings up synchronous platform resources and installs each
555            /// unconnected IPC transport into its generated endpoint state.
556            #[cfg(target_arch = "arm")]
557            fn init(config: Config) -> (Peripherals, Channels) {
558                if config.logger { #logger_init }
559                ::consortium_runtime_mcu::log::trace!("consortium controller init: mpu/nvic bring-up");
560                #steal
561                if config.mpu { #(#mpu_stmts)* }
562                #enable_dcache
563                if config.nvic {
564                    #nvic
565                    #peripheral_nvic
566                }
567                #hal_peripherals
568                if config.time_driver { #time_bringup }
569                __consortium_install_channels(#(__p.#doorbell_arguments),*);
570                let channels = Channels::take();
571                let peripherals = #peripheral_value;
572                ::consortium_runtime_mcu::log::trace!("consortium controller init: resources ready");
573                (peripherals, channels)
574            }
575
576            /// Error union suitable for `#[consortium_runtime_mcu::main]`.
577            #[cfg(target_arch = "arm")]
578            #[cfg_attr(target_os = "none", derive(Debug, defmt::Format))]
579            #[cfg_attr(not(target_os = "none"), derive(Debug))]
580            #[allow(dead_code)]
581            enum ConsortiumError {
582                Connect(::consortium_ipc_transport_memory::ConnectError),
583            }
584
585            #[cfg(target_arch = "arm")]
586            impl ::core::convert::From<::consortium_ipc_transport_memory::ConnectError>
587                for ConsortiumError
588            {
589                fn from(e: ::consortium_ipc_transport_memory::ConnectError) -> Self {
590                    Self::Connect(e)
591                }
592            }
593
594            #peripherals
595        }
596    }
597}
598
599/// Whether `name`'s Cortex-M architecture ships a `disable_cache` MPU helper in
600/// [`consortium_runtime_mcu::mpu`]. ARMv7-M (`cm4f`/`cm7`) and ARMv8-M (`cm33`)
601/// do; ARMv6-M (`cm0p`) has no data cache and no helper.
602fn mcore_has_mpu_helper(name: McoreName) -> bool {
603    !matches!(name, McoreName::Cm0p)
604}
605
606/// The chip HAL crate the generated peripheral handles are typed against,
607/// selected by chip family: STM32MP2 uses `consortium-hal-stm32mp2`, and the
608/// i.MX 9 families use `consortium-hal-imx9`.
609fn hal_crate_path(chip: &str) -> String {
610    if chip.to_ascii_lowercase().starts_with("stm32") {
611        "::consortium_hal_stm32mp2".to_string()
612    } else {
613        "::consortium_hal_imx9".to_string()
614    }
615}
616
617impl Session {
618    /// Lowers the validated manifest into the [`components`](crate::components)
619    /// IR.
620    ///
621    /// Assumes the session is already validated by [`Session::from_toml`]:
622    /// doorbells exist, parties are visible, channels are in bounds, and
623    /// regions fit SRAM. Only topology the validator does not constrain is
624    /// rejected here.
625    ///
626    /// # Errors
627    ///
628    /// Returns [`CfgError::Manifest`] when a channel's `primary`/`secondary`
629    /// party has no declared endpoint, when its doorbell name cannot be typed,
630    /// or when a memory-pattern channel has no `config.memory`.
631    pub fn lower(&self) -> Result<LoweredIr, CfgError> {
632        consortium_log::trace!(
633            "lowering manifest: {} ipc channels, {} peripherals",
634            self.config.ipc.channels.len(),
635            self.config.peripheral.len()
636        );
637        let remoteproc = self.lower_remoteproc()?;
638        let mut transceivers = Vec::new();
639        let mut regions: Vec<SharedWindow> = Vec::new();
640
641        // Doorbells in first-appearance order. This only fixes a provisional
642        // `uioN` layout; the whole device list is re-sorted into memory order
643        // after assembly below, which rewrites every recorded id.
644        let mut doorbell_order: Vec<String> = Vec::new();
645        let mut bells: Vec<HardwareDoorbell> = Vec::new();
646
647        for channel in &self.config.ipc.channels {
648            consortium_log::trace!("lowering ipc channel '{}'", channel.name);
649            let primary_party = channel.primary;
650            let secondary_party = channel.secondary;
651            let primary = endpoint(channel, primary_party, "primary")?;
652            let secondary = endpoint(channel, secondary_party, "secondary")?;
653            let primary_core = primary_party.core();
654            let secondary_core = secondary_party.core();
655
656            let meta: DoorbellMeta = channel.doorbell.parse().map_err(|err: CfgError| {
657                consortium_log::trace!(
658                    "cannot type doorbell '{}' for channel '{}': {}",
659                    channel.doorbell,
660                    channel.name,
661                    err
662                );
663                CfgError::Manifest(err.to_string())
664            })?;
665            let db_doorbell = self.doorbell(&channel.doorbell).ok_or_else(|| {
666                consortium_log::trace!(
667                    "unknown doorbell '{}' for channel '{}'",
668                    channel.doorbell,
669                    channel.name
670                );
671                CfgError::Manifest(format!("unknown doorbell '{}'", channel.doorbell))
672            })?;
673
674            let memory = channel.memory().ok_or_else(|| {
675                consortium_log::trace!(
676                    "channel '{}' uses memory pattern but sets no config.memory",
677                    channel.name
678                );
679                CfgError::Manifest(format!(
680                    "ipc channel '{}' uses the memory pattern but sets no config.memory",
681                    channel.name
682                ))
683            })?;
684
685            // Region lives in `uio0`; its mmap slot is its declaration order.
686            let region_mmap = regions.len();
687            regions.push(SharedWindow {
688                memory,
689                cache: channel.cache(),
690            });
691
692            // Reserve / reuse this doorbell's provisional `uioN` device (offset
693            // by 1 for the shared-memory device at pre-sort `uio0`).
694            let doorbell_key = channel.doorbell.to_ascii_lowercase();
695            let doorbell_uio = match doorbell_order.iter().position(|n| *n == doorbell_key) {
696                Some(index) => index + 1,
697                None => {
698                    doorbell_order.push(doorbell_key.clone());
699                    doorbell_order.len()
700                }
701            };
702
703            // The AP resolves addresses through UIO; controller cores embed the
704            // physical base. Each base is the chip's canonical offset; the real
705            // address adds the security-domain alias for the accessing party
706            // (e.g. STM32MP2 maps peripherals non-secure at 0x4000_0000 and
707            // secure at 0x5000_0000). Record both participants that are
708            // controllers, aliased to that party's world.
709            let region_cp =
710                controller_bases([primary_party, secondary_party], memory.base, |secure| {
711                    memory_alias(&self.database, memory.base, secure)
712                });
713            let doorbell_cp = controller_doorbell_bases(
714                db_doorbell,
715                [primary_party, secondary_party],
716                |secure| peripheral_alias(&self.database, secure),
717            );
718
719            let region_address = Address {
720                ap: ApAddress {
721                    uio_id: 0,
722                    mmap_id: region_mmap,
723                },
724                cp: region_cp,
725            };
726            let doorbell_address = Address {
727                ap: ApAddress {
728                    uio_id: doorbell_uio,
729                    mmap_id: 0,
730                },
731                cp: doorbell_cp,
732            };
733
734            let channel_primary = Channel {
735                doorbell_chan: primary.chan as u8,
736                ack_chan: primary.ack_chan.map(|chan| chan as u8),
737                type_name: primary.r#type.clone(),
738                buffer_name: format!("{}_tx_buf", channel.name),
739            };
740            let channel_secondary = Channel {
741                doorbell_chan: secondary.chan as u8,
742                ack_chan: secondary.ack_chan.map(|chan| chan as u8),
743                type_name: secondary.r#type.clone(),
744                buffer_name: format!("{}_rx_buf", channel.name),
745            };
746
747            // Record this doorbell's used channels and the IRQ of each
748            // participating party (keyed by core) for NVIC enablement. Only the
749            // parties on this channel are recorded, so the secure/non-secure
750            // IRQ variant is the one this channel actually uses.
751            record_bell(
752                &mut bells,
753                &doorbell_key,
754                &doorbell_address,
755                meta,
756                [
757                    (
758                        primary_core,
759                        party_irq(db_doorbell, primary_party),
760                        primary.chan as u8,
761                        party_window(db_doorbell, primary_party, DoorbellRingerSide::Primary),
762                    ),
763                    (
764                        secondary_core,
765                        party_irq(db_doorbell, secondary_party),
766                        secondary.chan as u8,
767                        party_window(db_doorbell, secondary_party, DoorbellRingerSide::Secondary),
768                    ),
769                ],
770            );
771
772            transceivers.push(Transceiver {
773                name: channel.name.clone(),
774                doorbell_address,
775                mtu: channel.mtu,
776                codec: channel.codec,
777                region: Region {
778                    r#type: memory_type(&self.database, memory.base),
779                    cache: channel.cache(),
780                    address: region_address,
781                    length: memory.length,
782                },
783                channel_primary: (primary_core, channel_primary),
784                channel_secondary: (secondary_core, channel_secondary),
785                doorbell: meta,
786                doorbell_window: (
787                    party_window(db_doorbell, primary_party, DoorbellRingerSide::Primary),
788                    party_window(db_doorbell, secondary_party, DoorbellRingerSide::Secondary),
789                ),
790            });
791        }
792
793        // Assemble the UIO device list in a provisional order: shared memory
794        // first, then each doorbell. Linux is non-secure, so every mapped window
795        // is offset by the non-secure alias. The final `uioN` ids are fixed by
796        // the memory-order sort after the dbg device is appended.
797        let mut uio = Vec::new();
798        consortium_log::trace!("lowering {} shared-memory regions to UIO", regions.len());
799        if let Some(mut device) = regions.as_slice().to_uio()? {
800            for window in &mut device.mmap {
801                window.0 += memory_alias(&self.database, window.0, false);
802            }
803            uio.push(("ipc-shm".to_string(), device));
804        }
805        let doorbell_alias = peripheral_alias(&self.database, false);
806        for name in &doorbell_order {
807            consortium_log::trace!("lowering doorbell '{}' to UIO", name);
808            let db_doorbell = self.doorbell(name).ok_or_else(|| {
809                consortium_log::trace!("unknown doorbell '{}' during UIO assembly", name);
810                CfgError::Manifest(format!("unknown doorbell '{name}'"))
811            })?;
812            if let Some(mut device) = db_doorbell.to_uio()? {
813                for window in &mut device.mmap {
814                    window.0 += doorbell_alias;
815                }
816                // The interrupt-parent (GIC) label is a chip-level property; the
817                // STM32MP2 root sets no default, so a root-level doorbell node
818                // needs it explicit or its IRQ never binds.
819                device.interrupt_parent = Some(self.database.interrupt_parent.clone());
820                uio.push((name.clone(), device));
821            }
822        }
823
824        // Debug log rings carry no security world (the table is keyed by a bare
825        // core), so each logger embeds its ring through the non-secure SRAM
826        // alias, matching both the firmware's own view of memory and Linux's
827        // (Linux is non-secure too, so the AP maps the same physical address).
828
829        // The AP maps every dbg ring through one aggregate `dbg-shm` UIO device,
830        // appended after the IPC/doorbell devices. Sort by core name so the mmap
831        // window order is deterministic and agrees with the host codegen, which
832        // sorts the same way.
833        let dbg_uio_id = uio.len();
834        let mut dbg_cores: Vec<_> = self.config.dbg.cores.iter().collect();
835        dbg_cores.sort_by_key(|(core, _)| core.to_string());
836
837        let mut dbg_windows = Vec::new();
838        let mut dbg_rings = HashMap::new();
839        for (mmap_id, (core, entry)) in dbg_cores.into_iter().enumerate() {
840            let dbg_alias = memory_alias(&self.database, entry.memory.base, false);
841            let base = u64::from(entry.memory.base) + u64::from(dbg_alias);
842            dbg_windows.push((base as u32, entry.memory.length));
843            dbg_rings.insert(
844                *core,
845                DebugRing {
846                    base,
847                    length: u64::from(entry.memory.length),
848                    mtu: entry.mtu,
849                    ap_uio_id: dbg_uio_id,
850                    ap_mmap_id: mmap_id,
851                    elf: ElfLocation::verbatim(self.endpoint_artifact(*core).unwrap_or_default()),
852                },
853            );
854        }
855        if !dbg_windows.is_empty() {
856            uio.push((
857                "dbg-shm".to_string(),
858                UioDevice {
859                    mmap: dbg_windows,
860                    irq: None,
861                    backing: UioBacking::ReservedMemory,
862                    dma_coherent: Vec::new(),
863                    interrupt_parent: None,
864                    clock: None,
865                },
866            ));
867        }
868
869        // Order every UIO device — shared memory, doorbells, and the aggregate
870        // dbg-ring device alike — by ascending mapped base, so the `uioN` ids
871        // follow the memory map the kernel enumerates rather than the assembly
872        // order above. The provisional ids recorded in the transceiver/doorbell
873        // addresses and dbg rings are then rewritten to their sorted positions.
874        let device_base =
875            |device: &UioDevice| device.mmap.iter().map(|(base, _)| *base).min().unwrap_or(0);
876        let mut remap = vec![0usize; uio.len()];
877        let old_index: HashMap<String, usize> = uio
878            .iter()
879            .enumerate()
880            .map(|(id, (name, _))| (name.clone(), id))
881            .collect();
882        uio.sort_by_key(|(_, device)| device_base(device));
883        for (new_id, (name, _)) in uio.iter().enumerate() {
884            remap[old_index[name]] = new_id;
885        }
886        for transceiver in &mut transceivers {
887            transceiver.region.address.ap.uio_id = remap[transceiver.region.address.ap.uio_id];
888            transceiver.doorbell_address.ap.uio_id = remap[transceiver.doorbell_address.ap.uio_id];
889        }
890        for bell in &mut bells {
891            bell.address.ap.uio_id = remap[bell.address.ap.uio_id];
892        }
893        for ring in dbg_rings.values_mut() {
894            ring.ap_uio_id = remap[ring.ap_uio_id];
895        }
896
897        // The doorbell database records only a raw NVIC position, so recover
898        // each controller-core line's name from that core's chip map — the same
899        // dump the PAC's `Interrupt` enum is generated from, making the name the
900        // variant generated bring-up should spell. A core the PAC's shipped
901        // table does not describe (`has_pac_interrupts`) keeps the raw number,
902        // because there the variant of that name carries another core's line.
903        let mut bella = BellA { bells };
904        let mut maps: HashMap<VisibleCore, Option<crate::chipmap::ChipMap>> = HashMap::new();
905        bella.resolve_variants(|core, number| {
906            let map = maps
907                .entry(core)
908                .or_insert_with(|| {
909                    crate::chipmap::load(&self.config.profile.chip, &core.to_string()).ok()
910                })
911                .as_ref()?;
912            if !map.has_pac_interrupts() {
913                return None;
914            }
915            map.irq_by_number(number).map(|entry| entry.name.clone())
916        });
917
918        let peripherals = self.lower_peripherals()?;
919        consortium_log::trace!(
920            "lowering complete: {} transceivers, {} uio devices, {} peripheral instances",
921            transceivers.len(),
922            uio.len(),
923            peripherals.instances.len()
924        );
925        let core_peripherals =
926            Self::lower_core_peripherals(&self.config.profile.chip, &transceivers, &peripherals);
927        Ok(LoweredIr {
928            ipc_shm: ShmNode::new(transceivers),
929            bella,
930            uio,
931            dbg: DebugLogs::new(dbg_rings, self.config.dbg.mode),
932            peripherals,
933            core_peripherals,
934            remoteproc,
935            time_driver: self.config.profile.time_driver,
936        })
937    }
938
939    /// Applies optional manifest fields to the chip database's remoteproc
940    /// defaults and produces the single layout consumed by every later stage.
941    pub(crate) fn lower_remoteproc(
942        &self,
943    ) -> Result<HashMap<VisibleCore, RemoteprocMemoryLayout>, CfgError> {
944        for core in self.config.remoteproc.keys() {
945            if !self.database.remoteproc.contains_key(core) {
946                return Err(CfgError::Manifest(format!(
947                    "[remoteproc.{core}] has no loader contract for chip '{}'",
948                    self.config.profile.chip
949                )));
950            }
951            let visible: VisibleCore = core.parse().map_err(|_| {
952                CfgError::Manifest(format!("[remoteproc.{core}] is not a visible core name"))
953            })?;
954            if !self
955                .config
956                .endpoints
957                .keys()
958                .any(|party| party.core() == visible)
959            {
960                return Err(CfgError::Manifest(format!(
961                    "[remoteproc.{core}] requires an [endpoints] firmware entry for `{core}`"
962                )));
963            }
964        }
965
966        self.database
967            .remoteproc
968            .iter()
969            .filter_map(|(name, default)| {
970                let core: VisibleCore = match name.parse() {
971                    Ok(core) => core,
972                    Err(_) => {
973                        return Some(Err(CfgError::Manifest(format!(
974                            "chip database remoteproc entry '{name}' is not a visible core"
975                        ))));
976                    }
977                };
978                if !self
979                    .config
980                    .endpoints
981                    .keys()
982                    .any(|party| party.core() == core)
983                {
984                    return None;
985                }
986                let overrides = self.config.remoteproc.get(name);
987                let window = |default: consortium_cfg_common::database::RemoteprocWindow,
988                              override_window: Option<
989                    &consortium_cfg_common::config::RemoteprocWindowOverride,
990                >| {
991                    RemoteprocMemoryWindow {
992                        origin: override_window
993                            .and_then(|value| value.origin)
994                            .unwrap_or(default.origin),
995                        length: override_window
996                            .and_then(|value| value.length)
997                            .unwrap_or(default.length),
998                        reserve: override_window
999                            .and_then(|value| value.reserve)
1000                            .unwrap_or(default.reserve),
1001                    }
1002                };
1003                let regions = overrides
1004                    .into_iter()
1005                    .flat_map(|value| &value.regions)
1006                    .map(|(region_name, region)| RemoteprocNamedRegion {
1007                        name: region_name.clone(),
1008                        attributes: LinkerRegionAttributes::new(
1009                            region.read,
1010                            region.write,
1011                            region.execute,
1012                        ),
1013                        window: RemoteprocMemoryWindow {
1014                            origin: region.origin,
1015                            length: region.length,
1016                            reserve: region.reserve,
1017                        },
1018                        sections: region
1019                            .sections
1020                            .iter()
1021                            .map(|section| LinkerSectionBinding {
1022                                output: section.output.clone(),
1023                                inputs: section.inputs.clone(),
1024                                keep: section.keep,
1025                                insert_after: section.insert_after.clone(),
1026                            })
1027                            .collect(),
1028                    })
1029                    .collect();
1030                let layout = RemoteprocMemoryLayout {
1031                    core,
1032                    compatible: default.compatible.clone(),
1033                    vector_section: default.vector_section.clone(),
1034                    vector_placement: default.vector_placement,
1035                    vector: RemoteprocNamedRegion {
1036                        name: default.vector.name.clone(),
1037                        attributes: LinkerRegionAttributes::new(
1038                            default.vector.read,
1039                            default.vector.write,
1040                            default.vector.execute,
1041                        ),
1042                        window: window(default.vector.window, overrides.map(|value| &value.vector)),
1043                        sections: Vec::new(),
1044                    },
1045                    flash: window(default.flash, overrides.map(|value| &value.flash)),
1046                    shmem: RemoteprocNamedRegion {
1047                        name: default.shmem.name.clone(),
1048                        attributes: LinkerRegionAttributes::new(
1049                            default.shmem.read,
1050                            default.shmem.write,
1051                            default.shmem.execute,
1052                        ),
1053                        window: window(default.shmem.window, overrides.map(|value| &value.shmem)),
1054                        sections: Vec::new(),
1055                    },
1056                    ram: window(default.ram, overrides.map(|value| &value.ram)),
1057                    regions,
1058                };
1059                if let Err(error) = layout.validate() {
1060                    return Some(Err(CfgError::Manifest(format!(
1061                        "[remoteproc.{name}] is invalid: {error}"
1062                    ))));
1063                }
1064                Some(Ok((core, layout)))
1065            })
1066            .collect()
1067    }
1068
1069    /// The peripheral instance names each controller core's bundled chip map
1070    /// lists, which is exactly the set code generation may name as a HAL
1071    /// peripheral singleton. Cores with no bundled map get no entry, and their
1072    /// generated code keeps the address-based construction.
1073    fn lower_core_peripherals(
1074        chip: &str,
1075        transceivers: &[Transceiver],
1076        peripherals: &Peripherals,
1077    ) -> HashMap<VisibleCore, Vec<String>> {
1078        // Every controller core code generation runs for: a party to some
1079        // transceiver, or the owner of some peripheral.
1080        let mut cores: Vec<VisibleCore> = transceivers
1081            .iter()
1082            .flat_map(|t| [t.channel_primary.0, t.channel_secondary.0])
1083            .chain(peripherals.instances.iter().map(|p| p.owner))
1084            .filter(|core| matches!(core, VisibleCore::Mcore(_)))
1085            .collect();
1086        cores.sort_by_key(|core| core.to_string());
1087        cores.dedup();
1088
1089        cores
1090            .into_iter()
1091            .filter_map(|core| {
1092                let map = crate::chipmap::load(chip, &core.to_string()).ok()?;
1093                Some((core, map.peripheral_names()))
1094            })
1095            .collect()
1096    }
1097
1098    /// Resolves the `[peripheral.<name>]` entries owned by controller cores into
1099    /// the [`Peripherals`] IR, using the per-core chip map for bases and
1100    /// interrupt numbers.
1101    ///
1102    /// Only I2C and GPIO blocks are brought up today; other blocks (and cores
1103    /// without a bundled chip map) are skipped, yielding no generated handle.
1104    fn lower_peripherals(&self) -> Result<Peripherals, CfgError> {
1105        let chip = &self.config.profile.chip;
1106        let mut instances = Vec::new();
1107
1108        for (name, peripheral) in &self.config.peripheral {
1109            let VisibleCore::Mcore(_) = peripheral.owner else {
1110                continue;
1111            };
1112            let core = peripheral.owner.to_string();
1113            consortium_log::trace!("lowering peripheral '{}' on core '{}'", name, core);
1114            let map = match crate::chipmap::load(chip, &core) {
1115                Ok(map) => map,
1116                Err(DataError::ChipMapUnavailable { .. }) => {
1117                    consortium_log::trace!(
1118                        "no chip map for core '{}'; peripheral '{}' gets no bring-up",
1119                        core,
1120                        name
1121                    );
1122                    continue;
1123                }
1124                Err(err) => {
1125                    consortium_log::trace!("chip map load failed for core '{}': {}", core, err);
1126                    return Err(err.into());
1127                }
1128            };
1129            let Some(entry) = map.peripheral(name) else {
1130                consortium_log::trace!("peripheral '{}' absent from chip map for '{}'", name, core);
1131                continue;
1132            };
1133            // The HAL's peripheral singletons carry the non-secure base the
1134            // chip dump lists. A secure owner used to be rebased onto the secure
1135            // alias here; refuse rather than hand the driver the wrong window.
1136            if peripheral.secure {
1137                consortium_log::trace!(
1138                    "peripheral '{}' is secure; the singleton path has no secure alias",
1139                    name
1140                );
1141                return Err(CfgError::Manifest(format!(
1142                    "peripheral '{name}' sets `secure = true`, but generated bring-up takes the \
1143                     chip HAL's peripheral singleton, which names only the non-secure alias; \
1144                     drive secure peripherals from firmware instead"
1145                )));
1146            }
1147            let singleton = entry.name.clone();
1148
1149            let kind = match entry.kind() {
1150                "i2c" | "lpi2c" => {
1151                    let irq = map.irq(name).ok_or_else(|| {
1152                        consortium_log::trace!(
1153                            "i2c peripheral '{}' has no interrupt entry in chip map for '{}'",
1154                            name,
1155                            core
1156                        );
1157                        CfgError::Manifest(format!(
1158                            "i2c peripheral '{name}' has no interrupt entry in the chip map for '{core}'"
1159                        ))
1160                    })?;
1161                    let clock_hz = peripheral.clock.ok_or_else(|| {
1162                        consortium_log::trace!(
1163                            "i2c peripheral '{}' missing required `clock` field",
1164                            name
1165                        );
1166                        CfgError::Manifest(format!(
1167                            "i2c peripheral '{name}' needs `clock` (its input clock in Hz) to generate bring-up"
1168                        ))
1169                    })?;
1170                    let frequency = peripheral.frequency.unwrap_or(100_000);
1171                    // The PAC variant is only nameable where the shipped table
1172                    // is this core's; elsewhere the line stays a raw number and
1173                    // no vector is generated for it.
1174                    if !map.has_pac_interrupts() {
1175                        consortium_log::trace!(
1176                            "core '{}' is not described by the chip PAC's interrupt table; \
1177                             peripheral '{}' gets its NVIC enable but no generated vector, so \
1178                             the firmware must install '{}' itself",
1179                            core,
1180                            name,
1181                            irq.name,
1182                        );
1183                    }
1184                    let line = InterruptLine {
1185                        number: u32::from(irq.number),
1186                        variant: map.has_pac_interrupts().then(|| irq.name.clone()),
1187                    };
1188                    PeripheralKind::I2c {
1189                        clock_hz,
1190                        frequency,
1191                        irq: line,
1192                    }
1193                }
1194                "gpio" | "rgpio" => PeripheralKind::Gpio,
1195                _ => PeripheralKind::Other,
1196            };
1197
1198            instances.push(PeripheralInstance {
1199                owner: peripheral.owner,
1200                key: name.clone(),
1201                singleton,
1202                kind,
1203            });
1204        }
1205
1206        // Deterministic emit order regardless of the config map iteration order.
1207        instances.sort_by(|a, b| a.key.cmp(&b.key));
1208
1209        Ok(Peripherals {
1210            hal_crate: hal_crate_path(chip),
1211            instances,
1212        })
1213    }
1214
1215    /// The `[endpoints]` artifact root declared for `core`, used to *seed* that
1216    /// coprocessor's debug-ring [`elf`](crate::components::dbg::DebugRing::elf).
1217    ///
1218    /// This is only a placeholder: it names the firmware's source directory, not
1219    /// its compiled ELF. The build pipeline overwrites it with the real
1220    /// build-host artifact path via
1221    /// [`DebugLogs::with_resolved_elf_paths`](crate::components::dbg::DebugLogs::with_resolved_elf_paths)
1222    /// once Stage 1 has compiled the firmware, before generating the AP codegen
1223    /// that `include_bytes!`es the image. Keeping `lower` a pure function of the
1224    /// manifest means the substitution happens in the builder, not here.
1225    ///
1226    /// A core may correspond to a secure and a non-secure party; the ring is per
1227    /// core, so the first endpoint whose party projects to `core` is used.
1228    fn endpoint_artifact(&self, core: VisibleCore) -> Option<String> {
1229        self.config
1230            .endpoints
1231            .iter()
1232            .find(|(party, _)| party.core() == core)
1233            .map(|(_, root)| root.clone())
1234    }
1235}
1236
1237/// Resolves the endpoint a channel declares for `party`, named by `role` for
1238/// the error message.
1239fn endpoint<'c>(
1240    channel: &'c consortium_cfg_common::config::IpcChannel,
1241    party: Party,
1242    role: &str,
1243) -> Result<&'c consortium_cfg_common::config::IpcEndpoint, CfgError> {
1244    channel.endpoints.get(&party).ok_or_else(|| {
1245        consortium_log::trace!(
1246            "channel '{}' names {} party '{}' with no matching [party.{}]",
1247            channel.name,
1248            role,
1249            party,
1250            party
1251        );
1252        CfgError::Manifest(format!(
1253            "ipc channel '{}' names {role} party '{party}' but has no matching [party.{party}]",
1254            channel.name
1255        ))
1256    })
1257}
1258
1259/// The IRQ a doorbell assigns to `party`, if the database lists that exact party
1260/// (matching secure/non-secure and instance index).
1261fn party_irq(doorbell: &HardwareDoorbellPeripheral, party: Party) -> Option<u32> {
1262    doorbell
1263        .parties
1264        .iter()
1265        .find(|entry| entry.core == party)
1266        .map(|entry| entry.irq)
1267}
1268
1269/// The doorbell register window `party` drives, as the codegen's side marker.
1270///
1271/// Only a block that declares a `window_length` has per-side windows. For a
1272/// single-window doorbell (HSEM, IPCC) the marker means the *core bank* rather
1273/// than a window, and the bank is the descriptor role — so those fall back to
1274/// `role` and their generated output is unchanged by this mapping.
1275fn party_window(
1276    doorbell: &HardwareDoorbellPeripheral,
1277    party: Party,
1278    role: DoorbellRingerSide,
1279) -> DoorbellRingerSide {
1280    if doorbell.window_length.is_none() {
1281        return role;
1282    }
1283    let window = doorbell
1284        .parties
1285        .iter()
1286        .find(|entry| entry.core == party)
1287        .map_or(DoorbellWindow::A, |entry| entry.window);
1288    match window {
1289        DoorbellWindow::A => DoorbellRingerSide::Primary,
1290        DoorbellWindow::B => DoorbellRingerSide::Secondary,
1291    }
1292}
1293
1294/// The controller-core physical-base map for a doorbell: like
1295/// [`controller_bases`], but each party maps to *its own* register window
1296/// rather than sharing one base. Session validation has already rejected a
1297/// party the doorbell does not list.
1298fn controller_doorbell_bases(
1299    doorbell: &HardwareDoorbellPeripheral,
1300    parties: [Party; 2],
1301    alias: impl Fn(bool) -> u32,
1302) -> HashMap<VisibleCore, u32> {
1303    parties
1304        .into_iter()
1305        .filter(|party| party.core() != VisibleCore::Ap)
1306        .filter_map(|party| {
1307            let entry = doorbell.parties.iter().find(|entry| entry.core == party)?;
1308            Some((party.core(), entry.memory + alias(party_secure(party))))
1309        })
1310        .collect()
1311}
1312
1313/// The controller-core physical-base map for an address: every participant that
1314/// is not the AP maps to `base` plus the alias for that party's security world
1315/// (the AP resolves through UIO instead).
1316fn controller_bases(
1317    parties: [Party; 2],
1318    base: u32,
1319    alias: impl Fn(bool) -> u32,
1320) -> HashMap<VisibleCore, u32> {
1321    parties
1322        .into_iter()
1323        .filter(|party| party.core() != VisibleCore::Ap)
1324        .map(|party| (party.core(), base + alias(party_secure(party))))
1325        .collect()
1326}
1327
1328/// Whether `party` accesses the bus from the secure world.
1329fn party_secure(party: Party) -> bool {
1330    match party {
1331        Party::AcoreSecure => true,
1332        Party::AcoreNonSecure => false,
1333        Party::Mcore { secure, .. } => secure,
1334    }
1335}
1336
1337/// The type of the chip memory region holding `base`. Validation has already
1338/// placed every window inside one; `Sram` is only the fallback for code paths
1339/// that run on an unvalidated session.
1340fn memory_type(database: &ChipDatabase, base: u32) -> MemoryRegionType {
1341    database
1342        .memory
1343        .regions
1344        .iter()
1345        .find(|region| region.contains(base, 0))
1346        .map_or(MemoryRegionType::Sram, |region| region.r#type)
1347}
1348
1349/// The alias offset a core in the given security world adds to `base`: the
1350/// chip's memory alias for SRAM windows, `0` for DRAM (which every core
1351/// addresses physically) or when the chip declares no alias.
1352fn memory_alias(database: &ChipDatabase, base: u32, secure: bool) -> u32 {
1353    if !memory_type(database, base).aliased() {
1354        return 0;
1355    }
1356    database
1357        .memory
1358        .aliases
1359        .iter()
1360        .find(|alias| alias.secure == secure)
1361        .map_or(0, |alias| alias.offset)
1362}
1363
1364/// The peripheral (doorbell) alias base for the given security world, or `0` if
1365/// the chip declares none.
1366fn peripheral_alias(database: &ChipDatabase, secure: bool) -> u32 {
1367    database
1368        .peripheral
1369        .aliases
1370        .iter()
1371        .find(|alias| alias.secure == secure)
1372        .map_or(0, |alias| alias.address)
1373}
1374
1375/// Records a doorbell's participating cores, creating the [`HardwareDoorbell`]
1376/// on first sight and merging later hits. Each entry is a
1377/// `(core, irq, channel, side)`: the channel is added to the doorbell's used
1378/// set, the IRQ (when known) is mapped to the core for NVIC enablement, and the
1379/// side is mapped to the core so the generated vector forwards with the right
1380/// marker.
1381fn record_bell(
1382    bells: &mut Vec<HardwareDoorbell>,
1383    key: &str,
1384    address: &Address,
1385    meta: DoorbellMeta,
1386    entries: impl IntoIterator<Item = (VisibleCore, Option<u32>, u8, DoorbellRingerSide)>,
1387) {
1388    let bell = match bells.iter_mut().find(|bell| bell.name == key) {
1389        Some(bell) => bell,
1390        None => {
1391            bells.push(HardwareDoorbell {
1392                name: key.to_string(),
1393                address: address.clone(),
1394                r#type: meta.r#type,
1395                id: meta.id,
1396                channels: Vec::new(),
1397                irq: HashMap::new(),
1398                side: HashMap::new(),
1399            });
1400            bells.last_mut().expect("just pushed")
1401        }
1402    };
1403
1404    for (core, irq, channel, side) in entries {
1405        if !bell.channels.contains(&channel) {
1406            bell.channels.push(channel);
1407        }
1408        if let Some(irq) = irq {
1409            bell.irq.insert(core, InterruptLine::new(irq));
1410        }
1411        bell.side.insert(core, side);
1412    }
1413}
1414
1415#[cfg(test)]
1416mod tests {
1417    use super::*;
1418    use consortium_cfg_common::config::{Codec, McoreName};
1419    use consortium_cfg_common::database::VectorPlacement;
1420
1421    const STM32: &str = include_str!("../Consortium.example.stm32mp257.toml");
1422
1423    fn cm33() -> VisibleCore {
1424        VisibleCore::Mcore(McoreName::Cm33)
1425    }
1426
1427    fn cm0p() -> VisibleCore {
1428        VisibleCore::Mcore(McoreName::Cm0p)
1429    }
1430
1431    fn cm7() -> VisibleCore {
1432        VisibleCore::Mcore(McoreName::Cm7)
1433    }
1434
1435    /// An i.MX 95 Linux + CM7 pair whose channel and debug ring sit in the DDR
1436    /// window the System Manager grants both.
1437    const IMX95_DDR: &str = r#"
1438        [profile]
1439        chip = "mimx9596xvt"
1440        shared = "./shared"
1441        deploy_root = "/opt/consortium"
1442
1443        [endpoints]
1444        linux = "./app"
1445        cm7   = "./firmware"
1446
1447        [dbg.cm7]
1448        memory = { base = 0x88300000, length = 0x2000 }
1449        mtu    = 64
1450
1451        [[ipc.channel]]
1452        name          = "sensor"
1453        doorbell      = "mu7"
1454        pattern       = "memory"
1455        primary       = "linux"
1456        secondary     = "cm7"
1457        party.linux   = { chan = 6, type = "SensorData" }
1458        party.cm7     = { chan = 7, type = "MotionControl" }
1459        config.memory = { base = 0x88304000, length = 0x4000 }
1460        mtu           = 128
1461    "#;
1462
1463    fn lowered() -> LoweredIr {
1464        Session::from_toml(STM32)
1465            .expect("example manifest loads")
1466            .lower()
1467            .expect("session lowers")
1468    }
1469
1470    fn transceiver<'a>(ir: &'a LoweredIr, name: &str) -> &'a Transceiver {
1471        ir.ipc_shm
1472            .transceivers()
1473            .iter()
1474            .find(|t| t.name == name)
1475            .unwrap_or_else(|| panic!("transceiver '{name}' present"))
1476    }
1477
1478    #[test]
1479    fn orders_uio_devices_by_ascending_memory_base() {
1480        let ir = lowered();
1481        let names: Vec<_> = ir.uio.iter().map(|(name, _)| name.as_str()).collect();
1482        // Every device is ordered by its mapped base: the SRAM carveouts
1483        // (dbg-shm @0x2006_2000, ipc-shm @0x2006_4000) precede the peripheral
1484        // doorbells (ipcc1 @0x4049_0000, ipcc2 @0x4625_0000).
1485        assert_eq!(names, ["dbg-shm", "ipc-shm", "ipcc1", "ipcc2"]);
1486    }
1487
1488    #[test]
1489    fn region_addresses_index_uio0_in_declaration_order() {
1490        let ir = lowered();
1491        let sensor = transceiver(&ir, "sensor");
1492        let lp = transceiver(&ir, "lp");
1493
1494        // Both IPC regions share the ipc-shm device (uio1, after dbg-shm at
1495        // uio0); mmap slot follows declaration order.
1496        assert_eq!(
1497            sensor.region.address.ap,
1498            ApAddress {
1499                uio_id: 1,
1500                mmap_id: 0
1501            }
1502        );
1503        assert_eq!(
1504            lp.region.address.ap,
1505            ApAddress {
1506                uio_id: 1,
1507                mmap_id: 1
1508            }
1509        );
1510
1511        // The controller side resolves to the manifest base plus the non-secure
1512        // SRAM alias (cm33 and cm0p are both non-secure here). Sensor lives in
1513        // SRAM2 (0x0006_4000), lp in LPSRAM3 (0x000C_4000).
1514        assert_eq!(sensor.region.address.cp[&cm33()], 0x2006_4000);
1515        assert_eq!(lp.region.address.cp[&cm0p()], 0x200C_4000);
1516    }
1517
1518    #[test]
1519    fn uio_windows_apply_non_secure_aliases() {
1520        let ir = lowered();
1521
1522        // SRAM regions map through the non-secure memory alias (0x2000_0000),
1523        // so sensor at canonical 0x0006_4000 is exposed at 0x2006_4000.
1524        let (_, shm) = ir
1525            .uio
1526            .iter()
1527            .find(|(name, _)| name == "ipc-shm")
1528            .expect("shared-memory uio device");
1529        assert_eq!(shm.mmap[0].0, 0x2006_4000);
1530
1531        // The IPCC1 doorbell window maps through the non-secure peripheral alias
1532        // (0x4000_0000): canonical 0x0049_0000 becomes 0x4049_0000.
1533        let (_, ipcc1) = ir
1534            .uio
1535            .iter()
1536            .find(|(name, _)| name == "ipcc1")
1537            .expect("ipcc1 uio device");
1538        assert_eq!(ipcc1.mmap[0].0, 0x4049_0000);
1539    }
1540
1541    #[test]
1542    fn controller_doorbell_base_applies_security_alias() {
1543        let ir = lowered();
1544        let sensor = transceiver(&ir, "sensor");
1545        // cm33 is non-secure, so it sees IPCC1 through the 0x4000_0000 alias.
1546        assert_eq!(sensor.doorbell_address.cp[&cm33()], 0x4049_0000);
1547    }
1548
1549    #[test]
1550    fn doorbell_addresses_get_their_own_uio_devices() {
1551        let ir = lowered();
1552        let sensor = transceiver(&ir, "sensor");
1553        let lp = transceiver(&ir, "lp");
1554
1555        // Doorbells sort after both SRAM carveouts, so hsem is uio2 and ipcc2
1556        // is uio3 (ipc-shm @uio0, dbg-shm @uio1).
1557        assert_eq!(
1558            sensor.doorbell_address.ap,
1559            ApAddress {
1560                uio_id: 2,
1561                mmap_id: 0
1562            }
1563        );
1564        assert_eq!(
1565            lp.doorbell_address.ap,
1566            ApAddress {
1567                uio_id: 3,
1568                mmap_id: 0
1569            }
1570        );
1571    }
1572
1573    #[test]
1574    fn uses_explicit_primary_secondary_parties() {
1575        let ir = lowered();
1576        let sensor = transceiver(&ir, "sensor");
1577
1578        // `primary = "linux"`, `secondary = "cm33"`.
1579        assert_eq!(sensor.channel_primary.0, VisibleCore::Ap);
1580        assert_eq!(sensor.channel_primary.1.doorbell_chan, 0);
1581        assert_eq!(sensor.channel_primary.1.type_name, "SensorData");
1582        assert_eq!(sensor.channel_secondary.0, cm33());
1583        assert_eq!(sensor.channel_secondary.1.doorbell_chan, 1);
1584        assert_eq!(sensor.channel_secondary.1.type_name, "MotionControl");
1585    }
1586
1587    #[test]
1588    fn carries_mtu_and_codec_through() {
1589        let ir = lowered();
1590        assert_eq!(transceiver(&ir, "sensor").mtu, 128);
1591        assert_eq!(transceiver(&ir, "sensor").codec, Codec::Postcard);
1592        assert_eq!(transceiver(&ir, "lp").codec, Codec::Rkyv);
1593    }
1594
1595    /// On i.MX 93 Linux drives MU side A and the CM33 side B. The CM33's chip
1596    /// map exposes both `MU1_MUA` and `MU1_MUB`, so the window must come from the
1597    /// database rather than from whichever singleton the dump lists first:
1598    /// getting this wrong emits `MuDoorbell::<Primary, _>::new(p.MU1_MUA)` and
1599    /// leaves both cores on the same half of the block.
1600    #[test]
1601    fn imx93_doorbell_puts_the_cm33_on_mub() {
1602        let manifest = r#"
1603            [profile]
1604            chip = "mimx9352"
1605            shared = "./shared"
1606            deploy_root = "/opt/consortium"
1607
1608            [endpoints]
1609            linux = "./app"
1610            cm33  = "./firmware"
1611
1612            [[ipc.channel]]
1613            name          = "sensor"
1614            doorbell      = "mu1"
1615            pattern       = "memory"
1616            primary       = "linux"
1617            secondary     = "cm33"
1618            party.linux   = { chan = 0, type = "SensorData" }
1619            party.cm33    = { chan = 1, type = "MotionControl" }
1620            config.memory = { base = 0x00480000, length = 0x4000 }
1621            mtu           = 128
1622        "#;
1623        let ir = Session::from_toml(manifest).unwrap().lower().unwrap();
1624        let sensor = transceiver(&ir, "sensor");
1625
1626        // Roles follow the manifest...
1627        assert_eq!(sensor.channel_primary.0, VisibleCore::Ap);
1628        assert_eq!(sensor.channel_secondary.0, cm33());
1629        // ...and so do the windows: Linux on MUA, the CM33 on MUB.
1630        assert_eq!(
1631            sensor.doorbell_window,
1632            (DoorbellRingerSide::Primary, DoorbellRingerSide::Secondary)
1633        );
1634        // The CM33 is on side B, one window above the block base (0x0423_0000
1635        // through the 0x4000_0000 non-secure alias) — `MU1_MUB` in its chip
1636        // dump. Linux takes MUA at the block base.
1637        assert_eq!(sensor.doorbell_address.cp[&cm33()], 0x4423_0000);
1638    }
1639
1640    /// The i.MX 95 CM7 likewise reaches MU7 through MUB — the window offset
1641    /// must apply to it, or it is handed Linux's half of the block.
1642    #[test]
1643    fn imx95_controller_doorbell_base_is_its_own_window() {
1644        let ir = Session::from_toml(IMX95_DDR).unwrap().lower().unwrap();
1645        let sensor = transceiver(&ir, "sensor");
1646        assert_eq!(
1647            sensor.doorbell_window,
1648            (DoorbellRingerSide::Primary, DoorbellRingerSide::Secondary)
1649        );
1650        assert_eq!(sensor.doorbell_address.cp[&cm7()], 0x4244_0000);
1651    }
1652
1653    /// i.MX 95 shares DDR, which every core addresses physically: neither the
1654    /// CM7's embedded base, the debug ring, nor Linux's UIO windows may take the
1655    /// SRAM alias (`0x2000_0000`), or both sides land 512 MiB off the window.
1656    #[test]
1657    fn dram_windows_take_no_memory_alias() {
1658        let ir = Session::from_toml(IMX95_DDR).unwrap().lower().unwrap();
1659        let sensor = transceiver(&ir, "sensor");
1660        assert_eq!(sensor.region.r#type, MemoryRegionType::Dram);
1661        assert_eq!(sensor.region.address.cp[&cm7()], 0x8830_4000);
1662
1663        let mmap = |device: &str| {
1664            ir.uio
1665                .iter()
1666                .find(|(name, _)| name == device)
1667                .unwrap_or_else(|| panic!("{device} uio device"))
1668                .1
1669                .mmap
1670                .clone()
1671        };
1672        assert_eq!(mmap("ipc-shm"), vec![(0x8830_4000, 0x4000)]);
1673        assert_eq!(mmap("dbg-shm"), vec![(0x8830_0000, 0x2000)]);
1674    }
1675
1676    /// The generated vector must name the same window it forwards.
1677    ///
1678    /// The vector's name comes from the chip map (looked up by the party's NVIC
1679    /// number) while the side marker comes from the party's declared window, so
1680    /// an `MU1_A` handler forwarding `Secondary` would mask the *other* half of
1681    /// the block and starve the executor. On i.MX 93 the CM33 is on MUB, so
1682    /// this is `MU1_B` forwarding `Secondary`.
1683    #[test]
1684    fn imx93_doorbell_vector_names_the_window_it_forwards() {
1685        let manifest = r#"
1686            [profile]
1687            chip = "mimx9352"
1688            shared = "./shared"
1689            deploy_root = "/opt/consortium"
1690
1691            [endpoints]
1692            linux = "./app"
1693            cm33  = "./firmware"
1694
1695            [[ipc.channel]]
1696            name          = "sensor"
1697            doorbell      = "mu1"
1698            pattern       = "memory"
1699            primary       = "linux"
1700            secondary     = "cm33"
1701            party.linux   = { chan = 0, type = "SensorData" }
1702            party.cm33    = { chan = 1, type = "MotionControl" }
1703            config.memory = { base = 0x00480000, length = 0x4000 }
1704            mtu           = 128
1705        "#;
1706        let ir = Session::from_toml(manifest).unwrap().lower().unwrap();
1707        let tokens = ir.bella.vectors(&cm33(), "::consortium_hal_imx9")[0].to_string();
1708
1709        assert!(tokens.contains("fn MU1_B ()"), "{tokens}");
1710        assert!(
1711            tokens.contains(
1712                ":: consortium_core :: bell :: service_interrupt :: < :: \
1713                 consortium_core :: bell :: mu :: Mu < :: consortium_ipc :: \
1714                 Secondary , 1usize > > ()"
1715            ),
1716            "{tokens}"
1717        );
1718    }
1719
1720    /// A single-window doorbell's side marker names the core *bank*, not a
1721    /// register window, so it must keep following the descriptor role. This is
1722    /// what keeps the STM32 output identical across the window change.
1723    #[test]
1724    fn single_window_doorbell_side_still_follows_the_role() {
1725        let ir = lowered();
1726        let sensor = transceiver(&ir, "sensor");
1727        assert_eq!(
1728            sensor.doorbell_window,
1729            (DoorbellRingerSide::Primary, DoorbellRingerSide::Secondary)
1730        );
1731    }
1732
1733    /// The i.MX 93 CM33 runs from TCM rather than a DDR carveout, so its
1734    /// windows carry `reserve: false` and the pipeline still gets a complete
1735    /// memory contract — without one, `csti build` fails outright at
1736    /// `write_generated_memory_x`.
1737    #[test]
1738    fn imx93_cm33_remoteproc_lowers_to_a_tcm_layout() {
1739        let manifest = r#"
1740            [profile]
1741            chip = "mimx9352"
1742            shared = "./shared"
1743            deploy_root = "/opt/consortium"
1744
1745            [endpoints]
1746            linux = "./app"
1747            cm33  = "./firmware"
1748
1749            [[ipc.channel]]
1750            name          = "sensor"
1751            doorbell      = "mu1"
1752            pattern       = "memory"
1753            primary       = "linux"
1754            secondary     = "cm33"
1755            party.linux   = { chan = 0, type = "SensorData" }
1756            party.cm33    = { chan = 1, type = "MotionControl" }
1757            config.memory = { base = 0x00480000, length = 0x4000 }
1758            mtu           = 128
1759        "#;
1760        let ir = Session::from_toml(manifest).unwrap().lower().unwrap();
1761        let layout = &ir.remoteproc[&"cm33".parse().unwrap()];
1762
1763        assert_eq!(layout.compatible, "fsl,imx93-cm33");
1764        assert_eq!(layout.vector.window.origin, 0x0FFE_0000);
1765        assert_eq!(layout.ram.origin, 0x2000_3000);
1766        // TCM is not DRAM, so nothing here is carved out of the kernel map.
1767        assert!(!layout.vector.window.reserve);
1768        assert!(!layout.flash.reserve);
1769        assert!(!layout.shmem.window.reserve);
1770        assert!(!layout.ram.reserve);
1771
1772        // The contract has to render, or the firmware crate gets no `memory.x`.
1773        let memory_x = layout.linker_memory().unwrap().render();
1774        assert!(memory_x.contains("m_interrupts"), "{memory_x}");
1775        assert!(memory_x.contains("m_rsc_tbl_in_tcm"), "{memory_x}");
1776    }
1777
1778    #[test]
1779    fn remoteproc_defaults_and_partial_overrides_lower_once() {
1780        let manifest = r#"
1781            [profile]
1782            chip = "mimx9596xvt"
1783            shared = "./shared"
1784            deploy_root = "/opt/consortium"
1785
1786            [endpoints]
1787            cm7 = "./mcu"
1788
1789            [remoteproc.cm7]
1790            ram = { origin = 0x81000000, length = 0x00400000 }
1791        "#;
1792        let ir = Session::from_toml(manifest)
1793            .expect("manifest validates")
1794            .lower()
1795            .expect("session lowers");
1796        let layout = &ir.remoteproc[&"cm7".parse().unwrap()];
1797
1798        assert_eq!(layout.flash.origin, 0x8000_1000);
1799        assert_eq!(layout.flash.length, 0x007f_f000);
1800        assert_eq!(layout.ram.origin, 0x8100_0000);
1801        assert_eq!(layout.ram.length, 0x0040_0000);
1802        assert_eq!(layout.vector_section, ".interrupts");
1803        assert_eq!(layout.vector_placement, VectorPlacement::Flash);
1804        assert_eq!(layout.vector.name, "m_interrupts");
1805        assert_eq!(layout.vector.window.length, 0x1000);
1806        assert_eq!(layout.shmem.name, "m_rsc_tbl_in_dram");
1807        assert_eq!(layout.shmem.window.origin, 0x8822_0000);
1808        let memory = layout.linker_memory().unwrap().render();
1809        assert!(memory.contains("m_interrupts (rx) : ORIGIN = 0x80000000, LENGTH = 0x1000"));
1810        assert!(memory.contains("FLASH (rx) : ORIGIN = 0x80000000, LENGTH = 0x800000"));
1811        assert!(memory.contains("_stext = 0x80001000;"));
1812        assert!(memory.contains("RAM (rwx) : ORIGIN = 0x81000000, LENGTH = 0x400000"));
1813        assert!(memory.contains("m_rsc_tbl_in_dram (rw) : ORIGIN = 0x88220000"));
1814        assert!(memory.contains("} > m_rsc_tbl_in_dram\n} INSERT AFTER .rodata;"));
1815
1816        let stm = Session::from_toml(
1817            r#"
1818                [profile]
1819                chip = "stm32mp257fak3"
1820                shared = "./shared"
1821                deploy_root = "/opt/consortium"
1822
1823                [endpoints]
1824                cm33 = "./mcu"
1825            "#,
1826        )
1827        .unwrap()
1828        .lower()
1829        .unwrap();
1830        let stm_layout = &stm.remoteproc[&"cm33".parse().unwrap()];
1831        assert_eq!(stm_layout.vector_section, ".isr_vectors");
1832        // STM32MP2 keeps the vector window separate: `FLASH` is the flash window
1833        // as declared and no `_stext` is pinned.
1834        assert_eq!(stm_layout.vector_placement, VectorPlacement::Separate);
1835        let stm_memory = stm_layout.linker_memory().unwrap().render();
1836        assert!(stm_memory.contains("NS_VECTOR_TBL (rwx) : ORIGIN = 0x80100000, LENGTH = 0x1000"));
1837        assert!(stm_memory.contains("FLASH (rx) : ORIGIN = 0x80101000, LENGTH = 0x7ff000"));
1838        assert!(!stm_memory.contains("_stext"));
1839        assert!(stm_memory.contains("SHMEM (rwx) : ORIGIN = 0x812f8000, LENGTH = 0x8000"));
1840        assert!(stm_memory.contains("RAM (rwx) : ORIGIN = 0x80a00000, LENGTH = 0x100000"));
1841        assert!(!stm_memory.contains("VIRTIO_SHMEM"));
1842        assert!(stm_memory.contains("} > SHMEM\n} INSERT AFTER .rodata;"));
1843    }
1844
1845    #[test]
1846    fn example_memory_x_files_match_structured_layouts() {
1847        let cases = [
1848            (
1849                include_str!("../../../examples/melt-pot/imx95/Consortium.toml"),
1850                "cm7",
1851                include_str!("../../../examples/melt-pot/imx95/mcu/memory.x"),
1852            ),
1853            (
1854                include_str!("../../../examples/melt-pot/imx93/Consortium.toml"),
1855                "cm33",
1856                include_str!("../../../examples/melt-pot/imx93/mcu/memory.x"),
1857            ),
1858            (
1859                include_str!("../../../examples/melt-pot/stm32mp25/Consortium.toml"),
1860                "cm33",
1861                include_str!("../../../examples/melt-pot/stm32mp25/mcu/memory.x"),
1862            ),
1863        ];
1864        for (manifest, core, expected) in cases {
1865            let ir = Session::from_toml(manifest).unwrap().lower().unwrap();
1866            let layout = &ir.remoteproc[&core.parse().unwrap()];
1867            assert_eq!(layout.linker_memory().unwrap().render(), expected);
1868        }
1869    }
1870
1871    #[test]
1872    fn rejects_party_without_endpoint() {
1873        // `secondary = "cm33"` names a party with no `[party.cm33]` endpoint.
1874        let manifest = r#"
1875            [profile]
1876            chip = "stm32mp257fak3"
1877            shared = "./shared"
1878            deploy_root = "/opt/consortium"
1879
1880            [[ipc.channel]]
1881            name = "solo"
1882            doorbell = "ipcc1"
1883            pattern = "memory"
1884            primary = "linux"
1885            secondary = "cm33"
1886            party.linux = { chan = 0, type = "Ping" }
1887            config.memory = { base = 0x00040000, length = 0x00008000 }
1888            mtu = 128
1889        "#;
1890
1891        let session = Session::from_toml(manifest).expect("manifest validates");
1892        assert!(matches!(session.lower(), Err(CfgError::Manifest(_))));
1893    }
1894
1895    #[test]
1896    fn bella_collects_doorbell_channels_and_core_irqs() {
1897        let ir = lowered();
1898
1899        let ipcc1 = ir
1900            .bella
1901            .bells
1902            .iter()
1903            .find(|bell| bell.name == "ipcc1")
1904            .expect("ipcc1 doorbell");
1905        // ipcc1 carries the primary (linux, chan 0) and secondary (cm33, chan 1).
1906        assert!(ipcc1.channels.contains(&0));
1907        assert!(ipcc1.channels.contains(&1));
1908        assert_eq!(ipcc1.id, Some(1));
1909        // cm33's NVIC line is sourced from the chip database, and lowering
1910        // resolves its name through the bundled chip map.
1911        assert_eq!(ipcc1.irq[&cm33()].number, 171);
1912        assert_eq!(ipcc1.irq[&cm33()].variant.as_deref(), Some("IPCC1_RX"));
1913
1914        let ipcc2 = ir
1915            .bella
1916            .bells
1917            .iter()
1918            .find(|bell| bell.name == "ipcc2")
1919            .expect("ipcc2 doorbell");
1920        assert!(ipcc2.irq.contains_key(&cm0p()));
1921    }
1922
1923    #[test]
1924    fn lowers_dbg_ring_with_non_secure_alias_and_env() {
1925        let ir = lowered();
1926
1927        // The example declares `[dbg.cm33]` at canonical 0x0006_2000; cm33 sees
1928        // SRAM through the non-secure alias (0x2000_0000), so its logger embeds
1929        // 0x2006_2000.
1930        let ring = ir.dbg.ring(cm33()).expect("cm33 has a debug ring");
1931        assert_eq!(ring.base, 0x2006_2000);
1932        assert_eq!(ring.length, 0x2000);
1933        assert_eq!(ring.mtu, 64);
1934        assert!(ir.dbg.ring(cm0p()).is_none());
1935
1936        // The logger is handed that resolved ring, not a build-time variable.
1937        let init = ir
1938            .initialize(cm33(), &syn::parse_quote!(::melt_pot_shared))
1939            .to_string();
1940        assert!(
1941            init.contains("ConsortiumLogger :: init ((537272320usize) as * mut u8 , 8192u16)"),
1942            "{init}"
1943        );
1944    }
1945
1946    #[test]
1947    fn lowered_ir_emits_valid_rust() {
1948        let ir = lowered();
1949        let uio = quote::format_ident!("uio");
1950        let ty_crate: syn::Path = syn::parse_str("::shared").unwrap();
1951
1952        let tokens = crate::components::ipc_shm::initialize_instances(
1953            &ir.ipc_shm,
1954            VisibleCore::Ap,
1955            &uio,
1956            &ty_crate,
1957            &ir.peripherals.hal_crate,
1958            &[],
1959        );
1960        syn::parse_file(&tokens.to_string()).expect("lowered ShmNode is valid Rust");
1961
1962        // The full AP resource entry point must parse as a complete module.
1963        let ap = ir.initialize(VisibleCore::Ap, &ty_crate);
1964        let ap_file =
1965            syn::parse_file(&ap.to_string()).expect("lowered AP init module is valid Rust");
1966        let ap_src = ap.to_string();
1967        assert!(ap_src.contains("channels !"));
1968        assert!(ap_src.contains("impl_channel !"));
1969        assert!(ap_src.contains("fn init"));
1970        assert!(ap_src.contains("Config"));
1971        assert!(ap_src.contains("Peripherals"));
1972        assert!(!ap_src.contains("impl Drop for Peripherals"));
1973        assert!(!ap_src.contains("impl Drop for Channels"));
1974        // The example declares `[dbg.cm33]`, so the host console is woven in.
1975        assert!(ap_src.contains("dbg_console :: spawn"));
1976        // AP platform resource construction remains fallible.
1977        assert!(ap_src.contains("ConsortiumInitError"));
1978        let _ = ap_file;
1979
1980        // A controller core emits a Secondary instance and synchronous init.
1981        let cm = ir.initialize(cm33(), &ty_crate);
1982        let cm_src = cm.to_string();
1983        syn::parse_file(&cm_src).expect("lowered controller init module is valid Rust");
1984        assert!(cm_src.contains("fn init"));
1985        assert!(!cm_src.contains("async fn init"));
1986        assert!(cm_src.contains("Secondary"));
1987
1988        let bella = ir
1989            .bella
1990            .nvic_enable(&cm33(), &ir.peripherals.hal_crate)
1991            .expect("cm33 has doorbell irqs");
1992        let wrapped = quote::quote! { fn _f() { #bella } };
1993        syn::parse_file(&wrapped.to_string()).expect("lowered Bella is valid Rust");
1994    }
1995
1996    #[test]
1997    fn cached_window_does_not_leave_gap_in_mpu_region_numbers() {
1998        let manifest = r#"
1999            [profile]
2000            chip = "mimx9596xvt"
2001            shared = "./shared"
2002            deploy_root = "/opt/consortium"
2003
2004            [[ipc.channel]]
2005            name = "cached"
2006            doorbell = "mu7"
2007            pattern = "memory"
2008            primary = "linux"
2009            secondary = "cm7"
2010            party.linux = { chan = 0, type = "Ping" }
2011            party.cm7 = { chan = 1, type = "Pong" }
2012            config.memory = { base = 0x88300000, length = 0x4000 }
2013            config.cache = true
2014            mtu = 128
2015
2016            [[ipc.channel]]
2017            name = "uncached"
2018            doorbell = "mu7"
2019            pattern = "memory"
2020            primary = "linux"
2021            secondary = "cm7"
2022            party.linux = { chan = 2, type = "Ping" }
2023            party.cm7 = { chan = 3, type = "Pong" }
2024            config.memory = { base = 0x88304000, length = 0x4000 }
2025            mtu = 128
2026        "#;
2027        let ir = Session::from_toml(manifest).unwrap().lower().unwrap();
2028        let ty_crate: syn::Path = syn::parse_str("::shared").unwrap();
2029        let cm7 = VisibleCore::Mcore(McoreName::Cm7);
2030        let source = ir.initialize(cm7, &ty_crate).to_string();
2031
2032        let disable_cache = source
2033            .split("disable_cache")
2034            .nth(1)
2035            .expect("uncached window emits an MPU carveout");
2036        assert!(disable_cache.contains("0u8"));
2037        assert!(!disable_cache.contains("1u8"));
2038        assert_eq!(source.matches("enable_dcache").count(), 1);
2039    }
2040}