Skip to content

Commit ae6c15d

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 asm block contains only the two instructions that must be adjacent — `out CCP, magic` and the protected store. The magic is passed as an `in(reg_upper)` operand (the `ldi`-capable r16..=r31 class, avr-libc's `"d"` constraint in `ccp_write_io`) instead of being loaded inside the block, so the compiler can hoist the load out of loops and reuse a register that already holds the value across consecutive same-magic writes. - The protected store is `st X` with the runtime register pointer rather than a compile-time `sts`: the pointer comes from the register reference the method is called on, which keeps writes correct for `derivedFrom` peripherals whose instances share one set of register spec types at different base addresses. The `sts` form would save one word and the X clobber per call site, but requires per-register address consts plus a codegen guard against aliased peripherals. - No tiny/xmega split (avr-libc has one): the split only optimizes compile-time-constant addresses; the pointer variant we use is identical on the xmega family and reduced-core tinys, and avoids `sts`, the one instruction that differs (7-bit form on reduced cores). - `UnlockRegister::ADDR` is generated as `<CPU_SPEC as PeripheralSpec>::ADDRESS as u8 + <offset>` — anchored on the peripheral's existing address const instead of duplicating a literal. Codegen rejects unlock registers at addresses >= 0x40, which `out` cannot reach. - 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 ae6c15d

11 files changed

Lines changed: 438 additions & 10 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: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
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 address is all the unlock sequence needs, and we can derive that
33+
// 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+
38+
// The unlock sequence writes the magic with `out`, which can only reach
39+
// I/O addresses below 0x40. On every CCP-bearing core those are identical
40+
// to the data-space addresses the SVD describes; reject anything else so
41+
// a bad settings entry fails here instead of in the assembler.
42+
if unlock_addr >= 0x40 {
43+
return Err(anyhow!(
44+
"CCP unlock register {} is at address {:#x}, but `out` can only \
45+
reach I/O addresses below 0x40",
46+
ccp.unlock_register,
47+
unlock_addr
48+
));
49+
}
50+
51+
// Anchor the address on the peripheral's `PeripheralSpec::ADDRESS` const
52+
// instead of duplicating it as a literal; only the register's byte offset
53+
// is emitted directly.
54+
let unlock_spec = register_spec_path(unlock_periph, unlock_reg, config);
55+
let unlock_periph_spec = util::ident(
56+
&unlock_periph.name,
57+
config,
58+
"peripheral_spec",
59+
Span::call_site(),
60+
);
61+
let unlock_offset = util::hex(unlock_reg.address_offset as u64);
62+
63+
mod_items.extend(quote! {
64+
impl crate::UnlockRegister for #unlock_spec {
65+
const ADDR: u8 =
66+
<#unlock_periph_spec as crate::PeripheralSpec>::ADDRESS as u8 + #unlock_offset;
67+
}
68+
});
69+
70+
for protected in &ccp.protected_registers {
71+
let (periph, reg, _) = resolve_register(peripherals, &protected.register)
72+
.with_context(|| format!("can't resolve protected register {}", protected.register))?;
73+
74+
// The unlock sequence in `generic_avr_ccp.rs` only supports 8-bit
75+
// writable registers (`RegisterSpec<Ux = u8> + Writable`); catch
76+
// mismatches here instead of failing later in the PAC build. Size and
77+
// access are usually inherited defaults on AVR SVDs, so only reject
78+
// explicit contradictions.
79+
if let Some(size) = reg.properties.size {
80+
if size != 8 {
81+
return Err(anyhow!(
82+
"protected register {} is {} bits wide; only 8-bit registers are supported",
83+
protected.register,
84+
size
85+
));
86+
}
87+
}
88+
if reg.properties.access == Some(Access::ReadOnly) {
89+
return Err(anyhow!(
90+
"protected register {} is read-only",
91+
protected.register
92+
));
93+
}
94+
95+
let reg_spec = register_spec_path(periph, reg, config);
96+
// Emit the magic as a hex literal so the generated code matches the
97+
// datasheet notation (0x9D/0xD8) instead of decimal.
98+
let magic = util::hex(protected.magic as u64);
99+
100+
mod_items.extend(quote! {
101+
impl crate::Protected for #reg_spec {
102+
const MAGIC: u8 = #magic;
103+
type CcpReg = #unlock_spec;
104+
}
105+
});
106+
}
107+
108+
Ok(mod_items)
109+
}
110+
111+
/// Look up a `PERIPHERAL.REGISTER` path from the settings file in the SVD and
112+
/// return the peripheral, the register and the register's data-space address.
113+
///
114+
/// Registers of derived peripherals are found on the base peripheral, but the
115+
/// address is computed from the derived peripheral's own base address.
116+
fn resolve_register<'a>(
117+
peripherals: &'a [Peripheral],
118+
path: &str,
119+
) -> Result<(&'a Peripheral, &'a Register, u64)> {
120+
let (periph_name, reg_name) = path.split_once('.').ok_or_else(|| {
121+
anyhow!("register path `{path}` must have the form `PERIPHERAL.REGISTER`")
122+
})?;
123+
124+
let periph = peripherals
125+
.iter()
126+
.find(|p| p.name == periph_name)
127+
.ok_or_else(|| anyhow!("no peripheral named `{periph_name}` in the SVD"))?;
128+
129+
// Follow `derivedFrom` (once, as mandated by the SVD spec) in case the
130+
// peripheral inherits its registers from another one.
131+
let base = match periph.derived_from.as_deref() {
132+
Some(base_name) => peripherals
133+
.iter()
134+
.find(|p| p.name == base_name)
135+
.ok_or_else(|| {
136+
anyhow!("peripheral `{periph_name}` is derived from unknown `{base_name}`")
137+
})?,
138+
None => periph,
139+
};
140+
141+
let reg = base
142+
.registers()
143+
.find(|r| r.name == reg_name)
144+
.ok_or_else(|| anyhow!("no register named `{reg_name}` in peripheral `{periph_name}`"))?;
145+
146+
let address = periph.base_address + reg.address_offset as u64;
147+
Ok((periph, reg, address))
148+
}
149+
150+
/// Build the module-relative path to a register's spec type, e.g.
151+
/// `cpu::ccp::CCP_SPEC`. The impls are emitted at the top level of the device
152+
/// module where all peripheral modules are siblings, so no crate-level prefix
153+
/// is needed.
154+
fn register_spec_path(periph: &Peripheral, reg: &Register, config: &Config) -> TokenStream {
155+
let span = Span::call_site();
156+
let periph_mod = util::ident(&periph.name, config, "peripheral_mod", span);
157+
let reg_mod = util::ident(&reg.name, config, "register_mod", span);
158+
let reg_spec = util::ident(&reg.name, config, "register_spec", span);
159+
quote! { #periph_mod::#reg_mod::#reg_spec }
160+
}

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)