In diesem Tutorial (wenn Sie es so nennen können) zeige ich Ihnen, wie Sie die Wiedergabe einer Audiodatei mit dem ESP32-Mikrocontroller schnell und einfach organisieren können.
Ein bisschen Theorie
Wie Wikipedia uns sagt, handelt es sich beim ESP32 um eine Reihe kostengünstiger Mikrocontroller mit geringem Stromverbrauch. Sie sind ein System auf einem Chip (SoC) mit integrierten Wi-Fi- und Bluetooth-Controllern und -Antennen. Basierend auf dem Tensilica Xtensa LX6-Kern in Single- und Dual-Core-Varianten. Ein Hochfrequenzpfad ist in das System integriert. MK wurde von der chinesischen Firma Espressif Systems entwickelt und wird von TSMC nach der 40-nm-Prozesstechnologie hergestellt. Weitere Informationen zu den Funktionen des Chips finden Sie auf der Wikipedia-Seite und in der offiziellen Dokumentation.
Als Teil dieses Controllers wollte ich einmal einen Sound darauf spielen. Zuerst dachte ich, ich müsste PWM verwenden. Nachdem ich die Dokumentation genauer gelesen hatte, stellte ich fest, dass zwei Kanäle eines 8-Bit-DAC vorhanden waren. Dies hat die Sache natürlich radikal verändert.
Die technische Referenz besagt, dass der DAC im ESP32 auf einer Kette von Widerständen aufgebaut ist (anscheinend bedeutet dies die R2R-Kette), wobei ein bestimmter Puffer verwendet wird. Die Ausgangsspannung kann von 0 Volt bis zur Versorgungsspannung (3,3 Volt) mit einer Auflösung von 8 Bit (d. H. 256 Werten) variiert werden. Die Umwandlung der beiden Kanäle ist unabhängig. Es gibt auch einen eingebauten CW-Generator und DMA-Unterstützung.
Ich entschied mich vorerst, nicht in DMA zu gehen und beschränkte mich darauf, einen Spieler basierend auf einem Timer zu bauen. Wie Sie wissen, reicht es aus, um die einfachste WAV-Datei im PCM-Format zu reproduzieren, Rohdaten mit der in der Datei angegebenen Abtastrate daraus zu lesen und durch die DAC-Kanäle zu schieben, um die Bitterkeit der Daten vorab auf die Bitterkeit des DAC zu reduzieren (falls erforderlich). Ich hatte Glück: Ich fand eine Reihe von Sounds im 8-Bit-Monoformat WAV PCM 8 Bit 11025 Hz, die aus den Ressourcen eines alten Spiels stammen. Dies bedeutet, dass wir nur einen DAC-Kanal verwenden.
Wir benötigen auch einen Timer, der 11025-Hz-Interrupts erzeugen kann. Laut derselben technischen Referenz verfügt ESP32 über zwei Timer-Module mit jeweils zwei Timern für insgesamt vier Timer. Sie sind 64 Bit breit, jeweils mit einem 16-Bit-Vorteiler und der Fähigkeit, einen Interrupt auf einer Ebene oder einer Kante zu erzeugen.
Von der Theorie zur Praxis
Mit dem Beispiel wave_gen von esp-idf machte ich mich auf den Weg, um den Code zu schreiben. Ich habe mich nicht darum gekümmert, ein Dateisystem zu erstellen: Das Ziel war es, Sound zu erhalten und aus ESP32 keinen vollwertigen Player zu machen.
Zunächst habe ich eine der WAV-Dateien in das sish-Array übernommen. Das in Debian integrierte Dienstprogramm xxd hat mir dabei sehr geholfen. Einfacher Befehl
$ xxd -i file.wav > file.c
Wir erhalten eine Sish-Datei mit einem Array von Daten in hexadezimaler Form und sogar mit einer separaten Variablen, die die Dateigröße in Bytes enthält.
Als nächstes habe ich die ersten 44 Bytes des Arrays auskommentiert - den Header der WAV-Datei. Unterwegs analysierte ich es nach Feldern und fand alle Informationen heraus, die ich dazu brauchte:
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
Von hier aus können Sie sehen, dass unsere Datei einen Kanal, eine Abtastrate von 11025 Hertz und eine Auflösung von 8 Bit pro Abtastung hat. Beachten Sie, dass ich, wenn ich den Header programmgesteuert analysieren möchte, die Bytereihenfolge berücksichtigen muss: In WAV ist es Little-Endian, dh das niedrigstwertige Byte zuerst.
Am Ende habe ich einen Strukturtyp zum Speichern von Soundinformationen erstellt:
typedef struct _audio_info
{
uint32_t sampleRate;
uint32_t dataLength;
const uint8_t *data;
} audio_info_t;
Und erstellte eine Instanz der Struktur selbst und füllte sie wie folgt aus:
const audio_info_t sound_wav_info =
{
11025, // sampleRate
111667, // dataLength
sound_wav // data
};
In dieser Struktur ist das sampleRate-Feld der Wert des gleichnamigen Header-Felds, das dataLength-Feld der Wert des subchunk2-Längenfelds und das Datenfeld ein Zeiger auf ein Array mit Daten.
Als nächstes habe ich die Header-Dateien eingefügt:
#include "driver/timer.h"
#include "driver/dac.h"
und erstellte Prototypfunktionen zum Initialisieren des Timers und seines Alarm-Interrupt-Handlers, wie im Beispiel "wave_gen" ausgeführt:
static void IRAM_ATTR timer0_ISR(void *ptr)
{
}
static void timerInit()
{
}
Dann füllte er die Initialisierungsfunktion aus.
Die Timer in ESP32 werden von APB_CLK_FREQ mit 80 MHz getaktet:
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
Um den Zählerwert zu erhalten, bei dem Sie einen Alarm-Interrupt erzeugen müssen, müssen Sie die Taktfrequenz des Timers durch den Wert des Vorteilers und dann durch die erforderliche Frequenz dividieren, mit der der Interrupt ausgelöst werden soll (für uns sind es 11025 Hz). Im Interrupt-Handler übergeben wir einen Zeiger auf die Struktur mit den Daten, die wir reproduzieren möchten.
Die Timer-Initialisierungsfunktion sieht also folgendermaßen aus:
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);
}
Die Taktfrequenz des Timers ist nicht durch 11025 teilbar, egal welchen Prescaler wir einstellen. Deshalb habe ich einen solchen Teiler ausgewählt, bei dem die Frequenz so nahe wie möglich an der gewünschten liegt.
Fahren wir nun mit dem Schreiben des Interrupt-Handlers fort. Hier ist alles einfach: Wir nehmen das nächste Byte aus dem Array, geben es an den DAC weiter und bewegen uns weiter entlang des Arrays. Zunächst müssen Sie jedoch die Timer-Interrupt-Flags löschen und den Alarm-Interrupt neu starten:
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 ++;
}
Ja, die Arbeit mit dem in ESP32 integrierten DAC läuft darauf hinaus, eine integrierte Funktion dac_output_voltage aufzurufen (eigentlich nicht).
Eigentlich ist das alles. Jetzt müssen wir den Betrieb des benötigten DAC-Kanals in der Funktion app_main () aktivieren und den Timer initialisieren:
void app_main(void)
{
…
ESP_ERROR_CHECK(dac_output_enable(DAC_CHANNEL_1));
timerInit();
Wir sammeln, blinken, hören :) Grundsätzlich können Sie den Lautsprecher direkt an das Bein des Controllers anschließen - er wird abgespielt. Es ist jedoch besser, einen Verstärker zu verwenden. Ich habe den TDA7050 verwendet, der in meinen Behältern herumlag.
Das ist alles. Ja, als ich endlich anfing zu singen, dachte ich auch, dass alles viel einfacher war als ich dachte. Vielleicht hilft dieser Artikel jedoch in gewisser Weise denjenigen, die gerade mit der Beherrschung des ESP32 begonnen haben.
Vielleicht fahre ich eines Tages (und wenn jemand diesen Unterartikel mag) einen ESP32-DAC mit DMA. Dort ist es noch interessanter, denn in diesem Fall müssen Sie mit dem eingebauten I2S-Modul arbeiten.
UPD.
Ich beschloss, ein Beispiel dafür zu geben, wie es für mich funktioniert, zu demonstrieren. Dies ist eine Karte von Heltec mit OLED und LoRa-Transceiver, die in diesem Fall natürlich nicht verwendet werden.