Tutoriel: réaliser un générateur de fréquences avec l’ESP32 dans l’environnement IDE Arduino.

Traducteur
EnglishFrenchGermanItalianSpanish

Une réalisation de Christophe Masclet, dans l’esprit Philroom

Christophe a la gentillesse de nous éclairer sur les capacités à pouvoir utiliser l’ESP32 pour des applications de traitement du signal et de synthèse audio. J’apprécie beaucoup son geste de partage à l’issue des échanges que nous avons eus lors de la mise en oeuvre de son projet. Ils m’ont vite éclairés sur la motivation et la rigueur technique de ce tout nouveau membre de www.philoc.fr.

Mises à jour

2020-04-27  Intégration de la contribution.

L’objet du tutoriel : les générateurs de fréquences et le traitement du signal

Les générateurs de fréquence/signaux sont des outils utiles pour le développement ou la réparation des circuits électroniques analogiques.
Leur finalité est simple: produire une forme d’onde, généralement sinusoïdale, à la fréquence voulue.

Ce tutoriel n’a pas pour finalité de réaliser un générateur très élaboré, cette démarche est surtout un prétexte pour présenter la mise en oeuvre du protocole I2S dans l’ESP32.

A noter que le sujet I2S est mal documenté et l’exemple présenté ici a été délicat à développer. Certaines parties du code ont été traduites depuis des programmes non compatibles avec l’IDE Arduino, d’autres ont été déduites de façon empirique, souvent à l’aide d’un oscilloscope et d’un analyseur de spectre

Cahier des charges

Générateur de fréquence 2 canaux:

– sortie A : onde sinusoïdale
– sortie B : onde carré

– Fréquence admissible: entre 20Hz et 20Khz.
– Audio: Encodage 16 bits / 44,1Khz

Réalisation

Avant-propos: L’ESP32 et l’audio

Les DACs internes à l’ESP32 effectuent un décodage en 8 bits, ce qui est équivalent à la qualité audio d’une Game Boy… Pour produire un signal de qualité on n’a pas d’autre choix que de se tourner vers des DACs externes, ceux-ci peuvent communiquer avec l’ESP32 selon deux protocoles: SPI ou I2S.

L’intérêt du protocole I2S est d’être destiné au transfert des flux audionumériques. Avec l’utilisation de la bibliothèque I2S.h l’ESP32 est capable gérer ces opérations de façon transparente.

L’utilisation de bibliothèque I2S.h intègre de fait le DMA qui présente bien des avantages: le Direct Memory Access est la fonction qui permet au flux audionumérique d’être lu depuis une mémoire dédiée sans passer par le micro-contrôleur. En pratique cela permet de libérer du temps de traitement.

Le matériel nécessaire

  • une carte de développement ESP32 ( Dev Kit C ) de Espressif. ( Prix 9,11 € HT chez Digikey)
  •  un DAC I2S UDA1334 de Adafruit ( Prix 6,30 € HT chez Digikey ).

Les branchements

L’I2S transite par 4 signaux :

  • bck: est l’horloge
  • ws: indique au DAC quand les données concernent le canal droit ou gauche
  • data_out: données sortantes
  • data_in: données entrantes.

Sur le convertisseur UDA1334, ces pins sont respectivement nommées: BCLK, WSEL et DIN.


Ci-dessus les branchements nécessaires pour relier un UDA1334 et un ESP32. Le DAC dispose d’une sortie stéreo sur mini-jack 2,5mm. Le signal stéréo est aussi accessible sur les pins Lout et Rout.

Le niveau “ Full Scale ” ( c’est à dire la modulation de tension maximale ) en sortie du DAC est de 2,9V.

Les données

Par définition les calculs trigonométriques sont relativement longs à effectuer en numérique et il n’est pas efficient de produire une sinusoïde en temps réel. Au contraire nous allons utiliser un échantillon “suréchantilloné” de sinusoïde.

Le programme suivant est fait pour calculer notre forme d’onde sinusoïdale. Après processus les données sont accessibles depuis la fenêtre Serial.

A noter que nous générons des valeurs “unsigned” qui sont conformes au protocole I2S.

// ************************************************************************************

#define SAMPLE_LENGH 8820
#define BIT_DEPTH_MULTIPLIER 32767 // ( 2 ^16bits ) / 2 ) - 1 = 32767

int to_Line;

void setup() {

  Serial.begin(115200);

  for (int i = 0; i < SAMPLE_LENGH; i++) {

    to_Line = to_Line + 1;

    if ( to_Line > 7 ) {
      to_Line = 0;
    }

    int32_t data_To_Print =  sin ( i * 2 * PI / SAMPLE_LENGH ) * BIT_DEPTH_MULTIPLIER;
    Serial.print(  data_To_Print );

    if ( i < SAMPLE_LENGH - 1 ) {

      if (to_Line != 7 ) {       // 8 datas for each line
        Serial.print(", ");
      } else {
        Serial.println(", ");
      }
    }
  }
}

void loop() {

  
}

// ************************************************************************************

Après exécution, dans la fenêtre Serial on peut lire ça:

On sélectionne et copie ces datas, ce sont les échantillons qui seront disponibles dans la bibliothèque waveform.h.

La bibliothèque waveform.h se présente au final sous la forme d’un tableau:

#define WAVE_SIZE 8820

int16_t wave_Sine[ WAVE_SIZE ]  = {  // Sine 16 bits encoded / 8820 samples

0, 23, 46, 70, 93, 116, 140, 
163, 186, 210, 233, 256, 280, 303, 326,
….
….
….
-490, -466, -443, -420, -396, -373, -350, -326, 
-303, -280, -256, -233, -210, -186, -163, -140, 
-116, -93, -70, -46, -23
};



La programmation

Dans notre programme nous utilisons la bibliothèque I2S.h qui est disponible à cette adresse:

https://github.com/espressif/arduino-esp32/blob/master/tools/sdk/include/driver/driver/i2s.h

Cette bibliothèque bien que répandue est surtout utilisée pour extraire les flux audio depuis les cartes SD. On trouve peu d’exemple qui permettent d’avoir la main sur le traitement de l’audio par le micro-contrôleur. La configuration du protocole se passe ainsi:

/******************************** i2s configuration ********************************/

#include "driver/i2s.h"

#define SAMPLE_RATE  44100        // Sample rate ( Hertz )
#define I2S_PORT 0                          // i2s port number

i2s_config_t i2s_config = {
  .mode = ( i2s_mode_t )( I2S_MODE_MASTER | I2S_MODE_TX ),
  .sample_rate = SAMPLE_RATE,
  .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
  .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
  .communication_format = ( i2s_comm_format_t )( I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB ),
  .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
  .dma_buf_count = 16,
  .dma_buf_len = 64,
};

L’expression .mode = ( i2s_mode_t )(I2S_MODE_MASTER | I2S_MODE_TX ) désigne le mode utilisé. Ici l’ESP32 est maître et transmet les données.

L’expression .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT désigne le nombre de bit par échantillon.

L’expression .channel_format = I2S_CHANNEL_FMT_LEFT_RIGHT désigne le format mono/stéréo. On peut utiliser l’expression I2S_CHANNEL_ONLY_RIGHT pour un signal mono.

L’expression .communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB) désigne le mode de transfert MSB/LSB.

L’expression .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1 désigne le haut niveau de priorité de l’interruption I2S.

L’expression .dma_buf_count = 16 désigne la granularité du flux.

L’expression .dma_buf_len = 64 désigne la taille du buffer

Puis on déclare les pins:

i2s_pin_config_t pin_config = {

  .bck_io_num = 26,       //  Pin 26 to UDA1334 BCLK
  .ws_io_num = 25,        //  Pin 25 to UDA1334 WSEL
  .data_out_num = 22,   //  Pin 22 to UDA1334 DIN
  .data_in_num = -1       //  Not used

};

Dans le void setup on démarre l’I2S de la façon suivante:

/****************************** void setup ******************************/

void setup() {

  i2s_driver_install ( ( i2s_port_t ) I2S_PORT, &i2s_config, 0, NULL) ;           // Begin Config
  i2s_set_pin ( (i2s_port_t ) I2S_PORT, &pin_config );                                   // Pin Config
  i2s_set_sample_rates ( ( i2s_port_t ) I2S_PORT, SAMPLE_RATE );          // Sample Rate Config

}

Voila, notre I2S est correctement configuré. Nous allons maintenant déclarer un tableau que nous appellerons audio[ ].
Ce tableau est l’endroit ou nous allons régulièrement déposer les échantillons, c’est aussi ici que l’I2S puise les datas avant de les transférer.
A noter que l’encodage doit suivre celui déjà configuré pour l’I2S: du 16 bits signé.

int16_t audio[ 256 ];

Nous allons aussi déclarer un système de variables qui permettront de lire l’échantillon à la fréquence voulue:

#define GENERATOR_FREQUENCY 1000  // Frequency ( Hertz )

uint32_t wave_Pointer;
uint16_t phase_Increment;

La variable phase_Increment indique à notre tête de lecture, le wave_Pointer, de combien de pas elle doit avancer entre chaque échantillon.
Elle est calculée en fonction de la fréquence désirée:

phase_Increment =  WAVE_SIZE / ( SAMPLE_RATE / GENERATOR_FREQUENCY );

Par la suite il suffira de changer la valeur du #define GENERATOR_FREQUENCY pour jouer la fréquence de son choix.

Lors du transfert audio, l’I2S envoie alternativement les échantillons du canal gauche et droit. Pour cela il puise directement dans le tableau audio(), voyons comment remplir ce tableau:

/****************************** void loop ******************************/

void loop() {

for ( int i = 0; i < 128  ; i++) {                                     // Write to audio array. 128 * 2 datas

    wave_Pointer = wave_Pointer + phase_Increment;

    if (wave_Pointer >= WAVE_SIZE  ) {
wave_Pointer = wave_Pointer - ( WAVE_SIZE );
}

audio [ i * 2 ] = wave_Sine[ wave_Pointer ];           // Write Sine Wave ( 2 bytes, Left Channel )

if ( wave_Pointer >= WAVE_SIZE >> 1 ) {               // Write Square Wave ( 2 bytes, Right Channel )

audio [(i * 2 ) + 1] = 32767;                                   // Low state
} else {
audio [(i * 2 ) + 1] = - 32767;                                // High state
}

}
...

La première partie de la loop concerne le placement la tête de lecture wave_Pointer, on s’assure que celle-ci ne dépasse jamais la longueur de la sinusoïde.

Ensuite on lit l’échantillon et le stocke dans une case du tableau audio(). Cette valeur concerne notre signal sinusoïdal / canal gauche
En fonction de l’emplacement de la tête de lecture on va créer une valeur basse ou haute qu’on stocke dans la case suivante du tableau. Cette valeur concerne notre signal carré / canal droit

Ces deux opérations sont répétées 128 fois pour remplir les 256 cases du tableau audio().

Enfin il suffit d’indiquer à l’I2S le nom du tableau audio() à lire ainsi que sa taille ( soit 256 x 2 bytes = 512 ):

…

i2s_write_bytes( (i2s_port_t)I2S_PORT, audio , 512,  portMAX_DELAY);

}

Résultat

Voici les signaux produits avec notre exemple “Frequency_Generator. ino” :

Canal droit:

Canal gauche:

Conclusion

Certes, notre générateur est moins beau que celui présenté comme image d’entête d’article mais il fonctionne. Après ces explications, la lecture de l’exemple “Frequency_Generator. info” devrait surtout vous paraître limpide. l’ESP32 est un micro-contrôleur puissant et l’I2S apporte définitivement des facilités non négligeables, le protocole est même tellement efficace qu’il est utilisé dans d’autres contextes comme la video… mais c’est une autre histoire.