网络知识 娱乐 使用STM32的I2S协议读取麦克风INMP441

使用STM32的I2S协议读取麦克风INMP441

目录

  • 介绍
  • 事前准备
    • 硬件准备
    • 软件准备
  • 麦克风模块的I2S通信原理
  • 使用Cube设置STM32的I2S通信
    • 电路连接
  • 编写代码
  • 在串口绘图器中查看音频波形

介绍

本文将详细介绍使用STM32的硬件I2S协议,从麦克风模块INMP441读取音频信号的步骤和避坑指南。

点这里,从Github下载工程文件

百度网盘:
链接:https://pan.baidu.com/s/1cglAGirn6bTQhIEpWDb7Ig?pwd=k9gh
提取码:k9gh

事前准备

硬件准备

  1. INMP441模块: 在这里插入图片描述

  2. 主控芯片:我用的是STM32F103ZET6,可以在STM的选型手册上看到哪些芯片带I2S接口,支持I2S协议的最低配STM32芯片是STM32F103R系列。

  3. 用来下载程序的Jlink-OB和用于串口通信的USB转TTL模块。

软件准备

  1. 本文采用STM32CubeMX生成初始代码 + KeilV5编程的方式,开发STM32程序。
  2. 使用Cube生成一个Keil v5 Project,并调好printf串口打印功能,方便后续开发。这里需读者具备开发STM32程序的基本能力。

麦克风模块的I2S通信原理

麦克风模块本质上是一个模数转换器(ADC),不断的把声波震动的幅值转换成数字信号,再通过I2S总线把数据发送到主控芯片。本案例中,I2S总线的主机是STM32,从机是麦克风模块。

除了电源引脚和芯片使能引脚,INMP441的引脚还包括SCK、SD、WS和LR:
SCK:I2S时钟线,是由主机产生的高频方波,用来控制每位数据的传输时序
SD:I2S数据线,从机通过这根线把AD采样值发送给主机
WS:I2S声道选择线,I2S协议可以传输左右两个声道的数据,WS信号是由主机发送给从机的,从机根据WS的电平高低,判断当前数据帧发送左声道还是右声道数据,WS低电平时,从机发送左声道数据,高电平发送右声道。
L/R:芯片左右声道选择线,每个麦克风只能检测一处声源,因此若要进行双声道录音,就要使用两个模块,一左一右放置,然后按照下图连线:
双声道音频采集电路
为了让麦克风知道自己是在左边还是右边,就需要将L/R置高(右声道)或置低(左声道)。以上图为例,左边麦克风的L/R接地了,因此在WS=0期间它会将自身的AD采样值发送到SD线,发送完成后它会将SD设置为高阻态(相当于悬空),让出SD线。随后WS=1时,右边的麦克风通过SD线发送右声道数据。因此主机是交替采集左右声道的数据,且不会发生信号冲突。下图是麦克风的I2S时序图:
INMP441的I2S时序图
如上图,SCK是由主机生成的高频方波信号,WS的周期是SCK的64倍,因此在WS=1或WS=0的期间,SCK会经历32个周期。从WS下降沿之后的第2个SCK上升沿开始,主机会在每个SCK上升沿读取一位数据,直到第25个SCK上升沿后,主机共读取到了24bit数据。INMP441内置的ADC是24位的,这24bit数据以大端模式传输,组成了左声道的一个采样值。同理,从WS上升沿之后的第2个SCK上升沿开始,主机开始读取右声道的采样值。下图是来自STM32手册的I2S时序图:
STM32的I2S时序图
由上图可知,STM32也是从第2个SCK上升沿开始读取数据,与麦克风的协议一致。在WS=0期间CLK共出现32次上升沿,且读取动作滞后了一个SCK周期,导致WS=0期间至多读取31bit数据,因此如果采样值是32bit的,则需要在下一次读取时第一个SCK上升沿时完成最后一位的读取。这解释了上图中,为何MSB receive的左边一位是LSB receive,这是上个数据的最低位。

下图是使用示波器采集到的I2S波形实例。蓝色是SD信号,黄色是CLK,示波器触发源被设置为WS下降沿,图中波形对应一个完整的24位数值。可以从图中Ch2第二个上升沿开始读取24位数据。麦克风发出的24位采样值是带符号的,因此下面第一张图中,最高是1,代表这个数值是负数,第二张图中的采样值是正数。
在这里插入图片描述
在这里插入图片描述

使用Cube设置STM32的I2S通信

以上是对I2S通信原理的解释,下面开始创建STM32程序。打开Cube,按照下图设置:
使用Cube设置STM32的I2S通信
使用Cube设置STM32的I2S通信
上图中,8kHz频率是最低选项,这是为了方便测试,可以调通后再提高。添加DMA以提高数据存取速度,DMA Mode选择Circular,这样STM32会连续不断的进行DMA存取操作,不必频繁调用读取命令。Data With暂时选择Word,也就是32位的。

在Cube中设置完成后,点击生成代码,然后在Keil中打开Project。
在这里插入图片描述
可以看到在Keil中,出现了i2s的库文件stm32f1xx_hal_i2s.c和初始化文件i2s.c,其中stm32f1xx_hal_i2s.c内有各个i2s的驱动函数和使用说明,i2s.c中是Cube根据上面的设置,生成的I2S通信初始化函数。

电路连接

在Cube上查看STM32的引脚分配如下:
在这里插入图片描述
本次案例不需双声道,只接一个麦克风,按照下图接线:
单声道音频采集接线
如上图,建议给SD线接一个10k的下拉电阻(模块本身是不下拉的)。这是因为,在每个24bit数据传输完成后,麦克风会立即将SD线设为高阻态,此时如果没有下拉,SD线会缓慢衰减到0V(如左下图所示),导致STM32会在第25bit读取到一个“1”,对开发工作造成误导。 右下图是不下拉时读取到的数据,图中数据最后的16进制“80”即二进制的1000 0000,这个1是多余的。
在这里插入图片描述在这里插入图片描述
物理连线:
在这里插入图片描述

编写代码

打开main.c,自己添加的代码都要放在Cube注释指定的位置,否则再次用Cube生成代码时会被覆盖。先放上连续的main.c文件(省略了最后一些自动生成的代码),下文会对各个需要添加代码的地方进行说明。

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2022 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "dma.h"
#include "i2s.h"
#include "usart.h"
#include "gpio.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include 
//#include "stm32f1xx_hal.h"
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */
uint32_t dma[4];
uint32_t val24;
int val32;
/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
unsigned cb_cnt=0;
//I2S接收完成回调函数
void HAL_I2S_RxCpltCallback(I2S_HandleTypeDef *hi2s)
{
	if(hi2s==&hi2s2){
		cb_cnt++;//回调次数计数
		//将两个32整型合并为一个
		//dat32 example: 0000fffb 00004f00
		val24=(dma[0]<<8)+(dma[1]>>8);
		//将24位有符号整型扩展到32位
		if(val24 & 0x800000){//negative
			val32=0xff000000 | val24;
		}else{//positive
			val32=val24;
		}
		//以采样频率的十分之一,串口发送采样值
		if(cb_cnt%10==0)
			printf("%drn",val32);
	}
}
/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  MX_DMA_Init();
  MX_I2S2_Init();
  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  //开启DMA传输
  HAL_I2S_Receive_DMA(&hi2s2,(uint16_t*)dma,4);	
  while (1)
  {
  	HAL_Delay(10);
	
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}
//后面都是Cube自动生成的代码,省略

以下是详细介绍:
加入stdio.h文件,是为了避免printf函数出现警告。仅添加这一行代码不能实现printf串口输出功能,具体操作可以去网上搜索。

/* USER CODE BEGIN Includes */
#include 
/* USER CODE END Includes */

添加全局变量,数组dma[]是一个缓冲区,芯片将硬件I2S读取到的数据通过DMA传送到这个数组,无须主程序的干预。val24是用来存储24位原始采样值的。val32是将val24扩展到32位得到的结果。具体数据转换逻辑见下文。

/* USER CODE BEGIN PV */
uint32_t dma[4];
uint32_t val24;
int val32;
/* USER CODE END PV */

在main函数前,添加I2S接收回调函数。在回调函数之前定义了全局变量cb_cnt,用来计算回调函数被执行的次数。回调函数中的代码是把原始的采样数据转换为32位有符号整型,然后以十分之一的采样速率,把转换结果发送到串口,用来在上位机的串口绘图器上观察波形。

/* USER CODE BEGIN 0 */
unsigned cb_cnt=0;
//I2S接收完成回调函数
void HAL_I2S_RxCpltCallback(I2S_HandleTypeDef *hi2s)
{
	if(hi2s==&hi2s2){
		cb_cnt++;//回调次数计数
		//将两个32整型合并为一个
		//dat32 example: 0000fffb 00004f00
		val24=(dma[0]<<8)+(dma[1]>>8);
		//将24位有符号整型扩展到32位
		if(val24 & 0x800000){//negative
			val32=0xff000000 | val24;
		}else{//positive
			val32=val24;
		}
		//以采样频率的十分之一,串口发送采样值
		if(cb_cnt%10==0)
			printf("%drn",val32);
	}
}
/* USER CODE END 0 */

主函数中,仅需在while循环前加了一个函数HAL_I2S_Receive_DMA(),以开启DMA传输:

  HAL_I2S_Receive_DMA(&hi2s2,(uint16_t*)dma,4);	

由于前面将DMA Mode设置为了Circular,因此只需要调用一次该函数,就能开启DMA的连续存取。该函数的原文注释如下:

/**
  * @brief  Receive an amount of data in non-blocking mode with DMA
  * @param  hi2s pointer to a I2S_HandleTypeDef structure that contains
  *         the configuration information for I2S module
  * @param  pData a 16-bit pointer to the Receive data buffer.
  * @param  Size number of data sample to be sent:
  * @note   When a 16-bit data frame or a 16-bit data frame extended is selected during the I2S
  *         configuration phase, the Size parameter means the number of 16-bit data length
  *         in the transaction and when a 24-bit data frame or a 32-bit data frame is selected
  *         the Size parameter means the number of 24-bit or 32-bit data length.
  * @note   The I2S is kept enabled at the end of transaction to avoid the clock de-synchronization
  *         between Master and Slave(example: audio streaming).
  * @retval HAL status
  */
HAL_StatusTypeDef HAL_I2S_Receive_DMA(I2S_HandleTypeDef *hi2s, uint16_t *pData, uint16_t Size)

根据以上说明,最后一个参数size的单位是由数据帧的长度决定的。前面在Cube中设置的数据格式为24 Bits Data on 32 Bits Frame,因此DMA读取数据的总长度为size×4字节。前面定义的DMA缓冲区是一个长度为4的uint32_t型数组,缓冲区中的数据格式为:前2个元素表示左声道数据,后2个表示右声道数据。根据前面的电路图,本次试验只接了一个左声道的麦克风,因此右声道数据始终为0.以下是在数组dma[]中,左声道数据的示例:
在这里插入图片描述
可以观察出,dma[0]左移8位 + dma[1]右移8位,就得到了24位采样值。例如将0000fff5 00007700合并为0xfff577:

val24 = (buf32[0]<<8) + (buf32[1]>>8);

在这个DMA数据获取的阶段非常容易出错,最终的数据格式和在Cube中设置的DMA Data With、main.c中DMA缓冲数组的元素类型和DMA接收函数的参数值size都有关系,在我之前的测试中,如果把缓冲区元素转为uint8_t,就会得到不一样的结果,24bit采样值被拆分为3个字节,且未必是高位字节在前、低位字节在后的排列顺序。总之需要小心修改,通过把数据打印到串口,来寻找规律。按照本文的配置,接收到的原始数据是用2个32位无符号整型表示一个采样值,这应该不是最佳的格式,但我没有继续试验,读者可以尝试在Cube中把Data With设为Half-Word再进行测试。

麦克风发送的数值是24位二进制补码的形式,二进制补码也是单片机存储有符号整型的方式,因此可以将采样值视为24位有符号整型,为了与C语言的整型格式一致,需要将这个24位整型扩展到32位,方法为:正数保持不变,对于负数在左边加一个0xff。例如24位数0xfff577,它的最高位(符号位)是1,代表负数,因此转换为0xffff f577,代码如下:

		if(val24 & 0x800000){//negative
			val32=0xff000000 | val24;
		}else{//positive
			val32=val24;
		}

在串口绘图器中查看音频波形

使用串口绘图工具SerialPlot查看声音波形,SerialPlot相当于一个带波形绘制功能的串口助手,它的下载和使用方法可以到网上搜到。由于声音采样速度比较快,在上面回调函数中我把串口发送频率设成了采样频率的十分之一。
在这里插入图片描述
可在图中看到波形随着外界声音而波动,声音越响幅值越大,这说明采集到的数据是正常的。

至此实验完成。本文使用STM32的硬件I2S协议和DMA功能实现了对INMP441音频数据的采集,并将数据化为了有符号整型格式,以便进一步处理。