Interface pour micro PDM

Interface pour micro PDM

OBJECTIF :

  • Concevoir sur le FPGA une interface permettant de filtrer un signal de type PDM ( Pulse-density modulation ) issu d’un micro.
  • Ce signal filtré sera ensuite envoyé sur le PC via une liaison UART, afin de le visualiser.

micro_pdm.png

Datasheet MEMS Microphone MP34DT01-M

Signal PDM ( Pulse Density Modulation )

Un signal PDM résulte d’une conversion sigma-delta 1 bit d’un signal analogique.

sigma_delta.svg

L’application d’un filtre passe-bas ou moyenneur à un signal PDM permet de retrouver les points du signal analogique initial :

filtrage.svg

Architecture du Système

Nous allons utiliser le FPGA afin de réaliser une interface pour le micro PDM.
Cette interface doit filtrer le signal PDM issu du micro, et envoyer les échantillons filtrés sur une liaison RS232.
Les échantillons seront ensuite affichés sur le PC.
C’est également au FPGA de fournir une horloge ( qu’on fixera à 1MHz ) au micro PDM.

presentation_systeme.svg


Filtre Moyenneur

Afin de réaliser un filtre simple, nous allons considérer le filtre moyenneur.

filtre_moyenneur.svg

Exemple d’un filtre moyenneur d’ordre 4

J’applique sur l’entrée du filtre un signal PWM à valeur moyenne constante :

signal_val_moy_cte.svg

Exemple d’un filtre moyenneur d’ordre 8

signal_val_moy_cte_2.svg

Codage VHDL

Une proposition de codage pour le filtre moyenneur est la suivante :

  • Le signal d’entrée vaut 0 ou 1, on associe à ces valeurs respectivement 0 et 255 pour faire le calcul de la moyenne
  • On utilise une pile pour stocker les valeurs
  • La mise à jour ou l’initialisation des piles est réalisées dans des boucles ; la boucle est réalisée intégralement lors d’un process.
  • l’accumulation d’une valeur suppose la mise à jour de cette valeur plusieurs fois dans un même process ; d’où l’utilisation d’une variable acc_v.
  • La division par 2^N correspond à un décalage ( shift right ) ; en VHDL, il suffit de récupérer une partie du vecteur initial ( affectation de d_out ).
Filtre moyenneur
entity average_filter is
    generic( SIZE  : integer := 3 ); -- Moyenne sur 2^SIZE échantillons
    port( 
        clk    : in std_logic;
        reset  : in std_logic;
        d_in  : in std_logic;
        d_out : out std_logic_vector(7 downto 0)
    );
end average_filter;

architecture arch_average_filter of average_filter is
    type t_pile is array(integer range 0 to 2**SIZE-1) of unsigned(7 downto 0);
    signal pile     : t_pile;
    signal new_ech  : unsigned(7 downto 0);

begin
    -- Détection de la nouvelle entrée d_in
    new_ech <= "11111111" when d_in = '1' else "00000000";

    process(clk, reset)
        variable acc_v : unsigned(SIZE+7 downto 0);
    begin
        if reset = '1' then
            -- Réinitialisation de la pile à zéro
            for i in 0 to (2**SIZE)-1 loop
                pile(i) <= "00000000";
            end loop;

        elsif rising_edge(clk) then
            -- Décalage de tous les éléments de la pile
            for i in 0 to 2**SIZE-2 loop
                pile(i+1) <= pile(i);
            end loop;
            -- Ajout du nouvel échantillon à la position 0
            pile(0) <= new_ech;
            
            -- Calcul de la moyenne
            acc_v := (others => '0');   
            for i in 0 to 2**SIZE-1 loop
                acc_v := acc_v + pile(i);
            end loop;

            -- Division par N (2^SIZE) en décalant à droite
            d_out <= std_logic_vector(acc_v(7+SIZE downto SIZE));
        end if;
    end process;
end arch_average_filter;

Le filtre a un paramètre de généricité ; la taille définitive est indiquée lors de l’instanciation :

average_filter0 : entity average_filter generic map(8) port map (
clk => clk_micro,
reset => reset,
d_in => P1B1,
d_out => data_to_send
);

Ordre du filtre et fréquence du signal PDM

Nous avons pu constater sur les figures précédentes que plus l’ordre du filtre est élevé, meilleure est la précision.
Cependant un ordre trop élevé ( correspondant à plus d’un quart de la fréquence moyenne du signal d’entrée), aurait pour effet de faire disparaitre ce signal.

filter_order.svg

Si l’on considère une horloge du filtre de fréquence 1 MHz, et des fréquences moyennes des signaux d’entrée comprises entre 0 et 1000Hz :

Tsig=11000=1ms T_{sig} =\frac{1}{1000} = 1ms , TPDM=11.106=1usT_{PDM}= \frac{1}{1.10^6} = 1us 1.1031.106=1000 \rightarrow \frac{1.10^{-3}}{1.10^{-6}}=1000

On peut supposer un bon filtrage pour un ordre de filtre de 256.

Test du Filtre Moyenneur en Simulation

Pour un filtre d’ordre 256 avec une fréquence 1 MHz, on peut tester le filtre avec un signal PWM de fréquence 2561.106256*1.10^{-6}

On peut envisager l’utilisation d’une boucle dans le fichier icebreaker_tb.vhd ( ici signal pwm de rapport cyclique 0.5 ) :

simulation filtre
stimuli: process
      begin
	 ...
       
     for i in 0 to 500 loop
					for j in 0 to 127 loop  
					P1B1 <= '1';
					wait for 1 us;
					end loop;  
					for k in 0 to 127 loop  
					P1B1 <= '0';
					wait for 1 us;
					end loop;  
        end loop;      
        wait;
end process;

Système Complet

systeme_complet.svg

fsm_sys.svg

Considérations Temporelles

La sérialisation des données sur l’UART implique de respecter un temps minimum pour l’envoi de chaque point.
Par rapport au signal PDM filtré disponible dans le FPGA, on enverra un point tous les T_send.
En tenant compte du débit sur la ligne, l’objectif est de pouvoir observer un nombre suffisant de points sur le PC, le pire des cas étant quand la fréquence du signal filtré est maximale.

envoi_uart.svg

considerations_temporelles.svg

  • La fréquence maximale du signal à afficher est 1kHz, soit une période T=1ms.
  • Il faut envoyer 10 bits pour chaque valeur
  • Pour un baudrate de 230400 bits/s, on enverra au plus vite 23040 valeurs/s
  • Ce qui fait, pendant T=1ms, 230401000=23\frac{23040}{1000}=23
  • On a alors 23 points pour afficher une période du signal d’entrée.

Si l’on veut choisir la fréquence T_send, on peut utiliser le compteur associé à la machine à états.
Ici T_send = 50us ( F_send=12MHz600=20000T_send=120000=50us)F\_send=\frac{12MHz}{600}=20000 \rightarrow T\_send=\frac{1}{20000}=50us ) , ce qui correspond alors à 20 points pour afficher une période du signal d’entrée.

Affichage PC

Les valeurs envoyées par la FPGA au PC sont des valeurs ( et non des chaines de caractères ).
On ne peut pas utiliser directement un terminal liaison série ( qui nécessite la réception de caractères ).
Nous pouvons donc utiliser les programmes python suivant :

Affichage dans un Terminal
import serial

# Configuration du port série
port = '/dev/ttyUSB1'  # à adapter
baudrate = 230400      # à adapter 

try:
    with serial.Serial(port, baudrate, timeout=1) as ser:
        print(f"Lecture des données depuis {port} à {baudrate} bauds...")
        while True:
            data = ser.read(1)  # Lecture d’un octet
            if data:
                value = int.from_bytes(data, byteorder='big')  # Convertir en entier
                print(f"{value}")
except serial.SerialException as e:
    print(f"Erreur d’ouverture du port série : {e}")
except KeyboardInterrupt:
    print("Arrêt par l’utilisateur.")
Affichage Graphique
import serial
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from collections import deque

# Configuration port série
port = '/dev/ttyUSB1' # à adapter
baudrate = 230400     # à adapter

# Initialisation fenêtre graphique
plt.style.use('ggplot')
fig, ax = plt.subplots()
ax.set_title("Données reçues en temps réel")
ax.set_xlabel("Nombre d'échantillons")
ax.set_ylabel("Valeur reçue (0-255)")
ax.set_ylim(0, 255)

# Buffer circulaire pour afficher les dernières valeurs
max_len = 200
data_buffer = deque([0]*max_len, maxlen=max_len)

line, = ax.plot(range(max_len), data_buffer)

def update(frame):
    n = ser.in_waiting
    if n:
        data = ser.read(n)
        data_buffer.extend(data)  # ajoute tous les octets reçus
        line.set_ydata(data_buffer)
    return line,

try:
    ser = serial.Serial(port, baudrate, timeout=0.01)
    ani = animation.FuncAnimation(fig, update, interval=20)  # appel toutes les 20 ms
    plt.show()
except serial.SerialException as e:
    print(f"Erreur port série : {e}")
except KeyboardInterrupt:
    print("Arrêt demandé par utilisateur.")
finally:
    if 'ser' in locals() and ser.is_open:
        ser.close()

Test avec le générateur de signaux Digilent Discovery

Dans l’immédiat et pour le confort de tous, le signal du micro peut être forcé directement par l’analyseur et générateur de signaux Digilent.

Il faut débrancher le micro et réaliser la connexion suivante :

digilent.jpg

zoom_digilent.jpg

Ouvrir le fichier gene_pdm.dwf3work avec le logiciel WaveForms.
Cette configuration permet de générer un signal PDM à valeur moyenne sinusoidale 1kHZ sur DIO0 ( fréquence PDM 1 MHz )

waveform.png

L’affichage avec print_serial.py devrait donner si tout est fonctionnel :

print_sinus.png

Test avec le micro PDM

Tester le système en appliquant un son au micro ( son à fréquence constante, fréquence entre 0 et 1kHz )