从零写一个ALSA声卡驱动学习(4)

前言:

大家好,我是txp,今天我继续来ALSA声卡驱动内容!

DMA 缓冲区信息

DMA 缓冲区由以下四个字段定义:dma_area、dma_addr、dma_bytes 和 dma_privat:

  • dma_area 是缓冲区的指针(逻辑地址),你可以通过它进行 memcpy() 读写操作。

  • dma_addr 是缓冲区的物理地址,仅当缓冲区是线性内存时才需要设置。

  • dma_bytes 表示缓冲区的大小(以字节为单位)。

  • dma_private 用于 ALSA 的 DMA 分配器。

如果你使用了“托管缓冲区分配模式”或者使用标准 API 函数 snd_pcm_lib_malloc_pages() 来分配缓冲区,那么这些字段会由 ALSA 的中间层自动设置,你不应该自行修改。你可以读取这些字段,但不要写入它们。

另一方面,如果你希望自行分配缓冲区,那么就需要在 hw_params 回调中进行管理。其中,dma_bytes 是必须设置的;若你的驱动支持 mmap,则还必须设置 dma_area。如果驱动不支持 mmap,这个字段可以不设置。dma_addr 是可选的字段,dma_private 也可以根据你的需求自定义使用。

运行状态

运行状态可以通过 runtime->status 获取,该字段是一个指向 struct snd_pcm_mmap_status 结构的指针。例如,你可以通过 runtime->status->hw_ptr 获取当前 DMA 的硬件指针。

DMA 的应用层指针可以通过 runtime->control 获取,它指向一个 struct snd_pcm_mmap_control 结构体。不过,不建议你直接访问这个值。

私有数据

你可以为每个子流(substream)分配一个数据结构,并将其存储在 runtime->private_data 中。通常,这一步是在 PCM 的 open 回调函数中完成的。

需要注意的是,不要将它与 pcm->private_data 混淆。pcm->private_data 通常在 PCM 设备创建时就被静态地设置为指向芯片实例;而 runtime->private_data 则是指向在 PCM open 回调中动态创建的数据结构:

static int snd_xxx_open(struct snd_pcm_substream *substream)
{
        struct my_pcm_data *data;
        ....
        data = kmalloc(sizeof(*data), GFP_KERNEL);
        substream->runtime->private_data = data;
        ....
}

所分配的对象必须在 close 回调中释放。

操作函数:

那么好了,接下来我将详细介绍每个 PCM 回调函数(ops)。通常情况下,每个回调函数在成功时必须返回 0,如果失败,则返回一个负数的错误码,例如 -EINVAL。要选择合适的错误码,建议参考内核中其他模块在处理相同类型请求失败时返回的值。

每个回调函数至少会传入一个参数,该参数是一个指向 struct snd_pcm_substream 的指针。若要从该 substream 实例中获取芯片相关的数据结构,可以使用以下宏:

int xxx(...) {
        struct mychip *chip = snd_pcm_substream_chip(substream);
        ....
}

该宏会读取 substream->private_data,它是 pcm->private_data 的一个副本。如果你需要为每个 PCM 子流分配不同的数据结构,可以覆盖 substream->private_data。

例如,cmi8330 驱动就为播放(playback)和录音(capture)方向分配了不同的 private_data,因为它在两个方向上使用了不同的编解码器(分别兼容 SB 和 AD)。

PCM 打开回调函数

static int snd_xxx_open(struct snd_pcm_substream *substream);

当一个 PCM 子流被打开时,会调用该函数。

在这个回调中,至少需要初始化 runtime->hw 结构体。通常可以像下面这样进行初始化:

static int snd_xxx_open(struct snd_pcm_substream *substream)
{
        struct mychip *chip = snd_pcm_substream_chip(substream);
        struct snd_pcm_runtime *runtime = substream->runtime;

        runtime->hw = snd_mychip_playback_hw;
        return 0;
}

其中,snd_mychip_playback_hw 是预先定义好的硬件描述信息。

你可以在这个回调中分配私有数据,具体做法参考前文的“私有数据(Private Data)”章节。

如果硬件配置有更多限制条件,也应该在这里设置相应的硬件约束。详细内容请参见“约束(Constraints)”章节。

close callback

static int snd_xxx_close(struct snd_pcm_substream *substream);

显然,该函数会在 PCM 子流被关闭时调用。 在 open 回调中为 PCM 子流分配的任何私有实例,都应在此处进行释放。

ioctl callback

该函数用于处理对 PCM 的特殊 ioctl 调用。但通常你可以将其设为 NULL,此时 PCM 核心会调用通用的 ioctl 回调函数 snd_pcm_lib_ioctl()。

如果你需要处理一些特殊的通道信息设置或复位流程,可以在这里传入你自定义的回调函数。

hw_params callback

static int snd_xxx_hw_params(struct snd_pcm_substream *substream,
                             struct snd_pcm_hw_params *hw_params);

当应用程序设置硬件参数(hw_params)时会调用该回调函数,也就是说,当缓冲区大小、period 大小、数据格式等被定义后,此函数会被调用一次。

许多硬件相关的初始化操作都应在这个回调中完成,包括缓冲区的分配。

要初始化的参数可以通过 params_xxx() 一系列宏来获取。

如果你为子流选择了托管缓冲区分配模式,那么在这个回调被调用之前,缓冲区已经被分配好了。或者,你也可以调用下面的辅助函数来分配缓冲区:

snd_pcm_lib_malloc_pages(substream, params_buffer_bytes(hw_params));

snd_pcm_lib_malloc_pages() 仅在 DMA 缓冲区已预先分配的情况下可用。更多详细信息请参考“缓冲区类型(Buffer Types)”章节。

需要注意的是,hw_params 回调和 prepare 回调在一次初始化过程中可能会被多次调用。例如,在 OSS 仿真层中,每次通过 ioctl 修改参数时,可能都会调用这些回调。

因此,你需要特别小心,避免重复分配同一个缓冲区,否则会导致内存泄漏!不过,上述的辅助函数可以安全地多次调用:如果之前已经分配过缓冲区,它会自动释放旧的缓冲区。

另一个需要注意的点是:该回调函数默认是非原子(可以调度的),也就是说,如果没有设置 nonatomic 标志,它可以调用 sleep 或 mutex 等调度相关的函数。而 trigger 回调则是原子的(不可调度),在其中不能使用互斥锁或其他调度相关的操作。关于这一点的详细说明,请参阅“原子性(Atomicity)”小节。

hw_free callback

static int snd_xxx_hw_free(struct snd_pcm_substream *substream);

该回调函数用于释放通过 hw_params 分配的资源。

在 close 回调被调用之前,系统一定会先调用此函数。同时,该回调也可能被调用多次,因此你需要跟踪各项资源是否已经释放,避免重复释放。

如果你为 PCM 子流选择了托管缓冲区分配模式,那么在此回调执行完之后,所分配的 PCM 缓冲区会被自动释放。否则,你需要手动释放缓冲区。

通常情况下,如果缓冲区是从预分配池中分配的,你可以使用标准的 API 函数 snd_pcm_lib_malloc_pages() 来释放它,例如:

snd_pcm_lib_free_pages(substream);

prepare callback

static int snd_xxx_prepare(struct snd_pcm_substream *substream);

当 PCM 被“准备好”时,会调用该回调函数。在这里你可以设置格式类型、采样率等参数。

它与 hw_params 的区别在于:每次调用 snd_pcm_prepare()(例如在 buffer underrun 恢复之后)时,都会触发 prepare 回调。

注意:此回调是非原子的,因此你可以在其中安全地使用调度相关的函数(如 sleep、mutex 等)。

在这个回调以及后续的回调函数中,你可以通过运行时结构体 substream->runtime 来获取参数值。例如,当前的采样率、数据格式和通道数可以通过 runtime->rate、runtime->format 和 runtime->channels 获取。已分配缓冲区的物理地址存储在 runtime->dma_area 中,缓冲区大小和 period 大小则分别存储在 runtime->buffer_size 和 runtime->period_size 中。

请注意,该回调在每次设置过程中可能会被多次调用,因此也需要谨慎处理。

trigger callback

static int snd_xxx_trigger(struct snd_pcm_substream *substream, int cmd);

该回调函数会在 PCM 开始、停止或暂停时被调用。

具体的操作类型通过第二个参数指定,该参数的取值为 <sound/pcm.h> 中定义的 SNDRV_PCM_TRIGGER_XXX。在这个回调中,至少需要处理 START 和 STOP 命令。

switch (cmd) {
case SNDRV_PCM_TRIGGER_START:
        /* do something to start the PCM engine */
        break;
case SNDRV_PCM_TRIGGER_STOP:
        /* do something to stop the PCM engine */
        break;
default:
        return -EINVAL;
}

当 PCM 支持暂停操作(在硬件描述表的 info 字段中指定)时,必须在该回调中处理 PAUSE_PUSH 和 PAUSE_RELEASE 命令。前者用于暂停 PCM,后者用于重新开始 PCM。

当 PCM 支持挂起/恢复操作时,无论是完整支持还是部分支持,也必须处理 SUSPEND 和 RESUME 命令。这些命令会在电源管理状态发生变化时发出。显然,SUSPEND 和 RESUME 命令分别对应于挂起和恢复 PCM 子流,通常它们的行为与 STOP 和 START 命令是相同的。详细说明请参考“电源管理(Power Management)”章节。

如前所述,该回调默认是原子的(除非设置了 nonatomic 标志),因此你不能在其中调用可能引发休眠的函数。

trigger 回调应尽可能保持简洁,仅用于真正启动或停止 DMA,而其他操作应提前在 hw_params 和 prepare 回调中正确完成初始化。

sync_stop callback:

static int snd_xxx_sync_stop(struct snd_pcm_substream *substream);

这个回调函数是可选的,可以设置为 NULL。它会在 PCM 核心停止音频流之后、执行 prepare、hw_params 或 hw_free 等操作之前被调用。由于此时中断处理程序(IRQ handler)可能仍在处理中,因此在进入下一步之前,需要等待其处理完毕;否则可能会因为资源冲突或访问已释放资源而导致系统崩溃。

典型的做法是在这里调用类似 synchronize_irq() 这样的同步函数。

对于大多数只需要调用 synchronize_irq() 的驱动,还有一个更简单的做法:在不实现 sync_stop 回调(即设置为 NULL)的前提下,驱动在申请中断后将返回的中断号赋值给 card->sync_irq 字段即可。这样 PCM 核心就会在适当的时候自动调用 synchronize_irq()。

如果 IRQ 处理函数是在声卡析构函数中释放的,那就不需要手动清除 card->sync_irq,因为声卡本身已经被释放。

所以,通常你只需在驱动代码中添加一行,将中断号赋值给 card->sync_irq 即可,前提是驱动不会重新申请中断。如果驱动会在运行中动态释放并重新申请中断(例如用于挂起/恢复),那就需要在适当的时机清除并重新设置 card->sync_irq。

pointer callback

static snd_pcm_uframes_t snd_xxx_pointer(struct snd_pcm_substream *substream)

当 PCM 中间层查询当前硬件在缓冲区中的位置时,会调用此回调函数。该位置必须以帧(frame)为单位返回,取值范围为 0 到 buffer_size - 1。

这个回调通常由 PCM 中间层的缓冲区更新例程调用,而该例程是在中断处理函数调用 snd_pcm_period_elapsed() 时触发的。随后,PCM 中间层会更新当前位置、计算可用空间,并唤醒处于等待状态的 poll 线程等。

该回调默认也是原子的。

copy and fill_silence ops

这些回调函数不是必须的,在大多数情况下可以省略。它们主要用于当硬件缓冲区不能位于常规内存空间时的特殊场景。

有些芯片具有无法映射的硬件专用缓冲区,此时你需要手动将数据从内存缓冲区传输到硬件缓冲区。

或者,当缓冲区在物理内存和虚拟内存中都不是连续的情况下,也必须实现这两个回调函数。

如果定义了这两个回调函数,数据拷贝和静音填充操作将由它们完成。具体细节将在后续的“缓冲区与内存管理”章节中介绍。

ack callback

这个回调函数同样不是必须的。当在读写操作中更新 appl_ptr(应用层指针)时,会调用该回调函数。

某些驱动,例如 emu10k1-fx 和 cs46xx,需要追踪当前的 appl_ptr 以管理其内部缓冲区,这个回调函数仅在这种情况下有用。

该回调函数可以返回 0 或负数错误码。如果返回值为 -EPIPE,PCM 核心会将其视为缓冲区溢出(XRUN),并自动将状态更改为 SNDRV_PCM_STATE_XRUN。 该回调默认是原子的。

page callback

这个回调函数也是可选的。mmap 操作会调用此回调来获取页错误(page fault)对应的内存地址。

对于标准的 SG 缓冲区或 vmalloc 缓冲区,不需要实现这个回调,因此它很少被使用。

mmap callback

这是另一个用于控制 mmap 行为的可选回调函数。

如果定义了该函数,PCM 核心在执行内存映射(memory-mapped)某个页面时,会调用此回调,而不是使用默认的辅助函数。

如果由于架构或设备的特殊需求需要进行特殊处理,你可以在这里自行实现所需的全部逻辑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值