I2S音频开发(使用USB音频进行验证)
一、硬件平台
STM32F407VGT6(立创天空星)、INMP441(I2S麦克风)
二、软件平台
STM32hal库、STM32USB中间件(用于开发USB麦克风)
三、音频接口与协议科普
音频编解码器_百度百科https://baike.baidu.com/item/%E9%9F%B3%E9%A2%91%E7%BC%96%E8%A7%A3%E7%A0%81%E5%99%A8/482649?fromModule=search-result_lemma
1.音频编码类型
脉冲编码调制(Pulse-code modulation,简称PCM)
脉冲密度调制(Pulse-density modulation,简称PDM)
脉冲强度调制(Pulse-Amplitude Modulation,简称PAM)
常见的音频接口传输的大多都是PCM或者PDM数据。
音视频工程场景中,我们常处理的音频信号,基本为 PCM 方式获取的数字信号。 对于想要进行调整的 PDM 数字信号,通常需要转换为 PCM 数字信号后,再行以 PCM 更具优势的直接编辑方式,进行相关操作。而位于计算机体系内用来实现音频存储的数字信号基础类型,亦为 PCM 类型的数字信号。
由此可见 PCM 数字信号的重要性。
原文链接:1.5.4 脉冲编码调制(PCM)& 脉冲密度调制(PDM) · 《音视频开发技术:原理与实践》©https://arikanli.cyberfederal.io/Chapter_1/Language/cn/Docs_1_5_4.html
2.音频接口分类
主要有以下几种
I2S、PCM、PDM等
I2S、PCM和PDM等数字音频接口介绍 - 深圳市矽源特科技有限公司https://www.chipsourcetek.com/Technology/3577.html数字音频接口 - ricks - 博客园
https://www.cnblogs.com/ricks/p/9451269.html
MEMS 麦克风 - PDM 与 I²S | DigiKeyhttps://www.digikey.cn/zh/articles/a-comparison-of-digital-pdm-and-i2s-interfaces-in-mems-microphones
3.音频编码与音频接口的对应关系
I2S接口支持的音频协议一般有(以STM32为例):I2S Philips 标准、MSB 对齐标准、LSB 对齐标准、PCM 标准。
其中I2S Philips 标准与PCM 标准虽然标准不一样但是传输的都是PCM数据,而LSB 对齐标准与MSB对齐标准不是太清楚,MSB标准好像是用来读取PDM数据的(根据ST下面这个应用手册看到的)
Interfacing PDM digital microphones using STM32 MCUs and MPUs - Application notehttps://www.st.com/resource/zh/application_note/an5027-interfacing-pdm-digital-microphones-using-stm32-mcus-and-mpus-stmicroelectronics.pdf
4.常见的MEMS麦克风种类
I2S麦克风结构框图(例如淘宝常见的INMP441)

PDM麦克风结构框图(例如ST的MP34DT05-A )

根据上面的对比图可以看出PDM麦克风只进行了PDM调制,而I2S麦克风通过内置波滤器来利用内部编解码器进行了I2S编码(编码后传输的数据其实就是PCM数据)
5.PDM数据转换为PCM数据方案
那么如何将PDM数据转化为PCM数据呢?
STM32提供了两种解决方案
方案一:PDM音频软件解码库(即PDM2PCM)


方案二:使用DFSDM外设滤波器

6.总结
总结下来就是IIS仅仅是PCM的一个分支,接口定义都是一样的。PCM接口和I2S接口传输的都是编码后的PCM数据,但是PCM接口更灵活。目标处理器不需要进行任何处理就可以直接使用得到的音频数据,而PDM接口由于传输的是PDM数据,还需要目标处理器将其转化为PCM数据才能正常使用。
四、I2S设备驱动设置
使用ST的CUBEMX进行设备驱动配置,以INMP441(I2S麦克风)为例。
第一步:选择目标芯片
STM32F407VGT6
第二步:基础配置
1.开启RCC的HSE时钟
2.根据板载的高速晶振设置时钟树界面的HSE时钟输入值。
3.调试配置选择SWD,其他默认。
第三步:开启I2S外设并配置
上文中提到了STM32的I2S接口支持四种模式,那么INMP441支持哪种模式呢?
可以查看它的数据手册,
INMP441 Datasheet by TDK InvenSense | Digi-Key Electronicshttps://www.digikey.cn/htmldatasheets/production/1431884/0/0/1/inmp441-datasheet.html其中提到INMP441采用的是业内标准 24 位 I²S 接口,默认数据格式为I²S(二进制补码),MSB优先。在这种格式中,每个字的MSB从每个半帧的开始延迟一个SCK周期。由于I2S是支持双声道的的所以对于I2S接口有一根线为WS用于声道选择,默认左声道先发送。而本文的目的是验证麦克风的功能对于声道数没有要求所以仅选择了左声道简化过程。
这里需要说明的是对于I2S标准不论使用单声道还是双声道,均需要左右声道交替采样。也就意味着即使只需要左声道的数据,也要在右声道数据采样期间进行等待而不能跳过。
数据手册中也有说明:
输出数据字长为每通道24位。INMP441的每个立体声数据字必须始终具有64个时钟周期。
至于为什么是64,而不是48,是因为 24 位数据被封装在 32 位帧中

以左声道为例,时序图如下
而STM32的I2S接口的I2S飞利浦协议时序图如下
可以看到STM32的时序图符合INMP441数据手册中的时序图:每个字的MSB从每个半帧的开始延迟一个SCK周期。
接下来就是实际配置
这里设置的目标频率为16khz,由于后面验证是是在电脑端验证所以要设置为电脑端支持的音频频率,此处的音频频率是指其支持1S生成16000个双声道的PCM数据
第四步:I2S的DMA配置
至于为什么需要使用DMA配置而不是直接阻塞接收或者中断接收,ST官方在stm32f4xx_hal_i2s.c文件中的HAL_I2S_Receive_IT这个函数上面的提示中写道
* @note It is recommended to use DMA for the I2S receiver to avoid de-synchronization
* between Master and Slave otherwise the I2S interrupt should be optimized.
大致意思就是:建议在I2S接收端使用DMA方式避免主从端不同步,否则需要优化I2S中断。
看来ST官方也觉得hal库对于I2S来说过于臃肿了,既然ST官方已经提示我们了
所以我们就选择使用DMA来处理这个问题吧
1.循环模式
选择循环模式是因为I2S接口接收麦克风的数据是不需要软件干什么的,不像别的器件那样接收的时候还要各种配置,像这种直来直去的数据传递,DMA是最擅长的了。由于这个数据的特点主要是量大,频率高,且持续时间一般较长。使用循环模式可以让硬件只在需要取数据的时候通知用户,其他时间自行运行。也不用用户总是要在中断中重启DMA传输。
2.数据宽度
虽然前文中提到24 位数据被封装在 32 位帧中的,但是实际获取时并非能够直接获取到32位的数据,老样子还是先查看STM32F4的参考手册,可以看到I2S外设是如何读取32位帧中的24位数据的
当使用 32 位数据包中的 16 位数据时,前 16 位 (MSB) 为有效位, 16 位 LSB 被强制清零,
无需任何软件操作或 DMA 请求(只需一个读/写操作)。
如果应用程序首选 DMA,则 24 位和 32 位数据帧需要对 SPI_DR 执行两次 CPU 读取或写
入操作,或者需要两次 DMA 操作。 24 位的数据帧,硬件会将 8 位非有效位扩展到带有 0 位
的 32 位。
所以我们需要将DMA的数据宽度设置为半字,这样的话STM32可以通过两次DMA操作来对24bit有效位的32bit帧进行读取,如果选择全字则会导致一种情况即每次读取数据需要以字的宽度读取两次,这样一来显而易见多做了一倍的无用功。
3.FIFO配置
通过参考手册的DMA章节,我们可以了解到DMA传输,如果没有使用FIFO配置(FIFO 的阈值级别控制)每完成一次从外设到 FIFO 的数据传输后,相应的数据立即就会移出并存储到目标中。
体现到代码中就是DMA读取一次麦克风的PCM数据(32位帧)就要触发一个DMA接收完成中断。提醒用户及时取走数据。
那如果使用FIFO 的阈值级别控制呢,则可以等待DMA将FIFO填满,然后再通知用户取走数据
那么FIFO总共多大
每个数据流都有一个独立的 4 字 FIFO,阈值级别可由软件配置为 1/4、 1/2、 3/4 或满。
所以我们如果将阈值级别设置为满则我们只需要在DMA读取四次麦克风的PCM数据后响应中断来读取数据即可,中断的触发次数直接减少了75%。
这里需要注意中断回调函数中不要进行耗时操作,避免两次的数据重叠。
因为DMA接收完成中断触发时,读取到的数据就已经在用户自定义的缓冲区中了,此时DMA已经在自动进行后续的数据接收了。(因为我们前面设置的DMA为循环模式,所以会自动执行)如果中断回调函数中进行了耗时操作,则可能在用户还没有将前一次的数据取走时,DMA已经使用下一次的数据覆盖掉了前一次的数据。
如果还想提高数据接收速度,STM32的DMA也支持硬件双缓冲模式,可以自行实现。(hal库中I2S没有使用DMA双缓冲的函数)
4.DMA中断
默认开启不用管
五、USB的音频设备配置
开始
第一步:USB设备开启
模式选择为仅设备,其他默认
第二步:USB设备中间件配置

此处的接口麦克风接口是新添加的,另外两个暂时没有删除
然后生成工程。
六、音频数据流转处理
1.数据流转

主要是借助STM32的I2S接口从INMP441读取PCM数据,然后再把数据通过USB接口传输给电脑
2.STM32内部数据处理
由于I2S实时数据量比较大,如果不使用缓冲直接进行数据转发则出错率较高甚至什么也听不到。实际上确实如此。
所以我们需要建立一个缓冲区来改善这种问题,就像下图那样,缓冲区就像一个水池子一样,我们可以通过INMP441的数据不断向其中“注水”,然后利用USB接口不断读取其中的数据“放水”,只要我们注入的水量始终比放水的量多那么这种平衡就会始终存在水池总是不会干,就可以一定程度上避免实时转发那种方式带来的高差错率问题。

但是如果注水的量过多,那么池子再大也会满,所以我们可以将这个缓冲区设置为循环缓冲区,即如果缓冲区满了,则从写入数据的指针重新指向缓冲区开头位置。这样虽然会带来覆盖数据的问题,但是会节省很多内存空间对于单片机来说这显然是必要的。
为了防止写入与读出的数据混乱可以通过控制写入与读取数据的指针之间的相对位置来实现,就是如果读取数据的指针始终跟在写入数据的指针的后面则读取到的数据总是之前写入的数据,从而避免读取到空的数据或者已经读到过的数据。
七、请欣赏我第一次听到的麦克风声音
录音演示
就是有些嘈杂的环境噪音,人物的音色有些变了,总体符合预期
八、经验教训记录
刚开始的时候看到STM32关于USB音频开发的文档,里面说通过I2S接口读取PDM麦克风的数据然后转化为PCM然后就可以上传到USB了。
我看到之后就在网上买了常见的INMP441麦克风,虽然麦克风的商品详情页不止一次的提醒过我这个是I2S接口的,我还看了相关的开发博客,但是由于前面看过ST官方的音频开发文档,所以就误以为I2S接口传输的都是PDM数据,需要自己转化为PCM数据。于是就在那里苦苦奋战两天,刚开始把原始数据读出来之后我觉得只要在转换为PCM数据就可以了。于是开始疯狂查询ST的PDM2PCM库到底怎么用,查到怎么用之后,一会儿这里编译不过去了(AC5编译器),一会儿工程开始运行直接进硬件错误中断了(AC6编译器),最后用AC5编译器的时候我鬼使神差的把PCM2PCM库的库文件移除出工程然后再移入,然后就好了。对,就是这么奇怪。于是我以为我已经得到了“PCM数据”,直到我把这些数据通过USB传输给电脑才发现什么声音也听不到。
于是我就又开始想这是什么?然后我在查相关的博客的时候发现原来I2S的飞利浦协议传输的本来就是PCM数据。
怎么说呢,我终于体会到了为什么说人在无语的时候是真的很想笑
于是就有了上面描述的解决方案,其中数据处理里面刚开始我没用缓冲区,导致声音根本不能听,一度以为自己又走错路了,后来加了之后才发现是自己数据处理的问题。
说这么多主要是想给自己长个记性,开发什么之前一定要捋清楚基本的概念,要不然自己晕头转向的瞎捣鼓两天结果发现自己白忙了这样的糗事肯定还会发生!!!