USB-4: Supporting more Standard Requests
After responding to the GET_DESCRIPTOR Device request the host will start sending different requests. Let's identify those, and then handle them.
Update the parser
The starter nrf52-code/usb-lib package contains unit tests for everything we need. Some of them have been commented out using a #[cfg(TODO)] attribute.
✅ Remove all #[cfg(TODO)] attributes so that everything is enabled.
✅ Update the parser in nrf52-code/usb-lib to handle GET_DESCRIPTOR requests for Configuration Descriptors.
When the host issues a GET_DESCRIPTOR Configuration request the device needs to respond with the requested configuration descriptor plus all the interface and endpoint descriptors associated to that configuration descriptor during the DATA stage.
As a reminder, all GET_DESCRIPTOR request types share the following properties:
bmRequestTypeis 0b10000000bRequestis 6 (i.e. the GET_DESCRIPTOR Request Code, defined in Table 9-4 of the USB specification)
A GET_DESCRIPTOR Configuration request is determined by the high byte of its wValue field:
- The high byte of
wValueis 2 (i.e. theCONFIGURATIONdescriptor type, defined in Table 9-5 of the USB specification)
✅ Update the parser in nrf52-code/usb-lib to handle SET_CONFIGURATION requests.
See the section on SET_CONFIGURATION for details on how to do this.
Once you've completed this, all your test cases should pass. If not, fix the code until they do!
Help
If you need a reference, you can find solutions to parsing GET_DESCRIPTOR Configuration and SET_CONFIGURATION requests in the following files:
Each file contains just enough code to parse the request in its name and the GET_DESCRIPTOR Device and SET_ADDRESS requests. So you can refer to nrf52-code/usb-lib-solutions/get-descriptor-config without getting "spoiled" about how to parse the SET_CONFIGURATION request.
Update the application
We're now going to be using nrf52-code/usb-app/src/bin/usb-4.rs.
Since the logic of the EP0SETUP event handling is getting more complex with each added event, you can see that usb-4.rs was refactored to add error handling: the event handling now happens in a separate function that returns a Result. When it encounters an invalid host request, it returns the Err variant which can be handled by stalling the endpoint:
fn on_event(/* parameters */) {
match event {
/* ... */
Event::UsbEp0Setup => {
if ep0setup(/* arguments */).is_err() {
// unsupported or invalid request:
// TODO add code to stall the endpoint
defmt::warn!("EP0IN: unexpected request; stalling the endpoint");
}
}
}
}
fn ep0setup(/* parameters */) -> Result<(), ()> {
let req = Request::parse(/* arguments_*/)?;
// ^ early returns an `Err` if it occurs
// TODO respond to the `req`; return `Err` if the request was invalid in this state
Ok(())
}
Note that there's a difference between the error handling done here and the error handling commonly done in std programs. std programs usually bubble up errors to the top main function (using the ? operator), report the error (or chain of errors) and then exit the application with a non-zero exit code. This approach is usually not appropriate for embedded programs as
maincannot return,- there may not be a console to print the error to and/or
- stopping the program, and e.g. requiring the user to reset it to make it work again, may not be desirable behavior.
For these reasons in embedded software errors tend to be handled as early as possible rather than propagated all the way up.
This does not preclude error reporting. The above snippet includes error reporting in the form of a defmt::warn! statement. This log statement may not be included in the final release of the program as it may not be useful, or even visible, to an end user but it is useful during development.
✅ For each green test, extend usb-4.rs to handle the new requests your parser is now able to recognize.
If that's all the information you need - go ahead! If you'd like some more detail, read on.
Dealing with unknown requests: Stalling the endpoint
You may come across host requests other than the ones listed in previous sections.
For this situation, the USB specification defines a device-side procedure for "stalling an endpoint", which amounts to the device telling the host that it doesn't support some request.
This procedure should be used to deal with invalid requests, requests whose
SETUPstage doesn't match any USB 2.0 standard request, and requests not supported by the device – for instance theSET_DESCRIPTORrequest is not mandatory.
✅ Use the dk::usbd::ep0stall() helper function to stall endpoint 0 in nrf52-code/usb-app/src/bin/usb-4.rs if an invalid request is received.
Updating Device State
At some point during the initialization you'll receive a SET_ADDRESS request that will move the device from the Default state to the Address state. If you are working on Linux, you'll also receive a SET_CONFIGURATION request that will move the device from the Address state to the Configured state. Additionally, some requests are only valid in certain states– for example SET_CONFIGURATION is only valid if the device is in the Address state. For this reason usb-4.rs will need to keep track of the device's current state.
The device state should be tracked using a resource so that it's preserved across multiple executions of the USBD event handler. The usb2 crate has a State enum with the 3 possible USB states: Default, Address and Configured. You can use that enum or roll your own.
✅ Start tracking and updating the device state to move your request handling forward.
Update the handling of the USBRESET event
Instead of ignoring it, we now want it to change the state of the USB device. See section 9.1 USB Device States of the USB specification for details on what to do. Note that fn on_event() was given state: &mut State.
Update the handling of SET_ADDRESS requests
This request should come right after the
GET_DESCRIPTOR Devicerequest if you're using Linux, or be the first request sent to the device by macOS.
A SET_ADDRESS request has the following fields as defined by Section 9.4.6 Set Address of the USB spec:
bmrequesttypeis 0b00000000brequestis 5 (i.e. the SET_ADDRESS Request Code, see table 9-4 in the USB spec)wValuecontains the address to be used for all subsequent accesseswIndexandwLengthare 0, there is nowData
It should be handled as follows:
-
If the device is in the
Defaultstate, then- if the requested address stored in
wValuewas0(Nonein theusbAPI) then the device should stay in theDefaultstate - otherwise the device should move to the
Addressstate
- if the requested address stored in
-
If the device is in the
Addressstate, then- if the requested address stored in
wValuewas0(Nonein theusbAPI) then the device should return to theDefaultstate - otherwise the device should remain in the
Addressstate but start using the new address
- if the requested address stored in
-
If the device is in the
Configuredstate this request results in "unspecified" behavior according to the USB specification. You should stall the endpoint in this case.
Note: According to the USB specification the device needs to respond to this request with a STATUS stage -- the DATA stage is omitted. The nRF52840 USBD peripheral will automatically issue the STATUS stage and switch to listening to the requested address (see the USBADDR register) so no interaction with the USBD peripheral is required for this request.
For more details, read the introduction of section 6.35.9 of the nRF52840 Product Specification 1.0.
Implement the handling of GET_DESCRIPTOR Configuration requests
So how should we respond to the host when it wants our Configuration Descriptor? As our only goal is to be enumerated we'll respond with the minimum amount of information possible.
✅ First, check the request
Configuration descriptors are requested by index, not by their configuration value. Since we reported a single configuration in our device descriptor the index in the request must be zero. Any other value should be rejected by stalling the endpoint (see section Dealing with unknown requests: Stalling the endpoint for more information).
✅ Next, create and send a response
The response should consist of the configuration descriptor, followed by interface descriptors and then by (optional) endpoint descriptors. We'll include a minimal single interface descriptor in the response. Since endpoints are optional we will include none.
The configuration descriptor and one interface descriptor will be concatenated in a single packet so this response should be completed in a single DATA stage.
The configuration descriptor in the response should contain these fields:
wTotalLength = 18= one configuration descriptor (9 bytes) and one interface descriptor (9 bytes)bNumInterfaces = 1, a single interface (the minimum value)bConfigurationValue = 42, any non-zero value will doiConfiguration = 0, string descriptors are not supportedbmAttributes { self_powered: true, remote_wakeup: false }, self-powered due to the debugger connectionbMaxPower = 250(500 mA), this is the maximum allowed value but any (non-zero?) value should do
The interface descriptor in the response should contain these fields:
bInterfaceNumber = 0, this is the first, and only, interfacebAlternateSetting = 0, alternate settings are not supportedbNumEndpoints = 0, no endpoint associated to this interface (other than the control endpoint)bInterfaceClass = bInterfaceSubClass = bInterfaceProtocol = 0, does not adhere to any specified USB interfaceiInterface = 0, string descriptors are not supported
Again, we strongly recommend that you use the usb2::configuration::Descriptor and usb2::interface::Descriptor abstractions here. Each descriptor instance can be transformed into its byte representation using the bytes method -- the method returns an array. To concatenate both arrays you can use an stack-allocated heapless::Vec buffer. If you haven't used the heapless crate before you can find example usage in the the src/bin/vec.rs file.
NOTE: the
usb2::configuration::Descriptorandusb2::interface::Descriptorstructs do not havebLengthandbDescriptorTypefields. Those fields have fixed values according to the USB spec so you cannot modify or set them. Whenbytes()is called on theDescriptorvalue, the returned array (which contains a binary representation of the descriptor, packed according to the USB 2.0 standard) will contain those fields set to their correct value.