Skip to content

Conversation

ijager
Copy link

@ijager ijager commented Sep 4, 2024

This PR adds USBHost in several layers:

  • USBHostDriver HAL for STM32 usb_v4 devices in embassy-stm32 crate
  • USBHostDriverTrait in embassy-usb-driver crate
  • USBHost implementation in embassy-usb crate
  • HIDKeyboard example for stm32g0

This is the implementation for issue #3295.

@i404788
Copy link

i404788 commented Oct 4, 2024

Hey @ijager,

I'm working on extending this PR to support 'universal' drivers (see wip @ https://github.com/i404788/embassy/tree/usb-asynch). I was wondering if there is a reason to have Channels be static (i.e. non-reconfigurable, and only one of In/Out). On the platform I'm looking to target (USBOTG), the current strategy will quickly consume all channels.

In the Hub handler example I'm working on, it's significantly more efficient to allocate 2 pipes: 1 for interrupt, and one for control IN/OUT, but that requires re-configuring the pipe.

Does the stm32 not support changing attributes? If it does, I think a shared Channel trait with a reconfigure(endpoint: &EndpointDescriptor) would improve the resource efficiency and convenience for one-shot packets. Only downside is that write and read may need to check if the direction is correct (could ChannelError::InvalidDirection if it's not supported; or just attempt the transaction on the specified side, I have at least one device which has bulk IN/OUT on the same endpoint_addr but with | 0x80 for in).

@nikvoid
Copy link
Contributor

nikvoid commented Oct 4, 2024

I'm also working in this direction. I completely redesigned channel and host API to make it usable from multiple concurrent tasks. On RP2040 you need to reconfigure hardware for each control/bulk/isochronous transfer and only interrupt channels are special. So in my variant channels other from interrupts are basically free, because the only resource they use is stack space. Interrupt channels are limited, but automatically freed on channel drop. Other type of channels should reconfigure hardware and block other tasks that want to issue transfers.

https://github.com/nikvoid/embassy/tree/rp2040-usb-host

https://github.com/nikvoid/embassy/blob/rp2040-usb-host/embassy-usb-driver/src/host.rs#L282
UsbChannel trait is parametrised by type and direction, where direction may be either In, Out, or InOut (I'm thinking of improving this part because InOut probably only suitable for Control pipes.

Updated host example: https://github.com/nikvoid/embassy/blob/rp2040-usb-host/examples/rp/src/bin/usb_host_keyboard.rs

@i404788
Copy link

i404788 commented Oct 5, 2024

@nikvoid Thanks for the notif.

That does look a lot closer to what I can work with. There is one difference that might cause an issue for USBOTG, there channels are limited (even non-interrupt) but each are independent (async-safe) and reconfigurable. Which is why I was looking at a reconfigure API for all channels (can still be type-state by consuming the channels); it seems like your RP2040 would support this (just changing values on the stack), and it would allow for the USBOTG to run without mutexes.

I will rebase to your branch, and if you're up for it we can collaborate on the final traits/interfaces.

@ijager
Copy link
Author

ijager commented Oct 6, 2024

Good that there's more people working on this. I am happy to change things up. If we have three targets and use cases together that's great for testing.

I am still on vacation for a week without computer so cannot really checks things right now.

@nikvoid
Copy link
Contributor

nikvoid commented Oct 6, 2024

OK. For now I'm going to write a hub driver and test it on few keyboards. By the way, what are good devices to test bulk and isochronous transfers on? I think mainly of mass storage and web camera, but I don't have one.

@i404788
Copy link

i404788 commented Oct 6, 2024

If you have a second RP2040, you could try the CDC-ACM, or MIDI drivers from embassy-usb in device mode (or just put it in usb flash mode).
If you happen to have another Raspberry Pi Zero or similar SBC you can use the linux gadget for mass storage.

Isochronous isn't used much, mainly audio & video equipment, if you have an android device you might be able to configure it as a webcam (or potentially in audio accessory mode: https://source.android.com/docs/core/audio/usb#accessoryAudio).

@nikvoid
Copy link
Contributor

nikvoid commented Oct 6, 2024

Now I have USB hub working for full speed devices, but not yet for low speed.
If I simply set preamble_en register before transfer to LS device, RxTimeout interrupt will be fired.
If that interrupt is disabled, it seems to work somehow. However INTERRUPT IN channel for keyboard is in dead silence.
Maybe I forgot something important to set on hub, or maybe another keyboard is really slow, or both.

@i404788
Copy link

i404788 commented Oct 6, 2024

Could be that you are missing the LS preamble on the interrupt channel, at least in USBOTG you need to set it per channel.

@nikvoid
Copy link
Contributor

nikvoid commented Oct 7, 2024

Yeah, indeed. I overlooked a register for that. Now it works, except for timeouts. There may be another register to configure it, I'll research on that later.

@nikvoid
Copy link
Contributor

nikvoid commented Oct 8, 2024

Timeouts appear to be a problem with specfic hub. Another hub is working fine.

@i404788
Copy link

i404788 commented Oct 9, 2024

@nikvoid I've converted most of your hub driver to a usb driver trait: https://github.com/i404788/embassy/blob/usb-asynch/embassy-usb/src/handlers/hub.rs).

If you could review the general structure, especially wait_for_event since that's where I'm least sure about the interface.
I've also removed a lot of the dependencies from the driver itself, so the overarching layer can more easily be changed.

@nikvoid
Copy link
Contributor

nikvoid commented Oct 9, 2024

Well, I don't see a reason to create device driver trait. I have updated rp/usb_host_hid_keyboard example with hub usage, as you can see, user can just try all drivers with some priorities, then create task for device. I think this is the very flexible way.

About handler trait: what purpose it serves? I see that it uses raw UsbHostDriver and channels. Could you please provide details about its usage? I also spotted several suspicious lines:

https://github.com/i404788/embassy/blob/fb70372f52f3615e92863ef01afcb5d176e5d4c5/embassy-usb/src/handlers/hub.rs#L115
On RP2040 there is a special register for interval, which is already used by driver. Even if it is not available on other platform, its driver could just use Timer::after as polyfill for periodic poll.

https://github.com/i404788/embassy/blob/fb70372f52f3615e92863ef01afcb5d176e5d4c5/embassy-usb/src/handlers/hub.rs#L119
Value returned by interrupt endpoint is a bitmap, where each bit corresponds to port, starting from bit 1

https://github.com/i404788/embassy/blob/fb70372f52f3615e92863ef01afcb5d176e5d4c5/embassy-usb/src/handlers/hub.rs#L53
1st argument is not a device address, it's rather an endpoint address (and yes, this is redundant because control channel are always 0, FIXME)

@i404788
Copy link

i404788 commented Oct 9, 2024

Thank you for the review.

About handler trait: what purpose it serves?

The main purpose is to create a common interface for drivers such that a user can easily re-use them & create a common initialization sequence. This allows (for example) for automatic enumeration & initialization of drivers from a registry without mapping out what each driver needs to get started; this is effectively similar to how larger OSes do it.

As for why UsbHostDriver instead of UsbHost, it's because UsbHost doesn't seem to provide anything you'd need in a driver while increasing the restrictions on usage. I'm aiming to avoid having to pigeonhole the drivers into embassy-usb for future development.

https://github.com/i404788/embassy/blob/fb70372f52f3615e92863ef01afcb5d176e5d4c5/embassy-usb/src/handlers/hub.rs#L115

I didn't know you had integrated the HW interrupt into the request_in, since it seems like it would do a manual request; I had suspected you got timeouts due to sending interrupt requests too frequently. I think we might want to make a separate interface for the interrupt request to avoid confusing with on-demand requests, but let me know your thoughts on that.

https://github.com/i404788/embassy/blob/fb70372f52f3615e92863ef01afcb5d176e5d4c5/embassy-usb/src/handlers/hub.rs#L119

Ah yes I see what you mean, I misinterpreted your impl and tried to optimize; I'll fix that.

https://github.com/i404788/embassy/blob/fb70372f52f3615e92863ef01afcb5d176e5d4c5/embassy-usb/src/handlers/hub.rs#L53

Ditto, my mistake.

@nikvoid
Copy link
Contributor

nikvoid commented Oct 9, 2024

The main purpose is to create a common interface for drivers such that a user can easily re-use them & create a common initialization sequence. This allows (for example) for automatic enumeration & initialization of drivers from a registry without mapping out what each driver needs to get started; this is effectively similar to how larger OSes do it.

I think the main problem with this is that we are on embedded device without Box and alloc, and with statically sized... let's call them "device driver structs", we need some way to return these different structs from automatic initialization method.
Correct me if I misunderstood what you mean.

I didn't know you had integrated the HW interrupt into the request_in, since it seems like it would do a manual request; I had suspected you got timeouts due to sending interrupt requests too frequently. I think we might want to make a separate interface for the interrupt request to avoid confusing with on-demand requests, but let me know your thoughts on that.

Usb is host-centric, and even interrupts are not interrupts, but rather poll-based things, so I though it would be appropriate name. We could just state about this behaviour in docs and/or rename method, e.g. to transfer_in/out

@i404788
Copy link

i404788 commented Oct 9, 2024

While I agree we should assume no-alloc for any infra code, I do regularly use alloc in project development, with the traits you now have the options. Functionally, I don't think your struct pattern is much worse; just that it seems to me most drivers don't need to assume it will run on embassy, rather any async rust would have the equivalent code; so a trait would allow for more portability and for 3rd party drivers that neatly integrate (similar to usbh with their Driver trait, or embedded-hal).

A few things I was planning to build based on the trait was a faster enumeration filter using the static spec, also with the new structure of the hub you don't need a mutexed device registry (or no registry at all just unique address assignment).

Btw the only reason I'm calling them handlers is because driver was already used to describe the UsbHostDriver and gadget-side Driver traits; but it seems like Handler is also already a trait so maybe it should just be UsbDriver or Driver and accept the potential confusion.

even interrupts are not interrupts, but rather poll-based things, so I though it would be appropriate name

If you configure HW interrupt polling you may still manually poll, at least on USBOTG (maybe that's a unnecessary use-case to consider). If we only allow automatic polling I think just documenting the behavior around this would be good enough. (I do agree that the HostDriver should ensure software polling works the same as hw polling with polyfills)

@nikvoid
Copy link
Contributor

nikvoid commented Oct 10, 2024

So, if I finally understand correctly, handler trait will be something like a middleware between device driver and usb host driver (or just device driver that stands directly over bus driver).

And I have more questions:
https://github.com/i404788/embassy/blob/fb70372f52f3615e92863ef01afcb5d176e5d4c5/embassy-usb/src/handlers/mod.rs#L67
This also not the case for rp2040, so handler probably would need to impl Drop.

https://github.com/i404788/embassy/blob/fb70372f52f3615e92863ef01afcb5d176e5d4c5/embassy-usb/src/handlers/mod.rs#L69
Currently all UsbHostDriver methods take &self, so mut is not necessary. As mentioned in previous question, dropping channels is a handler's responsibility, so it needs to store reference to UsbHostDriver anyway.

https://github.com/i404788/embassy/blob/fb70372f52f3615e92863ef01afcb5d176e5d4c5/embassy-usb/src/handlers/mod.rs#L87
UsbResumableHandler is needed to talk with more devices when channels are limited, right?

@i404788
Copy link

i404788 commented Oct 11, 2024

This also not the case for rp2040, so handler probably would need to impl Drop.

Hmm I was hoping this could be done in the Channel itself to avoid complicating the trait & implementation; e.g. give a Ref<AtomicU32>, and a it's own index, which it can use to xor to mark it is cleared & clear the registers needed if it's an hw interrupt pipe.

Currently all UsbHostDriver methods take &self, so mut is not necessary. As mentioned in previous question, dropping channels is a handler's responsibility, so it needs to store reference to UsbHostDriver anyway.

True I suppose it could be &self, then the assumption is that we have internal mutability (in our case usually registers, but if potentially in other drivers separate data needs to be managed) that the driver has to manage.

UsbResumableHandler is needed to talk with more devices when channels are limited, right?

Correct, so at a higher layer it can be decided to suspend a device in order to start/resume a different device.

@ijager
Copy link
Author

ijager commented Nov 11, 2024

@i404788, I have included all latest changes of your branch and have now a working example for STM32G0 based on the new traits.

Regarding the async fn wait_for_device_event(&self) -> embassy_usb_driver::host::DeviceEvent API, It might be better to actually pass in the event you're expecting. Because currently it is not fully defined what to wait for.

If a device is already connected and then I call wait_for_device_event(&self), expecting to get a Connected Event, I will never get it. If we pass in the Connected event then we can immediately return if the device was already connected.

Perhaps as an optional, so

// either 

async fn wait_for_device_event(&self, expected_event: host::DeviceEvent) ->host::DeviceEvent

// or

async fn wait_for_device_event(&self, expected_event: Option<host::DeviceEvent>) ->host::DeviceEvent

If passing in expected_event = None we could just wait for any future event. But currently only Connected and Disconnected exist.

@ijager
Copy link
Author

ijager commented Nov 11, 2024

Hmm also not ideal as we have to pass in the Device Speed, which is dummy:

    debug!("Detecting device");
    // Wait for root-port to detect device
    let speed = loop {
        match usbhost.wait_for_device_event(Connected(USBSpeed::Full)).await {
            Connected(speed) => break speed,
            _ => {}
        }
    };

@i404788
Copy link

i404788 commented Nov 14, 2024

It just doesn't respond at all to the request setup packet. No stall. Continuing with a new, different request will make it respond again.

Hmm that does sound strange, it could also be a bad request I recently got this with USBOTG as well. For example if the CRC is incorrect the device will just not respond.

default timeout (e.g. 50ms/500ms for low and full speed), and a UsbChannel method set_timeout to override it.

Sounds good, I'll see if I can get a decent interface around it for the configuration.

Regarding the async fn wait_for_device_event(&self) -> embassy_usb_driver::host::DeviceEvent API, It might be better to actually pass in the event you're expecting. Because currently it is not fully defined what to wait for.

I can see your reasoning here, but wait_for_device_event needs to serialize the events rather (i.e. Disconnected can only be returned if Connected was returned before, vice-versa), as I think ignoring certain events may cause additional unsoundness. Disconnects always need to be handled since channels and drivers will be invalidated (or at least made non-functional).
I'm open to other suggestions on how to improve it, because I agree it's not as intuitive as it could be.

@ijager
Copy link
Author

ijager commented Nov 14, 2024

You are right, the driver can be a state machine. Then we could make sure we always get connect before disconnect.

For unexpected disconnects, those are usually noticed because of failed channel operations/transfers. Is the idea to call wait_for_device_event() after detecting a disconnect due to a ChannelError?

Currently my USB task looks like this:

 loop {
        // Wait for device connected
        usbhost.wait_for_device_event().await;
        // do enumeration etc ...
        let Ok(mut kbd) = KbdHandler::try_register(&usbhost, enum_info).await else {
            ...
            break;
        };

        // Read device endpoints until disconnect
        loop {
            match kbd.wait_for_event().await {
                Ok(HandlerEvent::HandlerEvent(KbdEvent::KeyStatusUpdate(ev))) => {
                ...
                }
                Err(HostError::ChannelError(ChannelError::Disconnected)) | Ok(HandlerEvent::HandlerDisconnected) => {
                    info!("Device disconnected");
                    break;
                }
                other => warn!("Unhandled event: {:?}", other),
            }
        }
    // kbd is dropped here
    }

So I don't really need to wait_for_disconnect_event, as the break will make sure everything is dropped. What is your usecase for disconnect, do you have a different task that handles DeviceEvents?

@nikvoid
Copy link
Contributor

nikvoid commented Nov 16, 2024

For the RP2040 I checked the datasheet, RX_TIMEOUT seems like it would be triggered in case of NAK response as well so it's possible the device responded and requested to try again later (busy). @nikvoid could you check if this is the case for your slow hub?

EDIT: it seems like RP2040 unlike USBOTG does automatic retry given NAK_POLL register, but I'm still curious to know if it's getting a NAK or if it's simply not replying since the spec timeouts for NAKs are really long: https://beyondlogic.org/usbnutshell/usb6.shtml#SetupPacket.

I tried to take a closer look into this issue.
I captured communication through faulty hub with logic analyzer, and it seems like keyboard behind the hub doesn't respond with ACKs, or hub drops them. No NAKs, nothing. After 1ms hardware repeats the request and sometimes gets ACK (this was a bug - transactions weren't stopped after error), then software passes the partial descriptor request, but fails in next ones, as they only have single try attempt.
The average retry count to get partial device descriptor is 5.

fails in random places:
SETUP | No ACK     | SETUP | ACK | IN | DATA | OUT | No ACK       | ... wait and retry ... | SETUP | ...
'--setup -- timeout  '-- setup --- data in --- status -- timeout

EDIT: RX timeout seems to be no serious error, but rather an indication that hardware is about to repeat request. So +1 to adding a failsafe timeout.

let mut desc_len = slice[offset] as usize;
let mut desc_type = slice[offset + 1];

while desc_type != T::DESC_TYPE || desc_len != T::SIZE {
Copy link

@jakerr jakerr Nov 24, 2024

Choose a reason for hiding this comment

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

Only accepting descriptors that are the exact size expected as is done in this line would make the descriptor fail to parse if there were existance of any extentions to the descriptors such as those that exist in USB 2.0+.

For example MIDI 2.0 DeviceEndpoints can potentially have two extra bytes in positions 7 and 8 and so the DeviceEndpointDescriptor bLength may be 9 rather than 7.

Offset Field Size Value Description
0-6 (same as in USB 1.1)
7 bRefresh 1 0x00 Unused
8 bSynchAddress 1 0x00 Unused

What this means is in practice if you assume exact sizes, newer devices will be ignored by this driver if they use any extensions even though they should be backwards compatible.

The assertion that bytes beyond the expected size must be ignored is mentioned briefly in the USB spec in Chapter 9.5 "Descriptors":

If a descriptor returns with a value in its length field that is less than defined by this specification, the
descriptor is invalid and should be rejected by the host. If the descriptor returns with a value in its length field that is greater than defined by this specification, the extra bytes are ignored by the host, but the next
descriptor is located using the length returned rather than the length expected.

This would also be relevant to parse_endpoints (and other descriptor parsing logic above) which assumes the record length when advancing the working_buffer slice working_buffer = &working_buffer[EndpointDescriptor::SIZE..]; as that could break if some bytes in the extended area happened to collide with a valid start to a descriptor.

I think it would be best to refactor the code to always advance the slice being considered by the amount specified by bLength of the current slice, and never assume that it's safe to advance by a static amount.

Copy link

Choose a reason for hiding this comment

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

A sent an attempted fix as another PR targeting this PR branch. Please take a look :)

@jakerr
Copy link

jakerr commented Dec 4, 2024

This branch currently has quite a few commits mixed in that are duplicates of things that have already been merged into main that I think make it a bit unwieldy to review.

I tried my hand at rebasing it in my fork's usb-host that did require a couple conflict resolutions but hopefully I got them right.
Just leaving it here for the authors in case it's helpul.

@JYouren
Copy link
Contributor

JYouren commented Dec 28, 2024

Would this work support (virtual) serial ports easily?

@i404788
Copy link

i404788 commented Dec 28, 2024

@JYouren Yes, one of the drivers I made for myself is very similar to USB Serial (technically mine is a printer port but works largely the same), and it's quite easy to implement.

@JYouren
Copy link
Contributor

JYouren commented Dec 28, 2024

Awesome, you wouldn't be able to sign post me to that would you so I can have a look?

@i404788
Copy link

i404788 commented Dec 28, 2024

It isn't public but here are the important parts:

  • Make a struct with all desired/required channels
  • Implement UsbHostHandler to lookup & verify the endpoints of those channels for a new device
  • Add your own functions related to the specific function of the device (i.e. try_write, try_read)
Code:
pub struct Handler<H: UsbHostDriver> {
    bulk_in_channel: H::Channel<channel::Bulk, channel::In>,
    bulk_out_channel: H::Channel<channel::Bulk, channel::Out>,
    control_channel: H::Channel<channel::Control, channel::InOut>,
}

impl<H: UsbHostDriver> Handler<H> {
    pub async fn try_read<'a>(&mut self, buf: &'a mut [u8]) -> Result<&'a [u8], ChannelError> {
        let actual_read = self.bulk_in_channel.request_in(buf).await?;
        Ok(&buf[..actual_read])
    }

    pub async fn try_write(&mut self, buf: &[u8]) -> Result<(), ChannelError> {
        match select::select(
            Timer::after_millis(200),
            self.bulk_out_channel.request_out(buf),
        )
        .await
        {
            select::Either::First(_) => Err(ChannelError::Timeout),
            select::Either::Second(res) => {
                res?;
                Ok(())
            }
        }
    }
}

impl<H: UsbHostDriver> UsbHostHandler for Handler<H> {
    type PollEvent = ();
    type Driver = H;

    fn static_spec() -> StaticHandlerSpec {
        StaticHandlerSpec::new(None)
    }

    async fn try_register(bus: &H, enum_info: EnumerationInfo) -> Result<Self, RegisterError> {
        println!("Trying to register");
        for iface in enum_info.cfg_desc.iter_interface() {
            debug!("iface: {}", iface);
        }

        let iface = enum_info
            .cfg_desc
            .iter_interface()
            .find(|v| {
                matches!(
                    v,
                    InterfaceDescriptor {
                        interface_class: 0x07,
                        interface_subclass: 0x1,
                        interface_protocol: 0x2,
                        ..
                    }
                )
            })
            .ok_or(RegisterError::NoSupportedInterface)?;

        debug!("Found matching iface!");

        let interrupt_ep = iface
            .iter_endpoints()
            .find(|v| v.ep_type() == EndpointType::Interrupt && v.ep_dir() == Direction::In)
            .ok_or(RegisterError::NoSupportedInterface)?;

        debug!("Found matching intr ep");

        let bulk_in_ep = iface
            .iter_endpoints()
            .find(|v| v.ep_type() == EndpointType::Bulk && v.ep_dir() == Direction::In)
            .ok_or(RegisterError::NoSupportedInterface)?;

        debug!("Found bulk in ep");

        let bulk_out_ep = iface
            .iter_endpoints()
            .find(|v| v.ep_type() == EndpointType::Bulk && v.ep_dir() == Direction::Out)
            .ok_or(RegisterError::NoSupportedInterface)?;

        debug!("Found all endpoints");

        let bulk_in_channel = bus.alloc_channel(
            enum_info.device_address,
            &bulk_in_ep.into(),
            enum_info.ls_over_fs,
        )?;

        let bulk_out_channel = bus.alloc_channel(
            enum_info.device_address,
            &bulk_out_ep.into(),
            enum_info.ls_over_fs,
        )?;

        let control_channel = bus.alloc_channel(
            enum_info.device_address,
            &EndpointInfo::new(
                0.into(),
                EndpointType::Control,
                (enum_info.device_desc.max_packet_size0 as u16)
                    .min(enum_info.speed.max_packet_size()),
            ),
            enum_info.ls_over_fs,
        )?;

        Ok(Handler {
            control_channel,
            bulk_in_channel,
            bulk_out_channel,
        })
    }
}

The main difference for the serial version will be the InterfaceDescriptor matching, the rest can likely stay the same.

Then you can use it similarly to how I've used it here for kbd (note it does not handle disconnects or use any custom functions): https://github.com/i404788/esp-hal/blob/719d96101174fcfbd718b6bdb0177fe65d46a86f/examples/src/bin/embassy_usb_host.rs#L75-L97

@mikeemoo
Copy link

It this still under development? I'd really love USB host support

@deluxetiky
Copy link

It this still under development? I'd really love USB host support

Actually we are also waiting for USB host support! If we know latest state, it would be great.

@Codetector1374
Copy link
Contributor

Codetector1374 commented Mar 17, 2025

@Dummyc0m and I are trying to create an implementation here: https://github.com/vapor-keeb/async-usb-host/

very early stage, still fighting with a lot of design challenge with how to support hot plug and unplug multiple (kinds) of devices.

We got enumeration / hubs working so far. Trying to support a real device (Likely HID) next

@i404788
Copy link

i404788 commented Mar 17, 2025

@mikeemoo @deluxetiky The reason it's pretty quiet is because it's mostly complete, it already works on rp2040, stm32 and esp-hal. Although they may need some adjustments for upstream.

The last item open is the optional 'Transaction Timeout' which admittedly I've been procrastinating on since it's not needed for my use case. I'll try to rebase to main & add it this week, and open a final PR (or @ijager can rebase if he's available).

@Codetector1374 Neat, hot-plug is the hardest part indeed. Seems like it's duplicating a lot of the work of this PR, if you're just interested in a different implementation don't let me stop you; but if it's just for the functionality it's already finished.

@ijager
Copy link
Author

ijager commented Mar 17, 2025

I am also using this branch in a completed project since end of last year. I will probably get back to this as the client has asked for HUB support as well, but not in the next few months. So totally fine to open a new PR and then I can close this one.

@AlexCharlton
Copy link
Contributor

I've also spun off a branch to fix some issues present in this one (but otherwise, I'm very appreciative of the work that was put in here!)

While there are a number of commits in the branch, there are really only three issues to speak of.

First, EndpointIterator is broken if you have more than one endpoint. This is a fix, but I would say there's more that could be done for descriptor traversal. InterfaceIterator does not find alternate interfaces, nor does it correctly capture its buffer beyond the first one (though I'd say the concept of an interface buffer is a bit fraught thanks to the existence of alternate interfaces). Some additional ways of iterating through descriptors would also be useful. Otherwise, handlers need to do their own parsing of the ConfigurationDescriptor buffer if they want to access any class-specific descriptors. This additional functionality could readily be done in future PRs, though, so I wouldn't say it's crucial for the first pass.

Second, assuming I haven't misunderstood something about how USB works (which could well be the case) I don't think it's possible for wait_for_device_event to accurately return the bus speed of high speed devices upon a connection without first issuing a reset. This commit documents this fact and removes the call to bus_reset from enumerate_root, since there should be no need to do so at that point. What I have not done in this branch is square up the existing implementations of bus_reset.

Third, with only 256 bytes provided, it's not hard to exceed the configuration buffer size, causing a panic. This commit bumps the size to 512 bytes, which happens to be large enough for the device I'm testing with, but some configurability would probably be good. I've also added a guard, so that we get an error rather than a panic.

@i404788
Copy link

i404788 commented Mar 18, 2025

@AlexCharlton the first issue seems to have to do with the alignment of the endpoints (if the enpoints are aligned you won't encounter the issue, which likely was in our cases). You're right that this general area could be expanded upon to support more complex device configurations.

For point 2, I believe it depends on the hardware IP used, iirc USBOTG will do the speed detection for you. I could be wrong here as well, but detection worked correctly as far as I've tested with esp-hal, it's possible a redundant bus_reset slipped in.

For the configuration buffer, it seems like they can be up to 65535 bytes (total_len/wTotalLength), so I think we'll have to make some gradations on the size configs to not blow up the stack.

I appreciate your testing, I'll collate & add your improvements into the final PR.

@i404788 i404788 mentioned this pull request Apr 1, 2025
4 tasks
@i404788
Copy link

i404788 commented Apr 1, 2025

I've opened a new PR at #4032.

@AlexCharlton I've refactored the Descriptor parsing (also integrating @jakerr's test cases) & SetConfiguration procedure, so now the application or handler can choose the configuration through EnumerationInfo and can provide their own arbitrarily long buffer for it. The example handlers still use the 512 limit to read the active or default config. Let me know what you think.

@AlexCharlton
Copy link
Contributor

I've opened a new PR at #4032.

@AlexCharlton I've refactored the Descriptor parsing (also integrating @jakerr's test cases) & SetConfiguration procedure, so now the application or handler can choose the configuration through EnumerationInfo and can provide their own arbitrarily long buffer for it. The example handlers still use the 512 limit to read the active or default config. Let me know what you think.

Makes sense to me!

@ijager
Copy link
Author

ijager commented Apr 9, 2025

Closed in favor of #4032

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.