[usb_testutils] Support larger data stages

Data stages of multiple packets are required for larger
configuration descriptors and higher software layers.
Includes double-buffering support for higher IN data rates.
Modify tx_done callback to accept a result code.

Change-Id: I1cbdbc68051b0630cc917f09c6a880adb91ea849
Signed-off-by: Adrian Lees <a.lees@lowrisc.org>
diff --git a/sw/device/lib/testing/usb_testutils.c b/sw/device/lib/testing/usb_testutils.c
index 4831f9b..bd1a1fa 100644
--- a/sw/device/lib/testing/usb_testutils.c
+++ b/sw/device/lib/testing/usb_testutils.c
@@ -14,6 +14,78 @@
 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;
 
@@ -21,18 +93,9 @@
   CHECK_DIF_OK(dif_usbdev_irq_get_state(ctx->dev, &istate));
 
   if (!istate) {
-    // Nothing new to do right now, but keep buffers available for reception
-    CHECK_DIF_OK(dif_usbdev_fill_available_fifo(ctx->dev, ctx->buffer_pool));
     return;
   }
 
-  // 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;
-  }
-
   // 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
@@ -40,28 +103,47 @@
     uint16_t sentep;
     CHECK_DIF_OK(dif_usbdev_get_tx_sent(ctx->dev, &sentep));
     TRC_C('a' + sentep);
-    for (unsigned ep = 0; ep < USBDEV_NUM_ENDPOINTS; ep++) {
-      if (sentep & (1 << ep)) {
+    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 (ctx->in[ep].tx_done_callback) {
-          ctx->in[ep].tx_done_callback(ctx->in[ep].ep_ctx);
+        // 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++;
     }
-
-    // Clear the interrupt sooner so that we can respond more promptly to
-    // subsequent transmitted packets
-    CHECK_DIF_OK(dif_usbdev_irq_acknowledge(ctx->dev, kDifUsbdevIrqPktSent));
-    istate &= ~(1u << kDifUsbdevIrqPktSent);
   }
 
   // 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));
@@ -104,6 +186,13 @@
   // 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
@@ -158,10 +247,38 @@
   // TODO Errors? What Errors?
 }
 
-void usb_testutils_in_endpoint_setup(usb_testutils_ctx_t *ctx, uint8_t ep,
-                                     void *ep_ctx, void (*tx_done)(void *),
-                                     void (*flush)(void *),
-                                     void (*reset)(void *)) {
+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;
@@ -217,7 +334,7 @@
 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 *),
+    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);
diff --git a/sw/device/lib/testing/usb_testutils.h b/sw/device/lib/testing/usb_testutils.h
index ee3cb0c..6dcfc96 100644
--- a/sw/device/lib/testing/usb_testutils.h
+++ b/sw/device/lib/testing/usb_testutils.h
@@ -12,6 +12,86 @@
 #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 {
@@ -38,7 +118,7 @@
     /**
      * Callback for transmission of IN packet
      */
-    void (*tx_done_callback)(void *);
+    void (*tx_done_callback)(void *, usb_testutils_xfr_result_t);
     /**
      * Callback for periodically flushing IN data to host
      */
@@ -47,6 +127,10 @@
      * Callback for link reset
      */
     void (*reset)(void *);
+    /**
+     * Current in-progress transfer, if any
+     */
+    usb_testutils_transfer_t transfer;
   } in[USBDEV_NUM_ENDPOINTS];
 
   /**
@@ -91,22 +175,22 @@
 /**
  * Call to set up IN endpoint.
  *
- * @param ctx usbdev context pointer
+ * @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 *),
-                                     void (*flush)(void *),
-                                     void (*reset)(void *));
+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 usbdev context pointer
+ * @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
@@ -123,7 +207,7 @@
 /**
  * Call to set up a pair of IN and OUT endpoints.
  *
- * @param ctx usbdev context pointer
+ * @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
@@ -133,18 +217,17 @@
  * @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 *),
-                                  void (*rx)(void *,
-                                             dif_usbdev_rx_packet_info_t,
-                                             dif_usbdev_buffer_t),
-                                  void (*flush)(void *), void (*reset)(void *));
+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 usbdev context pointer
+ * @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);
@@ -152,7 +235,7 @@
 /**
  * Remove an OUT endpoint.
  *
- * @param ctx usbdev context pointer
+ * @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);
@@ -160,7 +243,7 @@
 /**
  * Remove a pair of IN and OUT endpoints
  *
- * @param ctx usbdev context pointer
+ * @param ctx usb test utils context pointer
  * @param ep endpoint number
  */
 void usb_testutils_endpoint_remove(usb_testutils_ctx_t *ctx, uint8_t ep);
@@ -169,7 +252,7 @@
  * Returns an indication of whether an endpoint is currently halted because
  * of the occurrence of an error.
  *
- * @param ctx usbdev context pointer
+ * @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
  */
@@ -182,7 +265,7 @@
  * Does not connect the device, since the default endpoint is not yet enabled.
  * See usb_testutils_connect().
  *
- * @param ctx uninitialized usbdev context pointer
+ * @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
@@ -194,16 +277,40 @@
                         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 usbdev context pointer
+ * @param ctx usb test utils context pointer
  */
 void usb_testutils_poll(usb_testutils_ctx_t *ctx);
 
 /**
  * Finalize the usbdev interface
  *
- * @param ctx initialized usbdev context pointer
+ * 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);
 
diff --git a/sw/device/lib/testing/usb_testutils_controlep.c b/sw/device/lib/testing/usb_testutils_controlep.c
index 6866aab..aada26d 100644
--- a/sw/device/lib/testing/usb_testutils_controlep.c
+++ b/sw/device/lib/testing/usb_testutils_controlep.c
@@ -123,14 +123,31 @@
         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;
         }
-        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));
+
+        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
@@ -267,7 +284,7 @@
   return kUsbTestutilsCtError;
 }
 
-static void ctrl_tx_done(void *ctctx_v) {
+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;
@@ -353,7 +370,7 @@
       .number = 0,
       .direction = USBDEV_ENDPOINT_DIR_IN,
   };
-  // Enable responding with STALL. Will be cleared by the HW.
+  // 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;
diff --git a/sw/device/tests/usbdev_stream_test.c b/sw/device/tests/usbdev_stream_test.c
index a119324..dc4cba5 100644
--- a/sw/device/tests/usbdev_stream_test.c
+++ b/sw/device/tests/usbdev_stream_test.c
@@ -36,13 +36,6 @@
 #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
@@ -216,15 +209,14 @@
 
 /**
  * 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),
 
+    // Up to 11 interfaces and STREAMS_MAX in the descriptor head specifies how
+    // many of the interfaces will be declared to the host
     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),
@@ -232,6 +224,42 @@
     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),
+
+    VEND_INTERFACE_DSCR(2, 2, 0x50, 1),
+    USB_BULK_EP_DSCR(0, 3U, USBDEV_MAX_PACKET_SIZE, 0),
+    USB_BULK_EP_DSCR(1, 3U, USBDEV_MAX_PACKET_SIZE, 0),
+
+    VEND_INTERFACE_DSCR(3, 2, 0x50, 1),
+    USB_BULK_EP_DSCR(0, 4U, USBDEV_MAX_PACKET_SIZE, 0),
+    USB_BULK_EP_DSCR(1, 4U, USBDEV_MAX_PACKET_SIZE, 0),
+
+    VEND_INTERFACE_DSCR(4, 2, 0x50, 1),
+    USB_BULK_EP_DSCR(0, 5U, USBDEV_MAX_PACKET_SIZE, 0),
+    USB_BULK_EP_DSCR(1, 5U, USBDEV_MAX_PACKET_SIZE, 0),
+
+    VEND_INTERFACE_DSCR(5, 2, 0x50, 1),
+    USB_BULK_EP_DSCR(0, 6U, USBDEV_MAX_PACKET_SIZE, 0),
+    USB_BULK_EP_DSCR(1, 6U, USBDEV_MAX_PACKET_SIZE, 0),
+
+    VEND_INTERFACE_DSCR(6, 2, 0x50, 1),
+    USB_BULK_EP_DSCR(0, 7U, USBDEV_MAX_PACKET_SIZE, 0),
+    USB_BULK_EP_DSCR(1, 7U, USBDEV_MAX_PACKET_SIZE, 0),
+
+    VEND_INTERFACE_DSCR(7, 2, 0x50, 1),
+    USB_BULK_EP_DSCR(0, 8U, USBDEV_MAX_PACKET_SIZE, 0),
+    USB_BULK_EP_DSCR(1, 8U, USBDEV_MAX_PACKET_SIZE, 0),
+
+    VEND_INTERFACE_DSCR(8, 2, 0x50, 1),
+    USB_BULK_EP_DSCR(0, 9U, USBDEV_MAX_PACKET_SIZE, 0),
+    USB_BULK_EP_DSCR(1, 9U, USBDEV_MAX_PACKET_SIZE, 0),
+
+    VEND_INTERFACE_DSCR(9, 2, 0x50, 1),
+    USB_BULK_EP_DSCR(0, 10U, USBDEV_MAX_PACKET_SIZE, 0),
+    USB_BULK_EP_DSCR(1, 10U, USBDEV_MAX_PACKET_SIZE, 0),
+
+    VEND_INTERFACE_DSCR(10, 2, 0x50, 1),
+    USB_BULK_EP_DSCR(0, 11U, USBDEV_MAX_PACKET_SIZE, 0),
+    USB_BULK_EP_DSCR(1, 11U, USBDEV_MAX_PACKET_SIZE, 0),
 };
 
 /**
@@ -414,7 +442,7 @@
 }
 
 // Callback for successful buffer transmission
-static void strm_tx_done(void *stream_v) {
+static void strm_tx_done(void *stream_v, usb_testutils_xfr_result_t result) {
   usbdev_stream_t *s = (usbdev_stream_t *)stream_v;
   usbdev_stream_test_ctx_t *ctx = s->ctx;
   usb_testutils_ctx_t *usbdev = ctx->usbdev;