WDF开发USB设备驱动教程(2)
2010-02-22 20:04
393 查看
PDF全文下载:http://bbs.driverdevelop.com/read.php?tid-120461.html
3.2 获取描述符
上一小节认识了USB的描述符后,这一节就来讲如何从
USB
设备获取它们。我列出了具体的代码,包括获取设备描述符、配置描述符和
String
描述符。看过代码后,大家会觉得在
WDF
中做这些操作,动作非常简洁,堪称舒心。
首先看获取设备描述符,一行代码足矣。
USB_DEVICE_DESCRIPTOR
UsbDeviceDescriptor;
WdfUsbTargetDeviceGetDeviceDescriptor(
IN
pContext->UsbDevice,
// WDF设备对象
OUT &
UsbDeviceDescriptor
// 返回的设备描述符
);
接下来看获取配置描述符。配置描述符囊括了USB
配
置所要用到的全部信息:设备描述(区别于设备描述符)、类描述、接口描述、端点描述。和设备描述符的定长不同的是,由于不同的设备其配置布局,包含的接口
与端点数不尽相同,故而配置描述符的长度是不定的。应该先取得配置描述符的长度,根据长度分配内存缓冲,然后二次获取设备描述符内容。
// 首先获得配置描述符的长度。它是变
长
的,包含了所用接口描述符、端点描述符。
status =
WdfUsbTargetDeviceRetrieveConfigDescriptor(pContext->UsbDevice, NULL, &size);
if(!NT_SUCCESS(status) && status != STATUS_BUFFER_TOO_SMALL)
break;
// 输出缓冲区不够长
if(OutputBufferLength < size)
break;
// 再次调用,正式取得配置描述符。
status =
WdfUsbTargetDeviceRetrieveConfigDescriptor(pContext->UsbDevice, pBufferOutput, &size);
最后我们看String
描述符的情况。
USB
设备的字符串描述符也是由设备固件定义,数量不限,甚至可以没有。它用来表述设备厂商(
Vendor
)对本设备的描述,这包括设备的制造商名称,产品名称,产品序列号,甚至包括接口的描述(不过
Windows
系统似乎不支持这个特性)。不同的字符串通过从
0
开始递增的
String ID
来区分。另外值得一提的是,
USB
协议允许字符串描述符支持多国语言,这样同一个
String ID
可以对应于一个以上的描述符。
String ID
为
0
的字符串描述符专门用来描述
USB
设备说支持的语言(用
Language ID
表示,比如英语的
ID
为
0x0904
)。这样主机可以通过获取设备的
0
号字符串来分析它所支持的语言种类,并获取相应语言版本的字符串描述符。
和配置描述符一样,字符串描述符的长度不确定。我们也是分两次调用,第一次调用获取描述符长度,然后分配内存缓冲区,再二次调用获取描述符内容。下面是CY001_WDF
工程中
GetStringDes
函数的实现,我们可以看到语言
ID
是怎么在这里起到作用的(可惜
CY001
的固件代码目前还只支持英语一种语言,呵呵):
NTSTATUS GetStringDes(
USHORT shIndex,
// String ID
USHORT shLanID,
// 语言
ID
VOID* pBufferOutput,
ULONG OutputBufferLength, ULONG* pulRetLen, PDEVICE_CONTEXT pContext)
{
NTSTATUS status;
USHORT numCharacters;
PUSHORT stringBuf;
WDFMEMORY memoryHandle;
KDBG(DPFLTR_INFO_LEVEL, "[GetStringDes] index:%d", shIndex);
ASSERT(pulRetLen);
ASSERT(pContext);
*pulRetLen = 0;
// 由于
String
描述符是一个变长字符数组,故首先取得其长度
status = WdfUsbTargetDeviceQueryString(
pContext->UsbDevice,
NULL,
NULL,
NULL, // 传入空字符串
&numCharacters,
shIndex,
shLanID
);
if(!NT_SUCCESS(status))
return status;
// 判读缓冲区的长度
if(OutputBufferLength < numCharacters){
status = STATUS_BUFFER_TOO_SMALL;
return status;
}
// 再次正式地取得
String
描述符
status = WdfUsbTargetDeviceQueryString(pContext->UsbDevice,
NULL,
NULL,
(PUSHORT)pBufferOutput,// Unicode字符串
&numCharacters,
shIndex,
shLanID
);
// 完成操作
if(NT_SUCCESS(status)){
((PUSHORT)pBufferOutput)[numCharacters] = L'/0';// 手动在字符串末尾添加
NULL
*pulRetLen = numCharacters+1;
}
return status;
}
获得了这些描述符之后,我们就可以通过对它们的分析,得到USB
设备的详细信息了。比如设备的版本(
1.1
还是
2.0
),有几个接口,接口中的端点数,端点的类型(控制、批量、中断或等时)。
运行CY001
开发板的
UsbKitApp.exe
,点击最上面的三个按钮,可以获得并打印出这些描述符的信息。如下图所示:
描述符按钮:
打印信息:
4. 设备初始化
做惯了WDM驱动的人都知道,驱动初始化在入口函数,设备初始化在
AddDevice
函数,这确是不刊之论。
WDF
框架中,驱动初始化我们已经讲了它的入口函数。然则设备初始化,到底怎么做呢?它是否还是对应到
AddDevice
函数?回答是
NO
。
WdfDriverCreate
调用已经指明了,设备初始化在自定义的PnpAdd函数中完成。大家稍微上翻一两页,就能看到定义PnpAdd函数的地方,不妨再写出来:
WDF_DRIVER_CONFIG_INIT(&config, PnpAdd);
调用
WDF_DRIVER_CONFIG_INIT
宏,并不强制你一定要传入一个有效的函数指针,如果传入NULL指针也是能过去的,只是设备就没有地方可以初始化了。
回过头来讨论PnpAdd函数,大家肯定脑子里已经在想,它和AddDevice是什么关系呢?看看它的函数申明先:
typedef
NTSTATUS
(*PFN_WDF_DRIVER_DEVICE_ADD)(
IN
WDFDRIVER
Driver
,
IN
PWDFDEVICE_INIT
DeviceInit
);
第一个参数是驱动对象,就是DriverEntry
函数中被初始化的那个。
第二个参数,是WDFDEVICE_INIT
结构体。这个结构体颇为复杂,
WDF
未能给出它的具体定义,只是暴露出了一系列
API
用来初始化这个结构体。具体来说,它涉及到了设备初始化的方方面面,甚至更多。比如定义设备名、设备缓冲方式的定义,属于正常的设备对象属性;而注册
PNP
和
Power
回调函数,则已经超出了传统的设备对象属性范围,越界到驱动对象里去了(这些回调函数,更像是驱动对象的分发函数或者分发函数的变体。
WDF
框架对
PNP
和
Power
管理有非常大的变动,内部机理,谁也不晓得,我们顺其自然罢了)。
WDFDEVICE_INIT结构体的初始化
API
颇为丰富。分成了三个系列。对应于普通设备对象(简称
Devcice
)的初始化,专门针对功能设备对象(简称
FDO
)的初始化,和专门针对物理设备对象(简称
PDO
)的初始化。总共加起来大概有
30
来个。对
USB
设备驱动而言,要用到的只是前两者系列
API
。物理设备对象的初始化
API
一般由总线驱动或更底层的驱动使用,生成的物理设备对象,将被上层功能驱动所挂载。
一一弄明白这些API
接口,很是一件烦心事。好在这些
API
的定义到时很
Readable
,有时候看看名称到也能够猜到一二。我下面尽量多分析几个。
PnpAdd函数所收到的这个
WDFDEVICE_INIT
结构体,是已经被初始化过的。最明显的一个理由是,通过它,可以调用
FDO
初始化
API
获得许多设备信息。比如:获取物理设备对象、获取注册表中的硬键、软键(也就是
Hardware
键和
Software
键)、获取物理设备对象的属性(设备
ID
、兼容
ID
等)。这些
API
列于下:
WdfFdoInitAllocAndQueryProperty
WdfFdoInitOpenRegistryKey
WdfFdoInitQueryProperty
WdfFdoInitWdmGetPhysicalDevice
这些API
的具体的使用方法很简单,确实起到了简化操作的目的。
WDF
文档中都有示例代码的。注意,这些
API
必须在
WdfDeviceCreate
被调用之前调用。因为一旦
WdfDeviceCreate
被调用后,
WDFDEVICE_INIT
结构的内容可能就已经变了甚至不存在了。
FDO初始化
API
中剩下的三个,两个是为过滤驱动准备的(
WdfFdoInitSetEventCallbacks
和
WdfFdoInitSetFilter
),一个为总线驱动准备(
WdfFdoInitSetDefaultChildListConfig
),我们就不用管它们了。
回头来看Devcice
初始化系列
API
。这里面涉及最多的是设置
Pnp
和
Power
属性、回调的
API
,由此也可见这两者的复杂程度。
CY001
中用到了一个
WdfDeviceInitSetPowerPolicyEventCallbacks
,下面会讲到
。
另一类是类型注册,用来在注册表中修改物理设备安装属性的(包括Type
、
GUID
、特性等)。它们使得设备即使在被安装后,也能改变它的
class ID
、
device type
这些安装时设定的设备属性。这确实是一件很实惠的事情。拿
CY001
为例,用
inf
文件安装好后,它的给定类
ID
是:
{9048DC75-B91C-4392-925A-44A7269D6BD4}
,类名称是:
CY001 Sample
。打开设备管理器,正如下图所能看到的:
但如果我在PnpAdd
函数中,调用
WdfDeviceInitSetDeviceType
函数并传入参数
FILE_DEVICE_SERIAL_PORT
,那下次再看到CY001
的时候,它的位置就会列于串口设备下面去了。
说一说这些API
的内部机理吧。
Windows
的安装(
Setup
)模块是一套挺复杂的东西,我就不多嘴多舌了。对于已经在系统中安装好的设备,它们的信息是统一被列在注册表中
Enum
和
Class
下的,也就是大家所说的硬件键和软件键。系统的
Setup
系统,正是从这些地方保存并查找设备的。而我们现在所讲到的这一系列的
API
,其工作就是修改设备对象这两个键的位置与值,这样
Setup
系统下次就会把它当成另外一个人看了。
这些API
列于下:
WdfDeviceInitSetCharacteristics
// 比如软盘设备:
FILE_FLOPPY_DISKETTE
WdfDeviceInitSetDeviceClass
// 比如系统设备类:
GUID_DEVCLASS_SYSTEM
WdfDeviceInitSetDeviceType
// 比如改成串口类型
FILE_DEVICE_SERIAL_PORT
WdfDeviceInitSetExclusive
// 独占打开,即一次只能创建设备对象的一个实例
//(对应于应用程序的Handle)
下面具体讲,如何进行USB
设备初始化、配置。
4.1
初始化过程
以前写USB
驱动,程序员大倒苦水,原因之一是
USB
设备的配置太麻烦了。这不禁让我想起了写文件过滤驱动的时候,里面有一个卷设备挂载操作,反反复复,这般那般,简直没完没了。代码还没开始写呢,脑子先被他转晕了。还好
USB
的设备配置任务虽然重(我指的是代码多),但总算都是些基本概念,不用太难为自己的脑细胞。
从USB
设备插入
PC
主机开始,到它能被操作系统识别,要经过一些特定的过程,枚举如下:
a)
设备插入主机后,USB
设备进行复位操作,将物理地址置
0
。
b)
主机检测到有物理设备接入,便通过地址查找的方式,查找地址为0
的
USB
设备;找到后,向
USB
设备发送请求,获取它的设备描述符。
c
)
主机分析设备描述符,并根据实际情况,为新插入设备重新分配一个物理地址(非0
);并把这个新地址,通过
Set Address
命令发送给设备。
d
)
设备收到并保存新地址,此后当主机查询设备的时候,USB
设备即当以此新地址来回应查询请求。
e
)
Set Address成功后,主机向刚分配地址的
USB
设备再次发送请求,获取设备描述符。
f
)
获取设备描述符成功后,主机发送请求获取配置和报告描述符。
g
)
根据获取的描述符,主机配置此USB
设备。
h
)
配置完成,设备正常工作。
上面的这个过程,凡是讲PNP
管理器的书籍,大抵都会讲。我这里仅仅简单列一下,详细透彻的说明,大家去找书看,《
Windows Internal
》就讲得非常详细。
a->d
这四个步骤,是设备被系统识别的过程,是由系统(总线驱动或其他的系统模块)和
USB
设备交互完成的。
e->g
这三个步骤由功能驱动负责来做。
我上面也说过了,以前用WDM
来完成这五个步骤,是比较烦难的。弄弄就是一大堆代码,虽然没有什么灵活机变的地方,但很容易一不小心就搞错了。在这篇文档中,我为了比较可能会举一些
WDM
的示例代码。但我主要想指给大家的路,是一条用
WDF
铺出的林中碎石密径,轻快、干净还漂亮。所以会有好多
WDF
代码示例,教你走,领着看。
4.2
创建WDF
设备
提到设备对象,让人一下子就想到DEVICE_OBJCET结构体。更有些人还会立刻想到《
Undocument Windows 2k
》
里面列出的关于这个结构体每个成员的详细解释。设备对象是最基本的内核对象之一。设备对象未必都对应到一个物理设备。好多“设备”都是存在于逻辑上的,比
如“卷”设备;还有一些设备对象,则连逻辑设备也不是,比如每个驱动都可能会有一个控制设备对象,它们纯粹只是一个“结构体”而已。
但对于代表物理设备的物理设备对象而言,系统通过操作这些对象,起到了实际控制物理设备本身的作用。
从结构体本身而言,DEVICE_OBJCET
够底层,够强大,够
Undocument
。另外,它还够难理解,够难使用,够易出错。用好它的人够厉害,用坏它的人,嗯,够不幸。处于对无数不幸人士的体贴,
WDF
提供了封装对象
WDFDVICE
。对于
WDFDEVICE
,它完全
undocument
(别沮丧),但无比易用,几乎不会出错。
哦,不要忘了,WDF
除了
WDFDVICE
外,还进一步又封装了一个
WDFUSBDEVICE
对象。从从属关系来说,
WDFUSBDEVICE
已经是
DEVICE_OBJCET
的孙子辈了。对于
USB
驱动,这个对象真是太好用了!
WDF对象封装得过于严实。到目前为止,我还不晓得有谁破译出它们内部的定义。这种情况下,黑客们大概是不太欢喜的。
调用WDF
驱动初始化函数后,框架就为驱动对象生成一个
WDFDEVICE
对象。这个对象句柄在
XXX
函数中作为参数传入。可以不保存这个句柄,因为我们需要根据这个对象句柄,生成
WDFUSBDEVICE
对象,只要保存后者就可以了。
要找一个可用来保存自有数据的地方。WDF
为每个框架对象都设计了一个特殊的“环境变量”——不仅仅是这里讲到的设备对象,而是所有框架对象——正可用来保存这些数据。这个“环境变量”,用起来有点像
WDM
设备对象中的设备扩展。但用起来要麻烦很多。
首先要申明“环境变量”的类型和大小。根据大小,框架为设备对象申请一块内存。
其次定义一个函数指针,通过这个函数可以获取“环境变量”。这可真麻烦。但这也是没有办法,因为框架对象是完全密封的,没有办法像设备扩展指针一样直接获取。这项技术说起来还是挺有趣的,我非要给大家说个明白不可。
注:我们使用KMDF
框架进行编程,一般不直接使用原始的
WDM
对象。在这里,我们把
WDM
对象称作原始对象(
RAW
),而把
KMDF
对象称作封装对象(
Wrapped
)。只要愿意,可以对
RAW
对象进行各种形式的封装。大家初学的时候遇到这些东西会感觉比较麻烦,但熟悉之后却能带来编程上的便利,它们都带有定义良好的接口。
我们要找到一个保存WDFUSBDEVICE
对象句柄的地方。
WDF
设备的“环境变量”,相当于
WDM
驱动中的设备扩展,是一个理想的地方。
//
创建WDFUSB
设备
status = WdfUsbTargetDeviceCreate(Device, WDF_NO_OBJECT_ATTRIBUTES, &DeviceContext->UsbDevice);
if(!NT_SUCCESS(status))
{
KDBG(DPFLTR_INFO_LEVEL, "WdfUsbTargetDeviceCreate failed with status 0x%08x/n", status);
return status;
}
上例中调用
WdfUsbTargetDeviceCreate
时候的Device
句柄,是初始化的时候由系统创建的。这个句柄代表了一个
WDFDEVICE
对象,也就是说系统其实已经为我们创建了一个
WDF
设备对象了,我们现在在它的基础上再封装出一个
WDF USB
设备对象。
创建WDF
设备对象是比较简单的,复杂的地方在于设置初始化结构体。我们可以分两个步骤来实现初始化:
1.
注册
PNP
、
Power
回调函数;
2.
设备命名;
对于设备驱动来讲,PNP
、
Power
分发是顶顶重要的,这一点和过滤驱动不同。如何处理好
PNP
、
Power
分发,是设备驱动开发过程中很头疼的事情。不仅事繁,而且事艰。
WDF
框架顶好的一个优点就是为所有的
PNP
、
Power
分发写了默认处理方法。这样我们只要注册少量感兴趣的回调函数,即能将它们轻松处理了。
// 注册PNP与Power回调函数。
WDF_PNPPOWER_EVENT_CALLBACKS_INIT(&pnpPowerCallbacks);
pnpPowerCallbacks.EvtDevicePrepareHardware
= PnpPrepareHardware; // 在此为设备驱动申请系统资源
pnpPowerCallbacks.EvtDeviceReleaseHardware = PnpReleaseHardware;
pnpPowerCallbacks.EvtDeviceSurpriseRemoval
= PnpSurpriseRemove; // 异常移除
pnpPowerCallbacks.EvtDeviceRelationsQuery
= PnpRelation;
pnpPowerCallbacks.EvtDeviceD0Entry
= PwrD0Entry; // 进入D0电源状态(工作状态),比如初次插入、或者唤醒
pnpPowerCallbacks.EvtDeviceD0Exit
= PwrD0Exit; // 离开D0电源状态(工作状态),比如休眠或设备移除
WdfDeviceInitSetPnpPowerEventCallbacks(DeviceInit, &pnpPowerCallbacks);
// 注册回调
// 读写请求中的缓冲区访问方式。默认为Buffered,还包括Direct和Neither。
WdfDeviceInitSetIoType(DeviceInit, WdfDeviceIoBuffered);
上面代码的全部任务,就是初始化结构体对象
WDFDEVICE_INIT
。WDK文档没有给出这个结构体的定义。但有一系列的宏或者方法,被定义了用来对它进行设置。上面的代码仅用到了其中的两个。这个结构体也是相当复杂的,大家还是结合WDK自己参透吧。
4.3 设备命名
这一小节乃是从《创建设备》节中分出来的,为了醒目的缘故。和WDM
驱动一样,设备对象是可以选择被命名的。就是说,设备对象可以被命名,也可以不命名,由程序员自己决定。命名的目的是为了能够被识别和使用,如果无此需要则命名可不必进行。
功能设备对象总是需要命名的,因为功能驱动是用来被User
程序使用的。
>>>>>>>>>>>>>>>>>>>>>>>>>>>
附:《查看WDM
设备对象名》
如果扯得远一点,我想和读者交流一下怎么在知道了一个设备对象地址后,手动查看这个设备对象名(首先要确认是否有用设备名)。
这部分内容纯属附加,不感兴趣的朋友
可
绕过。
假设现在知道了某个设备对象的地址为0xe1016b
a0
,我们可以通过下面的步骤手动查看它的设备名称(仅在XP
下测试):
1.
打开WinDBG
,运行在
local kernel
模式下。在控制窗口中输入命令
:
dt nt!_object_header 0xe1016b
a0
-0x18
这时候提示画面会出现内核结构体OBJECT_HEADER
(未文档的结构体)的内容。
XP
下
OBJECT_HEADER
的大小为
0x18
字节,并且其位置正好就在
DEVICE_OBJECT
上面。所以我们通过上面的
WinDBG
命令,可以得到一个正确的
OBJECT_HEADER
结构体内容。
2.
找到结构体中成员变量NameInfoOffset
的位置,看他的值。现在我们可以根据这个值判断设备对象是否有名字:如果
NameInfoOffset
值为
0
,说明这个对象未被命名;否则,就是拥有一个名称的,并且保存其名称的地方,就在
OBJECT_HEADER
上面某处(
NameInfoOffset
即为偏移)。
我假设你看到的内容和我下面的截图是一样的:
我们可以根据这个值,找到系统保存对象名称的地方。在控制窗口中运行这个命令:
dd 0xe1016b
a0
-0x18-0x10
得到一串内存数值后,第三个DWORD
值,就是保存设备名称的缓冲区地址。
上图是我电脑中运行后的结果,第三个DWORD
内容为
0xe1016ba0
。
3.
再运行db
命令,查看地址
0xe1016ba0
所指示
缓冲区
中的
内容
:
lkd>
db
0x
e1016ba0
0x
e1016ba0
XXXXXXXXXX
CY001_0....
//找到的设备名称
0xe1016bb0
..........................
........................
成功!
>>>>>>>>>>>>>>>>>>>>>>>>>>>
我们在CY001_WDF
驱动程序中,为设备命名形如“
CY001_X
”这样的名称,末位
X
,是区间
[0, 8]
的整数。因为不知道某个名字是否已经在系统中存在,所以需要一个循环尝试的过程。通过判断
WdfDeviceCreate
调用返回的错误值是否为STATUS_OBJECT_NAME_COLLISION
,可以知道当前尝试的名称是否在系统中引起了名字冲突;如果发生冲突,我们就需要重新尝试。最多尝试到名称“
CY001_8
”,如果
CY001_8
也已经注册了,就让驱动初始化失败。这样的话,我们的驱动目前最多支持同时
8
个
CY001
设备连接到系统中。
// 目前驱动支持同时
8
个实例,即可以同时有
8
个开发板链接在
PC
上,驱动对它们给予并行支持。
// 不同的设备,各以其名称的尾数(
0-7
)相别,并将尾数作为设备的
ID
。
// 下面的操作中,我们为当前设备寻找一个未使用的
ID
。
for(nInstance = 0; nInstance < MAX_INSTANCE_NUMBER; nInstance++){
wcsDeviceName[nLen-1] += nInstance;// 修改末尾的数字,使从
0
至
7
。
// 调用
WdfDeviceInitAssignName
接口,尝试着为当前设备命名;
// 此函数在系统中查找此名称是否唯一,如已存在则返回失败,否则以成功返回。
status = WdfDeviceInitAssignName(DeviceInit, &DeviceName);
// 创建
WDF
设备。上面所做的设置在这一步方能发挥到实质性作用。
status = WdfDeviceCreate(&DeviceInit, &attributes, &device);
if(!NT_SUCCESS(status))
{
if(status == STATUS_OBJECT_NAME_COLLISION)// 名字冲突
KDBG(DPFLTR_ERROR_LEVEL, "Invalid name: %wZ", &DeviceName);
else
{
KDBG(DPFLTR_ERROR_LEVEL, "WdfDeviceCreate failed with status 0x%08x!!!", status);
return status;
}
}else{
KdPrint(("Found valid name: %wZ", &DeviceName));
break;// 成功即退出
}
}
一旦命名成功,那么对应的名称就会出现在系统名称空间中。使用WinOBJ
工具,就能在
Device
子目录下看到了。
下面来看看WDF
环境下如何为设备创建符号链接或设备接口。(省略)
相关文章推荐
- WDF开发USB设备驱动教程(4)
- WDF开发USB设备驱动教程(5)
- WDF开发USB设备驱动教程(1)
- WDF开发USB设备驱动教程(3)
- 学习windows驱动(WDF USB设备驱动开发)
- Linux下的硬件驱动——USB设备(下)(驱动开发部分)
- 转 Linux下的硬件驱动——USB设备(下)(驱动开发部分)
- Linux 设备驱动开发详解之20章usb主机与设备驱动
- 开发wince下的usb音频设备驱动总结
- USB设备驱动开发-USB Gadget Driver(一)
- USB设备驱动开发之扩展(利用USB虚拟总线驱动模拟USB摄像头)
- USB自定义设备驱动开发——修改驱动后应用程序无法使用
- Linux USB 驱动开发(一)—— USB设备基础概念
- Windows CE下USB设备驱动开发的一些基础知识(转)
- USB Gadget设备驱动开发(一) USB Gadget软件结构
- Linux下的硬件驱动——USB设备(下)(驱动开发部分)
- 开发wince下的usb音频设备驱动总结
- USB设备驱动开发-USB协议相关
- 【工业串口和网络软件通讯平台(SuperIO)教程】四.开发设备驱动
- usb驱动开发13之设备生命线