Игрушка на Arduino: Саймон сказал

Хотите чем-либо занять ребёнка? Машинки и куклы надоели, хочется что-нибудь по-веселее? Предлагаю сделать простую электронную игрушку своими руками. В магазинах есть интересная игра под названием: «Саймон сказал»:

Суть игры заключается в том, чтобы игрок повторил код, который показал компьютер. Нечто подобное мы и будем собирать сегодня сами.

Для начала разберёмся с основными частями проекта:

  • 4 кнопки и 4 светодиода — это основа. Посредством светодиодов компьютер будет показывать код, а с помощью кнопок игрок будет его повторять.
  • Динамик будет озвучивать нажатие на кнопки и включение лампочек.
  • Семисегментный индикатор с драйвером 74HC595. Будет отображать текущий уровень сложности.
  • Фоторезистор. Для генератора случайных чисел. (Позже объясню).

Подключаем к Arduino по такой схеме:

  • Светодиоды подключены к 8, 6, 4, 2 выходам Arduino через резисторы по 220 Ом (номинал можно изменить от 150 до 330 Ом);
  • Кнопки к 9, 7, 5, 3 выходам со стягивающими резисторами 10 кОм (номинал не важен, важно его наличие);
  • Динамик(Я взял пассивный) подключен напрямую к 10 пину (поддерживает ШИМ);
  • Контакты семисегмента (через резистор) идут к выходам регистра.
  • Сам регистр подключен к 13-му, 12-му и 11-му контакту (такт, данные и защёлка); Подробнее в этой статье.
  • Ну, и фоторезистор к аналоговому входу 1;

Я всё сделал на Breadboard. Получилось так:

В дальнейшем планирую сделать печатную плату, добавить питание от USB и поместить в какой-либо корпус. Плату уже нарисовал (скачать можно внизу). Там 2 платы. 1-я — это верхняя(лицевая) часть с кнопками, светодиодами, индикатором и. т. д.. А 2-я (нижняя) — это микроконтроллер с обвязкой (вместо неё можно использовать Arduino);

Теперь о коде: (Скачать код полностью можно внизу по ссылке)

В первую очередь пишем глобальные переменные:

 #define CLOCK 13 #define DATA 12 #define LATCH 11

— это выходы регистра;

 #define buzzer 10 const int leds[4] = {8, 6, 4, 2}; const int buttons[4] = {9, 7, 5, 3};

— выходы динамика (пищалки), светодиодов и входы кнопок.

 const int notes[4] = {262, 330, 392, 523};

— а в это массиве хранятся ноты, которые будут проигрываться при нажатии кнопок. При желании их можно сделать одинаковыми. Я их брал из файла pitches.h. Откройте пример Digital > toneMelody. К нему прикреплён это файл:

В setup-е назначаем пины регистра, динамика и светодиодов выходами, а кнопки входами; ставим высокий уровень на защёлку.

 pinMode(CLOCK, OUTPUT); pinMode(DATA, OUTPUT); pinMode(LATCH, OUTPUT); pinMode(buzzer, OUTPUT); for(int i=0; i<4; i++)   pinMode(leds[i], OUTPUT),   pinMode(buttons[i], INPUT);  digitalWrite(LATCH, HIGH);

Теперь разберём функции:

  • для семисегмента я сначала написал 2 функции. 1-я будет выводить на него цифры (от 0 до 9. 10 — это 9 с точкой):
 void segWrite(int number){   byte numbers[11] = {0b11111100,0b01100000,0b11011010,     0b11110010,0b01100110,0b10110110,0b10111110,     0b11100000,0b11111110,0b11110110,0b11110111};   digitalWrite(LATCH, LOW);   shiftOut(DATA, CLOCK, LSBFIRST, numbers[number]);   digitalWrite(LATCH, HIGH); }

А 2-я будет выключать индикатор:

 void segOff(){   digitalWrite(LATCH, LOW);   shiftOut(DATA, CLOCK, LSBFIRST, 0);   digitalWrite(LATCH, HIGH); }
  • Потом решил добавить 3-ю, которая будет показывать небольшую анимацию перед началом игры:
 void animation(){   byte add = 1;   byte value = 0;   for(int i=0; i<6; i++){     value+=add;     digitalWrite(LATCH, LOW);     shiftOut(DATA, CLOCK, MSBFIRST, value);     digitalWrite(LATCH, HIGH);     add*=2;     delay(100);   } }
  • Следующая называется showled. Она будет показывать код игроку:
 void showled(int count){   tone(buzzer, notes[count]);   digitalWrite(leds[count], HIGH);   delay(300);   digitalWrite(leds[count], LOW);   noTone(buzzer);   delay(300); }
  • Следующие 2 функции противоположны друг-другу. Одна будет исполнятся, когда игрок выиграл:
 void soundwin(){   //262, 294, 330, 392; 330, 392;   tone(buzzer, 262);   digitalWrite(leds[0], HIGH);   delay(100);   tone(buzzer, 294);   digitalWrite(leds[1], HIGH);   delay(100);   tone(buzzer, 330);   digitalWrite(leds[2], HIGH);   delay(100);   tone(buzzer, 392);   digitalWrite(leds[3], HIGH);   delay(100);   noTone(buzzer);   for(int i=0; i<4; i++) digitalWrite(leds[i], LOW);   delay(100);   tone(buzzer, 330);   delay(100);   tone(buzzer, 392);   for(int i=0; i<4; i++) digitalWrite(leds[i], HIGH);   delay(500);   noTone(buzzer);   for(int i=0; i<4; i++) digitalWrite(leds[i], LOW); }

(ноты брал из того же файла);

Другая — когда проиграл:

 void soundlose(){   tone(buzzer, 1480);   for(int i=0; i<4; i++) digitalWrite(leds[i], HIGH);   delay(350);   for(int i=0; i<4; i++) digitalWrite(leds[i], LOW);   tone(buzzer, 1047);   delay(350);   tone(buzzer, 1480);   for(int i=0; i<4; i++) digitalWrite(leds[i], HIGH);   delay(500);   noTone(buzzer);   for(int i=0; i<4; i++) digitalWrite(leds[i], LOW); }
  • Функции для работы с кнопками: button() — будет ожидать нажатия любой кнопки и возвращать её номер.
 int button(){   int pressed = -1;   do{     for(int i=0; i<4; i++){       if(digitalRead(buttons[i])==HIGH){         pressed = i;         break;       }     }   } while(pressed<0);   return pressed; }

readButton() — а эта будет проверять, правильная ли кнопка нажата и, соответственно, возвращать: да или нет.

 boolean readbutton(int count){   int pressed = button();   delay(5);   tone(buzzer, notes[pressed]);   digitalWrite(leds[pressed], HIGH);   delay(100);   while(digitalRead(buttons[pressed])!=LOW);   noTone(buzzer);   digitalWrite(leds[pressed], LOW);   delay(100);   if(pressed==count) return true;   else return false; }

(И, кстати, данная функция не будет ничего возвращать, пока человек не отпустит кнопку. Таким образом, последнее состояние кнопки будет всегда LOW, потому для борьбы с дребезгом мы ограничимся только задержкой на 5 мс).

  • И последняя функция (самая интересная) будет делать случайные числа максимально случайными 🙂
 void setRandom(){   int analog = analogRead(0);   int light = analogRead(1);   unsigned long time = millis();   unsigned long results = analog * light * time;   do results = results / 10;   while(results>=1000);   results+=random(0, 300);   randomSeed(results); }

Всё что она делает — это формирует число, зависящее от освещённости (для этого и нужен фоторезистор), показаний с пустого аналог. входа и времени. А потом отправляет его в randomSeed();

Теперь сам цикл. (Для меня это было самым сложным, т. к. пришлось использовать метки)

— Вначале стоят 4 переменные, отвечающие за кол-во жизней, длину кода, текущий уровень и сам код.

 int lives = 2; int codelength = 10; int level = 0; int code[codelength];

(обращу ваше внимание, что Arduino будет показывать игроку сначала одну цифру кода, потом первую + вторую, + 3-ю, 4-ю и. т. д. Поэтому весь код надо разбивать на части, а лучше сразу сделать массив переменных, как это сделал я);

— Затем показываем анимацию и ожидаем нажатия любой кнопки для начала игры. Ну, и чуть-чуть ждём:

   animation();   button();   delay(1000);

— Кода игра началась, сразу заполняем ячейки кода random-ом:

 for(int i=0; i<codelength; i++){     setRandom();     code[i] = random(4);   }

— Вся игра будет крутиться в цикле, пока игрок не пройдёт все уровни, либо пока жизни не закончатся. Поэтому заводим цикл for():

 for(level=0; level < codelength; level++){

— В цикле ставим метку show, отображаем на индикаторе текущий уровень и проверяем, не закончились ли жизни:

 show: segWrite(level+1); if(lives==0) break;

— Потом крутим ячейки нашего кода и показываем их игроку (кол-во таких ячеек должно быть равно текущему уровню):

  //показываем код(одно за другим)     for(int count=0; count<=level; count++) showled(code[count]);

— Затем заводим ещё один цикл for(), в котором выводим на сегмент количество кнопок, которое ещё необходимо нажать, и проверяем нажатие кнопок. Если нажата не та кнопка, то играем печальную мелодию о проигрыше и переходим в начало первого цикла (заново показываем код):

    //проверяем код(одно за другим)     for(int count=0; count<=level; count++){       segWrite(level+1-count);       // если правильно - идём дальше       // если неправильно, уменьшаем жизни       if(!readbutton(code[count])){         segOff();         soundlose();         delay(1000);         lives--;         goto show;       }     }

— И, если данный цикл пройден, значит игрок нажал всё правильно. Поздравляем его и переходим на следующий уровень:

 segWrite(0); delay(100); soundwin(); delay(1000);

Надеюсь, я понятно объяснил. Если есть вопросы — пишите в комментариях. Буду рад ответить.

Список радиоэлементов

Обозначение Тип Номинал Количество Примечание Магазин Мой блокнот
Arduino или ATmega8 с обвязкой
МК AVR 8-бит ATmega8 1
Кварцевый резонатор 16 МГц 1
Конденсатор 22 пФ 2
Конденсатор 100 мФ 1
Светодиод 1 ON
Резистор 220 Ом 1
Тактовая кнопка 1 RESET
Резистор 1 — 10 КОм 1
Разъём USB 1
Периферия
IC1 Сдвиговый регистр CD74HC595 1
SEG1 Семисегмент 1
LED1-LED4 Светодиод 4 У меня 2 красных и 2 жёлтых
R1-8, R13-16 Резистор 220 Ом 12
R9-12 Резистор 1 — 10 КОм 4
S1-S4 Тактовая кнопка 4
SP1 Динамик (пищалка) активный/пассивный 1
PH1 Фоторезистор 1