/*
***PRESS GREEN "PLAY" BUTTON IN UPPER LEFT CORNER TO BEGIN SIMULATION***
"Simon" memory game.
Embedded software project by Andrew Adams, January 2024.
Uses FreeRTOS API to illustrate such embedded software concepts as tasks, interrupts, timers, and semaphores.
*/
#include <vector>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_random.h"
#include "driver/gpio.h"
#include "freertos/semphr.h"
using namespace std;
#define LED_RED GPIO_NUM_27
#define LED_YELLOW GPIO_NUM_18
#define LED_GREEN GPIO_NUM_14
#define LED_BLUE GPIO_NUM_5
#define PSH_BTN_GREEN GPIO_NUM_13
#define PSH_BTN_YELLOW GPIO_NUM_4
#define PSH_BTN_BLUE GPIO_NUM_2
#define PSH_BTN_RED GPIO_NUM_12
#define PSH_BTN_START GPIO_NUM_21
#define SPEAKER GPIO_NUM_19
#define LED_RED_ID 0
#define LED_YELLOW_ID 1
#define LED_GREEN_ID 2
#define LED_BLUE_ID 3
#define NOTE_C 4186
#define NOTE_D 4699
#define NOTE_E 5274
#define NOTE_F 5588
static const gpio_num_t led_id_lookup[4] = {LED_RED, LED_YELLOW, LED_GREEN, LED_BLUE}; // Associate LEDS with numeric ID
static const int btn_tones[4] = {NOTE_C, NOTE_D, NOTE_E, NOTE_F}; // Tones to use with lights
static vector<uint8_t> blink_seq; // Use vector to store growing sequence of blinks
// Set blink time and post-blink interval (in milliseconds)
static const TickType_t blink_time = 300;
static const TickType_t blink_interval = 100;
// Task handles
TaskHandle_t game_turn_task_handle = NULL;
TaskHandle_t player_turn_task_handle = NULL;
TaskHandle_t blink_seq_task_handle = NULL;
// Timer handle for player turn
TimerHandle_t player_turn_timer_handle = NULL;
// Semaphore handle for button ISRs
SemaphoreHandle_t btnSemaphore = NULL;
// Flags
static bool game_over = true;
static volatile bool red_press = false;
static volatile bool yellow_press = false;
static volatile bool green_press = false;
static volatile bool blue_press = false;
static volatile bool start_press = false;
static bool player_turn = false;
/** TASK game_turn_task: Adds to LED/tone blink sequence with each turn,
then passes control to task which blinks the current sequence **/
void game_turn_task(void *pvParameter)
{
uint8_t next_led = NULL;
// Wait for blink task to be initialized
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
// Generate random number to determine next light in sequence
while(!game_over) {
next_led = (uint8_t) (esp_random() % 4);
blink_seq.push_back(next_led);
// Pass game control to blink_seq_task via notification
xTaskNotifyGive(blink_seq_task_handle);
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
}
vTaskDelete(NULL);
}
/** TASK player_turn_task: Creates timer for player attempt, and waits for player move;
checks player move, ending game if incorrect **/
void player_turn_task(void *pvParameter)
{
uint8_t move_index; // Indexes which move of sequence player is on
// Create timer for player move timeout
player_turn_timer_handle = xTimerCreate(
"player_turn_timer",
2000 / portTICK_PERIOD_MS,
pdFALSE,
(void *) 0,
player_turn_time_out
);
while(!game_over){
// Wait to be passed game control after current sequence is blinked
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
// Initiate player turn
player_turn = true;
move_index = 0;
xTimerStart(player_turn_timer_handle, 0);
// Wait for player move.
// If correct move, reset move timer; if incorrect, end game
while(player_turn){
btnSemaphore = xSemaphoreCreateBinary();
xSemaphoreTake(btnSemaphore, portMAX_DELAY);
if(red_press) {
if(blink_seq[move_index] == LED_RED_ID) {
correct_move(LED_RED_ID, move_index);
} else {
end_game();
}
red_press = false;
} else if(yellow_press) {
if(blink_seq[move_index] == LED_YELLOW_ID) {
correct_move(LED_YELLOW_ID, move_index);
} else {
end_game();
}
yellow_press = false;
} else if(green_press) {
if(blink_seq[move_index] == LED_GREEN_ID) {
correct_move(LED_GREEN_ID, move_index);
} else {
end_game();
}
green_press = false;
} else if(blue_press) {
if(blink_seq[move_index] == LED_BLUE_ID) {
correct_move(LED_BLUE_ID, move_index);
} else {
end_game();
}
blue_press = false;
}
}
// Pause 500 milliseconds, then return control to game_turn_task
vTaskDelay(500 / portTICK_PERIOD_MS);
xTaskNotifyGive(game_turn_task_handle);
};
vTaskDelete(NULL);
}
/** TASK blink_seq_task: Blinks the LED/tone sequence for current round **/
void blink_seq_task(void* pvParameter)
{
// Blink_seq_task is initialized, so unblock game_turn_task
xTaskNotifyGive(game_turn_task_handle);
// Wait to receive game control, then blink sequence
while(!game_over){
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
for(uint8_t i: blink_seq){
gpio_num_t led_gpio = led_id_lookup[i];
gpio_set_level(led_gpio, 1);
tone(SPEAKER, btn_tones[i]);
vTaskDelay(blink_time / portTICK_PERIOD_MS);
gpio_set_level(led_gpio, 0);
noTone(SPEAKER);
vTaskDelay(blink_interval / portTICK_PERIOD_MS);
}
// Pass game control to player_turn_task
xTaskNotifyGive(player_turn_task_handle);
}
vTaskDelete(NULL);
}
void correct_move(uint8_t led_id, uint8_t &move_index)
{
// Blink corresponding LED and tone
gpio_num_t led_gpio = led_id_lookup[led_id];
gpio_set_level(led_gpio, 1);
tone(SPEAKER, btn_tones[led_id]);
vTaskDelay(blink_time / portTICK_PERIOD_MS);
gpio_set_level(led_gpio, 0);
noTone(SPEAKER);
// If last player move is not the end of current sequence, reset timer and increment move_index;
// if move is last in sequence, end player turn by stopping timer and resetting move_index
if(move_index != (blink_seq.size()-1)) {
xTimerReset(player_turn_timer_handle, 0);
move_index++;
} else {
player_turn = false;
xTimerStop(player_turn_timer_handle, 0);
move_index = 0;
}
}
// Ends game on incorrect move or timeout; resets game, and flashes LED/tone signal for end of game
void end_game()
{
blink_seq.clear();
player_turn = false;
game_over = true;
xTimerStop(player_turn_timer_handle, 0);
// Flash all LEDs and play low tone to signal game loss
gpio_set_level(LED_RED, 1);
gpio_set_level(LED_YELLOW, 1);
gpio_set_level(LED_GREEN, 1);
gpio_set_level(LED_BLUE, 1);
tone(SPEAKER, 1500);
vTaskDelay(500 / portTICK_PERIOD_MS);
gpio_set_level(LED_RED, 0);
gpio_set_level(LED_YELLOW, 0);
gpio_set_level(LED_GREEN, 0);
gpio_set_level(LED_BLUE, 0);
noTone(SPEAKER);
}
/** Create callback function for player turn timer, and ISRs for button presses **/
void player_turn_time_out(TimerHandle_t xTimer) {
end_game();
}
void IRAM_ATTR red_btn_ISR(){
if(player_turn) {
red_press = true;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(btnSemaphore, &xHigherPriorityTaskWoken);
}
}
void IRAM_ATTR yellow_btn_ISR(){
if(player_turn) {
yellow_press = true;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(btnSemaphore, &xHigherPriorityTaskWoken);
}
}
void IRAM_ATTR green_btn_ISR(){
if(player_turn) {
green_press = true;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(btnSemaphore, &xHigherPriorityTaskWoken);
}
}
void IRAM_ATTR blue_btn_ISR(){
if(player_turn) {
blue_press = true;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(btnSemaphore, &xHigherPriorityTaskWoken);
}
}
void IRAM_ATTR start_btn_ISR(){
if(game_over) {
start_press = true;
}
}
// SETUP
void setup(){
pinMode(LED_RED, OUTPUT);
pinMode(LED_YELLOW, OUTPUT);
pinMode(LED_BLUE, OUTPUT);
pinMode(LED_GREEN, OUTPUT);
ledcSetup(0, 1000, 8);
pinMode(SPEAKER, OUTPUT);
pinMode(PSH_BTN_RED, INPUT_PULLUP);
pinMode(PSH_BTN_YELLOW, INPUT_PULLUP);
pinMode(PSH_BTN_GREEN, INPUT_PULLUP);
pinMode(PSH_BTN_BLUE, INPUT_PULLUP);
pinMode(PSH_BTN_START, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(PSH_BTN_RED), red_btn_ISR, HIGH);
attachInterrupt(digitalPinToInterrupt(PSH_BTN_YELLOW), yellow_btn_ISR, HIGH);
attachInterrupt(digitalPinToInterrupt(PSH_BTN_GREEN), green_btn_ISR, HIGH);
attachInterrupt(digitalPinToInterrupt(PSH_BTN_BLUE), blue_btn_ISR, HIGH);
attachInterrupt(digitalPinToInterrupt(PSH_BTN_START), start_btn_ISR, HIGH);
}
// GAME LOOP
void loop(){
// While in "game over" state, watch for start button press; on press, set flags and create game tasks
if(game_over){
if(start_press){
game_over = false;
start_press = false;
vTaskDelay(300 / portTICK_PERIOD_MS);
xTaskCreate(
&game_turn_task,
"game_turn_task",
2048,
NULL,
5,
&game_turn_task_handle
);
xTaskCreate(
&blink_seq_task,
"blink_seq_task",
2048,
NULL,
5,
&blink_seq_task_handle
);
xTaskCreate(
&player_turn_task,
"player_turn_task",
2048,
NULL,
5,
&player_turn_task_handle
);
}
}
}