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

PCM 接口:

ALSA 的 PCM 中间层功能非常强大,驱动程序只需实现访问其硬件的底层函数即可。 要访问 PCM 层,首先需要包含头文件 。此外,如果需要访问与 hw_param 相关的一些函数,还可能需要包含

每个声卡设备最多可以有四个 PCM 实例。每个 PCM 实例对应一个 PCM 设备文件。这个实例数量的限制仅来自于 Linux 设备号的位数限制。一旦使用 64 位设备号,就可以支持更多的 PCM 实例。

一个 PCM 实例由 PCM 播放和录音流组成,每个 PCM 流又由一个或多个 PCM 子流(substream)构成。一些声卡支持多个播放功能。例如,emu10k1 声卡拥有多达 32 路立体声 PCM 播放子流。在这种情况下,每次打开设备时,通常会自动选择并打开一个空闲的子流。而当一个子流已被打开,后续打开操作将根据文件打开模式,要么阻塞等待,要么返回 EAGAIN 错误。但你在驱动中无需关心这些细节,PCM 中间层会自动处理这些工作。

下面的示例代码不包含任何硬件访问的例程,仅展示了如何构建 PCM 接口的框架结构 :

#include 
....

/* hardware definition */
static struct snd_pcm_hardware snd_mychip_playback_hw = {
        .info = (SNDRV_PCM_INFO_MMAP |
                 SNDRV_PCM_INFO_INTERLEAVED |
                 SNDRV_PCM_INFO_BLOCK_TRANSFER |
                 SNDRV_PCM_INFO_MMAP_VALID),
        .formats =          SNDRV_PCM_FMTBIT_S16_LE,
        .rates =            SNDRV_PCM_RATE_8000_48000,
        .rate_min =         8000,
        .rate_max =         48000,
        .channels_min =     2,
        .channels_max =     2,
        .buffer_bytes_max = 32768,
        .period_bytes_min = 4096,
        .period_bytes_max = 32768,
        .periods_min =      1,
        .periods_max =      1024,
};

/* hardware definition */
static struct snd_pcm_hardware snd_mychip_capture_hw = {
        .info = (SNDRV_PCM_INFO_MMAP |
                 SNDRV_PCM_INFO_INTERLEAVED |
                 SNDRV_PCM_INFO_BLOCK_TRANSFER |
                 SNDRV_PCM_INFO_MMAP_VALID),
        .formats =          SNDRV_PCM_FMTBIT_S16_LE,
        .rates =            SNDRV_PCM_RATE_8000_48000,
        .rate_min =         8000,
        .rate_max =         48000,
        .channels_min =     2,
        .channels_max =     2,
        .buffer_bytes_max = 32768,
        .period_bytes_min = 4096,
        .period_bytes_max = 32768,
        .periods_min =      1,
        .periods_max =      1024,
};

/* open callback */
static int snd_mychip_playback_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;
        /* more hardware-initialization will be done here */
        ....
        return 0;
}

/* close callback */
static int snd_mychip_playback_close(struct snd_pcm_substream *substream)
{
        struct mychip *chip = snd_pcm_substream_chip(substream);
        /* the hardware-specific codes will be here */
        ....
        return 0;

}

/* open callback */
static int snd_mychip_capture_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_capture_hw;
        /* more hardware-initialization will be done here */
        ....
        return 0;
}

/* close callback */
static int snd_mychip_capture_close(struct snd_pcm_substream *substream)
{
        struct mychip *chip = snd_pcm_substream_chip(substream);
        /* the hardware-specific codes will be here */
        ....
        return 0;
}

/* hw_params callback */
static int snd_mychip_pcm_hw_params(struct snd_pcm_substream *substream,
                             struct snd_pcm_hw_params *hw_params)
{
        /* the hardware-specific codes will be here */
        ....
        return 0;
}

/* hw_free callback */
static int snd_mychip_pcm_hw_free(struct snd_pcm_substream *substream)
{
        /* the hardware-specific codes will be here */
        ....
        return 0;
}

/* prepare callback */
static int snd_mychip_pcm_prepare(struct snd_pcm_substream *substream)
{
        struct mychip *chip = snd_pcm_substream_chip(substream);
        struct snd_pcm_runtime *runtime = substream->runtime;

        /* set up the hardware with the current configuration
         * for example...
         */
        mychip_set_sample_format(chip, runtime->format);
        mychip_set_sample_rate(chip, runtime->rate);
        mychip_set_channels(chip, runtime->channels);
        mychip_set_dma_setup(chip, runtime->dma_addr,
                             chip->buffer_size,
                             chip->period_size);
        return 0;
}

/* trigger callback */
static int snd_mychip_pcm_trigger(struct snd_pcm_substream *substream,
                                  int cmd)
{
        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;
        }
}

/* pointer callback */
static snd_pcm_uframes_t
snd_mychip_pcm_pointer(struct snd_pcm_substream *substream)
{
        struct mychip *chip = snd_pcm_substream_chip(substream);
        unsigned int current_ptr;

        /* get the current hardware pointer */
        current_ptr = mychip_get_hw_pointer(chip);
        return current_ptr;
}

/* operators */
static struct snd_pcm_ops snd_mychip_playback_ops = {
        .open =        snd_mychip_playback_open,
        .close =       snd_mychip_playback_close,
        .hw_params =   snd_mychip_pcm_hw_params,
        .hw_free =     snd_mychip_pcm_hw_free,
        .prepare =     snd_mychip_pcm_prepare,
        .trigger =     snd_mychip_pcm_trigger,
        .pointer =     snd_mychip_pcm_pointer,
};

/* operators */
static struct snd_pcm_ops snd_mychip_capture_ops = {
        .open =        snd_mychip_capture_open,
        .close =       snd_mychip_capture_close,
        .hw_params =   snd_mychip_pcm_hw_params,
        .hw_free =     snd_mychip_pcm_hw_free,
        .prepare =     snd_mychip_pcm_prepare,
        .trigger =     snd_mychip_pcm_trigger,
        .pointer =     snd_mychip_pcm_pointer,
};

/*
 *  definitions of capture are omitted here...
 */

/* create a pcm device */
static int snd_mychip_new_pcm(struct mychip *chip)
{
        struct snd_pcm *pcm;
        int err;

        err = snd_pcm_new(chip->card, "My Chip", 0, 1, 1, &pcm);
        if (err < 0)
                return err;
        pcm->private_data = chip;
        strcpy(pcm->name, "My Chip");
        chip->pcm = pcm;
        /* set operators */
        snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK,
                        &snd_mychip_playback_ops);
        snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE,
                        &snd_mychip_capture_ops);
        /* pre-allocation of buffers */
        /* NOTE: this may fail */
        snd_pcm_set_managed_buffer_all(pcm, SNDRV_DMA_TYPE_DEV,
                                       &chip->pci->dev,
                                       64*1024, 64*1024);
        return 0;
}

PCM 构造函数

PCM 实例是通过 snd_pcm_new() 函数分配的。最好为 PCM 创建一个构造函数,即:

static int snd_mychip_new_pcm(struct mychip *chip)
{
        struct snd_pcm *pcm;
        int err;

        err = snd_pcm_new(chip->card, "My Chip", 0, 1, 1, &pcm);
        if (err < 0)
                return err;
        pcm->private_data = chip;
        strcpy(pcm->name, "My Chip");
        chip->pcm = pcm;
        ...
        return 0;
}

snd_pcm_new() 函数有六个参数。第一个参数是该 PCM 所属的声卡指针,第二个参数是该 PCM 的 ID 字符串。

第三个参数(如上例中的 0)是该新 PCM 的索引,从 0 开始计数。如果你创建多个 PCM 实例,需要为该参数指定不同的数值。例如,第二个 PCM 设备的 index 应为 1。 第四和第五个参数分别表示播放和录音的子流(substream)数量。这里两个参数都使用了 1。如果不需要播放或录音子流,则将对应参数设为 0。

如果芯片支持多个播放或录音通道,你可以传入更大的数值,但在 open/close 等回调函数中必须妥善处理这些子流。当你需要知道当前操作的是哪个子流时,可以通过回调函数中传入的 struct snd_pcm_substream 数据来获取,方式如下:

struct snd_pcm_substream *substream;
int index = substream->number;

在创建 PCM 之后,你需要为每个 PCM 流设置操作函数(operators):

snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK,
                &snd_mychip_playback_ops);
snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE,
                &snd_mychip_capture_ops);

操作函数通常定义如下:

static struct snd_pcm_ops snd_mychip_playback_ops = {
        .open =        snd_mychip_pcm_open,
        .close =       snd_mychip_pcm_close,
        .hw_params =   snd_mychip_pcm_hw_params,
        .hw_free =     snd_mychip_pcm_hw_free,
        .prepare =     snd_mychip_pcm_prepare,
        .trigger =     snd_mychip_pcm_trigger,
        .pointer =     snd_mychip_pcm_pointer,
};

所有回调函数的说明都在“操作函数(Operators)”小节中。

在设置完操作函数之后,你可能还希望预分配缓冲区,并设置托管的内存分配模式。为此,只需调用以下函数:

snd_pcm_set_managed_buffer_all(pcm, SNDRV_DMA_TYPE_DEV,
                               &chip->pci->dev,
                               64*1024, 64*1024);

默认情况下,该函数会分配最多 64KB 的缓冲区。缓冲区管理的详细内容将在后续的“缓冲区与内存管理”章节中进行说明。

此外,你还可以通过 pcm->info_flags 为该 PCM 设置一些附加信息。可用的取值在 中以 SNDRV_PCM_INFO_XXX 形式定义,这些标志用于描述硬件特性(将在后文详细介绍)。

当你的声卡芯片仅支持半双工时,可以像下面这样设置:

pcm->info_flags = SNDRV_PCM_INFO_HALF_DUPLEX;

那析构函数呢?

PCM 实例通常不需要专门的析构函数。因为 PCM 设备会由中间层代码自动释放,所以你无需显式调用析构函数。

只有在你在驱动内部创建了特殊的数据结构,并且需要手动释放它们时,才需要定义析构函数。在这种情况下,可以将析构函数设置为 pcm->private_free:

static void mychip_pcm_free(struct snd_pcm *pcm)
{
        struct mychip *chip = snd_pcm_chip(pcm);
        /* free your own data */
        kfree(chip->my_private_pcm_data);
        /* do what you like else */
        ....
}

static int snd_mychip_new_pcm(struct mychip *chip)
{
        struct snd_pcm *pcm;
        ....
        /* allocate your own data */
        chip->my_private_pcm_data = kmalloc(...);
        /* set the destructor */
        pcm->private_data = chip;
        pcm->private_free = mychip_pcm_free;
        ....
}

运行时指针 —— PCM 信息的宝库

当 PCM 子流(substream)被打开时,会分配一个 PCM 运行时实例(runtime instance),并将其赋值给该子流。这个指针可以通过 substream->runtime 访问。

该 runtime 指针保存了大部分用于控制 PCM 的关键信息:例如 hw_params 和 sw_params 的副本配置、缓冲区指针、mmap 映射记录、自旋锁等。

运行时实例的定义可以在 中找到。以下是该头文件中的相关部分内容:

struct _snd_pcm_runtime {
        /* -- Status -- */
        struct snd_pcm_substream *trigger_master;
        snd_timestamp_t trigger_tstamp;       /* trigger timestamp */
        int overrange;
        snd_pcm_uframes_t avail_max;
        snd_pcm_uframes_t hw_ptr_base;        /* Position at buffer restart */
        snd_pcm_uframes_t hw_ptr_interrupt; /* Position at interrupt time*/

        /* -- HW params -- */
        snd_pcm_access_t access;      /* access mode */
        snd_pcm_format_t format;      /* SNDRV_PCM_FORMAT_* */
        snd_pcm_subformat_t subformat;        /* subformat */
        unsigned int rate;            /* rate in Hz */
        unsigned int channels;                /* channels */
        snd_pcm_uframes_t period_size;        /* period size */
        unsigned int periods;         /* periods */
        snd_pcm_uframes_t buffer_size;        /* buffer size */
        unsigned int tick_time;               /* tick time */
        snd_pcm_uframes_t min_align;  /* Min alignment for the format */
        size_t byte_align;
        unsigned int frame_bits;
        unsigned int sample_bits;
        unsigned int info;
        unsigned int rate_num;
        unsigned int rate_den;

        /* -- SW params -- */
        struct timespec tstamp_mode;  /* mmap timestamp is updated */
        unsigned int period_step;
        unsigned int sleep_min;               /* min ticks to sleep */
        snd_pcm_uframes_t start_threshold;
        /*
         * The following two thresholds alleviate playback buffer underruns; when
         * hw_avail drops below the threshold, the respective action is triggered:
         */
        snd_pcm_uframes_t stop_threshold;     /* - stop playback */
        snd_pcm_uframes_t silence_threshold;  /* - pre-fill buffer with silence */
        snd_pcm_uframes_t silence_size;       /* max size of silence pre-fill; when >= boundary,
                                               * fill played area with silence immediately */
        snd_pcm_uframes_t boundary;   /* pointers wrap point */

        /* internal data of auto-silencer */
        snd_pcm_uframes_t silence_start; /* starting pointer to silence area */
        snd_pcm_uframes_t silence_filled; /* size filled with silence */

        snd_pcm_sync_id_t sync;               /* hardware synchronization ID */

        /* -- mmap -- */
        volatile struct snd_pcm_mmap_status *status;
        volatile struct snd_pcm_mmap_control *control;
        atomic_t mmap_count;

        /* -- locking / scheduling -- */
        spinlock_t lock;
        wait_queue_head_t sleep;
        struct timer_list tick_timer;
        struct fasync_struct *fasync;

        /* -- private section -- */
        void *private_data;
        void (*private_free)(struct snd_pcm_runtime *runtime);

        /* -- hardware description -- */
        struct snd_pcm_hardware hw;
        struct snd_pcm_hw_constraints hw_constraints;

        /* -- timer -- */
        unsigned int timer_resolution;        /* timer resolution */

        /* -- DMA -- */
        unsigned char *dma_area;      /* DMA area */
        dma_addr_t dma_addr;          /* physical bus address (not accessible from main CPU) */
        size_t dma_bytes;             /* size of DMA area */

        struct snd_dma_buffer *dma_buffer_p;  /* allocated buffer */

#if defined(CONFIG_SND_PCM_OSS) || defined(CONFIG_SND_PCM_OSS_MODULE)
        /* -- OSS things -- */
        struct snd_pcm_oss_runtime oss;
#endif
};

对于每个声卡驱动中的操作函数(回调函数),大多数运行时结构中的字段应被视为只读。它们通常由 PCM 中间层负责修改和更新。

例外情况包括:硬件描述信息(hw)、DMA 缓冲区信息以及私有数据(private_data),这些字段是可以由驱动自行设置的。

此外,如果你使用的是标准的托管缓冲区分配模式(managed buffer allocation mode),则无需手动设置 DMA 缓冲区的信息。

在接下来的章节中,将对这些重要字段进行说明。

硬件描述

硬件描述符(struct snd_pcm_hardware)用于定义底层硬件的基本配置信息。最重要的是,你需要在 PCM 的 open 回调函数中设置这个描述符。

需要注意的是,运行时实例中保存的是描述符的一个副本,而不是对原始描述符的指针。也就是说,在 open 回调中,你可以根据需要修改这个副本(即 runtime->hw)。

例如,如果某些芯片型号上最多只支持 1 个通道,你仍然可以使用相同的硬件描述结构体,只需在 open 回调中修改 channels_max 即可:

struct snd_pcm_runtime *runtime = substream->runtime;
...
runtime->hw = snd_mychip_playback_hw; /* common definition */
if (chip->model == VERY_OLD_ONE)
        runtime->hw.channels_max = 1;

通常,你会像下面这样定义一个硬件描述符:

static struct snd_pcm_hardware snd_mychip_playback_hw = {
        .info = (SNDRV_PCM_INFO_MMAP |
                 SNDRV_PCM_INFO_INTERLEAVED |
                 SNDRV_PCM_INFO_BLOCK_TRANSFER |
                 SNDRV_PCM_INFO_MMAP_VALID),
        .formats =          SNDRV_PCM_FMTBIT_S16_LE,
        .rates =            SNDRV_PCM_RATE_8000_48000,
        .rate_min =         8000,
        .rate_max =         48000,
        .channels_min =     2,
        .channels_max =     2,
        .buffer_bytes_max = 32768,
        .period_bytes_min = 4096,
        .period_bytes_max = 32768,
        .periods_min =      1,
        .periods_max =      1024,
};
  • info 字段用于指定该 PCM 的类型和功能能力。其位标志定义在 中,形式为 SNDRV_PCM_INFO_XXX。在此字段中,你至少需要指定是否支持 mmap 以及支持哪种交错格式。如果硬件支持 mmap,则需要添加 SNDRV_PCM_INFO_MMAP 标志。如果硬件支持交错格式或非交错格式,则分别需要设置 SNDRV_PCM_INFO_INTERLEAVED 或 SNDRV_PCM_INFO_NONINTERLEAVED 标志;如果两者都支持,也可以同时设置。在上述示例中,还设置了 MMAP_VALID 和 BLOCK_TRANSFER,用于支持 OSS 的 mmap 模式。通常,这两个标志会一起设置。当然,只有在硬件真正支持 mmap 的情况下,才应设置 MMAP_VALID。其他可选的标志还包括 SNDRV_PCM_INFO_PAUSE 和 SNDRV_PCM_INFO_RESUME。PAUSE 标志表示 PCM 支持“暂停”操作,而 RESUME 标志表示 PCM 支持完整的“挂起/恢复”操作。如果设置了 PAUSE 标志,那么后续的 trigger 回调函数中必须处理对应的暂停/恢复(pause push/release)命令。即使没有设置 RESUME 标志,也可以定义挂起/恢复的 trigger 命令。详细说明见后文的“电源管理”章节。当 PCM 子流之间可以同步(通常是播放和录音流的同步启动/停止)时,也可以添加 SNDRV_PCM_INFO_SYNC_START 标志。在这种情况下,你需要在 trigger 回调中检查 PCM 子流的链表,这部分内容将在后续章节中介绍。

  • formats 字段包含了所支持的音频数据格式的位标志(SNDRV_PCM_FMTBIT_XXX)。如果硬件支持多种格式,则需要使用按位或(|)的方式将所有支持的格式组合起来。在上面的示例中,指定的是 16 位有符号小端格式(signed 16bit little-endian)。

  • rates 字段包含所支持的采样率的位标志(SNDRV_PCM_RATE_XXX)。如果芯片支持连续的采样率范围,还需要额外添加 SNDRV_PCM_RATE_CONTINUOUS 标志。预定义的采样率位标志只覆盖常见的标准采样率。如果你的芯片支持一些非常规的采样率,还需要添加 SNDRV_PCM_RATE_KNOT 标志,并手动设置相应的硬件约束(后文会详细介绍)。

  • rate_min 和 rate_max 用于定义支持的最小和最大采样率。它们的取值应与 rates 位标志相对应。

  • 正如你可能已经猜到的,channels_min 和 channels_max 用于定义支持的最小和最大通道数。

  • buffer_bytes_max 定义了缓冲区的最大字节数。没有 buffer_bytes_min 字段,因为它可以通过最小的 period 大小和最小的 period 数量计算得出。与此同时,period_bytes_min 和 period_bytes_max 分别定义了 period(周期)大小的最小值和最大值(以字节为单位);而 periods_min 和 periods_max 则定义了缓冲区中 period 数量的最小值和最大值。“period” 这个术语在 OSS(Open Sound System)中相当于“碎片(fragment)”。period 决定了何时触发一次 PCM 中断,这通常取决于硬件。一般来说,period 越小,中断次数越多,从而可以更及时地填充或清空缓冲区。在录音(capture)方向上,period 大小决定了输入延迟;而在播放(playback)方向上,整个缓冲区的大小决定了输出延迟。

  • 还有一个名为 fifo_size 的字段,用于指定硬件 FIFO 的大小。但目前该字段既不会被驱动使用,也不会被 alsa-lib 使用,因此你可以忽略它。

PCM 配置

接下来,我们回到 PCM 的运行时结构体记录。运行时实例中最常被访问的部分是 PCM 的配置信息。这些配置信息在应用程序通过 alsa-lib 发送 hw_params 数据之后,会被存储到运行时结构体中。很多字段都是从 hw_params 和 sw_params 结构体中复制过来的。

例如,format 字段表示应用程序选择的音频数据格式,它的值是一个枚举类型,即 SNDRV_PCM_FORMAT_XXX。

需要注意的一点是:配置好的缓冲区大小和 period 大小在运行时结构体中是以“帧(frame)”为单位存储的。在 ALSA 中,1 帧 = 通道数 × 每个采样的字节数。

如果你需要在帧数和字节数之间进行转换,可以使用辅助函数 frames_to_bytes() 和 bytes_to_frames()。

period_bytes = frames_to_bytes(runtime, runtime->period_size);

此外,许多软件参数(sw_params)也同样是以帧(frames)为单位进行存储的。请务必检查字段的数据类型。

snd_pcm_uframes_t 表示无符号整型的帧数,而 snd_pcm_sframes_t 表示有符号整型的帧数。

参考学习:

https://kernel.org/doc/html/latest/sound/kernel-api/writing-an-alsa-driver.html#pcm-interface

你可能感兴趣的:(音频ALSA驱动,linux,ALSA)