USB总线-Linux内核USB设备驱动之UAC2驱动分析(十)

本文详细介绍了UAC2.0(USB Audio Class 2.0)协议,包括其在Linux内核中的实现原理、驱动初始化配置、数据传输过程及工作流程等内容。UAC2.0相比UAC1.0提供了更多功能,支持更高带宽,更低延迟。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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_installoc_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_epout_ep表示USB设备输入和输出端点,in_ep_maxpsizeout_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数据结构关系

uac2驱动通过configfs的配置过程如下图所示,创建functions调用uac2驱动的afunc_alloc_inst函数,关联functions和配置时调用uac2驱动的afunc_alloc,使能gadget设备调用uac2驱动的afunc_bind函数,下面分析这三个函数的执行过程。

配置

afunc_alloc_instafunc_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用于设置描述符、端点、配置、注册声卡,主要的工作内容如下:

  1. 设置描述符的字符串索引值、初始化描述符中的配置参数。
  2. 设置接口描述符的编号,ac_intf=0,as_out_intf=1,as_in_intf=2。设置各个接口的alt值为0。
  3. 根据音频设备所需的带宽计算端点的最大包长。
  4. 根据端点描述符,匹配要使用的端点,同时再描述符中记录端点的地址。
  5. 处理描述符。
  6. 调用g_audio_setup函数创建音频设备。
    1. 分配uac请求和USB请求缓冲区,请求默认分配2个,缓冲区长度为端点的最大包长
    2. 创建声卡(包含声卡控制设备),一个声卡只有一个控制设备。
    3. 创建PCM子流和PCM设备。子流包含两类,分别为capture和playback,每个类下面又包含多个子流,子流是PCM设备功能的实现。
    4. 设置子流的操作函数为uac_pcm_ops,应用层要访问音频设备,最终会调用到uac_pcm_ops
    5. 分配DMA缓冲区,底层最终通过调用__get_free_pages分配。
    6. 注册声卡。声卡中包含很多设备,如控制设备、PCM设备、混音设备等,内核将不同的设备统一抽象成snd_device,最终通过snd_register_device注册。控制设备操作函数集合为snd_ctl_f_ops,PCM设备操作函数集合为snd_pcm_f_ops

afunc_bind

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_STATUSUSB_REQ_CLEAR_FEATURE等,USB设备控制器驱动就可以处理,但有一些标准的USB请求,如USB_REQ_GET_DESCRIPTOR,需要USB gadget驱动参与处理,还有一些USB请求,需要function驱动参与处理。如下图所示,当主机发送USB_REQ_GET_CONFIGURATIONUSB_REQ_SET_INTERFACE请求时,需要调用uac2驱动的afunc_set_alt函数处理,当主机发送USB_REQ_GET_INTERFACE请求时,需要调用afunc_get_alt函数处理,其他USB类请求命令,调用afunc_setup处理。

枚举

UAC2设备被枚举的过程如下(这里只说明uac2驱动参与处理的部分):

  1. 设置配置
    主机发送USB_REQ_GET_CONFIGURATION命令设置设备当前使用的配置。uac2驱动只有一个配置,因此只需要调用afunc_set_alt将配置下面所有接口的alt值设置为0。afunc_set_alt函数的执行流程如下图所示。若是音频控制接口,alt=0时,直接返回0,其他值直接报错;若是音频流输出接口,alt=0时,停止录音,alt=1时,开始录音;若是音频流输入接口,alt=0时,停止播放,alt=1时,开始播放。

afunc_set_alt

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;
}
  1. 设置接口
    主机发送USB_REQ_SET_INTERFACE命令设置设备接口。调用afunc_set_altas_in_intfas_out_intf接口的alt值设置为0。
  2. 发送USB类请求命令
    USB类请求命令需要调用afunc_setup处理。该函数的执行流程如下图所示。实际工作过程中,主机通过该函数获取录音或播放的采样频率,而录音或播放的通道数已经包含在描述符中,不需要额外获取。

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驱动是生产者,用户是消费者,播放时则相反。

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__);
}

参考资料

  1. Rockchip RK3399TRM V1.3 Part1
  2. Rockchip RK3399TRM V1.3 Part2
  3. Linux内核4.1版本源码
  4. UNIVERSAL SERIAL BUS DEVICE CLASS DEFINITION FOR AUDIO DEVICES Release 3.0-Errata
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值