24 releases

0.1.23 Sep 22, 2024
0.1.22 Jan 14, 2024
0.1.21 Aug 8, 2023
0.1.19 Jul 19, 2023
0.1.10 Nov 26, 2022

#71 in Hardware support

Download history 323/week @ 2024-08-22 226/week @ 2024-08-29 272/week @ 2024-09-05 248/week @ 2024-09-12 409/week @ 2024-09-19 415/week @ 2024-09-26 394/week @ 2024-10-03 290/week @ 2024-10-10 375/week @ 2024-10-17 337/week @ 2024-10-24 521/week @ 2024-10-31 514/week @ 2024-11-07 538/week @ 2024-11-14 475/week @ 2024-11-21 258/week @ 2024-11-28 265/week @ 2024-12-05

1,631 downloads per month
Used in 7 crates (via utralib)

MIT/Apache

50KB
965 lines

Unambiguous Thin Register Abstraction (UTRA)

Motivation

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 unambiguousness 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. Malleability 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.

API

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);
}

Dependencies

~1.5MB
~21K SLoC