ALSA之PCM分析

之前写过一个音频驱动CODEC分析,当时忽略了阐述最基本的概念。要了解一个东西,首先要明白它是什么它起到什么作用,然后才会更好对它的工作流程更好的分析。所以这里提一下:
CODEC :音频芯片的控制,比如静音、打开(关闭)ADC(DAC)、设置ADC(DAC)的增益、耳机模式的检测等操作。
I2S   :数字音频接口,用于CPU和Codec之间的数字音频流raw data的传输。每当有playback或record操作时,snd_soc_dai_ops.prepare()会被调用,启动I2S总线。
PCM   :我不知道为什么会取这个模块名,它其实是定义DMA操作的,用于将音频数据通过DMA传到I2S控制器的FIFO中。


音频数据流向:
     | DMA |                     | I2S/PCM/AC97 |
RAM --------> I2SControllerFIFO -----------------> CODEC ----> SPK/Headset


PCM模块初始化

[cpp] view plain copy
  1. struct snd_soc_platform s3c_soc_platform = {  
  2.        .name         = "s3c-pcm-audio",  
  3.        .pcm_ops      = &s3c_pcm_ops,  
  4.        .pcm_new      = s3c_pcm_new,  
  5.        .pcm_free     = s3c_pcm_free_dma_buffers,  
  6.        .suspend      = s3c_pcm_suspend,  
  7.        .resume       = s3c_pcm_resume,  
  8. };  
调用snd_soc_register_platform()向ALSA core注册一个snd_soc_platform结构体。

成员pcm_new需要调用dma_alloc_writecombine()给DMA分配一块write-combining的内存空间,并把这块缓冲区的相关信息保存到substream->dma_buffer中,相当于构造函数。pcm_free则相反。这些成员函数都还算简单,看看代码即可以理解其流程。


snd_pcm_ops

接着我们看一下snd_pcm_ops结构体,该结构体的操作函数集的实现是本模块的主体。

[cpp] view plain copy
  1. struct snd_pcm_ops {  
  2.        int (*open)(struct snd_pcm_substream *substream);  
  3.        int (*close)(struct snd_pcm_substream *substream);  
  4.        int (*ioctl)(struct snd_pcm_substream * substream,  
  5.                    unsigned int cmd, void *arg);  
  6.        int (*hw_params)(struct snd_pcm_substream *substream,  
  7.                       struct snd_pcm_hw_params *params);  
  8.        int (*hw_free)(struct snd_pcm_substream *substream);  
  9.        int (*prepare)(struct snd_pcm_substream *substream);  
  10.        int (*trigger)(struct snd_pcm_substream *substream, int cmd);  
  11.        snd_pcm_uframes_t (*pointer)(struct snd_pcm_substream *substream);  
  12.        int (*copy)(struct snd_pcm_substream *substream, int channel,  
  13.                   snd_pcm_uframes_t pos,  
  14.                   void __user *buf, snd_pcm_uframes_t count);  
  15.        int (*silence)(struct snd_pcm_substream *substream, int channel,   
  16.                      snd_pcm_uframes_t pos, snd_pcm_uframes_t count);  
  17.        struct page *(*page)(struct snd_pcm_substream *substream,  
  18.                           unsigned long offset);  
  19.        int (*mmap)(struct snd_pcm_substream *substream, struct vm_area_struct *vma);  
  20.        int (*ack)(struct snd_pcm_substream *substream);  
  21. };  
我们主要实现open、close、hw_params、hw_free、prepare和trigger接口。

open

open函数为PCM模块设定支持的传输模式、数据格式、通道数、period等参数,并为playback/capture stream分配相应的DMA通道。其一般实现如下:

[cpp] view plain copy
  1. static int s3c_pcm_open(struct snd_pcm_substream *substream)  
  2. {  
  3.        struct snd_soc_pcm_runtime *rtd = substream->private_data;  
  4.        struct snd_soc_dai *cpu_dai = rtd->dai->cpu_dai;  
  5.        struct snd_pcm_runtime *runtime = substream->runtime;  
  6.        struct audio_stream_a *s = runtime->private_data;  
  7.        int ret;  
  8.   
  9.        if (!cpu_dai->active) {  
  10.               audio_dma_request(&s[0], audio_dma_callback); //为playback stream分配DMA  
  11.               audio_dma_request(&s[1], audio_dma_callback); //为capture stream分配DMA  
  12.        }  
  13.          
  14.        //设定runtime硬件参数  
  15.        snd_soc_set_runtime_hwparams(substream, &s3c_pcm_hardware);  
  16.   
  17.        /* Ensure that buffer size is a multiple of period size */  
  18.        ret = snd_pcm_hw_constraint_integer(runtime,  
  19.                              SNDRV_PCM_HW_PARAM_PERIODS);  
  20.   
  21.        return ret;  
  22. }  

其中硬件参数要根据芯片的数据手册来定义,如:
[cpp] view plain copy
  1. static const struct snd_pcm_hardware s3c_pcm_hardware = {  
  2.        .info            = SNDRV_PCM_INFO_INTERLEAVED |  
  3.                                 SNDRV_PCM_INFO_BLOCK_TRANSFER |  
  4.                                 SNDRV_PCM_INFO_MMAP |  
  5.                                 SNDRV_PCM_INFO_MMAP_VALID |  
  6.                                 SNDRV_PCM_INFO_PAUSE |  
  7.                                 SNDRV_PCM_INFO_RESUME,  
  8.        .formats         = SNDRV_PCM_FMTBIT_S16_LE |  
  9.                                 SNDRV_PCM_FMTBIT_U16_LE |  
  10.                                 SNDRV_PCM_FMTBIT_U8 |  
  11.                                 SNDRV_PCM_FMTBIT_S8,  
  12.        .channels_min     = 2,  
  13.        .channels_max     = 2,  
  14.        .buffer_bytes_max = 128*1024,  
  15.        .period_bytes_min = PAGE_SIZE,  
  16.        .period_bytes_max = PAGE_SIZE*2,  
  17.        .periods_min      = 2,  
  18.        .periods_max      = 128,  
  19.        .fifo_size        = 32,  
  20. };  

关于peroid的概念有这样的描述:The “period” is a term that corresponds to a fragment in the OSS world. The period defines the size at which a PCM interrupt is generated. peroid的概念很重要,建议去alsa官网找相关详细说明了解一下。

上层ALSA lib可以通过接口来获得这些参数的,如snd_pcm_hw_params_get_buffer_size_max()来取得buffer_bytes_max。


关于DMA的中断处理

另外留意open函数中的audio_dma_request(&s[0], audio_dma_callback);中的audio_dma_callback,这是dma的中断函数,这里以callback的形式存在,其实到dma的底层还是这样的形式:static irqreturn_t dma_irq_handler(int irq, void *dev_id),在DMA中断处理dma_irq_handler()中调用callback。这些跟具体硬件平台的DMA实现相关,如果没有类似的机制,那么还是要在pcm模块中实现这个中断。

[cpp] view plain copy
  1. /*  
  2.  *  This is called when dma IRQ occurs at the end of each transmited block 
  3.  */  
  4. static void audio_dma_callback(void *data)  
  5. {  
  6.        struct audio_stream_a *s = data;      
  7.   
  8.        /*  
  9.         * If we are getting a callback for an active stream then we inform 
  10.         * the PCM middle layer we've finished a period 
  11.         */  
  12.        if (s->active)  
  13.               snd_pcm_period_elapsed(s->stream);  
  14.   
  15.        spin_lock(&s->dma_lock);  
  16.        if (s->periods > 0)   
  17.               s->periods--;      
  18.   
  19.        audio_process_dma(s); //dma启动  
  20.        spin_unlock(&s->dma_lock);  
  21. }  

hw_params

hw_params函数为substream(每打开一个playback或capture,ALSA core均产生相应的一个substream)设定DMA的源(目的)地址,以及DMA缓冲区的大小。

[cpp] view plain copy
  1. static int s3c_pcm_hw_params(struct snd_pcm_substream *substream,  
  2.                            struct snd_pcm_hw_params *params)  
  3. {  
  4.        struct snd_pcm_runtime *runtime = substream->runtime;  
  5.        int err = 0;  
  6.   
  7.        snd_pcm_set_runtime_buffer(substream, &substream->dma_buffer);  
  8.        runtime->dma_bytes = params_buffer_bytes(params);  
  9.        return err;  
  10. }  

hw_free是hw_params的相反操作,调用snd_pcm_set_runtime_buffer(substream, NULL)即可。
注:代码中的dma_buffer是DMA缓冲区,它通过4个字段定义:dma_area、dma_addr、dma_bytes和dma_private。其中dma_area是缓冲区逻辑地址,dma_addr是缓冲区的物理地址,dma_bytes是缓冲区的大小,dma_private是ALSA的DMA管理用到的。dma_buffer是在pcm_new()中初始化的;当然也可以把分配dma缓冲区的工作放到这部分来实现,但考虑到减少碎片,故还是在pcm_new中以最大size(即buffer_bytes_max)来分配。
 

prepare

当pcm“准备好了”调用该函数。在这里根据channels、buffer_bytes等来设定DMA传输参数,跟具体硬件平台相关。注:每次调用snd_pcm_prepare()的时候均会调用prepare函数。


trigger

当pcm开始、停止、暂停的时候都会调用trigger函数。

[cpp] view plain copy
  1. static int s3c_pcm_trigger(struct snd_pcm_substream *substream, int cmd)  
  2. {  
  3.        struct runtime_data *prtd = substream->runtime->private_data;  
  4.        int ret = 0;  
  5.   
  6.        spin_lock(&prtd->lock);  
  7.    
  8.        switch (cmd) {  
  9.        case SNDRV_PCM_TRIGGER_START:  
  10.        case SNDRV_PCM_TRIGGER_RESUME:  
  11.        case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:  
  12.               prtd->state |= ST_RUNNING;  
  13.               dma_ctrl(prtd->params->channel, DMAOP_START); //DMA开启  
  14.               break;  
  15.   
  16.        case SNDRV_PCM_TRIGGER_STOP:  
  17.        case SNDRV_PCM_TRIGGER_SUSPEND:  
  18.        case SNDRV_PCM_TRIGGER_PAUSE_PUSH:  
  19.               prtd->state &= ~ST_RUNNING;  
  20.               dma_ctrl(prtd->params->channel, DMAOP_STOP); //DMA停止  
  21.               break;  
  22.   
  23.        default:  
  24.               ret = -EINVAL;  
  25.               break;  
  26.        }  
  27.   
  28.        spin_unlock(&prtd->lock);  
  29.   
  30.        return ret;  
  31. }  
Trigger函数里面的操作应该是原子的,不要在调用这些操作时进入睡眠,trigger函数应尽量小,甚至仅仅是触发DMA。


pointer

static snd_pcm_uframes_t wmt_pcm_pointer(struct snd_pcm_substream *substream)
PCM中间层通过调用这个函数来获取缓冲区的位置。一般情况下,在中断函数中调用snd_pcm_period_elapsed()或在pcm中间层更新buffer的时候调用它。然后pcm中间层会更新指针位置和计算缓冲区可用空间,唤醒那些在等待的线程。这个函数也是原子的。
 

snd_pcm_runtime
我们会留意到ops各成员函数均需要取得一个snd_pcm_runtime结构体指针,这个指针可以通过substream->runtime来获得。snd_pcm_runtime是pcm运行时的信息。当打开一个pcm子流时,pcm运行时实例就会分配给这个子流。它拥有很多多种信息:hw_params和sw_params配置拷贝,缓冲区指针,mmap记录,自旋锁等。snd_pcm_runtime对于驱动程序操作集函数是只读的,仅pcm中间层可以改变或更新这些信息。


分享到:
  • 上一篇:ALSA之CODEC分析
  • 下一篇:简单原始的FFMpeg播放器
  • 查看评论
    1楼 yyj1982 2012-02-08 20:27发表 [回复]
    老大又来麻烦你了!
    下层通过在DMA中断中调用snd_pcm_period_elapsed来告诉ALSA中间层缓冲区指针跨越了period边界,然后ALSA中间层通过调用pointer来获取缓冲区指针的位置,然后开始写入/读取数据,我的问题是写入/读取数据后,ALSA中间层如何通知下层的driver更新缓冲区指针呢?
    例如:我的硬件有个缓冲区管理机制:
    当playback/capture时,缓冲区指针由硬件自己维护,driver可以通过读register实时获得缓冲区指针的值。当缓冲区指针跨越period边界时,硬件产生interrupt。然后driver可以在interrupt中往缓冲区中写入一个period长度的数据,然后通过设置register来更新缓冲区指针。
    由于ALSA的Driver不知道中间层什么时候写入数据,因此不知道何时通过设置register来更新缓冲区指针?不知老大可以方法?谢谢!
    Re: sepnic 2012-02-09 10:02发表 [回复]
    回复yyj1982:对于“当playback/capture时,缓冲区指针由硬件自己维护,driver可以通过读register实时获得缓冲区指针的值。”,我不明白你说的driver具体是指哪一个?codec?i2s?dma?
    我只能认为是dma buffer,但是dma的驱动和声卡驱动是独立开来的啊,并不需要你去留意什么register,你只要会调用dma接口就行了。
    所以我不明白为什么你还需要“通过设置register来更新缓冲区指针”。
    Re: yyj1982 2012-02-09 22:46发表 [回复]
    回复sepnic:我主要是不明白ALSA中间层如何维护dma buffer中的指针。
    例如,playback时,Application通过snd_pcm_writei往ALSA的中间层写入数据后,ALSA中间层如何更新dma buffer的指针或者ALSA中间层如何通知DMA driver来更新dma buffer的指针?
    Re: sepnic 2012-02-10 16:54发表 [回复]
    回复yyj1982:你到pcm_native.c和pcm_lib.c跟一下,简单描述:
    [cpp] view plain copy
    1. snd_pcm_writei  
    2.   ->snd_pcm_playback_ioctl  
    3.     ->snd_pcm_lib_write/snd_pcm_lib_writev  
    4.       ->snd_pcm_lib_write1  
    5.         ->snd_pcm_update_hw_ptr  
    6.           ->snd_pcm_update_hw_ptr_pos  
    7.             ->pos = substream->ops->pointer(substream);  

    就是上层每调用一次write或read的时候,都会触发底层pointer函数更新dma buffer的。
    Re: yyj1982 2012-02-13 12:48发表 [回复]
    回复sepnic:谢谢!

    你可能感兴趣的:(ALSA之PCM分析)