(L6.1) Embedded Pong: Async Rust on nRF52840-DK

This project demonstrates an embedded Pong game implementation on the nRF52840 Development Kit using Rust's async programming capabilities. The implementation leverages the Embassy framework, Rust's type system, and modern embedded libraries to create a functional game on a microcontroller, showcasing the potential of async Rust in embedded systems development.

System Architecture

The system is built around several key technological components. At its core is the Embassy framework, which provides an async runtime specifically designed for embedded systems. This allows for more efficient and readable concurrent programming compared to traditional bare-metal approaches. The primary hardware components include a ST7735S 1.77" SPI display for rendering game graphics and the nRF52840's integrated buttons for user input.

Rust's type system plays a crucial role in the implementation, providing compile-time guarantees of memory safety and preventing common embedded programming pitfalls. Unlike traditional C-based embedded development, Rust ensures memory safety without runtime overhead, making it particularly attractive for resource-constrained environments.

Execution Model

One of the most sophisticated aspects of the implementation is its multi-priority executor design. The system utilizes two distinct executors:

  1. High-Priority Executor: Dedicated to time-critical tasks such as input handling and interrupt management.
  2. Low-Priority Executor: Manages background operations and less time-sensitive tasks.

This hierarchical approach allows for fine-grained control over task scheduling. The SWI0_EGU0 interrupt is configured to enable the executor to wake from sleep states, striking a balance between system responsiveness and power efficiency.

Input Handling

Input management demonstrates the power of Embassy's async primitives. The input handler uses the select4 method to concurrently monitor four buttons on the nRF52840-DK. This approach provides non-blocking input detection, crucial for responsive game interactions.

#![allow(unused)]
fn main() {
#[embassy_executor::task]
async fn input_handler(btns: (AnyPin, AnyPin, AnyPin, AnyPin)) {
    let mut btn1 = Input::new(btns.0, Pull::Up);
    let mut btn2 = Input::new(btns.1, Pull::Up);
    let mut btn3 = Input::new(btns.2, Pull::Up);
    let mut btn4 = Input::new(btns.3, Pull::Up);

    loop {
        select4(
            btn1.wait_for_any_edge(),
            btn2.wait_for_any_edge(),
            btn3.wait_for_any_edge(),
            btn4.wait_for_any_edge(),
        )
        .await;

        // Input processing logic
        info!(
            "Detected input change 1: {}, 2: {}, 3: {}, 4: {}",
            btn1.is_low(),
            btn2.is_low(),
            btn3.is_low(),
            btn4.is_low()
        );
    }
}
}

Display Rendering

Graphics rendering leverages the embedded-graphics library, providing a high-level abstraction for pixel manipulation. The implementation uses a frame buffer approach, allowing for efficient graphics preparation before transferring data to the ST7735S display via asynchronous SPI communication.

The display task demonstrates several advanced Rust embedded programming techniques:

  • The display task demonstrates several advanced Rust embedded programming techniques:
  • Mutex for thread-safe shared state
  • Asynchronous SPI communication
  • Efficient memory management
#![allow(unused)]
fn main() {
#[embassy_executor::task]
async fn async_driver(back_buffer: FrameBufferRef, static_driver: DisplayRef, signal: SignalRef) {
    loop {
        if !signal.signaled() {
            signal.wait().await;
        }

        {
            let fb = back_buffer.lock().await;
            let drawable_area = fb.size();
            unwrap!(
                static_driver
                    .set_pixels_buffered(
                        0,
                        0,
                        drawable_area.width as u16 - 1,
                        drawable_area.height as u16 - 1,
                        fb.as_ref().iter().map(|&c| RawU16::from(c).into_inner()),
                    )
                    .await
            );
        }

        signal.reset();
        yield_now().await;
    }
}
}

Technical Challenges and Solutions

Several technical challenges were addressed during the implementation:

  1. Concurrency Management: Embassy's async runtime allows for efficient task switching without traditional threading overhead
  2. Memory Safety: Rust's ownership model and borrowing rules prevent common memory-related bugs.
  3. Low-Level Hardware Interaction: Trait-based abstractions in Embassy and embedded-hal provide clean interfaces for hardware communication.

Performance Considerations

The implementation prioritizes both performance and power efficiency. The use of async tasks allows for:

  • Minimal context switching overhead
  • Efficient use of microcontroller resources
  • Responsive input handling
  • Low-power operation through intelligent task scheduling

Conclusion

This project demonstrates the viability of async Rust in embedded game development. By combining the Embassy framework with Rust's type system and zero-cost abstractions, we created a robust, efficient, and readable embedded application. The implementation serves as a practical example of how modern programming techniques can be applied to resource-constrained systems.