[usbdev] Isochronous support

Add support for Isochronous Transfers to the host-side
component of the usbdev streaming tests.

Change-Id: I812a628e2b89d4576a83e27fb2964b6fbbaae405
Signed-off-by: Adrian Lees <a.lees@lowrisc.org>
diff --git a/sw/host/tests/usbdev/usbdev_stream/stream_test.cc b/sw/host/tests/usbdev/usbdev_stream/stream_test.cc
index 5f6d1c5..0b042a6 100644
--- a/sw/host/tests/usbdev/usbdev_stream/stream_test.cc
+++ b/sw/host/tests/usbdev/usbdev_stream/stream_test.cc
@@ -64,6 +64,7 @@
 #include "usb_device.h"
 #if STREAMTEST_LIBUSB
 #include "usbdev_int.h"
+#include "usbdev_iso.h"
 #endif
 #include "usbdev_serial.h"
 #include "usbdev_utils.h"
@@ -286,6 +287,18 @@
       } break;
 
 #if STREAMTEST_LIBUSB
+      case USBDevStream::StreamType_Isochronous: {
+        USBDevIso *iso;
+        iso = new USBDevIso(dev, idx, transfer_bytes, cfg.retrieve, cfg.check,
+                            cfg.send, cfg.verbose);
+        if (iso) {
+          opened = iso->Open(idx);
+          if (opened) {
+            streams[idx] = iso;
+          }
+        }
+      } break;
+
       case USBDevStream::StreamType_Interrupt:
         bulk = false;
         // no break; Bulk Transfers are handled identically to Interrupt
diff --git a/sw/host/tests/usbdev/usbdev_stream/usbdev_iso.cc b/sw/host/tests/usbdev/usbdev_stream/usbdev_iso.cc
new file mode 100644
index 0000000..2762e94
--- /dev/null
+++ b/sw/host/tests/usbdev/usbdev_stream/usbdev_iso.cc
@@ -0,0 +1,320 @@
+// 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_iso.h"
+
+#include <cassert>
+#include <cstdio>
+
+#include "usbdev_utils.h"
+
+// Stub callback function supplied to libusb.
+void LIBUSB_CALL USBDevIso::CbStubIN(struct libusb_transfer *xfr) {
+  USBDevIso *self = reinterpret_cast<USBDevIso *>(xfr->user_data);
+  self->CallbackIN(xfr);
+}
+
+void LIBUSB_CALL USBDevIso::CbStubOUT(struct libusb_transfer *xfr) {
+  USBDevIso *self = reinterpret_cast<USBDevIso *>(xfr->user_data);
+  self->CallbackOUT(xfr);
+}
+
+bool USBDevIso::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;
+
+  // Expected sequence number of first packet.
+  tst_seq_ = 0U;
+
+  // Maximum size of a packet in bytes.
+  maxPacketSize_ = USBDevice::kDevIsoMaxPacketSize;
+
+  return true;
+}
+
+void USBDevIso::Stop() {
+  SetClosing(true);
+
+  int rc = dev_->ReleaseInterface(interface_);
+  if (rc < 0) {
+    std::cerr << "" << std::endl;
+  }
+}
+
+void USBDevIso::Pause() {
+  SetClosing(true);
+
+  while (inActive_ || outActive_) {
+    dev_->Service();
+  }
+
+  int rc = dev_->ReleaseInterface(interface_);
+  if (rc < 0) {
+    std::cerr << "" << std::endl;
+  }
+}
+
+bool USBDevIso::Resume() {
+  SetClosing(false);
+
+  int rc = dev_->ClaimInterface(interface_);
+  if (rc < 0) {
+    return dev_->ErrorUSB("ERROR: Claiming interface", rc);
+  }
+  return true;
+}
+
+// Return an indication of whether this stream has completed its transfer.
+bool USBDevIso::Completed() const {
+  // Note: an Isochronous stream presently cannot know whether it has
+  // transferred sufficient data for the device-side software to have completed.
+  //
+  // TODO: perhaps we just time out after a period of inactivity. The device-
+  // side software ultimately decides success or failure of the test, at which
+  // point the test harness will be terminated.
+  return false;
+}
+
+// Return a summary report of the stream settings of status.
+std::string USBDevIso::Report(bool status, bool verbose) const { return ""; }
+
+void USBDevIso::DumpIsoTransfer(struct libusb_transfer *xfr) const {
+  for (int idx = 0U; idx < xfr->num_iso_packets; idx++) {
+    struct libusb_iso_packet_descriptor *pack = &xfrIn_->iso_packet_desc[idx];
+    std::cout << "Requested " << pack->length << " actual "
+              << pack->actual_length << std::endl;
+    // Buffer dumping works only because we have just a single Iso packet per
+    // transfer.
+    buffer_dump(stdout, (uint8_t *)xfr->buffer, pack->actual_length);
+  }
+}
+
+// Retrieving of IN traffic from device.
+bool USBDevIso::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) {
+    if (!xfrIn_) {
+      xfrIn_ = dev_->AllocTransfer(kNumIsoPackets);
+      if (!xfrIn_) {
+        return false;
+      }
+    }
+
+    dev_->FillIsoTransfer(xfrIn_, epIn_, space, maxPacketSize_, kNumIsoPackets,
+                          CbStubIN, this, kIsoTimeout);
+    dev_->SetIsoPacketLengths(xfrIn_, maxPacketSize_);
+
+    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 USBDevIso::ServiceOUT() {
+  // Do we have one or more packets ready for sending?
+  if (pktLen_.empty()) {
+    // Nothing to propagate at this time.
+    outActive_ = false;
+  } else {
+    uint32_t len = pktLen_.front();
+    pktLen_.pop();
+    // We should have propagated only valid packets to the OUT side ready for
+    // transmission.
+    assert(len >= sizeof(usbdev_stream_sig_t));
+
+    uint8_t *data;
+    size_t num_bytes = DataAvailable(&data);
+    assert(num_bytes >= len);
+
+    // Supply details of the single OUT packet.
+    if (!xfrOut_) {
+      xfrOut_ = dev_->AllocTransfer(kNumIsoPackets);
+      if (!xfrOut_) {
+        // Stream is not operational.
+        return false;
+      }
+    }
+    dev_->FillIsoTransfer(xfrOut_, epOut_, data, len, kNumIsoPackets, CbStubOUT,
+                          this, kIsoTimeout);
+    dev_->SetIsoPacketLengths(xfrOut_, len);
+
+    int rc = dev_->SubmitTransfer(xfrOut_);
+    if (rc < 0) {
+      return dev_->ErrorUSB("ERROR: Submitting OUT transfer", rc);
+    }
+    outActive_ = true;
+  }
+  // Stream remains operational, even if it presently has no work on the OUT
+  // side.
+  return true;
+}
+
+bool USBDevIso::Service() {
+  if (failed_) {
+    return false;
+  }
+  // (Re)start Isochronous IN traffic if not already in progress.
+  if (!inActive_ && !ServiceIN()) {
+    return false;
+  }
+  // (Re)start Isochronous 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 USBDevIso::CallbackIN(struct libusb_transfer *xfr) {
+  if (xfr->status != LIBUSB_TRANSFER_COMPLETED) {
+    std::cerr << PrefixID() << " Invalid/unexpected IN transfer status "
+              << xfr->status << std::endl;
+    failed_ = true;
+    return;
+  }
+
+  if (verbose_) {
+    std::cout << PrefixID() << "CallbackIN xfr " << xfr << " num_iso_packets "
+              << xfr->num_iso_packets << std::endl;
+    DumpIsoTransfer(xfr);
+  }
+
+  for (int idx = 0U; idx < xfr->num_iso_packets; idx++) {
+    struct libusb_iso_packet_descriptor *pack = &xfr->iso_packet_desc[idx];
+    if (pack->status != LIBUSB_TRANSFER_COMPLETED) {
+      std::cerr << "ERROR: pack " << idx << " status " << pack->status
+                << std::endl;
+      inActive_ = false;
+      return;
+    }
+
+    if (pack->actual_length) {
+      // Reset signature detection, because a new signature is included at the
+      // start of each Isochronous packet.
+      SigReset();
+
+      // Check that this packet is recognized as commencing with a valid
+      // signature, process the data within the packet, and then retain its
+      // details.
+      usbdev_stream_sig_t sig;
+      uint32_t dropped = SigDetect(&sig, xfr->buffer, pack->actual_length);
+      if (SigReceived() && dropped < pack->actual_length &&
+          sizeof(usbdev_stream_sig_t) <= pack->actual_length - dropped) {
+        // Pick up information from this packet signature.
+        SigProcess(sig);
+
+        // Valid packet received; payload includes the signature which we
+        // retain and propagate to the caller to permit synchronization.
+        uint32_t payload = pack->actual_length - dropped;
+        pktLen_.push(payload);
+
+        // Since packets may have been dropped we must use the supplied values
+        // of the device-side LFSR
+        uint16_t seq = (uint16_t)((sig.seq_hi << 8) | sig.seq_lo);
+        if (seq == tst_seq_) {
+          if (sig.init_lfsr != tst_lfsr_) {
+            std::cerr << "ERROR: Unexpected device-side LFSR value (expected 0x"
+                      << std::hex << tst_lfsr_ << " received 0x"
+                      << sig.init_lfsr << ")" << std::dec << std::endl;
+            inActive_ = false;
+            return;
+          }
+        } else if (seq < tst_seq_) {
+          std::cerr << "ERROR: Iso stream packets out of order (expected seq 0x"
+                    << std::hex << tst_seq_ << " received 0x" << seq << ")"
+                    << std::dec << std::endl;
+          inActive_ = false;
+          return;
+        } else {
+          // One or more packets has disappeared; use the supplied LFSR to
+          // resynchronize.
+          tst_lfsr_ = sig.init_lfsr;
+        }
+
+        // Remember the sequence number that we expect to see next.
+        tst_seq_ = seq + 1U;
+
+        // Supply the host-side LFSR value so that the device may check the
+        // content of received OUT packets.
+        const size_t sig_size = sizeof(usbdev_stream_sig_t);
+        uint8_t *dp = &xfr->buffer[dropped];
+        dp[offsetof(usbdev_stream_sig_t, init_lfsr)] = dpi_lfsr_;
+        ProcessData(dp + sig_size, payload - sig_size);
+
+        CommitData(payload);
+      } else {
+        std::cerr << PrefixID() << " received invalid Iso packet of "
+                  << pack->actual_length << " bytes" << std::endl;
+      }
+    }
+  }
+
+  if (CanSchedule()) {
+    // Attempt to set up another IN transfer.
+    failed_ = !ServiceIN();
+  } else {
+    inActive_ = false;
+  }
+}
+
+// Callback function supplied to libusb for OUT transfers.
+void USBDevIso::CallbackOUT(struct libusb_transfer *xfr) {
+  if (xfr->status != LIBUSB_TRANSFER_COMPLETED) {
+    std::cerr << PrefixID() << " Invalid/unexpected OUT transfer status "
+              << xfr->status << std::endl;
+    failed_ = true;
+    return;
+  }
+
+  if (verbose_) {
+    const void *buf = reinterpret_cast<void *>(xfr->buffer);
+    std::cout << PrefixID() << "CallbackOUT xfr " << xfr << " buffer " << buf
+              << " num_iso_packets " << xfr->num_iso_packets << std::endl;
+    DumpIsoTransfer(xfr);
+  }
+
+  for (int idx = 0U; idx < xfr->num_iso_packets; idx++) {
+    struct libusb_iso_packet_descriptor *pack = &xfr->iso_packet_desc[idx];
+    if (pack->status != LIBUSB_TRANSFER_COMPLETED) {
+      std::cout << "ERROR: pack " << idx << " status " << pack->status
+                << std::endl;
+      outActive_ = false;
+      exit(0);
+      return;
+    }
+
+    if (pack->actual_length) {
+      ConsumeData(pack->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_iso.h b/sw/host/tests/usbdev/usbdev_stream/usbdev_iso.h
new file mode 100644
index 0000000..53fe830
--- /dev/null
+++ b/sw/host/tests/usbdev/usbdev_stream/usbdev_iso.h
@@ -0,0 +1,152 @@
+// 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_ISO_H_
+#define OPENTITAN_SW_HOST_TESTS_USBDEV_USBDEV_STREAM_USBDEV_ISO_H_
+#include <queue>
+
+#include "usb_device.h"
+#include "usbdev_stream.h"
+
+class USBDevIso : public USBDevStream {
+ public:
+  USBDevIso(USBDevice *dev, unsigned id, uint32_t transfer_bytes, bool retrieve,
+            bool check, bool send, bool verbose)
+      : USBDevStream(id, transfer_bytes, retrieve, check, send, verbose),
+        dev_(dev),
+        failed_(false),
+        inActive_(false),
+        outActive_(false),
+        xfrIn_(nullptr),
+        xfrOut_(nullptr) {}
+  /**
+   * Open an Isochronous 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;
+  /**
+   * Indicates whether this stream has completed its transfer.
+   *
+   * @return         true iff this stream has nothing more to do.
+   */
+  virtual bool Completed() const;
+  /**
+   * Service this Isochronous stream.
+   *
+   * @return true iff the stream is still operational.
+   */
+  virtual bool Service();
+
+ private:
+  /**
+   * Diagnostic utility function to display the content of libusb Iso transfer.
+   *
+   * @param  xfr     The Isochronous transfer to be displayed.
+   */
+  void DumpIsoTransfer(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_;
+
+  // 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_;
+
+  // Expected device-side sequence number of next IN packet.
+  uint16_t tst_seq_;
+
+  // Lengths of packets (in bytes) of the Isochronous Data packets held in the
+  // circular buffer.
+  std::queue<uint32_t> pktLen_;
+
+  // 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 kIsoTimeout = 0U;
+
+  // Since the USB device is Full Speed it supports only one Isochronous
+  // Data packet per bus frame.
+  static constexpr unsigned kNumIsoPackets = 1U;
+};
+
+#endif  // OPENTITAN_SW_HOST_TESTS_USBDEV_USBDEV_STREAM_USBDEV_ISO_H_