blob: bf7f4e2cca7125cf91340a554d1dc090c0dc307f [file]
// Copyright 2025 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
use std::fs;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use anyhow::{Context, Result, anyhow};
use clap::{Args, Parser, Subcommand};
use minijinja::{Environment, State};
use serde::Serialize;
use serde::de::DeserializeOwned;
pub mod mpu_validation;
pub mod system_config;
pub mod validation;
use mpu_validation::MpuValidationMode;
use mpu_validation::pmsav7::validate_pmsav7_layout;
use system_config::ObjectConfig::Interrupt;
use system_config::{
InterruptTableConfig, MemoryMapping, MemoryMappingType, ObjectConfig, ProcessObjectConfig,
SystemConfig,
};
#[derive(Debug, Parser)]
pub struct Cli {
#[command(flatten)]
common_args: CommonArgs,
#[command(subcommand)]
command: Command,
}
#[derive(Args, Debug)]
pub struct CommonArgs {
#[arg(long, required(true))]
config: PathBuf,
#[arg(long, required(true))]
output: PathBuf,
#[arg(
long("template"),
value_name = "NAME=PATH",
value_parser = parse_template,
action = clap::ArgAction::Append
)]
templates: Vec<(String, PathBuf)>,
#[arg(long, default_value_t = true, action = clap::ArgAction::Set)]
userspace: bool,
}
fn parse_template(s: &str) -> Result<(String, PathBuf), String> {
s.split_once('=')
.map(|(name, path)| (name.to_string(), path.into()))
.ok_or_else(|| format!("invalid template format: '{s}'. Expected NAME=PATH."))
}
#[derive(Subcommand, Debug)]
enum Command {
RenderTargetTemplate,
RenderAppTemplate(AppArgs),
}
#[derive(Args, Debug)]
#[group(required = true, multiple = false)]
pub struct AppArgs {
#[arg(long)]
pub app_name: Option<String>,
#[arg(long)]
pub process_name: Option<String>,
}
enum AppType {
SingleProcess { app_name: String },
MultiProcess { process_name: String },
}
impl From<&AppArgs> for AppType {
fn from(args: &AppArgs) -> AppType {
if let Some(ref app_name) = args.app_name {
return AppType::SingleProcess {
app_name: app_name.clone(),
};
}
if let Some(ref process_name) = args.process_name {
return AppType::MultiProcess {
process_name: process_name.clone(),
};
}
unreachable!("AppArgs group requires either app_name or process_name");
}
}
/// Context defining the specific app and process being passed to the app template for rendering.
#[derive(Serialize)]
pub struct AppRenderContext<'a, A: ArchConfigInterface + Serialize> {
pub arch: &'a A,
pub app: &'a system_config::AppConfig,
pub process: &'a system_config::ProcessConfig,
pub is_multi_process_app: bool,
}
pub trait ArchConfigInterface {
fn get_arch_crate_name(&self) -> &'static str;
fn get_start_fn_address(&self, flash_start_address: u64) -> u64;
fn calculate_and_validate_config(
&mut self,
config: &mut system_config::BaseConfig,
) -> Result<()>;
fn get_interrupt_table_link_section(&self) -> Option<String>;
/// Returns true if the interrupt table entries should have no arguments.
///
/// Vectored interrupt controllers (like NVIC on ARM) require the entries to
/// have no arguments because the hardware jumps directly to the handler.
/// Non-vectored controllers (or architectures where all interrupts go to a
/// single handler like RISC-V) need to capture the `from_userspace` flag
/// earlier in the call chain and pass it to the specific interrupt handler.
fn bare_interrupt_table_entries(&self) -> bool;
/// Validate memory layout for MPU compatibility.
fn validate_mpu(&self, _config: &system_config::BaseConfig) -> Result<()> {
Ok(()) // Default: no MPU validation
}
/// Get required alignment for a process RAM region.
fn get_ram_alignment(&self, _size: u64) -> u64 {
32
}
/// Get minimum RAM alignment.
fn get_minimum_ram_alignment(&self) -> u64 {
32
}
}
pub fn parse_config<A: ArchConfigInterface + DeserializeOwned>(
cli: &Cli,
) -> Result<system_config::SystemConfig<A>> {
let json5_str =
fs::read_to_string(&cli.common_args.config).context("Failed to read config file")?;
let config: SystemConfig<A> =
serde_json5::from_str(&json5_str).context("Failed to parse config file")?;
Ok(config)
}
const FLASH_ALIGNMENT: u64 = 4;
/// Map the kernel's flash region as `ReadOnlyExecutable` into every app's
/// address space. This is required so that `svc_return` remains executable
/// after the privilege drop on Cortex-M.
///
/// TODO: https://pwbug.dev/465500606 - Isolate `svc_return` into its own
/// section to allow mapping only that instead of the whole kernel flash.
fn cortex_m_add_kernel_code_mapping(config: &mut system_config::BaseConfig) {
for app in &mut config.apps {
for process in &mut app.processes {
process.memory_mappings.insert(
0,
MemoryMapping {
name: "kernel_code".to_string(),
ty: MemoryMappingType::ReadOnlyExecutable,
start_address: config.kernel.flash_start_address,
size_bytes: config.kernel.flash_size_bytes,
},
);
}
}
}
impl ArchConfigInterface for system_config::Armv8MConfig {
fn get_arch_crate_name(&self) -> &'static str {
"arch_arm_cortex_m"
}
fn get_start_fn_address(&self, flash_start_address: u64) -> u64 {
// +1 to set the Thumb mode bit in the reset vector.
flash_start_address + 1
}
fn calculate_and_validate_config(
&mut self,
config: &mut system_config::BaseConfig,
) -> Result<()> {
cortex_m_add_kernel_code_mapping(config);
// PMSAv8 requires all region base addresses and sizes to be aligned to
// 32 bytes (the minimum MPU region granularity).
for app in &config.apps {
for process in &app.processes {
for mapping in &process.memory_mappings {
if mapping.start_address % 32 != 0 {
return Err(anyhow!(
"Unaligned memory mapping: process {} in application {}'s memory mapping {}'s start address ({:#10x}) must be aligned to 32 bytes",
process.name,
app.name,
mapping.name,
mapping.start_address,
));
}
if mapping.size_bytes % 32 != 0 {
return Err(anyhow!(
"Unaligned memory mapping: process {} in application {}'s memory mapping {}'s size ({}) must be aligned to 32 bytes",
process.name,
app.name,
mapping.name,
mapping.size_bytes,
));
}
}
}
}
Ok(())
}
fn get_interrupt_table_link_section(&self) -> Option<String> {
Some(".vector_table.interrupts".to_string())
}
fn bare_interrupt_table_entries(&self) -> bool {
true
}
}
impl ArchConfigInterface for system_config::Armv7MConfig {
fn get_arch_crate_name(&self) -> &'static str {
"arch_arm_cortex_m"
}
fn get_start_fn_address(&self, flash_start_address: u64) -> u64 {
// +1 to set the Thumb mode bit in the reset vector.
flash_start_address + 1
}
fn calculate_and_validate_config(
&mut self,
config: &mut system_config::BaseConfig,
) -> Result<()> {
cortex_m_add_kernel_code_mapping(config);
// Validate power-of-2 size for Armv7-M
for app in &config.apps {
for process in &app.processes {
if !process.ram_size_bytes.is_power_of_two() {
return Err(anyhow!(
"Process {} RAM size ({}) must be a power of 2 for Armv7-M",
process.name,
process.ram_size_bytes
));
}
if process.ram_size_bytes < 32 {
return Err(anyhow!(
"Process {} RAM size ({}) must be at least 32 bytes for Armv7-M",
process.name,
process.ram_size_bytes
));
}
}
}
Ok(())
}
fn get_ram_alignment(&self, size: u64) -> u64 {
size
}
fn get_interrupt_table_link_section(&self) -> Option<String> {
Some(".vector_table.interrupts".to_string())
}
fn bare_interrupt_table_entries(&self) -> bool {
true
}
fn validate_mpu(&self, config: &system_config::BaseConfig) -> Result<()> {
let kernel = &config.kernel;
// Build app list: (name, flash_start, flash_end, ram_start, ram_end)
let apps: Vec<_> = config
.apps
.iter()
.map(|app| {
(
app.name.clone(),
app.flash_start_address,
app.flash_start_address + app.flash_size_bytes,
app.ram_start_address,
app.ram_start_address + app.ram_size_bytes,
)
})
.collect();
let issues = validate_pmsav7_layout(
kernel.flash_start_address,
kernel.flash_start_address + kernel.flash_size_bytes,
kernel.ram_start_address,
kernel.ram_start_address + kernel.ram_size_bytes,
&apps,
);
if issues.is_empty() {
return Ok(());
}
// Report issues based on validation mode
let mode = kernel.mpu_validation;
let has_errors = issues.iter().any(|i| i.is_error);
for issue in &issues {
match mode {
MpuValidationMode::Strict => {
eprintln!("error: {}", issue);
}
MpuValidationMode::Warn => {
if issue.is_error {
eprintln!("warning: {}", issue);
} else {
eprintln!("info: {}", issue);
}
}
MpuValidationMode::Permissive => {
// Silent - no output in permissive mode
}
}
}
if mode == MpuValidationMode::Strict && has_errors {
return Err(anyhow!(
"PMSAv7 MPU validation failed. Use mpu_validation: \"warn\" to continue anyway."
));
}
Ok(())
}
}
impl ArchConfigInterface for system_config::RiscVConfig {
fn get_arch_crate_name(&self) -> &'static str {
"arch_riscv"
}
fn get_start_fn_address(&self, flash_start_address: u64) -> u64 {
flash_start_address
}
fn calculate_and_validate_config(
&mut self,
_config: &mut system_config::BaseConfig,
) -> Result<()> {
Ok(())
}
fn get_interrupt_table_link_section(&self) -> Option<String> {
None
}
fn bare_interrupt_table_entries(&self) -> bool {
false
}
}
pub struct SystemGenerator<'a, A: ArchConfigInterface> {
cli: Cli,
config: system_config::SystemConfig<A>,
env: Environment<'a>,
validators: Vec<Box<dyn crate::validation::ManifestValidator<A>>>,
}
impl<'a, A: ArchConfigInterface + Serialize> SystemGenerator<'a, A> {
pub fn new(cli: Cli, config: system_config::SystemConfig<A>) -> Result<Self> {
let mut instance = Self {
cli,
config,
env: Environment::new(),
validators: vec![Box::new(crate::validation::IdentifierValidator)],
};
instance.env.set_lstrip_blocks(true);
instance.env.set_trim_blocks(true);
// Run manifest validation first before rendering any templates.
for validator in &instance.validators {
validator.validate(&instance.config)?;
}
instance.env.add_filter("hex", hex);
instance.env.add_filter("to_lower_ident", to_lower_ident);
instance.env.add_filter("to_upper_ident", to_upper_ident);
for (name, path) in instance.cli.common_args.templates.clone() {
let template = fs::read_to_string(path)?;
instance.env.add_template_owned(name, template)?;
}
instance.populate_addresses();
instance.populate_process_and_thread_objects()?;
// This must be called after populate_addresses.
instance.populate_memory_mappings();
instance.populate_interrupt_table()?;
instance.config.base.userspace = instance.cli.common_args.userspace;
// Calculate and validate config after the populations above.
instance.config.calculate_and_validate()?;
// Run architecture-specific MPU compatibility validation.
instance.config.arch.validate_mpu(&instance.config.base)?;
Ok(instance)
}
pub fn generate(&mut self) -> Result<()> {
let out_str = match &self.cli.command {
Command::RenderTargetTemplate => self.render_system()?,
Command::RenderAppTemplate(args) => self.render_app_template(args)?,
};
let mut file = File::create(&self.cli.common_args.output)?;
file.write_all(out_str.as_bytes())
.context("Failed to write output")
}
fn render_system(&self) -> Result<String> {
let template = self.env.get_template("system")?;
template
.render(&self.config)
.context("Could not render system template")
}
fn render_app_template(&self, args: &AppArgs) -> Result<String> {
let template = self.env.get_template("app")?;
let is_multi_process_app = args.process_name.is_some();
let (app, process) = match AppType::from(args) {
AppType::MultiProcess { process_name } => self
.config
.base
.get_process_and_app(&process_name)
.ok_or_else(|| {
anyhow!(
"Unable to find process `{}` in system manifest",
process_name
)
})?,
AppType::SingleProcess { app_name } => {
let a = self.config.base.get_app(&app_name).ok_or_else(|| {
anyhow!("Unable to find app `{}` in system manifest", app_name)
})?;
(a, &a.processes[0])
}
};
let context = AppRenderContext {
arch: &self.config.arch,
app,
process,
is_multi_process_app,
};
template
.render(&context)
.context("Could not render app template")
}
#[must_use]
fn align(value: u64, alignment: u64) -> u64 {
debug_assert!(alignment.is_power_of_two());
(value + alignment - 1) & !(alignment - 1)
}
fn populate_addresses(&mut self) {
// Stack the apps after the kernel in flash and ram.
// TODO: davidroth - remove the requirement of setting the size of
// flash, and instead calculate it based on code size.
let mut next_flash_start_address =
self.config.base.kernel.flash_start_address + self.config.base.kernel.flash_size_bytes;
next_flash_start_address = Self::align(next_flash_start_address, FLASH_ALIGNMENT);
let mut next_ram_start_address =
self.config.base.kernel.ram_start_address + self.config.base.kernel.ram_size_bytes;
next_ram_start_address = Self::align(
next_ram_start_address,
self.config.arch.get_minimum_ram_alignment(),
);
self.config.base.arch_crate_name = self.config.arch.get_arch_crate_name();
self.config.base.bare_interrupt_table_entries =
self.config.arch.bare_interrupt_table_entries();
for app in self.config.base.apps.iter_mut() {
app.flash_start_address = next_flash_start_address;
next_flash_start_address = Self::align(
app.flash_start_address + app.flash_size_bytes,
FLASH_ALIGNMENT,
);
app.ram_start_address = next_ram_start_address;
// Calculate app RAM size as sum of process RAM sizes.
app.ram_size_bytes = app.processes.iter().map(|p| p.ram_size_bytes).sum();
let mut current_process_ram_start = app.ram_start_address;
for process in &mut app.processes {
let alignment = self.config.arch.get_ram_alignment(process.ram_size_bytes);
process.ram_start_address = Self::align(current_process_ram_start, alignment);
current_process_ram_start = process.ram_start_address + process.ram_size_bytes;
}
next_ram_start_address = Self::align(
app.ram_start_address + app.ram_size_bytes,
self.config.arch.get_minimum_ram_alignment(),
);
app.start_fn_address = self
.config
.arch
.get_start_fn_address(app.flash_start_address);
app.initial_sp = app.ram_start_address + app.ram_size_bytes;
}
}
fn resolve_linked_process(
apps: &[system_config::AppConfig],
linked_process: &str,
) -> Option<String> {
for app in apps {
if let Some(proc) = app.processes.iter().find(|pr| pr.name == linked_process) {
return Some(proc.name.clone());
}
}
None
}
fn populate_process_objects(
process: &mut system_config::ProcessConfig,
app_name: &str,
apps: &[system_config::AppConfig],
) -> Result<()> {
let process_name = &process.name;
// Verify user-declared process objects
for object in &mut process.objects {
if let ObjectConfig::Process(p) = object {
if p.name == "process" {
return Err(anyhow::anyhow!(
"Process '{}' in App '{}' manually requested a process object named 'process'. This is reserved for the process's own handle.",
process_name,
app_name
));
}
if let Some(resolved_process) =
Self::resolve_linked_process(apps, &p.linked_process)
{
p.process_object_name =
std::format!("object_{}_process", resolved_process.to_lowercase());
} else {
return Err(anyhow::anyhow!(
"Process '{}' in App '{}' requested handle to process object '{}' linked to '{}' but no such process found in system config",
process_name,
app_name,
p.name,
p.linked_process
));
}
}
}
// Add Process object for this process's own process. A fixed name is
// used so that generic code can be written to access a process's own
// process object.
process
.objects
.push(ObjectConfig::Process(ProcessObjectConfig {
name: "process".to_string(),
linked_process: process.name.clone(),
process_object_name: std::format!("object_{}_process", process.name.to_lowercase()),
}));
Ok(())
}
fn populate_thread_objects(process: &mut system_config::ProcessConfig) {
if process.threads().count() == 1 {
// Mark the first thread as the main_thread.
if let Some(thread) = process.threads_mut().next() {
thread.main_thread = true;
}
}
let main_thread = process
.threads()
.find(|t| t.main_thread)
.unwrap_or_else(|| {
panic!(
"Process `{}` must have at least one thread defined.",
process.name
)
});
// Save the main_thread_name for template usage.
process.main_thread_name = Some(main_thread.name.clone());
}
fn populate_process_and_thread_objects(&mut self) -> Result<()> {
let apps = self.config.base.apps.clone();
for app in self.config.base.apps.iter_mut() {
let app_name = &app.name;
for process in &mut app.processes {
Self::populate_process_objects(process, app_name, &apps)?;
Self::populate_thread_objects(process);
}
}
Ok(())
}
fn populate_memory_mappings(&mut self) {
for app in self.config.base.apps.iter_mut() {
for process in &mut app.processes {
process.memory_mappings.insert(
0,
MemoryMapping {
name: "flash".to_string(),
ty: MemoryMappingType::ReadOnlyExecutable,
start_address: app.flash_start_address,
size_bytes: app.flash_size_bytes,
},
);
process.memory_mappings.insert(
1,
MemoryMapping {
name: "ram".to_string(),
ty: MemoryMappingType::ReadWriteData,
start_address: process.ram_start_address,
size_bytes: process.ram_size_bytes,
},
);
}
}
}
fn populate_interrupt_table(&mut self) -> Result<()> {
// Add any interrupts handled by interrupt objects.
for app in &mut self.config.base.apps {
let app_name = &app.name;
for process in &mut app.processes {
for object in &mut process.objects {
let object_name = object.name().to_string();
let &mut Interrupt(ref mut interrupt_config) = object else {
continue;
};
// Allow defining interrupt objects without requiring an interrupt_table
// to also be defined in the config.
if self.config.base.kernel.interrupt_table.is_none() {
self.config.base.kernel.interrupt_table =
Some(InterruptTableConfig::default());
}
let interrupt_table = self.config.base.kernel.interrupt_table.as_mut().unwrap();
interrupt_config.object_ref_name =
std::format!("INTERRUPT_OBJECT_{}_{object_name}", process.name)
.to_uppercase();
if interrupt_config.irqs.len() > 16 {
return Err(anyhow!(
"Interrupt object {} in app {} has more than 16 interrupts",
object_name,
app_name,
));
}
for (index, irq_config) in interrupt_config.irqs.iter().enumerate() {
let irq_name = &irq_config.name;
let irq = irq_config.number;
if interrupt_table.table.contains_key(&irq.to_string()) {
return Err(anyhow!(
"IRQ {}={} in app {} object {} already handled.",
irq_name,
irq,
app_name,
object_name
));
}
let handler_name = std::format!(
"interrupt_handler_{}_{object_name}_{irq_name}",
process.name
)
.to_lowercase();
interrupt_table
.combined_ordered_table
.insert(irq, handler_name.clone());
interrupt_config.handlers.insert(irq, handler_name.clone());
interrupt_config.interrupt_signal_map.insert(
irq_name.to_string(),
std::format!(
"Signals::INTERRUPT_{}",
(b'A' + u8::try_from(index).unwrap()) as char
),
);
}
}
}
}
// If there's no interrupt_table defined by the user and no interrupt objects,
// there is no need to generate an interrupt table.
if self.config.base.kernel.interrupt_table.is_none() {
return Ok(());
}
let interrupt_table = self.config.base.kernel.interrupt_table.as_mut().unwrap();
// Add any kernel interrupt handlers defined in the config to the ordered list.
for irq_str in interrupt_table.table.keys() {
let irq = irq_str.parse::<u32>().unwrap();
interrupt_table
.combined_ordered_table
// Use the safe wrapper handler to keep the table elements safe.
.insert(irq, std::format!("interrupt_handler_{irq_str}"));
}
// Calculate the size of the interrupt table, which is the highest handled IRQ + 1
interrupt_table.table_size = interrupt_table
.combined_ordered_table
.keys()
.max()
.map(|max_irq| max_irq + 1)
.unwrap_or(0) as usize;
interrupt_table.link_section = self.config.arch.get_interrupt_table_link_section();
Ok(())
}
}
//
// Custom filters
//
#[must_use]
pub fn hex(_state: &State, value: usize) -> String {
format!("{value:#x}")
}
#[must_use]
pub fn to_lower_ident(_state: &State, value: String) -> String {
value.to_lowercase()
}
#[must_use]
pub fn to_upper_ident(_state: &State, value: String) -> String {
value.to_uppercase()
}