From d7378ff73111bfd53f21b387b13eb75c271ab094 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1niel=20Buga?= Date: Mon, 30 Mar 2026 16:48:01 +0200 Subject: [PATCH 1/5] Restructure OTG-FS driver --- esp-hal/CHANGELOG.md | 1 + esp-hal/Cargo.toml | 6 + esp-hal/src/otg_fs.rs | 335 ------------------ esp-hal/src/otg_fs/embassy_usb_device.rs | 196 ++++++++++ esp-hal/src/otg_fs/mod.rs | 136 +++++++ .../async/embassy_usb_ethernet/Cargo.lock | 14 +- .../async/embassy_usb_ethernet/src/main.rs | 2 +- examples/async/embassy_usb_serial/src/main.rs | 2 +- 8 files changed, 348 insertions(+), 344 deletions(-) delete mode 100644 esp-hal/src/otg_fs.rs create mode 100644 esp-hal/src/otg_fs/embassy_usb_device.rs create mode 100644 esp-hal/src/otg_fs/mod.rs diff --git a/esp-hal/CHANGELOG.md b/esp-hal/CHANGELOG.md index 4c2af4d5e02..b5dbfb72ea1 100644 --- a/esp-hal/CHANGELOG.md +++ b/esp-hal/CHANGELOG.md @@ -96,6 +96,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated embassy dependencies: embassy-sync to 0.8, embassy-embedded-hal to 0.6 (#5249) - `esp_hal::efuse::chip_revision` has been marked stable (#5287) - `esp_hal::efuse::chip_revision` now returns `ChipRevision` (#5287) +- The embassy-usb device drivers has been moved from `esp_hal::otg_fs::asynch` to `esp_hal::otg_fs::device`. (#5283) ### Fixed diff --git a/esp-hal/Cargo.toml b/esp-hal/Cargo.toml index 99d4bf94e35..7bcdd810b1a 100644 --- a/esp-hal/Cargo.toml +++ b/esp-hal/Cargo.toml @@ -76,6 +76,7 @@ procmacros = { version = "0.21.0", package = "esp-hal-procmacros", # They are needed when using the `unstable` feature. digest = { version = "0.10.7", default-features = false, features = ["core-api"], optional = true } embassy-usb-driver = { version = "0.2", optional = true } +embassy-usb-host = { version = "0.2", optional = true } embassy-usb-synopsys-otg = { version = "0.3", optional = true } embedded-can = { version = "0.4.1", optional = true } esp-synopsys-usb-otg = { version = "0.4.2", optional = true } @@ -339,3 +340,8 @@ mixed_attributes_style = "allow" [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(host_os, values("windows"))'] } + +#[patch.crates-io] +#embassy-usb-driver = { git = "https://github.com/leftger/embassy", branch = "feat/usb-host" } +#embassy-usb = { git = "https://github.com/leftger/embassy", branch = "feat/usb-host" } +#embassy-usb-otg-synopsys = { git = "https://github.com/leftger/embassy", branch = "feat/usb-host" } diff --git a/esp-hal/src/otg_fs.rs b/esp-hal/src/otg_fs.rs deleted file mode 100644 index 91e10da9cc1..00000000000 --- a/esp-hal/src/otg_fs.rs +++ /dev/null @@ -1,335 +0,0 @@ -//! # USB On-The-Go (USB OTG) -//! -//! ## Overview -//! The USB OTG Full-speed (FS) peripheral allows communication -//! with USB devices using either blocking (usb-device) or asynchronous -//! (embassy-usb) APIs. -//! -//! It can operate as either a USB Host or Device, and supports full-speed (FS) -//! and low-speed (LS) data rates of the USB 2.0 specification. -//! -//! The blocking driver uses the `esp_synopsys_usb_otg` crate, which provides -//! the `USB bus` implementation and `USB peripheral traits`. -//! -//! The asynchronous driver uses the `embassy_usb_synopsys_otg` crate, which -//! provides the `USB bus` and `USB device` implementations. -//! -//! The module also relies on other peripheral modules, such as `GPIO`, -//! `system`, and `clock control`, to configure and enable the `USB` peripheral. -//! -//! ## Configuration -//! To use the USB OTG Full-speed peripheral driver, you need to initialize the -//! peripheral and configure its settings. The [`Usb`] struct represents the USB -//! peripheral and requires the GPIO pins that implement [`UsbDp`], and -//! [`UsbDm`], which define the specific types used for USB pin selection. -//! -//! The returned `Usb` instance can be used with the `usb-device` crate, or it -//! can be further configured with [`asynch::Driver`] to be used with the -//! `embassy-usb` crate. -//! -//! ## Examples -//! Visit the [USB Serial] example for an example of using the USB OTG -//! peripheral. -//! -//! [USB Serial]: https://github.com/esp-rs/esp-hal/blob/main/examples/peripheral/usb_serial/src/main.rs -//! -//! ## Implementation State -//! - Low-speed (LS) is not supported. - -pub use esp_synopsys_usb_otg::UsbBus; -use esp_synopsys_usb_otg::UsbPeripheral; - -use crate::{ - gpio::InputSignal, - peripherals, - system::{GenericPeripheralGuard, Peripheral as PeripheralEnable}, -}; - -#[doc(hidden)] -/// Trait representing the USB D+ (data plus) pin. -pub trait UsbDp: crate::private::Sealed {} - -#[doc(hidden)] -/// Trait representing the USB D- (data minus) pin. -pub trait UsbDm: crate::private::Sealed {} - -for_each_analog_function! { - (USB_DM, $gpio:ident) => { - impl UsbDm for crate::peripherals::$gpio<'_> {} - }; - (USB_DP, $gpio:ident) => { - impl UsbDp for crate::peripherals::$gpio<'_> {} - }; -} - -/// USB peripheral. -pub struct Usb<'d> { - _usb0: peripherals::USB0<'d>, - _guard: GenericPeripheralGuard<{ PeripheralEnable::Usb as u8 }>, -} - -impl<'d> Usb<'d> { - /// Creates a new `Usb` instance. - pub fn new( - usb0: peripherals::USB0<'d>, - _usb_dp: impl UsbDp + 'd, - _usb_dm: impl UsbDm + 'd, - ) -> Self { - let guard = GenericPeripheralGuard::new(); - - Self { - _usb0: usb0, - _guard: guard, - } - } - - fn _enable() { - peripherals::USB_WRAP::regs().otg_conf().modify(|_, w| { - w.usb_pad_enable().set_bit(); - w.phy_sel().clear_bit(); - w.clk_en().set_bit(); - w.ahb_clk_force_on().set_bit(); - w.phy_clk_force_on().set_bit() - }); - - #[cfg(esp32s3)] - peripherals::LPWR::regs().usb_conf().modify(|_, w| { - w.sw_hw_usb_phy_sel().set_bit(); - w.sw_usb_phy_sel().set_bit() - }); - - use crate::gpio::Level; - - InputSignal::USB_OTG_IDDIG.connect_to(&Level::High); // connected connector is mini-B side - InputSignal::USB_SRP_BVALID.connect_to(&Level::High); // HIGH to force USB device mode - InputSignal::USB_OTG_VBUSVALID.connect_to(&Level::High); // receiving a valid Vbus from device - InputSignal::USB_OTG_AVALID.connect_to(&Level::Low); - } - - fn _disable() { - // TODO - } -} - -unsafe impl Sync for Usb<'_> {} - -unsafe impl UsbPeripheral for Usb<'_> { - const REGISTERS: *const () = peripherals::USB0::PTR.cast(); - - const HIGH_SPEED: bool = false; - const FIFO_DEPTH_WORDS: usize = 256; - const ENDPOINT_COUNT: usize = 5; - - fn enable() { - Self::_enable(); - } - - fn ahb_frequency_hz(&self) -> u32 { - // unused - 80_000_000 - } -} -/// Async functionality -pub mod asynch { - use embassy_usb_driver::{ - EndpointAddress, - EndpointAllocError, - EndpointType, - Event, - Unsupported, - }; - pub use embassy_usb_synopsys_otg::Config; - use embassy_usb_synopsys_otg::{ - Bus as OtgBus, - ControlPipe, - Driver as OtgDriver, - Endpoint, - In, - OtgInstance, - Out, - PhyType, - State, - on_interrupt, - otg_v1::Otg, - }; - use procmacros::handler; - - use super::*; - use crate::system::Cpu; - - // From ESP32-S3 TRM: - // Six additional endpoints (endpoint numbers 1 to 6), configurable as IN or OUT - const MAX_EP_COUNT: usize = 7; - - static STATE: State = State::new(); - - /// Asynchronous USB driver. - pub struct Driver<'d> { - inner: OtgDriver<'d, MAX_EP_COUNT>, - _usb: Usb<'d>, - } - - impl<'d> Driver<'d> { - const REGISTERS: Otg = unsafe { Otg::from_ptr(Usb::REGISTERS.cast_mut()) }; - - /// Initializes USB OTG peripheral with internal Full-Speed PHY, for - /// asynchronous operation. - /// - /// # Arguments - /// - /// * `ep_out_buffer` - An internal buffer used to temporarily store received packets. - /// - /// Must be large enough to fit all OUT endpoint max packet sizes. - /// Endpoint allocation will fail if it is too small. - pub fn new(peri: Usb<'d>, ep_out_buffer: &'d mut [u8], config: Config) -> Self { - // From `synopsys-usb-otg` crate: - // This calculation doesn't correspond to one in a Reference Manual. - // In fact, the required number of words is higher than indicated in RM. - // The following numbers are pessimistic and were figured out empirically. - const RX_FIFO_EXTRA_SIZE_WORDS: u16 = 30; - - let instance = OtgInstance { - regs: Self::REGISTERS, - state: &STATE, - fifo_depth_words: Usb::FIFO_DEPTH_WORDS as u16, - extra_rx_fifo_words: RX_FIFO_EXTRA_SIZE_WORDS, - endpoint_count: Usb::ENDPOINT_COUNT, - phy_type: PhyType::InternalFullSpeed, - calculate_trdt_fn: |_| 5, - }; - Self { - inner: OtgDriver::new(ep_out_buffer, instance, config), - _usb: peri, - } - } - } - - impl<'d> embassy_usb_driver::Driver<'d> for Driver<'d> { - type EndpointOut = Endpoint<'d, Out>; - type EndpointIn = Endpoint<'d, In>; - type ControlPipe = ControlPipe<'d>; - type Bus = Bus<'d>; - - fn alloc_endpoint_in( - &mut self, - ep_type: EndpointType, - ep_addr: Option, - max_packet_size: u16, - interval_ms: u8, - ) -> Result { - self.inner - .alloc_endpoint_in(ep_type, ep_addr, max_packet_size, interval_ms) - } - - fn alloc_endpoint_out( - &mut self, - ep_type: EndpointType, - ep_addr: Option, - max_packet_size: u16, - interval_ms: u8, - ) -> Result { - self.inner - .alloc_endpoint_out(ep_type, ep_addr, max_packet_size, interval_ms) - } - - fn start(self, control_max_packet_size: u16) -> (Self::Bus, Self::ControlPipe) { - let (bus, cp) = self.inner.start(control_max_packet_size); - - let mut bus = Bus { - inner: bus, - _usb: self._usb, - }; - - bus.init(); - - (bus, cp) - } - } - - /// Asynchronous USB bus mainly used internally by `embassy-usb`. - // We need a custom wrapper implementation to handle custom initialization. - pub struct Bus<'d> { - inner: OtgBus<'d, MAX_EP_COUNT>, - _usb: Usb<'d>, - } - - impl Bus<'_> { - fn init(&mut self) { - Usb::_enable(); - - let r = Driver::REGISTERS; - - // Wait for AHB ready. - while !r.grstctl().read().ahbidl() {} - - // Configure as device. - r.gusbcfg().modify(|w| { - // Force device mode - w.set_fdmod(true); - w.set_srpcap(false); - }); - - // Perform core soft-reset - while !r.grstctl().read().ahbidl() {} - r.grstctl().modify(|w| w.set_csrst(true)); - while r.grstctl().read().csrst() {} - - self.inner.config_v1(); - - // Enable PHY clock - r.pcgcctl().write(|w| w.0 = 0); - - crate::interrupt::bind_handler(crate::peripherals::Interrupt::USB, interrupt_handler); - } - - fn disable(&mut self) { - crate::interrupt::disable(Cpu::ProCpu, peripherals::Interrupt::USB); - - #[cfg(multi_core)] - crate::interrupt::disable(Cpu::AppCpu, peripherals::Interrupt::USB); - - Usb::_disable(); - } - } - - impl embassy_usb_driver::Bus for Bus<'_> { - async fn poll(&mut self) -> Event { - self.inner.poll().await - } - - fn endpoint_set_stalled(&mut self, ep_addr: EndpointAddress, stalled: bool) { - self.inner.endpoint_set_stalled(ep_addr, stalled) - } - - fn endpoint_is_stalled(&mut self, ep_addr: EndpointAddress) -> bool { - self.inner.endpoint_is_stalled(ep_addr) - } - - fn endpoint_set_enabled(&mut self, ep_addr: EndpointAddress, enabled: bool) { - self.inner.endpoint_set_enabled(ep_addr, enabled) - } - - async fn enable(&mut self) { - self.inner.enable().await - } - - async fn disable(&mut self) { - self.inner.disable().await - } - - async fn remote_wakeup(&mut self) -> Result<(), Unsupported> { - self.inner.remote_wakeup().await - } - } - - impl Drop for Bus<'_> { - fn drop(&mut self) { - Bus::disable(self); - } - } - - #[handler(priority = crate::interrupt::Priority::max())] - fn interrupt_handler() { - unsafe { on_interrupt(Driver::REGISTERS, &STATE, Usb::ENDPOINT_COUNT) } - } -} diff --git a/esp-hal/src/otg_fs/embassy_usb_device.rs b/esp-hal/src/otg_fs/embassy_usb_device.rs new file mode 100644 index 00000000000..2280112bbd9 --- /dev/null +++ b/esp-hal/src/otg_fs/embassy_usb_device.rs @@ -0,0 +1,196 @@ +//! USB OTG device driver for embassy-usb. + +use embassy_usb_driver::{EndpointAddress, EndpointAllocError, EndpointType, Event, Unsupported}; +pub use embassy_usb_synopsys_otg::Config; +use embassy_usb_synopsys_otg::{ + Bus as OtgBus, + ControlPipe, + Driver as OtgDriver, + Endpoint, + In, + OtgInstance, + Out, + PhyType, + State, + on_interrupt, + otg_v1::Otg, +}; +use procmacros::handler; + +use crate::{otg_fs::Usb, peripherals::Interrupt, system::Cpu}; + +// From ESP32-S3 TRM: +// Six additional endpoints (endpoint numbers 1 to 6), configurable as IN or OUT +const MAX_EP_COUNT: usize = 7; + +static DEVICE_STATE: State = State::new(); + +/// Asynchronous USB driver. +pub struct Driver<'d> { + inner: OtgDriver<'d, MAX_EP_COUNT>, + _usb: Usb<'d>, +} + +impl<'d> Driver<'d> { + const REGISTERS: Otg = unsafe { Otg::from_ptr(Usb::REGISTERS.cast_mut()) }; + + /// Initializes USB OTG peripheral with internal Full-Speed PHY, for + /// asynchronous operation. + /// + /// # Arguments + /// + /// * `ep_out_buffer` - An internal buffer used to temporarily store received packets. + /// + /// Must be large enough to fit all OUT endpoint max packet sizes. + /// Endpoint allocation will fail if it is too small. + pub fn new(peri: Usb<'d>, ep_out_buffer: &'d mut [u8], config: Config) -> Self { + // From `synopsys-usb-otg` crate: + // This calculation doesn't correspond to one in a Reference Manual. + // In fact, the required number of words is higher than indicated in RM. + // The following numbers are pessimistic and were figured out empirically. + const RX_FIFO_EXTRA_SIZE_WORDS: u16 = 30; + + let instance = OtgInstance { + regs: Self::REGISTERS, + state: &DEVICE_STATE, + fifo_depth_words: Usb::FIFO_DEPTH_WORDS as u16, + extra_rx_fifo_words: RX_FIFO_EXTRA_SIZE_WORDS, + endpoint_count: Usb::ENDPOINT_COUNT, + phy_type: PhyType::InternalFullSpeed, + calculate_trdt_fn: |_| 5, + }; + Self { + inner: OtgDriver::new(ep_out_buffer, instance, config), + _usb: peri, + } + } +} + +impl<'d> embassy_usb_driver::Driver<'d> for Driver<'d> { + type EndpointOut = Endpoint<'d, Out>; + type EndpointIn = Endpoint<'d, In>; + type ControlPipe = ControlPipe<'d>; + type Bus = Bus<'d>; + + fn alloc_endpoint_in( + &mut self, + ep_type: EndpointType, + ep_addr: Option, + max_packet_size: u16, + interval_ms: u8, + ) -> Result { + self.inner + .alloc_endpoint_in(ep_type, ep_addr, max_packet_size, interval_ms) + } + + fn alloc_endpoint_out( + &mut self, + ep_type: EndpointType, + ep_addr: Option, + max_packet_size: u16, + interval_ms: u8, + ) -> Result { + self.inner + .alloc_endpoint_out(ep_type, ep_addr, max_packet_size, interval_ms) + } + + fn start(self, control_max_packet_size: u16) -> (Self::Bus, Self::ControlPipe) { + let (bus, cp) = self.inner.start(control_max_packet_size); + + let mut bus = Bus { + inner: bus, + _usb: self._usb, + }; + + bus.init(); + + (bus, cp) + } +} + +/// Asynchronous USB bus mainly used internally by `embassy-usb`. +// We need a custom wrapper implementation to handle custom initialization. +pub struct Bus<'d> { + inner: OtgBus<'d, MAX_EP_COUNT>, + _usb: Usb<'d>, +} + +impl Bus<'_> { + fn init(&mut self) { + Usb::_enable(); + + let r = Driver::REGISTERS; + + // Wait for AHB ready. + while !r.grstctl().read().ahbidl() {} + + // Configure as device. + r.gusbcfg().modify(|w| { + // Force device mode + w.set_fdmod(true); + w.set_srpcap(false); + }); + + // Perform core soft-reset + while !r.grstctl().read().ahbidl() {} + r.grstctl().modify(|w| w.set_csrst(true)); + while r.grstctl().read().csrst() {} + + self.inner.config_v1(); + + // Enable PHY clock + r.pcgcctl().write(|w| w.0 = 0); + + crate::interrupt::bind_handler(Interrupt::USB, interrupt_handler); + } + + fn disable(&mut self) { + crate::interrupt::disable(Cpu::ProCpu, Interrupt::USB); + + #[cfg(multi_core)] + crate::interrupt::disable(Cpu::AppCpu, Interrupt::USB); + + Usb::_disable(); + } +} + +impl embassy_usb_driver::Bus for Bus<'_> { + async fn poll(&mut self) -> Event { + self.inner.poll().await + } + + fn endpoint_set_stalled(&mut self, ep_addr: EndpointAddress, stalled: bool) { + self.inner.endpoint_set_stalled(ep_addr, stalled) + } + + fn endpoint_is_stalled(&mut self, ep_addr: EndpointAddress) -> bool { + self.inner.endpoint_is_stalled(ep_addr) + } + + fn endpoint_set_enabled(&mut self, ep_addr: EndpointAddress, enabled: bool) { + self.inner.endpoint_set_enabled(ep_addr, enabled) + } + + async fn enable(&mut self) { + self.inner.enable().await + } + + async fn disable(&mut self) { + self.inner.disable().await + } + + async fn remote_wakeup(&mut self) -> Result<(), Unsupported> { + self.inner.remote_wakeup().await + } +} + +impl Drop for Bus<'_> { + fn drop(&mut self) { + Bus::disable(self); + } +} + +#[handler(priority = crate::interrupt::Priority::max())] +fn interrupt_handler() { + unsafe { on_interrupt(Driver::REGISTERS, &DEVICE_STATE, Usb::ENDPOINT_COUNT) } +} diff --git a/esp-hal/src/otg_fs/mod.rs b/esp-hal/src/otg_fs/mod.rs new file mode 100644 index 00000000000..590c594ba39 --- /dev/null +++ b/esp-hal/src/otg_fs/mod.rs @@ -0,0 +1,136 @@ +//! # USB On-The-Go (USB OTG) +//! +//! ## Overview +//! The USB OTG Full-speed (FS) peripheral allows communication +//! with USB devices using either blocking (usb-device) or asynchronous +//! (embassy-usb) APIs. +//! +//! It can operate as either a USB Host or Device, and supports full-speed (FS) +//! and low-speed (LS) data rates of the USB 2.0 specification. +//! +//! The blocking driver uses the `esp_synopsys_usb_otg` crate, which provides +//! the `USB bus` implementation and `USB peripheral traits`. +//! +//! The asynchronous driver uses the `embassy_usb_synopsys_otg` crate, which +//! provides the `USB bus` and `USB device` implementations. +//! +//! The module also relies on other peripheral modules, such as `GPIO`, +//! `system`, and `clock control`, to configure and enable the `USB` peripheral. +//! +//! ## Configuration +//! To use the USB OTG Full-speed peripheral driver, you need to initialize the +//! peripheral and configure its settings. The [`Usb`] struct represents the USB +//! peripheral and requires the GPIO pins that implement [`UsbDp`], and +//! [`UsbDm`], which define the specific types used for USB pin selection. +//! +//! The returned `Usb` instance can be used with the `usb-device` crate, or it +//! can be further configured with [`asynch::Driver`] to be used with the +//! `embassy-usb` crate. +//! +//! ## Examples +//! Visit the [USB Serial] example for an example of using the USB OTG +//! peripheral. +//! +//! [USB Serial]: https://github.com/esp-rs/esp-hal/blob/main/examples/peripheral/usb_serial/src/main.rs +//! +//! ## Implementation State +//! - Low-speed (LS) is not supported. + +pub use esp_synopsys_usb_otg::UsbBus; + +use crate::{ + gpio::InputSignal, + peripherals::{self, USB0}, + system::{GenericPeripheralGuard, Peripheral as PeripheralEnable}, +}; + +pub mod embassy_usb_device; + +#[doc(hidden)] +/// Trait representing the USB D+ (data plus) pin. +pub trait UsbDp: crate::private::Sealed {} + +#[doc(hidden)] +/// Trait representing the USB D- (data minus) pin. +pub trait UsbDm: crate::private::Sealed {} + +for_each_analog_function! { + (USB_DM, $gpio:ident) => { + impl UsbDm for crate::peripherals::$gpio<'_> {} + }; + (USB_DP, $gpio:ident) => { + impl UsbDp for crate::peripherals::$gpio<'_> {} + }; +} + +/// USB peripheral. +pub struct Usb<'d> { + _usb0: peripherals::USB0<'d>, + _guard: GenericPeripheralGuard<{ PeripheralEnable::Usb as u8 }>, +} + +impl<'d> Usb<'d> { + const REGISTERS: *const () = USB0::PTR.cast(); + const HIGH_SPEED: bool = false; + const FIFO_DEPTH_WORDS: usize = 256; + const ENDPOINT_COUNT: usize = 5; + + /// Creates a new `Usb` instance. + pub fn new( + usb0: peripherals::USB0<'d>, + _usb_dp: impl UsbDp + 'd, + _usb_dm: impl UsbDm + 'd, + ) -> Self { + let guard = GenericPeripheralGuard::new(); + + Self { + _usb0: usb0, + _guard: guard, + } + } + + fn _enable() { + peripherals::USB_WRAP::regs().otg_conf().modify(|_, w| { + w.usb_pad_enable().set_bit(); + w.phy_sel().clear_bit(); + w.clk_en().set_bit(); + w.ahb_clk_force_on().set_bit(); + w.phy_clk_force_on().set_bit() + }); + + #[cfg(esp32s3)] + peripherals::LPWR::regs().usb_conf().modify(|_, w| { + w.sw_hw_usb_phy_sel().set_bit(); + w.sw_usb_phy_sel().set_bit() + }); + + use crate::gpio::Level; + + InputSignal::USB_OTG_IDDIG.connect_to(&Level::High); // connected connector is mini-B side + InputSignal::USB_SRP_BVALID.connect_to(&Level::High); // HIGH to force USB device mode + InputSignal::USB_OTG_VBUSVALID.connect_to(&Level::High); // receiving a valid Vbus from device + InputSignal::USB_OTG_AVALID.connect_to(&Level::Low); + } + + fn _disable() { + // TODO + } +} + +// unsafe impl Sync for Usb<'_> {} + +unsafe impl esp_synopsys_usb_otg::UsbPeripheral for Usb<'_> { + const REGISTERS: *const () = Self::REGISTERS; + const HIGH_SPEED: bool = Self::HIGH_SPEED; + const FIFO_DEPTH_WORDS: usize = Self::FIFO_DEPTH_WORDS; + const ENDPOINT_COUNT: usize = Self::ENDPOINT_COUNT; + + fn enable() { + Self::_enable(); + } + + fn ahb_frequency_hz(&self) -> u32 { + // unused + 80_000_000 + } +} diff --git a/examples/async/embassy_usb_ethernet/Cargo.lock b/examples/async/embassy_usb_ethernet/Cargo.lock index c572c5c9619..c551bde5ac9 100644 --- a/examples/async/embassy_usb_ethernet/Cargo.lock +++ b/examples/async/embassy_usb_ethernet/Cargo.lock @@ -844,7 +844,7 @@ dependencies = [ [[package]] name = "esp32" version = "0.39.0" -source = "git+https://github.com/esp-rs/esp-pacs?rev=405f40e#405f40edcb665ce5dc9e99428ea23670e004ebc8" +source = "git+https://github.com/esp-rs/esp-pacs?rev=879efa6#879efa6a91c6f0e2c7672830a39cf002d8c973bb" dependencies = [ "critical-section", "vcell", @@ -853,7 +853,7 @@ dependencies = [ [[package]] name = "esp32c2" version = "0.28.0" -source = "git+https://github.com/esp-rs/esp-pacs?rev=405f40e#405f40edcb665ce5dc9e99428ea23670e004ebc8" +source = "git+https://github.com/esp-rs/esp-pacs?rev=879efa6#879efa6a91c6f0e2c7672830a39cf002d8c973bb" dependencies = [ "critical-section", "vcell", @@ -862,7 +862,7 @@ dependencies = [ [[package]] name = "esp32c3" version = "0.31.0" -source = "git+https://github.com/esp-rs/esp-pacs?rev=405f40e#405f40edcb665ce5dc9e99428ea23670e004ebc8" +source = "git+https://github.com/esp-rs/esp-pacs?rev=879efa6#879efa6a91c6f0e2c7672830a39cf002d8c973bb" dependencies = [ "critical-section", "vcell", @@ -871,7 +871,7 @@ dependencies = [ [[package]] name = "esp32c6" version = "0.22.0" -source = "git+https://github.com/esp-rs/esp-pacs?rev=405f40e#405f40edcb665ce5dc9e99428ea23670e004ebc8" +source = "git+https://github.com/esp-rs/esp-pacs?rev=879efa6#879efa6a91c6f0e2c7672830a39cf002d8c973bb" dependencies = [ "critical-section", "vcell", @@ -880,7 +880,7 @@ dependencies = [ [[package]] name = "esp32h2" version = "0.18.0" -source = "git+https://github.com/esp-rs/esp-pacs?rev=405f40e#405f40edcb665ce5dc9e99428ea23670e004ebc8" +source = "git+https://github.com/esp-rs/esp-pacs?rev=879efa6#879efa6a91c6f0e2c7672830a39cf002d8c973bb" dependencies = [ "critical-section", "vcell", @@ -889,7 +889,7 @@ dependencies = [ [[package]] name = "esp32s2" version = "0.30.0" -source = "git+https://github.com/esp-rs/esp-pacs?rev=405f40e#405f40edcb665ce5dc9e99428ea23670e004ebc8" +source = "git+https://github.com/esp-rs/esp-pacs?rev=879efa6#879efa6a91c6f0e2c7672830a39cf002d8c973bb" dependencies = [ "critical-section", "vcell", @@ -898,7 +898,7 @@ dependencies = [ [[package]] name = "esp32s3" version = "0.34.0" -source = "git+https://github.com/esp-rs/esp-pacs?rev=405f40e#405f40edcb665ce5dc9e99428ea23670e004ebc8" +source = "git+https://github.com/esp-rs/esp-pacs?rev=879efa6#879efa6a91c6f0e2c7672830a39cf002d8c973bb" dependencies = [ "critical-section", "vcell", diff --git a/examples/async/embassy_usb_ethernet/src/main.rs b/examples/async/embassy_usb_ethernet/src/main.rs index 2c68b7574f7..9113e5624fa 100644 --- a/examples/async/embassy_usb_ethernet/src/main.rs +++ b/examples/async/embassy_usb_ethernet/src/main.rs @@ -33,7 +33,7 @@ use esp_hal::{ interrupt::software::SoftwareInterruptControl, otg_fs::{ Usb, - asynch::{Config, Driver as UsbDriver}, + embassy_usb_device::{Config, Driver as UsbDriver}, }, timer::timg::TimerGroup, }; diff --git a/examples/async/embassy_usb_serial/src/main.rs b/examples/async/embassy_usb_serial/src/main.rs index 06580b1315e..0b194cd22b3 100644 --- a/examples/async/embassy_usb_serial/src/main.rs +++ b/examples/async/embassy_usb_serial/src/main.rs @@ -21,7 +21,7 @@ use esp_hal::{ interrupt::software::SoftwareInterruptControl, otg_fs::{ Usb, - asynch::{Config, Driver}, + embassy_usb_device::{Config, Driver}, }, timer::timg::TimerGroup, }; From 6582978297bb62d13ec6f22a6846e029dfe065cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1niel=20Buga?= Date: Mon, 30 Mar 2026 17:32:49 +0200 Subject: [PATCH 2/5] Add USB host impl --- esp-hal/Cargo.toml | 10 ++- esp-hal/src/otg_fs/embassy_usb_device.rs | 14 ++-- esp-hal/src/otg_fs/embassy_usb_host.rs | 92 ++++++++++++++++++++++++ esp-hal/src/otg_fs/mod.rs | 38 +++++++++- 4 files changed, 137 insertions(+), 17 deletions(-) create mode 100644 esp-hal/src/otg_fs/embassy_usb_host.rs diff --git a/esp-hal/Cargo.toml b/esp-hal/Cargo.toml index 7bcdd810b1a..295f317df2e 100644 --- a/esp-hal/Cargo.toml +++ b/esp-hal/Cargo.toml @@ -76,8 +76,7 @@ procmacros = { version = "0.21.0", package = "esp-hal-procmacros", # They are needed when using the `unstable` feature. digest = { version = "0.10.7", default-features = false, features = ["core-api"], optional = true } embassy-usb-driver = { version = "0.2", optional = true } -embassy-usb-host = { version = "0.2", optional = true } -embassy-usb-synopsys-otg = { version = "0.3", optional = true } +embassy-usb-synopsys-otg = { version = "0.3", optional = true, features = ["host"] } embedded-can = { version = "0.4.1", optional = true } esp-synopsys-usb-otg = { version = "0.4.2", optional = true } nb = { version = "1.1", optional = true } @@ -341,7 +340,6 @@ mixed_attributes_style = "allow" [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(host_os, values("windows"))'] } -#[patch.crates-io] -#embassy-usb-driver = { git = "https://github.com/leftger/embassy", branch = "feat/usb-host" } -#embassy-usb = { git = "https://github.com/leftger/embassy", branch = "feat/usb-host" } -#embassy-usb-otg-synopsys = { git = "https://github.com/leftger/embassy", branch = "feat/usb-host" } +[patch.crates-io] +embassy-usb-driver = { git = "https://github.com/leftger/embassy", branch = "feat/usb-host" } +embassy-usb-synopsys-otg = { git = "https://github.com/leftger/embassy", branch = "feat/usb-host" } diff --git a/esp-hal/src/otg_fs/embassy_usb_device.rs b/esp-hal/src/otg_fs/embassy_usb_device.rs index 2280112bbd9..af27ffc813a 100644 --- a/esp-hal/src/otg_fs/embassy_usb_device.rs +++ b/esp-hal/src/otg_fs/embassy_usb_device.rs @@ -19,15 +19,11 @@ use procmacros::handler; use crate::{otg_fs::Usb, peripherals::Interrupt, system::Cpu}; -// From ESP32-S3 TRM: -// Six additional endpoints (endpoint numbers 1 to 6), configurable as IN or OUT -const MAX_EP_COUNT: usize = 7; - -static DEVICE_STATE: State = State::new(); +static DEVICE_STATE: State<{ Usb::MAX_EP_COUNT }> = State::new(); /// Asynchronous USB driver. pub struct Driver<'d> { - inner: OtgDriver<'d, MAX_EP_COUNT>, + inner: OtgDriver<'d, { Usb::MAX_EP_COUNT }>, _usb: Usb<'d>, } @@ -55,7 +51,7 @@ impl<'d> Driver<'d> { state: &DEVICE_STATE, fifo_depth_words: Usb::FIFO_DEPTH_WORDS as u16, extra_rx_fifo_words: RX_FIFO_EXTRA_SIZE_WORDS, - endpoint_count: Usb::ENDPOINT_COUNT, + endpoint_count: Usb::MAX_EP_COUNT, phy_type: PhyType::InternalFullSpeed, calculate_trdt_fn: |_| 5, }; @@ -111,7 +107,7 @@ impl<'d> embassy_usb_driver::Driver<'d> for Driver<'d> { /// Asynchronous USB bus mainly used internally by `embassy-usb`. // We need a custom wrapper implementation to handle custom initialization. pub struct Bus<'d> { - inner: OtgBus<'d, MAX_EP_COUNT>, + inner: OtgBus<'d, { Usb::MAX_EP_COUNT }>, _usb: Usb<'d>, } @@ -192,5 +188,5 @@ impl Drop for Bus<'_> { #[handler(priority = crate::interrupt::Priority::max())] fn interrupt_handler() { - unsafe { on_interrupt(Driver::REGISTERS, &DEVICE_STATE, Usb::ENDPOINT_COUNT) } + unsafe { on_interrupt(Driver::REGISTERS, &DEVICE_STATE, Usb::MAX_EP_COUNT) } } diff --git a/esp-hal/src/otg_fs/embassy_usb_host.rs b/esp-hal/src/otg_fs/embassy_usb_host.rs new file mode 100644 index 00000000000..fc1d94b77c5 --- /dev/null +++ b/esp-hal/src/otg_fs/embassy_usb_host.rs @@ -0,0 +1,92 @@ +//! USB OTG host driver for embassy-usb-host. +use embassy_usb_driver::{ + EndpointInfo, + host::{ + DeviceEvent, + HostError, + UsbHostDriver, + channel::{Direction, Type}, + }, +}; +use embassy_usb_synopsys_otg::{ + PhyType, + host::{HostState, OtgHost as OtgHostDriver, OtgHostInstance, on_host_interrupt}, + otg_v1::Otg, +}; + +use crate::{handler, interrupt::Priority, otg_fs::Usb, peripherals::Interrupt, system::Cpu}; + +#[handler(priority = Priority::max())] +fn interrupt_handler() { + unsafe { + on_host_interrupt( + Driver::REGISTERS, + &HOST_STATE, + Usb::MAX_EP_COUNT.min(Usb::MAX_HOST_CH_COUNT), + ) + } +} + +/// UsbHost +pub struct Driver<'d> { + inner: OtgHostDriver<'d, { Usb::MAX_HOST_CH_COUNT }>, + _usb: Usb<'d>, +} + +static HOST_STATE: HostState<{ Usb::MAX_HOST_CH_COUNT }> = HostState::new(); + +impl<'d> Driver<'d> { + const REGISTERS: Otg = unsafe { Otg::from_ptr(Usb::REGISTERS.cast_mut()) }; + + /// Creates a new Driver for embassy-usb-host. + pub fn new(peri: Usb<'d>) -> Self { + let instance = OtgHostInstance { + regs: Self::REGISTERS, + state: &HOST_STATE, + fifo_depth_words: Usb::FIFO_DEPTH_WORDS as u16, + channel_count: Usb::MAX_EP_COUNT.min(Usb::MAX_HOST_CH_COUNT), + phy_type: PhyType::InternalFullSpeed, + }; + + Usb::_enable_host(); + crate::interrupt::bind_handler(Interrupt::USB, interrupt_handler); + + Self { + inner: OtgHostDriver::new(instance), + _usb: peri, + } + } +} + +impl<'d> UsbHostDriver for Driver<'d> { + type Channel = + as UsbHostDriver>::Channel; + + async fn wait_for_device_event(&self) -> DeviceEvent { + self.inner.wait_for_device_event().await + } + + async fn bus_reset(&self) { + self.inner.bus_reset().await + } + + fn alloc_channel( + &self, + addr: u8, + endpoint: &EndpointInfo, + pre: bool, + ) -> Result, HostError> { + self.inner.alloc_channel(addr, endpoint, pre) + } +} + +impl<'d> Drop for Driver<'d> { + fn drop(&mut self) { + crate::interrupt::disable(Cpu::ProCpu, Interrupt::USB); + + #[cfg(multi_core)] + crate::interrupt::disable(Cpu::AppCpu, Interrupt::USB); + + Usb::_disable(); + } +} diff --git a/esp-hal/src/otg_fs/mod.rs b/esp-hal/src/otg_fs/mod.rs index 590c594ba39..add8264b5ca 100644 --- a/esp-hal/src/otg_fs/mod.rs +++ b/esp-hal/src/otg_fs/mod.rs @@ -45,6 +45,7 @@ use crate::{ }; pub mod embassy_usb_device; +pub mod embassy_usb_host; #[doc(hidden)] /// Trait representing the USB D+ (data plus) pin. @@ -73,7 +74,13 @@ impl<'d> Usb<'d> { const REGISTERS: *const () = USB0::PTR.cast(); const HIGH_SPEED: bool = false; const FIFO_DEPTH_WORDS: usize = 256; - const ENDPOINT_COUNT: usize = 5; + + // S2/S3 are identical: Six additional endpoints (endpoint numbers 1 to 6), configurable as IN + // or OUT + const MAX_EP_COUNT: usize = 7; + + // S2/S3 are identical: Eight channels (pipes) + const MAX_HOST_CH_COUNT: usize = 8; /// Creates a new `Usb` instance. pub fn new( @@ -112,6 +119,33 @@ impl<'d> Usb<'d> { InputSignal::USB_OTG_AVALID.connect_to(&Level::Low); } + fn _enable_host() { + peripherals::USB_WRAP::regs().otg_conf().modify(|_, w| { + w.usb_pad_enable().set_bit(); + w.phy_sel().clear_bit(); + w.clk_en().set_bit(); + w.ahb_clk_force_on().set_bit(); + w.phy_clk_force_on().set_bit(); + w.pad_pull_override().set_bit(); + w.dp_pulldown().set_bit(); + w.dm_pulldown().set_bit(); + w.dp_pullup().clear_bit(); + w.dm_pullup().clear_bit() + }); + + #[cfg(esp32s3)] + peripherals::LPWR::regs().usb_conf().modify(|_, w| { + w.sw_hw_usb_phy_sel().set_bit(); + w.sw_usb_phy_sel().set_bit() + }); + + use crate::gpio::Level; + InputSignal::USB_SRP_BVALID.connect_to(&Level::Low); // HIGH to force USB device mode + InputSignal::USB_OTG_IDDIG.connect_to(&Level::Low); // Indicate A-Device by floating the ID pin + InputSignal::USB_OTG_VBUSVALID.connect_to(&Level::High); + InputSignal::USB_OTG_AVALID.connect_to(&Level::High); // Assume valid A device + } + fn _disable() { // TODO } @@ -123,7 +157,7 @@ unsafe impl esp_synopsys_usb_otg::UsbPeripheral for Usb<'_> { const REGISTERS: *const () = Self::REGISTERS; const HIGH_SPEED: bool = Self::HIGH_SPEED; const FIFO_DEPTH_WORDS: usize = Self::FIFO_DEPTH_WORDS; - const ENDPOINT_COUNT: usize = Self::ENDPOINT_COUNT; + const ENDPOINT_COUNT: usize = Self::MAX_EP_COUNT; fn enable() { Self::_enable(); From 536d44a617a304953a1f07c406542bda204a9cd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1niel=20Buga?= Date: Mon, 30 Mar 2026 17:52:03 +0200 Subject: [PATCH 3/5] Add example --- .../embassy_usb_hid_host/.cargo/config.toml | 24 +++++ .../async/embassy_usb_hid_host/Cargo.toml | 50 +++++++++ .../async/embassy_usb_hid_host/src/main.rs | 100 ++++++++++++++++++ 3 files changed, 174 insertions(+) create mode 100644 examples/async/embassy_usb_hid_host/.cargo/config.toml create mode 100644 examples/async/embassy_usb_hid_host/Cargo.toml create mode 100644 examples/async/embassy_usb_hid_host/src/main.rs diff --git a/examples/async/embassy_usb_hid_host/.cargo/config.toml b/examples/async/embassy_usb_hid_host/.cargo/config.toml new file mode 100644 index 00000000000..44dacc4278b --- /dev/null +++ b/examples/async/embassy_usb_hid_host/.cargo/config.toml @@ -0,0 +1,24 @@ +[target.'cfg(target_arch = "riscv32")'] +runner = "espflash flash --monitor" +rustflags = [ + "-C", "link-arg=-Tlinkall.x", + "-C", "force-frame-pointers", +] + +[target.'cfg(target_arch = "xtensa")'] +runner = "espflash flash --monitor" +rustflags = [ + # GNU LD + "-C", "link-arg=-Wl,-Tlinkall.x", + "-C", "link-arg=-nostartfiles", + + # LLD + # "-C", "link-arg=-Tlinkall.x", + # "-C", "linker=rust-lld", +] + +[env] +ESP_LOG = "info" + +[unstable] +build-std = ["core", "alloc"] diff --git a/examples/async/embassy_usb_hid_host/Cargo.toml b/examples/async/embassy_usb_hid_host/Cargo.toml new file mode 100644 index 00000000000..becf59b7df2 --- /dev/null +++ b/examples/async/embassy_usb_hid_host/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "embassy-usb-hid_host" +version = "0.0.0" +edition = "2024" +publish = false + +[dependencies] +embassy-executor = "0.10.0" +embassy-futures = "0.1" +embassy-usb-host = { version = "0.1.0", default-features = false } +esp-backtrace = { path = "../../../esp-backtrace", features = [ + "panic-handler", + "println", +] } +esp-bootloader-esp-idf = { path = "../../../esp-bootloader-esp-idf" } +esp-hal = { path = "../../../esp-hal", features = ["log-04", "unstable"] } +esp-rtos = { path = "../../../esp-rtos", features = ["embassy", "log-04"] } +esp-println = { path = "../../../esp-println", features = ["log-04"] } +log = "0.4" + +[features] +esp32s2 = [ + "esp-backtrace/esp32s2", + "esp-bootloader-esp-idf/esp32s2", + "esp-rtos/esp32s2", + "esp-hal/esp32s2", +] +esp32s3 = [ + "esp-backtrace/esp32s3", + "esp-bootloader-esp-idf/esp32s3", + "esp-rtos/esp32s3", + "esp-hal/esp32s3", +] + +[profile.release] +debug = true +debug-assertions = true +lto = "fat" +codegen-units = 1 + +[patch.crates-io] +embassy-time-driver = { git = "https://github.com/leftger/embassy", branch = "feat/usb-host" } +embassy-time-queue-utils = { git = "https://github.com/leftger/embassy", branch = "feat/usb-host" } +embassy-executor = { git = "https://github.com/leftger/embassy", branch = "feat/usb-host" } +embassy-executor-macros = { git = "https://github.com/leftger/embassy", branch = "feat/usb-host" } +embassy-executor-timer-queue = { git = "https://github.com/leftger/embassy", branch = "feat/usb-host" } +embassy-time = { git = "https://github.com/leftger/embassy", branch = "feat/usb-host" } +embassy-usb-host = { git = "https://github.com/leftger/embassy", branch = "feat/usb-host" } +embassy-usb-driver = { git = "https://github.com/leftger/embassy", branch = "feat/usb-host" } +embassy-usb-synopsys-otg = { git = "https://github.com/leftger/embassy", branch = "feat/usb-host" } diff --git a/examples/async/embassy_usb_hid_host/src/main.rs b/examples/async/embassy_usb_hid_host/src/main.rs new file mode 100644 index 00000000000..4d5aefa1ab3 --- /dev/null +++ b/examples/async/embassy_usb_hid_host/src/main.rs @@ -0,0 +1,100 @@ +//! CDC-ACM serial port example using embassy. +//! +//! This example should be built in release mode. +//! +//! The following wiring is assumed: +//! - DP => GPIO20 +//! - DM => GPIO19 + +#![no_std] +#![no_main] + +use embassy_executor::Spawner; +use embassy_usb_host::{UsbHost, class::hid::HidHost}; +use esp_backtrace as _; +use esp_hal::{ + interrupt::software::SoftwareInterruptControl, + otg_fs::{Usb, embassy_usb_host::Driver}, + timer::timg::TimerGroup, +}; +use log::*; + +esp_bootloader_esp_idf::esp_app_desc!(); + +#[esp_rtos::main] +async fn main(_spawner: Spawner) { + esp_println::println!("Init!"); + + esp_println::logger::init_logger_from_env(); + let peripherals = esp_hal::init(esp_hal::Config::default()); + + let sw_int = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT); + let timg0 = TimerGroup::new(peripherals.TIMG0); + esp_rtos::start(timg0.timer0, sw_int.software_interrupt0); + + let usb = Usb::new(peripherals.USB0, peripherals.GPIO20, peripherals.GPIO19); + let mut host = UsbHost::new(Driver::new(usb)); + info!("USB host initialized, waiting for device..."); + + loop { + // Wait for a device to connect + let speed = host.wait_for_connection().await; + info!("Device connected at speed {:?}", speed); + + // Enumerate the device + let mut config_buf = [0u8; 256]; + let result = host.enumerate(speed, &mut config_buf).await; + + let (dev_desc, addr, config_len) = match result { + Ok(r) => r, + Err(e) => { + error!("Enumeration failed: {:?}", e); + continue; + } + }; + + info!( + "Enumerated: VID={:04x} PID={:04x} addr={}", + dev_desc.vendor_id, dev_desc.product_id, addr + ); + + // Try to create a HID host driver + let mut hid = match HidHost::new( + host.driver(), + &config_buf[..config_len], + addr, + dev_desc.max_packet_size_0 as u16, + ) { + Ok(h) => h, + Err(e) => { + error!("HID init failed: {:?}", e); + continue; + } + }; + + // Disable idle repeat (STALL-tolerant: some devices don't support SET_IDLE) + if let Err(e) = hid.set_idle(0, 0).await { + error!("SET_IDLE failed: {:?}", e); + continue; + } + + info!("HID device ready, reading reports..."); + + // Read loop: log raw HID input reports + let mut buf = [0u8; 64]; + loop { + match hid.read(&mut buf).await { + Ok(n) if n > 0 => { + info!("HID report: {:x?}", &buf[..n]); + } + Ok(_) => {} + Err(e) => { + error!("HID read failed: {:?}", e); + break; + } + } + } + + info!("Device disconnected, waiting for next..."); + } +} From 672c50fa3323a8c8fdc58aaad6e81c2996b1ee4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1niel=20Buga?= Date: Mon, 30 Mar 2026 17:59:38 +0200 Subject: [PATCH 4/5] Clean up USB interrupt binding --- esp-hal/src/otg_fs/embassy_usb_device.rs | 11 ++++------- esp-hal/src/otg_fs/embassy_usb_host.rs | 9 +++------ esp-metadata-generated/src/_generated_esp32s2.rs | 12 +++++++----- esp-metadata-generated/src/_generated_esp32s3.rs | 10 ++++++---- esp-metadata/devices/esp32s2.toml | 2 +- esp-metadata/devices/esp32s3.toml | 2 +- examples/async/embassy_usb_hid_host/src/main.rs | 2 +- 7 files changed, 23 insertions(+), 25 deletions(-) diff --git a/esp-hal/src/otg_fs/embassy_usb_device.rs b/esp-hal/src/otg_fs/embassy_usb_device.rs index af27ffc813a..0ca297d121b 100644 --- a/esp-hal/src/otg_fs/embassy_usb_device.rs +++ b/esp-hal/src/otg_fs/embassy_usb_device.rs @@ -17,7 +17,7 @@ use embassy_usb_synopsys_otg::{ }; use procmacros::handler; -use crate::{otg_fs::Usb, peripherals::Interrupt, system::Cpu}; +use crate::{interrupt::Priority, otg_fs::Usb}; static DEVICE_STATE: State<{ Usb::MAX_EP_COUNT }> = State::new(); @@ -137,14 +137,11 @@ impl Bus<'_> { // Enable PHY clock r.pcgcctl().write(|w| w.0 = 0); - crate::interrupt::bind_handler(Interrupt::USB, interrupt_handler); + self._usb._usb0.bind_peri_interrupt(interrupt_handler); } fn disable(&mut self) { - crate::interrupt::disable(Cpu::ProCpu, Interrupt::USB); - - #[cfg(multi_core)] - crate::interrupt::disable(Cpu::AppCpu, Interrupt::USB); + self._usb._usb0.disable_peri_interrupt_on_all_cores(); Usb::_disable(); } @@ -186,7 +183,7 @@ impl Drop for Bus<'_> { } } -#[handler(priority = crate::interrupt::Priority::max())] +#[handler(priority = Priority::max())] fn interrupt_handler() { unsafe { on_interrupt(Driver::REGISTERS, &DEVICE_STATE, Usb::MAX_EP_COUNT) } } diff --git a/esp-hal/src/otg_fs/embassy_usb_host.rs b/esp-hal/src/otg_fs/embassy_usb_host.rs index fc1d94b77c5..752cb455328 100644 --- a/esp-hal/src/otg_fs/embassy_usb_host.rs +++ b/esp-hal/src/otg_fs/embassy_usb_host.rs @@ -14,7 +14,7 @@ use embassy_usb_synopsys_otg::{ otg_v1::Otg, }; -use crate::{handler, interrupt::Priority, otg_fs::Usb, peripherals::Interrupt, system::Cpu}; +use crate::{handler, interrupt::Priority, otg_fs::Usb}; #[handler(priority = Priority::max())] fn interrupt_handler() { @@ -49,7 +49,7 @@ impl<'d> Driver<'d> { }; Usb::_enable_host(); - crate::interrupt::bind_handler(Interrupt::USB, interrupt_handler); + peri._usb0.bind_peri_interrupt(interrupt_handler); Self { inner: OtgHostDriver::new(instance), @@ -82,10 +82,7 @@ impl<'d> UsbHostDriver for Driver<'d> { impl<'d> Drop for Driver<'d> { fn drop(&mut self) { - crate::interrupt::disable(Cpu::ProCpu, Interrupt::USB); - - #[cfg(multi_core)] - crate::interrupt::disable(Cpu::AppCpu, Interrupt::USB); + self._usb._usb0.disable_peri_interrupt_on_all_cores(); Usb::_disable(); } diff --git a/esp-metadata-generated/src/_generated_esp32s2.rs b/esp-metadata-generated/src/_generated_esp32s2.rs index 19e048a77e5..acde20db5bd 100644 --- a/esp-metadata-generated/src/_generated_esp32s2.rs +++ b/esp-metadata-generated/src/_generated_esp32s2.rs @@ -3581,7 +3581,8 @@ macro_rules! for_each_peripheral { enable_peri_interrupt, disable_peri_interrupt }))); _for_each_inner_peripheral!((@ peri_type #[doc = "UHCI0 peripheral singleton"] UHCI0 <= UHCI0() (unstable))); _for_each_inner_peripheral!((@ peri_type #[doc = - "USB0 peripheral singleton"] USB0 <= USB0() (unstable))); + "USB0 peripheral singleton"] USB0 <= USB0(USB : { bind_peri_interrupt, + enable_peri_interrupt, disable_peri_interrupt }) (unstable))); _for_each_inner_peripheral!((@ peri_type #[doc = "USB_WRAP peripheral singleton"] USB_WRAP <= USB_WRAP() (unstable))); _for_each_inner_peripheral!((@ peri_type #[doc = "XTS_AES peripheral singleton"] XTS_AES <= XTS_AES() (unstable))); @@ -3918,10 +3919,11 @@ macro_rules! for_each_peripheral { UART1 <= UART1(UART1 : { bind_peri_interrupt, enable_peri_interrupt, disable_peri_interrupt })), (@ peri_type #[doc = "UHCI0 peripheral singleton"] UHCI0 <= UHCI0() (unstable)), (@ peri_type #[doc = "USB0 peripheral singleton"] - USB0 <= USB0() (unstable)), (@ peri_type #[doc = "USB_WRAP peripheral singleton"] - USB_WRAP <= USB_WRAP() (unstable)), (@ peri_type #[doc = - "XTS_AES peripheral singleton"] XTS_AES <= XTS_AES() (unstable)), (@ peri_type - #[doc = "WIFI peripheral singleton"] WIFI <= WIFI(WIFI_MAC : { + USB0 <= USB0(USB : { bind_peri_interrupt, enable_peri_interrupt, + disable_peri_interrupt }) (unstable)), (@ peri_type #[doc = + "USB_WRAP peripheral singleton"] USB_WRAP <= USB_WRAP() (unstable)), (@ peri_type + #[doc = "XTS_AES peripheral singleton"] XTS_AES <= XTS_AES() (unstable)), (@ + peri_type #[doc = "WIFI peripheral singleton"] WIFI <= WIFI(WIFI_MAC : { bind_mac_interrupt, enable_mac_interrupt, disable_mac_interrupt }, WIFI_PWR : { bind_pwr_interrupt, enable_pwr_interrupt, disable_pwr_interrupt })), (@ peri_type #[doc = "DMA_SPI2 peripheral singleton"] DMA_SPI2 <= SPI2() (unstable)), (@ diff --git a/esp-metadata-generated/src/_generated_esp32s3.rs b/esp-metadata-generated/src/_generated_esp32s3.rs index 6eba140f47c..d0f1799512c 100644 --- a/esp-metadata-generated/src/_generated_esp32s3.rs +++ b/esp-metadata-generated/src/_generated_esp32s3.rs @@ -3738,9 +3738,10 @@ macro_rules! for_each_peripheral { disable_peri_interrupt }))); _for_each_inner_peripheral!((@ peri_type #[doc = "UHCI0 peripheral singleton"] UHCI0 <= UHCI0() (unstable))); _for_each_inner_peripheral!((@ peri_type #[doc = "USB0 peripheral singleton"] - USB0 <= USB0() (unstable))); _for_each_inner_peripheral!((@ peri_type #[doc = - "USB_DEVICE peripheral singleton"] USB_DEVICE <= USB_DEVICE(USB_DEVICE : { - bind_peri_interrupt, enable_peri_interrupt, disable_peri_interrupt }) + USB0 <= USB0(USB : { bind_peri_interrupt, enable_peri_interrupt, + disable_peri_interrupt }) (unstable))); _for_each_inner_peripheral!((@ peri_type + #[doc = "USB_DEVICE peripheral singleton"] USB_DEVICE <= USB_DEVICE(USB_DEVICE : + { bind_peri_interrupt, enable_peri_interrupt, disable_peri_interrupt }) (unstable))); _for_each_inner_peripheral!((@ peri_type #[doc = "USB_WRAP peripheral singleton"] USB_WRAP <= USB_WRAP() (unstable))); _for_each_inner_peripheral!((@ peri_type #[doc = "WCL peripheral singleton"] WCL @@ -4109,7 +4110,8 @@ macro_rules! for_each_peripheral { UART2 <= UART2(UART2 : { bind_peri_interrupt, enable_peri_interrupt, disable_peri_interrupt })), (@ peri_type #[doc = "UHCI0 peripheral singleton"] UHCI0 <= UHCI0() (unstable)), (@ peri_type #[doc = "USB0 peripheral singleton"] - USB0 <= USB0() (unstable)), (@ peri_type #[doc = + USB0 <= USB0(USB : { bind_peri_interrupt, enable_peri_interrupt, + disable_peri_interrupt }) (unstable)), (@ peri_type #[doc = "USB_DEVICE peripheral singleton"] USB_DEVICE <= USB_DEVICE(USB_DEVICE : { bind_peri_interrupt, enable_peri_interrupt, disable_peri_interrupt }) (unstable)), (@ peri_type #[doc = "USB_WRAP peripheral singleton"] USB_WRAP <= diff --git a/esp-metadata/devices/esp32s2.toml b/esp-metadata/devices/esp32s2.toml index 904cd7594f9..a0e51a4e2d6 100644 --- a/esp-metadata/devices/esp32s2.toml +++ b/esp-metadata/devices/esp32s2.toml @@ -84,7 +84,7 @@ peripherals = [ { name = "UART0", interrupts = { peri = "UART0" }, stable = true, clock_group = "UART" }, { name = "UART1", interrupts = { peri = "UART1" }, stable = true, clock_group = "UART" }, { name = "UHCI0", dma_peripheral = 3 }, - { name = "USB0" }, + { name = "USB0", interrupts = { peri = "USB" } }, { name = "USB_WRAP" }, { name = "XTS_AES" }, { name = "WIFI", interrupts = { mac = "WIFI_MAC", pwr = "WIFI_PWR" }, stable = true }, diff --git a/esp-metadata/devices/esp32s3.toml b/esp-metadata/devices/esp32s3.toml index a1532c72b57..d370f3fea5b 100644 --- a/esp-metadata/devices/esp32s3.toml +++ b/esp-metadata/devices/esp32s3.toml @@ -96,7 +96,7 @@ peripherals = [ { name = "UART1", interrupts = { peri = "UART1" }, stable = true, clock_group = "UART" }, { name = "UART2", interrupts = { peri = "UART2" }, stable = true, clock_group = "UART" }, { name = "UHCI0", dma_peripheral = 2 }, - { name = "USB0" }, + { name = "USB0", interrupts = { peri = "USB" } }, { name = "USB_DEVICE", interrupts = { peri = "USB_DEVICE" } }, { name = "USB_WRAP" }, { name = "WCL" }, diff --git a/examples/async/embassy_usb_hid_host/src/main.rs b/examples/async/embassy_usb_hid_host/src/main.rs index 4d5aefa1ab3..f7d7c4bfbd7 100644 --- a/examples/async/embassy_usb_hid_host/src/main.rs +++ b/examples/async/embassy_usb_hid_host/src/main.rs @@ -63,7 +63,7 @@ async fn main(_spawner: Spawner) { host.driver(), &config_buf[..config_len], addr, - dev_desc.max_packet_size_0 as u16, + dev_desc.max_packet_size0 as u16, ) { Ok(h) => h, Err(e) => { From dc3e6d74ee69ab583e9e16bf638c7f35f3ebb9c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1niel=20Buga?= Date: Thu, 2 Apr 2026 11:14:41 +0200 Subject: [PATCH 5/5] Add gamepad host example --- .../.cargo/config.toml | 25 ++++ .../async/embassy_usb_gamepad_host/Cargo.toml | 51 +++++++ .../embassy_usb_gamepad_host/src/main.rs | 135 ++++++++++++++++++ 3 files changed, 211 insertions(+) create mode 100644 examples/async/embassy_usb_gamepad_host/.cargo/config.toml create mode 100644 examples/async/embassy_usb_gamepad_host/Cargo.toml create mode 100644 examples/async/embassy_usb_gamepad_host/src/main.rs diff --git a/examples/async/embassy_usb_gamepad_host/.cargo/config.toml b/examples/async/embassy_usb_gamepad_host/.cargo/config.toml new file mode 100644 index 00000000000..d2cdabf30ee --- /dev/null +++ b/examples/async/embassy_usb_gamepad_host/.cargo/config.toml @@ -0,0 +1,25 @@ +[target.'cfg(target_arch = "riscv32")'] +runner = "espflash flash --monitor" +rustflags = [ + "-C", "link-arg=-Tlinkall.x", + "-C", "force-frame-pointers", +] + +[target.'cfg(target_arch = "xtensa")'] +runner = "espflash flash --monitor" +rustflags = [ + # GNU LD + "-C", "link-arg=-Wl,-Tlinkall.x", + "-C", "link-arg=-nostartfiles", + "-C", "link-arg=-Tdefmt.x", + + # LLD + # "-C", "link-arg=-Tlinkall.x", + # "-C", "linker=rust-lld", +] + +[env] +DEFMT_LOG = "info,embassy_usb_host=trace" + +[unstable] +build-std = ["core", "alloc"] diff --git a/examples/async/embassy_usb_gamepad_host/Cargo.toml b/examples/async/embassy_usb_gamepad_host/Cargo.toml new file mode 100644 index 00000000000..2cde89b187b --- /dev/null +++ b/examples/async/embassy_usb_gamepad_host/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "embassy-usb-hid_host" +version = "0.0.0" +edition = "2024" +publish = false + +[dependencies] +embassy-executor = "0.10.0" +embassy-futures = "0.1" +embassy-usb-host = { version = "0.1.0", default-features = false, features = ["defmt"] } +embassy-usb-driver = { version = "0.2.0", default-features = false } +esp-backtrace = { path = "../../../esp-backtrace", features = [ + "panic-handler", + "println", +] } +esp-bootloader-esp-idf = { path = "../../../esp-bootloader-esp-idf" } +esp-hal = { path = "../../../esp-hal", features = ["defmt", "unstable"] } +esp-rtos = { path = "../../../esp-rtos", features = ["embassy"] } +esp-println = { path = "../../../esp-println", features = ["defmt-espflash"] } +defmt = "1" + +[features] +esp32s2 = [ + "esp-backtrace/esp32s2", + "esp-bootloader-esp-idf/esp32s2", + "esp-rtos/esp32s2", + "esp-hal/esp32s2", +] +esp32s3 = [ + "esp-backtrace/esp32s3", + "esp-bootloader-esp-idf/esp32s3", + "esp-rtos/esp32s3", + "esp-hal/esp32s3", +] + +[profile.release] +debug = true +debug-assertions = true +lto = "fat" +codegen-units = 1 + +[patch.crates-io] +embassy-time-driver = { git = "https://github.com/embassy-rs/embassy" } +embassy-time-queue-utils = { git = "https://github.com/embassy-rs/embassy" } +embassy-executor = { git = "https://github.com/embassy-rs/embassy" } +embassy-executor-macros = { git = "https://github.com/embassy-rs/embassy" } +embassy-executor-timer-queue = { git = "https://github.com/embassy-rs/embassy" } +embassy-time = { git = "https://github.com/embassy-rs/embassy" } +embassy-usb-host = { git = "https://github.com/embassy-rs/embassy" } +embassy-usb-driver = { git = "https://github.com/embassy-rs/embassy" } +embassy-usb-synopsys-otg = { git = "https://github.com/embassy-rs/embassy" } diff --git a/examples/async/embassy_usb_gamepad_host/src/main.rs b/examples/async/embassy_usb_gamepad_host/src/main.rs new file mode 100644 index 00000000000..29ae01cad86 --- /dev/null +++ b/examples/async/embassy_usb_gamepad_host/src/main.rs @@ -0,0 +1,135 @@ +//! CDC-ACM serial port example using embassy. +//! +//! This example should be built in release mode. +//! +//! The following wiring is assumed: +//! - DP => GPIO20 +//! - DM => GPIO19 + +#![no_std] +#![no_main] + +use defmt::*; +use embassy_executor::Spawner; +use embassy_usb_driver::host::UsbHostDriver; +use embassy_usb_host::{ + UsbHost, + class::gip::{GipEvent, GipHost, RumbleCommand, XboxOneSGamepad}, +}; +use esp_backtrace as _; +use esp_hal::{ + interrupt::software::SoftwareInterruptControl, + otg_fs::{Usb, embassy_usb_host::Driver}, + timer::timg::TimerGroup, +}; + +esp_bootloader_esp_idf::esp_app_desc!(); + +#[esp_rtos::main] +async fn main(_spawner: Spawner) { + esp_println::println!("Init!"); + + let peripherals = esp_hal::init(esp_hal::Config::default()); + + let sw_int = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT); + let timg0 = TimerGroup::new(peripherals.TIMG0); + esp_rtos::start(timg0.timer0, sw_int.software_interrupt0); + + let usb = Usb::new(peripherals.USB0, peripherals.GPIO20, peripherals.GPIO19); + let mut host = UsbHost::new(Driver::new(usb)); + info!("USB host initialized, waiting for device..."); + + loop { + // Wait for a device to connect + let speed = host.wait_for_connection().await; + info!("Device connected at speed {:?}", speed); + + // Enumerate the device + let mut config_buf = [0u8; 256]; + let result = host.enumerate(speed, &mut config_buf).await; + + let (dev_desc, addr, config_len) = match result { + Ok(r) => r, + Err(e) => { + error!("Enumeration failed: {:?}", e); + continue; + } + }; + + info!( + "Enumerated: VID={:04x} PID={:04x} addr={}", + dev_desc.vendor_id, dev_desc.product_id, addr + ); + + let driver = host.driver(); + + let poll_for_disconnect = async { + loop { + if driver.wait_for_device_event().await + == embassy_usb_driver::host::DeviceEvent::Disconnected + { + warn!("Device disconnected"); + return; + } + } + }; + let handle_gamepad = async { + let result = GipHost::<_, XboxOneSGamepad>::try_register( + host.driver(), + &config_buf[..config_len], + addr, + dev_desc.vendor_id, + dev_desc.product_id, + ) + .await; + let mut gip = match result { + Ok(g) => { + info!("GIP device registered successfully"); + g + } + Err(e) => { + error!("GIP registration failed: {:?}", e); + return; + } + }; + loop { + match gip.poll().await { + Ok(GipEvent::Input(report)) => { + info!("GIP input report: {}", report); + let command = if report.a { + RumbleCommand { + left_trigger: 10, + right_trigger: 10, + strong: 10, + weak: 10, + } + } else { + RumbleCommand { + left_trigger: 0, + right_trigger: 0, + strong: 0, + weak: 0, + } + }; + + gip.set_rumble(&command).await.unwrap(); + } + Ok(GipEvent::GuideButton(pressed)) => { + info!( + "Guide button {}", + if pressed { "pressed" } else { "released" } + ); + } + Err(e) => { + error!("GIP poll error: {:?}", e); + break; + } + } + } + }; + + embassy_futures::select::select(poll_for_disconnect, handle_gamepad).await; + host.free_address(addr); + info!("Device disconnected, waiting for next..."); + } +}