blob: 637116799274977743619b23a0312bd95b41dca5 [file] [log] [blame]
// 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 <ddk/binding.h>
#include <ddk/debug.h>
#include <ddk/device.h>
#include <ddk/protocol/usb-composite.h>
#include <ddk/usb/usb.h>
#include <lib/sync/completion.h>
#include <zircon/device/usb-device.h>
#include <zircon/device/usb-tester.h>
#include <zircon/hw/usb.h>
#include <stdlib.h>
#include <string.h>
#include "usb-tester.h"
#define REQ_MAX_LEN 0x10000 // 64 K
#define REQ_TIMEOUT_SECS 5
#define TEST_DUMMY_DATA 42
#define MIN(a, b) (((a) < (b)) ? (a) : (b))
#define DIV_ROUND_UP(n, d) (((n) + (d)-1) / (d))
#define ISOCH_START_FRAME_DELAY 5
#define ISOCH_ADDITIONAL_IN_REQS 8
typedef struct {
uint8_t intf_num;
uint8_t alt_setting;
uint8_t in_addr;
uint8_t out_addr;
uint16_t in_max_packet;
uint16_t out_max_packet;
} isoch_loopback_intf_t;
typedef struct {
zx_device_t* parent;
zx_device_t* zxdev;
usb_protocol_t usb;
uint8_t bulk_in_addr;
uint8_t bulk_out_addr;
isoch_loopback_intf_t isoch_loopback_intf;
} usb_tester_t;
typedef struct {
usb_request_t* req;
sync_completion_t completion; // This will be passed as the request cookie.
list_node_t node;
} test_req_t;
static void test_req_complete(usb_request_t* req, void* cookie) {
sync_completion_signal((sync_completion_t*)cookie);
}
static zx_status_t test_req_alloc(usb_tester_t* usb_tester, size_t len, uint8_t ep_address,
test_req_t** out_req) {
test_req_t* test_req = malloc(sizeof(test_req_t));
if (!test_req) {
return ZX_ERR_NO_MEMORY;
}
test_req->completion = SYNC_COMPLETION_INIT;
usb_request_t* req;
zx_status_t status = usb_req_alloc(&usb_tester->usb, &req, len, ep_address);
if (status != ZX_OK) {
free(test_req);
return status;
}
req->cookie = &test_req->completion;
req->complete_cb = test_req_complete;
test_req->req = req;
*out_req = test_req;
return ZX_OK;
}
static void test_req_release(usb_tester_t* usb_tester, test_req_t* test_req) {
if (!test_req) {
return;
}
if (test_req->req) {
usb_req_release(&usb_tester->usb, test_req->req);
}
free(test_req);
}
// Waits for the request to complete and verifies its completion status and transferred length.
// Returns ZX_OK if the request completed successfully, and the transferred length equals the
// requested length.
static zx_status_t test_req_wait_complete(usb_tester_t* usb_tester, test_req_t* test_req) {
usb_request_t* req = test_req->req;
zx_status_t status = sync_completion_wait(&test_req->completion, ZX_SEC(REQ_TIMEOUT_SECS));
if (status == ZX_OK) {
status = req->response.status;
if (status == ZX_OK) {
if (req->response.actual != req->header.length) {
status = ZX_ERR_IO;
}
} else if (status == ZX_ERR_IO_REFUSED) {
usb_reset_endpoint(&usb_tester->usb, req->header.ep_address);
}
return status;
} else if (status == ZX_ERR_TIMED_OUT) {
// Cancel the request before returning.
status = usb_cancel_all(&usb_tester->usb, req->header.ep_address);
if (status != ZX_OK) {
zxlogf(ERROR, "failed to cancel usb transfers, err: %d\n", status);
return ZX_ERR_TIMED_OUT;
}
status = sync_completion_wait(&test_req->completion, ZX_TIME_INFINITE);
if (status != ZX_OK) {
zxlogf(ERROR, "failed to wait for request completion after cancelling request\n");
}
return ZX_ERR_TIMED_OUT;
} else {
return status;
}
}
// Fills the given test request with data of the requested pattern.
static zx_status_t test_req_fill_data(usb_tester_t* usb_tester, test_req_t* test_req,
uint32_t data_pattern) {
uint8_t* buf;
zx_status_t status = usb_req_mmap(&usb_tester->usb, test_req->req, (void**)&buf);
if (status != ZX_OK) {
return status;
}
for (size_t i = 0; i < test_req->req->header.length; ++i) {
switch (data_pattern) {
case USB_TESTER_DATA_PATTERN_CONSTANT:
buf[i] = TEST_DUMMY_DATA;
break;
case USB_TESTER_DATA_PATTERN_RANDOM:
buf[i] = rand();
break;
default:
return ZX_ERR_INVALID_ARGS;
}
}
return ZX_OK;
}
// Removes and frees the requests contained in the test_reqs list.
static void test_req_list_release_reqs(usb_tester_t* usb_tester, list_node_t* test_reqs) {
test_req_t* test_req;
test_req_t* temp;
list_for_every_entry_safe(test_reqs, test_req, temp, test_req_t, node) {
list_delete(&test_req->node);
test_req_release(usb_tester, test_req);
}
}
// Allocates the test requests and adds them to the out_test_reqs list.
static zx_status_t test_req_list_alloc(usb_tester_t* usb_tester, int num_reqs, size_t len,
uint8_t ep_addr, list_node_t* out_test_reqs) {
for (int i = 0; i < num_reqs; ++i) {
test_req_t* test_req;
zx_status_t status = test_req_alloc(usb_tester, len, ep_addr, &test_req);
if (status != ZX_OK) {
test_req_list_release_reqs(usb_tester, out_test_reqs);
return status;
}
list_add_tail(out_test_reqs, &test_req->node);
}
return ZX_OK;
}
// Waits for the completion of each request contained in the test_reqs list in sequential order.
// The caller should check each request for its completion status.
static void test_req_list_wait_complete(usb_tester_t* usb_tester, list_node_t* test_reqs) {
test_req_t* test_req;
list_for_every_entry(test_reqs, test_req, test_req_t, node) {
test_req_wait_complete(usb_tester, test_req);
}
}
// Fills each request in the test_reqs list with data of the requested data_pattern.
static zx_status_t test_req_list_fill_data(usb_tester_t* usb_tester, list_node_t* test_reqs,
uint32_t data_pattern) {
test_req_t* test_req;
list_for_every_entry(test_reqs, test_req, test_req_t, node) {
zx_status_t status = test_req_fill_data(usb_tester, test_req, data_pattern);
if (status != ZX_OK) {
return status;
}
}
return ZX_OK;
}
// Queues all requests contained in the test_reqs list.
static void test_req_list_queue(usb_tester_t* usb_tester, list_node_t* test_reqs,
uint64_t start_frame) {
bool first = true;
test_req_t* test_req;
list_for_every_entry(test_reqs, test_req, test_req_t, node) {
// Set the frame ID for the first request.
// The following requests will be scheduled for ASAP after that.
if (first) {
test_req->req->header.frame = start_frame;
first = false;
}
usb_request_queue(&usb_tester->usb, test_req->req);
}
}
static zx_status_t usb_tester_set_mode_fwloader(usb_tester_t* usb_tester) {
size_t out_len;
zx_status_t status = usb_control(&usb_tester->usb,
USB_DIR_OUT | USB_TYPE_VENDOR | USB_RECIP_DEVICE,
USB_TESTER_SET_MODE_FWLOADER, 0, 0, NULL, 0,
ZX_SEC(REQ_TIMEOUT_SECS), &out_len);
if (status != ZX_OK) {
zxlogf(ERROR, "failed to set mode fwloader, err: %d\n", status);
return status;
}
return ZX_OK;
}
// Tests the loopback of data from the bulk OUT EP to the bulk IN EP.
static zx_status_t usb_tester_bulk_loopback(usb_tester_t* usb_tester,
const usb_tester_params_t* params) {
if (params->len > REQ_MAX_LEN) {
return ZX_ERR_INVALID_ARGS;
}
test_req_t* out_req = NULL;
test_req_t* in_req = NULL;
zx_status_t status = test_req_alloc(usb_tester, params->len, usb_tester->bulk_out_addr,
&out_req);
if (status != ZX_OK) {
goto done;
}
status = test_req_alloc(usb_tester, params->len, usb_tester->bulk_in_addr, &in_req);
if (status != ZX_OK) {
goto done;
}
status = test_req_fill_data(usb_tester, out_req, params->data_pattern);
if (status != ZX_OK) {
goto done;
}
usb_request_queue(&usb_tester->usb, out_req->req);
usb_request_queue(&usb_tester->usb, in_req->req);
zx_status_t out_status = test_req_wait_complete(usb_tester, out_req);
zx_status_t in_status = test_req_wait_complete(usb_tester, in_req);
status = out_status != ZX_OK ? out_status : in_status;
if (status != ZX_OK) {
goto done;
}
void* out_data;
status = usb_req_mmap(&usb_tester->usb, out_req->req, &out_data);
if (status != ZX_OK) {
goto done;
}
void* in_data;
status = usb_req_mmap(&usb_tester->usb, in_req->req, &in_data);
if (status != ZX_OK) {
goto done;
}
status = memcmp(in_data, out_data, params->len) == 0 ? ZX_OK : ZX_ERR_IO;
done:
test_req_release(usb_tester, out_req);
test_req_release(usb_tester, in_req);
return status;
}
// Counts how many requests were successfully loopbacked between the OUT and IN EPs.
// Returns ZX_OK if no fatal error occurred during verification.
// out_num_passed will be populated with the number of successfully loopbacked requests.
static zx_status_t usb_tester_verify_loopback(usb_tester_t* usb_tester, list_node_t* out_reqs,
list_node_t* in_reqs, size_t* out_num_passed) {
size_t num_passed = 0;
test_req_t* out_req_unmatched_start = list_next_type(out_reqs, out_reqs, test_req_t, node);
test_req_t* in_req;
list_for_every_entry(in_reqs, in_req, test_req_t, node) {
// You can't transfer an isochronous request of length zero.
if (in_req->req->response.status != ZX_OK || in_req->req->response.actual == 0) {
zxlogf(TRACE, "skipping isoch req, status %d, read len %lu\n",
in_req->req->response.status, in_req->req->response.actual);
continue;
}
void* in_data;
zx_status_t status = usb_req_mmap(&usb_tester->usb, in_req->req, &in_data);
if (status != ZX_OK) {
return status;
}
// We will start searching the OUT requests from after the last matched OUT request to
// preserve expected ordering.
test_req_t* out_req = out_req_unmatched_start;
bool matched = false;
while (out_req && !matched) {
if (out_req->req->response.status == ZX_OK &&
out_req->req->response.actual == in_req->req->response.actual) {
void* out_data;
status = usb_req_mmap(&usb_tester->usb, out_req->req, &out_data);
if (status != ZX_OK) {
return status;
}
matched = memcmp(in_data, out_data, out_req->req->response.actual) == 0;
}
out_req = list_next_type(out_reqs, &out_req->node, test_req_t, node);
}
if (matched) {
out_req_unmatched_start = out_req;
num_passed++;
} else {
// Maybe IN data was corrupted.
zxlogf(TRACE, "could not find matching isoch req\n");
}
}
*out_num_passed = num_passed;
return ZX_OK;
}
static zx_status_t usb_tester_isoch_loopback(usb_tester_t* usb_tester,
const usb_tester_params_t* params,
usb_tester_result_t* result) {
if (params->len > REQ_MAX_LEN) {
return ZX_ERR_INVALID_ARGS;
}
isoch_loopback_intf_t* intf = &usb_tester->isoch_loopback_intf;
zx_status_t status = usb_set_interface(&usb_tester->usb, intf->intf_num, intf->alt_setting);
if (status != ZX_OK) {
zxlogf(ERROR, "usb_set_interface got err: %d\n", status);
goto done;
}
// TODO(jocelyndang): optionally allow the user to specify a packet size.
uint16_t packet_size = MIN(intf->in_max_packet, intf->out_max_packet);
size_t num_reqs = DIV_ROUND_UP(params->len, packet_size);
zxlogf(TRACE, "allocating %lu reqs of packet size %u, total bytes %lu\n",
num_reqs, packet_size, params->len);
list_node_t in_reqs = LIST_INITIAL_VALUE(in_reqs);
list_node_t out_reqs = LIST_INITIAL_VALUE(out_reqs);
// We will likely get a few empty IN requests, as there is a delay between the start of an
// OUT transfer and it being received. Allocate a few more IN requests to account for this.
status = test_req_list_alloc(usb_tester, num_reqs + ISOCH_ADDITIONAL_IN_REQS, packet_size,
intf->in_addr, &in_reqs);
if (status != ZX_OK) {
goto done;
}
status = test_req_list_alloc(usb_tester, num_reqs, packet_size, intf->out_addr, &out_reqs);
if (status != ZX_OK) {
goto done;
}
status = test_req_list_fill_data(usb_tester, &out_reqs, params->data_pattern);
if (status != ZX_OK) {
goto done;
}
// Find the current frame so we can schedule OUT and IN requests to start simultaneously.
uint64_t frame = usb_get_current_frame(&usb_tester->usb);
// Adds some delay so we don't miss the scheduled start frame.
uint64_t start_frame = frame + ISOCH_START_FRAME_DELAY;
zxlogf(TRACE, "scheduling isoch loopback to start on frame %lu\n", start_frame);
test_req_list_queue(usb_tester, &in_reqs, start_frame);
test_req_list_queue(usb_tester, &out_reqs, start_frame);
test_req_list_wait_complete(usb_tester, &out_reqs);
test_req_list_wait_complete(usb_tester, &in_reqs);
size_t num_passed = 0;
status = usb_tester_verify_loopback(usb_tester, &out_reqs, &in_reqs, &num_passed);
if (status != ZX_OK) {
goto done;
}
result->num_passed = num_passed;
result->num_packets = num_reqs;
zxlogf(TRACE, "%lu / %lu passed\n", num_passed, num_reqs);
done:;
zx_status_t res = usb_set_interface(&usb_tester->usb, intf->intf_num, 0);
if (res != ZX_OK) {
zxlogf(ERROR, "could not switch back to isoch interface default alternate setting\n");
}
test_req_list_release_reqs(usb_tester, &out_reqs);
test_req_list_release_reqs(usb_tester, &in_reqs);
return status;
}
static zx_status_t usb_tester_ioctl(void* ctx, uint32_t op, const void* in_buf, size_t in_len,
void* out_buf, size_t out_len, size_t* out_actual) {
usb_tester_t* usb_tester = ctx;
switch (op) {
case IOCTL_USB_TESTER_SET_MODE_FWLOADER:
return usb_tester_set_mode_fwloader(usb_tester);
case IOCTL_USB_TESTER_BULK_LOOPBACK: {
if (in_buf == NULL || in_len != sizeof(usb_tester_params_t)) {
return ZX_ERR_INVALID_ARGS;
}
return usb_tester_bulk_loopback(usb_tester, in_buf);
}
case IOCTL_USB_TESTER_ISOCH_LOOPBACK: {
if (in_buf == NULL || in_len != sizeof(usb_tester_params_t) ||
out_buf == NULL || out_len != sizeof(usb_tester_result_t)) {
return ZX_ERR_INVALID_ARGS;
}
usb_tester_result_t* result = out_buf;
zx_status_t status = usb_tester_isoch_loopback(usb_tester, in_buf, result);
if (status != ZX_OK) {
return status;
}
*out_actual = sizeof(*result);
return ZX_OK;
}
case IOCTL_USB_GET_DEVICE_DESC: {
usb_device_descriptor_t* descriptor = out_buf;
if (out_len < sizeof(*descriptor)) {
return ZX_ERR_BUFFER_TOO_SMALL;
}
usb_get_device_descriptor(&usb_tester->usb, descriptor);
*out_actual = sizeof(*descriptor);
return ZX_OK;
}
default:
return ZX_ERR_NOT_SUPPORTED;
}
}
static void usb_tester_free(usb_tester_t* ctx) {
free(ctx);
}
static void usb_tester_unbind(void* ctx) {
zxlogf(INFO, "usb_tester_unbind\n");
usb_tester_t* usb_tester = ctx;
device_remove(usb_tester->zxdev);
}
static void usb_tester_release(void* ctx) {
usb_tester_t* usb_tester = ctx;
usb_tester_free(usb_tester);
}
static zx_protocol_device_t usb_tester_device_protocol = {
.version = DEVICE_OPS_VERSION,
.ioctl = usb_tester_ioctl,
.unbind = usb_tester_unbind,
.release = usb_tester_release,
};
static bool want_interface(usb_interface_descriptor_t* intf, void* arg) {
return intf->bInterfaceClass == USB_CLASS_VENDOR;
}
static zx_status_t usb_tester_bind(void* ctx, zx_device_t* device) {
zxlogf(TRACE, "usb_tester_bind\n");
usb_tester_t* usb_tester = calloc(1, sizeof(usb_tester_t));
if (!usb_tester) {
return ZX_ERR_NO_MEMORY;
}
usb_tester->parent = device;
zx_status_t status = device_get_protocol(device, ZX_PROTOCOL_USB, &usb_tester->usb);
if (status != ZX_OK) {
goto error_return;
}
usb_composite_protocol_t usb_composite;
status = device_get_protocol(device, ZX_PROTOCOL_USB, &usb_composite);
if (status != ZX_OK) {
goto error_return;
}
status = usb_claim_additional_interfaces(&usb_composite, want_interface, NULL);
if (status != ZX_OK) {
goto error_return;
}
// Find the endpoints.
usb_desc_iter_t iter;
status = usb_desc_iter_init(&usb_tester->usb, &iter);
if (status != ZX_OK) {
goto error_return;
}
usb_interface_descriptor_t* intf = usb_desc_iter_next_interface(&iter, false);
while (intf) {
isoch_loopback_intf_t isoch_intf = {
.intf_num = intf->bInterfaceNumber,
.alt_setting = intf->bAlternateSetting
};
usb_endpoint_descriptor_t* endp = usb_desc_iter_next_endpoint(&iter);
while (endp) {
switch (usb_ep_type(endp)) {
case USB_ENDPOINT_BULK:
if (usb_ep_direction(endp) == USB_ENDPOINT_IN) {
usb_tester->bulk_in_addr = endp->bEndpointAddress;
zxlogf(TRACE, "usb_tester found bulk in ep: %x\n", usb_tester->bulk_in_addr);
} else {
usb_tester->bulk_out_addr = endp->bEndpointAddress;
zxlogf(TRACE, "usb_tester found bulk out ep: %x\n", usb_tester->bulk_out_addr);
}
break;
case USB_ENDPOINT_ISOCHRONOUS:
if (usb_ep_direction(endp) == USB_ENDPOINT_IN) {
isoch_intf.in_addr = endp->bEndpointAddress;
isoch_intf.in_max_packet = usb_ep_max_packet(endp);
} else {
isoch_intf.out_addr = endp->bEndpointAddress;
isoch_intf.out_max_packet = usb_ep_max_packet(endp);
}
break;
}
endp = usb_desc_iter_next_endpoint(&iter);
}
if (isoch_intf.in_addr && isoch_intf.out_addr) {
// Found isoch loopback endpoints.
memcpy(&usb_tester->isoch_loopback_intf, &isoch_intf, sizeof(isoch_intf));
zxlogf(TRACE, "usb tester found isoch loopback eps: %x (%u) %x (%u), intf %u %u\n",
isoch_intf.in_addr, isoch_intf.in_max_packet,
isoch_intf.out_addr, isoch_intf.out_max_packet,
isoch_intf.intf_num, isoch_intf.alt_setting);
}
intf = usb_desc_iter_next_interface(&iter, false);
}
usb_desc_iter_release(&iter);
// Check we found the pair of bulk endpoints.
if (!usb_tester->bulk_in_addr || !usb_tester->bulk_out_addr) {
zxlogf(ERROR, "usb_bind could not find bulk endpoints\n");
status = ZX_ERR_NOT_SUPPORTED;
goto error_return;
}
device_add_args_t args = {
.version = DEVICE_ADD_ARGS_VERSION,
.name = "usb-tester",
.ctx = usb_tester,
.ops = &usb_tester_device_protocol,
.flags = DEVICE_ADD_NON_BINDABLE,
.proto_id = ZX_PROTOCOL_USB_TESTER,
};
status = device_add(device, &args, &usb_tester->zxdev);
if (status != ZX_OK) {
goto error_return;
}
return ZX_OK;
error_return:
usb_tester_free(usb_tester);
return status;
}
static zx_driver_ops_t usb_tester_driver_ops = {
.version = DRIVER_OPS_VERSION,
.bind = usb_tester_bind,
};
ZIRCON_DRIVER_BEGIN(usb_tester, usb_tester_driver_ops, "zircon", "0.1", 3)
BI_ABORT_IF(NE, BIND_PROTOCOL, ZX_PROTOCOL_USB),
BI_ABORT_IF(NE, BIND_USB_VID, GOOGLE_VID),
BI_MATCH_IF(EQ, BIND_USB_PID, USB_TESTER_PID),
ZIRCON_DRIVER_END(usb_tester)