Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion MODULE.bazel.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions hal/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Licensed under the Apache-2.0 license
# SPDX-License-Identifier: Apache-2.0

load("@rules_rust//rust:defs.bzl", "rust_doc", "rust_library")

package(default_visibility = ["//visibility:public"])

rust_library(
name = "hal",
srcs = glob([
"src/*.rs",
"src/**/*.rs",
]),
crate_name = "openprot_hal",
edition = "2024",
)

rust_doc(
name = "openprot_hal_doc",
crate = ":hal",
)
264 changes: 264 additions & 0 deletions hal/src/field_mux.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
// Licensed under the Apache-2.0 license
// SPDX-License-Identifier: Apache-2.0

//! Register-field mux mechanism: inline `FieldWrite`s coalesced to one RMW per register at compile time.

use crate::resource::FieldWrite;
use core::marker::PhantomData;

/// Fixed-size compile-time coalescing scratch; narrowed to the exact live count before apply, so it never reaches the binary.
pub(crate) const MAX_REGS: usize = 16;

/// Central batch of register writes, coalesced to one op per distinct offset: `[..len]` of `ops` live.
#[derive(Clone, Copy)]
pub struct RegBatch {
/// Register ops, one per distinct offset; only `[..len]` are valid.
ops: [FieldWrite; MAX_REGS],
/// Number of live ops.
len: usize,
}

impl RegBatch {
/// An empty accumulator.
#[must_use]
pub const fn new() -> Self {
Self {
ops: [FieldWrite {
offset: 0,
set: 0,
clr: 0,
}; MAX_REGS],
len: 0,
}
}

/// Coalesce one write into the batch, grouped by register offset via [`find`](Self::find).
const fn push(&mut self, w: FieldWrite) {
let j = self.find(w.offset);
if j == self.len {
assert!(self.len < MAX_REGS, "MAX_REGS too small");
self.ops[self.len] = w;
self.len += 1;
} else {
self.ops[j].coalesce(w);
}
}

///merges via `FieldWrite::coalesce`. Crate-private, can only be called by fns with pin token authority provided.
pub(crate) const fn append(&mut self, writes: &[FieldWrite]) {
let mut i = 0;
while i < writes.len() {
self.push(writes[i]);
i += 1;
}
}

/// Merge another batch's live ops in, coalescing per register — the tuple/handle-list fold step.
pub(crate) const fn append_batch(&mut self, other: &RegBatch) {
let mut i = 0;
while i < other.len {
self.push(other.ops[i]);
i += 1;
}
}

/// Live op count — the exact `N` [`into_ops`](Self::into_ops) narrows to so the fixed scratch tail never reaches the binary.
#[must_use]
pub const fn len(&self) -> usize {
self.len
}

/// True if no route has been appended yet.
#[must_use]
pub const fn is_empty(&self) -> bool {
self.len == 0
}

/// Consume the batch into its exact `[FieldWrite; N]` ops (no zeroed tail); const-panics unless `N == len`.
#[must_use]
pub const fn into_ops<const N: usize>(self) -> [FieldWrite; N] {
assert!(self.len == N, "into_ops: N must equal batch len");
let mut out = [FieldWrite {
offset: 0,
set: 0,
clr: 0,
}; N];
let mut k = 0;
while k < N {
out[k] = self.ops[k];
k += 1;
}
out
}

// HACK: the MAX_REGS-wide `ops` rides to rodata; `into_ops` trims it but needs generic_const_exprs.
/// The live ops as a tight slice — `[..len]` of the fixed scratch.
#[must_use]
pub const fn as_slice(&self) -> &[FieldWrite] {
self.ops.split_at(self.len).0
}

/// Index of `offset` in `ops[..len]`, or `len` if absent — group-by-register for [`append`].
const fn find(&self, offset: u32) -> usize {
let mut j = 0;
while j < self.len {
if self.ops[j].offset == offset {
return j;
}
j += 1;
}
self.len
}
}

impl Default for RegBatch {
fn default() -> Self {
Self::new()
}
}

/// A register block a coalesced mux batch can be applied to: one read + one write per offset.
/// A target impls these two methods once; `apply` replaces a per-offset match in the applier.
pub trait RegBlock {
/// Read the 32-bit register at `offset`.
fn read_reg(&self, offset: u32) -> u32;
/// Write `val` to the 32-bit register at `offset`.
fn write_reg(&self, offset: u32, val: u32);
}

/// Apply an exact-N mux batch to a block: one RMW (`reg = (reg & !clr) | set`) per register. Panic-free — a flat walk over the tight slice, no indexing.
pub fn apply(block: &impl RegBlock, batch: &[FieldWrite]) {
for op in batch {
block.write_reg(op.offset, (block.read_reg(op.offset) & !op.clr) | op.set);
}
}

/// Coalesce a list of per-pin routes into one batch (one RMW per register) — the compile-time fold.
#[must_use]
pub const fn coalesce_routes(routes: &[&[FieldWrite]]) -> RegBatch {
let mut b = RegBatch::new();
let mut i = 0;
while i < routes.len() {
b.append(routes[i]);
i += 1;
}
b
}

/// A bound handle (or tuple of handles) whose SCU mux routes are folded to one RMW per register at
/// compile time in [`COALESCED`](MuxRoutes::COALESCED); the applier just flushes that const.
pub trait MuxRoutes {
/// This handle's routes, coalesced to one op per register — folded per monomorphization at compile time.
const COALESCED: RegBatch;
/// Distinct-register count of [`COALESCED`](Self::COALESCED) — the exact array length to narrow to before apply.
const LEN: usize = Self::COALESCED.len();
}

/// Route a handle through a shared reference: the applier reads only the type, so a borrow folds identically — non-Copy owners route without being consumed.
impl<H: MuxRoutes> MuxRoutes for &H {
const COALESCED: RegBatch = H::COALESCED;
}

/// Impl [`MuxRoutes`] for every tuple arity by folding each member's `COALESCED` into one batch at
/// compile time; recurses on the tail, so one invocation covers all arities down to the 1-tuple.
macro_rules! impl_muxroutes_tuple {
() => {};
($head:ident $(, $tail:ident)*) => {
impl<$head: MuxRoutes $(, $tail: MuxRoutes)*> MuxRoutes for ($head, $($tail,)*) {
const COALESCED: RegBatch = {
let mut out = RegBatch::new();
out.append_batch(&$head::COALESCED);
$( out.append_batch(&$tail::COALESCED); )*
out
};
}
impl_muxroutes_tuple!($($tail),*);
};
}

// HACK: tuple arity caps pins-per-apply at this ident count (no variadic generics); widen the list if needed.
impl_muxroutes_tuple!(A, B, C, D, E, F, G, H, I, J, K, L);

/// A silicon-fixed singleton register block (e.g. the SCU): `BASE` names it, no pin token carries it.
///
/// # Safety
/// `BASE` must name a valid, `'static`, aligned register block usable for `u32` volatile access at every offset its driver touches.
#[allow(unsafe_code)]
pub unsafe trait Block {
/// The silicon-fixed base of this singleton register block.
const BASE: *const u8;
}

/// Confined MMIO applier over block `B`, phantom-typed so one block's applier can't stand in for another's.
pub struct Mmio<B> {
base: *const u8,
_block: PhantomData<B>,
}

impl<B> Clone for Mmio<B> {
fn clone(&self) -> Self {
*self
}
}
impl<B> Copy for Mmio<B> {}

impl<B> Mmio<B> {
/// HAL-internal ctor: base always rides on authored `DATA` or a `Block::BASE`, whose validity was promised upstream.
#[must_use]
pub(crate) const fn from_raw(base: *const u8) -> Self {
Self {
base,
_block: PhantomData,
}
}

/// Read the 32-bit register at byte `offset`.
#[must_use]
pub fn read_reg(&self, offset: u32) -> u32 {
// SAFETY: `base` names a valid register block sized for `u32` access at every offset used.
#[allow(unsafe_code)]
// nosemgrep
unsafe {
self.base.add(offset as usize).cast::<u32>().read_volatile()
}
}

/// Write `val` to the 32-bit register at byte `offset`.
pub fn write_reg(&self, offset: u32, val: u32) {
// SAFETY: `base` names a valid register block sized for `u32` access at every offset used.
#[allow(unsafe_code)]
// nosemgrep
unsafe {
(self.base.add(offset as usize) as *mut u32).write_volatile(val)
}
}

/// True if `bit` is set in the register at byte `offset`.
#[must_use]
pub fn read_bit(&self, offset: u32, bit: u8) -> bool {
self.read_reg(offset) & (1 << bit) != 0
}

/// Read-modify-write a `mask`-wide field at `shift` in the register at `offset` to `val`.
pub fn modify_field(&self, offset: u32, shift: u8, mask: u32, val: u32) {
let cur = self.read_reg(offset);
self.write_reg(offset, (cur & !(mask << shift)) | ((val & mask) << shift));
}
}

impl<B: Block> Mmio<B> {
/// Mint a token-less singleton's applier from its `Block::BASE`; safe, the `unsafe impl Block` made the promise.
#[must_use]
pub const fn block() -> Self {
Self::from_raw(B::BASE)
}
}

impl<B> RegBlock for Mmio<B> {
fn read_reg(&self, offset: u32) -> u32 {
Mmio::read_reg(self, offset)
}
fn write_reg(&self, offset: u32, val: u32) {
Mmio::write_reg(self, offset, val)
}
}
Loading