blob: 41bf63f3519615028bb0aab7546694b526d0ba3a [file] [log] [blame]
// Copyright lowRISC contributors.
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
// SPDX-License-Identifier: Apache-2.0
use anyhow::{bail, ensure, Context, Result};
use lazy_static::lazy_static;
use regex::Regex;
use serde_annotate::Annotate;
use serialport::TTYPort;
use std::any::Any;
use std::cell::Cell;
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::marker::PhantomData;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use crate::collection;
use crate::io::gpio::{GpioMonitoring, GpioPin};
use crate::io::i2c::Bus;
use crate::io::spi::Target;
use crate::io::uart::Uart;
use crate::transport::common::fpga::{ClearBitstream, FpgaProgram};
use crate::transport::common::uart::{flock_serial, SerialPortExclusiveLock, SerialPortUart};
use crate::transport::cw310::CW310;
use crate::transport::{
Capabilities, Capability, Transport, TransportError, TransportInterfaceType, UpdateFirmware,
};
use crate::util::usb::UsbBackend;
pub mod c2d2;
pub mod dfu;
pub mod gpio;
pub mod i2c;
pub mod spi;
pub use dfu::HyperdebugDfu;
/// Implementation of the Transport trait for HyperDebug based on the
/// Nucleo-L552ZE-Q.
pub struct Hyperdebug<T: Flavor> {
spi_interface: BulkInterface,
i2c_names: HashMap<String, u8>,
i2c_interface: BulkInterface,
uart_ttys: HashMap<String, PathBuf>,
inner: Rc<Inner>,
phantom: PhantomData<T>,
}
/// Trait allowing slightly different treatment of USB devices that work almost like a
/// HyperDebug. E.g. C2D2 and Servo micro.
pub trait Flavor {
fn gpio_pin(inner: &Rc<Inner>, pinname: &str) -> Result<Rc<dyn GpioPin>>;
fn get_default_usb_vid() -> u16;
fn get_default_usb_pid() -> u16;
fn load_bitstream(_transport: &impl Transport, _fpga_program: &FpgaProgram) -> Result<()> {
Err(TransportError::UnsupportedOperation.into())
}
fn clear_bitstream(_clear: &ClearBitstream) -> Result<()> {
Err(TransportError::UnsupportedOperation.into())
}
}
pub const VID_GOOGLE: u16 = 0x18d1;
pub const PID_HYPERDEBUG: u16 = 0x520e;
/// 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<T: Flavor> Hyperdebug<T> {
const USB_CLASS_VENDOR: u8 = 255;
const USB_SUBCLASS_UART: u8 = 80;
const USB_SUBCLASS_SPI: u8 = 81;
const USB_SUBCLASS_I2C: u8 = 82;
const USB_PROTOCOL_UART: u8 = 1;
const USB_PROTOCOL_SPI: u8 = 2;
const USB_PROTOCOL_I2C: u8 = 1;
/// Establish connection with a particular HyperDebug.
pub fn open(
usb_vid: Option<u16>,
usb_pid: Option<u16>,
usb_serial: Option<&str>,
) -> Result<Self> {
let device = UsbBackend::new(
usb_vid.unwrap_or_else(T::get_default_usb_vid),
usb_pid.unwrap_or_else(T::get_default_usb_pid),
usb_serial,
)?;
let path = PathBuf::from("/sys/bus/usb/devices");
let mut console_tty: Option<PathBuf> = None;
let mut spi_interface: Option<BulkInterface> = None;
let mut i2c_interface: Option<BulkInterface> = None;
let mut uart_ttys: HashMap<String, PathBuf> = HashMap::new();
let config_desc = device.active_config_descriptor()?;
if let Some(idx) = config_desc.description_string_index() {
if let Ok(current_firmware_version) = device.read_string_descriptor_ascii(idx) {
if let Some(released_firmware_version) = dfu::official_firmware_version()? {
if current_firmware_version != released_firmware_version {
log::warn!(
"Current HyperDebug firmware version is {}, newest release is {}",
current_firmware_version,
released_firmware_version,
);
log::warn!("Consider running `opentitantool transport update-firmware`");
}
}
}
};
// Iterate through each USB interface, discovering e.g. supported UARTs.
for interface in config_desc.interfaces() {
for interface_desc in interface.descriptors() {
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 class/subclass/protocol of this USB interface.
if interface_desc.class_code() == Self::USB_CLASS_VENDOR
&& interface_desc.sub_class_code() == Self::USB_SUBCLASS_UART
&& interface_desc.protocol_code() == Self::USB_PROTOCOL_UART
{
// A serial console interface, use the ascii name to determine if it is the
// HyperDebug Shell, or a UART forwarding interface.
let idx = match interface_desc.description_string_index() {
Some(idx) => idx,
None => continue,
};
let interface_name = match device.read_string_descriptor_ascii(idx) {
Ok(interface_name) => interface_name,
_ => continue,
};
if interface_name.ends_with("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)?)
} else {
// We found an UART forwarding USB interface.
uart_ttys
.insert(interface_name.to_string(), Self::find_tty(&interface_path)?);
}
}
if interface_desc.class_code() == Self::USB_CLASS_VENDOR
&& interface_desc.sub_class_code() == Self::USB_SUBCLASS_SPI
&& interface_desc.protocol_code() == Self::USB_PROTOCOL_SPI
{
// We found the SPI forwarding USB interface (this one interface allows
// multiplexing physical SPI ports.)
Self::find_endpoints_for_interface(
&mut spi_interface,
&interface,
&interface_desc,
)?;
}
if interface_desc.class_code() == Self::USB_CLASS_VENDOR
&& interface_desc.sub_class_code() == Self::USB_SUBCLASS_I2C
&& interface_desc.protocol_code() == Self::USB_PROTOCOL_I2C
{
// We found the I2C forwarding USB interface (this one interface allows
// multiplexing physical I2C ports.)
Self::find_endpoints_for_interface(
&mut i2c_interface,
&interface,
&interface_desc,
)?;
}
}
}
// Eventually, the I2C bus names below should come from the HyperDebug firmware, declaring
// what it supports (as is the case with UARTs and SPI busses.)
let i2c_names: HashMap<String, u8> = collection! {
"0".to_string() => 0,
};
let result = Hyperdebug::<T> {
spi_interface: spi_interface.ok_or_else(|| {
TransportError::CommunicationError("Missing SPI interface".to_string())
})?,
i2c_names,
i2c_interface: i2c_interface.ok_or_else(|| {
TransportError::CommunicationError("Missing I2C interface".to_string())
})?,
uart_ttys,
inner: Rc::new(Inner {
console_tty: console_tty.ok_or_else(|| {
TransportError::CommunicationError("Missing console interface".to_string())
})?,
usb_device: RefCell::new(device),
gpio: Default::default(),
spis: Default::default(),
selected_spi: Cell::new(0),
i2cs: Default::default(),
uarts: Default::default(),
}),
phantom: PhantomData,
};
Ok(result)
}
/// 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).context(format!("find TTY: read_dir({:?})", path))? {
let entry = entry.context(format!("find TTY: entity {:?}", path))?;
if let Ok(filename) = entry.file_name().into_string() {
if filename.starts_with("tty") {
return Ok(PathBuf::from("/dev").join(entry.file_name()));
}
}
}
Err(TransportError::CommunicationError("Did not find ttyUSBn device".to_string()).into())
}
fn find_endpoints_for_interface(
interface_variable_output: &mut Option<BulkInterface>,
interface: &rusb::Interface,
interface_desc: &rusb::InterfaceDescriptor,
) -> Result<()> {
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 => {
ensure!(
in_endpoint.is_none(),
TransportError::CommunicationError("Multiple IN endpoints".to_string())
);
in_endpoint.replace(endpoint_desc.address());
}
rusb::Direction::Out => {
ensure!(
out_endpoint.is_none(),
TransportError::CommunicationError("Multiple OUT endpoints".to_string())
);
out_endpoint.replace(endpoint_desc.address());
}
}
}
match (in_endpoint, out_endpoint) {
(Some(in_endpoint), Some(out_endpoint)) => {
ensure!(
interface_variable_output.is_none(),
TransportError::CommunicationError("Multiple identical interfaces".to_string())
);
interface_variable_output.replace(BulkInterface {
interface: interface.number(),
in_endpoint,
out_endpoint,
});
Ok(())
}
_ => bail!(TransportError::CommunicationError(
"Missing one or more endpoints".to_string()
)),
}
}
}
/// 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_device: RefCell<UsbBackend>,
gpio: RefCell<HashMap<String, Rc<dyn GpioPin>>>,
spis: RefCell<HashMap<u8, Rc<dyn Target>>>,
selected_spi: Cell<u8>,
i2cs: RefCell<HashMap<u8, Rc<dyn Bus>>>,
uarts: RefCell<HashMap<PathBuf, Rc<dyn Uart>>>,
}
impl Inner {
/// Send a command to HyperDebug firmware, expecting to receive no output. Any output will be
/// reported through an `Err()` return.
pub fn cmd_no_output(&self, cmd: &str) -> Result<()> {
let mut unexpected_output: bool = false;
self.execute_command(cmd, |line| {
log::warn!("Unexpected HyperDebug output: {}", line);
unexpected_output = true;
})?;
if unexpected_output {
bail!(TransportError::CommunicationError(format!(
"Unexpected output to {}",
cmd
)));
}
Ok(())
}
/// Send a command to HyperDebug firmware, expecting to receive a single line of output. Any
/// more or less output will be reported through an `Err()` return.
pub fn cmd_one_line_output(&self, cmd: &str) -> Result<String> {
let mut result: Option<String> = None;
let mut unexpected_output: bool = false;
self.execute_command(cmd, |line| {
if unexpected_output {
// Third or subsequent line, report it.
log::warn!("Unexpected HyperDebug output: {}", line);
} else if result.is_none() {
// First line, remember it.
result = Some(line.to_string());
} else {
// Second line, report the first as well as this one.
log::warn!("Unexpected HyperDebug output: {}", result.as_ref().unwrap());
log::warn!("Unexpected HyperDebug output: {}", line);
unexpected_output = true;
}
})?;
if unexpected_output {
bail!(TransportError::CommunicationError(
"Unexpected output".to_string()
));
}
match result {
None => bail!(TransportError::CommunicationError(format!(
"No response to command {}",
cmd
))),
Some(str) => Ok(str),
}
}
/// Send a command to HyperDebug firmware, expecting to receive a single line of output. Any
/// more or less output will be reported through an `Err()` return.
pub fn cmd_one_line_output_match<'a>(
&self,
cmd: &str,
regex: &Regex,
buf: &'a mut String,
) -> Result<regex::Captures<'a>> {
*buf = self.cmd_one_line_output(cmd)?;
let Some(captures) = regex.captures(buf) else {
log::warn!("Unexpected HyperDebug output: {}", buf);
bail!(TransportError::CommunicationError(
"Unexpected output".to_string()
));
};
Ok(captures)
}
/// Send a command to HyperDebug firmware, with a callback to receive any output.
fn execute_command(&self, cmd: &str, mut callback: impl FnMut(&str)) -> Result<()> {
let port_name = self
.console_tty
.to_str()
.ok_or(TransportError::UnicodePathError)?;
let _lock = SerialPortExclusiveLock::lock(port_name)?;
let mut port = TTYPort::open(
&serialport::new(port_name, 115_200).timeout(std::time::Duration::from_millis(10)),
)
.expect("Failed to open port");
flock_serial(&port, port_name)?;
// 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: {:?}",
rc,
&std::str::from_utf8(&buf[0..rc])
);
}
Err(error) if error.kind() == ErrorKind::TimedOut => {
break;
}
Err(error) => return Err(error).context("communication error"),
}
}
// 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())
.context("communication error")?;
// 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])
.context("communication error")?;
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]).context("communication error")? == "> " {
// 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).context("communication error");
}
}
}
Err(error) => return Err(error).context("communication error"),
}
}
}
}
impl<T: Flavor> Transport for Hyperdebug<T> {
fn capabilities(&self) -> Result<Capabilities> {
Ok(Capabilities::new(
Capability::UART
| Capability::GPIO
| Capability::GPIO_MONITORING
| Capability::SPI
| Capability::I2C,
))
}
fn apply_default_configuration(&self) -> Result<()> {
self.inner.cmd_no_output("reinit")
}
// Create SPI Target instance, or return one from a cache of previously created instances.
fn spi(&self, instance: &str) -> Result<Rc<dyn Target>> {
// Execute a "spi info" command to look up the numeric index corresponding to the given
// alphanumeric SPI instance name.
let mut buf = String::new();
let mut buf2 = String::new();
let captures = self
.inner
.cmd_one_line_output_match(&format!("spi info {}", instance), &SPI_REGEX, &mut buf)
.or_else(|_| {
self.inner.cmd_one_line_output_match(
&format!("spiget {}", instance),
&SPI_REGEX,
&mut buf2,
)
})
.map_err(|_| {
TransportError::InvalidInstance(TransportInterfaceType::Spi, instance.to_string())
})?;
let idx = captures.get(1).unwrap().as_str().parse().unwrap();
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.inner,
&self.spi_interface,
idx,
)?);
self.inner
.spis
.borrow_mut()
.insert(idx, Rc::clone(&instance));
Ok(instance)
}
// Create I2C Target instance, or return one from a cache of previously created instances.
fn i2c(&self, instance: &str) -> Result<Rc<dyn Bus>> {
let &idx = self.i2c_names.get(instance).ok_or_else(|| {
TransportError::InvalidInstance(TransportInterfaceType::I2c, instance.to_string())
})?;
if let Some(instance) = self.inner.i2cs.borrow().get(&idx) {
return Ok(Rc::clone(instance));
}
let instance: Rc<dyn Bus> = Rc::new(i2c::HyperdebugI2cBus::open(
&self.inner,
&self.i2c_interface,
idx,
)?);
self.inner
.i2cs
.borrow_mut()
.insert(idx, Rc::clone(&instance));
Ok(instance)
}
// Create 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(SerialPortUart::open(
tty.to_str().ok_or(TransportError::UnicodePathError)?,
)?);
self.inner
.uarts
.borrow_mut()
.insert(tty.clone(), Rc::clone(&instance));
Ok(instance)
}
_ => Err(TransportError::InvalidInstance(
TransportInterfaceType::Uart,
instance.to_string(),
)
.into()),
}
}
// Create 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(T::gpio_pin(&self.inner, pinname)?);
Rc::clone(u)
}
Entry::Occupied(o) => Rc::clone(o.get()),
},
)
}
// Create GpioMonitoring instance.
fn gpio_monitoring(&self) -> Result<Rc<dyn GpioMonitoring>> {
// GpioMonitoring does not carry any state, so returning a new instance every time is
// harmless (save for some memory usage).
Ok(Rc::new(gpio::HyperdebugGpioMonitoring::open(&self.inner)?))
}
fn dispatch(&self, action: &dyn Any) -> Result<Option<Box<dyn Annotate>>> {
if let Some(update_firmware_action) = action.downcast_ref::<UpdateFirmware>() {
dfu::update_firmware(
&mut self.inner.usb_device.borrow_mut(),
&update_firmware_action.firmware,
&update_firmware_action.progress,
)
} else if let Some(fpga_program) = action.downcast_ref::<FpgaProgram>() {
T::load_bitstream(self, fpga_program).map(|_| None)
} else if let Some(clear) = action.downcast_ref::<ClearBitstream>() {
T::clear_bitstream(clear).map(|_| None)
} else {
Err(TransportError::UnsupportedOperation.into())
}
}
}
/// A `StandardFlavor` is a plain Hyperdebug board.
pub struct StandardFlavor;
impl Flavor for StandardFlavor {
fn gpio_pin(inner: &Rc<Inner>, pinname: &str) -> Result<Rc<dyn GpioPin>> {
Ok(Rc::new(gpio::HyperdebugGpioPin::open(inner, pinname)?))
}
fn get_default_usb_vid() -> u16 {
VID_GOOGLE
}
fn get_default_usb_pid() -> u16 {
PID_HYPERDEBUG
}
}
/// A `CW310Flavor` is a Hyperdebug attached to a CW310 board. Furthermore,
/// both the Hyperdebug and CW310 USB interfaces are attached to the host.
/// Hyperdebug is used for all IO with the CW310 board except for bitstream
/// programming.
pub struct CW310Flavor;
impl Flavor for CW310Flavor {
fn gpio_pin(inner: &Rc<Inner>, pinname: &str) -> Result<Rc<dyn GpioPin>> {
StandardFlavor::gpio_pin(inner, pinname)
}
fn get_default_usb_vid() -> u16 {
StandardFlavor::get_default_usb_vid()
}
fn get_default_usb_pid() -> u16 {
StandardFlavor::get_default_usb_pid()
}
fn load_bitstream(transport: &impl Transport, fpga_program: &FpgaProgram) -> Result<()> {
if fpga_program.skip() {
log::info!("Skip loading the __skip__ bitstream.");
return Ok(());
}
// First, try to establish a connection to the native CW310 interface
// which we will use for bitstream loading.
let cw310 = CW310::new(None, None, None, &[])?;
// The transport does not provide name resolution for the IO interface
// names, so: console=UART2 and RESET=CN10_29 on the Hyp+CW310.
// Open the console UART. We do this first so we get the receiver
// started and the uart buffering data for us.
let uart = transport.uart("UART2")?;
let reset_pin = transport.gpio_pin("CN10_29")?;
if fpga_program.check_correct_version(&*uart, &*reset_pin)? {
return Ok(());
}
// Program the FPGA bitstream.
log::info!("Programming the FPGA bitstream.");
let usb = cw310.device.borrow();
usb.spi1_enable(false)?;
usb.fpga_program(
&fpga_program.bitstream,
fpga_program.progress.as_ref().map(Box::as_ref),
)?;
Ok(())
}
fn clear_bitstream(_clear: &ClearBitstream) -> Result<()> {
let cw310 = CW310::new(None, None, None, &[])?;
let usb = cw310.device.borrow();
usb.spi1_enable(false)?;
usb.clear_bitstream()?;
Ok(())
}
}
lazy_static! {
pub static ref SPI_REGEX: Regex = Regex::new("^ +([0-9]+) ([^ ]+) ([0-9]+)").unwrap();
}