cotton-usb-host 0.3.0

USB host stack for embedded devices
Documentation
use crate::wire::SetupPacket;
use core::cell::Cell;
use core::ops::Deref;
use futures::Stream;

/// Errors reported from a USB operation
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[cfg_attr(feature = "std", derive(Debug))]
#[derive(Copy, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum UsbError {
    /// The device has stalled the endpoint
    ///
    /// See USB 2.0 section 8.5.2 and 8.5.3.4, and, for a prolific user
    /// of stall conditions, the USB Mass Storage Bulk-Only Transport
    /// section 6.
    Stall,
    /// The USB transaction has timed out
    ///
    /// A NAK response is automatically retried, but if NAKs persist, eventually
    /// the transfer will time out.
    Timeout,
    /// The input FIFO overflowed
    ///
    /// This error, produced by the USB host-controller hardware, probably
    /// represents a bug in cotton-usb-host (or perhaps a failure to service
    /// interrupts quickly enough).
    Overflow,
    /// The USB hardware experienced a bit-stuffing error
    BitStuffError,
    /// The USB hardware experienced a CRC error
    CrcError,
    /// The USB hardware received a DATA1 packet when expecting DATA0, or vice versa
    ///
    /// This probably indicates a bug in cotton-usb-host (which is
    /// intended to take care of the data toggle without client code
    /// needing to intervene).
    DataSeqError,
    /// A buffer supplied to cotton-usb-host was too small for the intended data
    BufferTooSmall,
    /// A USB transaction was attempted when all hardware resources were in use
    ///
    /// This error is returned from
    /// [`HostController::try_alloc_interrupt_pipe()`] when all
    /// interrupt-capable pipes are already in use. Users of
    /// [`UsbBus::interrupt_endpoint_in()`](crate::usb_bus::UsbBus::interrupt_endpoint_in)
    /// do not experience this error, as that call will wait for the
    /// next pipe to be available.
    AllPipesInUse,
    /// The device has reacted in a way contrary to the expected protocol
    ProtocolError,
    /// The limit of attached USB devices has been reached
    TooManyDevices,
    /// [`UsbDevice::open_in_endpoint()`](crate::usb_bus::UsbDevice::open_in_endpoint) was called with a bogus endpoint number
    NoSuchEndpoint,
}

/// Connection speed for a USB device
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[cfg_attr(feature = "std", derive(Debug))]
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum UsbSpeed {
    /// USB 1.1 Low Speed (1.5Mbits/s)
    Low1_5,
    /// USB 1.1 Full Speed (12Mbits/s)
    Full12,
    /// USB 2.0 High Speed (480Mbits/s)
    High480,
}

/// Events generated by hotplug/hot-unplug detection
///
/// See [`HostController::device_detect`]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[cfg_attr(feature = "std", derive(Debug))]
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum DeviceStatus {
    /// A device is connected (and negotiated a certain speed)
    Present(UsbSpeed),
    /// No device is connected
    Absent,
}

/// Special treatment for a particular transfer
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[cfg_attr(feature = "std", derive(Debug))]
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum TransferExtras {
    /// Normal transfer
    Normal,
    /// Low-speed transfer to a high-speed hub (USB 2.0 s8.6.5)
    WithPreamble,
}

/// The data phase of a USB control-endpoint transaction
///
/// A transaction on a control endpoint involves one of:
///  - IN (data transferred from device to host)
///  - OUT (data transferred from host to device)
///  - neither (the Setup packet contains all the relevant data)
///
/// See USB 2.0 section 8.5.3 and fiture 8-37.
#[cfg_attr(feature = "std", derive(Debug))]
#[derive(PartialEq, Eq)]
pub enum DataPhase<'a> {
    /// IN transaction (device-to-host)
    In(&'a mut [u8]),
    /// OUT transaction (host-to-device)
    Out(&'a [u8]),
    /// No data phase in control transaction
    None,
}

impl DataPhase<'_> {
    /// Is this DataPhase an IN variant?
    pub fn is_in(&self) -> bool {
        matches!(self, DataPhase::In(_))
    }

    /// Is this DataPhase an OUT variant?
    pub fn is_out(&self) -> bool {
        matches!(self, DataPhase::Out(_))
    }

    /// Is this DataPhase a no-data variant?
    pub fn is_none(&self) -> bool {
        matches!(self, DataPhase::None)
    }

    /// If this DataPhase is an IN variant, call the supplied function
    /// on the received data
    pub fn in_with<F: FnOnce(&mut [u8])>(&mut self, f: F) {
        if let Self::In(x) = self {
            f(x)
        }
    }
}

/// Is this a fixed-size transfer or variable-size transfer?
///
/// According to USB 2.0 s5.3.2, the host must behave differently in
/// each case, so needs to know. (In particular, a fixed-size transfer
/// doesn't have a zero-length packet even if the data fills an exact
/// number of packets -- whereas a variable-size transfer does have a
/// zero-length packet in that case.)
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[cfg_attr(feature = "std", derive(Debug))]
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum TransferType {
    /// Both ends know (via other means) how long the transfer should be
    FixedSize,
    /// Open-ended transfer (the size given is a maximum)
    VariableSize,
}

/// A packet as received on an interrupt IN endpoint
pub struct InterruptPacket {
    /// USB address (1-127) of device from which packet was received
    pub address: u8,
    /// Endpoint number on which packet was received
    pub endpoint: u8,
    /// Packet size (i.e., length of valid prefix of [`InterruptPacket::data`])
    pub size: u8,
    /// Packet contents
    pub data: [u8; 64],
}

impl Default for InterruptPacket {
    fn default() -> Self {
        Self::new()
    }
}

impl InterruptPacket {
    /// Construct a new InterruptPacket
    ///
    /// The address, endpoint, size and data should be filled-in before doing
    /// anything with it.
    pub const fn new() -> Self {
        Self {
            address: 0,
            endpoint: 0,
            size: 0,
            data: [0u8; 64],
        }
    }
}

impl Deref for InterruptPacket {
    type Target = [u8];

    fn deref(&self) -> &Self::Target {
        &self.data[0..(self.size as usize)]
    }
}

/// Encapsulating a particular USB hardware host controller
///
/// This trait can be implemented for different USB hardware (e.g.,
/// RP2040, Synopsys DWC, or XHCI) and allows the rest of the crate --
/// particularly [`UsbBus`](crate::usb_bus::UsbBus) -- to be hardware-agnostic.
pub trait HostController {
    /// The concrete type returned by [`HostController::alloc_interrupt_pipe`]
    type InterruptPipe: Stream<Item = InterruptPacket> + Unpin;
    /// The concrete type returned by [`HostController::device_detect`]
    type DeviceDetect: Stream<Item = DeviceStatus>;

    /// Return a Stream of device-detect events
    ///
    /// This stream covers hot-plug and hot-unplug to/from the USB host
    /// controller itself (i.e., not to/from downstream hubs).
    fn device_detect(&self) -> Self::DeviceDetect;

    /// Reset the root port of the USB controller (USB 2.0 section 11.5.1.5)
    ///
    /// TinyUSB does not appear to do this on RP2040, but it's surely
    /// important, especially if VBUS can remain powered across a
    /// reset of the RP2040 itself.
    fn reset_root_port(&self, rst: bool);

    /// Perform a USB control transfer
    ///
    /// A control-capable pipe is allocated for the duration of the
    /// transaction, and de-allocated again at the end.
    fn control_transfer(
        &self,
        address: u8,
        transfer_extras: TransferExtras,
        packet_size: u8,
        setup: SetupPacket,
        data_phase: DataPhase<'_>,
    ) -> impl core::future::Future<Output = Result<usize, UsbError>>;

    /// Perform a USB bulk in transfer
    ///
    /// A bulk-capable pipe is allocated for the duration of the
    /// transaction, and de-allocated again at the end.
    ///
    /// The passed-in data_toggle must be correct for the current state
    /// of the endpoint, and is updated for the endpoint state after the
    /// transaction.
    ///
    /// NB No "transfer_extras": LS devices can't have bulk endpoints.
    fn bulk_in_transfer(
        &self,
        address: u8,
        endpoint: u8,
        packet_size: u16,
        data: &mut [u8],
        transfer_type: TransferType,
        data_toggle: &Cell<bool>,
    ) -> impl core::future::Future<Output = Result<usize, UsbError>>;

    /// Perform a USB bulk out transfer
    ///
    /// A bulk-capable pipe is allocated for the duration of the
    /// transaction, and de-allocated again at the end.
    ///
    /// The passed-in data_toggle must be correct for the current state
    /// of the endpoint, and is updated for the endpoint state after the
    /// transaction.
    ///
    /// NB No "transfer_extras": LS devices can't have bulk endpoints.
    fn bulk_out_transfer(
        &self,
        address: u8,
        endpoint: u8,
        packet_size: u16,
        data: &[u8],
        transfer_type: TransferType,
        data_toggle: &Cell<bool>,
    ) -> impl core::future::Future<Output = Result<usize, UsbError>>;

    /// Allocate an interrupt pipe
    ///
    /// The pipe is owned by the returned object, and remains
    /// allocated as long as the returned object exists.
    ///
    /// If no interrupt-capable pipes are available when the function is
    /// called, it awaits for one to become available.
    ///
    /// The returned object implements a stream of [`InterruptPacket`] events.
    fn alloc_interrupt_pipe(
        &self,
        address: u8,
        transfer_extras: TransferExtras,
        endpoint: u8,
        max_packet_size: u16,
        interval_ms: u8,
    ) -> impl core::future::Future<Output = Self::InterruptPipe>;

    /// Allocate an interrupt pipe
    ///
    /// The pipe is owned by the returned object, and remains
    /// allocated as long as the returned object exists.
    ///
    /// If no interrupt-capable pipes are available when the function is
    /// called, it immediately returns `Err(UsbError::AllPipesInUse)`.
    ///
    /// The returned object implements a stream of [`InterruptPacket`] events.
    fn try_alloc_interrupt_pipe(
        &self,
        address: u8,
        transfer_extras: TransferExtras,
        endpoint: u8,
        max_packet_size: u16,
        interval_ms: u8,
    ) -> Result<Self::InterruptPipe, UsbError>;
}

#[cfg(all(test, feature = "std"))]
#[path = "tests/host_controller.rs"]
pub(crate) mod tests;