usb-function-test wip

Change-Id: I7aeac553779c89d1788ccf2732f9f133b68a725d
diff --git a/system/dev/usb/usb-function-test/driver.cpp b/system/dev/usb/usb-function-test/driver.cpp
new file mode 100644
index 0000000..03e1c25
--- /dev/null
+++ b/system/dev/usb/usb-function-test/driver.cpp
@@ -0,0 +1,405 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <assert.h>
+#include <inttypes.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <threads.h>
+
+#include <ddk/binding.h>
+#include <ddk/debug.h>
+#include <ddk/device.h>
+#include <ddk/driver.h>
+#include <ddk/protocol/usb-function.h>
+#include <usb/usb-request.h>
+#include <zircon/listnode.h>
+#include <zircon/process.h>
+#include <zircon/syscalls.h>
+#include <zircon/device/usb-function-test.h>
+#include <zircon/device/usb-peripheral.h>
+
+namespace usb_function_test {
+
+#define BULK_TX_COUNT   16
+#define BULK_RX_COUNT   16
+#define INTR_COUNT      8
+
+#define BULK_MAX_PACKET     512     // FIXME(voydanoff) USB 3.0 support
+#define BULK_REQ_SIZE       4096    // FIXME(voydanoff) increase this when DCI drivers support
+                                    //                  non-contiguous DMA buffers
+#define INTR_REQ_SIZE       1024
+
+typedef struct {
+    zx_device_t* zxdev;
+    usb_function_protocol_t function;
+
+    list_node_t bulk_out_reqs;      // list of usb_request_t
+    list_node_t bulk_in_reqs;       // list of usb_request_t
+    list_node_t intr_reqs;          // list of usb_request_t
+
+    uint8_t test_data[INTR_REQ_SIZE];
+    size_t test_data_length;
+
+    bool configured;
+    usb_speed_t speed;
+
+    mtx_t mutex;
+
+    uint8_t bulk_out_addr;
+    uint8_t bulk_in_addr;
+    uint8_t intr_addr;
+} usb_test_t;
+
+ static struct {
+    usb_interface_descriptor_t intf;
+    usb_endpoint_descriptor_t intr_ep;
+    usb_endpoint_descriptor_t bulk_out_ep;
+    usb_endpoint_descriptor_t bulk_in_ep;
+} descriptors = {
+    .intf = {
+        .bLength = sizeof(usb_interface_descriptor_t),
+        .bDescriptorType = USB_DT_INTERFACE,
+        .bInterfaceNumber = 0, // set later
+        .bAlternateSetting = 0,
+        .bNumEndpoints = 3,
+        .bInterfaceClass = USB_CLASS_VENDOR,
+        .bInterfaceSubClass = 0,
+        .bInterfaceProtocol = 0,
+        .iInterface = 0,
+    },
+    .intr_ep = {
+        .bLength = sizeof(usb_endpoint_descriptor_t),
+        .bDescriptorType = USB_DT_ENDPOINT,
+        .bEndpointAddress = 0, // set later
+        .bmAttributes = USB_ENDPOINT_INTERRUPT,
+        .wMaxPacketSize = htole16(INTR_REQ_SIZE),
+        .bInterval = 8,
+    },
+    .bulk_out_ep = {
+        .bLength = sizeof(usb_endpoint_descriptor_t),
+        .bDescriptorType = USB_DT_ENDPOINT,
+        .bEndpointAddress = 0, // set later
+        .bmAttributes = USB_ENDPOINT_BULK,
+        .wMaxPacketSize = htole16(BULK_MAX_PACKET),
+        .bInterval = 0,
+    },
+    .bulk_in_ep = {
+        .bLength = sizeof(usb_endpoint_descriptor_t),
+        .bDescriptorType = USB_DT_ENDPOINT,
+        .bEndpointAddress = 0, // set later
+        .bmAttributes = USB_ENDPOINT_BULK,
+        .wMaxPacketSize = htole16(BULK_MAX_PACKET),
+        .bInterval = 0,
+    },
+};
+
+static void test_intr_complete(usb_request_t* req, void* cookie) {
+    auto* test = static_cast<usb_test_t*>(cookie);
+
+    zxlogf(LTRACE, "%s %d %ld\n", __func__, req->response.status, req->response.actual);
+
+    mtx_lock(&test->mutex);
+    list_add_tail(&test->intr_reqs, &req->node);
+    mtx_unlock(&test->mutex);
+}
+
+static void test_bulk_out_complete(usb_request_t* req, void* cookie) {
+    auto* test = static_cast<usb_test_t*>(cookie);
+
+    zxlogf(LTRACE, "%s %d %ld\n", __func__, req->response.status, req->response.actual);
+
+    if (req->response.status == ZX_ERR_IO_NOT_PRESENT) {
+        mtx_lock(&test->mutex);
+        list_add_head(&test->bulk_out_reqs, &req->node);
+        mtx_unlock(&test->mutex);
+        return;
+    }
+    if (req->response.status == ZX_OK) {
+        mtx_lock(&test->mutex);
+        usb_request_t* in_req = list_remove_head_type(&test->bulk_in_reqs, usb_request_t, node);
+        mtx_unlock(&test->mutex);
+        if (in_req) {
+            // Send data back to host.
+            void* buffer;
+            usb_request_mmap(req, &buffer);
+            usb_request_copy_to(in_req, buffer, req->response.actual, 0);
+            req->header.length = req->response.actual;
+            usb_function_queue(&test->function, in_req);
+        } else {
+            zxlogf(ERROR, "%s: no bulk in request available\n", __func__);
+        }
+    } else {
+        zxlogf(ERROR, "%s: usb_read_complete called with status %d\n",
+                __func__, req->response.status);
+    }
+
+    // Requeue read.
+    usb_function_queue(&test->function, req);
+}
+
+static void test_bulk_in_complete(usb_request_t* req, void* cookie) {
+    auto* test = static_cast<usb_test_t*>(cookie);
+
+    zxlogf(LTRACE, "%s %d %ld\n", __func__, req->response.status, req->response.actual);
+
+    mtx_lock(&test->mutex);
+    list_add_tail(&test->bulk_in_reqs, &req->node);
+    mtx_unlock(&test->mutex);
+}
+
+static const usb_descriptor_header_t* test_get_descriptors(void* ctx, size_t* out_length) {
+    *out_length = sizeof(descriptors);
+    return (const usb_descriptor_header_t *)&descriptors;
+}
+
+static zx_status_t test_control(void* ctx, const usb_setup_t* setup, void* buffer,
+                                size_t buffer_length, size_t* out_actual) {
+    auto* test = static_cast<usb_test_t*>(ctx);
+    size_t length = le16toh(setup->wLength);
+    if (length > buffer_length) {
+        length = buffer_length;
+    }
+    *out_actual = 0;
+
+    zxlogf(TRACE, "%s\n", __func__);
+    if (setup->bmRequestType == (USB_DIR_OUT | USB_TYPE_VENDOR | USB_RECIP_INTERFACE) &&
+        setup->bRequest == USB_FUNCTION_TEST_SET_DATA) {
+        if (length > sizeof(test->test_data)) {
+            length = sizeof(test->test_data);
+        }
+        memcpy(test->test_data, buffer, length);
+        test->test_data_length = length;
+        *out_actual = length;
+        return ZX_OK;
+    } else if (setup->bmRequestType == (USB_DIR_IN | USB_TYPE_VENDOR | USB_RECIP_INTERFACE) &&
+        setup->bRequest == USB_FUNCTION_TEST_GET_DATA) {
+        if (length > test->test_data_length) {
+            length = test->test_data_length;
+        }
+        memcpy(buffer, test->test_data, length);
+        *out_actual = length;
+        return ZX_OK;
+    } else if (setup->bmRequestType == (USB_DIR_OUT | USB_TYPE_VENDOR | USB_RECIP_INTERFACE) &&
+        setup->bRequest == USB_FUNCTION_TEST_SEND_INTERUPT) {
+        mtx_lock(&test->mutex);
+        usb_request_t* req = list_remove_head_type(&test->intr_reqs, usb_request_t, node);
+        mtx_unlock(&test->mutex);
+        if (!req) {
+            zxlogf(ERROR, "%s: no interrupt request available\n", __func__);
+            // TODO(voydanoff) maybe stall in this case?
+            return ZX_OK;
+        }
+
+        usb_request_copy_to(req, test->test_data, test->test_data_length, 0);
+        req->header.length = test->test_data_length;
+        usb_function_queue(&test->function, req);
+        return ZX_OK;
+    } else {
+        return ZX_ERR_NOT_SUPPORTED;
+    }
+}
+
+static zx_status_t test_set_configured(void* ctx, bool configured, usb_speed_t speed) {
+    zxlogf(TRACE, "%s: %d %d\n", __func__, configured, speed);
+    auto* test = static_cast<usb_test_t*>(ctx);
+    zx_status_t status;
+
+    if (configured) {
+        if ((status = usb_function_config_ep(&test->function, &descriptors.intr_ep, NULL))
+                != ZX_OK ||
+            (status = usb_function_config_ep(&test->function, &descriptors.bulk_out_ep, NULL))
+                != ZX_OK ||
+            (status = usb_function_config_ep(&test->function, &descriptors.bulk_in_ep, NULL))
+                != ZX_OK) {
+            zxlogf(ERROR, "%s: usb_function_config_ep failed\n", __func__);
+            return status;
+        }
+        test->speed = speed;
+    } else {
+        usb_function_disable_ep(&test->function, test->bulk_out_addr);
+        usb_function_disable_ep(&test->function, test->bulk_in_addr);
+        usb_function_disable_ep(&test->function, test->intr_addr);
+        test->speed = USB_SPEED_UNDEFINED;
+    }
+    test->configured = configured;
+
+    if (configured) {
+        // Queue our OUT requests.
+        usb_request_t* req;
+        while ((req = list_remove_head_type(&test->bulk_out_reqs, usb_request_t, node)) != NULL) {
+            usb_function_queue(&test->function, req);
+        }
+    }
+
+    return ZX_OK;
+}
+
+static zx_status_t test_set_interface(void* ctx, unsigned interface, unsigned alt_setting) {
+    return ZX_ERR_NOT_SUPPORTED;
+}
+
+usb_function_interface_ops_t device_ops = {
+    .get_descriptors = test_get_descriptors,
+    .control = test_control,
+    .set_configured = test_set_configured,
+    .set_interface = test_set_interface,
+};
+
+static void usb_test_unbind(void* ctx) {
+    zxlogf(TRACE, "%s\n", __func__);
+    auto* test = static_cast<usb_test_t*>(ctx);
+
+    device_remove(test->zxdev);
+}
+
+static void usb_test_release(void* ctx) {
+    zxlogf(TRACE, "%s\n", __func__);
+    auto* test = static_cast<usb_test_t*>(ctx);
+    usb_request_t* req;
+
+    while ((req = list_remove_head_type(&test->bulk_out_reqs, usb_request_t, node)) != NULL) {
+        usb_request_release(req);
+    }
+    while ((req = list_remove_head_type(&test->bulk_in_reqs, usb_request_t, node)) != NULL) {
+        usb_request_release(req);
+    }
+    while ((req = list_remove_head_type(&test->intr_reqs, usb_request_t, node)) != NULL) {
+        usb_request_release(req);
+    }
+    mtx_destroy(&test->mutex);
+    free(test);
+}
+
+static zx_protocol_device_t usb_test_proto = [](){
+    zx_protocol_device_t dev;
+    dev.version = DEVICE_OPS_VERSION;
+    dev.unbind = usb_test_unbind;
+    dev.release = usb_test_release;
+    return dev;
+}();
+
+zx_status_t usb_test_bind(void* ctx, zx_device_t* parent) {
+    zxlogf(INFO, "%s\n", __func__);
+
+    auto* test = static_cast<usb_test_t*>(calloc(1, sizeof(usb_test_t)));
+    if (!test) {
+        return ZX_ERR_NO_MEMORY;
+    }
+
+    zx_status_t status = device_get_protocol(parent, ZX_PROTOCOL_USB_FUNCTION, &test->function);
+    if (status != ZX_OK) {
+        free(test);
+        return status;
+    }
+
+    list_initialize(&test->bulk_out_reqs);
+    list_initialize(&test->bulk_in_reqs);
+    list_initialize(&test->intr_reqs);
+    mtx_init(&test->mutex, mtx_plain);
+
+    usb_function_interface_t intf = {};
+    intf.ops = &device_ops;
+    intf.ctx = test;
+
+    status = usb_function_alloc_interface(&test->function, &descriptors.intf.bInterfaceNumber);
+    if (status != ZX_OK) {
+        zxlogf(ERROR, "%s: usb_function_alloc_interface failed\n", __func__);
+        goto fail;
+    }
+
+    status = usb_function_alloc_ep(&test->function, USB_DIR_OUT, &test->bulk_out_addr);
+    if (status != ZX_OK) {
+        zxlogf(ERROR, "%s: usb_function_alloc_ep failed\n", __func__);
+        goto fail;
+    }
+    status = usb_function_alloc_ep(&test->function, USB_DIR_IN, &test->bulk_in_addr);
+    if (status != ZX_OK) {
+        zxlogf(ERROR, "%s: usb_function_alloc_ep failed\n", __func__);
+        goto fail;
+    }
+    status = usb_function_alloc_ep(&test->function, USB_DIR_IN, &test->intr_addr);
+    if (status != ZX_OK) {
+        zxlogf(ERROR, "%s: usb_function_alloc_ep failed\n", __func__);
+        goto fail;
+    }
+
+    descriptors.bulk_out_ep.bEndpointAddress = test->bulk_out_addr;
+    descriptors.bulk_in_ep.bEndpointAddress = test->bulk_in_addr;
+    descriptors.intr_ep.bEndpointAddress = test->intr_addr;
+
+    // allocate bulk out usb requests
+    usb_request_t* req;
+    for (int i = 0; i < BULK_TX_COUNT; i++) {
+        status = usb_request_alloc(&req, BULK_REQ_SIZE, test->bulk_out_addr, sizeof(usb_request_t));
+        if (status != ZX_OK) {
+            goto fail;
+        }
+        req->complete_cb = test_bulk_out_complete;
+        req->cookie = test;
+        list_add_head(&test->bulk_out_reqs, &req->node);
+    }
+    // allocate bulk in usb requests
+    for (int i = 0; i < BULK_RX_COUNT; i++) {
+        status = usb_request_alloc(&req, BULK_REQ_SIZE, test->bulk_in_addr, sizeof(usb_request_t));
+        if (status != ZX_OK) {
+            goto fail;
+        }
+
+        req->complete_cb = test_bulk_in_complete;
+        req->cookie = test;
+        list_add_head(&test->bulk_in_reqs, &req->node);
+    }
+
+    // allocate interrupt requests
+    for (int i = 0; i < INTR_COUNT; i++) {
+        status = usb_request_alloc(&req, INTR_REQ_SIZE, test->intr_addr, sizeof(usb_request_t));
+        if (status != ZX_OK) {
+            goto fail;
+        }
+
+        req->complete_cb = test_intr_complete;
+        req->cookie = test;
+        list_add_head(&test->intr_reqs, &req->node);
+    }
+{
+    device_add_args_t args = {};
+    args.version = DEVICE_ADD_ARGS_VERSION;
+    args.name = "usb-function-test";
+    args.ctx = test;
+    args.ops = &usb_test_proto;
+    args.flags = DEVICE_ADD_NON_BINDABLE;
+
+    status = device_add(parent, &args, &test->zxdev);
+    if (status != ZX_OK) {
+        zxlogf(ERROR, "%s: add_device failed %d\n", __func__, status);
+        goto fail;
+    }
+}
+    usb_function_register(&test->function, &intf);
+
+    return ZX_OK;
+
+fail:
+    usb_test_release(test);
+    return status;
+}
+
+static zx_driver_ops_t driver_ops = [](){
+    zx_driver_ops_t ops;
+    ops.version = DRIVER_OPS_VERSION;
+    ops.bind = usb_test_bind;
+    return ops;
+}();
+
+} // namespace usb_function_test
+
+// clang-format off
+ZIRCON_DRIVER_BEGIN(usb_function_test, usb_function_test::driver_ops, "zircon", "0.1", 3)
+    BI_ABORT_IF(NE, BIND_PROTOCOL, ZX_PROTOCOL_USB_FUNCTION),
+    BI_ABORT_IF(NE, BIND_USB_VID, GOOGLE_USB_VID),
+    BI_MATCH_IF(EQ, BIND_USB_PID, GOOGLE_USB_FUNCTION_TEST_PID),
+ZIRCON_DRIVER_END(usb_function_test)
diff --git a/system/dev/usb/usb-function-test/rules.mk b/system/dev/usb/usb-function-test/rules.mk
new file mode 100644
index 0000000..5499c4f
--- /dev/null
+++ b/system/dev/usb/usb-function-test/rules.mk
@@ -0,0 +1,50 @@
+# Copyright 2018 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+LOCAL_DIR := $(GET_LOCAL_DIR)
+
+MODULE := $(LOCAL_DIR).driver
+
+MODULE_TYPE := driver
+
+MODULE_SRCS := \
+    $(LOCAL_DIR)/driver.cpp \
+
+MODULE_STATIC_LIBS := \
+    system/ulib/ddk \
+    system/ulib/sync \
+    system/dev/lib/usb \
+
+MODULE_LIBS := \
+    system/ulib/driver \
+    system/ulib/zircon \
+    system/ulib/c \
+
+MODULE_HEADER_DEPS := \
+    system/ulib/inet6 \
+
+include make/module.mk
+
+ifeq ($(HOST_PLATFORM),linux)
+
+MODULE := $(LOCAL_DIR)
+
+MODULE_TYPE := hosttest
+
+MODULE_SRCS += \
+    $(LOCAL_DIR)/test.cpp \
+
+MODULE_HOST_LIBS := \
+    system/ulib/pretty.hostlib \
+    system/ulib/unittest.hostlib \
+    third_party/ulib/usbhost \
+
+MODULE_COMPILEFLAGS := \
+    -Isystem/ulib/unittest/include \
+
+MODULE_PACKAGE := bin
+
+include make/module.mk
+
+endif # linux
diff --git a/system/dev/usb/usb-function-test/test.cpp b/system/dev/usb/usb-function-test/test.cpp
new file mode 100644
index 0000000..c3039ab
--- /dev/null
+++ b/system/dev/usb/usb-function-test/test.cpp
@@ -0,0 +1,286 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <pthread.h>
+
+#include <usbhost/usbhost.h>
+#include <unittest/unittest.h>
+#include <zircon/device/usb-function-test.h>
+#include <zircon/device/usb-peripheral.h>
+
+namespace {
+
+static struct usb_device* dev = nullptr;
+static struct usb_endpoint_descriptor* bulk_out_ep = nullptr;
+static struct usb_endpoint_descriptor* bulk_in_ep = nullptr;
+static struct usb_endpoint_descriptor* intr_ep = nullptr;
+
+static constexpr size_t BUFFER_SIZE = 4096;
+
+// Data to send to the device
+static uint8_t send_buf[BUFFER_SIZE] = {};
+// Buffer for receiving data from the device
+static int receive_buf[BUFFER_SIZE] = {};
+
+static constexpr int TIMEOUT = 1000; // 1 seecond
+
+
+// Fill send_buf with random values.
+static void randomize() {
+    // Generate some random data.
+    for (size_t i = 0; i < sizeof(send_buf); i++) {
+        send_buf[i] = static_cast<uint8_t>(random());
+    }
+}
+
+// This thread waits for completion of an interrupt request.
+static void* intr_complete_thread(void* arg) {
+    auto* req = static_cast<struct usb_request**>(arg);
+    *req = usb_request_wait(dev, TIMEOUT);
+    return nullptr;
+}
+
+// This thread waits for completion of a bulk OUT request and bulk IN request.
+static void* bulk_complete_thread(void* arg) {
+    auto* reqs = static_cast<struct usb_request**>(arg);
+    *reqs++ = usb_request_wait(dev, TIMEOUT);
+    *reqs = usb_request_wait(dev, TIMEOUT);
+    return nullptr;
+}
+
+// Tests control and interrupt transfers with specified transfer size.
+static bool control_interrupt_test(size_t transfer_size) {
+    BEGIN_TEST;
+
+    randomize();
+
+    // Send data to device via OUT control request.
+    int ret = usb_device_control_transfer(dev,
+                            USB_DIR_OUT | USB_TYPE_VENDOR | USB_TYPE_VENDOR | USB_RECIP_INTERFACE,
+                            USB_FUNCTION_TEST_SET_DATA, 0, 0, send_buf, transfer_size, TIMEOUT);
+    EXPECT_EQ(ret, transfer_size);
+
+    // Receive data back from device via IN control request.
+    ret = usb_device_control_transfer(dev,
+                            USB_DIR_IN | USB_TYPE_VENDOR | USB_TYPE_VENDOR | USB_RECIP_INTERFACE,
+                            USB_FUNCTION_TEST_GET_DATA, 0, 0, receive_buf, transfer_size, TIMEOUT);
+    EXPECT_EQ(ret, transfer_size);
+
+    // Sent and received data should match.
+    EXPECT_EQ(memcmp(send_buf, receive_buf, transfer_size), 0);
+
+    // Create a thread to wait for interrupt request.
+    pthread_t thread;
+    struct usb_request* complete_req = nullptr;
+    ret = pthread_create(&thread, nullptr, intr_complete_thread, &complete_req); 
+    EXPECT_EQ(ret, 0);
+
+    // Queue read for interrupt request
+    auto* req = usb_request_new(dev, intr_ep);
+    EXPECT_NE(req, nullptr);
+    req->buffer = receive_buf;
+    req->buffer_length = static_cast<int>(transfer_size);
+    ret = usb_request_queue(req);
+    EXPECT_EQ(ret, 0);
+
+    // Ask the device to send us an interrupt request containing the data we sent earlier.
+    ret = usb_device_control_transfer(dev,
+                            USB_DIR_OUT | USB_TYPE_VENDOR | USB_TYPE_VENDOR | USB_RECIP_INTERFACE,
+                            USB_FUNCTION_TEST_SEND_INTERUPT, 0, 0, nullptr, 0, TIMEOUT);
+    EXPECT_EQ(ret, 0);
+
+    pthread_join(thread, nullptr);
+
+    EXPECT_EQ(complete_req, req);
+
+    // Sent data should match payload of interrupt request.
+    EXPECT_EQ(memcmp(send_buf, receive_buf, transfer_size), 0);
+
+    usb_request_free(req);
+
+    END_TEST;
+}
+
+// Test control and interrupt requests with 8 byte transfer size.
+static bool control_interrupt_test_8() {
+    return control_interrupt_test(8);
+}
+
+// Test control and interrupt requests with 64 byte transfer size.
+static bool control_interrupt_test_64() {
+    return control_interrupt_test(64);
+}
+
+// Test control and interrupt requests with 100 byte transfer size.
+static bool control_interrupt_test_100() {
+    return control_interrupt_test(100);
+}
+
+// Test control and interrupt requests with 256 byte transfer size.
+static bool control_interrupt_test_256() {
+    return control_interrupt_test(256);
+}
+
+// Test control and interrupt requests with 1000 byte transfer size.
+static bool control_interrupt_test_1000() {
+    return control_interrupt_test(1000);
+}
+
+// Tests bulk OUT and IN transfers.
+// Send BUFFER_SIZE bytes to device, read back and compare.
+static bool bulk_test() {
+    BEGIN_TEST;
+
+    auto* send_req = usb_request_new(dev, bulk_out_ep);
+    EXPECT_NE(send_req, nullptr);
+    send_req->buffer = send_buf;
+    send_req->buffer_length = static_cast<int>(BUFFER_SIZE);
+
+    auto* receive_req = usb_request_new(dev, bulk_in_ep);
+    EXPECT_NE(send_req, nullptr);
+    receive_req->buffer = receive_buf;
+    receive_req->buffer_length = static_cast<int>(BUFFER_SIZE);
+
+    for (int i = 0; i < 10; i++) {
+        randomize();
+
+        // Create a thread to wait for request completions.
+        pthread_t thread;
+        struct usb_request* complete_reqs[2] = {};
+        int ret = pthread_create(&thread, nullptr, bulk_complete_thread, complete_reqs); 
+        EXPECT_EQ(ret, 0);
+
+        // Queue requests in both directions
+        ret = usb_request_queue(receive_req);
+        EXPECT_EQ(ret, 0);
+        ret = usb_request_queue(send_req);
+        EXPECT_EQ(ret, 0);
+
+        pthread_join(thread, nullptr);
+
+        EXPECT_NE(complete_reqs[0], nullptr);
+        EXPECT_NE(complete_reqs[1], nullptr);
+
+        // Sent and received data should match.
+        EXPECT_EQ(memcmp(send_buf, receive_buf, BUFFER_SIZE), 0);
+    }
+
+    usb_request_free(send_req);
+    usb_request_free(receive_req);
+
+    END_TEST;
+}
+
+// usb_host_load() will call this for all connected USB devices.
+static int usb_device_added(const char *dev_name, void *client_data) {
+    usb_descriptor_iter iter;
+    struct usb_descriptor_header* header;
+    struct usb_interface_descriptor* intf = nullptr;
+    int ret;
+    
+    dev = usb_device_open(dev_name);
+    if (!dev) {
+        fprintf(stderr, "usb_device_open failed for %s\n", dev_name);
+        return 0;
+    }
+
+    uint16_t vid = usb_device_get_vendor_id(dev);
+    uint16_t pid = usb_device_get_product_id(dev);
+
+    if (vid != GOOGLE_USB_VID || pid != GOOGLE_USB_FUNCTION_TEST_PID) {
+        // Device doesn't match, so keep looking.
+        return 0;
+    }
+
+    usb_descriptor_iter_init(dev, &iter);
+
+    while ((header = usb_descriptor_iter_next(&iter)) != nullptr) {
+        if (header->bDescriptorType == USB_DT_INTERFACE) {
+            intf = reinterpret_cast<struct usb_interface_descriptor*>(header);
+        } else if (header->bDescriptorType == USB_DT_ENDPOINT) {
+            auto* ep = reinterpret_cast<struct usb_endpoint_descriptor*>(header);
+            if (usb_endpoint_type(ep) == USB_ENDPOINT_XFER_BULK) {
+                if (usb_endpoint_dir_in(ep)) {
+                    bulk_in_ep = ep;
+                } else {
+                    bulk_out_ep = ep;
+                }
+            } else if (usb_endpoint_type(ep) == USB_ENDPOINT_XFER_INT) {
+                intr_ep = ep;
+            }
+        }
+    }
+
+    if (!intf || !bulk_out_ep || !bulk_in_ep || !intr_ep) {
+        fprintf(stderr, "could not find all our endpoints\n");
+        goto fail;
+    }
+
+    ret = usb_device_claim_interface(dev, intf->bInterfaceNumber);
+    if (ret < 0) {
+        fprintf(stderr, "usb_device_claim_interface failed\n");
+        goto fail;
+    }
+
+    // Test done, exit from usb_host_load().
+    return 1;
+
+fail:
+    usb_device_close(dev);
+    dev = nullptr;
+    // Test done, exit from usb_host_load().
+    return 1;
+}
+
+static int usb_device_removed(const char *dev_name, void *client_data) {
+    return 0;
+}
+
+static int usb_discovery_done(void *client_data) {
+    return 0;
+}
+
+} // anonymous namespace
+
+BEGIN_TEST_CASE(usb_function_tests)
+RUN_TEST(control_interrupt_test_8);
+RUN_TEST(control_interrupt_test_64);
+RUN_TEST(control_interrupt_test_100);
+RUN_TEST(control_interrupt_test_256);
+RUN_TEST(control_interrupt_test_1000);
+RUN_TEST(bulk_test);
+END_TEST_CASE(usb_function_tests)
+
+int main(int argc, char** argv) {
+    struct usb_host_context* context = usb_host_init();
+    if (!context) {
+        fprintf(stderr, "usb_host_context failed\n");
+        return -1;
+    }
+
+    auto ret = usb_host_load(context, usb_device_added, usb_device_removed, usb_discovery_done,
+                             nullptr);
+    if (ret < 0) {
+        fprintf(stderr, "usb_host_load failed!\n");
+        usb_host_cleanup(context);
+        goto fail;
+    }
+    if (!dev) {
+        fprintf(stderr, "No USB Function Test devices found!\n");
+        goto fail;
+    }
+
+    ret = unittest_run_all_tests(argc, argv) ? 0 : -1;
+
+fail:
+    if (dev) {
+        usb_device_close(dev);
+    }
+    usb_host_cleanup(context);
+    return ret;
+}
diff --git a/system/dev/usb/usb-peripheral/usb-peripheral.c b/system/dev/usb/usb-peripheral/usb-peripheral.c
index d9dc29e..8a27bf6 100644
--- a/system/dev/usb/usb-peripheral/usb-peripheral.c
+++ b/system/dev/usb/usb-peripheral/usb-peripheral.c
@@ -975,6 +975,10 @@
         function_desc.interface_class = USB_CLASS_MSC;
         function_desc.interface_subclass = USB_SUBCLASS_MSC_SCSI;
         function_desc.interface_protocol = USB_PROTOCOL_MSC_BULK_ONLY;
+    } else if (strcasecmp(USB_DEVICE_FUNCTIONS, "test") == 0) {
+        function_desc.interface_class = USB_CLASS_VENDOR;
+        function_desc.interface_subclass = 0;
+        function_desc.interface_protocol = 0;
     } else {
         zxlogf(ERROR, "usb_dev_set_default_config: unknown function %s\n", USB_DEVICE_FUNCTIONS);
         return ZX_ERR_INVALID_ARGS;
diff --git a/system/public/zircon/device/usb-function-test.h b/system/public/zircon/device/usb-function-test.h
new file mode 100644
index 0000000..a17f7a2
--- /dev/null
+++ b/system/public/zircon/device/usb-function-test.h
@@ -0,0 +1,29 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#pragma once
+
+// This file contains definitions for the USB function test driver.
+// The driver implements three endpoints: bulk out, bulk in and interrupt.
+// The driver supports:
+//
+// 1. Writing data to the device with USB control request.
+//
+// 2. Reading data back from the device with USB control request.
+//
+// 3. Requesting the device to send an interrupt packet containing the data
+//    sent via USB control request.
+//
+// 4. Looping back data via the two bulk endpoints.
+
+// USB control request to write data to the device.
+#define USB_FUNCTION_TEST_SET_DATA 1
+
+// USB control request to read badk data set by USB_FUNCTION_TEST_SET_DATA.
+#define USB_FUNCTION_TEST_GET_DATA 2
+
+// USB control request to request the device to send an interrupt request
+// containing the data set via USB_FUNCTION_TEST_SET_DATA.
+#define USB_FUNCTION_TEST_SEND_INTERUPT 3
+
diff --git a/system/public/zircon/device/usb-peripheral.h b/system/public/zircon/device/usb-peripheral.h
index 7c3a098..47e40ee 100644
--- a/system/public/zircon/device/usb-peripheral.h
+++ b/system/public/zircon/device/usb-peripheral.h
@@ -4,7 +4,6 @@
 
 #pragma once
 
-
 #include <stdint.h>
 
 typedef uint32_t usb_mode_t;
@@ -12,3 +11,15 @@
 #define USB_MODE_HOST ((usb_mode_t)1)
 #define USB_MODE_PERIPHERAL ((usb_mode_t)2)
 #define USB_MODE_OTG ((usb_mode_t)3)
+
+// Google's USB Vendor ID.
+#define GOOGLE_USB_VID 0x18D1
+
+// USB Product ID for Zircon CDC Ethernet Function.
+#define GOOGLE_USB_CDC_PID 0xA020
+
+// USB Product ID for Zircon USB Mass Storage Function.
+#define GOOGLE_USB_UMS_PID 0xA021
+
+// USB Product ID for Zircon USB Function Test.
+#define GOOGLE_USB_FUNCTION_TEST_PID 0xA022
diff --git a/system/uapp/usbctl/usbctl.c b/system/uapp/usbctl/usbctl.c
index 5eb8c65..894dd0c 100644
--- a/system/uapp/usbctl/usbctl.c
+++ b/system/uapp/usbctl/usbctl.c
@@ -21,13 +21,10 @@
 
 #define DEV_USB_PERIPHERAL_DIR  "/dev/class/usb-peripheral"
 
-#define GOOGLE_VID      0x18D1
-#define GOOGLE_CDC_PID  0xA020
-#define GOOGLE_UMS_PID  0xA021
-
 #define MANUFACTURER_STRING "Zircon"
 #define CDC_PRODUCT_STRING  "CDC Ethernet"
 #define UMS_PRODUCT_STRING  "USB Mass Storage"
+#define TEST_PRODUCT_STRING  "USB Function Test"
 #define SERIAL_STRING       "12345678"
 
 typedef zircon_usb_peripheral_FunctionDescriptor usb_function_descriptor_t;
@@ -44,6 +41,13 @@
     .interface_protocol = USB_PROTOCOL_MSC_BULK_ONLY,
 };
 
+const usb_function_descriptor_t test_function_desc = {
+    .interface_class = USB_CLASS_VENDOR,
+    .interface_subclass = 0,
+    .interface_protocol = 0,
+};
+
+
 typedef struct {
     const usb_function_descriptor_t* desc;
     const char* product_string;
@@ -54,15 +58,22 @@
 static const usb_function_t cdc_function = {
     .desc = &cdc_function_desc,
     .product_string = CDC_PRODUCT_STRING,
-    .vid = GOOGLE_VID,
-    .pid = GOOGLE_CDC_PID,
+    .vid = GOOGLE_USB_VID,
+    .pid = GOOGLE_USB_CDC_PID,
 };
 
 static const usb_function_t ums_function = {
     .desc = &ums_function_desc,
     .product_string = UMS_PRODUCT_STRING,
-    .vid = GOOGLE_VID,
-    .pid = GOOGLE_UMS_PID,
+    .vid = GOOGLE_USB_VID,
+    .pid = GOOGLE_USB_UMS_PID,
+};
+
+static const usb_function_t test_function = {
+    .desc = &test_function_desc,
+    .product_string = TEST_PRODUCT_STRING,
+    .vid = GOOGLE_USB_VID,
+    .pid = GOOGLE_USB_FUNCTION_TEST_PID,
 };
 
 static zircon_usb_peripheral_DeviceDescriptor device_desc = {
@@ -173,6 +184,8 @@
         status = device_init(svc, &cdc_function);
     } else if (!strcmp(command, "init-ums")) {
         status = device_init(svc, &ums_function);
+    } else if (!strcmp(command, "init-test")) {
+        status = device_init(svc, &test_function);
      } else {
         goto usage;
     }