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:

  1. A library which is used to perform register accesses
  2. 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. Write value into a register, replacing its entire contents
  • .wfo(field: Field, value:T) - Write field only. Write value 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 of field 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 to field and set it to zero in value, leaving other bits unchanged
  • ms(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);
}