我在做usb上位机驱动过程中做的笔记
驱动程序安装成功后,应用程序的设计
VC6+DDK xp+DS3.2
驱动程序安装好后,应用程序要通过安装的驱动程序与设备的通信,但是应用程序怎么才能找到对应用的驱动程序呢?通过设备的GUID找到设备路径。
在windows操作系统环境下,设备通常被当作特殊文件处理。要打开设备,就要知道该设备的路径,要找到设备的路径,要使用GUID来查找。
设备在安装时,windows安装器和相应设备的驱动程序负责将相应的设备与对应的GUID联系起来,并将GUID写入注册表,这样通过GUID(接口类GUID)就可以找到对应设备。
对于HID设备,因为它的驱动已经集成在操作系统中,在同一系统中GUID是一样的,但通常这个值在不同的系统下也许会不一样所以一般不直接使用这个GUID,而是使用一个API函数来获取(函数是void _stdcall HidD_GetHidGuid(Out LPGUID HidGuid)). 而我们自己做的嵌入式设备,因为驱动是自己写的,所以GUID肯定不一样,而且这个GUID不会因为设备用在不同的操作系统上而改变,因为这个GUID在生成设备驱动的时候已经生成,就对应这个设备了,这个设备类GUID可以在每个驱动的interface.h文件中看到。我们就是要用这个文件中的GUID宏定义来查找已连接上设备,把系统中查到的设备列举出来,然后检查它的VID,PID以及设备版本号,看是不是要访问的设备,如果是,就可以对设备进行各种操作了,不是的话就循环下一个设备,直到找到或遍历完为止。
1.下面这个函数用来获取所有与ClassGuid指定的GUID相同的设备,当然对于HID设备在同一个pc机上可能会检测到多个,但是我们自己做的嵌入式设备,一般都是一个,要找到我们要的设备通过VID,PID以及设备版本号。该函数返回HDEVINFO句柄,这个句柄指向ClassGuid指定的所有设备的一个信息集合。这个句柄传给SetupDiEnumDeviceInterfaces()函数的第一个参数。
HDEVINFO SetupDiGetClassDevs(const GUID * ClassGuid,
PCTSTR Enumerator
HWND hwndParent
DWND Flags
);
参数解析:
ClassGuid:设备的GUID指针。对于HID设备由函数void _stdcall HidD_GetHidGuid(Out LPGUID HidGuid)获得。对于自已做的嵌入式设备,因为驱动是我们自己写的,此设备类GUID在生成的驱动程序的interface.h中,是宏定义真接赋给此参数。
一个设备有两个GUID。一个是设备接口类GUID(Device Interface class GUID);一个是安装类GUID(Setup class GUID)。
设备接口类GUID在注册表HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\
DeviceClasses子键下。如果一个驱动程序安装好了,但是不知道它的接口类GUID,可以通过VID,PID等到DeviceClasses子键下中反查其GUID,注意,在DeviceClasses反查时有两个GUID目录下包含我们要找的VID,PID。其中有一个GUID目录包含好多设备路径,这个不是,另一个GUID目录仅包含一个设备的信息,这个才是我们要找到路径,在这个GUID目录下点#号,在右边可以看到一个名为SymbolicLink的项,它的值就是打开设备时要用到的路径(即设备接口名)。如果是通过DS生成的驱动,可以直接在interface.h中找到设备接口类的GUID,然后在注册表中找到它的位置。(不过既然在interface.h中找到了设备接口类GUID,就没必要再在注册表中找了,是吧?!)。
安装类GUID是设备安装时,由Windows安装器读取驱动程序inf文件中的安装类GUID添加到注册表中。HID设备的inf文件在Windows/inf文件夹下的input.inf.安装类GUID在注册表的HKEY_LOCAL_MACHINE_\SYSTEM\CurrentControlSet\Control\Class子键下,单击这个子键可以在右边找到在inf文件中设置的类名,图标等。再展开这个子键,可以看到0000子键和Properties子键。在0000子键下,可以看到用DS设置的子键(向导设置注册表时,Subkey表单项,Subkey表单项用来设置Value Name 表单项设置的注册表项出现的位置),单击这个子键,在右边就可以看到DeviceName(DeviceName就是Value Name表单项的值)项的键值(就是Default Value表单项填写的值),当然在inf文件中也能找到,因为它记录在inf文件中。单击0000子键,可以看到右边有很多表项,其中DriverDesc就是设备在设备管理器中显示的名称,InfPath是安装驱动后备份在Windows/inf目录(属性为隐藏)下的inf文件.MatchingDeviceId就是要匹配的硬件ID。(DS向导的注册表项的设置中,Type表单项用于设置这个新建的注册表项的类型,REG_SZ是字符串类型,根据Default Value的值而定。Root表单项设置需要增加到哪个根键下。Value Name表单项用于设置新建的注册表项名,Subkey表单项用于设置新建注册表项所在的根键,Default Value表单项就是新建表项的键值。Driver Variable Name是该项在驱动程序中出现的变量名)
同样在CurrentControlSet\Enum子键下,也有安装类GUID。
Enumerator:值为NULL表示将搜索全部设备;也可以指定一个设备的PnP名字的字符串,从而限制搜索。
hwndParent: 为父窗口的句柄,可以指定为NULL;
Flags:位4的值为1时,指定使用接口类GUID,位1为1时,表示列举出已经连接的设备,否则将连接全部安装的设备;
当函数调用失败时,将返回INVALID_HANDLE_VALUE,调用GetLastError()函数可以获取失败的原因。
BOOL SetupDiEnumDeviceInterfaces (HDEVINFO DeviceInfoSet,
PSP_DEVINFO_DATA DeviceInfoData,
Const GUID *InterfaceClassGuid,
DWORD MemberIndex,
PSP_DEVICE_INTERFACE_DATA DeviceInterfaceData
);
参数信息:
DeviceInfoSet(HDEVINFO类型): SetupDiGetClassDevs函数返回的设备集合的句柄保存在这个参数中
DeviceInfoData(SP_DEVINFO_DATA类型的指针,SP_DEVINFO_DATA是结构体,PSP_DEVINFO_DATA此结构体的指针):强制获取某个设备的信息。通常为NULL
InterfaceClassGuid:是指向设备的接口类GUID的指针,我们应用程序会指定一个变量来保存接口类GUID,假设为MyGUID(GUID类型),此时这个参数就会是&MyGuid。与第一个函数的参数值一样。
MemberIndex(设置为DWORD类型):用于控制现在SetupdinumDeviceInterfaces 函数获取的是哪一个设备的接口信息。0表示第一个设备,1为第二个设备。这个索引也是调用第一个函数时处理好的。索引的设备是第一个函数检测到的已连接的设备。当该值超过实际的索引数时,函数会返回零表示没找到与我们应用程序指定的VID,PID相符的设备;
DeviceInterfaceData(SP_DEVICE_INTERFACE_DATA类型指针):用来保存当前MemberIndex指定的设备的信息,在此函数调用前要先将该结构体变量的成员cbSize成员赋值为此结构体的大小。赋值的时候是&DeviceInterfaceData(定义:SP_DEVICE_INTERFACE_DATA DeviceInterfaceData)
此函数的返回值为BOOL类型
BOOL SetupDiGetDeviceInterfaceDetail(HDEVINFO DeviceInfoSet,
PSP_DEVICE_INTERFACE_DATA DeviceInterfaceData,
PSP_DEVICE_INTERFACE_DETALL_DATA DeviceInterfaceDetailData,
DWORD DeviceInterfaceDetailDataSize
PDWORD RequiredSize
PSP_DEVINFO_DATA DeviceInfoData
);
参数信息:
DeviceInfoSet:同样第一个函数的返回值;
DeviceInterfaceData:与第二个函数的最后一个参数一样
DeviceInterfaceDetailData: 比第二个参数获取的的设备信息还详细的设备信息,其中该结构体的成员变量DevicePath中就保存着用来打开设备的路径(或者叫设备接口名),该参数给API函数CreatFile以打开指定设备。在调用此函数之前,先要将该结构体的成员变量cbSize的值赋为该结构体的大小
DeviceInterfaceDetailDataSize:第三个参数所需的buffer大小。
RequiredSize:指定保存更详细设备信息所需要的buffer大小。
DeviceInfoData:是一个用来接收该接口所在设备的设备信息的结构体指针。通常值为NULL,表示该参数无效;
该函数调用两次,第一次获取保存设备详细信息buffer的大小,即RequiredSize,这时函数的第3,4个参数为NULL,等获取到buffer大小后,再次调用该函数此时第1,2,6都不变,第三个参数为buffer的地址,第四个为buffer大小。
记住当使用完后要销毁信息集合调用SetupDiDestroyDeviceInfoList(信息集合句柄);实现。
下面是查找设备的函数:
//查找设备的函数。如果指定的设备找到,则设置MyDevFound为TRUE,
//并将设备的路径保存在MyDevPathName中。如果指定的设备未找到,则
//设置MyDevFound为FALSE。
void CMyUsbDeviceTestAppDlg::FindMyDevice()
{
//定义一个GUID的结构体MyGuid并初始化为我们自定义USB设备的接口类GUID。
GUID MyGuid=GUID_DEVINTERFACE_COMPUTER00USB; //在interface.h文件中定义了
//定义一个DEVINFO的句柄hDevInfoSet来保存获取到的设备信息集合句柄。
HDEVINFO hDevInfoSet;
//定义MemberIndex,表示当前搜索到第几个设备,0表示第一个设备。
DWORD MemberIndex;
//DevInterfaceData,用来保存设备的驱动接口信息
SP_DEVICE_INTERFACE_DATA DevInterfaceData;
//定义一个BOOL变量,保存函数调用是否返回成功
BOOL Result;
//定义一个RequiredSize的变量,用来接收需要保存详细信息的缓冲长度。
DWORD RequiredSize;
//定义一个指向设备详细信息的结构体指针。
PSP_DEVICE_INTERFACE_DETAIL_DATA pDevDetailData;
//初始化设备未找到
MyDevFound=FALSE;
//对DevInterfaceData结构体的cbSize初始化为结构体大小
DevInterfaceData.cbSize=sizeof(DevInterfaceData);
//根据MyGuid来获取设备信息集合。其中Flags参数设置为
//DIGCF_DEVICEINTERFACE|DIGCF_PRESENT,前者表示使用的GUID为
//接口类GUID,后者表示只列举正在使用的设备,因为我们这里只
//查找已经连接上的设备。返回的句柄保存在hDevinfo中。注意设备
//信息集合在使用完毕后,要使用函数SetupDiDestroyDeviceInfoList
//销毁,不然会造成内存泄漏。
hDevInfoSet=SetupDiGetClassDevs(&MyGuid,
NULL, NULL, DIGCF_DEVICEINTERFACE|DIGCF_PRESENT);
//然后对设备集合中每个设备进行列举,检查是否是我们要找的设备
//当找到我们指定的设备,或者设备已经查找完毕时,就退出查找。
//首先指向第一个设备,即将MemberIndex置为0。
MemberIndex=0;
while(1)
{
//调用SetupDiEnumDeviceInterfaces在设备信息集合中获取编号为
//MemberIndex的设备信息。
Result=SetupDiEnumDeviceInterfaces(hDevInfoSet,
NULL, &MyGuid, MemberIndex, &DevInterfaceData);
//如果获取信息失败,则说明设备已经查找完毕,退出循环。
if(Result==FALSE) break;
//将MemberIndex指向下一个设备
MemberIndex++;
//如果获取信息成功,则继续获取该设备的详细信息。在获取设备
//详细信息时,需要先知道保存详细信息需要多大的缓冲区,这通过
//第一次调用函数SetupDiGetDeviceInterfaceDetail来获取。这时
//提供缓冲区和长度都为NULL的参数,并提供一个用来保存需要多大
//缓冲区的变量RequiredSize。
Result=SetupDiGetDeviceInterfaceDetail(hDevInfoSet,
&DevInterfaceData, NULL, NULL, &RequiredSize, NULL);
//然后,分配一个大小为RequiredSize缓冲区,用来保存设备详细信息。
pDevDetailData=(PSP_DEVICE_INTERFACE_DETAIL_DATA)malloc(RequiredSize);
if(pDevDetailData==NULL) //如果内存不足,则直接返回。
{
MessageBox("内存不足!");
SetupDiDestroyDeviceInfoList(hDevInfoSet);
return;
}
//并设置pDevDetailData的cbSize为结构体的大小(注意只是结构体大小,
//不包括后面缓冲区)。
pDevDetailData->cbSize=sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
//然后再次调用SetupDiGetDeviceInterfaceDetail函数来获取设备的
//详细信息。这次调用设置使用的缓冲区以及缓冲区大小。
Result=SetupDiGetDeviceInterfaceDetail(hDevInfoSet,
&DevInterfaceData, pDevDetailData, RequiredSize, NULL, NULL);
//将设备路径复制出来,然后销毁刚刚申请的内存。
MyDevPathName=pDevDetailData->DevicePath;
free(pDevDetailData);
//如果调用失败,则查找下一个设备。
if(Result==FALSE) continue;
//否则,说明调用成功,设备已经找到
MyDevFound=TRUE; //设置设备已经找到
//找到设备,退出循环。本程序只检测一个目标设备,查找到后就退出
//查找了。如果你需要将所有的目标设备都列出来的话,可以设置一个
//数组,找到后就保存在数组中,直到所有设备都查找完毕才退出查找
break;
}
//调用SetupDiDestroyDeviceInfoList函数销毁设备信息集合
SetupDiDestroyDeviceInfoList(hDevInfoSet);
}
找到设备好,我们要对设备进行操作。
打开设备的函数:
HANDLE CreateFile(
LPCTSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDispostion ,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile);
lpFileName:上面找到了设备后,把路径(即设备名)保存起来给这个参数用。
dwDesiredAccess:设置这个函数返回什么句柄,下面是参数值:
GENERIC_READ:函数会返回读句柄,传给ReadFile函数的第一个参数,这样ReadFile才能从设备读取数据。
GENERIC_WRITE:函数会返回写句柄,传给WriteFile函数的第一个参数,这样WriteFile才能将数据写到设备。
0:函数会返回NULL,不会访问设备,但可以获取设备属性,将该返回值赋给
BOOLEAN HidD_GetAttributes(
IN HANDLE HidDeviceObject,
OUT PHIDD_ATTRIBUTES Attributes
);
函数的每一个参数可以获取HID设备的VID,PID等。
dwShareMode:用来设置外部访问设备的权限。参数值为:
FILE_SHARE_READ:设备允许应用程序对自己进行读操作,随后可以调用ReadFile函数。
FILE_SHARE_WRITE:设备允许应用程序对自己进行写操作,随后可以调用WriteFile函数。
0:设备允许外部对自己进行任何操作,即不设置任何共享。
因为我们要读写设备,所以给此参数赋值为FILE_SHARE_READ | FILE_SHARE_WRITE;
lpSecurityAttributes:此值永远为NULL。要说它的作用就是用来决定此函数返回的句柄能否继承到子过程中去。设备为NULL是不能继承。
dwCreationDispostion:用来决定当被打开的设备(文件)不存在时执行怎样的操作。参数:
OPEN_EXISTING:此函数只用来打开已存在的设备。如果指定的设备不存在,那么此函数无效。一般都选此值。
其它参数看MSDN
dwFlagsAndAttributes:访问设备的方式。参数:
FILE_ATTRIBUTE_NORMAL:同步调用
FILE_FLAG_OVERLAPPED:异步调用
hTemplateFile:参数为NULL,即不使用临时文件。
既要读设备,又要写设备话就要调用两次CreateFile函数,以返回读句柄和写柄
从设备读取数据函数:
BOOL ReadFile(
HANDLE hFile,
LPVOID lpBuffer,
DWORD nNumberOfBytesToRead,
LPDWORD lpNumberOfBytesRead,
LPOVERLAPPED lpOverlapped);
读取成功返回非0,失败返回0
hFile:读设备的句柄
lpBuffer:接收数据buffer;
nNumberOfBytesToRead:读取数据的长度;
lpNumberOfBytesRead:实际读到的字节数的指针;当lpOverlapped不为空时(肯定不为空),该值可为NULL
lpOverlapped:指向OVERLAPPED结构体的指针。当CreateFile函数选择了异步方式时必须提供此参数。该参数指向的结构体提供一个事件的句柄,当IRP完成时会触发该事件。调用该函数后,事件标志会被自动清除,事件会自动被设置为无效。用该参数指向的结构体函数GetOverlappedResult代替lpNumberOfBytesRead。
WriteFile函数的参数与ReadFile对各自函数实现的功能一样.
DeviceIoControl方式访问设备驱动,ReadFile函数对应设备驱动的Read函数,WriteFile对应Write函数.DeviceIoControl对应EP1_READ_Handler / EP1_WRITE_Handler函数,具体是读还是写,要看第二个参数,第二个参数的值为读或者写的函数名,在我们的DS向导过程中有一步Add New IOCTL,点Add 会出现Add IO Control Code对话框,其中ParameterName就是第二个参数的值和函数名的前半部分,Access用来设置此ParameterName代表的是读还是写,比如这里的ParameterName就是EP1_READ/EP1_WRITE,而第二个参数的值相应就是EP1_READ/EP1_WRITE。
DeviceIoControl读函数与ReadFile实现的功能一模一样,只是除了他们对应调用的驱动函数不一样外,DeviceIoControl访问方式获取buffer和数据长度的方式不一样。
还有一点DeviceIoControl方式的数据方向是驱动程序与应用程序之间,且以驱动程序为主(即当应用程序传数据给驱动时,要以驱动程序做为参照,驱动程序看来这叫做数据输入,相应buffer为输入buffer.这时驱动程序应该将输入数据发送到物理设备,也就是说在输入buffer中的数据才是发送给设备的数据,反之成立)而ReadFile各WriteFile方式的数据方向是设备(其实也是驱动代替设备和主机通信,因为buffer在驱动中)与主机之间,且主机为主,当设备向主机发送数据时叫做数据输入,相应buffer为输入buffer,反之成立。
//使用这种方式,CreateFile函数的第二个参数设置为GENERIC_READ|GENERIC_WRITE(读/写方式打开设备,到底是读还是写,由DeviceIoControl函数控制)
BOOL DeviceIoControl(
HANDLE hDevice, //CreateFile返回的句柄,句柄为读/写,区别ReadFile的读句柄WriteFile的写句柄。
DWORD dwIoControlCode, //在向导中定义的函数名,要是读就是EP1_READ,函数代码是DS帮助生成的。代码在interface.h中
LPVOID lpInBuffer, //输入buffer
DWORD nInBufferSize, //输入buffer大小
LPVOIDlpOutBuffer, //输出buffer
DWORD nOutBufferSize, //输出buffer大小
LPDWORD lpBytesReturned, //buffer实际得到的字节数,一般不用,用最后一个参数代替
LPOVERLAPPED lpOverlapped);//指向一个OVERLAPPED结构体的指针。成员GetOverlappedResult函数可以获取buffer实际得到的字节数
当调用成功,函数返回非0.
关闭句柄函数:
BOOL CloseHandle(HANDLE hObject);
hObject为设备(文件)的句柄,注意不是读/写设备的句柄。
以上几个函数在用DS写驱动程序时会自动生成应用层代码。以上可以用于自定义的HID设备,对于鼠标,键盘之类的HID设备是os默认的,os内部已经有对它的应用操作,无需我们再找到它,然后操作它,只要枚举成功就可以用了,当然前提是我们下位机做的是os内部默认的这些设备.我们自定义 的HID设备有可能比鼠标多一些功能.
找到设备后的操作:
找到设备后,我们要判断这个设备是不是我们要操作的设备,对于HID设备通过BOOLEAN __STDCALL HidD_GetAtributes(IN HANDLE HidDeviceObject, OUT PHIDD_ATTRIBUTES Attributes);来获取指定设备的属性,例如VID,PID,设备版本号等。调用成功返回非零,调用失败返回0;参数HidDeviceObject是被访问设备的句柄,通过CreatFile函数返回此句柄。参数Attributes是一个指向HIDD_ATTRIBUTES结构体的指针,用来保存HID设备的属性。
对于我们自己的设备,因为是一个GUID对应一个设备(具体说是一个GUID对应一个设备驱动),所以通过GUID找到的设备只有一个,找到后就不用与VID,PID等比较,直接用即可。
以上这些完全可以封装成dll,然后只给应用层提供几个读写和端点控制接口,内部的查找,和找到设备后对设备的操作细节应用层完全可以不知道,只调用接口就可以了,就像我们写程序时,通常写几个接口函数,然后在主函数中调用函数实现功能,具体怎么实现的,主函数无需知道。类似的有usb.dll,周立功的EasyD12.dll等。我们也可以写自己的dll。
多线程应用程序设计
在数据采集系统应用程序设计中,多线程技术的应用 会大大地提高应用 程序的效率,辅助线程完成后台数据采集的工作.应用程序的界面操作不受后台数据处理的影响.
Usb数据采集系统的软件设计步骤如下:
1.查找usb设备,获得设备句柄.
2.封装对usb设备操作的动态链接库.
3.编写辅助线程和界面程序进行数据采集和界面数据显示.
DS生成了查找,访问设备的代码
本文来源:https://www.2haoxitong.net/k/doc/5496cded4afe04a1b071ded9.html
文档为doc格式