Examples#

This section includes an example RDL file, the auto-generated cargo doc documentation for the exported module, and example usage.

Example RDL#

Files: turboencabulator.rdl and udps.rdl.

The turboencabulator code was exported using

// in build.rs
Generator::new()
    .rdl_file("udps.rdl")
    .rdl_file("turboencabulator.rdl")
    .top("turbo_encab")
    .format_output(true)
    .generate();

Cargo docs for the exported turboencabulator module can be viewed here: turboencabulator docs. Click on the source button in the docs to see the generated source for any module.

Note that the generated code relies on the peakrdl-rust crate on crates.io.

Example Usage#

This section contains examples for common tasks and features that PeakRDL-rust supports.

Note that many examples contain type annotations for clarity. These annotations can typically be omitted in normal use.

Reading a Register#

use peakrdl_rust::reg::Reg;
use turboencabulator::components::turbo_encab::status::Status;

// Get a representation of the status register for the Turbo Encabulator
let status_reg: Reg<Status> = TURBO_ENCAB.status();

// Read the register, returning a newtype of its 32-bit value
let status: Status = status_reg.read();

// Access register fields from the previously read value
let side_fumbling: u16 = status.side_fumbling();
let stator_rpm: u16 = status.stator_rpm();

Links:

  • Docs for registers

  • Cargo docs for the top-level TurboEncab addrmap type

  • Cargo docs for the Status register type

  • Cargo docs for the Reg type

Writing a Register#

use peakrdl_rust::reg::Reg;
use turboencabulator::components::turbo_encab::ctrl::Ctrl;

// Get a representation of the control register for the Turbo Encabulator
let ctrl_reg: Reg<Ctrl> = TURBO_ENCAB.ctrl();

// Writing to the register takes a closure
ctrl_reg.write(|ctrl: &mut Ctrl| {
    // The input to the closure is the default value of the register,
    // which can be updated via field setter methods before being
    // written to hardware.
    ctrl.set_reset(false);
    ctrl.set_diractance(100);
    ctrl.set_reluctance((-0.375).into()); // fixed-point field
});

// Read the value back to check its fields
let ctrl_value = ctrl_reg.read();
assert_eq!(ctrl_value.reset(), false);
assert_eq!(ctrl_value.diractance(), 100);
assert_eq!(ctrl_value.reluctance().to_f32(), -0.375);

Links:

  • Docs for registers

  • Cargo docs for the top-level TurboEncab addrmap type

  • Cargo docs for the Ctrl register type

  • Cargo docs for the Reg type

Modifying a Register#

use peakrdl_rust::reg::Reg;
use turboencabulator::components::turbo_encab::ctrl::Ctrl;

// Get a representation of the control register for the Turbo Encabulator
let ctrl_reg: Reg<Ctrl> = TURBO_ENCAB.ctrl();

// The `modify` method performs a read-modify-write access.
ctrl_reg.modify(|ctrl: &mut Ctrl| {
    // The closure argument is the current value of the register
    assert_eq!(ctrl.reset(), false);
    assert_eq!(ctrl.diractance(), 100);

    // The value can be updated, and is then written back to hardware
    ctrl.set_reset(true)
});

// Read the value back to check its fields
let ctrl_value = ctrl_reg.read();
assert_eq!(ctrl_value.reset(), true);
assert_eq!(ctrl_value.diractance(), 100);

Links:

  • Cargo docs for the top-level TurboEncab addrmap type

  • Cargo docs for the Ctrl register type

  • Cargo docs for the Reg type

Arrays of Components#

use turboencabulator::components::turbo_encab::grammeter::Grammeter;

// The SystemRDL source defines an array of 12 grammeters. Handles to each
// are accessed by the getter method. These lightweight handles store nothing
// but an address to the corresponding component.
let grammeters: [Grammeter; 12] = TURBO_ENCAB.grammeter();

// If only one grammeter is required, the compiler optimizes out the computations
// for the other addresses.
let sync_failed: bool = TURBO_ENCAB.grammeter()[3].status().read().sync_failed();

Links:

Enum-Encoded Fields#

The Turbo Encabulator SystemRDL contains a state field with a defined encoding:

field {
    name = "Grammeter State";
    desc = "Indicates the state of the cardinal grammeter";

    enum grammeter_state_e {
        RESET = 2'd0 {
            desc = "Grammeter is in reset state and *not* ready to be used.";
        };
        SYNC = 2'd1 {
            desc = "Automatic synchronization in progress.";
        };
        READY = 2'd2 {
            desc = "Grammeter has successfully synchronized with the stator";
        };
        SYNC_FAIL = 2'd3 {
            desc = "
                Synchronization error!

                It is likely that the differential girdlespring is not on the 'up' position.
            ";
        };
    };
    sw=r; hw=w;
    encode = grammeter_state_e;
} state[3];

PeakRDL-rust translates this encoding into an enum type that can be used as follows:

use peakrdl_rust::encode::UnknownVariant;
use turboencabulator::components::turbo_encab::grammeter::status::state::GrammeterStateE;

match TURBO_ENCAB.grammeter()[3].status().read().state() {
    // Fields with the "encode" property are represented by a Rust enum
    Ok(GrammeterStateE::Reset) => println!("Grammeter 3 is in reset"),
    Ok(GrammeterStateE::Sync) => println!("Grammeter 3 is synchronizing"),
    Ok(GrammeterStateE::Ready) => println!("Grammeter 3 is ready"),
    Ok(GrammeterStateE::SyncFail) => println!("Grammeter 3 is in a failed state"),

    // When reading, a Result::Err is returned if the value doesn't match
    // any encoded variant. The Enum is returned directly (instead of a
    // Result type) if all possible states are encoded.
    Err(UnknownVariant(value)) => println!("Unknown state: {value}"),
}

Links:

Accessing a Memory#

SystemRDL memories implement the Memory trait.

use peakrdl_rust::mem::{MemEntry, Memory as _};
use turboencabulator::components::turbo_encab::measurements::Measurements;

let measurement_mem: Measurements = TURBO_ENCAB.measurements();
assert_eq!(measurement_mem.num_entries(), 57);
assert_eq!(measurement_mem.width(), 32);

// Access a memory entry by index
let mut first_entry: MemEntry<Measurements> = measurement_mem.index(0);
first_entry.write(0x1234_5678);
assert_eq!(first_entry.read(), 0x1234_5678);

// Iterate over all memory entries
for (i, mut entry) in measurement_mem.iter().enumerate() {
    entry.write(i as u32);
}
assert_eq!(measurement_mem.index(36).read(), 36);

Virtual registers instantiated within memories are fully supported.

Links:

Fixedpoint Fields#

use turboencabulator::components::turbo_encab::ctrl::ReluctanceFixedPoint;

let ctrl_reg = TURBO_ENCAB.ctrl();
ctrl_reg.write(|ctrl| {
    // The SystemRDL source describes the `reluctance` field as a signed fixed-point
    // number with 1 integer bit and 7 fractional bits. The field accessor methods
    // take and return a `FixedPoint` type, generic over the intwidth and fracwidth.
    //
    // `ReluctanceFixedPoint` is a type alias for `FixedPoint<i8, 1, 7>`
    ctrl.set_reluctance(ReluctanceFixedPoint::from_bits(0xD0_u8 as i8));
    ctrl.set_reluctance(ReluctanceFixedPoint::from_bits(-48));
    ctrl.set_reluctance((-0.375).into());
});

// The three field values above are equivalent
let ctrl_value = ctrl_reg.read();
assert_eq!(ctrl_value.reluctance().to_bits(), 0xD0_u8 as i8);
assert_eq!(ctrl_value.reluctance().to_bits(), -48);
assert_eq!(ctrl_value.reluctance().to_f32(), -0.375);

// Several convenience methods are available on the type:
assert_eq!(ReluctanceFixedPoint::intwidth(), 1);
assert_eq!(ReluctanceFixedPoint::fracwidth(), 7);
assert_eq!(ReluctanceFixedPoint::is_signed(), true);
assert_eq!(ReluctanceFixedPoint::resolution().to_f32(), 0.0078125);
assert_eq!(ReluctanceFixedPoint::min_value().to_f32(), -1.0);
assert_eq!(ReluctanceFixedPoint::max_value().to_f32(), 0.9921875);
assert_eq!(ReluctanceFixedPoint::quantize(1.0_f32), 0.9921875);

Links:

Advanced: Tunneled Registers#

Sometimes the registers to access are located on another device through a tunneled interface. For example, a chip with internal registers that is accessed over a SPI bus. PeakRDL-rust supports this by allowing the user to define a custom RegisterIO implementation.

// ZST implementing the register accesses over SPI.
// This can use a globally accessible SPI interface, or it can own the
// interface. It is passed around by shared reference.
struct SpiRegisterIO;

// Implementers of the `RawRegisterIO` trait automatically implement
// the `RegisterIO` trait. The blanket implementation takes care of
// common details like accesswidth and endianness, so we don't have to
// worry about that here.
impl peakrdl_rust::io::RawRegisterIO for SpiRegisterIO {
    // Some SPI error type defined externally. If the register access is
    // infallible, set this to `core::convert::Infallible` and the `Reg`
    // type will expose infallible access methods (e.g., `modify` in addition
    // to `try_modify`).
    type Error = SpiError;

    unsafe fn try_read<T: RegInt>(&self, ptr: *const T) -> Result<T, Self::Error> {
        // The device only supports 16-bit register accesses
        assert_eq!(core::mem::size_of::<T>(), 2, "SPI registers must be 16 bits wide");
        let spi_addr = ptr.addr() as u8;
        let command: [u8; 3] = build_spi_read_access(spi_addr);
        let result: [u8; 3] = spi_transaction(command)?;
        let data: &[u8; 2] = extract_spi_read_data(result);
        Ok(T::from_ne_bytes(data.try_into().unwrap()))
    }

    unsafe fn try_write<T: RegInt>(&self, ptr: *mut T, value: T) -> Result<(), Self::Error> {
        // The device only supports 16-bit register accesses
        assert_eq!(core::mem::size_of::<T>(), 2, "SPI registers must be 16 bits wide");
        let spi_addr = ptr.addr() as u8;
        let spi_data: &[u8] = value.to_ne_bytes().as_ref();
        let command: [u8; 3] = build_spi_write_access(spi_addr, spi_data);
        spi_transaction(command)?;
        Ok(())
    }
}

fn main() {
    // pass the RegisterIO implementation into the addrmap constructor
    let spi_registers = SpiAddrmap::from_ptr_with(0 as _, &SpiRegisterIO);
    // now access registers as normal
    let reg0_value = spi_registers.reg0().try_read().unwrap();
}

Links: