IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Voir le flux RSS

Le blog de f-leb

[Actualité] [FPGA] Créer un circuit logique pour piloter un anneau de LED adressables WS2812B

Noter ce billet
par , 02/08/2023 à 23h31 (7637 Affichages)
Les Arduinautes connaissent bien ce genre de LED programmables que l'on retrouve souvent sous la forme de rubans souples de longueur 1m, 3m, ... 10m et plus, et que l'on utilise pour créer des effets lumineux personnalisés. Voir les bibliothèques FastLED ou NeoPixel par Adafruit.

Pour la démonstration, j'utiliserai un anneau de 12 LED WS2812B (quelques euros l'anneau en cherchant bien), mais que je connecterai à une carte FPGA Altera DE0-Nano.

Nom : 20230802_130816.jpg
Affichages : 3629
Taille : 154,0 Ko
La carte FPGA, en grand seigneur des ténèbres, contrôle les pouvoirs de l'anneau Unique (forgé par Sauron)... mais je m'égare

/!\ Attention : une seule LED de cet anneau à la luminosité maximale peut consommer jusqu'à 50 mA. Il faudra prévoir une alimentation extérieure ou veiller à ne pas dépasser la limite de courant en contrôlant le nombre de LED allumées simultanément, leurs couleurs et leurs luminosités.

Principe

Ces rubans ou anneaux de LED sont constitués de LED multicolores RGB (Red-Green-Blue) adressables connectées en cascade, c’est à dire que l’on peut définir la luminosité et la couleur de chaque LED indépendamment. Le contrôleur intégré à chaque LED récupère et traite les données série en entrée (sur un seul fil) pour allumer sa LED ou communiquer les données à la LED suivante en sortie selon un timing et un protocole série bien précis.

Nom : cascad-method.jpg
Affichages : 1698
Taille : 14,9 Ko
Principe avec trois LED en cascade d'après WS2812B datasheet

Protocole de transmission série

Au niveau des données à communiquer, c'est très simple, la couleur de la LED est définie par les 24 bits des composantes Rouge-Vert-Bleu selon le schéma ci-dessous :

Nom : data-24bits.jpg
Affichages : 1707
Taille : 21,6 Ko
D'après WS2812B datasheet

Le bit de poids fort est transmis en premier, avec les couleurs suivant l'ordre : Green (G7 à G0), Red (R7 à R0) puis Blue (B7 à B0). C'est tout...

La transmission en cascade est aussi simple sur le principe. Au démarrage (ou après un signal Reset), le contrôleur de la LED PIX1 se sert des 24 premiers bits reçus pour allumer sa LED à la bonne couleur. Les signaux des 24 bits suivants seront reconditionnés par PIX1 et transmis sur sa sortie vers PIX2 qui s'en servira pour allumer sa LED PIX2. Et on poursuit... PIX1 reçoit les 24 bits suivants, qui seront transmis à PIX2, et PIX2 transmettra à PIX3 pour allumer sa LED PIX3, etc.
Chaque LED reçoit donc 24 bits de données et transmet le reste des données à la LED suivante. Pour piloter les 12 LED de l'anneau, il faut donc transmettre 12 x 24 = 288 bits. Pour redémarrer le cycle à partir de la première LED, on envoie un signal Reset et on recommence.

Les signaux physiques

Comment générer physiquement des "0" et des "1" sur un fil ? En suivant le codage des diagrammes suivants, extraits de la documentation :

Nom : sequence-chart.jpg
Affichages : 1701
Taille : 18,4 Ko
D'après WS2812B datasheet
T0H = 0,4 µs, T0L = 0,85 µs, T1H = 0,8 µs, T1L = 0,45 µs, avec une tolérance ± 150 ns.
Treset doit être supérieur à 50 µs.

Ce codage consiste donc à envoyer des impulsions électriques de durée variable pour représenter les bits "0" ou "1". Un état haut pendant 0,4 µs suivi d'un état bas pendant 0,85 µs représente un bit "0". Un état haut pendant 0,8 µs suivi d'un état bas pendant 0,45 µs représente un bit "1".
Ainsi, la durée de transmission d'un bit est : TH + TL = 1,25 µs ±300 ns.

Le contrôleur FPGA

Le contrôleur à implémenter dans la puce FPGA est donc le circuit qui génèrera en sortie le signal à transmettre au ruban ou à l'anneau.

Nom : rtl-view-ws2812b-controller.jpg
Affichages : 1705
Taille : 37,1 Ko
Le contrôleur FPGA

Entrées :
  • clk : à connecter à l'horloge principale 50 MHz. La durée d'un bit correspond à 60 périodes de l'horloge (60 / 50.106 = 1,2 µs).
  • address[7..0] : bus 8 bits, adresse de la LED en cours. La 1re LED est à l'adresse 0.
  • red[7..0], green[7..0], blue[7..0] : bus 8 bits, composantes rouge, verte et bleue de la LED en cours.
  • load : lorsque cette entrée est active, et sur front montant de l'horloge, un registre interne est chargé avec les composantes rouge, verte et bleue présentes en entrée pour la LED dont l'adresse est présente en entrée.
  • latch_n : entrée active à l'état bas, le registre interne est déverrouillé sur front descendant de l'horloge, et les données série sont transmises en sortie.

Sortie :
  • data_ws2812b : les données série à transmettre en entrée du ruban ou de l'anneau. La transmission débute au front montant de l'horloge qui suit le front descendant de l'entrée latch_n. Une fois les 12 x 24 bits transmis, la sortie est à l'état bas. Un signal Reset est donc transmis si l'état bas est maintenu pendant au moins 50 µs.


Ci-dessous, on montre des chronogrammes obtenus par simulation avec seulement deux LED :

Nom : waveform.jpg
Affichages : 1710
Taille : 108,5 Ko

  • 1re LED à l'adresse 0 : Rouge=0x33, Vert=0x44, Bleu=0x55
  • 2nd LED à l'adresse 1 : Rouge=0x66, Vert=0x77, Bleu=0x88

On voit le chargement du registre interne colors (2 x 24 bits) sur front montant de l'horloge lorsque l'entrée load est active.
Une fois le registre chargé, les données commencent à être transmises un cycle d'horloge après le front descendant du signal latch_n.

Si on fait un zoom arrière sur ces chronogrammes, on visualise le début de la trame envoyée (les huit premiers bits) juste après l'impulsion du signal latch_n (entourée en rouge) :

Nom : waveform2.jpg
Affichages : 1723
Taille : 245,2 Ko
Le premier octet transmis : 0x44, la composante verte de la 1re LED à l'adresse 0

Voici le code commenté du contrôleur en langage Verilog :

ws2812b_controller.v :
Code Verilog : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
module ws2812b_controller #(parameter NB_LEDS = 12)
   (
      input clk,				// borloge 50 MHz
      //input reset,       // reset du ruban
      input [7:0] red,
      input [7:0] green,
      input [7:0] blue,
      input [7:0] address, // numéro de la Led entre 0 et NB_LEDS-1
      input load,          // chargement du registre interne (colors)
      input latch_n,       // déverrouille les données et débute la transmission sur front descendant
      
      output data_ws2812b
   );
   
   reg [24 * NB_LEDS - 1 : 0] colors = 0;
   reg latch_n_previous = 1;
   
   localparam  T_HIGH   = 20, // 20 périodes à 50 Mhz = 20 x 20ns = 0,4 us
               T_LOW    = 40, // 40 périodes à 50 Mhz = 40 x 20ns = 0,8 us
               //T_RESET   = 3250,
               T        = T_HIGH + T_LOW;
               
   reg [11:0] t_counter = 0;           // compteur pour le temps
   reg [13:0] rgb_data_index = 0;      // indice de position entre 0 et 23 
   reg transfer_state_reg = 0;
   
   wire load_state;
   wire transfer_state;
   assign load_state = (load==1) & (address < NB_LEDS);
   assign transfer_state = (transfer_state_reg==1) & (~load_state);
   
   
   always@(posedge clk) begin
      if (load_state) begin  // chargement du registre
         colors[(24 * (NB_LEDS - address) - 1)-:24] = {green, red, blue};
      end
   end
   

   always@(posedge clk) begin
      if (transfer_state) begin  // transmission des données

         t_counter <= t_counter - 12'b1;
         
         if (t_counter==0) begin
            t_counter <= T;
            rgb_data_index <= rgb_data_index - 1;
            
            if (rgb_data_index==0) begin
               t_counter <= T;
               rgb_data_index <= 24 * NB_LEDS - 1;
               transfer_state_reg <= 0;
            end         
         end         
      end // if transfer_state   
      
      else if (!latch_n && latch_n != latch_n_previous) begin // si front descendant de latch_n
         rgb_data_index <= 24 * NB_LEDS - 1;
         t_counter <= T;
         transfer_state_reg <= 1;
      end
      
      latch_n_previous <= latch_n;     
   end

   // signal de sortie  
   assign data_ws2812b = (transfer_state==1) & (colors[rgb_data_index] ? t_counter > (T - T_LOW) : t_counter > (T - T_HIGH));
   
   
endmodule

Exemples d'application

Pour animer l'anneau de LED, il faut maintenant mettre en œuvre la description principale (fichier top.v ci-dessous) qui instancie le contrôleur, raccorde ses entrées à des signaux de contrôle et sa sortie vers l'entrée IN de l'anneau (lignes 27 à 39). Pour cette démonstration, le code ci-dessous va charger les couleurs initiales de chacune des 12 LED (lignes 45 à 59) puis, à intervalle régulier, on décale le motif initial d'une LED (lignes 80 à 84) pour donner l'illusion d'une roue multicolore qui se met à tourner.

top.v :
Code Verilog : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
module top
   (
      input CLOCK_50,         // Horloge 50MHz
      output GPIO_WS2812B     // à relier à l'entrée IN de l'anneau
   );
   
   localparam NB_LEDS = 12;      // Anneau avec 12 LED adressables
   localparam ON      = 8'h20,   // luminosité maxi=255, mais /!\ la consommation si toutes les Led sont allumées 
              OFF     = 8'h00;
   
    wire [7:0]    red;
    wire [7:0]    green;
    wire [7:0]    blue;
    wire [7:0]    address;       // numéro de la Led entre 0 et NB_LEDS-1
    wire          load;          // chargement du registre si load=1
    wire          latch_n;       // déverrouillage et transfert du registre sur front descendant

    reg [3:0] state = 0;      // état courant
  
    wire [23:0] rgb;
    reg [25:0] delay = 0;
    
    assign red    = rgb[23-: 8]; // bits 23 à 16
    assign green  = rgb[15-: 8]; // bits 15 à 8
    assign blue   = rgb[7-: 8];  // bits 7 à 0
   
    // instanciation contrôleur WS2812B
    ws2812b_controller #(.NB_LEDS(NB_LEDS)) ws2812b_controller_inst 
      (
        .clk(CLOCK_50),
        //.reset(1'b0),
        .data_ws2812b(GPIO_WS2812B),
        .address(address),
        .red(red),
        .green(green),
        .blue(blue),
        .load(load),
        .latch_n(latch_n)
      );

    integer i;  
    
    reg [23:0] led[0:NB_LEDS-1]; // état des LED
  
    initial begin // synthétisable avec Quartus Pro
    //            Red, Green, Blue
      led[0]   <= { ON , OFF, OFF}; // Led 0, Red
      led[1]   <= { OFF, ON , OFF}; // Led 1, Green
      led[2]   <= { OFF, OFF, ON }; // Led 2, Blue
      led[3]   <= { ON , ON , OFF}; // Led 3, Yellow
      led[4]   <= { ON , OFF, ON }; // Led 4, Purple
      led[5]   <= { OFF, ON , ON }; // Led 5, Cyan
      led[6]   <= { ON , ON , ON }; // Led 6, White
      led[7]   <= { OFF, ON , ON }; // Led 7, Cyan
      led[8]   <= { ON , OFF, ON }; // Led 8, Purple
      led[9]   <= { ON , ON , OFF}; // Led 9, Yellow
      led[10]  <= { OFF, OFF, ON }; // Led 10, Blue
      led[11]  <= { OFF, ON , OFF}; // Led 11, Green   
    end
     
   // assignation des sorties
   assign load = state < NB_LEDS;
   assign address = load ? state : 0;
   assign rgb = load ? led[state] : 0;
   assign latch_n = ~(state == NB_LEDS);

    
    // machine à états finis
    always @(posedge CLOCK_50) begin

      if (state < NB_LEDS) begin // chargement des couleurs pour chaque LED dans le registre
         state <= state + 1;
      end else begin
         case (state)
            NB_LEDS: begin // déverrouillage et transferts des données
               state <= state + 1;              
            end
            
            NB_LEDS+1: begin // rotation des Led
               if (delay[22]==1) begin // si fin temporisation
                  led[0] <= led[NB_LEDS - 1];
                  for (i=1; i<=NB_LEDS-1; i=i+1) begin
                     led[i] <= led[i-1];
                  end
                  
                  state <= 0; // retour à l'état initial
                  delay <= 0;
               end else begin
                  delay <= delay + 1;
               end
            end
         
            default: begin
               //
            end
         
         endcase
               
      end 
    
    end
    
    
endmodule

La vidéo filmée avec mon smartphone ne rend pas bien les couleurs, mais je suis quand même très fier du résultat


Rotation roue multicolore

Et une autre animation pour le fun...

top.v :
Code Verilog : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
module top
   (
      input CLOCK_50,         // Horloge 50MHz
      output GPIO_WS2812B     // à relier à l'entrée IN de l'anneau
   );
   
   localparam NB_LEDS = 12;      // Anneau avec 12 LED adressables
   localparam ON      = 8'h20,   // luminosité maxi=255, mais /!\ la consommation si toutes les Led sont allumées 
              OFF     = 8'h00;
   
    wire [7:0]    red;
    wire [7:0]    green;
    wire [7:0]    blue;
    wire [7:0]    address;       // numéro de la Led entre 0 et NB_LEDS-1
    wire          load;          // chargement du registre si load=1
    wire          latch_n;       // déverrouillage et transfert du registre sur front descendant

    reg [3:0] state = 0;      // état courant
  
    wire[23:0] rgb;
    reg [25:0] delay = 0;
    
    assign red    = rgb[23-: 8]; // bits 23 à 16
    assign green  = rgb[15-: 8]; // bits 15 à 8
    assign blue   = rgb[7-: 8];  // bits 7 à 0
   
    // instanciation contrôleur WS2812B
    ws2812b_controller #(.NB_LEDS(NB_LEDS)) ws2812b_controller_inst 
      (
        .clk(CLOCK_50),
        //.reset(1'b0),
        .data_ws2812b(GPIO_WS2812B),
        .address(address),
        .red(red),
        .green(green),
        .blue(blue),
        .load(load),
        .latch_n(latch_n)
      );

    integer i, c;  
    
    reg [23:0] led[0:NB_LEDS-1]; // état des LED
    reg [3:0] bottom_stack, pointer;
    reg [23:0] color[0:5];
  
    initial begin // synthétisable avec Quartus Pro
    //    
      for (i=0; i<NB_LEDS; i=i+1)  led[i] = {OFF, OFF, OFF};
         
      color[0] <= {ON, OFF, OFF};   // red
      color[1] <= {OFF, ON, OFF};   // green
      color[2] <= {OFF, OFF, ON};   // blue
      color[3] <= {ON, ON, OFF};    // yellow
      color[4] <= {OFF, ON, ON};    // cyan
      color[5] <= {ON, OFF, ON};    // purple
      
      bottom_stack <= NB_LEDS - 1;
      pointer <= 0;

    end
     
   // assignation des sorties
   assign load = state < NB_LEDS;
   assign address = load ? state : 0;
   assign rgb = load ? led[state] : 0;
   assign latch_n = ~(state == NB_LEDS);

    
    // machine à états finis
    always @(posedge CLOCK_50) begin

      if (state < NB_LEDS) begin // chargement des couleurs pour chaque LED dans le registre
         state <= state + 1;
      end else begin
         case (state)
            NB_LEDS: begin // déverrouillage et transferts des données
               state <= state + 1;              
            end
            
            NB_LEDS+1: begin // définition des couleurs des Led
               if (delay[21]==1) begin // si fin temporisation
               
                  for (i=0; i<=NB_LEDS-1 ; i=i+1) begin
                     if (i <= bottom_stack) begin                 
                        led[i] <= (i == pointer) ? color[c] : {OFF, OFF, OFF};                     
                     end else begin
                        led[i] <= color[c];                    
                     end
                  end   
                  
                  pointer <= pointer + 1;
                  
                  if (pointer == bottom_stack) begin
                     pointer <= 0;
                     bottom_stack <= bottom_stack - 1;
                     if (bottom_stack == 1)  begin
                        bottom_stack <= NB_LEDS - 1;
                        c <= c + 1;
                        if (c==5) c<=0;
                     end
                  end

                  state <= 0; // retour à l'état initial
                  delay <= 0;
               end else begin
                  delay <= delay + 1;
               end
            end
         
            default: begin
               //
            end
         
         endcase
               
      end 
    
    end
    
    
endmodule


Cycle rouge, vert, bleu, jaune, cyan, magenta

Envoyer le billet « [FPGA] Créer un circuit logique pour piloter un anneau de LED adressables WS2812B » dans le blog Viadeo Envoyer le billet « [FPGA] Créer un circuit logique pour piloter un anneau de LED adressables WS2812B » dans le blog Twitter Envoyer le billet « [FPGA] Créer un circuit logique pour piloter un anneau de LED adressables WS2812B » dans le blog Google Envoyer le billet « [FPGA] Créer un circuit logique pour piloter un anneau de LED adressables WS2812B » dans le blog Facebook Envoyer le billet « [FPGA] Créer un circuit logique pour piloter un anneau de LED adressables WS2812B » dans le blog Digg Envoyer le billet « [FPGA] Créer un circuit logique pour piloter un anneau de LED adressables WS2812B » dans le blog Delicious Envoyer le billet « [FPGA] Créer un circuit logique pour piloter un anneau de LED adressables WS2812B » dans le blog MySpace Envoyer le billet « [FPGA] Créer un circuit logique pour piloter un anneau de LED adressables WS2812B » dans le blog Yahoo

Mis à jour 01/12/2023 à 22h14 par f-leb

Tags: fpga, verilog
Catégories
FPGA

Commentaires

  1. Avatar de f-leb
    • |
    • permalink
    J'ai rajouté une animation, pour le fun...