| // 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_matcha/sw/autogen/top_matcha.h" |
| |
| #define USBDEV_BASE_ADDR TOP_MATCHA_USBDEV_BASE_ADDR |
| |
| static dif_usbdev_t usbdev; |
| static dif_usbdev_buffer_pool_t buffer_pool; |
| |
| // Internal function to create the packet that will form the next part of a |
| // larger buffer transfer |
| static bool usb_testutils_part_prepare(usb_testutils_ctx_t *ctx, |
| usb_testutils_transfer_t *transfer, |
| dif_usbdev_buffer_t *next_part, |
| bool *last) { |
| CHECK(ctx && transfer && last); |
| |
| // Allocate and fill a packet buffer |
| dif_result_t result = |
| dif_usbdev_buffer_request(ctx->dev, ctx->buffer_pool, next_part); |
| if (result != kDifOk) { |
| return false; |
| } |
| |
| // Determine the maximum bytes/packet |
| unsigned max_packet = USBDEV_MAX_PACKET_SIZE; |
| if (transfer->flags & kUsbTestutilsXfrMaxPacketSupplied) { |
| max_packet = (unsigned)(transfer->flags & kUsbTestutilsXfrMaxPacketMask); |
| } |
| |
| // How much are we sending this time? |
| unsigned part_len = transfer->length - transfer->offset; |
| if (part_len > max_packet) { |
| part_len = max_packet; |
| } |
| size_t bytes_written = 0U; |
| if (part_len) { |
| CHECK_DIF_OK(dif_usbdev_buffer_write(ctx->dev, next_part, |
| &transfer->buffer[transfer->offset], |
| part_len, &bytes_written)); |
| } |
| // Is this the last packet? |
| uint32_t next_offset = transfer->offset + bytes_written; |
| *last = true; |
| if (bytes_written == max_packet) { |
| if (next_offset < transfer->length || |
| (transfer->flags & kUsbTestutilsXfrEmployZLP)) { |
| *last = false; |
| } |
| } else { |
| CHECK(bytes_written < max_packet); |
| } |
| |
| transfer->offset = next_offset; |
| return true; |
| } |
| |
| // Internal function to perform the next part of a larger buffer transfer |
| static bool usb_testutils_transfer_next_part( |
| usb_testutils_ctx_t *ctx, uint8_t ep, usb_testutils_transfer_t *transfer) { |
| // Do we need to prepare a packet? |
| if (!transfer->next_valid && |
| !usb_testutils_part_prepare(ctx, transfer, &transfer->next_part, |
| &transfer->last)) { |
| return false; |
| } |
| |
| // Send the existing prepared packet |
| CHECK_DIF_OK(dif_usbdev_send(ctx->dev, ep, &transfer->next_part)); |
| transfer->next_valid = false; |
| |
| // If we're double-buffering, request and fill another buffer immediately; |
| // we'll then be able to supply it much more promptly later... |
| if ((transfer->flags & kUsbTestutilsXfrDoubleBuffered) && !transfer->last) { |
| transfer->next_valid = usb_testutils_part_prepare( |
| ctx, transfer, &transfer->next_part, &transfer->last); |
| } |
| |
| return true; |
| } |
| |
| 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) { |
| return; |
| } |
| |
| // 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); |
| unsigned ep = 0u; |
| while (sentep && ep < USBDEV_NUM_ENDPOINTS) { |
| if (sentep & (1u << ep)) { |
| // Free up the buffer and optionally callback |
| CHECK_DIF_OK( |
| dif_usbdev_clear_tx_status(ctx->dev, ctx->buffer_pool, ep)); |
| |
| // If we have a larger transfer in progress, continue with that |
| usb_testutils_transfer_t *transfer = &ctx->in[ep].transfer; |
| usb_testutils_xfr_result_t res = kUsbTestutilsXfrResultOk; |
| bool done = true; |
| if (transfer->buffer) { |
| if (transfer->next_valid || !transfer->last) { |
| if (usb_testutils_transfer_next_part(ctx, ep, transfer)) { |
| done = false; |
| } else { |
| res = kUsbTestutilsXfrResultFailed; |
| } |
| } |
| if (done) { |
| // Larger buffer transfer now completed; forget the buffer |
| transfer->buffer = NULL; |
| } |
| } |
| // Notify that we've sent the single packet, or larger buffer transfer |
| // is now complete |
| if (done && ctx->in[ep].tx_done_callback) { |
| ctx->in[ep].tx_done_callback(ctx->in[ep].ep_ctx, res); |
| } |
| sentep &= ~(1u << ep); |
| } |
| ep++; |
| } |
| } |
| |
| // Keep buffers available for packet reception |
| CHECK_DIF_OK(dif_usbdev_fill_available_fifo(ctx->dev, ctx->buffer_pool)); |
| |
| if (istate & (1u << kDifUsbdevIrqPktReceived)) { |
| // TODO: we run the risk of starving the IN side here if the rx_callback(s) |
| // are time-consuming |
| 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(ep, 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)); |
| |
| // 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; |
| } |
| |
| // 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? |
| } |
| |
| bool usb_testutils_transfer_send(usb_testutils_ctx_t *ctx, uint8_t ep, |
| const uint8_t *data, uint32_t length, |
| usb_testutils_xfr_flags_t flags) { |
| CHECK(ep < USBDEV_NUM_ENDPOINTS); |
| |
| usb_testutils_transfer_t *transfer = &ctx->in[ep].transfer; |
| if (transfer->buffer) { |
| // If there is an in-progress transfer, then we cannot accept another |
| return false; |
| } |
| |
| // Describe this transfer |
| transfer->buffer = data; |
| transfer->offset = 0U; |
| transfer->length = length; |
| transfer->flags = flags; |
| transfer->next_valid = false; |
| |
| if (!usb_testutils_transfer_next_part(ctx, ep, transfer)) { |
| // Forget about the attempted transfer |
| transfer->buffer = NULL; |
| return false; |
| } |
| |
| // Buffer transfer is underway... |
| return true; |
| } |
| |
| void usb_testutils_in_endpoint_setup( |
| usb_testutils_ctx_t *ctx, uint8_t ep, void *ep_ctx, |
| void (*tx_done)(void *, usb_testutils_xfr_result_t), 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 *, usb_testutils_xfr_result_t), |
| 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); |