Cours:Elen4 TNS TP TpsReel : Différence entre versions

De troyesGEII
Aller à : navigation, rechercher
(PID numérique avec un filtre numérique linéaire)
 
(74 révisions intermédiaires par le même utilisateur non affichées)
Ligne 2 : Ligne 2 :
  
 
<center>
 
<center>
'''<big>TP6 : Temps réel : Codage d'un bloc en C++ / démodulation et PID numérique</big>'''
+
'''<big>TP6 - Temps réel : bloc Gnuradio en Python / démodulation / PID Numérique</big>'''
 
</center>
 
</center>
  
Le but de ce TP est ...
+
Le but de ce TP est double :
 +
* réaliser des implémentations temps réel de filtre IIR en créant un bloc Gnuradio qui execute un traitement codé en Python
 +
* et étudier l'implémentation d'un PID numérique sous forme d'un filtre IIR.
  
 +
Dans les deux cas, un système temps réel sera considéré :
 +
* à intervalles de temps (réguliers ou non), une valeur ''x(n)'' rentre dans le système
 +
* le travail consiste à calculer ''y(n)'' avec l'équation au différences du filtre, à partir de la valeur ''x(n)'' et de ses versions précédentes ''x(n-1)'' et ''x(n-2)'' (et aussi ''y(n-1)'', ''y(n-2)'') qu'il est donc nécessaire de stocker.
 +
* La notion de ''temps réel'' implique que ''y(n)'' doit être calculer avant qu'une nouvelle valeurs de ''x(n)'' ne rentre dans le système.
  
=== Codage de bloc Gnuradio en c++ ===
+
=== Bloc Gnuradio en Python ===
  
==== Première partie - guide de création d'un bloc en C++ ====
+
Nous allons implémenter un filtre IIR dans gnuradio, en écrivant le traitement dans un bloc Python.
 +
 
 +
==== Filtre moyenneur====
 +
 
 +
{{q| a)}} Dans un premier temps, prendre connaissance du tutoriel suivant : [https://wiki.gnuradio.org/index.php?title=Creating_Your_First_Block wiki Gnuradio - ''Creating Your First Block'']
 +
 
 +
{{q| b)}} Nous allons écrire un filtre simple, qui implémente un simple moyenneur :
 +
<source lang="Python">
 +
 
 +
import numpy as np
 +
from gnuradio import gr
 +
 
 +
class blk(gr.sync_block): 
 +
    def __init__(self):  # Pas de paramètres
 +
        gr.sync_block.__init__(
 +
            self,
 +
            name="Filtre moyenneur", 
 +
            in_sig=[np.float32],
 +
            out_sig=[np.float32]
 +
        )
 +
        self.xn1 = 0 # initialisation d'un attribut pour stocker x(n-1)
 +
 
 +
    def work(self, input_items, output_items):
 +
        xn = input_items[0][0] # On ne "consomme" qu'un seul échantillon en entrée
 +
        yn = (xn + self.xn1) / 2
 +
        self.xn1 = xn
 +
        output_items[0][0] = yn # On ne produit donc également qu'un seul échantillon en sortie
 +
        return 1 # On retourne le nombre de valeurs consomées
 +
</source>
 +
 
 +
==== Filtre IIR : démodulation ====
 +
 
 +
En reprenant le signal modulé en amplitude et la fonction de transfert du filtre passe-bas du TP précédent :
 +
 
 +
* Obtenir l'équation aux différence du filtre, ''ie'' écrire ''y(n)='' en fonction de ''x(n)'', ''x(n-1)'', ''x(n-2)'', ''y(n-1)'' et ''y(n-2)''.
 +
* Créer un nouveau bloc Python dans le module <code>monModule</code> :
 +
** de type <code>sync</code>
 +
** une entrée en <code>np.float32</code>
 +
** une sortie en <code>np.float32</code>
 +
* Écrire le code permettant d'obtenir ''y(n)'' dans la méthode <code>work()</code> :
 +
** ''x(n)'' est donné par <code>in[0][0]</code>
 +
** il sera nécessaire de stocker dans des '''attributs''' les valeurs de ''x(n-1)'', ''x(n-2)'', ''y(n-1)'' et ''y(n-2)''.
 +
 
 +
{{q| a)}}  Réaliser cette implémentation du passe-bas et contrôler que la démodulation est identique à celle obtenue au [[Cours:Elen4_TNS_TP_TraitAudioEtDemodulation|TP 5]].
 +
 
 +
{{q| b)}}  Modifier votre code pour implémenter le filtre complet de démodulation.
 +
 
 +
=== Approximation de PID par filtre numérique linéaire ===
 +
 
 +
Cette partie sera travaillée dans un notebook Jupyter.
 +
 
 +
==== Rappels PID ====
 +
 
 +
* Principe
 +
 
 +
[[Fichier:PIDprincipe.png|400px]]
 +
 
 +
[[Fichier:PIDdetail.png|400px]]
 +
 
 +
* Définition mathématique, la sortie du controleur PID ''u(t)'' est donnée par:
 +
 
 +
[[Fichier:EqPID.png|400px]]
 +
 
 +
==== PID numérique "classique" ====
 +
 
 +
On considère que le temps est discrétisé : ''t = n.''d''t'' où d''t'' est l'intervalle de temps entre deux valeurs.
 +
 
 +
* En approximant grossièrement l'intégrale et la dérivée <br/>[[Fichier:PIDderivative.png|400px]] [[Fichier:PIDintegral.png|400px]]
 +
** La dérivée est calculée par ''D = (e(n)-e(n-1))/''d''t''
 +
** L'intégrale est approximée en cumulant progressivement : ''I = I + e(n).''d''t''
 +
C'est ainsi que vous avez implémenté vos PID jusqu'ici.
 +
 
 +
Voici un notebook exemple à explorer sur deux modélisations : asservissement de 1) température et de 2) position ''z'' d'un drone : [[Media:TNS TP6 starter.ipynb.zip|TNS_TP6_starter.ipynb.zip]]
 +
 
 +
{{q | a)}} ''Ki = Kd = 0'' => chercher le meilleur ''Kp'' => erreur statique.
 +
 
 +
{{q | b)}} ''Kd = 0'' => l'introduction du terme integral permet de corriger cette erreur statique.
 +
 
 +
{{q | c)}} Le terme dérivatif n'est pas nécessaire pour la température
 +
 
 +
{{q | d)}} Mais l'est pour asservir la position ''z'' du drone.
 +
 
 +
==== PID numérique avec un filtre numérique linéaire ====
 +
 
 +
Nous allons reprendre le PID, par sa fonction de transfert qui exprime un filtre linéaire continu, et construire un filtre linéaire numérique qui l'approxime.
 +
 
 +
 
 +
{{q | a)}} Cherchons la fonction de transfert du filtre continu d'un PID.
 +
<br/>En partant de l'équation différentielle
 +
<br/>[[Fichier:EqPID.png|500px]]
 +
<br/>On applique la transformée de Laplace (dérivée => multiplication par ''s'' ; intégrale => division par ''s'') :
 +
<br/>[[Fichier:eqFxTransfertPID.png|130]]
 +
<br/>Le terme dérivatif étant très sensible au bruit, on lui applique généralement un filtre passe bas (''w0/(w0+s)'') :
 +
<br/>[[Fichier:eqFxTransfertPIDetPB.png|130]]
 +
 
 +
{{q | b)}} Il s'agit maintenant de transformer la variable ''s'' en une variable ''z'' :
 +
* par transformée bilinéaire : <br/> ''s = (2/Ts)*(z-1)/(z+1)'' (voir le cours)
 +
* ou par ''backward euler'' (version un peu plus simple) : <br/>''s = (z-1)/(Ts*z)''.
 +
 
 +
En injectant le changement de variable dans la fonction de transfert ''H(s)'', obtenir la fonction de transfert ''H(z)'' du filtre linéaire numérique correspondant (dans un premier temps avec ''backward Euler'') :
 +
* soit "à la main"
 +
* ou en exploitant [https://www.sympy.org/en/index.html Sympy] (calcul symbolique). Un début :
 +
<source lang="Python">
 +
import sympy
 +
from sympy import symbols, Poly, Eq
 +
Ki, Kd, Kp, N, Ts, z, s = symbols('K_i K_d K_p N T_s z s')
 +
 
 +
Hs = Kp + (Ki/s)  + Kd*s * (N/(N+s))
 +
 
 +
Hz = Hs.subs(s, (z-1)/(Ts*z))
 +
 
 +
# ...
 +
</source>
 +
Dans les deux cas, il sera nécessaire d'obtenir ''H(z)'' sous la forme d'une fraction rationnelle.
 +
 
 +
{{q | c)}} Implémentation du filtre : classe <code>FilterController</code> dont voici le squelette
 +
<source lang="Python">
 +
class FilterController:
 +
 
 +
    def mfilter(self, e):
 +
        xn = e
 +
        # TODO
 +
        return yn   
 +
   
 +
    def __init__(self, K_p, K_i, K_d, N, T_s, set_point):
 +
        self.set_point = set_point
 +
        # A COMPLÉTER
 +
   
 +
    def get_control(self, measurement, dt):
 +
        e = self.set_point - measurement
 +
        u = self.mfilter(e)
 +
        return u
 +
</source>
 +
{{q | d)}} Appliquer ce contrôleur à la simulation d'asservissement de drone et vérifier qu'un comportement proche de celui de la partie précédente (PID numérique "classique") est obtenu.
 +
 
 +
{{q | e)}} Implémenter le filtre obtenu à l'aide de la transformée bilinéaire.
 +
 
 +
<!--
 +
=== Archive : codage de bloc Gnuradio en C++ ===
  
 
Ce guide est inspirée du guide [https://wiki.gnuradio.org/index.php?title=Creating_C%2B%2B_OOT_with_gr-modtool Creating an OOT (C++ block example)] en l'adaptant pour nos besoins :
 
Ce guide est inspirée du guide [https://wiki.gnuradio.org/index.php?title=Creating_C%2B%2B_OOT_with_gr-modtool Creating an OOT (C++ block example)] en l'adaptant pour nos besoins :
Ligne 18 : Ligne 162 :
 
'''Suivez bien toutes les étapes, sans aller trop rapidement'''
 
'''Suivez bien toutes les étapes, sans aller trop rapidement'''
  
===== Création d'un module externe =====
+
* Vous pourrez utiliser <code>gedit</code> pour éditer les divers fichiers.
 +
 
 +
==== Création d'un module externe ====
  
 
Dans les lignes qui suivent, le <code>$</code> indique que l'on saisi une commande dans un terminal. Le <code>$</code> n'est pas à saisir.
 
Dans les lignes qui suivent, le <code>$</code> indique que l'on saisi une commande dans un terminal. Le <code>$</code> n'est pas à saisir.
Ligne 35 : Ligne 181 :
 
$ gr_modtool newmod monModule
 
$ gr_modtool newmod monModule
 
</source>
 
</source>
* Le dossier <code>monModule</code> est créé et contient tout le code squelette d'un module OOT, mais il n'a pas encore de blocs. Déplacez-vous dans <code>monModule</code> :
+
* Le dossier <code>gr-monModule</code> est créé et contient tout le code squelette d'un module OOT, mais il n'a pas encore de blocs. Déplacez-vous dans <code>gr-monModule</code> :
 
<source>
 
<source>
$ cd monModule
+
$ cd gr-monModule
 
</source>
 
</source>
 
* Vous pouvez afficher le contenu de ce dossier par
 
* Vous pouvez afficher le contenu de ce dossier par
Ligne 44 : Ligne 190 :
 
</source>
 
</source>
  
===== Création d'un bloc dans ce module =====
+
==== Création d'un bloc dans ce module ====
  
 
* Ajoutez un nouveau bloc nommé <code>passetout</code> :
 
* Ajoutez un nouveau bloc nommé <code>passetout</code> :
Ligne 82 : Ligne 228 :
 
Adding file 'lib/passetout_impl.h'...
 
Adding file 'lib/passetout_impl.h'...
 
Adding file 'lib/passetout_impl.cc'...
 
Adding file 'lib/passetout_impl.cc'...
Adding file 'include/gnuradio/customModule/passetout.h'...
+
Adding file 'include/gnuradio/monModule/passetout.h'...
Adding file 'python/customModule/bindings/docstrings/passetout_pydoc_template.h'...
+
Adding file 'python/monModule/bindings/docstrings/passetout_pydoc_template.h'...
Adding file 'python/customModule/bindings/passetout_python.cc'...
+
Adding file 'python/monModule/bindings/passetout_python.cc'...
 
Adding file 'grc/monModule_passetout.block.yml'...
 
Adding file 'grc/monModule_passetout.block.yml'...
 
Editing grc/CMakeLists.txt...
 
Editing grc/CMakeLists.txt...
Ligne 91 : Ligne 237 :
 
Quelques indications :
 
Quelques indications :
 
* le fichier <code>passetout_impl.h</code> contient l'entête de la classe de notre bloc
 
* le fichier <code>passetout_impl.h</code> contient l'entête de la classe de notre bloc
* le fichier <code>passetout_impl.cpp</code> contient l'implémentation de la classe de notre bloc
+
* le fichier <code>passetout_impl.cc</code> contient l'implémentation de la classe de notre bloc
 
* le fichier <code>monModule_passetout.block.yml</code> va contenir des informations faisant le lien entre notre code c++ et le code python utilisé en interne par Gnuradio
 
* le fichier <code>monModule_passetout.block.yml</code> va contenir des informations faisant le lien entre notre code c++ et le code python utilisé en interne par Gnuradio
 
* Le tout sera compilé par un <code>Makefile</code> généré par <code>cmake</code>.
 
* Le tout sera compilé par un <code>Makefile</code> généré par <code>cmake</code>.
  
===== Fichier <code>passetout_impl.h</code> =====
+
==== Fichier <code>passetout_impl.h</code> ====
 +
 
 +
Le code utile généré est le suivant :
 +
<source lang=cpp>
 +
class passetout_impl : public passetout
 +
{
 +
private:
 +
    // Nothing to declare in this block.
 +
 
 +
public:
 +
    passetout_impl();
 +
    ~passetout_impl();
 +
 
 +
    // Where all the action really happens
 +
    int work(int noutput_items,
 +
            gr_vector_const_void_star& input_items,
 +
            gr_vector_void_star& output_items);
 +
};
 +
</source>
 +
 
 +
Pour se réveiller les neurones sur la POO, qu'est-ce que sont
 +
* <code>passetout_impl()</code>,
 +
* <code>~passetout_impl()</code>,
 +
* <code>private</code>, <code>public</code> ?
 +
 
 +
Le travail sera réalisé dans la méthode <code>work()</code>, mais nous n'avons pas de modification à apporter dans les déclarations par défaut.
 +
 
 +
==== Fichier <code>passetout_impl.cc</code> ====
 +
 
 +
Le code utile généré est le suivant :
 +
 
 +
{{boîte déroulante/début|titre=passetout_impl.cc}}
 +
<source lang=cpp>
 +
namespace monModule {
 +
 
 +
#pragma message("set the following appropriately and remove this warning")
 +
using input_type = float;
 +
#pragma message("set the following appropriately and remove this warning")
 +
using output_type = float;
 +
passetout::sptr passetout::make() { return gnuradio::make_block_sptr<passetout_impl>(); }
 +
 
 +
 
 +
/*
 +
* The private constructor
 +
*/
 +
passetout_impl::passetout_impl()
 +
    : gr::sync_block("passetout",
 +
                    gr::io_signature::make(
 +
                        1 /* min inputs */, 1 /* max inputs */, sizeof(input_type)),
 +
                    gr::io_signature::make(
 +
                        1 /* min outputs */, 1 /*max outputs */, sizeof(output_type)))
 +
{
 +
}
 +
 
 +
/*
 +
* Our virtual destructor.
 +
*/
 +
passetout_impl::~passetout_impl() {}
 +
 
 +
int passetout_impl::work(int noutput_items,
 +
                        gr_vector_const_void_star& input_items,
 +
                        gr_vector_void_star& output_items)
 +
{
 +
    auto in = static_cast<const input_type*>(input_items[0]);
 +
    auto out = static_cast<output_type*>(output_items[0]);
 +
 
 +
#pragma message("Implement the signal processing in your block and remove this warning")
 +
    // Do <+signal processing+>
 +
 
 +
    // Tell runtime system how many output items we produced.
 +
    return noutput_items;
 +
}
 +
 
 +
} /* namespace monModule */
 +
</source>
 +
{{boîte déroulante/fin}}
 +
 
 +
Détaillons :
 +
<source lang=cpp>
 +
#pragma message("set the following appropriately and remove this warning")
 +
using input_type = float;
 +
#pragma message("set the following appropriately and remove this warning")
 +
using output_type = float;
 +
</source>
 +
{{Todos| On spécifie ici le type des données d'entrée et de sortie. Pas de modification à apporter, il suffit de supprimer les deux <code>#pragma</code>}}
 +
 
 +
<source lang=cpp>
 +
passetout_impl::passetout_impl()
 +
    : gr::sync_block("passetout",
 +
                    gr::io_signature::make(
 +
                        1 /* min inputs */, 1 /* max inputs */, sizeof(input_type)),
 +
                    gr::io_signature::make(
 +
                        1 /* min outputs */, 1 /*max outputs */, sizeof(output_type)))
 +
{
 +
}
 +
</source>
 +
 
 +
{{Todos| On spécifie ici le nombre d'entrées (1) et le nombre de sorties (1). Pas de modification non plus.}}
 +
 
 +
{{Todos| Expliquez la syntaxe <code>passetout_impl::passetout_impl() : gr::sync_block(...)"</code> }}
 +
 
 +
<source lang=cpp>
 +
int passetout_impl::work(int noutput_items,
 +
                        gr_vector_const_void_star& input_items,
 +
                        gr_vector_void_star& output_items)
 +
{
 +
    auto in = static_cast<const input_type*>(input_items[0]);
 +
    auto out = static_cast<output_type*>(output_items[0]);
 +
 
 +
#pragma message("Implement the signal processing in your block and remove this warning")
 +
    // Do <+signal processing+>
 +
 
 +
    // Tell runtime system how many output items we produced.
 +
    return noutput_items;
 +
}
 +
</source>
 +
Cette méthode <code>work()</code> implémente véritablement le filtrage.
 +
* Les tableaux <code>in</code> et <code>out</code> contiennent respectivement les données en entrée et les données en sortie. Il faut donc calculer <code>out</code> en fonction de <code>in</code>
 +
* Les données d'entrée arrivent par paquets dans un buffer. La variable <code>noutput_items</code> contient le nombre de données disponibles en entrées.
 +
* à la fin de la méthode, on renvoie le nombre de données en sortie que l'on a calculé (entre 1 et <code>noutput_items</code>). Les données d'entrées éventuellement non consommées seront conservées dans le buffer pour le prochain appel de <code>work()</code>
 +
{{Todos| On souhaite ici seulement recopier l'entrée sur la sortie, donc }}
 +
# Enlever la ligne <code>#pragma</code>
 +
# ajouter le code <code> out[0] = in[0];</code> (on recopie le premier échantillon disponible en entrée)
 +
# changer <code>return noutput_items;</code> en <code>return 1;</code> (un seul échantillon a été consommé).
 +
 
 +
==== Fichier <code>monModule_passetout.block.yml</code> ====
 +
 
 +
Ce fichier au format YAML (''yet another markup language'') fait le lien entre le code c++ et le code python utilisé en interne par GnuRadio.
 +
 
 +
{{boîte déroulante/début|titre=monModule_passetout.block.yml}}
 +
<source lang=yaml>
 +
id: monModule_passetout
 +
label: passetout
 +
category: '[monModule]'
 +
 
 +
templates:
 +
  imports: from gnuradio import monModule
 +
  make: monModule.passetout()
 +
 
 +
#  Make one 'parameters' list entry for every parameter you want settable from the GUI.
 +
#    Keys include:
 +
#    * id (makes the value accessible as keyname, e.g. in the make entry)
 +
#    * label (label shown in the GUI)
 +
#    * dtype (e.g. int, float, complex, byte, short, xxx_vector, ...)
 +
#    * default
 +
parameters:
 +
- id: parametername_replace_me
 +
  label: FIX ME:
 +
  dtype: string
 +
  default: You need to fill in your grc/monModule_passetout.block.yaml
 +
#- id: ...
 +
#  label: ...
 +
#  dtype: ...
 +
 
 +
#  Make one 'inputs' list entry per input and one 'outputs' list entry per output.
 +
#  Keys include:
 +
#      * label (an identifier for the GUI)
 +
#      * domain (optional - stream or message. Default is stream)
 +
#      * dtype (e.g. int, float, complex, byte, short, xxx_vector, ...)
 +
#      * vlen (optional - data stream vector length. Default is 1)
 +
#      * optional (optional - set to 1 for optional inputs. Default is 0)
 +
inputs:
 +
#- label: ...
 +
#  domain: ...
 +
#  dtype: ...
 +
#  vlen: ...
 +
#  optional: ...
 +
 
 +
outputs:
 +
#- label: ...
 +
#  domain: ...
 +
#  dtype: ...
 +
#  vlen: ...
 +
#  optional: ...
 +
 
 +
#  'file_format' specifies the version of the GRC yml format used in the file
 +
#  and should usually not be changed.
 +
file_format: 1
 +
</source>
 +
{{boîte déroulante/fin}}
 +
 
 +
{{Todos| Mettre à jour la section <code>parameters</code> (pas de paramètres, on commente) :}}
 +
 
 +
<source lang=yaml>
 +
#parameters:
 +
#- id: parametername_replace_me
 +
#  label: FIX ME:
 +
#  dtype: string
 +
#  default: You need to fill in your grc/customModule_testMod.block.yaml
 +
</source>
 +
 
 +
{{Todos| Mettre à jour la section <code>inputs</code> :}}
 +
 
 +
<source lang=yaml>
 +
inputs:
 +
- label: in
 +
  domain: stream
 +
  dtype: float
 +
</source>
 +
 
 +
{{Todos| Mettre à jour la section <code>output</code> :}}
 +
 
 +
<source lang=yaml>
 +
outputs:
 +
- label: out
 +
  domain: stream
 +
  dtype: float
 +
</source>
 +
 
 +
==== Compilation et installation ====
 +
 
 +
Voici les étapes :
 +
<source lang=bash>
 +
$ cd CHEMIN-DE-VOTRE-DOSSIER/gr-monModule
 +
$ mkdir build
 +
$ cd build
 +
$ cmake ..
 +
$ make
 +
$ make install
 +
</source>
 +
 
 +
* <code>cmake ..</code> lance la création des fichiers <code>Makefile</code> qui permettent la compilation et l'installation
 +
* <code>make</code> lance la compilation
 +
* <code>make install</code> lance l'installation (copie des fichiers dans les dossiers gnuradio)
 +
 
 +
{{Todos| Exécuter chacune de ces lignes, en contrôlant l'absence d'erreur. Dans le cas contraire, contrôler les fichiers <code>.h</code>, <code>.cc</code> et <code>.yaml</code>}}
 +
 
 +
{{Todos| '''Après ces étapes, et avant de lancer gnuRadio, appeler l'enseignant qui va mettre à jours les bibliothèques partagées.'''}}. Sinon tenter :
 +
<source>
 +
$ export LD_LIBRARY_PATH=/usr/local/lib/x86_64-linux-gnu:$LD_LIBRARY_PATH
 +
$ gnuradio-companion &
 +
</source>
 +
 
 +
==== Utilisation du bloc et test ====
  
===== Fichier <code>passetout_impl.cpp</code> =====
+
* Dans Gnuradio, rafraichir la liste des bloc (bouton ''reload'')
 +
* le bloc <code>passetout</code> doit être présent dans la section <code>monModule</code>
 +
* Tester le bon fonctionnement de ce bloc en lui envoyant une sinusoïde que vous devez observer identique à la sortie du bloc. Vérifier avec des graphes.
  
===== Fichier <code>monModule_passetout.block.yml</code> =====
+
==== Application : démodulation ====
  
==== Seconde partie - codage d'un filtre RII ====
+
En reprenant le signal modulé en amplitude et la fonction de transfert du filtre passe-bas du TP précédent :
  
==== Seconde partie - codage d'un filtre RIF ====
+
* Obtenir l'équation aux différence du filtre, ''ie'' écrire ''y(n)='' en fonction de ''x(n)'', ''x(n-1)'', ''x(n-2)'', ''y(n-1)'' et ''y(n-2)''.
 +
* Créer un nouveau bloc C++ (similaire au passe-tout précédent) dans le module <code>monModule</code> :
 +
** de type <code>sync</code>
 +
** une entrée en <code>float</code>
 +
** une sortie en <code>float</code>
 +
* Écrire le code permettant d'obtenir ''y(n)'' dans la méthode <code>work()</code> :
 +
** ''x(n)'' est donné par <code>in0[0]</code>
 +
** il sera nécessaire d'affecter <code>out0[0]</code> pour produire ''y(n)''.
 +
** il sera nécessaire de stocker dans des '''attributs''' les valeurs de <code>in[0]</code> afin de disposer de ''x(n-1)'', ''x(n-2)'', ''y(n-1)'' et ''y(n-2)''.
 +
** On rappelle que les attributs doivent être déclarées dans le fichier <code>.h</code> et initialisés dans le constructeur de la classe.
  
=== Démodulation ===
+
{{Todos| Réaliser cette implémentation du passe-bas et contrôler que la démodulation est identique à celle obtenue au TP précédent}}
  
=== PID Numérique ===
+
{{Todos| Modifier votre code pour implémenter le filtre complet de démodulation}}
 +
-->

Version actuelle datée du 1 juin 2026 à 15:03

Retour à la page du cours

TP6 - Temps réel : bloc Gnuradio en Python / démodulation / PID Numérique

Le but de ce TP est double :

  • réaliser des implémentations temps réel de filtre IIR en créant un bloc Gnuradio qui execute un traitement codé en Python
  • et étudier l'implémentation d'un PID numérique sous forme d'un filtre IIR.

Dans les deux cas, un système temps réel sera considéré :

  • à intervalles de temps (réguliers ou non), une valeur x(n) rentre dans le système
  • le travail consiste à calculer y(n) avec l'équation au différences du filtre, à partir de la valeur x(n) et de ses versions précédentes x(n-1) et x(n-2) (et aussi y(n-1), y(n-2)) qu'il est donc nécessaire de stocker.
  • La notion de temps réel implique que y(n) doit être calculer avant qu'une nouvelle valeurs de x(n) ne rentre dans le système.

Bloc Gnuradio en Python

Nous allons implémenter un filtre IIR dans gnuradio, en écrivant le traitement dans un bloc Python.

Filtre moyenneur

a) Dans un premier temps, prendre connaissance du tutoriel suivant : wiki Gnuradio - Creating Your First Block

b) Nous allons écrire un filtre simple, qui implémente un simple moyenneur :

import numpy as np
from gnuradio import gr

class blk(gr.sync_block):  
    def __init__(self):  # Pas de paramètres
        gr.sync_block.__init__(
            self,
            name="Filtre moyenneur",  
            in_sig=[np.float32],
            out_sig=[np.float32]
        )
        self.xn1 = 0 # initialisation d'un attribut pour stocker x(n-1)

    def work(self, input_items, output_items):
        xn = input_items[0][0] # On ne "consomme" qu'un seul échantillon en entrée
        yn = (xn + self.xn1) / 2
        self.xn1 = xn
        output_items[0][0] = yn # On ne produit donc également qu'un seul échantillon en sortie
        return 1 # On retourne le nombre de valeurs consomées

Filtre IIR : démodulation

En reprenant le signal modulé en amplitude et la fonction de transfert du filtre passe-bas du TP précédent :

  • Obtenir l'équation aux différence du filtre, ie écrire y(n)= en fonction de x(n), x(n-1), x(n-2), y(n-1) et y(n-2).
  • Créer un nouveau bloc Python dans le module monModule :
    • de type sync
    • une entrée en np.float32
    • une sortie en np.float32
  • Écrire le code permettant d'obtenir y(n) dans la méthode work() :
    • x(n) est donné par in[0][0]
    • il sera nécessaire de stocker dans des attributs les valeurs de x(n-1), x(n-2), y(n-1) et y(n-2).

a) Réaliser cette implémentation du passe-bas et contrôler que la démodulation est identique à celle obtenue au TP 5.

b) Modifier votre code pour implémenter le filtre complet de démodulation.

Approximation de PID par filtre numérique linéaire

Cette partie sera travaillée dans un notebook Jupyter.

Rappels PID

  • Principe

PIDprincipe.png

PIDdetail.png

  • Définition mathématique, la sortie du controleur PID u(t) est donnée par:

EqPID.png

PID numérique "classique"

On considère que le temps est discrétisé : t = n.dt où dt est l'intervalle de temps entre deux valeurs.

  • En approximant grossièrement l'intégrale et la dérivée
    PIDderivative.png PIDintegral.png
    • La dérivée est calculée par D = (e(n)-e(n-1))/dt
    • L'intégrale est approximée en cumulant progressivement : I = I + e(n).dt

C'est ainsi que vous avez implémenté vos PID jusqu'ici.

Voici un notebook exemple à explorer sur deux modélisations : asservissement de 1) température et de 2) position z d'un drone : TNS_TP6_starter.ipynb.zip

a) Ki = Kd = 0 => chercher le meilleur Kp => erreur statique.

b) Kd = 0 => l'introduction du terme integral permet de corriger cette erreur statique.

c) Le terme dérivatif n'est pas nécessaire pour la température

d) Mais l'est pour asservir la position z du drone.

PID numérique avec un filtre numérique linéaire

Nous allons reprendre le PID, par sa fonction de transfert qui exprime un filtre linéaire continu, et construire un filtre linéaire numérique qui l'approxime.


a) Cherchons la fonction de transfert du filtre continu d'un PID.
En partant de l'équation différentielle
EqPID.png
On applique la transformée de Laplace (dérivée => multiplication par s ; intégrale => division par s) :
130
Le terme dérivatif étant très sensible au bruit, on lui applique généralement un filtre passe bas (w0/(w0+s)) :
130

b) Il s'agit maintenant de transformer la variable s en une variable z :

  • par transformée bilinéaire :
    s = (2/Ts)*(z-1)/(z+1) (voir le cours)
  • ou par backward euler (version un peu plus simple) :
    s = (z-1)/(Ts*z).

En injectant le changement de variable dans la fonction de transfert H(s), obtenir la fonction de transfert H(z) du filtre linéaire numérique correspondant (dans un premier temps avec backward Euler) :

  • soit "à la main"
  • ou en exploitant Sympy (calcul symbolique). Un début :
import sympy
from sympy import symbols, Poly, Eq
Ki, Kd, Kp, N, Ts, z, s = symbols('K_i K_d K_p N T_s z s')

Hs = Kp + (Ki/s)  + Kd*s * (N/(N+s))

Hz = Hs.subs(s, (z-1)/(Ts*z))

# ...

Dans les deux cas, il sera nécessaire d'obtenir H(z) sous la forme d'une fraction rationnelle.

c) Implémentation du filtre : classe FilterController dont voici le squelette

class FilterController:

    def mfilter(self, e):
        xn = e
        # TODO
        return yn    
    
    def __init__(self, K_p, K_i, K_d, N, T_s, set_point):
        self.set_point = set_point
        # A COMPLÉTER
    
    def get_control(self, measurement, dt):
        e = self.set_point - measurement
        u = self.mfilter(e)
        return u

d) Appliquer ce contrôleur à la simulation d'asservissement de drone et vérifier qu'un comportement proche de celui de la partie précédente (PID numérique "classique") est obtenu.

e) Implémenter le filtre obtenu à l'aide de la transformée bilinéaire.