From 5153e69f86919c326d0455dd4b74ae115bff9484 Mon Sep 17 00:00:00 2001 From: Josuah Demangeon Date: Tue, 27 Jan 2026 20:05:59 +0000 Subject: [PATCH 1/4] dts: bindings: dwc2: add extra hardware registers and devicetree entries Add 'gsnpsid' and 'ghwcfg3' to DWC2 USB peripheral in order to check that they are as expected at runtimne, which the host driver can use. Signed-off-by: Josuah Demangeon --- dts/arm/syna/sr100.dtsi | 2 ++ dts/bindings/usb/snps,dwc2.yaml | 11 +++++++++++ dts/vendor/nordic/nrf54h20.dtsi | 2 ++ dts/vendor/nordic/nrf54lm20_a_b.dtsi | 2 ++ dts/xtensa/espressif/esp32s3/esp32s3_common.dtsi | 2 ++ 5 files changed, 19 insertions(+) diff --git a/dts/arm/syna/sr100.dtsi b/dts/arm/syna/sr100.dtsi index 8c504ebc2b0ef..49ebc9751d636 100644 --- a/dts/arm/syna/sr100.dtsi +++ b/dts/arm/syna/sr100.dtsi @@ -189,8 +189,10 @@ num-out-eps = <8>; num-in-eps = <8>; clocks = <&gcr SYNA_USB_CLK>; + gsnpsid = <0x5532430a>; ghwcfg1 = <0x00000000>; ghwcfg2 = <0x02882054>; // 8 endpoints, dynamic sizing + ghwcfg3 = <0x0c78c468>; ghwcfg4 = <0xe2003e20>; // 9 in endpoints, desc-DMA status = "disabled"; }; diff --git a/dts/bindings/usb/snps,dwc2.yaml b/dts/bindings/usb/snps,dwc2.yaml index d5b0167f26b32..dc9daa3961f26 100644 --- a/dts/bindings/usb/snps,dwc2.yaml +++ b/dts/bindings/usb/snps,dwc2.yaml @@ -31,6 +31,11 @@ properties: description: | Number of configured IN endpoints including control endpoint. + gsnpsid: + type: int + description: | + Value of the GSNPSID register, used to declare the version of the core. + ghwcfg1: type: int required: true @@ -45,6 +50,12 @@ properties: Value of the GHWCFG2 register. It is used to determine available endpoint types during driver pre-initialization. + ghwcfg3: + type: int + description: | + Value of the GHWCFG3 register. It is used to determine available endpoint + types during driver pre-initialization. + ghwcfg4: type: int required: true diff --git a/dts/vendor/nordic/nrf54h20.dtsi b/dts/vendor/nordic/nrf54h20.dtsi index aecd7d8966782..9b686ca918047 100644 --- a/dts/vendor/nordic/nrf54h20.dtsi +++ b/dts/vendor/nordic/nrf54h20.dtsi @@ -670,8 +670,10 @@ interrupts = <134 NRF_DEFAULT_IRQ_PRIORITY>; num-in-eps = <8>; num-out-eps = <10>; + gsnpsid = <0x4f54430a>; ghwcfg1 = <0xaa555000>; ghwcfg2 = <0x22abfc72>; + ghwcfg3 = <0x0beac0e8>; ghwcfg4 = <0x1e10aa60>; status = "disabled"; }; diff --git a/dts/vendor/nordic/nrf54lm20_a_b.dtsi b/dts/vendor/nordic/nrf54lm20_a_b.dtsi index 39c94ffdea061..46fd019179225 100644 --- a/dts/vendor/nordic/nrf54lm20_a_b.dtsi +++ b/dts/vendor/nordic/nrf54lm20_a_b.dtsi @@ -249,8 +249,10 @@ interrupts = <90 NRF_DEFAULT_IRQ_PRIORITY>; num-in-eps = <16>; num-out-eps = <16>; + gsnpsid = <0x4f54500b>; ghwcfg1 = <0x0>; ghwcfg2 = <0x22affc52>; + ghwcfg3 = <0x0be0c0e8>; ghwcfg4 = <0x3e10aa60>; status = "disabled"; }; diff --git a/dts/xtensa/espressif/esp32s3/esp32s3_common.dtsi b/dts/xtensa/espressif/esp32s3/esp32s3_common.dtsi index 570f828e4734e..7b3d61740ee6b 100644 --- a/dts/xtensa/espressif/esp32s3/esp32s3_common.dtsi +++ b/dts/xtensa/espressif/esp32s3/esp32s3_common.dtsi @@ -436,8 +436,10 @@ clocks = <&clock ESP32_USB_MODULE>; num-out-eps = <6>; num-in-eps = <6>; + gsnpsid = <0x4f54400a>; ghwcfg1 = <0x00000000>; ghwcfg2 = <0x224dd930>; + ghwcfg3 = <0x00c804b5>; ghwcfg4 = <0xd3f0a030>; }; From cf5accb3d5430a8d8eb8252d5deee140b5da1ae1 Mon Sep 17 00:00:00 2001 From: Roman Leonov Date: Thu, 8 Jan 2026 11:58:50 +0100 Subject: [PATCH 2/4] drivers: usb: uhc_dwc2: add initial support Note: Control transfers only Add initial usb host driver for Synopsys DWC2 with vendor quirks. Signed-off-by: Roman Leonov --- drivers/usb/uhc/CMakeLists.txt | 2 + drivers/usb/uhc/Kconfig | 1 + drivers/usb/uhc/Kconfig.dwc2 | 74 + drivers/usb/uhc/uhc_dwc2.c | 1577 +++++++++++++++++ drivers/usb/uhc/uhc_dwc2.h | 92 + drivers/usb/uhc/uhc_dwc2_esp32_usb_otg.h | 185 ++ .../boards/esp32s3_devkitc_procpu.overlay | 9 + 7 files changed, 1940 insertions(+) create mode 100644 drivers/usb/uhc/Kconfig.dwc2 create mode 100644 drivers/usb/uhc/uhc_dwc2.c create mode 100644 drivers/usb/uhc/uhc_dwc2.h create mode 100644 drivers/usb/uhc/uhc_dwc2_esp32_usb_otg.h create mode 100644 samples/subsys/usb/shell/boards/esp32s3_devkitc_procpu.overlay 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..2d19ad25f7cfb --- /dev/null +++ b/drivers/usb/uhc/Kconfig.dwc2 @@ -0,0 +1,74 @@ +# 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 + default y + select EVENTS + help + DWC2 USB host controller driver. + +if UHC_DWC2 + +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 30 + help + The reset signaling can be generated on any Hub or Host Controller port by request from + the USB System Software. The USB 2.0 specification requires that "the reset signaling must + be driven for a minimum of 10ms" (see USB 2.0 chapter 7.1.7.5 for more details). + After the reset, the hub port will transition to the Enabled state (refer to Section 11.5). + + The default value is set to 30 ms to be safe. + +config UHC_DWC2_RESET_RECOVERY_MS + int "Reset recovery delay in ms" + default 30 + help + After a port stops driving the reset signal, the USB 2.0 specification requires that + the "USB System Software guarantees a minimum of 10 ms for reset recovery" before the + attached device is expected to respond to data transfers (see USB 2.0 chapter 7.1.7.3 for + more details). + The device may ignore any data transfers during the recovery interval. + + The default value is set to 30 ms to be safe. + + +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. + + +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..6099cd076e02c --- /dev/null +++ b/drivers/usb/uhc/uhc_dwc2.c @@ -0,0 +1,1577 @@ +/* + * Copyright (c) 2026 Roman Leonov + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#define DT_DRV_COMPAT snps_dwc2 + +#include "uhc_common.h" +#include "uhc_dwc2.h" + +#include +#include +#include +#include +#include + +#include +LOG_MODULE_REGISTER(uhc_dwc2, CONFIG_UHC_DRIVER_LOG_LEVEL); + +#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 +/* TODO: Configurable? */ +#define MAX_CHANNELS 16 + +enum uhc_dwc2_event { + /* No event has occurred or the event is no longer valid */ + UHC_DWC2_EVENT_NONE, + /* 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 has completed reset and enabled on the port */ + UHC_DWC2_EVENT_PORT_ENABLED, + /* 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, +}; + +enum uhc_dwc2_speed { + UHC_DWC2_SPEED_HIGH = 0, + UHC_DWC2_SPEED_FULL = 1, + UHC_DWC2_SPEED_LOW = 2, +}; + +enum uhc_dwc2_xfer_type { + UHC_DWC2_XFER_TYPE_CTRL = 0, + UHC_DWC2_XFER_TYPE_ISOCHRONOUS = 1, + UHC_DWC2_XFER_TYPE_BULK = 2, + UHC_DWC2_XFER_TYPE_INTR = 3, +}; + +enum uhc_dwc2_channel_pid { + UHC_DWC2_PID_DATA0 = 0, + UHC_DWC2_PID_DATA2 = 1, + UHC_DWC2_PID_DATA1 = 2, + UHC_DWC2_PID_MDATA_SETUP = 3, +}; + +enum { + EPSIZE_BULK_FS = 64, + EPSIZE_BULK_HS = 512, + + EPSIZE_ISO_FS_MAX = 1023, + EPSIZE_ISO_HS_MAX = 1024, +}; + +#define UHC_DWC2_CHANNEL_REGS(base, chan_idx) \ + ((struct usb_dwc2_host_chan *)(((mem_addr_t)(base)) + USB_DWC2_HCCHAR(chan_idx))) + +#define UHC_DWC2_FIFODEPTH(config) \ + ((uint32_t)(((config)->ghwcfg3 & USB_DWC2_GHWCFG3_DFIFODEPTH_MASK) >> \ + USB_DWC2_GHWCFG3_DFIFODEPTH_POS)) + +#define UHC_DWC2_NUMHSTCHNL(config) \ + ((uint32_t)(((config)->ghwcfg2 & USB_DWC2_GHWCFG2_NUMHSTCHNL_MASK) >> \ + USB_DWC2_GHWCFG2_NUMHSTCHNL_POS)) + + +/* 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) + +struct uhc_dwc2_channel { + /* Index of the channel */ + uint8_t index; + /* 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 transfer stage */ + enum uhc_control_stage ctrl_stg; + + /* Associated endpoint characteristics */ + /* Type of endpoint */ + enum uhc_dwc2_xfer_type type; + /* Address */ + uint8_t ep_addr; + /* Device Address */ + uint8_t dev_addr; + /* Maximum Packet Size */ + uint16_t ep_mps; + + /* Channel flags */ + /* Channel is claimed and has associated endpoint */ + uint8_t claimed: 1; + /* Transfer stage is IN */ + uint8_t data_stg_in: 1; + /* Transfer has no data stage */ + uint8_t data_stg_skip: 1; + /* Transfer will change the device address */ + uint8_t set_address: 1; + /* Channel executing transfer */ + uint8_t executing: 1; +}; + +struct uhc_dwc2_data { + struct k_thread thread; + /* Event bitmask the driver thread waits for */ + struct k_event event; + /* Port channels */ + struct uhc_dwc2_channel channel[MAX_CHANNELS]; + /* Bit mask of channels with pending interrupts */ + uint32_t pending_channels_msk; + /* Root Port flags */ + uint8_t debouncing: 1; + uint8_t has_device: 1; +}; + +/* TODO: search in case of present helper function like this */ +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 enum uhc_dwc2_speed dwc2_hal_get_speed(struct usb_dwc2_reg *const dwc2) +{ + uint32_t hprt = sys_read32((mem_addr_t)&dwc2->hprt); + uint8_t speed = usb_dwc2_get_hprt_prtspd(hprt); + + switch (speed) { + case USB_DWC2_HPRT_PRTSPD_HIGH: + return UHC_DWC2_SPEED_HIGH; + case USB_DWC2_HPRT_PRTSPD_FULL: + return UHC_DWC2_SPEED_FULL; + case USB_DWC2_HPRT_PRTSPD_LOW: + return UHC_DWC2_SPEED_LOW; + default: + LOG_ERR("Unexpected speed, assign Full-speed"); + break; + } + return UHC_DWC2_SPEED_FULL; +} + +static int dwc2_hal_flush_rx_fifo(struct usb_dwc2_reg *const dwc2, + const k_timeout_t timeout) +{ + const k_timepoint_t timepoint = sys_timepoint_calc(timeout); + + sys_write32(USB_DWC2_GRSTCTL_RXFFLSH, (mem_addr_t)&dwc2->grstctl); + + while (sys_read32((mem_addr_t)&dwc2->grstctl) & USB_DWC2_GRSTCTL_RXFFLSH) { + if (sys_timepoint_expired(timepoint)) { + LOG_ERR("Unable to flush RX FIFO"); + return -EIO; + } + + k_busy_wait(1); + } + + return 0; +} + +static int dwc2_hal_flush_tx_fifo(struct usb_dwc2_reg *const dwc2, + const uint8_t fnum, + const k_timeout_t timeout) +{ + const k_timepoint_t timepoint = sys_timepoint_calc(timeout); + 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) { + if (sys_timepoint_expired(timepoint)) { + LOG_ERR("Unable to flush TX FIFO"); + return -EIO; + } + + k_busy_wait(1); + } + + return 0; +} + +static inline void dwc2_hal_enable_port(struct usb_dwc2_reg *const dwc2, + const enum uhc_dwc2_speed speed) +{ + uint32_t hcfg = sys_read32((mem_addr_t)&dwc2->hcfg); + uint32_t hfir = sys_read32((mem_addr_t)&dwc2->hfir); + uint32_t ghwcfg2 = sys_read32((mem_addr_t)&dwc2->ghwcfg2); + uint8_t fslspclksel; + uint16_t frint; + + /* We can select Buffer DMA of Scatter-Gather DMA mode here: Buffer DMA for now */ + hcfg &= ~USB_DWC2_HCFG_DESCDMA; + + /* Disable periodic scheduling, will enable later */ + hcfg &= ~USB_DWC2_HCFG_PERSCHEDENA; + + if (usb_dwc2_get_ghwcfg2_hsphytype(ghwcfg2) == 0) { + /* Indicate to the OTG core what speed the PHY clock is at + * Note: FSLS PHY has an implicit 8 divider applied when in LS mode, + * so the values of FSLSPclkSel and FrInt have to be adjusted accordingly. + */ + fslspclksel = (speed == UHC_DWC2_SPEED_FULL) ? 1 : 2; + hcfg &= ~USB_DWC2_HCFG_FSLSPCLKSEL_MASK; + hcfg |= (fslspclksel << USB_DWC2_HCFG_FSLSPCLKSEL_POS); + + /* Disable dynamic loading */ + hfir &= ~USB_DWC2_HFIR_HFIRRLDCTRL; + /* Set frame interval to be equal to 1ms + * Note: FSLS PHY has an implicit 8 divider applied when in LS mode, + * so the values of FSLSPclkSel and FrInt have to be adjusted accordingly. + */ + frint = (speed == UHC_DWC2_SPEED_FULL) ? 48000 : 6000; + hfir &= ~USB_DWC2_HFIR_FRINT_MASK; + hfir |= (frint << USB_DWC2_HFIR_FRINT_POS); + + sys_write32(hcfg, (mem_addr_t)&dwc2->hcfg); + sys_write32(hfir, (mem_addr_t)&dwc2->hfir); + } else { + LOG_ERR("Configuring clocks for HS PHY is not implemented"); + } +} + +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(10)); + + /* 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) { + if (sys_timepoint_expired(timepoint)) { + LOG_ERR("Wait for Host mode timeout, GINTSTS 0x%08x", + sys_read32((mem_addr_t)&dwc2->gintsts)); + return -EIO; + } + + k_busy_wait(1); + } + + if (usb_dwc2_get_ghwcfg2_hsphytype(ghwcfg2) != 0) { + /* De-select 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 (external) */ + 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 (internal) */ + 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); + } else { + LOG_DBG("Fullspeed PHY init"); + sys_set_bits((mem_addr_t)&dwc2->gusbcfg, USB_DWC2_GUSBCFG_PHYSEL_USB11); + } + + return 0; +} + +static inline int dwc2_hal_init_gahbcfg(struct usb_dwc2_reg *const dwc2) + +{ + uint32_t ghwcfg2 = sys_read32((mem_addr_t)&dwc2->ghwcfg2); + + /* Disable Global Interrupt */ + sys_clear_bits((mem_addr_t)&dwc2->gahbcfg, USB_DWC2_GAHBCFG_GLBINTRMASK); + + /* TODO: Set AHB burst mode for some ECO only for ESP32S2 */ + /* Make config quirk? */ + + /* TODO: Disable HNP and SRP capabilities */ + /* Also move to quirk? */ + + /* Configure AHB */ + uint32_t gahbcfg = USB_DWC2_GAHBCFG_NPTXFEMPLVL | + usb_dwc2_set_gahbcfg_hbstlen(USB_DWC2_GAHBCFG_HBSTLEN_INCR16); + sys_write32(gahbcfg, (mem_addr_t)&dwc2->gahbcfg); + + 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_hal_core_reset(struct usb_dwc2_reg *const dwc2, const k_timeout_t timeout) +{ + const k_timepoint_t timepoint = sys_timepoint_calc(timeout); + + /* Check AHB master idle state */ + while ((sys_read32((mem_addr_t)&dwc2->grstctl) & USB_DWC2_GRSTCTL_AHBIDLE) == 0) { + if (sys_timepoint_expired(timepoint)) { + LOG_ERR("Wait for AHB idle timeout, GRSTCTL 0x%08x", + sys_read32((mem_addr_t)&dwc2->grstctl)); + return -EIO; + } + + k_busy_wait(1); + } + + /* Apply Core Soft Reset */ + sys_write32(USB_DWC2_GRSTCTL_CSFTRST, (mem_addr_t)&dwc2->grstctl); + + /* Wait for reset to complete */ + while ((sys_read32((mem_addr_t)&dwc2->grstctl) & USB_DWC2_GRSTCTL_CSFTRST) != 0 && + (sys_read32((mem_addr_t)&dwc2->grstctl) & USB_DWC2_GRSTCTL_CSFTRSTDONE) == 0) { + if (sys_timepoint_expired(timepoint)) { + LOG_ERR("Wait for CSR done timeout, GRSTCTL 0x%08x", + sys_read32((mem_addr_t)&dwc2->grstctl)); + return -EIO; + } + + k_busy_wait(1); + } + + /* CSFTRSTDONE is W1C so the write must have the bit set to clear it */ + sys_clear_bits((mem_addr_t)&dwc2->grstctl, USB_DWC2_GRSTCTL_CSFTRST); + + LOG_DBG("DWC2 core reset done"); + + return 0; +} + +static int dwc2_hal_init_host(struct usb_dwc2_reg *const dwc2) +{ + int ret; + + /* Init GUSBCFG */ + ret = dwc2_hal_init_gusbcfg(dwc2); + + if (ret != 0) { + LOG_ERR("Unable to configure USB global register"); + return ret; + } + + /* Init GAHBCFG */ + 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; + } + + /* Clear interrupts */ + sys_clear_bits((mem_addr_t)&dwc2->gintmsk, 0xFFFFFFFFUL); + sys_set_bits((mem_addr_t)&dwc2->gintmsk, USB_DWC2_GINTSTS_DISCONNINT); + + /* Clear status */ + uint32_t core_intrs = sys_read32((mem_addr_t)&dwc2->gintsts); + + sys_write32(core_intrs, (mem_addr_t)&dwc2->gintsts); + + return ret; +} + +static int dwc2_hal_set_fifo_sizes(struct usb_dwc2_reg *const dwc2, + const uint32_t ghwcfg2, + const uint32_t ghwcfg3) +{ + 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); + /* TODO: support HS */ + 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, K_MSEC(10)); + if (ret) { + return ret; + } + + return dwc2_hal_flush_rx_fifo(dwc2, K_MSEC(10)); +} + +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 bool uhc_dwc2_channel_xfer_is_done(struct uhc_dwc2_channel *channel) +{ + /* Only control transfers need to be handled in stages */ + if (channel->type != UHC_DWC2_XFER_TYPE_CTRL) { + return true; + } + + /* Is done when we finished the UHC_CONTROL_STAGE_STATUS */ + return (channel->ctrl_stg == UHC_CONTROL_STAGE_STATUS); +} + +static inline void uhc_dwc2_channel_process_ctrl(struct uhc_dwc2_channel *channel) +{ + struct uhc_transfer *const xfer = channel->xfer; + bool next_dir_is_in; + enum uhc_dwc2_channel_pid next_pid; + uint16_t size = 0; + uint16_t pkt_cnt; + uint8_t *dma_addr = NULL; + uint32_t hcchar; + uint32_t hctsiz; + + if (channel->ctrl_stg == UHC_CONTROL_STAGE_SETUP) { + /* Just finished UHC_CONTROL_STAGE_SETUP */ + if (channel->data_stg_skip) { + /* No data stage. Go strait to status */ + next_dir_is_in = true; + next_pid = UHC_DWC2_PID_DATA1; + channel->ctrl_stg = UHC_CONTROL_STAGE_STATUS; + } else { + /* Data stage is present, go to data stage */ + next_dir_is_in = channel->data_stg_in; + next_pid = UHC_DWC2_PID_DATA1; + channel->ctrl_stg = UHC_CONTROL_STAGE_DATA; + + /* + * NOTE: + * For OUT - number of bytes host sends to device + * For IN - number of bytes host reserves to receive + */ + if (!next_dir_is_in) { + size = xfer->buf->len; + } else { + /* TODO: Check the buffer is large enough for the next transfer? */ + size = net_buf_tailroom(xfer->buf); + } + + /* TODO: Toggle PID? */ + + if (xfer->buf != NULL) { + /* Get the tail of the buffer to append data */ + dma_addr = net_buf_tail(xfer->buf); + } + } + } else { + /* Just finished UHC_CONTROL_STAGE_DATA */ + hctsiz = sys_read32((mem_addr_t)&channel->regs->hctsiz); + /* Increase the net_buf for the actual transferred len */ + net_buf_add(xfer->buf, xfer->buf->size - usb_dwc2_get_hctsiz_xfersize(hctsiz)); + /* Status stage is always the opposite direction of data stage */ + next_dir_is_in = !channel->data_stg_in; + /* Status stage always has a PID of DATA1 */ + next_pid = UHC_DWC2_PID_DATA1; + channel->ctrl_stg = UHC_CONTROL_STAGE_STATUS; + } + + /* Calculate new packet count */ + pkt_cnt = calc_packet_count(size, channel->ep_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(next_pid) | + 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 uint32_t uhc_dwc2_channel_irq_handle_events(struct uhc_dwc2_channel *channel) +{ + uint32_t hcint = sys_read32((mem_addr_t)&channel->regs->hcint); + uint32_t channel_events = 0; + + /* Clear the interrupt bits by writing them back */ + sys_write32(hcint, (mem_addr_t)&channel->regs->hcint); + + /* TODO: Read hcchar and split paths for EP IN and OUT? */ + + /* TODO: Read hcchar and split paths for BULK/CTRL/ISOC/INTR? */ + + if (hcint & USB_DWC2_HCINT_CHHLTD) { + if (hcint & USB_DWC2_HCINT_XFERCOMPL) { + /* TODO: channel error count = 0 */ + + if (!uhc_dwc2_channel_xfer_is_done(channel)) { + /* Control transfer isn't finished yet - continue in ISR */ + uhc_dwc2_channel_process_ctrl(channel); + } else { + /* Transfer finished, notify thread */ + channel_events |= BIT(UHC_DWC2_CHANNEL_DO_RELEASE); + channel_events |= BIT(UHC_DWC2_CHANNEL_EVENT_CPLT); + } + } else if (hcint & USB_DWC2_HCINT_STALL) { + /* TODO: channel error count = 0 */ + + /* Expecting ACK interrupt next */ + sys_set_bits((mem_addr_t)&channel->regs->hcintmsk, USB_DWC2_HCINT_ACK); + + /* Notify thread */ + channel_events |= BIT(UHC_DWC2_CHANNEL_DO_RELEASE); + channel_events |= BIT(UHC_DWC2_CHANNEL_EVENT_STALL); + } + } else if (hcint & USB_DWC2_HCINT_ACK) { + /* TODO: channel error count = 1 */ + + /* Not expecting ACK interrupt anymore */ + sys_clear_bits((mem_addr_t)&channel->regs->hcintmsk, USB_DWC2_HCINT_ACK); + } else { + LOG_WRN("Channel has not been handled: HCINT=0x%08x", hcint); + } + + return channel_events; +} + +static enum uhc_dwc2_event uhc_dwc2_port_get_event(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; + + /* Read and clear core interrupt status */ + uint32_t core_intrs = sys_read32((mem_addr_t)&dwc2->gintsts); + + sys_write32(core_intrs, (mem_addr_t)&dwc2->gintsts); + + uint32_t port_intrs = 0; + + if (core_intrs & USB_DWC2_GINTSTS_PRTINT) { + /* Port interrupt occurred -> read and clear selected HPRT W1C bits */ + port_intrs = sys_read32((mem_addr_t)&dwc2->hprt); + /* W1C changed bits except the PRTENA */ + sys_write32(port_intrs & ~USB_DWC2_HPRT_PRTENA, (mem_addr_t)&dwc2->hprt); + } + + LOG_DBG("GINTSTS=%08Xh, HPRT=%08Xh", core_intrs, port_intrs); + + /* Handle disconnect first */ + if (core_intrs & USB_DWC2_GINTSTS_DISCONNINT) { + /* Port disconnected */ + uhc_dwc2_port_debounce_lock(dev); + return UHC_DWC2_EVENT_PORT_DISCONNECTION; + } + + /* To have better throughput, handle channels right after disconnection */ + if (core_intrs & USB_DWC2_GINTSTS_HCHINT) { + priv->pending_channels_msk = sys_read32((mem_addr_t)&dwc2->haint); + return UHC_DWC2_EVENT_PORT_PEND_CHANNEL; + } + + /* Handle port overcurrent as it is a failure state */ + if (port_intrs & USB_DWC2_HPRT_PRTOVRCURRCHNG) { + /* TODO: Overcurrent or overcurrent clear? */ + return UHC_DWC2_EVENT_PORT_OVERCURRENT; + } + + /* Handle port change bits */ + if (port_intrs & USB_DWC2_HPRT_PRTENCHNG) { + if (port_intrs & USB_DWC2_HPRT_PRTENA) { + /* Host port was enabled */ + return UHC_DWC2_EVENT_PORT_ENABLED; + } + /* Host port has been disabled */ + return UHC_DWC2_EVENT_PORT_DISABLED; + } + + /* Handle port connection */ + if (port_intrs & USB_DWC2_HPRT_PRTCONNDET && !priv->debouncing) { + /* Port connected */ + uhc_dwc2_port_debounce_lock(dev); + return UHC_DWC2_EVENT_PORT_CONNECTION; + } + + return UHC_DWC2_EVENT_NONE; +} + +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; + + /** + * HINT: Do 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_power_on(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_power_off(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 void uhc_dwc2_port_reset(const struct device *dev) +{ + const struct uhc_dwc2_config *const config = dev->config; + struct usb_dwc2_reg *const dwc2 = config->base; + + /* 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 the port time to recover */ + k_msleep(RESET_RECOVERY_MS); + + /* TODO: verify the port state after reset */ + + /* Finish the port config for the appeared device */ + /* TODO: set frame list for the ISOC/INTR xfer */ + /* TODO: enable periodic transfer */ +} + +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_hal_core_reset(dwc2, K_MSEC(10)); + if (ret) { + 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 *dev, + struct uhc_transfer *const xfer, + struct uhc_dwc2_channel **channel_hdl) +{ + const struct uhc_dwc2_config *const config = dev->config; + struct uhc_dwc2_data *priv = uhc_get_private(dev); + struct usb_dwc2_reg *const dwc2 = config->base; + const struct usb_device *udev = xfer->udev; + uint32_t hcint; + uint32_t hcchar; + + /* TODO: select non-claimed channel, use channel 0 for now */ + uint8_t idx = 0; + + struct uhc_dwc2_channel *channel = &priv->channel[idx]; + + switch (xfer->type) { + case USB_EP_TYPE_CONTROL: + channel->type = UHC_DWC2_XFER_TYPE_CTRL; + break; + case USB_EP_TYPE_BULK: + channel->type = UHC_DWC2_XFER_TYPE_BULK; + break; + case USB_EP_TYPE_INTERRUPT: + channel->type = UHC_DWC2_XFER_TYPE_INTR; + break; + case USB_EP_TYPE_ISO: + channel->type = UHC_DWC2_XFER_TYPE_ISOCHRONOUS; + break; + default: + return -EINVAL; + } + + /* Save channel characteristics underlying channel */ + channel->xfer = xfer; + channel->ep_addr = xfer->ep; + channel->dev_addr = udev->addr; + channel->ep_mps = xfer->mps; + + /* Claim channel */ + channel->claimed = 1; + + /* Init underlying channel registers */ + + /* 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, (1 << idx)); + + /* Enable transfer complete and channel halted interrupts */ + sys_set_bits((mem_addr_t)&channel->regs->hcintmsk, USB_DWC2_HCINT_XFERCOMPL | + USB_DWC2_HCINT_CHHLTD); + + hcchar = usb_dwc2_set_hcchar_mps(channel->ep_mps) | + usb_dwc2_set_hcchar_epnum(USB_EP_GET_IDX(channel->ep_addr)) | + usb_dwc2_set_hcchar_eptype(channel->type) | + usb_dwc2_set_hcchar_ec(1UL /* TODO: ep_config->mult */) | + usb_dwc2_set_hcchar_devaddr(channel->dev_addr); + + if (USB_EP_DIR_IS_IN(channel->ep_addr)) { + hcchar |= USB_DWC2_HCCHAR_EPDIR; + } + + if (false /* TODO: Support Hubs channel->ls_via_fs_hub */) { + hcchar |= USB_DWC2_HCCHAR_LSPDDEV; + } + + if (channel->type == UHC_DWC2_XFER_TYPE_INTR) { + hcchar |= USB_DWC2_HCCHAR_ODDFRM; + } + + sys_write32(hcchar, (mem_addr_t)&channel->regs->hcchar); + + LOG_DBG("Claimed channel%d", idx); + + *channel_hdl = channel; + 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; + channel->claimed = 0; + + LOG_DBG("Released channel%d", channel->index); + + return 0; +} + +static int uhc_dwc2_channel_start_transfer_ctrl(struct uhc_dwc2_channel *channel) +{ + /* Get information about the control transfer by analyzing the setup packet */ + struct uhc_transfer *const xfer = channel->xfer; + const struct usb_setup_packet *setup_pkt = (const struct usb_setup_packet *)xfer->setup_pkt; + uint16_t pkt_cnt; + uint32_t hctsiz; + uint32_t hcint; + uint32_t hcchar; + + LOG_HEXDUMP_DBG(xfer->setup_pkt, 8, "setup"); + + channel->ctrl_stg = UHC_CONTROL_STAGE_SETUP; + channel->data_stg_in = usb_reqtype_is_to_host(setup_pkt); + channel->data_stg_skip = (setup_pkt->wLength == 0); + channel->set_address = (setup_pkt->bRequest == USB_SREQ_SET_ADDRESS) ? 1 : 0; + + LOG_DBG("data_in: %s, data_skip: %s", + channel->data_stg_in ? "true" : "false", + channel->data_stg_skip ? "true" : "false"); + + if (USB_EP_GET_IDX(channel->ep_addr) == 0) { + /* Control stage is always OUT */ + sys_clear_bits((mem_addr_t)&channel->regs->hcchar, USB_DWC2_HCCHAR_EPDIR); + } + + if (xfer->interval != 0) { + LOG_WRN("Periodic transfer is not supported"); + } + + pkt_cnt = calc_packet_count(sizeof(struct usb_setup_packet), channel->ep_mps); + hctsiz = usb_dwc2_set_hctsiz_pid(UHC_DWC2_PID_MDATA_SETUP) | + usb_dwc2_set_hctsiz_pktcnt(pkt_cnt) | + usb_dwc2_set_hctsiz_xfersize(sizeof(struct usb_setup_packet)); + + 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); + + channel->executing = 1; + + return 0; +} + +struct uhc_dwc2_channel *uhc_dwc2_channel_get_pending(const struct device *dev) +{ + struct uhc_dwc2_data *priv = uhc_get_private(dev); + int chan_num = __builtin_ffs(priv->pending_channels_msk); + + if (chan_num) { + /* Clear the pending bit for that channel */ + priv->pending_channels_msk &= ~(1 << (chan_num - 1)); + return &priv->channel[chan_num - 1]; + } else { + return NULL; + } +} + +static inline void uhc_dwc2_submit_new_device(const struct device *dev, enum uhc_dwc2_speed speed) +{ + static const char *const uhc_dwc2_speed_str[] = {"High", "Full", "Low"}; + enum uhc_event_type type; + + LOG_DBG("New device, %s-speed", uhc_dwc2_speed_str[speed]); + + switch (speed) { + case UHC_DWC2_SPEED_LOW: + type = UHC_EVT_DEV_CONNECTED_LS; + break; + case UHC_DWC2_SPEED_FULL: + type = UHC_EVT_DEV_CONNECTED_FS; + break; + case UHC_DWC2_SPEED_HIGH: + type = UHC_EVT_DEV_CONNECTED_HS; + break; + default: + LOG_ERR("Unsupported speed %d", speed); + return; + } + + uhc_submit_event(dev, type, 0); +} + +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; + } + + LOG_DBG("Device removed"); + + uhc_submit_event(dev, UHC_EVT_DEV_REMOVED, 0); + + priv->has_device = 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("ep=%02Xh, mps=%d, int=%d, start_frame=%d, stage=%d, no_status=%d", + xfer->ep, xfer->mps, xfer->interval, + xfer->start_frame, xfer->stage, xfer->no_status); + + /* TODO: dma requirement, setup packet must be aligned 4 bytes */ + if (((uintptr_t)xfer->setup_pkt % 4)) { + LOG_WRN("Setup packet address %p is not 4-byte aligned", + xfer->setup_pkt); + } + + /* TODO: dma requirement, buffer addr that will used as dma addr also should be aligned */ + if ((xfer->buf != NULL) && ((uintptr_t)net_buf_tail(xfer->buf) % 4)) { + LOG_WRN("Buffer address %08lXh is not 4-byte aligned", + (uintptr_t)net_buf_tail(xfer->buf)); + } + + ret = uhc_dwc2_channel_claim(dev, xfer, &channel); + if (ret) { + LOG_ERR("Failed to claim channel: %d", ret); + return ret; + } + + switch (xfer->type) { + case USB_EP_TYPE_CONTROL: + ret = uhc_dwc2_channel_start_transfer_ctrl(channel); + break; + case USB_EP_TYPE_BULK: + case USB_EP_TYPE_INTERRUPT: + case USB_EP_TYPE_ISO: + default: + LOG_WRN("Channel type %d isn't supported yet", xfer->type); + uhc_dwc2_channel_release(dev, channel); + return -EINVAL; + } + + return ret; +} + +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; + struct uhc_dwc2_data *const priv = uhc_get_private(dev); + + 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)) { + /* Get port speed */ + enum uhc_dwc2_speed speed = dwc2_hal_get_speed(dwc2); + /* Notify the higher logic about the new device */ + uhc_dwc2_submit_new_device(dev, speed); + /* Mark that port has a device */ + priv->has_device = 1; + } else { + /* TODO: Implement handling */ + LOG_ERR("Port changed during debouncing connect"); + } + } + + if (event_mask & BIT(UHC_DWC2_EVENT_PORT_ENABLED)) { + /* Port has a device connected and finish the first reset, we know the speed */ + LOG_DBG("Port enabled"); + /* Enable the rest of the port */ + enum uhc_dwc2_speed speed = dwc2_hal_get_speed(dwc2); + + dwc2_hal_enable_port(dwc2, speed); + } + + 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_power_on(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_power_on(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; + int err; + + LOG_DBG("Thread channel%d events: %08Xh", channel->index, events); + + channel->executing = 0; + + if (events & BIT(UHC_DWC2_CHANNEL_EVENT_CPLT)) { + /** + * HINT: When device is processing SetAddress(), delay should be applied + */ + if (channel->set_address) { + k_msleep(SET_ADDR_DELAY_MS); + } + /* No error */ + err = 0; + } + + if (events & BIT(UHC_DWC2_CHANNEL_EVENT_STALL)) { + /* Request STALLed */ + err = -EPIPE; + } + + if (events & BIT(UHC_DWC2_CHANNEL_EVENT_ERROR)) { + LOG_ERR("Channel%d error", channel->index); + /* TODO: Channel has an error */ + LOG_WRN("Channel error handing has not been implemented yet"); + } + + if (events & BIT(UHC_DWC2_CHANNEL_DO_RELEASE)) { + uhc_dwc2_channel_release(dev, channel); + } + + /* Notify the upper logic */ + uhc_xfer_return(dev, xfer, err); +} + +static void uhc_dwc2_isr_handler(const struct device *dev) +{ + struct uhc_dwc2_data *const priv = uhc_get_private(dev); + struct uhc_dwc2_channel *channel = NULL; + enum uhc_dwc2_event port_event = uhc_dwc2_port_get_event(dev); + uint32_t channel_events = 0; + + /** + * TODO: port_event serialization might be simplified by + * obsoleting uhc_dwc2_port_get_event() but after the implementation of all channels type. + */ + + switch (port_event) { + case UHC_DWC2_EVENT_NONE: { + /* Port event occurred but should not be handled in higher logic */ + break; + } + case UHC_DWC2_EVENT_PORT_PEND_CHANNEL: { + /* Handle pending channel event */ + channel = uhc_dwc2_channel_get_pending(dev); + while (channel != NULL) { + channel_events = uhc_dwc2_channel_irq_handle_events(channel); + if (channel_events) { + /** + * The rest channel events be handled by the thread + */ + atomic_or(&channel->events, channel_events); + k_event_set(&priv->event, + BIT(UHC_DWC2_EVENT_PORT_PEND_CHANNEL + channel->index)); + } + channel = uhc_dwc2_channel_get_pending(dev); + } + break; + } + default: + /* Notify thread about port event */ + k_event_set(&priv->event, BIT(port_event)); + break; + } + + (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->event, 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->channel[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) +{ + uhc_dwc2_port_reset(dev); + + 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; + struct uhc_data *const data = dev->data; + int ret; + + /* Initialize the private data structure */ + + memset(priv, 0, sizeof(struct uhc_dwc2_data)); + k_mutex_init(&data->mutex); + k_event_init(&priv->event); + + ret = uhc_dwc2_quirk_pre_init(dev); + if (ret) { + LOG_ERR("Quirk pre init failed %d", ret); + return ret; + } + + for (uint32_t idx = 0; idx < MAX_CHANNELS; idx++) { + priv->channel[idx].regs = UHC_DWC2_CHANNEL_REGS(dwc2, idx); + priv->channel[idx].index = 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); + + 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 reg; + int ret; + + ret = uhc_dwc2_quirk_init(dev); + if (ret) { + LOG_ERR("Quirk init failed %d", ret); + return ret; + } + + /* Read hardware configuration registers */ + + reg = sys_read32((mem_addr_t)&dwc2->gsnpsid); + if (reg != config->gsnpsid) { + LOG_ERR("Unexpected GSNPSID 0x%08x instead of 0x%08x", reg, config->gsnpsid); + return -ENOTSUP; + } + + reg = sys_read32((mem_addr_t)&dwc2->ghwcfg1); + if (reg != config->ghwcfg1) { + LOG_ERR("Unexpected GHWCFG1 0x%08x instead of 0x%08x", reg, config->ghwcfg1); + return -ENOTSUP; + } + + reg = sys_read32((mem_addr_t)&dwc2->ghwcfg2); + if (reg != config->ghwcfg2) { + LOG_ERR("Unexpected GHWCFG2 0x%08x instead of 0x%08x", reg, config->ghwcfg2); + return -ENOTSUP; + } + + reg = sys_read32((mem_addr_t)&dwc2->ghwcfg3); + if (reg != config->ghwcfg3) { + LOG_ERR("Unexpected GHWCFG3 0x%08x instead of 0x%08x", reg, config->ghwcfg3); + return -ENOTSUP; + } + + reg = sys_read32((mem_addr_t)&dwc2->ghwcfg4); + if (reg != config->ghwcfg4) { + LOG_ERR("Unexpected GHWCFG4 0x%08x instead of 0x%08x", reg, config->ghwcfg4); + return -ENOTSUP; + } + + /* Select PHY & Core reset */ + + ret = uhc_dwc2_quirk_phy_pre_select(dev); + if (ret) { + LOG_ERR("Quirk PHY pre select failed %d", ret); + return ret; + } + + ret = dwc2_hal_core_reset(dwc2, K_MSEC(10)); + if (ret) { + LOG_ERR("DWC2 core reset failed after PHY init: %d", ret); + return ret; + } + + ret = uhc_dwc2_quirk_phy_post_select(dev); + if (ret) { + LOG_ERR("Quirk PHY post select failed %d", ret); + return ret; + } + + /* FIFO */ + + ret = dwc2_hal_set_fifo_sizes(dwc2, config->ghwcfg2, config->ghwcfg3); + if (ret) { + LOG_ERR("Unable to configure FIFO"); + return ret; + } + + /* Init controller as a USB host */ + + return dwc2_hal_init_host(dwc2); +} + +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_power_on(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_power_off(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) +{ + LOG_WRN("%s has not been implemented", __func__); + + return -ENOSYS; +} + +/* + * 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 = { 0 }; \ + \ + 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), \ + .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, \ + .gsnpsid = DT_INST_PROP(n, gsnpsid), \ + .ghwcfg1 = DT_INST_PROP(n, ghwcfg1), \ + .ghwcfg2 = DT_INST_PROP(n, ghwcfg2), \ + .ghwcfg3 = DT_INST_PROP(n, ghwcfg3), \ + .ghwcfg4 = DT_INST_PROP(n, ghwcfg4), \ + }; \ + \ + 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..db70783ad686f --- /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() before the thread is initialized */ + int (*pre_init)(const struct device *dev); + /* Called at the beginning of uhc_dwc2_init() */ + int (*init)(const struct device *dev); + /* Called on uhc_dwc2_enable() before the controller is initialized */ + int (*pre_enable)(const struct device *dev); + /* Called on uhc_dwc2_enable() after the controller is initialized */ + 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 on PHY pre-select */ + int (*phy_pre_select)(const struct device *dev); + /* Called on PHY post-select and core reset */ + int (*phy_post_select)(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; + /* Pointer to vendor quirks or NULL */ + const struct uhc_dwc2_vendor_quirks *const quirks; + /* 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); + + /* Hardware configuration registers */ + uint32_t gsnpsid; + uint32_t ghwcfg1; + uint32_t ghwcfg2; + uint32_t ghwcfg3; + uint32_t ghwcfg4; +}; + +#if DT_HAS_COMPAT_STATUS_OKAY(espressif_esp32_usb_otg) +#include "uhc_dwc2_esp32_usb_otg.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 *dev) \ +{ \ + const struct uhc_dwc2_config *const config = dev->config; \ + const struct uhc_dwc2_vendor_quirks *const quirks = \ + COND_CODE_1(IS_EQ(DT_NUM_INST_STATUS_OKAY(snps_dwc2), 1), \ + (UHC_DWC2_VENDOR_QUIRK_GET(0); ARG_UNUSED(config);), \ + (config->quirks;)) \ + if (quirks != NULL && quirks->fname != NULL) { \ + return quirks->fname(dev); \ + } \ + return 0; \ +} + +DWC2_QUIRK_FUNC_DEFINE(pre_init) +DWC2_QUIRK_FUNC_DEFINE(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(phy_pre_select) +DWC2_QUIRK_FUNC_DEFINE(phy_post_select) + +#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..c7c9ea449be41 --- /dev/null +++ b/drivers/usb/uhc/uhc_dwc2_esp32_usb_otg.h @@ -0,0 +1,185 @@ +/* + * 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 phy_context_t { + usb_phy_target_t target; + usb_phy_speed_t otg_speed; + usb_phy_ext_io_conf_t *ext_io_pins; + usb_wrap_hal_context_t wrap_hal; +}; + +struct usb_dw_esp32_config { + const struct device *clock_dev; + const clock_control_subsys_t clock_subsys; + int irq_source; + int irq_priority; + int irq_flags; + struct phy_context_t *phy_ctx; +}; + +struct usb_dw_esp32_data { + struct intr_handle_data_t *int_handle; +}; + +static void uhc_dwc2_isr_handler(const struct device *dev); + +static inline int esp32_usb_otg_init(const struct device *dev, + const struct usb_dw_esp32_config *cfg, + struct usb_dw_esp32_data *data) +{ + LOG_MODULE_DECLARE(uhc_dwc2, CONFIG_UHC_DRIVER_LOG_LEVEL); + + 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); + + if (cfg->phy_ctx->target == USB_PHY_TARGET_INT) { + 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); + } + + /* 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 inline int esp32_usb_otg_enable_phy(struct phy_context_t *phy_ctx, bool enable) +{ + LOG_MODULE_DECLARE(uhc_dwc2, CONFIG_UHC_DRIVER_LOG_LEVEL); + + if (enable) { + usb_wrap_hal_init(&phy_ctx->wrap_hal); + usb_wrap_ll_phy_enable_pad(phy_ctx->wrap_hal.dev, true); + +#if USB_WRAP_LL_EXT_PHY_SUPPORTED + usb_wrap_hal_phy_set_external(&phy_ctx->wrap_hal, + (phy_ctx->target == USB_PHY_TARGET_EXT)); +#endif + if (phy_ctx->target == USB_PHY_TARGET_INT) { + /* Configure pull resistors for host */ + 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(&phy_ctx->wrap_hal, &vals); + } + LOG_DBG("PHY enabled"); + } else { + usb_wrap_hal_disable(); + usb_wrap_ll_phy_enable_pad(phy_ctx->wrap_hal.dev, false); + + LOG_DBG("PHY disabled"); + } + return 0; +} + +#define QUIRK_ESP32_USB_OTG_DEFINE(n) \ + \ + static struct phy_context_t phy_ctx_##n = { \ + .target = USB_PHY_TARGET_INT, \ + .otg_speed = USB_PHY_SPEED_UNDEFINED, \ + .ext_io_pins = NULL, \ + .wrap_hal = {}, \ + }; \ + \ + static const struct usb_dw_esp32_config usb_otg_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_ctx = &phy_ctx_##n, \ + }; \ + \ + static struct usb_dw_esp32_data usb_otg_data_##n; \ + \ + static int esp32_usb_otg_pre_init_##n(const struct device *dev) \ + { \ + return esp32_usb_otg_enable_phy(&phy_ctx_##n, true); \ + } \ + \ + static int esp32_usb_otg_init_##n(const struct device *dev) \ + { \ + return esp32_usb_otg_init(dev, \ + &usb_otg_config_##n, &usb_otg_data_##n); \ + } \ + \ + static int esp32_usb_otg_shutdown_##n(const struct device *dev) \ + { \ + return esp32_usb_otg_enable_phy(&phy_ctx_##n, false); \ + } \ + \ + const struct uhc_dwc2_vendor_quirks uhc_dwc2_vendor_quirks_##n = { \ + .pre_init = esp32_usb_otg_pre_init_##n, \ + .init = esp32_usb_otg_init_##n, \ + .shutdown = esp32_usb_otg_shutdown_##n, \ + }; + +#define UHC_DWC2_IRQ_DT_INST_DEFINE(n) \ + static void uhc_dwc2_irq_enable_func_##n(const struct device *dev) \ + { \ + esp_intr_enable(usb_otg_data_##n.int_handle); \ + } \ + \ + static void uhc_dwc2_irq_disable_func_##n(const struct device *dev) \ + { \ + esp_intr_disable(usb_otg_data_##n.int_handle); \ + } + +DT_INST_FOREACH_STATUS_OKAY(QUIRK_ESP32_USB_OTG_DEFINE) + +#endif /* ZEPHYR_DRIVERS_USB_UHC_DWC2_ESP32_USB_OTG_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"; +}; From 64b7e21eb246ac55dfabdfaf5ae2eb692053e8f0 Mon Sep 17 00:00:00 2001 From: Roman Leonov Date: Tue, 3 Mar 2026 16:36:36 +0100 Subject: [PATCH 3/4] samples: subsys: usb: add shell HIL pytest for usbh Add host_prj.conf for the applicaiton and test_usbh_shell.py. Add the sample.usbh.shell.hil entry in the usbh subsys sample to be able to run and test usbh in CI with pytest harness for twister. Signed-off-by: Roman Leonov --- samples/subsys/usb/shell/host_prj.conf | 8 ++ .../usb/shell/pytest/test_usbh_shell.py | 121 ++++++++++++++++++ samples/subsys/usb/shell/sample.yaml | 23 +++- 3 files changed, 147 insertions(+), 5 deletions(-) create mode 100644 samples/subsys/usb/shell/host_prj.conf create mode 100644 samples/subsys/usb/shell/pytest/test_usbh_shell.py diff --git a/samples/subsys/usb/shell/host_prj.conf b/samples/subsys/usb/shell/host_prj.conf new file mode 100644 index 0000000000000..ff93f5c95ea9b --- /dev/null +++ b/samples/subsys/usb/shell/host_prj.conf @@ -0,0 +1,8 @@ +CONFIG_SHELL=y + +CONFIG_USB_HOST_STACK=y +CONFIG_USBH_SHELL=y +CONFIG_UDC_DWC2=n +CONFIG_LOG=y +CONFIG_USBH_LOG_LEVEL_WRN=y +CONFIG_UHC_DRIVER_LOG_LEVEL_WRN=y diff --git a/samples/subsys/usb/shell/pytest/test_usbh_shell.py b/samples/subsys/usb/shell/pytest/test_usbh_shell.py new file mode 100644 index 0000000000000..e9f2cd6cccd66 --- /dev/null +++ b/samples/subsys/usb/shell/pytest/test_usbh_shell.py @@ -0,0 +1,121 @@ +# Copyright (c) 2026 Roman Leonov +# +# SPDX-License-Identifier: Apache-2.0 + +import logging +import re +import time + +from twister_harness import Shell + +logger = logging.getLogger(__name__) +ANSI = re.compile(r"\x1B\[[0-?]*[ -/]*[@-~]") + + +def _contains(lines, needle: str) -> bool: + return any(needle in line for line in lines) + + +def _match(lines, pattern: str) -> bool: + rx = re.compile(pattern) + return any(rx.search(line) for line in lines) + + +def wait_for_device(shell, timeout_s=10.0, poll_s=0.5): + end = time.time() + timeout_s + last_resp = [] + while time.time() < end: + last_resp = shell.exec_command("usbh device list") + # There should be decimal number in the response + if any(re.match(r"^\s*\d+\s*$", dev_number) for dev_number in last_resp): + return last_resp + time.sleep(poll_s) + raise AssertionError(f"No USB device within {timeout_s}s. Last output:\n{last_resp}") + + +def test_usbh_init_enable_and_list(shell: Shell): + # usbh init + logger.info('run "usbh init"') + lines = shell.exec_command("usbh init") + assert _contains(lines, "host: USB host initialized"), f"Unexpected init output:\n{lines}" + + # usbh enable + logger.info('run "usbh enable"') + lines = shell.exec_command("usbh enable") + assert _contains(lines, "host: USB host enabled"), f"Unexpected enable output:\n{lines}" + + # usbh device list -> poll the list and expect at least one device + wait_for_device(shell, timeout_s=5.0, poll_s=0.5) + + logger.info('run "usbh device descriptor device 1"') + lines = shell.exec_command("usbh device descriptor device 1") + + # Remove ANSI symbols from the lines + clean_lines = [ANSI.sub("", symbol) for symbol in lines] + + # This is an error message, might gone, verify the length and type instead + assert _contains(clean_lines, "host: USB device with address 1"), ( + f"Missing device header:\n{clean_lines}" + ) + + # Check a few stable anchors of device descriptor + assert _match(clean_lines, r"^\s*bLength\s+18\s*$"), ( + f"Missing Device bLength 18:\n{clean_lines}" + ) + assert _match(clean_lines, r"^\s*bDescriptorType\s+1\s*$"), ( + f"Missing Device bDescriptorType 1:\n{clean_lines}" + ) + + logger.info('run "usbh device descriptor configuration 1 0"') + lines = shell.exec_command("usbh device descriptor configuration 1 0") + + # Remove ANSI symbols from the lines + clean_lines = [ANSI.sub("", symbol) for symbol in lines] + + # Check a few stable anchors of configuration descriptor + assert _match(clean_lines, r"^\s*bLength\s+9\s*$"), f"Missing Config bLength 9:\n{clean_lines}" + assert _match(clean_lines, r"^\s*bDescriptorType\s+2\s*$"), ( + f"Missing Config bDescriptorType 2:\n{clean_lines}" + ) + + # String 1: Manufacturer + logger.info('run "usbh device descriptor string 1 1 1"') + lines = shell.exec_command("usbh device descriptor string 1 1 1") + assert _match(lines, r"^00000000:"), f"Expected hexdump for string 1, got:\n{lines}" + + # String 2: Product + logger.info('run "usbh device descriptor string 1 1 2"') + lines = shell.exec_command("usbh device descriptor string 1 1 2") + assert _match(lines, r"^00000000:"), f"Expected hexdump for string 2, got:\n{lines}" + + # String 3: Serial + logger.info('run "usbh device descriptor string 1 1 3"') + lines = shell.exec_command("usbh device descriptor string 1 1 3") + assert _match(lines, r"^00000000:"), f"Expected hexdump for string 3, got:\n{lines}" + + # String 4: Product + logger.info('run "usbh device descriptor string 1 1 4"') + lines = shell.exec_command("usbh device descriptor string 1 1 4") + assert _match(lines, r"^00000000:"), f"Expected hexdump for string 4, got:\n{lines}" + + # String 5: Product + logger.info('run "usbh device descriptor string 1 1 5"') + lines = shell.exec_command("usbh device descriptor string 1 1 5") + assert _match(lines, r"^00000000:"), f"Expected hexdump for string 5, got:\n{lines}" + + # String 6: STALL string request + logger.info('run "usbh device descriptor string 1 1 6"') + lines = shell.exec_command("usbh device descriptor string 1 1 6") + assert _contains(lines, "host: Failed to request configuration descriptor"), ( + f"Expected failure message for string 6, got:\n{lines}" + ) + + # String 1: Manufacturer + logger.info('run "usbh device descriptor string 1 1 1"') + lines = shell.exec_command("usbh device descriptor string 1 1 1") + assert _match(lines, r"^00000000:"), f"Expected hexdump for string 1, got:\n{lines}" + + # String 2: Product + logger.info('run "usbh device descriptor string 1 1 2"') + lines = shell.exec_command("usbh device descriptor string 1 1 2") + assert _match(lines, r"^00000000:"), f"Expected hexdump for string 2, got:\n{lines}" diff --git a/samples/subsys/usb/shell/sample.yaml b/samples/subsys/usb/shell/sample.yaml index a0a03a8c4c51b..aec9c6f2d1200 100644 --- a/samples/subsys/usb/shell/sample.yaml +++ b/samples/subsys/usb/shell/sample.yaml @@ -1,10 +1,9 @@ sample: name: USB support shell sample -common: - depends_on: - - usbd + tests: sample.usbd.shell: + depends_on: usbd integration_platforms: - nrf52840dk/nrf52840 - nrf54h20dk/nrf54h20/cpuapp @@ -13,6 +12,7 @@ tests: - mimxrt1060_evk/mimxrt1062/qspi harness: keyboard tags: usb + sample.usbh.shell: tags: - usb @@ -24,8 +24,21 @@ tests: - nrf52840dk/nrf52840 - frdm_k64f build_only: true + sample.usbh.shell.virtual: tags: usb - extra_args: CONF_FILE="device_and_host_prj.conf" - platform_allow: qemu_cortex_m3 + extra_args: + - CONF_FILE="device_and_host_prj.conf" + platform_allow: + - qemu_cortex_m3 build_only: true + + sample.usbh.shell.hil: + harness: pytest + tags: + - usb + - shield + extra_args: + - CONF_FILE="host_prj.conf" + platform_allow: + - esp32s3_devkitc/esp32s3/procpu From f0fb8c3dc948855a238fbee81a399db3ba6b163f Mon Sep 17 00:00:00 2001 From: Roman Leonov Date: Tue, 3 Mar 2026 17:17:54 +0100 Subject: [PATCH 4/4] workflow: ci: add workflow for build and run usbh shell pytest Add workflow with zephyr container to test the subsys usbh shell sample Signed-off-by: Roman Leonov --- .../build_and_run_usbh_shell_pytest.yaml | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 .github/workflows/build_and_run_usbh_shell_pytest.yaml diff --git a/.github/workflows/build_and_run_usbh_shell_pytest.yaml b/.github/workflows/build_and_run_usbh_shell_pytest.yaml new file mode 100644 index 0000000000000..58cbbea454d4e --- /dev/null +++ b/.github/workflows/build_and_run_usbh_shell_pytest.yaml @@ -0,0 +1,193 @@ +name: Build and Run + +on: + pull_request: + types: [opened, reopened, synchronize] + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + zephyr_ver: ["v0.29.1"] + container: + image: ghcr.io/zephyrproject-rtos/ci:${{ matrix.zephyr_ver }} + + 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 + 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" + west init -l zephyr + west update hal_espressif + west blobs fetch hal_espressif + west zephyr-export + + - name: Locate Zephyr SDK in container + shell: bash + run: | + # Common locations used by Zephyr CI images; pick the first that exists. + 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 + + # Debug: show what we set + echo "ZEPHYR_SDK_INSTALL_DIR=${ZEPHYR_SDK_INSTALL_DIR:-}" + ls -la /opt || true + + - name: Twister build-only (ESP32S3) + shell: bash + env: + PYTEST_DISABLE_PLUGIN_AUTOLOAD: "1" + ZEPHYR_TOOLCHAIN_VARIANT: zephyr + run: | + . "$GITHUB_WORKSPACE/ws/.venv/bin/activate" + cd "$GITHUB_WORKSPACE/ws/zephyr" + + west twister -T samples/subsys/usb/shell --list-tests + + west twister \ + -T samples/subsys/usb/shell \ + -p esp32s3_devkitc/esp32s3/procpu \ + -s sample.usbh.shell.hil \ + --build-only \ + -j 1 -v \ + --outdir "$GITHUB_WORKSPACE/twister-out" + + - name: Pack twister-out artifact + shell: bash + run: | + cd "$GITHUB_WORKSPACE" + tar -czf twister-out.tgz twister-out + + - name: Upload artifacts + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: twister-out + path: twister-out.tgz + if-no-files-found: error + + run: + if: startsWith(github.head_ref, 'pr-add-') + runs-on: [self-hosted, docker, esp32s3, usb_host] + strategy: + matrix: + zephyr_ver: ["v0.29.1"] + container: + image: ghcr.io/zephyrproject-rtos/ci:${{ matrix.zephyr_ver }} + options: >- + --volume=/etc/zephyr:/etc/zephyr:ro + --device=/dev/ttyACM0 + needs: [build] + + steps: + - name: Checkout (workspace layout) + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + path: ws/zephyr + fetch-depth: 0 + + - name: Python venv + requirements (clean) + shell: bash + run: | + rm -rf "$GITHUB_WORKSPACE/ws/.venv" + 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 (runner, idempotent) + shell: bash + run: | + . "$GITHUB_WORKSPACE/ws/.venv/bin/activate" + cd "$GITHUB_WORKSPACE/ws" + if [ ! -d .west ]; then + west init -l zephyr + fi + west zephyr-export + + - name: Download twister-out artifact + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: twister-out + path: ${{ github.workspace }} + + - name: Unpack twister-out + shell: bash + run: | + cd "$GITHUB_WORKSPACE" + rm -rf twister-out + tar -xzf twister-out.tgz + + - name: Locate Zephyr SDK in container + shell: bash + run: | + # Common locations used by Zephyr CI images; pick the first that exists. + 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 + + # Debug: show what we set + echo "ZEPHYR_SDK_INSTALL_DIR=${ZEPHYR_SDK_INSTALL_DIR:-}" + ls -la /opt || true + + - name: Run on hardware (test-only) + 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 samples/subsys/usb/shell \ + -p esp32s3_devkitc/esp32s3/procpu \ + -s sample.usbh.shell.hil \ + --test-only \ + --device-testing \ + --hardware-map /etc/zephyr/hardware-map-esp32s3.yml \ + -j 1 -vvv \ + --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