Sunday, February 7, 2010

RGB Night-lights


My kids love lights; flashlights, LEDs, house lights, pen-lights, light-up teddy bears, the northern lights... you name it, they love it. I've been playing around with RGB LEDs for another project along with ping-pong ball diffusers and baby jar containers. It occurred to me that I could make a simple night-light with most of the same components & the girls were bound love 'em.

I put it all together on Saturday evening after the girls were in bed. I think I'm getting better at all this as I had the code working and 2 lights put together by midnight. The girls found them in the morning (unfortunately, very early...) and came into our bedroom to play with them. First off, they played some kind of colour matching game where they were shouting (yes, shouting, early, in bed...) "RED! I'm wearing red!", "Blue! My socks are blue!". After a bit of this they moved into their bedroom to make a 'tent' out of a couple of chairs and a duvet (comforter). They played with the lights in there for a while; I'm not sure what they were doing as I was drifting in and out of consciousness.

It's Sunday and I've just put the kids to bed. They wanted to go to bed with their lights right next to them. I declare them a success!


I apologise for the quality of this video. It was taken on an iPhone and I've not been able to get hold of them again to take a proper vid. I'm sure you get the idea though.



Ingredients:



  • ATtiny13
  • RGB LED (common anode)
  • 8 pin DIP socket
  • switch (the ones I used were SPDT)
  • coin cell battery holder
  • coin cell battery
  • baby jar
  • hookup wire (I used stranded 24 AWG).
  • ping pong ball
  • neodymium magnet
I used the magnet to attach everything to the lid of the baby jar. I've not had much luck with hot-glue and lids (metal ones), so I figured this should work better. I decided to put this together free-form (i.e. no perfboard), mainly for space reasons - but also as I like the aesthetics of the free-form projects (like this advent wreath or this programmable led).

First off I realised that the DIP socket would fit nicely onto the side of my coin cell holders:



Then I added the neodymium magnet to the center of the holder:



This was actually a bad idea... these things are very strong and everything you're playing with at this point has some kind of ferrous metal involved. This thing attracted the soldering iron, the solder, cut off leads, pliers, the helping hand... If I do this again, I'd definitely wait until the end to attach the magnets.

Next step was to solder on the RGB LED:



You may notice that the socket has changed orientation. I was planning on attaching wire to the leads of the LED so that it'd be easily positionable. Then I realised that, since I only had a tiny amount of room in the jar, I could leave the LEDs leads in place and bend them to put the LED into the correct position. That meant re-orientating the DIP socket (as seen above).

The LEDs common anode has not been attached at this point. I added some heat-shrink tubing to this lead to insulate it from the others. It has to be bent forward and could easily touch one of the adjacent leads.

Also, I sanded the LED casing to diffuse the light. Without this step there are obvious areas of red/green/blue that shine on the ping pong ball from the LED.

Now all that's left is the final bit of wiring:





I attached the switch to Vcc and added a ground wire from the battery holder to the DIP socket. The wire is bent around the magnet and was, er... "fun" to solder in place. The soldering iron kept 'pinging' onto the magnet just as I was getting into position... like I said before, put the magnet on last!

I decided to house the lights in baby food jars. The only modification was to cut some holes for my switches (I used a Proxxon rotary tool for this).

Here's a few shots of them in working order:


Code:

I'm running the ATtiny at 8MHz. This requires setting the fuse bits because the default setting is to divide the clock by 8. This code uses the same software PWM as the firefly-jar-II I wrote about previously. No other tricks here, other than a hack (in the main method) to increase the duration of the RedToYellow transition. That's just personal taste though & (since I'm fickle) I may remove that section in the future.

Current code is available on github. Here's the code that's running in my kids bedroom at the time of posting:

/*
 * rgb_strobe.c
 *
 * Distributed under Creative Commons 3.0 -- Attib & Share Alike
 *
 *  Created on: Feb 6, 2010
 *      Author: PaulBo
 */
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>

#ifndef F_CPU
    #define F_CPU 8000000UL
#endif

//Hardware definitions
#define RED_LED      PB2
#define GREEN_LED    PB1
#define BLUE_LED     PB0
#define ALL_LEDS    ((1 << RED_LED) | (1 << GREEN_LED) | (1 << BLUE_LED))

//Maximum value for led brightness
#define R_MAX 255
#define G_MAX 255
#define B_MAX 255

#define RED_INDEX   0
#define GREEN_INDEX 1
#define BLUE_INDEX  2

//Cycle States
#define RedToYellow     0
#define YellowToGreen   1
#define GreenToCyan     2
#define CyanToBlue      3
#define BlueToMagenta    4
#define MagentaToRed     5

//set red to max (we start in the RedToYellow state)
volatile unsigned char mRgbBuffer[] = {0,0,0};
unsigned char mRgbValues[] = {255,0,0};
unsigned char mState;

void init_timers()
{
    TIMSK0 = (1 << TOIE0);         // enable overflow interrupt
    TCCR0B = (1 << CS00);          // start timer, no prescale

    //enable interrupts
    sei();
}

void rgbCycle(){
    switch (mState) {
    case RedToYellow:
        mRgbValues[GREEN_INDEX]++;
        if (mRgbValues[GREEN_INDEX] == G_MAX)
            mState++;
        break;
    case YellowToGreen:
        mRgbValues[RED_INDEX]--;
        if (mRgbValues[RED_INDEX] == 0)
            mState++;
        break;
    case GreenToCyan:
        mRgbValues[BLUE_INDEX]++;
        if (mRgbValues[BLUE_INDEX] == B_MAX)
            mState++;
        break;
    case CyanToBlue:
        mRgbValues[GREEN_INDEX]--;
        if (mRgbValues[GREEN_INDEX] == 0)
            mState++;
        break;
    case BlueToMagenta:
        mRgbValues[RED_INDEX]++;
        if (mRgbValues[RED_INDEX] == R_MAX)
            mState++;
        break;
    case MagentaToRed:
        mRgbValues[BLUE_INDEX]--;
        if (mRgbValues[BLUE_INDEX] == 0)
            mState++;
        break;
    }

    //state should never advance beyond 5.
    //It wraps back to 0 when we reach 6
    mState %= 6;
}

int main(void){
    //Set LED pins to output
    DDRB |= ALL_LEDS;

    init_timers();

    while (1) {
        rgbCycle();
        _delay_ms(250);

        //I like the orange state and it only lasts for a second
        //so lets extend it a little bit more
        if(mState == RedToYellow)
        {
            _delay_ms(250);
            _delay_ms(250);
        }
    }
    return 0;
}

/*
 * Timer/Counter overflow interrupt. This is called each time
 * the counter overflows (255 counts/cycles).
 */
ISR(TIM0_OVF_vect)
{
    //static variables maintain state from one call to the next
    static unsigned char sPortBmask = ALL_LEDS;
    static unsigned char sCounter = 255;

    //set port pins straight away (no waiting for processing)
    PORTB = sPortBmask;

    //this counter will overflow back to 0 after reaching 255.
    //So we end up adjusting the LED states for every 256 interrupts/overflows.
    if(++sCounter == 0)
    {
        mRgbBuffer[RED_INDEX] = mRgbValues[RED_INDEX];
        mRgbBuffer[GREEN_INDEX] = mRgbValues[GREEN_INDEX];
        mRgbBuffer[BLUE_INDEX] = mRgbValues[BLUE_INDEX];

        //set all pins to low (remember this is a common anode LED)
        sPortBmask &=~ ALL_LEDS;
    }
    //this loop is considered for every overflow interrupt.
    //this is the software PWM.
    if(mRgbBuffer[RED_INDEX]   == sCounter) sPortBmask |= (1 << RED_LED);
    if(mRgbBuffer[GREEN_INDEX] == sCounter) sPortBmask |= (1 << GREEN_LED);
    if(mRgbBuffer[BLUE_INDEX]  == sCounter) sPortBmask |= (1 << BLUE_LED);
}

7 comments:

Anonymous said...

Great Job! ,clear, easy and small.
franmmd

Anonymous said...

Would this code work without any changes on an ATtiny85 instead of the ATtiny13 AVR chip?

PaulBo said...

I've not looked at an ATtiny85 datasheet to check. I would have though that any changes would be minimal (e.g. register names can/do change between chips).

Unknown said...

The only thing you have to change to make it work on the attiny85 is the adress of the timer interrupt mask its TIMSK in a Tiny85.

Great piece of code that helped me alot with my software pwming :) thanks

Ian said...

Hey thanks for the code, made for an evening of fun trying to get it to work with my ATTiny85 on a "Lectro Candle" kit from spark fun. Had to change output pins, TIMSK, and bring in my own delay function, but that's it!

Cheers!

inerlogic said...

alright.... what needs to be changed for common cathode LEDs?

Unknown said...

Thanks. Did the same for my girls but I put RGB Led in a plastic snowball. Effect are perfect.