USB开发—自上而下(一)

一直都有写技术文章的冲动,无奈自己才疏学浅,文笔太烂,提笔又不知道写什么好,今天终于下定决定,迈行动的第一步,由于是新手,写的不好请多多包涵。

在此之前,假定读者已经准备好一个具有USB通讯的开发板,开发板USB驱动已经写好,功能如下:

1,  USB采用Bulk传输

2,  端口2作为输入端口

3,  端口3作为输出端口

4,  设备VID=0x1234,PID=0x5678

写到这里,该做什么好呢。既然是自上而下进行USB开发,我们还是先来做一个应用 软件,用于监测USB设备的 状态。应用软件界面如下:

USB开发—自上而下(一)_第1张图片

该软件的初步功能为:当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):

         USB开发—自上而下(一)_第2张图片

        以上操作的大致意思是:

        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开发—自上而下(一)_第3张图片

当检测到一个新的USB设备到来时,IoInvalidateDeviceRelation函数就会被触发,继而又有下面的动作:

USB开发—自上而下(一)_第4张图片

很眼熟吧,是的,这个函数就是就是我们前面跳过没有说明的函数。这是一个递归调用的函数,输入参数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

Context->FirstDeviceNode = DeviceNode;
Context->Context = DeviceNode;
Context->Action  = IopActionInitChildServices;
2, 修改Context->DeviceNode,实际上Context->DeviceNode = DeviceNode

NTSTATUS IopTraverseDeviceTree(PDEVICETREE_TRAVERSE_CONTEXT Context)
{
    Context->DeviceNode = Context->FirstDeviceNode;
    Status = IopTraverseDeviceTreeNode(Context);
}
3, 第一次执行IopTraverseDeviceTreeNode,可以肯定的是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;
       }
    }
}
4, 执行Context->Action,由于这次ParentDeviceNode = DeviceNode,所以这个函数早早的就返回了

NTSTATUS IopActionInitChildServices(PDEVICE_NODE DeviceNode, PVOID Context)
{
    ParentDeviceNode = (PDEVICE_NODE)Context;
    if (DeviceNode == ParentDeviceNode) return STATUS_SUCCESS;
    IopStartDevice(DeviceNode);
}
5, 回到3继续执行 IopTraverseDeviceTreeNode,此时Context->DeviceNode = ChildDeviceNode,然后执行第 4步,DeviceNode实参为ParentDeviceNode = Context->DeviceNode = ChildDeviceNode,Context实参为 Context->Context = 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。顺着这个思路我们来逐步分解。

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)

此函数的作用: 从Enum->USB->VID_xxxx&PID_xxxx->DeviceParameters读取配置信息,这些配置信息由安装文件INF指定,指定的配置信息有:

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。上述函数运行之后:
CurrentControlSet->Conrol->DeviceClasses下新增一条以InterfaceGUID命名的子键。创建这个子键就是我们的目标,函数的具体用法请参照MSDN。

dev->next_stack_device =IoAttachDeviceToDeviceStack(device_object, physical_device_object)

最后做点总结工作:将新创建的设备附加到它的设备栈,并建立反向指针链表。

写到这里,我想设备应该可以被识别了,现在只需要做一个INF文件将其安装上就可以了。INF文件我也是参照 Libusb提供的模板进行修改的,注意在文档中加上:

[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}};

 编译、运行,观察应用软件状态界面。。。。。。很遗憾,USB设备连接上,软件没有任何状态变化。

回头思考一下,我们好像遗忘了什么。对了,就是在AddDevice被执行之后,紧接着发送了一个主功能码为 IRP_MJ_PNP、次功能码为IRP_MN_START_DEVICE的IRP,到目前为止,还没 有处理这个IRP的代码,现在我们加 上。

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开发—自上而下(一)_第5张图片

以下为应用软件的界面显示,可以捕捉到USB设备状态。

USB开发—自上而下(一)_第6张图片

转了一大圈,终于能够检测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

 

你可能感兴趣的:(驱动开发)