Ambient lighting with a strip of SK6812-RGBW LEDs

De Wiki LOGre
< Éclairage à base de ruban à LEDs SK6812-RGBW
Révision de 18 mai 2017 à 21:45 par Ebonet (discuter | contributions) (Version en anglais.)

(diff) ← Version précédente | Voir la version courante (diff) | Version suivante → (diff)
Aller à : navigation, rechercher


Language: Français  • English

Projet by: Edgar Bonet.

This is intended as an ambient LED-strip lighting for my hallway. It serves a dual purpose:

  • when the hallway lamp is turned on, it emits a warm white light to complement that lamp's lighting;
  • when it's dark, it acts as a dark red night light.

This lighting is based on a SK6812-RGBW LED strip. Each pixel on this strip has, in addition to the traditional RGB LEDs, an extra “warm white” LED which enables a better color rendering than the R+G+B combination. The strip is almost 3 m long, with 60 LEDs/m, for a total of 175 LEDs. In addition to the strip, the device has:

  • a light-dependent resistor (LDR) for measuring the ambient light and deciding when to switch on each of the two lighting modes;
  • an ATtiny13A microcontroller for reading the LDR and driving the LED strip;
  • a power supply: 5 V, 4 A.

Preliminary measurements

Some measurements were needed for choosing the power supply and for tuning the firmware.

Light-dependent resistor

The LDR is connected in series with a 10 kΩ pullup resistor. It sits inside the lamp, close to the bulb, in order to be more sensitive to the bulb light than to daylight. The measured illumination is compared to two thresholds:

  • above the high threshold we assume the lamp is turned on and the strip lights up in white;
  • below the low threshold we assume it is night time and the strips lights as a night light.

In order to choose the appropriate thresholds, I took data for three days with an Arduino (for making the measurements) and a Raspberry Pi (for data logging). Here are the results, shown as resistance variations as a function of time:

Sk6812 ldr data-en.png

This graph shows that:

  • the lamp affects the LDR way more than daylight, which makes it easy to know when that lamp is on;
  • complete darkness is also easy to tell apart from the weak light coming from the nearby living room;
  • in the darkness, the LDR is more than 1 MΩ, but that value is only known very roughly, as the ADC is close to saturation.

Current consumption

Mesuring the current draw on a multimeter.

I measured the current draw as a function of the number of lit LEDs and their color. This was done on a 300 LED strip (5 m at 60 LEDs/m). Only a few LEDs were lit in this experiment because the strip was powered from my PC's USB port. Here are the results:

Sk6812 power.svg

A linear regression on this data gives the current per LED as a function of the (R, G, B, W) color:

[math]I = 0,72\;\text{mA} + 7,4\;\text{mA} \times \left(\frac{R}{255} + \frac{G}{255} + \frac{B}{255}\right) + 14,8\;\text{mA} × \frac{W}{255}[/math]

From this, we can derive the current needed to light the strip as pure white (RGBW = (0, 0, 0, 255)):

175 × (0,72 mA + 14,8 mA) ≈ 2,7 A

This led me to choosing a 4 A power supply in order to have some safety margin.

Electronics

The circuit built on a perfboard.

The circuit schematic:

Sk6812-schematic.svg

On the top, the power handling. On the bottom, the data path. It should be noted that the LDR sits inside the hallway lamp, hence the connector labeled “to LDR”. Note also that the strip is connected to this circuit from both ends: one end gets power and data (from the three-pin connector labeled “to LED strip”), the other end gets power only (from the two-pin “power out” connector). Powering the strip from both ends is a way to limit the resistive losses along the strip's power bus.

Firmware

Driving the LED strip

The SK6812 LEDs are very similar to the well known WS2812 and are driven in the same manner. In addition to the RGB version, these are available in an RGBW configuration, with a white LED which has a better color rendering than the RGB combination. The version I have is “warm white”. One must send 32 bits per pixel, in the sequence: green, red, blue, then white.

Libraries meant for driving WS2812s often support the SK6812-RGBW chip, but they require a large amount of RAM (4 bytes per LED) for storing all the bits to be sent. However, I wanted to drive the strip with a minimal setup, using an ATtiny13A, which has only 64 bytes of RAM.

Useful links:

Using the information from the second link, I managed to drive the strip from an ATtiny13A clocked at 9.6 MHz. Given the low clock frequency, I had to generate the pulses with assembly code. A “zero” bit is transmitted as a short pulse, generated by:

sbi PORTB, PB3
cbi PORTB, PB3

this pulse is two CPU-cycles long, i.e. 0.208 µs. A “one” bit is transmitted as a long pulse, having four wait cycles, like this:

sbi PORTB, PB3
rjmp .
rjmp .
cbi PORTB, PB3

which gives a pulse six cycles long, i.e. 0.625 µs. The datasheet of the LEDs states that a short pulse should be 0.3±0.15 µs long and a long pulse 0.6±0.15 µs. These requirements are thus met.

Hysteresis

In order to avoid the program quickly switching the LEDs on and off when the light level is close to a threshold, I added some hysteresis to each of the thresholds. These hysteretic thresholds control the transitions of a finite state machine with the following states:

  • WHITE: strip lit white;
  • OFF: strip off;
  • RED: night light mode, dark red.

Complete firmware

Here is the version of the firmware I am currently using:

/*
 * Drive the SK6812 LED strip from an ATtiny13A.
 *
 * Analog input on PB4 = ADC2
 * Data sent through PB3.
 */
 
#include <avr/io.h>
#include <util/delay.h>
 
#define LED_COUNT 175
 
// Colors:        0xGGRRBBWW
#define LED_WHITE 0x00110044
#define LED_OFF   0x00000000
#define LED_RED   0x00010000
 
// Thresholds.
#define TH_OFF_WHITE   64
#define TH_WHITE_OFF  128
#define TH_RED_OFF    992
#define TH_OFF_RED   1012
 
int main(void)
{
    enum { WHITE, OFF, RED } state = OFF, previous_state;
 
    // Clock the CPU @ 9.6 MHz.
    CLKPR = _BV(CLKPCE);  // enable prescaler change
    CLKPR = 0;            // prescaler = 1
 
    // Configure ADC.
    DIDR0  = _BV(ADC2D);  // disable digital input on ADC2
    ADMUX  = _BV(MUX1);   // input = ADC2
    ADCSRA = _BV(ADEN)    // enable
           | _BV(ADSC)    // start first conversion
           | _BV(ADPS2)   // clock @ F_CPU / 64...
           | _BV(ADPS1);  // ... = 150 kHz
 
    // Discard first ADC reading.
    loop_until_bit_is_clear(ADCSRA, ADSC);
 
    // Set PB3 as output.
    DDRB |= _BV(PB3);
 
    for (;;) {
        // Take an ADC reading.
        ADCSRA |= _BV(ADSC);
        loop_until_bit_is_clear(ADCSRA, ADSC);
        uint16_t reading = ADC;
 
        // Update state.
        previous_state = state;
        switch (state) {
            case WHITE:
                if (reading >= TH_WHITE_OFF)
                    state = OFF;
                break;
            case OFF:
                if (reading < TH_OFF_WHITE)
                    state = WHITE;
                else if (reading >= TH_OFF_RED)
                    state = RED;
                break;
            case RED:
                if (reading < TH_RED_OFF)
                    state = OFF;
                break;
        }
        if (state == previous_state) continue;
 
        // Select color.
        uint32_t color;
        if (state == WHITE) color = LED_WHITE;
        else if (state == OFF) color = LED_OFF;
        else color = LED_RED;
 
        // Update the LED strip.
        for (uint8_t i = 0; i < LED_COUNT; i++) {  // addressed LED
            uint32_t val = color;
            for (int j = 0; j < 32; j++) {  // bit index
                if (val & 0x80000000)       // long pulse
                    asm volatile(
                    "    sbi %[port], %[bit] \n"
                    "    rjmp .              \n"
                    "    rjmp .              \n"
                    "    cbi %[port], %[bit] \n"
                    :: [port] "I" (_SFR_IO_ADDR(PORTB)),
                        [bit]  "I" (PB3));
                else                        // short pulse
                    asm volatile(
                    "    sbi %[port], %[bit] \n"
                    "    cbi %[port], %[bit] \n"
                    :: [port] "I" (_SFR_IO_ADDR(PORTB)),
                        [bit]  "I" (PB3));
                val <<= 1;
            }
            _delay_us(1);  // limit rate
        }
        _delay_us(60);  // break
    }
}