Une façon de calculer les logarithmes en base 2

Le calcul des logarithmes est une opĂ©ration assez courante dans le traitement numĂ©rique du signal. Plus souvent, peut-ĂȘtre, seules les convolutions (multiplication avec accumulation) et les amplitudes avec phases doivent ĂȘtre prises en compte. En rĂšgle gĂ©nĂ©rale, pour calculer les logarithmes sur un FPGA, l'algorithme CORDIC est utilisĂ© dans une version hyperbolique, ne nĂ©cessitant que des tables et des opĂ©rations arithmĂ©tiques simples. Cependant, ce n'est pas toujours pratique, surtout si le projet est grand, le cristal est petit et les danses avec optimisation commencent. C'est avec une telle situation que j'ai dĂ» faire face un jour. Les deux ports du bloc RAM (Cyclone IV) fonctionnaient dĂ©jĂ  Ă©troitement, ne laissant aucune fenĂȘtre libre. Je ne voulais pas utiliser un autre bloc pour CORDIC hyperbolique. Mais il y avait un multiplicateur, pour lequel une fenĂȘtre libre dĂ©cente a Ă©tĂ© obtenue dans le diagramme du temps. AprĂšs avoir rĂ©flĂ©chi un jour, j'ai composĂ© l'algorithme suivant, dans lequel les tableaux ne sont pas utilisĂ©s, mais il y a multiplication, plus prĂ©cisĂ©ment quadrature. Et comme la quadrature des circuits est plus simple que le cas gĂ©nĂ©ral de la multiplication, cet algorithme est peut-ĂȘtre intĂ©ressant pour les puces spĂ©cialisĂ©es, bien qu'il n'y ait bien sĂ»r aucune diffĂ©rence pour le FPGA. Plus de dĂ©tails sous la coupe.

Expliquer ce qui est quoi, c'est plus facile pour les vrais nombres. Commençons par eux. Nous procédons à l'implémentation entiÚre plus tard.

Soit un nombre X. Trouvez le nombre Y tel que X=2Y .
Nous supposons Ă©galement que X est compris entre 1 et 2. Cela ne limite pas trop la gĂ©nĂ©ralitĂ©, car X peut toujours ĂȘtre transfĂ©rĂ© Ă  cet intervalle par multiplication ou division par une puissance de deux. Pour Y, cela signifie ajouter ou soustraire un entier, ce qui est facile. Donc X se situe dans l'intervalle de 1 Ă  2. Alors Y se situera dans l'intervalle de 0 Ă  1. Nous Ă©crivons Y comme une fraction binaire infinie:

Y=b020+b12−1+...+bn2−n+...


Cotes bi dans cet enregistrement, il n'y a rien de plus que des bits de la représentation binaire du nombre Y. De plus, comme Y est inférieur à 1, il est évident que b0 = 0.

Équilibrons notre premiĂšre Ă©quation: X2=22Y et comme prĂ©cĂ©demment, nous Ă©crivons la reprĂ©sentation binaire de 2Y . De toute Ă©vidence,

2Y=b120+b22−1+...+bn2−(n−1)+...

C'est-Ă -dire bits bi est restĂ© le mĂȘme, seuls les pouvoirs de deux se sont dĂ©placĂ©s. Bat b0 non prĂ©sent dans la vue car il est Ă©gal Ă  zĂ©ro.

Deux cas sont possibles:

1) X2>2 , 2Y> 1, b1=1

2) X2<2 , 2Y <1, b1=0

Dans le premier cas, nous prenons comme nouvelle valeur de X X2/2 dans le second - X2 .

En consĂ©quence, la tĂąche a Ă©tĂ© rĂ©duite Ă  la premiĂšre. Le nouveau X se situe Ă  nouveau dans la plage de 1 Ă  2, le nouveau Y de 0 Ă  1. Mais nous avons appris un bit du rĂ©sultat. En suivant les mĂȘmes Ă©tapes Ă  l'avenir, nous pouvons obtenir autant de bits de Y que possible.

Voyons comment cela fonctionne dans un programme C:

#include <stdio.h> #include <math.h> int main() { double w=1.4; double s=0.0; double a=0.5; double u=w; for(int i=0; i<16; i++) { u=u*u; if(u>2) { u=u/2; s+=a; } a*=0.5; } w=log2(w); double err=100*abs(2*(sw)/(s+w)); printf("res=%f, log=%f, err=%f%c\n",s,w,err,'%'); return 0; } 

Nous avons calculé le logarithme avec une précision de 16 bits et comparé à ce que donne la bibliothÚque mathématique. Le programme a apporté:

res = 0.485413, log = 0.485427, err = 0.002931%

Le résultat a coïncidé avec la bibliothÚque avec une précision de 0,003%, ce qui montre l'efficacité de notre algorithme.

Passons à une implémentation entiÚre.

Soit des nombres binaires non signés à N bits représentant l'intervalle [0, 1]. Pour plus de commodité, nous considérons le numéro d'unité 2N mais pas 2 $ ^ N-1 $ , et en conséquence un numéro de deux 2N+1 . Nous allons écrire un programme à l'image et à la ressemblance du précédent, mais en travaillant avec des entiers:

 #include <stdio.h> #include <math.h> #define DIG 18 //  #define N_BITS 16 //    unsigned ONE=1<<(DIG-1); // unsigned TWO=ONE<<1; // unsigned SCALE=1<<(N_BITS+1); //  unsigned myLog(unsigned w) { unsigned s=0; unsigned long long u=w; for(int i=0; i<N_BITS+1; i++) { s<<=1; u=(u*u)>>(DIG-1); //    ! if(u&TWO) //      { u>>=1; s+=1; } printf("%X\n", (int)u); } return s; } int main() { double w=1.2345678; unsigned iw=(unsigned)(ONE*w); double dlog=log2(w); unsigned ilog=myLog(iw); unsigned test=(unsigned)(SCALE*dlog); int err=abs((int)(ilog-test)); printf("val=0x%X, res=0x%X, log=0x%X, err=%d\n",iw,ilog,test,err); return 0; } 

AprÚs avoir joué dans un programme avec différentes profondeurs de bits (DIG), précision de calcul (N_BITS) et arguments de logarithme (w), nous voyons que tout est calculé correctement. En particulier, avec les paramÚtres spécifiés dans cette source, le programme produit:

val = 0x27819, res = 0x9BA5, log = 0x9BA6, err = 1

Maintenant, tout est prĂȘt Ă  implĂ©menter un morceau de fer sur Veril, faisant exactement la mĂȘme chose que la fonction myLog en C. Les variables s et u de notre fonction peuvent ĂȘtre imprimĂ©es en boucle et comparĂ©es Ă  ce que le simulateur Verilog produit. La correspondance de ces variables avec l'implĂ©mentation de fer est trĂšs transparente et comprĂ©hensible. u est un registre de travail qui prend de nouvelles valeurs de X pendant les itĂ©rations. s est un registre Ă  dĂ©calage dans lequel le rĂ©sultat est accumulĂ©. L'interface de notre module ressemblera Ă  ceci:

 module logarithm( input clk, // input wr, //   input[17:0] din, //   output[nbits-1:0] dout, //   output rdy //  ); parameter nbits=16; //  

Le bus d'entrĂ©e est adoptĂ© sur 18 bits, respectivement, la largeur des multiplicateurs dans le Cyclone IV. Les chiffres sur notre module devraient ĂȘtre normalisĂ©s. C'est-Ă -dire avec un bit Ă©levĂ© Ă©gal Ă  un. Dans mon projet, cela s'est fait automatiquement. Mais dans ce cas, pour mettre en Ɠuvre le normalisateur, je pense que ce n'est difficile pour personne. La prĂ©cision des calculs est dĂ©finie par le paramĂštre nbits, par dĂ©faut Ă©gal Ă  16. Le module compte un bit par cycle et pendant 16 cycles, il calcule le logarithme avec une prĂ©cision de 16 bits. Si vous avez besoin plus rapidement avec la mĂȘme prĂ©cision ou plus prĂ©cisĂ©ment avec la mĂȘme vitesse, j'espĂšre que personne n'aura beaucoup de difficultĂ© Ă  diviser le module en plusieurs appareils et pipelining.

Voici le module complet et le code de test
 //--------------------- logarithm.v ------------------------------// module logarithm( input clk, // input wr, //   input[17:0] din, //   output[nbits-1:0] dout, //   output rdy //  ); parameter nbits=16; //  reg[4:0] cnt; // reg[17:0] acc; // - reg[nbits-1:0] res; // always @(posedge clk) if(wr) cnt<=nbits+1; else if(cnt != 0) cnt<=cnt-1; wire[35:0] square=acc*acc; //  wire bit=square[35]; //  wire[17:0] next = bit ? square[35:18] : square[34:17]; //  always @(posedge clk) if(wr) acc<=din; else if(cnt != 0) begin acc<=next; #10 $display("%X", acc); end always @(posedge clk) if(wr) res<=0; else if(cnt != 0) begin res[nbits-1:1]<=res[nbits-2:0]; res[0]<=bit; end assign dout=res; assign rdy=(cnt==0); endmodule //======================== testbench.v =====================// module testbench(); reg clk; // always #100 clk=~clk; reg wr; // reg[17:0] din; // wire rdy; // wire[15:0] dout; // logarithm log2( .clk (clk), .wr (wr), .din (din), .dout (dout), .rdy (rdy) ); //  n     task skipClk(integer n); integer i; begin for(i=0; i<n; i=i+1) @(posedge clk); #10 ; end endtask initial begin // $dumpfile("testbench.vcd"); $dumpvars(0, testbench); clk=0; wr=0; din=18'h27819; skipClk(3); wr=1; skipClk(1); wr=0; @(rdy); skipClk(3); $display("value=%X, result=%X", din, dout); $display("Done !"); $finish; end endmodule 


Exécutez le test avec ce script:

 #!/bin/sh rm -f *.vvp rm -f *.vcd iverilog -o testbench.vvp logarithm.v testbench.v vvp testbench.vvp gtkwave testbench.vcd testbench.gtkw 

En exĂ©cutant le test, nous voyons la sortie finale du simulateur - valeur = 27819, rĂ©sultat = 9ba5. Verilog a donnĂ© la mĂȘme chose que C. Le chronogramme ici est assez trivial et n'a pas d'intĂ©rĂȘt particulier. Par consĂ©quent, je ne l'apporte pas.

Comparez la sortie intermédiaire du simulateur (acc) et le programme en C (s):
Verilog C
30c5d 30C5D
252b1 252B1
2b2bc 2B2BC
3a3dc 3A3DC
35002 35002
2be43 2BE43
3c339 3C339
38a0d 38A0D
321b0 321B0
273a3 273A3
30163 30163
24214 24214
28caf 28CAF
34005 34005
2a408 2A408
37c9d 37C9D
30a15 30A15


Assurez-vous qu'ils correspondent petit Ă  petit. Au total, l'implĂ©mentation sur un verilo rĂ©pĂšte un peu le modĂšle C. C'est le rĂ©sultat qui devrait ĂȘtre obtenu en implĂ©mentant des algorithmes dans le matĂ©riel.

C’est probablement tout. J'espĂšre que quelqu'un trouvera cette expĂ©rience utile.

Source: https://habr.com/ru/post/fr469327/


All Articles