use super::*;
use alloc::vec;
use alloc::vec::Vec;
use crate::core::space::{AddressSpace, MemOps, RamStore, Region, RequesterId, UnassignedPolicy};
use crate::core::state::{ChunkReader, MachineShape, Migrations, StateReader, StateWriter};
use crate::core::value::Width;
use crate::core::wire::{Level, Resolve, Wire, WireIdAllocator, WireSource};
use crate::dev::ata::Medium;
const RAM_BASE: u64 = 0x1000;
const RAM_LEN: u64 = 0x40_0000;
const REGS: u64 = 0x1000_0000;
const BLOCKS: u64 = 256;
const LBA: u64 = 512;
const REG_CAP: u64 = 0x00;
const REG_CC: u64 = 0x14;
const REG_CSTS: u64 = 0x1c;
const REG_AQA: u64 = 0x24;
const REG_ASQ: u64 = 0x28;
const REG_ACQ: u64 = 0x30;
const ASQ: u64 = 0x0010_0000;
const ACQ: u64 = 0x0010_1000;
const IOSQ: u64 = 0x0010_2000;
const IOCQ: u64 = 0x0010_3000;
const DATA: u64 = 0x0020_0000;
const ADMIN_ENTRIES: u32 = 8;
const IO_ENTRIES: u32 = 8;
struct Rig {
ctrl: Arc<Controller>,
space: Arc<AddressSpace>,
wire: Arc<Wire>,
irq: WireSource,
store: Arc<RamStore>,
tail: u32,
head: u32,
io_tail: u32,
io_head: u32,
cid: u16,
}
fn stamp(lba: u64) -> Vec<u8> {
let mut out = vec![0u8; LBA as usize];
out[0] = lba as u8;
out[1] = 0xa5;
out
}
fn rig() -> Rig {
let store = Arc::new(RamStore::new(BLOCKS * LBA));
for lba in 0..BLOCKS {
RamStore::write_at(&store, lba * LBA, &stamp(lba)).expect("the image fits");
}
let ns = Namespace::new(Arc::clone(&store) as Arc<dyn Medium>, 9, false).expect("512-byte");
let ctrl = Arc::new(Controller::new(ns, Params::default()));
let space = Arc::new(AddressSpace::new("mem", 32).with_unassigned(UnassignedPolicy::ONES));
{
let mut topo = space.topology();
topo.map(
Region::ram("ram", Arc::new(RamStore::new(RAM_LEN))),
RAM_BASE,
)
.expect("the map fits");
topo.map(
Region::io(
"nvme.regs",
REGISTER_LEN,
Arc::clone(&ctrl) as Arc<dyn MemOps>,
),
REGS,
)
.expect("the map fits");
}
ctrl.attach_space(&space, RequesterId(7));
ctrl.set_master(true);
let ids = WireIdAllocator::new();
let id = ids.alloc();
let wire = Wire::builder().source(id).build_shared();
let irq = WireSource::new(Arc::clone(&wire), id);
ctrl.connect_irq(irq.clone());
Rig {
ctrl,
space,
wire,
irq,
store,
tail: 0,
head: 0,
io_tail: 0,
io_head: 0,
cid: 0,
}
}
impl Rig {
fn reg(&self, offset: u64) -> u32 {
self.space
.read(REGS + offset, Width::U32, MemAttrs::DEFAULT)
.expect("a register") as u32
}
fn set_reg(&self, offset: u64, value: u32) {
self.space
.write(
REGS + offset,
Width::U32,
u64::from(value),
MemAttrs::DEFAULT,
)
.expect("a register");
}
fn set_reg64(&self, offset: u64, value: u64) {
self.set_reg(offset, value as u32);
self.set_reg(offset + 4, (value >> 32) as u32);
}
fn poke(&self, addr: u64, bytes: &[u8]) {
self.space
.write_bytes(addr, bytes, MemAttrs::DEFAULT)
.expect("mapped memory");
}
fn peek(&self, addr: u64, len: u64) -> Vec<u8> {
let mut out = vec![0u8; len as usize];
self.space
.read_bytes(addr, &mut out, MemAttrs::DEFAULT)
.expect("mapped memory");
out
}
fn enable(&mut self) {
self.set_reg(REG_AQA, (ADMIN_ENTRIES - 1) | ((ADMIN_ENTRIES - 1) << 16));
self.set_reg64(REG_ASQ, ASQ);
self.set_reg64(REG_ACQ, ACQ);
self.set_reg(REG_CC, 1 | (6 << 17) | (4 << 21));
assert_eq!(self.reg(REG_CSTS) & 0x3, 1, "ready, and not fatal");
let cqe = self.admin(0x05, 0, IOCQ, 0, 1 | ((IO_ENTRIES - 1) << 16), 0x0003, 0);
assert_eq!(cqe.2, 0, "Create I/O Completion Queue");
let cqe = self.admin(
0x01,
0,
IOSQ,
0,
1 | ((IO_ENTRIES - 1) << 16),
0x0001 | (1 << 16),
0,
);
assert_eq!(cqe.2, 0, "Create I/O Submission Queue");
}
#[allow(clippy::too_many_arguments)]
fn admin(
&mut self,
opcode: u8,
nsid: u32,
prp1: u64,
prp2: u64,
cdw10: u32,
cdw11: u32,
cdw12: u32,
) -> (u32, u16, u16) {
self.cid = self.cid.wrapping_add(1);
let mut sqe = [0u8; 64];
let cdw0 = u32::from(opcode) | (u32::from(self.cid) << 16);
sqe[0..4].copy_from_slice(&cdw0.to_le_bytes());
sqe[4..8].copy_from_slice(&nsid.to_le_bytes());
sqe[24..32].copy_from_slice(&prp1.to_le_bytes());
sqe[32..40].copy_from_slice(&prp2.to_le_bytes());
sqe[40..44].copy_from_slice(&cdw10.to_le_bytes());
sqe[44..48].copy_from_slice(&cdw11.to_le_bytes());
sqe[48..52].copy_from_slice(&cdw12.to_le_bytes());
self.poke(ASQ + u64::from(self.tail) * 64, &sqe);
self.tail = (self.tail + 1) % ADMIN_ENTRIES;
self.set_reg(0x1000, self.tail);
let at = ACQ + u64::from(self.head) * 16;
let entry = self.peek(at, 16);
self.head = (self.head + 1) % ADMIN_ENTRIES;
self.set_reg(0x1004, self.head);
let dw0 = u32::from_le_bytes([entry[0], entry[1], entry[2], entry[3]]);
let dw3 = u32::from_le_bytes([entry[12], entry[13], entry[14], entry[15]]);
(dw0, dw3 as u16, (dw3 >> 17) as u16)
}
#[allow(clippy::too_many_arguments)]
fn io(&mut self, opcode: u8, nsid: u32, prp1: u64, prp2: u64, cdw10: u32, cdw12: u32) -> u16 {
self.cid = self.cid.wrapping_add(1);
let mut sqe = [0u8; 64];
let cdw0 = u32::from(opcode) | (u32::from(self.cid) << 16);
sqe[0..4].copy_from_slice(&cdw0.to_le_bytes());
sqe[4..8].copy_from_slice(&nsid.to_le_bytes());
sqe[24..32].copy_from_slice(&prp1.to_le_bytes());
sqe[32..40].copy_from_slice(&prp2.to_le_bytes());
sqe[40..44].copy_from_slice(&cdw10.to_le_bytes());
sqe[48..52].copy_from_slice(&cdw12.to_le_bytes());
self.poke(IOSQ + u64::from(self.io_tail) * 64, &sqe);
self.io_tail = (self.io_tail + 1) % IO_ENTRIES;
self.set_reg(0x1008, self.io_tail);
let entry = self.peek(IOCQ + u64::from(self.io_head) * 16, 16);
let dw3 = u32::from_le_bytes([entry[12], entry[13], entry[14], entry[15]]);
assert_eq!(
dw3 as u16, self.cid,
"the completion is for another command"
);
self.io_head = (self.io_head + 1) % IO_ENTRIES;
self.set_reg(0x100c, self.io_head);
(dw3 >> 17) as u16
}
}
#[test]
fn a_namespace_holds_a_whole_number_of_blocks() {
let store: Arc<dyn Medium> = Arc::new(RamStore::new(1000));
let e = Namespace::new(Arc::clone(&store), 9, false)
.expect_err("1000 is not a multiple of 512")
.to_string();
assert!(e.contains("whole number"), "{e}");
let store: Arc<dyn Medium> = Arc::new(RamStore::new(8192));
assert!(Namespace::new(Arc::clone(&store), 13, false).is_err());
let ns = Namespace::new(store, 12, false).expect("4096-byte blocks");
assert_eq!(ns.blocks(), 2);
assert_eq!(ns.lba_bytes(), 4096);
}
#[test]
fn a_read_only_medium_makes_a_read_only_namespace() {
let ns = Namespace::new(Arc::new(RamStore::new(LBA)) as Arc<dyn Medium>, 9, true)
.expect("one block");
assert!(ns.is_read_only());
}
#[test]
fn the_registers_answer_only_at_the_widths_the_specification_names() {
let r = rig();
assert!(
r.space
.read(REGS + REG_CAP, Width::U8, MemAttrs::DEFAULT)
.is_err()
);
assert!(
r.space
.read(REGS + 2, Width::U32, MemAttrs::DEFAULT)
.is_err(),
"an unaligned dword"
);
let wide = r
.space
.read(REGS + REG_CAP, Width::U64, MemAttrs::DEFAULT)
.expect("a qword read of CAP");
assert_eq!(
wide,
u64::from(r.reg(REG_CAP)) | (u64::from(r.reg(REG_CAP + 4)) << 32)
);
}
#[test]
fn the_admin_queue_registers_are_ignored_while_the_controller_runs() {
let mut r = rig();
r.enable();
r.set_reg64(REG_ASQ, 0xdead_0000);
assert_eq!(
r.reg(REG_ASQ),
ASQ as u32,
"ASQ moved under a running controller"
);
}
#[test]
fn a_prp_entry_aimed_at_the_doorbells_does_not_recurse() {
let mut r = rig();
r.enable();
let status = r.io(0x02, 1, REGS + 0x1000, 0, 0, 0);
assert_eq!(status, 0x0004, "generic status 04h, Data Transfer Error");
let status = r.io(0x02, 1, DATA, REGS + 0x1000, 0, 15);
assert!(
status != 0,
"a list read out of the register block succeeded"
);
assert_eq!(r.io(0x02, 1, DATA, 0, 5, 0), 0);
assert_eq!(r.peek(DATA, LBA), stamp(5));
}
#[test]
fn a_prp_list_that_points_at_itself_terminates() {
let mut r = rig();
r.enable();
let list = DATA + 0x1000;
for i in 0..512u64 {
let entry = if i == 511 {
list
} else {
DATA + 0x2000 + i * 4096
};
r.poke(list + i * 8, &entry.to_le_bytes());
}
let status = r.io(0x02, 1, DATA, list, 0, (3 * 1024 * 1024 / LBA - 1) as u32);
assert_ne!(status, 0, "a ring of PRP lists was walked to completion");
assert_eq!(r.io(0x02, 1, DATA, 0, 3, 0), 0);
}
#[test]
fn an_invalid_doorbell_value_is_fatal_rather_than_ignored() {
let mut r = rig();
r.enable();
r.set_reg(0x1008, IO_ENTRIES);
assert_eq!(r.reg(REG_CSTS) & 0x2, 0x2, "CSTS.CFS");
r.poke(IOSQ, &[0u8; 64]);
r.set_reg(0x1008, 1);
assert_eq!(
u32::from_le_bytes([
r.peek(IOCQ + 12, 4)[0],
r.peek(IOCQ + 12, 4)[1],
r.peek(IOCQ + 12, 4)[2],
r.peek(IOCQ + 12, 4)[3],
]),
0,
"a fatal controller posted a completion"
);
}
#[test]
fn a_command_on_a_queue_whose_completion_queue_is_full_waits() {
let mut r = rig();
r.enable();
for i in 0..(IO_ENTRIES - 1) {
let mut sqe = [0u8; 64];
let cdw0 = 0x02u32 | (u32::from(i as u16 + 100) << 16);
sqe[0..4].copy_from_slice(&cdw0.to_le_bytes());
sqe[4..8].copy_from_slice(&1u32.to_le_bytes());
sqe[24..32].copy_from_slice(&(DATA + u64::from(i) * 512).to_le_bytes());
sqe[40..44].copy_from_slice(&i.to_le_bytes());
r.poke(IOSQ + u64::from(i) * 64, &sqe);
}
r.set_reg(0x1008, IO_ENTRIES - 1);
let mut sqe = [0u8; 64];
let cdw0 = 0x02u32 | (0xbeefu32 << 16);
sqe[0..4].copy_from_slice(&cdw0.to_le_bytes());
sqe[4..8].copy_from_slice(&1u32.to_le_bytes());
sqe[24..32].copy_from_slice(&DATA.to_le_bytes());
r.poke(IOSQ + u64::from(IO_ENTRIES - 1) * 64, &sqe);
r.set_reg(0x1008, 0);
for i in 0..(IO_ENTRIES - 1) {
let entry = r.peek(IOCQ + u64::from(i) * 16 + 12, 4);
let dw3 = u32::from_le_bytes([entry[0], entry[1], entry[2], entry[3]]);
assert_eq!(dw3 as u16, i as u16 + 100);
}
let entry = r.peek(IOCQ + u64::from(IO_ENTRIES - 1) * 16 + 12, 4);
assert_eq!(
u32::from_le_bytes([entry[0], entry[1], entry[2], entry[3]]),
0,
"the eighth completion overwrote the host's queue"
);
r.set_reg(0x100c, 4);
let entry = r.peek(IOCQ + u64::from(IO_ENTRIES - 1) * 16 + 12, 4);
let dw3 = u32::from_le_bytes([entry[0], entry[1], entry[2], entry[3]]);
assert_eq!(dw3 as u16, 0xbeef, "the held command never ran");
}
#[test]
fn the_queue_commands_refuse_what_the_specification_says_they_refuse() {
let mut r = rig();
r.set_reg(REG_AQA, (ADMIN_ENTRIES - 1) | ((ADMIN_ENTRIES - 1) << 16));
r.set_reg64(REG_ASQ, ASQ);
r.set_reg64(REG_ACQ, ACQ);
r.set_reg(REG_CC, 1 | (6 << 17) | (4 << 21));
let (_, _, status) = r.admin(0x01, 0, IOSQ, 0, 1 | (7 << 16), 0x0001 | (1 << 16), 0);
assert_eq!(
status, 0x0100,
"command specific 00h, Completion Queue Invalid"
);
let (_, _, status) = r.admin(0x05, 0, IOCQ, 0, 7 << 16, 0x0003, 0);
assert_eq!(status, 0x0101, "Invalid Queue Identifier");
let (_, _, status) = r.admin(0x05, 0, IOCQ, 0, 1, 0x0003, 0);
assert_eq!(status, 0x0102, "Invalid Queue Size");
let (_, _, status) = r.admin(0x05, 0, IOCQ, 0, 1 | (7 << 16), 0x0002, 0);
assert_eq!(status, 0x0002, "Invalid Field in Command");
let (_, _, status) = r.admin(0x05, 0, IOCQ, 0, 1 | (7 << 16), 0x0003 | (32 << 16), 0);
assert_eq!(status, 0x0108, "Invalid Interrupt Vector");
let (_, _, status) = r.admin(0x05, 0, IOCQ, 0, 1 | (7 << 16), 0x0003, 0);
assert_eq!(status, 0);
let (_, _, status) = r.admin(0x01, 0, IOSQ, 0, 1 | (7 << 16), 0x0001 | (1 << 16), 0);
assert_eq!(status, 0);
let (_, _, status) = r.admin(0x04, 0, 0, 0, 1, 0, 0);
assert_eq!(status, 0x010c, "Invalid Queue Deletion");
let (_, _, status) = r.admin(0x00, 0, 0, 0, 1, 0, 0);
assert_eq!(status, 0, "the submission queue deletes");
let (_, _, status) = r.admin(0x04, 0, 0, 0, 1, 0, 0);
assert_eq!(status, 0, "and then so does the completion queue");
}
#[test]
fn set_features_answers_with_the_queues_it_allocated() {
let mut r = rig();
r.enable();
let (dw0, _, status) = r.admin(0x09, 0, 0, 0, 0x07, 0xffff_ffff, 0);
assert_eq!(status, 0);
assert_eq!(dw0 & 0xffff, u32::from(Params::default().io_queues) - 1);
assert_eq!(dw0 >> 16, u32::from(Params::default().io_queues) - 1);
let (again, _, status) = r.admin(0x0a, 0, 0, 0, 0x07, 0, 0);
assert_eq!((status, again), (0, dw0));
let (_, _, status) = r.admin(0x09, 0, 0, 0, 0x1e, 0, 0);
assert_eq!(status, 0x0002);
}
#[test]
fn an_asynchronous_event_request_is_held_rather_than_completed() {
let mut r = rig();
r.enable();
let before = r.peek(ACQ + u64::from(r.head) * 16, 16);
r.cid = r.cid.wrapping_add(1);
let mut sqe = [0u8; 64];
let cdw0 = 0x0cu32 | (u32::from(r.cid) << 16);
sqe[0..4].copy_from_slice(&cdw0.to_le_bytes());
r.poke(ASQ + u64::from(r.tail) * 64, &sqe);
r.tail = (r.tail + 1) % ADMIN_ENTRIES;
r.set_reg(0x1000, r.tail);
assert_eq!(
r.peek(ACQ + u64::from(r.head) * 16, 16),
before,
"an asynchronous event request completed with nothing to report"
);
let (_, _, status) = r.admin(0x0c, 0, 0, 0, 0, 0, 0);
assert_eq!(status, 0x0105, "Asynchronous Event Request Limit Exceeded");
}
#[test]
fn write_zeroes_moves_no_data_and_still_reaches_the_medium() {
let mut r = rig();
r.enable();
assert_eq!(r.io(0x08, 1, 0, 0, 10, 3), 0, "Write Zeroes");
let mut got = vec![0u8; (4 * LBA) as usize];
Medium::read_at(&*r.store, 10 * LBA, &mut got).expect("the medium reads");
assert!(got.iter().all(|b| *b == 0), "the blocks are not zeroed");
let mut next = vec![0u8; LBA as usize];
Medium::read_at(&*r.store, 14 * LBA, &mut next).expect("the medium reads");
assert_eq!(next, stamp(14));
}
#[test]
fn a_flush_reaches_the_medium_and_an_unknown_opcode_is_refused() {
let mut r = rig();
r.enable();
assert_eq!(r.io(0x00, 1, 0, 0, 0, 0), 0, "Flush");
assert_eq!(r.io(0x00, 0xffff_ffff, 0, 0, 0, 0), 0);
assert_eq!(r.io(0x02, 0xffff_ffff, DATA, 0, 0, 0), 0x000b);
assert_eq!(r.io(0x7f, 1, 0, 0, 0, 0), 0x0001, "Invalid Command Opcode");
}
#[test]
fn a_read_only_namespace_refuses_writes_and_says_so_in_identify() {
let store = Arc::new(RamStore::new(BLOCKS * LBA));
let ns = Namespace::new(Arc::clone(&store) as Arc<dyn Medium>, 9, true).expect("512-byte");
let ctrl = Arc::new(Controller::new(ns, Params::default()));
let space = Arc::new(AddressSpace::new("mem", 32).with_unassigned(UnassignedPolicy::ONES));
{
let mut topo = space.topology();
topo.map(
Region::ram("ram", Arc::new(RamStore::new(RAM_LEN))),
RAM_BASE,
)
.expect("the map fits");
topo.map(
Region::io(
"nvme.regs",
REGISTER_LEN,
Arc::clone(&ctrl) as Arc<dyn MemOps>,
),
REGS,
)
.expect("the map fits");
}
ctrl.attach_space(&space, RequesterId(7));
ctrl.set_master(true);
let ids = WireIdAllocator::new();
let id = ids.alloc();
let wire = Wire::builder().source(id).build_shared();
let irq = WireSource::new(Arc::clone(&wire), id);
ctrl.connect_irq(irq.clone());
let mut r = Rig {
ctrl,
space,
wire,
irq,
store,
tail: 0,
head: 0,
io_tail: 0,
io_head: 0,
cid: 0,
};
r.enable();
assert_eq!(r.io(0x01, 1, DATA, 0, 0, 0), 0x0280, "Write Fault");
assert_eq!(r.io(0x08, 1, 0, 0, 0, 0), 0x0280, "and Write Zeroes too");
let (_, _, status) = r.admin(0x06, 1, DATA, 0, 0x00, 0, 0);
assert_eq!(status, 0);
assert_eq!(r.peek(DATA + 99, 1)[0] & 1, 1, "NSATTR: write protected");
}
#[test]
fn the_output_is_a_level_the_host_releases() {
let mut r = rig();
r.enable();
assert_eq!(r.wire.resolve(Resolve::Or), Level::Low);
assert_eq!(r.irq.level(), Level::Low);
let mut sqe = [0u8; 64];
sqe[0..4].copy_from_slice(&(0x02u32 | (1 << 16)).to_le_bytes());
sqe[4..8].copy_from_slice(&1u32.to_le_bytes());
sqe[24..32].copy_from_slice(&DATA.to_le_bytes());
r.poke(IOSQ, &sqe);
r.set_reg(0x1008, 1);
assert_eq!(r.wire.resolve(Resolve::Or), Level::High);
assert!(r.ctrl.interrupt_pending());
r.ctrl.set_intx_disabled(true);
assert_eq!(r.wire.resolve(Resolve::Or), Level::Low);
assert!(r.ctrl.interrupt_pending(), "the condition is still there");
r.ctrl.set_intx_disabled(false);
assert_eq!(r.wire.resolve(Resolve::Or), Level::High);
r.set_reg(0x100c, 1);
assert_eq!(r.wire.resolve(Resolve::Or), Level::Low);
assert!(!r.ctrl.interrupt_pending());
}
fn snapshot(ctrl: &Controller) -> Vec<u8> {
let mut shape = MachineShape::new();
shape.add_device("nvme", CLASS_NAME).expect("a fresh shape");
let mut w = StateWriter::new(shape);
{
let mut chunk = w.chunk("nvme", CLASS_NAME, STATE_VERSION).expect("a chunk");
ctrl.save(&mut chunk).expect("it saves");
}
w.to_vec().expect("it encodes")
}
#[test]
fn the_controller_round_trips_through_a_snapshot() {
let mut r = rig();
r.enable();
assert_eq!(r.io(0x02, 1, DATA, 0, 7, 0), 0);
let bytes = snapshot(&r.ctrl);
let fresh = rig();
let reader = StateReader::new(&bytes).expect("we just wrote it");
let chunk = reader
.load("nvme", CLASS_NAME, STATE_VERSION, &Migrations::new())
.expect("it is in there");
fresh.ctrl.load(&mut chunk.reader()).expect("it loads");
assert_eq!(
snapshot(&fresh.ctrl),
bytes,
"the controller did not round trip"
);
let mut restored = Rig {
tail: r.tail,
head: r.head,
io_tail: r.io_tail,
io_head: r.io_head,
cid: r.cid,
..fresh
};
restored.ctrl.set_master(true);
assert_eq!(restored.io(0x02, 1, DATA, 0, 9, 0), 0);
assert_eq!(restored.peek(DATA, LBA), stamp(9));
}
#[test]
fn a_snapshot_describing_an_impossible_controller_is_refused() {
let r = rig();
let mut chunk = Vec::new();
for value in [1u32, 1, 0, 0x0007_0007] {
chunk.extend_from_slice(&value.to_le_bytes());
}
chunk.extend_from_slice(&ASQ.to_le_bytes());
chunk.extend_from_slice(&ACQ.to_le_bytes());
chunk.extend_from_slice(&0u32.to_le_bytes());
chunk.push(1);
chunk.extend_from_slice(&ASQ.to_le_bytes());
chunk.extend_from_slice(&0u32.to_le_bytes());
chunk.extend_from_slice(&0u32.to_le_bytes());
chunk.extend_from_slice(&0u32.to_le_bytes());
chunk.extend_from_slice(&0u16.to_le_bytes());
let e = r
.ctrl
.load(&mut ChunkReader::new(&chunk))
.expect_err("a zero-entry queue")
.to_string();
assert!(e.contains("submission queue"), "{e}");
for len in 0..64 {
let bytes: Vec<u8> = (0..len).map(|i| (i as u8).wrapping_mul(37)).collect();
let _ = r.ctrl.load(&mut ChunkReader::new(&bytes));
}
}
#[test]
fn arbitrary_traffic_terminates() {
let r = rig();
let mut seed = 0x243f_6a88_85a3_08d3u64;
let mut next = move || {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
seed
};
for _ in 0..20_000 {
let word = next();
let value = word as u32;
match (word >> 32) & 0x3 {
0 | 1 => {
let sel = (word >> 40) & 0x1f;
let offset = if sel < 0x10 {
sel * 4
} else {
0x1000 + (sel - 0x10) * 4
};
let _ = r.space.write(
REGS + offset,
Width::U32,
u64::from(value),
MemAttrs::DEFAULT,
);
}
2 => {
let addr = RAM_BASE + ((word >> 40) & 0x3f_ffff) * 4 % (RAM_LEN - 4);
let _ = r
.space
.write(addr, Width::U32, u64::from(value), MemAttrs::DEFAULT);
}
_ => {
let offset = ((word >> 40) & 0xf) * 4;
let live = r.space.read(REGS + offset, Width::U32, MemAttrs::DEFAULT);
let dbg = r.space.read(REGS + offset, Width::U32, MemAttrs::DEBUG);
assert_eq!(live, dbg, "a debug read of {offset:#x} disagreed");
}
}
}
}