1.概述
UAC(USB Audio Class)定义了使用USB协议播放或采集音频数据的设备应当遵循的规范。目前,UAC协议有UAC1.0和UAC2.0。UAC2.0协议相比UAC1.0协议,提供了更多的功能,支持更高的带宽,拥有更低的延迟。Linux内核中包含了UAC1.0和UAC2.0驱动,分别在f_uac1.c和f_uac2.c文件中实现。下面将以UAC2驱动为例,具体分析USB设备驱动的初始化、描述符配置、数据传输过程等。
2.初始化
2.1.定义
下面是UAC2.0的Gadget Function驱动的定义,驱动名称为uac2。alloc_inst
被设置为afunc_alloc_inst
,alloc_func
被设置为afunc_alloc
,这两个函数在Gadget Function API层被回调。该宏将定义一个usb_function_driver
数据结构,使用usb_function_register
函数注册到function API层。
[drivers/usb/gadget/function/f_uac2.c]
// 定义UAC设备驱动
DECLARE_USB_FUNCTION_INIT(uac2, afunc_alloc_inst, afunc_alloc);
2.2.配置
uac2驱动的数据结构关系如下图所示。struct f_uac2
表示uac2设备,由afunc_alloc
分配,包含了具体音频设备和USB配置信息。如g_audio
表示音频设备,包含了音频运行时参数、声卡、PCM设备等信息,uac_pcm_ops
表示音频PCM设备流的操作方法,in_ep
和out_ep
表示USB设备输入和输出端点,in_ep_maxpsize
和out_ep_maxpsize
表示输入端点和输出端点数据包最大长度,params
表示音频设备参数。比较重要的是func
,描述了USB设备功能,uac2设备驱动需要填充该数据结构。struct f_uac2_opts
表示uac2设备的选项,由afunc_alloc_inst
动态分配,内部嵌入了struct usb_function_instance
数据结构,表示一个USB Function实例,内部的fd
指针指向DECLARE_USB_FUNCTION_INIT
宏定义的uac2设备驱动结构体。
[drivers/usb/gadget/function/f_uac2.c]
struct f_uac2 {
struct g_audio g_audio; // uac2音频设备数据结构
/* ac_intf - audio control interface,接口描述符编号为0
* as_in_intf - audio streaming in interface,接口描述符编号为2
* as_out_intf - audio streaming out interface,接口描述符编号为1
*/
u8 ac_intf, as_in_intf, as_out_intf;
/* 上述三个接口描述符当前的alternate settings值,有两个值,0表示关闭,1表示使能,
* 主机可使用USB_REQ_GET_INTERFACE命令获取接口描述符当前的alternate settings值 */
u8 ac_alt, as_in_alt, as_out_alt; /* needed for get_alt() */
};
[drivers/usb/gadget/function/u_uac2.h]
struct f_uac2_opts {
struct usb_function_instance func_inst;
int p_chmask; // 录音通道掩码
int p_srate; // 录音采样率
int p_ssize; // 录音一帧数据占多少字节
int c_chmask; // 播放通道掩码
int c_srate; // 播放采样率
int c_ssize; // 播放一帧数据占多少字节
int req_number; // usb_request的数量
bool bound;
struct mutex lock;
int refcnt; // 引用计数
};
uac2驱动通过configfs的配置过程如下图所示,创建functions调用uac2驱动的afunc_alloc_inst
函数,关联functions和配置时调用uac2驱动的afunc_alloc
,使能gadget设备调用uac2驱动的afunc_bind
函数,下面分析这三个函数的执行过程。
afunc_alloc_inst
和afunc_alloc
是实现Gadget Function驱动的基础。afunc_alloc_inst
创建usb_function_instance
实例,设置uac2.0协议相关内容。afunc_alloc
创建usb_function
,设置uac2.0驱动的回调函数。具体的执行过程参考USB总线-Linux内核USB3.0设备控制器复合设备之USB gadget configfs分析(七)文章3.2节。
afunc_bind
用于设置描述符、端点、配置、注册声卡,主要的工作内容如下:
- 设置描述符的字符串索引值、初始化描述符中的配置参数。
- 设置接口描述符的编号,ac_intf=0,as_out_intf=1,as_in_intf=2。设置各个接口的alt值为0。
- 根据音频设备所需的带宽计算端点的最大包长。
- 根据端点描述符,匹配要使用的端点,同时再描述符中记录端点的地址。
- 处理描述符。
- 调用
g_audio_setup
函数创建音频设备。- 分配uac请求和USB请求缓冲区,请求默认分配2个,缓冲区长度为端点的最大包长
- 创建声卡(包含声卡控制设备),一个声卡只有一个控制设备。
- 创建PCM子流和PCM设备。子流包含两类,分别为capture和playback,每个类下面又包含多个子流,子流是PCM设备功能的实现。
- 设置子流的操作函数为
uac_pcm_ops
,应用层要访问音频设备,最终会调用到uac_pcm_ops
。 - 分配DMA缓冲区,底层最终通过调用
__get_free_pages
分配。 - 注册声卡。声卡中包含很多设备,如控制设备、PCM设备、混音设备等,内核将不同的设备统一抽象成
snd_device
,最终通过snd_register_device
注册。控制设备操作函数集合为snd_ctl_f_ops,PCM设备操作函数集合为snd_pcm_f_ops。
afunc_bind
函数注册的PCM子流的操作函数集合uac_pcm_ops
、控制设备操作函数集合为snd_ctl_f_ops
,PCM设备操作函数集合为snd_pcm_f_ops
如下所示。snd_pcm_f_ops
函数最终会调用到uac_pcm_ops
,具体调用过程后面分析。
[drivers/usb/gadget/function/u_audio.c]
// pcm流的操作函数
static const struct snd_pcm_ops uac_pcm_ops = {
.open = uac_pcm_open,
.close = uac_pcm_null,
.ioctl = snd_pcm_lib_ioctl,
.hw_params = uac_pcm_hw_params,
.hw_free = uac_pcm_hw_free,
.trigger = uac_pcm_trigger,
.pointer = uac_pcm_pointer,
.prepare = uac_pcm_null,
};
[sound/core/control.c]
static const struct file_operations snd_ctl_f_ops =
{ // SNDRV_DEVICE_TYPE_CONTROL设备操作函数
.owner = THIS_MODULE,
.read = snd_ctl_read,
.open = snd_ctl_open,
.release = snd_ctl_release,
.llseek = no_llseek,
.poll = snd_ctl_poll,
.unlocked_ioctl = snd_ctl_ioctl,
.compat_ioctl = snd_ctl_ioctl_compat,
.fasync = snd_ctl_fasync,
};
[sound/core/pcm_native.c]
const struct file_operations snd_pcm_f_ops[2] = {
{ // SNDRV_PCM_STREAM_PLAYBACK操作函数
.owner = THIS_MODULE,
.write = snd_pcm_write,
.write_iter = snd_pcm_writev,
.open = snd_pcm_playback_open,
.release = snd_pcm_release,
.llseek = no_llseek,
.poll = snd_pcm_playback_poll,
.unlocked_ioctl = snd_pcm_playback_ioctl,
.compat_ioctl = snd_pcm_ioctl_compat,
.mmap = snd_pcm_mmap,
.fasync = snd_pcm_fasync,
.get_unmapped_area = snd_pcm_get_unmapped_area,
},
{ // SNDRV_PCM_STREAM_CAPTURE操作函数
.owner = THIS_MODULE,
.read = snd_pcm_read,
.read_iter = snd_pcm_readv,
.open = snd_pcm_capture_open,
.release = snd_pcm_release,
.llseek = no_llseek,
.poll = snd_pcm_capture_poll,
.unlocked_ioctl = snd_pcm_capture_ioctl,
.compat_ioctl = snd_pcm_ioctl_compat,
.mmap = snd_pcm_mmap,
.fasync = snd_pcm_fasync,
.get_unmapped_area = snd_pcm_get_unmapped_area,
}
};
3.枚举
USB设备的枚举过程在第9章已经介绍过了,主要涉及USB设备端点0控制传输的3个过程。USB设备的枚举实质上是响应USB主机发送请求的过程。对于一些标准的USB请求,如USB_REQ_GET_STATUS
、USB_REQ_CLEAR_FEATURE
等,USB设备控制器驱动就可以处理,但有一些标准的USB请求,如USB_REQ_GET_DESCRIPTOR
,需要USB gadget驱动参与处理,还有一些USB请求,需要function驱动参与处理。如下图所示,当主机发送USB_REQ_GET_CONFIGURATION
或USB_REQ_SET_INTERFACE
请求时,需要调用uac2驱动的afunc_set_alt
函数处理,当主机发送USB_REQ_GET_INTERFACE
请求时,需要调用afunc_get_alt
函数处理,其他USB类请求命令,调用afunc_setup
处理。
UAC2设备被枚举的过程如下(这里只说明uac2驱动参与处理的部分):
- 设置配置
主机发送USB_REQ_GET_CONFIGURATION
命令设置设备当前使用的配置。uac2驱动只有一个配置,因此只需要调用afunc_set_alt
将配置下面所有接口的alt值设置为0。afunc_set_alt
函数的执行流程如下图所示。若是音频控制接口,alt=0时,直接返回0,其他值直接报错;若是音频流输出接口,alt=0时,停止录音,alt=1时,开始录音;若是音频流输入接口,alt=0时,停止播放,alt=1时,开始播放。
uac2设备枚举的使用没有发送USB_REQ_GET_INTERFACE
命令,获取当前接口的alt值时。但还是介绍下afunc_get_alt
函数,该函数直接返回当前接口的alt值。
[drivers/usb/gadget/function/f_uac2.c]
static int afunc_get_alt(struct usb_function *fn, unsigned intf)
{
struct f_uac2 *uac2 = func_to_uac2(fn);
struct g_audio *agdev = func_to_g_audio(fn);
if (intf == uac2->ac_intf)
return uac2->ac_alt;
else if (intf == uac2->as_out_intf)
return uac2->as_out_alt;
else if (intf == uac2->as_in_intf)
return uac2->as_in_alt;
else
......
return -EINVAL;
}
- 设置接口
主机发送USB_REQ_SET_INTERFACE
命令设置设备接口。调用afunc_set_alt
将as_in_intf
和as_out_intf
接口的alt值设置为0。 - 发送USB类请求命令
USB类请求命令需要调用afunc_setup
处理。该函数的执行流程如下图所示。实际工作过程中,主机通过该函数获取录音或播放的采样频率,而录音或播放的通道数已经包含在描述符中,不需要额外获取。
下面是UAC2协议中的名词,记录一下。
- Current setting attribute (CUR)
- Range attribute (RANGE)
- Interrupt Enable attribute (INTEN)
- Control Selector (CS)
- Channel Number (CN)
- Mixer Control Number (MCN)
4.工作过程分析
USB主机发送USB_REQ_SET_INTERFACE
命令时,uac2驱动将会调用afunc_set_alt
函数,若intf=2,alt=1,则开始录音,若intf=1,alt=1,则开始播放。下图是USB音频设备工作时数据流的传输过程。录音(capture)时,USB主机控制器向USB设备控制器发送音频数据,USB设备控制器收到以后通过DMA将其写入到usb_request的缓冲区中,随后再拷贝到DMA缓冲区中,用户可使用arecord、tinycap等工具从DMA缓冲区中读取音频数据,DMA缓冲区是一个FIFO,uac2驱动往里面填充数据,用户从里面读取数据。播放(playback)时,用户通过aplay、tinyplay等工具将音频数据写道DMA缓冲区中,uac2驱动从DMA缓冲区中读取数据,然后构造成usb_request,送到USB设备控制器,USB设备控制器再将音频数据发送到USB主机控制器。可以看出录音和播放的音频数据流方向相反,用户和uac2驱动构造了一个生产者和消费者模型,录音时,uac2驱动是生产者,用户是消费者,播放时则相反。
DMA缓冲区和USB设备控制器的数据交换都由u_audio_iso_complete
函数完成。录音时,将DMA缓冲区中的数据拷贝到usb_request缓冲区,播放时,将usb_request缓冲区中的数据拷贝到DMA缓冲区中,最后将usb_request填充到端点的队列中。如此循环往复。
[drivers/usb/gadget/function/u_audio.c]
static void u_audio_iso_complete(struct usb_ep *ep, struct usb_request *req)
{
......
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
/*
* For each IN packet, take the quotient of the current data
* rate and the endpoint's interval as the base packet size.
* If there is a residue from this division, add it to the
* residue accumulator.
*/
req->length = uac->p_pktsize;
uac->p_residue += uac->p_pktsize_residue;
/*
* Whenever there are more bytes in the accumulator than we
* need to add one more sample frame, increase this packet's
* size and decrease the accumulator.
*/
if (uac->p_residue / uac->p_interval >= uac->p_framesize) {
req->length += uac->p_framesize;
uac->p_residue -= uac->p_framesize * uac->p_interval;
}
req->actual = req->length;
}
......
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
// 录音时,将DMA缓冲区中的数据拷贝到usb_request缓冲区中
if (unlikely(pending < req->actual)) { // 处理DMA缓冲区回绕
memcpy(req->buf, runtime->dma_area + hw_ptr, pending);
memcpy(req->buf + pending, runtime->dma_area,
req->actual - pending);
} else {
memcpy(req->buf, runtime->dma_area + hw_ptr, req->actual);
}
} else {
// 播放时,将usb_request缓冲区中的数据拷贝到DMA缓冲区中
if (unlikely(pending < req->actual)) { // 处理DMA缓冲区回绕
memcpy(runtime->dma_area + hw_ptr, req->buf, pending);
memcpy(runtime->dma_area, req->buf + pending, req->actual - pending);
} else {
memcpy(runtime->dma_area + hw_ptr, req->buf, req->actual);
}
}
......
if ((hw_ptr % snd_pcm_lib_period_bytes(substream)) < req->actual)
snd_pcm_period_elapsed(substream); // 更新PCM设备信息,如DMA缓冲区状态
exit:
// 将usb_request重新填充到端点队列中,重复利用
if (usb_ep_queue(ep, req, GFP_ATOMIC))
dev_err(uac->card->dev, "%d Error!\n", __LINE__);
}
参考资料
- Rockchip RK3399TRM V1.3 Part1
- Rockchip RK3399TRM V1.3 Part2
- Linux内核4.1版本源码
- UNIVERSAL SERIAL BUS DEVICE CLASS DEFINITION FOR AUDIO DEVICES Release 3.0-Errata