After responding to the GET_DESCRIPTOR Device
request the host will start sending different requests. Let's identify those, and then handle them.
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:
bmRequestType
is 0b10000000bRequest
is 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
wValue
is 2 (i.e. theCONFIGURATION
descriptor 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!
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.
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
main
cannot 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.
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
SETUP
stage doesn't match any USB 2.0 standard request, and requests not supported by the device – for instance theSET_DESCRIPTOR
request 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.
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.
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
.
This request should come right after the
GET_DESCRIPTOR Device
request 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:
bmrequesttype
is 0b00000000brequest
is 5 (i.e. the SET_ADDRESS Request Code, see table 9-4 in the USB spec)wValue
contains the address to be used for all subsequent accesseswIndex
andwLength
are 0, there is nowData
It should be handled as follows:
-
If the device is in the
Default
state, then- if the requested address stored in
wValue
was0
(None
in theusb
API) then the device should stay in theDefault
state - otherwise the device should move to the
Address
state
- if the requested address stored in
-
If the device is in the
Address
state, then- if the requested address stored in
wValue
was0
(None
in theusb
API) then the device should return to theDefault
state - otherwise the device should remain in the
Address
state but start using the new address
- if the requested address stored in
-
If the device is in the
Configured
state 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.
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:
bLength = 9
, the size of this descriptor (must always be this value)bDescriptorType = 2
, configuration descriptor type (must always be this value)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:
bLength = 9
, the size of this descriptor (must always be this value)bDescriptorType = 4
, interface descriptor type (must always be this value)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::Descriptor
andusb2::interface::Descriptor
structs do not havebLength
andbDescriptorType
fields. Those fields have fixed values according to the USB spec so you cannot modify or set them. Whenbytes()
is called on theDescriptor
value, 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.