Target Specification and Hardware Registers with UTRA
Xous isolates target-specific code, such as the location and fields of hardware registers, inside the Unambiguous Thin Register Astraction (UTRA).
The UTRA is contained in the utralib
crate, and its contents are generated from SVD files using svd2utra
. SVD itself is an XML format commonly used by hardware vendors for the interchange of SoC registers, and is a native output format of SoC building tools such as LiteX. SVD files are also readily available for many commercial MCUs.
Specifying Targets
Services that talk to hardware will typically have one or more implementation
modules that encapsulate the target-specific back ends. These are gated with #[cfg(feature="X")]
directives, where "X" is the specific build target.
This means that a specific build target is selected by passing a --feature
flag during compilation, e.g. --feature precursor
, --feature hosted
, or --feature renode
.
About the UTRA
UTRA is a register abstraction for accessing hardware resources. It tries to be:
- Unambiguous -- the access rules should be concise and unambiguous to a systems programmer with a C background
- Thin -- it should hide constants, but not bury them so they become difficult to verify
Here is an example of an ambiguous style of register access, from a PAC generated using svd2rust:
// this seems clear -- as long as all the bit fields are specified
// (they actually aren't, so some non-obvious things are happening)
p.POWER.power.write(|w|
w.discharge().bit(true)
.soc_on().bit(false)
.kbddrive().bit(true)
.kbdscan().bits(3)
);
// what should this do?
// 1. just set the discharge bit to true and everything else to zero?
// 2. read the register first, change only the discharge bit to true, leaving the rest unchanged?
p.POWER.power.write(|w|
w.discharge().bit(true)
);
// answer: it does (1). You need to use the `modify()` function to have (2) happen.
While the closure-chaining is clever syntax, it's also ambiguous.
First, does the chaining imply an order of writes happening in
sequence, or do they all happen at once? The answer depends on Rust's
optimizer, which is very good and one can expect the behavior to be
the latter, but it is still write-ordering behavior that depends upon
the outcome of an optimizer and not a linguistic guarantee. Second,
the term write
itself is ambiguous when it comes to bitfields: do we
write just the bitfield, or do we write the entire register, assuming
the rest of the contents are zero? These types of ambiguity make it
hard to audit code, especially for experts in systems programming
who are not also experts in Rust.
The primary trade-off for achieving unambiguity and thinness is less type checking and type hardening, because we are not fully taking advantage of the advanced syntax features of Rust.
That being said, a certain degree of deliberate malleability in the register abstraction is desired to assist with security-oriented audits: for a security audit, it is often just as important to ask what the undefined bits do, as it is to check the settings of the defined bits. Malleabilty allows an auditor to quickly create targeted tests that exercise undefined bits. Existing Rust-based access crates create strict types that eliminate the class of errors where constants defined for one register are used in an incorrect type of register, but they also make it very hard to modify in an ad-hoc manner.
UTRA API Details
This crate is designed to serve as an alternative to svd2rust
. It generates
a crate which consists of:
- A library which is used to perform register accesses
- A "header file" (library) that is auto-generated from a given
soc.svd
file
The library provides the a function template for CSR
that provides the following
methods:
.r(reg: Register) -> T
- Read. Reads the entire contents of a CSR.rf(field: Field) -> T
- Read Field. Read a CSR and return only the masked and shifted value of a sub-field.wo(reg: Register, value:T)
- Write only. Writevalue
into a register, replacing its entire contents.wfo(field: Field, value:T)
- Write field only. Writevalue
into a field of a register, zeroizing all the other fields and replacing its entire contents.rmwf(field: Field, value:T)
- Read-modify-write a register. Replace just the contents offield
while leaving the other fields intact. The current implementation makes no guarantees about atomicity.
Register
and Field
are generated by the library; Field
refers to
the Register
to which it belongs, and thus it is not necessary to
specify it explicitly. Furthermore, the base address of the CSR
is
bound when the object is created, which allows the crate to work both
with physical and virtual addresses by replacing the base address with
the desired value depending upon the active addressing mode.
In addition to the CSR
function template, convenience constants for
the CSR base, as well as any memory bases and interrupts, are also
generated by this crate.
This set of API calls supports the most common set of use cases, which is reading, writing, and updating single fields of a register, or entire registers all at once.
The API does not natively support setting two fields simultaneously. This is because there can be nuances to this that depend upon the hardware implementation, such as bit fields that are self-resetting, registers that self-clear on read, or registers that have other automatic and implicit side effects.
Users that require multiple bit fields to be set simultaneously must explicitly read the CSR value, bind it to a temporary variable, mask out the fields they want to replace, and combine in the values before writing it back to the CSR.
To aid with this, the following helper functions are also available:
zf(field:Field, value:T) -> T
- Zeroize field. Take bits corresponding tofield
and set it to zero invalue
, leaving other bits unchangedms(field:Field, value:T) -> T
- Mask and shift. Take value, mask it to the field width, and then shift to its final position.
The idea here is that the .r(register)
method is used to read the
entire register; then successive .zf(field, value)
calls are made to
clear the fields prior to setting. Field values are OR'd with the
result of .ms(field, value)
to create the final composite register
value. Finally, .wo(value)
is used to overwrite the entire
register with the final composite register value.
The .ms(field,value)
can also be used to synthesize initial register
values that need to be committed all at once to a hardware register,
before a .wo(value)
call.
Example Usage
Let's assume you've used svd2utra.py to create a utra
crate in the
same directory as svd2utra.py, and you've added this to your Cargo.toml
file.
Now, inside your lib.rs
file, you might have something like this:
use utra
fn test_fn() {
// Audio tests
// The audio block is a pointer to *mut 32.
let mut audio = CSR::new(HW_AUDIO_BASE as *mut u32);
// Read the entire contents of the RX_CTL register
audio.r(utra::audio::RX_CTL);
// Or read just one field
audio.rf(utra::audio::RX_CTL_ENABLE);
// Do a read-modify-write of the specified field
audio.rmwf(utra::audio::RX_CTL_RESET, 1);
// Do a multi-field operation where all fields are updated in a single write.
// First read the field into a temp variable.
let mut stat = audio.r(utra::audio::RX_STAT);
// in read replica, zero EMPTY and RDCOUNT
stat = audio.zf(utra::audio::RX_STAT_EMPTY, stat);
stat = audio.zf(utra::audio::RX_STAT_RDCOUNT, stat);
// in read replica, now set RDCOUNT to 0x123
stat |= audio.ms(utra::audio::RX_STAT_RDCOUNT, 0x123);
// commit read replica to register, updating both EMPTY and RDCOUNT in a single write
audio.wo(utra::audio::RX_STAT, stat);
// UART tests
// Create the UART register as a pointer to *mut u8
let mut uart = CSR::new(HW_UART_BASE as *mut u8);
// Write the RXTX field of the RXTX register
uart.wfo(utra::uart::RXTX_RXTX, b'a');
// Or you can write the whole UART register
uart.wo(utra::uart::RXTX, b'a');
assert_ne!(uart.rf(pac::uart::TXFULL_TXFULL), 1);
// Anomalies
// This compiles but requires a cast since `audio` is a pointer to
// u32, whereas `uart` is a pointer to u8.
audio.wfo(utra::uart::RXTX_RXTX, b'a' as _);
// This also compiles, despite the fact that the register offset is
// mismatched and nonsensical
audio.wfo(utra::uart::TXFULL_TXFULL, 1);
}