blob: af633f1d194c713299315928706567bf707e487d [file]
// Licensed under the Apache-2.0 license
// SPDX-License-Identifier: Apache-2.0
use core::ptr::NonNull;
use core::ptr::{read_volatile, write_volatile};
use heapless::spsc::Queue;
const SWMBX_DEV_COUNT: usize = 2;
const SWMBX_NODE_COUNT: usize = 256;
const SWMBX_FIFO_COUNT: usize = 4;
const SWMBX_FIFO_DEPTH: usize = 256;
const _: () = assert!(SWMBX_NODE_COUNT == 256);
const _: () = assert!(SWMBX_NODE_COUNT == (u8::MAX as usize) + 1);
/// Number of address bits packed into each protection bitmap word.
const PROTECT_BITS_PER_WORD: usize = 32;
/// Shift converting a protection word index into its base node address.
const PROTECT_WORD_SHIFT: u32 = PROTECT_BITS_PER_WORD.ilog2();
/// Count of 32-bit words needed to cover every node's protection bit.
const PROTECT_WORD_COUNT: usize = SWMBX_NODE_COUNT / PROTECT_BITS_PER_WORD;
const SWMBX_PROTECT: u8 = 1 << 0;
const SWMBX_NOTIFY: u8 = 1 << 1;
const SWMBX_FIFO: u8 = 1 << 2;
const FLAG_MASK: u8 = SWMBX_PROTECT | SWMBX_NOTIFY | SWMBX_FIFO;
const SWMBX_FIFO_NOTIFY_START: u8 = 1 << 0;
const SWMBX_FIFO_NOTIFY_STOP: u8 = 1 << 1;
/// Error type for SW mailbox controller operations.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[repr(u16)]
pub enum SwmbxError {
InvalidPort = 0x1001,
InvalidAddress = 0x1002,
InvalidFifoIndex = 0x1003,
InvalidFifoDepth = 0x1004,
InvalidFlagMask = 0x1005,
InvalidProtectRange = 0x1006,
FifoFull = 0x1007,
FifoEmpty = 0x1008,
// 0x1009 retired: a null/misaligned region base is now a panic (programming
// bug), not a recoverable error. Reserved to keep the diagnostic-code ABI stable.
FifoNotMapped = 0x100A,
}
impl SwmbxError {
/// Stable numeric diagnostic code for firmware telemetry/logging.
pub const fn code(self) -> u16 {
self as u16
}
}
struct SharedRegion<T> {
base: NonNull<T>,
}
impl<T> SharedRegion<T> {
/// Wraps a caller-provided region base address.
///
/// # Safety
/// `addr` must, for the lifetime of the returned region, point to a single
/// valid, mapped, and uniquely-owned `T` (i.e. dereferenceable with correct
/// provenance and no conflicting aliases). Volatile reads/writes are issued
/// against it; an invalid address is undefined behavior.
///
/// # Panics
/// Panics if `addr` is null or not aligned for `T`. When `addr` is a
/// compile-time constant, this is reported as a compile error instead.
#[inline]
pub const unsafe fn from_addr(addr: usize) -> Self {
assert!(addr != 0, "region base is null");
assert!(
addr % core::mem::align_of::<T>() == 0,
"region base is misaligned for T"
);
// SAFETY: non-null is asserted above; full validity is guaranteed by
// this function's own safety contract, which the caller upholds.
Self {
base: unsafe { NonNull::new_unchecked(addr as *mut T) },
}
}
}
#[derive(Copy, Clone, Debug)]
struct SwmbxFifoEntry {
value: u8,
}
#[derive(Copy, Clone, Debug, Default)]
struct SwmbxNode {
notify_flag: bool,
enabled_flags: u8,
}
struct SwmbxFifo<const N: usize> {
queue: Queue<SwmbxFifoEntry, N>,
notify_flag: u8,
notify_start: bool,
fifo_write: bool,
fifo_offset: u8,
enabled: bool,
msg_index: usize,
max_msg_count: usize,
}
impl<const N: usize> SwmbxFifo<N> {
pub const fn new() -> Self {
Self {
queue: Queue::new(),
notify_flag: 0,
notify_start: false,
fifo_write: false,
fifo_offset: 0,
enabled: false,
msg_index: 0,
max_msg_count: SWMBX_FIFO_DEPTH,
}
}
pub fn append_write(&mut self, value: u8) -> Result<(), SwmbxError> {
if self.max_msg_count == 0 {
return Err(SwmbxError::InvalidFifoDepth);
}
if self.queue.len() < self.max_msg_count {
self.queue
.enqueue(SwmbxFifoEntry { value })
.map_err(|_| SwmbxError::FifoFull)?;
if self.msg_index == (self.max_msg_count - 1) {
self.msg_index = 0;
} else {
self.msg_index = (self.msg_index + 1) % self.max_msg_count;
}
Ok(())
} else {
Err(SwmbxError::FifoFull)
}
}
/// Removes and returns the oldest byte, or `FifoEmpty` if drained.
pub fn dequeue(&mut self) -> Result<u8, SwmbxError> {
if let Some(entry) = self.queue.dequeue() {
Ok(entry.value)
} else {
Err(SwmbxError::FifoEmpty)
}
}
pub fn flush(&mut self) {
while self.queue.dequeue().is_some() {}
self.msg_index = 0;
self.notify_start = false;
self.fifo_write = false;
}
}
/// In-memory software mailbox controller.
///
/// Models a shared register file with three overlaid behaviors: flag-gated
/// per-node write **protection** and change **notification**, plus optional
/// **FIFO** remapping of selected addresses. Protection and notification are
/// pure flag-gated policy applied per access; only the FIFO path is stateful,
/// running a small per-port transaction lifecycle (`send_start` opens a FIFO
/// transaction, `send_msg`/`get_msg` append/drain it while open, `send_stop`
/// closes it).
pub struct SwmbxCtrl {
// Global feature switches (SWMBX_PROTECT/NOTIFY/FIFO). Plain `u8`: the
// controller is driven from a single execution context (the i2c service's
// cooperative event loop feeds it serially over IPC), so no interior
// mutability or atomics are needed. Keeping it non-`Cell` also leaves the
// type `!Sync`, so accidental cross-context sharing is a compile error.
mbx_en: u8,
node: [[SwmbxNode; SWMBX_NODE_COUNT]; SWMBX_DEV_COUNT],
fifo: [SwmbxFifo<SWMBX_FIFO_DEPTH>; SWMBX_FIFO_COUNT],
mbx_fifo_execute: [bool; SWMBX_DEV_COUNT],
mbx_fifo_addr: [u8; SWMBX_DEV_COUNT],
mbx_fifo_idx: [u8; SWMBX_DEV_COUNT],
buffer_base: NonNull<u8>,
buffer_size: usize,
}
impl SwmbxCtrl {
#[inline]
fn read_in_region(&self, offset: u8) -> Result<u8, SwmbxError> {
if (offset as usize) >= self.buffer_size {
return Err(SwmbxError::InvalidAddress);
}
// SAFETY: `buffer_base` was validated once during construction and
// remains valid by `new_with_regions`'s safety contract. Bounds are
// checked above, so `add(offset)` stays within the mapped region.
unsafe {
Ok(read_volatile(
self.buffer_base.as_ptr().add(offset as usize),
))
}
}
#[inline]
fn write_in_region(&self, offset: u8, val: u8) -> Result<(), SwmbxError> {
if (offset as usize) >= self.buffer_size {
return Err(SwmbxError::InvalidAddress);
}
// SAFETY: see `read_in_region`; the cached base pointer is valid and
// the checked offset keeps the address within the mapped region.
unsafe { write_volatile(self.buffer_base.as_ptr().add(offset as usize), val) };
Ok(())
}
/// Creates a controller backed by a caller-provided buffer region.
///
/// # Safety
/// `buffer_base` must point to at least `buffer_size` bytes of valid, mapped,
/// uniquely-owned memory for the controller's lifetime; subsequent reads and
/// writes (e.g. via [`Self::get_msg`]/[`Self::send_msg`]) dereference it.
/// Passing an invalid address is undefined behavior even though later
/// accessor methods are safe.
pub unsafe fn new_with_regions(buffer_size: usize, buffer_base: usize) -> Self {
// SAFETY: `buffer_base` validity is part of this function's safety
// contract. We validate null/alignment once and cache the pointer.
let buffer_base = unsafe { SharedRegion::<u8>::from_addr(buffer_base).base };
SwmbxCtrl {
mbx_en: 0,
node: [[SwmbxNode::default(); SWMBX_NODE_COUNT]; SWMBX_DEV_COUNT],
fifo: [
SwmbxFifo::new(),
SwmbxFifo::new(),
SwmbxFifo::new(),
SwmbxFifo::new(),
],
mbx_fifo_execute: [false; SWMBX_DEV_COUNT],
mbx_fifo_addr: [0; SWMBX_DEV_COUNT],
mbx_fifo_idx: [0; SWMBX_DEV_COUNT],
buffer_base,
buffer_size,
}
}
/// Enables or disables notification for a single node on a given port.
///
/// When enabled, writes targeting `addr` can latch `notify_flag` for this
/// port, provided global notify behavior is also enabled via
/// [`Self::enable_behavior`].
///
/// # Errors
/// Returns [`SwmbxError::InvalidPort`] if `port` is out of range.
/// Returns [`SwmbxError::InvalidAddress`] if `addr` is out of range.
pub fn update_notify(&mut self, port: usize, addr: u8, enable: bool) -> Result<(), SwmbxError> {
if port >= SWMBX_DEV_COUNT {
return Err(SwmbxError::InvalidPort);
}
if (addr as usize) >= SWMBX_NODE_COUNT {
return Err(SwmbxError::InvalidAddress);
}
let node = &mut self.node[port][addr as usize];
if enable {
node.enabled_flags |= SWMBX_NOTIFY;
} else {
node.enabled_flags &= !SWMBX_NOTIFY;
}
Ok(())
}
/// Configures one FIFO endpoint mapping.
///
/// If `enable` is `true`, this binds FIFO `index` to `addr`, applies the
/// requested queue depth and notify mask, and resets transient transaction
/// state for that FIFO. If `enable` is `false`, the FIFO is disabled and
/// drained/reset.
///
/// # Errors
/// Returns [`SwmbxError::InvalidFifoIndex`] if `index` is out of range.
/// Returns [`SwmbxError::InvalidFifoDepth`] if enabling with depth `0` or
/// greater than [`SWMBX_FIFO_DEPTH`].
/// Returns [`SwmbxError::InvalidAddress`] if `addr` is out of range.
pub fn update_fifo(
&mut self,
index: usize,
addr: u8,
depth: usize,
notify: u8,
enable: bool,
) -> Result<(), SwmbxError> {
if index >= SWMBX_FIFO_COUNT {
return Err(SwmbxError::InvalidFifoIndex);
}
if enable && (depth == 0 || depth > SWMBX_FIFO_DEPTH) {
return Err(SwmbxError::InvalidFifoDepth);
}
if (addr as usize) >= SWMBX_NODE_COUNT {
return Err(SwmbxError::InvalidAddress);
}
let fifo = &mut self.fifo[index];
fifo.enabled = enable;
if enable {
fifo.fifo_offset = addr;
fifo.max_msg_count = depth;
fifo.notify_flag = notify;
fifo.msg_index = 0;
fifo.notify_start = false;
fifo.fifo_write = false;
fifo.queue = Queue::new();
} else {
fifo.notify_start = false;
fifo.fifo_write = false;
fifo.queue = Queue::new();
}
Ok(())
}
/// Flushes all pending messages from FIFO `index`.
///
/// This also resets FIFO-local transient state such as START latch and
/// transaction write tracking flags.
///
/// # Errors
/// Returns [`SwmbxError::InvalidFifoIndex`] if `index` is out of range.
pub fn flush_fifo(&mut self, index: usize) -> Result<(), SwmbxError> {
if index >= SWMBX_FIFO_COUNT {
return Err(SwmbxError::InvalidFifoIndex);
}
self.fifo[index].flush();
Ok(())
}
/// Enables or disables global SWMBX behavior flags.
///
/// # Errors
/// Returns [`SwmbxError::InvalidFlagMask`] if `flag` contains no known
/// behavior bits ([`FLAG_MASK`]).
pub fn enable_behavior(&mut self, flag: u8, enable: bool) -> Result<(), SwmbxError> {
if (flag & FLAG_MASK) == 0 {
return Err(SwmbxError::InvalidFlagMask);
}
if enable {
self.mbx_en |= flag;
} else {
self.mbx_en &= !flag;
}
Ok(())
}
/// Enables or disables write protection for one node on a given port.
///
/// A protected node suppresses writes only when global protect behavior is
/// also enabled via [`Self::enable_behavior`].
///
/// # Errors
/// Returns [`SwmbxError::InvalidPort`] if `port` is out of range.
/// Returns [`SwmbxError::InvalidAddress`] if `addr` is out of range.
pub fn update_protect(
&mut self,
port: usize,
addr: u8,
enable: bool,
) -> Result<(), SwmbxError> {
if port >= SWMBX_DEV_COUNT {
return Err(SwmbxError::InvalidPort);
}
if (addr as usize) >= SWMBX_NODE_COUNT {
return Err(SwmbxError::InvalidAddress);
}
let node = &mut self.node[port][addr as usize];
if enable {
node.enabled_flags |= SWMBX_PROTECT;
} else {
node.enabled_flags &= !SWMBX_PROTECT;
}
Ok(())
}
/// Applies protection bits from packed 32-bit words.
///
/// # Errors
/// Returns [`SwmbxError::InvalidPort`] if `port` is out of range, or
/// [`SwmbxError::InvalidProtectRange`] if `start_idx` plus `bitmap.len()`
/// would exceed [`PROTECT_WORD_COUNT`] (including on arithmetic overflow).
pub fn apply_protect(
&mut self,
port: usize,
bitmap: &[u32],
start_idx: usize,
) -> Result<(), SwmbxError> {
if port >= SWMBX_DEV_COUNT {
return Err(SwmbxError::InvalidPort);
}
// Checked add so a large `start_idx` cannot wrap past this guard and
// later overflow the `(start_idx + i) << PROTECT_WORD_SHIFT` shift.
match start_idx.checked_add(bitmap.len()) {
Some(end) if end <= PROTECT_WORD_COUNT => {}
_ => return Err(SwmbxError::InvalidProtectRange),
}
for (i, &val) in bitmap.iter().enumerate() {
let base = (start_idx + i) << PROTECT_WORD_SHIFT;
for bit in 0..PROTECT_BITS_PER_WORD {
let addr = base + bit;
if addr >= SWMBX_NODE_COUNT {
break;
}
let node = &mut self.node[port][addr];
if (val >> bit) & 1 != 0 {
node.enabled_flags |= SWMBX_PROTECT;
} else {
node.enabled_flags &= !SWMBX_PROTECT;
}
}
}
Ok(())
}
/// Reads one byte for `port` and `addr`.
///
/// If a FIFO transaction is active for `port` and FIFO behavior is enabled,
/// this dequeues from the mapped FIFO. Otherwise it reads directly from the
/// backing mailbox region.
///
/// # Errors
/// Returns [`SwmbxError::InvalidPort`] if `port` is out of range.
/// Returns [`SwmbxError::FifoEmpty`] when reading from an empty active FIFO.
/// Returns [`SwmbxError::InvalidAddress`] for out-of-bounds flat-buffer
/// reads relative to `buffer_size`.
pub fn get_msg(&mut self, port: usize, addr: u8) -> Result<u8, SwmbxError> {
if port >= SWMBX_DEV_COUNT {
return Err(SwmbxError::InvalidPort);
}
if self.mbx_fifo_execute[port] && (self.mbx_en & SWMBX_FIFO) != 0 {
let fifo_index = self.mbx_fifo_idx[port] as usize;
return self.fifo[fifo_index].dequeue();
}
self.read_in_region(addr)
}
/// Writes one byte for `port` and `addr`.
///
/// If a FIFO transaction is active for `port` and FIFO behavior is enabled,
/// the value is appended to the active FIFO. Otherwise this applies protect
/// and notify policy to a flat-buffer write path.
///
/// # Errors
/// Returns [`SwmbxError::InvalidPort`] if `port` is out of range.
/// Returns [`SwmbxError::FifoFull`] when appending to a full active FIFO.
/// Returns [`SwmbxError::InvalidAddress`] for out-of-bounds flat-buffer
/// writes relative to `buffer_size`.
pub fn send_msg(&mut self, port: usize, addr: u8, val: u8) -> Result<(), SwmbxError> {
if port >= SWMBX_DEV_COUNT {
return Err(SwmbxError::InvalidPort);
}
let mut write_to_buffer = false;
if self.mbx_fifo_execute[port] && (self.mbx_en & SWMBX_FIFO) != 0 {
let fifo_addr = self.mbx_fifo_addr[port];
let fifo_index = self.mbx_fifo_idx[port] as usize;
if let Err(err) = self.fifo[fifo_index].append_write(val) {
self.node[port][addr as usize].notify_flag = true;
return Err(err);
}
if (self.mbx_en & SWMBX_NOTIFY) != 0
&& (self.fifo[fifo_index].notify_flag & SWMBX_FIFO_NOTIFY_START) != 0
&& !self.fifo[fifo_index].notify_start
&& (self.node[port][fifo_addr as usize].enabled_flags & SWMBX_NOTIFY) != 0
{
self.node[port][fifo_addr as usize].notify_flag = true;
self.fifo[fifo_index].notify_start = true;
}
if !self.fifo[fifo_index].fifo_write {
self.fifo[fifo_index].fifo_write = true;
}
} else {
let node = &mut self.node[port][addr as usize];
if (node.enabled_flags & SWMBX_PROTECT) == 0 || (self.mbx_en & SWMBX_PROTECT) == 0 {
write_to_buffer = true;
}
if (self.mbx_en & SWMBX_NOTIFY) != 0 && (node.enabled_flags & SWMBX_NOTIFY) != 0 {
node.notify_flag = true;
}
}
if write_to_buffer {
self.write_in_region(addr, val)?;
}
Ok(())
}
/// Starts a transaction on `port` for `addr`.
///
/// If `addr` resolves to an enabled FIFO mapping, the port switches into
/// FIFO transaction mode for subsequent [`Self::send_msg`]/[`Self::get_msg`]
/// calls until [`Self::send_stop`]. Otherwise the port remains in flat
/// buffer mode.
///
/// # Errors
/// Returns [`SwmbxError::InvalidPort`] if `port` is out of range.
pub fn send_start(&mut self, port: usize, addr: u8) -> Result<(), SwmbxError> {
if port >= SWMBX_DEV_COUNT {
return Err(SwmbxError::InvalidPort);
}
if let Some(fifo_index) = self.check_fifo(addr) {
self.mbx_fifo_execute[port] = true;
self.mbx_fifo_addr[port] = addr;
self.mbx_fifo_idx[port] = fifo_index as u8;
} else {
self.mbx_fifo_execute[port] = false;
}
Ok(())
}
/// Returns the enabled FIFO index mapped to `addr`, if any.
pub fn check_fifo(&self, addr: u8) -> Option<usize> {
self.fifo
.iter()
.position(|f| f.fifo_offset == addr && f.enabled)
}
/// Stops the current transaction for `port`.
///
/// For FIFO transactions, this finalizes STOP-notify behavior and clears
/// per-port FIFO execution state.
///
/// # Errors
/// Returns [`SwmbxError::InvalidPort`] if `port` is out of range.
pub fn send_stop(&mut self, port: usize) -> Result<(), SwmbxError> {
if port >= SWMBX_DEV_COUNT {
return Err(SwmbxError::InvalidPort);
}
if self.mbx_fifo_execute[port] {
let fifo_addr = self.mbx_fifo_addr[port];
let fifo_index = self.mbx_fifo_idx[port] as usize;
if (self.mbx_en & SWMBX_NOTIFY) != 0
&& (self.fifo[fifo_index].notify_flag & SWMBX_FIFO_NOTIFY_STOP) != 0
&& self.fifo[fifo_index].fifo_write
&& (self.node[port][fifo_addr as usize].enabled_flags & SWMBX_NOTIFY) != 0
{
self.node[port][fifo_addr as usize].notify_flag = true;
}
self.fifo[fifo_index].notify_start = false;
self.fifo[fifo_index].fifo_write = false;
self.mbx_fifo_execute[port] = false;
self.mbx_fifo_addr[port] = 0;
self.mbx_fifo_idx[port] = 0;
}
Ok(())
}
/// Direct software write helper.
///
/// When `fifo` is `true`, writes to the FIFO mapped at `addr` regardless of
/// transaction state. When `fifo` is `false`, writes directly to the flat
/// backing mailbox region.
///
/// # Errors
/// Returns [`SwmbxError::FifoNotMapped`] when `fifo` is `true` and no FIFO
/// is mapped to `addr`.
/// Returns [`SwmbxError::FifoFull`] when writing to a full mapped FIFO.
/// Returns [`SwmbxError::InvalidAddress`] for out-of-bounds flat-buffer
/// writes relative to `buffer_size`.
pub fn swmbx_write(&mut self, fifo: bool, addr: u8, val: u8) -> Result<(), SwmbxError> {
if fifo {
if let Some(index) = self.check_fifo(addr) {
self.fifo[index].append_write(val)?;
Ok(())
} else {
Err(SwmbxError::FifoNotMapped)
}
} else {
self.write_in_region(addr, val)
}
}
/// Direct software read helper.
///
/// When `fifo` is `true`, reads from the FIFO mapped at `addr` regardless
/// of transaction state. When `fifo` is `false`, reads directly from the
/// flat backing mailbox region.
///
/// # Errors
/// Returns [`SwmbxError::FifoNotMapped`] when `fifo` is `true` and no FIFO
/// is mapped to `addr`.
/// Returns [`SwmbxError::FifoEmpty`] when reading an empty mapped FIFO.
/// Returns [`SwmbxError::InvalidAddress`] for out-of-bounds flat-buffer
/// reads relative to `buffer_size`.
pub fn swmbx_read(&mut self, fifo: bool, addr: u8) -> Result<u8, SwmbxError> {
if fifo {
if let Some(index) = self.check_fifo(addr) {
self.fifo[index].dequeue()
} else {
Err(SwmbxError::FifoNotMapped)
}
} else {
self.read_in_region(addr)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn host_backed_buffer_read_write() {
let mut host_buffer = [0u8; SWMBX_NODE_COUNT];
// SAFETY: the buffer region is backed by live, uniquely-owned host stack
// storage (`host_buffer`) for the duration of the test.
let mut ctrl = unsafe {
SwmbxCtrl::new_with_regions(host_buffer.len(), host_buffer.as_mut_ptr() as usize)
};
ctrl.swmbx_write(false, 0x13, 0x7a)
.expect("buffer write failed");
let val = ctrl.swmbx_read(false, 0x13).expect("buffer read failed");
assert_eq!(val, 0x7a);
}
#[test]
fn fifo_flow_works_with_host_backing_memory() {
let mut host_buffer = [0u8; SWMBX_NODE_COUNT];
// SAFETY: `buffer_base` is live, uniquely-owned host storage for the test.
let mut ctrl = unsafe {
SwmbxCtrl::new_with_regions(host_buffer.len(), host_buffer.as_mut_ptr() as usize)
};
ctrl.enable_behavior(SWMBX_FIFO | SWMBX_NOTIFY, true)
.expect("enable behavior failed");
ctrl.update_fifo(
0,
0x0d,
4,
SWMBX_FIFO_NOTIFY_START | SWMBX_FIFO_NOTIFY_STOP,
true,
)
.expect("fifo setup failed");
ctrl.update_notify(0, 0x0d, true)
.expect("notify setup failed");
ctrl.send_start(0, 0x0d).expect("send_start failed");
ctrl.send_msg(0, 0x0d, 0x11).expect("send_msg #1 failed");
ctrl.send_msg(0, 0x0d, 0x22).expect("send_msg #2 failed");
assert_eq!(ctrl.get_msg(0, 0x0d).expect("get_msg #1 failed"), 0x11);
assert_eq!(ctrl.get_msg(0, 0x0d).expect("get_msg #2 failed"), 0x22);
assert_eq!(ctrl.get_msg(0, 0x0d), Err(SwmbxError::FifoEmpty));
ctrl.send_stop(0).expect("send_stop failed");
assert!(!ctrl.mbx_fifo_execute[0]);
assert!(!ctrl.fifo[0].notify_start);
assert!(!ctrl.fifo[0].fifo_write);
}
#[test]
fn apply_protect_rejects_out_of_range_without_overflow() {
let mut dummy = 0u8;
// SAFETY: this test only exercises `apply_protect`, which never touches
// the buffer region; we still pass a non-null base because
// `new_with_regions` validates it at construction time.
let mut ctrl = unsafe { SwmbxCtrl::new_with_regions(0, &mut dummy as *mut u8 as usize) };
// A `start_idx` near `usize::MAX` must be rejected, not wrap past the
// range guard and overflow the internal shift.
assert_eq!(
ctrl.apply_protect(0, &[0xffff_ffff], usize::MAX),
Err(SwmbxError::InvalidProtectRange)
);
// Just past the last valid word is also rejected.
assert_eq!(
ctrl.apply_protect(0, &[0], PROTECT_WORD_COUNT),
Err(SwmbxError::InvalidProtectRange)
);
// A full, in-range bitmap is accepted.
ctrl.apply_protect(0, &[0u32; PROTECT_WORD_COUNT], 0)
.expect("full in-range bitmap should apply");
}
#[test]
fn flush_fifo_resets_start_latch_and_rearms_start_notify() {
let mut host_buffer = [0u8; SWMBX_NODE_COUNT];
// SAFETY: `buffer_base` is live, uniquely-owned host storage for the test.
let mut ctrl = unsafe {
SwmbxCtrl::new_with_regions(host_buffer.len(), host_buffer.as_mut_ptr() as usize)
};
ctrl.enable_behavior(SWMBX_FIFO | SWMBX_NOTIFY, true)
.expect("enable behavior failed");
ctrl.update_fifo(0, 0x0d, 4, SWMBX_FIFO_NOTIFY_START, true)
.expect("fifo setup failed");
ctrl.update_notify(0, 0x0d, true)
.expect("notify setup failed");
ctrl.send_start(0, 0x0d).expect("send_start #1 failed");
ctrl.send_msg(0, 0x0d, 0x11).expect("send_msg #1 failed");
assert!(ctrl.fifo[0].notify_start);
assert!(ctrl.fifo[0].fifo_write);
// Simulate consumption of the first notification before the next transaction.
ctrl.node[0][0x0d].notify_flag = false;
ctrl.flush_fifo(0).expect("flush_fifo failed");
assert!(!ctrl.fifo[0].notify_start);
assert!(!ctrl.fifo[0].fifo_write);
ctrl.send_stop(0).expect("send_stop failed");
ctrl.send_start(0, 0x0d).expect("send_start #2 failed");
ctrl.send_msg(0, 0x0d, 0x22).expect("send_msg #2 failed");
assert!(ctrl.node[0][0x0d].notify_flag);
assert!(ctrl.fifo[0].notify_start);
}
#[test]
fn update_fifo_rejects_invalid_configuration() {
let mut host_buffer = [0u8; SWMBX_NODE_COUNT];
// SAFETY: `buffer_base` is live, uniquely-owned host storage for the test.
let mut ctrl = unsafe {
SwmbxCtrl::new_with_regions(host_buffer.len(), host_buffer.as_mut_ptr() as usize)
};
assert_eq!(
ctrl.update_fifo(SWMBX_FIFO_COUNT, 0x0d, 4, 0, true),
Err(SwmbxError::InvalidFifoIndex)
);
assert_eq!(
ctrl.update_fifo(0, 0x0d, 0, 0, true),
Err(SwmbxError::InvalidFifoDepth)
);
assert_eq!(
ctrl.update_fifo(0, 0x0d, SWMBX_FIFO_DEPTH + 1, 0, true),
Err(SwmbxError::InvalidFifoDepth)
);
// `addr` is `u8`, so all values up to 0xff are representable and valid
// for a 256-node mailbox.
assert_eq!(ctrl.update_fifo(0, 0xff, 4, 0, true), Ok(()));
}
#[test]
fn flat_buffer_path_honors_protect_and_notify_gating() {
let mut host_buffer = [0u8; SWMBX_NODE_COUNT];
// SAFETY: `buffer_base` is live, uniquely-owned host storage for the test.
let mut ctrl = unsafe {
SwmbxCtrl::new_with_regions(host_buffer.len(), host_buffer.as_mut_ptr() as usize)
};
let addr = 0x21;
// Baseline write works.
ctrl.send_msg(0, addr, 0x12)
.expect("baseline send_msg failed");
assert_eq!(
ctrl.get_msg(0, addr).expect("baseline get_msg failed"),
0x12
);
// With node PROTECT enabled but global PROTECT disabled, writes still go through.
ctrl.update_protect(0, addr, true)
.expect("update_protect failed");
ctrl.send_msg(0, addr, 0x34)
.expect("send_msg with global protect disabled failed");
assert_eq!(
ctrl.get_msg(0, addr)
.expect("get_msg with global protect disabled failed"),
0x34
);
// Enabling global PROTECT blocks writes to protected nodes.
ctrl.enable_behavior(SWMBX_PROTECT, true)
.expect("enable protect failed");
ctrl.send_msg(0, addr, 0x56)
.expect("send_msg with protect enabled failed");
assert_eq!(
ctrl.get_msg(0, addr)
.expect("get_msg after blocked protected write failed"),
0x34
);
// Notify does not latch until both node-notify and global notify are enabled.
assert!(!ctrl.node[0][addr as usize].notify_flag);
ctrl.send_msg(0, addr, 0x78)
.expect("send_msg before notify enable failed");
assert!(!ctrl.node[0][addr as usize].notify_flag);
ctrl.update_notify(0, addr, true)
.expect("update_notify failed");
ctrl.send_msg(0, addr, 0x9a)
.expect("send_msg with only node notify failed");
assert!(!ctrl.node[0][addr as usize].notify_flag);
ctrl.enable_behavior(SWMBX_NOTIFY, true)
.expect("enable notify failed");
ctrl.send_msg(0, addr, 0xbc)
.expect("send_msg with notify enabled failed");
assert!(ctrl.node[0][addr as usize].notify_flag);
}
#[test]
fn fifo_full_returns_error_and_sets_node_notify() {
let mut host_buffer = [0u8; SWMBX_NODE_COUNT];
// SAFETY: `buffer_base` is live, uniquely-owned host storage for the test.
let mut ctrl = unsafe {
SwmbxCtrl::new_with_regions(host_buffer.len(), host_buffer.as_mut_ptr() as usize)
};
let addr = 0x0d;
ctrl.enable_behavior(SWMBX_FIFO, true)
.expect("enable fifo failed");
ctrl.update_fifo(0, addr, 1, 0, true)
.expect("fifo setup failed");
ctrl.send_start(0, addr).expect("send_start failed");
ctrl.send_msg(0, addr, 0x11)
.expect("first fifo write failed");
assert_eq!(ctrl.send_msg(0, addr, 0x22), Err(SwmbxError::FifoFull));
assert!(ctrl.node[0][addr as usize].notify_flag);
}
#[test]
fn send_stop_notify_requires_node_notify_and_write() {
let mut host_buffer = [0u8; SWMBX_NODE_COUNT];
// SAFETY: `buffer_base` is live, uniquely-owned host storage for the test.
let mut ctrl = unsafe {
SwmbxCtrl::new_with_regions(host_buffer.len(), host_buffer.as_mut_ptr() as usize)
};
let addr = 0x0d;
ctrl.enable_behavior(SWMBX_FIFO | SWMBX_NOTIFY, true)
.expect("enable behavior failed");
ctrl.update_fifo(0, addr, 4, SWMBX_FIFO_NOTIFY_STOP, true)
.expect("fifo setup failed");
// STOP notify configured, but node notify disabled: no latch.
ctrl.send_start(0, addr).expect("send_start #1 failed");
ctrl.send_msg(0, addr, 0x11).expect("send_msg #1 failed");
ctrl.send_stop(0).expect("send_stop #1 failed");
assert!(!ctrl.node[0][addr as usize].notify_flag);
// Enable node notify but do not write during transaction: no STOP notify latch.
ctrl.update_notify(0, addr, true)
.expect("update_notify failed");
ctrl.node[0][addr as usize].notify_flag = false;
ctrl.send_start(0, addr).expect("send_start #2 failed");
ctrl.send_stop(0).expect("send_stop #2 failed");
assert!(!ctrl.node[0][addr as usize].notify_flag);
// With node notify enabled and a write in transaction: STOP notify latches.
ctrl.send_start(0, addr).expect("send_start #3 failed");
ctrl.send_msg(0, addr, 0x22).expect("send_msg #2 failed");
ctrl.send_stop(0).expect("send_stop #3 failed");
assert!(ctrl.node[0][addr as usize].notify_flag);
}
#[test]
fn fifo_start_notify_latches_once_and_rearms_after_stop() {
let mut host_buffer = [0u8; SWMBX_NODE_COUNT];
// SAFETY: `buffer_base` is live, uniquely-owned host storage for the test.
let mut ctrl = unsafe {
SwmbxCtrl::new_with_regions(host_buffer.len(), host_buffer.as_mut_ptr() as usize)
};
let addr = 0x0d;
ctrl.enable_behavior(SWMBX_FIFO | SWMBX_NOTIFY, true)
.expect("enable behavior failed");
ctrl.update_fifo(0, addr, 4, SWMBX_FIFO_NOTIFY_START, true)
.expect("fifo setup failed");
ctrl.update_notify(0, addr, true)
.expect("update_notify failed");
ctrl.send_start(0, addr).expect("send_start #1 failed");
ctrl.send_msg(0, addr, 0x11).expect("send_msg #1 failed");
assert!(ctrl.node[0][addr as usize].notify_flag);
assert!(ctrl.fifo[0].notify_start);
// START-notify is one-shot during a single transaction.
ctrl.node[0][addr as usize].notify_flag = false;
ctrl.send_msg(0, addr, 0x22).expect("send_msg #2 failed");
assert!(!ctrl.node[0][addr as usize].notify_flag);
// STOP clears the latch; next transaction can notify START again.
ctrl.send_stop(0).expect("send_stop failed");
ctrl.send_start(0, addr).expect("send_start #2 failed");
ctrl.send_msg(0, addr, 0x33).expect("send_msg #3 failed");
assert!(ctrl.node[0][addr as usize].notify_flag);
}
#[test]
fn send_start_with_non_fifo_addr_falls_back_to_flat_buffer() {
let mut host_buffer = [0u8; SWMBX_NODE_COUNT];
// SAFETY: `buffer_base` is live, uniquely-owned host storage for the test.
let mut ctrl = unsafe {
SwmbxCtrl::new_with_regions(host_buffer.len(), host_buffer.as_mut_ptr() as usize)
};
let fifo_addr = 0x0d;
let flat_addr = 0x20;
ctrl.enable_behavior(SWMBX_FIFO, true)
.expect("enable fifo failed");
ctrl.update_fifo(0, fifo_addr, 4, 0, true)
.expect("fifo setup failed");
ctrl.send_start(0, fifo_addr)
.expect("send_start fifo failed");
assert!(ctrl.mbx_fifo_execute[0]);
// New START to a non-FIFO offset must disable FIFO execute for this port.
ctrl.send_start(0, flat_addr)
.expect("send_start flat-buffer failed");
assert!(!ctrl.mbx_fifo_execute[0]);
ctrl.send_msg(0, flat_addr, 0x5a)
.expect("flat-buffer send_msg failed");
assert_eq!(
ctrl.get_msg(0, flat_addr)
.expect("flat-buffer get_msg failed"),
0x5a
);
}
#[test]
fn apply_protect_bitmap_maps_first_and_last_node_bits() {
let mut host_buffer = [0u8; SWMBX_NODE_COUNT];
// SAFETY: `buffer_base` is live, uniquely-owned host storage for the test.
let mut ctrl = unsafe {
SwmbxCtrl::new_with_regions(host_buffer.len(), host_buffer.as_mut_ptr() as usize)
};
// Set protect bit for address 0.
ctrl.apply_protect(0, &[1], 0)
.expect("apply_protect low failed");
assert_ne!(ctrl.node[0][0].enabled_flags & SWMBX_PROTECT, 0);
assert_eq!(ctrl.node[0][1].enabled_flags & SWMBX_PROTECT, 0);
// Set protect bit for address 255 (bit 31 of last word).
ctrl.apply_protect(0, &[1u32 << 31], PROTECT_WORD_COUNT - 1)
.expect("apply_protect high failed");
assert_ne!(ctrl.node[0][255].enabled_flags & SWMBX_PROTECT, 0);
assert_eq!(ctrl.node[0][254].enabled_flags & SWMBX_PROTECT, 0);
ctrl.enable_behavior(SWMBX_PROTECT, true)
.expect("enable protect failed");
// Protected edges block writes when global protect is enabled.
ctrl.send_msg(0, 0x00, 0xaa)
.expect("send_msg protected low failed");
ctrl.send_msg(0, 0xff, 0xbb)
.expect("send_msg protected high failed");
assert_eq!(ctrl.get_msg(0, 0x00).expect("read low failed"), 0x00);
assert_eq!(ctrl.get_msg(0, 0xff).expect("read high failed"), 0x00);
}
#[test]
fn port_fifo_transaction_state_is_isolated() {
let mut host_buffer = [0u8; SWMBX_NODE_COUNT];
// SAFETY: `buffer_base` is live, uniquely-owned host storage for the test.
let mut ctrl = unsafe {
SwmbxCtrl::new_with_regions(host_buffer.len(), host_buffer.as_mut_ptr() as usize)
};
let addr = 0x0d;
ctrl.enable_behavior(SWMBX_FIFO, true)
.expect("enable fifo failed");
ctrl.update_fifo(0, addr, 4, 0, true)
.expect("fifo setup failed");
ctrl.send_start(0, addr).expect("send_start port0 failed");
ctrl.send_start(1, addr).expect("send_start port1 failed");
assert!(ctrl.mbx_fifo_execute[0]);
assert!(ctrl.mbx_fifo_execute[1]);
assert_eq!(ctrl.mbx_fifo_addr[0], addr);
assert_eq!(ctrl.mbx_fifo_addr[1], addr);
ctrl.send_stop(1).expect("send_stop port1 failed");
assert!(ctrl.mbx_fifo_execute[0]);
assert!(!ctrl.mbx_fifo_execute[1]);
ctrl.send_stop(0).expect("send_stop port0 failed");
assert!(!ctrl.mbx_fifo_execute[0]);
assert!(!ctrl.mbx_fifo_execute[1]);
}
#[test]
fn direct_helpers_validate_mapping_and_buffer_bounds() {
let mut host_buffer = [0u8; 16];
// SAFETY: `buffer_base` is live, uniquely-owned host storage for the test.
let mut ctrl = unsafe {
SwmbxCtrl::new_with_regions(host_buffer.len(), host_buffer.as_mut_ptr() as usize)
};
assert_eq!(ctrl.swmbx_read(true, 0x44), Err(SwmbxError::FifoNotMapped));
assert_eq!(
ctrl.swmbx_write(true, 0x44, 0x11),
Err(SwmbxError::FifoNotMapped)
);
assert_eq!(ctrl.swmbx_read(false, 16), Err(SwmbxError::InvalidAddress));
assert_eq!(
ctrl.swmbx_write(false, 16, 0x55),
Err(SwmbxError::InvalidAddress)
);
}
#[test]
fn invalid_port_and_flag_paths_return_errors() {
let mut host_buffer = [0u8; SWMBX_NODE_COUNT];
// SAFETY: `buffer_base` is live, uniquely-owned host storage for the test.
let mut ctrl = unsafe {
SwmbxCtrl::new_with_regions(host_buffer.len(), host_buffer.as_mut_ptr() as usize)
};
let bad_port = SWMBX_DEV_COUNT;
assert_eq!(
ctrl.update_notify(bad_port, 0x01, true),
Err(SwmbxError::InvalidPort)
);
assert_eq!(
ctrl.update_protect(bad_port, 0x01, true),
Err(SwmbxError::InvalidPort)
);
assert_eq!(
ctrl.apply_protect(bad_port, &[0], 0),
Err(SwmbxError::InvalidPort)
);
assert_eq!(ctrl.get_msg(bad_port, 0x01), Err(SwmbxError::InvalidPort));
assert_eq!(
ctrl.send_msg(bad_port, 0x01, 0x11),
Err(SwmbxError::InvalidPort)
);
assert_eq!(
ctrl.send_start(bad_port, 0x01),
Err(SwmbxError::InvalidPort)
);
assert_eq!(ctrl.send_stop(bad_port), Err(SwmbxError::InvalidPort));
assert_eq!(
ctrl.enable_behavior(0, true),
Err(SwmbxError::InvalidFlagMask)
);
assert_eq!(
ctrl.flush_fifo(SWMBX_FIFO_COUNT),
Err(SwmbxError::InvalidFifoIndex)
);
}
}