diff --git a/src/core/bios.cpp b/src/core/bios.cpp
index 23d5616b1..921fb8842 100644
--- a/src/core/bios.cpp
+++ b/src/core/bios.cpp
@@ -183,14 +183,6 @@ void BIOS::PatchBIOS(u8* image, u32 image_size, u32 address, u32 value, u32 mask
                 old_disasm.GetCharArray(), new_value, new_disasm.GetCharArray());
 }
 
-bool BIOS::PatchBIOSEnableTTY(u8* image, u32 image_size)
-{
-  Log_InfoPrintf("Patching BIOS to enable TTY/printf");
-  PatchBIOS(image, image_size, 0x1FC06F0C, 0x24010001);
-  PatchBIOS(image, image_size, 0x1FC06F14, 0xAF81A9C0);
-  return true;
-}
-
 bool BIOS::PatchBIOSFastBoot(u8* image, u32 image_size)
 {
   // Replace the shell entry point with a return back to the bootstrap.
diff --git a/src/core/bios.h b/src/core/bios.h
index caa4d026b..55e38a954 100644
--- a/src/core/bios.h
+++ b/src/core/bios.h
@@ -67,7 +67,6 @@ bool IsValidBIOSForRegion(ConsoleRegion console_region, ConsoleRegion bios_regio
 
 void PatchBIOS(u8* image, u32 image_size, u32 address, u32 value, u32 mask = UINT32_C(0xFFFFFFFF));
 
-bool PatchBIOSEnableTTY(u8* image, u32 image_size);
 bool PatchBIOSFastBoot(u8* image, u32 image_size);
 bool PatchBIOSForEXE(u8* image, u32 image_size, u32 r_pc, u32 r_gp, u32 r_sp, u32 r_fp);
 
diff --git a/src/core/bus.cpp b/src/core/bus.cpp
index a02bedc94..143a3459c 100644
--- a/src/core/bus.cpp
+++ b/src/core/bus.cpp
@@ -20,6 +20,7 @@
 #include "settings.h"
 #include "sio.h"
 #include "spu.h"
+#include "system.h"
 #include "timers.h"
 #include "timing_event.h"
 #include "util/state_wrapper.h"
@@ -191,6 +192,35 @@ void Reset()
   RecalculateMemoryTimings();
 }
 
+void AddTTYCharacter(char ch)
+{
+  if (ch == '\r')
+  {
+  }
+  else if (ch == '\n')
+  {
+    if (!m_tty_line_buffer.empty())
+    {
+      Log::Writef("TTY", "", LOGLEVEL_INFO, "\033[1;34m%s\033[0m", m_tty_line_buffer.c_str());
+#ifdef _DEBUG
+      if (CPU::IsTraceEnabled())
+        CPU::WriteToExecutionLog("TTY: %s\n", m_tty_line_buffer.c_str());
+#endif
+    }
+    m_tty_line_buffer.clear();
+  }
+  else
+  {
+    m_tty_line_buffer += ch;
+  }
+}
+
+void AddTTYString(const std::string_view& str)
+{
+  for (char ch : str)
+    AddTTYCharacter(ch);
+}
+
 bool DoState(StateWrapper& sw)
 {
   u32 ram_size = g_ram_size;
@@ -974,25 +1004,7 @@ static TickCount DoEXP2Access(u32 offset, u32& value)
   {
     if (offset == 0x23 || offset == 0x80)
     {
-      if (value == '\r')
-      {
-      }
-      else if (value == '\n')
-      {
-        if (!m_tty_line_buffer.empty())
-        {
-          Log_InfoPrintf("TTY: %s", m_tty_line_buffer.c_str());
-#ifdef _DEBUG
-          if (CPU::IsTraceEnabled())
-            CPU::WriteToExecutionLog("TTY: %s\n", m_tty_line_buffer.c_str());
-#endif
-        }
-        m_tty_line_buffer.clear();
-      }
-      else
-      {
-        m_tty_line_buffer += static_cast<char>(Truncate8(value));
-      }
+      AddTTYCharacter(static_cast<char>(value));
     }
     else if (offset == 0x41 || offset == 0x42)
     {
@@ -1002,6 +1014,23 @@ static TickCount DoEXP2Access(u32 offset, u32& value)
     {
       Log_DevPrintf("BIOS POST2 status: %02X", value & UINT32_C(0x0F));
     }
+#if 0
+    // TODO: Put behind configuration variable
+    else if (offset == 0x81)
+    {
+      Log_WarningPrintf("pcsx_debugbreak()");
+      Host::ReportErrorAsync("Error", "pcsx_debugbreak()");
+      System::PauseSystem(true);
+      CPU::ExitExecution();
+    }
+    else if (offset == 0x82)
+    {
+      Log_WarningPrintf("pcsx_exit() with status 0x%02X", value & UINT32_C(0xFF));
+      Host::ReportErrorAsync("Error", fmt::format("pcsx_exit() with status 0x{:02X}", value & UINT32_C(0xFF)));
+      System::ShutdownSystem(false);
+      CPU::ExitExecution();
+    }
+#endif
     else
     {
       Log_WarningPrintf("EXP2 write: 0x%08X <- 0x%08X", EXP2_BASE | offset, value);
diff --git a/src/core/bus.h b/src/core/bus.h
index 09469b48b..1ffa62116 100644
--- a/src/core/bus.h
+++ b/src/core/bus.h
@@ -9,6 +9,7 @@
 #include <bitset>
 #include <optional>
 #include <string>
+#include <string_view>
 #include <vector>
 
 class StateWrapper;
@@ -180,4 +181,8 @@ u8* GetMemoryRegionPointer(MemoryRegion region);
 std::optional<PhysicalMemoryAddress> SearchMemory(PhysicalMemoryAddress start_address, const u8* pattern,
                                                   const u8* mask, u32 pattern_length);
 
+// TTY Logging.
+void AddTTYCharacter(char ch);
+void AddTTYString(const std::string_view& str);
+
 } // namespace Bus
diff --git a/src/core/cpu_core.cpp b/src/core/cpu_core.cpp
index ef3d31b10..2c2620565 100644
--- a/src/core/cpu_core.cpp
+++ b/src/core/cpu_core.cpp
@@ -618,6 +618,70 @@ static void LogInstruction(u32 bits, u32 pc, Registers* regs)
   WriteToExecutionLog("%08x: %08x %s\n", pc, bits, instr.GetCharArray());
 }
 
+static void HandleWriteSyscall()
+{
+  const auto& regs = g_state.regs;
+  if (regs.a0 != 1) // stdout
+    return;
+
+  u32 addr = regs.a1;
+  const u32 count = regs.a2;
+  for (u32 i = 0; i < count; i++)
+  {
+    u8 value;
+    if (!SafeReadMemoryByte(addr++, &value) || value == 0)
+      break;
+
+    Bus::AddTTYCharacter(static_cast<char>(value));
+  }
+}
+
+static void HandlePutcSyscall()
+{
+  const auto& regs = g_state.regs;
+  if (regs.a0 != 0)
+    Bus::AddTTYCharacter(static_cast<char>(regs.a0));
+}
+
+static void HandlePutsSyscall()
+{
+  const auto& regs = g_state.regs;
+
+  u32 addr = regs.a1;
+  for (u32 i = 0; i < 1024; i++)
+  {
+    u8 value;
+    if (!SafeReadMemoryByte(addr++, &value) || value == 0)
+      break;
+
+    Bus::AddTTYCharacter(static_cast<char>(value));
+  }
+}
+
+void HandleA0Syscall()
+{
+  const auto& regs = g_state.regs;
+  const u32 call = regs.t1;
+  if (call == 0x03)
+    HandleWriteSyscall();
+  else if (call == 0x09 || call == 0x3c)
+    HandlePutcSyscall();
+  else if (call == 0x3e)
+    HandlePutsSyscall();
+}
+
+void HandleB0Syscall()
+{
+  const auto& regs = g_state.regs;
+  const u32 call = regs.t1;
+  if (call == 0x35)
+    HandleWriteSyscall();
+  else if (call == 0x3b || call == 0x3d)
+    HandlePutcSyscall();
+  else if (call == 0x3f)
+    HandlePutsSyscall();
+}
+
 const std::array<DebuggerRegisterListEntry, NUM_DEBUGGER_REGISTER_LIST_ENTRIES> g_debugger_register_list = {
   {{"zero", &CPU::g_state.regs.zero},
    {"at", &CPU::g_state.regs.at},
@@ -1764,7 +1828,9 @@ void UpdateDebugDispatcherFlag()
   const bool has_cop0_breakpoints =
     dcic.super_master_enable_1 && dcic.super_master_enable_2 && dcic.execution_breakpoint_enable;
 
-  const bool use_debug_dispatcher = has_any_breakpoints || has_cop0_breakpoints || s_trace_to_log;
+  const bool use_debug_dispatcher =
+    has_any_breakpoints || has_cop0_breakpoints || s_trace_to_log ||
+    (g_settings.cpu_execution_mode == CPUExecutionMode::Interpreter && g_settings.bios_tty_logging);
   if (use_debug_dispatcher == g_state.use_debug_dispatcher)
     return;
 
@@ -2056,6 +2122,11 @@ template<PGXPMode pgxp_mode, bool debug>
       {
         if (s_trace_to_log)
           LogInstruction(g_state.current_instruction.bits, g_state.current_instruction_pc, &g_state.regs);
+
+        if (UNLIKELY(g_state.current_instruction_pc == 0xA0))
+          HandleA0Syscall();
+        else if (UNLIKELY(g_state.current_instruction_pc == 0xB0))
+          HandleB0Syscall();
       }
 
 #if 0 // GTE flag test debugging
diff --git a/src/core/cpu_core_private.h b/src/core/cpu_core_private.h
index 6b44713eb..3f779fe8e 100644
--- a/src/core/cpu_core_private.h
+++ b/src/core/cpu_core_private.h
@@ -126,4 +126,8 @@ ALWAYS_INLINE static void StallUntilGTEComplete()
     (g_state.gte_completion_tick > g_state.pending_ticks) ? g_state.gte_completion_tick : g_state.pending_ticks;
 }
 
+// kernel call interception
+void HandleA0Syscall();
+void HandleB0Syscall();
+
 } // namespace CPU
\ No newline at end of file
diff --git a/src/core/cpu_recompiler_code_generator.cpp b/src/core/cpu_recompiler_code_generator.cpp
index 20f275df4..cc205e714 100644
--- a/src/core/cpu_recompiler_code_generator.cpp
+++ b/src/core/cpu_recompiler_code_generator.cpp
@@ -969,6 +969,14 @@ void CodeGenerator::BlockPrologue()
 
   EmitStoreCPUStructField(offsetof(State, exception_raised), Value::FromConstantU8(0));
 
+  if (g_settings.bios_tty_logging)
+  {
+    if (m_pc == 0xa0)
+      EmitFunctionCall(nullptr, &CPU::HandleA0Syscall);
+    else if (m_pc == 0xb0)
+      EmitFunctionCall(nullptr, &CPU::HandleB0Syscall);
+  }
+
 #if 0
   EmitFunctionCall(nullptr, &Thunks::LogPC, Value::FromConstantU32(m_pc));
 #endif
diff --git a/src/core/fullscreen_ui.cpp b/src/core/fullscreen_ui.cpp
index f32d98ff4..f4698a601 100644
--- a/src/core/fullscreen_ui.cpp
+++ b/src/core/fullscreen_ui.cpp
@@ -2871,9 +2871,9 @@ void FullscreenUI::DrawBIOSSettingsPage()
   DrawToggleSetting(bsi, FSUI_CSTR("Enable Fast Boot"),
                     FSUI_CSTR("Patches the BIOS to skip the boot animation. Safe to enable."), "BIOS", "PatchFastBoot",
                     Settings::DEFAULT_FAST_BOOT_VALUE);
-  DrawToggleSetting(bsi, FSUI_CSTR("Enable TTY Output"),
-                    FSUI_CSTR("Patches the BIOS to log calls to printf(). Only use when debugging, can break games."),
-                    "BIOS", "PatchTTYEnable", false);
+  DrawToggleSetting(bsi, FSUI_CSTR("Enable TTY Logging"),
+                    FSUI_CSTR("Logs BIOS calls to printf(). Not all games contain debugging messages."),
+                    "BIOS", "TTYLogging", false);
 
   EndMenuButtons();
 }
diff --git a/src/core/settings.cpp b/src/core/settings.cpp
index 9b91bb1fa..2fdaec5ef 100644
--- a/src/core/settings.cpp
+++ b/src/core/settings.cpp
@@ -317,7 +317,7 @@ void Settings::Load(SettingsInterface& si)
   gpu_fifo_size = static_cast<u32>(si.GetIntValue("Hacks", "GPUFIFOSize", DEFAULT_GPU_FIFO_SIZE));
   gpu_max_run_ahead = si.GetIntValue("Hacks", "GPUMaxRunAhead", DEFAULT_GPU_MAX_RUN_AHEAD);
 
-  bios_patch_tty_enable = si.GetBoolValue("BIOS", "PatchTTYEnable", false);
+  bios_tty_logging = si.GetBoolValue("BIOS", "TTYLogging", false);
   bios_patch_fast_boot = si.GetBoolValue("BIOS", "PatchFastBoot", DEFAULT_FAST_BOOT_VALUE);
 
   multitap_mode =
@@ -530,7 +530,7 @@ void Settings::Save(SettingsInterface& si) const
   si.SetBoolValue("PCDrv", "EnableWrites", pcdrv_enable_writes);
   si.SetStringValue("PCDrv", "Root", pcdrv_root.c_str());
 
-  si.SetBoolValue("BIOS", "PatchTTYEnable", bios_patch_tty_enable);
+  si.SetBoolValue("BIOS", "TTYLogging", bios_tty_logging);
   si.SetBoolValue("BIOS", "PatchFastBoot", bios_patch_fast_boot);
 
   for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++)
@@ -619,7 +619,7 @@ void Settings::FixIncompatibleSettings(bool display_osd_messages)
     g_settings.use_old_mdec_routines = false;
     g_settings.pcdrv_enable = false;
     g_settings.bios_patch_fast_boot = false;
-    g_settings.bios_patch_tty_enable = false;
+    g_settings.bios_tty_logging = false;
   }
 
   if (g_settings.pcdrv_enable && g_settings.pcdrv_root.empty())
diff --git a/src/core/settings.h b/src/core/settings.h
index f423dfbe1..9e39f75a9 100644
--- a/src/core/settings.h
+++ b/src/core/settings.h
@@ -228,7 +228,7 @@ struct Settings
     }
   } texture_replacements;
 
-  bool bios_patch_tty_enable = false;
+  bool bios_tty_logging = false;
   bool bios_patch_fast_boot = DEFAULT_FAST_BOOT_VALUE;
   bool enable_8mb_ram = false;
 
diff --git a/src/core/system.cpp b/src/core/system.cpp
index 0842bc357..9117f5cad 100644
--- a/src/core/system.cpp
+++ b/src/core/system.cpp
@@ -339,7 +339,7 @@ bool System::IsValid()
 
 bool System::IsExecuting()
 {
-  DebugAssert(IsValid());
+  DebugAssert(s_state != State::Shutdown);
   return s_system_executing;
 }
 
@@ -1389,15 +1389,6 @@ bool System::BootSystem(SystemBootParameters parameters)
   UpdateMultitaps();
   InternalReset();
 
-  // Enable tty by patching bios.
-  if (g_settings.bios_patch_tty_enable)
-  {
-    if (s_bios_image_info && s_bios_image_info->patch_compatible)
-      BIOS::PatchBIOSEnableTTY(Bus::g_bios, Bus::BIOS_SIZE);
-    else
-      Log_ErrorPrintf("Not patching TTY enable, as BIOS is not patch compatible.");
-  }
-
   // Load EXE late after BIOS.
   if (!exe_boot.empty() && !LoadEXE(exe_boot.c_str()))
   {
@@ -3556,7 +3547,8 @@ void System::CheckForSettingsChanges(const Settings& old_settings)
     if (g_settings.cpu_execution_mode == CPUExecutionMode::Recompiler &&
         (g_settings.cpu_recompiler_memory_exceptions != old_settings.cpu_recompiler_memory_exceptions ||
          g_settings.cpu_recompiler_block_linking != old_settings.cpu_recompiler_block_linking ||
-         g_settings.cpu_recompiler_icache != old_settings.cpu_recompiler_icache))
+         g_settings.cpu_recompiler_icache != old_settings.cpu_recompiler_icache ||
+         g_settings.bios_tty_logging != old_settings.bios_tty_logging))
     {
       Host::AddOSDMessage(TRANSLATE_STR("OSDMessage", "Recompiler options changed, flushing all blocks."), 5.0f);
 
diff --git a/src/duckstation-qt/biossettingswidget.cpp b/src/duckstation-qt/biossettingswidget.cpp
index 33558effb..4ebc7aa7a 100644
--- a/src/duckstation-qt/biossettingswidget.cpp
+++ b/src/duckstation-qt/biossettingswidget.cpp
@@ -16,15 +16,15 @@ BIOSSettingsWidget::BIOSSettingsWidget(SettingsDialog* dialog, QWidget* parent)
 
   m_ui.setupUi(this);
 
-  SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.enableTTYOutput, "BIOS", "PatchTTYEnable", false);
+  SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.enableTTYLogging, "BIOS", "TTYLogging", false);
   SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.fastBoot, "BIOS", "PatchFastBoot", false);
 
   dialog->registerWidgetHelp(m_ui.fastBoot, tr("Fast Boot"), tr("Unchecked"),
                              tr("Patches the BIOS to skip the console's boot animation. Does not work with all games, "
                                 "but usually safe to enable."));
   dialog->registerWidgetHelp(
-    m_ui.enableTTYOutput, tr("Enable TTY Output"), tr("Unchecked"),
-    tr("Patches the BIOS to log calls to printf(). Only use when debugging, can break games."));
+    m_ui.enableTTYLogging, tr("Enable TTY Logging"), tr("Unchecked"),
+    tr("Logs BIOS calls to printf(). Not all games contain debugging messages."));
 
   connect(m_ui.imageNTSCJ, QOverload<int>::of(&QComboBox::currentIndexChanged), [this](int index) {
     if (m_dialog->isPerGameSettings() && index == 0)
diff --git a/src/duckstation-qt/biossettingswidget.ui b/src/duckstation-qt/biossettingswidget.ui
index c728c0949..8af6a98d4 100644
--- a/src/duckstation-qt/biossettingswidget.ui
+++ b/src/duckstation-qt/biossettingswidget.ui
@@ -164,9 +164,9 @@
        </widget>
       </item>
       <item>
-       <widget class="QCheckBox" name="enableTTYOutput">
+       <widget class="QCheckBox" name="enableTTYLogging">
         <property name="text">
-         <string>Enable TTY Output</string>
+         <string>Enable TTY Logging</string>
         </property>
        </widget>
       </item>