CSE 291 Project Update 1
This week’s efforts were directed towards strengthening the field accessor types generated by svd2rust for PACs.
Previously, generated field writer types looked like this:
pub type EXTI0_W<'a, REG> = crate::FieldWriter<'a, REG, 4>;
pub type EXTI1_W<'a, REG> = crate::FieldWriter<'a, REG, 4>;
// more...
Some fields of the SYSCFG peripheral.
…and were constructed like this:
pub fn exti0(&mut self) -> EXTI0_W<EXTICR1rs> {
EXTI0_W::new(self, 0)
}
pub fn exti1(&mut self) -> EXTI1_W<EXTICR1rs> {
EXTI1_W::new(self, 4)
}
// more...
Each field is already represented by unique types, and yet the offset was not encoded in the type.
Additionally, field array accessor methods were unbounded, and as such, fallible:
pub const fn ch(&self, n: usize) -> &CH {
&self.ch[n]
}
Field array accessor for DMA channels.
If offsets are not encoded in the types, reasoning cannot be conducted to determine the peripheral state.
To fix this, we migrated the offset from a member to a generic constant:
pub type EXTI0_W<'a> = crate::FieldWriter<'a, EXTICR1rs, 4, 0 /* <- offset */>;
pub type EXTI1_W<'a> = crate::FieldWriter<'a, EXTICR1rs, 4, 4 /* <- offset */>;
// more...
This means for field arrays, many field types must be generated with the corresponding offset baked into the type:
pub type DLYM1R_W<'a> = DLYMR_W<'a, { 0 * 8 + 4 }>;
pub type DLYM2R_W<'a> = DLYMR_W<'a, { 1 * 8 + 4 }>;
pub type DLYM3R_W<'a> = DLYMR_W<'a, { 2 * 8 + 4 }>;
pub type DLYM4R_W<'a> = DLYMR_W<'a, { 3 * 8 + 4 }>;
Types for fields of the
DLYMxRarray from the SAI peripheral.
…and construction now looks like:
pub fn dlym1l(&mut self) -> DLYM1L_W {
DLYM1L_W::new(self)
}
pub fn dlym2l(&mut self) -> DLYM2L_W {
DLYM2L_W::new(self)
}
pub fn dlym3l(&mut self) -> DLYM3L_W {
DLYM3L_W::new(self)
}
pub fn dlym4l(&mut self) -> DLYM4L_W {
DLYM4L_W::new(self)
}
Dynamic dispatch of field array elements is now forbidden. This is due to a combination of limitations to const generic arithmetic and weak trait bounds.
In addition to changing the structure of the types in generic.rs,
these changes required updating the codegen macros. It’s quite the experience!
What it looks like when Rust emits over five thousand errors.
Without going into too much detail, we had to introduce dynamic rendering of the type and impl generics (since some fields have one concrete offset, and others don’t).
let mut generics = Punctuated::<_, Token![,]>::new();
generics.push(GenericParam::Lifetime(LifetimeParam::new(Lifetime::new(
"'a",
Span::call_site(),
))));
if matches!(f, Field::Array(_, _)) {
generics.push(GenericParam::Const(ConstParam {
attrs: Vec::new(),
const_token: Const::default(),
ident: Ident::new("O", Span::call_site()),
colon_token: Colon::default(),
ty: Type::Verbatim(quote! { u8 }),
eq_token: None,
default: None,
}));
}
let generics = Generics {
lt_token: None,
params: generics,
gt_token: None,
where_clause: None,
};
and generate the specific array element field types:
if let Field::Array(_, de) = &f {
for (i, fi) in svd::field::expand(&f, de).enumerate() {
let ident = format_ident!(
"{}_W",
inflections::case::to_constant_case(
field_accessor(&fi.name, config, span).to_string().as_str(),
)
);
let offset_calc = calculate_offset(i as _, de.dim_increment, offset);
mod_items.extend(quote! {
pub type #ident<'a> = #writer_ty<'a, #offset_calc>;
});
}
}
Modifying the svd2rust codegen macros gets irritating fast. The macros are very poorly written, and the generated structures are clearly not designed for such rigid usage.
While we did achieve this week’s goal (everything builds), we fear that this is a rather “bandaid” approach.
So we decided to bail on svd2rust in the interest of correctness.
Despite the ongoing efforts of @burrbull and @usbalbin to upstream these new design choices, we will pursue developing our own bespoke peripheral interface generation system.
Starting from Scratch
SVD
SVD files are riddled with errors, and patches are required for almost every peripheral. Why subject ourselves to patchwork when we can start clean?
Pure Rust
We propose a system for defining register/fields of peripherals with type-stated enforcement built in.
The separation between PAC and HAL is out. Register access should never be opaque.
Now, register types will encode the state they are in.
It is possible higher level encapsulating structures may still be needed, but it is also possible they will not, further investigation is required.
Preliminary design
Register blocks contain registers:
#[block(base_addr = 0x4001_0000, infer_offsets)]
struct SysCfg {
memrmp: MemRmp,
cfgr1: Cfgr1,
exticr1: ExtiCr1,
exticr2: ExtiCr2,
}
Registers contain fields:
#[register]
struct MemRmp {
#[field(offset = 0x00, width = 3, read, write)]
mem_mode: MemMode,
#[field(offset = 0x08, width = 1, read, write)]
fb_mode: bool,
}
These fields are the source of state.
#[field]
enum MemMode {
MainFlash,
SystemFlash,
Fsmc,
Sram1,
QuadSpi,
}
Type-states will be generated for each field variant.
Often times, however, field states are only valid when other field states are inhabited. We propose the use of entitlements to indicate state dependencies.
For example, in the CORDIC peripheral, functions are compatable with some scaling factors:
#[field]
enum Scale {
N0,
N1,
N2,
N3,
N4,
N5,
N6,
N7,
}
#[field]
enum Func {
#[state(entitlements = [N0])]
Cos,
#[state(entitlements = [N0])]
Sin,
#[state(entitlements = [N0])]
ATan2,
#[state(entitlements = [N0])]
Magnitude,
ATan, // no restriction
#[state(entitlements = [N1])]
CosH,
#[state(entitlements = [N1])]
SinH,
#[state(entitlements = [N1])]
ATanH,
#[state(entitlements = [N1, N2, N3, N4])]
Ln,
#[state(entitlements = [N0, N1, N2])]
Sqrt,
}
This is an extreme case with lots of entitelments, and even so, it’s very readable.
In addition, some register operations may be dependent on field states. They can be expressed in a similar way:
#[register]
struct ArgReg {
// cannot be read, and only written to
// if the enabled state is set
#[field(offset = 0x08, write(entitlements = [Enabled])]
arg: Arg
}
We will continue to explore this design next week. So far, the codegen macros are working!