diff --git a/test/sim/common/tb_cxxrtl_io.h b/test/sim/common/tb_cxxrtl_io.h
index c550ea9..e26c248 100644
--- a/test/sim/common/tb_cxxrtl_io.h
+++ b/test/sim/common/tb_cxxrtl_io.h
@@ -4,6 +4,10 @@
 #include <stdarg.h>
 #include <stdint.h>
 #include <stdio.h>
+#include <stdbool.h>
+
+// ----------------------------------------------------------------------------
+// Testbench IO hardware layout
 
 #define IO_BASE 0x80000000
 
@@ -11,6 +15,14 @@ typedef struct {
 	volatile uint32_t print_char;
 	volatile uint32_t print_u32;
 	volatile uint32_t exit;
+	uint32_t _pad0;
+	volatile uint32_t set_softirq;
+	volatile uint32_t clr_softirq;
+	uint32_t _pad1[2];
+	volatile uint32_t set_irq;
+	uint32_t _pad2[3];
+	volatile uint32_t clr_irq;
+	uint32_t _pad3[3];
 } io_hw_t;
 
 #define mm_io ((io_hw_t *const)IO_BASE)
@@ -24,6 +36,8 @@ typedef struct {
 
 #define mm_timer ((timer_hw_t *const)(IO_BASE + 0x100))
 
+// ----------------------------------------------------------------------------
+// Testbench IO convenience functions
 
 static inline void tb_putc(char c) {
 	mm_io->print_char = (uint32_t)c;
@@ -57,4 +71,28 @@ static inline void tb_printf(const char *fmt, ...) {
 
 #define tb_assert(cond, ...) if (!(cond)) {tb_printf(__VA_ARGS__); tb_exit(-1);}
 
+static inline void tb_set_softirq() {
+	mm_io->set_softirq = 1;
+}
+
+static inline void tb_clr_softirq() {
+	mm_io->clr_softirq = 1;
+}
+
+static inline bool tb_get_softirq() {
+	return (bool)mm_io->set_softirq;
+}
+
+static inline void tb_set_irq_masked(uint32_t mask) {
+	mm_io->set_irq = mask;
+}
+
+static inline void tb_clr_irq_masked(uint32_t mask) {
+	mm_io->clr_irq = mask;
+}
+
+static inline uint32_t tb_get_irq_mask() {
+	return mm_io->set_irq;
+}
+
 #endif
diff --git a/test/sim/sw_testcases/soft_irq.c b/test/sim/sw_testcases/soft_irq.c
new file mode 100644
index 0000000..bd1cd16
--- /dev/null
+++ b/test/sim/sw_testcases/soft_irq.c
@@ -0,0 +1,64 @@
+#include "tb_cxxrtl_io.h"
+#include "hazard3_csr.h"
+
+/*EXPECTED-OUTPUT***************************************************************
+
+Dry run
+Set mie only
+Then set IRQ
+-> handle_soft_irq
+mip     = 00000088  // mtip is also set, because mtimecmp is 0
+mie     = 00000008  // only msie is set
+mcause  = 80000003  // MSB indicates IRQ. LSBs are index of mip.
+mstatus = 00001880  // MPP = 3. mpie = 1, mie = 0, since we just took an IRQ.
+Returned from IRQ
+Clear mie, do another dry run
+
+*******************************************************************************/
+
+#define mip_msip 0x8u
+#define mie_msie mip_msip
+
+int main() {
+	tb_assert(!(read_csr(mip) & mip_msip), "mip.msip should be clear at start of test\n");
+
+	tb_puts("Dry run\n");
+	tb_set_softirq();
+	tb_assert(tb_get_softirq(), "Failed to set soft_irq through tb\n");
+	tb_assert(read_csr(mip) & mip_msip, "soft_irq not reflected in mip\n");
+
+	tb_clr_softirq();
+	tb_assert(!tb_get_softirq(), "Failed to clear soft_irq through tb\n");
+	tb_assert(!(read_csr(mip) & mip_msip), "soft_irq clear not reflected in mip\n");
+
+	tb_puts("Set mie only\n");
+	write_csr(mie, mie_msie);
+	asm volatile ("csrsi mstatus, 0x8");
+	// IRQ should not fire yet.
+
+	tb_puts("Then set IRQ\n");
+	tb_set_softirq(); 
+	tb_assert(!(read_csr(mip) & mip_msip), "soft_irq should have been cleared by IRQ\n");
+	tb_puts("Returned from IRQ\n");
+
+	tb_puts("Clear mie, do another dry run\n");
+	write_csr(mie, 0);
+	tb_set_softirq();
+	tb_assert(tb_get_softirq(), "Failed to set soft_irq through tb\n");
+	tb_assert(read_csr(mip) & mip_msip, "soft_irq not reflected in mip\n");
+
+	tb_clr_softirq();
+	tb_assert(!tb_get_softirq(), "Failed to clear soft_irq through tb\n");
+	tb_assert(!(read_csr(mip) & mip_msip), "soft_irq clear not reflected in mip\n");
+
+	return 0;
+}
+
+void __attribute__((interrupt)) isr_machine_softirq() {
+	tb_puts("-> handle_soft_irq\n");
+	tb_printf("mip     = %08x\n", read_csr(mip));
+	tb_printf("mie     = %08x\n", read_csr(mie));
+	tb_clr_softirq();
+	tb_printf("mcause  = %08x\n", read_csr(mcause));
+	tb_printf("mstatus = %08x\n", read_csr(mstatus));
+}
diff --git a/test/sim/tb_cxxrtl/tb.cpp b/test/sim/tb_cxxrtl/tb.cpp
index a84dd15..83441c4 100644
--- a/test/sim/tb_cxxrtl/tb.cpp
+++ b/test/sim/tb_cxxrtl/tb.cpp
@@ -18,13 +18,17 @@ uint8_t mem[MEM_SIZE];
 
 static const unsigned int IO_BASE = 0x80000000;
 enum {
-	IO_PRINT_CHAR = 0x000,
-	IO_PRINT_U32  = 0x004,
-	IO_EXIT       = 0x008,
-	IO_MTIME      = 0x100,
-	IO_MTIMEH     = 0x104,
-	IO_MTIMECMP   = 0x108,
-	IO_MTIMECMPH  = 0x10c
+	IO_PRINT_CHAR  = 0x000,
+	IO_PRINT_U32   = 0x004,
+	IO_EXIT        = 0x008,
+	IO_SET_SOFTIRQ = 0x010,
+	IO_CLR_SOFTIRQ = 0x014,
+	IO_SET_IRQ     = 0x020,
+	IO_CLR_IRQ     = 0x030,
+	IO_MTIME       = 0x100,
+	IO_MTIMEH      = 0x104,
+	IO_MTIMECMP    = 0x108,
+	IO_MTIMECMPH   = 0x10c
 };
 
 static const int TCP_BUF_SIZE = 256;
@@ -300,6 +304,18 @@ int main(int argc, char **argv) {
 					printf("Ran for %ld cycles\n", cycle + 1);
 					break;
 				}
+				else if (bus_addr == IO_BASE + IO_SET_SOFTIRQ) {
+					top.p_soft__irq.set<bool>(true);
+				}
+				else if (bus_addr == IO_BASE + IO_CLR_SOFTIRQ) {
+					top.p_soft__irq.set<bool>(false);
+				}
+				else if (bus_addr == IO_BASE + IO_SET_IRQ) {
+					top.p_irq.set<uint32_t>(top.p_irq.get<uint32_t>() | wdata);
+				}
+				else if (bus_addr == IO_BASE + IO_CLR_IRQ) {
+					top.p_irq.set<uint32_t>(top.p_irq.get<uint32_t>() & ~wdata);
+				}
 				else if (bus_addr == IO_BASE + IO_MTIME) {
 					mtime = (mtime & 0xffffffff00000000u) | wdata;
 				}
@@ -325,6 +341,12 @@ int main(int argc, char **argv) {
 						mem[bus_addr + 2] << 16 |
 						mem[bus_addr + 3] << 24;
 				}
+				else if (bus_addr == IO_BASE + IO_SET_SOFTIRQ || bus_addr == IO_BASE + IO_CLR_SOFTIRQ) {
+					rdata = top.p_soft__irq.get<bool>();
+				}
+				else if (bus_addr == IO_BASE + IO_SET_IRQ || bus_addr == IO_BASE + IO_CLR_IRQ) {
+					rdata = top.p_irq.get<uint32_t>();
+				}
 				else if (bus_addr == IO_BASE + IO_MTIME) {
 					rdata = mtime;
 				}