2 PAC Guide
Levi Pearson edited this page 2022-03-10 09:10:14 +00:00

Peripheral Access Crate API Guide

Crate API Structure

StructureDiagram

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.