HID 设备PC端软件的开发

市面上一些无驱的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 接收,可能都是先缓冲的缘故

你可能感兴趣的:(HID 设备PC端软件的开发)