Repository: probe-rs/probe-rs
Pull request #3260 (2025-04-26 09:02:23)

Xtensa: unaligned memory access


Patch:
diff --git a/changelog/changed-swd-probe-interface-sequence-type.md b/changelog/changed-swd-probe-interface-sequence-type.md
new file mode 100644
index 00000000..b22708e4
--- /dev/null
+++ b/changelog/changed-swd-probe-interface-sequence-type.md
@@ -0,0 +1,1 @@
+Changed SWD protocol probe interface with a type to allow passing single stream of enums instead of two streams of bits.
diff --git a/probe-rs-tools/src/bin/probe-rs/rpc/functions/monitor.rs b/probe-rs-tools/src/bin/probe-rs/rpc/functions/monitor.rs
index ec216956..2f5c375e 100644
--- a/probe-rs-tools/src/bin/probe-rs/rpc/functions/monitor.rs
+++ b/probe-rs-tools/src/bin/probe-rs/rpc/functions/monitor.rs
@@ -42,6 +42,7 @@ impl MonitorMode {
 }
 
 #[derive(Serialize, Deserialize, Schema)]
+// TODO: maybe this should be an enum of RttEvent | SemihostingEvent? Communicate semihosting stream without strings, by adding open/close events?
 pub enum MonitorEvent {
     RttDiscovered {
         up_channels: Vec<String>,
@@ -51,7 +52,10 @@ pub enum MonitorEvent {
         channel: u32,
         bytes: Vec<u8>,
     },
-    SemihostingOutput(SemihostingOutput),
+    SemihostingOutput {
+        stream: String,
+        data: String,
+    },
 }
 
 #[derive(Serialize, Deserialize, Schema)]
@@ -240,8 +244,8 @@ impl<F: FnMut(MonitorEvent)> MonitorEventHandler<F> {
             }
             SemihostingCommand::Errno(_) => Ok(None),
             other if SemihostingReader::is_io(other) => {
-                if let Some(output) = self.semihosting_reader.handle(other, core)? {
-                    (self.sender)(MonitorEvent::SemihostingOutput(output));
+                if let Some((stream, data)) = self.semihosting_reader.handle(other, core)? {
+                    (self.sender)(MonitorEvent::SemihostingOutput { stream, data });
                 }
                 Ok(None)
             }
@@ -250,12 +254,6 @@ impl<F: FnMut(MonitorEvent)> MonitorEventHandler<F> {
     }
 }
 
-#[derive(Serialize, Deserialize, Schema)]
-pub enum SemihostingOutput {
-    StdOut(String),
-    StdErr(String),
-}
-
 pub struct SemihostingReader {
     stdout_open: bool,
     stderr_open: bool,
@@ -286,7 +284,7 @@ impl SemihostingReader {
         &mut self,
         command: SemihostingCommand,
         core: &mut Core<'_>,
-    ) -> anyhow::Result<Option<SemihostingOutput>> {
+    ) -> anyhow::Result<Option<(String, String)>> {
         let out = match command {
             SemihostingCommand::Open(request) => {
                 self.handle_open(core, request)?;
@@ -299,7 +297,7 @@ impl SemihostingReader {
             SemihostingCommand::Write(request) => self.handle_write(core, request)?,
             SemihostingCommand::WriteConsole(request) => {
                 let str = request.read(core)?;
-                Some(SemihostingOutput::StdOut(str))
+                Some((String::from("stdout"), str))
             }
 
             _ => None,
@@ -355,18 +353,18 @@ impl SemihostingReader {
         &mut self,
         core: &mut Core<'_>,
         request: WriteRequest,
-    ) -> anyhow::Result<Option<SemihostingOutput>> {
+    ) -> anyhow::Result<Option<(String, String)>> {
         match request.file_handle() {
             handle if handle == Self::STDOUT.get() => {
                 if self.stdout_open {
                     let string = read_written_string(core, request)?;
-                    return Ok(Some(SemihostingOutput::StdOut(string)));
+                    return Ok(Some((String::from("stdout"), string)));
                 }
             }
             handle if handle == Self::STDERR.get() => {
                 if self.stderr_open {
                     let string = read_written_string(core, request)?;
-                    return Ok(Some(SemihostingOutput::StdErr(string)));
+                    return Ok(Some((String::from("stderr"), string)));
                 }
             }
             other => tracing::warn!(
diff --git a/probe-rs-tools/src/bin/probe-rs/util/cli.rs b/probe-rs-tools/src/bin/probe-rs/util/cli.rs
index 0533e23a..050df499 100644
--- a/probe-rs-tools/src/bin/probe-rs/util/cli.rs
+++ b/probe-rs-tools/src/bin/probe-rs/util/cli.rs
@@ -17,7 +17,7 @@ use crate::{
         functions::{
             CancelTopic,
             flash::{BootInfo, DownloadOptions, FlashLayout, ProgressEvent, VerifyResult},
-            monitor::{MonitorEvent, MonitorMode, MonitorOptions, SemihostingOutput},
+            monitor::{MonitorEvent, MonitorMode, MonitorOptions},
             probe::{
                 AttachRequest, AttachResult, DebugProbeEntry, DebugProbeSelector, SelectProbeResult,
             },
@@ -523,14 +523,13 @@ async fn print_monitor_event(
                 return;
             };
 
-            processor.process(&bytes);
-        }
-        MonitorEvent::SemihostingOutput(SemihostingOutput::StdOut(str)) => {
-            print!("{}", str)
-        }
-        MonitorEvent::SemihostingOutput(SemihostingOutput::StdErr(str)) => {
-            eprint!("{}", str)
+            processor.process(&bytes).await;
         }
+        MonitorEvent::SemihostingOutput { stream, data } => match stream.as_str() {
+            "stdout" => print!("{data}"),
+            "stderr" => eprint!("{data}"),
+            _ => {}
+        },
     }
 }
 
@@ -555,8 +554,8 @@ impl Channel {
         self.printer.prefix = format!("[{:width$}] ", self.channel, width = width);
     }
 
-    fn process(&mut self, bytes: &[u8]) {
-        _ = self.decoder.process(bytes, &mut self.printer);
+    async fn process(&mut self, bytes: &[u8]) {
+        _ = self.decoder.process(bytes, &mut self.printer).await;
     }
 }
 
diff --git a/probe-rs/src/architecture/xtensa/communication_interface.rs b/probe-rs/src/architecture/xtensa/communication_interface.rs
index f235dee2..ccb59d59 100644
--- a/probe-rs/src/architecture/xtensa/communication_interface.rs
+++ b/probe-rs/src/architecture/xtensa/communication_interface.rs
@@ -1,11 +1,11 @@
 //! Xtensa Debug Module Communication
 
 use std::{
+    collections::HashMap,
     ops::Range,
     time::{Duration, Instant},
 };
 
-use probe_rs_target::MemoryRange;
 use zerocopy::IntoBytes;
 
 use crate::{
@@ -109,6 +109,19 @@ pub(super) struct XtensaInterfaceState {
     pub(super) is_halted: bool,
 }
 
+/// Properties of a memory region.
+#[derive(Clone, Copy, Default)]
+pub struct MemoryRegionProperties {
+    /// Whether the CPU supports unaligned stores. (Hardware Alignment Option)
+    pub unaligned_store: bool,
+
+    /// Whether the CPU supports unaligned loads. (Hardware Alignment Option)
+    pub unaligned_load: bool,
+
+    /// Whether the CPU supports fast memory access in this region. (LDDR32.P/SDDR32.P instructions)
+    pub fast_memory_access: bool,
+}
+
 /// Properties of an Xtensa CPU core.
 pub struct XtensaCoreProperties {
     /// The number of hardware breakpoints the target supports. CPU-specific configuration value.
@@ -117,8 +130,8 @@ pub struct XtensaCoreProperties {
     /// The interrupt level at which debug exceptions are generated. CPU-specific configuration value.
     pub debug_level: DebugLevel,
 
-    /// The address ranges for which we can use LDDR32.P/SDDR32.P instructions.
-    pub fast_memory_access_ranges: Vec<Range<u64>>,
+    /// Known memory ranges with special properties.
+    pub memory_ranges: HashMap<Range<u64>, MemoryRegionProperties>,
 
     /// Configurable options in the Windowed Register Option
     pub window_option_properties: WindowProperties,
@@ -129,12 +142,60 @@ impl Default for XtensaCoreProperties {
         Self {
             hw_breakpoint_num: 2,
             debug_level: DebugLevel::L6,
-            fast_memory_access_ranges: vec![],
+            memory_ranges: HashMap::new(),
             window_option_properties: WindowProperties::lx(64),
         }
     }
 }
 
+impl XtensaCoreProperties {
+    /// Returns the memory range for the given address.
+    pub fn memory_properties_at(&self, address: u64) -> MemoryRegionProperties {
+        self.memory_ranges
+            .iter()
+            .find(|(range, _)| range.contains(&address))
+            .map(|(_, region)| *region)
+            .unwrap_or_default()
+    }
+
+    /// Returns the conservative memory range properties for the given address range.
+    pub fn memory_range_properties(&self, range: Range<u64>) -> MemoryRegionProperties {
+        let mut start = range.start;
+        let end = range.end;
+
+        if start == end {
+            return MemoryRegionProperties::default();
+        }
+
+        let mut properties = MemoryRegionProperties {
+            unaligned_store: true,
+            unaligned_load: true,
+            fast_memory_access: true,
+        };
+        while start < end {
+            // Find region that contains the start address.
+            let containing_region = self
+                .memory_ranges
+                .iter()
+                .find(|(range, _)| range.contains(&start));
+
+            let Some((range, region_properties)) = containing_region else {
+                // no point in continuing
+                return MemoryRegionProperties::default();
+            };
+
+            properties.unaligned_store &= region_properties.unaligned_store;
+            properties.unaligned_load &= region_properties.unaligned_load;
+            properties.fast_memory_access &= region_properties.fast_memory_access;
+
+            // Move start to the end of the region.
+            start = range.end;
+        }
+
+        properties
+    }
+}
+
 /// Properties of the windowed register file.
 #[derive(Clone, Copy, Debug)]
 pub struct WindowProperties {
@@ -596,13 +657,11 @@ impl<'probe> XtensaCommunicationInterface<'probe> {
     }
 
     fn memory_access_for(&self, address: u64, len: usize) -> Box<dyn MemoryAccess> {
-        let use_fast_access = self
+        if self
             .core_properties
-            .fast_memory_access_ranges
-            .iter()
-            .any(|r| r.intersects_range(&(address..address + len as u64)));
-
-        if use_fast_access {
+            .memory_range_properties(address..address + len as u64)
+            .fast_memory_access
+        {
             Box::new(FastMemoryAccess::new())
         } else {
             Box::new(SlowMemoryAccess::new())
@@ -629,12 +688,17 @@ impl<'probe> XtensaCommunicationInterface<'probe> {
         address: u64,
         mut dst: &mut [u8],
     ) -> Result<(), XtensaError> {
-        memory_access.load_initial_address_for_read(self, address as u32 & !0x3)?;
-
         let mut to_read = dst.len();
 
-        // Let's assume we can just do 32b reads, so let's do some pre-massaging on unaligned reads
-        let first_read = if address % 4 != 0 {
+        // Let's assume we can just do 32b reads, so let's
+        // do some pre-massaging on unaligned reads if needed.
+        let first_read = if address % 4 != 0
+            && !self
+                .core_properties
+                .memory_range_properties(address..address + dst.len() as u64)
+                .unaligned_load
+        {
+            memory_access.load_initial_address_for_read(self, address as u32 & !0x3)?;
             let offset = address as usize % 4;
 
             // Avoid executing another read if we only have to read a single word
@@ -650,6 +714,8 @@ impl<'probe> XtensaCommunicationInterface<'probe> {
 
             Some((first_read, offset, bytes_to_copy))
         } else {
+            // The read is either aligned or the core supports unaligned loads.
+            memory_access.load_initial_address_for_read(self, address as u32)?;
             None
         };
 
@@ -704,43 +770,6 @@ impl<'probe> XtensaCommunicationInterface<'probe> {
         })
     }
 
-    fn write_memory_unaligned8(
-        &mut self,
-        memory_access: &mut dyn MemoryAccess,
-        address: u32,
-        data: &[u8],
-    ) -> Result<(), XtensaError> {
-        if data.is_empty() {
-            return Ok(());
-        }
-
-        let offset = address as usize % 4;
-        let aligned_address = address & !0x3;
-
-        assert!(
-            offset + data.len() <= 4,
-            "Trying to write data crossing a word boundary"
-        );
-
-        // Avoid reading back if we have a complete word
-        let data = if offset == 0 && data.len() == 4 {
-            data.try_into().unwrap()
-        } else {
-            // Read the aligned word
-            let mut word = [0; 4];
-            self.read_memory_impl(memory_access, aligned_address as u64, &mut word)?;
-
-            // Replace the written bytes. This will also panic if the input is crossing a word boundary
-            word[offset..][..data.len()].copy_from_slice(data);
-
-            word
-        };
-
-        // Write the word back
-        memory_access.load_initial_address_for_write(self, aligned_address)?;
-        memory_access.write_one(self, u32::from_le_bytes(data))
-    }
-
     fn write_memory_impl(
         &mut self,
         memory_access: &mut dyn MemoryAccess,
@@ -749,12 +778,32 @@ impl<'probe> XtensaCommunicationInterface<'probe> {
     ) -> Result<(), XtensaError> {
         let mut addr = address as u32;
 
-        // We store the unaligned head of the data separately
+        // We store the unaligned head of the data separately. In case the core supports unaligned
+        // load/store, we can just write the data directly.
         let mut address_loaded = false;
-        if addr % 4 != 0 {
+        if addr % 4 != 0
+            && !self
+                .core_properties
+                .memory_range_properties(address..address + buffer.len() as u64)
+                .unaligned_store
+        {
+            // If the core does not support unaligned load/store, read-modify-write the first
+            // few unaligned bytes. We are calculating `unaligned_bytes` so that we are not going
+            // across a word boundary here.
             let unaligned_bytes = (4 - (addr % 4) as usize).min(buffer.len());
+            let aligned_address = address & !0x3;
+            let offset_in_word = address as usize % 4;
+
+            // Read the aligned word
+            let mut word = [0; 4];
+            self.read_memory_impl(memory_access, aligned_address, &mut word)?;
 
-            self.write_memory_unaligned8(memory_access, addr, &buffer[..unaligned_bytes])?;
+            // Replace the written bytes.
+            word[offset_in_word..][..unaligned_bytes].copy_from_slice(&buffer[..unaligned_bytes]);
+
+            // Write the word back.
+            memory_access.load_initial_address_for_write(self, aligned_address as u32)?;
+            memory_access.write_one(self, u32::from_le_bytes(word))?;
 
             buffer = &buffer[unaligned_bytes..];
             addr += unaligned_bytes as u32;
@@ -762,6 +811,8 @@ impl<'probe> XtensaCommunicationInterface<'probe> {
             address_loaded = true;
         }
 
+        // Store whole words. If the core needs aligned accesses, the above block will have
+        // already stored the first unaligned part.
         if buffer.len() >= 4 {
             if !address_loaded {
                 memory_access.load_initial_address_for_write(self, addr)?;
@@ -780,9 +831,22 @@ impl<'probe> XtensaCommunicationInterface<'probe> {
             buffer = chunks.remainder();
         }
 
-        // We store the narrow tail of the data separately
+        // We store the narrow tail of the data (1-3 bytes) separately.
         if !buffer.is_empty() {
-            self.write_memory_unaligned8(memory_access, addr, buffer)?;
+            // We have 1-3 bytes left to write. If the core does not support unaligned load/store,
+            // the above blocks took care of aligning `addr` so we don't have to worry about
+            // crossing a word boundary here.
+
+            // Read the aligned word
+            let mut word = [0; 4];
+            self.read_memory_impl(memory_access, addr as u64, &mut word)?;
+
+            // Replace the written bytes.
+            word[..buffer.len()].copy_from_slice(buffer);
+
+            // Write the word back. We need to set the address because the read may have changed it.
+            memory_access.load_initial_address_for_write(self, addr)?;
+            memory_access.write_one(self, u32::from_le_bytes(word))?;
         }
 
         // TODO: implement cache flushing on CPUs that need it.
diff --git a/probe-rs/src/probe/arm_debug_interface.rs b/probe-rs/src/probe/arm_debug_interface.rs
index 3d1d79c5..d05385e2 100644
--- a/probe-rs/src/probe/arm_debug_interface.rs
+++ b/probe-rs/src/probe/arm_debug_interface.rs
@@ -365,7 +365,7 @@ fn perform_swd_transfers<P: RawProtocolIo>(
         io_sequence.extend(&transfer.io_sequence());
     }
 
-    let result = probe.swd_io(io_sequence.direction_bits(), io_sequence.io_bits())?;
+    let result = probe.swd_io(io_sequence.io_items())?;
 
     let mut result_bits = &result[..];
 
@@ -826,32 +826,47 @@ enum TransferStatus {
     Failed(DapError),
 }
 
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum IoSequenceItem {
+    Output(bool),
+    Input,
+}
+
+impl From<IoSequenceItem> for bool {
+    fn from(item: IoSequenceItem) -> Self {
+        match item {
+            IoSequenceItem::Output(b) => b,
+            IoSequenceItem::Input => panic!("Input type is not supposed to hold a value!"),
+        }
+    }
+}
+
+impl From<IoSequenceItem> for u8 {
+    fn from(value: IoSequenceItem) -> Self {
+        match value {
+            IoSequenceItem::Output(b) => b as u8,
+            IoSequenceItem::Input => panic!("Input type is not supposed to hold a value!"),
+        }
+    }
+}
+
 struct IoSequence {
-    io: Vec<bool>,
-    direction: Vec<bool>,
+    io: Vec<IoSequenceItem>,
 }
 
 impl IoSequence {
-    const INPUT: bool = false;
-    const OUTPUT: bool = true;
-
     fn new() -> Self {
-        IoSequence {
-            io: vec![],
-            direction: vec![],
-        }
+        IoSequence { io: vec![] }
     }
 
     fn with_capacity(capacity: usize) -> Self {
         IoSequence {
             io: Vec::with_capacity(capacity),
-            direction: Vec::with_capacity(capacity),
         }
     }
 
     fn reserve(&mut self, idle_cycles_after: usize) {
         self.io.reserve(idle_cycles_after);
-        self.direction.reserve(idle_cycles_after);
     }
 
     fn from_bytes(data: &[u8], mut bits: usize) -> Self {
@@ -871,13 +886,11 @@ impl IoSequence {
     }
 
     fn add_output(&mut self, bit: bool) {
-        self.io.push(bit);
-        self.direction.push(Self::OUTPUT);
+        self.io.push(IoSequenceItem::Output(bit));
     }
 
     fn add_input(&mut self) {
-        self.io.push(false);
-        self.direction.push(Self::INPUT);
+        self.io.push(IoSequenceItem::Input);
     }
 
     fn add_input_sequence(&mut self, length: usize) {
@@ -886,17 +899,12 @@ impl IoSequence {
         }
     }
 
-    fn io_bits(&self) -> impl Iterator<Item = bool> + '_ {
+    fn io_items(&self) -> impl Iterator<Item = IoSequenceItem> + '_ {
         self.io.iter().copied()
     }
 
-    fn direction_bits(&self) -> impl Iterator<Item = bool> + '_ {
-        self.direction.iter().copied()
-    }
-
     fn extend(&mut self, other: &IoSequence) {
         self.io.extend_from_slice(&other.io);
-        self.direction.extend_from_slice(&other.direction);
     }
 }
 
@@ -1037,10 +1045,9 @@ pub trait RawProtocolIo {
     where
         I: IntoIterator<Item = bool>;
 
-    fn swd_io<D, S>(&mut self, dir: D, swdio: S) -> Result<Vec<bool>, DebugProbeError>
+    fn swd_io<S>(&mut self, swdio: S) -> Result<Vec<bool>, DebugProbeError>
     where
-        D: IntoIterator<Item = bool>,
-        S: IntoIterator<Item = bool>;
+        S: IntoIterator<Item = IoSequenceItem>;
 
     fn swj_pins(
         &mut self,
@@ -1278,10 +1285,10 @@ fn send_sequence<P: RawProtocolIo + JTAGAccess>(
         WireProtocol::Jtag => {
             // Swj sequences should be shifted out to tms, since that is the pin
             // shared between swd and jtag modes.
-            probe.jtag_shift_tms(sequence.io_bits(), false)?;
+            probe.jtag_shift_tms(sequence.io_items().map(|i| i.into()), false)?;
         }
         WireProtocol::Swd => {
-            probe.swd_io(sequence.direction_bits(), sequence.io_bits())?;
+            probe.swd_io(sequence.io_items())?;
         }
     }
 
@@ -1300,7 +1307,7 @@ mod test {
     };
 
     use super::{
-        JTAG_ABORT_IR_VALUE, JTAG_ACCESS_PORT_IR_VALUE, JTAG_DEBUG_PORT_IR_VALUE,
+        IoSequenceItem, JTAG_ABORT_IR_VALUE, JTAG_ACCESS_PORT_IR_VALUE, JTAG_DEBUG_PORT_IR_VALUE,
         JTAG_DR_BIT_LENGTH, JTAG_STATUS_OK, JTAG_STATUS_WAIT, ProbeStatistics, RawProtocolIo,
         SwdSettings,
     };
@@ -1326,8 +1333,7 @@ mod test {
 
     #[derive(Debug)]
     struct MockJaylink {
-        direction_input: Option<Vec<bool>>,
-        io_input: Option<Vec<bool>>,
+        io_input: Option<Vec<IoSequenceItem>>,
         transfer_responses: Vec<Vec<bool>>,
         jtag_transactions: Vec<ExpectedJtagTransaction>,
 
@@ -1345,7 +1351,6 @@ mod test {
     impl MockJaylink {
         fn new() -> Self {
             Self {
-                direction_input: None,
                 io_input: None,
                 transfer_responses: vec![vec![]],
                 jtag_transactions: vec![],
@@ -1584,19 +1589,12 @@ mod test {
             Ok(())
         }
 
-        fn swd_io<D, S>(&mut self, dir: D, swdio: S) -> Result<Vec<bool>, DebugProbeError>
+        fn swd_io<S>(&mut self, swdio: S) -> Result<Vec<bool>, DebugProbeError>
         where
-            D: IntoIterator<Item = bool>,
-            S: IntoIterator<Item = bool>,
+            S: IntoIterator<Item = IoSequenceItem>,
         {
-            self.direction_input = Some(dir.into_iter().collect());
             self.io_input = Some(swdio.into_iter().collect());
 
-            assert_eq!(
-                self.direction_input.as_ref().unwrap().len(),
-                self.io_input.as_ref().unwrap().len()
-            );
-
             let transfer_response = self.transfer_responses.remove(0);
 
             let io_bits = self.io_input.as_ref().map(|v| v.len()).unwrap();
diff --git a/probe-rs/src/probe/blackmagic/mod.rs b/probe-rs/src/probe/blackmagic/mod.rs
index 9511155c..381b1f02 100644
--- a/probe-rs/src/probe/blackmagic/mod.rs
+++ b/probe-rs/src/probe/blackmagic/mod.rs
@@ -25,7 +25,7 @@ use serialport::{SerialPortType, available_ports};
 
 use super::{
     DebugProbeError, ProbeCreationError, ProbeError, WireProtocol,
-    arm_debug_interface::{ProbeStatistics, RawProtocolIo, SwdSettings},
+    arm_debug_interface::{IoSequenceItem, ProbeStatistics, RawProtocolIo, SwdSettings},
     common::{JtagDriverState, RawJtagIo},
 };
 
@@ -599,6 +599,15 @@ impl From<bool> for SwdDirection {
     }
 }
 
+impl From<IoSequenceItem> for SwdDirection {
+    fn from(value: IoSequenceItem) -> Self {
+        match value {
+            IoSequenceItem::Input => SwdDirection::Input,
+            IoSequenceItem::Output(_) => SwdDirection::Output,
+        }
+    }
+}
+
 /// A Black Magic Probe.
 pub struct BlackMagicProbe {
     reader: BufReader<Box<dyn Read + Send>>,
@@ -908,24 +917,19 @@ impl BlackMagicProbe {
     ///
     /// The caller needs to ensure that the given iterators are not longer than the maximum transfer size
     /// allowed. It seems that the maximum transfer size is determined by [`self.max_mem_block_size`].
-    fn perform_swdio_transfer<D, S>(
-        &mut self,
-        dir: D,
-        swdio: S,
-    ) -> Result<Vec<bool>, DebugProbeError>
+    #[allow(clippy::unnecessary_fallible_conversions)] //  IoSequenceItem conversion may panic
+    fn perform_swdio_transfer<S>(&mut self, swdio: S) -> Result<Vec<bool>, DebugProbeError>
     where
-        D: IntoIterator<Item = bool>,
-        S: IntoIterator<Item = bool>,
+        S: IntoIterator<Item = IoSequenceItem>,
     {
-        let dir = dir.into_iter();
-        let swdio = swdio.into_iter();
+        let swdio_sequence = swdio.into_iter();
         let mut output = vec![];
 
         let mut accumulator = 0u32;
         let mut accumulator_length = 0;
 
-        for (dir, swdio) in dir.zip(swdio) {
-            let dir = SwdDirection::from(dir);
+        for swdio in swdio_sequence {
+            let dir: SwdDirection = swdio.into();
             if dir != self.swd_direction
                 || accumulator_length >= core::mem::size_of_val(&accumulator) * 8
             {
@@ -950,7 +954,11 @@ impl BlackMagicProbe {
                 accumulator_length = 0;
             }
             self.swd_direction = dir;
-            accumulator |= if swdio { 1 << accumulator_length } else { 0 };
+            accumulator |= if swdio.try_into().unwrap_or(false) {
+                1 << accumulator_length
+            } else {
+                0
+            };
             accumulator_length += 1;
         }
 
@@ -1273,13 +1281,12 @@ impl RawProtocolIo for BlackMagicProbe {
         Ok(())
     }
 
-    fn swd_io<D, S>(&mut self, dir: D, swdio: S) -> Result<Vec<bool>, DebugProbeError>
+    fn swd_io<S>(&mut self, swdio: S) -> Result<Vec<bool>, DebugProbeError>
     where
-        D: IntoIterator<Item = bool>,
-        S: IntoIterator<Item = bool>,
+        S: IntoIterator<Item = IoSequenceItem>,
     {
         self.probe_statistics.report_io();
-        self.perform_swdio_transfer(dir, swdio)
+        self.perform_swdio_transfer(swdio)
     }
 
     fn swj_pins(
diff --git a/probe-rs/src/probe/ftdi/mod.rs b/probe-rs/src/probe/ftdi/mod.rs
index 9c86872e..112dce57 100644
--- a/probe-rs/src/probe/ftdi/mod.rs
+++ b/probe-rs/src/probe/ftdi/mod.rs
@@ -13,7 +13,7 @@ use crate::{
     probe::{
         DebugProbe, DebugProbeError, DebugProbeInfo, DebugProbeSelector, JTAGAccess,
         ProbeCreationError, ProbeFactory, ScanChainElement, WireProtocol,
-        arm_debug_interface::{ProbeStatistics, RawProtocolIo, SwdSettings},
+        arm_debug_interface::{IoSequenceItem, ProbeStatistics, RawProtocolIo, SwdSettings},
         common::{JtagDriverState, RawJtagIo},
     },
 };
@@ -457,10 +457,9 @@ impl RawProtocolIo for FtdiProbe {
         Ok(())
     }
 
-    fn swd_io<D, S>(&mut self, _dir: D, _swdio: S) -> Result<Vec<bool>, DebugProbeError>
+    fn swd_io<S>(&mut self, _swdio: S) -> Result<Vec<bool>, DebugProbeError>
     where
-        D: IntoIterator<Item = bool>,
-        S: IntoIterator<Item = bool>,
+        S: IntoIterator<Item = IoSequenceItem>,
     {
         Err(DebugProbeError::NotImplemented {
             function_name: "swd_io",
diff --git a/probe-rs/src/probe/jlink/bits.rs b/probe-rs/src/probe/jlink/bits.rs
index a3f7a96a..2754435e 100644
--- a/probe-rs/src/probe/jlink/bits.rs
+++ b/probe-rs/src/probe/jlink/bits.rs
@@ -65,7 +65,11 @@ pub(crate) trait IteratorExt: Sized {
     fn collapse_bytes(self) -> ByteIter<Self>;
 }
 
-impl<I: Iterator<Item = bool>> IteratorExt for I {
+impl<I, T> IteratorExt for I
+where
+    I: Iterator<Item = T>,
+    T: Into<bool>,
+{
     fn collapse_bytes(self) -> ByteIter<Self> {
         ByteIter { inner: self }
     }
@@ -75,7 +79,11 @@ pub(crate) struct ByteIter<I> {
     inner: I,
 }
 
-impl<I: Iterator<Item = bool>> Iterator for ByteIter<I> {
+impl<I, T> Iterator for ByteIter<I>
+where
+    I: Iterator<Item = T>,
+    T: Into<bool>,
+{
     type Item = u8;
 
     fn next(&mut self) -> Option<u8> {
@@ -84,7 +92,7 @@ impl<I: Iterator<Item = bool>> Iterator for ByteIter<I> {
         let mut has_data = false;
         for (pos, bit) in self.inner.by_ref().take(8).enumerate() {
             has_data = true;
-            byte |= (bit as u8) << pos;
+            byte |= (bit.into() as u8) << pos;
         }
 
         has_data.then_some(byte)
diff --git a/probe-rs/src/probe/jlink/mod.rs b/probe-rs/src/probe/jlink/mod.rs
index e439a6a8..6a30d848 100644
--- a/probe-rs/src/probe/jlink/mod.rs
+++ b/probe-rs/src/probe/jlink/mod.rs
@@ -48,7 +48,7 @@ use crate::{
     probe::{
         DebugProbe, DebugProbeError, DebugProbeInfo, DebugProbeSelector, ProbeFactory,
         WireProtocol,
-        arm_debug_interface::{ProbeStatistics, RawProtocolIo, SwdSettings},
+        arm_debug_interface::{IoSequenceItem, ProbeStatistics, RawProtocolIo, SwdSettings},
     },
 };
 
@@ -791,10 +791,9 @@ impl JLink {
     ///
     /// The caller needs to ensure that the given iterators are not longer than the maximum transfer size
     /// allowed. It seems that the maximum transfer size is determined by [`JLink::max_mem_block_size`].
-    fn perform_swdio_transfer<D, S>(&self, dir: D, swdio: S) -> Result<Vec<bool>, DebugProbeError>
+    fn perform_swdio_transfer<S>(&self, swdio: S) -> Result<Vec<bool>, DebugProbeError>
     where
-        D: IntoIterator<Item = bool>,
-        S: IntoIterator<Item = bool>,
+        S: IntoIterator<Item = IoSequenceItem>,
     {
         self.require_interface_selected(Interface::Swd)?;
 
@@ -803,30 +802,42 @@ impl JLink {
         let max_bits = (self.max_mem_block_size - COMMAND_OVERHEAD) / 2 * 8;
         let max_bits = std::cmp::min(max_bits, 65535);
 
-        let dir_chunks = dir.into_iter().chunks(max_bits);
         let swdio_chunks = swdio.into_iter().chunks(max_bits);
 
-        #[allow(clippy::useless_conversion)]
-        let chunks = dir_chunks.into_iter().zip(swdio_chunks.into_iter());
-
         let mut output = Vec::new();
         let mut buf = Vec::with_capacity(self.max_mem_block_size);
         buf.resize(COMMAND_OVERHEAD, 0);
 
-        for (dir, swdio) in chunks {
+        for swdio in swdio_chunks.into_iter() {
             buf.truncate(COMMAND_OVERHEAD);
 
-            let mut dir_bit_count = 0;
-            buf.extend(dir.inspect(|_| dir_bit_count += 1).collapse_bytes());
-            let mut swdio_bit_count = 0;
-            buf.extend(swdio.inspect(|_| swdio_bit_count += 1).collapse_bytes());
+            const INPUT: bool = false;
+            const OUTPUT: bool = true;
+
+            let swdio: Vec<_> = swdio.collect();
 
-            assert_eq!(
-                dir_bit_count, swdio_bit_count,
-                "`dir` and `swdio` must have the same number of bits"
+            //  Extend using the direction bits:
+            buf.extend(
+                swdio
+                    .iter()
+                    .map(|item| match item {
+                        IoSequenceItem::Input => INPUT,
+                        IoSequenceItem::Output { .. } => OUTPUT,
+                    })
+                    .collapse_bytes(),
+            );
+            //  Extend using the value bits:
+            buf.extend(
+                swdio
+                    .iter()
+                    .map(|item| match item {
+                        IoSequenceItem::Input => false,
+                        IoSequenceItem::Output(x) => *x,
+                    })
+                    .collapse_bytes(),
             );
 
-            let num_bits = dir_bit_count as u16;
+            let num_bits = swdio.len() as u16;
 
             buf[0] = Command::HwJtag3 as u8;
             // buf[1] is dummy data for alignment
@@ -1206,13 +1217,12 @@ impl RawProtocolIo for JLink {
         Ok(())
     }
 
-    fn swd_io<D, S>(&mut self, dir: D, swdio: S) -> Result<Vec<bool>, DebugProbeError>
+    fn swd_io<S>(&mut self, swdio: S) -> Result<Vec<bool>, DebugProbeError>
     where
-        D: IntoIterator<Item = bool>,
-        S: IntoIterator<Item = bool>,
+        S: IntoIterator<Item = IoSequenceItem>,
     {
         self.probe_statistics.report_io();
-        self.perform_swdio_transfer(dir, swdio)
+        self.perform_swdio_transfer(swdio)
     }
 
     fn swj_pins(
diff --git a/probe-rs/src/vendor/espressif/sequences/esp32.rs b/probe-rs/src/vendor/espressif/sequences/esp32.rs
index 36f34df5..b585f850 100644
--- a/probe-rs/src/vendor/espressif/sequences/esp32.rs
+++ b/probe-rs/src/vendor/espressif/sequences/esp32.rs
@@ -10,7 +10,9 @@ use crate::{
     MemoryInterface, Session,
     architecture::xtensa::{
         Xtensa,
-        communication_interface::{ProgramCounter, XtensaCommunicationInterface, XtensaError},
+        communication_interface::{
+            MemoryRegionProperties, ProgramCounter, XtensaCommunicationInterface, XtensaError,
+        },
         sequences::XtensaDebugSequence,
         xdm,
     },
@@ -77,13 +79,24 @@ impl ESP32 {
 
 impl XtensaDebugSequence for ESP32 {
     fn on_connect(&self, interface: &mut XtensaCommunicationInterface) -> Result<(), crate::Error> {
-        interface
-            .core_properties()
-            .fast_memory_access_ranges
-            .extend_from_slice(&[
-                0x3FF8_0000..0x4000_0000, // Data
-                0x4000_0000..0x400C_2000, // Instruction
-            ]);
+        // Data
+        interface.core_properties().memory_ranges.insert(
+            0x3FF8_0000..0x4000_0000,
+            MemoryRegionProperties {
+                unaligned_store: true,
+                unaligned_load: true,
+                fast_memory_access: true,
+            },
+        );
+        // Instruction
+        interface.core_properties().memory_ranges.insert(
+            0x4000_0000..0x400C_2000,
+            MemoryRegionProperties {
+                unaligned_store: false,
+                unaligned_load: false,
+                fast_memory_access: true,
+            },
+        );
 
         self.disable_wdts(interface)
     }
diff --git a/probe-rs/src/vendor/espressif/sequences/esp32s2.rs b/probe-rs/src/vendor/espressif/sequences/esp32s2.rs
index c61ab7c6..bb3870bf 100644
--- a/probe-rs/src/vendor/espressif/sequences/esp32s2.rs
+++ b/probe-rs/src/vendor/espressif/sequences/esp32s2.rs
@@ -10,7 +10,9 @@ use crate::{
     MemoryInterface, Session,
     architecture::xtensa::{
         Xtensa,
-        communication_interface::{XtensaCommunicationInterface, XtensaError},
+        communication_interface::{
+            MemoryRegionProperties, XtensaCommunicationInterface, XtensaError,
+        },
         sequences::XtensaDebugSequence,
         xdm::{self, DebugControlBits, DebugRegisterError},
     },
@@ -146,14 +148,33 @@ impl ESP32S2 {
 
 impl XtensaDebugSequence for ESP32S2 {
     fn on_connect(&self, interface: &mut XtensaCommunicationInterface) -> Result<(), crate::Error> {
-        interface
-            .core_properties()
-            .fast_memory_access_ranges
-            .extend_from_slice(&[
-                0x3F40_0000..0x3F50_0000, // Peripherals
-                0x3FF9_E000..0x4000_0000, // Data
-                0x4000_0000..0x4007_2000, // Instruction
-            ]);
+        // Peripherals
+        interface.core_properties().memory_ranges.insert(
+            0x3F40_0000..0x3F50_0000,
+            MemoryRegionProperties {
+                unaligned_store: false,
+                unaligned_load: false,
+                fast_memory_access: true,
+            },
+        );
+        // Data
+        interface.core_properties().memory_ranges.insert(
+            0x3FF9_E000..0x4000_0000,
+            MemoryRegionProperties {
+                unaligned_store: true,
+                unaligned_load: true,
+                fast_memory_access: true,
+            },
+        );
+        // Instruction
+        interface.core_properties().memory_ranges.insert(
+            0x4000_0000..0x4007_2000,
+            MemoryRegionProperties {
+                unaligned_store: false,
+                unaligned_load: false,
+                fast_memory_access: true,
+            },
+        );
 
         self.disable_wdts(interface)
     }
diff --git a/probe-rs/src/vendor/espressif/sequences/esp32s3.rs b/probe-rs/src/vendor/espressif/sequences/esp32s3.rs
index cae13864..37a4f941 100644
--- a/probe-rs/src/vendor/espressif/sequences/esp32s3.rs
+++ b/probe-rs/src/vendor/espressif/sequences/esp32s3.rs
@@ -10,7 +10,9 @@ use crate::{
     MemoryInterface, Session,
     architecture::xtensa::{
         Xtensa,
-        communication_interface::{ProgramCounter, XtensaCommunicationInterface, XtensaError},
+        communication_interface::{
+            MemoryRegionProperties, ProgramCounter, XtensaCommunicationInterface, XtensaError,
+        },
         sequences::XtensaDebugSequence,
         xdm,
     },
@@ -83,15 +85,42 @@ impl ESP32S3 {
 
 impl XtensaDebugSequence for ESP32S3 {
     fn on_connect(&self, interface: &mut XtensaCommunicationInterface) -> Result<(), crate::Error> {
-        interface
-            .core_properties()
-            .fast_memory_access_ranges
-            .extend_from_slice(&[
-                0x3FC8_8000..0x3FD0_0000, // Internal DRAM
-                0x3FF0_0000..0x3FF2_0000, // Internal DROM
-                0x4000_0000..0x4006_0000, // Internal IROM
-                0x4037_0000..0x403E_0000, // Internal IRAM
-            ]);
+        // Internal DRAM
+        interface.core_properties().memory_ranges.insert(
+            0x3FC8_8000..0x3FD0_0000,
+            MemoryRegionProperties {
+                unaligned_store: true,
+                unaligned_load: true,
+                fast_memory_access: true,
+            },
+        );
+        // Internal DROM
+        interface.core_properties().memory_ranges.insert(
+            0x3FF0_0000..0x3FF2_0000,
+            MemoryRegionProperties {
+                unaligned_store: false,
+                unaligned_load: true,
+                fast_memory_access: true,
+            },
+        );
+        // Internal IROM
+        interface.core_properties().memory_ranges.insert(
+            0x4000_0000..0x4006_0000,
+            MemoryRegionProperties {
+                unaligned_store: false,
+                unaligned_load: false,
+                fast_memory_access: true,
+            },
+        );
+        // Internal IRAM
+        interface.core_properties().memory_ranges.insert(
+            0x4037_0000..0x403E_0000,
+            MemoryRegionProperties {
+                unaligned_store: false,
+                unaligned_load: false,
+                fast_memory_access: true,
+            },
+        );
 
         self.disable_wdts(interface)
     }


Test patch:
diff --git a/probe-rs-tools/src/bin/probe-rs/rpc/functions/test.rs b/probe-rs-tools/src/bin/probe-rs/rpc/functions/test.rs
index adef7b55..4d0b6629 100644
--- a/probe-rs-tools/src/bin/probe-rs/rpc/functions/test.rs
+++ b/probe-rs-tools/src/bin/probe-rs/rpc/functions/test.rs
@@ -13,7 +13,7 @@ use crate::{
             ListTestsEndpoint, MonitorTopic, RpcResult, RpcSpawnContext, RunTestEndpoint,
             WireTxImpl,
             flash::BootInfo,
-            monitor::{MonitorEvent, RttPoller, SemihostingOutput, SemihostingReader},
+            monitor::{MonitorEvent, RttPoller, SemihostingReader},
         },
         utils::run_loop::{ReturnReason, RunLoop},
     },
@@ -97,12 +97,6 @@ pub enum TestResult {
     Cancelled,
 }
 
-#[derive(Serialize, Deserialize, Schema)]
-pub enum TestEvent {
-    RttOutput(String),
-    SemihostingOutput(SemihostingOutput),
-}
-
 #[derive(Serialize, Deserialize, Schema)]
 pub struct ListTestsRequest {
     pub sessid: Key<Session>,
@@ -314,8 +308,8 @@ impl<F: FnMut(MonitorEvent)> ListEventHandler<F> {
                 Ok(Some(list.into()))
             }
             other if SemihostingReader::is_io(other) => {
-                if let Some(output) = self.semihosting_reader.handle(other, core)? {
-                    (self.sender)(MonitorEvent::SemihostingOutput(output));
+                if let Some((stream, data)) = self.semihosting_reader.handle(other, core)? {
+                    (self.sender)(MonitorEvent::SemihostingOutput { stream, data });
                 }
                 Ok(None)
             }
@@ -389,8 +383,8 @@ impl<F: FnMut(MonitorEvent)> RunEventHandler<F> {
                 Ok(Some(TestOutcome::Panic))
             }
             other if SemihostingReader::is_io(other) => {
-                if let Some(output) = self.semihosting_reader.handle(other, core)? {
-                    (self.sender)(MonitorEvent::SemihostingOutput(output));
+                if let Some((stream, data)) = self.semihosting_reader.handle(other, core)? {
+                    (self.sender)(MonitorEvent::SemihostingOutput { stream, data });
                 }
                 Ok(None)
             }
diff --git a/smoke-tester/src/tests.rs b/smoke-tester/src/tests.rs
index 50591f6f..d476fc50 100644
--- a/smoke-tester/src/tests.rs
+++ b/smoke-tester/src/tests.rs
@@ -82,7 +82,7 @@ fn test_write_read(
     println_test_status!(
         tracker,
         blue,
-        "Testing:  write and read at address {:#010X}: {scenario}",
+        "Testing: write and read at address {:#010X}: {scenario}",
         address
     );
 
@@ -118,6 +118,14 @@ fn test_memory_access(tracker: &TestTracker, core: &mut Core) -> TestResult {
     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,
+            "Testing region: {} ({:#010X} - {:#010X})",
+            ram.name.as_deref().unwrap_or("<unnamed region>"),
+            ram.range.start,
+            ram.range.end
+        );
 
         println_test_status!(tracker, blue, "Test - RAM Start 32");
         // Write first word
@@ -225,6 +233,14 @@ fn test_hw_breakpoints(tracker: &TestTracker, core: &mut Core) -> TestResult {
     // 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 {
+        println_test_status!(
+            tracker,
+            blue,
+            "Testing region: {} ({:#010X} - {:#010X})",
+            region.name.as_deref().unwrap_or("<unnamed region>"),
+            region.range.start,
+            region.range.end
+        );
         let initial_breakpoint_addr = region.range.start;
 
         let num_breakpoints = core.available_breakpoint_units().into_diagnostic()?;
diff --git a/smoke-tester/src/tests/stepping.rs b/smoke-tester/src/tests/stepping.rs
index 3097b20f..b2780139 100644
--- a/smoke-tester/src/tests/stepping.rs
+++ b/smoke-tester/src/tests/stepping.rs
@@ -1,5 +1,6 @@
 use std::time::Duration;
 
+use colored::Colorize;
 use linkme::distributed_slice;
 use miette::IntoDiagnostic;
 use probe_rs::{
@@ -7,13 +8,13 @@ use probe_rs::{
     config::MemoryRegion, probe::DebugProbeError,
 };
 
-use crate::{CORE_TESTS, TestFailure, TestResult, TestTracker};
+use crate::{CORE_TESTS, TestFailure, TestResult, TestTracker, println_test_status};
 
 const TEST_CODE: &[u8] = include_bytes!("test_arm.bin");
 
 #[distributed_slice(CORE_TESTS)]
 fn test_stepping(tracker: &TestTracker, core: &mut Core) -> TestResult {
-    println!("Testing stepping...");
+    println_test_status!(tracker, blue, "Testing stepping on core {}...", core.id());
 
     if core.architecture() != Architecture::Arm {
         // Not implemented for RISC-V yet
@@ -70,9 +71,11 @@ fn test_stepping(tracker: &TestTracker, core: &mut Core) -> TestResult {
 
     assert_eq!(r0_value, 0);
 
-    println!("R0 value ok!");
+    println_test_status!(tracker, green, "R0 value ok!");
 
-    println!(
+    println_test_status!(
+        tracker,
+        blue,
         "Core halted at {:#08x}, now trying to run...",
         core_information.pc
     );
@@ -84,25 +87,25 @@ fn test_stepping(tracker: &TestTracker, core: &mut Core) -> TestResult {
     match core.wait_for_core_halted(Duration::from_millis(100)) {
         Ok(()) => {}
         Err(Error::Probe(DebugProbeError::Timeout)) => {
-            println!("Core did not halt after timeout!");
+            println_test_status!(tracker, yellow, "Core did not halt after timeout!");
             core.halt(Duration::from_millis(100)).into_diagnostic()?;
 
             let pc: u64 = core
                 .read_core_reg(core.program_counter())
                 .into_diagnostic()?;
 
-            println!("Core stopped at: {pc:#08x}");
+            println_test_status!(tracker, blue, "Core stopped at: {pc:#08x}");
 
             let r2_val: u64 = core
                 .read_core_reg(registers.core_register(2))
                 .into_diagnostic()?;
 
-            println!("$r2 = {r2_val:#08x}");
+            println_test_status!(tracker, blue, "$r2 = {r2_val:#08x}");
         }
         Err(other) => return Err(other.into()),
     }
 
-    println!("Core halted again!");
+    println_test_status!(tracker, green, "Core halted again!");
 
     let core_status = core.status().into_diagnostic()?;
 
@@ -118,13 +121,17 @@ fn test_stepping(tracker: &TestTracker, core: &mut Core) -> TestResult {
 
     assert_eq!(pc, break_address);
 
-    println!("Core halted at {pc:#08x}, now trying to run...");
+    println_test_status!(
+        tracker,
+        blue,
+        "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)
         .into_diagnostic()?;
 
-    println!("Run core again, with pc = {:#010x}", pc + 2);
+    println_test_status!(tracker, blue, "Run core again, with pc = {:#010x}", pc + 2);
 
     // Run to the finish
     core.run().into_diagnostic()?;
@@ -136,20 +143,20 @@ fn test_stepping(tracker: &TestTracker, core: &mut Core) -> TestResult {
     match core.wait_for_core_halted(Duration::from_millis(100)) {
         Ok(()) => {}
         Err(Error::Probe(DebugProbeError::Timeout)) => {
-            println!("Core did not halt after timeout!");
+            println_test_status!(tracker, yellow, "Core did not halt after timeout!");
             core.halt(Duration::from_millis(100)).into_diagnostic()?;
 
             let pc: u64 = core
                 .read_core_reg(core.program_counter())
                 .into_diagnostic()?;
 
-            println!("Core stopped at: {pc:#08x}");
+            println_test_status!(tracker, blue, "Core stopped at: {pc:#08x}");
 
             let r2_val: u64 = core
                 .read_core_reg(registers.core_register(2))
                 .into_diagnostic()?;
 
-            println!("$r2 = {r2_val:#08x}");
+            println_test_status!(tracker, blue, "$r2 = {r2_val:#08x}");
         }
         Err(other) => return Err(other.into()),
     }