Skip to content

Commit 8d32b00

Browse files
committed
feat: add AVR target with configuration change protection support
Add an `avr` target to svd2rust, whose main purpose is support for the configuration change protection (CCP) mechanism of modern (xmega based) AVR cores: certain registers only accept writes within four instructions after a magic value has been written to an unlock register (`CPU.CCP`). The support consists of two parts: First, a new generic file (`generic_avr_ccp.rs`, included for the `avr` target) provides the `UnlockRegister` and `Protected` marker traits and, for protected registers, `write_protected`/`modify_protected` methods. These mirror `write`/`modify` but perform the unlock sequence in inline assembly, so the protected write is guaranteed to hit the four instruction window regardless of optimization level. Second, since SVD files carry no information about CCP (neither which register unlocks protected writes, nor which registers are protected), the protected register list is declared in the `--settings` YAML file, mirroring the RISC-V approach: avr_config: ccp: unlock_register: "CPU.CCP" protected_registers: - { register: "NVMCTRL.CTRLA", magic: 0x9D } - { register: "WDT.CTRLA", magic: 0xD8 } For every listed register, an `impl Protected` is emitted at the end of the generated device module. Design notes: - The unlock register's address is derived from the SVD (peripheral base address plus register offset) rather than duplicated in the settings file; only its `PERIPHERAL.REGISTER` path is given. - The magic is a raw byte rather than a named signature (SPM/IOREG) to keep svd2rust agnostic of particular AVR families. - Registers referenced in the settings file are resolved against the SVD and generation fails on unknown paths, so typos surface as build errors instead of silently missing impls. - The impls use module-relative paths (`cpu::ccp::CCP_SPEC`), so they are correct regardless of where the device module is mounted in the PAC crate (e.g. `avr-device` places it under `crate::<mcu>`). - A new `Settings::from_yaml` constructor lets PACs that drive svd2rust as a library (like `avr-device`) parse settings files without depending on `serde_yaml` themselves; the CLI uses it too.
1 parent e6cce73 commit 8d32b00

10 files changed

Lines changed: 392 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
1515
the `cortex-m::interrupt::InterruptNumber` trait. A new `add-cortex-m-int-num` option
1616
which allows adding the generation of the old trait.
1717
- Bump MSRV of generated code to 1.81
18+
- Add an `avr` target which includes generic code to access configuration
19+
change protected (CCP) registers in a more convenient way
20+
- Add AVR specific settings (`avr_config`) to the `--settings` file, declaring
21+
which registers are CCP protected; `Protected` trait implementations are
22+
generated from it
1823

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

src/config.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ pub enum Target {
8282
XtensaLX,
8383
#[cfg_attr(feature = "serde", serde(rename = "mips"))]
8484
Mips,
85+
#[cfg_attr(feature = "serde", serde(rename = "avr"))]
86+
Avr,
8587
#[cfg_attr(feature = "serde", serde(rename = "none"))]
8688
None,
8789
}
@@ -94,6 +96,7 @@ impl std::fmt::Display for Target {
9496
Target::RISCV => "riscv",
9597
Target::XtensaLX => "xtensa-lx",
9698
Target::Mips => "mips",
99+
Target::Avr => "avr",
97100
Target::None => "none",
98101
})
99102
}
@@ -107,14 +110,15 @@ impl Target {
107110
"riscv" => Target::RISCV,
108111
"xtensa-lx" => Target::XtensaLX,
109112
"mips" => Target::Mips,
113+
"avr" => Target::Avr,
110114
"none" => Target::None,
111115
_ => bail!("unknown target {}", s),
112116
})
113117
}
114118

115119
pub const fn all() -> &'static [Target] {
116120
use self::Target::*;
117-
&[CortexM, Msp430, RISCV, XtensaLX, Mips]
121+
&[CortexM, Msp430, RISCV, XtensaLX, Mips, Avr]
118122
}
119123
}
120124

@@ -353,9 +357,18 @@ pub struct Settings {
353357
pub crate_path: Option<CratePath>,
354358
/// RISC-V specific settings
355359
pub riscv_config: Option<riscv::RiscvConfig>,
360+
/// AVR specific settings
361+
pub avr_config: Option<avr::AvrConfig>,
356362
}
357363

358364
impl Settings {
365+
/// Parse chip-specific settings from their YAML representation, e.g. the
366+
/// contents of the file passed with `--settings`.
367+
#[cfg(all(feature = "serde", feature = "yaml"))]
368+
pub fn from_yaml(yaml: &str) -> Result<Self> {
369+
serde_yaml::from_str(yaml).map_err(Into::into)
370+
}
371+
359372
pub fn update_from(&mut self, source: Self) {
360373
if source.html_url.is_some() {
361374
self.html_url = source.html_url;
@@ -366,6 +379,9 @@ impl Settings {
366379
if source.riscv_config.is_some() {
367380
self.riscv_config = source.riscv_config;
368381
}
382+
if source.avr_config.is_some() {
383+
self.avr_config = source.avr_config;
384+
}
369385
}
370386

371387
pub fn extra_build(&self) -> Option<TokenStream> {
@@ -405,4 +421,5 @@ impl FromStr for CratePath {
405421
}
406422
}
407423

424+
pub mod avr;
408425
pub mod riscv;

src/config/avr.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/// AVR specific configuration
2+
///
3+
/// The SVD files for AVR devices do not contain any information about the
4+
/// configuration change protection (CCP) mechanism found on modern (xmega
5+
/// based) AVR cores: neither which register unlocks protected writes, nor
6+
/// which registers are protected by it. This configuration fills that gap.
7+
#[cfg_attr(feature = "serde", derive(serde::Deserialize), serde(default))]
8+
#[derive(Clone, PartialEq, Eq, Debug, Default)]
9+
#[non_exhaustive]
10+
pub struct AvrConfig {
11+
/// Configuration change protection (CCP) description for this device
12+
pub ccp: Option<CcpConfig>,
13+
}
14+
15+
#[cfg_attr(feature = "serde", derive(serde::Deserialize), serde(default))]
16+
#[derive(Clone, PartialEq, Eq, Debug, Default)]
17+
#[non_exhaustive]
18+
pub struct CcpConfig {
19+
/// `PERIPHERAL.REGISTER` path of the register that unlocks protected
20+
/// writes when the magic value is written to it, e.g. `CPU.CCP`
21+
pub unlock_register: String,
22+
/// All registers of the device that are configuration change protected
23+
pub protected_registers: Vec<CcpProtectedRegister>,
24+
}
25+
26+
#[cfg_attr(feature = "serde", derive(serde::Deserialize), serde(default))]
27+
#[derive(Clone, PartialEq, Eq, Debug, Default)]
28+
#[non_exhaustive]
29+
pub struct CcpProtectedRegister {
30+
/// `PERIPHERAL.REGISTER` path of the protected register, e.g.
31+
/// `NVMCTRL.CTRLA`
32+
pub register: String,
33+
/// Magic value that must be written to the unlock register to allow
34+
/// writing this register, e.g. `0x9D` (SPM) or `0xD8` (IOREG)
35+
pub magic: u8,
36+
}

src/generate/avr.rs

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
use crate::{svd::Peripheral, util, Config};
2+
use anyhow::{anyhow, Context, Result};
3+
use log::debug;
4+
use proc_macro2::{Span, TokenStream};
5+
use quote::quote;
6+
use svd_parser::svd::{Access, Register};
7+
8+
/// Whole AVR-specific generation.
9+
///
10+
/// SVD files for AVR devices carry no information about the configuration
11+
/// change protection (CCP) mechanism of modern (xmega based) AVR cores, so the
12+
/// list of protected registers and their unlock magic comes from the settings
13+
/// file ([`crate::config::avr::AvrConfig`]). Here we translate that list into
14+
/// `UnlockRegister` and `Protected` trait implementations for the generated
15+
/// register types, so that protected registers can be written with
16+
/// `write_protected`/`modify_protected` from `generic_avr_ccp.rs`.
17+
pub fn render(peripherals: &[Peripheral], config: &Config) -> Result<TokenStream> {
18+
let mut mod_items = TokenStream::new();
19+
20+
let Some(ccp) = config
21+
.settings
22+
.avr_config
23+
.as_ref()
24+
.and_then(|avr| avr.ccp.as_ref())
25+
else {
26+
return Ok(mod_items);
27+
};
28+
29+
debug!("Rendering AVR configuration change protection impls");
30+
31+
// The unlock register itself is not listed as protected in the SVD either;
32+
// its data-space address is all the unlock sequence needs, and we can
33+
// derive that from the SVD instead of asking for it in the settings file.
34+
let (unlock_periph, unlock_reg, unlock_addr) =
35+
resolve_register(peripherals, &ccp.unlock_register)
36+
.context("can't resolve CCP unlock register")?;
37+
let unlock_spec = register_spec_path(unlock_periph, unlock_reg, config);
38+
let unlock_addr = util::hex(unlock_addr);
39+
40+
mod_items.extend(quote! {
41+
impl crate::UnlockRegister for #unlock_spec {
42+
const PTR: *mut u8 = #unlock_addr as *mut u8;
43+
}
44+
});
45+
46+
for protected in &ccp.protected_registers {
47+
let (periph, reg, _) = resolve_register(peripherals, &protected.register)
48+
.with_context(|| format!("can't resolve protected register {}", protected.register))?;
49+
50+
// The unlock sequence in `generic_avr_ccp.rs` only supports 8-bit
51+
// writable registers (`RegisterSpec<Ux = u8> + Writable`); catch
52+
// mismatches here instead of failing later in the PAC build. Size and
53+
// access are usually inherited defaults on AVR SVDs, so only reject
54+
// explicit contradictions.
55+
if let Some(size) = reg.properties.size {
56+
if size != 8 {
57+
return Err(anyhow!(
58+
"protected register {} is {} bits wide; only 8-bit registers are supported",
59+
protected.register,
60+
size
61+
));
62+
}
63+
}
64+
if reg.properties.access == Some(Access::ReadOnly) {
65+
return Err(anyhow!(
66+
"protected register {} is read-only",
67+
protected.register
68+
));
69+
}
70+
71+
let reg_spec = register_spec_path(periph, reg, config);
72+
// Emit the magic as a hex literal so the generated code matches the
73+
// datasheet notation (0x9D/0xD8) instead of decimal.
74+
let magic = util::hex(protected.magic as u64);
75+
76+
mod_items.extend(quote! {
77+
impl crate::Protected for #reg_spec {
78+
const MAGIC: u8 = #magic;
79+
type CcpReg = #unlock_spec;
80+
}
81+
});
82+
}
83+
84+
Ok(mod_items)
85+
}
86+
87+
/// Look up a `PERIPHERAL.REGISTER` path from the settings file in the SVD and
88+
/// return the peripheral, the register and the register's data-space address.
89+
///
90+
/// Registers of derived peripherals are found on the base peripheral, but the
91+
/// address is computed from the derived peripheral's own base address.
92+
fn resolve_register<'a>(
93+
peripherals: &'a [Peripheral],
94+
path: &str,
95+
) -> Result<(&'a Peripheral, &'a Register, u64)> {
96+
let (periph_name, reg_name) = path.split_once('.').ok_or_else(|| {
97+
anyhow!("register path `{path}` must have the form `PERIPHERAL.REGISTER`")
98+
})?;
99+
100+
let periph = peripherals
101+
.iter()
102+
.find(|p| p.name == periph_name)
103+
.ok_or_else(|| anyhow!("no peripheral named `{periph_name}` in the SVD"))?;
104+
105+
// Follow `derivedFrom` (once, as mandated by the SVD spec) in case the
106+
// peripheral inherits its registers from another one.
107+
let base = match periph.derived_from.as_deref() {
108+
Some(base_name) => peripherals
109+
.iter()
110+
.find(|p| p.name == base_name)
111+
.ok_or_else(|| {
112+
anyhow!("peripheral `{periph_name}` is derived from unknown `{base_name}`")
113+
})?,
114+
None => periph,
115+
};
116+
117+
let reg = base
118+
.registers()
119+
.find(|r| r.name == reg_name)
120+
.ok_or_else(|| anyhow!("no register named `{reg_name}` in peripheral `{periph_name}`"))?;
121+
122+
let address = periph.base_address + reg.address_offset as u64;
123+
Ok((periph, reg, address))
124+
}
125+
126+
/// Build the module-relative path to a register's spec type, e.g.
127+
/// `cpu::ccp::CCP_SPEC`. The impls are emitted at the top level of the device
128+
/// module where all peripheral modules are siblings, so no crate-level prefix
129+
/// is needed.
130+
fn register_spec_path(periph: &Peripheral, reg: &Register, config: &Config) -> TokenStream {
131+
let span = Span::call_site();
132+
let periph_mod = util::ident(&periph.name, config, "peripheral_mod", span);
133+
let reg_mod = util::ident(&reg.name, config, "register_mod", span);
134+
let reg_spec = util::ident(&reg.name, config, "register_spec", span);
135+
quote! { #periph_mod::#reg_mod::#reg_spec }
136+
}

src/generate/device.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use crate::config::{Config, RustEdition, Target};
1111
use crate::util::{self, ident};
1212
use anyhow::{Context, Result};
1313

14-
use crate::generate::{interrupt, peripheral, riscv};
14+
use crate::generate::{avr, interrupt, peripheral, riscv};
1515

1616
/// Whole device generation
1717
pub fn render(d: &Device, config: &Config, device_x: &mut String) -> Result<TokenStream> {
@@ -139,6 +139,7 @@ pub fn render(d: &Device, config: &Config, device_x: &mut String) -> Result<Toke
139139
let generic_file = include_str!("generic.rs");
140140
let generic_reg_file = include_str!("generic_reg_vcell.rs");
141141
let generic_atomic_file = include_str!("generic_atomic.rs");
142+
let avr_ccp_file = include_str!("generic_avr_ccp.rs");
142143
if config.generic_mod {
143144
let mut file = File::create(
144145
config
@@ -155,6 +156,9 @@ pub fn render(d: &Device, config: &Config, device_x: &mut String) -> Result<Toke
155156
}
156157
writeln!(file, "\n{generic_atomic_file}")?;
157158
}
159+
if config.target == Target::Avr {
160+
writeln!(file, "\n{}", avr_ccp_file)?;
161+
}
158162

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

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

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

0 commit comments

Comments
 (0)