之前的文章我们讲过了I2S。
I2S是什么通信协议?它如何传输音频数据?它和I2C是什么关系?_i2c接口和i2s-CSDN博客文章浏览阅读836次,点赞12次,收藏14次。这个可以参考ADC来理解,我们的ADC也是有左对齐和右对齐的,假设我们的数据是12位的,而传输数据是按照16bit传输的,这时候12bit填不满16bit,我们就需要选择是将数据左对齐还是右对齐了。我们平时打吃鸡可以听声辨位,最主要的原因就是有左右声道,我们左右耳机的声音其实是不一样的,而我们的大脑可以同时处理这两个声道的声音,从而根据声音来判断敌人的位置。我们刚刚说了WS是声道选择线,而WS同时也是帧同步线,它一个周期就表示一帧,而一帧的数据里包含左声道和右声道的数据,所以WS的频率就等于音频的采样率。_i2c接口和i2shttps://blog.csdn.net/m0_63235356/article/details/145260573?spm=1001.2014.3001.5501https://blog.csdn.net/m0_63235356/article/details/145260573?spm=1001.2014.3001.5501https://blog.csdn.net/m0_63235356/article/details/145260573?spm=1001.2014.3001.5501https://blog.csdn.net/m0_63235356/article/details/145260573?spm=1001.2014.3001.5501对协议不太了解的小伙伴可以先去看看
养兵千日用兵一时,我们就是为的今天用I2S来驱动INMP441这个麦克风模块。
当然了,其他音频相关的模块基本用的也都是I2S,所以我们今天就以INMP441为例,来看看如何使用ESP-IDF的I2S。
因为你现在上网一搜ESP32+INMP441,出来的要么是STM32的,要么是用Arduino来编写ESP32的,再要么就是直接把代码给你的。
所以这可能是全网最详细的关于ESP-IDF使用I2S来驱动INMP441模块的文章了(只要你前置定语加的足够多)
接下来我们来看看INMP441的手册来了解了解它。
因为没有官方的中文手册(至少我没找到),所以我用的搜狗翻译机翻了一下(主要是每个月有免费翻译的额度,不用白不用),虽然挺拉的,但我们还是勉强看看。
关注同名公众号“折途想要敲代码”回复关键词“INMP441”即可免费下载所有相关资料
它这边介绍了一下相关的参数,什么信噪比啦,灵敏度啦我们都不用管,我们要注意的就一个,那就是它输出的是24bit的数据。
接下来是WS的频率,我们知道,WS的频率就是声音的采样率,INMP441的采样率是7.8k~50k,一般来说常见的声音采样率是44.1k,所以INMP441是满足要求的。
INMP441一共是有九个引脚,不过模块就引出了六个,看上面表格也可以看的出来,在模块中八号引脚直接永久拉高表示有效。然后GND只需要引出一个即可。
SCK、SD、WS这仨就是I2S用到的线了。
L/R是低电平的时候表示INMP441采集左声道的数据,高电平就是采集右声道的数据。
因为INMP441只能采集一个声道的数据,所以如果我们要采集立体声道的话需要两个INMP441,一个采集左声道一个采集右声道。
上面是描述INMP441三种工作状态的,之所以没放中文版是因为机翻的有错误,比如说2^18翻译成了218,所以小伙伴们私信我下载的中文手册在看的时候还是要结合原版查看的。
当上电之后,INMP441会过2^18个周期之后再正式进入工作模式,所以我们可以通过延时一段时间(100ms左右,具体看SCK的频率)来跳过这一阶段,不跳也可以,但是采集的数据都会是0。
如果光是上电,但是没有时钟信号,那么就会进入待机模式(姑且这么叫吧),恢复时钟信号的时候需要等待2^14个周期(5ms左右,具体看SCK的频率)。
然后我们不能从待机模式直接过渡到关机模式。
关机模式就是把INMP441的使能引脚拉低,如果要恢复工作模式就把使能引脚拉高,然后等待2^17个周期。
不过我们买的INMP441模块没有把使能引脚引出来,所以我们不需要考虑这个。
但如果我们是自己画个板子要用到INMP441这个芯片的话,就可以把使能引脚接到主控芯片上了。
还有一点要注意,那就是不要在没有给INMP441提供VDD的时候给它时钟信号,否则有可能会损坏INMP441。
最后看看上面截的图,是机翻的,勉强看看。
我们要抓的重点就是三个红框框出来的。
首先INMP441发出的格式是24位二进制补码,所以我们拿到手的数据还需要转换一下格式。
第二点是SD数据线需要加一个100k的下拉电阻,关于这一点,拿INMP441芯片焊在自己板子上的小伙伴要注意一下,用模块的话就不需要管这一点了,因为模块上已经集成了一个104的电阻。
最后一个就是如果我们要采集双声道数据,那么使用两个INMP441连线就要参考第三个红框这样。
一个INMP441的L/R接低电平,另一个接高电平,这样每采集一帧数据,WS低电平的时候负责左声道的INMP441发送数据,WS高电平的时候负责右声道的INMP441发送数据,所以两个INMP441共用时钟线和数据线也是没问题的。
以上就是关于INMP441的部分了,剩下是一些跟音频参数相关的,我看不懂,感兴趣的小伙伴自己去看手册。
接下来我们来看看如何使用ESP-IDF的I2S。
I2S - ESP32-S3 - — ESP-IDF 编程指南 release-v5.1 文档https://docs.espressif.com/projects/esp-idf/zh_CN/release-v5.1/esp32s3/api-reference/peripherals/i2s.html#_CPPv419i2s_tdm_slot_mask_thttps://docs.espressif.com/projects/esp-idf/zh_CN/release-v5.1/esp32s3/api-reference/peripherals/i2s.html#_CPPv419i2s_tdm_slot_mask_thttps://docs.espressif.com/projects/esp-idf/zh_CN/release-v5.1/esp32s3/api-reference/peripherals/i2s.html#_CPPv419i2s_tdm_slot_mask_thttps://docs.espressif.com/projects/esp-idf/zh_CN/release-v5.1/esp32s3/api-reference/peripherals/i2s.html#_CPPv419i2s_tdm_slot_mask_t一定要记得选对芯片型号呀呀呀呀呀!!!
上面这个是ESP32S3的。
下面这个是ESP32的(ESP32不仅表示ESP32这类芯片,同时ESP32也是一个具体型号的芯片,这边指的是这个特定型号的芯片)
不同型号的芯片,对于I2S的处理是不一样的。
你们猜我为什么单独拎出来说。
我昨晚折腾了一晚上,然后发现芯片型号没选对,数据处理错误导致串口绘图的数据极其离谱
关于I2S的代码,官方的编程指南里直接就有简单的代码,我们直接复制下来改改就能用,不过我还是像往常一样把相关函数给大家一一讲解一遍吧。
我们看看上面这个状态图,我们要接收来自INMP441的数据,一共需要四步。
第一步是new一个通道,第二步初始化这个通道,第三步使能这个通道,第四步就是读取数据了。
所以我们只需要看上面四个函数就行。
第一步new一个通道。
第一个参数是配置用的结构体。
第二个参数是用来发送数据的通道句柄指针,属于传出参数。
第三个参数是用来发送数据的通道句柄指针,属于传出参数。
如果我们只需要接收数据而不需要发送数据,那么第二个参数可以直接给NULL表示我们不需要配置用来发送数据的通道句柄。
接着可以看看示例代码是怎么处理的。
i2s_chan_handle_t rx_handle;
/* 通过辅助宏获取默认的通道配置
* 这个辅助宏在 'i2s_common.h' 中定义,由所有 I2S 通信模式共享
* 它可以帮助指定 I2S 角色和端口 ID */
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER);
/* 分配新的 TX 通道并获取该通道的句柄 */
i2s_new_channel(&chan_cfg, NULL, &rx_handle);
后两个参数不用说,我们看看第一个参数。
示例代码是使用了一个宏I2S_CHANNEL_DEFAULT_CONFIG来获取到第一个参数。
我们来看看这个宏返回了什么样的东西。
宏也有两个参数,用来配置在了返回的结构体的第一和第二个成员变量上。
配置的分别是使用的I2S资源编号和ESP32S3在I2S总线上扮演的角色。
示例代码中的参数表示自动分配I2S资源以及我们ESP32S3属于主机。
ESP32S3一共有两个I2S资源,I2S0和I2S1,一般我们就用示例代码中的参数,让它自动分配一个就行。
剩下三个参数都是配置DMA的,因为I2S的数据是用DMA来搬运的,不过我们不用管DMA,因为底层都帮我们封装好了。
三个参数的含义依次是使用的DMA的编号,DMA里I2S的数据帧数量,是否自动清理DMA的发送缓冲区。
如果我们I2S采集的数据是24bit的,那么dma_frame_num这个得配置为3的倍数,因为24bit是3Byte,不过它默认配置的240就是3的倍数,所以我们不用管。
DMA缓冲区大小不能超过4092(没错,就是4092),所以这个数据帧数量*3(INMP441一帧数据是24bit,也就是3Byte)不能超过4092。
在这个前提下,我们可以把这个参数配置的越大越好,因为缓冲区大小越大,容纳的数据越多,触发中断的次数也就越少,效率也就越高。
按照我个人的理解就是我们到时候读取数据的时候,一次性读取的数量不要超过这个数,但是实际测试下来好像超过了也没什么问题。
接下来我们进入第二步,初始化这个通道。
参数一传入要配置的通道句柄,我们就把我们刚刚new完的接收句柄传进去,然后参数二是配置用的结构体指针。
我们看看这个结构体。
这个结构体的成员有三个,也都是结构体,从上到下是配置时钟的,配置插槽的(???),配置相关GPIO口的。
我们再次一一查看。
配置时钟的结构体有三个成员,依次是采样率,时钟源,以及MCLK的倍数。
采样率就按照我们的需求来,时钟源就用默认的,这个MCLK在我们之前I2S那篇文章没提到,其实MCLK就是作为整个音频系统的参考时钟,确保所有设备同步工作。
我们使用采集24bit的数据的话,需要把这个MCLK配置为3的倍数,所以我们没得选只能用I2S_MCLK_MULTIPLE_384这个参数。
第二个插槽配置的结构体的成员比较多,不过我们不需要一一去配置。
示例代码里用了一个宏来直接帮我们获取一个配置好的这个结构体。
.slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_24BIT, I2S_SLOT_MODE_MONO),
然后我们看看这个宏帮我们配置的怎么样了。
然后关于这些参数都是什么意思,我懒得一个个去讲了,我们直接对着下面机翻的图来看吧。
我直接说我们要改的地方,第一个是slot_mask,这边默认是配了个both,实际上我们只用一个INMP441的话只采集一个声道的数据,所以我们这边要改成仅左声道或者是仅右声道。
第二个要改的是bit_order_lsb,是否低位先行,默认是false,也就是高位先行;这边我们改成true,也就是低位先行。
INMP441里面写了它是高位先行,那为什么我们这边要配置为低位先行呢?答案是我也不知道,我只知道配位高位先行的话得到的数据绝对值都非常大,配为低位先行的话会好一些(起码我说话的时候绘制的波形会跟着变动)。
关于这一点,我自己也有很多疑问,所以这边不一定是正确的。
数据不对也有可能是因为我自己数据没处理好,欢迎大家对我后面贴出的代码提出修正的建议。
最后是配置GPIO口的结构体,我们直接按照我们的接线去配置就行。
其中MCLK和DOUT我们不需要,因为我们没有MCLK且只接收数据不发送数据。
初始化完通道,接下来就是使能。
只需要把句柄传进去就行。
最后是读取数据。
参数一是句柄。
参数二是存放我们读取到的数据的地址,是传出参数。
参数三是我们要读取的数据长度,单位字节。
参数四是我们实际读取到的数据长度,是传出参数。
参数五是阻塞时间,会阻塞到我们真的读取到了参数三那么多个数据,可以直接给个portMAX_DELAY
然后我们要注意的是,我们要读取的数据长度得是3的倍数,因为INMP441的数据是24bit的,所以我们需要按照顺序把三个byte拼接起来,由于数据类型没有24bit的,所以我们只能使用32bit的数据类型。
而且INMP441传输的是补码,而实际上音频数据是有正负的,所以我们还需要把补码中是负数的拎出来处理一下(因为正数的补码和原码是一样的,所以不用管)。
处理的方式就是判断一下这个24bit的数据的最高位(也就是符号位)是否为1,为1就是负数,负数就要处理。
处理方式就是把有int32_t的高八位置1,这样就可以了,因为芯片存储数据本身用的也是补码,所以我们只要把24bit和32bit之间差的那8个bit补上1就是,如果是正数的话就补0,但是默认就是0,所以正数不用补。
如果上面一段看不懂的小伙伴可以直接用我下面的示例代码,或者去复习一下计算机原理这门课。
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/i2s_std.h"
#include "driver/gpio.h"
#define INMP_SD 1
#define INMP_SCK 2
#define INMP_WS 3
// 每次读取多少帧数据
#define DATA_LEN 64
i2s_chan_handle_t rx_handle;
void I2S_Init(void){// 通过辅助宏获取默认的通道配置它可以帮助指定 I2S 角色和端口 IDi2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER);chan_cfg.dma_frame_num = 1024;// 分配新的 TX 通道并获取该通道的句柄i2s_new_channel(&chan_cfg, NULL, &rx_handle);// 初始化或更新声道和时钟配置i2s_std_config_t std_cfg = {.clk_cfg = {.clk_src = I2C_CLK_SRC_DEFAULT, // 默认时钟.mclk_multiple = I2S_MCLK_MULTIPLE_384,.sample_rate_hz = 44100 // 44.1k采集率},.slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_24BIT, I2S_SLOT_MODE_MONO),.gpio_cfg = {.mclk = I2S_GPIO_UNUSED, // 不需要.dout = I2S_GPIO_UNUSED, // 不需要.bclk = INMP_SCK,.ws = INMP_WS,.din = INMP_SD,.invert_flags = { // 都不需要.mclk_inv = false,.bclk_inv = false,.ws_inv = false,},},};std_cfg.slot_cfg.slot_mask = I2S_STD_SLOT_LEFT; // 修改为左声道std_cfg.slot_cfg.bit_order_lsb = true; // 低位先行,这边我不确定,但采集的数据确实受环境声音的改变而改变,高位先行却没有/* 初始化通道 */i2s_channel_init_std_mode(rx_handle, &std_cfg);/* 在读取数据之前,先启动 RX 通道 */i2s_channel_enable(rx_handle);
}
void app_main(void){I2S_Init();uint8_t* read_data_buff = (uint8_t*)malloc(sizeof(uint8_t) * 3 * DATA_LEN);size_t len = 0;for(;;){i2s_channel_read(rx_handle, read_data_buff, DATA_LEN * 3, &len, portMAX_DELAY);for(uint16_t i = 0; i < len; i += 3){int32_t real_data = (read_data_buff[i] << 16) | (read_data_buff[i + 1] << 8) | (read_data_buff[i + 2]); // 拼接3个byteif(real_data & 0x00800000) real_data |= 0xFF000000; // 如果是负数,那么由24bit无符号转有符号32bit,需要在前8个bit补上1,也就是FFprintf("%ld\r\n",real_data);}}
}
上面代码把采集到的数据直接打印出来了,我们可以使用SerialPlot这个软件来串口绘图。
先配置串口,一般不用动,串口都是这么配置的。
然后把数据类型改为ASCII,默认是Binary的,但是我们使用printf打印的是ASCII类型。
接着修改下面这些参数来调整xy轴来达到自己想要的效果。
下面是我没发出声音的时候的波形。
下面是我唱歌的时候的波形。
估计是数据没有处理好,没有人家那样很漂亮的声波形状。