Dalam tutorial ini (jika Anda dapat menyebutnya begitu) saya akan menunjukkan kepada Anda bagaimana Anda dapat dengan cepat dan mudah mengatur pemutaran file audio menggunakan mikrokontroler ESP32.
Sedikit teori
Seperti yang dikatakan Wikipedia, ESP32 adalah rangkaian mikrokontroler berbiaya rendah dan berdaya rendah. Mereka adalah sistem pada chip (SoC) dengan Wi-Fi terintegrasi dan pengontrol dan antena Bluetooth. Berdasarkan inti Tensilica Xtensa LX6 dalam varian inti tunggal dan ganda. Jalur frekuensi radio diintegrasikan ke dalam sistem. MK dibuat dan dikembangkan oleh perusahaan Cina Espressif Systems, dan diproduksi oleh TSMC sesuai dengan teknologi proses 40 nm. Anda dapat membaca lebih lanjut tentang kemampuan chip di halaman Wikipedia dan di dokumentasi resmi.
Suatu kali, sebagai bagian dari penguasaan pengontrol ini, saya ingin memainkan suara di atasnya. Awalnya saya pikir saya harus menggunakan PWM. Namun, setelah membaca dokumentasi lebih dekat, saya menemukan keberadaan dua saluran DAC 8-bit. Tentu saja, ini mengubah masalah secara mendasar.
Referensi Teknis mengatakan bahwa DAC di ESP32 dibangun di atas rantai resistor (tampaknya, itu berarti rantai R2R) menggunakan buffer tertentu. Tegangan output dapat bervariasi dari 0 volt hingga tegangan suplai (3,3 volt) dengan resolusi 8 bit (yaitu 256 nilai). Konversi dari dua saluran tidak bergantung. Ada juga generator CW built-in dan dukungan DMA.
Saya memutuskan untuk tidak masuk ke DMA untuk saat ini, membatasi diri saya untuk membangun pemain berdasarkan pengatur waktu. Seperti yang Anda ketahui, untuk mereproduksi file WAV format PCM yang paling sederhana, cukup membaca data mentah darinya pada laju pengambilan sampel yang ditentukan dalam file dan mendorongnya melalui saluran DAC, yang sebelumnya mengurangi (jika perlu) bitness data menjadi bitness DAC. Saya beruntung: Saya menemukan satu set suara dalam format mono WAV PCM 8 bit 11025 Hz, yang diambil dari sumber daya game lama. Ini berarti kami hanya akan menggunakan satu saluran DAC.
Kami juga membutuhkan pengatur waktu yang mampu menghasilkan interupsi 11025 Hz. Menurut Referensi Teknis yang sama, ESP32 memiliki dua modul pengatur waktu di papan dengan masing-masing dua pengatur waktu, dengan total empat pengatur waktu. Mereka 64-bit, masing-masing dengan prescaler 16-bit dan kemampuan untuk menghasilkan interupsi pada level atau edge.
Dari teori ke praktek
Berbekal contoh wave_gen dari esp-idf, saya mulai menulis kodenya. Saya tidak repot-repot membuat sistem file: tujuannya adalah untuk mendapatkan suara, dan bukan membuat pemain penuh dari ESP32.
Untuk memulainya, saya mengambil alih salah satu file WAV ke sish array. Utilitas xxd yang dibangun di Debian banyak membantu saya dalam hal ini. Perintah sederhana
$ xxd -i file.wav > file.c
kami mendapatkan file sish dengan array data dalam bentuk heksadesimal di dalamnya dan bahkan dengan variabel terpisah yang berisi ukuran file dalam byte.
Selanjutnya, saya mengomentari 44 byte pertama dari array - header dari file WAV. Sepanjang jalan, saya membongkarnya dengan bidang dan menemukan semua informasi yang saya butuhkan tentangnya:
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
Dari sini Anda dapat melihat bahwa file kami memiliki satu saluran, laju pengambilan sampel 11025 hertz dan resolusi 8 bit per sampel. Perhatikan bahwa jika saya ingin mengurai tajuk secara terprogram, maka saya harus memperhitungkan urutan byte: di WAV adalah Little-endian, yaitu byte yang paling tidak signifikan terlebih dahulu.
Saya akhirnya membuat tipe struktur untuk menyimpan informasi suara:
typedef struct _audio_info
{
uint32_t sampleRate;
uint32_t dataLength;
const uint8_t *data;
} audio_info_t;
Dan membuat instance dari struktur itu sendiri, mengisinya sebagai berikut:
const audio_info_t sound_wav_info =
{
11025, // sampleRate
111667, // dataLength
sound_wav // data
};
Dalam struktur ini, bidang sampleRate adalah nilai bidang header dengan nama yang sama, bidang dataLength adalah nilai bidang subchunk2 panjang, dan bidang data adalah penunjuk ke array dengan data.
Lalu saya menghubungkan file header:
#include "driver/timer.h"
#include "driver/dac.h"
dan membuat prototipe fungsi untuk menginisialisasi pengatur waktu dan pengendali interupsi Alarm-nya, seperti pada contoh wave_gen:
static void IRAM_ATTR timer0_ISR(void *ptr)
{
}
static void timerInit()
{
}
Kemudian dia mulai mengisi fungsi inisialisasi.
Pengatur waktu di ESP32 pada akhirnya dihitung dari APB_CLK_FREQ sama dengan 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
Untuk mendapatkan nilai penghitung di mana Anda perlu menghasilkan interupsi Alarm, Anda perlu membagi frekuensi clock pengatur waktu dengan nilai prescaler, dan kemudian dengan frekuensi yang diperlukan untuk memicu interupsi (bagi kami itu adalah 11025 Hz). Dalam penangan interupsi, kita akan memberikan pointer ke struktur dengan data yang ingin kita reproduksi.
Jadi, fungsi inisialisasi timer terlihat seperti ini:
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);
}
Frekuensi clock timer tidak habis dibagi 11025, tidak peduli prescaler apa yang kita atur. Oleh karena itu, saya memilih pembagi seperti itu di mana frekuensinya sedekat mungkin dengan yang diperlukan.
Sekarang mari kita lanjutkan ke menulis penangan interupsi. Semuanya sederhana di sini: kita mengambil byte berikutnya dari array, memasukkannya ke DAC, dan melanjutkan array lebih jauh. Namun, pertama-tama, Anda perlu menghapus tanda penghitung waktu dan memulai ulang interupsi Alarm:
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 ++;
}
Ya, bekerja dengan DAC built-in di ESP32 bermuara pada memanggil satu fungsi built-in dac_output_voltage (sebenarnya tidak).
Sebenarnya itu saja. Sekarang kita perlu mengaktifkan operasi saluran DAC yang kita butuhkan di dalam fungsi app_main () dan menginisialisasi pengatur waktu:
void app_main(void)
{
β¦
ESP_ERROR_CHECK(dac_output_enable(DAC_CHANNEL_1));
timerInit();
Kami mengumpulkan, mem-flash, mendengarkan :) Pada prinsipnya, Anda dapat menghubungkan speaker langsung ke kaki pengontrol - itu akan diputar. Tapi lebih baik menggunakan amplifier. Saya menggunakan TDA7050 yang tergeletak di tempat sampah saya.
Itu saja. Ya, ketika saya akhirnya mulai menyanyi, saya juga berpikir bahwa semuanya ternyata jauh lebih mudah dari yang saya kira. Namun, mungkin artikel ini akan membantu Anda bagi mereka yang baru mulai menguasai ESP32.
Mungkin suatu hari nanti (dan jika ada yang menyukai artikel di bawah ini) saya akan mengendarai ESP32 DAC menggunakan DMA. Masih lebih menarik di sana, karena dalam hal ini Anda harus bekerja dengan modul I2S bawaan.
UPD.
Saya memutuskan untuk memberikan contoh bagaimana cara kerjanya bagi saya untuk menunjukkan. Ini adalah papan dari Heltec dengan transceiver OLED dan LoRa, yang tentu saja tidak digunakan dalam kasus ini.