小猫爪:i.MX RT1050学习笔记24-eDMA之eDMA&SAI&ASRC的“纠缠”(RT1170)

1 前言

  今天咱们来说说RT四位数系列的外设eDMA,因为DMA一般来说不会单独使用,所以还要引入两位重要的配角为SAI和ASRC,三者配合着可以实现千变万化的变化,也是因为一个偶然的机会接触到了eDMA,SAI和ASRC的几个使用场景,所以正好借这几个场景正好把这三兄弟的纠缠简单的描述一下,使用平台为RT1170。

2 eDMA详解

  eDMA全名Enhanced Direct Memory Access,加强型DMA,看名字就知道这个DMA肯定恐怖如斯,事实上的确如此,eDMA提供了大循环,小循环等硬件机制,让其不需要软件的干预就具有非常灵活的应用。下图为eDMA的系统框图。
在这里插入图片描述
(在这里我就不对eDMA的工作原理和流程做介绍了,如果对这个感兴趣可以直接参考其reference manul手册。)

  在这里重点说一个TCD(传输控制描述符),这个其实就是整个DMA的核心,它就相当于是DMA的通道配置表,RT1170有32个通道,每一个通道都有一个相对应的TCD,一旦某个通道的DMA申请触发,DMA控制器就会按照TCD里面的配置内容进行传输。是不是非常的简单。

  说到这里那就不得不提一下DMAMUX (Direct Memory Access Multiplexer),如果说TCD是DMA的传输控制中心,那么DMAMUX就是DMA的触发通道源配置中心,所有的DMA通道都可以通过DMAMUX随意配置触发源(这里提一下,这里的触发源指的都是硬件触发源,如果使用硬件触发源,就要在DMA中打开相应的硬件触发开关ERQ,而DMA通道的软件触发则在TCD的CSR寄存器中)。

  接下来,如果我们想使用DMA,我们就必须配置TCD,那么TCD到底有什么东西呢,下面是TCD的结构:
在这里插入图片描述
(注意到在偏移地址0x0008处,有两个不同的寄存器说明,那是因为TCD有几个寄存器在不同的工作模式下,具有不同的配置含义。)
下表对TCD中每一个寄存器都进行了详细的介绍。

寄存器名 作用
TCDx_SADDR 源地址寄存器,保存数据源的起始地址
TCDx_SOFF 源地址偏移,完成一次基本读写后源地址的变化量, (有符号类型)
TCDx_ATTR 传输属性寄存器,主要用于设置源地址和目的地址的数据宽度以及循环buffer大小
TCDx_NBYTES_MLNO TCD0_NBYTES_MLOFFNO TCD0_NBYTES_MLOFFYES 次循环传输数据量寄存器,根据次循环偏移使用情况分为禁止次循环偏移、开启但没有使用次循环偏移、开启并使用了次循环偏移。这三种情况对应同一个寄存器,但是寄存器各个位的含义不同
TCDx_SLAST 源地址最终调整寄存器,当主循环完成后,可通过设置该寄存器修改最终的源地址
TCDx_DADDR 目的地址寄存器,保存写入区域的起始地址
TCDx_DOFF 目的地址偏移寄存器, 完成一次基本读写后目的地址的变化量,(有符号类型)
TCDx_CITER_ELINKNO TCD0_CITER_ELINKYES 主循环计数和通道连接寄存器,同次循环传输数据量寄存器类似,根据配置不同,寄存器的各个位含义不同。它表示的是当前的主循环状态,特别注意,初始化时要保证与主循环起始计数值寄存器的值相等
TCDx_DLASTSGA 目的地址最终调整寄存器,当主循环完成后,可通过设置该寄存器修改最终的目的地址,或者是scatter/gather结构体地址
TCDx_CSR TCD 控制和状态寄存器
TCDx_BITER_ELINKNO TCD0_BITER_ELINKYE 主循环起始计数值寄存器,同主循环计数和通道连接寄存器类,根据配置不同,寄存器的各个位含义不同。

看到这些介绍,可能会有一些迷惑,那么接下来对某些进行详细说明,可能就会理解得更加清楚了。

  1. 数据宽度
      DMA 的源数据宽度与目的数据宽度可以不同。数据宽度的设置是通过 TCDa_ATTR 寄存器设置的。当源数据宽度与目的数据宽度相同时,执行一次读取执行一次写入。当源数据宽度小于目的数据宽度,例如源数据宽度为 16 位,目的数据宽度为 32 位,则 DMA 执行两次读取执行一次写入。

  2. 次循环与主循环
      次循环和主循环可以用于控制传输的数据量,次循环用于设置一次 DMA 传输请求传输的数据量单位为字节,主循环用于设置执行多少个次循环。下图表示了次循环和主循环的关系。
    在这里插入图片描述
    从图中还可以得到一个经常被忽略的一个点,就是每一次DMA请求,其实申请的是次循环搬运,而不是主循环。其中TCDx_NBYTES_MLNO[NBYTES]表示了一次循环所要搬运的字节数,TCDx_CITER_ELINKNO[CITER]表示了当前剩余的次循环次数,TCDx_BITER_ELINKNO[BITER]表示了起始的次循环次数。当主循环结束后,即TCDx_CITER_ELINKNO[CITER] = 0,TCDx_CITER_ELINKNO会自动装载起始值TCDx_BITER_ELINKNO。

  3. 源地址和目标地址的设置
      TCDx_SADDR 寄存器与 TCDx_DADDR 寄存器分别用于设置 DMA 源始地址与目的启始地址。TCDx_SOFF 与 TCDx_DOFF 寄存器分别用于设置 DMA 执行一次读写操作(这里的一次读写操作并不是次循环,而是在数据宽度中提到的读写)之后原地址和目的地址的偏移值。

  4. 源地址和目的地址偏移设置
      当完成一个次循环和主循环之后,可以通过设置相关寄存器设置下一次DMA传输的偏移地址。举个例子,如果一开始源地址为0,搬运了32个字节后,现在源地址变成32,如果我把次循环的偏移地址设置成32,则下一次次循环的源地址就变成了32+32,该偏移也可以设置成负数,主循环偏移同理。
      次循环偏移在寄存器TCDx_NBYTES_MLNO中配置,使用次循环偏移还需要使能CR[EMLM] = 1, 另外次循环和主循环的偏移只能相等或者某一个为0。主循环偏移则通过TCDx_SLAST和TCDx_DLASTSGA进行配置。

  5. 通道组优先级与通道优先级
      一共有32个通道,分成2个通道组,0-15为通道组0,16-31为通道组二,每个通道的优先级可以更改,一般使用较少,保持默认即可。

  6. LINK功能
      每个通道都可以通过使能次循环和主循环的链接功能,就是说,当一次次循环或者主循环完成后,会立刻激活所链接的通道进行传输。主循环可以通过寄存器TCDx_BITER_ELINKNO进行配置,次循环可以通过TCDx_BITER_ELINKNO配置,主循环可以通过TCDx_CSR配置。
      这里要注意一点,就是若同时使能主次循环的LINK功能,则两者LINK的通道必须相同。

  7. scatter/gather功能
      这个功能的意思想当于就是我先把TCD的配置保存在内存RAM中,如果使能scatter/gather功能,那么在一次主循环完成之后,DMA则自动会把RAM中的TCD配置装载到TCD寄存器中。这个功能的典型应用就是在两个buffer反复横跳。通过TCDx_CSR[ESG]使能scatter/gather后,TCDx_DLASTSGA寄存器填写被装载TCD地址。

  8. Modulo功能
      这个功能可以实现一个地址回环,比如这个我把地址回环大小配置成1024,那么当源地址或者目的地址一共累加了1024后,会直接恢复到原始源地址或者目的地址。可以通过配置TCDx_ATTR配置回环大小(这里注意,回环大小必须要是2的次方,并且源地址也必须的相应的2次方对齐,举个例子,回环大小为1KByets,那么源地址必须是1KByets对齐)。

  9. 错误和中断
       错误状态寄存器(ES)列出了所有可能的错误状态。当发生通道错误时每个通道可以独立配置处理方式,可以选择忽略错误也可以选择产生错误中断。错误中断使能寄存器(EEI) 是一个 32 位寄存器,每一位控制一个通道,我们可以直接修改该寄存设置通道发生错误时是否产生中断,也可以通过清除错误中断使能寄存器(SEEI)禁止错误中断寄存器(CEEI)设置单个通道。
      另外每个通道还有一个半完成和完成中断。半完成中断就是主循环完成了一半后,产生一次中断,完成中断就是主循环完成后,

  到这里,关于eDMA的功能就介绍完了。

3 SAI简介

  关于SAI(Synchronous Audio Interface),就是一个音频接口,这个外设还是比较简单的,它支持I2S,PCM, TDM模式。下图为SAI的功能框图:
在这里插入图片描述
从图中就可以看出TX和RX是分开的,时钟可以同步也可以异步,关于SAI这里我就不多做解读了,因为主角不是它,想深入了解的话,可以参考RT的RM手册,另外这篇NXP官方的AN对其的使用进行了详细介绍,大家可自行参考和研究。Using Synchronous Audio Interface (SAI) on S32K148.

4 ASRC简介

  关于ASRC(Asynchronous Sample Rate Converter),就是一个音频频率转换器,使用起来也是非常简单的,举个例子,比如一段48K采样频率的音频数据,经过ASRC就可以转换成16K采样频率的的音频数据。下图显示了ASRC支持的频率转换关系:
在这里插入图片描述
关于ASRC的具体操作这里也就不多做介绍了,想深入了解的话,可以参考RM手册。

5 联合使用样例

  接下来我们通过SAI和ASRC这两个外设来探索eDMA巧妙且恐怖的操作。

5.1 使用场景①

  现有一个2通道,16bit位宽的音频数据,总共大小10KBytes,需要通过一个SAI通道发送出去,发出去的格式也为2通道,16bit位宽。

  这个要求非常简单,就是非常通用的DMA应用,使用SAI的TX_FIFO作为DMA申请源,配置其watermask为16,也就是当TX_FIFO中的空FIFO大于或等于16的时候就会触发一次DMA搬运,一个FIFO存2个字节,所以一次搬运为32字节,这是次循环搬运字节数,所以主循环次数则为10K/32。如果只播放一次的话,就在完成中断后,关闭SAI发送;如果循环播放的话,就使能 scatter/gather功能,当完成一次主循环后,就重新装载TCD完成下一次传输的TCD初始化。下面直接贴出代码(这里只贴出关于DMA TCD的配置)。

static void DEMO_SAI_send_dma_init(void)
{
    
    
    edma_transfer_config_t transferConfig = {
    
    0};
    sai_transceiver_t    sai4_tx_config   = {
    
    0};
    uint32_t destAddr = SAI_TxGetDataRegisterAddress(DEMO_SAI, 0);

    sai4_tx_config.fifo.fifoWatermark = 16;
    EDMA_PrepareTransfer(&transferConfig, 
    					sai_rbuffer, 16 / 8U, (void *)destAddr, 16 / 8U,
                        (FSL_FEATURE_SAI_FIFO_COUNT - sai4_tx_config.fifo.fifoWatermark) * (16 / 8U), /* minor loop bytes 32 */
                         10 * 1024, /* major loop counts: 10K/32 */
                         kEDMA_MemoryToPeripheral);    
    EDMA_TcdSetTransferConfig(&sai_tx_emda_tcd[0], &transferConfig, NULL);
    EDMA_InstallTCD(ECALL_SAI4_DMA, dmaTxHandle_ecall_sai4.channel, &sai_tx_emda_tcd[0]); /*循环播放,需使能 scatter/gather*/
    EDMA_StartTransfer(&dmaTxHandle_ecall_sai);
       di
    SAI_TxSetFifoConfig(DEMO_SAI,&sai4_tx_config.fifo);
    /* Enable DMA enable bit */
    SAI_TxEnableDMA(DEMO_SAI, kSAI_FIFORequestDMAEnable, true);
    /* Enable SAI Tx clock */
    SAI_TxEnableMY(DEMO_SAI, true);
    /* Enable the channel FIFO */
    SAI_TxSetChannelFIFOMask(DEMO_SAI, 1U << 0);
}

5.2 使用场景②

  现有一个4通道,16bit位宽的音频数据,总共大小10KBytes,需要通过一个SAI通道发送出去,发出去的格式为8通道,16bit位宽,其中前四通道数据为原始音频数据,后四通道数据使用0填充。

  在这个场景下,则可以使用eDMA的LINK功能来实现,使用A通道次循环链接B通道,A通道搬运前4个通道的数据,即原始音频数据8个字节,A次循环搬运完成后触发B通道搬运后四个通道的数据,即填充0。

(注意:当打开了次循环链接功能后,主循环的次数最大即可配置2^9-1个,因为次循环配置寄存器和BITER为同一个配置寄存器,使能次循环链接后,BITER所占的位数就少了,如下图所示:
在这里插入图片描述
所以A通道的主循环次数我设置成了256次,可以通过统计中断进入次数的方式来实现发送完整数据。)

static void DEMO_SAI_send_dma_init(void)
    saiConfig.fifo.fifoWatermark = 24;
    EDMA_PrepareTransfer(&transferConfig, &padding_src, DEMO_AUDIO_BIT_WIDTH / 8U, (void *)destAddr, DEMO_AUDIO_BIT_WIDTH / 8U,
                        (FSL_FEATURE_SAI_FIFO_COUNT - saiConfig.fifo.fifoWatermark) * (DEMO_AUDIO_BIT_WIDTH / 8U) / 2 , /* minor loop bytes 8 */
                         (FSL_FEATURE_SAI_FIFO_COUNT - saiConfig.fifo.fifoWatermark) * (DEMO_AUDIO_BIT_WIDTH / 8U) / 2, /* major loop counts:1 */
                         kEDMA_MemoryToPeripheral);    
    EDMA_TcdSetTransferConfig(&tcdMemoryPoolPtr, &transferConfig, &tcdMemoryPoolPtr);
    EDMA_InstallTCD(DEMO_DMA, g_padding_Handle.channel, &tcdMemoryPoolPtr);    

    /* Configure and submit transfer structure 1 */
    EDMA_PrepareTransfer(&transferConfig, (void*)(&music[0]), DEMO_AUDIO_BIT_WIDTH / 8U, (void *)destAddr,
                         DEMO_AUDIO_BIT_WIDTH / 8U,
                         (FSL_FEATURE_SAI_FIFO_COUNT - saiConfig.fifo.fifoWatermark) * (DEMO_AUDIO_BIT_WIDTH / 8U) / 2,
                         256*8, kEDMA_MemoryToPeripheral);

    EDMA_TcdSetTransferConfig(&s_emdaTcd, &transferConfig, NULL);
    EDMA_TcdSetChannelLink(&s_emdaTcd, kEDMA_MinorLink, g_padding_Handle.channel);
    EDMA_TcdSetChannelLink(&s_emdaTcd, kEDMA_MajorLink, g_padding_Handle.channel);
    EDMA_InstallTCD(DEMO_DMA, g_dmaHandle.channel, &s_emdaTcd);
    EDMA_StartTransfer(&g_dmaHandle);
    
    SAI_TxSetFifoConfig(DEMO_SAI,&saiConfig.fifo);
    /* Enable DMA enable bit */
    SAI_TxEnableDMA(DEMO_SAI, kSAI_FIFORequestDMAEnable, true);
    /* Enable SAI Tx clock */
    SAI_TxEnable(DEMO_SAI, true);
    /* Enable the channel FIFO */
    SAI_TxSetChannelFIFOMask(DEMO_SAI, 1U << DEMO_SAI_CHANNEL);

5.3 使用场景③

  从SAI接受音频数据,格式为2通道,16bit位宽,48K采样率,经过ASRC转换成16K音频数据后,再经过SAI发送出去,其中SAI作为主机。

  对于这个场景,我们想到,如果接受和发送的SAI的时钟源是同一个,而ASRC的输入和输出时钟源又可以来自SAI,所以在时钟上是完全同步的,又因为SAI作为主机,发送时钟完全取决于有无数据需要发送,所以就可以构思如下图的数据流:

DMA
DMA
SAI_RX_FIFO
ASRC_IN_FIFO
ASRC
ASRC_OUT_FIFO
SAI_TX_FIFO

  由DMA将SAI接收到的数据直接搬运到ASRC的IN_FIFO,该过程中的DMA申请则由SAI的RX_FIFO触发,频率转换完成后,再通过DMA将ASRC的OUT_FIFO数据直接搬运到SAI的TX_FIFO发送,这一次的DMA申请由ASRC的OUT_FIFO触发,这样可以完全通过硬件的方式实现了一个音频数据转换的应用

   在这里还要注意一个点,一定要注意SAI的RX_FIFO的watermask和ASRC的OUT_FIFO的watermask设置问题,让两者在时间轴上做到一致,举个例子,比如在这个应用中,是48K转16K,也就是SAI接收n个字节,那么SAI必须要在同一时间内发送n/3个字节,而这点也就反映在两个FIFO的watermask和两个DMA一次申请所需要搬运的字节数的配置上

   这个场景就不贴代码了,因为比较简单,主要是对于这个应用中DMA和其他外设的配合所需要考虑到的东西比较绕,是DMA应用中一个外设到外设的典型应用。

5.4 使用场景④

  从SAI接受音频数据,格式为2通道,16bit位宽,48K采样率,经过ASRC转换成16K音频数据后,再经过SAI发送出去,其中SAI作为从机。

   这个场景怎么看上去和上面一个场景一模一样,其实还是有差别的,上面的场景中SAI发送端是作为主,而这个场景中,SAI发送端则是作为从机,作为从机可能表面上问题不大,但是却隐藏了一个非常致命的问题。

   如果SAI接收端和SAI发送端是同一个SAI,时钟来源为同一个时钟,那接下来所说的问题也就不存在,但是如果不一样,那么就会存在因为数据不同步而导致数据丢失以及通道数据错位的问题。

  因为当SAI作为从机,它的时钟则是来源于外部,如果还是使用场景③的思想,一旦SAI接收端这边接收数据,经过ASRC转换后,DMA将数据搬运到SAI发送端,但是这个时候SAI发送端的时钟还没有准备,这个时候,SAI发送端是不会把DMA搬运来的数据往外发送,但是DMA可不管那么多,只要有数据过来,我就给你搬过去,一旦数据溢出了这个时候就会发生数据丢失,如果正好在搬运到一半的时候,SAI发送端开始发送数据,错把右声道数据当作左声道发送,这就导致了左右声道错位。

  所以为了解决这个问题,就需要在某个地方做一些变动,在上面场景的基础上,我们把ASRC输出数据通过DMA搬运到一个回环buffer中,而SAI_TX需要发送数据的时候,就通过触发DMA申请从这个回环buffer中拿数据,这样就把上面的主动往TX_FIFO里送数据变成了TX_FIFO主动要数据,让前一个DMA的目的地址和后一个DMA的源地址在这个回环buffer里面永远按照相同的速度转圈圈(通过配置watermask和NBYTES实现,上面场景中已经解释过了),这样就实现了数据不丢失和不错位。这个时候数据流向如下图:

DMA
DMA
DMA
SAI_RX_FIFO
ASRC_IN_FIFO
ASRC
ASRC_OUT_FIFO
RING_BUFFER
SAI_TX_FIFO

  我们在往深处想一想,其实这样做还会存在两个点需要考虑到:

  ① 如果TX在RX接收数据之前开始发送,这个时候回环buffer里面的数据不是有效音频数据,但是也不要太过担心,当转满一圈后,这个时候回环buffer里面就是有效数据了,但是第一圈的数据不是真实的音频数据。

  ② 如果这个回环buffer满了,但是TX还是没有开始发送,这个时候则会开始丢数据,这一点则是可以通过增大回环buffer去改善,但是虽然会降低数据丢失的概率,但是如果接收端和发送端相差太多,延迟也会增加,对于这一点就只能通过主从机之间的协调去改善了。

  在这个应用中,将DMA的三个应用场景,即外设到外设,外设到内存,内存到外设,完美的糅合在一起,个人觉得是一个经典的eDMA应用。

  说到这里,关于eDMA,SAI和ASRC的“纠缠”就全部介绍完了,有想法的小伙伴可以私信我哦,互相讨论。

END

猜你喜欢

转载自blog.csdn.net/Oushuwen/article/details/118672175