[opentitantool] Add support for HyperDebug

Add a Transport implementation for the HyperDebug board.  The
implementation uses the rusb (Rust libusb) for finding the HyperDebug
USB device, and for bulk communication to do e.g. SPI communication.
Additionally, the impementation open the Linux /dev/ttyUSBn files
using a Rust serialport crate, which is used for serial communication
with the OPenTitan chip, as well as for sending generic text commands
to HyperDebug itself, to do e.g. GPIO-related operations.

Signed-off-by: Jes B. Klinke <jbk@chromium.org>
Change-Id: Id04c55d59da1f245da111c5a04f64f53cd2449a9
diff --git a/sw/host/opentitanlib/BUILD b/sw/host/opentitanlib/BUILD
index 6decd86..83153d0 100644
--- a/sw/host/opentitanlib/BUILD
+++ b/sw/host/opentitanlib/BUILD
@@ -29,6 +29,10 @@
         "src/transport/cw310/spi.rs",
         "src/transport/cw310/uart.rs",
         "src/transport/cw310/usb.rs",
+        "src/transport/hyperdebug/gpio.rs",
+        "src/transport/hyperdebug/mod.rs",
+        "src/transport/hyperdebug/spi.rs",
+        "src/transport/hyperdebug/uart.rs",
         "src/transport/mod.rs",
         "src/transport/ultradebug/gpio.rs",
         "src/transport/ultradebug/mod.rs",
diff --git a/sw/host/opentitanlib/src/transport/hyperdebug/gpio.rs b/sw/host/opentitanlib/src/transport/hyperdebug/gpio.rs
new file mode 100644
index 0000000..0f086d7
--- /dev/null
+++ b/sw/host/opentitanlib/src/transport/hyperdebug/gpio.rs
@@ -0,0 +1,50 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+use anyhow::Result;
+use std::rc::Rc;
+
+use crate::io::gpio::{GpioPin, PinDirection};
+use crate::transport::hyperdebug::{Hyperdebug, Inner, Error};
+
+pub struct HyperdebugGpioPin {
+    inner: Rc<Inner>,
+    pinname: String,
+}
+
+impl HyperdebugGpioPin {
+    pub fn open(hyperdebug: &Hyperdebug, pinname: &str) -> Result<Self> {
+        let result = Self {
+            inner: Rc::clone(&hyperdebug.inner),
+            pinname: pinname.to_string(),
+        };
+        Ok(result)
+    }
+}
+
+impl GpioPin for HyperdebugGpioPin {
+    /// Reads the value of the the GPIO pin `id`.
+    fn read(&self) -> Result<bool> {
+        let mut result: Result<bool> =
+            Err(Error::CommunicationError("No output from gpioget").into());
+        self.inner.execute_command(
+            &format!("gpioget {}", &self.pinname),
+            |line| {
+                result = Ok(line.trim_start().starts_with("1"))
+            })?;
+        result
+    }
+
+    /// Sets the value of the GPIO pin `id` to `value`.
+    fn write(&self, value: bool) -> Result<()> {
+        self.inner.execute_command(
+            &format!("gpioset {} {}", &self.pinname, if value { 1 } else { 0 }),
+            |_| {})
+    }
+
+    /// Sets the `direction` of GPIO `id` as input or output.
+    fn set_direction(&self, _direction: PinDirection) -> Result<()> {
+        unimplemented!()
+    }
+}
diff --git a/sw/host/opentitanlib/src/transport/hyperdebug/mod.rs b/sw/host/opentitanlib/src/transport/hyperdebug/mod.rs
new file mode 100644
index 0000000..0c49384
--- /dev/null
+++ b/sw/host/opentitanlib/src/transport/hyperdebug/mod.rs
@@ -0,0 +1,422 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+use anyhow::{ensure, Result};
+
+use std::cell::RefCell;
+use std::collections::hash_map::Entry;
+use std::collections::HashMap;
+use std::fs;
+use std::io::ErrorKind;
+use std::io::Read;
+use std::io::Write;
+use std::path::{Path, PathBuf};
+use std::rc::Rc;
+
+use thiserror::Error;
+
+use crate::io::gpio::GpioPin;
+use crate::io::spi::Target;
+use crate::io::uart::Uart;
+use crate::transport::{Capabilities, Capability, Transport, TransportError};
+use crate::collection;
+
+pub mod gpio;
+pub mod uart;
+pub mod spi;
+
+/// Implementation of the Transport trait for HyperDebug based on the
+/// Nucleo-L552ZE-Q.
+pub struct Hyperdebug {
+    spi_names: HashMap<String, u8>,
+    spi_interface: BulkInterface,
+    uart_ttys: HashMap<String, PathBuf>,
+    inner: Rc<Inner>,
+}
+
+/// Index of a single USB "interface", with its associated IN and OUT
+/// endpoints.  Used to instantiate e.g. SPI trait.
+#[derive(Copy, Clone)]
+pub struct BulkInterface {
+    interface: u8,
+    in_endpoint: u8,
+    out_endpoint: u8,
+}
+
+impl Hyperdebug {
+    pub const VID_GOOGLE: u16 = 0x18d1;
+    pub const PID_HYPERDEBUG: u16 = 0x520e;
+
+    /// Establish connection with a particular HyperDebug.
+    pub fn open(usb_vid: Option<u16>, usb_pid: Option<u16>, usb_serial: &Option<String>)
+                -> Result<Self> {
+        let devices = Self::scan(usb_vid, usb_pid, usb_serial)?;
+        ensure!(!devices.is_empty(), Error::NoDevice);
+        ensure!(devices.len() == 1, Error::MultipleDevices);
+        match devices.get(0) {
+            Some((device, _)) => Self::do_open(device),
+            _ => unimplemented!(),
+        }
+    }
+
+    fn do_open(device: &rusb::Device<rusb::GlobalContext>) -> Result<Self> {
+        let path = PathBuf::from("/sys/bus/usb/devices");
+
+        let mut console_tty: Option<PathBuf> = None;
+        let mut spi_interface: Option<BulkInterface> = None;
+        let mut uart_ttys: HashMap<String, PathBuf> = HashMap::new();
+
+        let handle = device.open()?;
+        let config_desc = device.active_config_descriptor()?;
+        // Iterate through each USB interface, discovering e.g. supported UARTs.
+        for interface in config_desc.interfaces() {
+            for interface_desc in interface.descriptors() {
+                let idx = match interface_desc.description_string_index() {
+                    Some(idx) => idx,
+                    None => { continue }
+                };
+                let interface_name = match handle.read_string_descriptor_ascii(idx) {
+                    Ok(interface_name) => interface_name,
+                    _ => { continue }
+                };
+                let ports = device.port_numbers()?.iter().map(
+                    |id| id.to_string()).collect::<Vec<String>>().join(".");
+                let interface_path = path
+                    .join(format!("{}-{}", device.bus_number(), ports))
+                    .join(format!("{}-{}:{}.{}",device.bus_number(), ports,
+                                  config_desc.number(), interface.number()));
+                // Check the ASCII name of this USB interface.
+                match interface_name.as_str() {
+                    "HyperDebug Shell" => {
+                        // We found the "main" control interface of HyperDebug, allowing textual
+                        // commands to be sent, to e.g. manipoulate GPIOs.
+                        console_tty = Some(Self::find_tty(&interface_path)?)
+                    }
+                    name if name.starts_with("UART") => {
+                        // We found an UART forwarding USB interface.
+                        uart_ttys.insert(
+                            name.to_string(),
+                            Self::find_tty(&interface_path)?);
+                    }
+                    "SPI" => {
+                        // We found the SPI forwarding USB interface (this one interface allows
+                        // multiplexing physical SPI ports.)
+                        let mut in_endpoint: Option<u8> = None;
+                        let mut out_endpoint: Option<u8> = None;
+                        for endpoint_desc in interface_desc.endpoint_descriptors() {
+                            if endpoint_desc.transfer_type() != rusb::TransferType::Bulk {
+                                continue;
+                            }
+                            match endpoint_desc.direction() {
+                                rusb::Direction::In => {
+                                    if let Some(_) = in_endpoint.replace(endpoint_desc.address()) {
+                                        return Err(Error::CommunicationError
+                                                   ("Multiple SPI IN endpoints").into());
+                                    }
+                                }
+                                rusb::Direction::Out => {
+                                    if let Some(_) = out_endpoint.replace(endpoint_desc.address()) {
+                                        return Err(Error::CommunicationError
+                                                   ("Multiple SPI OUT endpoints").into());
+                                    }
+                                }
+                            }
+                        }
+                        match (in_endpoint, out_endpoint) {
+                            (Some(in_endpoint), Some(out_endpoint)) => {
+                                if let Some(_) = spi_interface.replace(BulkInterface {
+                                    interface: interface.number(),
+                                    in_endpoint,
+                                    out_endpoint,
+                                }) {
+                                    return Err(Error::CommunicationError
+                                               ("Multiple SPI interfaces").into());
+                                }
+                            }
+                            _ => {
+                                return Err(Error::CommunicationError
+                                           ("Missing SPI interface").into());
+                            }
+                        }
+                    }
+                    _ => ()
+                }
+            }
+        }
+        // Eventually, the SPI aliases below should either go into configuration file, or come
+        // from the HyperDebug firmware, declaring what it supports (as is the case with UARTs.)
+        let spi_names: HashMap<String, u8> = collection! {
+            "SPI2".to_string() => 0,
+            "0".to_string() => 0,
+        };
+        let result = Hyperdebug {
+            spi_names,
+            spi_interface: spi_interface.ok_or(Error::CommunicationError("Missing SPI interface"))?,
+            uart_ttys,
+            inner: Rc::new(Inner {
+                console_tty: console_tty.ok_or(Error::CommunicationError("Missing console interface"))?,
+                usb_handle: RefCell::new(handle),
+                gpio: Default::default(),
+                spis: Default::default(),
+                uarts: Default::default(),
+            })
+        };
+        Ok(result)
+    }
+
+    /// Scan the USB bus for "our" devices, (shamelessly copied from
+    /// cw310, may be able to share.)
+    fn scan(
+        usb_vid: Option<u16>,
+        usb_pid: Option<u16>,
+        usb_serial: &Option<String>,
+    ) -> Result<Vec<(rusb::Device<rusb::GlobalContext>, String)>> {
+        let usb_vid = usb_vid.unwrap_or(Self::VID_GOOGLE);
+        let usb_pid = usb_pid.unwrap_or(Self::PID_HYPERDEBUG);
+        let mut devices = Vec::new();
+        for device in rusb::devices()?.iter() {
+            let descriptor = match device.device_descriptor() {
+                Ok(desc) => desc,
+                _ => {
+                    log::error!(
+                        "Could not read device descriptor for device at bus={} address={}",
+                        device.bus_number(),
+                        device.address()
+                    );
+                    continue;
+                }
+            };
+            if descriptor.vendor_id() != usb_vid {
+                continue;
+            }
+            if descriptor.product_id() != usb_pid {
+                continue;
+            }
+            let handle = match device.open() {
+                Ok(handle) => handle,
+                _ => {
+                    log::error!(
+                        "Could not open device at bus={} address={}",
+                        device.bus_number(),
+                        device.address()
+                    );
+                    continue;
+                }
+            };
+            let serial_number = match handle.read_serial_number_string_ascii(&descriptor) {
+                Ok(sn) => sn,
+                _ => {
+                    log::error!(
+                        "Could not read serial number from device at bus={} address={}",
+                        device.bus_number(),
+                        device.address()
+                    );
+                    continue;
+                }
+            };
+            if let Some(sn) = usb_serial {
+                if &serial_number != sn {
+                    continue;
+                }
+            }
+            devices.push((device, serial_number));
+        }
+        Ok(devices)
+    }
+
+    /// Locates the /dev/ttyUSBn node corresponding to a given interface in the sys directory
+    /// tree, e.g. /sys/bus/usb/devices/1-4/1-4:1.0 .
+    fn find_tty(path: &Path) -> Result<PathBuf> {
+        for entry in fs::read_dir(path)? {
+            let entry = entry?;
+            if let Ok(filename) = entry.file_name().into_string() {
+                if filename.starts_with("tty") {
+                    return Ok(PathBuf::from("/dev").join(entry.file_name()));
+                }
+            }
+        }
+        Err(Error::CommunicationError("Did not find ttyUSBn device").into())
+    }
+}
+
+/// Internal state of the Hyperdebug struct, this struct is reference counted such that Gpio,
+/// Spi and Uart sub-structs can all refer to this shared data, which is guaranteed to live on,
+/// even if the caller lets the outer Hyperdebug struct run out of scope.
+pub struct Inner {
+    console_tty: PathBuf,
+    usb_handle: RefCell<rusb::DeviceHandle<rusb::GlobalContext>>,
+    gpio: RefCell<HashMap<String, Rc<dyn GpioPin>>>,
+    spis: RefCell<HashMap<u8, Rc<dyn Target>>>,
+    uarts: RefCell<HashMap<PathBuf, Rc<dyn Uart>>>,
+}
+
+impl Inner {
+
+    /// Send a command to HyperDebug firmware, with a callback to receive any output.
+    pub fn execute_command(&self, cmd: &str, mut callback: impl FnMut(&str)) -> Result<()> {
+        let mut port = serialport::new(
+            self.console_tty.to_str().ok_or(Error::UnicodePathError)?,
+            115_200,
+        )
+        .timeout(std::time::Duration::from_millis(10))
+        .open()
+        .expect("Failed to open port");
+
+        // Ideally, we would invoke Linux flock() on the serial
+        // device, to detect minicom or another instance of
+        // opentitantool having the same serial port open.  Incoming
+        // serial data could go silenly missing, in such cases.
+        let mut buf = [0u8; 128];
+        loop {
+            match port.read(&mut buf) {
+                Ok(rc) => {
+                    log::info!("Discarded {} characters: {:?}\n", rc,
+                               &std::str::from_utf8(&buf[0..rc]));
+                }
+                Err(error) if error.kind() == ErrorKind::TimedOut => {
+                    break;
+                }
+                Err(error) => {
+                    return Err(error.into())
+                }
+            }
+        }
+        // Send Ctrl-C, followed by the command, then newline.  This will discard any previous
+        // partial input, before executing our command.
+        port.write(format!("\x03{}\n", cmd).as_bytes())?;
+
+        // Now process response from HyperDebug.  First we expect to see the echo of the command
+        // we just "typed". Then zero, one or more lines of useful output, which we want to pass
+        // to the callback, and then a prompt characters, indicating that the output is
+        // complete.
+        let mut seen_echo = false;
+        let mut len: usize = 0;
+        let mut repeated_timeouts: u8 = 0;
+        loop {
+            // Read more data, appending to existing buffer.
+            match port.read(&mut buf[len..128]) {
+                Ok(rc) => {
+                    repeated_timeouts = 0;
+                    len += rc;
+                    // See if we have one or more lines terminated with endline, if so, process
+                    // those and remove from the buffer by shifting the remaning data to the
+                    // front of the buffer.
+                    let mut line_start = 0;
+                    for i in 0..len {
+                        if buf[i] == b'\n' {
+                            // Found a complete line, process it
+                            let mut line_end = i;
+                            if line_end > line_start && buf[line_end - 1] == 13 {
+                                line_end -= 1;
+                            }
+                            let line = std::str::from_utf8(&buf[line_start..line_end])?;
+                            if seen_echo {
+                                callback(line);
+                            } else {
+                                if line.len() >= cmd.len()
+                                    && line[line.len() - cmd.len()..] == *cmd {
+                                    seen_echo = true;
+                                }
+                            }
+                            line_start = i + 1;
+                        }
+                    }
+                    // If any lines were processed, remove from the buffer.
+                    if line_start > 0 {
+                        buf.rotate_left(line_start);
+                        len -= line_start;
+                    }
+                }
+                Err(error) if error.kind() == ErrorKind::TimedOut => {
+                    if std::str::from_utf8(&buf[0..len])? == "> " {
+                        // No data arrived for a while, and the last we got was a command
+                        // prompt, this is what we expect when the command has finished
+                        // successfully.
+                        return Ok(())
+                    } else {
+                        // No data arrived for a while, but the last was no a command prompt,
+                        // this could be the command taking a little time to produce its output,
+                        // wait a longer while for additional data.  (Implemented by repeated
+                        // calls, alternatively could have been done by fiddling with timeout
+                        // setting of the underlying serial port object.)
+                        repeated_timeouts += 1;
+                        if repeated_timeouts == 10 {
+                            return Err(error.into());
+                        }
+                    }
+                }
+                Err(error) => {
+                    return Err(error.into())
+                }
+            }
+        }
+    }
+}
+
+#[derive(Debug, Error)]
+pub enum Error {
+    #[error("USB device did not match")]
+    NoMatch,
+    #[error("Found no HyperDebug USB device")]
+    NoDevice,
+    #[error("Found multiple HyperDebug USB devices, use --serial")]
+    MultipleDevices,
+    #[error("Error communicating with HyperDebug: {0}")]
+    CommunicationError(&'static str),
+    #[error("Encountered non-unicode path")]
+    UnicodePathError,
+}
+
+impl Transport for Hyperdebug {
+    fn capabilities(&self) -> Capabilities {
+        Capabilities::new(Capability::UART | Capability::GPIO | Capability::SPI)
+    }
+
+    // Crate SPI Target instance, or return one from a cache of previously created instances.
+    fn spi(&self, instance: &str) -> Result<Rc<dyn Target>> {
+        let &idx = self.spi_names.get(instance)
+            .ok_or_else(|| TransportError::InvalidInstance("spi", instance.to_string()))?;
+        if let Some(instance) = self.inner.spis.borrow().get(&idx) {
+            return Ok(Rc::clone(instance));
+        }
+        let instance: Rc<dyn Target> = Rc::new(spi::HyperdebugSpiTarget::open(
+            &self,
+            idx,
+        )?);
+        self.inner.spis.borrow_mut().insert(idx, Rc::clone(&instance));
+        Ok(instance)
+    }
+    
+    // Crate Uart instance, or return one from a cache of previously created instances.
+    fn uart(&self, instance: &str) -> Result<Rc<dyn Uart>> {
+        match self.uart_ttys.get(instance) {
+            Some(tty) => {
+                if let Some(instance) = self.inner.uarts.borrow().get(tty) {
+                    return Ok(Rc::clone(instance));
+                }
+                let instance: Rc<dyn Uart> = Rc::new(uart::HyperdebugUart::open(
+                            &self,
+                            tty,
+                        )?);
+                self.inner.uarts.borrow_mut().insert(tty.clone(), Rc::clone(&instance));
+                Ok(instance)
+            }
+            _ => Err(TransportError::InvalidInstance("uart", instance.to_string()).into())
+        }
+    }
+    
+    // Crate GpioPin instance, or return one from a cache of previously created instances.
+    fn gpio_pin(&self, pinname: &str) -> Result<Rc<dyn GpioPin>> {
+        Ok(match self.inner.gpio.borrow_mut().entry(pinname.to_string()) {
+            Entry::Vacant(v) => {
+                let u = v.insert(Rc::new(gpio::HyperdebugGpioPin::open(
+                    &self,
+                    pinname,
+                )?));
+                Rc::clone(u)
+            }
+            Entry::Occupied(o) => Rc::clone(o.get()),
+        })
+    }
+}
diff --git a/sw/host/opentitanlib/src/transport/hyperdebug/spi.rs b/sw/host/opentitanlib/src/transport/hyperdebug/spi.rs
new file mode 100644
index 0000000..1d0ad92
--- /dev/null
+++ b/sw/host/opentitanlib/src/transport/hyperdebug/spi.rs
@@ -0,0 +1,317 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+use anyhow::{ensure, Result};
+use rusb::{Direction,RequestType,Recipient};
+use std::mem::size_of;
+use std::time::Duration;
+use std::rc::Rc;
+use zerocopy::{AsBytes, FromBytes};
+
+use crate::io::spi::{SpiError, Target, Transfer, TransferMode};
+use crate::transport::hyperdebug::{Hyperdebug, Inner, BulkInterface, Error};
+
+pub struct HyperdebugSpiTarget {
+    inner: Rc<Inner>,
+    interface: BulkInterface,
+    _target_idx: u8,
+    max_chunk_size: usize,
+}
+
+const USB_SPI_PKT_ID_CMD_GET_USB_SPI_CONFIG: u16 = 0;
+const USB_SPI_PKT_ID_RSP_USB_SPI_CONFIG: u16 = 1;
+const USB_SPI_PKT_ID_CMD_TRANSFER_START: u16 = 2;
+const USB_SPI_PKT_ID_CMD_TRANSFER_CONTINUE: u16 = 3;
+//const USB_SPI_PKT_ID_CMD_RESTART_RESPONSE: u16 = 4;
+const USB_SPI_PKT_ID_RSP_TRANSFER_START: u16 = 5;
+const USB_SPI_PKT_ID_RSP_TRANSFER_CONTINUE: u16 = 6;
+
+//const USB_SPI_REQ_DISABLE: u8 = 1;
+const USB_SPI_REQ_ENABLE: u8 = 0;
+
+const USB_MAX_SIZE: usize = 64;
+const FULL_DUPLEX: usize = 65535;
+
+const TIMEOUT: Duration = Duration::from_millis(500);
+
+#[derive(AsBytes, FromBytes, Debug, Default)]
+#[repr(C)]
+struct RspUsbSpiConfig {
+    packet_id: u16,
+    max_write_chunk: u16,
+    max_read_chunk: u16,
+    feature_bitmap: u16,
+}
+
+#[derive(AsBytes, FromBytes, Debug)]
+#[repr(C)]
+struct CmdTransferStart {
+    packet_id: u16,
+    write_count: u16,
+    read_count: u16,
+    data: [u8; USB_MAX_SIZE - 6],
+}
+impl CmdTransferStart {
+    fn new() -> Self {
+        Self {
+            packet_id: USB_SPI_PKT_ID_CMD_TRANSFER_START,
+            write_count: 0,
+            read_count: 0,
+            data: [0; USB_MAX_SIZE - 6],
+        }
+    }
+}
+
+#[derive(AsBytes, FromBytes, Debug)]
+#[repr(C)]
+struct CmdTransferContinue {
+    packet_id: u16,
+    data_index: u16,
+    data: [u8; USB_MAX_SIZE - 4],
+}
+impl CmdTransferContinue {
+    fn new() -> Self {
+        Self {
+            packet_id: USB_SPI_PKT_ID_CMD_TRANSFER_CONTINUE,
+            data_index: 0,
+            data: [0; USB_MAX_SIZE - 4],
+        }
+    }
+}
+
+#[derive(AsBytes, FromBytes, Debug)]
+#[repr(C)]
+struct RspTransferStart {
+    packet_id: u16,
+    status_code: u16,
+    data: [u8; USB_MAX_SIZE - 4],
+}
+impl RspTransferStart {
+    fn new() -> Self {
+        Self {
+            packet_id: 0,
+            status_code: 0,
+            data: [0; USB_MAX_SIZE - 4],
+        }
+    }
+}
+
+#[derive(AsBytes, FromBytes, Debug)]
+#[repr(C)]
+struct RspTransferContinue {
+    packet_id: u16,
+    data_index: u16,
+    data: [u8; USB_MAX_SIZE - 4],
+}
+impl RspTransferContinue {
+    fn new() -> Self {
+        Self {
+            packet_id: 0,
+            data_index: 0,
+            data: [0; USB_MAX_SIZE - 4],
+        }
+    }
+}
+
+impl HyperdebugSpiTarget {
+    pub fn open(hyperdebug: &Hyperdebug, idx: u8) -> Result<Self> {
+        let mut usb_handle = hyperdebug.inner.usb_handle.borrow_mut();
+        
+        // Tell HyperDebug to enable SPI bridge.
+        usb_handle.write_control(
+            rusb::request_type(Direction::Out, RequestType::Vendor, Recipient::Interface),
+            USB_SPI_REQ_ENABLE,
+            0 /* wValue */,
+            hyperdebug.spi_interface.interface as u16,
+            &mut [],
+            TIMEOUT)?;
+
+        // Exclusively claim SPI interface, preparing for bulk transfers.
+        usb_handle.claim_interface(hyperdebug.spi_interface.interface)?;
+
+        // Initial bulk request/response to query capabilities.
+        usb_handle.write_bulk(hyperdebug.spi_interface.out_endpoint,
+                              &USB_SPI_PKT_ID_CMD_GET_USB_SPI_CONFIG.to_le_bytes(),
+                              TIMEOUT)?;
+        let mut resp: RspUsbSpiConfig = Default::default();
+        let rc = usb_handle.read_bulk(hyperdebug.spi_interface.in_endpoint, resp.as_bytes_mut(),
+                                      TIMEOUT)?;
+        ensure!(
+            rc == size_of::<RspUsbSpiConfig>(),
+            Error::CommunicationError("Unrecognized reponse to GET_USB_SPI_CONFIG")
+        );
+        ensure!(
+            resp.packet_id == USB_SPI_PKT_ID_RSP_USB_SPI_CONFIG,
+            Error::CommunicationError("Unrecognized reponse to GET_USB_SPI_CONFIG")
+        );
+        // Verify that interface supports concurrent read/write.
+        ensure!(
+            (resp.feature_bitmap & 0x0001) != 0,
+            Error::CommunicationError("HyperDebug does not support bidirectional SPI")
+        );
+
+        Ok(Self {
+            inner: Rc::clone(&hyperdebug.inner),
+            interface: hyperdebug.spi_interface,
+            _target_idx: idx,
+            max_chunk_size: std::cmp::min(resp.max_write_chunk, resp.max_read_chunk) as usize,
+        })
+    }
+
+    /// Transmit data for a single SPI operation, using one or more USB packets.
+    fn transmit(&self, wbuf: &[u8], rbuf_len: usize) -> Result<()> {
+        let mut req = CmdTransferStart::new();
+        req.write_count = wbuf.len() as u16;
+        req.read_count = rbuf_len as u16;
+        let databytes = std::cmp::min(USB_MAX_SIZE - 6, wbuf.len());
+        req.data[0..databytes].clone_from_slice(&wbuf[0..databytes]);
+        self.usb_write_bulk(&req.as_bytes()[0..6 + databytes])?;
+        let mut index = databytes;
+
+        while index < wbuf.len() {
+            let mut req = CmdTransferContinue::new();
+            req.data_index = index as u16;
+            let databytes = std::cmp::min(USB_MAX_SIZE - 4, wbuf.len() - index);
+            req.data[0..databytes].clone_from_slice(&wbuf[index..index + databytes]);
+            self.usb_write_bulk(&req.as_bytes()[0..4 + databytes])?;
+            index += databytes;
+        }
+        Ok(())
+    }
+    
+    /// Receive data for a single SPI operation, using one or more USB packets.
+    fn receive(&self, rbuf: &mut [u8]) -> Result<()> {
+        let mut resp = RspTransferStart::new();
+        let bytecount = self.usb_read_bulk(&mut resp.as_bytes_mut())?;
+        ensure!(
+            bytecount >= 4,
+            Error::CommunicationError("Unrecognized reponse to TRANSFER_START")
+        );
+        ensure!(
+            resp.packet_id == USB_SPI_PKT_ID_RSP_TRANSFER_START,
+            Error::CommunicationError("Unrecognized reponse to TRANSFER_START")
+        );
+        ensure!(resp.status_code == 0, Error::CommunicationError("SPI error"));
+        let databytes = bytecount - 4;
+        rbuf[0..databytes].clone_from_slice(&resp.data[0..databytes]);
+        let mut index = databytes;
+        while index < rbuf.len() {
+            let mut resp = RspTransferContinue::new();
+            let bytecount = self.usb_read_bulk(&mut resp.as_bytes_mut())?;
+            ensure!(
+                bytecount > 4,
+                Error::CommunicationError("Unrecognized reponse to TRANSFER_START")
+            );
+            ensure!(
+                resp.packet_id == USB_SPI_PKT_ID_RSP_TRANSFER_CONTINUE,
+                Error::CommunicationError("Unrecognized reponse to TRANSFER_START")
+            );
+            ensure!(
+                resp.data_index == index as u16,
+                Error::CommunicationError("Unexpected byte index in reponse to TRANSFER_START")
+            );
+            let databytes = bytecount - 4;
+            rbuf[index..index + databytes].clone_from_slice(&resp.data[0..0 + databytes]);
+            index += databytes;
+        }
+        Ok(())
+    }
+
+    /// Send one USB packet.
+    fn usb_write_bulk(&self, buf: &[u8]) -> Result<()> {
+        self.inner.usb_handle.borrow().write_bulk(
+            self.interface.out_endpoint, buf, TIMEOUT)?;
+        Ok(())
+    }
+
+    /// Receive one USB packet.
+    fn usb_read_bulk(&self, buf: &mut [u8]) -> Result<usize> {
+        Ok(self.inner.usb_handle.borrow().read_bulk(
+            self.interface.in_endpoint, buf, TIMEOUT)?)
+    }
+}
+
+impl Target for HyperdebugSpiTarget {
+    fn get_transfer_mode(&self) -> Result<TransferMode> {
+        Ok(TransferMode::Mode0)
+    }
+    fn set_transfer_mode(&self, _mode: TransferMode) -> Result<()> {
+        todo!();
+    }
+
+    fn get_bits_per_word(&self) -> Result<u32> {
+        Ok(8)
+    }
+    fn set_bits_per_word(&self, bits_per_word: u32) -> Result<()> {
+        match bits_per_word {
+            8 => Ok(()),
+            _ => Err(SpiError::InvalidWordSize(bits_per_word).into()),
+        }
+    }
+
+    fn get_max_speed(&self) -> Result<u32> {
+        todo!();
+    }
+    fn set_max_speed(&self, _frequency: u32) -> Result<()> {
+        log::info!(
+            "Setting of SPI speed not implemented for HyperDebug, ignoring\n",
+        );
+        Ok(())
+    }
+
+    fn get_max_transfer_count(&self) -> usize {
+        // The protocol imposes no limits to the number of Transfers
+        // in a transaction.
+        usize::MAX
+    }
+
+    fn max_chunk_size(&self) -> usize {
+        self.max_chunk_size
+    }
+
+    fn run_transaction(&self, transaction: &mut [Transfer]) -> Result<()> {
+        let mut idx: usize = 0;
+        while idx < transaction.len() {
+            match &mut transaction[idx..] {
+                [Transfer::Write(wbuf), Transfer::Read(rbuf), ..] => {
+                    // Hyperdebug can do SPI write followed by SPI read as a single USB
+                    // request/reply.  Take advantage of that by detecting pairs of
+                    // Transfer::Write followed by Transfer::Read.
+                    ensure!(wbuf.len() <= self.max_chunk_size,
+                            SpiError::InvalidDataLength(wbuf.len()));
+                    ensure!(rbuf.len() <= self.max_chunk_size,
+                            SpiError::InvalidDataLength(rbuf.len()));
+                    self.transmit(wbuf, rbuf.len())?;
+                    self.receive(rbuf)?;
+                    // Skip two steps ahead, as two items were processed.
+                    idx += 2;
+                    continue;
+                }
+                [Transfer::Write(wbuf), ..] => {
+                    ensure!(wbuf.len() <= self.max_chunk_size,
+                            SpiError::InvalidDataLength(wbuf.len()));
+                    self.transmit(wbuf, 0)?;
+                    self.receive(&mut [])?;
+                }
+                [Transfer::Read(rbuf), ..] => {
+                    ensure!(rbuf.len() <= self.max_chunk_size,
+                            SpiError::InvalidDataLength(rbuf.len()));
+                    self.transmit(&[], rbuf.len())?;
+                    self.receive(rbuf)?;
+                }
+                [Transfer::Both(wbuf, rbuf), ..] => {
+                    ensure!(rbuf.len() == wbuf.len(),
+                            SpiError::MismatchedDataLength(wbuf.len(), rbuf.len()));
+                    ensure!(wbuf.len() <= self.max_chunk_size,
+                            SpiError::InvalidDataLength(wbuf.len()));
+                    self.transmit(wbuf, FULL_DUPLEX)?;
+                    self.receive(rbuf)?;
+                }
+                [] => ()
+            }
+            idx += 1;
+        }
+        Ok(())
+    }
+}
diff --git a/sw/host/opentitanlib/src/transport/hyperdebug/uart.rs b/sw/host/opentitanlib/src/transport/hyperdebug/uart.rs
new file mode 100644
index 0000000..66a7233
--- /dev/null
+++ b/sw/host/opentitanlib/src/transport/hyperdebug/uart.rs
@@ -0,0 +1,66 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+use anyhow::Result;
+
+use std::cell::RefCell;
+use std::time::Duration;
+use std::io::Read;
+use std::io::Write;
+use std::path::Path;
+
+use crate::io::uart::Uart;
+use crate::transport::hyperdebug::Error;
+use crate::transport::hyperdebug::Hyperdebug;
+
+pub struct HyperdebugUart {
+    port: RefCell<Box<dyn serialport::SerialPort>>,
+}
+
+impl HyperdebugUart {
+    pub fn open(_hyperdebug: &Hyperdebug, tty: &Path) -> Result<Self> {
+        let port =
+            serialport::new(tty.to_str().ok_or(Error::UnicodePathError)?,
+                            115_200)
+            .timeout(Duration::from_millis(100))
+            .open().expect("Failed to open port");
+        Ok(HyperdebugUart {
+            port: RefCell::new(port),
+        })
+    }
+
+    // Not really forever, but close enough.  I'd rather use Duration::MAX, but
+    // it seems that the serialport library can compute an invalid `timeval` struct
+    // to pass to `poll`, which then leads to an `Invalid argument` error when
+    // trying to `read` or `write` without a timeout.  One hundred years should be
+    // longer than any invocation of this program.
+    const FOREVER: Duration = Duration::from_secs(100 * 365 * 86400);
+}
+
+impl Uart for HyperdebugUart {
+    fn read(&self, buf: &mut [u8]) -> Result<usize> {
+        self.port.borrow_mut().set_timeout(Self::FOREVER)?;
+        Ok(self.port.borrow_mut().read(buf)?)
+    }
+
+    fn write(&self, buf: &[u8]) -> Result<usize> {
+        Ok(self.port.borrow_mut().write(buf)?)
+    }
+
+    fn get_baudrate(&self) -> u32 {
+        match self.port.borrow().baud_rate() {
+            Ok(baud_rate) => baud_rate,
+            _ => panic!("SerialPort::baud_rate() returned Err")
+        }
+    }
+
+    fn set_baudrate(&self, baudrate: u32) -> Result<()> {
+        Ok(self.port.borrow_mut().set_baud_rate(baudrate)?)
+    }
+
+    fn read_timeout(&self, buf: &mut [u8], timeout: Duration) -> Result<usize> {
+        self.port.borrow_mut().set_timeout(timeout)?;
+        Ok(self.port.borrow_mut().read(buf)?)
+    }
+}
diff --git a/sw/host/opentitanlib/src/transport/mod.rs b/sw/host/opentitanlib/src/transport/mod.rs
index 39d54cf..be13504 100644
--- a/sw/host/opentitanlib/src/transport/mod.rs
+++ b/sw/host/opentitanlib/src/transport/mod.rs
@@ -15,6 +15,7 @@
 use crate::io::uart::Uart;
 
 pub mod cw310;
+pub mod hyperdebug;
 pub mod ultradebug;
 pub mod verilator;
 
diff --git a/sw/host/opentitantool/BUILD b/sw/host/opentitantool/BUILD
index 3faa8f4..81a3b9b 100644
--- a/sw/host/opentitantool/BUILD
+++ b/sw/host/opentitantool/BUILD
@@ -11,6 +11,7 @@
     name = "opentitantool",
     srcs = [
         "src/backend/cw310.rs",
+        "src/backend/hyperdebug.rs",
         "src/backend/mod.rs",
         "src/backend/ultradebug.rs",
         "src/backend/verilator.rs",
diff --git a/sw/host/opentitantool/src/backend/hyperdebug.rs b/sw/host/opentitantool/src/backend/hyperdebug.rs
new file mode 100644
index 0000000..9e527a0
--- /dev/null
+++ b/sw/host/opentitantool/src/backend/hyperdebug.rs
@@ -0,0 +1,17 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+use anyhow::Result;
+use opentitanlib::transport::hyperdebug::Hyperdebug;
+use opentitanlib::transport::Transport;
+
+use crate::backend::BackendOpts;
+
+pub fn create(args: &BackendOpts) -> Result<Box<dyn Transport>> {
+    Ok(Box::new(Hyperdebug::open(
+        args.usb_vid,
+        args.usb_vid,
+        &args.usb_serial,
+    )?))
+}
diff --git a/sw/host/opentitantool/src/backend/mod.rs b/sw/host/opentitantool/src/backend/mod.rs
index 9d142c1..4f1d0fd 100644
--- a/sw/host/opentitantool/src/backend/mod.rs
+++ b/sw/host/opentitantool/src/backend/mod.rs
@@ -14,6 +14,7 @@
 
 pub mod cw310;
 pub mod ultradebug;
+pub mod hyperdebug;
 pub mod verilator;
 
 #[derive(Debug, StructOpt)]
@@ -49,6 +50,7 @@
         "" => create_empty_transport(),
         "verilator" => verilator::create(&args.verilator_opts),
         "ultradebug" => ultradebug::create(args),
+        "hyperdebug" => hyperdebug::create(args),
         "cw310" => cw310::create(args),
         _ => Err(Error::UnknownInterface(args.interface.clone()).into()),
     }?);