介绍
本文将详细介绍使用STM32的硬件I2S协议,从麦克风模块INMP441读取音频信号的步骤和避坑指南。
百度网盘:
链接:https://pan.baidu.com/s/1cglAGirn6bTQhIEpWDb7Ig?pwd=k9gh
提取码:k9gh
也可参考我后续文章,把声音数据TXT转换成WAV:Python把TXT转WAV
事前准备
硬件准备
-
INMP441模块:
-
主控芯片:我用的是STM32F103ZET6,可以在STM的选型手册上看到哪些芯片带I2S接口,支持I2S协议的最低配STM32芯片是STM32F103R系列。
-
用来下载程序的Jlink-OB和用于串口通信的USB转TTL模块。
软件准备
- 本文采用STM32CubeMX生成初始代码 + KeilV5编程的方式,开发STM32程序。
- 使用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时序图:
如上图,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也是从第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,按照下图设置:
上图中,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音频数据的采集,并将数据化为了有符号整型格式,以便进一步处理。
平时比较忙,不常看消息,评论区回得慢或者没回,请见谅!