Linux ALSA驅動之三:PCM建立流程原始碼分析(基於Linux 5.18)

語言: CN / TW / HK

開啟掘金成長之旅!這是我參與「掘金日新計劃 · 2 月更文挑戰」的第 3 天,點選檢視活動詳情

1、基本概念及邏輯關係

  如上圖,通過上一節音效卡的學習我們已經知道PCM是音效卡的一個子裝置,或者表示一個PCM例項。

  每個音效卡最多可以包含4個pcm的例項,每個pcm例項對應一個pcm裝置檔案。pcm例項數量的這種限制源於linux裝置號所佔用的位大小,如果以後使用64位的裝置號,我們將可以建立更多的pcm例項。不過大多數情況下,在嵌入式裝置中,一個pcm例項已經足夠了。

  一個pcm例項由一個playback stream和一個capture stream組成,這兩個stream又分別有一個或多個substreams組成。可以用如下圖來表示他們直接的邏輯關係:

  當一個子流已經存在,並且已經被開啟,當再次被開啟的時候,會被阻塞。        

  在實際的應用中,通常不會如上圖這麼複雜,大多數情況下是一個音效卡有一個PCM例項,PCM下面有一個playback和capture,而playback和capture各自有一個substream。

  PCM層有幾個很重要的結構體,我們通過如下的UML圖來梳理他們直接的關係。

  1、snd_pcm: 掛在snd_card下面的一個snd_device。

  2、snd_pcm中的欄位:streams[2] :該陣列中的兩個元素指向兩個snd_pcm_str結構,分別代表playback stream和capture stream。

  3、snd_pcm_str中的substream欄位:指向snd_pcm_substream結構。

  4、snd_pcm_substream是pcm中間層的核心,絕大部分任務都是在substream中處理,尤其是他的ops(snd_pcm_ops)欄位,許多user空間的應用程式通過alsa-lib對驅動程式的請求都是由該結構中的函式處理。它的runtime欄位則指向snd_pcm_runtime結構,snd_pcm_runtime記錄這substream的一些重要的軟體和硬體執行環境和引數。

2、PCM建立流程

  PCM的整個建立流程請參考如下時序圖進行理解:

  alsa-driver的中間層已經提供新建PCM的API:

2.1、建立PCM例項

C int snd_pcm_new(struct snd_card *card, const char *id, int device, int playback_count, int capture_count, struct snd_pcm **rpcm)

  card: 表示所屬的音效卡。

  ID: PCM例項的ID(名字)。

  device: 表示目前建立的是該音效卡下的第幾個PCM,第一個PCM裝置從0開始計數。

  playback_count: 表示該PCM播放流中將會有幾個substream。

  capture_count :表示該PCM錄音流中將會有幾個substream。

  rpcm: 返回的PCM例項。

  該函式的主要作用是建立PCM邏輯裝置,建立回放子流和錄製子流例項,並初始化回放子流和錄製子流的PCM操作函式(資料搬運時,需要呼叫這些函式來驅動 codec、codec_dai、cpu_dai、dma 裝置工作)。

2.2、設定PCM裝置的操作函式

C void snd_pcm_set_ops(struct snd_pcm *pcm, int direction, const struct snd_pcm_ops *ops)   pcm: 上述snd_pcm_new 建立的PCM例項。

  direction: 是指SNDRV_PCM_STREAM_PLAYBACK或SNDRV_PCM_STREAM_CAPTURE,即設定為播放或者錄音功能。

  snd_pcm_ops: 結構中的函式通常就是我們驅動要實現的函式。

2.3、定義PCM的操作函式

  以AC97驅動(linux/sound/arm/pxa2xx-ac97.c)為例,在驅動中對於PCM進行了如下設定:

```C++ static const struct snd_pcm_ops pxa2xx_ac97_pcm_ops = { .open = pxa2xx_ac97_pcm_open, .close = pxa2xx_ac97_pcm_close, .hw_params = pxa2xx_pcm_hw_params, .prepare = pxa2xx_ac97_pcm_prepare, .trigger = pxa2xx_pcm_trigger, .pointer = pxa2xx_pcm_pointer, };

snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &pxa2xx_ac97_pcm_ops); snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &pxa2xx_ac97_pcm_ops); ```

2.4、定義硬體引數

```C++ static const struct snd_pcm_hardware pxa2xx_pcm_hardware = { .info = SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_MMAP_VALID | SNDRV_PCM_INFO_INTERLEAVED | SNDRV_PCM_INFO_PAUSE | SNDRV_PCM_INFO_RESUME, .formats = SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S24_LE | SNDRV_PCM_FMTBIT_S32_LE, .period_bytes_min = 32, .period_bytes_max = 8192 - 32, .periods_min = 1, .periods_max = 256, .buffer_bytes_max = 128 * 1024, .fifo_size = 32, };

int pxa2xx_pcm_open(struct snd_pcm_substream substream) { struct snd_soc_pcm_runtime rtd = substream->private_data; struct snd_pcm_runtime runtime = substream->runtime; struct snd_dmaengine_dai_dma_data dma_params; int ret;

runtime->hw = pxa2xx_pcm_hardware;

dma_params = snd_soc_dai_get_dma_data(asoc_rtd_to_cpu(rtd, 0), substream);
if (!dma_params)
        return 0;

/*
 * For mysterious reasons (and despite what the manual says)
 * playback samples are lost if the DMA count is not a multiple
 * of the DMA burst size.  Let's add a rule to enforce that.
 */
ret = snd_pcm_hw_constraint_step(runtime, 0, SNDRV_PCM_HW_PARAM_PERIOD_BYTES, 32);
if (ret)
        return ret;

ret = snd_pcm_hw_constraint_step(runtime, 0, SNDRV_PCM_HW_PARAM_BUFFER_BYTES, 32);
if (ret)
        return ret;

ret = snd_pcm_hw_constraint_integer(runtime, SNDRV_PCM_HW_PARAM_PERIODS);
if (ret < 0)
        return ret;

return snd_dmaengine_pcm_open(substream, dma_request_slave_channel(asoc_rtd_to_cpu(rtd, 0)->dev, dma_params->chan_name));

} ```

3、PCM相關原始碼分析

3.1、snd_pcm_new

```C++ / * snd_pcm_new - create a new PCM instance * @card: the card instance * @id: the id string * @device: the device index (zero based) * @playback_count: the number of substreams for playback * @capture_count: the number of substreams for capture * @rpcm: the pointer to store the new pcm instance * * Creates a new PCM instance. * * The pcm operators have to be set afterwards to the new instance * via snd_pcm_set_ops(). * * Return: Zero if successful, or a negative error code on failure. / int snd_pcm_new(struct snd_card card, const char *id, int device, int playback_count, int capture_count, struct snd_pcm rpcm) { / 直接呼叫函式_snd_pcm_new,引數internal傳入false / return _snd_pcm_new(card, id, device, playback_count, capture_count, false, rpcm); }

static int _snd_pcm_new(struct snd_card card, const char id, int device, int playback_count, int capture_count, bool internal, struct snd_pcm rpcm) { struct snd_pcm pcm; int err; / 1. 邏輯裝置的操作函式結構體, 主要用於註冊子裝置 */ static const struct snd_device_ops ops = { .dev_free = snd_pcm_dev_free, .dev_register = snd_pcm_dev_register, .dev_disconnect = snd_pcm_dev_disconnect, }; static const struct snd_device_ops internal_ops = { .dev_free = snd_pcm_dev_free, };

if (snd_BUG_ON(!card))
    return -ENXIO;
if (rpcm)
    *rpcm = NULL;
/* 2. 為snd_pcm結構體分配空間,根據傳入引數賦值 */
pcm = kzalloc(sizeof(*pcm), GFP_KERNEL);
if (!pcm)
    return -ENOMEM;
pcm->card = card;
pcm->device = device;
pcm->internal = internal;
mutex_init(&pcm->open_mutex);
init_waitqueue_head(&pcm->open_wait);
INIT_LIST_HEAD(&pcm->list);
if (id)
    strscpy(pcm->id, id, sizeof(pcm->id));

/* 3. 根據傳入的playback和capture的個數建立PCM流 snd_pcm_str */
err = snd_pcm_new_stream(pcm, SNDRV_PCM_STREAM_PLAYBACK,
                         playback_count);
if (err < 0)
    goto free_pcm;

err = snd_pcm_new_stream(pcm, SNDRV_PCM_STREAM_CAPTURE, capture_count);
if (err < 0)
    goto free_pcm;

/* 4. 建立一個PCM邏輯裝置,建立邏輯裝置,並新增到邏輯裝置連結串列 */
err = snd_device_new(card, SNDRV_DEV_PCM, pcm, internal ? &internal_ops : &ops);
if (err < 0)
        goto free_pcm;

if (rpcm)
        *rpcm = pcm;
return 0;

free_pcm: snd_pcm_free(pcm); return err; } ```

3.2、snd_pcm

```C++ struct snd_pcm { struct snd_card card; struct list_head list; int device; / device number / unsigned int info_flags; unsigned short dev_class; unsigned short dev_subclass; char id[64]; char name[80]; struct snd_pcm_str streams[2]; struct mutex open_mutex; wait_queue_head_t open_wait; void private_data; void (private_free) (struct snd_pcm pcm); bool internal; / pcm is for internal use only / bool nonatomic; / whole PCM operations are in non-atomic context / bool no_device_suspend; / don't invoke device PM suspend /

if IS_ENABLED(CONFIG_SND_PCM_OSS)

struct snd_pcm_oss oss;

endif

}; ```   這裡重要的變數有兩個streams與private_data。streams有兩個,是因為一個指向播放裝置,一個指向錄音裝置。private_data在很多結構裡都可以看到,和麵象物件裡的繼承有點類似,如果將snd_pcm理解為基類的話,private_data指向的就是它的繼承類,也就是真正的實現者。

  list,在pcm.c中有一個全域性變數snd_pcm_devices,將所有的snd_pcm物件連結起來,目的是外部提供一些可供列舉所有裝置的介面,看起來並不怎麼被用到。

  另外還有info_flags、dev_class等變數看起來是為一些特殊裝置預留的,對待一些特殊操作。

```C++ struct snd_pcm_str { int stream; / stream (direction) / struct snd_pcm pcm; / -- substreams -- / unsigned int substream_count; unsigned int substream_opened; struct snd_pcm_substream substream;

if IS_ENABLED(CONFIG_SND_PCM_OSS)

/* -- OSS things -- */
struct snd_pcm_oss_stream oss;

endif

ifdef CONFIG_SND_VERBOSE_PROCFS

struct snd_info_entry *proc_root;

ifdef CONFIG_SND_PCM_XRUN_DEBUG

unsigned int xrun_debug;    /* 0 = disabled, 1 = verbose, 2 = stacktrace */

endif

endif

struct snd_kcontrol *chmap_kctl; /* channel-mapping controls */
struct device dev;

}; ```   snd_pcm_str的主要作用是指向snd_pcm_substream,而snd_pcm_substream可以有多個,這也是snd_pcm_str存在的原因,否則snd_pcm直接指向snd_pcm_substream就可以了。

  這裡的dev是將pcm加入到檔案系統時要用到。包含的資訊,在下面介紹的snd_pcm_new_stream中會看到。

3.3、snd_pcm_new_stream

```C++ / * snd_pcm_new_stream - create a new PCM stream * @pcm: the pcm instance * @stream: the stream direction, SNDRV_PCM_STREAM_XXX * @substream_count: the number of substreams * * Creates a new stream for the pcm. * The corresponding stream on the pcm must have been empty before * calling this, i.e. zero must be given to the argument of * snd_pcm_new(). * * Return: Zero if successful, or a negative error code on failure. / int snd_pcm_new_stream(struct snd_pcm pcm, int stream, int substream_count) { int idx, err; / 3.1 根據傳入的引數,為PCM流(snd_pcm_str)賦值:方向,所屬的PCM,PCM子流的個數 / struct snd_pcm_str pstr = &pcm->streams[stream]; struct snd_pcm_substream substream, *prev;

if IS_ENABLED(CONFIG_SND_PCM_OSS)

mutex_init(&pstr->oss.setup_mutex);

endif

pstr->stream = stream;
pstr->pcm = pcm;
pstr->substream_count = substream_count;
if (!substream_count)
    return 0;

snd_device_initialize(&pstr->dev, pcm->card);
pstr->dev.groups = pcm_dev_attr_groups;
pstr->dev.type = &pcm_dev_type;
dev_set_name(&pstr->dev, "pcmC%iD%i%c", pcm->card->number, pcm->device, stream == SNDRV_PCM_STREAM_PLAYBACK ? 'p' : 'c');

/* proc */
if (!pcm->internal) {
    err = snd_pcm_stream_proc_init(pstr);
    if (err < 0) {
        pcm_err(pcm, "Error in snd_pcm_stream_proc_init\n");
        return err;
    }
}
prev = NULL;
for (idx = 0, prev = NULL; idx < substream_count; idx++) {
    /* 為子流分配空間,賦值(pcm,pcm流,ID, 方向.....) */
    substream = kzalloc(sizeof(*substream), GFP_KERNEL);
    if (!substream)
        return -ENOMEM;
    substream->pcm = pcm;
    substream->pstr = pstr;
    substream->number = idx;
    substream->stream = stream;
    sprintf(substream->name, "subdevice #%i", idx);
    substream->buffer_bytes_max = UINT_MAX;
    /* 新增子流到子流的連結串列 */
    if (prev == NULL)    /* 第一個子流 */
        pstr->substream = substream;
    else
        prev->next = substream;    /* 非第一個子流,新增到前一個子流後部 */
    /* proc */
    if (!pcm->internal) {
        err = snd_pcm_substream_proc_init(substream);
        if (err < 0) {
            pcm_err(pcm, "Error in snd_pcm_stream_proc_init\n");
            if (prev == NULL)
                pstr->substream = NULL;
            else
                prev->next = NULL;
            kfree(substream);
            return err;
        }
    }
    /* 結構體初始化 */
    substream->group = &substream->self_group;
    snd_pcm_group_init(&substream->self_group);
    list_add_tail(&substream->link_list, &substream->self_group.substreams);
    atomic_set(&substream->mmap_count, 0);
    prev = substream;
}
return 0;

} ```   函式引數中的int stream,是一個列舉型別: 

C++ enum { SNDRV_PCM_STREAM_PLAYBACK = 0, SNDRV_PCM_STREAM_CAPTURE, SNDRV_PCM_STREAM_LAST = SNDRV_PCM_STREAM_CAPTURE, };   從snd_device_initialize(&pstr->dev, pcm->card); 開始。dev最終會被傳入device_add函式中,用來構建檔案系統。

C void snd_device_initialize(struct device *dev, struct snd_card *card) { device_initialize(dev); if (card) dev->parent = &card->card_dev; dev->class = sound_class; dev->release = default_release; }   這段函式中可以看到dev->class被設定成sound_class,這個是我們之前提到的檔案放到snd目錄的原因。

3.4、snd_pcm_substream

```C++ struct snd_pcm_substream { struct snd_pcm pcm; struct snd_pcm_str pstr; void private_data; / copied from pcm->private_data / int number; char name[32]; / substream name / int stream; / stream (direction) / struct pm_qos_request latency_pm_qos_req; / pm_qos request / size_t buffer_bytes_max; / limit ring buffer size / struct snd_dma_buffer dma_buffer; size_t dma_max; / -- hardware operations -- / const struct snd_pcm_ops ops; / -- runtime information -- / struct snd_pcm_runtime runtime; / -- timer section -- / struct snd_timer timer; / timer / unsigned timer_running: 1; / time is running / long wait_time; / time in ms for R/W to wait for avail / / -- next substream -- / struct snd_pcm_substream next; / -- linked substreams -- / struct list_head link_list; / linked list member / struct snd_pcm_group self_group; / fake group for non linked substream (with substream lock inside) / struct snd_pcm_group group; / pointer to current group / / -- assigned files -- / int ref_count; atomic_t mmap_count; unsigned int f_flags; void (pcm_release)(struct snd_pcm_substream ); struct pid *pid;

if IS_ENABLED(CONFIG_SND_PCM_OSS)

/* -- OSS things -- */
struct snd_pcm_oss_substream oss;

endif

ifdef CONFIG_SND_VERBOSE_PROCFS

struct snd_info_entry *proc_root;

endif / CONFIG_SND_VERBOSE_PROCFS /

/* misc flags */
unsigned int hw_opened: 1;
unsigned int managed_buffer_alloc:1;

}; ```   snd_pcm_substream的內容有些多,此處只需要重要的進行介紹。

  private_data: 從snd_pcm中的private_data拷貝過來的,指向實現者的結構。

  const struct snd_pcm_ops *ops: 這部分是框架的內容,具體的操作需要實現者的參與,留給實現者的函式指標集。這個和檔案操作的設計策略是一致的。

  struct snd_pcm_runtime *runtime: 讀寫資料的時候由它來控制。到分析讀寫程式碼的時候,會重點關注它。

  struct snd_pcm_substream *next: 將多個snd_pcm_substream物件連結起來,它就是snd_pcm_str指向的連結。

  group: 在使用者空間可以通過SNDRV_PCM_IOCTL_LINK將多個substream連結起來。然後就可以對這些物件進行統一的操作。我沒遇到過具體的應用場景。

3.5、snd_pcm_set_ops

```C++ / * snd_pcm_set_ops - set the PCM operators * @pcm: the pcm instance * @direction: stream direction, SNDRV_PCM_STREAM_XXX * @ops: the operator table * * Sets the given PCM operators to the pcm instance. / void snd_pcm_set_ops(struct snd_pcm pcm, int direction, const struct snd_pcm_ops ops) { struct snd_pcm_str stream = &pcm->streams[direction]; struct snd_pcm_substream *substream;

for (substream = stream->substream; substream != NULL; substream = substream->next)
        substream->ops = ops;

} EXPORT_SYMBOL(snd_pcm_set_ops); ```   此函式是提供給呼叫側使用的。設定的內容可以參考pcm檔案結構簡圖。 

3.6、snd_pcm_dev_register

  在繼續分析snd_pcm_dev_register函式之前需要先介紹一個結構體。struct snd_minor。

C++ struct snd_minor { int type; /* SNDRV_DEVICE_TYPE_XXX */ int card; /* card number */ int device; /* device number */ const struct file_operations *f_ops; /* file operations */ void *private_data; /* private data for f_ops->open */ struct device *dev; /* device for sysfs */ struct snd_card *card_ptr; /* assigned card instance */ };   type: 裝置型別,比如是pcm, control, timer等裝置。

  card_number: 所屬的card。

  device: 當前裝置型別下的裝置編號。

  f_ops: 具體裝置的檔案操作集合。

  private_data: open函式的私有資料。

  card_ptr: 所屬的card。

此結構體是用來儲存當前裝置的上下文資訊,該card下所有邏輯裝置都存在此結構。

```C++ static int snd_pcm_dev_register(struct snd_device device) { / 1、新增pcm結構體到全域性連結串列snd_pcm_devices / int cidx, err; struct snd_pcm_substream substream; struct snd_pcm *pcm;

if (snd_BUG_ON(!device || !device->device_data))
    return -ENXIO;
/* snd_devcie儲存的是snd_pcm物件 */
pcm = device->device_data;

mutex_lock(&register_mutex);
/* snd_pcm物件將被儲存到全域性變數snd_pcm_devices中,用於列舉裝置等操作 */
err = snd_pcm_add(pcm);
if (err)
    goto unlock;
for (cidx = 0; cidx < 2; cidx++) {
    /* 2、確定PCM裝置節點名字 */
    int devtype = -1;
    if (pcm->streams[cidx].substream == NULL)
        continue;
    switch (cidx) {
    case SNDRV_PCM_STREAM_PLAYBACK:
        devtype = SNDRV_DEVICE_TYPE_PCM_PLAYBACK;
        break;
    case SNDRV_PCM_STREAM_CAPTURE:
        devtype = SNDRV_DEVICE_TYPE_PCM_CAPTURE;
        break;
    }
    /* register pcm */
    /* 將裝置新增到檔案系統,將snd_pcm_f_ops傳入,將被設定給snd_minor物件 */
    err = snd_register_device(devtype, pcm->card, pcm->device,
                              &snd_pcm_f_ops[cidx], pcm,
                              &pcm->streams[cidx].dev);
    if (err < 0) {
        list_del_init(&pcm->list);
        goto unlock;
    }

    for (substream = pcm->streams[cidx].substream; substream; substream = substream->next)
        /* 設定CONFIG_SND_PCM_TIMER巨集的時候,會去設定substream的時間 */
        snd_pcm_timer_init(substream);
}

pcm_call_notify(pcm, n_register);

unlock: mutex_unlock(&register_mutex); return err; }

/ * snd_register_device - Register the ALSA device file for the card * @type: the device type, SNDRV_DEVICE_TYPE_XXX * @card: the card instance * @dev: the device index * @f_ops: the file operations * @private_data: user pointer for f_ops->open() * @device: the device to register * * Registers an ALSA device file for the given card. * The operators have to be set in reg parameter. * * Return: Zero if successful, or a negative error code on failure. / int snd_register_device(int type, struct snd_card card, int dev, const struct file_operations f_ops, void private_data, struct device device) { int minor; int err = 0; struct snd_minor preg;

if (snd_BUG_ON(!device))
    return -EINVAL;

preg = kmalloc(sizeof *preg, GFP_KERNEL);
if (preg == NULL)
    return -ENOMEM;
/* 建立一個snd_minor,並新增到全域性結構體 snd_minors */
preg->type = type;
preg->card = card ? card->number : -1;
preg->device = dev;
preg->f_ops = f_ops;
preg->private_data = private_data;
preg->card_ptr = card;
mutex_lock(&sound_mutex);
/* 4、註冊一個裝置節點 */
minor = snd_find_free_minor(type, card, dev);
if (minor < 0) {
    err = minor;
    goto error;
}

preg->dev = device;
device->devt = MKDEV(major, minor);
err = device_add(device);
if (err < 0)
    goto error;

snd_minors[minor] = preg;

error: mutex_unlock(&sound_mutex); if (err < 0) kfree(preg); return err; } ```   當音效卡被註冊時,會註冊所有的邏輯裝置。主要的工作是建立PCM裝置節點具體的流程:

    1、新增pcm結構體到全域性連結串列snd_pcm_devices。

    2、確定PCM裝置節點名字。

    3、建立一個snd_minor,並新增到全域性結構體 snd_minors。

    4、註冊一個裝置節點

  可以看到新增到檔案系統的是播放裝置和錄音裝置,根據snd_pcm_str指向的內容來設定的。程式碼中看到snd_pcm也被定義為SNDRV_DEV_PCM裝置,但是檔案系統中並不會儲存這個型別的裝置。

  snd_pcm_timer_init是在CONFIG_SND_PCM_TIMER巨集被定義的時候,會起作用。

  通過下圖可以幫助你更好的理解各結構直接的邏輯關係。