关键流程设计
操作函数集实现
在 S 的驱动设计中,snd_soc_dai_ops 是一个非常重要的结构体,它是 dai 的操作函数集,所有对 S 接口的设置都是通过此结构体完成。所以,I2S 驱动中一项非常重要的部分就是实现此结构体中的函数接口。snd_soc_dai_ops 函数集可以分为如下几个部分:
-
cpu_dai 时钟配置函数,通常由 machine 驱动调用
-
set_sysclk:设置 dai 的主时钟 MCLK
-
set_clkdiv:设置分频系数,用于实现 BCLK 和 LRCK 的分频系数
-
set_bclk_ratio:设置 BCLK 和 LRCK 的比率
-
-
cpu_dai 格式设置,通常由 machine 驱动调用
-
set_fmt:设置主从模式(LRCK 和 BCLK 时钟由 SOC 提供还是由 codec 提供),BCLK 和 LRCK 的极性,以及传输模式
-
set_tdm_slot:cpu_dai 支持时分复用时,用于设置时分复用的 slot
-
set_channel_map:声道时分复用时的映射关系设置
-
-
ALSA PCM 音频操作,由 ALSA 的 soc-core 在执行音频操作时调用
-
hw_params:硬件参数设置,一般用于采样精度,通道位宽的设置
-
trigger:命令触发函数,用于执行音频数据传输的开始、结束、暂停、恢复等
-
static const struct snd_soc_dai_ops aic_i2s_dai_ops = {
.set_sysclk = aic_i2s_set_sysclk,
.set_bclk_ratio = aic_i2s_set_bclk_ratio,
.set_fmt = aic_i2s_set_fmt,
.set_tdm_slot = aic_i2s_set_tdm_slot,
.hw_params = aic_i2s_hw_params,
.trigger = aic_i2s_trigger,
};
在实现的几个接口函数中,除 params 和 trigger 外,其它函数是需要在 machine 驱动中根据 I2S 和 codec 双方所支持的格式、时钟等进行调用设置的,使 S 和 codec 两边的格式设置相同。
I2S 时钟设置
- MCLK
主时钟设置。
MCLK 是 S 的主时钟,主要作用是向外部的 codec 芯片提供工作时钟,由 S 模块的工作时钟分频得到。在驱动中由 sysclk 设置 MCLK 的频率,MCLK 一般采用 128fs,256fs,512fs 的表示方式,具体的设置需要参考实际使用的 codec 芯片规格书。Fs 是采样频率,常见的采样频率有 44.1khz,48khz,32khz 等,可以据此算出 MCLK 的频率值。一般会在 machine 驱动中调用设置 MCLK 的函数。
- LRCK 和 BCLK
左右声道时钟设置。
LRCK 是左右声道时钟。LRCK 的时钟频率等于 fs,在 D211 中,通过 PERIOD 位域设置 LRCK 的频率,LRCK_PERIOD 表示一个 LRCK 时钟周期内,有多少个 BCLK 周期。在 S 模式下,若为立体声(2 通道),32bit 采样深度,则 BCLK=64fs,则 PERIOD 应设置为(64/2-1)。若为 4 通道,24bit 采样深度,则 BCLK=96fs,则 PERIOD 应设置为(96/2-1)。由采样频率可以算出 BCLK 时钟的频率。并由 BCLK 的频率算出 LRCK,即采样率。
- period
bytes 对齐在使用 DMA 传输音频数据时,DMA 要求每次传输的数据长度必须 128bytes/8bytes 对齐。在 ALSA 框架下,音频数据以 period 为周期调用 DMA 传输,每次传输的数据长度为 bytes。所以,必须满足 bytes 按照 128bytes/8bytes 对齐。ALSA 中提供了相应的 API 接口(snd_pcm_hw_constraint_step)来满足这一需求。
static int aic_i2s_startup(struct snd_pcm_substream *substream, struct snd_soc_dai *dai) { int ret; /* Make sure that the period bytes are 8/128 bytes aligned according to * the DMA transfer requested. */ if (of_device_is_compatible(dai->dev->of_node, "artinchip,aic-i2s-v1.0")) { ret = snd_pcm_hw_constraint_step(substream->runtime, 0, SNDRV_PCM_HW_PARAM_PERIOD_BYTES, 8); if (ret < 0) { dev_err(dai->dev, "Could not apply period step: %d\n", ret); return ret; } ret = snd_pcm_hw_constraint_step(substream->runtime, 0, SNDRV_PCM_HW_PARAM_BUFFER_BYTES, 8); if (ret < 0) { dev_err(dai->dev, "Could not apply buffer step: %d\n", ret); return ret; } } else { ret = snd_pcm_hw_constraint_step(substream->runtime, 0, SNDRV_PCM_HW_PARAM_PERIOD_BYTES, 128); if (ret < 0) { dev_err(dai->dev, "Could not apply period step: %d\n", ret); return ret; } ret = snd_pcm_hw_constraint_step(substream->runtime, 0, SNDRV_PCM_HW_PARAM_BUFFER_BYTES, 128); if (ret < 0) { dev_err(dai->dev, "Could not apply buffer step: %d\n", ret); return ret; } } return ret; }
Record 流程
-
init
流程-
初始化 DMA 传输的起始地址、buf_len 以及 period_len。
I2S 模块使用 DMA 传输音频数据,DMA 采用环形链表形式,依次将音频数据传送到硬件。所以需要配置 DMA 传输时的起始地址(即 RX buffer 地址)以及 buf_len,period_len。
-
注册 HAL 层的回调函数。
在 Driver 层驱动,将 buf_len 配置为 period_len 的 2 倍,DMA 每传输 period_len 长度的数据,触发一次 DMA 中断,通知 CPU 向 pipe 设备写入数据。
-
-
start
流程按照 RT-Thread audio 的框架,在执行 rt_device_open 时,就会调用
start
流程,开始音频的录制,然后再通过 rt_device_control 设置音频的格式(采样率,通道数等)。按照这个流程,最开始可能会录制一段不符合设置的音频格式的数据,这显然是不合理的。所以,在 Driver 层的驱动实现中,start
流程并未做任何处理,而是在设置完音频格式后才开始音频的录制。 -
DMA 中断流程
DMA 每传输完 period_len 长度的数据后,触发一次 DMA 中断,然后通过 DMA 回调函数的逐级调用,最终调用 rt_audio_rx_done,将 RX buffer 的数据写入到 pipe 设备,每次写入 period_len 长度的音频数据。