Текстовая анимация с помощью Arduino

Я видел много статей о том, как подключить панель ЖК-дисплея к Arduino, и статьи, которые помогают рисовать пользовательские символы, но я не видел таких, которые бы использовали функцию пользовательских символов для анимации.

Нам понадобится

Что вам понадобится для этого:
- Arduino Uno
- HD44780-совместимый ЖК-дисплей
- Макет-прототип
- Один резистор 10 кОм
- Один потенциометр 10 кОм
- Паяльники и паяльные материалы

Дополнительно, если вы припаяете ЖК-дисплей к отдельной плате:
- Еще одна небольшая макетная плата с, по меньшей мере, двумя рядами и шестнадцатью отверстиями в ряду (по желанию)
- 16-контактный разъем типа «мама» или что-то подобное (например, разъемы для чипа)

Даже если вы не припаяете ЖК-дисплей к отдельной плате, вам, вероятно, понадобится 16-штырьковый ряд штекерных разъемов типа «папа-папа», чтобы вы могли прикрепить свой ЖК-дисплей непосредственно к макету без пайки.

Зачем это небольшое хакерство нужно вообще? Конечно, здесь нет большой полезности, если делать таким образом для общих целей анимации. Для этого нужен настоящий ЖК-дисплей, а не основной текстовый дисплей. Тем не менее, общий метод может быть интересен тем, у кого есть ограниченный компонент отображения, и необходим какой-то анимированный индикатор прогресса или уровня.

Шаг 1: подключите ЖК-дисплей к макету

Шаг 1: подключите ЖК-дисплей к макету

Шаг 1: подключите ЖК-дисплей к макету

Вы можете найти другие инструкции, которая объясняет, как сделать этот шаг для подключения ЖК-панели к вашему Arduino. Прежде чем вы сможете это сделать, у вас должно быть несколько проводов или заглушек, прикрепленных к отверстиям штифта ЖК-панели.

Мне посчастливилось найти ЖК-монитор, подключенный к макету, который кто-то использовал. Это была небольшая штуковина с двумя рядами по семь ножек на одном конце, а провода на «RA» и «RK» на другом конце.

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

Если вы посмотрите на заднюю панель, вы увидите, что есть числа, обозначающие контакты 1, 2, 13 и 14, поэтому мне было ясно, где каждый вывод. Если вы подключаете по своему усмотрению, то я настоятельно рекомендую использовать провода разных цветов, чтобы вы ничего не перепутали. В первый раз, когда я так сделал, я переключал каждую пару проводов, начиная с провода 3, и должен был пройтись по всем.

Я аккуратно согнул провода и вставил голые контакты в макет. Сначала я проделал со всеми нечетными проводами, вставив их все в свои отверстия и припаяв контакты 1 и 13 только для того, чтобы все было на месте. Затем я вставил все провода с четными номерами и спаял 2 и 14. После стало проще и можно было уже припаять каждый по очереди.

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

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

После этого я проверил каждую пару, чтобы убедиться, что я не сделал никаких ошибок.

Шаг 2: Подключение цепей

Шаг 2: Подключение цепей

На этом этапе вы должны иметь ЖК-дисплей с контактами, которые можно легко подключить к макетной плате и схеме на ней. Аналогичные инструкции вы найдете на других веб-страницах.

Выполняйте все эти подключения перед подключением питания к Arduino! И проверьте, и перепроверьте, и трижды проверьте провода, которые идут на Vcc и Ground, чтобы убедиться, что вы соединили их в правильном порядке.

Короткое замыкание и обратная проводка могут сжечь вашу ЖК-панель, плюс сжечь контакты и повредить Arduino. В этой цепи нет резисторов и диодов, чтобы предотвратить негативные последствия. Поэтому, пожалуйста, будьте осторожны! Я не могу нести ответственность, если вы подключите неправильно, или вы используете неправильную панель ЖК-дисплея для этого.

Типичная схема 44780 соединяет выводы в этом порядке:

LCD 1 (ЖК-дисплей 1) = Земля
LCD 2 = Vcc (+ 5V)
LCD 3 = контракт - подключается к среднему выступу потенциометра; Соедините одну сторону с землей, а другой через резистор 10 кОм к Vcc.
LCD 4 = Сброс (reset) = Arduino 7
LCD 5 = Чтение / запись = связь с землей, так как записываем, не читаем
LCD 6 = Включить (Enable) = Arduino 8
LCD 7 через 14 = линии данных DB0..DB7, соответственно. Меня интересуют только последние четыре бита, поэтому connect
LCD 11 - Arduino 9
LCD 12 - Arduino 10
LCD 13 - Arduino 11
LCD 14 - Arduino 12

Из-за этих настроек код эскиза (скетча) должен инициализировать объект LiquidCrystal:

LiquidCrystal lcd(7, 8, 9, 10, 11, 12);
// parameters, in order, represent pins for RS, EN, DB4, DB5, DB6, DB7

(в моем прикрепленном изображении вы могли заметить, что я неправильно подключил потенциометр! Я исправил это после того, как сделал снимок!)

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

Шаг 3. Нарисуйте то, что вы анимируете

Шаг 3. Нарисуйте то, что вы анимируете

Чип HD44780 позволяет вам создавать и использовать до восьми пользовательских символов, а библиотека LiquidCrystal позволяет вам «создать» персонаж. Но функция createChar (index, byteArray) фактически заменяет биты определения символа, и если у вас есть какой-либо рисунок на экране, биты обновляются немедленно, поэтому это больше похоже на вызов «defineChar» или вызов «createOrUpdateChar» ,

В своей анимации я хотел нарисовать танк, проходящий через экран. Я решил нарисовать его как три символа на бумаге и оставить четвертый для смещения битов. Поскольку каждый символ имеет ширину в 5 пикселей и высоту в 8 пикселей, это дает мне площадь 15x8 пикселей для рисования танка.

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

Шаг 4: строим биты танка в коде

Я перевел чертеж танка в длинные целые числа. Длинные целые дают вам 32 бита и поэтому я могу представить танк, используя восемь длинных целых чисел, каждая из которых действительно использует только 15 бит для начального рисунка.

Кусок кода для этого есть здесь. Его легче читать шрифтом фиксированной ширины.

Я мог бы определить их, используя шестнадцатеричные значения, но я полагал, что использование двоичной нотации Bnnnnnn сделает ее более читаемой.

// Нужно восемь 32-битных величин, которые я могу использовать для смещения битов
// В этих значениях находится исходное изображение танка

long tankImg[] = {
  ((long)B010000 << 10) | ((long) B000000 << 5) | B000000 // Наконечник антенны
,((long)B010111 << 10) | ((long) B011110 << 5) | B000000 // Верх башни
,((long)B001111 << 10) | ((long) B011111 << 5) | B011110 // Средняя башня со стволом
,((long)B000111 << 10) | ((long) B011110 << 5) | B000000 // Основание башни
,((long)B001111 << 10) | ((long) B011111 << 5) | B010000 // Гусеница/протектор
,((long)B010000 << 10) | ((long) B000000 << 5) | B001000
,((long)B010000 << 10) | ((long) B000000 << 5) | B001000
,((long)B001111 << 10) | ((long) B011111 << 5) | B010000 // Нижняя часть протектора, общая длина 24 пикселя
};

Шаг 5: Смещение символов

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

Я также просто возьму по модулю позицию x танка (tankx % 5), чтобы выяснить, как сдвинуть биты. Если бы модуль был равен нулю, я рисовал бы символы и определял бы биты символов. Но если бы модуль был ненулевым, это означало, что символы уже на экране, и я могу просто переопределить биты. Я также отрисовал бы ведущее пространство, чтобы исключить любой предшествующий рисунок пользовательского символа 0 (самый левый), когда бы я «двигал» фигуру.

Итак, для начала я бы инициализировал позицию tankx до некоторой величины, которая бы четко разделилась на пять.

int tankx = -15;
int tankcharx;
int tankchary = 1;

Затем, внутри loop(), я бы сделал разделение и модуль, чтобы выяснить, что рисовать и как рисовать. (Остерегайтесь того, как работает модуль с отрицательными числами.)

Еще один способ сделать это, ретроспективно, состоял бы в том, чтобы сделать tankx всегда неотрицательным и сдвинуть позицию символа х на -3 перед рендерингом.

Это часть кода loop(), которая выводит на экран пользовательские символы. В лучшем исполнении этого кода, я превратил функции рисования в функцию safeDrawCharAt, которая выполняет проверку позиции. Но здесь вы видите более раннюю версию, где я выполнял проверку позиции. Так как в любое время символы могут быть за кадром, мне нужно было выполнить проверку позиции перед вызовами setCursor() или write().

tankcharx = tankx / BITS_PER_CHAR;
 
  // Initial rendition, no rotation of treads
  if ((tankx % BITS_PER_CHAR) == 0) {
    // Полный сдвиг включен, нужно нарисовать пробел, где последний раз был танк
    if (tankcharx >= 1) {
      lcd.setCursor(tankcharx-1, tankchary);
      lcd.write(' ');
    }
    if (tankcharx >= 0 && tankcharx <= 7) { lcd.setCursor(tankcharx, tankchary); lcd.write((byte)0); } if (tankcharx+1 >= 0 && tankcharx+1 <= 7) { lcd.setCursor(tankcharx+1, tankchary); lcd.write((byte)1); } if (tankcharx+2 >= 0 && tankcharx+2 <= 7) { lcd.setCursor(tankcharx+2, tankchary); lcd.write((byte)2); } if (tankcharx+3 >= 0 && tankcharx+3 <= 7) {
      lcd.setCursor(tankcharx+3, tankchary);
      lcd.write((byte)3);
    }
  }

Шаг 6: двигаем биты танка

Теперь, когда символы на экране, вы можете изменять их биты. В конечном счете, вам нужно вызвать функцию createChar(index, byteArray), чтобы получить биты на ЖК-дисплее, а это означает, что для каждого символа требуется массив из восьми байтов. Я использую четыре настраиваемых символа, поэтому я использовал четыре массива байт.

byte sprite0[8];
byte sprite1[8];
byte sprite2[8];
byte sprite3[8];

Внутри loop() я взял побитовое деление по модулю, чтобы получить смещение. Здесь эффективность длинных целых вступает в игру. Во-первых, я бы скопировал long int из статического танка в локальную переменную. Тогда я бы переложил всю long int обратно на количество бит, которое мне понадобится.

int shiftbits = (tankx % BITS_PER_CHAR);

  if (shiftbits < 0) { shiftbits += BITS_PER_CHAR; }
  for (int y=0 ; y<8; y++)
  {
    long lval = tankImg[y];
...
    long lshifted = lval << (BITS_PER_CHAR-shiftbits); sprite0[y] = (byte)((lshifted >> (3*BITS_PER_CHAR)) & B011111);
    sprite1[y] = (byte)((lshifted >> (2*BITS_PER_CHAR)) & B011111);
    sprite2[y] = (byte)((lshifted >> (1*BITS_PER_CHAR)) & B011111);
    sprite3[y] = (byte)((lshifted >> (0*BITS_PER_CHAR)) & B011111);
  }

Наконец, когда все байтовые массивы были вычислены, я направил их на LCD.

  lcd.createChar(0, sprite0);
  lcd.createChar(1, sprite1);
  lcd.createChar(2, sprite2);
  lcd.createChar(3, sprite3);

Если вы вычисляете это путем смещения байтов, вам придется убедиться, что вы «переносите» последний бит в следующий байт по горизонтали, но так как я остаюсь в пределах 32-битного long int с каждым раундом, процессор делает перенос бита для меня.

В конце цикла я перемещаю танк, а затем перебираю цикл до начала после того, как танк ушел за пределы экрана.

  ++tankx;
  if (tankx >= 50) { tankx = -15; }
  ...
  delay(100);

Наконец, вы видите задержку. Важно использовать задержку, которая подходит для вашего ЖК-дисплея. У моего LCD синий экран с белой подсветкой, и исчезает всё довольно медленно, поэтому короткая задержка будет в итоге очень размытой.

Шаг 7: Анимация гусениц танка

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

Чертеж танка должен был использовать эту конфигурацию для начала, но на самом деле это означало, что я мог сначала включить все пиксели протектора (внутри длинного массива int), а затем отключить каждый третий пиксель. В зависимости от позиции tankx, по модулю 3, я бы выключил другой набор пикселей.

Однако исходным требованием было то, что общее количество пикселей в протекторе было кратным трем. В противном случае анимация будет показывать пустые пиксели.

Таким образом, я определил отдельное положение treadx (на самом деле не позиция x, скорее как счетчик, который будет содержать циклы 0, 1, 2, 0, 1, 2, ...), и на основе его значений я бы выяснил, какой бит протектора выключить. На моем рисунке это произойдет только в самых нижних четырех строках «y», которые соответствуют значениям 4, 5, 6 и 7 .

Поскольку все биты протектора танка были запущены, я мог бы использовать функцию C XOR, чтобы отключить их. Я применил бы это изменение к длинным целым, прежде чем я переместился бы в свою 0...4 побитовую позицию сдвига. Это произойдет, естественно, до того, как длинные целые числа будут разбиты на байты.

int treadx = 0;

void loop() {
  ...
  for (int y=0 ; y<8; y++)
  {
    // ...  do the other processing of the turret up here

    // this section handles the tread animation
    switch (treadx) {
      case 0:
        switch (y) {
          case 4:
            lval ^= 0x2cb0; break;
          case 7:
            lval ^= 0x2490; break;
        }
        break;
      case 1:
        switch (y) {
          case 4:
//            lval ^= 0x1240; break;
            lval ^= 0x36d0; break;
          case 5:
            lval ^= 0x0008; break;
          case 6:
            lval ^= 0x4000; break;
          case 7:
            lval ^= 0x0920; break;
        }
        break;
      case 2:
        switch (y) {
          case 4:
//            lval ^= 0x0920; break;
            lval ^= 0x1b60; break;
          case 5:
            lval ^= 0x4000; break;
          case 6:
            lval ^= 0x0008; break;
          case 7:
            lval ^= 0x1240; break;
        }
        break;
    }
    // ... делатем сдвиг longs ints и byte break здесь
  } // Конец для каждой из восьми строк
  // ... вызовы lcd.write и lcd.createChar здесь
  ++treadx;
  if (treadx == 3) { treadx = 0; }
} // конец цикла функции

Шаг 8: смотрим как работает

Конечным результатом является танк, который перемещается по экрану.

Следует отметить, что разные ЖК-панели имеют разные физические схемы и характеристики. На этой плате имеется линия, ширина которой составляет пиксель между каждым символом, как по вертикали, так и по горизонтали. Дисплей имеет ширину 8 символов и высоту 2 символа. Я экспериментировал с различными настройками по модулю, позволяя компенсировать движение персонажа в каждом шестом сдвиге, а не в каждом пятом. Это то, с чем вам нужно играть, в зависимости от того, что вы пытаетесь сделать. Другая ЖК-панель, которую я получил от принтера HP, и она не имеет ширины одного пикселя, разделяющего ряды, но между каждым символом все еще есть расстояние.

Другое, чего нужно остерегаться - время затухания и контраст. Потенциометр позволяет устанавливать различные контрастные параметры, что облегчает просмотр пикселей. Время затухания может варьироваться между панелями. Чем дольше вы затухаете, тем больше задержка вам нужна, иначе вы столкнетесь с размытой анимацией. Кто-то, вероятно, найдет способ воспользоваться этим угасанием. Мне кажется, что может быть умный способ генерировать «серые» уровни путем быстрого включения и выключения пикселей, но только в том случае, если время действительно точное.

Наконец, с точки зрения кодирования, это может быть не самый эффективный способ что-то делать. Вместо этого вы можете предварительно визуализировать все биты башни и все комбинации бит протектора и даже предварительно сдвинуть их во все необходимые места. (Давайте посмотрим, это будет пять комбинаций для первых четырех линий, представляющих турель (башню), и трижды пять для смещенных по битам комбинаций протектора, умноженных на восемь длинных целых чисел, всего 5x8 + 3x5x8 = 4x5x8 = 160 long ints = 640 байт.

Фактический код на шаге 9 (см.ниже).

Шаг 9: Код танка для 44780 / RT0802B-1 Ver 2.0

// Этот код предназначен для 14-контактной панели ЖК-дисплея 44780 с двумя дополнительными выводами для подсветки анода / катода

#include 

LiquidCrystal lcd(7, 8, 9, 10, 11, 12);
  // RS, EN, DB4, DB5, DB6, DB7

// Board
// 1 = ground
// 2 = Vcc
// 3 = pot 10k-20k for contrast
// 4 = RS
// 5 = RW
// 6 = EN
// 11 = D4
// 12 = D5
// 13 = D6
// 14 = D7
// Search internet on 44780 for various pages that describe the wiring in great detail

// RW has to be wired low to write, else it remains in "read" mode

byte sprite0[8];
byte sprite1[8];
byte sprite2[8];
byte sprite3[8];

void setup() {
  // set up the LCD's number of columns and rows:
  lcd.begin(8, 2);
  lcd.setCursor(0,0);
  lcd.print("tankdemo");
  lcd.setCursor(0,1);
  lcd.print("@@@@@@@@");
  memset(sprite0,7,8);
  memset(sprite1,7,8);
  memset(sprite2,7,8);
  memset(sprite3,7,8);
  lcd.setCursor(0,1);
//  Serial.begin(9600);
}

// Need eight 32-bit quantities that I can use for shifting bits around.
// The original tank image is in these values.

long tankImg[] = {
  ((long)B010000 << 10) | ((long) B000000 << 5) | B000000 // antenna tip
,((long)B010111 << 10) | ((long) B011110 << 5) | B000000 // turret top
,((long)B001111 << 10) | ((long) B011111 << 5) | B011110 // turret mid with barrel
,((long)B000111 << 10) | ((long) B011110 << 5) | B000000 // turret base
,((long)B001111 << 10) | ((long) B011111 << 5) | B010000 // tread top
,((long)B010000 << 10) | ((long) B000000 << 5) | B001000
,((long)B010000 << 10) | ((long) B000000 << 5) | B001000
,((long)B001111 << 10) | ((long) B011111 << 5) | B010000 // tread bottom, 24 pixels total in tread }; // tankx is the bitwise position across the screen. // tankcharx is the character-wise position, thus tankx / 5. // It can be negative. // At tankx zero, the tank is on the left of the screen // so tankImg bytes are broken into four custom chars // the fourth of which being blank bits // At tankx one, the tank bits shift a bit to the right // and if I'm clever, the treads are computed so they "rotate" // And so on // Because there are five bits horizontally per custom char // and the tank treads go every other, I can repeat the original // tank treads starting at even char positions int tankx = -15; int tankcharx; int tankchary = 1; int treadx = 0; #define BITS_PER_CHAR 6 void loop() { tankcharx = tankx / BITS_PER_CHAR; // Serial.print("tankx = "); // Serial.print(tankx); // Serial.print(" tankcharx = "); // Serial.println(tankcharx); // Initial rendition, no rotation of treads if ((tankx % BITS_PER_CHAR) == 0) { // Full shift is on, need to draw a blank where the tank last was if (tankcharx >= 1) {
      lcd.setCursor(tankcharx-1, tankchary);
      lcd.write(' ');
    }
    if (tankcharx >= 0 && tankcharx <= 7) { lcd.setCursor(tankcharx, tankchary); lcd.write((byte)0); } if (tankcharx+1 >= 0 && tankcharx+1 <= 7) { lcd.setCursor(tankcharx+1, tankchary); lcd.write((byte)1); } if (tankcharx+2 >= 0 && tankcharx+2 <= 7) { lcd.setCursor(tankcharx+2, tankchary); lcd.write((byte)2); } if (tankcharx+3 >= 0 && tankcharx+3 <= 7) {
      lcd.setCursor(tankcharx+3, tankchary);
      lcd.write((byte)3);
    }
  }
  // Compute the bits of the individual custom chars
  int shiftbits = (tankx % BITS_PER_CHAR);
//  Serial.print("shiftbits = ");
//  Serial.println(shiftbits);
  if (shiftbits < 0) { shiftbits += BITS_PER_CHAR; }
  for (int y=0 ; y<8; y++)
  {
    long lval = tankImg[y];
    switch (treadx) {
      case 0:
        switch (y) {
          case 4:
            lval ^= 0x2cb0; break;
          case 7:
            lval ^= 0x2490; break;
        }
        break;
      case 1:
        switch (y) {
          case 4:
//            lval ^= 0x1240; break;
            lval ^= 0x36d0; break;
          case 5:
            lval ^= 0x0008; break;
          case 6:
            lval ^= 0x4000; break;
          case 7:
            lval ^= 0x0920; break;
        }
        break;
      case 2:
        switch (y) {
          case 4:
//            lval ^= 0x0920; break;
            lval ^= 0x1b60; break;
          case 5:
            lval ^= 0x4000; break;
          case 6:
            lval ^= 0x0008; break;
          case 7:
            lval ^= 0x1240; break;
        }
        break;
    }
   
    long lshifted = lval << (BITS_PER_CHAR-shiftbits); sprite0[y] = (byte)((lshifted >> (3*BITS_PER_CHAR)) & B011111);
    sprite1[y] = (byte)((lshifted >> (2*BITS_PER_CHAR)) & B011111);
    sprite2[y] = (byte)((lshifted >> (1*BITS_PER_CHAR)) & B011111);
    sprite3[y] = (byte)((lshifted >> (0*BITS_PER_CHAR)) & B011111);
  }
  lcd.createChar(0, sprite0);
  lcd.createChar(1, sprite1);
  lcd.createChar(2, sprite2);
  lcd.createChar(3, sprite3);
  ++tankx;
  if (tankx >= 50) { tankx = -15; }
  ++treadx;
  if (treadx == 3) { treadx = 0; }
  delay(100);
}

Ардуино+