opentitanlib/io/
fpga_backdoor.rs

1// Copyright lowRISC contributors (OpenTitan project).
2// Licensed under the Apache License, Version 2.0, see LICENSE for details.
3// SPDX-License-Identifier: Apache-2.0
4
5use anyhow::{Context, Result, bail, ensure};
6use clap::Args;
7use serde::ser::{Serialize, SerializeStruct, Serializer};
8use std::time::{Duration, Instant};
9
10use crate::app::TransportWrapper;
11use crate::debug::dmi::{Dmi, OpenOcdDmi};
12use crate::io::jtag::{JtagChain, JtagParams, JtagTap};
13use crate::transport::Capability;
14use crate::util::vmem::Word;
15
16/// FPGA Backdoor loader register offsets (byte-addressed) and field definitions.
17/// See hw/ip/bkdr_loader/doc/registers.md
18/// TODO: it would be nice to use Bazel to auto-generate a rust "header" for this IP instead.
19pub mod regs {
20
21    // STATUS register
22    pub const STATUS_REG_OFFSET: usize = 0x0;
23    pub const STATUS_ERROR_BIT: u32 = 0;
24    pub const STATUS_CLEAR_IDLE_BIT: u32 = 1;
25
26    // CONTROL register
27    pub const CONTROL_REG_OFFSET: usize = 0x4;
28    pub const CONTROL_DONE_BIT: u32 = 0;
29    pub const CONTROL_WRITE_ENA_BIT: u32 = 1;
30    pub const CONTROL_CLEAR_START_BIT: u32 = 2;
31    pub const CONTROL_TARGET_IDX_MASK: u32 = 0xff;
32    pub const CONTROL_TARGET_IDX_OFFSET: usize = 8;
33
34    // Other registers (all have one 32-bit `VAL` field)
35    pub const NUM_BKDR_TARGETS_REG_OFFSET: usize = 0x8;
36    pub const MISSION_MODE_SWITCH_DELAY_REG_OFFSET: usize = 0xc;
37    pub const USR_ACCESS_TIMESTAMP_REG_OFFSET: usize = 0x10;
38    pub const TARGET_INFO_0_REG_OFFSET: usize = 0x100;
39    pub const WIDTH_INFO_0_REG_OFFSET: usize = 0x200;
40    pub const DEPTH_INFO_0_REG_OFFSET: usize = 0x300;
41    pub const READ_DATA_0_REG_OFFSET: usize = 0x400;
42    pub const WRITE_DATA_0_REG_OFFSET: usize = 0x500;
43    pub const INDEX_REG_OFFSET: usize = 0x600;
44}
45
46pub mod consts {
47    // How long the reset strapping is applied for when entering the backdoor loader.
48    pub const RESET_PULSE_MS: u64 = 50;
49
50    // How long the backdoor loader TAP strapping is held after leaving reset.
51    pub const HOLD_TAP_STRAPS_MS: u64 = 50;
52
53    // Time to wait for a clear operation to finish.
54    pub const CLEAR_TIMEOUT_SECS: u64 = 5;
55
56    // How many JTAG cycles to wait for before considering the `CONTROL.DONE` transaction
57    // as being completed. We default to 10000, which is a conservative threshold.
58    pub const JTAG_DONE_CYCLES: u64 = 10000;
59
60    // FIXME: This should be refactored so that the ot_transport JSON5 file declares clock
61    // frequencies for each device which we can then query through the transport. This
62    // is hardcoded for now for convenience.
63    pub const CW340_MAIN_CLOCK_FREQ_HZ: u64 = 24 * 1000 * 1000; // 24 MHz
64
65    // Parameters - see hw/ip/bkdr_loader/doc/interfaces.md
66    pub const DATA_REGS_PER_WORD: usize = 8; // MaxWordWidthDiv32
67}
68
69use consts::*;
70
71/// Apply the bkdr_loader TAP strapping and reset to enter the backdoor loader.
72pub fn enter_backdoor_loader(transport: &TransportWrapper) -> Result<()> {
73    transport.capabilities()?.request(Capability::GPIO).ok()?;
74    let pinmux_tap_backdoor = transport.pin_strapping("PINMUX_TAP_FPGA_BACKDOOR")?;
75    let reset = transport.pin_strapping("RESET")?;
76
77    pinmux_tap_backdoor.apply()?;
78    reset.apply()?;
79    std::thread::sleep(Duration::from_millis(RESET_PULSE_MS));
80    reset.remove()?;
81    std::thread::sleep(Duration::from_millis(HOLD_TAP_STRAPS_MS));
82    pinmux_tap_backdoor.remove()
83}
84
85/// A struct which represents a backdoor loader interface.
86///
87/// This struct represents an adaptor that has been configured to connect to a given JTAG chain,
88/// but has not yet been configured to access the backdoor TAP.
89pub struct BackdoorTap<'a> {
90    jtag: Box<dyn JtagChain + 'a>,
91    jtag_speed_khz: u64,
92}
93
94impl BackdoorTap<'_> {
95    /// Connect to the backdoor TAP, optionally enumerate information about all targets.
96    pub fn connect(self, enumerate: bool) -> Result<Backdoor> {
97        let openocd = self.jtag.connect(JtagTap::BackdoorTap)?.into_raw()?;
98        Backdoor::new(
99            OpenOcdDmi::new(openocd, "fpga_backdoor.tap")?,
100            self.jtag_speed_khz,
101            enumerate,
102        )
103    }
104}
105
106#[derive(Debug, Args, Clone)]
107pub struct BackdoorParams {
108    /// JTAG options to apply to the backdoor TAP.
109    #[command(flatten)]
110    pub jtag: JtagParams,
111}
112
113impl BackdoorParams {
114    pub fn create<'a>(&self, transport: &'a TransportWrapper) -> Result<BackdoorTap<'a>> {
115        Ok(BackdoorTap {
116            jtag: self.jtag.create(transport)?,
117            jtag_speed_khz: self.jtag.adapter_speed_khz,
118        })
119    }
120}
121
122/// Information about a specific backdoor target, e.g. OTP, ROM, FB0, SRAM.
123#[derive(Debug, Clone, Copy)]
124pub struct BackdoorTargetInfo {
125    /// The unique identifier of the backdoor target
126    pub id: u32,
127    /// The word width of the memory of the backdoor target.
128    pub width: u32,
129    /// The depth (number of words) of the memory of the backdoor target.
130    pub depth: u32,
131}
132
133impl BackdoorTargetInfo {
134    /// The target's unique identifier as a <= 4 character UTF-8 string.
135    pub fn id_str(&self) -> String {
136        let bytes = self.id.to_be_bytes();
137
138        String::from_utf8_lossy(&bytes).trim_end().to_owned()
139    }
140
141    // Convert a UTF-8 ID string into the unique u32 identifier format used by targets.
142    pub fn id_from_str(id: &str) -> Result<u32> {
143        let mut bytes = [32u8; 4];
144        let src = id.as_bytes();
145        let len = id.len().min(4);
146        bytes[..len].copy_from_slice(&src[..len]);
147
148        Ok(u32::from_be_bytes(bytes))
149    }
150}
151
152impl Serialize for BackdoorTargetInfo {
153    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
154    where
155        S: Serializer,
156    {
157        let mut s = serializer.serialize_struct("BackdoorTargetInfo", 4)?;
158        s.serialize_field("id", &self.id)?;
159        s.serialize_field("id_str", &self.id_str())?;
160        s.serialize_field("width", &self.width)?;
161        s.serialize_field("depth", &self.depth)?;
162        s.end()
163    }
164}
165
166impl std::fmt::Display for BackdoorTargetInfo {
167    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
168        write!(f, "{} {} x {}", self.id_str(), self.width, self.depth)
169    }
170}
171
172impl Word {
173    /// Convert the word to a series of 32-bit chunks to be written to the data registers.
174    fn to_u32_chunks(&self) -> Result<[u32; DATA_REGS_PER_WORD]> {
175        ensure!(
176            self.bytes.len() <= DATA_REGS_PER_WORD * 4,
177            "Word '{}' with {} bytes will not fit into {} 32-bit registers.",
178            hex::encode(self.bytes.clone()),
179            self.bytes.len(),
180            DATA_REGS_PER_WORD
181        );
182        let mut chunks = [0u32; DATA_REGS_PER_WORD];
183
184        // Bytes are stored in Big Endian; when written to registers, the u32
185        // chunks are provided in LSB-first order (Little Endian).
186        for (i, &b) in self.bytes.iter().rev().enumerate() {
187            // Within u32 chunks, bytes are still given in MSB-first order (Big Endian).
188            let chunk_idx = i / 4;
189            let byte_pos = i % 4;
190            chunks[chunk_idx] |= (b as u32) << (byte_pos * 8);
191        }
192
193        Ok(chunks)
194    }
195
196    /// Convert the 32-bit chunks read from data registers into a word (MSB-first byte stream).
197    fn from_u32_chunks(chunks: &[u32; DATA_REGS_PER_WORD], bytes_per_word: usize) -> Self {
198        let num_chunks = bytes_per_word.div_ceil(size_of::<u32>());
199        let padding_bytes = (num_chunks * size_of::<u32>()) - bytes_per_word;
200
201        Self {
202            bytes: chunks
203                .iter()
204                .take(num_chunks)
205                .rev()
206                .flat_map(|chunk| chunk.to_be_bytes())
207                .skip(padding_bytes)
208                .collect(),
209        }
210    }
211}
212
213/// Handle for interacting with a given target via the backdoor loader.
214pub struct BackdoorTarget<'a> {
215    backdoor: &'a mut Backdoor,
216    index: u8,
217    /// Information about the target.
218    pub info: BackdoorTargetInfo,
219}
220
221impl<'a> BackdoorTarget<'a> {
222    /// Write a sequence of words at a given offset (word index) in the target's memory.
223    ///
224    /// The `write_all` parameter is used to control whether writes can be optimized by
225    /// maintaining shadow CSRs to determine when register contents have genuinely changed.
226    /// The `check_status` parameter is used to control whether the status bit is polled
227    /// after all words are written, to check for any errors.
228    pub fn write(
229        &mut self,
230        start: u32,
231        words: &[Word],
232        write_all: bool,
233        check_status: bool,
234    ) -> Result<()> {
235        ensure!(
236            start + words.len() as u32 <= self.info.depth,
237            "fpga bkdr_loader write of len {:#x} to word {:#x} of {} is out of bounds (depth: {:#x})",
238            words.len(),
239            start,
240            self.info.id_str(),
241            self.info.depth,
242        );
243        self.backdoor
244            .write_target(self.index, start, words, write_all, check_status)
245    }
246
247    /// Read a sequence of words at a given offset (word index) from the target's memory.
248    ///
249    /// The `check_status` parameter is used to control whether the status bit is polled
250    /// after each word read, to check for any errors.
251    pub fn read(&mut self, start: u32, count: u32, check_status: bool) -> Result<Vec<Word>> {
252        ensure!(
253            start + count <= self.info.depth,
254            "fpga bkdr_loader read of len {:#x} to word {:#x} of {} is out of bounds (depth: {:#x})",
255            count,
256            start,
257            self.info.id_str(),
258            self.info.depth,
259        );
260        self.backdoor
261            .read_target(self.index, start, count, check_status)
262    }
263
264    /// Clear the entire memory of the target with a given word.
265    ///
266    /// An optimized fast-path for clearing memories, primarily used to replicate existing
267    /// bitstream synthesis defaults. The `check_status` parameter is used to control
268    /// whether the status bit is polled after clearing, to check for any errors.
269    pub fn clear(&mut self, word: &Word, check_status: bool) -> Result<()> {
270        self.backdoor.clear_target(self.index, word, check_status)
271    }
272}
273
274/// A struct which represents an active backdoor loader connection.
275pub struct Backdoor {
276    dmi: OpenOcdDmi,
277    jtag_speed_khz: u64,
278    targets: Vec<BackdoorTargetInfo>,
279}
280
281impl Backdoor {
282    /// Construct a [`Backdoor`] from a DMI connection to the backdoor TAP. Optionally
283    /// enumerate and discover information about all available targets.
284    pub fn new(dmi: OpenOcdDmi, jtag_speed_khz: u64, enumerate: bool) -> Result<Self> {
285        let mut fpga_backdoor = Self {
286            dmi,
287            jtag_speed_khz,
288            targets: Vec::new(),
289        };
290        if enumerate {
291            fpga_backdoor.enumerate()?;
292        }
293
294        Ok(fpga_backdoor)
295    }
296
297    /// Read from a DMI register with the given byte address offset.
298    /// DMI is a register interface; we must map the byte offsets to register (word) index.
299    fn dmi_read(&mut self, byte_addr: usize) -> Result<u32> {
300        self.dmi.dmi_read(byte_addr as u32 >> 2)
301    }
302
303    /// Write a value to a DMI register with the given byte address offset.
304    /// DMI is a register interface; we must map the byte offsets to register (word) index.
305    fn dmi_write(&mut self, byte_addr: usize, data: u32) -> Result<()> {
306        self.dmi.dmi_write(byte_addr as u32 >> 2, data)
307    }
308
309    // Enumerate the backdoor loader and retrieve information about available targets.
310    pub fn enumerate(&mut self) -> Result<()> {
311        self.targets.clear();
312
313        let num_targets = self
314            .dmi_read(regs::NUM_BKDR_TARGETS_REG_OFFSET)
315            .context("cannot read number of targets")? as usize;
316        log::info!("Number of FPGA bkdr_loader targets: {num_targets:?}");
317        for idx in 0..num_targets {
318            let addr_offset = idx * 4;
319            let target_info = BackdoorTargetInfo {
320                id: self
321                    .dmi_read(regs::TARGET_INFO_0_REG_OFFSET + addr_offset)
322                    .context("cannot read target info")?,
323                width: self
324                    .dmi_read(regs::WIDTH_INFO_0_REG_OFFSET + addr_offset)
325                    .context("cannot read width info")?,
326                depth: self
327                    .dmi_read(regs::DEPTH_INFO_0_REG_OFFSET + addr_offset)
328                    .context("cannot read depth info")?,
329            };
330            self.targets.push(target_info);
331        }
332
333        Ok(())
334    }
335
336    /// Communicate with the backdoor loader that we are finished using it.
337    ///
338    /// This transitions the bkdr_loader from it from its "Preload" state to "Mission mode",
339    /// causing it to re-route incoming JTAG back to the regular downstream interface.
340    pub fn set_done(mut self) -> Result<()> {
341        log::debug!("Finished using backdoor loader until next reset");
342
343        // We don't want the bkdr_loader to re-route JTAG mid-transaction, since that will
344        // cause us to see an unexpected response, as we will then be talking to an entirely
345        // different DMI / DTM (which can also put the RV_DM into a bad state). It will also
346        // potentially put the RV_dM debug infrastructure into a bad state. Configure the
347        // bkdr_loader to wait long enough so that we can finish our JTAG transaction.
348        // FIXME: These calculations are specific to the CW340.
349        let jtag_freq_hz: u64 = self.jtag_speed_khz * 1000;
350        let soc_clk_wait_cycles =
351            CW340_MAIN_CLOCK_FREQ_HZ.div_ceil(jtag_freq_hz) * JTAG_DONE_CYCLES;
352        let soc_clk_wait_cycles: u32 = soc_clk_wait_cycles.try_into().unwrap_or_else(|_| {
353            log::warn!(
354                "Configured JTAG speed ({} kHz) may overflow bkdr_loader wait time.",
355                self.jtag_speed_khz
356            );
357            log::warn!("Configuring maximum wait time.");
358            u32::MAX
359        });
360        self.dmi_write(
361            regs::MISSION_MODE_SWITCH_DELAY_REG_OFFSET,
362            soc_clk_wait_cycles,
363        )
364        .context("cannot write FPGA bkdr_loader mission_mode_switch_delay register")?;
365
366        self.dmi_write(regs::CONTROL_REG_OFFSET, 0b1 << regs::CONTROL_DONE_BIT)
367            .context("cannot write done to FPGA bkdr_loader control reg")?;
368
369        // Wait until the transition to mission mode is complete and the system exits reset
370        // before continuing. For most sensible JTAG speeds this should be basically instant;
371        // for very slow speeds (e.g. <= 50 kHz) we need to add some special casing.
372        let done_wait_millis = (JTAG_DONE_CYCLES * 1000).div_ceil(jtag_freq_hz);
373        std::thread::sleep(Duration::from_millis(done_wait_millis));
374
375        Ok(())
376    }
377
378    /// Retrieve information about all of the targets available via the backdoor interface.
379    pub fn targets(&self) -> &[BackdoorTargetInfo] {
380        &self.targets
381    }
382
383    /// Borrow a target by its integer identifier. Only one BackdoorTarget can exist at a time.
384    pub fn target_by_id(&mut self, id: u32) -> Option<BackdoorTarget<'_>> {
385        let (index, info) = self.targets.iter().enumerate().find(|&(_, t)| t.id == id)?;
386        let (index, info) = (index as u8, *info);
387
388        Some(BackdoorTarget {
389            backdoor: self,
390            index,
391            info,
392        })
393    }
394
395    /// Borrow a target by its string identifier. Only one BackdoorTarget can exist at a time.
396    pub fn target_by_id_str(&mut self, id: &str) -> Result<Option<BackdoorTarget<'_>>> {
397        let encoded_id = BackdoorTargetInfo::id_from_str(id)?;
398
399        Ok(self.target_by_id(encoded_id))
400    }
401
402    /// Write a sequence of words at a given offset (word index) to a specified target's memory.
403    ///
404    /// The `write_all` parameter is used to control whether writes can be optimized by
405    /// maintaining shadow CSRs to determine when register contents have actually changed.
406    /// The `check_status` parameter is used to control whether the status bit is polled
407    /// after all words are written, to check for any errors.
408    pub fn write_target(
409        &mut self,
410        target_index: u8,
411        start: u32,
412        words: &[Word],
413        write_all: bool,
414        check_status: bool,
415    ) -> Result<()> {
416        ensure!(
417            usize::from(target_index) < self.targets.len(),
418            "Target index {} is out of range for {} targets",
419            target_index,
420            self.targets.len()
421        );
422        let info = self.targets[target_index as usize];
423        let width = info.width as usize;
424        let regs_used = width.div_ceil(u32::BITS as usize);
425        ensure!(
426            regs_used <= DATA_REGS_PER_WORD,
427            "Advertised target width {:#x} is too wide for the data registers (needs: {:#x}, has: {:#x})",
428            width,
429            regs_used,
430            DATA_REGS_PER_WORD
431        );
432
433        // Cache previous written values in Shadow CSRs
434        let mut prev_regs = [0u32; DATA_REGS_PER_WORD];
435        let mut word_idx = start;
436        let mut first = true;
437
438        let mut control = (target_index as u32) << regs::CONTROL_TARGET_IDX_OFFSET;
439        control |= 0b1 << regs::CONTROL_WRITE_ENA_BIT;
440        self.dmi_write(regs::CONTROL_REG_OFFSET, control)
441            .context("cannot write to control register")?;
442
443        // We batch together all the necessary writes so that we can perform a single
444        // batched write operation at the end, which is optimized for throughput.
445        let mut writes = Vec::new();
446
447        for word in words {
448            let regs = word.to_u32_chunks()?;
449            for idx in 0..regs_used {
450                // Optimization - maintain shadow CSRs in software, and only write the
451                // data if there is a diff in that CSR from the previous contents. Vastly
452                // minimizes required operations for repetitive payloads.
453                if write_all || first || regs[idx] != prev_regs[idx] {
454                    let addr_offset = idx * 4;
455                    writes.push((
456                        ((regs::WRITE_DATA_0_REG_OFFSET + addr_offset) >> 2) as u32,
457                        regs[idx],
458                    ));
459                    prev_regs[idx] = regs[idx];
460                }
461            }
462            writes.push(((regs::INDEX_REG_OFFSET >> 2) as u32, word_idx));
463            first = false;
464            word_idx += 1;
465        }
466
467        self.dmi
468            .batched_dmi_writes(&writes)
469            .context("failed to perform DMI writes")?;
470
471        if check_status {
472            let status = self
473                .dmi_read(regs::STATUS_REG_OFFSET)
474                .context("cannot read status")?;
475            ensure!(
476                status & (0b1 << regs::STATUS_ERROR_BIT) == 0,
477                "fpga bkdr_loader reported an error writing to target {}",
478                info.id_str()
479            );
480        }
481
482        Ok(())
483    }
484
485    /// Read a sequence of words at a given offset (word index) from a specified target's memory.
486    ///
487    /// The `check_status` parameter is used to control whether the status bit is polled
488    /// after each word read, to check for any errors.
489    pub fn read_target(
490        &mut self,
491        target_index: u8,
492        start: u32,
493        count: u32,
494        check_status: bool,
495    ) -> Result<Vec<Word>> {
496        ensure!(
497            usize::from(target_index) < self.targets.len(),
498            "Target index {} is out of range for {} targets",
499            target_index,
500            self.targets.len()
501        );
502        let info = self.targets[target_index as usize];
503        let width = info.width as usize;
504        let bytes_per_word = width.div_ceil(u8::BITS as usize);
505        let regs_used = width.div_ceil(u32::BITS as usize);
506        ensure!(
507            regs_used <= DATA_REGS_PER_WORD,
508            "Advertised target width {:#x} is too wide for the data registers (needs: {:#x}, has: {:#x})",
509            width,
510            regs_used,
511            DATA_REGS_PER_WORD
512        );
513
514        let mut control = (target_index as u32) << regs::CONTROL_TARGET_IDX_OFFSET;
515        control &= !(0b1 << regs::CONTROL_WRITE_ENA_BIT);
516        self.dmi_write(regs::CONTROL_REG_OFFSET, control)
517            .context("cannot write to control register")?;
518
519        let mut words = Vec::new();
520
521        for word_idx in start..(start + count) {
522            self.dmi_write(regs::INDEX_REG_OFFSET, word_idx)
523                .context("cannot write word index")?;
524
525            if check_status {
526                let status = self
527                    .dmi_read(regs::STATUS_REG_OFFSET)
528                    .context("cannot read status")?;
529                ensure!(
530                    status & (0b1 << regs::STATUS_ERROR_BIT) == 0,
531                    "fpga bkdr_loader reported an error reading from word idx {} of target {}",
532                    word_idx,
533                    info.id_str()
534                );
535            }
536
537            let mut regs = [0u32; DATA_REGS_PER_WORD];
538            for (idx, reg) in regs.iter_mut().enumerate().take(regs_used) {
539                let addr_offset = idx * 4;
540                *reg = self
541                    .dmi_read(regs::READ_DATA_0_REG_OFFSET + addr_offset)
542                    .context("cannot read from read_data register")?;
543            }
544
545            words.push(Word::from_u32_chunks(&regs, bytes_per_word));
546        }
547
548        Ok(words)
549    }
550
551    /// Clear the entire memory of a specified target with a given word.
552    ///
553    /// An optimized fast-path for clearing memories, primarily used to replicate existing
554    /// bitstream synthesis defaults. The `check_status` parameter is used to control
555    /// whether the status bit is polled after clearing, to check for any errors.
556    pub fn clear_target(
557        &mut self,
558        target_index: u8,
559        word: &Word,
560        check_status: bool,
561    ) -> Result<()> {
562        ensure!(
563            usize::from(target_index) < self.targets.len(),
564            "Target index {} is out of range for {} targets",
565            target_index,
566            self.targets.len()
567        );
568        let info = self.targets[target_index as usize];
569
570        self.dmi
571            .batched_dmi_writes(
572                &word
573                    .to_u32_chunks()?
574                    .into_iter()
575                    .enumerate()
576                    .map(|(idx, reg)| {
577                        let addr_offset = idx * 4;
578                        (
579                            ((regs::WRITE_DATA_0_REG_OFFSET + addr_offset) >> 2) as u32,
580                            reg,
581                        )
582                    })
583                    .collect::<Vec<_>>(),
584            )
585            .context("failed to perform DMI writes")?;
586
587        let mut control = (target_index as u32) << regs::CONTROL_TARGET_IDX_OFFSET;
588        control |= 0b1 << regs::CONTROL_WRITE_ENA_BIT;
589        control |= 0b1 << regs::CONTROL_CLEAR_START_BIT;
590        self.dmi_write(regs::CONTROL_REG_OFFSET, control)
591            .context("cannot write to control register")?;
592
593        // Wait for the `CLEAR_IDLE` bit to appear set in the status register.
594        let timeout = Instant::now() + Duration::from_secs(CLEAR_TIMEOUT_SECS);
595        let mut status: u32;
596        loop {
597            status = self
598                .dmi_read(regs::STATUS_REG_OFFSET)
599                .context("cannot read status")?;
600            if status & (0b1 << regs::STATUS_CLEAR_IDLE_BIT) != 0 {
601                break;
602            }
603
604            if Instant::now() > timeout {
605                bail!(
606                    "Timed out after {} seconds waiting for {} clear to complete",
607                    CLEAR_TIMEOUT_SECS,
608                    info.id_str()
609                );
610            }
611        }
612
613        if check_status {
614            ensure!(
615                status & (0b1 << regs::STATUS_ERROR_BIT) == 0,
616                "fpga bkdr_loader reported an error writing to target {}",
617                info.id_str()
618            );
619        }
620
621        Ok(())
622    }
623}
624
625#[cfg(test)]
626mod tests {
627    use super::*;
628
629    #[test]
630    fn identifer_str_encoding() {
631        let (width, depth) = (1, 1);
632        for (id, id_str) in [
633            (0x4f545020, "OTP"),
634            (0x5352414d, "SRAM"),
635            (0x46493031, "FI01"),
636        ] {
637            assert_eq!(BackdoorTargetInfo { id, width, depth }.id_str(), id_str);
638            assert_eq!(BackdoorTargetInfo::id_from_str(id_str).unwrap(), id);
639        }
640    }
641
642    #[test]
643    fn byte_u32_conversion() {
644        // Bytes stored in words are Big Endian (MSB first).
645        let word = Word::new(vec![
646            0x5a, 0xa5, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0xbe, 0xef, 0xca, 0xfe,
647        ]);
648        // Register chunks are Little Endian, but bytes in each u32 are Big Endian.
649        // The 4th register should be half-used, the 4 remaining regs should be unused.
650        let mut expected = [0x0; DATA_REGS_PER_WORD];
651        expected[0] = 0xbeefcafe;
652        expected[1] = 0x89abcdef;
653        expected[2] = 0x01234567;
654        expected[3] = 0x00005aa5;
655
656        let chunks = word.to_u32_chunks().unwrap();
657        assert_eq!(chunks, expected);
658        assert_eq!(Word::from_u32_chunks(&chunks, word.bytes.len()), word);
659    }
660}