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 ®istry,
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}