Éclairage à base de ruban à LEDs SK6812-RGBW : Différence entre versions

De Wiki LOGre
Aller à : navigation, rechercher
(+ choix alim)
m (Force le nom de la page pour le changement de langue)
 
(5 révisions intermédiaires par un autre utilisateur non affichées)
Ligne 1 : Ligne 1 :
 
[[Category:Projets]] [[Category:Microcontroleur]]
 
[[Category:Projets]] [[Category:Microcontroleur]]
  
'''Projet réalisé par''' : [[Utilisateur:Ebonet|Edgar Bonet]].
+
{{langages|{{BASEPAGENAME}}}}
  
Le but de ce projet est de faire un éclairage d'appoint pour mon couloir
+
'''Projet réalisé par''' : [[Utilisateur:Ebonet|Edgar Bonet]].
à base d'un ruban à LEDs SK6812-RGBW. Cet éclairage aura deux fonctions:
 
  
* quand on allume la lumière, il s'allume en blanc chaud pour compléter l'éclairage d'origine ;
+
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 il fait noir, il s'allume en rouge sombre, façon veilleuse.
 
  
L'éclairage d'appoint sera assuré par un ruban à LEDs SK6812-RGBW.
+
* quand on allume la lumière de la pièce, il s'allume en blanc chaud pour compléter l'éclairage d'origine ;
Chaque pixel du ruban comporte, en plus des traditionnelles LEDs RGB,
+
* quand il fait noir, il s'allume en rouge sombre et fait office de veilleuse.
une LED de couleur blanc-chaud qui assure un meilleur rendu des couleurs
 
que la combinaison R+G+B. Il y aura 3 m de ruban à 60 LEDs/m,
 
soit 180 LEDs au total. En plus du ruban à LEDs, le dispositif
 
comportera :
 
  
* un capteur de lumière, pour savoir quand activer l'un des deux modes d'éclairage ;
+
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 :
* un microcontrôleur ATtiny, probablement un ATtiny13A, pour lire le capteur de lumière et piloter le ruban à LEDs ;
+
 
 +
* 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.
 
* une alimentation 5 V, 4 A.
  
== Mesure de la lumière ambiante ==
+
== Mesures préliminaires ==
 +
 
 +
Quelques mesures ont été nécessaires pour le choix du matériel et la mise au point du logiciel.
 +
 
 +
=== Photorésistance ===
  
Elle est réalisée à l'aide d'une photorésistance en série avec une
+
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 :
résistance de tirage de 10 kΩ. La photorésistance est placée tout
 
près de l'ampoule du couloir, 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 a été allumée, le ruban doit éclairer en blanc ;
+
* 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.
 
* en dessous du seuil bas, on considère qu'il fait nuit, le ruban doit passer en mode veilleuse.
  
== Pilotage du ruban à LEDs ==
+
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 :
 +
 
 +
[[Fichier:sk6812_ldr_data-fr.png|center]]
 +
 
 +
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é ===
 +
 
 +
[[Fichier:2017 02 09 Arduino LED.jpg|thumb|192px|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 :
 +
 
 +
[[Fichier:sk6812_power.svg|center]]
 +
 
 +
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&nbsp;=&nbsp;(0,&nbsp;0,&nbsp;0,&nbsp;255)) il faut
 +
 
 +
:175 × (0,72 mA + 14,8 mA) ≈ 2,7 A
 +
 
 +
Je me suis décidé sur une alimentation 4&nbsp;A, ce qui permet d'avoir une petite marge.
 +
 
 +
== Électronique ==
 +
 
 +
[[Fichier:sk6812-circuit.jpg|thumb|192px|Le circuit réalisé sur plaque à trous (perfboard).]]
 +
 
 +
Le schéma du circuit&nbsp;:
 +
 
 +
[[Fichier:sk6812-schematic.svg|center]]
 +
 
 +
En haut la partie 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é «&nbsp;to LDR&nbsp;». À noter aussi que les deux extrémités du ruban à LEDs sont connectées à ce circuit&nbsp;: l'une reçoit l'alimentation et le signal de commande (connecteur à trois broches «&nbsp;to LED strip&nbsp;»), l'autre reçoit seulement l'alimentation (connecteur à deux broches «&nbsp;power out&nbsp;»). 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&nbsp;bits par LED&nbsp;: 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&nbsp;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&nbsp;octets de RAM.
 +
 
 +
Liens utiles&nbsp;:
 +
 
 +
* [https://cpldcpu.com/2016/03/09/the-sk6812-another-intelligent-rgb-led/ The SK6812 – another intelligent RGB LED]&nbsp;: une introduction aux LEDs SK6812&nbsp;;
 +
* [https://wp.josh.com/2014/05/13/ws2812-neopixels-are-not-so-finicky-once-you-get-to-know-them/ NeoPixels Revealed: How to (not need to) generate precisely timed signals]&nbsp;: une étude des timings des signaux de commande, qui montre qu'on peut calculer les bits à envoyer à la volée plutôt que de les stocker préalablement en RAM.
 +
 
 +
En m'inspirant du deuxième lien, j'ai réussi à piloter le ruban avec un ATtiny13A cadencé par son oscillateur interne à 9,6&nbsp;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
 +
 
 +
<syntaxhighlight>
 +
sbi PORTB, PB3
 +
cbi PORTB, PB3
 +
</syntaxhighlight>
 +
 
 +
ce qui donne une largeur d'impulsion de deux cycles, soit 0,208&nbsp;µs. Le bit à une est représenté par une impulsion longue pour laquelle j'ajoute quatre cycles d'attente, comme ceci&nbsp;:
 +
 
 +
<syntaxhighlight>
 +
sbi PORTB, PB3
 +
rjmp .
 +
rjmp .
 +
cbi PORTB, PB3
 +
</syntaxhighlight>
  
Les LEDs SK6812 sont très similaires aux célèbres WS2812 et se pilotent
+
ce qui donne une impulsion d'une durée de six cycles, soit 0,625&nbsp;µs. La fiche technique des LEDs précise que l'impulsion courte est sensée durer 0.3±0.15&nbsp;µs et l'impulsion longue 0.6±0.15&nbsp;µs. Nous sommes donc bien conformes sur ce point.
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
+
=== Hystérésis ===
SK6812-RGBW, mais elles demandent toutes une grande quantité de RAM (4
 
octets par LED) pour stocker tous les bits à envoyer. Or je voudrais
 
piloter le ruban de façon minimaliste, avec seulement un ATtiny13A, qui
 
n'a que 64 octets de RAM.
 
  
Liens utiles :
+
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&nbsp;:
 +
* <code>WHITE</code>&nbsp;: ruban allumé en blanc&nbsp;;
 +
* <code>OFF</code>&nbsp;: ruban éteint&nbsp;;
 +
* <code>RED</code>&nbsp;: mode veilleuse, rouge sombre.
  
* [https://cpldcpu.com/2016/03/09/the-sk6812-another-intelligent-rgb-led/ The SK6812 – another intelligent RGB LED] : une introduction aux LEDs SK6812 ;
+
=== Firmware complet ===
* [https://wp.josh.com/2014/05/13/ws2812-neopixels-are-not-so-finicky-once-you-get-to-know-them/ NeoPixels Revealed: How to (not need to) generate precisely timed signals] : une étude des timings des signaux de commande, qui montre qu'on peut calculer les bits à envoyer à la volée plutôt que de les stocker préalablement en RAM.
 
  
En m'inspirant du deuxième lien, j'ai réussi à piloter le ruban avec un
+
Voici la version du firmware qui tourne en ce moment chez moi&nbsp;:
ATtiny13A. Ce programme fait progresser une LED bleue le long du ruban
 
dans un sens et une LED rouge dans l'autre sens :
 
  
 
<syntaxhighlight>
 
<syntaxhighlight>
 
/*
 
/*
  * Test the SK6812 LED strip.
+
  * Drive the SK6812 LED strip from an ATtiny13A.
* Version running on an ATtiny13A at 9.6 MHz.
 
 
  *
 
  *
  * Data sent through PB0.
+
* Analog input on PB4 = ADC2
 +
  * Data sent through PB3.
 
  */
 
  */
  
Ligne 66 : Ligne 118 :
 
#include <util/delay.h>
 
#include <util/delay.h>
  
#define LED_COUNT 300
+
#define LED_COUNT 175
 +
 
 +
// Colors:        0xGGRRBBWW
 +
#define LED_WHITE 0x00110044
 +
#define LED_OFF  0x00000000
 +
#define LED_RED  0x00010000
  
//                 GGRRBBWW
+
// Thresholds.
#define LED_OFF 0
+
#define TH_OFF_WHITE  64
#define LED_RED  0x00ff0000
+
#define TH_WHITE_OFF 128
#define LED_BLUE 0x0000ff00
+
#define TH_RED_OFF    992
 +
#define TH_OFF_RED  1012
  
 
int main(void)
 
int main(void)
 
{
 
{
 +
    enum { WHITE, OFF, RED } state = OFF, previous_state;
 +
 
     // Clock the CPU @ 9.6 MHz.
 
     // Clock the CPU @ 9.6 MHz.
 
     CLKPR = _BV(CLKPCE);  // enable prescaler change
 
     CLKPR = _BV(CLKPCE);  // enable prescaler change
 
     CLKPR = 0;            // prescaler = 1
 
     CLKPR = 0;            // prescaler = 1
  
     DDRB |= _BV(PB0);
+
    // 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 (;;) {
 
     for (;;) {
         for (int led = 0; led < LED_COUNT; led++) { // lit led
+
         // Take an ADC reading.
             for (int i = 0; i < LED_COUNT; i++) // addressed LED
+
        ADCSRA |= _BV(ADSC);
                uint32_t val;
+
        loop_until_bit_is_clear(ADCSRA, ADSC);
                if (i == led)
+
        uint16_t reading = ADC;
                    val = LED_BLUE;
+
 
                else if (i == LED_COUNT - 1 - led)
+
        // Update state.
                    val = LED_RED;
+
        previous_state = state;
                else
+
        switch (state) {
                    val = LED_OFF;
+
             case WHITE:
                for (int j = 0; j < 32; j++) {  // bit index
+
                if (reading >= TH_WHITE_OFF)
                    if (val & 0x80000000)      // long pulse
+
                    state = OFF;
                        asm volatile(
+
                break;
                        "    sbi %[port], %[bit] \n"
+
            case OFF:
                        "    rjmp .              \n"
+
                if (reading < TH_OFF_WHITE)
                        "    rjmp .              \n"
+
                    state = WHITE;
                        "    cbi %[port], %[bit] \n"
+
                else if (reading >= TH_OFF_RED)
                        :: [port] "I" (_SFR_IO_ADDR(PORTB)),
+
                    state = RED;
                          [bit]  "I" (PB0));
+
                break;
                    else                        // short pulse
+
            case RED:
                        asm volatile(
+
                if (reading < TH_RED_OFF)
                        "    sbi %[port], %[bit] \n"
+
                    state = OFF;
                        "    cbi %[port], %[bit] \n"
+
                break;
                        :: [port] "I" (_SFR_IO_ADDR(PORTB)),
+
        }
                          [bit]  "I" (PB0));
+
        if (state == previous_state) continue;
                    val <<= 1;
+
 
                }
+
        // 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(60);  // break
+
             _delay_us(1);  // limit rate
 
         }
 
         }
 +
        _delay_us(60);  // break
 
     }
 
     }
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
 
== Mesure du courant consommé ==
 
 
[[Fichier:2017 02 09 Arduino LED.jpg|thumb|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 sont effectuées 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. Voici les résultats :
 
 
[[Fichier:sk6812_power.svg|center]]
 
 
Une régression linéaire sur ces mesures donne le courant consommé par
 
chaque 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&nbsp;=&nbsp;(0,&nbsp;0,&nbsp;0,&nbsp;255)) il faut
 
 
:180 × (0,72 mA + 14,8 mA) ≈ 2,8 A
 
 
Je me suis décidé sur une alimentation 4&nbsp;A, ce qui permet d'avoir
 
une petite marge.
 

Version actuelle datée du 21 juin 2019 à 12:14


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 partie 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
    }
}