使用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和L\R:
    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 <stdio.h>
    //#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("%d\r\n",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 <stdio.h>
    /* 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("%d\r\n",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音频数据的采集,并将数据化为了有符号整型格式,以便进一步处理。

    平时比较忙,不常看消息,评论区回得慢或者没回,请见谅!

    物联沃分享整理
    物联沃-IOTWORD物联网 » 使用STM32的I2S协议读取INMP441麦克风

    发表评论