Skip to main content

tagprobe/
lib.rs

1//! Programming tags through the tag bases with probe-rs.
2//!
3//! The replacement for qtprogram's STM32_Programmer_CLI calls: list the
4//! bases, ask each who it is, check the connection, program an image with an
5//! erase plan, and read flash back. Every operation picks its probe driver
6//! from the base's identity:
7//!
8//! - a base that answers the identify command (`F6 99`, see
9//!   `embedded/bases/README.md`) has the ST-LINK fixes, so probe-rs's own
10//!   ST-LINK driver is used ([`Driver::Stock`]);
11//! - a base that does not -- every base in the field -- is driven through
12//!   [`FieldSafeProbe`], which uses only the commands all bases support
13//!   ([`Driver::FieldSafe`]).
14//!
15//! Attaches here go through reset, so they end whatever the tag was doing,
16//! like every tool in this tree. Nothing here may run while a tagcore
17//! monitor session holds the same base: detach first.
18//!
19//! The evaluation that led here is
20//! `docs/investigations/2026-10-probe-rs-evaluation.md`; the crate's design is
21//! in `host/libraries/tagprobe-rs/design/architecture.md`.
22//!
23//! # Hardware side effects
24//!
25//! - [`list`] and [`identify`] never touch nRST or SWD, so the tag keeps
26//!   running.
27//! - [`connect_check`], [`program`] and [`read_memory`] all hold the tag in
28//!   reset to attach. A running log is ended, and the tag reboots its firmware
29//!   afterwards. [`program`] also rewrites internal flash.
30//!
31//! # Threading
32//!
33//! Every call opens the base, uses it and closes it again; nothing is held
34//! between calls, so calls may come from any thread. Two calls on the *same*
35//! base at once are not serialised here and would interleave their USB
36//! exchanges: the caller owns that exclusion. The Stock driver is opened
37//! through [`StLinkFactory`] only, never probe-rs's `Lister`; see
38//! [`open_probe`] for why that matters on macOS.
39//!
40//! # Examples
41//!
42//! The connect check and programming sequence:
43//!
44//! ```no_run
45//! use std::path::Path;
46//! use tagprobe::{Driver, Erase, Progress};
47//!
48//! let base = tagprobe::list().into_iter().next().expect("no base on USB");
49//! let driver = Driver::for_identity(tagprobe::identify(&base).as_deref());
50//! let found = tagprobe::connect_check(&base)?;
51//! let chip = tagprobe::chip_for(found.device_id, Some(0x40000))?;
52//! tagprobe::program(&base, driver, chip, Path::new("tag.elf"), Erase::ImagePages, |p| {
53//!     if let Progress::Step(step) = p {
54//!         println!("{step}");
55//!     }
56//! })?;
57//! # Ok::<(), String>(())
58//! ```
59
60pub mod fieldsafe;
61
62use std::path::Path;
63use std::sync::Arc;
64use std::time::Duration;
65
66use probe_rs::architecture::arm::sequences::ArmDebugSequence;
67use probe_rs::config::{DebugSequence, Registry, TargetSelector};
68use probe_rs::flashing::{
69    self, DownloadOptions, ElfLoader, FlashProgress, ProgressEvent, ProgressOperation,
70};
71use probe_rs::probe::Probe;
72use probe_rs::probe::stlink::StLinkFactory;
73use probe_rs::probe::{DebugProbeSelector, ProbeFactory};
74use probe_rs::{MemoryInterface, Permissions, Session};
75use tagcore::{LinkAdapt, UsbDev};
76
77pub use fieldsafe::FieldSafeProbe;
78
79/// The probe-rs release these tools are built and tested against.
80///
81/// For display (an "about" line or a log header). It is not read from
82/// `Cargo.toml`, so it must be updated by hand with the dependency. The
83/// field-safe driver implements probe-rs's probe traits, which change between
84/// minor releases, so a probe-rs upgrade needs the bench checks in the
85/// evaluation document repeated.
86pub const PROBE_RS_VERSION: &str = "0.32.0";
87
88/// STM32L43x/L44x DBGMCU device ID.
89///
90/// The low 12 bits of `DBGMCU_IDCODE`. The L432 tags (BitTag, PresTag,
91/// CompassTag, UIUCTag) report it.
92pub const DEV_L432: u16 = 0x435;
93/// STM32U37x/U38x DBGMCU device ID.
94///
95/// The low 12 bits of `DBGMCU_IDCODE`. The IMUTag boards (STM32U375) report
96/// it.
97pub const DEV_U375: u16 = 0x454;
98
99/// Address of `DBGMCU_IDCODE` on the STM32L4.
100const L4_DBGMCU_IDCODE: u32 = 0xE004_2000;
101/// Address of `DBGMCU_IDCODE` on the STM32U3.
102const U3_DBGMCU_IDCODE: u32 = 0xE004_4000;
103/// Address of the STM32L4 flash status register, `FLASH_SR`.
104const L4_FLASH_SR: u64 = 0x4002_2010;
105/// `FLASH_SR.PEMPTY` (bit 17) on the STM32L4.
106///
107/// Records whether user flash read as empty at the last reset. While it is
108/// set, the part boots its system ROM bootloader instead of the image, so it
109/// must be clear after programming an erased part; see [`clear_l4_pempty`].
110const L4_FLASH_SR_PEMPTY: u32 = 1 << 17;
111/// Start of internal flash, where every tag image's vector table sits.
112const FLASH_BASE: u64 = 0x0800_0000;
113
114/// Which probe-rs driver drives a base.
115///
116/// Both drivers yield the same probe-rs [`Probe`], so everything above
117/// [`open_probe`] is driver-independent. Choose with [`Driver::for_identity`].
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub enum Driver {
120    /// probe-rs's own ST-LINK driver: the base answers the identify command,
121    /// so it has the ST-LINK fixes.
122    Stock,
123    /// [`FieldSafeProbe`]: the base predates the identify command.
124    FieldSafe,
125}
126
127impl Driver {
128    /// Returns the driver for a base with this identity (from [`identify`]).
129    ///
130    /// Any identity string selects [`Driver::Stock`]: the identify command
131    /// shipped in the same base firmware as the ST-LINK fixes the stock driver
132    /// needs. `None` -- a base that predates the command, or a genuine
133    /// ST-LINK -- selects [`Driver::FieldSafe`], which works on every base.
134    pub fn for_identity(identity: Option<&str>) -> Self {
135        if identity.is_some() {
136            Driver::Stock
137        } else {
138            Driver::FieldSafe
139        }
140    }
141
142    /// Returns a short human-readable description, e.g. for a status line.
143    pub fn name(self) -> &'static str {
144        match self {
145            Driver::Stock => "probe-rs ST-LINK driver",
146            Driver::FieldSafe => "field-safe driver",
147        }
148    }
149}
150
151/// Lists the ST-LINK-compatible probes on USB (bases and genuine ST-LINKs
152/// alike).
153///
154/// Uses tagcore's [`LinkAdapt::available`], the enumeration the monitor tools
155/// use, so the [`UsbDev`] values match theirs. No probe is opened for debug
156/// and no tag is reset.
157pub fn list() -> Vec<UsbDev> {
158    LinkAdapt::new().available()
159}
160
161/// Asks a base who it is with the bases' own `F6 99` identify command.
162///
163/// Returns the identity string (see [`LinkAdapt::base_identity`] for its
164/// format), or `None` for a base that predates the command, a genuine
165/// ST-LINK, or a base that cannot be opened (for example because another
166/// process holds it). Opens the base without touching nRST or SWD, so the tag
167/// is not reset.
168///
169/// Blocks for the identify reply's timeout (a few hundred milliseconds) when
170/// the base does not answer, because some old base firmware never replies.
171pub fn identify(usbdev: &UsbDev) -> Option<String> {
172    let mut link = LinkAdapt::new();
173    if !link.open(usbdev) {
174        return None;
175    }
176    let identity = link.base_identity();
177    link.detach();
178    identity
179}
180
181/// What a connect check found (`STM32_Programmer_CLI -c port=SWD mode=UR`).
182///
183/// Returned by [`connect_check`].
184#[derive(Debug, Clone, PartialEq)]
185pub struct Connect {
186    /// DBGMCU device ID, the low 12 bits of `DBGMCU_IDCODE`, e.g.
187    /// [`DEV_L432`]. Any plausible ID is reported, including parts this crate
188    /// has no target for; [`chip_for`] is where those are refused.
189    pub device_id: u16,
190    /// The part family's name, as STM32CubeProgrammer prints it; see
191    /// [`device_name`].
192    pub device_name: &'static str,
193    /// Target supply voltage measured by the base, in volts; `None` if the
194    /// base's voltage command failed.
195    pub voltage: Option<f32>,
196}
197
198/// Returns the family name for a DBGMCU device ID, as STM32CubeProgrammer
199/// prints it, or `"unknown STM32"` for an ID this crate does not know.
200pub fn device_name(device_id: u16) -> &'static str {
201    match device_id {
202        DEV_L432 => "STM32L43xxx/STM32L44xxx",
203        DEV_U375 => "STM32U37xx/STM32U38xx",
204        _ => "unknown STM32",
205    }
206}
207
208/// Returns the probe-rs target name for a device, from its ID and the flash
209/// size its image was linked for (`__flash0_end__ - __flash0_base__`).
210///
211/// The device ID does not give the flash size, and probe-rs's flash layout and
212/// chip erase depend on it (a KC target on a 128 KB KB would address flash
213/// that is not there), so the caller supplies the size -- from the tag's
214/// `TagInfo.intflashsz` or the image's linker symbols -- to choose the
215/// variant:
216///
217/// | Device | `flash_bytes` | Target |
218/// | --- | --- | --- |
219/// | [`DEV_L432`] | `None` or 256 KB | `STM32L432KC` |
220/// | [`DEV_L432`] | 128 KB | `STM32L432KB` |
221/// | [`DEV_U375`] | `None` or 1 MB | `STM32U375RG` |
222/// | [`DEV_U375`] | 512 KB | `STM32U375RE` |
223///
224/// `None` (an image without the symbols, or no image at all) takes the larger
225/// part.
226///
227/// # Errors
228///
229/// A message naming the device ID, and the size if one was given, for any
230/// other combination; nothing is guessed.
231pub fn chip_for(device_id: u16, flash_bytes: Option<u32>) -> Result<&'static str, String> {
232    match (device_id, flash_bytes) {
233        (DEV_L432, None | Some(0x40000)) => Ok("STM32L432KC"),
234        (DEV_L432, Some(0x20000)) => Ok("STM32L432KB"),
235        (DEV_U375, None | Some(0x100000)) => Ok("STM32U375RG"),
236        (DEV_U375, Some(0x80000)) => Ok("STM32U375RE"),
237        (id, Some(size)) => Err(format!(
238            "no probe-rs target for device 0x{id:03x} with {} KB of flash",
239            size / 1024
240        )),
241        (id, None) => Err(format!("no probe-rs target for device 0x{id:03x}")),
242    }
243}
244
245/// Holds the tag in reset, reads its device ID and the supply voltage, and
246/// releases it: the connect check. The tag reboots afterwards.
247///
248/// The counterpart of qtprogram's `STM32_Programmer_CLI -c port=SWD mode=UR`
249/// call. It uses tagcore's [`LinkAdapt`] directly rather than a probe-rs
250/// session, so it is the same on every base generation and needs no target
251/// name: the ID is read from the L4 `DBGMCU_IDCODE` address and then the U3
252/// one, and the first plausible value (low 12 bits neither all zeros nor all
253/// ones) wins.
254///
255/// Whatever the tag was doing is ended by the reset, and releasing nRST at the
256/// end boots its firmware from the start.
257///
258/// # Errors
259///
260/// - `"Cannot open the base"` if the base cannot be opened or attached;
261/// - `"No STM32 target found"` if neither ID address reads plausibly (no tag
262///   on the cradle, an unpowered tag, or an unrecognised part).
263pub fn connect_check(usbdev: &UsbDev) -> Result<Connect, String> {
264    let mut link = LinkAdapt::new();
265    if !link.attach_with_reset(true, usbdev) {
266        return Err("Cannot open the base".into());
267    }
268    let voltage = link.voltage();
269    let plausible = |id: u32| (id & 0xfff) != 0 && (id & 0xfff) != 0xfff;
270    let id = [L4_DBGMCU_IDCODE, U3_DBGMCU_IDCODE]
271        .iter()
272        .find_map(|a| link.read_debug32(*a).filter(|v| plausible(*v)));
273    link.assert_reset(true);
274    link.detach();
275    match id {
276        Some(idcode) => {
277            let device_id = (idcode & 0xfff) as u16;
278            Ok(Connect {
279                device_id,
280                device_name: device_name(device_id),
281                voltage,
282            })
283        }
284        None => Err("No STM32 target found".into()),
285    }
286}
287
288/// The attach/detach sequence used for every session: the ARM default, which
289/// writes nothing to DBGMCU and on detach clears DHCSR.C_DEBUGEN and DEMCR.
290///
291/// probe-rs's built-in STM32 sequences (`Stm32Armv7` for the L4,
292/// `Stm32Armv8` for the U3) set DBGMCU_CR's debug-in-Stop/Standby bits on
293/// attach and never clear C_DEBUGEN. The default sequence leaves the tag as a
294/// normal boot would, so nothing about its low-power state depends on having
295/// been programmed. (Joulescope measurements after each kind of attach found
296/// no difference in resting current; see the evaluation document. Note that
297/// every base sets C_DEBUGEN on SWD entry regardless.)
298#[derive(Debug)]
299pub struct TagSequence;
300
301impl ArmDebugSequence for TagSequence {}
302
303/// Opens `usbdev` with `driver`, as a probe-rs [`Probe`].
304///
305/// Nothing is attached yet: the tag is not reset until the caller attaches.
306///
307/// For [`Driver::Stock`] the base is found among the ST-LINKs by USB vendor
308/// and product ID and then by serial number. If the serial does not match (or
309/// `usbdev` has none) and there is exactly one candidate, that one is used.
310///
311/// The Stock path enumerates and opens through [`StLinkFactory`] **only, never
312/// probe-rs's `Lister`**. `Lister` also enumerates CMSIS-DAP probes over
313/// hidapi, and on macOS hidapi binds its `IOHIDManager` to the first thread
314/// that used it; a second programming run from another worker thread (as
315/// tagprogram-rs does) then aborted the whole process. Going back to `Lister`
316/// for convenience brings that crash back (fixed in commit 36c06ce2).
317///
318/// # Errors
319///
320/// The probe driver's message if the base cannot be opened, or `"Cannot tell
321/// this base from the other ST-LINKs"` when several ST-LINKs are connected
322/// and none matches `usbdev`'s serial number.
323pub fn open_probe(usbdev: &UsbDev, driver: Driver) -> Result<Probe, String> {
324    match driver {
325        Driver::FieldSafe => FieldSafeProbe::open(usbdev).map_err(|e| e.to_string()),
326        Driver::Stock => {
327            // Only the ST-LINK driver, never Lister: listing every kind of
328            // probe enumerates CMSIS-DAP over HID, and on macOS hidapi ties
329            // its IOHIDManager to the first thread that used it, so a second
330            // program from another worker thread aborted the process.
331            let probes: Vec<_> = StLinkFactory
332                .list_probes()
333                .into_iter()
334                .map(|item| item.info)
335                .filter(|p| p.vendor_id == usbdev.vid && p.product_id == usbdev.pid)
336                .collect();
337            let info = match probes.iter().find(|p| {
338                !usbdev.serial.is_empty() && p.serial_number.as_deref() == Some(&usbdev.serial)
339            }) {
340                Some(p) => p,
341                None if probes.len() == 1 => &probes[0],
342                None => return Err("Cannot tell this base from the other ST-LINKs".into()),
343            };
344            let selector = DebugProbeSelector {
345                vendor_id: info.vendor_id,
346                product_id: info.product_id,
347                interface: None,
348                serial_number: info.serial_number.clone(),
349            };
350            StLinkFactory
351                .open(&selector)
352                .map(Probe::from_specific_probe)
353                .map_err(|e| e.to_string())
354        }
355    }
356}
357
358/// Opens a session on `chip` under reset, with [`TagSequence`].
359///
360/// probe-rs's attach under reset holds nRST, attaches, arms reset vector catch
361/// and releases nRST, so the core is halted at its reset vector before any
362/// firmware instruction runs. Dropping the [`Session`] detaches through
363/// [`TagSequence`], which lets the core run.
364///
365/// # Errors
366///
367/// A message if probe-rs has no target named `chip`, the probe cannot be
368/// opened ([`open_probe`]), or the attach fails.
369fn session(usbdev: &UsbDev, driver: Driver, chip: &str) -> Result<Session, String> {
370    let registry = Registry::from_builtin_families();
371    let mut target = registry
372        .get_target_by_name(chip)
373        .map_err(|e| format!("probe-rs has no target {chip}: {e}"))?;
374    target.debug_sequence = DebugSequence::Arm(Arc::new(TagSequence));
375    open_probe(usbdev, driver)?
376        .attach_under_reset_with_registry(
377            TargetSelector::Specified(target),
378            Permissions::default(),
379            &registry,
380        )
381        .map_err(|e| format!("Cannot attach to the tag: {e}"))
382}
383
384/// The internal-flash erase that goes with a program.
385///
386/// Every variant also erases whatever sectors the image itself covers; they
387/// differ in what else is cleared. Internal flash only: a tag's external
388/// flash log is never touched here.
389#[derive(Debug, Clone, Copy, PartialEq, Eq)]
390pub enum Erase {
391    /// Only the sectors the image covers (qtprogram's "none").
392    ///
393    /// Anything outside the image, such as calibration constants stored
394    /// above it, is kept.
395    ImagePages,
396    /// Flash `start..end` first (qtprogram's "below calibration").
397    ///
398    /// Erases the sectors overlapping the range, then programs as
399    /// [`Erase::ImagePages`] in the same session.
400    Range {
401        /// First address to erase, inclusive (an absolute flash address,
402        /// e.g. `0x0800_0000`).
403        start: u64,
404        /// End of the range, exclusive.
405        end: u64,
406    },
407    /// The whole chip (qtprogram's "all").
408    ///
409    /// Destroys any calibration stored in internal flash.
410    All,
411}
412
413/// Progress during [`program`].
414///
415/// Delivered on the calling thread, in order, from inside probe-rs's flash
416/// loop.
417#[derive(Debug, Clone, PartialEq, Eq)]
418pub enum Progress {
419    /// An operation started: "Reading back", "Erasing", "Programming" or
420    /// "Verifying".
421    Step(&'static str),
422    /// Bytes done of a total, for the step under way.
423    ///
424    /// `total` is 0 when probe-rs did not announce a size for the step.
425    Bytes {
426        /// Bytes completed so far in this step.
427        done: u64,
428        /// Bytes the step will cover, or 0 if unknown.
429        total: u64,
430    },
431}
432
433/// Returns the step name reported in [`Progress::Step`] for a probe-rs flash
434/// operation.
435///
436/// `Fill` is probe-rs reading back flash it must preserve inside a sector it
437/// is about to erase, hence "Reading back".
438fn operation_name(op: ProgressOperation) -> &'static str {
439    match op {
440        ProgressOperation::Fill => "Reading back",
441        ProgressOperation::Erase => "Erasing",
442        ProgressOperation::Program => "Programming",
443        ProgressOperation::Verify => "Verifying",
444    }
445}
446
447/// Erases as `erase` says, programs and verifies `elf` in one session, clears
448/// FLASH_SR.PEMPTY on an STM32L4 if it is set, and starts the image with a
449/// debugger jump to its vector table (STM32_Programmer_CLI's `-g
450/// 0x08000000`). One session, so no reset falls between the erase and the
451/// program.
452///
453/// The counterpart of qtprogram's STM32_Programmer_CLI program call. `chip`
454/// comes from [`chip_for`]; `on_progress` is called synchronously with
455/// [`Progress`] events. Blocks until the image is running or an error ends
456/// the session; a full L432 image takes seconds, longer through
457/// [`Driver::FieldSafe`].
458///
459/// The tag is held in reset to attach, so whatever it was doing ends. The
460/// image is started by setting SP and PC from the vector table and resuming
461/// the core, not by a reset, so the first boot after programming is not a
462/// cold boot; the tag's next reset boots it normally.
463///
464/// # Errors
465///
466/// A message if the attach fails, the range erase fails, programming or
467/// verification fails, PEMPTY will not clear, or the core cannot be started.
468/// A failure after the erase leaves the tag with partly erased or programmed
469/// flash, which another program call repairs.
470pub fn program(
471    usbdev: &UsbDev,
472    driver: Driver,
473    chip: &str,
474    elf: &Path,
475    erase: Erase,
476    mut on_progress: impl FnMut(Progress),
477) -> Result<(), String> {
478    let mut session = session(usbdev, driver, chip)?;
479    let mut totals = std::collections::HashMap::new();
480    let mut done = std::collections::HashMap::new();
481    {
482        let mut progress = FlashProgress::new(|event| match event {
483            ProgressEvent::AddProgressBar { operation, total } => {
484                totals.insert(operation as u8, total.unwrap_or(0));
485            }
486            ProgressEvent::Started(op) => {
487                done.insert(op as u8, 0u64);
488                on_progress(Progress::Step(operation_name(op)));
489            }
490            ProgressEvent::Progress {
491                operation, size, ..
492            } => {
493                let d = done.entry(operation as u8).or_insert(0);
494                *d += size;
495                on_progress(Progress::Bytes {
496                    done: *d,
497                    total: *totals.get(&(operation as u8)).unwrap_or(&0),
498                });
499            }
500            _ => {}
501        });
502        if let Erase::Range { start, end } = erase {
503            flashing::erase(&mut session, &mut progress, start, end, false)
504                .map_err(|e| format!("Erase failed: {e}"))?;
505        }
506        let mut options = DownloadOptions::default();
507        options.verify = true;
508        options.do_chip_erase = erase == Erase::All;
509        options.progress = progress;
510        flashing::download_file_with_options(
511            &mut session,
512            elf,
513            ElfLoader(Default::default()),
514            options,
515        )
516        .map_err(|e| format!("Programming failed: {e}"))?;
517    }
518    let mut core = session.core(0).map_err(|e| e.to_string())?;
519    clear_l4_pempty(&mut core)?;
520    // Start as `-g 0x08000000` does: SP and PC from the vector table.
521    let sp = core.read_word_32(FLASH_BASE).map_err(|e| e.to_string())?;
522    let pc = core
523        .read_word_32(FLASH_BASE + 4)
524        .map_err(|e| e.to_string())?;
525    let sp_reg = core.stack_pointer();
526    let pc_reg = core.program_counter();
527    core.write_core_reg(sp_reg, sp).map_err(|e| e.to_string())?;
528    core.write_core_reg(pc_reg, pc & !1)
529        .map_err(|e| e.to_string())?;
530    core.run()
531        .map_err(|e| format!("Cannot start the image: {e}"))?;
532    Ok(())
533}
534
535/// On an STM32L4, clears FLASH_SR.PEMPTY if it is set. Writing 1 toggles the
536/// bit, so it is written only when found set, then read back; the rule of
537/// `embedded/tools/flash_release.py`. Does nothing on other parts.
538///
539/// The part is recognised by reading the L4 `DBGMCU_IDCODE` address; a read
540/// that fails (as it may on a U3) counts as "not an L4".
541///
542/// # Errors
543///
544/// A message if FLASH_SR cannot be read or written, or if PEMPTY is still set
545/// after the write, in which case the tag would boot its ROM bootloader.
546fn clear_l4_pempty(core: &mut probe_rs::Core<'_>) -> Result<(), String> {
547    let l4 = core
548        .read_word_32(u64::from(L4_DBGMCU_IDCODE))
549        .is_ok_and(|id| (id & 0xfff) as u16 == DEV_L432);
550    if !l4 {
551        return Ok(());
552    }
553    let sr = core.read_word_32(L4_FLASH_SR).map_err(|e| e.to_string())?;
554    if sr & L4_FLASH_SR_PEMPTY == 0 {
555        return Ok(());
556    }
557    core.write_word_32(L4_FLASH_SR, L4_FLASH_SR_PEMPTY)
558        .map_err(|e| e.to_string())?;
559    let after = core.read_word_32(L4_FLASH_SR).map_err(|e| e.to_string())?;
560    if after & L4_FLASH_SR_PEMPTY != 0 {
561        return Err("FLASH_SR.PEMPTY is still set; the tag would boot its ROM bootloader".into());
562    }
563    Ok(())
564}
565
566/// Reads `len` bytes of tag memory at `addr` in a session under reset (the
567/// core is held at its reset vector, so nothing runs while it is read).
568///
569/// `addr` is an absolute target address (flash at `0x0800_0000`, SRAM, or a
570/// register), at any alignment; `chip` comes from [`chip_for`]. Because the
571/// core is halted at the reset vector, SRAM holds what survived the reset,
572/// not what the firmware would initialise. Through [`Driver::FieldSafe`],
573/// unaligned edges are read as whole words, so reading a peripheral register
574/// with read side effects can disturb its neighbours.
575///
576/// When the session closes the core is released and the tag boots its
577/// firmware, as STM32_Programmer_CLI leaves it; the call waits 50 ms after
578/// that before returning.
579///
580/// # Errors
581///
582/// A message if the attach fails or the read faults (an unmapped address,
583/// for example).
584pub fn read_memory(
585    usbdev: &UsbDev,
586    driver: Driver,
587    chip: &str,
588    addr: u64,
589    len: usize,
590) -> Result<Vec<u8>, String> {
591    let mut session = session(usbdev, driver, chip)?;
592    let mut core = session.core(0).map_err(|e| e.to_string())?;
593    let mut buf = vec![0u8; len];
594    core.read_8(addr, &mut buf)
595        .map_err(|e| format!("Read failed: {e}"))?;
596    drop(core);
597    drop(session);
598    // Leave the tag running its firmware, as STM32_Programmer_CLI does.
599    std::thread::sleep(Duration::from_millis(50));
600    Ok(buf)
601}
602
603/// Hardware-free checks of target and driver selection.
604#[cfg(test)]
605mod tests {
606    use super::*;
607
608    /// Checks that [`chip_for`] maps device ID and flash size to the right
609    /// target and refuses an unknown device.
610    #[test]
611    fn chips_follow_device_and_flash_size() {
612        assert_eq!(chip_for(DEV_L432, Some(0x40000)), Ok("STM32L432KC"));
613        assert_eq!(chip_for(DEV_U375, Some(0x100000)), Ok("STM32U375RG"));
614        assert!(chip_for(0x123, None).is_err());
615    }
616
617    /// Checks that any identity string selects the stock driver and none
618    /// selects the field-safe one.
619    #[test]
620    fn driver_follows_identity() {
621        assert_eq!(Driver::for_identity(Some("base=x")), Driver::Stock);
622        assert_eq!(Driver::for_identity(None), Driver::FieldSafe);
623    }
624}