学习说话腺体或ESP32 DAC和小计时器

在开发一种非常有趣的设备(例如,如果只有足够的强度)的过程中,我决定如果该设备在说话,那会很好。目标微控制器Espressif Systems的ESP32中有一个两通道8位DAC派上用场了。



在本教程中(如果可以调用它),我将向您展示如何使用ESP32微控制器快速而简单地组织音频文件的播放。



一点理论



维基百科告诉我们,ESP32是一系列低成本,低功耗的微控制器。它们是具有集成Wi-Fi和蓝牙控制器以及天线的片上系统(SoC)。基于Tensilica Xtensa LX6内核的单核和双核变体。射频路径已集成到系统中。 MK由中国公司Espressif Systems创建和开发,由台积电根据40 nm制程技术制造。您可以在Wikipedia页面和官方文档中了解有关该芯片功能的更多信息。



曾经,作为掌握此控制器的一部分,我想在其上播放声音。起初我以为我必须使用PWM。但是,在仔细阅读文档之后,我发现存在8位DAC的两个通道。当然,这从根本上改变了事情。



《技术参考》说,ESP32中的DAC是基于使用特定缓冲区的电阻器链(显然是指R2R链)构建的。输出电压可以从0伏变化到电源电压(3.3伏),分辨率为8位(即256个值)。这两个通道的转换是独立的。还有一个内置的CW生成器和DMA支持。



我决定暂时不进入DMA,将自己限制为基于计时器构建播放器。如您所知,为了再现最简单的PCM格式的WAV文件,以文件中指定的采样率从原始数据中读取原始数据并通过DAC通道将其推入就足够了,从而将数据的比特率初步降低(如有必要)为DAC的比特率。我很幸运:我从旧游戏的资源中发现了WAV PCM 8位11025 Hz单声道格式的声音。这意味着我们将仅使用一个DAC通道。



我们还需要一个能够产生11025 Hz中断的计时器。根据同一《技术参考》,ESP32板载两个计时器模块,每个模块具有两个计时器,总共四个计时器。它们是64位的,每个都有16位的预分频器,并且能够在电平或边沿上产生中断。



从理论到实践



有了esp-idf的wave_gen示例,我着手编写代码。我并不在意创建文件系统:目标是获得声音,而不是使用ESP32打造成熟的播放器。



首先,我将其中一个WAV文件替换为sish数组。Debian内置的xxd实用程序对此提供了很多帮助。简单命令



$ xxd -i file.wav > file.c


我们得到一个Sish文件,其中包含十六进制形式的数据数组,甚至包含一个单独的变量(包含以字节为单位的文件大小)。



接下来,我注释掉了数组的前44个字节-WAV文件的标题。在此过程中,我按字段对它进行了分解,并找到了我所需的所有信息:



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


从这里您可以看到我们的文件具有一个通道,采样率为11025赫兹,每个样本的分辨率为8位。请注意,如果我想以编程方式解析标头,则需要考虑字节顺序:在WAV中,它是Little-endian,即最低有效字节在前。



我最终创建了一种用于存储声音信息的结构类型:



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


并创建了一个结构本身的实例,如下所示:



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


在此结构中,sampleRate字段是同名标题字段的值,dataLength字段是subchunk2长度字段的值,而data字段是指向包含数据的数组的指针。



接下来,我包括了头文件:



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


并创建函数原型以初始化计时器及其警报中断处理程序,如wave_gen示例所示:



static void IRAM_ATTR timer0_ISR(void *ptr)
{

}

static void timerInit()
{

}


然后,他开始填充初始化功能。



ESP32中的定时器最终以APB_CLK_FREQ等于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


要获取需要生成警报中断的计数器值,您需要将计时器的时钟频率除以预分频器的值,然后除以触发中断所需的频率(对我们而言为11025 Hz)。在中断处理程序中,我们将传递一个指向要复制的数据的结构的指针。



因此,计时器初始化函数如下所示:



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


无论我们设置了什么预分频器,定时器的时钟频率都不能被11025整除。因此,我选择了一种分频器,该分频器的频率应尽可能接近所需的分频器。



现在让我们继续编写中断处理程序。这里的一切都很简单:我们从阵列中取出下一个字节,将其馈送到DAC,然后沿阵列进一步移动。但是,首先,您需要清除计时器中断标志并重新启动警报中断:



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


是的,使用ESP32中的内置DAC归结为调用了一个内置函数dac_output_voltage(实际上不是)。



实际上,仅此而已。现在,我们需要在app_main()函数内部启用所需的DAC通道的操作,并初始化计时器:



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


我们收集,闪烁,收听:)原则上,您可以将扬声器直接连接到控制器支脚-它会播放。但是最好使用放大器。我使用了放在垃圾箱中的TDA7050。



就这样。是的,当我最终开始唱歌时,我还认为一切都比我想的容易得多。但是,也许本文会以某种方式对刚开始掌握ESP32的人有所帮助。



也许有一天(如果有人喜欢这篇文章),我将使用DMA驱动ESP32 DAC。在那里仍然更加有趣,因为在这种情况下,您将必须使用内置的I2S模块。



UPD。



我决定举一个例子来说明它如何工作。这是Heltec的带有OLED和LoRa收发器的电路板,当然在这种情况下不使用。






All Articles