2021暑假一起练—用ESP32-S2-mini-1模块制作了一个音频示波器、频谱仪
使用ESP32-S2-Mini-1模块的音频处理平台以及官方提供的ESP-IDF,完成2021暑假一起练中的项目3,音频示波器、频谱仪。
标签
嵌入式系统
显示
音频处理
孙健超
更新2021-09-14
2096

内容介绍

开发平台介绍

      乐鑫公司的ESP32-S2-Mini-1模块是一颗通用型Wi-Fi MCU模组,功能强大,具有丰富的外设接口,可用于可穿戴电子设备、智能家居等场景。ESP32-S2-MINI-1采用PCB板载天线,模组配置了4MB SPI flash,采用的是 ESP32-S2FN4 芯片。该芯片搭载了Xtensa® 32 位LX7 单核处理器,工作频率高达 240 MHz。用户可以关闭 CPU 的电源,利用低功耗协处理器监测外设的状态变化或某些模拟量是否超出阈值。

该模块可以广泛应用于下面的一些领域:

  • 通用低功耗 IoT 传感器 Hub
  • 通用低功耗 IoT 数据记录器
  • USB 设备
  • 语音识别
  • 图像识别
  • Mesh 网络
  • 家庭自动化
  • 智慧楼宇
  • 工业自动化
  • 健康/医疗/看护
  • Wi-Fi 玩具
  • 可穿戴电子产品
  • 零售 & 餐饮
  • 智能 POS 应用

当然,完成本项目只用到上述部分功能即可,包括ADC,DAC、SPI接口的128*64 OLED,SPI、以及按键等。

本次我的学习与开发主要来源于于ESP-IDF提供的丰富示例,在看懂示例后,设计出项目所需的函数、功能模块,学习各种api接口知识,逐步完成项目。

项目要求

频信号示波器/频谱仪:

  1. 将Mic或耳机插孔输入的语音信号进行ADC量化,并在OLED显示屏上将波形和频谱显示出来,并能够自动测量输入信号的参数 - 峰峰值、频率分量
  2. 通过DAC生成一个单频的模拟正弦波信号,将生成的波形连接到麦克风输入端,并进行ADC采集,再将采集到的波形显示在OLED屏上,并测量出其峰峰值、平均值、频率/周期

开发过程

      第一次做嵌入式的项目,所有东西都感觉很新奇。在听完老师讲的课程后,我开始了学习。一开始就连正确烧录都做不到,没有其他途径,自己在网上寻找解决办法,就这样,一步一步,逐渐学习了gpio、oled、SPI、I2C、freeRTOS、wifi等模块的相关知识,及应用,途中复习了通信协议、计原的中断服务等知识,终于,有了一个初步实现项目的思路,又经历一步一步尝试,将整个系统呈现出来。

项目设计思路

音频信号示波器/频谱仪:

1.用MIC对音频进行采集,再放大模拟信号放大。

2.用adc1以20kHz的速率进行采样。

3.将采集到的原始数据经过256位FFT处理。

4.将频谱或者音频数据(通过sw2控制)通过spi串口传输到oled屏上显示。

音频输出:

1.控制输出幅值和频率方面,通过sw1,sw2,sw4实现。

2.结果显示到屏幕上。

2.通过dac模块输出。

部分代码展示

主程序部分,主要完成初始化spi总线、oled和用到的gpio端口,调用各个函数的功能。

void app_main(void)
{

esp_err_t ret;
spi_bus_config_t buscfg={
.miso_io_num=PIN_NUM_MISO,
.mosi_io_num=PIN_NUM_MOSI,
.sclk_io_num=PIN_NUM_CLK,
.quadwp_io_num=-1,
.quadhd_io_num=-1,
.max_transfer_sz=128*8,
};
spi_device_interface_config_t devcfg={
.clock_speed_hz=20*1000*1000, //Clock out at 20 MHz
.mode=0, //SPI mode 0
.spics_io_num=PIN_NUM_CS,
.queue_size=1,
.pre_cb=oled_spi_pre_transfer_callback,
};
//Initialize the SPI bus
ret=spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO);
ESP_ERROR_CHECK(ret);
//Attach the oled to the SPI bus
ret=spi_bus_add_device(SPI2_HOST, &devcfg, &spi);
ESP_ERROR_CHECK(ret);
//GPIO config
gpio_config_t io_cfg = {
.intr_type = GPIO_INTR_DISABLE,
.mode = GPIO_MODE_OUTPUT,
.pin_bit_mask = ((1ULL<<PIN_NUM_SPEAKER) | (1ULL<<PIN_NUM_AUDIO)),
.pull_up_en = 0,
.pull_down_en = 0
};
gpio_config(&io_cfg);
gpio_set_level(PIN_NUM_SPEAKER,1);
gpio_set_level(PIN_NUM_AUDIO,1);
gpio_set_direction(PIN_NUM_DC, GPIO_MODE_OUTPUT);
gpio_set_direction(PIN_NUM_RST, GPIO_MODE_OUTPUT);
gpio_config_t io_cfg1 = {
.intr_type = GPIO_INTR_POSEDGE,
.mode = GPIO_MODE_INPUT,
.pin_bit_mask = (1ULL<<1|1ULL<<2|1ULL<<3|1ULL<<6), //输入控制信号
.pull_up_en = 0,
.pull_down_en = 0
};
gpio_config(&io_cfg1);
gpio_install_isr_service(0);
gpio_isr_handler_add(1,sw_isr_handler,(void*)GPIO_NUM_1);
gpio_isr_handler_add(2,sw_isr_handler,(void*)GPIO_NUM_2);
gpio_isr_handler_add(3,sw_isr_handler,(void*)GPIO_NUM_3);
gpio_isr_handler_add(6,sw_isr_handler,(void*)GPIO_NUM_6);
oled_init();
oled_start();

//初始化配置完成

char *str3="* Welcome *";
oled_string(20,40,str3,16);
oled_refresh();
vTaskDelay(1000 / portTICK_RATE_MS);
int j;
for(j=0;j<256;j++)
{
X[0][j]=j;
}
for(int i=0;i<7;i++)
{
j=power_2(i);
for(int k=0;k<j;k++)
{
sort(X[i%2]+256/j*k,X[(i+1)%2]+256/j*k,256/j);
}
}
for(j=0;j<256;j++)
{
fft_256_order[j]=X[1][j];
}

adc_addr=adc_dma_linker_init();
dac_addr=dac_dma_linker_init();
adc_dma_queue = xQueueCreate(5, sizeof(adc_dma_t));
io_evt_queue = xQueueCreate(5, sizeof(io_evt_t));
xTaskCreate(io_task, "io_task", 2048, NULL, 10, NULL);

}

oled_task以及io_task分别是为了顺利显示相应数值,以及处理按键输入的线程,按键中断后调用中断服务函数向任务队列里发送一个事件,线程得到触发中断io端口,从而进行相应的处理。都涉及用于adc和dac之间切换以及设置参数。这里做到每一个按键都对应了一个操作命令,判断是哪一个按键传来的中断指令,并准确执行下一段相应功能的程序,并且准确显示我们想要的结果。

void oled_task()
{

adc_dma_t ar;
for(;adc_switch==1;)
if(xQueueReceive(adc_dma_queue, &ar, 1000 / portTICK_RATE_MS))
{
data_process(ar.dma_linker);
if(flag2==0)
{
oled_freq();
}
else
{
oled_wave();
}
}
adc_dac_dma_linker_deinit();
adc_dac_dma_isr_deregister(dma_isr_handler, NULL);
adc_digi_deinit();
vTaskDelete(adc_task);
}

void dac_start()
{
const dac_digi_config_t cfg = {
.mode = DAC_CONV_NORMAL,
.interval = 10000000/SAMPLE_RATE,
.dig_clk.use_apll = false, // APB clk
.dig_clk.div_num = 7,
.dig_clk.div_b = 10,
.dig_clk.div_a = 0,
};
dac_digi_init();
dac_digi_controller_config(&cfg);
uint32_t int_mask = SPI_OUT_DONE_INT_ENA | SPI_OUT_EOF_INT_ENA | SPI_OUT_TOTAL_EOF_INT_ENA;

dac_output_enable(DAC_CHANNEL_1);
adc_dac_dma_linker_start(DMA_ONLY_DAC_OUTLINK, (void *)dac_addr, int_mask);
dac_digi_start();
}

void adc1_start()
{
adc_digi_config_t config = {
.conv_limit_en = false,
.conv_limit_num = 0,
.interval = 195,
.dig_clk.use_apll = 0, // APB clk
.dig_clk.div_num = 10,
.dig_clk.div_b = 10,
.dig_clk.div_a = 0,
.dma_eof_num = 512*2,
};
config.conv_mode = 1;//adc1
config.format = ADC_DIGI_FORMAT_12BIT;
adc_digi_pattern_table_t adc1_patt;
config.adc1_pattern_len = 1;
config.adc1_pattern = &adc1_patt;
adc1_patt.atten = ADC_ATTEN_DB_11;
adc1_patt.channel = ADC_CHANNEL_9;
adc_gpio_init(ADC_UNIT_1, ADC_CHANNEL_9);
adc_digi_controller_config(&config);
adc_digi_filter_t cfg = {
.adc_unit = ADC_UNIT_1,
.mode = flag1,
.channel = ADC_CHANNEL_9,
};
adc_digi_filter_enable(ADC_DIGI_FILTER_IDX0, 1);
adc_digi_filter_set_config(ADC_DIGI_FILTER_IDX0, &cfg);

uint32_t int_mask = SPI_IN_SUC_EOF_INT_ENA;
adc_dac_dma_isr_register(dma_isr_handler, NULL,int_mask);
adc_dac_dma_linker_start(DMA_ONLY_ADC_INLINK, (void *)adc_addr, int_mask);
adc_digi_start();
}

void io_task(void *arg)
{
io_evt_t ar;
for(;;)
if(xQueueReceive(io_evt_queue, &ar, portMAX_DELAY))
{
if(ar.io_num==1&&adc_switch==1)
{
flag1=flag1==5?0:flag1+1;
if(flag1==5)
{
adc_digi_filter_enable(ADC_DIGI_FILTER_IDX0, 0);
}
else
{
adc_digi_filter_t cfg = {
.adc_unit = ADC_UNIT_1,
.mode = flag1,
.channel = ADC_CHANNEL_9,
};

adc_digi_filter_enable(ADC_DIGI_FILTER_IDX0, 1);
adc_digi_filter_set_config(ADC_DIGI_FILTER_IDX0, &cfg);
}
}

if(ar.io_num==1&&adc_switch==0)
{
dac_switch=(dac_switch==0)?1:0;
if(dac_switch==0)
{
dac_digi_stop();
adc_dac_dma_linker_deinit();
dac_digi_deinit();
oled_work();
}
else
{
dac_addr=dac_dma_linker_init();
dac_start();
oled_work();
}
}

if(ar.io_num==2&&dac_switch==0)
{
adc_switch=(adc_switch==0)?1:0;
if(adc_switch==1)
{
adc1_start();
xTaskCreate(oled_task, "task", 2048, NULL, 10, adc_task);
}
if(adc_switch==0)
{
vTaskDelay(100 / portTICK_RATE_MS);
oled_clear();
oled_string(20,40,str_adc,16);
oled_refresh();
}
}

if(ar.io_num==3&&adc_switch==1) flag2=(flag2==1)?0:1;

if(ar.io_num==3&&adc_switch==0)
{
Amplitude=Amplitude==128?0:Amplitude+16;
oled_work();
dac_digi_stop();
dac_addr=dac_dma_linker_init();
dac_digi_start();
}
if(ar.io_num==6&&adc_switch==0)
{
freq=(freq==10)?0:freq+1;
oled_work();
dac_digi_stop();
dac_addr=dac_dma_linker_init();
dac_digi_start();
}
}
}

oled的操作部分代码,通过提供的spi_master.h库函数编写数据和指令传输函数对oled进行控制。具体的,给每一个要在屏幕上显示的变量,都独立便编写了函数以显示与调试,这里通过x,y控制显示的位置,还要起到给定字体大小的功能等。

void oled_spi_pre_transfer_callback(spi_transaction_t *t)
{
int dc=(int)t->user;
gpio_set_level(PIN_NUM_DC, dc);
}

void oled_start()
{
send_cmd(0x8D,spi);
send_cmd(0x14,spi);
send_cmd(0xAF,spi);
}

void oled_refresh()
{
uint8_t m;
for(m=0;m<8;m++)
{
send_cmd(0xb0+m,spi);
send_cmd(0x00,spi);
send_cmd(0x10,spi);
send_data(spi,screen[m],128);
}
}

void oled_clear()
{
uint8_t i,j;
for(i=0;i<8;i++)
for(j=0;j<128;j++)
screen[i][j]=0;
oled_refresh();
}

void oled_drawpoint(uint8_t x,uint8_t y)
{
uint8_t i=0,j=0;
x=127-x;

i=y/8;
j=y%8;
j=1<<j;
screen[i][x] |= j;
}

void oled_clearpoint(uint8_t x,uint8_t y)
{
uint8_t i=0,j=0;
x=127-x;

i=y/8;
j=y%8;
j=1<<j;
screen[i][x]=~screen[i][x];
screen[i][x] |= j;
screen[i][x]=~screen[i][x];
}

void oled_char(uint8_t x,uint8_t y,char chr,uint8_t size1)
{
uint8_t i,m,temp,size2,chr1;
uint8_t y0=y;

size2=(size1/8+((size1%8)?1:0))*(size1/2); //得到字体一个字符对应点阵集所占的字节数
chr1=chr-' '; //计算偏移后的值
for(i=0;i<size2;i++)
{
if(size1==12)
{temp=asc2_1206[chr1][i];} //调用1206字体
else if(size1==16)
{temp=asc2_1608[chr1][i];} //调用1608字体
else return;
for(m=0;m<8;m++)
{
if(temp&0x80)oled_drawpoint(x,y);
else oled_clearpoint(x,y);
temp<<=1;
y--;
if((y0-y)==size1)
{
y=y0;
x++;
break;
}
}
}
}

void oled_string(uint8_t x,uint8_t y,char *chr,uint8_t size1)
{
while((*chr>=' ')&&(*chr<='~')&&(*chr!='.'))//判断是不是非法字符!
{
oled_char(x,y,*chr,size1);
x+=size1/2;
if(x>128-size1) //换行
{
x=0;
y-=size1;
}
chr++;
}
}




void oled_init()
{
//Reset the display
gpio_set_level(PIN_NUM_RST, 0);
vTaskDelay(100 / portTICK_RATE_MS);
gpio_set_level(PIN_NUM_RST, 1);
vTaskDelay(100 / portTICK_RATE_MS);
}

还有其他程序,主要是优化以及计算等,主要通过调用各种函数库来实现,这里不再展开叙述。

学习心得

首先,十分高兴能参加本次硬禾学堂暑假在家练活动,我在其中学习到了很多知识,第一次接触到嵌入式开发,从什么都不会,到一步步做出成果,比掌握的知识更重于的,在于培养了我自己搜寻资料,自己开发,自己调试的能力。经过本次活动,我对于外设和硬件的理解更加深入,对于如何设计一个系统,也有了些自己的体会,也即从整体去把握,不能顾此失彼。同时,本次项目对于我的编程能力提出了挑战,要想结题,编程能力的提高是必不可少的,最终完成,也是对我编程能力的一个肯定。但是程序本身还有许多不规范,能提高的地方,目前只是有了想法,而且对于丰富系统功能,也有了些方向,若想实现,还需继续学习。希望还能参加硬禾学堂的学习活动。

运行截图

Fq9RiBiNM9My_GA7KtR_iL-TaZeOFhWvhv4PuCXm79PnXCsRFERUZdG-FqpfQK2jTossMJfTduJzblpJ8Gap

附件下载

xiangmu1.rar
项目代码

团队介绍

北京理工大学 信息与电子学院 孙健超
团队成员
孙健超

评论

0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号