RT1060 evkmimxrt1060_sai_edma_record_playback 详解
evkmimxrt1060_sai_edma_record_playback
详细解析 evkmimxrt1060_sai_edma_record_playback
- 硬件:RT1060-EVK
- IDE:MCUXpresso
- SDK:2.16.000
0. 工程用到的外设拆解

- UART
- I2C
- SAI: 在这个例子中,SAI是master,会向codec输出MCLK。
- Codec:
- DMAMUX
- eDMA
1. main函数主逻辑
1 | int main(void) |
工作流程和逻辑总结:
- 初始化阶段:
系统、引脚、时钟等基础硬件被配置。
**音频PLL被设置**,为SAI提供精确时钟。
用于codec控制的I2C 和 用于音频数据传输的SAI 的时钟被配置。
SAI的MCLK输出被使能,为外部codec提供工作时钟。
DMAMUX被配置,将SAI的Tx和Rx请求连接到指定的EDMA通道。
EDMA控制器被初始化,并为SAI的Tx和Rx创建了EDMA句柄,准备好进行数据传输。
SAI外设本身被初始化。
为SAI的Tx和Rx功能创建了EDMA传输句柄,这些句柄关联了SAI模块、对应的EDMA句柄以及传输完成后的回调函数 (
tx_callback
和rx_callback
)。**SAI被配置为I2S模式**,设置了位宽、声道、同步模式(Rx同步于Tx位时钟,Tx异步)、主从模式等参数。
此时,Codec是master,SAI是slave。
计算并设置了SAI的 位时钟速率。
音频codec (WM8960) 通过I2C接口被初始化和配置(包括音量设置)。
- 数据传输阶段 (主循环 while(1)):
工程使用了多个(
BUFFER_NUMBER
,这里是4个)大小为BUFFER_SIZE
(这里是1024字节) 的缓冲区 (Buffer 数组)。emptyBlock
变量跟踪当前有多少个缓冲区是空的。初始时,所有缓冲区都是空的,emptyBlock
等于BUFFER_NUMBER
。rx_index
指向下一个用于接收(录制)数据的缓冲区。tx_index
指向下一个用于发送(播放)数据的缓冲区。录制逻辑:
如果
emptyBlock > 0
(有空缓冲区),意味着可以启动一次新的接收传输。工程设置下一个 空缓冲区 为目标,启动一个EDMA接收任务。EDMA会自动将SAI接收到的数据传输到指定的缓冲区中。传输完成后,会触发rx_callback
,将emptyBlock
减一,并更新rx_index
。播放逻辑:
如果
emptyBlock
<BUFFER_NUMBER
(有非空缓冲区),意味着有数据可以播放。工程设置下一个 待播放的缓冲区 为 数据源,启动一个EDMA发送任务。EDMA会自动将缓冲区中的数据传输到SAI,SAI再发送给codec进行播放。传输完成后,会触发
tx_callback
,将emptyBlock
加一,并更新tx_index
。rx_index
和tx_index
在达到缓冲区末尾时会循环回0,形成一个循环缓冲队列。通过这种方式,当录制EDMA完成一个缓冲区时,
emptyBlock
减少,主循环就可以尽快启动播放该缓冲区;当播放EDMA完成一个缓冲区时,emptyBlock
增加,主循环就可以尽快将该缓冲区再次用于录制。这实现了音频数据的实时“录放”(Capture-Playback)。
- 回调函数 (
rx_callback
和tx_callback
):
这两个函数在对应的EDMA传输完成时由SAI EDMA驱动调用。
它们的主要作用是更新
emptyBlock
的计数器。rx_callback
(接收完成) 减少emptyBlock
,tx_callback
(发送完成) 增加emptyBlock
。它们也包含基本的错误处理(虽然这里的实现只是检查状态而没有具体处理)。
总的来说,这个工程通过配置SAI和EDMA,建立了一个高效的音频数据通道。数据通过EDMA从SAI Rx(录音输入)传输到内存缓冲区,然后再通过EDMA从内存缓冲区传输到SAI Tx(播放输出)。main 函数中的 while(1) 循环负责检查缓冲区的状态 (emptyBlock) 并不断启动新的EDMA传输任务,从而实现连续的音频录制和播放功能。回调函数是这个EDMA驱动模型的关键部分,它们在外设事件(传输完成)发生时被调用,通知主程序更新状态并安排下一个任务。
2. 几个时钟的区分
在I2S(或其他类似的串行音频接口)通信中,有几种不同的时钟信号:
MCLK
(Master Clock / System Clock): 通常是频率最高的时钟,用于驱动Codec的内部电路和PLL。BCLK
(Bit Clock): 位时钟,每个音频数据位对应一个BCLK周期。LRCLK
(Left/Right Clock / Frame Sync): 左右时钟或帧同步信号,指示当前传输的是左声道还是右声道数据,并同步一个音频帧的开始。(也是字选择信号)
时钟的主从关系主要体现在 BCLK
和 LRCLK
上。配置为 I2S Master 的设备会 生成并输出 BCLK 和 LRCLK;配置为 I2S Slave 的设备会 接收 BCLK 和 LRCLK。
然而,MCLK 的主从关系是相对独立的。在一个典型的系统中,通常有一个设备负责产生高频的MCLK来驱动整个音频链路。这个设备可以是由主控芯片(MCU)通过其某个时钟生成单元产生,也可以由外部的Codec自己产生(这种情况较少见)。
在工程中,BOARD_EnableSaiMclkOutput(true)
函数的作用是配置MIMXRT1060芯片上SAI1模块的MCLK引脚,使其作为 输出 引脚。这意味着芯片正在 产生并输出 MCLK。这个MCLK通常是由芯片内部的音频PLL (CLOCK_InitAudioPll
) 产生,然后通过SAI模块的MCLK引脚输出到外部的WM8960 Codec。
3. 位时钟是怎么产生的
来仔细看 SAI_TxSetBitClockRate
和 SAI_RxSetBitClockRate
这两个函数及其实现的逻辑。它们的核心功能是根据期望的音频参数(采样率、位宽、通道数)来计算并配置SAI模块的 位时钟 (BCLK) 相关的寄存器设置。
1. SAI_TxSetBitClockRate
函数 (配置发送功能的位时钟速率)
1 | /*! |
核心逻辑 (SAI_TxSetBitClockRate):
- 计算目标位时钟频率 (
bitClockFreq
):这是由音频格式(采样率、位宽、通道数)决定的,表示每秒需要传输的总位数。 - 计算所需的时钟源到目标位时钟频率的总分频比 (
bitClockDiv
):源时钟频率 / 目标位时钟频率
。 - 对计算出的分频比进行调整,确保分频结果不会高于目标频率。
- 根据计算出的分频比和硬件寄存器的编码方式(除以2减1)计算要写入寄存器的值。
- 将计算出的分频值写入SAI发送配置寄存器2 (
TCR2
) 的相应位字段 (DIV
)。
2. SAI_RxSetBitClockRate
函数 (配置接收功能的位时钟速率)
1 | /*! |
核心逻辑 (SAI_RxSetBitClockRate):
这个函数与 SAI_TxSetBitClockRate
的逻辑完全相同。唯一的区别在于,它操作的是SAI接收配置寄存器2 (RCR2
) 的 DIV
位字段 (I2S_RCR2_DIV_MASK
)。
为什么在 Slave 模式下也调用这些函数?
在这个工程中,SAI 被配置为 BCLK/LRCLK 的 Slave。在 Slave 模式下,SAI 不会通过其内部的分频器生成 BCLK,而是从外部设备(Codec Master)接收 BCLK。
在具体工程中,由于 SAI 被明确配置为 Slave (kSAI_Slave
),最重要的时钟(BCLK 和 LRCLK)实际上是由 Codec Master 产生的。MCU SAI 模块通过接收这些外部时钟来同步数据的发送和接收。因此,虽然代码中调用了 SetBitClockRate
函数,其主要作用可能不是用于配置时钟生成硬件(因为是 Slave 模式),而是计算分频并写入寄存器,方便后面使用。
来深入分析一下 Codec (WM8960) 如何在主模式下计算和产生 BCLK,以及代码中哪里体现了这一点。
当我们说 Codec 工作在 I2S 主模式 (Master) 时,这意味着它负责根据音频格式(采样率、位宽、通道数)来生成并输出 位时钟 (BCLK) 和 帧同步信号 (LRCLK)。它通常依赖于一个主时钟 (MCLK) 来驱动其内部时钟生成电路(如 PLL 和分频器)。
在这个工程中:
MCLK 源: 微控制器 (MIMXRT1060) 通过 SAI 的 MCLK 输出引脚提供 MCLK 给 WM8960 Codec。我们在 main.c 中配置并使能了这个输出 (BOARD_EnableSaiMclkOutput(true))。MCLK 的频率是根据音频 PLL 和 SAI 的时钟分频器设置得出的,main.c 中通过 DEMO_SAI_CLK_FREQ 宏定义来表示这个频率。
音频格式信息: Codec 需要知道要传输的音频数据的格式,才能计算出正确的 BCLK 频率。这些信息在 wm8960Config 结构体中进行了配置,并在初始化 Codec 时通过 CODEC_Init 函数传递给了 WM8960 驱动:
.format.sampleRate = kWM8960_AudioSampleRate16KHz
: 设定采样率为 16KHz。.format.bitWidth = kWM8960_AudioBitWidth16bit
: 设定数据位宽为 16 bit。.bus = kWM8960_BusI2S
: 设定使用标准的 I2S 格式。标准 I2S 格式下,一个立体声采样周期需要传输2 * bitWidth
的数据位(对于 16bit stereo 就是 32 位)。.format.mclk_HZ = 6144000U * 2
: 设定 Codec 接收到的 MCLK 频率。这里计算出来是 12.288 MHz。这与DEMO_SAI_CLK_FREQ
理论上应该匹配。
- Codec 如何计算 BCLK: Codec 内部有专门的时钟生成硬件。当 Codec 被配置为 I2S Master 并接收到 MCLK 后,它会根据接收到的 MCLK 频率 (format.mclk_HZ) 和配置的音频格式(采样率、位宽、总线格式)来计算并生成 BCLK 和 LRCLK。
对于标准的 I2S 格式,所需的 BCLK 频率通常是 采样率 * 每帧总位数。对于 16 位立体声,每帧是 32 位(16位左声道 + 16位右声道)。所以理论上 BCLK 频率应为 16KHz * 32 = 512 KHz。
Codec 内部的 PLL 和分频器会利用 MCLK (12.288 MHz) 来尝试生成这个 512 KHz 的 BCLK。例如,12.288 MHz / 512 KHz = 24。所以 Codec 内部可能需要将 MCLK 分频 24 倍来得到 BCLK。
LRCLK 的频率就是采样率,即 16 KHz。LRCLK 通常是 BCLK 的一个分频(对于 16 位立体声 I2S,LRCLK 频率是 BCLK 频率的 1/32)。
- 代码中的体现:
wm8960Config 结构体中的
.master_slave = true
指明了 Codec 是 Master。wm8960Config.format 中的
.sampleRate
和.bitWidth
提供了 Codec 计算 BCLK 所需的音频格式信息。wm8960Config.format.mclk_HZ 告知 Codec 它接收到的 MCLK 频率是多少,这是它进行内部时钟生成的输入。
Codec 驱动 (fsl_wm8960.c) 的作用: 虽然我们没有直接看到 WM8960 驱动中计算 BCLK 的 C 代码(因为时钟生成是 Codec 硬件的功能,驱动主要通过 I2C 配置 Codec 寄存器),但
CODEC_Init
(在 fsl_codec_adapter.c 中调用 WM8960 驱动的初始化函数) 会读取 wm8960Config 中的这些参数,并通过 I2C 接口将它们写入 WM8960 Codec 的内部配置寄存器。WM8960 Datasheet/Reference Manual: Codec 如何根据这些配置计算 BCLK 的具体细节(如内部 PLL 配置、分频器寄存器设置等)通常会在 WM8960 的数据手册或参考手册中详细说明。驱动程序 fsl_wm8960.c 就是根据这个手册来实现通过 I2C 配置 Codec 寄存器的逻辑。驱动会解析 wm8960Config 中的参数,并翻译成 Codec 寄存器中对应的位域值。
总结:
当 SAI 是 Slave 时,BCLK 是由 Codec (WM8960) 产生的。CodeC 作为 I2S Master,利用从微控制器接收到的 MCLK,并根据通过 I2C 接口由 Codec 驱动 (fsl_wm8960.c) 写入其内部寄存器的音频格式配置(采样率、位宽、总线格式等),通过其内部的时钟生成电路(PLL 和分频器)计算并生成所需的 BCLK 和 LRCLK。微控制器的 SAI 驱动代码通过 wm8960Config 和 CODEC_Init 将音频格式和 MCLK 频率信息传递给 Codec 驱动,由 Codec 驱动负责通过 I2C 将这些配置应用到 WM8960 硬件,CodeC 硬件本身负责根据这些配置生成 BCLK。
4. 时钟总结
好的,没问题。根据我们之前的讨论,我为你绘制一个简单的文本图,希望能帮助你更清晰地理解整个系统中各个外设的作用、连接关系以及时钟和数据流。
1 | +----------------------------------------------------------------+ |
图解说明:
MIMXRT1060: 整个系统的核心。
- Audio PLL & 时钟生成单元: 产生高频的音频主时钟 (MCLK),并通过 SAI1 模块输出给 Codec。
- LPI2C1: 作为 I2C 主设备,用于通过 I2C 总线向 WM8960 Codec 发送配置命令(如设置采样率、音量、主从模式等)。
- SAI1: 串行音频接口,配置为 **BCLK/LRCLK 的从设备 (Slave)**。它接收来自 Codec 的 BCLK 和 LRCLK 来同步数据传输,并通过数据引脚接收录音数据 (RX) 并发送播放数据 (TX)。
- SAI1 RX FIFO: 接收来自 Codec 的数字音频数据,并将数据暂存起来。当 FIFO 达到一定阈值时,会触发 EDMA 传输请求。
- SAI1 TX FIFO: 接收来自 EDMA 的播放数据,并通过 SAI 接口发送给 Codec。当 FIFO 需要数据时,会触发 EDMA 传输请求。
- DMAMUX: 将 SAI1 模块的 RX/TX EDMA 传输请求路由到特定的 EDMA 通道。
- EDMA: 增强型 DMA 控制器,负责在 SAI 的 FIFO 和内存缓冲区之间高效地传输音频数据,无需 CPU 干预。
- 内存缓冲区: 位于微控制器 RAM 中的一块区域,用于临时存储录制到的音频数据,以及存放等待播放的音频数据。
外部设备:
- WM8960 Codec: 音频编解码器。
- 接收微控制器提供的 MCLK。
- 接收微控制器通过 I2C 发送的配置命令。
- 作为 **BCLK/LRCLK 的主设备 (Master)**,根据 MCLK 和配置信息,内部生成并输出 BCLK 和 LRCLK 给 SAI1。
- 将来自麦克风的模拟音频转换为数字音频 (ADC),并通过数字音频输出引脚发送给 SAI1 RX FIFO。
- 将来自 SAI1 TX FIFO 的数字音频数据转换为模拟音频 (DAC),并通过模拟音频输出引脚发送给耳机/扬声器。
- 麦克风: 提供模拟音频输入。
- 耳机/扬声器: 输出模拟音频。
- WM8960 Codec: 音频编解码器。
时钟和数据流:
- 时钟流:
MCLK
从 MCU 流向 Codec;BCLK
和LRCLK
从 Codec 流向 MCU (SAI1)。 - 配置流: I2C 命令从 MCU 流向 Codec。
- 数据流 (录音 - RX): 麦克风 (模拟) -> Codec (ADC -> 数字) -> Codec 数字输出 -> SAI1 RX FIFO -> EDMA Channel RX -> 内存缓冲区。这个过程由 Codec 生成的 BCLK 和 LRCLK 同步。
- 数据流 (播放 - TX): 内存缓冲区 -> EDMA Channel TX -> SAI1 TX FIFO -> SAI1 数字输出 -> Codec (DAC -> 模拟) -> Codec 模拟输出 -> 耳机/扬声器。这个过程也由 Codec 生成的 BCLK 和 LRCLK 同步。
这个图和说明应该能清晰地展示出系统中各个组件的角色分工、连接方式以及音频数据和时钟的流动路径。

1 | graph LR |
RT1060 evkmimxrt1060_sai_edma_record_playback 详解
https://dustofstars.github.io/NXP/RT1060/SAI/rt1060-evkmimxrt1060-sai-edma-record-playback-详解/