Table des matières

Filtrage numérique d'un signal audio

1. Introduction

Cette page présente un filtre numérique temps réel implémenté sur l'Arduino Due (microcontrôleur SAM3X8E 32 bits, processeur Cortex M3). Le filtre fonctionne en 12 bits et peut traiter des signaux stéreo dans le domaine audio.

Pour l'explication détaillée des techniques utilisées :

2. Circuit d'interface

Le convertisseur analogique/numérique traite des tensions dans la plage [0,3.3] V. Le circuit analogique suivant permet de traiter des signaux alternatifs centrés sur la masse, dans la plage [-1.65,1.65] V :

../can/interfaceArduinoDue-ADC.svgFigure pleine page

Les deux entrées analogique A0 et A1 sont utilisées. L'amplificateur de gain unité effectue un décalage de manière à fournir un signal centré sur 1,65 V. Une tension nulle en entrée doit conduire à la valeur numérique 0x7FFF (convertisseur 12 bits). Le potentiomètre permet de faire le réglage. Pour une explication détaillée de ce circuit, voir Conversion analogique numérique et analogique.

Les signaux numériques traités par le microcontrôleur seront donc affectés d'un décalage, que l'on désignera par la constante offset, et qu'il faudra retrancher avant de faire le filtrage numérique (surtout pour un filtre qui coupe la composante DC).

Le convertisseur numérique-analogique (sorties DAC0 et DAC1) fonctionne aussi en 12 bits, mais la plage de tensions en sortie est réduite à [0.7,2.6] V environ. En effet l'étage de sortie n'est pas rail-to-rail, et il y donc un décalage de 0.7 V par rapport aux tensions d'alimentation. Pour un filtrage de gain unité, il y a donc un gain d'environ 1.4 à appliquer au signal numérique pour retrouver en sortie la même amplitude qu'en entrée. On appliquera ce gain correctif dans le programme de filtrage.

Pour récupérer un signal en sortie centré sur zéro, on ajoute le circuit suivant :

../synthese/interfaceArduinoDue-DAC.svgFigure pleine page

L'amplificateur doit être alimenté par une double alimentation dont le point milieu est relié à la masse de l'Arduino, par exemple -5/0/5 V.

Compte tenu du gain de 1.4, la plage de tensions effectivement traitée est [-1.15,1.15] V. On prévoira un traitement logiciel pour assurer la saturation en cas de dépassement, mais il est prudent de considérer plutôt la plage [-1.1,1.1] V. Le convertisseur analogique-numérique peut être configuré avec un gain de 1, 2 ou 4 pour amplifier les très faibles signaux.

3. Programme Arduino

3.a. introduction

Le programme effectue une numérisation sur une ou deux voies. Le convertisseur analogique-numérique (ADC) est déclenché avec un Timer à la fréquence d'échantillonnage. Pour chaque numérisation, une interruption est déclenchée afin de faire le filtrage et d'envoyer le résultat au convertisseur numérique-analogique (une ou deux voies). La programmation du filtre se fait depuis un ordinateur par un programme Python, mais on pourra sans difficulté construire une application embarquée autonome à partir de ce programme.

Trois formes de réalisation du filtrage sont implémentées : les formes directes de type I et II et la forme directe transposée de type II. Pour la définition de ces formes, voir Conception et mise en œuvre des filtres numériques.

Les coefficients du filtre et l'accumulateur sont soit des flottants 32 bits, soit des entiers 32 bits (plus rapide mais moins précis).

Voici tout d'abord la définition des constantes et des variables globales :

ArduinoDueFiltrageNumerique.ino
#include "Arduino.h"

#define IS_READY 100
#define SET_FILTRAGE_32BITS 103
#define SET_FILTRAGE_FLOAT 104

#define GAIN 1.45
#define SAT
#define TTLOUT 

// ADC
#define ADC_MR_TRIG1 (1<<1)
uint8_t nchannels;
#define MAX_NCHAN 2
uint8_t channels[MAX_NCHAN];
uint8_t gain[MAX_NCHAN];
uint8_t channels_pins[8] ={7,6,5,4,3,2,1,0};
uint32_t ticks;
volatile void (*ADC_Handler_function)();
uint8_t flag = 0;

// ADC
uint32_t chsel;

//filtrage
#define MAX_NCOEF 256
uint32_t nb; // nombre de coefficients b
float bbb[MAX_NCOEF];
int32_t bb[MAX_NCOEF];
uint32_t na; // nombre de coefficients a
float aaa[MAX_NCOEF];
int32_t aa[MAX_NCOEF];
uint32_t max_na_nb;
uint8_t ab_shift;
#define TAILLE_TAMPON 0x100 // supérieur à max(na,nb)
#define MASQUE_TAMPON 0xFF // TAILLE_TAMPON-1
int16_t x_0[TAILLE_TAMPON]; // tampon pour les x_k
int16_t y_0[TAILLE_TAMPON]; // tampon pour les y_k
int16_t x_1[TAILLE_TAMPON]; // tampon pour les x_k
int16_t y_1[TAILLE_TAMPON]; // tampon pour les y_k
int32_t ww_0[TAILLE_TAMPON];
int32_t ww_1[TAILLE_TAMPON];
float www_0[TAILLE_TAMPON];
float www_1[TAILLE_TAMPON];
uint8_t J_tampon; // indice pour les tampons
int16_t offset;
             

La macro GAIN fixe le gain à appliquer pour que la sortie soit de même amplitude que l'entrée (pour un filtrage de gain unité). Ce gain est déterminé par étallonnage. On peut aussi inclure ce gain dans les coefficients bk du filtre; dans ce cas, on écrira #define GAIN 1.

Lorsque la macro SAT est définie, une saturation est appliquée à la sortie du filtre en cas de dépassement avant redirection vers le convertisseur N/A. Si cette opération n'est pas nécessaire, on peut enlever cette ligne.

Lorsque la macro TTLOUT est définie, un signal TTL (sur la borne D3) est émis par la fonction de filtrage, dont la fréquence est la moitié de la fréquence d'échantillonnage. Ce signal permet de vérifier le bon fonctionnement de l'échantillonnage.

La taille des tampons circulaires est fixée à 0x100 (256) et ne doit pas être modifiée. Cela permet d'utiliser des indices de type uint8_t qui se comportent automatiquement de manière circulaire (255+1=0 et 0-1=255). Le nombre maximal de coefficients a ou b est ici fixé à 256; il ne peut dépasser cette valeur, car l'indice d'accès aux tableaux est de type uint8_t.

3.b. Configuration des convertisseurs et de l'échantillonnage

La fonction suivante configure le convertisseur A/N en fonction du nombre de voies nchannels, du numéro des voies channels et des gain gain. Chaque voie peut avoir son propre gain, égal à 1,2 ou 4. L'amplification se fait avec un offset, c'est-à-dire que la tension amplifiée est U-offset. Pour une explication détaillée de la configuration de l'ADC, voir Conversion analogique numérique et analogique.

void config_adc() {
    int k;
    pmc_enable_periph_clk(ID_ADC);
    adc_init(ADC, VARIANT_MCK, ADC_FREQ_MAX, 0);
    adc_configure_timing(ADC, 0, ADC_SETTLING_TIME_3, 1);
    adc_set_resolution(ADC,ADC_12_BITS);
    NVIC_EnableIRQ (ADC_IRQn) ;   
    ADC->ADC_IDR=0xFFFFFFFF ;  
    ADC->ADC_CHDR = 0xFFFF; 
    ADC->ADC_CHSR = 0x0000; 
    ADC->ADC_WPMR &= ~0x1;
    ADC->ADC_CHER = 0x0000;
    ADC->ADC_SEQR1 = 0x00000000;
    ADC->ADC_CGR = 0x00000000;
    ADC->ADC_COR = 0x00000000;
    for (k=0; k<nchannels; k++) {
      ADC->ADC_SEQR1 |= ((channels[k]&0xF)<<(4*k));
      ADC->ADC_CHER |= (1<<k);
      if (gain[k]==2) ADC->ADC_CGR |= 2 << (2*k);
      if (gain[k]==4) ADC->ADC_CGR |= 3 << (2*k);
      ADC->ADC_COR |= 1 << k; 
    }
    ADC->ADC_MR = (ADC->ADC_MR & 0xFFFFFFF0) | ADC_MR_TRIG1 | ADC_MR_TRGEN | ADC_MR_USEQ;
    ADC->ADC_IDR=~(1<<channels[0]); 
    ADC->ADC_IER=1<<channels[0];
}
                

Une interruption est déclenchée dès que la conversion des voies est faite (on se limitera ici à deux voies). Voici la fonction appelée lors d'une interruption. Elle appelle une fonction que l'on définira en fonction du type de filtrage à effectuer :

void ADC_Handler() {
    (*ADC_Handler_function)();
#ifdef TTLOUT
  if (flag==0) {
      PIOC->PIO_SODR = PIO_PC28; // pin 3
      flag = 1;
    }
  else {
      PIOC->PIO_CODR = PIO_PC28;
      flag = 0;
   }
#endif
}
                 

Le convertisseur est configuré pour être déclenché par un Timer. Voici la configuration du timer, qui génère la conversion avec une période égale à ticks fois la période de l'horloge, laquelle est la moitié de l'horloge principale de l'Arduino Due, c'est-à-dire 42MHz. Par exemple, avec ticks=1000, on a une fréquence d'échantillonnage de 42kHz (adaptée au traitement d'un signal audio).

void config_timer(uint32_t ticks) {
  uint32_t channel = 0;
  uint8_t clock = TC_CMR_TCCLKS_TIMER_CLOCK1; // horloge 84MHz/2=42 MHz
  pmc_enable_periph_clk (TC_INTERFACE_ID + 0*3+channel) ;
  TC_Configure(TC0,channel, TC_CMR_WAVE | TC_CMR_WAVSEL_UP_RC | TC_CMR_ACPA_CLEAR | TC_CMR_ACPC_SET |clock); 
  TC_SetRC(TC0,channel,ticks); 
  TC_SetRA(TC0,channel,ticks >> 1);
  TC_Start(TC0, channel);
}
void stop_timer() {
    uint32_t channel = 0;
    TC_Stop(TC0,channel);
    pmc_disable_periph_clk (TC_INTERFACE_ID + 0*3+channel);
}
                

La fonction suivante configure une seule sortie N/A (DAC0 ou DAC1) :

void config_dac(uint32_t channel) {
   pmc_enable_periph_clk(DACC_INTERFACE_ID);
   dacc_reset(DACC_INTERFACE);
   dacc_set_transfer_mode(DACC_INTERFACE, 0); // half word transfert (16 bits)
   dacc_set_power_save(DACC_INTERFACE, 0, 0);
   dacc_set_timing(DACC_INTERFACE, 0x08, 0, 0x10);
   dacc_set_analog_control(DACC_INTERFACE, DACC_ACR_IBCTLCH0(0x02)|DACC_ACR_IBCTLCH1(0x02)|DACC_ACR_IBCTLDACCORE(0x01));
   dacc_set_channel_selection(DACC_INTERFACE, channel);
   if ((dacc_get_channel_status(DACC_INTERFACE) & (1 << channel)) == 0) 
				dacc_enable_channel(DACC_INTERFACE, channel);
}
                

L'écriture d'une valeur à convertir se fera dans le registre DACC_INTERFACE->DACC_CDR (conversion data register), sous la forme d'un mot de 16 bits (mais seulement 12 bits sont utilisés).

La fonction suivante configure les deux sorties DAC0 et DAC1, pour faire un filtrage en stéréo (les deux voies subissent le même traitement).

void config_double_dac() {
   pmc_enable_periph_clk(DACC_INTERFACE_ID);
   dacc_reset(DACC_INTERFACE);
   dacc_set_transfer_mode(DACC_INTERFACE, 1); //  word transfert (32 bits)
   dacc_set_power_save(DACC_INTERFACE, 0, 0);
   dacc_set_timing(DACC_INTERFACE, 0x08, 0, 0x10);
   dacc_set_analog_control(DACC_INTERFACE, DACC_ACR_IBCTLCH0(0x02)|DACC_ACR_IBCTLCH1(0x02)|DACC_ACR_IBCTLDACCORE(0x01));
   DACC_INTERFACE->DACC_MR |= DACC_MR_TAG;
   DACC_INTERFACE->DACC_CHER = 0x3;
   chsel = (1<<13) | (1<<28);
}
                 

Dans ce cas, les deux nombres 12 bits à convertir doivent être écrits dans les 32 bits du registre DACC_INTERFACE->DACC_CDR. Avec cette écriture simultanée, le délai entre les deux conversions est très court, de l'ordre d'une microseconde, ce qui est pratiquement instantané à l'échelle d'une période d'échantillonnage audio.

3.c. Filtrage en flottant

On considère tout d'abord le filtrage avec des coefficients et un accumulateur sous forme de nombres à virgule flottante 32 bits. Ce type de filtrage est relativement lent car le microprocesseur Cortex M3 n'a pas d'unité de calcul en virgule flottante (le compilateur génère un calcul logiciel). Pour un filtre à faible nombre de coefficients (surtout les filtres récursifs), il est toutefois préférable de filtrer avec des flottants pour limiter les erreurs d'arrondis.

Il y a trois types de filtrage : forme directe I (FD2), forme directe II (FD2), et forme directe transposée II (FDT2). Pour chacun, il y a une fonction réalisant le filtrage pour une voie, et une autre le réalisant en stéréo pour deux voies.

Les deux premières fonctions réalisent une simple recopie de l'entrée vers la sortie. Elles seront utilisées pour la détermination du gain.

volatile void adc_copie_une_voie() {
     DACC_INTERFACE->DACC_CDR = *(ADC->ADC_CDR+channels[0]);  
}

volatile void adc_copie_deux_voies() {
     
     DACC_INTERFACE->DACC_CDR = *(ADC->ADC_CDR+channels[0]) | (*(ADC->ADC_CDR+channels[1]) << 16) | chsel;  
}
                    

Voici la forme directe de type I pour une voie. Pour une explication détaillée, voir Filtrage numérique sur un microcontrôleur.

volatile void adc_filtrage_float_FD1_une_voie() {
   float accum_0;
   int16_t sortie_0;
   uint8_t i,k;
   x_0[J_tampon] = ((int16_t)*(ADC->ADC_CDR+channels[0]))-offset;
   accum_0 = 0;
   i = J_tampon;
   for (k=0; k<nb; k++) {
     accum_0 += x_0[i]*bbb[k];
     i--;  
   }
   i = J_tampon-1;
   for (k=1; k<na; k++) {
     accum_0 -= y_0[i]*aaa[k];
     i--;  
   }
   sortie_0 = accum_0;
   y_0[J_tampon] = sortie_0;
   J_tampon++;
   sortie_0 = sortie_0*GAIN+offset;
#ifdef SAT
   if (sortie_0<0) sortie_0 = 0;
   else if (sortie_0 > 0xFFF) sortie_0 = 0xFFF;
#endif
   DACC_INTERFACE->DACC_CDR = sortie_0;
}
                   

L'accumulation, c'est-à-dire le calcul de la somme des valeurs du signal multipliées par les coefficients, se fait avec des flottants. Bien sûr, la valeur obtenue est convertie en un entier 12 bits pour être envoyée au convertisseur N/A. On notera la présence d'un offset, en principe égal à 0x7FF, qui doit être retranché avant le filtrage, puis ajouté avant l'écriture dans le registre du DAC. Si la macro SAT est définie, on effectue une saturation de la sortie en cas de débordement. Si l'on est certain que le signal d'entrée est assez faible pour ne pas déborder, on peut enlever la définition de SAT dans l'en-tête. Dans le cas d'un filtrage en flottants, le filtrage lui-même ne peut pas donner de débordements accidentels.

La fonction suivante réalise la même chose pour deux voies :

volatile void adc_filtrage_float_FD1_deux_voies() {
   float accum_0,accum_1;
   int16_t sortie_0, sortie_1;
   uint8_t i,k;
   x_0[J_tampon] = (int16_t)(*(ADC->ADC_CDR+channels[0]))-offset;
   x_1[J_tampon] = (int16_t)(*(ADC->ADC_CDR+channels[1]))-offset;
   accum_0 = 0;
   accum_1 = 0;
   i = J_tampon;
   for (k=0; k<nb; k++) {
     accum_0 += x_0[i]*bbb[k];
     accum_1 += x_1[i]*bbb[k];
     i--;  
   }
   i = J_tampon-1;
   for (k=1; k<na; k++) {
     accum_0 -= y_0[i]*aaa[k];
     accum_1 -= y_1[i]*aaa[k];
     i--;  
   }
   sortie_0 = accum_0;
   sortie_1 = accum_1;
   y_0[J_tampon] = sortie_0;
   y_1[J_tampon] = sortie_1;
   J_tampon++;
   sortie_0 = sortie_0*GAIN+offset;
   sortie_1 = sortie_1*GAIN+offset;
#ifdef SAT
   if (sortie_0<0) sortie_0 = 0;
   else if (sortie_0 > 0xFFF) sortie_0 = 0xFFF;
   if (sortie_1<0) sortie_1 = 0;
   else if (sortie_1 > 0xFFF) sortie_1 = 0xFFF;
#endif
   DACC_INTERFACE->DACC_CDR = (sortie_0) | (((uint32_t)(sortie_1)) << 16) | chsel;;
}
                     

Voici la réalisation directe de type II, qui opère avec un seul tampon circulaire :

volatile void adc_filtrage_float_FD2_une_voie() {
  float accum_0;
  int16_t sortie_0;
  uint8_t i,k;
  accum_0 = ((int16_t)(*(ADC->ADC_CDR+channels[0]))-offset)*aaa[0];
  i = J_tampon-1;
  for (k=1; k<na; k++) {
    accum_0 -= www_0[i]*aaa[k];
    i--;  
  }
  www_0[J_tampon] = accum_0;
  accum_0 = 0.0;
  i = J_tampon;
  for (k=0; k<nb; k++) {
      accum_0 += www_0[i]*bbb[k];
      i--;  
  }
  J_tampon++;
   sortie_0 = accum_0*GAIN+offset;
#ifdef SAT
   if (sortie_0<0) sortie_0 = 0;
   else if (sortie_0 > 0xFFF) sortie_0 = 0xFFF;
#endif
   DACC_INTERFACE->DACC_CDR = sortie_0;
}

volatile void adc_filtrage_float_FD2_deux_voies() {
  float accum_0,accum_1;
  int16_t sortie_0,sortie_1;
  uint8_t i,k;
  accum_0 = ((int16_t)(*(ADC->ADC_CDR+channels[0]))-offset)*aaa[0];
  accum_1 = ((int16_t)(*(ADC->ADC_CDR+channels[1]))-offset)*aaa[0];
  i = J_tampon-1;
  for (k=1; k<na; k++) {
    accum_0 -= www_0[i]*aaa[k];
    accum_1 -= www_1[i]*aaa[k];
    i--;  
  }
  www_0[J_tampon] = accum_0;
  www_1[J_tampon] = accum_1;
  accum_0 = 0.0;
  accum_1 = 0.0;
  i = J_tampon;
  for (k=0; k<nb; k++) {
      accum_0 += www_0[i]*bbb[k];
      accum_1 += www_1[i]*bbb[k];
      i--;  
  }
  J_tampon++;
   sortie_0 = accum_0*GAIN+offset;
   sortie_1 = accum_1*GAIN+offset;
#ifdef SAT
   if (sortie_0<0) sortie_0 = 0;
   else if (sortie_0 > 0xFFF) sortie_0 = 0xFFF;
   if (sortie_1<0) sortie_1 = 0;
   else if (sortie_1 > 0xFFF) sortie_1 = 0xFFF;
#endif
   DACC_INTERFACE->DACC_CDR = (sortie_0) | (((uint32_t)(sortie_1)) << 16) | chsel;;
}
                     

Voici la réalisation directe transposée de type II, qui a l'avantage de ne pas nécessiter de tampon circulaire. Un simple tableau de variables d'état suffit.

volatile void adc_filtrage_float_FDT2_une_voie() {
  int16_t entree_0,sortie_0;
  uint8_t k,i;
  i = 0;
  entree_0 = (int16_t)(*(ADC->ADC_CDR+channels[0]))-offset;
  sortie_0 = bbb[0]*entree_0+www_0[0];
  for (k=1; k<max_na_nb; k++) {
      www_0[i] = bbb[k]*entree_0 - aaa[k]*sortie_0 + www_0[i+1];
      i++; 
  }
   sortie_0 = sortie_0*GAIN+offset;
#ifdef SAT
   if (sortie_0<0) sortie_0 = 0;
   else if (sortie_0 > 0xFFF) sortie_0 = 0xFFF;
#endif
   DACC_INTERFACE->DACC_CDR = sortie_0;
}

volatile void adc_filtrage_float_FDT2_deux_voies() {
  int16_t entree_0,sortie_0,entree_1,sortie_1;
  uint8_t k,i;
  i = 0;
  entree_0 = (int16_t)(*(ADC->ADC_CDR+channels[0]))-offset;
  entree_1 = (int16_t)(*(ADC->ADC_CDR+channels[1]))-offset;
  sortie_0 = bbb[0]*entree_0+www_0[0];
  sortie_1 = bbb[0]*entree_1+www_1[0];
  for (k=1; k<max_na_nb; k++) {
      www_0[i] = bbb[k]*entree_0 - aaa[k]*sortie_0 + www_0[i+1];
      www_1[i] = bbb[k]*entree_1 - aaa[k]*sortie_1 + www_1[i+1];
      i++; 
  }
   sortie_0 = sortie_0*GAIN+offset;
   sortie_1 = sortie_1*GAIN+offset;
#ifdef SAT
   if (sortie_0<0) sortie_0 = 0;
   else if (sortie_0 > 0xFFF) sortie_0 = 0xFFF;
   if (sortie_1<0) sortie_1 = 0;
   else if (sortie_1 > 0xFFF) sortie_1 = 0xFFF;
#endif
   DACC_INTERFACE->DACC_CDR = (sortie_0) | (((uint32_t)(sortie_1)) << 16) | chsel;;
}
                      

3.d. Filtrages en entiers 32 bits

Avec un microcontrôleur 32 bits, la plus grande vitesse de filtrage est réalisée si l'on utilise des entiers 32 bits pour les coefficients du filtre et pour l'accumulateur. Les calculs sont faits avec une représentation en virgule fixe. Pour une explication détaillée, voir Filtrage numérique sur un microcontrôleur.

Les coefficients réels sont multipliés par 2s avant d'être convertis en entiers. À la fin de l'accumulation, la valeur obtenue est ramenée dans l'intervalle 12 bits [-0x7FF,0x7FF] par une division par 2s, c'est-à-dire un décalage de s bits vers la droite. La variable définissant le nombre s est notée ab_shift.

L'inconvénient du filtrage en virgule fixe est la possibilité d'un débordement lors de l'accumulation. Pour l'éviter, il faut prévoir des bits de sécurité. Par exemple, avec 6 bits de sécurité, il reste 32-12-6=14 bits effectifs pour le stockage des coefficients (il y a 12 bits pour le signal lui-même). Il s'en suit des erreurs d'arrondi plus importantes que pour un filtrage en flottants 32 bits.

Voici tout d'abord la forme directe de type I. Pour le filtrage en virgule fixe, cette forme a l'avantage de ne jamais conduire à un débordement dans les tampons circulaires x et y, puisque ces tampons stockent les valeurs d'entrée et de sortie.

volatile void adc_filtrage_32bits_FD1_une_voie() {
   int32_t accum_0;
   int16_t sortie_0;
   uint8_t i,k;
   x_0[J_tampon] = (int16_t)(*(ADC->ADC_CDR+channels[0]))-offset;
   accum_0 = 0;
   i = J_tampon;
   for (k=0; k<nb; k++) {
      accum_0 += x_0[i]*bb[k];
      i--;
   }
   i = J_tampon-1;
   for (k=1; k<na; k++) {
      accum_0 -= y_0[i]*aa[k];
      i--;
   }
   sortie_0 = accum_0 >> ab_shift;
   y_0[J_tampon] = sortie_0;
   J_tampon++;
   sortie_0 = sortie_0*GAIN+offset;
#ifdef SAT
   if (sortie_0<0) sortie_0 = 0;
   else if (sortie_0 > 0xFFF) sortie_0 = 0xFFF;
#endif
   DACC_INTERFACE->DACC_CDR = sortie_0;
}

volatile void adc_filtrage_32bits_FD1_deux_voies() {
   int32_t accum_0, accum_1;
   int16_t sortie_0, sortie_1;
   uint8_t i,k;
   x_0[J_tampon] = (int16_t)(*(ADC->ADC_CDR+channels[0]))-offset;
   x_1[J_tampon] = (int16_t)(*(ADC->ADC_CDR+channels[1]))-offset;
   accum_0 = 0;
   accum_1 = 0;
   i = J_tampon;
   for (k=0; k<nb; k++) {
      accum_0 += x_0[i]*bb[k];
      accum_1 += x_1[i]*bb[k];
      i--;
   }
   i = J_tampon-1;
   for (k=1; k<na; k++) {
      accum_0 -= y_0[i]*aa[k];
      accum_1 -= y_1[i]*aa[k];
      i--;
   }
   sortie_0 = accum_0 >> ab_shift;
   sortie_1 = accum_1 >> ab_shift;
   y_0[J_tampon] = sortie_0;
   y_1[J_tampon] = sortie_1;
   J_tampon++;
   sortie_0 = sortie_0*GAIN+offset;
   sortie_1 = sortie_1*GAIN+offset;
#ifdef SAT
   if (sortie_0<0) sortie_0 = 0;
   else if (sortie_0 > 0xFFF) sortie_0 = 0xFFF;
   if (sortie_1<0) sortie_1 = 0;
   else if (sortie_1 > 0xFFF) sortie_1 = 0xFFF;
#endif
   DACC_INTERFACE->DACC_CDR = (sortie_0) | (((uint32_t)(sortie_1)) << 16) | chsel;;
}
                   

Dans la méthode directe de type II, les valeurs stockées dans le tampon w peuvent dépasser très largement la plage [-0x7FF,0x7FF]. Il faut donc prévoir plus de bits de sécurité. En général, 4 ou 5 bits de sécurité suffisent.

volatile void adc_filtrage_32bits_FD2_une_voie() {
   int32_t accum_0;
   int16_t sortie_0;
    uint8_t i,k;
    accum_0 =  ((int16_t)(*(ADC->ADC_CDR+channels[0]))-offset)*aa[0];
    i = J_tampon-1;
    for (k=1; k<na; k++) {
      accum_0 -= ww_0[i]*aa[k];
      i--; 
    }
    ww_0[J_tampon] = accum_0 >> ab_shift;
    i = J_tampon;
    accum_0 = 0;
    for (k=0; k<nb; k++) {
      accum_0 += ww_0[i]*bb[k];
      i--;  
    }
    J_tampon++;
    sortie_0 = (accum_0 >> ab_shift)*GAIN+offset;
#ifdef SAT
   if (sortie_0<0) sortie_0 = 0;
   else if (sortie_0 > 0xFFF) sortie_0 = 0xFFF;
#endif
   DACC_INTERFACE->DACC_CDR = sortie_0;
}

volatile void adc_filtrage_32bits_FD2_deux_voies() {
   int32_t accum_0,accum_1;
   int16_t sortie_0,sortie_1;
    uint8_t i,k;
    accum_0 =  ((int16_t)(*(ADC->ADC_CDR+channels[0]))-offset)*aa[0];
    accum_1 =  ((int16_t)(*(ADC->ADC_CDR+channels[1]))-offset)*aa[0];
    i = J_tampon-1;
    for (k=1; k<na; k++) {
      accum_0 -= ww_0[i]*aa[k];
      accum_1 -= ww_1[i]*aa[k];
      i--; 
    }
    ww_0[J_tampon] = accum_0 >> ab_shift;
    ww_1[J_tampon] = accum_1 >> ab_shift;
    i = J_tampon;
    accum_0 = 0;
    accum_1 = 0;
    for (k=0; k<nb; k++) {
      accum_0 += ww_0[i]*bb[k];
      accum_1 += ww_1[i]*bb[k];
      i--;  
    }
    J_tampon++;
   sortie_0 = (accum_0 >> ab_shift)*GAIN+offset;
   sortie_1 = (accum_1 >> ab_shift)*GAIN+offset;
#ifdef SAT
   if (sortie_0<0) sortie_0 = 0;
   else if (sortie_0 > 0xFFF) sortie_0 = 0xFFF;
   if (sortie_1<0) sortie_1 = 0;
   else if (sortie_1 > 0xFFF) sortie_1 = 0xFFF;
#endif
   DACC_INTERFACE->DACC_CDR = (sortie_0) | (((uint32_t)(sortie_1)) << 16) | chsel;;
}
                   

Voici pour finir la réalisation directe transposée de type II :

volatile void adc_filtrage_32bits_FDT2_une_voie() {
   int32_t entree_0, sortie_0;
   uint8_t k,i;
   i = 0;
   entree_0 = (int16_t)(*(ADC->ADC_CDR+channels[0]))-offset;
   sortie_0 = (bb[0]*entree_0+ww_0[0]) >> ab_shift;
   for (k=1; k<max_na_nb; k++) {
       ww_0[i] = entree_0*bb[k] - sortie_0*aa[k] + ww_0[i+1];
       i++;
   }
   sortie_0 = sortie_0*GAIN+offset;
#ifdef SAT
   if (sortie_0<0) sortie_0 = 0;
   else if (sortie_0 > 0xFFF) sortie_0 = 0xFFF;
#endif
   DACC_INTERFACE->DACC_CDR = sortie_0;
}

volatile void adc_filtrage_32bits_FDT2_deux_voies() {
   int32_t entree_0, sortie_0, entree_1, sortie_1;
   uint8_t k,i;
   i = 0;
   entree_0 = (int16_t)(*(ADC->ADC_CDR+channels[0]))-offset;
   entree_1 = (int16_t)(*(ADC->ADC_CDR+channels[1]))-offset;
   sortie_0 = (bb[0]*entree_0+ww_0[0]) >> ab_shift;
   sortie_1 = (bb[0]*entree_1+ww_1[0]) >> ab_shift;
   for (k=1; k<max_na_nb; k++) {
       ww_0[i] = entree_0*bb[k] - sortie_0*aa[k] + ww_0[i+1];
       ww_1[i] = entree_1*bb[k] - sortie_1*aa[k] + ww_1[i+1];
       i++;
   }
   sortie_0 = sortie_0*GAIN+offset;
   sortie_1 = sortie_1*GAIN+offset;
#ifdef SAT
   if (sortie_0<0) sortie_0 = 0;
   else if (sortie_0 > 0xFFF) sortie_0 = 0xFFF;
   if (sortie_1<0) sortie_1 = 0;
   else if (sortie_1 > 0xFFF) sortie_1 = 0xFFF;
#endif
   DACC_INTERFACE->DACC_CDR = (sortie_0) | (((uint32_t)(sortie_1)) << 16) | chsel;;
}
                   

La fonction suivante initialise les tampons utilisés pour le filtrage :

void init_tampons() {
   int i;
   J_tampon = 0;
   for (i=0; i<TAILLE_TAMPON; i++) {
     x_0[i] = 0;
     y_0[i] = 0;
     x_1[i] = 0;
     y_1[i] = 0; 
     www_0[i] = 0.0;
     www_1[i] = 0.0;
     ww_0[i] = 0;
     ww_1[i] = 0;
   } 
}
                   

3.e. Communication avec l'ordinateur

Ces fonctions permettent à un programme Python (présenté plus loin) de fournir les coefficients du filtre et les différents paramètres de la numérisation.

La première fonction lit les informations de configuration de l'ADC. Les valeurs lues sont :

  • Le nombre de voies (8 bits).
  • Les numéros des bornes des voies utilisées (8 bits chacun), par exemple 0 et 1 pour les bornes A0 et A1.
  • Les gains des voies (8 bits chacun) égaux à 1,2 ou 4.
  • Le nombre de tops d'horloge à 42 MHz définissant la période d'échantillonnage (32 bits).

Il est possible de définir plus de deux voies en entrée, mais seules les deux premières sont filtrées. Les autres pourront servir à lire des informations de la part de capteurs, par exemple un potentiomètre pour modifier un paramètre d'un filtre.

void lecture_config_adc() {
   uint32_t c1,c2,c3,c4;
   int k;
   while (Serial.available()<1) {};
    nchannels = Serial.read();
    while (Serial.available()<nchannels) {};
    for (k=0; k<nchannels; k++) {
      if (k < MAX_NCHAN) channels[k] = channels_pins[Serial.read()]; 
    }
    while (Serial.available()<nchannels) {};
    for (k=0; k<nchannels; k++) {
      if (k < MAX_NCHAN) gain[k] = Serial.read(); 
    }
    if (nchannels > MAX_NCHAN) nchannels = MAX_NCHAN;
    while (Serial.available()<4) {};
    c1 = Serial.read();
    c2 = Serial.read();
    c3 = Serial.read();
    c4 = Serial.read();
    ticks = ((c1 << 24) | (c2 << 16) | (c3 << 8) | c4);
}
                  

La fonction suivante effectue la configuration d'un filtrage à virgule flottante. Elle appelle tout d'abord la fonction précédente, puis lit les informations suivantes :

  • Nombre de coefficients ak (8 bits).
  • Valeurs des coefficients ak, sous forme d'une mantisse 32 bits et d'un exposant 8 bits.
  • Nombre de coefficients bk (8 bits).
  • Valeurs des coefficients ak.
  • Valeur de l'offset (16 bits).
  • Type de réalisation, 1 pour FD1, 2 pour FD2, 3 pour FDT2. Une autre valeur conduit à une recopie de l'entrée vers la sortie.

Après la lecture de ces informations, la fonction d'interruption est choisie, les convertisseurs sont configurés, puis le Timer est déclenché.

void lecture_filtrage_float() {
  uint32_t c1,c2,c3,c4;
  int k;
  int32_t m;
  int8_t e;
  uint8_t type;
  lecture_config_adc();
    
    // filtrage
   while (Serial.available()<1) {};
   na = Serial.read();
   for (int k=0; k<na; k++) {
       while (Serial.available()<5) {};
       c1 = Serial.read();
       c2 = Serial.read();
       c3 = Serial.read();
       c4 = Serial.read();
       e = Serial.read();
       m = ((c1 << 24) | (c2 << 16) | (c3 << 8) | c4);
       aaa[k] = ((float)m)*pow(2,e-30);
   }
   while (Serial.available()<1) {};
   nb = Serial.read();
   for (int k=0; k<nb; k++) {
       while (Serial.available()<5) {};
       c1 = Serial.read();
       c2 = Serial.read();
       c3 = Serial.read();
       c4 = Serial.read();
       e = Serial.read();
       m = ((c1 << 24) | (c2 << 16) | (c3 << 8) | c4);
       bbb[k] = ((float)m)*pow(2,e-30);
   }
   while (Serial.available()<2) {};
   c1 = Serial.read();
   c2 = Serial.read();
   offset = ((c1 << 8) | c2);
   while (Serial.available()<1) {};
   type = Serial.read();
 
   init_tampons();
   max_na_nb = max(na,nb);
   for (k=na; k<max_na_nb; k++) aaa[k] = 0.0;
   for (k=nb; k<max_na_nb; k++) bbb[k] = 0.0;
   if (nchannels==1) {
     config_dac(0);
     if (type==1) ADC_Handler_function = adc_filtrage_float_FD1_une_voie;
     else if (type==2) ADC_Handler_function = adc_filtrage_float_FD2_une_voie;
     else if (type==3) ADC_Handler_function = adc_filtrage_float_FDT2_une_voie;
     else ADC_Handler_function = adc_copie_une_voie;
   }
   else if (nchannels==2) {
     config_double_dac();
     if (type==1) ADC_Handler_function = adc_filtrage_float_FD1_deux_voies;
     else if (type==2) ADC_Handler_function = adc_filtrage_float_FD2_deux_voies;
     else if (type==3) ADC_Handler_function = adc_filtrage_float_FDT2_deux_voies;
     else ADC_Handler_function = adc_copie_deux_voies;
   }
   config_adc();
   config_timer(ticks);
}
                   

La configuration du filtrage en virgule fixe (entiers 32 bits) est similaire à la précédente, mais les coefficients du filtre sont des entiers 32 bits. Il y en plus la lecture de ab_shift, un entier 8 bits.

void lecture_filtrage_32bits() {
  uint32_t c1,c2,c3,c4;
  int k;
  uint8_t type;
  lecture_config_adc();
  while (Serial.available()<1) {};
   na = Serial.read();
   for (int k=0; k<na; k++) {
       while (Serial.available()<4) {};
       c1 = Serial.read();
       c2 = Serial.read();
       c3 = Serial.read();
       c4 = Serial.read();
       aa[k] = ((c1 << 24) | (c2 << 16) | (c3 << 8) | c4);
   }
   while (Serial.available()<1) {};
   nb = Serial.read();
   for (int k=0; k<nb; k++) {
       while (Serial.available()<4) {};
       c1 = Serial.read();
       c2 = Serial.read();
       c3 = Serial.read();
       c4 = Serial.read();
       bb[k] = ((c1 << 24) | (c2 << 16) | (c3 << 8) | c4);
   }
   while (Serial.available()<1) {};
   ab_shift = Serial.read();
   while (Serial.available()<2) {};
   c1 = Serial.read();
   c2 = Serial.read();
   offset = ((c1 << 8) | c2);
   while (Serial.available()<1) {};
   type = Serial.read();
   init_tampons();
   max_na_nb = max(na,nb);
   for (k=na; k<max_na_nb; k++) aa[k] = 0.0;
   for (k=nb; k<max_na_nb; k++) bb[k] = 0.0;
   if (nchannels==1) {
     config_dac(0);
     if (type==1) ADC_Handler_function = adc_filtrage_32bits_FD1_une_voie;
     else if (type==2) ADC_Handler_function = adc_filtrage_32bits_FD2_une_voie;
     else if (type==3) ADC_Handler_function = adc_filtrage_32bits_FDT2_une_voie;
     else ADC_Handler_function = adc_copie_une_voie;
   }
   else if (nchannels==2) {
     config_double_dac();
     if (type==1) ADC_Handler_function = adc_filtrage_32bits_FD1_deux_voies;
     else if (type==2) ADC_Handler_function = adc_filtrage_32bits_FD2_deux_voies;
     else if (type==3) ADC_Handler_function = adc_filtrage_32bits_FDT2_deux_voies;
     else ADC_Handler_function = adc_copie_deux_voies;
   }
   config_adc();
   config_timer(ticks);
}
                    

Voici la fonction d'initialisation :

void setup() {
  Serial.begin(115200);
  pinMode(3,OUTPUT);
}
                    

La fonction suivante effectue la lecture du port série. Si un caractère de commande est lu, la fonction de lecture correspondante est appelée. Si la commande IS_READY est reçue, le Timer est stoppé et un caractère est renvoyé. Cette commande devra être exécutée avant de configurer le filtre, pour vérifier que l'Arduino n'est pas bloqué (à cause d'une cadence d'interruption trop élevée).

void lecture_serie() {
  char com;
    if (Serial.available()>0) {
        com = Serial.read();
        if (com==SET_FILTRAGE_FLOAT) lecture_filtrage_float();
        else if (com==SET_FILTRAGE_32BITS) lecture_filtrage_32bits();
        else if (com==IS_READY) {
          stop_timer();
          Serial.write("O");  
        }
    }
}
                     

La fonction principale effectue une lecture du port série toute les demi secondes :

void loop() {
  lecture_serie();
  delay(500);
}
                     

La fonction lecture_serie étant non bloquante, on pourra ajouter ici d'autres opérations d'interaction avec l'extérieur. Si des entrées analogiques autres que celles des deux signaux à filtrer sont utilisées, il est préférable de numériser toutes les voies simultanément. Par exemple, si l'on a besoin d'une troisième voie pour lire la valeur d'un capteur, on stockera sa valeur dans une variable globale uint16_t v3. Il suffit d'ajouter la ligne suivante dans la fonction de traitement :

v3 = *(ADC->ADC_CDR+channels[3]);
                         

La variable uint16_t v3 pourra être lue dans la fonction loop afin de déclencher différentes actions.

4. Programme Python

Ce programme (pour Python 3.x) est constitué d'une classe qui comporte des fonctions permettant de configurer le filtrage. La carte Arduino Due est reliée à l'ordinateur par le port USB de programmation (la prise micro-USB la plus proche du jack d'alimentation).

ArduinoDueFiltrageNumerique.py
import serial
import numpy
import math
import time
from matplotlib.pyplot import *
import scipy.signal

class Arduino():
    def __init__(self,port):
        self.ser = serial.Serial(port,baudrate=115200)
        time.sleep(1)
        self.SET_FILTRAGE_FLOAT = 104
        self.SET_FILTRAGE_32BITS = 103
        self.IS_READY = 100
        self.MAX_NCOEF = 256
        self.clockFreq = 42.0e6

    def close(self):
        self.ser.close()

    def write_int8(self,v):
        self.ser.write((v&0xFF).to_bytes(1,byteorder='big'))

    def write_int16(self,v):
        v = numpy.int16(v)
        char1 = int((v & 0xFF00) >> 8)
        char2 = int((v & 0x00FF))
        self.ser.write((char1).to_bytes(1,byteorder='big'))
        self.ser.write((char2).to_bytes(1,byteorder='big'))
        
    def write_int32(self,v):
        v = numpy.int32(v)
        char1 = int((v & 0xFF000000) >> 24)
        char2 = int((v & 0x00FF0000) >> 16)
        char3 = int((v & 0x0000FF00) >> 8)
        char4 = int((v & 0x000000FF))
        self.ser.write((char1).to_bytes(1,byteorder='big'))
        self.ser.write((char2).to_bytes(1,byteorder='big'))
        self.ser.write((char3).to_bytes(1,byteorder='big'))
        self.ser.write((char4).to_bytes(1,byteorder='big'))

    def write_float(self,v):
        if v!=0.0:
            e = math.floor(math.log(abs(v)/math.log(2)))
        else:
            e = 0
        m = numpy.int32(v*2**(30-e))
        self.write_int32(m)
        self.write_int8(e)
        

La fonction suivante de la classe Arduino configure et déclenche le filtrage avec des flottants. Si le temps d'exécution de la fonction de filtrage est supérieur à la période d'échantillonnage, celui-ci ne se fait plus régulièrement (cela est visible sur le signal TTL de la sortie 3). De plus, il ne répond plus à la commande IS_READY. Dans ce cas, la fonction stoppe et demande d'initialiser l'Arduino en appuyant sur son bouton RESET.

    def lancer_filtrage_float(self,voies,gains,fechant,a,b,offset,typ):
        a = numpy.array(a)
        b = numpy.array(b)
        na = len(a)
        nb = len(b)
        if na > self.MAX_NCOEF or nb > self.MAX_NCOEF:
            raise Exception("trop de coefficients de filtrage")
        print(b)
        print(a)
        (zeros,poles,gain) = scipy.signal.tf2zpk(b,a)
        for p in poles:
            if numpy.absolute(p) >= 1:
                print("Filtre instable")
        ticks = int(self.clockFreq/fechant)
        print("F echant = %d"%(self.clockFreq/ticks))
        self.write_int8(self.IS_READY)
        r = self.ser.read(1)
        if r!='O':
            print(u"Arduino bloqué : appuyez sur RESET")
            return 1
        self.write_int8(self.SET_FILTRAGE_FLOAT)
        nv = len(voies)
        self.write_int8(nv)
        for k in range(nv):
            self.write_int8(voies[k])
        for k in range(nv):
            self.write_int8(gains[k])
        self.write_int32(ticks)
        self.write_int8(na)
        for k in range(na):
            self.write_int8(a[k])
        self.write_int8(nb)
        for k in range(nb):
            self.write_float(b[k])
        self.write_int16(offset)
        self.write_int8(typ)
        return 0
        

La fonction suivante configure le filtrage avec des entiers 32 bits. L'argument gbits est le nombre de bits de sécurité. Le nombre de décalage de bits s est calculé en conséquence, et les coefficients du filtre sont multipliés par 2s puis convertis en entiers avant d'être transmis à l'arduino.

    def lancer_filtrage_32bits(self,voies,gains,fechant,a,b,offset,typ,gbits):
        a = numpy.array(a)
        b = numpy.array(b)
        na = len(a)
        nb = len(b)
        if na > self.MAX_NCOEF or nb > self.MAX_NCOEF:
            raise Exception("trop de coefficients de filtrage")
        mb = numpy.max(numpy.absolute(b))
        ma = numpy.max(numpy.absolute(a))
        m = max(ma,mb)
        P = 32+1-12-gbits # nombre de bits des coefficients
        s = numpy.floor(P-1-numpy.log(m)/numpy.log(2))
        a_int32 = numpy.array(a*2**s,dtype=numpy.int32)
        b_int32 = numpy.array(b*2**s,dtype=numpy.int32)
        (zeros,poles,gain) = scipy.signal.tf2zpk(b_int32,a_int32)
        for p in poles:
            if numpy.absolute(p) >= 1:
                print("Filtre instable")
        print(b_int32)
        print(a_int32)
        ticks = int(self.clockFreq/fechant)
        print("F echant = %d"%(self.clockFreq/ticks))
        self.write_int8(self.IS_READY)
        r = self.ser.read(1)
        if r!='O':
            print(u"Arduino bloqué : appuyez sur RESET")
            return 1
        self.write_int8(self.SET_FILTRAGE_32BITS)
        nv = len(voies)
        self.write_int8(nv)
        for k in range(nv):
            self.write_int8(voies[k])
        for k in range(nv):
            self.write_int8(gains[k])
        self.write_int32(ticks)
        self.write_int8(na)
        for k in range(na):
            self.write_int32(a_int32[k])
        self.write_int8(nb)
        for k in range(nb):
            self.write_int32(b_int32[k])
        self.write_int8(int(s))
        self.write_int16(offset)
        self.write_int8(typ)
        return 0
            

Deux fonctions de test sont ajoutées au fichier. La première effectue la configuration d'un filtre RIF passe-bas à 20 coefficients. La fréquence d'échantillonnage est 21kHz. La réponse fréquentielle du filtre est tracée. On peut vérifier son fonctionnement avec un générateur de fonction (GBF) et un oscilloscope.

def test_rif():
    ard = Arduino("COM17") 
    fechant = 21000.0
    voies = [0]
    gains = [1]
    fc = 1500.0
    a = [1.0]
    b = scipy.signal.firwin(numtaps=20,cutoff=[fc/fechant],window='hann',nyq=0.5)
    #b,a = scipy.signal.iirfilter(N=4,Wn=[fc/fechant*2],btype="lowpass",ftype="butter")
    w,h = scipy.signal.freqz(b,a)
    figure()
    subplot(211)
    plot(w/(2*numpy.pi)*fechant,numpy.absolute(h))
    xlabel("f")
    ylabel("GdB")
    grid()
    subplot(212)      
    plot(w/(2*numpy.pi)*fechant,numpy.unwrap(numpy.angle(h)))
    xlabel("f")
    ylabel("phase")
    grid()
    offset = 0x7FF
    typ = 1
    gbits = 6
    ard.lancer_filtrage_32bits(voies,gains,fechant,a,b,offset,typ,gbits)
    show(block=True)
           

La deuxième effectue la configuration d'un filtre récursif passe-bas d'ordre 2, appelé aussi filtre bi-quadratique, très utilisé en filtrage audio. Le filtrage est réalisé en flottants.

def test_biquad():
    ard = Arduino("COM17") 
    fechant = 21000.0
    voies = [0]
    gains = [1]
    fc = 1500.0
    b,a = scipy.signal.iirfilter(N=2,Wn=[fc/fechant*2],btype="lowpass",ftype="butter")
    w,h = scipy.signal.freqz(b,a)
    figure()
    subplot(211)
    plot(w/(2*numpy.pi)*fechant,numpy.absolute(h))
    xlabel("f")
    ylabel("GdB")
    grid()
    subplot(212)      
    plot(w/(2*numpy.pi)*fechant,numpy.unwrap(numpy.angle(h)))
    xlabel("f")
    ylabel("phase")
    grid()
    offset = 0x7FF
    typ = 1
    ard.lancer_filtrage_float(voies,gains,fechant,a,b,offset,typ)
    show(block=True)
           

La troisième effectue la configuration d'un filtre intégrateur de type passe-bande, avec une coupure de la fréquence nulle pour éviter la dérive. Sur ce type de filtre ayant un ou plusieurs pôles dont le module est très proche de 1, le comportement des différentes formes de filtrage peut être différent. Il faut faire des tests pour déterminer la meilleure.

def test_integrateur():
    ard = Arduino("COM17") 
    fechant = 21000.0
    voies = [0]
    gains = [1]
    fc = 1500.0
    r1=0.98
    r2=0.98
    g = 0.03
    b=[g,0,-g]
    a=[1,-(r1+r2),r1*r2]
    w,h = scipy.signal.freqz(b,a)
    figure()
    subplot(211)
    plot(w/(2*numpy.pi)*fechant,numpy.absolute(h))
    xlabel("f")
    ylabel("GdB")
    grid()
    subplot(212)      
    plot(w/(2*numpy.pi)*fechant,numpy.unwrap(numpy.angle(h)))
    xlabel("f")
    ylabel("phase")
    grid()
    offset = 0x7FF
    typ = 2
    ard.lancer_filtrage_float(voies,gains,fechant,a,b,offset,typ)
    show(block=True)
           

Pour tester le filtre, il faut vérifier qu'il se comporte bien en régime sinusoïdal permament, ou avec d'autres formes de signaux. Il faut aussi vérifier qu'il reste stable en présence de transitoires (changement brusque d'amplitude ou de fréquence).

5. Programme PythonQT

Le programme suivant tourne en python 3.x avec PyQT5. Il offre une interface graphique permettant de choisir les paramètres du filtre et trace sa réponse fréquentielle.

Il est constitué de trois fichiers, à placer dans le même répertoire que le fichier ArduinoDueFiltrageNumerique.py.

Le script filtreRIF.py ouvre une fenêtre pour configurer un filtre à réponse impulsionnelle finie. Pour transmettre les paramètres à l'Arduino, il faut d'abord choisir le bon port COM. Si l'Arduino ne répond pas (une fenêtre d'alerte s'ouvre), il faut appuyer sur le bouton RESET de la carte, puis appuyer à nouveau sur le bouton Transmettre.

fenetre

Le script filtreRII.py ouvre une fenêtre pour configurer un filtre à réponse impulsionnelle infinie, c'est-à-dire un filtre récursif calculé par transformation bilinéaire d'un filtre de type Butterworth, Chebychev, etc.

fenetre
Creative Commons LicenseTextes et figures sont mis à disposition sous contrat Creative Commons.