RT1060 evkmimxrt1060_sai_edma_record_playback 详解

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. 工程用到的外设拆解

image-20250528224056154
  1. UART
  2. I2C
  3. SAI: 在这个例子中,SAI是master,会向codec输出MCLK。
  4. Codec:
  5. DMAMUX
  6. eDMA

1. main函数主逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
int main(void)
{
sai_transfer_t xfer; // 用于定义一次SAI传输的数据缓冲区和大小
sai_transceiver_t saiConfig; // 用于配置SAI收发器,比如工作模式、数据格式等

BOARD_ConfigMPU(); // 配置内存保护单元 (MPU),通常用于设置内存区域的访问权限等,保证系统稳定性
BOARD_InitBootPins(); // 初始化开发板的引脚,将特定引脚配置为SAI、I2C、UART等外设的功能
BOARD_InitBootClocks(); // 初始化系统时钟,包括CPU核心时钟、外设时钟等
CLOCK_InitAudioPll(&audioPllConfig); // 初始化音频PLL (锁相环),这个PLL通常用来为SAI提供高精度的音频主时钟
BOARD_InitDebugConsole(); // 初始化调试控制台,通常是配置UART,用于通过串口打印信息(比如 "SAI example started!")

/*Clock setting for LPI2C*/
CLOCK_SetMux(kCLOCK_Lpi2cMux, DEMO_LPI2C_CLOCK_SOURCE_SELECT); // 选择LPI2C的时钟源
CLOCK_SetDiv(kCLOCK_Lpi2cDiv, DEMO_LPI2C_CLOCK_SOURCE_DIVIDER); // 设置LPI2C时钟的分频器,确定其工作频率

/*Clock setting for SAI1*/
CLOCK_SetMux(kCLOCK_Sai1Mux, DEMO_SAI1_CLOCK_SOURCE_SELECT); // 选择SAI1的时钟源
CLOCK_SetDiv(kCLOCK_Sai1PreDiv, DEMO_SAI1_CLOCK_SOURCE_PRE_DIVIDER); // 设置SAI1时钟的预分频器
CLOCK_SetDiv(kCLOCK_Sai1Div, DEMO_SAI1_CLOCK_SOURCE_DIVIDER); // 设置SAI1时钟的分频器,共同决定 SAI的工作频率和MCLK频率

/*Enable MCLK clock*/
BOARD_EnableSaiMclkOutput(true); // 使能SAI的主时钟(MCLK)输出。在这个例子中,SAI是master,会向codec输出MCLK。

/* Init DMAMUX */
DMAMUX_Init(DEMO_DMAMUX); // 初始化DMA多路复用器,它负责将外设请求(如SAI的Tx/Rx完成)路由到特定的EDMA通道
DMAMUX_SetSource(DEMO_DMAMUX, DEMO_TX_EDMA_CHANNEL, (uint8_t)DEMO_SAI_TX_SOURCE); // 将SAI Tx的数据请求路由到EDMA的某个通道
DMAMUX_EnableChannel(DEMO_DMAMUX, DEMO_TX_EDMA_CHANNEL); // 使能该EDMA通道的DMAMUX请求
DMAMUX_SetSource(DEMO_DMAMUX, DEMO_RX_EDMA_CHANNEL, (uint8_t)DEMO_SAI_RX_SOURCE); // 将SAI Rx的数据请求路由到EDMA的另一个通道
DMAMUX_EnableChannel(DEMO_DMAMUX, DEMO_RX_EDMA_CHANNEL); // 使能该EDMA通道的DMAMUX请求

PRINTF("SAI example started!\n\r"); // 打印启动信息

/* Init DMA and create handle for DMA */
EDMA_GetDefaultConfig(&dmaConfig); // 获取EDMA默认配置
EDMA_Init(DEMO_DMA, &dmaConfig); // 初始化EDMA控制器
EDMA_CreateHandle(&dmaTxHandle, DEMO_DMA, DEMO_TX_EDMA_CHANNEL); // 为SAI Tx创建一个EDMA句柄
EDMA_CreateHandle(&dmaRxHandle, DEMO_DMA, DEMO_RX_EDMA_CHANNEL); // 为SAI Rx创建一个EDMA句柄

/* SAI init */
SAI_Init(DEMO_SAI); // 初始化SAI外设模块本身
SAI_TransferTxCreateHandleEDMA(DEMO_SAI, &txHandle, tx_callback, NULL, &dmaTxHandle); // 为SAI Tx创建EDMA传输句柄,关联SAI模块、EDMA句柄和Tx完成回调函数
SAI_TransferRxCreateHandleEDMA(DEMO_SAI, &rxHandle, rx_callback, NULL, &dmaRxHandle); // 为SAI Rx创建EDMA传输句柄,关联SAI模块、EDMA句柄和Rx完成回调函数

/* I2S mode configurations */
SAI_GetClassicI2SConfig(&saiConfig, DEMO_AUDIO_BIT_WIDTH, kSAI_Stereo, 1U << DEMO_SAI_CHANNEL); // 获取标准的I2S模式配置,比如位宽、声道等
saiConfig.syncMode = DEMO_SAI_TX_SYNC_MODE; // 设置Tx同步模式(这里是异步)
saiConfig.bitClock.bclkPolarity = DEMO_SAI_TX_BIT_CLOCK_POLARITY; // 设置位时钟极性
saiConfig.masterSlave = DEMO_SAI_MASTER_SLAVE; // // 设置SAI模块在BCLK和LRCLK方面是Slave
SAI_TransferTxSetConfigEDMA(DEMO_SAI, &txHandle, &saiConfig); // 将配置应用到SAI Tx EDMA句柄
saiConfig.syncMode = DEMO_SAI_RX_SYNC_MODE; // 设置Rx同步模式(这里是同步,与Tx异步工作)
SAI_TransferRxSetConfigEDMA(DEMO_SAI, &rxHandle, &saiConfig); // 将配置应用到SAI Rx EDMA句柄

/* set bit clock divider */
SAI_TxSetBitClockRate(DEMO_SAI, DEMO_AUDIO_MASTER_CLOCK, DEMO_AUDIO_SAMPLE_RATE, DEMO_AUDIO_BIT_WIDTH,
DEMO_AUDIO_DATA_CHANNEL); // 设置SAI Tx的位时钟速率,基于MCLK、采样率、位宽和通道数计算
SAI_RxSetBitClockRate(DEMO_SAI, DEMO_AUDIO_MASTER_CLOCK, DEMO_AUDIO_SAMPLE_RATE, DEMO_AUDIO_BIT_WIDTH,
DEMO_AUDIO_DATA_CHANNEL); // 设置SAI Rx的位时钟速率

/* master clock configurations */
BOARD_MASTER_CLOCK_CONFIG(); // 这是一个宏,可能包含额外的MCLK配置,根据实际板级定义来确定具体功能

/* Use default setting to init codec */
if (CODEC_Init(&codecHandle, &boardCodecConfig) != kStatus_Success) // 初始化音频编解码器(codec),这里是WM8960
{
assert(false); // 如果初始化失败,停止程序
}
if (CODEC_SetVolume(&codecHandle, kCODEC_PlayChannelHeadphoneLeft | kCODEC_PlayChannelHeadphoneRight,
DEMO_CODEC_VOLUME) != kStatus_Success) // 设置耳机输出音量
{
assert(false); // 如果设置音量失败,停止程序
}

// --- 主循环:音频录制和播放的实时处理 ---
while (1)
{
// 录制部分 (RX)
if (emptyBlock > 0) // 检查是否有空的缓冲区可以用于接收(录制)音频数据
{
xfer.data = Buffer + rx_index * BUFFER_SIZE; // 设置本次EDMA接收传输的目标地址为当前可用的缓冲区
xfer.dataSize = BUFFER_SIZE; // 设置本次传输的数据大小(缓冲区大小)
if (kStatus_Success == SAI_TransferReceiveEDMA(DEMO_SAI, &rxHandle, &xfer)) // 启动一次SAI通过EDMA的接收传输
{
rx_index++; // 接收缓冲区索引递增,指向下一个缓冲区
}
if (rx_index == BUFFER_NUMBER) // 如果索引达到缓冲区总数,循环回到第一个缓冲区
{
rx_index = 0U;
}
}

// 播放部分 (TX)
if (emptyBlock < BUFFER_NUMBER) // 检查是否有已填充(录制完成)的缓冲区可以用于发送(播放)音频数据
{
xfer.data = Buffer + tx_index * BUFFER_SIZE; // 设置本次EDMA发送传输的数据源地址为当前待播放的缓冲区
xfer.dataSize = BUFFER_SIZE; // 设置本次传输的数据大小
if (kStatus_Success == SAI_TransferSendEDMA(DEMO_SAI, &txHandle, &xfer)) // 启动一次SAI通过EDMA的发送传输
{
tx_index++; // 发送缓冲区索引递增,指向下一个待播放的缓冲区
}
if (tx_index == BUFFER_NUMBER) // 如果索引达到缓冲区总数,循环回到第一个缓冲区
{
tx_index = 0U;
}
}

// emptyBlock 变量在这里是关键:
// - 当 EDMA 完成一次 RX (录制) 传输时,`rx_callback` 会被调用,将 `emptyBlock` 减一,表示一个缓冲区被数据填满,不再是空的。
// - 当 EDMA 完成一次 TX (播放) 传输时,`tx_callback` 会被调用,将 `emptyBlock` 加一,表示一个缓冲区的数据被发送完,变为空闲状态。
// - 通过判断 `emptyBlock` 的值,主循环知道哪些缓冲区可以用来录制 (emptyBlock > 0),哪些缓冲区有数据可以播放 (emptyBlock < BUFFER_NUMBER)。
// - 这里的逻辑实现了一个简单的“乒乓”缓冲或者循环缓冲机制,确保录制和播放能够连续进行。
}
}

工作流程和逻辑总结:

  1. 初始化阶段:
  • 系统、引脚、时钟等基础硬件被配置。

  • **音频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_callbackrx_callback)。

  • **SAI被配置为I2S模式**,设置了位宽、声道、同步模式(Rx同步于Tx位时钟,Tx异步)、主从模式等参数。

    此时,Codec是master,SAI是slave。

  • 计算并设置了SAI的 位时钟速率

  • 音频codec (WM8960) 通过I2C接口被初始化和配置(包括音量设置)。


  1. 数据传输阶段 (主循环 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_indextx_index 在达到缓冲区末尾时会循环回0,形成一个循环缓冲队列。

  • 通过这种方式,当录制EDMA完成一个缓冲区时,emptyBlock 减少,主循环就可以尽快启动播放该缓冲区;当播放EDMA完成一个缓冲区时,emptyBlock 增加,主循环就可以尽快将该缓冲区再次用于录制。这实现了音频数据的实时“录放”(Capture-Playback)。

  1. 回调函数 (rx_callbacktx_callback):
  • 这两个函数在对应的EDMA传输完成时由SAI EDMA驱动调用。

  • 它们的主要作用是更新 emptyBlock 的计数器。rx_callback (接收完成) 减少 emptyBlocktx_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): 左右时钟或帧同步信号,指示当前传输的是左声道还是右声道数据,并同步一个音频帧的开始。(也是字选择信号)

时钟的主从关系主要体现在 BCLKLRCLK 上。配置为 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_TxSetBitClockRateSAI_RxSetBitClockRate 这两个函数及其实现的逻辑。它们的核心功能是根据期望的音频参数(采样率、位宽、通道数)来计算并配置SAI模块的 位时钟 (BCLK) 相关的寄存器设置。

1. SAI_TxSetBitClockRate 函数 (配置发送功能的位时钟速率)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/*!
* brief Transmitter bit clock rate configurations.
*
* param base SAI base pointer. // SAI模块的基地址,指向具体的SAI实例(比如SAI1)
* param sourceClockHz, bit clock source frequency. // 位时钟的源频率,通常是MCLK或者其他的SAI时钟源频率
* param sampleRate audio data sample rate. // 音频采样率 (Hz),比如 16000 Hz (16KHz)
* param bitWidth, audio data bitWidth. // 音频数据位宽 (bits),比如 16 bits
* param channelNumbers, audio channel numbers. // 音频通道数,比如 2 (立体声)
*/
void SAI_TxSetBitClockRate(
I2S_Type *base, uint32_t sourceClockHz, uint32_t sampleRate, uint32_t bitWidth, uint32_t channelNumbers)
{
uint32_t tcr2 = base->TCR2; // 读取SAI发送配置寄存器2 (TCR2) 的当前值
uint32_t bitClockDiv = 0; // 用于存储计算出的位时钟分频值
uint32_t bitClockFreq = sampleRate * bitWidth * channelNumbers; // 计算目标位时钟频率: 采样率 * 位宽 * 通道数 = 每秒传输的总位数 = BCLK频率

assert(sourceClockHz >= bitClockFreq); // 断言:源时钟频率必须大于等于目标位时钟频率,否则无法通过分频得到

tcr2 &= ~I2S_TCR2_DIV_MASK; // 清除TCR2寄存器中位时钟分频器 (DIV) 字段的当前值

/* need to check the divided bclk, if bigger than target, then divider need to re-calculate. */
bitClockDiv = sourceClockHz / bitClockFreq; // 初步计算分频值: 源频率 / 目标频率
/* for the condition where the source clock is smaller than target bclk */
if (bitClockDiv == 0U) // 如果源频率小于目标频率(理论上不应该发生,但为了安全检查),分频值至少设为1
{
bitClockDiv++;
}
/* recheck the divider if properly or not, to make sure output blck not bigger than target*/
// 重新检查计算出的分频值是否合适,确保分频后的位时钟频率不会高于目标频率。
// 这里的逻辑是通过 integer division 的特性来确保分频值是足够大的整数。
// 如果 `sourceClockHz / bitClockDiv` 仍然大于 `bitClockFreq`,说明 `bitClockDiv` 偏小,需要再加1。
if ((sourceClockHz / bitClockDiv) > bitClockFreq)
{
bitClockDiv++;
}

{
// 设置TCR2寄存器的DIV字段。
// 注意这里的 `bitClockDiv / 2U - 1UL`:
// 大多数恩智浦微控制器的SAI模块的DIV字段配置的是 (分频值 / 2) - 1。
// 例如,如果需要分频64,DIV寄存器需要设置为 (64 / 2) - 1 = 31。
// 这里的代码就是根据计算出的总分频值 `bitClockDiv` 来得到寄存器实际需要写入的值。
tcr2 |= I2S_TCR2_DIV(bitClockDiv / 2U - 1UL);
}

base->TCR2 = tcr2; // 将修改后的TCR2值写回寄存器,应用配置
}

核心逻辑 (SAI_TxSetBitClockRate):

  1. 计算目标位时钟频率 (bitClockFreq):这是由音频格式(采样率、位宽、通道数)决定的,表示每秒需要传输的总位数。
  2. 计算所需的时钟源到目标位时钟频率的总分频比 (bitClockDiv): 源时钟频率 / 目标位时钟频率
  3. 对计算出的分频比进行调整,确保分频结果不会高于目标频率。
  4. 根据计算出的分频比和硬件寄存器的编码方式(除以2减1)计算要写入寄存器的值。
  5. 将计算出的分频值写入SAI发送配置寄存器2 (TCR2) 的相应位字段 (DIV)。

2. SAI_RxSetBitClockRate 函数 (配置接收功能的位时钟速率)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/*!
* brief Receiver bit clock rate configurations.
*
* param base SAI base pointer. // SAI模块的基地址
* param sourceClockHz, bit clock source frequency. // 位时钟的源频率
* param sampleRate audio data sample rate. // 音频采样率
* param bitWidth, audio data bitWidth. // 音频数据位宽
* param channelNumbers, audio channel numbers. // 音频通道数
*/
void SAI_RxSetBitClockRate(
I2S_Type *base, uint32_t sourceClockHz, uint32_t sampleRate, uint32_t bitWidth, uint32_t channelNumbers)
{
uint32_t rcr2 = base->RCR2; // 读取SAI接收配置寄存器2 (RCR2) 的当前值
uint32_t bitClockDiv = 0; // 用于存储计算出的位时钟分频值
uint32_t bitClockFreq = sampleRate * bitWidth * channelNumbers; // 计算目标位时钟频率,与发送功能相同

assert(sourceClockHz >= bitClockFreq); // 断言:源时钟频率必须大于等于目标位时钟频率

rcr2 &= ~I2S_RCR2_DIV_MASK; // 清除RCR2寄存器中位时钟分频器 (DIV) 字段的当前值

/* need to check the divided bclk, if bigger than target, then divider need to re-calculate. */
bitClockDiv = sourceClockHz / bitClockFreq; // 初步计算分频值
/* for the condition where the source clock is smaller than target bclk */
if (bitClockDiv == 0U) // 检查分频值是否为0
{
bitClockDiv++;
}
/* recheck the divider if properly or not, to make sure output blck not bigger than target*/
// 重新检查分频值,确保分频后的位时钟不高于目标频率
if ((sourceClockHz / bitClockDiv) > bitClockFreq)
{
bitClockDiv++;
}

{
// 设置RCR2寄存器的DIV字段,同样是除以2减1的编码方式
rcr2 |= I2S_RCR2_DIV(bitClockDiv / 2U - 1UL);
}

base->RCR2 = rcr2; // 将修改后的RCR2值写回寄存器,应用配置
}

核心逻辑 (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 和分频器)。

在这个工程中:

  1. MCLK 源: 微控制器 (MIMXRT1060) 通过 SAI 的 MCLK 输出引脚提供 MCLK 给 WM8960 Codec。我们在 main.c 中配置并使能了这个输出 (BOARD_EnableSaiMclkOutput(true))。MCLK 的频率是根据音频 PLL 和 SAI 的时钟分频器设置得出的,main.c 中通过 DEMO_SAI_CLK_FREQ 宏定义来表示这个频率。

  2. 音频格式信息: 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 理论上应该匹配。

  1. 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)。

  1. 代码中的体现:
  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
+----------------------------------------------------------------+
| 微控制器 (MIMXRT1060) |
| |
| +---------------------------+ |
| | Audio PLL & 时钟生成单元 | -- MCLK (主时钟输出) --> +-----+ |
| +---------------------------+ | | |
| | | |
| +---------------------------+ | | |
| | LPI2C1 (I2C控制器) | -- I2C总线 (配置信息) -> | | |
| +---------------------------+ | | |
| | | |
| +---------------------------+ <- BCLK (位时钟输入) ----+ | |
| | SAI1 (串行音频接口) | <- LRCLK (帧同步输入) --+ WM8960 Codec |
| | (BCLK/LRCLK 从设备) | | | |
| | | | (BCLK/LRCLK|
| | +---------------------+ | <--- 数字音频数据 (RX) ---+ 主设备, |
| | | SAI1 RX FIFO (接收) |<--+ | MCLK 输入)|
| | +---------------------+ | EDMA传输请求(RX) | | |
| | | +------------------------+-----+ |
| | | |
| | +---------------------+ | ---> 数字音频数据 (TX) ---> +-----+
| | | SAI1 TX FIFO (发送) |-->+ Codec (通过其DAC输出模拟音频)-->+耳机/扬声器|
| | +---------------------+ | EDMA传输请求(TX) +-----+-------+
| | ^ +------------------------+
| +---------------------------+ |
| | |
| +---------------------------+ |
| | DMAMUX (DMA多路复用器) | -- 路由EDMA请求 --> +-----------+ |
| | ^ | | 麦克风 | |
| +---------------------------+ +-----------+ |
| | EDMA请求 |
| +---------------------------+ |
| | EDMA (增强型DMA控制器) | |
| | (通道 RX 和 TX) | <--- EDMA传输 (数据) |
| | | | |
| | +---------------------+ | ---> EDMA传输 (数据) --> +-------------+ |
| | | EDMA Channel RX | --+ | 内存缓冲区 | |
| | +---------------------+ | +-------------+ |
| | +---------------------+ | <--- EDMA传输 (数据) |
| | | EDMA Channel TX | <--+ |
| | +---------------------+ |
| +---------------------------+ |
+----------------------------------------------------------------+

图解说明:

  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 中的一块区域,用于临时存储录制到的音频数据,以及存放等待播放的音频数据。
  2. 外部设备:

    • WM8960 Codec: 音频编解码器。
      • 接收微控制器提供的 MCLK。
      • 接收微控制器通过 I2C 发送的配置命令。
      • 作为 **BCLK/LRCLK 的主设备 (Master)**,根据 MCLK 和配置信息,内部生成并输出 BCLK 和 LRCLK 给 SAI1。
      • 将来自麦克风的模拟音频转换为数字音频 (ADC),并通过数字音频输出引脚发送给 SAI1 RX FIFO。
      • 将来自 SAI1 TX FIFO 的数字音频数据转换为模拟音频 (DAC),并通过模拟音频输出引脚发送给耳机/扬声器。
    • 麦克风: 提供模拟音频输入。
    • 耳机/扬声器: 输出模拟音频。

时钟和数据流:

  • 时钟流: MCLK 从 MCU 流向 Codec;BCLKLRCLK 从 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
graph LR
subgraph MCU [MCU MIMXRT1060]
Clock(音频PLL 时钟)
I2C(LPI2C1 I2C Master)
SAI[SAI1 BCLK_LRCLK Slave]
DMAMUX[DMAMUX]
EDMA[EDMA Channel RX TX]
Memory[内存缓冲区]
end

Codec[WM8960 Codec<br>BCLK_LRCLK Master<br>MCLK Input]
Mic[麦克风]
Headphones[耳机 扬声器]

%% Clock Flow
Clock --> Codec

%% Configuration Flow
I2C --> Codec

%% Data and Clock Flow (Playback - TX)
Memory --> EDMA
EDMA --> SAI
SAI --> Codec
SAI <-- Codec
Codec --> Headphones

%% Data Flow (Recording - RX)
Mic --> Codec
Codec --> SAI
SAI --> DMAMUX
DMAMUX --> EDMA
EDMA --> Memory

%% Note on EDMA Trigger: SAI FIFOs trigger EDMA requests via DMAMUX
SAI --> DMAMUX
DMAMUX --> EDMA
style Codec stroke:#FF0000
作者

Gavin

发布于

2025-05-28

更新于

2025-05-28

许可协议

CC BY-NC-SA 4.0

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×