[轉]QNX播放音頻audio-wav音源文件snd_pcm_ops

鋒影

email:[email protected]

如果你認爲本系列文章對你有所幫助,請大家有錢的捧個錢場,點擊此處贊助,贊助額0.1元起步,多少隨意

 

origin: https://blog.csdn.net/azloong/article/details/6146378

之前寫過一個音頻驅動CODEC分析,當時忽略了闡述最基本的概念。要了解一個東西,首先要明白它是什麼它起到什麼作用,然後纔會更好對它的工作流程更好的分析。所以這裏提一下:

CODEC :音頻芯片的控制,比如靜音、打開(關閉)ADC(DAC)、設置ADC(DAC)的增益、耳機模式的檢測等操作。

I2S   :數字音頻接口,用於CPU和Codec之間的數字音頻流raw data的傳輸。每當有playback或record操作時,snd_soc_dai_ops.prepare()會被調用,啓動I2S總線。

PCM   :我不知道爲什麼會取這個模塊名,它其實是定義DMA操作的,用於將音頻數據通過DMA傳到I2S控制器的FIFO中。


音頻數據流向:

  | copy_from_user |          | DMA |               | I2S/PCM/AC97 |
RAM--------------->dma buffer--------> I2S tx FIFO -----------------> CODEC -> SPK/Headset


PCM模塊初始化

    struct snd_soc_platform s3c_soc_platform = {
           .name         = "s3c-pcm-audio",
           .pcm_ops      = &s3c_pcm_ops,
           .pcm_new      = s3c_pcm_new,
           .pcm_free     = s3c_pcm_free_dma_buffers,
           .suspend      = s3c_pcm_suspend,
           .resume       = s3c_pcm_resume,
    };

調用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結構體,該結構體的操作函數集的實現是本模塊的主體。

    struct snd_pcm_ops {
           int (*open)(struct snd_pcm_substream *substream);
           int (*close)(struct snd_pcm_substream *substream);
           int (*ioctl)(struct snd_pcm_substream * substream,
                       unsigned int cmd, void *arg);
           int (*hw_params)(struct snd_pcm_substream *substream,
                          struct snd_pcm_hw_params *params);
           int (*hw_free)(struct snd_pcm_substream *substream);
           int (*prepare)(struct snd_pcm_substream *substream);
           int (*trigger)(struct snd_pcm_substream *substream, int cmd);
           snd_pcm_uframes_t (*pointer)(struct snd_pcm_substream *substream);
           int (*copy)(struct snd_pcm_substream *substream, int channel,
                      snd_pcm_uframes_t pos,
                      void __user *buf, snd_pcm_uframes_t count);
           int (*silence)(struct snd_pcm_substream *substream, int channel,
                         snd_pcm_uframes_t pos, snd_pcm_uframes_t count);
           struct page *(*page)(struct snd_pcm_substream *substream,
                              unsigned long offset);
           int (*mmap)(struct snd_pcm_substream *substream, struct vm_area_struct *vma);
           int (*ack)(struct snd_pcm_substream *substream);
    };

我們主要實現open、close、hw_params、hw_free、prepare和trigger接口。


open

open函數爲PCM模塊設定支持的傳輸模式、數據格式、通道數、period等參數,併爲playback/capture stream分配相應的DMA通道。其一般實現如下:

    static int s3c_pcm_open(struct snd_pcm_substream *substream)
    {
           struct snd_soc_pcm_runtime *rtd = substream->private_data;
           struct snd_soc_dai *cpu_dai = rtd->dai->cpu_dai;
           struct snd_pcm_runtime *runtime = substream->runtime;
           struct audio_stream_a *s = runtime->private_data;
           int ret;
     
           if (!cpu_dai->active) {
                  audio_dma_request(&s[0], audio_dma_callback); //爲playback stream分配DMA
                  audio_dma_request(&s[1], audio_dma_callback); //爲capture stream分配DMA
           }
           
           //設定runtime硬件參數
           snd_soc_set_runtime_hwparams(substream, &s3c_pcm_hardware);
     
           /* Ensure that buffer size is a multiple of period size */
           ret = snd_pcm_hw_constraint_integer(runtime,
                                 SNDRV_PCM_HW_PARAM_PERIODS);
     
           return ret;
    }

其中硬件參數要根據芯片的數據手冊來定義,如:

    static const struct snd_pcm_hardware s3c_pcm_hardware = {
           .info            = SNDRV_PCM_INFO_INTERLEAVED |
                                    SNDRV_PCM_INFO_BLOCK_TRANSFER |
                                    SNDRV_PCM_INFO_MMAP |
                                    SNDRV_PCM_INFO_MMAP_VALID |
                                    SNDRV_PCM_INFO_PAUSE |
                                    SNDRV_PCM_INFO_RESUME,
           .formats         = SNDRV_PCM_FMTBIT_S16_LE |
                                    SNDRV_PCM_FMTBIT_U16_LE |
                                    SNDRV_PCM_FMTBIT_U8 |
                                    SNDRV_PCM_FMTBIT_S8,
           .channels_min     = 2,
           .channels_max     = 2,
           .buffer_bytes_max = 128*1024,
           .period_bytes_min = PAGE_SIZE,
           .period_bytes_max = PAGE_SIZE*2,
           .periods_min      = 2,
           .periods_max      = 128,
           .fifo_size        = 32,
    };

關於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模塊中實現這個中斷。

    /*
     *  This is called when dma IRQ occurs at the end of each transmited block
     */
    static void audio_dma_callback(void *data)
    {
           struct audio_stream_a *s = data;    
     
           /*
            * If we are getting a callback for an active stream then we inform
            * the PCM middle layer we've finished a period
            */
           if (s->active)
                  snd_pcm_period_elapsed(s->stream);
     
           spin_lock(&s->dma_lock);
           if (s->periods > 0)
                  s->periods--;    
     
           audio_process_dma(s); //dma啓動
           spin_unlock(&s->dma_lock);
    }


hw_params

hw_params函數爲substream(每打開一個playback或capture,ALSA core均產生相應的一個substream)設定DMA的源(目的)地址,以及DMA緩衝區的大小。

    static int s3c_pcm_hw_params(struct snd_pcm_substream *substream,
                               struct snd_pcm_hw_params *params)
    {
           struct snd_pcm_runtime *runtime = substream->runtime;
           int err = 0;
     
           snd_pcm_set_runtime_buffer(substream, &substream->dma_buffer);
           runtime->dma_bytes = params_buffer_bytes(params);
           return err;
    }

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函數。

    static int s3c_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
    {
           struct runtime_data *prtd = substream->runtime->private_data;
           int ret = 0;
     
           spin_lock(&prtd->lock);
     
           switch (cmd) {
           case SNDRV_PCM_TRIGGER_START:
           case SNDRV_PCM_TRIGGER_RESUME:
           case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
                  prtd->state |= ST_RUNNING;
                  dma_ctrl(prtd->params->channel, DMAOP_START); //DMA開啓
                  break;
     
           case SNDRV_PCM_TRIGGER_STOP:
           case SNDRV_PCM_TRIGGER_SUSPEND:
           case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
                  prtd->state &= ~ST_RUNNING;
                  dma_ctrl(prtd->params->channel, DMAOP_STOP); //DMA停止
                  break;
     
           default:
                  ret = -EINVAL;
                  break;
           }
     
           spin_unlock(&prtd->lock);
     
           return ret;
    }

Trigger函數裏面的操作應該是原子的,不要在調用這些操作時進入睡眠,trigger函數應儘量小,甚至僅僅是觸發DMA。


pointer

static snd_pcm_uframes_t s3c_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中間層可以改變或更新這些信息。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章