[sw/ottf] implemented default ISRs

This commit adds the following to the OTTF:

- defualt (weak) ISRs
- FreeRTOS context saving (asm) subroutines to be called on ISR
  entry/exit

Signed-off-by: Timothy Trippel <ttrippel@google.com>
diff --git a/sw/device/lib/base/macros.h b/sw/device/lib/base/macros.h
index 7f5ded0..2e55205 100644
--- a/sw/device/lib/base/macros.h
+++ b/sw/device/lib/base/macros.h
@@ -93,4 +93,9 @@
  */
 #define OT_WARN_UNUSED_RESULT __attribute__((warn_unused_result))
 
+/**
+ * Attribute for weak functions that can be overridden, e.g., ISRs.
+ */
+#define OT_ATTR_WEAK __attribute__((weak))
+
 #endif  // OPENTITAN_SW_DEVICE_LIB_BASE_MACROS_H_
diff --git a/sw/device/lib/runtime/ibex.c b/sw/device/lib/runtime/ibex.c
index 776edee..7e3ff05 100644
--- a/sw/device/lib/runtime/ibex.c
+++ b/sw/device/lib/runtime/ibex.c
@@ -4,4 +4,18 @@
 
 #include "sw/device/lib/runtime/ibex.h"
 
+#include "sw/device/lib/base/csr.h"
+
 extern uint64_t ibex_mcycle_read(void);
+
+uint32_t ibex_mcause_read(void) {
+  uint32_t mtval;
+  CSR_READ(CSR_REG_MCAUSE, &mtval);
+  return mtval;
+}
+
+uint32_t ibex_mtval_read(void) {
+  uint32_t mtval;
+  CSR_READ(CSR_REG_MTVAL, &mtval);
+  return mtval;
+}
diff --git a/sw/device/lib/runtime/ibex.h b/sw/device/lib/runtime/ibex.h
index 5c389ba..d989f34 100644
--- a/sw/device/lib/runtime/ibex.h
+++ b/sw/device/lib/runtime/ibex.h
@@ -41,4 +41,38 @@
   return (uint64_t)cycle_high << 32 | cycle_low;
 }
 
+/**
+ * Reads the mcause register.
+ *
+ * When an exception is encountered, the corresponding exception code is stored
+ * in mcause register.
+ *
+ * A list of the exception codes can be found at:
+ * https://ibex-core.readthedocs.io/en/latest/03_reference/
+ * exception_interrupts.html#exceptions
+ */
+uint32_t ibex_mcause_read(void);
+
+/**
+ * Reads the mtval register.
+ *
+ * When an exception is encountered, the Machine Trap Value (mtval) register
+ * can holds exception-specific information to assist software in handling the
+ * trap.
+ *
+ * From the Ibex documentation (found at
+ * https://ibex-core.readthedocs.io/en/latest/03_reference/cs_registers.html)
+ * - In the case of errors in the load-store unit mtval holds the address of
+ * the transaction causing the error.
+ *
+ * - If a transaction is misaligned, mtval holds the address of the missing
+ *   transaction part.
+ *
+ * - In the case of illegal instruction exceptions, mtval holds the actual
+ * faulting instruction.
+ *
+ * - For all other exceptions, mtval is 0.
+ */
+uint32_t ibex_mtval_read(void);
+
 #endif  // OPENTITAN_SW_DEVICE_LIB_RUNTIME_IBEX_H_
diff --git a/sw/device/lib/testing/test_framework/FreeRTOSConfig.h b/sw/device/lib/testing/test_framework/FreeRTOSConfig.h
index d8db2ba..501367c 100644
--- a/sw/device/lib/testing/test_framework/FreeRTOSConfig.h
+++ b/sw/device/lib/testing/test_framework/FreeRTOSConfig.h
@@ -1,7 +1,7 @@
 // Copyright lowRISC contributors.
 // Licensed under the Apache License, Version 2.0, see LICENSE for details.
 // SPDX-License-Identifier: Apache-2.0
-//
+
 #ifndef OPENTITAN_SW_DEVICE_LIB_TESTING_TEST_FRAMEWORK_FREERTOSCONFIG_H_
 #define OPENTITAN_SW_DEVICE_LIB_TESTING_TEST_FRAMEWORK_FREERTOSCONFIG_H_
 
@@ -14,7 +14,7 @@
 // guide, since they are specific to FreeRTOS.
 
 // Main FreeRTOS configs.
-#define configUSE_PREEMPTION 0
+#define configUSE_PREEMPTION 1
 #define configUSE_IDLE_HOOK 0
 #define configUSE_TICK_HOOK 0
 #define configTICK_RATE_HZ ((TickType_t)5)
@@ -48,7 +48,7 @@
 // Task priorities. Allow these to be overridden.
 #ifndef uartPRIMARY_PRIORITY
 #define uartPRIMARY_PRIORITY (configMAX_PRIORITIES - 3)
-#endif
+#endif  // uartPRIMARY_PRIORITY
 
 // Set the following definitions to 1 to include the API function, or zero to
 // exclude the API function.
diff --git a/sw/device/lib/testing/test_framework/freertos_port.S b/sw/device/lib/testing/test_framework/freertos_port.S
index a11b1f6..e0ce730 100644
--- a/sw/device/lib/testing/test_framework/freertos_port.S
+++ b/sw/device/lib/testing/test_framework/freertos_port.S
@@ -8,13 +8,327 @@
 
 .extern pxCurrentTCB
 .extern xISRStackTop
+.extern ottf_exception_handler
+.extern ottf_software_isr
+.extern ottf_timer_isr
+.extern ottf_external_isr
 
 // -----------------------------------------------------------------------------
 
 /**
+ * Exception handler.
+ */
+.balign 4
+.global handler_exception
+.type handler_exception, @function
+handler_exception:
+  // Save all registers to the stack.
+  addi sp, sp, -PORT_CONTEXT_SIZE
+  sw   ra,  1 * PORT_WORD_SIZE(sp)
+  sw   t0,  2 * PORT_WORD_SIZE(sp)
+  sw   t1,  3 * PORT_WORD_SIZE(sp)
+  sw   t2,  4 * PORT_WORD_SIZE(sp)
+  sw   s0,  5 * PORT_WORD_SIZE(sp)
+  sw   s1,  6 * PORT_WORD_SIZE(sp)
+  sw   a0,  7 * PORT_WORD_SIZE(sp)
+  sw   a1,  8 * PORT_WORD_SIZE(sp)
+  sw   a2,  9 * PORT_WORD_SIZE(sp)
+  sw   a3, 10 * PORT_WORD_SIZE(sp)
+  sw   a4, 11 * PORT_WORD_SIZE(sp)
+  sw   a5, 12 * PORT_WORD_SIZE(sp)
+  sw   a6, 13 * PORT_WORD_SIZE(sp)
+  sw   a7, 14 * PORT_WORD_SIZE(sp)
+  sw   s2, 15 * PORT_WORD_SIZE(sp)
+  sw   s3, 16 * PORT_WORD_SIZE(sp)
+  sw   s4, 17 * PORT_WORD_SIZE(sp)
+  sw   s5, 18 * PORT_WORD_SIZE(sp)
+  sw   s6, 19 * PORT_WORD_SIZE(sp)
+  sw   s7, 20 * PORT_WORD_SIZE(sp)
+  sw   s8, 21 * PORT_WORD_SIZE(sp)
+  sw   s9, 22 * PORT_WORD_SIZE(sp)
+  sw  s10, 23 * PORT_WORD_SIZE(sp)
+  sw  s11, 24 * PORT_WORD_SIZE(sp)
+  sw   t3, 25 * PORT_WORD_SIZE(sp)
+  sw   t4, 26 * PORT_WORD_SIZE(sp)
+  sw   t5, 27 * PORT_WORD_SIZE(sp)
+  sw   t6, 28 * PORT_WORD_SIZE(sp)
+
+  // Save MSTATUS for the MPIE bit.
+  csrr t0, mstatus
+  sw t0, 29 * PORT_WORD_SIZE(sp)
+
+  // Save MEPC to the stack.
+  // NOTE: this ISQ is synchronous, therefore, we must update the ISR resturn
+  // address to point to the instruction after the one that triggered this IRQ.
+  csrr t0, mepc
+  addi t0, t0, PORT_WORD_SIZE
+  sw t0, 0(sp)
+
+  // Store stack pointer to current TCB.
+  lw t0, pxCurrentTCB
+  sw sp, 0(t0)
+
+  // Jump to the exception handler.
+  jal ottf_exception_handler
+
+  // Return from ISR.
+  j freertosIrqExit
+
+  // Set size so this function can be disassembled.
+  .size handler_exception, .-handler_exception
+
+// -----------------------------------------------------------------------------
+
+/**
+ * Software IRQ handler.
+ */
+.balign 4
+.global handler_irq_software
+.type handler_irq_software, @function
+handler_irq_software:
+  // Save all registers to the stack.
+  addi sp, sp, -PORT_CONTEXT_SIZE
+  sw   ra,  1 * PORT_WORD_SIZE(sp)
+  sw   t0,  2 * PORT_WORD_SIZE(sp)
+  sw   t1,  3 * PORT_WORD_SIZE(sp)
+  sw   t2,  4 * PORT_WORD_SIZE(sp)
+  sw   s0,  5 * PORT_WORD_SIZE(sp)
+  sw   s1,  6 * PORT_WORD_SIZE(sp)
+  sw   a0,  7 * PORT_WORD_SIZE(sp)
+  sw   a1,  8 * PORT_WORD_SIZE(sp)
+  sw   a2,  9 * PORT_WORD_SIZE(sp)
+  sw   a3, 10 * PORT_WORD_SIZE(sp)
+  sw   a4, 11 * PORT_WORD_SIZE(sp)
+  sw   a5, 12 * PORT_WORD_SIZE(sp)
+  sw   a6, 13 * PORT_WORD_SIZE(sp)
+  sw   a7, 14 * PORT_WORD_SIZE(sp)
+  sw   s2, 15 * PORT_WORD_SIZE(sp)
+  sw   s3, 16 * PORT_WORD_SIZE(sp)
+  sw   s4, 17 * PORT_WORD_SIZE(sp)
+  sw   s5, 18 * PORT_WORD_SIZE(sp)
+  sw   s6, 19 * PORT_WORD_SIZE(sp)
+  sw   s7, 20 * PORT_WORD_SIZE(sp)
+  sw   s8, 21 * PORT_WORD_SIZE(sp)
+  sw   s9, 22 * PORT_WORD_SIZE(sp)
+  sw  s10, 23 * PORT_WORD_SIZE(sp)
+  sw  s11, 24 * PORT_WORD_SIZE(sp)
+  sw   t3, 25 * PORT_WORD_SIZE(sp)
+  sw   t4, 26 * PORT_WORD_SIZE(sp)
+  sw   t5, 27 * PORT_WORD_SIZE(sp)
+  sw   t6, 28 * PORT_WORD_SIZE(sp)
+
+  // Save MSTATUS for the MPIE bit.
+  csrr t0, mstatus
+  sw t0, 29 * PORT_WORD_SIZE(sp)
+
+  // Save MEPC to the stack.
+  // NOTE: this ISQ is synchronous, therefore, we must update the ISR resturn
+  // address to point to the instruction after the one that triggered this IRQ.
+  csrr t0, mepc
+  addi t0, t0, PORT_WORD_SIZE
+  sw t0, 0(sp)
+
+  // Store stack pointer to current TCB.
+  lw t0, pxCurrentTCB
+  sw sp, 0(t0)
+
+  // Jump to the software ISR.
+  jal ottf_software_isr
+
+  // Return from ISR.
+  j freertosIrqExit
+
+  // Set size so this function can be disassembled.
+  .size handler_irq_software, .-handler_irq_software
+
+// -----------------------------------------------------------------------------
+
+/**
+ * Timer IRQ handler.
+ */
+.balign 4
+.global handler_irq_timer
+.type handler_irq_timer, @function
+handler_irq_timer:
+  // Save all registers to the stack.
+  addi sp, sp, -PORT_CONTEXT_SIZE
+  sw   ra,  1 * PORT_WORD_SIZE(sp)
+  sw   t0,  2 * PORT_WORD_SIZE(sp)
+  sw   t1,  3 * PORT_WORD_SIZE(sp)
+  sw   t2,  4 * PORT_WORD_SIZE(sp)
+  sw   s0,  5 * PORT_WORD_SIZE(sp)
+  sw   s1,  6 * PORT_WORD_SIZE(sp)
+  sw   a0,  7 * PORT_WORD_SIZE(sp)
+  sw   a1,  8 * PORT_WORD_SIZE(sp)
+  sw   a2,  9 * PORT_WORD_SIZE(sp)
+  sw   a3, 10 * PORT_WORD_SIZE(sp)
+  sw   a4, 11 * PORT_WORD_SIZE(sp)
+  sw   a5, 12 * PORT_WORD_SIZE(sp)
+  sw   a6, 13 * PORT_WORD_SIZE(sp)
+  sw   a7, 14 * PORT_WORD_SIZE(sp)
+  sw   s2, 15 * PORT_WORD_SIZE(sp)
+  sw   s3, 16 * PORT_WORD_SIZE(sp)
+  sw   s4, 17 * PORT_WORD_SIZE(sp)
+  sw   s5, 18 * PORT_WORD_SIZE(sp)
+  sw   s6, 19 * PORT_WORD_SIZE(sp)
+  sw   s7, 20 * PORT_WORD_SIZE(sp)
+  sw   s8, 21 * PORT_WORD_SIZE(sp)
+  sw   s9, 22 * PORT_WORD_SIZE(sp)
+  sw  s10, 23 * PORT_WORD_SIZE(sp)
+  sw  s11, 24 * PORT_WORD_SIZE(sp)
+  sw   t3, 25 * PORT_WORD_SIZE(sp)
+  sw   t4, 26 * PORT_WORD_SIZE(sp)
+  sw   t5, 27 * PORT_WORD_SIZE(sp)
+  sw   t6, 28 * PORT_WORD_SIZE(sp)
+
+  // Save MSTATUS for the MPIE bit.
+  csrr t0, mstatus
+  sw t0, 29 * PORT_WORD_SIZE(sp)
+
+  // Save MEPC to the stack.
+  // NOTE: this ISQ is asynchronous, therefore, we do not need to modify MEPC.
+  csrr t0, mepc
+  sw t0, 0(sp)
+
+  // Store stack pointer to current TCB.
+  lw t0, pxCurrentTCB
+  sw sp, 0(t0)
+
+  // Jump to timer ISR.
+  jal ottf_timer_isr
+
+  // Return from ISR.
+  j freertosIrqExit
+
+  // Set size so this function can be disassembled.
+  .size handler_irq_timer, .-handler_irq_timer
+
+// -----------------------------------------------------------------------------
+
+/**
+ * External IRQ handler.
+ */
+.balign 4
+.global handler_irq_external
+.type handler_irq_external, @function
+handler_irq_external:
+  // Save all registers to the stack.
+  addi sp, sp, -PORT_CONTEXT_SIZE
+  sw   ra,  1 * PORT_WORD_SIZE(sp)
+  sw   t0,  2 * PORT_WORD_SIZE(sp)
+  sw   t1,  3 * PORT_WORD_SIZE(sp)
+  sw   t2,  4 * PORT_WORD_SIZE(sp)
+  sw   s0,  5 * PORT_WORD_SIZE(sp)
+  sw   s1,  6 * PORT_WORD_SIZE(sp)
+  sw   a0,  7 * PORT_WORD_SIZE(sp)
+  sw   a1,  8 * PORT_WORD_SIZE(sp)
+  sw   a2,  9 * PORT_WORD_SIZE(sp)
+  sw   a3, 10 * PORT_WORD_SIZE(sp)
+  sw   a4, 11 * PORT_WORD_SIZE(sp)
+  sw   a5, 12 * PORT_WORD_SIZE(sp)
+  sw   a6, 13 * PORT_WORD_SIZE(sp)
+  sw   a7, 14 * PORT_WORD_SIZE(sp)
+  sw   s2, 15 * PORT_WORD_SIZE(sp)
+  sw   s3, 16 * PORT_WORD_SIZE(sp)
+  sw   s4, 17 * PORT_WORD_SIZE(sp)
+  sw   s5, 18 * PORT_WORD_SIZE(sp)
+  sw   s6, 19 * PORT_WORD_SIZE(sp)
+  sw   s7, 20 * PORT_WORD_SIZE(sp)
+  sw   s8, 21 * PORT_WORD_SIZE(sp)
+  sw   s9, 22 * PORT_WORD_SIZE(sp)
+  sw  s10, 23 * PORT_WORD_SIZE(sp)
+  sw  s11, 24 * PORT_WORD_SIZE(sp)
+  sw   t3, 25 * PORT_WORD_SIZE(sp)
+  sw   t4, 26 * PORT_WORD_SIZE(sp)
+  sw   t5, 27 * PORT_WORD_SIZE(sp)
+  sw   t6, 28 * PORT_WORD_SIZE(sp)
+
+  // Save MSTATUS for the MPIE bit.
+  csrr t0, mstatus
+  sw t0, 29 * PORT_WORD_SIZE(sp)
+
+  // Save MEPC to the stack.
+  // NOTE: this ISQ is asynchronous, therefore, we do not need to modify MEPC.
+  csrr t0, mepc
+  sw t0, 0(sp)
+
+  // Store stack pointer to current TCB.
+  lw t0, pxCurrentTCB
+  sw sp, 0(t0)
+
+  // Jump to external ISR.
+  jal ottf_external_isr
+
+  // Return from ISR.
+  j freertosIrqExit
+
+  // Set size so this function can be disassembled.
+  .size handler_irq_external, .-handler_irq_external
+
+// -----------------------------------------------------------------------------
+
+/**
+ * ISR exit sub-routine restores registers from the stack.
+ */
+.balign 4
+.global freertosIrqExit
+.type freertosIrqExit, @function
+freertosIrqExit:
+  // Load the stack pointer for the current TCB.
+	lw  t1, pxCurrentTCB
+	lw  sp, 0(t1)
+
+  // Load the correct MEPC for the next instruction in the current task.
+	lw t0, 0(sp)
+	csrw mepc, t0
+
+  // Load MSTATUS for the MPIE bit.
+	lw  t0, 29 * PORT_WORD_SIZE(sp)
+	csrw mstatus, t0
+
+  // Restore all registers from the stack.
+  lw   ra,  1 * PORT_WORD_SIZE(sp)
+  lw   t0,  2 * PORT_WORD_SIZE(sp)
+  lw   t1,  3 * PORT_WORD_SIZE(sp)
+  lw   t2,  4 * PORT_WORD_SIZE(sp)
+  lw   s0,  5 * PORT_WORD_SIZE(sp)
+  lw   s1,  6 * PORT_WORD_SIZE(sp)
+  lw   a0,  7 * PORT_WORD_SIZE(sp)
+  lw   a1,  8 * PORT_WORD_SIZE(sp)
+  lw   a2,  9 * PORT_WORD_SIZE(sp)
+  lw   a3, 10 * PORT_WORD_SIZE(sp)
+  lw   a4, 11 * PORT_WORD_SIZE(sp)
+  lw   a5, 12 * PORT_WORD_SIZE(sp)
+  lw   a6, 13 * PORT_WORD_SIZE(sp)
+  lw   a7, 14 * PORT_WORD_SIZE(sp)
+  lw   s2, 15 * PORT_WORD_SIZE(sp)
+  lw   s3, 16 * PORT_WORD_SIZE(sp)
+  lw   s4, 17 * PORT_WORD_SIZE(sp)
+  lw   s5, 18 * PORT_WORD_SIZE(sp)
+  lw   s6, 19 * PORT_WORD_SIZE(sp)
+  lw   s7, 20 * PORT_WORD_SIZE(sp)
+  lw   s8, 21 * PORT_WORD_SIZE(sp)
+  lw   s9, 22 * PORT_WORD_SIZE(sp)
+  lw  s10, 23 * PORT_WORD_SIZE(sp)
+  lw  s11, 24 * PORT_WORD_SIZE(sp)
+  lw   t3, 25 * PORT_WORD_SIZE(sp)
+  lw   t4, 26 * PORT_WORD_SIZE(sp)
+  lw   t5, 27 * PORT_WORD_SIZE(sp)
+  lw   t6, 28 * PORT_WORD_SIZE(sp)
+  addi sp, sp, PORT_CONTEXT_SIZE
+
+  // This exits the ISR completely, and does not return control flow to the ISR
+  // that called this sub-routine.
+  mret
+
+  // Set size so this function can be disassembled.
+  .size freertosIrqExit, .-freertosIrqExit
+// -----------------------------------------------------------------------------
+
+/**
  * FreeRTOS, expects this function to exist and uses it to start the first task. 
  */
-.balign 8
+.balign 4
 .global xPortStartFirstTask
 .type xPortStartFirstTask, @function
 xPortStartFirstTask:
@@ -133,7 +447,7 @@
  *      0 - (pxCode)
  * -----Stack Top----
  */
-.balign 8
+.balign 4
 .global pxPortInitialiseStack
 .type pxPortInitialiseStack, @function
 pxPortInitialiseStack:
diff --git a/sw/device/lib/testing/test_framework/freertos_port.c b/sw/device/lib/testing/test_framework/freertos_port.c
index f5f155c..c30d346 100644
--- a/sw/device/lib/testing/test_framework/freertos_port.c
+++ b/sw/device/lib/testing/test_framework/freertos_port.c
@@ -2,13 +2,12 @@
 // Licensed under the Apache License, Version 2.0, see LICENSE for details.
 // SPDX-License-Identifier: Apache-2.0
 
-#include "sw/device/lib/base/csr.h"
-#include "sw/device/lib/base/memory.h"
 #include "sw/device/lib/dif/dif_rv_timer.h"
-#include "sw/device/lib/handler.h"
 #include "sw/device/lib/irq.h"
+#include "sw/device/lib/runtime/hart.h"
 #include "sw/device/lib/runtime/log.h"
 #include "sw/device/lib/testing/check.h"
+#include "sw/device/lib/testing/test_framework/FreeRTOSConfig.h"
 #include "sw/vendor/freertos_freertos_kernel/include/FreeRTOS.h"
 #include "sw/vendor/freertos_freertos_kernel/include/task.h"
 #include "sw/vendor/freertos_freertos_kernel/portable/GCC/RISC-V/portmacro.h"
@@ -21,14 +20,29 @@
 // functions.
 
 // ----------------------------------------------------------------------------
-// Timer.
+// Timer Setup (for use when preemptive scheduling is enabled)
 // ----------------------------------------------------------------------------
+#if configUSE_PREEMPTION
+
 static dif_rv_timer_t timer;
 static const uint32_t kTimerHartId = (uint32_t)kTopEarlgreyPlicTargetIbex0;
 static const uint32_t kTimerComparatorId = 0;
 static const uint64_t kTimerDeadline =
     100;  // Counter must reach 100 for an IRQ to be triggered.
 
+// Override the timer ISR to support preemptive context switching.
+void ottf_timer_isr(void) {
+  LOG_INFO("Handling timer IQR ...");
+  CHECK(dif_rv_timer_irq_disable(&timer, kTimerHartId, NULL) == kDifRvTimerOk);
+  CHECK(dif_rv_timer_counter_write(&timer, kTimerHartId, 0) == kDifRvTimerOk);
+  CHECK(dif_rv_timer_irq_clear(&timer, kTimerHartId, kTimerComparatorId) ==
+        kDifRvTimerOk);
+  // TODO: increment scheduler tick and switch context if necessary
+  CHECK(dif_rv_timer_irq_enable(&timer, kTimerHartId, kTimerComparatorId,
+                                kDifRvTimerEnabled) == kDifRvTimerOk);
+  LOG_INFO("Done.");
+}
+
 void vPortSetupTimerInterrupt(void) {
   LOG_INFO("Configuring timer interrupt ...");
 
@@ -61,13 +75,17 @@
                                          kDifRvTimerEnabled) == kDifRvTimerOk);
 }
 
+#endif  // configUSE_PREEMPTION
+
 // ----------------------------------------------------------------------------
-// Scheduler.
+// Scheduler Setup
 // ----------------------------------------------------------------------------
 extern void xPortStartFirstTask(void);
 
 BaseType_t xPortStartScheduler(void) {
+#if configUSE_PREEMPTION
   vPortSetupTimerInterrupt();
+#endif  // configUSE_PREEMPTION
   irq_timer_ctrl(true);
   irq_external_ctrl(true);
   irq_software_ctrl(true);
@@ -85,18 +103,3 @@
     wait_for_interrupt();
   }
 }
-
-// ----------------------------------------------------------------------------
-// ISRs.
-// TODO: add support for remaining ISRs.
-// ----------------------------------------------------------------------------
-void handler_irq_timer(void) {
-  LOG_INFO("Handling timer IQR ...");
-  CHECK(dif_rv_timer_irq_disable(&timer, kTimerHartId, NULL) == kDifRvTimerOk);
-  CHECK(dif_rv_timer_counter_write(&timer, kTimerHartId, 0) == kDifRvTimerOk);
-  CHECK(dif_rv_timer_irq_clear(&timer, kTimerHartId, kTimerComparatorId) ==
-        kDifRvTimerOk);
-  CHECK(dif_rv_timer_irq_enable(&timer, kTimerHartId, kTimerComparatorId,
-                                kDifRvTimerEnabled) == kDifRvTimerOk);
-  LOG_INFO("Done.");
-}
diff --git a/sw/device/lib/testing/test_framework/meson.build b/sw/device/lib/testing/test_framework/meson.build
index b3b1092..ee1425a 100644
--- a/sw/device/lib/testing/test_framework/meson.build
+++ b/sw/device/lib/testing/test_framework/meson.build
@@ -78,6 +78,7 @@
 # OpenTitan Test Framework (OTTF) sources & includes.
 ottf_sources = [
   'ottf.c',
+  'ottf_isrs.c',
   'example_earlgrey_test.c',
   'freertos_hooks.c',
   'freertos_port.S',
@@ -103,6 +104,7 @@
     ],
     dependencies: [
       sw_lib_mem,
+      sw_lib_runtime_ibex,
       sw_lib_runtime_hart,
       sw_lib_runtime_log,
       sw_lib_runtime_print,
diff --git a/sw/device/lib/testing/test_framework/ottf_isrs.c b/sw/device/lib/testing/test_framework/ottf_isrs.c
new file mode 100644
index 0000000..3e302e2
--- /dev/null
+++ b/sw/device/lib/testing/test_framework/ottf_isrs.c
@@ -0,0 +1,101 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+#include "sw/device/lib/testing/test_framework/ottf_isrs.h"
+
+#include "sw/device/lib/base/csr.h"
+#include "sw/device/lib/base/macros.h"
+#include "sw/device/lib/runtime/hart.h"
+#include "sw/device/lib/runtime/ibex.h"
+#include "sw/device/lib/runtime/log.h"
+#include "sw/device/lib/testing/test_framework/FreeRTOSConfig.h"
+
+OT_ATTR_WEAK void ottf_exception_handler(void) {
+  uint32_t mcause = ibex_mcause_read();
+
+  switch ((ottf_exc_id_t)(mcause & kIdMax)) {
+    case kInstrMisaligned:
+      ottf_instr_misaligned_fault();
+      break;
+    case kInstrAccessFault:
+      ottf_instr_access_fault();
+      break;
+    case kIllegalInstrFault:
+      ottf_illegal_instr_fault();
+      break;
+    case kBreakpoint:
+      ottf_breakpoint();
+      break;
+    case kLoadAccessFault:
+      ottf_load_store_fault();
+      break;
+    case kStoreAccessFault:
+      ottf_load_store_fault();
+      break;
+    case kMachineECall:
+      ottf_machine_ecall();
+      break;
+    case kUserECall:
+      ottf_user_ecall();
+      break;
+    default:
+      LOG_INFO("Unknown exception triggered!");
+      abort();
+  }
+}
+
+OT_ATTR_WEAK void ottf_instr_misaligned_fault(void) {
+  LOG_INFO("Misaligned instruction, mtval holds the fault address.");
+  LOG_INFO("MTVAL value is 0x%x", ibex_mtval_read());
+  abort();
+}
+
+OT_ATTR_WEAK void ottf_instr_access_fault(void) {
+  LOG_INFO("Instruction access fault, mtval holds the fault address.");
+  LOG_INFO("MTVAL value is 0x%x", ibex_mtval_read());
+  abort();
+}
+
+OT_ATTR_WEAK void ottf_illegal_instr_fault(void) {
+  LOG_INFO("Illegal instruction fault, mtval shows the faulting instruction.");
+  LOG_INFO("MTVAL value is 0x%x", ibex_mtval_read());
+  abort();
+}
+
+OT_ATTR_WEAK void ottf_breakpoint(void) {
+  LOG_INFO("Breakpoint triggered, mtval holds the breakpoint address.");
+  LOG_INFO("MTVAL value is 0x%x", ibex_mtval_read());
+  abort();
+}
+
+OT_ATTR_WEAK void ottf_load_store_fault(void) {
+  LOG_INFO("Load/Store fault, mtval holds the fault address.");
+  LOG_INFO("MTVAL value is 0x%x", ibex_mtval_read());
+  abort();
+}
+
+OT_ATTR_WEAK void ottf_machine_ecall(void) {
+  LOG_INFO("Machine-mode environment call (syscall).");
+  abort();
+}
+
+OT_ATTR_WEAK void ottf_user_ecall(void) {
+  LOG_INFO("User-mode environment call (syscall).");
+  abort();
+}
+
+OT_ATTR_WEAK void ottf_software_isr(void) {
+  LOG_INFO("Software IRQ triggered!");
+  abort();
+}
+
+OT_ATTR_WEAK void ottf_timer_isr(void) {
+  LOG_INFO("Timer IRQ triggered!");
+  abort();
+}
+
+OT_ATTR_WEAK void ottf_external_isr(void) {
+  LOG_INFO("External IRQ triggered!");
+  abort();
+}
diff --git a/sw/device/lib/testing/test_framework/ottf_isrs.h b/sw/device/lib/testing/test_framework/ottf_isrs.h
new file mode 100644
index 0000000..e76487f
--- /dev/null
+++ b/sw/device/lib/testing/test_framework/ottf_isrs.h
@@ -0,0 +1,134 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef OPENTITAN_SW_DEVICE_LIB_TESTING_TEST_FRAMEWORK_OTTF_ISRS_H_
+#define OPENTITAN_SW_DEVICE_LIB_TESTING_TEST_FRAMEWORK_OTTF_ISRS_H_
+
+/**
+ * An OTTF exception type.
+ *
+ * This enum is used to decode RISC-V exception causes generated by Ibex.
+ */
+typedef enum ottf_exc_id {
+  kInstrMisaligned = 0,
+  kInstrAccessFault = 1,
+  kIllegalInstrFault = 2,
+  kBreakpoint = 3,
+  kLoadAccessFault = 5,
+  kStoreAccessFault = 7,
+  kUserECall = 8,
+  kMachineECall = 11,
+  kIdMax = 31
+} ottf_exc_id_t;
+
+/**
+ * OTTF exception handler.
+ *
+ * `ottf_isrs.c` provides a weak definition of this symbol, which can be
+ * overriden at link-time by providing an additional non-weak definition.
+ */
+void ottf_exception_handler(void);
+
+/**
+ * OTTF instruction misaligned fault handler.
+ *
+ * Called by default implementation of `ottf_exception_handler`. If that
+ * function is overriden, this function may not be called.
+ *
+ * `ottf_isrs.c` provides a weak definition of this symbol, which can be
+ * overriden at link-time by providing an additional non-weak definition.
+ */
+void ottf_instr_misaligned_fault(void);
+
+/**
+ * OTTF instruction access fault handler.
+ *
+ * Called by default implementation of `ottf_exception_handler`. If that
+ * function is overriden, this function may not be called.
+ *
+ * `ottf_isrs.c` provides a weak definition of this symbol, which can be
+ * overriden at link-time by providing an additional non-weak definition.
+ */
+void ottf_instr_access_fault(void);
+
+/**
+ * OTTF illegal instruction fault handler.
+ *
+ * Called by default implementation of `ottf_exception_handler`. If that
+ * function is overriden, this function may not be called.
+ *
+ * `ottf_isrs.c` provides a weak definition of this symbol, which can be
+ * overriden at link-time by providing an additional non-weak definition.
+ */
+void ottf_illegal_instr_fault(void);
+
+/**
+ * OTTF breakpoint handler.
+ *
+ * Called by default implementation of `ottf_exception_handler`. If that
+ * function is overriden, this function may not be called.
+ *
+ * `ottf_isrs.c` provides a weak definition of this symbol, which can be
+ * overriden at link-time by providing an additional non-weak definition.
+ */
+void ottf_breakpoint(void);
+
+/**
+ * OTTF load/store fault handler.
+ *
+ * Called by default implementation of `ottf_exception_handler`. If that
+ * function is overriden, this function may not be called.
+ *
+ * `ottf_isrs.c` provides a weak definition of this symbol, which can be
+ * overriden at link-time by providing an additional non-weak definition.
+ */
+void ottf_load_store_fault(void);
+
+/**
+ * OTTF machine-mode evironment call handler.
+ *
+ * Called by default implementation of `ottf_exception_handler`. If that
+ * function is overriden, this function may not be called.
+ *
+ * `ottf_isrs.c` provides a weak definition of this symbol, which can be
+ * overriden at link-time by providing an additional non-weak definition.
+ */
+void ottf_machine_ecall(void);
+
+/**
+ * OTTF user-mode evironment call handler.
+ *
+ * Called by default implementation of `ottf_exception_handler`. If that
+ * function is overriden, this function may not be called.
+ *
+ * `ottf_isrs.c` provides a weak definition of this symbol, which can be
+ * overriden at link-time by providing an additional non-weak definition.
+ */
+void ottf_user_ecall(void);
+
+/**
+ * OTTF software IRQ handler.
+ *
+ * `ottf_isrs.c` provides a weak definition of this symbol, which can be
+ * overriden at link-time by providing an additional non-weak definition.
+ */
+void ottf_software_isr(void);
+
+/**
+ * OTTF timer IRQ handler.
+ *
+ * `ottf_isrs.c` provides a weak definition of this symbol, which can be
+ * overriden at link-time by providing an additional non-weak definition.
+ */
+void ottf_timer_isr(void);
+
+/**
+ * OTTF external IRQ handler.
+ *
+ * `ottf_isrs.c` provides a weak definition of this symbol, which can be
+ * overriden at link-time by providing an additional non-weak definition.
+ */
+void ottf_external_isr(void);
+
+#endif  // OPENTITAN_SW_DEVICE_LIB_TESTING_TEST_FRAMEWORK_OTTF_ISRS_H_