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::time::{Duration, Instant};
13
14use anyhow::{Context, Result, bail, ensure};
15use regex::Regex;
16use serde::{Deserialize, Serialize};
17use thiserror::Error;
18
19use ot_hal::dif::lc_ctrl::LcCtrlReg;
20
21use crate::impl_serializable_error;
22use crate::io::jtag::{Jtag, JtagChain, JtagError, JtagParams, JtagTap, RiscvReg};
23use crate::regex;
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        let mut buf = String::new();
120        let regex_captures = Self::wait_until_regex_match(
121            &mut stderr,
122            regex!("Info : Listening on port ([0-9]+) for tcl connections"),
123            Self::OPENOCD_TCL_READY_TMO,
124            log_stdio,
125            &mut buf,
126        )
127        .context("OpenOCD was not ready in time to accept a connection")?;
128        let openocd_port: u16 = regex_captures.get(1).unwrap().as_str().parse()?;
129        // Print stdout and stderr with log
130        if log_stdio {
131            std::thread::spawn(move || {
132                printer::accumulate(
133                    stdout,
134                    concat!(module_path!(), "::stdout"),
135                    Default::default(),
136                )
137            });
138            std::thread::spawn(move || {
139                printer::accumulate(
140                    stderr,
141                    concat!(module_path!(), "::stderr"),
142                    Default::default(),
143                )
144            });
145        }
146
147        let kill_guard = scopeguard::guard(child, |mut child| {
148            let _ = child.kill();
149        });
150
151        log::info!("Connecting to OpenOCD tcl interface...");
152
153        let stream = TcpStream::connect(("localhost", openocd_port))
154            .context("failed to connect to OpenOCD socket")?;
155
156        // Disable TCP Nagle delay to ensure minimal latency to OpenOCD.
157        // Without this, roundtrip communications can take 50ms which adds
158        // up to be longer than certain timeouts, e.g. the RMA loop in ROM.
159        stream
160            .set_nodelay(true)
161            .context("failed to disable TCP socket delay")?;
162
163        let mut connection = Self {
164            server_process: scopeguard::ScopeGuard::into_inner(kill_guard),
165            reader: BufReader::new(stream.try_clone()?),
166            writer: stream,
167        };
168
169        // Test the connection by asking for OpenOCD's version.
170        let version = connection.execute("version")?;
171        log::info!("OpenOCD version: {version}");
172
173        Ok(connection)
174    }
175
176    /// Send a string to OpenOCD Tcl interface.
177    fn send(&mut self, cmd: &str) -> Result<()> {
178        // The protocol is to send the command followed by a `0x1a` byte,
179        // see https://openocd.org/doc/html/Tcl-Scripting-API.html#Tcl-RPC-server
180
181        // Sanity check to ensure that the command string is not malformed.
182        if cmd.contains('\x1A') {
183            bail!("TCL command string should be contained inside the text to send");
184        }
185
186        self.writer
187            .write_all(cmd.as_bytes())
188            .context("failed to send a command to OpenOCD server")?;
189        self.writer
190            .write_all(&[0x1a])
191            .context("failed to send the command terminator to OpenOCD server")?;
192        self.writer.flush().context("failed to flush stream")?;
193        Ok(())
194    }
195
196    fn recv(&mut self) -> Result<String> {
197        let mut buf = Vec::new();
198        self.reader.read_until(0x1A, &mut buf)?;
199        if !buf.ends_with(b"\x1A") {
200            bail!(OpenOcdError::PrematureExit(
201                String::from_utf8_lossy(&buf).to_string()
202            ));
203        }
204        buf.pop();
205        String::from_utf8(buf).context("failed to parse OpenOCD response as UTF-8")
206    }
207
208    pub fn shutdown(mut self) -> Result<()> {
209        self.execute("shutdown")?;
210        // Wait for it to exit.
211        self.server_process
212            .wait()
213            .context("failed to wait for OpenOCD server to exit")?;
214        Ok(())
215    }
216
217    /// Send a TCL command to OpenOCD and wait for its response.
218    pub fn execute(&mut self, cmd: &str) -> Result<String> {
219        self.send(cmd)?;
220        self.recv()
221    }
222
223    /// Load instruction register of a given tap.
224    pub fn irscan(&mut self, tap: &str, ir: u32) -> Result<()> {
225        let cmd = format!("irscan {} {:#x}", tap, ir);
226        let result = self.execute(&cmd)?;
227        ensure!(result.is_empty(), "unexpected response: '{result}'");
228        Ok(())
229    }
230
231    /// Command for scanning a data register.
232    pub fn drscan_cmd<T: ParseInt + LowerHex>(&self, tap: &str, numbits: u32, data: T) -> String {
233        format!("drscan {} {} {:#x}", tap, numbits, data)
234    }
235
236    /// Load data register of a given tap and return the scan.
237    pub fn drscan<T: ParseInt + LowerHex>(
238        &mut self,
239        tap: &str,
240        numbits: u32,
241        data: T,
242    ) -> Result<T> {
243        let cmd = self.drscan_cmd(tap, numbits, data);
244        let result = self.execute(&cmd)?;
245        Ok(T::from_str_radix(&result, 16).map_err(|x| x.into())?)
246    }
247}
248
249/// An JTAG interface driver over OpenOCD.
250pub struct OpenOcdJtagChain {
251    /// OpenOCD server instance.
252    openocd: OpenOcd,
253}
254
255/// Errors related to the OpenOCD server.
256#[derive(Error, Debug, Deserialize, Serialize)]
257pub enum OpenOcdError {
258    #[error("OpenOCD initialization failed: {0}")]
259    InitializeFailure(String),
260    #[error("OpenOCD server exited prematurely: {0}")]
261    PrematureExit(String),
262    #[error("Generic error {0}")]
263    Generic(String),
264}
265impl_serializable_error!(OpenOcdError);
266
267impl OpenOcdJtagChain {
268    /// Start OpenOCD with given JTAG options but do not connect any TAP.
269    pub fn new(adapter_command: &str, opts: &JtagParams) -> Result<OpenOcdJtagChain> {
270        let mut openocd = OpenOcd::spawn(&opts.openocd, opts.log_stdio)?;
271
272        openocd.execute(adapter_command)?;
273        openocd.execute(&format!("adapter speed {}", opts.adapter_speed_khz))?;
274        openocd.execute("transport select jtag")?;
275        openocd.execute("scan_chain")?;
276
277        Ok(OpenOcdJtagChain { openocd })
278    }
279}
280
281impl JtagChain for OpenOcdJtagChain {
282    fn connect(mut self: Box<Self>, tap: JtagTap) -> Result<Box<dyn Jtag>> {
283        // Pass through the config for the chosen TAP.
284        let target = match tap {
285            JtagTap::RiscvTap => include_str!(env!("openocd_riscv_target_cfg")),
286            JtagTap::LcTap => include_str!(env!("openocd_lc_target_cfg")),
287            JtagTap::BackdoorTap => include_str!(env!("openocd_fpga_backdoor_target_cfg")),
288        };
289        self.openocd.execute(target)?;
290
291        // Capture outputs during initialization to see if error has occurred during the process.
292        let resp = self.openocd.execute("capture init")?;
293        if resp.contains("JTAG scan chain interrogation failed") {
294            bail!(OpenOcdError::InitializeFailure(resp));
295        }
296
297        Ok(Box::new(OpenOcdJtagTap {
298            openocd: self.openocd,
299            jtag_tap: tap,
300        }))
301    }
302
303    fn into_raw(self: Box<Self>) -> Result<OpenOcd> {
304        Ok(self.openocd)
305    }
306}
307
308/// An JTAG interface driver over OpenOCD.
309pub struct OpenOcdJtagTap {
310    /// OpenOCD server instance.
311    openocd: OpenOcd,
312    /// JTAG TAP OpenOCD is connected to.
313    jtag_tap: JtagTap,
314}
315
316impl OpenOcdJtagTap {
317    /// Send a TCL command to OpenOCD and wait for its response.
318    fn send_tcl_cmd(&mut self, cmd: &str) -> Result<String> {
319        self.openocd.execute(cmd)
320    }
321
322    fn read_memory_impl<T: ParseInt>(&mut self, addr: u32, buf: &mut [T]) -> Result<usize> {
323        // Ibex does not have a MMU so always tell OpenOCD that we are using physical addresses
324        // otherwise it will try to translate the address through the (non-existent) MMU
325        let cmd = format!(
326            "read_memory 0x{addr:x} {width} {count} phys",
327            width = 8 * size_of::<T>(),
328            count = buf.len()
329        );
330        let response = self.send_tcl_cmd(cmd.as_str())?;
331        response.trim().split(' ').try_fold(0, |idx, val| {
332            if idx < buf.len() {
333                buf[idx] = T::from_str(val).context(format!(
334                    "expected response to be an hexadecimal byte, got '{response}'"
335                ))?;
336                Ok(idx + 1)
337            } else {
338                bail!("OpenOCD returned too much data on read".to_string())
339            }
340        })
341    }
342
343    fn write_memory_impl<T: ToString>(&mut self, addr: u32, bigbuf: &[T]) -> Result<()> {
344        const CHUNK_SIZE: usize = 1024;
345        for (idx, buf) in bigbuf.chunks(CHUNK_SIZE).enumerate() {
346            // Convert data to space-separated strings.
347            let data: Vec<_> = buf.iter().map(ToString::to_string).collect();
348            let data_str = &data[..].join(" ");
349            // See [read_memory] about physical addresses
350            let cmd = format!(
351                "write_memory 0x{chunk_addr:x} {width} {{ {data_str} }} phys",
352                chunk_addr = addr + (idx * CHUNK_SIZE * size_of::<T>()) as u32,
353                width = 8 * size_of::<T>()
354            );
355            let response = self.send_tcl_cmd(cmd.as_str())?;
356            if !response.is_empty() {
357                bail!("unexpected response: '{response}'");
358            }
359        }
360
361        Ok(())
362    }
363
364    /// Read a register: this function does not attempt to translate the
365    /// name or number of the register. If force is set, bypass OpenOCD's
366    /// register cache.
367    fn read_register<T: ParseInt>(&mut self, reg_name: &str, force: bool) -> Result<T> {
368        let cmd = format!(
369            "get_reg {} {{ {} }}",
370            if force { "-force" } else { "" },
371            reg_name,
372        );
373        let response = self.send_tcl_cmd(cmd.as_str())?;
374        // the expected output format is 'reg_name 0xabcdef', e.g 'pc 0x10009858'
375        let (out_reg_name, value) = response.trim().split_once(' ').with_context(|| {
376            format!("expected response of the form 'reg value', got '{response}'")
377        })?;
378        ensure!(
379            out_reg_name == reg_name,
380            "OpenOCD returned the value for register '{out_reg_name}' instead of '{reg_name}"
381        );
382        T::from_str(value).context(format!(
383            "expected value to be an hexadecimal string, got '{value}'"
384        ))
385    }
386
387    fn write_register<T: ToString>(&mut self, reg_name: &str, value: T) -> Result<()> {
388        let cmd = format!("set_reg {{ {reg_name} {} }}", T::to_string(&value));
389        let response = self.send_tcl_cmd(cmd.as_str())?;
390        if !response.is_empty() {
391            bail!("unexpected response: '{response}'");
392        }
393
394        Ok(())
395    }
396}
397
398impl Jtag for OpenOcdJtagTap {
399    fn into_raw(self: Box<Self>) -> Result<OpenOcd> {
400        Ok(self.openocd)
401    }
402
403    fn as_raw(&mut self) -> Result<&mut OpenOcd> {
404        Ok(&mut self.openocd)
405    }
406
407    fn disconnect(self: Box<Self>) -> Result<()> {
408        self.openocd.shutdown()
409    }
410
411    fn tap(&self) -> JtagTap {
412        self.jtag_tap
413    }
414
415    fn read_lc_ctrl_reg(&mut self, reg: &LcCtrlReg) -> Result<u32> {
416        ensure!(
417            matches!(self.jtag_tap, JtagTap::LcTap),
418            JtagError::Tap(self.jtag_tap)
419        );
420        let reg_offset = reg.word_offset();
421        let cmd = format!("riscv dmi_read 0x{reg_offset:x}");
422        let response = self.send_tcl_cmd(cmd.as_str())?;
423
424        let value = u32::from_str(response.trim()).context(format!(
425            "expected response to be hexadecimal word, got '{response}'"
426        ))?;
427
428        Ok(value)
429    }
430
431    fn write_lc_ctrl_reg(&mut self, reg: &LcCtrlReg, value: u32) -> Result<()> {
432        ensure!(
433            matches!(self.jtag_tap, JtagTap::LcTap),
434            JtagError::Tap(self.jtag_tap)
435        );
436        let reg_offset = reg.word_offset();
437        let cmd = format!("riscv dmi_write 0x{reg_offset:x} 0x{value:x}");
438        let response = self.send_tcl_cmd(cmd.as_str())?;
439
440        if !response.is_empty() {
441            bail!("unexpected response: '{response}'");
442        }
443
444        Ok(())
445    }
446
447    fn read_memory(&mut self, addr: u32, buf: &mut [u8]) -> Result<usize> {
448        ensure!(
449            matches!(self.jtag_tap, JtagTap::RiscvTap),
450            JtagError::Tap(self.jtag_tap)
451        );
452        self.read_memory_impl(addr, buf)
453    }
454
455    fn read_memory32(&mut self, addr: u32, buf: &mut [u32]) -> Result<usize> {
456        ensure!(
457            matches!(self.jtag_tap, JtagTap::RiscvTap),
458            JtagError::Tap(self.jtag_tap)
459        );
460        self.read_memory_impl(addr, buf)
461    }
462
463    fn write_memory(&mut self, addr: u32, buf: &[u8]) -> Result<()> {
464        ensure!(
465            matches!(self.jtag_tap, JtagTap::RiscvTap),
466            JtagError::Tap(self.jtag_tap)
467        );
468        self.write_memory_impl(addr, buf)
469    }
470
471    fn write_memory32(&mut self, addr: u32, buf: &[u32]) -> Result<()> {
472        ensure!(
473            matches!(self.jtag_tap, JtagTap::RiscvTap),
474            JtagError::Tap(self.jtag_tap)
475        );
476        self.write_memory_impl(addr, buf)
477    }
478
479    fn halt(&mut self) -> Result<()> {
480        ensure!(
481            matches!(self.jtag_tap, JtagTap::RiscvTap),
482            JtagError::Tap(self.jtag_tap)
483        );
484        let response = self.send_tcl_cmd("halt")?;
485        if !response.is_empty() {
486            bail!("unexpected response: '{response}'");
487        }
488
489        Ok(())
490    }
491
492    fn wait_halt(&mut self, timeout: Duration) -> Result<()> {
493        ensure!(
494            matches!(self.jtag_tap, JtagTap::RiscvTap),
495            JtagError::Tap(self.jtag_tap)
496        );
497        let cmd = format!("wait_halt {}", timeout.as_millis());
498        let response = self.send_tcl_cmd(cmd.as_str())?;
499        if !response.is_empty() {
500            bail!("unexpected response: '{response}'");
501        }
502        Ok(())
503    }
504
505    fn resume(&mut self) -> Result<()> {
506        ensure!(
507            matches!(self.jtag_tap, JtagTap::RiscvTap),
508            JtagError::Tap(self.jtag_tap)
509        );
510        let response = self.send_tcl_cmd("resume")?;
511        if !response.is_empty() {
512            bail!("unexpected response: '{response}'");
513        }
514
515        Ok(())
516    }
517
518    fn resume_at(&mut self, addr: u32) -> Result<()> {
519        ensure!(
520            matches!(self.jtag_tap, JtagTap::RiscvTap),
521            JtagError::Tap(self.jtag_tap)
522        );
523        let cmd = format!("resume 0x{:x}", addr);
524        let response = self.send_tcl_cmd(&cmd)?;
525        if !response.is_empty() {
526            bail!("unexpected response: '{response}'");
527        }
528
529        Ok(())
530    }
531
532    fn reset(&mut self, run: bool) -> Result<()> {
533        ensure!(
534            matches!(self.jtag_tap, JtagTap::RiscvTap),
535            JtagError::Tap(self.jtag_tap)
536        );
537        let cmd = format!("reset {}", if run { "run" } else { "halt" });
538        let response = self.send_tcl_cmd(&cmd)?;
539        if !response.is_empty() {
540            bail!("unexpected response: '{response}'");
541        }
542
543        Ok(())
544    }
545
546    fn step(&mut self) -> Result<()> {
547        ensure!(
548            matches!(self.jtag_tap, JtagTap::RiscvTap),
549            JtagError::Tap(self.jtag_tap)
550        );
551        let response = self.send_tcl_cmd("step")?;
552        if !response.is_empty() {
553            bail!("unexpected response: '{response}'");
554        }
555
556        Ok(())
557    }
558
559    fn step_at(&mut self, addr: u32) -> Result<()> {
560        ensure!(
561            matches!(self.jtag_tap, JtagTap::RiscvTap),
562            JtagError::Tap(self.jtag_tap)
563        );
564        let cmd = format!("step 0x{:x}", addr);
565        let response = self.send_tcl_cmd(&cmd)?;
566        if !response.is_empty() {
567            bail!("unexpected response: '{response}'");
568        }
569
570        Ok(())
571    }
572
573    fn read_riscv_reg(&mut self, reg: &RiscvReg) -> Result<u32> {
574        ensure!(
575            matches!(self.jtag_tap, JtagTap::RiscvTap),
576            JtagError::Tap(self.jtag_tap)
577        );
578        self.read_register::<u32>(reg.name(), true)
579    }
580
581    fn write_riscv_reg(&mut self, reg: &RiscvReg, val: u32) -> Result<()> {
582        ensure!(
583            matches!(self.jtag_tap, JtagTap::RiscvTap),
584            JtagError::Tap(self.jtag_tap)
585        );
586        self.write_register(reg.name(), val)
587    }
588
589    fn set_breakpoint(&mut self, address: u32, hw: bool) -> Result<()> {
590        let cmd = format!("bp {:#x} 2{}", address, if hw { " hw" } else { "" });
591        let response = self.send_tcl_cmd(&cmd)?;
592        if !response.starts_with("breakpoint set at ") {
593            bail!("unexpected response: '{response}'");
594        }
595        Ok(())
596    }
597
598    fn remove_breakpoint(&mut self, addr: u32) -> Result<()> {
599        let cmd = format!("rbp {:#x}", addr);
600        let response = self.send_tcl_cmd(&cmd)?;
601        if !response.is_empty() {
602            bail!("unexpected response: '{response}'");
603        }
604        Ok(())
605    }
606
607    fn remove_all_breakpoints(&mut self) -> Result<()> {
608        let response = self.send_tcl_cmd("rbp all")?;
609        if !response.is_empty() {
610            bail!("unexpected response: '{response}'");
611        }
612        Ok(())
613    }
614}