Éclairage à base de ruban à LEDs SK6812-RGBW

De Wiki LOGre
Aller à : navigation, rechercher


Langue : Français  • English


Projet réalisé par : Edgar Bonet.

Le but de ce projet est de faire un éclairage d'appoint à base d'un ruban à LEDs pour le vestibule de mon appartement. Cet éclairage a deux fonctions :

  • quand on allume la lumière de la pièce, il s'allume en blanc chaud pour compléter l'éclairage d'origine ;
  • quand il fait noir, il s'allume en rouge sombre et fait office de veilleuse.

L'éclairage est assuré par un ruban à LEDs SK6812-RGBW. Chaque pixel de ce ruban comporte, en plus des traditionnelles LEDs RGB, une LED de couleur blanc-chaud qui assure un meilleur rendu des couleurs que la combinaison R+G+B. Il y a un peu moins de 3 m de ruban à 60 LEDs/m, soit 175 LEDs au total. En plus du ruban à LEDs, le dispositif comporte :

  • une photorésistance qui fait office de capteur de lumière, pour savoir quand activer l'un des deux modes d'éclairage ;
  • un microcontrôleur ATtiny13A, pour lire la photorésistance et piloter le ruban ;
  • une alimentation 5 V, 4 A.

Mesures préliminaires

Quelques mesures ont été nécessaires pour le choix du matériel et la mise au point du logiciel.

Photorésistance

La photorésistance est en série avec une résistance de tirage de 10 kΩ. Elle est placée tout près de l'ampoule du luminaire, de sorte que celle-ci l'éclaire bien plus que la lumière du jour. La luminosité mesurée est alors comparée à deux seuils :

  • au dessus du seuil haut, on considère que la lumière doit être allumée, et le ruban doit éclairer en blanc ;
  • en dessous du seuil bas, on considère qu'il fait nuit, le ruban doit passer en mode veilleuse.

Afin de déterminer les seuils appropriés, j'ai accumulé des mesures pendant trois jours à l'aide d'un Arduino (pour prendre les mesures) et un Raspberry Pi (pour les enregistrer dans un fichier). Voici les résultats, exprimés en résistance en fonction du temps :

Sk6812 ldr data-fr.png

On constate sur ce graphique que :

  • la lampe éclaire la photorésistance bien plus que la lumière du jour, ce qui permet de reconnaître sans ambigüité l'allumage de cette lampe ;
  • l'obscurité nocturne est aussi clairement distinguée de la pénombre qu'on a quand seule une lumière du salon est allumée ;
  • la nuit, la photorésistance affiche plus de 1 MΩ, mais sa valeur est déterminée de façon très imprécise à cause de la discrétisation du convertisseur analogique-numérique qui est proche de la saturation.

Courant consommé

Mesure au multimètre du courant consommé.

J'ai réalisé des mesures de courant consommé en fonction du nombre de LEDs allumées et de leur couleur. Ces mesures ont été faites sur un ruban de 300 LEDs (5 m et 60 LEDs par mètre). Elles sont limitées à un petit nombre de LEDs allumées car c'est le port USB de mon portable qui sert d'alimentation pour cette expérience. Voici les résultats :

Sk6812 power.svg

Une régression linéaire sur ces mesures donne le courant consommé par LED en fonction de sa couleur (R, G, B, W) :

[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]

À partir de là, on déduit que pour allumer le ruban en blanc seul (RGBW = (0, 0, 0, 255)) il faut

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

Je me suis décidé sur une alimentation 4 A, ce qui permet d'avoir une petite marge.

Électronique

Le circuit réalisé sur plaque à trous (perfboard).

Le schéma du circuit :

Sk6812-schematic.svg

En haut la parie alimentation, en bas la partie signal. À noter que la photorésistance est déportée tout près de l'ampoule, d'où le connecteur libellé « to LDR ». À noter aussi que les deux extrémités du ruban à LEDs sont connectées à ce circuit : l'une reçoit l'alimentation et le signal de commande (connecteur à trois broches « to LED strip »), l'autre reçoit seulement l'alimentation (connecteur à deux broches « power out »). Le fait d'alimenter le ruban des deux côtés permet de limiter les pertes ohmiques sur ses longues pistes d'alimentation.

Firmware

Pilotage du ruban à LEDs

Les LEDs SK6812 sont très similaires aux célèbres WS2812 et se pilotent de la même manière. Outre la classique version RGB, elles existent aussi en version RGBW, avec une LED blanche qui permet de faire de l'éclairage avec un meilleur rendu de couleurs qu'en combinant RGB. La version que j'ai est un blanc chaud. Il faut envoyer 32 bits par LED : dans l'ordre vert, rouge, bleu et blanc.

Les bibliothèques pour piloter les WS2812 supportent souvent les SK6812-RGBW, mais elles demandent toutes une grande quantité de RAM (4 octets par LED) pour stocker tous les bits à envoyer. Or je voulais piloter le ruban de façon minimaliste, avec seulement un ATtiny13A, qui n'a que 64 octets de RAM.

Liens utiles :

En m'inspirant du deuxième lien, j'ai réussi à piloter le ruban avec un ATtiny13A cadencé par son oscillateur interne à 9,6 MHz. Compte tenu de la fréquence d'horloge modérée, il a fallu générer les impulsions en assembleur. Un bit à zéro est représenté par une impulsion courte, générée avec

sbi PORTB, PB3
cbi PORTB, PB3

ce qui donne une largeur d'impulsion de deux cycles, soit 0,208 µs. Le bit à une est représenté par une impulsion longue pour laquelle j'ajoute quatre cycles d'attente, comme ceci :

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

ce qui donne une impulsion d'une durée de six cycles, soit 0,625 µs. La fiche technique des LEDs précise que l'impulsion courte est sensée durer 0.3±0.15 µs et l'impulsion longue 0.6±0.15 µs. Nous sommes donc bien conformes sur ce point.

Hystérésis

Pour éviter que le programme n'hésite lorsqu'il est proche d'un seuil de luminosité, une plage d'hystérésis est ajoutée à chacun de ses seuils. Ceux-ci gouvernent les transitions d'une machine à état qui a les trois états suivants :

  • WHITE : ruban allumé en blanc ;
  • OFF : ruban éteint ;
  • RED : mode veilleuse, rouge sombre.

Firmware complet

Voici la version du firmware qui tourne en ce moment chez moi :

/*
 * 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
    }
}