mirror of
https://github.com/saymrwulf/xous-core.git
synced 2026-11-04 04:27:34 +00:00
add doc notes on the UTRA
This commit is contained in:
parent
27d061fae2
commit
e414c87414
1 changed files with 177 additions and 0 deletions
177
svd2utra/README.md
Normal file
177
svd2utra/README.md
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
# 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](https://crates.io/crates/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.
|
||||
|
||||
## 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:
|
||||
|
||||
```Rust
|
||||
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);
|
||||
}
|
||||
|
||||
```
|
||||
Loading…
Reference in a new issue