diff --git a/.github/workflows/twister_uhc_build_and_run.yaml b/.github/workflows/twister_uhc_build_and_run.yaml new file mode 100644 index 0000000000000..0d9be08547fc5 --- /dev/null +++ b/.github/workflows/twister_uhc_build_and_run.yaml @@ -0,0 +1,107 @@ +name: USB UHC Driver Test (esp32s3) + +on: + pull_request: + types: [opened, reopened, synchronize] + workflow_dispatch: + +permissions: + contents: read + +jobs: + twister-tests: + if: startsWith(github.head_ref, 'pr-add-') || github.event_name == 'workflow_dispatch' + runs-on: [self-hosted, docker, esp32s3, usb_host] + + strategy: + matrix: + zephyr_ver: ["v0.29.2"] + + container: + image: ghcr.io/zephyrproject-rtos/ci:${{ matrix.zephyr_ver }} + options: >- + --volume=/etc/zephyr:/etc/zephyr:ro + --device=/dev/ttyACM0 + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + path: ws/zephyr + fetch-depth: 0 + + - name: Setup Python deps (west + twister requirements) + shell: bash + run: | + python3 -m venv "$GITHUB_WORKSPACE/ws/.venv" + . "$GITHUB_WORKSPACE/ws/.venv/bin/activate" + pip install -U pip wheel esptool pyserial + pip install -r "$GITHUB_WORKSPACE/ws/zephyr/scripts/requirements.txt" \ + -r "$GITHUB_WORKSPACE/ws/zephyr/scripts/requirements-build-test.txt" + + - name: West init/update/export (modules + blobs) + shell: bash + run: | + . "$GITHUB_WORKSPACE/ws/.venv/bin/activate" + cd "$GITHUB_WORKSPACE/ws" + + if [ ! -d .west ]; then + west init -l zephyr + fi + + west update hal_espressif + west blobs fetch hal_espressif + west zephyr-export + + - name: Locate Zephyr SDK in container + shell: bash + run: | + for d in /opt/zephyr-sdk* /opt/toolchains/zephyr-sdk* /usr/local/zephyr-sdk*; do + if [ -d "$d" ]; then + echo "Found Zephyr SDK at $d" + echo "ZEPHYR_SDK_INSTALL_DIR=$d" >> "$GITHUB_ENV" + break + fi + done + + - name: Clean previous Twister output + shell: bash + run: | + rm -rf "$GITHUB_WORKSPACE/twister-out" + rm -rf "$GITHUB_WORKSPACE"/twister-out.* + + - name: Build and run on hardware + shell: bash + env: + PYTEST_DISABLE_PLUGIN_AUTOLOAD: "1" + ZEPHYR_TOOLCHAIN_VARIANT: zephyr + run: | + git config --global --add safe.directory "$GITHUB_WORKSPACE/ws/zephyr" + + . "$GITHUB_WORKSPACE/ws/.venv/bin/activate" + cd "$GITHUB_WORKSPACE/ws/zephyr" + + west twister -T tests/drivers/uhc --list-tests + + west twister \ + -T tests/drivers/uhc \ + -p esp32s3_devkitc/esp32s3/procpu \ + -s drivers.usb.uhc \ + --device-testing \ + --device-serial /dev/ttyACM0 \ + --flash-before \ + --west-runner=esp32 \ + --log-level debug \ + -j 1 \ + --outdir "$GITHUB_WORKSPACE/twister-out" + + - name: Upload Twister reports + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: twister-reports + path: | + twister-out/**/twister_harness.log + twister-out/twister.xml + twister-out/twister_report.xml + twister-out/twister.json diff --git a/boards/nordic/nrf54lm20dk/nrf54lm20_a_b_cpuapp_common.dtsi b/boards/nordic/nrf54lm20dk/nrf54lm20_a_b_cpuapp_common.dtsi index fc4275f44bc12..42c73d126c453 100644 --- a/boards/nordic/nrf54lm20dk/nrf54lm20_a_b_cpuapp_common.dtsi +++ b/boards/nordic/nrf54lm20dk/nrf54lm20_a_b_cpuapp_common.dtsi @@ -110,6 +110,10 @@ temp_sensor: &temp { status = "okay"; }; +zephyr_uhc0: &usbhs { + status = "okay"; +}; + zephyr_udc0: &usbhs { status = "okay"; }; diff --git a/boards/nordic/nrf54lm20dk/nrf54lm20dk_nrf54lm20a_cpuapp.yaml b/boards/nordic/nrf54lm20dk/nrf54lm20dk_nrf54lm20a_cpuapp.yaml index 36c37206b219a..571be8ea2ed74 100644 --- a/boards/nordic/nrf54lm20dk/nrf54lm20dk_nrf54lm20a_cpuapp.yaml +++ b/boards/nordic/nrf54lm20dk/nrf54lm20dk_nrf54lm20a_cpuapp.yaml @@ -21,4 +21,5 @@ supported: - pwm - spi - usbd + - usbh - watchdog diff --git a/boards/nordic/nrf54lm20dk/nrf54lm20dk_nrf54lm20a_cpuapp_ns.yaml b/boards/nordic/nrf54lm20dk/nrf54lm20dk_nrf54lm20a_cpuapp_ns.yaml index 2bd2c023ca718..0edce1a2a541f 100644 --- a/boards/nordic/nrf54lm20dk/nrf54lm20dk_nrf54lm20a_cpuapp_ns.yaml +++ b/boards/nordic/nrf54lm20dk/nrf54lm20dk_nrf54lm20a_cpuapp_ns.yaml @@ -17,6 +17,7 @@ supported: - pwm - spi - usbd + - usbh - watchdog vendor: nordic sysbuild: true diff --git a/boards/nordic/nrf54lm20dk/nrf54lm20dk_nrf54lm20b_cpuapp.yaml b/boards/nordic/nrf54lm20dk/nrf54lm20dk_nrf54lm20b_cpuapp.yaml index 6b1a8ed806ae4..55c4d2ccea8f0 100644 --- a/boards/nordic/nrf54lm20dk/nrf54lm20dk_nrf54lm20b_cpuapp.yaml +++ b/boards/nordic/nrf54lm20dk/nrf54lm20dk_nrf54lm20b_cpuapp.yaml @@ -21,4 +21,5 @@ supported: - pwm - spi - usbd + - usbh - watchdog diff --git a/boards/nordic/nrf54lm20dk/nrf54lm20dk_nrf54lm20b_cpuapp_ns.yaml b/boards/nordic/nrf54lm20dk/nrf54lm20dk_nrf54lm20b_cpuapp_ns.yaml index afd7bcf905c00..8c94a106832c5 100644 --- a/boards/nordic/nrf54lm20dk/nrf54lm20dk_nrf54lm20b_cpuapp_ns.yaml +++ b/boards/nordic/nrf54lm20dk/nrf54lm20dk_nrf54lm20b_cpuapp_ns.yaml @@ -17,6 +17,7 @@ supported: - pwm - spi - usbd + - usbh - watchdog vendor: nordic sysbuild: true diff --git a/drivers/usb/common/usb_dwc2_hw.h b/drivers/usb/common/usb_dwc2_hw.h index 50022d1a3eeb7..595a10e79bd41 100644 --- a/drivers/usb/common/usb_dwc2_hw.h +++ b/drivers/usb/common/usb_dwc2_hw.h @@ -223,6 +223,8 @@ USB_DWC2_GET_FIELD_DEFINE(gahbcfg_hbstlen, GAHBCFG_HBSTLEN) #define USB_DWC2_GUSBCFG_ULPICLK_SUSM BIT(USB_DWC2_GUSBCFG_ULPICLK_SUSM_POS) #define USB_DWC2_GUSBCFG_ULPIFSLS_POS 17UL #define USB_DWC2_GUSBCFG_ULPIFSLS BIT(USB_DWC2_GUSBCFG_ULPIFSLS_POS) +#define USB_DWC2_GUSBCFG_PHYLPCLK_SEL_POS 15UL +#define USB_DWC2_GUSBCFG_PHYLPCLK_SEL_LP BIT(USB_DWC2_GUSBCFG_PHYLPCLK_SEL_POS) #define USB_DWC2_GUSBCFG_DDR_SEL_POS 7UL #define USB_DWC2_GUSBCFG_DDR_SINGLE 0UL #define USB_DWC2_GUSBCFG_DDR_DOUBLE BIT(USB_DWC2_GUSBCFG_DDR_SEL_POS) @@ -374,7 +376,13 @@ USB_DWC2_SET_FIELD_DEFINE(gnptxfsiz_nptxfstaddr, GNPTXFSIZ_NPTXFSTADDR) #define USB_DWC2_GGPIO_STM32_PWRDWN BIT(USB_DWC2_GGPIO_STM32_PWRDWN_POS) /* GSNPSID register */ +#define USB_DWC2_GSNPSID_REV_2_92A 0x4F54292AUL +#define USB_DWC2_GSNPSID_REV_4_20A 0x4F54420AUL #define USB_DWC2_GSNPSID_REV_5_00A 0x4F54500AUL +#define USB_DWC2_GSNPSID_REV_POS 0UL +#define USB_DWC2_GSNPSID_REV_MASK (0xFFFFUL << USB_DWC2_GSNPSID_REV_POS) + +USB_DWC2_GET_FIELD_DEFINE(gsnpsid_rev, GSNPSID_REV) /* GHWCFG1 register */ #define USB_DWC2_GHWCFG1 0x0044UL diff --git a/drivers/usb/uhc/CMakeLists.txt b/drivers/usb/uhc/CMakeLists.txt index 69f1cea8433b2..8f834f1d6e086 100644 --- a/drivers/usb/uhc/CMakeLists.txt +++ b/drivers/usb/uhc/CMakeLists.txt @@ -3,8 +3,10 @@ # SPDX-License-Identifier: Apache-2.0 zephyr_library() +zephyr_library_include_directories(${ZEPHYR_BASE}/drivers/usb/common/) zephyr_library_sources(uhc_common.c) +zephyr_library_sources_ifdef(CONFIG_UHC_DWC2 uhc_dwc2.c) zephyr_library_sources_ifdef(CONFIG_UHC_MAX3421E uhc_max3421e.c) zephyr_library_sources_ifdef(CONFIG_UHC_VIRTUAL uhc_virtual.c) zephyr_library_sources_ifdef(CONFIG_UHC_NXP_EHCI uhc_mcux_common.c uhc_mcux_ehci.c) diff --git a/drivers/usb/uhc/Kconfig b/drivers/usb/uhc/Kconfig index a9cdbad42e4d2..c4c2895d9bfc0 100644 --- a/drivers/usb/uhc/Kconfig +++ b/drivers/usb/uhc/Kconfig @@ -43,6 +43,7 @@ module = UHC_DRIVER module-str = uhc drv source "subsys/logging/Kconfig.template.log_config" +source "drivers/usb/uhc/Kconfig.dwc2" source "drivers/usb/uhc/Kconfig.max3421e" source "drivers/usb/uhc/Kconfig.virtual" source "drivers/usb/uhc/Kconfig.mcux" diff --git a/drivers/usb/uhc/Kconfig.dwc2 b/drivers/usb/uhc/Kconfig.dwc2 new file mode 100644 index 0000000000000..a952b915682c9 --- /dev/null +++ b/drivers/usb/uhc/Kconfig.dwc2 @@ -0,0 +1,100 @@ +# Copyright (c) 2026 Roman Leonov +# SPDX-License-Identifier: Apache-2.0 + +config UHC_DWC2 + bool "DWC2 USB host controller driver" + depends on DT_HAS_SNPS_DWC2_ENABLED + depends on !UDC_DWC2 + select EVENTS + select REGULATOR + help + DWC2 USB host controller driver. + +if UHC_DWC2 + +config UHC_DWC2_MAX_CHANNELS + int "UHC DWC2 driver maximum supported number of channels" + default 16 + range 1 16 + help + Number of channels driver can support. + Reduce this value to save memory. + Increase to support more concurrent transfers. + +config UHC_DRIVER_HIGH_SPEED_SUPPORT_ENABLED + bool "UHC DWC2 High speed" + default y if SOC_SERIES_NRF54LX + help + Enables HS support code if the SoC/board can support it. + +config UHC_DWC2_STACK_SIZE + int "UHC DWC2 driver thread stack size" + default 512 + help + DWC2 driver thread stack size. + +config UHC_DWC2_THREAD_PRIORITY + int "UHC DWC2 driver thread priority" + default 8 + help + DWC2 driver thread priority. + +config UHC_DWC2_DEBOUNCE_DELAY_MS + int "Debounce delay in ms" + default 250 + help + On connection of a USB device, the USB 2.0 specification requires + a "debounce interval with a minimum duration of 100ms" to allow the connection to stabilize + (see USB 2.0 chapter 7.1.7.3 for more details). + During the debounce interval, no new connection/disconnection events are registered. + + The default value is set to 250 ms to be safe. + +config UHC_DWC2_RESET_HOLD_MS + int "Reset hold in ms" + default 50 + help + Root-port reset hold time in milliseconds. + + USB 2.0 requires ordinary reset signaling to be asserted for at least + 10 ms, but reset issued from a root port must last at least 50 ms.. + + The default value is set to 50 ms. + +config UHC_DWC2_RESET_RECOVERY_MS + int "Reset recovery delay in ms" + default 30 + help + Post-reset recovery delay in milliseconds. + + USB 2.0 requires system software to allow at least 10 ms for reset + recovery after the port stops driving reset, before the attached + device is expected to respond to data transfers. + + The attached device may ignore data transfers during this recovery + interval. + + The default value is set to 30 ms. + +config UHC_DWC2_SET_ADDR_DELAY_MS + int "Delay after set device address" + default 10 + help + "After successful completion of the Status stage, the device is allowed a SetAddress() + recovery interval of 2 ms. At the end of this interval, the device must be able to accept + Setup packets addressed to the new address. Also, at the end of the recovery interval, the + device must not respond to tokens sent to the old address (unless, of course, the old and new + address is the same)." See USB 2.0 chapter 9.2.6.3 for more details. + + The default value is set to 10 ms to be safe. + +config UHC_DWC2_USBHS_VBUS_READY_TIMEOUT + int "USBHS VBUS ready event timeout in ms" + depends on SOC_SERIES_NRF54L + default 0 + help + VBUS ready event timeout for the USBHS variant of DWC2 found on nRF MCUs. + If the VBUS is not ready and the Nordic USBHS controller is used, the udc_enable() is + blocked for this amount of time. Set it to zero to wait forever. + +endif # UHC_DWC2 diff --git a/drivers/usb/uhc/uhc_dwc2.c b/drivers/usb/uhc/uhc_dwc2.c new file mode 100644 index 0000000000000..479d47342d69c --- /dev/null +++ b/drivers/usb/uhc/uhc_dwc2.c @@ -0,0 +1,1702 @@ +/* + * Copyright (c) 2026 Roman Leonov + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#define DT_DRV_COMPAT snps_dwc2 + +#include +#include +#include +#include +#include + +#include +LOG_MODULE_REGISTER(uhc_dwc2, CONFIG_UHC_DRIVER_LOG_LEVEL); + +#include "uhc_common.h" +#include "uhc_dwc2.h" + +#define DEBOUNCE_DELAY_MS CONFIG_UHC_DWC2_DEBOUNCE_DELAY_MS +#define RESET_HOLD_MS CONFIG_UHC_DWC2_RESET_HOLD_MS +#define RESET_RECOVERY_MS CONFIG_UHC_DWC2_RESET_RECOVERY_MS +#define SET_ADDR_DELAY_MS CONFIG_UHC_DWC2_SET_ADDR_DELAY_MS +#define HIGH_SPEED CONFIG_UHC_DRIVER_HIGH_SPEED_SUPPORT_ENABLED +#define MAX_CHANNELS CONFIG_UHC_DWC2_MAX_CHANNELS + +enum uhc_dwc2_event { + /* A device has been connected to the port */ + UHC_DWC2_EVENT_PORT_CONNECTION, + /* A device is connected to the port but has not been reset. */ + UHC_DWC2_EVENT_PORT_DISABLED, + /* A device disconnection has been detected */ + UHC_DWC2_EVENT_PORT_DISCONNECTION, + /* Port error detected */ + UHC_DWC2_EVENT_PORT_ERROR, + /* Overcurrent detected */ + UHC_DWC2_EVENT_PORT_OVERCURRENT, + /* Port has pending channel event */ + UHC_DWC2_EVENT_PORT_PEND_CHANNEL +}; + +enum uhc_dwc2_channel_event { + /* The channel has completed a transfer. Channel is now halted */ + UHC_DWC2_CHANNEL_EVENT_CPLT, + /* The channel has completed a transfer. Request STALLed. Channel is now halted */ + UHC_DWC2_CHANNEL_EVENT_STALL, + /* The channel has encountered an error. Channel is now halted */ + UHC_DWC2_CHANNEL_EVENT_ERROR, + /* Need to release the channel for the next transfer */ + UHC_DWC2_CHANNEL_DO_RELEASE, + /* Need to submit the transfer after channel error. Channel is now halted */ + UHC_DWC2_CHANNEL_DO_REINIT, + /* Need to rewind the buffer being transmitted. Channel is now halted */ + UHC_DWC2_CHANNEL_DO_REWIND, +}; + +#define EPSIZE_BULK_FS 64U + +/* Mask to clear HPRT register */ +#define USB_DWC2_HPRT_W1C_MSK (USB_DWC2_HPRT_PRTENA | \ + USB_DWC2_HPRT_PRTCONNDET | \ + USB_DWC2_HPRT_PRTENCHNG | \ + USB_DWC2_HPRT_PRTOVRCURRCHNG) + +/* Mask of errors received for host channels (STALL excluded) */ +#define USB_DWC2_HCINT_ERRORS (USB_DWC2_HCINT_AHBERR | \ + USB_DWC2_HCINT_XACTERR | \ + USB_DWC2_HCINT_BBLERR | \ + USB_DWC2_HCINT_FRMOVRUN | \ + USB_DWC2_HCINT_DTGERR) + +/* Mask of reason of transfer completion for host channels */ +#define USB_DWC2_HCINT_CPLT_BITS (USB_DWC2_HCINT_XFERCOMPL | \ + USB_DWC2_HCINT_STALL | \ + USB_DWC2_HCINT_BBLERR | \ + USB_DWC2_HCINT_XACTERR) +struct uhc_dwc2_channel { + /* Index of the channel */ + uint8_t index; + /* Accessed in ISR. Number of error to track errors for transactions. */ + uint8_t error_count; + /* Used to save pending completion bits, while waiting channel to be halted. */ + uint32_t hcint_cplt_pending; + /* Cached pointer to channel registers */ + const struct usb_dwc2_host_chan *regs; + /* Pointer to the transfer */ + struct uhc_transfer *xfer; + /* Channel events */ + atomic_t events; + /* Control Channel transfer helpers */ + uint32_t data_stage_size; +}; + +struct uhc_dwc2_data { + struct k_thread thread; + /* Event bitmask the driver thread waits for */ + struct k_event events; + /* Semaphore used to indicate that port is enabled */ + struct k_sem sem_port_enabled; + /* Port channels */ + struct uhc_dwc2_channel channels[MAX_CHANNELS]; + /* Root Port flags */ + uint8_t debouncing: 1; + uint8_t has_device: 1; +}; + +static inline uint16_t calc_packet_count(const uint16_t size, const uint8_t mps) +{ + if (size == 0) { + return 1; /* in Buffer DMA mode Zero Length Packet still counts as 1 packet */ + } else { + return DIV_ROUND_UP(size, mps); + } +} + +static inline void dwc2_hal_set_reset(struct usb_dwc2_reg *const dwc2, const bool reset_on) +{ + uint32_t hprt; + + hprt = sys_read32((mem_addr_t)&dwc2->hprt); + if (reset_on) { + hprt |= USB_DWC2_HPRT_PRTRST; + } else { + hprt &= ~USB_DWC2_HPRT_PRTRST; + } + sys_write32(hprt & (~USB_DWC2_HPRT_W1C_MSK), (mem_addr_t)&dwc2->hprt); +} + +static inline void dwc2_hal_set_power(struct usb_dwc2_reg *const dwc2, const bool power_on) +{ + uint32_t hprt; + + hprt = sys_read32((mem_addr_t)&dwc2->hprt); + if (power_on) { + hprt |= USB_DWC2_HPRT_PRTPWR; + } else { + hprt &= ~USB_DWC2_HPRT_PRTPWR; + } + sys_write32(hprt & (~USB_DWC2_HPRT_W1C_MSK), (mem_addr_t)&dwc2->hprt); +} + +static inline uint32_t dwc2_hal_get_speed(struct usb_dwc2_reg *const dwc2) +{ + uint32_t hprt = sys_read32((mem_addr_t)&dwc2->hprt); + + return usb_dwc2_get_hprt_prtspd(hprt); +} + +static int dwc2_hal_flush_rx_fifo(struct usb_dwc2_reg *const dwc2) +{ + const k_timepoint_t timepoint = sys_timepoint_calc(K_MSEC(100)); + + sys_write32(USB_DWC2_GRSTCTL_RXFFLSH, (mem_addr_t)&dwc2->grstctl); + + while (sys_read32((mem_addr_t)&dwc2->grstctl) & USB_DWC2_GRSTCTL_RXFFLSH) { + k_busy_wait(1); + + if (sys_timepoint_expired(timepoint)) { + LOG_ERR("Wait for RXFFLSH timeout"); + return -ETIMEDOUT; + } + } + + return 0; +} + +static int dwc2_hal_flush_tx_fifo(struct usb_dwc2_reg *const dwc2, const uint8_t fnum) +{ + const k_timepoint_t timepoint = sys_timepoint_calc(K_MSEC(100)); + uint32_t grstctl; + + grstctl = usb_dwc2_set_grstctl_txfnum(fnum) | USB_DWC2_GRSTCTL_TXFFLSH; + sys_write32(grstctl, (mem_addr_t)&dwc2->grstctl); + + while (sys_read32((mem_addr_t)&dwc2->grstctl) & USB_DWC2_GRSTCTL_TXFFLSH) { + k_busy_wait(1); + + if (sys_timepoint_expired(timepoint)) { + LOG_ERR("Wait for TXFFLSH timeout"); + return -ETIMEDOUT; + } + } + + return 0; +} + +static void dwc2_hal_config_timings(struct usb_dwc2_reg *const dwc2) +{ + uint32_t gusbcfg = sys_read32((mem_addr_t)&dwc2->gusbcfg); + uint32_t ghwcfg2 = sys_read32((mem_addr_t)&dwc2->ghwcfg2); + uint32_t hcfg = sys_read32((mem_addr_t)&dwc2->hcfg); + uint32_t hfir = sys_read32((mem_addr_t)&dwc2->hfir); + uint32_t hprt = sys_read32((mem_addr_t)&dwc2->hprt); + uint32_t phy_clock_mhz = 0; + uint32_t fslspclk = 0; + uint32_t frint = 0; + bool hs_phy = (usb_dwc2_get_ghwcfg2_hsphytype(ghwcfg2) != USB_DWC2_GHWCFG2_HSPHYTYPE_NO_HS); + + if (hs_phy) { + fslspclk = USB_DWC2_HCFG_FSLSPCLKSEL_CLK3060; + if (gusbcfg | USB_DWC2_GUSBCFG_PHYIF_16_BIT) { + phy_clock_mhz = 30U; + } else { + phy_clock_mhz = 60U; + } + } else { + fslspclk = USB_DWC2_HCFG_FSLSPCLKSEL_CLK48; + phy_clock_mhz = 48U; + } + + switch (usb_dwc2_get_hprt_prtspd(hprt)) { + case USB_DWC2_HPRT_PRTSPD_HIGH: + frint = 125U * phy_clock_mhz - 1; + break; + case USB_DWC2_HPRT_PRTSPD_FULL: + frint = 1000U * phy_clock_mhz - 1U; + break; + case USB_DWC2_HPRT_PRTSPD_LOW: + if (hs_phy) { + gusbcfg |= USB_DWC2_GUSBCFG_PHYLPCLK_SEL_LP; + sys_write32(gusbcfg, (mem_addr_t)&dwc2->gusbcfg); + + frint = 1000U * phy_clock_mhz - 1U; + } else { + fslspclk = USB_DWC2_HCFG_FSLSPCLKSEL_CLK6; + frint = 1000U * 6U - 1U; + } + break; + default: + LOG_WRN("Port has unexpected speed, configure FrInt for full speed"); + frint = 1000U * phy_clock_mhz - 1U; + break; + } + + hcfg &= ~USB_DWC2_HCFG_FSLSPCLKSEL_MASK; + hcfg |= fslspclk << USB_DWC2_HCFG_FSLSPCLKSEL_POS; + + hfir &= ~USB_DWC2_HFIR_FRINT_MASK; + hfir |= usb_dwc2_set_hfir_frint(frint); + + sys_write32(hcfg, (mem_addr_t)&dwc2->hcfg); + sys_write32(hfir, (mem_addr_t)&dwc2->hfir); + + LOG_DBG("Timings: speed=%u, phy_clk=%uMHz, fslspclk=%u, frint=%u", + usb_dwc2_get_hprt_prtspd(hprt), + phy_clock_mhz, + fslspclk, + frint); +} + +static inline int dwc2_hal_init_gusbcfg(struct usb_dwc2_reg *const dwc2) +{ + uint32_t gusbcfg = sys_read32((mem_addr_t)&dwc2->gusbcfg); + uint32_t ghwcfg2 = sys_read32((mem_addr_t)&dwc2->ghwcfg2); + uint32_t ghwcfg4 = sys_read32((mem_addr_t)&dwc2->ghwcfg4); + const k_timepoint_t timepoint = sys_timepoint_calc(K_MSEC(100)); + + /* Enable Host mode */ + sys_set_bits((mem_addr_t)&dwc2->gusbcfg, USB_DWC2_GUSBCFG_FORCEHSTMODE); + + /* Wait until core is in host mode */ + while ((sys_read32((mem_addr_t)&dwc2->gintsts) & USB_DWC2_GINTSTS_CURMOD) == 0) { + k_busy_wait(1); + + if (sys_timepoint_expired(timepoint)) { + LOG_ERR("Wait for Host mode timeout, GINTSTS 0x%08x", + sys_read32((mem_addr_t)&dwc2->gintsts)); + return -ETIMEDOUT; + } + } + + if (usb_dwc2_get_ghwcfg2_hsphytype(ghwcfg2) == USB_DWC2_GHWCFG2_HSPHYTYPE_NO_HS) { + /* Select FS PHY */ + LOG_DBG("Fullspeed PHY init"); + sys_set_bits((mem_addr_t)&dwc2->gusbcfg, USB_DWC2_GUSBCFG_PHYSEL_USB11); + } else { + /* Deselect FS PHY */ + gusbcfg &= ~USB_DWC2_GUSBCFG_PHYSEL_USB11; + + if (usb_dwc2_get_ghwcfg2_hsphytype(ghwcfg2) == USB_DWC2_GHWCFG2_HSPHYTYPE_ULPI) { + LOG_DBG("Highspeed ULPI PHY init"); + /* Select ULPI PHY */ + gusbcfg |= USB_DWC2_GUSBCFG_ULPI_UTMI_SEL_ULPI; + /* ULPI is always 8-bit interface */ + gusbcfg &= ~USB_DWC2_GUSBCFG_PHYIF_16_BIT; + /* ULPI select single data rate */ + gusbcfg &= ~USB_DWC2_GUSBCFG_DDR_DOUBLE; + /* Default internal VBUS Indicator and Drive */ + gusbcfg &= ~(USB_DWC2_GUSBCFG_ULPIEVBUSD | USB_DWC2_GUSBCFG_ULPIEVBUSI); + /* Disable FS/LS ULPI and Suspend mode */ + gusbcfg &= ~(USB_DWC2_GUSBCFG_ULPIFSLS | USB_DWC2_GUSBCFG_ULPICLK_SUSM); + } else { + LOG_DBG("Highspeed UTMI+ PHY init"); + /* Select UTMI+ PHY */ + gusbcfg &= ~USB_DWC2_GUSBCFG_ULPI_UTMI_SEL_ULPI; + /* Set 16-bit interface if supported */ + if (usb_dwc2_get_ghwcfg4_phydatawidth(ghwcfg4)) { + gusbcfg |= USB_DWC2_GUSBCFG_PHYIF_16_BIT; + } else { + gusbcfg &= ~USB_DWC2_GUSBCFG_PHYIF_16_BIT; + } + } + sys_write32(gusbcfg, (mem_addr_t)&dwc2->gusbcfg); + } + + return 0; +} + +static inline int dwc2_hal_init_gahbcfg(struct usb_dwc2_reg *const dwc2) +{ + uint32_t ghwcfg2; + uint32_t gahbcfg; + + /* Disable Global Interrupt */ + sys_clear_bits((mem_addr_t)&dwc2->gahbcfg, USB_DWC2_GAHBCFG_GLBINTRMASK); + + /* Configure AHB */ + gahbcfg = sys_read32((mem_addr_t)&dwc2->gahbcfg); + gahbcfg &= ~USB_DWC2_GAHBCFG_HBSTLEN_MASK; + gahbcfg |= usb_dwc2_set_gahbcfg_hbstlen(USB_DWC2_GAHBCFG_HBSTLEN_INCR16); + sys_write32(gahbcfg, (mem_addr_t)&dwc2->gahbcfg); + + ghwcfg2 = sys_read32((mem_addr_t)&dwc2->ghwcfg2); + if (usb_dwc2_get_ghwcfg2_otgarch(ghwcfg2) == USB_DWC2_GHWCFG2_OTGARCH_INTERNALDMA) { + sys_set_bits((mem_addr_t)&dwc2->gahbcfg, USB_DWC2_GAHBCFG_DMAEN); + } else { + /* TODO: Implement Slave mode */ + LOG_ERR("DMA isn't supported by the hardware"); + return -ENXIO; + } + + /* Enable Global Interrupt */ + sys_set_bits((mem_addr_t)&dwc2->gahbcfg, USB_DWC2_GAHBCFG_GLBINTRMASK); + return 0; +} + +static int dwc2_core_soft_reset(const struct device *dev) +{ + const struct uhc_dwc2_config *const config = dev->config; + struct usb_dwc2_reg *const dwc2 = config->base; + mem_addr_t grstctl_reg = (mem_addr_t)&dwc2->grstctl; + mem_addr_t gsnpsid_reg = (mem_addr_t)&dwc2->gsnpsid; + k_timepoint_t timepoint; + uint32_t gsnpsid; + uint32_t grstctl; + uint32_t gsnpsid_rev; + + /* Check AHB master idle state before starting reset */ + timepoint = sys_timepoint_calc(K_MSEC(100)); + while (!(sys_read32(grstctl_reg) & USB_DWC2_GRSTCTL_AHBIDLE)) { + k_busy_wait(1); + + if (sys_timepoint_expired(timepoint)) { + LOG_ERR("Wait for AHB idle timeout, GRSTCTL 0x%08x", + sys_read32(grstctl_reg)); + return -ETIMEDOUT; + } + } + + /* + * Load GSNPSID before reset. On some cores it may not be readable + * after reset is asserted. + */ + gsnpsid = sys_read32(gsnpsid_reg); + gsnpsid_rev = usb_dwc2_get_gsnpsid_rev(gsnpsid); + + /* Apply Core Soft Reset */ + sys_set_bits(grstctl_reg, USB_DWC2_GRSTCTL_CSFTRST); + + if (gsnpsid_rev < usb_dwc2_get_gsnpsid_rev(USB_DWC2_GSNPSID_REV_4_20A)) { + /* + * Before v4.20a, CSFTRST is self-clearing. + * Wait until hardware clears it. + */ + timepoint = sys_timepoint_calc(K_MSEC(100)); + while (sys_read32(grstctl_reg) & USB_DWC2_GRSTCTL_CSFTRST) { + k_busy_wait(1); + + if (sys_timepoint_expired(timepoint)) { + LOG_ERR("Wait for CSR clear timeout, GRSTCTL 0x%08x", + sys_read32(grstctl_reg)); + return -ETIMEDOUT; + } + + if (uhc_dwc2_quirk_is_phy_clk_off(dev)) { + LOG_ERR("Core soft reset stuck, PHY clock is off"); + return -EIO; + } + } + + /* Hardware requires at least 3 PHY clocks after CSFTRST clears. */ + k_busy_wait(1); + } else { + /* + * From v4.20a, CSFTRST is write-only/reset-controlled. + * Wait for CSFTRSTDONE, then clear it. + */ + timepoint = sys_timepoint_calc(K_MSEC(100)); + while (!(sys_read32(grstctl_reg) & USB_DWC2_GRSTCTL_CSFTRSTDONE)) { + k_busy_wait(1); + + if (sys_timepoint_expired(timepoint)) { + LOG_ERR("Wait for CSR done timeout, GRSTCTL 0x%08x", + sys_read32(grstctl_reg)); + return -ETIMEDOUT; + } + + if (uhc_dwc2_quirk_is_phy_clk_off(dev)) { + LOG_ERR("Core soft reset stuck, PHY clock is off"); + return -EIO; + } + } + + /* Clear CSFTRST, and write 1 to CSFTRSTDONE to clear the done indication. */ + grstctl = sys_read32(grstctl_reg); + grstctl &= ~USB_DWC2_GRSTCTL_CSFTRST; + grstctl |= USB_DWC2_GRSTCTL_CSFTRSTDONE; + sys_write32(grstctl, grstctl_reg); + } + + /* Wait for AHB master idle again after reset */ + timepoint = sys_timepoint_calc(K_MSEC(100)); + while (!(sys_read32(grstctl_reg) & USB_DWC2_GRSTCTL_AHBIDLE)) { + k_busy_wait(1); + + if (sys_timepoint_expired(timepoint)) { + LOG_ERR("Wait for AHB idle after reset timeout, GRSTCTL 0x%08x", + sys_read32(grstctl_reg)); + return -ETIMEDOUT; + } + } + + LOG_DBG("DWC2 core reset done"); + + return 0; +} + +static int dwc2_hal_set_fifo_sizes(struct usb_dwc2_reg *const dwc2) +{ + const uint32_t ghwcfg2 = sys_read32((mem_addr_t)&dwc2->ghwcfg2); + const uint32_t ghwcfg3 = sys_read32((mem_addr_t)&dwc2->ghwcfg3); + /* TODO: Check the FIFO setting on hardware, that supports HS */ + const uint32_t nptx_largest = EPSIZE_BULK_FS / 4; + const uint32_t ptx_largest = 256 / 4; + const uint32_t dfifodepth = FIELD_GET(USB_DWC2_GHWCFG3_DFIFODEPTH_MASK, ghwcfg3); + const uint32_t numhstchnl = FIELD_GET(USB_DWC2_GHWCFG2_NUMHSTCHNL_MASK, ghwcfg2); + uint32_t fifo_available = dfifodepth - (numhstchnl + 1); + const uint16_t fifo_nptxfdep = 2 * nptx_largest; + const uint16_t fifo_rxfsiz = 2 * (ptx_largest + 2) + numhstchnl + 1; + const uint16_t fifo_ptxfsiz = fifo_available - (fifo_nptxfdep + fifo_rxfsiz); + uint32_t gdfifocfg; + uint32_t grxfsiz; + uint32_t gnptxfsiz; + uint32_t hptxfsiz; + int ret; + + LOG_DBG("Setting FIFO sizes: top=%u, nptx=%u, rx=%u, ptx=%u", + fifo_available * 4, fifo_nptxfdep * 4, fifo_rxfsiz * 4, fifo_ptxfsiz * 4); + + /* FIFO config */ + gdfifocfg = usb_dwc2_set_gdfifocfg_epinfobaseaddr(fifo_available); + gdfifocfg |= usb_dwc2_set_gdfifocfg_gdfifocfg(fifo_available); + sys_write32(gdfifocfg, (mem_addr_t)&dwc2->gdfifocfg); + + /* RX FIFO size */ + fifo_available -= fifo_rxfsiz; + grxfsiz = usb_dwc2_set_grxfsiz(fifo_rxfsiz); + sys_write32(grxfsiz, (mem_addr_t)&dwc2->grxfsiz); + + /* Non-periodic TX FIFO size */ + fifo_available -= fifo_nptxfdep; + gnptxfsiz = usb_dwc2_set_gnptxfsiz_nptxfdep(fifo_nptxfdep); + gnptxfsiz |= usb_dwc2_set_gnptxfsiz_nptxfstaddr(fifo_available); + sys_write32(gnptxfsiz, (mem_addr_t)&dwc2->gnptxfsiz); + + /* Periodic TX FIFO size */ + fifo_available -= fifo_ptxfsiz; + hptxfsiz = usb_dwc2_set_hptxfsiz_ptxfsize(fifo_ptxfsiz); + hptxfsiz |= fifo_available; + sys_write32(hptxfsiz, (mem_addr_t)&dwc2->hptxfsiz); + + /* Flush TX and RX FIFOs */ + ret = dwc2_hal_flush_tx_fifo(dwc2, MAX_CHANNELS); + if (ret != 0) { + return ret; + } + + return dwc2_hal_flush_rx_fifo(dwc2); +} + +static void uhc_dwc2_port_debounce_lock(const struct device *dev) +{ + const struct uhc_dwc2_config *const config = dev->config; + struct usb_dwc2_reg *const dwc2 = config->base; + struct uhc_dwc2_data *const priv = uhc_get_private(dev); + + /* Disable connection and disconnection interrupts to prevent spurious events */ + sys_clear_bits((mem_addr_t)&dwc2->gintmsk, USB_DWC2_GINTSTS_PRTINT | + USB_DWC2_GINTSTS_DISCONNINT); + /* Set the debounce lock flag */ + priv->debouncing = 1; +} + +static void uhc_dwc2_port_debounce_unlock(const struct device *dev) +{ + const struct uhc_dwc2_config *const config = dev->config; + struct usb_dwc2_reg *const dwc2 = config->base; + struct uhc_dwc2_data *const priv = uhc_get_private(dev); + + /* Clear the flag */ + priv->debouncing = 0; + /* Clear Connection and disconnection interrupt in case it triggered again */ + sys_set_bits((mem_addr_t)&dwc2->gintsts, USB_DWC2_GINTSTS_DISCONNINT); + /* Clear the PRTCONNDET interrupt by writing 1 to the corresponding bit (W1C logic) */ + sys_set_bits((mem_addr_t)&dwc2->hprt, USB_DWC2_HPRT_PRTCONNDET); + /* Re-enable the HPRT (connection) and disconnection interrupts */ + sys_set_bits((mem_addr_t)&dwc2->gintmsk, USB_DWC2_GINTSTS_PRTINT | + USB_DWC2_GINTSTS_DISCONNINT); +} + +static inline void uhc_dwc2_channel_process_control(struct uhc_dwc2_channel *channel) +{ + struct uhc_transfer *const xfer = channel->xfer; + const struct usb_setup_packet *setup = (const struct usb_setup_packet *)xfer->setup_pkt; + bool next_dir_is_in; + uint16_t size = 0; + uint16_t pkt_cnt; + uint8_t *dma_addr = NULL; + uint32_t hcchar; + uint32_t hctsiz; + uint16_t remaining; + uint16_t actual_len; + + if (xfer->stage == UHC_CONTROL_STAGE_SETUP) { + /* Just finished UHC_CONTROL_STAGE_SETUP */ + if (setup->wLength == 0) { + /* No data stage. Go strait to status */ + next_dir_is_in = true; + xfer->stage = UHC_CONTROL_STAGE_STATUS; + } else { + /* Data stage is present, go to data stage */ + next_dir_is_in = usb_reqtype_is_to_host(setup); + xfer->stage = UHC_CONTROL_STAGE_DATA; + + /* + * NOTE: Sizes + * for IN - wLength and net_buf_tailroom(xfer->buf) + * for OUT - xfer->buf->len + */ + if (next_dir_is_in) { + size = sys_le16_to_cpu(setup->wLength); + + LOG_DBG("Control DATA IN prog=%u, tailroom=%u", + size, net_buf_tailroom(xfer->buf)); + + dma_addr = net_buf_tail(xfer->buf); + } else { + size = xfer->buf->len; + + LOG_DBG("Control DATA OUT len=%u, tailroom=%u", + xfer->buf->len, net_buf_tailroom(xfer->buf)); + + LOG_HEXDUMP_DBG(xfer->buf->data, xfer->buf->len, + "Control DATA OUT:"); + + dma_addr = xfer->buf->data; + } + } + } else { + /* Finished UHC_CONTROL_STAGE_DATA */ + hctsiz = sys_read32((mem_addr_t)&channel->regs->hctsiz); + remaining = usb_dwc2_get_hctsiz_xfersize(hctsiz); + actual_len = channel->data_stage_size - remaining; + + if (usb_reqtype_is_to_host(setup)) { + net_buf_add(xfer->buf, actual_len); + + LOG_DBG("Control DATA IN completed, prog=%u, rem=%u, act=%u, tailroom=%u", + channel->data_stage_size, + remaining, + actual_len, + net_buf_tailroom(xfer->buf)); + + LOG_HEXDUMP_DBG(xfer->buf->data, xfer->buf->len, "Control DATA IN:"); + } else { + LOG_DBG("Control DATA OUT completed, prog=%u, rem=%u, act=%u", + channel->data_stage_size, remaining, actual_len); + } + /* Status stage is always the opposite direction of data stage */ + next_dir_is_in = !usb_reqtype_is_to_host(setup); + xfer->stage = UHC_CONTROL_STAGE_STATUS; + } + + /* Calculate new packet count */ + pkt_cnt = calc_packet_count(size, xfer->mps); + + if (next_dir_is_in) { + sys_set_bits((mem_addr_t)&channel->regs->hcchar, USB_DWC2_HCCHAR_EPDIR); + } else { + sys_clear_bits((mem_addr_t)&channel->regs->hcchar, USB_DWC2_HCCHAR_EPDIR); + } + + hctsiz = usb_dwc2_set_hctsiz_pid(USB_DWC2_HCTSIZ_PID_DATA1) | + usb_dwc2_set_hctsiz_pktcnt(pkt_cnt) | + usb_dwc2_set_hctsiz_xfersize(size); + + sys_write32(hctsiz, (mem_addr_t)&channel->regs->hctsiz); + sys_write32((uint32_t) dma_addr, (mem_addr_t)&channel->regs->hcdma); + + /* TODO: Configure split transaction if needed */ + + /* TODO: sync CACHE */ + + hcchar = sys_read32((mem_addr_t)&channel->regs->hcchar); + hcchar |= USB_DWC2_HCCHAR_CHENA; + hcchar &= ~USB_DWC2_HCCHAR_CHDIS; + sys_write32(hcchar, (mem_addr_t)&channel->regs->hcchar); +} + +static bool uhc_dwc2_xfer_is_done(const struct uhc_transfer *xfer) +{ + return xfer->type != USB_EP_TYPE_CONTROL || + xfer->stage == UHC_CONTROL_STAGE_STATUS; +} + +static uint32_t uhc_dwc2_handle_xfer_complete(struct uhc_dwc2_channel *channel, + uint32_t channel_events) +{ + if ((channel_events & BIT(UHC_DWC2_CHANNEL_EVENT_CPLT)) && + !uhc_dwc2_xfer_is_done(channel->xfer)) { + uhc_dwc2_channel_process_control(channel); + return 0; + } + + return channel_events; +} + +static uint32_t uhc_dwc2_channel_handle_in_bulk_control(struct uhc_dwc2_channel *channel, + uint32_t hcint) +{ + uint32_t channel_events = 0; + + if (hcint & USB_DWC2_HCINT_CHHLTD) { + if (hcint & (USB_DWC2_HCINT_XFERCOMPL | USB_DWC2_HCINT_STALL | + USB_DWC2_HCINT_BBLERR)) { + /* Transfer completed, STALLed or Babble Error */ + channel->error_count = 0; + /* TODO: Mask ACK */ + + if (hcint & USB_DWC2_HCINT_XFERCOMPL) { + channel_events |= BIT(UHC_DWC2_CHANNEL_EVENT_CPLT); + } else if (hcint & USB_DWC2_HCINT_STALL) { + channel_events |= BIT(UHC_DWC2_CHANNEL_EVENT_STALL); + } else { + channel_events |= BIT(UHC_DWC2_CHANNEL_EVENT_ERROR); + } + channel_events |= BIT(UHC_DWC2_CHANNEL_DO_RELEASE); + } else if (hcint & USB_DWC2_HCINT_XACTERR) { + channel->error_count++; + if (channel->error_count >= 3) { + LOG_ERR("IN channel%d error retry limit, HCINT 0x%08x", + channel->index, hcint); + channel_events |= BIT(UHC_DWC2_CHANNEL_DO_RELEASE); + channel_events |= BIT(UHC_DWC2_CHANNEL_EVENT_ERROR); + } else { + /* TODO: Unmask ACK, NAK, DTGERR */ + LOG_DBG("IN channel%d error, HCINT 0x%08x, retry %u", + channel->index, hcint, channel->error_count); + channel_events |= BIT(UHC_DWC2_CHANNEL_DO_REINIT); + } + } else { + /* TODO: Add handling for other cases */ + LOG_WRN("IN halted, unhandled HCINT 0x%08x", hcint); + } + } else if (hcint & (USB_DWC2_HCINT_ACK | USB_DWC2_HCINT_NAK | USB_DWC2_HCINT_DTGERR)) { + channel->error_count = 0; + /* TODO: Mask ACK, NAK, DTGERR */ + } else { + /* TODO: Add handling for other cases */ + LOG_WRN("IN not halted, unhandled HCINT 0x%08x", hcint); + } + + return uhc_dwc2_handle_xfer_complete(channel, channel_events); +} + +static inline uint32_t uhc_dwc2_channel_handle_out_bulk_control(struct uhc_dwc2_channel *channel, + uint32_t hcint) +{ + uint32_t channel_events = 0; + + if (hcint & USB_DWC2_HCINT_CHHLTD) { + if (hcint & (USB_DWC2_HCINT_XFERCOMPL | USB_DWC2_HCINT_STALL)) { + /* Transfer completed or STALLed*/ + channel->error_count = 1; + /* TODO: Mask ACK */ + + if (hcint & USB_DWC2_HCINT_XFERCOMPL) { + channel_events |= BIT(UHC_DWC2_CHANNEL_EVENT_CPLT); + } else { + channel_events |= BIT(UHC_DWC2_CHANNEL_EVENT_STALL); + } + channel_events |= BIT(UHC_DWC2_CHANNEL_DO_RELEASE); + } else if (hcint & USB_DWC2_HCINT_XACTERR) { + /* Transfer Error */ + if (hcint & (USB_DWC2_HCINT_NAK | USB_DWC2_HCINT_NYET | + USB_DWC2_HCINT_ACK)) { + channel->error_count = 1; + LOG_DBG("OUT channel%d NAK/NYET/ACK, HCINT 0x%08x, reint/rewind", + channel->index, hcint); + channel_events |= BIT(UHC_DWC2_CHANNEL_DO_REINIT); + channel_events |= BIT(UHC_DWC2_CHANNEL_DO_REWIND); + } else { + channel->error_count++; + if (channel->error_count >= 3) { + LOG_ERR("OUT channel%d error retry limit, HCINT 0x%08x", + channel->index, hcint); + channel_events |= BIT(UHC_DWC2_CHANNEL_DO_RELEASE); + channel_events |= BIT(UHC_DWC2_CHANNEL_EVENT_ERROR); + } else { + LOG_DBG("OUT channel%d error, HCINT 0x%08x, rewind %u", + channel->index, hcint, channel->error_count); + channel_events |= BIT(UHC_DWC2_CHANNEL_DO_REINIT); + channel_events |= BIT(UHC_DWC2_CHANNEL_DO_REWIND); + } + } + } else { + /* TODO: Add handling for other cases */ + LOG_WRN("OUT halted, unhandled HCINT 0x%08x", hcint); + } + } else if (hcint & USB_DWC2_HCINT_ACK) { + channel->error_count = 1; + /* TODO: Unmask ACK */ + } else { + /* TODO: Expand handling */ + LOG_WRN("OUT not halted, unhandled HCINT 0x%08x", hcint); + } + + return uhc_dwc2_handle_xfer_complete(channel, channel_events); +} + +static uint32_t uhc_dwc2_channel_handle_irq_events(struct uhc_dwc2_channel *channel) +{ + struct uhc_transfer *const xfer = channel->xfer; + uint32_t channel_events; + uint32_t hcint; + + hcint = sys_read32((mem_addr_t)&channel->regs->hcint); + sys_write32(hcint, (mem_addr_t)&channel->regs->hcint); + + LOG_DBG("HCINT 0x%08x", hcint); + + if ((hcint & USB_DWC2_HCINT_CPLT_BITS) != 0U || + channel->hcint_cplt_pending != 0) { + /* + * Completion bits might come earlier, than channel halted interrupt. + * Save the hcint complete mask as pending and wait for channel halted. + */ + if ((hcint & USB_DWC2_HCINT_CHHLTD) == 0U) { + /* Channel not halted yet, wait for channel halted */ + channel->hcint_cplt_pending |= hcint & USB_DWC2_HCINT_CPLT_BITS; + return 0U; + } + + /* Channel halted, restore pending bits */ + hcint |= channel->hcint_cplt_pending; + channel->hcint_cplt_pending = 0U; + } + + if (xfer->type == USB_EP_TYPE_BULK || xfer->type == USB_EP_TYPE_CONTROL) { + /* Bulk & Control */ + if (USB_EP_DIR_IS_IN(xfer->ep)) { + channel_events = uhc_dwc2_channel_handle_in_bulk_control(channel, hcint); + } else { + channel_events = uhc_dwc2_channel_handle_out_bulk_control(channel, hcint); + } + } else { + LOG_ERR("Unhandled transfer type %u, HCINT 0x%08x", xfer->type, hcint); + channel_events = 0; + } + + return channel_events; +} + +static inline bool uhc_dwc2_port_debounce(const struct device *dev, enum uhc_dwc2_event event) +{ + const struct uhc_dwc2_config *config = dev->config; + struct usb_dwc2_reg *dwc2 = config->base; + + /* Perform the debounce delay outside of the global lock */ + uhc_unlock_internal(dev); + + k_msleep(DEBOUNCE_DELAY_MS); + + uhc_lock_internal(dev, K_FOREVER); + + bool connected = ((sys_read32((mem_addr_t)&dwc2->hprt) & USB_DWC2_HPRT_PRTCONNSTS) != 0); + bool want_connected = (event == UHC_DWC2_EVENT_PORT_CONNECTION); + + uhc_dwc2_port_debounce_unlock(dev); + + /* True if stable state matches the event */ + return connected == want_connected; +} + +static inline void uhc_dwc2_port_enable(const struct device *dev) +{ + const struct uhc_dwc2_config *const config = dev->config; + struct usb_dwc2_reg *const dwc2 = config->base; + + sys_clear_bits((mem_addr_t)&dwc2->haintmsk, 0xFFFFFFFFUL); + sys_set_bits((mem_addr_t)&dwc2->gintmsk, USB_DWC2_GINTSTS_PRTINT | + USB_DWC2_GINTSTS_HCHINT); + dwc2_hal_set_power(dwc2, true); +} + +static inline void uhc_dwc2_port_disable(const struct device *dev) +{ + const struct uhc_dwc2_config *const config = dev->config; + struct usb_dwc2_reg *const dwc2 = config->base; + + sys_clear_bits((mem_addr_t)&dwc2->haintmsk, 0xFFFFFFFFUL); + sys_clear_bits((mem_addr_t)&dwc2->gintmsk, USB_DWC2_GINTSTS_PRTINT | + USB_DWC2_GINTSTS_HCHINT); + dwc2_hal_set_power(dwc2, false); +} + +static inline int uhc_dwc2_soft_reset(const struct device *dev) +{ + const struct uhc_dwc2_config *const config = dev->config; + struct usb_dwc2_reg *const dwc2 = config->base; + int ret; + + /* TODO: Check that port has no ongoing transfers */ + + /* Disable Global IRQ */ + config->irq_disable_func(dev); + + /* Reset the core */ + ret = dwc2_core_soft_reset(dev); + if (ret != 0) { + LOG_ERR("Failed to perform soft reset: %d", ret); + return ret; + } + + /* Re-config after reset */ + dwc2_hal_init_gahbcfg(dwc2); + + /* Enable Global IRQ */ + config->irq_enable_func(dev); + + return ret; +} + +static int uhc_dwc2_channel_claim(const struct device *const dev, + struct uhc_transfer *const xfer, + struct uhc_dwc2_channel **const channel_p) +{ + struct uhc_dwc2_data *const priv = uhc_get_private(dev); + /* TODO: select non-claimed channel, use channel 0 for now */ + uint8_t idx = 0; + struct uhc_dwc2_channel *const channel = &priv->channels[idx]; + + LOG_DBG("Claimed channel%d, configuring it with xfer %p, channel %p", + idx, (void *)xfer, (void *)channel); + + /* Save channel characteristics of the underlying channel */ + channel->xfer = xfer; + + *channel_p = channel; + + return 0; +} + +static int uhc_dwc2_channel_configure(const struct device *const dev, + struct uhc_dwc2_channel *const channel) +{ + const struct uhc_dwc2_config *const config = dev->config; + struct usb_dwc2_reg *const dwc2 = config->base; + struct uhc_transfer *const xfer = channel->xfer; + struct usb_device *const udev = xfer->udev; + uint32_t hcint; + uint32_t hcintmsk; + uint32_t hcchar; + + /* Clear the interrupt bits by writing them back */ + hcint = sys_read32((mem_addr_t)&channel->regs->hcint); + sys_write32(hcint, (mem_addr_t)&channel->regs->hcint); + + /* Enable channel interrupt in the core */ + sys_set_bits((mem_addr_t)&dwc2->haintmsk, BIT(channel->index)); + + hcintmsk = sys_read32((mem_addr_t)&channel->regs->hcintmsk); + + /* Enable transfer complete and channel halted interrupts */ + hcintmsk |= USB_DWC2_HCINT_XFERCOMPL; + hcintmsk |= USB_DWC2_HCINT_CHHLTD; + /* Enable error interrupts */ + hcintmsk |= USB_DWC2_HCINT_ERRORS; + hcintmsk |= USB_DWC2_HCINT_STALL; + sys_write32(hcintmsk, (mem_addr_t)&channel->regs->hcintmsk); + + /* Configure the channel main properties */ + hcchar = usb_dwc2_set_hcchar_mps(xfer->mps); + hcchar |= usb_dwc2_set_hcchar_epnum(USB_EP_GET_IDX(xfer->ep)); + hcchar |= usb_dwc2_set_hcchar_eptype(xfer->type); + hcchar |= usb_dwc2_set_hcchar_ec(1UL /* TODO: ep_config->mult */); + hcchar |= usb_dwc2_set_hcchar_devaddr(udev->addr); + + if (USB_EP_DIR_IS_IN(xfer->ep)) { + hcchar |= USB_DWC2_HCCHAR_EPDIR; + } + + if (false /* TODO: Support Hubs channel->ls_via_fs_hub */) { + hcchar |= USB_DWC2_HCCHAR_LSPDDEV; + } + + if (xfer->type == USB_EP_TYPE_INTERRUPT) { + hcchar |= USB_DWC2_HCCHAR_ODDFRM; + } + + sys_write32(hcchar, (mem_addr_t)&channel->regs->hcchar); + + return 0; +} + +static int uhc_dwc2_channel_release(const struct device *dev, + struct uhc_dwc2_channel *channel) +{ + const struct uhc_dwc2_config *const config = dev->config; + struct usb_dwc2_reg *const dwc2 = config->base; + + sys_clear_bits((mem_addr_t)&dwc2->haintmsk, (1 << channel->index)); + + /* Release channel */ + channel->xfer = NULL; + + LOG_DBG("Released channel%d", channel->index); + + return 0; +} + +static void uhc_dwc2_channel_reinit(struct uhc_dwc2_channel *channel) +{ + uint32_t hcchar; + + /* Clear old channel interrupts before re-enabling. */ + sys_write32(0xFFFFFFFFU, (mem_addr_t)&channel->regs->hcint); + + /* + * Re-enable the channel using the already-programmed HCTSIZ/HCDMA. + * For DMA mode, HCTSIZ/HCDMA may already contain the remaining transfer. + */ + hcchar = sys_read32((mem_addr_t)&channel->regs->hcchar); + hcchar &= ~USB_DWC2_HCCHAR_CHDIS; + hcchar |= USB_DWC2_HCCHAR_CHENA; + sys_write32(hcchar, (mem_addr_t)&channel->regs->hcchar); +} + +static int uhc_dwc2_channel_start_control_xfer(struct uhc_dwc2_channel *channel) +{ + struct uhc_transfer *const xfer = channel->xfer; + const struct usb_setup_packet *setup = (const struct usb_setup_packet *)xfer->setup_pkt; + uint16_t pkt_cnt; + uint32_t hctsiz; + uint32_t hcint; + uint32_t hcchar; + + xfer->stage = UHC_CONTROL_STAGE_SETUP; + + /* Save information about control transfer by analyzing the setup packet */ + channel->data_stage_size = setup->wLength; + + /* Control stage is always OUT */ + sys_clear_bits((mem_addr_t)&channel->regs->hcchar, USB_DWC2_HCCHAR_EPDIR); + + pkt_cnt = calc_packet_count(sizeof(struct usb_setup_packet), xfer->mps); + hctsiz = usb_dwc2_set_hctsiz_pid(USB_DWC2_HCTSIZ_PID_SETUP) | + usb_dwc2_set_hctsiz_pktcnt(pkt_cnt) | + usb_dwc2_set_hctsiz_xfersize(sizeof(struct usb_setup_packet)); + + LOG_HEXDUMP_DBG(setup, 8, "SETUP"); + + sys_write32(hctsiz, (mem_addr_t)&channel->regs->hctsiz); + sys_write32((uint32_t)xfer->setup_pkt, (mem_addr_t)&channel->regs->hcdma); + + /* TODO: Configure split transaction if needed */ + + hcint = sys_read32((mem_addr_t)&channel->regs->hcint); + sys_write32(hcint, (mem_addr_t)&channel->regs->hcint); + + /* TODO: sync CACHE */ + + hcchar = sys_read32((mem_addr_t)&channel->regs->hcchar); + hcchar |= USB_DWC2_HCCHAR_CHENA; + hcchar &= ~USB_DWC2_HCCHAR_CHDIS; + sys_write32(hcchar, (mem_addr_t)&channel->regs->hcchar); + + return 0; +} + +static inline void uhc_dwc2_submit_new_device(const struct device *dev) +{ + struct uhc_dwc2_data *const priv = uhc_get_private(dev); + const struct uhc_dwc2_config *const config = dev->config; + struct usb_dwc2_reg *const dwc2 = config->base; + uint32_t hprt = sys_read32((mem_addr_t)&dwc2->hprt); + + switch (usb_dwc2_get_hprt_prtspd(hprt)) { + case USB_DWC2_HPRT_PRTSPD_HIGH: + uhc_submit_event(dev, UHC_EVT_DEV_CONNECTED_HS, 0); + break; + case USB_DWC2_HPRT_PRTSPD_FULL: + uhc_submit_event(dev, UHC_EVT_DEV_CONNECTED_FS, 0); + break; + case USB_DWC2_HPRT_PRTSPD_LOW: + uhc_submit_event(dev, UHC_EVT_DEV_CONNECTED_LS, 0); + break; + default: + LOG_WRN("Port has unexpected speed, submit as full speed"); + uhc_submit_event(dev, UHC_EVT_DEV_CONNECTED_FS, 0); + break; + } + + priv->has_device = 1; +} + +static inline void uhc_dwc2_submit_dev_gone(const struct device *dev) +{ + struct uhc_dwc2_data *const priv = uhc_get_private(dev); + + if (!priv->has_device) { + return; + } + + uhc_submit_event(dev, UHC_EVT_DEV_REMOVED, 0); + priv->has_device = 0; +} + +static int uhc_dwc2_validate_control_xfer(const struct uhc_transfer *xfer) +{ + const struct usb_setup_packet *setup; + uint16_t wLength; + + /* Control transfer should always have control type */ + if (xfer->type != USB_EP_TYPE_CONTROL) { + LOG_ERR("Channel type %d isn't supported yet", xfer->type); + return -EINVAL; + } + + /* Only via EP0 */ + if (USB_EP_GET_IDX(xfer->ep) != 0) { + LOG_ERR("Control transfer must use endpoint 0"); + return -EINVAL; + } + + /* Has correct MPS */ + if ((xfer->mps != 8) && (xfer->mps != 16) && + (xfer->mps != 32) && (xfer->mps != 64)) { + LOG_ERR("Invalid control MPS %u", xfer->mps); + return -EINVAL; + } + + /* For DMA, ponter should be DMA aligned */ + if (((uintptr_t)xfer->setup_pkt % 4) != 0) { + LOG_ERR("Setup packet address %p is not 4-byte aligned", + xfer->setup_pkt); + return -EINVAL; + } + + setup = (const struct usb_setup_packet *)xfer->setup_pkt; + wLength = sys_le16_to_cpu(setup->wLength); + + /* For data stage, xfer object should hold the buffer */ + if ((wLength != 0) && (xfer->buf == NULL)) { + LOG_ERR("Control transfer with data stage requires buffer"); + return -EINVAL; + } + + if (usb_reqtype_is_to_host(setup)) { + /* Transfer has DATA IN step, so the buffer size should be enough */ + if (wLength > net_buf_tailroom(xfer->buf)) { + LOG_ERR("Control IN buffer is too small: wLength=%u tailroom=%u", + wLength, net_buf_tailroom(xfer->buf)); + return -EINVAL; + } + /* For DMA, buffer pointer should be also aligned */ + if (((uintptr_t)net_buf_tail(xfer->buf) % 4) != 0) { + LOG_WRN("Control IN buffer tail %p is not 4-byte aligned", + net_buf_tail(xfer->buf)); + return -EINVAL; + } + } else { + /* Transfer has DATA OUT step, wLength != 0 but buffer is absent or too small */ + if ((wLength != 0) && + (xfer->buf != 0) && + (wLength > xfer->buf->len)) { + LOG_ERR("Control OUT buffer is too small or absent: wLength=%u", wLength); + return -EINVAL; + } + /* For DMA, buffer pointer should be also aligned */ + if ((xfer->buf != 0) && ((uintptr_t)xfer->buf->data % 4) != 0) { + LOG_WRN("Control OUT buffer data %p is not 4-byte aligned", + xfer->buf->data); + return -EINVAL; + } + } + + return 0; +} + +static int uhc_dwc2_submit_xfer(const struct device *const dev, struct uhc_transfer *const xfer) +{ + struct uhc_dwc2_channel *channel = NULL; + int ret; + + LOG_DBG("addr=%u, ep=%02Xh, mps=%d, int=%d, start_frame=%d, stage=%d, no_status=%d", + xfer->udev->addr, xfer->ep, xfer->mps, xfer->interval, + xfer->start_frame, xfer->stage, xfer->no_status); + + ret = uhc_dwc2_validate_control_xfer(xfer); + if (ret != 0) { + LOG_ERR("Invalid xfer for control transfer: %d", ret); + return ret; + } + + ret = uhc_dwc2_channel_claim(dev, xfer, &channel); + if (ret != 0) { + LOG_ERR("Failed to claim channel: %d", ret); + return ret; + } + + ret = uhc_dwc2_channel_configure(dev, channel); + if (ret != 0) { + LOG_ERR("Failed to configure channel: %d", ret); + uhc_dwc2_channel_release(dev, channel); + return ret; + } + + return uhc_dwc2_channel_start_control_xfer(channel); +} + +static void uhc_dwc2_port_handle_events(const struct device *dev, uint32_t event_mask) +{ + const struct uhc_dwc2_config *const config = dev->config; + struct usb_dwc2_reg *const dwc2 = config->base; + + LOG_DBG("Port events: %08Xh", event_mask); + + if (event_mask & BIT(UHC_DWC2_EVENT_PORT_CONNECTION)) { + /* Port connected */ + LOG_DBG("Port connected"); + /* Debounce port connection */ + if (uhc_dwc2_port_debounce(dev, UHC_DWC2_EVENT_PORT_CONNECTION)) { + /* Notify the higher logic about the new device */ + uhc_dwc2_submit_new_device(dev); + } else { + /* TODO: Implement handling */ + LOG_ERR("Port changed during debouncing connect"); + } + } + + if (event_mask & BIT(UHC_DWC2_EVENT_PORT_DISCONNECTION)) { + /* Port disconnected */ + /* Debounce port disconnection */ + if (uhc_dwc2_port_debounce(dev, UHC_DWC2_EVENT_PORT_DISCONNECTION)) { + LOG_DBG("Port disconnected"); + /* Notify upper layer */ + uhc_dwc2_submit_dev_gone(dev); + /* Reset the controller to handle new connection */ + uhc_dwc2_soft_reset(dev); + /* Prepare for device connection */ + uhc_dwc2_port_enable(dev); + } else { + /* TODO: Implement handling */ + LOG_ERR("Port changed during debouncing disconnect"); + } + } + + if (event_mask & BIT(UHC_DWC2_EVENT_PORT_ERROR)) { + LOG_DBG("Port error"); + /* Notify upper layer */ + uhc_dwc2_submit_dev_gone(dev); + /* TODO: recover from the error */ + + /* Reset the controller to handle new connection */ + uhc_dwc2_soft_reset(dev); + /* Prepare for device connection */ + uhc_dwc2_port_enable(dev); + } + + if (event_mask & BIT(UHC_DWC2_EVENT_PORT_OVERCURRENT)) { + LOG_ERR("Port overcurrent"); + /* TODO: Handle overcurrent */ + LOG_WRN("Handle overcurrent is not implemented"); + /* Just power off the port via registers */ + dwc2_hal_set_power(dwc2, false); + } +} + +static void uhc_dwc2_channel_handle_events(const struct device *dev, + struct uhc_dwc2_channel *channel) +{ + uint32_t events = (uint32_t)atomic_set(&channel->events, 0); + struct uhc_transfer *const xfer = channel->xfer; + const struct usb_setup_packet *setup = (const struct usb_setup_packet *)xfer->setup_pkt; + int err = -EIO; + + __ASSERT_NO_MSG(xfer != NULL); + + LOG_DBG("Thread channel%d events: %08Xh", channel->index, events); + + /* ERROR, STALL and COMPLETE are mutually exclusive */ + + if (events & BIT(UHC_DWC2_CHANNEL_EVENT_ERROR)) { + err = -EIO; + } else if (events & BIT(UHC_DWC2_CHANNEL_EVENT_STALL)) { + err = -EPIPE; + } else if (events & BIT(UHC_DWC2_CHANNEL_EVENT_CPLT)) { + if (setup->bRequest == USB_SREQ_SET_ADDRESS) { + /* When new address is processing, some devices require a delay. */ + k_msleep(SET_ADDR_DELAY_MS); + } + err = 0; + } + + if (events & BIT(UHC_DWC2_CHANNEL_DO_REWIND)) { + /* TODO: Implement rewind xfer */ + LOG_WRN("DO_REWIND not implemented yet"); + } + + if (events & BIT(UHC_DWC2_CHANNEL_DO_REINIT)) { + uhc_dwc2_channel_reinit(channel); + } + + /* TODO: DO_RELEASE is mutually exclusive with DO_REWIND and DO_INIT */ + + if (events & BIT(UHC_DWC2_CHANNEL_DO_RELEASE)) { + uhc_dwc2_channel_release(dev, channel); + uhc_xfer_return(dev, xfer, err); + } +} + +static void uhc_dwc2_isr_handler(const struct device *dev) +{ + const struct uhc_dwc2_config *const config = dev->config; + struct uhc_dwc2_data *const priv = uhc_get_private(dev); + struct usb_dwc2_reg *const dwc2 = config->base; + uint32_t gintsts; + uint32_t hprt = 0; + uint32_t ch_events = 0; + unsigned int ch_index; + + /* Read and clear core interrupt status */ + gintsts = sys_read32((mem_addr_t)&dwc2->gintsts); + sys_write32(gintsts, (mem_addr_t)&dwc2->gintsts); + + LOG_DBG("GINTSTS=0x%08X", gintsts); + + /* Handle disconnect first */ + if (gintsts & USB_DWC2_GINTSTS_DISCONNINT) { + /* Port disconnected */ + uhc_dwc2_port_debounce_lock(dev); + k_event_post(&priv->events, BIT(UHC_DWC2_EVENT_PORT_DISCONNECTION)); + } + + /* To have better throughput, handle channels right after disconnection */ + if (gintsts & USB_DWC2_GINTSTS_HCHINT) { + /* Handle pending channel event */ + uint32_t haint = sys_read32((mem_addr_t)&dwc2->haint); + + LOG_DBG("HAINT 0x%08x", haint); + + /* Channel count might be configured via menuconfig, trim it if needed */ + if (haint & ~BIT_MASK(MAX_CHANNELS)) { + haint &= BIT_MASK(MAX_CHANNELS); + LOG_WRN("HAINT trimmed to valid channel mask 0x%08X", haint); + } + + while (haint != 0U) { + /* Channel index = channel number - 1 */ + ch_index = __builtin_ffs(haint) - 1U; + + __ASSERT_NO_MSG(priv->channels[ch_index].index == ch_index); + + ch_events = uhc_dwc2_channel_handle_irq_events(&priv->channels[ch_index]); + if (ch_events) { + atomic_or(&priv->channels[ch_index].events, ch_events); + k_event_set(&priv->events, + BIT(UHC_DWC2_EVENT_PORT_PEND_CHANNEL + ch_index)); + } + haint &= ~BIT(ch_index); + } + } + + if (gintsts & USB_DWC2_GINTSTS_PRTINT) { + /* Port interrupt, read and clear all, except port enable */ + hprt = sys_read32((mem_addr_t)&dwc2->hprt); + sys_write32(hprt & ~USB_DWC2_HPRT_PRTENA, (mem_addr_t)&dwc2->hprt); + + LOG_DBG("HPRT 0x%08x", hprt); + + /* Handle port overcurrent as it is a failure state */ + if (hprt & USB_DWC2_HPRT_PRTOVRCURRCHNG) { + /* TODO: Overcurrent or overcurrent clear? */ + k_event_post(&priv->events, BIT(UHC_DWC2_EVENT_PORT_OVERCURRENT)); + } + + /* Handle port change */ + if (hprt & USB_DWC2_HPRT_PRTENCHNG) { + if (hprt & USB_DWC2_HPRT_PRTENA) { + /* Configure Host timings */ + dwc2_hal_config_timings(dwc2); + /* Give port enable semaphore to unlock bus reset sequence */ + k_sem_give(&priv->sem_port_enabled); + } else { + /* Host port has been disabled */ + k_event_post(&priv->events, BIT(UHC_DWC2_EVENT_PORT_DISABLED)); + } + } + + /* Handle port connection */ + if (hprt & USB_DWC2_HPRT_PRTCONNDET && !priv->debouncing) { + /* Port connected */ + uhc_dwc2_port_debounce_lock(dev); + k_event_post(&priv->events, BIT(UHC_DWC2_EVENT_PORT_CONNECTION)); + } + } + + (void)uhc_dwc2_quirk_irq_clear(dev); +} + +static void uhc_dwc2_thread(void *arg0, void *arg1, void *arg2) +{ + const struct device *const dev = (const struct device *)arg0; + struct uhc_dwc2_data *const priv = uhc_get_private(dev); + uint32_t event_mask; + + while (true) { + event_mask = k_event_wait_safe(&priv->events, UINT32_MAX, false, K_FOREVER); + + uhc_lock_internal(dev, K_FOREVER); + + /* Handle port events */ + uhc_dwc2_port_handle_events(dev, event_mask); + + /* Interate channels events */ + for (uint32_t index = 0; index < MAX_CHANNELS; index++) { + if (event_mask & BIT(UHC_DWC2_EVENT_PORT_PEND_CHANNEL + index)) { + uhc_dwc2_channel_handle_events(dev, &priv->channels[index]); + } + } + + uhc_unlock_internal(dev); + } +} + +static int uhc_dwc2_lock(const struct device *const dev) +{ + struct uhc_data *data = dev->data; + + return k_mutex_lock(&data->mutex, K_FOREVER); +} + +static int uhc_dwc2_unlock(const struct device *const dev) +{ + struct uhc_data *data = dev->data; + + return k_mutex_unlock(&data->mutex); +} + +static int uhc_dwc2_sof_enable(const struct device *const dev) +{ + LOG_WRN("%s has not been implemented", __func__); + + return -ENOSYS; +} + +static int uhc_dwc2_bus_suspend(const struct device *const dev) +{ + LOG_WRN("%s has not been implemented", __func__); + + return -ENOSYS; +} + +static int uhc_dwc2_bus_reset(const struct device *const dev) +{ + const struct uhc_dwc2_config *const config = dev->config; + struct uhc_dwc2_data *const priv = uhc_get_private(dev); + struct usb_dwc2_reg *const dwc2 = config->base; + int ret; + + /* Reset the port */ + dwc2_hal_set_reset(dwc2, true); + + /* Hold the bus in the reset state */ + k_msleep(RESET_HOLD_MS); + + /* Return the bus to the idle state. Port enabled event should occur */ + dwc2_hal_set_reset(dwc2, false); + + /* Give 10 ms to the port to become enabled */ + ret = k_sem_take(&priv->sem_port_enabled, K_MSEC(10)); + if (ret != 0) { + /* Port didn't change its state to ENABLED */ + LOG_ERR("Port wasn't enabled after reset"); + return ret; + } + + /* Give the device some extra time to recover after port reset */ + k_msleep(RESET_RECOVERY_MS); + + /* FIFO */ + ret = dwc2_hal_set_fifo_sizes(dwc2); + if (ret != 0) { + LOG_ERR("Unable to configure FIFO"); + return ret; + } + + /* TODO: set frame list for the ISOC/INTR xfer */ + /* TODO: enable periodic transfer */ + + uhc_submit_event(dev, UHC_EVT_RESETED, 0); + return 0; +} + +static int uhc_dwc2_bus_resume(const struct device *const dev) +{ + LOG_WRN("%s has not been implemented", __func__); + + return -ENOSYS; +} + +static int uhc_dwc2_enqueue(const struct device *const dev, struct uhc_transfer *const xfer) +{ + int ret; + + (void)uhc_xfer_append(dev, xfer); + + uhc_lock_internal(dev, K_FOREVER); + + ret = uhc_dwc2_submit_xfer(dev, xfer); + + uhc_unlock_internal(dev); + + return ret; +} + +static int uhc_dwc2_dequeue(const struct device *const dev, struct uhc_transfer *const xfer) +{ + LOG_WRN("%s has not been implemented", __func__); + + return -ENOSYS; +} + +static int uhc_dwc2_preinit(const struct device *const dev) +{ + const struct uhc_dwc2_config *const config = dev->config; + struct uhc_dwc2_data *const priv = uhc_get_private(dev); + struct usb_dwc2_reg *const dwc2 = config->base; + int ret; + + for (uint32_t idx = 0; idx < MAX_CHANNELS; idx++) { + priv->channels[idx].index = idx; + priv->channels[idx].regs = + (struct usb_dwc2_host_chan *)((mem_addr_t)dwc2 + USB_DWC2_HCCHAR(idx)); + } + + k_thread_create(&priv->thread, + config->stack, + config->stack_size, + uhc_dwc2_thread, + (void *)dev, NULL, NULL, + K_PRIO_COOP(CONFIG_UHC_DWC2_THREAD_PRIORITY), + K_ESSENTIAL, + K_NO_WAIT); + k_thread_name_set(&priv->thread, dev->name); + + ret = uhc_dwc2_quirk_post_preinit(dev); + if (ret != 0) { + LOG_ERR("Quirk post preinit failed %d", ret); + return ret; + } + + return 0; +} + +static int uhc_dwc2_init(const struct device *const dev) +{ + const struct uhc_dwc2_config *const config = dev->config; + struct usb_dwc2_reg *const dwc2 = config->base; + uint32_t hcfg; + uint32_t hfir; + uint32_t gsnpsid; + uint32_t gintsts; + int ret; + + ret = uhc_dwc2_quirk_pre_init(dev); + if (ret != 0) { + LOG_ERR("Quirk pre init failed %d", ret); + return ret; + } + + gsnpsid = sys_read32((mem_addr_t)&dwc2->gsnpsid); + if (gsnpsid == 0) { + LOG_ERR("Unexpected GSNPSID 0x%08x", gsnpsid); + return -ENOTSUP; + } + + LOG_DBG("DWC2 Core rev. %04x", usb_dwc2_get_gsnpsid_rev(gsnpsid)); + + ret = dwc2_core_soft_reset(dev); + if (ret != 0) { + LOG_ERR("DWC2 core reset failed after PHY init: %d", ret); + return ret; + } + + ret = dwc2_hal_init_gusbcfg(dwc2); + if (ret != 0) { + LOG_ERR("Unable to configure USB global register"); + return ret; + } + + ret = dwc2_hal_init_gahbcfg(dwc2); + if (ret != 0) { + /* TODO: Implement Slave Mode */ + LOG_WRN("DMA isn't supported, but Slave Mode isn't implemented yet"); + return ret; + } + + hcfg = sys_read32((mem_addr_t)&dwc2->hcfg); + /* Only Buffer DMA for now */ + hcfg &= ~USB_DWC2_HCFG_DESCDMA; + /* Work on maximum supported speed */ + hcfg &= ~USB_DWC2_HCFG_FSLSSUPP; + /* Disable periodic scheduling, will enable after port is enabled */ + hcfg &= ~USB_DWC2_HCFG_PERSCHEDENA; + + sys_write32(hcfg, (mem_addr_t)&dwc2->hcfg); + + hfir = sys_read32((mem_addr_t)&dwc2->hfir); + /* Enable dynamic loading if needed */ + if (usb_dwc2_get_gsnpsid_rev(gsnpsid) > + usb_dwc2_get_gsnpsid_rev(USB_DWC2_GSNPSID_REV_2_92A)) { + hfir |= USB_DWC2_HFIR_HFIRRLDCTRL; + } else { + hfir &= ~USB_DWC2_HFIR_HFIRRLDCTRL; + } + sys_write32(hfir, (mem_addr_t)&dwc2->hfir); + + /* Clear status */ + gintsts = sys_read32((mem_addr_t)&dwc2->gintsts); + sys_write32(gintsts, (mem_addr_t)&dwc2->gintsts); + + return ret; +} + +static int uhc_dwc2_enable(const struct device *const dev) +{ + int ret; + + ret = uhc_dwc2_quirk_pre_enable(dev); + if (ret != 0) { + LOG_ERR("Quirk pre enable failed %d", ret); + return ret; + } + + uhc_dwc2_soft_reset(dev); + + uhc_dwc2_port_enable(dev); + + ret = uhc_dwc2_quirk_post_enable(dev); + if (ret != 0) { + LOG_ERR("Quirk post enable failed %d", ret); + return ret; + } + + return 0; +} + +static int uhc_dwc2_disable(const struct device *const dev) +{ + const struct uhc_dwc2_config *const config = dev->config; + int ret; + /* TODO: Check ongoing transfer? */ + + uhc_dwc2_submit_dev_gone(dev); + + config->irq_disable_func(dev); + + uhc_dwc2_port_disable(dev); + + ret = uhc_dwc2_quirk_disable(dev); + if (ret != 0) { + LOG_ERR("Quirk disable failed %d", ret); + return ret; + } + + return 0; +} + +static int uhc_dwc2_shutdown(const struct device *const dev) +{ + int ret; + + ret = uhc_dwc2_quirk_shutdown(dev); + if (ret) { + LOG_ERR("Quirk shutdown failed %d", ret); + return ret; + } + + return 0; +} + +/* + * Device Definition and Initialization + */ +static const struct uhc_api uhc_dwc2_api = { + /* Common */ + .lock = uhc_dwc2_lock, + .unlock = uhc_dwc2_unlock, + .init = uhc_dwc2_init, + .enable = uhc_dwc2_enable, + .disable = uhc_dwc2_disable, + .shutdown = uhc_dwc2_shutdown, + /* Bus related */ + .bus_reset = uhc_dwc2_bus_reset, + .sof_enable = uhc_dwc2_sof_enable, + .bus_suspend = uhc_dwc2_bus_suspend, + .bus_resume = uhc_dwc2_bus_resume, + /* EP related */ + .ep_enqueue = uhc_dwc2_enqueue, + .ep_dequeue = uhc_dwc2_dequeue, +}; + +#define UHC_DWC2_DT_INST_REG_ADDR(n) \ + COND_CODE_1(DT_NUM_REGS(DT_DRV_INST(n)), \ + (DT_INST_REG_ADDR(n)), \ + (DT_INST_REG_ADDR_BY_NAME(n, core))) + +#if !defined(UHC_DWC2_IRQ_DT_INST_DEFINE) +#define UHC_DWC2_IRQ_FLAGS_TYPE0(n) 0 +#define UHC_DWC2_IRQ_FLAGS_TYPE1(n) DT_INST_IRQ(n, type) +#define DW_IRQ_FLAGS(n) \ + _CONCAT(UHC_DWC2_IRQ_FLAGS_TYPE, DT_INST_IRQ_HAS_CELL(n, type))(n) + +#define UHC_DWC2_IRQ_DT_INST_DEFINE(n) \ + static void uhc_dwc2_irq_enable_func_##n(const struct device *dev) \ + { \ + IRQ_CONNECT(DT_INST_IRQN(n), \ + DT_INST_IRQ(n, priority), \ + uhc_dwc2_isr_handler, \ + DEVICE_DT_INST_GET(n), \ + DW_IRQ_FLAGS(n)); \ + \ + irq_enable(DT_INST_IRQN(n)); \ + } \ + \ + static void uhc_dwc2_irq_disable_func_##n(const struct device *dev) \ + { \ + irq_disable(DT_INST_IRQN(n)); \ + } +#endif + +/* Multi-instance device definition for DWC2 host controller */ +#define UHC_DWC2_DEVICE_DEFINE(n) \ + \ + K_THREAD_STACK_DEFINE(uhc_dwc2_stack_##n, \ + CONFIG_UHC_DWC2_STACK_SIZE); \ + \ + UHC_DWC2_IRQ_DT_INST_DEFINE(n) \ + \ + static struct uhc_dwc2_data uhc_dwc2_priv_##n = { \ + .events = Z_EVENT_INITIALIZER(uhc_dwc2_priv_##n.events), \ + .sem_port_enabled = \ + Z_SEM_INITIALIZER(uhc_dwc2_priv_##n.sem_port_enabled, \ + 0, \ + 1), \ + }; \ + \ + static struct uhc_data uhc_data_##n = { \ + .mutex = Z_MUTEX_INITIALIZER(uhc_data_##n.mutex), \ + .priv = &uhc_dwc2_priv_##n, \ + }; \ + \ + static const struct uhc_dwc2_config uhc_dwc2_config_##n = { \ + .base = (struct usb_dwc2_reg *)UHC_DWC2_DT_INST_REG_ADDR(n), \ + .quirks = UHC_DWC2_VENDOR_QUIRK_GET(n), \ + .quirk_data = &uhc_dwc2_quirk_data_##n, \ + .quirk_config = &uhc_dwc2_quirk_config_##n, \ + .stack = uhc_dwc2_stack_##n, \ + .stack_size = K_THREAD_STACK_SIZEOF(uhc_dwc2_stack_##n), \ + .irq_enable_func = uhc_dwc2_irq_enable_func_##n, \ + .irq_disable_func = uhc_dwc2_irq_disable_func_##n, \ + }; \ + \ + DEVICE_DT_INST_DEFINE(n, uhc_dwc2_preinit, NULL, \ + &uhc_data_##n, &uhc_dwc2_config_##n, \ + POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEVICE, \ + &uhc_dwc2_api); + +DT_INST_FOREACH_STATUS_OKAY(UHC_DWC2_DEVICE_DEFINE) diff --git a/drivers/usb/uhc/uhc_dwc2.h b/drivers/usb/uhc/uhc_dwc2.h new file mode 100644 index 0000000000000..7b8ee1c0a417d --- /dev/null +++ b/drivers/usb/uhc/uhc_dwc2.h @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2026 Roman Leonov + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_DRIVERS_USB_UHC_DWC2_H +#define ZEPHYR_DRIVERS_USB_UHC_DWC2_H + +#include +#include +#include +#include + +/* Vendor quirks per driver instance */ +struct uhc_dwc2_vendor_quirks { + /* Called on uhc_dwc2_preinit() after the thread is initialized */ + int (*post_preinit)(const struct device *dev); + /* Called at the beginning of uhc_dwc2_init(), before controller is initialized */ + int (*pre_init)(const struct device *dev); + /* Called on uhc_dwc2_enable() before enable the port power */ + int (*pre_enable)(const struct device *dev); + /* Called on uhc_dwc2_enable() after enable the port power */ + int (*post_enable)(const struct device *dev); + /* Called at the end of uhc_dwc2_disable() */ + int (*disable)(const struct device *dev); + /* Called at the end of uhc_dwc2_shutdown() */ + int (*shutdown)(const struct device *dev); + /* Called at the end of IRQ handling */ + int (*irq_clear)(const struct device *dev); + /* Called while waiting for bits that require PHY to be clocked */ + int (*is_phy_clk_off)(const struct device *dev); +}; + +/* Driver configuration per instance */ +struct uhc_dwc2_config { + /* Pointer to base address of DWC_OTG registers */ + struct usb_dwc2_reg *const base; + /* Vendor specific quirks */ + const struct uhc_dwc2_vendor_quirks *const quirks; + void *quirk_data; + const void *quirk_config; + /* Pointer to the stack used by the driver thread */ + k_thread_stack_t *stack; + /* Size of the stack used by the driver thread */ + size_t stack_size; + + void (*irq_enable_func)(const struct device *dev); + void (*irq_disable_func)(const struct device *dev); +}; + +#define UHC_DWC2_QUIRK_CONFIG(dev) \ + (((const struct uhc_dwc2_config *)dev->config)->quirk_config) + +#define UHC_DWC2_QUIRK_DATA(dev) \ + (((const struct uhc_dwc2_config *)dev->config)->quirk_data) + +#if DT_HAS_COMPAT_STATUS_OKAY(espressif_esp32_usb_otg) +#include "uhc_dwc2_esp32_usb_otg.h" +#endif + +#if DT_HAS_COMPAT_STATUS_OKAY(nordic_nrf_usbhs_nrf54l) +#include "uhc_dwc2_nrf_usbhs_nrf54l.h" +#endif + +#define UHC_DWC2_VENDOR_QUIRK_GET(n) \ + COND_CODE_1(DT_NODE_VENDOR_HAS_IDX(DT_DRV_INST(n), 1), \ + (&uhc_dwc2_vendor_quirks_##n), \ + (NULL)) + +#define DWC2_QUIRK_FUNC_DEFINE(fname) \ +static inline int uhc_dwc2_quirk_##fname(const struct device *const dev) \ +{ \ + const struct uhc_dwc2_config *const config = dev->config; \ + const struct uhc_dwc2_vendor_quirks *const quirks = config->quirks; \ + \ + if (quirks != NULL && quirks->fname != NULL) { \ + return config->quirks->fname(dev); \ + } \ + return 0; \ +} + +DWC2_QUIRK_FUNC_DEFINE(post_preinit) +DWC2_QUIRK_FUNC_DEFINE(pre_init) +DWC2_QUIRK_FUNC_DEFINE(pre_enable) +DWC2_QUIRK_FUNC_DEFINE(post_enable) +DWC2_QUIRK_FUNC_DEFINE(disable) +DWC2_QUIRK_FUNC_DEFINE(shutdown) +DWC2_QUIRK_FUNC_DEFINE(irq_clear) +DWC2_QUIRK_FUNC_DEFINE(is_phy_clk_off) + +#endif /* ZEPHYR_DRIVERS_USB_UHC_DWC2_H */ diff --git a/drivers/usb/uhc/uhc_dwc2_esp32_usb_otg.h b/drivers/usb/uhc/uhc_dwc2_esp32_usb_otg.h new file mode 100644 index 0000000000000..98cf2e77709b5 --- /dev/null +++ b/drivers/usb/uhc/uhc_dwc2_esp32_usb_otg.h @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2026 Roman Leonov + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_DRIVERS_USB_UHC_DWC2_ESP32_USB_OTG_H +#define ZEPHYR_DRIVERS_USB_UHC_DWC2_ESP32_USB_OTG_H + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +struct esp32_usb_otg_config { + const struct device *clock_dev; + const clock_control_subsys_t clock_subsys; + int irq_source; + int irq_priority; + int irq_flags; + usb_phy_target_t phy_target; +}; + +struct esp32_usb_otg_data { + struct intr_handle_data_t *int_handle; + usb_wrap_hal_context_t wrap_hal; +}; + +static void uhc_dwc2_isr_handler(const struct device *dev); + +static inline int esp32_usb_otg_init(const struct device *dev) +{ + const struct esp32_usb_otg_config *const cfg = UHC_DWC2_QUIRK_CONFIG(dev); + struct esp32_usb_otg_data *const data = UHC_DWC2_QUIRK_DATA(dev); + + int ret; + + if (!device_is_ready(cfg->clock_dev)) { + return -ENODEV; + } + + ret = clock_control_on(cfg->clock_dev, cfg->clock_subsys); + + if (ret != 0) { + return ret; + } + + /* pinout config to work in USB_OTG_MODE_HOST */ + esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ZERO_INPUT, + USB_OTG_IDDIG_IN_IDX, + false); + esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ZERO_INPUT, + USB_SRP_BVALID_IN_IDX, + false); + esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ONE_INPUT, + USB_OTG_VBUSVALID_IN_IDX, + false); + esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ONE_INPUT, + USB_OTG_AVALID_IN_IDX, + false); + + /* allocate interrupt but keep it disabled to avoid + * spurious suspend/resume event at enumeration phase + */ + ret = esp_intr_alloc(cfg->irq_source, + ESP_INTR_FLAG_INTRDISABLED | + ESP_PRIO_TO_FLAGS(cfg->irq_priority) | + ESP_INT_FLAGS_CHECK(cfg->irq_flags), + (intr_handler_t)uhc_dwc2_isr_handler, + (void *)dev, + &data->int_handle); + + return ret; +} + +static int esp32_usb_otg_enable_phy(const struct device *const dev) +{ + const struct esp32_usb_otg_config *const cfg = UHC_DWC2_QUIRK_CONFIG(dev); + struct esp32_usb_otg_data *const data = UHC_DWC2_QUIRK_DATA(dev); + + usb_wrap_hal_init(&data->wrap_hal); + usb_wrap_ll_phy_enable_pad(data->wrap_hal.dev, true); + +#if USB_WRAP_LL_EXT_PHY_SUPPORTED + if (cfg->phy_target == USB_PHY_TARGET_INT) { + usb_wrap_hal_phy_set_external(&data->wrap_hal, false); + } else { + /* If External PHY target is supported, set external to true */ + LOG_WRN("esp32 phy: External phy support isn't implemented"); + return -EINVAL; + } +#endif + + if (cfg->phy_target == USB_PHY_TARGET_INT) { + /* Set drive capabilities for DM and DP */ + gpio_ll_set_drive_capability(GPIO_LL_GET_HW(0), USBPHY_DM_NUM, GPIO_DRIVE_CAP_3); + gpio_ll_set_drive_capability(GPIO_LL_GET_HW(0), USBPHY_DP_NUM, GPIO_DRIVE_CAP_3); + /* Configure pull resistors for Host DM and DP */ + usb_wrap_pull_override_vals_t vals = { + .dp_pu = false, + .dm_pu = false, + .dp_pd = true, + .dm_pd = true, + }; + usb_wrap_hal_phy_enable_pull_override(&data->wrap_hal, &vals); + } + + LOG_DBG("PHY enabled"); + return 0; +} + +static int esp32_usb_otg_disable_phy(const struct device *const dev) +{ + struct esp32_usb_otg_data *const data = UHC_DWC2_QUIRK_DATA(dev); + + usb_wrap_hal_disable(); + usb_wrap_ll_phy_enable_pad(data->wrap_hal.dev, false); + + LOG_DBG("PHY disabled"); + return 0; +} + +static int esp32_usb_otg_pre_init(const struct device *const dev) +{ + int ret; + + /* Power up PHY */ + ret = esp32_usb_otg_enable_phy(dev); + + if (ret) { + LOG_ERR("Unable to power on esp32 usb otg PHY"); + return ret; + } + + /* Enable clock, configure pins and init interrupt */ + return esp32_usb_otg_init(dev); +} + +static int esp32_usb_otg_shutdown(const struct device *const dev) +{ + const struct esp32_usb_otg_config *const cfg = UHC_DWC2_QUIRK_CONFIG(dev); + struct esp32_usb_otg_data *const data = UHC_DWC2_QUIRK_DATA(dev); + int ret; + + /* Disable & free interrupt handler */ + if (data->int_handle != NULL) { + /* Stor interrupts first */ + ret = esp_intr_disable(data->int_handle); + if (ret != 0) { + LOG_ERR("Unable to disable interrupt: %d", ret); + return ret; + } + + ret = esp_intr_free(data->int_handle); + if (ret != 0) { + LOG_ERR("Unable to free interrupt: %d", ret); + return ret; + } + + data->int_handle = NULL; + } + + /* Disable PHY */ + esp32_usb_otg_disable_phy(dev); + + /* Put USB OTG input signals into a safe inactive state */ + esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ONE_INPUT, + USB_OTG_IDDIG_IN_IDX, + false); + esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ZERO_INPUT, + USB_SRP_BVALID_IN_IDX, + false); + esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ZERO_INPUT, + USB_OTG_VBUSVALID_IN_IDX, + false); + esp_rom_gpio_connect_in_signal(GPIO_MATRIX_CONST_ZERO_INPUT, + USB_OTG_AVALID_IN_IDX, + false); + + ret = clock_control_off(cfg->clock_dev, cfg->clock_subsys); + if (ret != 0) { + LOG_ERR("Unable to off the clock: %d", ret); + } + + return ret; +} + +static void esp32_usb_otg_irq_enable_func(const struct device *const dev) +{ + struct esp32_usb_otg_data *const data = UHC_DWC2_QUIRK_DATA(dev); + + esp_intr_enable(data->int_handle); +} + +static void esp32_usb_otg_irq_disable_func(const struct device *const dev) +{ + struct esp32_usb_otg_data *const data = UHC_DWC2_QUIRK_DATA(dev); + + esp_intr_disable(data->int_handle); +} + +#define QUIRK_ESP32_USB_OTG_DEFINE(n) \ + \ + static const struct esp32_usb_otg_config uhc_dwc2_quirk_config_##n = { \ + .clock_dev = DEVICE_DT_GET(DT_INST_CLOCKS_CTLR(n)), \ + .clock_subsys = (clock_control_subsys_t) \ + DT_INST_CLOCKS_CELL(n, offset), \ + .irq_source = DT_INST_IRQ_BY_IDX(n, 0, irq), \ + .irq_priority = DT_INST_IRQ_BY_IDX(n, 0, priority), \ + .irq_flags = DT_INST_IRQ_BY_IDX(n, 0, flags), \ + .phy_target = USB_PHY_TARGET_INT, \ + }; \ + \ + static struct esp32_usb_otg_data uhc_dwc2_quirk_data_##n; \ + \ + static const struct uhc_dwc2_vendor_quirks uhc_dwc2_vendor_quirks_##n = {\ + .pre_init = esp32_usb_otg_pre_init, \ + .shutdown = esp32_usb_otg_shutdown, \ + }; + +#define UHC_DWC2_IRQ_DT_INST_DEFINE(n) \ + static void uhc_dwc2_irq_enable_func_##n(const struct device *dev) \ + { \ + esp32_usb_otg_irq_enable_func(dev); \ + } \ + \ + static void uhc_dwc2_irq_disable_func_##n(const struct device *dev) \ + { \ + esp32_usb_otg_irq_disable_func(dev); \ + } + +DT_INST_FOREACH_STATUS_OKAY(QUIRK_ESP32_USB_OTG_DEFINE) + +#endif /* ZEPHYR_DRIVERS_USB_UHC_DWC2_ESP32_USB_OTG_H */ diff --git a/drivers/usb/uhc/uhc_dwc2_nrf_usbhs_nrf54l.h b/drivers/usb/uhc/uhc_dwc2_nrf_usbhs_nrf54l.h new file mode 100644 index 0000000000000..ec6a4f242caec --- /dev/null +++ b/drivers/usb/uhc/uhc_dwc2_nrf_usbhs_nrf54l.h @@ -0,0 +1,367 @@ +/* + * SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_DRIVERS_UHC_DWC2_NRF_USBHS_NRF54L_H +#define ZEPHYR_DRIVERS_UHC_DWC2_NRF_USBHS_NRF54L_H + +#include +#include +#include +#include +#include + +struct nrf_usbhs_nrf54l_config { + NRF_USBHS_Type *wrapper_base; + const struct device *vregusb_dev; +}; + +struct nrf_usbhs_nrf54l_data { + struct k_event events; + struct onoff_manager *pclk24m_mgr; + struct onoff_client pclk24m_cli; + bool pclk24m_requested; + bool vregusb_enabled; +}; + +/* + * On USBHS, we cannot access the DWC2 register until VBUS is detected and + * valid. If the user tries to force usbh_enable() and the corresponding + * uhc_enable() without a "VBUS ready" notification, the event wait will block + * until a valid VBUS signal is detected or until the + * CONFIG_UHC_DWC2_USBHS_VBUS_READY_TIMEOUT timeout expires. + */ +#define USBHS_VBUS_READY BIT(0) + +static inline bool usbhs_core_is_enabled(NRF_USBHS_Type *wrapper) +{ + return (wrapper->ENABLE & USBHS_ENABLE_CORE_Msk) != 0U; +} + +static void vregusb_event_cb(const struct device *dev, + const struct regulator_event *const evt, + const void *const user_data) +{ + const struct device *const dwc2_dev = user_data; + struct nrf_usbhs_nrf54l_data *const data = UHC_DWC2_QUIRK_DATA(dwc2_dev); + + if (evt->type == REGULATOR_VOLTAGE_DETECTED) { + k_event_post(&data->events, USBHS_VBUS_READY); + } + + if (evt->type == REGULATOR_VOLTAGE_REMOVED) { + k_event_set_masked(&data->events, 0, USBHS_VBUS_READY); + } +} + +static inline int usbhs_enable_core(const struct device *dev) +{ + const struct nrf_usbhs_nrf54l_config *const cfg = UHC_DWC2_QUIRK_CONFIG(dev); + NRF_USBHS_Type *wrapper = cfg->wrapper_base; + + if (!usbhs_core_is_enabled(wrapper)) { + /* Power up peripheral, but keep D+ pull-up blocked for now. */ + wrapper->ENABLE = USBHS_ENABLE_CORE_Msk; + + wrapper->PHY.OVERRIDEVALUES = (1 << 24) | (1 << 23); + wrapper->PHY.INPUTOVERRIDE = (1 << 31) | (1 << 30) | (1 << 24) | (1 << 23); + + wrapper->ENABLE = USBHS_ENABLE_PHY_Msk | USBHS_ENABLE_CORE_Msk; + + k_busy_wait(45); + + wrapper->TASKS_START = 1UL; + + k_busy_wait(1); + + /* + * DWC2 opmode is now guaranteed to be Non-Driving, allow D+ pull-up to + * become active once driver clears DCTL SftDiscon bit. + */ + wrapper->PHY.INPUTOVERRIDE = (1 << 31); + } + + return 0; +} + +static inline int usbhs_pre_init_power_and_clock(const struct device *dev) +{ + const struct nrf_usbhs_nrf54l_config *const cfg = UHC_DWC2_QUIRK_CONFIG(dev); + struct nrf_usbhs_nrf54l_data *const data = UHC_DWC2_QUIRK_DATA(dev); + NRF_USBHS_Type *wrapper = cfg->wrapper_base; + int err; + + if (!device_is_ready(cfg->vregusb_dev)) { + LOG_ERR("%s is not ready", cfg->vregusb_dev->name); + return -ENODEV; + } + + err = regulator_set_callback(cfg->vregusb_dev, vregusb_event_cb, dev); + if (err != 0) { + LOG_ERR("Failed to set regulator callback"); + return err; + } + + err = regulator_enable(cfg->vregusb_dev); + if (err != 0) { + LOG_ERR("Failed to enable %s", cfg->vregusb_dev->name); + return err; + } + data->vregusb_enabled = true; + + data->pclk24m_mgr = z_nrf_clock_control_get_onoff(CLOCK_CONTROL_NRF_SUBSYS_HF24M); + + sys_notify_init_spinwait(&data->pclk24m_cli.notify); + err = onoff_request(data->pclk24m_mgr, &data->pclk24m_cli); + if (err < 0) { + LOG_ERR("Failed to request PCLK24M %d", err); + return err; + } + + /* Prefer waiting for the notify result here if available. */ + data->pclk24m_requested = true; + + /* + * Make DWC2 bus/register access possible. + * If the PHY clock is also required for register access/reset, + * keep PHY enabled for the driver lifetime, not only runtime enable. + */ + wrapper->ENABLE = USBHS_ENABLE_CORE_Msk | USBHS_ENABLE_PHY_Msk; + + k_busy_wait(45); + + wrapper->TASKS_START = 1UL; + + k_busy_wait(1); + + return 0; +} + +#if (0) +static inline int usbhs_init_vreg_and_clock(const struct device *dev) +{ + const struct nrf_usbhs_nrf54l_config *const cfg = UHC_DWC2_QUIRK_CONFIG(dev); + struct nrf_usbhs_nrf54l_data *const data = UHC_DWC2_QUIRK_DATA(dev); + k_timeout_t timeout = K_FOREVER; + int err; + + if (!device_is_ready(cfg->vregusb_dev)) { + LOG_ERR("%s is not ready", cfg->vregusb_dev->name); + return -ENODEV; + } + + if (data->vregusb_enabled == false) { + err = regulator_set_callback(cfg->vregusb_dev, vregusb_event_cb, dev); + if (err != 0) { + LOG_ERR("Failed to set regulator callback"); + return err; + } + + err = regulator_enable(cfg->vregusb_dev); + if (err != 0) { + LOG_ERR("Failed to enable %s", cfg->vregusb_dev->name); + return err; + } + + data->vregusb_enabled = true; + } + + data->pclk24m_mgr = z_nrf_clock_control_get_onoff(CLOCK_CONTROL_NRF_SUBSYS_HF24M); + + if (CONFIG_UHC_DWC2_USBHS_VBUS_READY_TIMEOUT > 0) { + timeout = K_MSEC(CONFIG_UHC_DWC2_USBHS_VBUS_READY_TIMEOUT); + } + + if (!k_event_wait(&data->events, USBHS_VBUS_READY, false, timeout)) { + LOG_ERR("Timed out while waiting VBUS to be ready"); + return -ETIMEDOUT; + } + + /* Request PCLK24M using clock control driver */ + if (data->pclk24m_requested == false) { + sys_notify_init_spinwait(&data->pclk24m_cli.notify); + + err = onoff_request(data->pclk24m_mgr, &data->pclk24m_cli); + if (err < 0) { + LOG_ERR("Failed to request PCLK24M %d", err); + return err; + } + + data->pclk24m_requested = true; + } + + err = usbhs_enable_core(dev); + if (err != 0) { + LOG_ERR("Failed to enable DWC2 core"); + return err; + } + + return 0; +} +#endif // + +static inline int usbhs_pre_enable_core(const struct device *dev) +{ + const struct nrf_usbhs_nrf54l_config *const cfg = UHC_DWC2_QUIRK_CONFIG(dev); + struct nrf_usbhs_nrf54l_data *const data = UHC_DWC2_QUIRK_DATA(dev); + NRF_USBHS_Type *wrapper = cfg->wrapper_base; + k_timeout_t timeout = K_FOREVER; + + if (CONFIG_UHC_DWC2_USBHS_VBUS_READY_TIMEOUT > 0) { + timeout = K_MSEC(CONFIG_UHC_DWC2_USBHS_VBUS_READY_TIMEOUT); + } + + if (!k_event_wait(&data->events, USBHS_VBUS_READY, false, timeout)) { + LOG_ERR("Timed out while waiting VBUS to be ready"); + return -ETIMEDOUT; + } + + /* + * Host-mode PHY input/output override. + * Keep this to the minimum needed for host mode and pull state. + */ + wrapper->PHY.OVERRIDEVALUES = (1 << 24) | (1 << 23); + wrapper->PHY.INPUTOVERRIDE = (1 << 31) | (1 << 30) | (1 << 24) | (1 << 23); + + /* TODO: Port power? */ + + return 0; +} + +static inline int usbhs_disable_core(const struct device *dev) +{ + const struct nrf_usbhs_nrf54l_config *const cfg = UHC_DWC2_QUIRK_CONFIG(dev); + NRF_USBHS_Type *wrapper = cfg->wrapper_base; + + /* Put host PHY signalling into safe inactive state. */ + wrapper->PHY.OVERRIDEVALUES = (1 << 24) | (1 << 23); + wrapper->PHY.INPUTOVERRIDE = (1 << 31) | (1 << 30) | (1 << 24) | (1 << 23); + + /* + * Disable host port power here if applicable. + * + * Example placeholder: + * wrapper->HOST.PORTPOWER = 0; + */ + + /* + * Do NOT do this here if the UHC core may still access DWC2 registers + * or if the next enable assumes clocks are already alive: + * + * wrapper->ENABLE = 0UL; + * onoff_cancel_or_release(...); + */ + + return 0; +} + +#if (0) +static inline int usbhs_disable_core(const struct device *dev) +{ + const struct nrf_usbhs_nrf54l_config *const cfg = UHC_DWC2_QUIRK_CONFIG(dev); + NRF_USBHS_Type *wrapper = cfg->wrapper_base; + + /* Set ID to Host and forcefully disable D+ pull-up. */ + wrapper->PHY.OVERRIDEVALUES = (1 << 24) | (1 << 23); + wrapper->PHY.INPUTOVERRIDE = (1 << 31) | (1 << 30) | (1 << 24) | (1 << 23); + + wrapper->ENABLE = 0UL; + + return 0; +} +#endif // + +static inline int usbhs_shutdown(const struct device *dev) +{ + const struct nrf_usbhs_nrf54l_config *const cfg = UHC_DWC2_QUIRK_CONFIG(dev); + struct nrf_usbhs_nrf54l_data *const data = UHC_DWC2_QUIRK_DATA(dev); + NRF_USBHS_Type *wrapper = cfg->wrapper_base; + int err; + + /* Safe inactive PHY state before power-off. */ + wrapper->PHY.OVERRIDEVALUES = (1 << 24) | (1 << 23); + wrapper->PHY.INPUTOVERRIDE = (1 << 31) | (1 << 30) | (1 << 24) | (1 << 23); + + wrapper->ENABLE = 0UL; + + if (data->pclk24m_requested) { + err = onoff_cancel_or_release(data->pclk24m_mgr, &data->pclk24m_cli); + if (err < 0) { + LOG_ERR("Failed to stop PCLK24M %d", err); + return err; + } + data->pclk24m_requested = false; + } + + if (data->vregusb_enabled) { + err = regulator_disable(cfg->vregusb_dev); + if (err != 0) { + LOG_ERR("Failed to disable %s: %d", cfg->vregusb_dev->name, err); + return err; + } + data->vregusb_enabled = false; + } + + k_event_set_masked(&data->events, 0, USBHS_VBUS_READY); + + return 0; +} + +#if (0) +static inline int usbhs_shutdown(const struct device *dev) +{ + const struct nrf_usbhs_nrf54l_config *const cfg = UHC_DWC2_QUIRK_CONFIG(dev); + struct nrf_usbhs_nrf54l_data *const data = UHC_DWC2_QUIRK_DATA(dev); + int err; + + err = usbhs_disable_core(dev); + if (err != 0) { + return err; + } + + if (data->pclk24m_requested) { + err = onoff_cancel_or_release(data->pclk24m_mgr, &data->pclk24m_cli); + if (err < 0) { + LOG_ERR("Failed to stop PCLK24M %d", err); + return err; + } + + data->pclk24m_requested = false; + } + + if (data->vregusb_enabled) { + err = regulator_disable(cfg->vregusb_dev); + if (err != 0) { + LOG_ERR("Failed to disable %s: %d", cfg->vregusb_dev->name, err); + return err; + } + + data->vregusb_enabled = false; + } + + return 0; +} +#endif + +#define QUIRK_NRF_USBHS_NRF54L_DEFINE(n) \ + struct uhc_dwc2_vendor_quirks uhc_dwc2_vendor_quirks_##n = { \ + .pre_init = usbhs_pre_init_power_and_clock, \ + .pre_enable = usbhs_pre_enable_core, \ + .disable = usbhs_disable_core, \ + .shutdown = usbhs_shutdown, \ + }; \ + \ + static const struct nrf_usbhs_nrf54l_config uhc_dwc2_quirk_config_##n = {\ + .wrapper_base = UINT_TO_POINTER(DT_REG_ADDR(DT_INST_PARENT(n))),\ + .vregusb_dev = DEVICE_DT_GET(DT_PHANDLE(DT_INST_PARENT(n), \ + regulator)), \ + }; \ + \ + static struct nrf_usbhs_nrf54l_data uhc_dwc2_quirk_data_##n = { \ + .events = Z_EVENT_INITIALIZER(uhc_dwc2_quirk_data_##n.events), \ + }; + +DT_INST_FOREACH_STATUS_OKAY(QUIRK_NRF_USBHS_NRF54L_DEFINE) + +#endif /* ZEPHYR_DRIVERS_UHC_DWC2_NRF_USBHS_NRF54L_H */ diff --git a/samples/subsys/usb/shell/boards/esp32s3_devkitc_procpu.overlay b/samples/subsys/usb/shell/boards/esp32s3_devkitc_procpu.overlay new file mode 100644 index 0000000000000..9f66255629ad7 --- /dev/null +++ b/samples/subsys/usb/shell/boards/esp32s3_devkitc_procpu.overlay @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +zephyr_uhc0: &usb_otg { + status = "okay"; +}; diff --git a/tests/drivers/uhc/CMakeLists.txt b/tests/drivers/uhc/CMakeLists.txt new file mode 100644 index 0000000000000..8ccce1cbd9f71 --- /dev/null +++ b/tests/drivers/uhc/CMakeLists.txt @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(test_uhc_driver) + +target_sources(app PRIVATE + src/test_uhc_common.c + src/test_uhc_control.c + src/test_uhc_buffer.c +) diff --git a/tests/drivers/uhc/boards/esp32s3_devkitc_procpu.overlay b/tests/drivers/uhc/boards/esp32s3_devkitc_procpu.overlay new file mode 100644 index 0000000000000..9f66255629ad7 --- /dev/null +++ b/tests/drivers/uhc/boards/esp32s3_devkitc_procpu.overlay @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +zephyr_uhc0: &usb_otg { + status = "okay"; +}; diff --git a/tests/drivers/uhc/prj.conf b/tests/drivers/uhc/prj.conf new file mode 100644 index 0000000000000..7bbc1fb1499cb --- /dev/null +++ b/tests/drivers/uhc/prj.conf @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: Copyright Nordic Semiconductor ASA +# SPDX-License-Identifier: Apache-2.0 + +CONFIG_LOG=y +CONFIG_ZTEST=y + +CONFIG_UHC_DRIVER=y +CONFIG_UHC_DWC2=y +CONFIG_UHC_BUF_POOL_SIZE=2048 +CONFIG_USB_HOST_STACK=y + +# Stack usage +CONFIG_ZTEST_STACK_SIZE=4096 +CONFIG_THREAD_STACK_INFO=y +CONFIG_INIT_STACKS=y +CONFIG_SYS_HEAP_RUNTIME_STATS=y +CONFIG_ASSERT=y +CONFIG_ASSERT_VERBOSE=y diff --git a/tests/drivers/uhc/src/test_uhc_buffer.c b/tests/drivers/uhc/src/test_uhc_buffer.c new file mode 100644 index 0000000000000..a0ac7f891f124 --- /dev/null +++ b/tests/drivers/uhc/src/test_uhc_buffer.c @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2026 Roman Leonov + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +LOG_MODULE_REGISTER(test_uhc_buffer, LOG_LEVEL_INF); + +#include "test_uhc_common.h" + +ZTEST(uhc_driver_test_buffer, test_uhc_hs_xfer_alloc_with_buf_max_size) +{ + const struct device *uhc_dev; + struct uhc_transfer *xfer; + struct usb_device udev = { + .dev_desc = { + .bMaxPacketSize0 = 64, + }, + }; + + test_uhc_print_current_stack_usage(); + + uhc_dev = test_uhc_init(); + + xfer = uhc_xfer_alloc_with_buf(uhc_dev, USB_CONTROL_EP_IN, &udev, + NULL, NULL, TEST_UHC_MAX_XFER_DATA_BUF_SIZE); + + zassert_not_null(xfer, "Failed to allocate UHC transfer with %d-byte buffer", + TEST_UHC_MAX_XFER_DATA_BUF_SIZE); + zassert_not_null(xfer->buf, "Transfer buffer is NULL"); + zassert_true(net_buf_tailroom(xfer->buf) >= TEST_UHC_MAX_XFER_DATA_BUF_SIZE, + "Buffer tailroom too small: %u", net_buf_tailroom(xfer->buf)); + + uhc_xfer_buf_free(uhc_dev, xfer->buf); + xfer->buf = NULL; + + zassert_ok(uhc_xfer_free(uhc_dev, xfer)); + + test_uhc_shutdown(); + + test_uhc_print_current_stack_usage(); +} + +ZTEST(uhc_driver_test_buffer, test_uhc_fsls_xfer_alloc_with_buf_max_size) +{ + const struct device *uhc_dev; + struct uhc_transfer *xfer; + struct usb_device udev = { + .dev_desc = { + .bMaxPacketSize0 = 8, + }, + }; + + test_uhc_print_current_stack_usage(); + + uhc_dev = test_uhc_init(); + + xfer = uhc_xfer_alloc_with_buf(uhc_dev, USB_CONTROL_EP_IN, &udev, + NULL, NULL, TEST_UHC_MAX_XFER_DATA_BUF_SIZE); + + zassert_not_null(xfer, "Failed to allocate UHC transfer with %d-byte buffer", + TEST_UHC_MAX_XFER_DATA_BUF_SIZE); + zassert_not_null(xfer->buf, "Transfer buffer is NULL"); + zassert_true(net_buf_tailroom(xfer->buf) >= TEST_UHC_MAX_XFER_DATA_BUF_SIZE, + "Buffer tailroom too small: %u", net_buf_tailroom(xfer->buf)); + + uhc_xfer_buf_free(uhc_dev, xfer->buf); + xfer->buf = NULL; + + zassert_ok(uhc_xfer_free(uhc_dev, xfer)); + + test_uhc_shutdown(); + + test_uhc_print_current_stack_usage(); +} + +ZTEST_SUITE(uhc_driver_test_buffer, NULL, NULL, NULL, NULL, NULL); \ No newline at end of file diff --git a/tests/drivers/uhc/src/test_uhc_common.c b/tests/drivers/uhc/src/test_uhc_common.c new file mode 100644 index 0000000000000..03a19b28223e8 --- /dev/null +++ b/tests/drivers/uhc/src/test_uhc_common.c @@ -0,0 +1,482 @@ +/* + * Copyright (c) 2026 Roman Leonov + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +LOG_MODULE_REGISTER(test_uhc_common, LOG_LEVEL_INF); + +#define UHC_NODE DT_NODELABEL(zephyr_uhc0) + +#define UHC_TEST_EP0_INIT_MPS 8 +#define UHC_TEST_SHORT_REQ_LEN 8 +#define UHC_TEST_EVENT_TIMEOUT K_SECONDS(5) + +K_MSGQ_DEFINE(uhc_test_msgq, sizeof(struct uhc_event), 16, sizeof(uint32_t)); + +static const struct device *const uhc_dev = DEVICE_DT_GET(UHC_NODE); + +static int test_uhc_event_cb(const struct device *dev, const struct uhc_event *const event) +{ + struct uhc_event copy = *event; + + ARG_UNUSED(dev); + + return k_msgq_put(&uhc_test_msgq, ©, K_NO_WAIT); +} + +const struct device *test_uhc_init(void) +{ + int ret; + + zassert_true(device_is_ready(uhc_dev), "UHC is not ready"); + zassert_false(uhc_is_initialized(uhc_dev), "UHC already initialized"); + + ret = uhc_init(uhc_dev, test_uhc_event_cb, NULL); + zassert_true(ret == 0, "uhc_init failed: %d", ret); + zassert_true(uhc_is_initialized(uhc_dev), "UHC not initialized"); + + return uhc_dev; +} + +void test_uhc_enable(void) +{ + int ret; + + zassert_true(device_is_ready(uhc_dev), "UHC is not ready"); + + ret = uhc_enable(uhc_dev); + zassert_true(ret == 0, "uhc_enable failed: %d", ret); + + zassert_true(uhc_is_enabled(uhc_dev), "UHC not enabled"); +} + +void test_uhc_print_current_stack_usage(void) +{ + size_t unused; + int ret; + + ret = k_thread_stack_space_get(k_current_get(), &unused); + zassert_equal(ret, 0, "k_thread_stack_space_get failed: %d", ret); + + printk("Current thread unused stack: %zu bytes\n", unused); +} + +void test_uhc_dev_init(struct usb_device *const udev) +{ + zassert_not_null(udev, "udev is NULL"); + memset(udev, 0, sizeof(struct usb_device)); + k_mutex_init(&udev->mutex); +} + +static void test_uhc_wait_event(enum uhc_event_type type) +{ + struct uhc_event event; + int ret; + + ret = k_msgq_get(&uhc_test_msgq, &event, UHC_TEST_EVENT_TIMEOUT); + zassert_not_equal(ret, -ENOMSG, "Timeout waiting for UHC event"); + zassert_equal(ret, 0, "Unable to get message from queue: %d", ret); + zassert_equal(event.type, type, "Unexpected event: %d. Expected: %d", event.type, type); +} + +void test_uhc_wait_connection(enum usb_device_speed *dev_speed) +{ + struct uhc_event event; + int ret; + + ret = k_msgq_get(&uhc_test_msgq, &event, UHC_TEST_EVENT_TIMEOUT); + zassert_not_equal(ret, -ENOMSG, "Timeout waiting for UHC event"); + zassert_equal(ret, 0, "Unable to get message from queue: %d", ret); + + switch (event.type) { + case UHC_EVT_DEV_CONNECTED_HS: + *dev_speed = USB_SPEED_SPEED_HS; + break; + case UHC_EVT_DEV_CONNECTED_FS: + *dev_speed = USB_SPEED_SPEED_FS; + break; + case UHC_EVT_DEV_CONNECTED_LS: + *dev_speed = USB_SPEED_SPEED_LS; + break; + default: + zassert_true(false, "Unexpected connection event: %d.", event.type); + break; + } +} + +void test_uhc_wait_ep_request(void) +{ + test_uhc_wait_event(UHC_EVT_EP_REQUEST); +} + +void test_uhc_wait_removed(void) +{ + test_uhc_wait_event(UHC_EVT_DEV_REMOVED); +} + +void test_uhc_bus_reset(void) +{ + int ret; + + ret = uhc_bus_reset(uhc_dev); + zassert_equal(ret, 0, "uhc_bus_reset failed: %d", ret); + + test_uhc_wait_event(UHC_EVT_RESETED); +} + +void test_uhc_disable(void) +{ + int ret; + + ret = uhc_disable(uhc_dev); + zassert_equal(ret, 0, "uhc_disable failed: %d", ret); +} + +void test_uhc_disable_wait_removed(void) +{ + int ret; + + ret = uhc_disable(uhc_dev); + zassert_equal(ret, 0, "uhc_disable failed: %d", ret); + + test_uhc_wait_event(UHC_EVT_DEV_REMOVED); +} + +void test_uhc_shutdown(void) +{ + int ret; + + ret = uhc_shutdown(uhc_dev); + zassert_equal(ret, 0, "uhc_shutdown failed: %d", ret); +} + +static void test_uhc_control_request_in_data(struct usb_device *udev, + const struct usb_setup_packet *setup, + void *data, + size_t data_len) +{ + struct uhc_transfer *xfer; + int ret; + + zassert_not_null(udev, "udev is NULL"); + zassert_not_null(setup, "setup is NULL"); + zassert_not_null(data, "data is NULL"); + zassert_true(data_len > 0, "data_len is zero"); + zassert_false(usb_reqtype_is_to_device(setup), + "Expected device-to-host control request"); + + xfer = uhc_xfer_alloc_with_buf(uhc_dev, 0x80, udev, NULL, NULL, data_len); + zassert_not_null(xfer, "Failed to allocate UHC transfer"); + zassert_not_null(xfer->buf, "Transfer buffer is NULL"); + + memcpy(xfer->setup_pkt, setup, sizeof(*setup)); + + ret = uhc_ep_enqueue(uhc_dev, xfer); + zassert_equal(ret, 0, "uhc_ep_enqueue failed: %d", ret); + + test_uhc_wait_event(UHC_EVT_EP_REQUEST); + + zassert_equal(xfer->err, 0, "Control transfer failed: %d", xfer->err); + zassert_equal(xfer->buf->len, data_len, + "Unexpected IN data length: got %u expected %u", + xfer->buf->len, data_len); + + memcpy(data, xfer->buf->data, data_len); + + uhc_xfer_buf_free(uhc_dev, xfer->buf); + uhc_xfer_free(uhc_dev, xfer); +} + +size_t test_uhc_control_request_in_data_short_allowed(struct usb_device *udev, + const struct usb_setup_packet *setup, + void *data, + size_t data_len) +{ + struct uhc_transfer *xfer; + size_t actual_len; + int ret; + + zassert_not_null(udev, "udev is NULL"); + zassert_not_null(setup, "setup is NULL"); + zassert_not_null(data, "data is NULL"); + zassert_true(data_len > 0, "data_len is zero"); + zassert_false(usb_reqtype_is_to_device(setup), + "Expected device-to-host control request"); + + xfer = uhc_xfer_alloc_with_buf(uhc_dev, 0x80, udev, NULL, NULL, data_len); + zassert_not_null(xfer, "Failed to allocate UHC transfer"); + zassert_not_null(xfer->buf, "Transfer buffer is NULL"); + + memcpy(xfer->setup_pkt, setup, sizeof(*setup)); + + ret = uhc_ep_enqueue(uhc_dev, xfer); + zassert_equal(ret, 0, "uhc_ep_enqueue failed: %d", ret); + + test_uhc_wait_event(UHC_EVT_EP_REQUEST); + + zassert_equal(xfer->err, 0, "Control transfer failed: %d", xfer->err); + + zassert_true(xfer->buf->len <= data_len, + "Received more data than requested: got %u expected max %u", + xfer->buf->len, data_len); + + actual_len = xfer->buf->len; + + memcpy(data, xfer->buf->data, actual_len); + + uhc_xfer_buf_free(uhc_dev, xfer->buf); + uhc_xfer_free(uhc_dev, xfer); + + return actual_len; +} + +static void test_uhc_control_request_out_data(struct usb_device *udev, + const struct usb_setup_packet *setup, + const void *data, + size_t data_len) +{ + struct uhc_transfer *xfer; + int ret; + + zassert_not_null(udev, "udev is NULL"); + zassert_not_null(setup, "setup is NULL"); + zassert_not_null(data, "data is NULL"); + zassert_true(data_len > 0, "data_len is zero"); + zassert_true(usb_reqtype_is_to_device(setup), + "Expected host-to-device control request"); + zassert_equal(sys_le16_to_cpu(setup->wLength), data_len, + "wLength/data_len mismatch"); + + xfer = uhc_xfer_alloc_with_buf(uhc_dev, 0x00, udev, NULL, NULL, data_len); + + zassert_not_null(xfer, "Failed to allocate UHC transfer"); + zassert_not_null(xfer->buf, "Transfer buffer is NULL"); + + memcpy(xfer->setup_pkt, setup, sizeof(*setup)); + + net_buf_add_mem(xfer->buf, data, data_len); + + ret = uhc_ep_enqueue(uhc_dev, xfer); + zassert_equal(ret, 0, "uhc_ep_enqueue failed: %d", ret); + + test_uhc_wait_event(UHC_EVT_EP_REQUEST); + + /* Expect STALL as normal behavior */ + zassert_true(xfer->err == 0 || xfer->err == -EPIPE, + "Unexpected Control OUT transfer error: %d", xfer->err); + + if (xfer->err == -EPIPE) { + printk("Request STALLed\n"); + } + + uhc_xfer_buf_free(uhc_dev, xfer->buf); + uhc_xfer_free(uhc_dev, xfer); +} + +static void test_uhc_control_request(struct usb_device *udev, + const struct usb_setup_packet *setup) +{ + struct uhc_transfer *xfer; + uint8_t ep; + int ret; + + zassert_not_null(udev, "udev is NULL"); + zassert_not_null(setup, "setup is NULL"); + zassert_equal(sys_le16_to_cpu(setup->wLength), 0, + "Expected zero-length control request"); + zassert_true(usb_reqtype_is_to_device(setup), + "Expected host-to-device control request"); + + ep = usb_reqtype_is_to_device(setup) ? 0x00 : 0x80; + + xfer = uhc_xfer_alloc(uhc_dev, ep, udev, NULL, NULL); + zassert_not_null(xfer, "Failed to allocate UHC transfer"); + + memcpy(xfer->setup_pkt, setup, sizeof(*setup)); + + ret = uhc_ep_enqueue(uhc_dev, xfer); + zassert_equal(ret, 0, "uhc_ep_enqueue failed: %d", ret); + + test_uhc_wait_event(UHC_EVT_EP_REQUEST); + + zassert_equal(xfer->err, 0, "Control request failed: %d", xfer->err); + + uhc_xfer_free(uhc_dev, xfer); +} + +void test_uhc_dev_get_dev_desc(struct usb_device *udev, void *buf, size_t len) +{ + struct usb_setup_packet setup = { + .bmRequestType = (USB_REQTYPE_DIR_TO_HOST << 7), + .bRequest = USB_SREQ_GET_DESCRIPTOR, + .wValue = sys_cpu_to_le16((USB_DESC_DEVICE << 8) | 0), + .wIndex = 0, + .wLength = sys_cpu_to_le16(len), + }; + + test_uhc_control_request_in_data(udev, &setup, buf, len); +} + +void test_uhc_dev_get_cfg_desc(struct usb_device *udev, void *buf, size_t len) +{ + struct usb_setup_packet setup = { + .bmRequestType = (USB_REQTYPE_DIR_TO_HOST << 7), + .bRequest = USB_SREQ_GET_DESCRIPTOR, + .wValue = sys_cpu_to_le16((USB_DESC_CONFIGURATION << 8) | 0), + .wIndex = 0, + .wLength = sys_cpu_to_le16(len), + }; + + test_uhc_control_request_in_data(udev, &setup, buf, len); +} + +void test_uhc_dev_get_string_desc(struct usb_device *udev, + uint8_t index, + uint16_t lang_id, + void *buf, + size_t len) +{ + struct usb_setup_packet setup = { + .bmRequestType = (USB_REQTYPE_DIR_TO_HOST << 7), + .bRequest = USB_SREQ_GET_DESCRIPTOR, + .wValue = sys_cpu_to_le16((USB_DESC_STRING << 8) | index), + .wIndex = sys_cpu_to_le16(lang_id), + .wLength = sys_cpu_to_le16(len), + }; + + test_uhc_control_request_in_data(udev, &setup, buf, len); +} + +size_t test_uhc_dev_get_string_desc_short_allowed(struct usb_device *udev, + uint8_t index, + uint16_t lang_id, + void *buf, + size_t len) +{ + struct usb_setup_packet setup = { + .bmRequestType = USB_REQTYPE_DIR_TO_HOST << 7, + .bRequest = USB_SREQ_GET_DESCRIPTOR, + .wValue = sys_cpu_to_le16((USB_DESC_STRING << 8) | index), + .wIndex = sys_cpu_to_le16(lang_id), + .wLength = sys_cpu_to_le16(len), + }; + + return test_uhc_control_request_in_data_short_allowed(udev, &setup, buf, len); +} + +#define UVC_SET_CUR 0x01 +#define UVC_GET_CUR 0x81 +#define UVC_VS_PROBE_CONTROL 0x01 + +void test_uhc_dev_uvc_get_probe_cur(struct usb_device *udev, + uint8_t iface, + void *buf, + size_t len) +{ + struct usb_setup_packet setup = { + .bmRequestType = USB_REQTYPE_DIR_TO_DEVICE << 7 | + USB_REQTYPE_TYPE_CLASS | + USB_REQTYPE_RECIPIENT_INTERFACE, + .bRequest = UVC_GET_CUR, + .wValue = sys_cpu_to_le16(UVC_VS_PROBE_CONTROL << 8), + .wIndex = sys_cpu_to_le16(iface), + .wLength = sys_cpu_to_le16(len), + }; + + test_uhc_control_request_out_data(udev, &setup, buf, len); +} + +void test_uhc_dev_uvc_set_probe_cur(struct usb_device *udev, + uint8_t iface, + const void *buf, + size_t len) +{ + struct usb_setup_packet setup = { + .bmRequestType = USB_REQTYPE_DIR_TO_DEVICE << 7 | + USB_REQTYPE_TYPE_CLASS | + USB_REQTYPE_RECIPIENT_INTERFACE, + .bRequest = UVC_SET_CUR, + .wValue = sys_cpu_to_le16(UVC_VS_PROBE_CONTROL << 8), + .wIndex = sys_cpu_to_le16(iface), + .wLength = sys_cpu_to_le16(len), + }; + + test_uhc_control_request_out_data(udev, &setup, buf, len); +} + +void test_uhc_dev_set_address(struct usb_device *udev, uint8_t addr) +{ + struct usb_setup_packet setup = { + .bmRequestType = USB_REQTYPE_DIR_TO_DEVICE | + USB_REQTYPE_TYPE_STANDARD | + USB_REQTYPE_RECIPIENT_DEVICE, + .bRequest = USB_SREQ_SET_ADDRESS, + .wValue = sys_cpu_to_le16(addr), + .wIndex = 0, + .wLength = 0, + }; + + test_uhc_control_request(udev, &setup); + + udev->addr = addr; +} + +void test_uhc_dev_set_config(struct usb_device *udev, uint8_t cfg) +{ + struct usb_setup_packet setup = { + .bmRequestType = USB_REQTYPE_DIR_TO_DEVICE | + USB_REQTYPE_TYPE_STANDARD | + USB_REQTYPE_RECIPIENT_DEVICE, + .bRequest = USB_SREQ_SET_CONFIGURATION, + .wValue = sys_cpu_to_le16(cfg), + .wIndex = 0, + .wLength = 0, + }; + + test_uhc_control_request(udev, &setup); +} + +void test_uhc_dev_get_short_dev_desc(struct usb_device *udev, uint8_t *ep0_mps) +{ + uint8_t mps; + + zassert_not_null(udev, "udev is NULL"); + zassert_not_null(ep0_mps, "ep0_mps is NULL"); + + udev->dev_desc.bMaxPacketSize0 = UHC_TEST_EP0_INIT_MPS; + + test_uhc_dev_get_dev_desc(udev, &udev->dev_desc, UHC_TEST_SHORT_REQ_LEN); + + zassert_equal(udev->dev_desc.bDescriptorType, USB_DESC_DEVICE, + "Unexpected descriptor type: %u", udev->dev_desc.bDescriptorType); + + mps = udev->dev_desc.bMaxPacketSize0; + zassert_true(mps == 8 || mps == 16 || mps == 32 || mps == 64, + "Invalid EP0 MPS: %u", mps); + + *ep0_mps = mps; +} + +void test_uhc_dev_get_full_dev_desc(struct usb_device *udev, uint8_t ep0_mps) +{ + zassert_not_null(udev, "udev is NULL"); + zassert_not_equal(udev->dev_desc.bMaxPacketSize0, 0, "Unknown EP0 MPS"); + + udev->dev_desc.bDescriptorType = 0; + udev->dev_desc.bMaxPacketSize0 = ep0_mps; + + test_uhc_dev_get_dev_desc(udev, &udev->dev_desc, sizeof(struct usb_device_descriptor)); + + zassert_equal(udev->dev_desc.bDescriptorType, USB_DESC_DEVICE, + "Unexpected descriptor type: %u", udev->dev_desc.bDescriptorType); +} \ No newline at end of file diff --git a/tests/drivers/uhc/src/test_uhc_common.h b/tests/drivers/uhc/src/test_uhc_common.h new file mode 100644 index 0000000000000..26bada5d5f87a --- /dev/null +++ b/tests/drivers/uhc/src/test_uhc_common.h @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2026 Roman Leonov + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + + +#define TEST_UHC_MAX_XFER_DATA_BUF_SIZE (1024U + 256U) + +void test_uhc_print_current_stack_usage(void); + +const struct device *test_uhc_init(void); +void test_uhc_enable(void); +void test_uhc_wait_connection(enum usb_device_speed *speed); +void test_uhc_wait_ep_request(void); +void test_uhc_wait_removed(void); +void test_uhc_bus_reset(void); +void test_uhc_disable(void); +void test_uhc_disable_wait_removed(void); +void test_uhc_shutdown(void); + +void test_uhc_dev_init(struct usb_device *const udev); +void test_uhc_dev_set_address(struct usb_device *udev, uint8_t addr); +void test_uhc_dev_set_config(struct usb_device *udev, uint8_t cfg); +void test_uhc_dev_get_short_dev_desc(struct usb_device *udev, uint8_t *ep0_mps); +void test_uhc_dev_get_full_dev_desc(struct usb_device *udev, uint8_t ep0_mps); +void test_uhc_dev_get_cfg_desc(struct usb_device *udev, void *buf, size_t len); +void test_uhc_dev_get_string_desc(struct usb_device *udev, + uint8_t index, + uint16_t lang_id, + void *buf, + size_t len); +size_t test_uhc_dev_get_string_desc_short_allowed(struct usb_device *udev, + uint8_t index, + uint16_t lang_id, + void *buf, + size_t len); + +void test_uhc_dev_uvc_get_probe_cur(struct usb_device *udev, uint8_t iface, void *buf, size_t len); +void test_uhc_dev_uvc_set_probe_cur(struct usb_device *udev, uint8_t iface, void *buf, size_t len); + + + + + diff --git a/tests/drivers/uhc/src/test_uhc_control.c b/tests/drivers/uhc/src/test_uhc_control.c new file mode 100644 index 0000000000000..ac22f8d400c4c --- /dev/null +++ b/tests/drivers/uhc/src/test_uhc_control.c @@ -0,0 +1,466 @@ +/* + * Copyright (c) 2026 Roman Leonov + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +LOG_MODULE_REGISTER(test_uhc_control, LOG_LEVEL_INF); + +#include "test_uhc_common.h" + +static void test_uhc_prepare_device(struct usb_device *udev, + enum usb_device_speed *speed) +{ + zassert_not_null(udev, "udev is NULL"); + zassert_not_null(speed, "speed is NULL"); + + *speed = USB_SPEED_UNKNOWN; + + test_uhc_enable(); + test_uhc_wait_connection(speed); + test_uhc_bus_reset(); + test_uhc_dev_init(udev); + udev->dev_desc.bMaxPacketSize0 = 8; +} + +static void test_uhc_prepare_addressed_device(struct usb_device *udev, + enum usb_device_speed *speed, + uint8_t dev_addr) +{ + uint8_t ep0_mps; + + zassert_not_null(udev, "udev is NULL"); + zassert_not_null(speed, "speed is NULL"); + + *speed = USB_SPEED_UNKNOWN; + + test_uhc_enable(); + test_uhc_wait_connection(speed); + test_uhc_bus_reset(); + + test_uhc_dev_init(udev); + + test_uhc_dev_get_short_dev_desc(udev, &ep0_mps); + test_uhc_dev_get_full_dev_desc(udev, ep0_mps); + test_uhc_dev_set_address(udev, dev_addr); +} + +static void test_uhc_control_cleanup(void) +{ + test_uhc_disable_wait_removed(); + test_uhc_shutdown(); +} + +ZTEST(uhc_driver_test_control, test_uhc_control_without_buf_expect_ret_error) +{ + const struct device *uhc_dev; + struct usb_device udev; + struct uhc_transfer *xfer; + enum usb_device_speed speed; + struct usb_setup_packet setup = { + .bmRequestType = USB_REQTYPE_DIR_TO_HOST | + USB_REQTYPE_TYPE_STANDARD | + USB_REQTYPE_RECIPIENT_DEVICE, + .bRequest = USB_SREQ_GET_DESCRIPTOR, + .wValue = sys_cpu_to_le16((USB_DESC_DEVICE << 8) | 0), + .wIndex = 0, + .wLength = sys_cpu_to_le16(8), + }; + int ret; + + uhc_dev = test_uhc_init(); + + test_uhc_prepare_device(&udev, &speed); + + xfer = uhc_xfer_alloc(uhc_dev, 0x80, &udev, NULL, NULL); + zassert_not_null(xfer, "Failed to allocate transfer"); + + memcpy(xfer->setup_pkt, &setup, sizeof(setup)); + + ret = uhc_ep_enqueue(uhc_dev, xfer); + zassert_equal(ret, -EINVAL, + "Expected -EINVAL for data stage without buffer, got %d", + ret); + + uhc_xfer_free(uhc_dev, xfer); + + test_uhc_control_cleanup(); +} + +ZTEST(uhc_driver_test_control, test_uhc_control_unaligned_buf_expect_ret_error) +{ + const struct device *uhc_dev; + enum usb_device_speed speed; + struct usb_setup_packet setup = { + .bmRequestType = USB_REQTYPE_DIR_TO_HOST | + USB_REQTYPE_TYPE_STANDARD | + USB_REQTYPE_RECIPIENT_DEVICE, + .bRequest = USB_SREQ_GET_DESCRIPTOR, + .wValue = sys_cpu_to_le16((USB_DESC_DEVICE << 8) | 0), + .wIndex = 0, + .wLength = sys_cpu_to_le16(8), + }; + struct uhc_transfer *xfer; + struct usb_device udev; + int ret; + + uhc_dev = test_uhc_init(); + test_uhc_prepare_device(&udev, &speed); + + xfer = uhc_xfer_alloc_with_buf(uhc_dev, 0x80, &udev, NULL, NULL, 8); + zassert_not_null(xfer, "Failed to allocate transfer"); + zassert_not_null(xfer->buf, "Transfer buffer is NULL"); + + memcpy(xfer->setup_pkt, &setup, sizeof(setup)); + + net_buf_add_u8(xfer->buf, 0); + + zassert_not_equal((uintptr_t)net_buf_tail(xfer->buf) % 4, 0, + "Failed to create unaligned transfer buffer tail"); + + ret = uhc_ep_enqueue(uhc_dev, xfer); + zassert_equal(ret, -EINVAL, + "Expected -EINVAL for unaligned buffer, got %d", + ret); + + uhc_xfer_buf_free(uhc_dev, xfer->buf); + uhc_xfer_free(uhc_dev, xfer); + + test_uhc_control_cleanup(); +} + +ZTEST(uhc_driver_test_control, test_uhc_control_get_cfg_desc_header_and_full) +{ + struct usb_device udev; + struct usb_cfg_descriptor cfg_desc; + uint8_t cfg_buffer[TEST_UHC_MAX_XFER_DATA_BUF_SIZE] = { 0 }; + enum usb_device_speed speed; + uint32_t total_len; + + test_uhc_init(); + + test_uhc_prepare_addressed_device(&udev, &speed, 1); + + memset(&cfg_desc, 0, sizeof(cfg_desc)); + + test_uhc_dev_get_cfg_desc(&udev, &cfg_desc, sizeof(cfg_desc)); + + total_len = sys_le16_to_cpu(cfg_desc.wTotalLength); + + zassert_equal(cfg_desc.bLength, sizeof(struct usb_cfg_descriptor), + "Unexpected config descriptor length: %u", + cfg_desc.bLength); + + zassert_equal(cfg_desc.bDescriptorType, USB_DESC_CONFIGURATION, + "Unexpected descriptor type: %u", + cfg_desc.bDescriptorType); + + printf("cfg_desc.bLength = %u\n", total_len); + + zassert_true(total_len >= sizeof(struct usb_cfg_descriptor), + "Invalid config descriptor total length: %u", + total_len); + + zassert_true(cfg_desc.bNumInterfaces > 0, + "Configuration descriptor reports no interfaces"); + + + zassert_true(total_len <= sizeof(cfg_buffer), + "Config descriptor too large for test buffer: %u", + total_len); + + test_uhc_dev_get_cfg_desc(&udev, cfg_buffer, total_len); + + zassert_equal(cfg_buffer[0], sizeof(struct usb_cfg_descriptor), + "Unexpected config descriptor bLength: %u", + cfg_buffer[0]); + + zassert_equal(cfg_buffer[1], USB_DESC_CONFIGURATION, + "Unexpected config descriptor type: %u", + cfg_buffer[1]); + + zassert_equal(sys_get_le16(&cfg_buffer[2]), total_len, + "Unexpected config descriptor total length"); + + test_uhc_control_cleanup(); +} + +ZTEST(uhc_driver_test_control, + test_uhc_control_get_cfg_desc_small_buf_expect_ret_error) +{ + const struct device *uhc_dev; + struct usb_device udev; + struct usb_cfg_descriptor cfg_desc; + struct uhc_transfer *xfer; + enum usb_device_speed speed; + struct usb_setup_packet setup; + uint16_t total_len; + size_t small_len; + int ret; + + uhc_dev = test_uhc_init(); + + test_uhc_prepare_addressed_device(&udev, &speed, 1); + + memset(&cfg_desc, 0, sizeof(cfg_desc)); + + test_uhc_dev_get_cfg_desc(&udev, &cfg_desc, sizeof(cfg_desc)); + + total_len = sys_le16_to_cpu(cfg_desc.wTotalLength); + + zassert_true(total_len > sizeof(struct usb_cfg_descriptor), + "Config descriptor is too small for this test: %u", + total_len); + + small_len = sizeof(struct usb_cfg_descriptor); + + setup = (struct usb_setup_packet) { + .bmRequestType = USB_REQTYPE_DIR_TO_HOST << 7, + .bRequest = USB_SREQ_GET_DESCRIPTOR, + .wValue = sys_cpu_to_le16((USB_DESC_CONFIGURATION << 8) | 0), + .wIndex = 0, + .wLength = sys_cpu_to_le16(total_len), + }; + + xfer = uhc_xfer_alloc_with_buf(uhc_dev, 0x80, &udev, NULL, NULL, small_len); + + zassert_not_null(xfer, "Failed to allocate transfer"); + zassert_not_null(xfer->buf, "Transfer buffer is NULL"); + + memcpy(xfer->setup_pkt, &setup, sizeof(setup)); + + ret = uhc_ep_enqueue(uhc_dev, xfer); + zassert_equal(ret, -EINVAL, + "Expected -EINVAL for too-small config descriptor buffer, got %d", + ret); + + uhc_xfer_buf_free(uhc_dev, xfer->buf); + uhc_xfer_free(uhc_dev, xfer); + + test_uhc_control_cleanup(); +} + +ZTEST(uhc_driver_test_control, test_uhc_control_get_string_desc_zero_255) +{ + struct usb_device udev; + enum usb_device_speed speed; + uint8_t string_desc[255] = { 0 }; + size_t actual_len; + + test_uhc_init(); + + test_uhc_prepare_addressed_device(&udev, &speed, 1); + + actual_len = test_uhc_dev_get_string_desc_short_allowed(&udev, + 0, + 0, + string_desc, + sizeof(string_desc)); + + zassert_true(actual_len >= 4, + "String descriptor zero too short: %u", + actual_len); + + zassert_true(actual_len <= sizeof(string_desc), + "String descriptor zero too long: %u", + actual_len); + + zassert_equal(string_desc[1], USB_DESC_STRING, + "Unexpected descriptor type: %u", + string_desc[1]); + + zassert_equal(string_desc[0], actual_len, + "Unexpected descriptor bLength: got %u expected %u", + string_desc[0], actual_len); + + test_uhc_control_cleanup(); +} + +ZTEST(uhc_driver_test_control, + test_uhc_control_stall_then_valid_request_succeeds) +{ + const struct device *uhc_dev; + struct usb_device udev; + struct uhc_transfer *xfer; + struct usb_cfg_descriptor cfg_desc; + enum usb_device_speed speed; + struct usb_setup_packet stall_setup = { + .bmRequestType = USB_REQTYPE_DIR_TO_HOST << 7, + .bRequest = USB_SREQ_GET_DESCRIPTOR, + /* + * Descriptor type 0xFF is intentionally unsupported. + * A compliant device should STALL this request. + */ + .wValue = sys_cpu_to_le16((0xFF << 8) | 0), + .wIndex = 0, + .wLength = sys_cpu_to_le16(8), + }; + int ret; + + uhc_dev = test_uhc_init(); + + test_uhc_prepare_addressed_device(&udev, &speed, 1); + + xfer = uhc_xfer_alloc_with_buf(uhc_dev, 0x80, &udev, NULL, NULL, 8); + zassert_not_null(xfer, "Failed to allocate UHC transfer"); + zassert_not_null(xfer->buf, "Transfer buffer is NULL"); + + memcpy(xfer->setup_pkt, &stall_setup, sizeof(stall_setup)); + + ret = uhc_ep_enqueue(uhc_dev, xfer); + zassert_equal(ret, 0, "uhc_ep_enqueue failed: %d", ret); + + test_uhc_wait_ep_request(); + + zassert_equal(xfer->err, -EPIPE, + "Expected STALL/-EPIPE, got %d", + xfer->err); + + uhc_xfer_buf_free(uhc_dev, xfer->buf); + uhc_xfer_free(uhc_dev, xfer); + + memset(&cfg_desc, 0, sizeof(cfg_desc)); + + /* + * A new SETUP packet on EP0 should recover from the previous STALL. + * Verify that the control pipe still works by issuing a valid request. + */ + test_uhc_dev_get_cfg_desc(&udev, &cfg_desc, sizeof(cfg_desc)); + + zassert_equal(cfg_desc.bLength, sizeof(struct usb_cfg_descriptor), + "Unexpected config descriptor length: %u", + cfg_desc.bLength); + + zassert_equal(cfg_desc.bDescriptorType, USB_DESC_CONFIGURATION, + "Unexpected descriptor type: %u", + cfg_desc.bDescriptorType); + + zassert_true(sys_le16_to_cpu(cfg_desc.wTotalLength) >= + sizeof(struct usb_cfg_descriptor), + "Invalid config descriptor total length: %u", + sys_le16_to_cpu(cfg_desc.wTotalLength)); + + test_uhc_control_cleanup(); +} + +#if (0) +ZTEST(uhc_driver_test_control, + test_uhc_control_disable_during_pending_request) +{ + const struct device *uhc_dev; + struct usb_device udev; + struct uhc_transfer *xfer; + enum usb_device_speed speed; + struct usb_setup_packet setup = { + .bmRequestType = USB_REQTYPE_DIR_TO_HOST << 7, + .bRequest = USB_SREQ_GET_DESCRIPTOR, + .wValue = sys_cpu_to_le16((USB_DESC_STRING << 8) | 0), + .wIndex = 0, + .wLength = sys_cpu_to_le16(255), + }; + int ret; + + uhc_dev = test_uhc_init(); + + test_uhc_prepare_addressed_device(&udev, &speed, 1); + + xfer = uhc_xfer_alloc_with_buf(uhc_dev, 0x80, &udev, NULL, NULL, 255); + zassert_not_null(xfer, "Failed to allocate UHC transfer"); + zassert_not_null(xfer->buf, "Transfer buffer is NULL"); + + memcpy(xfer->setup_pkt, &setup, sizeof(setup)); + + ret = uhc_ep_enqueue(uhc_dev, xfer); + zassert_equal(ret, 0, "uhc_ep_enqueue failed: %d", ret); + + ret = uhc_disable(uhc_dev); + zassert_equal(ret, 0, "uhc_disable failed: %d", ret); + + test_uhc_wait_ep_request(); + + zassert_equal(xfer->err, -ESHUTDOWN, + "Expected -ESHUTDOWN after disable, got %d", + xfer->err); + + test_uhc_wait_removed(); + + uhc_xfer_buf_free(uhc_dev, xfer->buf); + uhc_xfer_free(uhc_dev, xfer); +} +#endif // + +#define UHC_TEST_UVC_VS_IFACE 1 +#define UHC_TEST_UVC_PROBE_LEN 26 + +ZTEST(uhc_driver_test_control, test_uhc_control_uvc_vs_probe_set_cur) +{ + struct usb_device udev; + enum usb_device_speed speed; + uint8_t probe[UHC_TEST_UVC_PROBE_LEN]; + + test_uhc_init(); + + test_uhc_prepare_addressed_device(&udev, &speed, 1); + + memset(probe, 0, sizeof(probe)); + + test_uhc_dev_uvc_get_probe_cur(&udev, + UHC_TEST_UVC_VS_IFACE, + probe, + sizeof(probe)); + + test_uhc_dev_uvc_set_probe_cur(&udev, + UHC_TEST_UVC_VS_IFACE, + probe, + sizeof(probe)); + + test_uhc_control_cleanup(); +} + +#define UHC_TEST_ENUM_ADDR 1 +#define UHC_TEST_ENUM_CONFIG 1 +#define UHC_TEST_ENUM_ATTEMPTS 10 + +ZTEST(uhc_driver_test_control, test_uhc_control_fast_enum) +{ + enum usb_device_speed speed = USB_SPEED_UNKNOWN; + struct usb_device udev; + uint8_t ep0_mps; + + test_uhc_print_current_stack_usage(); + + test_uhc_init(); + + for (int attempt = 0; attempt < UHC_TEST_ENUM_ATTEMPTS; attempt++) { + + test_uhc_enable(); + + test_uhc_wait_connection(&speed); + + test_uhc_bus_reset(); + + test_uhc_dev_init(&udev); + + test_uhc_dev_get_short_dev_desc(&udev, &ep0_mps); + + test_uhc_dev_get_full_dev_desc(&udev, ep0_mps); + + test_uhc_dev_set_address(&udev, UHC_TEST_ENUM_ADDR); + + test_uhc_dev_set_config(&udev, UHC_TEST_ENUM_CONFIG); + + test_uhc_disable_wait_removed(); + } + + test_uhc_shutdown(); + + test_uhc_print_current_stack_usage(); +} + + +ZTEST_SUITE(uhc_driver_test_control, NULL, NULL, NULL, NULL, NULL); diff --git a/tests/drivers/uhc/testcase.yaml b/tests/drivers/uhc/testcase.yaml new file mode 100644 index 0000000000000..00e04fbb1b548 --- /dev/null +++ b/tests/drivers/uhc/testcase.yaml @@ -0,0 +1,6 @@ +tests: + drivers.usb.uhc: + tags: + - drivers + platform_allow: + - esp32s3_devkitc/esp32s3/procpu