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