Skip to content

Commit aeada2e

Browse files
authored
Merge pull request #784 from G33KatWork/avr_ccp
Implement special register access mode to write to AVR protected registers
2 parents e6cce73 + ae6c15d commit aeada2e

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)