Merge pull request #155 from ferrous-systems/improve-material

Improve material
This commit is contained in:
Tanks Transfeld 2021-04-23 13:19:27 +02:00 committed by GitHub
commit 4d18924e31
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 68 additions and 46 deletions

View file

@ -43,10 +43,8 @@ fn on_event(_usbd: &USBD, event: Event) {
Event::UsbReset => todo!(), Event::UsbReset => todo!(),
Event::UsbEp0DataDone => todo!(), Event::UsbEp0DataDone => todo!(),
// leave this at it is for now.
Event::UsbEp0Setup => { Event::UsbEp0Setup => todo!(),
log::info!("goal reached; move to the next section");
dk::exit()
}
} }
} }

View file

@ -2,17 +2,22 @@
The next step is to respond to the GET_DESCRIPTOR request with a device descriptor. The next step is to respond to the GET_DESCRIPTOR request with a device descriptor.
✅ Open the file `src/bin/usb-3.rs`. Implement the response to the GET_DESCRIPTOR request. Use the following guide for assistance.
❗️ Keep the cable connected to the J3 port for the rest of the workshop ❗️ Keep the cable connected to the J3 port for the rest of the workshop
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. ✅ Open the file `src/bin/usb-3.rs`.
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. Part of this response is already implemented. We'll go through this.
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 implement responding to a GET_DESCRIPTOR Device request, extend `usb-3.rs` so that it uses `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: We'll use the `dk::usb::Ep0In` abstraction. An instance of it 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`. `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.
✅ Implement the EP0DATADONE event by calling the `end` method of the `EP0In` API.
✅ Implement the response to the GET_DESCRIPTOR request. Extend `usb-3.rs` so that it uses `Ep0In` to respond to the `GET_DESCRIPTOR Device` request (and only to that request).
**Values of the device descriptor**
- `bLength = 18`, the size of the descriptor (must always be this value) - `bLength = 18`, the size of the descriptor (must always be this value)
- `bDescriptorType = 1`, device descriptor type (must always be this value) - `bDescriptorType = 1`, device descriptor type (must always be this value)
@ -25,14 +30,17 @@ To implement responding to a GET_DESCRIPTOR Device request, extend `usb-3.rs` so
- `bNumConfigurations = 1`, must be at least `1` so this is the minimum value - `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. >(\*) the `common` crate refers to the crate in the `advanced/common` folder. It is already part of the `firmware` crate dependencies.
**Use the `usb2::device::Descriptor` abstraction**
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`. 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: the `usb2::device::Descriptor` struct does not have `bLength` and `bDescriptorType` fields. Those fields have fixed values according to the USB spec so you cannot modify or set them. When `bytes()` is called on the `Descriptor` value the returned array, the binary representation of the descriptor, will contain those fields set to their correct value. **The length of the device descriptor**
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 your answer must be 18 bytes long (do *not* pad the response with zeroes). The `usb2::device::Descriptor` struct does not have `bLength` and `bDescriptorType` fields. Those fields have fixed values according to the USB spec so you cannot modify or set them. When `bytes()` is called on the `Descriptor` value the returned array, the binary representation of the descriptor, will contain those fields set to their correct value.
Don't forget to also handle the `EP0DATADONE` event! 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 your answer must be 18 bytes long (do *not* pad the response with zeroes).
**Expected log output**
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): 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):

View file

@ -12,7 +12,7 @@ RTIC makes a clearer distinction between the application's initialization phase,
✅ Build the `rtic-hello` example and look at the generated `rtic-expansion.rs` file. ✅ Build the `rtic-hello` example and look at the generated `rtic-expansion.rs` file.
The generated code should look like this. Note that interrupts are disabled during the execution of the `init` function: You can use `rustfmt` on `target/rtic-expansion.rs` to make the generated code easier to read. Among other things, the file should contain the following lines. Note that interrupts are disabled during the execution of the `init` function:
``` rust ``` rust
fn main() -> ! { fn main() -> ! {

View file

@ -7,24 +7,32 @@ In `usb-2.rs`, you will find a short description of each register above the vari
[nrf product spec]: https://infocenter.nordicsemi.com/pdf/nRF52840_PS_v1.1.pdf [nrf product spec]: https://infocenter.nordicsemi.com/pdf/nRF52840_PS_v1.1.pdf
## Writing a parser for the data of this SETUP stage.
❗️ Keep the cable connected to the J3 port for the rest of the workshop
✅ Parse GET_DESCRIPTOR requests for DEVICE descriptors.
Modify `Request::parse()` in `advanced/common/usb/src/lib.rs` to recognize a GET_DESCRIPTOR request of type DEVICE so that the `get_descriptor_device` test passes. Note that the parser already handles SET_ADDRESS requests.
**Getting Started:**
**1. Writing code that can be tested**
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. This way, you can test it on your development machine (e.g. `x86_64`) using the standard `cargo test` functionality. 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. This way, you can test it on your development machine (e.g. `x86_64`) using the standard `cargo test` functionality.
So that's what we'll do here. In `advanced/common/usb/lib.rs` 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 in VS code. So that's what we'll do here. In `advanced/common/usb/lib.rs` 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 in VS code.
The definition of `Descriptor::Configuration` as well as the associated test has been "commented out" using an `#[cfg(TODO)]` attribute because it is not handled by the firmware yet. Delete the `#[cfg(TODO)]` so that the unit tests can access it. This pattern is used for enum members and test functions throughout this workshop, so keep it in mind should you see it again. The definition of `Descriptor::Configuration` as well as the associated test has been "commented out" using an `#[cfg(TODO)]` attribute because it is not handled by the firmware yet. Delete the `#[cfg(TODO)]` so that the unit tests can access it. This pattern is used for enum members and test functions throughout this workshop, so keep it in mind should you see it again.
✅ Parse the data of this SETUP stage. **2. Description of GET_DESCRIPTOR request**
❗️ Keep the cable connected to the J3 port for the rest of the workshop
Start with the GET_DESCRIPTOR request, which is described in detail in section 9.4.3 of the [USB specification][usb_spec]. All the constants we'll be using are also described in Tables 9-3, 9-4 and 9-5 of the same document.
[usb_spec]: https://www.usb.org/document-library/usb-20-specification
We can recognize a GET_DESCRIPTOR request by the following properties: We can recognize a GET_DESCRIPTOR request by the following properties:
- `bmRequestType` is **0b10000000** - `bmRequestType` is **0b10000000**
- `bRequest` is **6** (i.e. the GET_DESCRIPTOR Request Code, defined in table 9-4 in the USB spec) - `bRequest` is **6** (i.e. the GET_DESCRIPTOR Request Code, defined in table 9-4 in the USB spec)
**3. Description of DEVICE descriptor requests**
In this task, we only want to parse DEVICE descriptor requests. They have the following properties: In this task, we only want to parse DEVICE descriptor requests. They have the following properties:
- the descriptor type is **1** (i.e. DEVICE, defined in table 9-5 of the USB spec) - the descriptor type is **1** (i.e. DEVICE, defined in table 9-5 of the USB spec)
@ -32,29 +40,33 @@ In this task, we only want to parse DEVICE descriptor requests. They have the fo
- the wIndex is **0** for our purposes - the wIndex is **0** for our purposes
- ❗you need to fetch the descriptor type from the high byte of `wValue`, and the descriptor index from the the low byte of `wValue` - ❗you need to fetch the descriptor type from the high byte of `wValue`, and the descriptor index from the the low byte of `wValue`
Check section 9.4.3 of the [USB specification][usb_spec] for a very detailed description of the requests. All the constants we'll be using are also described in Tables 9-3, 9-4 and 9-5 of the same document.
**4. Remember that you can define binary literals by prefixing them with `0b`.**
**5. You can use bit shifts (`>>`) and casts (`as u8`) to get the high/low bytes of `wValue`.**
**6. Return `Err` if properties aren't met.**
You will also find this information in the `// TODO implement ...` comment in the `Request::parse()` function of `lib.rs` file. You will also find this information in the `// TODO implement ...` comment in the `Request::parse()` function of `lib.rs` file.
> NOTE: If you'd like to learn more, take a look at Section 9.4.3 Get Descriptor of the USB specification. > NOTE: If you'd like to learn more, take a look at Section 9.4.3 Get Descriptor of the USB specification.
To complete the task, proceed like this:
1. **Parse GET_DESCRIPTOR requests for DEVICE descriptors:**
Modify `Request::parse()` in `advanced/common/usb/src/lib.rs` to recognize a GET_DESCRIPTOR request of type DEVICE so that the `get_descriptor_device` test passes. Note that the parser already handles SET_ADDRESS requests.
- remember the GET_DESCRIPTOR fields described at the start of this section
- remember that you can define binary literals by prefixing them with `0b`
- you can use bit shifts (`>>`) and casts (`as u8`) to get the high/low bytes of `wValue`
See `advanced/common/usb/solution-get-descriptor-device.rs` for a solution. See `advanced/common/usb/solution-get-descriptor-device.rs` for a solution.
2. **Read incoming request information and pass it to the parser:** ✅ Read incoming request information and pass it to the parser:
modify `usb-2.rs` to read `USBD` registers and parse the SETUP data when an EPSETUP event is received.
modify `usb-2.rs` to read `USBD` registers and parse the SETUP data when an EP0SETUP event is received.
**Getting Started:**
- for a mapping of register names to the `USBD` API, check the entry for `nrf52840_hal::target::usbd` in the documentation you've created using `cargo doc` - for a mapping of register names to the `USBD` API, check the entry for `nrf52840_hal::target::usbd` in the documentation you've created using `cargo doc`
- remember that we've learned how to read registers in `events.rs` - let bmrequesttype = usbd.bmrequesttype.read().bits() as u8;
- remember that we've learned how to read registers in `events.rs`.
- you will need to put together the higher and lower bits of `wlength`, `windex` and `wvalue` to get the whole field - you will need to put together the higher and lower bits of `wlength`, `windex` and `wvalue` to get the whole field
- > Note: If you're using a Mac, you need to catch `SetAddress` requests returned by the parser as these are sent before the first GetDescriptor request. You can handle them by doing nothing. - > Note: If you're using a Mac, you need to catch `SetAddress` requests returned by the parser as these are sent before the first GetDescriptor request. You can handle them by doing nothing.
3. when you have successfully received a GET_DESCRIPTOR request for a Device descriptor you are done. You should see an output like this: 1. when you have successfully received a GET_DESCRIPTOR request for a Device descriptor you are done. You should see an output like this:
``` console ``` console
INFO:usb_2 -- USB: UsbReset @ 438.842772ms INFO:usb_2 -- USB: UsbReset @ 438.842772ms
@ -67,5 +79,7 @@ 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. `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 step 1. in `advanced/common/usb/solution-get-descriptor-device.rs`.
You can find a solution to step 2. in `advanced/firmware/src/bin/usb-2-solution.rs`. You can find a solution to this step in `advanced/firmware/src/bin/usb-2-solution.rs`.
[usb_spec]: https://www.usb.org/document-library/usb-20-specification

View file

@ -10,22 +10,24 @@ In this starter code the USBD peripheral is initialized in `init` and a task, na
❗️ Keep the cable connected to the J3 port for the rest of the workshop ❗️ Keep the cable connected to the J3 port for the rest of the workshop
Go to `fn on_event`, line 39. In this section you'll need to implement the following USB events until you reach the EP0SETUP event: This code will panic because `USBRESET` is not implemented yet.
- `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. Since we are currently not dealing with any other state, you can handle this state by doing nothing. ✅ Go to `fn on_event`, line 39. In this section you'll need to implement the following USB events `USBRESET` and `EP0SETUP` so that your log output will look like this:
- `EP0SETUP`. The USBD peripheral has detected the SETUP stage of a control transfer. If you get to this point move to the next section.
- `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 ``` console
(..) INFO:usb_1 -- USB: UsbReset
INFO:usb_1 -- returning to the Default state
INFO:usb_1 -- USB: UsbEp0Setup INFO:usb_1 -- USB: UsbEp0Setup
INFO:usb_1 -- goal reached; move to the next section INFO:usb_1 -- goal reached; move to the next section
INFO:dk -- `dk::exit() called; exiting ...
``` ```
Do not overthink this exercise; it is not a trick question. There is very little to do and no new functionality to add. ## Help
- `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. Since we are currently not dealing with any other state, you can handle this state by adding a log statement to provide information that this event occurred.
- `EP0DATADONE`. The USBD peripheral is signaling the end of the DATA stage of a control transfer. Since you won't encounter this event just yet, you can leave it as it is.
- `EP0SETUP`. The USBD peripheral has detected the SETUP stage of a control transfer. Add a log statement containing "goal reached; move to the next section" and exit the application.
You can find the solution in the `usb-1-solution.rs` file. You can find the solution in the `usb-1-solution.rs` file.