opentitanlib/debug/
dmi.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 std::ops::{Deref, DerefMut};
6use std::time::Duration;
7
8use anyhow::{Context, Result, bail, ensure};
9use thiserror::Error;
10
11use super::openocd::OpenOcd;
12use crate::test_utils::poll::poll_until;
13
14/// Constants defined by RISC-V Debug Specification 0.13.
15pub mod consts {
16    // JTAG registers.
17    pub const DTMCS: u32 = 0x10;
18    pub const DMI: u32 = 0x11;
19
20    pub const DTMCS_VERSION_SHIFT: u32 = 0;
21    pub const DTMCS_ABITS_SHIFT: u32 = 4;
22    pub const DTMCS_IDLE_SHIFT: u32 = 12;
23    pub const DTMCS_DMIRESET_SHIFT: u32 = 16;
24
25    pub const DTMCS_VERSION_MASK: u32 = 0xf << DTMCS_VERSION_SHIFT;
26    pub const DTMCS_ABITS_MASK: u32 = 0x3f << DTMCS_ABITS_SHIFT;
27    pub const DTMCS_IDLE_MASK: u32 = 0x7 << DTMCS_IDLE_SHIFT;
28    pub const DTMCS_DMIRESET_MASK: u32 = 1 << DTMCS_DMIRESET_SHIFT;
29
30    pub const DTMCS_VERSION_0_13: u32 = 1;
31
32    pub const DMI_ADDRESS_SHIFT: u32 = 34;
33    pub const DMI_DATA_SHIFT: u32 = 2;
34
35    pub const DMI_OP_READ: u64 = 0x1;
36    pub const DMI_OP_WRITE: u64 = 0x2;
37
38    // Debug module registers.
39    pub const DATA0: u32 = 0x04;
40    pub const DATA1: u32 = 0x05;
41    pub const DMCONTROL: u32 = 0x10;
42    pub const DMSTATUS: u32 = 0x11;
43    pub const HARTINFO: u32 = 0x12;
44    pub const ABSTRACTCS: u32 = 0x16;
45
46    pub const DMSTATUS_ANYHALTED_MASK: u32 = 1 << 8;
47    pub const DMSTATUS_ANYRUNNING_MASK: u32 = 1 << 10;
48    pub const DMSTATUS_ANYUNAVAIL_MASK: u32 = 1 << 12;
49    pub const DMSTATUS_ANYNONEXISTENT_MASK: u32 = 1 << 14;
50    pub const DMSTATUS_ANYRESUMEACK_MASK: u32 = 1 << 16;
51    pub const DMSTATUS_ANYHAVERESET_MASK: u32 = 1 << 18;
52    pub const DMSTATUS_ALLHAVERESET_MASK: u32 = 1 << 19;
53
54    pub const DMCONTROL_HASEL_SHIFT: u32 = 26;
55    pub const DMCONTROL_HARTSELHI_SHIFT: u32 = 6;
56    pub const DMCONTROL_HARTSELLO_SHIFT: u32 = 16;
57
58    pub const DMCONTROL_DMACTIVE_MASK: u32 = 1 << 0;
59    pub const DMCONTROL_NDMRESET_MASK: u32 = 1 << 1;
60    pub const DMCONTROL_ACKHAVERESET_MASK: u32 = 1 << 28;
61    pub const DMCONTROL_RESUMEREQ_MASK: u32 = 1 << 30;
62    pub const DMCONTROL_HALTREQ_MASK: u32 = 1 << 31;
63
64    pub const ABSTRACTCS_CMDERR_MASK: u32 = (1 << 11) - (1 << 8);
65    pub const ABSTRACTCS_BUSY_MASK: u32 = 1 << 12;
66
67    pub const ABSTRACTCS_CMDERR_SHIFT: u32 = 8;
68
69    pub const ABSTRACTCS_CMDERR_NONE: u32 = 0;
70}
71
72use consts::*;
73
74/// Debug module interface (DMI) abstraction.
75pub trait Dmi {
76    /// Read a DMI register.
77    fn dmi_read(&mut self, addr: u32) -> Result<u32>;
78
79    /// Write a DMI register.
80    fn dmi_write(&mut self, addr: u32, data: u32) -> Result<()>;
81
82    /// Perform a batch of sequential writes to DMI registers.
83    /// May or may not be more optimized depending on the underlying implementation.
84    fn batched_dmi_writes(&mut self, writes: &[(u32, u32)]) -> Result<()> {
85        writes
86            .iter()
87            .try_for_each(|&(addr, data)| self.dmi_write(addr, data))
88    }
89
90    /// Perform a batch of sequential reads from DMI registers, returning the read values
91    /// in the same order as the given addresses.
92    /// May or may not be more optimized depending on the underlying implementation.
93    fn batched_dmi_reads(&mut self, addrs: &[u32]) -> Result<Vec<u32>> {
94        addrs.iter().map(|&addr| self.dmi_read(addr)).collect()
95    }
96}
97
98impl<T: Dmi> Dmi for &mut T {
99    fn dmi_read(&mut self, addr: u32) -> Result<u32> {
100        T::dmi_read(self, addr)
101    }
102
103    fn dmi_write(&mut self, addr: u32, data: u32) -> Result<()> {
104        T::dmi_write(self, addr, data)
105    }
106
107    fn batched_dmi_writes(&mut self, writes: &[(u32, u32)]) -> Result<()> {
108        T::batched_dmi_writes(self, writes)
109    }
110
111    fn batched_dmi_reads(&mut self, addrs: &[u32]) -> Result<Vec<u32>> {
112        T::batched_dmi_reads(self, addrs)
113    }
114}
115
116/// DMI interface via OpenOCD.
117pub struct OpenOcdDmi {
118    openocd: OpenOcd,
119    tap: String,
120    abits: u32,
121    /// Idle time to wait for after a DMI scan, encoded the same way as `DTMCS.idle`: 0 means
122    /// no wait is needed, 1 means entering Run-Test/Idle and leaving immediately is enough,
123    /// 2 means staying for 1 cycle before leaving, and so on. Every scan already ends in
124    /// Run-Test/Idle for one cycle via `-endstate`, which on its own already satisfies 0 and
125    /// 1; only values above 1 need an explicit extra `runtest` (see `wait_idle_cmd`).
126    extra_idle: u32,
127}
128
129/// Value used for `extra_idle` unless overridden by `OT_DMI_EXTRA_IDLE`.
130const DEFAULT_EXTRA_IDLE: u32 = 4;
131
132impl OpenOcdDmi {
133    /// Create a new DMI interface via OpenOCD.
134    ///
135    /// This should be an OpenOCD instance with JTAG scan chain already set up,
136    /// but not with target set up. If target has been set up, OpenOCD will access
137    /// DMI registers on its own, which will interfere with raw DMI operations.
138    pub fn new(mut openocd: OpenOcd, tap: &str) -> Result<Self> {
139        let target_names = openocd.execute("target names")?;
140        ensure!(
141            target_names.is_empty(),
142            "Target must not be setup when accessing DMI directly"
143        );
144
145        openocd.irscan(tap, DTMCS)?;
146        let res = openocd.drscan(tap, 32, DTMCS_DMIRESET_MASK)?;
147        let version = (res & DTMCS_VERSION_MASK) >> DTMCS_VERSION_SHIFT;
148        let abits = (res & DTMCS_ABITS_MASK) >> DTMCS_ABITS_SHIFT;
149        let idle = (res & DTMCS_IDLE_MASK) >> DTMCS_IDLE_SHIFT;
150
151        ensure!(
152            version == DTMCS_VERSION_0_13,
153            "DTMCS indicates version other than 0.13"
154        );
155
156        let extra_idle = match std::env::var("OT_DMI_EXTRA_IDLE") {
157            Ok(val) => val.parse().context("invalid OT_DMI_EXTRA_IDLE")?,
158            Err(_) => DEFAULT_EXTRA_IDLE,
159        };
160        log::info!(
161            "DTMCS.idle = {idle} (using {extra_idle} as extra_idle, i.e. {} extra runtest \
162             cycle(s) per scan; set OT_DMI_EXTRA_IDLE to override)",
163            extra_idle.saturating_sub(1)
164        );
165
166        openocd.irscan(tap, DMI)?;
167        Ok(Self {
168            openocd,
169            tap: tap.to_owned(),
170            abits,
171            extra_idle,
172        })
173    }
174
175    fn drscan_bits(&self) -> u32 {
176        self.abits + DMI_ADDRESS_SHIFT
177    }
178
179    /// Command for waiting in Run-Test/Idle after a scan, per `extra_idle`:
180    /// the scan's own `-endstate` already provides one cycle in Run-Test/Idle, which covers
181    /// `extra_idle` 0 and 1 on its own; only `extra_idle - 1` further cycles are needed above
182    /// that.
183    fn wait_idle_cmd(&self) -> String {
184        if self.extra_idle > 1 {
185            format!("runtest {}", self.extra_idle - 1)
186        } else {
187            String::new()
188        }
189    }
190
191    /// Wait for `extra_idle` cycles in Run-Test/Idle, if the DTM needs more than the one cycle
192    /// every scan already ends in.
193    fn wait_idle(&mut self) -> Result<()> {
194        let cmd = self.wait_idle_cmd();
195        if !cmd.is_empty() {
196            self.openocd.execute(&cmd)?;
197        }
198        Ok(())
199    }
200
201    fn dmi_op(&mut self, op: u64) -> Result<u64> {
202        let res = self.openocd.drscan(&self.tap, self.drscan_bits(), op)?;
203
204        // We just scanned into the DMI register, so the scanned result should be empty.
205        ensure!(res == 0, "Unexpected DMI initial response {res:#x}");
206
207        // Give the DTM the idle time it asked for to service the operation.
208        self.wait_idle()?;
209
210        // Read the result.
211        let res = self.openocd.drscan(&self.tap, self.drscan_bits(), 0)?;
212        ensure!(res & 3 == 0, "DMI operation failed with {res:#x}");
213
214        // Double check the address matches.
215        ensure!(
216            res >> DMI_ADDRESS_SHIFT == op >> DMI_ADDRESS_SHIFT,
217            "DMI operation address mismatch {res:#x}"
218        );
219
220        Ok(res)
221    }
222}
223
224impl Dmi for OpenOcdDmi {
225    fn dmi_read(&mut self, addr: u32) -> Result<u32> {
226        let output = (self.dmi_op((addr as u64) << DMI_ADDRESS_SHIFT | DMI_OP_READ)?
227            >> DMI_DATA_SHIFT) as u32;
228        log::debug!("DMI read {:#x} -> {:#x}", addr, output);
229        Ok(output)
230    }
231
232    fn dmi_write(&mut self, addr: u32, value: u32) -> Result<()> {
233        self.dmi_op(
234            (addr as u64) << DMI_ADDRESS_SHIFT | (value as u64) << DMI_DATA_SHIFT | DMI_OP_WRITE,
235        )?;
236        log::debug!("DMI write {:#x} <- {:#x}", addr, value);
237        Ok(())
238    }
239
240    fn batched_dmi_writes(&mut self, writes: &[(u32, u32)]) -> Result<()> {
241        if writes.is_empty() {
242            return Ok(());
243        }
244
245        log::debug!(
246            "DMI {} batched writes: {}",
247            writes.len(),
248            writes
249                .iter()
250                .map(|&(addr, value)| format!("{:#x} <- {:#x}", addr, value))
251                .collect::<Vec<_>>()
252                .join(", ")
253        );
254
255        // Chunk the batch to stay well under OpenOCD's ~4 MiB TCL RPC command buffer.
256        const CHUNK_SIZE: usize = 16384;
257
258        for chunk in writes.chunks(CHUNK_SIZE) {
259            // For optimized writes via drscan, we perform direct drscan write operations,
260            // waiting in the RunTest state after each one (per `DTMCS.idle`, see `wait_idle`)
261            // so the DTM has time to service it before the next write lands. We only check the
262            // returned scanned values at the end of each chunk, with a final write to check the
263            // scan status (as errors are sticky).
264            let mut cmd = chunk
265                .iter()
266                .map(|&(addr, value)| {
267                    let data = (addr as u64) << DMI_ADDRESS_SHIFT
268                        | (value as u64) << DMI_DATA_SHIFT
269                        | DMI_OP_WRITE;
270                    let scan = self.openocd.drscan_cmd(&self.tap, self.drscan_bits(), data);
271                    let idle = self.wait_idle_cmd();
272                    if idle.is_empty() {
273                        scan
274                    } else {
275                        format!("{scan}\n{idle}")
276                    }
277                })
278                .collect::<Vec<_>>()
279                .join("\n");
280            cmd.push_str(
281                format!(
282                    "\n{}",
283                    self.openocd.drscan_cmd(&self.tap, self.drscan_bits(), 0)
284                )
285                .as_str(),
286            );
287            let result = self.openocd.execute(&cmd)?;
288
289            // The final NOP scan's captured value carries the DMI status of this chunk:
290            // failed or dropped-while-busy operations leave a sticky nonzero op field, so a
291            // clean status here means every write in the chunk was accepted.
292            let res = u64::from_str_radix(result.trim(), 16)
293                .with_context(|| format!("unexpected DMI batched write response '{result}'"))?;
294            ensure!(
295                res & 3 == 0,
296                "DMI batched write failed with sticky status {res:#x}; at least one write was \
297                 dropped (is the JTAG clock too fast for the DMI to keep up?)"
298            );
299        }
300        Ok(())
301    }
302
303    fn batched_dmi_reads(&mut self, addrs: &[u32]) -> Result<Vec<u32>> {
304        // Pipelined DMI reads: each `drscan` shifts in the next read operation while shifting
305        // out the response of the previous one, so a chunk of N reads costs a single TCL round
306        // trip instead of 3 round trips per read via `dmi_read`. Operations execute strictly in
307        // order at the DTM.
308        const CHUNK_SIZE: usize = 16384;
309
310        let mut values = Vec::with_capacity(addrs.len());
311        for chunk in addrs.chunks(CHUNK_SIZE) {
312            let mut cmd = String::from("set _r {}");
313            for &addr in chunk {
314                let op = (addr as u64) << DMI_ADDRESS_SHIFT | DMI_OP_READ;
315                cmd.push_str(&format!(
316                    "\nlappend _r [{}]",
317                    self.openocd.drscan_cmd(&self.tap, self.drscan_bits(), op)
318                ));
319                let idle = self.wait_idle_cmd();
320                if !idle.is_empty() {
321                    cmd.push_str(&format!("\n{idle}"));
322                }
323            }
324            // A trailing NOP scan collects the final read's response.
325            cmd.push_str(&format!(
326                "\nlappend _r [{}]\nset _r",
327                self.openocd.drscan_cmd(&self.tap, self.drscan_bits(), 0)
328            ));
329
330            let result = self.openocd.execute(&cmd)?;
331            let entries = result
332                .split_whitespace()
333                .map(|s| {
334                    u64::from_str_radix(s, 16)
335                        .with_context(|| format!("unexpected DMI batched read response '{s}'"))
336                })
337                .collect::<Result<Vec<u64>>>()?;
338            ensure!(
339                entries.len() == chunk.len() + 1,
340                "DMI batched read returned {} entries, expected {}",
341                entries.len(),
342                chunk.len() + 1
343            );
344
345            // Entry k is captured while scanning in operation k and holds the response of
346            // operation k-1: entry 0 belongs to whatever preceded this chunk (only its status
347            // matters), entries 1..=N are the responses of this chunk's reads, in order.
348            for (idx, &res) in entries.iter().enumerate() {
349                ensure!(
350                    res & 3 == 0,
351                    "DMI batched read failed with sticky status {res:#x} at entry {idx} \
352                     (is the JTAG clock too fast for the DMI to keep up?)"
353                );
354                if idx > 0 {
355                    let addr = chunk[idx - 1];
356                    ensure!(
357                        res >> DMI_ADDRESS_SHIFT == addr as u64,
358                        "DMI batched read address mismatch: expected {addr:#x}, response {res:#x}"
359                    );
360                    values.push((res >> DMI_DATA_SHIFT) as u32);
361                }
362            }
363        }
364
365        Ok(values)
366    }
367}
368
369#[derive(Debug, Error)]
370pub enum DmiError {
371    #[error("Hart does not exist")]
372    Nonexistent,
373    #[error("Hart is not currently available")]
374    Unavailable,
375    #[error("Timeout waiting for hart to halt")]
376    WaitTimeout,
377}
378
379/// A debugger that communicates with the target via RISC-V Debug Module Interface (DMI).
380pub struct DmiDebugger<D> {
381    dmi: D,
382    hartsel_mask: Option<u32>,
383}
384
385impl<D> Deref for DmiDebugger<D> {
386    type Target = D;
387
388    fn deref(&self) -> &Self::Target {
389        &self.dmi
390    }
391}
392
393impl<D> DerefMut for DmiDebugger<D> {
394    fn deref_mut(&mut self) -> &mut Self::Target {
395        &mut self.dmi
396    }
397}
398
399impl<D: Dmi> DmiDebugger<D> {
400    pub fn new(dmi: D) -> Self {
401        Self {
402            dmi,
403            hartsel_mask: None,
404        }
405    }
406
407    /// Obtain bits valid in hartsel as a bitmask.
408    pub fn hartsel_mask(&mut self) -> Result<u32> {
409        if self.hartsel_mask.is_none() {
410            // Write all 1s to hartsel.
411            let dm_control = 0 << DMCONTROL_HASEL_SHIFT
412                | 0x3ff << DMCONTROL_HARTSELLO_SHIFT
413                | 0x3ff << DMCONTROL_HARTSELHI_SHIFT
414                | DMCONTROL_DMACTIVE_MASK;
415            self.dmi.dmi_write(DMCONTROL, dm_control)?;
416
417            // This is a WARL register so after writing 1, the readback value would be
418            // the mask for valid bits in the register.
419            let dm_control = self.dmi.dmi_read(DMCONTROL)?;
420            let hart_select = (dm_control >> DMCONTROL_HARTSELLO_SHIFT) & 0x3ff
421                | ((dm_control >> DMCONTROL_HARTSELHI_SHIFT) & 0x3ff) << 10;
422
423            self.hartsel_mask = Some(hart_select);
424        }
425
426        Ok(self.hartsel_mask.unwrap())
427    }
428
429    /// Selects a hart to debug.
430    pub fn select_hart(&mut self, hartid: u32) -> Result<DmiHart<'_, D>> {
431        // The hart selection is up to 20 bits.
432        if hartid >= (1 << 20) {
433            bail!("Invalid hartid: {hartid}");
434        }
435
436        // When selecting non-zero hart, ensure written bit to HARTSEL is legal.
437        if hartid != 0 {
438            let mask = self.hartsel_mask()?;
439            if (hartid & mask) != hartid {
440                bail!(DmiError::Nonexistent);
441            }
442        }
443
444        let hart_select = 0 << DMCONTROL_HASEL_SHIFT
445            | (hartid & 0x3ff) << DMCONTROL_HARTSELLO_SHIFT
446            | (hartid >> 10) << DMCONTROL_HARTSELHI_SHIFT
447            | DMCONTROL_DMACTIVE_MASK;
448        self.dmi.dmi_write(DMCONTROL, hart_select)?;
449
450        let mut hart = DmiHart {
451            debugger: self,
452            hart_select,
453        };
454
455        let dmstatus = hart.dmstatus()?;
456        if dmstatus & DMSTATUS_ANYNONEXISTENT_MASK != 0 {
457            bail!(DmiError::Nonexistent);
458        }
459        if dmstatus & DMSTATUS_ANYUNAVAIL_MASK != 0 {
460            bail!(DmiError::Unavailable);
461        }
462
463        Ok(hart)
464    }
465
466    /// Read a data register from DMI.
467    pub fn data(&mut self, idx: u32) -> Result<u32> {
468        ensure!(idx < 12, "data register index out of range {:#x}", idx);
469        self.dmi_read(DATA0 + idx)
470    }
471
472    /// Write a data register from DMI.
473    pub fn set_data(&mut self, idx: u32, data: u32) -> Result<()> {
474        ensure!(idx < 12, "data register index out of range {:#x}", idx);
475        self.dmi_write(DATA0 + idx, data)
476    }
477}
478
479/// A DMI debugger with specific hart selected.
480pub struct DmiHart<'a, D> {
481    debugger: &'a mut DmiDebugger<D>,
482
483    /// The value of DMCONTROL with hasel, hartsello and hartselhi set.
484    hart_select: u32,
485}
486
487impl<D> Deref for DmiHart<'_, D> {
488    type Target = DmiDebugger<D>;
489
490    fn deref(&self) -> &Self::Target {
491        self.debugger
492    }
493}
494
495impl<D> DerefMut for DmiHart<'_, D> {
496    fn deref_mut(&mut self) -> &mut Self::Target {
497        self.debugger
498    }
499}
500
501/// State of the hart.
502///
503/// If both `running` and `halted` are false, then the hart is in the process of transitioning between
504/// the two states (i.e. resuming or halting).
505pub struct HartState {
506    pub running: bool,
507    pub halted: bool,
508}
509
510impl<D: Dmi> DmiHart<'_, D> {
511    /// Read `dmstatus` for the selected hart.
512    pub fn dmstatus(&mut self) -> Result<u32> {
513        let dmstatus = self.debugger.dmi_read(DMSTATUS)?;
514
515        // `dmstatus` register have fields for any hart and all harts. If only a single hart is selected,
516        // then the "all" and "any" values should match. This performs a sanity check.
517        if (dmstatus ^ (dmstatus >> 1))
518            & (DMSTATUS_ANYHALTED_MASK
519                | DMSTATUS_ANYRUNNING_MASK
520                | DMSTATUS_ANYUNAVAIL_MASK
521                | DMSTATUS_ANYNONEXISTENT_MASK
522                | DMSTATUS_ANYRESUMEACK_MASK
523                | DMSTATUS_ANYHAVERESET_MASK)
524            != 0
525        {
526            bail!(
527                "Invalid dmstatus {:#x}: any and all bits mismatch",
528                dmstatus
529            );
530        }
531
532        Ok(dmstatus)
533    }
534
535    /// Write to dmcontrol without affecting hart selection.
536    pub fn set_dmcontrol(&mut self, value: u32) -> Result<()> {
537        self.debugger.dmi_write(DMCONTROL, value | self.hart_select)
538    }
539
540    /// Read hart info of the selected hart.
541    pub fn hartinfo(&mut self) -> Result<u32> {
542        self.debugger.dmi_read(HARTINFO)
543    }
544
545    /// Return the state of the hart.
546    pub fn state(&mut self) -> Result<HartState> {
547        let dmstatus = self.dmstatus()?;
548        let running = dmstatus & DMSTATUS_ANYRUNNING_MASK != 0;
549        let halted = dmstatus & DMSTATUS_ANYHALTED_MASK != 0;
550        assert!(!(running && halted));
551        Ok(HartState { running, halted })
552    }
553
554    /// Set the halt request bit.
555    pub fn set_halt_request(&mut self, active: bool) -> Result<()> {
556        self.set_dmcontrol(if active { DMCONTROL_HALTREQ_MASK } else { 0 })
557    }
558
559    /// Wait for the hart to halt.
560    pub fn wait_halt(&mut self) -> Result<()> {
561        // Per RISC-V debug specification, harts must respond within 1 second of receiving a halt or
562        // resume request.
563        poll_until(Duration::from_secs(1), Duration::from_millis(50), || {
564            Ok(self.state()?.halted)
565        })
566    }
567
568    /// Set the resume request bit.
569    pub fn set_resume_request(&mut self, active: bool) -> Result<()> {
570        self.set_dmcontrol(if active { DMCONTROL_RESUMEREQ_MASK } else { 0 })
571    }
572
573    /// Wait for the hart to resume.
574    pub fn wait_resume(&mut self) -> Result<()> {
575        // Per RISC-V debug specification, harts must respond within 1 second of receiving a halt or
576        // resume request.
577        poll_until(Duration::from_secs(1), Duration::from_secs(1), || {
578            Ok(self.state()?.running)
579        })
580    }
581}