Привет Хабр!

Предисловие


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



Внимание, под катом много фото.

Предварительные шаги


Выбор пал на создание напольной лампы-торшера, которая бы за счет приглушения света абажуром, не светилась бы, как прожектор ночью, занимала бы немного места, и могла бы одновременно отобразить несколько цветов одновременно. Нечто похожее уже было создано хабравчанином — тогда это была лампа, показывающая погоду, собранная на малинке. У меня не было задачи лазать в интернет, хотелось только «помигать светодиодами», поэтому в качестве мозга было решено взять Ардуинку, благо дома завалялась небольшая ее версия Нано, купленная у дяди Ляо. Для удаленного управления — решил использовать простейший bluetooth-модуль Bolutek за пару вечнозеленых.

Далее встал вопрос создания собственно лампы. Решено было не изобретать велосипед, а взять готовый. В качестве основы подошла напольная лампа-торшер, с бумажным абажуром, купленная в ближайшей Икее, за вполне сходные 500 рублей. Было решено, что внутри на стержне как раз можно будет закрепить несколько площадок для наклейки светодиодной ленты, которая бы создавала достойное по яркости освещение. Ленту взял цветную, модели 5050, 60 светодиодов на метр, без индивидуального управления светодиодами.

Площадки сделал из монтажной металлической ленты для крепления теплых полов — продается во всех строительных магазинах. Катушки ленты 20м хватило за глаза. Из ленты свернул кольца, диаметром примерно 14см, с возможностью закрепления на стержне лампы. Одновременно кольца выполняют роль теплоотвода — т.к. лента весьма ощутимо греется.

Далее стал думать, как независимо управлять цветом всех уровней, имея ограниченное количество PWM-выходов на ардуинке. Количество уровней выбрал 8, т.к. при большем лампа начинала сгибаться под весом металлических площадок. Все же шведские конструкторы не рассчитывали, что на стержень лампы будет крепиться еще что-то. Таким образом, я получил необходимость управлять 24-мя выходами, с возможностью плавной регулировки выходного напряжения на них. Ни одна ардуина стольких выходов не имеет (может имеет Мега, но такое решение примитивно), поэтому для решения поставленной задачи использовал библиотеку ShiftPWM, которая удовлетворит любые разумные требования по количеству уровней. С помощью недорогих и доступных микрух — сдвигового регистра 74hc595 и ключей uln2803 — обеспечил нужное количество управляемых выходов и нужный для питания лент ток на них.

Воплощение


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



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



Далее пришла очередь сборки собственно схемы. Схема подключения сдвиговых регистров 74hc595 взял типовую, с сайта-руководства по ShiftPWM.

Схема подключения сдвиговых регистров



Схема подключения uln2803



Собрал схему на макетной доске схему с одинарными светодиодами, залил скетч-пример — удостоверился, что все работает, как надо:



После этого пришло время собирать схему в продакшн-версии. Т.к. хотелось собрать схему максимально быстро, и времени пробовать ЛУТ-технологию не было — собрал контроллер на двух макетных текстолитовых платах, размера 4 на 6см. На первой разместил ардуину и модуль bluetooth для дистанционного управления, на второй — регистры, ключи, и разъемы для подключения лент:



Затем скрутил обе платы вместе, получился эдакий бутерброд:



В качестве блока питания — взял недорогой китайский, на 12В, с выходным током до 5А:



В качестве корпуса — использовал коммутаторную коробку, которая подошла по размерам:



Далее — разместил внутри корпуса блок питания, и бутерброд из плат, протянув внутрь провода от лент:



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

Для управления контроллером предварительно мной была написана программа под Андроид. Вообще же для управления схемой подойдет любой bluetooth-терминал — достаточно будет запрограммировать на передачу несколько предустановленных команд, а для выбора пользовательского цвета подсветки — задать RGB-код цвета в формате R,G,B.

Далее привожу код скетча, который следует залить. Схема имеет 10 предустановок режимов, а так же возможность задания своих собственных цветов.

Скетч
/*
 * ShiftPWM non-blocking RGB fades example, (c) Elco Jacobs, updated August 2012.
 *
 * This example for ShiftPWM shows how to control your LED's in a non-blocking way: no delay loops.
 * This example receives a number from the serial port to set the fading mode. Instead you can also read buttons or sensors.
 * It uses the millis() function to create fades. The block fades example might be easier to understand, so start there.
 *
 * Please go to www.elcojacobs.com/shiftpwm for documentation, fuction reference and schematics.
 * If you want to use ShiftPWM with LED strips or high power LED's, visit the shop for boards.
 */

// ShiftPWM uses timer1 by default. To use a different timer, before '#include <ShiftPWM.h>', add
// #define SHIFTPWM_USE_TIMER2  // for Arduino Uno and earlier (Atmega328)
// #define SHIFTPWM_USE_TIMER3  // for Arduino Micro/Leonardo (Atmega32u4)

// Clock and data pins are pins from the hardware SPI, you cannot choose them yourself.
// Data pin is MOSI (Uno and earlier: 11, Leonardo: ICSP 4, Mega: 51, Teensy 2.0: 2, Teensy 2.0++: 22) 
// Clock pin is SCK (Uno and earlier: 13, Leonardo: ICSP 3, Mega: 52, Teensy 2.0: 1, Teensy 2.0++: 21)

// You can choose the latch pin yourself.
const int ShiftPWM_latchPin=8;

// ** uncomment this part to NOT use the SPI port and change the pin numbers. This is 2.5x slower **
// #define SHIFTPWM_NOSPI
// const int ShiftPWM_dataPin = 11;
// const int ShiftPWM_clockPin = 13;


// If your LED's turn on if the pin is low, set this to true, otherwise set it to false.
const bool ShiftPWM_invertOutputs = false;

// You can enable the option below to shift the PWM phase of each shift register by 8 compared to the previous.
// This will slightly increase the interrupt load, but will prevent all PWM signals from becoming high at the same time.
// This will be a bit easier on your power supply, because the current peaks are distributed.
const bool ShiftPWM_balanceLoad = false;

#define rxPin 2
#define txPin 4

#include <SoftwareSerial.h>
#include <ShiftPWM.h>   // include ShiftPWM.h after setting the pins!

// Function prototypes (telling the compiler these functions exist).
void oneByOne(void);
void inOutTwoLeds(void);
void inOutAll(void);
void alternatingColors(void);
void hueShiftAll(void);
void randomColors(void);
void fakeVuMeter(void);
void rgbLedRainbow(unsigned long cycleTime, int rainbowWidth);
void printInstructions(void);
void setColor(int r, int g, int b);

// Here you set the number of brightness levels, the update frequency and the number of shift registers.
// These values affect the load of ShiftPWM.
// Choose them wisely and use the PrintInterruptLoad() function to verify your load.
unsigned char maxBrightness = 255;
unsigned char pwmFrequency = 75;
unsigned int numRegisters = 6;
unsigned int numOutputs = numRegisters*8;
unsigned int numRGBLeds = numRegisters*8/3;
unsigned int fadingMode = 0; //start with all LED's off.

int r = 0;
int g = 0;
int b = 0;

unsigned long startTime = 0; // start time for the chosen fading mode

SoftwareSerial mySerial =  SoftwareSerial(rxPin, txPin);

void setup(){
  while(!Serial){
    delay(100); 
  }
  Serial.begin(9600);

  pinMode(rxPin, INPUT);
  pinMode(txPin, OUTPUT);

  // Sets the number of 8-bit registers that are used.
  ShiftPWM.SetAmountOfRegisters(numRegisters);

  // SetPinGrouping allows flexibility in LED setup. 
  // If your LED's are connected like this: RRRRGGGGBBBBRRRRGGGGBBBB, use SetPinGrouping(4).
  ShiftPWM.SetPinGrouping(1); //This is the default, but I added here to demonstrate how to use the funtion
  
  ShiftPWM.Start(pwmFrequency,maxBrightness);
  printInstructions();
  
  mySerial.begin(9600);
}

void loop()
{    
  String command = "";
  while (mySerial.available() > 0)
  {
    char c = mySerial.read();
    Serial.println(c);
    command.concat(c);
  }
  
  command.trim();
  if (command == "")
  {
  }
  else
  {
    startTime = millis();
  }

  int firstCommaPos = -1;
  int lastCommaPos = -1;
  
  firstCommaPos = command.indexOf(',');
  lastCommaPos = command.lastIndexOf(',');
  
  if (firstCommaPos != -1 && lastCommaPos != -1 && lastCommaPos != firstCommaPos)
  {
    String rStr = command.substring(0, firstCommaPos);
    String gStr = command.substring(firstCommaPos + 1, lastCommaPos);
    String bStr = command.substring(lastCommaPos + 1);
    
//    Serial.println("r is -> " + rStr);
//    Serial.println("g is -> " + gStr);
//    Serial.println("b is -> " + bStr);

    r = rStr.toInt();
    g = gStr.toInt();
    b = bStr.toInt();
    
    fadingMode = 10;
  }

  if (command == "a")
    fadingMode = 0;
  if (command == "b")
    fadingMode = 1;
  if (command == "c")
    fadingMode = 2;
  if (command == "d")
    fadingMode = 3;
  if (command == "e")
    fadingMode = 4;
  if (command == "f")
    fadingMode = 5;
  if (command == "g")
    fadingMode = 6;
  if (command == "h")
    fadingMode = 7;
  if (command == "i")
    fadingMode = 8;
  if (command == "j")
    fadingMode = 9;

  Serial.println("command is -> " + command);  

  switch(fadingMode){
  case 0:
    // Turn all LED's off.
    ShiftPWM.SetAll(0);
    break;
  case 1:
    oneByOne();
    break;
  case 2:
    inOutAll();
    break;
  case 3:
    inOutTwoLeds();
    break;
  case 4:
    alternatingColors();
    break;
  case 5:
    hueShiftAll();
    break;
  case 6:
    randomColors();
    break;
  case 7:
    fakeVuMeter();
    break;
  case 8:
    rgbLedRainbow(3000,numRGBLeds);
    break;
  case 9:
    rgbLedRainbow(10000,5*numRGBLeds);    
    break;   
  case 10:
    setColor(r,g,b);
    break;   
  default:
    Serial.println("Unknown Mode!");
    delay(1000);
    break;
  }
}

void setColor(int r, int g, int b)
{
  ShiftPWM.SetAll(0);
  ShiftPWM.SetAllRGB(r,g,b);
}

void oneByOne(void){ // Fade in and fade out all outputs one at a time
  unsigned char brightness;
  unsigned long fadeTime = 500;
  unsigned long loopTime = numOutputs*fadeTime*2;
  unsigned long time = millis()-startTime;
  unsigned long timer = time%loopTime;
  unsigned long currentStep = timer%(fadeTime*2);

  int activeLED = timer/(fadeTime*2);

  if(currentStep <= fadeTime ){
    brightness = currentStep*maxBrightness/fadeTime; ///fading in
  }
  else{
    brightness = maxBrightness-(currentStep-fadeTime)*maxBrightness/fadeTime; ///fading out;
  }
  ShiftPWM.SetAll(0);
  ShiftPWM.SetOne(activeLED, brightness);
}

void inOutTwoLeds(void){ // Fade in and out 2 outputs at a time
  unsigned long fadeTime = 500;
  unsigned long loopTime = numOutputs*fadeTime;
  unsigned long time = millis()-startTime;
  unsigned long timer = time%loopTime;
  unsigned long currentStep = timer%fadeTime;

  int activeLED = timer/fadeTime;
  unsigned char brightness = currentStep*maxBrightness/fadeTime;

  ShiftPWM.SetAll(0);
  ShiftPWM.SetOne((activeLED+1)%numOutputs,brightness);
  ShiftPWM.SetOne(activeLED,maxBrightness-brightness);
}

void inOutAll(void){  // Fade in all outputs
  unsigned char brightness;
  unsigned long fadeTime = 2000;
  unsigned long time = millis()-startTime;
  unsigned long currentStep = time%(fadeTime*2);

  if(currentStep <= fadeTime ){
    brightness = currentStep*maxBrightness/fadeTime; ///fading in
  }
  else{
    brightness = maxBrightness-(currentStep-fadeTime)*maxBrightness/fadeTime; ///fading out;
  }
  ShiftPWM.SetAll(brightness);
}

void alternatingColors(void){ // Alternate LED's in 6 different colors
  unsigned long holdTime = 2000;
  unsigned long time = millis()-startTime;
  unsigned long shift = (time/holdTime)%6;
  for(unsigned int led=0; led<numRGBLeds; led++){
    switch((led+shift)%6){
    case 0:
      ShiftPWM.SetRGB(led,255,0,0);    // red
      break;
    case 1:
      ShiftPWM.SetRGB(led,0,255,0);    // green
      break;
    case 2:
      ShiftPWM.SetRGB(led,0,0,255);    // blue
      break;
    case 3:
      ShiftPWM.SetRGB(led,255,128,0);  // orange
      break;
    case 4:
      ShiftPWM.SetRGB(led,0,255,255);  // turqoise
      break;
    case 5:
      ShiftPWM.SetRGB(led,255,0,255);  // purple
      break;
    }
  }
}

void hueShiftAll(void){  // Hue shift all LED's
  unsigned long cycleTime = 10000;
  unsigned long time = millis()-startTime;
  unsigned long hue = (360*time/cycleTime)%360;
  ShiftPWM.SetAllHSV(hue, 255, 255); 
}

void randomColors(void){  // Update random LED to random color. Funky!
  unsigned long updateDelay = 100;
  static unsigned long previousUpdateTime;
  if(millis()-previousUpdateTime > updateDelay){
    previousUpdateTime = millis();
    ShiftPWM.SetHSV(random(numRGBLeds),random(360),255,255);
  }
}

void fakeVuMeter(void){ // imitate a VU meter
  static unsigned int peak = 0;
  static unsigned int prevPeak = 0;
  static unsigned long currentLevel = 0;
  static unsigned long fadeStartTime = startTime;
  
  unsigned long fadeTime = (currentLevel*2);// go slower near the top

  unsigned long time = millis()-fadeStartTime;
  currentLevel = time%(fadeTime);

  if(currentLevel==peak){
    // get a new peak value
    prevPeak = peak;
    while(abs(peak-prevPeak)<5){
      peak =  random(numRGBLeds); // pick a new peak value that differs at least 5 from previous peak
    }
  }

  if(millis() - fadeStartTime > fadeTime){
    fadeStartTime = millis();
    if(currentLevel<peak){ //fading in
      currentLevel++;
    }
    else{ //fading out
      currentLevel--;
    }
  }
  // animate to new top
  for(unsigned int led=0;led<numRGBLeds;led++){
    if(led<currentLevel){
      int hue = (numRGBLeds-1-led)*120/numRGBLeds; // From green to red
      ShiftPWM.SetHSV(led,hue,255,255); 
    }
    else if(led==currentLevel){
      int hue = (numRGBLeds-1-led)*120/numRGBLeds; // From green to red
      int value;
      if(currentLevel<peak){ //fading in        
        value = time*255/fadeTime;
      }
      else{ //fading out
        value = 255-time*255/fadeTime;
      }
      ShiftPWM.SetHSV(led,hue,255,value);       
    }
    else{
      ShiftPWM.SetRGB(led,0,0,0);
    }
  }
}

void rgbLedRainbow(unsigned long cycleTime, int rainbowWidth){
  // Displays a rainbow spread over a few LED's (numRGBLeds), which shifts in hue. 
  // The rainbow can be wider then the real number of LED's.
  unsigned long time = millis()-startTime;
  unsigned long colorShift = (360*time/cycleTime)%360; // this color shift is like the hue slider in Photoshop.

  for(unsigned int led=0;led<numRGBLeds;led++){ // loop over all LED's
    int hue = ((led)*360/(rainbowWidth-1)+colorShift)%360; // Set hue from 0 to 360 from first to last led and shift the hue
    ShiftPWM.SetHSV(led, hue, 255, 255); // write the HSV values, with saturation and value at maximum
  }
}

void printInstructions(void){
  Serial.println("---- ShiftPWM Non-blocking fades demo ----");
  Serial.println("");
  
  Serial.println("Type 'l' to see the load of the ShiftPWM interrupt (the % of CPU time the AVR is busy with ShiftPWM)");
  Serial.println("");
  Serial.println("Type any of these numbers to set the demo to this mode:");
  Serial.println("  0. All LED's off");
  Serial.println("  1. Fade in and out one by one");
  Serial.println("  2. Fade in and out all LED's");
  Serial.println("  3. Fade in and out 2 LED's in parallel");
  Serial.println("  4. Alternating LED's in 6 different colors");
  Serial.println("  5. Hue shift all LED's");
  Serial.println("  6. Setting random LED's to random color");
  Serial.println("  7. Fake a VU meter");
  Serial.println("  8. Display a color shifting rainbow as wide as the LED's");
  Serial.println("  9. Display a color shifting rainbow wider than the LED's");  
  Serial.println("");
  Serial.println("Type 'm' to see this info again");  
  Serial.println("");
  Serial.println("----");
}



Видео показывает режим радуги, ИМХО самый красивый режим работы:



И пара фотографий готового устройства — жаль, что фотокамера не может передать сочности цвета:



Итоги


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

Все компоненты контроллера были куплены на Ебее, все желающие могут найти их там — цены минимальны. Итоговая стоимость лампы получилась приблизительно: 500р сама лампа, 800р — за светодиодную ленту, 100р — блок питания, микросхемы — 50р, 200р — синезуб, корпус и стальная лента для крепления — 200р. Итого — около 1900р.
По времени сборка заняла примерно 2 недели в свободное время по вечерам. Наверняка можно сделать быстрее, если не отвлекаться.

Что можно сделать еще:

  • Использовать ЛУТ вместо макетных плат
  • Доработать софтину для телефона — чтобы можно было регулировать яркость/скорость эффектов и т.д.
  • Вместо Адруинки взять Малину — и получить возможность автоматического включения освещения, ручного управления существующими режимами или создания новых без перезаливки микропрограммы
  • Вместо лент 5050 взять ленту ws2811 — и получить возможность управления уже не уровнями целиком, а каждым отдельным светодиодом
  • Сейчас стоковая лампочка никак не связана с микроконтроллером — а можно поставить реле и управлять ей так же удаленно с телефона
  • Сделать включение по хлопку ладошами — чтобы не лазать каждый раз в программу для включения
  • ...
  • Еще десятки доделок


Буду рад увидеть в коментах отзывы, критику, и идеи об улучшении конструкции.

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


  1. Dare
    19.05.2015 13:50

    Подскажите, ощутима ли задержка при управлении через BT?


    1. alexpp Автор
      19.05.2015 13:57

      Нет, режим меняется сразу же после приема команды.


  1. Meklon
    19.05.2015 15:01

    Ключевой вопрос — что с нагревом?


    1. alexpp Автор
      19.05.2015 15:09

      Самая горячая часть схемы — БП, по ощущениям — градусов 45. Светодиодные ленты и контроллер — теплые, но не горячие.


      1. Meklon
        19.05.2015 16:30

        Отлично, спасибо)


      1. jar_ohty
        20.05.2015 13:45
        +1

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


        1. alexpp Автор
          20.05.2015 14:28

          А сколько времени в день у вас работает лента?


          1. jar_ohty
            20.05.2015 17:52

            Примерно за это время она наработала 2-3 тысячи часов. То есть немного.


            1. alexpp Автор
              20.05.2015 18:20

              2 тысячи часов по паре часов день работы — меньше 3-х лет службы. Если делать лампу на длительный срок, тогда да, тем кто решит повторить схему, можно подумать об охлаждении лент.


              1. jar_ohty
                20.05.2015 22:51

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


  1. istui
    19.05.2015 19:26
    +4

    Отличная идея и реализация.

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


    1. alexpp Автор
      19.05.2015 19:44

      Спасибо за отзыв!
      Добавить интерпретатор команд в Ардуинку можно, места в прошивке хватит — скетч занимает всего 17кб, остальное свободно. Но другое дело — удобство передачи этих команд с телефона/терминала, и второй момент — стоит выключить лампу из розетки, как все переданные команды пропадут из памяти контроллера. Малинка тем и хороша, что с легкостью позволяет сохранить все полученные данные в БД или на карточке памяти — и восстановить их при перезапуске.


      1. istui
        19.05.2015 20:48
        +1

        Тут все зависит от фантазии и желания/навыков программирования. В идеале для этого делается программка с удобным интерфейсом, которая эти команды создаст сама и передаст на лампу. Хотя на Малинке реализовать смену режимов можно намного быстрее и без промежуточных слоев — вопрос в том, что интереснее и полезнее: результат или процесс :)

        Для хранения данных в микроконтроллере есть энергонезависимая память (EEPROM) — команды можно хранить в ней не опасаясь отключений питания (или сохранять там предыдущий режим работы). К сожалению, у нее ограниченный ресурс (100 000 циклов перезаписи на ячейку), так что использовать ее надо с оглядкой на это.

        Альтернатива — микросхема флеш-памяти с интерфейсом I2C за пару долларов, но здесь это, на мой взгляд, перебор…


  1. Alexeyslav
    20.05.2015 11:29
    +1

    Есть кстати TLC5947 24-канальный, 12-Bit PWM, вместо костыльного программного PWM.


    1. alexpp Автор
      20.05.2015 13:37

      Спасибо за наводку. Уже есть, кто попросил собрать такую же лампу — попробую там применить TLC5947.