opentitanlib/debug/
openocd.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::collections::VecDeque;
6use std::fmt::LowerHex;
7use std::io::{BufRead, BufReader, Write};
8use std::mem::size_of;
9use std::net::TcpStream;
10use std::os::unix::process::CommandExt;
11use std::path::Path;
12use std::process::{Child, Command, Stdio};
13use std::sync::{Arc, Mutex};
14use std::thread::JoinHandle;
15use std::time::{Duration, Instant};
16
17use anyhow::{Context, Result, bail, ensure};
18use regex::Regex;
19use serde::{Deserialize, Serialize};
20use thiserror::Error;
21
22use ot_hal::dif::lc_ctrl::LcCtrlReg;
23
24use crate::impl_serializable_error;
25use crate::io::jtag::{Jtag, JtagChain, JtagError, JtagParams, JtagTap, RiscvReg};
26use crate::regex;
27use crate::util::parse_int::ParseInt;
28use crate::util::printer;
29
30/// Represents an OpenOCD server that we can interact with.
31pub struct OpenOcd {
32    /// OpenOCD child process.
33    server_process: Child,
34    /// Receiving side of the stream to the telnet interface of OpenOCD.
35    reader: BufReader<TcpStream>,
36    /// Sending side of the stream to the telnet interface of OpenOCD.
37    writer: TcpStream,
38    /// The last lines OpenOCD wrote to stderr, reported if it exits unexpectedly.
39    stderr_tail: Arc<Mutex<VecDeque<String>>>,
40    /// Thread draining OpenOCD's stderr; it ends when OpenOCD closes stderr.
41    stderr_thread: Option<JoinHandle<()>>,
42}
43
44impl Drop for OpenOcd {
45    fn drop(&mut self) {
46        let _ = self.server_process.kill();
47    }
48}
49
50impl OpenOcd {
51    /// How long to wait for OpenOCD to get ready to accept a TCL connection.
52    const OPENOCD_TCL_READY_TMO: Duration = Duration::from_secs(30);
53    /// How many of OpenOCD's last stderr lines to keep for error reports.
54    const STDERR_TAIL_LINES: usize = 20;
55    /// How long to wait for OpenOCD to be reaped after its socket closed.
56    const EXIT_STATUS_TMO: Duration = Duration::from_secs(1);
57
58    /// Wait until we see a particular message on the output.
59    fn wait_until_regex_match<'a>(
60        stderr: &mut impl BufRead,
61        regex: &Regex,
62        timeout: Duration,
63        log_stdio: bool,
64        s: &'a mut String,
65    ) -> Result<regex::Captures<'a>> {
66        let start = Instant::now();
67        loop {
68            // NOTE the read could block indefinitely, a proper solution would involved spawning
69            // a thread or using async.
70            let n = stderr.read_line(s)?;
71            if n == 0 {
72                bail!("OpenOCD stopped before being ready?");
73            }
74            if log_stdio {
75                log::info!(target: concat!(module_path!(), "::stderr"), "{}", s);
76            }
77            if regex.is_match(s) {
78                // This is not a `if let Some(capture) = regex.captures(s) {}` to to Rust
79                // borrow checker limitations. Can be modified if Polonius lands.
80                return Ok(regex.captures(s).unwrap());
81            }
82            s.clear();
83
84            if start.elapsed() >= timeout {
85                bail!("OpenOCD did not become ready to accept a TCL connection");
86            }
87        }
88    }
89
90    /// Spawn an OpenOCD server with given path.
91    pub fn spawn(path: &Path, log_stdio: bool) -> Result<Self> {
92        let mut cmd = Command::new(path);
93
94        // Let OpenOCD choose which port to bind to, in order to never unnecesarily run into
95        // issues due to a particular port already being in use.
96        // We don't use the telnet and GDB ports so disable them.
97        // The configuration will happen through the TCL interface, so use `noinit` to prevent
98        // OpenOCD from transition to execution mode.
99        cmd.arg("-c")
100            .arg("tcl_port 0; telnet_port disabled; gdb_port disabled; noinit;");
101
102        log::info!("Spawning OpenOCD: {cmd:?}");
103
104        cmd.stdin(Stdio::null())
105            .stdout(Stdio::piped())
106            .stderr(Stdio::piped());
107
108        // SAFETY: prctl is a syscall which is atomic and thus async-signal-safe.
109        unsafe {
110            cmd.pre_exec(|| {
111                // Since we use OpenOCD as a library, make sure it's killed when
112                // the parent process dies. This setting is preserved across execve.
113                rustix::process::set_parent_process_death_signal(Some(
114                    rustix::process::Signal::HUP,
115                ))?;
116                Ok(())
117            });
118        }
119
120        let mut child = cmd
121            .spawn()
122            .with_context(|| format!("failed to spawn openocd: {cmd:?}",))?;
123        let stdout = child.stdout.take().unwrap();
124        let mut stderr = BufReader::new(child.stderr.take().unwrap());
125        // Wait until we see 'Info : Listening on port XXX for tcl connections' before knowing
126        // which port to connect to.
127        if log_stdio {
128            log::info!("Waiting for OpenOCD to be ready to accept a TCL connection...");
129        }
130        let mut buf = String::new();
131        let regex_captures = Self::wait_until_regex_match(
132            &mut stderr,
133            regex!("Info : Listening on port ([0-9]+) for tcl connections"),
134            Self::OPENOCD_TCL_READY_TMO,
135            log_stdio,
136            &mut buf,
137        )
138        .context("OpenOCD was not ready in time to accept a connection")?;
139        let openocd_port: u16 = regex_captures.get(1).unwrap().as_str().parse()?;
140        // Print stdout with log
141        if log_stdio {
142            std::thread::spawn(move || {
143                printer::accumulate(
144                    stdout,
145                    concat!(module_path!(), "::stdout"),
146                    Default::default(),
147                )
148            });
149        }
150        // Always drain stderr, so OpenOCD never blocks on a full pipe, and keep its
151        // last lines: OpenOCD reports errors there, and they explain a later exit.
152        let stderr_tail = Arc::new(Mutex::new(VecDeque::with_capacity(Self::STDERR_TAIL_LINES)));
153        let tail = Arc::clone(&stderr_tail);
154        let stderr_thread = std::thread::spawn(move || {
155            // Read raw lines, so a line that is not valid UTF-8 does not end the drain.
156            let mut buf = Vec::new();
157            while let Ok(n) = stderr.read_until(b'\n', &mut buf)
158                && n > 0
159            {
160                let line = String::from_utf8_lossy(&buf)
161                    .trim_end_matches(['\r', '\n'])
162                    .to_string();
163                buf.clear();
164                if log_stdio {
165                    log::info!(target: concat!(module_path!(), "::stderr"), "{line}");
166                }
167                let mut tail = tail.lock().unwrap();
168                if tail.len() == Self::STDERR_TAIL_LINES {
169                    tail.pop_front();
170                }
171                tail.push_back(line);
172            }
173        });
174
175        let kill_guard = scopeguard::guard(child, |mut child| {
176            let _ = child.kill();
177        });
178
179        log::info!("Connecting to OpenOCD tcl interface...");
180
181        let stream = TcpStream::connect(("localhost", openocd_port))
182            .context("failed to connect to OpenOCD socket")?;
183
184        // Disable TCP Nagle delay to ensure minimal latency to OpenOCD.
185        // Without this, roundtrip communications can take 50ms which adds
186        // up to be longer than certain timeouts, e.g. the RMA loop in ROM.
187        stream
188            .set_nodelay(true)
189            .context("failed to disable TCP socket delay")?;
190
191        let mut connection = Self {
192            server_process: scopeguard::ScopeGuard::into_inner(kill_guard),
193            reader: BufReader::new(stream.try_clone()?),
194            writer: stream,
195            stderr_tail,
196            stderr_thread: Some(stderr_thread),
197        };
198
199        // Test the connection by asking for OpenOCD's version.
200        let version = connection.execute("version")?;
201        log::info!("OpenOCD version: {version}");
202
203        Ok(connection)
204    }
205
206    /// Send a string to OpenOCD Tcl interface.
207    fn send(&mut self, cmd: &str) -> Result<()> {
208        // The protocol is to send the command followed by a `0x1a` byte,
209        // see https://openocd.org/doc/html/Tcl-Scripting-API.html#Tcl-RPC-server
210
211        // Sanity check to ensure that the command string is not malformed.
212        if cmd.contains('\x1A') {
213            bail!("TCL command string should be contained inside the text to send");
214        }
215
216        self.writer
217            .write_all(cmd.as_bytes())
218            .context("failed to send a command to OpenOCD server")?;
219        self.writer
220            .write_all(&[0x1a])
221            .context("failed to send the command terminator to OpenOCD server")?;
222        self.writer.flush().context("failed to flush stream")?;
223        Ok(())
224    }
225
226    fn recv(&mut self) -> Result<String> {
227        let mut buf = Vec::new();
228        self.reader.read_until(0x1A, &mut buf)?;
229        if !buf.ends_with(b"\x1A") {
230            let partial = String::from_utf8_lossy(&buf).to_string();
231            let report = self.exit_report();
232            bail!(OpenOcdError::PrematureExit(if partial.is_empty() {
233                report
234            } else {
235                format!("{partial}; {report}")
236            }));
237        }
238        buf.pop();
239        String::from_utf8(buf).context("failed to parse OpenOCD response as UTF-8")
240    }
241
242    /// Describe how OpenOCD ended (exit code or signal) and what it last printed.
243    fn exit_report(&mut self) -> String {
244        // The socket can close a moment before the process can be reaped.
245        let deadline = Instant::now() + Self::EXIT_STATUS_TMO;
246        let status = loop {
247            match self.server_process.try_wait() {
248                Ok(None) if Instant::now() < deadline => {
249                    std::thread::sleep(Duration::from_millis(10))
250                }
251                Ok(status) => break status,
252                Err(_) => break None,
253            }
254        };
255        // Once OpenOCD is gone its stderr is closed: let the thread read the rest.
256        if status.is_some()
257            && let Some(thread) = self.stderr_thread.take()
258        {
259            let _ = thread.join();
260        }
261        let tail = self.stderr_tail.lock().unwrap();
262        let mut report = match status {
263            Some(status) => format!("process ended with {status}"),
264            None => "process still running".to_string(),
265        };
266        if !tail.is_empty() {
267            report.push_str("\nLast OpenOCD output:");
268            for line in tail.iter() {
269                report.push_str("\n  ");
270                report.push_str(line);
271            }
272        }
273        report
274    }
275
276    pub fn shutdown(mut self) -> Result<()> {
277        self.execute("shutdown")?;
278        // Wait for it to exit.
279        self.server_process
280            .wait()
281            .context("failed to wait for OpenOCD server to exit")?;
282        Ok(())
283    }
284
285    /// Send a TCL command to OpenOCD and wait for its response.
286    pub fn execute(&mut self, cmd: &str) -> Result<String> {
287        self.send(cmd)?;
288        self.recv()
289    }
290
291    /// Load instruction register of a given tap.
292    pub fn irscan(&mut self, tap: &str, ir: u32) -> Result<()> {
293        let cmd = format!("irscan {} {:#x}", tap, ir);
294        let result = self.execute(&cmd)?;
295        ensure!(result.is_empty(), "unexpected response: '{result}'");
296        Ok(())
297    }
298
299    /// Command for scanning a data register.
300    pub fn drscan_cmd<T: ParseInt + LowerHex>(&self, tap: &str, numbits: u32, data: T) -> String {
301        format!("drscan {} {} {:#x}", tap, numbits, data)
302    }
303
304    /// Load data register of a given tap and return the scan.
305    pub fn drscan<T: ParseInt + LowerHex>(
306        &mut self,
307        tap: &str,
308        numbits: u32,
309        data: T,
310    ) -> Result<T> {
311        let cmd = self.drscan_cmd(tap, numbits, data);
312        let result = self.execute(&cmd)?;
313        Ok(T::from_str_radix(&result, 16).map_err(|x| x.into())?)
314    }
315}
316
317/// An JTAG interface driver over OpenOCD.
318pub struct OpenOcdJtagChain {
319    /// OpenOCD server instance.
320    openocd: OpenOcd,
321}
322
323/// Errors related to the OpenOCD server.
324#[derive(Error, Debug, Deserialize, Serialize)]
325pub enum OpenOcdError {
326    #[error("OpenOCD initialization failed: {0}")]
327    InitializeFailure(String),
328    #[error("OpenOCD server exited prematurely: {0}")]
329    PrematureExit(String),
330    #[error("Generic error {0}")]
331    Generic(String),
332}
333impl_serializable_error!(OpenOcdError);
334
335impl OpenOcdJtagChain {
336    /// Start OpenOCD with given JTAG options but do not connect any TAP.
337    pub fn new(adapter_command: &str, opts: &JtagParams) -> Result<OpenOcdJtagChain> {
338        let mut openocd = OpenOcd::spawn(&opts.openocd, opts.log_stdio)?;
339
340        openocd.execute(adapter_command)?;
341        openocd.execute(&format!("adapter speed {}", opts.adapter_speed_khz))?;
342        openocd.execute("transport select jtag")?;
343        openocd.execute("scan_chain")?;
344
345        Ok(OpenOcdJtagChain { openocd })
346    }
347}
348
349impl JtagChain for OpenOcdJtagChain {
350    fn connect(mut self: Box<Self>, tap: JtagTap) -> Result<Box<dyn Jtag>> {
351        // Pass through the config for the chosen TAP.
352        let target = match tap {
353            JtagTap::RiscvTap => include_str!(env!("openocd_riscv_target_cfg")),
354            JtagTap::LcTap => include_str!(env!("openocd_lc_target_cfg")),
355            JtagTap::BackdoorTap => include_str!(env!("openocd_fpga_backdoor_target_cfg")),
356        };
357        self.openocd.execute(target)?;
358
359        // Capture outputs during initialization to see if error has occurred during the process.
360        let resp = self.openocd.execute("capture init")?;
361        if resp.contains("JTAG scan chain interrogation failed") {
362            bail!(OpenOcdError::InitializeFailure(resp));
363        }
364        // OpenOCD carries on with a target it could not examine (e.g. "Hart 0 failed
365        // to halt during examine()"), and later commands then fail in confusing ways.
366        // Only the RISC-V TAP has a debug module to examine (see #28588).
367        if tap == JtagTap::RiscvTap && resp.contains("examination failed") {
368            log::warn!("OpenOCD could not examine the target during init:\n{resp}");
369        }
370
371        Ok(Box::new(OpenOcdJtagTap {
372            openocd: self.openocd,
373            jtag_tap: tap,
374        }))
375    }
376
377    fn into_raw(self: Box<Self>) -> Result<OpenOcd> {
378        Ok(self.openocd)
379    }
380}
381
382/// An JTAG interface driver over OpenOCD.
383pub struct OpenOcdJtagTap {
384    /// OpenOCD server instance.
385    openocd: OpenOcd,
386    /// JTAG TAP OpenOCD is connected to.
387    jtag_tap: JtagTap,
388}
389
390impl OpenOcdJtagTap {
391    /// Send a TCL command to OpenOCD and wait for its response.
392    fn send_tcl_cmd(&mut self, cmd: &str) -> Result<String> {
393        self.openocd.execute(cmd)
394    }
395
396    fn read_memory_impl<T: ParseInt>(&mut self, addr: u32, buf: &mut [T]) -> Result<usize> {
397        // Ibex does not have a MMU so always tell OpenOCD that we are using physical addresses
398        // otherwise it will try to translate the address through the (non-existent) MMU
399        let cmd = format!(
400            "read_memory 0x{addr:x} {width} {count} phys",
401            width = 8 * size_of::<T>(),
402            count = buf.len()
403        );
404        let response = self.send_tcl_cmd(cmd.as_str())?;
405        response.trim().split(' ').try_fold(0, |idx, val| {
406            if idx < buf.len() {
407                buf[idx] = T::from_str(val).context(format!(
408                    "expected response to be an hexadecimal byte, got '{response}'"
409                ))?;
410                Ok(idx + 1)
411            } else {
412                bail!("OpenOCD returned too much data on read".to_string())
413            }
414        })
415    }
416
417    fn write_memory_impl<T: ToString>(&mut self, addr: u32, bigbuf: &[T]) -> Result<()> {
418        const CHUNK_SIZE: usize = 1024;
419        for (idx, buf) in bigbuf.chunks(CHUNK_SIZE).enumerate() {
420            // Convert data to space-separated strings.
421            let data: Vec<_> = buf.iter().map(ToString::to_string).collect();
422            let data_str = &data[..].join(" ");
423            // See [read_memory] about physical addresses
424            let cmd = format!(
425                "write_memory 0x{chunk_addr:x} {width} {{ {data_str} }} phys",
426                chunk_addr = addr + (idx * CHUNK_SIZE * size_of::<T>()) as u32,
427                width = 8 * size_of::<T>()
428            );
429            let response = self.send_tcl_cmd(cmd.as_str())?;
430            if !response.is_empty() {
431                bail!("unexpected response: '{response}'");
432            }
433        }
434
435        Ok(())
436    }
437
438    /// Read a register: this function does not attempt to translate the
439    /// name or number of the register. If force is set, bypass OpenOCD's
440    /// register cache.
441    fn read_register<T: ParseInt>(&mut self, reg_name: &str, force: bool) -> Result<T> {
442        let cmd = format!(
443            "get_reg {} {{ {} }}",
444            if force { "-force" } else { "" },
445            reg_name,
446        );
447        let response = self.send_tcl_cmd(cmd.as_str())?;
448        // the expected output format is 'reg_name 0xabcdef', e.g 'pc 0x10009858'
449        let (out_reg_name, value) = response.trim().split_once(' ').with_context(|| {
450            format!("expected response of the form 'reg value', got '{response}'")
451        })?;
452        ensure!(
453            out_reg_name == reg_name,
454            "OpenOCD returned the value for register '{out_reg_name}' instead of '{reg_name}"
455        );
456        T::from_str(value).context(format!(
457            "expected value to be an hexadecimal string, got '{value}'"
458        ))
459    }
460
461    fn write_register<T: ToString>(&mut self, reg_name: &str, value: T) -> Result<()> {
462        let cmd = format!("set_reg {{ {reg_name} {} }}", T::to_string(&value));
463        let response = self.send_tcl_cmd(cmd.as_str())?;
464        if !response.is_empty() {
465            bail!("unexpected response: '{response}'");
466        }
467
468        Ok(())
469    }
470}
471
472impl Jtag for OpenOcdJtagTap {
473    fn into_raw(self: Box<Self>) -> Result<OpenOcd> {
474        Ok(self.openocd)
475    }
476
477    fn as_raw(&mut self) -> Result<&mut OpenOcd> {
478        Ok(&mut self.openocd)
479    }
480
481    fn disconnect(self: Box<Self>) -> Result<()> {
482        self.openocd.shutdown()
483    }
484
485    fn tap(&self) -> JtagTap {
486        self.jtag_tap
487    }
488
489    fn read_lc_ctrl_reg(&mut self, reg: &LcCtrlReg) -> Result<u32> {
490        ensure!(
491            matches!(self.jtag_tap, JtagTap::LcTap),
492            JtagError::Tap(self.jtag_tap)
493        );
494        let reg_offset = reg.word_offset();
495        let cmd = format!("riscv dmi_read 0x{reg_offset:x}");
496        let response = self.send_tcl_cmd(cmd.as_str())?;
497
498        let value = u32::from_str(response.trim()).context(format!(
499            "expected response to be hexadecimal word, got '{response}'"
500        ))?;
501
502        Ok(value)
503    }
504
505    fn write_lc_ctrl_reg(&mut self, reg: &LcCtrlReg, value: u32) -> Result<()> {
506        ensure!(
507            matches!(self.jtag_tap, JtagTap::LcTap),
508            JtagError::Tap(self.jtag_tap)
509        );
510        let reg_offset = reg.word_offset();
511        let cmd = format!("riscv dmi_write 0x{reg_offset:x} 0x{value:x}");
512        let response = self.send_tcl_cmd(cmd.as_str())?;
513
514        if !response.is_empty() {
515            bail!("unexpected response: '{response}'");
516        }
517
518        Ok(())
519    }
520
521    fn read_memory(&mut self, addr: u32, buf: &mut [u8]) -> Result<usize> {
522        ensure!(
523            matches!(self.jtag_tap, JtagTap::RiscvTap),
524            JtagError::Tap(self.jtag_tap)
525        );
526        self.read_memory_impl(addr, buf)
527    }
528
529    fn read_memory32(&mut self, addr: u32, buf: &mut [u32]) -> Result<usize> {
530        ensure!(
531            matches!(self.jtag_tap, JtagTap::RiscvTap),
532            JtagError::Tap(self.jtag_tap)
533        );
534        self.read_memory_impl(addr, buf)
535    }
536
537    fn write_memory(&mut self, addr: u32, buf: &[u8]) -> Result<()> {
538        ensure!(
539            matches!(self.jtag_tap, JtagTap::RiscvTap),
540            JtagError::Tap(self.jtag_tap)
541        );
542        self.write_memory_impl(addr, buf)
543    }
544
545    fn write_memory32(&mut self, addr: u32, buf: &[u32]) -> Result<()> {
546        ensure!(
547            matches!(self.jtag_tap, JtagTap::RiscvTap),
548            JtagError::Tap(self.jtag_tap)
549        );
550        self.write_memory_impl(addr, buf)
551    }
552
553    fn halt(&mut self) -> Result<()> {
554        ensure!(
555            matches!(self.jtag_tap, JtagTap::RiscvTap),
556            JtagError::Tap(self.jtag_tap)
557        );
558        let response = self.send_tcl_cmd("halt")?;
559        if !response.is_empty() {
560            bail!("unexpected response: '{response}'");
561        }
562
563        Ok(())
564    }
565
566    fn wait_halt(&mut self, timeout: Duration) -> Result<()> {
567        ensure!(
568            matches!(self.jtag_tap, JtagTap::RiscvTap),
569            JtagError::Tap(self.jtag_tap)
570        );
571        let cmd = format!("wait_halt {}", timeout.as_millis());
572        let response = self.send_tcl_cmd(cmd.as_str())?;
573        if !response.is_empty() {
574            bail!("unexpected response: '{response}'");
575        }
576        Ok(())
577    }
578
579    fn resume(&mut self) -> Result<()> {
580        ensure!(
581            matches!(self.jtag_tap, JtagTap::RiscvTap),
582            JtagError::Tap(self.jtag_tap)
583        );
584        let response = self.send_tcl_cmd("resume")?;
585        if !response.is_empty() {
586            bail!("unexpected response: '{response}'");
587        }
588
589        Ok(())
590    }
591
592    fn resume_at(&mut self, addr: u32) -> Result<()> {
593        ensure!(
594            matches!(self.jtag_tap, JtagTap::RiscvTap),
595            JtagError::Tap(self.jtag_tap)
596        );
597        let cmd = format!("resume 0x{:x}", addr);
598        let response = self.send_tcl_cmd(&cmd)?;
599        if !response.is_empty() {
600            bail!("unexpected response: '{response}'");
601        }
602
603        Ok(())
604    }
605
606    fn reset(&mut self, run: bool) -> Result<()> {
607        ensure!(
608            matches!(self.jtag_tap, JtagTap::RiscvTap),
609            JtagError::Tap(self.jtag_tap)
610        );
611        let cmd = format!("reset {}", if run { "run" } else { "halt" });
612        let response = self.send_tcl_cmd(&cmd)?;
613        if !response.is_empty() {
614            bail!("unexpected response: '{response}'");
615        }
616
617        Ok(())
618    }
619
620    fn step(&mut self) -> Result<()> {
621        ensure!(
622            matches!(self.jtag_tap, JtagTap::RiscvTap),
623            JtagError::Tap(self.jtag_tap)
624        );
625        let response = self.send_tcl_cmd("step")?;
626        if !response.is_empty() {
627            bail!("unexpected response: '{response}'");
628        }
629
630        Ok(())
631    }
632
633    fn step_at(&mut self, addr: u32) -> Result<()> {
634        ensure!(
635            matches!(self.jtag_tap, JtagTap::RiscvTap),
636            JtagError::Tap(self.jtag_tap)
637        );
638        let cmd = format!("step 0x{:x}", addr);
639        let response = self.send_tcl_cmd(&cmd)?;
640        if !response.is_empty() {
641            bail!("unexpected response: '{response}'");
642        }
643
644        Ok(())
645    }
646
647    fn read_riscv_reg(&mut self, reg: &RiscvReg) -> Result<u32> {
648        ensure!(
649            matches!(self.jtag_tap, JtagTap::RiscvTap),
650            JtagError::Tap(self.jtag_tap)
651        );
652        self.read_register::<u32>(reg.name(), true)
653    }
654
655    fn write_riscv_reg(&mut self, reg: &RiscvReg, val: u32) -> Result<()> {
656        ensure!(
657            matches!(self.jtag_tap, JtagTap::RiscvTap),
658            JtagError::Tap(self.jtag_tap)
659        );
660        self.write_register(reg.name(), val)
661    }
662
663    fn set_breakpoint(&mut self, address: u32, hw: bool) -> Result<()> {
664        let cmd = format!("bp {:#x} 2{}", address, if hw { " hw" } else { "" });
665        let response = self.send_tcl_cmd(&cmd)?;
666        if !response.starts_with("breakpoint set at ") {
667            bail!("unexpected response: '{response}'");
668        }
669        Ok(())
670    }
671
672    fn remove_breakpoint(&mut self, addr: u32) -> Result<()> {
673        let cmd = format!("rbp {:#x}", addr);
674        let response = self.send_tcl_cmd(&cmd)?;
675        if !response.is_empty() {
676            bail!("unexpected response: '{response}'");
677        }
678        Ok(())
679    }
680
681    fn remove_all_breakpoints(&mut self) -> Result<()> {
682        let response = self.send_tcl_cmd("rbp all")?;
683        if !response.is_empty() {
684            bail!("unexpected response: '{response}'");
685        }
686        Ok(())
687    }
688}