市面上一些无驱的usbkey 一直是技术卖点,象招商银行的usbkey 就一直以无需安装驱动自豪。无驱动的usbkey确是比较方便,尤其在使用别人的计算机的时候。
刚好最近在研究一款HID 设备,在这方面做了一些探索。
首先工具一定要利索,最好能武装到牙齿,下面的三个工具最好都有,否则就回到了石器时代
bus hound
usb device viewer
hid descriptor tool
文档是一定要仔细看,
Universal Serial Bus Revision 2.0 specification ,尤其是那个著名的chap. 9
Device Class Definition for HID 1.11
google , 一定要多用,你碰到的问题,别人肯定已经碰到过,不过别太相信中文文章,绝大部分没有自己实验过,会误导
我们看一下招行的usbkey 抓包结果
25.0 CTL 80 06 00 01 00 00 12 00 GET DESCRIPTOR 1.1.0
25.0 DI 12 01 10 01 00 00 00 08 6e 09 10 a0 05 20 01 02 00 01 ........n.... .. 1.2.0
25.0 CTL 80 06 00 02 00 00 09 00 GET DESCRIPTOR 2.1.0
25.0 DI 09 02 1b 00 01 01 00 80 0f ......... 2.2.0
25.0 CTL 80 06 00 02 00 00 1b 00 GET DESCRIPTOR 3.1.0
25.0 DI 09 02 1b 00 01 01 00 80 0f 09 04 00 00 00 03 00 00 00 09 21 00 01 00 01 22 a6 00 ................ 3.2.0
25.0 CTL 00 09 01 00 00 00 00 00 SET CONFIG 4.1.0
25.0 CTL 21 0a 00 00 00 00 00 00 SET IDLE 5.1.0
25.0 USTS 04 00 00 c0 stall pid 5.2.0
25.0 CTL 81 06 00 22 00 00 e6 00 GET DESCRIPTOR 6.1.0
25.0 DI 06 a0 ff 09 a2 a1 01 09 a3 a1 03 06 a1 ff 09 c1 09 c2 09 c3 09 da 09 da 09 da 09 da 09 da 09 da ................ 6.2.0
09 da 09 da 09 da 09 da 09 da 09 da 09 da 09 da 09 da 09 da 09 da 09 da 09 da 09 da 09 da 09 da ................ 6.2.32
09 da 09 da 09 da 09 da 09 da 09 da 09 da 09 da 09 da 09 da 09 da 09 da 09 da 09 da 09 da 09 da ................ 6.2.64
09 da 09 da 09 da 09 da 09 da 09 da 09 da 09 da 09 da 09 da 09 da 09 da 09 da 09 da 09 da 09 da ................ 6.2.96
09 da 09 da 09 da 09 da 09 da 09 da 09 da 09 da 09 da 09 da 09 c4 15 00 25 ff 35 00 45 ff 75 08 ................ 6.2.128
95 44 b1 02 c0 c0 .D.... 6.2.160
对应的描述符表
usb_device_descriptor ROM_DATA dev_desc
= {
0x12, //bDefineLength
0x01, //bDescriptorType,设备描述符
0x10, //bcdUSBL
0x01, //bcdUSBH
0x00, //bDeviceClass
0x00, //bDeviceSubClass
0x00, //bDeviceProtocol
0x08, //bMaxPacketSize0, 包长
0x6e, //idVendorL 0x6e , VID=0x096E
0x09, // 0x09
0x10, //idProductL 0x10 , PID=0xA010
0xa0, // 0xa0
0x05, //bcdDevice , SN=0x2005
0x20, //
0x01, //iManufacturer
0x02, //iProduct
0x00, //iSerialNumber
0x01 //bNumConfigurattions,
};
unsigned char cfg_desc [] =
{
//configuration desc
0x09, // bLength
02, // bDescriptorType, CONFIGURATION,配置描述符
0x1b, // wTotalLength (low byte)
0x00, // wTotalLength (high byte)
0x01, // bNumInterfaces, 接口数
0x01, // bConfigurationValue
0x00, // iConfiguration (none)
0x80, // bmAttributes
0x0f, // bMaxPower (30 mA)
//interface desc
0x09, // bLength (Interface1 descriptor starts here)
0x04, // bDescriptorType
0x00, // bInterfaceNumber
0x00, // bAlternateSetting
0x00, // bNumEndpoints (excluding EP0),注意这样配置是不符合规范的,HID设备要求至少一个In endpoint, windows 下可以运行, linux 下不可以
0x03, // bInterfaceClass (HID code)
0x00, // bInterfaceSubClass ( not specified)
0x00, // bInterfaceProtocol ( not specified)
0x00, // iInterface (none)
// hid
0x09, // bLength (HID1 descriptor starts here)
0x21, // bDescriptorType, hid 描述
0x00, // bcdHID (low byte)
0x01, // bcdHID (high byte)
0x00, // bCountryCode (none)
0x01, // bNumDescriptors
0x22, // bDescriptorType report 描述
a6 // wDescriptorLength (low byte)
0x00, // wDescriptorLength (high byte)
};
很奇怪没有按要求至少有一个 in endpoint,而windows 下竟然能运行
再看那个很变态的report 表, 去掉那些奇怪的 09 开始的定义, 用dt 表示大致如下
char ReportDescriptor[] = {
0x06, 0xa0, 0xff, // USAGE_PAGE (Vendor Defined Page 1)
0x09, 0x01, // USAGE (Vendor Usage 1)
0xa1, 0x01, // COLLECTION (Application)
0x09, 0x02, // USAGE (Vendor Usage 2)
0xa1, 0x02, // COLLECTION (Logical)
0x06, 0xa0, 0xff, // USAGE_PAGE (Vendor Defined Page 1)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0xff, // LOGICAL_MAXIMUM (255)
0x35, 0x00, // PHYSICAL_MINIMUM (0)
0x45, 0xff, // PHYSICAL_MAXIMUM (255)
0x75, 0x08, // REPORT_SIZE (8)
0x95, 0x44, // REPORT_COUNT (68)
0xb1,0x02, // 不知道用途
0xc0, // END_COLLECTION
0xc0 // END_COLLECTION
};
即 in/out 的长度都是 0x44* 8 bit, 即68 byte
有了这几个描述表,只要应答正确, 基本上windows系统就能识别出usb hid 设备了,linux 下需要提供一个 in endpoint 才不会出错
windows 下打开文件句柄如下
HANDLE connectToIthUSBHIDDevice (DWORD deviceIndex)
{
GUID hidGUID;
HDEVINFO hardwareDeviceInfoSet;
SP_DEVICE_INTERFACE_DATA deviceInterfaceData;
PSP_INTERFACE_DEVICE_DETAIL_DATA deviceDetail;
ULONG requiredSize;
HANDLE deviceHandle = INVALID_HANDLE_VALUE;
DWORD result;
//Get the HID GUID value - used as mask to get list of devices
HidD_GetHidGuid (&hidGUID);
//Get a list of devices matching the criteria (hid interface, present)
hardwareDeviceInfoSet = SetupDiGetClassDevs (&hidGUID,
NULL, // Define no enumerator (global)
NULL, // Define no
(DIGCF_PRESENT | // Only Devices present
DIGCF_DEVICEINTERFACE)); // Function class devices.
deviceInterfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
//Go through the list and get the interface data
result = SetupDiEnumDeviceInterfaces (hardwareDeviceInfoSet,
NULL, //infoData,
&hidGUID, //interfaceClassGuid,
deviceIndex,
&deviceInterfaceData);
/* Failed to get a device - possibly the index is larger than the number of devices */
if (result == FALSE)
{
SetupDiDestroyDeviceInfoList (hardwareDeviceInfoSet);
return INVALID_HANDLE_VALUE;
}
//Get the details with null values to get the required size of the buffer
SetupDiGetDeviceInterfaceDetail (hardwareDeviceInfoSet,
&deviceInterfaceData,
NULL, //interfaceDetail,
0, //interfaceDetailSize,
&requiredSize,
0); //infoData))
//Allocate the buffer
deviceDetail = (PSP_INTERFACE_DEVICE_DETAIL_DATA)malloc(requiredSize);
deviceDetail->cbSize = sizeof(SP_INTERFACE_DEVICE_DETAIL_DATA);
//Fill the buffer with the device details
if (!SetupDiGetDeviceInterfaceDetail (hardwareDeviceInfoSet,
&deviceInterfaceData,
deviceDetail,
requiredSize,
&requiredSize,
NULL))
{
SetupDiDestroyDeviceInfoList (hardwareDeviceInfoSet);
free (deviceDetail);
return INVALID_HANDLE_VALUE;
}
//Open file on the device
deviceHandle = CreateFile (deviceDetail->DevicePath,
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL, // no SECURITY_ATTRIBUTES structure
OPEN_EXISTING, // No special create flags
0,
NULL); // No template file
SetupDiDestroyDeviceInfoList (hardwareDeviceInfoSet);
free (deviceDetail);
return deviceHandle;
}
HANDLE connectToUSBHIDDevice (DWORD vendorID, DWORD productID, DWORD versionNumber)
{
HANDLE deviceHandle = INVALID_HANDLE_VALUE;
DWORD index = 0;
HIDD_ATTRIBUTES deviceAttributes;
BOOL matched = FALSE;
while (!matched &&index <20 )
{
if((deviceHandle = connectToIthUSBHIDDevice (index)) != INVALID_HANDLE_VALUE)
{
if (!HidD_GetAttributes (deviceHandle, &deviceAttributes))
return INVALID_HANDLE_VALUE;
if ((vendorID == 0 || deviceAttributes.VendorID == vendorID) &&
(productID == 0 || deviceAttributes.ProductID == productID) &&
(versionNumber == 0 || deviceAttributes.VersionNumber == versionNumber))
return deviceHandle; /* matched */
CloseHandle (deviceHandle); /* not a match - close and try again */
}
index++;
}
return INVALID_HANDLE_VALUE;
}
对数据的访问,如果是有 in endpoint 和 out endpoint, 那么用常规的 ReadFile/WriteFile, 如果只有control endpoint, 用DeviceIoControl, 注意in 和out 是从device 的角度定义的
// in: PC ->Device
// out: Device->PC
发送数据给usb
memset(inbuffer,0,REPORT_PACKET_SIZE+1);
inbuffer[0]= 0x0 ;
memcpy(inbuffer+1,data);
bResult = DeviceIoControl(hDevice,IOCTL_HID_SET_OUTPUT_REPORT,
&inbuffer[0], REPORT_PACKET_SIZE+1,&outbuffer[0], 0,
&numBytesReturned,(LPOVERLAPPED) NULL);
按规范inbuffer[0] 填 report id, 所以数据长度要0x44+1
接收数据
outbuffer[0] = 0;
bResult = DeviceIoControl(hDevice,IOCTL_HID_GET_INPUT_REPORT,
&inbuffer[0], 0,&outbuffer[0],0x44,
&numBytesReturned,(LPOVERLAPPED) NULL);
按msdn 的文档,outbuffer[0] 填 reportid
按hid 的规范,返回的数据中outbuffer[0]要填reportid, 可是招行的usbkey 好象没有这样做,也能正确解释
linux 下就要严格得多,首先至少要有 中断类型的 in endpoint, 然后数据要严格的第一个byte填 reportid
首先建立hid 的文件节点
#!/bin/sh
mkdir -p /dev/usb
mknod /dev/usb/hiddev0 c 180 96
mknod /dev/usb/hiddev1 c 180 97
mknod /dev/usb/hiddev2 c 180 98
mknod /dev/usb/hiddev3 c 180 99
mknod /dev/usb/hiddev4 c 180 100
mknod /dev/usb/hiddev5 c 180 101
mknod /dev/usb/hiddev6 c 180 102
mknod /dev/usb/hiddev7 c 180 103
mknod /dev/usb/hiddev8 c 180 104
mknod /dev/usb/hiddev9 c 180 105
mknod /dev/usb/hiddev10 c 180 106
mknod /dev/usb/hiddev11 c 180 107
mknod /dev/usb/hiddev12 c 180 108
mknod /dev/usb/hiddev13 c 180 109
mknod /dev/usb/hiddev14 c 180 110
mknod /dev/usb/hiddev15 c 180 111
发送和接收的程序如下
HANDLE connectToUSBHIDDevice (DWORD vendorID, DWORD productID, DWORD versionNumber)
{
char evdev[50];
int fd = -1, index;
struct hiddev_event ev[64];
struct hiddev_devinfo dinfo;
char name[256] = "Unknown";
int matched = 0;
const char hid_dir[] = "/dev/usb";
index = 0 ;
while(!matched&&index <10)
{
sprintf(evdev, "%s/hiddev%d",hid_dir, index);
if ((fd = open(evdev, O_RDONLY)) > 0)
{
ioctl(fd, HIDIOCGDEVINFO, &dinfo);
if ((vendorID == 0 ||dinfo.vendor == vendorID) &&
(productID == 0 ||dinfo.product == (__s16)productID) &&
(versionNumber == 0 || dinfo.version == versionNumber))
return fd; /* matched */
close(fd); /* not a match - close and try again */
}
index++ ;
}
return INVALID_HANDLE_VALUE;
}
int SendOutputReport(int fd, unsigned char reportID, unsigned char* vals, int num_vals, int delay)
{
int ret = 0, i;
char sval;
struct hiddev_report_info out_report;
struct hiddev_usage_ref uref;
uref.report_type = HID_REPORT_TYPE_OUTPUT;
uref.report_id = reportID;
uref.field_index = 0;
uref.usage_index = 0;
/* usage code for this for this usage */
ret = ioctl(fd, HIDIOCGUCODE, &uref);
if(0 > ret){
perror("SendCommandFS: ioctl to get usage code for out report");
return ret;
}
/* fill in usage values. */
for( i = 0; (i < num_vals) && vals; i++){
uref.usage_index = i;
sval = vals[i];
uref.value = sval;
ioctl(fd, HIDIOCSUSAGE, &uref);
if(0 > ret){
perror("SendCommandFS:ioctl to set usage value for out report");
return ret;
}
}
/* tell the driver about the usage values */
out_report.report_type = HID_REPORT_TYPE_OUTPUT;
out_report.report_id = reportID;
ioctl(fd, HIDIOCGREPORTINFO, &out_report);
if (0 > ret) {
perror("SendCommandFS: ioctl to get out report info");
return ret;
}
/* this ioctl puts the report on the wire */
ret = ioctl(fd, HIDIOCSREPORT, &out_report);
if (0 > ret) {
perror("SendCommandFS: ioctl to send output report");
return ret;
}
usleep(delay);
return ret;
}
int GetInputReport(int fd, unsigned char reportID, unsigned char *vals, int num_vals, int delay)
{
int ret = 0, i;
/*struct hiddev_usage_ref_multi in_usage_multi;*/
struct hiddev_usage_ref uref;
struct hiddev_report_info in_report;
/* tell the driver about the usage values */
in_report.report_type = HID_REPORT_TYPE_INPUT;
in_report.report_id = reportID;
/* this ioctl gets the report on the wire */
ret = ioctl(fd, HIDIOCGREPORT, &in_report);
uref.report_type = HID_REPORT_TYPE_INPUT;
uref.report_id = reportID;
uref.field_index = 0;
uref.usage_index = 0;
uref.usage_code = 0xff000001;
ret = ioctl(fd, HIDIOCGUCODE, &uref);
for( i = 0; i < num_vals; i++){
uref.usage_index = i;
ioctl(fd, HIDIOCGUSAGE, &uref);
usleep(delay);
vals[i] = (unsigned char)(uref.value & 0x000000FF) ;
}
return ret;
}
数据好象是一个一个byte 发送, 最后再发一条触发的命令HIDIOCSREPORT
接收也是一样,先发一个控制命令HIDIOCGREPORT,然后一个一个byte 接收,可能都是先缓冲的缘故