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

news2024/11/26 4:01:41

简单的 ALSA Driver 实现

  • 一、概述
  • 二、Linux ALSA 音频设备驱动实例
    • 1、注册 Platform Device & Platform Driver
    • 2、创建 card
    • 3、PCM 设备相关设定
      • 3.1 创建 PCM Device
      • 3.2 设置 PCM 操作
      • 3.2 PCM HW 初始化
    • 4、Control 设备相关设定
      • 4.1 定义 snd_kcontrol_new
      • 4.2 构造 control
    • 5、填充 card 重要参数 & 注册 card
  • 三、示例来源

一、概述

本节主要根据例子来描述撰写简单的 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 缓冲区已被预先分配的情况下该函数被调用才可成立

dummy.c 函数实现如下:

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));
}
  • 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.2 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;
}

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 实现,我大致画了一个时序图,如下:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/31005.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C语言实现冒泡排序(图解)

目录 一、冒泡排序是什么&#xff1f; 二、图解冒泡排序过程 三、代码实现 3.1易错点&#xff08;切记切记&#xff09; 四、优化 4.1优化代码 一、冒泡排序是什么&#xff1f; int arr[]{9,8,7,6,5,4,3,2,1,0} &#xff0c;像这样的数组&#xff0c;升序排序。 冒泡排序…

Dynamic Potential-Based Reward Shaping将势能塑形奖励函数拓展为F(s,t,s‘,t‘)

摘要 基于势能的奖励塑形可以显著降低学习最优策略所需的时间&#xff0c;并且在多agent系统中&#xff0c;可以显著提高最终联合策略的性能。已经证明&#xff0c;它不会改变一个agent单独学习的最优策略或多个agent一起学习的纳什均衡。 ------然而&#xff0c;现有证明的一…

正厚软件-软件测试用例设计方法之二-边界值

正厚软件-刘老师的干货分享 上一课我们学习测试用例的等价类划分法&#xff0c;今天我们看下边界值法。 一、方法简介 1、定义&#xff1a;边界值分析法就是对输入或输出的边界值进行测试的一种黑盒测试方法。通常边界值分析法是作为对等价类划分法的补充。 2、边界值与等价类…

Linux零拷贝原理学习

文章目录一、为什么要有 DMA 技术?二、传统的文件传输有多糟糕&#xff1f;三、如何优化文件传输的性能&#xff1f;四、 如何实现零拷贝&#xff1f;mmap writesendfile使用零拷贝技术的项目五、PageCache 有什么作用&#xff1f;六、大文件传输用什么方式实现&#xff1f;一…

c++动态创建二维数组和释放

动态创建二维数组和释放 文章目录创建参考博客&#x1f60a;点此到文末惊喜↩︎ 创建 指针数组的方式 使用malloc和free可以兼容c相比于使用STL可以更加灵活但是debug可能数组显示不全 // 初始化int **arr;int row 5;//用于表示行数int col 5;//用于表示列数arr new int…

网站升级HTTPS教程

现在越来越多的网站开始使用https协议&#xff0c;其实百度从2014年底就已经开始支持https了&#xff0c;并且据说在排名上&#xff0c;同权重的网站&#xff0c;开启https会优待提升排名。先不管排不排名吧&#xff0c;https是一种更安全更先进的技术。作为互联网的弄潮儿&…

MPU9250数据转换

MPU9250数据转换1. MPU92501.1 MPU9250介绍2. 数据转换2.1 陀螺仪数据转换2.2 加速度数据转换1. MPU9250 1.1 MPU9250介绍 1&#xff09;MPU9250 内部集成有 3 轴陀螺仪、3 轴加速度计和 3 轴磁力计 2&#xff09;输出&#xff1a; 16 位的数字量; 3&#xff09; 通过集成电路…

MCE | 免疫检查点大组团

提到免疫&#xff0c;总会让人联想到由免疫失调引起的疾病&#xff0c;例如病毒感染&#xff0c;自身免疫病类风湿性关节炎&#xff0c;癌症等。实际上&#xff0c;机体的免疫应答受到严格的调控&#xff0c;并存在多种机制预防对自身蛋白的免疫反应。在过去的 20 年中&#xf…

3.2、封装成帧

3.2、封装成帧 3.2.1、封装成帧 数据链路层给上层交付的协议数据单元添加帧头和帧尾使之成为帧 在帧头和帧尾中包含有重要的控制信息 数据链路层将上层交付下来的协议数据单元封装成帧后&#xff0c;通过物理层将构成帧的各比特转换成电信号发送到传输媒体 那么接收方的数据…

dreamweaver作业静态HTML网页设计——摩尔庄园7页HTML+CSS+JS DW大学生网页作业制作设计 Dreamweaver简单网页

HTML实例网页代码, 本实例适合于初学HTML的同学。该实例里面有设置了css的样式设置&#xff0c;有div的样式格局&#xff0c;这个实例比较全面&#xff0c;有助于同学的学习,本文将介绍如何通过从头开始设计个人网站并将其转换为代码的过程来实践设计。 ⚽精彩专栏推荐&#x1…

15_移动端项目或者前后端分离项目接口规范

15_移动端项目或者前后端分离项目接口规范写在前面的话&#xff0c;主要是谈谈接口 随着前后端的分离&#xff0c;后端工程师不需要编写页面&#xff0c;甚至不需要编写JavaScript代码&#xff0c;只需要提供接口给前端工程师即可&#xff0c;可是就是仅仅一个接口&#xff0c…

mybatis-plus自带的乐观锁

文章目录1.场景1.1.模拟冲突2.添加乐观锁2.1数据库添加字段2.2配置文件中增加乐观锁拦截器2.3类的属性上添加注解2.4再次运行测试文件3.优化流程mysbatis-plus乐观锁原理&#xff1a;mysbatis-plus进行修改操作时&#xff0c;会将数据库中version字段的值拿出来和上一个查询时的…

大一学生WEB前端静态网页——旅游网页设计与实现-张家口 6页

⛵ 源码获取 文末联系 ✈ Web前端开发技术 描述 网页设计题材&#xff0c;DIVCSS 布局制作,HTMLCSS网页设计期末课程大作业 | 游景点介绍 | 旅游风景区 | 家乡介绍 | 等网站的设计与制作| HTML期末大学生网页设计作业 HTML&#xff1a;结构 CSS&#xff1a;样式 在操作方面上运…

【电力负荷预测】模拟退火算法结合狮群算法优化Elman神经网络电力负荷预测【含Matlab源码 1454期】

⛄一、模拟退火算法简介 1 引言 模拟退火算法(Simulated Annealing&#xff0c;SA)的思想最早由Metropolis等人于1953年提出&#xff1a;Kirkpatrick于1983年第一次使用模拟退火算法求解组合最优化问题[1] 。模拟退火算法是一种基于MonteCarlo迭代求解策略的随机寻优算法&…

图文详解Linux基础经典教程(09)——部署项目至CentOS

版权声明 本文原创作者&#xff1a;谷哥的小弟作者博客地址&#xff1a;http://blog.csdn.net/lfdfhl 概述 在之前的操作里&#xff0c;我们在CentOS中安装了JDK、Tomcat、MySQL。接下来&#xff0c;我们需要将JavaWeb项目部署至CentOS。 项目开发 请在IDEA中开发JavaWeb项…

SpringBoot SpringBoot 开发实用篇 6 监控 6.3 actuator

SpringBoot 【黑马程序员SpringBoot2全套视频教程&#xff0c;springboot零基础到项目实战&#xff08;spring boot2完整版&#xff09;】 SpringBoot 开发实用篇 文章目录SpringBootSpringBoot 开发实用篇6 监控6.3 actuator6.3.1 actuator6.3.2 监控原理6.3.3 小结6 监控 …

Win10下安装CARLA

在仿真环境中要使用lidar进行测试&#xff0c;目前prescan和matlab的lidar扫描方式无法设置&#xff0c;而CARLA中lidar是机械扫描形式&#xff0c;符合需求故选择该软件&#xff0c;只是测试不想重装系统&#xff0c;便在win10下进行安装测试。 1. 安装前需要安装的软件 1. …

小白必看 最核心的5大TikTok视频营销策略(附赠工具)

数据显示&#xff0c;TikTok的用户月人均使用时长达到了每月23.6小时&#xff0c;超过了YouTube的23.2小时。TikTok的用户支出在2022年第一季度达到了8.4亿美元&#xff0c;迄今为止其用户支出总额已超过46亿美元。可见&#xff0c;Tiktok是目前发展相对较好的的短视频社交媒体…

网页JS自动化脚本(一)安装油猴或暴力猴等脚本管理器并新建脚本

在我们的工作生活当中使用网页的机会越来越高,很多时候要进行重复的操作,所以进行一些JS脚本就成了可选项首先我们要在网页浏览器中安装上脚本管理器,这里示范的是安装暴力猴,是一个开源的免费的小软件,是在github上的一款软件,下载下来之后名称为Violentmonkey_2.13.0_chrome.…

Node与浏览器平台下的Event loop

Event loop 浏览器中的Event loop 浏览器平台下一共有两个任务队列&#xff0c;一个是宏任务一个是微任务。 从上至下执行所有的同步代码执行过程中将遇到的宏任务与微任务添加至相应的队列同步代码执行完毕后&#xff0c;执行满足条件的微任务回调微任务队列执行完毕后执行…