Start Elkretssimulator


RGB-led Ljusstake

Reinkarnation av ljusstake

En gammal ljusstake, vars gamla trasiga glödlampor var hopplösa få tag på, har legat och skräpat några år. Ett utmärkt julpyssel eller pyssel när som helst, för den delen.

RGB LED

En RGB LED är 3 stycken lysdioder (led) monterade i samma höjle.



Det finns 2 varianter att hålla isär för att det skall fungera. Den ena varianten har gemensam katod (-). Den andra har gemensam anod (+). De som fungerar tillsammans med kretsen WS2811, vilket är den kretsen jag har tillgång till (mer om det längre ner), har gemensam anod.

Skall du mata dessa med ström direkt bör du seriekoppla en resistor på kanske 330Ω för 3.3 volt eller 500Ω för 5 volt på ingångarna för att begränsa strömstyrkan, annars kan du ha sönder lysdioden (strömmen bör inte överstiga 10-20 mA genom lysdioden).

Det går eventuellt att driva RGB LED'n direkt från utgångarna på en enkortsdator. Men du bör kolla upp detta - hur stor ström ungångarna kan driva - för att inte riskera ha sönder ditt kontrollkort. Räkna med att lysdiod (en RGB har 3 stycken) konsumerar kanske 10-15 mA eller något sånt. I detta fall använder jag en styr/drivkrets för lysdioderna så jag behöver inte bry mig om denna detalj.

WS2811, WS2812B, osv.


För att styra strömmen till alla lysdioder underlättar det med kontrollkretsar för detta ändamål. Varje lysdiod innehåller 3 lysdioder och 9 RGB-led blir alltså 27 regulatorer. En liten krets, WS2811, gör problemet enklare.

Kretsen WS2811 finns monterad på ett bekvämt kretskort. Det gör det marginellt enklare att löda fast kablar. Första kretsen tar seriedata på sin in-pinne. Nästa krets i serie, dess insignal, matas med föregående krets utsignal. Signallinan flödar alltså igenom kretsarna och varje krets fångar upp sin adress. Denna lösning har fördelen att många LEDs kan kopplas i serie men enbart 3 ledningar.



Montering

För ändamålet behövs väldigt många sladdar. Ett seriöst terapiarbete klippa lagom längder och skala dess ändar.


Sladdar löds fast på kretskorten där kretsen WS2811 sitter förmonterad. Här är det viktigt att hålla ordning på färgerna annars kommer det snabbt urarta, då det är väldigt mycket sladdar.


Jag använder mig av krympslang för att fixa isolering hela vägen.



Sladdinferno. Det rosa är också krympslang som jag sedan trär över kretsarna när allt är färdiglött.



Från en annan vinkel...



Kontrollknappar


Jag har räknat ut att det behövs åtminståne 3 kontrollknappar här. Dels en knapp med vilken man kan välja ett program som ljusstaken använder. Jag har spånat ihop en ide om att när man trycker på denna knapp 1 gång så kan man låta första lampan lysa. Trycker man tre gånger kommer lampa 3 på ljusstaken lysa. osv. Tanken är sedan att man trycker så många gånger att man kommer fram till det ljusprogram man önskar och sedan trycker man inte mer. Efter t.ex. 2 sekunder (om man inte trycker något mer) startar sedan programmet.

Två ytterligare reglage (potentiometrar) används till ljusstyrka respektive valfritt. Valfritt är oftast hastigheten med vilket ett program körs, förutom i något program där man istället väljer färg med detta reglage. Potentiometrarna kopplas in som spänningsdelare med mitt-pinnen in på lediga analogportar på arduino-kortet. På så vis kam man alltså justera en spänningsnivå mellan 0 och 5 volt med potentiometern och värdet läses av i mjukvaran. En bild på en testkoppling nedan.



På det arduino kompatibla dccduino -kort jag använder för detta projekt finns det hål i vilka man kan löda fast kablarna vilket jag råkar gilla. OBS att ovan bild är en arduino orginal jag använde vid labbandet. Dessa kort är dock kompatibla så det är inget att grunna så mycket över.



Så stoppar vi in det i en låda. Det finns många arduino -lådor där ute. Jag valde en där det fanns rum för en knapp och 2 potentiometrar.



Och så trycker vi ner allting och sätter på locket...



Hur tung last kan USB dra?

I skarpt läge kommer ljusstaken drivas av en nätdel men när jag utvecklar mjukvaran kommer ljusstaken drivas av USB-porten.

Om man kopplar ihop ett system med 10 stycken RGB LEDs, då bör man instinktivt ställa sig frågan var dessa drar för ström tillsammans. Nu är styrkretsarna inkopplade till GND och 5V på arduinokortet och dessa är i sin tur kopplade till USBn. Så det är alltså USBn som sätter taket för maxströmmen. Det är såklart bra, eftersom den strömmen är högre än vad processorns utgångar tål, men samtidigt lite läskigt eftersom man riskerar skada datorn USB är inkopplad till.

Lite närmare granskning av kopplingsschemat till arduino uno visar dock att konstruktörerna varit förutseende och placerat en 500 mA polysäkring i serie med USBns strömmatning. Denna säkring begränsar strömmen till ungefär 500 mA. Det råkar vara ungefär vad max ström tillgänglig är för en USB 2.0.



Lite om färger

Programmeringen här handlar främst om att blanda färgerna rött, grönt och blått så att man får lite coola effekter. Så därför kan det vara läge att fundera lite vad det handlar om. Det finns lite olika färgmodeller. Det mest uppenbara när man pratar om lampor är såklart att blanda olika mängder av rött, grönt och blått - RGB - för att få olika färger (pratar man färgmodeller för tryck på papper så finns en hel bunt varianter där en del har väldigt många processfärger som blandas). Men lite exprimenterande visar ganska snart att man vill jobba ur ett annat perspektiv, HSV med konstant ljus där man istället ändrar nyansen är mycket mer bekvämt. Låt oss ta det från början.

Färger - RGB (RGB = Red, Green, Blue)

En RGB-LED använder, som det låter som, färgerna rött, grönt och blått. RGB är ett additivt färgsystem med vilket vi kan komponera i princip alla synbara färger. Tänder vi samtliga färger får vi vitt ljus.



Måla Rött-grönt B=0 B=100 B=250
Måla Rött-blått G=0 G=100 G=250
Måla Blått-grönt R=0 R=100 R=250

Det är detta system som en tv-apparat eller datamonitor använder för varje pixel på skärmen.

Färgstyrkan anges typiskt som ett värde 0-255. Dvs (R,G,B) = (0,0,0) betyder då att alla lampor är släckta och (R,G,B) = (255,255,255) betyder att alla är tända, vilket ger vitt ljus. (R,G,B) = (255,0,0) blir då rött.

Man kan exprimentera lite och blanda dessa 3 färger för att se vad som händer. Tyvär är datamonitorn 2-dimensionell så vi får fixera den 3:e färgen till någonting och blanda de två andra färgerna. Resultatet blir som till vänster här.

Men hur skall man komponera tjusiga färger med RGB på ett enkelt sätt, så att man t.ex. kan vandra över hela regnbågen? Lite amatörmässigt exprimenterande med loopar och kombinationer ger en del rätt så coola discoeffekter men man landar snabbt i en återvändsgränd. Det är någon som saknas här. Man vill inte hålla på att blanda rött, grönt och blått, man vill snarare välja en nyans med en fix siffra vilket leder in på ett annat betraktelsesätt av färgerna, HSV.

Färger - HSV (HSV = Hue, Saturation, Lightness)

Hue, saturation, lightness betyder alltså nyans, mättnad och intensitet varför HSV också kallas NMI här i sverige. Nyans är alltså regnbågens färger, de färger man får om man vandrar runt i perifierin av denna cirkel.



Måla med V=1.0 V=0.5 V=0.1
(det tar några sekunder innan färgerna räknats om)


Mättnad är hur långt ifrån origo (mitten) på cirkeln man är. Maximal mättnad, där färgen är som starkast, är längst ut på cirkeln. Intensitet är hur långt ifrån svart färgen är. Det syns inte i denna cirkel, eftersom cirkeln egentligen är en cylinder och intensiteten är då höjden på cylindern. Ibland brukar cylindern visas som en kon.

Vad är poängen med HSV-modellen i ett sådant här projekt?

Man kan t.ex. med denna modell som grund skapa ett konstant ljus med en viss nyans (färg) som vandrar runt i regnbågens alla färger i en evig loop. Om man tänker sig en funktion som konverterar till RGB från HSV, låt kalla den HSV2RGB(h,s,v), där argumenten ligger mellan 0 och 1. Då kan man alltså sätta s och v till lämpliga värden och sedan loopar vi igenom för alla h mellan 0 och 1, t.ex. med 0.001 -steg. Det blir en enkel loop för alla h mellan 0 och 1 och ut får vi RGB som tjusigt vandrar mellan regnbågens alla färger. Det är ett exempel på nyttan med denna färgmodell, dvs helt enkelt att vi kan utgå ifrån nyans, mättnad och intensitet i vår algoritm snarare än röd, grön och blå.

Studera följande lilla kod. Den ger effekten att ljusstakens lampor "surfar" på regnbågen i ett visst tempo (justerbart). Dvs, jag använder kontant ljus och kontant mättnad och sedan loopar jag bara över alla lampor och förändrar nyansen pyttelite. Jag låter funktionen HSVtoRGB generera RGB-värden som sedan skickas till lysdioderna.


void surfontherainbow()
{
  int *v;
  int l;
  for(int i=0;(i<1000)&&(!abort_program);i++)
  {
    for(l=0;(l<NUMLIGHTS)&&(!abort_program);l++)
    {
     v=HSVtoRGB((float)((i+l*10)%1000)/1000,1.0,0.4) ;
     light(l,v[0],v[1],v[2]);   
    }
   pixels.show();
   delay((10+pot2)/5);
  }
}

Program

Programmet använder Adafruit_NeoPixel.h som innehåller kod för att enkelt kommunicera med WS2811/12 -kretsarna. Så det får du ladda ner först.

Kort beskrivning

WS281X -kretsarnas datasignal är kopplad till datapinne 6. Det kan justeras som du ser i koden nedan. Potentiometrarnas mittpinne är kopplad till analogingångarna A0 resp. A2. Knappen med vilket vi väljer ljusprogram är kopplad mellan GND och datapinne 2.

Det finns 9 olika ljusprogram. Man väljer program genom att trycka på knappen. Vill man se program 3, så trycker man 3 gånger. Vill man köra program, så 9 trycker man 9 gånger. Beroende på program valt kan man sedan justera ytterligare med potentiometrarna, den ena ljusstyrka och den andra hastighet eller färg.

Det finns lite debugutskrifter till serieporten som ligger kvar. Programmet är inte jätteoptimerat, men det behövs inte heller.

Koden


// Ljusstake www.el.st
#include <Adafruit_NeoPixel.h>
#define PIN            6
#define NUMLIGHTS      10

Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NUMLIGHTS, PIN, NEO_RGB + NEO_KHZ800);

volatile int pot1, pot2;               // 2 trimpotentiometrar, analog 0 & 1
volatile int r1,g1,b1;      
float lightstrength;                   // ljusstyrka regleras med pot1

volatile int nrOfPushButton = 0;
volatile int quarterSecCount = 0;
volatile int doubleSecCount = 0;
volatile int cnt;
volatile int run_program=0;
volatile int abort_program=0;

void setup() 
{
  pixels.begin();                      // setup neopixel
  
  Serial.begin(9600);                  // setup serial
  
  // setup trigger interrupt signal change pin 2
  pinMode(2, INPUT);
  digitalWrite(2, HIGH);
  attachInterrupt(0, keyPress, FALLING);
  
  // setup timer interrupt  
  cli();                               // interrupt disable, innan justering görs
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1  = 0;                          // räknaren noll från start
  OCR1A =  3905;                       // compare match register (4 ggr/sek)
  TCCR1B |= (1 << WGM12); // CTC-mode=ON, dvs TCNT1=0 när TCNT1>=OCR1A
  TCCR1B |= (1 << CS12) | (1 << CS10); // 1024 prescaler
  TIMSK1 |= (1 << OCIE1A);  // enable timer compare interrupt
  sei();                               // interrupt enable, alla inställningar klara  
}

// kortare och tar hänsyn till ljusstyrka
void light(int i, int r, int g, int b)
{
    r=(int)((float)r*lightstrength);
    g=(int)((float)g*lightstrength);
    b=(int)((float)b*lightstrength);    
    pixels.setPixelColor(i, pixels.Color(r,g,b)); 
}

// slack alla
void allDark()
{
  for(int i=0;i<NUMLIGHTS;i++)
    light(i,0,0,0); 
  pixels.show();
}

// tand 1 lampa
void lightOne(int i, int r, int g, int b)
{
  allDark();
  light(i,r,g,b);
  pixels.show();
}

// tand alla
void lightAll(int r, int g, int b)
{
  for(int i=0;i<NUMLIGHTS;i++)
    light(i,r,g,b);
  pixels.show();
}

// lampa rinner fram och tillbaka som torkarblad
void wipingLight()
{
  for(int j=0;j<2;j++)
  {
    for(int i=0;i<NUMLIGHTS;i++)
    {
      lightOne(i,10,10,10); 
      pixels.show();
      delay(30);
    }  
    for(int i=NUMLIGHTS-2;i>0;i--)
    {  
      lightOne(i,10,10,10); 
      pixels.show();
      delay(30);
    }
  }  
}

void wipeClean()
{
    for(int i=0;i<NUMLIGHTS;i++)
    {
      light(i,10,10,10); 
      if(i)
        light(i-1,0,0,0); 
      pixels.show();
      delay(30);
    }  
    for(int i=NUMLIGHTS-1;i>=0;i--)
    {  
      lightOne(i,10,10,10); 
      pixels.show();
      delay(30);
    }
}

void randomLight(int green)
{
  int a,b,c;
    for(int i=0;(i<15)&&(!abort_program);i++)
    {
      a=random(20,255-green*2);            // styrka
      light(random(NUMLIGHTS),a,a,a); 
      pixels.show();
      delay(random(50,200));
    }  
    for(int i=0;(i<green)&&(!abort_program);i++)
    {
      a=random(30,255);                    // styrka
      light(random(NUMLIGHTS),0,a,0); 
      pixels.show();
      delay(random(50,200));
    } 
    for(int i=0;(i<(15-green))&&(!abort_program);i++)
    {
      b=random(0,5); // styrka
      light(random(NUMLIGHTS),b,b,b); 
      pixels.show();
      delay(random(10,50));
    }      
}

void winter()
{
    for(int i=0;(i<8)&&(!abort_program);i++)
    {
      for(int j=0;j<5;j++)
        randomLight(0);
      if(i<9)wipeClean();
    }     
}

void spring()
{
  for(int i=0;i<30;i++)
    randomLight(i/2);
  for(int j=0;(j<13)&&(!abort_program);j++)
  {
    for(int i=0;(i<5)&&(!abort_program);i++)
    {
      light(5+i, 0,j,0); 
      light(4-i, 0,j,0); 
      pixels.show();
      delay(40);
    }      
  }
}

void summer_rainbow()
{
  int i=0;
  for(int x=0;(x<1800)&&(!abort_program);x++)
  { 
      light(i++,sin(x/10)*128+1127,sin(x/10+60)*128+127,sin(x/10+120)*128+127);
        pixels.show();
       i%=NUMLIGHTS;
        delay(100);
  }
}

void summer_colors()
{
  int i=0;
  for(int x=0;(x<180)&&(!abort_program);x++)
  { 
      light(i++,sin(x)*100+100,sin(x+90)*100+100,sin(x+180)*100+100);
        pixels.show();
       i%=NUMLIGHTS;
        delay(100);
  }
}

void autumn1()
{
  int i=0;
  for(int x=0;(x<1800)&&(!abort_program);x++)
  { 
      light(i++,sin(x/10)*128+1127,sin(x/10+45)*128+127,sin(x/10+90)*128+127);
        pixels.show();
       i%=NUMLIGHTS;
        delay(100);
  }
}

void autumn2(int white)
{
  int a,b,x;
    for(int i=0;(i<(15-white))&&(!abort_program);i++)
    {
      a=random(3,8); // styrka
      x=random(1800);
      light(random(NUMLIGHTS),sin(x/10)*128+1127,sin(x/10+45)*128+127,sin(x/10+90)*128+127); 
      pixels.show();
      delay(random(50,200));
    }  
    for(int i=0;(i<white)&&(!abort_program);i++)
    {
      a=random(3,8); // styrka
      light(random(NUMLIGHTS),a,a,a); 
      pixels.show();
      delay(random(50,200));
    } 
    for(int i=0;(i<(15-white))&&(!abort_program);i++)
    {
      light(random(NUMLIGHTS),0,0,0); 
      pixels.show();
      delay(random(10,50));
    }      
}

void autumn()
{
  autumn1();
  for(int t=0;(t<10)&&(!abort_program);t++)
  {
    autumn2(t);
  }
}

void winterdarkness()
{
  for(int i=0;(i<NUMLIGHTS)&&(!abort_program);i++)
  {
    pixels.setPixelColor(i, pixels.Color(50,0,0)); 
    pixels.show();
    delay(10+pot2);
  }
  for(int i=0;(i<NUMLIGHTS)&&(!abort_program);i++)
  {
    pixels.setPixelColor(i, pixels.Color(0,0,50)); 
    pixels.show();
    delay(10+pot2);
  }
  for(int i=0;(i<NUMLIGHTS)&&(!abort_program);i++)
  {   
    pixels.setPixelColor(i, pixels.Color(0,50,0)); 
    pixels.show();
    delay(10+pot2);
  }
}

void seasons()
{
  winter();
  spring();
  summer_colors();
  summer_rainbow();  
  autumn();
}

void whiteLight()
{
  lightAll(255,255,255);
}

void colorFlame()
{
  r1+=random(12)-6; if(r1>255){r1=255;}; if(r1<0){r1=255;};
  g1+=random(8)-6; if(g1>128){g1=128;}; if(g1<0){g1=128;};
  b1+=random(9)-6; if(b1>128){b1=128;}; if(b1<0){b1=128;};
  light(random(NUMLIGHTS),r1,g1,b1);
  pixels.show(); 
  delay(10);    
}

void gradient()
{
  int i;
  for(int x=0;(x<18000)&&(!abort_program);x++)
  { 
      light(i++,sin(x/100)*128+1127,sin(x/100+45)*128+127,sin(x/100+90)*128+127);
      pixels.show();
      i%=NUMLIGHTS;
      delay(100);
  }  
}

int * HSVtoRGB(float h, float s, float v) 
{
    static int vr[3];
    int i;
    float r,g,b,f,p,q,t;
    i=(int)(h*6);
    f=h*6-(float)i;
    p=v*(1-s);
    q=v*(1-f*s);
    t=v*(1-(1-f)*s);
    switch (i%6) 
    {
        case 0: r=v, g=t, b=p; break;
        case 1: r=q, g=v, b=p; break;
        case 2: r=p, g=v, b=t; break;
        case 3: r=p, g=q, b=v; break;
        case 4: r=t, g=p, b=v; break;
        case 5: r=v, g=p, b=q; break;
    }
    vr[0]=(int)(r*255);
    vr[1]=(int)(g*255);
    vr[2]=(int)(b*255);    
    return vr;
}

void selectColor()
{
  int *v;
  v=HSVtoRGB((float)(pot2-100)/(1024-100),1.0,0.4) ;
  lightAll(v[0],v[1],v[2]);   
  pixels.show();
}

void surfontherainbow()
{
  int *v;
  int l;
  for(int i=0;(i<1000)&&(!abort_program);i++)
  {
    for(l=0;(l<NUMLIGHTS)&&(!abort_program);l++)
    {
     v=HSVtoRGB((float)((i+l*10)%1000)/1000,1.0,0.4) ;
     light(l,v[0],v[1],v[2]);   
    }
   pixels.show();
   delay((10+pot2)/5);
  }
}

void colortransition()
{
  int *v;
  for(int i=0;(i<10000)&&(!abort_program);i++)
  {
   v=HSVtoRGB((float)i/10000,1.0,0.4) ;
   lightAll(v[0],v[1],v[2]);   
   pixels.show();
   delay((10+pot2)/20);
  }
}

// succesivt stigande ljusstyrka
void pulstransition()
{
  int *vr;
  for(int v=30;v<100;v++)  
  for(int i=0;i<100;i++)
  {
      if(abort_program)
        return;
    
   vr=HSVtoRGB((float)i/100,1.0,(float)v/100) ;
   lightAll(vr[0],vr[1],vr[2]);
   pixels.show();
   delay((10+pot2)/5);
  }
}

void showProgram(int p)
{
 if(abort_program)
   return;
  switch(p)
  {
     case 1:whiteLight(); break;
     case 2:selectColor(); break;     
     case 3:colorFlame(); break;
     case 4:gradient(); break;
     case 5:winterdarkness(); break;
     case 6:colortransition(); break;
     case 7:pulstransition(); break;     
     case 8:surfontherainbow(); break;      
     case 9:seasons(); break;   
  } 
}

void gauge(int v)
{
  for(cnt=0;cnt<NUMLIGHTS;cnt++)
  {
     light(cnt, 0,0,0);
  }
  light((v-1)%NUMLIGHTS, 255,(v>NUMLIGHTS)?0:255,(v>NUMLIGHTS)?255:0);    
  pixels.show();  
}

void keyPress()
{
  abort_program=1;  
  // åtminstone  3x0.25 sek sedan senaste tryck
  if(quarterSecCount>2)                
  {
    quarterSecCount=0;
    // do whatever you wanna do when key press, in this case we just count
    nrOfPushButton++;
    nrOfPushButton%=10;
    if(nrOfPushButton==0)    
    {
      nrOfPushButton=1;
    }
    gauge(nrOfPushButton);
    doubleSecCount=0;      
  }
}

// anropas 4 ggr per sekund
ISR(TIMER1_COMPA_vect)                
{
  pot1=analogRead(0);  // ljusstyrka
  pot2=analogRead(2);  // funktionsvred
  lightstrength=((float)pot1)/1024.0;

  if(nrOfPushButton)
  {
    gauge(nrOfPushButton);
  }
  
  quarterSecCount++;       
  if(!(quarterSecCount%8))
  {
    doubleSecCount++;
    if((doubleSecCount>0)&&(nrOfPushButton>0))
    {
      abort_program=0;
      run_program=nrOfPushButton;
      nrOfPushButton=0;
      //showProgram(p);
    }
  }
  
  Serial.print("pot1: ");             
  Serial.print(pot1);  
  Serial.print(" pot2: ");
  Serial.print(pot2);    
  Serial.print(" strength: ");
  Serial.println(lightstrength);    
  Serial.println(doubleSecCount);  
  Serial.print(" program: ");  
  Serial.println(nrOfPushButton);    
}

void loop() 
{
 showProgram(run_program);
}
















Ref.
WS2811 - WS2812B
http://www.adafruit.com/datasheets/WS2811.pdf
http://www.adafruit.com/datasheets/WS2812B.pdf
https://github.com/adafruit/Adafruit_NeoPixel