Repository: probe-rs/probe-rs
Pull request #2405 (2024-04-23 14:19:19)

Simplify writing Xtensa instruction sequences
Xtensa reset sequences may need executing short programs. This PR should plant the seed to implement those in a neater way than just dumping the binary into the source code.


Patch:
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 28291c1e..6844881c 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -19,7 +19,7 @@
       "request": "launch",
       "name": "DAP-Server probe-rs", // Use this to test the dap server .
       "cargo": {
-        "args": ["build", "--bin=probe-rs", "--features=cli"],
+        "args": ["build", "--package", "probe-rs-tools"],
         "filter": {
           "name": "probe-rs",
           "kind": "bin"
@@ -30,63 +30,45 @@
       //     "RUST_LOG": "error",
       //     "DEFMT_LOG": "info"
       // },
-      "cwd": "${workspaceFolder}/probe-rs/src/bin/probe-rs/cmd/dap_server"
     },
     {
       "type": "lldb",
       "request": "launch",
       "name": "list supported chips",
       "cargo": {
-        "args": ["build", "--bin=probe-rs", "--features=cli"],
+        "args": ["build", "--package", "probe-rs-tools"],
         "filter": {
           "name": "probe-rs",
           "kind": "bin"
         }
       },
       "args": ["chip", "list"],
-      "cwd": "${workspaceFolder}/debugger"
     },
     {
       "type": "lldb",
       "request": "launch",
       "name": "list attached probes",
       "cargo": {
-        "args": ["build", "--bin=probe-rs", "--features=cli"],
+        "args": ["build", "--package", "probe-rs-tools"],
         "filter": {
           "name": "probe-rs",
           "kind": "bin"
         }
       },
       "args": ["list"],
-      "cwd": "${workspaceFolder}/debugger"
     },
     {
       "type": "lldb",
       "request": "launch",
       "name": "display help text",
       "cargo": {
-        "args": ["build", "--bin=probe-rs", "--features=cli"],
+        "args": ["build", "--package", "probe-rs-tools"],
         "filter": {
           "name": "probe-rs",
           "kind": "bin"
         }
       },
       "args": ["help"],
-      "cwd": "${workspaceFolder}/debugger"
-    },
-    {
-      "type": "lldb",
-      "request": "launch",
-      "name": "display dap-server debug subcommand help text",
-      "cargo": {
-        "args": ["build", "--bin=probe-rs", "--features=cli"],
-        "filter": {
-          "name": "probe-rs",
-          "kind": "bin"
-        }
-      },
-      "args": ["help", "dap-server", "debug"],
-      "cwd": "${workspaceFolder}/debugger"
     },
     {
       "type": "lldb",
diff --git a/Cargo.toml b/Cargo.toml
index 1fd48c03..67e787ef 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -18,7 +18,7 @@ members = [
     "smoke-tester",
     "xtask",
     "target-gen",
-    "cli",
+    "probe-rs-tools",
 ]
 exclude = ["probe-rs/tests/gpio-hal-blinky"]
 
diff --git a/changelog/added-efm32pg22-target.md b/changelog/added-efm32pg22-target.md
new file mode 100644
index 00000000..d27e76ed
--- /dev/null
+++ b/changelog/added-efm32pg22-target.md
@@ -0,0 +1,1 @@
+Added support for the EFM32PG22 series of chips
diff --git a/changelog/added-jtag-tap.md b/changelog/added-jtag-tap.md
new file mode 100644
index 00000000..fa05edd5
--- /dev/null
+++ b/changelog/added-jtag-tap.md
@@ -0,0 +1,1 @@
+Added the option to select which JTAG TAP a given RISC-V or Xtensa core belongs to.
\ No newline at end of file
diff --git a/changelog/changed-interfaces.md b/changelog/changed-interfaces.md
new file mode 100644
index 00000000..041a038e
--- /dev/null
+++ b/changelog/changed-interfaces.md
@@ -0,0 +1,4 @@
+Changed how architecture-specific core interfaces are accessed:
+ - `probe::Probe::try_get_xtensa_interface` now takes an `XtensaDebugInterfaceState` object that should be saved and reused between interface accesses.
+ - `probe::Probe::try_get_riscv_interface` has been renamed to `try_get_riscv_interface_factory` and returns a builder object to create state objects, and to attach the probe using such a state object.
+ - The corresponding `DebugProbe` APIs have been changed.
\ No newline at end of file
diff --git a/changelog/changed-moved-binary-to-separate-crate.md b/changelog/changed-moved-binary-to-separate-crate.md
index acca61d0..f819f6f8 100644
--- a/changelog/changed-moved-binary-to-separate-crate.md
+++ b/changelog/changed-moved-binary-to-separate-crate.md
@@ -1,4 +1,4 @@
 The binaries `probe-rs`, `cargo embed` and `cargo flash` have been moved to a separate
-`probe-rs-cli` crate. This means that the installation method has changed as well.
+`probe-rs-tools` crate. This means that the installation method has changed as well.
 
-See the probe-rs homepage at https://probe.rs or the README for the updated installation instructions.
\ No newline at end of file
+See the probe-rs homepage at https://probe.rs or the README for the updated installation instructions.
diff --git a/probe-rs-target/src/chip.rs b/probe-rs-target/src/chip.rs
index 746d685d..c363497b 100644
--- a/probe-rs-target/src/chip.rs
+++ b/probe-rs-target/src/chip.rs
@@ -154,8 +154,14 @@ pub struct ArmCoreAccessOptions {
 pub struct RiscvCoreAccessOptions {
     /// The hart id
     pub hart_id: Option<u32>,
+
+    /// The JTAG TAP index of the core's debug module
+    pub jtag_tap: Option<usize>,
 }
 
 /// The data required to access an Xtensa core
 #[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct XtensaCoreAccessOptions {}
+pub struct XtensaCoreAccessOptions {
+    /// The JTAG TAP index of the core's debug module
+    pub jtag_tap: Option<usize>,
+}
diff --git a/cli/Cargo.toml b/probe-rs-tools/Cargo.toml
similarity index 100%
rename from cli/Cargo.toml
rename to probe-rs-tools/Cargo.toml
diff --git a/cli/build.rs b/probe-rs-tools/build.rs
similarity index 100%
rename from cli/build.rs
rename to probe-rs-tools/build.rs
diff --git a/cli/src/bin/cargo-embed.rs b/probe-rs-tools/src/bin/cargo-embed.rs
similarity index 100%
rename from cli/src/bin/cargo-embed.rs
rename to probe-rs-tools/src/bin/cargo-embed.rs
diff --git a/cli/src/bin/cargo-flash.rs b/probe-rs-tools/src/bin/cargo-flash.rs
similarity index 100%
rename from cli/src/bin/cargo-flash.rs
rename to probe-rs-tools/src/bin/cargo-flash.rs
diff --git a/cli/src/bin/probe-rs/cmd.rs b/probe-rs-tools/src/bin/probe-rs/cmd.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd.rs
diff --git a/cli/src/bin/probe-rs/cmd/attach.rs b/probe-rs-tools/src/bin/probe-rs/cmd/attach.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/attach.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/attach.rs
diff --git a/cli/src/bin/probe-rs/cmd/benchmark.rs b/probe-rs-tools/src/bin/probe-rs/cmd/benchmark.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/benchmark.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/benchmark.rs
diff --git a/cli/src/bin/probe-rs/cmd/cargo_embed/config/default.toml b/probe-rs-tools/src/bin/probe-rs/cmd/cargo_embed/config/default.toml
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/cargo_embed/config/default.toml
rename to probe-rs-tools/src/bin/probe-rs/cmd/cargo_embed/config/default.toml
diff --git a/cli/src/bin/probe-rs/cmd/cargo_embed/config/mod.rs b/probe-rs-tools/src/bin/probe-rs/cmd/cargo_embed/config/mod.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/cargo_embed/config/mod.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/cargo_embed/config/mod.rs
diff --git a/cli/src/bin/probe-rs/cmd/cargo_embed/error.rs b/probe-rs-tools/src/bin/probe-rs/cmd/cargo_embed/error.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/cargo_embed/error.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/cargo_embed/error.rs
diff --git a/cli/src/bin/probe-rs/cmd/cargo_embed/mod.rs b/probe-rs-tools/src/bin/probe-rs/cmd/cargo_embed/mod.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/cargo_embed/mod.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/cargo_embed/mod.rs
diff --git a/cli/src/bin/probe-rs/cmd/cargo_embed/rttui/app.rs b/probe-rs-tools/src/bin/probe-rs/cmd/cargo_embed/rttui/app.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/cargo_embed/rttui/app.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/cargo_embed/rttui/app.rs
diff --git a/cli/src/bin/probe-rs/cmd/cargo_embed/rttui/channel.rs b/probe-rs-tools/src/bin/probe-rs/cmd/cargo_embed/rttui/channel.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/cargo_embed/rttui/channel.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/cargo_embed/rttui/channel.rs
diff --git a/cli/src/bin/probe-rs/cmd/cargo_embed/rttui/event.rs b/probe-rs-tools/src/bin/probe-rs/cmd/cargo_embed/rttui/event.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/cargo_embed/rttui/event.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/cargo_embed/rttui/event.rs
diff --git a/cli/src/bin/probe-rs/cmd/cargo_embed/rttui/mod.rs b/probe-rs-tools/src/bin/probe-rs/cmd/cargo_embed/rttui/mod.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/cargo_embed/rttui/mod.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/cargo_embed/rttui/mod.rs
diff --git a/cli/src/bin/probe-rs/cmd/cargo_embed/rttui/tab.rs b/probe-rs-tools/src/bin/probe-rs/cmd/cargo_embed/rttui/tab.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/cargo_embed/rttui/tab.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/cargo_embed/rttui/tab.rs
diff --git a/cli/src/bin/probe-rs/cmd/cargo_embed/rttui/tcp.rs b/probe-rs-tools/src/bin/probe-rs/cmd/cargo_embed/rttui/tcp.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/cargo_embed/rttui/tcp.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/cargo_embed/rttui/tcp.rs
diff --git a/cli/src/bin/probe-rs/cmd/cargo_flash/diagnostics.rs b/probe-rs-tools/src/bin/probe-rs/cmd/cargo_flash/diagnostics.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/cargo_flash/diagnostics.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/cargo_flash/diagnostics.rs
diff --git a/cli/src/bin/probe-rs/cmd/cargo_flash/mod.rs b/probe-rs-tools/src/bin/probe-rs/cmd/cargo_flash/mod.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/cargo_flash/mod.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/cargo_flash/mod.rs
diff --git a/cli/src/bin/probe-rs/cmd/chip.rs b/probe-rs-tools/src/bin/probe-rs/cmd/chip.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/chip.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/chip.rs
diff --git a/cli/src/bin/probe-rs/cmd/complete.rs b/probe-rs-tools/src/bin/probe-rs/cmd/complete.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/complete.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/complete.rs
diff --git a/cli/src/bin/probe-rs/cmd/dap_server/debug_adapter.rs b/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/debug_adapter.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/dap_server/debug_adapter.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/dap_server/debug_adapter.rs
diff --git a/cli/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap.rs b/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap.rs
diff --git a/cli/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap/adapter.rs b/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap/adapter.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap/adapter.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap/adapter.rs
diff --git a/cli/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap/core_status.rs b/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap/core_status.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap/core_status.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap/core_status.rs
diff --git a/cli/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap/dap_types.rs b/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap/dap_types.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap/dap_types.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap/dap_types.rs
diff --git a/cli/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap/debugProtocol.json b/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap/debugProtocol.json
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap/debugProtocol.json
rename to probe-rs-tools/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap/debugProtocol.json
diff --git a/cli/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap/repl_commands.rs b/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap/repl_commands.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap/repl_commands.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap/repl_commands.rs
diff --git a/cli/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap/repl_commands_helpers.rs b/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap/repl_commands_helpers.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap/repl_commands_helpers.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap/repl_commands_helpers.rs
diff --git a/cli/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap/repl_types.rs b/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap/repl_types.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap/repl_types.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap/repl_types.rs
diff --git a/cli/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap/request_helpers.rs b/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap/request_helpers.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap/request_helpers.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/dap_server/debug_adapter/dap/request_helpers.rs
diff --git a/cli/src/bin/probe-rs/cmd/dap_server/debug_adapter/protocol.rs b/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/debug_adapter/protocol.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/dap_server/debug_adapter/protocol.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/dap_server/debug_adapter/protocol.rs
diff --git a/cli/src/bin/probe-rs/cmd/dap_server/debug_adapter/snapshots/probe_rs__cmd__dap_server__debug_adapter__protocol__test__receive_request_with_invalid_json.snap b/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/debug_adapter/snapshots/probe_rs__cmd__dap_server__debug_adapter__protocol__test__receive_request_with_invalid_json.snap
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/dap_server/debug_adapter/snapshots/probe_rs__cmd__dap_server__debug_adapter__protocol__test__receive_request_with_invalid_json.snap
rename to probe-rs-tools/src/bin/probe-rs/cmd/dap_server/debug_adapter/snapshots/probe_rs__cmd__dap_server__debug_adapter__protocol__test__receive_request_with_invalid_json.snap
diff --git a/cli/src/bin/probe-rs/cmd/dap_server/debug_adapter/snapshots/probe_rs__cmd__dap_server__debug_adapter__protocol__test__receive_request_with_wrong_content_length.snap b/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/debug_adapter/snapshots/probe_rs__cmd__dap_server__debug_adapter__protocol__test__receive_request_with_wrong_content_length.snap
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/dap_server/debug_adapter/snapshots/probe_rs__cmd__dap_server__debug_adapter__protocol__test__receive_request_with_wrong_content_length.snap
rename to probe-rs-tools/src/bin/probe-rs/cmd/dap_server/debug_adapter/snapshots/probe_rs__cmd__dap_server__debug_adapter__protocol__test__receive_request_with_wrong_content_length.snap
diff --git a/cli/src/bin/probe-rs/cmd/dap_server/debug_adapter/snapshots/probe_rs__cmd__dap_server__debug_adapter__protocol__test__receive_request_would_block.snap b/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/debug_adapter/snapshots/probe_rs__cmd__dap_server__debug_adapter__protocol__test__receive_request_would_block.snap
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/dap_server/debug_adapter/snapshots/probe_rs__cmd__dap_server__debug_adapter__protocol__test__receive_request_would_block.snap
rename to probe-rs-tools/src/bin/probe-rs/cmd/dap_server/debug_adapter/snapshots/probe_rs__cmd__dap_server__debug_adapter__protocol__test__receive_request_would_block.snap
diff --git a/cli/src/bin/probe-rs/cmd/dap_server/debug_adapter/snapshots/probe_rs__cmd__dap_server__debug_adapter__protocol__test__receive_valid_request.snap b/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/debug_adapter/snapshots/probe_rs__cmd__dap_server__debug_adapter__protocol__test__receive_valid_request.snap
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/dap_server/debug_adapter/snapshots/probe_rs__cmd__dap_server__debug_adapter__protocol__test__receive_valid_request.snap
rename to probe-rs-tools/src/bin/probe-rs/cmd/dap_server/debug_adapter/snapshots/probe_rs__cmd__dap_server__debug_adapter__protocol__test__receive_valid_request.snap
diff --git a/cli/src/bin/probe-rs/cmd/dap_server/mod.rs b/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/mod.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/dap_server/mod.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/dap_server/mod.rs
diff --git a/cli/src/bin/probe-rs/cmd/dap_server/peripherals.rs b/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/peripherals.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/dap_server/peripherals.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/dap_server/peripherals.rs
diff --git a/cli/src/bin/probe-rs/cmd/dap_server/peripherals/svd_cache.rs b/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/peripherals/svd_cache.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/dap_server/peripherals/svd_cache.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/dap_server/peripherals/svd_cache.rs
diff --git a/cli/src/bin/probe-rs/cmd/dap_server/peripherals/svd_variables.rs b/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/peripherals/svd_variables.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/dap_server/peripherals/svd_variables.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/dap_server/peripherals/svd_variables.rs
diff --git a/cli/src/bin/probe-rs/cmd/dap_server/server.rs b/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/server.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/dap_server/server.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/dap_server/server.rs
diff --git a/cli/src/bin/probe-rs/cmd/dap_server/server/configuration.rs b/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/server/configuration.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/dap_server/server/configuration.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/dap_server/server/configuration.rs
diff --git a/cli/src/bin/probe-rs/cmd/dap_server/server/core_data.rs b/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/server/core_data.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/dap_server/server/core_data.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/dap_server/server/core_data.rs
diff --git a/cli/src/bin/probe-rs/cmd/dap_server/server/debug_rtt.rs b/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/server/debug_rtt.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/dap_server/server/debug_rtt.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/dap_server/server/debug_rtt.rs
diff --git a/cli/src/bin/probe-rs/cmd/dap_server/server/debugger.rs b/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/server/debugger.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/dap_server/server/debugger.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/dap_server/server/debugger.rs
diff --git a/cli/src/bin/probe-rs/cmd/dap_server/server/session_data.rs b/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/server/session_data.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/dap_server/server/session_data.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/dap_server/server/session_data.rs
diff --git a/cli/src/bin/probe-rs/cmd/dap_server/server/startup.rs b/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/server/startup.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/dap_server/server/startup.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/dap_server/server/startup.rs
diff --git a/cli/src/bin/probe-rs/cmd/dap_server/test.rs b/probe-rs-tools/src/bin/probe-rs/cmd/dap_server/test.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/dap_server/test.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/dap_server/test.rs
diff --git a/cli/src/bin/probe-rs/cmd/debug.rs b/probe-rs-tools/src/bin/probe-rs/cmd/debug.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/debug.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/debug.rs
diff --git a/cli/src/bin/probe-rs/cmd/download.rs b/probe-rs-tools/src/bin/probe-rs/cmd/download.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/download.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/download.rs
diff --git a/cli/src/bin/probe-rs/cmd/erase.rs b/probe-rs-tools/src/bin/probe-rs/cmd/erase.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/erase.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/erase.rs
diff --git a/cli/src/bin/probe-rs/cmd/gdb.rs b/probe-rs-tools/src/bin/probe-rs/cmd/gdb.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/gdb.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/gdb.rs
diff --git a/cli/src/bin/probe-rs/cmd/info.rs b/probe-rs-tools/src/bin/probe-rs/cmd/info.rs
similarity index 91%
rename from cli/src/bin/probe-rs/cmd/info.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/info.rs
index f3295524..a167a9e8 100644
--- a/cli/src/bin/probe-rs/cmd/info.rs
+++ b/probe-rs-tools/src/bin/probe-rs/cmd/info.rs
@@ -14,7 +14,9 @@ use probe_rs::{
             ApAddress, ApInformation, ArmProbeInterface, DpAddress, MemoryApInformation, Register,
         },
         riscv::communication_interface::RiscvCommunicationInterface,
-        xtensa::communication_interface::XtensaCommunicationInterface,
+        xtensa::communication_interface::{
+            XtensaCommunicationInterface, XtensaDebugInterfaceState,
+        },
     },
     probe::{list::Lister, Probe, WireProtocol},
     MemoryMappedRegister,
@@ -159,50 +161,52 @@ fn try_show_info(
     // If the current protocol we want to use is SWD, we have avoid this.
     if probe.has_riscv_interface() && protocol == WireProtocol::Jtag {
         tracing::debug!("Trying to show RISC-V chip information");
-        match probe.try_into_riscv_interface() {
-            Ok(mut interface) => {
-                if let Err(e) = show_riscv_info(&mut interface) {
-                    println!("Error showing RISC-V chip information: {:?}", anyhow!(e));
-                }
-
-                probe = interface.close();
-            }
-            Err((interface_probe, e)) => {
-                println!("Error while reading RISC-V info: {:?}", anyhow!(e));
-                probe = interface_probe;
+        match probe.try_get_riscv_interface_builder() {
+            Ok(factory) => {
+                let mut state = factory.create_state();
+                match factory.attach(&mut state) {
+                    Ok(mut interface) => {
+                        if let Err(e) = show_riscv_info(&mut interface) {
+                            println!("Error showing RISC-V chip information: {:?}", anyhow!(e));
+                        }
+                    }
+                    Err(e) => println!(
+                        "Error while attaching to RISC-V interface: {:?}",
+                        anyhow!(e)
+                    ),
+                };
             }
+            Err(e) => println!("Error while reading RISC-V info: {:?}", anyhow!(e)),
         }
     } else if protocol == WireProtocol::Swd {
         println!(
-                "Debugging RISC-V targets over SWD is not supported. For these targets, JTAG is the only supported protocol. RISC-V specific information cannot be printed."
-            );
+            "Debugging RISC-V targets over SWD is not supported. For these targets, JTAG is the only supported protocol. RISC-V specific information cannot be printed."
+        );
     } else {
         println!(
-                "Unable to debug RISC-V targets using the current probe. RISC-V specific information cannot be printed."
-            );
+            "Unable to debug RISC-V targets using the current probe. RISC-V specific information cannot be printed."
+        );
     }
 
     // This check is a bit weird, but `try_into_xtensa_interface` will try to switch the protocol to JTAG.
     // If the current protocol we want to use is SWD, we have avoid this.
     if probe.has_xtensa_interface() && protocol == WireProtocol::Jtag {
         tracing::debug!("Trying to show Xtensa chip information");
-        match probe.try_into_xtensa_interface() {
+        let mut state = XtensaDebugInterfaceState::default();
+        match probe.try_get_xtensa_interface(&mut state) {
             Ok(mut interface) => {
                 if let Err(e) = show_xtensa_info(&mut interface) {
                     println!("Error showing Xtensa chip information: {:?}", anyhow!(e));
                 }
-
-                probe = interface.close();
             }
-            Err((interface_probe, e)) => {
+            Err(e) => {
                 println!("Error showing Xtensa chip information: {:?}", anyhow!(e));
-                probe = interface_probe;
             }
         }
     } else if protocol == WireProtocol::Swd {
         println!(
-                "Debugging Xtensa targets over SWD is not supported. For these targets, JTAG is the only supported protocol. Xtensa specific information cannot be printed."
-            );
+            "Debugging Xtensa targets over SWD is not supported. For these targets, JTAG is the only supported protocol. Xtensa specific information cannot be printed."
+        );
     } else {
         println!(
             "Unable to debug Xtensa targets using the current probe. Xtensa specific information cannot be printed."
diff --git a/cli/src/bin/probe-rs/cmd/itm.rs b/probe-rs-tools/src/bin/probe-rs/cmd/itm.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/itm.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/itm.rs
diff --git a/cli/src/bin/probe-rs/cmd/list.rs b/probe-rs-tools/src/bin/probe-rs/cmd/list.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/list.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/list.rs
diff --git a/cli/src/bin/probe-rs/cmd/profile.rs b/probe-rs-tools/src/bin/probe-rs/cmd/profile.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/profile.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/profile.rs
diff --git a/cli/src/bin/probe-rs/cmd/read.rs b/probe-rs-tools/src/bin/probe-rs/cmd/read.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/read.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/read.rs
diff --git a/cli/src/bin/probe-rs/cmd/reset.rs b/probe-rs-tools/src/bin/probe-rs/cmd/reset.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/reset.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/reset.rs
diff --git a/cli/src/bin/probe-rs/cmd/run/mod.rs b/probe-rs-tools/src/bin/probe-rs/cmd/run/mod.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/run/mod.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/run/mod.rs
diff --git a/cli/src/bin/probe-rs/cmd/run/normal_run_mode.rs b/probe-rs-tools/src/bin/probe-rs/cmd/run/normal_run_mode.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/run/normal_run_mode.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/run/normal_run_mode.rs
diff --git a/cli/src/bin/probe-rs/cmd/run/test_run_mode.rs b/probe-rs-tools/src/bin/probe-rs/cmd/run/test_run_mode.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/run/test_run_mode.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/run/test_run_mode.rs
diff --git a/cli/src/bin/probe-rs/cmd/snapshots/probe_rs__cmd__chip__multiple_chip_output.snap b/probe-rs-tools/src/bin/probe-rs/cmd/snapshots/probe_rs__cmd__chip__multiple_chip_output.snap
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/snapshots/probe_rs__cmd__chip__multiple_chip_output.snap
rename to probe-rs-tools/src/bin/probe-rs/cmd/snapshots/probe_rs__cmd__chip__multiple_chip_output.snap
diff --git a/cli/src/bin/probe-rs/cmd/snapshots/probe_rs__cmd__chip__single_chip_output.snap b/probe-rs-tools/src/bin/probe-rs/cmd/snapshots/probe_rs__cmd__chip__single_chip_output.snap
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/snapshots/probe_rs__cmd__chip__single_chip_output.snap
rename to probe-rs-tools/src/bin/probe-rs/cmd/snapshots/probe_rs__cmd__chip__single_chip_output.snap
diff --git a/cli/src/bin/probe-rs/cmd/trace.rs b/probe-rs-tools/src/bin/probe-rs/cmd/trace.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/trace.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/trace.rs
diff --git a/cli/src/bin/probe-rs/cmd/write.rs b/probe-rs-tools/src/bin/probe-rs/cmd/write.rs
similarity index 100%
rename from cli/src/bin/probe-rs/cmd/write.rs
rename to probe-rs-tools/src/bin/probe-rs/cmd/write.rs
diff --git a/cli/src/bin/probe-rs/main.rs b/probe-rs-tools/src/bin/probe-rs/main.rs
similarity index 100%
rename from cli/src/bin/probe-rs/main.rs
rename to probe-rs-tools/src/bin/probe-rs/main.rs
diff --git a/cli/src/bin/probe-rs/util/cargo.rs b/probe-rs-tools/src/bin/probe-rs/util/cargo.rs
similarity index 100%
rename from cli/src/bin/probe-rs/util/cargo.rs
rename to probe-rs-tools/src/bin/probe-rs/util/cargo.rs
diff --git a/cli/src/bin/probe-rs/util/common_options.rs b/probe-rs-tools/src/bin/probe-rs/util/common_options.rs
similarity index 100%
rename from cli/src/bin/probe-rs/util/common_options.rs
rename to probe-rs-tools/src/bin/probe-rs/util/common_options.rs
diff --git a/cli/src/bin/probe-rs/util/flash.rs b/probe-rs-tools/src/bin/probe-rs/util/flash.rs
similarity index 100%
rename from cli/src/bin/probe-rs/util/flash.rs
rename to probe-rs-tools/src/bin/probe-rs/util/flash.rs
diff --git a/cli/src/bin/probe-rs/util/logging.rs b/probe-rs-tools/src/bin/probe-rs/util/logging.rs
similarity index 100%
rename from cli/src/bin/probe-rs/util/logging.rs
rename to probe-rs-tools/src/bin/probe-rs/util/logging.rs
diff --git a/cli/src/bin/probe-rs/util/mod.rs b/probe-rs-tools/src/bin/probe-rs/util/mod.rs
similarity index 100%
rename from cli/src/bin/probe-rs/util/mod.rs
rename to probe-rs-tools/src/bin/probe-rs/util/mod.rs
diff --git a/cli/src/bin/probe-rs/util/rtt.rs b/probe-rs-tools/src/bin/probe-rs/util/rtt.rs
similarity index 100%
rename from cli/src/bin/probe-rs/util/rtt.rs
rename to probe-rs-tools/src/bin/probe-rs/util/rtt.rs
diff --git a/cli/src/bin/probe-rs/util/test_data/.gitignore b/probe-rs-tools/src/bin/probe-rs/util/test_data/.gitignore
similarity index 100%
rename from cli/src/bin/probe-rs/util/test_data/.gitignore
rename to probe-rs-tools/src/bin/probe-rs/util/test_data/.gitignore
diff --git a/cli/src/bin/probe-rs/util/test_data/binary_cargo_config/.cargo/config b/probe-rs-tools/src/bin/probe-rs/util/test_data/binary_cargo_config/.cargo/config
similarity index 100%
rename from cli/src/bin/probe-rs/util/test_data/binary_cargo_config/.cargo/config
rename to probe-rs-tools/src/bin/probe-rs/util/test_data/binary_cargo_config/.cargo/config
diff --git a/cli/src/bin/probe-rs/util/test_data/binary_cargo_config/Cargo.toml b/probe-rs-tools/src/bin/probe-rs/util/test_data/binary_cargo_config/Cargo.toml
similarity index 100%
rename from cli/src/bin/probe-rs/util/test_data/binary_cargo_config/Cargo.toml
rename to probe-rs-tools/src/bin/probe-rs/util/test_data/binary_cargo_config/Cargo.toml
diff --git a/cli/src/bin/probe-rs/util/test_data/binary_cargo_config/src/main.rs b/probe-rs-tools/src/bin/probe-rs/util/test_data/binary_cargo_config/src/main.rs
similarity index 100%
rename from cli/src/bin/probe-rs/util/test_data/binary_cargo_config/src/main.rs
rename to probe-rs-tools/src/bin/probe-rs/util/test_data/binary_cargo_config/src/main.rs
diff --git a/cli/src/bin/probe-rs/util/test_data/binary_cargo_config_toml/.cargo/config.toml b/probe-rs-tools/src/bin/probe-rs/util/test_data/binary_cargo_config_toml/.cargo/config.toml
similarity index 100%
rename from cli/src/bin/probe-rs/util/test_data/binary_cargo_config_toml/.cargo/config.toml
rename to probe-rs-tools/src/bin/probe-rs/util/test_data/binary_cargo_config_toml/.cargo/config.toml
diff --git a/cli/src/bin/probe-rs/util/test_data/binary_cargo_config_toml/Cargo.toml b/probe-rs-tools/src/bin/probe-rs/util/test_data/binary_cargo_config_toml/Cargo.toml
similarity index 100%
rename from cli/src/bin/probe-rs/util/test_data/binary_cargo_config_toml/Cargo.toml
rename to probe-rs-tools/src/bin/probe-rs/util/test_data/binary_cargo_config_toml/Cargo.toml
diff --git a/cli/src/bin/probe-rs/util/test_data/binary_cargo_config_toml/src/main.rs b/probe-rs-tools/src/bin/probe-rs/util/test_data/binary_cargo_config_toml/src/main.rs
similarity index 100%
rename from cli/src/bin/probe-rs/util/test_data/binary_cargo_config_toml/src/main.rs
rename to probe-rs-tools/src/bin/probe-rs/util/test_data/binary_cargo_config_toml/src/main.rs
diff --git a/cli/src/bin/probe-rs/util/test_data/binary_project/Cargo.toml b/probe-rs-tools/src/bin/probe-rs/util/test_data/binary_project/Cargo.toml
similarity index 100%
rename from cli/src/bin/probe-rs/util/test_data/binary_project/Cargo.toml
rename to probe-rs-tools/src/bin/probe-rs/util/test_data/binary_project/Cargo.toml
diff --git a/cli/src/bin/probe-rs/util/test_data/binary_project/src/main.rs b/probe-rs-tools/src/bin/probe-rs/util/test_data/binary_project/src/main.rs
similarity index 100%
rename from cli/src/bin/probe-rs/util/test_data/binary_project/src/main.rs
rename to probe-rs-tools/src/bin/probe-rs/util/test_data/binary_project/src/main.rs
diff --git a/cli/src/bin/probe-rs/util/test_data/library_project/Cargo.toml b/probe-rs-tools/src/bin/probe-rs/util/test_data/library_project/Cargo.toml
similarity index 100%
rename from cli/src/bin/probe-rs/util/test_data/library_project/Cargo.toml
rename to probe-rs-tools/src/bin/probe-rs/util/test_data/library_project/Cargo.toml
diff --git a/cli/src/bin/probe-rs/util/test_data/library_project/src/lib.rs b/probe-rs-tools/src/bin/probe-rs/util/test_data/library_project/src/lib.rs
similarity index 100%
rename from cli/src/bin/probe-rs/util/test_data/library_project/src/lib.rs
rename to probe-rs-tools/src/bin/probe-rs/util/test_data/library_project/src/lib.rs
diff --git a/cli/src/bin/probe-rs/util/test_data/library_with_example_project/Cargo.toml b/probe-rs-tools/src/bin/probe-rs/util/test_data/library_with_example_project/Cargo.toml
similarity index 100%
rename from cli/src/bin/probe-rs/util/test_data/library_with_example_project/Cargo.toml
rename to probe-rs-tools/src/bin/probe-rs/util/test_data/library_with_example_project/Cargo.toml
diff --git a/cli/src/bin/probe-rs/util/test_data/library_with_example_project/examples/example.rs b/probe-rs-tools/src/bin/probe-rs/util/test_data/library_with_example_project/examples/example.rs
similarity index 100%
rename from cli/src/bin/probe-rs/util/test_data/library_with_example_project/examples/example.rs
rename to probe-rs-tools/src/bin/probe-rs/util/test_data/library_with_example_project/examples/example.rs
diff --git a/cli/src/bin/probe-rs/util/test_data/library_with_example_project/src/lib.rs b/probe-rs-tools/src/bin/probe-rs/util/test_data/library_with_example_project/src/lib.rs
similarity index 100%
rename from cli/src/bin/probe-rs/util/test_data/library_with_example_project/src/lib.rs
rename to probe-rs-tools/src/bin/probe-rs/util/test_data/library_with_example_project/src/lib.rs
diff --git a/cli/src/bin/probe-rs/util/test_data/multiple_binary_project/Cargo.toml b/probe-rs-tools/src/bin/probe-rs/util/test_data/multiple_binary_project/Cargo.toml
similarity index 100%
rename from cli/src/bin/probe-rs/util/test_data/multiple_binary_project/Cargo.toml
rename to probe-rs-tools/src/bin/probe-rs/util/test_data/multiple_binary_project/Cargo.toml
diff --git a/cli/src/bin/probe-rs/util/test_data/multiple_binary_project/src/bin/bin_a.rs b/probe-rs-tools/src/bin/probe-rs/util/test_data/multiple_binary_project/src/bin/bin_a.rs
similarity index 100%
rename from cli/src/bin/probe-rs/util/test_data/multiple_binary_project/src/bin/bin_a.rs
rename to probe-rs-tools/src/bin/probe-rs/util/test_data/multiple_binary_project/src/bin/bin_a.rs
diff --git a/cli/src/bin/probe-rs/util/test_data/multiple_binary_project/src/bin/bin_b.rs b/probe-rs-tools/src/bin/probe-rs/util/test_data/multiple_binary_project/src/bin/bin_b.rs
similarity index 100%
rename from cli/src/bin/probe-rs/util/test_data/multiple_binary_project/src/bin/bin_b.rs
rename to probe-rs-tools/src/bin/probe-rs/util/test_data/multiple_binary_project/src/bin/bin_b.rs
diff --git a/cli/src/bin/probe-rs/util/test_data/workspace_project/Cargo.toml b/probe-rs-tools/src/bin/probe-rs/util/test_data/workspace_project/Cargo.toml
similarity index 100%
rename from cli/src/bin/probe-rs/util/test_data/workspace_project/Cargo.toml
rename to probe-rs-tools/src/bin/probe-rs/util/test_data/workspace_project/Cargo.toml
diff --git a/cli/src/bin/probe-rs/util/test_data/workspace_project/workspace_bin/Cargo.toml b/probe-rs-tools/src/bin/probe-rs/util/test_data/workspace_project/workspace_bin/Cargo.toml
similarity index 100%
rename from cli/src/bin/probe-rs/util/test_data/workspace_project/workspace_bin/Cargo.toml
rename to probe-rs-tools/src/bin/probe-rs/util/test_data/workspace_project/workspace_bin/Cargo.toml
diff --git a/cli/src/bin/probe-rs/util/test_data/workspace_project/workspace_bin/src/main.rs b/probe-rs-tools/src/bin/probe-rs/util/test_data/workspace_project/workspace_bin/src/main.rs
similarity index 100%
rename from cli/src/bin/probe-rs/util/test_data/workspace_project/workspace_bin/src/main.rs
rename to probe-rs-tools/src/bin/probe-rs/util/test_data/workspace_project/workspace_bin/src/main.rs
diff --git a/cli/src/bin/probe-rs/util/test_data/workspace_project/workspace_lib/Cargo.toml b/probe-rs-tools/src/bin/probe-rs/util/test_data/workspace_project/workspace_lib/Cargo.toml
similarity index 100%
rename from cli/src/bin/probe-rs/util/test_data/workspace_project/workspace_lib/Cargo.toml
rename to probe-rs-tools/src/bin/probe-rs/util/test_data/workspace_project/workspace_lib/Cargo.toml
diff --git a/cli/src/bin/probe-rs/util/test_data/workspace_project/workspace_lib/src/lib.rs b/probe-rs-tools/src/bin/probe-rs/util/test_data/workspace_project/workspace_lib/src/lib.rs
similarity index 100%
rename from cli/src/bin/probe-rs/util/test_data/workspace_project/workspace_lib/src/lib.rs
rename to probe-rs-tools/src/bin/probe-rs/util/test_data/workspace_project/workspace_lib/src/lib.rs
diff --git a/probe-rs/src/architecture/riscv/communication_interface.rs b/probe-rs/src/architecture/riscv/communication_interface.rs
index 5e255554..44c4c255 100644
--- a/probe-rs/src/architecture/riscv/communication_interface.rs
+++ b/probe-rs/src/architecture/riscv/communication_interface.rs
@@ -5,11 +5,11 @@
 //! specification v0.13.2 .
 
 use crate::architecture::riscv::dtm::dtm_access::DtmAccess;
-use crate::probe::{DebugProbe, Probe};
 use crate::{
     architecture::riscv::*, memory_mapped_bitfield_register, probe::DeferredResultIndex,
     Error as ProbeRsError,
 };
+use std::any::Any;
 use std::collections::HashMap;
 
 /// Some error occurred when working with the RISC-V core.
@@ -91,7 +91,7 @@ pub enum AbstractCommandErrorKind {
     /// registers was read or written. This status is only
     /// written if `cmderr` contains 0.
     Busy = 1,
-    /// The requested command is not supported, reg
+    /// The requested command is not supported
     NotSupported = 2,
     /// An exception occurred while executing the command (e.g. while executing the Program Buffer).
     Exception = 3,
@@ -121,7 +121,7 @@ impl AbstractCommandErrorKind {
             5 => Bus,
             6 => _Reserved,
             7 => Other,
-            _ => panic!("cmderr is a 3 bit value, values higher than 7 should not occur."),
+            _ => unreachable!("cmderr is a 3 bit value, values higher than 7 should not occur."),
         }
     }
 }
@@ -173,33 +173,20 @@ impl CoreRegisterAbstractCmdSupport {
     }
 }
 
-#[derive(Debug)]
+/// Save stack of a scratch register.
+// TODO: we probably only need an Option, we don't seem to use scratch registers is nested situations.
+#[derive(Debug, Default)]
 struct ScratchState {
-    stack: Vec<(bool, u32)>,
-    should_save: bool,
-}
-
-impl Default for ScratchState {
-    fn default() -> Self {
-        Self {
-            stack: vec![],
-            should_save: true,
-        }
-    }
+    stack: Vec<u32>,
 }
 
 impl ScratchState {
     fn push(&mut self, value: u32) {
-        self.stack.push((self.should_save, value));
-        self.should_save = false;
+        self.stack.push(value);
     }
 
     fn pop(&mut self) -> Option<u32> {
-        let (should_save, value) = self.stack.pop()?;
-
-        self.should_save = should_save;
-
-        Some(value)
+        self.stack.pop()
     }
 }
 
@@ -222,8 +209,10 @@ pub struct RiscvCommunicationInterfaceState {
     /// Number of data registers for abstract commands
     data_register_count: u8,
 
+    /// Number of scratch registers
     nscratch: u8,
 
+    /// Whether the target supports autoexecuting the program buffer
     supports_autoexec: bool,
 
     /// Pointer to the configuration string
@@ -235,14 +224,27 @@ pub struct RiscvCommunicationInterfaceState {
     /// Number of harts
     num_harts: u32,
 
+    /// Describes, which memory access method should be used for a given access width
     memory_access_info: HashMap<RiscvBusAccess, MemoryAccessMethod>,
 
     /// describes, if the given register can be read / written with an
     /// abstract command
     abstract_cmd_register_info: HashMap<RegisterId, CoreRegisterAbstractCmdSupport>,
 
+    /// First scratch register's state
     s0: ScratchState,
+
+    /// Second scratch register's state
     s1: ScratchState,
+
+    /// Bitfield of enabled harts
+    enabled_harts: u32,
+
+    /// The index of the last selected hart
+    last_selected_hart: u32,
+
+    /// Store the value of the `hasresethaltreq` bit of the `dmcstatus` register.
+    hasresethaltreq: Option<bool>,
 }
 
 /// Timeout for RISC-V operations.
@@ -286,6 +288,9 @@ impl RiscvCommunicationInterfaceState {
 
             s0: ScratchState::default(),
             s1: ScratchState::default(),
+            enabled_harts: 0,
+            last_selected_hart: 0,
+            hasresethaltreq: None,
         }
     }
 
@@ -305,47 +310,64 @@ impl Default for RiscvCommunicationInterfaceState {
     }
 }
 
+/// The combined state of a RISC-V debug module and its transport interface.
+pub struct RiscvDebugInterfaceState {
+    pub(super) interface_state: RiscvCommunicationInterfaceState,
+    pub(super) dtm_state: Box<dyn Any + Send>,
+}
+
+impl RiscvDebugInterfaceState {
+    pub(super) fn new(dtm_state: Box<dyn Any + Send>) -> Self {
+        Self {
+            interface_state: RiscvCommunicationInterfaceState::new(),
+            dtm_state,
+        }
+    }
+}
+
+/// A single-use factory for creating RISC-V communication interfaces and their states.
+pub trait RiscvInterfaceBuilder<'probe> {
+    /// Creates a new RISC-V communication interface state object.
+    ///
+    /// The state object needs to be stored separately from the communication interface
+    /// and can be used to restore the state of the interface at a later time.
+    fn create_state(&self) -> RiscvDebugInterfaceState;
+
+    /// Consumes the factory and creates a communication interface
+    /// object initialised with the given state.
+    fn attach<'state>(
+        self: Box<Self>,
+        state: &'state mut RiscvDebugInterfaceState,
+    ) -> Result<RiscvCommunicationInterface<'state>, DebugProbeError>
+    where
+        'probe: 'state;
+}
+
 /// A interface that implements controls for RISC-V cores.
 #[derive(Debug)]
-pub struct RiscvCommunicationInterface {
+pub struct RiscvCommunicationInterface<'state> {
     /// The Debug Transport Module (DTM) is used to
     /// communicate with the Debug Module on the target chip.
-    dtm: Box<dyn DtmAccess>,
-    state: RiscvCommunicationInterfaceState,
-    enabled_harts: u32,
-    last_selected_hart: u32,
-
-    /// Store the value of the `hasresethaltreq` bit of the `dmcstatus` register.
-    hasresethaltreq: Option<bool>,
+    dtm: Box<dyn DtmAccess + 'state>,
+    state: &'state mut RiscvCommunicationInterfaceState,
 }
 
-impl RiscvCommunicationInterface {
+impl<'state> RiscvCommunicationInterface<'state> {
     /// Creates a new RISC-V communication interface with a given probe driver.
-    pub fn new(dtm_access: Box<dyn DtmAccess>) -> Result<Self, (Box<dyn DtmAccess>, RiscvError)> {
-        let state = RiscvCommunicationInterfaceState::new();
-
-        let mut s = Self {
-            dtm: dtm_access,
-            state,
-            enabled_harts: 0,
-            last_selected_hart: 0,
-            hasresethaltreq: None,
-        };
-
-        if let Err(err) = s.enter_debug_mode() {
-            return Err((s.dtm, err));
-        }
-
-        Ok(s)
+    pub fn new(
+        dtm: Box<dyn DtmAccess + 'state>,
+        state: &'state mut RiscvCommunicationInterfaceState,
+    ) -> Self {
+        Self { dtm, state }
     }
 
     /// Select current hart
     pub fn select_hart(&mut self, hart: u32) -> Result<(), RiscvError> {
-        if self.enabled_harts & (1 << hart) == 0 {
+        if self.state.enabled_harts & (1 << hart) == 0 {
             return Err(RiscvError::HartUnavailable);
         }
 
-        if self.last_selected_hart == hart {
+        if self.state.last_selected_hart == hart {
             return Ok(());
         }
 
@@ -353,13 +375,13 @@ impl RiscvCommunicationInterface {
         control.set_dmactive(true);
         control.set_hartsel(hart);
         self.write_dm_register(control)?;
-        self.last_selected_hart = hart;
+        self.state.last_selected_hart = hart;
         Ok(())
     }
 
     /// Check if the given hart is enabled
     pub fn hart_enabled(&self, hart: u32) -> bool {
-        self.enabled_harts & (1 << hart) != 0
+        self.state.enabled_harts & (1 << hart) != 0
     }
 
     /// Assert the target reset
@@ -377,12 +399,6 @@ impl RiscvCommunicationInterface {
         self.dtm.read_idcode()
     }
 
-    /// Mark S0 to be saved on the next `save_s0` call.
-    #[allow(unused)]
-    fn should_save_s0(&mut self, should_save: bool) {
-        self.state.s0.should_save = should_save;
-    }
-
     fn save_s0(&mut self) -> Result<bool, RiscvError> {
         let s0 = self.abstract_cmd_register_read(&registers::S0)?;
 
@@ -401,12 +417,6 @@ impl RiscvCommunicationInterface {
         Ok(())
     }
 
-    /// Mark S0 to be saved on the next `save_s0` call.
-    #[allow(unused)]
-    fn should_save_s1(&mut self, should_save: bool) {
-        self.state.s1.should_save = should_save;
-    }
-
     fn save_s1(&mut self) -> Result<bool, RiscvError> {
         let s1 = self.abstract_cmd_register_read(&registers::S1)?;
 
@@ -489,7 +499,7 @@ impl RiscvCommunicationInterface {
 
         // Hart 0 exists on every chip
         let mut num_harts = 1;
-        self.enabled_harts = 1;
+        self.state.enabled_harts = 1;
 
         // Check if anynonexistent is avaliable.
         // Some chips that have only one hart do not implement anynonexistent and allnonexistent.
@@ -519,7 +529,7 @@ impl RiscvCommunicationInterface {
                 }
 
                 if !status.allunavail() {
-                    self.enabled_harts |= 1 << num_harts;
+                    self.state.enabled_harts |= 1 << num_harts;
                 }
 
                 num_harts += 1;
@@ -1453,16 +1463,6 @@ impl RiscvCommunicationInterface {
         Ok(())
     }
 
-    /// Destruct the interface and return the stored probe driver.
-    pub fn close(self) -> Probe {
-        self.dtm.close()
-    }
-
-    /// Destruct the interface and return boxed DebugProbe
-    pub fn into_probe(self: Box<Self>) -> Box<dyn DebugProbe> {
-        self.dtm.into_probe()
-    }
-
     pub(crate) fn execute(&mut self) -> Result<(), RiscvError> {
         self.dtm.execute()
     }
@@ -1542,12 +1542,12 @@ impl RiscvCommunicationInterface {
     /// Returns a cached value if available, otherwise queries the
     /// `hasresethaltreq` bit in the `dmstatus` register.
     pub(crate) fn supports_reset_halt_req(&mut self) -> Result<bool, RiscvError> {
-        if let Some(has_reset_halt_req) = self.hasresethaltreq {
+        if let Some(has_reset_halt_req) = self.state.hasresethaltreq {
             Ok(has_reset_halt_req)
         } else {
             let dmstatus: Dmstatus = self.read_dm_register()?;
 
-            self.hasresethaltreq = Some(dmstatus.hasresethaltreq());
+            self.state.hasresethaltreq = Some(dmstatus.hasresethaltreq());
 
             Ok(dmstatus.hasresethaltreq())
         }
@@ -1860,7 +1860,7 @@ impl RiscvValue for u128 {
     }
 }
 
-impl MemoryInterface for RiscvCommunicationInterface {
+impl MemoryInterface for RiscvCommunicationInterface<'_> {
     fn supports_native_64bit_access(&mut self) -> bool {
         false
     }
diff --git a/probe-rs/src/architecture/riscv/dtm/dtm_access.rs b/probe-rs/src/architecture/riscv/dtm/dtm_access.rs
index 0e011fb5..4faec74a 100644
--- a/probe-rs/src/architecture/riscv/dtm/dtm_access.rs
+++ b/probe-rs/src/architecture/riscv/dtm/dtm_access.rs
@@ -1,11 +1,11 @@
 use crate::architecture::riscv::communication_interface::RiscvError;
-use crate::probe::{CommandResult, DebugProbe, DebugProbeError, DeferredResultIndex, Probe};
+use crate::probe::{CommandResult, DebugProbeError, DeferredResultIndex};
 use std::fmt;
 use std::time::Duration;
 
 pub trait DtmAccess: Send + fmt::Debug {
     /// Perform interface-specific initialisation upon attaching.
-    fn init(&mut self) -> Result<(), DebugProbeError> {
+    fn init(&mut self) -> Result<(), RiscvError> {
         Ok(())
     }
 
@@ -24,12 +24,6 @@ pub trait DtmAccess: Send + fmt::Debug {
         index: DeferredResultIndex,
     ) -> Result<CommandResult, RiscvError>;
 
-    /// Close the DTM access and return the underlying probe instance
-    fn close(self: Box<Self>) -> Probe;
-
-    /// Close the DTM access and return the underlying boxed debug access
-    fn into_probe(self: Box<Self>) -> Box<dyn DebugProbe>;
-
     /// Execute scheduled dmi accesses
     fn execute(&mut self) -> Result<(), RiscvError>;
 
diff --git a/probe-rs/src/architecture/riscv/dtm/jtag_dtm.rs b/probe-rs/src/architecture/riscv/dtm/jtag_dtm.rs
index 90edc5ed..cd8e73b9 100644
--- a/probe-rs/src/architecture/riscv/dtm/jtag_dtm.rs
+++ b/probe-rs/src/architecture/riscv/dtm/jtag_dtm.rs
@@ -6,19 +6,17 @@ use crate::architecture::riscv::dtm::dtm_access::DtmAccess;
 use bitfield::bitfield;
 use std::time::{Duration, Instant};
 
-use crate::architecture::riscv::communication_interface::RiscvError;
+use crate::architecture::riscv::communication_interface::{
+    RiscvCommunicationInterface, RiscvDebugInterfaceState, RiscvError, RiscvInterfaceBuilder,
+};
+use crate::probe::DebugProbeError;
 use crate::probe::{
     CommandResult, DeferredResultIndex, DeferredResultSet, JTAGAccess, JtagCommandQueue,
     JtagWriteCommand,
 };
-use crate::probe::{DebugProbe, DebugProbeError, Probe};
-
-/// Access to the Debug Transport Module (DTM),
-/// which is used to communicate with the RISC-V debug module.
-#[derive(Debug)]
-pub struct JtagDtm {
-    pub probe: Box<dyn JTAGAccess>,
 
+#[derive(Debug, Default)]
+struct DtmState {
     queued_commands: JtagCommandQueue,
     jtag_results: DeferredResultSet,
 
@@ -26,42 +24,46 @@ pub struct JtagDtm {
     abits: u32,
 }
 
-impl JtagDtm {
-    pub fn new(mut probe: Box<dyn JTAGAccess>) -> Result<Self, (Box<dyn JTAGAccess>, RiscvError)> {
-        let dtmcs_raw = match probe.read_register(DTMCS_ADDRESS, DTMCS_WIDTH) {
-            Ok(value) => value,
-            Err(e) => return Err((probe, e.into())),
-        };
+pub struct JtagDtmBuilder<'f>(&'f mut dyn JTAGAccess);
 
-        let raw_dtmcs = u32::from_le_bytes((&dtmcs_raw[..]).try_into().unwrap());
-
-        if raw_dtmcs == 0 {
-            return Err((probe, RiscvError::NoRiscvTarget));
-        }
-
-        let dtmcs = Dtmcs(raw_dtmcs);
+impl<'f> JtagDtmBuilder<'f> {
+    pub fn new(probe: &'f mut dyn JTAGAccess) -> Self {
+        Self(probe)
+    }
+}
 
-        tracing::debug!("{:?}", dtmcs);
+impl<'probe> RiscvInterfaceBuilder<'probe> for JtagDtmBuilder<'probe> {
+    fn create_state(&self) -> RiscvDebugInterfaceState {
+        RiscvDebugInterfaceState::new(Box::<DtmState>::default())
+    }
 
-        let abits = dtmcs.abits();
-        let idle_cycles = dtmcs.idle();
+    fn attach<'state>(
+        self: Box<Self>,
+        state: &'state mut RiscvDebugInterfaceState,
+    ) -> Result<RiscvCommunicationInterface<'state>, DebugProbeError>
+    where
+        'probe: 'state,
+    {
+        let dtm_state = state.dtm_state.downcast_mut::<DtmState>().unwrap();
+
+        Ok(RiscvCommunicationInterface::new(
+            Box::new(JtagDtm::new(self.0, dtm_state)),
+            &mut state.interface_state,
+        ))
+    }
+}
 
-        if dtmcs.version() != 1 {
-            return Err((
-                probe,
-                RiscvError::UnsupportedDebugTransportModuleVersion(dtmcs.version() as u8),
-            ));
-        }
+/// Access to the Debug Transport Module (DTM),
+/// which is used to communicate with the RISC-V debug module.
+#[derive(Debug)]
+pub struct JtagDtm<'probe> {
+    pub probe: &'probe mut dyn JTAGAccess,
+    state: &'probe mut DtmState,
+}
 
-        // Setup the number of idle cycles between JTAG accesses
-        probe.set_idle_cycles(idle_cycles as u8);
-
-        Ok(Self {
-            probe,
-            abits,
-            queued_commands: JtagCommandQueue::new(),
-            jtag_results: DeferredResultSet::new(),
-        })
+impl<'probe> JtagDtm<'probe> {
+    fn new(probe: &'probe mut dyn JTAGAccess, state: &'probe mut DtmState) -> Self {
+        Self { probe, state }
     }
 
     fn transform_dmi_result(response_bytes: Vec<u8>) -> Result<u32, DmiOperationStatus> {
@@ -91,7 +93,7 @@ impl JtagDtm {
     ) -> Result<Result<u32, DmiOperationStatus>, DebugProbeError> {
         let bytes = op.to_byte_batch();
 
-        let bit_size = self.abits + DMI_ADDRESS_BIT_OFFSET;
+        let bit_size = self.state.abits + DMI_ADDRESS_BIT_OFFSET;
 
         self.probe
             .write_register(DMI_ADDRESS, &bytes, bit_size)
@@ -104,9 +106,9 @@ impl JtagDtm {
     ) -> Result<DeferredResultIndex, RiscvError> {
         let bytes = op.to_byte_batch();
 
-        let bit_size = self.abits + DMI_ADDRESS_BIT_OFFSET;
+        let bit_size = self.state.abits + DMI_ADDRESS_BIT_OFFSET;
 
-        Ok(self.queued_commands.schedule(JtagWriteCommand {
+        Ok(self.state.queued_commands.schedule(JtagWriteCommand {
             address: DMI_ADDRESS,
             data: bytes.to_vec(),
             transform: |_, result| {
@@ -146,9 +148,34 @@ impl JtagDtm {
     }
 }
 
-impl DtmAccess for JtagDtm {
-    fn init(&mut self) -> Result<(), DebugProbeError> {
+impl DtmAccess for JtagDtm<'_> {
+    fn init(&mut self) -> Result<(), RiscvError> {
         self.probe.tap_reset()?;
+        let dtmcs_raw = self.probe.read_register(DTMCS_ADDRESS, DTMCS_WIDTH)?;
+
+        let raw_dtmcs = u32::from_le_bytes((&dtmcs_raw[..]).try_into().unwrap());
+
+        if raw_dtmcs == 0 {
+            return Err(RiscvError::NoRiscvTarget);
+        }
+
+        let dtmcs = Dtmcs(raw_dtmcs);
+
+        tracing::debug!("{:?}", dtmcs);
+
+        let abits = dtmcs.abits();
+        let idle_cycles = dtmcs.idle();
+
+        if dtmcs.version() != 1 {
+            return Err(RiscvError::UnsupportedDebugTransportModuleVersion(
+                dtmcs.version() as u8,
+            ));
+        }
+
+        // Setup the number of idle cycles between JTAG accesses
+        self.probe.set_idle_cycles(idle_cycles as u8);
+        self.state.abits = abits;
+
         Ok(())
     }
 
@@ -179,33 +206,26 @@ impl DtmAccess for JtagDtm {
         &mut self,
         index: DeferredResultIndex,
     ) -> Result<CommandResult, RiscvError> {
-        match self.jtag_results.take(index) {
+        match self.state.jtag_results.take(index) {
             Ok(result) => Ok(result),
             Err(index) => {
                 self.execute()?;
                 // We can lose data if `execute` fails.
-                self.jtag_results
+                self.state
+                    .jtag_results
                     .take(index)
                     .map_err(|_| RiscvError::BatchedResultNotAvailable)
             }
         }
     }
 
-    fn close(self: Box<Self>) -> Probe {
-        Probe::from_attached_probe(self.probe.into_probe())
-    }
-
-    fn into_probe(self: Box<Self>) -> Box<dyn DebugProbe> {
-        self.probe.into_probe()
-    }
-
     fn execute(&mut self) -> Result<(), RiscvError> {
-        let mut cmds = std::mem::take(&mut self.queued_commands);
+        let mut cmds = std::mem::take(&mut self.state.queued_commands);
 
         while !cmds.is_empty() {
             match self.probe.write_register_batch(&cmds) {
                 Ok(r) => {
-                    self.jtag_results.merge_from(r);
+                    self.state.jtag_results.merge_from(r);
                     return Ok(());
                 }
                 Err(e) => match e.error {
@@ -216,7 +236,7 @@ impl DtmAccess for JtagDtm {
 
                                 // queue up the remaining commands when we retry
                                 cmds.consume(e.results.len());
-                                self.jtag_results.merge_from(e.results);
+                                self.state.jtag_results.merge_from(e.results);
 
                                 self.probe.set_idle_cycles(self.probe.idle_cycles() + 1);
                             }
diff --git a/probe-rs/src/architecture/riscv/mod.rs b/probe-rs/src/architecture/riscv/mod.rs
index 95d162f5..0fbac42b 100644
--- a/probe-rs/src/architecture/riscv/mod.rs
+++ b/probe-rs/src/architecture/riscv/mod.rs
@@ -32,26 +32,19 @@ pub(crate) mod dtm;
 pub mod sequences;
 
 /// An interface to operate a RISC-V core.
-pub struct Riscv32<'probe> {
-    interface: &'probe mut RiscvCommunicationInterface,
-    state: &'probe mut RiscVState,
+pub struct Riscv32<'state> {
+    interface: RiscvCommunicationInterface<'state>,
+    state: &'state mut RiscvCoreState,
     sequence: Arc<dyn RiscvDebugSequence>,
 }
 
-impl<'probe> Riscv32<'probe> {
+impl<'state> Riscv32<'state> {
     /// Create a new RISC-V interface for a particular hart.
     pub fn new(
-        hart: u32,
-        interface: &'probe mut RiscvCommunicationInterface,
-        state: &'probe mut RiscVState,
+        interface: RiscvCommunicationInterface<'state>,
+        state: &'state mut RiscvCoreState,
         sequence: Arc<dyn RiscvDebugSequence>,
     ) -> Result<Self, RiscvError> {
-        if !interface.hart_enabled(hart) {
-            return Err(RiscvError::HartUnavailable);
-        }
-
-        interface.select_hart(hart)?;
-
         Ok(Self {
             interface,
             state,
@@ -203,7 +196,7 @@ impl<'probe> Riscv32<'probe> {
     }
 }
 
-impl<'probe> CoreInterface for Riscv32<'probe> {
+impl<'state> CoreInterface for Riscv32<'state> {
     fn wait_for_core_halted(&mut self, timeout: Duration) -> Result<(), crate::Error> {
         self.interface.wait_for_core_halted(timeout)?;
         self.state.pc_written = false;
@@ -286,7 +279,7 @@ impl<'probe> CoreInterface for Riscv32<'probe> {
 
     fn reset_and_halt(&mut self, timeout: Duration) -> Result<CoreInformation, Error> {
         self.sequence
-            .reset_system_and_halt(self.interface, timeout)?;
+            .reset_system_and_halt(&mut self.interface, timeout)?;
 
         let pc = self.read_core_reg(RegisterId(0x7b1))?;
 
@@ -626,12 +619,12 @@ impl<'probe> CoreInterface for Riscv32<'probe> {
     }
 
     fn reset_catch_set(&mut self) -> Result<(), Error> {
-        self.sequence.reset_catch_set(self.interface)?;
+        self.sequence.reset_catch_set(&mut self.interface)?;
         Ok(())
     }
 
     fn reset_catch_clear(&mut self) -> Result<(), Error> {
-        self.sequence.reset_catch_clear(self.interface)?;
+        self.sequence.reset_catch_clear(&mut self.interface)?;
         Ok(())
     }
 
@@ -641,7 +634,7 @@ impl<'probe> CoreInterface for Riscv32<'probe> {
     }
 }
 
-impl<'probe> MemoryInterface for Riscv32<'probe> {
+impl MemoryInterface for Riscv32<'_> {
     fn supports_native_64bit_access(&mut self) -> bool {
         self.interface.supports_native_64bit_access()
     }
@@ -725,7 +718,7 @@ impl<'probe> MemoryInterface for Riscv32<'probe> {
 
 #[derive(Debug)]
 /// Flags used to control the [`SpecificCoreState`](crate::core::SpecificCoreState) for RiscV architecture
-pub struct RiscVState {
+pub struct RiscvCoreState {
     /// A flag to remember whether we want to use hw_breakpoints during stepping of the core.
     hw_breakpoints_enabled: bool,
 
@@ -739,7 +732,7 @@ pub struct RiscVState {
     semihosting_command: Option<SemihostingCommand>,
 }
 
-impl RiscVState {
+impl RiscvCoreState {
     pub(crate) fn new() -> Self {
         Self {
             hw_breakpoints_enabled: false,
diff --git a/probe-rs/src/architecture/xtensa/arch/instruction/mod.rs b/probe-rs/src/architecture/xtensa/arch/instruction/mod.rs
index 850b10e6..d13eb8e0 100644
--- a/probe-rs/src/architecture/xtensa/arch/instruction/mod.rs
+++ b/probe-rs/src/architecture/xtensa/arch/instruction/mod.rs
@@ -55,14 +55,63 @@ impl Instruction {
         (3, word)
     }
 
+    /// Encodes the instruction into a Little Endian sequence of bytes and appends it to the given
+    /// vector.
     pub fn encode_into_vec(self, vec: &mut Vec<u8>) {
         let (bytes, narrow) = self.encode_bytes();
 
         vec.extend_from_slice(&narrow.to_le_bytes()[..bytes]);
     }
 
+    /// Returns the instruction encoded as a Little Endian sequence of bytes.
+    pub fn to_bytes(self) -> Vec<u8> {
+        let mut bytes = Vec::new();
+        self.encode_into_vec(&mut bytes);
+        bytes
+    }
+
     pub const fn encode(self) -> InstructionEncoding {
         let narrow = self.encode_bytes().1;
         InstructionEncoding::Narrow(narrow)
     }
 }
+
+pub(crate) fn into_binary(instructions: impl IntoIterator<Item = Instruction>) -> Vec<u8> {
+    Program::instructions_into_bytes(instructions)
+}
+
+#[derive(Debug, Default)]
+pub(crate) struct Program {
+    bytes: Vec<u8>,
+}
+
+impl Program {
+    // TODO:
+    // - add origin address
+    // - add ability to retrieve the current PC for jumps
+    pub fn new() -> Self {
+        Program { bytes: Vec::new() }
+    }
+
+    pub fn add_instruction(&mut self, instruction: Instruction) {
+        instruction.encode_into_vec(&mut self.bytes);
+    }
+
+    pub fn add_instructions(&mut self, instructions: impl IntoIterator<Item = Instruction>) {
+        for instruction in instructions {
+            self.add_instruction(instruction);
+        }
+    }
+
+    pub fn into_bytes(self) -> Vec<u8> {
+        self.bytes
+    }
+
+    pub fn instructions_into_bytes(instructions: impl IntoIterator<Item = Instruction>) -> Vec<u8> {
+        let mut program = Program::new();
+
+        program.add_instructions(instructions);
+
+        program.into_bytes()
+    }
+}
diff --git a/probe-rs/src/architecture/xtensa/communication_interface.rs b/probe-rs/src/architecture/xtensa/communication_interface.rs
index 573a769d..e1b4fbb5 100644
--- a/probe-rs/src/architecture/xtensa/communication_interface.rs
+++ b/probe-rs/src/architecture/xtensa/communication_interface.rs
@@ -6,10 +6,11 @@ use std::{
 };
 
 use crate::{
-    architecture::xtensa::arch::{
-        instruction::Instruction, CpuRegister, Register, SpecialRegister,
+    architecture::xtensa::{
+        arch::{instruction::Instruction, CpuRegister, Register, SpecialRegister},
+        xdm::XdmState,
     },
-    probe::{DebugProbeError, DeferredResultIndex, JTAGAccess, Probe},
+    probe::{DebugProbeError, DeferredResultIndex, JTAGAccess},
     BreakpointCause, Error as ProbeRsError, HaltReason, MemoryInterface,
 };
 
@@ -58,7 +59,7 @@ impl From<XtensaError> for ProbeRsError {
 
 #[derive(Clone, Copy)]
 #[allow(unused)]
-pub(super) enum DebugLevel {
+enum DebugLevel {
     L2 = 2,
     L3 = 3,
     L4 = 4,
@@ -91,49 +92,68 @@ impl DebugLevel {
     }
 }
 
-struct XtensaCommunicationInterfaceState {
-    /// Pairs of (register, deferred value). The value is optional where None means "being restored"
+/// Xtensa interface state.
+// FIXME: This struct is a weird mix between core state, debug module state and core configuration.
+pub(super) struct XtensaInterfaceState {
+    /// Pairs of (register, read handle). The value is optional where None means "being restored"
     saved_registers: HashMap<Register, Option<DeferredResultIndex>>,
 
+    /// Whether the core is halted.
+    // This roughly relates to Core Debug States (true = Running, false = [Stopped, Stepping])
     is_halted: bool,
-}
-
-/// An interface that implements controls for Xtensa cores.
-pub struct XtensaCommunicationInterface {
-    /// The Xtensa debug module
-    pub(super) xdm: Xdm,
-    state: XtensaCommunicationInterfaceState,
 
+    /// The number of hardware breakpoints the target supports. CPU-specific configuration value.
     hw_breakpoint_num: u32,
+
+    /// The interrupt level at which debug exceptions are generated. CPU-specific configuration value.
     debug_level: DebugLevel,
 }
 
-impl XtensaCommunicationInterface {
-    /// Create the Xtensa communication interface using the underlying probe driver
-    pub fn new(probe: Box<dyn JTAGAccess>) -> Result<Self, (Box<dyn JTAGAccess>, DebugProbeError)> {
-        let xdm = Xdm::new(probe).map_err(|(probe, e)| (probe, e.into()))?;
+impl Default for XtensaInterfaceState {
+    fn default() -> Self {
+        Self {
+            saved_registers: Default::default(),
+            is_halted: Default::default(),
 
-        let mut s = Self {
-            xdm,
-            state: XtensaCommunicationInterfaceState {
-                saved_registers: Default::default(),
-                is_halted: false,
-            },
-            // TODO chip-specific configuration
+            // FIXME: these are per-chip configuration parameters
             hw_breakpoint_num: 2,
             debug_level: DebugLevel::L6,
-        };
-
-        match s.init() {
-            Ok(()) => Ok(s),
-
-            Err(e) => Err((s.xdm.free(), e.into())),
         }
     }
+}
 
-    /// Destruct the interface and return the stored probe driver.
-    pub fn close(self) -> Probe {
-        Probe::from_attached_probe(self.xdm.probe.into_probe())
+/// Debug module and transport state.
+#[derive(Default)]
+pub struct XtensaDebugInterfaceState {
+    interface_state: XtensaInterfaceState,
+    xdm_state: XdmState,
+}
+
+/// The higher level of the XDM functionality.
+// TODO: this includes core state and CPU configuration that don't exactly belong
+// here but one layer up.
+pub struct XtensaCommunicationInterface<'probe> {
+    /// The Xtensa debug module
+    pub(super) xdm: Xdm<'probe>,
+    state: &'probe mut XtensaInterfaceState,
+}
+
+impl<'probe> XtensaCommunicationInterface<'probe> {
+    /// Create the Xtensa communication interface using the underlying probe driver
+    pub fn new(
+        probe: &'probe mut dyn JTAGAccess,
+        state: &'probe mut XtensaDebugInterfaceState,
+    ) -> Self {
+        let XtensaDebugInterfaceState {
+            interface_state,
+            xdm_state,
+        } = state;
+        let xdm = Xdm::new(probe, xdm_state);
+
+        Self {
+            xdm,
+            state: interface_state,
+        }
     }
 
     /// Read the targets IDCODE.
@@ -141,8 +161,12 @@ impl XtensaCommunicationInterface {
         Ok(self.xdm.read_idcode()?)
     }
 
-    fn init(&mut self) -> Result<(), XtensaError> {
-        // TODO any initialization that needs to be done
+    /// Enter debug mode.
+    pub fn enter_debug_mode(&mut self) -> Result<(), XtensaError> {
+        self.xdm.enter_debug_mode()?;
+
+        self.state.is_halted = self.xdm.status()?.stopped();
+
         Ok(())
     }
 
@@ -150,7 +174,7 @@ impl XtensaCommunicationInterface {
     ///
     /// On the Xtensa architecture this is the `NIBREAK` configuration parameter.
     pub fn available_breakpoint_units(&self) -> u32 {
-        self.hw_breakpoint_num
+        self.state.hw_breakpoint_num
     }
 
     /// Halts the core.
@@ -194,7 +218,7 @@ impl XtensaCommunicationInterface {
 
     /// Steps the core by one instruction.
     pub fn step(&mut self) -> Result<(), XtensaError> {
-        self.schedule_write_register(ICountLevel(self.debug_level as u32))?;
+        self.schedule_write_register(ICountLevel(self.state.debug_level as u32))?;
 
         // An exception is generated at the beginning of an instruction that would overflow ICOUNT.
         self.schedule_write_register(ICount(-2_i32 as u32))?;
@@ -203,7 +227,7 @@ impl XtensaCommunicationInterface {
         self.wait_for_core_halted(Duration::from_millis(100))?;
 
         // Avoid stopping again
-        self.schedule_write_register(ICountLevel(self.debug_level as u32 + 1))?;
+        self.schedule_write_register(ICountLevel(self.state.debug_level as u32 + 1))?;
 
         Ok(())
     }
@@ -314,8 +338,8 @@ impl XtensaCommunicationInterface {
         match register.into() {
             Register::Cpu(register) => Ok(self.schedule_read_cpu_register(register)),
             Register::Special(register) => self.schedule_read_special_register(register),
-            Register::CurrentPc => self.schedule_read_special_register(self.debug_level.pc()),
-            Register::CurrentPs => self.schedule_read_special_register(self.debug_level.ps()),
+            Register::CurrentPc => self.schedule_read_special_register(self.state.debug_level.pc()),
+            Register::CurrentPs => self.schedule_read_special_register(self.state.debug_level.ps()),
         }
     }
 
@@ -338,10 +362,10 @@ impl XtensaCommunicationInterface {
             Register::Cpu(register) => self.schedule_write_cpu_register(register, value),
             Register::Special(register) => self.schedule_write_special_register(register, value),
             Register::CurrentPc => {
-                self.schedule_write_special_register(self.debug_level.pc(), value)
+                self.schedule_write_special_register(self.state.debug_level.pc(), value)
             }
             Register::CurrentPs => {
-                self.schedule_write_special_register(self.debug_level.ps(), value)
+                self.schedule_write_special_register(self.state.debug_level.ps(), value)
             }
         }
     }
@@ -700,7 +724,7 @@ fn as_bytes_mut<T: DataType>(data: &mut [T]) -> &mut [u8] {
     }
 }
 
-impl MemoryInterface for XtensaCommunicationInterface {
+impl<'probe> MemoryInterface for XtensaCommunicationInterface<'probe> {
     fn read(&mut self, address: u64, dst: &mut [u8]) -> Result<(), crate::Error> {
         self.read_memory(address, dst)?;
 
diff --git a/probe-rs/src/architecture/xtensa/mod.rs b/probe-rs/src/architecture/xtensa/mod.rs
index 07411adc..f9ac2ca3 100644
--- a/probe-rs/src/architecture/xtensa/mod.rs
+++ b/probe-rs/src/architecture/xtensa/mod.rs
@@ -7,7 +7,7 @@ use probe_rs_target::{Architecture, CoreType, InstructionSet};
 use crate::{
     architecture::xtensa::{
         arch::{instruction::Instruction, Register, SpecialRegister},
-        communication_interface::{DebugCause, IBreakEn},
+        communication_interface::{DebugCause, IBreakEn, XtensaCommunicationInterface},
         registers::{FP, PC, RA, SP, XTENSA_CORE_REGSISTERS},
         sequences::XtensaDebugSequence,
     },
@@ -20,8 +20,6 @@ use crate::{
     SemihostingCommand,
 };
 
-use self::communication_interface::XtensaCommunicationInterface;
-
 pub(crate) mod arch;
 mod xdm;
 
@@ -30,10 +28,14 @@ pub(crate) mod registers;
 pub(crate) mod sequences;
 
 #[derive(Debug)]
-/// Flags used to control the [`SpecificCoreState`](crate::core::SpecificCoreState) for Xtensa
-/// architecture.
-pub struct XtensaState {
+/// Xtensa core state.
+pub struct XtensaCoreState {
+    /// Whether hardware breakpoints are enabled.
     breakpoints_enabled: bool,
+
+    /// Whether each hardware breakpoint is set.
+    // 2 is the architectural upper limit. The actual count is stored in
+    // [`communication_interface::XtensaInterfaceState`]
     breakpoint_set: [bool; 2],
 
     /// Whether the PC was written since we last halted. Used to avoid incrementing the PC on
@@ -44,7 +46,7 @@ pub struct XtensaState {
     semihosting_command: Option<SemihostingCommand>,
 }
 
-impl XtensaState {
+impl XtensaCoreState {
     /// Creates a new [`XtensaState`].
     pub(crate) fn new() -> Self {
         Self {
@@ -55,6 +57,7 @@ impl XtensaState {
         }
     }
 
+    /// Creates a bitmask of the currently set breakpoints.
     fn breakpoint_mask(&self) -> u32 {
         self.breakpoint_set
             .iter()
@@ -65,8 +68,8 @@ impl XtensaState {
 
 /// An interface to operate an Xtensa core.
 pub struct Xtensa<'probe> {
-    interface: &'probe mut XtensaCommunicationInterface,
-    state: &'probe mut XtensaState,
+    interface: XtensaCommunicationInterface<'probe>,
+    state: &'probe mut XtensaCoreState,
     sequence: Arc<dyn XtensaDebugSequence>,
 }
 
@@ -76,8 +79,8 @@ impl<'probe> Xtensa<'probe> {
 
     /// Create a new Xtensa interface for a particular core.
     pub fn new(
-        interface: &'probe mut XtensaCommunicationInterface,
-        state: &'probe mut XtensaState,
+        interface: XtensaCommunicationInterface<'probe>,
+        state: &'probe mut XtensaCoreState,
         sequence: Arc<dyn XtensaDebugSequence>,
     ) -> Self {
         Self {
@@ -134,8 +137,7 @@ impl<'probe> Xtensa<'probe> {
             actual_instructions[2],
         );
 
-        let mut expected_instruction = vec![];
-        Instruction::Break(1, 14).encode_into_vec(&mut expected_instruction);
+        let expected_instruction = Instruction::Break(1, 14).to_bytes();
 
         tracing::debug!(
             "Expected instructions={0:#08x} {1:#08x} {2:#08x}",
@@ -301,7 +303,7 @@ impl<'probe> CoreInterface for Xtensa<'probe> {
     fn reset(&mut self) -> Result<(), Error> {
         self.state.semihosting_command = None;
         self.sequence
-            .reset_system_and_halt(self.interface, Duration::from_millis(500))?;
+            .reset_system_and_halt(&mut self.interface, Duration::from_millis(500))?;
 
         self.run()
     }
@@ -309,7 +311,7 @@ impl<'probe> CoreInterface for Xtensa<'probe> {
     fn reset_and_halt(&mut self, timeout: Duration) -> Result<CoreInformation, Error> {
         self.state.semihosting_command = None;
         self.sequence
-            .reset_system_and_halt(self.interface, timeout)?;
+            .reset_system_and_halt(&mut self.interface, timeout)?;
 
         self.core_info()
     }
diff --git a/probe-rs/src/architecture/xtensa/xdm.rs b/probe-rs/src/architecture/xtensa/xdm.rs
index 6a56328c..7d6973d3 100644
--- a/probe-rs/src/architecture/xtensa/xdm.rs
+++ b/probe-rs/src/architecture/xtensa/xdm.rs
@@ -97,69 +97,84 @@ pub enum DebugRegisterError {
     Unexpected(u8),
 }
 
-#[derive(thiserror::Error, Debug, Clone, Copy)]
+#[derive(thiserror::Error, Debug, Clone, Copy, docsplay::Display)]
 pub enum Error {
-    #[error("Error {access} register {narsel:#04X}")]
+    /// Error {access} register {narsel:#04X}
     Xdm {
+        /// Nexus Address Register selector. Contains the ID of the register being accessed.
         narsel: u8,
+
+        /// Access type (reading or writing)
         access: &'static str,
+
+        /// The error that occurred.
         source: DebugRegisterError,
     },
 
-    #[error("ExecExeception")]
+    /// The instruction execution has encountered an exception.
     ExecExeception,
 
-    #[error("ExecBusy")]
+    /// The core is still executing a previous instruction.
     ExecBusy,
 
-    #[error("ExecOverrun")]
+    /// Instruction execution overrun.
     ExecOverrun,
 
-    #[error("Instruction ignored")]
+    /// The instruction was ignored. Most often this indicates that the core was not halted before
+    /// requesting instruction execution.
     InstructionIgnored,
 
-    #[error("XdmPoweredOff")]
+    /// The Xtensa Debug Module is powered off.
     XdmPoweredOff,
 }
 
-#[derive(Debug)]
-pub struct Xdm {
-    pub probe: Box<dyn JTAGAccess>,
-
-    device_id: u32,
-
+#[derive(Debug, Default)]
+pub struct XdmState {
+    /// The last instruction to be executed.
+    // This is used to:
+    // - detect incorrect uses of `schedule_write_ddr_and_execute` which expects an instruction to
+    //   be loaded
+    // - wait for the last instruction to complete before proceeding, as some instructions can be
+    //   assumed to complete instantly.
     last_instruction: Option<Instruction>,
 
+    /// The command queue for the current batch. JTAG accesses are batched to reduce the number of
+    /// IO operations.
     queue: JtagCommandQueue,
+
+    /// The results of the reads in the already executed batched JTAG commands.
     jtag_results: DeferredResultSet,
 
+    /// Read handles for accesses that need to force capturing their bits.
+    ///
+    /// The batching system tries to minimize the number of captured bits, in order to reduce
+    /// the number of JTAG operations. However, some accesses need to capture their bits to
+    /// complete correctly, or to - ironically - increase performance. We store their otherwise
+    /// ignored handles in this vector and drop them when we're done with the batch.
     status_idxs: Vec<DeferredResultIndex>,
 }
 
-impl Xdm {
-    pub fn new(probe: Box<dyn JTAGAccess>) -> Result<Self, (Box<dyn JTAGAccess>, XtensaError)> {
-        // TODO implement openocd's esp32_queue_tdi_idle() to prevent potentially damaging flash ICs
-
-        let mut x = Self {
-            probe,
-            device_id: 0,
-            last_instruction: None,
-
-            queue: JtagCommandQueue::new(),
-            jtag_results: DeferredResultSet::new(),
+/// The lower level functions of the Xtensa Debug Module.
+// TODO: this is mostly JTAG-specific, but not specifically. We should probably split this up, e.g.
+// move the instruction execution into the current communication_interface module.
+#[derive(Debug)]
+pub struct Xdm<'probe> {
+    /// The JTAG interface.
+    pub probe: &'probe mut dyn JTAGAccess,
 
-            status_idxs: Vec::new(),
-        };
+    /// Debug module state.
+    state: &'probe mut XdmState,
+}
 
-        if let Err(e) = x.init() {
-            return Err((x.free(), e));
-        }
+impl<'probe> Xdm<'probe> {
+    pub fn new(probe: &'probe mut dyn JTAGAccess, state: &'probe mut XdmState) -> Self {
+        // TODO implement openocd's esp32_queue_tdi_idle() to prevent potentially damaging flash ICs
 
-        Ok(x)
+        Self { probe, state }
     }
 
     #[tracing::instrument(skip(self))]
-    fn init(&mut self) -> Result<(), XtensaError> {
+    pub(crate) fn enter_debug_mode(&mut self) -> Result<(), XtensaError> {
         self.probe.tap_reset()?;
 
         let mut pwr_control = PowerControl(0);
@@ -221,9 +236,6 @@ impl Xdm {
             status
         })?;
 
-        // TODO check status and clear bits if required
-        self.device_id = device_id;
-
         Ok(())
     }
 
@@ -240,18 +252,18 @@ impl Xdm {
     }
 
     pub(super) fn execute(&mut self) -> Result<(), XtensaError> {
-        let mut queue = std::mem::take(&mut self.queue);
+        let mut queue = std::mem::take(&mut self.state.queue);
 
         tracing::debug!("Executing {} commands", queue.len());
 
         // Drop the status readers when we're done.
         // We take now to avoid a possibly recursive call to clear before it's time.
-        let _idxs = std::mem::take(&mut self.status_idxs);
+        let _idxs = std::mem::take(&mut self.state.status_idxs);
 
         while !queue.is_empty() {
             match self.probe.write_register_batch(&queue) {
                 Ok(result) => {
-                    self.jtag_results.merge_from(result);
+                    self.state.jtag_results.merge_from(result);
                     return Ok(());
                 }
                 Err(e) => {
@@ -277,7 +289,7 @@ impl Xdm {
 
                     // queue up the remaining commands when we retry
                     queue.consume(e.results.len());
-                    self.jtag_results.merge_from(e.results);
+                    self.state.jtag_results.merge_from(e.results);
                 }
             }
         }
@@ -289,12 +301,13 @@ impl Xdm {
         &mut self,
         index: DeferredResultIndex,
     ) -> Result<CommandResult, XtensaError> {
-        match self.jtag_results.take(index) {
+        match self.state.jtag_results.take(index) {
             Ok(result) => Ok(result),
             Err(index) => {
                 self.execute()?;
                 // We can lose data if `execute` fails.
-                self.jtag_results
+                self.state
+                    .jtag_results
                     .take(index)
                     .map_err(|_| XtensaError::BatchedResultNotAvailable)
             }
@@ -302,7 +315,7 @@ impl Xdm {
     }
 
     fn do_nexus_op(&mut self, nar: u8, ndr: u32, transform: TransformFn) -> DeferredResultIndex {
-        let nar = self.queue.schedule(JtagWriteCommand {
+        let nar = self.state.queue.schedule(JtagWriteCommand {
             address: TapInstruction::Nar.code(),
             data: nar.to_le_bytes().to_vec(),
             len: TapInstruction::Nar.bits(),
@@ -326,9 +339,9 @@ impl Xdm {
         });
 
         // We save the nar reader because we want to capture the previous status.
-        self.status_idxs.push(nar);
+        self.state.status_idxs.push(nar);
 
-        self.queue.schedule(JtagWriteCommand {
+        self.state.queue.schedule(JtagWriteCommand {
             address: TapInstruction::Ndr.code(),
             data: ndr.to_le_bytes().to_vec(),
             len: TapInstruction::Ndr.bits(),
@@ -419,7 +432,7 @@ impl Xdm {
         let status_reader = self
             .schedule_dbg_read_and_transform(DebugStatus::ADDRESS, transform_instruction_status);
 
-        self.status_idxs.push(status_reader);
+        self.state.status_idxs.push(status_reader);
     }
 
     /// Instructs Core to enter Core Stopped state instead of vectoring on a Debug Exception/Interrupt.
@@ -502,7 +515,7 @@ impl Xdm {
 
     pub(super) fn schedule_write_instruction(&mut self, instruction: Instruction) {
         tracing::debug!("Preparing instruction: {:?}", instruction);
-        self.last_instruction = Some(instruction);
+        self.state.last_instruction = Some(instruction);
 
         match instruction.encode() {
             InstructionEncoding::Narrow(inst) => {
@@ -513,7 +526,7 @@ impl Xdm {
 
     pub(super) fn schedule_execute_instruction(&mut self, instruction: Instruction) {
         tracing::debug!("Executing instruction: {:?}", instruction);
-        self.last_instruction = Some(instruction);
+        self.state.last_instruction = Some(instruction);
 
         match instruction.encode() {
             InstructionEncoding::Narrow(inst) => {
@@ -540,7 +553,7 @@ impl Xdm {
     }
 
     pub(super) fn schedule_write_ddr_and_execute(&mut self, ddr: u32) {
-        if let Some(instruction) = self.last_instruction {
+        if let Some(instruction) = self.state.last_instruction {
             tracing::debug!("Executing instruction via DDREXEC write: {:?}", instruction);
         } else {
             tracing::warn!("Writing DDREXEC without instruction");
@@ -554,7 +567,7 @@ impl Xdm {
         // Assume some instructions complete practically instantly and don't waste bandwidth
         // checking their results.
         if !matches!(
-            self.last_instruction,
+            self.state.last_instruction,
             Some(
                 Instruction::Lddr32P(_)
                     | Instruction::Sddr32P(_)
@@ -593,10 +606,6 @@ impl Xdm {
 
         Ok(())
     }
-
-    pub(super) fn free(self) -> Box<dyn JTAGAccess> {
-        self.probe
-    }
 }
 
 type TransformFn = fn(&JtagWriteCommand, Vec<u8>) -> Result<CommandResult, ProbeRsError>;
diff --git a/probe-rs/src/config/registry.rs b/probe-rs/src/config/registry.rs
index ad662d59..577e5584 100644
--- a/probe-rs/src/config/registry.rs
+++ b/probe-rs/src/config/registry.rs
@@ -97,6 +97,7 @@ fn add_generic_targets(vec: &mut Vec<ChipFamily>) {
                     core_type: CoreType::Riscv,
                     core_access_options: CoreAccessOptions::Riscv(RiscvCoreAccessOptions {
                         hart_id: None,
+                        jtag_tap: None,
                     }),
                 }],
                 memory_map: vec![],
diff --git a/probe-rs/src/config/sequences/esp.rs b/probe-rs/src/config/sequences/esp.rs
index fc3cf186..4eb74550 100644
--- a/probe-rs/src/config/sequences/esp.rs
+++ b/probe-rs/src/config/sequences/esp.rs
@@ -6,7 +6,10 @@ use crate::{
     architecture::{
         riscv::communication_interface::RiscvCommunicationInterface,
         xtensa::{
-            arch::{instruction::Instruction, CpuRegister, Register},
+            arch::{
+                instruction::{into_binary, Instruction},
+                CpuRegister, Register,
+            },
             communication_interface::XtensaCommunicationInterface,
             sequences::XtensaDebugSequence,
         },
@@ -130,10 +133,11 @@ fn attach_flash_xtensa(
     // We're very intrusive here but the flashing process should reset the MCU again anyway
     sequence.reset_system_and_halt(interface, Duration::from_millis(500))?;
 
-    let mut instructions = vec![];
-    Instruction::CallX8(CpuRegister::A4).encode_into_vec(&mut instructions);
-    // Set a breakpoint at the end of the code
-    Instruction::Break(0, 0).encode_into_vec(&mut instructions);
+    let instructions = into_binary([
+        Instruction::CallX8(CpuRegister::A4),
+        // Set a breakpoint at the end of the code
+        Instruction::Break(0, 0),
+    ]);
 
     // Download code
     interface.write_8(load_addr as u64, &instructions)?;
diff --git a/probe-rs/src/core.rs b/probe-rs/src/core.rs
index 19b5d339..bd61c406 100644
--- a/probe-rs/src/core.rs
+++ b/probe-rs/src/core.rs
@@ -922,6 +922,15 @@ pub enum ResolvedCoreOptions {
         options: XtensaCoreAccessOptions,
     },
 }
+impl ResolvedCoreOptions {
+    fn interface_idx(&self) -> usize {
+        match self {
+            Self::Arm { .. } => 0, // TODO
+            Self::Riscv { options, .. } => options.jtag_tap.unwrap_or(0),
+            Self::Xtensa { options, .. } => options.jtag_tap.unwrap_or(0),
+        }
+    }
+}
 
 impl std::fmt::Debug for ResolvedCoreOptions {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
diff --git a/probe-rs/src/core/core_state.rs b/probe-rs/src/core/core_state.rs
index 9e31873b..af009e64 100644
--- a/probe-rs/src/core/core_state.rs
+++ b/probe-rs/src/core/core_state.rs
@@ -5,8 +5,11 @@ use crate::{
             core::{CortexAState, CortexMState},
             ApAddress, ArmProbeInterface, DpAddress,
         },
-        riscv::{communication_interface::RiscvCommunicationInterface, RiscVState},
-        xtensa::{communication_interface::XtensaCommunicationInterface, XtensaState},
+        riscv::{
+            communication_interface::{RiscvCommunicationInterface, RiscvError},
+            RiscvCoreState,
+        },
+        xtensa::{communication_interface::XtensaCommunicationInterface, XtensaCoreState},
     },
     Core, CoreType, Error, Target,
 };
@@ -31,6 +34,10 @@ impl CombinedCoreState {
         self.specific_state.core_type()
     }
 
+    pub fn interface_idx(&self) -> usize {
+        self.core_state.core_access_options.interface_idx()
+    }
+
     pub(crate) fn attach_arm<'probe>(
         &'probe mut self,
         target: &'probe Target,
@@ -153,7 +160,7 @@ impl CombinedCoreState {
     pub(crate) fn attach_riscv<'probe>(
         &'probe mut self,
         target: &'probe Target,
-        interface: &'probe mut RiscvCommunicationInterface,
+        mut interface: RiscvCommunicationInterface<'probe>,
     ) -> Result<Core<'probe>, Error> {
         let memory_regions = &target.memory_map;
         let name = &target.cores[self.id].name;
@@ -174,23 +181,25 @@ impl CombinedCoreState {
             );
         };
 
+        let hart = options.hart_id.unwrap_or_default();
+        if !interface.hart_enabled(hart) {
+            return Err(RiscvError::HartUnavailable.into());
+        }
+
+        interface.select_hart(hart)?;
+
         Ok(Core::new(
             self.id,
             name,
             memory_regions,
-            crate::architecture::riscv::Riscv32::new(
-                options.hart_id.unwrap_or_default(),
-                interface,
-                s,
-                debug_sequence,
-            )?,
+            crate::architecture::riscv::Riscv32::new(interface, s, debug_sequence)?,
         ))
     }
 
     pub(crate) fn attach_xtensa<'probe>(
         &'probe mut self,
         target: &'probe Target,
-        interface: &'probe mut XtensaCommunicationInterface,
+        interface: XtensaCommunicationInterface<'probe>,
     ) -> Result<Core<'probe>, Error> {
         let memory_regions = &target.memory_map;
         let name = &target.cores[self.id].name;
@@ -279,9 +288,9 @@ pub enum SpecificCoreState {
     /// The state of an ARMv8-M core.
     Armv8m(CortexMState),
     /// The state of an RISC-V core.
-    Riscv(RiscVState),
+    Riscv(RiscvCoreState),
     /// The state of an Xtensa core.
-    Xtensa(XtensaState),
+    Xtensa(XtensaCoreState),
 }
 
 impl SpecificCoreState {
@@ -293,8 +302,8 @@ impl SpecificCoreState {
             CoreType::Armv7em => SpecificCoreState::Armv7m(CortexMState::new()),
             CoreType::Armv8a => SpecificCoreState::Armv8a(CortexAState::new()),
             CoreType::Armv8m => SpecificCoreState::Armv8m(CortexMState::new()),
-            CoreType::Riscv => SpecificCoreState::Riscv(RiscVState::new()),
-            CoreType::Xtensa => SpecificCoreState::Xtensa(XtensaState::new()),
+            CoreType::Riscv => SpecificCoreState::Riscv(RiscvCoreState::new()),
+            CoreType::Xtensa => SpecificCoreState::Xtensa(XtensaCoreState::new()),
         }
     }
 
diff --git a/probe-rs/src/flashing/loader.rs b/probe-rs/src/flashing/loader.rs
index 0a6d04b7..86aa9206 100644
--- a/probe-rs/src/flashing/loader.rs
+++ b/probe-rs/src/flashing/loader.rs
@@ -185,10 +185,10 @@ impl FlashLoader {
             // Figure out flash size from the memory map. We need a different bootloader for each size.
             Ok(match sess.target().debug_sequence.clone() {
                 DebugSequence::Riscv(sequence) => {
-                    sequence.detect_flash_size(sess.get_riscv_interface()?)
+                    sequence.detect_flash_size(&mut sess.get_riscv_interface(0)?)
                 }
                 DebugSequence::Xtensa(sequence) => {
-                    sequence.detect_flash_size(sess.get_xtensa_interface()?)
+                    sequence.detect_flash_size(&mut sess.get_xtensa_interface(0)?)
                 }
                 DebugSequence::Arm(_) => panic!("There are no ARM ESP targets."),
             })
diff --git a/probe-rs/src/probe.rs b/probe-rs/src/probe.rs
index a31d5127..0125308a 100644
--- a/probe-rs/src/probe.rs
+++ b/probe-rs/src/probe.rs
@@ -18,11 +18,9 @@ use crate::architecture::arm::{
     communication_interface::{DapProbe, UninitializedArmProbe},
     PortType, SwoAccess,
 };
-use crate::architecture::riscv::communication_interface::{
-    RiscvCommunicationInterface, RiscvError,
-};
+use crate::architecture::riscv::communication_interface::{RiscvError, RiscvInterfaceBuilder};
 use crate::architecture::xtensa::communication_interface::{
-    XtensaCommunicationInterface, XtensaError,
+    XtensaCommunicationInterface, XtensaDebugInterfaceState, XtensaError,
 };
 use crate::config::RegistryError;
 use crate::config::TargetSelector;
@@ -388,6 +386,16 @@ impl Probe {
         }
     }
 
+    /// Returns the JTAG scan chain
+    pub fn scan_chain(&self) -> Result<&[ScanChainElement], DebugProbeError> {
+        self.inner.scan_chain()
+    }
+
+    /// Selects the JTAG TAP to be used for communication.
+    pub fn select_jtag_tap(&mut self, index: usize) -> Result<(), DebugProbeError> {
+        self.inner.select_jtag_tap(index)
+    }
+
     /// Get the currently used maximum speed for the debug protocol in kHz.
     ///
     /// Not all probes report which speed is used, meaning this value is not
@@ -407,16 +415,18 @@ impl Probe {
     /// can be used to communicate with chips using the Xtensa
     /// architecture.
     ///
+    /// The user is responsible for creating and managing the [`XtensaDebugInterfaceState`] state
+    /// object.
+    ///
     /// If an error occurs while trying to connect, the probe is returned.
-    pub fn try_into_xtensa_interface(
-        self,
-    ) -> Result<XtensaCommunicationInterface, (Self, DebugProbeError)> {
+    pub fn try_get_xtensa_interface<'probe>(
+        &'probe mut self,
+        state: &'probe mut XtensaDebugInterfaceState,
+    ) -> Result<XtensaCommunicationInterface<'probe>, DebugProbeError> {
         if !self.attached {
-            Err((self, DebugProbeError::NotAttached))
+            Err(DebugProbeError::NotAttached)
         } else {
-            self.inner
-                .try_get_xtensa_interface()
-                .map_err(|(probe, err)| (Probe::from_attached_probe(probe), err))
+            Ok(self.inner.try_get_xtensa_interface(state)?)
         }
     }
 
@@ -442,26 +452,25 @@ impl Probe {
         }
     }
 
-    /// Check if the probe has an interface to
-    /// debug RISC-V chips.
+    /// Check if the probe has an interface to debug RISC-V chips.
     pub fn has_riscv_interface(&self) -> bool {
         self.inner.has_riscv_interface()
     }
 
-    /// Try to get a [`RiscvCommunicationInterface`], which can
-    /// can be used to communicate with chips using the RISC-V
-    /// architecture.
+    /// Try to get a [`RiscvInterfaceBuilder`] object, which can be used to set up a communication
+    /// interface with chips using the RISC-V architecture.
+    ///
+    /// The returned object can be used to create the interface state, which is required to
+    /// attach to the RISC-V target. The user is responsible for managing this state object.
     ///
     /// If an error occurs while trying to connect, the probe is returned.
-    pub fn try_into_riscv_interface(
-        self,
-    ) -> Result<RiscvCommunicationInterface, (Self, RiscvError)> {
+    pub fn try_get_riscv_interface_builder<'probe>(
+        &'probe mut self,
+    ) -> Result<Box<dyn RiscvInterfaceBuilder<'probe> + 'probe>, DebugProbeError> {
         if !self.attached {
-            Err((self, DebugProbeError::NotAttached.into()))
+            Err(DebugProbeError::NotAttached)
         } else {
-            self.inner
-                .try_get_riscv_interface()
-                .map_err(|(probe, err)| (Probe::from_attached_probe(probe), err))
+            self.inner.try_get_riscv_interface_builder()
         }
     }
 
@@ -559,11 +568,25 @@ pub trait DebugProbe: Send + fmt::Debug {
     ///
     fn set_scan_chain(&mut self, scan_chain: Vec<ScanChainElement>) -> Result<(), DebugProbeError>;
 
+    /// Returns the JTAG scan chain
+    fn scan_chain(&self) -> Result<&[ScanChainElement], DebugProbeError>;
+
     /// Attach to the chip.
     ///
     /// This should run all the necessary protocol init routines.
     fn attach(&mut self) -> Result<(), DebugProbeError>;
 
+    /// Selects the JTAG TAP to be used for communication.
+    fn select_jtag_tap(&mut self, index: usize) -> Result<(), DebugProbeError> {
+        if index != 0 {
+            return Err(DebugProbeError::NotImplemented {
+                function_name: "select_jtag_tap",
+            });
+        }
+
+        Ok(())
+    }
+
     /// Detach from the chip.
     ///
     /// This should run all the necessary protocol deinit routines.
@@ -607,18 +630,17 @@ pub trait DebugProbe: Send + fmt::Debug {
         ))
     }
 
-    /// Get the dedicated interface to debug RISC-V chips. Ensure that the
-    /// probe actually supports this by calling [DebugProbe::has_riscv_interface] first.
-    fn try_get_riscv_interface(
-        self: Box<Self>,
-    ) -> Result<RiscvCommunicationInterface, (Box<dyn DebugProbe>, RiscvError)> {
-        Err((
-            self.into_probe(),
-            DebugProbeError::InterfaceNotAvailable {
-                interface_name: "RISC-V",
-            }
-            .into(),
-        ))
+    /// Try to get a [`RiscvInterfaceBuilder`] object, which can be used to set up a communication
+    /// interface with chips using the RISC-V architecture.
+    ///
+    /// Ensure that the probe actually supports this by calling
+    /// [DebugProbe::has_riscv_interface] first.
+    fn try_get_riscv_interface_builder<'probe>(
+        &'probe mut self,
+    ) -> Result<Box<dyn RiscvInterfaceBuilder<'probe> + 'probe>, DebugProbeError> {
+        Err(DebugProbeError::InterfaceNotAvailable {
+            interface_name: "RISC-V",
+        })
     }
 
     /// Check if the probe offers an interface to debug RISC-V chips.
@@ -628,15 +650,13 @@ pub trait DebugProbe: Send + fmt::Debug {
 
     /// Get the dedicated interface to debug Xtensa chips. Ensure that the
     /// probe actually supports this by calling [DebugProbe::has_xtensa_interface] first.
-    fn try_get_xtensa_interface(
-        self: Box<Self>,
-    ) -> Result<XtensaCommunicationInterface, (Box<dyn DebugProbe>, DebugProbeError)> {
-        Err((
-            self.into_probe(),
-            DebugProbeError::InterfaceNotAvailable {
-                interface_name: "Xtensa",
-            },
-        ))
+    fn try_get_xtensa_interface<'probe>(
+        &'probe mut self,
+        _state: &'probe mut XtensaDebugInterfaceState,
+    ) -> Result<XtensaCommunicationInterface<'probe>, DebugProbeError> {
+        Err(DebugProbeError::InterfaceNotAvailable {
+            interface_name: "Xtensa",
+        })
     }
 
     /// Check if the probe offers an interface to debug Xtensa chips.
@@ -890,11 +910,13 @@ impl fmt::Display for DebugProbeSelector {
 /// This trait should be implemented by all probes which offer low-level access to
 /// the JTAG protocol, i.e. direction control over the bytes sent and received.
 pub trait JTAGAccess: DebugProbe {
-    /// Returns `IDCODE` and `IR` length information about the devices on the JTAG chain.
+    /// Scans `IDCODE` and `IR` length information about the devices on the JTAG chain.
     ///
     /// If configured, this will use the data from [`DebugProbe::set_scan_chain`]. Otherwise, it
     /// will try to measure and extract `IR` lengths by driving the JTAG interface.
-    fn scan_chain(&mut self) -> Result<Vec<JtagChainItem>, DebugProbeError>;
+    ///
+    /// The measured scan chain will be stored in the probe's internal state.
+    fn scan_chain(&mut self) -> Result<(), DebugProbeError>;
 
     /// Executes a TAP reset.
     fn tap_reset(&mut self) -> Result<(), DebugProbeError>;
diff --git a/probe-rs/src/probe/arm_debug_interface.rs b/probe-rs/src/probe/arm_debug_interface.rs
index 935386f3..c0d3dc8d 100644
--- a/probe-rs/src/probe/arm_debug_interface.rs
+++ b/probe-rs/src/probe/arm_debug_interface.rs
@@ -1320,9 +1320,7 @@ mod test {
     use crate::{
         architecture::arm::{PortType, RawDapAccess},
         error::Error,
-        probe::{
-            DebugProbe, DebugProbeError, JTAGAccess, JtagChainItem, ScanChainElement, WireProtocol,
-        },
+        probe::{DebugProbe, DebugProbeError, JTAGAccess, ScanChainElement, WireProtocol},
     };
 
     use super::{
@@ -1529,7 +1527,7 @@ mod test {
     }
 
     impl JTAGAccess for MockJaylink {
-        fn scan_chain(&mut self) -> Result<Vec<JtagChainItem>, DebugProbeError> {
+        fn scan_chain(&mut self) -> Result<(), DebugProbeError> {
             todo!()
         }
 
@@ -1672,6 +1670,10 @@ mod test {
             todo!()
         }
 
+        fn scan_chain(&self) -> Result<&[ScanChainElement], DebugProbeError> {
+            todo!()
+        }
+
         fn attach(&mut self) -> Result<(), DebugProbeError> {
             todo!()
         }
diff --git a/probe-rs/src/probe/cmsisdap/mod.rs b/probe-rs/src/probe/cmsisdap/mod.rs
index a8a15c29..feeb5350 100644
--- a/probe-rs/src/probe/cmsisdap/mod.rs
+++ b/probe-rs/src/probe/cmsisdap/mod.rs
@@ -755,6 +755,22 @@ impl DebugProbe for CmsisDap {
         Ok(())
     }
 
+    /// Returns the JTAG scan chain
+    fn scan_chain(&self) -> Result<&[ScanChainElement], DebugProbeError> {
+        match self.active_protocol() {
+            Some(WireProtocol::Jtag) => {
+                if let Some(ref chain) = self.scan_chain {
+                    Ok(chain.as_slice())
+                } else {
+                    Ok(&[])
+                }
+            }
+            _ => Err(DebugProbeError::InterfaceNotAvailable {
+                interface_name: "JTAG",
+            }),
+        }
+    }
+
     /// Enters debug mode.
     #[tracing::instrument(skip(self))]
     fn attach(&mut self) -> Result<(), DebugProbeError> {
diff --git a/probe-rs/src/probe/common.rs b/probe-rs/src/probe/common.rs
index ce1c2234..ce8ddec2 100644
--- a/probe-rs/src/probe/common.rs
+++ b/probe-rs/src/probe/common.rs
@@ -379,6 +379,7 @@ pub(crate) struct JtagDriverState {
     // The maximum IR address
     pub max_ir_address: u32,
     pub expected_scan_chain: Option<Vec<ScanChainElement>>,
+    pub scan_chain: Vec<JtagChainItem>,
     pub chain_params: ChainParams,
     /// Idle cycles necessary between consecutive
     /// accesses to the DMI register
@@ -392,6 +393,7 @@ impl Default for JtagDriverState {
             current_ir_reg: 1,
             max_ir_address: 0x0F,
             expected_scan_chain: None,
+            scan_chain: Vec::new(),
             chain_params: ChainParams::default(),
             jtag_idle_cycles: 0,
         }
@@ -446,12 +448,10 @@ pub(crate) trait RawJtagIo {
     }
 
     /// Configures the probe to address the given target.
-    fn select_target(
-        &mut self,
-        chain: &[JtagChainItem],
-        target: usize,
-    ) -> Result<(), DebugProbeError> {
-        let Some(params) = ChainParams::from_jtag_chain(chain, target) else {
+    fn select_target(&mut self, target: usize) -> Result<(), DebugProbeError> {
+        let state = self.state_mut();
+
+        let Some(params) = ChainParams::from_jtag_chain(&state.scan_chain, target) else {
             return Err(DebugProbeError::TargetNotFound);
         };
 
@@ -630,7 +630,7 @@ fn prepare_write_register(
 }
 
 impl<Probe: DebugProbe + RawJtagIo + 'static> JTAGAccess for Probe {
-    fn scan_chain(&mut self) -> Result<Vec<JtagChainItem>, DebugProbeError> {
+    fn scan_chain(&mut self) -> Result<(), DebugProbeError> {
         const MAX_CHAIN: usize = 8;
 
         self.reset_jtag_state_machine()?;
@@ -695,13 +695,18 @@ impl<Probe: DebugProbe + RawJtagIo + 'static> JTAGAccess for Probe {
         )
         .map_err(|e| DebugProbeError::Other(e.into()))?;
 
+        tracing::info!("Found {} TAPs on reset scan", idcodes.len());
         tracing::debug!("Detected IR lens: {:?}", ir_lens);
 
-        Ok(idcodes
+        let chain = idcodes
             .into_iter()
             .zip(ir_lens)
             .map(|(idcode, irlen)| JtagChainItem { irlen, idcode })
-            .collect())
+            .collect::<Vec<_>>();
+
+        self.state_mut().scan_chain = chain;
+
+        Ok(())
     }
 
     fn tap_reset(&mut self) -> Result<(), DebugProbeError> {
diff --git a/probe-rs/src/probe/espusbjtag/mod.rs b/probe-rs/src/probe/espusbjtag/mod.rs
index db01a24f..806a2319 100644
--- a/probe-rs/src/probe/espusbjtag/mod.rs
+++ b/probe-rs/src/probe/espusbjtag/mod.rs
@@ -7,8 +7,10 @@ use crate::{
             communication_interface::{DapProbe, UninitializedArmProbe},
             SwoAccess,
         },
-        riscv::communication_interface::{RiscvCommunicationInterface, RiscvError},
-        xtensa::communication_interface::XtensaCommunicationInterface,
+        riscv::{communication_interface::RiscvInterfaceBuilder, dtm::jtag_dtm::JtagDtmBuilder},
+        xtensa::communication_interface::{
+            XtensaCommunicationInterface, XtensaDebugInterfaceState,
+        },
     },
     probe::{
         common::RawJtagIo, DebugProbe, DebugProbeError, DebugProbeInfo, DebugProbeSelector,
@@ -21,7 +23,6 @@ use self::protocol::ProtocolHandler;
 
 use super::{common::JtagDriverState, JTAGAccess};
 
-use crate::architecture::riscv::dtm::jtag_dtm::JtagDtm;
 use probe_rs_target::ScanChainElement;
 
 /// Probe factory for USB JTAG interfaces built into certain ESP32 chips.
@@ -116,17 +117,23 @@ impl DebugProbe for EspUsbJtag {
         Ok(())
     }
 
-    fn attach(&mut self) -> Result<(), DebugProbeError> {
-        tracing::debug!("Attaching to ESP USB JTAG");
+    fn scan_chain(&self) -> Result<&[ScanChainElement], DebugProbeError> {
+        if let Some(ref scan_chain) = self.jtag_state.expected_scan_chain {
+            Ok(scan_chain)
+        } else {
+            Ok(&[])
+        }
+    }
 
-        let chain = self.scan_chain()?;
-        tracing::info!("Found {} TAPs on reset scan", chain.len());
+    fn select_jtag_tap(&mut self, index: usize) -> Result<(), DebugProbeError> {
+        self.select_target(index)
+    }
 
-        if chain.len() > 1 {
-            tracing::info!("More than one TAP detected, defaulting to tap0");
-        }
+    fn attach(&mut self) -> Result<(), DebugProbeError> {
+        tracing::debug!("Attaching to ESP USB JTAG");
 
-        self.select_target(&chain, 0)
+        self.scan_chain()?;
+        self.select_target(0)
     }
 
     fn detach(&mut self) -> Result<(), crate::Error> {
@@ -151,18 +158,10 @@ impl DebugProbe for EspUsbJtag {
         Ok(())
     }
 
-    fn try_get_riscv_interface(
-        self: Box<Self>,
-    ) -> Result<RiscvCommunicationInterface, (Box<dyn DebugProbe>, RiscvError)> {
-        let jtag_dtm = match JtagDtm::new(self) {
-            Ok(dtm) => Box::new(dtm),
-            Err((probe, err)) => return Err((probe.into_probe(), err)),
-        };
-        // This probe is intended for RISC-V.
-        match RiscvCommunicationInterface::new(jtag_dtm) {
-            Ok(interface) => Ok(interface),
-            Err((probe, err)) => Err((probe.into_probe(), err)),
-        }
+    fn try_get_riscv_interface_builder<'probe>(
+        &'probe mut self,
+    ) -> Result<Box<dyn RiscvInterfaceBuilder<'probe> + 'probe>, DebugProbeError> {
+        Ok(Box::new(JtagDtmBuilder::new(self)))
     }
 
     fn get_swo_interface(&self) -> Option<&dyn SwoAccess> {
@@ -212,14 +211,11 @@ impl DebugProbe for EspUsbJtag {
         Ok(None)
     }
 
-    fn try_get_xtensa_interface(
-        self: Box<Self>,
-    ) -> Result<XtensaCommunicationInterface, (Box<dyn DebugProbe>, DebugProbeError)> {
-        // This probe is intended for Xtensa.
-        match XtensaCommunicationInterface::new(self) {
-            Ok(interface) => Ok(interface),
-            Err((probe, err)) => Err((probe.into_probe(), err)),
-        }
+    fn try_get_xtensa_interface<'probe>(
+        &'probe mut self,
+        state: &'probe mut XtensaDebugInterfaceState,
+    ) -> Result<XtensaCommunicationInterface<'probe>, DebugProbeError> {
+        Ok(XtensaCommunicationInterface::new(self, state))
     }
 
     fn has_xtensa_interface(&self) -> bool {
diff --git a/probe-rs/src/probe/fake_probe.rs b/probe-rs/src/probe/fake_probe.rs
index b1cddb79..75c43d28 100644
--- a/probe-rs/src/probe/fake_probe.rs
+++ b/probe-rs/src/probe/fake_probe.rs
@@ -316,6 +316,15 @@ impl DebugProbe for FakeProbe {
         Ok(())
     }
 
+    fn scan_chain(&self) -> Result<&[ScanChainElement], DebugProbeError> {
+        match &self.scan_chain {
+            Some(chain) => Ok(chain),
+            None => Err(DebugProbeError::Other(anyhow::anyhow!(
+                "No scan chain set for fake probe"
+            ))),
+        }
+    }
+
     fn set_speed(&mut self, speed_khz: u32) -> Result<u32, DebugProbeError> {
         self.speed = speed_khz;
 
diff --git a/probe-rs/src/probe/ftdi/mod.rs b/probe-rs/src/probe/ftdi/mod.rs
index c34ea1b6..8071b7ee 100644
--- a/probe-rs/src/probe/ftdi/mod.rs
+++ b/probe-rs/src/probe/ftdi/mod.rs
@@ -5,8 +5,10 @@ use crate::{
             communication_interface::{DapProbe, UninitializedArmProbe},
             ArmCommunicationInterface,
         },
-        riscv::communication_interface::{RiscvCommunicationInterface, RiscvError},
-        xtensa::communication_interface::XtensaCommunicationInterface,
+        riscv::{communication_interface::RiscvInterfaceBuilder, dtm::jtag_dtm::JtagDtmBuilder},
+        xtensa::communication_interface::{
+            XtensaCommunicationInterface, XtensaDebugInterfaceState,
+        },
     },
     probe::{
         arm_debug_interface::{ProbeStatistics, RawProtocolIo, SwdSettings},
@@ -27,7 +29,6 @@ use std::{
 mod command_compacter;
 mod ftdaye;
 
-use crate::architecture::riscv::dtm::jtag_dtm::JtagDtm;
 use command_compacter::Command;
 use ftdaye::{error::FtdiError, ChipType};
 
@@ -321,6 +322,14 @@ impl DebugProbe for FtdiProbe {
         Ok(())
     }
 
+    fn scan_chain(&self) -> Result<&[ScanChainElement], DebugProbeError> {
+        if let Some(ref scan_chain) = self.jtag_state.expected_scan_chain {
+            Ok(scan_chain)
+        } else {
+            Ok(&[])
+        }
+    }
+
     fn attach(&mut self) -> Result<(), DebugProbeError> {
         tracing::debug!("Attaching...");
 
@@ -328,14 +337,12 @@ impl DebugProbe for FtdiProbe {
             .attach()
             .map_err(|e| DebugProbeError::ProbeSpecific(Box::new(e)))?;
 
-        let chain = self.scan_chain()?;
-        tracing::info!("Found {} TAPs on reset scan", chain.len());
-
-        if chain.len() > 1 {
-            tracing::info!("More than one TAP detected, defaulting to tap0");
-        }
+        self.scan_chain()?;
+        self.select_target(0)
+    }
 
-        self.select_target(&chain, 0)
+    fn select_jtag_tap(&mut self, index: usize) -> Result<(), DebugProbeError> {
+        self.select_target(index)
     }
 
     fn detach(&mut self) -> Result<(), crate::Error> {
@@ -375,17 +382,10 @@ impl DebugProbe for FtdiProbe {
         Some(WireProtocol::Jtag)
     }
 
-    fn try_get_riscv_interface(
-        self: Box<Self>,
-    ) -> Result<RiscvCommunicationInterface, (Box<dyn DebugProbe>, RiscvError)> {
-        let jtag_dtm = match JtagDtm::new(self) {
-            Ok(jtag_dtm) => Box::new(jtag_dtm),
-            Err((access, err)) => return Err((access.into_probe(), err)),
-        };
-        match RiscvCommunicationInterface::new(jtag_dtm) {
-            Ok(interface) => Ok(interface),
-            Err((probe, err)) => Err((probe.into_probe(), err)),
-        }
+    fn try_get_riscv_interface_builder<'probe>(
+        &'probe mut self,
+    ) -> Result<Box<dyn RiscvInterfaceBuilder<'probe> + 'probe>, DebugProbeError> {
+        Ok(Box::new(JtagDtmBuilder::new(self)))
     }
 
     fn has_riscv_interface(&self) -> bool {
@@ -409,13 +409,11 @@ impl DebugProbe for FtdiProbe {
         true
     }
 
-    fn try_get_xtensa_interface(
-        self: Box<Self>,
-    ) -> Result<XtensaCommunicationInterface, (Box<dyn DebugProbe>, DebugProbeError)> {
-        match XtensaCommunicationInterface::new(self) {
-            Ok(interface) => Ok(interface),
-            Err((probe, err)) => Err((probe.into_probe(), err)),
-        }
+    fn try_get_xtensa_interface<'probe>(
+        &'probe mut self,
+        state: &'probe mut XtensaDebugInterfaceState,
+    ) -> Result<XtensaCommunicationInterface<'probe>, DebugProbeError> {
+        Ok(XtensaCommunicationInterface::new(self, state))
     }
 
     fn has_xtensa_interface(&self) -> bool {
diff --git a/probe-rs/src/probe/jlink/mod.rs b/probe-rs/src/probe/jlink/mod.rs
index 5334216c..d0a08f0a 100644
--- a/probe-rs/src/probe/jlink/mod.rs
+++ b/probe-rs/src/probe/jlink/mod.rs
@@ -29,25 +29,26 @@ use self::interface::{Interface, Interfaces};
 use self::speed::SpeedConfig;
 use self::swo::SwoMode;
 use crate::architecture::arm::{ArmError, Pins};
-use crate::architecture::riscv::communication_interface::RiscvError;
-use crate::architecture::riscv::dtm::jtag_dtm::JtagDtm;
-use crate::architecture::xtensa::communication_interface::XtensaCommunicationInterface;
+use crate::architecture::xtensa::communication_interface::{
+    XtensaCommunicationInterface, XtensaDebugInterfaceState,
+};
 use crate::probe::common::{JtagDriverState, RawJtagIo};
 use crate::probe::jlink::bits::IteratorExt;
 use crate::probe::usb_util::InterfaceExt;
 use crate::probe::JTAGAccess;
-use crate::probe::ProbeFactory;
 use crate::{
     architecture::{
         arm::{
-            communication_interface::DapProbe, communication_interface::UninitializedArmProbe,
-            swo::SwoConfig, ArmCommunicationInterface, SwoAccess,
+            communication_interface::{DapProbe, UninitializedArmProbe},
+            swo::SwoConfig,
+            ArmCommunicationInterface, SwoAccess,
         },
-        riscv::communication_interface::RiscvCommunicationInterface,
+        riscv::{communication_interface::RiscvInterfaceBuilder, dtm::jtag_dtm::JtagDtmBuilder},
     },
     probe::{
         arm_debug_interface::{ProbeStatistics, RawProtocolIo, SwdSettings},
-        DebugProbe, DebugProbeError, DebugProbeInfo, DebugProbeSelector, WireProtocol,
+        DebugProbe, DebugProbeError, DebugProbeInfo, DebugProbeSelector, ProbeFactory,
+        WireProtocol,
     },
 };
 
@@ -892,14 +893,8 @@ impl DebugProbe for JLink {
                 tracing::debug!("Resetting JTAG chain using trst");
                 self.reset_trst()?;
 
-                let chain = self.scan_chain()?;
-                tracing::info!("Found {} TAPs on reset scan", chain.len());
-
-                if chain.len() > 1 {
-                    tracing::info!("More than one TAP detected, defaulting to tap0");
-                }
-
-                self.select_target(&chain, 0)?;
+                self.scan_chain()?;
+                self.select_target(0)?;
             }
             WireProtocol::Swd => {
                 // Attaching is handled in sequence
@@ -913,6 +908,25 @@ impl DebugProbe for JLink {
         Ok(())
     }
 
+    fn select_jtag_tap(&mut self, index: usize) -> Result<(), DebugProbeError> {
+        self.select_target(index)
+    }
+
+    fn scan_chain(&self) -> Result<&[ScanChainElement], DebugProbeError> {
+        match self.active_protocol() {
+            Some(WireProtocol::Jtag) => {
+                if let Some(ref scan_chain) = self.jtag_state.expected_scan_chain {
+                    Ok(scan_chain)
+                } else {
+                    Ok(&[])
+                }
+            }
+            _ => Err(DebugProbeError::InterfaceNotAvailable {
+                interface_name: "JTAG",
+            }),
+        }
+    }
+
     fn detach(&mut self) -> Result<(), crate::Error> {
         Ok(())
     }
@@ -932,29 +946,16 @@ impl DebugProbe for JLink {
         Ok(())
     }
 
-    fn try_get_riscv_interface(
-        mut self: Box<Self>,
-    ) -> Result<RiscvCommunicationInterface, (Box<dyn DebugProbe>, RiscvError)> {
+    fn try_get_riscv_interface_builder<'probe>(
+        &'probe mut self,
+    ) -> Result<Box<dyn RiscvInterfaceBuilder<'probe> + 'probe>, DebugProbeError> {
         if self.supported_protocols.contains(&WireProtocol::Jtag) {
-            if let Err(e) = self.select_protocol(WireProtocol::Jtag) {
-                return Err((self, e.into()));
-            }
-            let jtag_dtm = match JtagDtm::new(self) {
-                Ok(jtag_dtm) => Box::new(jtag_dtm),
-                Err((access, err)) => return Err((access.into_probe(), err)),
-            };
-            match RiscvCommunicationInterface::new(jtag_dtm) {
-                Ok(interface) => Ok(interface),
-                Err((probe, err)) => Err((probe.into_probe(), err)),
-            }
+            self.select_protocol(WireProtocol::Jtag)?;
+            Ok(Box::new(JtagDtmBuilder::new(self)))
         } else {
-            Err((
-                self,
-                DebugProbeError::InterfaceNotAvailable {
-                    interface_name: "JTAG",
-                }
-                .into(),
-            ))
+            Err(DebugProbeError::InterfaceNotAvailable {
+                interface_name: "JTAG",
+            })
         }
     }
 
@@ -996,24 +997,17 @@ impl DebugProbe for JLink {
         Ok(Some((self.read_target_voltage()? as f32) / 1000f32))
     }
 
-    fn try_get_xtensa_interface(
-        mut self: Box<Self>,
-    ) -> Result<XtensaCommunicationInterface, (Box<dyn DebugProbe>, DebugProbeError)> {
+    fn try_get_xtensa_interface<'probe>(
+        &'probe mut self,
+        state: &'probe mut XtensaDebugInterfaceState,
+    ) -> Result<XtensaCommunicationInterface<'probe>, DebugProbeError> {
         if self.supported_protocols.contains(&WireProtocol::Jtag) {
-            if let Err(e) = self.select_protocol(WireProtocol::Jtag) {
-                return Err((self, e));
-            }
-            match XtensaCommunicationInterface::new(self) {
-                Ok(interface) => Ok(interface),
-                Err((probe, err)) => Err((probe.into_probe(), err)),
-            }
+            self.select_protocol(WireProtocol::Jtag)?;
+            Ok(XtensaCommunicationInterface::new(self, state))
         } else {
-            Err((
-                self,
-                DebugProbeError::InterfaceNotAvailable {
-                    interface_name: "JTAG",
-                },
-            ))
+            Err(DebugProbeError::InterfaceNotAvailable {
+                interface_name: "JTAG",
+            })
         }
     }
 
diff --git a/probe-rs/src/probe/stlink/mod.rs b/probe-rs/src/probe/stlink/mod.rs
index 8879b723..3b6ba21b 100644
--- a/probe-rs/src/probe/stlink/mod.rs
+++ b/probe-rs/src/probe/stlink/mod.rs
@@ -165,6 +165,21 @@ impl DebugProbe for StLink<StLinkUsbDevice> {
         Ok(())
     }
 
+    fn scan_chain(&self) -> Result<&[ScanChainElement], DebugProbeError> {
+        match self.active_protocol() {
+            Some(WireProtocol::Jtag) => {
+                if let Some(ref scan_chain) = self.scan_chain {
+                    Ok(scan_chain)
+                } else {
+                    Ok(&[])
+                }
+            }
+            _ => Err(DebugProbeError::InterfaceNotAvailable {
+                interface_name: "JTAG",
+            }),
+        }
+    }
+
     #[tracing::instrument(skip(self))]
     fn attach(&mut self) -> Result<(), DebugProbeError> {
         self.enter_idle()?;
diff --git a/probe-rs/src/probe/wlink/mod.rs b/probe-rs/src/probe/wlink/mod.rs
index cd3ed900..7f9b4de5 100644
--- a/probe-rs/src/probe/wlink/mod.rs
+++ b/probe-rs/src/probe/wlink/mod.rs
@@ -3,7 +3,6 @@
 //! The protocol is mostly undocumented, and is changing between firmware versions.
 //! For more details see: <https://github.com/ch32-rs/wlink>
 
-use crate::architecture::riscv::dtm::jtag_dtm::JtagDtm;
 use core::fmt;
 use std::time::Duration;
 
@@ -13,10 +12,12 @@ use probe_rs_target::ScanChainElement;
 use self::{commands::Speed, usb_interface::WchLinkUsbDevice};
 use super::JTAGAccess;
 use crate::{
-    architecture::riscv::communication_interface::{RiscvCommunicationInterface, RiscvError},
+    architecture::riscv::{
+        communication_interface::RiscvInterfaceBuilder, dtm::jtag_dtm::JtagDtmBuilder,
+    },
     probe::{
-        DebugProbe, DebugProbeError, DebugProbeInfo, DebugProbeSelector, JtagChainItem,
-        ProbeCreationError, ProbeFactory, WireProtocol,
+        DebugProbe, DebugProbeError, DebugProbeInfo, DebugProbeSelector, ProbeCreationError,
+        ProbeFactory, WireProtocol,
     },
 };
 
@@ -308,7 +309,7 @@ impl DebugProbe for WchLink {
 
         self.chip_family = resp.chip_family;
 
-        tracing::info!("attached riscvchip {:?}", self.chip_family);
+        tracing::info!("attached riscv chip {:?}", self.chip_family);
 
         self.chip_id = resp.chip_id;
 
@@ -366,14 +367,10 @@ impl DebugProbe for WchLink {
         true
     }
 
-    fn try_get_riscv_interface(
-        self: Box<Self>,
-    ) -> Result<RiscvCommunicationInterface, (Box<dyn DebugProbe>, RiscvError)> {
-        let jtag_dtm = match JtagDtm::new(self) {
-            Ok(jtag_dtm) => Box::new(jtag_dtm),
-            Err((access, err)) => return Err((access.into_probe(), err)),
-        };
-        RiscvCommunicationInterface::new(jtag_dtm).map_err(|(probe, err)| (probe.into_probe(), err))
+    fn try_get_riscv_interface_builder<'probe>(
+        &'probe mut self,
+    ) -> Result<Box<dyn RiscvInterfaceBuilder<'probe> + 'probe>, DebugProbeError> {
+        Ok(Box::new(JtagDtmBuilder::new(self)))
     }
 
     fn set_scan_chain(
@@ -382,12 +379,16 @@ impl DebugProbe for WchLink {
     ) -> Result<(), DebugProbeError> {
         Ok(())
     }
+
+    fn scan_chain(&self) -> Result<&[ScanChainElement], DebugProbeError> {
+        Ok(&[])
+    }
 }
 
 /// Wrap WCH-Link's USB based DMI access as a fake JTAGAccess
 impl JTAGAccess for WchLink {
-    fn scan_chain(&mut self) -> Result<Vec<JtagChainItem>, DebugProbeError> {
-        Ok(vec![])
+    fn scan_chain(&mut self) -> Result<(), DebugProbeError> {
+        Ok(())
     }
 
     fn tap_reset(&mut self) -> Result<(), DebugProbeError> {
diff --git a/probe-rs/src/session.rs b/probe-rs/src/session.rs
index fc7e0f60..37e44e41 100644
--- a/probe-rs/src/session.rs
+++ b/probe-rs/src/session.rs
@@ -2,9 +2,9 @@ use crate::architecture::arm::ap::AccessPort;
 use crate::architecture::arm::component::get_arm_components;
 use crate::architecture::arm::sequences::{ArmDebugSequence, DefaultArmSequence};
 use crate::architecture::arm::{ArmError, DpAddress};
-use crate::architecture::riscv::communication_interface::RiscvError;
+use crate::architecture::riscv::communication_interface::{RiscvDebugInterfaceState, RiscvError};
 use crate::architecture::xtensa::communication_interface::{
-    XtensaCommunicationInterface, XtensaError,
+    XtensaCommunicationInterface, XtensaDebugInterfaceState, XtensaError,
 };
 use crate::config::{ChipInfo, CoreExt, RegistryError, Target, TargetSelector};
 use crate::core::{Architecture, CombinedCoreState};
@@ -39,7 +39,7 @@ use std::{fmt, sync::Arc, time::Duration};
 ///
 /// # Usage
 /// The Session is the common handle that gives a user exclusive access to an active probe.
-/// You can create and share a session between threads to enable multiple stakeholders (e.g. GDB and RTT) to access the target taking turns, by using  `Arc<FairMutex<Session>>.`
+/// You can create and share a session between threads to enable multiple stakeholders (e.g. GDB and RTT) to access the target taking turns, by using `Arc<FairMutex<Session>>`.
 ///
 /// If you do so, make sure that both threads sleep in between tasks such that other stakeholders may take their turn.
 ///
@@ -49,38 +49,54 @@ use std::{fmt, sync::Arc, time::Duration};
 #[derive(Debug)]
 pub struct Session {
     target: Target,
-    interface: ArchitectureInterface,
+    interfaces: ArchitectureInterface,
     cores: Vec<CombinedCoreState>,
     configured_trace_sink: Option<TraceSink>,
 }
 
-pub(crate) enum ArchitectureInterface {
-    Arm(Box<dyn ArmProbeInterface + 'static>),
-    Riscv(Box<RiscvCommunicationInterface>),
-    Xtensa(Box<XtensaCommunicationInterface>),
+#[allow(clippy::large_enum_variant)]
+enum JtagInterface {
+    Riscv(RiscvDebugInterfaceState),
+    Xtensa(XtensaDebugInterfaceState),
+    Unknown,
 }
 
-impl fmt::Debug for ArchitectureInterface {
+impl JtagInterface {
+    /// Returns the debug module's intended architecture.
+    fn architecture(&self) -> Option<Architecture> {
+        match self {
+            JtagInterface::Riscv(_) => Some(Architecture::Riscv),
+            JtagInterface::Xtensa(_) => Some(Architecture::Xtensa),
+            JtagInterface::Unknown => None,
+        }
+    }
+}
+
+impl fmt::Debug for JtagInterface {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         match self {
-            ArchitectureInterface::Arm(_) => f.write_str("ArchitectureInterface::Arm(..)"),
-            ArchitectureInterface::Riscv(iface) => f
-                .debug_tuple("ArchitectureInterface::Riscv")
-                .field(iface)
-                .finish(),
-            ArchitectureInterface::Xtensa(_) => {
-                f.debug_tuple("ArchitectureInterface::Xtensa(..)").finish()
-            }
+            JtagInterface::Riscv(_) => f.write_str("Riscv(..)"),
+            JtagInterface::Xtensa(_) => f.write_str("Xtensa(..)"),
+            JtagInterface::Unknown => f.write_str("Unknown"),
         }
     }
 }
 
-impl From<&ArchitectureInterface> for Architecture {
-    fn from(value: &ArchitectureInterface) -> Self {
-        match value {
-            ArchitectureInterface::Arm(_) => Architecture::Arm,
-            ArchitectureInterface::Riscv(_) => Architecture::Riscv,
-            ArchitectureInterface::Xtensa(_) => Architecture::Xtensa,
+// TODO: this is somewhat messy because I omitted separating the Probe out of the ARM interface.
+#[allow(clippy::large_enum_variant)]
+enum ArchitectureInterface {
+    Arm(Box<dyn ArmProbeInterface + 'static>),
+    Jtag(Probe, Vec<JtagInterface>),
+}
+
+impl fmt::Debug for ArchitectureInterface {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            ArchitectureInterface::Arm(_) => f.write_str("ArchitectureInterface::Arm(..)"),
+            ArchitectureInterface::Jtag(_, ifaces) => f
+                .debug_tuple("ArchitectureInterface::Other(..)")
+                .field(ifaces)
+                .finish(),
         }
     }
 }
@@ -92,9 +108,26 @@ impl ArchitectureInterface {
         combined_state: &'probe mut CombinedCoreState,
     ) -> Result<Core<'probe>, Error> {
         match self {
-            ArchitectureInterface::Arm(iface) => combined_state.attach_arm(target, iface),
-            ArchitectureInterface::Riscv(iface) => combined_state.attach_riscv(target, iface),
-            ArchitectureInterface::Xtensa(iface) => combined_state.attach_xtensa(target, iface),
+            ArchitectureInterface::Arm(interface) => combined_state.attach_arm(target, interface),
+            ArchitectureInterface::Jtag(probe, ifaces) => {
+                let idx = combined_state.interface_idx();
+                match &mut ifaces[idx] {
+                    JtagInterface::Riscv(state) => {
+                        let factory = probe.try_get_riscv_interface_builder()?;
+                        let iface = factory.attach(state)?;
+                        combined_state.attach_riscv(target, iface)
+                    }
+                    JtagInterface::Xtensa(state) => {
+                        let iface = probe.try_get_xtensa_interface(state)?;
+                        combined_state.attach_xtensa(target, iface)
+                    }
+                    JtagInterface::Unknown => {
+                        unreachable!(
+                            "Tried to attach to unknown interface {idx}. This should never happen."
+                        )
+                    }
+                }
+            }
         }
     }
 }
@@ -123,16 +156,10 @@ impl Session {
             })
             .collect();
 
-        let mut session = match target.architecture() {
-            Architecture::Arm => {
-                Self::attach_arm(probe, target, attach_method, permissions, cores)?
-            }
-            Architecture::Riscv => {
-                Self::attach_riscv(probe, target, attach_method, permissions, cores)?
-            }
-            Architecture::Xtensa => {
-                Self::attach_xtensa(probe, target, attach_method, permissions, cores)?
-            }
+        let mut session = if let Architecture::Arm = target.architecture() {
+            Self::attach_arm(probe, target, attach_method, permissions, cores)?
+        } else {
+            Self::attach_jtag(probe, target, attach_method, permissions, cores)?
         };
 
         session.clear_all_hw_breakpoints()?;
@@ -236,7 +263,7 @@ impl Session {
 
             let mut session = Session {
                 target,
-                interface: ArchitectureInterface::Arm(interface),
+                interfaces: ArchitectureInterface::Arm(interface),
                 cores,
                 configured_trace_sink: None,
             };
@@ -259,27 +286,23 @@ impl Session {
         } else {
             Ok(Session {
                 target,
-                interface: ArchitectureInterface::Arm(interface),
+                interfaces: ArchitectureInterface::Arm(interface),
                 cores,
                 configured_trace_sink: None,
             })
         }
     }
 
-    fn attach_riscv(
+    fn attach_jtag(
         mut probe: Probe,
         target: Target,
         _attach_method: AttachMethod,
         _permissions: Permissions,
         cores: Vec<CombinedCoreState>,
     ) -> Result<Self, Error> {
-        // TODO: Handle attach under reset
-
-        let sequence_handle = match &target.debug_sequence {
-            DebugSequence::Riscv(sequence) => sequence.clone(),
-            _ => unreachable!("Mismatch between architecture and sequence type!"),
-        };
-
+        // While we still don't support mixed architectures
+        // (they'd need per-core debug sequences), we can at least
+        // handle most of the setup in the same way.
         if let Some(jtag) = target.jtag.as_ref() {
             if let Some(scan_chain) = jtag.scan_chain.clone() {
                 probe.set_scan_chain(scan_chain)?;
@@ -288,72 +311,99 @@ impl Session {
 
         probe.attach_to_unspecified()?;
 
-        let interface = probe
-            .try_into_riscv_interface()
-            .map_err(|(_probe, err)| err)?;
+        // We try to guess the TAP number. Normally we trust the scan chain, but some probes are
+        // only quasi-JTAG (wch-link), so we'll have to work with at least 1, but if we're guessing
+        // we can also use the highest number specified in the target YAML.
 
-        let mut session = Session {
-            target,
-            interface: ArchitectureInterface::Riscv(Box::new(interface)),
-            cores,
-            configured_trace_sink: None,
+        // FIXME: This is terribly JTAG-specific. Since we don't really support anything else yet,
+        // it should be fine for now.
+        let highest_idx = cores.iter().map(|c| c.interface_idx()).max().unwrap_or(0);
+        let tap_count = match probe.scan_chain() {
+            Ok(scan_chain) => scan_chain.len().max(highest_idx + 1),
+            Err(_) => highest_idx + 1,
         };
+        let mut interfaces = std::iter::repeat_with(|| JtagInterface::Unknown)
+            .take(tap_count)
+            .collect::<Vec<_>>();
 
-        {
-            // Wait for the cores to be halted.
-            for core_id in 0..session.cores.len() {
-                let mut core = session.core(core_id)?;
+        // Create a new interface by walking through the cores and initialising the TAPs that
+        // we find mentioned.
+        for core in cores.iter() {
+            let iface_idx = core.interface_idx();
+
+            let core_arch = core.core_type().architecture();
 
-                core.halt(Duration::from_millis(100))?;
+            if let Some(debug_arch) = interfaces[iface_idx].architecture() {
+                if core_arch == debug_arch {
+                    // Already initialised.
+                    continue;
+                }
+                return Err(Error::Probe(DebugProbeError::Other(anyhow::anyhow!(
+                    "{core_arch:?} core can not be mixed with a {debug_arch:?} debug module.",
+                ))));
             }
-        }
 
-        session.halted_access(|sess| sequence_handle.on_connect(sess.get_riscv_interface()?))?;
+            probe.select_jtag_tap(iface_idx)?;
 
-        Ok(session)
-    }
+            interfaces[iface_idx] = match core_arch {
+                Architecture::Riscv => {
+                    let factory = probe.try_get_riscv_interface_builder()?;
+                    let mut state = factory.create_state();
+                    {
+                        let mut interface = factory.attach(&mut state)?;
+                        interface.enter_debug_mode()?;
+                    }
 
-    fn attach_xtensa(
-        mut probe: Probe,
-        target: Target,
-        _attach_method: AttachMethod,
-        _permissions: Permissions,
-        cores: Vec<CombinedCoreState>,
-    ) -> Result<Self, Error> {
-        let sequence_handle = match &target.debug_sequence {
-            DebugSequence::Xtensa(sequence) => sequence.clone(),
-            _ => unreachable!("Mismatch between architecture and sequence type!"),
-        };
+                    JtagInterface::Riscv(state)
+                }
+                Architecture::Xtensa => {
+                    let mut state = XtensaDebugInterfaceState::default();
+                    {
+                        let mut interface = probe.try_get_xtensa_interface(&mut state)?;
+                        interface.enter_debug_mode()?;
+                    }
 
-        if let Some(jtag) = target.jtag.as_ref() {
-            if let Some(scan_chain) = jtag.scan_chain.clone() {
-                probe.set_scan_chain(scan_chain)?;
-            }
+                    JtagInterface::Xtensa(state)
+                }
+                _ => {
+                    return Err(Error::Probe(DebugProbeError::Other(anyhow::anyhow!(
+                        "Unsupported core architecture {core_arch:?}",
+                    ))));
+                }
+            };
         }
 
-        probe.attach_to_unspecified()?;
-
-        let interface = probe
-            .try_into_xtensa_interface()
-            .map_err(|(_probe, err)| err)?;
+        let interfaces = ArchitectureInterface::Jtag(probe, interfaces);
 
         let mut session = Session {
             target,
-            interface: ArchitectureInterface::Xtensa(Box::new(interface)),
+            interfaces,
             cores,
             configured_trace_sink: None,
         };
 
-        {
-            // Wait for the cores to be halted.
-            for core_id in 0..session.cores.len() {
-                let mut core = session.core(core_id)?;
+        // Wait for the cores to be halted.
+        for core_id in 0..session.cores.len() {
+            let mut core = session.core(core_id)?;
 
-                core.halt(Duration::from_millis(100))?;
-            }
+            core.halt(Duration::from_millis(100))?;
         }
 
-        session.halted_access(|sess| sequence_handle.on_connect(sess.get_xtensa_interface()?))?;
+        // Connect to the cores
+        match session.target.debug_sequence.clone() {
+            DebugSequence::Xtensa(sequence) => {
+                for core_id in 0..session.cores.len() {
+                    sequence.on_connect(&mut session.get_xtensa_interface(core_id)?)?;
+                }
+            }
+
+            DebugSequence::Riscv(sequence) => {
+                for core_id in 0..session.cores.len() {
+                    sequence.on_connect(&mut session.get_riscv_interface(core_id)?)?;
+                }
+            }
+            _ => unreachable!("Other architectures should have already been handled"),
+        };
 
         Ok(session)
     }
@@ -414,6 +464,13 @@ impl Session {
         r
     }
 
+    fn interface_idx(&self, core: usize) -> Result<usize, Error> {
+        self.cores
+            .get(core)
+            .map(|c| c.interface_idx())
+            .ok_or(Error::CoreNotFound(core))
+    }
+
     /// Attaches to the core with the given number.
     ///
     /// ## Usage
@@ -435,7 +492,8 @@ impl Session {
             .cores
             .get_mut(core_index)
             .ok_or(Error::CoreNotFound(core_index))?;
-        self.interface.attach(&self.target, combined_state)
+
+        self.interfaces.attach(&self.target, combined_state)
     }
 
     /// Read available trace data from the specified data sink.
@@ -482,7 +540,7 @@ impl Session {
 
     /// Get the Arm probe interface.
     pub fn get_arm_interface(&mut self) -> Result<&mut dyn ArmProbeInterface, ArmError> {
-        let interface = match &mut self.interface {
+        let interface = match &mut self.interfaces {
             ArchitectureInterface::Arm(state) => state.deref_mut(),
             _ => return Err(ArmError::NoArmTarget),
         };
@@ -491,25 +549,32 @@ impl Session {
     }
 
     /// Get the RISC-V probe interface.
-    pub fn get_riscv_interface(&mut self) -> Result<&mut RiscvCommunicationInterface, RiscvError> {
-        let interface = match &mut self.interface {
-            ArchitectureInterface::Riscv(interface) => interface,
-            _ => return Err(RiscvError::NoRiscvTarget),
-        };
-
-        Ok(interface)
+    pub fn get_riscv_interface(
+        &mut self,
+        core_id: usize,
+    ) -> Result<RiscvCommunicationInterface, Error> {
+        let tap_idx = self.interface_idx(core_id)?;
+        if let ArchitectureInterface::Jtag(probe, ifaces) = &mut self.interfaces {
+            if let JtagInterface::Riscv(state) = &mut ifaces[tap_idx] {
+                let factory = probe.try_get_riscv_interface_builder()?;
+                return Ok(factory.attach(state)?);
+            }
+        }
+        Err(RiscvError::NoRiscvTarget.into())
     }
 
     /// Get the Xtensa probe interface.
     pub fn get_xtensa_interface(
         &mut self,
-    ) -> Result<&mut XtensaCommunicationInterface, XtensaError> {
-        let interface = match &mut self.interface {
-            ArchitectureInterface::Xtensa(interface) => interface,
-            _ => return Err(XtensaError::NoXtensaTarget),
-        };
-
-        Ok(interface)
+        core_id: usize,
+    ) -> Result<XtensaCommunicationInterface, Error> {
+        let tap_idx = self.interface_idx(core_id)?;
+        if let ArchitectureInterface::Jtag(probe, ifaces) = &mut self.interfaces {
+            if let JtagInterface::Xtensa(state) = &mut ifaces[tap_idx] {
+                return Ok(probe.try_get_xtensa_interface(state)?);
+            }
+        }
+        Err(XtensaError::NoXtensaTarget.into())
     }
 
     #[tracing::instrument(skip_all)]
@@ -567,7 +632,7 @@ impl Session {
     /// NotImplemented if no custom erase sequence exists
     /// Err(e) if the custom erase sequence failed
     pub fn sequence_erase_all(&mut self) -> Result<(), Error> {
-        let ArchitectureInterface::Arm(ref mut interface) = self.interface else {
+        let ArchitectureInterface::Arm(ref mut interface) = self.interfaces else {
             return Err(Error::Probe(DebugProbeError::NotImplemented {
                 function_name: "Debug Erase Sequence",
             }));
@@ -687,7 +752,16 @@ impl Session {
 
     /// Return the `Architecture` of the currently connected chip.
     pub fn architecture(&self) -> Architecture {
-        Architecture::from(&self.interface)
+        match &self.interfaces {
+            ArchitectureInterface::Arm(_) => Architecture::Arm,
+            ArchitectureInterface::Jtag(_, ifaces) => {
+                if let JtagInterface::Riscv(_) = &ifaces[0] {
+                    Architecture::Riscv
+                } else {
+                    Architecture::Xtensa
+                }
+            }
+        }
     }
 
     /// Clears all hardware breakpoints on all cores
@@ -789,17 +863,16 @@ fn get_target_from_selector(
             }
 
             if found_chip.is_none() && probe.has_riscv_interface() {
-                match probe.try_into_riscv_interface() {
-                    Ok(mut interface) => {
+                match probe.try_get_riscv_interface_builder() {
+                    Ok(factory) => {
+                        let mut state = factory.create_state();
+                        let mut interface = factory.attach(&mut state)?;
                         let idcode = interface.read_idcode();
 
                         tracing::debug!("ID Code read over JTAG: {:x?}", idcode);
-
-                        probe = interface.close();
                     }
-                    Err((returned_probe, err)) => {
+                    Err(err) => {
                         tracing::debug!("Error during autodetection of RISC-V chips: {}", err);
-                        probe = returned_probe;
                     }
                 }
             } else {
diff --git a/probe-rs/targets/EFM32PG22_Series.yaml b/probe-rs/targets/EFM32PG22_Series.yaml
new file mode 100644
index 00000000..4ad398c0
--- /dev/null
+++ b/probe-rs/targets/EFM32PG22_Series.yaml
@@ -0,0 +1,227 @@
+name: EFM32PG22 Series
+generated_from_pack: true
+pack_file_release: 4.4.0
+variants:
+- name: EFM32PG22C200F128IM32
+  cores:
+  - name: main
+    type: armv8m
+    core_access_options: !Arm
+      ap: 0
+      psel: 0
+  memory_map:
+  - !Ram
+    name: IRAM1
+    range:
+      start: 0x20000000
+      end: 0x20008000
+    cores:
+    - main
+  - !Nvm
+    name: IROM1
+    range:
+      start: 0x0
+      end: 0x20000
+    is_boot_memory: true
+    cores:
+    - main
+  flash_algorithms:
+  - geckos2
+- name: EFM32PG22C200F128IM40
+  cores:
+  - name: main
+    type: armv8m
+    core_access_options: !Arm
+      ap: 0
+      psel: 0
+  memory_map:
+  - !Ram
+    name: IRAM1
+    range:
+      start: 0x20000000
+      end: 0x20008000
+    cores:
+    - main
+  - !Nvm
+    name: IROM1
+    range:
+      start: 0x0
+      end: 0x20000
+    is_boot_memory: true
+    cores:
+    - main
+  flash_algorithms:
+  - geckos2
+- name: EFM32PG22C200F256IM32
+  cores:
+  - name: main
+    type: armv8m
+    core_access_options: !Arm
+      ap: 0
+      psel: 0
+  memory_map:
+  - !Ram
+    name: IRAM1
+    range:
+      start: 0x20000000
+      end: 0x20008000
+    cores:
+    - main
+  - !Nvm
+    name: IROM1
+    range:
+      start: 0x0
+      end: 0x40000
+    is_boot_memory: true
+    cores:
+    - main
+  flash_algorithms:
+  - geckos2
+- name: EFM32PG22C200F256IM40
+  cores:
+  - name: main
+    type: armv8m
+    core_access_options: !Arm
+      ap: 0
+      psel: 0
+  memory_map:
+  - !Ram
+    name: IRAM1
+    range:
+      start: 0x20000000
+      end: 0x20008000
+    cores:
+    - main
+  - !Nvm
+    name: IROM1
+    range:
+      start: 0x0
+      end: 0x40000
+    is_boot_memory: true
+    cores:
+    - main
+  flash_algorithms:
+  - geckos2
+- name: EFM32PG22C200F512IM32
+  cores:
+  - name: main
+    type: armv8m
+    core_access_options: !Arm
+      ap: 0
+      psel: 0
+  memory_map:
+  - !Ram
+    name: IRAM1
+    range:
+      start: 0x20000000
+      end: 0x20008000
+    cores:
+    - main
+  - !Nvm
+    name: IROM1
+    range:
+      start: 0x0
+      end: 0x80000
+    is_boot_memory: true
+    cores:
+    - main
+  flash_algorithms:
+  - geckos2
+- name: EFM32PG22C200F512IM40
+  cores:
+  - name: main
+    type: armv8m
+    core_access_options: !Arm
+      ap: 0
+      psel: 0
+  memory_map:
+  - !Ram
+    name: IRAM1
+    range:
+      start: 0x20000000
+      end: 0x20008000
+    cores:
+    - main
+  - !Nvm
+    name: IROM1
+    range:
+      start: 0x0
+      end: 0x80000
+    is_boot_memory: true
+    cores:
+    - main
+  flash_algorithms:
+  - geckos2
+- name: EFM32PG22C200F64IM32
+  cores:
+  - name: main
+    type: armv8m
+    core_access_options: !Arm
+      ap: 0
+      psel: 0
+  memory_map:
+  - !Ram
+    name: IRAM1
+    range:
+      start: 0x20000000
+      end: 0x20008000
+    cores:
+    - main
+  - !Nvm
+    name: IROM1
+    range:
+      start: 0x0
+      end: 0x10000
+    is_boot_memory: true
+    cores:
+    - main
+  flash_algorithms:
+  - geckos2
+- name: EFM32PG22C200F64IM40
+  cores:
+  - name: main
+    type: armv8m
+    core_access_options: !Arm
+      ap: 0
+      psel: 0
+  memory_map:
+  - !Ram
+    name: IRAM1
+    range:
+      start: 0x20000000
+      end: 0x20008000
+    cores:
+    - main
+  - !Nvm
+    name: IROM1
+    range:
+      start: 0x0
+      end: 0x10000
+    is_boot_memory: true
+    cores:
+    - main
+  flash_algorithms:
+  - geckos2
+flash_algorithms:
+- name: geckos2
+  description: EFM32/EFR32 Gecko S2
+  default: true
+  instructions: DUgBaAH0fBGx9bAfBNELSQpoQvQAMgpgCUlB9nEyCmAAIkpggGgHSQHqgCEGSgAgSfgCEHBHAL8EgOAPaIAAQDwAA0AA/P8DBAAAAAVJQfZxMAhgBEgBIgJgACIAIApgcEcAvzwAA0AMIANAsLUJTQAkWfgFAIRCJL8AILC9IEYA8Ar4ACgE9QBUHL8BILC97+cAvwQAAAAPSQJGCMoBMwPRACkB8QQB+NGTsbC1C0wBJQtJJWBIYAIgCGABIAAhAPBe+E/0gFEAKGVQGL8BILC9ACBwRwC/BOD//wwQA0AQAANALenwTQVGyBySRiJKIk8g8AMEASAQYC7gBfUAUIhDoOsFC2keXEWh6wAAb+oEATi/o0bQRohCfWCIvwFGTh3Y+AAAAC64YArQCCAIIQjxBAgA8CT4ACgG8QQG8NAW4AQgOGABIAAhASYA8Bj4eLnaRF1EpOsLBEH2/3EALMzRT/SAUAEhA0oAJhFQAOABJjBGvejwjQwQA0AQAANAcLUQTg1KDkwzHQngBUCNQgS/ACBwvQE0BL9v8AIAcL0VaR1C8tAQaDVCIPABAW/wAQAIv0/w/zARYHC9DAADQIBpZ/8CAAEAAAAAAAAAAAA=
+  load_address: 0x20000020
+  pc_init: 0x1
+  pc_uninit: 0x4d
+  pc_program_page: 0xe5
+  pc_erase_sector: 0x99
+  pc_erase_all: 0x6d
+  data_section_offset: 0x1c8
+  flash_properties:
+    address_range:
+      start: 0x0
+      end: 0x100000
+    page_size: 0x3000
+    erased_byte_value: 0xff
+    program_page_timeout: 260
+    erase_sector_timeout: 200
+    sectors:
+    - size: 0x2000
+      address: 0x0
diff --git a/probe-rs/targets/esp32.yaml b/probe-rs/targets/esp32.yaml
index fa85a35c..9bacb3b4 100644
--- a/probe-rs/targets/esp32.yaml
+++ b/probe-rs/targets/esp32.yaml
@@ -13,7 +13,9 @@ variants:
     cores:
       - name: main
         type: xtensa
-        core_access_options: !Xtensa {}
+        core_access_options:
+          !Xtensa
+            jtag_tap: 0
     memory_map:
       - !Nvm
         range:
diff --git a/probe-rs/targets/esp32s3.yaml b/probe-rs/targets/esp32s3.yaml
index 9fc90325..e5a1884c 100644
--- a/probe-rs/targets/esp32s3.yaml
+++ b/probe-rs/targets/esp32s3.yaml
@@ -14,7 +14,9 @@ variants:
     cores:
       - name: cpu0
         type: xtensa
-        core_access_options: !Xtensa {}
+        core_access_options:
+          !Xtensa
+            jtag_tap: 0
     memory_map:
       - !Nvm
         range:
diff --git a/target-gen/src/generate.rs b/target-gen/src/generate.rs
index 63b19d6d..e7f7ad38 100644
--- a/target-gen/src/generate.rs
+++ b/target-gen/src/generate.rs
@@ -179,10 +179,13 @@ fn create_core(processor: &Processor) -> Result<ProbeCore> {
                 debug_base: None,
                 cti_base: None,
             }),
-            Architecture::Riscv => {
-                CoreAccessOptions::Riscv(RiscvCoreAccessOptions { hart_id: None })
+            Architecture::Riscv => CoreAccessOptions::Riscv(RiscvCoreAccessOptions {
+                hart_id: None,
+                jtag_tap: None,
+            }),
+            Architecture::Xtensa => {
+                CoreAccessOptions::Xtensa(XtensaCoreAccessOptions { jtag_tap: None })
             }
-            Architecture::Xtensa => CoreAccessOptions::Xtensa(XtensaCoreAccessOptions {}),
         },
     })
 }


Test patch:
diff --git a/smoke-tester/src/dut_definition.rs b/smoke-tester/src/dut_definition.rs
index 50758ec1..e51c2b9a 100644
--- a/smoke-tester/src/dut_definition.rs
+++ b/smoke-tester/src/dut_definition.rs
@@ -1,4 +1,4 @@
-//! # DUT Defintions
+//! # DUT Definitions
 //!
 //! This module handles the definition of the different devices under test (DUTs),
 //! which are used by the tester.
diff --git a/smoke-tester/src/main.rs b/smoke-tester/src/main.rs
index 048d1243..0a50130e 100644
--- a/smoke-tester/src/main.rs
+++ b/smoke-tester/src/main.rs
@@ -59,6 +59,13 @@ impl SingleTestReport {
     fn has_fatal_error(&self) -> bool {
         matches!(self.result, Err(TestFailure::Fatal(_)))
     }
+
+    fn failed(&self) -> bool {
+        matches!(
+            self.result,
+            Err(TestFailure::Error(_) | TestFailure::Fatal(_))
+        )
+    }
 }
 
 #[derive(Debug, Subcommand)]
@@ -126,6 +133,8 @@ fn run_test(definitions: &[DutDefinition], markdown_summary: Option<PathBuf>) ->
         // We don't care about existing flash contents
         let permissions = Permissions::default().allow_erase_all();
 
+        let mut fail_counter = 0;
+
         let mut session = probe
             .attach(definition.chip.clone(), permissions)
             .into_diagnostic()
@@ -150,6 +159,10 @@ fn run_test(definitions: &[DutDefinition], markdown_summary: Option<PathBuf>) ->
                 if result.has_fatal_error() {
                     return Err(miette::miette!("Test failed with fatal error"));
                 }
+
+                if result.failed() {
+                    fail_counter += 1;
+                }
             }
 
             // Ensure core is not running anymore.
@@ -166,6 +179,10 @@ fn run_test(definitions: &[DutDefinition], markdown_summary: Option<PathBuf>) ->
             if result.has_fatal_error() {
                 return Err(miette::miette!("Test failed with fatal error"));
             }
+
+            if result.failed() {
+                fail_counter += 1;
+            }
         }
 
         drop(session);
@@ -180,7 +197,11 @@ fn run_test(definitions: &[DutDefinition], markdown_summary: Option<PathBuf>) ->
                 .into_diagnostic()?;
         }
 
-        Ok(())
+        match fail_counter {
+            0 => Ok(()),
+            1 => Err(miette::miette!("1 test failed")),
+            count => Err(miette::miette!("{count} tests failed")),
+        }
     });
 
     println!();
@@ -352,7 +373,7 @@ impl<'dut> TestTracker<'dut> {
         let mut tests_ok = true;
 
         for definition in self.dut_definitions {
-            print_dut_status!(self, blue, "Starting Test",);
+            print_dut_status!(self, blue, "Starting Test");
 
             if let DefinitionSource::File(path) = &definition.source {
                 print!(" - {}", path.display());
@@ -368,7 +389,7 @@ impl<'dut> TestTracker<'dut> {
                         name: definition.chip.name.clone(),
                         succesful: true,
                     });
-                    println_dut_status!(self, green, "Tests Passed",);
+                    println_dut_status!(self, green, "Tests Passed");
                 }
                 Ok(Err(e)) => {
                     tests_ok = false;
@@ -383,7 +404,7 @@ impl<'dut> TestTracker<'dut> {
                         println_dut_status!(self, red, " caused by:    {}", source);
                     }
 
-                    println_dut_status!(self, red, "Tests Failed",);
+                    println_dut_status!(self, red, "Tests Failed");
                 }
                 Err(_join_err) => {
                     tests_ok = false;
@@ -400,9 +421,9 @@ impl<'dut> TestTracker<'dut> {
         }
 
         if tests_ok {
-            println_status!(self, green, "All DUTs passed.",);
+            println_status!(self, green, "All DUTs passed.");
         } else {
-            println_status!(self, red, "Some DUTs failed some tests.",);
+            println_status!(self, red, "Some DUTs failed some tests.");
         }
 
         report
diff --git a/smoke-tester/src/tests.rs b/smoke-tester/src/tests.rs
index 44ebcc10..3d17ff44 100644
--- a/smoke-tester/src/tests.rs
+++ b/smoke-tester/src/tests.rs
@@ -234,11 +234,15 @@ pub fn test_flashing(tracker: &TestTracker, session: &mut Session) -> Result<(),
         probe_rs_target::BinaryFormat::Idf => Format::Idf(Default::default()),
         probe_rs_target::BinaryFormat::Raw => Default::default(),
     };
-    download_file_with_options(session, test_binary, format, options)
-        .map_err(|err| TestFailure::Error(Box::new(err)))?;
+
+    let result = download_file_with_options(session, test_binary, format, options);
 
     println!();
 
+    if let Err(err) = result {
+        return Err(TestFailure::Error(Box::new(err)));
+    }
+
     println_test_status!(
         tracker,
         blue,