Aprendiendo a hablar glándulas o ESP32 DAC y un pequeño temporizador

Durante el desarrollo de un dispositivo muy interesante (eh, si solo hubiera suficiente fuerza), decidí que sería bueno si el dispositivo estuviera hablando. La presencia de un DAC de 8 bits de dos canales en el microcontrolador de destino, ESP32 de Espressif Systems, fue útil.



En este tutorial (si puede llamarlo así), le mostraré cómo puede organizar de forma rápida y sencilla la reproducción de un archivo de audio utilizando el microcontrolador ESP32.



Un poco de teoría



Como nos dice Wikipedia, el ESP32 es una serie de microcontroladores de bajo costo y bajo consumo. Son un sistema en un chip (SoC) con controladores y antenas Wi-Fi y Bluetooth integrados. Basado en el núcleo Tensilica Xtensa LX6 en variantes de uno y dos núcleos. Una ruta de radiofrecuencia está integrada en el sistema. MK fue creado y desarrollado por la empresa china Espressif Systems, y es fabricado por TSMC según la tecnología de proceso de 40 nm. Puede leer más sobre las capacidades del chip en la página de Wikipedia y en la documentación oficial.



Una vez, como parte de dominar este controlador, quise reproducir un sonido en él. Al principio pensé que tendría que usar PWM. Sin embargo, después de leer la documentación más de cerca, descubrí la presencia de dos canales de un DAC de 8 bits. Por supuesto, esto cambió fundamentalmente el asunto.



La Referencia técnica dice que el DAC en el ESP32 está construido sobre una cadena de resistencias (aparentemente, significa la cadena R2R) usando un cierto búfer. La tensión de salida se puede variar de 0 voltios a tensión de alimentación (3,3 voltios) con una resolución de 8 bits (es decir, 256 valores). La conversión de los dos canales es independiente. También hay un generador CW incorporado y soporte DMA.



Decidí no entrar en DMA por ahora, limitándome a construir un reproductor basado en un temporizador. Como sabe, para reproducir el archivo WAV más simple de formato PCM, es suficiente leer los datos sin procesar a la frecuencia de muestreo especificada en el archivo y enviarlos a través de los canales DAC, reduciendo preliminarmente (si es necesario) el bitness de los datos al bitness del DAC. Tuve suerte: encontré un conjunto de sonidos en el formato mono WAV PCM de 8 bits 11025 Hz, extraídos de los recursos de un juego antiguo. Esto significa que usaremos solo un canal DAC.



También necesitaremos un temporizador capaz de generar interrupciones de 11025 Hz. Según la misma Referencia Técnica, ESP32 tiene a bordo dos módulos de temporizador con dos temporizadores cada uno, para un total de cuatro temporizadores. Tienen 64 bits de ancho, cada uno con un preescalador de 16 bits y la capacidad de generar una interrupción en un nivel o un borde.



De la teoría a la práctica



Armado con el ejemplo wave_gen de esp-idf, me puse en camino para escribir el código. No me molesté en crear un sistema de archivos: el objetivo era obtener sonido y no convertir ESP32 en un reproductor completo.



Para empezar, superé uno de los archivos WAV en la matriz sish. La utilidad xxd incorporada en Debian me ayudó mucho con esto. Comando simple



$ xxd -i file.wav > file.c


obtenemos un archivo sish con una matriz de datos en forma hexadecimal dentro e incluso con una variable separada que contiene el tamaño del archivo en bytes.



A continuación, comenté los primeros 44 bytes de la matriz: el encabezado del archivo WAV. En el camino, lo analicé por campos y encontré toda la información que necesitaba al respecto:



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


Desde aquí puede ver que nuestro archivo tiene un canal, una frecuencia de muestreo de 11025 hercios y una resolución de 8 bits por muestra. Tenga en cuenta que si quisiera analizar el encabezado mediante programación, necesitaría tener en cuenta el orden de los bytes: en WAV es Little-endian, es decir, el byte menos significativo primero.



Terminé creando un tipo de estructura para almacenar información de sonido:



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


Y creó una instancia de la estructura en sí, llenándola de la siguiente manera:



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


En esta estructura, el campo sampleRate es el valor del campo de encabezado del mismo nombre, el campo dataLength es el valor del campo de longitud subchunk2 y el campo de datos es un puntero a una matriz con datos.



Luego conecté los archivos de encabezado:



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


y creó prototipos de funciones para inicializar el temporizador y su manejador de interrupciones de alarma, como en el ejemplo wave_gen:



static void IRAM_ATTR timer0_ISR(void *ptr)
{

}

static void timerInit()
{

}


Luego comenzó a llenar la función de inicialización.



Los temporizadores en ESP32 se sincronizan eventualmente desde 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 obtener el valor del contador en el que necesita generar una interrupción de alarma, debe dividir la frecuencia de reloj del temporizador por el valor del preescalador y luego por la frecuencia requerida con la que debe activarse la interrupción (para nosotros es 11025 Hz). En el manejador de interrupciones pasaremos un puntero a la estructura con los datos que queremos reproducir.



Por lo tanto, la función de inicialización del temporizador se ve así:



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 frecuencia del reloj del temporizador no es divisible por 11025, sin importar el preescalador que establezcamos. Por lo tanto, seleccioné un divisor en el que la frecuencia sea lo más cercana posible a la requerida.



Ahora pasemos a escribir el manejador de interrupciones. Aquí todo es simple: tomamos el siguiente byte de la matriz, lo alimentamos al DAC y avanzamos a lo largo de la matriz. Sin embargo, en primer lugar, debe borrar los indicadores de interrupción del temporizador y reiniciar la interrupción de la alarma:



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


Sí, trabajar con el DAC incorporado en ESP32 se reduce a llamar a una función incorporada dac_output_voltage (en realidad no).



De hecho, eso es todo. Ahora necesitamos habilitar la operación del canal DAC que necesitamos dentro de la función app_main () e inicializar el temporizador:



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


Ensamblando, parpadeando, escuchando :) Básicamente, puedes conectar el altavoz directamente a la pata del controlador - se reproducirá. Pero es mejor usar un amplificador. Usé el TDA7050 que estaba tirado en mis contenedores.



Eso es todo. Sí, cuando finalmente comencé a cantar, también pensé que todo resultó ser mucho más fácil de lo que pensaba. Sin embargo, tal vez este artículo ayude de alguna manera a aquellos que recién comienzan a dominar el ESP32.



Quizás algún día (y si a alguien le gusta este artículo) conduciré un ESP32 DAC usando DMA. Allí es aún más interesante, porque en este caso tendrás que trabajar con el módulo I2S incorporado.



UPD.



Decidí dar un ejemplo de cómo funciona para mí demostrarlo. Se trata de una placa de Heltec con transceptor OLED y LoRa, que, por supuesto, no se utilizan en este caso.






All Articles