| // 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" |
| #include "usbdev_iso.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_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 |
| // 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; |
| } |