Repository: probe-rs/probe-rs
Pull request #2377 (2024-04-16 19:58:18)

Remove default rtt options, remove channel name configuration
This PR removes the global default and instead requires the user to configure each RTT channel. This is meant to simplify the implementation and remove an implicit precedence relation between options.

Flipping the defaults means they are now slightly inconsistent with `probe-rs run`. Because `probe-rs run` uses a `--no-location` flag to disable locations, it's slightly more distruptive to change its default behaviour and I didn't want to make this change here - though likely a `--with-location` flag would be the result.

Channel name configuration has been removed and probe-rs now exclusively uses the name read from the target. Nothing used this option - `cargo embed`'s tab titles introduced in the current development cycle are unaffected.

bugadani: @noppej I don't know if this is the right way. I'd probably add location/timestamp defaults to `RttConfig` and make it available for the debugger, too, but I don't want to step on your toes without asking.
noppej: > @noppej I don't know if this is the right way. I'd probably add location/timestamp defaults to `RttConfig` and make it available for the debugger, too, but I don't want to step on your toes without asking.

@bugadani I'm not sure what you are referring to by the "this" in "if this is the right way"?  Can you elaborate please?

bugadani: `this` referred to passing the global config into the attach functions. This question is no longer relevant, however, as I instead propose removing the global configurations altogether. While this places a slight burden on the users (and is a breaking change), configuration should rarely change and we lose none of the flexibility.

There are differing opinions whether "global" default are useful or not. I believe the common use case doesn't requre too many RTT channels and so optimizing for them is likely just unnecessary complexity.

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 92c14e4f..80d78a66 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -286,6 +286,15 @@ dependencies = [
  "rustc-demangle",
 ]
 
+[[package]]
+name = "backtrace-ext"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50"
+dependencies = [
+ "backtrace",
+]
+
 [[package]]
 name = "base64"
 version = "0.21.7"
@@ -538,9 +547,9 @@ dependencies = [
 
 [[package]]
 name = "cargo-config2"
-version = "0.1.24"
+version = "0.1.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88d9bdc858a15454c2d0a5138d8dcf4bcabc06fde679abdea8330393fbc0ef05"
+checksum = "d83ce0be8bd1479e5de6202def660e6c7e27e4e0599bffa4fed05bd380ec2ede"
 dependencies = [
  "home",
  "serde",
@@ -1875,7 +1884,7 @@ dependencies = [
  "futures-util",
  "http 0.2.12",
  "hyper 0.14.28",
- "rustls 0.21.10",
+ "rustls 0.21.11",
  "tokio",
  "tokio-rustls 0.24.1",
 ]
@@ -2082,6 +2091,12 @@ dependencies = [
  "windows-sys 0.52.0",
 ]
 
+[[package]]
+name = "is_ci"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
+
 [[package]]
 name = "itertools"
 version = "0.4.19"
@@ -2330,8 +2345,16 @@ version = "7.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4edc8853320c2a0dab800fbda86253c8938f6ea88510dc92c5f1ed20e794afc1"
 dependencies = [
+ "backtrace",
+ "backtrace-ext",
  "cfg-if",
  "miette-derive",
+ "owo-colors",
+ "supports-color",
+ "supports-hyperlinks",
+ "supports-unicode",
+ "terminal_size",
+ "textwrap",
  "thiserror",
  "unicode-width",
 ]
@@ -2489,9 +2512,9 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
 
 [[package]]
 name = "nusb"
-version = "0.1.7"
+version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ac267a828c32759883a6097809fc4c32d76925099725aae562717cb023b431c"
+checksum = "dccb3bb9c89ba9393f2f723da29221a39a3310c3950fbe5896eec05daa82753e"
 dependencies = [
  "atomic-waker",
  "core-foundation",
@@ -2548,6 +2571,12 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
 
+[[package]]
+name = "owo-colors"
+version = "4.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caff54706df99d2a78a5a4e3455ff45448d81ef1bb63c22cd14052ca0e993a3f"
+
 [[package]]
 name = "parking"
 version = "2.2.0"
@@ -3131,7 +3160,7 @@ dependencies = [
  "once_cell",
  "percent-encoding",
  "pin-project-lite",
- "rustls 0.21.10",
+ "rustls 0.21.11",
  "rustls-native-certs",
  "rustls-pemfile 1.0.4",
  "serde",
@@ -3153,9 +3182,9 @@ dependencies = [
 
 [[package]]
 name = "reqwest"
-version = "0.12.3"
+version = "0.12.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e6cc1e89e689536eb5aeede61520e874df5a4707df811cd5da4aa5fbb2aae19"
+checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10"
 dependencies = [
  "base64 0.22.0",
  "bytes",
@@ -3321,9 +3350,9 @@ dependencies = [
 
 [[package]]
 name = "rustls"
-version = "0.21.10"
+version = "0.21.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba"
+checksum = "7fecbfb7b1444f477b345853b1fce097a2c6fb637b2bfb87e6bc5db0f043fae4"
 dependencies = [
  "log",
  "ring",
@@ -3615,9 +3644,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.115"
+version = "1.0.116"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd"
+checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
 dependencies = [
  "itoa",
  "ryu",
@@ -3776,14 +3805,16 @@ checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
 name = "smoke_tester"
 version = "0.23.0"
 dependencies = [
- "anyhow",
  "clap",
  "colored",
  "linkme",
  "log",
+ "miette",
  "pretty_env_logger",
  "probe-rs",
+ "probe-rs-target",
  "serde",
+ "thiserror",
  "toml",
 ]
 
@@ -3865,6 +3896,27 @@ version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
 
+[[package]]
+name = "supports-color"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9829b314621dfc575df4e409e79f9d6a66a3bd707ab73f23cb4aa3a854ac854f"
+dependencies = [
+ "is_ci",
+]
+
+[[package]]
+name = "supports-hyperlinks"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c0a1e5168041f5f3ff68ff7d95dcb9c8749df29f6e7e89ada40dd4c9de404ee"
+
+[[package]]
+name = "supports-unicode"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2"
+
 [[package]]
 name = "svd-parser"
 version = "0.14.5"
@@ -3890,9 +3942,9 @@ dependencies = [
 
 [[package]]
 name = "svg"
-version = "0.16.0"
+version = "0.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "583e1c5c326fd6fede8797006de3b95ad6bcd60a592952952c5ba7ddd7e84c83"
+checksum = "700efb40f3f559c23c18b446e8ed62b08b56b2bb3197b36d57e0470b4102779e"
 
 [[package]]
 name = "syn"
@@ -3978,7 +4030,7 @@ dependencies = [
  "predicates",
  "probe-rs",
  "probe-rs-target",
- "reqwest 0.12.3",
+ "reqwest 0.12.4",
  "scroll",
  "serde_yaml",
  "tokio",
@@ -4008,6 +4060,16 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "terminal_size"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7"
+dependencies = [
+ "rustix",
+ "windows-sys 0.48.0",
+]
+
 [[package]]
 name = "termtree"
 version = "0.4.1"
@@ -4060,18 +4122,18 @@ dependencies = [
 
 [[package]]
 name = "thiserror"
-version = "1.0.58"
+version = "1.0.59"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297"
+checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.58"
+version = "1.0.59"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
+checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -4179,7 +4241,7 @@ version = "0.24.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
 dependencies = [
- "rustls 0.21.10",
+ "rustls 0.21.11",
  "tokio",
 ]
 
diff --git a/Cargo.toml b/Cargo.toml
index 2c0f6e0c..67cf1052 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -26,7 +26,7 @@ probe-rs = { path = "probe-rs", version = "0.23.0" }
 probe-rs-target = { path = "probe-rs-target", version = "0.23.0" }
 
 docsplay = "0.1.1"
-thiserror = "1.0.58"
+thiserror = "1.0.59"
 anyhow = "1.0.82"
 
 [workspace.metadata.release]
diff --git a/changelog/added-list-break-to-debug.md b/changelog/added-list-break-to-debug.md
new file mode 100644
index 00000000..375292ba
--- /dev/null
+++ b/changelog/added-list-break-to-debug.md
@@ -0,0 +1,1 @@
+Add the `list_break` command to `probe-rs debug` to list the set breakpoints.
diff --git a/changelog/changed-rtt-defaults.md b/changelog/changed-rtt-defaults.md
new file mode 100644
index 00000000..0ae9b225
--- /dev/null
+++ b/changelog/changed-rtt-defaults.md
@@ -0,0 +1,4 @@
+RTT: defmt location information is no longer displayed by default.
+RTT: timestamps are now displayed by default, if available.
+RTT: `show_timestamps` and `show_location` can now only be set in the up channel configuration.
+RTT: channel names are no longer configurable.
\ No newline at end of file
diff --git a/changelog/fixed-esp-jtag-select.md b/changelog/fixed-esp-jtag-select.md
new file mode 100644
index 00000000..f30538bb
--- /dev/null
+++ b/changelog/fixed-esp-jtag-select.md
@@ -0,0 +1,1 @@
+Fixed selecting one of multiple ESP USB JTAG probes
\ No newline at end of file
diff --git a/changelog/fixed-esp32-riscv-reset.md b/changelog/fixed-esp32-riscv-reset.md
new file mode 100644
index 00000000..89289176
--- /dev/null
+++ b/changelog/fixed-esp32-riscv-reset.md
@@ -0,0 +1,1 @@
+Probe-rs should now succeed more at flashing RISC-V ESP32 devices
\ No newline at end of file
diff --git a/probe-rs/Cargo.toml b/probe-rs/Cargo.toml
index 1031efdd..d2ad1bdb 100644
--- a/probe-rs/Cargo.toml
+++ b/probe-rs/Cargo.toml
@@ -105,14 +105,14 @@ object = { version = "0.35", default-features = false, features = [
     "std",
 ] }
 paste = "1"
-nusb = { version = "0.1.7" }
+nusb = { version = "0.1.8" }
 futures-lite = "2"
 async-io = "2"
 scroll = "0.12"
 serde = { version = "1", features = ["derive"] }
 serde_yaml = "0.9"
 static_assertions = "1"
-svg = "0.16"
+svg = "0.17"
 thiserror = { workspace = true }
 tracing = { version = "0.1", features = ["log"] }
 uf2-decode = "0.2"
@@ -160,7 +160,7 @@ rand = { version = "0.8", optional = true }
 rustyline = { version = "14", optional = true }
 sanitize-filename = { version = "0.5", optional = true }
 schemafy = { version = "0.6", optional = true }
-serde_json = { version = "1.0.115", optional = true }
+serde_json = { version = "1.0.116", optional = true }
 signal-hook = { version = "0.3", optional = true }
 svd-parser = { version = "0.14", features = ["expand"], optional = true }
 termtree = { version = "0.4", optional = true }
@@ -182,7 +182,7 @@ ratatui = { version = "0.26.2", default-features = false, features = [
 ], optional = true }
 ansi-parser = { version = "0.9.0", optional = true }
 parking_lot = "0.12.1"
-cargo-config2 = { version = "0.1.24", optional = true }
+cargo-config2 = { version = "0.1.26", optional = true }
 
 [build-dependencies]
 bincode = "1"
diff --git a/probe-rs/src/architecture/riscv/communication_interface.rs b/probe-rs/src/architecture/riscv/communication_interface.rs
index f0e6431d..af017988 100644
--- a/probe-rs/src/architecture/riscv/communication_interface.rs
+++ b/probe-rs/src/architecture/riscv/communication_interface.rs
@@ -314,6 +314,9 @@ pub struct RiscvCommunicationInterface {
     state: RiscvCommunicationInterfaceState,
     enabled_harts: u32,
     last_selected_hart: u32,
+
+    /// Store the value of the `hasresethaltreq` bit of the `dmcstatus` register.
+    hasresethaltreq: Option<bool>,
 }
 
 impl RiscvCommunicationInterface {
@@ -326,6 +329,7 @@ impl RiscvCommunicationInterface {
             state,
             enabled_harts: 0,
             last_selected_hart: 0,
+            hasresethaltreq: None,
         };
 
         if let Err(err) = s.enter_debug_mode() {
@@ -421,8 +425,9 @@ impl RiscvCommunicationInterface {
         Ok(())
     }
 
-    fn enter_debug_mode(&mut self) -> Result<(), RiscvError> {
+    pub(crate) fn enter_debug_mode(&mut self) -> Result<(), RiscvError> {
         tracing::debug!("Building RISC-V interface");
+        self.dtm.init()?;
 
         // Reset error bits from previous connections
         self.dtm.clear_error_state()?;
@@ -430,6 +435,7 @@ impl RiscvCommunicationInterface {
         // read the  version of the debug module
         let status: Dmstatus = self.read_dm_register()?;
 
+        self.state.progbuf_cache.fill(0);
         self.state.debug_version = DebugModuleVersion::from(status.version() as u8);
 
         // Only version of 0.13 of the debug specification is currently supported.
@@ -680,7 +686,7 @@ impl RiscvCommunicationInterface {
     }
 
     /// Schedules a DM register read, flushes the queue and returns the result.
-    pub(super) fn read_dm_register<R: MemoryMappedRegister<u32>>(
+    pub(crate) fn read_dm_register<R: MemoryMappedRegister<u32>>(
         &mut self,
     ) -> Result<R, RiscvError> {
         tracing::debug!(
@@ -711,7 +717,7 @@ impl RiscvCommunicationInterface {
         Ok(register_value)
     }
 
-    pub(super) fn write_dm_register<R: MemoryMappedRegister<u32>>(
+    pub(crate) fn write_dm_register<R: MemoryMappedRegister<u32>>(
         &mut self,
         register: R,
     ) -> Result<(), RiscvError> {
@@ -1080,7 +1086,8 @@ impl RiscvCommunicationInterface {
             let error = AbstractCommandErrorKind::parse(status.cmderr() as u8);
 
             tracing::error!(
-                "Executing the abstract command for perform_memory_write failed: {:?} ({:x?})",
+                "Executing the abstract command for write_{} failed: {:?} ({:x?})",
+                V::WIDTH.byte_width() * 8,
                 error,
                 status,
             );
@@ -1142,7 +1149,8 @@ impl RiscvCommunicationInterface {
             let error = AbstractCommandErrorKind::parse(status.cmderr() as u8);
 
             tracing::error!(
-                "Executing the abstract command for write_32 failed: {:?} ({:x?})",
+                "Executing the abstract command for write_multiple_{} failed: {:?} ({:x?})",
+                V::WIDTH.byte_width() * 8,
                 error,
                 status,
             );
@@ -1526,6 +1534,127 @@ impl RiscvCommunicationInterface {
     {
         V::schedule_write_to_register::<R>(self, value)
     }
+
+    /// Check if the connected device supports halt after reset.
+    ///
+    /// 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 {
+            Ok(has_reset_halt_req)
+        } else {
+            let dmstatus: Dmstatus = self.read_dm_register()?;
+
+            self.hasresethaltreq = Some(dmstatus.hasresethaltreq());
+
+            Ok(dmstatus.hasresethaltreq())
+        }
+    }
+
+    // Resume the core.
+    pub(crate) fn resume_core(&mut self) -> Result<(), RiscvError> {
+        // set resume request.
+        let mut dmcontrol: Dmcontrol = self.read_dm_register()?;
+        dmcontrol.set_dmactive(true);
+        dmcontrol.set_resumereq(true);
+        self.write_dm_register(dmcontrol)?;
+
+        // check if request has been acknowleged.
+        let status: Dmstatus = self.read_dm_register()?;
+        if !status.allresumeack() {
+            return Err(RiscvError::RequestNotAcknowledged);
+        }
+
+        // clear resume request.
+        dmcontrol.set_resumereq(false);
+        self.write_dm_register(dmcontrol)?;
+
+        Ok(())
+    }
+
+    pub(crate) fn reset_hart_and_halt(&mut self, timeout: Duration) -> Result<(), RiscvError> {
+        tracing::debug!("Resetting core, setting hartreset bit");
+
+        let mut dmcontrol: Dmcontrol = self.read_dm_register()?;
+        dmcontrol.set_dmactive(true);
+        dmcontrol.set_hartreset(true);
+        dmcontrol.set_haltreq(true);
+
+        self.write_dm_register(dmcontrol)?;
+
+        // Read back register to verify reset is supported
+        let readback: Dmcontrol = self.read_dm_register()?;
+
+        if readback.hartreset() {
+            tracing::debug!("Clearing hartreset bit");
+            // Reset is performed by setting the bit high, and then low again
+            let mut dmcontrol = readback;
+            dmcontrol.set_dmactive(true);
+            dmcontrol.set_hartreset(false);
+
+            self.write_dm_register(dmcontrol)?;
+        } else {
+            // Hartreset is not supported, whole core needs to be reset
+            //
+            // TODO: Cache this
+            tracing::debug!("Hartreset bit not supported, using ndmreset");
+            dmcontrol.set_hartreset(false);
+            dmcontrol.set_ndmreset(true);
+            dmcontrol.set_haltreq(true);
+
+            self.write_dm_register(dmcontrol)?;
+
+            tracing::debug!("Clearing ndmreset bit");
+            dmcontrol.set_ndmreset(false);
+            dmcontrol.set_haltreq(true);
+
+            self.write_dm_register(dmcontrol)?;
+        }
+
+        let start = Instant::now();
+
+        loop {
+            // check that cores have reset
+            let readback: Dmstatus = self.read_dm_register()?;
+
+            if readback.allhavereset() && readback.allhalted() {
+                break;
+            }
+
+            if start.elapsed() > timeout {
+                return Err(RiscvError::RequestNotAcknowledged);
+            }
+        }
+
+        // clear the reset request
+        dmcontrol.set_haltreq(false);
+        dmcontrol.set_ackhavereset(true);
+        dmcontrol.set_hartreset(false);
+        dmcontrol.set_ndmreset(false);
+
+        self.write_dm_register(dmcontrol)?;
+
+        // Reenable halt on breakpoint because this gets disabled if we reset the core
+        self.debug_on_sw_breakpoint(true)?; // TODO: only restore if enabled before?
+
+        Ok(())
+    }
+
+    pub(crate) fn debug_on_sw_breakpoint(&mut self, enabled: bool) -> Result<(), RiscvError> {
+        let mut dcsr = Dcsr(self.read_csr(0x7b0)?);
+
+        dcsr.set_ebreakm(enabled);
+        dcsr.set_ebreaks(enabled);
+        dcsr.set_ebreaku(enabled);
+
+        match self.abstract_cmd_register_write(0x7b0, dcsr.0) {
+            Err(RiscvError::AbstractCommand(AbstractCommandErrorKind::NotSupported)) => {
+                tracing::debug!("Could not write core register {:#x} with abstract command, falling back to program buffer", 0x7b0);
+                self.write_csr_progbuf(0x7b0, dcsr.0)
+            }
+            other => other,
+        }
+    }
 }
 
 pub(crate) trait LargeRegister {
@@ -2097,17 +2226,17 @@ impl From<u32> for AccessMemoryCommand {
     }
 }
 
-memory_mapped_bitfield_register! { struct Sbaddress0(u32); 0x39, "sbaddress0", impl From; }
-memory_mapped_bitfield_register! { struct Sbaddress1(u32); 0x3a, "sbaddress1", impl From; }
-memory_mapped_bitfield_register! { struct Sbaddress2(u32); 0x3b, "sbaddress2", impl From; }
-memory_mapped_bitfield_register! { struct Sbaddress3(u32); 0x37, "sbaddress3", impl From; }
+memory_mapped_bitfield_register! { pub struct Sbaddress0(u32); 0x39, "sbaddress0", impl From; }
+memory_mapped_bitfield_register! { pub struct Sbaddress1(u32); 0x3a, "sbaddress1", impl From; }
+memory_mapped_bitfield_register! { pub struct Sbaddress2(u32); 0x3b, "sbaddress2", impl From; }
+memory_mapped_bitfield_register! { pub struct Sbaddress3(u32); 0x37, "sbaddress3", impl From; }
 
-memory_mapped_bitfield_register! { struct Sbdata0(u32); 0x3c, "sbdata0", impl From; }
-memory_mapped_bitfield_register! { struct Sbdata1(u32); 0x3d, "sbdata1", impl From; }
-memory_mapped_bitfield_register! { struct Sbdata2(u32); 0x3e, "sbdata2", impl From; }
-memory_mapped_bitfield_register! { struct Sbdata3(u32); 0x3f, "sbdata3", impl From; }
+memory_mapped_bitfield_register! { pub struct Sbdata0(u32); 0x3c, "sbdata0", impl From; }
+memory_mapped_bitfield_register! { pub struct Sbdata1(u32); 0x3d, "sbdata1", impl From; }
+memory_mapped_bitfield_register! { pub struct Sbdata2(u32); 0x3e, "sbdata2", impl From; }
+memory_mapped_bitfield_register! { pub struct Sbdata3(u32); 0x3f, "sbdata3", impl From; }
 
-memory_mapped_bitfield_register! { struct Confstrptr0(u32); 0x19, "confstrptr0", impl From; }
-memory_mapped_bitfield_register! { struct Confstrptr1(u32); 0x1a, "confstrptr1", impl From; }
-memory_mapped_bitfield_register! { struct Confstrptr2(u32); 0x1b, "confstrptr2", impl From; }
-memory_mapped_bitfield_register! { struct Confstrptr3(u32); 0x1c, "confstrptr3", impl From; }
+memory_mapped_bitfield_register! { pub struct Confstrptr0(u32); 0x19, "confstrptr0", impl From; }
+memory_mapped_bitfield_register! { pub struct Confstrptr1(u32); 0x1a, "confstrptr1", impl From; }
+memory_mapped_bitfield_register! { pub struct Confstrptr2(u32); 0x1b, "confstrptr2", impl From; }
+memory_mapped_bitfield_register! { pub struct Confstrptr3(u32); 0x1c, "confstrptr3", impl From; }
diff --git a/probe-rs/src/architecture/riscv/dtm/dtm_access.rs b/probe-rs/src/architecture/riscv/dtm/dtm_access.rs
index 6014f206..0e011fb5 100644
--- a/probe-rs/src/architecture/riscv/dtm/dtm_access.rs
+++ b/probe-rs/src/architecture/riscv/dtm/dtm_access.rs
@@ -4,6 +4,11 @@ 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> {
+        Ok(())
+    }
+
     /// Asserts a reset of the physical pins
     fn target_reset_assert(&mut self) -> Result<(), DebugProbeError>;
 
diff --git a/probe-rs/src/architecture/riscv/dtm/jtag_dtm.rs b/probe-rs/src/architecture/riscv/dtm/jtag_dtm.rs
index 5a8c740f..90edc5ed 100644
--- a/probe-rs/src/architecture/riscv/dtm/jtag_dtm.rs
+++ b/probe-rs/src/architecture/riscv/dtm/jtag_dtm.rs
@@ -41,7 +41,7 @@ impl JtagDtm {
 
         let dtmcs = Dtmcs(raw_dtmcs);
 
-        tracing::debug!("Dtmcs: {:?}", dtmcs);
+        tracing::debug!("{:?}", dtmcs);
 
         let abits = dtmcs.abits();
         let idle_cycles = dtmcs.idle();
@@ -109,7 +109,7 @@ impl JtagDtm {
         Ok(self.queued_commands.schedule(JtagWriteCommand {
             address: DMI_ADDRESS,
             data: bytes.to_vec(),
-            transform: |result| {
+            transform: |_, result| {
                 Self::transform_dmi_result(result)
                     .map(CommandResult::U32)
                     .map_err(|e| crate::Error::Riscv(e.map_as_err().unwrap_err()))
@@ -147,6 +147,11 @@ impl JtagDtm {
 }
 
 impl DtmAccess for JtagDtm {
+    fn init(&mut self) -> Result<(), DebugProbeError> {
+        self.probe.tap_reset()?;
+        Ok(())
+    }
+
     fn target_reset_assert(&mut self) -> Result<(), DebugProbeError> {
         self.probe.target_reset_assert()
     }
diff --git a/probe-rs/src/architecture/riscv/mod.rs b/probe-rs/src/architecture/riscv/mod.rs
index aa8fccbe..c6ea20ae 100644
--- a/probe-rs/src/architecture/riscv/mod.rs
+++ b/probe-rs/src/architecture/riscv/mod.rs
@@ -3,6 +3,7 @@
 #![allow(clippy::inconsistent_digit_grouping)]
 
 use crate::{
+    architecture::riscv::sequences::RiscvDebugSequence,
     core::{
         Architecture, BreakpointCause, CoreInformation, CoreRegisters, RegisterId, RegisterValue,
     },
@@ -17,7 +18,10 @@ use anyhow::{anyhow, Result};
 use bitfield::bitfield;
 use communication_interface::{AbstractCommandErrorKind, RiscvCommunicationInterface, RiscvError};
 use registers::{FP, RA, RISCV_CORE_REGSISTERS, SP};
-use std::time::{Duration, Instant};
+use std::{
+    sync::Arc,
+    time::{Duration, Instant},
+};
 
 #[macro_use]
 pub(crate) mod registers;
@@ -27,39 +31,39 @@ pub mod communication_interface;
 pub(crate) mod dtm;
 pub mod sequences;
 
-/// An interface to operate RISC-V cores.
+/// An interface to operate a RISC-V core.
 pub struct Riscv32<'probe> {
-    hart: u32,
     interface: &'probe mut RiscvCommunicationInterface,
     state: &'probe mut RiscVState,
+    sequence: Arc<dyn RiscvDebugSequence>,
 }
 
 impl<'probe> Riscv32<'probe> {
-    /// Create a new RISC-V interface.
+    /// Create a new RISC-V interface for a particular hart.
     pub fn new(
         hart: u32,
         interface: &'probe mut RiscvCommunicationInterface,
         state: &'probe mut RiscVState,
+        sequence: Arc<dyn RiscvDebugSequence>,
     ) -> Result<Self, RiscvError> {
         if !interface.hart_enabled(hart) {
             return Err(RiscvError::HartUnavailable);
         }
 
+        interface.select_hart(hart)?;
+
         Ok(Self {
-            hart,
             interface,
             state,
+            sequence,
         })
     }
 
     fn read_csr(&mut self, address: u16) -> Result<u32, RiscvError> {
-        self.interface.select_hart(self.hart)?;
         self.interface.read_csr(address)
     }
 
     fn write_csr(&mut self, address: u16, value: u32) -> Result<(), RiscvError> {
-        self.interface.select_hart(self.hart)?;
-
         tracing::debug!("Writing CSR {:#x}", address);
 
         match self.interface.abstract_cmd_register_write(address, value) {
@@ -71,48 +75,14 @@ impl<'probe> Riscv32<'probe> {
         }
     }
 
-    // Resume the core.
+    /// Resume the core.
     fn resume_core(&mut self) -> Result<(), crate::Error> {
-        self.interface.select_hart(self.hart)?;
         self.state.semihosting_command = None;
-
-        // set resume request.
-        let mut dmcontrol: Dmcontrol = self.interface.read_dm_register()?;
-        dmcontrol.set_dmactive(true);
-        dmcontrol.set_resumereq(true);
-        self.interface.write_dm_register(dmcontrol)?;
-
-        // check if request has been acknowleged.
-        let status: Dmstatus = self.interface.read_dm_register()?;
-        if !status.allresumeack() {
-            return Err(RiscvError::RequestNotAcknowledged.into());
-        };
-
-        // clear resume request.
-        dmcontrol.set_resumereq(false);
-        self.interface.write_dm_register(dmcontrol)?;
+        self.interface.resume_core()?;
 
         Ok(())
     }
 
-    /// Check if the connected device supports halt after reset.
-    ///
-    /// Returns a cached value if available, otherwise queries the
-    /// `hasresethaltreq` bit in the `dmstatus` register.
-    fn supports_reset_halt_req(&mut self) -> Result<bool, crate::Error> {
-        self.interface.select_hart(self.hart)?;
-
-        if let Some(has_reset_halt_req) = self.state.hasresethaltreq {
-            Ok(has_reset_halt_req)
-        } else {
-            let dmstatus: Dmstatus = self.interface.read_dm_register()?;
-
-            self.state.hasresethaltreq = Some(dmstatus.hasresethaltreq());
-
-            Ok(dmstatus.hasresethaltreq())
-        }
-    }
-
     /// Check if the current breakpoint is a semihosting call
     fn check_for_semihosting(&mut self) -> Result<Option<SemihostingCommand>, Error> {
         let pc: u32 = self.read_core_reg(self.program_counter().id)?.try_into()?;
@@ -166,21 +136,16 @@ impl<'probe> Riscv32<'probe> {
 
 impl<'probe> CoreInterface for Riscv32<'probe> {
     fn wait_for_core_halted(&mut self, timeout: Duration) -> Result<(), crate::Error> {
-        self.interface.select_hart(self.hart)?;
         Ok(self.interface.wait_for_core_halted(timeout)?)
     }
 
     fn core_halted(&mut self) -> Result<bool, crate::Error> {
-        self.interface.select_hart(self.hart)?;
-
         let dmstatus: Dmstatus = self.interface.read_dm_register()?;
 
         Ok(dmstatus.allhalted())
     }
 
     fn status(&mut self) -> Result<crate::core::CoreStatus, crate::Error> {
-        self.interface.select_hart(self.hart)?;
-
         // TODO: We should use hartsum to determine if any hart is halted
         //       quickly
 
@@ -223,14 +188,11 @@ impl<'probe> CoreInterface for Riscv32<'probe> {
         }
     }
 
-    fn halt(&mut self, timeout: Duration) -> Result<CoreInformation, crate::Error> {
-        self.interface.select_hart(self.hart)?;
+    fn halt(&mut self, timeout: Duration) -> Result<CoreInformation, Error> {
         Ok(self.interface.halt(timeout)?)
     }
 
-    fn run(&mut self) -> Result<(), crate::Error> {
-        self.interface.select_hart(self.hart)?;
-
+    fn run(&mut self) -> Result<(), Error> {
         // Before we run, we always perform a single instruction step, to account for possible breakpoints that might get us stuck on the current instruction.
         self.step()?;
 
@@ -240,98 +202,23 @@ impl<'probe> CoreInterface for Riscv32<'probe> {
         Ok(())
     }
 
-    fn reset(&mut self) -> Result<(), crate::Error> {
-        self.interface.select_hart(self.hart)?;
+    fn reset(&mut self) -> Result<(), Error> {
+        self.reset_and_halt(Duration::from_secs(1))?;
+        self.resume_core()?;
 
-        match self.reset_and_halt(Duration::from_millis(500)) {
-            Ok(_) => self.resume_core()?,
-            Err(error) => {
-                return Err(RiscvError::DebugProbe(DebugProbeError::Other(
-                    anyhow::anyhow!("Error during reset").context(error),
-                ))
-                .into());
-            }
-        }
         Ok(())
     }
 
-    fn reset_and_halt(
-        &mut self,
-        timeout: Duration,
-    ) -> Result<crate::core::CoreInformation, crate::Error> {
-        self.interface.select_hart(self.hart)?;
-
-        tracing::debug!("Resetting core, setting hartreset bit");
-
-        let mut dmcontrol: Dmcontrol = self.interface.read_dm_register()?;
-        dmcontrol.set_dmactive(true);
-        dmcontrol.set_hartreset(true);
-        dmcontrol.set_haltreq(true);
-
-        self.interface.write_dm_register(dmcontrol)?;
-
-        // Read back register to verify reset is supported
-        let readback: Dmcontrol = self.interface.read_dm_register()?;
-
-        if readback.hartreset() {
-            tracing::debug!("Clearing hartreset bit");
-            // Reset is performed by setting the bit high, and then low again
-            let mut dmcontrol = readback;
-            dmcontrol.set_dmactive(true);
-            dmcontrol.set_hartreset(false);
-
-            self.interface.write_dm_register(dmcontrol)?;
-        } else {
-            // Hartreset is not supported, whole core needs to be reset
-            //
-            // TODO: Cache this
-            tracing::debug!("Hartreset bit not supported, using ndmreset");
-            dmcontrol.set_hartreset(false);
-            dmcontrol.set_ndmreset(true);
-            dmcontrol.set_haltreq(true);
-
-            self.interface.write_dm_register(dmcontrol)?;
-
-            tracing::debug!("Clearing ndmreset bit");
-            dmcontrol.set_ndmreset(false);
-            dmcontrol.set_haltreq(true);
-
-            self.interface.write_dm_register(dmcontrol)?;
-        }
-
-        let start = Instant::now();
-
-        loop {
-            // check that cores have reset
-            let readback: Dmstatus = self.interface.read_dm_register()?;
-
-            if readback.allhavereset() && readback.allhalted() {
-                break;
-            }
-
-            if start.elapsed() > timeout {
-                return Err(RiscvError::RequestNotAcknowledged.into());
-            }
-        }
-
-        // acknowledge the reset, clear the halt request
-        dmcontrol.set_hartreset(false);
-        dmcontrol.set_ndmreset(false);
-        dmcontrol.set_ackhavereset(true);
-
-        self.interface.write_dm_register(dmcontrol)?;
-
-        // Reenable halt on breakpoint because this gets disabled if we reset the core
-        self.debug_on_sw_breakpoint(true)?; // TODO: only restore if enabled before?
+    fn reset_and_halt(&mut self, timeout: Duration) -> Result<CoreInformation, Error> {
+        self.sequence
+            .reset_system_and_halt(self.interface, timeout)?;
 
         let pc = self.read_core_reg(RegisterId(0x7b1))?;
 
         Ok(CoreInformation { pc: pc.try_into()? })
     }
 
-    fn step(&mut self) -> Result<crate::core::CoreInformation, crate::Error> {
-        self.interface.select_hart(self.hart)?;
-
+    fn step(&mut self) -> Result<CoreInformation, crate::Error> {
         let halt_reason = self.status()?;
         let flashing_done = self.state.hw_breakpoints_enabled;
         if matches!(
@@ -397,8 +284,6 @@ impl<'probe> CoreInterface for Riscv32<'probe> {
     }
 
     fn read_core_reg(&mut self, address: RegisterId) -> Result<RegisterValue, crate::Error> {
-        self.interface.select_hart(self.hart)?;
-
         self.read_csr(address.0)
             .map(|v| v.into())
             .map_err(|e| e.into())
@@ -409,15 +294,11 @@ impl<'probe> CoreInterface for Riscv32<'probe> {
         address: RegisterId,
         value: RegisterValue,
     ) -> Result<(), crate::Error> {
-        self.interface.select_hart(self.hart)?;
-
         let value: u32 = value.try_into()?;
         self.write_csr(address.0, value).map_err(|e| e.into())
     }
 
     fn available_breakpoint_units(&mut self) -> Result<u32, crate::Error> {
-        self.interface.select_hart(self.hart)?;
-
         // TODO: This should probably only be done once, when initialising
 
         tracing::debug!("Determining number of HW breakpoints supported");
@@ -492,7 +373,6 @@ impl<'probe> CoreInterface for Riscv32<'probe> {
     /// NOTE: For riscv, this assumes that only execution breakpoints are used.
     fn hw_breakpoints(&mut self) -> Result<Vec<Option<u64>>, Error> {
         // this can be called w/o halting the core via Session::new - temporarily halt if not halted
-        self.interface.select_hart(self.hart)?;
 
         let was_running = !self.core_halted()?;
         if was_running {
@@ -542,8 +422,6 @@ impl<'probe> CoreInterface for Riscv32<'probe> {
     }
 
     fn enable_breakpoints(&mut self, state: bool) -> Result<(), crate::Error> {
-        self.interface.select_hart(self.hart)?;
-
         // Loop through all triggers, and enable/disable them.
         let tselect = 0x7a0;
         let tdata1 = 0x7a1;
@@ -579,8 +457,6 @@ impl<'probe> CoreInterface for Riscv32<'probe> {
     }
 
     fn set_hw_breakpoint(&mut self, bp_unit_index: usize, addr: u64) -> Result<(), crate::Error> {
-        self.interface.select_hart(self.hart)?;
-
         let addr = valid_32bit_address(addr)?;
 
         if !self.hw_breakpoints_enabled() {
@@ -636,7 +512,6 @@ impl<'probe> CoreInterface for Riscv32<'probe> {
 
     fn clear_hw_breakpoint(&mut self, unit_index: usize) -> Result<(), crate::Error> {
         // this can be called w/o halting the core via Session::new - temporarily halt if not halted
-        self.interface.select_hart(self.hart)?;
 
         let was_running = !self.core_halted()?;
         if was_running {
@@ -682,16 +557,9 @@ impl<'probe> CoreInterface for Riscv32<'probe> {
         self.state.hw_breakpoints_enabled
     }
 
-    fn debug_on_sw_breakpoint(&mut self, enabled: bool) -> Result<(), crate::error::Error> {
-        self.interface.select_hart(self.hart)?;
-
-        let mut dcsr = Dcsr(self.read_core_reg(RegisterId(0x7b0))?.try_into()?);
-
-        dcsr.set_ebreakm(enabled);
-        dcsr.set_ebreaks(enabled);
-        dcsr.set_ebreaku(enabled);
-
-        self.write_csr(0x7b0, dcsr.0).map_err(|e| e.into())
+    fn debug_on_sw_breakpoint(&mut self, enabled: bool) -> Result<(), Error> {
+        self.interface.debug_on_sw_breakpoint(enabled)?;
+        Ok(())
     }
 
     fn architecture(&self) -> Architecture {
@@ -723,8 +591,6 @@ impl<'probe> CoreInterface for Riscv32<'probe> {
     }
 
     fn fpu_support(&mut self) -> Result<bool, crate::error::Error> {
-        self.interface.select_hart(self.hart)?;
-
         // Read the extensions from the Machine ISA regiseter.
         let isa_extensions =
             Misa::from(self.read_csr(Misa::get_mmio_address() as u16)?).extensions();
@@ -734,42 +600,16 @@ impl<'probe> CoreInterface for Riscv32<'probe> {
     }
 
     fn reset_catch_set(&mut self) -> Result<(), Error> {
-        self.interface.select_hart(self.hart)?;
-
-        if !self.supports_reset_halt_req()? {
-            return Err(Error::Riscv(RiscvError::ResetHaltRequestNotSupported));
-        }
-
-        let mut dmcontrol: Dmcontrol = self.interface.read_dm_register()?;
-
-        dmcontrol.set_dmactive(true);
-        dmcontrol.set_resethaltreq(true);
-
-        self.interface.write_dm_register(dmcontrol)?;
-
+        self.sequence.reset_catch_set(self.interface)?;
         Ok(())
     }
 
     fn reset_catch_clear(&mut self) -> Result<(), Error> {
-        self.interface.select_hart(self.hart)?;
-
-        if !self.supports_reset_halt_req()? {
-            return Err(Error::Riscv(RiscvError::ResetHaltRequestNotSupported));
-        }
-
-        let mut dmcontrol: Dmcontrol = self.interface.read_dm_register()?;
-
-        dmcontrol.set_dmactive(true);
-        dmcontrol.set_clrresethaltreq(true);
-
-        self.interface.write_dm_register(dmcontrol)?;
-
+        self.sequence.reset_catch_clear(self.interface)?;
         Ok(())
     }
 
     fn debug_core_stop(&mut self) -> Result<(), Error> {
-        self.interface.select_hart(self.hart)?;
-
         self.debug_on_sw_breakpoint(false)?;
         Ok(())
     }
@@ -781,87 +621,70 @@ impl<'probe> MemoryInterface for Riscv32<'probe> {
     }
 
     fn read_word_64(&mut self, address: u64) -> Result<u64, crate::error::Error> {
-        self.interface.select_hart(self.hart)?;
         self.interface.read_word_64(address)
     }
 
     fn read_word_32(&mut self, address: u64) -> Result<u32, Error> {
-        self.interface.select_hart(self.hart)?;
         self.interface.read_word_32(address)
     }
 
     fn read_word_16(&mut self, address: u64) -> Result<u16, Error> {
-        self.interface.select_hart(self.hart)?;
         self.interface.read_word_16(address)
     }
 
     fn read_word_8(&mut self, address: u64) -> Result<u8, Error> {
-        self.interface.select_hart(self.hart)?;
         self.interface.read_word_8(address)
     }
 
     fn read_64(&mut self, address: u64, data: &mut [u64]) -> Result<(), Error> {
-        self.interface.select_hart(self.hart)?;
         self.interface.read_64(address, data)
     }
 
     fn read_32(&mut self, address: u64, data: &mut [u32]) -> Result<(), Error> {
-        self.interface.select_hart(self.hart)?;
         self.interface.read_32(address, data)
     }
 
     fn read_16(&mut self, address: u64, data: &mut [u16]) -> Result<(), Error> {
-        self.interface.select_hart(self.hart)?;
         self.interface.read_16(address, data)
     }
 
     fn read_8(&mut self, address: u64, data: &mut [u8]) -> Result<(), Error> {
-        self.interface.select_hart(self.hart)?;
         self.interface.read_8(address, data)
     }
 
     fn write_word_64(&mut self, address: u64, data: u64) -> Result<(), Error> {
-        self.interface.select_hart(self.hart)?;
         self.interface.write_word_64(address, data)
     }
 
     fn write_word_32(&mut self, address: u64, data: u32) -> Result<(), Error> {
-        self.interface.select_hart(self.hart)?;
         self.interface.write_word_32(address, data)
     }
 
     fn write_word_16(&mut self, address: u64, data: u16) -> Result<(), Error> {
-        self.interface.select_hart(self.hart)?;
         self.interface.write_word_16(address, data)
     }
 
     fn write_word_8(&mut self, address: u64, data: u8) -> Result<(), Error> {
-        self.interface.select_hart(self.hart)?;
         self.interface.write_word_8(address, data)
     }
 
     fn write_64(&mut self, address: u64, data: &[u64]) -> Result<(), Error> {
-        self.interface.select_hart(self.hart)?;
         self.interface.write_64(address, data)
     }
 
     fn write_32(&mut self, address: u64, data: &[u32]) -> Result<(), Error> {
-        self.interface.select_hart(self.hart)?;
         self.interface.write_32(address, data)
     }
 
     fn write_16(&mut self, address: u64, data: &[u16]) -> Result<(), Error> {
-        self.interface.select_hart(self.hart)?;
         self.interface.write_16(address, data)
     }
 
     fn write_8(&mut self, address: u64, data: &[u8]) -> Result<(), Error> {
-        self.interface.select_hart(self.hart)?;
         self.interface.write_8(address, data)
     }
 
     fn write(&mut self, address: u64, data: &[u8]) -> Result<(), Error> {
-        self.interface.select_hart(self.hart)?;
         self.interface.write(address, data)
     }
 
@@ -870,7 +693,6 @@ impl<'probe> MemoryInterface for Riscv32<'probe> {
     }
 
     fn flush(&mut self) -> Result<(), Error> {
-        self.interface.select_hart(self.hart)?;
         self.interface.flush()
     }
 }
@@ -881,9 +703,6 @@ pub struct RiscVState {
     /// A flag to remember whether we want to use hw_breakpoints during stepping of the core.
     hw_breakpoints_enabled: bool,
 
-    /// Store the value of the `hasresethaltreq` bit of the `dmcstatus` register.
-    hasresethaltreq: Option<bool>,
-
     /// The semihosting command that was decoded at the current program counter
     semihosting_command: Option<SemihostingCommand>,
 }
@@ -892,36 +711,56 @@ impl RiscVState {
     pub(crate) fn new() -> Self {
         Self {
             hw_breakpoints_enabled: false,
-            hasresethaltreq: None,
             semihosting_command: None,
         }
     }
 }
 
 memory_mapped_bitfield_register! {
-    /// `dmcontrol` register, located at
-    /// address 0x10
+    /// `dmcontrol` register, located at address 0x10
     pub struct Dmcontrol(u32);
     0x10, "dmcontrol",
     impl From;
-    _, set_haltreq: 31;
-    _, set_resumereq: 30;
-    hartreset, set_hartreset: 29;
-    _, set_ackhavereset: 28;
-    hasel, set_hasel: 26;
-    hartsello, set_hartsello: 25, 16;
-    hartselhi, set_hartselhi: 15, 6;
-    _, set_resethaltreq: 3;
-    _, set_clrresethaltreq: 2;
-    ndmreset, set_ndmreset: 1;
-    dmactive, set_dmactive: 0;
+
+    /// Requests the currently selected harts to halt.
+    pub _, set_haltreq: 31;
+
+    /// Requests the currently selected harts to resume.
+    pub _, set_resumereq: 30;
+
+    /// Requests the currently selected harts to reset. Optional.
+    pub hartreset, set_hartreset: 29;
+
+    /// Writing 1 clears the `havereset` flag of selected harts.
+    pub _, set_ackhavereset: 28;
+
+    /// Selects the definition of currently selected harts. If 1, multiple harts may be selected.
+    pub hasel, set_hasel: 26;
+
+    /// The lower 10 bits of the currently selected harts.
+    pub hartsello, set_hartsello: 25, 16;
+
+    /// The upper 10 bits of the currently selected harts.
+    pub hartselhi, set_hartselhi: 15, 6;
+
+    /// Writes the halt-on-reset request bit for all currently selected hart. Optional.
+    pub _, set_resethaltreq: 3;
+
+    /// Clears the halt-on-reset request bit for all currently selected harts. Optional.
+    pub _, set_clrresethaltreq: 2;
+
+    /// This bit controls the reset signal from the DM to the rest of the system.
+    pub ndmreset, set_ndmreset: 1;
+
+    /// Reset signal for the debug module.
+    pub dmactive, set_dmactive: 0;
 }
 
 impl Dmcontrol {
     /// Currently selected harts
     ///
     /// Combination of the `hartselhi` and `hartsello` registers.
-    fn hartsel(&self) -> u32 {
+    pub fn hartsel(&self) -> u32 {
         self.hartselhi() << 10 | self.hartsello()
     }
 
@@ -929,7 +768,7 @@ impl Dmcontrol {
     ///
     /// This sets the `hartselhi` and `hartsello` registers.
     /// This is a 20 bit register, larger values will be truncated.
-    fn set_hartsel(&mut self, value: u32) {
+    pub fn set_hartsel(&mut self, value: u32) {
         self.set_hartsello(value & 0x3ff);
         self.set_hartselhi((value >> 10) & 0x3ff);
     }
@@ -963,21 +802,21 @@ memory_mapped_bitfield_register! {
 }
 
 bitfield! {
-        struct Dcsr(u32);
-        impl Debug;
-
-        xdebugver, _: 31, 28;
-        ebreakm, set_ebreakm: 15;
-        ebreaks, set_ebreaks: 13;
-        ebreaku, set_ebreaku: 12;
-        stepie, set_stepie: 11;
-        stopcount, set_stopcount: 10;
-        stoptime, set_stoptime: 9;
-        cause, set_cause: 8, 6;
-        mprven, set_mprven: 4;
-        nmip, _: 3;
-        step, set_step: 2;
-        prv, set_prv: 1,0;
+    struct Dcsr(u32);
+    impl Debug;
+
+    xdebugver, _: 31, 28;
+    ebreakm, set_ebreakm: 15;
+    ebreaks, set_ebreaks: 13;
+    ebreaku, set_ebreaku: 12;
+    stepie, set_stepie: 11;
+    stopcount, set_stopcount: 10;
+    stoptime, set_stoptime: 9;
+    cause, set_cause: 8, 6;
+    mprven, set_mprven: 4;
+    nmip, _: 3;
+    step, set_step: 2;
+    prv, set_prv: 1,0;
 }
 
 memory_mapped_bitfield_register! {
diff --git a/probe-rs/src/architecture/riscv/sequences.rs b/probe-rs/src/architecture/riscv/sequences.rs
index 088c2d4b..1c8a3179 100644
--- a/probe-rs/src/architecture/riscv/sequences.rs
+++ b/probe-rs/src/architecture/riscv/sequences.rs
@@ -1,8 +1,12 @@
 //! Debug sequences to operate special requirements RISC-V targets.
 
+use crate::architecture::riscv::communication_interface::RiscvError;
+use crate::architecture::riscv::Dmcontrol;
+
 use super::communication_interface::RiscvCommunicationInterface;
 use std::fmt::Debug;
 use std::sync::Arc;
+use std::time::Duration;
 
 /// A interface to operate debug sequences for RISC-V targets.
 ///
@@ -20,6 +24,55 @@ pub trait RiscvDebugSequence: Send + Sync + Debug {
     ) -> Result<Option<usize>, crate::Error> {
         Ok(None)
     }
+
+    /// Configure the target to stop code execution after a reset. After this, the core will halt when it comes
+    /// out of reset.
+    fn reset_catch_set(
+        &self,
+        interface: &mut RiscvCommunicationInterface,
+    ) -> Result<(), RiscvError> {
+        if !interface.supports_reset_halt_req()? {
+            return Err(RiscvError::ResetHaltRequestNotSupported);
+        }
+
+        let mut dmcontrol: Dmcontrol = interface.read_dm_register()?;
+
+        dmcontrol.set_dmactive(true);
+        dmcontrol.set_resethaltreq(true);
+
+        interface.write_dm_register(dmcontrol)?;
+
+        Ok(())
+    }
+
+    /// Free hardware resources allocated by ResetCatchSet.
+    fn reset_catch_clear(
+        &self,
+        interface: &mut RiscvCommunicationInterface,
+    ) -> Result<(), RiscvError> {
+        if !interface.supports_reset_halt_req()? {
+            return Err(RiscvError::ResetHaltRequestNotSupported);
+        }
+
+        let mut dmcontrol: Dmcontrol = interface.read_dm_register()?;
+
+        dmcontrol.set_dmactive(true);
+        dmcontrol.set_clrresethaltreq(true);
+
+        interface.write_dm_register(dmcontrol)?;
+
+        Ok(())
+    }
+
+    /// Executes a system-wide reset without debug domain (or warm-reset that preserves debug connection) via software mechanisms.
+    fn reset_system_and_halt(
+        &self,
+        interface: &mut RiscvCommunicationInterface,
+        timeout: Duration,
+    ) -> Result<(), crate::Error> {
+        interface.reset_hart_and_halt(timeout)?;
+        Ok(())
+    }
 }
 
 /// The default sequences that is used for RISC-V chips that do not specify a specific sequence.
diff --git a/probe-rs/src/architecture/xtensa/communication_interface.rs b/probe-rs/src/architecture/xtensa/communication_interface.rs
index 98c00ac1..573a769d 100644
--- a/probe-rs/src/architecture/xtensa/communication_interface.rs
+++ b/probe-rs/src/architecture/xtensa/communication_interface.rs
@@ -16,26 +16,25 @@ use crate::{
 use super::xdm::{Error as XdmError, Xdm};
 
 /// Possible Xtensa errors
-#[derive(thiserror::Error, Debug)]
+#[derive(thiserror::Error, Debug, docsplay::Display)]
 pub enum XtensaError {
-    /// An error originating from the DebugProbe
-    #[error("Debug Probe Error")]
+    /// An error originating from the DebugProbe occurred
     DebugProbe(#[from] DebugProbeError),
+
     /// Xtensa debug module error
-    #[error("Xtensa debug module error")]
     XdmError(#[from] XdmError),
-    /// A timeout occurred
+
+    /// The operation has timed out
     // TODO: maybe we could be a bit more specific
-    #[error("The operation has timed out")]
     Timeout,
+
     /// The connected target is not an Xtensa device.
-    #[error("Connected target is not an Xtensa device.")]
     NoXtensaTarget,
+
     /// The requested register is not available.
-    #[error("The requested register is not available.")]
     RegisterNotAvailable,
+
     /// The result index of a batched command is not available.
-    #[error("The requested data is not available due to a previous error.")]
     BatchedResultNotAvailable,
 }
 
@@ -154,62 +153,6 @@ impl XtensaCommunicationInterface {
         self.hw_breakpoint_num
     }
 
-    /// Enters OCD mode and halts the core.
-    pub fn enter_ocd_mode(&mut self) -> Result<(), XtensaError> {
-        self.xdm.halt()?;
-        tracing::info!("Entered OCD mode");
-        Ok(())
-    }
-
-    /// Returns whether the core is in OCD mode.
-    pub fn is_in_ocd_mode(&mut self) -> Result<bool, XtensaError> {
-        self.xdm.is_in_ocd_mode()
-    }
-
-    /// Leaves OCD mode, restores state and resumes program execution.
-    pub fn leave_ocd_mode(&mut self) -> Result<(), XtensaError> {
-        self.restore_registers()?;
-        self.resume()?;
-        self.xdm.leave_ocd_mode()?;
-        tracing::info!("Left OCD mode");
-        Ok(())
-    }
-
-    /// Resets the processor core.
-    pub fn reset(&mut self) -> Result<(), XtensaError> {
-        match self.reset_and_halt(Duration::from_millis(500)) {
-            Ok(_) => {
-                self.resume()?;
-
-                Ok(())
-            }
-            Err(error) => Err(XtensaError::DebugProbe(DebugProbeError::Other(
-                anyhow::anyhow!("Error during reset").context(error),
-            ))),
-        }
-    }
-
-    /// Resets the processor core and halts it immediately.
-    pub fn reset_and_halt(&mut self, timeout: Duration) -> Result<(), XtensaError> {
-        self.xdm.target_reset_assert()?;
-        self.xdm.halt_on_reset(true);
-        self.xdm.target_reset_deassert()?;
-        self.wait_for_core_halted(timeout)?;
-        self.xdm.halt_on_reset(false);
-
-        // TODO: this is only necessary to run code, so this might not be the best place
-        // Make sure the CPU is in a known state and is able to run code we download.
-        self.write_register({
-            let mut ps = ProgramStatus(0);
-            ps.set_intlevel(1);
-            ps.set_user_mode(true);
-            ps.set_woe(true);
-            ps
-        })?;
-
-        Ok(())
-    }
-
     /// Halts the core.
     pub fn halt(&mut self) -> Result<(), XtensaError> {
         tracing::debug!("Halting core");
@@ -219,7 +162,7 @@ impl XtensaCommunicationInterface {
     /// Returns whether the core is halted.
     pub fn is_halted(&mut self) -> Result<bool, XtensaError> {
         if !self.state.is_halted {
-            self.state.is_halted = self.xdm.is_halted()?;
+            self.state.is_halted = self.xdm.status()?.stopped();
         }
 
         Ok(self.state.is_halted)
@@ -465,7 +408,7 @@ impl XtensaCommunicationInterface {
     }
 
     #[tracing::instrument(skip(self))]
-    fn restore_registers(&mut self) -> Result<(), XtensaError> {
+    pub(super) fn restore_registers(&mut self) -> Result<(), XtensaError> {
         tracing::debug!("Restoring registers");
 
         // Clone the list of saved registers so we can iterate over it, but code may still save
@@ -662,7 +605,7 @@ impl XtensaCommunicationInterface {
         self.xdm.execute()
     }
 
-    fn write_memory(&mut self, address: u64, data: &[u8]) -> Result<(), XtensaError> {
+    pub(crate) fn write_memory(&mut self, address: u64, data: &[u8]) -> Result<(), XtensaError> {
         tracing::debug!("Writing {} bytes to address {:08x}", data.len(), address);
         if data.is_empty() {
             return Ok(());
@@ -728,6 +671,13 @@ impl XtensaCommunicationInterface {
 
         Ok(())
     }
+
+    pub(crate) fn reset_and_halt(&mut self, timeout: Duration) -> Result<(), XtensaError> {
+        self.xdm.reset_and_halt()?;
+        self.wait_for_core_halted(timeout)?;
+
+        Ok(())
+    }
 }
 
 /// DataType
diff --git a/probe-rs/src/architecture/xtensa/mod.rs b/probe-rs/src/architecture/xtensa/mod.rs
index 9b73e42c..07411adc 100644
--- a/probe-rs/src/architecture/xtensa/mod.rs
+++ b/probe-rs/src/architecture/xtensa/mod.rs
@@ -1,6 +1,6 @@
 //! All the interface bits for Xtensa.
 
-use std::time::Duration;
+use std::{sync::Arc, time::Duration};
 
 use probe_rs_target::{Architecture, CoreType, InstructionSet};
 
@@ -9,6 +9,7 @@ use crate::{
         arch::{instruction::Instruction, Register, SpecialRegister},
         communication_interface::{DebugCause, IBreakEn},
         registers::{FP, PC, RA, SP, XTENSA_CORE_REGSISTERS},
+        sequences::XtensaDebugSequence,
     },
     core::{
         registers::{CoreRegisters, RegisterId, RegisterValue},
@@ -62,22 +63,28 @@ impl XtensaState {
     }
 }
 
-/// An interface to operate Xtensa cores.
+/// An interface to operate an Xtensa core.
 pub struct Xtensa<'probe> {
     interface: &'probe mut XtensaCommunicationInterface,
     state: &'probe mut XtensaState,
+    sequence: Arc<dyn XtensaDebugSequence>,
 }
 
 impl<'probe> Xtensa<'probe> {
     const IBREAKA_REGS: [SpecialRegister; 2] =
         [SpecialRegister::IBreakA0, SpecialRegister::IBreakA1];
 
-    /// Create a new Xtensa interface.
+    /// Create a new Xtensa interface for a particular core.
     pub fn new(
         interface: &'probe mut XtensaCommunicationInterface,
         state: &'probe mut XtensaState,
+        sequence: Arc<dyn XtensaDebugSequence>,
     ) -> Self {
-        Self { interface, state }
+        Self {
+            interface,
+            state,
+            sequence,
+        }
     }
 
     fn core_info(&mut self) -> Result<CoreInformation, Error> {
@@ -293,12 +300,16 @@ impl<'probe> CoreInterface for Xtensa<'probe> {
 
     fn reset(&mut self) -> Result<(), Error> {
         self.state.semihosting_command = None;
-        Ok(self.interface.reset()?)
+        self.sequence
+            .reset_system_and_halt(self.interface, Duration::from_millis(500))?;
+
+        self.run()
     }
 
     fn reset_and_halt(&mut self, timeout: Duration) -> Result<CoreInformation, Error> {
         self.state.semihosting_command = None;
-        self.interface.reset_and_halt(timeout)?;
+        self.sequence
+            .reset_system_and_halt(self.interface, timeout)?;
 
         self.core_info()
     }
@@ -443,17 +454,18 @@ impl<'probe> CoreInterface for Xtensa<'probe> {
     }
 
     fn reset_catch_set(&mut self) -> Result<(), Error> {
-        self.interface.xdm.halt_on_reset(true);
-        Ok(())
+        Err(Error::NotImplemented("reset_catch_set"))
     }
 
     fn reset_catch_clear(&mut self) -> Result<(), Error> {
-        self.interface.xdm.halt_on_reset(false);
-        Ok(())
+        Err(Error::NotImplemented("reset_catch_clear"))
     }
 
     fn debug_core_stop(&mut self) -> Result<(), Error> {
-        self.interface.leave_ocd_mode()?;
+        self.interface.restore_registers()?;
+        self.interface.resume()?;
+        self.interface.xdm.leave_ocd_mode()?;
+        tracing::info!("Left OCD mode");
         Ok(())
     }
 }
diff --git a/probe-rs/src/architecture/xtensa/sequences.rs b/probe-rs/src/architecture/xtensa/sequences.rs
index 225210e2..9969f75e 100644
--- a/probe-rs/src/architecture/xtensa/sequences.rs
+++ b/probe-rs/src/architecture/xtensa/sequences.rs
@@ -1,6 +1,8 @@
-use std::{fmt::Debug, sync::Arc};
+use std::{fmt::Debug, sync::Arc, time::Duration};
 
-use crate::architecture::xtensa::communication_interface::XtensaCommunicationInterface;
+use crate::architecture::xtensa::communication_interface::{
+    ProgramStatus, XtensaCommunicationInterface, XtensaError,
+};
 
 /// A interface to operate debug sequences for Xtensa targets.
 ///
@@ -21,6 +23,27 @@ pub trait XtensaDebugSequence: Send + Sync + Debug {
     ) -> Result<Option<usize>, crate::Error> {
         Ok(None)
     }
+
+    /// Executes a system-wide reset without debug domain (or warm-reset that preserves debug connection) via software mechanisms.
+    fn reset_system_and_halt(
+        &self,
+        interface: &mut XtensaCommunicationInterface,
+        timeout: Duration,
+    ) -> Result<(), XtensaError> {
+        interface.reset_and_halt(timeout)?;
+
+        // TODO: this is only necessary to run code, so this might not be the best place
+        // Make sure the CPU is in a known state and is able to run code we download.
+        interface.write_register({
+            let mut ps = ProgramStatus(0);
+            ps.set_intlevel(1);
+            ps.set_user_mode(true);
+            ps.set_woe(true);
+            ps
+        })?;
+
+        Ok(())
+    }
 }
 
 /// The default sequences that is used for Xtensa chips that do not specify a specific sequence.
diff --git a/probe-rs/src/architecture/xtensa/xdm.rs b/probe-rs/src/architecture/xtensa/xdm.rs
index 70b1821b..6a56328c 100644
--- a/probe-rs/src/architecture/xtensa/xdm.rs
+++ b/probe-rs/src/architecture/xtensa/xdm.rs
@@ -85,22 +85,26 @@ impl From<PowerDevice> for TapInstruction {
     }
 }
 
-#[derive(thiserror::Error, Debug, Copy, Clone)]
+#[derive(thiserror::Error, Debug, Copy, Clone, docsplay::Display)]
 pub enum DebugRegisterError {
-    #[error("Register is busy")]
+    /// Register is busy
     Busy,
 
-    #[error("Register-specific error")]
+    /// Register-specific error
     Error,
 
-    #[error("Unexpected value")]
-    Unexpected,
+    /// Unexpected value {0}
+    Unexpected(u8),
 }
 
 #[derive(thiserror::Error, Debug, Clone, Copy)]
 pub enum Error {
-    #[error("Error while accessing register")]
-    Xdm(#[from] DebugRegisterError),
+    #[error("Error {access} register {narsel:#04X}")]
+    Xdm {
+        narsel: u8,
+        access: &'static str,
+        source: DebugRegisterError,
+    },
 
     #[error("ExecExeception")]
     ExecExeception,
@@ -126,8 +130,6 @@ pub struct Xdm {
 
     last_instruction: Option<Instruction>,
 
-    halt_on_reset: bool,
-
     queue: JtagCommandQueue,
     jtag_results: DeferredResultSet,
 
@@ -143,8 +145,6 @@ impl Xdm {
             device_id: 0,
             last_instruction: None,
 
-            halt_on_reset: false,
-
             queue: JtagCommandQueue::new(),
             jtag_results: DeferredResultSet::new(),
 
@@ -160,6 +160,8 @@ impl Xdm {
 
     #[tracing::instrument(skip(self))]
     fn init(&mut self) -> Result<(), XtensaError> {
+        self.probe.tap_reset()?;
+
         let mut pwr_control = PowerControl(0);
 
         pwr_control.set_debug_wakeup(true);
@@ -240,6 +242,8 @@ impl Xdm {
     pub(super) fn execute(&mut self) -> Result<(), XtensaError> {
         let mut queue = std::mem::take(&mut self.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);
@@ -252,9 +256,10 @@ impl Xdm {
                 }
                 Err(e) => {
                     match e.error {
-                        ProbeRsError::Xtensa(XtensaError::XdmError(Error::Xdm(
-                            DebugRegisterError::Busy,
-                        ))) => {
+                        ProbeRsError::Xtensa(XtensaError::XdmError(Error::Xdm {
+                            source: DebugRegisterError::Busy,
+                            ..
+                        })) => {
                             // The specific nexus register may need some longer delay. For now we just
                             // retry, but we should probably add some no-ops later.
                         }
@@ -301,19 +306,22 @@ impl Xdm {
             address: TapInstruction::Nar.code(),
             data: nar.to_le_bytes().to_vec(),
             len: TapInstruction::Nar.bits(),
-            transform: |capture| {
-                let nar = TapInstruction::Nar.capture_to_u8(&capture);
-                let error = match nar & 0b00000011 {
-                    0 => return Ok(CommandResult::None),
-                    1 => DebugRegisterError::Error,
-                    2 => DebugRegisterError::Busy,
-                    _ => DebugRegisterError::Unexpected,
-                };
+            transform: |write, capture| {
+                let capture = TapInstruction::Nar.capture_to_u8(&capture);
+                let nar = write.data[0] >> 1;
+                let write = write.data[0] & 1 == 1;
 
                 // eww...?
-                Err(ProbeRsError::Xtensa(XtensaError::XdmError(Error::Xdm(
-                    error,
-                ))))
+                Err(ProbeRsError::Xtensa(XtensaError::XdmError(Error::Xdm {
+                    narsel: nar,
+                    access: if write { "writing" } else { "reading" },
+                    source: match capture & 0b00000011 {
+                        0 => return Ok(CommandResult::None),
+                        1 => DebugRegisterError::Error,
+                        2 => DebugRegisterError::Busy,
+                        _ => DebugRegisterError::Unexpected(capture),
+                    },
+                })))
             },
         });
 
@@ -403,7 +411,7 @@ impl Xdm {
         self.execute()
     }
 
-    fn status(&mut self) -> Result<DebugStatus, XtensaError> {
+    pub(super) fn status(&mut self) -> Result<DebugStatus, XtensaError> {
         self.read_nexus_register::<DebugStatus>()
     }
 
@@ -442,11 +450,6 @@ impl Xdm {
         });
     }
 
-    pub(super) fn is_in_ocd_mode(&mut self) -> Result<bool, XtensaError> {
-        let reg = self.read_nexus_register::<DebugControlSet>()?;
-        Ok(reg.0.enable_ocd())
-    }
-
     pub(super) fn leave_ocd_mode(&mut self) -> Result<(), XtensaError> {
         // clear all clearable status bits
         self.write_nexus_register({
@@ -481,10 +484,6 @@ impl Xdm {
         Ok(())
     }
 
-    pub(super) fn is_halted(&mut self) -> Result<bool, XtensaError> {
-        self.status().map(|status| status.stopped())
-    }
-
     pub(super) fn resume(&mut self) -> Result<(), XtensaError> {
         tracing::debug!("resuming...");
         // Clear pending interrupts first that would re-enter us into the Stopped state
@@ -567,7 +566,7 @@ impl Xdm {
         }
     }
 
-    pub fn target_reset_assert(&mut self) -> Result<(), XtensaError> {
+    pub fn reset_and_halt(&mut self) -> Result<(), XtensaError> {
         self.pwr_write(PowerDevice::PowerControl, {
             let mut pwr_control = PowerControl(0);
 
@@ -579,14 +578,7 @@ impl Xdm {
 
             pwr_control.0
         })?;
-
-        Ok(())
-    }
-
-    pub fn target_reset_deassert(&mut self) -> Result<(), XtensaError> {
-        if self.halt_on_reset {
-            self.halt()?;
-        }
+        self.halt()?;
 
         self.pwr_write(PowerDevice::PowerControl, {
             let mut pwr_control = PowerControl(0);
@@ -602,28 +594,33 @@ impl Xdm {
         Ok(())
     }
 
-    pub(crate) fn halt_on_reset(&mut self, en: bool) {
-        self.halt_on_reset = en;
-    }
-
     pub(super) fn free(self) -> Box<dyn JTAGAccess> {
         self.probe
     }
 }
 
-type TransformFn = fn(Vec<u8>) -> Result<CommandResult, ProbeRsError>;
+type TransformFn = fn(&JtagWriteCommand, Vec<u8>) -> Result<CommandResult, ProbeRsError>;
 
-fn transform_u32(capture: Vec<u8>) -> Result<CommandResult, ProbeRsError> {
+fn transform_u32(
+    _command: &JtagWriteCommand,
+    capture: Vec<u8>,
+) -> Result<CommandResult, ProbeRsError> {
     Ok(CommandResult::U32(
         TapInstruction::Ndr.capture_to_u32(&capture),
     ))
 }
 
-fn transform_noop(_capture: Vec<u8>) -> Result<CommandResult, ProbeRsError> {
+fn transform_noop(
+    _command: &JtagWriteCommand,
+    _capture: Vec<u8>,
+) -> Result<CommandResult, ProbeRsError> {
     Ok(CommandResult::None)
 }
 
-fn transform_instruction_status(capture: Vec<u8>) -> Result<CommandResult, ProbeRsError> {
+fn transform_instruction_status(
+    _command: &JtagWriteCommand,
+    capture: Vec<u8>,
+) -> Result<CommandResult, ProbeRsError> {
     let status = DebugStatus(TapInstruction::Ndr.capture_to_u32(&capture));
 
     if status.exec_overrun() {
@@ -645,14 +642,6 @@ fn transform_instruction_status(capture: Vec<u8>) -> Result<CommandResult, Probe
     Ok(CommandResult::None)
 }
 
-// TODO: I don't think these should be transformed into XtensaError directly. We might want to
-// attach register-specific messages via an in-between type.
-impl From<DebugRegisterError> for XtensaError {
-    fn from(e: DebugRegisterError) -> Self {
-        XtensaError::XdmError(e.into())
-    }
-}
-
 bitfield::bitfield! {
     #[derive(Copy, Clone)]
     pub struct PowerControl(u8);
diff --git a/probe-rs/src/bin/probe-rs/cmd/cargo_embed/config/default.toml b/probe-rs/src/bin/probe-rs/cmd/cargo_embed/config/default.toml
index 95387336..adab2ab0 100644
--- a/probe-rs/src/bin/probe-rs/cmd/cargo_embed/config/default.toml
+++ b/probe-rs/src/bin/probe-rs/cmd/cargo_embed/config/default.toml
@@ -47,10 +47,6 @@ connect_under_reset = false
 enabled = false
 # The duration in ms for which the logger should retry to attach to RTT.
 timeout = 3000
-# Whether timestamps in the RTTUI are enabled
-show_timestamps = true
-# Whether locations for defmt messages are enabled in the RTTUI
-show_location = false
 # Whether to save rtt history buffer on exit.
 log_enabled = false
 # Where to save rtt history buffer relative to manifest path.
diff --git a/probe-rs/src/bin/probe-rs/cmd/cargo_embed/config/mod.rs b/probe-rs/src/bin/probe-rs/cmd/cargo_embed/config/mod.rs
index 70b08d10..f1d41bdb 100644
--- a/probe-rs/src/bin/probe-rs/cmd/cargo_embed/config/mod.rs
+++ b/probe-rs/src/bin/probe-rs/cmd/cargo_embed/config/mod.rs
@@ -77,12 +77,13 @@ pub struct General {
 }
 
 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+// Note: default values are defined in `RttChannelConfig`.
 pub struct UpChannelConfig {
     pub channel: usize,
     #[serde(default)]
     pub mode: Option<ChannelMode>,
     #[serde(default)]
-    pub format: DataFormat,
+    pub format: Option<DataFormat>,
     #[serde(default)]
     pub show_location: Option<bool>,
     #[serde(default)]
@@ -119,10 +120,6 @@ pub struct Rtt {
     /// Connection timeout in ms.
     #[serde(with = "duration_ms")]
     pub timeout: Duration,
-    /// Whether to show timestamps in RTTUI
-    pub show_timestamps: bool,
-    /// Whether to show location info in RTTUI for defmt channels.
-    pub show_location: bool,
     /// Whether to save rtt history buffer on exit to file named history.txt
     pub log_enabled: bool,
     /// Where to save rtt history buffer relative to manifest path.
diff --git a/probe-rs/src/bin/probe-rs/cmd/cargo_embed/mod.rs b/probe-rs/src/bin/probe-rs/cmd/cargo_embed/mod.rs
index 05dcb745..c6f2a16e 100644
--- a/probe-rs/src/bin/probe-rs/cmd/cargo_embed/mod.rs
+++ b/probe-rs/src/bin/probe-rs/cmd/cargo_embed/mod.rs
@@ -340,24 +340,33 @@ fn run_rttui_app(
         channels: vec![],
     };
 
+    // Make sure our defaults are the same as the ones intended in the config struct.
+    let default_channel_config = RttChannelConfig::default();
+
     let mut require_defmt = false;
     for channel_config in config.rtt.up_channels.iter() {
-        rtt_config.channels.push(RttChannelConfig {
+        // Where `channel_config` is unspecified, apply default from `default_channel_config`.
+        let rtt_channel_config = RttChannelConfig {
             channel_number: Some(channel_config.channel),
-            channel_name: None,
-            data_format: channel_config.format,
+            data_format: channel_config
+                .format
+                .unwrap_or(default_channel_config.data_format),
             show_timestamps: channel_config
                 .show_timestamps
-                .unwrap_or(config.rtt.show_timestamps),
+                .unwrap_or(default_channel_config.show_timestamps),
             show_location: channel_config
                 .show_location
-                .unwrap_or(config.rtt.show_location),
-            defmt_log_format: channel_config.defmt_log_format.clone(),
-            mode: channel_config.mode,
-        });
-        if channel_config.format == DataFormat::Defmt {
+                .unwrap_or(default_channel_config.show_location),
+            defmt_log_format: channel_config
+                .defmt_log_format
+                .clone()
+                .or_else(|| default_channel_config.defmt_log_format.clone()),
+            mode: channel_config.mode.or(default_channel_config.mode),
+        };
+        if rtt_channel_config.data_format == DataFormat::Defmt {
             require_defmt = true;
         }
+        rtt_config.channels.push(rtt_channel_config);
     }
     // In case we have down channels without up channels, add them separately.
     for channel_config in config.rtt.down_channels.iter() {
@@ -369,7 +378,6 @@ fn run_rttui_app(
             // Set up channel defaults, we don't read from it anyway.
             rtt_config.channels.push(RttChannelConfig {
                 channel_number: Some(channel_config.channel),
-                channel_name: None,
                 data_format: DataFormat::String,
                 show_timestamps: false,
                 show_location: false,
diff --git a/probe-rs/src/bin/probe-rs/cmd/cargo_embed/rttui/tab.rs b/probe-rs/src/bin/probe-rs/cmd/cargo_embed/rttui/tab.rs
index 3fb963ca..6f7ca03a 100644
--- a/probe-rs/src/bin/probe-rs/cmd/cargo_embed/rttui/tab.rs
+++ b/probe-rs/src/bin/probe-rs/cmd/cargo_embed/rttui/tab.rs
@@ -172,7 +172,9 @@ impl Tab {
             ChannelData::Binary { data } => {
                 let mut string = self.messages.pop().unwrap_or_default();
 
-                string.reserve(data.len() * 5 - 1);
+                if !data.is_empty() {
+                    string.reserve(data.len() * 5 - 1);
+                }
 
                 let string =
                     data.iter()
diff --git a/probe-rs/src/bin/probe-rs/cmd/cargo_embed/rttui/tcp.rs b/probe-rs/src/bin/probe-rs/cmd/cargo_embed/rttui/tcp.rs
index 252b7b47..7e383a7c 100644
--- a/probe-rs/src/bin/probe-rs/cmd/cargo_embed/rttui/tcp.rs
+++ b/probe-rs/src/bin/probe-rs/cmd/cargo_embed/rttui/tcp.rs
@@ -24,7 +24,7 @@ impl TcpPublisher {
         }
 
         if let Some(socket) = self.socket.as_mut() {
-            if socket.write(bytes).is_err() {
+            if socket.write_all(bytes).is_err() {
                 // Discard socket on error. Try reconnect next time.
                 self.socket = None;
             }
diff --git a/probe-rs/src/bin/probe-rs/cmd/debug.rs b/probe-rs/src/bin/probe-rs/cmd/debug.rs
index 3cff1104..69b93c19 100644
--- a/probe-rs/src/bin/probe-rs/cmd/debug.rs
+++ b/probe-rs/src/bin/probe-rs/cmd/debug.rs
@@ -16,6 +16,7 @@ use probe_rs::flashing::FileDownloadError;
 use probe_rs::probe::list::Lister;
 use probe_rs::probe::DebugProbeError;
 use probe_rs::CoreDumpError;
+use probe_rs::CoreInterface;
 use probe_rs::{
     debug::{debug_info::DebugInfo, registers::DebugRegisters, stack_frame::StackFrame},
     Core, CoreType, InstructionSet, MemoryInterface, RegisterValue,
@@ -543,6 +544,22 @@ impl DebugCli {
             },
         });
 
+        cli.add_command(Command {
+            name: "list_break",
+            help_text: "List all set breakpoints",
+            function: |cli_data, _| {
+                cli_data
+                    .core
+                    .hw_breakpoints()?
+                    .into_iter()
+                    .enumerate()
+                    .flat_map(|(idx, bpt)| bpt.map(|bpt| (idx, bpt)))
+                    .for_each(|(idx, bpt)| println!("Breakpoint {idx} - {bpt:#010X}"));
+
+                Ok(CliState::Continue)
+            },
+        });
+
         cli.add_command(Command {
             name: "bt",
             help_text: "Show backtrace",
diff --git a/probe-rs/src/bin/probe-rs/cmd/run/mod.rs b/probe-rs/src/bin/probe-rs/cmd/run/mod.rs
index 39498fa7..87d41773 100644
--- a/probe-rs/src/bin/probe-rs/cmd/run/mod.rs
+++ b/probe-rs/src/bin/probe-rs/cmd/run/mod.rs
@@ -239,7 +239,9 @@ impl RunLoop {
         F: FnMut(HaltReason, &mut Core) -> Result<Option<R>>,
     {
         if catch_hardfault || catch_reset {
-            core.halt(Duration::from_millis(100))?;
+            if !core.core_halted()? {
+                core.halt(Duration::from_millis(100))?;
+            }
 
             if catch_hardfault {
                 match core.enable_vector_catch(VectorCatchCondition::HardFault) {
diff --git a/probe-rs/src/bin/probe-rs/util/rtt.rs b/probe-rs/src/bin/probe-rs/util/rtt.rs
index 4f1629c4..8954b553 100644
--- a/probe-rs/src/bin/probe-rs/util/rtt.rs
+++ b/probe-rs/src/bin/probe-rs/util/rtt.rs
@@ -77,9 +77,8 @@ fn attach_to_rtt(
     }
 }
 
-/// Used by serde to provide defaults for `RttChannelConfig::show_location`
-fn default_include_location() -> bool {
-    // Setting this to true to allow compatibility with behaviour prior to when this option was introduced.
+/// Used by serde to provide defaults for `RttChannelConfig::show_timestamps`
+fn default_show_timestamps() -> bool {
     true
 }
 
@@ -123,7 +122,6 @@ impl RttConfig {
 #[serde(rename_all = "camelCase")]
 pub struct RttChannelConfig {
     pub channel_number: Option<usize>,
-    pub channel_name: Option<String>,
     #[serde(default)]
     pub data_format: DataFormat,
 
@@ -131,13 +129,12 @@ pub struct RttChannelConfig {
     #[serde(default)]
     pub mode: Option<ChannelMode>,
 
-    #[serde(default)]
+    #[serde(default = "default_show_timestamps")]
     /// Control the inclusion of timestamps for DataFormat::String.
     pub show_timestamps: bool,
 
-    #[serde(default = "default_include_location")]
+    #[serde(default)]
     /// Control the inclusion of source location information for DataFormat::Defmt.
-    // TODO: third option: if available
     pub show_location: bool,
 
     #[serde(default)]
@@ -400,7 +397,6 @@ impl RttActiveUpChannel {
 
         let channel_name = up_channel
             .name()
-            .or(channel_config.channel_name.as_deref())
             .map(ToString::to_string)
             .unwrap_or_else(|| {
                 format!(
@@ -481,10 +477,9 @@ pub struct RttActiveDownChannel {
 }
 
 impl RttActiveDownChannel {
-    pub fn new(down_channel: DownChannel, channel_config: &RttChannelConfig) -> Self {
+    pub fn new(down_channel: DownChannel) -> Self {
         let channel_name = down_channel
             .name()
-            .or(channel_config.channel_name.as_deref())
             .map(ToString::to_string)
             .unwrap_or_else(|| format!("Unnamed RTT down channel - {}", down_channel.number()));
 
@@ -580,14 +575,7 @@ impl RttActiveTarget {
         }
 
         for channel in rtt.down_channels.into_iter() {
-            let channel_config = rtt_config
-                .channel_config(channel.number())
-                .cloned()
-                .unwrap_or_default();
-            active_down_channels.insert(
-                channel.number(),
-                RttActiveDownChannel::new(channel, &channel_config),
-            );
+            active_down_channels.insert(channel.number(), RttActiveDownChannel::new(channel));
         }
 
         // It doesn't make sense to pretend RTT is active, if there are no active up channels
diff --git a/probe-rs/src/config/sequences/atsam.rs b/probe-rs/src/config/sequences/atsam.rs
index 43f21186..120f5998 100644
--- a/probe-rs/src/config/sequences/atsam.rs
+++ b/probe-rs/src/config/sequences/atsam.rs
@@ -5,17 +5,23 @@ use crate::{
         self,
         arm::{
             ap::MemoryAp,
+            armv7m::Dhcsr,
+            communication_interface::{DapProbe, SwdSequence},
             memory::adi_v5_memory_interface::ArmProbe,
             sequences::{ArmDebugSequence, ArmDebugSequenceError, DebugEraseSequence},
-            ApAddress, ArmError, ArmProbeInterface, DpAddress,
+            ApAddress, ArmError, ArmProbeInterface, DpAddress, Pins,
         },
     },
     probe::DebugProbeError,
     session::MissingPermissions,
-    Permissions,
+    MemoryMappedRegister, Permissions,
 };
 use bitfield::bitfield;
-use std::sync::Arc;
+use probe_rs_target::CoreType;
+use std::{
+    sync::Arc,
+    time::{Duration, Instant},
+};
 
 use anyhow::Result;
 
@@ -187,17 +193,15 @@ impl DsuStatusB {
 }
 
 /// A wrapper for different types that can perform SWD Commands (SWJ_Pins SWJ_Sequence)
-struct SwdSequenceShim<'a>(&'a mut dyn architecture::arm::communication_interface::DapProbe);
+struct SwdSequenceShim<'a>(&'a mut dyn DapProbe);
 
-impl<'a> From<&'a mut dyn architecture::arm::communication_interface::DapProbe>
-    for SwdSequenceShim<'a>
-{
-    fn from(p: &'a mut dyn architecture::arm::communication_interface::DapProbe) -> Self {
+impl<'a> From<&'a mut dyn DapProbe> for SwdSequenceShim<'a> {
+    fn from(p: &'a mut dyn DapProbe) -> Self {
         Self(p)
     }
 }
 
-impl<'a> architecture::arm::communication_interface::SwdSequence for SwdSequenceShim<'a> {
+impl<'a> SwdSequence for SwdSequenceShim<'a> {
     fn swj_sequence(&mut self, bit_len: u8, bits: u64) -> Result<(), DebugProbeError> {
         self.0.swj_sequence(bit_len, bits)
     }
@@ -262,8 +266,8 @@ impl AtSAM {
         tracing::info!("Chip-Erase started...");
 
         // Wait for it to finish
-        let start = std::time::Instant::now();
-        while start.elapsed() < std::time::Duration::from_secs(8) {
+        let start = Instant::now();
+        while start.elapsed() < Duration::from_secs(8) {
             let current_dsu_statusa = DsuStatusA::from(memory.read_word_8(DsuStatusA::ADDRESS)?);
             if current_dsu_statusa.done() {
                 tracing::info!("Chip-Erase complete");
@@ -284,7 +288,7 @@ impl AtSAM {
             } else if current_dsu_statusa.fail() {
                 return Err(ArmError::ChipEraseFailed);
             }
-            std::thread::sleep(std::time::Duration::from_millis(250));
+            std::thread::sleep(Duration::from_millis(250));
         }
 
         tracing::error!("Chip-Erase failed to complete within 8 seconds");
@@ -302,24 +306,23 @@ impl AtSAM {
     /// Subject to probe communication errors
     pub fn reset_hardware_with_extension(
         &self,
-        interface: &mut dyn architecture::arm::communication_interface::SwdSequence,
+        interface: &mut dyn SwdSequence,
     ) -> Result<(), ArmError> {
-        let mut pins = architecture::arm::Pins(0);
+        let mut pins = Pins(0);
         pins.set_nreset(true);
         pins.set_swdio_tms(true);
         pins.set_swclk_tck(true);
 
-        let mut pin_values = architecture::arm::Pins(0);
-
         // First set nReset, SWDIO and SWCLK to low.
-        let _ = interface.swj_pins(pin_values.0 as u32, pins.0 as u32, 0)?;
-        std::thread::sleep(std::time::Duration::from_millis(10));
+        let mut pin_values = Pins(0);
+        interface.swj_pins(pin_values.0 as u32, pins.0 as u32, 0)?;
+        std::thread::sleep(Duration::from_millis(10));
 
         // Next release nReset, but keep SWDIO and SWCLK low. This should put the device into
-        // reset extension ()
+        // reset extension.
         pin_values.set_nreset(true);
-        let _ = interface.swj_pins(pin_values.0 as u32, pins.0 as u32, 0)?;
-        std::thread::sleep(std::time::Duration::from_millis(20));
+        interface.swj_pins(pin_values.0 as u32, pins.0 as u32, 0)?;
+        std::thread::sleep(Duration::from_millis(20));
 
         Ok(())
     }
@@ -331,13 +334,19 @@ impl AtSAM {
     /// # Errors
     /// Subject to probe communication errors
     pub fn release_reset_extension(&self, memory: &mut dyn ArmProbe) -> Result<(), ArmError> {
+        // Enable debug mode if it is not already enabled
+        let mut dhcsr = Dhcsr(0);
+        dhcsr.enable_write();
+        dhcsr.set_c_debugen(true);
+        memory.write_word_32(Dhcsr::ADDRESS_OFFSET, dhcsr.0)?;
+
         // clear the reset extension bit
         let mut dsu_statusa = DsuStatusA(0);
         dsu_statusa.set_crstext(true);
         memory.write_8(DsuStatusA::ADDRESS, &[dsu_statusa.0])?;
 
-        let start = std::time::Instant::now();
-        while start.elapsed() < std::time::Duration::from_millis(100) {
+        let start = Instant::now();
+        while start.elapsed() < Duration::from_millis(100) {
             let current_dsu_statusa = DsuStatusA::from(memory.read_word_8(DsuStatusA::ADDRESS)?);
             if !current_dsu_statusa.crstext() {
                 return Ok(());
@@ -351,25 +360,21 @@ impl AtSAM {
     ///
     /// # Errors
     /// Subject to probe communication errors
-    pub fn reset_hardware(
-        &self,
-        interface: &mut dyn architecture::arm::communication_interface::SwdSequence,
-    ) -> Result<(), ArmError> {
-        let mut pins = architecture::arm::Pins(0);
+    pub fn reset_hardware(&self, interface: &mut dyn SwdSequence) -> Result<(), ArmError> {
+        let mut pins = Pins(0);
         pins.set_nreset(true);
         pins.set_swdio_tms(true);
         pins.set_swclk_tck(true);
 
         let mut pin_values = pins;
-        pin_values.set_nreset(false);
 
-        let _ = interface.swj_pins(pin_values.0 as u32, pins.0 as u32, 0)?;
-        std::thread::sleep(std::time::Duration::from_millis(10));
+        pin_values.set_nreset(false);
+        interface.swj_pins(pin_values.0 as u32, pins.0 as u32, 0)?;
+        std::thread::sleep(Duration::from_millis(10));
 
         pin_values.set_nreset(true);
-
-        let _ = interface.swj_pins(pin_values.0 as u32, pins.0 as u32, 0)?;
-        std::thread::sleep(std::time::Duration::from_millis(10));
+        interface.swj_pins(pin_values.0 as u32, pins.0 as u32, 0)?;
+        std::thread::sleep(Duration::from_millis(10));
 
         Ok(())
     }
@@ -379,7 +384,7 @@ impl AtSAM {
     fn ensure_target_reset(
         &self,
         result: Result<(), ArmError>,
-        interface: &mut dyn architecture::arm::communication_interface::SwdSequence,
+        interface: &mut dyn SwdSequence,
     ) -> Result<(), ArmError> {
         // Fall back if the probe's swj_pins does not support the full set of pins
         match result {
@@ -388,19 +393,14 @@ impl AtSAM {
             })) => {
                 tracing::warn!("Using fallback reset method");
 
-                let mut pins = architecture::arm::Pins(0);
+                let mut pins = Pins(0);
                 pins.set_nreset(true);
 
-                let mut pin_values = pins;
-                pin_values.set_nreset(false);
-
-                let _ = interface.swj_pins(pin_values.0 as u32, pins.0 as u32, 0)?;
-                std::thread::sleep(std::time::Duration::from_millis(10));
+                interface.swj_pins(0, pins.0 as u32, 0)?;
+                std::thread::sleep(Duration::from_millis(10));
 
-                pin_values.set_nreset(true);
-
-                let _ = interface.swj_pins(pin_values.0 as u32, pins.0 as u32, 0)?;
-                std::thread::sleep(std::time::Duration::from_millis(10));
+                interface.swj_pins(pins.0 as u32, pins.0 as u32, 0)?;
+                std::thread::sleep(Duration::from_millis(10));
 
                 Ok(())
             }
@@ -410,14 +410,24 @@ impl AtSAM {
 }
 
 impl ArmDebugSequence for AtSAM {
+    fn debug_core_start(
+        &self,
+        interface: &mut dyn ArmProbeInterface,
+        core_ap: MemoryAp,
+        _core_type: CoreType,
+        _debug_base: Option<u64>,
+        _cti_base: Option<u64>,
+    ) -> Result<(), ArmError> {
+        let mut core = interface.memory_interface(core_ap)?;
+
+        self.release_reset_extension(&mut *core)
+    }
+
     /// `reset_hardware_assert` for ATSAM devices
     ///
     /// Instead of keeping `nReset` asserted, the device is instead put into CPU Reset Extension
     /// which will keep the CPU Core in reset until manually released by the debugger probe.
-    fn reset_hardware_assert(
-        &self,
-        interface: &mut dyn architecture::arm::communication_interface::DapProbe,
-    ) -> Result<(), ArmError> {
+    fn reset_hardware_assert(&self, interface: &mut dyn DapProbe) -> Result<(), ArmError> {
         let mut shim = SwdSequenceShim::from(interface);
         let result = self.reset_hardware_with_extension(&mut shim);
 
@@ -429,11 +439,10 @@ impl ArmDebugSequence for AtSAM {
     /// Instead of de-asserting `nReset` here (this was already done during the CPU Reset Extension process),
     /// the device is released from Reset Extension.
     fn reset_hardware_deassert(&self, memory: &mut dyn ArmProbe) -> Result<(), ArmError> {
-        let mut pins = architecture::arm::Pins(0);
+        let mut pins = Pins(0);
         pins.set_nreset(true);
 
-        let current_pins =
-            architecture::arm::Pins(memory.swj_pins(pins.0 as u32, pins.0 as u32, 0)? as u8);
+        let current_pins = Pins(memory.swj_pins(pins.0 as u32, pins.0 as u32, 0)? as u8);
         if !current_pins.nreset() {
             return Err(ArmDebugSequenceError::SequenceSpecific(
                 "Expected nReset to already be de-asserted".into(),
diff --git a/probe-rs/src/config/sequences/esp.rs b/probe-rs/src/config/sequences/esp.rs
index 733cec59..fc3cf186 100644
--- a/probe-rs/src/config/sequences/esp.rs
+++ b/probe-rs/src/config/sequences/esp.rs
@@ -8,6 +8,7 @@ use crate::{
         xtensa::{
             arch::{instruction::Instruction, CpuRegister, Register},
             communication_interface::XtensaCommunicationInterface,
+            sequences::XtensaDebugSequence,
         },
     },
     MemoryInterface,
@@ -40,6 +41,7 @@ impl EspFlashSizeDetector {
     pub fn detect_flash_size_esp32(
         &self,
         interface: &mut XtensaCommunicationInterface,
+        sequence: &impl XtensaDebugSequence,
     ) -> Result<Option<usize>, crate::Error> {
         tracing::info!("Detecting flash size");
         attach_flash_xtensa(
@@ -47,6 +49,7 @@ impl EspFlashSizeDetector {
             self.stack_pointer,
             self.load_address,
             self.attach_fn,
+            sequence,
         )?;
         detect_flash_size_esp32(interface, self.spiflash_peripheral)
     }
@@ -54,6 +57,7 @@ impl EspFlashSizeDetector {
     pub fn detect_flash_size_xtensa(
         &self,
         interface: &mut XtensaCommunicationInterface,
+        sequence: &impl XtensaDebugSequence,
     ) -> Result<Option<usize>, crate::Error> {
         tracing::info!("Detecting flash size");
         attach_flash_xtensa(
@@ -61,6 +65,7 @@ impl EspFlashSizeDetector {
             self.stack_pointer,
             self.load_address,
             self.attach_fn,
+            sequence,
         )?;
         tracing::info!("Flash attached");
         detect_flash_size(interface, self.spiflash_peripheral)
@@ -120,9 +125,10 @@ fn attach_flash_xtensa(
     stack_pointer: u32,
     load_addr: u32,
     attach_fn: u32,
+    sequence: &impl XtensaDebugSequence,
 ) -> Result<(), crate::Error> {
     // We're very intrusive here but the flashing process should reset the MCU again anyway
-    interface.reset_and_halt(Duration::from_millis(500))?;
+    sequence.reset_system_and_halt(interface, Duration::from_millis(500))?;
 
     let mut instructions = vec![];
     Instruction::CallX8(CpuRegister::A4).encode_into_vec(&mut instructions);
diff --git a/probe-rs/src/config/sequences/esp32.rs b/probe-rs/src/config/sequences/esp32.rs
index becee5f8..1d785b54 100644
--- a/probe-rs/src/config/sequences/esp32.rs
+++ b/probe-rs/src/config/sequences/esp32.rs
@@ -69,6 +69,6 @@ impl XtensaDebugSequence for ESP32 {
         &self,
         interface: &mut XtensaCommunicationInterface,
     ) -> Result<Option<usize>, crate::Error> {
-        self.inner.detect_flash_size_esp32(interface)
+        self.inner.detect_flash_size_esp32(interface, self)
     }
 }
diff --git a/probe-rs/src/config/sequences/esp32c2.rs b/probe-rs/src/config/sequences/esp32c2.rs
index 75484de9..ce026dc1 100644
--- a/probe-rs/src/config/sequences/esp32c2.rs
+++ b/probe-rs/src/config/sequences/esp32c2.rs
@@ -1,12 +1,13 @@
 //! Sequence for the ESP32C2.
 
-use std::sync::Arc;
+use std::{sync::Arc, time::Duration};
 
 use probe_rs_target::Chip;
 
 use crate::{
     architecture::riscv::{
         communication_interface::RiscvCommunicationInterface, sequences::RiscvDebugSequence,
+        Dmcontrol,
     },
     config::sequences::esp::EspFlashSizeDetector,
     MemoryInterface,
@@ -60,4 +61,36 @@ impl RiscvDebugSequence for ESP32C2 {
     ) -> Result<Option<usize>, crate::Error> {
         self.inner.detect_flash_size_riscv(interface)
     }
+
+    fn reset_system_and_halt(
+        &self,
+        interface: &mut RiscvCommunicationInterface,
+        timeout: Duration,
+    ) -> Result<(), crate::Error> {
+        interface.halt(timeout)?;
+
+        // Reset all peripherals except for the RTC block.
+
+        // At this point the core is reset and halted, ready for us to issue a system reset
+        // Trigger reset via RTC_CNTL_SW_SYS_RST
+        interface.write_word_32(0x6000_8000, 0x9C00_A000)?;
+
+        // Workaround for stuck in cpu start during calibration.
+        interface.write_word_32(0x6001_F068, 0)?;
+
+        // Wait for the reset to take effect.
+        std::thread::sleep(Duration::from_millis(10));
+
+        let mut dmcontrol = Dmcontrol(0);
+        dmcontrol.set_dmactive(true);
+        dmcontrol.set_ackhavereset(true);
+        interface.write_dm_register(dmcontrol)?;
+
+        interface.enter_debug_mode()?;
+        self.on_connect(interface)?;
+
+        interface.reset_hart_and_halt(timeout)?;
+
+        Ok(())
+    }
 }
diff --git a/probe-rs/src/config/sequences/esp32c3.rs b/probe-rs/src/config/sequences/esp32c3.rs
index 412e1458..b839c365 100644
--- a/probe-rs/src/config/sequences/esp32c3.rs
+++ b/probe-rs/src/config/sequences/esp32c3.rs
@@ -1,12 +1,13 @@
 //! Sequence for the ESP32C3.
 
-use std::sync::Arc;
+use std::{sync::Arc, time::Duration};
 
 use probe_rs_target::Chip;
 
 use crate::{
     architecture::riscv::{
         communication_interface::RiscvCommunicationInterface, sequences::RiscvDebugSequence,
+        Dmcontrol,
     },
     config::sequences::esp::EspFlashSizeDetector,
     MemoryInterface,
@@ -65,4 +66,36 @@ impl RiscvDebugSequence for ESP32C3 {
     ) -> Result<Option<usize>, crate::Error> {
         self.inner.detect_flash_size_riscv(interface)
     }
+
+    fn reset_system_and_halt(
+        &self,
+        interface: &mut RiscvCommunicationInterface,
+        timeout: Duration,
+    ) -> Result<(), crate::Error> {
+        interface.reset_hart_and_halt(timeout)?;
+
+        // Reset all peripherals except for the RTC block.
+
+        // At this point the core is reset and halted, ready for us to issue a system reset
+        // Trigger reset via RTC_CNTL_SW_SYS_RST
+        interface.write_word_32(0x6000_8000, 0x9C00_A000)?;
+
+        // Workaround for stuck in cpu start during calibration.
+        interface.write_word_32(0x6001_F068, 0)?;
+
+        // Wait for the reset to take effect.
+        std::thread::sleep(Duration::from_millis(10));
+
+        let mut dmcontrol = Dmcontrol(0);
+        dmcontrol.set_dmactive(true);
+        dmcontrol.set_ackhavereset(true);
+        interface.write_dm_register(dmcontrol)?;
+
+        interface.enter_debug_mode()?;
+        self.on_connect(interface)?;
+
+        interface.reset_hart_and_halt(timeout)?;
+
+        Ok(())
+    }
 }
diff --git a/probe-rs/src/config/sequences/esp32c6.rs b/probe-rs/src/config/sequences/esp32c6.rs
index 937ba42c..f9e50950 100644
--- a/probe-rs/src/config/sequences/esp32c6.rs
+++ b/probe-rs/src/config/sequences/esp32c6.rs
@@ -1,12 +1,14 @@
 //! Sequences for the ESP32C6.
 
-use std::sync::Arc;
+use std::{sync::Arc, time::Duration};
 
 use probe_rs_target::Chip;
 
 use crate::{
     architecture::riscv::{
-        communication_interface::RiscvCommunicationInterface, sequences::RiscvDebugSequence,
+        communication_interface::{RiscvCommunicationInterface, Sbaddress0, Sbcs, Sbdata0},
+        sequences::RiscvDebugSequence,
+        Dmcontrol,
     },
     config::sequences::esp::EspFlashSizeDetector,
     MemoryInterface,
@@ -65,4 +67,46 @@ impl RiscvDebugSequence for ESP32C6 {
     ) -> Result<Option<usize>, crate::Error> {
         self.inner.detect_flash_size_riscv(interface)
     }
+
+    fn reset_system_and_halt(
+        &self,
+        interface: &mut RiscvCommunicationInterface,
+        timeout: Duration,
+    ) -> Result<(), crate::Error> {
+        interface.halt(timeout)?;
+
+        // System reset, ported from OpenOCD.
+        interface.write_dm_register(Sbcs(0x48000))?;
+        interface.write_dm_register(Sbaddress0(0x600b1034))?;
+        interface.write_dm_register(Sbdata0(0x80000000_u32))?;
+
+        // clear dmactive to clear sbbusy otherwise debug module gets stuck
+        interface.write_dm_register(Dmcontrol(0))?;
+
+        interface.write_dm_register(Sbcs(0x48000))?;
+        interface.write_dm_register(Sbaddress0(0x600b1038))?;
+        interface.write_dm_register(Sbdata0(0x10000000_u32))?;
+
+        // clear dmactive to clear sbbusy otherwise debug module gets stuck
+        interface.write_dm_register(Dmcontrol(0))?;
+
+        let mut dmcontrol = Dmcontrol(0);
+        dmcontrol.set_dmactive(true);
+        dmcontrol.set_resumereq(true);
+        interface.write_dm_register(dmcontrol)?;
+
+        std::thread::sleep(Duration::from_millis(10));
+
+        let mut dmcontrol = Dmcontrol(0);
+        dmcontrol.set_dmactive(true);
+        dmcontrol.set_ackhavereset(true);
+        interface.write_dm_register(dmcontrol)?;
+
+        interface.enter_debug_mode()?;
+        self.on_connect(interface)?;
+
+        interface.reset_hart_and_halt(timeout)?;
+
+        Ok(())
+    }
 }
diff --git a/probe-rs/src/config/sequences/esp32h2.rs b/probe-rs/src/config/sequences/esp32h2.rs
index 232c9780..66c3fcf5 100644
--- a/probe-rs/src/config/sequences/esp32h2.rs
+++ b/probe-rs/src/config/sequences/esp32h2.rs
@@ -1,12 +1,14 @@
 //! Sequences for the ESP32H2.
 
-use std::sync::Arc;
+use std::{sync::Arc, time::Duration};
 
 use probe_rs_target::Chip;
 
 use crate::{
     architecture::riscv::{
-        communication_interface::RiscvCommunicationInterface, sequences::RiscvDebugSequence,
+        communication_interface::{RiscvCommunicationInterface, Sbaddress0, Sbcs, Sbdata0},
+        sequences::RiscvDebugSequence,
+        Dmcontrol,
     },
     config::sequences::esp::EspFlashSizeDetector,
     MemoryInterface,
@@ -65,4 +67,46 @@ impl RiscvDebugSequence for ESP32H2 {
     ) -> Result<Option<usize>, crate::Error> {
         self.inner.detect_flash_size_riscv(interface)
     }
+
+    fn reset_system_and_halt(
+        &self,
+        interface: &mut RiscvCommunicationInterface,
+        timeout: Duration,
+    ) -> Result<(), crate::Error> {
+        interface.halt(timeout)?;
+
+        // System reset, ported from OpenOCD.
+        interface.write_dm_register(Sbcs(0x48000))?;
+        interface.write_dm_register(Sbaddress0(0x600b1034))?;
+        interface.write_dm_register(Sbdata0(0x80000000_u32))?;
+
+        // clear dmactive to clear sbbusy otherwise debug module gets stuck
+        interface.write_dm_register(Dmcontrol(0))?;
+
+        interface.write_dm_register(Sbcs(0x48000))?;
+        interface.write_dm_register(Sbaddress0(0x600b1038))?;
+        interface.write_dm_register(Sbdata0(0x10000000_u32))?;
+
+        // clear dmactive to clear sbbusy otherwise debug module gets stuck
+        interface.write_dm_register(Dmcontrol(0))?;
+
+        let mut dmcontrol = Dmcontrol(0);
+        dmcontrol.set_dmactive(true);
+        dmcontrol.set_resumereq(true);
+        interface.write_dm_register(dmcontrol)?;
+
+        std::thread::sleep(Duration::from_millis(10));
+
+        let mut dmcontrol = Dmcontrol(0);
+        dmcontrol.set_dmactive(true);
+        dmcontrol.set_ackhavereset(true);
+        interface.write_dm_register(dmcontrol)?;
+
+        interface.enter_debug_mode()?;
+        self.on_connect(interface)?;
+
+        interface.reset_hart_and_halt(timeout)?;
+
+        Ok(())
+    }
 }
diff --git a/probe-rs/src/config/sequences/esp32s2.rs b/probe-rs/src/config/sequences/esp32s2.rs
index 043e0a77..a8e6b40c 100644
--- a/probe-rs/src/config/sequences/esp32s2.rs
+++ b/probe-rs/src/config/sequences/esp32s2.rs
@@ -67,6 +67,6 @@ impl XtensaDebugSequence for ESP32S2 {
         &self,
         interface: &mut XtensaCommunicationInterface,
     ) -> Result<Option<usize>, crate::Error> {
-        self.inner.detect_flash_size_xtensa(interface)
+        self.inner.detect_flash_size_xtensa(interface, self)
     }
 }
diff --git a/probe-rs/src/config/sequences/esp32s3.rs b/probe-rs/src/config/sequences/esp32s3.rs
index 9c18791b..61b01ed0 100644
--- a/probe-rs/src/config/sequences/esp32s3.rs
+++ b/probe-rs/src/config/sequences/esp32s3.rs
@@ -67,6 +67,6 @@ impl XtensaDebugSequence for ESP32S3 {
         &self,
         interface: &mut XtensaCommunicationInterface,
     ) -> Result<Option<usize>, crate::Error> {
-        self.inner.detect_flash_size_xtensa(interface)
+        self.inner.detect_flash_size_xtensa(interface, self)
     }
 }
diff --git a/probe-rs/src/core.rs b/probe-rs/src/core.rs
index cb67bbc3..19b5d339 100644
--- a/probe-rs/src/core.rs
+++ b/probe-rs/src/core.rs
@@ -1,6 +1,12 @@
 use crate::{
-    architecture::arm::sequences::ArmDebugSequence, config::DebugSequence, debug::DebugRegisters,
-    error::Error, CoreType, InstructionSet, MemoryInterface, Target,
+    architecture::{
+        arm::sequences::ArmDebugSequence, riscv::sequences::RiscvDebugSequence,
+        xtensa::sequences::XtensaDebugSequence,
+    },
+    config::DebugSequence,
+    debug::DebugRegisters,
+    error::Error,
+    CoreType, InstructionSet, MemoryInterface, Target,
 };
 use anyhow::anyhow;
 pub use probe_rs_target::{Architecture, CoreAccessOptions};
@@ -415,7 +421,13 @@ impl<'probe> Core<'probe> {
                 }
             }
             CoreAccessOptions::Riscv(options) => {
-                let core_state = CoreState::new(ResolvedCoreOptions::Riscv { options });
+                let DebugSequence::Riscv(sequence) = target.debug_sequence.clone() else {
+                    panic!(
+                        "Mismatch between sequence and core kind. This is a bug, please report it."
+                    );
+                };
+
+                let core_state = CoreState::new(ResolvedCoreOptions::Riscv { sequence, options });
                 CombinedCoreState {
                     id,
                     core_state,
@@ -423,7 +435,13 @@ impl<'probe> Core<'probe> {
                 }
             }
             CoreAccessOptions::Xtensa(options) => {
-                let core_state = CoreState::new(ResolvedCoreOptions::Xtensa { options });
+                let DebugSequence::Xtensa(sequence) = target.debug_sequence.clone() else {
+                    panic!(
+                        "Mismatch between sequence and core kind. This is a bug, please report it."
+                    );
+                };
+
+                let core_state = CoreState::new(ResolvedCoreOptions::Xtensa { sequence, options });
                 CombinedCoreState {
                     id,
                     core_state,
@@ -706,6 +724,10 @@ impl<'probe> Core<'probe> {
         self.inner.floating_point_register_count()
     }
 
+    pub(crate) fn reset_catch_set(&mut self) -> Result<(), Error> {
+        self.inner.reset_catch_set()
+    }
+
     pub(crate) fn reset_catch_clear(&mut self) -> Result<(), Error> {
         self.inner.reset_catch_clear()
     }
@@ -814,7 +836,7 @@ impl<'probe> CoreInterface for Core<'probe> {
     }
 
     fn hw_breakpoints(&mut self) -> Result<Vec<Option<u64>>, Error> {
-        todo!()
+        self.inner.hw_breakpoints()
     }
 
     fn enable_breakpoints(&mut self, state: bool) -> Result<(), Error> {
@@ -874,7 +896,7 @@ impl<'probe> CoreInterface for Core<'probe> {
     }
 
     fn reset_catch_set(&mut self) -> Result<(), Error> {
-        todo!()
+        self.reset_catch_set()
     }
 
     fn reset_catch_clear(&mut self) -> Result<(), Error> {
@@ -892,9 +914,11 @@ pub enum ResolvedCoreOptions {
         options: ArmCoreAccessOptions,
     },
     Riscv {
+        sequence: Arc<dyn RiscvDebugSequence>,
         options: RiscvCoreAccessOptions,
     },
     Xtensa {
+        sequence: Arc<dyn XtensaDebugSequence>,
         options: XtensaCoreAccessOptions,
     },
 }
@@ -907,8 +931,16 @@ impl std::fmt::Debug for ResolvedCoreOptions {
                 .field("sequence", &"<ArmDebugSequence>")
                 .field("options", options)
                 .finish(),
-            Self::Riscv { options } => f.debug_struct("Riscv").field("options", options).finish(),
-            Self::Xtensa { options } => f.debug_struct("Xtensa").field("options", options).finish(),
+            Self::Riscv { options, .. } => f
+                .debug_struct("Riscv")
+                .field("sequence", &"<RiscvDebugSequence>")
+                .field("options", options)
+                .finish(),
+            Self::Xtensa { options, .. } => f
+                .debug_struct("Xtensa")
+                .field("sequence", &"<XtensaDebugSequence>")
+                .field("options", options)
+                .finish(),
         }
     }
 }
diff --git a/probe-rs/src/core/core_state.rs b/probe-rs/src/core/core_state.rs
index 2c08388b..9e31873b 100644
--- a/probe-rs/src/core/core_state.rs
+++ b/probe-rs/src/core/core_state.rs
@@ -158,12 +158,14 @@ impl CombinedCoreState {
         let memory_regions = &target.memory_map;
         let name = &target.cores[self.id].name;
 
-        let ResolvedCoreOptions::Riscv { options } = &self.core_state.core_access_options else {
+        let ResolvedCoreOptions::Riscv { options, sequence } = &self.core_state.core_access_options
+        else {
             unreachable!(
                 "The stored core state is not compatible with the RISC-V architecture. \
                 This should never happen. Please file a bug if it does."
             );
         };
+        let debug_sequence = sequence.clone();
 
         let SpecificCoreState::Riscv(s) = &mut self.specific_state else {
             unreachable!(
@@ -180,6 +182,7 @@ impl CombinedCoreState {
                 options.hart_id.unwrap_or_default(),
                 interface,
                 s,
+                debug_sequence,
             )?,
         ))
     }
@@ -192,6 +195,15 @@ impl CombinedCoreState {
         let memory_regions = &target.memory_map;
         let name = &target.cores[self.id].name;
 
+        let ResolvedCoreOptions::Xtensa { sequence, .. } = &self.core_state.core_access_options
+        else {
+            unreachable!(
+                "The stored core state is not compatible with the Xtensa architecture. \
+                This should never happen. Please file a bug if it does."
+            );
+        };
+        let debug_sequence = sequence.clone();
+
         let SpecificCoreState::Xtensa(s) = &mut self.specific_state else {
             unreachable!(
                 "The stored core state is not compatible with the Xtensa architecture. \
@@ -203,7 +215,7 @@ impl CombinedCoreState {
             self.id,
             name,
             memory_regions,
-            crate::architecture::xtensa::Xtensa::new(interface, s),
+            crate::architecture::xtensa::Xtensa::new(interface, s, debug_sequence),
         ))
     }
 
diff --git a/probe-rs/src/flashing/flash_algorithm.rs b/probe-rs/src/flashing/flash_algorithm.rs
index ef2bc269..7fbaa907 100644
--- a/probe-rs/src/flashing/flash_algorithm.rs
+++ b/probe-rs/src/flashing/flash_algorithm.rs
@@ -160,7 +160,7 @@ impl FlashAlgorithm {
         true
     }
 
-    const FLASH_ALGO_STACK_SIZE: u64 = 512;
+    const FLASH_ALGO_MIN_STACK_SIZE: u64 = 512;
 
     // Header for RISC-V Flash Algorithms
     const RISCV_FLASH_BLOB_HEADER: [u32; 2] = [riscv::assembly::EBREAK, riscv::assembly::EBREAK];
@@ -274,6 +274,33 @@ impl FlashAlgorithm {
             return Err(FlashError::InvalidFlashAlgorithmLoadAddress { address: addr_load });
         }
 
+        // Memory layout when `data_ram_region == ram_region`:
+        //
+        // Single buffering:
+        //
+        // ++-------------+-----------------------+---------------+ <- RAM region end
+        // || Loader code |         Stack         |  Data buffer  |
+        // ++-------------+-----------------------+---------------+
+        // ^- addr_load   ^- code_end             ^- data_start_addr = stack_top_addr
+        //  ^- code_start                         ^---------------^ buffer_page_size
+        //
+        // Double buffering:
+        //
+        // ++-------------+-------+---------------+---------------+ <- RAM region end
+        // || Loader code | Stack | Data buffer 1 | Data buffer 2 |
+        // ++-------------+-------+---------------+---------------+
+        // ^- addr_load           ^- data_start_addr = stack_top_addr
+        //  ^- code_start         ^---------------^---------------^ buffer_page_size (x2)
+        //                ^- code_end
+        //
+        // Configured stack with single buffer fitting:
+        //
+        // ++-------------+----+------------------+---------------+ <- RAM region end
+        // || Loader code |    |      Stack       |  Data buffer  |
+        // ++-------------+----+------------------+---------------+
+        // ^- addr_load   ^- code_end             ^- data_start_addr = stack_top_addr
+        //  ^- code_start                         ^---------------^ buffer_page_size (x1)
+
         let code_start = addr_load + header_size;
         let code_size_bytes = (instructions.len() * size_of::<u32>()) as u64;
         let code_end = code_start + code_size_bytes;
@@ -282,6 +309,8 @@ impl FlashAlgorithm {
 
         let remaining_ram = ram_region.range.end - code_end;
 
+        // How much space does a data buffer take up in the current RAM region?
+        // If the data is in a different region, we don't need to account for it for the stack.
         let buffer_page_size_in_instr_region = if ram_region == data_ram_region {
             buffer_page_size
         } else {
@@ -290,6 +319,7 @@ impl FlashAlgorithm {
 
         // Try to find a stack size that fits with at least one page of data.
         let stack_size = if let Some(configured_stack) = raw.stack_size {
+            // The user has configured a stack size. Let's use it.
             let stack_size = configured_stack as u64;
 
             // Make sure at least one data page fits into RAM.
@@ -299,50 +329,69 @@ impl FlashAlgorithm {
             }
             stack_size
         } else {
-            // Make sure at least one data page fits into RAM, and also
-            // avoid a panic if the RAM region is too small.
-            if buffer_page_size_in_instr_region >= remaining_ram {
-                // We don't have any space for a stack
+            // No stack is specified, let's try to use as much as we can.
+            let required_for_double_buffer =
+                2 * buffer_page_size_in_instr_region + Self::FLASH_ALGO_MIN_STACK_SIZE;
+            let required_for_single_buffer =
+                buffer_page_size_in_instr_region + Self::FLASH_ALGO_MIN_STACK_SIZE;
+
+            if remaining_ram >= required_for_double_buffer {
+                // We have space for two pages of data and a stack.
+                remaining_ram - 2 * buffer_page_size_in_instr_region
+            } else if remaining_ram > required_for_single_buffer {
+                // We have space for one page of data and a stack.
+                remaining_ram - buffer_page_size_in_instr_region
+            } else {
+                // We don't have enough space for a stack
                 return Err(FlashError::InvalidFlashAlgorithmStackSize);
             }
-
-            // Use up to 512 bytes of RAM out of the remaining for stack.
-            (remaining_ram - buffer_page_size_in_instr_region).min(Self::FLASH_ALGO_STACK_SIZE)
         };
+        tracing::debug!("The flash algorithm will be configured with {stack_size} bytes of stack");
 
-        let stack_top_addr = code_end + stack_size;
-
-        tracing::debug!("The flash algorithm will be configured with {stack_size} bytes of stack below {stack_top_addr:08x}");
-
+        // We have the stack size, let's lay out the data block(s)
         // Determine the bounds of the data region.
-        let data_start_addr = if let Some(data_load_addr) = raw.data_load_address {
-            // Specified, use what the user gave us
+        let data_load_addr = if let Some(data_load_addr) = raw.data_load_address {
             data_load_addr
         } else if ram_region == data_ram_region {
-            // Not specified, same region, place above stack
-            stack_top_addr
+            let remaining_ram = remaining_ram - stack_size;
+
+            let data_size = if remaining_ram >= 2 * buffer_page_size {
+                2 * buffer_page_size
+            } else {
+                buffer_page_size
+            };
+
+            ram_region.range.end - data_size
         } else {
-            // Not specified, different region, place at start of data RAM region
             data_ram_region.range.start
         };
+        let data_range = data_load_addr..data_ram_region.range.end;
 
-        let data_region_end_addr = data_ram_region.range.end;
+        // Now we can place the stack.
+        let stack_top_addr = if ram_region == data_ram_region {
+            // Under the data...
+            data_range.start
+        } else {
+            // ... or the top of the region if data is in a different region.
+            ram_region.range.end
+        };
+        tracing::debug!("Stack top: {:#010X?}", stack_top_addr);
 
         // Data buffer 1
-        let first_buffer_start = data_start_addr;
+        let first_buffer_start = data_range.start;
 
         // Data buffer 2
         let second_buffer_start = first_buffer_start + buffer_page_size;
         let second_buffer_end = second_buffer_start + buffer_page_size;
 
         // Determine whether we can use double buffering or not by the remaining RAM region size.
-        let page_buffers = if second_buffer_end <= data_region_end_addr {
+        let page_buffers = if second_buffer_end <= data_range.end {
             vec![first_buffer_start, second_buffer_start]
         } else {
             vec![first_buffer_start]
         };
 
-        tracing::debug!("Page buffers: {:08x?}", page_buffers);
+        tracing::debug!("Page buffers: {:#010X?}", page_buffers);
 
         let name = raw.name.clone();
 
diff --git a/probe-rs/src/probe.rs b/probe-rs/src/probe.rs
index c3bf1954..a31d5127 100644
--- a/probe-rs/src/probe.rs
+++ b/probe-rs/src/probe.rs
@@ -818,22 +818,20 @@ impl DebugProbeSelector {
 impl TryFrom<&str> for DebugProbeSelector {
     type Error = DebugProbeSelectorParseError;
     fn try_from(value: &str) -> Result<Self, Self::Error> {
-        let split = value.split(':').collect::<Vec<_>>();
-        let mut selector = if split.len() > 1 {
-            DebugProbeSelector {
-                vendor_id: u16::from_str_radix(split[0], 16)?,
-                product_id: u16::from_str_radix(split[1], 16)?,
-                serial_number: None,
-            }
-        } else {
-            return Err(DebugProbeSelectorParseError::Format);
-        };
-
-        if split.len() == 3 {
-            selector.serial_number = Some(split[2].to_string());
-        }
-
-        Ok(selector)
+        // Split into at most 3 parts: VID, PID, Serial.
+        // We limit the number of splits to allow for colons in the
+        // serial number (EspJtag uses MAC address)
+        let mut split = value.splitn(3, ':');
+
+        let vendor_id = split.next().unwrap(); // First split is always successful
+        let product_id = split.next().ok_or(DebugProbeSelectorParseError::Format)?;
+        let serial_number = split.next().map(|s| s.to_string());
+
+        Ok(DebugProbeSelector {
+            vendor_id: u16::from_str_radix(vendor_id, 16)?,
+            product_id: u16::from_str_radix(product_id, 16)?,
+            serial_number,
+        })
     }
 }
 
@@ -898,6 +896,9 @@ pub trait JTAGAccess: DebugProbe {
     /// will try to measure and extract `IR` lengths by driving the JTAG interface.
     fn scan_chain(&mut self) -> Result<Vec<JtagChainItem>, DebugProbeError>;
 
+    /// Executes a TAP reset.
+    fn tap_reset(&mut self) -> Result<(), DebugProbeError>;
+
     /// Read a JTAG register.
     ///
     /// This function emulates a read by performing a write with all zeros to the DR.
@@ -940,7 +941,7 @@ pub trait JTAGAccess: DebugProbe {
             match self
                 .write_register(write.address, &write.data, write.len)
                 .map_err(crate::Error::Probe)
-                .and_then(|response| (write.transform)(response))
+                .and_then(|response| (write.transform)(write, response))
             {
                 Ok(res) => results.push(idx, res),
                 Err(e) => return Err(BatchExecutionError::new(e, results)),
@@ -964,7 +965,7 @@ pub struct JtagWriteCommand {
     pub len: u32,
 
     /// A function to transform the raw response into a [`CommandResult`]
-    pub transform: fn(Vec<u8>) -> Result<CommandResult, crate::Error>,
+    pub transform: fn(&JtagWriteCommand, Vec<u8>) -> Result<CommandResult, crate::Error>,
 }
 
 /// Represents a Jtag Tap within the chain.
@@ -1248,4 +1249,16 @@ mod test {
         assert!(probe_info.is_probe_type::<ftdi::FtdiProbeFactory>());
         assert!(!probe_info.is_probe_type::<espusbjtag::EspUsbJtagFactory>());
     }
+
+    #[test]
+    fn test_parsing_many_colons() {
+        let selector: DebugProbeSelector = "303a:1001:DC:DA:0C:D3:FE:D8".try_into().unwrap();
+
+        assert_eq!(selector.vendor_id, 0x303a);
+        assert_eq!(selector.product_id, 0x1001);
+        assert_eq!(
+            selector.serial_number,
+            Some("DC:DA:0C:D3:FE:D8".to_string())
+        );
+    }
 }
diff --git a/probe-rs/src/probe/arm_debug_interface.rs b/probe-rs/src/probe/arm_debug_interface.rs
index 9d089b0c..935386f3 100644
--- a/probe-rs/src/probe/arm_debug_interface.rs
+++ b/probe-rs/src/probe/arm_debug_interface.rs
@@ -1533,6 +1533,10 @@ mod test {
             todo!()
         }
 
+        fn tap_reset(&mut self) -> Result<(), DebugProbeError> {
+            todo!()
+        }
+
         fn read_register(&mut self, _address: u32, _len: u32) -> Result<Vec<u8>, DebugProbeError> {
             todo!()
         }
diff --git a/probe-rs/src/probe/common.rs b/probe-rs/src/probe/common.rs
index f7e437ec..ce1c2234 100644
--- a/probe-rs/src/probe/common.rs
+++ b/probe-rs/src/probe/common.rs
@@ -496,7 +496,11 @@ fn shift_ir(
 
     // Check the bit length, enough data has to be available
     if data.len() * 8 < len || len == 0 {
-        return Err(DebugProbeError::Other(anyhow!("Invalid data length")));
+        return Err(DebugProbeError::Other(anyhow!(
+            "Invalid data length. IR bits: {}, expected: {}",
+            data.len(),
+            len
+        )));
     }
 
     // BYPASS commands before and after shifting out data where required
@@ -546,7 +550,11 @@ fn shift_dr(
 
     // Check the bit length, enough data has to be available
     if data.len() * 8 < register_bits || register_bits == 0 {
-        return Err(DebugProbeError::Other(anyhow!("Invalid data length")));
+        return Err(DebugProbeError::Other(anyhow!(
+            "Invalid data length. DR bits: {}, expected: {}",
+            data.len(),
+            register_bits
+        )));
     }
 
     // Last bit of data is shifted out when we exit the SHIFT-DR State
@@ -696,6 +704,10 @@ impl<Probe: DebugProbe + RawJtagIo + 'static> JTAGAccess for Probe {
             .collect())
     }
 
+    fn tap_reset(&mut self) -> Result<(), DebugProbeError> {
+        self.reset_jtag_state_machine()
+    }
+
     fn set_idle_cycles(&mut self, idle_cycles: u8) {
         self.state_mut().jtag_idle_cycles = idle_cycles as usize;
     }
@@ -747,7 +759,7 @@ impl<Probe: DebugProbe + RawJtagIo + 'static> JTAGAccess for Probe {
             )
             .map_err(|e| BatchExecutionError::new(e.into(), DeferredResultSet::new()))?;
 
-            bits.push((idx, write.transform, op));
+            bits.push((idx, write, op));
         }
 
         tracing::debug!("Sending to chip...");
@@ -760,13 +772,13 @@ impl<Probe: DebugProbe + RawJtagIo + 'static> JTAGAccess for Probe {
         let mut responses = DeferredResultSet::with_capacity(bits.len());
 
         let mut bitstream = bitstream.as_bitslice();
-        for (idx, transform, bits) in bits.into_iter() {
+        for (idx, command, bits) in bits.into_iter() {
             if idx.should_capture() {
                 // TODO: this back-and-forth between BitVec and Vec is probably unnecessary
                 let mut reg_bits = bitstream[..bits].to_bitvec();
                 reg_bits.force_align();
                 let response = reg_bits.into_vec();
-                match transform(response) {
+                match (command.transform)(command, response) {
                     Ok(response) => responses.push(idx, response),
                     Err(e) => return Err(BatchExecutionError::new(e, responses)),
                 }
diff --git a/probe-rs/src/probe/jlink/interface.rs b/probe-rs/src/probe/jlink/interface.rs
index 3d2b3462..87fafaa7 100644
--- a/probe-rs/src/probe/jlink/interface.rs
+++ b/probe-rs/src/probe/jlink/interface.rs
@@ -9,24 +9,22 @@ enum_and_set!(
     #[non_exhaustive]
     #[derive(Debug, Copy, Clone, PartialEq, Eq)]
     pub enum Interface {
-        /// JTAG interface (IEEE 1149.1). Supported by most J-Link probes (some embedded J-Links might
-        /// only support SWD).
+        /// JTAG interface (IEEE 1149.1). Supported by most J-Link probes (some embedded J-Links
+        /// might only support SWD).
         Jtag = 0,
-        /// SWD interface (Serial Wire Debug), used by most Cortex-M chips, and supported by almost all
-        /// J-Link probes.
+        /// SWD interface (Serial Wire Debug), used by most Cortex-M chips, and supported by almost
+        /// all J-Link probes.
         Swd = 1,
         /// Background Debug Mode 3, a single-wire debug interface used on some NXP microcontrollers.
         Bdm3 = 2,
         /// FINE, a two-wire debugging interface used by Renesas RX MCUs.
         ///
-        /// **Note**: due to a bug, attempting to select FINE with [`JLink::select_inferface`] will
+        /// **Note**: due to a bug, attempting to select FINE with `JLink::select_interface()` will
         /// currently hang the probe.
-        ///
-        /// [`JLink::select_inferface`]: crate::JLink::select_interface
         // FIXME: There's a curious bug that hangs the probe when selecting the FINE interface.
-        // Specifically, the probe never sends back the previous interface after it receives the `c7 03`
-        // SELECT_IF cmd, even though the normal J-Link software also just sends `c7 03` and gets back
-        // the right response.
+        // Specifically, the probe never sends back the previous interface after it receives the
+        // `c7 03` SELECT_IF cmd, even though the normal J-Link software also just sends `c7 03`
+        // and gets back the right response.
         Fine = 3,
         /// In-Circuit System Programming (ICSP) interface of PIC32 chips.
         Pic32Icsp = 4,
diff --git a/probe-rs/src/probe/wlink/mod.rs b/probe-rs/src/probe/wlink/mod.rs
index db9b2f23..cd3ed900 100644
--- a/probe-rs/src/probe/wlink/mod.rs
+++ b/probe-rs/src/probe/wlink/mod.rs
@@ -390,6 +390,10 @@ impl JTAGAccess for WchLink {
         Ok(vec![])
     }
 
+    fn tap_reset(&mut self) -> Result<(), DebugProbeError> {
+        Ok(())
+    }
+
     fn read_register(&mut self, address: u32, len: u32) -> Result<Vec<u8>, DebugProbeError> {
         tracing::debug!("read register 0x{:08x}", address);
         assert_eq!(len, 32);
diff --git a/probe-rs/src/session.rs b/probe-rs/src/session.rs
index 0b34a7c5..fc7e0f60 100644
--- a/probe-rs/src/session.rs
+++ b/probe-rs/src/session.rs
@@ -299,6 +299,15 @@ impl Session {
             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)?;
+
+                core.halt(Duration::from_millis(100))?;
+            }
+        }
+
         session.halted_access(|sess| sequence_handle.on_connect(sess.get_riscv_interface()?))?;
 
         Ok(session)
@@ -335,6 +344,15 @@ impl Session {
             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)?;
+
+                core.halt(Duration::from_millis(100))?;
+            }
+        }
+
         session.halted_access(|sess| sequence_handle.on_connect(sess.get_xtensa_interface()?))?;
 
         Ok(session)
diff --git a/probe-rs/targets/BCM2711.yaml b/probe-rs/targets/BCM2711.yaml
index 992dd627..d16d61ff 100644
--- a/probe-rs/targets/BCM2711.yaml
+++ b/probe-rs/targets/BCM2711.yaml
@@ -41,7 +41,7 @@ variants:
       - !Ram
           range:
             start: 0x00000000
-            end: 0x3b3fffff
+            end: 0x3b400000
           is_boot_memory: false
           cores:
             - core0
diff --git a/probe-rs/targets/BCM2712.yaml b/probe-rs/targets/BCM2712.yaml
index cb104d42..ad8d370b 100644
--- a/probe-rs/targets/BCM2712.yaml
+++ b/probe-rs/targets/BCM2712.yaml
@@ -41,7 +41,7 @@ variants:
       - !Ram
           range:
             start: 0x00000000
-            end: 0x3b3fffff
+            end: 0x3b400000
           is_boot_memory: false
           cores:
             - core0
diff --git a/probe-rs/targets/esp32c6.yaml b/probe-rs/targets/esp32c6.yaml
index bc0d11ba..d02f7d36 100644
--- a/probe-rs/targets/esp32c6.yaml
+++ b/probe-rs/targets/esp32c6.yaml
@@ -24,20 +24,14 @@ variants:
       - !Nvm
         range: # External Flash on Instruction/Data Bus
           start: 0x42000000
-          end: 0x42FFFFFF
+          end: 0x43000000
         is_alias: true
         cores:
           - main
       - !Ram
         range:
           start: 0x40800000
-          end: 0x4087FFFF
-        cores:
-          - main
-      - !Ram
-        range:
-          start: 0x50000000
-          end: 0x50003FFF
+          end: 0x40880000
         cores:
           - main
     flash_algorithms:
diff --git a/probe-rs/targets/esp32c6_lp.yaml b/probe-rs/targets/esp32c6_lp.yaml
index c12e155a..6e3f44f5 100644
--- a/probe-rs/targets/esp32c6_lp.yaml
+++ b/probe-rs/targets/esp32c6_lp.yaml
@@ -25,20 +25,20 @@ variants:
       - !Nvm
         range: # External Flash on Instruction/Data Bus
           start: 0x42000000
-          end: 0x42FFFFFF
+          end: 0x43000000
         is_alias: true
         cores:
           - lp
       - !Ram
         range:
           start: 0x40800000
-          end: 0x4087FFFF
+          end: 0x40880000
         cores:
           - lp
       - !Ram
         range:
           start: 0x50000000
-          end: 0x50003FFF
+          end: 0x50004000
         cores:
           - lp
     flash_algorithms: []
diff --git a/probe-rs/targets/esp32h2.yaml b/probe-rs/targets/esp32h2.yaml
index 953d84ef..58ce13f1 100644
--- a/probe-rs/targets/esp32h2.yaml
+++ b/probe-rs/targets/esp32h2.yaml
@@ -24,14 +24,14 @@ variants:
       - !Nvm
         range: # External Flash on Instruction/Data Bus
           start: 0x42000000
-          end: 0x42FFFFFF
+          end: 0x43000000
         is_alias: true
         cores:
           - main
       - !Ram
         range:
           start: 0x40800000
-          end: 0x4087FFFF
+          end: 0x40880000
         cores:
           - main
     flash_algorithms:
diff --git a/probe-rs/targets/iMX7ULP.yaml b/probe-rs/targets/iMX7ULP.yaml
index 32c9ceb0..40932c58 100644
--- a/probe-rs/targets/iMX7ULP.yaml
+++ b/probe-rs/targets/iMX7ULP.yaml
@@ -22,7 +22,7 @@ variants:
       - !Ram
           range:
             start: 0x60000000
-            end: 0x7fffffff
+            end: 0x80000000
           is_boot_memory: false
           cores:
             - core0
diff --git a/target-gen/Cargo.toml b/target-gen/Cargo.toml
index 4df59055..0fe6129d 100644
--- a/target-gen/Cargo.toml
+++ b/target-gen/Cargo.toml
@@ -32,7 +32,7 @@ zip = "0.6.6"
 clap = { version = "4.5", features = ["derive"] }
 colored = "2"
 anyhow = "1.0.82"
-reqwest = { version = "0.12.3", features = ["json", "rustls-tls"], default-features = false }
+reqwest = { version = "0.12.4", features = ["json", "rustls-tls"], default-features = false }
 futures = "0.3.30"
 tokio = { version = "1.37.0", features = ["macros", "rt", "rt-multi-thread"] }
 tracing-subscriber = { version = "0.3.18", features = [


Test patch:
diff --git a/.github/workflows/smoketest.yml b/.github/workflows/smoketest.yml
index fec93f1e..623659c0 100644
--- a/.github/workflows/smoketest.yml
+++ b/.github/workflows/smoketest.yml
@@ -22,25 +22,28 @@ jobs:
       - name: Install target for cross-compilation
         run: rustup target add aarch64-unknown-linux-gnu
 
-      - name: Install cross 
-        uses: taiki-e/install-action@v2.32.17
+      - name: Install cross
+        uses: taiki-e/install-action@v2.33.3
         with:
           tool: cross
 
       - name: Cache Dependencies
         uses: Swatinem/rust-cache@v2.7.3
-      
+
       - name: Build smoke-tester
         run:
           cross build --release --target aarch64-unknown-linux-gnu -p smoke_tester
-      
+
       - uses: actions/upload-artifact@v4
         with:
           name: smoke_tester
           path: target/aarch64-unknown-linux-gnu/release/smoke_tester
 
   test:
-    runs-on: ["self-hosted", "linux", "ARM64", "smoke-test"]
+    runs-on: ["self-hosted", "linux", "ARM64", "${{ matrix.runner }}"]
+    strategy:
+      matrix:
+        runner: ["smoke-test", "smoke-tester-2"]
 
     needs: build
 
@@ -52,4 +55,4 @@ jobs:
       - name: Run smoke-tester
         run:  |
           chmod +x smoke_tester
-          ./smoke_tester --dut-definitions $SMOKE_TESTER_CONFIG --markdown-summary $GITHUB_STEP_SUMMARY
+          ./smoke_tester test --dut-definitions $SMOKE_TESTER_CONFIG --markdown-summary $GITHUB_STEP_SUMMARY
diff --git a/probe-rs/src/bin/probe-rs/util/test_data/binary_cargo_config/Cargo.toml b/probe-rs/src/bin/probe-rs/util/test_data/binary_cargo_config/Cargo.toml
index 543d8739..9e372072 100644
--- a/probe-rs/src/bin/probe-rs/util/test_data/binary_cargo_config/Cargo.toml
+++ b/probe-rs/src/bin/probe-rs/util/test_data/binary_cargo_config/Cargo.toml
@@ -8,7 +8,7 @@ edition = "2018"
 
 [dependencies]
 panic-halt = "0.2.0"
-cortex-m-rt = "0.7.3"
+cortex-m-rt = "0.7.4"
 
 # This package is not part of the probe-rs workspace
 [workspace]
diff --git a/probe-rs/src/bin/probe-rs/util/test_data/binary_cargo_config_toml/Cargo.toml b/probe-rs/src/bin/probe-rs/util/test_data/binary_cargo_config_toml/Cargo.toml
index d8e5072b..aeab2d9f 100644
--- a/probe-rs/src/bin/probe-rs/util/test_data/binary_cargo_config_toml/Cargo.toml
+++ b/probe-rs/src/bin/probe-rs/util/test_data/binary_cargo_config_toml/Cargo.toml
@@ -8,7 +8,7 @@ edition = "2018"
 
 [dependencies]
 panic-halt = "0.2.0"
-cortex-m-rt = "0.7.3"
+cortex-m-rt = "0.7.4"
 
 # This package is not part of the probe-rs workspace
 [workspace]
diff --git a/probe-rs/tests/gpio-hal-blinky/Cargo.lock b/probe-rs/tests/gpio-hal-blinky/Cargo.lock
index 000e9397..9a0851ba 100644
--- a/probe-rs/tests/gpio-hal-blinky/Cargo.lock
+++ b/probe-rs/tests/gpio-hal-blinky/Cargo.lock
@@ -50,7 +50,7 @@ dependencies = [
  "bare-metal",
  "bitfield",
  "critical-section",
- "embedded-hal",
+ "embedded-hal 0.2.7",
  "volatile-register",
 ]
 
@@ -88,9 +88,9 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
 
 [[package]]
 name = "embedded-dma"
-version = "0.1.2"
+version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46c8c02e4347a0267ca60813c952017f4c5948c232474c6010a381a337f1bda4"
+checksum = "994f7e5b5cb23521c22304927195f236813053eb9c065dd2226a32ba64695446"
 dependencies = [
  "stable_deref_trait",
 ]
@@ -105,17 +105,29 @@ dependencies = [
  "void",
 ]
 
+[[package]]
+name = "embedded-hal"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89"
+
+[[package]]
+name = "embedded-io"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
+
 [[package]]
 name = "embedded-storage"
-version = "0.2.0"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "723dce4e9f25b6e6c5f35628e144794e5b459216ed7da97b7c4b66cdb3fa82ca"
+checksum = "a21dea9854beb860f3062d10228ce9b976da520a73474aed3171ec276bc0c032"
 
 [[package]]
 name = "fixed"
-version = "1.24.0"
+version = "1.27.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02c69ce7e7c0f17aa18fdd9d0de39727adb9c6281f2ad12f57cbe54ae6e76e7d"
+checksum = "2fc715d38bea7b5bf487fcd79bcf8c209f0b58014f3018a7a19c2b855f472048"
 dependencies = [
  "az",
  "bytemuck",
@@ -129,7 +141,7 @@ version = "0.1.0"
 dependencies = [
  "cortex-m",
  "cortex-m-rt",
- "embedded-hal",
+ "embedded-hal 0.2.7",
  "microbit",
  "panic-halt",
 ]
@@ -146,20 +158,20 @@ dependencies = [
 
 [[package]]
 name = "microbit"
-version = "0.13.0"
+version = "0.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22b772d53b18a80479fb066b72eb242bba80d217312cee590cd93e1263062827"
+checksum = "3ea941ae60d022668f789ff6a6439fdb59b5cea202cd2affbb967e9af7425356"
 dependencies = [
  "microbit-common",
 ]
 
 [[package]]
 name = "microbit-common"
-version = "0.13.0"
+version = "0.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "45651d001be4281a6f4536c30f3d8522f231bc56fd58d5baf2c630f565d31256"
+checksum = "37409f40befaa165d8a94255955475337422f5fe2fecaeeeedde170562ee114b"
 dependencies = [
- "embedded-hal",
+ "embedded-hal 1.0.0",
  "nrf51-hal",
  "tiny-led-matrix",
 ]
@@ -181,15 +193,16 @@ checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d"
 
 [[package]]
 name = "nrf-hal-common"
-version = "0.14.1"
+version = "0.17.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd37b3fd039327b4005cc0d4ef20bce0a41bf521071774d5adb854e4b1230639"
+checksum = "56fbecd598c7f16e32bd4783447ca023aa7897d4a4c8e70f6b7c5739c81b4ceb"
 dependencies = [
  "cast",
  "cfg-if",
  "cortex-m",
  "embedded-dma",
- "embedded-hal",
+ "embedded-hal 1.0.0",
+ "embedded-io",
  "embedded-storage",
  "fixed",
  "nb 1.1.0",
@@ -200,20 +213,19 @@ dependencies = [
 
 [[package]]
 name = "nrf51-hal"
-version = "0.14.1"
+version = "0.17.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "509b38924d7f589eb4803bd65c1e198156f520af10f56ff738edcde66291b860"
+checksum = "5fb186285d15fdfe7329ebb25e64b898f1c619831c43dc96849f11c3ef244b2a"
 dependencies = [
- "embedded-hal",
  "nrf-hal-common",
  "nrf51-pac",
 ]
 
 [[package]]
 name = "nrf51-pac"
-version = "0.10.1"
+version = "0.12.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a2b2c345355dcf3a92847312cef7f0a8f0b4355a367318444a07118f862f42a"
+checksum = "137f187dc6ee482e27312086bd3c3a83e1c273512782cf131a61957f72fc4219"
 dependencies = [
  "cortex-m",
  "cortex-m-rt",
diff --git a/probe-rs/tests/gpio-hal-blinky/Cargo.toml b/probe-rs/tests/gpio-hal-blinky/Cargo.toml
index f113f5b3..47ac193f 100644
--- a/probe-rs/tests/gpio-hal-blinky/Cargo.toml
+++ b/probe-rs/tests/gpio-hal-blinky/Cargo.toml
@@ -13,4 +13,4 @@ cortex-m = { version = "0.7", features = [
     "inline-asm",
 ] }
 panic-halt = "0.2.0"
-microbit = "0.13.0"
+microbit = "0.14.0"
diff --git a/smoke-tester/Cargo.toml b/smoke-tester/Cargo.toml
index 8adc77a1..14a854c4 100644
--- a/smoke-tester/Cargo.toml
+++ b/smoke-tester/Cargo.toml
@@ -9,12 +9,15 @@ publish = false
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
+
+serde = { version = "1", features = ["derive"] }
 probe-rs = { workspace = true }
+probe-rs-target = { workspace = true }
 toml = "0.8.12"
-anyhow = "1.0.82"
-serde = { version = "1", features = ["derive"] }
 pretty_env_logger = "0.5.0"
 log = "0.4.21"
-clap = "4.5"
+clap = { version = "4.5", features = ["derive"] }
 colored = "2.1.0"
 linkme = "0.3.25"
+miette = { version = "7.2.0", features = ["fancy"] }
+thiserror.workspace = true
diff --git a/smoke-tester/README.md b/smoke-tester/README.md
index 28b80dc2..a7a942fb 100644
--- a/smoke-tester/README.md
+++ b/smoke-tester/README.md
@@ -22,8 +22,7 @@ chip = "nrf51822_xxAB"
 
 probe_selector = "0d28:0204:9900360150494e4500492002000000600000000097969901"
 
-# Optional binary (ELF format), used to test flashing.
-# The path is relative to the .toml file.
+# Optional binary (ELF format), used to test flashing. Needs to be specified as an absolute path.
 flash_test_binary = "gpio_hal_blinky"
 ```
 
diff --git a/smoke-tester/src/dut_definition.rs b/smoke-tester/src/dut_definition.rs
index 318bdb1f..50758ec1 100644
--- a/smoke-tester/src/dut_definition.rs
+++ b/smoke-tester/src/dut_definition.rs
@@ -3,9 +3,11 @@
 //! This module handles the definition of the different devices under test (DUTs),
 //! which are used by the tester.
 
-use anyhow::{bail, ensure, Context, Result};
+use miette::IntoDiagnostic;
+use miette::Result;
+use miette::WrapErr;
 use probe_rs::{
-    config::{get_target_by_name, search_chips},
+    config::get_target_by_name,
     probe::{list::Lister, DebugProbeSelector, Probe},
     Target,
 };
@@ -31,21 +33,21 @@ struct RawDutDefinition {
 impl RawDutDefinition {
     /// Try to parse a DUT definition from a file.
     fn from_file(file: &Path) -> Result<Self> {
-        let file_content = std::fs::read_to_string(file)?;
+        let file_content = std::fs::read_to_string(file).into_diagnostic()?;
 
-        let definition: RawDutDefinition = toml::from_str(&file_content)?;
+        let definition: RawDutDefinition = toml::from_str(&file_content).into_diagnostic()?;
 
         Ok(definition)
     }
 }
 
-#[derive(Clone)]
+#[derive(Debug, Clone)]
 pub enum DefinitionSource {
     File(PathBuf),
     Cli,
 }
 
-#[derive(Clone)]
+#[derive(Debug, Clone)]
 pub struct DutDefinition {
     pub chip: Target,
 
@@ -69,10 +71,10 @@ pub struct DutDefinition {
 }
 
 impl DutDefinition {
-    pub fn new(chip: &str, probe: &str) -> Result<Self> {
+    pub fn new(chip: &str, probe: &str) -> miette::Result<Self> {
         let target = lookup_unique_target(chip)?;
 
-        let selector: DebugProbeSelector = probe.parse()?;
+        let selector: DebugProbeSelector = probe.parse().into_diagnostic()?;
 
         Ok(DutDefinition {
             chip: target,
@@ -83,7 +85,7 @@ impl DutDefinition {
         })
     }
 
-    pub fn autodetect_probe(chip: &str) -> Result<Self> {
+    pub fn autodetect_probe(chip: &str) -> miette::Result<Self> {
         let target = lookup_unique_target(chip)?;
 
         Ok(DutDefinition {
@@ -106,7 +108,7 @@ impl DutDefinition {
     pub fn collect(directory: impl AsRef<Path>) -> Result<Vec<DutDefinition>> {
         let directory = directory.as_ref();
 
-        ensure!(
+        miette::ensure!(
             directory.is_dir(),
             "Unable to collect target definitions from path '{}'. Path is not a directory.",
             directory.display()
@@ -114,8 +116,8 @@ impl DutDefinition {
 
         let mut definitions = Vec::new();
 
-        for file in directory.read_dir()? {
-            let file_path = file?.path();
+        for file in directory.read_dir().into_diagnostic()? {
+            let file_path = file.into_diagnostic()?.path();
 
             // Ignore files without .toml ending
             if file_path.extension() != Some(OsStr::new("toml")) {
@@ -126,8 +128,9 @@ impl DutDefinition {
                 continue;
             }
 
-            let definition = DutDefinition::from_file(&file_path)
-                .with_context(|| format!("Failed to parse definition '{}'", file_path.display()))?;
+            let definition = DutDefinition::from_file(&file_path).wrap_err_with(|| {
+                format!("Failed to parse definition '{}'", file_path.display())
+            })?;
 
             definitions.push(definition);
         }
@@ -149,21 +152,22 @@ impl DutDefinition {
             Some(selector) => {
                 let probe = lister
                     .open(selector)
-                    .with_context(|| format!("Failed to open probe with selector {selector}"))?;
+                    .into_diagnostic()
+                    .wrap_err_with(|| format!("Failed to open probe with selector {selector}"))?;
 
                 Ok(probe)
             }
             None => {
                 let probes = lister.list_all();
 
-                ensure!(!probes.is_empty(), "No probes detected!");
+                miette::ensure!(!probes.is_empty(), "No probes detected!");
 
-                ensure!(
+                miette::ensure!(
                     probes.len() < 2,
                     "Multiple probes detected. Specify which probe to use using the '--probe' argument."
                 );
 
-                let probe = probes[0].open()?;
+                let probe = probes[0].open().into_diagnostic()?;
 
                 Ok(probe)
             }
@@ -171,7 +175,7 @@ impl DutDefinition {
     }
 
     fn from_raw_definition(raw_definition: RawDutDefinition, source_file: &Path) -> Result<Self> {
-        let probe_selector = Some(raw_definition.probe_selector.try_into()?);
+        let probe_selector = Some(raw_definition.probe_selector.try_into().into_diagnostic()?);
 
         let target = lookup_unique_target(&raw_definition.chip)?;
 
@@ -190,35 +194,33 @@ impl DutDefinition {
 }
 
 fn lookup_unique_target(chip: &str) -> Result<Target> {
-    let targets = search_chips(chip)?;
-
-    ensure!(
-        !targets.is_empty(),
-        "Unable to find any chip matching {}",
-        &chip
-    );
-
-    if targets.len() > 1 {
-        let target_string = String::from(chip).to_ascii_uppercase();
-        if targets.contains(&target_string) {
-            // Multiple chips returned, but one was an exact match so we're using it
-            let target = get_target_by_name(target_string)?;
-            return Ok(target);
-        } else {
-            eprintln!(
-                "For tests, chip definition must be exact. Chip name {} matches multiple chips:",
-                &chip
-            );
-
-            for target in &targets {
-                eprintln!("\t{target}");
-            }
+    let target = get_target_by_name(chip).into_diagnostic()?;
 
-            bail!("Chip definition does not match exactly.");
-        }
+    if !target.name.eq_ignore_ascii_case(chip) {
+        miette::bail!("Chip definition does not match exactly, the chip is specified as {}, but the entry in the registry is {}", chip, target.name);
     }
 
-    let target = get_target_by_name(&targets[0])?;
-
     Ok(target)
 }
+
+#[test]
+fn find_unique_target() {
+    let target = lookup_unique_target("nRF52840_xxAA").unwrap();
+
+    assert_eq!(target.name, "nRF52840_xxAA");
+}
+
+#[test]
+fn find_unique_target_failure() {
+    // this should fail because the full name is nRF52840_xxAA
+    lookup_unique_target("nRF52840_xx").unwrap_err();
+}
+
+#[test]
+fn find_unique_target_with_non_unique_prefix() {
+    // There is also a chip named "esp32c6_lp" in the registry, this ensures
+    // that looking up just "esp32c6" still works.
+    let target = lookup_unique_target("esp32c6").unwrap();
+
+    assert_eq!(target.name, "esp32c6");
+}
diff --git a/smoke-tester/src/macros.rs b/smoke-tester/src/macros.rs
index 2209f00d..df19acf4 100644
--- a/smoke-tester/src/macros.rs
+++ b/smoke-tester/src/macros.rs
@@ -37,7 +37,7 @@ macro_rules! println_dut_status {
 #[macro_export]
 macro_rules! println_test_status {
     ($tracker:expr, $color:ident,$($arg:tt)*) => ({
-        let prefix = format!("[{}/{}]({}) - Test [{}/{}]", $tracker.current_dut(), $tracker.num_duts(), $tracker.current_dut_name(), $tracker.current_test(), $tracker.num_tests()).$color();
+        let prefix = format!("[{}/{}]({}) - Test [{}]", $tracker.current_dut(), $tracker.num_duts(), $tracker.current_dut_name(), $tracker.current_test()).$color();
         print!("{} - ", prefix);
         println!($($arg)*);
     })
diff --git a/smoke-tester/src/main.rs b/smoke-tester/src/main.rs
index 3db1dcc1..048d1243 100644
--- a/smoke-tester/src/main.rs
+++ b/smoke-tester/src/main.rs
@@ -1,85 +1,121 @@
 use std::{
-    error::Error,
     io::Write,
-    marker::PhantomData,
-    path::Path,
+    path::{Path, PathBuf},
     process::ExitCode,
     time::{Duration, Instant},
 };
 
-use crate::{
-    dut_definition::{DefinitionSource, DutDefinition},
-    tests::test_flashing,
-};
-use anyhow::{Context, Result};
-use colored::Colorize;
+use crate::dut_definition::{DefinitionSource, DutDefinition};
 
-use clap::{Arg, Command};
+use clap::{Parser, Subcommand};
+use colored::Colorize;
 use linkme::distributed_slice;
+use miette::{Context, IntoDiagnostic, Result};
 use probe_rs::Permissions;
 
 mod dut_definition;
 mod macros;
 mod tests;
 
+#[derive(Debug, miette::Diagnostic, thiserror::Error)]
+pub enum TestFailure {
+    #[error("Test returned an error")]
+    Error(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
+
+    #[error("The test was skipped: {0}")]
+    Skipped(String),
+
+    #[error("Test is not implemented for target {0:?}: {1}")]
+    UnimplementedForTarget(Box<probe_rs::Target>, String),
+    #[error("A resource necessary to execute the test is not available: {0}")]
+    MissingResource(String),
+
+    /// A fatal error means that all future tests will be cancelled as well.
+    #[error("A fatal error occured")]
+    Fatal(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
+}
+
+impl From<miette::ErrReport> for TestFailure {
+    fn from(report: miette::ErrReport) -> Self {
+        TestFailure::Error(report.into())
+    }
+}
+
+impl From<probe_rs::Error> for TestFailure {
+    fn from(error: probe_rs::Error) -> Self {
+        TestFailure::Error(error.into())
+    }
+}
+
+pub type TestResult = Result<(), TestFailure>;
+
+#[derive(Debug)]
+struct SingleTestReport {
+    result: TestResult,
+    _duration: Duration,
+}
+
+impl SingleTestReport {
+    fn has_fatal_error(&self) -> bool {
+        matches!(self.result, Err(TestFailure::Fatal(_)))
+    }
+}
+
+#[derive(Debug, Subcommand)]
+enum Command {
+    Test {
+        #[arg(long, value_name = "FILE")]
+        markdown_summary: Option<PathBuf>,
+    },
+}
+
+#[derive(Debug, Parser)]
+struct Opt {
+    #[command(subcommand)]
+    command: Command,
+
+    #[arg(long, global = true, value_name = "DIRECTORY", conflicts_with_all = ["chip", "probe", "single_dut"])]
+    dut_definitions: Option<PathBuf>,
+
+    #[arg(long, global = true, value_name = "CHIP", conflicts_with_all = ["dut_definitions", "single_dut"])]
+    chip: Option<String>,
+
+    #[arg(long, global = true, value_name = "PROBE")]
+    probe: Option<String>,
+
+    #[arg(long, global = true, value_name = "FILE", conflicts_with_all = ["chip", "dut_definitions"])]
+    single_dut: Option<PathBuf>,
+}
+
 fn main() -> Result<ExitCode> {
     pretty_env_logger::init();
 
-    let app = Command::new("smoke tester")
-        .arg(
-            Arg::new("dut_definitions")
-                .long("dut-definitions")
-                .value_name("DIRECTORY")
-                .conflicts_with_all(["chip", "probe", "single_dut"])
-                .required(true),
-        )
-        .arg(
-            Arg::new("chip")
-                .long("chip")
-                .value_name("CHIP")
-                .conflicts_with_all(["dut_definitions", "single_dut"])
-                .required(true),
-        )
-        .arg(
-            Arg::new("probe")
-                .long("probe")
-                .value_name("PROBE")
-                .required(false),
-        )
-        .arg(
-            Arg::new("single_dut")
-                .long("single-dut")
-                .value_name("FILE")
-                .required(true)
-                .conflicts_with_all(["chip", "dut_definitions"]),
-        )
-        .arg(
-            Arg::new("markdown_summary")
-                .long("markdown-summary")
-                .value_name("FILE")
-                .required(false),
-        );
-
-    let matches = app.get_matches();
-
-    let definitions = if let Some(dut_definitions) = matches.get_one::<String>("dut_definitions") {
+    let opt = Opt::parse();
+
+    let definitions = if let Some(dut_definitions) = opt.dut_definitions.as_deref() {
         let definitions = DutDefinition::collect(dut_definitions)?;
         println!("Found {} target definitions.", definitions.len());
         definitions
-    } else if let Some(single_dut) = matches.get_one::<String>("single_dut") {
+    } else if let Some(single_dut) = opt.single_dut.as_deref() {
         vec![DutDefinition::from_file(Path::new(single_dut))?]
     } else {
         // Chip needs to be specified
-        let chip = matches.get_one::<String>("chip").unwrap(); // If dut-definitions is not present, chip must be present
+        let chip = opt.chip.as_deref().unwrap(); // If dut-definitions is not present, chip must be present
 
-        if let Some(probe) = matches.get_one::<String>("probe") {
+        if let Some(probe) = &opt.probe {
             vec![DutDefinition::new(chip, probe)?]
         } else {
             vec![DutDefinition::autodetect_probe(chip)?]
         }
     };
 
-    let mut test_tracker = TestTracker::new(definitions, 0);
+    match opt.command {
+        Command::Test { markdown_summary } => run_test(&definitions, markdown_summary),
+    }
+}
+
+fn run_test(definitions: &[DutDefinition], markdown_summary: Option<PathBuf>) -> Result<ExitCode> {
+    let mut test_tracker = TestTracker::new(definitions);
 
     let result = test_tracker.run(|tracker, definition| {
         let probe = definition.open_probe()?;
@@ -92,7 +128,8 @@ fn main() -> Result<ExitCode> {
 
         let mut session = probe
             .attach(definition.chip.clone(), permissions)
-            .context("Failed to attach to chip")?;
+            .into_diagnostic()
+            .wrap_err("Failed to attach to chip")?;
         let cores = session.list_cores();
 
         // TODO: Handle different cores. Handling multiple cores is not supported properly yet,
@@ -100,25 +137,35 @@ fn main() -> Result<ExitCode> {
         for (core_index, core_type) in cores.into_iter().take(1) {
             println_dut_status!(tracker, blue, "Core {}: {:?}", core_index, core_type);
 
-            let mut core = session.core(core_index)?;
+            let mut core = session.core(core_index).into_diagnostic()?;
 
             println_dut_status!(tracker, blue, "Halting core..");
 
-            core.reset_and_halt(Duration::from_millis(500))?;
+            core.reset_and_halt(Duration::from_millis(500))
+                .into_diagnostic()?;
 
             for test_fn in CORE_TESTS {
-                tracker.run_test(|tracker| test_fn(tracker, &mut core))?;
+                let result = tracker.run_test(|tracker| test_fn(tracker, &mut core));
+
+                if result.has_fatal_error() {
+                    return Err(miette::miette!("Test failed with fatal error"));
+                }
             }
 
             // Ensure core is not running anymore.
-            core.reset_and_halt(Duration::from_millis(200))?;
+            core.reset_and_halt(Duration::from_millis(200))
+                .into_diagnostic()
+                .wrap_err_with(|| {
+                    format!("Failed to reset core with index {core_index} after test")
+                })?;
         }
 
-        if let Some(flash_binary) = &definition.flash_test_binary {
-            tracker.run_test(|tracker| {
-                test_flashing(tracker, &mut session, flash_binary)?;
-                Ok(())
-            })?;
+        for test in SESSION_TESTS {
+            let result = tracker.run_test(|tracker| test(tracker, &mut session));
+
+            if result.has_fatal_error() {
+                return Err(miette::miette!("Test failed with fatal error"));
+            }
         }
 
         drop(session);
@@ -128,8 +175,9 @@ fn main() -> Result<ExitCode> {
         if definition.reset_connected {
             let probe = definition.open_probe()?;
 
-            let _session =
-                probe.attach_under_reset(definition.chip.clone(), Permissions::default())?;
+            let _session = probe
+                .attach_under_reset(definition.chip.clone(), Permissions::default())
+                .into_diagnostic()?;
         }
 
         Ok(())
@@ -139,17 +187,26 @@ fn main() -> Result<ExitCode> {
 
     let printer = ConsoleReportPrinter;
 
-    printer.print(&result, std::io::stdout())?;
-
-    if let Some(summary_file) = matches.get_one::<String>("markdown_summary") {
-        let mut file = std::fs::File::create(summary_file)?;
+    printer
+        .print(&result, std::io::stdout())
+        .into_diagnostic()?;
+
+    if let Some(summary_file) = &markdown_summary {
+        let mut file = std::fs::File::create(summary_file)
+            .into_diagnostic()
+            .wrap_err_with(|| {
+                format!(
+                    "Failed to create markdown summary file at location {}",
+                    summary_file.display()
+                )
+            })?;
 
-        writeln!(file, "## smoke-tester")?;
+        writeln!(file, "## smoke-tester").into_diagnostic()?;
 
         for dut in &result.dut_tests {
             let test_state = if dut.succesful { "Passed" } else { "Failed" };
 
-            writeln!(file, " - {}: {}", dut.name, test_state)?;
+            writeln!(file, " - {}: {}", dut.name, test_state).into_diagnostic()?;
         }
     }
 
@@ -236,22 +293,19 @@ impl ConsoleReportPrinter {
     }
 }
 
-pub struct TestTracker<'a> {
-    dut_definitions: Vec<DutDefinition>,
+#[derive(Debug)]
+pub struct TestTracker<'dut> {
+    dut_definitions: &'dut [DutDefinition],
     current_dut: usize,
-    num_tests: usize,
     current_test: usize,
-    _marker: PhantomData<&'a ()>,
 }
 
-impl<'a> TestTracker<'a> {
-    fn new(dut_definitions: Vec<DutDefinition>, num_tests: usize) -> Self {
+impl<'dut> TestTracker<'dut> {
+    fn new(dut_definitions: &'dut [DutDefinition]) -> Self {
         Self {
             dut_definitions,
             current_dut: 0,
-            num_tests,
             current_test: 0,
-            _marker: PhantomData,
         }
     }
 
@@ -276,26 +330,28 @@ impl<'a> TestTracker<'a> {
         self.current_test + 1
     }
 
-    fn num_tests(&self) -> usize {
-        self.num_tests
-    }
-
     fn advance_test(&mut self) {
         self.current_test += 1;
     }
 
+    pub fn current_target(&self) -> &probe_rs::Target {
+        &self.dut_definitions[self.current_dut].chip
+    }
+
+    pub fn current_dut_definition(&self) -> &DutDefinition {
+        &self.dut_definitions[self.current_dut]
+    }
+
     #[must_use]
     fn run(
         &mut self,
-        handle_dut: impl Fn(&mut TestTracker, &DutDefinition) -> Result<(), probe_rs::Error>
-            + Sync
-            + Send,
+        handle_dut: impl Fn(&mut TestTracker, &DutDefinition) -> miette::Result<()> + Sync + Send,
     ) -> TestReport {
         let mut report = TestReport::new();
 
         let mut tests_ok = true;
 
-        for definition in &self.dut_definitions.clone() {
+        for definition in self.dut_definitions {
             print_dut_status!(self, blue, "Starting Test",);
 
             if let DefinitionSource::File(path) = &definition.source {
@@ -354,8 +410,8 @@ impl<'a> TestTracker<'a> {
 
     fn run_test(
         &mut self,
-        test: impl FnOnce(&TestTracker) -> Result<(), probe_rs::Error>,
-    ) -> Result<(), probe_rs::Error> {
+        test: impl FnOnce(&TestTracker) -> Result<(), TestFailure>,
+    ) -> SingleTestReport {
         let start_time = Instant::now();
 
         let test_result = test(self);
@@ -372,16 +428,35 @@ impl<'a> TestTracker<'a> {
             Ok(()) => {
                 println_test_status!(self, green, "Test passed in {formatted_duration}.");
             }
+            Err(TestFailure::UnimplementedForTarget(target, message)) => {
+                println_test_status!(
+                    self,
+                    yellow,
+                    "Test not implemented for {}: {}",
+                    target.name,
+                    message
+                );
+            }
+            Err(TestFailure::MissingResource(message)) => {
+                println_test_status!(self, yellow, "Missing resource for test: {}", message);
+            }
             Err(_e) => {
                 println_test_status!(self, red, "Test failed in {formatted_duration}.");
             }
         };
         self.advance_test();
 
-        test_result
+        SingleTestReport {
+            result: test_result,
+            _duration: duration,
+        }
     }
 }
 
 /// A list of all tests which run on cores.
 #[distributed_slice]
-pub static CORE_TESTS: [fn(&TestTracker, &mut probe_rs::Core) -> Result<(), probe_rs::Error>];
+pub static CORE_TESTS: [fn(&TestTracker, &mut probe_rs::Core) -> TestResult];
+
+/// A list of all tests which run on `Session`.
+#[distributed_slice]
+pub static SESSION_TESTS: [fn(&TestTracker, &mut probe_rs::Session) -> TestResult];
diff --git a/smoke-tester/src/tests.rs b/smoke-tester/src/tests.rs
index 591709d9..44ebcc10 100644
--- a/smoke-tester/src/tests.rs
+++ b/smoke-tester/src/tests.rs
@@ -1,20 +1,21 @@
-use std::{path::Path, time::Instant};
+use std::time::Instant;
 
 use colored::Colorize;
 use linkme::distributed_slice;
 use probe_rs::{
+    config::MemoryRegion,
     flashing::{download_file_with_options, DownloadOptions, FlashProgress, Format},
     Architecture, Core, MemoryInterface, Session,
 };
 
 pub mod stepping;
 
-use anyhow::{Context, Result};
+use miette::{IntoDiagnostic, Result, WrapErr};
 
-use crate::{println_test_status, TestTracker, CORE_TESTS};
+use crate::{println_test_status, TestFailure, TestResult, TestTracker, CORE_TESTS, SESSION_TESTS};
 
 #[distributed_slice(CORE_TESTS)]
-pub fn test_register_read(tracker: &TestTracker, core: &mut Core) -> Result<(), probe_rs::Error> {
+pub fn test_register_read(tracker: &TestTracker, core: &mut Core) -> TestResult {
     println_test_status!(tracker, blue, "Testing register read...");
 
     let register = core.registers();
@@ -22,6 +23,7 @@ pub fn test_register_read(tracker: &TestTracker, core: &mut Core) -> Result<(),
     for register in register.core_registers() {
         let _: u64 = core
             .read_core_reg(register)
+            .into_diagnostic()
             .with_context(|| format!("Failed to read register {}", register.name()))?;
     }
 
@@ -29,7 +31,7 @@ pub fn test_register_read(tracker: &TestTracker, core: &mut Core) -> Result<(),
 }
 
 #[distributed_slice(CORE_TESTS)]
-fn test_register_write(tracker: &TestTracker, core: &mut Core) -> Result<(), probe_rs::Error> {
+fn test_register_write(tracker: &TestTracker, core: &mut Core) -> TestResult {
     println_test_status!(tracker, blue, "Testing register write...");
 
     let register = core.registers();
@@ -54,9 +56,10 @@ fn test_register_write(tracker: &TestTracker, core: &mut Core) -> Result<(), pro
 
         // Write new value
 
-        core.write_core_reg(register, test_value)?;
+        core.write_core_reg(register, test_value)
+            .into_diagnostic()?;
 
-        let readback: u64 = core.read_core_reg(register)?;
+        let readback: u64 = core.read_core_reg(register).into_diagnostic()?;
 
         assert_eq!(
             test_value, readback,
@@ -70,117 +73,153 @@ fn test_register_write(tracker: &TestTracker, core: &mut Core) -> Result<(), pro
 }
 
 #[distributed_slice(CORE_TESTS)]
-fn test_memory_access(tracker: &TestTracker, core: &mut Core) -> Result<(), probe_rs::Error> {
-    let memory_regions = core.memory_regions().cloned().collect::<Vec<_>>();
+fn test_memory_access(tracker: &TestTracker, core: &mut Core) -> TestResult {
+    let memory_regions = core
+        .memory_regions()
+        .filter_map(|region| match region {
+            MemoryRegion::Ram(ram) => Some(ram),
+            _ => None,
+        })
+        .cloned()
+        .collect::<Vec<_>>();
 
     // Try to write all memory regions
-    for region in memory_regions {
-        match region {
-            probe_rs::config::MemoryRegion::Ram(ram) => {
-                let ram_start = ram.range.start;
-                let ram_size = ram.range.end - ram.range.start;
-
-                println_test_status!(tracker, blue, "Test - RAM Start 32");
-                // Write first word
-                core.write_word_32(ram_start, 0xababab)?;
-                let value = core.read_word_32(ram_start)?;
-                assert_eq!(value, 0xababab);
-
-                println_test_status!(tracker, blue, "Test - RAM End 32");
-                // Write last word
-                core.write_word_32(ram_start + ram_size - 4, 0xababac)?;
-                let value = core.read_word_32(ram_start + ram_size - 4)?;
-                assert_eq!(value, 0xababac);
-
-                println_test_status!(tracker, blue, "Test - RAM Start 8");
-                // Write first byte
-                core.write_word_8(ram_start, 0xac)?;
-                let value = core.read_word_8(ram_start)?;
-                assert_eq!(value, 0xac);
-
-                println_test_status!(tracker, blue, "Test - RAM 8 Unaligned");
-                let address = ram_start + 1;
-                let data = 0x23;
-                // Write last byte
-                core.write_word_8(address, data)
-                    .with_context(|| format!("Write_word_8 to address {address:08x}"))?;
-
-                let value = core
-                    .read_word_8(address)
-                    .with_context(|| format!("read_word_8 from address {address:08x}"))?;
-                assert_eq!(value, data);
-
-                println_test_status!(tracker, blue, "Test - RAM End 8");
-                // Write last byte
-                core.write_word_8(ram_start + ram_size - 1, 0xcd)
-                    .with_context(|| {
-                        format!("Write_word_8 to address {:08x}", ram_start + ram_size - 1)
-                    })?;
-
-                let value = core
-                    .read_word_8(ram_start + ram_size - 1)
-                    .with_context(|| {
-                        format!("read_word_8 from address {:08x}", ram_start + ram_size - 1)
-                    })?;
-                assert_eq!(value, 0xcd);
-            }
-            // Ignore other types of regions
-            _other => {}
-        }
+    for ram in memory_regions {
+        let ram_start = ram.range.start;
+        let ram_size = ram.range.end - ram.range.start;
+
+        println_test_status!(tracker, blue, "Test - RAM Start 32");
+        // Write first word
+        core.write_word_32(ram_start, 0xababab)?;
+        let value = core.read_word_32(ram_start).into_diagnostic()?;
+        assert_eq!(
+            value, 0xababab,
+            "Error reading back 4 bytes from address {:#010X}",
+            ram_start
+        );
+
+        println_test_status!(tracker, blue, "Test - RAM End 32");
+        // Write last word
+        let addr = ram_start + ram_size - 4;
+        core.write_word_32(addr, 0xababac).into_diagnostic()?;
+        let value = core.read_word_32(addr).into_diagnostic()?;
+        assert_eq!(
+            value, 0xababac,
+            "Error reading back 4 bytes from address {:#010X}",
+            addr
+        );
+
+        println_test_status!(tracker, blue, "Test - RAM Start 8");
+        // Write first byte
+        core.write_word_8(ram_start, 0xac).into_diagnostic()?;
+        let value = core.read_word_8(ram_start).into_diagnostic()?;
+        assert_eq!(
+            value, 0xac,
+            "Error reading back 1 byte from address {:#010X}",
+            ram_start
+        );
+
+        println_test_status!(tracker, blue, "Test - RAM 8 Unaligned");
+        let address = ram_start + 1;
+        let data = 0x23;
+        // Write last byte
+        core.write_word_8(address, data)
+            .into_diagnostic()
+            .wrap_err_with(|| format!("write_word_8 to address {address:#010X}"))?;
+
+        let value = core
+            .read_word_8(address)
+            .into_diagnostic()
+            .wrap_err_with(|| format!("read_word_8 from address {address:#010X}"))?;
+        assert_eq!(
+            value, data,
+            "Error reading back 1 byte from address {:#010X}",
+            address
+        );
+
+        println_test_status!(tracker, blue, "Test - RAM End 8");
+        // Write last byte
+        let address = ram_start + ram_size - 1;
+        core.write_word_8(address, 0xcd)
+            .into_diagnostic()
+            .wrap_err_with(|| format!("write_word_8 to address {address:#010X}"))?;
+
+        let value = core
+            .read_word_8(address)
+            .into_diagnostic()
+            .wrap_err_with(|| format!("read_word_8 from address {address:#010X}"))?;
+        assert_eq!(
+            value, 0xcd,
+            "Error reading back 1 byte from address {:#010X}",
+            address
+        );
     }
 
     Ok(())
 }
 
 #[distributed_slice(CORE_TESTS)]
-fn test_hw_breakpoints(tracker: &TestTracker, core: &mut Core) -> Result<(), probe_rs::Error> {
+fn test_hw_breakpoints(tracker: &TestTracker, core: &mut Core) -> TestResult {
     println_test_status!(tracker, blue, "Testing HW breakpoints");
 
-    let memory_regions: Vec<_> = core.memory_regions().cloned().collect();
+    let memory_regions: Vec<_> = core
+        .memory_regions()
+        .filter_map(|region| match region {
+            MemoryRegion::Nvm(region) => Some(region),
+            _ => None,
+        })
+        .cloned()
+        .collect();
+
+    if memory_regions.is_empty() {
+        return Err(TestFailure::Skipped(
+            "No NVM memory regions found, unable to test HW breakpoints.".to_string(),
+        ));
+    }
 
     // For this test, we assume that code is executed from Flash / non-volatile memory, and try to set breakpoints
     // in these regions.
     for region in memory_regions {
-        match region {
-            probe_rs::config::MemoryRegion::Nvm(nvm) => {
-                let initial_breakpoint_addr = nvm.range.start;
+        let initial_breakpoint_addr = region.range.start;
 
-                let num_breakpoints = core.available_breakpoint_units()?;
+        let num_breakpoints = core.available_breakpoint_units().into_diagnostic()?;
 
-                println_test_status!(tracker, blue, "{} breakpoints supported", num_breakpoints);
+        println_test_status!(tracker, blue, "{} breakpoints supported", num_breakpoints);
 
-                for i in 0..num_breakpoints {
-                    core.set_hw_breakpoint(initial_breakpoint_addr + 4 * i as u64)?;
-                }
+        for i in 0..num_breakpoints {
+            core.set_hw_breakpoint(initial_breakpoint_addr + 4 * i as u64)
+                .into_diagnostic()?;
+        }
 
-                // Try to set an additional breakpoint, which should fail
-                core.set_hw_breakpoint(initial_breakpoint_addr + num_breakpoints as u64 * 4)
-                    .expect_err(
-                        "Trying to use more than supported number of breakpoints should fail.",
-                    );
+        // Try to set an additional breakpoint, which should fail
+        core.set_hw_breakpoint(initial_breakpoint_addr + num_breakpoints as u64 * 4)
+            .expect_err("Trying to use more than supported number of breakpoints should fail.");
 
-                // Clear all breakpoints again
-                for i in 0..num_breakpoints {
-                    core.clear_hw_breakpoint(initial_breakpoint_addr + 4 * i as u64)?;
-                }
-            }
-
-            // Skip other regions
-            _other => {}
+        // Clear all breakpoints again
+        for i in 0..num_breakpoints {
+            core.clear_hw_breakpoint(initial_breakpoint_addr + 4 * i as u64)
+                .into_diagnostic()?;
         }
     }
 
     Ok(())
 }
 
-pub fn test_flashing(
-    tracker: &TestTracker,
-    session: &mut Session,
-    test_binary: &Path,
-) -> Result<()> {
+#[distributed_slice(SESSION_TESTS)]
+pub fn test_flashing(tracker: &TestTracker, session: &mut Session) -> Result<(), TestFailure> {
+    let Some(test_binary) = tracker
+        .current_dut_definition()
+        .flash_test_binary
+        .as_deref()
+    else {
+        return Err(TestFailure::MissingResource(
+            "No flash test binary specified".to_string(),
+        ));
+    };
+
     let progress = FlashProgress::new(|event| {
         log::debug!("Flash Event: {:?}", event);
-        eprint!(".");
+        print!(".");
     });
 
     let mut options = DownloadOptions::default();
@@ -191,7 +230,12 @@ pub fn test_flashing(
 
     let start_time = Instant::now();
 
-    download_file_with_options(session, test_binary, Format::Elf, options)?;
+    let format = match session.target().default_format {
+        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)))?;
 
     println!();
 
diff --git a/smoke-tester/src/tests/stepping.rs b/smoke-tester/src/tests/stepping.rs
index 05a2441c..3c34d05c 100644
--- a/smoke-tester/src/tests/stepping.rs
+++ b/smoke-tester/src/tests/stepping.rs
@@ -1,23 +1,26 @@
 use std::time::Duration;
 
-use anyhow::Result;
 use linkme::distributed_slice;
+use miette::IntoDiagnostic;
 use probe_rs::{
     config::MemoryRegion, probe::DebugProbeError, Architecture, BreakpointCause, Core, CoreStatus,
     Error, HaltReason, MemoryInterface,
 };
 
-use crate::{TestTracker, CORE_TESTS};
+use crate::{TestFailure, TestResult, TestTracker, CORE_TESTS};
 
 const TEST_CODE: &[u8] = include_bytes!("test_arm.bin");
 
 #[distributed_slice(CORE_TESTS)]
-fn test_stepping(_tracker: &TestTracker, core: &mut Core) -> Result<(), probe_rs::Error> {
+fn test_stepping(tracker: &TestTracker, core: &mut Core) -> TestResult {
     println!("Testing stepping...");
 
     if core.architecture() == Architecture::Riscv {
         // Not implemented for RISC-V yet
-        return Ok(());
+        return Err(TestFailure::UnimplementedForTarget(
+            Box::new(tracker.current_target().clone()),
+            "Testing stepping is not implemented for RISC-V yet.".to_string(),
+        ));
     }
 
     let ram_region = core.memory_regions().find_map(MemoryRegion::as_ram_region);
@@ -25,25 +28,28 @@ fn test_stepping(_tracker: &TestTracker, core: &mut Core) -> Result<(), probe_rs
     let ram_region = if let Some(ram_region) = ram_region {
         ram_region.clone()
     } else {
-        return Err(probe_rs::Error::Other(anyhow::anyhow!(
-            "No RAM configured for core!"
-        )));
+        return Err(TestFailure::Skipped(
+            "No RAM configured for core, unable to test stepping".to_string(),
+        ));
     };
 
-    core.reset_and_halt(Duration::from_millis(100))?;
+    core.reset_and_halt(Duration::from_millis(100))
+        .into_diagnostic()?;
 
     let code_load_address = ram_region.range.start;
 
-    core.write_8(code_load_address, TEST_CODE)?;
+    core.write_8(code_load_address, TEST_CODE)
+        .into_diagnostic()?;
 
     let registers = core.registers();
-    core.write_core_reg(registers.pc().unwrap(), code_load_address)?;
+    core.write_core_reg(registers.pc().unwrap(), code_load_address)
+        .into_diagnostic()?;
 
-    let core_information = core.step()?;
+    let core_information = core.step().into_diagnostic()?;
 
     let expected_pc = code_load_address + 2;
 
-    let core_status = core.status()?;
+    let core_status = core.status().into_diagnostic()?;
 
     assert_eq!(
         core_information.pc, expected_pc,
@@ -54,7 +60,9 @@ fn test_stepping(_tracker: &TestTracker, core: &mut Core) -> Result<(), probe_rs
         log::warn!("Unexpected core status: {:?}!", core_status);
     }
 
-    let r0_value: u64 = core.read_core_reg(registers.core_register(0))?;
+    let r0_value: u64 = core
+        .read_core_reg(registers.core_register(0))
+        .into_diagnostic()?;
 
     assert_eq!(r0_value, 0);
 
@@ -67,28 +75,32 @@ fn test_stepping(_tracker: &TestTracker, core: &mut Core) -> Result<(), probe_rs
 
     // Run up to the software breakpoint (bkpt) at offset 0x6
     let break_address = code_load_address + 0x6;
-    core.run()?;
+    core.run().into_diagnostic()?;
 
     match core.wait_for_core_halted(Duration::from_millis(100)) {
         Ok(()) => {}
         Err(Error::Probe(DebugProbeError::Timeout)) => {
             println!("Core did not halt after timeout!");
-            core.halt(Duration::from_millis(100))?;
+            core.halt(Duration::from_millis(100)).into_diagnostic()?;
 
-            let pc: u64 = core.read_core_reg(core.program_counter())?;
+            let pc: u64 = core
+                .read_core_reg(core.program_counter())
+                .into_diagnostic()?;
 
             println!("Core stopped at: {pc:#08x}");
 
-            let r2_val: u64 = core.read_core_reg(registers.core_register(2))?;
+            let r2_val: u64 = core
+                .read_core_reg(registers.core_register(2))
+                .into_diagnostic()?;
 
             println!("$r2 = {r2_val:#08x}");
         }
-        Err(other) => return Err(other),
+        Err(other) => return Err(other.into()),
     }
 
     println!("Core halted again!");
 
-    let core_status = core.status()?;
+    let core_status = core.status().into_diagnostic()?;
 
     assert!(matches!(
         core_status,
@@ -96,19 +108,22 @@ fn test_stepping(_tracker: &TestTracker, core: &mut Core) -> Result<(), probe_rs
             | CoreStatus::Halted(HaltReason::Breakpoint(BreakpointCause::Unknown))
     ));
 
-    let pc: u64 = core.read_core_reg(core.program_counter())?;
+    let pc: u64 = core
+        .read_core_reg(core.program_counter())
+        .into_diagnostic()?;
 
     assert_eq!(pc, break_address);
 
     println!("Core halted at {pc:#08x}, now trying to run...");
 
     // Increase PC by 2 to skip breakpoint.
-    core.write_core_reg(core.program_counter(), pc + 2)?;
+    core.write_core_reg(core.program_counter(), pc + 2)
+        .into_diagnostic()?;
 
     println!("Run core again, with pc = {:#010x}", pc + 2);
 
     // Run to the finish
-    core.run()?;
+    core.run().into_diagnostic()?;
 
     // Final breakpoint is at offset 0x10
 
@@ -118,20 +133,24 @@ fn test_stepping(_tracker: &TestTracker, core: &mut Core) -> Result<(), probe_rs
         Ok(()) => {}
         Err(Error::Probe(DebugProbeError::Timeout)) => {
             println!("Core did not halt after timeout!");
-            core.halt(Duration::from_millis(100))?;
+            core.halt(Duration::from_millis(100)).into_diagnostic()?;
 
-            let pc: u64 = core.read_core_reg(core.program_counter())?;
+            let pc: u64 = core
+                .read_core_reg(core.program_counter())
+                .into_diagnostic()?;
 
             println!("Core stopped at: {pc:#08x}");
 
-            let r2_val: u64 = core.read_core_reg(registers.core_register(2))?;
+            let r2_val: u64 = core
+                .read_core_reg(registers.core_register(2))
+                .into_diagnostic()?;
 
             println!("$r2 = {r2_val:#08x}");
         }
-        Err(other) => return Err(other),
+        Err(other) => return Err(other.into()),
     }
 
-    let core_status = core.status()?;
+    let core_status = core.status().into_diagnostic()?;
 
     assert!(matches!(
         core_status,
@@ -139,12 +158,16 @@ fn test_stepping(_tracker: &TestTracker, core: &mut Core) -> Result<(), probe_rs
             | CoreStatus::Halted(HaltReason::Breakpoint(BreakpointCause::Unknown))
     ));
 
-    let pc: u64 = core.read_core_reg(core.program_counter())?;
+    let pc: u64 = core
+        .read_core_reg(core.program_counter())
+        .into_diagnostic()?;
 
     assert_eq!(pc, break_address, "{pc:#08x} != {break_address:#08x}");
 
     // Register r2 should be 1 to indicate end of test.
-    let r2_val: u64 = core.read_core_reg(registers.core_register(2))?;
+    let r2_val: u64 = core
+        .read_core_reg(registers.core_register(2))
+        .into_diagnostic()?;
     assert_eq!(1, r2_val);
 
     Ok(())