Import USB DIF/testutils from OpenTitan

- Bring in the unmodified OpenTitan usbdev code, as of 04/03/2024. A
  follow-up will patch these to work for Matcha.

Change-Id: I8a43b181cdb314b010fdf0f52fc299a81a333209
diff --git a/sw/device/lib/dif/autogen/dif_usbdev_autogen.c b/sw/device/lib/dif/autogen/dif_usbdev_autogen.c
new file mode 100644
index 0000000..47b8e0a
--- /dev/null
+++ b/sw/device/lib/dif/autogen/dif_usbdev_autogen.c
@@ -0,0 +1,309 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+// THIS FILE HAS BEEN GENERATED, DO NOT EDIT MANUALLY. COMMAND:
+// util/make_new_dif.py --mode=regen --only=autogen
+
+#include "sw/device/lib/dif/autogen/dif_usbdev_autogen.h"
+
+#include <stdint.h>
+
+#include "sw/device/lib/dif/dif_base.h"
+
+#include "usbdev_regs.h"  // Generated.
+
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_init(mmio_region_t base_addr, dif_usbdev_t *usbdev) {
+  if (usbdev == NULL) {
+    return kDifBadArg;
+  }
+
+  usbdev->base_addr = base_addr;
+
+  return kDifOk;
+}
+
+dif_result_t dif_usbdev_alert_force(const dif_usbdev_t *usbdev,
+                                    dif_usbdev_alert_t alert) {
+  if (usbdev == NULL) {
+    return kDifBadArg;
+  }
+
+  bitfield_bit32_index_t alert_idx;
+  switch (alert) {
+    case kDifUsbdevAlertFatalFault:
+      alert_idx = USBDEV_ALERT_TEST_FATAL_FAULT_BIT;
+      break;
+    default:
+      return kDifBadArg;
+  }
+
+  uint32_t alert_test_reg = bitfield_bit32_write(0, alert_idx, true);
+  mmio_region_write32(usbdev->base_addr, USBDEV_ALERT_TEST_REG_OFFSET,
+                      alert_test_reg);
+
+  return kDifOk;
+}
+
+/**
+ * Get the corresponding interrupt register bit offset of the IRQ.
+ */
+static bool usbdev_get_irq_bit_index(dif_usbdev_irq_t irq,
+                                     bitfield_bit32_index_t *index_out) {
+  switch (irq) {
+    case kDifUsbdevIrqPktReceived:
+      *index_out = USBDEV_INTR_COMMON_PKT_RECEIVED_BIT;
+      break;
+    case kDifUsbdevIrqPktSent:
+      *index_out = USBDEV_INTR_COMMON_PKT_SENT_BIT;
+      break;
+    case kDifUsbdevIrqDisconnected:
+      *index_out = USBDEV_INTR_COMMON_DISCONNECTED_BIT;
+      break;
+    case kDifUsbdevIrqHostLost:
+      *index_out = USBDEV_INTR_COMMON_HOST_LOST_BIT;
+      break;
+    case kDifUsbdevIrqLinkReset:
+      *index_out = USBDEV_INTR_COMMON_LINK_RESET_BIT;
+      break;
+    case kDifUsbdevIrqLinkSuspend:
+      *index_out = USBDEV_INTR_COMMON_LINK_SUSPEND_BIT;
+      break;
+    case kDifUsbdevIrqLinkResume:
+      *index_out = USBDEV_INTR_COMMON_LINK_RESUME_BIT;
+      break;
+    case kDifUsbdevIrqAvEmpty:
+      *index_out = USBDEV_INTR_COMMON_AV_EMPTY_BIT;
+      break;
+    case kDifUsbdevIrqRxFull:
+      *index_out = USBDEV_INTR_COMMON_RX_FULL_BIT;
+      break;
+    case kDifUsbdevIrqAvOverflow:
+      *index_out = USBDEV_INTR_COMMON_AV_OVERFLOW_BIT;
+      break;
+    case kDifUsbdevIrqLinkInErr:
+      *index_out = USBDEV_INTR_COMMON_LINK_IN_ERR_BIT;
+      break;
+    case kDifUsbdevIrqRxCrcErr:
+      *index_out = USBDEV_INTR_COMMON_RX_CRC_ERR_BIT;
+      break;
+    case kDifUsbdevIrqRxPidErr:
+      *index_out = USBDEV_INTR_COMMON_RX_PID_ERR_BIT;
+      break;
+    case kDifUsbdevIrqRxBitstuffErr:
+      *index_out = USBDEV_INTR_COMMON_RX_BITSTUFF_ERR_BIT;
+      break;
+    case kDifUsbdevIrqFrame:
+      *index_out = USBDEV_INTR_COMMON_FRAME_BIT;
+      break;
+    case kDifUsbdevIrqPowered:
+      *index_out = USBDEV_INTR_COMMON_POWERED_BIT;
+      break;
+    case kDifUsbdevIrqLinkOutErr:
+      *index_out = USBDEV_INTR_COMMON_LINK_OUT_ERR_BIT;
+      break;
+    default:
+      return false;
+  }
+
+  return true;
+}
+
+static dif_irq_type_t irq_types[] = {
+    kDifIrqTypeEvent, kDifIrqTypeEvent, kDifIrqTypeEvent, kDifIrqTypeEvent,
+    kDifIrqTypeEvent, kDifIrqTypeEvent, kDifIrqTypeEvent, kDifIrqTypeEvent,
+    kDifIrqTypeEvent, kDifIrqTypeEvent, kDifIrqTypeEvent, kDifIrqTypeEvent,
+    kDifIrqTypeEvent, kDifIrqTypeEvent, kDifIrqTypeEvent, kDifIrqTypeEvent,
+    kDifIrqTypeEvent,
+};
+
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_irq_get_type(const dif_usbdev_t *usbdev,
+                                     dif_usbdev_irq_t irq,
+                                     dif_irq_type_t *type) {
+  if (usbdev == NULL || type == NULL || irq == kDifUsbdevIrqLinkOutErr + 1) {
+    return kDifBadArg;
+  }
+
+  *type = irq_types[irq];
+
+  return kDifOk;
+}
+
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_irq_get_state(
+    const dif_usbdev_t *usbdev, dif_usbdev_irq_state_snapshot_t *snapshot) {
+  if (usbdev == NULL || snapshot == NULL) {
+    return kDifBadArg;
+  }
+
+  *snapshot =
+      mmio_region_read32(usbdev->base_addr, USBDEV_INTR_STATE_REG_OFFSET);
+
+  return kDifOk;
+}
+
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_irq_acknowledge_state(
+    const dif_usbdev_t *usbdev, dif_usbdev_irq_state_snapshot_t snapshot) {
+  if (usbdev == NULL) {
+    return kDifBadArg;
+  }
+
+  mmio_region_write32(usbdev->base_addr, USBDEV_INTR_STATE_REG_OFFSET,
+                      snapshot);
+
+  return kDifOk;
+}
+
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_irq_is_pending(const dif_usbdev_t *usbdev,
+                                       dif_usbdev_irq_t irq, bool *is_pending) {
+  if (usbdev == NULL || is_pending == NULL) {
+    return kDifBadArg;
+  }
+
+  bitfield_bit32_index_t index;
+  if (!usbdev_get_irq_bit_index(irq, &index)) {
+    return kDifBadArg;
+  }
+
+  uint32_t intr_state_reg =
+      mmio_region_read32(usbdev->base_addr, USBDEV_INTR_STATE_REG_OFFSET);
+
+  *is_pending = bitfield_bit32_read(intr_state_reg, index);
+
+  return kDifOk;
+}
+
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_irq_acknowledge_all(const dif_usbdev_t *usbdev) {
+  if (usbdev == NULL) {
+    return kDifBadArg;
+  }
+
+  // Writing to the register clears the corresponding bits (Write-one clear).
+  mmio_region_write32(usbdev->base_addr, USBDEV_INTR_STATE_REG_OFFSET,
+                      UINT32_MAX);
+
+  return kDifOk;
+}
+
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_irq_acknowledge(const dif_usbdev_t *usbdev,
+                                        dif_usbdev_irq_t irq) {
+  if (usbdev == NULL) {
+    return kDifBadArg;
+  }
+
+  bitfield_bit32_index_t index;
+  if (!usbdev_get_irq_bit_index(irq, &index)) {
+    return kDifBadArg;
+  }
+
+  // Writing to the register clears the corresponding bits (Write-one clear).
+  uint32_t intr_state_reg = bitfield_bit32_write(0, index, true);
+  mmio_region_write32(usbdev->base_addr, USBDEV_INTR_STATE_REG_OFFSET,
+                      intr_state_reg);
+
+  return kDifOk;
+}
+
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_irq_force(const dif_usbdev_t *usbdev,
+                                  dif_usbdev_irq_t irq, const bool val) {
+  if (usbdev == NULL) {
+    return kDifBadArg;
+  }
+
+  bitfield_bit32_index_t index;
+  if (!usbdev_get_irq_bit_index(irq, &index)) {
+    return kDifBadArg;
+  }
+
+  uint32_t intr_test_reg = bitfield_bit32_write(0, index, val);
+  mmio_region_write32(usbdev->base_addr, USBDEV_INTR_TEST_REG_OFFSET,
+                      intr_test_reg);
+
+  return kDifOk;
+}
+
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_irq_get_enabled(const dif_usbdev_t *usbdev,
+                                        dif_usbdev_irq_t irq,
+                                        dif_toggle_t *state) {
+  if (usbdev == NULL || state == NULL) {
+    return kDifBadArg;
+  }
+
+  bitfield_bit32_index_t index;
+  if (!usbdev_get_irq_bit_index(irq, &index)) {
+    return kDifBadArg;
+  }
+
+  uint32_t intr_enable_reg =
+      mmio_region_read32(usbdev->base_addr, USBDEV_INTR_ENABLE_REG_OFFSET);
+
+  bool is_enabled = bitfield_bit32_read(intr_enable_reg, index);
+  *state = is_enabled ? kDifToggleEnabled : kDifToggleDisabled;
+
+  return kDifOk;
+}
+
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_irq_set_enabled(const dif_usbdev_t *usbdev,
+                                        dif_usbdev_irq_t irq,
+                                        dif_toggle_t state) {
+  if (usbdev == NULL) {
+    return kDifBadArg;
+  }
+
+  bitfield_bit32_index_t index;
+  if (!usbdev_get_irq_bit_index(irq, &index)) {
+    return kDifBadArg;
+  }
+
+  uint32_t intr_enable_reg =
+      mmio_region_read32(usbdev->base_addr, USBDEV_INTR_ENABLE_REG_OFFSET);
+
+  bool enable_bit = (state == kDifToggleEnabled) ? true : false;
+  intr_enable_reg = bitfield_bit32_write(intr_enable_reg, index, enable_bit);
+  mmio_region_write32(usbdev->base_addr, USBDEV_INTR_ENABLE_REG_OFFSET,
+                      intr_enable_reg);
+
+  return kDifOk;
+}
+
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_irq_disable_all(
+    const dif_usbdev_t *usbdev, dif_usbdev_irq_enable_snapshot_t *snapshot) {
+  if (usbdev == NULL) {
+    return kDifBadArg;
+  }
+
+  // Pass the current interrupt state to the caller, if requested.
+  if (snapshot != NULL) {
+    *snapshot =
+        mmio_region_read32(usbdev->base_addr, USBDEV_INTR_ENABLE_REG_OFFSET);
+  }
+
+  // Disable all interrupts.
+  mmio_region_write32(usbdev->base_addr, USBDEV_INTR_ENABLE_REG_OFFSET, 0u);
+
+  return kDifOk;
+}
+
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_irq_restore_all(
+    const dif_usbdev_t *usbdev,
+    const dif_usbdev_irq_enable_snapshot_t *snapshot) {
+  if (usbdev == NULL || snapshot == NULL) {
+    return kDifBadArg;
+  }
+
+  mmio_region_write32(usbdev->base_addr, USBDEV_INTR_ENABLE_REG_OFFSET,
+                      *snapshot);
+
+  return kDifOk;
+}
diff --git a/sw/device/lib/dif/autogen/dif_usbdev_autogen.h b/sw/device/lib/dif/autogen/dif_usbdev_autogen.h
new file mode 100644
index 0000000..293587c
--- /dev/null
+++ b/sw/device/lib/dif/autogen/dif_usbdev_autogen.h
@@ -0,0 +1,323 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef OPENTITAN_SW_DEVICE_LIB_DIF_AUTOGEN_DIF_USBDEV_AUTOGEN_H_
+#define OPENTITAN_SW_DEVICE_LIB_DIF_AUTOGEN_DIF_USBDEV_AUTOGEN_H_
+
+// THIS FILE HAS BEEN GENERATED, DO NOT EDIT MANUALLY. COMMAND:
+// util/make_new_dif.py --mode=regen --only=autogen
+
+/**
+ * @file
+ * @brief <a href="/hw/ip/usbdev/doc/">USBDEV</a> Device Interface Functions
+ */
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "sw/device/lib/base/macros.h"
+#include "sw/device/lib/base/mmio.h"
+#include "sw/device/lib/dif/dif_base.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif  // __cplusplus
+
+/**
+ * A handle to usbdev.
+ *
+ * This type should be treated as opaque by users.
+ */
+typedef struct dif_usbdev {
+  /**
+   * The base address for the usbdev hardware registers.
+   */
+  mmio_region_t base_addr;
+} dif_usbdev_t;
+
+/**
+ * Creates a new handle for a(n) usbdev peripheral.
+ *
+ * This function does not actuate the hardware.
+ *
+ * @param base_addr The MMIO base address of the usbdev peripheral.
+ * @param[out] usbdev Out param for the initialized handle.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_init(mmio_region_t base_addr, dif_usbdev_t *usbdev);
+
+/**
+ * A usbdev alert type.
+ */
+typedef enum dif_usbdev_alert {
+  /**
+   * This fatal alert is triggered when a fatal TL-UL bus integrity fault is
+   * detected.
+   */
+  kDifUsbdevAlertFatalFault = 0,
+} dif_usbdev_alert_t;
+
+/**
+ * Forces a particular alert, causing it to be escalated as if the hardware
+ * had raised it.
+ *
+ * @param usbdev A usbdev handle.
+ * @param alert The alert to force.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_alert_force(const dif_usbdev_t *usbdev,
+                                    dif_usbdev_alert_t alert);
+
+/**
+ * A usbdev interrupt request type.
+ */
+typedef enum dif_usbdev_irq {
+  /**
+   * Raised if a packet was received using an OUT or SETUP transaction. This
+   * interrupt is directly tied to whether the RX FIFO is empty, so it should be
+   * cleared only after handling the FIFO entry.
+   */
+  kDifUsbdevIrqPktReceived = 0,
+  /**
+   * Raised if a packet was sent as part of an IN transaction. This interrupt is
+   * directly tied to whether a sent packet has not been acknowledged in the
+   * !!in_sent register. It should be cleared only after clearing all bits in
+   * the !!in_sent register.
+   */
+  kDifUsbdevIrqPktSent = 1,
+  /**
+   * Raised if VBUS is lost thus the link is disconnected.
+   */
+  kDifUsbdevIrqDisconnected = 2,
+  /**
+   * Raised if link is active but SOF was not received from host for 4.096 ms.
+   * The SOF should be every 1 ms.
+   */
+  kDifUsbdevIrqHostLost = 3,
+  /**
+   * Raised if the link is at SE0 longer than 3 us indicating a link reset (host
+   * asserts for min 10 ms, device can react after 2.5 us).
+   */
+  kDifUsbdevIrqLinkReset = 4,
+  /**
+   * Raised if the line has signaled J for longer than 3ms and is therefore in
+   * suspend state.
+   */
+  kDifUsbdevIrqLinkSuspend = 5,
+  /**
+   * Raised when the link becomes active again after being suspended.
+   */
+  kDifUsbdevIrqLinkResume = 6,
+  /**
+   * Raised when the AV FIFO is empty and the device interface is enabled. This
+   * interrupt is directly tied to the FIFO status, so the AV FIFO must be
+   * provided a free buffer before the interrupt is cleared. If the condition is
+   * not cleared, the interrupt can re-assert.
+   */
+  kDifUsbdevIrqAvEmpty = 7,
+  /**
+   * Raised when the RX FIFO is full and the device interface is enabled. This
+   * interrupt is directly tied to the FIFO status, so the RX FIFO must have an
+   * entry removed before the interrupt is cleared. If the condition is not
+   * cleared, the interrupt can re-assert.
+   */
+  kDifUsbdevIrqRxFull = 8,
+  /**
+   * Raised if a write was done to the Available Buffer FIFO when the FIFO was
+   * full.
+   */
+  kDifUsbdevIrqAvOverflow = 9,
+  /**
+   * Raised if a packet to an IN endpoint started to be received but was then
+   * dropped due to an error. After transmitting the IN payload, the USB device
+   * expects a valid ACK handshake packet. This error is raised if either the
+   * packet or CRC is invalid or a different token was received.
+   */
+  kDifUsbdevIrqLinkInErr = 10,
+  /**
+   * Raised if a CRC error occured.
+   */
+  kDifUsbdevIrqRxCrcErr = 11,
+  /**
+   * Raised if an invalid packed identifier (PID) was received.
+   */
+  kDifUsbdevIrqRxPidErr = 12,
+  /**
+   * Raised if an invalid bitstuffing was received.
+   */
+  kDifUsbdevIrqRxBitstuffErr = 13,
+  /**
+   * Raised when the USB frame number is updated with a valid SOF.
+   */
+  kDifUsbdevIrqFrame = 14,
+  /**
+   * Raised if VBUS is applied.
+   */
+  kDifUsbdevIrqPowered = 15,
+  /**
+   * Raised if a packet to an OUT endpoint started to be received but was then
+   * dropped due to an error. This error is raised if either the data toggle,
+   * token, packet or CRC is invalid or if there is no buffer available in the
+   * Received Buffer FIFO.
+   */
+  kDifUsbdevIrqLinkOutErr = 16,
+} dif_usbdev_irq_t;
+
+/**
+ * A snapshot of the state of the interrupts for this IP.
+ *
+ * This is an opaque type, to be used with the `dif_usbdev_irq_get_state()`
+ * and `dif_usbdev_irq_acknowledge_state()` functions.
+ */
+typedef uint32_t dif_usbdev_irq_state_snapshot_t;
+
+/**
+ * Returns the type of a given interrupt (i.e., event or status) for this IP.
+ *
+ * @param usbdev A usbdev handle.
+ * @param irq An interrupt request.
+ * @param[out] type Out-param for the interrupt type.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_irq_get_type(const dif_usbdev_t *usbdev,
+                                     dif_usbdev_irq_t irq,
+                                     dif_irq_type_t *type);
+
+/**
+ * Returns the state of all interrupts (i.e., pending or not) for this IP.
+ *
+ * @param usbdev A usbdev handle.
+ * @param[out] snapshot Out-param for interrupt state snapshot.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_irq_get_state(
+    const dif_usbdev_t *usbdev, dif_usbdev_irq_state_snapshot_t *snapshot);
+
+/**
+ * Returns whether a particular interrupt is currently pending.
+ *
+ * @param usbdev A usbdev handle.
+ * @param irq An interrupt request.
+ * @param[out] is_pending Out-param for whether the interrupt is pending.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_irq_is_pending(const dif_usbdev_t *usbdev,
+                                       dif_usbdev_irq_t irq, bool *is_pending);
+
+/**
+ * Acknowledges all interrupts that were pending at the time of the state
+ * snapshot.
+ *
+ * @param usbdev A usbdev handle.
+ * @param snapshot Interrupt state snapshot.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_irq_acknowledge_state(
+    const dif_usbdev_t *usbdev, dif_usbdev_irq_state_snapshot_t snapshot);
+
+/**
+ * Acknowledges all interrupts, indicating to the hardware that all
+ * interrupts have been successfully serviced.
+ *
+ * @param usbdev A usbdev handle.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_irq_acknowledge_all(const dif_usbdev_t *usbdev);
+
+/**
+ * Acknowledges a particular interrupt, indicating to the hardware that it has
+ * been successfully serviced.
+ *
+ * @param usbdev A usbdev handle.
+ * @param irq An interrupt request.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_irq_acknowledge(const dif_usbdev_t *usbdev,
+                                        dif_usbdev_irq_t irq);
+
+/**
+ * Forces a particular interrupt, causing it to be serviced as if hardware had
+ * asserted it.
+ *
+ * @param usbdev A usbdev handle.
+ * @param irq An interrupt request.
+ * @param val Value to be set.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_irq_force(const dif_usbdev_t *usbdev,
+                                  dif_usbdev_irq_t irq, const bool val);
+
+/**
+ * A snapshot of the enablement state of the interrupts for this IP.
+ *
+ * This is an opaque type, to be used with the
+ * `dif_usbdev_irq_disable_all()` and `dif_usbdev_irq_restore_all()`
+ * functions.
+ */
+typedef uint32_t dif_usbdev_irq_enable_snapshot_t;
+
+/**
+ * Checks whether a particular interrupt is currently enabled or disabled.
+ *
+ * @param usbdev A usbdev handle.
+ * @param irq An interrupt request.
+ * @param[out] state Out-param toggle state of the interrupt.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_irq_get_enabled(const dif_usbdev_t *usbdev,
+                                        dif_usbdev_irq_t irq,
+                                        dif_toggle_t *state);
+
+/**
+ * Sets whether a particular interrupt is currently enabled or disabled.
+ *
+ * @param usbdev A usbdev handle.
+ * @param irq An interrupt request.
+ * @param state The new toggle state for the interrupt.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_irq_set_enabled(const dif_usbdev_t *usbdev,
+                                        dif_usbdev_irq_t irq,
+                                        dif_toggle_t state);
+
+/**
+ * Disables all interrupts, optionally snapshotting all enable states for later
+ * restoration.
+ *
+ * @param usbdev A usbdev handle.
+ * @param[out] snapshot Out-param for the snapshot; may be `NULL`.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_irq_disable_all(
+    const dif_usbdev_t *usbdev, dif_usbdev_irq_enable_snapshot_t *snapshot);
+
+/**
+ * Restores interrupts from the given (enable) snapshot.
+ *
+ * @param usbdev A usbdev handle.
+ * @param snapshot A snapshot to restore from.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_irq_restore_all(
+    const dif_usbdev_t *usbdev,
+    const dif_usbdev_irq_enable_snapshot_t *snapshot);
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif  // __cplusplus
+
+#endif  // OPENTITAN_SW_DEVICE_LIB_DIF_AUTOGEN_DIF_USBDEV_AUTOGEN_H_
diff --git a/sw/device/lib/dif/autogen/dif_usbdev_autogen_unittest.cc b/sw/device/lib/dif/autogen/dif_usbdev_autogen_unittest.cc
new file mode 100644
index 0000000..ad3e952
--- /dev/null
+++ b/sw/device/lib/dif/autogen/dif_usbdev_autogen_unittest.cc
@@ -0,0 +1,388 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+// THIS FILE HAS BEEN GENERATED, DO NOT EDIT MANUALLY. COMMAND:
+// util/make_new_dif.py --mode=regen --only=autogen
+
+#include "sw/device/lib/dif/autogen/dif_usbdev_autogen.h"
+
+#include "gtest/gtest.h"
+#include "sw/device/lib/base/mmio.h"
+#include "sw/device/lib/base/mock_mmio.h"
+#include "sw/device/lib/dif/dif_test_base.h"
+
+#include "usbdev_regs.h"  // Generated.
+
+namespace dif_usbdev_autogen_unittest {
+namespace {
+using ::mock_mmio::MmioTest;
+using ::mock_mmio::MockDevice;
+using ::testing::Eq;
+using ::testing::Test;
+
+class UsbdevTest : public Test, public MmioTest {
+ protected:
+  dif_usbdev_t usbdev_ = {.base_addr = dev().region()};
+};
+
+class InitTest : public UsbdevTest {};
+
+TEST_F(InitTest, NullArgs) {
+  EXPECT_DIF_BADARG(dif_usbdev_init(dev().region(), nullptr));
+}
+
+TEST_F(InitTest, Success) {
+  EXPECT_DIF_OK(dif_usbdev_init(dev().region(), &usbdev_));
+}
+
+class AlertForceTest : public UsbdevTest {};
+
+TEST_F(AlertForceTest, NullArgs) {
+  EXPECT_DIF_BADARG(dif_usbdev_alert_force(nullptr, kDifUsbdevAlertFatalFault));
+}
+
+TEST_F(AlertForceTest, BadAlert) {
+  EXPECT_DIF_BADARG(
+      dif_usbdev_alert_force(nullptr, static_cast<dif_usbdev_alert_t>(32)));
+}
+
+TEST_F(AlertForceTest, Success) {
+  // Force first alert.
+  EXPECT_WRITE32(USBDEV_ALERT_TEST_REG_OFFSET,
+                 {{USBDEV_ALERT_TEST_FATAL_FAULT_BIT, true}});
+  EXPECT_DIF_OK(dif_usbdev_alert_force(&usbdev_, kDifUsbdevAlertFatalFault));
+}
+
+class IrqGetTypeTest : public UsbdevTest {};
+
+TEST_F(IrqGetTypeTest, NullArgs) {
+  dif_irq_type_t type;
+
+  EXPECT_DIF_BADARG(
+      dif_usbdev_irq_get_type(nullptr, kDifUsbdevIrqPktReceived, &type));
+
+  EXPECT_DIF_BADARG(
+      dif_usbdev_irq_get_type(&usbdev_, kDifUsbdevIrqPktReceived, nullptr));
+
+  EXPECT_DIF_BADARG(
+      dif_usbdev_irq_get_type(nullptr, kDifUsbdevIrqPktReceived, nullptr));
+}
+
+TEST_F(IrqGetTypeTest, BadIrq) {
+  dif_irq_type_t type;
+
+  EXPECT_DIF_BADARG(dif_usbdev_irq_get_type(
+      &usbdev_, static_cast<dif_usbdev_irq_t>(kDifUsbdevIrqLinkOutErr + 1),
+      &type));
+}
+
+TEST_F(IrqGetTypeTest, Success) {
+  dif_irq_type_t type;
+
+  EXPECT_DIF_OK(
+      dif_usbdev_irq_get_type(&usbdev_, kDifUsbdevIrqPktReceived, &type));
+  EXPECT_EQ(type, 0);
+}
+
+class IrqGetStateTest : public UsbdevTest {};
+
+TEST_F(IrqGetStateTest, NullArgs) {
+  dif_usbdev_irq_state_snapshot_t irq_snapshot = 0;
+
+  EXPECT_DIF_BADARG(dif_usbdev_irq_get_state(nullptr, &irq_snapshot));
+
+  EXPECT_DIF_BADARG(dif_usbdev_irq_get_state(&usbdev_, nullptr));
+
+  EXPECT_DIF_BADARG(dif_usbdev_irq_get_state(nullptr, nullptr));
+}
+
+TEST_F(IrqGetStateTest, SuccessAllRaised) {
+  dif_usbdev_irq_state_snapshot_t irq_snapshot = 0;
+
+  EXPECT_READ32(USBDEV_INTR_STATE_REG_OFFSET,
+                std::numeric_limits<uint32_t>::max());
+  EXPECT_DIF_OK(dif_usbdev_irq_get_state(&usbdev_, &irq_snapshot));
+  EXPECT_EQ(irq_snapshot, std::numeric_limits<uint32_t>::max());
+}
+
+TEST_F(IrqGetStateTest, SuccessNoneRaised) {
+  dif_usbdev_irq_state_snapshot_t irq_snapshot = 0;
+
+  EXPECT_READ32(USBDEV_INTR_STATE_REG_OFFSET, 0);
+  EXPECT_DIF_OK(dif_usbdev_irq_get_state(&usbdev_, &irq_snapshot));
+  EXPECT_EQ(irq_snapshot, 0);
+}
+
+class IrqIsPendingTest : public UsbdevTest {};
+
+TEST_F(IrqIsPendingTest, NullArgs) {
+  bool is_pending;
+
+  EXPECT_DIF_BADARG(dif_usbdev_irq_is_pending(nullptr, kDifUsbdevIrqPktReceived,
+                                              &is_pending));
+
+  EXPECT_DIF_BADARG(
+      dif_usbdev_irq_is_pending(&usbdev_, kDifUsbdevIrqPktReceived, nullptr));
+
+  EXPECT_DIF_BADARG(
+      dif_usbdev_irq_is_pending(nullptr, kDifUsbdevIrqPktReceived, nullptr));
+}
+
+TEST_F(IrqIsPendingTest, BadIrq) {
+  bool is_pending;
+  // All interrupt CSRs are 32 bit so interrupt 32 will be invalid.
+  EXPECT_DIF_BADARG(dif_usbdev_irq_is_pending(
+      &usbdev_, static_cast<dif_usbdev_irq_t>(32), &is_pending));
+}
+
+TEST_F(IrqIsPendingTest, Success) {
+  bool irq_state;
+
+  // Get the first IRQ state.
+  irq_state = false;
+  EXPECT_READ32(USBDEV_INTR_STATE_REG_OFFSET,
+                {{USBDEV_INTR_STATE_PKT_RECEIVED_BIT, true}});
+  EXPECT_DIF_OK(dif_usbdev_irq_is_pending(&usbdev_, kDifUsbdevIrqPktReceived,
+                                          &irq_state));
+  EXPECT_TRUE(irq_state);
+
+  // Get the last IRQ state.
+  irq_state = true;
+  EXPECT_READ32(USBDEV_INTR_STATE_REG_OFFSET,
+                {{USBDEV_INTR_STATE_LINK_OUT_ERR_BIT, false}});
+  EXPECT_DIF_OK(
+      dif_usbdev_irq_is_pending(&usbdev_, kDifUsbdevIrqLinkOutErr, &irq_state));
+  EXPECT_FALSE(irq_state);
+}
+
+class AcknowledgeStateTest : public UsbdevTest {};
+
+TEST_F(AcknowledgeStateTest, NullArgs) {
+  dif_usbdev_irq_state_snapshot_t irq_snapshot = 0;
+  EXPECT_DIF_BADARG(dif_usbdev_irq_acknowledge_state(nullptr, irq_snapshot));
+}
+
+TEST_F(AcknowledgeStateTest, AckSnapshot) {
+  const uint32_t num_irqs = 17;
+  const uint32_t irq_mask = (1u << num_irqs) - 1;
+  dif_usbdev_irq_state_snapshot_t irq_snapshot = 1;
+
+  // Test a few snapshots.
+  for (size_t i = 0; i < num_irqs; ++i) {
+    irq_snapshot = ~irq_snapshot & irq_mask;
+    irq_snapshot |= (1u << i);
+    EXPECT_WRITE32(USBDEV_INTR_STATE_REG_OFFSET, irq_snapshot);
+    EXPECT_DIF_OK(dif_usbdev_irq_acknowledge_state(&usbdev_, irq_snapshot));
+  }
+}
+
+TEST_F(AcknowledgeStateTest, SuccessNoneRaised) {
+  dif_usbdev_irq_state_snapshot_t irq_snapshot = 0;
+
+  EXPECT_READ32(USBDEV_INTR_STATE_REG_OFFSET, 0);
+  EXPECT_DIF_OK(dif_usbdev_irq_get_state(&usbdev_, &irq_snapshot));
+  EXPECT_EQ(irq_snapshot, 0);
+}
+
+class AcknowledgeAllTest : public UsbdevTest {};
+
+TEST_F(AcknowledgeAllTest, NullArgs) {
+  EXPECT_DIF_BADARG(dif_usbdev_irq_acknowledge_all(nullptr));
+}
+
+TEST_F(AcknowledgeAllTest, Success) {
+  EXPECT_WRITE32(USBDEV_INTR_STATE_REG_OFFSET,
+                 std::numeric_limits<uint32_t>::max());
+
+  EXPECT_DIF_OK(dif_usbdev_irq_acknowledge_all(&usbdev_));
+}
+
+class IrqAcknowledgeTest : public UsbdevTest {};
+
+TEST_F(IrqAcknowledgeTest, NullArgs) {
+  EXPECT_DIF_BADARG(
+      dif_usbdev_irq_acknowledge(nullptr, kDifUsbdevIrqPktReceived));
+}
+
+TEST_F(IrqAcknowledgeTest, BadIrq) {
+  EXPECT_DIF_BADARG(
+      dif_usbdev_irq_acknowledge(nullptr, static_cast<dif_usbdev_irq_t>(32)));
+}
+
+TEST_F(IrqAcknowledgeTest, Success) {
+  // Clear the first IRQ state.
+  EXPECT_WRITE32(USBDEV_INTR_STATE_REG_OFFSET,
+                 {{USBDEV_INTR_STATE_PKT_RECEIVED_BIT, true}});
+  EXPECT_DIF_OK(dif_usbdev_irq_acknowledge(&usbdev_, kDifUsbdevIrqPktReceived));
+
+  // Clear the last IRQ state.
+  EXPECT_WRITE32(USBDEV_INTR_STATE_REG_OFFSET,
+                 {{USBDEV_INTR_STATE_LINK_OUT_ERR_BIT, true}});
+  EXPECT_DIF_OK(dif_usbdev_irq_acknowledge(&usbdev_, kDifUsbdevIrqLinkOutErr));
+}
+
+class IrqForceTest : public UsbdevTest {};
+
+TEST_F(IrqForceTest, NullArgs) {
+  EXPECT_DIF_BADARG(
+      dif_usbdev_irq_force(nullptr, kDifUsbdevIrqPktReceived, true));
+}
+
+TEST_F(IrqForceTest, BadIrq) {
+  EXPECT_DIF_BADARG(
+      dif_usbdev_irq_force(nullptr, static_cast<dif_usbdev_irq_t>(32), true));
+}
+
+TEST_F(IrqForceTest, Success) {
+  // Force first IRQ.
+  EXPECT_WRITE32(USBDEV_INTR_TEST_REG_OFFSET,
+                 {{USBDEV_INTR_TEST_PKT_RECEIVED_BIT, true}});
+  EXPECT_DIF_OK(dif_usbdev_irq_force(&usbdev_, kDifUsbdevIrqPktReceived, true));
+
+  // Force last IRQ.
+  EXPECT_WRITE32(USBDEV_INTR_TEST_REG_OFFSET,
+                 {{USBDEV_INTR_TEST_LINK_OUT_ERR_BIT, true}});
+  EXPECT_DIF_OK(dif_usbdev_irq_force(&usbdev_, kDifUsbdevIrqLinkOutErr, true));
+}
+
+class IrqGetEnabledTest : public UsbdevTest {};
+
+TEST_F(IrqGetEnabledTest, NullArgs) {
+  dif_toggle_t irq_state;
+
+  EXPECT_DIF_BADARG(dif_usbdev_irq_get_enabled(
+      nullptr, kDifUsbdevIrqPktReceived, &irq_state));
+
+  EXPECT_DIF_BADARG(
+      dif_usbdev_irq_get_enabled(&usbdev_, kDifUsbdevIrqPktReceived, nullptr));
+
+  EXPECT_DIF_BADARG(
+      dif_usbdev_irq_get_enabled(nullptr, kDifUsbdevIrqPktReceived, nullptr));
+}
+
+TEST_F(IrqGetEnabledTest, BadIrq) {
+  dif_toggle_t irq_state;
+
+  EXPECT_DIF_BADARG(dif_usbdev_irq_get_enabled(
+      &usbdev_, static_cast<dif_usbdev_irq_t>(32), &irq_state));
+}
+
+TEST_F(IrqGetEnabledTest, Success) {
+  dif_toggle_t irq_state;
+
+  // First IRQ is enabled.
+  irq_state = kDifToggleDisabled;
+  EXPECT_READ32(USBDEV_INTR_ENABLE_REG_OFFSET,
+                {{USBDEV_INTR_ENABLE_PKT_RECEIVED_BIT, true}});
+  EXPECT_DIF_OK(dif_usbdev_irq_get_enabled(&usbdev_, kDifUsbdevIrqPktReceived,
+                                           &irq_state));
+  EXPECT_EQ(irq_state, kDifToggleEnabled);
+
+  // Last IRQ is disabled.
+  irq_state = kDifToggleEnabled;
+  EXPECT_READ32(USBDEV_INTR_ENABLE_REG_OFFSET,
+                {{USBDEV_INTR_ENABLE_LINK_OUT_ERR_BIT, false}});
+  EXPECT_DIF_OK(dif_usbdev_irq_get_enabled(&usbdev_, kDifUsbdevIrqLinkOutErr,
+                                           &irq_state));
+  EXPECT_EQ(irq_state, kDifToggleDisabled);
+}
+
+class IrqSetEnabledTest : public UsbdevTest {};
+
+TEST_F(IrqSetEnabledTest, NullArgs) {
+  dif_toggle_t irq_state = kDifToggleEnabled;
+
+  EXPECT_DIF_BADARG(
+      dif_usbdev_irq_set_enabled(nullptr, kDifUsbdevIrqPktReceived, irq_state));
+}
+
+TEST_F(IrqSetEnabledTest, BadIrq) {
+  dif_toggle_t irq_state = kDifToggleEnabled;
+
+  EXPECT_DIF_BADARG(dif_usbdev_irq_set_enabled(
+      &usbdev_, static_cast<dif_usbdev_irq_t>(32), irq_state));
+}
+
+TEST_F(IrqSetEnabledTest, Success) {
+  dif_toggle_t irq_state;
+
+  // Enable first IRQ.
+  irq_state = kDifToggleEnabled;
+  EXPECT_MASK32(USBDEV_INTR_ENABLE_REG_OFFSET,
+                {{USBDEV_INTR_ENABLE_PKT_RECEIVED_BIT, 0x1, true}});
+  EXPECT_DIF_OK(dif_usbdev_irq_set_enabled(&usbdev_, kDifUsbdevIrqPktReceived,
+                                           irq_state));
+
+  // Disable last IRQ.
+  irq_state = kDifToggleDisabled;
+  EXPECT_MASK32(USBDEV_INTR_ENABLE_REG_OFFSET,
+                {{USBDEV_INTR_ENABLE_LINK_OUT_ERR_BIT, 0x1, false}});
+  EXPECT_DIF_OK(
+      dif_usbdev_irq_set_enabled(&usbdev_, kDifUsbdevIrqLinkOutErr, irq_state));
+}
+
+class IrqDisableAllTest : public UsbdevTest {};
+
+TEST_F(IrqDisableAllTest, NullArgs) {
+  dif_usbdev_irq_enable_snapshot_t irq_snapshot = 0;
+
+  EXPECT_DIF_BADARG(dif_usbdev_irq_disable_all(nullptr, &irq_snapshot));
+
+  EXPECT_DIF_BADARG(dif_usbdev_irq_disable_all(nullptr, nullptr));
+}
+
+TEST_F(IrqDisableAllTest, SuccessNoSnapshot) {
+  EXPECT_WRITE32(USBDEV_INTR_ENABLE_REG_OFFSET, 0);
+  EXPECT_DIF_OK(dif_usbdev_irq_disable_all(&usbdev_, nullptr));
+}
+
+TEST_F(IrqDisableAllTest, SuccessSnapshotAllDisabled) {
+  dif_usbdev_irq_enable_snapshot_t irq_snapshot = 0;
+
+  EXPECT_READ32(USBDEV_INTR_ENABLE_REG_OFFSET, 0);
+  EXPECT_WRITE32(USBDEV_INTR_ENABLE_REG_OFFSET, 0);
+  EXPECT_DIF_OK(dif_usbdev_irq_disable_all(&usbdev_, &irq_snapshot));
+  EXPECT_EQ(irq_snapshot, 0);
+}
+
+TEST_F(IrqDisableAllTest, SuccessSnapshotAllEnabled) {
+  dif_usbdev_irq_enable_snapshot_t irq_snapshot = 0;
+
+  EXPECT_READ32(USBDEV_INTR_ENABLE_REG_OFFSET,
+                std::numeric_limits<uint32_t>::max());
+  EXPECT_WRITE32(USBDEV_INTR_ENABLE_REG_OFFSET, 0);
+  EXPECT_DIF_OK(dif_usbdev_irq_disable_all(&usbdev_, &irq_snapshot));
+  EXPECT_EQ(irq_snapshot, std::numeric_limits<uint32_t>::max());
+}
+
+class IrqRestoreAllTest : public UsbdevTest {};
+
+TEST_F(IrqRestoreAllTest, NullArgs) {
+  dif_usbdev_irq_enable_snapshot_t irq_snapshot = 0;
+
+  EXPECT_DIF_BADARG(dif_usbdev_irq_restore_all(nullptr, &irq_snapshot));
+
+  EXPECT_DIF_BADARG(dif_usbdev_irq_restore_all(&usbdev_, nullptr));
+
+  EXPECT_DIF_BADARG(dif_usbdev_irq_restore_all(nullptr, nullptr));
+}
+
+TEST_F(IrqRestoreAllTest, SuccessAllEnabled) {
+  dif_usbdev_irq_enable_snapshot_t irq_snapshot =
+      std::numeric_limits<uint32_t>::max();
+
+  EXPECT_WRITE32(USBDEV_INTR_ENABLE_REG_OFFSET,
+                 std::numeric_limits<uint32_t>::max());
+  EXPECT_DIF_OK(dif_usbdev_irq_restore_all(&usbdev_, &irq_snapshot));
+}
+
+TEST_F(IrqRestoreAllTest, SuccessAllDisabled) {
+  dif_usbdev_irq_enable_snapshot_t irq_snapshot = 0;
+
+  EXPECT_WRITE32(USBDEV_INTR_ENABLE_REG_OFFSET, 0);
+  EXPECT_DIF_OK(dif_usbdev_irq_restore_all(&usbdev_, &irq_snapshot));
+}
+
+}  // namespace
+}  // namespace dif_usbdev_autogen_unittest
diff --git a/sw/device/lib/dif/dif_usbdev.c b/sw/device/lib/dif/dif_usbdev.c
new file mode 100644
index 0000000..3e5105f
--- /dev/null
+++ b/sw/device/lib/dif/dif_usbdev.c
@@ -0,0 +1,922 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+#include "sw/device/lib/dif/dif_usbdev.h"
+
+#include <assert.h>
+
+#include "sw/device/lib/base/bitfield.h"
+
+#include "usbdev_regs.h"  // Generated.
+
+/**
+ * Definition in the header file (and probably other places) must be updated if
+ * there is a hardware change.
+ */
+static_assert(USBDEV_NUM_ENDPOINTS == USBDEV_PARAM_N_ENDPOINTS,
+              "Mismatch in number of endpoints");
+
+/**
+ * Max packet size is equal to the size of device buffers.
+ */
+#define USBDEV_BUFFER_ENTRY_SIZE_BYTES USBDEV_MAX_PACKET_SIZE
+
+/**
+ * Constants used to indicate that a buffer pool is full or empty.
+ */
+#define BUFFER_POOL_FULL (USBDEV_NUM_BUFFERS - 1)
+#define BUFFER_POOL_EMPTY -1
+
+/**
+ * Hardware information for endpoints.
+ */
+typedef struct endpoint_hw_info {
+  uint32_t config_in_reg_offset;
+  uint8_t bit_index;
+} endpoint_hw_info_t;
+
+/**
+ * Helper macro to define an `endpoint_hw_info_t` entry for endpoint N.
+ *
+ * Note: This uses the bit indices of `USBDEV_IN_SENT` register for the sake
+ * of conciseness because other endpoint registers use the same layout.
+ */
+#define ENDPOINT_HW_INFO_ENTRY(N)                                  \
+  [N] = {.config_in_reg_offset = USBDEV_CONFIGIN_##N##_REG_OFFSET, \
+         .bit_index = USBDEV_IN_SENT_SENT_##N##_BIT}
+
+static const endpoint_hw_info_t kEndpointHwInfos[USBDEV_NUM_ENDPOINTS] = {
+    ENDPOINT_HW_INFO_ENTRY(0),  ENDPOINT_HW_INFO_ENTRY(1),
+    ENDPOINT_HW_INFO_ENTRY(2),  ENDPOINT_HW_INFO_ENTRY(3),
+    ENDPOINT_HW_INFO_ENTRY(4),  ENDPOINT_HW_INFO_ENTRY(5),
+    ENDPOINT_HW_INFO_ENTRY(6),  ENDPOINT_HW_INFO_ENTRY(7),
+    ENDPOINT_HW_INFO_ENTRY(8),  ENDPOINT_HW_INFO_ENTRY(9),
+    ENDPOINT_HW_INFO_ENTRY(10), ENDPOINT_HW_INFO_ENTRY(11),
+};
+
+#undef ENDPOINT_HW_INFO_ENTRY
+
+/**
+ * Static functions for the free buffer pool.
+ */
+
+/**
+ * Checks if a buffer pool is full.
+ *
+ * A buffer pool is full if it contains `USBDEV_NUM_BUFFERS` buffers.
+ *
+ * @param pool A buffer pool.
+ * @return `true` if the buffer pool if full, `false` otherwise.
+ */
+OT_WARN_UNUSED_RESULT
+static bool buffer_pool_is_full(dif_usbdev_buffer_pool_t *pool) {
+  return pool->top == BUFFER_POOL_FULL;
+}
+
+/**
+ * Checks if a buffer pool is empty.
+ *
+ * @param pool A buffer pool.
+ * @return `true` if the buffer pool is empty, `false` otherwise.
+ */
+OT_WARN_UNUSED_RESULT
+static bool buffer_pool_is_empty(dif_usbdev_buffer_pool_t *pool) {
+  return pool->top == BUFFER_POOL_EMPTY;
+}
+
+/**
+ * Checks if a buffer id is valid.
+ *
+ * A buffer id is valid if it is less than `USBDEV_NUM_BUFFERS`.
+ *
+ * @param buffer_id A buffer id.
+ * @return `true` if `buffer_id` is valid, `false` otherwise.
+ */
+OT_WARN_UNUSED_RESULT
+static bool buffer_pool_is_valid_buffer_id(uint8_t buffer_id) {
+  return buffer_id < USBDEV_NUM_BUFFERS;
+}
+
+/**
+ * Adds a buffer to a buffer pool.
+ *
+ * @param pool A buffer pool.
+ * @param buffer_id A buffer id.
+ * @return `true` if the operation was successful, `false` otherwise.
+ */
+OT_WARN_UNUSED_RESULT
+static bool buffer_pool_add(dif_usbdev_buffer_pool_t *pool, uint8_t buffer_id) {
+  if (buffer_pool_is_full(pool) || !buffer_pool_is_valid_buffer_id(buffer_id)) {
+    return false;
+  }
+
+  ++pool->top;
+  pool->buffers[pool->top] = buffer_id;
+
+  return true;
+}
+
+/**
+ * Removes a buffer from a buffer pool.
+ *
+ * @param pool A buffer pool.
+ * @param buffer_id A buffer id.
+ * @return `true` if the operation was successful, `false` otherwise.
+ */
+OT_WARN_UNUSED_RESULT
+static bool buffer_pool_remove(dif_usbdev_buffer_pool_t *pool,
+                               uint8_t *buffer_id) {
+  if (buffer_pool_is_empty(pool) || buffer_id == NULL) {
+    return false;
+  }
+
+  *buffer_id = pool->buffers[pool->top];
+  --pool->top;
+
+  return true;
+}
+
+/**
+ * Initializes the buffer pool.
+ *
+ * At the end of this operation, the buffer pool contains `USBDEV_NUM_BUFFERS`
+ * buffers.
+ *
+ * @param pool A buffer pool.
+ * @return `true` if the operation was successful, `false` otherwise.
+ */
+OT_WARN_UNUSED_RESULT
+static bool buffer_pool_init(dif_usbdev_buffer_pool_t *pool) {
+  // Start with an empty pool
+  pool->top = -1;
+
+  // Add all buffers
+  for (uint8_t i = 0; i < USBDEV_NUM_BUFFERS; ++i) {
+    if (!buffer_pool_add(pool, i)) {
+      return false;
+    }
+  }
+
+  return true;
+}
+
+/**
+ * Utility functions
+ */
+
+/**
+ * Checks if the given value is a valid endpoint number.
+ */
+OT_WARN_UNUSED_RESULT
+static bool is_valid_endpoint(uint8_t endpoint_number) {
+  return endpoint_number < USBDEV_NUM_ENDPOINTS;
+}
+
+/**
+ * Enables/disables the functionality controlled by the register at `reg_offset`
+ * for an endpoint.
+ */
+OT_WARN_UNUSED_RESULT
+static dif_result_t endpoint_functionality_enable(const dif_usbdev_t *usbdev,
+                                                  uint32_t reg_offset,
+                                                  uint8_t endpoint,
+                                                  dif_toggle_t new_state) {
+  if (usbdev == NULL || !is_valid_endpoint(endpoint) ||
+      !dif_is_valid_toggle(new_state)) {
+    return kDifBadArg;
+  }
+
+  uint32_t reg_val = mmio_region_read32(usbdev->base_addr, reg_offset);
+  reg_val = bitfield_bit32_write(reg_val, kEndpointHwInfos[endpoint].bit_index,
+                                 dif_toggle_to_bool(new_state));
+  mmio_region_write32(usbdev->base_addr, reg_offset, reg_val);
+  return kDifOk;
+}
+
+/**
+ * Returns the address that corresponds to the given buffer and offset
+ * into that buffer.
+ */
+OT_WARN_UNUSED_RESULT
+static uint32_t get_buffer_addr(uint8_t buffer_id, size_t offset) {
+  return USBDEV_BUFFER_REG_OFFSET +
+         (buffer_id * USBDEV_BUFFER_ENTRY_SIZE_BYTES) + offset;
+}
+
+/**
+ * USBDEV DIF library functions.
+ */
+
+dif_result_t dif_usbdev_configure(const dif_usbdev_t *usbdev,
+                                  dif_usbdev_buffer_pool_t *buffer_pool,
+                                  dif_usbdev_config_t config) {
+  if (usbdev == NULL || buffer_pool == NULL) {
+    return kDifBadArg;
+  }
+
+  // Configure the free buffer pool.
+  if (!buffer_pool_init(buffer_pool)) {
+    return kDifError;
+  }
+
+  // Check enum fields.
+  if (!dif_is_valid_toggle(config.have_differential_receiver) ||
+      !dif_is_valid_toggle(config.use_tx_d_se0) ||
+      !dif_is_valid_toggle(config.single_bit_eop) ||
+      !dif_is_valid_toggle(config.pin_flip) ||
+      !dif_is_valid_toggle(config.clock_sync_signals)) {
+    return kDifBadArg;
+  }
+
+  // Determine the value of the PHY_CONFIG register.
+  uint32_t phy_config_val = 0;
+  phy_config_val = bitfield_bit32_write(
+      phy_config_val, USBDEV_PHY_CONFIG_USE_DIFF_RCVR_BIT,
+      dif_toggle_to_bool(config.have_differential_receiver));
+  phy_config_val =
+      bitfield_bit32_write(phy_config_val, USBDEV_PHY_CONFIG_TX_USE_D_SE0_BIT,
+                           dif_toggle_to_bool(config.use_tx_d_se0));
+  phy_config_val =
+      bitfield_bit32_write(phy_config_val, USBDEV_PHY_CONFIG_EOP_SINGLE_BIT_BIT,
+                           dif_toggle_to_bool(config.single_bit_eop));
+  phy_config_val =
+      bitfield_bit32_write(phy_config_val, USBDEV_PHY_CONFIG_PINFLIP_BIT,
+                           dif_toggle_to_bool(config.pin_flip));
+  phy_config_val = bitfield_bit32_write(
+      phy_config_val, USBDEV_PHY_CONFIG_USB_REF_DISABLE_BIT,
+      !dif_toggle_to_bool(config.clock_sync_signals));
+
+  // Write configuration to PHY_CONFIG register
+  mmio_region_write32(usbdev->base_addr, USBDEV_PHY_CONFIG_REG_OFFSET,
+                      phy_config_val);
+
+  return kDifOk;
+}
+
+dif_result_t dif_usbdev_fill_available_fifo(
+    const dif_usbdev_t *usbdev, dif_usbdev_buffer_pool_t *buffer_pool) {
+  if (usbdev == NULL || buffer_pool == NULL) {
+    return kDifBadArg;
+  }
+
+  // Remove buffers from the pool and write them to the AV FIFO until it is full
+  while (true) {
+    uint32_t status =
+        mmio_region_read32(usbdev->base_addr, USBDEV_USBSTAT_REG_OFFSET);
+    bool av_full = bitfield_bit32_read(status, USBDEV_USBSTAT_AV_FULL_BIT);
+    if (av_full || buffer_pool_is_empty(buffer_pool)) {
+      break;
+    }
+    uint8_t buffer_id;
+    if (!buffer_pool_remove(buffer_pool, &buffer_id)) {
+      return kDifError;
+    }
+    uint32_t reg_val =
+        bitfield_field32_write(0, USBDEV_AVBUFFER_BUFFER_FIELD, buffer_id);
+    mmio_region_write32(usbdev->base_addr, USBDEV_AVBUFFER_REG_OFFSET, reg_val);
+  }
+
+  return kDifOk;
+}
+
+dif_result_t dif_usbdev_endpoint_setup_enable(const dif_usbdev_t *usbdev,
+                                              uint8_t endpoint,
+                                              dif_toggle_t new_state) {
+  return endpoint_functionality_enable(usbdev, USBDEV_RXENABLE_SETUP_REG_OFFSET,
+                                       endpoint, new_state);
+}
+
+dif_result_t dif_usbdev_endpoint_out_enable(const dif_usbdev_t *usbdev,
+                                            uint8_t endpoint,
+                                            dif_toggle_t new_state) {
+  return endpoint_functionality_enable(usbdev, USBDEV_RXENABLE_OUT_REG_OFFSET,
+                                       endpoint, new_state);
+}
+
+dif_result_t dif_usbdev_endpoint_set_nak_out_enable(const dif_usbdev_t *usbdev,
+                                                    uint8_t endpoint,
+                                                    dif_toggle_t new_state) {
+  return endpoint_functionality_enable(usbdev, USBDEV_SET_NAK_OUT_REG_OFFSET,
+                                       endpoint, new_state);
+}
+
+dif_result_t dif_usbdev_endpoint_stall_enable(const dif_usbdev_t *usbdev,
+                                              dif_usbdev_endpoint_id_t endpoint,
+                                              dif_toggle_t new_state) {
+  if (endpoint.direction == USBDEV_ENDPOINT_DIR_IN) {
+    return endpoint_functionality_enable(usbdev, USBDEV_IN_STALL_REG_OFFSET,
+                                         endpoint.number, new_state);
+  } else {
+    return endpoint_functionality_enable(usbdev, USBDEV_OUT_STALL_REG_OFFSET,
+                                         endpoint.number, new_state);
+  }
+}
+
+dif_result_t dif_usbdev_endpoint_stall_get(const dif_usbdev_t *usbdev,
+                                           dif_usbdev_endpoint_id_t endpoint,
+                                           bool *state) {
+  if (usbdev == NULL || state == NULL || !is_valid_endpoint(endpoint.number)) {
+    return kDifBadArg;
+  }
+
+  ptrdiff_t reg_offset = endpoint.direction == USBDEV_ENDPOINT_DIR_IN
+                             ? USBDEV_IN_STALL_REG_OFFSET
+                             : USBDEV_OUT_STALL_REG_OFFSET;
+  uint32_t reg_val = mmio_region_read32(usbdev->base_addr, reg_offset);
+  *state =
+      bitfield_bit32_read(reg_val, kEndpointHwInfos[endpoint.number].bit_index);
+
+  return kDifOk;
+}
+
+dif_result_t dif_usbdev_endpoint_iso_enable(const dif_usbdev_t *usbdev,
+                                            dif_usbdev_endpoint_id_t endpoint,
+                                            dif_toggle_t new_state) {
+  if (endpoint.direction == USBDEV_ENDPOINT_DIR_IN) {
+    return endpoint_functionality_enable(usbdev, USBDEV_IN_ISO_REG_OFFSET,
+                                         endpoint.number, new_state);
+  } else {
+    return endpoint_functionality_enable(usbdev, USBDEV_OUT_ISO_REG_OFFSET,
+                                         endpoint.number, new_state);
+  }
+}
+
+dif_result_t dif_usbdev_endpoint_enable(const dif_usbdev_t *usbdev,
+                                        dif_usbdev_endpoint_id_t endpoint,
+                                        dif_toggle_t new_state) {
+  if (endpoint.direction == USBDEV_ENDPOINT_DIR_IN) {
+    return endpoint_functionality_enable(usbdev, USBDEV_EP_IN_ENABLE_REG_OFFSET,
+                                         endpoint.number, new_state);
+  } else {
+    return endpoint_functionality_enable(
+        usbdev, USBDEV_EP_OUT_ENABLE_REG_OFFSET, endpoint.number, new_state);
+  }
+  return kDifOk;
+}
+
+dif_result_t dif_usbdev_interface_enable(const dif_usbdev_t *usbdev,
+                                         dif_toggle_t new_state) {
+  if (usbdev == NULL || !dif_is_valid_toggle(new_state)) {
+    return kDifBadArg;
+  }
+
+  uint32_t reg_val =
+      mmio_region_read32(usbdev->base_addr, USBDEV_USBCTRL_REG_OFFSET);
+  reg_val = bitfield_bit32_write(reg_val, USBDEV_USBCTRL_ENABLE_BIT,
+                                 dif_toggle_to_bool(new_state));
+  mmio_region_write32(usbdev->base_addr, USBDEV_USBCTRL_REG_OFFSET, reg_val);
+
+  return kDifOk;
+}
+
+dif_result_t dif_usbdev_recv(const dif_usbdev_t *usbdev,
+                             dif_usbdev_rx_packet_info_t *info,
+                             dif_usbdev_buffer_t *buffer) {
+  if (usbdev == NULL || info == NULL || buffer == NULL) {
+    return kDifBadArg;
+  }
+
+  // Check if the RX FIFO is empty
+  uint32_t fifo_status =
+      mmio_region_read32(usbdev->base_addr, USBDEV_USBSTAT_REG_OFFSET);
+  if (bitfield_bit32_read(fifo_status, USBDEV_USBSTAT_RX_EMPTY_BIT)) {
+    return kDifUnavailable;
+  }
+
+  // Read fifo entry
+  const uint32_t fifo_entry =
+      mmio_region_read32(usbdev->base_addr, USBDEV_RXFIFO_REG_OFFSET);
+  // Init packet info
+  *info = (dif_usbdev_rx_packet_info_t){
+      .endpoint = bitfield_field32_read(fifo_entry, USBDEV_RXFIFO_EP_FIELD),
+      .is_setup = bitfield_bit32_read(fifo_entry, USBDEV_RXFIFO_SETUP_BIT),
+      .length = bitfield_field32_read(fifo_entry, USBDEV_RXFIFO_SIZE_FIELD),
+  };
+  // Init buffer struct
+  *buffer = (dif_usbdev_buffer_t){
+      .id = bitfield_field32_read(fifo_entry, USBDEV_RXFIFO_BUFFER_FIELD),
+      .offset = 0,
+      .remaining_bytes = info->length,
+      .type = kDifUsbdevBufferTypeRead,
+  };
+
+  return kDifOk;
+}
+
+dif_result_t dif_usbdev_buffer_request(const dif_usbdev_t *usbdev,
+                                       dif_usbdev_buffer_pool_t *buffer_pool,
+                                       dif_usbdev_buffer_t *buffer) {
+  if (usbdev == NULL || buffer_pool == NULL || buffer == NULL) {
+    return kDifBadArg;
+  }
+
+  if (buffer_pool_is_empty(buffer_pool)) {
+    return kDifUnavailable;
+  }
+
+  uint8_t buffer_id;
+  if (!buffer_pool_remove(buffer_pool, &buffer_id)) {
+    return kDifError;
+  }
+
+  *buffer = (dif_usbdev_buffer_t){
+      .id = buffer_id,
+      .offset = 0,
+      .remaining_bytes = USBDEV_BUFFER_ENTRY_SIZE_BYTES,
+      .type = kDifUsbdevBufferTypeWrite,
+  };
+
+  return kDifOk;
+}
+
+dif_result_t dif_usbdev_buffer_return(const dif_usbdev_t *usbdev,
+                                      dif_usbdev_buffer_pool_t *buffer_pool,
+                                      dif_usbdev_buffer_t *buffer) {
+  if (usbdev == NULL || buffer_pool == NULL || buffer == NULL) {
+    return kDifBadArg;
+  }
+
+  switch (buffer->type) {
+    case kDifUsbdevBufferTypeRead:
+    case kDifUsbdevBufferTypeWrite:
+      // Return the buffer to the free buffer pool
+      if (!buffer_pool_add(buffer_pool, buffer->id)) {
+        return kDifError;
+      }
+      // Mark the buffer as stale
+      buffer->type = kDifUsbdevBufferTypeStale;
+      return kDifOk;
+    default:
+      return kDifBadArg;
+  }
+}
+
+dif_result_t dif_usbdev_buffer_read(const dif_usbdev_t *usbdev,
+                                    dif_usbdev_buffer_pool_t *buffer_pool,
+                                    dif_usbdev_buffer_t *buffer, uint8_t *dst,
+                                    size_t dst_len, size_t *bytes_written) {
+  if (usbdev == NULL || buffer_pool == NULL || buffer == NULL ||
+      buffer->type != kDifUsbdevBufferTypeRead || dst == NULL) {
+    return kDifBadArg;
+  }
+
+  // bytes_to_copy is the minimum of remaining_bytes and dst_len
+  size_t bytes_to_copy = buffer->remaining_bytes;
+  if (bytes_to_copy > dst_len) {
+    bytes_to_copy = dst_len;
+  }
+  // Copy from buffer to dst
+  const uint32_t buffer_addr = get_buffer_addr(buffer->id, buffer->offset);
+  mmio_region_memcpy_from_mmio32(usbdev->base_addr, buffer_addr, dst,
+                                 bytes_to_copy);
+  // Update buffer state
+  buffer->offset += bytes_to_copy;
+  buffer->remaining_bytes -= bytes_to_copy;
+
+  if (bytes_written != NULL) {
+    *bytes_written = bytes_to_copy;
+  }
+
+  // Check if there are any remaining bytes
+  if (buffer->remaining_bytes > 0) {
+    return kDifOk;
+  }
+
+  // Return the buffer to the free buffer pool
+  if (!buffer_pool_add(buffer_pool, buffer->id)) {
+    return kDifError;
+  }
+
+  // Mark the buffer as stale
+  buffer->type = kDifUsbdevBufferTypeStale;
+  return kDifOk;
+}
+
+dif_result_t dif_usbdev_buffer_write(const dif_usbdev_t *usbdev,
+                                     dif_usbdev_buffer_t *buffer,
+                                     const uint8_t *src, size_t src_len,
+                                     size_t *bytes_written) {
+  if (usbdev == NULL || buffer == NULL ||
+      buffer->type != kDifUsbdevBufferTypeWrite || src == NULL) {
+    return kDifBadArg;
+  }
+
+  // bytes_to_copy is the minimum of remaining_bytes and src_len.
+  size_t bytes_to_copy = buffer->remaining_bytes;
+  if (bytes_to_copy > src_len) {
+    bytes_to_copy = src_len;
+  }
+
+  // Write bytes to the buffer
+  uint32_t buffer_addr = get_buffer_addr(buffer->id, buffer->offset);
+  mmio_region_memcpy_to_mmio32(usbdev->base_addr, buffer_addr, src,
+                               bytes_to_copy);
+
+  buffer->offset += bytes_to_copy;
+  buffer->remaining_bytes -= bytes_to_copy;
+
+  if (bytes_written) {
+    *bytes_written = bytes_to_copy;
+  }
+
+  if (buffer->remaining_bytes == 0 && bytes_to_copy < src_len) {
+    return kDifError;
+  }
+
+  return kDifOk;
+}
+
+dif_result_t dif_usbdev_send(const dif_usbdev_t *usbdev, uint8_t endpoint,
+                             dif_usbdev_buffer_t *buffer) {
+  if (usbdev == NULL || !is_valid_endpoint(endpoint) || buffer == NULL ||
+      buffer->type != kDifUsbdevBufferTypeWrite) {
+    return kDifBadArg;
+  }
+
+  // Get the configin register offset of the endpoint.
+  const uint32_t config_in_reg_offset =
+      kEndpointHwInfos[endpoint].config_in_reg_offset;
+
+  // Configure USBDEV_CONFIGINX register.
+  // Note: Using mask and offset values for the USBDEV_CONFIGIN0 register
+  // for all endpoints because all USBDEV_CONFIGINX registers have the same
+  // layout.
+  uint32_t config_in_val = 0;
+  config_in_val = bitfield_field32_write(
+      config_in_val, USBDEV_CONFIGIN_0_BUFFER_0_FIELD, buffer->id);
+  config_in_val = bitfield_field32_write(
+      config_in_val, USBDEV_CONFIGIN_0_SIZE_0_FIELD, buffer->offset);
+  mmio_region_write32(usbdev->base_addr, config_in_reg_offset, config_in_val);
+
+  // Mark the packet as ready for transmission
+  config_in_val =
+      bitfield_bit32_write(config_in_val, USBDEV_CONFIGIN_0_RDY_0_BIT, true);
+  mmio_region_write32(usbdev->base_addr, config_in_reg_offset, config_in_val);
+
+  // Mark the buffer as stale. It will be returned to the free buffer pool
+  // in dif_usbdev_get_tx_status once transmission is complete.
+  buffer->type = kDifUsbdevBufferTypeStale;
+
+  return kDifOk;
+}
+
+dif_result_t dif_usbdev_get_tx_sent(const dif_usbdev_t *usbdev,
+                                    uint16_t *sent) {
+  if (usbdev == NULL || sent == NULL) {
+    return kDifBadArg;
+  }
+  *sent = mmio_region_read32(usbdev->base_addr, USBDEV_IN_SENT_REG_OFFSET);
+  return kDifOk;
+}
+
+dif_result_t dif_usbdev_clear_tx_status(const dif_usbdev_t *usbdev,
+                                        dif_usbdev_buffer_pool_t *buffer_pool,
+                                        uint8_t endpoint) {
+  if (usbdev == NULL || buffer_pool == NULL || !is_valid_endpoint(endpoint)) {
+    return kDifBadArg;
+  }
+  // Get the configin register offset and bit index of the endpoint.
+  uint32_t config_in_reg_offset =
+      kEndpointHwInfos[endpoint].config_in_reg_offset;
+  uint32_t config_in_reg_val =
+      mmio_region_read32(usbdev->base_addr, config_in_reg_offset);
+  uint8_t buffer = bitfield_field32_read(config_in_reg_val,
+                                         USBDEV_CONFIGIN_0_BUFFER_0_FIELD);
+
+  mmio_region_write32(usbdev->base_addr, config_in_reg_offset,
+                      1u << USBDEV_CONFIGIN_0_PEND_0_BIT);
+  // Clear IN_SENT bit (rw1c).
+  mmio_region_write32(usbdev->base_addr, USBDEV_IN_SENT_REG_OFFSET,
+                      1u << endpoint);
+  // Return the buffer back to the free buffer pool.
+  if (!buffer_pool_add(buffer_pool, buffer)) {
+    return kDifError;
+  }
+  return kDifOk;
+}
+
+dif_result_t dif_usbdev_get_tx_status(const dif_usbdev_t *usbdev,
+                                      uint8_t endpoint,
+                                      dif_usbdev_tx_status_t *status) {
+  if (usbdev == NULL || status == NULL || !is_valid_endpoint(endpoint)) {
+    return kDifBadArg;
+  }
+
+  // Get the configin register offset and bit index of the endpoint.
+  uint32_t config_in_reg_offset =
+      kEndpointHwInfos[endpoint].config_in_reg_offset;
+  uint8_t endpoint_bit_index = kEndpointHwInfos[endpoint].bit_index;
+
+  // Read the configin register.
+  uint32_t config_in_val =
+      mmio_region_read32(usbdev->base_addr, config_in_reg_offset);
+
+  // Check the status of the packet.
+  if (bitfield_bit32_read(config_in_val, USBDEV_CONFIGIN_0_RDY_0_BIT)) {
+    // Packet is marked as ready to be sent and pending transmission.
+    *status = kDifUsbdevTxStatusPending;
+  } else if (bitfield_bit32_read(mmio_region_read32(usbdev->base_addr,
+                                                    USBDEV_IN_SENT_REG_OFFSET),
+                                 endpoint_bit_index)) {
+    // Packet was sent successfully.
+    *status = kDifUsbdevTxStatusSent;
+  } else if (bitfield_bit32_read(config_in_val, USBDEV_CONFIGIN_0_PEND_0_BIT)) {
+    // Canceled due to an IN SETUP packet or link reset.
+    *status = kDifUsbdevTxStatusCancelled;
+  } else {
+    // No packet has been queued for this endpoint.
+    *status = kDifUsbdevTxStatusNoPacket;
+  }
+
+  return kDifOk;
+}
+
+dif_result_t dif_usbdev_address_set(const dif_usbdev_t *usbdev, uint8_t addr) {
+  if (usbdev == NULL) {
+    return kDifBadArg;
+  }
+
+  uint32_t reg_val =
+      mmio_region_read32(usbdev->base_addr, USBDEV_USBCTRL_REG_OFFSET);
+  reg_val = bitfield_field32_write(reg_val, USBDEV_USBCTRL_DEVICE_ADDRESS_FIELD,
+                                   addr);
+  mmio_region_write32(usbdev->base_addr, USBDEV_USBCTRL_REG_OFFSET, reg_val);
+
+  return kDifOk;
+}
+
+dif_result_t dif_usbdev_address_get(const dif_usbdev_t *usbdev, uint8_t *addr) {
+  if (usbdev == NULL || addr == NULL) {
+    return kDifBadArg;
+  }
+
+  uint32_t reg_val =
+      mmio_region_read32(usbdev->base_addr, USBDEV_USBCTRL_REG_OFFSET);
+  // Note: Size of address is 7 bits.
+  *addr = bitfield_field32_read(reg_val, USBDEV_USBCTRL_DEVICE_ADDRESS_FIELD);
+
+  return kDifOk;
+}
+
+dif_result_t dif_usbdev_clear_data_toggle(const dif_usbdev_t *usbdev,
+                                          uint8_t endpoint) {
+  if (usbdev == NULL) {
+    return kDifBadArg;
+  }
+  mmio_region_write32(usbdev->base_addr, USBDEV_DATA_TOGGLE_CLEAR_REG_OFFSET,
+                      1u << endpoint);
+  return kDifOk;
+}
+
+dif_result_t dif_usbdev_status_get_frame(const dif_usbdev_t *usbdev,
+                                         uint16_t *frame_index) {
+  if (usbdev == NULL || frame_index == NULL) {
+    return kDifBadArg;
+  }
+
+  uint32_t reg_val =
+      mmio_region_read32(usbdev->base_addr, USBDEV_USBSTAT_REG_OFFSET);
+  // Note: size of frame index is 11 bits.
+  *frame_index = bitfield_field32_read(reg_val, USBDEV_USBSTAT_FRAME_FIELD);
+
+  return kDifOk;
+}
+
+dif_result_t dif_usbdev_status_get_host_lost(const dif_usbdev_t *usbdev,
+                                             bool *host_lost) {
+  if (usbdev == NULL || host_lost == NULL) {
+    return kDifBadArg;
+  }
+
+  *host_lost =
+      mmio_region_get_bit32(usbdev->base_addr, USBDEV_USBSTAT_REG_OFFSET,
+                            USBDEV_USBSTAT_HOST_LOST_BIT);
+
+  return kDifOk;
+}
+
+dif_result_t dif_usbdev_status_get_link_state(
+    const dif_usbdev_t *usbdev, dif_usbdev_link_state_t *link_state) {
+  if (usbdev == NULL || link_state == NULL) {
+    return kDifBadArg;
+  }
+
+  uint32_t val =
+      mmio_region_read32(usbdev->base_addr, USBDEV_USBSTAT_REG_OFFSET);
+  val = bitfield_field32_read(val, USBDEV_USBSTAT_LINK_STATE_FIELD);
+
+  switch (val) {
+    case USBDEV_USBSTAT_LINK_STATE_VALUE_DISCONNECTED:
+      *link_state = kDifUsbdevLinkStateDisconnected;
+      break;
+    case USBDEV_USBSTAT_LINK_STATE_VALUE_POWERED:
+      *link_state = kDifUsbdevLinkStatePowered;
+      break;
+    case USBDEV_USBSTAT_LINK_STATE_VALUE_POWERED_SUSPENDED:
+      *link_state = kDifUsbdevLinkStatePoweredSuspended;
+      break;
+    case USBDEV_USBSTAT_LINK_STATE_VALUE_ACTIVE:
+      *link_state = kDifUsbdevLinkStateActive;
+      break;
+    case USBDEV_USBSTAT_LINK_STATE_VALUE_SUSPENDED:
+      *link_state = kDifUsbdevLinkStateSuspended;
+      break;
+    case USBDEV_USBSTAT_LINK_STATE_VALUE_ACTIVE_NOSOF:
+      *link_state = kDifUsbdevLinkStateActiveNoSof;
+      break;
+    case USBDEV_USBSTAT_LINK_STATE_VALUE_RESUMING:
+      *link_state = kDifUsbdevLinkStateResuming;
+      break;
+    default:
+      return kDifError;
+  }
+
+  return kDifOk;
+}
+
+dif_result_t dif_usbdev_status_get_sense(const dif_usbdev_t *usbdev,
+                                         bool *sense) {
+  if (usbdev == NULL || sense == NULL) {
+    return kDifBadArg;
+  }
+
+  *sense = mmio_region_get_bit32(usbdev->base_addr, USBDEV_USBSTAT_REG_OFFSET,
+                                 USBDEV_USBSTAT_SENSE_BIT);
+
+  return kDifOk;
+}
+
+dif_result_t dif_usbdev_status_get_available_fifo_depth(
+    const dif_usbdev_t *usbdev, uint8_t *depth) {
+  if (usbdev == NULL || depth == NULL) {
+    return kDifBadArg;
+  }
+
+  uint32_t reg_val =
+      mmio_region_read32(usbdev->base_addr, USBDEV_USBSTAT_REG_OFFSET);
+  // Note: Size of available FIFO depth is 3 bits.
+  *depth = bitfield_field32_read(reg_val, USBDEV_USBSTAT_AV_DEPTH_FIELD);
+
+  return kDifOk;
+}
+
+dif_result_t dif_usbdev_status_get_available_fifo_full(
+    const dif_usbdev_t *usbdev, bool *is_full) {
+  if (usbdev == NULL || is_full == NULL) {
+    return kDifBadArg;
+  }
+
+  uint32_t reg_val =
+      mmio_region_read32(usbdev->base_addr, USBDEV_USBSTAT_REG_OFFSET);
+  *is_full = bitfield_bit32_read(reg_val, USBDEV_USBSTAT_AV_FULL_BIT);
+
+  return kDifOk;
+}
+
+dif_result_t dif_usbdev_status_get_rx_fifo_depth(const dif_usbdev_t *usbdev,
+                                                 uint8_t *depth) {
+  if (usbdev == NULL || depth == NULL) {
+    return kDifBadArg;
+  }
+
+  uint32_t reg_val =
+      mmio_region_read32(usbdev->base_addr, USBDEV_USBSTAT_REG_OFFSET);
+  // Note: Size of RX FIFO depth is 3 bits.
+  *depth = bitfield_field32_read(reg_val, USBDEV_USBSTAT_RX_DEPTH_FIELD);
+
+  return kDifOk;
+}
+
+dif_result_t dif_usbdev_status_get_rx_fifo_empty(const dif_usbdev_t *usbdev,
+                                                 bool *is_empty) {
+  if (usbdev == NULL || is_empty == NULL) {
+    return kDifBadArg;
+  }
+
+  uint32_t reg_val =
+      mmio_region_read32(usbdev->base_addr, USBDEV_USBSTAT_REG_OFFSET);
+  *is_empty = bitfield_bit32_read(reg_val, USBDEV_USBSTAT_RX_EMPTY_BIT);
+
+  return kDifOk;
+}
+
+dif_result_t dif_usbdev_set_osc_test_mode(const dif_usbdev_t *usbdev,
+                                          dif_toggle_t enable) {
+  if (usbdev == NULL || !dif_is_valid_toggle(enable)) {
+    return kDifBadArg;
+  }
+  bool set_tx_osc_mode = dif_toggle_to_bool(enable);
+  uint32_t reg_val =
+      mmio_region_read32(usbdev->base_addr, USBDEV_PHY_CONFIG_REG_OFFSET);
+  reg_val = bitfield_bit32_write(
+      reg_val, USBDEV_PHY_CONFIG_TX_OSC_TEST_MODE_BIT, set_tx_osc_mode);
+  mmio_region_write32(usbdev->base_addr, USBDEV_PHY_CONFIG_REG_OFFSET, reg_val);
+  return kDifOk;
+}
+
+dif_result_t dif_usbdev_set_wake_enable(const dif_usbdev_t *usbdev,
+                                        dif_toggle_t enable) {
+  if (usbdev == NULL || !dif_is_valid_toggle(enable)) {
+    return kDifBadArg;
+  }
+  uint32_t reg_val;
+  if (dif_toggle_to_bool(enable)) {
+    reg_val =
+        bitfield_bit32_write(0, USBDEV_WAKE_CONTROL_SUSPEND_REQ_BIT, true);
+  } else {
+    reg_val = bitfield_bit32_write(0, USBDEV_WAKE_CONTROL_WAKE_ACK_BIT, true);
+  }
+  mmio_region_write32(usbdev->base_addr, USBDEV_WAKE_CONTROL_REG_OFFSET,
+                      reg_val);
+  return kDifOk;
+}
+
+dif_result_t dif_usbdev_get_wake_status(const dif_usbdev_t *usbdev,
+                                        dif_usbdev_wake_status_t *status) {
+  if (usbdev == NULL || status == NULL) {
+    return kDifBadArg;
+  }
+  uint32_t reg_val =
+      mmio_region_read32(usbdev->base_addr, USBDEV_WAKE_EVENTS_REG_OFFSET);
+  status->active =
+      bitfield_bit32_read(reg_val, USBDEV_WAKE_EVENTS_MODULE_ACTIVE_BIT);
+  status->disconnected =
+      bitfield_bit32_read(reg_val, USBDEV_WAKE_EVENTS_DISCONNECTED_BIT);
+  status->bus_reset =
+      bitfield_bit32_read(reg_val, USBDEV_WAKE_EVENTS_BUS_RESET_BIT);
+  return kDifOk;
+}
+
+dif_result_t dif_usbdev_resume_link_to_active(const dif_usbdev_t *usbdev) {
+  if (usbdev == NULL) {
+    return kDifBadArg;
+  }
+  uint32_t reg_val =
+      mmio_region_read32(usbdev->base_addr, USBDEV_USBCTRL_REG_OFFSET);
+  reg_val = bitfield_bit32_write(reg_val, USBDEV_USBCTRL_RESUME_LINK_ACTIVE_BIT,
+                                 true);
+  mmio_region_write32(usbdev->base_addr, USBDEV_USBCTRL_REG_OFFSET, reg_val);
+  return kDifOk;
+}
+
+dif_result_t dif_usbdev_get_phy_pins_status(
+    const dif_usbdev_t *usbdev, dif_usbdev_phy_pins_sense_t *status) {
+  if (usbdev == NULL || status == NULL) {
+    return kDifBadArg;
+  }
+  uint32_t reg_val =
+      mmio_region_read32(usbdev->base_addr, USBDEV_PHY_PINS_SENSE_REG_OFFSET);
+  status->rx_dp =
+      bitfield_bit32_read(reg_val, USBDEV_PHY_PINS_SENSE_RX_DP_I_BIT);
+  status->rx_dn =
+      bitfield_bit32_read(reg_val, USBDEV_PHY_PINS_SENSE_RX_DN_I_BIT);
+  status->rx_d = bitfield_bit32_read(reg_val, USBDEV_PHY_PINS_SENSE_RX_D_I_BIT);
+  status->tx_dp =
+      bitfield_bit32_read(reg_val, USBDEV_PHY_PINS_SENSE_TX_DP_O_BIT);
+  status->tx_dn =
+      bitfield_bit32_read(reg_val, USBDEV_PHY_PINS_SENSE_TX_DN_O_BIT);
+  status->tx_d = bitfield_bit32_read(reg_val, USBDEV_PHY_PINS_SENSE_TX_D_O_BIT);
+  status->tx_se0 =
+      bitfield_bit32_read(reg_val, USBDEV_PHY_PINS_SENSE_TX_SE0_O_BIT);
+  status->output_enable =
+      bitfield_bit32_read(reg_val, USBDEV_PHY_PINS_SENSE_TX_OE_O_BIT);
+  status->vbus_sense =
+      bitfield_bit32_read(reg_val, USBDEV_PHY_PINS_SENSE_PWR_SENSE_BIT);
+  return kDifOk;
+}
+
+dif_result_t dif_usbdev_set_phy_pins_state(
+    const dif_usbdev_t *usbdev, dif_toggle_t override_enable,
+    dif_usbdev_phy_pins_drive_t overrides) {
+  if (usbdev == NULL || !dif_is_valid_toggle(override_enable)) {
+    return kDifBadArg;
+  }
+  bool drive_en = dif_toggle_to_bool(override_enable);
+  uint32_t reg_val =
+      bitfield_bit32_write(0, USBDEV_PHY_PINS_DRIVE_EN_BIT, drive_en);
+  if (drive_en) {
+    reg_val = bitfield_bit32_write(reg_val, USBDEV_PHY_PINS_DRIVE_DP_O_BIT,
+                                   overrides.dp);
+    reg_val = bitfield_bit32_write(reg_val, USBDEV_PHY_PINS_DRIVE_DN_O_BIT,
+                                   overrides.dn);
+    reg_val = bitfield_bit32_write(reg_val, USBDEV_PHY_PINS_DRIVE_D_O_BIT,
+                                   overrides.data);
+    reg_val = bitfield_bit32_write(reg_val, USBDEV_PHY_PINS_DRIVE_SE0_O_BIT,
+                                   overrides.se0);
+    reg_val = bitfield_bit32_write(reg_val, USBDEV_PHY_PINS_DRIVE_OE_O_BIT,
+                                   overrides.output_enable);
+    reg_val =
+        bitfield_bit32_write(reg_val, USBDEV_PHY_PINS_DRIVE_RX_ENABLE_O_BIT,
+                             overrides.diff_receiver_enable);
+    reg_val =
+        bitfield_bit32_write(reg_val, USBDEV_PHY_PINS_DRIVE_DP_PULLUP_EN_O_BIT,
+                             overrides.dp_pullup_en);
+    reg_val =
+        bitfield_bit32_write(reg_val, USBDEV_PHY_PINS_DRIVE_DN_PULLUP_EN_O_BIT,
+                             overrides.dn_pullup_en);
+  }
+  mmio_region_write32(usbdev->base_addr, USBDEV_PHY_PINS_DRIVE_REG_OFFSET,
+                      reg_val);
+  return kDifOk;
+}
diff --git a/sw/device/lib/dif/dif_usbdev.h b/sw/device/lib/dif/dif_usbdev.h
new file mode 100644
index 0000000..12c1a09
--- /dev/null
+++ b/sw/device/lib/dif/dif_usbdev.h
@@ -0,0 +1,876 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef OPENTITAN_SW_DEVICE_LIB_DIF_DIF_USBDEV_H_
+#define OPENTITAN_SW_DEVICE_LIB_DIF_DIF_USBDEV_H_
+
+/**
+ * @file
+ * @brief <a href="/hw/ip/usbdev/doc/">USB Device</a> Device Interface Functions
+ */
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "sw/device/lib/base/macros.h"
+#include "sw/device/lib/base/mmio.h"
+#include "sw/device/lib/dif/dif_base.h"
+
+#include "sw/device/lib/dif/autogen/dif_usbdev_autogen.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif  // __cplusplus
+
+/**
+ * Hardware constants.
+ */
+#define USBDEV_NUM_ENDPOINTS 12
+#define USBDEV_MAX_PACKET_SIZE 64
+// Internal constant that should not be used by clients. Defined here because
+// it is used in the definition of `dif_usbdev_buffer_pool` below.
+#define USBDEV_NUM_BUFFERS 32
+
+// Constants used for the `dif_usbdev_endpoint_id` direction field.
+#define USBDEV_ENDPOINT_DIR_IN 1
+#define USBDEV_ENDPOINT_DIR_OUT 0
+
+typedef struct dif_usbdev_endpoint_id {
+  /**
+   * Endpoint number.
+   */
+  unsigned int number : 4;
+  /**
+   * Reserved. Should be zero.
+   */
+  unsigned int reserved : 3;
+  /**
+   * Endpoint direction. 1 = IN endpoint, 0 = OUT endpoint
+   */
+  unsigned int direction : 1;
+} dif_usbdev_endpoint_id_t;
+
+/**
+ * Free buffer pool.
+ *
+ * A USB device has a fixed number of buffers that are used for storing incoming
+ * and outgoing packets and the software is responsible for keeping track of
+ * free buffers. The pool is implemented as a stack for constant-time add and
+ * remove. `top` points to the last free buffer added to the pool. The pool is
+ * full when `top == USBDEV_NUM_BUFFERS - 1` and empty when `top == -1`.
+ */
+typedef struct dif_usbdev_buffer_pool {
+  uint8_t buffers[USBDEV_NUM_BUFFERS];
+  int8_t top;
+} dif_usbdev_buffer_pool_t;
+
+/**
+ * Buffer types.
+ */
+typedef enum dif_usbdev_buffer_type {
+  /**
+   * For reading payloads of incoming packets.
+   */
+  kDifUsbdevBufferTypeRead,
+  /**
+   * For writing payloads of outgoing packets.
+   */
+  kDifUsbdevBufferTypeWrite,
+  /**
+   * Clients must not use a buffer after it is handed over to hardware or
+   * returned to the free buffer pool. This type exists to protect against such
+   * cases.
+   */
+  kDifUsbdevBufferTypeStale,
+} dif_usbdev_buffer_type_t;
+
+/**
+ * A USB device buffer.
+ *
+ * This struct represents a USB device buffer that has been provided to a client
+ * in response to a buffer request. Clients should treat instances of this
+ * struct as opaque objects and should pass them to the appropriate functions of
+ * this library to read and write payloads of incoming and outgoing packets,
+ * respectively.
+ *
+ * See also: `dif_usbdev_recv`, `dif_usbdev_buffer_read`,
+ * `dif_usbdev_buffer_request`, `dif_usbdev_buffer_write`,
+ * `dif_usbdev_send`, `dif_usbdev_buffer_return`.
+ */
+typedef struct dif_usbdev_buffer {
+  /**
+   * Hardware buffer id.
+   */
+  uint8_t id;
+  /**
+   * Byte offset for the next read or write operation.
+   */
+  uint8_t offset;
+  /**
+   * For read buffers: remaining number of bytes to read.
+   * For write buffers: remaining number of bytes that can be written.
+   */
+  uint8_t remaining_bytes;
+  /**
+   * Type of this buffer.
+   */
+  dif_usbdev_buffer_type_t type;
+} dif_usbdev_buffer_t;
+
+/**
+ * Configuration for initializing a USB device.
+ */
+typedef struct dif_usbdev_config {
+  /**
+   * Activate the single-ended D signal for detecting K and J symbols, for use
+   * with a differential receiver.
+   */
+  dif_toggle_t have_differential_receiver;
+  /**
+   * Use the TX interface with D and SE0 signals instead of Dp/Dn, for use with
+   * certain transceivers.
+   */
+  dif_toggle_t use_tx_d_se0;
+  /*
+   * Recognize a single SE0 bit as end of packet instead of requiring
+   * two bits.
+   */
+  dif_toggle_t single_bit_eop;
+  /**
+   * Flip the D+/D- pins.
+   */
+  dif_toggle_t pin_flip;
+  /**
+   * Reference signal generation for clock synchronization.
+   */
+  dif_toggle_t clock_sync_signals;
+} dif_usbdev_config_t;
+
+/**
+ * Configures a USB device with runtime information.
+ *
+ * This function should need to be called once for the lifetime of `handle`.
+ *
+ * @param usbdev A USB device.
+ * @param buffer_pool A USB device buffer pool.
+ * @param config Runtime configuration parameters for a USB device.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_configure(const dif_usbdev_t *usbdev,
+                                  dif_usbdev_buffer_pool_t *buffer_pool,
+                                  dif_usbdev_config_t config);
+
+/**
+ * Fill the available buffer FIFO of a USB device.
+ *
+ * The USB device has a small FIFO (AV FIFO) that stores free buffers for
+ * incoming packets. It is the responsibility of the software to ensure that the
+ * AV FIFO is never empty. If the host tries to send a packet when the AV FIFO
+ * is empty, the USB device will respond with a NAK. While this will typically
+ * cause the host to retry transmission for regular data packets, there are
+ * transactions in the USB protocol during which the USB device is not allowed
+ * to send a NAK. Thus, the software must make sure that the AV FIFO is never
+ * empty by calling this function periodically.
+ *
+ * @param usbdev A USB device.
+ * @param buffer_pool A USB device buffer pool.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_fill_available_fifo(
+    const dif_usbdev_t *usbdev, dif_usbdev_buffer_pool_t *buffer_pool);
+
+/**
+ * Enable or disable reception of SETUP packets for an endpoint.
+ *
+ * This controls whether the pair of IN and OUT endpoints with the specified
+ * endpoint number are control endpoints.
+ *
+ * @param usbdev A USB device.
+ * @param endpoint An endpoint number.
+ * @param new_state New SETUP packet reception state.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_endpoint_setup_enable(const dif_usbdev_t *usbdev,
+                                              uint8_t endpoint,
+                                              dif_toggle_t new_state);
+
+/**
+ * Enable or disable reception of OUT packets for an active endpoint.
+ *
+ * When disabling reception of OUT packets, what the endpoint will do depends
+ * on other factors. If the endpoint is currently configured as a control
+ * endpoint (receives SETUP packets) or it is configured as an isochronous
+ * endpoint, disabling reception of OUT packets will cause them to be ignored.
+ *
+ * If the endpoint is neither a control nor isochronous endpoint, then its
+ * behavior depends on whether it is configured to respond with STALL. If the
+ * STALL response is not active, then disabling reception will cause usbdev to
+ * NAK the packet. Otherwise, the STALL response takes priority, regardless of
+ * the setting here.
+ *
+ * @param usbdev A USB device.
+ * @param endpoint An OUT endpoint number.
+ * @param new_state New OUT packet reception state.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_endpoint_out_enable(const dif_usbdev_t *usbdev,
+                                            uint8_t endpoint,
+                                            dif_toggle_t new_state);
+
+/**
+ * Enable or disable clearing the out_enable bit after completion of an OUT
+ * transaction to an endpoint.
+ *
+ * If set_nak_out is enabled, an OUT endpoint will disable reception of OUT
+ * packets after each successful OUT transaction to that endpoint, requiring a
+ * call to `dif_usbdev_endpoint_out_enable()` to enable reception again.
+ *
+ * @param usbdev A USB device.
+ * @param endpoint An OUT endpoint number.
+ * @param new_state New set_nak_on_out state.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_endpoint_set_nak_out_enable(const dif_usbdev_t *usbdev,
+                                                    uint8_t endpoint,
+                                                    dif_toggle_t new_state);
+
+/**
+ * Enable or disable STALL for an endpoint.
+ *
+ * @param usbdev A USB device.
+ * @param endpoint An endpoint ID.
+ * @param new_state New STALL state.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_endpoint_stall_enable(const dif_usbdev_t *usbdev,
+                                              dif_usbdev_endpoint_id_t endpoint,
+                                              dif_toggle_t new_state);
+
+/**
+ * Get STALL state of an endpoint.
+ *
+ * @param usbdev A USB device.
+ * @param endpoint An endpoint ID.
+ * @param[out] state Current STALL state.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_endpoint_stall_get(const dif_usbdev_t *usbdev,
+                                           dif_usbdev_endpoint_id_t endpoint,
+                                           bool *state);
+
+/**
+ * Enable or disable isochronous mode for an endpoint.
+ *
+ * Isochronous endpoints transfer data periodically. Since isochronous transfers
+ * do not have a handshaking stage, isochronous endpoints cannot report errors
+ * or STALL conditions.
+ *
+ * @param usbdev A USB device.
+ * @param endpoint An endpoint.
+ * @param new_state New isochronous state.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_endpoint_iso_enable(const dif_usbdev_t *usbdev,
+                                            dif_usbdev_endpoint_id_t endpoint,
+                                            dif_toggle_t new_state);
+
+/**
+ * Enable or disable an endpoint.
+ *
+ * An enabled endpoint responds to packets from the host. A disabled endpoint
+ * ignores them.
+ *
+ * @param usbdev A USB device.
+ * @param endpoint An endpoint.
+ * @param new_state New endpoint state.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_endpoint_enable(const dif_usbdev_t *usbdev,
+                                        dif_usbdev_endpoint_id_t endpoint,
+                                        dif_toggle_t new_state);
+
+/**
+ * Enable the USB interface of a USB device.
+ *
+ * Calling this function causes the USB device to assert the full-speed pull-up
+ * signal to indicate its presence to the host. Ensure the default endpoint is
+ * set up before enabling the interface.
+ *
+ * @param usbdev A USB device.
+ * @param new_state New interface state.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_interface_enable(const dif_usbdev_t *usbdev,
+                                         dif_toggle_t new_state);
+
+/**
+ * Information about a received packet.
+ */
+typedef struct dif_usbdev_rx_packet_info {
+  /**
+   * Endpoint of the packet.
+   */
+  uint8_t endpoint;
+  /**
+   * Payload length in bytes.
+   */
+  uint8_t length;
+  /**
+   * Indicates if the packet is a SETUP packet.
+   */
+  bool is_setup;
+} dif_usbdev_rx_packet_info_t;
+
+/**
+ * Get the packet at the front of RX FIFO.
+ *
+ * The USB device has a small FIFO (RX FIFO) that stores received packets until
+ * the software has a chance to process them. It is the responsibility of the
+ * software to ensure that the RX FIFO is never full. If the host tries to send
+ * a packet when the RX FIFO is full, the USB device will respond with a NAK.
+ * While this will typically cause the host to retry transmission for regular
+ * data packets, there are transactions in the USB protocol during which the USB
+ * device is not allowed to send a NAK. Thus, the software must read received
+ * packets as soon as possible.
+ *
+ * Reading received packets involves two main steps:
+ * - Calling this function, i.e. `dif_usbdev_recv`, and
+ * - Calling `dif_usbdev_buffer_read` until the entire packet payload
+ * is read.
+ *
+ * In order to read an incoming packet, clients should first call this function
+ * to get information about the packet and the buffer that holds the packet
+ * payload. Then, clients should call `dif_usbdev_buffer_read` with this buffer
+ * one or more times (depending on the sizes of their internal buffers) until
+ * the entire packet payload is read. Once the entire payload is read, the
+ * buffer is returned to the free buffer pool. If the clients want to ignore the
+ * payload of a packet, e.g. for an unsupported or a zero-length packet, they
+ * can call `dif_usbdev_buffer_return` to immediately return the buffer to the
+ * free buffer pool.
+ *
+ * @param usbdev A USB device.
+ * @param[out] packet_info Packet information.
+ * @param[out] buffer Buffer that holds the packet payload.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_recv(const dif_usbdev_t *usbdev,
+                             dif_usbdev_rx_packet_info_t *packet_info,
+                             dif_usbdev_buffer_t *buffer);
+
+/**
+ * Read incoming packet payload.
+ *
+ * Clients should call this function with a buffer provided by `dif_usbdev_recv`
+ * to read the payload of an incoming packet. This function copies the smaller
+ * of `dst_len` and remaining number of bytes in the buffer to `dst`. The buffer
+ * that holds the packet payload is returned to the free buffer pool when the
+ * entire packet payload is read.
+ *
+ * See also: `dif_usbdev_recv`.
+ *
+ * @param usbdev A USB device.
+ * @param buffer_pool A USB device buffer pool.
+ * @param buffer A buffer provided by `dif_usbdev_recv`.
+ * @param[out] dst Destination buffer.
+ * @param dst_len Length of the destination buffer.
+ * @param[out] bytes_written Number of bytes written to destination buffer.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_buffer_read(const dif_usbdev_t *usbdev,
+                                    dif_usbdev_buffer_pool_t *buffer_pool,
+                                    dif_usbdev_buffer_t *buffer, uint8_t *dst,
+                                    size_t dst_len, size_t *bytes_written);
+
+/**
+ * Return a buffer to the free buffer pool.
+ *
+ * This function immediately returns the given buffer to the free buffer pool.
+ * Since `dif_usbdev_buffer_read` and `dif_usbdev_get_tx_status` return the
+ * buffers that they work on to the free buffer pool automatically, this
+ * function should only be called to discard the payload of a received
+ * packet or a packet that was being prepared for transmission before it is
+ * queued for transmission from an endpoint.
+ *
+ * See also: `dif_usbdev_recv`, `dif_usbdev_buffer_request`.
+ *
+ * @param usbdev A USB device.
+ * @param buffer_pool A USB device buffer pool.
+ * @param buffer A buffer provided by `dif_usbdev_recv` or
+ *               `dif_usbdev_buffer_request`.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_buffer_return(const dif_usbdev_t *usbdev,
+                                      dif_usbdev_buffer_pool_t *buffer_pool,
+                                      dif_usbdev_buffer_t *buffer);
+
+/**
+ * Request a buffer for outgoing packet payload.
+ *
+ * Clients should call this function to request a buffer to write the payload of
+ * an outgoing packet. Sending a packet from a particular endpoint to the host
+ * involves four main steps:
+ * - Calling this function, i.e. `dif_usbdev_buffer_request`,
+ * - Calling `dif_usbdev_buffer_write`,
+ * - Calling `dif_usbdev_send`, and
+ * - Calling `dif_usbdev_get_tx_status`.
+ *
+ * In order to send a packet, clients should first call this function to obtain
+ * a buffer for the packet payload. Clients should then call
+ * `dif_usbdev_buffer_write` (one or more times depending on the sizes of their
+ * internal buffers) to write the packet payload to this buffer. After writing
+ * the packet payload, clients should call `dif_usbdev_send` to mark the packet
+ * as ready for transmission from a particular endpoint. Then, clients should
+ * call `dif_usbdev_get_tx_status` to check the status of the transmission.
+ * `dif_usbdev_get_tx_status` returns the buffer that holds the packet payload
+ * to the free buffer pool once the packet is either successfully transmitted or
+ * canceled due to an incoming SETUP packet or a link reset. If the packet
+ * should no longer be sent, clients can call `dif_usbdev_buffer_return` to
+ * return the buffer to the free buffer pool as long as `dif_usbdev_send` is not
+ * called yet.
+ *
+ * See also: `dif_usbdev_buffer_write`, `dif_usbdev_send`,
+ * `dif_usbdev_get_tx_status`, `dif_usbdev_buffer_return`.
+ *
+ * @param usbdev A USB device.
+ * @param buffer_pool A USB device buffer pool.
+ * @param[out] buffer A buffer for writing outgoing packet payload.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_buffer_request(const dif_usbdev_t *usbdev,
+                                       dif_usbdev_buffer_pool_t *buffer_pool,
+                                       dif_usbdev_buffer_t *buffer);
+
+/**
+ * Write outgoing packet payload.
+ *
+ * Clients should call this function with a buffer provided by
+ * `dif_usbdev_buffer_request` to write the payload of an outgoing packet. This
+ * function copies the smaller of `src_len` and remaining number of bytes in the
+ * buffer to the buffer. Clients should then call `dif_usbdev_send` to queue the
+ * packet for transmission from a particular endpoint.
+ *
+ * See also: `dif_usbdev_buffer_request`, `dif_usbdev_send`,
+ * `dif_usbdev_get_tx_status`, `dif_usbdev_buffer_return`.
+ *
+ * @param usbdev A USB device.
+ * @param buffer A buffer provided by `dif_usbdev_buffer_request`.
+ * @param src Source buffer.
+ * @param src_len Length of the source buffer.
+ * @param[out] bytes_written Number of bytes written to the USB device buffer.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_buffer_write(const dif_usbdev_t *usbdev,
+                                     dif_usbdev_buffer_t *buffer,
+                                     const uint8_t *src, size_t src_len,
+                                     size_t *bytes_written);
+
+/**
+ * Mark a packet ready for transmission from an endpoint.
+ *
+ * The USB device has 12 endpoints, each of which can be used to send packets to
+ * the host. Since a packet is not actually transmitted to the host until the
+ * host sends an IN token, clients must write the packet payload to a device
+ * buffer and mark it as ready for transmission from a particular endpoint. A
+ * packet queued for transmission from a particular endpoint is transmitted once
+ * the host sends an IN token for that endpoint.
+ *
+ * After a packet is queued for transmission, clients should check its status by
+ * calling `dif_usbdev_get_tx_status`. While the USB device handles transmission
+ * errors automatically by retrying transmission, transmission of a packet may
+ * be canceled if the endpoint receives a SETUP packet or the link is reset
+ * before the queued packet is transmitted. In these cases, clients should
+ * handle the SETUP packet or the link reset first and then optionally send the
+ * same packet again. Clients must also make sure that the given endpoint does
+ * not already have a packet pending for transmission before calling this
+ * function.
+ *
+ * See also: `dif_usbdev_buffer_request`, `dif_usbdev_buffer_write`,
+ * `dif_usbdev_get_tx_status`, `dif_usbdev_buffer_return`.
+ *
+ * @param usbdev A USB device.
+ * @param endpoint An OUT endpoint number.
+ * @param buffer A buffer provided by `dif_usbdev_buffer_request`.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_send(const dif_usbdev_t *usbdev, uint8_t endpoint,
+                             dif_usbdev_buffer_t *buffer);
+
+/**
+ * Get which IN endpoints have sent packets.
+ *
+ * This function provides which endpoints have buffers that have successfully
+ * completed transmission to the host. It may be used to guide calls to
+ * `dif_usbdev_clear_tx_status` to return the used buffer to the pool and clear
+ * the state for the next transaction.
+ *
+ * @param usbdev A USB device.
+ * @param[out] sent A bitmap of which endpoints have sent packets.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_get_tx_sent(const dif_usbdev_t *usbdev, uint16_t *sent);
+
+/**
+ * Clear the TX state of the provided endpoint and restore its associated buffer
+ * to the pool.
+ *
+ * Note that this function should only be called when an endpoint has been
+ * provided a buffer. Without it, the buffer pool will become corrupted, as this
+ * function does not check the status.
+ *
+ * In addition, if the endpoint has not yet completed or canceled the
+ * transaction, the user must not call this function while the device is in an
+ * active state. Otherwise, the user risks corrupting an ongoing transaction.
+ *
+ * @param usbdev A USB device.
+ * @param buffer_pool A USB device buffer pool.
+ * @param endpoint An IN endpoint number.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_clear_tx_status(const dif_usbdev_t *usbdev,
+                                        dif_usbdev_buffer_pool_t *buffer_pool,
+                                        uint8_t endpoint);
+
+/**
+ * Status of an outgoing packet.
+ */
+typedef enum dif_usbdev_tx_status {
+  /**
+   *  There is no packet for the given OUT endpoint.
+   */
+  kDifUsbdevTxStatusNoPacket,
+  /**
+   * Packet is pending transmission.
+   */
+  kDifUsbdevTxStatusPending,
+  /**
+   * Packet was sent successfully.
+   */
+  kDifUsbdevTxStatusSent,
+  /**
+   * Transmission was canceled due to an incoming SETUP packet.
+   */
+  kDifUsbdevTxStatusCancelled,
+} dif_usbdev_tx_status_t;
+
+/**
+ * Get the status of a packet that has been queued to be sent from an endpoint.
+ *
+ * While the USB device handles transmission errors automatically by retrying
+ * transmission, transmission of a packet may be canceled if the endpoint
+ * receives a SETUP packet or the link is reset before the queued packet is
+ * transmitted. In these cases, clients should handle the SETUP packet or the
+ * link reset first and then optionally send the same packet again.
+ *
+ * This function does not modify any device state. `dif_usbdev_clear_tx_status`
+ * can be used to clear the status and return the buffer to the pool.
+ *
+ * @param usbdev A USB device.
+ * @param endpoint An IN endpoint number.
+ * @param[out] status Status of the packet.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_get_tx_status(const dif_usbdev_t *usbdev,
+                                      uint8_t endpoint,
+                                      dif_usbdev_tx_status_t *status);
+
+/**
+ * Set the address of a USB device.
+ *
+ * @param usbdev A USB device.
+ * @param addr New address. Only the last 7 bits are significant.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_address_set(const dif_usbdev_t *usbdev, uint8_t addr);
+
+/**
+ * Get the address of a USB device.
+ *
+ * @param usbdev A USB device.
+ * @param[out] addr Current address.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_address_get(const dif_usbdev_t *usbdev, uint8_t *addr);
+
+/**
+ * Clear the data toggle bits for the selected endpoint.
+ *
+ * @param usbdev A USB device.
+ * @param endpoint An endpoint number.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_clear_data_toggle(const dif_usbdev_t *usbdev,
+                                          uint8_t endpoint);
+
+/**
+ * Get USB frame index.
+ *
+ * @param usbdev A USB device.
+ * @param[out] frame_index USB frame index.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_status_get_frame(const dif_usbdev_t *usbdev,
+                                         uint16_t *frame_index);
+
+/**
+ * Check if the host is lost.
+ *
+ * The host is lost if the link is still active but a start of frame packet has
+ * not been received in the last 4.096ms.
+ *
+ * @param usbdev A USB device.
+ * @param[out] host_lost Status of the host. `true` if the host is lost, `false`
+ * otherwise.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_status_get_host_lost(const dif_usbdev_t *usbdev,
+                                             bool *host_lost);
+
+/**
+ * USB link state.
+ */
+typedef enum dif_usbdev_link_state {
+  kDifUsbdevLinkStateDisconnected,
+  kDifUsbdevLinkStatePowered,
+  kDifUsbdevLinkStatePoweredSuspended,
+  kDifUsbdevLinkStateActive,
+  kDifUsbdevLinkStateSuspended,
+  kDifUsbdevLinkStateActiveNoSof,
+  kDifUsbdevLinkStateResuming,
+} dif_usbdev_link_state_t;
+
+/**
+ * Get USB link state.
+ *
+ * @param usbdev A USB device.
+ * @param[out] link_state USB link state.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_status_get_link_state(
+    const dif_usbdev_t *usbdev, dif_usbdev_link_state_t *link_state);
+
+/**
+ * Get the state of the sense pin.
+ *
+ * @param usbdev A USB device.
+ * @param[out] sense State of the sense pin. `true` if the host is providing
+ * VBUS, `false` otherwise.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_status_get_sense(const dif_usbdev_t *usbdev,
+                                         bool *sense);
+
+/**
+ * Get the depth of the AV FIFO.
+ *
+ * See also: `dif_usbdev_fill_available_fifo`.
+ *
+ * @param usbdev A USB device.
+ * @param[out] depth Depth of the AV FIFO.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_status_get_available_fifo_depth(
+    const dif_usbdev_t *usbdev, uint8_t *depth);
+/**
+ * Check if AV FIFO is full.
+ *
+ * See also: `dif_usbdev_fill_available_fifo`.
+ *
+ * @param usbdev A USB device.
+ * @param[out] is_full State of the AV FIFO. `true` if full, false otherwise.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_status_get_available_fifo_full(
+    const dif_usbdev_t *usbdev, bool *is_full);
+/**
+ * Get the depth of the RX FIFO.
+ *
+ * See also: `dif_usbdev_recv`.
+ *
+ * @param usbdev A USB device.
+ * @param[out] depth Depth of the RX FIFO.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_status_get_rx_fifo_depth(const dif_usbdev_t *usbdev,
+                                                 uint8_t *depth);
+
+/**
+ * Check if the RX FIFO is empty.
+ *
+ * See also: `dif_usbdev_recv`.
+ *
+ * @param usbdev A USB device.
+ * @param[out] is_empty State of the RX FIFO. `true` if empty, `false`
+ * otherwise.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_status_get_rx_fifo_empty(const dif_usbdev_t *usbdev,
+                                                 bool *is_empty);
+
+/**
+ * Control whether oscillator test mode is enabled.
+ *
+ * In oscillator test mode, usbdev transmits a continuous 0101 pattern for
+ * evaluating the reference clock's quality.
+ *
+ * @param usbdev A USB device.
+ * @param enable Whether the test mode should be enabled.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_set_osc_test_mode(const dif_usbdev_t *usbdev,
+                                          dif_toggle_t enable);
+
+/**
+ * Control whether the AON wake module is active.
+ *
+ * @param usbdev A USB device.
+ * @param enable Whether the AON wake module is enabled.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_set_wake_enable(const dif_usbdev_t *usbdev,
+                                        dif_toggle_t enable);
+
+typedef struct dif_usbdev_wake_status {
+  /** Whether the AON wake module is active. */
+  bool active;
+  /** Whether the USB disconnected while the AON wake module was active. */
+  bool disconnected;
+  /** Whether the USB was reset while the AON wake module was active. */
+  bool bus_reset;
+} dif_usbdev_wake_status_t;
+
+/**
+ * Get the status of the AON wake module.
+ *
+ * Note that the conditions triggering exit from suspended state must be read
+ * before disabling the AON wake module. Once the AON wake module is
+ * deactivated, that status information is lost.
+ *
+ * Also note that the ordinary resume condition does not report to the usbdev
+ * module. Instead, it should be obtained from the module monitoring wakeup
+ * sources.
+ *
+ * @param usbdev A USB device.
+ * @param[out] status The status of the module.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_get_wake_status(const dif_usbdev_t *usbdev,
+                                        dif_usbdev_wake_status_t *status);
+
+/**
+ * Force the link state machine to resume to an active state.
+ *
+ * This is used when waking from a low-power suspended state to resume to an
+ * active state. It moves the usbdev out of the Powered state (from the USB
+ * device state machine in the spec) without receiving a bus reset. Without help
+ * from software, the usbdev module cannot determine on its own when a bus reset
+ * is required.
+ *
+ * @param usbdev A USB device.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_resume_link_to_active(const dif_usbdev_t *usbdev);
+
+typedef struct dif_usbdev_phy_pins_sense {
+  /** USB D+ input. */
+  bool rx_dp : 1;
+  /** USB D- input. */
+  bool rx_dn : 1;
+  /** USB data input from an external differential receiver, if available. */
+  bool rx_d : 1;
+  /** USB transmit D+ output. */
+  bool tx_dp : 1;
+  /** USB transmit D- output. */
+  bool tx_dn : 1;
+  /** USB transmit data value output. */
+  bool tx_d : 1;
+  /** USB single-ended zero output. */
+  bool tx_se0 : 1;
+  /** USB output enable for D+ / D-. */
+  bool output_enable : 1;
+  /** USB VBUS sense pin. */
+  bool vbus_sense : 1;
+} dif_usbdev_phy_pins_sense_t;
+
+/**
+ * Get the current state of the USB PHY pins.
+ *
+ * @param usbdev A USB device.
+ * @param[out] status The current state of the pins.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_get_phy_pins_status(
+    const dif_usbdev_t *usbdev, dif_usbdev_phy_pins_sense_t *status);
+
+typedef struct dif_usbdev_phy_pins_drive {
+  /** USB D+ output, for use with dn. */
+  bool dp : 1;
+  /** USB D- output. for use with dp. */
+  bool dn : 1;
+  /** USB data output, encoding K and J when se0 is 0. */
+  bool data : 1;
+  /** USB single-ended zero output. */
+  bool se0 : 1;
+  /** USB output enable for D+ / D-. */
+  bool output_enable : 1;
+  /** Enable control pin for the differential receiver. */
+  bool diff_receiver_enable : 1;
+  /** Controls whether to pull up the D+ pin. */
+  bool dp_pullup_en : 1;
+  /** Controls whether to pull up the D- pin. */
+  bool dn_pullup_en : 1;
+} dif_usbdev_phy_pins_drive_t;
+
+/**
+ * Control whether to override the USB PHY and drive pins as GPIOs.
+ *
+ * @param usbdev A USB device.
+ * @param override_enable Enable / disable the GPIO-like overrides.
+ * @param overrides The values to set the pins to.
+ * @return The result of the operation.
+ */
+OT_WARN_UNUSED_RESULT
+dif_result_t dif_usbdev_set_phy_pins_state(
+    const dif_usbdev_t *usbdev, dif_toggle_t override_enable,
+    dif_usbdev_phy_pins_drive_t overrides);
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif  // __cplusplus
+
+#endif  // OPENTITAN_SW_DEVICE_LIB_DIF_DIF_USBDEV_H_
diff --git a/sw/device/lib/dif/dif_usbdev.md b/sw/device/lib/dif/dif_usbdev.md
new file mode 100644
index 0000000..0e5f851
--- /dev/null
+++ b/sw/device/lib/dif/dif_usbdev.md
@@ -0,0 +1,48 @@
+# USB Device DIF Checklist
+
+This checklist is for [Development Stage](../../../../doc/project_governance/development_stages.md) transitions for the [USB Device DIF](../../../../hw/ip/usbdev/README.md).
+All checklist items refer to the content in the [Checklist](../../../../doc/project_governance/checklist/README.md).
+
+<h2>DIF Checklist</h2>
+
+<h3>S1</h3>
+
+Type           | Item                   | Resolution  | Note/Collaterals
+---------------|------------------------|-------------|------------------
+Implementation | [DIF_EXISTS][]         | Done        |
+Implementation | [DIF_USED_IN_TREE][]   | Done        |
+Tests          | [DIF_TEST_ON_DEVICE][] | Done        |
+
+[DIF_EXISTS]:         ../../../../doc/project_governance/checklist/README.md#dif_exists
+[DIF_USED_IN_TREE]:   ../../../../doc/project_governance/checklist/README.md#dif_used_in_tree
+[DIF_TEST_ON_DEVICE]: ../../../../doc/project_governance/checklist/README.md#dif_test_on_device
+
+<h3>S2</h3>
+
+Type           | Item                        | Resolution  | Note/Collaterals
+---------------|-----------------------------|-------------|------------------
+Coordination   | [DIF_HW_FEATURE_COMPLETE][] | Done        | [HW Dashboard](../../../../hw/README.md)
+Implementation | [DIF_FEATURES][]            | Done        |
+
+[DIF_HW_FEATURE_COMPLETE]: ../../../../doc/project_governance/checklist/README.md#dif_hw_feature_complete
+[DIF_FEATURES]:            ../../../../doc/project_governance/checklist/README.md#dif_features
+
+<h3>S3</h3>
+
+Type           | Item                             | Resolution  | Note/Collaterals
+---------------|----------------------------------|-------------|------------------
+Coordination   | [DIF_HW_DESIGN_COMPLETE][]       | Not Started |
+Coordination   | [DIF_HW_VERIFICATION_COMPLETE][] | Not Started |
+Documentation  | [DIF_DOC_HW][]                   | Not Started |
+Code Quality   | [DIF_CODE_STYLE][]               | Not Started |
+Tests          | [DIF_TEST_UNIT][]                | Done        |
+Review         | [DIF_TODO_COMPLETE][]            | Not Started |
+Review         | Reviewer(s)                      | Not Started |
+Review         | Signoff date                     | Not Started |
+
+[DIF_HW_DESIGN_COMPLETE]:       ../../../../doc/project_governance/checklist/README.md#dif_hw_design_complete
+[DIF_HW_VERIFICATION_COMPLETE]: ../../../../doc/project_governance/checklist/README.md#dif_hw_verification_complete
+[DIF_DOC_HW]:                   ../../../../doc/project_governance/checklist/README.md#dif_doc_hw
+[DIF_CODE_STYLE]:               ../../../../doc/project_governance/checklist/README.md#dif_code_style
+[DIF_TEST_UNIT]:                ../../../../doc/project_governance/checklist/README.md#dif_test_unit
+[DIF_TODO_COMPLETE]:            ../../../../doc/project_governance/checklist/README.md#dif_todo_complete
diff --git a/sw/device/lib/dif/dif_usbdev_unittest.cc b/sw/device/lib/dif/dif_usbdev_unittest.cc
new file mode 100644
index 0000000..404706a
--- /dev/null
+++ b/sw/device/lib/dif/dif_usbdev_unittest.cc
@@ -0,0 +1,1045 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+#include "sw/device/lib/dif/dif_usbdev.h"
+
+#include "gtest/gtest.h"
+#include "sw/device/lib/base/mmio.h"
+#include "sw/device/lib/base/mock_mmio.h"
+#include "sw/device/lib/base/multibits.h"
+#include "sw/device/lib/dif/dif_base.h"
+#include "sw/device/lib/dif/dif_test_base.h"
+
+#include "usbdev_regs.h"  // Generated.
+
+namespace dif_usbdev_unittest {
+namespace {
+using mock_mmio::MmioTest;
+using mock_mmio::MockDevice;
+using testing::Test;
+
+class UsbdevTest : public Test, public MmioTest {
+ protected:
+  dif_usbdev_t usbdev_ = {
+      .base_addr = dev().region(),
+  };
+};
+
+TEST_F(UsbdevTest, NullArgsTest) {
+  dif_usbdev_config_t config;
+  dif_usbdev_buffer_pool_t buffer_pool;
+  bool bool_arg;
+  dif_usbdev_rx_packet_info_t packet_info;
+  dif_usbdev_buffer_t buffer;
+  uint8_t uint8_arg;
+  size_t size_arg;
+  dif_usbdev_endpoint_id_t endpoint_id;
+  dif_usbdev_tx_status_t tx_status;
+  uint16_t uint16_arg;
+  dif_usbdev_link_state_t link_state;
+  dif_usbdev_wake_status_t wake_status;
+  dif_usbdev_phy_pins_sense_t phy_pins_status;
+  dif_usbdev_phy_pins_drive_t phy_pins_drive;
+
+  EXPECT_DIF_BADARG(dif_usbdev_configure(nullptr, &buffer_pool, config));
+  EXPECT_DIF_BADARG(dif_usbdev_configure(&usbdev_, nullptr, config));
+  EXPECT_DIF_BADARG(dif_usbdev_fill_available_fifo(nullptr, &buffer_pool));
+  EXPECT_DIF_BADARG(dif_usbdev_fill_available_fifo(&usbdev_, nullptr));
+  EXPECT_DIF_BADARG(dif_usbdev_endpoint_setup_enable(nullptr, /*endpoint=*/0,
+                                                     kDifToggleEnabled));
+  EXPECT_DIF_BADARG(dif_usbdev_endpoint_out_enable(nullptr, /*endpoint=*/0,
+                                                   kDifToggleEnabled));
+  EXPECT_DIF_BADARG(dif_usbdev_endpoint_set_nak_out_enable(
+      nullptr, /*endpoint=*/0, kDifToggleEnabled));
+  EXPECT_DIF_BADARG(dif_usbdev_endpoint_stall_enable(nullptr, endpoint_id,
+                                                     kDifToggleEnabled));
+  EXPECT_DIF_BADARG(
+      dif_usbdev_endpoint_stall_get(nullptr, endpoint_id, &bool_arg));
+  EXPECT_DIF_BADARG(
+      dif_usbdev_endpoint_stall_get(&usbdev_, endpoint_id, nullptr));
+  EXPECT_DIF_BADARG(
+      dif_usbdev_endpoint_iso_enable(nullptr, endpoint_id, kDifToggleEnabled));
+  EXPECT_DIF_BADARG(
+      dif_usbdev_endpoint_enable(nullptr, endpoint_id, kDifToggleEnabled));
+  EXPECT_DIF_BADARG(dif_usbdev_interface_enable(nullptr, kDifToggleEnabled));
+  EXPECT_DIF_BADARG(dif_usbdev_recv(nullptr, &packet_info, &buffer));
+  EXPECT_DIF_BADARG(dif_usbdev_recv(&usbdev_, nullptr, &buffer));
+  EXPECT_DIF_BADARG(dif_usbdev_recv(&usbdev_, &packet_info, nullptr));
+  EXPECT_DIF_BADARG(dif_usbdev_buffer_read(
+      nullptr, &buffer_pool, &buffer, &uint8_arg, /*dst_len=*/1, &size_arg));
+  EXPECT_DIF_BADARG(dif_usbdev_buffer_read(
+      &usbdev_, nullptr, &buffer, &uint8_arg, /*dst_len=*/1, &size_arg));
+  EXPECT_DIF_BADARG(dif_usbdev_buffer_read(
+      &usbdev_, &buffer_pool, nullptr, &uint8_arg, /*dst_len=*/1, &size_arg));
+  EXPECT_DIF_BADARG(dif_usbdev_buffer_read(&usbdev_, &buffer_pool, &buffer,
+                                           nullptr, /*dst_len=*/1, &size_arg));
+  EXPECT_DIF_BADARG(dif_usbdev_buffer_read(&usbdev_, &buffer_pool, &buffer,
+                                           &uint8_arg, /*dst_len=*/1, nullptr));
+  EXPECT_DIF_BADARG(dif_usbdev_buffer_return(nullptr, &buffer_pool, &buffer));
+  EXPECT_DIF_BADARG(dif_usbdev_buffer_return(&usbdev_, nullptr, &buffer));
+  EXPECT_DIF_BADARG(dif_usbdev_buffer_return(&usbdev_, &buffer_pool, nullptr));
+  EXPECT_DIF_BADARG(dif_usbdev_buffer_request(nullptr, &buffer_pool, &buffer));
+  EXPECT_DIF_BADARG(dif_usbdev_buffer_request(&usbdev_, nullptr, &buffer));
+  EXPECT_DIF_BADARG(dif_usbdev_buffer_request(&usbdev_, &buffer_pool, nullptr));
+  EXPECT_DIF_BADARG(dif_usbdev_buffer_write(nullptr, &buffer, &uint8_arg,
+                                            /*src_len=*/1, &size_arg));
+  EXPECT_DIF_BADARG(dif_usbdev_buffer_write(&usbdev_, nullptr, &uint8_arg,
+                                            /*src_len=*/1, &size_arg));
+  EXPECT_DIF_BADARG(dif_usbdev_buffer_write(&usbdev_, &buffer, nullptr,
+                                            /*src_len=*/1, &size_arg));
+  EXPECT_DIF_BADARG(dif_usbdev_buffer_write(&usbdev_, &buffer, &uint8_arg,
+                                            /*src_len=*/1, nullptr));
+  EXPECT_DIF_BADARG(dif_usbdev_send(nullptr, /*endpoint=*/0, &buffer));
+  EXPECT_DIF_BADARG(dif_usbdev_send(&usbdev_, /*endpoint=*/0, nullptr));
+  EXPECT_DIF_BADARG(dif_usbdev_get_tx_sent(nullptr, &uint16_arg));
+  EXPECT_DIF_BADARG(dif_usbdev_get_tx_sent(&usbdev_, nullptr));
+  EXPECT_DIF_BADARG(
+      dif_usbdev_clear_tx_status(nullptr, &buffer_pool, /*endpoint=*/0));
+  EXPECT_DIF_BADARG(
+      dif_usbdev_clear_tx_status(&usbdev_, nullptr, /*endpoint=*/0));
+  EXPECT_DIF_BADARG(
+      dif_usbdev_get_tx_status(nullptr, /*endpoint=*/0, &tx_status));
+  EXPECT_DIF_BADARG(
+      dif_usbdev_get_tx_status(&usbdev_, /*endpoint=*/0, nullptr));
+  EXPECT_DIF_BADARG(dif_usbdev_address_set(nullptr, /*addr=*/1));
+  EXPECT_DIF_BADARG(dif_usbdev_address_get(nullptr, &uint8_arg));
+  EXPECT_DIF_BADARG(dif_usbdev_address_get(&usbdev_, nullptr));
+  EXPECT_DIF_BADARG(dif_usbdev_clear_data_toggle(nullptr, /*endpoint=*/1));
+  EXPECT_DIF_BADARG(dif_usbdev_status_get_frame(nullptr, &uint16_arg));
+  EXPECT_DIF_BADARG(dif_usbdev_status_get_frame(&usbdev_, nullptr));
+  EXPECT_DIF_BADARG(dif_usbdev_status_get_host_lost(nullptr, &bool_arg));
+  EXPECT_DIF_BADARG(dif_usbdev_status_get_host_lost(&usbdev_, nullptr));
+  EXPECT_DIF_BADARG(dif_usbdev_status_get_link_state(nullptr, &link_state));
+  EXPECT_DIF_BADARG(dif_usbdev_status_get_link_state(&usbdev_, nullptr));
+  EXPECT_DIF_BADARG(dif_usbdev_status_get_sense(nullptr, &bool_arg));
+  EXPECT_DIF_BADARG(dif_usbdev_status_get_sense(&usbdev_, nullptr));
+  EXPECT_DIF_BADARG(
+      dif_usbdev_status_get_available_fifo_depth(nullptr, &uint8_arg));
+  EXPECT_DIF_BADARG(
+      dif_usbdev_status_get_available_fifo_depth(&usbdev_, nullptr));
+  EXPECT_DIF_BADARG(
+      dif_usbdev_status_get_available_fifo_full(nullptr, &bool_arg));
+  EXPECT_DIF_BADARG(
+      dif_usbdev_status_get_available_fifo_full(&usbdev_, nullptr));
+  EXPECT_DIF_BADARG(dif_usbdev_status_get_rx_fifo_depth(nullptr, &uint8_arg));
+  EXPECT_DIF_BADARG(dif_usbdev_status_get_rx_fifo_depth(&usbdev_, nullptr));
+  EXPECT_DIF_BADARG(dif_usbdev_status_get_rx_fifo_empty(nullptr, &bool_arg));
+  EXPECT_DIF_BADARG(dif_usbdev_status_get_rx_fifo_empty(&usbdev_, nullptr));
+  EXPECT_DIF_BADARG(dif_usbdev_set_osc_test_mode(nullptr, kDifToggleEnabled));
+  EXPECT_DIF_BADARG(dif_usbdev_set_wake_enable(nullptr, kDifToggleEnabled));
+  EXPECT_DIF_BADARG(dif_usbdev_get_wake_status(nullptr, &wake_status));
+  EXPECT_DIF_BADARG(dif_usbdev_get_wake_status(&usbdev_, nullptr));
+  EXPECT_DIF_BADARG(dif_usbdev_resume_link_to_active(nullptr));
+  EXPECT_DIF_BADARG(dif_usbdev_get_phy_pins_status(nullptr, &phy_pins_status));
+  EXPECT_DIF_BADARG(dif_usbdev_get_phy_pins_status(&usbdev_, nullptr));
+  EXPECT_DIF_BADARG(dif_usbdev_set_phy_pins_state(nullptr, kDifToggleEnabled,
+                                                  phy_pins_drive));
+}
+
+TEST_F(UsbdevTest, PhyConfig) {
+  dif_usbdev_buffer_pool_t buffer_pool;
+  dif_usbdev_config_t phy_config = {
+      .have_differential_receiver = kDifToggleEnabled,
+      .use_tx_d_se0 = kDifToggleDisabled,
+      .single_bit_eop = kDifToggleDisabled,
+      .pin_flip = kDifToggleEnabled,
+      .clock_sync_signals = kDifToggleEnabled,
+  };
+  EXPECT_WRITE32(USBDEV_PHY_CONFIG_REG_OFFSET,
+                 {
+                     {USBDEV_PHY_CONFIG_USE_DIFF_RCVR_BIT, 1},
+                     {USBDEV_PHY_CONFIG_TX_USE_D_SE0_BIT, 0},
+                     {USBDEV_PHY_CONFIG_EOP_SINGLE_BIT_BIT, 0},
+                     {USBDEV_PHY_CONFIG_PINFLIP_BIT, 1},
+                     {USBDEV_PHY_CONFIG_USB_REF_DISABLE_BIT, 0},
+                 });
+  EXPECT_DIF_OK(dif_usbdev_configure(&usbdev_, &buffer_pool, phy_config));
+
+  dif_usbdev_phy_pins_sense_t phy_pins_status;
+  EXPECT_READ32(USBDEV_PHY_PINS_SENSE_REG_OFFSET,
+                {
+                    {USBDEV_PHY_PINS_SENSE_RX_DP_I_BIT, 1},
+                    {USBDEV_PHY_PINS_SENSE_RX_DN_I_BIT, 0},
+                    {USBDEV_PHY_PINS_SENSE_RX_D_I_BIT, 1},
+                    {USBDEV_PHY_PINS_SENSE_TX_DP_O_BIT, 0},
+                    {USBDEV_PHY_PINS_SENSE_TX_DN_O_BIT, 1},
+                    {USBDEV_PHY_PINS_SENSE_TX_D_O_BIT, 0},
+                    {USBDEV_PHY_PINS_SENSE_TX_SE0_O_BIT, 0},
+                    {USBDEV_PHY_PINS_SENSE_TX_OE_O_BIT, 1},
+                    {USBDEV_PHY_PINS_SENSE_PWR_SENSE_BIT, 1},
+                });
+  EXPECT_DIF_OK(dif_usbdev_get_phy_pins_status(&usbdev_, &phy_pins_status));
+  EXPECT_TRUE(phy_pins_status.rx_dp);
+  EXPECT_FALSE(phy_pins_status.rx_dn);
+  EXPECT_TRUE(phy_pins_status.rx_d);
+  EXPECT_FALSE(phy_pins_status.tx_dp);
+  EXPECT_TRUE(phy_pins_status.tx_dn);
+  EXPECT_FALSE(phy_pins_status.tx_d);
+  EXPECT_FALSE(phy_pins_status.tx_se0);
+  EXPECT_TRUE(phy_pins_status.output_enable);
+  EXPECT_TRUE(phy_pins_status.vbus_sense);
+
+  dif_usbdev_phy_pins_drive_t overrides = {
+      .dp = 0,
+      .dn = 1,
+      .data = 0,
+      .se0 = 1,
+      .output_enable = 1,
+      .diff_receiver_enable = 0,
+      .dp_pullup_en = 1,
+      .dn_pullup_en = 0,
+  };
+  EXPECT_WRITE32(
+      USBDEV_PHY_PINS_DRIVE_REG_OFFSET,
+      {
+          {USBDEV_PHY_PINS_DRIVE_DP_O_BIT, overrides.dp},
+          {USBDEV_PHY_PINS_DRIVE_DN_O_BIT, overrides.dn},
+          {USBDEV_PHY_PINS_DRIVE_D_O_BIT, overrides.data},
+          {USBDEV_PHY_PINS_DRIVE_SE0_O_BIT, overrides.se0},
+          {USBDEV_PHY_PINS_DRIVE_OE_O_BIT, overrides.output_enable},
+          {USBDEV_PHY_PINS_DRIVE_RX_ENABLE_O_BIT,
+           overrides.diff_receiver_enable},
+          {USBDEV_PHY_PINS_DRIVE_DP_PULLUP_EN_O_BIT, overrides.dp_pullup_en},
+          {USBDEV_PHY_PINS_DRIVE_DN_PULLUP_EN_O_BIT, overrides.dn_pullup_en},
+          {USBDEV_PHY_PINS_DRIVE_EN_BIT, 1},
+      });
+  EXPECT_DIF_OK(
+      dif_usbdev_set_phy_pins_state(&usbdev_, kDifToggleEnabled, overrides));
+
+  EXPECT_WRITE32(USBDEV_PHY_PINS_DRIVE_REG_OFFSET, 0);
+  EXPECT_DIF_OK(
+      dif_usbdev_set_phy_pins_state(&usbdev_, kDifToggleDisabled, overrides));
+
+  EXPECT_READ32(USBDEV_PHY_CONFIG_REG_OFFSET,
+                {
+                    {USBDEV_PHY_CONFIG_USE_DIFF_RCVR_BIT, 1},
+                    {USBDEV_PHY_CONFIG_TX_USE_D_SE0_BIT, 0},
+                    {USBDEV_PHY_CONFIG_EOP_SINGLE_BIT_BIT, 0},
+                    {USBDEV_PHY_CONFIG_PINFLIP_BIT, 1},
+                    {USBDEV_PHY_CONFIG_USB_REF_DISABLE_BIT, 0},
+                    {USBDEV_PHY_CONFIG_TX_OSC_TEST_MODE_BIT, 0},
+                });
+  EXPECT_WRITE32(USBDEV_PHY_CONFIG_REG_OFFSET,
+                 {
+                     {USBDEV_PHY_CONFIG_USE_DIFF_RCVR_BIT, 1},
+                     {USBDEV_PHY_CONFIG_TX_USE_D_SE0_BIT, 0},
+                     {USBDEV_PHY_CONFIG_EOP_SINGLE_BIT_BIT, 0},
+                     {USBDEV_PHY_CONFIG_PINFLIP_BIT, 1},
+                     {USBDEV_PHY_CONFIG_USB_REF_DISABLE_BIT, 0},
+                     {USBDEV_PHY_CONFIG_TX_OSC_TEST_MODE_BIT, 1},
+                 });
+  EXPECT_DIF_OK(dif_usbdev_set_osc_test_mode(&usbdev_, kDifToggleEnabled));
+
+  EXPECT_READ32(USBDEV_PHY_CONFIG_REG_OFFSET,
+                {
+                    {USBDEV_PHY_CONFIG_USE_DIFF_RCVR_BIT, 1},
+                    {USBDEV_PHY_CONFIG_TX_USE_D_SE0_BIT, 0},
+                    {USBDEV_PHY_CONFIG_EOP_SINGLE_BIT_BIT, 0},
+                    {USBDEV_PHY_CONFIG_PINFLIP_BIT, 1},
+                    {USBDEV_PHY_CONFIG_USB_REF_DISABLE_BIT, 0},
+                    {USBDEV_PHY_CONFIG_TX_OSC_TEST_MODE_BIT, 1},
+                });
+  EXPECT_WRITE32(USBDEV_PHY_CONFIG_REG_OFFSET,
+                 {
+                     {USBDEV_PHY_CONFIG_USE_DIFF_RCVR_BIT, 1},
+                     {USBDEV_PHY_CONFIG_TX_USE_D_SE0_BIT, 0},
+                     {USBDEV_PHY_CONFIG_EOP_SINGLE_BIT_BIT, 0},
+                     {USBDEV_PHY_CONFIG_PINFLIP_BIT, 1},
+                     {USBDEV_PHY_CONFIG_USB_REF_DISABLE_BIT, 0},
+                     {USBDEV_PHY_CONFIG_TX_OSC_TEST_MODE_BIT, 0},
+                 });
+  EXPECT_DIF_OK(dif_usbdev_set_osc_test_mode(&usbdev_, kDifToggleDisabled));
+}
+
+TEST_F(UsbdevTest, ConnectAndConfig) {
+  // Connect the interface.
+  EXPECT_READ32(USBDEV_USBCTRL_REG_OFFSET, 0);
+  EXPECT_WRITE32(USBDEV_USBCTRL_REG_OFFSET, {{USBDEV_USBCTRL_ENABLE_BIT, 1}});
+  EXPECT_DIF_OK(dif_usbdev_interface_enable(&usbdev_, kDifToggleEnabled));
+
+  // Disconnect the interface.
+  EXPECT_READ32(USBDEV_USBCTRL_REG_OFFSET,
+                {
+                    {USBDEV_USBCTRL_ENABLE_BIT, 1},
+                    {USBDEV_USBCTRL_DEVICE_ADDRESS_OFFSET, 127},
+                });
+  EXPECT_WRITE32(USBDEV_USBCTRL_REG_OFFSET,
+                 {
+                     {USBDEV_USBCTRL_ENABLE_BIT, 0},
+                     {USBDEV_USBCTRL_DEVICE_ADDRESS_OFFSET, 127},
+                 });
+  EXPECT_DIF_OK(dif_usbdev_interface_enable(&usbdev_, kDifToggleDisabled));
+
+  dif_usbdev_endpoint_id_t endpoint = {
+      .number = 2,
+      .direction = 1,
+  };
+  EXPECT_READ32(USBDEV_EP_IN_ENABLE_REG_OFFSET,
+                {
+                    {USBDEV_EP_IN_ENABLE_ENABLE_0_BIT, 1},
+                });
+  EXPECT_WRITE32(USBDEV_EP_IN_ENABLE_REG_OFFSET,
+                 {
+                     {USBDEV_EP_IN_ENABLE_ENABLE_0_BIT, 1},
+                     {USBDEV_EP_IN_ENABLE_ENABLE_2_BIT, 1},
+                 });
+  EXPECT_DIF_OK(
+      dif_usbdev_endpoint_enable(&usbdev_, endpoint, kDifToggleEnabled));
+
+  endpoint.number = 6;
+  endpoint.direction = 0;
+  EXPECT_READ32(USBDEV_EP_OUT_ENABLE_REG_OFFSET,
+                {
+                    {USBDEV_EP_OUT_ENABLE_ENABLE_5_BIT, 1},
+                    {USBDEV_EP_OUT_ENABLE_ENABLE_6_BIT, 1},
+                });
+  EXPECT_WRITE32(USBDEV_EP_OUT_ENABLE_REG_OFFSET,
+                 {
+                     {USBDEV_EP_OUT_ENABLE_ENABLE_5_BIT, 1},
+                 });
+  EXPECT_DIF_OK(
+      dif_usbdev_endpoint_enable(&usbdev_, endpoint, kDifToggleDisabled));
+
+  endpoint.number = 11;
+  endpoint.direction = 0;
+  EXPECT_READ32(USBDEV_OUT_ISO_REG_OFFSET, {
+                                               {USBDEV_OUT_ISO_ISO_5_BIT, 1},
+                                           });
+  EXPECT_WRITE32(USBDEV_OUT_ISO_REG_OFFSET, {
+                                                {USBDEV_OUT_ISO_ISO_5_BIT, 1},
+                                                {USBDEV_OUT_ISO_ISO_11_BIT, 1},
+                                            });
+  EXPECT_DIF_OK(
+      dif_usbdev_endpoint_iso_enable(&usbdev_, endpoint, kDifToggleEnabled));
+
+  endpoint.number = 7;
+  endpoint.direction = 1;
+  EXPECT_READ32(USBDEV_IN_ISO_REG_OFFSET, {
+                                              {USBDEV_IN_ISO_ISO_1_BIT, 1},
+                                              {USBDEV_IN_ISO_ISO_7_BIT, 1},
+                                          });
+  EXPECT_WRITE32(USBDEV_IN_ISO_REG_OFFSET, {
+                                               {USBDEV_IN_ISO_ISO_1_BIT, 1},
+                                           });
+  EXPECT_DIF_OK(
+      dif_usbdev_endpoint_iso_enable(&usbdev_, endpoint, kDifToggleDisabled));
+}
+
+TEST_F(UsbdevTest, OutEndpointConfig) {
+  EXPECT_READ32(USBDEV_RXENABLE_SETUP_REG_OFFSET,
+                {
+                    {USBDEV_RXENABLE_SETUP_SETUP_0_BIT, 1},
+                    {USBDEV_RXENABLE_SETUP_SETUP_10_BIT, 1},
+                    {USBDEV_RXENABLE_SETUP_SETUP_11_BIT, 1},
+                });
+  EXPECT_WRITE32(USBDEV_RXENABLE_SETUP_REG_OFFSET,
+                 {
+                     {USBDEV_RXENABLE_SETUP_SETUP_0_BIT, 1},
+                     {USBDEV_RXENABLE_SETUP_SETUP_9_BIT, 1},
+                     {USBDEV_RXENABLE_SETUP_SETUP_10_BIT, 1},
+                     {USBDEV_RXENABLE_SETUP_SETUP_11_BIT, 1},
+                 });
+  EXPECT_DIF_OK(dif_usbdev_endpoint_setup_enable(&usbdev_, /*endpoint=*/9,
+                                                 kDifToggleEnabled));
+
+  EXPECT_READ32(USBDEV_RXENABLE_SETUP_REG_OFFSET,
+                {
+                    {USBDEV_RXENABLE_SETUP_SETUP_8_BIT, 1},
+                    {USBDEV_RXENABLE_SETUP_SETUP_9_BIT, 1},
+                    {USBDEV_RXENABLE_SETUP_SETUP_10_BIT, 1},
+                });
+  EXPECT_WRITE32(USBDEV_RXENABLE_SETUP_REG_OFFSET,
+                 {
+                     {USBDEV_RXENABLE_SETUP_SETUP_8_BIT, 1},
+                     {USBDEV_RXENABLE_SETUP_SETUP_9_BIT, 1},
+                 });
+  EXPECT_DIF_OK(dif_usbdev_endpoint_setup_enable(&usbdev_, /*endpoint=*/10,
+                                                 kDifToggleDisabled));
+
+  EXPECT_READ32(USBDEV_RXENABLE_OUT_REG_OFFSET,
+                {
+                    {USBDEV_RXENABLE_OUT_OUT_0_BIT, 1},
+                    {USBDEV_RXENABLE_OUT_OUT_2_BIT, 1},
+                    {USBDEV_RXENABLE_OUT_OUT_9_BIT, 1},
+                });
+  EXPECT_WRITE32(USBDEV_RXENABLE_OUT_REG_OFFSET,
+                 {
+                     {USBDEV_RXENABLE_OUT_OUT_0_BIT, 1},
+                     {USBDEV_RXENABLE_OUT_OUT_2_BIT, 1},
+                     {USBDEV_RXENABLE_OUT_OUT_5_BIT, 1},
+                     {USBDEV_RXENABLE_OUT_OUT_9_BIT, 1},
+                 });
+  EXPECT_DIF_OK(dif_usbdev_endpoint_out_enable(&usbdev_, /*endpoint=*/5,
+                                               kDifToggleEnabled));
+
+  EXPECT_READ32(USBDEV_RXENABLE_OUT_REG_OFFSET,
+                {
+                    {USBDEV_RXENABLE_OUT_OUT_1_BIT, 1},
+                    {USBDEV_RXENABLE_OUT_OUT_3_BIT, 1},
+                    {USBDEV_RXENABLE_OUT_OUT_7_BIT, 1},
+                });
+  EXPECT_WRITE32(USBDEV_RXENABLE_OUT_REG_OFFSET,
+                 {
+                     {USBDEV_RXENABLE_OUT_OUT_1_BIT, 1},
+                     {USBDEV_RXENABLE_OUT_OUT_7_BIT, 1},
+                 });
+  EXPECT_DIF_OK(dif_usbdev_endpoint_out_enable(&usbdev_, /*endpoint=*/3,
+                                               kDifToggleDisabled));
+
+  EXPECT_READ32(USBDEV_SET_NAK_OUT_REG_OFFSET,
+                {
+                    {USBDEV_SET_NAK_OUT_ENABLE_10_BIT, 1},
+                });
+  EXPECT_WRITE32(USBDEV_SET_NAK_OUT_REG_OFFSET,
+                 {
+                     {USBDEV_SET_NAK_OUT_ENABLE_9_BIT, 1},
+                     {USBDEV_SET_NAK_OUT_ENABLE_10_BIT, 1},
+                 });
+  EXPECT_DIF_OK(dif_usbdev_endpoint_set_nak_out_enable(&usbdev_, /*endpoint=*/9,
+                                                       kDifToggleEnabled));
+
+  EXPECT_READ32(USBDEV_SET_NAK_OUT_REG_OFFSET,
+                {
+                    {USBDEV_SET_NAK_OUT_ENABLE_8_BIT, 1},
+                    {USBDEV_SET_NAK_OUT_ENABLE_9_BIT, 1},
+                });
+  EXPECT_WRITE32(USBDEV_SET_NAK_OUT_REG_OFFSET,
+                 {
+                     {USBDEV_SET_NAK_OUT_ENABLE_9_BIT, 1},
+                 });
+  EXPECT_DIF_OK(dif_usbdev_endpoint_set_nak_out_enable(&usbdev_, /*endpoint=*/8,
+                                                       kDifToggleDisabled));
+}
+
+TEST_F(UsbdevTest, StallConfig) {
+  dif_usbdev_endpoint_id_t endpoint = {
+      .number = 1,
+      .direction = 1,
+  };
+  EXPECT_READ32(USBDEV_IN_STALL_REG_OFFSET,
+                {
+                    {USBDEV_IN_STALL_ENDPOINT_0_BIT, 1},
+                });
+  EXPECT_WRITE32(USBDEV_IN_STALL_REG_OFFSET,
+                 {
+                     {USBDEV_IN_STALL_ENDPOINT_0_BIT, 1},
+                     {USBDEV_IN_STALL_ENDPOINT_1_BIT, 1},
+                 });
+  EXPECT_DIF_OK(
+      dif_usbdev_endpoint_stall_enable(&usbdev_, endpoint, kDifToggleEnabled));
+
+  endpoint.number = 3;
+  endpoint.direction = 0;
+  EXPECT_READ32(USBDEV_OUT_STALL_REG_OFFSET,
+                {
+                    {USBDEV_OUT_STALL_ENDPOINT_5_BIT, 1},
+                });
+  EXPECT_WRITE32(USBDEV_OUT_STALL_REG_OFFSET,
+                 {
+                     {USBDEV_OUT_STALL_ENDPOINT_3_BIT, 1},
+                     {USBDEV_OUT_STALL_ENDPOINT_5_BIT, 1},
+                 });
+  EXPECT_DIF_OK(
+      dif_usbdev_endpoint_stall_enable(&usbdev_, endpoint, kDifToggleEnabled));
+
+  bool enabled;
+  endpoint.number = 5;
+  endpoint.direction = 1;
+  EXPECT_READ32(USBDEV_IN_STALL_REG_OFFSET,
+                {
+                    {USBDEV_IN_STALL_ENDPOINT_5_BIT, 1},
+                });
+  EXPECT_DIF_OK(dif_usbdev_endpoint_stall_get(&usbdev_, endpoint, &enabled));
+  EXPECT_TRUE(enabled);
+
+  endpoint.number = 11;
+  endpoint.direction = 0;
+  EXPECT_READ32(USBDEV_OUT_STALL_REG_OFFSET,
+                {
+                    {USBDEV_OUT_STALL_ENDPOINT_5_BIT, 1},
+                    {USBDEV_OUT_STALL_ENDPOINT_9_BIT, 1},
+                    {USBDEV_OUT_STALL_ENDPOINT_10_BIT, 1},
+                });
+  EXPECT_DIF_OK(dif_usbdev_endpoint_stall_get(&usbdev_, endpoint, &enabled));
+  EXPECT_FALSE(enabled);
+}
+
+TEST_F(UsbdevTest, OutPacket) {
+  constexpr uint32_t kMaxAvBuffers = 4;
+  dif_usbdev_buffer_pool_t buffer_pool;
+  dif_usbdev_config_t phy_config = {
+      .have_differential_receiver = kDifToggleEnabled,
+      .use_tx_d_se0 = kDifToggleDisabled,
+      .single_bit_eop = kDifToggleDisabled,
+      .pin_flip = kDifToggleDisabled,
+      .clock_sync_signals = kDifToggleEnabled,
+  };
+  EXPECT_WRITE32(USBDEV_PHY_CONFIG_REG_OFFSET,
+                 {
+                     {USBDEV_PHY_CONFIG_USE_DIFF_RCVR_BIT, 1},
+                     {USBDEV_PHY_CONFIG_TX_USE_D_SE0_BIT, 0},
+                     {USBDEV_PHY_CONFIG_EOP_SINGLE_BIT_BIT, 0},
+                     {USBDEV_PHY_CONFIG_PINFLIP_BIT, 0},
+                     {USBDEV_PHY_CONFIG_USB_REF_DISABLE_BIT, 0},
+                 });
+  EXPECT_DIF_OK(dif_usbdev_configure(&usbdev_, &buffer_pool, phy_config));
+
+  // Add buffers to the AV FIFO to receive.
+  for (uint32_t i = 0; i < kMaxAvBuffers; i++) {
+    int top = buffer_pool.top;
+    EXPECT_READ32(USBDEV_USBSTAT_REG_OFFSET,
+                  {
+                      {USBDEV_USBSTAT_FRAME_OFFSET, 10},
+                      {USBDEV_USBSTAT_LINK_STATE_OFFSET,
+                       USBDEV_USBSTAT_LINK_STATE_VALUE_ACTIVE},
+                      {USBDEV_USBSTAT_SENSE_BIT, 1},
+                      {USBDEV_USBSTAT_AV_DEPTH_OFFSET, i},
+                      {USBDEV_USBSTAT_AV_FULL_BIT, 0},
+                  });
+    EXPECT_WRITE32(
+        USBDEV_AVBUFFER_REG_OFFSET,
+        {{USBDEV_AVBUFFER_BUFFER_OFFSET, buffer_pool.buffers[top - i]}});
+  }
+  EXPECT_READ32(USBDEV_USBSTAT_REG_OFFSET,
+                {
+                    {USBDEV_USBSTAT_FRAME_OFFSET, 10},
+                    {USBDEV_USBSTAT_LINK_STATE_OFFSET,
+                     USBDEV_USBSTAT_LINK_STATE_VALUE_ACTIVE},
+                    {USBDEV_USBSTAT_SENSE_BIT, 1},
+                    {USBDEV_USBSTAT_AV_DEPTH_OFFSET, kMaxAvBuffers},
+                    {USBDEV_USBSTAT_AV_FULL_BIT, 1},
+                });
+  EXPECT_DIF_OK(dif_usbdev_fill_available_fifo(&usbdev_, &buffer_pool));
+
+  // No read data available yet.
+  dif_usbdev_rx_packet_info_t rx_packet_info;
+  dif_usbdev_buffer_t buffer;
+  EXPECT_READ32(USBDEV_USBSTAT_REG_OFFSET,
+                {
+                    {USBDEV_USBSTAT_FRAME_OFFSET, 15},
+                    {USBDEV_USBSTAT_LINK_STATE_OFFSET,
+                     USBDEV_USBSTAT_LINK_STATE_VALUE_ACTIVE},
+                    {USBDEV_USBSTAT_SENSE_BIT, 1},
+                    {USBDEV_USBSTAT_AV_DEPTH_OFFSET, kMaxAvBuffers},
+                    {USBDEV_USBSTAT_AV_FULL_BIT, 1},
+                    {USBDEV_USBSTAT_RX_EMPTY_BIT, 1},
+                });
+  EXPECT_EQ(dif_usbdev_recv(&usbdev_, &rx_packet_info, &buffer),
+            kDifUnavailable);
+
+  // Receive OUT packet all at once.
+  uint32_t expected_data[4], recvd_data[4];
+  for (size_t i = 0; i < sizeof(expected_data) / sizeof(expected_data[0]);
+       i++) {
+    expected_data[i] = i * 1023;
+  }
+  EXPECT_READ32(USBDEV_USBSTAT_REG_OFFSET,
+                {
+                    {USBDEV_USBSTAT_FRAME_OFFSET, 15},
+                    {USBDEV_USBSTAT_LINK_STATE_OFFSET,
+                     USBDEV_USBSTAT_LINK_STATE_VALUE_ACTIVE},
+                    {USBDEV_USBSTAT_SENSE_BIT, 1},
+                    {USBDEV_USBSTAT_AV_DEPTH_OFFSET, kMaxAvBuffers - 1},
+                    {USBDEV_USBSTAT_AV_FULL_BIT, 0},
+                    {USBDEV_USBSTAT_RX_EMPTY_BIT, 0},
+                });
+  EXPECT_READ32(USBDEV_RXFIFO_REG_OFFSET,
+                {
+                    {USBDEV_RXFIFO_EP_OFFSET, 1},
+                    {USBDEV_RXFIFO_SETUP_BIT, 0},
+                    {USBDEV_RXFIFO_SIZE_OFFSET, sizeof(expected_data)},
+                    {USBDEV_RXFIFO_BUFFER_OFFSET, 0},
+                });
+  EXPECT_DIF_OK(dif_usbdev_recv(&usbdev_, &rx_packet_info, &buffer));
+  EXPECT_EQ(rx_packet_info.endpoint, 1);
+  EXPECT_EQ(rx_packet_info.length, sizeof(expected_data));
+  EXPECT_FALSE(rx_packet_info.is_setup);
+  EXPECT_EQ(buffer.id, 0);
+  EXPECT_EQ(buffer.offset, 0);
+  EXPECT_EQ(buffer.remaining_bytes, sizeof(expected_data));
+  EXPECT_EQ(buffer.type, kDifUsbdevBufferTypeRead);
+
+  size_t bytes_written;
+  for (size_t i = 0; i < sizeof(expected_data) / sizeof(expected_data[0]);
+       i++) {
+    EXPECT_READ32(USBDEV_BUFFER_REG_OFFSET + buffer.id * 64 + 4 * i,
+                  expected_data[i]);
+  }
+  EXPECT_DIF_OK(dif_usbdev_buffer_read(&usbdev_, &buffer_pool, &buffer,
+                                       reinterpret_cast<uint8_t *>(recvd_data),
+                                       sizeof(recvd_data), &bytes_written));
+  EXPECT_EQ(bytes_written, sizeof(recvd_data));
+  EXPECT_EQ(memcmp(expected_data, recvd_data, sizeof(expected_data)), 0);
+
+  // One more received packet to test other offsets.
+  EXPECT_READ32(USBDEV_USBSTAT_REG_OFFSET,
+                {
+                    {USBDEV_USBSTAT_FRAME_OFFSET, 25},
+                    {USBDEV_USBSTAT_LINK_STATE_OFFSET,
+                     USBDEV_USBSTAT_LINK_STATE_VALUE_ACTIVE},
+                    {USBDEV_USBSTAT_SENSE_BIT, 1},
+                    {USBDEV_USBSTAT_AV_DEPTH_OFFSET, kMaxAvBuffers - 2},
+                    {USBDEV_USBSTAT_AV_FULL_BIT, 1},
+                    {USBDEV_USBSTAT_RX_EMPTY_BIT, 0},
+                });
+  EXPECT_READ32(USBDEV_RXFIFO_REG_OFFSET,
+                {
+                    {USBDEV_RXFIFO_EP_OFFSET, 0},
+                    {USBDEV_RXFIFO_SETUP_BIT, 1},
+                    {USBDEV_RXFIFO_SIZE_OFFSET, sizeof(expected_data) - 1},
+                    {USBDEV_RXFIFO_BUFFER_OFFSET, 1},
+                });
+  EXPECT_DIF_OK(dif_usbdev_recv(&usbdev_, &rx_packet_info, &buffer));
+  EXPECT_EQ(rx_packet_info.endpoint, 0);
+  EXPECT_EQ(rx_packet_info.length, sizeof(expected_data) - 1);
+  EXPECT_TRUE(rx_packet_info.is_setup);
+  EXPECT_EQ(buffer.id, 1);
+  EXPECT_EQ(buffer.offset, 0);
+  EXPECT_EQ(buffer.remaining_bytes, sizeof(expected_data) - 1);
+  EXPECT_EQ(buffer.type, kDifUsbdevBufferTypeRead);
+
+  memset(recvd_data, 0, sizeof(recvd_data));
+  for (size_t i = 0; i < sizeof(expected_data) / sizeof(expected_data[0]);
+       i++) {
+    EXPECT_READ32(USBDEV_BUFFER_REG_OFFSET + buffer.id * 64 + 4 * i,
+                  expected_data[i]);
+  }
+  EXPECT_DIF_OK(dif_usbdev_buffer_read(&usbdev_, &buffer_pool, &buffer,
+                                       reinterpret_cast<uint8_t *>(recvd_data),
+                                       4, &bytes_written));
+  EXPECT_EQ(bytes_written, 4);
+  EXPECT_DIF_OK(dif_usbdev_buffer_read(
+      &usbdev_, &buffer_pool, &buffer,
+      reinterpret_cast<uint8_t *>(recvd_data) + bytes_written,
+      sizeof(recvd_data) - bytes_written, &bytes_written));
+  EXPECT_EQ(bytes_written, sizeof(recvd_data) - 4 - 1);
+  EXPECT_EQ(memcmp(expected_data, recvd_data, sizeof(expected_data)), 0);
+}
+
+TEST_F(UsbdevTest, InPacket) {
+  dif_usbdev_buffer_pool_t buffer_pool;
+  dif_usbdev_config_t phy_config = {
+      .have_differential_receiver = kDifToggleEnabled,
+      .use_tx_d_se0 = kDifToggleDisabled,
+      .single_bit_eop = kDifToggleDisabled,
+      .pin_flip = kDifToggleDisabled,
+      .clock_sync_signals = kDifToggleEnabled,
+  };
+  EXPECT_WRITE32(USBDEV_PHY_CONFIG_REG_OFFSET,
+                 {
+                     {USBDEV_PHY_CONFIG_USE_DIFF_RCVR_BIT, 1},
+                     {USBDEV_PHY_CONFIG_TX_USE_D_SE0_BIT, 0},
+                     {USBDEV_PHY_CONFIG_EOP_SINGLE_BIT_BIT, 0},
+                     {USBDEV_PHY_CONFIG_PINFLIP_BIT, 0},
+                     {USBDEV_PHY_CONFIG_USB_REF_DISABLE_BIT, 0},
+                 });
+  EXPECT_DIF_OK(dif_usbdev_configure(&usbdev_, &buffer_pool, phy_config));
+
+  dif_usbdev_buffer_t buffer;
+  EXPECT_DIF_OK(dif_usbdev_buffer_request(&usbdev_, &buffer_pool, &buffer));
+  EXPECT_EQ(buffer.type, kDifUsbdevBufferTypeWrite);
+
+  uint32_t data[16];
+  uint8_t *bytes = reinterpret_cast<uint8_t *>(data);
+  size_t bytes_written;
+  EXPECT_DIF_OK(dif_usbdev_buffer_return(&usbdev_, &buffer_pool, &buffer));
+  EXPECT_EQ(buffer.type, kDifUsbdevBufferTypeStale);
+  // Can't return a stale buffer.
+  EXPECT_DIF_BADARG(dif_usbdev_buffer_return(&usbdev_, &buffer_pool, &buffer));
+  // Can't submit a stale buffer.
+  EXPECT_DIF_BADARG(dif_usbdev_buffer_write(&usbdev_, &buffer, bytes,
+                                            sizeof(data), &bytes_written));
+
+  // Request the buffer.
+  EXPECT_DIF_OK(dif_usbdev_buffer_request(&usbdev_, &buffer_pool, &buffer));
+  for (size_t i = 0; i < sizeof(data); i++) {
+    bytes[i] = i;
+    if (i % 4 == 3) {
+      EXPECT_WRITE32(USBDEV_BUFFER_REG_OFFSET + buffer.id * 64 + i - 3,
+                     data[i / 4]);
+    }
+  }
+  EXPECT_DIF_OK(dif_usbdev_buffer_write(&usbdev_, &buffer, bytes, sizeof(data),
+                                        &bytes_written));
+  EXPECT_EQ(bytes_written, sizeof(data));
+
+  // Queue up the buffer for transmission.
+  EXPECT_WRITE32(USBDEV_CONFIGIN_5_REG_OFFSET,
+                 {
+                     {USBDEV_CONFIGIN_5_BUFFER_5_OFFSET, buffer.id},
+                     {USBDEV_CONFIGIN_5_SIZE_5_OFFSET, bytes_written},
+                 });
+  EXPECT_WRITE32(USBDEV_CONFIGIN_5_REG_OFFSET,
+                 {
+                     {USBDEV_CONFIGIN_5_BUFFER_5_OFFSET, buffer.id},
+                     {USBDEV_CONFIGIN_5_SIZE_5_OFFSET, bytes_written},
+                     {USBDEV_CONFIGIN_5_RDY_5_BIT, 1},
+                 });
+  EXPECT_DIF_OK(dif_usbdev_send(&usbdev_, /*endpoint=*/5, &buffer));
+
+  // Get TX status for a buffer with a transmission that had to be canceled. The
+  // buffer is returned to the free pool.
+  dif_usbdev_tx_status_t tx_status;
+  EXPECT_READ32(USBDEV_CONFIGIN_0_REG_OFFSET,
+                {
+                    {USBDEV_CONFIGIN_0_BUFFER_0_OFFSET, buffer.id},
+                    {USBDEV_CONFIGIN_0_SIZE_0_OFFSET, sizeof(data)},
+                    {USBDEV_CONFIGIN_0_RDY_0_BIT, 0},
+                    {USBDEV_CONFIGIN_0_PEND_0_BIT, 1},
+                });
+  EXPECT_READ32(USBDEV_IN_SENT_REG_OFFSET, {
+                                               {USBDEV_IN_SENT_SENT_0_BIT, 0},
+                                           });
+  EXPECT_DIF_OK(dif_usbdev_get_tx_status(&usbdev_, /*endpoint=*/0, &tx_status));
+  EXPECT_EQ(tx_status, kDifUsbdevTxStatusCancelled);
+  EXPECT_READ32(USBDEV_CONFIGIN_0_REG_OFFSET,
+                {
+                    {USBDEV_CONFIGIN_0_BUFFER_0_OFFSET, buffer.id},
+                    {USBDEV_CONFIGIN_0_SIZE_0_OFFSET, sizeof(data)},
+                    {USBDEV_CONFIGIN_0_RDY_0_BIT, 0},
+                    {USBDEV_CONFIGIN_0_PEND_0_BIT, 1},
+                });
+  EXPECT_WRITE32(USBDEV_CONFIGIN_0_REG_OFFSET,
+                 {{USBDEV_CONFIGIN_0_PEND_0_BIT, 1}});
+  EXPECT_WRITE32(USBDEV_IN_SENT_REG_OFFSET, {{USBDEV_IN_SENT_SENT_0_BIT, 1}});
+  EXPECT_DIF_OK(
+      dif_usbdev_clear_tx_status(&usbdev_, &buffer_pool, /*endpoint=*/0));
+
+  // Request a new buffer.
+  EXPECT_DIF_OK(dif_usbdev_buffer_request(&usbdev_, &buffer_pool, &buffer));
+  for (size_t i = 0; i < sizeof(data); i++) {
+    bytes[i] = i;
+    if (i % 4 == 3) {
+      EXPECT_WRITE32(USBDEV_BUFFER_REG_OFFSET + buffer.id * 64 + i - 3,
+                     data[i / 4]);
+    }
+  }
+  EXPECT_DIF_OK(dif_usbdev_buffer_write(&usbdev_, &buffer, bytes, sizeof(data),
+                                        &bytes_written));
+
+  // Queue the buffer for transmission.
+  EXPECT_WRITE32(USBDEV_CONFIGIN_4_REG_OFFSET,
+                 {
+                     {USBDEV_CONFIGIN_4_BUFFER_4_OFFSET, buffer.id},
+                     {USBDEV_CONFIGIN_4_SIZE_4_OFFSET, sizeof(data)},
+                 });
+  EXPECT_WRITE32(USBDEV_CONFIGIN_4_REG_OFFSET,
+                 {
+                     {USBDEV_CONFIGIN_4_BUFFER_4_OFFSET, buffer.id},
+                     {USBDEV_CONFIGIN_4_SIZE_4_OFFSET, sizeof(data)},
+                     {USBDEV_CONFIGIN_4_RDY_4_BIT, 1},
+                 });
+  EXPECT_DIF_OK(dif_usbdev_send(&usbdev_, /*endpoint=*/4, &buffer));
+
+  // Get status of an endpoint without a buffer queued for transmission.
+  EXPECT_READ32(USBDEV_CONFIGIN_7_REG_OFFSET,
+                {
+                    {USBDEV_CONFIGIN_7_BUFFER_7_OFFSET, buffer.id},
+                    {USBDEV_CONFIGIN_7_SIZE_7_OFFSET, sizeof(data)},
+                    {USBDEV_CONFIGIN_7_RDY_7_BIT, 0},
+                });
+  EXPECT_READ32(USBDEV_IN_SENT_REG_OFFSET, {
+                                               {USBDEV_IN_SENT_SENT_7_BIT, 0},
+                                           });
+  EXPECT_DIF_OK(dif_usbdev_get_tx_status(&usbdev_, /*endpoint=*/7, &tx_status));
+  EXPECT_EQ(tx_status, kDifUsbdevTxStatusNoPacket);
+
+  // Get TX status for a queued, but not sent buffer.
+  EXPECT_READ32(USBDEV_CONFIGIN_8_REG_OFFSET,
+                {
+                    {USBDEV_CONFIGIN_8_BUFFER_8_OFFSET, buffer.id},
+                    {USBDEV_CONFIGIN_8_SIZE_8_OFFSET, sizeof(data)},
+                    {USBDEV_CONFIGIN_8_RDY_8_BIT, 1},
+                });
+  EXPECT_DIF_OK(dif_usbdev_get_tx_status(&usbdev_, /*endpoint=*/8, &tx_status));
+  EXPECT_EQ(tx_status, kDifUsbdevTxStatusPending);
+
+  // Buffer was transmitted successfully.
+  uint16_t endpoints_done;
+  EXPECT_READ32(USBDEV_IN_SENT_REG_OFFSET, {
+                                               {USBDEV_IN_SENT_SENT_3_BIT, 1},
+                                               {USBDEV_IN_SENT_SENT_5_BIT, 1},
+                                           });
+  EXPECT_DIF_OK(dif_usbdev_get_tx_sent(&usbdev_, &endpoints_done));
+  EXPECT_EQ(endpoints_done, (1u << 3) | (1u << 5));
+
+  EXPECT_READ32(USBDEV_CONFIGIN_5_REG_OFFSET,
+                {
+                    {USBDEV_CONFIGIN_5_BUFFER_5_OFFSET, buffer.id},
+                    {USBDEV_CONFIGIN_5_SIZE_5_OFFSET, sizeof(data)},
+                    {USBDEV_CONFIGIN_5_RDY_5_BIT, 0},
+                });
+  EXPECT_READ32(USBDEV_IN_SENT_REG_OFFSET, {
+                                               {USBDEV_IN_SENT_SENT_3_BIT, 1},
+                                               {USBDEV_IN_SENT_SENT_5_BIT, 1},
+                                           });
+  EXPECT_DIF_OK(dif_usbdev_get_tx_status(&usbdev_, /*endpoint=*/5, &tx_status));
+  EXPECT_EQ(tx_status, kDifUsbdevTxStatusSent);
+  EXPECT_EQ(buffer.type, kDifUsbdevBufferTypeStale);
+
+  EXPECT_READ32(USBDEV_CONFIGIN_5_REG_OFFSET,
+                {
+                    {USBDEV_CONFIGIN_5_BUFFER_5_OFFSET, buffer.id},
+                    {USBDEV_CONFIGIN_5_SIZE_5_OFFSET, sizeof(data)},
+                    {USBDEV_CONFIGIN_5_RDY_5_BIT, 0},
+                });
+  EXPECT_WRITE32(USBDEV_CONFIGIN_5_REG_OFFSET,
+                 {{USBDEV_CONFIGIN_5_PEND_5_BIT, 1}});
+  EXPECT_WRITE32(USBDEV_IN_SENT_REG_OFFSET, {{USBDEV_IN_SENT_SENT_5_BIT, 1}});
+  EXPECT_DIF_OK(
+      dif_usbdev_clear_tx_status(&usbdev_, &buffer_pool, /*endpoint=*/5));
+}
+
+TEST_F(UsbdevTest, DeviceAddresses) {
+  uint8_t address = 101;
+  EXPECT_READ32(USBDEV_USBCTRL_REG_OFFSET,
+                {
+                    {USBDEV_USBCTRL_ENABLE_BIT, 1},
+                    {USBDEV_USBCTRL_DEVICE_ADDRESS_OFFSET, 0},
+                });
+  EXPECT_WRITE32(USBDEV_USBCTRL_REG_OFFSET,
+                 {
+                     {USBDEV_USBCTRL_ENABLE_BIT, 1},
+                     {USBDEV_USBCTRL_DEVICE_ADDRESS_OFFSET, address},
+                 });
+  EXPECT_DIF_OK(dif_usbdev_address_set(&usbdev_, address));
+
+  EXPECT_READ32(USBDEV_USBCTRL_REG_OFFSET,
+                {
+                    {USBDEV_USBCTRL_ENABLE_BIT, 1},
+                    {USBDEV_USBCTRL_DEVICE_ADDRESS_OFFSET, 58},
+                });
+  EXPECT_DIF_OK(dif_usbdev_address_get(&usbdev_, &address));
+  EXPECT_EQ(address, 58);
+}
+
+TEST_F(UsbdevTest, Status) {
+  EXPECT_WRITE32(USBDEV_DATA_TOGGLE_CLEAR_REG_OFFSET,
+                 {{USBDEV_DATA_TOGGLE_CLEAR_CLEAR_3_BIT, 1}});
+  EXPECT_DIF_OK(dif_usbdev_clear_data_toggle(&usbdev_, /*endpoint=*/3));
+  EXPECT_WRITE32(USBDEV_DATA_TOGGLE_CLEAR_REG_OFFSET,
+                 {{USBDEV_DATA_TOGGLE_CLEAR_CLEAR_9_BIT, 1}});
+  EXPECT_DIF_OK(dif_usbdev_clear_data_toggle(&usbdev_, /*endpoint=*/9));
+
+  uint16_t frame;
+  EXPECT_READ32(USBDEV_USBSTAT_REG_OFFSET,
+                {
+                    {USBDEV_USBSTAT_FRAME_OFFSET, 92},
+                    {USBDEV_USBSTAT_SENSE_BIT, 1},
+                    {USBDEV_USBSTAT_LINK_STATE_OFFSET,
+                     USBDEV_USBSTAT_LINK_STATE_VALUE_ACTIVE},
+                    {USBDEV_USBSTAT_AV_DEPTH_OFFSET, 2},
+                });
+  EXPECT_DIF_OK(dif_usbdev_status_get_frame(&usbdev_, &frame));
+  EXPECT_EQ(frame, 92);
+
+  bool host_lost;
+  EXPECT_READ32(USBDEV_USBSTAT_REG_OFFSET,
+                {
+                    {USBDEV_USBSTAT_FRAME_OFFSET, 18},
+                    {USBDEV_USBSTAT_SENSE_BIT, 1},
+                    {USBDEV_USBSTAT_LINK_STATE_OFFSET,
+                     USBDEV_USBSTAT_LINK_STATE_VALUE_ACTIVE_NOSOF},
+                    {USBDEV_USBSTAT_AV_DEPTH_OFFSET, 2},
+                    {USBDEV_USBSTAT_HOST_LOST_BIT, 1},
+                });
+  EXPECT_DIF_OK(dif_usbdev_status_get_host_lost(&usbdev_, &host_lost));
+  EXPECT_TRUE(host_lost);
+  EXPECT_READ32(USBDEV_USBSTAT_REG_OFFSET,
+                {
+                    {USBDEV_USBSTAT_FRAME_OFFSET, 18},
+                    {USBDEV_USBSTAT_LINK_STATE_OFFSET,
+                     USBDEV_USBSTAT_LINK_STATE_VALUE_ACTIVE_NOSOF},
+                    {USBDEV_USBSTAT_SENSE_BIT, 1},
+                    {USBDEV_USBSTAT_AV_DEPTH_OFFSET, 2},
+                    {USBDEV_USBSTAT_HOST_LOST_BIT, 0},
+                });
+  EXPECT_DIF_OK(dif_usbdev_status_get_host_lost(&usbdev_, &host_lost));
+  EXPECT_FALSE(host_lost);
+
+  bool vbus_sense;
+  EXPECT_READ32(USBDEV_USBSTAT_REG_OFFSET,
+                {
+                    {USBDEV_USBSTAT_FRAME_OFFSET, 31},
+                    {USBDEV_USBSTAT_LINK_STATE_OFFSET,
+                     USBDEV_USBSTAT_LINK_STATE_VALUE_ACTIVE_NOSOF},
+                    {USBDEV_USBSTAT_SENSE_BIT, 1},
+                });
+  EXPECT_DIF_OK(dif_usbdev_status_get_sense(&usbdev_, &vbus_sense));
+  EXPECT_TRUE(vbus_sense);
+
+  EXPECT_READ32(USBDEV_USBSTAT_REG_OFFSET,
+                {
+                    {USBDEV_USBSTAT_FRAME_OFFSET, 31},
+                    {USBDEV_USBSTAT_LINK_STATE_OFFSET,
+                     USBDEV_USBSTAT_LINK_STATE_VALUE_DISCONNECTED},
+                    {USBDEV_USBSTAT_SENSE_BIT, 0},
+                });
+  EXPECT_DIF_OK(dif_usbdev_status_get_sense(&usbdev_, &vbus_sense));
+  EXPECT_FALSE(vbus_sense);
+
+  uint8_t av_fifo_depth;
+  EXPECT_READ32(USBDEV_USBSTAT_REG_OFFSET,
+                {
+                    {USBDEV_USBSTAT_FRAME_OFFSET, 11},
+                    {USBDEV_USBSTAT_LINK_STATE_OFFSET,
+                     USBDEV_USBSTAT_LINK_STATE_VALUE_ACTIVE},
+                    {USBDEV_USBSTAT_SENSE_BIT, 1},
+                    {USBDEV_USBSTAT_AV_DEPTH_OFFSET, 3},
+                    {USBDEV_USBSTAT_RX_EMPTY_BIT, 1},
+                });
+  EXPECT_DIF_OK(
+      dif_usbdev_status_get_available_fifo_depth(&usbdev_, &av_fifo_depth));
+  EXPECT_EQ(av_fifo_depth, 3);
+
+  bool av_fifo_full;
+  EXPECT_READ32(USBDEV_USBSTAT_REG_OFFSET,
+                {
+                    {USBDEV_USBSTAT_FRAME_OFFSET, 12},
+                    {USBDEV_USBSTAT_LINK_STATE_OFFSET,
+                     USBDEV_USBSTAT_LINK_STATE_VALUE_ACTIVE},
+                    {USBDEV_USBSTAT_SENSE_BIT, 1},
+                    {USBDEV_USBSTAT_AV_DEPTH_OFFSET, 4},
+                    {USBDEV_USBSTAT_AV_FULL_BIT, 1},
+                    {USBDEV_USBSTAT_RX_EMPTY_BIT, 1},
+                });
+  EXPECT_DIF_OK(
+      dif_usbdev_status_get_available_fifo_full(&usbdev_, &av_fifo_full));
+  EXPECT_TRUE(av_fifo_full);
+
+  uint8_t rx_fifo_depth;
+  EXPECT_READ32(USBDEV_USBSTAT_REG_OFFSET,
+                {
+                    {USBDEV_USBSTAT_FRAME_OFFSET, 12},
+                    {USBDEV_USBSTAT_LINK_STATE_OFFSET,
+                     USBDEV_USBSTAT_LINK_STATE_VALUE_ACTIVE},
+                    {USBDEV_USBSTAT_SENSE_BIT, 1},
+                    {USBDEV_USBSTAT_AV_DEPTH_OFFSET, 4},
+                    {USBDEV_USBSTAT_AV_FULL_BIT, 1},
+                    {USBDEV_USBSTAT_RX_EMPTY_BIT, 0},
+                    {USBDEV_USBSTAT_RX_DEPTH_OFFSET, 2},
+                });
+  EXPECT_DIF_OK(dif_usbdev_status_get_rx_fifo_depth(&usbdev_, &rx_fifo_depth));
+  EXPECT_EQ(rx_fifo_depth, 2);
+
+  bool rx_fifo_empty;
+  EXPECT_READ32(USBDEV_USBSTAT_REG_OFFSET,
+                {
+                    {USBDEV_USBSTAT_FRAME_OFFSET, 12},
+                    {USBDEV_USBSTAT_LINK_STATE_OFFSET,
+                     USBDEV_USBSTAT_LINK_STATE_VALUE_ACTIVE},
+                    {USBDEV_USBSTAT_SENSE_BIT, 1},
+                    {USBDEV_USBSTAT_AV_DEPTH_OFFSET, 4},
+                    {USBDEV_USBSTAT_AV_FULL_BIT, 1},
+                    {USBDEV_USBSTAT_RX_EMPTY_BIT, 1},
+                });
+  EXPECT_DIF_OK(dif_usbdev_status_get_rx_fifo_empty(&usbdev_, &rx_fifo_empty));
+  EXPECT_TRUE(rx_fifo_empty);
+}
+
+TEST_F(UsbdevTest, LinkState) {
+  dif_usbdev_link_state_t link_state;
+  EXPECT_READ32(USBDEV_USBSTAT_REG_OFFSET,
+                {
+                    {USBDEV_USBSTAT_FRAME_OFFSET, 27},
+                    {USBDEV_USBSTAT_SENSE_BIT, 1},
+                    {USBDEV_USBSTAT_LINK_STATE_OFFSET,
+                     USBDEV_USBSTAT_LINK_STATE_VALUE_ACTIVE},
+                    {USBDEV_USBSTAT_AV_DEPTH_OFFSET, 2},
+                });
+  EXPECT_DIF_OK(dif_usbdev_status_get_link_state(&usbdev_, &link_state));
+  EXPECT_EQ(link_state, kDifUsbdevLinkStateActive);
+
+  EXPECT_READ32(USBDEV_USBSTAT_REG_OFFSET,
+                {
+                    {USBDEV_USBSTAT_LINK_STATE_OFFSET,
+                     USBDEV_USBSTAT_LINK_STATE_VALUE_DISCONNECTED},
+                });
+  EXPECT_DIF_OK(dif_usbdev_status_get_link_state(&usbdev_, &link_state));
+  EXPECT_EQ(link_state, kDifUsbdevLinkStateDisconnected);
+
+  EXPECT_READ32(USBDEV_USBSTAT_REG_OFFSET,
+                {
+                    {USBDEV_USBSTAT_SENSE_BIT, 1},
+                    {USBDEV_USBSTAT_LINK_STATE_OFFSET,
+                     USBDEV_USBSTAT_LINK_STATE_VALUE_POWERED},
+                });
+  EXPECT_DIF_OK(dif_usbdev_status_get_link_state(&usbdev_, &link_state));
+  EXPECT_EQ(link_state, kDifUsbdevLinkStatePowered);
+
+  EXPECT_READ32(USBDEV_USBSTAT_REG_OFFSET,
+                {
+                    {USBDEV_USBSTAT_SENSE_BIT, 1},
+                    {USBDEV_USBSTAT_LINK_STATE_OFFSET,
+                     USBDEV_USBSTAT_LINK_STATE_VALUE_POWERED_SUSPENDED},
+                });
+  EXPECT_DIF_OK(dif_usbdev_status_get_link_state(&usbdev_, &link_state));
+  EXPECT_EQ(link_state, kDifUsbdevLinkStatePoweredSuspended);
+
+  EXPECT_READ32(USBDEV_USBSTAT_REG_OFFSET,
+                {
+                    {USBDEV_USBSTAT_SENSE_BIT, 1},
+                    {USBDEV_USBSTAT_LINK_STATE_OFFSET,
+                     USBDEV_USBSTAT_LINK_STATE_VALUE_SUSPENDED},
+                });
+  EXPECT_DIF_OK(dif_usbdev_status_get_link_state(&usbdev_, &link_state));
+  EXPECT_EQ(link_state, kDifUsbdevLinkStateSuspended);
+
+  EXPECT_READ32(USBDEV_USBSTAT_REG_OFFSET,
+                {
+                    {USBDEV_USBSTAT_SENSE_BIT, 1},
+                    {USBDEV_USBSTAT_LINK_STATE_OFFSET,
+                     USBDEV_USBSTAT_LINK_STATE_VALUE_ACTIVE_NOSOF},
+                });
+  EXPECT_DIF_OK(dif_usbdev_status_get_link_state(&usbdev_, &link_state));
+  EXPECT_EQ(link_state, kDifUsbdevLinkStateActiveNoSof);
+
+  EXPECT_READ32(USBDEV_USBSTAT_REG_OFFSET,
+                {
+                    {USBDEV_USBSTAT_SENSE_BIT, 1},
+                    {USBDEV_USBSTAT_LINK_STATE_OFFSET,
+                     USBDEV_USBSTAT_LINK_STATE_VALUE_RESUMING},
+                });
+  EXPECT_DIF_OK(dif_usbdev_status_get_link_state(&usbdev_, &link_state));
+  EXPECT_EQ(link_state, kDifUsbdevLinkStateResuming);
+}
+
+TEST_F(UsbdevTest, WakeFromSleep) {
+  EXPECT_WRITE32(USBDEV_WAKE_CONTROL_REG_OFFSET,
+                 {{USBDEV_WAKE_CONTROL_SUSPEND_REQ_BIT, 1}});
+  EXPECT_DIF_OK(dif_usbdev_set_wake_enable(&usbdev_, kDifToggleEnabled));
+
+  dif_usbdev_wake_status_t wake_status;
+  EXPECT_READ32(USBDEV_WAKE_EVENTS_REG_OFFSET,
+                {
+                    {USBDEV_WAKE_EVENTS_MODULE_ACTIVE_BIT, 1},
+                    {USBDEV_WAKE_EVENTS_DISCONNECTED_BIT, 0},
+                    {USBDEV_WAKE_EVENTS_BUS_RESET_BIT, 1},
+                });
+  EXPECT_DIF_OK(dif_usbdev_get_wake_status(&usbdev_, &wake_status));
+  EXPECT_TRUE(wake_status.active);
+  EXPECT_FALSE(wake_status.disconnected);
+  EXPECT_TRUE(wake_status.bus_reset);
+
+  EXPECT_READ32(USBDEV_USBCTRL_REG_OFFSET,
+                {
+                    {USBDEV_USBCTRL_ENABLE_BIT, 1},
+                    {USBDEV_USBCTRL_DEVICE_ADDRESS_OFFSET, 88},
+                    {USBDEV_USBCTRL_RESUME_LINK_ACTIVE_BIT, 0},
+                });
+  EXPECT_WRITE32(USBDEV_USBCTRL_REG_OFFSET,
+                 {
+                     {USBDEV_USBCTRL_ENABLE_BIT, 1},
+                     {USBDEV_USBCTRL_DEVICE_ADDRESS_OFFSET, 88},
+                     {USBDEV_USBCTRL_RESUME_LINK_ACTIVE_BIT, 1},
+                 });
+  EXPECT_DIF_OK(dif_usbdev_resume_link_to_active(&usbdev_));
+
+  EXPECT_WRITE32(USBDEV_WAKE_CONTROL_REG_OFFSET,
+                 {{USBDEV_WAKE_CONTROL_WAKE_ACK_BIT, 1}});
+  EXPECT_DIF_OK(dif_usbdev_set_wake_enable(&usbdev_, kDifToggleDisabled));
+}
+
+}  // namespace
+}  // namespace dif_usbdev_unittest
diff --git a/sw/device/lib/testing/usb_testutils.c b/sw/device/lib/testing/usb_testutils.c
new file mode 100644
index 0000000..bacc95b
--- /dev/null
+++ b/sw/device/lib/testing/usb_testutils.c
@@ -0,0 +1,443 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+#include "sw/device/lib/testing/usb_testutils.h"
+
+#include "sw/device/lib/dif/dif_usbdev.h"
+#include "sw/device/lib/testing/test_framework/check.h"
+
+#include "hw/top_earlgrey/sw/autogen/top_earlgrey.h"
+
+#define USBDEV_BASE_ADDR TOP_EARLGREY_USBDEV_BASE_ADDR
+
+static dif_usbdev_t usbdev;
+static dif_usbdev_buffer_pool_t buffer_pool;
+
+// Internal function to create the packet that will form the next part of a
+// larger buffer transfer
+static bool usb_testutils_part_prepare(usb_testutils_ctx_t *ctx,
+                                       usb_testutils_transfer_t *transfer,
+                                       dif_usbdev_buffer_t *next_part,
+                                       bool *last) {
+  CHECK(ctx && transfer && last);
+
+  // Allocate and fill a packet buffer
+  dif_result_t result =
+      dif_usbdev_buffer_request(ctx->dev, ctx->buffer_pool, next_part);
+  if (result != kDifOk) {
+    return false;
+  }
+
+  // Determine the maximum bytes/packet
+  unsigned max_packet = USBDEV_MAX_PACKET_SIZE;
+  if (transfer->flags & kUsbTestutilsXfrMaxPacketSupplied) {
+    max_packet = (unsigned)(transfer->flags & kUsbTestutilsXfrMaxPacketMask);
+  }
+
+  // How much are we sending this time?
+  unsigned part_len = transfer->length - transfer->offset;
+  if (part_len > max_packet) {
+    part_len = max_packet;
+  }
+  size_t bytes_written = 0U;
+  if (part_len) {
+    CHECK_DIF_OK(dif_usbdev_buffer_write(ctx->dev, next_part,
+                                         &transfer->buffer[transfer->offset],
+                                         part_len, &bytes_written));
+  }
+  //  Is this the last packet?
+  uint32_t next_offset = transfer->offset + bytes_written;
+  *last = true;
+  if (bytes_written == max_packet) {
+    if (next_offset < transfer->length ||
+        (transfer->flags & kUsbTestutilsXfrEmployZLP)) {
+      *last = false;
+    }
+  } else {
+    CHECK(bytes_written < max_packet);
+  }
+
+  transfer->offset = next_offset;
+  return true;
+}
+
+// Internal function to perform the next part of a larger buffer transfer
+static bool usb_testutils_transfer_next_part(
+    usb_testutils_ctx_t *ctx, uint8_t ep, usb_testutils_transfer_t *transfer) {
+  // Do we need to prepare a packet?
+  if (!transfer->next_valid &&
+      !usb_testutils_part_prepare(ctx, transfer, &transfer->next_part,
+                                  &transfer->last)) {
+    return false;
+  }
+
+  // Send the existing prepared packet
+  CHECK_DIF_OK(dif_usbdev_send(ctx->dev, ep, &transfer->next_part));
+  transfer->next_valid = false;
+
+  // If we're double-buffering, request and fill another buffer immediately;
+  // we'll then be able to supply it much more promptly later...
+  if ((transfer->flags & kUsbTestutilsXfrDoubleBuffered) && !transfer->last) {
+    transfer->next_valid = usb_testutils_part_prepare(
+        ctx, transfer, &transfer->next_part, &transfer->last);
+  }
+
+  return true;
+}
+
+void usb_testutils_poll(usb_testutils_ctx_t *ctx) {
+  uint32_t istate;
+
+  // Collect a set of interrupts
+  CHECK_DIF_OK(dif_usbdev_irq_get_state(ctx->dev, &istate));
+
+  if (!istate) {
+    return;
+  }
+
+  // Process IN completions first so we get the fact that send completed
+  // before processing a response to that transmission
+  // This is also important for device IN performance
+  if (istate & (1u << kDifUsbdevIrqPktSent)) {
+    uint16_t sentep;
+    CHECK_DIF_OK(dif_usbdev_get_tx_sent(ctx->dev, &sentep));
+    TRC_C('a' + sentep);
+    unsigned ep = 0u;
+    while (sentep && ep < USBDEV_NUM_ENDPOINTS) {
+      if (sentep & (1u << ep)) {
+        // Free up the buffer and optionally callback
+        CHECK_DIF_OK(
+            dif_usbdev_clear_tx_status(ctx->dev, ctx->buffer_pool, ep));
+
+        // If we have a larger transfer in progress, continue with that
+        usb_testutils_transfer_t *transfer = &ctx->in[ep].transfer;
+        usb_testutils_xfr_result_t res = kUsbTestutilsXfrResultOk;
+        bool done = true;
+        if (transfer->buffer) {
+          if (transfer->next_valid || !transfer->last) {
+            if (usb_testutils_transfer_next_part(ctx, ep, transfer)) {
+              done = false;
+            } else {
+              res = kUsbTestutilsXfrResultFailed;
+            }
+          }
+          if (done) {
+            // Larger buffer transfer now completed; forget the buffer
+            transfer->buffer = NULL;
+          }
+        }
+        // Notify that we've sent the single packet, or larger buffer transfer
+        // is now complete
+        if (done && ctx->in[ep].tx_done_callback) {
+          ctx->in[ep].tx_done_callback(ctx->in[ep].ep_ctx, res);
+        }
+        sentep &= ~(1u << ep);
+      }
+      ep++;
+    }
+  }
+
+  // Keep buffers available for packet reception
+  CHECK_DIF_OK(dif_usbdev_fill_available_fifo(ctx->dev, ctx->buffer_pool));
+
+  if (istate & (1u << kDifUsbdevIrqPktReceived)) {
+    // TODO: we run the risk of starving the IN side here if the rx_callback(s)
+    // are time-consuming
+    while (true) {
+      bool is_empty;
+      CHECK_DIF_OK(dif_usbdev_status_get_rx_fifo_empty(ctx->dev, &is_empty));
+      if (is_empty) {
+        break;
+      }
+
+      dif_usbdev_rx_packet_info_t packet_info;
+      dif_usbdev_buffer_t buffer;
+      CHECK_DIF_OK(dif_usbdev_recv(ctx->dev, &packet_info, &buffer));
+
+      unsigned ep = packet_info.endpoint;
+      if (ctx->out[ep].rx_callback) {
+        ctx->out[ep].rx_callback(ctx->out[ep].ep_ctx, packet_info, buffer);
+      } else {
+        // Note: this could happen following endpoint removal
+        TRC_S("USB: unexpected RX ");
+        TRC_I(ep, 8);
+        CHECK_DIF_OK(
+            dif_usbdev_buffer_return(ctx->dev, ctx->buffer_pool, &buffer));
+      }
+    }
+  }
+  if (istate & (1u << kDifUsbdevIrqLinkReset)) {
+    TRC_S("USB: Bus reset");
+    // Link reset
+    for (int ep = 0; ep < USBDEV_NUM_ENDPOINTS; ep++) {
+      // Notify the IN endpoint first because transmission is more significantly
+      // impacted, and then the OUT endpoint before advancing to the next
+      // endpoint number in case the order is important to the client(s)
+      if (ctx->in[ep].reset) {
+        ctx->in[ep].reset(ctx->in[ep].ep_ctx);
+      }
+      if (ctx->out[ep].reset) {
+        ctx->out[ep].reset(ctx->out[ep].ep_ctx);
+      }
+    }
+  }
+
+  // Clear the interrupts that we've received and handled
+  CHECK_DIF_OK(dif_usbdev_irq_acknowledge_state(ctx->dev, istate));
+
+  // Record bus frame
+  if ((istate & (1u << kDifUsbdevIrqFrame))) {
+    // The first bus frame is 1
+    CHECK_DIF_OK(dif_usbdev_status_get_frame(ctx->dev, &ctx->frame));
+    ctx->got_frame = true;
+  }
+
+  // Note: LinkInErr will be raised in response to a packet being NAKed by the
+  // host which is not expected behavior on a physical USB but this is something
+  // that the DPI model does to exercise packet resending when running
+  // usbdev_stream_test
+  //
+  // We can expect AVFIFO empty and RXFIFO full interrupts when using a real
+  // host and also 'LinkOut' errors because these can be triggered by a lack of
+  // space in the RXFIFO
+
+  if (istate &
+      ~((1u << kDifUsbdevIrqLinkReset) | (1u << kDifUsbdevIrqPktReceived) |
+        (1u << kDifUsbdevIrqPktSent) | (1u << kDifUsbdevIrqFrame) |
+        (1u << kDifUsbdevIrqAvEmpty) | (1u << kDifUsbdevIrqRxFull) |
+        (1u << kDifUsbdevIrqLinkOutErr) | (1u << kDifUsbdevIrqLinkInErr))) {
+    // Report anything that really should not be happening during testing,
+    //   at least for now
+    //
+    // TODO - introduce deliberate generation of each of these errors, and
+    //        modify usb_testutils_ to return the information without faulting
+    //        it?
+    if (istate &
+        ((1u << kDifUsbdevIrqRxFull) | (1u << kDifUsbdevIrqAvOverflow) |
+         (1u << kDifUsbdevIrqLinkInErr) | (1u << kDifUsbdevIrqRxCrcErr) |
+         (1u << kDifUsbdevIrqRxPidErr) | (1u << kDifUsbdevIrqRxBitstuffErr) |
+         (1u << kDifUsbdevIrqLinkOutErr))) {
+      LOG_INFO("USB: Unexpected interrupts: 0x%08x", istate);
+    } else {
+      // Other events are optionally reported
+      TRC_C('I');
+      TRC_I(istate, 12);
+      TRC_C(' ');
+    }
+  }
+
+  // TODO - clean this up
+  // Frame ticks every 1ms, use to flush data every 16ms
+  // (faster in DPI but this seems to work ok)
+  //
+  // Ensure that we do not flush until we have received a frame
+  if (ctx->got_frame && (ctx->frame & 0xf) == 1) {
+    if (ctx->flushed == 0) {
+      for (unsigned ep = 0; ep < USBDEV_NUM_ENDPOINTS; ep++) {
+        if (ctx->in[ep].flush) {
+          ctx->in[ep].flush(ctx->in[ep].ep_ctx);
+        }
+      }
+      ctx->flushed = 1;
+    }
+  } else {
+    ctx->flushed = 0;
+  }
+  // TODO Errors? What Errors?
+}
+
+bool usb_testutils_transfer_send(usb_testutils_ctx_t *ctx, uint8_t ep,
+                                 const uint8_t *data, uint32_t length,
+                                 usb_testutils_xfr_flags_t flags) {
+  CHECK(ep < USBDEV_NUM_ENDPOINTS);
+
+  usb_testutils_transfer_t *transfer = &ctx->in[ep].transfer;
+  if (transfer->buffer) {
+    // If there is an in-progress transfer, then we cannot accept another
+    return false;
+  }
+
+  // Describe this transfer
+  transfer->buffer = data;
+  transfer->offset = 0U;
+  transfer->length = length;
+  transfer->flags = flags;
+  transfer->next_valid = false;
+
+  if (!usb_testutils_transfer_next_part(ctx, ep, transfer)) {
+    // Forget about the attempted transfer
+    transfer->buffer = NULL;
+    return false;
+  }
+
+  // Buffer transfer is underway...
+  return true;
+}
+
+void usb_testutils_in_endpoint_setup(
+    usb_testutils_ctx_t *ctx, uint8_t ep, void *ep_ctx,
+    void (*tx_done)(void *, usb_testutils_xfr_result_t), void (*flush)(void *),
+    void (*reset)(void *)) {
+  ctx->in[ep].ep_ctx = ep_ctx;
+  ctx->in[ep].tx_done_callback = tx_done;
+  ctx->in[ep].flush = flush;
+  ctx->in[ep].reset = reset;
+
+  dif_usbdev_endpoint_id_t endpoint = {
+      .number = ep,
+      .direction = USBDEV_ENDPOINT_DIR_IN,
+  };
+
+  CHECK_DIF_OK(
+      dif_usbdev_endpoint_stall_enable(ctx->dev, endpoint, kDifToggleDisabled));
+
+  // Enable IN traffic from device to host
+  CHECK_DIF_OK(
+      dif_usbdev_endpoint_enable(ctx->dev, endpoint, kDifToggleEnabled));
+}
+
+void usb_testutils_out_endpoint_setup(
+    usb_testutils_ctx_t *ctx, uint8_t ep,
+    usb_testutils_out_transfer_mode_t out_mode, void *ep_ctx,
+    void (*rx)(void *, dif_usbdev_rx_packet_info_t, dif_usbdev_buffer_t),
+    void (*reset)(void *)) {
+  ctx->out[ep].ep_ctx = ep_ctx;
+  ctx->out[ep].rx_callback = rx;
+  ctx->out[ep].reset = reset;
+
+  dif_usbdev_endpoint_id_t endpoint = {
+      .number = ep,
+      .direction = USBDEV_ENDPOINT_DIR_OUT,
+  };
+
+  CHECK_DIF_OK(
+      dif_usbdev_endpoint_stall_enable(ctx->dev, endpoint, kDifToggleDisabled));
+
+  // Enable/disable the endpoint and reception of OUT packets?
+  dif_toggle_t enabled = kDifToggleEnabled;
+  if (out_mode == kUsbdevOutDisabled) {
+    enabled = kDifToggleDisabled;
+  }
+  // Enable/disable generation of NAK responses once OUT packet has been
+  // received?
+  dif_toggle_t nak = kDifToggleDisabled;
+  if (out_mode == kUsbdevOutMessage) {
+    nak = kDifToggleEnabled;
+  }
+
+  CHECK_DIF_OK(dif_usbdev_endpoint_enable(ctx->dev, endpoint, enabled));
+  CHECK_DIF_OK(dif_usbdev_endpoint_out_enable(ctx->dev, ep, enabled));
+  CHECK_DIF_OK(dif_usbdev_endpoint_set_nak_out_enable(ctx->dev, ep, nak));
+}
+
+void usb_testutils_endpoint_setup(
+    usb_testutils_ctx_t *ctx, uint8_t ep,
+    usb_testutils_out_transfer_mode_t out_mode, void *ep_ctx,
+    void (*tx_done)(void *, usb_testutils_xfr_result_t),
+    void (*rx)(void *, dif_usbdev_rx_packet_info_t, dif_usbdev_buffer_t),
+    void (*flush)(void *), void (*reset)(void *)) {
+  usb_testutils_in_endpoint_setup(ctx, ep, ep_ctx, tx_done, flush, reset);
+
+  // Note: register the link reset handler only on the IN endpoint so that it
+  // does not get invoked twice
+  usb_testutils_out_endpoint_setup(ctx, ep, out_mode, ep_ctx, rx, NULL);
+}
+
+void usb_testutils_in_endpoint_remove(usb_testutils_ctx_t *ctx, uint8_t ep) {
+  // Disable IN traffic
+  dif_usbdev_endpoint_id_t endpoint = {
+      .number = ep,
+      .direction = USBDEV_ENDPOINT_DIR_IN,
+  };
+  CHECK_DIF_OK(
+      dif_usbdev_endpoint_enable(ctx->dev, endpoint, kDifToggleDisabled));
+
+  // Remove callback handlers
+  ctx->in[ep].tx_done_callback = NULL;
+  ctx->in[ep].flush = NULL;
+  ctx->in[ep].reset = NULL;
+}
+
+void usb_testutils_out_endpoint_remove(usb_testutils_ctx_t *ctx, uint8_t ep) {
+  // Disable OUT traffic
+  dif_usbdev_endpoint_id_t endpoint = {
+      .number = ep,
+      .direction = USBDEV_ENDPOINT_DIR_OUT,
+  };
+  CHECK_DIF_OK(
+      dif_usbdev_endpoint_enable(ctx->dev, endpoint, kDifToggleDisabled));
+
+  // Return the rest of the OUT endpoint configuration to its default state
+  CHECK_DIF_OK(dif_usbdev_endpoint_set_nak_out_enable(ctx->dev, endpoint.number,
+                                                      kDifToggleDisabled));
+  CHECK_DIF_OK(dif_usbdev_endpoint_out_enable(ctx->dev, endpoint.number,
+                                              kDifToggleDisabled));
+
+  // Remove callback handlers
+  ctx->out[ep].rx_callback = NULL;
+  ctx->out[ep].reset = NULL;
+}
+
+void usb_testutils_endpoint_remove(usb_testutils_ctx_t *ctx, uint8_t ep) {
+  usb_testutils_in_endpoint_remove(ctx, ep);
+  usb_testutils_out_endpoint_remove(ctx, ep);
+}
+
+void usb_testutils_init(usb_testutils_ctx_t *ctx, bool pinflip,
+                        bool en_diff_rcvr, bool tx_use_d_se0) {
+  CHECK(ctx != NULL);
+  ctx->dev = &usbdev;
+  ctx->buffer_pool = &buffer_pool;
+
+  // Ensure that we do not invoke the endpoint 'flush' functions before
+  // detection of the first bus frame
+  ctx->got_frame = false;
+  ctx->frame = 0u;
+
+  CHECK_DIF_OK(
+      dif_usbdev_init(mmio_region_from_addr(USBDEV_BASE_ADDR), ctx->dev));
+
+  dif_usbdev_config_t config = {
+      .have_differential_receiver = dif_bool_to_toggle(en_diff_rcvr),
+      .use_tx_d_se0 = dif_bool_to_toggle(tx_use_d_se0),
+      .single_bit_eop = kDifToggleDisabled,
+      .pin_flip = dif_bool_to_toggle(pinflip),
+      .clock_sync_signals = kDifToggleEnabled,
+  };
+  CHECK_DIF_OK(dif_usbdev_configure(ctx->dev, ctx->buffer_pool, config));
+
+  // Set up context
+  for (int i = 0; i < USBDEV_NUM_ENDPOINTS; i++) {
+    usb_testutils_endpoint_setup(ctx, i, kUsbdevOutDisabled, NULL, NULL, NULL,
+                                 NULL, NULL);
+  }
+
+  // All about polling...
+  CHECK_DIF_OK(dif_usbdev_irq_disable_all(ctx->dev, NULL));
+
+  // Provide buffers for any packet reception
+  CHECK_DIF_OK(dif_usbdev_fill_available_fifo(ctx->dev, ctx->buffer_pool));
+
+  // Preemptively enable SETUP reception on endpoint zero for the
+  // Default Control Pipe; all other settings for that endpoint will be applied
+  // once the callback handlers are registered by a call to _endpoint_setup()
+  CHECK_DIF_OK(
+      dif_usbdev_endpoint_setup_enable(ctx->dev, 0, kDifToggleEnabled));
+}
+
+void usb_testutils_fin(usb_testutils_ctx_t *ctx) {
+  // Remove the endpoints in reverse order so that Endpoint Zero goes down last
+  for (int ep = USBDEV_NUM_ENDPOINTS - 1; ep >= 0; ep--) {
+    usb_testutils_endpoint_remove(ctx, ep);
+  }
+
+  // Disconnect from the bus
+  CHECK_DIF_OK(dif_usbdev_interface_enable(ctx->dev, kDifToggleDisabled));
+}
+
+// `extern` declarations to give the inline functions in the
+// corresponding header a link location.
+
+extern int usb_testutils_halted(usb_testutils_ctx_t *ctx,
+                                dif_usbdev_endpoint_id_t endpoint);
diff --git a/sw/device/lib/testing/usb_testutils.h b/sw/device/lib/testing/usb_testutils.h
new file mode 100644
index 0000000..6dcfc96
--- /dev/null
+++ b/sw/device/lib/testing/usb_testutils.h
@@ -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
+
+#ifndef OPENTITAN_SW_DEVICE_LIB_TESTING_USB_TESTUTILS_H_
+#define OPENTITAN_SW_DEVICE_LIB_TESTING_USB_TESTUTILS_H_
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#include "sw/device/lib/dif/dif_usbdev.h"
+#include "usb_testutils_diags.h"
+
+// Result codes to rx/tx callback handlers
+typedef enum {
+  /**
+   * Successful completion.
+   */
+  kUsbTestutilsXfrResultOk = 0u,
+  /**
+   * Failed to transfer because of internal error,
+   * eg. buffer exhaustion.
+   */
+  kUsbTestutilsXfrResultFailed = 1u,
+  /**
+   * Link reset interrupted transfer.
+   */
+  kUsbTestutilsXfrResultLinkReset = 2u,
+  /**
+   * Canceled by suspend, endpoint removal or finalization.
+   */
+  kUsbTestutilsXfrResultCanceled = 3u,
+} usb_testutils_xfr_result_t;
+
+// Flags affecting the transfer of larger data buffers
+typedef enum {
+  kUsbTestutilsXfrMaxPacketMask = 0x7fu,  // Max packet size [0,0x40U]
+  /**
+   * Explicitly specify the maximum packet size; otherwise the device default
+   * of USBDEV_MAX_PACKET_SIZE shall be assumed.
+   */
+  kUsbTestutilsXfrMaxPacketSupplied = 0x100u,
+  /**
+   * Employ double-buffering to minimize the response time to notification of
+   * each packet transmission. This does require that two device packet buffers
+   * be available for use, but it increases the transfer rate.
+   */
+  kUsbTestutilsXfrDoubleBuffered = 0x200u,
+  /**
+   * Emit/Expect Zero Length Packet as termination of data stage in the event
+   * that the final packet of the transfer is maximum length
+   */
+  kUsbTestutilsXfrEmployZLP = 0x400u,
+} usb_testutils_xfr_flags_t;
+
+// In-progress larger buffer transfer to/from host
+typedef struct usb_testutils_transfer {
+  /**
+   * Start of buffer for transfer
+   */
+  const uint8_t *buffer;
+  /**
+   * Total number of bytes to be transferred to/from buffer
+   */
+  uint32_t length;
+  /**
+   * Byte offset of the _next_ packet to be transferred
+   */
+  uint32_t offset;
+  /**
+   * Flags modifying the transfer
+   */
+  usb_testutils_xfr_flags_t flags;
+  /**
+   * Indicates that the last packet of the transfer has been reached;
+   * if 'next_valid' is true, then 'next_part' holds the last packet, already
+   * prepared for sending; if next_valid is false, then the last packet has
+   * already been supplied to usbdev and we're just awaiting the 'pkt_sent'
+   * interrupt.
+   */
+  bool last;
+  /**
+   * The next part has been prepared and is ready to send to usbdev
+   */
+  bool next_valid;
+  /**
+   * When sending IN data to the host, we may employ double-buffering and keep
+   * an additional buffer ready to be sent as soon as we're notified of the
+   * transfer of its predecessor
+   */
+  dif_usbdev_buffer_t next_part;
+} usb_testutils_transfer_t;
+
+typedef struct usb_testutils_ctx usb_testutils_ctx_t;
+
+struct usb_testutils_ctx {
+  dif_usbdev_t *dev;
+  dif_usbdev_buffer_pool_t *buffer_pool;
+  int flushed;
+  /**
+   * Have we received an indication of USB activity?
+   */
+  bool got_frame;
+  /**
+   * Most recent bus frame number received from host
+   */
+  uint16_t frame;
+
+  /**
+   * IN endpoints
+   */
+  struct {
+    /**
+     * Opaque context handle for callback functions
+     */
+    void *ep_ctx;
+    /**
+     * Callback for transmission of IN packet
+     */
+    void (*tx_done_callback)(void *, usb_testutils_xfr_result_t);
+    /**
+     * Callback for periodically flushing IN data to host
+     */
+    void (*flush)(void *);
+    /**
+     * Callback for link reset
+     */
+    void (*reset)(void *);
+    /**
+     * Current in-progress transfer, if any
+     */
+    usb_testutils_transfer_t transfer;
+  } in[USBDEV_NUM_ENDPOINTS];
+
+  /**
+   * OUT endpoints
+   */
+  struct {
+    /**
+     * Opaque context handle for callback functions
+     */
+    void *ep_ctx;
+    /**
+     * Callback for reception of IN packet
+     */
+    void (*rx_callback)(void *, dif_usbdev_rx_packet_info_t,
+                        dif_usbdev_buffer_t);
+    /**
+     * Callback for link reset
+     */
+    void (*reset)(void *);
+  } out[USBDEV_NUM_ENDPOINTS];
+};
+
+typedef enum usb_testutils_out_transfer_mode {
+  /**
+   * The endpoint does not support OUT transactions.
+   */
+  kUsbdevOutDisabled = 0,
+  /**
+   * Software does NOT need to call usb_testutils_clear_out_nak() after every
+   * received transaction. If software takes no action, usbdev will allow an
+   * endpoint's transactions to proceed as long as a buffer is available.
+   */
+  kUsbdevOutStream = 1,
+  /**
+   * Software must call usb_testutils_clear_out_nak() after every received
+   * transaction to re-enable packet reception. This gives software time to
+   * respond with the appropriate handshake when it's ready.
+   */
+  kUsbdevOutMessage = 2,
+} usb_testutils_out_transfer_mode_t;
+
+/**
+ * Call to set up IN endpoint.
+ *
+ * @param ctx usb test utils context pointer
+ * @param ep endpoint number
+ * @param ep_ctx context pointer for callee
+ * @param tx_done(void *ep_ctx) callback once send has been Acked
+ * @param flush(void *ep_ctx) called every 16ms based USB host timebase
+ * @param reset(void *ep_ctx) called when an USB link reset is detected
+ */
+void usb_testutils_in_endpoint_setup(
+    usb_testutils_ctx_t *ctx, uint8_t ep, void *ep_ctx,
+    void (*tx_done)(void *, usb_testutils_xfr_result_t), void (*flush)(void *),
+    void (*reset)(void *));
+
+/**
+ * Call to set up OUT endpoint.
+ *
+ * @param ctx usb test utils context pointer
+ * @param ep endpoint number
+ * @param out_mode the transfer mode for OUT transactions
+ * @param ep_ctx context pointer for callee
+ * @param rx(void *ep_ctx, usbbufid_t buf, int size, int setup)
+          called when a packet is received
+ * @param reset(void *ep_ctx) called when an USB link reset is detected
+ */
+void usb_testutils_out_endpoint_setup(
+    usb_testutils_ctx_t *ctx, uint8_t ep,
+    usb_testutils_out_transfer_mode_t out_mode, void *ep_ctx,
+    void (*rx)(void *, dif_usbdev_rx_packet_info_t, dif_usbdev_buffer_t),
+    void (*reset)(void *));
+
+/**
+ * Call to set up a pair of IN and OUT endpoints.
+ *
+ * @param ctx usb test utils context pointer
+ * @param ep endpoint number
+ * @param out_mode the transfer mode for OUT transactions
+ * @param ep_ctx context pointer for callee
+ * @param tx_done(void *ep_ctx) callback once send has been Acked
+ * @param rx(void *ep_ctx, usbbufid_t buf, int size, int setup)
+          called when a packet is received
+ * @param flush(void *ep_ctx) called every 16ms based USB host timebase
+ * @param reset(void *ep_ctx) called when an USB link reset is detected
+ */
+void usb_testutils_endpoint_setup(
+    usb_testutils_ctx_t *ctx, uint8_t ep,
+    usb_testutils_out_transfer_mode_t out_mode, void *ep_ctx,
+    void (*tx_done)(void *, usb_testutils_xfr_result_t),
+    void (*rx)(void *, dif_usbdev_rx_packet_info_t, dif_usbdev_buffer_t),
+    void (*flush)(void *), void (*reset)(void *));
+
+/**
+ * Remove an IN endpoint.
+ *
+ * @param ctx usb test utils context pointer
+ * @param ep endpoint number
+ */
+void usb_testutils_in_endpoint_remove(usb_testutils_ctx_t *ctx, uint8_t ep);
+
+/**
+ * Remove an OUT endpoint.
+ *
+ * @param ctx usb test utils context pointer
+ * @param ep endpoint number
+ */
+void usb_testutils_out_endpoint_remove(usb_testutils_ctx_t *ctx, uint8_t ep);
+
+/**
+ * Remove a pair of IN and OUT endpoints
+ *
+ * @param ctx usb test utils context pointer
+ * @param ep endpoint number
+ */
+void usb_testutils_endpoint_remove(usb_testutils_ctx_t *ctx, uint8_t ep);
+
+/**
+ * Returns an indication of whether an endpoint is currently halted because
+ * of the occurrence of an error.
+ *
+ * @param ctx usb test utils context pointer
+ * @param ep endpoint number
+ * @return true iff the endpoint is halted as a result of an error condition
+ */
+inline bool usb_testutils_endpoint_halted(usb_testutils_ctx_t *ctx,
+                                          dif_usbdev_endpoint_id_t endpoint);
+
+/**
+ * Initialize the usbdev interface
+ *
+ * Does not connect the device, since the default endpoint is not yet enabled.
+ * See usb_testutils_connect().
+ *
+ * @param ctx uninitialized usb test utils context pointer
+ * @param pinflip boolean to indicate if PHY should be configured for D+/D- flip
+ * @param en_diff_rcvr boolean to indicate if PHY should enable an external
+ *                     differential receiver, activating the single-ended D
+ *                     input
+ * @param tx_use_d_se0 boolean to indicate if PHY uses D/SE0 for TX instead of
+ *                     Dp/Dn
+ */
+void usb_testutils_init(usb_testutils_ctx_t *ctx, bool pinflip,
+                        bool en_diff_rcvr, bool tx_use_d_se0);
+
+/**
+ * Send a larger data transfer from the given endpoint
+ *
+ * The usb_testutils layer will, if necessary, break this transfer into multiple
+ * packet buffers to be transferred in turn across the USB. The caller shall be
+ * notified via the tx_done_callback handler of successful completion of the
+ * entire transfer, or failure, and the caller must guarantee the availability
+ * of the supplied data throughout the operation.
+ *
+ * @param ctx        usb test utils context pointer
+ * @param ep         endpoint number
+ * @param data       buffer of data to be transferred
+ * @param length     number of bytes to be transferred
+ * @param flags      flags modifying the transfer operation
+ * @return           true iff the data has been accepted for transmission
+ */
+bool usb_testutils_transfer_send(usb_testutils_ctx_t *ctx, uint8_t ep,
+                                 const uint8_t *data, uint32_t length,
+                                 usb_testutils_xfr_flags_t flags);
+
+/**
+ * Call regularly to poll the usbdev interface
+ *
+ * @param ctx usb test utils context pointer
+ */
+void usb_testutils_poll(usb_testutils_ctx_t *ctx);
+
+/**
+ * Finalize the usbdev interface
+ *
+ * Removes all endpoint handlers and disconnects the device from the USB.
+ * This should be used only if the USB device is no longer required, or if it is
+ * required to be restarted with, for example, a different bus configuration.
+ *
+ * @param ctx initialized usb test utils context pointer
+ */
+void usb_testutils_fin(usb_testutils_ctx_t *ctx);
+
+#endif  // OPENTITAN_SW_DEVICE_LIB_TESTING_USB_TESTUTILS_H_
diff --git a/sw/device/lib/testing/usb_testutils_controlep.c b/sw/device/lib/testing/usb_testutils_controlep.c
new file mode 100644
index 0000000..513c0da
--- /dev/null
+++ b/sw/device/lib/testing/usb_testutils_controlep.c
@@ -0,0 +1,413 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+#include "sw/device/lib/testing/usb_testutils_controlep.h"
+
+#include "sw/device/lib/dif/dif_usbdev.h"
+#include "sw/device/lib/testing/test_framework/check.h"
+#include "sw/device/lib/testing/usb_testutils.h"
+
+// Device descriptor
+static uint8_t dev_dscr[] = {
+    18,    // bLength
+    1,     // bDescriptorType
+    0x00,  // bcdUSB[0]
+    0x02,  // bcdUSB[1]
+    0x00,  // bDeviceClass (defined at interface level)
+    0x00,  // bDeviceSubClass
+    0x00,  // bDeviceProtocol
+    64,    // bMaxPacketSize0
+
+    0xd1,  // idVendor[0] 0x18d1 Google Inc.
+    0x18,  // idVendor[1]
+    0x3a,  // idProduct[0] lowRISC generic FS USB
+    0x50,  // idProduct[1] (allocated by Google)
+
+    0,    // bcdDevice[0]
+    0x1,  // bcdDevice[1]
+    0,    // iManufacturer
+    0,    // iProduct
+    0,    // iSerialNumber
+    1     // bNumConfigurations
+};
+
+// SETUP requests
+typedef enum usb_setup_req {
+  kUsbSetupReqGetStatus = 0,
+  kUsbSetupReqClearFeature = 1,
+  kUsbSetupReqSetFeature = 3,
+  kUsbSetupReqSetAddress = 5,
+  kUsbSetupReqGetDescriptor = 6,
+  kUsbSetupReqSetDescriptor = 7,
+  kUsbSetupReqGetConfiguration = 8,
+  kUsbSetupReqSetConfiguration = 9,
+  kUsbSetupReqGetInterface = 10,
+  kUsbSetupReqSetInterface = 11,
+  kUsbSetupReqSynchFrame = 12
+} usb_setup_req_t;
+
+// Vendor-specific requests defined by our device/test framework
+typedef enum vendor_setup_req {
+  kVendorSetupReqTestConfig = 0x7C,
+  kVendorSetupReqTestStatus = 0x7E
+} vendor_setup_req_t;
+
+typedef enum usb_req_type {  // bmRequestType
+  kUsbReqTypeRecipientMask = 0x1f,
+  kUsbReqTypeDevice = 0,
+  kUsbReqTypeInterface = 1,
+  kUsbReqTypeEndpoint = 2,
+  kUsbReqTypeOther = 3,
+  kUsbReqTypeTypeMask = 0x60,
+  kUsbReqTypeStandard = 0,
+  kUsbReqTypeClass = 0x20,
+  kUsbReqTypeVendor = 0x40,
+  kUsbReqTypeReserved = 0x60,
+  kUsbReqTypeDirMask = 0x80,
+  kUsbReqTypeDirH2D = 0x00,
+  kUsbReqTypeDirD2H = 0x80,
+} usb_req_type_t;
+
+typedef enum usb_desc_type {  // Descriptor type (wValue hi)
+  kUsbDescTypeDevice = 1,
+  kUsbDescTypeConfiguration,
+  kUsbDescTypeString,
+  kUsbDescTypeInterface,
+  kUsbDescTypeEndpoint,
+  kUsbDescTypeDeviceQualifier,
+  kUsbDescTypeOtherSpeedConfiguration,
+  kUsbDescTypeInterfacePower,
+} usb_desc_type_t;
+
+typedef enum usb_feature_req {
+  kUsbFeatureEndpointHalt = 0,        // recipient is endpoint
+  kUsbFeatureDeviceRemoteWakeup = 1,  // recipient is device
+  kUsbFeatureTestMode = 2,            // recipient is device
+  kUsbFeatureBHnpEnable = 3,          // recipient is device only if OTG
+  kUsbFeatureAHnpSupport = 4,         // recipient is device only if OTG
+  kUsbFeatureAAltHnpSupport = 5       // recipient is device only if OTG
+} usb_feature_req_t;
+
+typedef enum usb_status {
+  kUsbStatusSelfPowered = 1,  // Device status request
+  kUsbStatusRemWake = 2,      // Device status request
+  kUsbStatusHalted = 1        // Endpoint status request
+} usb_status_t;
+
+static usb_testutils_ctstate_t setup_req(usb_testutils_controlep_ctx_t *ctctx,
+                                         usb_testutils_ctx_t *ctx,
+                                         int bmRequestType, int bRequest,
+                                         int wValue, int wIndex, int wLength) {
+  size_t len;
+  uint32_t stat;
+  int zero, type;
+  size_t bytes_written;
+  // Endpoint for SetFeature/ClearFeature/GetStatus requests
+  dif_usbdev_endpoint_id_t endpoint = {
+      .number = (uint8_t)wIndex,
+      .direction = ((bmRequestType & 0x80U) != 0U),
+  };
+  dif_usbdev_buffer_t buffer;
+  CHECK_DIF_OK(dif_usbdev_buffer_request(ctx->dev, ctx->buffer_pool, &buffer));
+  switch (bRequest) {
+    case kUsbSetupReqGetDescriptor:
+      if ((wValue & 0xff00) == 0x100) {
+        // Device descriptor
+        len = sizeof(dev_dscr);
+        if (wLength < len) {
+          len = wLength;
+        }
+        CHECK_DIF_OK(dif_usbdev_buffer_write(ctx->dev, &buffer, dev_dscr, len,
+                                             &bytes_written));
+        CHECK_DIF_OK(dif_usbdev_send(ctx->dev, ctctx->ep, &buffer));
+        return kUsbTestutilsCtWaitIn;
+      } else if ((wValue & 0xff00) == 0x200) {
+        usb_testutils_xfr_flags_t flags = kUsbTestutilsXfrDoubleBuffered;
+
+        // Configuration descriptor
+        len = ctctx->cfg_dscr_len;
+        if (wLength < len) {
+          len = wLength;
+        } else if (wLength > len) {
+          // Since we're not sending as much as requested, we may need to use
+          // a Zero Length Packet to mark the end of the data stage
+          flags |= kUsbTestutilsXfrEmployZLP;
+        }
+
+        if (len >= USBDEV_MAX_PACKET_SIZE) {
+          CHECK_DIF_OK(
+              dif_usbdev_buffer_return(ctx->dev, ctx->buffer_pool, &buffer));
+
+          if (!usb_testutils_transfer_send(ctx, 0U, ctctx->cfg_dscr, len,
+                                           flags)) {
+            return kUsbTestutilsCtError;
+          }
+        } else {
+          CHECK_DIF_OK(dif_usbdev_buffer_write(
+              ctx->dev, &buffer, ctctx->cfg_dscr, len, &bytes_written));
+          CHECK_DIF_OK(dif_usbdev_send(ctx->dev, ctctx->ep, &buffer));
+        }
+        return kUsbTestutilsCtWaitIn;
+      }
+      return kUsbTestutilsCtError;  // unknown
+
+    case kUsbSetupReqSetAddress:
+      TRC_S("SA");
+      ctctx->new_dev = wValue & 0x7f;
+      // send zero length packet for status phase
+      CHECK_DIF_OK(dif_usbdev_send(ctx->dev, ctctx->ep, &buffer));
+      return kUsbTestutilsCtAddrStatIn;
+
+    case kUsbSetupReqSetConfiguration:
+      TRC_S("SC");
+      // only ever expect this to be 1 since there is one config descriptor
+      ctctx->usb_config = wValue;
+      // send zero length packet for status phase
+      CHECK_DIF_OK(dif_usbdev_send(ctx->dev, ctctx->ep, &buffer));
+      if (wValue) {
+        ctctx->device_state = kUsbTestutilsDeviceConfigured;
+      } else {
+        // Device deconfigured
+        ctctx->device_state = kUsbTestutilsDeviceAddressed;
+      }
+      return kUsbTestutilsCtStatIn;
+
+    case kUsbSetupReqGetConfiguration:
+      len = sizeof(ctctx->usb_config);
+      if (wLength < len) {
+        len = wLength;
+      }
+      // return the value that was set
+      CHECK_DIF_OK(dif_usbdev_buffer_write(
+          ctx->dev, &buffer, &ctctx->usb_config, len, &bytes_written));
+      CHECK_DIF_OK(dif_usbdev_send(ctx->dev, ctctx->ep, &buffer));
+      return kUsbTestutilsCtWaitIn;
+
+    case kUsbSetupReqSetFeature:
+      if (wValue == kUsbFeatureEndpointHalt) {
+        CHECK_DIF_OK(dif_usbdev_endpoint_stall_enable(ctx->dev, endpoint,
+                                                      kDifToggleEnabled));
+        // send zero length packet for status phase
+        CHECK_DIF_OK(dif_usbdev_send(ctx->dev, ctctx->ep, &buffer));
+        return kUsbTestutilsCtStatIn;
+      }
+      return kUsbTestutilsCtError;  // unknown
+
+    case kUsbSetupReqClearFeature:
+      if (wValue == kUsbFeatureEndpointHalt) {
+        CHECK_DIF_OK(dif_usbdev_endpoint_stall_enable(ctx->dev, endpoint,
+                                                      kDifToggleDisabled));
+        // send zero length packet for status phase
+        CHECK_DIF_OK(dif_usbdev_send(ctx->dev, ctctx->ep, &buffer));
+      }
+      return kUsbTestutilsCtStatIn;
+
+    case kUsbSetupReqGetStatus:
+      len = 2;
+      type = bmRequestType & kUsbReqTypeRecipientMask;
+      if (type == kUsbReqTypeDevice) {
+        stat = kUsbStatusSelfPowered;
+      } else if (type == kUsbReqTypeEndpoint) {
+        bool halted;
+        CHECK_DIF_OK(
+            dif_usbdev_endpoint_stall_get(ctx->dev, endpoint, &halted));
+        stat = halted ? kUsbStatusHalted : 0;
+      } else {
+        stat = 0;
+      }
+      if (wLength < len) {
+        len = wLength;
+      }
+      // return the value that was set
+      CHECK_DIF_OK(dif_usbdev_buffer_write(ctx->dev, &buffer, (uint8_t *)&stat,
+                                           len, &bytes_written));
+      CHECK_DIF_OK(dif_usbdev_send(ctx->dev, ctctx->ep, &buffer));
+      return kUsbTestutilsCtWaitIn;
+
+    case kUsbSetupReqSetInterface:
+      // Don't support alternate interfaces, so just ignore
+      // send zero length packet for status phase
+      CHECK_DIF_OK(dif_usbdev_send(ctx->dev, ctctx->ep, &buffer));
+      return kUsbTestutilsCtStatIn;
+
+    case kUsbSetupReqGetInterface:
+      zero = 0;
+      len = 1;
+      if (wLength < len) {
+        len = wLength;
+      }
+      // Don't support interface, so return zero
+      CHECK_DIF_OK(dif_usbdev_buffer_write(ctx->dev, &buffer, (uint8_t *)&zero,
+                                           len, &bytes_written));
+      CHECK_DIF_OK(dif_usbdev_send(ctx->dev, ctctx->ep, &buffer));
+      return kUsbTestutilsCtWaitIn;
+
+    case kUsbSetupReqSynchFrame:
+      zero = 0;
+      len = 2;
+      if (wLength < len) {
+        len = wLength;
+      }
+      // Don't support synch_frame so return zero
+      CHECK_DIF_OK(dif_usbdev_buffer_write(ctx->dev, &buffer, (uint8_t *)&zero,
+                                           len, &bytes_written));
+      CHECK_DIF_OK(dif_usbdev_send(ctx->dev, ctctx->ep, &buffer));
+      return kUsbTestutilsCtWaitIn;
+
+    default:
+      // We implement a couple of bespoke, vendor-defined Setup requests to
+      // allow the DPI model to access the test configuration (Control Read) and
+      // to report the test status (Control Write)
+      if ((bmRequestType & kUsbReqTypeTypeMask) == kUsbReqTypeVendor &&
+          ctctx->test_dscr) {
+        switch ((vendor_setup_req_t)bRequest) {
+          case kVendorSetupReqTestConfig: {
+            TRC_S("TC");
+            // Test config descriptor
+            len = ctctx->test_dscr_len;
+            if (wLength < len) {
+              len = wLength;
+            }
+            CHECK_DIF_OK(dif_usbdev_buffer_write(
+                ctx->dev, &buffer, ctctx->test_dscr, len, &bytes_written));
+            CHECK_DIF_OK(dif_usbdev_send(ctx->dev, ctctx->ep, &buffer));
+            return kUsbTestutilsCtWaitIn;
+          } break;
+          case kVendorSetupReqTestStatus: {
+            // TODO - pass the received test status to the OTTF directly?
+          } break;
+        }
+      }
+      return kUsbTestutilsCtError;
+  }
+  return kUsbTestutilsCtError;
+}
+
+static void ctrl_tx_done(void *ctctx_v, usb_testutils_xfr_result_t result) {
+  usb_testutils_controlep_ctx_t *ctctx =
+      (usb_testutils_controlep_ctx_t *)ctctx_v;
+  usb_testutils_ctx_t *ctx = ctctx->ctx;
+  TRC_C('A' + ctctx->ctrlstate);
+  switch (ctctx->ctrlstate) {
+    case kUsbTestutilsCtAddrStatIn:
+      // Now the status was sent on device 0 can switch to new device ID
+      CHECK_DIF_OK(dif_usbdev_address_set(ctx->dev, ctctx->new_dev));
+      TRC_I(ctctx->new_dev, 8);
+      ctctx->ctrlstate = kUsbTestutilsCtIdle;
+      // We now have a device address on the USB
+      ctctx->device_state = kUsbTestutilsDeviceAddressed;
+      return;
+    case kUsbTestutilsCtStatIn:
+      ctctx->ctrlstate = kUsbTestutilsCtIdle;
+      return;
+    case kUsbTestutilsCtWaitIn:
+      ctctx->ctrlstate = kUsbTestutilsCtStatOut;
+      return;
+
+    default:
+      break;
+  }
+  TRC_S("USB: unexpected IN ");
+  TRC_I((ctctx->ctrlstate << 24), 32);
+}
+
+static void ctrl_rx(void *ctctx_v, dif_usbdev_rx_packet_info_t packet_info,
+                    dif_usbdev_buffer_t buffer) {
+  usb_testutils_controlep_ctx_t *ctctx =
+      (usb_testutils_controlep_ctx_t *)ctctx_v;
+  usb_testutils_ctx_t *ctx = ctctx->ctx;
+  CHECK_DIF_OK(dif_usbdev_endpoint_out_enable(ctx->dev, /*endpoint=*/0,
+                                              kDifToggleEnabled));
+
+  TRC_C('0' + ctctx->ctrlstate);
+  size_t bytes_written;
+  // TODO: Should check for canceled IN transactions due to receiving a SETUP
+  // packet.
+  switch (ctctx->ctrlstate) {
+    case kUsbTestutilsCtIdle:
+      // Waiting to be set up
+      if (packet_info.is_setup && (packet_info.length == 8)) {
+        alignas(uint32_t) uint8_t bp[8];
+        CHECK_DIF_OK(dif_usbdev_buffer_read(ctx->dev, ctx->buffer_pool, &buffer,
+                                            bp, sizeof(bp), &bytes_written));
+        int bmRequestType = bp[0];
+        int bRequest = bp[1];
+        int wValue = (bp[3] << 8) | bp[2];
+        int wIndex = (bp[5] << 8) | bp[4];
+        int wLength = (bp[7] << 8) | bp[6];
+        TRC_C('0' + bRequest);
+
+        ctctx->ctrlstate = setup_req(ctctx, ctx, bmRequestType, bRequest,
+                                     wValue, wIndex, wLength);
+        if (ctctx->ctrlstate != kUsbTestutilsCtError) {
+          return;
+        }
+
+        TRC_C(':');
+        for (int i = 0; i < packet_info.length; i++) {
+          TRC_I(bp[i], 8);
+        }
+      }
+      break;
+
+    case kUsbTestutilsCtStatOut:
+      // Have sent some data, waiting STATUS stage
+      if (!packet_info.is_setup && (packet_info.length == 0)) {
+        CHECK_DIF_OK(
+            dif_usbdev_buffer_return(ctx->dev, ctx->buffer_pool, &buffer));
+        ctctx->ctrlstate = kUsbTestutilsCtIdle;
+        return;
+      }
+      // anything else is unexpected
+      break;
+
+    default:
+      // Error
+      break;
+  }
+  dif_usbdev_endpoint_id_t endpoint = {
+      .number = 0,
+      .direction = USBDEV_ENDPOINT_DIR_IN,
+  };
+  // Enable responding with STALL. Will be cleared by the HW upon next SETUP.
+  CHECK_DIF_OK(
+      dif_usbdev_endpoint_stall_enable(ctx->dev, endpoint, kDifToggleEnabled));
+  endpoint.direction = USBDEV_ENDPOINT_DIR_OUT;
+  CHECK_DIF_OK(
+      dif_usbdev_endpoint_stall_enable(ctx->dev, endpoint, kDifToggleEnabled));
+  TRC_S("USB: unCT ");
+  TRC_I((ctctx->ctrlstate << 24) | ((int)packet_info.is_setup << 16) |
+            packet_info.length,
+        32);
+  if (buffer.type != kDifUsbdevBufferTypeStale) {
+    // Return the unused buffer.
+    CHECK_DIF_OK(dif_usbdev_buffer_return(ctx->dev, ctx->buffer_pool, &buffer));
+  }
+  ctctx->ctrlstate = kUsbTestutilsCtIdle;
+}
+
+// Callback for the USB link reset
+static void ctrl_reset(void *ctctx_v) {
+  usb_testutils_controlep_ctx_t *ctctx =
+      (usb_testutils_controlep_ctx_t *)ctctx_v;
+  ctctx->ctrlstate = kUsbTestutilsCtIdle;
+}
+
+void usb_testutils_controlep_init(usb_testutils_controlep_ctx_t *ctctx,
+                                  usb_testutils_ctx_t *ctx, int ep,
+                                  const uint8_t *cfg_dscr, size_t cfg_dscr_len,
+                                  const uint8_t *test_dscr,
+                                  size_t test_dscr_len) {
+  ctctx->ctx = ctx;
+  usb_testutils_endpoint_setup(ctx, ep, kUsbdevOutMessage, ctctx, ctrl_tx_done,
+                               ctrl_rx, NULL, ctrl_reset);
+  ctctx->ep = ep;
+  ctctx->ctrlstate = kUsbTestutilsCtIdle;
+  ctctx->cfg_dscr = cfg_dscr;
+  ctctx->cfg_dscr_len = cfg_dscr_len;
+  ctctx->test_dscr = test_dscr;
+  ctctx->test_dscr_len = test_dscr_len;
+  CHECK_DIF_OK(dif_usbdev_interface_enable(ctx->dev, kDifToggleEnabled));
+  ctctx->device_state = kUsbTestutilsDeviceDefault;
+}
diff --git a/sw/device/lib/testing/usb_testutils_controlep.h b/sw/device/lib/testing/usb_testutils_controlep.h
new file mode 100644
index 0000000..9cbd378
--- /dev/null
+++ b/sw/device/lib/testing/usb_testutils_controlep.h
@@ -0,0 +1,147 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef OPENTITAN_SW_DEVICE_LIB_TESTING_USB_TESTUTILS_CONTROLEP_H_
+#define OPENTITAN_SW_DEVICE_LIB_TESTING_USB_TESTUTILS_CONTROLEP_H_
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "sw/device/lib/testing/usb_testutils.h"
+
+typedef enum usb_testutils_ctstate {
+  kUsbTestutilsCtIdle,
+  kUsbTestutilsCtWaitIn,      // Queued IN data stage, waiting ack
+  kUsbTestutilsCtStatOut,     // Waiting for OUT status stage
+  kUsbTestutilsCtAddrStatIn,  // Queued status stage, waiting ack.
+                              // After which, set dev_addr
+  kUsbTestutilsCtStatIn,      // Queued status stage, waiting ack
+  kUsbTestutilsCtError        // Something bad
+} usb_testutils_ctstate_t;
+
+typedef enum usb_testutils_device_state {
+  kUsbTestutilsDeviceAttached,
+  kUsbTestutilsDevicePowered,
+  kUsbTestutilsDeviceDefault,
+  kUsbTestutilsDeviceAddressed,
+  kUsbTestutilsDeviceConfigured,
+  kUsbTestutilsDeviceSuspended,
+} usb_testutils_device_state_t;
+
+typedef struct usb_testutils_controlep_ctx {
+  usb_testutils_ctx_t *ctx;
+  int ep;
+  usb_testutils_ctstate_t ctrlstate;
+  usb_testutils_device_state_t device_state;
+  uint32_t new_dev;
+  uint8_t usb_config;
+  /**
+   * USB configuration descriptor
+   */
+  const uint8_t *cfg_dscr;
+  size_t cfg_dscr_len;
+  /**
+   * Optional test descriptor, or NULL
+   */
+  const uint8_t *test_dscr;
+  size_t test_dscr_len;
+} usb_testutils_controlep_ctx_t;
+
+/**
+ * Initialize control endpoint
+ *
+ * @param ctctx uninitialized context for this instance
+ * @param ctx initialized context for usbdev driver
+ * @param ep endpoint (if this is other than 0 make sure you know why)
+ * @param cfg_dscr configuration descriptor for the device
+ * @param cfg_dscr_len length of cfg_dscr
+ * @param test_dscr optional test descriptor, or NULL
+ * @param test_dscr_len length of test_dscr
+ */
+void usb_testutils_controlep_init(usb_testutils_controlep_ctx_t *ctctx,
+                                  usb_testutils_ctx_t *ctx, int ep,
+                                  const uint8_t *cfg_dscr, size_t cfg_dscr_len,
+                                  const uint8_t *test_dscr,
+                                  size_t test_dscr_len);
+
+/***********************************************************************/
+/* Below this point are macros used to construct the USB configuration */
+/* descriptor. Use them to initialize a uint8_t array for cfg_dscr     */
+
+#define USB_CFG_DSCR_LEN 9
+#define USB_CFG_DSCR_HEAD(total_len, nint)                                   \
+  /* This is the actual configuration descriptor                 */          \
+  USB_CFG_DSCR_LEN,     /* bLength                                   */      \
+      2,                /* bDescriptorType                           */      \
+      (total_len)&0xff, /* wTotalLength[0]                           */      \
+      (total_len) >> 8, /* wTotalLength[1]                           */      \
+      (nint),           /* bNumInterfaces                            */      \
+      1,                /* bConfigurationValue                       */      \
+      0,                /* iConfiguration                            */      \
+      0xC0,             /* bmAttributes: must-be-one, self-powered   */      \
+      50 /* bMaxPower                                 */ /* MUST be followed \
+                                                            by (nint)        \
+                                                            Interface +      \
+                                                            Endpoint         \
+                                                            Descriptors */
+
+// KEEP BLANK LINE ABOVE, it is in the macro!
+
+#define USB_INTERFACE_DSCR_LEN 9
+#define VEND_INTERFACE_DSCR(inum, nep, subclass, protocol)               \
+  /* interface descriptor, USB spec 9.6.5, page 267-269, Table 9-12 */   \
+  USB_INTERFACE_DSCR_LEN, /* bLength                             */      \
+      4,                  /* bDescriptorType                     */      \
+      (inum),             /* bInterfaceNumber                    */      \
+      0,                  /* bAlternateSetting                   */      \
+      (nep),              /* bNumEndpoints                       */      \
+      0xff,               /* bInterfaceClass (Vendor Specific)   */      \
+      (subclass),         /* bInterfaceSubClass                  */      \
+      (protocol),         /* bInterfaceProtocol                  */      \
+      0 /* iInterface                          */ /* MUST be followed by \
+                                                     (nep) Endpoint      \
+                                                     Descriptors */
+
+// KEEP BLANK LINE ABOVE, it is in the macro!
+
+#define USB_EP_DSCR_LEN 7
+#define USB_EP_DSCR(in, ep, attr, maxsize, interval)                          \
+  /* endpoint descriptor, USB spec 9.6.6, page 269-271, Table 9-13   */       \
+  USB_EP_DSCR_LEN,                 /* bLength                              */ \
+      5,                           /* bDescriptorType                      */ \
+      (ep) | (((in) << 7) & 0x80), /* bEndpointAddress, top bit set for IN */ \
+      attr,                        /* bmAttributes                         */ \
+      (maxsize)&0xff,              /* wMaxPacketSize[0]                    */ \
+      (maxsize) >> 8,              /* wMaxPacketSize[1]                    */ \
+      (interval)                   /* bInterval                            */
+
+// KEEP BLANK LINE ABOVE, it is in the macro!
+#define USB_BULK_EP_DSCR(in, ep, maxsize, interval)                           \
+  /* endpoint descriptor, USB spec 9.6.6, page 269-271, Table 9-13   */       \
+  USB_EP_DSCR_LEN,                 /* bLength                              */ \
+      5,                           /* bDescriptorType                      */ \
+      (ep) | (((in) << 7) & 0x80), /* bEndpointAddress, top bit set for IN */ \
+      0x02,                        /* bmAttributes (0x02=bulk, data)       */ \
+      (maxsize)&0xff,              /* wMaxPacketSize[0]                    */ \
+      (maxsize) >> 8,              /* wMaxPacketSize[1]                    */ \
+      (interval)                   /* bInterval                            */
+
+// KEEP BLANK LINE ABOVE, it is in the macro!
+
+/***********************************************************************/
+/* Below this point are macros used to construct the test descriptor   */
+/* Use them to initialize a uint8_t array for test_dscr                */
+#define USB_TESTUTILS_TEST_DSCR_LEN 0x10u
+#define USB_TESTUTILS_TEST_DSCR(num, arg0, arg1, arg2, arg3)            \
+  0x7e, 0x57, 0xc0, 0xf1u,                /* Header signature        */ \
+      (USB_TESTUTILS_TEST_DSCR_LEN)&0xff, /* Descriptor length[0]    */ \
+      (USB_TESTUTILS_TEST_DSCR_LEN) >> 8, /* Descriptor length[1]    */ \
+      (num)&0xff,                         /* Test number[0]          */ \
+      (num) >> 8,                         /* Test number[1]          */ \
+      (arg0), (arg1), (arg2), (arg3),     /* Test-specific arugments */ \
+      0x1fu, 0x0cu, 0x75, 0xe7u           /* Tail signature */
+
+// KEEP BLANK LINE ABOVE, it is in the macro!
+
+#endif  // OPENTITAN_SW_DEVICE_LIB_TESTING_USB_TESTUTILS_CONTROLEP_H_
diff --git a/sw/device/lib/testing/usb_testutils_diags.h b/sw/device/lib/testing/usb_testutils_diags.h
new file mode 100644
index 0000000..9d2e375
--- /dev/null
+++ b/sw/device/lib/testing/usb_testutils_diags.h
@@ -0,0 +1,29 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef OPENTITAN_SW_DEVICE_LIB_TESTING_USB_TESTUTILS_DIAGS_H_
+#define OPENTITAN_SW_DEVICE_LIB_TESTING_USB_TESTUTILS_DIAGS_H_
+// Diagnostic, testing and performance measurements utilities for verification
+// of usbdev and development of the usb_testutils support software; the
+// requirements of this software are peculiar in that the USBDPI model used in
+// top-level requires packet responses very promptly, so the introduction of
+// logging/tracing code can substantially alter behavior and cause malfunction
+
+// Used for tracing what is going on. This may impact timing which is critical
+// when simulating with the USB DPI module.
+#define USBUTILS_ENABLE_TRC 0
+
+#if USBUTILS_ENABLE_TRC
+// May be useful on FPGA CW310
+#include "sw/device/lib/runtime/log.h"
+#define TRC_S(s) LOG_INFO("%s", s)
+#define TRC_I(i, b) LOG_INFO("0x%x", i)
+#define TRC_C(c) LOG_INFO("%c", c)
+#else
+#define TRC_S(s)
+#define TRC_I(i, b)
+#define TRC_C(c)
+#endif
+
+#endif  // OPENTITAN_SW_DEVICE_LIB_TESTING_USB_TESTUTILS_DIAGS_H_
diff --git a/sw/device/lib/testing/usb_testutils_simpleserial.c b/sw/device/lib/testing/usb_testutils_simpleserial.c
new file mode 100644
index 0000000..05a799d
--- /dev/null
+++ b/sw/device/lib/testing/usb_testutils_simpleserial.c
@@ -0,0 +1,76 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+#include "sw/device/lib/testing/usb_testutils_simpleserial.h"
+
+#include "sw/device/lib/dif/dif_usbdev.h"
+#include "sw/device/lib/testing/test_framework/check.h"
+#include "sw/device/lib/testing/usb_testutils.h"
+
+#define MAX_GATHER 16
+
+static void ss_rx(void *ssctx_v, dif_usbdev_rx_packet_info_t packet_info,
+                  dif_usbdev_buffer_t buffer) {
+  usb_testutils_ss_ctx_t *ssctx = (usb_testutils_ss_ctx_t *)ssctx_v;
+  usb_testutils_ctx_t *ctx = ssctx->ctx;
+
+  while (packet_info.length--) {
+    uint8_t data;
+    size_t bytes_written;
+    CHECK_DIF_OK(dif_usbdev_buffer_read(ctx->dev, ctx->buffer_pool, &buffer,
+                                        &data, sizeof(data), &bytes_written));
+    ssctx->got_byte(data);
+  }
+}
+
+// Called periodically by the main loop to ensure characters don't
+// stick around too long
+static void ss_flush(void *ssctx_v) {
+  usb_testutils_ss_ctx_t *ssctx = (usb_testutils_ss_ctx_t *)ssctx_v;
+  usb_testutils_ctx_t *ctx = ssctx->ctx;
+  if (ssctx->cur_cpos <= 0) {
+    return;
+  }
+  if ((ssctx->cur_cpos & 0x3) != 0) {
+    size_t bytes_written;
+    CHECK_DIF_OK(dif_usbdev_buffer_write(ctx->dev, &ssctx->cur_buf,
+                                         ssctx->chold.data_b, /*src_len=*/4,
+                                         &bytes_written));
+  }
+  CHECK_DIF_OK(dif_usbdev_send(ctx->dev, ssctx->ep, &ssctx->cur_buf));
+  ssctx->cur_cpos = -1;  // given it to the hardware
+}
+
+// Simple send byte will gather data for a while and send
+void usb_testutils_simpleserial_send_byte(usb_testutils_ss_ctx_t *ssctx,
+                                          uint8_t c) {
+  usb_testutils_ctx_t *ctx = ssctx->ctx;
+  if (ssctx->cur_cpos == -1) {
+    CHECK_DIF_OK(
+        dif_usbdev_buffer_request(ctx->dev, ctx->buffer_pool, &ssctx->cur_buf));
+    ssctx->cur_cpos = 0;
+  }
+  ssctx->chold.data_b[ssctx->cur_cpos++ & 0x3] = c;
+  if ((ssctx->cur_cpos & 0x3) == 0) {
+    size_t bytes_written;
+    CHECK_DIF_OK(dif_usbdev_buffer_write(ctx->dev, &ssctx->cur_buf,
+                                         ssctx->chold.data_b, /*src_len=*/4,
+                                         &bytes_written));
+    if (ssctx->cur_cpos >= MAX_GATHER) {
+      CHECK_DIF_OK(dif_usbdev_send(ctx->dev, ssctx->ep, &ssctx->cur_buf));
+      ssctx->cur_cpos = -1;  // given it to the hardware
+    }
+  }
+}
+
+void usb_testutils_simpleserial_init(usb_testutils_ss_ctx_t *ssctx,
+                                     usb_testutils_ctx_t *ctx, int ep,
+                                     void (*got_byte)(uint8_t)) {
+  usb_testutils_endpoint_setup(ctx, ep, kUsbdevOutStream, ssctx, NULL, ss_rx,
+                               ss_flush, NULL);
+  ssctx->ctx = ctx;
+  ssctx->ep = ep;
+  ssctx->got_byte = got_byte;
+  ssctx->cur_cpos = -1;
+}
diff --git a/sw/device/lib/testing/usb_testutils_simpleserial.h b/sw/device/lib/testing/usb_testutils_simpleserial.h
new file mode 100644
index 0000000..3172f87
--- /dev/null
+++ b/sw/device/lib/testing/usb_testutils_simpleserial.h
@@ -0,0 +1,48 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef OPENTITAN_SW_DEVICE_LIB_TESTING_USB_TESTUTILS_SIMPLESERIAL_H_
+#define OPENTITAN_SW_DEVICE_LIB_TESTING_USB_TESTUTILS_SIMPLESERIAL_H_
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "sw/device/lib/dif/dif_usbdev.h"
+#include "sw/device/lib/testing/usb_testutils.h"
+
+// This is only here because caller of _init needs it
+typedef struct usb_testutils_ss_ctx {
+  usb_testutils_ctx_t *ctx;
+  int ep;
+  dif_usbdev_buffer_t cur_buf;
+  int cur_cpos;
+  union usb_ss_b2w {
+    uint32_t data_w;
+    uint8_t data_b[4];
+  } chold;
+  void (*got_byte)(uint8_t);
+} usb_testutils_ss_ctx_t;
+
+/**
+ * Send a byte on a simpleserial endpoint
+ *
+ * @param ssctx instance context
+ * @param c byte to send
+ */
+void usb_testutils_simpleserial_send_byte(usb_testutils_ss_ctx_t *ssctx,
+                                          uint8_t c);
+
+/**
+ * Initialize a simpleserial endpoint
+ *
+ * @param ssctx unintialized simpleserial instance context
+ * @param ctx initialized usbdev context
+ * @param ep endpoint number for this instance
+ * @param got_byte callback function for when a byte is received
+ */
+void usb_testutils_simpleserial_init(usb_testutils_ss_ctx_t *ssctx,
+                                     usb_testutils_ctx_t *ctx, int ep,
+                                     void (*got_byte)(uint8_t));
+
+#endif  // OPENTITAN_SW_DEVICE_LIB_TESTING_USB_TESTUTILS_SIMPLESERIAL_H_
diff --git a/sw/device/tests/usbdev_stream_test.c b/sw/device/tests/usbdev_stream_test.c
new file mode 100644
index 0000000..a119324
--- /dev/null
+++ b/sw/device/tests/usbdev_stream_test.c
@@ -0,0 +1,729 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+//
+// USB streaming data test
+//
+// This test requires interaction with the USB DPI model or a test application
+// on the USB host. The test initializes the USB device and configures a set of
+// endpoints for data streaming using bulk transfers.
+//
+// The DPI model mimicks a USB host. After device initialization, it detects
+// the assertion of the pullup and first assigns an address to the device.
+// For this test it will then repeatedly fetch data via IN requests to
+// each stream and propagate that data to the corresponding OUT endpoints.
+//
+// The data itself is pseudo-randomly generated by the sender and,
+// independently, by the receiving code to check that the data has been
+// propagated unmodified and without data loss, corruption, replication etc.
+
+#include "sw/device/lib/dif/dif_pinmux.h"
+#include "sw/device/lib/runtime/log.h"
+#include "sw/device/lib/runtime/print.h"
+#include "sw/device/lib/testing/pinmux_testutils.h"
+#include "sw/device/lib/testing/test_framework/check.h"
+#include "sw/device/lib/testing/test_framework/ottf_main.h"
+#include "sw/device/lib/testing/usb_testutils.h"
+#include "sw/device/lib/testing/usb_testutils_controlep.h"
+
+#include "hw/top_earlgrey/sw/autogen/top_earlgrey.h"  // Generated.
+
+// Maximum number of concurrent streams
+#ifdef USBDEV_NUM_ENDPOINTS
+// Endpoint zero implements the default control pipe
+#define STREAMS_MAX (USBDEV_NUM_ENDPOINTS - 1U)
+#else
+#define STREAMS_MAX 11U
+#endif
+
+// TODO - currently we are unable to send the configuration descriptor
+// if we try to describe more than two bidirectional endpoints
+#if STREAMS_MAX > 2U
+#undef STREAMS_MAX
+#define STREAMS_MAX 2U
+#endif
+
+// Number of streams to be tested
+#ifndef NUM_STREAMS
+#define NUM_STREAMS STREAMS_MAX
+#endif
+
+// Maximum number of buffer simultaneously awaiting transmission
+// (we must leave some available for packet reception)
+#ifndef MAX_TX_BUFFERS
+#define MAX_TX_BUFFERS 24U
+#endif
+
+// This takes about 256s presently with 10MHz CPU in CW310 FPGA and physical
+// USB with randomized packet sizes and the default memcpy implementation;
+// The _MEM_FASTER switch drops the run time to 187s
+#define TRANSFER_BYTES_FPGA (0x10U << 20)
+
+// This is appropriate for a Verilator chip simulation with 15 min timeout
+#define TRANSFER_BYTES_VERILATOR 0x2400U
+
+// This is about the amount that we can transfer within a 1 hour 'eternal' test
+//#define TRANSFER_BYTES_LONG (0xD0U << 20)
+
+// Stream signature words
+#define STREAM_SIGNATURE_HEAD 0x579EA01AU
+#define STREAM_SIGNATURE_TAIL 0x160AE975U
+
+// Seed numbers for the LFSR generators in each transfer direction for
+// the given stream number
+#define USBTST_LFSR_SEED(s) (uint8_t)(0x10U + (s)*7U)
+#define USBDPI_LFSR_SEED(s) (uint8_t)(0x9BU - (s)*7U)
+
+// Buffer size randomization
+#define BUFSZ_LFSR_SEED(s) (uint8_t)(0x17U + (s)*7U)
+
+// Simple LFSR for 8-bit sequences
+// Note: zero is an isolated state that shall be avoided
+#define LFSR_ADVANCE(lfsr) \
+  (((lfsr) << 1) ^         \
+   ((((lfsr) >> 1) ^ ((lfsr) >> 2) ^ ((lfsr) >> 3) ^ ((lfsr) >> 7)) & 1U))
+
+// Forward declaration to context state
+typedef struct usbdev_stream_test_ctx usbdev_stream_test_ctx_t;
+
+/**
+ * Stream signature
+ * Note: this needs to be transferred over a byte stream
+ */
+typedef struct __attribute__((packed)) usbdev_stream_sig {
+  /**
+   * Head signature word
+   */
+  uint32_t head_sig;
+  /**
+   * Initial value of LFSR
+   */
+  uint8_t init_lfsr;
+  /**
+   * Stream number
+   */
+  uint8_t stream;
+  /**
+   * Reserved fields; should be zero
+   */
+  uint8_t reserved1;
+  uint8_t reserved2;
+  /**
+   * Number of bytes to be transferred
+   */
+  uint32_t num_bytes;
+  /**
+   * Tail signature word
+   */
+  uint32_t tail_sig;
+} usbdev_stream_sig_t;
+
+// Sanity check because the host-side code relies upon the same structure
+static_assert(sizeof(usbdev_stream_sig_t) == 0x10U,
+              "Host-side code relies upon signature structure");
+
+/**
+ * Context state for a single stream
+ */
+typedef struct usbdev_stream {
+  /**
+   * Pointer to test context; callback functions receive only stream pointer
+   */
+  usbdev_stream_test_ctx_t *ctx;
+  /**
+   * Stream IDentifier
+   */
+  uint8_t id;
+  /**
+   * Has the stream signature been sent yet?
+   */
+  bool sent_sig;
+  /**
+   * USB device endpoint being used for data transmission
+   */
+  uint8_t tx_ep;
+  /**
+   * Transmission Linear Feedback Shift Register (for PRND data generation)
+   */
+  uint8_t tx_lfsr;
+  /**
+   * Total number of bytes presented to the USB device for transmission
+   */
+  uint32_t tx_bytes;
+  /**
+   * Transmission-side LFSR for selection of buffer size
+   */
+  uint8_t tx_buf_size;
+
+  /**
+   * USB device endpoint being used for data reception
+   */
+  uint8_t rx_ep;
+  /**
+   * Reception-side LFSR state (mirrors USBDPI generation of PRND data)
+   */
+  uint8_t rx_lfsr;
+  /**
+   * Reception-side shadow of transmission LFSR
+   */
+  uint8_t rxtx_lfsr;
+  /**
+   * Total number of bytes received from the USB device
+   */
+  uint32_t rx_bytes;
+  /**
+   * Size of transfer in bytes
+   */
+  uint32_t transfer_bytes;
+} usbdev_stream_t;
+
+/**
+ * Context state for streaming test
+ */
+struct usbdev_stream_test_ctx {
+  /**
+   * Context pointer
+   */
+  usb_testutils_ctx_t *usbdev;
+  /**
+   * State information for each of the test streams
+   */
+  usbdev_stream_t streams[STREAMS_MAX];
+  /**
+   * Per-endpoint limits on the number of buffers that may be queued for
+   * transmission
+   */
+  uint8_t tx_bufs_limit[USBDEV_NUM_ENDPOINTS];
+  /**
+   * Per-endpoint counts of completed buffers queued for transmission
+   */
+  uint8_t tx_bufs_queued[USBDEV_NUM_ENDPOINTS];
+  /**
+   * Total number of completed buffers
+   */
+  uint8_t tx_queued_total;
+  /**
+   * Buffers that have been filled but cannot yet be presented for transmission
+   * TODO - perhaps absorb the buffer queuing into usb_testutils because the dif
+   * API is explicitly not robust against back-to-back sending of multiple
+   * buffers to a single endpoint, and because the read performance is reliant
+   * upon having additional buffer(s) already available for immediate
+   * presentation
+   */
+  // 12 X 24 X 4 (or 8?)( BYTES... could perhaps simplify this at some point
+  dif_usbdev_buffer_t tx_bufs[USBDEV_NUM_ENDPOINTS][MAX_TX_BUFFERS];
+};
+
+/**
+ * Configuration values for USB.
+ * TODO - dynamically construct a config descriptor appropriate to the test;
+ *        this would avoid creating unusable ports on the host and also provide
+ *        a little more testing
+ */
+static const uint8_t config_descriptors[] = {
+    USB_CFG_DSCR_HEAD(USB_CFG_DSCR_LEN + STREAMS_MAX * (USB_INTERFACE_DSCR_LEN +
+                                                        2 * USB_EP_DSCR_LEN),
+                      STREAMS_MAX),
+
+    VEND_INTERFACE_DSCR(0, 2, 0x50, 1),
+    USB_BULK_EP_DSCR(0, 1U, USBDEV_MAX_PACKET_SIZE, 0),
+    USB_BULK_EP_DSCR(1, 1U, USBDEV_MAX_PACKET_SIZE, 0),
+
+    VEND_INTERFACE_DSCR(1, 2, 0x50, 1),
+    USB_BULK_EP_DSCR(0, 2U, USBDEV_MAX_PACKET_SIZE, 0),
+    USB_BULK_EP_DSCR(1, 2U, USBDEV_MAX_PACKET_SIZE, 0),
+};
+
+/**
+ * Test descriptor
+ */
+static const uint8_t test_descriptor[] = {
+    USB_TESTUTILS_TEST_DSCR(1, NUM_STREAMS | 0xF0U, 0, 0, 0)};
+
+/**
+ * USB device context types.
+ */
+static usb_testutils_ctx_t usbdev;
+static usb_testutils_controlep_ctx_t usbdev_control;
+
+/**
+ * Pinmux handle
+ */
+static dif_pinmux_t pinmux;
+
+/**
+ * State information for streaming data test
+ */
+static usbdev_stream_test_ctx_t stream_test;
+
+/**
+ * Specify whether to perform verbose logging, for visibility
+ *   (Note that this substantially alters the timing of interactions with the
+ * DPI model and will increase the simulation time)
+ */
+static bool verbose = false;
+
+/**
+ * Send only maximal length packets?
+ * (important for performance measurements on the USB, but obviously undesirable
+ *  for testing reliability/function)
+ */
+static bool max_packets = false;
+
+/**
+ * Number of streams to be created
+ */
+static const unsigned nstreams = NUM_STREAMS;
+
+/**
+ * Diagnostic logging; expensive
+ */
+static bool log_traffic = false;
+
+// Dump a sequence of bytes as hexadecimal and ASCII for diagnostic purposes
+static void buffer_dump(const uint8_t *data, size_t n) {
+  base_hexdump_fmt_t fmt = {
+      .bytes_per_word = 1,
+      .words_per_line = 0x20u,
+      .alphabet = &kBaseHexdumpDefaultFmtAlphabet,
+  };
+
+  base_hexdump_with(fmt, (char *)data, n);
+}
+
+// Create a stream signature buffer
+static uint32_t buffer_sig_create(usbdev_stream_t *s,
+                                  dif_usbdev_buffer_t *buf) {
+  usbdev_stream_sig_t sig;
+
+  sig.head_sig = STREAM_SIGNATURE_HEAD;
+  sig.init_lfsr = s->tx_lfsr;
+  sig.stream = s->id;
+  sig.reserved1 = 0U;
+  sig.reserved2 = 0U;
+  sig.num_bytes = s->transfer_bytes;
+  sig.tail_sig = STREAM_SIGNATURE_TAIL;
+
+  size_t bytes_written;
+  CHECK_DIF_OK(dif_usbdev_buffer_write(usbdev.dev, buf, (uint8_t *)&sig,
+                                       sizeof(sig), &bytes_written));
+  CHECK(bytes_written == sizeof(sig));
+
+  // Note: stream signature is not included in the count of bytes transferred
+
+  return bytes_written;
+}
+
+// Fill a buffer with LFSR-generated data
+static void buffer_fill(usbdev_stream_t *s, dif_usbdev_buffer_t *buf,
+                        uint8_t num_bytes) {
+  alignas(uint32_t) uint8_t data[USBDEV_MAX_PACKET_SIZE];
+
+  CHECK(num_bytes <= buf->remaining_bytes);
+  CHECK(num_bytes <= sizeof(data));
+
+  if (true) {
+    // Emit LFSR-generated byte stream; keep this brief so that we can
+    // reduce our latency in responding to USB events (usb_testutils employs
+    // polling at present)
+    uint8_t lfsr = s->tx_lfsr;
+
+    const uint8_t *edp = &data[num_bytes];
+    uint8_t *dp = data;
+    while (dp < edp) {
+      *dp++ = lfsr;
+      lfsr = LFSR_ADVANCE(lfsr);
+    }
+
+    // Update the LFSR for the next packet
+    s->tx_lfsr = lfsr;
+  } else {
+    // Undefined buffer contents; useful for profiling IN throughput on
+    // CW310, because the CPU load at 10MHz can be an appreciable slowdown
+  }
+
+  if (verbose && log_traffic) {
+    buffer_dump(data, num_bytes);
+  }
+
+  size_t bytes_written;
+
+  CHECK_DIF_OK(dif_usbdev_buffer_write(usbdev.dev, buf, data, num_bytes,
+                                       &bytes_written));
+  CHECK(bytes_written == num_bytes);
+  s->tx_bytes += bytes_written;
+}
+
+// Check the contents of a received buffer
+static void buffer_check(usbdev_stream_test_ctx_t *ctx, usbdev_stream_t *s,
+                         dif_usbdev_rx_packet_info_t packet_info,
+                         dif_usbdev_buffer_t buf) {
+  usb_testutils_ctx_t *usbdev = ctx->usbdev;
+  uint8_t len = packet_info.length;
+
+  if (len > 0) {
+    alignas(uint32_t) uint8_t data[USBDEV_MAX_PACKET_SIZE];
+
+    CHECK(len <= sizeof(data));
+
+    size_t bytes_read;
+
+    // Notes: the buffer being read here is USBDEV memory accessed as MMIO, so
+    //        only the DIF accesses it directly. when we consume the final bytes
+    //        from the read buffer, it is automatically returned to the buffer
+    //        pool.
+    CHECK_DIF_OK(dif_usbdev_buffer_read(usbdev->dev, usbdev->buffer_pool, &buf,
+                                        data, len, &bytes_read));
+    CHECK(bytes_read == len);
+
+    if (log_traffic) {
+      buffer_dump(data, bytes_read);
+    }
+
+    // Check received data against expected LFSR-generated byte stream;
+    // keep this brief so that we can reduce our latency in responding to
+    // USB events (usb_testutils employs polling at present)
+    uint8_t rxtx_lfsr = s->rxtx_lfsr;
+    uint8_t rx_lfsr = s->rx_lfsr;
+
+    const uint8_t *esp = &data[bytes_read];
+    const uint8_t *sp = data;
+    while (sp < esp) {
+      // Received data should be the XOR of two LFSR-generated PRND streams -
+      // ours on the
+      //   transmission side, and that of the DPI model
+      uint8_t expected = rxtx_lfsr ^ rx_lfsr;
+      CHECK(expected == *sp,
+            "S%u: Unexpected received data 0x%02x : (LFSRs 0x%02x 0x%02x)",
+            s->id, *sp, rxtx_lfsr, rx_lfsr);
+
+      rxtx_lfsr = LFSR_ADVANCE(rxtx_lfsr);
+      rx_lfsr = LFSR_ADVANCE(rx_lfsr);
+      sp++;
+    }
+
+    // Update the LFSRs for the next packet
+    s->rxtx_lfsr = rxtx_lfsr;
+    s->rx_lfsr = rx_lfsr;
+  } else {
+    // In the event that we've received a zero-length data packet, we still
+    // must return the buffer to the pool
+    CHECK_DIF_OK(
+        dif_usbdev_buffer_return(usbdev->dev, usbdev->buffer_pool, &buf));
+  }
+}
+
+// Callback for successful buffer transmission
+static void strm_tx_done(void *stream_v) {
+  usbdev_stream_t *s = (usbdev_stream_t *)stream_v;
+  usbdev_stream_test_ctx_t *ctx = s->ctx;
+  usb_testutils_ctx_t *usbdev = ctx->usbdev;
+
+  // If we do not have at least one queued buffer then something has gone wrong
+  // and this callback is inappropriate
+  uint8_t tx_ep = s->tx_ep;
+  uint8_t nqueued = ctx->tx_bufs_queued[tx_ep];
+
+  if (verbose) {
+    LOG_INFO("strm_tx_done called. %u (%u total) buffers(s) are queued",
+             nqueued, ctx->tx_queued_total);
+  }
+
+  CHECK(nqueued > 0);
+
+  // Note: since buffer transmission and completion signalling both occur within
+  // the foreground code (polling, not interrupt-driven) there is no issue of
+  // potential races here
+
+  if (nqueued > 0) {
+    // Shuffle the buffer descriptions, without using memmove
+    for (unsigned idx = 1u; idx < nqueued; idx++) {
+      ctx->tx_bufs[tx_ep][idx - 1u] = ctx->tx_bufs[tx_ep][idx];
+    }
+
+    // Is there another buffer ready to be transmitted?
+    ctx->tx_queued_total--;
+    ctx->tx_bufs_queued[tx_ep] = --nqueued;
+
+    if (nqueued) {
+      CHECK_DIF_OK(
+          dif_usbdev_send(usbdev->dev, tx_ep, &ctx->tx_bufs[tx_ep][0u]));
+    }
+  }
+}
+
+// Callback for buffer reception
+static void strm_rx(void *stream_v, dif_usbdev_rx_packet_info_t packet_info,
+                    dif_usbdev_buffer_t buf) {
+  usbdev_stream_t *s = (usbdev_stream_t *)stream_v;
+  usbdev_stream_test_ctx_t *ctx = s->ctx;
+  usb_testutils_ctx_t *usbdev = ctx->usbdev;
+
+  CHECK(packet_info.endpoint == s->rx_ep);
+
+  // We do not expect to receive SETUP packets to this endpoint
+  CHECK(!packet_info.is_setup);
+
+  if (verbose) {
+    LOG_INFO("Stream %u: Received buffer of %u bytes(s)", s->id,
+             packet_info.length);
+  }
+
+  if (true) {
+    buffer_check(ctx, s, packet_info, buf);
+  } else {
+    // Note: this is just test code for measuring the OUT throughput
+    usb_testutils_ctx_t *usbdev = ctx->usbdev;
+    CHECK_DIF_OK(
+        dif_usbdev_buffer_return(usbdev->dev, usbdev->buffer_pool, &buf));
+  }
+
+  s->rx_bytes += packet_info.length;
+}
+
+// Callback for unexpected data reception (IN endpoint)
+static void rx_show(void *stream_v, dif_usbdev_rx_packet_info_t packet_info,
+                    dif_usbdev_buffer_t buf) {
+  usbdev_stream_t *s = (usbdev_stream_t *)stream_v;
+  usbdev_stream_test_ctx_t *ctx = s->ctx;
+  usb_testutils_ctx_t *usbdev = ctx->usbdev;
+  uint8_t data[0x100U];
+  size_t bytes_read;
+  CHECK_DIF_OK(dif_usbdev_buffer_read(usbdev->dev, usbdev->buffer_pool, &buf,
+                                      data, packet_info.length, &bytes_read));
+  LOG_INFO("rx_show packet of %u byte(s) - read %u", packet_info.length,
+           bytes_read);
+  buffer_dump(data, bytes_read);
+}
+
+// Returns an indication of whether a stream has completed its data transfer
+bool stream_completed(const usbdev_stream_t *s) {
+  return (s->tx_bytes >= s->transfer_bytes) &&
+         (s->rx_bytes >= s->transfer_bytes);
+}
+
+// Initialise a stream, preparing it for use
+static void stream_init(usbdev_stream_test_ctx_t *ctx, usbdev_stream_t *s,
+                        uint8_t id, uint8_t ep_in, uint8_t ep_out,
+                        uint32_t transfer_bytes) {
+  // We need to be able to locate the test context given only the stream
+  // pointer within the strm_tx_done callback from usb_testutils
+  s->ctx = ctx;
+
+  // Remember the stream IDentifier
+  s->id = id;
+
+  // Not yet sent stream signature
+  s->sent_sig = false;
+
+  // Initialise the transfer state
+  s->tx_bytes = 0u;
+  s->rx_bytes = 0u;
+  s->transfer_bytes = transfer_bytes;
+
+  // Initialise the LFSR state for transmission and reception sides
+  // - we use a simple LFSR to generate a PRND stream to transmit to the USBPI
+  // - the USBDPI XORs the received data with another LFSR-generated stream of
+  //   its own, and transmits the result back to us
+  // - to check the returned data, our reception code mimics both LFSRs
+  s->tx_lfsr = USBTST_LFSR_SEED(id);
+  s->rxtx_lfsr = s->tx_lfsr;
+  s->rx_lfsr = USBDPI_LFSR_SEED(id);
+
+  // Packet size randomization
+  s->tx_buf_size = BUFSZ_LFSR_SEED(id);
+
+  // Set up the endpoint for IN transfers (TO host)
+  //
+  // Note: We install the rx_show handler to catch any misdirected data
+  // transfers
+  void (*rx)(void *, dif_usbdev_rx_packet_info_t, dif_usbdev_buffer_t) =
+      (ep_in == ep_out) ? strm_rx : rx_show;
+
+  s->tx_ep = ep_in;
+  usb_testutils_endpoint_setup(ctx->usbdev, ep_in, kUsbdevOutStream, s,
+                               strm_tx_done, rx, NULL, NULL);
+  s->rx_ep = ep_out;
+  if (ep_out != ep_in) {
+    // Set up the endpoint for OUT transfers (FROM host)
+    usb_testutils_endpoint_setup(ctx->usbdev, ep_out, kUsbdevOutStream, s, NULL,
+                                 strm_rx, NULL, NULL);
+  }
+}
+
+// Service the given stream, preparing and/or sending any data that we can;
+// data reception is handled via callbacks and requires no attention here
+static void stream_service(usbdev_stream_test_ctx_t *ctx, usbdev_stream_t *s) {
+  // Generate output data as soon as possible and make it available for
+  //   collection by the host
+
+  uint8_t tx_ep = s->tx_ep;
+  uint8_t nqueued = ctx->tx_bufs_queued[tx_ep];
+
+  if (s->tx_bytes < s->transfer_bytes &&        // More bytes to transfer?
+      nqueued < ctx->tx_bufs_limit[tx_ep] &&    // Endpoint allowed buffer?
+      ctx->tx_queued_total < MAX_TX_BUFFERS) {  // Total buffers not exceeded?
+    dif_usbdev_buffer_t buf;
+
+    // See whether we can populate another buffer yet
+    dif_result_t dif_result =
+        dif_usbdev_buffer_request(usbdev.dev, usbdev.buffer_pool, &buf);
+    if (dif_result == kDifOk) {
+      // This is just for reporting the number of buffers presented to the
+      // USB device, as a progress indicator
+      static unsigned bufs_sent = 0u;
+      uint32_t num_bytes;
+
+      if (s->sent_sig) {
+        if (max_packets) {
+          num_bytes = USBDEV_MAX_PACKET_SIZE;
+        } else {
+          // Vary the amount of data sent per buffer
+          num_bytes = s->tx_buf_size % (USBDEV_MAX_PACKET_SIZE + 1u);
+          s->tx_buf_size = LFSR_ADVANCE(s->tx_buf_size);
+        }
+        uint32_t tx_left = s->transfer_bytes - s->tx_bytes;
+        if (num_bytes > tx_left)
+          num_bytes = tx_left;
+
+        buffer_fill(s, &buf, num_bytes);
+      } else {
+        // Construct a signature to send to the host-side software,
+        // identifying the stream and its properties
+        num_bytes = buffer_sig_create(s, &buf);
+        s->sent_sig = true;
+      }
+
+      // Remember the buffer until we're informed that it has been
+      // successfully transmitted
+      //
+      // Note: since the 'tx_done' callback occurs from foreground code that
+      // is polling, there is no issue of interrupt races here
+      ctx->tx_bufs[tx_ep][nqueued] = buf;
+      ctx->tx_bufs_queued[tx_ep] = ++nqueued;
+      ctx->tx_queued_total++;
+
+      // Can we present this buffer for transmission yet?
+      if (nqueued <= 1U) {
+        CHECK_DIF_OK(dif_usbdev_send(usbdev.dev, tx_ep, &buf));
+      }
+
+      if (verbose) {
+        LOG_INFO(
+            "Stream %u: %uth buffer (of 0x%x byte(s)) awaiting transmission",
+            s->id, bufs_sent, num_bytes);
+      }
+      bufs_sent++;
+    } else {
+      // If we have no more buffers available right now, continue polling...
+      CHECK(dif_result == kDifUnavailable);
+    }
+  }
+}
+
+OTTF_DEFINE_TEST_CONFIG();
+
+bool test_main(void) {
+  // Context state for streaming test
+  usbdev_stream_test_ctx_t *ctx = &stream_test;
+
+  CHECK(kDeviceType == kDeviceSimVerilator || kDeviceType == kDeviceFpgaCw310,
+        "This test is not expected to run on platforms other than the "
+        "Verilator simulation or CW310 FPGA. It needs logic on the host side "
+        "to retrieve, scramble and return the generated byte stream");
+
+  LOG_INFO("Running USBDEV Stream Test");
+
+  // Check we can support the requested number of streams
+  CHECK(nstreams && nstreams < USBDEV_NUM_ENDPOINTS);
+
+  // Decide upon the number of bytes to be transferred for the entire test
+  uint32_t transfer_bytes = TRANSFER_BYTES_FPGA;
+  if (kDeviceType == kDeviceSimVerilator) {
+    transfer_bytes = TRANSFER_BYTES_VERILATOR;
+  }
+  transfer_bytes = (transfer_bytes + nstreams - 1) / nstreams;
+  LOG_INFO(" - %u stream(s), 0x%x bytes each", nstreams, transfer_bytes);
+
+  CHECK_DIF_OK(dif_pinmux_init(
+      mmio_region_from_addr(TOP_EARLGREY_PINMUX_AON_BASE_ADDR), &pinmux));
+  pinmux_testutils_init(&pinmux);
+  CHECK_DIF_OK(dif_pinmux_input_select(
+      &pinmux, kTopEarlgreyPinmuxPeripheralInUsbdevSense,
+      kTopEarlgreyPinmuxInselIoc7));
+
+  // Remember context state for usb_testutils context
+  ctx->usbdev = &usbdev;
+
+  // Call `usbdev_init` here so that DPI will not start until the
+  // simulation has finished all of the printing, which takes a while
+  // if `--trace` was passed in.
+  usb_testutils_init(ctx->usbdev, /*pinflip=*/false, /*en_diff_rcvr=*/false,
+                     /*tx_use_d_se0=*/false);
+  usb_testutils_controlep_init(&usbdev_control, ctx->usbdev, 0,
+                               config_descriptors, sizeof(config_descriptors),
+                               test_descriptor, sizeof(test_descriptor));
+  while (usbdev_control.device_state != kUsbTestutilsDeviceConfigured) {
+    usb_testutils_poll(ctx->usbdev);
+  }
+
+  // Initialise the state of each stream
+  for (unsigned id = 0U; id < nstreams; id++) {
+    // Which endpoint are we using for the IN transfers to the host?
+    const uint8_t ep_in = 1u + id;
+    // Which endpoint are we using for the OUT transfers from the host?
+    const uint8_t ep_out = 1u + id;
+    stream_init(ctx, &ctx->streams[id], id, ep_in, ep_out, transfer_bytes);
+  }
+
+  // Decide how many buffers each endpoint may queue up for transmission;
+  // we must ensure that there are buffers available for reception, and we
+  // do not want any endpoint to starve another
+  for (unsigned s = 0U; s < nstreams; s++) {
+    // This is slightly overspending the available buffers, leaving the
+    //   endpoints to vie for the final few buffers, so it's important that
+    //   we limit the total number of buffers across all endpoints too
+    unsigned ep = ctx->streams[s].tx_ep;
+    ctx->tx_bufs_queued[ep] = 0U;
+    ctx->tx_bufs_limit[ep] = (MAX_TX_BUFFERS + nstreams - 1) / nstreams;
+  }
+  ctx->tx_queued_total = 0U;
+
+  if (verbose) {
+    LOG_INFO("Commencing data transfer...");
+  }
+
+  bool done = false;
+  do {
+    for (unsigned s = 0U; s < nstreams; s++) {
+      stream_service(ctx, &ctx->streams[s]);
+
+      // We must keep polling regularly in order to handle detection of packet
+      // transmission as well as perform packet reception and checking
+      usb_testutils_poll(ctx->usbdev);
+    }
+
+    // See whether any streams still have more work to do
+    unsigned s = 0U;
+    while (s < nstreams && stream_completed(&ctx->streams[s])) {
+      s++;
+    }
+    done = (s >= nstreams);
+  } while (!done);
+
+  // Determine the total counts of bytes sent and received
+  uint32_t tx_bytes = 0U;
+  uint32_t rx_bytes = 0U;
+  for (unsigned s = 0U; s < nstreams; s++) {
+    tx_bytes += ctx->streams[s].tx_bytes;
+    rx_bytes += ctx->streams[s].rx_bytes;
+  }
+
+  LOG_INFO("USB sent 0x%x byte(s), received and checked 0x%x byte(s)", tx_bytes,
+           rx_bytes);
+
+  CHECK(tx_bytes == nstreams * transfer_bytes,
+        "Unexpected count of byte(s) sent to USB host");
+
+  return true;
+}
diff --git a/sw/device/tests/usbdev_test.c b/sw/device/tests/usbdev_test.c
new file mode 100644
index 0000000..af52cf5
--- /dev/null
+++ b/sw/device/tests/usbdev_test.c
@@ -0,0 +1,141 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+//
+// USB device test
+//
+// This test is a stripped down version of the hello_usbdev example application.
+// It requires interaction with the USB DPI model mimicking the host and thus
+// can only be run in the Verilator simulation. The test initializes the USB
+// device and configures USB Endpoint 1 as a simpleserial endpoint. The test
+// then starts polling the USB device for data sent by the host. Any data
+// received on Endpoint 1 is stored in a buffer and printed via UART.
+//
+// The DPI model mimicks the USB host. After device initialization, it detects
+// the assertion of the pullup and first assigns an address to the device. It
+// then sends various USB transactions to the device including two OUT
+// transactions with a data payload of "Hi!" to Endpoint 1. If these two OUT
+// transactions are succesfully received by the device, the test passes.
+
+#include "sw/device/lib/dif/dif_pinmux.h"
+#include "sw/device/lib/runtime/log.h"
+#include "sw/device/lib/runtime/print.h"
+#include "sw/device/lib/testing/pinmux_testutils.h"
+#include "sw/device/lib/testing/test_framework/check.h"
+#include "sw/device/lib/testing/test_framework/ottf_main.h"
+#include "sw/device/lib/testing/usb_testutils.h"
+#include "sw/device/lib/testing/usb_testutils_controlep.h"
+#include "sw/device/lib/testing/usb_testutils_simpleserial.h"
+
+#include "hw/top_earlgrey/sw/autogen/top_earlgrey.h"  // Generated.
+
+/**
+ * Configuration values for USB.
+ */
+static const uint8_t config_descriptors[] = {
+    USB_CFG_DSCR_HEAD(
+        USB_CFG_DSCR_LEN + 2 * (USB_INTERFACE_DSCR_LEN + 2 * USB_EP_DSCR_LEN),
+        2),
+    VEND_INTERFACE_DSCR(0, 2, 0x50, 1),
+    USB_BULK_EP_DSCR(0, 1, 32, 0),
+    USB_BULK_EP_DSCR(1, 1, 32, 4),
+    VEND_INTERFACE_DSCR(1, 2, 0x50, 1),
+    USB_BULK_EP_DSCR(0, 2, 32, 0),
+    USB_BULK_EP_DSCR(1, 2, 32, 4),
+};
+
+/**
+ * Test descriptor
+ */
+static const uint8_t test_descriptor[] = {
+    USB_TESTUTILS_TEST_DSCR(0, 0, 0, 0, 0)};
+
+/**
+ * USB device context types.
+ */
+static usb_testutils_ctx_t usbdev;
+static usb_testutils_controlep_ctx_t usbdev_control;
+static usb_testutils_ss_ctx_t simple_serial;
+
+/**
+ * Pinmux handle
+ */
+static dif_pinmux_t pinmux;
+
+/**
+ * Makes `c` into a printable character, replacing it with `replacement`
+ * as necessary.
+ */
+static char make_printable(char c, char replacement) {
+  if (c == 0xa || c == 0xd) {
+    return c;
+  }
+
+  if (c < ' ' || c > '~') {
+    c = replacement;
+  }
+  return c;
+}
+
+static const size_t kExpectedUsbCharsRecved = 6;
+static const char kExpectedUsbRecved[7] = "Hi!Hi!";
+static size_t usb_chars_recved_total;
+static char buffer[7];
+
+/**
+ * Callback for processing USB reciept.
+ */
+static void usb_receipt_callback(uint8_t c) {
+  c = make_printable(c, '?');
+  base_printf("%c", c);
+  if (usb_chars_recved_total < kExpectedUsbCharsRecved) {
+    buffer[usb_chars_recved_total] = c;
+    ++usb_chars_recved_total;
+  }
+}
+
+OTTF_DEFINE_TEST_CONFIG();
+
+bool test_main(void) {
+  CHECK(kDeviceType == kDeviceSimVerilator || kDeviceType == kDeviceFpgaCw310,
+        "This test is not expected to run on platforms other than the "
+        "Verilator simulation or CW310 FPGA. It needs the USB DPI model "
+        "or host application.");
+
+  LOG_INFO("Running USBDEV test");
+
+  CHECK_DIF_OK(dif_pinmux_init(
+      mmio_region_from_addr(TOP_EARLGREY_PINMUX_AON_BASE_ADDR), &pinmux));
+  pinmux_testutils_init(&pinmux);
+  CHECK_DIF_OK(dif_pinmux_input_select(
+      &pinmux, kTopEarlgreyPinmuxPeripheralInUsbdevSense,
+      kTopEarlgreyPinmuxInselIoc7));
+
+  // Call `usbdev_init` here so that DPI will not start until the
+  // simulation has finished all of the printing, which takes a while
+  // if `--trace` was passed in.
+  usb_testutils_init(&usbdev, /*pinflip=*/false, /*en_diff_rcvr=*/false,
+                     /*tx_use_d_se0=*/false);
+  usb_testutils_controlep_init(&usbdev_control, &usbdev, 0, config_descriptors,
+                               sizeof(config_descriptors), test_descriptor,
+                               sizeof(test_descriptor));
+  while (usbdev_control.device_state != kUsbTestutilsDeviceConfigured) {
+    usb_testutils_poll(&usbdev);
+  }
+  usb_testutils_simpleserial_init(&simple_serial, &usbdev, 1,
+                                  usb_receipt_callback);
+
+  while (usb_chars_recved_total < kExpectedUsbCharsRecved) {
+    usb_testutils_poll(&usbdev);
+  }
+
+  base_printf("\r\n");
+  for (int i = 0; i < kExpectedUsbCharsRecved; i++) {
+    CHECK(buffer[i] == kExpectedUsbRecved[i],
+          "Received char #%d mismatched: exp = %x, actual = %x", i,
+          kExpectedUsbRecved[i], buffer[i]);
+  }
+  LOG_INFO("USB received %d characters: %s", usb_chars_recved_total, buffer);
+
+  return true;
+}