Comment générer un signal analogique périodique ?

NIVEAU 3

Objectifs

  • Générer un signal analogique périodique à partir de données numériques
  • Préparer les échantillons numériques

Pré-requis

Pourquoi générer un signal analogique périodique ?

La génération de signaux périodiques est utilisé dans de multiples applications.

On peut, par exemple, citer les générateurs de fonctions basse fréquence (GBF). Ceux-ci sont des outils d’instrumentation permettant d’appliquer diverses formes de signaux en entrée de systèmes linéaires pour les caractériser. Ils étaient initialement réalisés de manière analogique. Mais avec la démocratisation des systèmes numériques (et l’augmentation des fréquences de calculs), il est désormais plus simple de générer les formes d’ondes classiques de manière numérique.

Les samplers audio utilisent également utiliser ce principe pour restituer des signaux sonores acquis à une certaine fréquence d’échantillonnage. C’est sur cet exemple que nous allons poursuivre ce tutoriel.

Principe de génération des signaux

Tout signal numérique peut être vu comme une suite finie de \(N\) échantillons. Ces échantillons sont stockés dans un tableau. Ils ont été échantillonnés à une fréquence \(F_E\) connue et sur un certain nombre d’informations binaires \(p\) (soient \(2^p\) niveaux différents).

Attention les systèmes embarqués numériques traitent des signaux uniquement à valeurs positives. Les signaux audio sont à valeur moyenne nulle. Il sera donc indispensable de supprimer la composante continue des échantillons numériques après leur conversion et avant d’amplifier en puissance ce signal.

Etapes de restitution

Pour restituer des données numériques (un son par exemple), il est important de respecter les étapes suivantes :

  • préparer les échantillons numériques : à partir d’un fichier sonore, d’un tableau de données… pour qu’ils puissent être par la suite stockés dans un système embarqué
  • générer le signal périodique à intervalle régulier :
    • récupérer le bon échantillon dans le tableau précédent (compteur)
    • envoyer l’échantillon à un convertisseur numérique-analogique (interne ou externe)
    • répéter les deux étapes précédentes à intervalle régulier (respectant la fréquence d’échantillonnage des données)

Préparer les échantillons numériques

Pour préparer les échantillons numériques, afin qu’ils puissent être stockés dans le microcontroleur de la carte Nucléo, nous allons utiliser Matlab. Il existe cependant d’autres méthodes possibles… (programme en C, en python…)

Lecture d’un son sous Matlab

Nous allons voir dans un premier temps comment récupérer et écouter un son au format WAV ou MP3 à l’aide de Matlab.

Il faut utiliser la fonction audioread de Matlab.

nom_fichier_entree = 'Laser.mp3';
son_initial = audioread(nom_fichier_entree);    % pour récupérer le signal sonore dans une matrice

Pour réécouter un fichier sonore sous Matlab, il faut utiliser la commande sound. Elle nécessite 2 paramètres : la matrice contenant les données (sur 1 ou 2 colonnes, selon que le fichier sonore initial est en stéréo ou en mono) et la fréquence d’échantillonnage.
Les signaux sonores sont majoritairement échantillonnés sur 16 bits à la fréquence de 44100 Hz (signaux sonores allant de 20Hz à 20kHz).

Fs_initial = 44100;
sound(son_initial_filtre, Fs_initial);

Affichage de la FFT du signal initial (optionnel)

Matlab est avant tout un puissant outil de calculs, en particulier sur les signaux numériques. Il est ainsi possible de réaliser des transformées de Fourier de différents signaux. La FFT est une discrétisation de la transformée de Fourier.

Le calcul de la FFT dépend de la fréquence d’échantillonnage et du nombre d’échantillons présents dans le signal initial. Le résultat de la FFT sous Matlab est compris entre les fréquences 0 et \(F_E\) (fréquence d’échantillonnage) et possède le même nombre de points que le nombre d’échantillons initial.

W_initial = linspace(-Fs_initial/2,Fs_initial/2-((Fs_initial/2)/length(son_initial)), length(son_initial));
fft_son_init = fft(son_initial(:,1))/Fs_initial;       % Passage en mono et FFT

fft_son_init = fftshift(fft_son_init);      % Affichage de la FFT
figure(1);
plot(W_initial,abs(fft_son_init));
xlabel('Frequence en Hz');
ylabel('FFT du signal sonore');

On obtient alors le spectre suivant pour le son laser :

Ré-échantillonnage du signal

La carte Nucléo L476RG possède un convertisseur numérique-analogique interne (Générer une tension analogique) de 12 bits. Les échantillons sonores ayant souvent une précision de 16 bits, nous allons perdre un peu qualité sonore.
Mais le point le plus critique vient de la fréquence d’échantillonnage. En effet, 44100 Hz est une valeur trop élevée pour l’utilisation du DAC de la carte Nucléo. Il va donc falloir sous-échantillonner le signal.

Comme nous allons sous-échantillonner, et donc utiliser une fréquence d’échantillonnage plus basse, une partie du spectre du signal initial peut se replier. Il est donc indispensable de filtrer le signal initial à une fréquence inférieure à la moitié de la fréquence d’échantillonnage visée (Shannon).

On peut alors utiliser le script suivant :

[b,a] = butter(20,Fs_n/(2*Fs_initial),'low');
son_initial_filtre = filter(b,a,son_initial);
%sound(son_initial_filtre, Fs_initial);

Pour sous-échantilloner, il existe la commande resample.

Fs_n = 8000;
son_echantillonne = resample(son_initial_filtre, Fs_n, Fs_initial);

Pour pouvoir écouter la nouvelle version, il suffit de réutiliser la commande sound, mais avec la nouvelle fréquence d’échantillonnage.

Normalisation et décalage

Les systèmes embarqués numériques, possédant des convertisseurs numérique-analogique, permettent de restituer des données uniquement positives.
Les signaux sonores sont quant à eux à valeur moyenne nulle. Il faut donc normaliser le signal pour qu’il soit coder sur le bon nombre d’informations binaires et ensuite le décaler pour que toutes les valeurs soient positives et centrer sur le milieu de la plage disponible sur le convertisseur.

Pour cela, on peut utiliser le script suivant :

% Normalisation entre 0 et 1 avec valeur moyenne de 0.5
M=max(abs(son_echantillonne(:,1)));
son_normalise=(son_echantillonne(:,1)./(2*M))+0.5;  % Normalisation entre 0 et 1.0

Ecriture dans un fichier header

fichier=fopen(nom_fichier_sortie,'w');
fprintf(fichier,'#define NB_ECH %d \n', length(son_normalise));
fprintf(fichier,'const float son_ech[%d]={', length(son_normalise));
for k=1:length(son_normalise)-1
    fprintf(fichier,'%f,', son_normalise(k,1));
    if mod(k,100) == 0
        fprintf(fichier,'\n');
    end
end
fprintf(fichier,'%f};\n', son_normalise(length(son_normalise),1));
fclose(fichier);

Code complet

%% Rééchantillonnage d'un son MP3/WAV
%   Utilisation pour carte Nucléo/Arduino
%   Fichier de sortie .h
clear all;
close all;
%% AUTEUR : Villemejane Julien / LEnsE 2019
%   Version 1.0 -- 09/03/2019

nom_fichier_sortie = 'fichier_son.h';
nom_fichier_entree = 'Laser.mp3';
Fs_initial = 44100;     % Fréquence d'échantillonnage initiale
Fs_n = 12000;           % Nouvelle fréquence d'échantillonnage

son_initial = audioread(nom_fichier_entree);    % pour récupérer le signal sonore dans une matrice

W_initial = linspace(-Fs_initial/2,Fs_initial/2-((Fs_initial/2)/length(son_initial)), length(son_initial));
fft_son_init = fft(son_initial(:,1))/Fs_initial;       % Passage en mono et FFT

fft_son_init = fftshift(fft_son_init);      % Affichage de la FFT
figure(1);
plot(W_initial,20*log(abs(fft_son_init)));
xlabel('Frequence en Hz');
ylabel('FFT du signal sonore (dB)');

% Filtrage Passe-bas
[b,a] = butter(20,Fs_n/(2*Fs_initial),'low'); 
son_initial_filtre = filter(b,a,son_initial); 
%sound(son_initial_filtre, Fs_initial); 

fft_son_filtre = fft(son_initial_filtre(:,1))/Fs_initial; 
fft_son_filtre = fftshift(fft_son_filtre);      

% Affichage de la FFT figure(2); plot(W_initial,20*log(abs(fft_son_init)),W_initial,20*log(abs(fft_son_filtre))); 
xlabel('Frequence en Hz'); 
ylabel('FFT du signal sonore (dB)'); 
legend('Son initial','Son filtre'); 

% Rééchantillonnage 
son_echantillonne = resample(son_initial_filtre, Fs_n, Fs_initial); %sound(son_echantillonne, Fs_n); 

% Normalisation entre 0 et 1 avec valeur moyenne de 0.5 M=max(abs(son_echantillonne(:,1))); 
son_normalise=(son_echantillonne(:,1)./(2*M))+0.5;  % Normalisation entre 0 et 1.0 
% Création d'un fichier contenant la définition et les échantillons fichier=fopen(nom_fichier_sortie,'w'); 
fprintf(fichier,'#define NB_ECH %d \n', length(son_normalise)); fprintf(fichier,'const float son_ech[%d]={', length(son_normalise)); 
for k=1:length(son_normalise)-1     
   fprintf(fichier,'%f,', son_normalise(k,1));     
   if mod(k,100) == 0         
      fprintf(fichier,'\n');     
   end 
end 
fprintf(fichier,'%f};\n', son_normalise(length(son_normalise),1)); fclose(fichier); 

Générer le signal analogique à intervalle régulier

Principe de fonctionnement

Pour pouvoir reproduire les échantillons au bon rythme, il est indispensable de les envoyer à un convertisseur numérique-analogique (interne – Générer une tension analogique – ou externe à la carte – Interfacer un convertisseur numérique-analogique externe en SPI -) de manière régulièreFaire une action à intervalle régulier – et à la fréquence à laquelle ils ont été (ré)échantillonnés .

Il est donc nécessaire de mettre en oeuvre les éléments suivants :

  • une action de conversion à intervalle régulier
  • une structure pour accueillir les différents coefficients et les valeurs des différents échantillons (voir paragraphe précédent sur la préparation des échantillons)
  • une sortie analogique

Enfin pour pouvoir vérifier le bon fonctionnement du système, il peut être intéressant de caractériser son temps de réponse (voir tutoriel Caractériser un traitement numérique).

Code d’exemple

#include "mbed.h"
#include "sonTest.hpp"    // fichier contenant le tableau son_ech

DigitalOut myled(LED1);
AnalogOut sortie_son(A2);  // DAC

Ticker tictoc;

void convert_audio_emetteur();
void convert_audio_recepteur();

int indice_tab;
float FE = 9000;
float TE = 1/FE;

int main() {
    tictoc.attach(&convert_audio_emetteur, TE);
    while(1);
}

void convert_audio_emetteur(){
    myled = !myled;
    if(indice_tab < NB_ECH){
        sortie_son.write((son_ech[indice_tab]-0.5)*0.05+0.5);
        indice_tab++;
    }
    else{
        if(indice_tab < NB_ECH + 2*F_ECH){
            sortie_son.write(0.5);
            indice_tab++;
        }
        else{
            indice_tab = 0;
        }
    }
}

MInE Prototyper Prototyper avec Nucleo et MBED

Nucleo – Générer un signal analogique périodique