Linux ALSA驱动框架(六)--ASoC架构中的Platfrom

(1)
Platform驱动在ASoC中的作用

ASoC被分为Machine,Platform和Codec三大部件,Platform驱动的主要作用是完成音频数据的管理,最终通过CPU的数字音频接口(DAI)把音频数据传送给Codec进行处理,最终由Codec输出驱动耳机或者是喇叭的音信信号。在具体实现上,ASoC有把Platform驱动分为两个部分:snd_soc_platform_driver和snd_soc_dai_driver。其中,platform_driver负责管理音频数据,把音频数据通过dma或其他操作传送至cpu dai中,dai_driver则主要完成cpu一侧的dai的参数配置,同时也会通过一定的途径把必要的dma等参数与snd_soc_platform_driver进行交互。
snd_soc_register_platform-->snd_soc_platform_driver
snd_soc_register_codec--->snd_soc_dai_driver

(2)
snd_soc_platform_driver的注册

ASoC把snd_soc_platform_driver注册为一个系统的platform_driver,前者只是针对ASoC子系统的,后者是来自Linux的设备驱动模型。我们要做的就是:
1. 定义一个snd_soc_platform_driver结构的实例;
2. 在platform_driver的probe回调中利用ASoC的API:snd_soc_register_platform()注册上面定义的实例;
3. 实现snd_soc_platform_driver中的各个回调函数;

sound/soc/ingenic/asoc-dma-hrtimer.c 
static struct snd_soc_platform_driver jz_pcm_platform = { 
    .ops            = &jz_pcm_ops,
    .pcm_new        = jz_pcm_new,
    .pcm_free       = jz_pcm_free,
};

static int jz_pcm_platform_probe(struct platform_device *pdev)
{
  ret = snd_soc_register_platform(&pdev->dev, &jz_pcm_platform);

}

int snd_soc_register_platform(struct device *dev,                                                                                       
        const struct snd_soc_platform_driver *platform_drv)                                                                             
{                                                                                                                                       
    struct snd_soc_platform *platform;                                                                                                  
    int ret;                                                                                                                            
                                                                                                                                        
    dev_dbg(dev, "ASoC: platform register %s\n", dev_name(dev));                                                                        
                                                                                                                                        
    platform = kzalloc(sizeof(struct snd_soc_platform), GFP_KERNEL);                                                                    
    if (platform == NULL)                                                                                                               
        return -ENOMEM;                                                                                                                 
                                                                                                                                        
    ret = snd_soc_add_platform(dev, platform, platform_drv);                                                                            
    if (ret)                                                                                                                            
        kfree(platform);                                                                                                                
                                                                                                                                        
    return ret;                                                                                                                         
}                                                                                                                                       

int snd_soc_add_platform(struct device *dev, struct snd_soc_platform *platform,                                                         
        const struct snd_soc_platform_driver *platform_drv)                                                                             
{                                                                                                                                       
    /* create platform component name */
    platform->name = fmt_single_name(dev, &platform->id);                                                                               
    if (platform->name == NULL) {                                                                                                       
        kfree(platform);                                                                                                                
        return -ENOMEM;
    }                                                                                                                                   
    platform->dev = dev;
    platform->driver = platform_drv;                                                                                                    
    platform->dapm.dev = dev;
    platform->dapm.platform = platform;                                                                                                 
    platform->dapm.stream_event = platform_drv->stream_event;                                                                           
    mutex_init(&platform->mutex);
    mutex_lock(&client_mutex);
    list_add(&platform->list, &platform_list);                                                                                          
    mutex_unlock(&client_mutex);                                                                                                        
    
    dev_dbg(dev, "ASoC: Registered platform '%s'\n", platform->name);                                                                   
    
    return 0;
}       
EXPORT_SYMBOL_GPL(snd_soc_add_platform); 

snd_soc_register_platform() 该函数用于注册一个snd_soc_platform,只有注册以后,它才可以被Machine驱动使用。它的代码已经清晰地表达了它的实现过程:
1. snd_soc_platform实例申请内存;
2. 从platform_device中获得它的名字,用于Machine驱动的匹配工作;
3. 初始化snd_soc_platform的字段;
4. 把snd_soc_platform实例连接到全局链表platform_list中;
5. 调用snd_soc_instantiate_cards,触发声卡的machine、platform、codec、dai等的匹配工作;

(2)
cpu的snd_soc_dai driver驱动的注册

dai驱动通常对应cpu的一个或几个I2S/PCM接口,与snd_soc_platform一样,dai驱动也是实现为一个platform driver,实现一个dai驱动大致可以分为以下几个步骤:
1. 定义一个snd_soc_dai_driver结构的实例;
2. 在对应的platform_driver中的probe回调中通过API:snd_soc_register_dai或者snd_soc_register_dais,注册snd_soc_dai实例;
3. 实现snd_soc_dai_driver结构中的probe、suspend等回调;
4. 实现snd_soc_dai_driver结构中的snd_soc_dai_ops字段中的回调函数;

sound/soc/ingenic/icodec/icdc_d4.c
static struct snd_soc_dai_driver  icdc_d4_codec_dai = { 
    .name = "icdc-d4-hifi",
    .playback = { 
        .stream_name = "Playback",
        .channels_min = 2,
        .channels_max = 2,
        .rates = ICDC_D4_RATE,
        .formats = ICDC_D4_FORMATS,
    },  
    .capture = { 
        .stream_name = "Capture",
        .channels_min = 1,
        .channels_max = 2,
        .rates = ICDC_D4_RATE,
        .formats = ICDC_D4_FORMATS,
    },  
    .symmetric_rates = 1,
    .ops = &icdc_d4_dai_ops,
};

static struct snd_soc_dai_ops icdc_d4_dai_ops = {                                                                                       
    .hw_params  = icdc_d4_hw_params,                                                                                                    
    .digital_mute   = icdc_d4_digital_mute,                                                                                             
    .trigger = icdc_d4_trigger,                                                                                                         
}; 

static struct snd_soc_codec_driver soc_codec_dev_icdc_d4_codec = {
    .probe =    icdc_d4_probe,
    .remove =   icdc_d4_remove,
    .read =     icdc_d4_read,
    .write =    icdc_d4_write,
    .writable_register = icdc_d4_vaild_register,
    .reg_cache_default = icdc_d4_reg_defcache,
    .reg_word_size = sizeof(u8),
    .reg_cache_step = 1,
    .reg_cache_size = ICDC_REG_NUM,
    .set_bias_level = icdc_d4_set_bias_level,

    .controls =     icdc_d4_snd_controls,
    .num_controls = ARRAY_SIZE(icdc_d4_snd_controls),
    .dapm_widgets = icdc_d4_dapm_widgets,
    .num_dapm_widgets = ARRAY_SIZE(icdc_d4_dapm_widgets),
    .dapm_routes = intercon,
    .num_dapm_routes = ARRAY_SIZE(intercon),
};

static int icdc_d4_platform_probe(struct platform_device *pdev)
{
   ret = snd_soc_register_codec(&pdev->dev,&soc_codec_dev_icdc_d4_codec, &icdc_d4_codec_dai, 1);

}

sound/soc/ingenic/asoc-v12/asoc-i2s-v12.c
static struct snd_soc_dai_driver jz_i2s_dai = { 
        .probe   = jz_i2s_probe,
        .suspend = jz_i2s_suspend,
        .resume  = jz_i2s_resume,
        .playback = { 
            .channels_min = 1,
            .channels_max = 2,
            .rates = JZ_I2S_RATE,
            .formats = JZ_I2S_FORMATS,
        },
        .capture = { 
            .channels_min = 2,
            .channels_max = 2,
            .rates = JZ_I2S_RATE,
            .formats = JZ_I2S_FORMATS,
        },
        .ops = &jz_i2s_dai_ops,
};
//snd_soc_dai_driver  该结构需要自己根据不同的soc芯片进行定义,关键字段介绍如下:
1. probe、remove  回调函数,分别在声卡加载和卸载时被调用;
2. suspend、resume  电源管理回调函数;
3. ops  指向snd_soc_dai_ops结构,用于配置和控制该dai;
4. playback  snd_soc_pcm_stream结构,用于指出该dai支持的声道数,码率,数据格式等能力;
5. capture  snd_soc_pcm_stream结构,用于指出该dai支持的声道数,码率,数据格式等能力;

static struct snd_soc_dai_ops jz_i2s_dai_ops = { 
    .startup    = jz_i2s_startup,
    .trigger    = jz_i2s_trigger,
    .hw_params  = jz_i2s_hw_params,
    .shutdown   = jz_i2s_shutdown,
    .set_fmt    = jz_set_dai_fmt,
    .set_sysclk = jz_set_sysclk,
    .set_clkdiv = jz_set_clkdiv,
};

static int jz_i2s_platfrom_probe(struct platform_device *pdev)
{
  ret = snd_soc_register_component(&pdev->dev, &jz_i2s_component,&jz_i2s_dai, 1);

}

int snd_soc_register_component(struct device *dev,const struct snd_soc_component_driver *cmpnt_drv,struct snd_soc_dai_driver *dai_drv,int num_dai)
{
     if (1 == num_dai)
        ret = snd_soc_register_dai(dev, dai_drv);
    else
        ret = snd_soc_register_dais(dev, dai_drv, num_dai);
    if (ret < 0) {
        dev_err(dev, "ASoC: Failed to regster DAIs: %d\n", ret);
        goto error_component_name;
    }
    mutex_lock(&client_mutex);
    list_add(&cmpnt->list, &component_list);
    mutex_unlock(&client_mutex);

}

static int snd_soc_register_dai(struct device *dev,struct snd_soc_dai_driver *dai_drv)
{
   struct snd_soc_codec *codec;
    struct snd_soc_dai *dai;
    dev_dbg(dev, "ASoC: dai register %s\n", dev_name(dev));
    dai = kzalloc(sizeof(struct snd_soc_dai), GFP_KERNEL);
    if (dai == NULL)
        return -ENOMEM;
    /* create DAI component name */
    dai->name = fmt_single_name(dev, &dai->id);
    if (dai->name == NULL) {
        kfree(dai);
        return -ENOMEM;
    }    
    dai->dev = dev; 
    dai->driver = dai_drv;
    dai->dapm.dev = dev; 
    if (!dai->driver->ops)
        dai->driver->ops = &null_dai_ops;
    mutex_lock(&client_mutex);
    list_for_each_entry(codec, &codec_list, list) {
        if (codec->dev == dev) {
            dev_dbg(dev, "ASoC: Mapped DAI %s to CODEC %s\n",
                dai->name, codec->name);
            dai->codec = codec;
            break;
        }
    }
    if (!dai->codec)
        dai->dapm.idle_bias_off = 1;
    list_add(&dai->list, &dai_list);
    mutex_unlock(&client_mutex);
    dev_dbg(dev, "ASoC: Registered DAI '%s'\n", dai->name);
    return 0;
  
}
//snd_soc_dai  该结构在snd_soc_register_dai函数中通过动态内存申请获得, 简要介绍一下几个重要字段:
driver 指向关联的snd_soc_dai_driver结构,由注册时通过参数传入;
playback_dma_data 用于保存该dai播放stream的dma信息,例如dma的目标地址,dma传送单元大小和通道号等;
capture_dma_data 同上,用于录音stream;
platform 指向关联的snd_soc_platform结构;


(3)
snd_soc_dai_driver中的ops字段

sound/soc/ingenic/asoc-v12/asoc-i2s-v12.c
static struct snd_soc_dai_driver jz_i2s_dai = { 
   .ops = &jz_i2s_dai_ops,
}

static struct snd_soc_dai_ops jz_i2s_dai_ops = { 
    .startup    = jz_i2s_startup,
    .trigger    = jz_i2s_trigger,
    .hw_params  = jz_i2s_hw_params,
    .shutdown   = jz_i2s_shutdown,
    .set_fmt    = jz_set_dai_fmt,
    .set_sysclk = jz_set_sysclk,
    .set_clkdiv = jz_set_clkdiv,
};
ops字段指向一个snd_soc_dai_ops结构,该结构实际上是一组回调函数的集合,dai的配置和控制几乎都是通过这些回调函数来实现的,这些回调函数基本可以分为3大类,驱动程序可以根据实际情况实现其中的一部分:
<1>工作时钟配置函数  通常由machine驱动调用:
1. set_sysclk  设置dai的主时钟;
2. set_pll  设置PLL参数;
3. set_clkdiv  设置分频系数;
4. dai的格式配置函数  通常由machine驱动调用:
5. set_fmt   设置dai的格式;
6. set_tdm_slot  如果dai支持时分复用,用于设置时分复用的slot;
7. set_channel_map 声道的时分复用映射设置;
8. set_tristate  设置dai引脚的状态,当与其他dai并联使用同一引脚时需要使用该回调;

sound/soc/ingenic/asoc-board/pd_x1830_audio_icdc.c 
int pd_x1830_audio_i2s_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 *cpu_dai = rtd->cpu_dai;
        int ret, rate = params_rate(params);

    ret = snd_soc_dai_set_sysclk(cpu_dai, JZ_I2S_INNER_CODEC, rate * 256, SND_SOC_CLOCK_OUT);
    if (ret)
        return ret;
    return 0;
};

static struct snd_soc_ops pd_x1830_audio_i2s_ops = { 
    .hw_params = pd_x1830_audio_i2s_hw_params,
};

sound/soc/soc-core.c
int snd_soc_dai_set_sysclk(struct snd_soc_dai *dai, int clk_id,
    unsigned int freq, int dir) 
{
    if (dai->driver && dai->driver->ops->set_sysclk)
        return dai->driver->ops->set_sysclk(dai, clk_id, freq, dir);
    else if (dai->codec && dai->codec->driver->set_sysclk)
        return dai->codec->driver->set_sysclk(dai->codec, clk_id, 0,
                              freq, dir);
    else 
        return -EINVAL;
}
EXPORT_SYMBOL_GPL(snd_soc_dai_set_sysclk);

sound/soc/ingenic/asoc-v12/asoc-i2s-v12.c
static int jz_set_sysclk(struct snd_soc_dai *dai, int clk_id,                                                                           
        unsigned int freq, int dir)                                                                                                     
{                                                                                                                                       
    struct jz_i2s *jz_i2s = dev_get_drvdata(dai->dev);                                                                                  
    struct device *aic = jz_i2s->aic;                                                                                                   
                                                                                                                                        
    I2S_DEBUG_MSG("enter %s clk_id %d req %d clk dir %d\n", __func__,                                                                   
            clk_id, freq, dir);                                                                                                         
                                                                                                                                        
    if (clk_id == JZ_I2S_INNER_CODEC) {                                                                                                 
#if defined(CONFIG_SOC_X1830) || defined(CONFIG_SOC_X1630)                                                                              
                __i2s_codec_master(aic);                                                                                                
#endif                                                                                                                                  
        __aic_select_internal_codec(aic);                                                                                               
        __i2s_bclk_input(aic);                                                                                                          
        __i2s_sync_input(aic);                                                                                                          
    } else {                                                                                                                            
#if defined(CONFIG_SOC_X1830) || defined(CONFIG_SOC_X1630)                                                                              
                __i2s_codec_slave(aic); /*Disable inner-codec master mode*/                                                             
#endif                                                                                                                                  
        __aic_select_external_codec(aic);                                                                                               
    }                                                                                                                                   
                                                                                                                                        
    aic_set_rate(aic, freq);                                                                                                            
                                                                                                                                        
    if (dir  == SND_SOC_CLOCK_OUT) {                                                                                                    
#if defined(CONFIG_SOC_X1830) || defined(CONFIG_SOC_X1630)                                                                              
        __i2s_select_sysclk_output(aic);                                                                                                
#endif                                                                                                                                  
                __i2s_enable_sysclk_output(aic);                                                                                        
        } else {                                                                                                                        
        __i2s_select_sysclk_input(aic);                                                                                                 
        __i2s_disable_sysclk_output(aic);                                                                                               
    }                                                                                                                                   
    return 0;                                                                                                                           
                                                                                                                                        

<2>标准的snd_soc_ops回调  通常由soc-core在进行PCM操作时调用:
startup
shutdown
hw_params
hw_free
prepare
trigger

<3>抗pop,pop声  由soc-core调用:
digital_mute 

以下这些api通常被machine驱动使用,machine驱动在他的snd_pcm_ops字段中的hw_params回调中使用这些api:
snd_soc_dai_set_fmt()  实际上会调用snd_soc_dai_ops或者codec driver中的set_fmt回调;
sound/soc/soc-core.c
int snd_soc_dai_set_fmt(struct snd_soc_dai *dai, unsigned int fmt) 
{
    if (dai->driver == NULL)
        return -EINVAL;
    if (dai->driver->ops->set_fmt == NULL)
        return -ENOTSUPP;
    return dai->driver->ops->set_fmt(dai, fmt);
}
EXPORT_SYMBOL_GPL(snd_soc_dai_set_fmt);

snd_soc_dai_set_pll() 实际上会调用snd_soc_dai_ops或者codec driver中的set_pll回调;
sound/soc/soc-core.c
int snd_soc_dai_set_pll(struct snd_soc_dai *dai, int pll_id, int source,
    unsigned int freq_in, unsigned int freq_out)
{
    if (dai->driver && dai->driver->ops->set_pll)
        return dai->driver->ops->set_pll(dai, pll_id, source,
                     freq_in, freq_out);
    else if (dai->codec && dai->codec->driver->set_pll)
        return dai->codec->driver->set_pll(dai->codec, pll_id, source,
                           freq_in, freq_out);
    else
        return -EINVAL;
}
EXPORT_SYMBOL_GPL(snd_soc_dai_set_pll);

snd_soc_dai_set_sysclk()  实际上会调用snd_soc_dai_ops或者codec driver中的set_sysclk回调;
sound/soc/soc-core.c
int snd_soc_dai_set_sysclk(struct snd_soc_dai *dai, int clk_id,
    unsigned int freq, int dir) 
{
    if (dai->driver && dai->driver->ops->set_sysclk)
        return dai->driver->ops->set_sysclk(dai, clk_id, freq, dir);
    else if (dai->codec && dai->codec->driver->set_sysclk)
        return dai->codec->driver->set_sysclk(dai->codec, clk_id, 0,
                              freq, dir);
    else 
        return -EINVAL;
}
EXPORT_SYMBOL_GPL(snd_soc_dai_set_sysclk);

snd_soc_dai_set_clkdiv()  实际上会调用snd_soc_dai_ops或者codec driver中的set_clkdiv回调;
sound/soc/soc-core.c
int snd_soc_dai_set_clkdiv(struct snd_soc_dai *dai,
    int div_id, int div)
{
    if (dai->driver && dai->driver->ops->set_clkdiv)
        return dai->driver->ops->set_clkdiv(dai, div_id, div);
    else
        return -EINVAL;
}
EXPORT_SYMBOL_GPL(snd_soc_dai_set_clkdiv);

sound/soc/ingenic/asoc-v12/asoc-i2s-v12.c
static struct snd_soc_dai_ops jz_i2s_dai_ops = { 
    .startup    = jz_i2s_startup,
    .trigger    = jz_i2s_trigger,
    .hw_params  = jz_i2s_hw_params,
    .shutdown   = jz_i2s_shutdown,
    .set_fmt    = jz_set_dai_fmt,
    .set_sysclk = jz_set_sysclk,
    .set_clkdiv = jz_set_clkdiv,
};

snd_soc_dai_set_fmt(struct snd_soc_dai *dai, unsigned int fmt)的第二个参数fmt在这里特别说一下,ASoC目前只是用了它的低16位,并且为它专门定义了一些宏来方便我们使用:
bit 0-3 用于设置接口的格式:

#define SND_SOC_DAIFMT_I2S      1 /* I2S mode */  
#define SND_SOC_DAIFMT_RIGHT_J      2 /* Right Justified mode */  
#define SND_SOC_DAIFMT_LEFT_J       3 /* Left Justified mode */  
#define SND_SOC_DAIFMT_DSP_A        4 /* L data MSB after FRM LRC */  
#define SND_SOC_DAIFMT_DSP_B        5 /* L data MSB during FRM LRC */  
#define SND_SOC_DAIFMT_AC97     6 /* AC97 */  
#define SND_SOC_DAIFMT_PDM      7 /* Pulse density modulation */  

bit 4-7 用于设置接口时钟的开关特性:
#define SND_SOC_DAIFMT_CONT     (1 << 4) /* continuous clock */  
#define SND_SOC_DAIFMT_GATED        (2 << 4) /* clock is gated */ 
bit 8-11 用于设置接口时钟的相位:
#define SND_SOC_DAIFMT_NB_NF        (1 << 8) /* normal bit clock + frame */  
#define SND_SOC_DAIFMT_NB_IF        (2 << 8) /* normal BCLK + inv FRM */  
#define SND_SOC_DAIFMT_IB_NF        (3 << 8) /* invert BCLK + nor FRM */  
#define SND_SOC_DAIFMT_IB_IF        (4 << 8) /* invert BCLK + FRM */  

bit 12-15 用于设置接口主从格式
#define SND_SOC_DAIFMT_CBM_CFM      (1 << 12) /* codec clk & FRM master */  
#define SND_SOC_DAIFMT_CBS_CFM      (2 << 12) /* codec clk slave & FRM master */  
#define SND_SOC_DAIFMT_CBM_CFS      (3 << 12) /* codec clk master & frame slave */  
#define SND_SOC_DAIFMT_CBS_CFS      (4 << 12) /* codec clk & FRM slave */

(4)
snd_soc_platform_driver中的ops字段

sound/soc/ingenic/asoc-dma-hrtimer.c
struct snd_pcm_ops jz_pcm_ops = { 
    .open       = jz_pcm_open,
    .close      = jz_pcm_close,
    .prepare    = jz_pcm_prepare,
    .ioctl      = snd_pcm_lib_ioctl,
    .hw_params  = jz_pcm_hw_params,
    .hw_free    = snd_pcm_lib_free_pages,
    .trigger    = jz_pcm_trigger,
    .pointer    = jz_pcm_pointer,
};

static struct snd_soc_platform_driver jz_pcm_platform = {
    .ops            = &jz_pcm_ops,
    .pcm_new        = jz_pcm_new,
    .pcm_free       = jz_pcm_free,
};

static int jz_pcm_platform_probe(struct platform_device *pdev)
{

ret = snd_soc_register_platform(&pdev->dev, &jz_pcm_platform);
}


该ops字段是一个snd_pcm_ops结构,实现该结构中的各个回调函数是soc platform驱动的主要工作,他们基本都涉及dma操作以及dma buffer的管理等工作。下面介绍几个重要的回调函数:

ops.open 

当应用程序打开一个pcm设备时,该函数会被调用,通常,该函数会使用snd_soc_set_runtime_hwparams()设置substream中的snd_pcm_runtime结构里面的hw_params相关字段,然后为snd_pcm_runtime的private_data字段申请一个私有结构,用于保存该平台的dma参数。

static int jz_pcm_open(struct snd_pcm_substream *substream)                                                                             
{                                                                                                                                       
    struct snd_soc_pcm_runtime *rtd = snd_pcm_substream_chip(substream);                                                                
    struct jz_dma_pcm *jz_pcm = snd_soc_platform_get_drvdata(rtd->platform);                                                            
    struct dma_chan *chan = jz_pcm->chan[substream->stream];                                                                            
    struct jz_pcm_runtime_data *prtd = NULL;                                                                                            
    int ret;                                                                                                                            
                                                                                                                                        
    DMA_DEBUG_MSG("%s enter\n", __func__);                                                                                              
    ret = snd_soc_set_runtime_hwparams(substream, &jz_pcm_hardware); //把pcm的硬件信息设置进去                                                                    
    if (ret)                                                                                                                            
        return ret;                                                                                                                     
                                                                                                                                        
    ret = snd_pcm_hw_constraint_integer(substream->runtime, SNDRV_PCM_HW_PARAM_PERIODS);                                                
    if (ret < 0)                                                                                                                        
        return ret;                                                                                                                     
                                                                                                                                        
    prtd = kzalloc(sizeof(*prtd), GFP_KERNEL);                                                                                          
    if (!prtd)                                                                                                                          
        return -ENOMEM;                                                                                                                 
                                                                                                                                        
    hrtimer_init(&prtd->hr_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);                                                                   
    prtd->hr_timer.function = jz_asoc_hrtimer_callback;                                                                                 
    prtd->dma_chan = chan;                                                                                                              
    prtd->substream = substream;                                                                                                        
    substream->runtime->private_data = prtd;                                                                                            
    return 0;                                                                                                                           

static const struct snd_pcm_hardware jz_pcm_hardware = {
    .info = SNDRV_PCM_INFO_MMAP |
        SNDRV_PCM_INFO_PAUSE |
        SNDRV_PCM_INFO_RESUME |
        SNDRV_PCM_INFO_MMAP_VALID |
        SNDRV_PCM_INFO_INTERLEAVED |
        SNDRV_PCM_INFO_BLOCK_TRANSFER,
    .formats = SNDRV_PCM_FMTBIT_S24_LE |
        SNDRV_PCM_FMTBIT_S20_3LE |
        SNDRV_PCM_FMTBIT_S18_3LE |
        SNDRV_PCM_FMTBIT_S16_LE |
        SNDRV_PCM_FMTBIT_S8,
    .rates                  = SNDRV_PCM_RATE_8000_192000,                                                                               
    .rate_min               = 8000,                                                                                                     
    .rate_max               = 192000,                                                                                                   
    .channels_min           = 1,                                                                                                        
    .channels_max           = 2,                                                                                                        
    .buffer_bytes_max       = JZ_DMA_BUFFERSIZE,                                                                                        
    .period_bytes_min       = PERIOD_BYTES_MIN,                                                                                         
    .period_bytes_max       = JZ_DMA_BUFFERSIZE / PERIODS_MIN,                                                                          
    .periods_min            = PERIODS_MIN,                                                                                              
    .periods_max            = PERIODS_MAX,                                                                                              
    .fifo_size              = 0,                                                                                                        
};


ops.hw_params 

驱动的hw_params阶段,该函数会被调用。通常,该函数会通过snd_soc_dai_get_dma_data函数获得对应的dai的dma参数,获得的参数一般都会保存在snd_pcm_runtime结构的private_data字段。然后通过snd_pcm_set_runtime_buffer函数设置snd_pcm_runtime结构中的dma buffer的地址和大小等参数。要注意的是,该回调可能会被多次调用,具体实现时要小心处理多次申请资源的问题。

static int jz_pcm_hw_params(struct snd_pcm_substream *substream,
        struct snd_pcm_hw_params *params)
{
    struct snd_soc_pcm_runtime *rtd = snd_pcm_substream_chip(substream);
    struct jz_dma_pcm *jz_pcm = snd_soc_platform_get_drvdata(rtd->platform);
    struct jz_pcm_runtime_data *prtd = substream->runtime->private_data;
    struct jz_pcm_dma_params *dma_params = snd_soc_dai_get_dma_data(rtd->cpu_dai, substream);
    struct dma_slave_config slave_config;
    unsigned long long time_ns;
    int ret;
    DMA_SUBSTREAM_MSG(substream, "%s enter\n", __func__);

    if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
        slave_config.direction = DMA_TO_DEVICE;
        slave_config.dst_addr = dma_params->dma_addr;
    } else {
        slave_config.direction = DMA_FROM_DEVICE;
        slave_config.src_addr = dma_params->dma_addr;
    }
    slave_config.dst_addr_width = dma_params->buswidth;
    slave_config.dst_maxburst = dma_params->max_burst;
    slave_config.src_addr_width = dma_params->buswidth; /*jz dmaengine needed*/
    slave_config.src_maxburst = dma_params->max_burst;
    ret = dmaengine_slave_config(prtd->dma_chan, &slave_config);
    if (ret)
        return ret;

    time_ns = 1000LL * 1000 * 1000 * params_period_size(params);
    do_div(time_ns, params_rate(params));
    prtd->expires = ns_to_ktime(time_ns);

    ret = snd_pcm_lib_malloc_pages(substream, params_buffer_bytes(params));
    if (ret < 0)
        return ret;

    if (substream->stream == SNDRV_PCM_STREAM_CAPTURE)
    DMA_SUBSTREAM_MSG(substream, "%s leave \n\
            buf(%p, %dbytes) period(%d frames, %lldns)\n\
            rate %d channel %d, format %d\n", __func__,
            substream->runtime->dma_area,
            params_buffer_bytes(params),
            params_period_size(params),
            time_ns,
            params_rate(params),
 prtd->expires = ns_to_ktime(time_ns);

    ret = snd_pcm_lib_malloc_pages(substream, params_buffer_bytes(params));
    if (ret < 0)
        return ret;

    if (substream->stream == SNDRV_PCM_STREAM_CAPTURE)
    DMA_SUBSTREAM_MSG(substream, "%s leave \n\
            buf(%p, %dbytes) period(%d frames, %lldns)\n\
            rate %d channel %d, format %d\n", __func__,
            substream->runtime->dma_area,
            params_buffer_bytes(params),
            params_period_size(params),
            time_ns,
            params_rate(params),
            params_channels(params),
            snd_pcm_format_physical_width(params_format(params))/8);
    return 0;
}

ops.prepare

正式开始数据传送之前会调用该函数,该函数通常会完成dma操作的必要准备工作。

ops.trigger

数据传送的开始,暂停,恢复和停止时,该函数会被调用。

ops.pointer

该函数返回传送数据的当前位置

(5)
音频数据的dma操作

soc-platform驱动的最主要功能就是要完成音频数据的传送,大多数情况下,音频数据都是通过dma来完成的。

申请dma buffer

因为dma的特殊性,dma buffer是一块特殊的内存,比如有的平台规定只有某段地址范围的内存才可以进行dma操作,而多数嵌入式平台还要求dma内存的物理地址是连续的,以方便dma控制器对内存的访问。在ASoC架构中,dma buffer的信息保存在snd_pcm_substream结构的snd_dma_buffer *buf字段中

struct snd_pcm_substream 
{
  struct snd_dma_buffer dma_buffer;
}

/*  
 * info for buffer allocation
 */ 
struct snd_dma_buffer {
    struct snd_dma_device dev;  /* device type */
    unsigned char *area;    /* virtual pointer */
    dma_addr_t addr;    /* physical address */
    size_t bytes;       /* buffer size in bytes */
    void *private_data; /* private for allocator; don't touch */
};

sound/soc/ingenic/asoc-dma-hrtimer.c
static struct snd_soc_platform_driver jz_pcm_platform = { 
    .ops            = &jz_pcm_ops,
    .pcm_new        = jz_pcm_new,
    .pcm_free       = jz_pcm_free,
};


static int jz_pcm_new(struct snd_soc_pcm_runtime *rtd)
{
    struct snd_pcm *pcm = rtd->pcm;
    struct jz_dma_pcm *jz_pcm = snd_soc_platform_get_drvdata(rtd->platform);
    struct snd_pcm_substream *substream;
    dma_cap_mask_t mask;
    size_t buffer_size = JZ_DMA_BUFFERSIZE;
    size_t buffer_bytes_max = JZ_DMA_BUFFERSIZE;
    int ret = -EINVAL;
    int i;

    DMA_DEBUG_MSG("%s enter\n", __func__);

    for (i = 0; i < 2; i++) {
        substream = pcm->streams[i].substream;
        if (!substream)
            continue;
        if (!jz_pcm->chan[i]) {
            dma_cap_zero(mask);
            dma_cap_set(DMA_SLAVE, mask);
            dma_cap_set(DMA_CYCLIC, mask);
            jz_pcm->chan[i] = dma_request_channel(mask, filter, jz_pcm);

            if (!jz_pcm->chan[i])
                goto out;

            ret = snd_pcm_lib_preallocate_pages(substream,
                    SNDRV_DMA_TYPE_DEV,
                    jz_pcm->chan[i]->device->dev,
                    buffer_size,
                    buffer_bytes_max);
            if (ret)
                goto out;
        }
    }
    return 0;
out:
    dev_err(rtd->dev, "Failed to alloc dma buffer %d\n", ret);
    jz_pcm_free(pcm);
    return ret;
}

pcm_new字段指向了jz_pcm_new函数,dma_new函数进一步为playback和capture分别调用snd_pcm_lib_preallocate_pages函数

在声卡的hw_params阶段,snd_soc_platform_driver结构的ops->hw_params会被调用,在该回调用,通常会使用api:snd_soc_dai_get_dma_data()把substream->dma_buffer的数值拷贝到substream->runtime的相关字段中(.dma_area, .dma_addr,  .dma_bytes),这样以后就可以通过substream->runtime获得这些地址和大小信息了。

struct snd_pcm_ops jz_pcm_ops = { 
    .open       = jz_pcm_open,
    .close      = jz_pcm_close,
    .prepare    = jz_pcm_prepare,
    .ioctl      = snd_pcm_lib_ioctl,
    .hw_params  = jz_pcm_hw_params,
    .hw_free    = snd_pcm_lib_free_pages,
    .trigger    = jz_pcm_trigger,
    .pointer    = jz_pcm_pointer,
};
static int jz_pcm_hw_params(struct snd_pcm_substream *substream,struct snd_pcm_hw_params *params)
{
  
}

dma buffer获得后,即是获得了dma操作的源地址,那么目的地址在哪里?其实目的地址当然是在dai中,也就是前面介绍的snd_soc_dai结构的playback_dma_data和capture_dma_data字段中,而这两个字段的值也是在hw_params阶段,由snd_soc_dai_driver结构的ops->hw_params回调,利用api:snd_soc_dai_set_dma_data进行设置的。紧随其后,snd_soc_platform_driver结构的ops->hw_params回调利用api:snd_soc_dai_get_dma_data获得这些dai的dma信息,其中就包括了dma的目的地址信息。这些dma信息通常还会被保存在substream->runtime->private_data中,以便在substream的整个生命周期中可以随时获得这些信息,从而完成对dma的配置和操作。

static struct snd_soc_dai_ops jz_i2s_dai_ops = {
    .startup    = jz_i2s_startup,
    .trigger    = jz_i2s_trigger,
    .hw_params  = jz_i2s_hw_params,
    .shutdown   = jz_i2s_shutdown,
    .set_fmt    = jz_set_dai_fmt,
    .set_sysclk = jz_set_sysclk,
    .set_clkdiv = jz_set_clkdiv,
};

static int jz_i2s_hw_params(struct snd_pcm_substream *substream,struct snd_pcm_hw_params *params, struct snd_soc_dai *dai)
{
   if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
        /* channel */
        __i2s_channel(aic, channels);

        /* format */
        if (fmt_width == 8)
            buswidth = DMA_SLAVE_BUSWIDTH_1_BYTE;
        else if (fmt_width == 16) 
            buswidth = DMA_SLAVE_BUSWIDTH_2_BYTES;
        else
            buswidth = DMA_SLAVE_BUSWIDTH_4_BYTES;

        jz_i2s->tx_dma_data.buswidth = buswidth;
        jz_i2s->tx_dma_data.max_burst = (I2S_TFIFO_DEPTH * buswidth)/2;
        trigger = I2S_TFIFO_DEPTH - (jz_i2s->tx_dma_data.max_burst/(int)buswidth);
        __i2s_set_oss(aic, fmt_width);
        __i2s_set_transmit_trigger(aic, (trigger/2));
        snd_soc_dai_set_dma_data(dai, substream, (void *)&jz_i2s->tx_dma_data);
    } else {
        /* format */
        if (fmt_width == 8)
            buswidth = DMA_SLAVE_BUSWIDTH_1_BYTE;
        else if (fmt_width == 16)
            buswidth = DMA_SLAVE_BUSWIDTH_2_BYTES;
        else
            buswidth = DMA_SLAVE_BUSWIDTH_4_BYTES;

        jz_i2s->rx_dma_data.buswidth = buswidth;
        jz_i2s->rx_dma_data.max_burst = (I2S_RFIFO_DEPTH * buswidth)/2/2 /*To reduce overrun happen*/;
        trigger = jz_i2s->rx_dma_data.max_burst/(int)buswidth;
        __i2s_set_iss(aic, fmt_width);
        __i2s_set_receive_trigger(aic, (trigger/2 - 1));
        snd_soc_dai_set_dma_data(dai, substream, (void *)&jz_i2s->rx_dma_data);
    }
}

static inline void snd_soc_dai_set_dma_data(struct snd_soc_dai *dai,
                        const struct snd_pcm_substream *ss,
                        void *data)
{
    if (ss->stream == SNDRV_PCM_STREAM_PLAYBACK)
        dai->playback_dma_data = data;
    else
        dai->capture_dma_data = data;
}

(6)
dma buffer管理

播放时,应用程序把音频数据源源不断地写入dma buffer中,然后相应platform的dma操作则不停地从该buffer中取出数据,经dai送往codec中。录音时则正好相反,codec源源不断地把A/D转换好的音频数据经过dai送入dma buffer中,而应用程序则不断地从该buffer中读走音频数据。

Linux ALSA驱动框架(六)--ASoC架构中的Platfrom_第1张图片


环形缓冲区正好适合用于这种情景的buffer管理,理想情况下,大小为Count的缓冲区具备一个读指针和写指针,我们期望他们都可以闭合地做环形移动,但是实际的情况确实:缓冲区通常都是一段连续的地址,他是有开始和结束两个边界,每次移动之前都必须进行一次判断,当指针移动到末尾时就必须人为地让他回到起始位置。在实际应用中,我们通常都会把这个大小为Count的缓冲区虚拟成一个大小为n*Count的逻辑缓冲区,相当于理想状态下的圆形绕了n圈之后,然后把这段总的距离拉平为一段直线,每一圈对应直线中的一段,因为n比较大,所以大多数情况下不会出现读写指针的换位的情况(如果不对buffer进行扩展,指针到达末端后,回到起始端时,两个指针的前后相对位置会发生互换)。扩展后的逻辑缓冲区在计算剩余空间可条件判断是相对方便。alsa driver也使用了该方法对dma buffer进行管理:

Linux ALSA驱动框架(六)--ASoC架构中的Platfrom_第2张图片

snd_pcm_runtime结构中,使用了四个相关的字段来完成这个逻辑缓冲区的管理:
1. snd_pcm_runtime.hw_ptr_base  环形缓冲区每一圈的基地址,当读写指针越过一圈后,它按buffer size进行移动;
2. snd_pcm_runtime.status->hw_ptr  硬件逻辑位置,播放时相当于读指针,录音时相当于写指针;
3. snd_pcm_runtime.control->appl_ptr  应用逻辑位置,播放时相当于写指针,录音时相当于读指针;
4. snd_pcm_runtime.boundary  扩展后的逻辑缓冲区大小,通常是(2^n)*size;


通过这几个字段,我们可以很容易地获得缓冲区的有效数据,剩余空间等信息,也可以很容易地把当前逻辑位置映射回真实的dma buffer中。例如,获得播放缓冲区的空闲空间:
static inline snd_pcm_uframes_t snd_pcm_playback_avail(struct snd_pcm_runtime *runtime)                         
{                                                                                                               
    snd_pcm_sframes_t avail = runtime->status->hw_ptr + runtime->buffer_size - runtime->control->appl_ptr;      
    if (avail < 0)                                                                                              
        avail += runtime->boundary;                                                                             
    else if ((snd_pcm_uframes_t) avail >= runtime->boundary)                                                    
        avail -= runtime->boundary;                                                                             
    return avail;                                                                                               
}

static inline snd_pcm_uframes_t snd_pcm_capture_avail(struct snd_pcm_runtime *runtime)                          
{                                                                                                               
    snd_pcm_sframes_t avail = runtime->status->hw_ptr - runtime->control->appl_ptr;                             
    if (avail < 0)                                                                                              
        avail += runtime->boundary;                                                                             
    return avail;
}


要想映射到真正的缓冲区位置,只要减去runtime->hw_ptr_base即可。下面的api用于更新这几个指针的当前位置:
int snd_pcm_update_hw_ptr(struct snd_pcm_substream *substream)      
{                                                             
    return snd_pcm_update_hw_ptr0(substream, 0);     
}

所以要想通过snd_pcm_playback_avail等函数获得正确的信息前,应该先要调用这个api更新指针位置。以播放(playback)为例,我现在知道至少有3个途径可以完成对dma buffer的写入:
1. 应用程序调用alsa-lib的snd_pcm_writei、snd_pcm_writen函数;
2. 应用程序使用ioctl:SNDRV_PCM_IOCTL_WRITEI_FRAMES或SNDRV_PCM_IOCTL_WRITEN_FRAMES;
3. 应用程序使用alsa-lib的snd_pcm_mmap_begin/snd_pcm_mmap_commit;

以上几种方式最终把数据写入dma buffer中,然后修改runtime->control->appl_ptr的值。播放过程中,通常会配置成每一个period size生成一个dma中断,中断处理函数最重要的任务就是:
1. 更新dma的硬件的当前位置,该数值通常保存在runtime->private_data中;
2. 调用snd_pcm_period_elapsed函数,该函数会进一步调用snd_pcm_update_hw_ptr0函数更新上述所说的4个缓冲区管理字段,然后唤醒相应的等待进程;

void snd_pcm_period_elapsed(struct snd_pcm_substream *substream)
{   
    struct snd_pcm_runtime *runtime;
    unsigned long flags;

    if (PCM_RUNTIME_CHECK(substream))
        return;
    runtime = substream->runtime;

    if (runtime->transfer_ack_begin)
        runtime->transfer_ack_begin(substream);

    snd_pcm_stream_lock_irqsave(substream, flags);
    if (!snd_pcm_running(substream) ||
        snd_pcm_update_hw_ptr0(substream, 1) < 0)
        goto _end;

    if (substream->timer_running)
        snd_timer_interrupt(substream->timer, 1);
 _end:
    snd_pcm_stream_unlock_irqrestore(substream, flags);
    if (runtime->transfer_ack_end)
        runtime->transfer_ack_end(substream);
    kill_fasync(&runtime->fasync, SIGIO, POLL_IN);
//如果设置了transfer_ack_begin和transfer_ack_end回调,snd_pcm_period_elapsed还会调用这两个回调函数。
}
EXPORT_SYMBOL(snd_pcm_period_elapsed);


ASoC中Platform驱动的几个重要数据结构之间的关系:

cpu_dai:

sound/soc/ingenic/asoc-v13/asoc-i2s-v13.c

struct snd_soc_dai_ops   /   snd_soc_dai_driver   / jz_i2s_dai_ops

dma_platform:

sound/soc/ingenic/asoc-v13/asoc-dma-v13.c

struct snd_soc_platform_driver    /     struct snd_soc_platform   /  struct snd_pcm_ops

应用层把数据给 dma_paltform -----(dma buffer)--->cpu_dai---->code


Linux ALSA驱动框架(六)--ASoC架构中的Platfrom_第3张图片


一堆的private_data:

Linux ALSA驱动框架(六)--ASoC架构中的Platfrom_第4张图片




你可能感兴趣的:(alsa)