Постоянство зрения — это фундаментальный принцип, который позволяет нашим глазам воспринимать непрерывное движение на отдельных изображениях. Эта концепция имеет решающее значение для иллюзии движения в кино. Когда я был ребенком, меня всегда это увлекало. Итак, в этом проекте мы собираемся использовать это увлекательное оптическое явление для создания подобного POV дисплея. POV дисплеи также называются дисплеями постоянного видения или голографическими дисплеями. Также их иногда называют вращающимися светодиодными дисплеями.
Вы наверняка видели много подобных проектов в сети, но большинство из них имеют очень низкое разрешение и отображают лишь текст и геометрию. Мы же планируем создать POV-дисплей, который не будет ограничиваться только текстами или простыми фигурами. Наш дисплей сможет отображать изображения и анимацию с разрешением 128 пикселей. Мы остановились на этом разрешении, потому что это оптимальная точка, при которой мы можем получить достойное качество изображения и при этом подобный дисплей будет сравнительно легко создать.
Ранее на нашем сайте мы рассматривали создание вращающегося светодиодного дисплея на Arduino, но, конечно же, это значительно более простой проект чем тот, который мы собираемся рассмотреть в этой статье.
Мы выбрали модуль ESP32 в качестве «мозга» этого дисплея, поскольку он дешев, его легко приобрести и он достаточно мощный для работы дисплея. Дисплей будет иметь два вращающихся рычага, каждый из которых будет оснащен 64 светодиодами, что обеспечит общее разрешение 128 пикселей. Дисплей будет вращаться с постоянной скоростью, а светодиоды будут мигать в тщательно продуманных схемах, управляемых микроконтроллером ESP32. Эта синхронизация позволяет дисплею генерировать изображения или текст, которые кажутся парящими в воздухе, создавая плавное и непрерывное визуальное восприятие.
Особенности нашего POV дисплея:
- Разрешение 128 пикселей.
- Частота кадров 20 кадров в секунду.
- Легко построить.
- Легко контролировать.
- На базе ESP32.
- Полностью открытый исходный код.
- Сопутствующее веб-приложение для легкого преобразования изображений.
Необходимые компоненты
- Модуль ESP32 WROOM — x1 (купить на AliExpress).
- Регистр сдвига 74HC595D – x16 (купить на AliExpress).
- CH340K USB-контроллер UART – x1 (купить на AliExpress).
- TP4056 — микросхема зарядного устройства для литий-ионных аккумуляторов — x1 (купить на AliExpress).
- AMS1117 3,3 В LDO – x1 (купить на AliExpress — можно купить не в виде модуля, а в виде отдельной микросхемы (от 5 до 7 рублей за штуку), но на момент публикации данной статьи не нашел на алиэкспрессе магазина с дешевой доставкой данной микросхемы).
- AO3401 P — МОП-транзистор — x1 (купить на AliExpress).
- 2N7002DW двойной N — МОП-транзистор – x1
- Датчики Холла – x2
- Диод SS34 – x1 (купить на AliExpress).
- USB-разъем типа C, 16 контактов — x1 (купить на AliExpress).
- Светодиод SMD Синий 0603 — x128
- 775 Мотор – x1
- Контроллер скорости двигателя постоянного тока — x1
- SMD резисторы и конденсаторы
- SMD светодиоды
- SMD Тактильные переключатели
- Ползунковый переключатель SDM
- Разъемы
- Пользовательская печатная плата
- Деталь, напечатанная на 3D-принтере, и крепежные винты.
- Прочие инструменты и расходные материалы.
Реклама: ООО «АЛИБАБА.КОМ (РУ)» ИНН: 7703380158
Схема проекта
Принципиальная схема POV дисплея высокого разрешения на основе модуля ESP32 представлена на следующем рисунке. Ее также можно скачать в виде PDF файла по ссылке в конце статьи.
Порт USB типа C в схеме используется как для зарядки, так и для целей программирования. Питание от USB-порта подается на схему контроллера цепи питания, построенную на основе P-канального MOSFET U7 и диода D67. Разъем J4 можно использовать для подачи внешнего напряжения 5 В. Тот же порт можно использовать для подключения модуля беспроводного зарядного устройства, если мы хотим, чтобы он работал непрерывно без зарядки. Для регулирования напряжения мы использовали популярный LDO AMS1117 3,3 В, который способен выдавать ток до 1 А с падением напряжения примерно 1,1 В при полной нагрузке. Разъем J3 используется для подключения внешнего переключателя включения и выключения всей схемы. Для зарядки внутренней батареи мы используем контроллер заряда TP4056, который может заряжать батарею максимальным током заряда 1 А. Теперь, если мы посмотрим на схему программирования, она построена на чипе CH340K от WCH. Для автоматического сброса мы использовали двойной МОП-транзистор 2N7002DW. Теперь мозгом всей схемы является модуль ESP32-WROOM. Мы выбрали этот модуль SoC, потому что он дешев, легко доступен и достаточно мощный, чтобы обеспечить достаточную частоту кадров на дисплее. Мы соединили пиксели как две секции. Каждая секция имеет 64 пикселя или светодиода. Итак, в общей сложности мы имеем разрешение 128 пикселей. Мы также использовали два датчика Холла для измерения оборотов и определения положения. На печатной плате мы добавили для него место для поверхностного монтажа, но позже решили использовать обычный датчик A3144 в корпусе TO-92, поскольку его легче приобрести и легко установить с помощью конструкции держателя тока.
Для управления светодиодами мы использовали сдвиговые регистры 74HC595D. Поскольку один s74HC595 может управлять до 8 светодиодами, мы использовали в общей сложности 16 таких чипов для управления всеми 128 светодиодами. Мы использовали резистор сопротивлением 1 кОм для ограничения тока, но вы можете изменить это значение в зависимости от необходимой вам яркости. Поскольку у нас две секции, нам достаточно половины оборота, чтобы нарисовать целый кадр или изображение. Одна секция будет рисовать половину изображения, а другая — вторую половину изображения. Сделав это, мы смогли удвоить частоту кадров. Дисплей может обеспечить приблизительную частоту кадров 20 кадров в секунду.
Каждый пиксель будет управляться через эти последовательно соединенные регистры сдвига.
Печатная плата для POV дисплея
Для этого проекта мы решили создать собственную печатную плату с помощью KiCad. Это гарантирует, что конечный продукт будет максимально компактным, а также простым в сборке и использовании. Печатная плата имеет размеры примерно 210 x 60 мм. Вот верхний и нижний слои печатной платы.
А вот 3D-вид печатной платы.
И вот полностью собранная плата.
Gerber файлы для изготовления печатной платы вы можете скачать по ссылке в конце статьи.
Напечатанные на 3D-принтере детали для POV дисплея
Мы разработали классный напечатанный на 3D-принтере POV-дисплей с помощью Fusion360. Файлы для всех напечатанных на 3D-принтере деталей можно загрузить по ссылке GitHub, приведенной в конце статьи, вместе со эскизом Arduino и растровым файлом. Вы также можете узнать больше о 3D-печати и о том, как начать с ней работать.
Вот держатель печатной платы вместе с самой платой.
А вот 3D-вид POV дисплея вместе с монтажной подставкой. Все модели, которые вы видите здесь, созданы с использованием fusion360.
А вот полностью собранный светодиодный POV дисплей.
Как работает постоянство видения?
Теперь давайте посмотрим, как работает отображение на POV дисплее. Если вы посмотрите на изображение ниже, то увидите, что мы разделили круг на 32 равные части. Это указывает на ряд пикселей. Итак, если мы разделим его таким образом, чтобы завершить изображение, нам придется нарисовать 32 строки пикселей при каждом повороте. При отображении каждой из этих строк мы должны включать или выключать каждый пиксель в зависимости от данных пикселя.
На нашем дисплее мы делим каждое изображение на 360 частей по радиусу. Это означает, что мы должны нарисовать 360 линий с интервалом в 1 градус при каждом повороте, чтобы нарисовать изображение. В каждой строке будет 64 пикселя или светодиода (всего 128 светодиодов с обоих плеч), которыми мы должны манипулировать в соответствии с данными пикселей. Проблема с отображением обычного изображения заключается в том, что они используют декартову систему координат. Но для того, чтобы отобразить изображение на вращающемся дисплее POV, нам необходимо изображение с полярными координатами. В декартовой системе координат пиксели имеют квадратную форму, а положение определяется расстоянием по горизонтали (x) и расстоянием по вертикали (y). Полярные координаты, с другой стороны, основаны на круговой сетке. Пиксели выглядят как клинья или трапецеидальные искажения, а положение определяется радиусом (r) и углом от горизонтали (θ). Таким образом, пиксели неоднородны: по мере увеличения расстояния от начала координат пиксели увеличиваются в площади и меняют форму.
Чтобы получить пиксельные данные из изображения, мы должны использовать некоторые тригонометрические вычисления и некоторую интерполяцию. Однако выполнение этого для каждого пикселя каждого изображения займет много времени и постепенно увеличит время отклика пикселя. Чтобы избежать этого и получить минимальное время покоя пикселей и, следовательно, максимальную частоту обновления, мы будем использовать заранее вычисленное значение для обработки изображения, которое будет объяснено ниже в статье. При нашей нынешней настройке рисование одного кадра или изображения с разрешением 128 пикселей и 360 сегментами займет около 50 мс, что дает эффективную частоту 20 кадров в секунду.
Следующей задачей было оптимизировать способ хранения изображения. Потому что даже при использовании обычных массивов изображений, преобразованных традиционными инструментами, каждый пиксель будет занимать 1 байт пространства. То есть изображению размером 128×128 потребуется 16384 байта или 16,384 Кбайт места. Но при этом мы будем ограничены количеством изображений, которые мы можем хранить в пространстве кода. Чтобы преодолеть эту проблему, а также улучшить оптимизацию, мы использовали новый подход. Каждая строка пикселей изображения будет храниться в 16 байтах. Каждый из этих байтов будет содержать данные из 8 пикселей в виде единицы или нуля, т.е. черного или белого цвета. Затем эти данные будут декодированы с помощью простой функции для получения фактических данных пикселей. При использовании этого метода одному изображению размером 128×128 пикселей потребуется всего 2048 (128×16) байт или 2,048 килобайт пространства. Используя этот метод, нам удалось уменьшить размер изображения в 8 раз. Чтобы преобразовать изображение в такой формат, мы также создали веб-приложение, ссылка на которое приведена в следующем разделе статьи.
Как преобразовать изображение в код для отображения POV?
Чтобы преобразовать изображение, сначала убедитесь, что ваши изображения имеют разрешение 128×128 пикселей и имеют черно-белый формат. Оттенки серого могут обрабатываться неправильно. Вы можете конвертировать столько изображений, сколько захотите. Для этого откройте следующий конвертер изображений. После того, как изображения выбраны, нажмите «Конвертировать». Он создаст соответствующие массивы. Если вы выберете несколько изображений, выходные данные будут иметь одинаковое количество массивов с такими именами, как Image_1, Image_2, Image _3 и т. д. Вы можете скопировать и вставить этот массив в свой код, чтобы использовать их. Если вы хотите, там будет флажок для инвертирования цвета. Для этого установите флажок и еще раз нажмите кнопку конвертировать. Он создаст новые массивы для копирования.
Объяснение кода программы
Теперь давайте посмотрим на код Arduino для POV дисплея. Как обычно, мы включили в код все необходимые библиотеки с помощью функции include, при этом единственной сторонней библиотекой, которая понадобилась, была MultiShiftRegister, которая используется для управления сдвиговыми регистрами 74HC595. Вы также можете видеть, что мы также включили два заголовочных файла. Первый заголовочный файл Images.h содержит все изображения, хранящиеся в виде оптимизированных массивов данных. Заголовочный файл Precompute.h содержит данные поиска, которые используются для вычисления данных пикселей с использованием полярных координат. Вы можете скачать все необходимые файлы из репозитория GitHub, ссылка на который приведена внизу этой статьи.
После подключения библиотек и необходимых заголовочных файлов мы определили все необходимые глобальные переменные. Мы также создали два экземпляра сдвигового регистра для каждого плеча. Мы будем использовать эти экземпляры для управления светодиодами в каждом плече отдельно.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
#include <Arduino.h> #include <MultiShiftRegister.h> #include "Images.h" #include "Precompute.h" // Pin Definitions #define latchPin 14 #define clockPin 15 #define dataPin 13 #define latchPinx 17 #define clockPinx 18 #define dataPinx 16 #define HALL_SENSOR1_PIN 36 #define HALL_SENSOR2_PIN 39 // Global Variables volatile unsigned long lastHallTrigger = 0; volatile float rotationTime = 0; // Time for one rotation in milliseconds int hallSensor1State = 0; int hallSensor2State = 0; bool halfFrame = false; int numberOfRegisters = 8; int offset = 270; int repeatvalue = 1; int hys = 3000; int frame = 0; int repeat = 0; int anim = 0; int frameHoldTime = 1; // Number of loops to hold each frame int frameHoldCounter = 0; // Counter to track loops for current frame //Shift register Driver instances for both hands MultiShiftRegister msr(numberOfRegisters, latchPin, clockPin, dataPin); MultiShiftRegister msrx(numberOfRegisters, latchPinx, clockPinx, dataPinx); |
Далее у нас есть функция getValueFromAngle, которая принимает 3 параметра, включая имя 2D-массива, угол и радиус, и возвращает данные пикселей. Функция получает угол и обрабатывает его, используя значение смещения, чтобы получить скорректированный угол. Затем этот скорректированный угол используется с радиусом и значениями из массивов precomputedCos и PrecomputedSin для расчета данных X и Y для соответствующего пикселя. Затем он извлекает значение этого пикселя и возвращает его.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
//Function to calculate polar co-ordintes, and get corresponding data from the arrays. int getValueFromAngle(const uint8_t arrayName[][16], int angle, int radius) { // Adjust the angle by subtracting offset to rotate counter-clockwise int adjustedAngle = angle - offset; if (adjustedAngle < 0) adjustedAngle += 360; // Ensure the angle stays within 0-359 degrees // Invert the targetX calculation to flip the image horizontally int targetX = 127 - precomputedCos[radius][adjustedAngle]; // Flipping targetX int targetY = precomputedSin[radius][adjustedAngle]; if (targetX >= 0 && targetX < 128 && targetY >= 0 && targetY < 128) { int byteIndex = targetX / 8; int bitIndex = 7 - (targetX % 8); return (arrayName[targetY][byteIndex] >> bitIndex) & 1; // Extract the bit value and return it } else { return -1; // Out of bounds } } |
Затем у нас есть две процедуры прерывания для обоих датчиков Холла. Первая процедура используется как для определения положения, так и для измерения скорости вращения. Вторая процедура используется только для определения положения. Мы использовали для этого два датчика, потому что для рисования изображения нам нужно всего лишь полповорота. Поэтому определение положения каждые пол-оборота имеет решающее значение для синхронизации дисплея.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
//Процедура прерывания датчика Холла 1 void ISR_HallSensor1() { unsigned long currentTime = micros(); // Check if HYS ms have passed since the last trigger if (currentTime - lastHallTrigger >= hys) { rotationTime = (currentTime - lastHallTrigger) / 1000.0; lastHallTrigger = currentTime; hallSensor1State = 1; halfFrame = true; } } //Процедура прерывания датчика Холла 2 void ISR_HallSensor2() { unsigned long currentTime = micros(); // Check if HYS ms have passed since the last trigger if (currentTime - lastHallTrigger >= hys) { lastHallTrigger = currentTime; hallSensor2State = 1; halfFrame = false; } } |
Позже у нас есть функция DisplayFrame. Эта функция обрабатывает выборку и рисование изображения. Она рисует изображения построчно: первое плечо используется для рисования первой половины, а второе плечо используется для одновременного рисования противоположной половины изображения. Она также синхронизирует время отрисовки кадра, используя данные RPM из процедуры прерывания.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
//Функция для расчета числа оборотов в минуту и соответствующего отображения каждого кадра void DisplayFrame(const uint8_t ImageName[][16]) { float timePerSegment = rotationTime / 360.0; // Предполагаем, что за полуоборот 180 сегментов for (int i = 0; i < 180; i++) { unsigned long segmentStartTime = micros(); for (int j = 0; j < 64; j++) { // Первое плечо (msr) отображает первую половину кадра if (getValueFromAngle(ImageName, i + (halfFrame ? 0 : 180), j)) { msr.set(j); } else { msr.clear(j); } // Второе плечо (msr) отображает вторую половину кадра if (getValueFromAngle(ImageName, i + (halfFrame ? 180 : 0), j)) { msrx.set(j); } else { msrx.clear(j); } } msr.shift(); msrx.shift(); while (micros() - segmentStartTime < timePerSegment * 1000) ; } } |
В функции setup() мы инициализировали все необходимые контакты как входы и выходы. Затем мы подключили два прерывания к соответствующим выводам с нарастающим фронтом (более подробно об использовании прерываний в модуле ESP32 вы можете узнать в этой статье). Затем мы отключили все пиксели, прежде чем что-либо рисовать.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
void setup() { // Initialize pins pinMode(latchPin, OUTPUT); pinMode(clockPin, OUTPUT); pinMode(dataPin, OUTPUT); pinMode(latchPinx, OUTPUT); pinMode(clockPinx, OUTPUT); pinMode(dataPinx, OUTPUT); pinMode(HALL_SENSOR1_PIN, INPUT); pinMode(HALL_SENSOR2_PIN, INPUT); attachInterrupt(digitalPinToInterrupt(HALL_SENSOR1_PIN), ISR_HallSensor1, RISING); attachInterrupt(digitalPinToInterrupt(HALL_SENSOR2_PIN), ISR_HallSensor2, RISING); for (int i = 0; i < 64; i++) { msr.clear(i); msrx.clear(i); } msr.shift(); msrx.shift(); Serial.begin(115200); } |
В функции loop() мы рисуем все анимации и изображения одно за другим. Переменная anim используется для циклического просмотра каждого изображения и анимации. Переменная frameHoldeTime управляет скоростью анимации. Если вы установите для этого параметра значение два, анимация будет воспроизводиться на половине скорости, а если вы установите четыре, она будет воспроизводиться на скорости ¼ и так далее. Другая переменная, repeatvalue (значение повтора), определяет, сколько времени вам понадобится для воспроизведения анимации. Единица означает, что она будет сыграна только один раз перед воспроизведением следующей, а если равна 2, то она будет сыграна дважды и так далее. Каждая из этих переменных должна быть использована в предыдущем изображении или анимации.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 |
void loop() { if (hallSensor1State || hallSensor2State) { if (anim == 0) { DisplayFrame(CDArrays[frame]); // Increment the counter frameHoldCounter++; // Check if it's time to move to the next frame if (frameHoldCounter >= frameHoldTime) { // Reset the counter frameHoldCounter = 0; // Move to the next frame frame++; if (frame > 21) { frame = 0; repeat++; if (repeat == 1) { repeat = 0; anim++; frameHoldTime = 1; repeatvalue = 1; } } } } if (anim == 1) { DisplayFrame(ImageCD_22); // Increment the counter frameHoldCounter++; // Check if it's time to move to the next frame if (frameHoldCounter >= frameHoldTime) { // Reset the counter frameHoldCounter = 0; // Move to the next frame frame++; if (frame > 50) { frame = 0; repeat++; if (repeat == repeatvalue) { repeat = 0; anim++; frameHoldTime = 2; repeatvalue = 1; } } } } if (anim == 2) { DisplayFrame(ViasionArrays[frame]); // Increment the counter frameHoldCounter++; // Check if it's time to move to the next frame if (frameHoldCounter >= frameHoldTime) { // Reset the counter frameHoldCounter = 0; // Move to the next frame frame++; if (frame > 21) { frame = 0; repeat++; if (repeat == repeatvalue) { repeat = 0; anim++; frameHoldTime = 1; repeatvalue = 1; } } } } if (anim == 3) { DisplayFrame(Image_Viasion22); // Increment the counter frameHoldCounter++; // Check if it's time to move to the next frame if (frameHoldCounter >= frameHoldTime) { // Reset the counter frameHoldCounter = 0; // Move to the next frame frame++; if (frame > 50) { frame = 0; repeat++; if (repeat == repeatvalue) { repeat = 0; anim++; frameHoldTime = 2; repeatvalue = 1; } } } } if (anim == 4) { DisplayFrame(ViasionOTRArrays[frame]); // Increment the counter frameHoldCounter++; // Check if it's time to move to the next frame if (frameHoldCounter >= frameHoldTime) { // Reset the counter frameHoldCounter = 0; // Move to the next frame frame++; if (frame > 11) { frame = 0; repeat++; if (repeat == repeatvalue) { repeat = 0; anim++; frameHoldTime = 1; repeatvalue = 10; } } } } if (anim == 5) { DisplayFrame(CatRunArray[frame]); // Increment the counter frameHoldCounter++; // Check if it's time to move to the next frame if (frameHoldCounter >= frameHoldTime) { // Reset the counter frameHoldCounter = 0; // Move to the next frame frame++; if (frame > 9) { frame = 0; repeat++; if (repeat == repeatvalue) { repeat = 0; anim++; frameHoldTime = 2; repeatvalue = 2; } } } } else if (anim == 6) { DisplayFrame(CatArrays[frame]); // Increment the counter frameHoldCounter++; // Check if it's time to move to the next frame if (frameHoldCounter >= frameHoldTime) { // Reset the counter frameHoldCounter = 0; // Move to the next frame frame++; if (frame > 61) { frame = 0; repeat++; if (repeat == repeatvalue) { repeat = 0; anim++; frameHoldTime = 2; repeatvalue = 3; } } } } else if (anim == 7) { DisplayFrame(RunningSFArrays[frame]); // Increment the counter frameHoldCounter++; // Check if it's time to move to the next frame if (frameHoldCounter >= frameHoldTime) { // Reset the counter frameHoldCounter = 0; // Move to the next frame frame++; if (frame > 11) { frame = 0; repeat++; if (repeat == repeatvalue) { repeat = 0; anim++; frameHoldTime = 2; repeatvalue = 1; } } } } else if (anim == 8) { DisplayFrame(Dance1Arrays[frame]); // Increment the counter frameHoldCounter++; // Check if it's time to move to the next frame if (frameHoldCounter >= frameHoldTime) { // Reset the counter frameHoldCounter = 0; // Move to the next frame frame++; if (frame > 93) { frame = 0; repeat++; if (repeat == repeatvalue) { repeat = 0; anim++; frameHoldTime = 1; repeatvalue = 4; } } } } else if (anim == 9) { DisplayFrame(EYEArrays[frame]); // Increment the counter frameHoldCounter++; // Check if it's time to move to the next frame if (frameHoldCounter >= frameHoldTime) { // Reset the counter frameHoldCounter = 0; // Move to the next frame frame++; if (frame > 73) { frame = 0; repeat++; if (repeat == repeatvalue) { repeat = 0; anim++; frameHoldTime = 2; repeatvalue = 4; } } } } else if (anim == 10) { DisplayFrame(GlobexArrays[frame]); // Increment the counter frameHoldCounter++; // Check if it's time to move to the next frame if (frameHoldCounter >= frameHoldTime) { // Reset the counter frameHoldCounter = 0; // Move to the next frame frame++; if (frame > 14) { frame = 0; repeat++; if (repeat == repeatvalue) { repeat = 0; anim++; frameHoldTime = 20; repeatvalue = 1; } } } } else if (anim == 11) { DisplayFrame(ClockArrays[frame]); // Increment the counter frameHoldCounter++; // Check if it's time to move to the next frame if (frameHoldCounter >= frameHoldTime) { // Reset the counter frameHoldCounter = 0; // Move to the next frame frame++; if (frame > 13) { frame = 0; repeat++; if (repeat == repeatvalue) { repeat = 0; anim = 0; frameHoldTime = 2; repeatvalue = 1; } } } } hallSensor1State = 0; hallSensor2State = 0; } } |
При компиляции обязательно выберите большое приложение без OTA в качестве формата раздела, поскольку код содержит множество анимаций и эффектов и требует немного места для кода.
Файлы для проекта
Все необходимые файлы для создания этого проекта вы можете скачать по следующей ссылке.
Исходный код программы
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 |
/* * Project Name: POV display * Project Brief: Firmware for ESP32 POV Display. Display resolution 128 pixels * Author: Jobit Joseph * Copyright © Jobit Joseph * Copyright © Semicon Media Pvt Ltd * Copyright © Circuitdigest.com * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, in version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ #include <Arduino.h> #include <MultiShiftRegister.h> #include "Images.h" #include "Precompute.h" // Pin Definitions #define latchPin 14 #define clockPin 15 #define dataPin 13 #define latchPinx 17 #define clockPinx 18 #define dataPinx 16 #define HALL_SENSOR1_PIN 36 #define HALL_SENSOR2_PIN 39 // Global Variables volatile unsigned long lastHallTrigger = 0; volatile float rotationTime = 0; // Time for one rotation in milliseconds int hallSensor1State = 0; int hallSensor2State = 0; bool halfFrame = false; int numberOfRegisters = 8; int offset = 270; int repeatvalue = 1; int hys = 3000; int frame = 0; int repeat = 0; int anim = 0; int frameHoldTime = 1; // Number of loops to hold each frame int frameHoldCounter = 0; // Counter to track loops for current frame //Shift register Driver instances for both hands MultiShiftRegister msr(numberOfRegisters, latchPin, clockPin, dataPin); MultiShiftRegister msrx(numberOfRegisters, latchPinx, clockPinx, dataPinx); //Function to calculate polar co-ordintes, and get corresponding data from the arrays. int getValueFromAngle(const uint8_t arrayName[][16], int angle, int radius) { // Adjust the angle by subtracting offset to rotate counter-clockwise int adjustedAngle = angle - offset; if (adjustedAngle < 0) adjustedAngle += 360; // Ensure the angle stays within 0-359 degrees // Invert the targetX calculation to flip the image horizontally int targetX = 127 - precomputedCos[radius][adjustedAngle]; // Flipping targetX int targetY = precomputedSin[radius][adjustedAngle]; if (targetX >= 0 && targetX < 128 && targetY >= 0 && targetY < 128) { int byteIndex = targetX / 8; int bitIndex = 7 - (targetX % 8); return (arrayName[targetY][byteIndex] >> bitIndex) & 1; // Extract the bit value and return it } else { return -1; // Out of bounds } } //Hall sensor 1 interrupt routine void ISR_HallSensor1() { unsigned long currentTime = micros(); // Check if HYS ms have passed since the last trigger if (currentTime - lastHallTrigger >= hys) { rotationTime = (currentTime - lastHallTrigger) / 1000.0; lastHallTrigger = currentTime; hallSensor1State = 1; halfFrame = true; } } //Hall sensor 2 interrupt routine void ISR_HallSensor2() { unsigned long currentTime = micros(); // Check if HYS ms have passed since the last trigger if (currentTime - lastHallTrigger >= hys) { lastHallTrigger = currentTime; hallSensor2State = 1; halfFrame = false; } } //Function to calculate RPM and display each frame accordingly void DisplayFrame(const uint8_t ImageName[][16]) { float timePerSegment = rotationTime / 360.0; // Assuming 180 segments per half rotation for (int i = 0; i < 180; i++) { unsigned long segmentStartTime = micros(); for (int j = 0; j < 64; j++) { // First arm (msr) displays the first half of the frame if (getValueFromAngle(ImageName, i + (halfFrame ? 0 : 180), j)) { msr.set(j); } else { msr.clear(j); } // Second arm (msrx) displays the second half of the frame if (getValueFromAngle(ImageName, i + (halfFrame ? 180 : 0), j)) { msrx.set(j); } else { msrx.clear(j); } } msr.shift(); msrx.shift(); while (micros() - segmentStartTime < timePerSegment * 1000) ; } } void setup() { // Initialize pins pinMode(latchPin, OUTPUT); pinMode(clockPin, OUTPUT); pinMode(dataPin, OUTPUT); pinMode(latchPinx, OUTPUT); pinMode(clockPinx, OUTPUT); pinMode(dataPinx, OUTPUT); pinMode(HALL_SENSOR1_PIN, INPUT); pinMode(HALL_SENSOR2_PIN, INPUT); attachInterrupt(digitalPinToInterrupt(HALL_SENSOR1_PIN), ISR_HallSensor1, RISING); attachInterrupt(digitalPinToInterrupt(HALL_SENSOR2_PIN), ISR_HallSensor2, RISING); for (int i = 0; i < 64; i++) { msr.clear(i); msrx.clear(i); } msr.shift(); msrx.shift(); Serial.begin(115200); } void loop() { if (hallSensor1State || hallSensor2State) { if (anim == 0) { DisplayFrame(CDArrays[frame]); // Increment the counter frameHoldCounter++; // Check if it's time to move to the next frame if (frameHoldCounter >= frameHoldTime) { // Reset the counter frameHoldCounter = 0; // Move to the next frame frame++; if (frame > 21) { frame = 0; repeat++; if (repeat == 1) { repeat = 0; anim++; frameHoldTime = 1; repeatvalue = 1; } } } } if (anim == 1) { DisplayFrame(ImageCD_22); // Increment the counter frameHoldCounter++; // Check if it's time to move to the next frame if (frameHoldCounter >= frameHoldTime) { // Reset the counter frameHoldCounter = 0; // Move to the next frame frame++; if (frame > 50) { frame = 0; repeat++; if (repeat == repeatvalue) { repeat = 0; anim++; frameHoldTime = 2; repeatvalue = 1; } } } } if (anim == 2) { DisplayFrame(ViasionArrays[frame]); // Increment the counter frameHoldCounter++; // Check if it's time to move to the next frame if (frameHoldCounter >= frameHoldTime) { // Reset the counter frameHoldCounter = 0; // Move to the next frame frame++; if (frame > 21) { frame = 0; repeat++; if (repeat == repeatvalue) { repeat = 0; anim++; frameHoldTime = 1; repeatvalue = 1; } } } } if (anim == 3) { DisplayFrame(Image_Viasion22); // Increment the counter frameHoldCounter++; // Check if it's time to move to the next frame if (frameHoldCounter >= frameHoldTime) { // Reset the counter frameHoldCounter = 0; // Move to the next frame frame++; if (frame > 50) { frame = 0; repeat++; if (repeat == repeatvalue) { repeat = 0; anim++; frameHoldTime = 2; repeatvalue = 1; } } } } if (anim == 4) { DisplayFrame(ViasionOTRArrays[frame]); // Increment the counter frameHoldCounter++; // Check if it's time to move to the next frame if (frameHoldCounter >= frameHoldTime) { // Reset the counter frameHoldCounter = 0; // Move to the next frame frame++; if (frame > 11) { frame = 0; repeat++; if (repeat == repeatvalue) { repeat = 0; anim++; frameHoldTime = 1; repeatvalue = 10; } } } } if (anim == 5) { DisplayFrame(CatRunArray[frame]); // Increment the counter frameHoldCounter++; // Check if it's time to move to the next frame if (frameHoldCounter >= frameHoldTime) { // Reset the counter frameHoldCounter = 0; // Move to the next frame frame++; if (frame > 9) { frame = 0; repeat++; if (repeat == repeatvalue) { repeat = 0; anim++; frameHoldTime = 2; repeatvalue = 2; } } } } else if (anim == 6) { DisplayFrame(CatArrays[frame]); // Increment the counter frameHoldCounter++; // Check if it's time to move to the next frame if (frameHoldCounter >= frameHoldTime) { // Reset the counter frameHoldCounter = 0; // Move to the next frame frame++; if (frame > 61) { frame = 0; repeat++; if (repeat == repeatvalue) { repeat = 0; anim++; frameHoldTime = 2; repeatvalue = 3; } } } } else if (anim == 7) { DisplayFrame(RunningSFArrays[frame]); // Increment the counter frameHoldCounter++; // Check if it's time to move to the next frame if (frameHoldCounter >= frameHoldTime) { // Reset the counter frameHoldCounter = 0; // Move to the next frame frame++; if (frame > 11) { frame = 0; repeat++; if (repeat == repeatvalue) { repeat = 0; anim++; frameHoldTime = 2; repeatvalue = 1; } } } } else if (anim == 8) { DisplayFrame(Dance1Arrays[frame]); // Increment the counter frameHoldCounter++; // Check if it's time to move to the next frame if (frameHoldCounter >= frameHoldTime) { // Reset the counter frameHoldCounter = 0; // Move to the next frame frame++; if (frame > 93) { frame = 0; repeat++; if (repeat == repeatvalue) { repeat = 0; anim++; frameHoldTime = 1; repeatvalue = 4; } } } } else if (anim == 9) { DisplayFrame(EYEArrays[frame]); // Increment the counter frameHoldCounter++; // Check if it's time to move to the next frame if (frameHoldCounter >= frameHoldTime) { // Reset the counter frameHoldCounter = 0; // Move to the next frame frame++; if (frame > 73) { frame = 0; repeat++; if (repeat == repeatvalue) { repeat = 0; anim++; frameHoldTime = 2; repeatvalue = 4; } } } } else if (anim == 10) { DisplayFrame(GlobexArrays[frame]); // Increment the counter frameHoldCounter++; // Check if it's time to move to the next frame if (frameHoldCounter >= frameHoldTime) { // Reset the counter frameHoldCounter = 0; // Move to the next frame frame++; if (frame > 14) { frame = 0; repeat++; if (repeat == repeatvalue) { repeat = 0; anim++; frameHoldTime = 20; repeatvalue = 1; } } } } else if (anim == 11) { DisplayFrame(ClockArrays[frame]); // Increment the counter frameHoldCounter++; // Check if it's time to move to the next frame if (frameHoldCounter >= frameHoldTime) { // Reset the counter frameHoldCounter = 0; // Move to the next frame frame++; if (frame > 13) { frame = 0; repeat++; if (repeat == repeatvalue) { repeat = 0; anim = 0; frameHoldTime = 2; repeatvalue = 1; } } } } hallSensor1State = 0; hallSensor2State = 0; } } |