This week’s efforts were directed towards implementing the first level of static analysis and the fascilitation of entitlements.

Static Analysis

The first level of static analysis has been implemented:

  • fields must be in order and cannot overlap
  • fields must reside within the register (32 bits)
#[register(infer_offsets)]
pub struct Csr {
    #[field(read, write, reset = Cos)]
    func: Func,
    #[field(read, write, reset = P20)]
    precision: Precision,
    #[field(read, write, reset = N0)]
    scale: Scale,

    #[field(offset = 0x10, reset = Disabled)]
    ien: Enable,
    #[field(read, write, reset = Disabled)]
    dmaren: Enable,
    #[field(read, write, reset = Disabled)]
    dmawen: Enable,
    #[field(read, write, reset = One)]
    nres: NData,
    #[field(read, write, reset = One)]
    nargs: NData,
    #[field(read, write, reset = Q15)]
    ressize: DataSize,
    #[field(read, write, reset = Q15)]
    argsize: DataSize,

    #[field(offset = 0x1f, read, reset = NoData)]
    rrdy: Rrdy,
}

This register declaration is correct.

#[register(infer_offsets)]
pub struct Csr {
    #[field(read, write, reset = Cos)]
    func: Func,
    #[field(read, write, reset = P20)]
    precision: Precision,
    #[field(read, write, reset = N0)]
    scale: Scale,

    #[field(offset = 0x14, reset = Disabled)]
    ien: Enable,
    #[field(read, write, reset = Disabled)]
    dmaren: Enable,
    #[field(read, write, reset = Disabled)]
    dmawen: Enable,
    #[field(read, write, reset = One)]
    nres: NData,
    #[field(read, write, reset = One)]
    nargs: NData,
    #[field(read, write, reset = Q15)]
    ressize: DataSize,
    #[field(read, write, reset = Q15)]
    argsize: DataSize,

    #[field(offset = 0x1f, read, reset = NoData)]
    rrdy: Rrdy,
}

This register declaration produces both a field overlap error and a register out of bounds error:

Both of these errors are produced by panics during constant evaluation.

The register macro expands the struct as a module with many constants for static analysis. The above errors are emitted by:

const _: () = {
    if !(argsize::OFFSET + super::data_size::WIDTH < rrdy::OFFSET) {
        {
            core::panicking::panic_fmt(core::const_format_args!(
                "field domains must be in order and non-overlapping. overlaps with argsize",
            ));
        };
    }
};
const _: () = {
    if !(rrdy::OFFSET + super::rrdy::WIDTH <= 32) {
        {
            core::panicking::panic_fmt(core::const_format_args!(
                "field domain exceeds register domain",
            ));
        };
    }
};

Failure of This Approach

#[register(infer_offsets)]
pub struct Csr {
    #[field(read, write, reset = Cos)]
    func: Func,
    #[field(read, write, reset = P20)]
    precision: Precision,
    #[field(read, write, reset = N0)]
    scale: Scale,

    #[field(offset = 0x11, reset = Disabled)]
    ien: Enable,
    #[field(read, write, reset = Disabled)]
    dmaren: Enable,
    #[field(read, write, reset = Disabled)]
    dmawen: Enable,
    #[field(read, write, reset = One)]
    nres: NData,
    #[field(read, write, reset = One)]
    nargs: NData,
    #[field(read, write, reset = Q15)]
    ressize: DataSize,
    #[field(read, write, reset = Q15)]
    argsize: DataSize,

    #[field(offset = 0x1f, read, reset = NoData)]
    rrdy: Rrdy,
}

This register declaration is incorrect but produces no compiler errors. Why?

There is a gap between argsize and rrdy, so shifting all of those fields by one bit does not cause a colision with rrdy nor the register bounds.

This is the kind of mistake which does not violate any rules but is simply a misrepresentation of the hardware.

How could we catch this?

How could we make this kind of error harder to make?

Entitlements

We’ve talked a lot about the idea of entitlements. But how can these be enforced?

Back in the project propositum we introduced the idea of observation as a means of representing resource usage by multiple peripherals, requiring a frozen state of the observed peripheral component.

The word “observation” was a placeholder, and did not fully encapsulate the purpose. While mutating the locked state was not permitted (the state is frozen), invoking actions on the locked peripheral component is still permitted.

For example, the SPI peripheral would want to lock a pin in a particular alternate function. The CLK or MOSI pins would be pulled high or low by the SPI peripheral, which is certainly more involvement than purely an observation.

Well, as it turns out, the system we created for observation is exactly what entitlements are!

For example, say multiple peripherals require a certain bus characteristic to be in effect, let’s say the PLLQ output of the RCC PLL.

With standard move semantics (the classic way to represent the forbidding of state change) would only allow one peripheral to require this state to be frozen.

With the observation entitlement system, multiple entitlement tokens can be dispatched to all peripherals which require that state.

let ([tim15_entitlement, spi3_entitlement], frozen_pllq) = pllq.freeze();

// both of these peripherals hold the entitlement token
// as a means of requiring that state to be set
let tim15 = ctx.device.TIM15.attach(tim15_token);
let spi3 = ctx.device.SPI3.attach(spi3_token);

frozen_pllq.disable(); // does not compile, this method no longer is available

Let’s look at this in action with the CORDIC peripheral.

The block struct is transformed into a fully generic struct like so:

struct Cordic<Csr, Wdata, Rdata> {
    csr: Csr,
    wdata: Wdata,
    rdata: Rdata,
}

But looking back at the declaration:

#[block(
    base_addr = 0x4002_1000,
    infer_offsets,
    entitlements = [super::ahb::cordic_en::Enabled]
)]
struct Cordic {
    csr: Csr,
    wdata: WData,
    rdata: RData,
}

This block is entitled to a particular state of the ahb bus controller. The struct shall express this with an additional field:

struct Cordic<Csr, Wdata, Rdata, Entitlement0> {
    csr: Csr,
    wdata: Wdata,
    rdata: Rdata,

    entitlement0: Entitlement0,
}

So the reset type signature will be as follows:

type CordicReset = Cordic<csr::Reset, wdata::Reset, rdata::Reset, Unsatisfied>;

All method implementations will require the Entitlement0 generic parameter to be Entitlement<T>.

Let’s see this in action:

let ahb = rcc.ahb.cordic_en::<Enabled>().transition(); // move ahb out of rcc and transition

let ([cordic_en_entitlement], frozen_cordic_en) = ahb.cordic_en.freeze(); // move cordic_en out of ahb and freeze

let cordic = ctx.device.CORDIC; // : Cordic<csr::Reset, wdata::Reset, rdata::Reset, Unsatisfied>

let cordic = cordic.csr(|reg| {
    reg.func::<Sin>().transition()
}); // does not compile: method csr(..) doesn't exist

let cordic = cordic.attach(cordic_en_entitlement); // : Cordic<csr::Reset, wdata::Reset, rdata::Reset, Entitlement<ahb::cordic_en::Enabled>>

let cordic = cordic.csr(|reg| {
    reg.func::<Sin>().transition()
}); // works

In this case the peripheral was entitled to a particular state, but sometimes multiple states are valid. This would result in the generic parameter of Entitlement to be promoted to a generic of the impl block, with a trait requirement. This trait would be generated and implemented by the register and block macros.