Общие сведения
"Реактор" - это игра на скорость реакции сделанная на основе Arduino UNO/Piranha UNO с использованием библиотеки Arduino FreeRTOS. Цель игры - набрать 9 очков быстрее противника. Очки можно заработать нажав на кнопку, соответствующую цвету горящему на Trema-модуле NeoPixel.
Видео
Редактируется...
Нам понадобится
Модули
- 1x Arduino/Piranha UNO
- 1x Trema Shield
- 3x Trema модуль - Матрица 8x8 Flash-I2C
- 3x Кнопка аркадная 30мм, красная
- 3x Кнопка аркадная 30мм, синяя
- 3x Кнопка аркадная 30мм, белая
- 3x Плоский громкоговоритель
- 1x i2c hub
- 1x Trema-модуль Зуммер
- 1x Trema-модуль NeoPixel
Библиотеки
- iarduino_I2C_Matrix_8x8
- iarduino_NeoPixel
- Arduino_FreeRTOS - устанавливается из менеджера библиотек Arduino IDE. Подробнее...
Корпус
Подключение
Для удобства подключения мы воспользуемся Trema Shield для Arduino.
Устанавливаем Trema Shield на Piranha UNO

Подключаем модули
NeoPixel
| Вывод модуля | Вывод Arduino |
|---|---|
| IN S | 10 |
| IN V | Vcc |
| IN G | GND |
Зуммер
| Вывод модуля | Вывод Arduino |
|---|---|
| S | 11 |
| V | Vcc |
| G | GND |

Подключаем кнопки
рекомендуем припаять конденсатор номиналом 1uF на на выводы каждой кнопки
| Игрок | Цвет | Вывод Arduino |
|---|---|---|
| 1 | Синий | 2 |
| 1 | Белый | 3 |
| 1 | Красный | 4 |
| 2 | Синий | 5 |
| 2 | Белый | 6 |
| 2 | Красный | 7 |
| 3 | Синий | A0 |
| 3 | Белый | A1 |
| 3 | Красный | A2 |

Скетч проекта
На заметку: Скетч написан с использованием макросов PINC и PIND, которые переопределены как P_3_PORT_READ и P_1_2_PORT_READс побитовым сдвигом. Для работы с другими микроконтроллерами (не UNO) их необходимо переопределить в соответствии с подключением кнопок и портами этих микроконтроллеров
/*☢*/
#include <iarduino_NeoPixel.h>
#include <Arduino_FreeRTOS.h>
#include <Wire.h>
#include <iarduino_I2C_Matrix_8x8.h>
// Определяем макросы чтения состояния выводов
#define P_3_PORT_READ (PINC << 6);
#define P_1_2_PORT_READ (PIND >> 2);
// Определяем количество игроков
#define PLAYERS 3
// Определяем цвета
#define RED 0xFF0000
#define GREEN 0x00FF00
#define BLUE 0x0000FF
#define WHITE 0xFFFFFF
#define BLACK 0x000000
// Определяем макрос ожидания
#define sleep(A) vTaskDelay(A/portTICK_PERIOD_MS)
// Определяем временные интервалы в миллисекундах
#define SECOND 1000
#define HALF 500
#define DEBOUNCE 50
#define FADE_DELAY 25
#define DISPLAY_DELAY 50
#define TIMEOUT 2000UL
#define WIN_TIMER 1000UL
#define GAMEOVER_TIMER 10000UL
// Определяем битовые маски кнопок игроков
#define BLUE_BUT_BITMASK 0b001
#define WHITE_BUT_BITMASK 0b010
#define RED_BUT_BITMASK 0b100
#define BITMASK 0b111
// Определяем побитовый сдвиг для второго и третьего игроков
#define SHIFT_P2 3
#define SHIFT_P3 6
// Определяем частоты нот
#define NOTE_A3 220
#define NOTE_C4 262
#define NOTE_G4 392
// Изображение для неправильного ответа
uint8_t image_x[] {
0b10000001,
0b01000010,
0b00100100,
0b00011000,
0b00011000,
0b00100100,
0b01000010,
0b10000001
};
/* Константы */
// Количество кнопок
constexpr uint8_t BUTTONS = 9;
// Адреса матриц на шине I2C
constexpr uint8_t P1_MATR_ADDR = 9;
constexpr uint8_t P2_MATR_ADDR = 10;
constexpr uint8_t P3_MATR_ADDR = 11;
// Выводы NeoPixel и Зуммера
constexpr uint8_t NEOPIN = 10;
constexpr uint8_t NEONUM = 4;
constexpr uint8_t SPKPIN = 11;
/* Выводы кнопок игроков
Эти константы используются только для
функции begin() объекта кнопок.
Функция чтения кнопок осуществляется через
макросы P_3_PORT_READ и P_1_2_PORT_READ.
При смене модели микропроцессора
или подключения кнопок к другим портам
их необходимо переопределить
*/
constexpr uint8_t P1_R_BUT = 2;
constexpr uint8_t P1_W_BUT = 3;
constexpr uint8_t P1_B_BUT = 4;
constexpr uint8_t P2_R_BUT = 5;
constexpr uint8_t P2_W_BUT = 6;
constexpr uint8_t P2_B_BUT = 7;
constexpr uint8_t P3_R_BUT = A0;
constexpr uint8_t P3_W_BUT = A1;
constexpr uint8_t P3_B_BUT = A2;
// Переменные состояния игры и счёта
uint8_t gamestate, p1_score, p2_score, p3_score;
// Переменные для функции перелива цвета NeoPixel
uint8_t fade;
uint8_t spec;
// Переменные блокировки кнопок игроков
bool p1_stop, p2_stop, p3_stop;
// Переменные первого нажатия кнопок
bool p1_first_press, p2_first_press, p3_first_press;
// Переменная случайного цвета
uint8_t r_color;
// Определения нового типа для цвета
typedef uint32_t color;
// Константы для идентификации цвета
enum {
red,
blue,
white
};
// Константы идентификации состояния игры
enum {
// На старт
start,
// Ожидания выбора цвета
running,
// Цвет выбран
stop,
// Сброс нажатых кнопок
reset_input,
// Оценка правильности нажатых кнопок
evaluate,
// Нажата правильная кнопка
win,
// Игра окончена
gameover
};
// Константы идентификации игроков
enum {
player1,
player2,
player3
};
// Переменные таймеров
unsigned long callMillis = 0;
unsigned long startMillis = 0;
// Объекты NeoPixel и матриц 8x8
iarduino_NeoPixel leds(NEOPIN, NEONUM);
iarduino_I2C_Matrix_8x8 disp[PLAYERS] {P1_MATR_ADDR, P2_MATR_ADDR, P3_MATR_ADDR};
// Функция вычисления бОльшего счёта
uint8_t getHighest()
{
uint8_t m = (uint8_t)max(max(p1_score, p2_score), p3_score);
return m;
}
// Функция сброса состояния игры
void reset()
{
gamestate = start;
p1_score = p2_score = p3_score = 0;
p1_stop = p2_stop = p3_stop = false;
p1_first_press = p2_first_press = p3_first_press = false;
fade = 0;
spec = 0;
r_color = 0;
}
// Определяем класс кнопок
class reactor_buttons{
public:
// Функция включения внутрисхемной подтяжки на всех кнопках
void begin() {
pinMode(P1_R_BUT, INPUT_PULLUP);
pinMode(P1_W_BUT, INPUT_PULLUP);
pinMode(P1_B_BUT, INPUT_PULLUP);
pinMode(P2_W_BUT, INPUT_PULLUP);
pinMode(P2_R_BUT, INPUT_PULLUP);
pinMode(P2_B_BUT, INPUT_PULLUP);
pinMode(P3_R_BUT, INPUT_PULLUP);
pinMode(P3_W_BUT, INPUT_PULLUP);
pinMode(P3_B_BUT, INPUT_PULLUP);
}
// Функция обновления состояния кнопок
uint16_t state() {
// Читаем кнопки третьего игрока,
_b_state = P_3_PORT_READ;
// читаем состояние кнопок первого и второго игрока,
_b_state ^= P_1_2_PORT_READ;
// выключаем ненужные биты и инвертируем состояние
// (1 - нажата, 0 - не нажата)
_b_state = ~_b_state & 0b0000000111111111;
// возвращаем состояние всех кнопок в одной
// шестнадцатибитной переменной.
return _b_state;
}
// Возвращаем биты кнопок первого игрока
uint8_t p1_state() {
return _b_state & BITMASK;
}
// Возвращаем биты кнопок второго игрока
uint8_t p2_state() {
return (_b_state >> SHIFT_P2) & BITMASK;
}
// Возвращаем биты кнопок третьего игрока
uint8_t p3_state() {
return (_b_state >> SHIFT_P3) & BITMASK;
}
private:
// Переменная состояния всех кнопок
uint16_t _b_state;
};
// Создаём объект кнопок
reactor_buttons buttons;
// Функция проверки кнопок
void buttonTest()
{
uint8_t i = 0;
// Пока не прошлись по всем кнопкам...
while(i < BUTTONS) {
// Пока не нажата ни одна кнопка, остановиться.
while(!buttons.state());
// Если нажата кнопка соответствующая биту i...
if (buttons.state() & 1 << i)
i++;
// вывести её номер на экран
disp[0].print(i);
}
}
void setup()
{
// Инициируем кнопки
buttons.begin();
// Ждём одну секунду до завершения переходных процессов
// связанных с подачей питания
delay(1000);
// Вызываем функцию сброса параметров игры
reset();
// Инициируем матрицы 8x8
for (auto& i:disp) {
i.begin();
i.bright(255);
i.angle(0);
}
/*
Если хотя бы одна кнопка нажата при старте скетча -
входим в режим тестирования кнопок.
*/
if (buttons.state()) {
buttonTest();
}
// Инициируем модуль NeoPixel и устанавливаем отсутствие цвета
leds.begin();
leds.setColor(NeoPixelAll, BLACK);
leds.write();
// Создаём задачу для звука
xTaskCreate(
TaskSound, // Указатель на функцию
"Sound", // Строка названия
64, // Размер стэка
NULL,
0, // Приоритет 0 - 3 (0 - самый низкий, 3 - самый высокий)
NULL
);
// Создаём задачу для NeoPixel
xTaskCreate(TaskLight, "Light", 64, NULL, 0, NULL);
// Создаём задачу игры
xTaskCreate(TaskGame, "Game", 128, NULL, 3, NULL);
// Создаём задачу обработки матриц 8x8
xTaskCreate(TaskDisplays, "Displays", 256, NULL, 2, NULL);
}
void loop()
{
// При использовании FreeRTOS тело функции loop остаётся пустым
}
// Функция ожидания
void wait()
{
// Если перелив цветов остановлен
if (gamestate == stop) {
// выбираем случайный цвет из трёх возможных
r_color = random(3);
// Устанавливаем это цвет на NeoPixel
if (r_color == red) {
setLed(RED);
}
else if (r_color == blue) {
setLed(BLUE);
}
else if (r_color == white) {
setLed(WHITE);
}
gamestate = evaluate;
}
// Если кто-то нажал правильную кнопку
else if (gamestate == win) {
// Ждём немного
if (millis() - callMillis > WIN_TIMER)
// и сбрасываем состояние игровых кнопок
gamestate = reset_input;
}
// Если игра окончена
else if (gamestate == gameover) {
if (millis() - callMillis > GAMEOVER_TIMER) {
// сбрасываем переменные игры,
reset();
// устанавливаем состояния игры на старт
gamestate == start;
}
}
}
// Функция зачисления очков
void playerScore(uint8_t player)
{
/*
Функция принимает идентификатор игрока
и начисляет этому игроку очки. Если у
одного из игроков 9 очков - функция
завершает игру.
*/
uint8_t score;
if (player1 == player)
score = ++p1_score;
else if (player2 == player)
score = ++p2_score;
else if (player3 == player)
score = ++p3_score;
gamestate = win;
callMillis = millis();
if (getHighest() >= 9) {
gamestate = gameover;
}
}
// Функция обновления кнопок
void readButtons(uint16_t& b_state, uint8_t& p1, uint8_t& p2, uint8_t& p3)
{
b_state = buttons.state();
p1 = buttons.p1_state();
p2 = buttons.p2_state();
p3 = buttons.p3_state();
if (gamestate != start && millis() - startMillis > SECOND) {
if (p1 && p1_first_press)
p1_stop = true;
if (p1 && !p1_first_press)
p1_first_press = true;
if (p2 && p2_first_press)
p2_stop = true;
if (p2 && !p2_first_press)
p2_first_press = true;
if (p3 && p3_first_press)
p3_stop = true;
if (p3 && !p3_first_press)
p3_first_press = true;
}
}
// Функция проверки кнопок
//(эту функцию хотелось бы оптимизировать, но c'est la vie)
void checkButtons(uint16_t b_state, uint8_t p1, uint8_t p2, uint8_t p3)
{
// Если прошло немного времени - сбросить состояние кнопок
if (millis() - callMillis > TIMEOUT) {
gamestate = reset_input;
}
// Если нажата хоть одна кнопка
if (b_state) {
// Переборка случайного цвета
switch (r_color) {
case red:
// Если зажата правильная кнопка
if (p1 & RED_BUT_BITMASK
// и только она
&& !(p1 ^ RED_BUT_BITMASK)
// и игрок не проштрафился
&& !p1_stop) {
// Увеличиваем счёт этого игрока на одно очко.
playerScore(player1);
return;
}
else if (p2 & RED_BUT_BITMASK
&& !(p2 ^ RED_BUT_BITMASK)
&& !p2_stop) {
playerScore(player2);
return;
}
else if (p3 & RED_BUT_BITMASK
&& !(p3 ^ RED_BUT_BITMASK)
&& !p3_stop) {
playerScore(player3);
return;
}
case white:
if (p1 & WHITE_BUT_BITMASK
&& !(p1 ^ WHITE_BUT_BITMASK)
&& !p1_stop) {
playerScore(player1);
return;
}
else if (p2 & WHITE_BUT_BITMASK
&& !(p2 ^ WHITE_BUT_BITMASK)
&& !p2_stop) {
playerScore(player2);
return;
}
else if (p3 & WHITE_BUT_BITMASK
&& !(p3 ^ WHITE_BUT_BITMASK)
&& !p3_stop) {
playerScore(player3);
return;
}
break;
case blue:
if (p1 & BLUE_BUT_BITMASK
&& !(p1 ^ BLUE_BUT_BITMASK)
&& !p1_stop) {
playerScore(player1);
return;
}
else if (p2 & BLUE_BUT_BITMASK
&& !(p2 ^ BLUE_BUT_BITMASK)
&& !p2_stop) {
playerScore(player2);
return;
}
else if (p3 & BLUE_BUT_BITMASK
&& !(p3 ^ BLUE_BUT_BITMASK)
&& !p3_stop) {
playerScore(player3);
return;
}
break;
default:
break;
}
}
}
// Функция ожидания выборки цвета
void run(uint16_t timer, unsigned long callMillis)
{
if (millis() - callMillis > timer * 1000)
gamestate = stop;
}
// Функция-задача игры
void TaskGame()
{
// Временной интервал
uint16_t timer = 0;
// Кнопки
uint16_t b_state;
uint8_t p1, p2, p3;
for (;;) {
// Если мы не ждём выборки цвета
if (gamestate != running)
// устанавливаем временной интервал для следующего раунда
timer = random(2, 8);
// Перебираем состояния игры
switch (gamestate) {
// Раунд выигран
case win:
// ждём
wait();
break;
// Если цвет определён
case stop:
// запоминаем время выбранного цвета
callMillis = millis();
// ждём
wait();
break;
// Если игра в режиме определения цвета нажатой кнопки
case evaluate:
// Читаем состояние кнопок
readButtons(b_state, p1, p2, p3);
// Проверяем кнопки
checkButtons(b_state, p1, p2, p3);
break;
// Если происходит сброс состояния кнопок
case reset_input:
// Сбрасываем штрафные флаги
p1_stop = p2_stop = p3_stop = false;
// Сбрасываем флаги первого нажатия за раунд
p1_first_press = p2_first_press = p3_first_press = false;
// Переключаем игру в режим ожидания выбора цвета
gamestate = running;
break;
// Если игра ожидает старта
case start:
// Если нажата хоть одна кнопка
if (buttons.state()) {
// Переключаем игру в режим ожидания выбора цвета
gamestate = running;
// Записываем время переключения
callMillis = millis();
// Записываем время старта
startMillis = millis();
}
break;
// Если игра ожидает выборки цвета
case running:
// Читаем кнопки (для штрафов)
readButtons(b_state, p1, p2, p3);
// Вызываем функцию ожидания
run(timer, callMillis);
break;
case gameover:
// Если игра закончена - ждём
wait();
break;
}
// Спим DEBOUNCE мс
sleep(DEBOUNCE);
}
}
// Функция-задача для матриц 8x8
void TaskDisplays()
{
for (;;) {
// Если игра не ожидает старта
if (gamestate != start) {
// Если игрок не проштрафился
if (!p1_stop)
// Выводим его счёт
disp[player1].print(p1_score);
else
// иначе выводим изображение штрафа
disp[player1].drawImage(image_x);
if (!p2_stop)
disp[player2].print(p2_score);
else
disp[player2].drawImage(image_x);
if (!p3_stop)
disp[player3].print(p3_score);
else
disp[player3].drawImage(image_x);
}
// Если игра ожидает старта - выводим случайные изображения
else {
uint8_t r_image[8];
for (auto& i:r_image)
i = random(256);
for (auto& i:disp) {
i.drawImage(r_image);
}
}
// Спим DISPLAY_DELAY мс
sleep(DISPLAY_DELAY);
}
}
// Функция-задача для звука
void TaskSound()
{
bool once = false;
for (;;) {
// Если происходит ожидание выбора цвета
if (gamestate == running) {
// Звук - мало ионизирующих частиц
playSound();
// Сбрасываем флаг однократного проигрывания
once = false;
}
// Если происходит оценка нажатых кнопок
else if (gamestate == evaluate)
// Звук - много ионизирующих частиц
playEvalSound();
// Если раунд выигран
else if (gamestate == win)
// Звук - нажата правильная кнопка
playWinSound(once);
// Если игра окончена
else if (gamestate == gameover)
// Звук - игра окончена
playGamerOverSound(once);
// Иначе
else
// Выключаем звук
stopSound();
}
}
// Функция имитации счётчика Гейгера: мало ионизирующих частиц
void playSound()
{
tone(SPKPIN, 100, 10);
sleep(random(30, 500));
}
// Функция имитации счётчика Гейгера: много ионизирующих частиц
void playEvalSound()
{
tone(SPKPIN, 100, 10);
sleep(random(10, 70));
}
// Функция проигрывания звука правильного ответа
void playWinSound(bool& once)
{
if (!once) {
for (int i = 0; i < 2; i++) {
tone(SPKPIN, NOTE_A3 * (i + 1));
sleep(100);
noTone(SPKPIN);
sleep(50);
}
once = true;
}
}
// Функция проигрывания звука окончания игры
void playGamerOverSound(bool& once)
{
if (!once) {
tone(SPKPIN, NOTE_C4);
sleep(100);
noTone(SPKPIN);
sleep(100);
tone(SPKPIN, NOTE_C4);
sleep(50);
noTone(SPKPIN);
sleep(50);
tone(SPKPIN, NOTE_C4);
sleep(50);
noTone(SPKPIN);
sleep(50);
tone(SPKPIN, NOTE_G4);
sleep(200);
noTone(SPKPIN);
sleep(200);
once = true;
}
}
// Функция остановки проигрывания звука
void stopSound()
{
noTone(SPKPIN);
}
// Функция-задача для NeoPixel
void TaskLight()
{
for (;;) {
// Если ожидание выборки цвета
if (gamestate == running)
// переливаем цвета
runLed();
// Установка цвета при чтении кнопок (gamestate == evaluate)
// происходит в функции wait() TODO: исправить это
// Если игра ожидает старта
else if (gamestate == start)
// устанавливаем зелёный цвет
setLed(GREEN);
}
}
// Функция переливания цветов на NeoPixel
void runLed()
{
fade++;
uint8_t r, g, b;
// Проходим по всем светодиодам
for (uint16_t i=0; i<leds.count(); i++) {
// Определяем положение очередного светодиода на смещённом спектре цветов
spec = ((uint16_t)(i*256/leds.count()) + fade);
if (spec<85) {
b=0; r=spec*3; g=255-r;
}
else if (spec<170) {
spec-=85; g=0; b=spec*3; r=255-b;
}
else {
spec-=170; r=0; g=spec*3; b=255-g;
}
// Устанавливаем выбранный цвет для очередного светодиода
leds.setColor(i, r,g,b);
}
leds.write();
sleep(FADE_DELAY);
}
// Функция установки цвета на NeoPixel
void setLed(color c)
{
leds.setColor(NeoPixelAll, c);
leds.write();
}

Обсуждение