blob: 4831f9b17b7be67687e1b3f826dc21349b182809 [file] [log] [blame]
// Copyright lowRISC contributors.
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
// SPDX-License-Identifier: Apache-2.0
#include "sw/device/lib/testing/usb_testutils.h"
#include "sw/device/lib/dif/dif_usbdev.h"
#include "sw/device/lib/testing/test_framework/check.h"
#include "hw/top_earlgrey/sw/autogen/top_earlgrey.h"
#define USBDEV_BASE_ADDR TOP_EARLGREY_USBDEV_BASE_ADDR
static dif_usbdev_t usbdev;
static dif_usbdev_buffer_pool_t buffer_pool;
void usb_testutils_poll(usb_testutils_ctx_t *ctx) {
uint32_t istate;
// Collect a set of interrupts
CHECK_DIF_OK(dif_usbdev_irq_get_state(ctx->dev, &istate));
if (!istate) {
// Nothing new to do right now, but keep buffers available for reception
CHECK_DIF_OK(dif_usbdev_fill_available_fifo(ctx->dev, ctx->buffer_pool));
return;
}
// Record bus frame
if ((istate & (1u << kDifUsbdevIrqFrame))) {
// The first bus frame is 1
CHECK_DIF_OK(dif_usbdev_status_get_frame(ctx->dev, &ctx->frame));
ctx->got_frame = true;
}
// Process IN completions first so we get the fact that send completed
// before processing a response to that transmission
// This is also important for device IN performance
if (istate & (1u << kDifUsbdevIrqPktSent)) {
uint16_t sentep;
CHECK_DIF_OK(dif_usbdev_get_tx_sent(ctx->dev, &sentep));
TRC_C('a' + sentep);
for (unsigned ep = 0; ep < USBDEV_NUM_ENDPOINTS; ep++) {
if (sentep & (1 << ep)) {
// Free up the buffer and optionally callback
CHECK_DIF_OK(
dif_usbdev_clear_tx_status(ctx->dev, ctx->buffer_pool, ep));
if (ctx->in[ep].tx_done_callback) {
ctx->in[ep].tx_done_callback(ctx->in[ep].ep_ctx);
}
}
}
// Clear the interrupt sooner so that we can respond more promptly to
// subsequent transmitted packets
CHECK_DIF_OK(dif_usbdev_irq_acknowledge(ctx->dev, kDifUsbdevIrqPktSent));
istate &= ~(1u << kDifUsbdevIrqPktSent);
}
// Keep buffers available for packet reception
CHECK_DIF_OK(dif_usbdev_fill_available_fifo(ctx->dev, ctx->buffer_pool));
if (istate & (1u << kDifUsbdevIrqPktReceived)) {
while (true) {
bool is_empty;
CHECK_DIF_OK(dif_usbdev_status_get_rx_fifo_empty(ctx->dev, &is_empty));
if (is_empty) {
break;
}
dif_usbdev_rx_packet_info_t packet_info;
dif_usbdev_buffer_t buffer;
CHECK_DIF_OK(dif_usbdev_recv(ctx->dev, &packet_info, &buffer));
unsigned ep = packet_info.endpoint;
if (ctx->out[ep].rx_callback) {
ctx->out[ep].rx_callback(ctx->out[ep].ep_ctx, packet_info, buffer);
} else {
// Note: this could happen following endpoint removal
TRC_S("USB: unexpected RX ");
TRC_I(endpoint, 8);
CHECK_DIF_OK(
dif_usbdev_buffer_return(ctx->dev, ctx->buffer_pool, &buffer));
}
}
}
if (istate & (1u << kDifUsbdevIrqLinkReset)) {
TRC_S("USB: Bus reset");
// Link reset
for (int ep = 0; ep < USBDEV_NUM_ENDPOINTS; ep++) {
// Notify the IN endpoint first because transmission is more significantly
// impacted, and then the OUT endpoint before advancing to the next
// endpoint number in case the order is important to the client(s)
if (ctx->in[ep].reset) {
ctx->in[ep].reset(ctx->in[ep].ep_ctx);
}
if (ctx->out[ep].reset) {
ctx->out[ep].reset(ctx->out[ep].ep_ctx);
}
}
}
// Clear the interrupts that we've received and handled
CHECK_DIF_OK(dif_usbdev_irq_acknowledge_state(ctx->dev, istate));
// Note: LinkInErr will be raised in response to a packet being NAKed by the
// host which is not expected behavior on a physical USB but this is something
// that the DPI model does to exercise packet resending when running
// usbdev_stream_test
//
// We can expect AVFIFO empty and RXFIFO full interrupts when using a real
// host and also 'LinkOut' errors because these can be triggered by a lack of
// space in the RXFIFO
if (istate &
~((1u << kDifUsbdevIrqLinkReset) | (1u << kDifUsbdevIrqPktReceived) |
(1u << kDifUsbdevIrqPktSent) | (1u << kDifUsbdevIrqFrame) |
(1u << kDifUsbdevIrqAvEmpty) | (1u << kDifUsbdevIrqRxFull) |
(1u << kDifUsbdevIrqLinkOutErr) | (1u << kDifUsbdevIrqLinkInErr))) {
// Report anything that really should not be happening during testing,
// at least for now
//
// TODO - introduce deliberate generation of each of these errors, and
// modify usb_testutils_ to return the information without faulting
// it?
if (istate &
((1u << kDifUsbdevIrqRxFull) | (1u << kDifUsbdevIrqAvOverflow) |
(1u << kDifUsbdevIrqLinkInErr) | (1u << kDifUsbdevIrqRxCrcErr) |
(1u << kDifUsbdevIrqRxPidErr) | (1u << kDifUsbdevIrqRxBitstuffErr) |
(1u << kDifUsbdevIrqLinkOutErr))) {
LOG_INFO("USB: Unexpected interrupts: 0x%08x", istate);
} else {
// Other events are optionally reported
TRC_C('I');
TRC_I(istate, 12);
TRC_C(' ');
}
}
// TODO - clean this up
// Frame ticks every 1ms, use to flush data every 16ms
// (faster in DPI but this seems to work ok)
//
// Ensure that we do not flush until we have received a frame
if (ctx->got_frame && (ctx->frame & 0xf) == 1) {
if (ctx->flushed == 0) {
for (unsigned ep = 0; ep < USBDEV_NUM_ENDPOINTS; ep++) {
if (ctx->in[ep].flush) {
ctx->in[ep].flush(ctx->in[ep].ep_ctx);
}
}
ctx->flushed = 1;
}
} else {
ctx->flushed = 0;
}
// TODO Errors? What Errors?
}
void usb_testutils_in_endpoint_setup(usb_testutils_ctx_t *ctx, uint8_t ep,
void *ep_ctx, void (*tx_done)(void *),
void (*flush)(void *),
void (*reset)(void *)) {
ctx->in[ep].ep_ctx = ep_ctx;
ctx->in[ep].tx_done_callback = tx_done;
ctx->in[ep].flush = flush;
ctx->in[ep].reset = reset;
dif_usbdev_endpoint_id_t endpoint = {
.number = ep,
.direction = USBDEV_ENDPOINT_DIR_IN,
};
CHECK_DIF_OK(
dif_usbdev_endpoint_stall_enable(ctx->dev, endpoint, kDifToggleDisabled));
// Enable IN traffic from device to host
CHECK_DIF_OK(
dif_usbdev_endpoint_enable(ctx->dev, endpoint, kDifToggleEnabled));
}
void usb_testutils_out_endpoint_setup(
usb_testutils_ctx_t *ctx, uint8_t ep,
usb_testutils_out_transfer_mode_t out_mode, void *ep_ctx,
void (*rx)(void *, dif_usbdev_rx_packet_info_t, dif_usbdev_buffer_t),
void (*reset)(void *)) {
ctx->out[ep].ep_ctx = ep_ctx;
ctx->out[ep].rx_callback = rx;
ctx->out[ep].reset = reset;
dif_usbdev_endpoint_id_t endpoint = {
.number = ep,
.direction = USBDEV_ENDPOINT_DIR_OUT,
};
CHECK_DIF_OK(
dif_usbdev_endpoint_stall_enable(ctx->dev, endpoint, kDifToggleDisabled));
// Enable/disable the endpoint and reception of OUT packets?
dif_toggle_t enabled = kDifToggleEnabled;
if (out_mode == kUsbdevOutDisabled) {
enabled = kDifToggleDisabled;
}
// Enable/disable generation of NAK responses once OUT packet has been
// received?
dif_toggle_t nak = kDifToggleDisabled;
if (out_mode == kUsbdevOutMessage) {
nak = kDifToggleEnabled;
}
CHECK_DIF_OK(dif_usbdev_endpoint_enable(ctx->dev, endpoint, enabled));
CHECK_DIF_OK(dif_usbdev_endpoint_out_enable(ctx->dev, ep, enabled));
CHECK_DIF_OK(dif_usbdev_endpoint_set_nak_out_enable(ctx->dev, ep, nak));
}
void usb_testutils_endpoint_setup(
usb_testutils_ctx_t *ctx, uint8_t ep,
usb_testutils_out_transfer_mode_t out_mode, void *ep_ctx,
void (*tx_done)(void *),
void (*rx)(void *, dif_usbdev_rx_packet_info_t, dif_usbdev_buffer_t),
void (*flush)(void *), void (*reset)(void *)) {
usb_testutils_in_endpoint_setup(ctx, ep, ep_ctx, tx_done, flush, reset);
// Note: register the link reset handler only on the IN endpoint so that it
// does not get invoked twice
usb_testutils_out_endpoint_setup(ctx, ep, out_mode, ep_ctx, rx, NULL);
}
void usb_testutils_in_endpoint_remove(usb_testutils_ctx_t *ctx, uint8_t ep) {
// Disable IN traffic
dif_usbdev_endpoint_id_t endpoint = {
.number = ep,
.direction = USBDEV_ENDPOINT_DIR_IN,
};
CHECK_DIF_OK(
dif_usbdev_endpoint_enable(ctx->dev, endpoint, kDifToggleDisabled));
// Remove callback handlers
ctx->in[ep].tx_done_callback = NULL;
ctx->in[ep].flush = NULL;
ctx->in[ep].reset = NULL;
}
void usb_testutils_out_endpoint_remove(usb_testutils_ctx_t *ctx, uint8_t ep) {
// Disable OUT traffic
dif_usbdev_endpoint_id_t endpoint = {
.number = ep,
.direction = USBDEV_ENDPOINT_DIR_OUT,
};
CHECK_DIF_OK(
dif_usbdev_endpoint_enable(ctx->dev, endpoint, kDifToggleDisabled));
// Return the rest of the OUT endpoint configuration to its default state
CHECK_DIF_OK(dif_usbdev_endpoint_set_nak_out_enable(ctx->dev, endpoint.number,
kDifToggleDisabled));
CHECK_DIF_OK(dif_usbdev_endpoint_out_enable(ctx->dev, endpoint.number,
kDifToggleDisabled));
// Remove callback handlers
ctx->out[ep].rx_callback = NULL;
ctx->out[ep].reset = NULL;
}
void usb_testutils_endpoint_remove(usb_testutils_ctx_t *ctx, uint8_t ep) {
usb_testutils_in_endpoint_remove(ctx, ep);
usb_testutils_out_endpoint_remove(ctx, ep);
}
void usb_testutils_init(usb_testutils_ctx_t *ctx, bool pinflip,
bool en_diff_rcvr, bool tx_use_d_se0) {
CHECK(ctx != NULL);
ctx->dev = &usbdev;
ctx->buffer_pool = &buffer_pool;
// Ensure that we do not invoke the endpoint 'flush' functions before
// detection of the first bus frame
ctx->got_frame = false;
ctx->frame = 0u;
CHECK_DIF_OK(
dif_usbdev_init(mmio_region_from_addr(USBDEV_BASE_ADDR), ctx->dev));
dif_usbdev_config_t config = {
.have_differential_receiver = dif_bool_to_toggle(en_diff_rcvr),
.use_tx_d_se0 = dif_bool_to_toggle(tx_use_d_se0),
.single_bit_eop = kDifToggleDisabled,
.pin_flip = dif_bool_to_toggle(pinflip),
.clock_sync_signals = kDifToggleEnabled,
};
CHECK_DIF_OK(dif_usbdev_configure(ctx->dev, ctx->buffer_pool, config));
// Set up context
for (int i = 0; i < USBDEV_NUM_ENDPOINTS; i++) {
usb_testutils_endpoint_setup(ctx, i, kUsbdevOutDisabled, NULL, NULL, NULL,
NULL, NULL);
}
// All about polling...
CHECK_DIF_OK(dif_usbdev_irq_disable_all(ctx->dev, NULL));
// Provide buffers for any packet reception
CHECK_DIF_OK(dif_usbdev_fill_available_fifo(ctx->dev, ctx->buffer_pool));
// Preemptively enable SETUP reception on endpoint zero for the
// Default Control Pipe; all other settings for that endpoint will be applied
// once the callback handlers are registered by a call to _endpoint_setup()
CHECK_DIF_OK(
dif_usbdev_endpoint_setup_enable(ctx->dev, 0, kDifToggleEnabled));
}
void usb_testutils_fin(usb_testutils_ctx_t *ctx) {
// Remove the endpoints in reverse order so that Endpoint Zero goes down last
for (int ep = USBDEV_NUM_ENDPOINTS - 1; ep >= 0; ep--) {
usb_testutils_endpoint_remove(ctx, ep);
}
// Disconnect from the bus
CHECK_DIF_OK(dif_usbdev_interface_enable(ctx->dev, kDifToggleDisabled));
}
// `extern` declarations to give the inline functions in the
// corresponding header a link location.
extern int usb_testutils_halted(usb_testutils_ctx_t *ctx,
dif_usbdev_endpoint_id_t endpoint);