Skip to content

Implement special register access mode to write to AVR protected registers#784

Merged
burrbull merged 1 commit into
rust-embedded:masterfrom
G33KatWork:avr_ccp
Jul 15, 2026
Merged

Implement special register access mode to write to AVR protected registers#784
burrbull merged 1 commit into
rust-embedded:masterfrom
G33KatWork:avr_ccp

Conversation

@G33KatWork

Copy link
Copy Markdown
Contributor

The newer AVR Attiny chips contain a mechanism Microchip calls CCP: Configuration Change Protection.

There are a few special registers that configure system-level peripherals like the clock- or the flash-controller which should not be modified by accident. To prevent that, a special unlock byte has to be written to the CCP unlock register and then the protected register has to be written within the next four executed instructions. See chapter 8.5.7 on page 56 on the linked datasheet for more details.

I tried doing this using the regular register access mechanisms, but the rust compiler always reordered and optimized code so that this didn't work out. Even if it would have, it wouldn't be exactly reliable.

This patch adds a new Target type for AVRs and if it's selected the new file generic_avr_ccp.rs is emitted when generating code. This file contains a bunch of traits to define an unlock register, protected registers and blanket implementations that implement protected writes in case a register implements the aforementioned marker traits.

This has to be used in conjunction with atdf2svd to get an SVD. After generating the pac, you still need to manually define the list of protected registers like this:

pub use crate::generic::ProtectedWritable;

#[cfg(feature = "attiny817")]
pub mod attiny817 {
    use crate::generic::{UnlockRegister, Protected};

    // Mark the CPU.CCP register with the UnlockRegister trait so that it can be used to unlock the below defined registers
    impl UnlockRegister for crate::attiny817::cpu::ccp::CCP_SPEC { const PTR: *mut u8 = 0x34 as *mut u8; }

    // Configuration change protected registers in NVMCTRL
    impl Protected for crate::attiny817::nvmctrl::ctrla::CTRLA_SPEC { const MAGIC: u8 = 0x9D; type CcpReg = crate::attiny817::cpu::ccp::CCP_SPEC; }
    impl Protected for crate::attiny817::nvmctrl::ctrlb::CTRLB_SPEC { const MAGIC: u8 = 0xD8; type CcpReg = crate::attiny817::cpu::ccp::CCP_SPEC; }

    // Configuration change protected registers in CLKCTRL
    impl Protected for crate::attiny817::clkctrl::mclkctrlb::MCLKCTRLB_SPEC { const MAGIC: u8 = 0xD8; type CcpReg = crate::attiny817::cpu::ccp::CCP_SPEC; }
    impl Protected for crate::attiny817::clkctrl::mclklock::MCLKLOCK_SPEC { const MAGIC: u8 = 0xD8; type CcpReg = crate::attiny817::cpu::ccp::CCP_SPEC; }
    impl Protected for crate::attiny817::clkctrl::xosc32kctrla::XOSC32KCTRLA_SPEC { const MAGIC: u8 = 0xD8; type CcpReg = crate::attiny817::cpu::ccp::CCP_SPEC; }
    impl Protected for crate::attiny817::clkctrl::mclkctrla::MCLKCTRLA_SPEC { const MAGIC: u8 = 0xD8; type CcpReg = crate::attiny817::cpu::ccp::CCP_SPEC; }
    impl Protected for crate::attiny817::clkctrl::osc20mctrla::OSC20MCTRLA_SPEC { const MAGIC: u8 = 0xD8; type CcpReg = crate::attiny817::cpu::ccp::CCP_SPEC; }
    impl Protected for crate::attiny817::clkctrl::osc20mcaliba::OSC20MCALIBA_SPEC { const MAGIC: u8 = 0xD8; type CcpReg = crate::attiny817::cpu::ccp::CCP_SPEC; }
    impl Protected for crate::attiny817::clkctrl::osc20mcalibb::OSC20MCALIBB_SPEC { const MAGIC: u8 = 0xD8; type CcpReg = crate::attiny817::cpu::ccp::CCP_SPEC; }
    impl Protected for crate::attiny817::clkctrl::osc32kctrla::OSC32KCTRLA_SPEC { const MAGIC: u8 = 0xD8; type CcpReg = crate::attiny817::cpu::ccp::CCP_SPEC; }

    // Configuration change protected registers in RSTCTRL
    impl Protected for crate::attiny817::rstctrl::swrr::SWRR_SPEC { const MAGIC: u8 = 0xD8; type CcpReg = crate::attiny817::cpu::ccp::CCP_SPEC; }

    // Configuration change protected registers in CPUINT
    impl Protected for crate::attiny817::cpuint::ctrla::CTRLA_SPEC { const MAGIC: u8 = 0xD8; type CcpReg = crate::attiny817::cpu::ccp::CCP_SPEC; }

    // Configuration change protected registers in BOD
    impl Protected for crate::attiny817::bod::ctrla::CTRLA_SPEC { const MAGIC: u8 = 0xD8; type CcpReg = crate::attiny817::cpu::ccp::CCP_SPEC; }

    // Configuration change protected registers in WDT
    impl Protected for crate::attiny817::wdt::ctrla::CTRLA_SPEC { const MAGIC: u8 = 0xD8; type CcpReg = crate::attiny817::cpu::ccp::CCP_SPEC; }
    impl Protected for crate::attiny817::wdt::status::STATUS_SPEC { const MAGIC: u8 = 0xD8; type CcpReg = crate::attiny817::cpu::ccp::CCP_SPEC; }

    // Configuration change protected registers in TCD0
    impl Protected for crate::attiny817::tcd0::faultctrl::FAULTCTRL_SPEC { const MAGIC: u8 = 0xD8; type CcpReg = crate::attiny817::cpu::ccp::CCP_SPEC; }
}

I have not found a way yet to figure out the information that a register is protected by using the ATDF/SVD.

There will be a follow-up PR on avr-device which uses svd2rust to generate PACs for all kinds of AVR devices. I added support for the attiny817 which depends on this new CCP register access code.

As soon as I submitted that PR, I'll cross-reference it.

I am open to suggestions on how to make the code nicer. Especially the parts written in assembler. It was very finicky to make this work and I am still not 100% satisfied with how it looks. I tried getting rid of the ldi instruction, but couldn't make it work.

@burrbull

Copy link
Copy Markdown
Member

I have not found a way yet to figure out the information that a register is protected by using the ATDF/SVD.

If ATDF contains this information, svd2rust could use atdf2svd as library dependency and take it directly.

@G33KatWork

Copy link
Copy Markdown
Contributor Author

I just checked again. There is a key link missing.

The CCP register is defined and the two magic values that are required to be written into it to either unlock an IOREG or perform a flash memory write.

However, if we look at the register description for an IOREG-protected register like CLKCTRL.MCLKCTRLB, I don't see mentioned that it is actually IOREG protected.

So I think I was wrong. I have this stuff laying around for a while now and must've forgotten that this link is missing.

@burrbull

Copy link
Copy Markdown
Member

Generic part looks good to me. But I still don't see sense to merge this without Protected implementations.

As we have a possibility to assign settings.yaml files with additional descriptions, I suggest to add Avr section here.
See how it is done for riscv: #856

@G33KatWork G33KatWork force-pushed the avr_ccp branch 2 times, most recently from 9298272 to 8d32b00 Compare July 14, 2026 23:02
@G33KatWork

Copy link
Copy Markdown
Contributor Author

I just revisited the patch and implemented the settings.yaml approach. Here is an example of how such a file looks like:

# Configuration change protection (CCP) description for this chip.
#
# The ATDF/SVD files do not encode which registers are CCP protected, so the
# list is maintained here by hand, from the "Register Summary" tables in the
# datasheet (protected registers are marked "Configuration Change Protection").
#
# Magic values (CPU.CCP signatures): 0x9D = SPM, 0xD8 = IOREG.
avr_config:
  ccp:
    unlock_register: "CPU.CCP"
    protected_registers:
      - { register: "NVMCTRL.CTRLA", magic: 0x9D }
      - { register: "NVMCTRL.CTRLB", magic: 0xD8 }
      - { register: "CLKCTRL.MCLKCTRLA", magic: 0xD8 }
      - { register: "CLKCTRL.MCLKCTRLB", magic: 0xD8 }
      - { register: "CLKCTRL.MCLKLOCK", magic: 0xD8 }
      - { register: "CLKCTRL.OSC20MCTRLA", magic: 0xD8 }
      - { register: "CLKCTRL.OSC20MCALIBA", magic: 0xD8 }
      - { register: "CLKCTRL.OSC20MCALIBB", magic: 0xD8 }
      - { register: "CLKCTRL.OSC32KCTRLA", magic: 0xD8 }
      - { register: "CLKCTRL.XOSC32KCTRLA", magic: 0xD8 }
      - { register: "RSTCTRL.SWRR", magic: 0xD8 }
      - { register: "CPUINT.CTRLA", magic: 0xD8 }
      - { register: "BOD.CTRLA", magic: 0xD8 }
      - { register: "WDT.CTRLA", magic: 0xD8 }
      - { register: "WDT.STATUS", magic: 0xD8 }
      - { register: "TCD0.FAULTCTRL", magic: 0xD8 }

Once this patch has been merged, I'll put up a PR for avr-device that makes use of this feature.

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.
@burrbull burrbull added this pull request to the merge queue Jul 15, 2026
Merged via the queue into rust-embedded:master with commit aeada2e Jul 15, 2026
60 checks passed
@G33KatWork G33KatWork deleted the avr_ccp branch July 15, 2026 18:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants