打印
[其他ST产品]

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

[复制链接]
2942|29
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
硬件准备
INMP441模块:

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

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

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

使用特权

评论回复
沙发
个百zz分点个|  楼主 | 2024-6-30 17:59 | 只看该作者
麦克风模块的I2S通信原理
麦克风模块本质上是一个模数转换器(ADC),不断的把声波震动的幅值转换成数字信号,再通过I2S总线把数据发送到主控芯片。本案例中,I2S总线的主机是STM32,从机是麦克风模块。

除了电源引脚和芯片使能引脚,INMP441的引脚还包括SCK、SD、WS和L\R:
SCK:I2S时钟线,是由主机产生的高频方波,用来控制每位数据的传输时序
SD:I2S数据线,从机通过这根线把AD采样值发送给主机
WS:I2S声道选择线,I2S协议可以传输左右两个声道的数据,WS信号是由主机发送给从机的,从机根据WS的电平高低,判断当前数据帧发送左声道还是右声道数据,WS低电平时,从机发送左声道数据,高电平发送右声道。
L/R:芯片左右声道选择线,每个麦克风只能检测一处声源,因此若要进行双声道录音,就要使用两个模块,一左一右放置,然后按照下图连线:

使用特权

评论回复
板凳
个百zz分点个|  楼主 | 2024-6-30 17:59 | 只看该作者
为了让麦克风知道自己是在左边还是右边,就需要将L/R置高(右声道)或置低(左声道)。以上图为例,左边麦克风的L/R接地了,因此在WS=0期间它会将自身的AD采样值发送到SD线,发送完成后它会将SD设置为高阻态(相当于悬空),让出SD线。随后WS=1时,右边的麦克风通过SD线发送右声道数据。因此主机是交替采集左右声道的数据,且不会发生信号冲突。下图是麦克风的I2S时序图:


使用特权

评论回复
地板
个百zz分点个|  楼主 | 2024-6-30 18:00 | 只看该作者
如上图,SCK是由主机生成的高频方波信号,WS的周期是SCK的64倍,因此在WS=1或WS=0的期间,SCK会经历32个周期。从WS下降沿之后的第2个SCK上升沿开始,主机会在每个SCK上升沿读取一位数据,直到第25个SCK上升沿后,主机共读取到了24bit数据。INMP441内置的ADC是24位的,这24bit数据以大端模式传输,组成了左声道的一个采样值。

使用特权

评论回复
5
个百zz分点个|  楼主 | 2024-6-30 18:01 | 只看该作者
同理,从WS上升沿之后的第2个SCK上升沿开始,主机开始读取右声道的采样值。下图是来自STM32手册的I2S时序图:

使用特权

评论回复
6
个百zz分点个|  楼主 | 2024-6-30 18:02 | 只看该作者
由上图可知,STM32也是从第2个SCK上升沿开始读取数据,与麦克风的协议一致。在WS=0期间CLK共出现32次上升沿,且读取动作滞后了一个SCK周期,导致WS=0期间至多读取31bit数据,因此如果采样值是32bit的,则需要在下一次读取时第一个SCK上升沿时完成最后一位的读取。这解释了上图中,为何MSB receive的左边一位是LSB receive,这是上个数据的最低位。

使用特权

评论回复
7
个百zz分点个|  楼主 | 2024-6-30 18:03 | 只看该作者
下图是使用示波器采集到的I2S波形实例。蓝色是SD信号,黄色是CLK,示波器触发源被设置为WS下降沿,图中波形对应一个完整的24位数值。可以从图中Ch2第二个上升沿开始读取24位数据。麦克风发出的24位采样值是带符号的,因此下面第一张图中,最高是1,代表这个数值是负数,第二张图中的采样值是正数。

使用特权

评论回复
8
个百zz分点个|  楼主 | 2024-6-30 18:03 | 只看该作者

使用特权

评论回复
9
个百zz分点个|  楼主 | 2024-6-30 18:04 | 只看该作者
使用Cube设置STM32的I2S通信
以上是对I2S通信原理的解释,下面开始创建STM32程序。打开Cube,按照下图设置:

使用特权

评论回复
10
个百zz分点个|  楼主 | 2024-6-30 18:04 | 只看该作者
上图中,8kHz频率是最低选项,这是为了方便测试,可以调通后再提高。添加DMA以提高数据存取速度,DMA Mode选择Circular,这样STM32会连续不断的进行DMA存取操作,不必频繁调用读取命令。Data With暂时选择Word,也就是32位的。

使用特权

评论回复
11
个百zz分点个|  楼主 | 2024-6-30 18:04 | 只看该作者
在Cube中设置完成后,点击生成代码,然后在Keil中打开Project。

可以看到在Keil中,出现了i2s的库文件stm32f1xx_hal_i2s.c和初始化文件i2s.c,其中stm32f1xx_hal_i2s.c内有各个i2s的驱动函数和使用说明,i2s.c中是Cube根据上面的设置,生成的I2S通信初始化函数。

使用特权

评论回复
12
个百zz分点个|  楼主 | 2024-6-30 18:14 | 只看该作者
电路连接
在Cube上查看STM32的引脚分配如下:

使用特权

评论回复
13
个百zz分点个|  楼主 | 2024-6-30 18:14 | 只看该作者
本次案例不需双声道,只接一个麦克风,按照下图接线:

使用特权

评论回复
14
个百zz分点个|  楼主 | 2024-6-30 18:15 | 只看该作者
如上图,建议给SD线接一个10k的下拉电阻(模块本身是不下拉的)。这是因为,在每个24bit数据传输完成后,麦克风会立即将SD线设为高阻态,此时如果没有下拉,SD线会缓慢衰减到0V(如左下图所示),导致STM32会在第25bit读取到一个“1”,对开发工作造成误导。 右下图是不下拉时读取到的数据,图中数据最后的16进制“80”即二进制的1000 0000,这个1是多余的。

使用特权

评论回复
15
个百zz分点个|  楼主 | 2024-6-30 18:15 | 只看该作者
物理连线:

使用特权

评论回复
16
个百zz分点个|  楼主 | 2024-6-30 18:18 | 只看该作者
编写代码
打开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自动生成的代码,省略

使用特权

评论回复
17
个百zz分点个|  楼主 | 2024-6-30 18:18 | 只看该作者
以下是详细介绍:
加入stdio.h文件,是为了避免printf函数出现警告。仅添加这一行代码不能实现printf串口输出功能,具体操作可以去网上搜索。

/* USER CODE BEGIN Includes */
#include <stdio.h>
/* USER CODE END Includes */

使用特权

评论回复
18
个百zz分点个|  楼主 | 2024-6-30 18:18 | 只看该作者
添加全局变量,数组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 */

使用特权

评论回复
19
个百zz分点个|  楼主 | 2024-6-30 18:18 | 只看该作者
在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 */

使用特权

评论回复
20
个百zz分点个|  楼主 | 2024-6-30 18:18 | 只看该作者
主函数中,仅需在while循环前加了一个函数HAL_I2S_Receive_DMA(),以开启DMA传输:

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

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

50

主题

631

帖子

0

粉丝