您的位置:首页 > 运维架构 > Linux

Linux那些事儿 之 戏说USB(21)设备的生命线(四)

2015-03-25 16:40 477 查看
继续urb

urb_list,还记得每个端点都会有的那个urb队列么?那个队列就是由这里的urb_list一个一个的链接起来的。HCD每收到一个urb,就会将它添加到这个urb指定的那个端点的urb队列里去。这个链表的头儿在哪儿?当然是在端点里,就是端点里的那个struct list_head结构体成员。

dev,傻强都知道,它表示的是urb要去的那个usb设备。

pipe,urb到达端点之前,需要经过一个通往端点的管道,就是这个pipe。那第一个问题,怎么表示一个pipe?人生有两极,管道有两端,一端是主机上的缓冲区,一端是设备上的端点,既然有两端,总要有个方向吧,不然urb要待在管道里无所适从的仰天长叹,我从哪里来,又该往哪里去?而且早先说过,端点有四种类型,那么与端点相生相依的管道也应该不只一种吧。这么说来,确定一条管道至少要知道两端的地址、方向和类型了,不过这两端里主机是确定的,需要确定的只是另一端设备的地址和端点的地址。那怎么将这些内容揉合起来表示成一个管道?一个包含了各种成员属性的结构再加上一些操作函数?多么完美的封装,但是不需要这么搞,写代码的哥们儿和俺的人生信条差不多,复杂简单化,一个整型值再加上一些宏就够了。

先看看管道,也就是这个整型值的构成,bit7用来表示方向,bit8~14表示设备地址,bit15~18表示端点号,早先说过,设备地址用7位来表示,端点号用4位来表示,剩下来的bit30~31表示管道类型。再看看围绕管道的一些宏,在include/linux/usb.h里定义

#define PIPE_ISOCHRONOUS		0
#define PIPE_INTERRUPT			1
#define PIPE_CONTROL			2
#define PIPE_BULK			3

#define usb_pipein(pipe)	((pipe) & USB_DIR_IN)
#define usb_pipeout(pipe)	(!usb_pipein(pipe))

#define usb_pipedevice(pipe)	(((pipe) >> 8) & 0x7f)
#define usb_pipeendpoint(pipe)	(((pipe) >> 15) & 0xf)

#define usb_pipetype(pipe)	(((pipe) >> 30) & 3)
#define usb_pipeisoc(pipe)	(usb_pipetype((pipe)) == PIPE_ISOCHRONOUS)
#define usb_pipeint(pipe)	(usb_pipetype((pipe)) == PIPE_INTERRUPT)
#define usb_pipecontrol(pipe)	(usb_pipetype((pipe)) == PIPE_CONTROL)
#define usb_pipebulk(pipe)	(usb_pipetype((pipe)) == PIPE_BULK)
这些宏什么意思,就不用我说了,不知道的都可以去跳浩然高科了。我可是亲眼目睹过这样的惨状,触目惊心啊,在我交大的历史记忆里抹上了浓重的一笔。

现在看第二个问题,如何创建一个管道?主机和设备不是练家子,没练过千里传音什么的绝世神功,要交流必须通过管道,你必须得创建一个管道给urb,它才知道路怎么走。

内核的include/linux/usb.h文件里有很多专门用来创建不同管道的宏。

static inline unsigned int __create_pipe(struct usb_device *dev,
		unsigned int endpoint)
{
	return (dev->devnum << 8) | (endpoint << 15);
}

/* Create various pipes... */
#define usb_sndctrlpipe(dev, endpoint)	\
	((PIPE_CONTROL << 30) | __create_pipe(dev, endpoint))
#define usb_rcvctrlpipe(dev, endpoint)	\
	((PIPE_CONTROL << 30) | __create_pipe(dev, endpoint) | USB_DIR_IN)
#define usb_sndisocpipe(dev, endpoint)	\
	((PIPE_ISOCHRONOUS << 30) | __create_pipe(dev, endpoint))
#define usb_rcvisocpipe(dev, endpoint)	\
	((PIPE_ISOCHRONOUS << 30) | __create_pipe(dev, endpoint) | USB_DIR_IN)
#define usb_sndbulkpipe(dev, endpoint)	\
	((PIPE_BULK << 30) | __create_pipe(dev, endpoint))
#define usb_rcvbulkpipe(dev, endpoint)	\
	((PIPE_BULK << 30) | __create_pipe(dev, endpoint) | USB_DIR_IN)
#define usb_sndintpipe(dev, endpoint)	\
	((PIPE_INTERRUPT << 30) | __create_pipe(dev, endpoint))
#define usb_rcvintpipe(dev, endpoint)	\
	((PIPE_INTERRUPT << 30) | __create_pipe(dev, endpoint) | USB_DIR_IN)
端点是有四种的,对应着管道也就有四种,同时端点是有IN也有OUT的,相应的管道也就有两个方向,于是二四得八,上面就出现了八个创建管道的宏。有了struct usb_device结构体,也就是说知道了设备地址,再加上端点号,你就可以需要什么管道就创建什么管道。__create_pipe宏只是一个幕后的角色,用来将设备地址和端点号放在管道正确的位置上。

status,urb的当前状态。urb当然是可以有多种状态的,像咱们这种除了三点一线就是三点一线的小角色都被码上了各种各样的状态,何况usb通信的顶梁柱urb。

transfer_flags,一些标记,可用的值都在include/linux/usb.h里有定义

#define URB_SHORT_NOT_OK	0x0001	/* report short reads as errors */
#define URB_ISO_ASAP		0x0002	/* iso-only; use the first unexpired
					 * slot in the schedule */
#define URB_NO_TRANSFER_DMA_MAP	0x0004	/* urb->transfer_dma valid on submit */
#define URB_NO_FSBR		0x0020	/* UHCI-specific */
#define URB_ZERO_PACKET		0x0040	/* Finish bulk OUT with short packet */
#define URB_NO_INTERRUPT	0x0080	/* HINT: no non-error interrupt
					 * needed */
URB_SHORT_NOT_OK,这个标记只对用来从IN端点读取数据的urb有效,意思就是说如果从一个IN端点那里读取了一个比较短的数据包,就可以认为是错误的。那么这里的short究竟short到什么程度?

之前说到端点的时候,就知道端点描述符里有一个叫wMaxPacketSize这样的东东,指明了端点一次能够处理的最大字节数。然后,在另外某个地方也提了,在usb的世界里是有很多种包的,四种PID类型,每种PID下边儿还有一些细分的品种。这四种PID里面,有一个叫Data的,也只有它里边儿有个数据字段,像其它的,Token、Handshake之类的PID类型都是没有这个字段的,所以里里外外看过去,还只有Data PID类型的包最实在,就是用来传输数据的,但是它里面并不是只有一个数据字段,还有SYNC、PID、地址域、CRC等陪伴在数据字段的左右。镜头向前回退了这么多,那现在一个问题出来了,每个端点描述符里的wMaxPacketSize所表示的最大字节数都包括了哪些部分?是整个packet的长度么?我可以负责任的告诉你,它只包括了Data包里面数据字段,俗称data
payload,其它那些七大姑八大姨什么的都是协议本身需要的信息,和TCP/IP里的报头差不多。

wMaxPacketSize与short有什么关系?关系还不小,short不short就是与wMaxPacketSize相比的,如果从IN端点那儿收到了一个比wMaxPacketSize要短的包,同时也设置了URB_SHORT_NOT_OK这个标志,那么就可以认为传输出错了。本来如果收到一个比较短的包是意味着这次传输到此为止就结束了,你想想data payload的长度最大必须为wMaxPacketSize这个规定是不可违背的了,但是如果端点想给你的数据不止那么多,怎么办?就需要分成多个wMaxPacketSize大小的data
payload来传输,事情有时不会那么凑巧,刚好能平分成多个整份,这时,最后一个data payload的长度就会比wMaxPacketSize要小,这种情况本来意味着端点已经传完了它想传的,释放完了自己的需求,这次传输就该结束了,不过如果你设置了URB_SHORT_NOT_OK标志,HCD这边就会认为错误发生了。

URB_ISO_ASAP,这个标志只是为了方便等时传输用的。等时传输和中断传输在spec里都被认为是periodic transfers,也就是周期传输,咱们都知道在usb的世界里都是主机占主导地位,设备是没多少发言权的,但是对于等时传输和中断传输,端点可以对主机表达自己一种美好的期望,希望主机能够隔多长时间访问自己一次,这个期望的时间就是这里说的周期。当然,期望与现实是有一段距离的,如果期望的都能成为现实,咱们还研究usb干吗。端点的这个期望能不能得到满足,要看主机控制器答应不答应。对于等时传输,一般来说也就一帧(微帧)一次,主机那儿也很忙,再多也抽不出空儿来。那么如果你有个用于等时传输的urb,你提交给HCD的时候,就得告诉HCD它应该从哪一帧开始的,就要对下面要说的那个start_frame赋值,也就是说告诉HCD等时传输开始的那一帧(微帧)的帧号,如果你留心,应该还会记得前面说过在每帧或微帧(Mircoframe)的开始都会有个SOF
Token包,这个包里就含有个帧号字段,记录了那一帧的编号。这样的话,一是比较烦,作为一个男人,烦心的事儿已经够多了,钞票一天比一天难赚,前方怎么也看不到岸,还要去设置这个start_frame,你说烦不烦,二是到你设置的那一帧的时候,如果主机控制器没空开始等时传输,你说怎么办,要知道usb的世界里它可是老大。于是,就出现了URB_ISO_ASAP,它的意思就是告诉HCD啥时候不忙就啥时候开始,就不用指定什么开始的帧号了,是不是感觉特轻松?所以说,你如果想进行等时传输,又不想标新立异的话,就还是把它给设置了吧。

URB_NO_TRANSFER_DMA_MAP,还有URB_NO_SETUP_DMA_MAP,这两个标志都是有关DMA的,什么是DMA?就是外设,比如咱们的usb摄像头,和内存之间直接进行数据交换,把CPU给撇一边儿了,本来,在咱们的电脑里,CPU自认为是老大,什么事都要去插一脚,都要经过它去协调处理,可是这样的话就影响了数据传输的速度,就像革命青年上山下乡那会儿,谁对哪个mm有意思了,要先向自己的老爸老妈汇报思想动态,说想和哪家姑娘处对象,等着老爸老妈经过高层协商说可以交往了,然后才能和人家姑娘见面,这多慢啊,哪像现在,老爸老妈都不知道那,下一代可能都已经培育出来了,有DMA和没有DMA区别就是这么大。

usb的世界里也是要与时俱进,要创建和谐社会的,所以dma也是少不了的。一般来说,都是驱动里提供了kmalloc等分配的缓冲区,HCD做一定的DMA映射处理,DMA映射是干吗的?外设和内存之间进行数据交换,总要互相认识吧,外设是通过各种总线连到主机里边儿的,使用的是总线地址,而内存使用的是虚拟地址,它们之间本来就是两条互不相交的平行线,要让它们中间产生连接点,必须得将一个地址转化为另一个地址,这样才能找得到对方,才能互通有无,而DMA映射就是干这个的。这只是轻描淡写三言两语的粗略说法,实际上即使千言万语也道不完的。它可是高技术含量的活儿,所以在某些平台上非常的费时费力,为了分担点HCD的压力,于是就有了这里的两个标志,告诉HCD不要再自己做DMA映射了,驱动提供的urb里已经提供有DMA缓冲区地址。具体提供了哪些DMA缓冲区?就涉及到下面的transfer_buffer,transfer_dma,还有setup_packet,setup_dma这两对儿了。

URB_NO_FSBR,这是给UHCI用的。

URB_ZERO_PACKET,这个标志表示批量的OUT传输必须使用一个short packet来结束。批量传输的数据大于批量端点的wMaxPacketSize时,需要分成多个Data包来传输,最后一个data payload的长度可能等于wMaxPacketSize,也可能小于,当等于wMaxPacketSize时,如果同时设置了URB_ZERO_PACKET标志,就需要再发送一个长度为0的数据包来结束这次传输,如果小于wMaxPacketSize就没必要多此一举了。你要问,当批量传输的数据小于wMaxPacketSize时那?也没必要再发送0长的数据包,因为此时发送的这个数据包本身就是一个short
packet。

URB_NO_INTERRUPT,这个标志用来告诉HCD,在URB完成后,不要请求一个硬件中断,当然这就意味着你的结束处理函数可能不会在urb完成后立即被调用,而是在之后的某个时间被调用,咱们的usb core会保证为每个urb调用一次结束处理函数。

transfer_buffer,transfer_dma,transfer_buffer_length,前面说过管道的一端是主机上的缓冲区,一端是设备上的端点,这三个家伙就是描述主机上的那个缓冲区的。transfer_buffer是使用kmalloc分配的缓冲区,transfer_dma是使用usb_buffer_alloc分配的dma缓冲区,HCD不会同时使用它们两个,如果你的urb自带了transfer_dma,就要同时设置URB_NO_TRANSFER_DMA_MAP来告诉HCD一声,不用它再费心做DMA映射了。transfer_buffer
是必须要设置的,因为不是所有的主机控制器都能够使用DMA的,万一遇到这样的情况,也好有个备用。transfer_buffer_length指的就是transfer_buffer或transfer_dma的长度。

actual_length,urb结束之后,会用这个字段告诉你实际上传输了多少数据。

setup_packet,setup_dma,同样是两个缓冲区,一个是kmalloc分配的,一个是用usb_buffer_alloc分配的,不过,这两个缓冲区是控制传输专用的,记得struct usb_ctrlrequest不?它们保存的就是一个struct usb_ctrlrequest结构体,如果你的urb设置了setup_dma,同样要设置URB_NO_SETUP_DMA_MAP标志来告诉HCD。如果进行的是控制传输,setup_packet是必须要设置的,也是为了防止出现主机控制器不能使用DMA的情况。

start_frame,如果你没有指定URB_ISO_ASAP标志,就必须自己设置start_frame,指定等时传输在哪帧或微帧开始。如果指定了URB_ISO_ASAP,urb结束时会使用这个值返回实际的开始帧号。

interval,等时和中断传输专用。interval间隔时间的意思,什么的间隔时间?就是上面说的端点希望主机轮询自己的时间间隔。这个值和端点描述符里的bInterval是一样的,你不能随便儿的指定一个,然后就去做春秋大梦,以为到时间了梦里的名车美女都会跑出来,协议里对你能指定的值是有范围限制的,对于中断传输,全速时,这个范围为1~255ms,低速是为10~255ms,高速时为1~16,这个1~16只是bInterval可以取的值,实际的间隔时间需要计算一下,为2的(bInterval-1)次方乘以125微秒,也就是2的(bInterval-1)次方个微帧。对于等时传输,没有低速了,等时传输根本就不是低速端点负担得起的,有多大能耐就做多大事,对于全速和高速,这个范围也是为1~16,间隔时间由2的(bInterval-1)次方算出来,单位为帧或微帧。这样看来,每一帧或微帧里,你最多只能期望有一次等时和中断传输,不能再多了。

不过即使完全按照上面的范围来取,你的期望也并不是就肯定可以实现的,因为对于高速来说,最多有80%的总线时间给这两种传输用,对于低速和全速要多点儿,达到90%,这个时间怎么分配,都由主机控制器掌握着,所以你的期望能不能实现还要看主机控制器的脸色,没办法,它就有这种权力。

context,驱动设置了给下面的结束处理函数用的。比如可以将自己驱动里描述自己设备的结构体放在里边儿,在结束处理函数里就可以取出来。

complete,一个指向结束处理函数的指针,传输成功完成,或者中间发生错误的时候就会调用它,驱动可以在这里边儿检查urb的状态,并做一些处理,比如可以释放这个urb,或者重新提交给HCD。就说摄像头吧,你向HCD提交了个等时的urb从摄像头那里读取视频数据,传输完成的时候调用了你指定的这个结束处理函数,并在里面取出了urb里面获得的数据进行解码等处理,然后怎么着?总不会这一个urb读取的数据就够你向mm表白了吧,你的爱慕之情可是犹如滔滔江水连绵不绝,所以需要获得更多的数据,那你也总不会再去创建、初始化一个等时的urb吧,即使再穷极无聊的人也不会那么做,明显刚刚的那个可以继续用的,只要将它再次提交给HCD就可以了。这个函数指针的定义在include/linux/usb.h

961 typedef void (*usb_complete_t)(struct urb *);

还有三个,都是等时传输专用的,等时传输与其它传输不一样,可以指定传输多少个packet,每个packet使用struct usb_iso_packet_descriptor结构来描述。1155行的iso_frame_desc就表示了一个变长的struct usb_iso_packet_descriptor结构体数组,而1149行的number_of_packets指定了要这个结构体数组的大小,也就是要传输多少个packet。

要说明的是这里说的packet不是说你在一次等时传输里传输了多个Data packet,而是说你在一个urb里指定了多次的等时传输,每个struct usb_iso_packet_descriptor结构体都代表了一次等时传输。这里说一下等时传输底层的packet情况。不像控制传输最少要有SETUP和STATUS两个阶段的transaction,等时传输只有Isochronous transaction,即等时transaction一个阶段,一次等时传输就是一次等时transaction的过程。而等时transaction也只有两个阶段,就是主机向设备发送OUT
Token包,然后发送一个Data包,或者是主机向设备发送IN Token包,然后设备向主机发送一个Data包,这个Data包里data payload的长度只能小于或者等于等时端点的wMaxPacketSize值。这里没有了Handshake包,因为不需要,等时传输是不保证数据完全正确无误的到达的,没有什么错误重传机制,也就不需要使用Handshake包来汇报OK不OK。对它来说实时要比正确性重要的多,你的摄像头一秒钟少给你一帧多给你一帧,没什么本质的区别,如果给你延迟个几秒,就明显的感觉不爽了。

所以对于等时传输来说,在完成了number_of_packets次传输之后,会去调用你的结束处理函数,在里面对数据做处理,而error_count记录了这么多次传输中发生错误的次数。

现在看看struct usb_iso_packet_descriptor结构的定义,在include/linux/usb.h里定义

struct usb_iso_packet_descriptor {
	unsigned int offset;
	unsigned int length;		/* expected length */
	unsigned int actual_length;
	int status;
};
offset表示transfer_buffer里的偏移位置,你不是指定了要进行number_of_packets次等时传输么,那么也要准备够这么多次传输用的缓冲区吧,当然不是说让你准备多个缓冲区,没必要,都放transfer_buffer或者transfer_dma里面好了,只要记着每次传输对应的数据偏移就可以。length是预期的这次等时传输Data包里数据的长度,注意这里说的是预期,因为实际传输时因为种种原因可能不会有那么多数据,urb结束时,每个struct usb_iso_packet_descriptor结构体的actual_length就表示了各次等时传输实际传输的数据长度,而status分别记录了它们的状态。

不管你理解不理解,struct urb都是暂且说到这里了,最后听听David Brownell大侠的呼声

I'd rather see 'struct urb' start to shrink, not grow!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: