Aprender a falar glândulas, ou ESP32 DAC e um pequeno cronômetro

Durante o desenvolvimento de um dispositivo muito interessante (eh, se ao menos houvesse força suficiente), decidi que seria bom se o dispositivo estivesse falando. A presença de um DAC de 8 bits de dois canais no microcontrolador de destino, ESP32 da Espressif Systems, foi útil.



Neste tutorial (se você pode chamá-lo assim), vou mostrar como você pode organizar de forma rápida e simples a reprodução de um arquivo de áudio usando o microcontrolador ESP32.



Um pouco de teoria



Como a Wikipedia nos diz, o ESP32 é uma série de microcontroladores de baixo custo e baixa potência. Eles são um sistema em um chip (SoC) com controladores e antenas Wi-Fi e Bluetooth integrados. Baseado no núcleo Tensilica Xtensa LX6 em variantes de núcleo único e dual. Um caminho de radiofrequência é integrado ao sistema. O MK foi criado e desenvolvido pela empresa chinesa Espressif Systems, e é fabricado pela TSMC de acordo com a tecnologia de processo de 40 nm. Você pode ler mais sobre os recursos do chip na página da Wikipedia e na documentação oficial.



Certa vez, como parte da masterização deste controlador, eu queria tocar um som nele. No início, pensei que teria que usar o PWM. No entanto, depois de ler a documentação mais de perto, descobri a presença de dois canais de um DAC de 8 bits. Claro, isso mudou radicalmente o assunto.



A Referência Técnica diz que o DAC no ESP32 é construído em uma cadeia de resistores (aparentemente, significa a cadeia R2R) usando um determinado buffer. A tensão de saída pode variar de 0 volts à tensão de alimentação (3,3 volts) com uma resolução de 8 bits (ou seja, 256 valores). A conversão dos dois canais é independente. Há também um gerador CW integrado e suporte DMA.



Decidi não entrar no DMA por enquanto, limitando-me a construir um jogador baseado em um cronômetro. Como você sabe, para reproduzir o arquivo WAV mais simples do formato PCM, é suficiente ler os dados brutos dele na taxa de amostragem especificada no arquivo e enviá-los através dos canais DAC, reduzindo preliminarmente (se necessário) a quantidade de bits dos dados para a quantidade de bits do DAC. Tive sorte: encontrei um conjunto de sons no formato mono WAV PCM 8 bits 11025 Hz, extraído dos recursos de um jogo antigo. Isso significa que usaremos apenas um canal DAC.



Também precisaremos de um temporizador capaz de gerar interrupções de 11025 Hz. De acordo com a mesma Referência Técnica, o ESP32 possui a bordo dois módulos temporizadores com dois temporizadores cada, totalizando quatro temporizadores. Eles são de 64 bits, cada um com um prescaler de 16 bits e a capacidade de gerar uma interrupção em um nível ou borda.



Da teoria à prática



Armado com o exemplo wave_gen da esp-idf, comecei a escrever o código. Não me preocupei em criar um sistema de arquivos: o objetivo era obter som, e não fazer do ESP32 um reprodutor completo.



Para começar, passei um dos arquivos WAV para o array sish. O utilitário xxd embutido no Debian me ajudou muito com isso. Comando simples



$ xxd -i file.wav > file.c


obtemos um arquivo sish com um array de dados em formato hexadecimal interno e até mesmo com uma variável separada que contém o tamanho do arquivo em bytes.



Em seguida, comentei os primeiros 44 bytes do array - o cabeçalho do arquivo WAV. Ao longo do caminho, desmontei-o por campos e descobri todas as informações de que precisava sobre ele:



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


Aqui você pode ver que nosso arquivo possui um canal, uma taxa de amostragem de 11025 hertz e uma resolução de 8 bits por amostra. Observe que, se eu quisesse analisar o cabeçalho de forma programática, precisaria levar em consideração a ordem dos bytes: em WAV, é Little-endian, ou seja, o byte menos significativo primeiro.



Acabei criando um tipo de estrutura para armazenar informações de som:



typedef struct _audio_info
{
	uint32_t sampleRate;
	uint32_t dataLength;
	const uint8_t *data;
} audio_info_t;


E criou uma instância da própria estrutura, preenchendo-a da seguinte forma:



const audio_info_t sound_wav_info =
{
	11025, // sampleRate
	111667, // dataLength
	sound_wav // data
};


Nessa estrutura, o campo sampleRate é o valor do campo de cabeçalho de mesmo nome, o campo dataLength é o valor do campo de comprimento subchunk2 e o campo de dados é um ponteiro para uma matriz com dados.



Em seguida, incluí os arquivos de cabeçalho:



#include "driver/timer.h"
#include "driver/dac.h"


e criou protótipos de função para inicializar o temporizador e seu manipulador de interrupção de alarme, como no exemplo wave_gen:



static void IRAM_ATTR timer0_ISR(void *ptr)
{

}

static void timerInit()
{

}


Então ele começou a preencher a função de inicialização.



Os temporizadores em ESP32 são cronometrados eventualmente de APB_CLK_FREQ igual a 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


Para obter o valor do contador no qual você precisa gerar uma interrupção de alarme, você precisa dividir a frequência do relógio do temporizador pelo valor do prescaler e, em seguida, pela frequência necessária com a qual a interrupção deve ser disparada (para nós é 11025 Hz). No tratador de interrupção, passaremos um ponteiro para a estrutura com os dados que queremos reproduzir.



Assim, a função de inicialização do temporizador se parece com isto:



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);
}


A frequência do relógio do temporizador não é divisível por 11025, não importa o prescaler definido. Portanto, selecionei um divisor em que a frequência seja a mais próxima possível da necessária.



Agora, vamos escrever o manipulador de interrupções. Tudo é simples aqui: pegamos o próximo byte do array, o alimentamos no DAC e avançamos ao longo do array. No entanto, em primeiro lugar, você precisa limpar os sinalizadores de interrupção do cronômetro e reiniciar a interrupção do 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 ++;
}


Sim, trabalhar com o DAC integrado no ESP32 se resume a chamar uma função integrada dac_output_voltage (na verdade, não).



Na verdade, isso é tudo. Agora precisamos habilitar a operação do canal DAC que precisamos dentro da função app_main () e inicializar o temporizador:



void app_main(void)
{
    
    ESP_ERROR_CHECK(dac_output_enable(DAC_CHANNEL_1));
    timerInit();


Coletamos, piscamos, ouvimos :) Em princípio, você pode conectar o alto-falante diretamente à perna do controlador - ele tocará. Mas é melhor usar um amplificador. Usei o TDA7050 que estava escondido nas minhas caixas.



Isso é tudo. Sim, quando finalmente comecei a cantar, também pensei que tudo acabou sendo muito mais fácil do que eu pensava. No entanto, talvez este artigo ajude de alguma forma aqueles que estão apenas começando a dominar o ESP32.



Talvez um dia (e se alguém gostar deste artigo) eu dirigirei um DAC ESP32 usando DMA. É ainda mais interessante lá, porque neste caso você terá que trabalhar com o módulo I2S embutido.



UPD.



Decidi dar um exemplo de como funciona para mim demonstrar. Esta é uma placa da Heltec com transceptor OLED e LoRa, que, obviamente, não são usados ​​neste caso.






All Articles