Repository: chipsalliance/caliptra-sw
Pull request #165 (2023-04-11 22:18:10)

Fix non-word-write NMI when boot ROM copies mailbox contents to ICCM
This problem currently only occurs when running under the RTL. In addition to fixing the bug, this PR also adds support to sw-emulator for NMIs and includes a test that the behavior matches the RTL.


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 21a9ddd0..c8a2b769 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -151,6 +151,7 @@ dependencies = [
  "caliptra-registers",
  "caliptra-test-harness",
  "cfg-if",
+ "zerocopy",
 ]
 
 [[package]]
@@ -263,16 +264,25 @@ dependencies = [
 name = "caliptra-hw-model"
 version = "0.1.0"
 dependencies = [
+ "caliptra-builder",
  "caliptra-emu-bus",
  "caliptra-emu-cpu",
  "caliptra-emu-periph",
  "caliptra-emu-types",
  "caliptra-hw-model-types",
  "caliptra-registers",
+ "caliptra-test-harness-types",
  "caliptra-verilated",
  "ureg",
 ]
 
+[[package]]
+name = "caliptra-hw-model-test-fw"
+version = "0.1.0"
+dependencies = [
+ "caliptra-test-harness",
+]
+
 [[package]]
 name = "caliptra-hw-model-types"
 version = "0.1.0"
@@ -463,6 +473,10 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "caliptra-test-harness-types"
+version = "0.1.0"
+
 [[package]]
 name = "caliptra-verilated"
 version = "0.1.0"
diff --git a/Cargo.toml b/Cargo.toml
index 4b2cf7c3..b6e5e393 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -23,6 +23,7 @@ members = [
   "fmc/test-fw/test-rt",
   "hw-latest/verilated",
   "hw-model",
+  "hw-model/test-fw",
   "hw-model/types",
   "registers",
   "registers/bin/generator",
@@ -54,6 +55,7 @@ members = [
   "rom/dev/tools/test-rt",
   "test",
   "test-harness",
+  "test-harness/types",
 ]
 
 [profile.firmware]
diff --git a/drivers/src/mailbox.rs b/drivers/src/mailbox.rs
index ed2ce0b9..993f3671 100644
--- a/drivers/src/mailbox.rs
+++ b/drivers/src/mailbox.rs
@@ -263,40 +263,26 @@ impl MailboxRecvTxn {
         mbox.cmd().read()
     }
 
-    /// Returns the value stored in the data length register
+    /// Returns the value stored in the data length register. This is the total
+    /// size of the mailbox data in bytes.
     pub fn dlen(&self) -> u32 {
         let mbox = mbox::RegisterBlock::mbox_csr();
         mbox.dlen().read()
     }
 
-    fn dequeue(&self, buf: &mut [u8]) -> CaliptraResult<()> {
+    fn dequeue(&self, buf: &mut [u32]) -> CaliptraResult<()> {
         let mbox = mbox::RegisterBlock::mbox_csr();
-
-        let byte_count = min(buf.len(), mbox.dlen().read() as usize);
-        let remainder = byte_count % size_of::<u32>();
-
-        let n = byte_count - remainder;
-
-        for idx in (0..n).step_by(size_of::<u32>()) {
-            let bytes = buf
-                .get_mut(idx..idx + size_of::<u32>())
-                .ok_or(err_u32!(DequeueErr))?;
-            bytes.copy_from_slice(&mbox.dataout().read().to_le_bytes());
+        let dlen_bytes = mbox.dlen().read() as usize;
+        let dlen_words = (dlen_bytes + 3) / 4;
+        let words_to_read = min(buf.len(), dlen_words);
+        for dest_word in buf[0..words_to_read].iter_mut() {
+            *dest_word = mbox.dataout().read();
         }
-
-        // Handle the remainder.
-        if remainder > 0 {
-            let part = mbox.dataout().read();
-            for idx in 0..remainder {
-                let byte = buf.get_mut(n + idx).ok_or(err_u32!(EnqueueErr))?;
-                *byte = ((part >> (idx << 3)) & 0xFF) as u8;
-            }
-        }
-
         Ok(())
     }
 
-    /// Retrieves data from the maibox without performing state transition.
+    /// Pulls at most `data.len()` words from the mailbox FIFO without performing state transition.
+    ///
     /// # Arguments
     ///
     /// * `data` - data buffer.
@@ -305,15 +291,16 @@ impl MailboxRecvTxn {
     ///
     /// Status of Operation
     ///   
-    pub fn copy_request(&self, offset: usize, data: &mut [u8]) -> CaliptraResult<()> {
+    pub fn copy_request(&self, offset: usize, data: &mut [u32]) -> CaliptraResult<()> {
         if self.state != MailboxOpState::Execute {
             raise_err!(InvalidStateErr)
         }
         self.dequeue(&mut data[offset..])
     }
 
-    /// Retrieves data from the maibox.
+    /// Pulls at most `data.len()` words from the mailbox FIFO.
     /// Transitions from Execute --> Idle (releases the lock)
+    ///
     /// # Arguments
     ///
     /// * `data` - data buffer.
@@ -322,7 +309,7 @@ impl MailboxRecvTxn {
     ///
     /// Status of Operation
     ///   
-    pub fn recv_request(&mut self, data: &mut [u8]) -> CaliptraResult<()> {
+    pub fn recv_request(&mut self, data: &mut [u32]) -> CaliptraResult<()> {
         self.copy_request(0, data)?;
         self.complete(true)?;
         Ok(())
diff --git a/hw-model/Cargo.toml b/hw-model/Cargo.toml
index 20699074..2b615d70 100644
--- a/hw-model/Cargo.toml
+++ b/hw-model/Cargo.toml
@@ -19,4 +19,9 @@ caliptra-emu-types = { path = "../sw-emulator/lib/types" }
 caliptra-hw-model-types = { path = "types" }
 caliptra-registers = { path = "../registers" }
 ureg = { path = "../ureg" }
-caliptra-verilated = { path = "../hw-latest/verilated", optional = true, features = ["verilator"] }
\ No newline at end of file
+caliptra-verilated = { path = "../hw-latest/verilated", optional = true, features = ["verilator"] }
+
+[dev-dependencies]
+caliptra-builder = { path = "../builder" }
+caliptra-registers = { path = "../registers" }
+caliptra-test-harness-types = { path = "../test-harness/types" }
diff --git a/hw-model/src/lib.rs b/hw-model/src/lib.rs
index 8b34a65d..355f8355 100644
--- a/hw-model/src/lib.rs
+++ b/hw-model/src/lib.rs
@@ -28,6 +28,18 @@ pub use model_verilated::ModelVerilated;
 
 use caliptra_emu_types::RvSize;
 
+/// Ideally, general-purpose functions would return `impl HwModel` instead of
+/// `DefaultHwModel` to prevent users from calling functions that aren't
+/// available on all HwModel implementations.  Unfortunately, rust-analyzer
+/// (used by IDEs) can't fully resolve associated types from `impl Trait`, so
+/// such functions should use `DefaultHwModel` until they fix that. Users should
+/// treat `DefaultHwModel` as if it were `impl HwModel`.
+#[cfg(not(feature = "verilator"))]
+pub type DefaultHwModel = ModelEmulated;
+
+#[cfg(feature = "verilator")]
+pub type DefaultHwModel = ModelVerilated;
+
 /// Constructs an HwModel based on the cargo features and environment
 /// variables. Most test cases that need to construct a HwModel should use this
 /// function over HwModel::new_unbooted().
@@ -35,16 +47,8 @@ use caliptra_emu_types::RvSize;
 /// The model returned by this function does not have any fuses programmed and
 /// is not yet ready to execute code in the microcontroller. Most test cases
 /// should use [`new`] instead.
-///
-/// Ideally this function would return `Result<impl HwModel, Box<dyn Error>>`
-/// to prevent users from calling functions that weren't available on HwModel
-/// implementations.  Unfortunately, rust-analyzer (used by IDEs) can't fully
-/// resolve associated types from `impl Trait`, so this function will return the
-/// full type until they fix that. Users should treat this return type as if it
-/// were `impl HwModel`.
-#[cfg(not(feature = "verilator"))]
-pub fn new_unbooted(params: InitParams) -> Result<ModelEmulated, Box<dyn Error>> {
-    ModelEmulated::new_unbooted(params)
+pub fn new_unbooted(params: InitParams) -> Result<DefaultHwModel, Box<dyn Error>> {
+    DefaultHwModel::new_unbooted(params)
 }
 
 /// Constructs an HwModel based on the cargo features and environment variables,
@@ -53,19 +57,8 @@ pub fn new_unbooted(params: InitParams) -> Result<ModelEmulated, Box<dyn Error>>
 /// (optionally) uploading firmware. Most test cases that need to construct a
 /// HwModel should use this function over [`HwModel::new()`] and
 /// [`crate::new_unbooted`].
-#[cfg(not(feature = "verilator"))]
-pub fn new(params: BootParams) -> Result<ModelEmulated, Box<dyn Error>> {
-    ModelEmulated::new(params)
-}
-
-#[cfg(feature = "verilator")]
-pub fn new_unbooted(params: InitParams) -> Result<ModelVerilated, Box<dyn Error>> {
-    ModelVerilated::new_unbooted(params)
-}
-
-#[cfg(feature = "verilator")]
-pub fn new(params: BootParams) -> Result<ModelVerilated, Box<dyn Error>> {
-    ModelVerilated::new(params)
+pub fn new(params: BootParams) -> Result<DefaultHwModel, Box<dyn Error>> {
+    DefaultHwModel::new(params)
 }
 
 pub struct InitParams<'a> {
diff --git a/rom/dev/src/flow/cold_reset/fmc_alias.rs b/rom/dev/src/flow/cold_reset/fmc_alias.rs
index 8d8e9e96..bbd85ee8 100644
--- a/rom/dev/src/flow/cold_reset/fmc_alias.rs
+++ b/rom/dev/src/flow/cold_reset/fmc_alias.rs
@@ -26,10 +26,10 @@ use caliptra_drivers::{
 use caliptra_image_types::ImageManifest;
 use caliptra_image_verify::{ImageVerificationInfo, ImageVerifier};
 use caliptra_x509::{FmcAliasCertTbs, FmcAliasCertTbsParams};
-use zerocopy::FromBytes;
+use zerocopy::{AsBytes, FromBytes};
 
 extern "C" {
-    static mut MAN1_ORG: u8;
+    static mut MAN1_ORG: u32;
 }
 
 rom_err_def! {
@@ -147,13 +147,13 @@ impl FmcAliasLayer {
     /// * `Manifest` - Caliptra Image Bundle Manifest
     fn load_manifest(txn: &MailboxRecvTxn) -> CaliptraResult<ImageManifest> {
         let slice = unsafe {
-            let ptr = &mut MAN1_ORG as *mut u8;
-            core::slice::from_raw_parts_mut(ptr, core::mem::size_of::<ImageManifest>())
+            let ptr = &mut MAN1_ORG as *mut u32;
+            core::slice::from_raw_parts_mut(ptr, core::mem::size_of::<ImageManifest>() / 4)
         };
 
         txn.copy_request(0, slice)?;
 
-        ImageManifest::read_from(slice).ok_or(err_u32!(ManifestReadFailure))
+        ImageManifest::read_from(slice.as_bytes()).ok_or(err_u32!(ManifestReadFailure))
     }
 
     /// Verify the image
@@ -196,8 +196,8 @@ impl FmcAliasLayer {
         );
 
         let fmc_dest = unsafe {
-            let addr = (manifest.fmc.load_addr) as *mut u8;
-            core::slice::from_raw_parts_mut(addr, manifest.fmc.size as usize)
+            let addr = (manifest.fmc.load_addr) as *mut u32;
+            core::slice::from_raw_parts_mut(addr, manifest.fmc.size as usize / 4)
         };
 
         txn.copy_request(0, fmc_dest)?;
@@ -209,8 +209,8 @@ impl FmcAliasLayer {
         );
 
         let runtime_dest = unsafe {
-            let addr = (manifest.runtime.load_addr) as *mut u8;
-            core::slice::from_raw_parts_mut(addr, manifest.runtime.size as usize)
+            let addr = (manifest.runtime.load_addr) as *mut u32;
+            core::slice::from_raw_parts_mut(addr, manifest.runtime.size as usize / 4)
         };
 
         txn.copy_request(0, runtime_dest)?;
@@ -273,7 +273,7 @@ impl FmcAliasLayer {
 
         // TODO: Need a better way to get the Manifest address
         let slice = unsafe {
-            let ptr = &MAN1_ORG as *const u8;
+            let ptr = &MAN1_ORG as *const u32;
             ptr as u32
         };
 
diff --git a/sw-emulator/lib/bus/src/clock.rs b/sw-emulator/lib/bus/src/clock.rs
index a9b9084c..32214798 100644
--- a/sw-emulator/lib/bus/src/clock.rs
+++ b/sw-emulator/lib/bus/src/clock.rs
@@ -28,11 +28,11 @@ use crate::Bus;
 /// # Example
 ///
 /// ```
-/// use caliptra_emu_bus::{Bus, BusError, Timer, TimerAction};
+/// use caliptra_emu_bus::{Bus, BusError, Timer, ActionHandle};
 /// use caliptra_emu_types::{RvAddr, RvData, RvSize};
 /// struct MyPeriph {
 ///     timer: Timer,
-///     action0: Option<TimerAction>,
+///     action0: Option<ActionHandle>,
 /// }
 /// impl Bus for MyPeriph {
 ///     fn read(&mut self, size: RvSize, addr: RvAddr) -> Result<RvData, BusError> {
@@ -77,7 +77,7 @@ impl Timer {
     /// If the scheduled time for `action` has come, `action` will be set to
     /// None and the function will return true. Otherwise (or if action is None),
     /// the function will return false.
-    pub fn fired(&self, action: &mut Option<TimerAction>) -> bool {
+    pub fn fired(&self, action: &mut Option<ActionHandle>) -> bool {
         let has_fired = if let Some(ref action) = action {
             debug_assert_eq!(
                 action.0.id.timer_ptr,
@@ -95,33 +95,22 @@ impl Timer {
     }
 
     /// Schedules a future call to [`Bus::poll()`] at `time`, and returns a
-    /// `TimerAction` that can be used with [`Timer::cancel`] or
+    /// `ActionHandle` that can be used with [`Timer::cancel`] or
     /// [`Timer::fired`].
-    pub fn schedule_poll_at(&self, time: u64) -> TimerAction {
-        self.clock.schedule_action_at(time, TimerActionType::Poll)
+    pub fn schedule_poll_at(&self, time: u64) -> ActionHandle {
+        self.clock.schedule_action_at(time, TimerAction::Poll)
     }
 
     /// Schedules a future call to [`Bus::poll()`] at `self.now() + time`, and
-    /// returns a `TimerAction` that can be used with [`Timer::cancel`] or
+    /// returns a `ActionHandle` that can be used with [`Timer::cancel`] or
     /// [`Timer::fired`].
-    pub fn schedule_poll_in(&self, ticks_from_now: u64) -> TimerAction {
+    pub fn schedule_poll_in(&self, ticks_from_now: u64) -> ActionHandle {
         self.schedule_poll_at(self.now().wrapping_add(ticks_from_now))
     }
 
-    /// Schedules a future call to [`Bus::warm_reset()` or `Bus::update_reset()`] at `self.now() + time`, and
-    /// returns a `TimerAction` that can be used with [`Timer::cancel`] or
-    /// [`Timer::fired`].
-    pub fn schedule_reset_in(
-        &self,
-        ticks_from_now: u64,
-        reset_type: TimerActionType,
-    ) -> TimerAction {
-        assert!(
-            reset_type != TimerActionType::WarmReset || reset_type != TimerActionType::UpdateReset,
-            "Cannot schedule a reset timer action with {} action type.",
-            reset_type as u32
-        );
-
+    /// Schedules an action at `self.now() + time`, and returns an `ActionHandle`
+    /// that can be used with [`Timer::cancel`] or [`Timer::fired`].
+    pub fn schedule_action_in(&self, ticks_from_now: u64, reset_type: TimerAction) -> ActionHandle {
         self.clock
             .schedule_action_at(self.now().wrapping_add(ticks_from_now), reset_type)
     }
@@ -130,8 +119,8 @@ impl Timer {
     ///
     /// * Panics
     ///
-    /// Panics if the supplied `TimerAction` was not created by this Timer.
-    pub fn cancel(&self, handle: TimerAction) {
+    /// Panics if the supplied `ActionHandle` was not created by this Timer.
+    pub fn cancel(&self, handle: ActionHandle) {
         self.clock.cancel(handle)
     }
 }
@@ -167,7 +156,7 @@ impl Clock {
     /// Increments the clock by `delta`, and returns a list of timer
     /// actions fired.
     #[inline]
-    pub fn increment(&self, delta: u64) -> HashSet<TimerActionType> {
+    pub fn increment(&self, delta: u64) -> HashSet<TimerAction> {
         self.clock.increment(delta)
     }
 
@@ -178,48 +167,50 @@ impl Clock {
         &self,
         delta: u64,
         bus: &mut impl Bus,
-    ) -> HashSet<TimerActionType> {
-        let fired_action_types = self.increment(delta);
-        for action_type in fired_action_types.iter() {
-            match action_type {
-                TimerActionType::Poll => bus.poll(),
-                TimerActionType::WarmReset => {
+    ) -> HashSet<TimerAction> {
+        let fired_actions = self.increment(delta);
+        for action in fired_actions.iter() {
+            match action {
+                TimerAction::Poll => bus.poll(),
+                TimerAction::WarmReset => {
                     bus.warm_reset();
                     break;
                 }
-                TimerActionType::UpdateReset => {
+                TimerAction::UpdateReset => {
                     bus.update_reset();
                     break;
                 }
+                TimerAction::Nmi { .. } => {}
+                TimerAction::SetNmiVec { .. } => {}
             }
         }
-        fired_action_types
+        fired_actions
     }
 }
 
 /// Represents an action scheduled with a `Timer`. Returned by
 /// [`Timer::schedule_poll_at`] and passed to [`Timer::has_fired()`] or
 /// [`Timer::cancel`].
-pub struct TimerAction(TimerActionImpl);
-impl From<TimerActionImpl> for TimerAction {
-    fn from(val: TimerActionImpl) -> Self {
-        TimerAction(val)
+pub struct ActionHandle(ActionHandleImpl);
+impl From<ActionHandleImpl> for ActionHandle {
+    fn from(val: ActionHandleImpl) -> Self {
+        ActionHandle(val)
     }
 }
 
-/// The internal representation of `TimerAction`. We keep these types separate
+/// The internal representation of `ActionHandle`. We keep these types separate
 /// to avoid exposing these implementation-specific traits.
 #[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
-struct TimerActionImpl {
+struct ActionHandleImpl {
     /// The time the action is supposed to fire.
     time: u64,
 
     id: TimerActionId,
 
-    action_type: TimerActionType,
+    action: TimerAction,
 }
-impl From<TimerAction> for TimerActionImpl {
-    fn from(val: TimerAction) -> Self {
+impl From<ActionHandle> for ActionHandleImpl {
+    fn from(val: ActionHandle) -> Self {
         val.0
     }
 }
@@ -243,18 +234,20 @@ impl Default for TimerActionId {
     }
 }
 
-#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
-pub enum TimerActionType {
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
+pub enum TimerAction {
     Poll,
     WarmReset,
     UpdateReset,
+    Nmi { mcause: u32 },
+    SetNmiVec { addr: u32 },
 }
 
 struct ClockImpl {
     now: Cell<u64>,
     next_action_time: Cell<Option<u64>>,
     next_action_id: Cell<u64>,
-    future_poll_actions: RefCell<BTreeSet<TimerActionImpl>>,
+    action_handles: RefCell<BTreeSet<ActionHandleImpl>>,
 }
 impl ClockImpl {
     fn new() -> Rc<Self> {
@@ -262,7 +255,7 @@ impl ClockImpl {
             now: Cell::new(0),
             next_action_time: Cell::new(None),
             next_action_id: Cell::new(0),
-            future_poll_actions: RefCell::new(BTreeSet::new()),
+            action_handles: RefCell::new(BTreeSet::new()),
         })
     }
 
@@ -272,8 +265,8 @@ impl ClockImpl {
     }
 
     #[inline]
-    fn increment(&self, delta: u64) -> HashSet<TimerActionType> {
-        let mut fired_action_types: HashSet<TimerActionType> = HashSet::new();
+    fn increment(&self, delta: u64) -> HashSet<TimerAction> {
+        let mut fired_actions: HashSet<TimerAction> = HashSet::new();
         assert!(
             delta < (u64::MAX >> 1),
             "Cannot increment the current time by more than {} clock cycles.",
@@ -282,37 +275,37 @@ impl ClockImpl {
         self.now.set(self.now.get().wrapping_add(delta));
         if let Some(next_action_time) = self.next_action_time.get() {
             if self.has_fired(next_action_time) {
-                self.remove_fired_actions(&mut fired_action_types);
-                return fired_action_types;
+                self.remove_fired_actions(&mut fired_actions);
+                return fired_actions;
             }
         }
-        fired_action_types
+        fired_actions
     }
 
-    fn schedule_action_at(self: &Rc<Self>, time: u64, action_type: TimerActionType) -> TimerAction {
+    fn schedule_action_at(self: &Rc<Self>, time: u64, action: TimerAction) -> ActionHandle {
         assert!(
             time.wrapping_sub(self.now()) < (u64::MAX >> 1),
             "Cannot schedule a timer action more than {} clock cycles from now.",
             (u64::MAX >> 1)
         );
-        let new_action = TimerActionImpl {
+        let new_action = ActionHandleImpl {
             time,
             id: self.next_action_id(),
-            action_type,
+            action,
         };
-        let mut actions = self.future_poll_actions.borrow_mut();
+        let mut actions = self.action_handles.borrow_mut();
         actions.insert(new_action);
         self.recompute_next_action_time(&actions);
         new_action.into()
     }
-    fn cancel(self: &Rc<Self>, action: TimerAction) {
-        let action = TimerActionImpl::from(action);
+    fn cancel(self: &Rc<Self>, action: ActionHandle) {
+        let action = ActionHandleImpl::from(action);
         assert_eq!(
             Rc::as_ptr(self),
             action.id.timer_ptr,
             "Supplied action was not created by this timer."
         );
-        let mut future_actions = self.future_poll_actions.borrow_mut();
+        let mut future_actions = self.action_handles.borrow_mut();
         future_actions.remove(&action);
         self.recompute_next_action_time(&future_actions)
     }
@@ -328,19 +321,19 @@ impl ClockImpl {
     fn has_fired(&self, action_time: u64) -> bool {
         self.now().wrapping_sub(action_time) < (u64::MAX >> 1)
     }
-    fn recompute_next_action_time(&self, future_actions: &BTreeSet<TimerActionImpl>) {
+    fn recompute_next_action_time(&self, future_actions: &BTreeSet<ActionHandleImpl>) {
         self.next_action_time
             .set(self.find_next_action(future_actions).map(|a| a.time));
     }
     fn find_next_action<'a>(
         &self,
-        future_actions: &'a BTreeSet<TimerActionImpl>,
-    ) -> Option<&'a TimerActionImpl> {
+        future_actions: &'a BTreeSet<ActionHandleImpl>,
+    ) -> Option<&'a ActionHandleImpl> {
         let search_start = self.now().wrapping_sub(u64::MAX >> 1);
-        let search_action = TimerActionImpl {
+        let search_action = ActionHandleImpl {
             time: search_start,
             id: TimerActionId::default(),
-            action_type: TimerActionType::Poll,
+            action: TimerAction::Poll,
         };
         if let Some(action) = future_actions.range(&search_action..).next() {
             return Some(action);
@@ -349,15 +342,15 @@ impl ClockImpl {
     }
 
     #[cold]
-    fn remove_fired_actions(&self, fired_action_types: &mut HashSet<TimerActionType>) {
-        let mut future_actions = self.future_poll_actions.borrow_mut();
+    fn remove_fired_actions(&self, fired_actions: &mut HashSet<TimerAction>) {
+        let mut future_actions = self.action_handles.borrow_mut();
         while let Some(action) = self.find_next_action(&future_actions) {
             if !self.has_fired(action.time) {
                 break;
             }
             let action = *action;
             future_actions.remove(&action);
-            fired_action_types.insert(action.action_type);
+            fired_actions.insert(action.action);
         }
         self.recompute_next_action_time(&future_actions);
     }
@@ -389,7 +382,7 @@ mod tests {
         let mut action3 = Some(timer.schedule_poll_in(100));
         let mut action4 = Some(timer.schedule_poll_in(101));
         let mut action5 = Some(timer.schedule_poll_in(102));
-        let mut action6 = Option::<TimerAction>::None;
+        let mut action6 = Option::<ActionHandle>::None;
 
         assert!(clock.increment(24).is_empty());
         assert_eq!(clock.now().wrapping_sub(t0), 24);
diff --git a/sw-emulator/lib/bus/src/lib.rs b/sw-emulator/lib/bus/src/lib.rs
index bf14de38..d874c548 100644
--- a/sw-emulator/lib/bus/src/lib.rs
+++ b/sw-emulator/lib/bus/src/lib.rs
@@ -22,7 +22,7 @@ mod rom;
 pub mod testing;
 
 pub use crate::bus::{Bus, BusError};
-pub use crate::clock::{Clock, Timer, TimerAction, TimerActionType};
+pub use crate::clock::{ActionHandle, Clock, Timer, TimerAction};
 pub use crate::dynamic_bus::DynamicBus;
 pub use crate::ram::Ram;
 pub use crate::register::{
diff --git a/sw-emulator/lib/cpu/src/cpu.rs b/sw-emulator/lib/cpu/src/cpu.rs
index eda5b4bc..55cdbf6c 100644
--- a/sw-emulator/lib/cpu/src/cpu.rs
+++ b/sw-emulator/lib/cpu/src/cpu.rs
@@ -15,7 +15,7 @@ Abstract:
 use crate::csr_file::{Csr, CsrFile};
 use crate::types::{RvInstr, RvMStatus};
 use crate::xreg_file::{XReg, XRegFile};
-use caliptra_emu_bus::{Bus, BusError, Clock, TimerActionType};
+use caliptra_emu_bus::{Bus, BusError, Clock, TimerAction};
 use caliptra_emu_types::{RvAddr, RvData, RvException, RvSize};
 
 pub type InstrTracer<'a> = dyn FnMut(u32, RvInstr) + 'a;
@@ -61,6 +61,9 @@ pub struct Cpu<TBus: Bus> {
     /// The next program counter after the current instruction is finished executing.
     next_pc: RvData,
 
+    /// The NMI vector. In the real CPU, this is hardwired from the outside.
+    nmivec: u32,
+
     // The bus the CPU uses to talk to memory and peripherals.
     pub bus: TBus,
 
@@ -101,6 +104,7 @@ impl<TBus: Bus> Cpu<TBus> {
             clock,
             is_execute_instr: false,
             watch_ptr_cfg: WatchPtrCfg::new(),
+            nmivec: 0,
         }
     }
 
@@ -300,24 +304,22 @@ impl<TBus: Bus> Cpu<TBus> {
     }
 
     /// Step a single instruction
-    ///
-    /// # Error
-    ///
-    /// * `RvException` - Exception
     pub fn step(&mut self, instr_tracer: Option<&mut InstrTracer>) -> StepAction {
         let fired_action_types = self
             .clock
             .increment_and_process_timer_actions(1, &mut self.bus);
         for action_type in fired_action_types.iter() {
             match action_type {
-                TimerActionType::WarmReset => {
+                TimerAction::WarmReset => {
                     self.reset_pc();
                     break;
                 }
-                TimerActionType::UpdateReset => {
+                TimerAction::UpdateReset => {
                     self.reset_pc();
                     break;
                 }
+                TimerAction::Nmi { mcause } => return self.handle_nmi(*mcause, 0),
+                TimerAction::SetNmiVec { addr } => self.nmivec = *addr,
                 _ => {}
             }
         }
@@ -329,16 +331,14 @@ impl<TBus: Bus> Cpu<TBus> {
     }
 
     /// Handle synchronous exception
-    ///
-    /// # Error
-    ///
-    /// * `RvException` - Exception
     fn handle_exception(&mut self, exception: RvException) -> StepAction {
         let ret = self.handle_trap(
             false,
             self.read_pc(),
             exception.cause().into(),
             exception.info(),
+            // Cannot panic; mtvec is a valid CSR
+            self.read_csr(Csr::MTVEC).unwrap() & !0b11,
         );
         match ret {
             Ok(_) => StepAction::Continue,
@@ -346,6 +346,15 @@ impl<TBus: Bus> Cpu<TBus> {
         }
     }
 
+    /// Handle non-maskable interrupt (VeeR-specific)
+    fn handle_nmi(&mut self, cause: u32, info: u32) -> StepAction {
+        let ret = self.handle_trap(false, self.read_pc(), cause, info, self.nmivec);
+        match ret {
+            Ok(_) => StepAction::Continue,
+            Err(_) => StepAction::Fatal,
+        }
+    }
+
     /// Handle synchronous & asynchronous trap
     ///
     /// # Error
@@ -357,6 +366,7 @@ impl<TBus: Bus> Cpu<TBus> {
         pc: RvAddr,
         cause: u32,
         info: u32,
+        next_pc: u32,
     ) -> Result<(), RvException> {
         // TODO: Implement Interrupt Handling
         // 1. Support for vectored asynchronous interrupts
@@ -372,7 +382,6 @@ impl<TBus: Bus> Cpu<TBus> {
         status.set_mie(0);
         self.write_csr(Csr::MSTATUS, status.0)?;
 
-        let next_pc = self.read_csr(Csr::MTVEC)? & !0b11;
         self.write_pc(next_pc);
         println!("{:x}", next_pc);
         Ok(())
diff --git a/sw-emulator/lib/periph/src/asym_ecc384.rs b/sw-emulator/lib/periph/src/asym_ecc384.rs
index 861ca37f..3c25eaf2 100644
--- a/sw-emulator/lib/periph/src/asym_ecc384.rs
+++ b/sw-emulator/lib/periph/src/asym_ecc384.rs
@@ -14,7 +14,7 @@ Abstract:
 
 use crate::helpers::{bytes_from_words_le, words_from_bytes_le};
 use crate::{KeyUsage, KeyVault};
-use caliptra_emu_bus::{BusError, Clock, ReadOnlyRegister, ReadWriteRegister, Timer, TimerAction};
+use caliptra_emu_bus::{ActionHandle, BusError, Clock, ReadOnlyRegister, ReadWriteRegister, Timer};
 use caliptra_emu_crypto::{Ecc384, Ecc384PubKey, Ecc384Signature};
 use caliptra_emu_derive::Bus;
 use caliptra_emu_types::{RvData, RvSize};
@@ -214,19 +214,19 @@ pub struct AsymEcc384 {
     timer: Timer,
 
     /// Operation complete callback
-    op_complete_action: Option<TimerAction>,
+    op_complete_action: Option<ActionHandle>,
 
     /// Key read complete action
-    op_key_read_complete_action: Option<TimerAction>,
+    op_key_read_complete_action: Option<ActionHandle>,
 
     /// Seed read complete action
-    op_seed_read_complete_action: Option<TimerAction>,
+    op_seed_read_complete_action: Option<ActionHandle>,
 
     /// Msg read complete action
-    op_msg_read_complete_action: Option<TimerAction>,
+    op_msg_read_complete_action: Option<ActionHandle>,
 
     /// Key write complete action
-    op_key_write_complete_action: Option<TimerAction>,
+    op_key_write_complete_action: Option<ActionHandle>,
 }
 
 impl AsymEcc384 {
diff --git a/sw-emulator/lib/periph/src/doe.rs b/sw-emulator/lib/periph/src/doe.rs
index 72a37f4d..8758051f 100644
--- a/sw-emulator/lib/periph/src/doe.rs
+++ b/sw-emulator/lib/periph/src/doe.rs
@@ -14,7 +14,7 @@ Abstract:
 
 use crate::{KeyVault, SocRegistersInternal};
 use caliptra_emu_bus::{
-    BusError, Clock, ReadOnlyRegister, ReadWriteMemory, ReadWriteRegister, Timer, TimerAction,
+    ActionHandle, BusError, Clock, ReadOnlyRegister, ReadWriteMemory, ReadWriteRegister, Timer,
 };
 use caliptra_emu_crypto::Aes256Cbc;
 use caliptra_emu_derive::Bus;
@@ -82,7 +82,7 @@ pub struct Doe {
     soc_reg: SocRegistersInternal,
 
     /// Operation Complete Action
-    op_complete_action: Option<TimerAction>,
+    op_complete_action: Option<ActionHandle>,
 }
 
 impl Doe {
@@ -249,7 +249,7 @@ mod tests {
         let soc_reg = SocRegistersInternal::new(
             &clock,
             Mailbox::new(MailboxRam::new()),
-            Iccm::new(),
+            Iccm::new(&clock),
             CaliptraRootBusArgs::default(),
         );
         let mut doe = Doe::new(&clock, key_vault.clone(), soc_reg);
@@ -308,7 +308,7 @@ mod tests {
         let soc_reg = SocRegistersInternal::new(
             &clock,
             Mailbox::new(MailboxRam::new()),
-            Iccm::new(),
+            Iccm::new(&clock),
             CaliptraRootBusArgs::default(),
         );
         let mut doe = Doe::new(&clock, key_vault.clone(), soc_reg);
@@ -365,7 +365,7 @@ mod tests {
         let soc_reg = SocRegistersInternal::new(
             &clock,
             Mailbox::new(MailboxRam::new()),
-            Iccm::new(),
+            Iccm::new(&clock),
             CaliptraRootBusArgs::default(),
         );
         let mut doe = Doe::new(&clock, key_vault, soc_reg.clone());
diff --git a/sw-emulator/lib/periph/src/hash_sha256.rs b/sw-emulator/lib/periph/src/hash_sha256.rs
index 510c3ce0..71d3e8ce 100644
--- a/sw-emulator/lib/periph/src/hash_sha256.rs
+++ b/sw-emulator/lib/periph/src/hash_sha256.rs
@@ -13,8 +13,8 @@ Abstract:
 --*/
 
 use caliptra_emu_bus::{
-    BusError, Clock, ReadOnlyMemory, ReadOnlyRegister, ReadWriteMemory, ReadWriteRegister, Timer,
-    TimerAction,
+    ActionHandle, BusError, Clock, ReadOnlyMemory, ReadOnlyRegister, ReadWriteMemory,
+    ReadWriteRegister, Timer,
 };
 use caliptra_emu_crypto::{Sha256, Sha256Mode};
 use caliptra_emu_derive::Bus;
@@ -97,7 +97,7 @@ pub struct HashSha256 {
 
     timer: Timer,
 
-    op_complete_action: Option<TimerAction>,
+    op_complete_action: Option<ActionHandle>,
 }
 
 impl HashSha256 {
diff --git a/sw-emulator/lib/periph/src/hash_sha512.rs b/sw-emulator/lib/periph/src/hash_sha512.rs
index 2fac71f7..acfdd75b 100644
--- a/sw-emulator/lib/periph/src/hash_sha512.rs
+++ b/sw-emulator/lib/periph/src/hash_sha512.rs
@@ -16,7 +16,7 @@ use crate::helpers::words_from_bytes_le;
 use crate::key_vault::KeyUsage;
 use crate::KeyVault;
 use caliptra_emu_bus::{
-    BusError, Clock, ReadOnlyMemory, ReadOnlyRegister, ReadWriteRegister, Timer, TimerAction,
+    ActionHandle, BusError, Clock, ReadOnlyMemory, ReadOnlyRegister, ReadWriteRegister, Timer,
 };
 use caliptra_emu_crypto::EndianessTransform;
 use caliptra_emu_crypto::{Sha512, Sha512Mode};
@@ -182,13 +182,13 @@ pub struct HashSha512 {
     timer: Timer,
 
     /// Operation complete action
-    op_complete_action: Option<TimerAction>,
+    op_complete_action: Option<ActionHandle>,
 
     /// Block read complete action
-    op_block_read_complete_action: Option<TimerAction>,
+    op_block_read_complete_action: Option<ActionHandle>,
 
     /// Hash write complete action
-    op_hash_write_complete_action: Option<TimerAction>,
+    op_hash_write_complete_action: Option<ActionHandle>,
 }
 
 impl HashSha512 {
diff --git a/sw-emulator/lib/periph/src/hmac_sha384.rs b/sw-emulator/lib/periph/src/hmac_sha384.rs
index d07d0209..6f6526aa 100644
--- a/sw-emulator/lib/periph/src/hmac_sha384.rs
+++ b/sw-emulator/lib/periph/src/hmac_sha384.rs
@@ -14,8 +14,8 @@ Abstract:
 
 use crate::{KeyUsage, KeyVault};
 use caliptra_emu_bus::{
-    BusError, Clock, ReadOnlyMemory, ReadOnlyRegister, ReadWriteMemory, ReadWriteRegister, Timer,
-    TimerAction, WriteOnlyMemory,
+    ActionHandle, BusError, Clock, ReadOnlyMemory, ReadOnlyRegister, ReadWriteMemory,
+    ReadWriteRegister, Timer, WriteOnlyMemory,
 };
 use caliptra_emu_crypto::EndianessTransform;
 use caliptra_emu_crypto::{Hmac512, Hmac512Mode};
@@ -179,16 +179,16 @@ pub struct HmacSha384 {
     timer: Timer,
 
     /// Operation complete action
-    op_complete_action: Option<TimerAction>,
+    op_complete_action: Option<ActionHandle>,
 
     /// Key read complete action
-    op_key_read_complete_action: Option<TimerAction>,
+    op_key_read_complete_action: Option<ActionHandle>,
 
     /// Block read complete action
-    op_block_read_complete_action: Option<TimerAction>,
+    op_block_read_complete_action: Option<ActionHandle>,
 
     /// Tag write complete action
-    op_tag_write_complete_action: Option<TimerAction>,
+    op_tag_write_complete_action: Option<ActionHandle>,
 }
 
 impl HmacSha384 {
diff --git a/sw-emulator/lib/periph/src/iccm.rs b/sw-emulator/lib/periph/src/iccm.rs
index 00bbec21..efd979e6 100644
--- a/sw-emulator/lib/periph/src/iccm.rs
+++ b/sw-emulator/lib/periph/src/iccm.rs
@@ -14,7 +14,10 @@ Abstract:
 use caliptra_emu_bus::Bus;
 use caliptra_emu_bus::BusError;
 use caliptra_emu_bus::BusError::StoreAccessFault;
+use caliptra_emu_bus::Clock;
 use caliptra_emu_bus::Ram;
+use caliptra_emu_bus::Timer;
+use caliptra_emu_bus::TimerAction;
 use caliptra_emu_types::RvAddr;
 use caliptra_emu_types::RvData;
 use caliptra_emu_types::RvSize;
@@ -35,29 +38,25 @@ impl Iccm {
         self.iccm.borrow_mut().locked = false;
     }
 
-    pub fn new() -> Self {
+    pub fn new(clock: &Clock) -> Self {
         Self {
-            iccm: Rc::new(RefCell::new(IccmImpl::new())),
+            iccm: Rc::new(RefCell::new(IccmImpl::new(clock))),
         }
     }
 }
 
-impl Default for Iccm {
-    fn default() -> Self {
-        Iccm::new()
-    }
-}
-
 struct IccmImpl {
     ram: Ram,
     locked: bool,
+    timer: Timer,
 }
 
 impl IccmImpl {
-    pub fn new() -> Self {
+    pub fn new(clock: &Clock) -> Self {
         Self {
             ram: Ram::new(vec![0; ICCM_SIZE_BYTES]),
             locked: false,
+            timer: clock.timer(),
         }
     }
 }
@@ -70,21 +69,48 @@ impl Bus for Iccm {
 
     /// Write data of specified size to given address
     fn write(&mut self, size: RvSize, addr: RvAddr, val: RvData) -> Result<(), BusError> {
-        if self.iccm.borrow_mut().locked {
+        // NMIs don't fire immediately; a couple instructions is a fairly typicaly delay on VeeR.
+        const NMI_DELAY: u64 = 2;
+
+        // From RISC-V_VeeR_EL2_PRM.pdf
+        const NMI_CAUSE_DBUS_STORE_ERROR: u32 = 0xf000_0000;
+
+        let mut iccm = self.iccm.borrow_mut();
+
+        if size != RvSize::Word || (addr & 0x3) != 0 {
+            iccm.timer.schedule_action_in(
+                NMI_DELAY,
+                TimerAction::Nmi {
+                    mcause: NMI_CAUSE_DBUS_STORE_ERROR,
+                },
+            );
+            return Ok(());
+        }
+        if iccm.locked {
             return Err(StoreAccessFault);
         }
-
-        self.iccm.borrow_mut().ram.write(size, addr, val)
+        iccm.ram.write(size, addr, val)
     }
 }
 
 #[cfg(test)]
 mod tests {
+
     use super::*;
 
+    fn next_action(clock: &Clock) -> Option<TimerAction> {
+        let mut actions = clock.increment(4);
+        match actions.len() {
+            0 => None,
+            1 => actions.drain().next(),
+            _ => panic!("More than one action scheduled; unexpected"),
+        }
+    }
+
     #[test]
     fn test_unlocked_write() {
-        let mut iccm = Iccm::new();
+        let clock = Clock::new();
+        let mut iccm = Iccm::new(&clock);
         for word_offset in (0u32..ICCM_SIZE_BYTES as u32).step_by(4) {
             assert_eq!(iccm.read(RvSize::Word, word_offset).unwrap(), 0);
             assert_eq!(
@@ -93,11 +119,13 @@ mod tests {
             );
             assert_eq!(iccm.read(RvSize::Word, word_offset).ok(), Some(u32::MAX));
         }
+        assert_eq!(next_action(&clock), None);
     }
 
     #[test]
     fn test_locked_write() {
-        let mut iccm = Iccm::new();
+        let clock = Clock::new();
+        let mut iccm = Iccm::new(&clock);
         iccm.lock();
         for word_offset in (0u32..ICCM_SIZE_BYTES as u32).step_by(4) {
             assert_eq!(iccm.read(RvSize::Word, word_offset).unwrap(), 0);
@@ -106,5 +134,19 @@ mod tests {
                 Some(BusError::StoreAccessFault)
             );
         }
+        assert_eq!(next_action(&clock), None);
+    }
+
+    #[test]
+    fn test_byte_write() {
+        let clock = Clock::new();
+        let mut iccm = Iccm::new(&clock);
+        assert_eq!(iccm.write(RvSize::Byte, 0, 42), Ok(()));
+        assert_eq!(
+            next_action(&clock),
+            Some(TimerAction::Nmi {
+                mcause: 0xf000_0000
+            })
+        );
     }
 }
diff --git a/sw-emulator/lib/periph/src/root_bus.rs b/sw-emulator/lib/periph/src/root_bus.rs
index f2982ce9..2f3bcccf 100644
--- a/sw-emulator/lib/periph/src/root_bus.rs
+++ b/sw-emulator/lib/periph/src/root_bus.rs
@@ -216,7 +216,7 @@ impl CaliptraRootBus {
         let mailbox_ram = MailboxRam::new();
         let mailbox = Mailbox::new(mailbox_ram.clone());
         let rom = Rom::new(std::mem::take(&mut args.rom));
-        let iccm = Iccm::new();
+        let iccm = Iccm::new(clock);
         let soc_reg = SocRegistersInternal::new(clock, mailbox.clone(), iccm.clone(), args);
 
         Self {
diff --git a/sw-emulator/lib/periph/src/sha512_acc.rs b/sw-emulator/lib/periph/src/sha512_acc.rs
index ab144883..39cc0f5f 100644
--- a/sw-emulator/lib/periph/src/sha512_acc.rs
+++ b/sw-emulator/lib/periph/src/sha512_acc.rs
@@ -13,7 +13,7 @@ Abstract:
 --*/
 use crate::MailboxRam;
 use caliptra_emu_bus::{
-    Bus, BusError, Clock, ReadOnlyMemory, ReadOnlyRegister, ReadWriteRegister, Timer, TimerAction,
+    ActionHandle, Bus, BusError, Clock, ReadOnlyMemory, ReadOnlyRegister, ReadWriteRegister, Timer,
 };
 use caliptra_emu_crypto::{EndianessTransform, Sha512, Sha512Mode};
 use caliptra_emu_derive::Bus;
@@ -135,7 +135,7 @@ pub struct Sha512Accelerator {
     state_machine: StateMachine<Context>,
 
     /// Operation complete action
-    op_complete_action: Option<TimerAction>,
+    op_complete_action: Option<ActionHandle>,
 }
 
 impl Sha512Accelerator {
diff --git a/sw-emulator/lib/periph/src/soc_reg.rs b/sw-emulator/lib/periph/src/soc_reg.rs
index d9680d20..52a19dfa 100644
--- a/sw-emulator/lib/periph/src/soc_reg.rs
+++ b/sw-emulator/lib/periph/src/soc_reg.rs
@@ -17,8 +17,8 @@ use crate::root_bus::ReadyForFwCbArgs;
 use crate::{CaliptraRootBusArgs, Iccm, Mailbox};
 use caliptra_emu_bus::BusError::{LoadAccessFault, StoreAccessFault};
 use caliptra_emu_bus::{
-    Bus, BusError, Clock, ReadOnlyMemory, ReadOnlyRegister, ReadWriteRegister, Register, Timer,
-    TimerAction, TimerActionType,
+    ActionHandle, Bus, BusError, Clock, ReadOnlyMemory, ReadOnlyRegister, ReadWriteRegister,
+    Register, Timer, TimerAction,
 };
 use caliptra_emu_derive::Bus;
 use caliptra_emu_types::{RvAddr, RvData, RvSize};
@@ -426,7 +426,7 @@ struct SocRegistersImpl {
     internal_fw_update_reset_wait_cycles: ReadWriteRegister<u32, FwUpdateResetWaitCycles::Register>,
 
     /// INTERNAL_NMI_VECTOR Register
-    #[register(offset = 0x062c)]
+    #[register(offset = 0x062c, write_fn = on_write_internal_nmi_vector)]
     internal_nmi_vector: ReadWriteRegister<u32>,
 
     /// Mailbox
@@ -442,21 +442,21 @@ struct SocRegistersImpl {
     timer: Timer,
 
     /// Firmware Write Complete action
-    op_fw_write_complete_action: Option<TimerAction>,
+    op_fw_write_complete_action: Option<ActionHandle>,
     #[allow(clippy::type_complexity)]
     op_fw_write_complete_cb: Option<Box<dyn FnOnce(&mut Mailbox)>>,
 
     /// Firmware Read Complete action
-    op_fw_read_complete_action: Option<TimerAction>,
+    op_fw_read_complete_action: Option<ActionHandle>,
 
     /// IDEVID CSR Read Complete action
-    op_idevid_csr_read_complete_action: Option<TimerAction>,
+    op_idevid_csr_read_complete_action: Option<ActionHandle>,
 
     /// LDEVID Cert Read Complete action
-    op_ldevid_cert_read_complete_action: Option<TimerAction>,
+    op_ldevid_cert_read_complete_action: Option<ActionHandle>,
 
     /// Reset Trigger action
-    op_reset_trigger_action: Option<TimerAction>,
+    op_reset_trigger_action: Option<ActionHandle>,
 
     /// test bench services callback
     tb_services_cb: Box<dyn FnMut(u8)>,
@@ -708,10 +708,16 @@ impl SocRegistersImpl {
         self.internal_fw_update_reset.write(size, val)?;
 
         // Schedule a firmware update reset timer action.
-        self.op_reset_trigger_action = Some(
-            self.timer
-                .schedule_reset_in(0, TimerActionType::UpdateReset),
-        );
+        self.op_reset_trigger_action =
+            Some(self.timer.schedule_action_in(0, TimerAction::UpdateReset));
+        Ok(())
+    }
+    fn on_write_internal_nmi_vector(&mut self, size: RvSize, val: RvData) -> Result<(), BusError> {
+        if size != RvSize::Word {
+            return Err(BusError::StoreAccessFault);
+        }
+        self.timer
+            .schedule_action_in(0, TimerAction::SetNmiVec { addr: val });
         Ok(())
     }
 
@@ -881,7 +887,7 @@ mod tests {
         let args = CaliptraRootBusArgs::default();
         let args = CaliptraRootBusArgs { log_dir, ..args };
         let mut soc_reg: SocRegistersInternal =
-            SocRegistersInternal::new(&clock, mailbox.clone(), Iccm::new(), args);
+            SocRegistersInternal::new(&clock, mailbox.clone(), Iccm::new(&clock), args);
 
         soc_reg
             .write(RvSize::Word, CPTRA_DBG_MANUF_SERVICE_REG_START, 1)
@@ -948,7 +954,7 @@ mod tests {
         let args = CaliptraRootBusArgs::default();
         let args = CaliptraRootBusArgs { log_dir, ..args };
         let mut soc_reg: SocRegistersInternal =
-            SocRegistersInternal::new(&clock, mailbox.clone(), Iccm::new(), args);
+            SocRegistersInternal::new(&clock, mailbox.clone(), Iccm::new(&clock), args);
         soc_reg
             .write(RvSize::Word, CPTRA_DBG_MANUF_SERVICE_REG_START, 2)
             .unwrap();
@@ -1013,7 +1019,7 @@ mod tests {
             ..Default::default()
         };
         let mut soc_reg: SocRegistersInternal =
-            SocRegistersInternal::new(&clock, mailbox, Iccm::new(), args);
+            SocRegistersInternal::new(&clock, mailbox, Iccm::new(&clock), args);
 
         let _ = soc_reg.write(RvSize::Word, CPTRA_GENERIC_OUTPUT_WIRES_START, b'h'.into());
 


Test patch:
diff --git a/drivers/test-fw/Cargo.toml b/drivers/test-fw/Cargo.toml
index ea1be866..c0bb9b02 100644
--- a/drivers/test-fw/Cargo.toml
+++ b/drivers/test-fw/Cargo.toml
@@ -12,6 +12,7 @@ caliptra-registers = { path = "../../registers" }
 caliptra-kat = { path = "../../kat" }
 caliptra-test-harness = { path = "../../test-harness" }
 cfg-if = "1.0.0"
+zerocopy = "0.6.1"
 
 [features]
 emu = ["caliptra-test-harness/emu"]
diff --git a/drivers/test-fw/build.rs b/drivers/test-fw/build.rs
index d81c4361..325480e0 100644
--- a/drivers/test-fw/build.rs
+++ b/drivers/test-fw/build.rs
@@ -13,7 +13,6 @@ Abstract:
 --*/
 
 fn main() {
-    println!("cargo:rerun-if-changed=scripts/rom.ld");
-    println!("cargo:rerun-if-changed=src/bin/start.S");
-    println!("cargo:rustc-link-arg=-Tdrivers/test-fw/scripts/rom.ld");
+    println!("cargo:rerun-if-changed=../../test-harness/scripts/rom.ld");
+    println!("cargo:rustc-link-arg=-Ttest-harness/scripts/rom.ld");
 }
diff --git a/drivers/test-fw/src/bin/mailbox_tests.rs b/drivers/test-fw/src/bin/mailbox_tests.rs
index e1e5544b..8b6f08fa 100644
--- a/drivers/test-fw/src/bin/mailbox_tests.rs
+++ b/drivers/test-fw/src/bin/mailbox_tests.rs
@@ -19,6 +19,7 @@ use caliptra_drivers::Mailbox;
 use caliptra_registers::mbox::{self};
 use core::mem::size_of;
 use core::slice;
+use zerocopy::AsBytes;
 
 use caliptra_test_harness::test_suite;
 
@@ -82,7 +83,7 @@ fn test_mailbox_loopback() {
             4 * size_of::<u32>() - 1,
         )
     };
-    let mut request_received = [0u8; 128];
+    let mut request_received = [0u32; 32];
 
     let mut ii = 0;
     while ii < 2 {
@@ -99,7 +100,7 @@ fn test_mailbox_loopback() {
             if let Some(mut recv_txn) = Mailbox::default().try_start_recv_txn() {
                 assert_eq!(mbox.dlen().read(), recv_txn.dlen());
                 assert!(recv_txn.recv_request(&mut request_received[..]).is_ok());
-                assert_eq!(request, &request_received[..request.len()]);
+                assert_eq!(request, &request_received.as_bytes()[..request.len()]);
                 assert!(recv_txn.recv_request(&mut request_received[..]).is_err());
                 for nn in &mut request_received[0..request.len()] {
                     *nn = 42
diff --git a/hw-model/test-fw/Cargo.toml b/hw-model/test-fw/Cargo.toml
new file mode 100644
index 00000000..4af23671
--- /dev/null
+++ b/hw-model/test-fw/Cargo.toml
@@ -0,0 +1,24 @@
+# Licensed under the Apache-2.0 license
+
+[package]
+name = "caliptra-hw-model-test-fw"
+version = "0.1.0"
+edition = "2021"
+
+[features]
+default = []
+riscv = ["caliptra-test-harness/riscv"]
+emu = ["caliptra-test-harness/emu"]
+
+[dependencies]
+caliptra-test-harness = { path = "../../test-harness" }
+
+[[bin]]
+name = "test_iccm_unaligned_write"
+path = "test_iccm_unaligned_write.rs"
+required-features = ["riscv"]
+
+[[bin]]
+name = "test_iccm_byte_write"
+path = "test_iccm_byte_write.rs"
+required-features = ["riscv"]
\ No newline at end of file
diff --git a/hw-model/test-fw/build.rs b/hw-model/test-fw/build.rs
new file mode 100644
index 00000000..54da4a16
--- /dev/null
+++ b/hw-model/test-fw/build.rs
@@ -0,0 +1,6 @@
+// Licensed under the Apache-2.0 license
+
+fn main() {
+    println!("cargo:rerun-if-changed=../../test-harness/scripts/rom.ld");
+    println!("cargo:rustc-link-arg=-Ttest-harness/scripts/rom.ld");
+}
diff --git a/hw-model/test-fw/test_iccm_byte_write.rs b/hw-model/test-fw/test_iccm_byte_write.rs
new file mode 100644
index 00000000..203e2da2
--- /dev/null
+++ b/hw-model/test-fw/test_iccm_byte_write.rs
@@ -0,0 +1,30 @@
+// Licensed under the Apache-2.0 license
+
+#![no_main]
+#![no_std]
+
+use ::core::arch::global_asm;
+
+// Needed to bring in startup code
+#[allow(unused)]
+use caliptra_test_harness;
+
+#[panic_handler]
+pub fn panic(_info: &core::panic::PanicInfo) -> ! {
+    loop {}
+}
+
+global_asm!(
+    r#"
+main:
+    lui t0, 0x40000
+    sb x0, 0(t0)
+    nop
+    nop
+    nop
+    nop
+    nop
+    nop
+    ret
+"#
+);
diff --git a/hw-model/test-fw/test_iccm_unaligned_write.rs b/hw-model/test-fw/test_iccm_unaligned_write.rs
new file mode 100644
index 00000000..6c71e1a2
--- /dev/null
+++ b/hw-model/test-fw/test_iccm_unaligned_write.rs
@@ -0,0 +1,30 @@
+// Licensed under the Apache-2.0 license
+
+#![no_main]
+#![no_std]
+
+use ::core::arch::global_asm;
+
+// Needed to bring in startup code
+#[allow(unused)]
+use caliptra_test_harness;
+
+#[panic_handler]
+pub fn panic(_info: &core::panic::PanicInfo) -> ! {
+    loop {}
+}
+
+global_asm!(
+    r#"
+main:
+    lui t0, 0x40000
+    sw x0, 1(t0)
+    nop
+    nop
+    nop
+    nop
+    nop
+    nop
+    ret
+"#
+);
diff --git a/hw-model/tests/model_tests.rs b/hw-model/tests/model_tests.rs
new file mode 100644
index 00000000..0dbcacd7
--- /dev/null
+++ b/hw-model/tests/model_tests.rs
@@ -0,0 +1,76 @@
+// Licensed under the Apache-2.0 license
+
+use caliptra_builder::FwId;
+use caliptra_hw_model::{BootParams, DefaultHwModel, HwModel, InitParams};
+use caliptra_test_harness_types as harness;
+
+const BASE_FWID: FwId = FwId {
+    crate_name: "caliptra-hw-model-test-fw",
+    bin_name: "",
+    features: &["emu"],
+};
+
+fn run_fw_elf(elf: &[u8]) -> DefaultHwModel {
+    let rom = caliptra_builder::elf2rom(elf).unwrap();
+    let model = caliptra_hw_model::new(BootParams {
+        init_params: InitParams {
+            rom: &rom,
+            ..Default::default()
+        },
+        ..Default::default()
+    })
+    .unwrap();
+    model
+}
+
+#[test]
+fn test_iccm_byte_write_nmi_failure() {
+    let elf = caliptra_builder::build_firmware_elf(&FwId {
+        bin_name: "test_iccm_byte_write",
+        ..BASE_FWID
+    })
+    .unwrap();
+    let symbols = caliptra_builder::elf_symbols(&elf).unwrap();
+    let main_symbol = symbols.iter().find(|s| s.name == "main").unwrap();
+    let main_addr = main_symbol.value as u32;
+
+    let mut model = run_fw_elf(&elf);
+    model.step_until_exit_success().unwrap_err();
+
+    let soc_ifc: caliptra_registers::soc_ifc::RegisterBlock<_> = model.soc_ifc();
+    assert_eq!(
+        soc_ifc.cptra_fw_error_non_fatal().read(),
+        harness::ERROR_NMI
+    );
+    let nmi_info = harness::ExtErrorInfo::from(soc_ifc.cptra_fw_extended_error_info().read());
+
+    // Exactly where the PC is when the NMI fires is a bit fuzzy...
+    assert!(nmi_info.mepc >= main_addr + 4 && nmi_info.mepc <= main_addr + 12);
+    assert_eq!(nmi_info.mcause, harness::NMI_CAUSE_DBUS_STORE_ERROR);
+}
+
+#[test]
+fn test_iccm_unaligned_write_nmi_failure() {
+    let elf = caliptra_builder::build_firmware_elf(&FwId {
+        bin_name: "test_iccm_unaligned_write",
+        ..BASE_FWID
+    })
+    .unwrap();
+    let symbols = caliptra_builder::elf_symbols(&elf).unwrap();
+    let main_symbol = symbols.iter().find(|s| s.name == "main").unwrap();
+    let main_addr = main_symbol.value as u32;
+
+    let mut model = run_fw_elf(&elf);
+    model.step_until_exit_success().unwrap_err();
+
+    let soc_ifc: caliptra_registers::soc_ifc::RegisterBlock<_> = model.soc_ifc();
+    assert_eq!(
+        soc_ifc.cptra_fw_error_non_fatal().read(),
+        harness::ERROR_NMI
+    );
+    let ext_info = harness::ExtErrorInfo::from(soc_ifc.cptra_fw_extended_error_info().read());
+
+    // Exactly where the PC is when the NMI fires is a bit fuzzy...
+    assert!(ext_info.mepc >= main_addr + 4 && ext_info.mepc <= main_addr + 12);
+    assert_eq!(ext_info.mcause, harness::NMI_CAUSE_DBUS_STORE_ERROR);
+}
diff --git a/drivers/test-fw/scripts/rom.ld b/test-harness/scripts/rom.ld
similarity index 100%
rename from drivers/test-fw/scripts/rom.ld
rename to test-harness/scripts/rom.ld
diff --git a/test-harness/src/start.S b/test-harness/src/start.S
index c8363fd4..8d4a6d43 100644
--- a/test-harness/src/start.S
+++ b/test-harness/src/start.S
@@ -16,6 +16,13 @@ Abstract:
 .global _start
 _start:
 
+.equ CPTRA_FW_ERROR_NON_FATAL, 0x3003000c
+.equ CPTRA_FW_EXTENDED_ERROR_INFO, 0x30030018
+.equ CPTRA_INTERNAL_NMI_VECTOR, 0x3003062c
+
+.equ CPTRA_ERROR_EXCEPTION, 0x03000002
+.equ CPTRA_ERROR_NMI, 0x03000003
+
 .option push
 .option norelax
     la gp, __global_pointer$
@@ -28,6 +35,11 @@ _start:
     la t0, trap_vec
     csrw MTVEC, t0
 
+    # Setup NMI handler
+    li t0, CPTRA_INTERNAL_NMI_VECTOR
+    la t1, nmi_vec
+    sw t1, 0(t0)
+
     # Copy BSS
     la t0, BSS_START
     la t1, BSS_END
@@ -65,7 +77,39 @@ end_copy_data:
 1:
     j 1b
 
+.align 2
+nmi_vec:
+    li t0, CPTRA_FW_ERROR_NON_FATAL
+    li t1, CPTRA_ERROR_NMI
+    sw t1, 0(t0)
+    j exception_common
+
+.align 2
 trap_vec:
+    li t0, CPTRA_FW_ERROR_NON_FATAL
+    li t1, CPTRA_ERROR_EXCEPTION
+    sw t1, 0(t0)
+
+exception_common:
+    li t0, CPTRA_FW_EXTENDED_ERROR_INFO
+    sw sp, 0(t0)
+
+    csrr t1, mepc
+    sw t1, 4(t0)
+
+    csrr t1, mcause
+    sw t1, 8(t0)
+
+    // MSCAUSE = 0x7FF
+    csrr t1, 0x7ff
+    sw t1, 12(t0)
+
+    csrr t1, mstatus
+    sw t1, 16(t0)
+
+    csrr t1, mtval
+    sw t1, 20(t0)
+
     # exit the emulator with error
     la t0, EMU_CTRL_EXIT
     li t1, 0x01
@@ -73,5 +117,10 @@ trap_vec:
 1:
     j 1b
 
+    nop
+    nop
+    nop
+    nop
+
 .section .data
 .equ  EMU_CTRL_EXIT, 0x300300C8
diff --git a/test-harness/types/Cargo.toml b/test-harness/types/Cargo.toml
new file mode 100644
index 00000000..2106d2df
--- /dev/null
+++ b/test-harness/types/Cargo.toml
@@ -0,0 +1,10 @@
+# Licensed under the Apache-2.0 license
+
+[package]
+name = "caliptra-test-harness-types"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
diff --git a/test-harness/types/src/lib.rs b/test-harness/types/src/lib.rs
new file mode 100644
index 00000000..bb648780
--- /dev/null
+++ b/test-harness/types/src/lib.rs
@@ -0,0 +1,39 @@
+// Licensed under the Apache-2.0 license
+
+/// Errors codes placed into CPTRA_FW_ERROR_NON_FATAL
+/// by the test harness.
+pub const ERROR_EXCEPTION: u32 = 0x0300_0002;
+pub const ERROR_NMI: u32 = 0x0300_0003;
+
+// From RISC-V_VeeR_EL2_PRM.pdf
+pub const NMI_CAUSE_PIN_ASSERTION: u32 = 0x0000_0000;
+pub const NMI_CAUSE_DBUS_STORE_ERROR: u32 = 0xf000_0000;
+pub const NMI_CAUSE_DBUS_NON_BLOCKING_LOAD_ERROR: u32 = 0xf000_0001;
+pub const NMI_CAUSE_FAST_INTERRUPT_DOUBLE_BIT_ECC_ERROR: u32 = 0xf000_1000;
+pub const NMI_CAUSE_FAST_INTERRUPT_DCCM_REGION_ACCESS_ERROR: u32 = 0xf000_1001;
+pub const NMI_CAUSE_FAST_INTERRUPT_NON_DCCM_REGION: u32 = 0xf000_1002;
+
+/// Error info collected by the test-harness's trap/NMI handlers and placed into
+/// CPTRA_FW_EXTENDED_ERROR_INFO. See `test-harness/start.S` for more
+/// information.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct ExtErrorInfo {
+    pub sp: u32,
+    pub mepc: u32,
+    pub mcause: u32,
+    pub mscause: u32,
+    pub mstatus: u32,
+    pub mtval: u32,
+}
+impl From<[u32; 8]> for ExtErrorInfo {
+    fn from(value: [u32; 8]) -> Self {
+        ExtErrorInfo {
+            sp: value[0],
+            mepc: value[1],
+            mcause: value[2],
+            mscause: value[3],
+            mstatus: value[4],
+            mtval: value[5],
+        }
+    }
+}