- spread out USB information

- add intermediate-step solutions
- add more hints
- refactor exercise & text to have the same amount of work on all OSes
- add `usb` parser solutions
- make `dk-run` less silent
- rename rtic binaries (shorter names)
- link to the main svd2rust API docs
This commit is contained in:
Jorge Aparicio 2020-06-25 19:14:39 +02:00
parent 1f65e197a8
commit 886487dee6
24 changed files with 2051 additions and 232 deletions

View file

@ -2,7 +2,7 @@
> Advanced workshop
In this workshop we'll build a toy USB device application that gets enumerated by the host.
In this workshop we'll build a toy USB device application that gets enumerated and configured by the host.
The embedded application will run in a fully event driven fashion: only doing work when the host asks for it.
@ -81,7 +81,17 @@ The `firmware` workspace has been configured to cross-compile applications to th
The `dk-run` process will terminate when the microcontroller enters the "halted" state. From the embedded application, one can enter the "halted" state using the `asm::bkpt` function. For convenience, an `exit` function is provided in the `dk` Hardware Abstraction Layer (HAL). This function is divergent like `std::process::exit` (`fn() -> !`) and can be used to halt the device and terminate the `dk-run` process.
Note that when the `dk-run` tool sees the device enter the halted state it will proceed to reset-halt the device. This is particularly important when writing USB applications because simply leaving the device in a halted state will make it appear as an unresponsive USB device to the host; some OSes (e.g. Linux) will try to make an unresponsive device respond by power cycling the entire USB bus -- this will cause all other USB devices on the bus to be re-enumerated. Reset-halting the device will cause it to be electrically disconnected from the host USB bus and avoid the "power cycle the USB bus" scenario.
Note that when the `dk-run` tool sees the device enter the halted state it will proceed to *reset-halt* the device. This is particularly important when writing USB applications because simply leaving the device in a halted state will make it appear as an unresponsive USB device to the host. Some OSes (e.g. Linux) will try to make an unresponsive device respond by power cycling the entire USB bus -- this will cause all other USB devices on the bus to be re-enumerated. Reset-halting the device will cause it to be electrically disconnected from the host USB bus and avoid the "power cycle the whole USB bus" scenario.
## Checking the API documentation
We'll be using the `dk` Hardware Abstraction Layer. It's good to have handy its API documentation. You can generate the documentation for that crate from the command line:
``` console
$ cargo doc -p dk --open
```
Run this command from within the `advanced/firmware` folder. It will open the generated documentation in your default web browser.
## RTIC hello
@ -91,7 +101,7 @@ Open the `src/bin/rtic-hello.rs` file.
RTIC applications are written in RTIC's Domain Specific Language (DSL). The DSL extends Rust syntax with custom attributes like `#[init]` and `#[idle]`.
RTIC makes a clearer distinction between the application's initialization phase, the `#[init]` function, and the application's main loop or main logic, the `#[idle]` function. The initialization phase runs with interrupts disabled and interrupts are re-enabled before the `idle` function is executed.
RTIC makes a clearer distinction between the application's initialization phase, the `#[init]` function, and the application's main loop or main logic, the `#[idle]` function. The initialization phase runs with interrupts disabled and interrupts are re-enabled before the `idle` function is executed.
`rtic::app` is a procedural macro that generates extra Rust code, in addition to the user's functions. The fully expanded version of the macro can be found in the file `target/rtic-expansion.rs`. This file will contain the expansion of the procedural macro for the last compiled RTIC application.
@ -108,55 +118,71 @@ fn main() -> ! {
## Dealing with registers
Open the `src/bin/rtic-events.rs` file.
Open the `src/bin/events.rs` file.
In this and the next section we'll look into the RTIC's event handling features. To explore these features we'll use the action of connecting a USB cable to the DK's port J2 as the event we'd like to handle.
In this and the next section we'll look into RTIC's event handling features. To explore these features we'll use the action of connecting a USB cable to the DK's port J2 as the event we'd like to handle.
The example application enables the signaling of this "USB power" event in the `init` function. This is done using the low level register API generated by the [`svd2rust`] tool. The register API was generated from a SVD (System View Description) file, a file that describes all the peripherals and registers, and their memory layout, on a Cortex-M microcontroller.
The example application enables the signaling of this "USB power" event in the `init` function. This is done using the low level register API generated by the [`svd2rust`] tool. The register API was generated from a SVD (System View Description) file, a file that describes all the peripherals and registers, and their memory layout, on a device. In our case the device was the nRF52840; a sample SVD file for this microcontroller can be found [here][nrf52840.svd].
[`svd2rust`]: https://crates.io/crates/svd2rust
[nrf52840.svd]: https://github.com/NordicSemiconductor/nrfx/blob/master/mdk/nrf52840.svd
In the `svd2rust` API, peripherals are represented as structs. The fields of each peripheral struct are the registers associated to that peripheral. Each register field exposes methods to `read` and `write` to the register in a single memory operation.
In the `svd2rust` API, peripherals are represented as structs. The fields of each peripheral struct are the registers associated to that peripheral. Each register field exposes methods to `read` and `write` to the register in a *single* memory operation.
The `read` and `write` methods take closure arguments. These closures in turn grant access to a "constructor" value, usually named `r` or `w`, which provides methods to modify the bitfields of a register. At the same time the API of these "constructors" prevent you from modifying the reserved parts of the register: you cannot write arbitrary values into registers; you can only write valid values into registers.
The `read` and `write` methods take closure arguments. These closures in turn grant access to a "constructor" value, usually named `r` or `w`, which provides methods to modify the bitfields of a register. At the same time the API of these "constructors" prevent you from modifying the reserved parts of the register: that is you cannot write arbitrary values into registers; you can only write valid values into registers.
In Cortex-M devices interrupt handling needs to be enabled on two sides: on the peripheral side and on the core side. The register operations done in `init` take care of the peripheral side. The core side of the operation involves writing to the registers of the Nested Vector Interrupt Controller (NVIC) peripheral. This second part doesn't need to be in RTIC application because the framework takes care of it.
Apart from the `read` and `write` methods there's a `modify` method that performs a read-modify-write operation on the register; this API is also closure-based. The `svd2rust`-generated API is documented in detail in the `svd2rust` crate starting at [this section][svd2rust-api].
[svd2rust-api]: https://docs.rs/svd2rust/0.17.0/svd2rust/#peripheral-api
In Cortex-M devices interrupt handling needs to be enabled on two sides: on the peripheral side and on the core side. The register operations done in `init` take care of the peripheral side. The core side of the operation involves writing to the registers of the Nested Vector Interrupt Controller (NVIC) peripheral. This second part doesn't need to be done by the user in RTIC applications because the framework takes care of it.
## Event handling
Below the `idle` function you'll see a `#[task]` handler (function). This *task* is bound to the POWER_CLOCK interrupt signal and will be executed, function call style, every time the interrupt signal is raised by the hardware.
Below the `idle` function you'll see a `#[task]` handler, a function. This *task* is bound to the POWER_CLOCK interrupt signal and will be executed, function-call style, every time the interrupt signal is raised by the hardware.
"Run" the `rtic-events` application. Then connect a micro-USB cable to your PC/laptop then connect the other end to the DK (port J2). You'll see the "POWER event occurred" message after the cable is connected.
"Run" the `events` application. Then connect a micro-USB cable to your PC/laptop then connect the other end to the DK (port J3). You'll see the "POWER event occurred" message after the cable is connected.
Note that all tasks will be prioritized over the `idle` function so the execution of `idle` will be interrupted (paused) by the `on_power_event` task. When the `on_power_event` task finishes (returns) the execution of the `idle` will be resumed. This will become more obvious in the next section.
Try this: add an infinite loop to `init` so that it never returns. Now run the program and connect the USB cable. What behavior do you observe?
Try this: add an infinite loop to the end of `init` so that it never returns. Now run the program and connect the USB cable. What behavior do you observe? How would you explain this behavior? (hint: look at the `rtfm-expansion.rs` file: under what conditions is the `init` function executed?)
## Task state
Now let's say we want to change the previous program to count how many times the USB cable (port J3) has been connected and disconnected.
Tasks run from start to finish, like functions, in response to events. To preserve some state between the different executions of a task we can add a *resource* to the task. In RTIC, resources are the mechanism used to share data between different tasks in a memory safe manner but they can also be used to hold task state.
Tasks run from start to finish, like functions, in response to events. To preserve some state between the different executions of a task we can add a *resource* to the task. In RTIC, resources are the mechanism used to *share* data between different tasks in a memory safe manner but they can also be used to hold task state.
To get the desired behavior we'll want to store some counter in the state of the `on_power_event` task.
Open the `src/bin/rtic-resources.rs` file. The starter code shows the syntax to declare a resource, the `Resources` struct, and the syntax to associate a resource to a task, the `resources` list in the `#[task]` attribute.
Open the `src/bin/resource.rs` file. The starter code shows the syntax to declare a resource, the `Resources` struct, and the syntax to associate a resource to a task, the `resources` list in the `#[task]` attribute.
In the starter code a resource is used to *move* the POWER peripheral from `init` to the `on_power_event` task. The POWER peripheral then becomes part of the state of the `on_power_event` task. The resources of a task are available via the `Context` argument of the task.
In the starter code a resource is used to *move* (by value) the POWER peripheral from `init` to the `on_power_event` task. The POWER peripheral then becomes part of the state of the `on_power_event` task. The resources of a task are available via the `Context` argument of the task.
To elaborate more on this *move* action: in the `svd2rust` API, peripheral types like `POWER` are *singletons* (only a single instance of the type can ever exist). The consequence of this design is that holding a peripheral instance, like `POWER`, *by value* means that the function (or task) has exclusive access, or ownership, over the peripheral. This is the case of the `init` function: it owns the `POWER` peripheral but then transfer ownership over it to a task using the resource initialization mechanism.
We have moved the POWER peripheral into the task because we want to clear the USBDETECTED interrupt flag after it has been set by the hardware. If we miss this step the `on_power_event` task (function) will be called again once it returns and then again and again and again (ad infinitum).
Also note that in the starter code the `idle` function has been modified. Pay attention to the logs when you run the starter code.
Your task in this section will be to modify the program so that it prints the number of times the USB cable has been connected to the DK each time the cable is connected.
Your task in this section will be to modify the program so that it prints the number of times the USB cable has been connected to the DK every time the cable is connected, as shown below.
## USB basics
``` console
(..)
INFO:resource -- on_power_event: cable connected 1 time
(..)
INFO:resource -- on_power_event: cable connected 2 times
(..)
INFO:resource -- on_power_event: cable connected 3 times
```
Some basics about the USB protocol. The protocol is complex so we'll leave out many details and focus on the concepts required to get enumeration working.
You can find a solution to this exercise in the `resource-solution.rs` file.
A USB device, the nRF52840 in our case, can be one of these three states: the Default state, the Address state or the Configured state.
## USB enumeration
After being powered the device will start in the Default state. The enumeration process will take the device from the Default state to the Address state. As a result of the enumeration process the device will be assigned an address, in the range `1..=127`, by the host.
The USB protocol is complex so we'll leave out many details and focus only on the concepts required to get enumeration and configuration working. There are also several USB specific terms so we recommend checking chapter 2, "Terms and Abbreviations", of the USB specification (linked at the bottom of this document) every now and then.
So what is enumeration? A USB device, like the nRF52840, can be one of these three states: the Default state, the Address state or the Configured state. After being powered the device will start in the Default state. The enumeration process will take the device from the Default state to the Address state. As a result of the enumeration process the device will be assigned an address, in the range `1..=127`, by the host.
Each OS may perform the enumeration process slightly differently but the process will always involve these host actions:
@ -164,172 +190,584 @@ Each OS may perform the enumeration process slightly differently but the process
- GET_DESCRIPTOR request to get the device descriptor.
- SET_ADDRESS request to assign an address to the device.
The device descriptor is a binary encoded data structure sent by the device to the host. It contains information about the device, like its product and vendor identifiers and how many *configurations* it has.
A *configuration* is akin to an operation mode. USB devices usually have a single configuration that will be the only mode in which they'll operate, for example a USB mouse will always act as a USB mouse. Some devices, though, may provide a second configuration for the purpose of firmware upgrades. For example a printer may enter DFU (Device Firmware Upgrade) mode, a second *configuration*, so that a user can update its firmware; while in DFU mode the printer will not provide printing functionality.
Like the device descriptor, the configuration descriptor is also a binary encoded data structure sent by the device to the host. This descriptor contains information about the *interfaces* and *endpoints* the configuration exposes.
An interface is closest to a USB device's function. For example, a USB mouse may expose a single HID (Human Interface Device) interface to report user input to the host. USB devices can expose multiple interfaces. For example, the nRF52840 Dongle could expose both a CDC ACM interface (AKA virtual serial port) *and* a HID interface; the first interface could be used for (`log::info!`-style) logs; and the second one could provide a RPC (Remote Procedure Call) interface to the host for controlling the nRF52840's radio.
An interface is made up of one or more *endpoints*. An *endpoint* is similar to a UDP or TCP port on a PC in that they allow logical multiplexing of data over a single physical USB bus. Endpoints have directions: a endpoint can either be an IN endpoint or an OUT endpoint. The direction is always from the perspective of the host so in an IN endpoint data travels from the device to the host and in an OUT endpoint data travels from the host to the device. To give an example, a HID interface can use two (interrupt) endpoints, one IN and one OUT, for bidirectional communication with the host. A single endpoint cannot be used by more than one interface (with the exception of the special "endpoint 0").
Endpoints are identified by their address, a zero-based index, and direction. There are three types of non-zero endpoints ("endpoint 0" is special): bulk endpoints, interrupt endpoints and isochronous endpoints. Each endpoint type has different reliability and latency data transfer properties but it's not important to discuss them for this workshop.
"Endpoint 0", also known as the *control pipe*, actually refers to two endpoints: endpoint 0 IN and endpoint 0 OUT so the control pipe supports data transfers in both directions. The control pipe is mandatory: it must always be present and must always be active.
In this workshop we'll implement the minimal amount of functionality to make enumeration work. To that end you need to consider the following requirements:
- a USB device must support at least one configuration
- each configuration must expose at least one interface
- the control pipe (endpoint 0) must be implemented
- endpoint 0 is implicitly associated to all interfaces
- the number of endpoints bound to an interface can be zero -- endpoint 0 is never included in the endpoint count of an interface
Although the control pipe should be bidirectional, in practice to complete the enumeration data only needs to be transferred from the device to the host (IN direction).
These host actions will be perceived as *events* by the nRF52840. There are more USB concepts involved that we'll need to cover like descriptors, configurations, interfaces and endpoints but for now let's see how to handle USB events.
## Dealing with USB events
Open the `src/bin/rtic-usb-1.rs` file.
The USBD peripheral on the nRF52840 contains a series of registers, called EVENTS registers, that indicate the reason for entering the USBD event handler. These events must be handled by the application to complete the enumeration process.
The USB peripheral contains a series of registers, called EVENTS registers, that indicate the reason for entering the USBD event handler. These events must be handled by the application to complete the enumeration process.
Open the `src/bin/usb-1.rs` file. In this starter code the USBD peripheral is initialized in `init` and a task, named `main`, is bound to the interrupt signal USBD. This task will be called every time a new USBD event needs to be handled. The `main` task uses a helper `next_event` function to check all the event registers; if any event is set (occurred) then the function returns the event, represented by the `Event` enum, wrapped in the `Some` variant. This `Event` is then passed to the `on_event` function for further processing.
In this stage you will need to deal with the following USB events until you reach the EP0STATUS event.
Connect the USB cable to the port J3 then run the starter code.
In this section as a warm-up exercise you'll need to deal with the following USB events until you reach the EP0SETUP event.
- `USBRESET`. This event indicates that the host issued a USB reset signal. According to the USB specification this will move the device from any state to the `Default` state. Where are not dealing with any other state so doing nothing in response to this event is OK for now.
- `EP0SETUP`. The USBD peripheral has detected the SETUP stage of a control transfer. If you get to this point move to the next section.
- `EP0SETUP`. The USBD peripheral is signaling the end of the DATA stage of a control transfer. You won't encounter this event just yet.
- `EP0DATADONE`. The USBD peripheral is signaling the end of the DATA stage of a control transfer. You won't encounter this event just yet.
When you are done you should see this output:
``` console
(..)
INFO:usb_1 -- USB: UsbEp0Setup
INFO:usb_1 -- goal reached; move to the next section
```
Do not overthink this exercise; it is not a trick question. There is very little to do and literally nothing to add.
You can find the solution in the `usb-1-solution.rs` file.
Before we continue we need to discuss how data transfers work under the USB protocol.
## USB Endpoints
Under the USB protocol data transfers occur over *endpoints*.
Endpoints are similar to UDP or TCP ports on a PC in that they allow logical multiplexing of data over a single physical USB bus. USB endpoints, however, have directions: an endpoint can either be an IN endpoint or an OUT endpoint. The direction is always from the perspective of the host so in an IN endpoint data travels from the device to the host and in an OUT endpoint data travels from the host to the device.
Endpoints are identified by their address, a zero-based index, and direction. There are four types of endpoints: control endpoints, bulk endpoints, interrupt endpoints and isochronous endpoints. Each endpoint type has different properties: reliability, latency, etc. In this workshop we'll only need to deal with control endpoints.
All USB devices must use "endpoint 0" as the default control endpoint. "Endpoint 0" actually refers to two endpoints: endpoint 0 IN and endpoint 0 OUT. This endpoint pair is used to establish a *control pipe*, a bidirectional communication channel between the host and device where data is exchanged using a predefined format. The default control pipe over endpoint 0 is mandatory: it must always be present and must always be active.
For detailed information about endpoints check section 5.3.1, Device Endpoints, of the USB specification.
## Control transfers
The control pipe handles *control transfers*, a special kind of data transfer used by the host to issue *requests*. A control transfer is a data transfer that occurs in three stages: a SETUP stage, an optional DATA stage and a STATUS stage.
During the SETUP stage the host sends 8 bytes of data that identify the control request. Depending on the issued request there may be a DATA stage or not; during the DATA stage data is transferred either from the device to the host or the other way around. During the STATUS stage the device acknowledges, or not, the whole control request.
For detailed information about control transfers check section 5.5, Control Transfers, of the USB specification.
## SETUP stage
Control transfers over endpoint 0 consists of three stages: a SETUP stage, an optional DATA stage and a STATUS stage.
At the end of program `usb-1` we received a EP0SETUP event. This event signals the *end* of the SETUP stage of a control transfer. The nRF52840 USBD peripheral will automatically receive the SETUP data and store it in the following registers: BMREQUESTTYPE, BREQUEST, WVALUE{L,H}, WINDEX{L,H} and WLENGTH{L,H}. These registers are documented in sections 6.35.13.31 to 6.35.13.38 of the nRF52840 Product Specification.
The SETUP stage consists of the host sending a 8 byte header to the device. This header describes the host request: is the host expecting a response (data) from the device, is the host about to send some data to the device or is this a request with no data payload? The SETUP stage conveys this information.
The format of this setup data is documented in section 9.3 of the USB specification. Your next task is to parse the setup data according to section 9.4, Standard Descriptor Requests, of the USB specification (tables 9-3, 9-4 and 9-5 are the most relevant part of the section).
The nRF52840 USBD peripheral will parse this header and store its contents in the following registers: BMREQUESTTYPE, BREQUEST, WVALUE{L,H}, WINDEX{L,H} and WLENGTH{L,H}. These registers match the logical division of the setup packet, in *fields*, as specified in the USB 2.0 specification. The fields that start with the letter 'b' are 1-byte large; the ones that start with the letter 'w' are 2-bytes large.
Note that you won't need to be able to parse *all* the standard requests in this workshop. For now you should be able to parse the GET_DESCRIPTOR request, which is described in detail in section 9.4.3 of the USB specification.
Open the `src/bin/rtic-usb-2.rs` file.
When you need to write some `no_std` code that does not involve device-specific I/O you should consider writing it as a separate crate. Having this kind of code in a separate crate lets you test it on your development machine (e.g. `x86_64`) using the standard `cargo test` functionality.
The task here is to write a SETUP packet parser in the `common/usb` crate and reach the GOAL comment when executing the program. The starter code has already read the relevant registers using helper functions.
So that's what we'll do here. In the `advanced/common/usb` folder you'll find starter code for writing a `no_std` SETUP data parser. The starter code contains some unit tests; you can run them with `cargo test` (from within the `usb` folder) or you can use Rust Analyzer's "Test" button if you have the file open in VS code.
To implement the parser, refer to table 9-3 under section 9.4 of the USB 2.0 specification (page 250). Note that at this stage you only need to be able to parse the `GetDescriptor` variant of the `Request` enum and within that variant you only need to handle the `Device` variant of the `Descriptor` enum.
To sum up the work to do here:
You should develop this part completely on the host. Switch the workspace to the `common/usb` directory and run the unit tests on the host using Rust Analyzer's "Test" button.
1. write a SETUP data parser in `advanced/common/usb`. You only need to handle the GET_DESCRIPTOR request and make the `get_descriptor_device` test pass for now.
2. modify `usb-1` to read (USBD registers) and parse the SETUP data when the EPSETUP event is received.
3. when you have successfully received a GET_DESCRIPTOR request for a Device descriptor you are done and can move to the next section.
Alternatively, you can start from `usb-2` instead of `usb-1`. In either case, the tasks are the same.
If you are logging like the `usb-2` starter code does then you should see an output like this once you are done:
``` console
INFO:usb_2 -- USB: UsbReset @ 438.842772ms
INFO:usb_2 -- USB: UsbEp0Setup @ 514.984128ms
INFO:usb_2 -- SETUP: bmrequesttype: 128, brequest: 6, wlength: 64, windex: 0, wvalue: 256
INFO:usb_2 -- GET_DESCRIPTOR Device [length=64]
INFO:usb_2 -- Goal reached; move to the next section
```
`wlength` / `length` can vary depending on the OS, USB port (USB 2.0 vs USB 3.0) or the presence of a USB hub so you may see a different value.
You can find a solution to task (1) in `advanced/common/usb/get-device-descriptor.rs`.
You can find a solution to task (2) in `advanced/firmware/src/bin/usb-2-solution.rs`.
## Device descriptor
After receiving a GET_DESCRIPTOR request during the SETUP stage the device needs to respond with a descriptor during the DATA stage.
After receiving a GET_DESCRIPTOR request during the SETUP stage the device needs to respond with a *descriptor* during the DATA stage.
Open the `src/bin/rtic-usb-3.rs` file.
A descriptor is a binary encoded data structure sent by the device to the host. The device descriptor, in particular, contains information about the device, like its product and vendor identifiers and how many *configurations* it has. The format of the device descriptor is specified in section 9.6.1, Device, of the USB specification.
This starter code parses the GET_DESCRIPTOR request and sends back a zero-byte response to the host using the `Ep0In` abstraction, which we'll cover in the next section. The starter code is wrong because a zero-byte response is not a valid descriptor.
As far as the enumeration process goes, the most relevant fields of the device descriptor are the number of configurations and `bcdUSB`, the version of the USB specification the devices adheres to. In `bcdUSB` you should report compatibility with USB 2.0.
The task in this section is to build a device descriptor using the `usb2::device::Descriptor` API, turn that `Descriptor` instance into a sequence of bytes and respond with that. Note that the device must send back at most `length` bytes to the host; if the byte representation of `Descriptor` is longer than `length` then the slice must be truncated to `length` bytes.
What about (the number of) configurations?
As for the contents of the descriptor you should use these values:
A *configuration* is akin to an operation mode. USB devices usually have a single configuration that will be the only mode in which they'll operate, for example a USB mouse will always act as a USB mouse. Some devices, though, may provide a second configuration for the purpose of firmware upgrades. For example a printer may enter DFU (Device Firmware Upgrade) mode, a second *configuration*, so that a user can update its firmware; while in DFU mode the printer will not provide printing functionality.
- Vendor ID: `consts::VID`
- Product ID: `consts::PID`
- Max packet size for endpoint 0: 64 bytes
- Number of configurations: 1
- bcdDevice: `0x01_00`, which means version "1.00"
- everything else can be set to `0` or `None`
The specification mandates that a device must have at least one available configuration so we can report a single configuration in the device descriptor.
We suggest you check the API docs of the `usb2` crate by running the command `cargo doc -p usb2 --open`. This should open the API docs in your browser.
## DATA stage
More information about the fields of the device descriptor can be found in section 9.6.1 of the USB 2.0 spec.
The next step is to respond to the GET_DESCRIPTOR request with a device descriptor. To do this we'll use the `dk::usb::Ep0In` abstraction -- we'll look into what the abstraction does in a future section; for now we'll just use it.
After that has been fixed you'll reach the EP0DATADONE event. It indicates that the data transfer has completed. You must call the `Ep0In.end` method at that point. After that you'll likely get a new request from the host.
An instance of this abstraction is available in the `board` value (`#[init]` function). The first step is to make this `Ep0In` instance available to the `on_event` function.
The `Ep0In` API has two methods: `start` and `end` (also see their API documentation). `start` is used to start a DATA stage; this method takes a *slice of bytes* (`[u8]`) as argument; this argument is the response data. The `end` method needs to be called after `start`, when the EP0DATADONE event is raised, to complete the control transfer. `Ep0In` will automatically issue the STATUS stage that must follow the DATA stage.
To goal of this section is to use `Ep0In` to respond to the `GET_DESCRIPTOR Device` request (and only to that request). The response must be a device descriptor with its fields set to these values:
- `bLength = 18`, size of this descriptor (see table 9-8 of the USB spec)
- `bDescriptorType = 1`, means device descriptor (see table 9-5 of the USB spec)
- `bcdUSB = 0x0200`, means USB 2.0
- `bDeviceClass = bDeviceSubClass = bDeviceProtocol = 0`, these are unimportant for enumeration
- `bMaxPacketSize0 = 64`, this is the most performant option (minimizes exchanges between the device and the host) and it's assumed by the `Ep0In` abstraction
- `idVendor = common::VID`, value expected by the `usb-list` tool (\*)
- `idProduct = common::PID`, value expected by the `usb-list` tool (\*)
- `bcdDevice = 0x0100`, this means version 1.0 but any value should do
- `iManufacturer = iProduct = iSerialNumber = 0`, string descriptors not supported
- `bNumConfigurations = 1`, must be at least `1` so this is the minimum value
(\*) the `common` crate refers to the crate in the `advanced/common` folder. It is already part of the `firmware` crate dependencies.
Although you can create the device descriptor by hand as an array filled with magic values we *strongly* recommend you use the `usb2::device::Descriptor` abstraction. The crate is already in the dependency list of the project; you can open its API documentation with the following command: `cargo doc -p usb2 --open`.
Note that the device descriptor is 18 bytes long but the host may ask for fewer bytes (see `wlength` field in the SETUP data). In that case you must respond with the amount of bytes the host asked for. The opposite may also happen: `wlength` may be larger than the size of the device descriptor; in this case you must answer must be 18 bytes long (do *not* pad the response with zeroes).
If you need help getting the `Ep0In` value into the `on_event` function check out the `src/bin/usb-3.rs` file.
Once you have successfully responded to the GET_DESCRIPTOR Device request you should get logs like these (if you are logging like `usb-3` does):
``` console
INFO:usb_3 -- USB: UsbReset @ 342.071532ms
INFO:usb_3 -- USB: UsbEp0Setup @ 414.855956ms
INFO:usb_3 -- SETUP: bmrequesttype: 128, brequest: 6, wlength: 64, windex: 0, wvalue: 256
INFO:usb_3 -- GET_DESCRIPTOR Device [length=64]
INFO:dk::usbd -- EP0IN: start 18B transfer
INFO:usb_3 -- USB: UsbEp0DataDone @ 415.222166ms
INFO:dk::usbd -- EP0IN: transfer done
INFO:usb_3 -- USB: UsbReset @ 465.637206ms
INFO:usb_3 -- USB: UsbEp0Setup @ 538.208007ms
INFO:usb_3 -- SETUP: bmrequesttype: 0, brequest: 5, wlength: 0, windex: 0, wvalue: 27
ERROR:usb_3 -- unknown request (goal achieved if GET_DESCRIPTOR Device was handled)
INFO:dk -- `dk::exit() called; exiting ...`
```
A solution to this exercise can be found in `src/bin/usb-3-solution.rs`
## DMA
Let's zoom into the `Ep0In` abstraction next. You can use the "go to definition" to see the implementation of the `Ep0In.start` method. What this method does is start a DMA transfer to send `bytes` to the host. The data (`bytes`) is first copied into an internal buffer and then the DMA is configured to move the data from that internal buffer to the USBD peripheral.
The signature of the `start` method does *not* ensure that (a) `bytes` won't be deallocated before the DMA transfer is over (e.g. `bytes` could be pointing into the stack) or that (b) `bytes` won't be modified right after the DMA transfer starts (this would be a data race in the general case). For these two safety reasons the API is implemented using an internal buffer. The internal buffer has a `'static` lifetime so it's guaranteed to never be deallocated -- this prevent issue (a). The `busy` flag prevents any further modification to the buffer -- from the public API -- while the DMA transfer is in progress.
The signature of the `start` method does *not* ensure that:
- `bytes` won't be deallocated before the DMA transfer is over (e.g. `bytes` could be pointing into the stack), or that
- `bytes` won't be modified right after the DMA transfer starts (this would be a data race in the general case).
For these two safety reasons the API is implemented using an internal buffer. The internal buffer has a `'static` lifetime so it's guaranteed to never be deallocated -- this prevents issue (a). The `busy` flag prevents any further modification to the internal buffer -- from the public API -- while the DMA transfer is in progress.
Apart from thinking about lifetimes and explicit data races in the surface API one must internally use memory fences to prevent reordering of memory operations (e.g. by the compiler), which can also cause data races. DMA transfers run in parallel to the instructions performed by the processor and are "invisible" to the compiler.
In the implementation of the `start` method, data is copied from `bytes` to the internal buffer, `memcpy` operation, and then the DMA transfer is started. The compiler sees the start of the DMA transfer as an unrelated memory operation so it may move the `memcpy` to *after* the DMA transfer has started. This reordering results in a data race: the processor modifies the internal buffer while the DMA is reading data out from it. To avoid this reordering a memory fence, `dma_start`, is used. The fence pairs with the store operation that starts the DMA transfer and prevents the previous `memcpy`, and any other memory operation, from being move to *after* the store operation.
In the implementation of the `start` method, data is copied from `bytes` to the internal buffer, `memcpy` operation, and then the DMA transfer is started with a write to the `TASKS_STARTEPIN0` register. The compiler sees the start of the DMA transfer (register write) as an unrelated memory operation so it may move the `memcpy` to *after* the DMA transfer has started. This reordering results in a data race: the processor modifies the internal buffer while the DMA is reading data out from it.
Another memory fence, `dma_end`, is need at the end of the DMA transfer. In the general case, this prevents instruction reordering that would result in the processor accessing the internal buffer *before* the DMA transfer has finished. This is particularly problematic with DMA transfers that modify a region of memory which the processor intends to read after the transfer.
To avoid this reordering a memory fence, `dma_start`, is used. The fence pairs with the *store* operation (register write) that starts the DMA transfer and prevents the previous `memcpy`, and any other memory operation, from being move to *after* the store operation.
Another memory fence, `dma_end`, is needed at the end of the DMA transfer. In the general case, this prevents instruction reordering that would result in the processor accessing the internal buffer *before* the DMA transfer has finished. This is particularly problematic with DMA transfers that modify a region of memory which the processor intends to read after the transfer.
Not relevant to the DMA operation but relevant to the USB specification, the `start` method sets a shortcut in the USBD peripheral to issue a STATUS stage right after the DATA stage is finished. Thanks to this it is not necessary to manually start a STATUS stage after calling the `end` method.
## More standard requests
After responding to the `GET_DESCRIPTOR Device` request the host will start sending different requests. The parser in `common/usb` will need to be updated to handle these requests:
1. `SET_ADDRESS`, see section 9.4.6 of the USB spec
2. `GET_DESCRIPTOR Configuration`, see section 9.4.3 of the USB spec
3. `SET_CONFIGURATION`, see section 9.4.7 of the USB spec -- this request is likely to only be observed on Linux during enumeration
We suggest you incrementally extend the parser to handle these requests in that order. So, for example, add `SET_ADDRESS` support first and then check how far the `usb` program goes.
The starter `common/usb` code contains unit tests for these other requests as well as extra `Request` variants for these requests. All of them have been commented out using a `#[cfg(TODO)]` attribute which you can remove once you need any new variant or new unit test.
If at any point you need them you can find solution to parsing the above three requests in the following files:
- `advanced/common/src/set-address.rs`
- `advanced/common/src/get-descriptor-configuration.rs`
- `advanced/common/src/set-configuration.rs`
Each file contains just enough code to parse the request in its name and the `GET_DESCRIPTOR Device` request. So you can refer to `set-configuration.rs` without getting "spoiled" about how to parse the `SET_ADDRESS` request.
## Error handling: stalling the endpoint
You may come across host requests other than the ones listed in the previous section. Here's what you should do if you encounter any of those.
The USB specification defines a device-side procedure for "stalling a 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 the SET_DESCRIPTOR request is not mandatory.
Open the `src/bin/rtic-usb-4.rs` file.
You can use the `dk::usbd:ep0stall` helper function or write 1 to the `TASKS_EP0STALL` register to stall endpoint 0. This is what you should use
In this starter code the code that handles the SETUP stage of endpoint 0 has been refactored into a separate function, `ep0setup`, that uses Rust's built-in error handling features. This function will return the `Err` variant when it encounters an invalid request or a request that is not supported.
The logic of the `EP0SETUP` event handling is going to get rather complex so we suggest this refactor to add error handling: move the event handling logic into a separate function *that returns a `Result`*. That function should return the `Err` variant when it encounters an invalid host request. So in code that may look like this:
You can use the `usbd:ep0stall` helper function or write 1 to the TASKS_EP0STALL register to stall endpoint 0.
``` rust
fn on_event(/* parameters */) {
match event {
Event::EP0SETUP => {
if ep0setup(/* arguments */).is_err() {
log::error!("EP0: unexpected request; stalling the endpoint");
// TODO stall the endpoint
}
}
}
}
## State
fn ep0setup(/* parameters */) -> Result<(), ()> {
let req = Request::parse(/* arguments_*/)?;
// ^ early returns an `Err` if it occurs
The starter code in `src/bin/rtic-usb-4.rs` also passes a `State` variable to the `ep0setup` function. This variable tracks the state of the USB device, which can be one of `Default`, `Address` or `Configured`.
// TODO respond to the `req`; return `Err` if the request was invalid in this state
From this point you'll need to track the state of the device in software to be able to correctly respond to the host requests.
Ok(())
}
```
The handling of the USB reset condition (`Event::UsbReset`) will need to be updated. According to the USB specification this needs to change the device state, from any state, to the `Default` state.
Note that there's a difference between the error handling done here and the error handling commonly done in `std` programs. In `std` programs you 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 non-zero exit code. This approach is usually not appropriate for embedded programs as (1) `main` cannot return, (2) there may not be a console to print the error to and/or (3) 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 than propagated all the way up.
This does not preclude error reporting. The above snippet includes error reporting in the form of a `log::error!` 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 error reporting is possible and certainly useful during development.
## Device state
Eventually you'll receive a `SET_ADDRESS` request that will move the device from the `Default` state to the `Address` state and if you are working on Linux you'll receive a `SET_CONFIGURATION` request that will move the device from the `Address` state to the `Configured` state. Also, 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 the firmware 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 many 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.
Once you start tracking the device state don't forget to update the handling of the `USBRESET` event. This event changes the state of the USB device. See section 9.1, USB Device States, of the USB specification for more details.
## A code hint
If you would like a hint about how device state and error handling should be integrated into your code, check out the `src/bin/usb-4.rs` file.
## SET_ADDRESS
This request changes the device's state as follows:
This request should come right after the `GET_DESCRIPTOR Device` request, though some OSes may issue a USB reset in between.
Section 9.4.6, Set Address, describes how to handle this request but below you can find a summary:
- If the device is in the `Default` state, then
- if the requested address was `0` (`None` in the `usb2` API) then the device should stay in the `Default` state
- if the requested address was `0` (`None` in the `usb` API) then the device should stay in the `Default` state
- otherwise the device should move to the `Address` state
- If the device is in the `Address` state, then
- if the requested address was `0` (`None` in the `usb2` API) then the device should return to the `Default` state
- if the requested address was `0` (`None` in the `usb` API) then the device should return to the `Default` state
- otherwise the device should remain in the `Address` state but start using the new address
- If the device is in the `Configured` state this request results in "unspecified" behavior according to the USB specication. You should stall the endpoint in this case.
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 only to the requested address (see the USBADDR register) so no further interaction with the USBD peripheral is required for this request.
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 (pages 486 and 487).
## Configuration descriptors
## GET_DESCRIPTOR Configuration
When the host issues a GET_DESCRIPTOR request to request a configuration descriptor 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.
For simplicity we'll report that the device has zero interfaces and zero endpoints, other than the control endpoint 0 which doesn't need to be reported. So in response to a `GET_DESCRIPTOR Configuration 0` request the device should respond with the binary representation of a `usb2::configuration::Descriptor` instance. The instance should contain these values:
We have covered configurations and endpoints but what is an *interface*?
- wTotalLength: `usb2::configuration::Descriptor::SIZE`, as just the configuration descriptor is being reported back
- bNumInterfaces: `0`
- bConfigurationValue: `1`
- bmAttributes: `{ self_powered: true, remote_wakeup: false }`
- bMaxPower: `250`, equivalent to 500 mA
### Interface
Note that the index of the `GET_DESCRIPTOR Configuration` request must be `0` because the device reported a single configuration in its device descriptor. Any other index is an "out of bounds" attempt and should be handled by stalling the endpoint.
An interface is closest to a USB device's function. For example, a USB mouse may expose a single HID (Human Interface Device) interface to report user input to the host. USB devices can expose multiple interfaces within a configuration. For example, the nRF52840 Dongle could expose both a CDC ACM interface (AKA virtual serial port) *and* a HID interface; the first interface could be used for (`log::info!`-style) logs; and the second one could provide a RPC (Remote Procedure Call) interface to the host for controlling the nRF52840's radio.
## (Linux) SET_CONFIGURATION
An interface is made up of one or more *endpoints*. To give an example, a HID interface can use two (interrupt) endpoints, one IN and one OUT, for bidirectional communication with the host. A single endpoint cannot be used by more than one interface with the exception of the special "endpoint 0", which can be (and usually is) shared by all interfaces.
On Linux, the host will sent a SET_CONFIGURATION request, after enumeration, to put the device in the `Configured` state. You'll need to handle the request according to the following logic:
For detailed information about interfaces check section 9.6.5, Interface, of the USB specification.
- If the device is in the `Default` state, you should stall the endpoint.
### Configuration descriptor
The configuration descriptor describes one of the device configurations to the host. The descriptor contains the following information about a particular configuration:
- the total length of the configuration: this is the number of bytes required to transfer this configuration descriptor and the interface and endpoint descriptors associated to it
- its number of interfaces
- its configuration value -- this is *not* an index and can be any non-zero value
- whether the configuration is self-powered
- whether the configuration supports remote wakeup
- its maximum power consumption
The format of the configuration descriptor is specified in section 9.6.3, Configuration, of the USB specification. What may not be obvious from that section is that number of interfaces must be greater than or equal to one and that the configuration value cannot be zero.
### Interface descriptor
The interface descriptor describes one of the device interfaces to the host. The descriptor contains the following information about a particular interface:
- its interface number -- this is a zero-based index
- its alternate setting -- this allows configuring the interface
- its number of endpoints
- class, subclass and protocol -- these define the interface (HID, or TTY ACM, or DFU, etc.) according to the USB specification
The format of the interface descriptor is specified in section 9.6.5, Interface, of the USB specification. The most relevant parts of that section are: the number of endpoints can be zero and endpoint zero must not be accounted when counting endpoints.
### Endpoint descriptor
We will not need to deal with endpoint descriptors in this workshop but they are specified in section 9.6.6, Endpoint, of the USB specification.
### Response
So how should we respond to the host? As the goal is to be enumerated we'll respond with the minimum amount of information possible.
First, configuration descriptors are requested by *index*, not by their configuration value. The index specified in the host request should be checked. As we reported a single configuration in the device descriptor the index in the request must be zero. Any other value should be rejected by stalling the endpoint.
Next the response should continue a concatenation of the configuration descriptor, followed by interface descriptors and then by endpoint descriptors. The minimum amount of interfaces a device must have is one so we'll include a single interface descriptor in the response. The interface does not need to have any endpoint associated to it so we'll include zero endpoint descriptors in the response.
Thus the response will be one configuration descriptor and one interface descriptor. The two 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 (see table 9-10 in the USB spec)
- `bDescriptorType = 2`, configuration descriptor (see table 9-5 in the USB spec)
- `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 do
- `iConfiguration = 0`, string descriptors are not supported
- `bmAttributes { self_powered: true, remote_wakeup: false }`, self-powered due to the debugger connection
- `bMaxPower = 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 (see table 9-11 in the USB spec)
- `bDescriptorType = 4`, interface descriptor (see table 9-5 in the USB spec)
- `bInterfaceNumber = 0`, this is the first, and only, interface
- `bAlternateSetting = 0`, alternate settings are not supported
- `bNumEndpoints = 0`, no endpoint associated to this interface (other than the control endpoint)
- `bInterfaceClass = bInterfaceSubClass = bInterfaceProtocol = 0`, does not adhere to any specified USB interface
- `iInterface = 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 the `heapless` crate before you can find example usage in the the `src/bin/vec.rs` file.
[`heapless::Vec`]: https://docs.rs/heapless/0.5.5/heapless/struct.Vec.html
## SET_CONFIGURATION (likely Linux only)
On Linux, the host will likely send a SET_CONFIGURATION request right after enumeration to put the device in the `Configured` state. For now you can reject (stall) the request. It is not necessary at this stage because the device has already been enumerated.
## Idle state
Once you have handled all the previously covered requests the device should be enumerated and remain idle awaiting for a new host request. Your logs may look like this:
``` console
INFO:usb_4 -- USB: UsbReset @ 318.66455ms
INFO:usb_4 -- USB reset condition detected
INFO:usb_4 -- USB: UsbEp0Setup @ 391.418456ms
INFO:usb_4 -- EP0: GetDescriptor { descriptor: Device, length: 64 }
INFO:dk::usbd -- EP0IN: start 18B transfer
INFO:usb_4 -- USB: UsbEp0DataDone @ 391.723632ms
INFO:usb_4 -- EP0IN: transfer complete
INFO:dk::usbd -- EP0IN: transfer done
INFO:usb_4 -- USB: UsbReset @ 442.016601ms
INFO:usb_4 -- USB reset condition detected
INFO:usb_4 -- USB: UsbEp0Setup @ 514.709471ms
INFO:usb_4 -- EP0: SetAddress { address: Some(17) }
INFO:usb_4 -- USB: UsbEp0Setup @ 531.37207ms
INFO:usb_4 -- EP0: GetDescriptor { descriptor: Device, length: 18 }
INFO:dk::usbd -- EP0IN: start 18B transfer
INFO:usb_4 -- USB: UsbEp0DataDone @ 531.646727ms
INFO:usb_4 -- EP0IN: transfer complete
INFO:dk::usbd -- EP0IN: transfer done
INFO:usb_4 -- USB: UsbEp0Setup @ 531.829832ms
INFO:usb_4 -- EP0: GetDescriptor { descriptor: DeviceQualifier, length: 10 }
ERROR:usb_4 -- EP0IN: unexpected request; stalling the endpoint
INFO:usb_4 -- USB: UsbEp0Setup @ 532.226562ms
INFO:usb_4 -- EP0: GetDescriptor { descriptor: DeviceQualifier, length: 10 }
ERROR:usb_4 -- EP0IN: unexpected request; stalling the endpoint
INFO:usb_4 -- USB: UsbEp0Setup @ 532.592772ms
INFO:usb_4 -- EP0: GetDescriptor { descriptor: DeviceQualifier, length: 10 }
ERROR:usb_4 -- EP0IN: unexpected request; stalling the endpoint
INFO:usb_4 -- USB: UsbEp0Setup @ 533.020018ms
INFO:usb_4 -- EP0: GetDescriptor { descriptor: Configuration { index: 0 }, length: 9 }
INFO:dk::usbd -- EP0IN: start 9B transfer
INFO:usb_4 -- USB: UsbEp0DataDone @ 533.386228ms
INFO:usb_4 -- EP0IN: transfer complete
INFO:dk::usbd -- EP0IN: transfer done
INFO:usb_4 -- USB: UsbEp0Setup @ 533.569335ms
INFO:usb_4 -- EP0: GetDescriptor { descriptor: Configuration { index: 0 }, length: 18 }
INFO:dk::usbd -- EP0IN: start 18B transfer
INFO:usb_4 -- USB: UsbEp0DataDone @ 533.935546ms
INFO:usb_4 -- EP0IN: transfer complete
INFO:dk::usbd -- EP0IN: transfer done
INFO:usb_4 -- USB: UsbEp0Setup @ 534.118651ms
INFO:usb_4 -- EP0: SetConfiguration { value: Some(42) }
ERROR:usb_4 -- EP0IN: unexpected request; stalling the endpoint
```
Note that these logs are from a Linux host where a `SET_CONFIGURATION` request is sent after the `SET_ADDRESS` request. On other OSes you may not get that request before the bus goes idle. Also note that there are some `GET_DESCRIPTOR DeviceQualifier` requests in this case; you do not need to parse them in the `usb` crate as they'll be rejected (stalled) anyways.
You can find traces for other OSes in these files (they are next to this README):
- `win-enumeration.txt`
- `macos-enumeration.txt` (TODO)
At this point you can double check that enumeration worked by running the `list-usb` tool.
``` console
Bus 001 Device 013: ID 1366:1015 <- J-Link on the nRF52840 Development Kit
(..)
Bus 001 Device 016: ID 2020:0717 <- nRF52840 on the nRF52840 Development Kit
```
Both the J-Link and the nRF52840 should appear in the list.
You can find a working solution up to this point in `src/bin/usb-4-solution.rs`. Note that the solution uses the `usb2` crate to parse SETUP packets and that crate supports parsing all standard requests.
## Inspecting the descriptors
There's a tool in the `advanced/host/` folder called `print-descs`. You can run this tool to print all the descriptors reported by your application.
``` console
$ print-descs
DeviceDescriptor {
bLength: 18,
bDescriptorType: 1,
bcdUSB: 512,
bDeviceClass: 0,
bDeviceSubClass: 0,
bDeviceProtocol: 0,
bMaxPacketSize: 64,
idVendor: 8224,
idProduct: 1815,
bcdDevice: 256,
iManufacturer: 0,
iProduct: 0,
iSerialNumber: 0,
bNumConfigurations: 1,
}
address: 22
config0: ConfigDescriptor {
bLength: 9,
bDescriptorType: 2,
wTotalLength: 18,
bNumInterfaces: 1,
bConfigurationValue: 42,
iConfiguration: 0,
bmAttributes: 192,
bMaxPower: 250,
extra: None,
}
iface0: [
InterfaceDescriptor {
bLength: 9,
bDescriptorType: 4,
bInterfaceNumber: 0,
bAlternateSetting: 0,
bNumEndpoints: 0,
bInterfaceClass: 0,
bInterfaceSubClass: 0,
bInterfaceProtocol: 0,
iInterface: 0,
},
]
```
The output above corresponds to the descriptor values we suggested. If you used different values, e.g. for `bMaxPower`, you'll a slightly different output.
## Getting it configured
At this stage the device will be in the `Address` stage. It has been identified and enumerated by the host but cannot yet be used by host applications. The device must first move to the `Configured` state before the host can start, for example, HID communication or send non-standard requests over the control endpoint.
Windows and macOS will enumerate the device but not automatically configure it after enumeration. Here's what you should do to force the host to configure the device.
### Linux
Nothing extra needs to be done on Linux. The host will automatically send a `SET_CONFIGURATION` request so proceed to the `SET_CONFIGURATION` section to see how to handle the request.
### Windows
After getting the device enumerated and into the idle state, open the Zadig tool (covered in the setup instructions; see the top README) and use it to associate the nRF52840 USB device to the WinUSB driver. The nRF52840 will appear as a "unknown device" with a VID and PID that matches the ones defined in the `common` crate
Now modify the `print-descs` program to "open" the device -- this operation is commented out in the source code. With this modification `print-descs` will cause Windows to send a `SET_CONFIGURATION` request to configure the device. You'll need to run `print-descs` to test out the correct handling of the `SET_CONFIGURATION` request.
### macOS
> TODO uncomment the `open` line in `print-descs` and see if that forces the host to send a SET_CONFIGURATION request
### SET_CONFIGURATION
Section 9.4.7, Set Configuration, of the USB spec describes how to handle this request but below you can find a summary:
- If the device is in the `Default` state, you should stall the endpoint because the operation is not permitted in that state.
- If the device is in the `Address` state, then
- if the requested configuration value is 0 (`None` in the `usb2` API) then stay in the `Address` state
- if the requested configuration value is 0 (`None` in the `usb` API) then stay in the `Address` state
- if the requested configuration value is non-zero and valid (was previously reported in a configuration descriptor) then move to the `Configured` state
- if the requested configuration value is not valid then stall the endpoint
- If the device is in the `Configured` state, then
- if the requested configuration value is 0 (`None` in the `usb2` API) then return to the `Address` state
- if the requested configuration value is 0 (`None` in the `usb` API) then return to the `Address` state
- if the requested configuration value is non-zero and valid (was previously reported in a configuration descriptor) then move to the `Configured` state with the new configuration value
- if the requested configuration value is not valid then stall the endpoint
In all the cases where you did not stall the endpoint (returned `Err`) you'll need to acknowledge the request by starting a STATUS stage. This can be done by writing 1 to the TASKS_EP0STATUS register.
For more details, read the section 9.4.7 'SET_CONFIGURATION' of the USB 2.0 specification.
NOTE: On Windows, you may get a `GET_STATUS` request *before* the `SET_CONFIGURATION` request and although you *should* respond to it, stalling the `GET_STATUS` request seems sufficient to get the device to the `Configured` state.
## (homework) String descriptors
### Expected output
> NOTE(japaric) I hardly think the workshop can fit any more material within the allocated time frame but this is one, of many things, people could continue working on
Once you are correctly handling the `SET_CONFIGURATION` request you should get logs like these:
``` console
INFO:usb_5 -- USB: UsbReset @ 397.15576ms
INFO:usb_5 -- USB reset condition detected
INFO:usb_5 -- USB: UsbEp0Setup @ 470.00122ms
INFO:usb_5 -- EP0: GetDescriptor { descriptor: Device, length: 64 }
INFO:dk::usbd -- EP0IN: start 18B transfer
INFO:usb_5 -- USB: UsbEp0DataDone @ 470.306395ms
INFO:usb_5 -- EP0IN: transfer complete
INFO:dk::usbd -- EP0IN: transfer done
INFO:usb_5 -- USB: UsbReset @ 520.721433ms
INFO:usb_5 -- USB reset condition detected
INFO:usb_5 -- USB: UsbEp0Setup @ 593.292235ms
INFO:usb_5 -- EP0: SetAddress { address: Some(21) }
INFO:usb_5 -- USB: UsbEp0Setup @ 609.954832ms
INFO:usb_5 -- EP0: GetDescriptor { descriptor: Device, length: 18 }
INFO:dk::usbd -- EP0IN: start 18B transfer
INFO:usb_5 -- USB: UsbEp0DataDone @ 610.260008ms
INFO:usb_5 -- EP0IN: transfer complete
INFO:dk::usbd -- EP0IN: transfer done
INFO:usb_5 -- USB: UsbEp0Setup @ 610.443113ms
INFO:usb_5 -- EP0: GetDescriptor { descriptor: DeviceQualifier, length: 10 }
WARN:usb_5 -- EP0IN: stalled
INFO:usb_5 -- USB: UsbEp0Setup @ 610.809325ms
INFO:usb_5 -- EP0: GetDescriptor { descriptor: DeviceQualifier, length: 10 }
WARN:usb_5 -- EP0IN: stalled
INFO:usb_5 -- USB: UsbEp0Setup @ 611.175535ms
INFO:usb_5 -- EP0: GetDescriptor { descriptor: DeviceQualifier, length: 10 }
WARN:usb_5 -- EP0IN: stalled
INFO:usb_5 -- USB: UsbEp0Setup @ 611.511228ms
INFO:usb_5 -- EP0: GetDescriptor { descriptor: Configuration { index: 0 }, length: 9 }
INFO:dk::usbd -- EP0IN: start 9B transfer
INFO:usb_5 -- USB: UsbEp0DataDone @ 611.846922ms
INFO:usb_5 -- EP0IN: transfer complete
INFO:dk::usbd -- EP0IN: transfer done
INFO:usb_5 -- USB: UsbEp0Setup @ 612.030027ms
INFO:usb_5 -- EP0: GetDescriptor { descriptor: Configuration { index: 0 }, length: 18 }
INFO:dk::usbd -- EP0IN: start 18B transfer
INFO:usb_5 -- USB: UsbEp0DataDone @ 612.365721ms
INFO:usb_5 -- EP0IN: transfer complete
INFO:dk::usbd -- EP0IN: transfer done
INFO:usb_5 -- USB: UsbEp0Setup @ 612.640378ms
INFO:usb_5 -- EP0: SetConfiguration { value: Some(42) }
INFO:usb_5 -- entering the configured state
```
These logs are from a Linux host. You can find traces for other OSes in these files (they are next to this README):
- `win-configured.txt`, this file only contains the logs produced by running `print-descs`
- `macos-configured.txt` (TODO)
You can find a solution to this part of the exercise in `src/bin/usb-5-solution.rs`.
## Next steps
We have covered only a few of the core features of the RTIC framework but the framework has many more features like *software* tasks, tasks that can be spawned by the software; message passing between tasks; and task scheduling, which allows the creation of periodic tasks. We encourage to check the [RTIC book][rtic-book] which describes the features we haven't covered here.
[rtic-book]: https://rtic.rs/0.5/book/en/
[`usb-device`] is a library for building USB devices. It has been built using traits (the pillar of Rust's *generics*) such that USB interfaces like HID and TTY ACM can be implemented in a device agnostic manner. The device details then are limited to a trait *implementation*. There's a work in progress implementation of the `usb-device` trait for the nRF52840 device in [this PR] and there are many `usb-device` "classes" like [HID] and [TTY ACM] that can be used with that trait implementation.
[this PR]: https://github.com/nrf-rs/nrf-hal/pull/144
[HID]: https://crates.io/crates/usbd-hid
[TTY ACM]: https://crates.io/crates/usbd-serial
[`usb-device`]: https://crates.io/crates/usb-device
## String descriptors
> TODO more material if needed
## Custom control transfers
> TODO more material if needed
## References

View file

@ -0,0 +1,196 @@
//! Some USB 2.0 data types
// NOTE this is a solution to exercise `usb-2`
#![deny(missing_docs)]
#![deny(warnings)]
#![no_std]
#[cfg(TODO)]
use core::num::NonZeroU8;
/// Standard USB request
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Request {
/// GET_DESCRIPTOR
// see section 9.4.3 of the USB specification
GetDescriptor {
/// Requested descriptor
descriptor: Descriptor,
/// How many bytes of data to return
length: u16,
},
/// SET_ADDRESS
// see section 9.4.6 of the USB specification
#[cfg(TODO)]
SetAddress {
/// New device address, in the range `1..=127`
address: Option<NonZeroU8>,
},
/// SET_CONFIGURATION
// see section 9.4.7 of the USB specification
#[cfg(TODO)]
SetConfiguration {
/// bConfigurationValue to change the device to
value: Option<NonZeroU8>,
},
// there are even more standard requests but we don't need to support them
}
impl Request {
/// Parses SETUP packet data into a USB request
///
/// Returns `Err` if the SETUP data doesn't match a supported standard request
// see section 9.4 of the USB specification; in particular tables 9-3, 9-4 and 9-5
pub fn parse(
bmrequesttype: u8,
brequest: u8,
wvalue: u16,
windex: u16,
wlength: u16,
) -> Result<Self, ()> {
// see table 9-4
const GET_DESCRIPTOR: u8 = 6;
if bmrequesttype == 0b10000000 && brequest == GET_DESCRIPTOR {
// see table 9-5
const DEVICE: u8 = 1;
const CONFIGURATION: u8 = 2;
let desc_ty = (wvalue >> 8) as u8;
let desc_index = wvalue as u8;
let langid = windex;
if desc_ty == DEVICE && desc_index == 0 && langid == 0 {
Ok(Request::GetDescriptor {
descriptor: Descriptor::Device,
length: wlength,
})
} else if desc_ty == CONFIGURATION && langid == 0 {
Ok(Request::GetDescriptor {
descriptor: Descriptor::Configuration { index: desc_index },
length: wlength,
})
} else {
Err(())
}
} else {
Err(())
}
}
}
/// Descriptor types that appear in GET_DESCRIPTOR requests
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Descriptor {
/// Device descriptor
Device,
/// Configuration descriptor
Configuration {
/// Index of the descriptor
index: u8,
},
// there are even more descriptor types but we don't need to support them
}
#[cfg(test)]
mod tests {
#[cfg(TODO)]
use core::num::NonZeroU8;
use crate::{Descriptor, Request};
#[test]
fn get_descriptor_device() {
// OK: GET_DESCRIPTOR Device [length=18]
assert_eq!(
Request::parse(0b1000_0000, 0x06, 0x0100, 0, 18),
Ok(Request::GetDescriptor {
descriptor: Descriptor::Device,
length: 18
})
);
// wrong descriptor index
assert!(Request::parse(0b1000_0000, 0x06, 0x01_01, 0, 18).is_err());
// ^^
// has language ID but shouldn't
assert!(Request::parse(0b1000_0000, 0x06, 0x01_00, 1033, 18).is_err());
// ^^^^
}
#[test]
fn get_descriptor_configuration() {
// OK: GET_DESCRIPTOR Configuration 0 [length=9]
assert_eq!(
Request::parse(0b1000_0000, 0x06, 0x02_00, 0, 9),
Ok(Request::GetDescriptor {
descriptor: Descriptor::Configuration { index: 0 },
length: 9
})
);
// has language ID but shouldn't
assert!(Request::parse(0b1000_0000, 0x06, 0x02_00, 1033, 9).is_err());
// ^^^^
}
#[cfg(TODO)]
#[test]
fn set_address() {
// OK: SET_ADDRESS 16
assert_eq!(
Request::parse(0b0000_0000, 0x05, 0x00_10, 0, 0),
Ok(Request::SetAddress {
address: NonZeroU8::new(0x10)
})
);
// OK: SET_ADDRESS 0
assert_eq!(
Request::parse(0b0000_0000, 0x05, 0x00_00, 0, 0),
Ok(Request::SetAddress { address: None })
);
// address is outside the valid range
assert!(Request::parse(0b0000_0000, 0x05, 0x00_ff, 1033, 0).is_err());
// ^^
// has language id but shouldn't
assert!(Request::parse(0b0000_0000, 0x05, 0x00_10, 1033, 0).is_err());
// ^^^^
// length should be zero
assert!(Request::parse(0b0000_0000, 0x05, 0x00_10, 0, 1).is_err());
// ^
}
#[cfg(TODO)]
#[test]
fn set_configuration() {
// OK: SET_CONFIGURATION 1
assert_eq!(
Request::parse(0b0000_0000, 0x09, 0x00_01, 0, 0),
Ok(Request::SetConfiguration {
value: NonZeroU8::new(1)
})
);
// OK: SET_CONFIGURATION 0
assert_eq!(
Request::parse(0b0000_0000, 0x09, 0x00_00, 0, 0),
Ok(Request::SetConfiguration { value: None })
);
// has language id but shouldn't
assert!(Request::parse(0b0000_0000, 0x09, 0x00_01, 1033, 0).is_err());
// ^^^^
// length should be zero
assert!(Request::parse(0b0000_0000, 0x09, 0x00_01, 0, 1).is_err());
// ^
}
}

View file

@ -0,0 +1,192 @@
//! Some USB 2.0 data types
// NOTE this is a solution to exercise `usb-2`
#![deny(missing_docs)]
#![deny(warnings)]
#![no_std]
#[cfg(TODO)]
use core::num::NonZeroU8;
/// Standard USB request
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Request {
/// GET_DESCRIPTOR
// see section 9.4.3 of the USB specification
GetDescriptor {
/// Requested descriptor
descriptor: Descriptor,
/// How many bytes of data to return
length: u16,
},
/// SET_ADDRESS
// see section 9.4.6 of the USB specification
#[cfg(TODO)]
SetAddress {
/// New device address, in the range `1..=127`
address: Option<NonZeroU8>,
},
/// SET_CONFIGURATION
// see section 9.4.7 of the USB specification
#[cfg(TODO)]
SetConfiguration {
/// bConfigurationValue to change the device to
value: Option<NonZeroU8>,
},
// there are even more standard requests but we don't need to support them
}
impl Request {
/// Parses SETUP packet data into a USB request
///
/// Returns `Err` if the SETUP data doesn't match a supported standard request
// see section 9.4 of the USB specification; in particular tables 9-3, 9-4 and 9-5
pub fn parse(
bmrequesttype: u8,
brequest: u8,
wvalue: u16,
windex: u16,
wlength: u16,
) -> Result<Self, ()> {
// see table 9-4
const GET_DESCRIPTOR: u8 = 6;
if bmrequesttype == 0b10000000 && brequest == GET_DESCRIPTOR {
// see table 9-5
const DEVICE: u8 = 1;
let desc_ty = (wvalue >> 8) as u8;
let desc_index = wvalue as u8;
let langid = windex;
if desc_ty == DEVICE && desc_index == 0 && langid == 0 {
Ok(Request::GetDescriptor {
descriptor: Descriptor::Device,
length: wlength,
})
} else {
Err(())
}
} else {
Err(())
}
}
}
/// Descriptor types that appear in GET_DESCRIPTOR requests
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Descriptor {
/// Device descriptor
Device,
/// Configuration descriptor
#[cfg(TODO)]
Configuration {
/// Index of the descriptor
index: u8,
},
// there are even more descriptor types but we don't need to support them
}
#[cfg(test)]
mod tests {
#[cfg(TODO)]
use core::num::NonZeroU8;
use crate::{Descriptor, Request};
#[test]
fn get_descriptor_device() {
// OK: GET_DESCRIPTOR Device [length=18]
assert_eq!(
Request::parse(0b1000_0000, 0x06, 0x0100, 0, 18),
Ok(Request::GetDescriptor {
descriptor: Descriptor::Device,
length: 18
})
);
// wrong descriptor index
assert!(Request::parse(0b1000_0000, 0x06, 0x01_01, 0, 18).is_err());
// ^^
// has language ID but shouldn't
assert!(Request::parse(0b1000_0000, 0x06, 0x01_00, 1033, 18).is_err());
// ^^^^
}
#[cfg(TODO)]
#[test]
fn get_descriptor_configuration() {
// OK: GET_DESCRIPTOR Configuration 0 [length=9]
assert_eq!(
Request::parse(0b1000_0000, 0x06, 0x02_00, 0, 9),
Ok(Request::GetDescriptor {
descriptor: Descriptor::Configuration { index: 0 },
length: 9
})
);
// has language ID but shouldn't
assert!(Request::parse(0b1000_0000, 0x06, 0x02_00, 1033, 9).is_err());
// ^^^^
}
#[cfg(TODO)]
#[test]
fn set_address() {
// OK: SET_ADDRESS 16
assert_eq!(
Request::parse(0b0000_0000, 0x05, 0x00_10, 0, 0),
Ok(Request::SetAddress {
address: NonZeroU8::new(0x10)
})
);
// OK: SET_ADDRESS 0
assert_eq!(
Request::parse(0b0000_0000, 0x05, 0x00_00, 0, 0),
Ok(Request::SetAddress { address: None })
);
// address is outside the valid range
assert!(Request::parse(0b0000_0000, 0x05, 0x00_ff, 1033, 0).is_err());
// ^^
// has language id but shouldn't
assert!(Request::parse(0b0000_0000, 0x05, 0x00_10, 1033, 0).is_err());
// ^^^^
// length should be zero
assert!(Request::parse(0b0000_0000, 0x05, 0x00_10, 0, 1).is_err());
// ^
}
#[cfg(TODO)]
#[test]
fn set_configuration() {
// OK: SET_CONFIGURATION 1
assert_eq!(
Request::parse(0b0000_0000, 0x09, 0x00_01, 0, 0),
Ok(Request::SetConfiguration {
value: NonZeroU8::new(1)
})
);
// OK: SET_CONFIGURATION 0
assert_eq!(
Request::parse(0b0000_0000, 0x09, 0x00_00, 0, 0),
Ok(Request::SetConfiguration { value: None })
);
// has language id but shouldn't
assert!(Request::parse(0b0000_0000, 0x09, 0x00_01, 1033, 0).is_err());
// ^^^^
// length should be zero
assert!(Request::parse(0b0000_0000, 0x09, 0x00_01, 0, 1).is_err());
// ^
}
}

View file

@ -4,12 +4,14 @@
#![deny(warnings)]
#![no_std]
#[cfg(TODO)]
use core::num::NonZeroU8;
/// Standard USB request
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Request {
/// GET_DESCRIPTOR
// see section 9.4.3 of the USB specification
GetDescriptor {
/// Requested descriptor
descriptor: Descriptor,
@ -17,21 +19,29 @@ pub enum Request {
length: u16,
},
/// SET_CONFIGURATION
SetConfiguration {
/// bConfigurationValue to change the device to
value: Option<NonZeroU8>,
},
/// SET_ADDRESS
// see section 9.4.6 of the USB specification
#[cfg(TODO)]
SetAddress {
/// New device address, in the range `1..=127`
address: Option<NonZeroU8>,
},
/// SET_CONFIGURATION
// see section 9.4.7 of the USB specification
#[cfg(TODO)]
SetConfiguration {
/// bConfigurationValue to change the device to
value: Option<NonZeroU8>,
},
// there are even more standard requests but we don't need to support them
}
impl Request {
/// Parses SETUP packet data into a USB request
///
/// Returns `Err` if the SETUP data doesn't match a supported standard request
// see section 9.4 of the USB specification; in particular tables 9-3, 9-4 and 9-5
pub fn parse(
_bmrequesttype: u8,
_brequest: u8,
@ -39,7 +49,7 @@ impl Request {
_windex: u16,
_wlength: u16,
) -> Result<Self, ()> {
// TODO
// FIXME
Err(())
}
}
@ -51,14 +61,17 @@ pub enum Descriptor {
Device,
/// Configuration descriptor
#[cfg(TODO)]
Configuration {
/// Index of the descriptor
index: u8,
},
// there are even more descriptor types but we don't need to support them
}
#[cfg(test)]
mod tests {
#[cfg(TODO)]
use core::num::NonZeroU8;
use crate::{Descriptor, Request};
@ -76,12 +89,14 @@ mod tests {
// wrong descriptor index
assert!(Request::parse(0b1000_0000, 0x06, 0x01_01, 0, 18).is_err());
// ^^
// has language ID but shouldn't
assert!(Request::parse(0b1000_0000, 0x06, 0x01_00, 1033, 18).is_err());
// ^^^^
}
#[ignore]
#[cfg(TODO)]
#[test]
fn get_descriptor_configuration() {
// OK: GET_DESCRIPTOR Configuration 0 [length=9]
@ -95,9 +110,10 @@ mod tests {
// has language ID but shouldn't
assert!(Request::parse(0b1000_0000, 0x06, 0x02_00, 1033, 9).is_err());
// ^^^^
}
#[ignore]
#[cfg(TODO)]
#[test]
fn set_address() {
// OK: SET_ADDRESS 16
@ -108,14 +124,26 @@ mod tests {
})
);
// OK: SET_ADDRESS 0
assert_eq!(
Request::parse(0b0000_0000, 0x05, 0x00_00, 0, 0),
Ok(Request::SetAddress { address: None })
);
// address is outside the valid range
assert!(Request::parse(0b0000_0000, 0x05, 0x00_ff, 1033, 0).is_err());
// ^^
// has language id but shouldn't
assert!(Request::parse(0b0000_0000, 0x05, 0x00_10, 1033, 0).is_err());
// ^^^^
// length should be zero
assert!(Request::parse(0b0000_0000, 0x05, 0x00_10, 0, 1).is_err());
// ^
}
#[ignore]
#[cfg(TODO)]
#[test]
fn set_configuration() {
// OK: SET_CONFIGURATION 1
@ -126,10 +154,18 @@ mod tests {
})
);
// OK: SET_CONFIGURATION 0
assert_eq!(
Request::parse(0b0000_0000, 0x09, 0x00_00, 0, 0),
Ok(Request::SetConfiguration { value: None })
);
// has language id but shouldn't
assert!(Request::parse(0b0000_0000, 0x09, 0x00_01, 1033, 0).is_err());
// ^^^^
// length should be zero
assert!(Request::parse(0b0000_0000, 0x09, 0x00_01, 0, 1).is_err());
// ^
}
}

View file

@ -0,0 +1,197 @@
//! Some USB 2.0 data types
// NOTE this is a solution to exercise `usb-2`
#![deny(missing_docs)]
#![deny(warnings)]
#![no_std]
use core::num::NonZeroU8;
/// Standard USB request
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Request {
/// GET_DESCRIPTOR
// see section 9.4.3 of the USB specification
GetDescriptor {
/// Requested descriptor
descriptor: Descriptor,
/// How many bytes of data to return
length: u16,
},
/// SET_ADDRESS
// see section 9.4.6 of the USB specification
SetAddress {
/// New device address, in the range `1..=127`
address: Option<NonZeroU8>,
},
/// SET_CONFIGURATION
// see section 9.4.7 of the USB specification
#[cfg(TODO)]
SetConfiguration {
/// bConfigurationValue to change the device to
value: Option<NonZeroU8>,
},
// there are even more standard requests but we don't need to support them
}
impl Request {
/// Parses SETUP packet data into a USB request
///
/// Returns `Err` if the SETUP data doesn't match a supported standard request
// see section 9.4 of the USB specification; in particular tables 9-3, 9-4 and 9-5
pub fn parse(
bmrequesttype: u8,
brequest: u8,
wvalue: u16,
windex: u16,
wlength: u16,
) -> Result<Self, ()> {
// see table 9-4
const GET_DESCRIPTOR: u8 = 6;
const SET_ADDRESS: u8 = 5;
if bmrequesttype == 0b10000000 && brequest == GET_DESCRIPTOR {
// see table 9-5
const DEVICE: u8 = 1;
let desc_ty = (wvalue >> 8) as u8;
let desc_index = wvalue as u8;
let langid = windex;
if desc_ty == DEVICE && desc_index == 0 && langid == 0 {
Ok(Request::GetDescriptor {
descriptor: Descriptor::Device,
length: wlength,
})
} else {
Err(())
}
} else if bmrequesttype == 0b00000000 && brequest == SET_ADDRESS {
if wvalue < 128 && windex == 0 && wlength == 0 {
Ok(Request::SetAddress {
address: NonZeroU8::new(wvalue as u8),
})
} else {
Err(())
}
} else {
Err(())
}
}
}
/// Descriptor types that appear in GET_DESCRIPTOR requests
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Descriptor {
/// Device descriptor
Device,
/// Configuration descriptor
#[cfg(TODO)]
Configuration {
/// Index of the descriptor
index: u8,
},
// there are even more descriptor types but we don't need to support them
}
#[cfg(test)]
mod tests {
use core::num::NonZeroU8;
use crate::{Descriptor, Request};
#[test]
fn get_descriptor_device() {
// OK: GET_DESCRIPTOR Device [length=18]
assert_eq!(
Request::parse(0b1000_0000, 0x06, 0x0100, 0, 18),
Ok(Request::GetDescriptor {
descriptor: Descriptor::Device,
length: 18
})
);
// wrong descriptor index
assert!(Request::parse(0b1000_0000, 0x06, 0x01_01, 0, 18).is_err());
// ^^
// has language ID but shouldn't
assert!(Request::parse(0b1000_0000, 0x06, 0x01_00, 1033, 18).is_err());
// ^^^^
}
#[cfg(TODO)]
#[test]
fn get_descriptor_configuration() {
// OK: GET_DESCRIPTOR Configuration 0 [length=9]
assert_eq!(
Request::parse(0b1000_0000, 0x06, 0x02_00, 0, 9),
Ok(Request::GetDescriptor {
descriptor: Descriptor::Configuration { index: 0 },
length: 9
})
);
// has language ID but shouldn't
assert!(Request::parse(0b1000_0000, 0x06, 0x02_00, 1033, 9).is_err());
// ^^^^
}
#[test]
fn set_address() {
// OK: SET_ADDRESS 16
assert_eq!(
Request::parse(0b0000_0000, 0x05, 0x00_10, 0, 0),
Ok(Request::SetAddress {
address: NonZeroU8::new(0x10)
})
);
// OK: SET_ADDRESS 0
assert_eq!(
Request::parse(0b0000_0000, 0x05, 0x00_00, 0, 0),
Ok(Request::SetAddress { address: None })
);
// address is outside the valid range
assert!(Request::parse(0b0000_0000, 0x05, 0x00_ff, 1033, 0).is_err());
// ^^
// has language id but shouldn't
assert!(Request::parse(0b0000_0000, 0x05, 0x00_10, 1033, 0).is_err());
// ^^^^
// length should be zero
assert!(Request::parse(0b0000_0000, 0x05, 0x00_10, 0, 1).is_err());
// ^
}
#[cfg(TODO)]
#[test]
fn set_configuration() {
// OK: SET_CONFIGURATION 1
assert_eq!(
Request::parse(0b0000_0000, 0x09, 0x00_01, 0, 0),
Ok(Request::SetConfiguration {
value: NonZeroU8::new(1)
})
);
// OK: SET_CONFIGURATION 0
assert_eq!(
Request::parse(0b0000_0000, 0x09, 0x00_00, 0, 0),
Ok(Request::SetConfiguration { value: None })
);
// has language id but shouldn't
assert!(Request::parse(0b0000_0000, 0x09, 0x00_01, 1033, 0).is_err());
// ^^^^
// length should be zero
assert!(Request::parse(0b0000_0000, 0x09, 0x00_01, 0, 1).is_err());
// ^
}
}

View file

@ -0,0 +1,197 @@
//! Some USB 2.0 data types
// NOTE this is a solution to exercise `usb-2`
#![deny(missing_docs)]
#![deny(warnings)]
#![no_std]
use core::num::NonZeroU8;
/// Standard USB request
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Request {
/// GET_DESCRIPTOR
// see section 9.4.3 of the USB specification
GetDescriptor {
/// Requested descriptor
descriptor: Descriptor,
/// How many bytes of data to return
length: u16,
},
/// SET_ADDRESS
// see section 9.4.6 of the USB specification
#[cfg(TODO)]
SetAddress {
/// New device address, in the range `1..=127`
address: Option<NonZeroU8>,
},
/// SET_CONFIGURATION
// see section 9.4.7 of the USB specification
SetConfiguration {
/// bConfigurationValue to change the device to
value: Option<NonZeroU8>,
},
// there are even more standard requests but we don't need to support them
}
impl Request {
/// Parses SETUP packet data into a USB request
///
/// Returns `Err` if the SETUP data doesn't match a supported standard request
// see section 9.4 of the USB specification; in particular tables 9-3, 9-4 and 9-5
pub fn parse(
bmrequesttype: u8,
brequest: u8,
wvalue: u16,
windex: u16,
wlength: u16,
) -> Result<Self, ()> {
// see table 9-4
const GET_DESCRIPTOR: u8 = 6;
const SET_CONFIGURATION: u8 = 9;
if bmrequesttype == 0b10000000 && brequest == GET_DESCRIPTOR {
// see table 9-5
const DEVICE: u8 = 1;
let desc_ty = (wvalue >> 8) as u8;
let desc_index = wvalue as u8;
let langid = windex;
if desc_ty == DEVICE && desc_index == 0 && langid == 0 {
Ok(Request::GetDescriptor {
descriptor: Descriptor::Device,
length: wlength,
})
} else {
Err(())
}
} else if bmrequesttype == 0b00000000 && brequest == SET_CONFIGURATION {
if wvalue < 256 && windex == 0 && wlength == 0 {
Ok(Request::SetConfiguration {
value: NonZeroU8::new(wvalue as u8),
})
} else {
Err(())
}
} else {
Err(())
}
}
}
/// Descriptor types that appear in GET_DESCRIPTOR requests
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Descriptor {
/// Device descriptor
Device,
/// Configuration descriptor
#[cfg(TODO)]
Configuration {
/// Index of the descriptor
index: u8,
},
// there are even more descriptor types but we don't need to support them
}
#[cfg(test)]
mod tests {
use core::num::NonZeroU8;
use crate::{Descriptor, Request};
#[test]
fn get_descriptor_device() {
// OK: GET_DESCRIPTOR Device [length=18]
assert_eq!(
Request::parse(0b1000_0000, 0x06, 0x0100, 0, 18),
Ok(Request::GetDescriptor {
descriptor: Descriptor::Device,
length: 18
})
);
// wrong descriptor index
assert!(Request::parse(0b1000_0000, 0x06, 0x01_01, 0, 18).is_err());
// ^^
// has language ID but shouldn't
assert!(Request::parse(0b1000_0000, 0x06, 0x01_00, 1033, 18).is_err());
// ^^^^
}
#[cfg(TODO)]
#[test]
fn get_descriptor_configuration() {
// OK: GET_DESCRIPTOR Configuration 0 [length=9]
assert_eq!(
Request::parse(0b1000_0000, 0x06, 0x02_00, 0, 9),
Ok(Request::GetDescriptor {
descriptor: Descriptor::Configuration { index: 0 },
length: 9
})
);
// has language ID but shouldn't
assert!(Request::parse(0b1000_0000, 0x06, 0x02_00, 1033, 9).is_err());
// ^^^^
}
#[cfg(TODO)]
#[test]
fn set_address() {
// OK: SET_ADDRESS 16
assert_eq!(
Request::parse(0b0000_0000, 0x05, 0x00_10, 0, 0),
Ok(Request::SetAddress {
address: NonZeroU8::new(0x10)
})
);
// OK: SET_ADDRESS 0
assert_eq!(
Request::parse(0b0000_0000, 0x05, 0x00_00, 0, 0),
Ok(Request::SetAddress { address: None })
);
// address is outside the valid range
assert!(Request::parse(0b0000_0000, 0x05, 0x00_ff, 1033, 0).is_err());
// ^^
// has language id but shouldn't
assert!(Request::parse(0b0000_0000, 0x05, 0x00_10, 1033, 0).is_err());
// ^^^^
// length should be zero
assert!(Request::parse(0b0000_0000, 0x05, 0x00_10, 0, 1).is_err());
// ^
}
#[test]
fn set_configuration() {
// OK: SET_CONFIGURATION 1
assert_eq!(
Request::parse(0b0000_0000, 0x09, 0x00_01, 0, 0),
Ok(Request::SetConfiguration {
value: NonZeroU8::new(1)
})
);
// OK: SET_CONFIGURATION 0
assert_eq!(
Request::parse(0b0000_0000, 0x09, 0x00_00, 0, 0),
Ok(Request::SetConfiguration { value: None })
);
// has language id but shouldn't
assert!(Request::parse(0b0000_0000, 0x09, 0x00_01, 1033, 0).is_err());
// ^^^^
// length should be zero
assert!(Request::parse(0b0000_0000, 0x09, 0x00_01, 0, 1).is_err());
// ^
}
}

View file

@ -26,7 +26,7 @@ const APP: () = {
}
#[idle]
fn idle(_cx: idle::Context) -> ! {
fn main(_cx: main::Context) -> ! {
log::info!("idle: going to sleep");
// sleep in the background

View file

@ -0,0 +1,59 @@
#![no_main]
#![no_std]
use cortex_m::asm;
use dk::peripheral::POWER;
use panic_log as _; // panic handler
#[rtic::app(device = dk)]
const APP: () = {
struct Resources {
power: POWER,
counter: usize, // <- new resource
}
#[init]
fn init(_cx: init::Context) -> init::LateResources {
let board = dk::init().unwrap();
let power = board.power;
power.intenset.write(|w| w.usbdetected().set_bit());
log::info!("USBDETECTED interrupt enabled");
init::LateResources {
power,
counter: 0, // <- initialize the new resource
}
}
#[idle]
fn main(_cx: main::Context) -> ! {
loop {
log::info!("idle: going to sleep");
asm::wfi();
log::info!("idle: woke up");
}
}
#[task(binds = POWER_CLOCK, resources = [power, counter])]
// ^^^^^^^ we want to access the resource from here
fn on_power_event(cx: on_power_event::Context) {
log::debug!("POWER event occurred");
let power = cx.resources.power;
let counter = cx.resources.counter;
*counter += 1;
let n = *counter;
log::info!(
"on_power_event: cable connected {} time{}",
n,
if n != 1 { "s" } else { "" }
);
// clear the interrupt flag; otherwise this task will run again after it returns
power.events_usbdetected.reset();
}
};

View file

@ -8,7 +8,7 @@ use panic_log as _; // panic handler
#[rtic::app(device = dk)]
const APP: () = {
struct Resources {
power: POWER,
power: POWER, // <- resource declaration
}
#[init]
@ -21,11 +21,13 @@ const APP: () = {
log::info!("USBDETECTED interrupt enabled");
init::LateResources { power }
init::LateResources {
power, // <- resource initialization
}
}
#[idle]
fn idle(_cx: idle::Context) -> ! {
fn main(_cx: main::Context) -> ! {
loop {
log::info!("idle: going to sleep");
asm::wfi();
@ -34,10 +36,17 @@ const APP: () = {
}
#[task(binds = POWER_CLOCK, resources = [power])]
// ^^^^^^^ resource access list
fn on_power_event(cx: on_power_event::Context) {
log::info!("POWER event occurred");
// resources available to this task
let resources = cx.resources;
// the POWER peripheral can be accessed through a reference
let power: &mut POWER = resources.power;
// clear the interrupt flag; otherwise this task will run again after it returns
cx.resources.power.events_usbdetected.reset();
power.events_usbdetected.reset();
}
};

View file

@ -19,13 +19,11 @@ const APP: () = {
usbd::init(board.power, &board.usbd);
usbd::connect(&board.usbd);
init::LateResources { usbd: board.usbd }
}
#[task(binds = USBD, resources = [usbd])]
fn usb(cx: usb::Context) {
fn main(cx: main::Context) {
let usbd = cx.resources.usbd;
while let Some(event) = usbd::next_event(usbd) {
@ -34,14 +32,20 @@ const APP: () = {
}
};
fn on_event(usbd: &USBD, event: Event) {
fn on_event(_usbd: &USBD, event: Event) {
log::info!("USB: {:?}", event);
match event {
Event::UsbReset => usbd::todo(usbd),
Event::UsbReset => {
// going from the Default state to the Default state is a no-operation
log::info!("returning to the Default state");
}
Event::UsbEp0DataDone => usbd::todo(usbd),
Event::UsbEp0DataDone => todo!(),
Event::UsbEp0Setup => usbd::todo(usbd),
Event::UsbEp0Setup => {
log::info!("goal reached; move to the next section");
dk::exit()
}
}
}

View file

@ -0,0 +1,52 @@
#![no_main]
#![no_std]
use dk::{
peripheral::USBD,
usbd::{self, Event},
};
use panic_log as _; // panic handler
#[rtic::app(device = dk)]
const APP: () = {
struct Resources {
usbd: USBD,
}
#[init]
fn init(_cx: init::Context) -> init::LateResources {
let board = dk::init().unwrap();
// initialize the USBD peripheral
// NOTE this will block if the USB cable is not connected to port J3
usbd::init(board.power, &board.usbd);
log::info!("USBD initialized");
init::LateResources { usbd: board.usbd }
}
#[task(binds = USBD, resources = [usbd])]
fn main(cx: main::Context) {
let usbd = cx.resources.usbd;
while let Some(event) = usbd::next_event(usbd) {
on_event(usbd, event)
}
}
};
fn on_event(_usbd: &USBD, event: Event) {
log::info!("USB: {:?} @ {:?}", event, dk::uptime());
match event {
Event::UsbReset => todo!(),
Event::UsbEp0DataDone => todo!(),
Event::UsbEp0Setup => {
log::info!("goal reached; move to the next section");
dk::exit()
}
}
}

View file

@ -0,0 +1,81 @@
#![no_main]
#![no_std]
use dk::{
peripheral::USBD,
usbd::{self, Event},
};
use panic_log as _; // panic handler
use usb::{Descriptor, Request};
#[rtic::app(device = dk)]
const APP: () = {
struct Resources {
usbd: USBD,
}
#[init]
fn init(_cx: init::Context) -> init::LateResources {
let board = dk::init().unwrap();
usbd::init(board.power, &board.usbd);
init::LateResources { usbd: board.usbd }
}
#[task(binds = USBD, resources = [usbd])]
fn main(cx: main::Context) {
let usbd = cx.resources.usbd;
while let Some(event) = usbd::next_event(usbd) {
on_event(usbd, event)
}
}
};
fn on_event(usbd: &USBD, event: Event) {
log::info!("USB: {:?} @ {:?}", event, dk::uptime());
match event {
Event::UsbReset => {
// nothing to do here at the moment
}
Event::UsbEp0DataDone => todo!(),
Event::UsbEp0Setup => {
let bmrequesttype = usbd.bmrequesttype.read().bits() as u8;
let brequest = usbd.brequest.read().brequest().bits();
let wlength = (u16::from(usbd.wlengthh.read().wlengthh().bits()) << 8)
| u16::from(usbd.wlengthl.read().wlengthl().bits());
let windex = (u16::from(usbd.windexh.read().windexh().bits()) << 8)
| u16::from(usbd.windexl.read().windexl().bits());
let wvalue = (u16::from(usbd.wvalueh.read().wvalueh().bits()) << 8)
| u16::from(usbd.wvaluel.read().wvaluel().bits());
log::info!(
"SETUP: bmrequesttype: {}, brequest: {}, wlength: {}, windex: {}, wvalue: {}",
bmrequesttype,
brequest,
wlength,
windex,
wvalue
);
if let Ok(Request::GetDescriptor { descriptor, length }) =
Request::parse(bmrequesttype, brequest, wvalue, windex, wlength)
{
match descriptor {
Descriptor::Device => {
log::info!("GET_DESCRIPTOR Device [length={}]", length);
log::info!("Goal reached; move to the next section");
dk::exit()
}
}
} else {
unreachable!() // don't care about this for now
}
}
}
}

View file

@ -20,13 +20,11 @@ const APP: () = {
usbd::init(board.power, &board.usbd);
usbd::connect(&board.usbd);
init::LateResources { usbd: board.usbd }
}
#[task(binds = USBD, resources = [usbd])]
fn on_usbd(cx: on_usbd::Context) {
fn main(cx: main::Context) {
let usbd = cx.resources.usbd;
while let Some(event) = usbd::next_event(usbd) {
@ -35,25 +33,26 @@ const APP: () = {
}
};
fn on_event(usbd: &USBD, event: Event) {
log::info!("USB: {:?}", event);
fn on_event(_usbd: &USBD, event: Event) {
log::info!("USB: {:?} @ {:?}", event, dk::uptime());
match event {
Event::UsbReset => {
// nothing to do here at the moment
}
Event::UsbEp0DataDone => usbd::todo(usbd),
Event::UsbEp0DataDone => todo!(),
Event::UsbEp0Setup => {
let bmrequesttype = usbd.bmrequesttype.read().bits() as u8;
let brequest = usbd.brequest.read().brequest().bits();
let wlength = usbd::wlength(usbd);
let windex = usbd::windex(usbd);
let wvalue = usbd::wvalue(usbd);
// TODO read USBD registers
let bmrequesttype = 0;
let brequest = 0;
let wlength = 0;
let windex = 0;
let wvalue = 0;
log::info!(
"bmrequesttype: {}, brequest: {}, wlength: {}, windex: {}, wvalue: {}",
"SETUP: bmrequesttype: {}, brequest: {}, wlength: {}, windex: {}, wvalue: {}",
bmrequesttype,
brequest,
wlength,
@ -61,21 +60,20 @@ fn on_event(usbd: &USBD, event: Event) {
wvalue
);
// FIXME modify `advanced/common/usb` to make this work
if let Ok(Request::GetDescriptor { descriptor, length }) =
Request::parse(bmrequesttype, brequest, wvalue, windex, wlength)
{
match descriptor {
Descriptor::Device => {
// GOAL
log::info!("GET_DESCRIPTOR Device [length={}]", length);
usbd::todo(usbd)
log::info!("Goal reached; move to the next section");
dk::exit()
}
_ => usbd::todo(usbd),
}
} else {
usbd::todo(usbd)
unreachable!() // don't care about this for now
}
}
}

View file

@ -0,0 +1,99 @@
#![no_main]
#![no_std]
use dk::{
peripheral::USBD,
usbd::{self, Ep0In, Event},
};
use panic_log as _; // panic handler
use usb::{Descriptor, Request};
#[rtic::app(device = dk)]
const APP: () = {
struct Resources {
usbd: USBD,
ep0in: Ep0In,
}
#[init]
fn init(_cx: init::Context) -> init::LateResources {
let board = dk::init().unwrap();
usbd::init(board.power, &board.usbd);
init::LateResources {
ep0in: board.ep0in,
usbd: board.usbd,
}
}
#[task(binds = USBD, resources = [usbd, ep0in])]
fn main(cx: main::Context) {
let usbd = cx.resources.usbd;
let ep0in = cx.resources.ep0in;
while let Some(event) = usbd::next_event(usbd) {
on_event(usbd, ep0in, event)
}
}
};
fn on_event(usbd: &USBD, ep0in: &mut Ep0In, event: Event) {
log::info!("USB: {:?} @ {:?}", event, dk::uptime());
match event {
Event::UsbReset => {
// nothing to do here at the moment
}
Event::UsbEp0DataDone => ep0in.end(usbd),
Event::UsbEp0Setup => {
let bmrequesttype = usbd.bmrequesttype.read().bits() as u8;
let brequest = usbd.brequest.read().brequest().bits();
let wlength = usbd::wlength(usbd);
let windex = usbd::windex(usbd);
let wvalue = usbd::wvalue(usbd);
log::info!(
"SETUP: bmrequesttype: {}, brequest: {}, wlength: {}, windex: {}, wvalue: {}",
bmrequesttype,
brequest,
wlength,
windex,
wvalue
);
if let Ok(Request::GetDescriptor { descriptor, length }) =
Request::parse(bmrequesttype, brequest, wvalue, windex, wlength)
{
match descriptor {
Descriptor::Device => {
log::info!("GET_DESCRIPTOR Device [length={}]", length);
let desc = usb2::device::Descriptor {
bDeviceClass: 0,
bDeviceProtocol: 0,
bDeviceSubClass: 0,
bMaxPacketSize0: usb2::device::bMaxPacketSize0::B64,
bNumConfigurations: core::num::NonZeroU8::new(1).unwrap(),
bcdDevice: 0x01_00, // 1.00
iManufacturer: None,
iProduct: None,
iSerialNumber: None,
idProduct: consts::PID,
idVendor: consts::VID,
};
let desc_bytes = desc.bytes();
let resp =
&desc_bytes[..core::cmp::min(desc_bytes.len(), usize::from(length))];
ep0in.start(&resp, usbd);
}
}
} else {
log::error!("unknown request (goal achieved if GET_DESCRIPTOR Device was handled)");
dk::exit()
}
}
}
}

View file

@ -6,10 +6,7 @@ use dk::{
usbd::{self, Ep0In, Event},
};
use panic_log as _; // panic handler
// use one of these
// use usb::{Descriptor, Request}; // your implementation
use usb2::{GetDescriptor as Descriptor, StandardRequest as Request}; // crates.io impl
use usb::{Descriptor, Request};
#[rtic::app(device = dk)]
const APP: () = {
@ -24,8 +21,6 @@ const APP: () = {
usbd::init(board.power, &board.usbd);
usbd::connect(&board.usbd);
init::LateResources {
ep0in: board.ep0in,
usbd: board.usbd,
@ -33,7 +28,7 @@ const APP: () = {
}
#[task(binds = USBD, resources = [usbd, ep0in])]
fn on_usbd(cx: on_usbd::Context) {
fn main(cx: main::Context) {
let usbd = cx.resources.usbd;
let ep0in = cx.resources.ep0in;
@ -44,14 +39,14 @@ const APP: () = {
};
fn on_event(usbd: &USBD, ep0in: &mut Ep0In, event: Event) {
log::info!("USB: {:?}", event);
log::info!("USB: {:?} @ {:?}", event, dk::uptime());
match event {
Event::UsbReset => {
// nothing to do here at the moment
}
Event::UsbEp0DataDone => usbd::todo(usbd),
Event::UsbEp0DataDone => todo!(), // <- TODO
Event::UsbEp0Setup => {
let bmrequesttype = usbd.bmrequesttype.read().bits() as u8;
@ -61,7 +56,7 @@ fn on_event(usbd: &USBD, ep0in: &mut Ep0In, event: Event) {
let wvalue = usbd::wvalue(usbd);
log::info!(
"bmrequesttype: {}, brequest: {}, wlength: {}, windex: {}, wvalue: {}",
"SETUP: bmrequesttype: {}, brequest: {}, wlength: {}, windex: {}, wvalue: {}",
bmrequesttype,
brequest,
wlength,
@ -79,13 +74,12 @@ fn on_event(usbd: &USBD, ep0in: &mut Ep0In, event: Event) {
// FIXME send back a valid device descriptor, truncated to `length` bytes
// let desc = usb2::device::Descriptor { .. };
let resp = [];
let _ = ep0in.start(&resp, usbd);
ep0in.start(&resp, usbd);
}
_ => usbd::todo(usbd),
}
} else {
usbd::todo(usbd)
log::error!("unknown request (goal achieved if GET_DESCRIPTOR Device was handled)");
dk::exit()
}
}
}

View file

@ -0,0 +1,175 @@
#![no_main]
#![no_std]
use core::num::NonZeroU8;
use dk::{
peripheral::USBD,
usbd::{self, Ep0In, Event},
};
use panic_log as _; // panic handler
use usb2::{GetDescriptor as Descriptor, StandardRequest as Request, State};
#[rtic::app(device = dk)]
const APP: () = {
struct Resources {
ep0in: Ep0In,
state: State,
usbd: USBD,
}
#[init]
fn init(_cx: init::Context) -> init::LateResources {
let board = dk::init().unwrap();
usbd::init(board.power, &board.usbd);
init::LateResources {
usbd: board.usbd,
state: State::Default,
ep0in: board.ep0in,
}
}
#[task(binds = USBD, resources = [state, usbd, ep0in])]
fn main(cx: main::Context) {
let usbd = cx.resources.usbd;
let state = cx.resources.state;
let ep0in = cx.resources.ep0in;
while let Some(event) = usbd::next_event(usbd) {
on_event(usbd, state, ep0in, event)
}
}
};
fn on_event(usbd: &USBD, state: &mut State, ep0in: &mut Ep0In, event: Event) {
log::info!("USB: {:?} @ {:?}", event, dk::uptime());
match event {
Event::UsbReset => {
log::info!("USB reset condition detected");
*state = State::Default;
}
Event::UsbEp0DataDone => {
log::info!("EP0IN: transfer complete");
ep0in.end(usbd)
}
Event::UsbEp0Setup => {
if ep0setup(usbd, state, ep0in).is_err() {
log::warn!("EP0IN: unexpected request; stalling the endpoint");
usbd::ep0stall(usbd);
}
}
}
}
/// The `bConfigurationValue` of the only supported configuration
const CONFIG_VAL: u8 = 42;
fn ep0setup(usbd: &USBD, state: &mut State, ep0in: &mut Ep0In) -> Result<(), ()> {
let bmrequesttype = usbd.bmrequesttype.read().bits() as u8;
let brequest = usbd.brequest.read().brequest().bits();
let wlength = usbd::wlength(usbd);
let windex = usbd::windex(usbd);
let wvalue = usbd::wvalue(usbd);
let request = Request::parse(bmrequesttype, brequest, wvalue, windex, wlength)?;
log::info!("EP0: {:?}", request);
match request {
// section 9.4.3
// this request is valid in any state
Request::GetDescriptor { descriptor, length } => match descriptor {
Descriptor::Device => {
let desc = usb2::device::Descriptor {
bDeviceClass: 0,
bDeviceProtocol: 0,
bDeviceSubClass: 0,
bMaxPacketSize0: usb2::device::bMaxPacketSize0::B64,
bNumConfigurations: core::num::NonZeroU8::new(1).unwrap(),
bcdDevice: 0x0100, // 1.00
iManufacturer: None,
iProduct: None,
iSerialNumber: None,
idProduct: consts::PID,
idVendor: consts::VID,
};
let bytes = desc.bytes();
ep0in.start(&bytes[..core::cmp::min(bytes.len(), length.into())], usbd);
}
Descriptor::Configuration { index } => {
if index == 0 {
let mut resp = heapless::Vec::<u8, heapless::consts::U64>::new();
let conf_desc = usb2::configuration::Descriptor {
wTotalLength: (usb2::configuration::Descriptor::SIZE
+ usb2::interface::Descriptor::SIZE)
.into(),
bNumInterfaces: NonZeroU8::new(1).unwrap(),
bConfigurationValue: core::num::NonZeroU8::new(CONFIG_VAL).unwrap(),
iConfiguration: None,
bmAttributes: usb2::configuration::bmAttributes {
self_powered: true,
remote_wakeup: false,
},
bMaxPower: 250, // 500 mA
};
let iface_desc = usb2::interface::Descriptor {
bInterfaceNumber: 0,
bAlternativeSetting: 0,
bNumEndpoints: 0,
bInterfaceClass: 0,
bInterfaceSubClass: 0,
bInterfaceProtocol: 0,
iInterface: None,
};
resp.extend_from_slice(&conf_desc.bytes()).unwrap();
resp.extend_from_slice(&iface_desc.bytes()).unwrap();
ep0in.start(&resp[..core::cmp::min(resp.len(), length.into())], usbd);
} else {
// out of bounds access: stall the endpoint
return Err(());
}
}
_ => return Err(()),
},
Request::SetAddress { address } => {
match state {
State::Default => {
if let Some(address) = address {
*state = State::Address(address);
} else {
// stay in the default state
}
}
State::Address(..) => {
if let Some(address) = address {
// use the new address
*state = State::Address(address);
} else {
*state = State::Default;
}
}
// unspecified behavior
State::Configured { .. } => return Err(()),
}
// the response to this request is handled in hardware
}
// stall any other request
_ => return Err(()),
}
Ok(())
}

View file

@ -8,16 +8,14 @@ use dk::{
use panic_log as _; // panic handler
// use one of these
// use usb::{Descriptor, Request}; // your implementation
use usb2::State;
use usb2::{GetDescriptor as Descriptor, StandardRequest as Request}; // crates.io impl
use usb::{Descriptor, Request};
#[rtic::app(device = dk)]
const APP: () = {
struct Resources {
usbd: USBD,
ep0in: Ep0In,
state: State,
state: usb2::State,
}
#[init]
@ -26,17 +24,15 @@ const APP: () = {
usbd::init(board.power, &board.usbd);
usbd::connect(&board.usbd);
init::LateResources {
usbd: board.usbd,
state: State::Default,
state: usb2::State::Default,
ep0in: board.ep0in,
}
}
#[task(binds = USBD, resources = [usbd, ep0in, state])]
fn on_usbd(cx: on_usbd::Context) {
fn main(cx: main::Context) {
let usbd = cx.resources.usbd;
let ep0in = cx.resources.ep0in;
let state = cx.resources.state;
@ -47,13 +43,12 @@ const APP: () = {
}
};
fn on_event(usbd: &USBD, ep0in: &mut Ep0In, state: &mut State, event: Event) {
log::info!("USB: {:?}", event);
fn on_event(usbd: &USBD, ep0in: &mut Ep0In, state: &mut usb2::State, event: Event) {
log::info!("USB: {:?} @ {:?}", event, dk::uptime());
match event {
Event::UsbReset => {
// TODO change `state`
}
// TODO change `state`
Event::UsbReset => todo!(),
Event::UsbEp0DataDone => ep0in.end(usbd),
@ -61,13 +56,12 @@ fn on_event(usbd: &USBD, ep0in: &mut Ep0In, state: &mut State, event: Event) {
if ep0setup(usbd, ep0in, state).is_err() {
// unsupported or invalid request: stall the endpoint
log::warn!("EP0IN: stalled");
usbd::todo(usbd)
}
}
}
}
fn ep0setup(usbd: &USBD, ep0in: &mut Ep0In, _state: &mut State) -> Result<(), ()> {
fn ep0setup(usbd: &USBD, ep0in: &mut Ep0In, _state: &mut usb2::State) -> Result<(), ()> {
let bmrequesttype = usbd.bmrequesttype.read().bits() as u8;
let brequest = usbd.brequest.read().brequest().bits();
let wlength = usbd::wlength(usbd);
@ -106,15 +100,13 @@ fn ep0setup(usbd: &USBD, ep0in: &mut Ep0In, _state: &mut State) -> Result<(), ()
let _ = ep0in.start(&bytes[..core::cmp::min(bytes.len(), length.into())], usbd);
}
_ => usbd::todo(usbd),
// TODO Configuration descriptor
// Descriptor::Configuration => todo!(),
},
Request::SetAddress { .. } => usbd::todo(usbd),
Request::SetConfiguration { .. } => usbd::todo(usbd),
// this request is not supported
_ => return Err(()),
// TODO
// Request::SetAddress { .. } => todo!(),
// Request::SetConfiguration { .. } => todo!(),
}
Ok(())

View file

@ -8,7 +8,7 @@ use dk::{
usbd::{self, Ep0In, Event},
};
use panic_log as _; // panic handler
use usb2::{GetDescriptor, StandardRequest, State};
use usb2::{GetDescriptor as Descriptor, StandardRequest as Request, State};
#[rtic::app(device = dk)]
const APP: () = {
@ -24,8 +24,6 @@ const APP: () = {
usbd::init(board.power, &board.usbd);
usbd::connect(&board.usbd);
init::LateResources {
usbd: board.usbd,
state: State::Default,
@ -34,7 +32,7 @@ const APP: () = {
}
#[task(binds = USBD, resources = [state, usbd, ep0in])]
fn usb(cx: usb::Context) {
fn main(cx: main::Context) {
let usbd = cx.resources.usbd;
let state = cx.resources.state;
let ep0in = cx.resources.ep0in;
@ -46,7 +44,7 @@ const APP: () = {
};
fn on_event(usbd: &USBD, state: &mut State, ep0in: &mut Ep0In, event: Event) {
log::info!("USB: {:?}", event);
log::info!("USB: {:?} @ {:?}", event, dk::uptime());
match event {
Event::UsbReset => {
@ -69,7 +67,7 @@ fn on_event(usbd: &USBD, state: &mut State, ep0in: &mut Ep0In, event: Event) {
}
/// The `bConfigurationValue` of the only supported configuration
const CONFIG_VAL: u8 = 1;
const CONFIG_VAL: u8 = 42;
fn ep0setup(usbd: &USBD, state: &mut State, ep0in: &mut Ep0In) -> Result<(), ()> {
let bmrequesttype = usbd.bmrequesttype.read().bits() as u8;
@ -78,14 +76,14 @@ fn ep0setup(usbd: &USBD, state: &mut State, ep0in: &mut Ep0In) -> Result<(), ()>
let windex = usbd::windex(usbd);
let wvalue = usbd::wvalue(usbd);
let request = StandardRequest::parse(bmrequesttype, brequest, wvalue, windex, wlength)?;
log::info!("{:?}", request);
let request = Request::parse(bmrequesttype, brequest, wvalue, windex, wlength)?;
log::info!("EP0: {:?}", request);
match request {
// section 9.4.3
// this request is valid in any state
StandardRequest::GetDescriptor { descriptor, length } => match descriptor {
GetDescriptor::Device => {
Request::GetDescriptor { descriptor, length } => match descriptor {
Descriptor::Device => {
let desc = usb2::device::Descriptor {
bDeviceClass: 0,
bDeviceProtocol: 0,
@ -100,15 +98,17 @@ fn ep0setup(usbd: &USBD, state: &mut State, ep0in: &mut Ep0In) -> Result<(), ()>
idVendor: consts::VID,
};
let bytes = desc.bytes();
ep0in.start(&bytes[..core::cmp::min(bytes.len(), length.into())], usbd)?
ep0in.start(&bytes[..core::cmp::min(bytes.len(), length.into())], usbd);
}
GetDescriptor::Configuration { index } => {
Descriptor::Configuration { index } => {
if index == 0 {
let mut full_desc = heapless::Vec::<u8, heapless::consts::U64>::new();
let mut resp = heapless::Vec::<u8, heapless::consts::U64>::new();
let conf_desc = usb2::configuration::Descriptor {
wTotalLength: usb2::configuration::Descriptor::SIZE.into(),
wTotalLength: (usb2::configuration::Descriptor::SIZE
+ usb2::interface::Descriptor::SIZE)
.into(),
bNumInterfaces: NonZeroU8::new(1).unwrap(),
bConfigurationValue: core::num::NonZeroU8::new(CONFIG_VAL).unwrap(),
iConfiguration: None,
@ -129,12 +129,9 @@ fn ep0setup(usbd: &USBD, state: &mut State, ep0in: &mut Ep0In) -> Result<(), ()>
iInterface: None,
};
full_desc.extend_from_slice(&conf_desc.bytes()).unwrap();
full_desc.extend_from_slice(&iface_desc.bytes()).unwrap();
ep0in.start(
&full_desc[..core::cmp::min(full_desc.len(), length.into())],
usbd,
)?;
resp.extend_from_slice(&conf_desc.bytes()).unwrap();
resp.extend_from_slice(&iface_desc.bytes()).unwrap();
ep0in.start(&resp[..core::cmp::min(resp.len(), length.into())], usbd);
} else {
// out of bounds access: stall the endpoint
return Err(());
@ -144,7 +141,7 @@ fn ep0setup(usbd: &USBD, state: &mut State, ep0in: &mut Ep0In) -> Result<(), ()>
_ => return Err(()),
},
StandardRequest::SetAddress { address } => {
Request::SetAddress { address } => {
match state {
State::Default => {
if let Some(address) = address {
@ -170,9 +167,7 @@ fn ep0setup(usbd: &USBD, state: &mut State, ep0in: &mut Ep0In) -> Result<(), ()>
// the response to this request is handled in hardware
}
StandardRequest::SetConfiguration { value } => {
log::info!("SET_CONFIGURATION {:?} ({:?})", value, state);
Request::SetConfiguration { value } => {
match *state {
// unspecified behavior
State::Default => return Err(()),

View file

@ -0,0 +1,33 @@
#![deny(unused_must_use)]
#![no_main]
#![no_std]
use cortex_m_rt::entry;
use heapless::{consts, Vec};
use panic_log as _; // the panicking behavior
#[entry]
fn main() -> ! {
dk::init().unwrap();
// a stack-allocated `Vec` with capacity for 6 bytes
let mut buffer = Vec::<u8, consts::U6>::new();
// ^^ capacity; this is a type not a value
// `heapless::Vec` exposes the same API surface as `std::Vec` but some of its methods return a
// `Result` to indicate whether the operation failed due to the `heapless::Vec` being full
log::info!("start: {:?}", buffer);
buffer.push(0).expect("buffer full");
log::info!("after `push`: {:?}", buffer);
buffer.extend_from_slice(&[1, 2, 3]).expect("buffer full");
log::info!("after `extend`: {:?}", buffer);
// TODO try this operation
// buffer.extend_from_slice(&[4, 5, 6, 7]).expect("buffer full");
// TODO try changing the capacity of the `heapless::Vec`
dk::exit()
}

View file

@ -5,9 +5,23 @@ fn main() -> Result<(), anyhow::Error> {
let dev_desc = dev.device_descriptor()?;
if dev_desc.vendor_id() == consts::VID && dev_desc.product_id() == consts::PID {
println!("{:#?}", dev_desc);
println!("address: {}", dev.address());
for i in 0..dev_desc.num_configurations() {
println!("{}: {:#?}", i, dev.config_descriptor(i)?);
let conf_desc = dev.config_descriptor(i)?;
println!("config{}: {:#?}", i, conf_desc);
for iface in conf_desc.interfaces() {
println!(
"iface{}: {:#?}",
iface.number(),
iface.descriptors().collect::<Vec<_>>()
);
}
}
// TODO open the device; this will force the OS to configure it
// let mut handle = dev.open()?;
return Ok(());
}
}

View file

@ -0,0 +1,42 @@
INFO:rtic_usb_4 -- USB: UsbReset @ 305.594085691s
INFO:rtic_usb_4 -- USB reset condition detected
INFO:rtic_usb_4 -- USB: UsbEp0Setup @ 305.681701658s
INFO:rtic_usb_4 -- EP0: GetDescriptor { descriptor: Device, length: 64 }
INFO:dk::usbd -- EP0IN: start 18B transfer
INFO:rtic_usb_4 -- USB: UsbEp0DataDone @ 305.682037352s
INFO:rtic_usb_4 -- EP0IN: transfer complete
INFO:dk::usbd -- EP0IN: transfer done
INFO:rtic_usb_4 -- USB: UsbReset @ 305.682220457s
INFO:rtic_usb_4 -- USB reset condition detected
INFO:rtic_usb_4 -- USB: UsbEp0Setup @ 305.769470213s
INFO:rtic_usb_4 -- EP0: SetAddress { address: Some(7) }
INFO:rtic_usb_4 -- USB: UsbEp0Setup @ 305.780456541s
INFO:rtic_usb_4 -- EP0: GetDescriptor { descriptor: Device, length: 18 }
INFO:dk::usbd -- EP0IN: start 18B transfer
INFO:rtic_usb_4 -- USB: UsbEp0DataDone @ 305.780761718s
INFO:rtic_usb_4 -- EP0IN: transfer complete
INFO:dk::usbd -- EP0IN: transfer done
INFO:rtic_usb_4 -- USB: UsbEp0Setup @ 305.784606932s
INFO:rtic_usb_4 -- EP0: GetDescriptor { descriptor: Device, length: 18 }
INFO:dk::usbd -- EP0IN: start 18B transfer
INFO:rtic_usb_4 -- USB: UsbEp0DataDone @ 305.784912108s
INFO:rtic_usb_4 -- EP0IN: transfer complete
INFO:dk::usbd -- EP0IN: transfer done
INFO:rtic_usb_4 -- USB: UsbEp0Setup @ 305.785095213s
INFO:rtic_usb_4 -- EP0: GetDescriptor { descriptor: Configuration { index: 0 }, length: 9 }
INFO:dk::usbd -- EP0IN: start 9B transfer
INFO:rtic_usb_4 -- USB: UsbEp0DataDone @ 305.78540039s
INFO:rtic_usb_4 -- EP0IN: transfer complete
INFO:dk::usbd -- EP0IN: transfer done
INFO:rtic_usb_4 -- USB: UsbEp0Setup @ 305.785583495s
INFO:rtic_usb_4 -- EP0: GetDescriptor { descriptor: Configuration { index: 0 }, length: 18 }
INFO:dk::usbd -- EP0IN: start 18B transfer
INFO:rtic_usb_4 -- USB: UsbEp0DataDone @ 305.785919188s
INFO:rtic_usb_4 -- EP0IN: transfer complete
INFO:dk::usbd -- EP0IN: transfer done
INFO:rtic_usb_4 -- USB: UsbEp0Setup @ 305.786102293s
INFO:rtic_usb_4 -- EP0: GetStatus(Device)
WARN:rtic_usb_4 -- EP0IN: stalled
INFO:rtic_usb_4 -- USB: UsbEp0Setup @ 305.786621093s
INFO:rtic_usb_4 -- EP0: SetConfiguration { value: Some(42) }
INFO:rtic_usb_4 -- entering the configured stat

View file

@ -0,0 +1,27 @@
INFO:rtic_usb_4 -- USB: UsbReset @ 101.837157ms
INFO:rtic_usb_4 -- USB reset condition detected
INFO:rtic_usb_4 -- USB: UsbEp0Setup @ 190.216063ms
INFO:rtic_usb_4 -- EP0: GetDescriptor { descriptor: Device, length: 64 }
INFO:dk::usbd -- EP0IN: start 18B transfer
INFO:rtic_usb_4 -- USB: UsbEp0DataDone @ 190.551757ms
INFO:rtic_usb_4 -- EP0IN: transfer complete
INFO:dk::usbd -- EP0IN: transfer done
INFO:rtic_usb_4 -- USB: UsbReset @ 190.734862ms
INFO:rtic_usb_4 -- USB reset condition detected
INFO:rtic_usb_4 -- USB: UsbEp0Setup @ 277.954101ms
INFO:rtic_usb_4 -- EP0: SetAddress { address: Some(6) }
INFO:rtic_usb_4 -- USB: UsbEp0Setup @ 288.940428ms
INFO:rtic_usb_4 -- EP0: GetDescriptor { descriptor: Device, length: 18 }
INFO:dk::usbd -- EP0IN: start 18B transfer
INFO:rtic_usb_4 -- USB: UsbEp0DataDone @ 289.245603ms
INFO:rtic_usb_4 -- EP0IN: transfer complete
INFO:dk::usbd -- EP0IN: transfer done
INFO:rtic_usb_4 -- USB: UsbEp0Setup @ 296.783446ms
INFO:rtic_usb_4 -- EP0: GetDescriptor { descriptor: Configuration { index: 0 }, length: 255 }
INFO:dk::usbd -- EP0IN: start 18B transfer
INFO:rtic_usb_4 -- USB: UsbEp0DataDone @ 297.11914ms
INFO:rtic_usb_4 -- EP0IN: transfer complete
INFO:dk::usbd -- EP0IN: transfer done
INFO:rtic_usb_4 -- USB: UsbEp0Setup @ 297.302245ms
INFO:rtic_usb_4 -- EP0: GetDescriptor { descriptor: DeviceQualifier, length: 10 }
WARN:rtic_usb_4 -- EP0IN: stalled

View file

@ -27,16 +27,14 @@ impl Ep0In {
///
/// # Panics
///
/// This function panics if the last transfer was not finished by calling the `end` function
pub fn start(&mut self, bytes: &[u8], usbd: &USBD) -> Result<(), ()> {
if self.busy {
panic!("EP0IN: last transfer has not completed");
}
if bytes.len() > self.buffer.len() {
log::error!("EP0IN: multi-packet data transfers are not supported");
return Err(());
}
/// - This function panics if the last transfer was not finished by calling the `end` function
/// - This function panics if `bytes` is larger than the maximum packet size (64 bytes)
pub fn start(&mut self, bytes: &[u8], usbd: &USBD) {
assert!(!self.busy, "EP0IN: last transfer has not completed");
assert!(
bytes.len() <= self.buffer.len(),
"EP0IN: multi-packet data transfers are not supported"
);
let n = bytes.len();
self.buffer[..n].copy_from_slice(bytes);
@ -58,8 +56,6 @@ impl Ep0In {
// start DMA transfer
dma_start();
usbd.tasks_startepin[0].write(|w| w.tasks_startepin().set_bit());
Ok(())
}
/// Completes a data transfer
@ -79,6 +75,7 @@ impl Ep0In {
usbd.events_ep0datadone.reset();
self.busy = false;
log::info!("EP0IN: transfer done");
}
}
}
@ -87,7 +84,7 @@ impl Ep0In {
// caller's memory operations
//
// This function call *must* be *followed* by a memory *store* operation. Memory operations that
// follow this function call will *not* be moved, by the compiler or the instruction pipeline, to
// *precede* this function call will *not* be moved, by the compiler or the instruction pipeline, to
// *after* the function call.
fn dma_start() {
atomic::fence(Ordering::Release);
@ -97,7 +94,7 @@ fn dma_start() {
// memory operations
//
// This function call *must* be *preceded* by a memory *load* operation. Memory operations that
// follow this function call will *not* be moved, by the compiler or the instruction pipeline, to
// *follow* this function call will *not* be moved, by the compiler or the instruction pipeline, to
// *before* the function call.
fn dma_end() {
atomic::fence(Ordering::Acquire);
@ -110,7 +107,7 @@ fn dma_end() {
pub fn init(power: POWER, usbd: &USBD) {
let mut once = true;
// wait until the USB has been connected
// wait until the USB cable has been connected
while power.events_usbdetected.read().bits() == 0 {
if once {
log::info!("waiting for USB connection on port J3");
@ -153,18 +150,11 @@ pub fn init(power: POWER, usbd: &USBD) {
w.ep0setup().set_bit();
w.usbreset().set_bit()
});
}
/// Connects the device to the host (enables the D+ line pull-up)
pub fn connect(usbd: &USBD) {
// enable the D+ line pull-up
usbd.usbpullup.write(|w| w.connect().set_bit());
}
/// Disconnects the device from the host (disables the D+ line pull-up)
pub fn disconnect(usbd: &USBD) {
usbd.usbpullup.reset();
}
/// Stalls endpoint 0
pub fn ep0stall(usbd: &USBD) {
usbd.tasks_ep0stall.write(|w| w.tasks_ep0stall().set_bit());
@ -210,13 +200,6 @@ pub fn next_event(usbd: &USBD) -> Option<Event> {
None
}
/// Use this instead of the `todo!` / `unimplemented!` macro
pub fn todo(usbd: &USBD) {
disconnect(usbd);
log::error!("unimplemented");
crate::exit()
}
/// Reads the WLENGTHL and WLENGTHH registers and returns the 16-bit WLENGTH component of a setup packet
pub fn wlength(usbd: &USBD) -> u16 {
u16::from(usbd.wlengthl.read().wlengthl().bits())

View file

@ -158,6 +158,8 @@ fn notmain() -> Result<i32, anyhow::Error> {
core.reset_and_halt()?;
log::info!("reset and halted the core");
eprintln!("flashing program ..");
// load program into memory
for section in &sections {
core.write_32(section.start, &section.data)?;
@ -177,6 +179,8 @@ fn notmain() -> Result<i32, anyhow::Error> {
log::info!("loaded program into RAM");
eprintln!("DONE");
core.run()?;
} else {
// XXX the device may have already loaded SP and PC at this point in this case?
@ -186,10 +190,12 @@ fn notmain() -> Result<i32, anyhow::Error> {
log::info!("flashed program");
eprintln!("DONE");
core.reset()?;
}
// run
eprintln!("resetting device");
let core = Rc::new(core);
let rtt_addr_res = rtt_addr.ok_or_else(|| anyhow!("RTT control block not available"))?;