Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
the `cortex-m::interrupt::InterruptNumber` trait. A new `add-cortex-m-int-num` option
which allows adding the generation of the old trait.
- Bump MSRV of generated code to 1.81
- Add an `avr` target which includes generic code to access configuration
change protected (CCP) registers in a more convenient way
- Add AVR specific settings (`avr_config`) to the `--settings` file, declaring
which registers are CCP protected; `Protected` trait implementations are
generated from it

## [v0.37.1] - 2025-10-17

Expand Down
19 changes: 18 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ pub enum Target {
XtensaLX,
#[cfg_attr(feature = "serde", serde(rename = "mips"))]
Mips,
#[cfg_attr(feature = "serde", serde(rename = "avr"))]
Avr,
#[cfg_attr(feature = "serde", serde(rename = "none"))]
None,
}
Expand All @@ -94,6 +96,7 @@ impl std::fmt::Display for Target {
Target::RISCV => "riscv",
Target::XtensaLX => "xtensa-lx",
Target::Mips => "mips",
Target::Avr => "avr",
Target::None => "none",
})
}
Expand All @@ -107,14 +110,15 @@ impl Target {
"riscv" => Target::RISCV,
"xtensa-lx" => Target::XtensaLX,
"mips" => Target::Mips,
"avr" => Target::Avr,
"none" => Target::None,
_ => bail!("unknown target {}", s),
})
}

pub const fn all() -> &'static [Target] {
use self::Target::*;
&[CortexM, Msp430, RISCV, XtensaLX, Mips]
&[CortexM, Msp430, RISCV, XtensaLX, Mips, Avr]
}
}

Expand Down Expand Up @@ -353,9 +357,18 @@ pub struct Settings {
pub crate_path: Option<CratePath>,
/// RISC-V specific settings
pub riscv_config: Option<riscv::RiscvConfig>,
/// AVR specific settings
pub avr_config: Option<avr::AvrConfig>,
}

impl Settings {
/// Parse chip-specific settings from their YAML representation, e.g. the
/// contents of the file passed with `--settings`.
#[cfg(all(feature = "serde", feature = "yaml"))]
pub fn from_yaml(yaml: &str) -> Result<Self> {
serde_yaml::from_str(yaml).map_err(Into::into)
}

pub fn update_from(&mut self, source: Self) {
if source.html_url.is_some() {
self.html_url = source.html_url;
Expand All @@ -366,6 +379,9 @@ impl Settings {
if source.riscv_config.is_some() {
self.riscv_config = source.riscv_config;
}
if source.avr_config.is_some() {
self.avr_config = source.avr_config;
}
}

pub fn extra_build(&self) -> Option<TokenStream> {
Expand Down Expand Up @@ -405,4 +421,5 @@ impl FromStr for CratePath {
}
}

pub mod avr;
pub mod riscv;
36 changes: 36 additions & 0 deletions src/config/avr.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/// AVR specific configuration
///
/// The SVD files for AVR devices do not contain any information about the
/// configuration change protection (CCP) mechanism found on modern (xmega
/// based) AVR cores: neither which register unlocks protected writes, nor
/// which registers are protected by it. This configuration fills that gap.
#[cfg_attr(feature = "serde", derive(serde::Deserialize), serde(default))]
#[derive(Clone, PartialEq, Eq, Debug, Default)]
#[non_exhaustive]
pub struct AvrConfig {
/// Configuration change protection (CCP) description for this device
pub ccp: Option<CcpConfig>,
}

#[cfg_attr(feature = "serde", derive(serde::Deserialize), serde(default))]
#[derive(Clone, PartialEq, Eq, Debug, Default)]
#[non_exhaustive]
pub struct CcpConfig {
/// `PERIPHERAL.REGISTER` path of the register that unlocks protected
/// writes when the magic value is written to it, e.g. `CPU.CCP`
pub unlock_register: String,
/// All registers of the device that are configuration change protected
pub protected_registers: Vec<CcpProtectedRegister>,
}

#[cfg_attr(feature = "serde", derive(serde::Deserialize), serde(default))]
#[derive(Clone, PartialEq, Eq, Debug, Default)]
#[non_exhaustive]
pub struct CcpProtectedRegister {
/// `PERIPHERAL.REGISTER` path of the protected register, e.g.
/// `NVMCTRL.CTRLA`
pub register: String,
/// Magic value that must be written to the unlock register to allow
/// writing this register, e.g. `0x9D` (SPM) or `0xD8` (IOREG)
pub magic: u8,
}
160 changes: 160 additions & 0 deletions src/generate/avr.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
use crate::{svd::Peripheral, util, Config};
use anyhow::{anyhow, Context, Result};
use log::debug;
use proc_macro2::{Span, TokenStream};
use quote::quote;
use svd_parser::svd::{Access, Register};

/// Whole AVR-specific generation.
///
/// SVD files for AVR devices carry no information about the configuration
/// change protection (CCP) mechanism of modern (xmega based) AVR cores, so the
/// list of protected registers and their unlock magic comes from the settings
/// file ([`crate::config::avr::AvrConfig`]). Here we translate that list into
/// `UnlockRegister` and `Protected` trait implementations for the generated
/// register types, so that protected registers can be written with
/// `write_protected`/`modify_protected` from `generic_avr_ccp.rs`.
pub fn render(peripherals: &[Peripheral], config: &Config) -> Result<TokenStream> {
let mut mod_items = TokenStream::new();

let Some(ccp) = config
.settings
.avr_config
.as_ref()
.and_then(|avr| avr.ccp.as_ref())
else {
return Ok(mod_items);
};

debug!("Rendering AVR configuration change protection impls");

// The unlock register itself is not listed as protected in the SVD either;
// its address is all the unlock sequence needs, and we can derive that
// from the SVD instead of asking for it in the settings file.
let (unlock_periph, unlock_reg, unlock_addr) =
resolve_register(peripherals, &ccp.unlock_register)
.context("can't resolve CCP unlock register")?;

// The unlock sequence writes the magic with `out`, which can only reach
// I/O addresses below 0x40. On every CCP-bearing core those are identical
// to the data-space addresses the SVD describes; reject anything else so
// a bad settings entry fails here instead of in the assembler.
if unlock_addr >= 0x40 {
return Err(anyhow!(
"CCP unlock register {} is at address {:#x}, but `out` can only \
reach I/O addresses below 0x40",
ccp.unlock_register,
unlock_addr
));
}

// Anchor the address on the peripheral's `PeripheralSpec::ADDRESS` const
// instead of duplicating it as a literal; only the register's byte offset
// is emitted directly.
let unlock_spec = register_spec_path(unlock_periph, unlock_reg, config);
let unlock_periph_spec = util::ident(
&unlock_periph.name,
config,
"peripheral_spec",
Span::call_site(),
);
let unlock_offset = util::hex(unlock_reg.address_offset as u64);

mod_items.extend(quote! {
impl crate::UnlockRegister for #unlock_spec {
const ADDR: u8 =
<#unlock_periph_spec as crate::PeripheralSpec>::ADDRESS as u8 + #unlock_offset;
}
});

for protected in &ccp.protected_registers {
let (periph, reg, _) = resolve_register(peripherals, &protected.register)
.with_context(|| format!("can't resolve protected register {}", protected.register))?;

// The unlock sequence in `generic_avr_ccp.rs` only supports 8-bit
// writable registers (`RegisterSpec<Ux = u8> + Writable`); catch
// mismatches here instead of failing later in the PAC build. Size and
// access are usually inherited defaults on AVR SVDs, so only reject
// explicit contradictions.
if let Some(size) = reg.properties.size {
if size != 8 {
return Err(anyhow!(
"protected register {} is {} bits wide; only 8-bit registers are supported",
protected.register,
size
));
}
}
if reg.properties.access == Some(Access::ReadOnly) {
return Err(anyhow!(
"protected register {} is read-only",
protected.register
));
}

let reg_spec = register_spec_path(periph, reg, config);
// Emit the magic as a hex literal so the generated code matches the
// datasheet notation (0x9D/0xD8) instead of decimal.
let magic = util::hex(protected.magic as u64);

mod_items.extend(quote! {
impl crate::Protected for #reg_spec {
const MAGIC: u8 = #magic;
type CcpReg = #unlock_spec;
}
});
}

Ok(mod_items)
}

/// Look up a `PERIPHERAL.REGISTER` path from the settings file in the SVD and
/// return the peripheral, the register and the register's data-space address.
///
/// Registers of derived peripherals are found on the base peripheral, but the
/// address is computed from the derived peripheral's own base address.
fn resolve_register<'a>(
peripherals: &'a [Peripheral],
path: &str,
) -> Result<(&'a Peripheral, &'a Register, u64)> {
let (periph_name, reg_name) = path.split_once('.').ok_or_else(|| {
anyhow!("register path `{path}` must have the form `PERIPHERAL.REGISTER`")
})?;

let periph = peripherals
.iter()
.find(|p| p.name == periph_name)
.ok_or_else(|| anyhow!("no peripheral named `{periph_name}` in the SVD"))?;

// Follow `derivedFrom` (once, as mandated by the SVD spec) in case the
// peripheral inherits its registers from another one.
let base = match periph.derived_from.as_deref() {
Some(base_name) => peripherals
.iter()
.find(|p| p.name == base_name)
.ok_or_else(|| {
anyhow!("peripheral `{periph_name}` is derived from unknown `{base_name}`")
})?,
None => periph,
};

let reg = base
.registers()
.find(|r| r.name == reg_name)
.ok_or_else(|| anyhow!("no register named `{reg_name}` in peripheral `{periph_name}`"))?;

let address = periph.base_address + reg.address_offset as u64;
Ok((periph, reg, address))
}

/// Build the module-relative path to a register's spec type, e.g.
/// `cpu::ccp::CCP_SPEC`. The impls are emitted at the top level of the device
/// module where all peripheral modules are siblings, so no crate-level prefix
/// is needed.
fn register_spec_path(periph: &Peripheral, reg: &Register, config: &Config) -> TokenStream {
let span = Span::call_site();
let periph_mod = util::ident(&periph.name, config, "peripheral_mod", span);
let reg_mod = util::ident(&reg.name, config, "register_mod", span);
let reg_spec = util::ident(&reg.name, config, "register_spec", span);
quote! { #periph_mod::#reg_mod::#reg_spec }
}
19 changes: 18 additions & 1 deletion src/generate/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::config::{Config, RustEdition, Target};
use crate::util::{self, ident};
use anyhow::{Context, Result};

use crate::generate::{interrupt, peripheral, riscv};
use crate::generate::{avr, interrupt, peripheral, riscv};

/// Whole device generation
pub fn render(d: &Device, config: &Config, device_x: &mut String) -> Result<TokenStream> {
Expand Down Expand Up @@ -139,6 +139,7 @@ pub fn render(d: &Device, config: &Config, device_x: &mut String) -> Result<Toke
let generic_file = include_str!("generic.rs");
let generic_reg_file = include_str!("generic_reg_vcell.rs");
let generic_atomic_file = include_str!("generic_atomic.rs");
let avr_ccp_file = include_str!("generic_avr_ccp.rs");
if config.generic_mod {
let mut file = File::create(
config
Expand All @@ -155,6 +156,9 @@ pub fn render(d: &Device, config: &Config, device_x: &mut String) -> Result<Toke
}
writeln!(file, "\n{generic_atomic_file}")?;
}
if config.target == Target::Avr {
writeln!(file, "\n{}", avr_ccp_file)?;
}

if !config.make_mod {
out.extend(quote! {
Expand All @@ -173,6 +177,9 @@ pub fn render(d: &Device, config: &Config, device_x: &mut String) -> Result<Toke
}
syn::parse_file(generic_atomic_file)?.to_tokens(&mut tokens);
}
if config.target == Target::Avr {
syn::parse_file(avr_ccp_file)?.to_tokens(&mut tokens);
}

out.extend(quote! {
#[allow(unused_imports)]
Expand Down Expand Up @@ -356,5 +363,15 @@ pub fn render(d: &Device, config: &Config, device_x: &mut String) -> Result<Toke
});
}

// The CCP impls reference the register spec types generated above, so they
// are emitted at the very end of the device module for readability (impl
// order is irrelevant to the compiler).
if config.target == Target::Avr {
out.extend(
avr::render(&d.peripherals, config)
.context("can't render AVR configuration change protection")?,
);
}

Ok(out)
}
Loading
Loading