一直都有写技术文章的冲动,无奈自己才疏学浅,文笔太烂,提笔又不知道写什么好,今天终于下定决定,迈出行动的第一步,由于是新手,写的不好请多多包涵。
写到这里,该做什么好呢。既然是自上而下进行USB开发,我们还是先来做一个应用 软件,用于监测USB设备的 状态。应用软件界面如下:在此之前,假定读者已经准备好一个具有USB通讯的开发板,开发板USB驱动已经写好,功能如下:
1, USB采用Bulk传输
2, 端口2作为输入端口
3, 端口3作为输出端口
4, 设备VID=0x1234,PID=0x5678
该软件的初步功能为:当USB设备插入PC,软件界面显示Connected,否则软件界面显示DisConnect。
想让软件能够检测到USB设备的接入状态,需要在Windows窗体创建时,向窗体句柄注册一个通知:告诉操作系统,当这类设备被检测到时,需要向此窗体发送消息。
void RegistDeviceNotify( HANDLE hwnd ) { GUIDusbGuid = {0xa5dcbf10, 0x6530, 0x11d2, {0x90, 0x1f, 0x00, 0xc0, 0x4f, 0xb9,0x51, 0xed}}; DEV_BROADCAST_DEVICEINTERFACEDevInt = {0}; DevInt.dbcc_size =sizeof(DevInt); DevInt.dbcc_devicetype =DBT_DEVTYP_DEVICEINTERFACE; DevInt.dbcc_classguid =usbGuid; m_hDeviceNotify =RegisterDeviceNotification( hwnd, &DevInt, DEVICE_NOTIFY_WINDOW_HANDLE ); }
这样,我们的窗体就会接收WINDOWS消息,并在合适的时候改变界面显示。
LRESULT CDemoMainFrame::OnDeviceChanged( UINTnEventType, DWORD dwData ) { If(DBT_DEVICEARRIVAL== nEventType ) m_pBtnStatus->SetText(_T("Connected") ); }
问题的关键点在于上述标红色的代码部分,直接用这个GUID进行注册时是收不到正确的消息的。该GUID代表的是该设备的接口类GUID,接口类GUID由驱动代码生成。
目前我所知道的GUID有三种: DeviceGUID。在INF安装文件中,可以找到类似DeviceGUID ="{70DC4A53-B2C3-4ADC-8928-1AC2C2E5DDED}"的代码,不过这类GUID的作用我还没有弄清楚。 ClassGUID。在INF安装文件中,可以找到类似ClassGuid = {EB781AAF-9C70-4523-A5DF-642A87ECA567}的代码,这类GUID我们可以经常见到。打开设备管理器—选择USB设备—右键属性—设备类GUID就可以看到我们设置的GUID。对应注册表的位置为:HKEY_LOCAL_MACHINE—SYSTERM—CurrentControlSet—Control—Class,展开之后可以看到很多已安装设备的ClassGUID。 InterfaceGUID。此GUID由我们的驱动代码控制,窗口消息注册时就是用的此GUID,在注册表中的位置为:HKEY_LOCAL_MACHINE—SYSTERM—CurrentControlSet—Control—DeviceClass,只有已成功安装驱动的USB设备才可以找到,所以目前我们的USB设备是看不到InterfaceGUID的。
现在我们集中精力,将我们的USB设备的InterfaceGUID给整出来。由于驱动本身的复杂性,我并不打算从零开始写一个USB驱动程序,而是借鉴国外一个比较好的开源项目—Libusb,并对其中的关键点进行分析。
首先我们要定义一个入口函数。驱动程序与常规的应用程序一样,也是需要入口函数的,它的入口函数名为:DriverEntry。设备插上电脑之后,操作系统经过万里长征,最终会调用我们的入口函数。抛开细节,以一般NT驱动调用为例,我们会发下如下流程(节选自ReactOS):
以上操作的大致意思是:
NT类驱动调用NtLoadDriver加载驱动程序,参数为string类型,表示ServiceName,其中涉及到很多注册表的操作与字符串的操作,这里没有注明;
之后会根据.sys文件路径调用MmLoadSystemImage加载驱动程序映像,返回ModuleObject,这是一个类型为PLDR_DATA_TABLE_ENTRY的指针,其他的不用关心,只需要知道ModuleObject->EntryPoint指向我们的驱动程序入口DriverEntry就可以了;
以IopRootDeviceNode为Parent调用函数IopCreateDeviceNode,创建DeviceNode,这也是一个比较复杂的结构体,我们所需要知道的信息是:DeviceNode->PhysicalDeviceObject就指向驱动函数AddDevice中的PDO参数,其他的诸如DeviceNode->Parent、DeviceNode->Child、DeviceNode->LastChild、DeviceNode->Sibling主要描述了上下层驱动与同层节点之间的关系;
有了DeviceNode,有了ModuleObject,接下来就要调用IopInitializeDriverModule创建DriverObject了,注意这里是创建,没错。创建是通过函数IopCreateDriver实现的,Driver对象创建了之后,还需要调用InitializationFunction,这是一个函数指针,函数地址就是ModuleObject->EntryPoint;
接下来是函数IopInitializeDevice的调用,这里就要分情况了:如果是Legacy的,就直接返回,什么也不干,其他的则调用AddDevice函数,Legacy翻译过来是遗留的意思,这里可以理解为一般的老式驱动吧,事实上,我们一般的NT驱动都是在DriverEntry中就完成了FDO的创建与堆叠的形成,AddDevice是永远都不会调用的,而且,需要说明的是通过NtLoadDriver加载的驱动都是Legacy的,这个函数之所以写这么复杂是因为普通的PNP驱动也会调用到这个函数分支上来;
DriverObject与FDO都有了,剩下的就是照本宣科了。不同的操作系统实现可能不太一样。我看到的首先是IRP_MN_START_DEVICE的发送,其他的细节就不多说了;
最后有一个函数很可疑:IoSynchronousInvalideDeviceRelations,这个函数是干嘛的呢,这里先跳过,留作后面分析;
可惜的是,这不是我们USB驱动的加载过程,真正的USB驱动加载过程大致如下:
当检测到一个新的USB设备到来时,IoInvalidateDeviceRelation函数就会被触发,继而又有下面的动作:
很眼熟吧,是的,这个函数就是就是我们前面跳过没有说明的函数。这是一个递归调用的函数,输入参数DeviceObject代表父节点PDO,递归创建子节点DriverObject与FDO。
首先发送一个IRP_MN_QUERY_DEVICE_RELATIONS的请求,查询当前节点下有多少个子节点,并创建子节点的DeviceNode(参考函数IopCreateDeviceNode);
接下来的一系列操作看得人真是云里雾里,跳过这些细节,直接来看函数IopActionInitChildServices,这个函数又是我们熟悉的领域了:LoadImage映像,创建DriverObject,调用AddDevice(这次是真的要调用AddDevice了),发送IRP_MN_START_DEVICE请求,是不是有一种拨开云雾见青天的感觉?
说到这里,好像有一件很重要的事情忘了说了:最后IoSynchronousInvalideDeviceRelations是如何被递归调用的呢?原来函数IoSynchronousInvalideDeviceRelations被调用前的输入参数代表的是父节点的PDO,再次调用这个函数的PDO是子节点的PDO,当然它也可能是另外一些孩子的父亲,这个父亲到孩子的转变细节如下:
1, 初始化Context
2, 修改Context->DeviceNode,实际上Context->DeviceNode = DeviceNodeContext->FirstDeviceNode = DeviceNode; Context->Context = DeviceNode; Context->Action = IopActionInitChildServices;
3, 第一次执行IopTraverseDeviceTreeNode,可以肯定的是ParentDeviceNode = DeviceNodeNTSTATUS IopTraverseDeviceTree(PDEVICETREE_TRAVERSE_CONTEXT Context) { Context->DeviceNode = Context->FirstDeviceNode; Status = IopTraverseDeviceTreeNode(Context); }
4, 执行Context->Action,由于这次ParentDeviceNode = DeviceNode,所以这个函数早早的就返回了NTSTATUS IopTraverseDeviceTreeNode(PDEVICETREE_TRAVERSE_CONTEXT Context) { ParentDeviceNode = Context->DeviceNode; Status = (Context->Action)(ParentDeviceNode, Context->Context); for (ChildDeviceNode = ParentDeviceNode->Child;ChildDeviceNode != NULL;ChildDeviceNode = ChildDeviceNode->Sibling) { Context->DeviceNode = ChildDeviceNode; Status = IopTraverseDeviceTreeNode(Context); if (!NT_SUCCESS(Status)) { return Status; } } }
5, 回到3继续执行 IopTraverseDeviceTreeNode,此时Context->DeviceNode = ChildDeviceNode,然后执行第 4步,DeviceNode实参为ParentDeviceNode = Context->DeviceNode = ChildDeviceNode,Context实参为 Context->Context = DeviceNode,是不是对这段代码非常纠结~NTSTATUS IopActionInitChildServices(PDEVICE_NODE DeviceNode, PVOID Context) { ParentDeviceNode = (PDEVICE_NODE)Context; if (DeviceNode == ParentDeviceNode) return STATUS_SUCCESS; IopStartDevice(DeviceNode); }
说了半天,连我自己都不知道东西南北了,好在微软已经帮我们做了大部分的事情,这一切都不需要我们操心,我们只需要实现DriverEntry,并填充好相应的派遣函数就好。
NTSTATUS DDKAPIDriverEntry( DRIVER_OBJECT *driver_object, UNICODE_STRING *registry_path ) { int i = 0; USBMSG("[loading-driver]v%d.%d.%d.%d\n", VERSION_MAJOR, VERSION_MINOR, VERSION_MICRO,VERSION_NANO); for (i = 0; i <=IRP_MJ_MAXIMUM_FUNCTION; i++) { driver_object->MajorFunction[i]= dispatch; } driver_object->DriverExtension->AddDevice= add_device; driver_object->DriverUnload =unload; return STATUS_SUCCESS; }
DriverEntry的作用简单明了:填充了IRP派遣函数,初始化AddDevice指针,初始化DriverUnload指针,需要说明的是,宏USBMSG使用了一个可变参数宏__VA_ARGS__,这是C99中的新特性,而3790版本DDK的默认编译器还不支持这种新特性,我的解决办法是:从较高版本的DDK中拷贝bin文件夹到3790版本,新版本的cl.exe是支持这种新特性的。在ReactOS的研究代码中可以看到,系统先后调用了函数DriverEntry、AddDevice,然后生成一个主功能码为IRP_MJ_PNP,次功能码为IRP_MN_START_DEVICE的IRP。顺着这个思路我们来逐步分解。
此函数的作用: 从Enum->USB->VID_xxxx&PID_xxxx->DeviceParameters读取配置信息,这些配置信息由安装文件INF指定,指定的配置信息有:Libusb中add_device函数比较长,我就不一一贴出来了,仅仅查看其中的关键点。
reg_get_hardware_id(physical_device_object,id, sizeof(id))
此函数的作用: 从Enum->USB->VID_xxxx&PID_xxxx下读取键值HardwareID
reg_get_compatible_id(physical_device_object,compat_id, sizeof(compat_id))
此函数的作用: 从Enum->USB->VID_xxxx&PID_xxxx下读取键值CompatibleIDs
status= IoCreateDevice(driver_object,sizeof(libusb_device_t),&nt_device_name,device_type, 0, FALSE,&device_object);
此函数的作用: 根据名称创建设备
status= IoCreateSymbolicLink(&symbolic_link_name, &nt_device_name);
此函数的作用: 根据名称创建链接
reg_get_properties(dev)
SurpriseRemovalOK—指定设备是否支持热插拔,支持则不为过滤驱动
DeviceInterfaceGUIDs—用户自定义GUID,这个GUID就是InterfaceGUID
InitialConfigValue—USB设备指定配置,主要用于存在多个配置参数的USB设备
Status=IoRegisterDeviceInterface(physical_device_object,(LPGUID)&dev->device_interface_guid,NULL,
&dev->device_interface_name);关键的函数在这里,前面代码已经将dev->device_interface_guid填充好,默认使用用户自定义GUID,如果用户没有指定GUID,则使用固定GUID。上述函数运行之后:
写到这里,我想设备应该可以被识别了,现在只需要做一个INF文件将其安装上就可以了。INF文件我也是参照 Libusb提供的模板进行修改的,注意在文档中加上:dev->next_stack_device =IoAttachDeviceToDeviceStack(device_object, physical_device_object)
最后做点总结工作:将新创建的设备附加到它的设备栈,并建立反向指针链表。
[libusb_add_reg_hw] HKR,,SurpriseRemovalOK,0x00010001,1 HKR,,DeviceInterfaceGUIDs,0x00010000,"{41443A29-6DA1-4DB8-8A3C-16E774057BF5}"
执行结果:目的很明确,我们要在Enum->USB->VID_xxxx&PID_xxxx->DeviceParameters中添加键值,图例为INF安装后的
驱动安装后,我们可以查看注册表:CurrentControlSet->Control->DeviceClasses,发现了一个名称为{41443a29-6da1-4db8-8a3c-16e774057bf5}的子键,目标实现!
再来回头看与应用软件窗体绑定的GUID,令:
GUID usbGuid = {0x41443A29, 0x6DA1,0x4DB8, {0x8A, 0x3C, 0x16, 0xE7, 0x74, 0x05, 0x7B, 0xF5}};
回头思考一下,我们好像遗忘了什么。对了,就是在AddDevice被执行之后,紧接着发送了一个主功能码为 IRP_MJ_PNP、次功能码为IRP_MN_START_DEVICE的IRP,到目前为止,还没 有处理这个IRP的代码,现在我们加 上。编译、运行,观察应用软件状态界面。。。。。。很遗憾,USB设备连接上,软件没有任何状态变化。
NTSTATUS dispatch_pnp( libusb_device_t *dev, IRP*irp ) { …………………………………………………………… switch (stack_location->MinorFunction) { case IRP_MN_START_DEVICE: { PoSetPowerState( dev->self,DevicePowerState, dev->power_state ); if (dev->device_interface_in_use) { status = IoSetDeviceInterfaceState( &dev->device_interface_name,TRUE ); if (!NT_SUCCESS(status)) { USBERR0("IRP_MN_START_DEVICE:enabling device interface failed\n"); } } return pass_irp_down(dev,irp, on_start_complete, NULL); } break; default: break; } remove_lock_release(dev); return pass_irp_down(dev, irp, NULL, NULL); }
上述标红色的部分是关键,运行函数IoSetDeviceInterfaceState之后,操作系统会对所有注册该InterfaceGUID的窗体发送消息。接下来我们再来编译、安装,再次观察应用软件状态……..,USB设备连接,界面显示Connected,USB设备断开,界面显示DisConnect,OK!
以下图片显示了USB设备连接上时,从WinDebug端抓上来的Log数据,其中的玄机请自行参悟。
以下为应用软件的界面显示,可以捕捉到USB设备状态。
转了一大圈,终于能够检测USB设备,虽然这只是USB开发中的一小步,但其中涉及到的知识与技巧不可谓不繁杂。由于这篇文章只打算针对USB开发过程本身进行讲解,其他理论知识与开发技巧就不展开描述了,需要的同学可以参考相关书籍书文章,我在实践的过程中也遇到了很多问题和麻烦,通过看书与动手都能将大部分的问题解决,剩下的希望能与大家共同探讨。第一次写技术文章,错误的地方希望大家批评指正,写的不好也希望不要见怪哈。
参考资料:
1, <
> 2, <
> 3, http://wenku.baidu.com/link?url=ykU_VW9WhrZ_mS6Pfnr6UYE70PiMc2Ra4WYz_LkuKauGoMoQYEihlpFN7FiBU5Y25v1ExtYlqkmp-Sukem-OtiOVVldK0FXF2zP9cGNLxj7
4, http://www.windowstipspage.com/symbol-server-path-windbg-debugging/
5, http://blog.csdn.net/chenyujing1234/article/details/7739129
6, http://blog.sina.com.cn/s/blog_58f750e80100g84e.html
备注:
3主要描述了WinDebug的常规用法与技巧
4主要用于解决WinDebug符号加载问题,特别是Symbol Mismatch的问题
5主要描述了如何双机对驱动进行调试
6主要描述了怎样用VS2005创建一个驱动开发模板
测试程序USBDemoTest_V2015_05_15 驱动程序USBDemo_V2015_05_15