Peripheral Access Crate API Guide
Additional Reference Links
- SVD-covered registers and fields
- Generated stm32f1 code
- Generated stm32f1 PAC docs
- API description from svd2rust
Crate API Structure
Crate top-level items
The whole set of peripherals is managed by a singleton object of type Peripherals in the base namespace of the crate. You take ownership of this object via Peripherals::take()
; this returns Option<Peripherals>
because it can only be performed once and will subsequently return None
.
The Peripherals
struct will have a single field for each peripheral that was described in the SVD file from which the crate was built, with the same name. Each of these peripheral fields has a type by the same name (e.g. the GPIOA
field is of type GPIOA
) defined at the top level of the crate. This is a proxy object; essentially it guards ownership of a method that returns the pointer to the base address of the memory-mapped registers for the peripheral. It looks like:
pub const fn ptr() -> *const RegisterBlock
Each proxy object also has a Deref
impl for that same RegisterBlock
, which is defined inside a sub-module of the appropriately-cased name of the field/type. So if you have an object p: Peripherals
the field p.GPIOA
will be a proxy object of type GPIOA
that will dereference to gpioa::RegisterBlock
, which is where the individual registers are described. If you want to access the field crl
of gpioa::RegisterBlock
, you can do it via p.GPIOA.crl
because of the Deref<gpioa::RegisterBlock>
impl on the GPIOA
proxy.
Because Peripherals
is a zero-sized type that exists largely to control access to constant functions, it ends up pretty much evaporating once compiler optimizations are turned on. Although it can be a little awkward to trace through the auto-generated documentation due to the indirection, it's not really necessary to use the crate-level documentation since it all follows an extremely regular pattern based on the SVD file, which follows the programmer's reference manuals from the manufacturer.
For convenience, the peripherals from the CPU itself are re-exported under the structure CorePeripherals
; this has the same style API as Peripherals
(and in fact is named Peripherals
in the cortex_m
crate).
Peripheral sub-modules
Each peripheral has its own sub-module in the crate, named as a lowercase version of the field name and proxy object type that it has in the top-level Peripherals
object.
The most important object in this sub-module is the RegisterBlock
object, which plays the same role that Peripherals
did at the top level. It is a struct with one field per register; these fields are named as an all-lowercase version of the register name from the SVD file and they have a type that is an all-uppercase version of the name. For example, on the STM32F103, the rcc::RegisterBlock
struct has a field named cfgr
of type CFGR
. So if you have a value p: Peripherals
and use the expression p.RCC.cfgr
you will get the object of type CFGR
that can be used to access the CFGR
register of the RCC
peripheral.
A major difference is that rather than being zero-sized, it is actually allocated at the region of the address space where the peripheral registers are memory-mapped, so its fields are at the locations in address space corresponding to the peripheral control registers they are named after. It does not take away from usable RAM or Flash space.
Each field is defined as a single-member structure holding a vcell::VolatileCell<T>
where T
is the register width, which is u32
on the STM32 family. The vcell
crate's VolatileCell
structure is a transparent wrapper around the standard Rust UnsafeCell
that provides get
and set
access methods that do ptr::read_volatile()
and ptr::write_volatile()
operations to ensure that the compiler doesn't optimize them away or re-order them.
Each register also has a sub-module (with the same name as the register's field name) under the peripheral sub-module, where proxy objects for individual field access are defined, often along with enum definitions for the values that are valid for each field. Whether they exist or not depends on whether they were present in the SVD file. For example, the CFGR
register of the RCC
peripheral mentioned earlier has the sub-module rcc::cfgr
where those items are found.
Register sub-modules
Each register sub-module is organized around two structs, R
and W
. A register with any readable fields will have R
, and with any writable fields it will have W
. Both have a single field called bits
that holds an integer of appropriate width, such as u32
, to match the type T
used for the vcell::VolatileCell<T>
mapped to the register itself in the parent module. You can think of an R
as a copy of the register's contents from which you can extract individual fields, and a W
as a staging value that you can modify individual fields of before copying them all-at-once into the register.
The first-level methods defined in the register sub-module are actually defined on the field access struct from the peripheral sub-module; e.g. for the STM32F103's rcc::cfgr
sub-module, methods are defined on the rcc::CFGR
struct that holds the vcell::VolatileCell<u32>
for the register. The basic methods defined are read
, write
, modify
, reset
, and reset_value
. These methods do their work by giving controlled access to R
and W
values and using those to directly interact with the register cell. The availability of all the methods depends on the presence of readable and writable fields in the register.
The methods look like this:
fn read(&self) -> R
fn reset_value(&self) -> T // where T is the integer type of the underlying vcell::VolatileCell<T>
fn reset(&self)
fn write<F>(&self, f: F) // where F: FnOnce(&mut W) -> &mut W
fn modify<F>(&self, f: F) // where for<'w> F: FnOnce(&R, &'w mut W) -> &'w mut W
The read method is available whenever there are readable fields. It takes a copy of the current register contents via the underlying vcell
's volatile_read
operation and gives it to you as an R
value.
The reset_value
method is not often used directly by the programmer, but it is important to understand what it is to understand how write
works. Hardware typically has a network of circuitry throughout it so that when it is first powered on, or perhaps when it has gone into an undesired state, it can be brought back to a well-defined "reset" state. Although RAM usually has at least partially random values when a system is powered on or reset, memory-mapped configuration registers are usually given well-defined values by the reset circuitry in a peripheral so they don't start out in a random configuration. The reset value is usually given by the programming manual for the part and the SVD file, but if you need to use it in your program (and it was set correctly in the SVD file) you can access it via this method.
The reset
method just writes the register's reset value back into all the writable fields, which puts that register back into the state which it had when first starting up. The write is performed via the vcell's volatile_write
operation.
The write
method allows you to set one or more fields of a register by passing in a FnOnce
closure. That closure will be given a mutable reference to a W
value that is initialized with the reset value of the register, not the current value actually contained in the register. This ensures that as long as any field you change is set to a valid value, the whole W
should contain only valid field values. At the end of your closure's execution, the W
is written to the register via the vcell
's volatile_write
operation.
The modify
method allows you to change one or more fields of a register by passing in a FnOnce
closure. That closure will be given a R
reference and a mutable W
reference that is initialized to hold the same bit-pattern as the R
. The R
is set to the value of the underlying register via the underlying vcell
's volatile_read
operation just before your closure is invoked. Your closure can read any fields via the R
and overwrite any fields via the W
. At the end of your closure's execution, the W
is written to the register via the vcell
's volatile_write
operation.
Field-level read access
Every R
definition on a u32
register will include the method fn bits(&self) -> u32
, which reads and returns the entire contents of the register. If you need the whole register value for some reason, you can just call reg.read().bits()
, although it is generally better to access specific fields, even if they are the width of the entire register.
For each field defined for this register in the source SVD file, a method named after the field will exist, which takes &self
and returns an enum
type with the same name as the method, but all-caps and with an R
appended. For example, for the STM32F103, the rcc::cfgr::R
struct has a method pub fn sw(&self) -> SWR
which reads the SW
field of the CFGR
register of the RCC
peripheral. The enumerated possible values are described by the type rcc::cfgr::SWR
.
The enumerated "possible values" read type is also defined in the current module; it will have one variant for each enumerated value in the SVD file for the register field, with the same name as in the SVD file but in all-caps. If the enumerated values don't cover all possible bit patterns that a field of its bit-width could hold, it will also have a variant named _Reserved(T)
where T
is an integer type large enough to hold all possible field values; this is used for when the value doesn't match any defined enumerated values in the SVD, or when no enumerated values were defined.
Each field value enumerated type is also equipped with a set of methods of the following form:
fn bits(&self) -> T
fn _from(value: T) -> Self
is_X(&self) -> bool // for each X where X is the lowercased name of one of the enum variants
For single-bit fields, the bits method is replaced with the following three methods:
fn bit(&self) -> bool
fn bit_is_set(&self) -> bool
fn bit_is_clear(&self) -> bool
The bits
method is usually only needed if no variants were defined in the SVD file for the field; it can also be helpful if the field value has an interesting numeric value instead of just being a selector. The bit
, bit_is_set
, and bit_is_clear
methods are similar; they may be the only ones available for a single-bit field if the SVD doesn't give any information about what being set or clear means.
The _from
method is a helper to construct the enumerated type inside the field access methods on R
.
The rest of the methods are boolean predicates, for when checking for a specific value is more useful than a pattern match. Not strictly necessary, but sometimes helpful.
Field-level write access
Every W
definition will include the method unsafe fn bits(&mut self, bits: u32) -> &mut Self
which will replace the entire contents of the register with the supplied bits
parameter. This is unsafe because you must manually verify that all sub-fields of the register are being given valid bit-values. These two operations form the basis for implementing all the field-specific access methods that may also exist.
For each field in this register defined in the source SVD file, a method named after the field will exist on the W
struct that takes &mut self
and returns a proxy object. The proxy's type is the all-caps name of the field with an underscore in front and a W
at the end. Each one is defined as a struct with a single field that holds the same &mut W
passed to the field-writing method to begin with. The function of this proxy is to hold methods that will update the named field within the W
value. Those methods allow specifying the field value either by a raw numeric value or by an enumerated type, which has the same name as the proxy type but without the underscore.
The proxy methods generally follow this form:
fn bits(self, value: T) -> &'a mut W // (which is sometimes marked unsafe as well)
fn variant(self, value: ENUMW) -> &'a mut W // where ENUMW is the enumerated type for this field's values
Also, for each variant in the enumerated type for this field's values, there is a setter named as a lowercase version of the variant which takes self and returns &'a mut W
after setting the proxy's field in the W
to the value named by the setter method.
For the example of the STM32F103's RCC CFGR
register, rcc:cfgr::W
has a method pub fn sw(&mut self) -> _SWW
, which returns a _SWW
proxy that holds a &mut W
reference. The enumerated values for writing to that field are described by rcc::cfgr::SWW
. Writing a value to this field would be accomplished with chained methods such as this:
rcc.CFGR.write(|w| w.sw().pll());
// or
rcc.CFGR.write(|w| w.sw().variant(SWW::PLL));
// or
rcc.CFGR.write(|w| unsafe { w.sw().bits(2) });
The bits
method is unsafe in this case because the SVD did not define variants for all possible bitfield values, meaning that some numbers don't represent valid field values and it is up to the programmer to verify that they are passing a valid number.
Since the value-setting methods on the proxy object all return &'a mut W
, they can be easily chained to set multiple fields in the same write
or update
call.