6.8.5. 设计说明

6.8.5.1. 源码说明

源代码位于:linux-5.10/sound/soc/zx/aic-i2s.c

6.8.5.2. 模块架构

内核中音频采用ALSA驱动框架,该框架管理所有与音频相关的软件与硬件,I2S的驱动设计需要遵循该框架的基本要求。ALSA音频框架将底层的硬件驱动分为三个部分:machine、pltform与codec。三者的关系如下图所示:

../../../_images/design_11.png

ALSA框架将底层划分为三部分后,使得platform和codec的驱动实现变得更加简单,二者只专注于实现自己的功能代码,由machine驱动来实现platform和codec的耦合,二者依靠cpu_dai和codec_dai进行数据传输。platform驱动的主要作用是完成音频数据的管理,最终通过SOC的数字音频接口(cpu_dai)把音频数据传送给codec进行播放或将codec采集的音频数据存储到内存中。

在具体的实现上,ALSA将platform驱动(platform可以简单理解为SOC端的驱动)分为两部分:实现音频数据传输和管理的DMA驱动和CPU DAI的驱动。且ALSA框架中已实现了DMA对音频数据管理部分的驱动 代码,所以I2S的驱动只需要实现CPU DAI部分的驱动,音频数据管理部分只需要指定数据传输的起始地址或目的地址,以及传输位宽即可。

6.8.5.2.1. CPU DAI驱动

在驱动实现上,无论是codec还是platform,ALSA将它们统一划分为snd_soc_component和snd_soc_dai,所以,就要相应的实现snd_soc_component_driver和snd_soc_dai_driver。 然后调用snd_soc_register_component进行统一注册。snd_soc_component_driver主要是注册与dapm相关的音频控件等信息,snd_soc_dai_driver主要是注册数字音频接口I2S或PCM等的信息及底层操作函数。

由于在platform端,主要是I2S接口和DMA的传输配置,不存在音频控件,所以CPU DAI的驱动主要是实现snd_soc_dai_driver。包括指定I2S接口支持的通道数、采样率、支持的数据格式,以及对I2S配置和控制的回调函数集合snd_soc_dai_ops的实现

6.8.5.2.2. 音频DMA驱动

ALSA架构中,对DMA的一些配置和传输的函数已经由ALSA框架实现,所以这部分驱动实现只需要指定playback和capture中DMA传输的地址以及传输的位宽,然后调用devm_snd_dmaengine_pcm_register 进行注册即可。

6.8.5.3. 关键流程设计

6.8.5.3.1. 操作函数集实现

在I2S的驱动设计中,snd_soc_dai_ops是一个非常重要的结构体,它是cpu_dai的操作函数集,所有对I2S接口的设置都是通过此结构体完成。所以,I2S驱动中一项非常重要的部分就是实现此结构体中的函数接口。snd_soc_dai_ops函数集可以分为如下几个部分:

  1. cpu_dai时钟配置函数,通常由machine驱动调用

    • set_sysclk:设置cpu_dai的主时钟MCLK

    • set_clkdiv:设置分频系数,用于实现BCLK和LRCK的分频系数

    • set_bclk_ratio:设置BCLK和LRCK的比率

  2. cpu_dai格式设置,通常由machine驱动调用

    • set_fmt:设置主从模式(LRCK和BCLK时钟由SOC提供还是由codec提供),BCLK和LRCK的极性,以及传输模式

    • set_tdm_slot:cpu_dai支持时分复用时,用于设置时分复用的slot

    • set_channel_map:声道时分复用时的映射关系设置

  3. ALSA PCM音频操作,由ALSA的soc-core在执行音频操作时调用

    • hw_params:硬件参数设置,一般用于采样精度,通道位宽的设置

    • trigger:命令触发函数,用于执行音频数据传输的开始、结束、暂停、恢复等

在I2S的驱动中,需要实现的接口有:

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,
};

在实现的几个接口函数中,除hw_params和trigger外,其它函数是需要在machine驱动中根据I2S和codec双方所支持的格式、时钟等进行调用设置的,使I2S和codec两边的格式设置相同。

6.8.5.3.2. I2S时钟设置

6.8.5.3.2.1. MCLK

MCLK是I2S的主时钟,主要作用是向外部的codec芯片提供工作时钟,由I2S模块的工作时钟分频得到。在驱动中由aic_i2s_set_sysclk设置MCLK的频率,MCLK一般采用128fs,256fs,512fs的表示方式,具体的设置需要参考实际使用的codec芯片规格书。Fs是采样频率,常见的采样频率有44.1khz,48khz,32khz等,可以据此算出MCLK的频率值。一般会在machine驱动中调用设置MCLK的函数。

6.8.5.3.2.2. LRCK和BCLK

LRCK是左右声道时钟。LRCK的时钟频率等于fs,在M4中,通过LRCK_PERIOD位域设置LRCK的频率,LRCK_PERIOD表示一个LRCK时钟周期内,有多少个BCLK周期。在I2S模式下,若为立体声(2通道),32bit采样深度,则BCLK=64fs,则LRCK_PERIOD应设置为(64/2-1)。若为4通道,24bit采样深度,则BCLK=96fs,则LRCK_PERIOD应设置为(96/2-1)。由采样频率可以算出BCLK时钟的频率。并由BCLK的频率算出LRCK,即采样率。

6.8.5.3.3. period bytes对齐

在使用DMA传输音频数据时,DMA要求每次传输的数据长度必须128bytes/8bytes对齐。在ALSA框架下,音频数据以period为周期调用DMA传输,每次传输的数据长度为period bytes。所以,必须满足period 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,
        "zx,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;
}

6.8.5.4. 数据结构设计

6.8.5.4.1. aic_i2s

struct aic_i2s {
        struct clk *clk;
        struct reset_control *rst;
        struct regmap *regmap;

        struct snd_dmaengine_dai_dma_data playback_dma_data;
        struct snd_dmaengine_dai_dma_data capture_dma_data;
        unsigned int mclk_freq;
        unsigned int bclk_ratio;
        unsigned int format;
        unsigned int slots;
        unsigned int slot_width;
};

部分变量说明:

  • playback_dma_data:播放时的音频数据结构,用于配置DMA传输的目的地址,数据宽度等信息

  • capture_dma_data:录音时音频数据结构,用于配置DMA传输的起始地址,数据宽度等信息

  • mclk_freq:I2S的MCLK时钟频率

  • bclk_ratio:LRCK与BCLK的比率

  • format:设置I2S的传输格式

  • slots:设置I2S的通道数

  • slot_width:设置I2S的每个通道占用位数

6.8.5.4.2. aic_i2s_clk_div

struct aic_i2s_clk_div {
        u8 div; /* bclk和mclk的分频系数 */
        u8 val; /* 分频系数div所对应的寄存器的值 */
};

6.8.5.5. 接口设计

6.8.5.5.1. aic_i2s_set_sysclk

函数原型

static int aic_i2s_set_sysclk(struct snd_soc_dai *dai, int clk_id, unsigned int freq, int dir)

功能说明

设置I2S模块输出的mclk时钟频率

参数定义

dai:指向cpu_dai的指针 | clk_id:要设置的时钟id | freq:设置的时钟频率 | dir: unused

返回值

0:执行成功 | -EINVAL:参数非法

注意事项

6.8.5.5.2. aic_i2s_set_bclk_ratio

函数原型

static int aic_i2s_set_bclk_ratio(struct snd_soc_dai *dai, unsigned int ratio)

功能说明

设置I2S模块LRCK与BCLK时钟频率的比率

参数定义

dai:指向cpu_dai的指针 | ratio:需要设置的比率

返回值

0:执行成功 | -EINVAL:参数非法

注意事项

6.8.5.5.3. aic_i2s_set_fmt

函数原型

static int aic_i2s_set_fmt(struct snd_soc_dai *dai, unsigned int fmt)

功能说明

设置I2S模块的格式

参数定义

dai:指向cpu_dai的指针 | fmt:需要设置的格式

返回值

0:执行成功 | -EINVAL:参数非法

注意事项

通过该函数可以设置的格式有: | 1. I2S的主从模式 | 2. BCLK和LRCK的极性 | 3. I2S的数据格式

6.8.5.5.4. aic_i2s_set_tdm_slot

函数原型

static int aic_i2s_set_tdm_slot(struct snd_soc_dai *dai,unsigned int tx_mask, unsigned int rx_mask,int slots, int slot_width)

功能说明

设置I2S模块TDM模式下的通道个数和宽度

参数定义

dai:指向cpu_dai的指针 | tx_mask:tx slot的mask | rx_mask:rx slot的mask | slots:设置的通道个数 | slot_width:设置的通道宽度

返回值

0:执行成功 | -EINVAL:参数非法

注意事项

6.8.5.5.5. aic_i2s_hw_params

函数原型

static int aic_i2s_hw_params(struct snd_pcm_substream *substream, struct snd_pcm_hw_params *params, struct snd_soc_dai *dai)

功能说明

设置I2S模块硬件参数

参数定义

substream:指向playback或capture的substream | params:指向硬件参数指针 | dai:指向cpu_dai的指针

返回值

0:执行成功 | -EINVAL:参数非法

注意事项

通过该函数,可以设置采样精度,帧率,以及时钟等参数

6.8.5.5.6. aic_i2s_trigger

函数原型

static int aic_i2s_trigger(struct snd_pcm_substream *substream, int cmd, struct snd_soc_dai *dai)

功能说明

I2S的触发函数

参数定义

substream:指向playback或capture的substream | cmd:触发的命令 | dai:指向cpu_dai的指针

返回值

0:执行成功 | -EINVAL:参数非法

注意事项

通过该函数,可以开始或停止音频的播放或录音