Home Logo

Home Logo



Rust in Embedded Systems - Memory Game

August 21, 2024

Quick Overview

For the last part of this series, I’ll present you with a small Memory Game that I had to program using all the information that we have accumulated so far. From GPIO, PWM, all the way to writing some text on the LCD using SPI. Let’s get right into the challenge.

Challenge

Memory game

Write a memory game using the EEPROM module, two LEDs (one green and one red), and the LCD display:

The program must display 4 random letters, that can be either A, B, X or Y (corresponding to the buttons on the Pico Explorer Base); each letter will be displayed for 500ms. After they are displayed, the user must recreate the sequence in the exact order (the sequence can have repeating letters), by pressing the buttons on the Explorer Base.

If the sequence is inserted correctly, the green LED should be turned on, else, the red LED should turn on to indicate a failure and should reset the score to 0 (the score represents the number of consecutive rounds played without failure).

The game should keep track of the highest score by storing it at address 0x00 in the EEPROM (we assume that its representation doesn’t exceed a byte). If the score of a player is higher than the stored value, the highest score will be updated in the memory.

We start off as usual, by specifying we won’t use neither the standard main function as our entrypoint nor the standard output. Also, I’ll be adding all the imports of the functions and crates used in here as well.

#![no_main]
#![no_std]

use core::fmt::Write;
use cyw43::new;
use embassy_executor::Spawner;
use defmt::info;
use embassy_futures::select::{select4, Either4};
use embassy_time::Timer;
use {defmt_rtt as _, panic_probe as _};
use ipw_embedded::display;
use embedded_graphics::{pixelcolor::Rgb565, text::Text};
use embedded_graphics::prelude::Point;
use embedded_graphics::mono_font::ascii::FONT_7X13_BOLD;
use embedded_graphics::mono_font::MonoTextStyle;
use embedded_graphics::Drawable;
use embassy_rp::{bind_interrupts, i2c::{Config as I2cConfig, I2c, InterruptHandler as I2CInterruptHandler}};
use embedded_hal_async::i2c::{Error, I2c as _};
use embassy_rp::peripherals::I2C0;
use embassy_rp::gpio::{Level, Pull, Output, Input};
use rand::{Rng, RngCore};
use embassy_rp::clocks::RoscRng;

Then, I pasted the code for the SPI controller for our LCD from the last part, plus the introduction of the asynchronous main task.

#[embassy_executor::main]
async fn main(_spawner: Spawner) {

    // SPI Controller Setup

    let peripherals = embassy_rp::init(Default::default());
    let miso = peripherals.PIN_4;
    let display_cs = peripherals.PIN_17;
    let mosi = peripherals.PIN_19;
    let clk = peripherals.PIN_18;
    let rst = peripherals.PIN_0;
    let dc = peripherals.PIN_16;
    let mut display_config = embassy_rp::spi::Config::default();
    display_config.frequency = 64_000_000;
    display_config.phase = embassy_rp::spi::Phase::CaptureOnSecondTransition;
    display_config.polarity = embassy_rp::spi::Polarity::IdleHigh;

    // Init SPI
    let spi: embassy_rp::spi::Spi<'_, _, embassy_rp::spi::Blocking> =
        embassy_rp::spi::Spi::new_blocking(
            peripherals.SPI0,
            clk,
            mosi,
            miso,
            display_config.clone(),
        );
    let spi_bus: embassy_sync::blocking_mutex::Mutex<
        embassy_sync::blocking_mutex::raw::NoopRawMutex,
        _,
    > = embassy_sync::blocking_mutex::Mutex::new(core::cell::RefCell::new(spi));

    let display_spi = embassy_embedded_hal::shared_bus::blocking::spi::SpiDeviceWithConfig::new(
        &spi_bus,
        embassy_rp::gpio::Output::new(display_cs, embassy_rp::gpio::Level::High),
        display_config,
    );

    let dc = embassy_rp::gpio::Output::new(dc, embassy_rp::gpio::Level::Low);
    let rst = embassy_rp::gpio::Output::new(rst, embassy_rp::gpio::Level::Low);
    let di = display::SPIDeviceInterface::new(display_spi, dc);

    // Init ST7789 LCD
    let mut display = st7789::ST7789::new(di, rst, 240, 240);
    display.init(&mut embassy_time::Delay).unwrap();
    display
        .set_orientation(st7789::Orientation::Portrait)
        .unwrap();
    use embedded_graphics::draw_target::DrawTarget;
    display.clear(embedded_graphics::pixelcolor::RgbColor::BLACK).unwrap();

After that, I continued with the I2C part of the code, defining the PINs for the SDA and SCL. Then, I specified the default I2C configuration and created the variables that store the device address, the highest score address, and last but not least, the highest score itself. Finally, I used write_read to specify where to read the highest score from.

// I2C
    
    let sda = peripherals.PIN_20; // PIN_20 is the SDA pin
    let scl = peripherals.PIN_21; // PIN_21 is the SCL pin

    let mut bus = I2c::new_async(peripherals.I2C0, scl, sda, Irqs, I2cConfig::default()); // Default I2C configuration
    
    const TARGET_ID: u8 = 0x50u8; // I2C address of the target device
    const HIGHEST_SCORE_ADDR: [u8; 2] = [0x00u8; 2]; 
    let mut highest_score: u8 = 0;

    Timer::after_secs(2).await;
 
    bus.write_read(TARGET_ID, &HIGHEST_SCORE_ADDR, &mut [highest_score]).await.unwrap();

    let mut current_score: u8 = 0;

Before diving into the important part of the code, we also have to define the PINs for each of the buttons, plus the two LEDs.

// GPIO
    let mut button_a = Input::new(peripherals.PIN_12, Pull::Up);
    let mut button_b = Input::new(peripherals.PIN_13, Pull::Up);
    let mut button_x = Input::new(peripherals.PIN_14, Pull::Up);
    let mut button_y = Input::new(peripherals.PIN_15, Pull::Up);

    let mut pin_gren = Output::new(peripherals.PIN_6, Level::Low);
    let mut pin_red = Output::new(peripherals.PIN_1, Level::Low);

Ok, now for the key part, we are first spinning it off with generating the random values that are going to be printed out on the display.

Also, remember that from this point on, all the code needs to placed in the loop, as for MCUs that don’t run any operating systems like in our case, they don’t return anything.

loop {
        // Generate random sequence
        let mut sequence: [u8; 4] = [0; 4];
        let mut rng = RoscRng;
        for i in 0..4 {
            sequence[i] = rng.gen_range(0..4);
        }

Next, we’ll print the sequence on the display and define what to match each of the numbers to.

let color_yellow = Rgb565::new(255, 255, 0);
        let color_black = Rgb565::new(0, 0, 0);

        // Display the sequence
        let style = MonoTextStyle::new(&FONT_7X13_BOLD, color_yellow);
        for &s in &sequence {
            let letter = match s {
                0 => "A",
                1 => "B",
                2 => "X",
                _ => "Y",
            };
            Text::new(letter, Point::new(120, 120), style).draw(&mut display).unwrap();
            Timer::after_secs(1).await;
            display.clear(color_black).unwrap();
            Timer::after_secs(1).await;
        }

Now, we’ll specify the user input and match each of the buttons to the previously defined numbers. Plus, I added the info for making it easier to debug when we flash our code onto the board.

// User input
        let mut user_input: [u8; 4] = [0; 4];
        for i in 0..4 {
            let button = select4(button_a.wait_for_falling_edge(), button_b.wait_for_falling_edge(), button_x.wait_for_falling_edge(), button_y.wait_for_falling_edge()).await;
            match button {
                Either4::First(_) => user_input[i] = 0,
                Either4::Second(_) => user_input[i] = 1,
                Either4::Third(_) => user_input[i] = 2,
                Either4::Fourth(_) => user_input[i] = 3,
            }
                info!("Selected value: {}", user_input[i]);
            Timer::after_secs(1).await;
        }

For our last part, we’ll add the checker for the sequence and we’ll write to the EEPROM the highest score if the user input matches the sequence. Also, we’ll light the green LED if it’s correct.

// Check if user input matches the sequence
        if user_input == sequence {
            current_score += 1;
            if current_score > highest_score {
                highest_score = current_score;
                let buf: [u8; 3] = [HIGHEST_SCORE_ADDR[0], HIGHEST_SCORE_ADDR[1], highest_score];
                bus.write(TARGET_ID, &buf).await.unwrap();
            }
            pin_gren.set_high();
            Timer::after_secs(1).await;
            pin_gren.set_low();
        } else {
            pin_red.set_high();
            Timer::after_secs(1).await;
            pin_red.set_low();
            current_score = 0;
        }
        Timer::after_secs(1).await;

Now, a little surprise, I also added a small piece of code at the end of each round that mentions our current and highest score.

Because MCUs don’t have heap, we’ll have use the library heapless that just basically writes the string we want to print onto the stack.

let mut res: heapless::String<100> = heapless::String::new();
        write!(&mut res, "Current score: {}\nHighest score: {}", current_score, highest_score).unwrap();

        Text::new(&res, Point::new(20, 120), style).draw(&mut display).unwrap();
        Timer::after_secs(3).await;
        display.clear(color_black).unwrap();
        Timer::after_secs(1).await;
    }
}

I really hope you enjoyed this small series of tutorials, cause I sure did. Feel free to connect with me if you got any questions. See you in the next post! 👋