Dans ce tutoriel (si vous pouvez l'appeler ainsi), je vais vous montrer comment organiser rapidement et tout simplement la lecture d'un fichier audio à l'aide du microcontrôleur ESP32.
Un peu de théorie
Comme Wikipédia nous le dit, l'ESP32 est une série de microcontrôleurs à faible consommation et à faible consommation d'énergie. Il s'agit d'un système sur puce (SoC) avec contrôleurs et antennes Wi-Fi et Bluetooth intégrés. Basé sur le noyau Tensilica Xtensa LX6 en versions simple et double cœur. Un chemin de radiofréquence est intégré au système. MK a été créé et développé par la société chinoise Espressif Systems, et est fabriqué par TSMC selon la technologie de processus 40 nm. Vous pouvez en savoir plus sur les capacités de la puce sur la page Wikipédia et dans la documentation officielle.
Une fois, dans le cadre de la maîtrise de ce contrôleur, j'ai voulu jouer un son dessus. Au début, je pensais que je devrais utiliser PWM. Cependant, après avoir lu la documentation de plus près, j'ai découvert la présence de deux canaux d'un DAC 8 bits. Bien sûr, cela a fondamentalement changé la donne.
La référence technique indique que le DAC de l'ESP32 est construit sur une chaîne de résistances (apparemment, cela signifie la chaîne R2R) utilisant un certain tampon. La tension de sortie peut varier de 0 volts à la tension d'alimentation (3,3 volts) avec une résolution de 8 bits (soit 256 valeurs). La conversion des deux canaux est indépendante. Il existe également un générateur CW intégré et un support DMA.
J'ai décidé de ne pas me lancer dans le DMA pour le moment, me limitant à construire un lecteur basé sur une minuterie. Comme vous le savez, pour reproduire le fichier WAV le plus simple du format PCM, il suffit d'en lire les données brutes à la fréquence d'échantillonnage spécifiée dans le fichier et de les pousser à travers les canaux DAC, en réduisant au préalable (si nécessaire) le bitness des données au bitness du DAC. J'ai eu de la chance: j'ai trouvé un ensemble de sons au format mono WAV PCM 8 bits 11025 Hz, extraits des ressources d'un ancien jeu. Cela signifie que nous n'utiliserons qu'un seul canal DAC.
Nous aurons également besoin d'une minuterie capable de générer des interruptions à 11025 Hz. Selon la même référence technique, l'ESP32 a à bord deux modules de minuterie avec deux minuteries chacun, pour un total de quatre minuteries. Ils ont une largeur de 64 bits, chacun avec un prescaler 16 bits et la possibilité de générer une interruption sur un niveau ou un front.
De la théorie à la pratique
Armé de l'exemple wave_gen de esp-idf, je me suis mis à écrire le code. Je n'ai pas pris la peine de créer un système de fichiers: le but était d'obtenir du son, et non de faire un lecteur à part entière avec ESP32.
Pour commencer, j'ai dépassé l'un des fichiers WAV dans le tableau sish. L'utilitaire xxd intégré à Debian m'a beaucoup aidé. Commande simple
$ xxd -i file.wav > file.c
nous obtenons un fichier sish avec un tableau de données sous forme hexadécimale à l'intérieur et même avec une variable séparée qui contient la taille du fichier en octets.
Ensuite, j'ai commenté les 44 premiers octets du tableau - l'en-tête du fichier WAV. En cours de route, je l'ai démonté par champs et j'ai trouvé toutes les informations dont j'avais besoin à ce sujet:
const uint8_t sound_wav[] = {
// 0x52, 0x49, 0x46, 0x46, // chunk "RIFF"
// 0xaa, 0xb4, 0x01, 0x00, // chunk length
// 0x57, 0x41, 0x56, 0x45, // "WAVE"
// 0x66, 0x6d, 0x74, 0x20, // subchunk1 "fmt"
// 0x10, 0x00, 0x00, 0x00, // subchunk1 length
// 0x01, 0x00, // audio format PCM
// 0x01, 0x00, // 1 channel, mono
// 0x11, 0x2b, 0x00, 0x00, // sample rate
// 0x11, 0x2b, 0x00, 0x00, // byte rate
// 0x01, 0x00, // bytes per sample
// 0x08, 0x00, // bits per sample per channel
// 0x64, 0x61, 0x74, 0x61, // subchunk2 "data"
// 0x33, 0xb4, 0x01, 0x00, // subchunk2 length, bytes
De là, vous pouvez voir que notre fichier a un canal, un taux d'échantillonnage de 11025 hertz et une résolution de 8 bits par échantillon. Notez que si je voulais analyser l'en-tête par programme, alors je devrais prendre en compte l'ordre des octets: en WAV, c'est Little-endian, c'est-à-dire l'octet le moins significatif en premier.
J'ai fini par créer un type de structure pour stocker les informations sonores:
typedef struct _audio_info
{
uint32_t sampleRate;
uint32_t dataLength;
const uint8_t *data;
} audio_info_t;
Et créé une instance de la structure elle-même, en la remplissant comme suit:
const audio_info_t sound_wav_info =
{
11025, // sampleRate
111667, // dataLength
sound_wav // data
};
Dans cette structure, le champ sampleRate est la valeur du champ d'en-tête du même nom, le champ dataLength est la valeur du champ de longueur subchunk2 et le champ de données est un pointeur vers un tableau avec des données.
Ensuite, j'ai connecté les fichiers d'en-tête:
#include "driver/timer.h"
#include "driver/dac.h"
et créé des fonctions prototypes pour initialiser la minuterie et son gestionnaire d'interruption d'alarme, comme cela est fait dans l'exemple wave_gen:
static void IRAM_ATTR timer0_ISR(void *ptr)
{
}
static void timerInit()
{
}
Puis il a commencé à remplir la fonction d'initialisation.
Les minuteries dans ESP32 sont finalement cadencées à partir de APB_CLK_FREQ égal à 80 MHz:
driver / timer.h:
#define TIMER_BASE_CLK (APB_CLK_FREQ) /*!< Frequency of the clock on the input of the timer groups */
soc / soc.h:
#define APB_CLK_FREQ ( 80*1000000 ) //unit: Hz
Pour obtenir la valeur du compteur à laquelle vous devez générer une interruption d'alarme, vous devez diviser la fréquence d'horloge de la minuterie par la valeur du prédécaleur, puis par la fréquence requise avec laquelle l'interruption doit être déclenchée (pour nous, c'est 11025 Hz). Dans le gestionnaire d'interruption, nous passerons un pointeur vers la structure avec les données que nous voulons reproduire.
Ainsi, la fonction d'initialisation de la minuterie ressemble à ceci:
static void timerInit()
{
timer_config_t config = {
.divider = 8, //
.counter_dir = TIMER_COUNT_UP, //
.counter_en = TIMER_PAUSE, // -
.alarm_en = TIMER_ALARM_EN, // Alarm
.intr_type = TIMER_INTR_LEVEL, //
.auto_reload = 1, //
};
//
ESP_ERROR_CHECK(timer_init(TIMER_GROUP_0, TIMER_0, &config));
//
ESP_ERROR_CHECK(timer_set_counter_value(TIMER_GROUP_0, TIMER_0, 0x00000000ULL));
// Alarm
ESP_ERROR_CHECK(timer_set_alarm_value(TIMER_GROUP_0, TIMER_0, TIMER_BASE_CLK / config.divider / sound_wav_info.sampleRate));
//
ESP_ERROR_CHECK(timer_enable_intr(TIMER_GROUP_0, TIMER_0));
//
timer_isr_register(TIMER_GROUP_0, TIMER_0, timer0_ISR, (void *)&sound_wav_info, ESP_INTR_FLAG_IRAM, NULL);
//
timer_start(TIMER_GROUP_0, TIMER_0);
}
La fréquence d'horloge de la minuterie n'est pas divisible par 11025, quel que soit le prescaler défini. Par conséquent, j'ai sélectionné un tel diviseur auquel la fréquence est aussi proche que possible de celle requise.
Passons maintenant à l'écriture du gestionnaire d'interruption. Tout est simple ici: nous prenons l'octet suivant du tableau, le transmettons au DAC et nous nous déplaçons plus loin dans le tableau. Cependant, tout d'abord, vous devez effacer les indicateurs d'interruption du minuteur et redémarrer l'interruption d'alarme:
static uint32_t wav_pos = 0;
static void IRAM_ATTR timer0_ISR(void *ptr)
{
//
timer_group_clr_intr_status_in_isr(TIMER_GROUP_0, TIMER_0);
// Alarm
timer_group_enable_alarm_in_isr(TIMER_GROUP_0, TIMER_0);
audio_info_t *audio = (audio_info_t *)ptr;
if (wav_pos >= audio->dataLength) wav_pos = 0;
dac_output_voltage(DAC_CHANNEL_1, *(audio->data + wav_pos));
wav_pos ++;
}
Oui, travailler avec le DAC intégré dans ESP32 revient à appeler une fonction intégrée dac_output_voltage (en fait pas).
En fait, c'est tout. Nous devons maintenant activer le fonctionnement du canal DAC dont nous avons besoin dans la fonction app_main () et initialiser le minuteur:
void app_main(void)
{
…
ESP_ERROR_CHECK(dac_output_enable(DAC_CHANNEL_1));
timerInit();
Nous collectons, flash, écoutons :) En principe, vous pouvez connecter le haut-parleur directement à la patte du contrôleur - il jouera. Mais il vaut mieux utiliser un amplificateur. J'ai utilisé le TDA7050 qui traînait dans mes bacs.
C'est tout. Oui, quand j'ai finalement commencé à chanter, j'ai aussi pensé que tout s'était avéré beaucoup plus facile que je ne le pensais. Cependant, cet article aidera peut-être d'une manière ou d'une autre ceux qui viennent de commencer à maîtriser l'ESP32.
Peut-être qu'un jour (et si quelqu'un aime ce sous-article) je piloterai un DAC ESP32 en utilisant DMA. C'est encore plus intéressant là-bas, car dans ce cas, vous devrez travailler avec le module I2S intégré.
UPD.
J'ai décidé de donner un exemple de la façon dont cela fonctionne pour moi de démontrer. Il s'agit d'une carte Heltec avec émetteur-récepteur OLED et LoRa, qui, bien sûr, ne sont pas utilisées dans ce cas.