blob: 1a279bc971acb05c2fe500e8c71c61368a3c9ea5 [file] [log] [blame]
// SPDX-FileCopyrightText: CHERIoT contributors
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <cdefs.h>
#include <optional>
#include <stdint.h>
#include <utils.hh>
/**
* A driver for OpenTitan USB Device, which is used in the Sonata system.
*
* This peripheral's source and documentation can be found at:
* https://github.com/lowRISC/opentitan/tree/ab878b5d3578939a04db72d4ed966a56a869b2ed/hw/ip/usbdev
*
* With rendered register documentation served at:
* https://opentitan.org/book/hw/ip/usbdev/doc/registers.html
*
* An incredibly brief overview of how the USB device and it's buffers:
* Data packet ingress and egress goes via a pool of 64 byte buffers living
* in a 2kB SRAM packet buffer, which is accessable as a large MMIO region.
* The software manages these buffers using buffer IDs as references.
* IDs are pushed or popped into different FIFOs by either the software or
* device depending on whether they contain a packet to be sent, are available
* for a packet to be received into them, or contain a packet that has been
* received.
*
* See https://opentitan.org/book/hw/ip/usbdev/doc/programmers_guide.html for
* more information.
*/
class OpenTitanUsbdev : private utils::NoCopyNoMove
{
public:
/// Supported sizes for the USB Device.
static constexpr uint8_t MaxPacketLength = 64u;
static constexpr uint8_t BufferCount = 32u;
static constexpr uint8_t MaxEndpoints = 12u;
/**
* The offset from the start of the USB Device MMIO region at which
* packet buffer memory begins.
*/
static constexpr uint32_t BufferStartAddress = 0x800u;
/// Device Registers
uint32_t interruptState;
uint32_t interruptEnable;
uint32_t interruptTest;
uint32_t alertTest;
uint32_t usbControl;
uint32_t endpointOutEnable;
uint32_t endpointInEnable;
uint32_t usbStatus;
uint32_t availableOutBuffer;
uint32_t availableSetupBuffer;
uint32_t receiveBuffer;
/// Register to enable receive SETUP transactions
uint32_t receiveEnableSetup;
/// Register to enable receive OUT transactions
uint32_t receiveEnableOut;
/// Register to set NAK (Not/Negated Acknowledge) after OUT transactions
uint32_t setNotAcknowledgeOut;
/// Register showing ACK receival to indicate a successful IN send
uint32_t inSent;
/// Registers for controlling the stalling of OUT and IN endpoints
uint32_t outStall;
uint32_t inStall;
/**
* IN transaction configuration registers. There is one register per
* endpoint for the USB device.
*/
uint32_t configIn[MaxEndpoints];
/**
* Registers for configuring which endpoints should be treated as
* isochronous endpoints. This means that if the corresponding bit is set,
* then that no handshake packet will be sent for an OUT/IN transaction on
* that endpoint.
*/
uint32_t outIsochronous;
uint32_t inIsochronous;
/// Registers for configuring if endpoints data toggle on transactions
uint32_t outDataToggle;
uint32_t inDataToggle;
private:
/**
* Registers to sense/drive the USB PHY pins. That is, these registers can
* be used to respectively read out the state of the USB device inputs and
* outputs, or to control the inputs and outputs from software. These
* registers are kept private as they are intended to be used for debugging
* purposes or during chip testing, and not in actual software.
*/
[[maybe_unused]] uint32_t phyPinsSense;
[[maybe_unused]] uint32_t phyPinsDrive;
public:
/// Config register for the USB PHY pins.
uint32_t phyConfig;
/// Interrupt definitions for OpenTitan's USB Device.
enum class UsbdevInterrupt : uint32_t
{
/// Interrupt asserted whilst the receive FIFO (buffer) is not empty.
PacketReceived = 1u << 0,
/**
* Interrupt asserted when a packet was sent as part of an IN
* transaction, but not cleared from the `inSent` register.
*/
PacketSent = 1u << 1,
/**
* Interrupt raised when VBUS (power supply) is lost, i.e. the link to
* the USB host controller has been disconnected.
*/
Disconnected = 1u << 2,
/**
* Interrupt raised when the link is active, but a Start of Frame (SOF)
* packet has not been received within a given timeout threshold, which
* is set to 4.096 milliseconds.
*/
HostLost = 1u << 3,
/**
* Interrupt raised when a Bus Reset condition is indicated on the link
* by the link being held in an SE0 state (Single Ended Zero, both lines
* being pulled low) for longer than 3 microseconds.
*/
LinkReset = 1u << 4,
/**
* Interrupt raised when the link has entered the suspend state, due to
* being idle for more than 3 milliseconds.
*/
LinkSuspend = 1u << 5,
/// Interrupt raised on link transition from suspended to non-idle.
LinkResume = 1u << 6,
/// Interrupt asserted whilst the Available OUT buffer is empty.
AvailableOutEmpty = 1u << 7,
/// Interrupt asserted whilst the Receive buffer is full.
ReceiveFull = 1u << 8,
/**
* Interrupt raised when the Available OUT buffer or the Available SETUP
* buffer overflows.
*/
AvailableBufferOverflow = 1u << 9,
/// Interrupt raised when an error occurs during an IN transaction.
LinkInError = 1u << 10,
/**
* Interrupt raised when a CRC (cyclic redundancy check) error occurs on
* a received packet; i.e. there was an error in transmission.
*/
RedundancyCheckError = 1u << 11,
/// Interrupt raised when an invalid Packet Identifier is received.
PacketIdentifierError = 1u << 12,
/// Interrupt raised when a bit stuffing violation is detected.
BitstuffingError = 1u << 13,
/**
* Interrupt raised when the USB frame number is updated with a valid
* SOF (Start of Frame) packet.
*/
FrameUpdated = 1u << 14,
/// Interrupt raised when VBUS (power supply) is detected.
Powered = 1u << 15,
/// Interrupt raised when an error occurs during an OUT transaction.
LinkOutError = 1u << 16,
/// Interrupt asserted whilst the Available SETUP buffer is empty.
AvailableSetupEmpty = 1u << 17,
};
/**
* Definitions of fields (and their locations) for the USB Control register
* (offset 0x10).
*
* https://opentitan.org/book/hw/ip/usbdev/doc/registers.html#usbctrl
*/
enum class UsbControlField : uint32_t
{
Enable = 1u << 0,
ResumeLinkActive = 1u << 1,
DeviceAddress = 0x7Fu << 16,
};
/**
* Definitions of fields (and their locations) for the USB Status register
* (offset 0x1c).
*
* https://opentitan.org/book/hw/ip/usbdev/doc/registers.html#usbstat
*/
enum class UsbStatusField : uint32_t
{
Frame = 0x7FFu << 0,
HostLost = 1u << 11,
LinkState = 0x7u << 12,
Sense = 1u << 15,
AvailableOutDepth = 0xFu << 16,
AvailableSetupDepth = 0x7u << 20,
AvailableOutFull = 1u << 23,
ReceiveDepth = 0xFu << 24,
AvailableSetupFull = 1u << 30,
ReceiveEmpty = 1u << 31,
};
/**
* Definitions of fields (and their locations) for the Receive FIFO
* buffer register (offset 0x28).
*
* https://opentitan.org/book/hw/ip/usbdev/doc/registers.html#rxfifo
*/
enum class ReceiveBufferField : uint32_t
{
BufferId = 0x1Fu << 0,
Size = 0x7Fu << 8,
Setup = 1u << 19,
EndpointId = 0xFu << 20,
};
/**
* Definitions of fields (and their locations) for a Config In register
* (where there is one such register for each endpoint). These are
* the registers with offsets 0x44 up to (and not including) 0x74.
*
* https://opentitan.org/book/hw/ip/usbdev/doc/registers.html#configin
*/
enum class ConfigInField : uint32_t
{
BufferId = 0x1Fu << 0,
Size = 0x7Fu << 8,
Sending = 1u << 29,
Pending = 1u << 30,
Ready = 1u << 31,
};
/**
* Definitions of fields (and their locations) for the PHY Config
* Register (offset 0x8c).
*
* https://opentitan.org/book/hw/ip/usbdev/doc/registers.html#phy_config
*/
enum class PhyConfigField : uint32_t
{
UseDifferentialReceiver = 1u << 0,
// Other PHY Configuration fields are omitted.
};
/**
* Ensure that the Available OUT and Available SETUP buffers are kept
* supplied with buffers for packet reception.
*
* @param bufferBitmap A bitmap of the buffers that are not currently
* committed (where 1 corresponds to not in use).
* @returns The updated bitmap after supplying buffers.
*/
[[nodiscard]] uint64_t supply_buffers(uint64_t bufferBitmap) volatile
{
constexpr uint32_t SetupFullBit =
uint32_t(UsbStatusField::AvailableSetupFull);
constexpr uint32_t OutFullBit =
uint32_t(UsbStatusField::AvailableOutFull);
for (uint8_t index = 0; index < BufferCount; index++)
{
const uint32_t Buffer = (1u << index);
if (!(bufferBitmap & Buffer))
{
continue; // Skip buffers that are not available
}
// If a buffer is available, and either Available SETUP or OUT are
// not yet full, then commit that buffer and mark it as in use.
if (usbStatus & SetupFullBit)
{
if (usbStatus & OutFullBit)
{
break; // Both are full - stop trying to supply buffers.
}
availableOutBuffer = index;
}
else
{
availableSetupBuffer = index;
}
bufferBitmap &= ~Buffer;
}
return bufferBitmap;
}
/**
* Enable a specified interrupt / interrupts.
*/
void interrupt_enable(UsbdevInterrupt interrupt) volatile
{
interruptEnable = interruptEnable | uint32_t(interrupt);
}
/**
* Disable a specified interrupt / interrupts.
*/
void interrupt_disable(UsbdevInterrupt interrupt) volatile
{
interruptEnable = interruptEnable & ~uint32_t(interrupt);
}
/**
* Initialise the USB device, ensuring that packet buffers are available for
* reception and that the PHY has been appropriately configured. Note that
* at this stage, endpoints have not been configured and the device has not
* been connected to the USB.
*
* @param bufferBitmap An out-parameter, to initialise a bitmap of the
* buffers that are not currently commited (1 corresponds to not in use).
*
* @returns 0 if initialisation is sucessful, and non-zero otherwise.
*/
[[nodiscard]] int init(uint64_t &bufferBitmap) volatile
{
bufferBitmap = supply_buffers((uint64_t(1u) << BufferCount) - 1u);
phyConfig = uint32_t(PhyConfigField::UseDifferentialReceiver);
return 0;
}
/**
* Set up the configuration of an OUT endpoint for the USB device.
*
* @param endpointId The ID of the OUT endpoint to configure.
* @param enabled Whether the OUT endpoint should be enabled or not.
* @param setup Whether SETUP transactions should be enabled for the
* endpoint.
* @param isochronous Whether the endpoint should operate isochronously or
* non-isochronously.
*
* @returns 0 if configuration is successful, and non-zero otherwise.
*/
[[nodiscard]] int out_endpoint_configure(uint8_t endpointId,
bool enabled,
bool setup,
bool isochronous) volatile
{
if (endpointId >= MaxEndpoints)
{
return -1;
}
const uint32_t Mask = 1u << endpointId;
endpointOutEnable = (endpointOutEnable & ~Mask) | (enabled ? Mask : 0u);
outIsochronous = (outIsochronous & ~Mask) | (isochronous ? Mask : 0u);
receiveEnableSetup = (receiveEnableSetup & ~Mask) | (setup ? Mask : 0u);
receiveEnableOut = (receiveEnableOut & ~Mask) | (enabled ? Mask : 0u);
return 0;
}
/**
* Set up the configuration of an IN endpoint for the USB device.
*
* @param endpointId The ID of the IN endpoint to configure
* @param enabled Whether the IN endpoint should be enabled or not.
* @param isochronous Whether the endpoint should operate isochronously or
* non-isochronously.
*
* @returns 0 if configuration is successful, and non-zero otherwise.
*/
[[nodiscard]] int in_endpoint_configure(uint8_t endpointId,
bool enabled,
bool isochronous) volatile
{
if (endpointId >= MaxEndpoints)
{
return -1;
}
const uint32_t Mask = 1u << endpointId;
endpointInEnable = (endpointInEnable & ~Mask) | (enabled ? Mask : 0u);
inIsochronous = (inIsochronous & ~Mask) | (isochronous ? Mask : 0u);
return 0;
}
/**
* Set the STALL state of a specified endpoint pair (both IN and OUT).
*
* @param endpointId The ID of the endpoint pair to modify.
* @param stalling Whether the endpoints are stalling or not.
*
* @returns 0 if successful, and non-zero otherwise.
*/
[[nodiscard]] int endpoint_stalling_set(uint8_t endpointId,
bool stalling) volatile
{
if (endpointId >= MaxEndpoints)
{
return -1;
}
const uint32_t Mask = 1u << endpointId;
outStall = (outStall & ~Mask) | (stalling ? Mask : 0u);
inStall = (inStall & ~Mask) | (stalling ? Mask : 0u);
return 0;
}
/**
* Connect the device to the USB, indicating its presence to the USB host
* controller. Endpoints must already have been configured at this point
* because traffic may be received imminently.
*
* @returns 0 if successful, and non-zero otherwise.
* @returns -1 if endpoint 0 isn't enabled,
* suggesting the endpoints haven't been configured.
*/
[[nodiscard]] int connect() volatile
{
if (!(endpointInEnable & endpointOutEnable & 0b1))
{
return -1;
}
usbControl = usbControl | uint32_t(UsbControlField::Enable);
return 0;
}
/**
* Disconnect the device from the USB.
*
* @returns 0 if successful, and non-zero otherwise.
*/
void disconnect() volatile
{
usbControl = usbControl & ~uint32_t(UsbControlField::Enable);
}
/**
* Check whether the USB device is connected (i.e. pullup enabled).
*
* @returns True to indicate it is connected, and false otherwise.
*/
[[nodiscard]] bool connected() volatile
{
return (usbControl & uint32_t(UsbControlField::Enable));
}
/**
* Set the device address on the USB; this address will have been supplied
* by the USB host controller in the standard `SET_ADDRESS` Control
* Transfer.
*
* @param address The device address to set on the USB.
*
* @returns 0 if successful, and non-zero otherwise.
*/
[[nodiscard]] int device_address_set(uint8_t address) volatile
{
if (address >= 0x80)
{
return -1; // Device addresses are only 7 bits long.
}
constexpr uint32_t Mask = uint32_t(UsbControlField::DeviceAddress);
usbControl = (usbControl & ~Mask) | (address << 16);
return 0;
}
/**
* Check and retrieve the endpoint and buffer numbers of a
* recently-collected IN data packet. The caller is responsible for reusing
* or releasing the buffer.
*
* @param endpointId An out-parameter, to which the ID of the endpoint for
* a recently-collected IN data packet will be written.
* @param bufferId An out-parameter, to which the ID of the buffer for a
* recently-collected IN data packet will be written.
*
* @returns 0 if successful, and non-zero otherwise.
*/
[[nodiscard]] int retrieve_collected_packet(uint8_t &endpointId,
uint8_t &bufferId) volatile
{
constexpr uint32_t BufferIdMask = uint32_t(ConfigInField::BufferId);
uint32_t sent = inSent;
// Clear the first encountered packet sent indication.
for (endpointId = 0; endpointId < MaxEndpoints; endpointId++)
{
const uint32_t EndpointBit = 1u << endpointId;
if (sent & EndpointBit)
{
// Clear the `in_sent` bit for this specific endpoint, and
// indicate which buffer has been released.
inSent = EndpointBit;
bufferId = (configIn[endpointId] & BufferIdMask);
return 0;
}
}
// If no packet sent indications were found, then fail.
return -1;
}
/**
* Present a packet on the specified IN endpoint for collection by the USB
* host controller.
*
* @param bufferId The buffer to use to store the packet.
* @param endpointId The IN endpoint used to send the packet.
* @param data The packet to be transmitted.
* @param size The size of the packet.
*/
void packet_send(uint8_t bufferId,
uint8_t endpointId,
const uint32_t *data,
uint8_t size) volatile
{
// Transmission of zero length packets is common over USB
if (size > 0)
{
usbdev_transfer(buffer(bufferId), data, size, true);
}
constexpr uint32_t ReadyBit = uint32_t(ConfigInField::Ready);
configIn[endpointId] = bufferId | (size << 8);
configIn[endpointId] = configIn[endpointId] | ReadyBit;
}
/// The information associated with a received packet
struct ReceiveBufferInfo
{
uint32_t info;
/// The endpoint ID the received packet was received on
constexpr uint8_t endpoint_id()
{
return (info & uint32_t(ReceiveBufferField::EndpointId)) >> 20;
}
/// The size of the received packet
constexpr uint16_t size()
{
return (info & uint32_t(ReceiveBufferField::Size)) >> 8;
}
/// Whether the received packet was a setup packet
constexpr bool is_setup()
{
return (info & uint32_t(ReceiveBufferField::Setup)) != 0;
}
/// The buffer ID used to store the received packet
constexpr uint8_t buffer_id()
{
return (info & uint32_t(ReceiveBufferField::BufferId)) >> 0;
}
};
/**
* If a packet has been received, removes the packet's buffer from the
* receive FIFO giving it's information and ownership to the user.
*
* `packet_data_get` can be used to retrieve the packet's data.
*
* @returns Information about the received packet, if a packet had been
* received.
*/
[[nodiscard]] std::optional<ReceiveBufferInfo> packet_take() volatile
{
if (!(usbStatus & uint32_t(UsbStatusField::ReceiveDepth)))
{
return {}; // No packets received
}
return ReceiveBufferInfo{receiveBuffer};
}
/**
* Retrieves the data from a buffer containing a received packet.
*
* @param destination A destination buffer to read the packet's data into.
*/
void packet_data_get(ReceiveBufferInfo bufferInfo,
uint32_t *destination) volatile
{
const auto [id, size] =
std::pair{bufferInfo.buffer_id(), bufferInfo.size()};
// Reception of Zero Length Packets occurs in the Status Stage of IN
// Control Transfers.
if (size > 0)
{
usbdev_transfer(destination, buffer(id), size, false);
}
}
private:
/**
* Return a pointer to the given offset within the USB device register
* space; this is used to access the packet buffer memory.
*
* @param bufferId The buffer number to access the packet buffer memory for
*
* @returns A pointer to the buffer's memory.
*/
uint32_t *buffer(uint8_t bufferId) volatile
{
const uint32_t Offset = BufferStartAddress + bufferId * MaxPacketLength;
const uintptr_t Address = reinterpret_cast<uintptr_t>(this) + Offset;
return const_cast<uint32_t *>(reinterpret_cast<uint32_t *>(Address));
}
/**
* Perform a transfer to or from packet buffer memory. This function is
* hand-optimised to perform a faster, unrolled, word-based data transfer
* for efficiency.
*
* @param destination A pointer to transfer the source data to.
* @param source A pointer to the data to be transferred.
* @param size The size of the data pointed to by `source`.
* @param toDevice True if the transfer is to the device (e.g. when sending
* a packet), and False if not (e.g. when receiving a packet).
*/
static void usbdev_transfer(uint32_t *destination,
const uint32_t *source,
uint8_t size,
bool toDevice)
{
// Unroll word transfer. Each word transfer is 4 bytes, so we must round
// to the closest multiple of (4 * words) when unrolling.
constexpr uint8_t UnrollFactor = 4u;
constexpr uint32_t UnrollMask = (UnrollFactor * 4u) - 1;
// Round down to the previous multiple for unrolling
const uint32_t UnrollSize = (size & ~UnrollMask);
const uint32_t *sourceEnd = reinterpret_cast<uint32_t *>(
reinterpret_cast<uintptr_t>(source) + UnrollSize);
// This is manulally unrolled for two reasons:
// 1. We can't do partial writes to the USB packet buffer,
// which memcpy will attempt and causes a BUS fault.
// 2. In the sonata system at the time of writing,
// the core clock is 40MHz compared to the USB device's 48MHz.
// This approach was found to be significantly faster than when
// left to compiler to optimisation.
//
// Ensure the unrolling here matches `UnrollFactor`.
while (source < sourceEnd)
{
destination[0] = source[0];
destination[1] = source[1];
destination[2] = source[2];
destination[3] = source[3];
destination += UnrollFactor;
source += UnrollFactor;
}
// Copy the remaining whole words.
for (size &= UnrollMask; size >= UnrollFactor; size -= UnrollFactor)
{
*destination++ = *source++;
}
if (size == 0)
{
return;
}
// Copy trailing tail bytes, as USBDEV only supports 32-bit accesses.
if (toDevice)
{
// Collect final bytes into a word.
const volatile uint8_t *trailingBytes =
reinterpret_cast<const volatile uint8_t *>(source);
uint32_t partialWord = trailingBytes[0];
if (size > 1)
{
partialWord |= trailingBytes[1] << 8;
}
if (size > 2)
{
partialWord |= trailingBytes[2] << 16;
}
// Write the final word to the device.
*destination = partialWord;
}
else
{
volatile uint8_t *destinationBytes =
reinterpret_cast<volatile uint8_t *>(destination);
// Collect the final word from the device.
const uint32_t TrailingBytes = *source;
// Unpack it into final bytes.
destinationBytes[0] = static_cast<uint8_t>(TrailingBytes);
if (size > 1)
{
destinationBytes[1] = static_cast<uint8_t>(TrailingBytes >> 8);
}
if (size > 2)
{
destinationBytes[2] = static_cast<uint8_t>(TrailingBytes >> 16);
}
}
}
};