Le WS2812B
Présentation
Le WS2812B est à peu près le périphérique le plus simple que vous pouvez imaginer, du point de vue matériel.
Il a 4 broches: Vdd (3.5 à 5.3VDC), GND, Din et Dout.
Sur les pins Din et Din les bits de données représentant la luminosité RVB sont transmis en série
, la puce retire les premiers 24 bits (8 bits chacun pour R, G, B) et envoie les bits restants à la broche Dout.
En connectant les LED dans une chaîne, avec le Dout d'une LED allant à la Din de la prochaine LED, chaque LED enlève les bits dont elle a besoin de l'avant du flux de données, et envoie le reste du flux de données à la prochaine LED. En théorie, il n'y a pas de limite au nombre de LED que vous pouvez piloter avec une seule ligne de données - la seule limitation est que le temps nécessaire pour mettre à jour toutes les LED augmente linéairement avec le nombre de LED dans la chaîne. Cela fait un schéma très intelligent et efficace pour adresser des données RVB uniques à un nombre quelconque de LED câblées dans une chaîne.
Données série auto-synchronisées
Tous les protocoles de données série nécessitent une horloge pour réassembler les données reçues.
Cette horloge peut être un signal d'horloge explicite tel que la ligne SCK sur un périphérique SPI ou l'horloge peut être une horloge implicite pré-convenue telle que les paramètres de débit en bauds sur un périphérique UART (avec les bits START et STOP servant à synchroniser les données à l'horloge pré-convenue), ou l'horloge peut être intégrée dans le flux de données série.
Le WS2812B utilise une forme de cette troisième méthode, où chaque bit est constitué d'un '1' suivi d'un '0', et la valeur du bit est déterminée uniquement par si l'intervalle '1' est plus long ou plus court que l'intervalle '0'.
Pour le WS2812B, chaque bit est défini ainsi:
Chacun de ces temps a une tolérance de +/- 0.15μs, donc il y a une bonne marge de manœuvre à la synchronisation des données en série.
Les données série sont auto-cadencées, où seuls les rapports de '1' à '0' comptent, dans les limites de temps générales.
Un seul bit de données prend 1,25μs, ce qui signifie qu'un seul octet prend 10μs, et les 3 octets de données RGB prennent 30μs.
Gardez à l'esprit qu'il n'y a pas de temps supplémentaire entre les bits ou les octets ou les triplets d'octets.
Le seul délai que nous ajoutions jamais au flux de données est le délai de 'réinitialisation' d'au moins 50μs (mais il peut s'agir d'une durée plus longue que celle-ci).
Ainsi, un '0' inactif sur la ligne de sortie de données d'au moins 50μs réinitialise toutes les puces pour le lot suivant de données entrantes.
Le microcontrôleur
Entre les deux familles de microcontrôleurs que nous utilisons dans ce tutoriel, nous utiliserons un AVR pour piloter les puces WS2812B.
Une des principales raisons de ce choix est que le WS2812B est essentiellement une puce de 5V, et AVR peut fonctionner à 5V, contrairement à la plupart des ARM Cortex M3.
Cela nous évite d'avoir à dériver une tension de 3,3 V en courant continu à partir de la tension de la puce LED de 5 V, et de devoir fournir une grille de décalage de niveau d'un 3,3 V cc aux puces LED 5 V.
Faire les maths
De nombreux AVR peuvent fonctionner sur un oscillateur interne de 8 MHz ou 0,125 μs/cycle.
Cela signifie qu'un bit de données de 1,25μs en seulement 10 cycles d'horloge.
Ce n'est pas beaucoup! C'est possible d'écrire le code en assembleur AVR qui peut piloter le WS2812B correctement avec un AVR 8MHz.
Nous ne pouvons pas obtenir les temps exacts spécifiés ci-dessus, mais nous pouvons obtenir un timing qui correspond bien aux limites de temps.
Pour être précis, pour sortir le bit '1', 7 cycles d'horloge (0,875μs) à '1' suivi de 3 cycles (0,375μs) à '0' ,
et pour le bit '0' de 3 cycle (0,375μs) à '1', suivi de 7 cycle (0,875μs) à '0'.
De plus, nous devons garder ce timing sur une chaîne de N octets (ou plutôt, 3 x N octets pour N LEDs).
Le code
Ce qui suit est le code du langage assembleur que j'ai imaginé pour piloter les WS2812B. Il a fallu une bonne dose de comptage de cycles et de violon, mais à la fin, j'ai été ravi de découvrir que j'avais atteint les spécifications. Je vais d'abord lister le code, puis l'expliquer. À propos, la fonction s'appelle output_grb parce que pour une raison inexplicable le WS2812B requiert des données sérielles dans l'ordre G-R-B plutôt que l'ordre universel R-G-B. Allez comprendre. Pour chaque octet, les données sont décalées en commençant par le MSB, bit 7.
#define __SFR_OFFSET 0
#include <avr/io.h>
;extern void output_grb(u8 * ptr, u16 count)
;
; r18 = data byte
; r19 = 7-bit count
; r20 = 1 output
; r21 = 0 output
; r22 = SREG save
; r24:25 = 16-bit count
; r26:27 (X) = data pointer
.equ OUTBIT, 0
.global output_grb
output_grb:
movw XH:XL, r24:r25 ; X = p_buf
movw r24:r25, r22:r23 ;r24:25 = count
in r22, SREG ;save SREG (global int state)
cli ;no interrupts from here on, we're cycle-counting
in r20, PORTB
ori r20, (1<<OUTBIT) ;our '1' output
in r21, PORTB
andi r21, ~(1<<OUTBIT) ;our '0' output
ldi r19, 7 ;7 bit counter (8th bit is different)
ld r18,X+ ;get first data byte
loop1:
out PORTB, r20 ; 1 +0 start of a bit pulse
lsl r18 ; 1 +1 next bit into C, MSB first
brcs L1 ; 1/2 +2 branch if 1
out PORTB, r21 ; 1 +3 end hi for '0' bit (3 clocks hi)
nop ; 1 +4
bst r18, 7 ; 1 +5 save last bit of data for fast branching
subi r19, 1 ; 1 +6 how many more bits for this byte?
breq bit8 ; 1/2 +7 last bit, do differently
rjmp loop1 ; 2 +8, 10 total for 0 bit
L1:
nop ; 1 +4
bst r18, 7 ; 1 +5 save last bit of data for fast branching
subi r19, 1 ; 1 +6 how many more bits for this byte
out PORTB, r21 ; 1 +7 end hi for '1' bit (7 clocks hi)
brne loop1 ; 2/1 +8 10 total for 1 bit (fall thru if last bit)
bit8:
ldi r19, 7 ; 1 +9 bit count for next byte
out PORTB, r20 ; 1 +0 start of a bit pulse
brts L2 ; 1/2 +1 branch if last bit is a 1
nop ; 1 +2
out PORTB, r21 ; 1 +3 end hi for '0' bit (3 clocks hi)
ld r18, X+ ; 2 +4 fetch next byte
sbiw r24, 1 ; 2 +6 dec byte counter
brne loop1 ; 2 +8 loop back or return
out SREG, r22 ; restore global int flag
ret
L2:
ld r18, X+ ; 2 +3 fetch next byte
sbiw r24, 1 ; 2 +5 dec byte counter
out PORTB, r21 ; 1 +7 end hi for '1' bit (7 clocks hi)
brne loop1 ; 2 +8 loop back or return
out SREG, r22 ; restore global int flag
ret
Ce code est une fonction appelable dans C. En C la déclaration de fonction est
extern void output_grb (u8 * ptr, nombre de u16);
Ainsi, il prend deux arguments, un pointeur (16 bits) pour un tableau de données de 8 bits, et un compteur de 16 bits du nombre d'octets dans le tableau. Puisque nous sommes en train d'écrire une fonction ASM appelée par C, nous avons besoin de connaître certains détails avr-gcc, tels que comment déclarer une telle fonction et la rendre visible au code C, comment accéder aux paramètres passés, et Les registres AVR que nous pouvons utiliser sans se soucier de les restaurer au retour. Nous pouvons trouver toutes ces informations sur le Wiki avr-gcc. Par exemple, nous apprenons que le 1er paramètre est passé en r24: r25, le 2ème paramètre en r22: r23, et que les registres r18-r27, r30, r31 peuvent être utilisés sans restauration.
Sur la base de cette information, nous pouvons affecter notre pointeur de données à r26: 27 (la paire de registres 'X') et notre compteur de données à r24: 25. Nous devons déplacer notre compteur de données car l'instruction sbiw ne fonctionne qu'avec des paires de registres commençant à r24. Maintenant, nous allons regarder chaque section du code à tour de rôle, alors voici la section d'initialisation de notre fonction:
Initialization
.global output_grb
output_grb:
movw XH:XL, r24:r25 ; X = p_buf
movw r24:r25, r22:r23 ;r24:25 = count
in r22, SREG ;save SREG (global int state)
cli ;no interrupts from here on, we're cycle-counting
in r20, PORTB
ori r20, (1<<OUTBIT) ;our '1' output
in r21, PORTB
andi r21, ~(1<<OUTBIT) ;our '0' output
ldi r19, 7 ;7 bit counter (8th bit is different)
ld r18,X+ ;get first data byte
Ici, nous déplaçons les deux paramètres de 16 bits comme mentionné, enregistrer SREG (avec un drapeau d'interruption globale) en r22 maintenant libre et désactiver les interruptions (toutes les interruptions du tout vont faire exploser ce code en raison du timing serré), puis nous lisons notre port de sortie (PORTB) et créer 2 copies des données de port - un avec une valeur de sortie '0' sur notre broche de sortie de données série, et un avec une valeur de sortie '1' sur la broche de sortie. Avec ces deux valeurs sauvegardées, nous pouvons rapidement écrire un '0' ou un '1' sur la ligne de sortie de données série (et rapide est le nom du jeu avec 10 cycles par bit!).
Ensuite, nous chargeons un registre de compteur avec le numéro 7. Notre algorithme utilise un code différent pour les 7 premiers bits de chaque octet que pour le 8ème bit, car sur le 8ème bit, nous devons récupérer le prochain octet de données, décrémenter le compteur d'octets et quittez si nous sommes à 0. Ce compteur de bits nous dira quand nous avons décalé 7 bits de données. Finalement, nous obtenons le premier octet de données et tombons dans la boucle de sortie série.
'0' Data Bit, First 7 Bits
loop1:
out PORTB, r20 ; 1 +0 start of a bit pulse
lsl r18 ; 1 +1 next bit into C, MSB first
brcs l1 ; 1/2 +2 branch if 1
out PORTB, r21 ; 1 +3 end hi for '0' bit (3 clocks hi)
nop ; 1 +4
bst r18, 7 ; 1 +5 save last bit of data for fast branching
subi r19, 1 ; 1 +6 how many more bits for this byte?
breq bit8 ; 1/2 +7 last bit, do differently
rjmp loop1 ; 2 +8, 10 total for 0 bit
Au début de chaque bit de données, nous envoyons un '1' et décalons le bit à envoyer (le MSB de r18) dans le drapeau de report. Ensuite, nous branchons à l'étiquette «L1» si le bit est un «1», ou tomber à travers si c'est un «0». Ici, nous allons nous concentrer sur l'état «fall-thru» ou «0». Notez après chaque instruction qu'il y a 2 nombres dans le commentaire. Le premier est le nombre d'horloge de l'unité centrale de l'instruction, et le second est le nombre total d'heures écoulées depuis le début du bit de sortie. Les instructions de branchement prennent 1 heure si la branche n'est pas prise et 2 si elle est prise, d'où la notation 1/2. Le 2ème nombre est le nombre total d'horloges depuis le début de la sortie du bit. Ainsi nous voyons que pour un bit de sortie '0', quand nous tombons par l'instruction brcs et sortons un '0' à la ligne de sortie, notre impulsion '1' a été 3 horloges ou 375ns et après 3 horloges nous avons réglé la sortie à '0'.
Après avoir mis la ligne de sortie à '0' nous avons un nop, alors nous déplaçons le MSB de r18 (c'est le bit suivant à sortir, pas le bit courant) dans le flag T. Nous faisons cela pour tous les 7 bits (7 fois dans la boucle), mais nous nous intéressons uniquement au dernier ou au huitième bit. Pour les autres bits, l'indicateur T est ignoré. Ensuite, nous décrémenterons notre compteur de bits et si 0, nous nous connecterons à 'bit8' pour sortir le 8ème bit. Si notre compteur de bits n'est pas 0, nous retournons à 'loop1' pour sortir un autre bit. Notez que dans le cas d'une boucle vers 'loop1' notre boucle totale a pris 10 horloges pour sortir un bit de données '0'. Nous regarderons l'affaire pour le huitième bit plus tard.
Ensuite, nous allons regarder le cas où l'un des 7 premiers bits est un '1':
'1' Data Bit, First 7 Bits
L1:
nop ; 1 +4
bst r18, 7 ; 1 +5 save last bit of data for fast branching
subi r19, 1 ; 1 +6 how many more bits for this byte
out PORTB, r21 ; 1 +7 end hi for '1' bit (7 clocks hi)
brne loop1 ; 2/1 +8, 10 total for 1 bit (fall thru if last bit)
Quand nous arrivons à 'L1' nous sommes à +4 horloges plutôt que +3 parce qu'une branche prise est 2 horloges, pas 1. Encore une fois nous avons un nop suivi par la copie du bit 7 dans T, puis le décompte du compteur de bits. Ensuite, nous mettons la ligne de sortie à '0' (en faisant une sortie '1' de 7 horloges) et ensuite, testons le résultat de la décrémentation du compteur de bits (nous pouvons le faire car l'instruction 'out' ne modifie aucun des Indicateurs de CPU). Si nous ne sommes pas au bit 8, nous prenons la branche (2 autres horloges) pour un total de 10 horloges, comme désiré. Si nous ne prenons pas la branche, nous passons à 'bit8'
'0' Data Bit, 8th Bit
bit8:
ldi r19, 7 ; 1 +9 bit count for next byte
out PORTB, r20 ; 1 +0 start of a bit pulse
brts L2 ; 1/2 +1 branch if last bit is a 1
nop ; 1 +2
out PORTB, r21 ; 1 +3 end hi for '0' bit (3 clocks hi)
ld r18, X+ ; 2 +4 fetch next byte
sbiw r24, 1 ; 2 +6 dec byte counter
brne loop1 ; 2 +8 loop back or return
out SREG, r22 ; restore global int flag
ret
Rappelez-vous que lorsque nous nous branchons ou que nous tombons à 'bit8', nous n'avons exécuté que 9 horloges. Nous allons maintenant utiliser cette 10ème cycle d'horloge pour charger le compteur de bits avec 7 pour l'octet suivant.
Maintenant à 10 cycles horloges nous définissons la ligne de sortie à '1' pour le début du 8ème bit de données.
Comme nous avons déjà déplacé le 8ème bit de données dans T, nous n'avons pas besoin de décaler le bit pour que nous sauvegardions une horloge, ce dont nous aurons besoin plus tard.
Pour un bit '0', nous passons par la branche 'brts' et faisons un nop avant de mettre la ligne de sortie de données à '0' (pour garder avec un HI de 3 cycles pour un bit '0').
Maintenant, nous chargeons l'octet de données suivant dans r18 et décrémentez le compteur d'octets. Si nous avons plus d'octets à faire, nous retournons à 'loop1' pour l'octet suivant. Sinon, nous restaurons les interruptions globales et revenons de l'appel de sous-programme.
'1' Data Bit, 8th Bit
L2:
ld r18, X+ ; 2 +3 fetch next byte
sbiw r24, 1 ; 2 +5 dec byte counter
out PORTB, r21 ; 1 +7 end hi for '1' bit (7 clocks hi)
brne loop1 ; 2 +8 loop back or return
out SREG, r22 ; restore global int flag
ret
Si notre huitième bit de données est à '1', nous sautons le nop (parce que nous avons pris les brts, en ajoutant une horloge supplémentaire), puis chargeons l'octet suivant et décrémente le compteur d'octets, tout comme dans le bit '0'. Après avoir décrémenté le compteur, nous avons mis la ligne de sortie à '0', puis ramené à 'loop1' s'il y a plus d'octets de données à produire. Sinon, nous restaurons les interruptions globales et renvoyons, comme dans le cas du bit '0'.
Une mise en garde
Chaque WS2812B peut consommer jusqu'à 18.5mA par LED, ou 55.5mA si les 3 LED de la puce sont entièrement allumées (ce qui serait blanc brillant).
À 5V cela équivaut à environ 92mW par LED ou jusqu'à 277mW par puce.
Il est très facile de se retrouver avec une bande de WS2812B qui demande beaucoup d'Ampères de courant - une seule bande de 1 mètre avec 60 / mètre pourrait tirer jusqu'à 3,3 Ampères et 16,5 Watts.
Donc, faites d'abord les calculs, et assurez-vous que votre alimentation, vos fils et vos connecteurs peuvent gérer le courant que votre installation peut tirer.
Un exemple simple
Voici un exemple simple de code C qui conduit une chaîne de 6 LED de WS2812Bs.
Une façon très courante d'acheter ces LED est de les placer dans des bandes flexibles de 30, 60 ou 144 LED par mètre, et vous pouvez en couper autant que nécessaire et connecter le Din, le Vdd et le GND à l'avant de la bande ( ils ont des flèches ou du texte indiquant la direction du flux de données).
Nous avons donc coupé une bande de 6 LED que nous allons utiliser pour notre exemple.
Pour 6 LED RVB, nous aurons besoin d'un tampon de données de 6 * 3 ou 18 octets. Nous ferons tour à tour chaque LED à travers un motif de rouge, vert, bleu, jaune (rouge + vert), aqua (vert + bleu) et violet (rouge + bleu). Rappelez-vous que l'ordre des données pour le WS2812B est GRB, donc par exemple, buf [0] contiendra la valeur verte pour le 1er WS2812B, buf [1] contiendra la valeur rouge, et buf [2] contiendra la valeur bleue, buf [3] contiendrait la valeur verte pour le 2ème WS2812B, et ainsi de suite.
Le bit de sortie pour les données série dans cet exemple est PB0. Ce code montre également l'utilisation de la fonction de délai avr-gcc _delay_ms (), que je ne recommande pas en général (utilisez des minuteurs et des interruptions) mais dans ce cas, c'était rapide et facile et ne provoque aucune complication.
//
// AVR_2812
// 6 WS2812B LEDs
// 8MHz internal osc
//
#define F_CPU 8000000
#include <avr/io.h>
#include <util/delay.h>
#include <stdint.h>
typedef uint8_t u8;
typedef uint16_t u16;
#define NUM_WS2812 6
#define NUM_LEDS (NUM_WS2812*3)
enum {S_R, S_G, S_B, S_Y, S_V, S_T};
#define MAX 50
// declaration of our ASM function
extern void output_grb(u8 * ptr, u16 count);
void set_color(u8 * p_buf, u8 led, u8 r, u8 g, u8 b)
{
u16 index = 3*led;
p_buf[index++] = g;
p_buf[index++] = r;
p_buf[index] = b;
}
int main(void)
{
u8 buf[NUM_LEDS];
int count = 0;
DDRB = 1; // bit 0 is our output
memset(buf, 0, sizeof(buf));
u8 state = S_R;
u8 val = 0;
u8 first_time = 1;
while(1)
{
output_grb(buf, sizeof(buf));
switch (state)
{
case S_R:
if (++val <= MAX)
{
if (!first_time)
{
set_color(buf, 5, val, MAX-val, MAX-val);
}
set_color(buf, 0, val, 0, 0);
}
else
{
first_time = 0;
state = S_G;
val = 0;
}
break;
case S_G:
if (++val <= MAX)
{
set_color(buf, 0, MAX-val, val, 0);
set_color(buf, 1, 0, val, 0);
}
else
{
state = S_B;
val = 0;
}
break;
case S_B:
if (++val <= MAX)
{
set_color(buf, 1, 0, MAX-val, val);
set_color(buf, 2, 0, 0, val);
}
else
{
state = S_Y;
val = 0;
}
break;
case S_Y:
if (++val <= MAX)
{
set_color(buf, 2, val, 0, MAX-val);
set_color(buf, 3, val, val, 0);
}
else
{
state = S_V;
val = 0;
}
break;
case S_V:
if (++val <= MAX)
{
set_color(buf, 3, MAX-val, MAX-val, val);
set_color(buf, 4, val, 0, val);
}
else
{
state = S_T;
val = 0;
}
break;
case S_T:
if (++val <= MAX)
{
set_color(buf, 4, MAX-val, val, MAX-val);
set_color(buf, 5, 0, val, val);
}
else
{
state = S_R;
val = 0;
}
break;
default:
state = S_R;
break;
}
_delay_ms(100);
}
}
Dimensions
Sachan que la led WS2812 dait 5x5mm