1 背景
某个基于I2S接口的语音应用项目中,在系统测试阶段,发现设备在语音通话中会概率性的出现噪声。
1.1 I2S接口
I2S(Inter-IC Sound)接口是一种用于音频数据传输的串行总线标准,广泛应用于嵌入式系统中的音频处理,以便与音频编解码器(如DAC和ADC)进行高效的数据交换。
1.2 循环DMA
循环DMA(Cyclic DMA)是一种特殊的DMA传输模式,特别适用于需要连续不断传输数据的应用场景,如音频播放、视频流处理等。循环DMA通过硬件自动管理缓冲区,在完成一次传输后自动重新开始,无需CPU干预即可实现连续数据流。
1.3 项目应用
本项目的嵌入式硬件是基于linux内核的多核心多线程系统,I2S接口的语音收发驱动应用用了循环DMA机制,几个相关模块的语音收发示意图如下图1所示。

图1 I2S语音收发示意图
其中:
1)循环DMA模块为嵌入式硬件的系统DMA模块,实现I2S驱动模块与外部数据源之间的DMA数据的周期性搬移(包括硬件中断);
2)I2S驱动模块,实现对I2S接口的硬件初始化(包括循环DMA),从应用层的语音交换模块读取语音数据(Voc-TX)并通过循环DMA发送到外部收据源,或者反之(Voc-RX)。
3)语音交换模块,位于应用层,将业务层需要收发的语音数据经过I2S驱动模块发往外部数据源,或者反之。
补充说明:
1)为了保证模块间语音数据收发的一致性和实时性,I2S驱动模块和语音交换模块均采样毫秒级定时(比如1ms)的ping-pong数据收发通信机制,定时源统一来自于循环DMA模块的中断;
2)I2S驱动模块的定时采样于循环DMA模块Voc-RX(数据接收)的周期性中断;
3)语音交换模块的定时采样于I2S驱动模块(通过信号量机制);
2 问题定位
2.1 问题现象
噪声问题随机性出现,一旦拨测出语音中有噪声,噪声会一直存在,直到系统再次重启(硬件或软件重启)才会消失。
2.2 定位过程
1)问题重现比较困难,有一定随机性,有时重启(加拨测)100次以上也不一定能重现;往往要要折腾3~4小时才能重现1~2次。
2)经过多次尝试能够稳定重现问题之后,发现噪声的出现与系统的启动过程强相关(随机相关),应该与系统的初始化过程有关系;但是启动过程又涉及到过个模块,还需要进一步缩小范围。
3)后来对问题录音分析时,注意到噪声的产生伴随着Voc-RX(收语音流)和Voc-TX(发语音流)有失步现象,开始将定位重点放到语音收发定时的一致性问题,其中I2S驱动模块是关键。
4)结合2和3的分析,最后通过仔细分析I2S驱动模块的初始化过程代码找到问题原因。
3 原因分析
3.1 循环DMA的初始化
I2S驱动代码中循环DMA初始化的示意代码如下:
#... ... 省略无关代码
#循环DMA初始化,准备DMA传输描述符
struct dma_async_tx_descriptor *txd = dmaengine_prep_dma_cyclic(tx_chan, ...);
struct dma_async_tx_descriptor *rxd = dmaengine_prep_dma_cyclic(rx_chan, ...);
#... ... 省略(设置回调函数和参数)
#提交DMA队列
dma_cookie_t tx_cookie = dmaengine_submit(txd);
dma_cookie_t rx_cookie = dmaengine_submit(txd);
#启动DMA传输
dma_async_issue_pending(tx_chan);
dma_async_issue_pending(rx_chan);
3.2 原因分析
1)如1.3章节所述,I2S驱动模块的定时采样于循环DMA模块Voc-RX(数据接收)的中断,即初始化代码中的rx_chan所对应DMA通道的周期性中断(dmaengine_prep_dma_cyclic函数会设定周期时长);
2)需要注意的一点是DMA通道的数据收Rx和发Tx是两个不同的通道(分别对应代码中的rx_chan和tx_chann);这两个通道在初始化代码中是先后调用的(尤其是最后启动DMA传输的dma_async_issue_pending函数),在正常情况下,这两个(黄色代码段)相邻的函数调用的时延通常在几个纳秒级别,相对DMA的周期性中断定时(毫秒级别)可以忽略不计。
3)在linux这种支持多任务抢占的系统中,在同一初始化任务的执行过程中,系统会有概率因为其他高优先级任务的抢占调度中断而出现调度时延(可达到毫秒级别),从而导致两个通道的数据传输同步出现错位:如前1.3描述,语音交换和I2S驱动模块的pingpong定时只与DMA收通道的中断同步,整个语音收发就会出现乱序而产生噪声。
4)考虑到驱动初始化时系统被高优先级任务打断的概率非常低,同时存在不确定性,这也刚好可以解释问题的随机性和难以重现。
3.3 修正措施
#... ... 省略无关代码
#启动DMA传输
//ensure cycle write&read DMA issue in the same time
preempt_disable();dma_async_issue_pending(tx_chan);
dma_async_issue_pending(rx_chan);
preempt_enable();
说明:
1)在Linux内核中,preempt_disable 和 preempt_enable 是用于控制内核抢占(Preemption)的宏,主要用于防止内核代码在执行过程中被其他高优先级任务抢占,从而保证关键代码段的原子性和一致性。
2)如上红色部分新增代码,为了保证rx_chan和tx_chann两个循环DMA通道的启动一致性,需要在初始化时特别是调用dma_async_issue_pending函数时防止调用过程打断的内核函数preempt_disable/ preempt_enable。
3)修订后的代码,经过反复测试验证确认问题解决。
4 总结
1)在Linux系统的多任务抢占性调度机制下,关键硬件(如循环DMA通道)的初始化需考虑调度时序和一致性问题。
2)噪声源于I2S驱动初始化时收发DMA通道的启动存在概率性调度中断时延导致时序错位产生。
3)通过在启动DMA时添加preempt_disable/preempt_enable保护,确保两个通道同步启动可以解决该问题。