Nous avons déjà parlé du Gatemate dans les colonnes du FLF. Mais jusqu’à présent, seul le kit de développement officiel de CologneChip était disponible. Le tarif du kit officiel étant assez élevé on attendait avec impatience d’avoir des cartes développées par des tiers pour pouvoir investir.
Ce qui est chose faite avec les kits suivant. La note reste éditable au grès des sorties de kit muni du FPGA.
Evaluation board officielle par CologneChip
À tout seigneur tout honneur, citons d’abord du kit officiel qui est proposé à un prix de 226€ sur digikey.
Le kit officiel du GateMateA1 proposé par CologneChip dans sa belle boite en carton
Olimex GateMate-A1
Annoncé en décembre 2023 par Olimex, le kit GateMateA1 devrait être proposé au tarif de 50€.
La carte GateMate A1 annoncée par Olimex
TrenzMicro TEG2000-01-P001
Le SoM de TrenzMicro TEG2000-01-P001 est dans les même ordre de grandeur au niveau tarif (69€) mais nécessitera une carte d’accueil pour s’en servir.
Le SoM (System on Module) de Trenz micro avec le GatemateA1
Un langage de description matériel écrit en Ruby visiblement maintenu par deux personnes. Je ne sais pas ce qu’il vaut.
Le processus de synthèse/placement/routage/bitstream prenant beaucoup de temps, on est amené à faire d’autres activité pendant le traitement. Ce «switch» de tâche nous amène à faire des erreurs fréquentes de version de bitstream au moment de la configuration du FPGA.
Il est fréquent de passer des heures voir des jours sur un bug qui n’en était finalement pas un puisque nous n’avions pas mis à jour la version du bitstream.
Pour éviter ce problème il faut pouvoir lire la version du bitstream généré de manière à s’assurer qu’on travail bien avec la bonne.
C’est exactement l’objet de la macro «usr_access» des FPGA de la série 7 de Xilinx.
Cette macro est appelée de la manière suivante en VHDL :
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.numeric_std.all;
Entity usr_accesse2 is
port
(
CFGCLK : out std_logic;
DATA : out std_logic_vector(31 downto 0);
DATAVALID : out std_logic
);
end entity;
Architecture usr_accesse2_1 of usr_accesse2 is
begin
CFGCLK <= '1';
DATA <= x"00000E0F";
DATAVALID <= '1';
end architecture usr_accesse2_1;
Et la valeurs de la sortie «DATA» est ré-inscriptible jusqu’à la génération du bitstream avec l’option -g USR_ACCESS.
Pour y mettre la date et l’heure on utilisera l’option timestamp dans le menu tools->edit device property.
Cette action à pour effet d’ajouter la commande suivante dans le xdc :
Avec Cocotb nous avons parfois des coroutines qui sont susceptible de rester «coincées» dans une boucle d’attente infinie. Si l’on y prête pas garde, on a vite fait de remplir son disque dur de traces totalement inutile.
# Une coroutine qui attend bien trop longtemps
async def too_long_coroutine(self):
await Timer(1, units="sec")
Pour éviter ce problème, l’idéal serait de pouvoir ajouter un «timeout» à l’appel de la coroutine susceptible de bloquer.
Ça tombe bien, cocotb a prévu un trigger pour ça : with_timeout()
from cocotb.triggers import with_timeout
await with_timeout(testcls.too_long_coroutine(), 100, "ns")
Sauf que python n’a pas trop l’air d’accord pour exécuter notre coroutine comme un trigger.
TypeError: All triggers must be instances of Trigger! Got: coroutine
C’est dommage, on perd beaucoup de l’intérêt de ce trigger !
La solution donnée par marlonjames est d’«empaquetter» la coroutine dans la fonction start_soon() comme ceci :
C’est une réalité aujourd’hui, il y a encore beaucoup d’entreprises qui tournent exclusivement avec le système d’exploitation de Microsoft.
L’environnement Microsoft n’est pas idéal pour faire fonctionner les outils de simulations libre, mais c’est tout de même possible.
Voyons comment faire pour installer Yosys, GHDL, smtbmc, gtkwave et autres outils classique dans le monde de la simulation libre sur Windows 10 Famille.
MSYS2
MSYS2 est un environnement/shell de développement open-source pour windows. Pour l’installer il suffit de se rendre sur la page officiel du projet et de télécharger l’installeur (83.5Mo).
Le répertoire par défaut «c:/msys64» convient très bien et installera un raccourci dans le menu de windows. N’oubliez pas de désactiver votre antivirus avant de lancer l’installation, sinon il aura peur de gpg et vous empêchera de l’installer correctement !
Console MSYS2 permettant d’installer les différents package avec pacman
Une fois installé, la console se lance. Nous allons commencer par mettre à jours MSYS avec pacman :
$ pacman -Syu
La commande aura pour effet de fermer la fenêtre de console, qu’il faudra réouvrir pour lancer les commandes suivantes pour installer les dépendances:
À ce point de l’installation on peut fermer la fenêtre de console pour aller ouvrir la console nommée «MSYS2 MINGW64» dans laquelle on exécutera les programmes fraîchement installés.
Le répertoire «home/user» de cette console se retrouve ensuite dans le répertoire C:\msys2\home de votre ordinateur.
Cocotb
Cocotb utilise l’installateur de python nommé «pip»:
En ouvrant la console nommée «MSYS2 MINGW64» on est sûr d’avoir désormais un environnement de simulation correct pour développer des IP sous Windows.
L’exécution de ses différents makefile habituels est presque transparente. Il faudra quand même adapter certaine ligne de commande. Par exemple avec GHDL il est obligatoire d’élaborer le design avec «-e» avant de le lancer avec «-r». Alors que sous Linux on peut passer directement de l’analyse à l’exécution, GHDL devine ce qu’il faut élaborer.
Faire une FFT dans un FPGA est quelque chose qui n’est pas trivial. L’avantage, suivant la taille du FPGA, est de pouvoir en faire tourner plusieurs en parallèle pour accélérer le traitement. L’inconvénient étant le temps de développement qui est décuplé par rapport à une solution embarquée sur les habituels DSP ou microcontrôleurs.
Pour accélérer le développement, l’utilisation de modules fournis par les constructeurs est très tentante. Bien sûr, si on utilise la FFT d’un constructeur X, elle ne sera pas utilisable sur le FPGA du constructeur Y… Mais c’est de bonne guerre.
Plus gênant est la difficulté de simuler le module sur son PC pour valider l’algorithme que l’on souhaite mettre en œuvre.
Pour compiler le modèle il faut d’abord le générer à partir d’un projet Vivado. On crée donc un projet Vivado avec un FPGA cible et on instancie le bloc «Fast Fourrier Transform» dans l’«IP designer». Pour pouvoir générer le modèle il faut que les entrées/sorties soient connectées à quelques chose, dans notre cas nous nous contenterons d’exporter les ports.
Le bloc FFT de Vivado
L’archive au format zip est générée dans le répertoire suivant :
et à laquelle nous ajouterons un fichier main() et un Makefile. Par contre ne rêvez pas, il n’y a pas les sources du modèle 😉 le modèle se trouve dans le fichier binaire de librairie libIp_xfft_v9_1_bitacc_cmodel.so
L’explication pour la compilation est donnée sur le site officiel. Avec g++ ça donne :
La compilation génère un binaire nommé run_fft qu’il faut lancer en intégrant les librairies du répertoire courant pour le lien dynamique :
$ LD_LIBRARY_PATH=$$LD_LIBRARY_PATH:. ./run_fft
Running the C model...
Simulation completed successfully
Outputs from simulation are correct
$
Le résultat est relativement frustrant: certes il n’y a pas d’erreur, mais enfin bon … on n’est pas super avancé. On aimerait bien avoir de belles courbes et pouvoir admirer le résultat spectral de cette FFT !
Pour cela il va falloir se plonger dans le code «main()» et injecter son propre signal.
Plongée dans le code
Pour avoir la documentation du modèle on pourra bien sûr se référer à la documentation officiel, mais on peut également se plonger dans le header xfft_v9_1_bitacc_cmodel.h qui est bien commenté.
Le calcul est lancé avec la fonction xilinx_ip_xfft_v9_1_bitacc_simulate déclarée ainsi :
/**
* Simulate this bit-accurate C-Model.
*
* @param state Internal state of this C-Model. State
* may span multiple simulations.
* @param inputs Inputs to this C-Model.
* @param outputs Outputs from this C-Model.
*
* @returns Exit code Zero for SUCCESS, Non-zero otherwise.
*/
Ip_xilinx_ip_xfft_v9_1_DLL
int xilinx_ip_xfft_v9_1_bitacc_simulate
(
struct xilinx_ip_xfft_v9_1_state* state,
struct xilinx_ip_xfft_v9_1_inputs inputs,
struct xilinx_ip_xfft_v9_1_outputs* outputs
);
L’état est créé avec la fonction xilinx_ip_xfft_v9_1_create_state() et la structure d’entrée (inputs) possède un tableau de double pour la partie imaginaire et un tableau de double pour la partie réelle. La taille de la FFT étant donnée en 2^n par l’attribut nfft.
struct xilinx_ip_xfft_v9_1_inputs
{
int nfft; //@- log2(point size)
double* xn_re; //@- Input data (real)
int xn_re_size;
double* xn_im; //@- Input data (imaginary)
int xn_im_size;
int* scaling_sch; //@- Scaling schedule
int scaling_sch_size;
int direction; //@- Transform direction
}; // end xilinx_ip_xfft_v9_1_inputs
La structure de sortie est encore plus simple :
struct xilinx_ip_xfft_v9_1_outputs
{
double* xk_re; //@- Output data (real)
int xk_re_size;
double* xk_im; //@- Output data (imaginary)
int xk_im_size;
int blk_exp; //@- Block exponent
int overflow; //@- Overflow occurred
}; // xilinx_ip_xfft_v9_1_outputs
Dans l’exemple donnée, la partie imaginaire est fixée à 0 sur les 1024 échantillons et la partie réel à 0.5.
// Create input data frame: constant data
double constant_input = 0.5;
int i;
for (i=0; i<samples; i++) {
xn_re[i] = constant_input;
xn_im[i] = 0.0;
}
Si le signal est constant, en toute logique seule la fréquence continue (0Hz) doit être différente de 0. C’est ce qui est vérifié après avoir effectué le calcul :
// Check xk_re data: only xk_re[0] should be non-zero
double expected_xk_re_0;
if (C_HAS_SCALING == 0) {
expected_xk_re_0 = constant_input * (1 << C_NFFT_MAX);
} else {
expected_xk_re_0 = constant_input;
}
if (xk_re[0] != expected_xk_re_0) {
cerr << "ERROR:" << channel_text << " xk_re[0] is incorrect: expected " << expected_xk_re_0 << ", actual " << xk_re[0] << endl;
ok = false;
}
for (i=1; i<samples; i++) {
if (xk_re[i] != 0.0) {
cerr << "ERROR:" << channel_text << " xk_re[" << i << "] is incorrect: expected " << 0.0 << ", actual " << xk_re[i] << endl;
ok = false;
}
}
// Check xk_im data: all values should be zero
for (i=1; i<samples; i++) {
if (xk_im[i] != 0.0) {
cerr << "ERROR:" << channel_text << " xk_im[" << i << "] is incorrect: expected " << 0.0 << ", actual " << xk_im[i] << endl;
ok = false;
}
}
Transformée de wavelet
Tout ceci n’est pas très parlant pour le moment, testons maintenant le modèle sur la «wavelet» générée à partir d’un script python. Le script permettant de générer le signal et de l’écrire dans un fichier *.txt se trouve dans le répertoire cmodel du dépot github.
Le signal que l’on va injecter dans le modèle FFT de xilinx
Le script génère un fichier ysig.txt avec toutes les valeurs flottantes écrites en ASCII. On va ensuite relire le fichier avec le programme C++ :
Le programme écrira le résultat sous dans le fichier xfft_out.txt une fois le résultat calculé:
// save outputs in xfft_out.txt
std::ofstream outfile; outfile.open("xfft_out.txt");
if(outputs.xk_re_size != outputs.xk_im_size){
printf("Error imaginary part size is not equal to real part");
}
for(int i=0; i < outputs.xk_re_size; i++){
outfile << outputs.xk_re[i] << ", " << outputs.xk_im[i] << endl;
}
Fichier que l’on relira pour l’afficher au moyen du script python plot_fft.py
Résultat plutôt concluant puisque identique au calcul de la fonction magnitude_spectrum de pylab.
Et nous avons la bonne surprise d’obtenir le même spectre du module qu’avec la fonction de pylab.
On peut maintenant jouer avec les paramètres du module Xilinx et affiner notre modèle de simulation avant de le synthétiser dans un FPGA (de chez Xilinx évidement 😉
On dit souvent que pour bien apprendre un sujet en informatique il faut écrire une doc. Pour des besoins pro j’ai du me re-mettre au traitement numérique du signal. Je commence en général par un bouquin et un projet. Pour le projet comme c’est du pro je c’est à ma discrétion, par contre pour le bouquin je me suis plongé dans le livre de Richard G.Lyons «Understanding digital signal processing» qui a le mérite d’être richement illustré de graphes et d’équations avec beaucoup d’explications visuelles et «avec les mains».
Du bon soleil pour apprendre
L’idée de cette note est donc de faire des exercices en rapport avec ce qui est dans ce livre mais pas que, le tout de manière pratique en python et de voir les implications que ça peut avoir avec les FPGA.
Un signal discret
Dans un premier temps nous aurons besoin de numpy et pylab en python3
import numpy as np
import pylab as plt
Le signal de base est une sinusoïde. Pour représenter un signal de 1 Hertz en python on va d’abord créer un tableau d’un certain nombre de valeur de 0 à 1 secondes :
# Freq
f0 = 1
# 40 points de 0 à 39
t = np.linspace(0, 1, 40)
Puis calculer le sinus
y = np.sin(2*math.pi*f0*t)
Signal qu’il est facile de «plotter» ensuite :
plt.plot(t, y)
plt.show()
Ce qui nous donne cette belle courbe de sinus :
D’après Richard il ne faut pas relier les points
Mais pour bien se représenter un signal numérique il ne faut pas relier les points. Il vaut mieux mettre des points avec des lignes verticales comme ceci :
fix, ax = plt.subplots()
ax.stem(t, y, 'b', markerfmt="b.")
plt.show()
Ce qui nous donne la figure suivante :
Les points non reliés entre eux donnent une meilleur vision d’un signal numérique (discret)
Cette dernière figure illustre bien la notion d’échantillonnage avec une fréquence d’échantillonnage fs de 40Hertz (temps en secondes et 40 points) soit :
# Freq
f0 = 1
# Temps total
T = 1
# Nombre de points:
N = 40
# Fréquence d’échantillonnage :
print(f"fs = {N/T} Hertz")
# fs = 40.0 Hertz
Ici, la fréquence d’échantillonnage (40Hertz) est largement supérieur à la fréquence du signal enregistré (1 Hertz). On peut s’amuser maintenant à monter la fréquence du signal à la fréquence de Nyquist :
f0 = 5f0 = 10f0 = 20
Ce que nous dit Nyquist, c’est qu’avec tous les signaux ci-dessus, il est possible de retrouver la sinusoïde du début. Mais si on augmente encore la fréquence on obtient un repliement du spectre.
f0 = 30f0 = 40
On peut ajouter le l’analyse de spectre en augmentant également le nombre de points mesuré :
# Freq
f0 = 1
# Temps total
T = 1
# Nombre de points:
N = 100
# 100 points de 0 à 99
t = np.linspace(0, T, N)
y = np.sin(2*np.pi*f0*t)
fix, ax = plt.subplots(1,2)
ax[0].stem(t, y, 'b', markerfmt="b.")
ax[1].magnitude_spectrum(y, Fs=N/T, ds="steps-mid")
plt.show()
(f0=1) À gauche le signal sur 100 points, à droite l’analyse des fréquences
Ondelettes
Pour faire une ondelette (wavelet) on multiplie un cosinus (périodique) avec une gaussienne (exp(-t²/2)) :
# Décalage en seconde:
retard = 5
y = np.cos(2*np.pi*f0*t)*np.exp(-np.power(t-retard,2)/2)
La fréquence du signal est toujours de 1Hz mais le signal n’est plus périodique à l’infini
La vidéo suivante explique tout ce que vous avez toujours voulu savoir sur les ondelettes.
Si on change la fréquence du signal, en passant à 2Hz par exemple. On se rend compte que l’échantillonnage tronque les maximum locaux :
La courbe n’est plus symétrique
Ce qui casse la symétrie de la courbe.
Hilbert avec scipy
La transformée de hilbert permet de calculer la partie imaginaire du signal réel. Le package python nommé scipy inclue la fonction qui la calcule.
[...]
from scipy import signal
[...]
# morlet wavelet
y = np.cos(2*np.pi*f0*t)*np.exp(-np.power(t-retard,2)/2)
himg = signal.hilbert(y).imag
hreal = signal.hilbert(y).real
[...]
On distingue bien la partie réel (le signal d’origine) et la partie imaginaire en orange
Comme nous avons la partie réelle et la partie imaginaire de notre signal, il est possible désormais de calculer son module pour en tirer l’enveloppe:
Si l’on diminue la fréquence d’échantillonnage (division par 10) on remarque que l’enveloppe ne passe plus par les maximums. La transformée de Hilbert semble les avoirs tout de même déduit :
Avec une fréquence de pulsation de 2 Hertz et une division de la fréquence échantillonnage par 10 on constate que l’enveloppe demeure alors que les maximums réel et imaginaire sont tronqués.
Peut-on calculer l’enveloppe sans racine carré et carré ?
habs = np.abs(himg) + np.abs(hreal)
Hmmm ça marche moins bien sans carré (courbe rouge)
Et juste avec des carrés ?
hsquare= np.power(himg, 2) + np.power(hreal, 2)
Hmm c’est pas magique non plus
Transformée de Fourrier
Mais que faites vous encore sur ce blog ! Vite allez visionner l’excellente vidéo de 3Blue1Brown qui parle de la transformée de Fourrier avec force de graphes et de dessins. Vous ne verrez plus la transformée de fourrier comme avant 😉
Sinon y a aussi cette formule trouvée sur twitter qui est vraiment très parlante :
Jusqu’à présent, pour calculer et afficher la transformée de fourrier de notre signal, nous nous sommes servi exclusivement de la fonction magnitude_spectrum() inclue dans pylab. C’est intéressant pour avoir un aperçu du spectre, mais ça ne permet pas de dire que l’on maîtrise ça.
Nombre complexes en python
Python permet visiblement d’utiliser nativement des nombres complexes avec ‘j’ à condition d’y mettre un nombre devant :
In [2]: j
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-2-3eedd8854d1e> in <module>
----> 1 j
NameError: name 'j' is not defined
In [3]: 1j
Out[3]: 1j
In [4]: 0.02j
Out[4]: 0.02j
In [5]: -3j
Out[5]: (-0-3j)
In [7]: np.exp(1j)
Out[7]: (0.5403023058681398+0.8414709848078965j)
Tentons donc de calculer la transformée de fourrier en mode «brute de force» pour voir:
#temps: 0 points de 0 à N-1
t = np.linspace(0, T, N)
# morlet wavelet
y = np.cos(2*np.pi*f0*t)*np.exp(-np.power(t-retard,2)/2)
# transformée de fourrier
freqs = np.array([])
for k in range(N):
listexp = [y[n]*np.exp(1j*2*np.pi*k*n/N) for n in range(N)]
xk = (1/N)*np.array(listexp).sum()
freqs = np.append(freqs, xk)
#fréquence: 0 points de 0 à N-1
k = np.linspace(0, T, N)
Visiblement nous avons un problème «complexe»
Là ce que nous venons de calculer est la version complexe de la transformée de fourrier dont pylab nous «plot» la partie réelle. Voyons voir le module :
Nous avons donc toujours deux pics, sachant que le second pic est au delà de la fréquence de Nyquist (Fs=10Hz) et semble «normal».
Par contre nous avons un facteur 2 entre le calcul de magnitude de python et celui que l’on vient de calculer.
Peut-être parce que la formule de l’image est celle de la transformée inverse ? La transformée discrète donnée dans le livre est plutôt celle là :
Page 60: On retrouve encore moins le facteur 2 sur cette formule.
Voyons voir avec cette nouvelle formule :
# transformée de fourrier
freqs = np.array([])
for k in range(N):
listexp = [y[n]*np.exp(-1j*2*np.pi*k*n/N) for n in range(N)]
xk = np.array(listexp).sum()
freqs = np.append(freqs, xk)
fourrier_module = np.sqrt(np.power(freqs.imag, 2) + np.power(freqs.real, 2))
Bon c’est pas un facteur 2 cette fois!
Si on veut «matcher» la courbe de magnitude il faut ajouter un facteur 2/N au calcul du module :
Avec le facteur (2/N) on obtient une superposition des courbes.
cos – sin
Pour faire entrer le calcul de la transformée dans un FPGA, l’exponentielle d’un complexe n’est pas super pratique. Décomposons donc en différence cos-sin avec la formule d’Euler, on devrait obtenir le même résultat:
# transformée de fourrier
freqs = np.array([])
for k in range(N):
listexp = []
for n in range(N):
angle = 2*np.pi*k*n/N
listexp.append(y[n]*(np.cos(angle) - 1j*np.sin(angle)))
xk = np.array(listexp).sum()
freqs = np.append(freqs, xk)
Et nous obtenons exactement le même graphe qu’avant.
C’est sans surprise qu’on obtient la même chose en sommant indépendamment partie réelle et partie imaginaire :
# transformée de fourrier
freqs_real = np.array([])
freqs_img = np.array([])
for k in range(N):
listreal = []
listimg = []
for n in range(N):
angle = 2*np.pi*k*n/N
listreal.append(y[n]*(np.cos(angle)))
listimg.append(y[n]*(-np.sin(angle)))
xkreal = np.array(listreal).sum()
xkimg = np.array(listimg).sum()
freqs_real = np.append(freqs_real, xkreal)
freqs_img = np.append(freqs_img, xkimg)
fourrier_module = (2/N)*np.sqrt(np.power(freqs_img, 2) + np.power(freqs_real, 2))
Nous permettant au passage de dégager le ‘j’ des nombres complexes qui ne passe pas très bien dans un FPGA.
Des entiers ou des virgules fixes
Pour le moment c’était facile: on avait les flottant de python. Seulement voilà, dans un FPGA, les flottants ne sont pas simple. Nous avons besoin de fixer la taille (en bits) des variables/registres utilisés. Il faut également fixer la position de la virgule si l’on souhaite simplifier le calcul.
Le second problème nous vient des fonctions sin() et cos() qui ne sont pas calculables simplement. L’astuce consiste à pré-calculer les valeurs et les stocker dans une table qui ira remplir une ROM du FPGA.
Pour gérer des entiers en virgule fixe et de taille hétérogène on installera le module fxpmath :
La première surprise de cette méthode est le temps de calcul: on passe d’un calcul de la transformée quasi instantanée à un calcul qui prend presque une minute.
La seconde surprise vient avec le «bruit haute fréquence» qui apparaît dans le résultat et le second pic qui disparaît.
Avec les calculs intermédiaire en S8.8 on parvient à la même courbe, modulo le bruit en haute fréquences.
Le problème de ce bruit vient de l’arrondi calculé sur Pi, si on ajuste la virgule de Pi comme ceci :
# Frequence du signal
Sf0 = 2
f0 = (Sf0 * Fs)/T
# Décalage en seconde:
retard = 5
#temps: 0 points de 0 à N-1
t = np.linspace(0, T, N)
# morlet wavelet
y = np.cos(2*np.pi*f0*t)*np.exp(-np.power(t-retard,2)/2)
YTYPE="S1.15"
ysint = Fxp(y, dtype=YTYPE)
DTYPE="S8.8"
D2TYPE="S16.16"
fixpi = Fxp(np.pi, dtype="U3.13")
# transformée de fourrier
freqs_real = np.array([])
freqs_img = np.array([])
for k in range(N):
listreal = []
listimg = []
for n in range(N):
angle = Fxp(2*fixpi*Fxp(k*n, dtype="U16.0")/N, dtype=D2TYPE)
listreal.append(Fxp(y[n], dtype=YTYPE)*Fxp( np.cos(angle), dtype=YTYPE))
listimg.append (Fxp(y[n], dtype=YTYPE)*Fxp(-np.sin(angle), dtype=YTYPE))
xkreal = Fxp(np.array(listreal).sum(), dtype=DTYPE)
xkimg = Fxp(np.array(listimg ).sum(), dtype=DTYPE)
print(f"Freq {k}/{Fs} ({k*T/N}) -> {np.sqrt(xkreal*xkreal + xkimg*xkimg)})")
freqs_real = np.append(freqs_real, xkreal)
freqs_img = np.append(freqs_img, xkimg)
fourrier_power = Fxp(Fxp(freqs_img*freqs_img, dtype=DTYPE) + Fxp(freqs_real*freqs_real, dtype=DTYPE), dtype=DTYPE)
fourrier_module = Fxp((2/N)*Fxp(np.sqrt(fourrier_power), dtype=DTYPE), dtype=DTYPE)
Avec 128 échantillons (2^7)
Par contre on a un décalage de fréquence avec la fonction magnitude_spectrum() de pylab :
Mais d’où vient ce décalage ?
Ce décalage provient de l’axe des x qui n’est pas le même pour le calcul de python et le calcul maison. En effet, notre calcul «à la main» s’étend sur tout l’espace «de nyquist» (0 à N-1) alors que la fonction magnitude_spectrum() n’affiche le spectre que sur la moitée.
Pour recentrer tout ça on peut simplement récupérer la table des fréquences fournie par magnitude_spectrum() et l’utiliser comme axe des x dans l’affichage de notre spectre :
La fréquence est maintenant correcte, reste un problème de magnitude. Problème d’arrondi ?
Et nous obtenons la bonne fréquence pour les deux modes de calculs. Reste maintenant un problème de magnitude maximum, est-ce un problème d’arrondi de la virgule fixe ? Possible.
Ressources
Le code de cet article se trouve sur le dépôt github suivant.