从零写一个ALSA声卡驱动学习(7)之ALSA内存管理

MIDI (MPU401-UART) Interfa

许多声卡都内建了 MIDI(MPU401-UART)接口。当声卡支持标准的 MPU401-UART 接口时,大多数情况下你可以使用 ALSA 的 MPU401-UART API。该 MPU401-UART API 定义在 <sound/mpu401.h> 文件中。

一些声音芯片具有类似但稍有不同的 MPU401 实现。例如,emu10k1 芯片就有其自身的 MPU401 处理方式。

MIDI 构造函数

要创建一个 rawmidi 对象,请调用 snd_mpu401_uart_new() 函数 :

struct snd_rawmidi *rmidi;
snd_mpu401_uart_new(card, 0, MPU401_HW_MPU401, port, info_flags,
                    irq, &rmidi);

第一个参数是声卡的指针,第二个参数是该组件的索引。你最多可以创建 8 个 rawmidi 设备。

第三个参数是硬件类型,使用 MPU401_HW_XXX。如果不是特殊类型,可以使用 MPU401_HW_MPU401。

第四个参数是 I/O 端口地址。许多向后兼容的 MPU401 接口会使用如 0x330 这样的 I/O 端口地址。或者,它也可能是其自身 PCI I/O 区域的一部分,这取决于芯片的设计。

第五个参数是用于附加信息的位标志(bitflag)。如果上述 I/O 端口地址是 PCI I/O 区域的一部分,那么该 MPU401 的 I/O 端口可能已由驱动程序本身分配(保留)。在这种情况下,应传入位标志 MPU401_INFO_INTEGRATED,这样 mpu401-uart 层会自行分配 I/O 端口。

如果控制器仅支持输入或输出的 MIDI 数据流,分别传入 MPU401_INFO_INPUT 或 MPU401_INFO_OUTPUT 位标志。此时创建的 rawmidi 实例将是单向数据流。

MPU401_INFO_MMIO 位标志用于将访问方式从 inb/outb 更改为 MMIO(通过 readb 和 writeb)。在这种情况下,你需要将内存映射(iomapped)的地址传递给 snd_mpu401_uart_new()。

当设置了 MPU401_INFO_TX_IRQ 标志时,默认的中断处理程序不会检查输出流。驱动程序需要自行调用 snd_mpu401_uart_interrupt_tx() 来在中断处理函数中启动输出流的处理。

如果 MPU-401 接口与声卡上的其他逻辑设备共享中断,应设置 MPU401_INFO_IRQ_HOOK 标志(详见下文)。

通常,端口地址对应的是命令端口,而端口地址 + 1 对应的是数据端口。如果不是这种情况,你可以在之后手动修改 struct snd_mpu401 结构体中的 cport 字段。然而,snd_mpu401_uart_new() 不会显式返回 struct snd_mpu401 的指针。你需要将 rmidi->private_data 强制转换为 struct snd_mpu401 类型:

struct snd_mpu401 *mpu;
mpu = rmidi->private_data;

并根据需要重新设置 cpor:

mpu->cport = my_own_control_port;

第六个参数用于指定要分配的 ISA 中断号。如果不需要分配中断(例如你的代码已经分配了一个共享中断,或者设备本身不使用中断),则传入 -1。对于没有中断的 MPU-401 设备,将改用轮询定时器来代替中断处理。

MIDI Interrupt Handler

当在 snd_mpu401_uart_new() 中分配了中断时,会自动使用独占的 ISA 中断处理程序,因此除了创建 mpu401 相关内容外,你无需执行其他操作。

否则,你需要设置 MPU401_INFO_IRQ_HOOK 标志,并在你自己的中断处理函数中显式调用 snd_mpu401_uart_interrupt(),前提是你已经确定发生了 UART 中断。

在这种情况下,你需要将 snd_mpu401_uart_new() 返回的 rawmidi 对象中的 private_data 作为 snd_mpu401_uart_interrupt() 的第二个参数传入:

snd_mpu401_uart_interrupt(irq, rmidi->private_data, regs);

RawMIDI Interface

Raw MIDI 接口用于那些可以作为字节流访问的硬件 MIDI 端口。它不适用于那些无法直接理解 MIDI 指令的合成器芯片。

ALSA 会负责文件和缓冲区的管理。你所需要做的只是编写一些代码,在缓冲区和硬件之间传输数据。

rawmidi API 定义在 <sound/rawmidi.h> 头文件中。

RawMIDI Constructor

要创建一个 rawmidi 设备,请调用 snd_rawmidi_new() 函数 :

struct snd_rawmidi *rmidi;
err = snd_rawmidi_new(chip->card, "MyMIDI", 0, outs, ins, &rmidi);
if (err < 0)
        return err;
rmidi->private_data = chip;
strcpy(rmidi->name, "My MIDI");
rmidi->info_flags = SNDRV_RAWMIDI_INFO_OUTPUT |
                    SNDRV_RAWMIDI_INFO_INPUT |
                    SNDRV_RAWMIDI_INFO_DUPLEX;

第一个参数是声卡的指针,第二个参数是设备的 ID 字符串。

第三个参数是该组件的索引。你最多可以创建 8 个 rawmidi 设备。

第四个和第五个参数分别是该设备的输出子流和输入子流的数量(一个子流相当于一个 MIDI 端口)。

设置 info_flags 字段用于指定设备的功能特性。如果至少有一个输出端口,则设置 SNDRV_RAWMIDI_INFO_OUTPUT;如果至少有一个输入端口,则设置 SNDRV_RAWMIDI_INFO_INPUT;如果设备能够同时处理输入和输出,则设置 SNDRV_RAWMIDI_INFO_DUPLEX。

在创建好 rawmidi 设备之后,你需要为每个子流设置操作函数(回调函数)。有辅助函数可以用于为设备的所有子流统一设置这些操作函数:

snd_rawmidi_set_ops(rmidi, SNDRV_RAWMIDI_STREAM_OUTPUT, &snd_mymidi_output_ops);
snd_rawmidi_set_ops(rmidi, SNDRV_RAWMIDI_STREAM_INPUT, &snd_mymidi_input_ops);

操作函数通常是这样定义的:

static struct snd_rawmidi_ops snd_mymidi_output_ops = {
        .open =    snd_mymidi_output_open,
        .close =   snd_mymidi_output_close,
        .trigger = snd_mymidi_output_trigger,
};

这些回调函数在 RawMIDI 回调函数 部分有详细说明。 如果存在多个子流,你应该为每个子流指定一个唯一的名称:

struct snd_rawmidi_substream *substream;
list_for_each_entry(substream,
                    &rmidi->streams[SNDRV_RAWMIDI_STREAM_OUTPUT].substreams,
                    list {
        sprintf(substream->name, "My MIDI Port %d", substream->number + 1);
}
/* same for SNDRV_RAWMIDI_STREAM_INPUT */

RawMIDI Callbacks

在所有回调函数中,你为 rawmidi 设备设置的私有数据可以通过 substream->rmidi->private_data 访问。

如果有多个端口,你的回调函数可以通过传递给每个回调的 struct snd_rawmidi_substream 数据来确定端口索引:

struct snd_rawmidi_substream *substream;
int index = substream->number;
  • RawMIDI open callback

static int snd_xxx_open(struct snd_rawmidi_substream *substream);

当一个子流被打开时会调用此函数。你可以在这里初始化硬件,但此时不应开始传输或接收数据。

  • RawMIDI close callback

static int snd_xxx_close(struct snd_rawmidi_substream *substream);

rawmidi 设备的 open 和 close 回调函数是通过互斥锁(mutex)串行化的,并且它们是允许休眠(sleep)的。

  • Rawmidi trigger callback for output substreams

static void snd_xxx_output_trigger(struct snd_rawmidi_substream *substream, int up);

当子流缓冲区中有一些必须传输的数据时,会以 up 参数为非零值调用此函数。
要从缓冲区读取数据,请调用 snd_rawmidi_transmit_peek()。该函数会返回已读取的字节数;如果缓冲区中没有足够的数据,返回值将小于请求的字节数。 在数据成功传输之后,调用 snd_rawmidi_transmit_ack() 来从子流缓冲区中移除这些数据:

unsigned char data;
while (snd_rawmidi_transmit_peek(substream, &data, 1) == 1) {
        if (snd_mychip_try_to_transmit(data))
                snd_rawmidi_transmit_ack(substream, 1);
        else
                break; /* hardware FIFO full */
}

如果你事先知道硬件会接收数据,可以使用 snd_rawmidi_transmit() 函数,它会一次性读取一些数据并将其从缓冲区中移除 :

while (snd_mychip_transmit_possible()) {
        unsigned char data;
        if (snd_rawmidi_transmit(substream, &data, 1) != 1)
                break; /* no more data */
        snd_mychip_transmit(data);
}

如果你事先知道可以接收多少字节的数据,那么在使用 snd_rawmidi_transmit*() 函数时可以使用大于 1 的缓冲区大小。

trigger 回调函数不能发生休眠(sleep)。如果在清空子流缓冲区之前硬件的 FIFO 已满,你必须稍后继续传输数据:要么在中断处理程序中进行,要么如果硬件不支持 MIDI 传输中断,则通过定时器完成。

当需要中止数据传输时,trigger 回调函数会以 up 参数为 0 被调用。

  • RawMIDI trigger callback for input substreams

static void snd_xxx_input_trigger(struct snd_rawmidi_substream *substream, int up);

当以非零的 up 参数调用时,表示启用数据接收;当以 up 参数为 0 调用时,表示禁用数据接收。

trigger 回调函数不能发生休眠(sleep);实际从设备读取数据的操作通常在中断处理程序中完成。

当数据接收被启用时,中断处理程序应对所有接收到的数据调用 snd_rawmidi_receive():

void snd_mychip_midi_interrupt(...)
{
        while (mychip_midi_available()) {
                unsigned char data;
                data = mychip_midi_read();
                snd_rawmidi_receive(substream, &data, 1);
        }
}
  • drain callback

static void snd_xxx_drain(struct snd_rawmidi_substream *substream);

此函数仅用于输出子流。它应等待从子流缓冲区中读取的所有数据都已传输完毕。这样可以确保在关闭设备和卸载驱动时不会丢失数据。

这个回调函数是可选的。如果你没有在 struct snd_rawmidi_ops 结构体中设置 drain 回调,ALSA 会默认等待 50 毫秒来代替。

Miscellaneous Devices

FM OPL3

FM OPL3 仍然在许多芯片中使用(主要是为了向后兼容)。ALSA 也提供了一个良好的 OPL3 FM 控制层。OPL3 API 定义在 <sound/opl3.h> 中。

可以通过 direct-FM API 直接访问 FM 寄存器,该 API 定义在 <sound/asound_fm.h> 中。在 ALSA 的原生模式下,FM 寄存器通过硬件相关设备(Hardware-Dependent Device)的 direct-FM 扩展 API 访问;而在 OSS 兼容模式 下,可以通过 /dev/dmfmX 设备中的 OSS direct-FM 兼容 API 访问 FM 寄存器。

要创建 OPL3 组件,你需要调用两个函数。第一个是用于构造 opl3_t 实例的构造函数:

struct snd_opl3 *opl3;
snd_opl3_create(card, lport, rport, OPL3_HW_OPL3_XXX,
                integrated, &opl3);

第一个参数是声卡指针,第二个参数是左声道端口地址,第三个参数是右声道端口地址。在大多数情况下,右端口地址是左端口地址加 2。

第四个参数是硬件类型。

如果左端口和右端口已经由声卡驱动分配,则第五个参数(integrated)传入非零值;否则,opl3 模块会自行分配指定的端口。

当访问硬件需要使用特殊方法而不是标准的 I/O 访问时,你可以通过 snd_opl3_new() 单独创建一个 opl3 实例:

struct snd_opl3 *opl3;
snd_opl3_new(card, OPL3_HW_OPL3_XXX, &opl3);

然后需要为私有访问函数、私有数据和析构函数设置 command、private_data 和 private_free 字段。l_port 和 r_port 不一定需要设置,但 command 必须被正确设置。你可以通过 opl3->private_data 字段来获取私有数据。

在通过 snd_opl3_new() 创建 opl3 实例之后,调用 snd_opl3_init() 来将芯片初始化到正确的状态。注意:snd_opl3_create() 会在内部自动调用该初始化函数。

如果 opl3 实例创建成功,那么接下来就为该 opl3 实例创建一个 hwdep(硬件相关)设备:

struct snd_hwdep *opl3hwdep;
snd_opl3_hwdep_new(opl3, 0, 1, &opl3hwdep);

第一个参数是你创建的 opl3_t 实例,第二个参数是索引号,通常为 0。

第三个参数是分配给 OPL3 端口的音序器(sequencer)客户端的索引偏移量。如果系统中存在 MPU401-UART,那么这里应传入 1(因为 UART 总是占用 0)。

Hardware-Dependent Devices

有些芯片需要用户空间的访问来进行特殊控制或加载微代码。在这种情况下,你可以创建一个 hwdep(硬件相关)设备。hwdep API 定义在 <sound/hwdep.h> 中。你可以在 OPL3 驱动或 isa/sb/sb16_csp.c 中找到相关示例。

hwdep 实例 的创建是通过 snd_hwdep_new() 函数完成的:

struct snd_hwdep *hw;
snd_hwdep_new(card, "My HWDEP", 0, &hw);

其中第三个参数是索引号。

接着你可以将任意指针值传递给 private_data。如果你分配了私有数据,那么也应当定义一个析构函数。析构函数通过 private_free 字段进行设置:

struct mydata *p = kmalloc(sizeof(*p), GFP_KERNEL);
hw->private_data = p;
hw->private_free = mydata_free;

析构函数的实现如下:

static void mydata_free(struct snd_hwdep *hw)
{
        struct mydata *p = hw->private_data;
        kfree(p);
}

可以为该实例定义任意的文件操作。文件操作函数通过 ops 表来定义。 例如,假设该芯片需要一个 ioctl 操作:

hw->ops.open = mydata_open;
hw->ops.ioctl = mydata_ioctl;
hw->ops.release = mydata_release;

然后根据需要实现相应的回调函数。

IEC958 (S/PDIF)

通常,IEC958 设备的控制项是通过 控制接口(control interface) 来实现的。可以使用宏 SNDRV_CTL_NAME_IEC958()(定义在 <include/asound.h> 中)来生成 IEC958 控制项的名称字符串。

对于 IEC958 状态位,有一些标准控制项。这些控制项使用类型 SNDRV_CTL_ELEM_TYPE_IEC958,元素大小固定为 4 字节的数组(即 value.iec958.status[x])。在实现 info 回调函数时,对于这种类型不需要指定 value 字段(但必须设置 count 字段)。

  • 控制项 "IEC958 Playback Con Mask" 用于返回 IEC958 消费者模式下的状态位掩码。

  • 控制项 "IEC958 Playback Pro Mask" 则返回专业模式下的状态位掩码。

这两个控制项都是只读的。 同时,还定义了 "IEC958 Playback Default" 控制项,用于获取和设置当前默认的 IEC958 状态位。 由于历史原因,Playback Mask 和 Playback Default 控制项的两种形式都可以在 SNDRV_CTL_ELEM_IFACE_PCM 或 SNDRV_CTL_ELEM_IFACE_MIXER 接口上实现。不过,驱动程序应在同一个接口上同时提供 mask 和 default 控制项。 此外,你还可以定义控制开关,用于启用/禁用某些功能,或设置为原始位模式(raw bit mode)。具体实现方式依赖于芯片本身,但控制项名称应采用 "IEC958 xxx" 的形式,最好使用 SNDRV_CTL_NAME_IEC958() 宏来定义名称。 你可以在以下驱动中找到多个实现示例,例如:pci/emu10k1、pci/ice1712 或 pci/cmipci.c。

Buffer and Memory Management

Buffer Types

ALSA 根据总线类型和系统架构,提供了多种不同的缓冲区分配函数。这些函数的 API 设计是一致的。要分配物理连续的页面,可以使用 snd_malloc_xxx_pages() 函数,其中 xxx 表示总线类型。

带有回退机制的页面分配可以通过 snd_dma_alloc_pages_fallback() 实现。该函数会尝试分配指定数量的页面,如果内存不足,它会逐步减少请求的页面数量,直到找到足够的空间,最少会尝试分配一页。

释放页面时,请调用 snd_dma_free_pages() 函数。

通常,ALSA 驱动程序会在模块加载时尝试预先分配并保留一块较大的物理连续内存,以备后用。这种方式称为“预分配(pre-allocation)”。如前所述,在构造 PCM 实例时(以 PCI 总线为例),你可以调用如下函数:

snd_pcm_lib_preallocate_pages_for_all(pcm, SNDRV_DMA_TYPE_DEV,
                                      &pci->dev, size, max);

其中,size 是要预分配的字节数,max 是通过 prealloc 的 proc 文件可设置的最大值。分配器会在给定的大小范围内,尽可能申请一块最大的内存区域。

第二个参数(type)和第三个参数(设备指针)取决于所使用的总线类型。对于普通设备,传入 SNDRV_DMA_TYPE_DEV 类型,并将设备指针(通常与 card->dev 相同)作为第三个参数。

如果要预分配与总线无关的连续缓冲区,可以使用 SNDRV_DMA_TYPE_CONTINUOUS 类型。这种情况下你可以将设备指针设置为 NULL,这是默认模式,表示使用 GFP_KERNEL 标志进行内存分配。如果你需要限制地址范围(例如低地址),则应为设备设置一致性 DMA 掩码位(coherent DMA mask bits),并像普通设备内存分配一样传入设备指针。对于这种类型,如果没有地址限制,传入 NULL 也是允许的。

对于散列聚集(scatter-gather)缓冲区,应使用 SNDRV_DMA_TYPE_DEV_SG 类型,并传入设备指针(参见“非连续缓冲区”章节)。

一旦缓冲区完成预分配,你就可以在 hw_params 回调函数中使用该分配器了:

snd_pcm_lib_malloc_pages(substream, size);

请注意,使用此函数前必须先进行预分配。

但大多数驱动程序使用的是“托管缓冲区分配模式”,而不是手动分配和释放缓冲区。这种方式是通过调用 snd_pcm_set_managed_buffer_all() 来实现的,而不是使用 snd_pcm_lib_preallocate_pages_for_all():

snd_pcm_set_managed_buffer_all(pcm, SNDRV_DMA_TYPE_DEV,
                               &pci->dev, size, max);

这两个函数所传递的参数是相同的。它们的区别在于:在托管模式下,PCM 核心会在调用 PCM 的 hw_params 回调之前,自动调用 snd_pcm_lib_malloc_pages(),并在调用 hw_free 回调之后,自动调用 snd_pcm_lib_free_pages()。因此,驱动程序无需在其回调函数中显式调用这两个函数。

这种方式使得许多驱动可以将 hw_params 和 hw_free 回调设置为 NULL。

External Hardware Buffers

有些芯片具有自己的硬件缓冲区,且不支持从主机内存进行 DMA 传输。在这种情况下,你需要选择以下两种方式之一:

  • 1)直接将音频数据复制/设置到外部硬件缓冲区;

  • 2)创建一个中间缓冲区,并在中断(或更推荐使用的 tasklet)中将数据从中间缓冲区复制/设置到外部硬件缓冲区。

如果外部硬件缓冲区足够大,第一种方式效果很好。这种方法不需要额外的缓冲区,因此更高效。但你需要为数据传输实现 copy 回调函数,并为播放实现 fill_silence 回调函数。不过,这种方式有一个缺点:不支持 mmap 映射。相关示例包括 GUS 的 GF1 PCM 和 emu8000 的 wavetable PCM。

第二种方式允许对缓冲区使用 mmap,但你必须在中断或 tasklet 中处理数据从中间缓冲区到硬件缓冲区的传输。你可以在 vxpocket 驱动中找到示例。

还有一种情况是芯片使用 PCI 内存映射区域作为缓冲区,而不是主机内存。在这种情况下,只有在某些体系结构(如 Intel 架构)上才支持 mmap。对于不支持 mmap 的情况,数据不能像普通方式那样传输,因此你也需要实现 copy 和 fill_silence 回调函数,方式与前述类似。相关示例见于 rme32.c 和 rme96.c。

copy 和 fill_silence 回调函数的具体实现依赖于硬件是支持交错格式(interleaved)还是非交错格式(non-interleaved)的音频样本格式。copy 回调的定义如下(根据是播放还是采集方向略有不同):

static int playback_copy(struct snd_pcm_substream *substream,
             int channel, unsigned long pos,
             struct iov_iter *src, unsigned long count);
static int capture_copy(struct snd_pcm_substream *substream,
             int channel, unsigned long pos,
             struct iov_iter *dst, unsigned long count);

在使用交错格式(interleaved samples)的情况下,第二个参数(channel)不使用。第三个参数(pos)表示偏移位置,以字节为单位。

第四个参数的含义在播放(playback)和采集(capture)模式下是不同的:

  • 在播放模式下,它表示源数据指针;

  • 在采集模式下,它表示目标数据指针。 最后一个参数是要复制的数据字节数。 在这个回调函数中,你的处理方式也会根据播放或采集的方向而不同。

  • 对于播放(playback)情况,你需要将指定数量(count)的数据,从给定的源指针(src)复制到硬件缓冲区中指定偏移位置(pos)。

  • 如果以类似 memcpy 的方式编写代码,拷贝操作如下所示:

my_memcpy_from_iter(my_buffer + pos, src, count);

对于采集(capture)方向,你需要将指定偏移位置(pos)处的硬件缓冲区中的数据,按给定的字节数(count)复制到指定的目标指针(dst)中:

my_memcpy_to_iter(dst, my_buffer + pos, count);

传入的 src 或 dst 是一个指向 struct iov_iter 的指针,它包含了指针和数据大小。应使用定义在 linux/uio.h 中的现有辅助函数来复制或访问这些数据。

细心的读者可能会注意到,这些回调函数的参数单位是字节(bytes),而不像其他回调那样是以帧(frames)为单位。这是为了让编码更简单,就像前面的例子所示;同时也便于统一处理交错(interleaved)与非交错(non-interleaved)两种情况,具体将在下文解释。

对于非交错格式的样本,回调函数的实现会稍微复杂一些。该回调会针对每个通道调用一次,通道号通过第二个参数传入,因此每次传输总共会调用 N 次(N 为通道数)。 其他参数的含义基本与交错格式情况相同。回调函数的目的是从用户空间缓冲区中复制数据,或将数据写入该缓冲区,但仅限于指定的通道。

具体实现细节,请参考 isa/gus/gus_pcm.c 或 pci/rme9652/rme9652.c 等示例文件。

通常在播放(playback)过程中,还会定义另一个名为 fill_silence 的回调函数。它的实现方式与上面提到的 copy 回调类似:

static int silence(struct snd_pcm_substream *substream, int channel,
                   unsigned long pos, unsigned long count);

这些参数的含义与 copy 回调函数中的相同,但 fill_silence 回调中没有缓冲区指针参数。在交错格式(interleaved samples)中,channel 参数也没有意义,这与 copy 回调类似。

fill_silence 回调的作用是:在硬件缓冲区中指定的偏移位置(pos)设置给定数量(count)的静音数据。假设数据格式是带符号(signed),那么静音数据就是 0,这种情况下可以使用类似 memset 的函数来实现,示例如下:

my_memset(my_buffer + pos, 0, count);

对于非交错格式(non-interleaved samples),实现方式会再次变得稍微复杂一些,因为在每次传输中,该回调函数会为每个通道调用一次,总共调用 N 次(N 为通道数)。

例如,可参考 isa/gus/gus_pcm.c 中的实现。

Non-Contiguous Buffers

如果你的硬件支持类似 emu10k1 的页表(page table)机制,或类似 via82xx 的缓冲区描述符(buffer descriptors),那么你可以使用散列聚集(scatter-gather,简称 SG)DMA。ALSA 提供了用于处理 SG 缓冲区的接口,该 API 定义在 <sound/pcm.h> 中。

要创建 SG 缓冲区处理器,可以在 PCM 构造函数中调用 snd_pcm_set_managed_buffer() 或 snd_pcm_set_managed_buffer_all(),并使用 SNDRV_DMA_TYPE_DEV_SG 类型,就像其他 PCI 预分配方式一样。你需要传入 &pci->dev,其中 pci 是表示芯片的 struct pci_dev 指针:

snd_pcm_set_managed_buffer_all(pcm, SNDRV_DMA_TYPE_DEV_SG,
                               &pci->dev, size, max);

struct snd_sg_buf 实例会被创建并赋值给 substream->dma_private。你可以像下面这样对该指针进行类型转换:

struct snd_sg_buf *sgbuf = (struct snd_sg_buf *)substream->dma_private;

然后在调用 snd_pcm_lib_malloc_pages() 时,通用的 SG 缓冲区处理器会分配给定大小的非连续内核页面,并将它们映射为虚拟上的连续内存。虚拟地址通过 runtime->dma_area 访问。

由于该缓冲区在物理上是非连续的,因此物理地址 runtime->dma_addr 会被设置为 0。实际的物理地址表保存在 sgbuf->table 中。你可以通过 snd_pcm_sgbuf_get_addr() 函数,在指定偏移位置获取对应的物理地址。

如果你需要手动释放 SG 缓冲区的数据,只需像平常一样调用标准 API 函数 snd_pcm_lib_free_pages() 即可。

Vmalloc’ed Buffers

你也可以使用通过 vmalloc() 分配的缓冲区,例如用作中间缓冲区。在设置缓冲区预分配类型为 SNDRV_DMA_TYPE_VMALLOC 之后,就可以直接通过标准的 snd_pcm_lib_malloc_pages() 等函数进行分配 :

snd_pcm_set_managed_buffer_all(pcm, SNDRV_DMA_TYPE_VMALLOC,
                               NULL, 0, 0);

此时将 NULL 作为设备指针参数传入,表示将分配默认的内存页(GFP_KERNEL 和 GFP_HIGHMEM)。

另外需要注意的是,这里传入的大小(size)和最大大小(max size)参数都是 0。由于每次调用 vmalloc 都应能成功,因此不需要像其他连续内存页那样预先分配缓冲区。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值