В предыдущих двух статьях, мы разобрались как работает светодиодный индикатор 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, что важно в реальном проекте.