Проект arduino электронная игральная кость

Игральный кубик (кости) на Arduino. Random числа

от автора

в

В предыдущих двух статьях, мы разобрались как работает светодиодный индикатор 8х8 пикселей, напрямую, без помощи доп. регистров и прочего. А также протестировали датчики наклона. Что можно собрать, используя эти два навыка? Правильно – игральный кубик. Раз по отдельности всё у нас работает, осталось только объединить всё это в одно устройство.

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

Я не буду касаться кода динамической индикации, если что можно обратиться к предыдущей статье. Только в этот раз массив мы зададим пустой, не будем инициализировать картинку в нём заранее, ведь она будет меняться.

char indication[8][8]; 

Помимо пустого массива индикации, который мы будем использовать для отображения на индикаторе, создадим ещё заготовки для цифр кубика. Например единица.

const char one[8][8] = {
  {'0', '0', '0', '0', '0', '0', '0', '0'},      // Единица
  {'0', '0', '0', '0', '0', '0', '0', '0'},
  {'0', '0', '0', '0', '0', '0', '0', '0'},
  {'0', '0', '0', '1', '1', '0', '0', '0'},
  {'0', '0', '0', '1', '1', '0', '0', '0'},
  {'0', '0', '0', '0', '0', '0', '0', '0'},
  {'0', '0', '0', '0', '0', '0', '0', '0'},
  {'0', '0', '0', '0', '0', '0', '0', '0'}
};

Создадим по аналогии остальные, так будет выглядеть пять

const char five[8][8] = {
  {'1', '1', '0', '0', '0', '0', '1', '1'},      // Пять
  {'1', '1', '0', '0', '0', '0', '1', '1'},
  {'0', '0', '0', '0', '0', '0', '0', '0'},
  {'0', '0', '0', '1', '1', '0', '0', '0'},
  {'0', '0', '0', '1', '1', '0', '0', '0'},
  {'0', '0', '0', '0', '0', '0', '0', '0'},
  {'1', '1', '0', '0', '0', '0', '1', '1'},
  {'1', '1', '0', '0', '0', '0', '1', '1'}
};

По аналогии зададим другие числа, не буду перечислять их в статье, принцип понятен. Начнём с самого необходимого – в зависимости от случайного значения от 1 до 6, будем менять массив indication, на значение другого массива. Кстати о функции random. Как бы это странно не звучало, но микроконтроллеру тяжело из ниоткуда придумать число с помощью кода – код выполняется прямолинейно. Поэтому обычно берётся АЦП аналогового входа, ведь если его оставить в воздухе, на нём постоянно будет изменяться напряжение. Оцифровав его, путем дальнейших преобразований, можно получить достаточно случайное число. В Arduino это делается очень просто, сначала читаем значение с аналогового входа, я использую 4, потому-что он у меня свободен, и “висит в воздухе”.

  randomSeed(analogRead(4));     // Случайное значение с аналогового входа

А потом генерируем случайное число, где в скобках указываем минимальное и максимальное значение, которое нам нужно получить от включительно и до (не включительно).

  randomValue = random(1, 7);     // Принимаем значение от 1 до 7

Теперь, получив случайное число, попробуем поменять значение массива indication. К сожалению нельзя написать массив1 = массив2, ничего не получится. Значения нужно переписывать в цикле, по двум измерениям, в нашем случае. Попробуем для понимания сбросить экран и напишем функцию очистки, в котором один цикл for (для значений i) будет вложен в другой цикл for (для значений J), ведь суммарно нам нужно получить 64 итерации.

void reset_display()
{
  for (int i = 0; i < 8; i++)
  {
    for (int j = 0; j < 8; j++)
    {
      indication[i][j] = 48;
    }
  }
}

В результате этого, все значения массива indication примут значение равное 0. Точно таким же методом, мы можем переназначить значения массива на значения другого массива, например в данном цикле, значение массива one будет скопировано в массив indication

for (int i = 0; i < 8; i++)
  {
    for (int j = 0; j < 8; j++)
    {
      indication[i][j] = one[i][j];
    }
  }

Далее, дело за малым, в зависимости от значения переменной random, мы назначаем массиву значения от 1 до 6, для отображения необходимого значения. Для этого воспользуемся оператором множественного значения switch

 void random_value ()
{
  randomSeed(analogRead(4));                  // Случайное значение с аналогового входа
  randomValue = random(1, 7);                 // Принимаем значение от 1 до 7
  for (int i = 0; i < 8; i++)                 // Цикл, который значения одного массива передаёт другому
  {
    for (int j = 0; j < 8; j++)
    {
      switch (randomValue) {
        case 1:
          indication[i][j] = one[i][j];       // В случае рандомного значения = 1, назначаем циклу indication значение массива one
          break;
        case 2:
          indication[i][j] = two[i][j];       // И т.д. для остальных значений
          break;
        case 3:
          indication[i][j] = three[i][j];
          break;
        case 4:
          indication[i][j] = four[i][j];
          break;
        case 5:
          indication[i][j] = five[i][j];
          break;
        case 6:
          indication[i][j] = six[i][j];
          break;
      }
    }
  }
}

Мы уже смотрели в статье с датчиками наклона, как запустить действие при многоразовой тряске – для этого мы используем счётчик переменную valCrash. Как только мы зафиксировали более 16 изменений выхода датчика с одного состояния на другое – нам нужно немного изменить переменные, для запуска отображения числа на индикаторе. Для этого мы обнуляем дисплей, сбрасываем переменные времени и активируем флаг event = 1;

void crash()
{
  if (digitalRead(vibration) == readSensor && event == 0)    // Если состояние датчика равно переменной состояния
  {
    readSensor = !readSensor;                                // Меняем переменную состояния на противоположное значение
    valCrash++;                                              // Инкрементируем счётчик трясок
    resetTime = millis();
    if (valCrash > 16)                                       // Если количество трясок более 20, то делаем действие
    {
      reset_display();
      eventTime = millis();
      resetTime = millis();
      event = 1;
      valCrash = 0;                                          // Обнуляем счётчик трясок
      delay(30);
    }
  }
}

Если же нет никого действия в течении 7 секунд – мы обнуляем счётчик трясок valCrash и очищаем дисплей – чтобы не потреблять энергию просто так и не показывать старый результат.

void resetCrash()
{
  if (millis() - resetTime > 7000)                          // Сбрасываем дисплей, если время простоя более 7 секунд
  {
    resetTime = millis();                                   // Приравниваем переменную сброса к времени контроллера
    valCrash = 0;
    reset_display();                                        // Функция сброса
  }
}

Функция сброса дисплея реализуется просто. Мы уже научились назначать значения одного массива другому. В данном случае просто заполняем массив нулями в двух циклах.

void reset_display()                                        // Функция сброса информации на экране
{
  for (int i = 0; i < 8; i++)
  {
    for (int j = 0; j < 8; j++)
    {
      indication[i][j] = 48;                                // Заполняем нулями все 64 значения 
    }
  }
}

Также для красоты перед выпадением числа на кубике – можно сделать любую анимацию. Чтобы её легко можно было изменить – я сделал её в 8 массивах, которые меняются по очереди в цикле. Для примера, так выглядят первые два кадра.

const char frame1[8][8] = {
  {'0', '0', '0', '0', '0', '0', '0', '1'},                          // Кадр 1
  {'1', '0', '0', '0', '0', '0', '0', '0'},
  {'0', '0', '0', '0', '0', '0', '0', '1'},
  {'1', '0', '0', '0', '0', '0', '0', '0'},
  {'0', '0', '0', '0', '0', '0', '0', '1'},
  {'1', '0', '0', '0', '0', '0', '0', '0'},
  {'0', '0', '0', '0', '0', '0', '0', '1'},
  {'1', '0', '0', '0', '0', '0', '0', '0'}
};

const char frame2[8][8] = {
  {'0', '0', '0', '0', '0', '0', '1', '0'},                          // Кадр 2
  {'0', '1', '0', '0', '0', '0', '0', '0'},
  {'0', '0', '0', '0', '0', '0', '1', '0'},
  {'0', '1', '0', '0', '0', '0', '0', '0'},
  {'0', '0', '0', '0', '0', '0', '1', '0'},
  {'0', '1', '0', '0', '0', '0', '0', '0'},
  {'0', '0', '0', '0', '0', '0', '1', '0'},
  {'0', '1', '0', '0', '0', '0', '0', '0'}
};

В ключевом условии if (millis() – shakeTime > 80 && event == 1) мы запускаем действие анимации, как только флаг event == 1. Каждые 80мс, мы меняем кадр в цикле, чтобы глаз успевал заметить кадр. Этим значением можно замедлять или ускорять анимацию.

void shaking()
{
    if (millis() - shakeTime > 80 && event == 1)        // Функция
  {
    if (a > 7)
    {
      a = 0;
    }
    for (int i = 0; i < 8; i++)                        // Цикл, который значения одного массива передаёт другому
    {
      for (int j = 0; j < 8; j++)
      {
        switch (a) {
          case 0:
            indication[i][j] = frame1[i][j];          // В случае рандомного значения = 1, назначаем циклу indication значение массива one
            break;
          case 1:
            indication[i][j] = frame2[i][j];          // И т.д. для остальных значений
            break;
          case 2:
            indication[i][j] = frame3[i][j];
            break;
          case 3:
            indication[i][j] = frame4[i][j];
            break;
          case 4:
            indication[i][j] = frame5[i][j];
            break;
          case 5:
            indication[i][j] = frame6[i][j];
            break;
          case 6:
            indication[i][j] = frame7[i][j];
            break;
          case 7:
            indication[i][j] = frame8[i][j];
            break;
        }
      }
    }
    a++;
    shakeTime = millis();
  }
}

Ну и действие отображения самого числа. Оно срабатывает спустя время eventTime >= 1500, чтобы дать поработать анимации. Анимация же сразу отключится, потому-что мы снимаем флаг event = 0; в этой функции.

void action ()                                       // Проверка условий для запуска отображения случайного числа
{
  if (millis() - eventTime >= 1500 && event == 1)
  {
    event = 0;                                       // Сбрасываем флаг действия
    reset_display();                                 // Очищаем экран
    random_value();                                  // Задаём массиву случайное значение
    eventTime = millis();                            // Сбрасываем переменную времени действия
  }
}

Ну а в главном цикле мы проверяем постоянно все наши функции. Сначала при тряске активируются действия в crash();, затем если набрали необходимое число трясок, запускаем функцию анимации shaking(); . action(); активируется через определённое время после старта анимации, и выключают саму анимацию, заменяя картинку на экране случайным числом. resetCrash(); очищает экран, при отсутствии действий со стороны пользователя, а dynamic_indi(); у нас в работе всегда – это динамическая индикация экрана.

void loop()
{
  shaking();                                          // Функция анимации
  action();                                           // Вызов проверки функции действия
  resetCrash();                                       // Вызов функции сброса экрана и действия
  crash();                                            // Вызов функции проверки тряски датчика наклона
  dynamic_indi();                                     // Динамическая индикация всегда должна быть активна
}

Ссылка на проект

В следующих статьях мы перенесём проект на автономную плату, и я расскажу как уйти от arduino, что важно в реальном проекте.