Ferrous Systems Embedded Rust Workshops

repository·main·Indexed 42 days ago

https://github.com/ferrous-systems/embedded-trainings-2020

Source material, code samples, and exercises for beginner and advanced Embedded Rust workshops. Includes support for nRF52840 Development Kit (DK) and Dongle, RTIC framework implementation for resource management and interrupt handling, and USB request parsing.

Tokens
49.9K
Snippets
147
Records
230
Agent score
53%

What's inside Embedded Rust Trainings

  1. What is a USB interface and how does it relate to endpoints?

    main

    In the USB hierarchy, an interface represents a specific function of a device (e.g., a HID interface for a mouse or a CDC ACM interface for a virtual serial port).

    Key relationships:

    • Configurations contain one or more interfaces.
    • Interfaces are composed of one or more endpoints.
    • Endpoints are the actual communication channels. A single endpoint (except for the special endpoint 0) cannot be shared by more than one interface.
    • Endpoint 0 is a special control endpoint that is typically shared by all interfaces within a configuration.

    Example: A device like the nRF52840 Dongle might expose multiple interfaces within a single configuration: one for logging (CDC ACM) and another for remote procedure calls (HID).

  2. How USB control transfers work

    main

    A control transfer is a specific type of data transfer used by a USB host to issue requests via the control pipe. Every control transfer consists of three distinct stages:

    1. SETUP stage: The host sends exactly 8 bytes of data to identify the specific control request.
    2. DATA stage (optional): Depending on the request, data is transferred either from the device to the host (IN) or from the host to the device (OUT).
    3. STATUS stage: The device acknowledges whether the entire control request was successful or not.

    For full technical details, refer to section 5.5 (Control Transfers) of the USB 2.0 specification.

  3. Understand and use task handlers for interrupt events

    main

    In this framework, tasks are functions marked with the #[task] attribute that are bound to specific hardware interrupt signals. When the hardware raises the interrupt signal, the task is executed in a function-call style.

    Key behaviors:

    • Prioritization: Tasks are prioritized over the idle function. If the idle function is running when an interrupt occurs, its execution is paused (interrupted) to run the task. Once the task returns, the idle function resumes.
    • Execution Context: Tasks run as soon as the interrupt signal is raised by the hardware.

    To test event handling, you can run the events application and connect a micro-USB cable to the DK (port J3). This should trigger a POWER_CLOCK interrupt, resulting in a "POWER event occurred" message.

    // Example of a task bound to an interrupt
    #[task]
    fn on_power_event() {
        // This code runs when the POWER_CLOCK interrupt is raised
        // It will interrupt the idle function
    }
  4. Identify microcontroller requirements for Rust embedded projects

    main

    To start an embedded Rust project, you must identify two key pieces of information from your microcontroller's datasheet:

    1. Processor Architecture and Sub-architecture: This determines your compilation target. Use rustup target list to see available targets. Targets follow the $ARCHITECTURE-$VENDOR-$OS-$ABI format. For bare-metal microcontrollers, the $OS is typically none. If the $ABI ends in hf, it uses the hardfloat ABI (required if using an FPU).

    2. Memory Layout: You need the exact address range and size for Flash and RAM.

    Example for nRF52840:

    • Flash: 1 MB, range 0x0000_0000 - 0x0010_0000.
    • RAM: 256 KB, range 0x2000_0000 - 0x2004_0000.
  5. Handle USB Events in USBD peripheral

    main

    The nRF52840 USBD peripheral uses EVENTS registers to signal reasons for entering the USBD event handler. To complete the USB enumeration process, your application must check these registers and respond to the specific events.

    In the provided starter code (firmware/src/bin/usb-1.rs), the main task is bound to the USBD interrupt signal. It uses usbd::next_event() to poll the event registers. If an event has occurred, next_event() returns Some(Event), where Event is an enum representing the specific USB event. This event is then passed to the on_event function for processing.

    // Conceptual flow in firmware/src/bin/usb-1.rs
    // The main task is bound to the USBD interrupt
    // It calls usbd::next_event() to retrieve the event
    
    if let Some(event) = usbd::next_event() {
        on_event(event);
    }
  6. Use Endpoint 0 for the Default Control Pipe

    main

    Every USB device is required to implement Endpoint 0 as its default control endpoint.

    Endpoint 0 consists of a pair of endpoints: endpoint 0 IN and endpoint 0 OUT. This pair establishes a control pipe, which is a mandatory, bidirectional communication channel between the host and the device. Data exchanged over this pipe must follow a predefined format. The control pipe over endpoint 0 must always be present and active for the device to function correctly.

  7. Understand the RTIC application lifecycle and DSL

    main

    RTIC (Real-Time Interrupt-driven Concurrency) uses a Domain Specific Language (DSL) implemented via Rust procedural macros to manage evented, time-sensitive applications.

    Key lifecycle phases:

    • Initialization (#[init]): This phase runs with interrupts disabled. It is used to set up the system state and resources.
    • Main Loop (#[idle]): This function runs after initialization is complete. Interrupts are re-enabled before #[idle] is executed.

    The #[rtic::app] macro generates the underlying boilerplate required to manage these phases and the interrupt vector table.

  8. Understand the USB configuration descriptor hierarchy

    main

    A USB configuration descriptor defines a specific device configuration to the host. The hierarchy follows a nested structure:

    1. Device: The complete USB device.
    2. Configuration: A device can have one or more configurations (bNumConfigurations). Each configuration contains a set of interfaces.
    3. Interface: A configuration contains one or more interfaces (bNumInterfaces).
    4. Endpoints: Each interface contains one or more endpoints (bNumEndpoints), including control endpoints and data endpoints (IN/OUT).
  9. Understand the code organization in the advanced folder

    main

    The advanced directory is organized into three distinct Cargo workspaces to manage different compilation targets and environments:

    1. host workspace: Contains code intended to run on the host machine (natively compiled).
    2. firmware workspace: Contains code intended to run on the nRF52840 SoC (cross-compiled for ARM Cortex-M architecture).
    3. common workspace: Contains no_std code that is shared and can be used as a dependency by both the host and firmware workspaces.

    This separation allows each workspace to use different compilation profiles tailored to its specific target architecture.

    $ cd advanced
    
    $ tree -L 1 .
    .
    ├── common
    ├── firmware
    ├── host
    └── README.md
  10. How stack overflow protection works with flip-link

    main

    Without stack protection, exceeding stack boundaries leads to undefined behavior that may manifest as erratic or irreproducible bugs.

    When flip-link is enabled, it reorders the memory layout so that the stack grows into an unmapped region. If the stack overflows, the hardware triggers a fault, causing the program to crash immediately. This makes the error easy to detect and debug.

    If you attempt to run code that overflows the stack without flip-link enabled, the behavior will be unpredictable and may change depending on your rustc version or timing.

  11. Understand the Dongle's Radio Puzzle response protocol

    main

    The Dongle's behavior changes based on the size of the radio packet it receives. All responses are valid UTF-8, so you can use str::from_utf8 to process them.

    Incoming Packet SizeDongle Response Behavior
    Zero-sized (b"")Responds with the encrypted ASCII string (substitution cipher).
    One-byte (b"[char]")Responds with the direct mapping from the plaintext u8 value to the ciphertext u8 value.
    Any other lengthResponds with the string correct if the packet matches the decrypted string, otherwise responds with incorrect.