PCM data flow - 3 - ASoC codec driver

来源:互联网 发布:javascript教材 编辑:程序博客网 时间:2024/05/22 00:30

上一章提到codec_drv的几个组成部分,下面逐一介绍,基本是以内核文档Documentation/sound/alsa/soc/codec.txt中的内容为脉络来分析的。codec的作用,在概述中有说明,本章主要罗列下codec driver中重要的数据结构及注册流程。


Codec DAI and PCM configuration


codec_dai和pcm配置信息通过结构体snd_soc_dai_driver描述,包括dai的能力描述和操作接口,snd_soc_dai_driver最终会被注册到asoc-core中。

/* * Digital Audio Interface Driver. * * Describes the Digital Audio Interface in terms of its ALSA, DAI and AC97 * operations and capabilities. Codec and platform drivers will register this * structure for every DAI they have. * This structure covers the clocking, formating and ALSA operations for each * interface. */struct snd_soc_dai_driver {    /* DAI description */    const char *name;    unsigned int id;    int ac97_control;    /* DAI driver callbacks */    int (*probe)(struct snd_soc_dai *dai);    int (*remove)(struct snd_soc_dai *dai);    int (*suspend)(struct snd_soc_dai *dai);    int (*resume)(struct snd_soc_dai *dai);    /* ops */    const struct snd_soc_dai_ops *ops;    /* DAI capabilities */    struct snd_soc_pcm_stream capture;    struct snd_soc_pcm_stream playback;    unsigned int symmetric_rates:1;    /* probe ordering - for components with runtime dependencies */    int probe_order;    int remove_order;};

·          name:codec_dai的名称标识,machine中的dai_link通过codec_dai_name来匹配codec_dai;

·          probe:codec_dai的probe函数,由snd_soc_instantiate_card回调;

·          playback:回放能力描述信息,如所支持的声道数、采样率、音频格式;

·          capture:录制能力描述信息,如所支持声道数、采样率、音频格式;

·          ops:指向codec_dai的操作函数集,这些函数集非常重要,它定义了dai的时钟配置、格式配置、硬件参数配置等回调。

例子,wm8994有三个dai,这里只列其一:

static const struct snd_soc_dai_ops wm8994_aif1_dai_ops = {    .set_sysclk = wm8994_set_dai_sysclk,    .set_fmt    = wm8994_set_dai_fmt,    .hw_params  = wm8994_hw_params,    .shutdown   = wm8994_aif_shutdown,    .digital_mute   = wm8994_aif_mute,    .set_pll    = wm8994_set_fll,    .set_tristate   = wm8994_set_tristate,};static struct snd_soc_dai_driver wm8994_dai[] = {    {        .name = "wm8994-aif1",        .id = 1,        .playback = {            .stream_name = "AIF1 Playback",            .channels_min = 1,            .channels_max = 2,            .rates = WM8994_RATES,            .formats = WM8994_FORMATS,            .sig_bits = 24,        },        .capture = {            .stream_name = "AIF1 Capture",            .channels_min = 1,            .channels_max = 2,            .rates = WM8994_RATES,            .formats = WM8994_FORMATS,            .sig_bits = 24,         },        .ops = &wm8994_aif1_dai_ops,    },    // ...省略...


Codec control IO


移动设备的音频codec,其控制接口一般是I2C或SPI。codec的读写接口访问操作codec寄存器,当dapm触发或mixer控件发生改变时,需要调用到读写接口。

在snd_soc_codec_driver结构体中,有如下字段描述codec的控制接口:

    /* codec IO */    unsigned int (*read)(struct snd_soc_codec *, unsigned int);    int (*write)(struct snd_soc_codec *, unsigned int, unsigned int);    int (*display_register)(struct snd_soc_codec *, char *,                size_t, unsigned int);    int (*volatile_register)(struct snd_soc_codec *, unsigned int);    int (*readable_register)(struct snd_soc_codec *, unsigned int);    int (*writable_register)(struct snd_soc_codec *, unsigned int);    unsigned int reg_cache_size;    short reg_cache_step;    short reg_word_size;    const void *reg_cache_default;    short reg_access_size;    const struct snd_soc_reg_access *reg_access_default;    enum snd_soc_compress_type compress_type;

·          read:读取codec寄存器接口;

·          write:写入codec寄存器接口;

·          volatile_register:判断指定的寄存器是否是volatile属性;假如是,则asoc-core读取它的时候不会只是读cache,会直接通过控制接口访问硬件IO;

·          readable_register:判断指定的寄存器是否可读;

·          reg_cache_default:codec寄存器的缺省值;

·          reg_cache_size:codec缺省的寄存器值数组大小;

·          reg_word_size:codec寄存器值宽度。


在Linux-3.4.5中,这里面很多信息都改用regmap描述了。asoc-core中判断是否用的是regmap,如果是,则调用regmap接口。见如下函数:

int snd_soc_update_bits(struct snd_soc_codec *codec, unsigned short reg,                unsigned int mask, unsigned int value){    bool change;    unsigned int old, new;    int ret;    if (codec->using_regmap) {        // 当前使用regmap,调用regmap接口,其中codec->control_data是regmap私有数据        ret = regmap_update_bits_check(codec->control_data, reg,                           mask, value, &change);    } else {        // 非regmap,调用snd_soc_codec_driver定义的read/write回调        ret = snd_soc_read(codec, reg);        if (ret < 0)            return ret;        old = ret;        new = (old & ~mask) | (value & mask);        change = old != new;        if (change)            ret = snd_soc_write(codec, reg, new);    }    if (ret < 0)        return ret;    return change;}

使用regmap,使得控制接口抽象化,codec_drv不用操心当前控制方式是什么;

regmap在线调试目录是/sys/kernel/debug/regmap

关于wm8994的regmap描述,请自行查阅driver/mfd/wm8994-regmap.c。


Mixers and audio controls


音频控件多用于部件开关和音量的设定,音频控件可通过soc.h中的宏来定义。例如单一型控件:

#define SOC_SINGLE(xname, reg, shift, max, invert) \{   .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname, \    .info = snd_soc_info_volsw, .get = snd_soc_get_volsw,\    .put = snd_soc_put_volsw, \    .private_value =  SOC_SINGLE_VALUE(reg, shift, max, invert) }

这种控件只有一个设置量,一般用于部件开关。宏定义的参数说明:

·          xname:控件的名称标识;

·          reg:控件对应的寄存器地址;

·          shift:控件控制位在寄存器中的偏移;

·          max:控件设置值范围,一般来说,如果控制位只有1位的话,那么max=1,因为设置值只有0和1;

·          invert:设定值是否取反。

其他类型控件类似,不一一介绍了。

上述只是宏定义,音频控件真正的结构是snd_kcontrol_new

struct snd_kcontrol_new {    snd_ctl_elem_iface_t iface; /* interface identifier */    unsigned int device;        /* device/client number */    unsigned int subdevice;     /* subdevice (substream) number */    const unsigned char *name;  /* ASCII name of item */    unsigned int index;     /* index of item */    unsigned int access;        /* access rights */    unsigned int count;     /* count of same elements */    snd_kcontrol_info_t *info;    snd_kcontrol_get_t *get;    snd_kcontrol_put_t *put;    union {        snd_kcontrol_tlv_rw_t *c;        const unsigned int *p;    } tlv;    unsigned long private_value;};

codec初始化时,通过snd_soc_add_codec_controls把所有定义好的音频控件注册到alsa-core中,上层可以通过tinymix、alsa_amixer等工具查看修改这些控件的设定。

音频控件的详细分析见dapm系列文章。


Codec audio operations


Codec的音频操作接口通过结构体snd_soc_dai_ops描述:

struct snd_soc_dai_ops {    /*     * DAI clocking configuration, all optional.     * Called by soc_card drivers, normally in their hw_params.     */    int (*set_sysclk)(struct snd_soc_dai *dai,        int clk_id, unsigned int freq, int dir);    int (*set_pll)(struct snd_soc_dai *dai, int pll_id, int source,        unsigned int freq_in, unsigned int freq_out);    int (*set_clkdiv)(struct snd_soc_dai *dai, int div_id, int div);    /*     * DAI format configuration     * Called by soc_card drivers, normally in their hw_params.     */    int (*set_fmt)(struct snd_soc_dai *dai, unsigned int fmt);    int (*set_tdm_slot)(struct snd_soc_dai *dai,        unsigned int tx_mask, unsigned int rx_mask,        int slots, int slot_width);    int (*set_channel_map)(struct snd_soc_dai *dai,        unsigned int tx_num, unsigned int *tx_slot,        unsigned int rx_num, unsigned int *rx_slot);    int (*set_tristate)(struct snd_soc_dai *dai, int tristate);    /*     * DAI digital mute - optional.     * Called by soc-core to minimise any pops.     */    int (*digital_mute)(struct snd_soc_dai *dai, int mute);    /*     * ALSA PCM audio operations - all optional.     * Called by soc-core during audio PCM operations.     */    int (*startup)(struct snd_pcm_substream *,        struct snd_soc_dai *);    void (*shutdown)(struct snd_pcm_substream *,        struct snd_soc_dai *);    int (*hw_params)(struct snd_pcm_substream *,        struct snd_pcm_hw_params *, struct snd_soc_dai *);    int (*hw_free)(struct snd_pcm_substream *,        struct snd_soc_dai *);    int (*prepare)(struct snd_pcm_substream *,        struct snd_soc_dai *);    int (*trigger)(struct snd_pcm_substream *, int,        struct snd_soc_dai *);    /*     * For hardware based FIFO caused delay reporting.     * Optional.     */    snd_pcm_sframes_t (*delay)(struct snd_pcm_substream *,        struct snd_soc_dai *);};

注释比较详细的了,codec音频操作接口分为5大部分:时钟配置、格式配置、数字静音、PCM音频接口、FIFO延迟报告。着重说下时钟配置及格式配置接口:

·          set_sysclk:codec_dai的系统时钟设置,当上层pcm_open时,需要回调该接口设置codec的系统时钟,codec才能正常工作;

·          set_pll:Codec FLL设置,codec一般有个MCLK的输入时钟,回调该接口基于MCLK来设定codec FLL的时钟频率;得到正确的FLL时钟频率后,则codec的sysclk、bclk、lrclk均可从FLL分频出来(假设codec作为master);

·          set_fmt:codec_dai的格式设置,相关格式见soc-dai.h定义;

·          SND_SOC_DAIFMT_I2S——音频数据是I2S格式;

·          SND_SOC_DAIFMT_DSP_A——音频数据是语音常用的PCM格式;

·          SND_SOC_DAIFMT_CBM_CFM——codec作为master,BCLK和LRCLK由codec提供;

·          SND_SOC_DAIFMT_CBS_CFS——codec作为 slave ,BCLK和LRCLK由soc platform提供;

·          hw_params:codec_dai的硬件参数设置,根据上层设定的声道数、采样率、数据格式,来设置codec_dai相关寄存器。


以上接口一般在Machine驱动中回调,我们看看Machine驱动goni_wm8994.c的goni_hifi_hw_params函数:
static int goni_hifi_hw_params(struct snd_pcm_substream *substream,        struct snd_pcm_hw_params *params){    struct snd_soc_pcm_runtime *rtd = substream->private_data;    struct snd_soc_dai *codec_dai = rtd->codec_dai;    struct snd_soc_dai *cpu_dai = rtd->cpu_dai;    unsigned int pll_out = 24000000; // 这是MCLK的时钟频率,codec的源时钟    int ret = 0;    /* set the cpu DAI configuration */    ret = snd_soc_dai_set_fmt(cpu_dai, SND_SOC_DAIFMT_I2S |            SND_SOC_DAIFMT_NB_NF | SND_SOC_DAIFMT_CBM_CFM);    if (ret < 0)        return ret;    /* set codec DAI configuration */    ret = snd_soc_dai_set_fmt(codec_dai, SND_SOC_DAIFMT_I2S |            SND_SOC_DAIFMT_NB_NF | SND_SOC_DAIFMT_CBM_CFM);    if (ret < 0)        return ret;    /* set the codec FLL */    ret = snd_soc_dai_set_pll(codec_dai, WM8994_FLL1, 0, pll_out,            params_rate(params) * 256);    if (ret < 0)        return ret;    /* set the codec system clock */    ret = snd_soc_dai_set_sysclk(codec_dai, WM8994_SYSCLK_FLL1,            params_rate(params) * 256, SND_SOC_CLOCK_IN);    if (ret < 0)        return ret;    return 0;}

其中snd_soc_dai_set_fmt()实际上会调用cpu_dai或codec_dai的set_fmt回调,

snd_soc_dai_set_pll()、snd_soc_dai_set_sysclk()类似。

·          MCLK作为codec的源时钟,频率为24Mhz;

·          设置cpu_dai和codec_dai格式:I2S,BCLK和LRCLK由codec提供;

·          设置codec_dai的FLL1:时钟源是MCLK,时钟源频率是24Mhz,目的时钟频率是256fs;

·          设置codec_dai的系统时钟:时钟源是FLL1,系统时钟频率是256fs。


codec_dai的时钟设置非常重要,设置错误将会导致很多问题,典型如下:

·          无声-->> 检查codec的系统时钟、codec_dai的位时钟和帧时钟是否使能;

·          声音失真-->> 检查音频源数据的采样率是否和codec_dai的帧时钟一致;

·          声音断续 -->> 检查codec的系统时钟和位时钟、帧时钟是否同步,出现这种情况,可能是sysclk和BCLK/LRCLK不是由同一个时钟源分频出来导致的,取决于具体codec设计。


DAPM description


概念:Dynamic Audio Power Management,动态音频电源管理,为移动Linux设备设计,使得音频系统任何时候都工作在最低功耗状态。

目的:使能必要的最少的部件,令音频系统正常工作。

原理:当音频路径发生改变(比如上层使用tinymix工具设置音频通路)时,或发生数据流事件(比如启动或停止播放)时,都会触发dapm去遍历所有邻近的音频部件,检查是否存在完整的音频路径(complete path:该部件必须往前能到达输入端点如DAC/Mic/Linein,往后能到达输出端点如ADC/HP/SPK),如果存在完整的音频路径,则该路径上面的所有部件都是需要上电的,其他部件则下电。

对于此的完整分析,见另一个系列的文章。

说说DAPM部件中最典型的mixer widget:Mixes several analog signals into a single analog signal. 它可以把几路模拟信号混合到一路输出。如WM8994的SPKMIXL:


如图,SPKMIXL有5路输入,分别是:MIXINL、IN1LP、DAC1L、DAC2L、MIXEROUTL,因此这里可以构成5条路径。

·          1、如下5个控件控制SPKMIXL输入:

static const struct snd_kcontrol_new left_speaker_mixer[] = {SOC_DAPM_SINGLE("DAC2 Switch", WM8994_SPEAKER_MIXER, 9, 1, 0),SOC_DAPM_SINGLE("Input Switch", WM8994_SPEAKER_MIXER, 7, 1, 0),SOC_DAPM_SINGLE("IN1LP Switch", WM8994_SPEAKER_MIXER, 5, 1, 0),SOC_DAPM_SINGLE("Output Switch", WM8994_SPEAKER_MIXER, 3, 1, 0),SOC_DAPM_SINGLE("DAC1 Switch", WM8994_SPEAKER_MIXER, 1, 1, 0),};
·          2、定义SPKMIXL的dapm widget:
SND_SOC_DAPM_MIXER_E("SPKL", WM8994_POWER_MANAGEMENT_3, 8, 0,             left_speaker_mixer, ARRAY_SIZE(left_speaker_mixer),             late_enable_ev, SND_SOC_DAPM_PRE_PMU),
留意WM8994_POWER_MANAGEMENT_3寄存器的bit8正是控制SPKMIXL通断电的。

·          3、定义SPKMIXL相关路由:

static const struct snd_soc_dapm_route intercon[] = {    // ...    { "SPKL", "DAC1 Switch", "DAC1L" },    { "SPKL", "DAC2 Switch", "DAC2L" },

最终上层会看到两个控件:"SPKL DAC1 Switch","SPKL DAC2 Switch";前者用于SPKL选中DAC1L作为输入,后者用于SPKL选中DAC2L作为输入。

但控件"SPKL DAC1 Switch"或"SPKL DAC2 Switch"的打开,不代表能使得"SPKL"上电。只有当SPKL位于完整的音频路径中时,SPKL才会上电,WM8994_POWER_MANAGEMENT_3寄存器的bit8会被置为1。


Codec register


当platform_driver:

static struct platform_driver wm8994_codec_driver = {    .driver = {        .name = "wm8994-codec",        .owner = THIS_MODULE,        .pm = &wm8994_pm_ops,    },    .probe = wm8994_probe,    .remove = __devexit_p(wm8994_remove),};

与name ="wm8994-codec"的platform_device(该platform_device在driver/mfd/wm8994-core.c中创建并初始化完成)匹配后,立即回调wm8994_probe注册codec:

static int __devinit wm8994_probe(struct platform_device *pdev){    struct wm8994_priv *wm8994;    wm8994 = devm_kzalloc(&pdev->dev, sizeof(struct wm8994_priv),                  GFP_KERNEL);    if (wm8994 == NULL)        return -ENOMEM;    platform_set_drvdata(pdev, wm8994);    wm8994->wm8994 = dev_get_drvdata(pdev->dev.parent);    wm8994->pdata = dev_get_platdata(pdev->dev.parent);    return snd_soc_register_codec(&pdev->dev, &soc_codec_dev_wm8994,            wm8994_dai, ARRAY_SIZE(wm8994_dai));}


snd_soc_register_codec:将codec_driver和codec_dai_driver注册到asoc-core。

/** * snd_soc_register_codec - Register a codec with the ASoC core * * @codec: codec to register */int snd_soc_register_codec(struct device *dev,               const struct snd_soc_codec_driver *codec_drv,               struct snd_soc_dai_driver *dai_drv,               int num_dai)
创建一个snd_soc_codec实例,包含codec_drv(snd_soc_dai_driver)的相关信息,封装给asoc-core使用,相关代码段如下:
    struct snd_soc_codec *codec;    dev_dbg(dev, "codec register %s\n", dev_name(dev));    codec = kzalloc(sizeof(struct snd_soc_codec), GFP_KERNEL);    if (codec == NULL)        return -ENOMEM;    /* create CODEC component name */    codec->name = fmt_single_name(dev, &codec->id);    if (codec->name == NULL) {        kfree(codec);        return -ENOMEM;    }    // 初始化codec的寄存器缓存配置及读写接口    codec->write = codec_drv->write;    codec->read = codec_drv->read;    codec->volatile_register = codec_drv->volatile_register;    codec->readable_register = codec_drv->readable_register;    codec->writable_register = codec_drv->writable_register;    codec->ignore_pmdown_time = codec_drv->ignore_pmdown_time;    codec->dapm.bias_level = SND_SOC_BIAS_OFF;    codec->dapm.dev = dev;    codec->dapm.codec = codec;    codec->dapm.seq_notifier = codec_drv->seq_notifier;    codec->dapm.stream_event = codec_drv->stream_event;    codec->dev = dev;    codec->driver = codec_drv;    codec->num_dai = num_dai;    mutex_init(&codec->mutex);

把以上创建并初始化好的codec插入到codec_list链表上,Machine驱动初始化时会遍历该链表,以匹配并绑定dai_link上的codec:

list_add(&codec->list, &codec_list);

把codec_drv中的snd_soc_dai_driver(wm8994有3个dai,分别是aif1、aif2和aif3)注册到asoc-core:

    /* register any DAIs */    if (num_dai) {        ret = snd_soc_register_dais(dev, dai_drv, num_dai);        if (ret < 0)            goto fail;    }

snd_soc_register_dais会把dai插入到dai_list链表中:list_add(&dai->list,&dai_list); Machine驱动初始化时会遍历该链表,以匹配并绑定dai_link上的codec_dai。


最后顺便提下dai_link中的codec和codec_dai的区别:codec指音频codec共有的部分,包括codec初始化函数、控制接口、寄存器缓存、控件列表、dapm部件列表、音频路由列表、偏置电压设置函数等描述信息;而codec_dai指codec上的音频接口驱动描述,各个接口的描述信息不一定都是一致的,所以各个音频接口都有它自身的驱动描述,包括音频接口的初始化函数、操作函数集、能力描述等。

我开始时认为:codec_dai从属于codec,dai_link没有必要同时声明codec和codec_dai,应该可以实现codec_dai就能找到它对应的父设备codec的方法。后来想到系统上如果有两个以上的codec,而恰好不同codec上的codec_dai有重名的话,此时就必须同时声明codec和codec_dai才能找到正确的音频接口了。

1 0
原创粉丝点击