[usbdev] Host-side streaming code

Host-side component of the usbdev streaming test
(usbdev_stream_test) for use with a physical host.
This commit provides implementations of Bulk Transfers
using either serial port file descriptors with byte-
level granularity, or libusb-managed packet-level
Bulk Transfers.

Change-Id: I52c110d6003e7b7edd366f078acdbf2496e7c423
Signed-off-by: Adrian Lees <a.lees@lowrisc.org>
diff --git a/sw/host/tests/usbdev/usbdev_stream/README.md b/sw/host/tests/usbdev/usbdev_stream/README.md
new file mode 100644
index 0000000..5688720
--- /dev/null
+++ b/sw/host/tests/usbdev/usbdev_stream/README.md
@@ -0,0 +1,175 @@
+# Host-side streaming software
+
+To verify the USB device (USBDEV) with a physical USB host requires special-purpose software to
+communicate with the device, checking all of the data transferred. This `stream_test` program forms
+the host-side component of the verification effort, complementing the following device-side tests:
+
+- usbdev_stream_test
+  - Device-side software that streams Bulk Transfer data to/from multiple endpoints, generating the
+    source data using a LFSR, and then checking the data returned by the USB host against the
+    device-side predictions.
+
+- usbdev_iso_test
+  - Derived from `usbdev_stream_test` and sharing a common body of code, this test employs
+    Isochronous transfers instead of Bulk Transfers. Isochronous Transfers have the important
+    distinction that the packet stream is inherently unreliable and there is no acknowledgement or
+    retrying of bad packets for Isochronous Transfers. Instead, this transfer type prioritizes the
+    timely delivery of new data over any retrying and error detection.
+  - Since the USB guarantees the bandwidth available to Isochronous endpoints, it is not possible
+    to implement the full complement of 12 supported endpoints in this test, and instead only
+    four concurrent streams are used. This allows the test to operate with a number of intervening
+    hubs between the USB host and device, without suffering from a failure to guarantee the
+    bandwidth requirements.
+
+- usbdev_mixed_test
+  - This test is more comprehensive than the above two tests in that it tests a combination of
+    Bulk Transfers, Interrupt Transfers and Isochronous Transfers concurrently.
+
+This host-side component typically uses `libusb` in order to verify all of the four Transfer Types
+supported by the USB:
+
+- Bulk Transfers
+- Isochronous Transfers
+- Interrupt Transfers
+- Control Transfers
+
+The `stream_test` program is also able to communicate over serial port connections using the
+regular file I/O API of the OS. If `libusb` is not available on the host platform, then the
+host-side software may be built with the macro 'STREAMTEST_LIBUSB' undefined or set to 0, allowing
+direct serial port access to be used on other Linux or Linux-like hosts.
+
+## Device identification and the 'USBDevice' class
+
+When the 'stream_test' software has been built with `libusb` support, it is capable of locating and
+identifying the USB device automatically by searching for a connected device having the expected
+Vendor and Product IDs. If the device is connected and the device-side test software has configured
+the USB device in response to the configuration requests from the USB host, it should be detected
+by `stream_test`, and testing will commence.
+
+If there is a connection issue or the configuration sequence does not completely successfully,
+there may be some useful diagnostic information available via `sudo dmesg -w` for a Linux host.
+
+```
+[101210.703949] usb 3-9.3: new full-speed USB device number 78 using xhci_hcd
+[101210.854843] usb 3-9.3: New USB device found, idVendor=18d1, idProduct=503a, bcdDevice= 1.00
+[101210.854862] usb 3-9.3: New USB device strings: Mfr=0, Product=0, SerialNumber=0
+[101210.921072] usb_serial_simple 3-9.3:1.0: google converter detected
+[101210.921480] usb 3-9.3: google converter now attached to ttyUSB0
+...
+```
+
+The start of the normal device identification and configuration is shown above. The
+`usbdev_stream_test` test software presents the device in a manner that makes it suitable for
+use by the 'usb-serial-simple' driver for Bulk Transfer communications. This makes an endpoint pair
+behave two reliable, unidirectional, serial byte streams without any requirement for configuration
+commands, flow control or handshaking etc.
+
+## USBDPI model
+
+Since verification of USBDEV is also performed using DV simulation (at both chip- and block-level)
+and Verilator top-level simulation, there is a counterpart implementation of the 'stream_test'
+functionality within the USBDPI model. This employs the same, umodified device-side tests and
+offers greater control over the USB traffic than may be achieved using a physical host.
+
+## Test Descriptor
+
+Having located the USBDEV using `libusb` the host-side code will attempt to read a test descriptor
+from the device-side test code using a 'Vendor Specific' Control Transfer. This is a bespoke
+command that is interpreted by the device-side test software, and which returns a description of
+the functionality required of the streaming code, including the number of streams to be tested
+concurrently, and the transfer type of each of the streams.
+
+## Stream signatures
+
+The host-side code initiates communications with each of the device-side endpoints by claiming the
+interface for exclusive use and performing an initial read from the IN endpoint. The start of the
+received data stream is expected to consist of a stream signature that specifies the properties
+of the stream, including the initial value of the LFSR used to generate the pseudo-random byte
+stream.
+
+## LFSR-based data generation
+
+The device-side test software has one LFSR implementation for each of its streams, and uses these
+to generate the byte streams. Since the host-side code is informed of the initial state of each LFSR
+via the stream signatures, and employs the same algoriths, the host-side code may check the received
+data for correctness and thus detect any transmission errors and signal integrity issues on the USB.
+
+Having verified the received bytes against expectations using its own model of the device-side LFSR,
+the host-side code in `stream_test` then combines each byte with the output from its own LFSR for
+that stream, before sending the results stream back to the device. This resulting byte stream is
+simply the bytewise XOR of the device-side LFSR output and the host-side LFSR output.
+
+Upon receipt of data from the USB host, the device-side test software will check the received
+data against its own prediction of the XOR of the two LFSR outputs, thus verifying the correct,
+error-free transmission of data from host to device.
+
+## Circular Buffer Implementation
+
+Within the `USBDevStream` base class, from which the other stream types derive, there is an
+implementation of a circular buffer using statically-allocated storage within the USBDevStream
+instance itself. This is a relatively small buffer that keeps the byte stream flowing from and to
+the device, without the need for additional data movement.
+
+For the serial port implementation in 'USBDevSerial' this is a generalized byte stream with the
+amount of data being received/transmitted having byte-level granularity. For the other stream
+implementations the transfers are packet-based, in accordance with the interface exposed by
+`libusb` and it is therefore necessary to anticipate the recept of a maximum-length packet,
+since is the device-side test software that determines the individual, randomized sizes of the
+packets, rather than the USB requests from the host.
+
+To this end, in addition to the usual 'read' and 'write' indexes into the circular buffer
+implementation, there is an 'end' index which tracks the current used size of the circular buffer,
+allowing the buffer contents to wrap prematurely in the event that another maximum-length packet
+cannot be accommodated contiguously.
+
+## Special considerations for Isochronous streams
+
+Since Isochronous Transfers prioritize the timely delivery or recent data over the reliable,
+error-free delivery of all data, Isochronous packets are subject to be dropped from the byte
+stream at any point, but particularly in the OUT direction (host to device) since the device-side
+software is running on much slower hardware than the host-side code. Additionally, the USB host
+controls the USB so packets that are requested will usually have sufficient space available
+for storage.
+
+Packets transmitted to the USB device may be dropped silently when there is no storage space
+available, or in the event of data corruption occurring on the USB. As such the streaming code
+on the device side populates the first part of each Isochronous packet with a stream signature
+that indicates both the sequence number of the packet and the initial value of the device-side
+LFSR for that packet. This allows the USB host to detect the disappearance of packets on the IN
+side (device to host), and to run both its mirror of the device-side LFSR and its own host-side LFSR
+forwards to resynchronize.
+
+One further point to note regarding tests that employ Isochronous Transfers is that the host-side
+`stream_test` code is unable to ascertain when all data has been transferred, because it receives
+no guarantee that transmitted data has arrived at the USB device.
+
+Since the device-side tests software is ultimately responsible for deciding the pass/fail status
+of the test, the test framework on the host shall be expected to terminate the `stream_test` when
+a verdict has been reached.
+
+## Suspend/Resume testing
+
+In addition to the normal streaming and checking operations of the `stream_test` software, it may
+be used to exercise the Suspend/Resume behavior of the USB device. The USB device has a companion
+module called `usbdev_aon_wake` that may be used to keep the bus alive and monitor events occurring
+on the USB whilst most of the chip enters a lower power 'sleep' state.
+
+In order for a Linux host to put a USB device to perform Suspend signaling, which indicates to the
+device that it is presently not required to communicate and may enter a low power mode, all file
+handles including those held by the `libusb` code must be closed.
+
+To initiate a Suspend operation, the `stream_test` software will therefore temporarily cease the
+scheduling of new transfers on any stream, and await the completion of existing transfers,
+before then 'closing' the streams. Note that the USBDevStream instances remain extant, and that
+their internal state remains intact for use after resuming; in particular, communication must
+continue from the point at which the stream was suspended without packet loss.
+
+After an appropriate interval in the Suspended state, `stream_test` will then invoke 'Resume()' on
+each of the streams in turn, causing them to reestablish communication with the USBDEV. Each of
+the streams continues from the point of suspension with all LFSR state and byte counts unmodified.
+
+Note that it may prove necessary to run the OpenTitanTool test harness and the `stream_test`
+host-side code on separate hosts to test this behavior meaningfully, because something within the
+standard USB driver stack on a Linux host causes USB devices to be reawakened from their Suspended
+state every few seconds, making it impossible to put the USBDEV into a low power sleep state for
+any prolonged period.
diff --git a/sw/host/tests/usbdev/usbdev_stream/stream_test.cc b/sw/host/tests/usbdev/usbdev_stream/stream_test.cc
new file mode 100644
index 0000000..5f6d1c5
--- /dev/null
+++ b/sw/host/tests/usbdev/usbdev_stream/stream_test.cc
@@ -0,0 +1,580 @@
+// Copyright lowRISC contributors (OpenTitan project).
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+//
+// USB streaming data test
+//
+// Linux host-side application that receives a stream of LFSR-generated data
+// from the USB device, checks the received bytestream and then XORs it with a
+// host-side LFSR-generated byte stream to transmit back to the device.
+//
+// By default the streaming test expects a number of USB serial connections to
+// the target device, one port per endpoint:
+//
+//   /dev/ttyUSB0 - supplies and receives LFSR-generated byte stream for one/
+//                  the only endpoint
+//   /dev/ttyUSB1 - a secondary stream
+//   /dev/ttyUSB..
+//
+// Note that the mapping from device endpoints to USB port number is not
+// guaranteed, and  when multiple streams are used, it is _not_ necessarily the
+// case that ascending streams/endpoints in usbdev_stream_test are mapped to
+// a contiguous range of ascending ttyUSBi port names.
+//
+// Either or both of the initial input port and the initial output port may be
+// overridden using command line parameters.
+//
+// Usage:
+//   stream [-v<bool>][-c<bool>][-r<bool>][-s<bool>]
+//          [[-d<bus>:<address>] | [--device <bus>:<address>]]
+//          [<input port>[ <output port>]]
+//
+//   --device   programmatically specify a particular USB device by bus number
+//              and device address (see 'lsusb' output).
+//
+//   -c   check any retrieved data against expectations
+//   -d   specify a particular USB device by bus number and device address
+//   -r   retrieve data from device
+//   -s   send data to device
+//   -t   use serial ports (ttyUSBx) in preference to libusb Bulk Transfer
+//        streams for usbdev_stream_test
+//   -v   verbose reporting
+//   -z   perform suspend-resume signaling throughout the test
+//
+// <bool> values may be 0,1,n or y, and they default to 1.
+//
+// Build without libusb dependency:
+//   eg. g++ -Wall -Werror -o stream_test *.cc
+#include "stream_test.h"
+
+#include <cassert>
+#include <cctype>
+#include <cerrno>
+#include <cinttypes>
+#include <cstdbool>
+#include <cstddef>
+#include <cstdint>
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <ctime>
+#include <iostream>
+#include <sys/time.h>
+
+#include "usb_device.h"
+#if STREAMTEST_LIBUSB
+#include "usbdev_int.h"
+#endif
+#include "usbdev_serial.h"
+#include "usbdev_utils.h"
+
+// Test properties
+//
+// 16MiB takes about 40s presently with no appreciable CPU activity on the CW310
+// (ie. undefined transmitted data, and no checking of received data) but ca.
+// 152s with LFSR generation and checking across all of the 11 streams possible.
+//
+// Note: in normal use such as regression tests, the stream signatures will
+//       override the specified transfer amount.
+constexpr uint32_t kTransferBytes = (0x10U << 20);
+
+// Has any data yet been received from the device?
+bool received = false;
+
+// Time of first data reception.
+uint64_t start_time = 0U;
+
+// Configuration settings for the test.
+TestConfig cfg(false,  // Not verbose
+               true,   // Retrieve data from the device
+               true,   // Check the retrieved data
+               true);  // Send modified data to the device
+
+static USBDevice dev;
+
+// State information for each of the streams.
+static USBDevStream *streams[STREAMS_MAX];
+
+// Parse a command line option and return boolean value.
+static bool GetBool(const char *p);
+
+// Construct a modified port name for the next stream.
+static void PortNext(char *next, size_t n, const char *curr);
+
+// Parse a command line option and return boolean value.
+bool GetBool(const char *p) {
+  return (*p == '1') || (tolower(*p) == 'y') || (*p == '\r') || (*p == '\n') ||
+         (*p == '\0');
+}
+
+// Parse a command line option, retrieving a byte and indicating
+// success/failure.
+bool GetByte(const char **pp, uint8_t &byte) {
+  const char *p = *pp;
+  if (isdigit(*p)) {
+    uint32_t n = 0u;
+    do {
+      n = (n * 10) + *p++ - '0';
+    } while (n < 0x100u && isdigit(*p));
+    if (n < 0x100u) {
+      byte = (uint8_t)n;
+      *pp = p;
+      return true;
+    }
+  }
+  return false;
+}
+
+// Parse a command line option specifying the bus number and device address.
+bool GetDevice(const char *p, uint8_t &busNumber, uint8_t &devAddress) {
+  return GetByte(&p, busNumber) && (*p++ == ':') && GetByte(&p, devAddress) &&
+         *p == '\0';
+}
+
+// Construct a modified port name for the next stream.
+void PortNext(char *next, size_t n, const char *curr) {
+  // We're expecting a port name of the form '/dev/ttyUSB<n>'
+  if (curr != next) {
+    strncpy(next, curr, n);
+  }
+  while (*next != '\0') {
+    if (isdigit(*next)) {
+      int port = atoi(next);
+      snprintf(next, n, "%u", (unsigned)port + 1U);
+      break;
+    }
+    next++;
+    n--;
+  }
+}
+
+// Report command line syntax.
+void ReportSyntax(void) {
+  fputs(
+      "Usage:\n"
+      "  stream [-n<streams>][-v<bool>][-c<bool>][-r<bool>][-s<bool>][-t][-z]\n"
+      "         [[-d<bus>:<address>] | [--device <bus>:<address>]]\n"
+      "         [<input port>[ <output port>]]"
+      "\n\n"
+      "   --device   programmatically specify a particular USB device by bus\n"
+      "              number and device address (see 'lsusb' output).\n"
+      "\n\n"
+      "  -c   check any retrieved data against expectations\n"
+      "  -d   specify a particular USB device by bus number"
+      " and device address\n"
+      "  -r   retrieve data from device\n"
+      "  -s   send data to device\n"
+      "  -t   use serial ports (ttyUSBx) in preference to libusb Bulk\n"
+      "       Transfer streams for usbdev_stream_test\n"
+      "  -v   verbose reporting\n"
+      "  -z   perform suspend-resume signaling throughout the test"
+      "\n\n"
+      "  <bool> values may be 0,1,n or y, and they default to 1\n",
+      stderr);
+}
+
+static int RunTest(USBDevice *dev, const char *in_port, const char *out_port) {
+  // We need to modify the port names for each non-initial stream.
+  char out_name[FILENAME_MAX];
+  char in_name[FILENAME_MAX];
+
+  // Collect the test number and the test arguments so that we may ascertain
+  // the transfer type of each of the streams.
+  uint8_t testNum = dev->TestNumber();
+  uint8_t testArg[4];
+  for (unsigned arg = 0U; arg < 4U; arg++) {
+    testArg[arg] = dev->TestArg(arg);
+  }
+
+  // Determine the number of streams from the test descriptor; the device-side
+  // software supplies the stream count.
+  unsigned nstreams = 2U;
+  switch (testNum) {
+    case USBDevice::kUsbTestNumberStreams:
+    case USBDevice::kUsbTestNumberIso:
+    case USBDevice::kUsbTestNumberMixed:
+      // The lower nibble of the first test argument specifies the stream count
+      // in these test descriptions.
+      nstreams = testArg[0] & 0xfU;
+      break;
+      // Other tests default to 2 Bulk streams.
+    default:
+      nstreams = 2U;
+      break;
+  }
+
+  // Decide upon the number of bytes to be transferred for the entire test.
+  uint32_t transfer_bytes = kTransferBytes;
+  transfer_bytes = (transfer_bytes + nstreams - 1) / nstreams;
+  if (cfg.verbose) {
+    std::cout << " - " << nstreams << " stream(s), 0x" << std::hex
+              << transfer_bytes << std::dec << " bytes each" << std::endl;
+  }
+
+  // Initialize all streams.
+  for (unsigned idx = 0U; idx < nstreams; idx++) {
+    USBDevStream::StreamType streamType;
+
+    switch (testNum) {
+      case USBDevice::kUsbTestNumberStreams:
+        // For the basic streaming test where all active endpoints are using
+        // Bulk Transfer types, we may either use the ttyUSBn serial port
+        // interface or we may use libusb.
+        //
+        // In the former case we cannot support suspend-resume testing because
+        // data will get buffered somewhere within the software layers and
+        // lost when the file descriptors are closed and opened.
+        if (cfg.serial && !cfg.suspending) {
+          streamType = USBDevStream::StreamType_Serial;
+        } else {
+          streamType = USBDevStream::StreamType_Bulk;
+        }
+        break;
+      case USBDevice::kUsbTestNumberIso:
+        streamType = USBDevStream::StreamType_Isochronous;
+        break;
+      case USBDevice::kUsbTestNumberMixed: {
+        uint32_t mixedTypes =
+            (testArg[3] << 16) | (testArg[2] << 8) | testArg[1];
+        // Two bits per stream specify the stream/transfer type in terms of the
+        // USB standard endpoint types.
+        switch ((mixedTypes >> (idx * 2)) & 3U) {
+          case 0U:
+            streamType = USBDevStream::StreamType_Control;
+            break;
+          case 1U:
+            streamType = USBDevStream::StreamType_Isochronous;
+            break;
+          case 2U:
+            streamType = USBDevStream::StreamType_Bulk;
+            break;
+          default:
+            streamType = USBDevStream::StreamType_Interrupt;
+            break;
+        }
+      } break;
+      // Other tests default to 2 Bulk streams.
+      default:
+        streamType = USBDevStream::StreamType_Bulk;
+        break;
+    }
+
+    std::cout << "S" << idx << ": " << USBDevStream::StreamTypeName(streamType)
+              << std::endl;
+
+    bool opened(false);
+#if STREAMTEST_LIBUSB
+    bool bulk(true);
+#endif
+    switch (streamType) {
+      case USBDevStream::StreamType_Serial: {
+        USBDevSerial *s;
+        s = new USBDevSerial(idx, transfer_bytes, cfg.retrieve, cfg.check,
+                             cfg.send, cfg.verbose);
+        if (s) {
+          opened = s->Open(in_port, out_port);
+          if (opened) {
+            streams[idx] = s;
+
+            // Modify the port name for the next stream.
+            PortNext(out_name, sizeof(out_name), out_port);
+            PortNext(in_name, sizeof(in_name), in_port);
+            out_port = out_name;
+            in_port = in_name;
+          }
+        }
+      } break;
+
+#if STREAMTEST_LIBUSB
+      case USBDevStream::StreamType_Interrupt:
+        bulk = false;
+        // no break; Bulk Transfers are handled identically to Interrupt
+        // Transfers.
+      case USBDevStream::StreamType_Bulk: {
+        USBDevInt *interrupt;
+        interrupt = new USBDevInt(dev, bulk, idx, transfer_bytes, cfg.retrieve,
+                                  cfg.check, cfg.send, cfg.verbose);
+        if (interrupt) {
+          opened = interrupt->Open(idx);
+          if (opened) {
+            streams[idx] = interrupt;
+          }
+        }
+      } break;
+#endif
+      default:
+        assert(!"Unrecognised/invalid stream type");
+        break;
+    }
+
+    if (!opened) {
+      std::cerr << "Failed to open stream" << std::endl;
+      if (idx > 0U) {
+        do {
+          idx--;
+          delete streams[idx];
+        } while (idx > 0U);
+      }
+      return 1;
+    }
+  }
+
+  std::cout << "Streaming...\r" << std::flush;
+
+  // Times are in microseconds.
+  constexpr uint32_t kRunInterval = 5 * 1000000;  // Running before suspending.
+  constexpr uint32_t kSuspendingInterval = 5 * 1000;    // Suspending.
+  constexpr uint32_t kSuspendedInterval = 5 * 1000000;  // Device is suspended.
+  // Resume Signaling shall occur for at least 20ms but we have no control.
+  // over its duration, so there's little point trying to communicate sooner.
+  constexpr uint32_t kResumeInterval = 30 * 1000;  // Resuming before traffic.
+  uint64_t start_time = time_us();
+  uint32_t prev_bytes = 0;
+  bool done = false;
+  do {
+    uint32_t total_bytes = 0U;
+    uint32_t total_recv = 0U;
+    uint32_t total_sent = 0U;
+    bool failed = false;
+
+    done = false;
+    switch (dev->CurrentState()) {
+      case USBDevice::StateStreaming:
+        done = true;
+        for (unsigned idx = 0U; idx < nstreams; idx++) {
+          // Service this stream.
+          if (!streams[idx]->Service()) {
+            failed = true;
+            break;
+          }
+
+          // Update the running totals.
+          total_bytes += streams[idx]->TransferBytes();
+          total_recv += streams[idx]->BytesRecvd();
+          total_sent += streams[idx]->BytesSent();
+
+          // Has the stream completed all its work yet?
+          if (!streams[idx]->Completed()) {
+            done = false;
+          }
+        }
+
+        // Initiate transition to Suspended.
+        if (cfg.suspending && elapsed_time(start_time) >= kRunInterval) {
+          std::cout << "Waiting to suspend" << std::endl;
+          // Notify all of the streams that no more traffic shall be initiated.
+          for (unsigned idx = 0U; idx < nstreams; idx++) {
+            streams[idx]->Pause();
+          }
+          if (true) {
+            // Initiate autosuspend.
+            dev->Suspend();
+            // Start of Suspending interval.
+            start_time = time_us();
+          } else {
+            // TODO: There remains an issue in which the host-side software
+            // apparently fails to complete one or more transfers, which
+            // prevents us from properly resuming the transfers after the device
+            // has cycled through suspend-resume; this code persists in order
+            // to demonstrate and further investigate that.
+            //
+            // At the time the issue appeared to be in the behavior of the
+            // driver stack/libusb.
+
+            std::cout << "Attempting to resume" << std::endl;
+            // Notify all of the streams that no more traffic shall be
+            // initiated.
+            for (unsigned idx = 0U; idx < nstreams; idx++) {
+              streams[idx]->Resume();
+            }
+            std::cout << "Resuming streaming..." << std::endl;
+            // Start of Running interval.
+            start_time = time_us();
+          }
+        }
+        break;
+
+      // Put the device into Suspended for a while.
+      case USBDevice::StateSuspending:
+        if (elapsed_time(start_time) >= kSuspendingInterval) {
+          dev->SetState(USBDevice::StateSuspended);
+          // Start of Suspended interval.
+          start_time = time_us();
+          std::cout << "Suspended" << std::endl;
+        }
+        break;
+
+      case USBDevice::StateSuspended:
+        if (elapsed_time(start_time) >= kSuspendedInterval) {
+          dev->Resume();
+          // Start of Resuming interval.
+          start_time = time_us();
+        }
+        break;
+
+      case USBDevice::StateResuming:
+        if (elapsed_time(start_time) >= kResumeInterval) {
+          for (unsigned idx = 0U; idx < nstreams; idx++) {
+            streams[idx]->Resume();
+          }
+
+          dev->SetState(USBDevice::StateStreaming);
+          // Start of Running interval.
+          start_time = time_us();
+        }
+        break;
+    }
+
+    // Service the USBDevice to keep USB transfers flowing.
+    if (!failed) {
+      failed = !dev->Service();
+    }
+
+    // Tidy up if something went wrong.
+    if (failed) {
+      for (unsigned idx = 0U; idx < nstreams; idx++) {
+        (void)streams[idx]->Stop();
+      }
+      return 3;
+    }
+
+    // Down counting of the number of bytes remaining to be transferred.
+    if (std::abs((int32_t)total_sent - (int32_t)prev_bytes) >= 0x1000 || done) {
+      // Note: if there are Isochronous streams present then the bytes left
+      // count may hit zero some time before the test completes on the device
+      // side because packet delivery is not guaranteed.
+      uint32_t bytes_left =
+          (total_sent < total_bytes) ? (total_bytes - total_sent) : 0U;
+      std::cout << "Bytes received: 0x" << std::hex << total_recv
+                << " -- Left to send: 0x" << bytes_left << "         \r"
+                << std::dec << std::flush;
+      prev_bytes = total_sent;
+    }
+  } while (!done);
+
+  uint64_t elapsed_time = time_us() - start_time;
+
+  // Report time elapsed from the start of data transfer.
+  for (unsigned idx = 0U; idx < nstreams; idx++) {
+    streams[idx]->Stop();
+  }
+
+  // TODO: introduce a crude estimate of the performance being achieved,
+  // for profiling the performance of IN and OUT traffic; totals and individual
+  // endpoints?
+  double elapsed_secs = elapsed_time / 1e6;
+  printf("Test completed in %.2lf seconds (%" PRIu64 "us)\n", elapsed_secs,
+         elapsed_time);
+
+  return 0;
+}
+
+int main(int argc, char *argv[]) {
+  const uint16_t kVendorID = 0x18d1u;
+  const uint16_t kProductID = 0x503au;
+  const char *out_port = nullptr;
+  const char *in_port = nullptr;
+  uint8_t devAddress = 0u;
+  uint8_t busNumber = 0u;
+
+  cfg.override_flags = false;
+
+  // Collect options and alternative port names.
+  for (int i = 1; i < argc; i++) {
+    if (argv[i][0] == '-') {
+      switch (tolower(argv[i][1])) {
+        case 'c':
+          cfg.check = GetBool(&argv[i][2]);
+          cfg.override_flags = true;
+          break;
+        case 'd':
+          if (!GetDevice(&argv[i][2], busNumber, devAddress)) {
+            std::cerr << "ERROR: Unrecognised option '" << argv[i] << "'"
+                      << std::endl;
+            ReportSyntax();
+            return 7;
+          }
+          break;
+        case 'r':
+          cfg.retrieve = GetBool(&argv[i][2]);
+          cfg.override_flags = true;
+          break;
+        case 's':
+          cfg.send = GetBool(&argv[i][2]);
+          cfg.override_flags = true;
+          break;
+        case 't':
+          cfg.serial = GetBool(&argv[i][2]);
+          break;
+        case 'v':
+          cfg.verbose = GetBool(&argv[i][2]);
+          break;
+        case 'z':
+          cfg.suspending = GetBool(&argv[i][2]);
+          break;
+        case '-':
+          // The bus/address may be specified programmatically as '--device'
+          // with confidence that this parameter/syntax will not change.
+          if (!strcmp(&argv[i][2], "device") && i < argc - 1) {
+            // The next argument should be 'bus:address'
+            if (GetDevice(argv[++i], busNumber, devAddress)) {
+              break;
+            }
+          }
+          // no break
+        default:
+          std::cerr << "ERROR: Unrecognised option '" << argv[i] << "'"
+                    << std::endl;
+          ReportSyntax();
+          return 6;
+      }
+    } else if (!out_port) {
+      out_port = argv[i];
+    } else if (!in_port) {
+      in_port = argv[i];
+    } else {
+      std::cerr << "ERROR: Parameter '" << argv[i] << "' unrecognised"
+                << std::endl;
+      ReportSyntax();
+      return 7;
+    }
+  }
+
+  // Furnish test with default port names.
+  if (!out_port) {
+    out_port = "/dev/ttyUSB0";
+  }
+  if (!in_port) {
+    in_port = "/dev/ttyUSB0";
+  }
+
+  std::cout << "USB Streaming Test" << std::endl
+            << " (host-side implementation of usbdev streaming tests)"
+            << std::endl;
+
+  // Locate the USB device using Vendor and Product IDs, and optionally a
+  // specific device address and bus number to handle the presence of multiple
+  // similar devices.
+  USBDevice dev(cfg.verbose);
+  if (!dev.Init(kVendorID, kProductID, devAddress, busNumber)) {
+    return 2;
+  }
+
+  if (!dev.Open()) {
+    dev.Fin();
+    return 3;
+  }
+
+  // Read a vendor-specific test descriptor from the device-side software in
+  // order to ascertain the test configuration and required behavior.
+  if (!dev.ReadTestDesc()) {
+    dev.Fin();
+    return 3;
+  }
+
+  int rc = RunTest(&dev, in_port, out_port);
+
+  dev.Fin();
+
+  return rc;
+}
diff --git a/sw/host/tests/usbdev/usbdev_stream/stream_test.h b/sw/host/tests/usbdev/usbdev_stream/stream_test.h
new file mode 100644
index 0000000..2337476
--- /dev/null
+++ b/sw/host/tests/usbdev/usbdev_stream/stream_test.h
@@ -0,0 +1,79 @@
+// Copyright lowRISC contributors (OpenTitan project).
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+#ifndef OPENTITAN_SW_HOST_TESTS_USBDEV_USBDEV_STREAM_STREAM_TEST_H_
+#define OPENTITAN_SW_HOST_TESTS_USBDEV_USBDEV_STREAM_STREAM_TEST_H_
+#include <cstdint>
+
+// Maximum number of concurrent streams, leaving at least one endpoint for
+// the Default Control Pipe.
+#ifdef USBDEV_NUM_ENDPOINTS
+#define STREAMS_MAX (USBDEV_NUM_ENDPOINTS - 1U)
+#else
+#define STREAMS_MAX 11U
+#endif
+
+/**
+ * Simple container for test configuration.
+ */
+struct TestConfig {
+  /**
+   * Initialize test settings; see descriptions below.
+   */
+  TestConfig(bool init_verb, bool init_retr, bool init_chk, bool init_send)
+      : verbose(init_verb),
+        retrieve(init_retr),
+        check(init_chk),
+        send(init_send),
+#if STREAMTEST_LIBUSB
+        serial(false),
+#else
+        serial(true),
+#endif
+        suspending(false) {
+  }
+  /**
+   * Verbose logging/diagnostic reporting.
+   */
+  bool verbose;
+  /**
+   * Override the stream flags using the command line arguments?
+   */
+  bool override_flags;
+  /**
+   * Retrieve data from the device.
+   */
+  bool retrieve;
+  /**
+   * Check the retrieved data against expectations.
+   */
+  bool check;
+  /**
+   * Send data to the device.
+   */
+  bool send;
+  /**
+   * Favor serial ports over libusb for Bulk Transfer streams?
+   */
+  bool serial;
+  /**
+   * Are we performing suspend-resume testing whilst streaming?
+   */
+  bool suspending;
+};
+
+// Has any data yet been received from the device?
+extern bool received;
+
+// Time of first data reception.
+extern uint64_t start_time;
+
+// Configuration settings for the test.
+extern TestConfig cfg;
+
+/**
+ * Report command line syntax.
+ */
+void ReportSyntax(void);
+
+#endif  // OPENTITAN_SW_HOST_TESTS_USBDEV_USBDEV_STREAM_STREAM_TEST_H_
diff --git a/sw/host/tests/usbdev/usbdev_stream/usb_device.cc b/sw/host/tests/usbdev/usbdev_stream/usb_device.cc
new file mode 100644
index 0000000..123c46c
--- /dev/null
+++ b/sw/host/tests/usbdev/usbdev_stream/usb_device.cc
@@ -0,0 +1,364 @@
+// Copyright lowRISC contributors (OpenTitan project).
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+#include "usb_device.h"
+
+#include <cassert>
+#include <cstring>
+#include <fcntl.h>
+#include <iostream>
+#include <linux/usbdevice_fs.h>
+#include <unistd.h>
+
+#include "usbdev_utils.h"
+
+// Initialize USB access, with intent to use a device with the given properties.
+bool USBDevice::Init(uint16_t vendorID, uint16_t productID, uint8_t devAddress,
+                     uint8_t busNumber) {
+#if STREAMTEST_LIBUSB
+  // Initialize libusb
+  int rc = libusb_init(&ctx_);
+  if (rc < 0) {
+    return ErrorUSB("ERROR: Initializing libusb", rc);
+  }
+
+  // Remember vendor and product IDs because the device may be opened and
+  // closed many times during the test.
+  vendorID_ = vendorID;
+  productID_ = productID;
+
+  // Remember the specified bus number and device address.
+  addrSpec_ = devAddress;
+  busSpec_ = busNumber;
+#endif
+  return true;
+}
+
+// Finalize use of the device.
+bool USBDevice::Fin() {
+  (void)Close();
+#if STREAMTEST_LIBUSB
+  libusb_exit(ctx_);
+#endif
+  return true;
+}
+
+// Open the device, if not already open.
+bool USBDevice::Open() {
+  // Check whether we have already opened the device.
+  if (devh_) {
+    return true;
+  }
+
+#if STREAMTEST_LIBUSB
+  // Locate our USB device.
+  std::cout << "Locating USB device" << std::endl;
+  unsigned numTries = 30u;
+  bool found = false;
+  do {
+    // No device handle at present.
+    devh_ = nullptr;
+
+    // We need to traverse a list of all devices before opening it; since
+    // we require the port numbers leading to our device, we cannot take the
+    // easier approach of using _open_device_with_vid_pid().
+    libusb_device **dev_list;
+    ssize_t num_devs = libusb_get_device_list(ctx_, &dev_list);
+    int idx;
+    for (idx = 0; idx < num_devs; idx++) {
+      int rc = libusb_get_device_descriptor(dev_list[idx], &devDesc_);
+      if (rc >= 0) {
+        if (verbose_) {
+          std::cout << "Device: "
+                    << "VendorID: " << std::hex << devDesc_.idVendor
+                    << " ProductID: " << devDesc_.idProduct << std::dec
+                    << std::endl;
+        }
+        if (devDesc_.idVendor == vendorID_ &&
+            devDesc_.idProduct == productID_) {
+          // Read device identification; there could be multiple USB devices
+          // present with the same Vendor and Product IDs.
+          uint8_t addr = libusb_get_device_address(dev_list[idx]);
+          uint8_t bus = libusb_get_bus_number(dev_list[idx]);
+          // A device address of 0 is invalid for an addressed/configured
+          // device on the USB, and we use this to denote 'no specific physical
+          // device.'
+          assert(addr);
+
+          // Filter by bus and address, if specific bus/device required; this
+          // may be because they were specified explicitly when the program
+          // started or simply because we're reopening the same device.
+          if (!addrSpec_ || (addrSpec_ == addr && busSpec_ == bus)) {
+            // We are interested in this device; remember its location.
+            busNumber_ = bus;
+            devAddress_ = addr;
+
+            // Open a handle to our device.
+            libusb_device *dev = dev_list[idx];
+            rc = libusb_open(dev, &devh_);
+            if (rc < 0) {
+              std::cerr << "Error opening device " << (int)bus << ":"
+                        << (int)addr << " - " << libusb_error_name(rc)
+                        << std::endl;
+              // Continue trying other devices; the system could have multiple
+              // identical devices visible but access permissions may restrict
+              // which device(s) may be opened.
+            } else {
+              // Obtain the list of port numbers; required for suspend/resume.
+              uint8_t bus = libusb_get_bus_number(dev);
+              if (verbose_) {
+                std::cout << "Device path: " << (unsigned)bus << "-";
+              }
+              devPath_ = std::to_string(bus) + '-';
+              uint8_t ports[8];
+              int rc = libusb_get_port_numbers(dev, ports, sizeof(ports));
+              if (rc >= 0) {
+                unsigned num_ports = (unsigned)rc;
+                for (unsigned idx = 0u; idx < num_ports; idx++) {
+                  if (verbose_) {
+                    std::cout << (unsigned)ports[idx];
+                  }
+                  devPath_ += std::to_string(ports[idx]);
+                  if (idx + 1 < num_ports) {
+                    std::cout << '.';
+                    devPath_ += '.';
+                  }
+                }
+                std::cout << std::endl;
+              } else {
+                std::cerr << "Error getting port list: "
+                          << libusb_error_name(rc) << std::endl;
+                return false;
+              }
+              break;
+            }
+          }
+          // else This is not the device you are looking for...
+        }
+      }
+    }
+
+    // Unreference all devices and release device list.
+    libusb_free_device_list(dev_list, 1u);
+
+    if (devh_) {
+      // Ensure that if we close and reopen this device, we shall return to the
+      // same device.
+      busSpec_ = busNumber_;
+      addrSpec_ = devAddress_;
+      found = true;
+    } else if (numTries-- > 0u) {
+      // Retry a number of times before reporting failure.
+      std::cout << '.' << std::flush;
+      sleep(1);
+    } else {
+      std::cerr << "Unable to locate USB device" << std::endl;
+      return false;
+    }
+  } while (!found);
+
+  // Report that we have at least found the device.
+  std::cout << "Device found (Bus " << (int)busNumber_ << " Device "
+            << (int)devAddress_ << ")" << std::endl;
+  if (verbose_) {
+    std::cout << " - Path: " << devPath_ << std::endl;
+  }
+
+  // We need to detach the kernel driver and claim the interface to have maximal
+  // control, eg. suspending device.
+  int rc = libusb_set_auto_detach_kernel_driver(devh_, 1u);
+  if (rc < 0) {
+    std::cerr << "Error detaching kernel driver: " << libusb_error_name(rc)
+              << std::endl;
+    return false;
+  }
+
+  // Read and check the currently active configuration; this should just be 1
+  // since our test software sets up only a single configuration.
+  int config;
+  rc = libusb_get_configuration(devh_, &config);
+  if (rc < 0) {
+    std::cerr << "Error getting configuration: " << libusb_error_name(rc)
+              << std::endl;
+  }
+
+  std::cout << "Configuration: " << config << std::endl;
+#else
+  std::cout << "Running without libusb" << std::endl;
+  devh_ = true;
+#endif
+  return true;
+}
+
+// Close the device, if open.
+bool USBDevice::Close() {
+#if STREAMTEST_LIBUSB
+  if (devh_) {
+    libusb_close(devh_);
+    devh_ = nullptr;
+  }
+#else
+  devh_ = false;
+#endif
+  return true;
+}
+
+bool USBDevice::Service() {
+#if STREAMTEST_LIBUSB
+  struct timeval tv = {0};
+  int rc = libusb_handle_events_timeout(ctx_, &tv);
+  if (rc < 0) {
+    return ErrorUSB("ERROR: Handling events", rc);
+  }
+#endif
+  return true;
+}
+
+bool USBDevice::ReadTestDesc() {
+  std::cout << "Reading Test Descriptor" << std::endl;
+
+  if (!Open()) {
+    return false;
+  }
+#if STREAMTEST_LIBUSB
+  uint8_t bmRequestType = LIBUSB_ENDPOINT_IN | LIBUSB_REQUEST_TYPE_VENDOR |
+                          LIBUSB_RECIPIENT_ENDPOINT;
+  std::cout << "req type " << (int)bmRequestType << std::endl;
+  // Send a Vendor-Specific command to read the test descriptor.
+  uint8_t testDesc[0x10u];
+  int rc = libusb_control_transfer(devh_, bmRequestType, kVendorTestConfig, 0u,
+                                   0u, testDesc, sizeof(testDesc),
+                                   kControlTransferTimeout);
+  if (rc < 0) {
+    std::cerr << "Error reading test descriptor: " << libusb_error_name(rc)
+              << std::endl;
+    return false;
+  }
+
+  if (verbose_) {
+    std::cout << "Test Descriptor:" << std::endl;
+    for (unsigned idx = 0u; idx < sizeof(testDesc); idx++) {
+      printf("%u: 0x%02x\n", idx, testDesc[idx]);
+    }
+  }
+
+  // Validate the received test descriptor.
+  const uint8_t test_sig_head[] = {0x7eu, 0x57u, 0xc0u, 0xf1u};
+  const uint8_t test_sig_tail[] = {0x1fu, 0x0cu, 0x75u, 0xe7u};
+  const uint8_t *dp = testDesc;
+  if (!memcmp(dp, test_sig_head, 4) && 0x10u == get_le16(&dp[4]) &&
+      !memcmp(&dp[12], test_sig_tail, 4)) {
+    usb_testutils_test_number_t testNum =
+        (usb_testutils_test_number_t)get_le16(&dp[6]);
+
+    if (verbose_) {
+      std::cout << "Test number: " << testNum << " args " << std::hex
+                << (int)dp[8] << " " << (int)dp[9] << " " << (int)dp[10] << " "
+                << (int)dp[11] << std::dec << std::endl;
+    }
+
+    // Retain the test number and the test arguments.
+    testNumber_ = testNum;
+    testArg_[0] = dp[8];
+    testArg_[1] = dp[9];
+    testArg_[2] = dp[10];
+    testArg_[3] = dp[11];
+    return true;
+  }
+
+  return false;
+#else
+  // Default test configuration, since we are unable to issue Vendor-Specific
+  // commands.
+  // - streaming test, 2 bulk streams.
+  testNumber_ = USBDevice::kUsbTestNumberStreams;
+  testArg_[0] = USBDevice::kUsbdevStreamFlagsDefault | 2U;
+  testArg_[1] = 0U;
+  testArg_[2] = 0U;
+  testArg_[3] = 0U;
+
+  return true;
+#endif
+}
+
+bool USBDevice::Suspend() {
+  std::cout << "Suspending Device " << devPath_ << std::endl;
+
+  // We need to relinquish our access to the device otherwise the kernel
+  // will refuse to autosuspend the device!
+  Close();
+
+  std::string powerPath = "/sys/bus/usb/devices/" + devPath_ + "/power/";
+  std::string filename = powerPath + "autosuspend_delay_ms";
+
+  int fd = open(filename.c_str(), O_WRONLY);
+  if (fd < 0) {
+    std::cerr << "Failed to open '" << filename << "'" << std::endl;
+    std::cerr << "  (Note: this requires super user permissions)" << std::endl;
+    return false;
+  }
+  int rc = write(fd, "0", 1);
+  if (rc < 0) {
+    std::cerr << "Write failed" << std::endl;
+  }
+  rc = close(fd);
+  if (rc < 0) {
+    std::cerr << "Close failed" << std::endl;
+  }
+
+  // Enable auto-suspend behavior.
+  filename = powerPath + "control";
+  fd = open(filename.c_str(), O_WRONLY);
+  if (fd < 0) {
+    std::cerr << "Failed to open '" << filename << "'" << std::endl;
+    std::cerr << "  (Note: this requires super user permissions)" << std::endl;
+    return false;
+  }
+  rc = write(fd, "auto", 4);
+  if (rc < 0) {
+    std::cerr << "Write failed" << std::endl;
+  }
+  rc = close(fd);
+  if (rc < 0) {
+    std::cerr << "Close failed" << std::endl;
+  }
+
+  SetState(StateSuspending);
+  return true;
+}
+
+bool USBDevice::Resume() {
+  std::cout << "Resuming Device" << std::endl;
+
+  std::string powerPath = "/sys/bus/usb/devices/" + devPath_ + "/power/";
+  std::string filename = powerPath + "control";
+
+  int fd = open(filename.c_str(), O_WRONLY);
+  if (fd < 0) {
+    std::cerr << "Failed to open '" << filename << "'" << std::endl;
+    return false;
+  }
+  int rc = write(fd, "on", 2);
+  if (rc < 0) {
+    std::cerr << "Write failed" << std::endl;
+  }
+  close(fd);
+
+  if (!Open()) {
+    return false;
+  }
+
+  SetState(StateResuming);
+  return true;
+}
+
+bool USBDevice::Disconnect() {
+  // TODO: Are we able to implement a Disconnect/Reconnect function here?
+  //       Most hubs do not have the capacity to power cycle an individual
+  //       port.
+  //
+  // Power Off
+  // Power On
+
+  return false;
+}
diff --git a/sw/host/tests/usbdev/usbdev_stream/usb_device.h b/sw/host/tests/usbdev/usbdev_stream/usb_device.h
new file mode 100644
index 0000000..5cbb915
--- /dev/null
+++ b/sw/host/tests/usbdev/usbdev_stream/usb_device.h
@@ -0,0 +1,355 @@
+// Copyright lowRISC contributors (OpenTitan project).
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+#ifndef OPENTITAN_SW_HOST_TESTS_USBDEV_USBDEV_STREAM_USB_DEVICE_H_
+#define OPENTITAN_SW_HOST_TESTS_USBDEV_USBDEV_STREAM_USB_DEVICE_H_
+#include <iostream>
+
+// The 'usbdev_serial' class may be used on platforms where libusb support is
+// not available; libusb is required to exercise Isochronous streams, Interrupt
+// streams and Control Transfers.
+#if STREAMTEST_LIBUSB
+#include <libusb-1.0/libusb.h>
+#endif
+
+class USBDevice {
+ public:
+#if STREAMTEST_LIBUSB
+  USBDevice(bool verbose = false)
+      : verbose_(verbose),
+        state_(StateStreaming),
+        ctx_(nullptr),
+        devh_(nullptr) {}
+#else
+  USBDevice(bool verbose = false)
+      : verbose_(verbose), state_(StateStreaming), devh_(false) {}
+#endif
+
+  // Device states.
+  enum USBDevState {
+    StateStreaming,
+    StateSuspending,
+    StateSuspended,
+    StateResuming
+  };
+
+  // DPI test numbers.
+  typedef enum usb_testutils_test_number {
+    kUsbTestNumberSmoke = 0,
+    kUsbTestNumberStreams,
+    kUsbTestNumberIso,
+    kUsbTestNumberMixed,
+    kUsbTestNumberSuspend,
+    kUsbTestNumberExc,
+  } usb_testutils_test_number_t;
+
+  // Test/stream flags; these are common with device-side software.
+  typedef enum {
+    /**
+     * Mask for extracting the stream ID number.
+     */
+    kUsbdevStreamFlagID = 0x0FU,
+    /**
+     * Host shall retrieve IN data from the device for this stream.
+     */
+    kUsbdevStreamFlagRetrieve = 0x10U,
+    /**
+     * Host shall check that IN data matches as expected.
+     */
+    kUsbdevStreamFlagCheck = 0x20U,
+    /**
+     * DPI model (or Host) shall retry IN data fetches, where possible.
+     */
+    kUsbdevStreamFlagRetry = 0x40U,
+    /**
+     * Host shall send OUT data to the device for this stream.
+     */
+    kUsbdevStreamFlagSend = 0x80U,
+    /**
+     * Default stream flags; for non-libusb builds.
+     */
+    kUsbdevStreamFlagsDefault = kUsbdevStreamFlagRetrieve |
+                                kUsbdevStreamFlagCheck |
+                                kUsbdevStreamFlagRetry | kUsbdevStreamFlagSend
+  } usbdev_stream_flags_t;
+
+  // Vendor-Specific requests.
+  static constexpr uint8_t kVendorGetData = 0x7du;
+  static constexpr uint8_t kVendorPutData = 0x7fu;
+
+  // Maximum packet size of 64 bytes is used for Control, Interrupt and Bulk
+  // Transfers.
+  static constexpr unsigned kDevDataMaxPacketSize = 0x40U;
+
+  // Our USB device has a maximum packet size of just 64 bytes even for
+  // Isochronous transfers; this may one day be increased.
+  static constexpr unsigned kDevIsoMaxPacketSize = 0x40U;
+
+  /**
+   * Initialize USB device before test; called once at startup.
+   *
+   * @param  vendorID   Vendor ID code of the USB device.
+   * @param  productID  Product ID code of the USB device.
+   * @param  devAddress Device address (0 for first accessible device).
+   * @param  busNumber  Bus number (if devAddress non-zero).
+   * @return true iff initialization was successful.
+   */
+  bool Init(uint16_t productID, uint16_t vendorID, uint8_t devAddress = 0u,
+            uint8_t busNumber = 0u);
+  /**
+   * Finalize USB device before completing test.
+   *
+   * @return true iff finalization was successful.
+   */
+  bool Fin();
+  /**
+   * (Re)open the device. If the device is already open, this function just
+   * returns immediately.
+   *
+   * @return true iff opening was successful, or the device was already open.
+   */
+  bool Open();
+  /**
+   * Close the device, if open. If the device is already closed, this function
+   * just returns immediately.
+   *
+   * @return true iff closing was successful, or the device was already closed.
+   */
+  bool Close();
+  /**
+   * Service the device; keep libusb transfers being processed.
+   *
+   * @return true iff the device is still operational.
+   */
+  bool Service();
+
+#if STREAMTEST_LIBUSB
+  /**
+   * Claim an interface on the device.
+   *
+   * @param interface The interface number of the interface to be claimed.
+   * @return The result of the operation.
+   */
+  int ClaimInterface(unsigned interface) const {
+    return libusb_claim_interface(devh_, (int)interface);
+  }
+  /**
+   * Release an interface on the device.
+   *
+   * @param interface The interface number of the interface to be claimed.
+   * @return The result of the operation.
+   */
+  int ReleaseInterface(unsigned interface) const {
+    return libusb_release_interface(devh_, (int)interface);
+  }
+  /**
+   * Emit a textual report of an error returned by libusb.
+   *
+   * @param  rc      Return code from libusb function.
+   * @return false as a convenience for indicating failure to caller.
+   */
+  inline bool ErrorUSB(const char *prefix, int rc) {
+    std::cerr << prefix << " (" << rc << ", " << libusb_error_name(rc) << ")"
+              << std::endl;
+    return false;
+  }
+  /**
+   * Retrieve libusb device handle.
+   *
+   * @return The device handle, or nullptr if none eg. device not open.
+   */
+  libusb_device_handle *DeviceHandle() const { return devh_; }
+  /**
+   * Fill Control Transfer descriptor.
+   */
+  void FillControlTransfer(struct libusb_transfer *xfr, uint8_t ep,
+                           uint8_t *buffer, libusb_transfer_cb_fn callback,
+                           void *user_data, unsigned timeout_ms) {
+    libusb_fill_control_transfer(xfr, devh_, buffer, callback, user_data,
+                                 timeout_ms);
+    // Complete the endpoint number because we're _not_ always targeting
+    // Endpoint Zero.
+    xfr->endpoint = ep;
+  }
+  /**
+   * Fill Bulk Transfer descriptor.
+   */
+  void FillBulkTransfer(struct libusb_transfer *xfr, uint8_t ep,
+                        uint8_t *buffer, size_t len,
+                        libusb_transfer_cb_fn callback, void *user_data,
+                        unsigned timeout_ms) const {
+    libusb_fill_bulk_transfer(xfr, devh_, ep, buffer, len, callback, user_data,
+                              timeout_ms);
+  }
+  /**
+   * Fill Interrupt Transfer descriptor.
+   */
+  void FillIntTransfer(struct libusb_transfer *xfr, uint8_t ep, uint8_t *buffer,
+                       size_t len, libusb_transfer_cb_fn callback,
+                       void *user_data, unsigned timeout_ms) const {
+    libusb_fill_interrupt_transfer(xfr, devh_, ep, buffer, len, callback,
+                                   user_data, timeout_ms);
+  }
+  /**
+   * Fill Isochronous transfer descriptor,
+   */
+  void FillIsoTransfer(struct libusb_transfer *xfr, uint8_t ep, uint8_t *buffer,
+                       size_t len, unsigned num_iso_packets,
+                       libusb_transfer_cb_fn callback, void *user_data,
+                       unsigned timeout_ms) const {
+    libusb_fill_iso_transfer(xfr, devh_, ep, buffer, len, num_iso_packets,
+                             callback, user_data, timeout_ms);
+  }
+  /**
+   * Complete the lengths of the packets in this Isochronous transfer, assuming
+   * that packets are of uniform size.
+   *
+   * @param  xfr     Isochronous transfer to be completed.
+   * @param  len     Total length of all packets within the transfer.
+   */
+  void SetIsoPacketLengths(struct libusb_transfer *xfr, unsigned len) const {
+    libusb_set_iso_packet_lengths(xfr, len);
+  }
+  /**
+   * Allocate a transfer descriptor to be completed by the caller.
+   *
+   * @return  Pointer to transfer transcript, or NULL if allocation failed.
+   */
+  struct libusb_transfer *AllocTransfer(unsigned iso_packets) const {
+    return libusb_alloc_transfer((int)iso_packets);
+  }
+  /**
+   * Free an allocated transfer descriptor.
+   *
+   * @param  xfr     The transfer descriptor to be freed.
+   */
+  void FreeTransfer(struct libusb_transfer *xfr) const {
+    return libusb_free_transfer(xfr);
+  }
+  /**
+   * Submit a transfer for processing.
+   *
+   * @param  xfr     The transfer to be submitted.
+   */
+  int SubmitTransfer(struct libusb_transfer *xfr) const {
+    return libusb_submit_transfer(xfr);
+  }
+  /**
+   * Cancel a pending transfer.
+   *
+   * @param  xfr     The transfer to be cancelled.
+   */
+  int CancelTransfer(struct libusb_transfer *xfr) const {
+    return libusb_cancel_transfer(xfr);
+  }
+#endif
+
+  /**
+   * Read Test Descriptor from the DUT using a Vendor-Specific command.
+   *
+   * @return true iff the operation was successful.
+   */
+  bool ReadTestDesc();
+  /**
+   * Return the test number from the test descriptor.
+   *
+   * @return test number
+   */
+  uint8_t TestNumber() const { return testNumber_; }
+  /**
+   * Return the specified test argument from the test descriptor.
+   *
+   * @param  arg     Argument number.
+   * @return test argument
+   */
+  uint8_t TestArg(unsigned arg) const {
+    // Presently all defined tests support up to 4 arguments.
+    return (arg < 4U) ? testArg_[arg] : 0U;
+  }
+  /**
+   * Reset and Reconfigure the DUT.
+   *
+   * @return true iff the operation was successful.
+   */
+  bool Reset();
+  /**
+   * Suspend device.
+   *
+   * @return true iff the operation was successful.
+   */
+  bool Suspend();
+  /**
+   * Resume operation of suspended device.
+   */
+  bool Resume();
+  /**
+   * Disconnect and reconnect device.
+   *
+   * @param true iff the operation was succesful.
+   */
+  bool Disconnect();
+  /**
+   * Returns the current state of the device.
+   *
+   * @return current state.
+   */
+  USBDevState CurrentState() const { return state_; }
+  /**
+   * Sets the current state of the device in the Suspend/Resume signaling.
+   *
+   * @param  state   The current state of the device.
+   */
+  void SetState(USBDevState state) { state_ = state; }
+
+ private:
+  // Verbose logging/reporting.
+  bool verbose_;
+
+  // Current device state.
+  USBDevState state_;
+
+  // Vendor ID of USB device.
+  uint16_t vendorID_;
+  // Product ID of USB device.
+  uint16_t productID_;
+
+  // Specified bus number.
+  uint8_t busSpec_;
+  // Specified device address.
+  uint8_t addrSpec_;
+
+  // Bus number of the device we're using.
+  uint8_t busNumber_;
+  // (Bus-local) device address of the device we're using.
+  uint8_t devAddress_;
+
+#if STREAMTEST_LIBUSB
+  // Context information for libusb.
+  libusb_context *ctx_;
+
+  // Device handle.
+  libusb_device_handle *devh_;
+
+  // Device descriptor.
+  libusb_device_descriptor devDesc_;
+#else
+  // Device handle; just retain whether open/closed.
+  bool devh_;
+#endif
+
+  // Device path (bus number - ports numbers).
+  std::string devPath_;
+
+  usb_testutils_test_number_t testNumber_;
+
+  uint8_t testArg_[4];
+
+  // TODO: We should introduce a timeout on libusb Control Transfers.
+  static const unsigned kControlTransferTimeout = 0u;
+
+  // Vendor-Specific Commands.
+  static const uint8_t kVendorTestConfig = 0x7cu;
+  static const uint8_t kVendorTestStatus = 0x7eu;
+};
+
+#endif  // OPENTITAN_SW_HOST_TESTS_USBDEV_USBDEV_STREAM_USB_DEVICE_H_
diff --git a/sw/host/tests/usbdev/usbdev_stream/usbdev_int.cc b/sw/host/tests/usbdev/usbdev_stream/usbdev_int.cc
new file mode 100644
index 0000000..6851081
--- /dev/null
+++ b/sw/host/tests/usbdev/usbdev_stream/usbdev_int.cc
@@ -0,0 +1,275 @@
+// Copyright lowRISC contributors (OpenTitan project).
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+#include "usbdev_int.h"
+
+#include <cassert>
+#include <cstdio>
+
+#include "usbdev_utils.h"
+
+// Stub callback function supplied to libusb.
+void LIBUSB_CALL USBDevInt::CbStubIN(struct libusb_transfer *xfr) {
+  USBDevInt *self = reinterpret_cast<USBDevInt *>(xfr->user_data);
+  self->CallbackIN(xfr);
+}
+
+void LIBUSB_CALL USBDevInt::CbStubOUT(struct libusb_transfer *xfr) {
+  USBDevInt *self = reinterpret_cast<USBDevInt *>(xfr->user_data);
+  self->CallbackOUT(xfr);
+}
+
+bool USBDevInt::Open(unsigned interface) {
+  int rc = dev_->ClaimInterface(interface);
+  if (rc < 0) {
+    return dev_->ErrorUSB("ERROR: Claiming interface", rc);
+  }
+
+  // Retain the interface number.
+  interface_ = interface;
+
+  // Remember the (assumed) endpoints which we're using.
+  epOut_ = interface + 1U;
+  epIn_ = 0x80U | epOut_;
+
+  // No transfers in progress.
+  xfrIn_ = nullptr;
+
+  xfrOut_ = nullptr;
+
+  maxPacketSize_ = USBDevice::kDevDataMaxPacketSize;
+
+  return true;
+}
+
+void USBDevInt::Stop() {
+  SetClosing(true);
+
+  int rc = dev_->ReleaseInterface(interface_);
+  if (rc < 0) {
+    std::cerr << "" << std::endl;
+  }
+}
+
+void USBDevInt::Pause() {
+  SetClosing(true);
+
+  if (verbose_) {
+    std::cout << PrefixID() << "waiting to close" << std::endl;
+  }
+  while (inActive_ || outActive_) {
+    dev_->Service();
+  }
+  if (verbose_) {
+    std::cout << PrefixID() << " closed" << std::endl;
+  }
+
+  int rc = dev_->ReleaseInterface(interface_);
+  if (rc < 0) {
+    std::cerr << "" << std::endl;
+  }
+}
+
+bool USBDevInt::Resume() {
+  SetClosing(false);
+
+  int rc = dev_->ClaimInterface(interface_);
+  if (rc < 0) {
+    return dev_->ErrorUSB("ERROR: Claiming interface", rc);
+  }
+  return true;
+}
+
+// Return a summary report of the stream settings of status.
+std::string USBDevInt::Report(bool status, bool verbose) const { return ""; }
+
+void USBDevInt::DumpIntTransfer(struct libusb_transfer *xfr) const {
+  const void *buf = reinterpret_cast<void *>(xfr->buffer);
+  std::cout << "Buffer " << buf << " length " << xfr->length
+            << " => actual length " << xfr->actual_length << std::endl;
+  buffer_dump(stdout, xfr->buffer, xfr->actual_length);
+}
+
+// Retrieving of IN traffic from device.
+bool USBDevInt::ServiceIN() {
+  // Ensure that we have enough space available for a full packet; the device
+  // software decides upon the length of each packet.
+  uint8_t *space;
+  bool ok = ProvisionSpace(&space, maxPacketSize_);
+  if (ok) {
+    uint32_t to_fetch = maxPacketSize_;
+    if (to_fetch > transfer_bytes_ - bytes_recvd_) {
+      to_fetch = transfer_bytes_ - bytes_recvd_;
+    }
+
+    if (!xfrIn_) {
+      xfrIn_ = dev_->AllocTransfer(0U);
+      if (!xfrIn_) {
+        return false;
+      }
+    }
+
+    if (bulk_) {
+      dev_->FillBulkTransfer(xfrIn_, epIn_, space, to_fetch, CbStubIN, this,
+                             kDataTimeout);
+    } else {
+      dev_->FillIntTransfer(xfrIn_, epIn_, space, to_fetch, CbStubIN, this,
+                            kDataTimeout);
+    }
+
+    int rc = dev_->SubmitTransfer(xfrIn_);
+    if (rc < 0) {
+      return dev_->ErrorUSB("ERROR: Submitting IN transfer", rc);
+    }
+    inActive_ = true;
+  } else {
+    inActive_ = false;
+  }
+
+  return true;
+}
+
+// Sending of OUT traffic to device.
+bool USBDevInt::ServiceOUT() {
+  // Do we have any data ready to send?
+  uint8_t *data;
+  uint32_t num_bytes = DataAvailable(&data);
+  if (num_bytes > 0U) {
+    // Supply details of the single OUT packet.
+    if (!xfrOut_) {
+      xfrOut_ = dev_->AllocTransfer(0U);
+      if (!xfrOut_) {
+        // Stream is not operational.
+        return false;
+      }
+    }
+
+    if (bulk_) {
+      dev_->FillBulkTransfer(xfrOut_, epOut_, data, num_bytes, CbStubOUT, this,
+                             kDataTimeout);
+    } else {
+      dev_->FillIntTransfer(xfrOut_, epOut_, data, num_bytes, CbStubOUT, this,
+                            kDataTimeout);
+    }
+
+    int rc = dev_->SubmitTransfer(xfrOut_);
+    if (rc < 0) {
+      return dev_->ErrorUSB("ERROR: Submitting OUT transfer", rc);
+    }
+    outActive_ = true;
+  } else {
+    // Nothing to propagate at this time.
+    outActive_ = false;
+  }
+  // Stream remains operational, even if it presently has no work on the OUT
+  // side.
+  return true;
+}
+
+bool USBDevInt::Service() {
+  if (failed_) {
+    return false;
+  }
+  // (Re)start IN traffic if not already in progress.
+  if (!inActive_ && !ServiceIN()) {
+    return false;
+  }
+  // (Re)start OUT traffic if not already in progress and there is
+  // data available to be transmitted.
+  if (!outActive_ && !ServiceOUT()) {
+    return false;
+  }
+  return true;
+}
+
+// Callback function supplied to libusb for IN transfers.
+void USBDevInt::CallbackIN(struct libusb_transfer *xfr) {
+  if (xfr->status != LIBUSB_TRANSFER_COMPLETED) {
+    std::cerr << PrefixID() << " Invalid/unexpected IN transfer status "
+              << xfr->status << std::endl;
+    std::cerr << "length " << xfr->length << " actual " << xfr->actual_length
+              << std::endl;
+    failed_ = true;
+    return;
+  }
+
+  if (verbose_) {
+    std::cout << PrefixID() << "CallbackIN xfr " << xfr << std::endl;
+    DumpIntTransfer(xfr);
+  }
+
+  // Collect and parse signature bytes at the start of the IN stream.
+  uint8_t *dp = xfr->buffer;
+  int nrecvd = xfr->actual_length;
+
+  // Update the circular buffer with the amount of data that we've received
+  CommitData(nrecvd);
+
+  if (!SigReceived()) {
+    if (nrecvd > 0 && !SigReceived()) {
+      uint32_t dropped = SigDetect(&sig_, dp, (uint32_t)nrecvd);
+
+      // Consume stream signature, rather than propagating it to the output
+      // side.
+      if (SigReceived()) {
+        SigProcess(sig_);
+        dropped += sizeof(usbdev_stream_sig_t);
+      }
+
+      // Skip past any dropped bytes, including the signature, so that if there
+      // are additional bytes we may process them.
+      nrecvd = ((uint32_t)nrecvd > dropped) ? ((uint32_t)nrecvd - dropped) : 0;
+      dp += dropped;
+
+      if (dropped) {
+        DiscardData(dropped);
+      }
+    }
+  }
+
+  bool ok = true;
+  if (nrecvd > 0) {
+    // Check the received LFSR-generated byte(s) and combine them with the
+    // output of our host-side LFSR.
+    ok = ProcessData(dp, nrecvd);
+  }
+
+  if (ok) {
+    if (CanSchedule()) {
+      // Attempt to set up another IN transfer.
+      failed_ = !ServiceIN();
+    } else {
+      inActive_ = false;
+    }
+  } else {
+    failed_ = true;
+  }
+}
+
+// Callback function supplied to libusb for OUT transfers.
+void USBDevInt::CallbackOUT(struct libusb_transfer *xfr) {
+  if (xfr->status != LIBUSB_TRANSFER_COMPLETED) {
+    std::cerr << PrefixID() << " Invalid/unexpected OUT transfer status "
+              << xfr->status << std::endl;
+    std::cerr << "length " << xfr->length << " actual " << xfr->actual_length
+              << std::endl;
+    failed_ = true;
+    return;
+  }
+
+  if (verbose_) {
+    std::cout << PrefixID() << "CallbackOUT xfr " << xfr << std::endl;
+    DumpIntTransfer(xfr);
+  }
+
+  // Note: we're not expecting any truncation on OUT transfers.
+  assert(xfr->actual_length == xfr->length);
+  ConsumeData(xfr->actual_length);
+
+  if (CanSchedule()) {
+    // Attempt to set up another OUT transfer.
+    failed_ = !ServiceOUT();
+  } else {
+    outActive_ = false;
+  }
+}
diff --git a/sw/host/tests/usbdev/usbdev_stream/usbdev_int.h b/sw/host/tests/usbdev/usbdev_stream/usbdev_int.h
new file mode 100644
index 0000000..2e751c5
--- /dev/null
+++ b/sw/host/tests/usbdev/usbdev_stream/usbdev_int.h
@@ -0,0 +1,150 @@
+// Copyright lowRISC contributors (OpenTitan project).
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+#ifndef OPENTITAN_SW_HOST_TESTS_USBDEV_USBDEV_STREAM_USBDEV_INT_H_
+#define OPENTITAN_SW_HOST_TESTS_USBDEV_USBDEV_STREAM_USBDEV_INT_H_
+#include <queue>
+
+#include "usb_device.h"
+#include "usbdev_stream.h"
+
+// Interrupt and Bulk Transfer-based streams to usbdev; as far as the host-
+// and device-side code at application level is concerned, these are the same.
+//
+// The differences are at the service/delivery level offered by the lower USB
+// layers.
+class USBDevInt : public USBDevStream {
+ public:
+  USBDevInt(USBDevice *dev, bool bulk, unsigned id, uint32_t transfer_bytes,
+            bool retrieve, bool check, bool send, bool verbose)
+      : USBDevStream(id, transfer_bytes, retrieve, check, send, verbose),
+        dev_(dev),
+        bulk_(bulk),
+        failed_(false),
+        inActive_(false),
+        outActive_(false),
+        xfrIn_(nullptr),
+        xfrOut_(nullptr) {}
+  /**
+   * Open an Interrupt connection to specified device interface.
+   *
+   * @param  interface  Interface number.
+   * @return The success of the operation.
+   */
+  bool Open(unsigned interface);
+  /**
+   * Finalize the stream, prior to shutting down.
+   */
+  virtual void Stop();
+  /**
+   * Pause the stream, prior to suspending the device.
+   */
+  virtual void Pause();
+  /**
+   * Resume stremaing.
+   */
+  virtual bool Resume();
+  /**
+   * Return a summary report of the stream settings or status.
+   *
+   * @param  status    Indicates whether settings or status requested.
+   * @param  verbose   true iff a more verbose report is required.
+   * @return Status report
+   */
+  virtual std::string Report(bool status = false, bool verbose = false) const;
+  /**
+   * Service this Interrupt stream.
+   *
+   * @return true iff the stream is still operational.
+   */
+  virtual bool Service();
+
+ private:
+  /**
+   * Diagnostic utility function to display the content of libusb Bulk/Interrupt
+   * transfer.
+   *
+   * @param  xfr     The Interrupt transfer to be displayed.
+   */
+  void DumpIntTransfer(struct libusb_transfer *xfr) const;
+  /**
+   * Retrieving of IN traffic from device.
+   *
+   * @return true iff the stream is still operational.
+   */
+  bool ServiceIN();
+  /**
+   * Sending of OUT traffic to device.
+   *
+   * @return true iff the stream is still operational.
+   */
+  bool ServiceOUT();
+  /**
+   * Callback function supplied to libusb for IN transfers; transfer has
+   * completed and requires attention.
+   *
+   * @param  xfr     The transfer that has completed.
+   */
+  void CallbackIN(struct libusb_transfer *xfr);
+  /**
+   * Callback function supplied to libusb for OUT transfers; transfer has
+   * completed and requires attention.
+   *
+   * @param  xfr     The transfer that has completed.
+   */
+  void CallbackOUT(struct libusb_transfer *xfr);
+  /**
+   * Stub callback function supplied to libusb for IN transfers.
+   *
+   * @param  xfr     The transfer that has completed.
+   */
+  static void LIBUSB_CALL CbStubIN(struct libusb_transfer *xfr);
+  /**
+   * Stub callback function supplied to libusb for OUT transfers.
+   *
+   * @param  xfr     The transfer that has completed.
+   */
+  static void LIBUSB_CALL CbStubOUT(struct libusb_transfer *xfr);
+
+  // USB device.
+  USBDevice *dev_;
+
+  // Bulk Transfer Type?
+  bool bulk_;
+
+  // The number of the interface being used by this stream.
+  unsigned interface_;
+
+  // Has this stream experienced a failure?
+  bool failed_;
+
+  // Is an IN transfer in progress?
+  bool inActive_;
+
+  // Is an OUT transfer in progress?
+  bool outActive_;
+
+  // Do we currently have an IN transfer?
+  struct libusb_transfer *xfrIn_;
+
+  // Do we currently have an OUT transfer?
+  struct libusb_transfer *xfrOut_;
+
+  // Maximum packet size for this stream.
+  uint8_t maxPacketSize_;
+
+  // Endpoint numbers used by this stream.
+  uint8_t epIn_;
+  uint8_t epOut_;
+
+  // Stream signature.
+  // Note: this is constructed piecemeal as the bytes are received from the
+  // device.
+  usbdev_stream_sig_t sig_;
+
+  // No timeout at present; the device-side code is responsible for signaling
+  // test completion/failure. This may need to change for CI tests.
+  static constexpr unsigned kDataTimeout = 0U;
+};
+
+#endif  // OPENTITAN_SW_HOST_TESTS_USBDEV_USBDEV_STREAM_USBDEV_INT_H_
diff --git a/sw/host/tests/usbdev/usbdev_stream/usbdev_serial.cc b/sw/host/tests/usbdev/usbdev_stream/usbdev_serial.cc
new file mode 100644
index 0000000..2877ad7
--- /dev/null
+++ b/sw/host/tests/usbdev/usbdev_stream/usbdev_serial.cc
@@ -0,0 +1,175 @@
+// Copyright lowRISC contributors (OpenTitan project).
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+#include "usbdev_serial.h"
+
+#include <cstdio>
+#include <iostream>
+#include <unistd.h>
+
+#include "stream_test.h"
+#include "usbdev_utils.h"
+
+USBDevSerial::~USBDevSerial() { Stop(); }
+
+// Iniitialise a stream between the specified input and output ports.
+bool USBDevSerial::Open(const char *in_name, const char *out_name) {
+  // Take a copy of the port names.
+  inPort_ = in_name;
+  outPort_ = out_name;
+  return OpenPorts();
+}
+
+// Open the input and output ports to the board/device for this stream.
+bool USBDevSerial::OpenPorts() {
+  in_ = port_open(inPort_.c_str(), false);
+  if (in_ < 0) {
+    return false;
+  }
+  out_ = port_open(outPort_.c_str(), true);
+  if (out_ < 0) {
+    close(in_);
+    return false;
+  }
+
+  if (cfg.verbose) {
+    printf("S%u: input '%s' (%d) output '%s' (%d)\n", id_, inPort_.c_str(), in_,
+           outPort_.c_str(), out_);
+  }
+
+  return true;
+}
+
+void USBDevSerial::Stop() {
+  // Close any open port handles.
+  if (in_ >= 0) {
+    close(in_);
+    in_ = -1;
+  }
+  if (out_ >= 0) {
+    close(out_);
+    out_ = -1;
+  }
+}
+
+void USBDevSerial::Pause() {
+  // This is the same behavior as stopping.
+  Stop();
+}
+
+bool USBDevSerial::Resume() {
+  // I don't think we can expect this to suspend/resume at the byte level
+  // anyway; we seem to resume at a different point in the LFSR, presumably
+  // because some internal buffer within the kernel/driver level dumps some
+  // bytes when we close the handle.
+  return OpenPorts();
+}
+
+// Return a summary report of the stream settings of status.
+std::string USBDevSerial::Report(bool status, bool verbose) const { return ""; }
+
+// Sending of OUT traffic to device.
+bool USBDevSerial::ServiceOUT() {
+  uint8_t *data;
+  size_t to_send = DataAvailable(&data);
+  if (to_send > 0U) {
+    ssize_t nsent;
+    if (send_) {
+      if (cfg.verbose) {
+        std::cout << PrefixID() << "Trying to send " << to_send << "byte(s)"
+                  << std::endl;
+      }
+      // Propagate the modified bytes to the output port.
+      nsent = send_bytes(out_, &buf_.data[buf_.rd_idx], to_send);
+      if (nsent < 0) {
+        return false;
+      }
+    } else {
+      nsent = to_send;
+    }
+
+    if (cfg.verbose) {
+      std::cout << PrefixID() << (send_ ? "Sent" : "Dropped") << nsent
+                << " byte(s)" << std::endl;
+    }
+
+    ConsumeData(nsent);
+  }
+
+  return true;
+}
+
+// Retrieving of IN traffic from device.
+bool USBDevSerial::ServiceIN() {
+  // Decide how many bytes to try to read into our buffer.
+  uint8_t *dp;
+  uint32_t space_bytes = SpaceAvailable(&dp);
+
+  uint32_t to_fetch = space_bytes;
+  if (to_fetch > transfer_bytes_ - bytes_recvd_) {
+    to_fetch = transfer_bytes_ - bytes_recvd_;
+  }
+
+  ssize_t nrecvd;
+  if (!SigReceived() || retrieve_) {
+    // Read as many bytes as we can from the input port.
+    nrecvd = recv_bytes(in_, dp, to_fetch);
+    if (nrecvd < 0) {
+      return false;
+    }
+
+    // Update the circular buffer with the amount of data that we've written.
+    CommitData(nrecvd);
+
+    if (nrecvd > 0 && !SigReceived()) {
+      uint32_t dropped = SigDetect(&sig_, dp, (uint32_t)nrecvd);
+
+      // Consume stream signature, rather than propagating it to the output
+      // side.
+      if (SigReceived()) {
+        SigProcess(sig_);
+        dropped += sizeof(usbdev_stream_sig_t);
+      }
+
+      // Skip past any dropped bytes, including the signature, so that if there
+      // are additional bytes we may process them.
+      nrecvd = ((uint32_t)nrecvd > dropped) ? ((uint32_t)nrecvd - dropped) : 0;
+      dp += dropped;
+
+      if (dropped) {
+        DiscardData(dropped);
+      }
+    }
+  } else {
+    // Generate a stream of bytes _as if_ we'd received them correctly from
+    // the device.
+    GenerateData(dp, to_fetch);
+    nrecvd = to_fetch;
+
+    // Update the circular buffer with the amount of data that we've written.
+    CommitData(nrecvd);
+  }
+
+  bool ok = true;
+  if (nrecvd > 0) {
+    // Check the received LFSR-generated byte(s) and combine them with the
+    // output of our host-side LFSR.
+    ok = ProcessData(dp, nrecvd);
+  }
+
+  return ok;
+}
+
+// Service this stream.
+bool USBDevSerial::Service() {
+  // The base class may perform some diagnostic reporting common to all streams.
+  bool ok = USBDevStream::Service();
+  if (ok) {
+    // Handle OUT traffic first to try to create more buffer space.
+    ok = ServiceOUT();
+    if (ok) {
+      ok = ServiceIN();
+    }
+  }
+  return ok;
+}
diff --git a/sw/host/tests/usbdev/usbdev_stream/usbdev_serial.h b/sw/host/tests/usbdev/usbdev_stream/usbdev_serial.h
new file mode 100644
index 0000000..04ad93e
--- /dev/null
+++ b/sw/host/tests/usbdev/usbdev_stream/usbdev_serial.h
@@ -0,0 +1,91 @@
+// Copyright lowRISC contributors (OpenTitan project).
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+#ifndef OPENTITAN_SW_HOST_TESTS_USBDEV_USBDEV_STREAM_USBDEV_SERIAL_H_
+#define OPENTITAN_SW_HOST_TESTS_USBDEV_USBDEV_STREAM_USBDEV_SERIAL_H_
+#include "usbdev_stream.h"
+
+/**
+ * Bulk Transfer Stream over ttyUSBn serial connection using File Descriptors.
+ */
+class USBDevSerial : public USBDevStream {
+ public:
+  USBDevSerial(unsigned id, uint32_t transfer_bytes, bool retrieve, bool check,
+               bool send, bool verbose)
+      : USBDevStream(id, transfer_bytes, retrieve, check, send, verbose) {}
+  ~USBDevSerial();
+
+  /**
+   * Open the input and output ports to the board/device for this stream.
+   *
+   * @param  in_name   Device name of input serial port.
+   * @param  out_name  Device name of output serial port.
+   * @return true iff successful.
+   */
+  bool Open(const char *in_name, const char *out_name);
+  /**
+   * Finalize the stream, prior to shutting down.
+   */
+  virtual void Stop();
+  /**
+   * Pause the stream, prior to suspending the device.
+   */
+  virtual void Pause();
+  /**
+   * Resume stremaing.
+   */
+  virtual bool Resume();
+  /**
+   * Return a summary report of the stream settings or status.
+   *
+   * @param  status    Indicates whether settings or status requested.
+   * @param  verbose   true iff a more verbose report is required.
+   * @return Status report
+   */
+  virtual std::string Report(bool status = false, bool verbose = false) const;
+  /**
+   * Service this serial stream.
+   *
+   * @return true iff the stream is still operational.
+   */
+  virtual bool Service();
+
+ private:
+  /**
+   * Retrieving of IN traffic from device.
+   *
+   * @return true iff the stream is still operational.
+   */
+  bool ServiceIN();
+  /**
+   * Sending of OUT traffic to device.
+   *
+   * @return true iff the stream is still operational.
+   */
+  bool ServiceOUT();
+  /**
+   * Open the input and output ports for this stream.
+   *
+   * @return true iff opened successfully.
+   */
+  bool OpenPorts();
+
+  // Input port handle.
+  int in_;
+
+  // Output port handle.
+  int out_;
+
+  // Input port name.
+  std::string inPort_;
+
+  // Output port name.
+  std::string outPort_;
+
+  // Stream signature.
+  // Note: this is constructed piecemeal as the bytes are received from the
+  // device.
+  usbdev_stream_sig_t sig_;
+};
+
+#endif  // OPENTITAN_SW_HOST_TESTS_USBDEV_USBDEV_STREAM_USBDEV_SERIAL_H_
diff --git a/sw/host/tests/usbdev/usbdev_stream/usbdev_stream.cc b/sw/host/tests/usbdev/usbdev_stream/usbdev_stream.cc
new file mode 100644
index 0000000..c950c46
--- /dev/null
+++ b/sw/host/tests/usbdev/usbdev_stream/usbdev_stream.cc
@@ -0,0 +1,391 @@
+// Copyright lowRISC contributors (OpenTitan project).
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+#include "usbdev_stream.h"
+
+#include <cassert>
+#include <cstdio>
+#include <cstring>
+#include <iostream>
+
+#include "stream_test.h"
+#include "usb_device.h"
+#include "usbdev_utils.h"
+
+// 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)
+
+// Simple LFSR for 8-bit sequences.
+#define LFSR_ADVANCE(lfsr) \
+  (((lfsr) << 1) ^         \
+   ((((lfsr) >> 1) ^ ((lfsr) >> 2) ^ ((lfsr) >> 3) ^ ((lfsr) >> 7)) & 1u))
+
+USBDevStream::USBDevStream(unsigned id, uint32_t transfer_bytes, bool retrieve,
+                           bool check, bool send, bool verbose) {
+  // Remember Stream IDentifier and flags.
+  SetProperties(id, retrieve, check, send);
+  verbose_ = verbose;
+
+  // Stream is not in the process of being closed.
+  closing_ = false;
+
+  // Not yet received stream signature.
+  sig_recvd_ = kSigStateStart;
+
+  // Initialise LFSR state.
+  tst_lfsr_ = USBTST_LFSR_SEED(id);
+  dpi_lfsr_ = USBDPI_LFSR_SEED(id);
+
+  // Initialize circular buffer.
+  buf_.wr_idx = 0U;
+  buf_.rd_idx = 0U;
+  buf_.end_idx = 0U;
+
+  // Number of bytes to be transferred.
+  transfer_bytes_ = transfer_bytes;
+
+  // Total counts of bytes received and sent.
+  bytes_recvd_ = 0U;
+  bytes_sent_ = 0U;
+}
+
+// Detect a stream signature within the byte stream;
+// simple restartable parser that drops all bytes until we find a header
+// signature.
+//
+// The signature permits usbdev_stream_test to pass test parameters to this
+// program as well as overcoming the issue of the mapping from device
+// endpoints/streams to USB ports not being under our control.
+uint32_t USBDevStream::SigDetect(usbdev_stream_sig_t *sig, const uint8_t *sp,
+                                 uint32_t nrecv) {
+  uint32_t dropped = 0U;
+  assert(nrecv > 0);
+  do {
+    if (sig_recvd_ == kSigStateStart) {
+      sig_recvd_ = kSigStateCheckHead;
+      sig_cnt_ = 0U;
+    }
+
+    // Collect the signature into a structure with appropriate alignment
+    // for direct access to members.
+    uint8_t *sigp = reinterpret_cast<uint8_t *>(sig);
+    sigp[sig_cnt_] = *sp;
+
+    // Advance the parser beyond this byte
+    // Note: valid signature bytes remain excluded from the returned count of
+    // dropped bytes.
+    const size_t tail_offset = offsetof(usbdev_stream_sig_t, tail_sig);
+    bool discard = false;
+    switch (sig_recvd_) {
+      // Check the bytes of the header signature.
+      case kSigStateCheckHead: {
+        const unsigned sh = sig_cnt_ << 3;
+        uint8_t match = (uint8_t)(STREAM_SIGNATURE_HEAD >> sh);
+        if (match == *sp) {
+          if (++sig_cnt_ >= 4U) {
+            sig_recvd_ = kSigStateSkipBody;
+          }
+        } else {
+          discard = true;
+        }
+      } break;
+
+      // Just collect the signature body for later validation.
+      case kSigStateSkipBody: {
+        if (++sig_cnt_ >= tail_offset) {
+          sig_recvd_ = kSigStateCheckTail;
+        }
+      } break;
+
+      // Check the bytes of the tail signature
+      case kSigStateCheckTail: {
+        const unsigned sh = (sig_cnt_ - tail_offset) << 3;
+        const uint8_t match = (uint8_t)(STREAM_SIGNATURE_TAIL >> sh);
+        if (match == *sp) {
+          if (sig_cnt_ >= sizeof(usbdev_stream_sig_t) - 1U) {
+            // Note: at this point we could use the signature words to identify
+            // the Endianness of the sender and thus reverse the other fields
+            // if necessary.
+
+            // Basic sanity check of signature values before we accept it;
+            // some of the checking must be stream-type dependent, so this is
+            // not a complete guarantee of validity.
+            uint8_t stream = sig->stream & USBDevice::kUsbdevStreamFlagID;
+            if (sig->num_bytes > 0U && sig->num_bytes < 0x10000000U &&
+                stream < STREAMS_MAX) {
+              if (verbose_) {
+                std::cout << PrefixID() << "Signature accepted" << std::endl;
+              }
+              sig_recvd_ = kSigStateReceived;
+            } else {
+              std::cout << PrefixID() << "Signature rejected" << std::endl;
+              // Report the signature to explain why it's been rejected.
+              SigReport(*sig);
+              discard = true;
+            }
+          } else {
+            sig_cnt_++;
+          }
+        } else {
+          discard = true;
+        }
+      } break;
+
+      default:
+        // Note: should not be called once we have a valid stream signature.
+        assert(!"Invalid/undefined sig_recvd state");
+        break;
+    }
+
+    if (discard) {
+      // Mismatch; discard the checked data, and resume from the beginning.
+      dropped += sig_cnt_ + 1U;
+      sig_recvd_ = kSigStateStart;
+      sig_cnt_ = 0U;
+    }
+    // Advance to next byte.
+    sp++;
+  } while (--nrecv > 0U && sig_recvd_ != kSigStateReceived);
+
+  return dropped;
+}
+
+void USBDevStream::SigProcess(const usbdev_stream_sig_t &sig) {
+  // Stream IDentifier and flags.
+  uint8_t stream = sig.stream & USBDevice::kUsbdevStreamFlagID;
+  bool retrieve = ((sig.stream & USBDevice::kUsbdevStreamFlagRetrieve) != 0U);
+  bool check = ((sig.stream & USBDevice::kUsbdevStreamFlagCheck) != 0U);
+  bool send = ((sig.stream & USBDevice::kUsbdevStreamFlagSend) != 0U);
+  SetProperties(stream, retrieve, check, send);
+
+  // If this is the start of the sequence then we also have the number of bytes
+  // to be transferred.
+  if (!(sig.seq_hi | sig.seq_lo)) {
+    transfer_bytes_ = sig.num_bytes;
+  }
+}
+
+void USBDevStream::SigReport(const usbdev_stream_sig_t &sig) {
+  // Sequence number; important for unreliable packet-based streams
+  // (Isochronous streams).
+  uint16_t seq = sig_read8(offsetof(usbdev_stream_sig_t, seq_lo)) |
+                 (sig_read8(offsetof(usbdev_stream_sig_t, seq_hi)) << 8);
+
+  // Stream IDentifier and flags.
+  uint8_t stream = sig.stream & USBDevice::kUsbdevStreamFlagID;
+  bool retrieve = ((sig.stream & USBDevice::kUsbdevStreamFlagRetrieve) != 0U);
+  bool check = ((sig.stream & USBDevice::kUsbdevStreamFlagCheck) != 0U);
+  bool send = ((sig.stream & USBDevice::kUsbdevStreamFlagSend) != 0U);
+
+  printf("Signature detected: stream #%u LFSR 0x%02x bytes 0x%x\n", stream,
+         sig.init_lfsr, sig.num_bytes);
+  printf(" - retrieve %c check %c send %c\n", retrieve ? 'Y' : 'N',
+         check ? 'Y' : 'N', send ? 'Y' : 'N');
+  printf(" - seq #%u\n", seq);
+}
+
+// Return an indication of whether this stream has completed its transfer.
+bool USBDevStream::Completed() const {
+  return (bytes_recvd_ >= transfer_bytes_) && (bytes_sent_ >= transfer_bytes_);
+}
+
+bool USBDevStream::ProvisionSpace(uint8_t **space, uint32_t len) {
+  uint32_t space_bytes = SpaceAvailable(space);
+  if (space_bytes >= len) {
+    return true;
+  }
+
+  // If we're failing to allocate sufficient space right at the end of the
+  // buffer, wrap sooner.
+  if (buf_.rd_idx <= buf_.wr_idx && (kBufferSize - buf_.wr_idx) < len &&
+      buf_.rd_idx > len) {
+    buf_.end_idx = buf_.wr_idx;
+    buf_.wr_idx = 0U;
+    // Return pointer to the start of the free space.
+    if (space) {
+      *space = buf_.data;
+    }
+    return true;
+  }
+
+  return false;
+}
+
+uint32_t USBDevStream::SpaceAvailable(uint8_t **space) {
+  uint32_t space_bytes = kBufferSize - buf_.wr_idx;
+  uint8_t *start = &buf_.data[buf_.wr_idx];
+
+  if (buf_.rd_idx > buf_.wr_idx) {
+    // All space is contiguous.
+    space_bytes = (buf_.rd_idx - 1U) - buf_.wr_idx;
+  } else if (!buf_.rd_idx) {
+    // Leave one unused byte at the end of the circular buffer.
+    space_bytes--;
+  }
+
+  // Return pointer to the start of the free space.
+  if (space) {
+    *space = start;
+  }
+  return space_bytes;
+}
+
+bool USBDevStream::AddData(const uint8_t *data, uint32_t len) {
+  // Ascertain the amount of space available.
+  uint32_t space_bytes = SpaceAvailable(nullptr);
+  if (space_bytes < len) {
+    return false;
+  }
+
+  // Size of first contiguous chunk.
+  uint32_t chunk = kBufferSize - buf_.wr_idx;
+  if (len < chunk) {
+    chunk = len;
+  }
+  if (data) {
+    // Data is not already present in the buffer.
+    memcpy(&buf_.data[buf_.wr_idx], data, chunk);
+    data += chunk;
+  }
+  buf_.wr_idx += chunk;
+  if (buf_.wr_idx > buf_.end_idx) {
+    buf_.end_idx = buf_.wr_idx;
+  }
+
+  // Write index wraparound.
+  if (buf_.wr_idx >= kBufferSize) {
+    buf_.wr_idx = 0U;
+  }
+
+  return true;
+}
+
+void USBDevStream::GenerateData(uint8_t *dp, uint32_t len) {
+  // Generate a stream of bytes _as if_ we'd received them correctly from
+  // the device
+  uint8_t next_lfsr = tst_lfsr_;
+  for (unsigned idx = 0U; idx < len; idx++) {
+    dp[idx] = next_lfsr;
+    next_lfsr = LFSR_ADVANCE(next_lfsr);
+  }
+}
+
+bool USBDevStream::ProcessData(uint8_t *dp, uint32_t len) {
+  bool ok = true;
+
+  if (len > 0U) {
+    // Record the time of the first data reception.
+    if (!received) {
+      start_time = time_us();
+      received = true;
+    }
+
+    if (verbose_) {
+      std::cout << "S" << ID()
+                << (cfg.retrieve ? ": Received " : ": Generated ") << len
+                << " byte(s)" << std::endl;
+    }
+
+    // We can just check and overwrite the input data in-situ.
+    const uint8_t *sp = dp;
+    for (uint32_t idx = 0U; idx < len; idx++) {
+      uint8_t expected = tst_lfsr_;
+      uint8_t recvd = sp[idx];
+
+      // Check whether the received byte is as expected.
+      if (retrieve_ && check_) {
+        if (recvd != expected) {
+          printf("S%u: Mismatched data from device 0x%02x, expected 0x%02x\n",
+                 id_, recvd, expected);
+          ok = false;
+        }
+      }
+
+      // Simply XOR the two LFSR-generated streams together.
+      dp[idx] = recvd ^ dpi_lfsr_;
+      if (verbose_) {
+        printf("S%u: 0x%02x <- 0x%02x ^ 0x%02x\n", id_, dp[idx], recvd,
+               dpi_lfsr_);
+      }
+
+      // Advance our LFSRs.
+      tst_lfsr_ = LFSR_ADVANCE(tst_lfsr_);
+      dpi_lfsr_ = LFSR_ADVANCE(dpi_lfsr_);
+    }
+
+    // Update the buffer writing state.
+    bytes_recvd_ += len;
+  }
+
+  return ok;
+}
+
+uint32_t USBDevStream::DataAvailable(uint8_t **data) {
+  // Read index wraparound.
+  if (buf_.rd_idx >= buf_.end_idx && buf_.wr_idx > 0U &&
+      buf_.wr_idx < buf_.rd_idx) {
+    buf_.rd_idx = 0U;
+  }
+
+  uint32_t data_bytes = buf_.end_idx - buf_.rd_idx;
+  uint8_t *start = &buf_.data[buf_.rd_idx];
+
+  if (buf_.wr_idx >= buf_.rd_idx) {
+    data_bytes = buf_.wr_idx - buf_.rd_idx;
+  }
+  if (data) {
+    *data = start;
+  }
+  return data_bytes;
+}
+
+bool USBDevStream::DiscardData(uint32_t len) {
+  // Ascertain the amount of data available.
+  uint32_t data_bytes = DataAvailable(nullptr);
+  if (data_bytes < len) {
+    return false;
+  }
+
+  // Adjust buffer state to discard the specified number of bytes.
+  uint32_t chunk = buf_.end_idx - buf_.rd_idx;
+  assert(len <= chunk);
+  if (len < chunk || (len == chunk && buf_.wr_idx >= buf_.end_idx)) {
+    buf_.rd_idx += len;
+  } else {
+    buf_.rd_idx = len - chunk;
+  }
+
+  return true;
+}
+
+bool USBDevStream::ConsumeData(uint32_t len) {
+  if (!DiscardData(len)) {
+    return false;
+  }
+  // Update the count of transmitted bytes.
+  bytes_sent_ += len;
+  return true;
+}
+
+void USBDevStream::ClearBuffer() {
+  // Check that the write index is valid before we advance the read index to
+  // clear the buffer.
+  assert(buf_.wr_idx < kBufferSize);
+  assert(buf_.wr_idx <= buf_.end_idx);
+  buf_.rd_idx = buf_.wr_idx;
+}
+
+// Service the given data stream; this base class implementation just provides
+// some generic progress reporting.
+bool USBDevStream::Service() {
+  if (verbose_) {
+    printf("S%u : rd_idx 0x%x wr_idx 0x%x\n", id_, buf_.rd_idx, buf_.wr_idx);
+  }
+  return true;
+}
diff --git a/sw/host/tests/usbdev/usbdev_stream/usbdev_stream.h b/sw/host/tests/usbdev/usbdev_stream/usbdev_stream.h
new file mode 100644
index 0000000..9012422
--- /dev/null
+++ b/sw/host/tests/usbdev/usbdev_stream/usbdev_stream.h
@@ -0,0 +1,426 @@
+// Copyright lowRISC contributors (OpenTitan project).
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+#ifndef OPENTITAN_SW_HOST_TESTS_USBDEV_USBDEV_STREAM_USBDEV_STREAM_H_
+#define OPENTITAN_SW_HOST_TESTS_USBDEV_USBDEV_STREAM_USBDEV_STREAM_H_
+#include <cstddef>
+#include <cstdint>
+#include <string>
+#include <sys/types.h>
+
+/**
+ * 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
+   * Note: for Isochronous Transfers, this is the initial value of the sender's
+   *       LFSR for _this packet_.
+   */
+  uint8_t init_lfsr;
+  /**
+   * Stream number and flags.
+   */
+  uint8_t stream;
+  /**
+   * Sequence number, low part; for non-Isochronous streams this will always be
+   * zero because a signature is used only at the start of the data stream.
+   */
+  uint8_t seq_lo;
+  /**
+   * Sequence number, high part; for non-Isochronous streams this will always be
+   * zero because a signature is used only at the start of the data stream.
+   */
+  uint8_t seq_hi;
+  /**
+   * Number of bytes to be transferred.
+   */
+  uint32_t num_bytes;
+  /**
+   * Tail signature word.
+   */
+  uint32_t tail_sig;
+} usbdev_stream_sig_t;
+
+// Data stream.
+class USBDevStream {
+ public:
+  USBDevStream(unsigned id, uint32_t num_bytes, bool retrieve, bool check,
+               bool send, bool verbose);
+  virtual ~USBDevStream() {}
+
+  /**
+   * Stream implementations.
+   */
+  enum StreamType {
+    // File descriptor access to serial port.
+    StreamType_Serial,
+    // Standard USB Endpoint/Transfer Types all accessed via libusb.
+    StreamType_Bulk,
+    StreamType_Interrupt,
+    StreamType_Isochronous,
+    StreamType_Control
+  };
+  /**
+   * Record whether or not the stream is in the process of closing
+   * (shutting down or suspending).
+   *
+   * @param  closing   Whether or not the stream is now closing.
+   */
+  void SetClosing(bool closing) { closing_ = closing; }
+  /**
+   * Indicate whether an additional I/O transfer may safely be scheduled
+   * on this stream, eg. it is not suspending/shutting down.
+   *
+   * @return true iff another I/O transfer may be scheduled.
+   */
+  bool CanSchedule() const { return !closing_; }
+  /**
+   * Finalize the stream, prior to shutting down.
+   */
+  virtual void Stop() = 0;
+  /**
+   * Pause the stream, prior to suspending the device.
+   */
+  virtual void Pause() = 0;
+  /**
+   * Resume streaming.
+   */
+  virtual bool Resume() = 0;
+  /**
+   * Return the Stream IDentifier of this stream.
+   */
+  unsigned ID() const { return id_; }
+  /**
+   * Return a Stream IDentifier prefix suitable for logging/reporting.
+   */
+  std::string PrefixID() {
+    std::string s("S");
+    s += std::to_string(id_);
+    s += ": ";
+    return s;
+  }
+  /**
+   * Return a summary report of the stream settings or status.
+   *
+   * @param  status    Indicates whether settings or status requested.
+   * @param  verbose   true iff a more verbose report is required.
+   * @return Status report.
+   */
+  virtual std::string Report(bool status = false,
+                             bool verbose = false) const = 0;
+  /**
+   * Set Stream IDentifier and flags.
+   */
+  void SetProperties(unsigned id, bool retrieve, bool check, bool send) {
+    id_ = id;
+    retrieve_ = retrieve;
+    check_ = check;
+    send_ = send;
+  }
+  /**
+   * Service this stream.
+   *
+   * @return         true iff test should continue, false indicates error.
+   */
+  virtual bool Service();
+  /**
+   * Indicates whether this stream has completed its transfer.
+   *
+   * @return         true iff this stream has nothing more to do.
+   */
+  virtual bool Completed() const;
+  /**
+   * Returns the total number of bytes to be transferred by this stream.
+   *
+   * @param          Number of bytes to be transferred.
+   */
+  uint32_t TransferBytes() const { return transfer_bytes_; }
+  /**
+   * Returns a count of the number of bytes received from the device
+   *
+   * @return         Number of bytes received.
+   */
+  uint32_t BytesRecvd() const { return bytes_recvd_; }
+  /**
+   * Returns a count of the number of bytes sent to the device.
+   *
+   * @return         Number of bytes sent.
+   */
+  uint32_t BytesSent() const { return bytes_sent_; }
+  /**
+   * Return the textual name of the given stream type.
+   *
+   * @param  type    Stream type.
+   * @return name of stream type.
+   */
+  static const char *StreamTypeName(StreamType type) {
+    switch (type) {
+      case StreamType_Serial:
+        return "Serial";
+      case StreamType_Bulk:
+        return "Bulk";
+      case StreamType_Interrupt:
+        return "Interrupt";
+      case StreamType_Isochronous:
+        return "Isochronous";
+      case StreamType_Control:
+        return "Control";
+      default:
+        return "<Unknown>";
+    }
+  }
+
+ protected:
+  /**
+   * Provision the given number of bytes of contiguous space,
+   * and optionally a pointer to the start of the free space.
+   *
+   * @param  space   Receives the pointer to the start of the free space,
+   *                 or NULL iff pointer not required.
+   * @param  len     Amount of space (in bytes) to provision.
+   * @return true iff the operation was successful.
+   */
+  bool ProvisionSpace(uint8_t **space, uint32_t len);
+  /**
+   * Returns the amount of contiguous free space available in the buffer,
+   * and optionally a pointer to the start of the free space.
+   *
+   * @param  space    Receives the pointer to the start of the free space,
+   *                  or NULL iff pointer not required.
+   * @return The contiguous free space available (in bytes).
+   */
+  uint32_t SpaceAvailable(uint8_t **space);
+  /**
+   * Add the specified number of bytes to the circular buffer; if `data` is NULL
+   * then the bytes shall already be present in the buffer and copying is not
+   * performed.
+   *
+   * @param  data    The data to be added to the buffer, or NULL
+   * @param  len     The number of bytes to be added.
+   * @return The success of the operation.
+   */
+  bool AddData(const uint8_t *data, uint32_t len);
+  /**
+   * Record that the specified number of bytes have been added to the circular
+   * buffer. The space must already have provisioned and the data bytes shall
+   * already be in the buffer.
+   */
+  bool CommitData(uint32_t len) { return AddData(nullptr, len); }
+  /**
+   * CLear the circular buffer by removing all of its contained data bytes.
+   *
+   * Note: this is achieved by advancing the read index to match the current
+   * write index, so all committed/added data is discarded, by any write data
+   * that has only been provisioned at this point does remain valid and may
+   * still be committed subsequently.
+   */
+  void ClearBuffer();
+
+  /**
+   * States in reception of signature.
+   */
+  typedef enum {
+    kSigStateStart = 0,
+    kSigStateCheckHead,
+    kSigStateSkipBody,
+    kSigStateCheckTail,
+    // Signature has been correctly received.
+    kSigStateReceived,
+  } sig_state_t;
+
+  /**
+   * Reset the signature detection; Isochronous streams including a
+   * new signature at the start of each packet transferred.
+   */
+  void SigReset() {
+    sig_recvd_ = kSigStateStart;
+    sig_cnt_ = 0U;
+  }
+  /**
+   * Has valid signature been received on this stream? Note that there may be
+   * some additional validity checks required for specific= transfer types.
+   *
+   * @return true iff a signature has been received and detected.
+   */
+  bool SigReceived() const { return (sig_recvd_ == kSigStateReceived); }
+  /**
+   * Collect stream flags from the supplied signature.
+   *
+   * @param  sig     The latest stream signature received.
+   */
+  void SigProcess(const usbdev_stream_sig_t &sig);
+  /**
+   * Detect and parse stream/packet signature,
+   * returning a count of the number of bytes to be discarded from the start
+   * of this data. Usually this is zero, but if a valid stream signature is
+   * required, bytes must be discarded until the signature is received.
+   */
+  uint32_t SigDetect(usbdev_stream_sig_t *sig, const uint8_t *sp,
+                     uint32_t nrecv);
+  /**
+   * Diagnostic utility function to report the contents of a stream/packet
+   * signature.
+   *
+   * @param  sig     Stream signature to be reported.
+   */
+  void SigReport(const usbdev_stream_sig_t &sig);
+  /**
+   * Generate a sequence of bytes _as if_ we'd received them correctly from the
+   * device.
+   */
+  void GenerateData(uint8_t *dp, uint32_t len);
+  /**
+   * Process the given sequence of bytes according to the current stream state.
+   */
+  bool ProcessData(uint8_t *dp, uint32_t len);
+  /**
+   * Return the number of contiguous bytes of data available in the stream
+   * buffer, and a pointer to the first byte of data. This may be fewer than the
+   * total number of bytes in the buffer, if the data wraps at the end of the
+   * circular buffer.
+   *
+   * @param  data   Receives the pointer to the first data byte.
+   * @return The number of contiguous data bytes available.
+   */
+  uint32_t DataAvailable(uint8_t **data);
+  /**
+   * Update the stream buffer to indicate that data has been discarded
+   * (removed from the buffer but not sent to the USB device).
+   *
+   * @param  len    Number of bytes of data consumed.
+   * @return true iff the buffer was successfully updated.
+   */
+  bool DiscardData(uint32_t len);
+  /**
+   * Update the stream buffer to indicate that data has been consumed.
+   *
+   * @param  len    Number of bytes of data consumed.
+   * @return true iff the buffer was successfully updated.
+   */
+  bool ConsumeData(uint32_t len);
+
+  /**
+   * Size of circular buffer used for streaming.
+   */
+  static constexpr uint32_t kBufferSize = 0x10000U;
+
+  // Utility function for collecting a byte from the stream signature, handling
+  // wrap around at the end of the circular buffer.
+  inline uint8_t sig_read8(size_t offset) {
+    uint32_t rd_idx = buf_.rd_idx + offset;
+    if (rd_idx >= kBufferSize) {
+      rd_idx -= kBufferSize;
+    }
+    return buf_.data[rd_idx];
+  }
+
+  // Utility function for collecting a 16-bit word from the stream signature,
+  // handling wrap around at the end of the circular buffer.
+  inline uint16_t sig_read16(size_t offset) {
+    uint32_t rd_idx = buf_.rd_idx + offset;
+    if (rd_idx >= kBufferSize) {
+      rd_idx -= kBufferSize;
+    }
+    uint16_t d = buf_.data[rd_idx++];
+    if (rd_idx >= kBufferSize) {
+      rd_idx -= kBufferSize;
+    }
+    return d | (buf_.data[rd_idx++] << 8);
+  }
+
+  // Utility function for collecting a 32-bit word from the stream signature,
+  // handling wrap around at the end of the circular buffer.
+  inline uint32_t sig_read32(size_t offset) {
+    uint32_t rd_idx = buf_.rd_idx + offset;
+    unsigned n = 4U;
+    uint32_t d = 0U;
+    while (n-- > 0U) {
+      if (rd_idx >= kBufferSize) {
+        rd_idx -= kBufferSize;
+      }
+      // Transmission of multi-byte value is little endian.
+      d = (d >> 8) | (buf_.data[rd_idx++] << 24);
+    }
+    return d;
+  }
+  /**
+   * Stream IDentifier.
+   */
+  unsigned id_;
+  /**
+   * Is the stream being closed?
+   */
+  bool closing_;
+  /**
+   * Have we received the stream signature yet?
+   */
+  sig_state_t sig_recvd_;
+  unsigned sig_cnt_;
+  /**
+   * Retrieve IN data for this stream?
+   */
+  bool retrieve_;
+  /**
+   * Check the received data against expectations?
+   */
+  bool check_;
+  /**
+   * Send OUT data for this stream?
+   */
+  bool send_;
+  /**
+   * Verbose reporting?
+   */
+  bool verbose_;
+  /**
+   * Total number of bytes received.
+   */
+  uint32_t bytes_recvd_;
+  /**
+   * Total number of bytes sent.
+   */
+  uint32_t bytes_sent_;
+  /**
+   * Device-side LFSR; byte stream expected from usbdev_stream_test.
+   */
+  uint8_t tst_lfsr_;
+  /**
+   * Host/DPI-side LFSR.
+   */
+  uint8_t dpi_lfsr_;
+  /**
+   * Number of bytes to be transferred.
+   */
+  uint32_t transfer_bytes_;
+  /**
+   * Circular buffer of streamed data.
+   */
+  struct {
+    /**
+     * Offset at which to write the next received data (IN from device).
+     */
+    uint32_t wr_idx;
+    /**
+     * Offset of next byte to be read from the buffer (OUT to device).
+     */
+    uint32_t rd_idx;
+    /**
+     * Offset beyond used portion of the buffer; for packet-based transmission
+     * we wrap before the end of the circular buffer if a maximum-length packet
+     * does not fit.
+     */
+    uint32_t end_idx;
+    /**
+     * Circular buffer of data being transferred from input to output port.
+     */
+    uint8_t data[kBufferSize];
+  } buf_;
+};
+
+#endif  // OPENTITAN_SW_HOST_TESTS_USBDEV_USBDEV_STREAM_USBDEV_STREAM_H_
diff --git a/sw/host/tests/usbdev/usbdev_stream/usbdev_utils.cc b/sw/host/tests/usbdev/usbdev_stream/usbdev_utils.cc
new file mode 100644
index 0000000..cb1e5f2
--- /dev/null
+++ b/sw/host/tests/usbdev/usbdev_stream/usbdev_utils.cc
@@ -0,0 +1,171 @@
+// Copyright lowRISC contributors (OpenTitan project).
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+#include "usbdev_utils.h"
+
+#include <cerrno>
+#include <cstdio>
+#include <cstring>
+#include <ctime>
+#include <fcntl.h>
+#include <iostream>
+#include <sys/time.h>
+#include <termios.h>
+#include <unistd.h>
+
+#include "stream_test.h"
+
+// Utility function to report an error returned from file/terminal-related
+// functions.
+static void report_error(const char *rsn) {
+  std::cerr << "ERROR: " << rsn << ": " << strerror(errno) << " (error "
+            << errno << ")" << std::endl;
+}
+
+// Open and configure a serial port connection to/from the USB device.
+int port_open(const char *dev_name, bool write) {
+  const char *port_type = write ? "output" : "input";
+  int fd = open(dev_name, write ? O_WRONLY : O_RDONLY);
+  if (fd < 0) {
+    std::cerr << "ERROR: Could not open " << port_type << " port '" << dev_name
+              << "'" << std::endl;
+    ReportSyntax();
+    return -1;
+  }
+
+  // We need to ensure that we can send full 8-bit binary data with no character
+  // translations and no character echo etc.
+  struct termios tty;
+  if (tcgetattr(fd, &tty) != 0) {
+    report_error("Failed getting terminal attributes");
+    close(fd);
+    return -1;
+  }
+
+  // 8 bits, no parity, no hardware handshaking.
+  tty.c_cflag &= ~(PARENB | CSTOPB | CSIZE | CRTSCTS);
+  tty.c_cflag |= CS8 | CREAD | CLOCAL;
+
+  // No character echo.
+  tty.c_lflag &= ~(ICANON | ECHO | ECHOE | ECHONL | ISIG);
+
+  // No software handshaking, no special characters.
+  tty.c_iflag &= ~(IXON | IXOFF | IXANY);
+  tty.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL);
+
+  // Disable line feed conversions and special characters on output traffic.
+  tty.c_oflag &= ~(OPOST | ONLCR);
+
+  // Non-blocking.
+  tty.c_cc[VTIME] = 0;
+  tty.c_cc[VMIN] = 0;
+
+  // Set in/out baud rate to be as high as possible; just in case, but it has
+  // no impact upon the measured transfer speed.
+  cfsetispeed(&tty, B4000000);
+  cfsetospeed(&tty, B4000000);
+
+  // Save tty settings, also checking for error.
+  if (tcsetattr(fd, TCSANOW, &tty) != 0) {
+    report_error("Failed setting terminal attributes");
+    close(fd);
+    return -1;
+  }
+
+  return fd;
+}
+
+// Receive a sequence of bytes from the USB device, non-blocking.
+ssize_t recv_bytes(int in, uint8_t *buf, size_t len) {
+  ssize_t nread = 0;
+
+  // Read as many bytes as we can from the input port.
+  ssize_t n = read(in, buf, len);
+  if (cfg.verbose) {
+    printf("Received %zd byte(s)\n", n);
+    for (int idx = 0; idx < n; idx++) {
+      printf("0x%02x\n", buf[idx]);
+    }
+    fflush(stdout);
+  }
+  if (n < 0) {
+    report_error("Failed to read from input port");
+    return -1;
+  }
+
+  nread += n;
+  buf += n;
+  len -= (size_t)n;
+
+  return nread;
+}
+
+// Send a sequence of bytes to the USB device, non-blocking.
+ssize_t send_bytes(int out, const uint8_t *data, size_t len) {
+  ssize_t nwritten = 0;
+
+  if (len > 0u) {
+    ssize_t n = write(out, data, len);
+    if (n < 0) {
+      report_error("Failed to write to output port");
+      return -1;
+    }
+
+    nwritten += n;
+    data += n;
+    len -= n;
+  }
+
+  return nwritten;
+}
+
+// Current monotonic wall clock time in microseconds.
+uint64_t time_us(void) {
+  struct timeval ts;
+  int ret = gettimeofday(&ts, NULL);
+  if (ret < 0)
+    return (uint64_t)0u;
+  return ((uint64_t)ts.tv_sec * 1000000u) + ts.tv_usec;
+}
+
+// Dump a sequence of bytes as hexadecimal and ASCII for diagnostic purposes.
+void buffer_dump(FILE *out, const uint8_t *data, size_t n) {
+  static const char hex_digits[] = "0123456789abcdef";
+  const unsigned ncols = 0x20u;
+  char buf[ncols * 4u + 2u];
+
+  while (n > 0u) {
+    const unsigned chunk = (n > ncols) ? ncols : (unsigned)n;
+    const uint8_t *row = data;
+    unsigned idx = 0u;
+    char *dp = buf;
+
+    // Columns of hexadecimal bytes.
+    while (idx < chunk) {
+      dp[0] = hex_digits[row[idx] >> 4];
+      dp[1] = hex_digits[row[idx++] & 0xfu];
+      dp[2] = ' ';
+      dp += 3;
+    }
+    while (idx++ < ncols) {
+      dp[2] = dp[1] = dp[0] = ' ';
+      dp += 3;
+    }
+
+    // Printable ASCII characters.
+    for (idx = 0u; idx < chunk; idx++) {
+      uint8_t ch = row[idx];
+      *dp++ = (ch < ' ' || ch >= 0x80u) ? '.' : ch;
+    }
+    *dp = '\0';
+    fprintf(out, "%s\n", buf);
+    data += chunk;
+    n -= chunk;
+  }
+
+  fflush(stdout);
+}
+
+// Link locations for inline functions.
+extern uint64_t elapsed_time(uint64_t start);
+extern uint16_t get_le16(const uint8_t *p);
diff --git a/sw/host/tests/usbdev/usbdev_stream/usbdev_utils.h b/sw/host/tests/usbdev/usbdev_stream/usbdev_utils.h
new file mode 100644
index 0000000..19232e6
--- /dev/null
+++ b/sw/host/tests/usbdev/usbdev_stream/usbdev_utils.h
@@ -0,0 +1,73 @@
+// Copyright lowRISC contributors (OpenTitan project).
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+#ifndef OPENTITAN_SW_HOST_TESTS_USBDEV_USBDEV_STREAM_USBDEV_UTILS_H_
+#define OPENTITAN_SW_HOST_TESTS_USBDEV_USBDEV_STREAM_USBDEV_UTILS_H_
+#include <cstdint>
+#include <cstdio>
+
+/**
+ * Open and configure a serial port connection to/from the USB device.
+ * The returned file descriptor may be passed directly to close().
+ *
+ * @param  dev_name  Device path of serial port.
+ * @param  write     Indicates whether to open for writing or reading.
+ * @return           File descriptor, or negative to indicate failure.
+ */
+int port_open(const char *dev_name, bool write);
+
+/**
+ * Receive a sequence of bytes from the USB device, non-blocking.
+ *
+ * @param  in        File descriptor.
+ * @param  buf       Buffer to receive data.
+ * @param  len       Buffer length (= maximum number of bytes to receive).
+ * @return           Number of bytes received, or -ve to indicate failure.
+ */
+ssize_t recv_bytes(int in, uint8_t *buf, size_t len);
+
+/**
+ * Send a sequence of bytes to the USB device, non-blocking.
+ *
+ * @param  out       File descriptor.
+ * @param  data      Data to be transmitted.
+ * @param  len       Number of bytes to be transmitted.
+ * @return           Number of bytes transmitted, or -ve to indicate failure.
+ */
+
+ssize_t send_bytes(int out, const uint8_t *data, size_t len);
+
+/**
+ * Dump a sequence of bytes as hexadecimal and ASCII for diagnostic purposes.
+ *
+ * @param  out       Output stream.
+ * @param  data      Data buffer.
+ * @param  n         Number of bytes.
+ */
+void buffer_dump(FILE *out, const uint8_t *data, size_t n);
+
+/**
+ * Return current monotonic wall time in microseconds.
+ *
+ * @return           Current time.
+ */
+uint64_t time_us(void);
+
+/**
+ * Return microseconds elapsed since the given monotonic wall time.
+ *
+ * @param  start     Monotonic wall time at the start of the time interval.
+ * @return           Elapsed time in microseconds.
+ */
+inline uint64_t elapsed_time(uint64_t start) { return time_us() - start; }
+
+/**
+ * Collect a 16-bit quantity transmitted in USB byte ordering, without
+ * assuming alignment or host endianness.
+ *
+ * @param  p         Pointer to 2 bytes.
+ * @return           The 16-bit quantity read.
+ */
+inline uint16_t get_le16(const uint8_t *p) { return p[0] | (p[1] << 8); }
+
+#endif  // OPENTITAN_SW_HOST_TESTS_USBDEV_USBDEV_STREAM_USBDEV_UTILS_H_