Repository: chipsalliance/caliptra-sw
Pull request #237 (2023-05-04 09:23:47)

Separate internal/external buses for sw-emulator mailbox to support uC -> SoC transactions
-  Mailbox split into Soc and Caliptra side.
- State machine implements ExecSoc and ExecUc states and respective transition arcs.
- Implementation of unlock register.
- Integration tests 



Patch:
diff --git a/hw-model/src/lib.rs b/hw-model/src/lib.rs
index a75914a5..d78bcfb9 100644
--- a/hw-model/src/lib.rs
+++ b/hw-model/src/lib.rs
@@ -9,6 +9,8 @@ use std::{
 
 use caliptra_emu_bus::Bus;
 
+use caliptra_registers::mbox;
+use caliptra_registers::mbox::enums::{MboxFsmE, MboxStatusE};
 use rand::{rngs::StdRng, RngCore, SeedableRng};
 
 pub mod mmio;
@@ -28,6 +30,7 @@ pub use model_emulated::ModelEmulated;
 
 #[cfg(feature = "verilator")]
 pub use model_verilated::ModelVerilated;
+use ureg::Mmio;
 
 /// Ideally, general-purpose functions would return `impl HwModel` instead of
 /// `DefaultHwModel` to prevent users from calling functions that aren't
@@ -127,6 +130,7 @@ pub enum ModelError {
     ReadyForFirmwareTimeout { cycles: u32 },
     ProvidedIccmTooLarge,
     ProvidedDccmTooLarge,
+    UnexpectedMailboxFsmStatus { expected: u32, actual: u32 },
 }
 impl Error for ModelError {}
 impl Display for ModelError {
@@ -149,8 +153,104 @@ impl Display for ModelError {
             ),
             ModelError::ProvidedDccmTooLarge => write!(f, "Provided DCCM image too large"),
             ModelError::ProvidedIccmTooLarge => write!(f, "Provided ICCM image too large"),
+            ModelError::UnexpectedMailboxFsmStatus { expected, actual } => write!(
+                f,
+                "Expected mailbox FSM status to be {expected}, was {actual}"
+            ),
+        }
+    }
+}
+
+pub struct MailboxRequest {
+    pub cmd: u32,
+    pub data: Vec<u8>,
+}
+
+pub struct MailboxRecvTxn<'a, TModel: HwModel> {
+    model: &'a mut TModel,
+    pub req: MailboxRequest,
+}
+impl<'a, Model: HwModel> MailboxRecvTxn<'a, Model> {
+    pub fn respond_success(self) {
+        self.complete(MboxStatusE::CmdComplete);
+    }
+    pub fn respond_failure(self) {
+        self.complete(MboxStatusE::CmdFailure);
+    }
+    pub fn respond_with_data(self, data: &[u8]) -> Result<(), ModelError> {
+        let mbox = self.model.soc_mbox();
+        let mbox_fsm_ps = mbox.status().read().mbox_fsm_ps();
+        if !mbox_fsm_ps.mbox_execute_soc() {
+            return Err(ModelError::UnexpectedMailboxFsmStatus {
+                expected: MboxFsmE::MboxExecuteSoc as u32,
+                actual: mbox_fsm_ps as u32,
+            });
         }
+        mbox_write_fifo(&mbox, data)?;
+        drop(mbox);
+        self.complete(MboxStatusE::DataReady);
+        Ok(())
+    }
+
+    fn complete(self, status: MboxStatusE) {
+        self.model
+            .soc_mbox()
+            .status()
+            .write(|w| w.status(|_| status));
+        // mbox_fsm_ps isn't updated immediately after execute is cleared (!?),
+        // so step an extra clock cycle to wait for fm_ps to update
+        self.model.step();
+        assert!(self
+            .model
+            .soc_mbox()
+            .status()
+            .read()
+            .mbox_fsm_ps()
+            .mbox_execute_uc());
+    }
+}
+
+fn mbox_read_fifo(mbox: mbox::RegisterBlock<impl Mmio>) -> Vec<u8> {
+    let mut dlen = mbox.dlen().read();
+    let mut result = vec![];
+    while dlen >= 4 {
+        result.extend_from_slice(&mbox.dataout().read().to_le_bytes());
+        dlen -= 4;
+    }
+    if dlen > 0 {
+        // Unwrap cannot panic because dlen is less than 4
+        result.extend_from_slice(
+            &mbox.dataout().read().to_le_bytes()[..usize::try_from(dlen).unwrap()],
+        );
+    }
+    result
+}
+
+fn mbox_write_fifo(mbox: &mbox::RegisterBlock<impl Mmio>, buf: &[u8]) -> Result<(), ModelError> {
+    const MAILBOX_SIZE: u32 = 128 * 1024;
+
+    let Ok(input_len) = u32::try_from(buf.len()) else {
+        return Err(ModelError::BufferTooLargeForMailbox);
+    };
+    if input_len > MAILBOX_SIZE {
+        return Err(ModelError::BufferTooLargeForMailbox);
     }
+    mbox.dlen().write(|_| input_len);
+
+    let mut remaining = buf;
+    while remaining.len() >= 4 {
+        // Panic is impossible because the subslice is always 4 bytes
+        let word = u32::from_le_bytes(remaining[..4].try_into().unwrap());
+        mbox.datain().write(|_| word);
+        remaining = &remaining[4..];
+    }
+    if !remaining.is_empty() {
+        let mut word_bytes = [0u8; 4];
+        word_bytes[..remaining.len()].copy_from_slice(remaining);
+        let word = u32::from_le_bytes(word_bytes);
+        mbox.datain().write(|_| word);
+    }
+    Ok(())
 }
 
 /// Firmware Load Command Opcode
@@ -355,39 +455,19 @@ pub trait HwModel {
         cmd: u32,
         buf: &[u8],
     ) -> std::result::Result<Option<Vec<u8>>, ModelError> {
-        const MAILBOX_SIZE: u32 = 128 * 1024;
-
         if self.soc_mbox().lock().read().lock() {
             return Err(ModelError::UnableToLockMailbox);
         }
-        let Ok(input_len) = u32::try_from(buf.len()) else {
-            return Err(ModelError::BufferTooLargeForMailbox);
-        };
-        if input_len > MAILBOX_SIZE {
-            return Err(ModelError::BufferTooLargeForMailbox);
-        }
-
-        self.soc_mbox().cmd().write(|_| cmd);
-        self.soc_mbox().dlen().write(|_| input_len);
 
         writeln!(
             self.output().logger(),
-            "<<< Executing mbox cmd 0x{cmd:08x} ({input_len} bytes) from SoC"
+            "<<< Executing mbox cmd 0x{cmd:08x} ({} bytes) from SoC",
+            buf.len(),
         )
         .unwrap();
-        let mut remaining = buf;
-        while remaining.len() >= 4 {
-            // Panic is impossible because the subslice is always 4 bytes
-            let word = u32::from_le_bytes(remaining[..4].try_into().unwrap());
-            self.soc_mbox().datain().write(|_| word);
-            remaining = &remaining[4..];
-        }
-        if !remaining.is_empty() {
-            let mut word_bytes = [0u8; 4];
-            word_bytes[..remaining.len()].copy_from_slice(remaining);
-            let word = u32::from_le_bytes(word_bytes);
-            self.soc_mbox().datain().write(|_| word);
-        }
+
+        self.soc_mbox().cmd().write(|_| cmd);
+        mbox_write_fifo(&self.soc_mbox(), buf)?;
 
         // Ask the microcontroller to execute this command
         self.soc_mbox().execute().write(|w| w.execute(true));
@@ -411,24 +491,13 @@ pub trait HwModel {
             return Err(ModelError::UnknownCommandStatus(status as u32));
         }
 
-        let mut result = vec![];
-        let mut dlen = self.soc_mbox().dlen().read();
-
+        let dlen = self.soc_mbox().dlen().read();
         writeln!(
             self.output().logger(),
             ">>> mbox cmd response data ({dlen} bytes)"
         )
         .unwrap();
-        while dlen >= 4 {
-            result.extend_from_slice(&self.soc_mbox().dataout().read().to_le_bytes());
-            dlen -= 4;
-        }
-        if dlen > 0 {
-            // Unwrap cannot panic because dlen is less than 4
-            result.extend_from_slice(
-                &self.soc_mbox().dataout().read().to_le_bytes()[..usize::try_from(dlen).unwrap()],
-            );
-        }
+        let result = mbox_read_fifo(self.soc_mbox());
 
         self.soc_mbox().execute().write(|w| w.execute(false));
         // mbox_fsm_ps isn't updated immediately after execute is cleared (!?),
@@ -446,6 +515,40 @@ pub trait HwModel {
         }
         Ok(())
     }
+
+    fn wait_for_mailbox_receive(&mut self) -> Result<MailboxRecvTxn<Self>, ModelError>
+    where
+        Self: Sized,
+    {
+        loop {
+            if let Some(txn) = self.try_mailbox_receive()? {
+                let req = txn.req;
+                return Ok(MailboxRecvTxn { model: self, req });
+            }
+        }
+    }
+
+    fn try_mailbox_receive(&mut self) -> Result<Option<MailboxRecvTxn<Self>>, ModelError>
+    where
+        Self: Sized,
+    {
+        if !self
+            .soc_mbox()
+            .status()
+            .read()
+            .mbox_fsm_ps()
+            .mbox_execute_soc()
+        {
+            self.step();
+            return Ok(None);
+        }
+        let cmd = self.soc_mbox().cmd().read();
+        let data = mbox_read_fifo(self.soc_mbox());
+        Ok(Some(MailboxRecvTxn {
+            model: self,
+            req: MailboxRequest { cmd, data },
+        }))
+    }
 }
 
 #[cfg(test)]
@@ -454,7 +557,7 @@ mod tests {
     use caliptra_builder::FwId;
     use caliptra_emu_bus::Bus;
     use caliptra_emu_types::RvSize;
-    use caliptra_registers::soc_ifc;
+    use caliptra_registers::{mbox::enums::MboxStatusE, soc_ifc};
 
     use crate as caliptra_hw_model;
 
@@ -615,4 +718,51 @@ mod tests {
             Err(ModelError::MailboxCmdFailed)
         );
     }
+
+    #[test]
+    pub fn test_mailbox_receive() {
+        let rom = caliptra_builder::build_firmware_rom(&FwId {
+            crate_name: "caliptra-hw-model-test-fw",
+            bin_name: "mailbox_sender",
+            features: &["emu"],
+        })
+        .unwrap();
+
+        let mut model = caliptra_hw_model::new(BootParams {
+            init_params: InitParams {
+                rom: &rom,
+                ..Default::default()
+            },
+            ..Default::default()
+        })
+        .unwrap();
+
+        // Test 8-byte request, respond-with-success
+        let txn = model.wait_for_mailbox_receive().unwrap();
+        assert_eq!(txn.req.cmd, 0xe000_0000);
+        assert_eq!(
+            txn.req.data,
+            [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]
+        );
+        txn.respond_success();
+        model.step_until(|m| m.soc_ifc().cptra_fw_extended_error_info().at(0).read() != 0);
+        assert_eq!(
+            &model.soc_ifc().cptra_fw_extended_error_info().read()[..2],
+            &[MboxStatusE::CmdComplete as u32, 8]
+        );
+
+        // Test 3-byte request, respond with failure
+        let txn = model.wait_for_mailbox_receive().unwrap();
+        assert_eq!(txn.req.cmd, 0xe000_1000);
+        assert_eq!(txn.req.data, [0xdd, 0xcc, 0xbb]);
+        txn.respond_failure();
+        model.step_until(|m| m.soc_ifc().cptra_fw_extended_error_info().at(0).read() != 0);
+        assert_eq!(
+            &model.soc_ifc().cptra_fw_extended_error_info().read()[..2],
+            &[MboxStatusE::CmdFailure as u32, 3]
+        );
+
+        // TODO: Add test for txn.respond_with_data (this doesn't work yet due
+        // to https://github.com/chipsalliance/caliptra-rtl/issues/78)
+    }
 }
diff --git a/sw-emulator/app/src/main.rs b/sw-emulator/app/src/main.rs
index 0d99694c..8d7e6919 100644
--- a/sw-emulator/app/src/main.rs
+++ b/sw-emulator/app/src/main.rs
@@ -15,7 +15,8 @@ Abstract:
 use caliptra_emu_bus::Clock;
 use caliptra_emu_cpu::{Cpu, RvInstr, StepAction};
 use caliptra_emu_periph::{
-    CaliptraRootBus, CaliptraRootBusArgs, Mailbox, ReadyForFwCb, TbServicesCb, UploadUpdateFwCb,
+    CaliptraRootBus, CaliptraRootBusArgs, MailboxInternal, ReadyForFwCb, TbServicesCb,
+    UploadUpdateFwCb,
 };
 use caliptra_hw_model::BusMmio;
 use caliptra_hw_model_types::{DeviceLifecycle, SecurityState};
@@ -249,12 +250,12 @@ fn main() -> io::Result<()> {
             while !args.mailbox.try_acquire_lock() {}
 
             let firmware_buffer = current_fw_buf.clone();
-            args.schedule_later(FW_WRITE_TICKS, move |mailbox: &mut Mailbox| {
+            args.schedule_later(FW_WRITE_TICKS, move |mailbox: &mut MailboxInternal| {
                 upload_fw_to_mailbox(mailbox, firmware_buffer);
             });
         }),
         security_state,
-        upload_update_fw: UploadUpdateFwCb::new(move |mailbox: &mut Mailbox| {
+        upload_update_fw: UploadUpdateFwCb::new(move |mailbox: &mut MailboxInternal| {
             while !mailbox.try_acquire_lock() {}
             upload_fw_to_mailbox(mailbox, update_fw_buf.clone());
         }),
@@ -374,7 +375,7 @@ fn change_dword_endianess(data: &mut Vec<u8>) {
     }
 }
 
-fn upload_fw_to_mailbox(mailbox: &mut Mailbox, firmware_buffer: Rc<Vec<u8>>) {
+fn upload_fw_to_mailbox(mailbox: &mut MailboxInternal, firmware_buffer: Rc<Vec<u8>>) {
     // Write the cmd to mailbox.
     let _ = mailbox.write_cmd(FW_LOAD_CMD_OPCODE);
 
diff --git a/sw-emulator/lib/periph/src/doe.rs b/sw-emulator/lib/periph/src/doe.rs
index 8758051f..80b756c5 100644
--- a/sw-emulator/lib/periph/src/doe.rs
+++ b/sw-emulator/lib/periph/src/doe.rs
@@ -211,7 +211,7 @@ impl Doe {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{CaliptraRootBusArgs, Iccm, KeyUsage, Mailbox, MailboxRam};
+    use crate::{CaliptraRootBusArgs, Iccm, KeyUsage, MailboxInternal, MailboxRam};
     use caliptra_emu_bus::Bus;
     use caliptra_emu_crypto::EndianessTransform;
     use caliptra_emu_types::RvAddr;
@@ -248,7 +248,7 @@ mod tests {
         let key_vault = KeyVault::new();
         let soc_reg = SocRegistersInternal::new(
             &clock,
-            Mailbox::new(MailboxRam::new()),
+            MailboxInternal::new(MailboxRam::new()),
             Iccm::new(&clock),
             CaliptraRootBusArgs::default(),
         );
@@ -307,7 +307,7 @@ mod tests {
         let key_vault = KeyVault::new();
         let soc_reg = SocRegistersInternal::new(
             &clock,
-            Mailbox::new(MailboxRam::new()),
+            MailboxInternal::new(MailboxRam::new()),
             Iccm::new(&clock),
             CaliptraRootBusArgs::default(),
         );
@@ -364,7 +364,7 @@ mod tests {
         let key_vault = KeyVault::new();
         let soc_reg = SocRegistersInternal::new(
             &clock,
-            Mailbox::new(MailboxRam::new()),
+            MailboxInternal::new(MailboxRam::new()),
             Iccm::new(&clock),
             CaliptraRootBusArgs::default(),
         );
diff --git a/sw-emulator/lib/periph/src/lib.rs b/sw-emulator/lib/periph/src/lib.rs
index 36df0ea2..e5f1c74d 100644
--- a/sw-emulator/lib/periph/src/lib.rs
+++ b/sw-emulator/lib/periph/src/lib.rs
@@ -38,7 +38,7 @@ pub use hmac_sha384::HmacSha384;
 pub use iccm::Iccm;
 pub use key_vault::KeyUsage;
 pub use key_vault::KeyVault;
-pub use mailbox::{Mailbox, MailboxRam};
+pub use mailbox::{MailboxExternal, MailboxInternal, MailboxRam};
 pub use root_bus::{
     ActionCb, CaliptraRootBus, CaliptraRootBusArgs, ReadyForFwCb, SocToCaliptraBus, TbServicesCb,
     UploadUpdateFwCb,
diff --git a/sw-emulator/lib/periph/src/mailbox.rs b/sw-emulator/lib/periph/src/mailbox.rs
index 0fd51d3a..0fb3cef5 100644
--- a/sw-emulator/lib/periph/src/mailbox.rs
+++ b/sw-emulator/lib/periph/src/mailbox.rs
@@ -24,6 +24,14 @@ use tock_registers::{register_bitfields, LocalRegisterCopy};
 
 /// Maximum mailbox capacity.
 const MAX_MAILBOX_CAPACITY_BYTES: usize = 128 << 10;
+const OFFSET_LOCK: RvAddr = 0x00;
+const OFFSET_CMD: RvAddr = 0x08;
+const OFFSET_DLEN: RvAddr = 0x0C;
+const OFFSET_DATAIN: RvAddr = 0x10;
+const OFFSET_DATAOUT: RvAddr = 0x14;
+const OFFSET_EXECUTE: RvAddr = 0x18;
+const OFFSET_STATUS: RvAddr = 0x1C;
+const OFFSET_UNLOCK: RvAddr = 0x20;
 
 register_bitfields! [
     u32,
@@ -52,6 +60,8 @@ register_bitfields! [
 
 ];
 
+type StatusRegister = LocalRegisterCopy<u32, Status::Register>;
+
 #[derive(Clone)]
 pub struct MailboxRam {
     ram: Rc<RefCell<Ram>>,
@@ -87,122 +97,199 @@ impl Default for MailboxRam {
 }
 
 #[derive(Clone)]
-pub struct Mailbox {
+pub struct MailboxExternal {
     regs: Rc<RefCell<MailboxRegs>>,
 }
 
-impl Mailbox {
-    const OFFSET_LOCK: RvAddr = 0x00;
-    const OFFSET_CMD: RvAddr = 0x08;
-    const OFFSET_DLEN: RvAddr = 0x0C;
-    const OFFSET_DATAIN: RvAddr = 0x10;
-    const OFFSET_DATAOUT: RvAddr = 0x14;
-    const OFFSET_EXECUTE: RvAddr = 0x18;
-    const OFFSET_STATUS: RvAddr = 0x1C;
+impl Bus for MailboxExternal {
+    /// Read data of specified size from given address
+    fn read(&mut self, size: RvSize, addr: RvAddr) -> Result<RvData, BusError> {
+        let mut regs = self.regs.borrow_mut();
+        regs.set_request(MailboxRequester::Soc);
+        let result = regs.read(size, addr);
+        regs.set_request(MailboxRequester::Caliptra);
+        result
+    }
+
+    /// Write data of specified size to given address
+    fn write(&mut self, size: RvSize, addr: RvAddr, val: RvData) -> Result<(), BusError> {
+        let mut regs = self.regs.borrow_mut();
+        regs.set_request(MailboxRequester::Soc);
+        let result = regs.write(size, addr, val);
+        regs.set_request(MailboxRequester::Caliptra);
+        result
+    }
+}
 
+#[derive(Clone)]
+pub struct MailboxInternal {
+    regs: Rc<RefCell<MailboxRegs>>,
+}
+
+/// Mailbox Peripheral
+
+impl MailboxInternal {
     pub fn new(ram: MailboxRam) -> Self {
         Self {
             regs: Rc::new(RefCell::new(MailboxRegs::new(ram))),
         }
     }
 
+    pub fn as_external(&self) -> MailboxExternal {
+        MailboxExternal {
+            regs: self.regs.clone(),
+        }
+    }
+
+    // Release the lock by writing to the  unlock register.
+    /// Only the uC can release the lock.
+    pub fn try_force_unlock(&mut self) -> Result<(), BusError> {
+        self.regs.borrow_mut().write(RvSize::Word, OFFSET_UNLOCK, 1)
+    }
+
     pub fn read_dlen(&mut self) -> Result<u32, BusError> {
-        self.regs.borrow_mut().read(RvSize::Word, Self::OFFSET_DLEN)
+        self.regs
+            .borrow_mut()
+            .set_request(MailboxRequester::Caliptra);
+        self.regs.borrow_mut().read(RvSize::Word, OFFSET_DLEN)
     }
 
     pub fn write_dlen(&mut self, dlen: u32) -> Result<(), BusError> {
         self.regs
             .borrow_mut()
-            .write(RvSize::Word, Self::OFFSET_DLEN, dlen)
+            .set_request(MailboxRequester::Caliptra);
+        self.regs
+            .borrow_mut()
+            .write(RvSize::Word, OFFSET_DLEN, dlen)
     }
 
     pub fn read_cmd(&mut self) -> Result<u32, BusError> {
-        self.regs.borrow_mut().read(RvSize::Word, Self::OFFSET_CMD)
+        self.regs
+            .borrow_mut()
+            .set_request(MailboxRequester::Caliptra);
+        self.regs.borrow_mut().read(RvSize::Word, OFFSET_CMD)
     }
 
     pub fn write_cmd(&mut self, cmd: u32) -> Result<(), BusError> {
         self.regs
             .borrow_mut()
-            .write(RvSize::Word, Self::OFFSET_CMD, cmd)
+            .set_request(MailboxRequester::Caliptra);
+        self.regs.borrow_mut().write(RvSize::Word, OFFSET_CMD, cmd)
     }
 
     pub fn read_datain(&mut self) -> Result<u32, BusError> {
         self.regs
             .borrow_mut()
-            .read(RvSize::Word, Self::OFFSET_DATAIN)
+            .set_request(MailboxRequester::Caliptra);
+        self.regs.borrow_mut().read(RvSize::Word, OFFSET_DATAIN)
     }
 
     pub fn write_datain(&mut self, val: u32) -> Result<(), BusError> {
         self.regs
             .borrow_mut()
-            .write(RvSize::Word, Self::OFFSET_DATAIN, val)
+            .set_request(MailboxRequester::Caliptra);
+        self.regs
+            .borrow_mut()
+            .write(RvSize::Word, OFFSET_DATAIN, val)
     }
 
     pub fn read_dataout(&mut self) -> Result<u32, BusError> {
         self.regs
             .borrow_mut()
-            .read(RvSize::Word, Self::OFFSET_DATAOUT)
+            .set_request(MailboxRequester::Caliptra);
+        self.regs.borrow_mut().read(RvSize::Word, OFFSET_DATAOUT)
     }
 
     pub fn write_dataout(&mut self, val: u32) -> Result<(), BusError> {
         self.regs
             .borrow_mut()
-            .write(RvSize::Word, Self::OFFSET_DATAOUT, val)
+            .set_request(MailboxRequester::Caliptra);
+        self.regs
+            .borrow_mut()
+            .write(RvSize::Word, OFFSET_DATAOUT, val)
     }
 
     pub fn try_acquire_lock(&mut self) -> bool {
-        let result = self.regs.borrow_mut().read(RvSize::Word, Self::OFFSET_LOCK);
+        self.regs
+            .borrow_mut()
+            .set_request(MailboxRequester::Caliptra);
+        let result = self.regs.borrow_mut().read(RvSize::Word, OFFSET_LOCK);
         matches!(result, Ok(0))
     }
 
     pub fn is_locked(&mut self) -> bool {
+        self.regs
+            .borrow_mut()
+            .set_request(MailboxRequester::Caliptra);
         self.regs.borrow().state_machine.context().locked == 1
     }
 
     pub fn read_execute(&mut self) -> Result<u32, BusError> {
         self.regs
             .borrow_mut()
-            .read(RvSize::Word, Self::OFFSET_EXECUTE)
+            .set_request(MailboxRequester::Caliptra);
+        self.regs.borrow_mut().read(RvSize::Word, OFFSET_EXECUTE)
     }
 
     pub fn write_execute(&mut self, val: u32) -> Result<(), BusError> {
         self.regs
             .borrow_mut()
-            .write(RvSize::Word, Self::OFFSET_EXECUTE, val)?;
+            .set_request(MailboxRequester::Caliptra);
+        self.regs
+            .borrow_mut()
+            .write(RvSize::Word, OFFSET_EXECUTE, val)?;
         Ok(())
     }
 
-    pub fn is_command_exec_requested(&self) -> bool {
-        matches!(self.regs.borrow_mut().state_machine.state, States::Exec)
+    pub fn is_command_exec_set_requested(&self) -> bool {
+        self.regs
+            .borrow_mut()
+            .set_request(MailboxRequester::Caliptra);
+        matches!(self.regs.borrow_mut().state_machine.state, States::ExecUc)
     }
 
     pub fn is_status_cmd_busy(&mut self) -> bool {
+        self.regs
+            .borrow_mut()
+            .set_request(MailboxRequester::Caliptra);
         self.match_status(Status::STATUS::CMD_BUSY.value)
     }
 
     pub fn is_status_data_ready(&mut self) -> bool {
+        self.regs
+            .borrow_mut()
+            .set_request(MailboxRequester::Caliptra);
         self.match_status(Status::STATUS::DATA_READY.value)
     }
 
     pub fn is_status_cmd_complete(&mut self) -> bool {
+        self.regs
+            .borrow_mut()
+            .set_request(MailboxRequester::Caliptra);
         self.match_status(Status::STATUS::CMD_COMPLETE.value)
     }
 
     fn match_status(&mut self, status: u32) -> bool {
+        self.regs
+            .borrow_mut()
+            .set_request(MailboxRequester::Caliptra);
         let val = self
             .regs
             .borrow_mut()
-            .read(RvSize::Word, Self::OFFSET_STATUS)
+            .read(RvSize::Word, OFFSET_STATUS)
             .unwrap();
         let reg = InMemoryRegister::<u32, Status::Register>::new(val);
         reg.read(Status::STATUS) == status
     }
 
     fn set_status(&mut self, status_in: u32) -> Result<(), BusError> {
+        self.regs
+            .borrow_mut()
+            .set_request(MailboxRequester::Caliptra);
         let status = self
             .regs
             .borrow_mut()
-            .read(RvSize::Word, Self::OFFSET_STATUS)
+            .read(RvSize::Word, OFFSET_STATUS)
             .unwrap();
 
         let status_reg: ReadWriteRegister<u32, Status::Register> = ReadWriteRegister::new(status);
@@ -210,30 +297,57 @@ impl Mailbox {
 
         self.regs
             .borrow_mut()
-            .write(RvSize::Word, Self::OFFSET_STATUS, status_reg.reg.get())?;
+            .write(RvSize::Word, OFFSET_STATUS, status_reg.reg.get())?;
         Ok(())
     }
 
     pub fn set_status_cmd_complete(&mut self) -> Result<(), BusError> {
+        self.regs
+            .borrow_mut()
+            .set_request(MailboxRequester::Caliptra);
         self.set_status(Status::STATUS::CMD_COMPLETE.value)
     }
 
     pub fn set_status_data_ready(&mut self) -> Result<(), BusError> {
+        self.regs
+            .borrow_mut()
+            .set_request(MailboxRequester::Caliptra);
         self.set_status(Status::STATUS::DATA_READY.value)
     }
 }
 
-impl Bus for Mailbox {
+impl Bus for MailboxInternal {
     /// Read data of specified size from given address
     fn read(&mut self, size: RvSize, addr: RvAddr) -> Result<RvData, BusError> {
+        self.regs
+            .borrow_mut()
+            .set_request(MailboxRequester::Caliptra);
         self.regs.borrow_mut().read(size, addr)
     }
 
     /// Write data of specified size to given address
     fn write(&mut self, size: RvSize, addr: RvAddr, val: RvData) -> Result<(), BusError> {
+        self.regs
+            .borrow_mut()
+            .set_request(MailboxRequester::Caliptra);
         self.regs.borrow_mut().write(size, addr, val)
     }
 }
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+
+pub enum MailboxRequester {
+    Caliptra = 0,
+    Soc = 1,
+}
+
+impl From<MailboxRequester> for u32 {
+    fn from(val: MailboxRequester) -> Self {
+        match val {
+            MailboxRequester::Caliptra => 0,
+            MailboxRequester::Soc => 1,
+        }
+    }
+}
 
 /// Mailbox Peripheral
 #[derive(Bus)]
@@ -270,8 +384,14 @@ pub struct MailboxRegs {
     #[register(offset = 0x0000_001c, write_fn = write_status, read_fn = read_status)]
     _status: ReadWriteRegister<u32>,
 
+    /// MBOX_UNLOCK register
+    #[register(offset = 0x0000_0020, write_fn = write_unlock, read_fn = read_unlock)]
+    _unlock: ReadWriteRegister<u32>,
+
     /// State Machine
     state_machine: StateMachine<Context>,
+
+    pub requester: MailboxRequester,
 }
 
 impl MailboxRegs {
@@ -284,6 +404,7 @@ impl MailboxRegs {
     const DATA_OUT_VAL: RvData = 0x0;
     const EXEC_VAL: RvData = 0x0;
     const STATUS_VAL: RvData = 0x0;
+    const UNLOCK_VAL: RvData = 0x0;
 
     /// Create a new instance of Mailbox registers
     pub fn new(ram: MailboxRam) -> Self {
@@ -296,15 +417,20 @@ impl MailboxRegs {
             data_out: ReadOnlyRegister::new(Self::DATA_OUT_VAL),
             execute: ReadWriteRegister::new(Self::EXEC_VAL),
             _status: ReadWriteRegister::new(Self::STATUS_VAL),
+            _unlock: ReadWriteRegister::new(Self::UNLOCK_VAL),
             state_machine: StateMachine::new(Context::new(ram)),
+            requester: MailboxRequester::Caliptra,
         }
     }
+    pub fn set_request(&mut self, requester: MailboxRequester) {
+        self.requester = requester;
+    }
 
     // Todo: Implement read_lock callback fn
     pub fn read_lock(&mut self, _size: RvSize) -> Result<u32, BusError> {
         if self
             .state_machine
-            .process_event(Events::RdLock(Owner(0)))
+            .process_event(Events::RdLock(self.requester))
             .is_ok()
         {
             Ok(0)
@@ -314,7 +440,7 @@ impl MailboxRegs {
     }
 
     // Todo: Implement read_user callback fn
-    pub fn read_user(&self, _size: RvSize) -> Result<u32, BusError> {
+    pub fn read_user(&self, _size: RvSize) -> Result<MailboxRequester, BusError> {
         Ok(self.state_machine.context.user)
     }
 
@@ -357,19 +483,37 @@ impl MailboxRegs {
         Ok(mb.context.data_out)
     }
 
-    // Todo: Implement write ex callback fn
+    /// Write to execute register
     pub fn write_ex(&mut self, _size: RvSize, val: RvData) -> Result<(), BusError> {
-        let _ = self.state_machine.process_event(if val & 1 != 0 {
-            Events::ExecSet
-        } else {
-            Events::ExecClear
-        });
+        let event = {
+            match self.requester {
+                MailboxRequester::Caliptra => {
+                    if val & 1 != 0 {
+                        Events::UcExecSet
+                    } else {
+                        Events::UcExecClear
+                    }
+                }
+                _ => {
+                    if val & 1 != 0 {
+                        Events::SocExecSet
+                    } else {
+                        Events::SocExecClear
+                    }
+                }
+            }
+        };
+
+        let _ = self.state_machine.process_event(event);
         self.execute.reg.set(val);
         Ok(())
     }
 
     // Todo: Implement write status callback fn
     pub fn write_status(&mut self, _size: RvSize, val: RvData) -> Result<(), BusError> {
+        // Send event to state machine.
+        let _ = self.state_machine.process_event(Events::SetStatus);
+
         let val = LocalRegisterCopy::<u32, Status::Register>::new(val);
         self.state_machine
             .context
@@ -383,7 +527,8 @@ impl MailboxRegs {
         let mut result = self.state_machine.context.status;
         result.modify(match self.state_machine.state {
             // TODO: What about MBOX_EXECUTE_SOC?
-            States::Exec => Status::MBOX_FSM_PS::MBOX_EXECUTE_UC,
+            States::ExecUc => Status::MBOX_FSM_PS::MBOX_EXECUTE_UC,
+            States::ExecSoc => Status::MBOX_FSM_PS::MBOX_EXECUTE_SOC,
             States::Idle => Status::MBOX_FSM_PS::MBOX_IDLE,
             States::RdyForCmd => Status::MBOX_FSM_PS::MBOX_RDY_FOR_CMD,
             States::RdyForData => Status::MBOX_FSM_PS::MBOX_RDY_FOR_DATA,
@@ -391,6 +536,15 @@ impl MailboxRegs {
         });
         Ok(result.get())
     }
+
+    pub fn write_unlock(&mut self, _size: RvSize, _val: RvData) -> Result<(), BusError> {
+        let _ = self.state_machine.process_event(Events::WrUnlock);
+        Ok(())
+    }
+
+    pub fn read_unlock(&self, _size: RvSize) -> Result<u32, BusError> {
+        Ok(self.state_machine.context.unlock)
+    }
 }
 
 #[derive(PartialEq)]
@@ -412,15 +566,43 @@ pub struct Cmd(pub u32);
 statemachine! {
     transitions: {
         // CurrentState Event [guard] / action = NextState
-        *Idle + RdLock(Owner) [is_not_locked] / lock = RdyForCmd,
+
+        //move from idle to rdy for command when lock is acquired.
+        *Idle + RdLock(MailboxRequester) [is_not_locked] / lock = RdyForCmd,
+
+        //move from rdy for cmd to rdy for dlen when cmd is written to.
         RdyForCmd  + CmdWrite(Cmd) / set_cmd = RdyForDlen,
+        RdyForCmd + WrUnlock  / unlock_and_reset = Idle,
+
+        //move from rdy for dlen to rdy for data when dlen is written to.
         RdyForDlen + DlenWrite(DataLength) / init_dlen = RdyForData,
+        RdyForDlen + WrUnlock = Idle,
+
         RdyForData + DataWrite(DataIn) / enqueue = RdyForData,
-        RdyForData + ExecSet = Exec,
-        Exec + DataRead / dequeue = Exec,
-        Exec + DlenWrite(DataLength) / init_dlen = Exec,
-        Exec + DataWrite(DataIn) / enqueue = Exec,
-        Exec + ExecClear [is_locked] / unlock = Idle
+        RdyForData + WrUnlock  / unlock_and_reset = Idle,
+
+        //move from rdy for data to execute uc  when soc sets execute bit.
+        RdyForData + SocExecSet = ExecUc,
+
+        //move from rdy for data to execute soc when soc sets execute bit.
+        RdyForData + UcExecSet = ExecSoc,
+
+        ExecUc + DataRead / dequeue = ExecUc,
+        ExecUc + DlenWrite(DataLength) / init_dlen = ExecUc,
+        ExecUc + DataWrite(DataIn) / enqueue = ExecUc,
+        ExecUc + SocExecClear [is_locked] / unlock = Idle,
+        ExecUc + UcExecClear [is_locked] / unlock = Idle,
+        ExecUc + SetStatus = ExecSoc,
+        ExecUc + WrUnlock  / unlock_and_reset = Idle,
+
+        ExecSoc + DataRead / dequeue = ExecSoc,
+        ExecSoc + DlenWrite(DataLength) / init_dlen = ExecSoc,
+        ExecSoc + DataWrite(DataIn) / enqueue = ExecSoc,
+        ExecSoc + UcExecClear [is_locked] / unlock = Idle,
+        ExecSoc + SocExecClear [is_locked] / unlock = Idle,
+        ExecSoc + SetStatus = ExecUc,
+        ExecSoc + WrUnlock  / unlock_and_reset = Idle,
+
     }
 }
 
@@ -429,39 +611,42 @@ pub struct Context {
     /// lock state
     pub locked: u32,
     /// Who acquired the lock.
-    pub user: u32,
+    pub user: MailboxRequester,
     /// Execute flag
     pub exec: bool,
     /// number of data elements
     pub dlen: u32,
     /// Fifo storage
-    pub ring_buffer: RingBuffer,
+    pub fifo: Fifo,
     /// Mailbox Status
-    status: LocalRegisterCopy<u32, Status::Register>,
+    status: StatusRegister,
     /// Command
     pub cmd: u32,
     // data_out
     data_out: u32,
+    // unlock
+    pub unlock: u32,
 }
 
 impl Context {
     fn new(ram: MailboxRam) -> Self {
         Self {
             locked: 0,
-            user: 0,
+            user: MailboxRequester::Caliptra,
             exec: false,
             dlen: 0,
             status: LocalRegisterCopy::new(0),
-            ring_buffer: RingBuffer::new(ram),
+            fifo: Fifo::new(ram),
             cmd: 0,
             data_out: 0,
+            unlock: 0,
         }
     }
 }
 
 impl StateMachineContext for Context {
     // guards
-    fn is_not_locked(&mut self, _user: &Owner) -> Result<(), ()> {
+    fn is_not_locked(&mut self, _user: &MailboxRequester) -> Result<(), ()> {
         if self.locked == 1 {
             // no transition
             Err(())
@@ -469,6 +654,7 @@ impl StateMachineContext for Context {
             Ok(())
         }
     }
+
     fn is_locked(&mut self) -> Result<(), ()> {
         if self.locked != 0 {
             Ok(())
@@ -477,22 +663,33 @@ impl StateMachineContext for Context {
             Err(())
         }
     }
+
+    //fn is_not_busy(&mut self, event_data: &StatusRegister) -> Result<(), ()> {
+    //    let status = event_data.read(Status::STATUS);
+    //    if status != Status::STATUS::CMD_BUSY.value {
+    //        Ok(())
+    //    } else {
+    // no transition
+    //        Err(())
+    //    }
+    //}
+
     // actions
     fn init_dlen(&mut self, data_len: &DataLength) {
-        self.ring_buffer.reset();
+        self.fifo.reset();
         self.dlen = data_len.0;
 
-        self.ring_buffer.latch_dlen(self.dlen as usize);
+        self.fifo.latch_dlen(self.dlen as usize);
     }
 
     fn set_cmd(&mut self, cmd: &Cmd) {
         self.cmd = cmd.0;
     }
 
-    fn lock(&mut self, user: &Owner) {
-        self.ring_buffer.reset();
+    fn lock(&mut self, user: &MailboxRequester) {
+        self.fifo.reset();
         self.locked = 1;
-        self.user = user.0;
+        self.user = *user;
     }
     fn unlock(&mut self) {
         self.locked = 0;
@@ -500,16 +697,20 @@ impl StateMachineContext for Context {
         self.status.set(0);
     }
     fn dequeue(&mut self) {
-        if let Ok(data_out) = self.ring_buffer.dequeue() {
+        if let Ok(data_out) = self.fifo.dequeue() {
             self.data_out = data_out;
         }
     }
     fn enqueue(&mut self, data_in: &DataIn) {
-        self.ring_buffer.enqueue(data_in.0);
+        self.fifo.enqueue(data_in.0);
+    }
+    fn unlock_and_reset(&mut self) {
+        self.unlock();
+        self.fifo.reset();
     }
 }
 
-pub struct RingBuffer {
+pub struct Fifo {
     latched_dlen: u32,
     capacity: usize,
     read_index: usize,
@@ -517,10 +718,10 @@ pub struct RingBuffer {
     mailbox_ram: MailboxRam,
 }
 
-impl RingBuffer {
+impl Fifo {
     pub fn new(ram: MailboxRam) -> Self {
         let ram_size = ram.ram.borrow().data().len();
-        RingBuffer {
+        Fifo {
             latched_dlen: 0,
             capacity: ram_size,
             read_index: 0,
@@ -572,52 +773,130 @@ mod tests {
 
     const OFFSET_USER: RvAddr = 0x04;
 
+    pub fn get_mailbox() -> MailboxInternal {
+        // Acquire lock
+        MailboxInternal::new(MailboxRam::new())
+    }
+
     #[test]
-    fn test_send_receive() {
-        let request_to_send: [u32; 4] = [0x1111_1111, 0x2222_2222, 0x3333_3333, 0x4444_4444];
+    fn test_sm_arc_rdyfordata_unlock() {
         // Acquire lock
-        let mut mb = Mailbox::new(MailboxRam::new());
-        assert_eq!(mb.read(RvSize::Word, Mailbox::OFFSET_LOCK).unwrap(), 0);
+        let mut mb = get_mailbox();
+        assert_eq!(mb.read(RvSize::Word, OFFSET_LOCK).unwrap(), 0);
         // Confirm it is locked
-        let lock = mb.read(RvSize::Word, Mailbox::OFFSET_LOCK).unwrap();
+        let lock = mb.read(RvSize::Word, OFFSET_LOCK).unwrap();
         assert_eq!(lock, 1);
 
         let user = mb.read(RvSize::Word, OFFSET_USER).unwrap();
         assert_eq!(user, 0);
 
         // Write command
-        assert_eq!(
-            mb.write(RvSize::Word, Mailbox::OFFSET_CMD, 0x55).ok(),
-            Some(())
-        );
+        assert_eq!(mb.write(RvSize::Word, OFFSET_CMD, 0x55).ok(), Some(()));
         // Confirm it is locked
         assert_eq!(mb.regs.borrow().state_machine.context.locked, 1);
 
+        // Release lock
+        let _ = mb
+            .regs
+            .borrow_mut()
+            .state_machine
+            .process_event(Events::WrUnlock);
+
+        // Check transition to idle
+        assert!(matches!(
+            mb.regs.borrow().state_machine.state(),
+            States::Idle
+        ));
+    }
+
+    #[test]
+    fn test_sm_arc_rdyforcmd_unlock() {
+        let mb = get_mailbox();
+        assert_eq!(mb.regs.borrow().state_machine.context().locked, 0);
+        assert_eq!(mb.regs.borrow().state_machine.context().dlen, 0);
+        // Acquire lock
+        let _ = mb
+            .regs
+            .borrow_mut()
+            .state_machine
+            .process_event(Events::RdLock(MailboxRequester::Caliptra));
+
+        assert!(matches!(
+            mb.regs.borrow().state_machine.state(),
+            States::RdyForCmd
+        ));
+        assert_eq!(mb.regs.borrow().state_machine.context().locked, 1);
+
+        // Release lock
+        let _ = mb
+            .regs
+            .borrow_mut()
+            .state_machine
+            .process_event(Events::WrUnlock);
+
+        assert_eq!(mb.regs.borrow().state_machine.context().locked, 0);
+        assert!(matches!(
+            mb.regs.borrow().state_machine.state(),
+            States::Idle
+        ));
+    }
+
+    #[test]
+    fn test_soc_to_caliptra_lock() {
+        let mut caliptra = MailboxInternal::new(MailboxRam::new());
+        let mut soc = caliptra.as_external();
+
+        assert_eq!(soc.read(RvSize::Word, OFFSET_LOCK).unwrap(), 0);
+        // Confirm it is locked
+        let soc_has_lock = soc.read(RvSize::Word, OFFSET_LOCK).unwrap();
+        assert_eq!(soc_has_lock, 1);
+
+        // Confirm caliptra has lock
+        let caliptra_has_lock = caliptra.read(RvSize::Word, OFFSET_LOCK).unwrap();
+        assert_eq!(caliptra_has_lock, 1);
+    }
+
+    #[test]
+    fn test_send_receive() {
+        let request_to_send: [u32; 4] = [0x1111_1111, 0x2222_2222, 0x3333_3333, 0x4444_4444];
+
+        let mut caliptra = MailboxInternal::new(MailboxRam::new());
+        let mut soc = caliptra.as_external();
+
+        assert_eq!(soc.read(RvSize::Word, OFFSET_LOCK).unwrap(), 0);
+        // Confirm it is locked
+        let lock = soc.read(RvSize::Word, OFFSET_LOCK).unwrap();
+        assert_eq!(lock, 1);
+
+        let user = soc.read(RvSize::Word, OFFSET_USER).unwrap();
+        assert_eq!(user, MailboxRequester::Soc as u32);
+
+        // Write command
+        assert_eq!(soc.write(RvSize::Word, OFFSET_CMD, 0x55).ok(), Some(()));
+        // Confirm it is locked
+        assert_eq!(soc.regs.borrow().state_machine.context.locked, 1);
+
         let dlen = request_to_send.len() as u32;
         let dlen = dlen * 4;
         // Write dlen
-        assert_eq!(
-            mb.write(RvSize::Word, Mailbox::OFFSET_DLEN, dlen).ok(),
-            Some(())
-        );
+        assert_eq!(soc.write(RvSize::Word, OFFSET_DLEN, dlen).ok(), Some(()));
 
         // Confirm it is locked
-        assert_eq!(mb.regs.borrow().state_machine.context.locked, 1);
+        assert_eq!(soc.regs.borrow().state_machine.context.locked, 1);
 
         for data_in in request_to_send.iter() {
             // Write datain
             assert_eq!(
-                mb.write(RvSize::Word, Mailbox::OFFSET_DATAIN, *data_in)
-                    .ok(),
+                soc.write(RvSize::Word, OFFSET_DATAIN, *data_in).ok(),
                 Some(())
             );
             // Confirm it is locked
-            assert_eq!(mb.regs.borrow().state_machine.context.locked, 1);
+            assert_eq!(soc.regs.borrow().state_machine.context.locked, 1);
         }
         assert_eq!(
-            mb.write(
+            soc.write(
                 RvSize::Word,
-                Mailbox::OFFSET_STATUS,
+                OFFSET_STATUS,
                 Status::STATUS::DATA_READY.value
             )
             .ok(),
@@ -625,63 +904,58 @@ mod tests {
         );
 
         // Write exec
-        assert_eq!(
-            mb.write(RvSize::Word, Mailbox::OFFSET_EXECUTE, 0x55).ok(),
-            Some(())
-        );
+        assert_eq!(soc.write(RvSize::Word, OFFSET_EXECUTE, 0x55).ok(), Some(()));
         // Confirm it is locked
-        assert_eq!(mb.regs.borrow().state_machine.context.locked, 1);
+        assert_eq!(soc.regs.borrow().state_machine.context.locked, 1);
 
         assert!(matches!(
-            mb.regs.borrow().state_machine.state(),
-            States::Exec
+            soc.regs.borrow().state_machine.state(),
+            States::ExecUc
         ));
 
-        let status = mb.read(RvSize::Word, Mailbox::OFFSET_STATUS).unwrap();
+        let status = caliptra.read(RvSize::Word, OFFSET_STATUS).unwrap();
         assert_eq!(
             status,
             (Status::STATUS::DATA_READY + Status::MBOX_FSM_PS::MBOX_EXECUTE_UC).value
         );
 
-        let cmd = mb.read(RvSize::Word, Mailbox::OFFSET_CMD).unwrap();
+        let cmd = caliptra.read(RvSize::Word, OFFSET_CMD).unwrap();
         assert_eq!(cmd, 0x55);
 
-        let dlen = mb.read(RvSize::Word, Mailbox::OFFSET_DLEN).unwrap();
+        let dlen = caliptra.read(RvSize::Word, OFFSET_DLEN).unwrap();
         assert_eq!(dlen, (request_to_send.len() * 4) as u32);
 
         request_to_send.iter().for_each(|data_in| {
             // Read dataout
-            let data_out = mb.read(RvSize::Word, Mailbox::OFFSET_DATAOUT).unwrap();
+            let data_out = caliptra.read(RvSize::Word, OFFSET_DATAOUT).unwrap();
             // compare with queued data.
             assert_eq!(*data_in, data_out);
         });
         assert_eq!(
-            mb.write(
-                RvSize::Word,
-                Mailbox::OFFSET_STATUS,
-                Status::STATUS::CMD_COMPLETE.value
-            )
-            .ok(),
+            caliptra
+                .write(
+                    RvSize::Word,
+                    OFFSET_STATUS,
+                    Status::STATUS::CMD_COMPLETE.value
+                )
+                .ok(),
             Some(())
         );
 
-        // Receiver resets exec register
-        assert_eq!(
-            mb.write(RvSize::Word, Mailbox::OFFSET_EXECUTE, 0).ok(),
-            Some(())
-        );
+        // Requester resets exec register
+        assert_eq!(soc.write(RvSize::Word, OFFSET_EXECUTE, 0).ok(), Some(()));
         // Confirm it is unlocked
-        assert_eq!(mb.regs.borrow().state_machine.context.locked, 0);
+        assert_eq!(caliptra.regs.borrow().state_machine.context.locked, 0);
 
         assert!(matches!(
-            mb.regs.borrow().state_machine.state(),
+            caliptra.regs.borrow().state_machine.state(),
             States::Idle
         ));
     }
 
     #[test]
     fn test_sm_init() {
-        let mb = Mailbox::new(MailboxRam::new());
+        let mb = get_mailbox();
         assert!(matches!(
             mb.regs.borrow().state_machine.state(),
             States::Idle
@@ -691,7 +965,7 @@ mod tests {
 
     #[test]
     fn test_sm_lock() {
-        let mb = Mailbox::new(MailboxRam::new());
+        let mb = get_mailbox();
         assert_eq!(mb.regs.borrow().state_machine.context().locked, 0);
         assert_eq!(mb.regs.borrow().state_machine.context().dlen, 0);
 
@@ -699,7 +973,7 @@ mod tests {
             .regs
             .borrow_mut()
             .state_machine
-            .process_event(Events::RdLock(Owner(0)));
+            .process_event(Events::RdLock(MailboxRequester::Caliptra));
         assert!(matches!(
             mb.regs.borrow().state_machine.state(),
             States::RdyForCmd
@@ -730,17 +1004,17 @@ mod tests {
             .regs
             .borrow_mut()
             .state_machine
-            .process_event(Events::ExecSet);
+            .process_event(Events::UcExecSet);
         assert!(matches!(
             mb.regs.borrow().state_machine.state(),
-            States::Exec
+            States::ExecSoc
         ));
 
         let _ = mb
             .regs
             .borrow_mut()
             .state_machine
-            .process_event(Events::ExecClear);
+            .process_event(Events::UcExecClear);
         assert!(matches!(
             mb.regs.borrow().state_machine.state(),
             States::Idle
@@ -751,26 +1025,23 @@ mod tests {
     #[test]
     fn test_send_receive_max_limit() {
         // Acquire lock
-        let mut mb = Mailbox::new(MailboxRam::new());
-        assert_eq!(mb.read(RvSize::Word, Mailbox::OFFSET_LOCK).unwrap(), 0);
+        let mut mb = get_mailbox();
+        assert_eq!(mb.read(RvSize::Word, OFFSET_LOCK).unwrap(), 0);
         // Confirm it is locked
-        let lock = mb.read(RvSize::Word, Mailbox::OFFSET_LOCK).unwrap();
+        let lock = mb.read(RvSize::Word, OFFSET_LOCK).unwrap();
         assert_eq!(lock, 1);
 
         let user = mb.read(RvSize::Word, OFFSET_USER).unwrap();
-        assert_eq!(user, 0);
+        assert_eq!(user, MailboxRequester::Caliptra as u32);
 
         // Write command
-        assert_eq!(
-            mb.write(RvSize::Word, Mailbox::OFFSET_CMD, 0x55).ok(),
-            Some(())
-        );
+        assert_eq!(mb.write(RvSize::Word, OFFSET_CMD, 0x55).ok(), Some(()));
 
         // Write dlen
         assert_eq!(
             mb.write(
                 RvSize::Word,
-                Mailbox::OFFSET_DLEN,
+                OFFSET_DLEN,
                 (MAX_MAILBOX_CAPACITY_BYTES + 4) as u32
             )
             .ok(),
@@ -780,23 +1051,21 @@ mod tests {
         for data_in in (0..MAX_MAILBOX_CAPACITY_BYTES).step_by(4) {
             // Write datain
             assert_eq!(
-                mb.write(RvSize::Word, Mailbox::OFFSET_DATAIN, data_in as u32)
-                    .ok(),
+                mb.write(RvSize::Word, OFFSET_DATAIN, data_in as u32).ok(),
                 Some(())
             );
         }
 
         // Write an additional DWORD. This should be a no-op.
         assert_eq!(
-            mb.write(RvSize::Word, Mailbox::OFFSET_DATAIN, 0xDEADBEEF)
-                .ok(),
+            mb.write(RvSize::Word, OFFSET_DATAIN, 0xDEADBEEF).ok(),
             Some(())
         );
 
         assert_eq!(
             mb.write(
                 RvSize::Word,
-                Mailbox::OFFSET_STATUS,
+                OFFSET_STATUS,
                 Status::STATUS::DATA_READY.value
             )
             .ok(),
@@ -804,46 +1073,40 @@ mod tests {
         );
 
         // Write exec
-        assert_eq!(
-            mb.write(RvSize::Word, Mailbox::OFFSET_EXECUTE, 0x55).ok(),
-            Some(())
-        );
+        assert_eq!(mb.write(RvSize::Word, OFFSET_EXECUTE, 0x55).ok(), Some(()));
 
         assert!(matches!(
             mb.regs.borrow().state_machine.state(),
-            States::Exec
+            States::ExecSoc
         ));
 
-        let status = mb.read(RvSize::Word, Mailbox::OFFSET_STATUS).unwrap();
+        let status = mb.read(RvSize::Word, OFFSET_STATUS).unwrap();
         assert_eq!(
             status,
-            (Status::STATUS::DATA_READY + Status::MBOX_FSM_PS::MBOX_EXECUTE_UC).value
+            (Status::STATUS::DATA_READY + Status::MBOX_FSM_PS::MBOX_EXECUTE_SOC).value
         );
 
-        let cmd = mb.read(RvSize::Word, Mailbox::OFFSET_CMD).unwrap();
+        let cmd = mb.read(RvSize::Word, OFFSET_CMD).unwrap();
         assert_eq!(cmd, 0x55);
 
-        let dlen = mb.read(RvSize::Word, Mailbox::OFFSET_DLEN).unwrap();
+        let dlen = mb.read(RvSize::Word, OFFSET_DLEN).unwrap();
         assert_eq!(dlen, (MAX_MAILBOX_CAPACITY_BYTES + 4) as u32);
 
         let mut data_out = 0;
         for data_in in (0..MAX_MAILBOX_CAPACITY_BYTES).step_by(4) {
             // Read dataout
-            data_out = mb.read(RvSize::Word, Mailbox::OFFSET_DATAOUT).unwrap();
+            data_out = mb.read(RvSize::Word, OFFSET_DATAOUT).unwrap();
             // compare with queued data.
             assert_eq!(data_in as u32, data_out);
         }
 
         // Read an additional DWORD. This should return the last word
-        assert_eq!(
-            mb.read(RvSize::Word, Mailbox::OFFSET_DATAOUT).unwrap(),
-            data_out
-        );
+        assert_eq!(mb.read(RvSize::Word, OFFSET_DATAOUT).unwrap(), data_out);
 
         assert_eq!(
             mb.write(
                 RvSize::Word,
-                Mailbox::OFFSET_STATUS,
+                OFFSET_STATUS,
                 Status::STATUS::CMD_COMPLETE.value
             )
             .ok(),
@@ -851,10 +1114,7 @@ mod tests {
         );
 
         // Receiver resets exec register
-        assert_eq!(
-            mb.write(RvSize::Word, Mailbox::OFFSET_EXECUTE, 0).ok(),
-            Some(())
-        );
+        assert_eq!(mb.write(RvSize::Word, OFFSET_EXECUTE, 0).ok(), Some(()));
         // Confirm it is unlocked
         assert_eq!(mb.regs.borrow().state_machine.context.locked, 0);
 
diff --git a/sw-emulator/lib/periph/src/root_bus.rs b/sw-emulator/lib/periph/src/root_bus.rs
index 2f3bcccf..4f7bdf33 100644
--- a/sw-emulator/lib/periph/src/root_bus.rs
+++ b/sw-emulator/lib/periph/src/root_bus.rs
@@ -14,7 +14,8 @@ Abstract:
 
 use crate::{
     iccm::Iccm, soc_reg::SocRegistersExternal, AsymEcc384, Doe, EmuCtrl, HashSha256, HashSha512,
-    HmacSha384, KeyVault, Mailbox, MailboxRam, Sha512Accelerator, SocRegistersInternal, Uart,
+    HmacSha384, KeyVault, MailboxExternal, MailboxInternal, MailboxRam, Sha512Accelerator,
+    SocRegistersInternal, Uart,
 };
 use caliptra_emu_bus::{Clock, Ram, Rom};
 use caliptra_emu_derive::Bus;
@@ -48,13 +49,17 @@ impl From<Box<dyn FnMut(u8) + 'static>> for TbServicesCb {
     }
 }
 
-type ReadyForFwCbSchedFn<'a> = dyn FnOnce(u64, Box<dyn FnOnce(&mut Mailbox)>) + 'a;
+type ReadyForFwCbSchedFn<'a> = dyn FnOnce(u64, Box<dyn FnOnce(&mut MailboxInternal)>) + 'a;
 pub struct ReadyForFwCbArgs<'a> {
-    pub mailbox: &'a mut Mailbox,
+    pub mailbox: &'a mut MailboxInternal,
     pub(crate) sched_fn: Box<ReadyForFwCbSchedFn<'a>>,
 }
 impl<'a> ReadyForFwCbArgs<'a> {
-    pub fn schedule_later(self, ticks_from_now: u64, cb: impl FnOnce(&mut Mailbox) + 'static) {
+    pub fn schedule_later(
+        self,
+        ticks_from_now: u64,
+        cb: impl FnOnce(&mut MailboxInternal) + 'static,
+    ) {
         (self.sched_fn)(ticks_from_now, Box::new(cb));
     }
 }
@@ -87,10 +92,10 @@ impl From<Box<dyn FnMut(ReadyForFwCbArgs) + 'static>> for ReadyForFwCb {
     }
 }
 
-type UploadUpdateFwFn = Box<dyn FnMut(&mut Mailbox)>;
+type UploadUpdateFwFn = Box<dyn FnMut(&mut MailboxInternal)>;
 pub struct UploadUpdateFwCb(pub UploadUpdateFwFn);
 impl UploadUpdateFwCb {
-    pub fn new(f: impl FnMut(&mut Mailbox) + 'static) -> Self {
+    pub fn new(f: impl FnMut(&mut MailboxInternal) + 'static) -> Self {
         Self(Box::new(f))
     }
     pub(crate) fn take(&mut self) -> UploadUpdateFwFn {
@@ -109,8 +114,8 @@ impl std::fmt::Debug for UploadUpdateFwCb {
             .finish()
     }
 }
-impl From<Box<dyn FnMut(&mut Mailbox) + 'static>> for UploadUpdateFwCb {
-    fn from(value: Box<dyn FnMut(&mut Mailbox)>) -> Self {
+impl From<Box<dyn FnMut(&mut MailboxInternal) + 'static>> for UploadUpdateFwCb {
+    fn from(value: Box<dyn FnMut(&mut MailboxInternal)>) -> Self {
         Self(value)
     }
 }
@@ -194,7 +199,7 @@ pub struct CaliptraRootBus {
     pub mailbox_sram: MailboxRam,
 
     #[peripheral(offset = 0x3002_0000, mask = 0x0000_0fff)]
-    pub mailbox: Mailbox,
+    pub mailbox: MailboxInternal,
 
     #[peripheral(offset = 0x3002_1000, mask = 0x0000_0fff)]
     pub sha512_acc: Sha512Accelerator,
@@ -214,7 +219,7 @@ impl CaliptraRootBus {
     pub fn new(clock: &Clock, mut args: CaliptraRootBusArgs) -> Self {
         let key_vault = KeyVault::new();
         let mailbox_ram = MailboxRam::new();
-        let mailbox = Mailbox::new(mailbox_ram.clone());
+        let mailbox = MailboxInternal::new(mailbox_ram.clone());
         let rom = Rom::new(std::mem::take(&mut args.rom));
         let iccm = Iccm::new(clock);
         let soc_reg = SocRegistersInternal::new(clock, mailbox.clone(), iccm.clone(), args);
@@ -242,7 +247,7 @@ impl CaliptraRootBus {
         SocToCaliptraBus {
             // TODO: This should not be the same mailbox bus as the one used
             // internaly
-            mailbox: self.mailbox.clone(),
+            mailbox: self.mailbox.as_external(),
             soc_ifc: self.soc_reg.external_regs(),
         }
     }
@@ -251,7 +256,7 @@ impl CaliptraRootBus {
 #[derive(Bus)]
 pub struct SocToCaliptraBus {
     #[peripheral(offset = 0x3002_0000, mask = 0x0000_0fff)]
-    mailbox: Mailbox,
+    mailbox: MailboxExternal,
 
     #[peripheral(offset = 0x3003_0000, mask = 0x0000_ffff)]
     soc_ifc: SocRegistersExternal,
diff --git a/sw-emulator/lib/periph/src/soc_reg.rs b/sw-emulator/lib/periph/src/soc_reg.rs
index f7277bf3..9296f071 100644
--- a/sw-emulator/lib/periph/src/soc_reg.rs
+++ b/sw-emulator/lib/periph/src/soc_reg.rs
@@ -14,7 +14,7 @@ Abstract:
 
 use crate::helpers::{bytes_from_words_le, words_from_bytes_le};
 use crate::root_bus::ReadyForFwCbArgs;
-use crate::{CaliptraRootBusArgs, Iccm, Mailbox};
+use crate::{CaliptraRootBusArgs, Iccm, MailboxInternal};
 use caliptra_emu_bus::BusError::{LoadAccessFault, StoreAccessFault};
 use caliptra_emu_bus::{
     ActionHandle, Bus, BusError, Clock, ReadOnlyMemory, ReadOnlyRegister, ReadWriteRegister,
@@ -31,7 +31,7 @@ use tock_registers::registers::InMemoryRegister;
 // Second parameter is schedule(ticks_from_now: u64, cb: Box<dyn FnOnce(&mut
 // Mailbox)>), which is called to schedule firmware writing in the future
 type ReadyForFwCallback = Box<dyn FnMut(ReadyForFwCbArgs)>;
-type UploadUpdateFwCallback = Box<dyn FnMut(&mut Mailbox)>;
+type UploadUpdateFwCallback = Box<dyn FnMut(&mut MailboxInternal)>;
 type BootFsmGoCallback = Box<dyn FnMut()>;
 
 mod constants {
@@ -181,7 +181,12 @@ const FUSE_END_ADDR: u32 = 0x340;
 
 impl SocRegistersInternal {
     /// Create an instance of SOC register peripheral
-    pub fn new(clock: &Clock, mailbox: Mailbox, iccm: Iccm, args: CaliptraRootBusArgs) -> Self {
+    pub fn new(
+        clock: &Clock,
+        mailbox: MailboxInternal,
+        iccm: Iccm,
+        args: CaliptraRootBusArgs,
+    ) -> Self {
         Self {
             regs: Rc::new(RefCell::new(SocRegistersImpl::new(
                 clock, mailbox, iccm, args,
@@ -428,7 +433,7 @@ struct SocRegistersImpl {
     internal_nmi_vector: ReadWriteRegister<u32>,
 
     /// Mailbox
-    mailbox: Mailbox,
+    mailbox: MailboxInternal,
 
     /// ICCM
     iccm: Iccm,
@@ -439,7 +444,7 @@ struct SocRegistersImpl {
     /// Firmware Write Complete action
     op_fw_write_complete_action: Option<ActionHandle>,
     #[allow(clippy::type_complexity)]
-    op_fw_write_complete_cb: Option<Box<dyn FnOnce(&mut Mailbox)>>,
+    op_fw_write_complete_cb: Option<Box<dyn FnOnce(&mut MailboxInternal)>>,
 
     /// Firmware Read Complete action
     op_fw_read_complete_action: Option<ActionHandle>,
@@ -478,7 +483,12 @@ impl SocRegistersImpl {
     /// The number of CPU clock cycles it takes to read the firmware from the mailbox.
     const FW_READ_TICKS: u64 = 0;
 
-    pub fn new(clock: &Clock, mailbox: Mailbox, iccm: Iccm, mut args: CaliptraRootBusArgs) -> Self {
+    pub fn new(
+        clock: &Clock,
+        mailbox: MailboxInternal,
+        iccm: Iccm,
+        mut args: CaliptraRootBusArgs,
+    ) -> Self {
         let regs = Self {
             cptra_hw_error_fatal: ReadWriteRegister::new(0),
             cptra_hw_error_non_fatal: ReadWriteRegister::new(0),
@@ -621,7 +631,7 @@ impl SocRegistersImpl {
             let op_fw_write_complete_action = &mut self.op_fw_write_complete_action;
             let op_fw_write_complete_cb = &mut self.op_fw_write_complete_cb;
             let timer = &self.timer;
-            let sched_fn = move |ticks_from_now: u64, cb: Box<dyn FnOnce(&mut Mailbox)>| {
+            let sched_fn = move |ticks_from_now: u64, cb: Box<dyn FnOnce(&mut MailboxInternal)>| {
                 *op_fw_write_complete_action = Some(timer.schedule_poll_in(ticks_from_now));
                 *op_fw_write_complete_cb = Some(cb);
             };
@@ -750,7 +760,7 @@ mod tests {
     };
     use tock_registers::{interfaces::ReadWriteable, registers::InMemoryRegister};
 
-    fn send_data_to_mailbox(mailbox: &mut Mailbox, cmd: u32, data: &[u8]) {
+    fn send_data_to_mailbox(mailbox: &mut MailboxInternal, cmd: u32, data: &[u8]) {
         while !mailbox.try_acquire_lock() {}
 
         mailbox.write_cmd(cmd).unwrap();
@@ -779,7 +789,7 @@ mod tests {
     }
 
     fn download_idev_id_csr(
-        mailbox: &mut Mailbox,
+        mailbox: &mut MailboxInternal,
         path: &mut PathBuf,
         soc_reg: &mut SocRegistersInternal,
     ) {
@@ -794,7 +804,7 @@ mod tests {
     }
 
     fn download_ldev_id_cert(
-        mailbox: &mut Mailbox,
+        mailbox: &mut MailboxInternal,
         path: &mut PathBuf,
         soc_reg: &mut SocRegistersInternal,
     ) {
@@ -808,7 +818,7 @@ mod tests {
             .modify(DebugManufService::REQ_LDEVID_CERT::CLEAR)
     }
 
-    fn download_to_file(mailbox: &mut Mailbox, path: &mut PathBuf, file: &str) {
+    fn download_to_file(mailbox: &mut MailboxInternal, path: &mut PathBuf, file: &str) {
         path.push(file);
         let mut file = std::fs::File::create(path).unwrap();
 
@@ -841,7 +851,7 @@ mod tests {
         ];
         let clock = Clock::new();
         let mailbox_ram = MailboxRam::new();
-        let mut mailbox = Mailbox::new(mailbox_ram);
+        let mut mailbox = MailboxInternal::new(mailbox_ram);
         let mut log_dir = PathBuf::new();
         log_dir.push("/tmp");
         let args = CaliptraRootBusArgs::default();
@@ -900,7 +910,7 @@ mod tests {
         ];
         let clock = Clock::new();
         let mailbox_ram = MailboxRam::new();
-        let mut mailbox = Mailbox::new(mailbox_ram);
+        let mut mailbox = MailboxInternal::new(mailbox_ram);
         let mut log_dir = PathBuf::new();
         log_dir.push("/tmp");
         let args = CaliptraRootBusArgs::default();
@@ -957,7 +967,7 @@ mod tests {
 
         let clock = Clock::new();
         let mailbox_ram = MailboxRam::new();
-        let mailbox = Mailbox::new(mailbox_ram);
+        let mailbox = MailboxInternal::new(mailbox_ram);
         let args = CaliptraRootBusArgs {
             tb_services_cb: TbServicesCb::new(move |ch| output2.borrow_mut().push(ch)),
             ..Default::default()


Test patch:
diff --git a/drivers/test-fw/Cargo.toml b/drivers/test-fw/Cargo.toml
index 74617958..87a988f3 100644
--- a/drivers/test-fw/Cargo.toml
+++ b/drivers/test-fw/Cargo.toml
@@ -59,6 +59,11 @@ name = "mailbox_driver_responder"
 path = "src/bin/mailbox_driver_responder.rs"
 required-features = ["riscv"]
 
+[[bin]]
+name = "mailbox_driver_sender"
+path = "src/bin/mailbox_driver_sender.rs"
+required-features = ["riscv"]
+
 [[bin]]
 name = "keyvault"
 path = "src/bin/keyvault_tests.rs"
diff --git a/drivers/test-fw/src/bin/mailbox_driver_sender.rs b/drivers/test-fw/src/bin/mailbox_driver_sender.rs
new file mode 100644
index 00000000..dcca6010
--- /dev/null
+++ b/drivers/test-fw/src/bin/mailbox_driver_sender.rs
@@ -0,0 +1,67 @@
+// Licensed under the Apache-2.0 license
+
+//! A very simple program that uses the driver to send mailbox messages
+
+#![no_main]
+#![no_std]
+
+// Needed to bring in startup code
+#[allow(unused)]
+use caliptra_test_harness::{self, println};
+
+use caliptra_drivers::{self, Mailbox, MailboxSendTxn};
+
+#[panic_handler]
+pub fn panic(_info: &core::panic::PanicInfo) -> ! {
+    loop {}
+}
+
+fn start_send_txn() -> MailboxSendTxn {
+    let mbox = Mailbox::default();
+    loop {
+        if let Some(txn) = mbox.try_start_send_txn() {
+            return txn;
+        }
+    }
+}
+
+#[no_mangle]
+extern "C" fn main() {
+    // 0 byte request
+    let mut txn = start_send_txn();
+    txn.send_request(0xa000_0000, b"").unwrap();
+    while !txn.is_response_ready() {}
+    txn.complete().unwrap();
+
+    // 3 byte request
+    let mut txn = start_send_txn();
+    txn.send_request(0xa000_1000, b"Hi!").unwrap();
+    while !txn.is_response_ready() {}
+    txn.complete().unwrap();
+
+    // 4 byte request
+    let mut txn = start_send_txn();
+    txn.send_request(0xa000_2000, b"Hi!!").unwrap();
+    while !txn.is_response_ready() {}
+    txn.complete().unwrap();
+
+    // 6 byte request
+    let mut txn = start_send_txn();
+    txn.send_request(0xa000_3000, b"Hello!").unwrap();
+    while !txn.is_response_ready() {}
+    txn.complete().unwrap();
+
+    // 8 byte request
+    let mut txn = start_send_txn();
+    txn.send_request(0xa000_4000, b"Hello!!!").unwrap();
+    while !txn.is_response_ready() {}
+    txn.complete().unwrap();
+
+    // write_cmd / write_dlen / execute_request used separately
+    let mut txn = start_send_txn();
+    txn.write_cmd(0xb000_0000).unwrap();
+    txn.write_dlen(0).unwrap();
+    txn.execute_request().unwrap();
+    while !txn.is_response_ready() {}
+    txn.complete().unwrap();
+}
diff --git a/drivers/tests/integration_tests.rs b/drivers/tests/integration_tests.rs
index 820ca27c..fc403083 100644
--- a/drivers/tests/integration_tests.rs
+++ b/drivers/tests/integration_tests.rs
@@ -206,6 +206,48 @@ fn test_mailbox_soc_to_uc() {
     }
 }
 
+#[test]
+fn test_mailbox_uc_to_soc() {
+    let mut model = start_driver_test("mailbox_driver_sender").unwrap();
+
+    // 0 byte request
+    let txn = model.wait_for_mailbox_receive().unwrap();
+    assert_eq!(txn.req.cmd, 0xa000_0000);
+    assert_eq!(txn.req.data, b"");
+    txn.respond_success();
+
+    // 3 byte request
+    let txn = model.wait_for_mailbox_receive().unwrap();
+    assert_eq!(txn.req.cmd, 0xa000_1000);
+    assert_eq!(txn.req.data, b"Hi!");
+    // NOTE: The current driver doesn't actually look at the result
+    txn.respond_success();
+
+    // 4 byte request
+    let txn = model.wait_for_mailbox_receive().unwrap();
+    assert_eq!(txn.req.cmd, 0xa000_2000);
+    assert_eq!(txn.req.data, b"Hi!!");
+    txn.respond_success();
+
+    // 6 byte request
+    let txn = model.wait_for_mailbox_receive().unwrap();
+    assert_eq!(txn.req.cmd, 0xa000_3000);
+    assert_eq!(txn.req.data, b"Hello!");
+    txn.respond_success();
+
+    // 8 byte request
+    let txn = model.wait_for_mailbox_receive().unwrap();
+    assert_eq!(txn.req.cmd, 0xa000_4000);
+    assert_eq!(txn.req.data, b"Hello!!!");
+    txn.respond_success();
+
+    // write_cmd / write_dlen / execute_request used separately
+    let txn = model.wait_for_mailbox_receive().unwrap();
+    assert_eq!(txn.req.cmd, 0xb000_0000);
+    assert_eq!(txn.req.data, b"");
+    txn.respond_success();
+}
+
 #[test]
 fn test_pcrbank() {
     run_driver_test("pcrbank");
diff --git a/hw-model/test-fw/Cargo.toml b/hw-model/test-fw/Cargo.toml
index 3ccf5c58..58e867ee 100644
--- a/hw-model/test-fw/Cargo.toml
+++ b/hw-model/test-fw/Cargo.toml
@@ -28,3 +28,8 @@ required-features = ["riscv"]
 name = "mailbox_responder"
 path = "mailbox_responder.rs"
 required-features = ["riscv"]
+
+[[bin]]
+name = "mailbox_sender"
+path = "mailbox_sender.rs"
+required-features = ["riscv"]
diff --git a/hw-model/test-fw/mailbox_responder.rs b/hw-model/test-fw/mailbox_responder.rs
index 973029c6..d17afaac 100644
--- a/hw-model/test-fw/mailbox_responder.rs
+++ b/hw-model/test-fw/mailbox_responder.rs
@@ -23,7 +23,9 @@ extern "C" fn main() {
     let mbox = caliptra_registers::mbox::RegisterBlock::mbox_csr();
 
     loop {
-        while !mbox.status().read().mbox_fsm_ps().mbox_execute_uc() {}
+        while !mbox.status().read().mbox_fsm_ps().mbox_execute_uc() {
+            // Wait for a request from the SoC.
+        }
         let cmd = mbox.cmd().read();
 
         match cmd {
diff --git a/hw-model/test-fw/mailbox_sender.rs b/hw-model/test-fw/mailbox_sender.rs
new file mode 100644
index 00000000..9d93d5f7
--- /dev/null
+++ b/hw-model/test-fw/mailbox_sender.rs
@@ -0,0 +1,85 @@
+// Licensed under the Apache-2.0 license
+
+//! A very simple program that sends mailbox transactions.
+
+#![no_main]
+#![no_std]
+
+// Needed to bring in startup code
+#[allow(unused)]
+use caliptra_test_harness::println;
+
+use caliptra_registers::{self};
+
+#[panic_handler]
+pub fn panic(_info: &core::panic::PanicInfo) -> ! {
+    loop {}
+}
+
+struct Response {
+    cmd: u32,
+    dlen: u32,
+    buf: &'static [u32],
+}
+
+const RESPONSES: [Response; 3] = [
+    Response {
+        cmd: 0xe000_0000,
+        dlen: 8,
+        buf: &[0x6745_2301, 0xefcd_ab89],
+    },
+    Response {
+        cmd: 0xe000_1000,
+        dlen: 3,
+        buf: &[0xaabbccdd],
+    },
+    Response {
+        cmd: 0xe000_2000,
+        dlen: 0,
+        buf: &[],
+    },
+];
+
+#[no_mangle]
+extern "C" fn main() {
+    let soc_ifc = caliptra_registers::soc_ifc::RegisterBlock::soc_ifc_reg();
+    soc_ifc.cptra_flow_status().write(|w| w.ready_for_fw(true));
+    let mbox = caliptra_registers::mbox::RegisterBlock::mbox_csr();
+
+    let mut response_iter = RESPONSES.iter().cycle();
+    loop {
+        soc_ifc.cptra_fw_extended_error_info().at(0).write(|_| 0);
+        assert!(!mbox.lock().read().lock());
+
+        let response = response_iter.next().unwrap();
+        mbox.cmd().write(|_| response.cmd);
+        mbox.dlen().write(|_| response.dlen);
+        for word in response.buf.iter() {
+            mbox.datain().write(|_| *word);
+        }
+        mbox.execute().write(|w| w.execute(true));
+        while mbox.status().read().status().cmd_busy() {}
+        let status = mbox.status().read().status();
+        soc_ifc
+            .cptra_fw_extended_error_info()
+            .at(1)
+            .write(|_| mbox.dlen().read());
+        soc_ifc
+            .cptra_fw_extended_error_info()
+            .at(2)
+            .write(|_| mbox.dataout().read());
+        soc_ifc
+            .cptra_fw_extended_error_info()
+            .at(3)
+            .write(|_| mbox.dataout().read());
+        soc_ifc
+            .cptra_fw_extended_error_info()
+            .at(4)
+            .write(|_| mbox.dataout().read());
+        soc_ifc
+            .cptra_fw_extended_error_info()
+            .at(0)
+            .write(|_| status.into());
+        mbox.execute().write(|w| w.execute(false));
+    }
+}