Добрый день, Geeks, хочу сегодня рассказать про эту достаточно простую и, тем не менее, интересную LED-матрицу, которую я собрал еще в 2017 году в сентябре. Пришло время простым и понятным языком рассказать всем, кто хоть как-то знаком с электроникой и программированием, как сделать такое устройство.

image

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

Компоненты


Приступим. Первое, что нужно для разработки любого устройства – это, как минимум, подготовить все необходимые радиокомпоненты или хотя бы основные составляющие.
image
  1. Резисторы 150 ом 0.25 Ватт — 7 шт.
  2. Конденсаторы 50 вольт 1 микрофарад — 5 шт.
  3. Кварцевый резонатор 16 мгц — 1 шт.
  4. Разъём типа гребёнка ( потребуется 18 контактов ) — 1 шт.
  5. Сдвиговый регистр 74ch595 корпус DIP — 1 шт.
  6. Панель под микросхему 74ch595 корпус DIP (16 ножек) — 1 шт.
  7. Светодиоды 5мм 3 вольта — 35 шт.
  8. Микроконтроллер ATmega328p корпус DIP — 1 шт.
  9. Панель под микросхему ATmega328p корпус DIP (28 ножек) — 1 шт.
  10. Монтажная плата 5x7 — 1 шт.

Рекомендую при необходимости купить флюс, припой и паяльник.

Я намеренно не указываю марку проводов, которая вам подойдет, так как совсем не владею информацией об их параметрах. Могу посоветовать МГТФ, вполне возможно, что подойдут. Если вы знаете, какие провода точно оптимальны, оставьте информацию в комментариях или напишите мне в личные сообщения jsusdev.

Я не буду вам советовать какие-либо конкретные интернет магазины, потому что это было давно и не правда.

Сдвиговый регистр 74ch595


Наверное, многим новичкам станет не по себе от понимания принципов работы микросхемы 74ch595 вне этой статьи. Сейчас я попробую максимально доступно объяснить, как она работает и чем будет полезна в конкретном случае с моей LED-матрицей. 

Проще говоря, микросхема предназначена для увеличения количества цифровых пинов.

Распиновка. Внимание! Рисунок имеет незначительные неточности в маркировке контактов, это сделано для более простого усвоения и понимания работы.

image
Самые загадочные контакты управления, которые вызывают интерес: 

  • output pin * — контакты вывода
  • DS — (Serial Data Input) контакт который определяет состояние напряжения на контактах вывода
  • SH — (Shift Register Clock Input) контакт который записывает состояние которое определенно в DS
  • ST — (Storage Register Clock Input) контакт который открывает микросхему для записи и закрывает, устанавливая на контакты вывода нужные состояния определенные DS

Уверен, визуальный пример поможет вам осознать происходящее лучше.

image

Если нет, то я оставил и интерактивную версию:74ch595.swf, все кнопочки работают, можно понажимать.

Если вы осознали, как это работает, то единственное, что мне осталось, так это добавить пример кода для работы с микросхемой.

Этот код только для окончательного понимания как это работает программно. Код, который можно загрузить, будет в спойлере ниже.

int ST = 7;
int SH = 6;
int DS = 5;

digitalWrite(ST, 1); // начало записи в байт

  for (int i = 0; i < 8; i++) {

    digitalWrite(SH, 1); // начало записи бита 

      digitalWrite(DS, 1); // < --- последний аргумент это бит который будет записан

    digitalWrite(SH, 0); // конец записи бита

  }

digitalWrite(ST, 0); // конец записи в байт


Код который будет работать
int ST = 7;
int SH = 6;
int DS = 5;

void setup() {
  
  pinMode(7, 1);
  pinMode(6, 1);
  pinMode(5, 1);

}

void loop() {

  digitalWrite(ST, 1); // начало записи в байт
  
    for (int i = 0; i < 8; i++) {

      digitalWrite(SH, 1); // начало записи бита

        digitalWrite(DS, 1); // < --- последний аргумент это бит который будет записан

      digitalWrite(SH, 0); // конец записи бита

    }
  
  digitalWrite(ST, 0); // конец записи в байт

}


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

Подготовка к разработке матрицы


В моем случае светодиоды мешали друг другу на плате, и чтобы светодиоды «встали» на плату необходимо обточить мелкую окантовку, если конечно это требуется. Внимание! Нужно именно обточить, потому что есть еще немало способов от нее избавиться. Так как светодиоды на самом деле очень хрупкие, желательно обрабатывать их аккуратно, процесс выпаивания в случае чего мне ни разу не давался легко.
image
Хотелось бы обратить особое внимание на конденсаторы, я не могу объяснить их необходимость специальным языком, но без них светодиоды не затухают до конца и от этого устройство выглядит неоптимизированным, конденсаторы стабилизируют ток между контроллером и светодиодом.

image

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

Не стоит беспокоиться о количестве проводов на фото, далее покажу как все устроено.

image

Совсем забыл, и вы, наверное, упустили из внимания, что на устройстве визуально расположено всего 6 резисторов, но на самом деле их 7: один из них находится прямо под 74ch595.

image

Стоит добавить, что кварцевый генератор (16 МГц) можно заменить на 24 МГц, это существенно ускорит работу, но такие изменения применимы только к готовому устройству, так как ATmega328p просто не прошивается на кварцевом генераторе 24 МГц.

Разработка матрицы


Принципиальная схема устройства.
image

Внимание! Светодиоды расположены «минусом» к ATmega328p и в тоже время «плюсом» к 74ch595. Расположение компонентов на плате.
image
Обратная часть (вариант без конденсаторов)
image
3D-модель, которую можно повертеть в редакторе Google SketchUp.
Саму модель можно скачать тут: 3D_Schema.skp

Когда мы запаяли все компоненты, можно припаивать провода.
image
Стартуем:
image
Соединяем контакты rx, tx, reset и светодиоды. (светодиоды соединены отрезанными от них ножками):
image
Соединяем компоненты питание и кварцевый генератор:
image
Соединяем светодиоды с 74ch595:
image
Соединяем светодиоды с ATmega328p и ATmega328p и 74ch595:
image
Соединяем конденсаторы:
image
Поздравляю физическая часть закончена:
image
Наверняка устройство сразу не оживет без прошивки, но можно попробовать стандартный blink Arduino. (Да, в некоторых случаях действительно может заработать).

У меня Blink заработал. Если у вас хоть что-то на первом ряду будет мигать значит все ок, если нет – читаем дальше.

image

Разработка программы


Чтобы микроконтроллер управлял своими контактами, их надо «инициализировать».
Да, я мог сделать иначе, например, использовать массив и цикл, чтобы сократить количество кода в функции. Вообще, множество программных решений, которые я принял в процессе реализации, губительны с какой-то позиции, например, для программируемой памяти (Flash), но позитивны для динамической (RAM). Мне удалось снизить использование динамической памяти с 69% до 23%.
Хочу отдельно заметить, что отсутствие динамической памяти очень критично для устройства: оно просто может зависнуть в любой момент из-за нехватки памяти, в данном случае, динамической памяти всего 2 Кб, а программируемой 32 Кб.

Поэтому примите и смиритесь или сделайте лучше!


// функция инициализации 

void init() {

    pinMode(5, 1);
    pinMode(6, 1);
    pinMode(7, 1);
    pinMode(9, 1);
    pinMode(10, 1);
    pinMode(11, 1);
    pinMode(12, 1);
    pinMode(13, 1);
    
    digitalWrite(5,0);
    digitalWrite(6,0);
    digitalWrite(7,0);
    digitalWrite(9,0);
    digitalWrite(10,0);
    digitalWrite(11,0);
    digitalWrite(12,0);
    digitalWrite(13,0);
    
  };


Для управления матрицей я разработал такую функцию. Она является ключевой по сути.


void path(char bits[5]) {
    
    for (int l = 0;l < 5;l++) {
      
      digitalWrite(9, 1);
      digitalWrite(10, 1);  
      digitalWrite(11, 1);  // эта часть просто отключает пины
      digitalWrite(12, 1);    
      digitalWrite(13, 1);  

      if (l == 1) digitalWrite(13, 0);
      if (l == 2) digitalWrite(12, 0);
      if (l == 3) digitalWrite(11, 0); // эта часть включает пины в зависимости от условия
      if (l == 4) digitalWrite(10, 0);
      if (l == 0) digitalWrite(9, 0);

      
      // эта часть нужна для управления регистром, см. пункт выше
    
      digitalWrite(7, 1); 
        for (int i = 0; i < 8;i++) {
          digitalWrite(6, 1);
           digitalWrite(5, (bits[l] >> i) & 1);  

           /* (bits[l] >> i) 
            * такой вот способ преобразовывать 
            * из 10 в 2, спасибо 
            * чуваку который мне 
            * помог с этим условием  
            */

          digitalWrite(6, 0);
        }
      digitalWrite(7, 0);
      
      delay(2); // каким то образом эта строчка помогает избавиться мерцания
    }
  };
  

Каждый элемент массива – это одна колонка светодиодов.


char Matrx_symbol[5] = { 127, // 1
                         0,   // 2
                         0,   // 3
                         0,   // 4
                         0    // 5
                       };

path(Matrx_symbol);


Нумерация колонок идет справа налево, то есть зеркально массиву.
image
Что это за магические цифры в массиве?
image
Проще говоря, колонка светодиодов – это байт — 1 бит, вмещающий число в десятичной системе счисления от 0 до 127, этим числом мы определяем, каким светодиодам нужно зажечься. 

Конечно, мы не будем собирать свои символы путем грубого или осознанного перебора бит в байте. Я разработал страницу, на которой можно сформировать любой символ Symbol creater.

image

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

image

Функция, в которой определены символы. Изначально я планировал, что функцию можно будет совершенствовать, расширяя количество символов. Сейчас их 87, но я предполагал, что их может быть до 255. Но увы, наверное, в этом релизе нет, так как интерпретатор Arduino не определяет символы, код которых выше чем 127.

Буквально только что я об этом узнал в 5:31 по МСК

image

Функция, которая конвертирует строку в набор перебираемых символов, принимает два параметра:
  1. саму строку «Hellow world!»
  2. время между переключением символов в мс.


Функция которая конвертирует строку в набор перебираемых символов. Принимает два параметра это саму строку «Hellow world!» и вторым параметром время между переключением символов в мс.

Много кода

void message(String text, int Delay) {

    int len = text.length();

    for (int i = 0; i < len; i++) {      
     
      for (int Delay_ = 0; Delay_ < Delay/15; Delay_++){

       if (text[i] == 32) { char Symbol[5] = {0,0,0,0,0}; path(Symbol); } // 
       if (text[i] == 43) { char Symbol[5] = {8,8,62,8,8}; path(Symbol); } // +
       if (text[i] == 45) { char Symbol[5] = {8,8,8,8,8}; path(Symbol); } // -
       if (text[i] == 61) { char Symbol[5] = {20,20,20,20,20}; path(Symbol); } // =
       if (text[i] == 95) { char Symbol[5] = {1,1,1,1,1}; path(Symbol); } // _
       if (text[i] == 41) { char Symbol[5] = {62,65,0,0,0}; path(Symbol); } // )
       if (text[i] == 40) { char Symbol[5] = {0,0,0,65,62}; path(Symbol); } // (
       if (text[i] == 123) { char Symbol[5] = {0,65,65,54,8}; path(Symbol); } // {
       if (text[i] == 125) { char Symbol[5] = {8,54,65,65,0}; path(Symbol); } // }
       if (text[i] == 62) { char Symbol[5] = {8,20,34,65,0}; path(Symbol); } // >
       if (text[i] == 60) { char Symbol[5] = {0,65,34,20,8}; path(Symbol); } // <
       if (text[i] == 46) { char Symbol[5] = {0,0,1,0,0}; path(Symbol); } // .
       if (text[i] == 44) { char Symbol[5] = {0,0,3,2,0}; path(Symbol); } // ,
       if (text[i] == 63) { char Symbol[5] = {48,72,69,64,48}; path(Symbol); } // ?
       if (text[i] == 33) { char Symbol[5] = {0,0,125,0,0}; path(Symbol); } // !
       if (text[i] == 64) { char Symbol[5] = {58,69,93,65,62}; path(Symbol); } // @
       if (text[i] == 35) { char Symbol[5] = {20,127,20,127,20}; path(Symbol); } // #
       if (text[i] == 36) { char Symbol[5] = {38,73,127,73,50}; path(Symbol); } // $
       if (text[i] == 37) { char Symbol[5] = {19,11,4,50,49}; path(Symbol); } // %
       if (text[i] == 94) { char Symbol[5] = {0,32,64,32,0}; path(Symbol); } // ^
       if (text[i] == 58) { char Symbol[5] = {0,0,0,34,0}; path(Symbol); } // :
       if (text[i] == 42) { char Symbol[5] = {8,20,42,20,8}; path(Symbol); } // * 
       if (text[i] == 38) { char Symbol[5] = {2,53,73,73,54}; path(Symbol); } // &
       if (text[i] == 34) { char Symbol[5] = {0,112,0,112,0}; path(Symbol); } // "
       if (text[i] == 59) { char Symbol[5] = {0,0,0,38,2}; path(Symbol); } // ;
       if (text[i] == 48) { char Symbol[5] = {62,65,65,65,62}; path(Symbol); } // 0
       if (text[i] == 49) { char Symbol[5] = {0,1,127,33,16}; path(Symbol); } // 1
       if (text[i] == 50) { char Symbol[5] = {49,73,69,67,49}; path(Symbol); } // 2
       if (text[i] == 51) { char Symbol[5] = {54,73,73,65,34}; path(Symbol); } // 3
       if (text[i] == 52) { char Symbol[5] = {5,127,37,20,12}; path(Symbol); } // 4
       if (text[i] == 53) { char Symbol[5] = {6,73,73,73,114}; path(Symbol); } // 5
       if (text[i] == 54) { char Symbol[5] = {38,73,73,73,62}; path(Symbol); } // 6
       if (text[i] == 55) { char Symbol[5] = {96,80,79,64,64}; path(Symbol); } // 7
       if (text[i] == 56) { char Symbol[5] = {54,73,73,73,54}; path(Symbol); } // 8
       if (text[i] == 57) { char Symbol[5] = {62,73,73,73,50}; path(Symbol); } // 9
       if (text[i] == 97) { char Symbol[5] = {1,62,37,37,18}; path(Symbol); } // a
       if (text[i] == 65) { char Symbol[5] = {3,28,36,28,3}; path(Symbol); } // A
       if (text[i] == 98) { char Symbol[5] = {6,9,9,9,126}; path(Symbol); } // b
       if (text[i] == 66) { char Symbol[5] = {6,57,73,73,127}; path(Symbol); } // B
       if (text[i] == 99) { char Symbol[5] = {18,33,33,33,30}; path(Symbol); } // c
       if (text[i] == 67) { char Symbol[5] = {34,65,65,65,62}; path(Symbol); } // C
       if (text[i] == 100) { char Symbol[5] = {126,9,9,9,6}; path(Symbol); } // d
       if (text[i] == 68) { char Symbol[5] = {62,65,65,65,127}; path(Symbol); } // D
       if (text[i] == 101) { char Symbol[5] = {26,37,37,37,30}; path(Symbol); } // e
       if (text[i] == 69) { char Symbol[5] = {65,65,73,73,127}; path(Symbol); } // E
       if (text[i] == 102) { char Symbol[5] = {0,32,36,31,4}; path(Symbol); } // f
       if (text[i] == 70) { char Symbol[5] = {64,64,72,72,127}; path(Symbol); } // F
       if (text[i] == 103) { char Symbol[5] = {62,73,73,74,48}; path(Symbol); } // g
       if (text[i] == 71) { char Symbol[5] = {38,73,73,65,62}; path(Symbol); } // G 
       if (text[i] == 104) { char Symbol[5] = {0,7,8,8,63}; path(Symbol); } // h 
       if (text[i] == 72) { char Symbol[5] = {127,8,8,8,127}; path(Symbol); } // H
       if (text[i] == 105) { char Symbol[5] = {0,0,47,0,0}; path(Symbol); } // i
       if (text[i] == 73) { char Symbol[5] = {0,65,127,65,0}; path(Symbol); } // I
       if (text[i] == 106) { char Symbol[5] = {0,94,1,1,2}; path(Symbol); } // j
       if (text[i] == 74) { char Symbol[5] = {126,1,1,1,6}; path(Symbol); } // J
       if (text[i] == 107) { char Symbol[5] = {0,16,9,6,63}; path(Symbol); } // k
       if (text[i] == 75) { char Symbol[5] = {64,33,18,12,127}; path(Symbol); } // K
       if (text[i] == 108) { char Symbol[5] = {0,1,63,0,0}; path(Symbol); } // l
       //if (text[i] == 76) { char Symbol[5] = {1,1,1,1,127}; path(Symbol); } // L
       if (text[i] == 109) { char Symbol[5] = {15,16,24,16,15}; path(Symbol); } // m
       if (text[i] == 77) { char Symbol[5] = {63,64,56,64,63}; path(Symbol); } // M
       if (text[i] == 110) { char Symbol[5] = {31,32,32,32,63}; path(Symbol); } // n
       if (text[i] == 78) { char Symbol[5] = {127,4,8,16,127}; path(Symbol); } // N
       if (text[i] == 111) { char Symbol[5] = {14,17,17,17,14}; path(Symbol); } // o
       if (text[i] == 79) { char Symbol[5] = {62,65,65,65,62}; path(Symbol); } // O
       if (text[i] == 112) { char Symbol[5] = {0,24,36,36,63}; path(Symbol); } // p
       if (text[i] == 80) { char Symbol[5] = {56,68,68,68,63}; path(Symbol); } // P
       if (text[i] == 113) { char Symbol[5] = {31,36,36,36,24}; path(Symbol); } // q
       if (text[i] == 81) { char Symbol[5] = {61,66,65,65,62}; path(Symbol); } // Q
       if (text[i] == 114) { char Symbol[5] = {0,48,16,32,63}; path(Symbol); } // r
       if (text[i] == 82) { char Symbol[5] = {56,69,70,68,63}; path(Symbol); } // R
       if (text[i] == 115) { char Symbol[5] = {0,18,37,41,18}; path(Symbol); } // s
       //if (text[i] == 83) { char Symbol[5] = {38,73,73,73,50}; path(Symbol); } // S
       if (text[i] == 116) { char Symbol[5] = {0,2,17,62,16}; path(Symbol); } // t
       if (text[i] == 84) { char Symbol[5] = {64,64,127,64,64}; path(Symbol); }// T
       if (text[i] == 117) { char Symbol[5] = {29,2,1,1,30}; path(Symbol); } // u
       if (text[i] == 85) { char Symbol[5] = {126,1,1,1,126}; path(Symbol); } // U
       if (text[i] == 118) { char Symbol[5] = {60,2,1,2,60}; path(Symbol); } // v
       if (text[i] == 86) { char Symbol[5] = {124,2,1,2,124}; path(Symbol); } // V
       if (text[i] == 119) { char Symbol[5] = {30,1,6,1,30}; path(Symbol); } // w
       if (text[i] == 87) { char Symbol[5] = {126,1,6,1,126}; path(Symbol); } // W
       if (text[i] == 120) { char Symbol[5] = {17,10,4,10,17}; path(Symbol); } // x
       if (text[i] == 88) { char Symbol[5] = {99,20,8,20,99}; path(Symbol); } // X
       if (text[i] == 121) { char Symbol[5] = {56,4,6,57,0}; path(Symbol); } // y
       if (text[i] == 89) { char Symbol[5] = {112,8,7,8,112}; path(Symbol); } // Y
       if (text[i] == 122) { char Symbol[5] = {17,25,21,19,17}; path(Symbol); } // z
       if (text[i] == 90) { char Symbol[5] = {97,81,73,69,67}; path(Symbol); } // Z
       if (text[i] == 76) { char Symbol[5] = {24,60,30,60,24}; path(Symbol); } // L
       if (text[i] == 83) { char Symbol[5] = {6,113,1,113,6}; path(Symbol); } // S   
      
      }
    }


Теперь, когда мы «разобрали» принцип работы функций данной программы в контроллере, нужно его оформить.

Очень много кода

class Jsus {
  
  public:
  
  void init() {
    
    pinMode(5, 1);
    pinMode(6, 1);
    pinMode(7, 1);
    pinMode(9, 1);
    pinMode(10, 1);
    pinMode(11, 1);
    pinMode(12, 1);
    pinMode(13, 1);
    
    digitalWrite(5,0);
    digitalWrite(6,0);
    digitalWrite(7,0);
    digitalWrite(9,0);
    digitalWrite(10,0);
    digitalWrite(11,0);
    digitalWrite(12,0);
    digitalWrite(13,0);
    
  };
  
  void path(char bits[5]) {
    
    for (int l = 0;l < 5;l++) {
      
      digitalWrite(9, 1);
      digitalWrite(10, 1);  
      digitalWrite(11, 1);  
      digitalWrite(12, 1);    
      digitalWrite(13, 1);  

      if (l == 1) digitalWrite(13, 0);
      if (l == 2) digitalWrite(12, 0);
      if (l == 3) digitalWrite(11, 0);
      if (l == 4) digitalWrite(10, 0);
      if (l == 0) digitalWrite(9, 0);

      
      digitalWrite(7, 1);
        for (int i = 0; i < 8;i++) {
          digitalWrite(6, 1);
           digitalWrite(5, (bits[l] >> i) & 1);  
          digitalWrite(6, 0);
        }
      digitalWrite(7, 0);
      
      delay(2);
    }
  };
  
  void message(String text, int Delay) {

    int len = text.length();

    for (int i = 0; i < len; i++) {      
     
      for (int Delay_ = 0; Delay_ < Delay/15; Delay_++){

       if (text[i] == 32) { char Symbol[5] = {0,0,0,0,0}; path(Symbol); } // 
       if (text[i] == 43) { char Symbol[5] = {8,8,62,8,8}; path(Symbol); } // +
       if (text[i] == 45) { char Symbol[5] = {8,8,8,8,8}; path(Symbol); } // -
       if (text[i] == 61) { char Symbol[5] = {20,20,20,20,20}; path(Symbol); } // =
       if (text[i] == 95) { char Symbol[5] = {1,1,1,1,1}; path(Symbol); } // _
       if (text[i] == 41) { char Symbol[5] = {62,65,0,0,0}; path(Symbol); } // )
       if (text[i] == 40) { char Symbol[5] = {0,0,0,65,62}; path(Symbol); } // (
       if (text[i] == 123) { char Symbol[5] = {0,65,65,54,8}; path(Symbol); } // {
       if (text[i] == 125) { char Symbol[5] = {8,54,65,65,0}; path(Symbol); } // }
       if (text[i] == 62) { char Symbol[5] = {8,20,34,65,0}; path(Symbol); } // >
       if (text[i] == 60) { char Symbol[5] = {0,65,34,20,8}; path(Symbol); } // <
       if (text[i] == 46) { char Symbol[5] = {0,0,1,0,0}; path(Symbol); } // .
       if (text[i] == 44) { char Symbol[5] = {0,0,3,2,0}; path(Symbol); } // ,
       if (text[i] == 63) { char Symbol[5] = {48,72,69,64,48}; path(Symbol); } // ?
       if (text[i] == 33) { char Symbol[5] = {0,0,125,0,0}; path(Symbol); } // !
       if (text[i] == 64) { char Symbol[5] = {58,69,93,65,62}; path(Symbol); } // @
       if (text[i] == 35) { char Symbol[5] = {20,127,20,127,20}; path(Symbol); } // #
       if (text[i] == 36) { char Symbol[5] = {38,73,127,73,50}; path(Symbol); } // $
       if (text[i] == 37) { char Symbol[5] = {19,11,4,50,49}; path(Symbol); } // %
       if (text[i] == 94) { char Symbol[5] = {0,32,64,32,0}; path(Symbol); } // ^
       if (text[i] == 58) { char Symbol[5] = {0,0,0,34,0}; path(Symbol); } // :
       if (text[i] == 42) { char Symbol[5] = {8,20,42,20,8}; path(Symbol); } // * 
       if (text[i] == 38) { char Symbol[5] = {2,53,73,73,54}; path(Symbol); } // &
       if (text[i] == 34) { char Symbol[5] = {0,112,0,112,0}; path(Symbol); } // "
       if (text[i] == 59) { char Symbol[5] = {0,0,0,38,2}; path(Symbol); } // ;
       if (text[i] == 48) { char Symbol[5] = {62,65,65,65,62}; path(Symbol); } // 0
       if (text[i] == 49) { char Symbol[5] = {0,1,127,33,16}; path(Symbol); } // 1
       if (text[i] == 50) { char Symbol[5] = {49,73,69,67,49}; path(Symbol); } // 2
       if (text[i] == 51) { char Symbol[5] = {54,73,73,65,34}; path(Symbol); } // 3
       if (text[i] == 52) { char Symbol[5] = {5,127,37,20,12}; path(Symbol); } // 4
       if (text[i] == 53) { char Symbol[5] = {6,73,73,73,114}; path(Symbol); } // 5
       if (text[i] == 54) { char Symbol[5] = {38,73,73,73,62}; path(Symbol); } // 6
       if (text[i] == 55) { char Symbol[5] = {96,80,79,64,64}; path(Symbol); } // 7
       if (text[i] == 56) { char Symbol[5] = {54,73,73,73,54}; path(Symbol); } // 8
       if (text[i] == 57) { char Symbol[5] = {62,73,73,73,50}; path(Symbol); } // 9
       if (text[i] == 97) { char Symbol[5] = {1,62,37,37,18}; path(Symbol); } // a
       if (text[i] == 65) { char Symbol[5] = {3,28,36,28,3}; path(Symbol); } // A
       if (text[i] == 98) { char Symbol[5] = {6,9,9,9,126}; path(Symbol); } // b
       if (text[i] == 66) { char Symbol[5] = {6,57,73,73,127}; path(Symbol); } // B
       if (text[i] == 99) { char Symbol[5] = {18,33,33,33,30}; path(Symbol); } // c
       if (text[i] == 67) { char Symbol[5] = {34,65,65,65,62}; path(Symbol); } // C
       if (text[i] == 100) { char Symbol[5] = {126,9,9,9,6}; path(Symbol); } // d
       if (text[i] == 68) { char Symbol[5] = {62,65,65,65,127}; path(Symbol); } // D
       if (text[i] == 101) { char Symbol[5] = {26,37,37,37,30}; path(Symbol); } // e
       if (text[i] == 69) { char Symbol[5] = {65,65,73,73,127}; path(Symbol); } // E
       if (text[i] == 102) { char Symbol[5] = {0,32,36,31,4}; path(Symbol); } // f
       if (text[i] == 70) { char Symbol[5] = {64,64,72,72,127}; path(Symbol); } // F
       if (text[i] == 103) { char Symbol[5] = {62,73,73,74,48}; path(Symbol); } // g
       if (text[i] == 71) { char Symbol[5] = {38,73,73,65,62}; path(Symbol); } // G 
       if (text[i] == 104) { char Symbol[5] = {0,7,8,8,63}; path(Symbol); } // h 
       if (text[i] == 72) { char Symbol[5] = {127,8,8,8,127}; path(Symbol); } // H
       if (text[i] == 105) { char Symbol[5] = {0,0,47,0,0}; path(Symbol); } // i
       if (text[i] == 73) { char Symbol[5] = {0,65,127,65,0}; path(Symbol); } // I
       if (text[i] == 106) { char Symbol[5] = {0,94,1,1,2}; path(Symbol); } // j
       if (text[i] == 74) { char Symbol[5] = {126,1,1,1,6}; path(Symbol); } // J
       if (text[i] == 107) { char Symbol[5] = {0,16,9,6,63}; path(Symbol); } // k
       if (text[i] == 75) { char Symbol[5] = {64,33,18,12,127}; path(Symbol); } // K
       if (text[i] == 108) { char Symbol[5] = {0,1,63,0,0}; path(Symbol); } // l
       //if (text[i] == 76) { char Symbol[5] = {1,1,1,1,127}; path(Symbol); } // L
       if (text[i] == 109) { char Symbol[5] = {15,16,24,16,15}; path(Symbol); } // m
       if (text[i] == 77) { char Symbol[5] = {63,64,56,64,63}; path(Symbol); } // M
       if (text[i] == 110) { char Symbol[5] = {31,32,32,32,63}; path(Symbol); } // n
       if (text[i] == 78) { char Symbol[5] = {127,4,8,16,127}; path(Symbol); } // N
       if (text[i] == 111) { char Symbol[5] = {14,17,17,17,14}; path(Symbol); } // o
       if (text[i] == 79) { char Symbol[5] = {62,65,65,65,62}; path(Symbol); } // O
       if (text[i] == 112) { char Symbol[5] = {0,24,36,36,63}; path(Symbol); } // p
       if (text[i] == 80) { char Symbol[5] = {56,68,68,68,63}; path(Symbol); } // P
       if (text[i] == 113) { char Symbol[5] = {31,36,36,36,24}; path(Symbol); } // q
       if (text[i] == 81) { char Symbol[5] = {61,66,65,65,62}; path(Symbol); } // Q
       if (text[i] == 114) { char Symbol[5] = {0,48,16,32,63}; path(Symbol); } // r
       if (text[i] == 82) { char Symbol[5] = {56,69,70,68,63}; path(Symbol); } // R
       if (text[i] == 115) { char Symbol[5] = {0,18,37,41,18}; path(Symbol); } // s
       //if (text[i] == 83) { char Symbol[5] = {38,73,73,73,50}; path(Symbol); } // S
       if (text[i] == 116) { char Symbol[5] = {0,2,17,62,16}; path(Symbol); } // t
       if (text[i] == 84) { char Symbol[5] = {64,64,127,64,64}; path(Symbol); }// T
       if (text[i] == 117) { char Symbol[5] = {29,2,1,1,30}; path(Symbol); } // u
       if (text[i] == 85) { char Symbol[5] = {126,1,1,1,126}; path(Symbol); } // U
       if (text[i] == 118) { char Symbol[5] = {60,2,1,2,60}; path(Symbol); } // v
       if (text[i] == 86) { char Symbol[5] = {124,2,1,2,124}; path(Symbol); } // V
       if (text[i] == 119) { char Symbol[5] = {30,1,6,1,30}; path(Symbol); } // w
       if (text[i] == 87) { char Symbol[5] = {126,1,6,1,126}; path(Symbol); } // W
       if (text[i] == 120) { char Symbol[5] = {17,10,4,10,17}; path(Symbol); } // x
       if (text[i] == 88) { char Symbol[5] = {99,20,8,20,99}; path(Symbol); } // X
       if (text[i] == 121) { char Symbol[5] = {56,4,6,57,0}; path(Symbol); } // y
       if (text[i] == 89) { char Symbol[5] = {112,8,7,8,112}; path(Symbol); } // Y
       if (text[i] == 122) { char Symbol[5] = {17,25,21,19,17}; path(Symbol); } // z
       if (text[i] == 90) { char Symbol[5] = {97,81,73,69,67}; path(Symbol); } // Z
       if (text[i] == 76) { char Symbol[5] = {24,60,30,60,24}; path(Symbol); } // L
       if (text[i] == 83) { char Symbol[5] = {6,113,1,113,6}; path(Symbol); } // S   
      
      }
    }
  };  
  
};

Jsus matrix;



Код для старта.


void setup() {

  matrix.init();

}


void loop() {

   matrix.message("Fuck RKN!", 400);

}


Тут можно найти готовый к загрузке в Arduino cкетч.

Результаты разработки


2018. Текущая версия устройства:



2017. Текущая версия устройства (не оптимизированная):



2016. Первая тестовая версия устройства. Еще даже без микросхем на плате:



Все необходимые ссылки можно найти в репозитории на Github проекта: Arduino-matrix-module.

Если статья не ответила на какие-то вопросы реализации, пишите их в комментариях.

Всё, вроде ничего не забыл. Поки чмоки лавки лавки.

Комментарии (41)


  1. tretyakovmax
    20.04.2018 14:56

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


  1. blindmen
    20.04.2018 15:11

    влепите ему плюсов! это самая наглядная статья для чайников из всех что я когда либо видел!


    1. Gryphon88
      20.04.2018 17:13

      В качестве бонуса ещё бы схему без МК, а с кнопками как на гифке, чтоб самые маленькие могли пощупать шайтан-машину — сдвиговый регистр.


  1. JsusDev Автор
    20.04.2018 15:11

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


  1. Alozar
    20.04.2018 15:21

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


    1. GennPen
      20.04.2018 16:20

      Одна перемычка под микросхемой, которую и вовсе не видно если смотреть с эстетической стороны.


  1. madf
    20.04.2018 16:01
    +1

    Опыт — это хорошо, но сомнительность данного применения в виде "как есть".


  1. Mike_soft
    20.04.2018 16:13
    +1

    Светодиоды есть и в цилиндрических корпусах (в т.ч. и с плоским торцом), и в «квадратном» корпусе. в чем сакральный смысл «обточки»?


    1. JsusDev Автор
      20.04.2018 16:37

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


    1. JsusDev Автор
      20.04.2018 16:43

      Принял к сведению, исправил.


  1. Z55
    20.04.2018 16:38

    Такого варианта «помигать светодиодом» я ещё не видел, Зачёт!!! ))


  1. Nibi
    20.04.2018 16:38
    +2

    Электролитические конденсаторы прямо на портах МК? Зачем так издеваться над МК? Это же большой импульсный ток, рано или поздно пины повыгорают.


  1. Gryphon88
    20.04.2018 17:03

    Спасибо за наглядный туториал.
    Вы в SketchUp модель выгружали или прям в нём делали? Люблю его за простоту, корпуса в нём прикидывать хорошо, но вот рисовать плату с нуля (без готовых компонентов) я ленился.

    И вопрос к знатокам Arduino: код вида

          digitalWrite(9, 1);
          digitalWrite(10, 1);  
          digitalWrite(11, 1);
          digitalWrite(12, 1);    
          digitalWrite(13, 1);  
    преобразуется в маски по числу задействованных регистров, или по-пионерски — сколько строк, столько отдельных команд?


    1. JsusDev Автор
      20.04.2018 17:14

      Почти все компоненты рисовал прямо в SketchUp, кроме платы ее просто наложил на блок как текстуру.


    1. GarryC
      20.04.2018 17:26

      «А вы то сами как думаете?»
      Если хотите по-взрослому, то Александреску Вам в помощь, а в самой Ардуино все по-пионерски.


      1. Gryphon88
        20.04.2018 17:33

        Сурово. Я думал, что за столько-то лет разработки базовые функции оптимизировали.
        Александреску я не осилил толком, но такие вещи и на макросах можно разматывать.


        1. Alex_ME
          21.04.2018 12:19

          Хорошая статья на тему: Работа с портами ввода-вывода микроконтроллеров на Си++.
          С Variadic Templates всё становится без черной магии с меньшим количеством черной магии.


          Может быть, еще можно через C++14 constexpr сделать, циклами. Тогда будет совсем без магии. Хотя, там вроде функции без побочных эффектов должны быть? Я еще в этой теме не совсем разобрался.


          1. Gryphon88
            21.04.2018 15:10

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


          1. Alex_ME
            21.04.2018 15:41

            Кстати, кто-нибудь знает способ, как убедится, что constexpr-функция действительно constexpr? Я пытался смотреть дизассемблерный код, но там компилятор (ARM Compiler 6) и так инлайнит. А без оптимизации он вроде и constexpr отказывается делать… Может, ошибаюсь.


  1. FGV
    20.04.2018 17:24

    вам порты меги не жалко?
    1) при зажигании 7 светодиодов нагрузка на порт будет = 7*(5-2)/150 = 140мА (по даташиту если память не изменяет память на пин порта около 20мА максимум);
    2) акромя вкорячивания 140мА в светодиоды бонусом еще надо заряжать электролиты, т.е. еще дополнительная токовая нагрузка.


    1. GarryC
      20.04.2018 17:28

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


      1. FGV
        20.04.2018 17:43

        хм. динамическая индикация вроде как, тут электролиты как раз мешают. И на верхнее плечо лучше поставить таки нормальные транзисторные ключи (это убережет мегу от токового изнасилования).
        виной тому кстати криво написаный код + адурина, как сделано:
        1) гасим индикатор (выкл. все линии)
        2) вдвигаем данные в регистр
        3) щелкаем регистром (введенные данные появляются на выходе)
        4) включаем линию.
        п.2 — самые затратный по времени, по идее надо так:
        1) вдвигаем данные в регистр
        2) гасим индикатор (выкл. все линии)
        3) щелкаем регистром (введенные данные появляются на выходе)
        4) включаем линию.


        1. FGV
          20.04.2018 18:09

          правильное динамо

          и никаких конденсаторов не надо.


    1. mickvav
      20.04.2018 18:08

      Ну, если автор маленько перегрел конденсаторы при пайке или взял БУ-ные, то всё не так страшно. Да и с другой стороны диодов у него сдвиговый регистр — там тоже немного просядет (хотя микроконтроллер сдохнет первым, конечно).


      1. JsusDev Автор
        20.04.2018 23:53

        Да конденсаторы БУ причем из какой то совсем старой техники.


    1. JsusDev Автор
      20.04.2018 23:52

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

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


      1. GarryC
        23.04.2018 11:35

        Дело не в компетенции, просто есть вещи, которые следует делать не задумываясь — ставить ограничивающие ток резисторы или умощняющие транзисторы. И только хорошенько подумав и убедившись, что все правильно получается, можно их не ставить. Ну а аргумент «ведь работает же» — это не для инженерного дела (надеюсь, мы именно им занимаемся).


        1. Mike_soft
          23.04.2018 11:57

          в журнале Микропроцессорные средства и системы году в 1985 был шутливый тест «являетесь ли вы инженером» (у них вообще на последнем развороте был раздел юмора). ну и там вопрос типа:
          «каково напряжение питания микросхемы ***» с вариантами ответов
          1)«5 вольт»
          2)«9 вольт»
          3)«я подал 12, и до сих пор работает»…


  1. GarryC
    20.04.2018 17:31

    Вообще то, после таких программ (смотри листинги в посте) я начинаю понимать Ардуино-хэйтеров, хотя сам к ним не отношусь.


  1. neochapay
    20.04.2018 17:48

    Платку бы развести… для версии 3.0


  1. ionicman
    20.04.2018 17:54

    А чего не готовый модуль с Али за 1.7$?


    1. JsusDev Автор
      21.04.2018 00:02

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

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


  1. Alex_ME
    20.04.2018 18:05

    Однажды подарил младшему брату какой-то starter kit Arduino, и там была светодиодная матрица, с которой я поигрался. В итоге получилось что-то такое (извиняйте, что не на гитхабе). JsusDev, посмотрите, если интересно. Может найдете что-то полезное для себя.


    Запись паттерна осуществляется в буфер, который отображается на матрице с помощью динамической индикации на таймере и не требует никаких дополнительных телодвижений в основной программе, помимо вкл/выкл светодиодов или записи буфера. Также сверху прикрутил возможность плавного сдвига одного паттерна на другой, а уже на этом реализовал бегущую строку.


    • Код под ATmega 2560, отличается отображение регистров на пины ардуины и, возможно, инициализация таймера
    • Также у 2560 меги куча ОЗУ, и поэтому я забыл указать, чтобы хранение символов было во флеше. Напрямую в 328 не влезет. Прочитать про это: 1, 2


    1. JsusDev Автор
      21.04.2018 00:04

      Спасибо, принял к сведенью обязательно изучу.


  1. quwy
    21.04.2018 01:38

    А зачем тут вообще регистр сдвига? Даже при использовании кварца имеем 18 свободных линий GPIO (23 всего — 2 XTAL — 2 UART — 1 RESET = 18), а ваша матрица 5x7 требует 12 линий.


    1. Gryphon88
      21.04.2018 02:19

      По-моему, эта статья туториал в духе «дети, существует сдвиговый регистр. Его используют, когда линий не хватает». А то правда есть люди, которые вместо Uno берут Mega, потому что линии на подключение диодов кончились.


      1. quwy
        21.04.2018 03:41

        Тогда стоило бы использовать кристалл, у которого реально выходов не хватает, а то каким-то велосипедостроением попахивает. Да и вид у конструкции слижком уж утилитарный для туториала…


        1. Gryphon88
          23.04.2018 01:46

          По фен-шую много стоило бы сделать иначе, от платы до кода, но и так неплохо. Тем более что читатели хабра быстро понимают, что в комментах не меньше информации, чем в самой статье. Вообще, под статьёй можно найти годную эррату или пару полноценных бранчей.


    1. JsusDev Автор
      21.04.2018 05:01

      Вы заметили что на плате размещено по мимо микросхем 18 контактов гребенки? Да вы можете спросить если так то почему тогда ты не используешь их? они что для красоты ?! нет они предполагаются для соединения оставшихся пинов ATmega328p.


  1. evilrussian
    21.04.2018 08:36

    Отличная статья. Сообщение при старте ок)


  1. Dmitriy62
    21.04.2018 13:08

    Отличная статья для начинающих радиолюбителей. Автор показал не оптимальную реализацию, а варианты использования различных радиокомпонентов с подробным объяснением. Как раз подробного (и наглядного в данном случае) объяснения и не хватает некоторым авторам, которые считают себя очень крутыми и пишут соответственно, наверное, тоже для крутых. Побольше таких статей!..