Skip to content

drivers: usb: dwc2: esp32: Add support #85600

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions boards/espressif/esp32s3_devkitc/esp32s3_devkitc_procpu.dts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2024 Espressif Systems (Shanghai) Co., Ltd.
* Copyright (c) 2024-2025 Espressif Systems (Shanghai) Co., Ltd.
*
* SPDX-License-Identifier: Apache-2.0
*/
Expand All @@ -18,6 +18,8 @@
aliases {
i2c-0 = &i2c0;
watchdog0 = &wdt0;
uart-0 = &uart0;
sw0 = &button0;
};

chosen {
Expand All @@ -29,11 +31,6 @@
zephyr,bt-hci = &esp32_bt_hci;
};

aliases {
uart-0 = &uart0;
sw0 = &button0;
};

buttons {
compatible = "gpio-keys";
button0: button_0 {
Expand Down Expand Up @@ -151,3 +148,7 @@
&wifi {
status = "okay";
};

zephyr_udc0: &usb_otg {
status = "okay";
};
13 changes: 7 additions & 6 deletions boards/espressif/esp32s3_devkitm/esp32s3_devkitm_procpu.dts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 Espressif Systems (Shanghai) Co., Ltd.
* Copyright (c) 2022-2025 Espressif Systems (Shanghai) Co., Ltd.
*
* SPDX-License-Identifier: Apache-2.0
*/
Expand All @@ -18,6 +18,8 @@
aliases {
i2c-0 = &i2c0;
watchdog0 = &wdt0;
uart-0 = &uart0;
sw0 = &button0;
};

chosen {
Expand All @@ -29,11 +31,6 @@
zephyr,bt-hci = &esp32_bt_hci;
};

aliases {
uart-0 = &uart0;
sw0 = &button0;
};

buttons {
compatible = "gpio-keys";
button0: button_0 {
Expand Down Expand Up @@ -151,3 +148,7 @@
&wifi {
status = "okay";
};

zephyr_udc0: &usb_otg {
status = "okay";
};
6 changes: 5 additions & 1 deletion boards/espressif/esp32s3_eye/esp32s3_eye_procpu.dts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2024 Espressif Systems (Shanghai) Co., Ltd.
* Copyright (c) 2024-2025 Espressif Systems (Shanghai) Co., Ltd.
*
* SPDX-License-Identifier: Apache-2.0
*/
Expand Down Expand Up @@ -208,3 +208,7 @@
&wifi {
status = "okay";
};

zephyr_udc0: &usb_otg {
status = "okay";
};
57 changes: 36 additions & 21 deletions drivers/usb/udc/udc_dwc2.c
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ enum dwc2_drv_event_type {
/* Get Data FIFO access register */
#define UDC_DWC2_EP_FIFO(base, idx) ((mem_addr_t)base + 0x1000 * (idx + 1))

/* Percentage limit of how much SPRAM can be allocated for RxFIFO */
#define MAX_RXFIFO_GDFIFO_PERCENTAGE 25

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we introduced a maximum limit in percentage, maybe it could be better to make this constant configurable (I tried it with several values and seems that we can set the range 25-75). WDYT ?

Something like:

Suggested change
#define MAX_RXFIFO_GDFIFO_PERCENTAGE 25
#define MAX_RXFIFO_GDFIFO_PERCENTAGE CONFIG_UDC_DRIVER_RXFIFO_THRESHOLD

with the changes in zephyr/drivers/usb/udc/Kconfig :

config UDC_DRIVER_RXFIFO_USAGE_LIMIT
	int "RX FIFO usage limit (%)"
	range 25 75
	default 25
	help 
	  Limits how much of total available SPRAM (GDFIFOCFG field in GDFIFOCFG
	  register) can be used by RxFIFO.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why 25 to 75? If this ends up as Kconfig, then 100% should be the default (disable the limit altogether, which should remove the code that puts the limit in place) because the DWC2 Kconfig options are shared by all targets. You could then add different default based on your target.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then 100% should be the default

Yes, agreed.

May I ask is there any available Nordic board (dev kit) that can easily be used to verify usb applications?


enum dwc2_suspend_type {
DWC2_SUSPEND_NO_POWER_SAVING,
DWC2_SUSPEND_HIBERNATION,
Expand Down Expand Up @@ -2099,6 +2102,13 @@ static int udc_dwc2_init_controller(const struct device *dev)
if (priv->dynfifosizing) {
uint32_t gnptxfsiz;
uint32_t default_depth;
uint32_t spram_size;
uint32_t max_rxfifo;

/* Get available SPRAM size and calculate max allocatable RX fifo size */
val = sys_read32((mem_addr_t)&base->gdfifocfg);
spram_size = usb_dwc2_get_gdfifocfg_gdfifocfg(val);
max_rxfifo = ((spram_size * MAX_RXFIFO_GDFIFO_PERCENTAGE) / 100);

/* TODO: For proper runtime FIFO sizing UDC driver would have to
* have prior knowledge of the USB configurations. Only with the
Expand All @@ -2119,7 +2129,7 @@ static int udc_dwc2_init_controller(const struct device *dev)
* to store reset value. Read the reset value and make sure that
* the programmed value is not greater than what driver sets.
*/
priv->rxfifo_depth = MIN(priv->rxfifo_depth, default_depth);
priv->rxfifo_depth = MIN(MIN(priv->rxfifo_depth, default_depth), max_rxfifo);
sys_write32(usb_dwc2_set_grxfsiz(priv->rxfifo_depth), grxfsiz_reg);

/* Set TxFIFO 0 depth */
Expand Down Expand Up @@ -3289,6 +3299,30 @@ static const struct udc_api udc_dwc2_api = {
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(UDC_DWC2_IRQ_DT_INST_DEFINE)
#define UDC_DWC2_IRQ_FLAGS_TYPE0(n) 0
#define UDC_DWC2_IRQ_FLAGS_TYPE1(n) DT_INST_IRQ(n, type)
#define DW_IRQ_FLAGS(n) \
_CONCAT(UDC_DWC2_IRQ_FLAGS_TYPE, DT_INST_IRQ_HAS_CELL(n, type))(n)

#define UDC_DWC2_IRQ_DT_INST_DEFINE(n) \
static void udc_dwc2_irq_enable_func_##n(const struct device *dev) \
{ \
IRQ_CONNECT(DT_INST_IRQN(n), \
DT_INST_IRQ(n, priority), \
udc_dwc2_isr_handler, \
DEVICE_DT_INST_GET(n), \
DW_IRQ_FLAGS(n)); \
\
irq_enable(DT_INST_IRQN(n)); \
} \
\
static void udc_dwc2_irq_disable_func_##n(const struct device *dev) \
{ \
irq_disable(DT_INST_IRQN(n)); \
}
#endif

#define UDC_DWC2_PINCTRL_DT_INST_DEFINE(n) \
COND_CODE_1(DT_INST_PINCTRL_HAS_NAME(n, default), \
(PINCTRL_DT_INST_DEFINE(n)), ())
Expand All @@ -3297,11 +3331,6 @@ static const struct udc_api udc_dwc2_api = {
COND_CODE_1(DT_INST_PINCTRL_HAS_NAME(n, default), \
((void *)PINCTRL_DT_INST_DEV_CONFIG_GET(n)), (NULL))

#define UDC_DWC2_IRQ_FLAGS_TYPE0(n) 0
#define UDC_DWC2_IRQ_FLAGS_TYPE1(n) DT_INST_IRQ(n, type)
#define DW_IRQ_FLAGS(n) \
_CONCAT(UDC_DWC2_IRQ_FLAGS_TYPE, DT_INST_IRQ_HAS_CELL(n, type))(n)

/*
* A UDC driver should always be implemented as a multi-instance
* driver, even if your platform does not require it.
Expand Down Expand Up @@ -3333,21 +3362,7 @@ static const struct udc_api udc_dwc2_api = {
k_thread_name_set(&priv->thread_data, dev->name); \
} \
\
static void udc_dwc2_irq_enable_func_##n(const struct device *dev) \
{ \
IRQ_CONNECT(DT_INST_IRQN(n), \
DT_INST_IRQ(n, priority), \
udc_dwc2_isr_handler, \
DEVICE_DT_INST_GET(n), \
DW_IRQ_FLAGS(n)); \
\
irq_enable(DT_INST_IRQN(n)); \
} \
\
static void udc_dwc2_irq_disable_func_##n(const struct device *dev) \
{ \
irq_disable(DT_INST_IRQN(n)); \
} \
UDC_DWC2_IRQ_DT_INST_DEFINE(n) \
Comment on lines -3336 to +3365
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a stray change that was not justified in the commit message. Anyway, I do not agree with it, you should use irq_enable()/irq_disable().

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for the time being we are using Espressif's interrupt allocator
I thought about turning it into a quirk API call, but it would be probably disliked as a solution
do you have a different suggestion?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not understand the problem with irq_enable/irq_disable. How was it accepted not to use generic API for your platform? Are you working on fixing it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did some checking, and the current behavior for ESP Xtensa chips is: Zephyr's API is used to control non-muxed interrupt lines (such as wifi, systimer). Most peripherals such as USB allow routing sources through an interrupt matrix and for this, the interrupt controller API is used. I'm not sure how much this deviates from the standard implementation, but I believe significant changes would be needed to otherwise accommodate this.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jfischer-no, for esp32 device, we still need the irq calls in there, otherwise it is no-go for now. Would you review it again?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still cannot find an explanation as to why your platform cannot use generic functions, or why this has not been done all the time is supported in Zephyr.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ESP chips use an interrupt controller peripheral to route interrupt sources coming from each peripheral to the CPU's IRQ lines. For this reason we implement an interrupt controller driver and call it's API directly from all ESP drivers. A few peripherals have their interrupt source lines connected directly to the CPU IRQ's, thus having dedicated lines (e.g. wifi). As USB OTG is routed using the Interrupt Matrix peripheral we need to connect it's interrupt source to the IRQ accessing HAL LL calls. As this can't be accomplished using Zephyr's irq_connect() without making significant changes to the way we manage interrupts, we are asking to treat interrupt allocation as a quirk for the time being. We are already discussing internally if we should rework the interrupt system implementation and we did a review recently, so this will still be an ongoing work for us. Also, we are still not implementing static interrupt allocation even for IRQ lines that are static and don't need routing.

\
static struct udc_ep_config ep_cfg_out[DT_INST_PROP(n, num_out_eps)]; \
static struct udc_ep_config ep_cfg_in[DT_INST_PROP(n, num_in_eps)]; \
Expand Down
165 changes: 165 additions & 0 deletions drivers/usb/udc/udc_dwc2_vendor_quirks.h
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,171 @@ DT_INST_FOREACH_STATUS_OKAY(QUIRK_NRF_USBHS_DEFINE)

#endif /*DT_HAS_COMPAT_STATUS_OKAY(nordic_nrf_usbhs) */

#if DT_HAS_COMPAT_STATUS_OKAY(espressif_esp32_usb_otg)

#include <zephyr/drivers/interrupt_controller/intc_esp32.h>
#include <zephyr/drivers/clock_control.h>
#include <zephyr/logging/log.h>

#include <esp_err.h>
#include <esp_private/usb_phy.h>
#include <hal/usb_wrap_hal.h>
#include <soc/usb_dwc_periph.h>

#include <esp_rom_gpio.h>
#include <driver/gpio.h>
#include <soc/usb_pins.h>
#include <soc/gpio_sig_map.h>

struct phy_context_t {
usb_phy_target_t target;
usb_phy_controller_t controller;
usb_phy_status_t status;
usb_otg_mode_t otg_mode;
usb_phy_speed_t otg_speed;
usb_phy_ext_io_conf_t *iopins;
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 udc_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)
{
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_DEVICE */
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_ONE_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_ZERO_INPUT, USB_OTG_AVALID_IN_IDX, false);

if (cfg->phy_ctx->target == USB_PHY_TARGET_INT) {
gpio_set_drive_capability(USBPHY_DM_NUM, GPIO_DRIVE_CAP_3);
gpio_set_drive_capability(USBPHY_DP_NUM, GPIO_DRIVE_CAP_3);
}
Comment on lines +373 to +382
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can it use pinctrl if not why?

Copy link
Collaborator Author

@raffarost raffarost Apr 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we are adding support initially for OTG using only internal transceiver, pins are fixed (cannot be configured freely).
Later, when adding config for external transceiver support, we will use pinctrl indeed.


/* 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)udc_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(udc_dwc2, CONFIG_UDC_DRIVER_LOG_LEVEL);

if (enable) {
usb_wrap_ll_enable_bus_clock(true);
usb_wrap_hal_init(&phy_ctx->wrap_hal);

#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

LOG_DBG("PHY enabled");
} else {
usb_wrap_ll_enable_bus_clock(false);
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, \
.controller = USB_PHY_CTRL_OTG, \
.otg_mode = USB_OTG_MODE_DEVICE, \
.otg_speed = USB_PHY_SPEED_FULL, \
.iopins = 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_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_enable_phy_##n(const struct device *dev) \
{ \
return esp32_usb_otg_enable_phy(&phy_ctx_##n, true); \
} \
\
static int esp32_usb_otg_disable_phy_##n(const struct device *dev) \
{ \
return esp32_usb_otg_enable_phy(&phy_ctx_##n, false); \
} \
\
const struct dwc2_vendor_quirks dwc2_vendor_quirks_##n = { \
.init = esp32_usb_otg_init_##n, \
.post_enable = esp32_usb_otg_enable_phy_##n, \
.disable = esp32_usb_otg_disable_phy_##n, \
}; \

#define UDC_DWC2_IRQ_DT_INST_DEFINE(n) \
static void udc_dwc2_irq_enable_func_##n(const struct device *dev) \
{ \
esp_intr_enable(usb_otg_data_##n.int_handle); \
} \
\
static void udc_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 /*DT_HAS_COMPAT_STATUS_OKAY(espressif_esp32_usb_otg) */

/* Add next vendor quirks definition above this line */

#endif /* ZEPHYR_DRIVERS_USB_UDC_DWC2_VENDOR_QUIRKS_H */
12 changes: 12 additions & 0 deletions dts/bindings/usb/espressif,esp32-usb-otg.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Copyright (c) 2025 Espressif Systems (Shanghai) Co., Ltd.
# SPDX-License-Identifier: Apache-2.0
#
description: ESP32 USB-OTG (DWC2 compatible controller)

compatible: "espressif,esp32-usb-otg"

include: ["snps,dwc2.yaml"]

properties:
clocks:
required: true
16 changes: 15 additions & 1 deletion dts/xtensa/espressif/esp32s3/esp32s3_common.dtsi
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 Espressif Systems (Shanghai) Co., Ltd.
* Copyright (c) 2022-2025 Espressif Systems (Shanghai) Co., Ltd.
*
* SPDX-License-Identifier: Apache-2.0
*/
Expand Down Expand Up @@ -405,6 +405,20 @@
clocks = <&clock ESP32_USB_MODULE>;
};

usb_otg: usb_otg@60080000 {
compatible = "espressif,esp32-usb-otg", "snps,dwc2";
reg = <0x60080000 DT_SIZE_K(256)>;
status = "disabled";
interrupts = <USB_INTR_SOURCE IRQ_DEFAULT_PRIORITY 0>;
interrupt-parent = <&intc>;
clocks = <&clock ESP32_USB_MODULE>;
num-out-eps = <6>;
num-in-eps = <6>;
ghwcfg1 = <0x00000000>;
ghwcfg2 = <0x224dd930>;
ghwcfg4 = <0xd3f0a030>;
};

timer0: counter@6001f000 {
compatible = "espressif,esp32-timer";
reg = <0x6001f000 DT_SIZE_K(4)>;
Expand Down
Loading