tiamo
VIP专家组
VIP专家组
  • 注册日期2002-02-26
  • 最后登录2018-01-09
  • 粉丝17
  • 关注4
  • 积分50分
  • 威望142点
  • 贡献值1点
  • 好评度40点
  • 原创分2分
  • 专家分15分
  • 原创先锋奖
  • 社区居民
阅读:30027回复:71

[原创]windows的usb体系结构..新年新贴

楼主#
更多 发布于:2005-02-15 02:37
题目起得很大..所以我分好几个部分描述

大致的提纲是
1.背景知识介绍..一些关于电路理论,计算机组成原理的东西
2.usb相关背景知识
3.windows usb整体架构介绍
4.usbport.sys分析
5.usbuhci.sys分析
6.usbhub.sys分析

usb协议方面的涉及的不算太多..毕竟都是有文档的东西
主要着眼于windows的实现部分..

过年了..我去买了个logitech的无线光电鼠..正好是个usb接口的..于是就想了解下windows关于usb这个部分的东西..
于是就有了这些东西

先还是说说我的工作环境
自己的系统是2003 build 3790...
反汇编的sys文件(没有源代码,当然只能用ida了哟)是checked windows xp 2600(没有打sp的)...
硬件环境...i815e主板..ich2南桥...82801BA...uhci..支援usb 1.0.
VID = 8086(这个当然了)...PID = 2442 REV = 02

因为是反汇编...所以信息严重依赖我的硬件平台
而且以为我的主板usb host controller不支援usb 2.0...所以关于2.0的代码我全部ignore了...

好了...进入今天的主题...

今天要说的是关于bus的一些基本的知识
说道一个总线...
那就有几个基本的问题需要回答
1.总线是什么样的类型,串行还是并行的?这个决定了总线的基本组成逻辑以及通讯方式
2.总线上的设备是怎么进行交流的?这个涉及设备的地址解码,总线信号的响应的问题
3.总线的仲裁(arbiter)是怎么进行的?这个涉及总线的使用权所有权归谁管理,谁驱动总线信号的问题

任何一个总线..最关心的问题就是第二个..因为她实际上就是我们最直接能体会到的....
比如我的数据是怎么发送到我的设备上的..而不会发送到别的设备上....我设备的数据又是怎么传递给我的..这个就是最基本的问题..

首先..我们来看看总线的两种基本组成逻辑..他们会决定具体的总线操作形式...

第一类总线她的数据传输是并行的...也就是并行总线..她把要发送的数据分成位..每一个位(bit)用一根数据线发送出去..总线拥有的数据线的条数决定了一次能发送的bit的数目...如果有32根线..那么一次就能发送32 bits的数据也就是一个dword..如果有4个dword要发送..就得分成4次发送..每一次占据一个固定的时间长度...这个时间长度就叫作总线时钟周期..一般把一次读写操作花的时间叫一个总线周期....

并行总线是现在用得非常多的总线类型..她的特点决定了她只是用在近距离的局部范围内(因为得用很多线,而且后面会知道她对时钟要求很高..所以不太适合远距离传输)..比如像pci..就是并行的....

第二类的总线..数据传输是串行的..就是串行总线..她的数据发送方式跟并行的不同...同样她把数据分成bit..但是每次只是传输一个bit..过一定的时间以后再发送第二个bit...然后第三个bit...这样就只是需要很少的几根线就行了...就像usb才4根线而已..而且还有一个是电源线...串行总线又会根据是否同步发送接收的方式分成同步跟异步两种....同步就是说..发送端跟接收端是工作在同一个时钟信号下的(时钟信号通常在一组单独的信号线传输)..总线总是处于工作状态...而异步方式就是说发送端跟接收端工作不需要同步..总线是可以空闲的...发送端跟接收端约定一个协议表示又数据要传输了...传输完成了总线就进入空闲状态...

接下来就来看看如何作地址解码...马上就会看到两种总线是如何影响到地址解码过程的...

那么什么是地址解码?...一个总线上总是要连接很多的设备..那么怎么去表示每一个不同的设备呢?....设备总是跟总线连接到一起的..那么对于总线上的信号(也就是数据)..设备怎么知道是送给自己的还是送给旁边某个设备的呢?....这就需要一种方式对设备进行标记..
总线给每个连接到她上面的设备设定一个唯一的地址..这就是总线地址..然后总线每次在传输数据的时候用某种方式告诉总线上的设备..我要发送或者接收的数据是给哪个地址的....每个总线上的设备用这个地址跟自己拥有的地址作比较..如果相同.或者在一个范围内(很多设备都可能会占据有一段的总线地址)..那么这个设备就被选中..作为跟另外那一端(一般会是一个总线控制器)交流的对象..数据就在他们两个之间流动..而其他的设备就忽略这些总线上的信号..当这次总线操作完成了...以后又有新的操作要进行了..总线又通知每个设备这次她要交流的对象..被选中的对象就又去交流..其他的设备一样忽略...
以上就是基本的原理...听起来是不是很智能? 呵呵....

回过头来..你会不会想究竟是怎么个控制法的?
某个设备怎么知道要忽略这些数据?某个设备又怎么知道应该去读写这些数据?
这些问题就落到地址解码逻辑部分了..或许你觉得很复杂..其实基本的原理并不难....我在学校念书的时候..就自己用74LSxxx的电路作过8086 cpu的地址解码逻辑的..说穿了其实很简单..虽然那些技术已经完全不适应与现在的总线结构了...但是基本原理还是一样的..

下面我就一点一点的来讲这个部分是怎么完成的...不感兴趣的同学可以跳过了...呵呵
一般的讲芯片都会设计一个总开关..当你打开这个开关的时候.芯片就开始作正常的工作..而当你关上这个开关的时候..芯片的内部操作就完全停止..或许芯片会对外表现一种高阻态(这个部分参看下面的解释)....
这样我们如果能从地址信号里面提取出某种控制信息来控制这个开关的话..就能达到设备对某些地址工作..对其他地址不工作的功能了.

这个芯片的总开关就被称作芯片的"使能"端(enable)...一般用EN来表示....我们对总线提供的地址信号进行解释..产生适当的信号引入到芯片的EN脚..这样就能达到控制的目的了...这也就是地址解码逻辑的主要功能...

那..我们怎么去获取这个地址信号呢? 作为例子.看最简单的情况..总线把每个设备用一个8位的数字表示..然后用8根线(这也就是地址总线)连接到设备的某8根脚上面..然后每次总线操作的时候就在这8个脚上面送适当的信号...比如总线要访问00001001b地址的设备..他们总线就在8根线上(假设叫A0到A8)送的电信号为
A0 = A3 = 1
A1 = A2 = A4 = A5 = A6 = A7 = 0

如果我们的设备要对这个地址响应..就必须要让EN有效
写作表达试就是
EN = A0*A3*(~A1)*(~A2)*(~A4)*(~A5)*(~A6)*(~A7)
其中*表示逻辑与...~表示非

有了这个表达式就能选择与门非门或门等等搭建好一个组合电路..产生合适的EN信号...enable芯片了...
看看上面这个表达式..什么时候EN才是1..
必须要A0 = A3 = 1而且其他全部为0的时候EN才是1
所以只有在地址线上送00001001的时候芯片才工作..这样就达到了芯片对某个特定的地址响应的目的...

那如果芯片要对某些地址响应怎么办?
同样的方法..只是EN的表达式得变化了..
比如芯片响应00000001到00000011得地址
那么EN就是
EN = A0*(~A2)*(~A3)*(~A4)*(~A5)*(~A6)*(~A7)

这个例子还是太简单..具体怎么从这个里面提取合适得EN表达式..有很多的方法..都是属于数字电路的基本功..真值表.卡诺图..这里没有办法详讲了...

这样地址解码逻辑的部分的基本原理就算是清楚了..
回头再说几个现实的问题.
首先..设备的地址是谁负责分配的...plug and play 都知道了...
pci总线上的设备地址是靠系统软件分配的..通过写某个pci寄存器完成..isa总线上的设备地址是需要人去完成的..就是所谓跳线..你需要去调整在设备上的几个开关来设定设备的地址...usb总线上的设备地址也是系统软件来设置的..通过所谓的set address request完成.

然后...上面的例子可以看到..地址解码逻辑需要总线送地址信号..因为EN端必须要有效.芯片才会工作..如果你在芯片工作的时候..比如正在交换数据的时候让EN变的无效了..芯片工作就不正常了..这就需要总线在和设备交流的过程中一直送地址信号..来保证EN总是有效的....
可以看到这样的方式效率很低...必须要有额外的线来送地址信号...有没有办法减少这些线呢...如果只是在与设备交流的开始的一段时间里面送地址信号..然后让设备"记忆"到她自己是这次操作的对象..那就可以让地址线根数据线合而为一....送完地址后..设备记忆下来...然后用同样的线作为数据交流的通道...

办法当然是有的..从8088开始就是这种数据地址线共享的方式了..现在的pci总线也是这样地址数据线共享..所以他们的引脚都写AD0..AD1...A就表示Address...D表示Data...表示这个线某些时候是地址信号某些时候是数据信号...

嗯...注意...上面这种共享的方式只是限于并行总线...串行总线用完全不同的方式进行地址解码...后面将会看到

共享马上就带来好多的问题...
首先是设备记忆的问题...怎么能记忆?..这就是数字电路里面的时序电路发挥作用的时候了...

数字电路一般被分成了两类..一类就上像上面EN那样完成一个输入到输出的运算的功能的..被叫做组合逻辑电路....他们最大的特点就是输出总是根据输入发送变化..也就是没有办法保存自己的状态..任何一个时刻你去查看他们的输出..都是用他们的输入计算出来的...
另外一类就有些智能了...她能记忆自己的状态..输出的状态很可能跟自己的前一个输出状态有关系...他们就是时序电路..跟时间序列有关系的电路....
两类电路相互配合完成不同的功能...
组合逻辑完成一些运算的功能...比如作作加法..作作乘法..完成"逻辑"运行的功能
时序电路主要的功能是实现一种"有穷状态自动机"...用来保存一些状态信息...关于有穷状态自动机(Finite State Machine)的话题..就没有办法多说了...形式语言与自动机本身就是一门很深的学问

锁存器是一个很普通的时序电路...一般的..她在时钟上升沿或者下降沿来的时候锁存输入..然后产生输出...在其他的时候输出都不跟随输入变化...这就是所谓边缘触发的D触发器...当然也有电平触发的D触发器...具体使用哪钟...得看你使用得总线配置...

设想下面得情况...
总线控制器送地址信号..然后让锁存器Enable(她也有自己得EN端)
锁存器根据地址信号产生出要送给设备得EN信号...然后锁存..这也是他名字的由来....这样送给设备得EN信号就一直到下一次总线控制器送锁存器得EN信号之前都保持不变...这样就完成了记忆功能..

现在责任就全转移到总线控制器头上了..因为她要产生合适得锁存器使用EN信号....这个当然很简单了..如果是电平触发得...在送地址信号得同时送这个信号就行了...这信号一般被称为ALE Address Latch Enable....
地址锁存以后..总线就不用担心设备中途突然被Disable得情况了...

并行总线的地址解码部分得逻辑基本就是这样..是不是很简单?
串行总线的我放到后面讲...

接下来看看数据传输的部分....
总线即要发送数据又要接收数据...怎么办呢?怎么知道是要把数据从总线上引进来还应让自己的数据发送到总线上去?

引?发送?这是什么意思?学过电路的知道..如果给定一个点的电压是固定的啊...电压并不存在方向的啊....
说的没错....是这样的....在某种电路的帮助下..你可以实现这样的功能...让某个时候你内部的电压跟随外部变化(实现引的功能)..让某种时候...你的内部电压去改变外部电压(实现发送功能)...而在另外的时候...可以让你完全从电路里面断开...你也不影响到外部电压...外部也不影响到你....
是不是很诡异...哈哈...

完成这种功能的电路叫三态门...tri-state..这个词相信很多地方看到了吧....

我的计算机接口技术的老师说...三态门是总线实现的关键技术...确实有同感...

所谓3态门..是说电路内部有3种状态..有一种就是高阻态...高阻就是你对地阻抗(是说电阻电容电感一起产生的影响)...对电源阻抗都很大...这样就相当于断开一样...另外两种状态算是正常工作状态..输出由输入决定....

控制这种状态变化的脚...呵呵...还是叫EN....
EN为1的时候电路正常工作..输出电压跟随输入电压
EN为0的时候电路表现高阻态

如果我们把两个3态门调个头并连起来...并且让他们的EN相互反向..
这样把左边的信号叫A,右边的信号叫B
EN为1的时候..其中一个3态门工作...B跟随A...另外一个高阻态..当她不存在
EN为0的时候..另外一个工作..因为是反过来的接的..所以A跟随B.原来的那个高阻态...

这样在EN的控制下...就实现了这些引跟发送的过程了.....

也许你注意到我用跟随这个词了...什么意思...
其实她来自"射级跟随器"这个词组...是一种三极管的工作状态...
如果完全不懂电路...忽略好了..呵呵

现在如果有合适的EN信号去控制3态门的话就能完成读取发送的功能了.....

总线控制器在发出总线信号的时候...同样要准备好这个控制信号...不仅仅是自己要用...总线上的设备也得用...
一般得这就是一个READ信号..或者是个WRITE信号...用这个信号去控制那些3态门就能完成

好了...到这里...地址解码逻辑也完了...读取写入控制逻辑也完了
剩下得就是具体得总线通讯协议了....这就是跟每个总线相关的信息了..所谓...implementation dependence....

了解这些基本的知识有助于了解总线协议本身...起码不会感觉自己完全不明白具体操作是怎么进行的

还有一个问题...就是总线的共享问题...
设备都是连接到总线上的..如果某个时候有两个设备都想用这个总线怎么办?....很明显不能让两个设备同时往总线上送信号...不然信号叠加以后就错误了....

这样就需要有一个仲裁器(arbiter)来决定当然谁永远总线的控制权..

设备要使用总线的时候..往arbiter送一个request的信号(或许通过某个特殊的信号线..比如pci的bus master设备使用的)....
arbiter经过裁决以后决定..可以让她占用总线..于是回送一个ack信号(也许也是用一个特殊的信号线回送)....然后设备通过某种方式(比如让request信号线跟ack信号线都保持高电平)告诉总线她正在占用控制权...arbiter让她的3态门全部高阻态..交出总线控制权...如果这个时候还有别的设备要访问总线..她也执行相同的操作...不过arbiter却知道现在不能满足这种要求..或者就一直延时到总线可用为止...占用总线的设备用完了以后..用某种方式通知arbiter(比如同时让request跟ack都为低电平)..arbiter回收总线控制权..或许就会回答刚刚后到的那个request..送ack信号...

这就是总线仲裁的基本逻辑模型...

很明显...上面这些描述都基于并行总线...

下面就来看看串行总线的部分....

串行总线应用的范围(远距离)决定了她要付出更多的劳动..
首当其冲的就是信号的干扰问题...
因为并行总线的长度都非常短(几厘米)..而且都是有足够的电磁保护的..相对的讲..受到的干扰要小很多...
但是串行的就不同了..她的传输距离要比并行的长很多(几米或者几十米).而且环境上也没有并行那么安全..这样就需要某些手段作一些安全措施..

然后还有就是异步串行总线的同步问题...
如果这边已经开始发送数据了...对端却没有反应..或者反应不正常...那肯定坏了...如果两边的时钟有偏差...数据采样点不一样..在小范围内或许还不会很多...但是你想..一次采样差1ms..10次就差10ms..或许你已经漏了好多些的数据了....
所以..必须有某种方式让两边的数据采样同步..要保证在无外界干扰的情况下对方读的数据跟自己写的是一样的..

最简单的方法就是传输同步信号..但是这样就必须要提供多的数据线..

还有的办法就是发送端用某种方式把同步信号叠加到数据信号里面..接收端再把同步信号提取出来...

有模拟电路基础的马上就想到..用载波调制嘛...可惜在数字电路里面这行不通....原因嘛...因为数字信号本身就是方波..根据傅立叶变换..她已经化作了拥有无数频率的正弦波...已经找不到一个合适的载波频率了...这个部分...我承认是在我的大学课程中难度仅次于离散数学的一个部分...傅立叶变换..拉普拉斯变换..卷积积分..激励函数..这些名词....想想都头疼....虽然还比不上所谓...群..半群..域..环...这些词的冲击...呵呵..题外话....

不能用载波..就必须要在信号本身上面想办法
就是所谓的编码....

并行总线之间发送高低电平的方式用在串行总线上的话...如果连续出现多个相同的1或者0...就有可能发送同步的问题...如果信号常常有变化..那么对端就可以在变化的边缘调整时钟.达到同步的目的..因为变化总是发生在时钟的边缘....

基于以上的事实...串行总线里面广泛的运用了NRZI编码...不归零翻转编码...Non Return to Zero Inverted....
简单的说..就是如果输入信号是1..就保持输出信号不变化..如果是0就翻转....她其实还是会产生同步问题...但是如果她跟bit stuffing 结合一起用的话..就能比较好的解决同步的问题...

bit stuffing..位填充...如果在输入信号里面有若干(由通讯协议决定这个若干究竟是几..比如usb是6)个1的话..就插入一个0..这样编码后的信号肯定会反转...这个翻转的变化沿就提供了同步的方法..
接收端接收的时候..如果遇到若干个相同不变化的信号..那么就能肯定下一个信号必须要翻转..她是bit stuffing的结果..并且要从最终的信号里面删除掉这个信号...

为什么这里能插入?上面的却不能插入呢?...因为这里的0,1已经不是原来的含义了...上面的你在发送的时候作了stuffing..接收的时候却没有办法判断究竟这个是stuffing的结果还是本身就是一个0..

光有这些操作还是不够了...串行通讯一般都会携带某种形式的CRC..比如usb...再比如LAN...呵呵..lan也是串行通讯的哟....

数据传输过程中的问题解决了...
下面回答这样一个问题
接收端怎么知道什么时候去读取数据并对数据进行解释?
并不是任何时候总线上的数据都是有意义的呀?

回答这个问题之前..先回想并行总线怎么没有遇到这个问题..
因为芯片在EN信号的控制下工作的..EN信号又是最终由总线控制器提供的...总线控制器在必要的时候让设备去读去写..当然就没有这个问题了...

串行环境下就不同了..没有EN信号..原因就跟没有同步信号一样

解决办法就是用一个特殊的bits string(回想串行是一个bit一个bit 串着发出去的)..来告诉对端的设备要开始一次数据传输了..

设备检测到这个特殊的bits string(一般叫着前导帧)..就开始准备自己的内部状态准备接收数据了...同时的..这个前导帧也会提供一些同步的信息(01交替出现)让对端同步自己的时钟..

数据传输开始的判断问题这样也就解决了...

回过头来..或许你要问...我计算机的信号都是并行的
怎么转串行发送呢?接收的时候怎么办呢?

这个解决办法...有一个电路的名字叫移位寄存器
她能实现串行转并行..并行转串行的方法..
比较简单的同步移位寄存器就是用若干个D触发器级连起来构成

或许你注意到了...我一直用对端..对方这样的词来描述..
而且从来都没有提到并行总线那样的地址解码过程..
这是怎么回事情..

因为串行总线使用的数据线本身就很少..不可能把地址当作特别的信号来发送..只能是把地址当作数据的一部分发送出去..所以地址解析的功能就延后了....

一般的...在前导帧以后就会有设备地址数据...每个收到数据的设备解码这个数据作锁存(方面跟上面并行的一样)..设置设备芯片的EN信号..完成地址解码过程.....

关于设备地址..很好的例子就是Lan...在以太网的帧头包含有对方的mac 地址...每个收到(lan是一种广播模式..每个处于相同的广播域的设备都能收到这份数据)的设备比较mac地址是否是自己的mac地址.(当然还有广播地址与多播地址的比较)...然后决定自己是否响应这个数据包....

usb也是一样的...在某些token里面会携带device的address和endpoint number..这些就是足够的信息用作地址解码的...

地址解码部分有了上面的基础..想起来也就不难了..

接下来是数据传输发送部分....3态门..同上..

最后是总线仲裁部分....
这个部分稍微显得复杂...usb总线仲裁由host完成..因为所有的usb transaction都是由host发起的...不存在由设备申请使用总线的时候

而像lan这种..却是使用一种CSMA/CD的方式...所谓..带冲突检测的多路存取载波侦听....不管什么时候读这个总是觉得别扭..呵呵..

关键的地方就在冲突检测上面....每个要占用总线的设备总是在发送之前侦听网线上的信号512 bits的时间...如果网线上的信号显示由设备在使用..则等待...直到没有人使用了再发送..如果有设备同时发送的话...则发生了一次冲突..作一次随机指数回避..然后再侦听..重复上面的过程....512个bits也就是为什么以太网的帧最小长度是60个字节的原因(4个字节的CRC)....

lan的总线共享方式是一种竞争的方式..并没有一个arbiter在运作..
也是必然...lan的物理结构(大..远)决定了不可能有arbiter存在...

7788写了这么多.....
这次主要的是一些基本的关于总线的知识....
算作是计算机组成原理的一个概括好了....

最新喜欢:

yvqvanyvqvan gaooogaooo ltgbjltgbj
tiamo
VIP专家组
VIP专家组
  • 注册日期2002-02-26
  • 最后登录2018-01-09
  • 粉丝17
  • 关注4
  • 积分50分
  • 威望142点
  • 贡献值1点
  • 好评度40点
  • 原创分2分
  • 专家分15分
  • 原创先锋奖
  • 社区居民
沙发#
发布于:2005-02-22 10:54
第二部分开始了.......

进度稍微有点慢....大家忍受一下...

usb的详细规范就没有办法在这里一一说明了
这里只是关注我自己觉得可能要注意的地方...

还有呢...我只是一个作软件的勉强算是程序员吧
所以对硬件的东西很不熟悉..以至于作硬件的朋友会觉得我有些过分的强调某些或许是很基础很简单的事实..呵呵..见谅...

还是一样...
硬件部分开头..接着是软件部分

usb使用2根数据线传送数据D+跟D-
但是理论上只是需要一根数据线就够了...比如并行总线使用的方式

usb使用这2根数据线作为差分放大电路的输入端..经过差分放大以后
使用差分放大的输出端作为接收信号...发送的时候刚刚相反

差分放大电路主要的目的是为了解决所谓\"零点漂移\"的问题..
所谓零点漂移是指在直接耦合放大电路里面即使输入点电压为0
输出端也会有变化缓慢的输出电压..
所谓直接耦合放大电路......呃...打住...这样下去就没完没了了
具体的请参考模拟电路相关部分

所以usb不使用D+跟D-上的信号作为自己的信号定义
而是使用他们经过差分放大以后的信号作为信号定义
具体的定义见usb spec 1.1的表7.1

有了信号定义
现在来看几个特别的事件的信号
第一是连接跟断开信号
在hub的downstream port上连接有两个电阻..所谓下拉电阻..
他们把D+跟D-信号都掐位到小于VIL(输入最小电压)...也就是所谓的
SE0状态

当有设备连接到port上的时候..因为设备里面的上拉电阻使得D+或者D-信号提升.超过VIH..经过差分放大呈现出1或者0...这样就能让hub知道有设备连接上来了...
而对于full speed跟low speed的设备来说...上拉电阻所接入的信号线是不同的low speed的上拉电阻接在D-上面..而full speed的上拉电阻接在D+上面...
所以差分以后的信号..对full speed来说是1..对low speed来说就是0...这也是辨认接入设备的速度的方法...

设备接入的时候信号从SE0变迁到差分1或者差分0的时间超过了2.5微秒...hub就认为有新的设备连接进来了
设备断开的时候刚刚相反...信号变迁到SE0..持续时间超过2.5微秒就认为设备断开了....

动态连接的硬件辨认方式就是如此...那么如何让软件知道呢..
hub检测到这种情况了以后设置对应的port的connect change位..然后用interrupt transfer的方式告诉给hub的驱动程序...hub的驱动程序然后调用IoInvalidateDeviceRelations....然后内核发出pnp irp..然后再然后..呵呵...具体的过程后面会描述

接下来要说的就是数据信号...
数据信号是由差分0跟差分1组成的...
发送端通过驱动D+跟D-以差模信号(幅度相等、极性相反)..经过差分放大以后体现\"逻辑0跟逻辑1\"来

数据都是组装在packet里面发送的....
在每个packet的开头发送一个start of packet告诉总线上的设备
要开始一个新的packet了...在packet结束的时候发送end of packet信号...然后进行idle状态

start of packet的标志就是bus从idle状态转入data k状态
而end of packet标志是bus从data 状态转入SE0状态2 bits的时间然后转入data j状态1 bits的时间...然后bus就进入idle状态

bus上的设备必须要检查start of packet..解码随后的packet的内容
决定自己是否要对这个packet作出响应.对于不响应的packet必须要忽略...这里必须要实现一个状态机..因为并不能只是判断当前收到的packet就能决定自己是否响应..后面的软件协议部分会有详细的解释

接着说reset信号...hub驱动总线进入SE0状态超过2.5微秒..连接的设备就应该把这个当然是一个reset信号..设备进行reset.完成以后对缺省地址(也就是0)进行响应

suspend信号...她其实不是一个信号...设备检测到bus进入idle状态超过3毫秒就应该进入suspend状态..只是从bus上获得suspend状态所需要不超过10毫安的电流..任何bus上的其他信号都应该让设备从suspend状态回复过来..对于full speed的设备.每毫秒一次的start of frame信号能阻止设备进入suspend状态..而对于low speed的设备.hub会在start of frame的时候产生keep alive信号来阻止设备进入suspend状态...只有在明确的让hub把某个port设置成suspend状态的时候...hub才不转发start of frame信号跟keep alive信号..这样设备才会进入suspend状态..这也就是selective suspend..在设置了某个port进入suspend状态以后..hub停止转发所以信号到这个设备..设备就进入了suspend状态...在这个状态里面..设备必须要继续驱动连接在D+D-上的上拉电阻..一来保持bus的idle状态..二来让hub不会认为设备连接断开了...

global suspend 信号...host controller停止发送start of frame信号..这样bus上的所有设备都进入suspend状态(hub只是转发.不会产生信号)....

resume...分成两种..
第一是host发起的resume信号...host驱动总线进入resume状态..也就是data k状态至少20毫秒.然后驱动一个low speed的SE0信号接着一个data j信号..而且必须在3毫秒以内发出start of frame信号以阻止设备再次进入suspend状态..

第二种是remote wakeup的时候设备发出的resume信号..设备驱动总线进入data k状态1毫秒到15毫秒之间.然后放弃总线驱动权...设备连接的hub检测到这个resume信号.如果hub的upstream port也是resume状态的....她就把这个resume信号发送给她每个enabled port..包括先前产生resume信号的设备..hub同时向上发送这个resume信号..直到遇到非resume状态的hub或者达到host为止..
最后遇到的这个hub会在15毫秒以后驱动bus..所有接受到resume信号的bus必须要在15ms以内让自己的upstream port来驱动自己的enabled port...最终到达的hub维持resume信号至少20毫秒..然后跟一个low speed的SE0然后1bit的data j来结束这个resume信号进入正常的bus 操作(比如转发start of frame以及开始进行数据传输)

明白了总线上的信号表示方法
接下来要说的就是数据传输本身了
首先是数据编码..使用nrzi + bit stuffing的方式作为数据编码方式每遇到6个1就插入一个0..

接着就进入了software管理范围...
数据传输被分成了单个的packet.实际的数据被放置在每个packet里面进行传输...packet作为数据传输的一个单位.

bus从idle状态转入data k状态就表示了一个新的packet开始
在每个packet的开头传输一个同步数据..这个同步数据的目的就像她的名字一样..用来让设备同步自己的内部时钟...
他是有7个0紧跟一个1构成的..经过nrzi以后就变成kjkjkjkk信号
设备利用信号的变化边缘同步自己的内部时钟
紧跟这同步数据以后的是一个PID packet identifier..他指出了这个packet使用的格式信息错误处理方式等等..他的格式以及含义请参考spec..

根据pid的不同.随后跟的内容也不同..同样参考spec

packet结束以后发送end of packet信号(2 bits时间的SE0 data j)

bus上的设备必须识别start of packet信号..然后同步自己的时钟.然后解析pid..决定自己是否响应这个pid..不响应的忽略随后的信号..等待end of packet..重新检测start of packet..解码pid.重复这个过程..

设备只是对指定了自己的地址以及能提供适当操作的endpoint进行响应...在某些pid里面指定了packet所要操作的设备以及endpoint(通过设备地址跟endpoint number指定)...其他的pid未指定这些信息的都不能作为一个传输的首个pid发送....

设备检测到带有地址信息的packet..然后与自己的地址比较来决定自己是否要响应这个已经以后若干个不带地址信息的packet...对于不响应的设备..必须要忽略后面紧跟的所有不带地址信息的packet..直到遇到了新的带有地址信息的packet再重复如上判断..而对于要响应的设备..同时暗示了随后的那些不带地址信息的packet都是在跟自己交流..自己必须要进行响应....
整体逻辑如上....
具体的传输使用什么的数据格式..使用什么样子的packet组合方式.请参考spec

接下来就是收尾的工作了...大致的说说windows对一个usb设备的操作过程

当hub启动的时候..hub的fdo发出一个interrupt transfer的urb到hub的endpoint1...这个endpoint是一个用来通知hub的外部事件的endpoint....

当有事件发生的时候(比如一个设备连接进来了)...host controller完成这个irp...hub的fdo从输出结果里面知道了是哪个port上发生了变化...hub fdo读取这个port的状态..判断发生了什么样子的变化..假设是有某个设备连接进来了..hub fdo就reset port..完成以后读取device descriptor利用里面的vendor id,product id和bcddevice组成device id,同时当然要生成hardware id跟compatible id..查找合适的驱动程序..安装或者加载...
驱动程序加载以后..就能对这个设备进行操作了...

大体的流程
首先读取config descriptor.选择一个config
然后对于config descriptor里面的每个interface发出一个select interface请求...windows为每个interface里面的每个endpoint返回一个pipe handle...
完成了这些步骤以后..驱动程序就能跟设备进行交流了

交流的方式是利用urb进行了...驱动程序建立好一个urb.然后用internal io control的方式传递给自己attached的设备...
urb结构里面最重要的一个成员便是pipe handle.他指定了要操作的endpoint是哪个..

驱动程序必须要设置这个成员(他是在select interface的时候返回的)..除非指定了使用默认的control endpoint(也就是endpoint0)

底层的pdo把这个urb交给root hub的pdo...然后转换成具体的操作数据传递给host controller..然后由host controller转换成usb的信号发送出去...

至于...urb的排队..urb的完成..urb的取消..等等操作就属于usbport.sys的功能....

而设备的enum...则是属于usbhub.sys的功能

至于usbuhci.sys这些则是完成把urb转换成usb bus信号的功能..

所以以后的几个部分分别有不同的侧重点...
到时候会详细描述...
tiamo
VIP专家组
VIP专家组
  • 注册日期2002-02-26
  • 最后登录2018-01-09
  • 粉丝17
  • 关注4
  • 积分50分
  • 威望142点
  • 贡献值1点
  • 好评度40点
  • 原创分2分
  • 专家分15分
  • 原创先锋奖
  • 社区居民
板凳#
发布于:2005-03-01 22:46
windows的usb整体架构

这里主要讲讲windows为usb设备维护的几个基本结构

首先第一个是device handle

对于每一个usb总线上的设备..windows为其创建一个device handle.关联到设备的pdo上面..这个handle就是填写在urb header的DeviceHandle成员的值...通常情况并不需要我们自己去填写这个值..因为你的irp会传递给你的设备对应的pdo..这个pdo会帮你填写这个成员...

在这个device handle结构里面还维护有几个其他的成员..比如device的usb地址...device连接到的hub所在的port number..device descriptor.还有就是一个内建的pipe handle(用于default endpoint构成的control pipe)..以及其他endpoints 构成的pipes的list header...同时还有保存一个config handle

接着第二个结构就是pipe handle...他表示了一个pipe...这个pipe也许是由有着两个相同endpoint地址的endpoints构成的contol pipe.或者是其他的interrupt pipe等等...

对于endpoint 0..windows特殊对待...他的结构不是动态分配的..而是直接作为一个inner class放置到device handle里面的...
而其他的pipe handle则是动态创建..通过LIST_ENTRY连接到device handle的listheader成员上的...

pipe handle也是一个很小的结构..除了endpoint descriptor以外还有一个指向endpoint 结构的指针

接下来的结构就是endpoint本身了..
这是一个很大的结构...
保存着诸多的信息...除了一些于endpoint相关的参数(比如device speed,max packet size等等)以外..最最主要的是若干个LIST_ENTRY..他们起着连接endpoint到某些不同的ListHeader的作用
比如closed 链表..state change链表等等...
还有些LIST_ENTRY起着urb排队的list header的作用...

然后值config handlez...在device handle结构里面有一个指向他的指针...他表示了当前所使用的配置信息....
很小....除了保存有configuration descriptor还有一个list header...用于连接interface handle

interface handle结构描述了某个interface
也是一个不大的结构...不过却是一个变长的结构
他保存有一份interface descriptor
然后还保存有若干个pipe handle
struct interface_handle
{
  //.....
  ULONG ulPipesCount;
  pipe_handle PipeHandles[1];
};

device handle   config handle   interface handle
+------------+  +------------+
|config   |--->|config desc |   +-----------------+
|pipe zero   |  |interface   |----->|list entry for   |
|pipes list  |<-+ |list header |<-----|config handle    |
|header      |+ | +------------+   |pipes count      |
+------------+| |                     +-----------------+
       | +---------------------|pipe1 list entry |
       +---------------------->|for device handle|
  +---------------------------------|endpoint pointer |
  +->endpoint +--------------+   +-----------------+
        |list entries  |   |pipe2 ...to pipeN|
        |list headers  |   |link with prev   |
        |..............|   |pipe struct......|
        +--------------+   +-----------------+


基本的连接情况就是如此
至于endpoint结构里面的诸多list entry与list header..他们大都是用在传输数据的时候...常常变化..而不像其他的结构里面的list entry只是起一个连接架构的作用..

上面这个图里面...
device handle是在创建pdo的时候创建的..
config handle与连接起来的interface handle.以及interface handle里面的pipe handle还有pipe handle所指向的endpoint这些结构是在你发出select configuration 的urb的时候才创建的

说完这些静态的结构以后..还必须要提到的就是每当你发出一个urb的时候所要进行的工作...

都知道urb是一个联合...他是由好多个struct组成的一个union

但是对于windows内部来说...urb被分成了4种
第一种..属于直接完成的urb..也就是说不会在usb总线上出现的urb..比如select configuration...select interface.abort pipe等等就属于这一类...他们几乎都不涉及数据的传送排队等等..属于同步完成范畴...

第二种用于interrupt根bulk 传输....使用URB_INTERRUTP_OR_BULK_TRANSFER这个结构

第三种用于ISO 传输...

最后一种就是control 传输了...他使用的结构只是在interrupt or bulk的后面多一个8个字节的setup packet
struct _setup_packet
{
  UCHAR bmRequest;
  UCHAR bRequest;
  USHORT wValue;
  USHORT wIndex;
  USHORT wLength;
};

很多的结构都属于这一类..对比一些就会看到windows把某些成员标记成了reserved..就是说windows会自己填写这些成员..而不需要程序员去填写...

对于第二种第三种第四种urb...会有某个对应的usb传输发生在usb总线上面....windows会对这些urb按照所属的endpoint进行排队..一个一个的提交给硬件

这个属于usbport完成的功能...在这里大致的提一下

每个需要进行传输的urb...windows创建一个transfer的结构...
这个结构里面包含了这次传输的基本的参数信息.比如device 地址..endpoint number.传输方式(IN,OUT,SETUP,ISO).等等..同时还有很多list entry配合endpoint结构里面的某些list header完成排队调度的功能..

首先一个transfer进入对应的endpoint的pending 链表...
在特定的情况下..usbport会刷新这个pending链表..
transfer就进入了一个map 链表(这个链表不在endpoint管理范围内)
因为硬件操纵的是物理地址而不是虚拟地址..所以在提交给硬件之前..必须要进行map...所以transfer从pending链表出来以后进入了map 链表..等待完成虚拟地址到物理地址的映射..
这个map链表是一个全局的链表..并不是每个endpoint维护一个map 链表....
在一定的条件下..usbport刷新这个map 链表...通过dma的方式对transfer要使用的buffer进行map...完成以后.transfer进入到其对应的endpoint的active 链表...这个时候transfer就能提交给硬件了....
在一个workthread的帮助下..从active链表里面取出一个transfer提交给miniport driver...miniport driver再提交给硬件...

当硬件完成这次传输以后..通知miniport driver..再通知到usbport..于是这个transfer再次进入done 链表...
在usbport刷新done 链表的时候...最终完成这个transfer对应的urb以及对应的irp(如果有的话)...同时释放transfer结构对应的内存..

几个特别的地方要说一下...
第一就是...如果某个urb只是单方向的..不涉及数据交换的..比如设置某个port状态这种..用setup结构已经完全够了的情况..transfer从pending链表出来..并不进入map链表..而是直接进入endpoint的active 链表..因为并不需要buffer..所以不用map

第二...usbport操纵的结构是transfer..与transfer关联的是urb..而不是irp...完全可以自己构造一个urb进行传输...而不需要某个irp..

在无cancel跟无abort发生的情况下的transfer就是这样一种流程

很容易理解下面这个状态变迁图

pending----->map----->active---->done

加入了cancel跟abort的情况稍微复杂一点
因为任何一个状态都能转移到cancel 链表里面去

这个主要是通过cancel routine来完成了...先到这里..在说到usbport的时候会有详细的讨论这个问题...
这里只是需要对正常状态的流程有个了解..

明白了windows为每个usb设备维护的信息..明白了urb的传输完成
现在来看看usb设备的device stack搭建情况

stack的搭建工作就属于usb hub的任务了..分工很明确..usbport负责维护usb设备的信息.负责处理urb...而usb hub则是致力于bus relationship....

usbport实现了两个接口..一个是USB_BUS_INTERFACE_USBDI.另外一个是USB_BUS_INTERFACE_HUB
这两个接口都有公开的定义...能在ddk里面找到

usbhub利用这两个接口里面的函数通知usbport要创建一个新的device handle了...等等一类的操作...具体看看这两个接口的定义就能体会了...

想想也很显然...device handle是由usbport创建管理销毁的..usbhub没有必要去实现一份相同的工作...交给usbport完成就ok

所以..usbhub在enum出一个新的pdo的时候..通知usbport为这个pdo创建一个device handle结构并返回这个结构的地址...然后usbhub保存这个地址到pdo的device extension里面...
以后提交到pdo的urb..usbhub都填写urb的header里面的device handle成员....

这就是usbport在usbhub帮助下完成的device管理工作..
usbport同时还对urb进行过滤..以提供更多的usbport没有实现的功能..比如xp跟2003以后版本支持的URB_OS_FEATURE_DESCRIPTOR_REQUEST 就是由usbhub来完成的..

usbhub还要完成的几个功能就是usb设备的电源管理..以及与selective suspend关联的idle request...这些都会在讲到usbhub的时候具体分析

还有一个没有提到的驱动就是用于compose设备的usbccgp.sys..
我并没有对这个sys作ida...不过他的功能同usb hub差不多..

至于usbuhci....那是一个与硬件接口息息相关的驱动..谈不上什么特别的架构.....

今天就到这里...
这个作为以后的代码分析的一个总体大概
tiamo
VIP专家组
VIP专家组
  • 注册日期2002-02-26
  • 最后登录2018-01-09
  • 粉丝17
  • 关注4
  • 积分50分
  • 威望142点
  • 贡献值1点
  • 好评度40点
  • 原创分2分
  • 专家分15分
  • 原创先锋奖
  • 社区居民
地板#
发布于:2005-03-03 20:16
啊~~~~~~~高级会员了啊我是....

谢谢斑主哟......
tiamo
VIP专家组
VIP专家组
  • 注册日期2002-02-26
  • 最后登录2018-01-09
  • 粉丝17
  • 关注4
  • 积分50分
  • 威望142点
  • 贡献值1点
  • 好评度40点
  • 原创分2分
  • 专家分15分
  • 原创先锋奖
  • 社区居民
地下室#
发布于:2005-03-07 16:25
今天进入重头了..usbport.sys跟踪分析..

usbport是一个usb host controller的port driver.
而usbuhci是uhci类型的usb host controller的miniport driver
usbehci则是ehci类型的usb host controller的miniport driver
usbport负责创建管理usb host controller的fdo
那么对应的pdo是由谁创建管理的呢?
当然是这个host controller所在的总线的driver创建管理的了
当前的架构上这个bus一般是pci.

usbport主要完成的功能如下
1.抽象层次上管理usb host controller,实际的操作是由各个miniport driver 完成的.
2.创建并管理集成在host controller里面的root hub的pdo.这个pdo上会attach 由usbhub.sys创建并管理的fdo.
3.管理维护所有挂在当前这个host controller上的最多127个usb 设备.
4.处理大多数针对当前这个host controller管理下的bus上的所有usb设备所发出的usb request blocks(URB).

基本功能如上
这里的文字也主要针对上面这些功能一一描述
其中
1.这个是集成到各个其他操作里面的.通用的操作由usbport完成.特定了和硬件相关的操作通过调用各个miniport所注册的callback routine来完成.usb host controller的miniport跟其他各种的miniport driver一样.在driverentry里面填写好一个结构.调用usbport的一个导出函数.usbport修改这些结构的某些成员提供自己实现的一些函数指针以供miniport driver调用.
struct _USB_MINIPORT_REGISTRATION_PACKET
{
    ULONG    ulVersion;
    ULONG    ulFlags;
    ULONG    ulTotalBandWidth;
    // some other fields
    // .....
    // miniport\'s routines
    // ............
    // output service routines
    // filled by usbport.sys
    // .........
};


2.root hub的pdo创建很简单.不外乎就是在query bus relationship的是创建一个device并且返回就行了.至于关联到这个root hub pdo上的操作,大部分都交给了usb host controller fdo来完成了.马上就能看到其实大部分的操作都是发送给这个pdo的.

3.跟4.就是今天的重点部分了.这两个部分也是usbport.sys里面着墨最多的地方.我讲的顺序也是先说第三部分.再说第四部分.但是这两个部分并不是完全分开的.比如设备管理的时候可能涉及到要向某个设备发送一些urb.最典型的就是要set address.这里当然就必须由第四个部分来完成了.而且urb的处理也极大的依赖管理设备的时候所建立和维护的诸多数据结构.但是为了讲解方便.在说到第三个部分的时候总是假设usbport有某种方法能把某个urb发送出去并且完成.暂时不管他具体是怎么完成的.

首先是设备管理
在前面说到usbport实现了两个interface以提供usbhub.sys调用,并在usbhub的帮助下来收集所有连接到总线上的设备信息.这里我们跳过这两个interface的具体说明以及调用方式调用时间等等信息,等到讲到usbhub的时候再回头来看这个过程.那个时候再来说究竟在什么时候在什么样子的情况下usbhub会调用哪个函数.如果没有调用上下文要去分析一个函数的功能是比较盲目的.这里大家了解这样一个规则.usbhub用某种方式得知了总线上有个设备连接进来了(比如你插入了一个u盘).usbhub通过调用USBPORTBUSIF_CreateUsbDevice.通知usbport这个事件发生了.(函数参数信息可以查阅ddk).下面得分析也就是从这个地方开始的.

在经过必要的参数检查测试以后,usbport为这个新报告的设备分配一个结构,用来保存必要的信息
00000000 DEVICE_HANDLE   struc ; (sizeof=0X70)
00000000 Sig             dd ?    // signature = \'HveD\'
00000004 DeviceAddress   dw ?    // usb device addr (1-127)
00000006 PortNumber      dw ?    // port number
00000008 RefCount        dd ?    // reference count
0000000C Tt              dd ?    // for usb2.0 
00000010 ParentHandle    dd ?    // parent hub device handle
00000014 ConfigHandle    dd ?    // CONFIG_HANDLE*
00000018 PipeHandle      PIPE_HANDLE ? // for default pipe
00000038 DeviceSpeed     dd ?    // see ddk
0000003C DeviceDesc      USB_DEVICE_DESCRIPTOR ?
0000004E                 db ?    // padding,alignment
0000004F                 db ?    // padding,alignment
00000050 DeviceFlags     dd ?    // flags
00000054 DeviceHandleListEntry LIST_ENTRY ? // link all device handle together
0000005C PipeHandleListHead LIST_ENTRY ? // pipe handle list header
00000064 TtCount         dd ?    // for usb2.0
00000068 TtListHead      LIST_ENTRY ? // for usb2.0
00000070 DEVICE_HANDLE   ends


usbport设置好某些域以后开始open the default pipe,完成以后,发送一个get device descriptor的urb到这个新出现的usb设备用来填写DeviceDesc成员.成功以后把这个新创建的device handle添加到由usb host controller所维护的device handle list上去

createdevice(parenthandle,portnumber,portstatus):
    check parameters
    allocate buffer for device handle struct
    handle->refcount = 0
    handle->confighandle = 0
    handle->parenthandle = parenthandle
    handle->portnumber = portnumber
    handle->deviceaddress = 0 // default usb address
    according to portstatus,set handle->devicespeed
    according to devicespeed,set default pipe\'s maxpacketsize
    init other pipe_handle\'s member for the default pipe
    open the default pipe\'s endpoint
    get the device descriptor
    link the device handle to the fdo\'s device handle list header


在看open default pipe之前得先看看pipe handle这个结构
00000000 PIPE_HANDLE     struc ; (sizeof=0X20)
00000000 Sig             dd ?    // signature = \'HpiP\'
00000004 EndPointDesc    USB_ENDPOINT_DESCRIPTOR ?
0000000B                 db ?    // padding,alignment
0000000C Flags1          db ?    // flags
0000000D Flags2          db ?
0000000E Flags3          db ?
0000000F Flags4          db ?
00000010 field_10        dd ?    // unknown
00000014 EndpointPointer dd ?    // ENDPOINT*
00000018 ListEntry       LIST_ENTRY ? // link pipe handle to device handle\'s PipeHandleListHead member
00000020 PIPE_HANDLE     ends


很简单的一个结构
现在来看open endpoint这个函数
先照样作一些检查.然后为endpoint结构分配内存.因为这个endpoint结构不仅仅是usbport使用.miniport也会使用.所有这个结构的大小是不固定的.miniport在注册自己的时候会指定自己所需要的结构大小,usbport在这个大小的基础上加上自己所管理的大小来决定要分配多大的内存.
然后usbport作很多的初始化工作.设置大部分endpoint结构的成员.必要的时候还需要miniport的帮助.
完成以后把这个pipe handle连接到device的pipe handle list header上面.同时把新创建的endpoint也连接到由fdo维护的全局endpoint list header上面去

endpoint是一个很大的结构,0x160个字节(checked xp版本),篇幅原因不一一列举了.看几个与我们分析相关的成员
00000000 ENDPOINT        struc ; (sizeof=0X160)
00000000 Sig             dd ?    // \'PEch\'
00000010 FunctionDevice  dd ?     // host controller fdo
00000024 DeviceHandle    dd ?     // device handle
00000030 CurrentState    dd ?     // endpoint state
00000034 NextState       dd ?     // those two field will be protected by StateLock(see below)
00000038 FrameNumber32Bit dd ?    // for iso,current frame number
0000003C WorkerRoutine   dd ?     // core work routine
00000040 ActiveTransfer  LIST_ENTRY ? // activer transfer list header
00000048 PendingTransfer LIST_ENTRY ? // pending transfer list header
00000050 CancelTransferList LIST_ENTRY ? // cancel transfer list header
00000058 AbortIrpList    LIST_ENTRY ? // abort irp list header
00000060 EndpointListEntry LIST_ENTRY ? // link all endpoints together to fdo\'s list header
00000068 AttendLink      LIST_ENTRY ?  // see below
00000070 StateChangeListEntry LIST_ENTRY ? // endpoint state change list entry linked to fdo\'s list header
00000078 ClosedLink      LIST_ENTRY ? // for lazy close,linked to fdo\'s list header
00000098 FlushList       LIST_ENTRY ? // flush list entry,linked to fdo
000000B8 ResourceLock    USB_LOCK ?   // spin lock for this struct
000000D4 StateLock       USB_LOCK ?   // spin lock for endpoint\'s state
000000F0 SavedResIrql    db ?    // save irpl
000000F1 SavedStateIrql  db ?
000000F8 CommonBuffer    dd ?    // for miniport driver COMMON_BUFFER*
000000FC DeviceAddress   dw ?    // device addr,copied from device handle
000000FE EndPointAddr    dw ?    // endpoint number
00000100 MaxPacketSize   dw ?    // max packet size
00000102 Period          db ?    // period
00000104 DeviceSpeed     dd ?    // device speed
00000108 BandWidth       dd ?    // bandwidth
0000010C Offset          dd ?    // schedule offset
00000110 TransferType    dd ?    // transfer type
00000114 Direction       dd ?    // in or out
00000118 CommonBufferVir dd ?    // for common buffer
0000011C CommonBufferPhy dd ?
00000120 CommonBufferLen dd ?
00000128 MaxTransferLen  dd ?
0000012E PortNumber      dw ?
0000013C ClientContextPointer dd ? // point to miniport endpoint context
00000160 ENDPOINT        ends


许多成员的作用就像他的名字所描述的一样
几个特别提出来要注意的地方
首先是state.有两个成员表征了endpoint的state信息.
state是用来描述当前endpoint的状态的.在不同的状态下面对于发送到endpoint的urb的处理方式是不一样.这个会在后面看到

然后是诸多的list entry,他们大多数是用来作urb的排队的.后面也会一一看到用法

workroutine是一个函数指针.他的存在是因为发送到root hub的urb必须特殊处理.而不用转换成usb信号出现在usb总线上面.所有root hub对应的两个endpoint的workroutine是特别设计的.其他的endpoint对应的workroutine是一个通用的函数.
这个workroutine要作的工作就是处理urb

至于那个common buffer,多数情况下是不是用来作传输用的,因为usbport所使用的dma是一个master而且还能scatter-gather的..所以大部分的urb都直接使用他自己的buffer本身占用的物理页,而不需要一个额外的copy操作.只有在某个buffer很不幸的跨越了两个不连续的物理页的时候才需要使用到这个common buffer作额外的copy操作.特别注意这个common buffer是提供给miniport driver使用的.而不是由usbport来使用的..miniport driver还会从这个common buffer里面提交一些内存来作额外的维护信息.

create device就到这里.基本操作就是分配device handle结构.分配endpoint zero 的结构.链接他们到各自对应的list header上面去

device创建好了以后.usbhub再发出另外一个initialize device的请求.对于usbport来说这个请求唯一的处理就是为其分配一个地址,并且发出set address的urb

完成这两步,这个device就能交付使用了.
使用的时候第一个步骤就是select configuration
device的驱动程序读取device的configuration descriptor然后选择适当的interface构造一个select configuration的urb传递下来.中间的处理由usbhub完成,暂时忽略,假定这个urb到达了root hub的pdo.经过一系列的检查(这个部分会在urb的处理的时候再次提到).最终来到处理函数_USBPORT_SelectConfiguration

首先为其分配一个config handle的结构
00000000 CONFIG_HANDLE   struc ; (sizeof=0X14)
00000000 Sig             dd ?    // \'HgfC\'
00000004 ConfigDescPointer dd ?  // ptr config desc buffer 
00000008 InterfaceListHead LIST_ENTRY ? // link interfaces together
00000010 ConfigDesc      dd ?   //  USB_CONFIGURATION_DESCRIPTOR buffer
00000014 CONFIG_HANDLE   ends


很简单的一个变长结构(因为configurtation descriptor是变长的),实际上的configuration descriptor就跟在这个结构的末尾,从0ffset 0x10开始.

当然.device handle结构的config handle成员指向了这个新分配的内存.

接下来发送一个set configuration的urb到设备.
然后一一open 这个select configuration里面所描述的interface.并且填写将来要返回的USBD_INTERFACE_INFORMATION结构

填写information结构是一个很简单的操作,所有必须的信息都在前面的步骤里面完成了.我们只是看看open interface完成的操作

首先还是要分配结构
00000000 INTERFACE_HANDLE struc ; (sizeof=0X3C)
00000000 Sig             dd ?   // \'HxfI\'
00000004 ListEntry       LIST_ENTRY ? // linked to config handle\'s interface list header
0000000C HasAltSetting   dd ?
00000010 InterfaceDesc   USB_INTERFACE_DESCRIPTOR ?
00000019 Padding         db 3 dup(?)
0000001C PipeHandle      PIPE_HANDLE ?
0000003C INTERFACE_HANDLE ends


这个结构是变长的.结尾由若干个pile handle结构.个数当然由interface desc里面的endpointnumber指出

结构分配完了..照例对各个成员进行初始化
然后调用open endpoint函数(如上所述)

最后再把这些个interface handle连接到config handle上面.工作就完成了...

客户驱动程序在这个select configuration urb返回的时候就能查看USBD_INTERFACE_INFORMATION结构一一了解各个自己感兴趣的东西.当然其中最最主要的就是所返回的pipe handle.正如你所想象的那样.他就是一个指向PIPE_HANDLE结构的指针.

客户驱动保存这个指针,因为随后的很多urb都需要你去填写这个pipe handle成员.usbport也需要使用这个指针去寻找对应的endpoint结构.

select configuration完成以后,就能开始传输数据了,这也就是下来的内容了.urb的处理

在进入下一个主题之前我总结几个事实让大家注意

1.对于每个usb总线上的设备,usbport在usbhub的帮助下为其创建一个device handle,并把这些device handle链接到一起,并为endpoint 0 创建一个pipe handle.

2.在进行select configuration的时候,usbport创建一个config handle结构,保存其指针到对应的device handle,然后创建若干个interface handle,全部链接到config handle上去,对于每个interface里面的全部endpoint,为其创建一个pipe handle,全部连接到对应的device handle上面去.对于每个pipe handle,为其创建一个endpoint结构,保存其指针到pipe handle,并把所有创建的endpoint全部链接到一起.

3.客户驱动必须要保存由select configuration所返回的pipe handle,并在随后的urb里面恰当的填写这个值

上面的这些若干个连接连接就是usbport维护管理这些结构的关键.

维护管理用的基础结构了解好了.接下来就是urb的处理问题了.

说到urb的处理,就不能不提一些internal io control的问题.都知道urb是通过一个internal io control提交的.除了这个以外还有若干个其他的internal io control.比如用于获取port状态的,比如用于reset port的.等等等等.这些处理这里先不提.因为大部分的处理都是由usbhub完成的.usbport单独完成的功能很少.还有部分是两个配合完成的.这个留到usbhub的地方再回头来说

首先明白一个事实,客户驱动把urb提交给的是自己的pdo,这个pdo作一些过滤动作以后直接提交给了root hub的pdo.至于是不是这样的一个情况.等到说起usbhub的时候就明白了.现在先假定如此

usbport在一份数据结构的帮助下对每个urb作一些检查.然后转到特定的处理函数.先看这个数据结构的定义
00000000 URB_DISPATCH_TABLE struc ; (sizeof=0X14)
00000000 DispatchRoutine dd ?   // process routine
00000004 TransferLen     dw ?
00000006                 db ?   // padding
00000007                 db ? 
00000008 bmRequestDirection db ? // setup packet
00000009 bmRequestType   db ?
0000000A Recipient       db ?
0000000B bRequest        db ?
0000000C Flags           db ?
0000000D                 db ? 
0000000E                 db ? 
0000000F                 db ? 
00000010 FunctionCode    dd ?
00000014 URB_DISPATCH_TABLE ends


很简单的一个结构.TransferLen用于固定大小的传输,用来检查说提交的buffer大小是否正确,如果是0,则不检查大小信息
接下来的几个成员用于control transfer用来填充setup packet
许多的urb都不需要你完全的填充所有的setup packet成员.usbport在这里会为你填充这些已知的固定的成员

flags则是控制usbport的操作方式的.下面会详细的解释
function code则是指明这份数据是对应哪个function的
理所当然的,usbport为每个function code准备一个这样的结构构成一个数组.

processurb(pUrb)
    check function code
    validate pUrb->DeviceHandle
    if(UrbDispatchTable[FunCode].Flags & 4)
    {
        // force usbport to use default pipe
        get pipe handle from device handle
        save it to pUrb
    }

    if(UrbDispatchTable[FunCode].Flags & 1)
    {
        // actual transfer needed
        if(UrbDispatchTable[FunCode].Flags & 8)
        {
            // no transfer buffer needed
            pUrb->TransferBuffer = 0
            pUrb->TransferBufferLength = 0
            pUrb->TransferBufferMdl = 0
        }
        validate pipe handle
        if(pUrb->TransferBufferLength !=0 && 
           pUrb->TransferBufferMdl == 0)
        {
            IoAllocateMdl();
            MmBuildMdlForNonPagedPool();
            set a flag in UrbHeader,indicates that when we
            complete this urb,we must free the mdl
        }
        
        allocate a transfer struct
     }

     check transfer length
     status = call UrbDispatchTable[FunCode]->DispatchRoutine(...);
     if(status != pending)
         complete the urb

     return status;


proccess urb是一个公共的处理函数.他根据dispatch table的flags成员作一些有限度的处理然后交给真正的dispatch routine处理

这个dispatch routine就各式各样了
大致分成4类.
1类就是不需要transfer结构了.参考上flags & 1非0的分支
这类urb大多直接完成了,比如select configuration,比如get frame length

2类属于control 传输.这类就根据dispatch table里面的那些setup packet成员填充自己的setup pack结构.然后将transfer排队

3类属于interrupt or bulk 传输,直接排队transfer

4类属于iso transfer,最主要的是要检查各个Packet.而且要设置start frame number,最后还是要排队transfer

不需要排队的urb大多是一些没有实现的urb,比如set frame length,或者是一些要特别处理的,比如select configuration

其他的最终都是要排队transfer的.

特别注意参加排队的是transfer结构,而不是irp或者其他
对照endpoint结构的那些成员PendingTransferList等等就能明白,排队的对象并不是irp

transfer 也是一个不小的结构
00000000 TRANSFER        struc ; (sizeof=0XC0)
00000010 Direction       dd ?
00000014 TimeoutInterval dd ?
00000018 SubmitTime      LARGE_INTEGER ?
00000024 TransferedLen   dd ?
00000028 Status          dd ?
0000002C Irp             dd ?
00000030 pEvent          dd ?
00000034 UrbPointer      dd ?
00000038 TransferListEntry LIST_ENTRY ?  // link entry
00000044 MappedRegisterBase dd ?   // for dma
00000048 NumberOfMappedRegister dd ?
0000004C TransferDirection dd ?
00000050 TransferBufferLen dd ?
00000060 SetupPacket     URB_SETUP_PACKET ?
00000068 TransferMdl     dd ?
00000070 AdapterDBList   LIST_ENTRY ? // for double buffer
00000078 ParentPointer   dd ?     // split 
0000007C EndpointPointer dd ?
00000080 ClientTransferPointer dd ? // passed to miniport
00000084 ChildTransferListHead LIST_ENTRY ?
0000008C SplitTransferListEntry dd ?
00000094 IsoTransferInfo dd ?  // iso
00000098 sgList          SG_LIST ? // scatter-gather list
000000C0 TRANSFER        ends


留下了一些分析相关的成员
其中如果这个transfer对应有irp则会设置Irp成员,这个是可以选的
如果没有对应irp也是可以的,那么怎么知道这个transfer完成了呢?用irp的话还可以使用complete routine,要是没有irp呢?
这就是下面的那个pEvent成员的作用了,他指向一个event,完成的时候会设置这个event

还有几个list entry.存在的主要原因就是允许传输大于MaxTransferLength的数据,那么就必须把原来的buffer切割成小的buffer.为每个buffer创建一个child transfer,这些list entry就是用来管理这个的

至于AdapterDBList.则是上面说的某个buffer跨越了非连续的两个物理页的情况下,用于miniport通知的,miniport必须要使用额外的缓冲而不是transfer所提供的缓冲,所以usbport必须要提供空间来保存这些额外的缓冲信息

最后的是sgList,usbport把要传输的buffer映射成一个一个的物理页用sgList这个结构来描述.这个结构会传递给miniport driver使用.

好了,来看真实的排队情况
usbport通过调用_USBPORT_QueueTransferUrb函数来排队某个urb
这个函数很简单
_USBPORT_QueueTransferUrb(pUrb,pEndpoint):
    do some check
    update some fields in transfer struct
    if(transfer associates with an irp)
       call _USBPORT_QueuePendingTransferIrp
    else
       call _USBPORT_QueuePendingUrbToEndpoint

    call _USBPORT_FlushPendingList


根据transfer是否关联有irp调用不同的函数
在有irp的情况下首先是要设置irp的cancel routine,然后再调用
_USBPORT_QueuePendingUrbToEndpoint函数..
也其实就是多一个设置cancel routine的步骤,至于cancel部分后面会有专门的讲解,先放一放.主要来看后面这个函数,更是非常简单

_USBPORT_QueuePendingUrbToEndpoint(pTransfer,pEndpoint):
    link transfer->TransferListEntry to 
    Endpoint->PendingTransfer


接下来当然是_USBPORT_FlushPendingList函数了
看他的名字都知道是在干什么.
这个函数显得很复杂,因为是几个很关键的函数之一
_USBPORT_FlushPendingList(pEndpoint):
    bContinue = TRUE
    do
    {
        if(Endpoint is not root hub\'s endpoint)
        {
            check Endpoint->ActiveTransfer list
            if(is not empty)
            {
                get a transfer from active list
                bContinue = FALSE
                call _USBPORT_CoreEndpointWorker
                if return != 0
                    call _USBPORT_InvalidateEndpoint
            }
        }

        if(bContinue == FALSE)
           break;
    
        check Endpoint->PendingTransfer
        if(is not empty)
        {
            reset canel routine
            if(irp has not been canceled)
            {
                bContinue = FALSE
                call _USBPORT_QueueActiveUrbToEndpoint
                if( return != 0 )
                    call _USBPORT_FlushMapTransferList
                else
                {
                    call _USBPORT_CoreEndpointWorker
                    if return != 0
                        call _USBPORT_InvalidateEndpoint
                }
            }
        }
    }while(bContinue);


或者看了会很奇怪.先不管,我把全部代码流程都列出来
然后再总体讨论
_USBPORT_QueueActiveUrbToEndpoint(pTransfer,pEndpoint):
    bNeedMap = FALSE
    if(Endpoint is stopped || Transfer is aborted)
       link transfer to pEndpoint->CancelTransfer
    else
    {
        if(Transfer\'s length != 0)
        {
            link transfer to fdo\'s MapTransferList
            bNeedMap = TRUE;
        }
        else
        {
            link transfer to pEndpoint->ActiveTransfer
        }
    }
    return bNeedMap


这个函数比较简单.作作判断决定transfer该进入什么样子的list
然后返回一个标记表明是否需要进行map,只有在transfer的transfer length也就是pUrb->TransferBufferLength非0的时候才需要进行Map

_USBPORT_FlushMapTransferList:
    while(!pFdoExt->DoMapping)
    {
        if(pFdoExt->MapTransferList is not empty)
        {
            pFdoExt->DoMapping = TRUE;
            get a transfer from the list
            AllocateAdapterChannel(_USBPORT_MapTransfer);
        }
        else
            break;
    }


也是一个不算复杂的函数
首先检查当前是否在map.如果为false然后检查map transfer list是否是空.不空则取一个处理调用AllocateAdapterChannel,传递的参数是_USBPORT_MapTransfer,在这个函数里面会重新设置pFdo->DoMapping = FALSE.

_USBPORT_MapTransfer是个比较复杂的函数了.他涉及到transfer的切割,sgList结构的填写,少安毋躁..哈哈

00000000 SG_LIST         struc ; (sizeof=0X28)
00000000 Flags           dd ?
00000004 CurrentVa       dd ?
00000008 MappedSysAddress dd ?
0000000C EntriesCount    dd ?
00000010 Entries         SG_ENTRY ? // var size
00000028 SG_LIST         ends

00000000 SG_ENTRY        struc ; (sizeof=0X18)
00000000 PhysicalAddr    ULARGE_INTEGER ?
00000008 VirtualAddr     dd ?
0000000C Length          dd ?
00000010 Offset          dd ?
00000014 field_14        dd ? // unknown
00000018 SG_ENTRY        ends

_USBPORT_MapTransfer(pFdo,pIrp,pMapRegisterBase,pTransfer)
    pFdoExt->DoMapping = FALSE
    // 1. build sgList
    ULONG ulMappedLength = pUrb->TransferBufferLength
    do
    {
        IoMapTransfer(...&ulMappedLength..);
        add physical:virtual to sgList
        ulMappedLength = pUrb->TransferBufferLength - ulMappedLength
     }while(ulMappedLength != 0);
    
    // 2. split transfer
    if this is an iso transfer
        init some field for iso transfer only

    LIST_ENTRY listHeader;
    call _USBPORT_SplitTransfer(..pTransfer,&listHeader);
    while(listHeader is not empty)
    {
        get transfer from list header
        insert to endpoint\'s ActiveTransfer
    }

    // 3. endpoint worker
    call _USBPORT_CoreEndpointWorker
    if return != 0
        call _USBPORT_InvalidateEndpoint


看到这里我想你也能大致CoreEndpointWorker跟InvalidateEndpoint的大致作用.很明显是用来处理active transfer list的

先不管他,来看split
USBPORT_SplitTransfer(pFdoExt,pEndpoint,pTransfer,pListHead)
    init pTransfer->ChildTransferListHead
    if(pTransfer->TransferBufferLength <= pEndpoint->MaxTransferLength)
    {
        insert pTransfer to pListHead
    }
    else
    {
        ASSERT(pTransfer->Type == interrupt ||
               pTransfer->Type == bulk)

        call _USBPORT_SplitBulkInterruptTransfer
    }


很简单的逻辑..只有在transfer length 大于endpoint的最大传输大小并且是interrupt或者bulk的时候才split.
最多传输长度并不是MaxPacketSize,这个长度要大得多.

_USBPORT_SplitBulkInterruptTransfer(pFdo,pEndpoint,pTransfer,pListHead)
    ULONG ulChildrenCount = pEndpoint->MaxTransferLen / pTransfer->TransferBufferLength

    ExAllocatePool(sizeof(TRANSFER) * ulChildrenCount)
    copy from pTransfer to all the new allocated structs
    reinit all the children transfer\'s list entry
    
    ULONG ulLeftTransferLen = pTransfer->TransferBufferLength
    pChildTransfer = FirstChildTransfer
    pOrgSgList = &pTransfer->sgList
    curSgEntryIndex = 0
    curOffsetInEntry = 0
    OffsetInTotalBuffer = 0
    while(leftTransferLen != 0)
    {
        call _USBPORT_MakeSplitTransfer(pFdo,pOrgSgList,pChildTransfer,MaxTransferLength,MaxPacketLength,&curSgEntryIndex,&curOffsetInEntry,leftTransferLength,OffsetInTotalBuffer)

        leftTransferLen = return value
        link pChildTransfer to pListHead
        OffsetInTotalBuffer += pChildTransfer->TransferBufferLength
        pChildTransfer = NextChildTransfer
    }


这个过程有些复杂
简单的讲
这个函数首先计算需要多少个children transfer
然后分配这么多个结构.然后把原来结构的内容copy到新的每个结构里面,但是那些list必须要重新初始化成正确的值.不能指向原来的transfer

然后调用一个辅助函数切割原来transfer的sgList.每个小的transfer的大小最多是pEndpoint->MaxTransfer这么大

sgList是由一个一个的sgEntry组成的.这些sgEntry的长度却未必就是pEndpoint->MaxTransfer这么大.所以必须由两变量来表示当前处理的是哪个entry,以及当前处理到了这个entry的哪个offset
也就是上面curSgEntryIndex和curOffsetInEntry的作用,USBPORT_MakeSplitTransfer辅助函数会修改这两个值

最后注意.原来那个被切割的transfer并没有进入pListHeader
当然也就没有进入Endpoint的ActiveTransfer

USBPORT_MakeSplitTransfer(pFdo,pOrgSgList,pChildTransfer,MaxTransferLen,MaxPacketLen,pCurEntryIndex,pCurOffsetInEntry,LeftTransferLen,OffsetInTotalBuffer)
     ASSERT(MaxTransferLen % MaxPacketLen == 0)

     length = MaxTransferLen
     left = pOrgSgList->sgEntry[*pCurEntryIndex].Length - *pCurOffsetInEntry
     pChildTransfer->sgList.Count = 0
     i = 0
     while(length > left)
     {
         pChildTransfer->sgList.Count ++
         pChildTransfer->sgList.sgEntry[ i ]. Length = left
         pChildTransfer->sgList.sgEntry[ i ].VirtualAddr = pOrgSgList->sgEntry[*pCurEntryIndex].VirtualAddr + *pCurOffsetInEntry
         pChildTransfer->sgList.sgEntry[ i ].PhysicalAddr = pOrgSgList->sgEntry[*pCurEntryIndex].PhysicalAddr + *pCurOffsetInEntry
         i ++
         *pCurEntryIndex ++
         *pCurOffsetInEntry = 0
         LeftTransferLen -= left
         length -= left
         left = pOrgSgList->sgEntry[*pCurEntryIndex].Length
     }

     if(length)
     {
         pChildTransfer->sgList.Count ++
         pChildTransfer->sgList.sgEntry[ i ].Length = length
         pChildTransfer->sgList.sgEntry[ i ].VirtualAddr = pOrgSgList->sgEntry[*pCurEntryIndex].VirtualAddr + *pCurOffsetInEntry;
         pChildTransfer->sgList.sgEntry[ i ].PhysicalAddr = pOrgSgList->sgEntry[*pCurEntryIndex].PhysicalAddr + *pCurOffsetInEntry;
         *pCurOffsetInEntry += length
         if(*pCurOffsetInEntry == pOrgSgList->sgEntry[*pCurEntryIndex].Length)
         {
             *pCurOffsetInEntry = 0
             *pCurEntryIndex ++
         }
     }

     return LeftTransferLen - length


逻辑很简单.按照MaxTransferLen划分切割就行了

接下来就是要真正处理active transfer的部分了
_USBPORT_CoreEndpointWorker(pEndpoint,bRecursion)
     if(!bRecursion)
     {
         if(pEndpoint->Busy)
            return 1
         else
            pEndpoint->Busy = 1
     }

     call _USBPORT_GetEndpointState(pEndpoint)
     state = return value
     if(state == closed)
     {
         pEndpoint->Busy = 0
         return 0
     }
     
     call _USBPORT_PollEndpoint(pEndpoint)
     call _USBPORT_GetEndpointState(pEndpoint)
     state = return value
     if(state == removed)
     {
         pEndpoint->CurrentState = close
         pEndpoint->NextState = close
         insert the endpoint to fdo\'s ClosedEndpointListHead
         pEndpoint->Busy = 0
         return 0
     }
     
     if(pEndpoint->ActiveTransfer is empty &&
        pEndpoint->PendingTransfer is empty &&
        pEndpoint->CancelTransfer is empty)
     {
        pEndpoint->Busy = 0
        call _USBPORT_FlushAbortList(pEndpoint)
        return 0
     }
     
     call _USBPORT_GetEndpointState(pEndpoint)
     state = return value
     if(state != pEndpoint->NextState)
     {
         pEndpoint->Busy = 0
         return 1
     }

     call pEndpoint->WorkRoutine(pEndpoint)

     call _USBPORT_FlushAbortList(pEndpoint)

     pEndpoint->Busy = 0

     return 0


眼见函数就要分析完了.这个家伙马上又引出来若干个函数..哈哈

其中_USBPORT_FlushAbortList函数在讲到cancel的时候再说
他其实是一个无关紧要的函数.

而_USBPORT_PollEndpoint函数则是
调用miniport对应的Poll函数
在这个函数里面,miniport driver总是检查自己的transfer有没有完成了的,如果有就告诉usbport自己完成了某个transfer

至于_USBPORT_GetEndpointState更是简单
如果pEndpoint->CurrentState == pEndpoint->NextState则返回CurrentState.否则返回一个特殊的值ENDPOINT_STATE_TRANSITION
这个值从来不会被设置到CurrentState跟NextState成员上面

至于这个state的用法这里先大致的说说
state表征了endpoint的当前状态
能够设置的值有active,pause,remove,close,后两种是在特别的情况下才设置的.当你关闭一个endpoint的时候(select configuration却填一个0的config desc),endpoint进入remove状态,上面已经看到他马上就转换成了close状态而进入了fdo的closed enpoint list,作lazy close.

而active状态就是正常状态.在这个状态下,endpoint把自己的active list里面的transfer一一提交给miniport driver
而pause的状态是为了处理cancel跟abort的情况的
如果active list里面的transfer有某个被标记成了canceled或者aborted.endpoint就从active状态进入pause状态.进入pause状态以后一一完成那些标记成canceled和aborted的transfer,直到全部都处理完了.就转入active状态.

那上面这些操作都是由谁来完成的呢..当然不用说也是pEndpoint->WorkRoutine要作的事情了

当然,状态的转换并不是这么简单的事情.一个最主要的原因是要同步endpoint的FrameNumber32Bit成员到当前usb host controller的Frame Number,这个主要是用于Iso传输的.所以状态转换的过程是个迂回的.
endpoint首先进入fdo的state change list,然后fdo让miniport driver在下一个start of frame的时候引发一个中断.在isr的dpc里面同步endpoint的Frame Number,然后把endpoint从state change list里面取出来,这样endpoint才算是进入了新的状态,也就是让endpoint的CurrentState跟NextState相同

然后要注意的是.这个函数如果返回值是1的话紧接着调用的就是
_USBPORT_InvalidateEndpoint函数

这个函数告诉一个辅助线程(通过设置一个Event),调用一次_USBPORT_CoreEndpointWorker

miniport driver也会在比如某个传输完成的时候调用这个函数来排队CoreEndpointWorker的调用.使得usbport能继续向其提交新的传输.而不是停下来

上面这段话是什么意思呢
看这个流程
进入pending -> flush pending -> 进入map ->flush map->MapTransfer进入 active->CoreWorker 提交一些但不是全部的transfer给硬件

这里就层层返回了..并没有连续的提交.
那要提交下一个怎么办呢.必须要再次进入CoreWorker才可以
这就是通过InvalidateEndpoint函数来完成的

Invalidate这个函数把你要Invalidate的Endpoint收集到一个list里面链接到fdo的AttendListHead上.然后唤醒work thread
work thread从fdo的AttendListHead上一一取出每个endpoint 然后调用CoreWorker.如果返回还是1,则保存到另外一个list上面
当第一遍循环完了.再把那些还是返回1的endpoint重新链接到fdo的AttendListHead上面.再次排队下一个CoreWorker调用,也就是再次设置Event,而不是直接循环再调用CoreWorker

希望能明白这种排队poll调用的机制
记住一点.transfer的处理是要通过调用CoreWorker才会传递给miniport然后传递给硬件的.InvalidateEndpoint正是引发一次CoreWorker调用的关键

然后来看看实际上的work routine
先看通用的这个.root hub的是一个特殊的函数
USBPORT_DmaEndpointWorker(pEndpoint)
    call _USBPORT_GetEndpointState(pEndpoint)
    curState = return value
    if(curState == active)
       nextState=_USBPORT_DmaEndpointActive(pFdo,pEndpoint)
    else if(curState == pause)
       nextState=_USBPORT_DmaEndpointPaused(pFdo,pEndpoint)
    else
       ASSERT(0)

    _USBPORT_FlushCancelList(pEndpoint)

    if(nextState != curState)
    {
        _USBPORT_SetEndpointState(pEndpoint,nextState)
    }
    else if(nextState == pause)
    {
        _USBPORT_InvalidateEndpoint(pEndpoint)
    }


很简单的一个函数,根据当前endpoint的state调用不同的函数,函数的返回值是新的状态
如果跟以前的状态不同,则要调用一次set state函数.这个函数完成的功能就是我上面说说的那样,把endpoint放入state change list,然后告诉miniport driver引发一次start of frame的中断

对于一直处于pause状态的endpoint,不断的排队CoreWorker调用,直到离开pause状态为止.为什么要这样呢.
处于active状态的endpoint,corewoker的调用自然有miniport通过invalidate来调用,但是pause状态却没有人去调用invalidate,只好自己来了.

接下来来看两个处理函数
_USBPORT_DmaEndpointActive(pFdo,pEndpoint)
    nextState = active
    while(pEndpoint->ActiveTransfer is not empty)
    {
        go through the list,for each
        if(pTransfer had been marked to force complete)
        {
            _USBPORT_QueueDoneTransfer(pTransfer)
            break
        }

        if(pTransfer had been submitted)
        {
             if(pTransfer had been marked canceled or aborted)
             {
                 nextState = pause
                 break
             }
             else
                 continue
        }

        if(pTransfer is iso transfer)
        {
            call miniport\'s IsoTransfer Routine
            retValue = return value
        }
        else
        {
            call miniport\'s SubmitTransfer Routine
            retValue = return value
        }

        if(retValue == sucess)
        {
            set this transfer has been submitted
            set transfer\'s timeout value
        }
        else if(retValue == need_to_wait)
        {
            break
        }
        else
        {
             ASSERT(pTransfer is Iso transfer)
             _USBPORT_ErrorCompleteIsoTransfer
             break
        }
    }

    return nextState


也是个比较简单的函数.只是几个特殊情况要作一下说
首先是第一个if判断.
为什么会有些transfer被标记成了强制完成呢
主要是因为short packet ok这个标记.允许传输比你设置的buffer要小的数据就可能发生这样的情况.
比如你期望传递1000个字节的数据.这个urb在提交的过程中可能超过了最大传输长度.以至于被切成了两个transfer提交给硬件.而硬件却只是传输了其中的100个字节,这样只是完成了第一个transfer,这个时候usbport检测到这种情况,标记第二个transfer为强制完成状态
这也就是这个标记的来源

另外的测试是关于cancel跟abort的
cancel跟abort实际上都只是设置了一个标记,然后就返回了.真正的cancel跟abort的操作还很多,比如transfer结构的内存释放等等,但是如果这个transfer是已经提交给miniport了就必须要先从miniport那里收回这个transfer的控制权才可以.所以并不是在调用cancel或者abort的时候就能完成全部的操作的,只是设置了一个标记.这个部分马上就会讲到

接着注意,transfer一直都在active list里面,并没有取出来作处理,而只是单纯的遍历操作,所以有个submitted标记表示是否有提交给硬件.

至于timeout,是在提交urb的时候设置的一个参数,如果你希望urb只是占用一定的时间进行处理,不管完成了没有,都返回的话就可以使用这个功能,时间上urbport设置了一个定时器,周期的检查每个endpoint的active list里面设置了submitted标记并且启用了timeoute功能(也是设置一个标记)的transfer,时间到了当然是设置起abort标记了.下次endpoint的workroutine运行的时候就能检测到这种情况加以处理了

transfer最终被提交给了miniport的相关routine,这个routine返回一个值表示自己还能继续接受transfer或者必须要等待或者是iso出错(其他的类型的transfer出错不是这样处理的)

注意几个循环继续跟退出的条件以及返回值的变化

在说pause处理之前,就必须要说cancel跟abort了
因为pause状态是专门为他们这两个操作设计的

cancel发生在调用IoCancelIrp的时候,而abort则是发生在客户驱动abort pipe的时候.

先说cancel
irp的cancel是个很经典的问题,随便找个写驱动的书上都会讲到(如果没有,那就扔了别看那个垃圾书了)

cancel最最主要的问题就是同步.
这里就省略了.这种大众化的东西随便都能找到介绍

usbport对cancel的处理方式是这样的:
先设置irp的cancelroutine
然后检查irp->Cancel值
如果为false就表示irp没有被cancel,正常处理
如果为true就将cancelroutine重新设置成0
如果返回得值非0就表示cancelroutine还没有被调用,则complete这个irp,否则表示cancelroutine已经被调用了.正常处理
正常处理就是把irp保存到一个数组里面(当然得在spinlock保护下)

而cancel routine里面则先到数组里面去寻找irp,如果没有找到就直接返回了,找到了则complete这个irp

cancel routine的语意是简单的,任何被cancel的irp都必须存在于那个irp的数组里面,不存在的就表示这个irp已经被完成了

设置cancel routine的地方就必须要保证这个语意
没有被cancel的irp,将来就可能被cancel,就必须要加入到数组里面
正在被cancel的irp,因为cancel routine要从数组里面寻找这个irp,就可以选择是自己完成还是让cancel routine来完成

CancelRoutine(pIrp):
    IoReleaseCancelLock()
    KeAcquireSpinLock(&IrpTableLock,..)
    if(FindIrpInIrpTable(pIrpTable,pIrp))
    {
        RemoveIrpFromIrpTable(pIrpTable,pIrp)
    }
    else
    {
        pIrp = 0
    }
    KeReleaseSpinLock(&IrpTableLock,..)
    if(pIrp)
       IoCompleteRequest(pIrp,...)

EnqueueIrp(pIrp):
    KeAcquireSpinLock(&IrpTableLock,..)
    IoSetCancelRoutine(pIrp,CancelRoutine)
    if(pIrp->Cancel)
    {
        pCancel = IoSetCancelRoutine(pIrp,0)
        if(pCancel)
        {
            KeReleaseSpinLock(&IrpTableLock,..)
            IoCompleteRequest(pIrp,..)
            return
        }
    }

    InsertIrpToIrpTable(pIrpTable,pIrp)
    KeReleaseSpinLock(&IrpTableLock,..)

DequeueIrp:
    KeAcquireSpinLock(&IrpTableLock,..)
    pCancel = IoSetCancelRoutine(pIrp,0)
    if(pCancel)
    {
        RemoveIrpFromIrpTable(pIrpTable,pIrp)
    }
    else
        pIrp = 0
    KeReleaseSpinLock(&IrpTableLock,...)
    if(pIrp)
    {
        Contine to process the irp
    }


这个部分没有什么特别的,irp的cancel模型到处都能找到

usbport所使用的模型就是上面我描述的这样
只不过对于不同阶段的transfer,cancel所表征的语意是不同的
对于pending阶段(也就是还在pEndpoint->PendingTransfer list里面的transfer),cancel作的工作很简单,直接释放掉这个transfer所占的内存就够了.
对于map阶段对于active阶段就不行了,必须要作额外的工作,比如allocated的channel必须要free,对于提交到硬件的transfer必须要收回,这些工作就没有办法在cancel routine里面来完成了.
那么真正的cancel是怎么进行的呢(只是针对非pending list的transfer)

被分成了两步,首先是相关irp的完成,这个部分没有任何疑问直接用STATUS_CANCELED完成这个irp.
第二步就设置到内存,dma资源的是否问题了,这个是在endpoint的workroutine的帮助下完成的

对于要cancel的transfer,cancel routine简单的给他设置一个标记,表示这个transfer已经被cancel掉了.然后InvalidateEndpoint一下,排队CoreWorker调用,进而进入endpoint的workroutine,假定当前状态是一个active状态.
上面的代码可以看到active状态检查到active list里面有设置了cancel标记的transfer的时候,endpoint经过若干个步骤以后就转入到了pause状态,进入了下面这个函数
_USBPORT_DmaEndpointPaused(pFdo,pEndpoint)
    pTransfer = pEndpoint->ActiveTransfer->Flink
    nextState = active
    while(pTransfer != &pEndpoint->ActiveTransfer)
    {
        if(pTransfer had not been marked canceled and aborted)
        {
            pTransfer = pTransfer->TransferListEntry.Flink
            continue
        }
     
        if(pTransfer had been submitted)
        {
            ask miniport to abort this transfer
        }

        remove this transfer from the active list

        if(pTransfer is a child transfer)
            call _USBPORT_CancelSplitTransfer
        else
            insert this transfer to endpoint\'s CancelTransferList
    }


_USBPORT_CancelSplitTransfer函数很简单
把这个child transfer从其parent的children list里面remove出来然后释放掉自己,如果其parent的children list为空了,则把parent加入到endpoint\'s CancelTransferList里面

pause的操作很简单,加入那些cancel和abort的transfer到CancelTransferList里面.那么什么时候完成他们的呢

往上看你就发现了在调用pause处理函数以后立即就有一个FlushCancelList的调用
这个函数的功能自然就水到渠成了
把CancelTransferList里面的transfer一一取出来,释放掉为他们分配的资源.这里就不列举他们的代码了

至于abort.跟上面的操作几乎一样.

回头来问一个问题,上面我们都假定transfer已经进入了active list了.如果transfer还位于map list怎么办?呵呵
上面代码已经给出来了,他们直接就进入了cancel list,而没有去作映射也没有进入active list

最后最后了.active list里面的transfer在Invalidate的帮助下源源不断的提交给硬件.那pending list呢?map list呢?

那是怎么让transfer从pending list进入到map list然后进入到active list里面的呢?

实际上还是由InvalidateEndpoint来完成的
我们知道InvalidateEndpoint设置一个Event,通告一个work thread停止等待,work thread唤醒以后作的第一件事情就是调用函数
_USBPORT_FlushAllEndpoints

这个函数的功能自然就不言而喻了.
他把每个非close状态的endpoint收集到一个list里面
然后为这个list里面的每个endpoint调用一次
_USBPORT_FlushPendingList

最后当然是urb的完成了
上面已经由代码展示了这个完成的处理
调用_USBPORT_QueueDoneTransfer函数把这个完成了的transfer放入到fdo的DoneTransferList里面
然后排队一个dpc
在这个dpc的处理函数里面把他们一一取出来完成
自然少不了调用InvalidateEndpoint函数了

作个总结
大部分urb被usbport排队.先进入pending list,然后在FlushPendingList的时候进入Map list,在MapTransfer的时候进入ActiveList,在CoreWorker的时候提交给硬件

Pending list里面的transfer的cancel跟abort只是直接释放资源
map list跟active list里面的cancel分成两步,一个是irp的cancel完成,一个是资源的释放.资源的释放被推迟了.cancel routine为其设置了一个标记.当coreworker检查到这个标记以后,endpoint进入pause状态,在这个状态下,收集所以设置了cancel跟abort标记的transfer进入cancel list,在FlushCancelList的是cancel掉他们

写完了..今天的文章..

比较的乱自己都觉得...
毕竟usbport本身就是一个400多K的sys文件.似乎不应该是这样说说就能完完全全说明白的

实际上还有好多好多的东西都没有涉及到,比如电源管理,远程唤醒等等,毕竟是一个很大的驱动

[编辑 -  3/7/05 by  tiamo]
tiamo
VIP专家组
VIP专家组
  • 注册日期2002-02-26
  • 最后登录2018-01-09
  • 粉丝17
  • 关注4
  • 积分50分
  • 威望142点
  • 贡献值1点
  • 好评度40点
  • 原创分2分
  • 专家分15分
  • 原创先锋奖
  • 社区居民
5楼#
发布于:2005-03-20 02:12
今天要说的是usbhub

usbhub他是一个usb hub device的驱动程序

当kernel枚举到某个usb host controller的时候,usb host controller就会创建一个pdo,这个pdo代表了跟host controller关联到一起的root hub,windows为这个pdo加载的驱动程序就是usbhub.sys...

usbhub致力于usb设备的枚举,pnp管理,电源管理这些事务,而实际的usb的数据信息传输则是由usbport来完成的.所以在usbhub这个驱动程序里面并没有多少usb 2.0跟usb 1.1的差别

usbhub主要完成的功能,也就成了今天这个文章里面最主要的内容

列一个表,讲解的顺序也就如下
1.DriverEntry
2.AddDevice
3.pnp for hub fdo
4.pnp for pdo
5.internal io control dispatch
6.power dispatch

上面的顺序也几乎是一个sys驱动里面的routine所被调用的顺序
重点放在pnp跟power上面
而pnp的部分又被分成了两个一个是for fdo的,一个是for pdo的
重点中的重点是fdo的query bus relationship的处理
而其他的比如start,stop,remove这些只是稍微提一下
而for pdo的部分,query id,query text,query caps这些算是比较重要的处理,但是这些处理的过程都是很显而易见的

至于power部分.遵循windows的通用power处理模型

internal io control则是对外的接口.大部分的usb device的请求都是用过他来进行了
这里简单的列一个表.说明大部分的io control的处理语意.简单流程.完成的功能.这里还要设计到部分usbport的功能

好.进入主题
1.DriverEntry.简单.设置了大多数的dispatch routine,driver unload,add device,然后query了部分reg config,然后为usb的serial number generation作了些准备就return.
至于都设置了哪些dispatch routine
如下
create 简单完成
close 简单完成
device control 面向user mode的那些诸如get node info请求
internal device control 面向kernel mode的请求,下面详细介绍
pnp 下面详细介绍
power 下面详细介绍
system control 交给wmilib完成

至于什么是serial number generation,以及主要致力于在这个generation过程中所分配的资源的释放问题的driver unload routine,会在说到这个serial number的时候交代

2.AddDevice,当系统枚举到一个usb hub的pdo的时候,加载usbhub.sys然后在适当的情况下调用这个函数.
这个函数作的工作很简单
无外乎就是创建一个fdo,然后初始化他的device extension的诸多成员.usbhub同时在这里初始化了wmilib的调用信息.为自己的fdo的wmi注册

3.pnp for fdo
完成了adddevice调用以后,kernel就会开始发送pnp的irp到usbhub,这些irp都是为这个新创建的fdo准备的

前导的query caps这个很简单,设置一个完成routine,然后传递下去
在complete routine里面设置了Surprise Remove OK,这个设置会阻止safe removal hardware对话框以及任务栏图标的出现.特别注意一点,他是为hub本身这个设备而设置的.对于连接到hub上的设备来说,设置还是不设置这个标记的决定权在于那个设备所对应的fdo的驱动程序.(当然还有filter do)

接着是start
在这个irp里面,首先把这个irp传递下去,等待lower driver 完成这个irp,然后hub再完成自己的初始化,诸如获取root hub的pdo,获取device descriptor,获取hub descriptor,获取configuration descriptor,计算自己用于remote wakeup的power state,获取由usbport提供的USBHUB跟USBD两个interface以备将来使用.接着hub 发出一个select configuration的urb,等待完成,保证必要的结果,接着分配一个重复使用的irp(通过在complete routine里面返回more process required),并利用自己的device extension的空间构造一个用于interrupt transfer的urb,同时为这个urb分配必要的输入输出缓冲区.接着hub初始化他的每个port,然后发出这个interrupt urb

题外话.这个urb是用来干什么的?
按照usb的规范.hub除了默认的用于control transfer的endpoint0以外,还有一个用于interrupt transfer的endpoint,这个endpoint用来通知host有一个变化发生到了hub或者hub的某个port或者某些ports上面.
他的输出情况是一个bitmap,这个bitmap的每一个位代表了发生变化的port编号,而第0位代表了hub本身.
比如.如果interrupt transfer返回的数据缓冲里面的2进制值是00101就表示hub本身和端口2(从1开始计算)发生了变化,当host解析这个数据以后就应该对发生变化的hub或者端口发出get status请求来得到具体是发生了什么样子的变化,从而决定应该作什么样的操作

一般的情况下.一个比较智能的hub会用nak回答这个interrupt,直到实际上检测到了变化为止.而不是返回一个全0的bitmap,对于nak的transfer.usb host controller的miniport driver会自动的重试.

这个urb的处理是整个usb fdo处理的核心部分.hub枚举连接到其上的设备也是依靠他来完成的,这个先放一放,下面会有专门的部分讲解.

接下来的irp就不一定有顺序了
我们一个一个的来看吧
query remove
cancel remove
query stop
cancel stop
这些都是为pnp状态变化作准备的
他们都设置了Irp->Status = success
然后传递给下层驱动了

stop
首先检查连接到hub上的device有没有被stop(windows在stop hub的时候会首先发送stop到这些device去),如果没有,就表示这个device的driver没有正确的处理stop这个请求,hub主动的stop掉这些device对应的pdo,接着hub作一系列的cleanup工作.
诸如
cancel掉自己的wait wake power irp,cancel掉自己的idle request irp,用delete pending 完成连接到自己上面的device所发出的wait wake irp以及idle notification irp,abort掉自己在start里面发出的urb,free为这个urb所分配的irp,以及输入输出缓冲区,然后通知usbport 那些为连接到这个hub上的device所创建的device handle应该要被删除了(通过在start的时候所获取的interface完成).disable 掉自己所有的port,释放为自己的device descriptor,hub descriptor,configuration descriptor所分配的空间,然后发出一个设置了null configuration descriptor的select configuration urb来unconfig 这个hub.最后,当然这个irp传递给了下层驱动.

surprise removal
为什么这个放到这么前面来说?因为大多数情况下在remove之前都会遇到他.当某个设备在某次query bus relation的时候没有被返回,kernel就会在恰当的时候发送他到那个消失了的device去,而当这个device open handle变成0(不是pointer count)的时候,kernel再发送remove到这个device..这个部分可以参考ddk的描述

fdo的处理很简单,设置status = success,然后传递下去

remove
跟stop差不多,首先remove掉那些连接到自己上的device并且没有处理remove操作的pdo(所有连接到自己上面的pdo都被放入了一个数组里面,如果处理了remove irp,pdo会把自己从这个数组里面删除,这样fdo只用遍历这个数组就知道哪些pdo还没有处理remove操作了,另外,在创建pdo的时候,也就是在query bus relation的时候,pdo被加入到了这个数组里面).然后hub作如同stop一样的clean操作,因为按照规范,stop并不会作为remove的前导irp发送,所以完全可能一个处于start状态的设备在没有收到stop的情况下收到remove请求,当然hub保留了一个标志在其device extension里面,以避免对一个已经完成了clean操作的设备再作第二次clean操作.
clean完了以后.hub取消fdo的wmi的注册,然后传递remove下去,然后deattch自己,然后delete自己.

关于hub fdo的start,stop,remove就如上.
hub fdo除了处理这些状态变迁的pnp irp以外(当然还有query caps和query relation).还处理了一个pnp irp,就是query device state

hub fdo根据自己保存的当前状态(hub的状态变化会通过上面那个interrupt urb返回)来回应query device state.

下面就是重头戏了.query device relation的处理
正如大多数的bus 的fdo一样.只是处理bus relation

(注:大多数bus的fdo处理bus relation,而bus枚举出来的pdo处理target relation,usbhub也是这样,而其他的relation,据我跟踪到的情况,pci会为有着multi function的device处理remove relation)

先说这个irp会在什么时候被发送
kernel会在start以后发送这个irp,kernel还会在某些特别的情况下发送这个irp,比如当query device state返回了那些错误的值的时候

但是大多数情况是因为某个地方用busRelation作为参数调用了IoInvaliateDeviceRelations函数而引起kernel发送这个irp

usbhub也是一样.这里先回到上面这个那个urb的处理上来

当hub硬件检测到某个port发生了状态变化的时候,这个urb被完成,usbhub设置的complete routine被调用,usbhub读取发生变化的port的状态,发现了是属于一个连接状态变化(connection change),也就是说或者是一个设备被连接了进来,或者是一个设备被拔了下来,usbhub就会调用IoInvalidateDeviceRelations函数.(实际上的处理并不只是port的connection变化处理,还有hub状态变化,以及port的其他状态变化,比如reset完成,resume信号被检测到等等处理,而且按照规范,还会通过control pipe发送clear feature的request来应答硬件所报告的这个变化情况)

hub的fdo收到了这个irp以后,遍历自己的每个port,获取其状态,根据其状态作进一步处理
queryBusRel:
    for(i = 0;i < NumberOfPorts; i ++)
    {
        get port i + 1 status // base is port 1
        if(status & connected)
        {
            if(fdoExt->portData[ i ]->pdo)
            {
                ref the pdo
            }
            else
            {
                reset port i + 1
                create pdo,do some initialization (*)
                ref the new created pdo
                fdoExt->portData[ i ]->pdo = new created pdo
            }
        }
        else
        {
            if(fdoExt->portData[ i ]->pdo)
            {
                pdoExt->ReportMissing = TRUE
                free pdo\'s serial number string
                tell usbport to remove pdo\'s device handle
                disable port i + 1
            }

            fdoExt->portData[ i ]->pdo = 0
            fdoExt->portData[ i ]->connect = notConnected
        }
    }

    pBusRel = ExAllocatePool()
    pBusRel->Count = 0
    for(i = 0; i < NumberOfPorts; i ++)
    {
        if(fdoExt->portData[ i ]->pdo)
           pBusRel->DeviceObject[pBusRel->Count ++] = fdoExt->portData[ i ]->pdo
    }

    pIrp->IoStatus.Information = pBusRel


大致的处理就如上,逻辑上是非常简单的

下面来看上面标记有*的地方的处理.这个是hub为连接到其上的device作的创建初始化工作

createPortDevice:
     generate a name(such as usbpdo-2)
     IoCreateDevice
     Device->StackSize = roothubPdo->StackSize
     use port number to build instance string
     tell usbport to create new device handle
     save the device handle to the pdo\'s extension
     reset port,e.g. reset the device
     tell usbport to init the device(allocate a usb addr)
     get the device,configuration desc for the device

     if the device reports a serial number string
        verify the reported serial number string
     if verify failed or the device didn\'t report
        if serial number string generation isn\'t disabled
            generate a serial number string for the device

     special check the new device is a hub or not
     if the device is a hub
        save the only one interface descriptor to pdoExt


大部分的工作都是由usbport来完成的,主要就是用于usbport维护device handle的数据结构的创建,分配usb address

然后按照需要为这个usb device生成一个serial number string
这个serial number string被用在了两个地方
一个是用来回应Query instance id请求,一个是用来设置caps的UniqueId成员

他是使用一个逐次增加的数字来生成这个string

usbhub为每个设备记录一份数据,里面放置有vendor id跟product id还有device object
00000000 SERIAL_NUMBER_TABLE_ENTRY struc ; (sizeof=0XC)
00000000 idVendor        dd ?
00000004 idProduct       dd ?
00000008 PhysicalDevice  dd ?
0000000C SERIAL_NUMBER_TABLE_ENTRY ends


然后把这些entry组成一个数组
每次生成string的时候都遍历这个数组
查找具有相同的vendor id和product id并且PhysicalDevice非空的元素并计数
直到找到一个PhysicalDevice为空(以前有,然后被删除了)或者整个数组都遍历完了为止

然后用计算出来的数字生成一个Inst %d的string作为他的serial number string
上面这个数组是动态分配增长的.所以在driverunload里面进行释放工作

最后注意一点.新设备的stacksize是跟roothub的pdo的stack size相同的.这是因为发送到pdo的irp要么被完成了(比如create,close,power,wmi,pnp)要么被传递给了root hub pdo(也就是internal io control)

那些被传递给root hub pdo的internal io control不会再往下传递了.所以这里并没有使用rootHubPdo->StackSize + 1这个数字,因为+1是不需要的

注意这个事实.internal io control是直接传递给root hub pdo的
而不是传个fdo,然后再pdo再fdo再pdo这样的方式.如下图
root hub pdo
    |
    +---root hub fdo
           |
           +----hub A pdo
                 |
                 +---hub A fdo
                     |
                     +---device pdo
                          |
                          +--device fdo


一般的讲,urb在device fdo的地方构造,然后用internal io control传递给device pdo,他是由 hub A创建的,device pdo直接把这个irp传给了root hub的pdo,而不是交给hub A fdo,然后再交给hub A pdo.....这个在说到internal io control的处理的时候就会知道原因

接下来要说的就是为这个新创建出来的pdo所发送的pnp irp

4.pnp for port device pdo
同样先是query caps
rawDevice = 0,removable = 1
如果serial number generation is enabled,uniqueId = 1
如果wakeup is supported
DeviceD1 = DeviceD2 = WakeFromD0 = WakeFromD1 = WakeFromD2 = 1

然后根据fdo的caps在start的时候所计算的device power state map设置pdo的caps里面的device power state map

最后完成这个irp

start port pdo
创建device interface,如果是hub则是_GUID_DEVINTERFACE_USB_HUB否则则是_GUID_DEVINTERFACE_USB_DEVICE,然后按照需求进行restore(多发生在stop了以后再start的情况,会重新通知usbport创建device handle(stop会remove掉这个device handle),还会进行reset),然后注册pdo的wmi,最后完成这个irp

query remove
cancel remove
query stop
cancel stop
直接用success完成irp

stop
complete wait wake irp,complete idle notification request irp,告诉usbport remove 这个pdo的device hanlde(start会重新创建这个device handle).然后disable掉pdo连接的port,然后删除start的时候注册的device interface,最后完成这个irp

surprise removal
删除device interface然后完成这个irp

remove
complete wait wake irp,complete idle notification irp,删除device interface.disable port,remove device handle,清除掉fdoExt->portData数组里对应的pdo指针和connect成员
释放掉serial number的内存,取消pdo wmi的注册,最后delete device,完成irp

query device relation
target relation,简单到不能再简单

query interface
因为usbport对于_USB_BUS_INTERFACE_USBDI_GUID这个interface要求必须设置interface结构的InterfaceSpecificData为device handle
所以usbhub设置了这个成员然后传递给root hub pdo
这里是第一次看到usbhub为某个request设置device handle,后面还会看到internal io control的request也会设置device handle成员.正是因为usbhub无条件的设置这个device handle,所以irp直接传递给root hub的pdo的,一来能节省不必要的传递过程,二来因为usbhub这种强制的设置功能.如果传递给fdo再pdo的话,device handle总是被usbhub用pdo的device handle修改,这样device handle成员就发生了变化.这是不允许的

query bus information
返回 GUID_BUS_TYPE_USB

query resource requirement
这个irp的处理有点奇怪.
都知道usb设备是不需要硬件资源的.usbhub的处理方式只是设置了一个标记.因为kernel在为pdo创建device key的时候会发送一系列的irp到pdo.只有在这些irp都成功了以后,kernel才会创建或者更新device key,这个irp就是其中一个.从源代码上来看.他并不算是最后一个,但是确是这一系列的irp中只发送一次的一个.只有在这个irp发送完成了以后.诸如IoOpenDeviceRegistryKey这些函数获取到的信息才真实的反应了当前这个设备的信息.正如你想的那样.有地方要依靠他们的存在.

query device state
如果fdo的处理一样,根据pdo的状态回应这个query irp

query device text
用irp里面的LCID作为language id,然后用device descriptor里面的iProduct作为string index发送get string descriptor request到usb设备,如果成功了则返回,否则使用使用English (United States) = 409h作为language id再次get string descriptor,如果还是失败,则返回generic usb string,他是在driverEntry里面从注册表里面读出来的

query device id
这个处理过程就没有任何好多说的了,也就是用那些idVendor,idProduct生成各种id,利用device tree可以查看到usbhub为pdo生成的各种id string

pnp的处理就到这里
接下来的是internal device control的处理
这个也是usb设备的接口

5.internal device control
再次提到device handle这个东西,传递给usbport的urb必须要设置URB_HEADER->UsbdDeviceHandle成员,很明显这个是由usbhub创建的pdo来完成的.正如上面看到的那样,pdo在通知usbport创建device handle的时候保存这个device handle到了device extension里面,所以pdo会从自己的device extension里面取得这个device handle并用他来设置URB_HEADER里面得成员.这也是为什么urb只是经过一个pdo以后就交给了root hub pdo.因为如果交给了另外一个hub创建得pdo的话,新的pdo会覆盖原来或许本来已经设置正确了的device handle,这当然不行了

也正是这个原因.internal device control最先被处理的应该是hub创建的pdo,而不应该传递给hub的fdo,所以下面的都是pdo的处理

这里列个表,来描述各个internal device control的处理情况

1.submit urb 220003h
usbhub首先根据当前pdo的状态(是否已经start,是否是位于d0状态,等等)适当的failed掉这些状态的urb.如果当前的pdo状态是能够接受urb的状态,则继续

然后检查URB_HEADER->Function对几个特别的function作额外的处理
这些要特别处理的Function是
select configuration
select interface
control transfer
interrupt transfer
iso transfer
get ms feature descriptor

中间的3个transfer会检查ReportMissing状态(查看上面QueryBusRelation的处理),适当的fail这个urb

最后一个ms feature descriptor则完全是有usbhub来完成的.详细的信息可以看这个网页http://www.sourcequest.com/modsContent.htm

对于select configuration的情况.如果是config device,则要对configuration descriptor作一次validate,具体也就是检查他的bLength,bDescriptorType,wTotalLength作一次检查,然后检查在configuration descriptor设置的MaxPower是否大于hub能提供的PerPortPower,如果也通过了,则设置一个complete routine,然后传递给root hub pdo

对于select interface也设置了相同的complete routine

在这个complete routine里面主要是处理带宽不够的情况.设置一个timer.用来记录这个wmi事件.使用这个timer是基于这样的事实:客户端的驱动在遇到select configuration和select interface返回带宽不够的时候重试urb,因为如果在重试成功了以后就会cancel这个timer.

最终这个urb被传递给了root hub pdo,交由usbport进行排队,传输,完成..

2.reset port 220007h
这个的语意是reset device所连接到hub对应的port,也就是说是用来reset device的upstream port.实际上就是发送reset信号到device去.

主要的port reset是由usbhub处理,而reset port也必须reset 由usbport管理的device handle.流程如下
resetUpStreamPort:
    tell usbport to mark device handle busy
    tell usbport to remove device handle but keep the struct data 
    send reset request to hub hardware
    tell usbport to create new device handle
    send reset request for set usb address
    tell usbport to allocate a new address for the device
    tell usbport to use the new created device handle to reinitialize old device handle
    tell usbport to remove the new created device handle


3.get root hub pdo 22000fh
直接交给root hub pdo,root hub pdo会返回两个指针,虽然msdn说一个是root hub pdo,另外一个是host controller fdo,但是实际上两个都是指向了root hub pdo,不过这个地方很奇怪,hub已经保存有这两个指针,为什么不用自己保存的指针填充这个结果而要交给root hub处理呢?

4.get port status 220013h
usbhub处理,发送control urb到hub硬件获得这个port status,然后返回

5.enable port 220017h
usbhub处理,同上

6.get hub count 22001bh
usbhub,root hub都处理,usbhub 先把count += 1然后另外发送一个get hub count到自己的下层驱动,把返回的结果加在count上,返回,为什么要新创建一个urb下传呢?因为原先的irp的stack size的数目不够.

7.get hub name 220020h
usbhub处理,返回hub的symlink name.

8.get bus info 220420h
通过interface的方式交由usbport处理完成

9.get controll name 220424h
同上

10.get parent hub info 22042ch
usbhub pdo返回自己的parent fdo对应的pdo,以及自己的port number

11.get device handle 220423h
usbhub pdo返回保存在自己device extension里面的device handle

12.cycle port 22001Fh
模拟一次unplug and plug event

设置fdoExt->portData[portNumber]->pdo
然后IoInvalidateDeviceRelations.
对比上面的代码就知道会新创建一个pdo,而kernel会发生surprise removal以及remove pnp irp到原先的那个pdo完成删除工作

13.notification 220027h
这个稍微有些复杂,因为他是和power management关联在一起的
说完power management再回头来看他的处理过程

6.power dispatch
这个部分处理很有模版化的趋势
先看fdo的处理

query power
FdoQueryPower
   if(bQuerySystemPower)
   {
       newDeviceState = map system state to device state
   }

   if(newDeviceState == D3)
   {
       if(fdo had submitted wait wake irp)
           cancel it // because we are going to d3
   }

   PoStartNextPowerIrp
   PoCallDriver


wait wake
FdoWaitWake:
    set complete routine = FdoWaitWakeComplete
    PoStartNextPowerIrp
    PoCallDriver


wait wake complete
FdoWaitWakeComplete:
    if(irp->IoStatus->Status == STATUS_SUCCESS)
    {
        request power irp to set device state to D0
        // in the complete routine,complete children port pdo\'s
        // wait wake irp with STATUS_SUCCESS
    }
    else
    {
        complete all children port pdo\'s wait wake irp with
        error
    }


set fdo system power state
FdoSetSystemPowerState:
    if(newSystemState == S0)
    {
        if(currentDeviceState != D0)
        {
            save this irp
            Request set device power d0 Irp
            return pending

            // in the complete routine,call down the saved set system power irp
        }
        start next
        call down
    }
    else
    {
        if(some one need wait wake)
        // hub or device that connected to the hub
        {
            newDeviceState =map system state to device state
        }
        else
            newDeviceState = D3

        if(prevDeviceState == newDeviceState)
        // didn\'t need to change device state
        {
            start next power irp
            call down
        }
        else
        {
            save the set system power irp
            request set device power state irp
            return pending
            // in the complete routine,call down the
            // saved set system power irp
        }
    }


fdo set device power irp
FdoSetDevicePowerIrp:
    if(newState == prevState)
    {
        start next
        call down
        return
    }

    // is not the same
    if(newState != D0)
    {
        cancel interrupt transfer
    }

    set complete routine = FdoSetDeviceStateComplete
    call down


set fdo device state complete
FdoSetDeviceStateComplete:
    if(irp->IoStatus.Status != STATUS_SUCCESS)
    {
        start next
        return success
    }

    if(newState == D0)
    {
        if(prevState == D3)
        {
            query work item = FdoSetPowerD0Worker
            save this irp
            return more_process_required
        }

        submit interrupt transfer
        start next
    }

    return success


fdo set power d0 worker
FdoSetPowerD0Worker:
    power on all ports
    flush changes when the hub was powered off
    invalidate bus relation
    submit interrupt transfer
    start next
    complete this power irp


pdo的power处理相对简单很多

pdo power dispatch

PdoPowerDispatch:
   if(minor == WAIT_WAKE)
   {
       return PdoWaitWake
   }

    if(pdoExt->ParentFdoExt->CurrentDevicePowerState != d0)
    {
        if(minor == set || minor == query)
        {
            request set parent hub d0
            // complete = PdoSetHubD0ForPowerProcess
            return pending
        }
    }

    switch(minor)
    {
    case query
        return PdoQueryPower
    case set
        return PdoSetPower
    default
        status = pIrp->IoStatus.Status
        start next
        complete
        break
    }

    return status


PdoSetHubD0ForPowerProcess:
    switch(minor)
    {
    case query
        PdoQueryPower
    case set
        PdoSetPower
    default
        start next
        complete
        break
    }

    return success


pdo wait wake
PdoWaitWake:
    set cancel routine
    save this irp
    if(!pdoExt->ParentFdo->WaitWakeIrpSubmitted)
    {
       call PoRequestPowerIrp to request a wait wake power irp  to the ParentFdo stack
       start next
       return pending
    }


pdo query power
PdoQueryPower:
    if(bQuery == query device state)
    {
        start next
        return SUCCESS
    }

    newDeviceState = map system state to device state

    if(newDeviceState != D3)
    {
        start next
        return success
    }

    // here new device state = D3
    if(!pdoExt->WaitWakeIrp)
    {
       start next
       return success
    }

    // here new state == D3,and has a wait wake irp pending
    if(newSystemState >= PowerSystemHibernate)
    {
        start next
        return success
    }

    // device new state is d3,this state can\'t wake system
    // and the system go to a wakable state
    // so fail it
    return STATUS_UNSUCCESSFUL


pdo set power state
PdoSetPowerState:
    if(bSetSystemPowerState)
    {
        start next
        complete success
        return success
    }

    if(newState == prevState)
    {
        start next
        complete success
        return success
    }

    switch(newState)
    {
    case D0:
        if(prevState == D3)
        {
            if(pdo is suspended)
                resume it
            else if(pdo is power down)
                power it
           
            check port status
            if(status & connected)
            {
                tell usbport to reinit device handle
            }
        }
        else
        {
            send clear suspend request
            complete idle request
            clear remote wakeup feature
        }
        break

    case D1
    case D2
        if(prevState == D1 || prevState == D2)
        {
            start next
            complete success
            return success
        }

        if(pdoExt->WaitWakeIrp)
        {
            // set hardware to support remote wake
            enable port
            set remote wakeup feature
        }

        tell hardware to suspend port
        break

    case D3
        complete idle notification with status = POWER_STATE_INVALID
        complete wait wake with status = POWER_STATE_INVALID
        if no one request wait wake
            cancel parent hub\'s wait wake irp

        close port
        break
    }


    start next
    complete success
    return success


处理的方式都是显而易见的.遵循ms的power处理的规范
没有什么特别注意的地方.查看msdn就能找到这个处理流程的描述

唯一一个让我觉得奇怪的是
他并没有按照msdn所描述的那样调用PoSetPowerState函数
而且根据我跟踪分析的大多数驱动
比如pci.sys,usbstor.sys,hidclass.sys,usbhub.sys这些都没有调用PoSetPowerState函数...令我觉得很是奇怪

再回头来看idle notification这个request
至于这个idle request是用来干什么的,改设置什么参数.参考msdn

usbhub对这个的处理方式很直接
所以发送到pdo的idle notification request都设置一个cancel routine然后保存起来
在某些地方检查整个hub是否进入了一个idle状态,if and only if所有的port上的设备都发送了idle notification的irp的时候,hub进入了idle状态,于是hub 回调每个port所提供的回调函数,然后往自己的parent hub发送一个idle notification request

而这个idle notification irp的完成.当然是在hub自己的idle request完成的时候一一完成自己的每个port上的idle request

usbhub的就到这里...
打字也是一件痛苦的事情
游客

返回顶部