Linux ALSA 之三:简单的 ALSA Driver 实现

这篇具有很好参考价值的文章主要介绍了Linux ALSA 之三:简单的 ALSA Driver 实现。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、概述

本节主要根据例子来描述撰写简单的 Alsa Driver 实现步骤,其中涉及的 Alsa Api 具体功能基本已经在Linux ALSA 之二:ALSA 声卡与设备中已经描述。

二、Linux ALSA 音频设备驱动实例

1、注册 Platform Device & Platform Driver

在 dummy.c 中直接 module_init() 入口函数中注册 Platform Device & Platform Driver,匹配后进入 platform_driver 的 probe() 函数。

#define SND_DUMMY_DRIVER	"snd_dummy"
static struct platform_driver snd_dummy_driver = {
	.probe		= snd_dummy_probe,
	.remove		= snd_dummy_remove,
	.driver		= {
		.name	= SND_DUMMY_DRIVER,
		.pm	= SND_DUMMY_PM_OPS,
	},
};

static int __init alsa_card_dummy_init(void)
{
	...
	//1.1 注册 Platform Driver 
	err = platform_driver_register(&snd_dummy_driver);
	if (err < 0)
		return err;
	
	// 用来模拟 DMA Memory 是否已经有被分配(后面会详解)
	err = alloc_fake_buffer();
	if (err < 0) {
		platform_driver_unregister(&snd_dummy_driver);
		return err;
	}

	cards = 0;
	for (i = 0; i < SNDRV_CARDS; i++) {
		struct platform_device *device;
		if (! enable[i])
			continue;
		//1.2 注册 Platform Device
		device = platform_device_register_simple(SND_DUMMY_DRIVER,
							 i, NULL, 0);
		if (IS_ERR(device))
			continue;
		if (!platform_get_drvdata(device)) {
			platform_device_unregister(device);
			continue;
		}
		devices[i] = device;
		cards++;
	}
	...
	return 0;
}

2、创建 card

在前面 driver & device 匹配后则会调用 probe() 函数。对于每个声卡而言,必须创建一个 card 实例,用于管理这个声卡上的所有设备(组件),如 pcm、control、MIDI、synthesizer 等。

static int snd_dummy_probe(struct platform_device *devptr)
{
	struct snd_card *card;
	struct snd_dummy *dummy;
	struct dummy_model *m = NULL, **mdl;
	int idx, err;
	int dev = devptr->id;
	
	//2.1 创建 card,其中 index 为 card 索引号,xid 为标识字符串,extra_size 为要分配的 private_data size
	err = snd_card_new(&devptr->dev, index[dev], id[dev], THIS_MODULE,
			   sizeof(struct snd_dummy), &card);
	if (err < 0)
		return err;
	dummy = card->private_data;
	dummy->card = card;
	...
	return err;
}

3、PCM 设备相关设定

每个声卡下都可以有多个 PCM 实例,一个 PCM 实例对应一个设备文件。PCM 实例由 PCM Playback & Capture Streams 组成,而每个 PCM Streams 又由一个或多个 PCM Substreams 组成(一般只有一个 Substream)。

3.1 创建 PCM Device

#define MAX_PCM_DEVICES		4
#define MAX_PCM_SUBSTREAMS	128
static int pcm_devs[SNDRV_CARDS] = {[0 ... (SNDRV_CARDS - 1)] = 1};
static int pcm_substreams[SNDRV_CARDS] = {[0 ... (SNDRV_CARDS - 1)] = 8};

static int snd_card_dummy_pcm(struct snd_dummy *dummy, int device,
			      int substreams)
{
	struct snd_pcm *pcm;
	struct snd_pcm_ops *ops;
	int err;
	//3.1 创建 PCM Device, 
	//Param1: card 指针; Param2: 标识字符串; Param3: PCM 设备索引(0 标识第一个 PCM 设备); Param4&5: Playback&Capture Substreams 数
	err = snd_pcm_new(dummy->card, "Dummy PCM", device,
			       substreams, substreams, &pcm);
	if (err < 0)
		return err;
	dummy->pcm = pcm;
	...
	return 0;
}

static int snd_dummy_probe(struct platform_device *devptr)
{
	...
	//创建所有 PCM Devices
	for (idx = 0; idx < MAX_PCM_DEVICES && idx < pcm_devs[dev]; idx++) {
		if (pcm_substreams[dev] < 1)
			pcm_substreams[dev] = 1;
		if (pcm_substreams[dev] > MAX_PCM_SUBSTREAMS)
			pcm_substreams[dev] = MAX_PCM_SUBSTREAMS;
		err = snd_card_dummy_pcm(dummy, idx, pcm_substreams[dev]);
		if (err < 0)
			goto __nodev;
	}
	...
	return err;
}

3.2 设置 PCM 操作

static struct snd_pcm_ops dummy_pcm_ops = {
	.open =		dummy_pcm_open,
	.close =	dummy_pcm_close,
	.ioctl =	snd_pcm_lib_ioctl,
	.hw_params =	dummy_pcm_hw_params,
	.hw_free =	dummy_pcm_hw_free,
	.prepare =	dummy_pcm_prepare,
	.trigger =	dummy_pcm_trigger,
	.pointer =	dummy_pcm_pointer,
};

static struct snd_pcm_ops dummy_pcm_ops_no_buf = {
	.open =		dummy_pcm_open,
	.close =	dummy_pcm_close,
	.ioctl =	snd_pcm_lib_ioctl,
	.hw_params =	dummy_pcm_hw_params,
	.hw_free =	dummy_pcm_hw_free,
	.prepare =	dummy_pcm_prepare,
	.trigger =	dummy_pcm_trigger,
	.pointer =	dummy_pcm_pointer,
	.copy_user =	dummy_pcm_copy,
	.copy_kernel =	dummy_pcm_copy_kernel,
	.fill_silence =	dummy_pcm_silence,
	.page =		dummy_pcm_page,
};

static int snd_card_dummy_pcm(struct snd_dummy *dummy, int device,
			      int substreams)
{
	struct snd_pcm *pcm;
	struct snd_pcm_ops *ops;
	int err;
	...

	// 3.2.1 根据 DMA Memory 是否已经存在选择不同的 ops
	if (fake_buffer)
		ops = &dummy_pcm_ops_no_buf;
	else
		ops = &dummy_pcm_ops;
	
	// 3.2.2 设置 pcm ops 操作
	snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, ops);
	snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, ops);
	pcm->private_data = dummy;
	pcm->info_flags = 0;
	strcpy(pcm->name, "Dummy PCM");
	if (!fake_buffer) {
		snd_pcm_lib_preallocate_pages_for_all(pcm,
			SNDRV_DMA_TYPE_CONTINUOUS,
			snd_dma_continuous_data(GFP_KERNEL),
			0, 64*1024);
	}
	return 0;
}

如上代码,设置 PCM 操作使用 snd_pcm_set_ops(),定义如下

/**
 * 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)

其中 PCM 操作结构体 snd_pcm_ops 定义如下:(详细描述已在注释中)

struct snd_pcm_ops {
	/* This is called when a pcm substream is opened */
	int (*open)(struct snd_pcm_substream *substream);
	/* This is called when a pcm substream is closed */
	int (*close)(struct snd_pcm_substream *substream);
	/* This is used for any special call to pcm ioctls. But usually could pass a generic ioctl callback snd_pcm_lib_ioctl. */
	int (*ioctl)(struct snd_pcm_substream * substream,
		     unsigned int cmd, void *arg);
	/* This is called when the hardware parameter (hw_params) is set up by the application, 
	that is, once when the buffer size, the period size, the format, etc. 
	Many hardware setups should be done in this callback, including the allocation of buffer. */
	int (*hw_params)(struct snd_pcm_substream *substream,
			 struct snd_pcm_hw_params *params);
	/* This is called to release the resources allocated via hw_params */
	int (*hw_free)(struct snd_pcm_substream *substream);
	/* This is callback is called when the pcm is 'prepared'. You can set the format type,
	sample rate, etc. The difference from hw_params is that the prepare callback will be 
	called each time snd_pcm_prepare() is called, i.e. when recovering after underruns, etc.
	Be careful that this callback will be called many times at each setup, too. */
	int (*prepare)(struct snd_pcm_substream *substream);
	/* This is called when the pcm is started, stopped or paused. Which action is specified 
	in the second argument, SNDRV_PCM_TRIGGER_XXX in <sound/pcm.h>.  At Least, 
	the START and STOP commands must be defined in this callback. */
	int (*trigger)(struct snd_pcm_substream *substream, int cmd);
	/* This callback is called when the PCM middle layer inquires the current hardware position 
	on the buffer. The position must be returned in frames, ranging from 0 to buffer_size-1.
	This is called usually from the buffer-update routine in the pcm middle layer, which
	is invoked when snd_pcm_period_elapsed() is called in the interrupt routine. Then 
	the pcm middle layer updates the position and calculates the available space, and wakes 
	up the sleeping poll threads, etc. This callback is alse atmoic. */
	snd_pcm_uframes_t (*pointer)(struct snd_pcm_substream *substream);
	int (*get_time_info)(struct snd_pcm_substream *substream,
			struct timespec *system_ts, struct timespec *audio_ts,
			struct snd_pcm_audio_tstamp_config *audio_tstamp_config,
			struct snd_pcm_audio_tstamp_report *audio_tstamp_report);
	/* For copy and silence callbacks: These callbacks are not mandatory, and can be omitted in most case.
	These callbacks are used when the hardware buffer cannot be in the normal memory space. 
	Some chips have their own buffer on the hardware which is not mappable. In such a case, 
	you have to transfer the data manually from the memory buffer to the hardware buffer. 
	Or, if the buffer is non-contiguous on both physical and virtual memory spaces, these callbacks must be defined, too.*/
	int (*fill_silence)(struct snd_pcm_substream *substream, int channel,
			    unsigned long pos, unsigned long bytes);
	int (*copy_user)(struct snd_pcm_substream *substream, int channel,
			 unsigned long pos, void __user *buf,
			 unsigned long bytes);
	int (*copy_kernel)(struct snd_pcm_substream *substream, int channel,
			   unsigned long pos, void *buf, unsigned long bytes);
	struct page *(*page)(struct snd_pcm_substream *substream,
			     unsigned long offset);
	int (*mmap)(struct snd_pcm_substream *substream, struct vm_area_struct *vma);
	/* This callbacks is also not mandatory. This callback is called when the appl_ptr 
	is updated in read or write operations. Some drivers like emu10k1-fx and cs46xx need 
	to track the current appl_ptr for the internal buffer, and this callback is useful 
	only for such a purpose. */
	int (*ack)(struct snd_pcm_substream *substream);
};

在上述代码中可以看到针对 fake_buffer 的不同对应的 ops 也不同,其中 fake_buffer 用于描述 DMA 是否已经有自己的缓冲区 Buffer,具体描述如下:

1) fake_buffer = 1
   => 在 module_init 中有申请 fake buffer(当成是 DMA 已经有相应的缓冲区)
      => 由于该 DMA 已经有相应可用的 Buffer, 故无需 allocate_pages for DMA
         => 对应的 snd_pcm_ops = dummy_pcm_ops_no_buf
         	=> 在 ops->hw_params 中不会再 allocate DMA 缓冲区
         	   => 该 snd_pcm_ops 则需要填充 copy_user 等 callbacks, 在 pcm_write() 时最终会将数据给到 copy_user(), 需要 driver 在 copy_user() 中将 Audio Data Copy to DMA Buffer.
         	   
2) fake_buffer = 0
   => 在 module_init 中没有申请 fake buffer(当成是 DMA 没有相应的缓冲区)
      => 由于该 DMA 没有相应可用的 Buffer, 故需要 allocate_pages for DMA
        【在 snd_pcm_new 之后先 call snd_pcm_lib_preallocate_pages_for_all() 预分配 DMA Buffer(预分配=> 未分配), 即设置相应参数,如 size & max_size】
         => 对应的 snd_pcm_ops = dummy_pcm_ops
         	=> 在 ops->hw_params 中会 call snd_pcm_lib_malloc_pages() 分配 DMA 缓冲区, 并将 dma buffer info 保存在 substream->dma_buffer && substream-> runtime->dma_buffer_p & dma_area & dma_addr & dma_bytes
         	   => 该 snd_pcm_ops 则不会填充 copy_user 等 callbacks, 在 pcm_write() 时会将 Audio Data 直接送到前面分配的 DMA Buffer 中, 即 runtime->dma_area + hwOffset.      

举例:
1)mtk-dtv 对于 audio 都会有被分配好一块 DDR,此时使用到的 DMA 都有对应物理地址连续的一片 DDR 作为 DMA 缓冲区,此时则属于上述 fake_buffer=1 Case;
2)mtk-mt2701 等移动设备对于 PCM DMA 的话则没有分配好 DMA 缓冲区,即需要在 driver 中去分配,此时则属于上述 fake_buffer=0 Case.
Note:上述例子由于涉及 ASOC,故后面的章节中会讲解到。

  • 当一个 PCM 子流被打开时,会为该 substeam 分配一个 runtime 实例(即 substream->runtime),runtime 包含各种各样的信息:hw_params & sw_params 配置的拷贝、缓冲区指针、mmap 记录、自旋锁等,几乎 PCM 的所有控制信息均能从中取得,定义如下:
struct snd_pcm_runtime {
	/* -- Status -- */
	struct snd_pcm_substream *trigger_master;
	struct timespec trigger_tstamp;	/* trigger timestamp */
	bool trigger_tstamp_latched;     /* trigger timestamp latched in low-level driver/hardware */
	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 */
	unsigned long hw_ptr_jiffies;	/* Time when hw_ptr is updated */
	unsigned long hw_ptr_buffer_jiffies; /* buffer time in jiffies */
	snd_pcm_sframes_t delay;	/* extra delay; typically FIFO size */
	u64 hw_ptr_wrap;                /* offset for hw_ptr due to boundary wrap-around */

	/* -- 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 */
	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;
	unsigned int no_period_wakeup: 1;

	/* -- SW params -- */
	int tstamp_mode;		/* mmap timestamp is updated */
  	unsigned int period_step;
	snd_pcm_uframes_t start_threshold;
	snd_pcm_uframes_t stop_threshold;
	snd_pcm_uframes_t silence_threshold; /* Silence filling happens when
						noise is nearest than this */
	snd_pcm_uframes_t silence_size;	/* Silence filling size */
	snd_pcm_uframes_t boundary;	/* pointers wrap point */

	snd_pcm_uframes_t silence_start; /* starting pointer to silence area */
	snd_pcm_uframes_t silence_filled; /* size filled with silence */

	union snd_pcm_sync_id sync;	/* hardware synchronization ID */

	/* -- mmap -- */
	struct snd_pcm_mmap_status *status;
	struct snd_pcm_mmap_control *control;

	/* -- locking / scheduling -- */
	snd_pcm_uframes_t twake; 	/* do transfer (!poll) wakeup if non-zero */
	wait_queue_head_t sleep;	/* poll sleep */
	wait_queue_head_t tsleep;	/* transfer sleep */
	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 */
	int tstamp_type;		/* timestamp type */

	/* -- 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 */

	/* -- audio timestamp config -- */
	struct snd_pcm_audio_tstamp_config audio_tstamp_config;
	struct snd_pcm_audio_tstamp_report audio_tstamp_report;
	struct timespec driver_tstamp;

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

snd_pcm_runtime 中的大多数记录对被声卡驱动操作集中的函数是只读的,仅仅 PCM 中间层可更新或修改这些信息,但是硬件描述、中断回调函数、DMA 缓冲区信息和私有数据是例外的。

下面解释 snd_pcm_runtime 结构体中的几个重要成员:

(1)Hw Params
包含了基本硬件配置的定义,需要在 ops->open() 函数中赋值。runtime 实例保存的是硬件描述的拷贝而非指针,这意味着在 open()函数中可以修改被拷贝的硬件描述(runtime->hw)。

struct snd_pcm_hardware {
	unsigned int info;		/* SNDRV_PCM_INFO_* */
	u64 formats;			/* SNDRV_PCM_FMTBIT_* */
	unsigned int rates;		/* SNDRV_PCM_RATE_* */
	unsigned int rate_min;		/* min rate */
	unsigned int rate_max;		/* max rate */
	unsigned int channels_min;	/* min channels */
	unsigned int channels_max;	/* max channels */
	size_t buffer_bytes_max;	/* max buffer size */
	size_t period_bytes_min;	/* min period size */
	size_t period_bytes_max;	/* max period size */
	unsigned int periods_min;	/* min # of periods */
	unsigned int periods_max;	/* max # of periods */
	size_t fifo_size;		/* fifo size in bytes */
};

buffer_bytes_max 定义最大的缓冲区大小,注意:没有 buffer_bytes_min 字段,这是因为它可以通过最小的周期大小和最小的周期数量计算出来;
period 信息定义了 PCM 中断产生的周期。更小的周期大小意味着
更多的中断,在录音时,周期大小定义了输入延迟,在播放时,整个缓冲区大小对应着输出延迟。
Note:对缓冲区和周期大小的配置以帧(frame)形式存储,而 frames_to_bytes()bytes_to_frames()可完成帧和字节的转换。

For pcm:
1 frame => format2bytes*channels;
1 sample => format2bytes.

dummy.c 定义如下:

static const struct snd_pcm_hardware dummy_pcm_hardware = {
	.info =			(SNDRV_PCM_INFO_MMAP |
				 SNDRV_PCM_INFO_INTERLEAVED |
				 SNDRV_PCM_INFO_RESUME |
				 SNDRV_PCM_INFO_MMAP_VALID),
	.formats =		USE_FORMATS,
	.rates =		USE_RATE,
	.rate_min =		USE_RATE_MIN,
	.rate_max =		USE_RATE_MAX,
	.channels_min =		USE_CHANNELS_MIN,
	.channels_max =		USE_CHANNELS_MAX,
	.buffer_bytes_max =	MAX_BUFFER_SIZE,
	.period_bytes_min =	MIN_PERIOD_SIZE,
	.period_bytes_max =	MAX_PERIOD_SIZE,
	.periods_min =		USE_PERIODS_MIN,
	.periods_max =		USE_PERIODS_MAX,
	.fifo_size =		0,
};

snd_pcm_ops 中的 open() 函数将被调用,在这个函数中,至少需要初始化 runtime->hw 字段,dummy 代码范例如下:

static int dummy_pcm_open(struct snd_pcm_substream *substream)
{
	// 通过 snd_pcm_substream_chip() 先获得 snd_card 私有函数
	struct snd_dummy *dummy = snd_pcm_substream_chip(substream);
	struct dummy_model *model = dummy->model;

	//获得 pcm runtime 指针
	struct snd_pcm_runtime *runtime = substream->runtime;
	...
	//初始化 runtime->hw
	runtime->hw = dummy->pcm_hw;
	...
	return 0;
}

上述代码中的 dummy->pcm_hw 是预先定义的硬件描述。

(2)DMA Buffer Info
包含 dma_area(逻辑地址)、 dma_addr(物理地址)、 dma_bytes(缓冲区大小)和 dma_private
(被 ALSA DMA 分配器使用)。可以由 snd_pcm_lib_malloc_pages() 实现, ALSA 中间层会设置 DMA
缓冲区信息的相关字段,这种情况下,驱动中不能再写这些信息,只能读取。也就是说,如果使用标准的缓冲区分配函数 snd_pcm_lib_malloc_pages() 分配缓冲区,则我们不需要自己维护 DMA 缓冲区信息。如果缓冲区由自己分配,则需要在 hw_params()函数中管理缓冲区信息,至少需管理 dma_bytes 和 dma_addr,如果支持 mmap,则必须管理 dma_area,对 dma_private 的管理视情况而定。

(3) 运行 Status
通过 runtime->status 可以获得运行状态,它是snd_pcm_mmap_status 结构体的指针,例如,通过 runtime->status->hw_ptr 可以获得目前的 DMA 硬件指针。此外,通过 runtime->control 可以获得 DMA 应用指针,它指向 snd_pcm_mmap_control 结构体指针,但是不建议直接访问该指针。

  • 驱动中通常可以给 snd_pcm_ops 的 ioctl()成员函数传递通用的 snd_pcm_lib_ioctl()函数,如下:
/* .ioctl =	snd_pcm_lib_ioctl, */
int snd_pcm_lib_ioctl(struct snd_pcm_substream *substream,
		      unsigned int cmd, void *arg)
{
	switch (cmd) {
	case SNDRV_PCM_IOCTL1_RESET:
		return snd_pcm_lib_ioctl_reset(substream, arg);
	case SNDRV_PCM_IOCTL1_CHANNEL_INFO:
		return snd_pcm_lib_ioctl_channel_info(substream, arg);
	case SNDRV_PCM_IOCTL1_FIFO_SIZE:
		return snd_pcm_lib_ioctl_fifo_size(substream, arg);
	}
	return -ENXIO;
}
  • 当一个 PCM 子流被打开时,snd_pcm_ops 的 hw_params() 成员函数将在应用程序设置硬件参数(PCM substream 的周期大小、缓冲区大小和格式等)的时候被调用。仅当 DMA 缓冲区已被预先分配(即被 call snd_pcm_lib_preallocate_pages_for_all())的情况下会去分配缓冲区(详细描述如上),如下:
static int dummy_pcm_hw_params(struct snd_pcm_substream *substream,
			       struct snd_pcm_hw_params *hw_params)
{
	if (fake_buffer) {
		/* runtime->dma_bytes has to be set manually to allow mmap */
		substream->runtime->dma_bytes = params_buffer_bytes(hw_params);
		return 0;
	}
	return snd_pcm_lib_malloc_pages(substream,
					params_buffer_bytes(hw_params));
}
  • 当 PCM 子流被关闭时,close() 函数将会被调用。如果 open() 函数中分配了私有函数,则在 close() 函数中应该释放 substream 的私有函数,如下:
static int dummy_pcm_close(struct snd_pcm_substream *substream)
{
	get_dummy_ops(substream)->free(substream);
	return 0;
}
  • ioctl() 函数传递通用的 snd_pcm_lib_ioctl() 函数。
  • hw_params() 成员函数将在应用程序在设置硬件参数(PCM 子流的周期大小、缓冲区大小和格式等)的时候被调用。在这个函数中将完成大量硬件设置,甚至包括缓冲区分配,这时可调用如下辅助函数:
static int snd_xxx_hw_params(struct snd_pcm_substream *substream,struct snd_pcm_hw_params *hw_params); //仅当 DMA 缓冲区已被预先分配的情况下该函数被调用才可成立
  • hw_free() 成员函数与 hw_params() 相对,它释放由 hw_params() 分配的资源,如下:
static int dummy_pcm_hw_free(struct snd_pcm_substream *substream)
{
	if (fake_buffer)
		return 0;
	return snd_pcm_lib_free_pages(substream);
}
  • prepare() 成员函数当 PCM 被 “准备” 时被调用,在其中可设置采样率、格式等。prepare() 函数与 hw_params()函数的不同在于对 prepare()的调用发生在 snd_pcm_prepare()每次被调用的时
    候。如下:
#define get_dummy_ops(substream) \
	(*(const struct dummy_timer_ops **)(substream)->runtime->private_data)

static int dummy_pcm_prepare(struct snd_pcm_substream *substream)
{
	return get_dummy_ops(substream)->prepare(substream);
}
  • trigger() 成员函数在 PCM 被开始、停止或暂停时调用。在 trigger() 函数中至少要处理 SNDRV_PCM_TRIGGER_START 和 SNDRV_PCM_TRIGGER_STOP 命令,如果 PCM 支持暂停,还应处理 SNDRV_PCM_TRIGGER_PAUSE_PUSH 和 SNDRV_PCM_TRIGGER_PAUSE_RELEASE 命令等。【注意 trigger()函数是原子的,中途不能睡眠。】
static int dummy_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
{
	switch (cmd) {
	case SNDRV_PCM_TRIGGER_START:
	case SNDRV_PCM_TRIGGER_RESUME:
		return get_dummy_ops(substream)->start(substream);
	case SNDRV_PCM_TRIGGER_STOP:
	case SNDRV_PCM_TRIGGER_SUSPEND:
		return get_dummy_ops(substream)->stop(substream);
	}
	return -EINVAL;
}
  • pointer() 函数用于 PCM 中间层查询目前缓冲区的硬件位置,该函数以帧的形式返回 0~buffer_size – 1 的位置,此函数也是原子的。如下:
static snd_pcm_uframes_t dummy_pcm_pointer(struct snd_pcm_substream *substream)
{
	return get_dummy_ops(substream)->pointer(substream);
}
  • copy() 和 silence() 函数一般可以省略,但是,当硬件缓冲区不处于常规内容中时需要。例如,一些设备有自己的不能被映射的硬件缓冲区,这种情况下,我们不得不将数据从内存缓冲区复制到硬件缓冲区。

3.3 PCM HW 初始化

如前面所述,一般在 snd_pcm_ops 中的 open() 成员函数中都需要初始化 pcm_hw,一般不同的 substream 对应的 pcm hw 都是不一样的,故一般在 open() 才会对 pcm hw init,而 dummy.c 则直接在 probe() 函数中做了,如下:

static int snd_dummy_probe(struct platform_device *devptr)
{
	...
	for (mdl = dummy_models; *mdl && model[dev]; mdl++) {
		if (strcmp(model[dev], (*mdl)->name) == 0) {
			printk(KERN_INFO
				"snd-dummy: Using model '%s' for card %i\n",
				(*mdl)->name, card->number);
			m = dummy->model = *mdl;
			break;
		}
	}
	...
	dummy->pcm_hw = dummy_pcm_hardware;
	if (m) {
		if (m->formats)
			dummy->pcm_hw.formats = m->formats;
		if (m->buffer_bytes_max)
			dummy->pcm_hw.buffer_bytes_max = m->buffer_bytes_max;
		if (m->period_bytes_min)
			dummy->pcm_hw.period_bytes_min = m->period_bytes_min;
		if (m->period_bytes_max)
			dummy->pcm_hw.period_bytes_max = m->period_bytes_max;
		if (m->periods_min)
			dummy->pcm_hw.periods_min = m->periods_min;
		if (m->periods_max)
			dummy->pcm_hw.periods_max = m->periods_max;
		if (m->rates)
			dummy->pcm_hw.rates = m->rates;
		if (m->rate_min)
			dummy->pcm_hw.rate_min = m->rate_min;
		if (m->rate_max)
			dummy->pcm_hw.rate_max = m->rate_max;
		if (m->channels_min)
			dummy->pcm_hw.channels_min = m->channels_min;
		if (m->channels_max)
			dummy->pcm_hw.channels_max = m->channels_max;
	}
    ...
	return err;
}

3.4 PCM 周期中断操作[特殊]

在前面有提到,snd_pcm_hardware 中的 period 定义 PCM 中断产生的周期,表示在 peroid 之后需要有一个中断来 update runtime->status->ptr(即可以理解为 DMA Buffer RdPtr),那么该操作在 alsa driver 是怎么实现的呢?下面以 dummy.c 举例,

在 dummy.c 有专门定义 system timer interface 以及 timer ops,以实现 PCM 周期中断操作,定义如下:

struct dummy_systimer_pcm {
	/* ops must be the first item */
	const struct dummy_timer_ops *timer_ops;
	spinlock_t lock;
	struct timer_list timer;
	unsigned long base_time;
	unsigned int frac_pos;	/* fractional sample position (based HZ) */
	unsigned int frac_period_rest;
	unsigned int frac_buffer_size;	/* buffer_size * HZ */
	unsigned int frac_period_size;	/* period_size * HZ */
	unsigned int rate;
	int elapsed;
	struct snd_pcm_substream *substream;
};
  • timer_ops 创建 & 配置 period 为周期的 timer interrupt,定义如下:
struct dummy_timer_ops {
	int (*create)(struct snd_pcm_substream *);
	void (*free)(struct snd_pcm_substream *);
	int (*prepare)(struct snd_pcm_substream *);
	int (*start)(struct snd_pcm_substream *);
	int (*stop)(struct snd_pcm_substream *);
	snd_pcm_uframes_t (*pointer)(struct snd_pcm_substream *);
};
  • timer timer 中断结构体,具体定义见 /include/linux/timer.h
  • base_time timer base time;

对于 dummy_timer_ops 具体实现如下:

static const struct dummy_timer_ops dummy_systimer_ops = {
	.create =	dummy_systimer_create,
	.free =		dummy_systimer_free,
	.prepare =	dummy_systimer_prepare,
	.start =	dummy_systimer_start,
	.stop =		dummy_systimer_stop,
	.pointer =	dummy_systimer_pointer,
};
  • dummy_systimer_create
    在 snd_pcm_ops->open 函数中会调用该函数,主要实现 init timer,如下:
static int dummy_systimer_create(struct snd_pcm_substream *substream)
{
	struct dummy_systimer_pcm *dpcm;

	dpcm = kzalloc(sizeof(*dpcm), GFP_KERNEL);
	if (!dpcm)
		return -ENOMEM;
	substream->runtime->private_data = dpcm;
	timer_setup(&dpcm->timer, dummy_systimer_callback, 0);
	spin_lock_init(&dpcm->lock);
	dpcm->substream = substream;
	return 0;
}
  • dummy_systimer_prepare
    在 snd_pcm_ops->prepare 函数中会调用该函数,主要是根据 runtime->buffer_size,period_size 为 start 前第一个 timer interrupt 配置周期等;
static int dummy_systimer_prepare(struct snd_pcm_substream *substream)
{
	struct snd_pcm_runtime *runtime = substream->runtime;
	struct dummy_systimer_pcm *dpcm = runtime->private_data;

	dpcm->frac_pos = 0;
	dpcm->rate = runtime->rate;
	dpcm->frac_buffer_size = runtime->buffer_size * HZ;
	dpcm->frac_period_size = runtime->period_size * HZ;
	dpcm->frac_period_rest = dpcm->frac_period_size;
	dpcm->elapsed = 0;

	return 0;
}
  • dummy_systimer_start
    在 snd_pcm_ops->start 函数中会调用该函数,主要是根据 prepare 配置的参数创建一个 timer interrupt;
static void dummy_systimer_rearm(struct dummy_systimer_pcm *dpcm)
{
	mod_timer(&dpcm->timer, jiffies +
		(dpcm->frac_period_rest + dpcm->rate - 1) / dpcm->rate);
}
static int dummy_systimer_start(struct snd_pcm_substream *substream)
{
	struct dummy_systimer_pcm *dpcm = substream->runtime->private_data;
	spin_lock(&dpcm->lock);
	dpcm->base_time = jiffies;
	dummy_systimer_rearm(dpcm);
	spin_unlock(&dpcm->lock);
	return 0;
}

在前面 dummy_systimer_create() 定义 timer 时有定义 callback 为 dummy_systimer_callback(),定义如下:

static void dummy_systimer_callback(struct timer_list *t)
{
	struct dummy_systimer_pcm *dpcm = from_timer(dpcm, t, timer);
	unsigned long flags;
	int elapsed = 0;
	
	spin_lock_irqsave(&dpcm->lock, flags);
	dummy_systimer_update(dpcm);
	dummy_systimer_rearm(dpcm);
	elapsed = dpcm->elapsed;
	dpcm->elapsed = 0;
	spin_unlock_irqrestore(&dpcm->lock, flags);
	if (elapsed)
		snd_pcm_period_elapsed(dpcm->substream);
}

该中断函数主要就是调用 snd_pcm_preriod_elapsed() 函数,该函数主要是 update runtime->status->hw_ptr,以保证可以及时更新 free space,使得 user space 一直写数据下来。其还会调用 snd_pcm_ops->pointer() 函数,进而调用 dummy_systimer_pointer() 函数,循坏创建 period timer interrupt.

static snd_pcm_uframes_t
dummy_systimer_pointer(struct snd_pcm_substream *substream)
{
	struct dummy_systimer_pcm *dpcm = substream->runtime->private_data;
	snd_pcm_uframes_t pos;

	spin_lock(&dpcm->lock);
	dummy_systimer_update(dpcm);
	pos = dpcm->frac_pos / HZ;
	spin_unlock(&dpcm->lock);
	return pos;
}

4、Control 设备相关设定

每个声卡下都有一个 Control 设备,在前面中已经讲解,在创建 snd_card 的时候会自动创建 Control 设备。故此时则按照Linux ALSA 之二:ALSA 声卡与设备中创建 Control 设备的方式创建相应的 Control,如下:

4.1 定义 snd_kcontrol_new

创建一个新的 control 至少需要实现 snd_kcontrol_new 中的 info()、get() 和 put() 这 3个成员函数,dummy.c 实现如下:

/*
 * mixer interface
 */

#define DUMMY_VOLUME(xname, xindex, addr) \
{ .iface = SNDRV_CTL_ELEM_IFACE_MIXER, \
  .access = SNDRV_CTL_ELEM_ACCESS_READWRITE | SNDRV_CTL_ELEM_ACCESS_TLV_READ, \
  .name = xname, .index = xindex, \
  .info = snd_dummy_volume_info, \
  .get = snd_dummy_volume_get, .put = snd_dummy_volume_put, \
  .private_value = addr, \
  .tlv = { .p = db_scale_dummy } }

/* 获取该 control 的详细信息 */
static int snd_dummy_volume_info(struct snd_kcontrol *kcontrol,
				 struct snd_ctl_elem_info *uinfo)
{
	uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
	uinfo->count = 2;
	uinfo->value.integer.min = -50;
	uinfo->value.integer.max = 100;
	return 0;
}

/* 得到 control 的目前值并返回用户空间 */
static int snd_dummy_volume_get(struct snd_kcontrol *kcontrol,
				struct snd_ctl_elem_value *ucontrol)
{
	struct snd_dummy *dummy = snd_kcontrol_chip(kcontrol);
	int addr = kcontrol->private_value;

	spin_lock_irq(&dummy->mixer_lock);
	ucontrol->value.integer.value[0] = dummy->mixer_volume[addr][0];
	ucontrol->value.integer.value[1] = dummy->mixer_volume[addr][1];
	spin_unlock_irq(&dummy->mixer_lock);
	return 0;
}

/* 从用户空间写入值, 如果值被改变则返回1, 否则返回0 */
static int snd_dummy_volume_put(struct snd_kcontrol *kcontrol,
				struct snd_ctl_elem_value *ucontrol)
{
	struct snd_dummy *dummy = snd_kcontrol_chip(kcontrol);
	int change, addr = kcontrol->private_value;
	int left, right;

	left = ucontrol->value.integer.value[0];
	if (left < -50)
		left = -50;
	if (left > 100)
		left = 100;
	right = ucontrol->value.integer.value[1];
	if (right < -50)
		right = -50;
	if (right > 100)
		right = 100;
	spin_lock_irq(&dummy->mixer_lock);
	change = dummy->mixer_volume[addr][0] != left ||
	         dummy->mixer_volume[addr][1] != right;
	dummy->mixer_volume[addr][0] = left;
	dummy->mixer_volume[addr][1] = right;
	spin_unlock_irq(&dummy->mixer_lock);
	return change;
}

static const DECLARE_TLV_DB_SCALE(db_scale_dummy, -4500, 30, 0);

static struct snd_kcontrol_new snd_dummy_controls[] = {
DUMMY_VOLUME("Master Volume", 0, MIXER_ADDR_MASTER),
...
};

4.2 构造 control

当所有事情准备好后,我们需要创建一个 control,调用 snd_ctl_add() 和 snd_ctl_new() 这两个函数来完成,dummy.c 实现如下:

static int snd_card_dummy_new_mixer(struct snd_dummy *dummy)
{
	struct snd_card *card = dummy->card;
	struct snd_kcontrol *kcontrol;
	unsigned int idx;
	int err;

	spin_lock_init(&dummy->mixer_lock);
	strcpy(card->mixername, "Dummy Mixer");
	dummy->iobox = 1;

	for (idx = 0; idx < ARRAY_SIZE(snd_dummy_controls); idx++) {
		/* 创建 snd_kcontrol 并返回指针 */
		kcontrol = snd_ctl_new1(&snd_dummy_controls[idx], dummy);
		/* 将创建的 snd_kcontrol 添加到对应的 card 中 */
		err = snd_ctl_add(card, kcontrol);
		if (err < 0)
			return err;
		if (!strcmp(kcontrol->id.name, "CD Volume"))
			dummy->cd_volume_ctl = kcontrol;
		else if (!strcmp(kcontrol->id.name, "CD Capture Switch"))
			dummy->cd_switch_ctl = kcontrol;

	}
	return 0;
}

static int snd_dummy_probe(struct platform_device *devptr)
{
	...
	err = snd_card_dummy_new_mixer(dummy);
	...
	return err;
}

5、填充 card 重要参数 & 注册 card

在创建好所有该 card 下的 device 最后需要调用 snd_card_register() 注册声卡,dummy.c 示例代码如下:

static int snd_dummy_probe(struct platform_device *devptr)
{
	...
	/* 填充 card driver, shortname, longname 等重要参数 */
	strcpy(card->driver, "Dummy");
	strcpy(card->shortname, "Dummy");
	sprintf(card->longname, "Dummy %i", dev + 1);

	dummy_proc_init(dummy);

	/* 注册声卡 */
	err = snd_card_register(card);
	if (err == 0) {
		platform_set_drvdata(devptr, card);
		return 0;
	}
      __nodev:
	snd_card_free(card);
	return err;
}

三、示例来源

其中上文中描述的例子来源:kernel/sound/drivers/dummy.c
关于 dummy.c 简单的 ALSA Driver 实现,我大致画了一个时序图,如下:
Linux ALSA 之三:简单的 ALSA Driver 实现文章来源地址https://www.toymoban.com/news/detail-416756.html

到了这里,关于Linux ALSA 之三:简单的 ALSA Driver 实现的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【linux】NVIDIA驱动失效简单解决方案:NVIDIA-SMI has failed because it couldn‘t communicate with the NVIDIA driver

    之前在ubuntu20.04安装过NVIDIA驱动可以正常使用,突然一天发现外扩显示器无法识别,使用NVIDIA-SMI查看时报错: DKMS全称是DynamicKernel ModuleSupport,它可以帮我们维护内核外的驱动程序,在内核版本变动之后可以自动重新生成新的模块。 1)安装DKMS 2)查看已安装的NVIDIA版本 3)重

    2024年02月11日
    浏览(45)
  • Kubernetes(K8s)从入门到精通系列之三:K8s的基本概念和术语之资源对象概述

    K8s中的基本概念和术语大多是围绕资源对象(Resource Object)来说的,而资源对象在总体上可分为以下两类: 某种资源的对象,例如节点(Node)、Pod、服务(Service)、存储卷(Volume)。 与资源对象相关的事物与动作,例如标签(Label)、注解(Annotation)、命名空间(Namespace)、部署(Deployment)、

    2024年02月14日
    浏览(66)
  • 【Mybatis】深入学习MyBatis:概述、主要特性以及配置与映射

    🍎 个人博客: 个人主页 🏆 个人专栏:   Mybatis    ⛳️   功不唐捐,玉汝于成 目录 前言 正文 一、概述 MyBatis简介 主要特性 1. 动态SQL 2.结果映射 3 .插件机制 二、MyBatis配置文件 1.配置文件结构 数据库连接信息 全局配置 映射器扫描 2.SQL映射文件 SQL语句定义 参数映射和

    2024年02月04日
    浏览(56)
  • Linux ALSA音频工具aplay、arecord、amixer的使用方法

    ALSA 是Advanced Linux Sound Architecture的缩写,先进的Linux音频架构,为Linux操作系统提供音频和MIDI功能。 aplay是播放命令。 -t, 指定输出数据格式,默认是wave格式,还支持raw,voc,au,后两种格式不懂。 -D,指定播放设备,设备名的格式为hw:0,0,表示0号card的0号device。 -c,指定c

    2024年02月06日
    浏览(40)
  • Linux ALSA 之十四:ASOC DAPM 之 Widget & Path & Route 分析

    在前面文章已经介绍了 DAPM 基本知识,了解了代表音频控件的 widget,并且系统中注册的各种 widget 需要互相连接在一起才能协调工作,故对于 ASOC Driver 中当使用到 DAPM 时需要定义 Widget 以及连接路径的 routes(ASOC DAPM Core 注册 Route 时会对连接的两个 widget 建立 path)。在本节中

    2024年02月06日
    浏览(40)
  • 深入探索RK3588平台开发:解析Linux音频调试与alsa-utils工具

    近期我深入研究了RK3588平台的开发,特别是在音频领域的探索。在这个系列的讲解中,我们将重点关注Linux音频调试,并深入探讨与之相关的alsa-utils工具。通过本文,我将为大家详细介绍如何在RK3588平台上进行高效的音频开发,让我们一同踏入这个令人兴奋的领域。 RK3588是瑞

    2024年01月25日
    浏览(53)
  • Linux音频处理:MP3解码、PCM、播放PCM、ALSA(Advanced Linux Sound Architecture)、MPEG(Moving Picture Experts Group)

    将MP3音频文件中的数字音频数据转换为可以播放或处理的音频信号的过程。MP3(MPEG-1 Audio Layer 3)是一种常见的音频压缩格式,用于将音频文件压缩到较小的文件大小,同时保持相对高的音质。 以下是MP3解码的一般步骤: 读取MP3文件 : 首先,需要读取存储在MP3文件中的音频

    2024年02月03日
    浏览(43)
  • C++ //练习 2.33 利用本节定义的变量,判断下列语句的运行结果。

    练习 2.33 利用本节定义的变量,判断下列语句的运行结果。 环境:Linux Ubuntu(云服务器) 工具:vim   解释 a合法 b合法 c合法 d非法 e非法 g非法

    2024年01月23日
    浏览(53)
  • 【数据结构】简单的01背包 | 主要思想分析

    简单背包问题是一个经典的组合优化问题,在计算机科学和算法领域被广泛研究和应用。 问题描述如下:给定一组物品,每个物品有对应的重量和价值,以及一个固定的背包容量。目标是选择一些物品放入背包中,使得放入背包中的物品总重量不超过背包容量,同时使得物品

    2024年02月13日
    浏览(37)
  • 请简单介绍一下Shiro框架是什么?Shiro在Java安全领域的主要作用是什么?Shiro主要提供了哪些安全功能

    会话管理(Session Management):Shiro能够管理用户的会话状态,包括会话的创建、维护和销毁。它支持多种会话存储方式(如内存、数据库等),并提供了会话超时、会话失效等机制,以确保会话的安全性。 跨平台集成:Shiro不仅适用于Web应用程序,还可以轻松集成到桌面应用

    2024年04月13日
    浏览(60)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包