[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_