usb声卡驱动(六)
前面记录了usb声卡驱动的注册过程。
下面,查看usb声卡里面pcm的打开和关闭,都做了什么工作。
一点基础前提
因为本系列文章的核心是,usb声卡驱动。所以并不会深入到alsa内部的细节。但是在进行pcm的打开和关闭之前。需要知道一些alsa内部的一些东西。
在《usb声卡驱动(四)》和《usb声卡驱动(五)》中提到了一个函数:
int snd_card_register(struct snd_card * card);
该函数,需要知道如下的一个简单逻辑。
- 它遍历所有的component,比如前面提到的pcm
- 然后调用这些component的dev_register回调。
- 在这些回调中,会有各个component专用的一些设置。比如调用device_create创建对应的驱动文件等。这样应用就可以通过文件系统来使用它了。
好了,仅仅知道这些就可以了。下面,看看pcm的打开。
pcm的打开
在《usb声卡驱动(四)》中,提及到,每个pcm可以有多个substream.而每次open时,就是open一个substream出来。而substream的个数,由下面函数的第四,第五参数指定。
int snd_pcm_new(struct snd_card * card, const char * id, int device, int playback_count, int capture_count, struct snd_pcm ** rpcm)
当有多个substream时,每次open,就打开一个substream,在使用过程中,通过substream的index来区分
当只有一个substream时,第一次open,就打开一个substream。如果重复打开,则依赖文件是否要阻塞,如果阻塞,则open调用不会返回。如果非阻塞,则返回一个错误值。
在usb声卡驱动中,只有一个substream,想一想为什么?
代码如下(详情见《usb声卡驱动(五)》):
//啦啦啦,终于找到,我们信息苦苦期待的创建pcm对象啦
err = snd_pcm_new(chip->card, "USB Audio", chip->pcm_devs,
stream == SNDRV_PCM_STREAM_PLAYBACK ? 1 : 0,
stream == SNDRV_PCM_STREAM_PLAYBACK ? 0 : 1,
&pcm);
对于usb声卡驱动而言,在open时,substream直接由参数,传递进来。那么substrea是什么时候创建的呢?
因为本笔记不是对alsa的解析,所以只给出alsa内部的简单逻辑,等有空了,再来详细记录一下alsa内部的世界吧。
- 当调用snd_pcm_new函数创建pcm实例时,内部还会根据传递的playback(capture)的个数,来创建对应个数的substream。
- 这些substream通过内部的next,形成一个链表
- 同一个方向(playback或者capture)的substream。由一个包装类snd_pcm_str包装。因此,会有两个包装类(playback或者capture),他们组成一个snd_pcm_str的数组。
- pcm实例(一个特殊的component)持有上面的snd_pcm_str数组。
- 当需要open的时候,则分配一个已经创建好的substream即可。
alsa针对pcm的open的内部逻辑,几乎就变成了上面的逆序了。简述如下:
- 首先得到pcm实例,和方向信息
- 根据方向信息,从snd_pcm_str的数组中,选择对应的snd_pcm_str对象。
- 从snd_pcm_str对象的substream获取substream的链表。然后判断合适的substream返回即可。
- 调用这个substream的open回调,即《usb声卡驱动(五)》中谈到的snd_usb_playback_ops或者snd_usb_capture_ops中的open回调
接下来,就是snd_usb_playback_ops或者snd_usb_capture_ops中的open回调了.从《usb声卡驱动(四)》可以知道,这个open回调里面,几乎就是对runtime对象的hw字段进行初始化。
如下:
static int snd_usb_playback_open(struct snd_pcm_substream *substream)
{
return snd_usb_pcm_open(substream, SNDRV_PCM_STREAM_PLAYBACK);
}
static int snd_usb_pcm_open(struct snd_pcm_substream *substream, int direction)
{
struct snd_usb_stream *as = snd_pcm_substream_chip(substream);
struct snd_pcm_runtime *runtime = substream->runtime;
struct snd_usb_substream *subs = &as->substream[direction];
subs->interface = -1;
subs->altset_idx = 0;
//预定义好的硬件描述
runtime->hw = snd_usb_hardware;
runtime->private_data = subs;
subs->pcm_substream = substream;
/* runtime PM is also done there */
/* initialize DSD/DOP context */
subs->dsd_dop.byte_idx = 0;
subs->dsd_dop.channel = 0;
subs->dsd_dop.marker = 1;
return setup_hw_info(runtime, subs);
}
注意,上面的hw赋值,snd_usb_hardware的值如下:
static struct snd_pcm_hardware snd_usb_hardware =
{
//表示硬件支持的功能和特性
.info = SNDRV_PCM_INFO_MMAP |
SNDRV_PCM_INFO_MMAP_VALID |
SNDRV_PCM_INFO_BATCH |
SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_BLOCK_TRANSFER |
SNDRV_PCM_INFO_PAUSE,
//最大buffer
.buffer_bytes_max = 1024 * 1024,
//最小period
.period_bytes_min = 64,
//最大period
.period_bytes_max = 512 * 1024,
//最小/大period数
.periods_min = 2,
.periods_max = 1024,
};
这个结构体,表示的是usb声卡支持的硬件功能和特性。
上面的函数内容,没有什么可以赘述的,进入setup_hw_info函数,如下:
static int setup_hw_info(struct snd_pcm_runtime *runtime, struct snd_usb_substream *subs)
{
struct audioformat *fp;
unsigned int pt, ptmin;
int param_period_time_if_needed;
int err;
//赋值格式
runtime->hw.formats = subs->formats;
//初始化各种值
runtime->hw.rate_min = 0x7fffffff;
runtime->hw.rate_max = 0;
runtime->hw.channels_min = 256;
runtime->hw.channels_max = 0;
runtime->hw.rates = 0;
ptmin = UINT_MAX;
/* check min/max rates and channels */
//检查各种最小和最大值
list_for_each_entry(fp, &subs->fmt_list, list) {
runtime->hw.rates |= fp->rates;
if (runtime->hw.rate_min > fp->rate_min)
runtime->hw.rate_min = fp->rate_min;
if (runtime->hw.rate_max < fp->rate_max)
runtime->hw.rate_max = fp->rate_max;
if (runtime->hw.channels_min > fp->channels)
runtime->hw.channels_min = fp->channels;
if (runtime->hw.channels_max < fp->channels)
runtime->hw.channels_max = fp->channels;
if (fp->fmt_type == UAC_FORMAT_TYPE_II && fp->frame_size > 0) {
/* FIXME: there might be more than one audio formats... */
runtime->hw.period_bytes_min = runtime->hw.period_bytes_max =
fp->frame_size;
}
pt = 125 * (1 << fp->datainterval);//计算出来的周期间隔,单位微妙
ptmin = min(ptmin, pt);
}
//电源管理相关,暂时跳过
err = snd_usb_autoresume(subs->stream->chip);
if (err < 0)
return err;
//该值表示,是否需要周期间隔的调整。全速设备不需要,因为它固定
param_period_time_if_needed = SNDRV_PCM_HW_PARAM_PERIOD_TIME;
//全速设备,间隔时间固定为1000us
if (subs->speed == USB_SPEED_FULL)
/* full speed devices have fixed data packet interval */
ptmin = 1000;
if (ptmin == 1000)
/* if period time doesn't go below 1 ms, no rules needed */
param_period_time_if_needed = -1;
//设置,周期间隔的最小值,和最大值
snd_pcm_hw_constraint_minmax(runtime, SNDRV_PCM_HW_PARAM_PERIOD_TIME,
ptmin, UINT_MAX);
//下面一些列都是在添加规则,当修改对应的参数时,对应的回调函数将会被触发
if ((err = snd_pcm_hw_rule_add(runtime, 0, SNDRV_PCM_HW_PARAM_RATE,
hw_rule_rate, subs,
SNDRV_PCM_HW_PARAM_FORMAT,
SNDRV_PCM_HW_PARAM_CHANNELS,
param_period_time_if_needed,
-1)) < 0)
goto rep_err;
if ((err = snd_pcm_hw_rule_add(runtime, 0, SNDRV_PCM_HW_PARAM_CHANNELS,
hw_rule_channels, subs,
SNDRV_PCM_HW_PARAM_FORMAT,
SNDRV_PCM_HW_PARAM_RATE,
param_period_time_if_needed,
-1)) < 0)
goto rep_err;
if ((err = snd_pcm_hw_rule_add(runtime, 0, SNDRV_PCM_HW_PARAM_FORMAT,
hw_rule_format, subs,
SNDRV_PCM_HW_PARAM_RATE,
SNDRV_PCM_HW_PARAM_CHANNELS,
param_period_time_if_needed,
-1)) < 0)
goto rep_err;
if (param_period_time_if_needed >= 0) {
err = snd_pcm_hw_rule_add(runtime, 0,
SNDRV_PCM_HW_PARAM_PERIOD_TIME,
hw_rule_period_time, subs,
SNDRV_PCM_HW_PARAM_FORMAT,
SNDRV_PCM_HW_PARAM_CHANNELS,
SNDRV_PCM_HW_PARAM_RATE,
-1);
if (err < 0)
goto rep_err;
}
//判断是否都是一些常规的采样率。见《usb声卡驱动(四)》
if ((err = snd_usb_pcm_check_knot(runtime, subs)) < 0)
goto rep_err;
return 0;
rep_err:
//电源管理相关,进入suspend状态
snd_usb_autosuspend(subs->stream->chip);
return err;
}
依然是对hw里面的字段就行赋值。没有什么可记录的,一些关键位置见上面的注释
上面是playback的open,接下来看看capture的open,如下:
static int snd_usb_capture_open(struct snd_pcm_substream *substream)
{
return snd_usb_pcm_open(substream, SNDRV_PCM_STREAM_CAPTURE);
}
他们调用到了同一个snd_usb_pcm_open函数中,也是对hw的各种初始化。
虽然上面的函数没有什么可记录,但需要注意一下几点:
- 周期间隔的计算,即在《usb声卡驱动(三)》中谈到的“2的(bInterval-1)次方乘以125us”
- 根据不同的条件,hw里面的值可能会不同,此时使用了《usb声卡驱动(四)》的限制规则。举例如下:
if ((err = snd_pcm_hw_rule_add(runtime, 0, SNDRV_PCM_HW_PARAM_RATE,
hw_rule_rate, subs,
SNDRV_PCM_HW_PARAM_FORMAT,
SNDRV_PCM_HW_PARAM_CHANNELS,
param_period_time_if_needed,
-1)) < 0)
如果应用修改格式,通道,还有周期时间,那么hw_rule_rate函数就会被触发,
它会根据各种要求,进行必要的设置。内部又是对hw字段的各种赋值,也没啥好记录的。
总结就是:open里面是对runtime的hw的正确赋值。
pcm的关闭
有了前面的打开逻辑,现在直接进入substream的close回调中。如下:
static int snd_usb_playback_close(struct snd_pcm_substream *substream)
{
return snd_usb_pcm_close(substream, SNDRV_PCM_STREAM_PLAYBACK);
}
static int snd_usb_capture_close(struct snd_pcm_substream *substream)
{
return snd_usb_pcm_close(substream, SNDRV_PCM_STREAM_CAPTURE);
}
static int snd_usb_pcm_close(struct snd_pcm_substream *substream, int direction)
{
struct snd_usb_stream *as = snd_pcm_substream_chip(substream);
struct snd_usb_substream *subs = &as->substream[direction];
//停止usb端点。分别停止同步端点和数据端点
//至于usb端点如何停止,详见后面关于端点操作的详细介绍
stop_endpoints(subs, true);
if (!as->chip->shutdown && subs->interface >= 0) {
//切换接口的设置为0,因为设置0,表示了0带宽。用于释放usb资源
usb_set_interface(subs->dev, subs->interface, 0);
subs->interface = -1;
}
subs->pcm_substream = NULL;
//当pcm关闭时,电源状态进入一种挂起的状态,暂且不表
snd_usb_autosuspend(subs->stream->chip);
return 0;
}
上面的函数,会去调用stop_endpoint函数,如下:
static void stop_endpoints(struct snd_usb_substream *subs, bool wait)
{
//snd_usb_endpoint_stop停止给定的端点,主要的操作就是将端点的使用计数
//减一,如果使用计数变成0,则unlink所有激活的urb
if (test_and_clear_bit(SUBSTREAM_FLAG_SYNC_EP_STARTED, &subs->flags))
snd_usb_endpoint_stop(subs->sync_endpoint);
if (test_and_clear_bit(SUBSTREAM_FLAG_DATA_EP_STARTED, &subs->flags))
snd_usb_endpoint_stop(subs->data_endpoint);
if (wait) {
//表示等待对应的端点中所有已经提交的队列被处理完成。
snd_usb_endpoint_sync_pending_stop(subs->sync_endpoint);
snd_usb_endpoint_sync_pending_stop(subs->data_endpoint);
}
}
这个函数,根据需要,分别停止,同步端点和数据端点,并等到真正的端点关闭。关于同步端点和数据端点,可见《usb声卡驱动(二)》
各个端点的操作函数的语义,见上面的注释
hw参数设置
在open了一个pcm,并正确的返回了一个流之后。常常通过IOCTL,获取这个pcm相关的硬件信息,然后应用选择不同的配置并设置硬件。
在设置里面,分成了两种:hw,和sw。前者表示硬件参数,后者表示软件参数。
先来看看hw的数据结构用下面表示
struct snd_pcm_hw_params {
unsigned int flags;
struct snd_mask masks[SNDRV_PCM_HW_PARAM_LAST_MASK -
SNDRV_PCM_HW_PARAM_FIRST_MASK + 1];
struct snd_mask mres[5]; /* reserved masks */
struct snd_interval intervals[SNDRV_PCM_HW_PARAM_LAST_INTERVAL -
SNDRV_PCM_HW_PARAM_FIRST_INTERVAL + 1];
struct snd_interval ires[9]; /* reserved intervals */
unsigned int rmask; /* W: requested masks */
unsigned int cmask; /* R: changed masks */
unsigned int info; /* R: Info flags for returned setup */
unsigned int msbits; /* R: used most significant bits */
unsigned int rate_num; /* R: rate numerator */
unsigned int rate_den; /* R: rate denominator */
snd_pcm_uframes_t fifo_size; /* R: chip FIFO size in frames */
unsigned char reserved[64]; /* reserved for future */
};
- flags:目前不知为何用,从源码上面看,跟限制规则有关
- masks:用于描述access、format、subformat这几个参数
- intervals:用于描述内核定义的所有interval参数.
- rmask:用户空间想要修改的参数bit集合。
- cmask:用户空间成功修改的参数bit集合。
- info: 表示硬件的一些参数,跟runtime的info一致,形如:SNDRV_PCM_INFO_XXX
- msbits: 有效位
- rate_num:采样率的分子
- rate_den:采样率的分母
- fifo_size:某些硬件的fifo大小
当应用通过ioctl,传递SNDRV_PCM_IOCTL_HW_PARAMS命令时,驱动根据snd_pcm_hw_params和限制规则(open的时候添加的规则)来决定硬件的一些特性。
当设置硬件参数时,pcm的hw_params回调会被触发.根据《usb声卡驱动(四)》可以知道应该在这个回调中,做一些硬件设置,包括buffer的分配。
接下来看看usb声卡驱动的这个回调,如下:
static int snd_usb_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *hw_params)
{
struct snd_usb_substream *subs = substream->runtime->private_data;
struct audioformat *fmt;
int ret;
//调动alsa提供的buffer分配函数
//注意不要自己分配函数,因为该回调可能会被调用多次,如果自己修改需要注意内存的泄漏
//同时,分配的内存需要在hw_free回调中进行释放
ret = snd_pcm_lib_alloc_vmalloc_buffer(substream,
params_buffer_bytes(hw_params));
if (ret < 0)
return ret;
//设置各种硬件参数
subs->pcm_format = params_format(hw_params);
subs->period_bytes = params_period_bytes(hw_params);
subs->period_frames = params_period_size(hw_params);
subs->buffer_periods = params_periods(hw_params);
subs->channels = params_channels(hw_params);
subs->cur_rate = params_rate(hw_params);
fmt = find_format(subs);
if (!fmt) {
dev_dbg(&subs->dev->dev,
"cannot set format: format = %#x, rate = %d, channels = %d\n",
subs->pcm_format, subs->cur_rate, subs->channels);
return -EINVAL;
}
down_read(&subs->stream->chip->shutdown_rwsem);
//如果正处于shutdown状态则返回,否则继续设置参数
if (subs->stream->chip->shutdown)
ret = -ENODEV;
else
ret = set_format(subs, fmt);
up_read(&subs->stream->chip->shutdown_rwsem);
if (ret < 0)
return ret;
//need_setup_ep表示要配置usb使用的端点
//每一个端点在使用之前,都需要先设置参数,而每一次的参数修改,都需要重新配置usb端点
subs->interface = fmt->iface;
subs->altset_idx = fmt->altset_idx;
subs->need_setup_ep = true;
return 0;
}
上面的函数,都是对subs的赋值。其中需要注意的是snd_pcm_lib_alloc_vmalloc_buffer函数。
它分配这个substream的buffer。它的内容如下:
#define snd_pcm_lib_alloc_vmalloc_buffer(subs, size) \
_snd_pcm_lib_alloc_vmalloc_buffer \
(subs, size, GFP_KERNEL | __GFP_HIGHMEM | __GFP_ZERO)
int _snd_pcm_lib_alloc_vmalloc_buffer(struct snd_pcm_substream *substream,
size_t size, gfp_t gfp_flags)
{
struct snd_pcm_runtime *runtime;
if (PCM_RUNTIME_CHECK(substream))
return -EINVAL;
runtime = substream->runtime;
//重复调用的情况下,释放上一次分配的内存
if (runtime->dma_area) {
if (runtime->dma_bytes >= size)
return 0; /* already large enough */
vfree(runtime->dma_area);
}
//将分配的内存起始地址保存在dma_area,后面有用处
runtime->dma_area = __vmalloc(size, gfp_flags, PAGE_KERNEL);
if (!runtime->dma_area)
return -ENOMEM;
//分配的内存的大小
runtime->dma_bytes = size;
return 1;
}
这个函数调用__vmalloc函数,在常规内存,或者高端内存中分配buffer,注意,这里面没有DMA区域。
分配完成之后,将地址,赋值给runtime的dma_area.
虽然,这里的名字叫dma_area,但它并没有用到dma内存。
总结:hw_params回调,进行进一步的初始化,并分配buffer
sw参数设置
设置完了硬件参数以后,接下来可以设置软件参数了。来看看软件参数的数据结构
struct snd_pcm_sw_params {
int tstamp_mode; /* timestamp mode */
unsigned int period_step;
unsigned int sleep_min; /* min ticks to sleep */
snd_pcm_uframes_t avail_min; /* min avail frames for wakeup */
snd_pcm_uframes_t xfer_align; /* obsolete: xfer size need to be a multiple */
snd_pcm_uframes_t start_threshold; /* min hw_avail frames for automatic start */
snd_pcm_uframes_t stop_threshold; /* min avail frames for automatic stop */
snd_pcm_uframes_t silence_threshold; /* min distance from noise for silence filling */
snd_pcm_uframes_t silence_size; /* silence block size */
snd_pcm_uframes_t boundary; /* pointers wrap point */
unsigned int proto; /* protocol version */
unsigned int tstamp_type; /* timestamp type (req. proto >= 2.0.12) */
unsigned char reserved[56]; /* reserved for future */
};
- tstamp_mode:值为下面
enum {
SNDRV_PCM_TSTAMP_NONE = 0,
SNDRV_PCM_TSTAMP_ENABLE,
SNDRV_PCM_TSTAMP_LAST = SNDRV_PCM_TSTAMP_ENABLE,
};
表示是否启用时间戳
2. period_step:未见其用处,不知道有何用,查看源码也没弄明白,暂时先放放
3. sleep_min:未见其用处
4. avail_min:最小可用帧。到达这个之后,唤醒等待中的线程
5. xfer_align:已过时,也未见其用处
6. start_threshold:开始运行的阈值。
7. stop_threshold:开始停止的阈值
8. silence_threshold:xrun状态下,使用静默数据填充的阈值
9. silence_size:silence数据块大小
10. boundary:buffer的边界
11. proto:protocol 版本
12. tstamp_type:时间戳类型,值为
enum {
SNDRV_PCM_TSTAMP_TYPE_GETTIMEOFDAY = 0, /* gettimeofday equivalent */
SNDRV_PCM_TSTAMP_TYPE_MONOTONIC, /* posix_clock_monotonic equivalent */
SNDRV_PCM_TSTAMP_TYPE_MONOTONIC_RAW, /* monotonic_raw (no NTP) */
SNDRV_PCM_TSTAMP_TYPE_LAST = SNDRV_PCM_TSTAMP_TYPE_MONOTONIC_RAW,
};
表示,runtime里面时间戳的模式。各个模式的定义,见上面的注释
应用程序使用ioctl,传递SNDRV_PCM_IOCTL_SW_PARAMS命令,将用户空间的参数,设置进驱动。驱动将这些值,设置到对应的substream的runtime里面。
这个设置并没有对应的回调会被触发。
参数相关的释放
在上面的参数设置中。可以看到有两个主要方向:
- 修改runtime中的各种各样的值
- 分配buffer
runtime中的值随,runtime的释放而释放。runtime的产生在open时刻,因此,runtime的释放,应该在release时刻,注意并不是close时刻。runtime是由alsa分配的,相应的释放也由alsa管理,我们不讨论其细节。
相应的buffer分配在hw_params时刻;那么buffer的释放就应该在hw_free时刻。这个buffer由usb声卡驱动分配,现在看一下它的释放细节,如下:
static int snd_usb_hw_free(struct snd_pcm_substream *substream)
{
struct snd_usb_substream *subs = substream->runtime->private_data;
subs->cur_audiofmt = NULL;
subs->cur_rate = 0;
subs->period_bytes = 0;
down_read(&subs->stream->chip->shutdown_rwsem);
if (!subs->stream->chip->shutdown) {
//端点的引用计数减一。为0时,unlink所有的urb
stop_endpoints(subs, true);
snd_usb_endpoint_deactivate(subs->sync_endpoint);
snd_usb_endpoint_deactivate(subs->data_endpoint);
}
up_read(&subs->stream->chip->shutdown_rwsem);
//释放hw_params中分配的内存
return snd_pcm_lib_free_vmalloc_buffer(substream);
}
主要的资源释放在snd_pcm_lib_free_vmalloc_buffer,即释放由snd_pcm_lib_alloc_vmalloc_buffer分配的buffer
至此,已经翻阅完了,pcm设备的参数设置和释放
接下来,应该进入一个关键的部分。以playback为例,先查看prepare。然后再查看开始,暂停,停止的操作。同时查看,端点的操作,以及数据传输情况。
下篇见