Mots-clés : Pipeline, Multiplieur

Un avantage majeur des langages de description matérielle est le parallélisme d'exécution des instructions. Plusieurs instructions peuvent traiter en parallèle plusieurs données de signal dans le même cycle d'horloge.

Cependant, lorsque les données sont entrées en série, le parallélisme d'exécution des instructions ne montre pas son avantage. De plus, dans de nombreux cas, certains calculs ne peuvent pas être achevés en un ou deux cycles d'horloge. Si chaque donnée d'entrée série doit attendre que le calcul précédent soit terminé avant de commencer le calcul suivant, l'efficacité est assez faible. Le pipeline est la solution à la faible efficacité du calcul de données en série sur plusieurs cycles.

Pipeline

L'idée de base du pipeline est de décomposer un processus répété en plusieurs sous-processus, chacun implémenté par une unité fonctionnelle dédiée. Plusieurs processus de traitement sont décalés dans le temps et traversent séquentiellement les étages fonctionnels, de sorte que chaque sous-processus puisse être exécuté en parallèle avec les autres sous-processus.

Supposons que le processus de lavage du linge dans une laverie automatique soit divisé en 4 étapes : collecte du linge, lavage, séchage et rangement dans les armoires. Chaque étape prend une demi-heure, donc laver le linge une fois prend 2 heures.

Considérez le pire cas : la laverie ne dispose que d’une seule machine à laver, d’un seul sèche-linge et d’un seul casier. Si un lot de linge est livré toutes les demi-heures, et que chaque fois vous devez attendre 2 heures que le lot précédent termine son lavage, alors laver 4 lots de linge prend 8 heures.

Le schéma est le suivant :

Modernisez l’équipement de cette laverie en introduisant 4 ensembles d’équipements de blanchisserie au total, et augmentez le personnel à 4 personnes, chacune responsable d’une étape du processus de lavage. Par conséquent, chaque lot de linge peut être immédiatement placé dans différentes machines à laver par la même personne. Comme les horaires sont décalés, chaque lot de linge peut être lavé, séché et rangé dans les casiers par les mêmes personnes utilisant différents équipements et créneaux horaires (une demi-heure). Le schéma est le suivant.

On peut voir que laver 4 lots de linge ne prend que 3.5 heures, ce qui améliore considérablement l’efficacité.

En fait, après 2 heures, le premier ensemble d’équipements de blanchisserie a terminé le processus de lavage et est inactif. Si un 5e lot de linge est envoyé à ce moment-là, le premier ensemble d’équipements peut se remettre à fonctionner. Par analogie, tant que des lots de linge sont introduits en continu, les 4 ensembles d’équipements de blanchisserie peuvent achever en continu le processus de nettoyage de tout le linge. À l’exception du premier lot qui prend 2 heures, un lot de linge sera lavé et terminé toutes les demi-heures par la suite.

Plus il y a de lots de linge, plus le temps gagné est évident. S’il y a N lots de linge, le temps nécessaire est de (4+N) demi-heures.

Bien sûr, le processus de blanchisserie modernisé présente aussi des inconvénients. L’augmentation des équipements et du personnel accroît le coût d’investissement, réduit l’espace restant dans la laverie et rend l’état de fonctionnement plus chargé.

Semblable au processus de lavage, le chemin de traitement des données peut aussi être considéré comme une chaîne de production. Chaque unité de traitement numérique sur le chemin peut être considérée comme une étape, ce qui introduit un délai.

La conception de pipeline divise systématiquement le chemin en unités de traitement numériques (étages), et insère des registres entre les unités de traitement pour stocker temporairement les données intermédiaires. Les unités divisées peuvent s'exécuter en parallèle par étage sans s'affecter mutuellement. Par conséquent, la conception de pipeline peut finalement améliorer le débit de données, c'est-à-dire augmenter la vitesse de traitement des données.

L'inconvénient de la conception de pipeline est que chaque étage de traitement nécessite des registres supplémentaires pour sauvegarder les états de calcul intermédiaires, et que plusieurs instructions exécutées en parallèle augmentent inévitablement la consommation d'énergie.

Ensuite, un multiplicateur est conçu, et une comparaison est faite entre l'adoption et la non-adoption de la conception de pipeline.


Conception générale d'un multiplicateur

Introduction

Peut-être quelqu'un demandera-t-il : utiliser directement le signe de multiplication*pour effectuer la multiplication de deux nombres, n'est-ce pas plus rapide et plus simple ?

Si vous avez ce doute, cela signifie que votre compréhension des langages de description matérielle est encore insuffisante. Comme mentionné précédemment, Verilog décrit des circuits matériels. Lorsque l'on utilise directement le signe de multiplication pour effectuer le processus de multiplication, le compilateur mappe cette expression de multiplication vers un multiplicateur par défaut lors de la compilation, mais sa structure est inconnue.

Par exemple, dans la conception FPGA, un cœur IP peut être appelé directement pour générer un multiplicateur hautes performances. Lorsque la largeur de bits est faible, le résultat peut être produit en un cycle ; lorsque la largeur de bits est grande, il peut également être produit de manière pipeline. Sous réserve que les exigences puissent être satisfaites, vous pouvez l'utiliser avec prudence*ou appeler directement l'IP pour effectuer l'opération de multiplication.

Mais l'IP de multiplicateur présente aussi de nombreux défauts, tels que des limitations de largeur de bits, un timing inconnu, etc. En particulier, l'utilisation du signe de multiplication peut enfouir un grand danger caché pour l'incertitude de la conception numérique.

Dans de nombreux cas, la multiplication par une constante est implémentée sous la forme d'un décalage et d'une addition, par exemple :

Exemple

A = A<<1 ;       // réaliser A * 2
A = (A<<1) + A ;   // correspond à A * 3
A = (A<<3) + (A<<2) + (A<<1) + A ; // correspond à A * 15

Un registre à décalage et un additionneur peuvent réaliser l’opération de multiplication par 3. Mais multiplier par 15 nécessite 3 registres à décalage et 3 additionneurs (bien sûr, multiplier par 15 peut aussi être réalisé par décalage et soustraction).

Parfois, un circuit numérique ne peut pas effectuer l’addition de plusieurs variables simultanément dans un seul cycle. Par conséquent, en conception numérique, l’opération d’addition la plus sûre consiste à n’additionner que 2 éléments de données en même temps ; la pire conception consiste à additionner 4 éléments de données ou plus en même temps.

Si la conception contient une opération qui additionne 4 éléments de données en même temps, alors cette partie de la conception est dangereuse et peut provoquer des violations de timing.

À ce stade, un multiplicateur pipeline avec des paramètres de conception configurables et un timing contrôlable devient nécessaire.

Principe de conception

Semblable à la multiplication décimale, le processus de multiplication de 13 par 5 est illustré ci-dessous :

On peut en déduire que le multiplicande est décalé et accumulé en fonction du bit correspondant du multiplicateur, ce qui achève ainsi le processus de multiplication.

En supposant qu’une seule accumulation puisse être effectuée par cycle, le nombre minimal de cycles d’horloge pour une multiplication est exactement la largeur de bits du multiplicateur. Par conséquent, il est recommandé d’utiliser le nombre ayant la plus petite largeur de bits comme multiplicateur, afin que la période de calcul soit plus courte.

Conception du multiplicateur

Étant donné que chaque opération de multiplication ne peut produire qu’un seul résultat (conception non pipeline), le code de conception est le suivant.

Exemple

module    mult_low
    #(paramètre N=4,
      paramètre M=4)
     (
      input                     clk,
      input                     rstn,
      input                     data_rdy ,  // validation de l'entrée de données
      input [N-1:0]             mult1,      // multiplicande
      input [M-1:0]             mult2,      // multiplicateur

      output                    res_rdy ,   // validation de la sortie de données
      output [N+M-1:0]          res         // résultat de la multiplication
      );

    //calculer le compteur
    reg [31:0]           cnt ;
    // compteur de cycles de multiplication
    wire [31:0]          cnt_temp = (cnt == M)? 'b0 : cnt + 1'b1 ;
    always @(posedge clk or negedge rstn) begin
        if (!rstn) begin
            cnt    <= 'b0 ;
        end
        else if (data_rdy) begin    // commencer le comptage lorsque les données sont activées
            cnt    <= cnt_temp ;
        end
        else if (cnt != 0 ) begin  // empêcher que la durée d'activation de l'entrée soit trop courte
            cnt    <= cnt_temp ;
        end
        else begin
            cnt    <= 'b0 ;
        end
    end

    //multiplier
    reg [M-1:0]          mult2_shift ;
    reg [M+N-1:0]        mult1_shift ;
    reg [M+N-1:0]        mult1_acc ;
    always @(posedge clk or negedge rstn) begin
        if (!rstn) begin
            mult2_shift    <= 'b0 ;
            mult1_shift    <= 'b0 ;
            mult1_acc      <= 'b0 ;
        end
        else if (data_rdy && cnt=='b0) begin  // initialisation
            mult1_shift    <= {{(N){1'b0}}, mult1} << 1 ;  
            mult2_shift    <= mult2 >> 1 ;  
            mult1_acc      <= mult2[0] ? {{(N){1'b0}}, mult1} : 'b0 ;
        end
        else if (cnt != M) begin
            mult1_shift    <= mult1_shift << 1 ;  // multiplicande multiplié par 2
            mult2_shift    <= mult2_shift >> 1 ;  // décaler le multiplicateur vers la droite pour faciliter le contrôle
            // vérifier si le bit correspondant du multiplicateur est 1 ; si c'est 1, accumuler
            mult1_acc      <= mult2_shift[0] ? mult1_acc + mult1_shift : mult1_acc ;
        end
        else begin
            mult2_shift    <= 'b0 ;
            mult1_shift    <= 'b0 ;
            mult1_acc      <= 'b0 ;
        end
    end

    //résultats
    reg [M+N-1:0]        res_r ;
    reg                  res_rdy_r ;
    always @(posedge clk or negedge rstn) begin
        if (!rstn) begin
            res_r          <= 'b0 ;
            res_rdy_r      <= 'b0 ;
        end  
        else if (cnt == M) begin
            res_r          <= mult1_acc ;  // sortir le résultat à la fin des cycles de multiplication
            res_rdy_r      <= 1'b1 ;
        end
        else begin
            res_r          <= 'b0 ;
            res_rdy_r      <= 'b0 ;
        end
    end

    assign res_rdy       = res_rdy_r;
    assign res           = res_r;

endmodule

testbench

Exemple

`timescale 1ns/1ns

module test ;
    paramètre    N = 8 ;
    paramètre    M = 4 ;
    reg          clk, rstn;
 
   //horloge
    always begin
        clk = 0 ; #5 ;
        clk = 1 ; #5 ;
    end

   //réinitialisation
    initial begin
        rstn      = 1'b0 ;
        #8 ;      rstn      = 1'b1 ;
    end

    //pas de pipeline
    reg                  data_rdy_low ;
    reg [N-1:0]          mult1_low ;
    reg [M-1:0]          mult2_low ;
    wire [M+N-1:0]       res_low ;
    wire                 res_rdy_low ;

    // utiliser une tâche pour un stimulus périodique
    task mult_data_in ;  
        input [M+N-1:0]   mult1_task, mult2_task ;
        begin
            wait(!test.u_mult_low.res_rdy) ;  //pas l'état de sortie
            @(negedge clk ) ;
            data_rdy_low = 1'b1 ;
            mult1_low = mult1_task ;
            mult2_low = mult2_task ;
            @(negedge clk ) ;
            data_rdy_low = 1'b0 ;
            wait(test.u_mult_low.res_rdy) ; //tester l'état de sortie
        end
    endtask

    //pilote
    initial begin
        #55 ;
        mult_data_in(25, 5 ) ;
        mult_data_in(16, 10 ) ;
        mult_data_in(10, 4 ) ;
        mult_data_in(15, 7) ;
        mult_data_in(215, 9) ;
    end

    mult_low  #(.N(N), .M(M))
    u_mult_low
    (
      .clk              (clk),
      .rstn             (rstn),
      .data_rdy         (data_rdy_low),
      .mult1            (mult1_low),
      .mult2            (mult2_low),
      .res_rdy          (res_rdy_low),
      .res              (res_low));

   //fin de simulation
   initial begin
      forever begin
         #100;
         if ($time >= 10000)  $finish ;
      end
   end

endmodule // test

Les résultats de la simulation sont les suivants.

Comme on peut le voir sur la figure, les deux éléments de données d'entrée obtiennent le résultat de multiplication correct après un délai de 4 cycles. En incluant le temps de latence de l'entrée des données intermédiaires, le calcul de 4 multiplications prend environ 20 cycles d'horloge.

Conception d'un multiplicateur pipeline

Ensuite, les états intermédiaires du processus d'exécution de la multiplication sont sauvegardés pour permettre un fonctionnement en pipeline. Le code de conception est le suivant.

Le fichier de code pour un processus de calcul d'accumulation unique est le suivant (mult_cell.v):

Exemple

module    mult_cell
    #(paramètre N=4,
      paramètre M=4)
    (
      input                     clk,
      input                     rstn,
      input                     en,
      input [M+N-1:0]           mult1,      // multiplicande
      input [M-1:0]             mult2,      // multiplicateur
      input [M+N-1:0]           mult1_acci, // résultat d'accumulation précédent

      output reg [M+N-1:0]      mult1_o,     // valeur sauvegardée après décalage du multiplicande
      output reg [M-1:0]        mult2_shift, // valeur sauvegardée après décalage du multiplicateur
      output reg [N+M-1:0]      mult1_acco,  // résultat d'accumulation actuel
      output reg                rdy );

    always @(posedge clk or negedge rstn) begin
        if (!rstn) begin
            rdy            <= 'b0 ;
            mult1_o        <= 'b0 ;
            mult1_acco     <= 'b0 ;
            mult2_shift    <= 'b0 ;
        end
        else if (en) begin
            rdy            <= 1'b1 ;
            mult2_shift    <= mult2 >> 1 ;
            mult1_o        <= mult1 << 1 ;
            if (mult2[0]) begin
                // accumuler si le bit correspondant du multiplicateur vaut 1
                mult1_acco  <= mult1_acci + mult1 ;  
            end
            else begin
                mult1_acco  <= mult1_acci ; // conserver si le bit correspondant du multiplicateur est 1
            end
        end
        else begin
            rdy            <= 'b0 ;
            mult1_o        <= 'b0 ;
            mult1_acco     <= 'b0 ;
            mult2_shift    <= 'b0 ;
        end
    end

endmodule

Instanciation de niveau supérieur

Plusieurs instanciations de module réalisent plusieurs accumulations. Le fichier de code est le suivant (mult_man.v) :

Exemple

module    mult_man
    #(paramètre N=4,
      paramètre M=4)
    (
      input                     clk,
      input                     rstn,
      input                     data_rdy ,
      input [N-1:0]             mult1,
      input [M-1:0]             mult2,

      output                    res_rdy ,
      output [N+M-1:0]          res );

    wire [N+M-1:0]       mult1_t [M-1:0] ;
    wire [M-1:0]         mult2_t [M-1:0] ;
    wire [N+M-1:0]       mult1_acc_t [M-1:0] ;
    wire [M-1:0]         rdy_t ;

    // La première instanciation est équivalente à une initialisation ; l'instruction generate ne peut pas être utilisée
    mult_cell      #(.N(N), .M(M))
    u_mult_step0
    (
      .clk              (clk),
      .rstn             (rstn),
      .en               (data_rdy),
      .mult1            ({{(M){1'b0}}, mult1}),
      .mult2            (mult2),
      .mult1_acci       ({(N+M){1'b0}}),
      //sortie
      .mult1_acco       (mult1_acc_t[0]),
      .mult2_shift      (mult2_t[0]),
      .mult1_o          (mult1_t[0]),
      .rdy              (rdy_t[0]) );

    // Les instanciations multiples de modules utilisent l'instruction generate
    genvar               i ;
    generate
        for(i=1; i<=M-1; i=i+1) begin: mult_stepx
            mult_cell      #(.N(N), .M(M))
            u_mult_step
            (
              .clk              (clk),
              .rstn             (rstn),
              .en               (rdy_t[i-1]),
              .mult1            (mult1_t[i-1]),
              .mult2            (mult2_t[i-1]),
              // Le résultat d'accumulation précédent est utilisé comme entrée pour l'accumulation suivante
              .mult1_acci       (mult1_acc_t[i-1]),
              //sortie
              .mult1_acco       (mult1_acc_t[i]),                                      
              .mult1_o          (mult1_t[i]),  // transfert d'état de décalage du multiplicande
              .mult2_shift      (mult2_t[i]),  // transfert d'état de décalage du multiplicateur
              .rdy              (rdy_t[i]) );
        end
    endgenerate

    assign res_rdy       = rdy_t[M-1];
    assign res           = mult1_acc_t[M-1];

endmodule

testbench

Ajoutez la description de simulation suivante au banc de test de l'exemple de conception de multiplicateur non pipeliné afin d'obtenir les résultats de simulation de l'opération de multiplication pipelinée.

Les 2 canaux de données sont fournis en entrée en continu et en série, et un module d'auto-vérification est inclus pour déterminer automatiquement l'exactitude des résultats de multiplication.

Exemple

    reg          data_rdy ;
    reg [N-1:0]  mult1 ;
    reg [M-1:0]  mult2 ;
    wire                 res_rdy ;
    wire [N+M-1:0]       res ;

    //pilote
    initial begin
        #55 ;
        @(negedge clk ) ;
        data_rdy  = 1'b1 ;
        mult1  = 25;      mult2      = 5;
        #10 ;      mult1  = 16;      mult2      = 10;
        #10 ;      mult1  = 10;      mult2      = 4;
        #10 ;      mult1  = 15;      mult2      = 7;
        mult2      = 7;   repeat(32)    #10   mult1   = mult1 + 1 ;
        mult2      = 1;   repeat(32)    #10   mult1   = mult1 + 1 ;
        mult2      = 15;  repeat(32)    #10   mult1   = mult1 + 1 ;
        mult2      = 3;   repeat(32)    #10   mult1   = mult1 + 1 ;
        mult2      = 11;  repeat(32)    #10   mult1   = mult1 + 1 ;
        mult2      = 4;   repeat(32)    #10   mult1   = mult1 + 1 ;
        mult2      = 9;   repeat(32)    #10   mult1   = mult1 + 1 ;
    end

    // décaler les données d'entrée pour faciliter la vérification ultérieure
    reg  [N-1:0]   mult1_ref [M-1:0];
    reg  [M-1:0]   mult2_ref [M-1:0];
    always @(posedge clk) begin
        mult1_ref[0] <= mult1 ;
        mult2_ref[0] <= mult2 ;
    end

    genvar         i ;
    generate
        for(i=1; i<=M-1; i=i+1) begin
            always @(posedge clk) begin
            mult1_ref[i] <= mult1_ref[i-1];
            mult2_ref[i] <= mult2_ref[i-1];
            end
        end
    endgenerate
   
    // auto-vérification
    reg  error_flag ;
    always @(posedge clk) begin
        # 1 ;
        if (mult1_ref[M-1] * mult2_ref[M-1] != res && res_rdy) begin
            error_flag <= 1'b1 ;
        end
        else begin
            error_flag <= 1'b0 ;
        end
    end

    //instanciation de module
    mult_man  #(.N(N), .M(M))
     u_mult
     (
      .clk              (clk),
      .rstn             (rstn),
      .data_rdy         (data_rdy),
      .mult1            (mult1),
      .mult2            (mult2),
      .res_rdy          (res_rdy),
      .res              (res));

Résultats de simulation

Les résultats de simulation des premières dizaines de cycles d’horloge sont les suivants.

Comme on peut le voir sur la figure, le signal de vérification du résultat de simulation error_flag reste à 0, ce qui indique que la conception du multiplicateur est correcte.

Les données sont saisies en série en continu sous la commande de l’horloge. Après un retard de 4 cycles d’horloge, les résultats de sortie de multiplication sont également émis en continu à chaque cycle d’horloge sans retard supplémentaire, réalisant ainsi un fonctionnement en pipeline.

Par rapport aux multiplicateurs généraux qui n’utilisent pas de pipeline, l’efficacité de multiplication est grandement améliorée.

Cependant, les ressources de registres utilisées par le multiplicateur pipeline sont également environ 4 fois celles du précédent multiplicateur sans pipeline.

Par conséquent, le choix d’adopter ou non une conception en pipeline dans une conception numérique doit être mis en balance en termes de ressources et d’efficacité.

Téléchargement du code source

Téléchargement