天天看點

Linux 音頻驅動(六) ALSA音頻驅動之PCM Write資料傳遞過程

目錄

      • 1. 前言
      • 2. PCM Data Flow
      • 3. 總結

1. 前言

本文,我們将以回放(Playback,播放音頻)為例,講解PCM Data是如何從使用者空間到核心空間,最後傳遞到Codec。

在Linux 音頻驅動(一) ASoC音頻架構簡介中,我們給出了回放(Playback)PCM資料流示意圖:

Linux 音頻驅動(六) ALSA音頻驅動之PCM Write資料傳遞過程
  1. 對于Linux來說,由于分為 user space 和kernel space,而且兩者之間不能随便互相通路。是以使用者如果播放音頻,則需要調用copy_from_user()将使用者資料從user space拷貝到kernel space (DMA Buffer)。
  2. DMA 負責将DMA Buffer中的音頻資料搬運到I2S TX FIFO。
  3. 通過I2S總線,将音頻資料傳送到Codec。
  4. Codec内部經過DAC轉換,将模拟信号傳到揚聲器SPK(頭戴式耳機HP、耳塞式耳機Earp)。

下面基于源碼講解PCM Data Flow。

2. PCM Data Flow

核心版本:Kernel 版本:3.10

核心源碼檔案:

         ./kernel-3.10/sound/core/device.c

         ./kernel-3.10/sound/core/init.c

         ./kernel-3.10/sound/core/pcm.c

         ./kernel-3.10/sound/core/pcm_lib.c

         ./kernel-3.10/sound/core/pcm_native.c

         ./kernel-3.10/sound/soc/soc-pcm.c

Tinyalsa源碼檔案:

         ./external/tinyalsa/pcm.c

         ./external/kernel-headers/original/uapi/sound/asound.h

User Space

在我的源碼包裡,使用者空間應用程式使用的是 tinyalsa提供的接口write PCM Data,即播放音頻檔案。

Write PCM邏輯裝置是通過 ioctl() 函數完成的,即應用程式将需要播放的音頻資料通過pcm_write() --> ioctl() 傳遞到核心。

// ./external/kernel-headers/original/uapi/sound/asound.h, line 448
struct snd_xferi {
	snd_pcm_sframes_t result;
	void __user *buf;
	snd_pcm_uframes_t frames;
};
// ./external/tinyalsa/pcm.c, line 483
int pcm_write(struct pcm *pcm, const void *data, unsigned int count)
{
    struct snd_xferi x;
    ......
    x.buf = (void*)data;
    x.frames = count / (pcm->config.channels *
                        pcm_format_to_bits(pcm->config.format) / 8);
    ......
    ioctl(pcm->fd, SNDRV_PCM_IOCTL_WRITEI_FRAMES, &x);
    ......
}
           

音頻資料中的幾個重要概念:

Format:樣本長度(采樣精度 or 采樣深度),音頻資料最基本的機關,常見的有 8 位和 16 位;

Channel:聲道數,分為單聲道 mono 和立體聲stereo;

Frame:幀,構成一個完整的聲音單元,Frame = Format * Channel;

Rate:又稱 sample rate:采樣率,即每秒的采樣次數,針對幀而言;

Period size:周期,每次硬體中斷處理音頻資料的幀數,對于音頻裝置的資料讀寫,以此為機關;

Buffer size:資料緩沖區大小,這裡指runtime 的 buffer size,而不是結構體 snd_pcm_hardware 中定義的buffer_bytes_max;一般來說 buffer_size = period_size * period_count,period_count 相當于處理完一個 buffer 資料所需的硬體中斷次數。

為了通過系統調用

ioctl()

傳遞音頻資料,定義了

struct snd_xferi x

x.buf

指向本次要播放的音頻資料,

x.frames

表示本次音頻資料總共有多少幀(frame)。

Kernel Space

通過系統調用

ioctl()

傳遞資料到核心,在核心空間是PCM邏輯裝置對應的

snd_pcm_f_ops[0].unlocked_ioctl()

// ./kernel-3.10/sound/core/pcm_native.c, line 3481
const struct file_operations snd_pcm_f_ops[2] = {
	{
		.owner =		THIS_MODULE,
		.write =		snd_pcm_write,
        ......
		.unlocked_ioctl =	snd_pcm_playback_ioctl,
        ......
	},
	{
		.owner =		THIS_MODULE,
		.read =			snd_pcm_read,
        ......
		.unlocked_ioctl =	snd_pcm_capture_ioctl,
        ......
	}
};
           

snd_pcm_playback_ioctl()

直接調用了

snd_pcm_playback_ioctl1()

// ./kernel-3.10/sound/core/pcm_native.c, line 2784
static long snd_pcm_playback_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
	struct snd_pcm_file *pcm_file;
	pcm_file = file->private_data;
    ......
	return snd_pcm_playback_ioctl1(file, pcm_file->substream, cmd, (void __user *)arg);
}
           

snd_pcm_playback_ioctl1()函數:

a. 定義

struct snd_xferi xferi

,為了擷取使用者空間傳來的arg;

b. 調用

put_user()

清除snd_xferi.result狀态;

c. 調用

copy_from_user()

将取使用者空間的arg拷貝到核心空間,即拷貝音頻資料存儲空間的指針buf和音頻資料幀數frames;

d. 調用

snd_pcm_lib_write()

e. 調用

put_user()

回填write結果_xferi->result。

// ./kernel-3.10/sound/core/pcm_native.c, line 2624
static int snd_pcm_playback_ioctl1(struct file *file, struct snd_pcm_substream *substream, unsigned int cmd, void __user *arg)
{
    ......
	switch (cmd) {
	case SNDRV_PCM_IOCTL_WRITEI_FRAMES:
	{
		struct snd_xferi xferi;
		struct snd_xferi __user *_xferi = arg;
		struct snd_pcm_runtime *runtime = substream->runtime;
		snd_pcm_sframes_t result;
		if (runtime->status->state == SNDRV_PCM_STATE_OPEN)
			return -EBADFD;
		if (put_user(0, &_xferi->result))
			return -EFAULT;
		if (copy_from_user(&xferi, _xferi, sizeof(xferi)))
			return -EFAULT;
		result = snd_pcm_lib_write(substream, xferi.buf, xferi.frames);
		__put_user(result, &_xferi->result);
		return result < 0 ? result : 0;
	}
	......
}
           

snd_pcm_lib_write()

做一些參數檢查後調用

snd_pcm_lib_write1()

。注意調用

snd_pcm_lib_write1()

時傳入的最後一個參數

snd_pcm_lib_write_transfer

,該函數完成音頻資料從 kernel space 到 DMA Buffer 的傳輸。

// ./kernel-3.10/sound/core/pcm_lib.c, line 2101
snd_pcm_sframes_t snd_pcm_lib_write(struct snd_pcm_substream *substream, const void __user *buf, snd_pcm_uframes_t size)
{
	struct snd_pcm_runtime *runtime;
	int nonblock;
	......
	nonblock = !!(substream->f_flags & O_NONBLOCK);
	......
	return snd_pcm_lib_write1(substream, (unsigned long)buf, size, nonblock, snd_pcm_lib_write_transfer);
}
           

如下

snd_pcm_lib_write1()

代碼段中,第40行調用

transfer()

,即調用

snd_pcm_lib_write_transfer()

,将音頻資料從 kernel space 拷貝到 DMA Buffer。然後,在第49行,調用

snd_pcm_start()

啟動DMA傳輸,将音頻資料從 DMA Buffer 拷貝到 I2S TX FIFO。

// ./kernel-3.10/sound/core/pcm_lib.c, line 1985
static snd_pcm_sframes_t snd_pcm_lib_write1(struct snd_pcm_substream *substream, unsigned long data,
					    snd_pcm_uframes_t size, int nonblock, transfer_f transfer)
{
	struct snd_pcm_runtime *runtime = substream->runtime;
	snd_pcm_uframes_t xfer = 0;
	snd_pcm_uframes_t offset = 0;
	snd_pcm_uframes_t avail;
	int err = 0;

	if (size == 0)
		return 0;
    ......
	runtime->twake = runtime->control->avail_min ? : 1;
	if (runtime->status->state == SNDRV_PCM_STATE_RUNNING)
		snd_pcm_update_hw_ptr(substream);
	avail = snd_pcm_playback_avail(runtime);
	while (size > 0) {
		snd_pcm_uframes_t frames, appl_ptr, appl_ofs;
		snd_pcm_uframes_t cont;
		if (!avail) {
			if (nonblock) {
				err = -EAGAIN;
				goto _end_unlock;
			}
			runtime->twake = min_t(snd_pcm_uframes_t, size,
					runtime->control->avail_min ? : 1);
			err = wait_for_avail(substream, &avail);
			if (err < 0)
				goto _end_unlock;
		}
		frames = size > avail ? avail : size;
		cont = runtime->buffer_size - runtime->control->appl_ptr % runtime->buffer_size;
		if (frames > cont)
			frames = cont;
		......
		appl_ptr = runtime->control->appl_ptr;
		appl_ofs = appl_ptr % runtime->buffer_size;
		snd_pcm_stream_unlock_irq(substream);
		err = transfer(substream, appl_ofs, data, offset, frames);  //将音頻資料從 kernel space 拷貝到 DMA Buffer
		snd_pcm_stream_lock_irq(substream);
		......
		offset += frames;
		size -= frames;
		xfer += frames;
		avail -= frames;
		if (runtime->status->state == SNDRV_PCM_STATE_PREPARED &&
		    snd_pcm_playback_hw_avail(runtime) >= (snd_pcm_sframes_t)runtime->start_threshold) {
			err = snd_pcm_start(substream);  //啟動DMA傳輸,将音頻資料從 DMA Buffer 拷貝到 I2S TX FIFO
			if (err < 0)
				goto _end_unlock;
		}
	}
    ......
}
           

snd_pcm_lib_write_transfer()中:

a. 如果有設定過substream->ops->copy回調函數,則執行substream->ops->copy()将音頻資料從 kernel space 拷貝到 DMA Buffer。

b. 如果沒有設定substream->ops->copy回調函數,則直接調用copy_from_user()将音頻資料從 kernel space 拷貝到 DMA Buffer。

// ./kernel-3.10/sound/core/pcm_lib.c, line 1962
static int snd_pcm_lib_write_transfer(struct snd_pcm_substream *substream, unsigned int hwoff,
				      unsigned long data, unsigned int off, snd_pcm_uframes_t frames)
{
	struct snd_pcm_runtime *runtime = substream->runtime;
	int err;
	char __user *buf = (char __user *) data + frames_to_bytes(runtime, off);
	if (substream->ops->copy) {
		if ((err = substream->ops->copy(substream, -1, hwoff, buf, frames)) < 0)
			return err;
	} else {
		char *hwbuf = runtime->dma_area + frames_to_bytes(runtime, hwoff);
		if (copy_from_user(hwbuf, buf, frames_to_bytes(runtime, frames)))
			return -EFAULT;
	}
	return 0;
}
           
注:本例中,substream->ops->copy回調函數是在soc_new_pcm()中設定的。在soc_new_pcm()中,如果有設定platform->driver->ops (即PCM DMA驅動操作函數集),則PCM邏輯裝置的某些操作函數将會被platform->driver->ops中覆寫掉,如下代碼段第26行。
// ./kernel-3.10/sound/soc/soc-pcm.c, line 2005
int soc_new_pcm(struct snd_soc_pcm_runtime *rtd, int num)
{
    ......
	/* ASoC PCM operations */
	if (rtd->dai_link->dynamic) {
		rtd->ops.open		= dpcm_fe_dai_open;
		rtd->ops.hw_params	= dpcm_fe_dai_hw_params;
		rtd->ops.prepare	= dpcm_fe_dai_prepare;
		rtd->ops.trigger	= dpcm_fe_dai_trigger;
		rtd->ops.hw_free	= dpcm_fe_dai_hw_free;
		rtd->ops.close		= dpcm_fe_dai_close;
		rtd->ops.pointer	= soc_pcm_pointer;
		rtd->ops.ioctl		= soc_pcm_ioctl;
	} else {
		rtd->ops.open		= soc_pcm_open;
		rtd->ops.hw_params	= soc_pcm_hw_params;
		rtd->ops.prepare	= soc_pcm_prepare;
		rtd->ops.trigger	= soc_pcm_trigger;
		rtd->ops.hw_free	= soc_pcm_hw_free;
		rtd->ops.close		= soc_pcm_close;
		rtd->ops.pointer	= soc_pcm_pointer;
		rtd->ops.ioctl		= soc_pcm_ioctl;
	}

	if (platform->driver->ops) {
		rtd->ops.ack		= platform->driver->ops->ack;
		rtd->ops.copy		= platform->driver->ops->copy;
		rtd->ops.silence	= platform->driver->ops->silence;
		rtd->ops.page		= platform->driver->ops->page;
		rtd->ops.mmap		= platform->driver->ops->mmap;
	}
    ......
}
           

現在看一下

snd_pcm_start()

如何啟動DMA傳輸的。

snd_pcm_start()

直接調用了

snd_pcm_action()

。此處要注意

snd_pcm_action()

的第一個參數

snd_pcm_action_start

snd_pcm_action()

在多個地方會被調用,要注意其被調用時的第一個參數是什麼。

// ./kernel-3.10/sound/core/pcm_native.c, line 891
static struct action_ops snd_pcm_action_start = {
	.pre_action = snd_pcm_pre_start,
	.do_action = snd_pcm_do_start,
	.undo_action = snd_pcm_undo_start,
	.post_action = snd_pcm_post_start
};
// ./kernel-3.10/sound/core/pcm_native.c, line 904
int snd_pcm_start(struct snd_pcm_substream *substream)
{
	return snd_pcm_action(&snd_pcm_action_start, substream, SNDRV_PCM_STATE_RUNNING);
}
           

snd_pcm_action()

中會調用

snd_pcm_action_group()

snd_pcm_action_single()

。為簡化講解,我們關注

snd_pcm_action_single()

snd_pcm_action_single()

函數非常簡單,就是執行

struct action_ops *ops

指向的回調函數集,本例中為上文提到的

snd_pcm_action_start

// ./kernel-3.10/sound/core/pcm_native.c, line 785
static int snd_pcm_action(struct action_ops *ops, struct snd_pcm_substream *substream, int state)
{
	int res;

	if (snd_pcm_stream_linked(substream)) {
		if (!spin_trylock(&substream->group->lock)) {
			spin_unlock(&substream->self_group.lock);
			spin_lock(&substream->group->lock);
			spin_lock(&substream->self_group.lock);
		}
		res = snd_pcm_action_group(ops, substream, state, 1);
		spin_unlock(&substream->group->lock);
	} else {
		res = snd_pcm_action_single(ops, substream, state);
	}
	return res;
}
// ./kernel-3.10/sound/core/pcm_native.c, line 765
static int snd_pcm_action_single(struct action_ops *ops,
				 struct snd_pcm_substream *substream,
				 int state)
{
	int res;
	
	res = ops->pre_action(substream, state);
	if (res < 0)
		return res;
	res = ops->do_action(substream, state);
	if (res == 0)
		ops->post_action(substream, state);
	else if (ops->undo_action)
		ops->undo_action(substream, state);
	return res;
}
           

我們重點看一下

ops->do_action()

,即

snd_pcm_do_start()

。在前面

soc_new_pcm()

代碼段中,我們看到

substream->ops->trigger()

被設定為

soc_pcm_trigger()

,該函數依次調用codec_dai driver的trigger函數、pcm_dma的trigger函數、cpu_dai driver的trigger函數。

// ./kernel-3.10/sound/core/pcm_native.c, line 862
static int snd_pcm_do_start(struct snd_pcm_substream *substream, int state)
{
	if (substream->runtime->trigger_master != substream)
		return 0;
	return substream->ops->trigger(substream, SNDRV_PCM_TRIGGER_START);
}
           
// ./kernel-3.10/sound/core/soc-pcm.c, line 609
static int soc_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
{
    struct snd_soc_pcm_runtime *rtd = substream->private_data;
	struct snd_soc_platform *platform = rtd->platform;
	struct snd_soc_dai *cpu_dai = rtd->cpu_dai;
	struct snd_soc_dai *codec_dai = rtd->codec_dai;
	int ret;

	if (codec_dai->driver->ops->trigger) {
		ret = codec_dai->driver->ops->trigger(substream, cmd, codec_dai);  //調用codec_dai driver的trigger函數 (可選的)
		if (ret < 0)
			return ret;
	}

	if (platform->driver->ops && platform->driver->ops->trigger) {
		ret = platform->driver->ops->trigger(substream, cmd);  //調用pcm_dma的trigger函數
		if (ret < 0)
			return ret;
	}

	if (cpu_dai->driver->ops->trigger) {
		ret = cpu_dai->driver->ops->trigger(substream, cmd, cpu_dai);  //調用cpu_dai driver的trigger函數 (可選的)
		if (ret < 0)
			return ret;
	}
	return 0;
}
           

問題:為什麼執行trigger函數的順序是codec_dai --> pcm_dma --> cpu_dai 呢 ?是否可以調整順序 ?

思考:

是可以調整的。(下面的思考是我個人想法,其實我傾向于相信這部分的設計是大神有意為之的,可能我還沒了解吧。)

首先,codec_dai和cpu_dai的trigger函數是可選的。檢視struct snd_soc_dai_ops結構體的源碼,可以看到其中PCM operation函數集注釋說“ALSA PCM audio operations - all optional.”。

其次,假設codec_dai和cpu_dai的trigger都有定義,個人認為相對理想的順序應該是codec_dai --> cpu_dai --> pcm_dma。因為,當我們啟動DMA将音頻資料拷貝到cpu_dai(I2S TX Buffer)時,如果該硬體還沒有ready,那DMA搬過去的資料可能會丢失。同理,如果cpu_dai開始傳資料了,而codec_dai(I2S RX Buffer)沒有ready,那資料也有可能丢失。理論上應該是在啟動DMA之前,資料流下一接收裝置應該已經ready。是以,我才會說相對理想的trigger順序應該是codec_dai --> cpu_dai --> pcm_dma。

(我目前使用的MTK平台沒有定義codec_dai和cpu_dai的trigger函數,沒有辦法研究該順序是否有意義。有條件的讀者可以調整順序,看是否會對音頻播放造成問題。)

下面介紹trigger函數的内容不同的SoC平台會有差異,本例基于MTK平台的源碼分析。

本例中,codec_dai driver的trigger函數、pcm_dma的trigger函數、cpu_dai driver的trigger函數分别如下:

codec_dai driver的trigger函數:

mt6323_codec_trigger()

,雖然該函數有定義,但是該函數沒有做任何事情。
// ./kernel-3.10/sound/soc/mediatek/mt_soc_audio_v3/mt_soc_codec_63xx.c, line 1070
static struct snd_soc_dai_driver mtk_6331_dai_codecs[] =
{
    {
        .name = MT_SOC_CODEC_TXDAI_NAME,
        .ops = &mt6323_aif1_dai_ops,
        ......
    },
    ......
}
// ./kernel-3.10/sound/soc/mediatek/mt_soc_audio_v3/mt_soc_codec_63xx.c, line 1063
static const struct snd_soc_dai_ops mt6323_aif1_dai_ops =
{
    .startup    = mt63xx_codec_startup,
    .prepare   = mt63xx_codec_prepare,
    .trigger     = mt6323_codec_trigger,
};
// ./kernel-3.10/sound/soc/mediatek/mt_soc_audio_v3/mt_soc_codec_63xx.c, line 1048
static int mt6323_codec_trigger(struct snd_pcm_substream *substream , int command , struct snd_soc_dai *Daiport)
{
    switch (command)
    {
        case SNDRV_PCM_TRIGGER_START:
        case SNDRV_PCM_TRIGGER_RESUME:
        case SNDRV_PCM_TRIGGER_STOP:
        case SNDRV_PCM_TRIGGER_SUSPEND:
            break;
    }

    return 0;
}
           
pcm_dma的trigger函數:

mtk_pcm_I2S0dl1_trigger()

// ./kernel-3.10/sound/soc/mediatek/mt_soc_audio_v3/mt_soc_pcm_dl1_i2s0Dl1.c, line 730
static struct snd_soc_platform_driver mtk_I2S0dl1_soc_platform =
{
    .ops        = &mtk_I2S0dl1_ops,
    .pcm_new    = mtk_asoc_pcm_I2S0dl1_new,
    .probe      = mtk_afe_I2S0dl1_probe,
};
// ./kernel-3.10/sound/soc/mediatek/mt_soc_audio_v3/mt_soc_pcm_dl1_i2s0Dl1.c, line 715
static struct snd_pcm_ops mtk_I2S0dl1_ops =
{
    ......
    .trigger =  mtk_pcm_I2S0dl1_trigger,
    .pointer =  mtk_pcm_I2S0dl1_pointer,
    .copy =     mtk_pcm_I2S0dl1_copy,
    ......
};
// ./kernel-3.10/sound/soc/mediatek/mt_soc_audio_v3/mt_soc_pcm_dl1_i2s0Dl1.c, line 529
static int mtk_pcm_I2S0dl1_trigger(struct snd_pcm_substream *substream, int cmd)
{
    //printk("mtk_pcm_I2S0dl1_trigger cmd = %d\n", cmd);

    switch (cmd)
    {
        case SNDRV_PCM_TRIGGER_START:
        case SNDRV_PCM_TRIGGER_RESUME:
            return mtk_pcm_I2S0dl1_start(substream);  // 啟動 dma 傳輸
        case SNDRV_PCM_TRIGGER_STOP:
        case SNDRV_PCM_TRIGGER_SUSPEND:
            return mtk_pcm_I2S0dl1_stop(substream);  // 停止 dma 傳輸
    }
    return -EINVAL;
}
// ./kernel-3.10/sound/soc/mediatek/mt_soc_audio_v3/mt_soc_pcm_dl1_i2s0Dl1.c, line 493
static int mtk_pcm_I2S0dl1_start(struct snd_pcm_substream *substream)
{
    struct snd_pcm_runtime *runtime = substream->runtime;
    printk("%s\n", __func__);
    // here start digital part

    SetConnection(Soc_Aud_InterCon_Connection, Soc_Aud_InterConnectionInput_I05, Soc_Aud_InterConnectionOutput_O00);
    SetConnection(Soc_Aud_InterCon_Connection, Soc_Aud_InterConnectionInput_I06, Soc_Aud_InterConnectionOutput_O01);
    SetConnection(Soc_Aud_InterCon_Connection, Soc_Aud_InterConnectionInput_I05, Soc_Aud_InterConnectionOutput_O03);
    SetConnection(Soc_Aud_InterCon_Connection, Soc_Aud_InterConnectionInput_I06, Soc_Aud_InterConnectionOutput_O04);

    SetIrqEnable(Soc_Aud_IRQ_MCU_MODE_IRQ1_MCU_MODE, true);

    SetSampleRate(Soc_Aud_Digital_Block_MEM_DL1, runtime->rate);
    SetChannels(Soc_Aud_Digital_Block_MEM_DL1, runtime->channels);
    SetMemoryPathEnable(Soc_Aud_Digital_Block_MEM_DL1, true);

    EnableAfe(true);
    ......
    return 0;
}
           
cpu_dai driver的trigger函數:本例中沒有實作該函數
// ./kernel-3.10/sound/soc/mediatek/mt_soc_audio_v3/mt_soc_dai_stub.c, line 98
static struct snd_soc_dai_driver mtk_dai_stub_dai[] =
{
    {
        ......
        .name = MT_SOC_DL1DAI_NAME,
        .ops = &mtk_dai_stub_ops,
    },
    ......
}
// ./kernel-3.10/sound/soc/mediatek/mt_soc_audio_v3/mt_soc_dai_stub.c, line 93
static struct snd_soc_dai_ops mtk_dai_stub_ops =
{
    .startup    = multimedia_startup,
};
           

3. 總結

回放(Playback)PCM資料流示意圖:

Linux 音頻驅動(六) ALSA音頻驅動之PCM Write資料傳遞過程

最後簡單總結一下PCM write時的資料傳遞:

  i.  應用程式調用tinyalsa提供的接口

pcm_write()-->ioctl()

将需要回放的音頻資料指針和幀數傳遞給核心。

 ii.  核心在

snd_pcm_lib_write_transfer()

函數中使用

copy_from_user()

将音頻資料從user space拷貝到kernel space,即從應用程式的buffer拷貝到DMA buffer。

iii.  核心在

snd_pcm_start()

中啟動DMA傳輸,将音頻資料從DMA buffer拷貝到I2S TX FIFO。(實質上是通過pcm_dma的trigger函數來做的。)

繼續閱讀