CODEC :音频芯片的控制,比如静音、打开(关闭)ADC(DAC)、设置ADC(DAC)的增益、耳机模式的检测等操作。
I2S :数字音频接口,用于CPU和Codec之间的数字音频流raw data的传输。每当有playback或record操作时,snd_soc_dai_ops.prepare()会被调用,启动I2S总线。
PCM :我不知道为什么会取这个模块名,它其实是定义DMA操作的,用于将音频数据通过DMA传到I2S控制器的FIFO中。
这里的PCM实际是就是更新和管理音频数据流的地址,分配DMA等等,将RAM中存放的音频数据的地址传给I2S,不是PCM协议。
音频数据流向:
| DMA | | I2S/PCM/AC97 |
RAM --------> I2SControllerFIFO -----------------> CODEC ----> SPK/Headset
PCM模块初始化:
struct snd_soc_platform rk29_soc_platform = { .name = "rockchip-audio", .pcm_ops = &rockchip_pcm_ops, .pcm_new = rockchip_pcm_new, .pcm_free = rockchip_pcm_free_dma_buffers, }; EXPORT_SYMBOL_GPL(rk29_soc_platform); static int __init rockchip_soc_platform_init(void) { DBG("Enter::%s, %d\n", __FUNCTION__, __LINE__); return snd_soc_register_platform(&rk29_soc_platform); } module_init(rockchip_soc_platform_init); static void __exit rockchip_soc_platform_exit(void) { snd_soc_unregister_platform(&rk29_soc_platform); }调用snd_soc_register_platform()向ALSA core注册一个snd_soc_platform结构体。
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
open函数为PCM模块设定支持的传输模式、数据格式、通道数、period等参数,并为playback/capture stream分配相应的DMA通道。其一般实现如下:
static int rockchip_pcm_open(struct snd_pcm_substream *substream) { struct snd_pcm_runtime *runtime = substream->runtime; struct rockchip_runtime_data *prtd; DBG("Enter::%s----%d\n",__FUNCTION__,__LINE__); snd_soc_set_runtime_hwparams(substream, &rockchip_pcm_hardware); prtd = kzalloc(sizeof(struct rockchip_runtime_data), GFP_KERNEL); if (prtd == NULL) return -ENOMEM; spin_lock_init(&prtd->lock); runtime->private_data = prtd; return 0; }
int snd_soc_set_runtime_hwparams(struct snd_pcm_substream *substream, const struct snd_pcm_hardware *hw) { struct snd_pcm_runtime *runtime = substream->runtime; runtime->hw.info = hw->info; runtime->hw.formats = hw->formats; runtime->hw.period_bytes_min = hw->period_bytes_min; runtime->hw.period_bytes_max = hw->period_bytes_max; runtime->hw.periods_min = hw->periods_min; runtime->hw.periods_max = hw->periods_max; runtime->hw.buffer_bytes_max = hw->buffer_bytes_max; runtime->hw.fifo_size = hw->fifo_size; return 0; }
关于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。
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)来分配。
关于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模块中实现这个中断。
void rockchip_pcm_dma_irq(s32 ch, void *data) { struct snd_pcm_substream *substream = data; struct rockchip_runtime_data *prtd; unsigned long flags; DBG("Enter::%s----%d\n",__FUNCTION__,__LINE__); prtd = substream->runtime->private_data; if (substream) snd_pcm_period_elapsed(substream); spin_lock(&prtd->lock); prtd->dma_loaded--; if (prtd->state & ST_RUNNING) { rockchip_pcm_enqueue(substream); } spin_unlock(&prtd->lock); local_irq_save(flags); if (prtd->state & ST_RUNNING) { if (prtd->dma_loaded) { if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) audio_start_dma(substream, DMA_MODE_WRITE); else audio_start_dma(substream, DMA_MODE_READ); } } local_irq_restore(flags); }
prepare
当pcm“准备好了”调用该函数。在这里根据channels、buffer_bytes等来设定DMA传输参数,跟具体硬件平台相关。注:每次调用snd_pcm_prepare()的时候均会调用prepare函数。
trigger
当pcm开始、停止、暂停的时候都会调用trigger函数。
static int rockchip_pcm_trigger(struct snd_pcm_substream *substream, int cmd) { struct rockchip_runtime_data *prtd = substream->runtime->private_data; int ret = 0; /**************add by qiuen for volume*****/ struct snd_soc_pcm_runtime *rtd = substream->private_data; struct snd_soc_dai *pCodec_dai = rtd->dai->codec_dai; int vol = 0; int streamType = 0; DBG("Enter::%s----%d\n",__FUNCTION__,__LINE__); if(cmd==SNDRV_PCM_TRIGGER_VOLUME){ vol = substream->number % 100; streamType = (substream->number / 100) % 100; DBG("enter:vol=%d,streamType=%d\n",vol,streamType); if(pCodec_dai->ops->set_volume) pCodec_dai->ops->set_volume(streamType, vol); } /****************************************************/ spin_lock(&prtd->lock); switch (cmd) { case SNDRV_PCM_TRIGGER_START: DBG(" START \n"); prtd->state |= ST_RUNNING; rk29_dma_ctrl(prtd->params->channel, RK29_DMAOP_START); /* if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { audio_start_dma(substream, DMA_MODE_WRITE); } else { audio_start_dma(substream, DMA_MODE_READ); } */ #ifdef CONFIG_ANDROID_POWER android_lock_suspend(&audio_lock); DBG("%s::start audio , lock system suspend\n" , __func__ ); #endif break; case SNDRV_PCM_TRIGGER_RESUME: DBG(" RESUME \n"); break; case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: DBG(" RESTART \n"); break; case SNDRV_PCM_TRIGGER_STOP: case SNDRV_PCM_TRIGGER_SUSPEND: case SNDRV_PCM_TRIGGER_PAUSE_PUSH: DBG(" STOPS \n"); prtd->state &= ~ST_RUNNING; rk29_dma_ctrl(prtd->params->channel, RK29_DMAOP_STOP); //disable_dma(prtd->params->channel); #ifdef CONFIG_ANDROID_POWER android_unlock_suspend(&audio_lock ); DBG("%s::stop audio , unlock system suspend\n" , __func__ ); #endif break; default: ret = -EINVAL; break; } spin_unlock(&prtd->lock); return ret; }
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中间层可以改变或更新这些信息。