[usbdev] Add new usbdev_test

This new test is a stripped down version of the hello_usbdev example
application more suitable for testing in CI.

Signed-off-by: Pirmin Vogel <vogelpi@lowrisc.org>
diff --git a/ci/run_verilator_pytest.sh b/ci/run_verilator_pytest.sh
index af07425..19fa894 100755
--- a/ci/run_verilator_pytest.sh
+++ b/ci/run_verilator_pytest.sh
@@ -22,6 +22,7 @@
   "tests/dif_uart_sanitytest_sim_verilator.elf"
   "tests/flash_ctrl_test_sim_verilator.elf"
   "tests/sha256_test_sim_verilator.elf"
+  "tests/usbdev_test_sim_verilator.elf"
 )
 
 FAIL_TARGETS=()
diff --git a/hw/dv/dpi/usbdpi/usbdpi.c b/hw/dv/dpi/usbdpi/usbdpi.c
index 3e65180..32dd722 100644
--- a/hw/dv/dpi/usbdpi/usbdpi.c
+++ b/hw/dv/dpi/usbdpi/usbdpi.c
@@ -529,9 +529,9 @@
       ctx->data[1] = 0x82;
       ctx->data[2] = 0 | CRC5(0x82, 11) << 3;
       ctx->data[3] = USB_PID_DATA0;
-      ctx->data[4] = 0x48;
-      ctx->data[5] = 0x69;
-      ctx->data[6] = 0x21;
+      ctx->data[4] = 0x48;  // "H"
+      ctx->data[5] = 0x69;  // "i"
+      ctx->data[6] = 0x21;  // "!"
       add_crc16(ctx->data, ctx->datastart, 7);
       // ctx->data[7] = 0xE0; // pre-computed CRC16
       // ctx->data[8] = 0x61;
diff --git a/sw/device/tests/meson.build b/sw/device/tests/meson.build
index e467d3c..e812708 100644
--- a/sw/device/tests/meson.build
+++ b/sw/device/tests/meson.build
@@ -49,10 +49,22 @@
   ),
 )
 
+usbdev_test_lib = declare_dependency(
+  link_with: static_library(
+    'usbdev_test_lib',
+    sources: ['usbdev_test.c'],
+    dependencies: [
+      sw_lib_usb,
+      sw_lib_base_log,
+    ],
+  ),
+)
+
 sw_tests += {
   'aes_test': aes_test_lib,
   'flash_ctrl_test': flash_ctrl_test_lib,
   'sha256_test': sha256_test_lib,
+  'usbdev_test': usbdev_test_lib,
 }
 
 foreach sw_test_name, sw_test_lib : sw_tests
diff --git a/sw/device/tests/usbdev_test.c b/sw/device/tests/usbdev_test.c
new file mode 100644
index 0000000..563e1f8
--- /dev/null
+++ b/sw/device/tests/usbdev_test.c
@@ -0,0 +1,113 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+//
+// USB device test
+//
+// This test is a stripped down version of the hello_usbdev example application.
+// It requires interaction with the USB DPI model mimicking the host and thus
+// can only be run in the Verilator simulation. The test initializes the USB
+// device and configures USB Endpoint 1 as a simpleserial endpoint. The test
+// then starts polling the USB device for data sent by the host. Any data
+// received on Endpoint 1 is stored in a buffer and printed via UART.
+//
+// The DPI model mimicks the USB host. After device initialization, it detects
+// the assertion of the pullup and first assigns an address to the device. It
+// then sends various USB transactions to the device including two OUT
+// transactions with a data payload of "Hi!" to Endpoint 1. If these two OUT
+// transactions are succesfully received by the device, the test passes.
+
+#include "sw/device/lib/usbdev.h"
+
+#include "sw/device/lib/base/log.h"
+#include "sw/device/lib/runtime/check.h"
+#include "sw/device/lib/testing/test_main.h"
+#include "sw/device/lib/uart.h"
+#include "sw/device/lib/usb_controlep.h"
+#include "sw/device/lib/usb_simpleserial.h"
+
+/**
+ * Configuration values for USB.
+ */
+static uint8_t config_descriptors[] = {
+    USB_CFG_DSCR_HEAD(
+        USB_CFG_DSCR_LEN + 2 * (USB_INTERFACE_DSCR_LEN + 2 * USB_EP_DSCR_LEN),
+        2),
+    VEND_INTERFACE_DSCR(0, 2, 0x50, 1), USB_BULK_EP_DSCR(0, 1, 32, 0),
+    USB_BULK_EP_DSCR(1, 1, 32, 4), VEND_INTERFACE_DSCR(1, 2, 0x50, 1),
+    USB_BULK_EP_DSCR(0, 2, 32, 0), USB_BULK_EP_DSCR(1, 2, 32, 4),
+};
+
+/**
+ * USB device context types.
+ */
+static usbdev_ctx_t usbdev;
+static usb_controlep_ctx_t usbdev_control;
+static usb_ss_ctx_t simple_serial;
+
+/**
+ * Makes `c` into a printable character, replacing it with `replacement`
+ * as necessary.
+ */
+static char make_printable(char c, char replacement) {
+  if (c == 0xa || c == 0xd) {
+    return c;
+  }
+
+  if (c < ' ' || c > '~') {
+    c = replacement;
+  }
+  return c;
+}
+
+static const size_t kExpectedUsbCharsRecved = 6;
+static const char kExpectedUsbRecved[7] = "Hi!Hi!";
+static size_t usb_chars_recved_total;
+static char buffer[7];
+
+/**
+ * Callback for processing USB reciept.
+ */
+static void usb_receipt_callback(uint8_t c) {
+  c = make_printable(c, '?');
+  uart_send_char(c);
+  if (usb_chars_recved_total < kExpectedUsbCharsRecved) {
+    buffer[usb_chars_recved_total] = c;
+    ++usb_chars_recved_total;
+  }
+}
+
+const test_config_t kTestConfig = {};
+
+bool test_main(void) {
+  CHECK(kDeviceType == kDeviceSimVerilator,
+        "This test is not expected to run on platforms other than the "
+        "Verilator simulation. It needs the USB DPI model.");
+
+  uart_init(kUartBaudrate);
+  base_set_stdout(uart_stdout);
+
+  LOG_INFO("Running USBDEV test");
+
+  // Call `usbdev_init` here so that DPI will not start until the
+  // simulation has finished all of the printing, which takes a while
+  // if `--trace` was passed in.
+  usbdev_init(&usbdev);
+  usb_controlep_init(&usbdev_control, &usbdev, 0, config_descriptors,
+                     sizeof(config_descriptors));
+  usb_simpleserial_init(&simple_serial, &usbdev, 1, usb_receipt_callback);
+
+  while (usb_chars_recved_total < kExpectedUsbCharsRecved) {
+    usbdev_poll(&usbdev);
+  }
+
+  uart_send_str("\r\n");
+  for (int i = 0; i < kExpectedUsbCharsRecved; i++) {
+    CHECK(buffer[i] == kExpectedUsbRecved[i],
+          "Recieved char #%d mismatched: exp = %x, actual = %x", i,
+          kExpectedUsbRecved[i], buffer[i]);
+  }
+  LOG_INFO("USB recieved %d characters: %s", usb_chars_recved_total, buffer);
+
+  return true;
+}