ESP32 I2S音频总线学习笔记(二):I2S读取INMP441音频数据
简介
在这个系列的上一篇文章中,我们介绍了ESP32 I2S音频总线的相关知识,简要了解了什么是I2S总线、它的通信格式,以及相关的底层API函数。没有看过上篇文章的可以点击文章进行回顾:
ESP32 I2S音频总线学习笔记(一):初识I2S通信与配置基础
这篇文章将介绍一个小案例——ESP32驱动INMP441读取音频数据,它是关于如何使用I2S读取数据的一个应用,主要是将ESP32读取到的音频数据发送到串口上并实时显示波形,这个我们可以通过串口绘图仪来实现。在这之前先来看一下INMP441这个模块吧
INMP441全向麦克风模块
NMP441是一款高性能、低功耗、数字输出,带底部端口的全向MEMS麦克风。该完整的INMP441解决方案由一个MEMS传感器、信号调节模块、数字混合滤波器、电源管理和行业标准的24位I²S接口组成。I²S接口允许INMP441直接连接到数字处理器,如DSP和微控制器,无需额外的音频解码器。NMP441具有高信噪比,是一种适用于近场应用的理想选择。
产品特性:
具有高精度24位数据的数字I²S接口
高信噪比为61 dBA
高灵敏度-26 dBFS
从60 Hz到15 kHz的稳定频率响应
低功耗:低电流消耗1.4 mA
高PSR:-75 dBFS
功能框图
INMP441模块使用到的芯片内置ADC,用于将采集到的模拟信号转换成数字信号,上面还有滤波器和硬件控制、电源相关的引脚。
引脚定义
VDD | 输入电源,1.8V至3.3V |
---|---|
GND | 电源地 |
SCK | I²S接口的串行数据时钟 |
WS | 用于I²S接口的串行数据字选择 |
SD | I²S接口的串行数据输出 |
L/R | 左/右声道选择 |
其中L/R是 左/右声道选择。设置为低电平时,麦克风在I²S帧的左声道输出信号。设置为高电平时,麦克风在右声道输出信号。
安装I2S驱动
上篇文章我们在介绍I2S底层API函数提到,在使用I2S通信时需要加载I2S驱动,不知道小伙伴们还记不记得。这个加载I2S驱动的函数就是:esp_err_t i2s_driver_install(i2s_port_t i2s_num, const i2s_config_t *i2s_config, int queue_size, void *i2s_queue)
,里面有4个参数需要配置,在上次都有提到每个参数的意义。 其中比较复杂的是i2s_config这个结构体变量,我们需要对结构体的每个参数进行配置,如下:
typedef struct {
i2s_mode_t mode; /*< 设置 I2S 的工作模式 */
uint32_t sample_rate; /*!< 设置音频采样率 */
i2s_bits_per_sample_t bits_per_sample; /*!< 设置采样位数 */
i2s_channel_fmt_t channel_format; /*!< 设置数据通道格式.*/
i2s_comm_format_t communication_format; /*!< 设置I2C数据传输格式 */
int intr_alloc_flags; /*!< 设置中断相关标志位*/
int dma_buf_count; dma缓存个数,
int dma_buf_len;
} i2s_driver_config_t;
typedef i2s_driver_config_t i2s_config_t;
除了后面几个整型变量,每个结构体成员其实是一个枚举类型
I2S工作模式mode
typedef enum {
I2S_MODE_MASTER = (0x1 << 0), /*!< Master mode*/
I2S_MODE_SLAVE = (0x1 << 1), /*!< Slave mode*/
I2S_MODE_TX = (0x1 << 2), /*!< TX mode*/
I2S_MODE_RX = (0x1 << 3), /*!< RX mode*/
#if SOC_I2S_SUPPORTS_DAC
//built-in DAC functions are only supported on I2S0 for ESP32 chip.
I2S_MODE_DAC_BUILT_IN = (0x1 << 4), /*!< Output I2S data to built-in DAC, no matter the data format is 16bit or 32 bit, the DAC module will only take the 8bits from MSB*/
#endif // SOC_I2S_SUPPORTS_DAC
#if SOC_I2S_SUPPORTS_ADC
I2S_MODE_ADC_BUILT_IN = (0x1 << 5), /*!< Input I2S data from built-in ADC, each data can be 12-bit width at most*/
#endif // SOC_I2S_SUPPORTS_ADC
// PDM functions are only supported on I2S0 (all chips).
I2S_MODE_PDM = (0x1 << 6), /*!< I2S PDM mode*/
} i2s_mode_t;
音频采样率bits_per_sample
typedef enum {
I2S_BITS_PER_SAMPLE_8BIT = 8, /*!< data bit-width: 8 */
I2S_BITS_PER_SAMPLE_16BIT = 16, /*!< data bit-width: 16 */
I2S_BITS_PER_SAMPLE_24BIT = 24, /*!< data bit-width: 24 */
I2S_BITS_PER_SAMPLE_32BIT = 32, /*!< data bit-width: 32 */
} i2s_bits_per_sample_t;
通道格式channel_format
typedef enum {
I2S_CHANNEL_FMT_RIGHT_LEFT, /*!< Separated left and right channel */
I2S_CHANNEL_FMT_ALL_RIGHT, /*!< Load right channel data in both two channels */
I2S_CHANNEL_FMT_ALL_LEFT, /*!< Load left channel data in both two channels */
I2S_CHANNEL_FMT_ONLY_RIGHT, /*!< Only load data in right channel (mono mode) */
I2S_CHANNEL_FMT_ONLY_LEFT, /*!< Only load data in left channel (mono mode) */
#if SOC_I2S_SUPPORTS_TDM
// Multiple channels are available with TDM feature
I2S_CHANNEL_FMT_MULTIPLE, /*!< More than two channels are used */
#endif
} i2s_channel_fmt_t;
通信传输格式communication_format
typedef enum {
I2S_COMM_FORMAT_STAND_I2S = 0X01, /*!< I2S communication I2S Philips standard, data launch at second BCK*/
I2S_COMM_FORMAT_STAND_MSB = 0X02, /*!< I2S communication MSB alignment standard, data launch at first BCK*/
I2S_COMM_FORMAT_STAND_PCM_SHORT = 0x04, /*!< PCM Short standard, also known as DSP mode. The period of synchronization signal (WS) is 1 bck cycle.*/
I2S_COMM_FORMAT_STAND_PCM_LONG = 0x0C, /*!< PCM Long standard. The period of synchronization signal (WS) is channel_bit*bck cycles.*/
I2S_COMM_FORMAT_STAND_MAX, /*!< standard max*/
//old definition will be removed in the future.
I2S_COMM_FORMAT_I2S __attribute__((deprecated)) = 0x01, /*!< I2S communication format I2S, correspond to `I2S_COMM_FORMAT_STAND_I2S`*/
I2S_COMM_FORMAT_I2S_MSB __attribute__((deprecated)) = 0x01, /*!< I2S format MSB, (I2S_COMM_FORMAT_I2S |I2S_COMM_FORMAT_I2S_MSB) correspond to `I2S_COMM_FORMAT_STAND_I2S`*/
I2S_COMM_FORMAT_I2S_LSB __attribute__((deprecated)) = 0x02, /*!< I2S format LSB, (I2S_COMM_FORMAT_I2S |I2S_COMM_FORMAT_I2S_LSB) correspond to `I2S_COMM_FORMAT_STAND_MSB`*/
I2S_COMM_FORMAT_PCM __attribute__((deprecated)) = 0x04, /*!< I2S communication format PCM, correspond to `I2S_COMM_FORMAT_STAND_PCM_SHORT`*/
I2S_COMM_FORMAT_PCM_SHORT __attribute__((deprecated)) = 0x04, /*!< PCM Short, (I2S_COMM_FORMAT_PCM | I2S_COMM_FORMAT_PCM_SHORT) correspond to `I2S_COMM_FORMAT_STAND_PCM_SHORT`*/
I2S_COMM_FORMAT_PCM_LONG __attribute__((deprecated)) = 0x08, /*!< PCM Long, (I2S_COMM_FORMAT_PCM | I2S_COMM_FORMAT_PCM_LONG) correspond to `I2S_COMM_FORMAT_STAND_PCM_LONG`*/
} i2s_comm_format_t;
知道了每个参数的含义以及知道它可以配置哪些参数,就可以调用i2s_driver_install这个函数了。
这里我们举一个安装I2S驱动的例子,就比较容易理解了。同时配置的时候,我们把它放在一个函数里面,起名为i2s_install( )。
// 安装I2S驱动
void i2s_install(){
// 配置I2S接收
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
.sample_rate = SAMPLE_RATE,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
.communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB),
.intr_alloc_flags = 0,
.dma_buf_count = 16,
.dma_buf_len = bufferLen,
.use_apll = false
};
if (ESP_OK != i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL)) {
Serial.println("Install I2S driver failed");
return;
}
}
配置I2S引脚
I2S通信最重要的三个信号是位时钟BCK、字时钟WS、数据引脚SD,因此我们需要对其引脚进行配置,设置I2S引脚的函数是
esp_err_t i2s_set_pin(i2s_port_t i2s_num, const i2s_pin_config_t *pin)
,第一个参数传入I2S端口,填I2S_NUM_0或I2S_NUM_1, 第二个参数是结构体如下:
typedef struct {
int mck_io_num; /*!< MCK in out pin. Note that ESP32 supports setting MCK on GPIO0/GPIO1/GPIO3 only*/
int bck_io_num; /*!< BCK in out pin*/
int ws_io_num; /*!< WS in out pin*/
int data_out_num; /*!< DATA out pin*/
int data_in_num; /*!< DATA in pin*/
} i2s_pin_config_t;
其中mck_io_num; bck_io_num,ws_io_num等都是整型变量,data_out_num如果我们没有用到就传入-1,在driver/i2s.h头文件定义了
#define I2S_PIN_NO_CHANGE (-1) /*!< Use in i2s_pin_config_t for pins which should not be changed */
假设我们要配置的引脚位时钟BCK、字时钟WS、数据引脚SD分别是D13, D12, D14,同样把要配置的参数写入一个函数i2s_setpin()里面,配置I2S引脚示例如下:
// 配置I2S引脚
#define I2S_MIC_WS 12
#define I2S_MIC_SD 14
#define I2S_MIC_BCK 13
void i2s_setpin(){
i2s_pin_config_t pin_config = {};
pin_config.bck_io_num = I2S_MIC_BCK;
pin_config.ws_io_num = I2S_MIC_WS;
pin_config.data_out_num = I2S_PIN_NO_CHANGE;
pin_config.data_in_num = I2S_MIC_SD;
if (ESP_OK != i2s_set_pin(I2S_PORT, &pin_config)) {
Serial.println("I2S set pin failed");
return;
}
}
安装完I2S驱动和配置好I2S引脚后,我们只要在setup()函数里面调用这两个函数就可以了。
void setup() {
Serial.begin(115200);
Serial.println("Setup I2S ...");
delay(1000);
i2s_install();
i2s_setpin();
delay(500);
}
读取I2S数据
上面只是对I2S进行了一下初始化,要通过INMP441读取i2s数据,我们只需要调用esp_err_t i2s_read(i2s_port_t i2s_num, void *dest, size_t size, size_t *bytes_read, TickType_t ticks_to_wait);
这个函数。因为前面在初始化I2S时的量化位数是16位,所以每个采样点的数据大小为2字节,我们将读取到的数据放入一个缓存区数组sBuffer,将数组长度bufferLen定义为64,确保每次从I2S接口读取时能获得足够的音频数据。 如果读取成功2s_read这个函数会返回一个ESP_OK,成功后就进入数据处理部分。这里我们将读取到的音频数据求均值然后可以用串口绘图仪观察它的数据波形,代码如下:
void loop() {
size_t bytesIn = 0;
esp_err_t result = i2s_read(I2S_PORT, &sBuffer, bufferLen, &bytesIn, portMAX_DELAY);
if (result == ESP_OK)
{
int samples_read = bytesIn / 2;
if (samples_read > 0) {
float mean = 0;
for (int i = 0; i < samples_read; ++i) {
mean += (sBuffer[i]);
}
mean /= samples_read;
Serial.println(mean);
delay(50);
}
}
}
完整代码
#include "driver/i2s.h"
#define SAMPLE_RATE (44100)
#define I2S_MIC_WS 12
#define I2S_MIC_SD 14
#define I2S_MIC_BCK 13
#define I2S_PORT I2S_NUM_0
#define bufferLen 64
int16_t sBuffer[bufferLen];
// 安装I2S驱动
void i2s_install(){
// 配置I2S接收
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
.sample_rate = SAMPLE_RATE,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
.communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB),
.intr_alloc_flags = 0,
.dma_buf_count = 16,
.dma_buf_len = bufferLen,
.use_apll = false
};
if (ESP_OK != i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL)) {
Serial.println("Install I2S driver failed");
return;
}
}
// 配置I2S引脚
void i2s_setpin(){
i2s_pin_config_t pin_config = {};
pin_config.bck_io_num = I2S_MIC_BCK;
pin_config.ws_io_num = I2S_MIC_WS;
pin_config.data_out_num = I2S_PIN_NO_CHANGE;
pin_config.data_in_num = I2S_MIC_SD;
if (ESP_OK != i2s_set_pin(I2S_PORT, &pin_config)) {
Serial.println("I2S set pin failed");
return;
}
}
void setup() {
Serial.begin(115200);
Serial.println("Setup I2S ...");
delay(1000);
i2s_install();
i2s_setpin();
i2s_start(I2S_PORT);
delay(500);
}
void loop() {
size_t bytesIn = 0;
esp_err_t result = i2s_read(I2S_PORT, &sBuffer, bufferLen, &bytesIn, portMAX_DELAY);
if (result == ESP_OK)
{
int samples_read = bytesIn / 2;
if (samples_read > 0) {
float mean = 0;
for (int i = 0; i < samples_read; ++i) {
mean += (sBuffer[i]);
}
mean /= samples_read;
Serial.println(mean);
delay(50);
}
}
}
接线图
写完代码后就可以开始接线了,接线图如下图所示:供电这里接3.3V,L/R接地。
实验效果
一开始接收到的是外界的声音,波形是杂乱无章的。后面用嘴吹气,波形会跟着吹气变化,不吹气波形是平缓不变的,后面大概吹了几次,可以看到波形变化,如下图:
总结
本篇文章介绍了通过ESP32的I2S通信实时读取INMP441麦克风模块的音频数据,并通过串口绘图仪显示音频数据波形。后面我们还会介绍使用INMP441采集音频并实时播放音频,感兴趣的可以先关注收藏一下!