Эксперимент 20. Перетягивание каната

← Светильник, управляемый по USB | Оглавление | Конец первой серии экспериментов!

В этом эксперименте мы создаем еще одну игру, на этот раз нужно быстрее соперника нажать кнопку 20 раз.

В старой версии блокнота хакера по ошибке были указаны резисторы на 100 кОм, которых нет в наборе. На самом деле в уроке смело можно и нужно использовать резисторы на 10 кОм из комплекта.

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

Для дополнительного задания

  • 1 сервопривод
  • 1 конденсатор 220 мкФ

Принципиальная схема

Схема на макетке

Обратите внимание

  • Схема подключения кнопок с использованием конденсаторов, резисторов и микросхемы 74HC14, которая называется инвертирующий триггер Шмитта, нужна для аппаратного подавления дребезга. Посмотрите видеоурок на эту тему.
  • В этом эксперименте нам нужно очень много цифровых портов, поэтому нам пришлось использовать порт 0. Пользоваться им неудобно из-за того, что он соединен с одним из каналов последовательного порта, поэтому перед прошивкой микроконтроллера нам придется отключать провод, идущий к пьезопищалке, а после прошивки подключать его обратно.

Скетч

p200_button_wrestling.ino
#define BUZZER_PIN     0
#define FIRST_BAR_PIN  4
#define BAR_COUNT      10
#define MAX_SCORE      20
// глобальные переменные, используемые в прерываниях (см. далее)
// должны быть отмечены как нестабильные (англ. volatile)
volatile int score = 0;
 
void setup()
{
  for (int i = 0; i < BAR_COUNT; ++i)
    pinMode(i + FIRST_BAR_PIN, OUTPUT);
  pinMode(BUZZER_PIN, OUTPUT);
  // Прерывание (англ. interrupt) приостанавливает основную
  // программу, выполняет заданную функцию, а затем возобновляет
  // основную программу. Нам нужно прерывание на нажатие кнопки,
  // т.е. при смене сигнала с высокого на низкий, т.е. на
  // нисходящем (англ. falling) фронте
  attachInterrupt(INT1, pushP1, FALLING); // INT1 — это 3-й пин
  attachInterrupt(INT0, pushP2, FALLING); // INT0 — это 2-й пин
}
 
void pushP1() { ++score; } // функция-прерывание 1-го игрока
void pushP2() { --score; } // функция-прерывание 2-го игрока
void loop()
{
  tone(BUZZER_PIN, 2000, 1000); // даём сигнал к старту.
  // пока никто из игроков не выиграл, обновляем «канат»
  while (abs(score) < MAX_SCORE) {
    int bound = map(score, -MAX_SCORE, MAX_SCORE, 0, BAR_COUNT);
    int left = min(bound, BAR_COUNT / 2 - 1);
    int right = max(bound, BAR_COUNT / 2);
    for (int i = 0; i < BAR_COUNT; ++i)
      digitalWrite(i + FIRST_BAR_PIN, i >= left && i <= right);
  }
  tone(BUZZER_PIN, 4000, 1000); // даём сигнал победы
  while (true) {} // «подвешиваем» плату до перезагрузки
}

Пояснения к коду

  • Код нашей обычной программы исполняется инструкция за инструкцией и если мы, например, проверяем состояние датчика, мы к нему обратимся только в те моменты, когда очередь дойдет до соответствующей инструкции. Однако мы можем использовать прерывания:
    • по наступлении определенного события
    • на определенном порту ход программы будет приостанавливаться для выполнения
    • определенной функции, а затем программа продолжит исполняться с того места, где была приостановлена.
  • Arduino Uno позволяет делать прерывания на портах 2 и 3.
  • В setup() прописывается инструкция attachInterrupt(interrupt, action, event), где
    • interrupt может принимать значения INT0 или INT1 для портов 2 и 3 соответственно. Можно задать эти значения и с помощью функции digitalPinToInterrupt(pin), где вместо pin указать номер пина.
    • action — имя функции, которая будет вызываться при наступлении события
    • event — событие, которое мы отслеживаем. Может принимать значение RISING (изменение от низкого уровня сигнала к высокому, от 0 к 1), FALLING (от высокого уровня к низкому, от 1 к 0), CHANGE (от 0 к 1 или от 1 к 0), LOW (при низком уровне сигнала).
  • Глобальные переменные, к которым мы обращаемся из функции, обрабатывающей прерывания, должны объявляться с использованием ключевого слова volatile, как в данном эксперименте volatile int score = 0.
  • Внутри функции, вызываемой по прерыванию, нельзя использовать delay().
  • Функция abs(value) возвращает абсолютное значение value (значение по модулю). Обратите внимание, что функция может сработать некорректно, если передавать ей выражение, которое еще не вычислено, например abs(++a), лучше передавать ей просто переменную.
  • Функция min(val1, val2) вернет меньшее из val1 и val2.
  • Функция max(val1, val2) вернет большее из val1 и val2.
  • В данном эксперименте мы вычисляем значение, которое записывается на светодиоды, прямо в digitalWrite()
  • Мы уже знакомы с логическим «и» (&&). Нередко нужен оператор «логическое «или»: ||. Он возвращает «истину», если хотя бы один из операндов имеет значение «истина». false || false вернет false, а true || true, true || false и false || true вернут true.
  • Мы использовали while(true){} для того, чтобы loop() остановился после того, как кто-то выиграл: у while всегда истинное условие и он бесконечно ничего не выполняет!

Вопросы для проверки себя

  1. Каким образом мы подавляем дребезг аппаратно?
  2. Для чего используются прерывания?
  3. Каким образом можно включить обработку внешних прерываний?
  4. О каких нюансах работы с уже известными нам вещами следует помнить при работе с прерываниями?
  5. Как выбрать максимальное из двух значений? Минимальное?
  6. Как получить абсолютное значение переменной? Чего следует избегать при использовании этой функции?
  7. Когда оператор логическое «или» возвращает «ложь»?

Задания для самостоятельного решения

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

← Светильник, управляемый по USB | Оглавление | Конец первой серии экспериментов!