2013年9月11日 星期三

簡單講講 USB Human Interface Device

恩,發本文的原因是看到了以前畢業的朋友在旁邊的對話框問了一些問題,我想這些問題
不是三言兩語可以解釋完畢,但是我也不想又太細究 HID 內部描述表格的解說與視窗上
HID APIs 家族詳細使用方法,主要以偏向解決問題與實作面上的一些重點,希望可以有些
許幫助,因為詳細的文獻找 USB Complete: The Developer's Guide fourth edition 細讀第 11 章與
第 12 章就有,HID Descriptor 變化多端不可能逐一介紹全部情形,詳細情形除了參考上面資料
也一定要自己讀過訂製 USB 規範組織的 USBIF 發布的標準文獻

Device Class Definition for HID - USB.org  ( HID1_11.pdf )‎

HID Usage Tables 1.12 - USB.org              ( Hut1_12v2.pdf )

因為背這個沒甚麼用,寫韌體軟體的時候在去查閱想要用的型式就好
至於 USB 規範那本 Spec ,恩 假設讀者有念過有概念  : )  這應該是基本功夫,USB 基本知識
不再解說,我們要針對面對多變 HID Descriptor 的型式,怎麼樣的令 HID Descriptor 使得軟韌
體會比較好處理。

Human Interface Device 大概是許多人學 USB 入門選擇的項目,因為它可以讓初學的開發者
避開 艱困的 Windows Driver 或 Linux Driver 領域,可以使用系統提供的 HID APIs 去與韌體
程式通訊,但是相對來講,較複雜的層面就轉移到韌體上,韌體上要多宣告 Human Interface
Device 專屬的 HID Descriptor 與 HID Report Descriptor,一般會使用 USBIF 提供的小小工具
HID Descriptor Tool 來產生我們要的 Descriptor ,然後再貼到韌體程式碼裡面。

HID 正如其名,原本最初的目的是為了人機介面的互動,例如滑鼠移動,搖桿按鈕被壓下,
此外還被用在各種不同的輸入控制或一些感應器上面,我也看過有 UPS 或 Power Supply 用
HID 來作溫度電壓電流的監控,甚至連螢幕 USBIF 都訂出了給螢幕調整參數用的標準
HID Report Descriptor,這一類被稱為 Monitor Control,這也是後面等一下要講的例子,總之
用 HID 來規劃一些資料交換是蠻好用的型式,不過缺點就是資料交換速度也不能太快
對於 Full-Speed 來說大概是 64KB/sec,不過具有Alternative的機能介面的 HID,你還是躲不過要
寫 Driver 的窘境,馬上來介紹一些 HID 的基本特性啦

第一點  具有固定長度結構,這玩意稱為 Report,Host 藉由 Control TransferInterrupt Transfer

第二點 至少一定要有 1 個 Interrupt IN Endpoint,稱為 Input Report,中文稱輸入報告。
上面第二點要注意,我們一般不提最重要的 EP 0,因為 EP 0 用作 Control Transfer 是本來就要
有,所以至少要有 1 個 Interrupt IN Endpoint,是指除了必要的 EP 0,作為 HID 設備得還要至少
一個 Interrupt IN Endpoint。

第三點 最多可以有一個 Interrupt IN Endpoint 與 一個 Interrupt Out Endpoint,當然這是指 Interface
(有時本文會講機能介面) 只有一個的情況,有多個介面的 USB 裝置稱為 Composite Device,這
種情況就會可能有多個 Interrupt Endpoint。

第四點 Windows 98 Gold 支援的 USB 1.0,並不支援 Interrupt Out Endpoint ,這點目前好像
是多餘的事情,除非你還守在 WIN98 時代的骨灰開發者,我知道還有不過很稀少,因為
WIN98 作 Real-Time 控制速度很快,應用程式可以直接用組合語言控制硬體,2000 以後的
系統你就別想,乖乖的寫驅動程式。

馬上來看看最重要的術語 : Reports (報告)

HID 有三種類型的報告可以用 

1. Input     Report   (至少一個)
2. Output  Report   (可選)
3. Feature Report   (可選)

因為 HID 有規範特性的 Class Request,也就是說韌體上你除了要實作 USB Standard Request
另外還得實作 HID Class Request,當然韌體方面問題不大,大部分廠商都有框架程式碼可以
修改,要自己從頭寫起的機會不大,不過你有機會自己細寫韌體的部分就會看到韌體會有

Set / Get Report
Set / Get Idle
Set / Get Protocol

以上六條 Class Request ,這也不是甚麼大問題,依照韌體規範寫一寫而已。

現在來看一個例子,EIZO 的液晶螢幕 S2243W 就有實現 HID 裡面的 Monitor Control
就是說可以用 USB 調整螢幕裡面的那些參數,例如亮度,對比等等與螢幕相關的參數
它的 USB Descriptor 相當標準,是學習的好對象,我把用 USBlyzer 看到的 Descriptor 貼出來
瞧瞧看 :


非常標準的表格,可以看到標準的 USB Descriptor 階層化的排列

Device 
 ............Configuration
                       .............Interface
                                     .....HID Descriptor
                                     .....Endpoint Descriptor (EP1IN)

可以看到 假如是 USB 設備,HID Descriptor 就變成類似附加的資訊,會被插入在
Endpoint Descriptor 之前,至於裡面的欄位作用為何前面講過請讀者自己去閱讀相關
文獻,因為這是基本功夫讀者要自己具備,至少階層化描述子架構的關念要有就可以了。

接著,根據 HID Report Descriptor 會被附加在全部 Descriptor 的最後面,如下圖
(只列出部分,因為 EIZO 定義了很多參數可調,整個 Descriptor 很長)


最後面結尾你查閱 HID 規範就會知道是 End Collection 對應的代碼是 C0,沒錯

Usage Page  (用途頁)
Usage          (用途)
Collection
     ...
End Collection

這種就是最簡單的 HID Report Descriptor 的型式,你在設計 USB HID 設備想要輕鬆的話
最好就把韌體端的 HID Report Descriptor 的型式規劃像 HID Monitor 的這種型式,因為
這種型式會讓 Host 端的程式變得很好寫,也會比較簡單,因為 Feature Report 是採用
控制傳輸,這種傳輸可以很方便的可以自創自訂命令,只有當要資料的時候,程式端
使用 HidD_GetFeature API 與 HidD_SetFeature API 就可以很輕易的跟韌體做資料交換,而且
該兩條 API 回傳採用成功就是 TRUE 或是失敗就是 FALSE 可以避開要懂作業系統上,物件
同步的相關知識,以下我們就看看一個範例程式怎麼讀出該螢幕的名稱。

首先我先用 USBlyzer 觀察 原廠的軟體怎麼跟螢幕韌體溝通,如下圖


嘿嘿,我從 USBlyzer 擷取到了 EIZO 的軟體怎麼跟螢幕內的韌體通訊,很明顯,畫面顯示
Report 的類型 Feature Report,也就是說程式要跟該螢幕韌體通訊,你得要呼叫的 HID API 是

HidD_GetFeature

在讓我們看看為什麼 Report 會長這個模樣,這個又回到原本 HID Report Descriptor 是怎麼樣
的型式,型式如下 :

  Report Size (8)  75 08 
  Report Count (8)  95 08 
  Logical Maximum (255)  26 FF 00 
  Report ID (50)  85 32 
  Usage (Vendor-Defined 195)  09 C3 
  Feature (Data,Var,Abs,NWrp,Lin,Pref,NNul,NVol,Buf)  B2 02 01

沒錯,有沒有看到阿,Report Size 是 8 就是說資料大小是 8-bit 也就是 1 個 Byte,然後呢
Report Count 是 8,所以傳 8 次,Logical Maximum 是 255,也沒錯,Range Lg/Ph 欄位的確
顯示每個 BYTE 可以放入 0 ~ 255 之間的資料,Report ID 是 50 與 Usage 是 195 都是相符
核,所以說,搞懂 USB 最重要的就是你要知道怎麼規畫好這些 Descriptor ,這個會影響到
你的 Host 與 Device 端的通訊,弄懂了你自己寫軟韌體腦袋就會很清楚,至於 Feature 裡面
是在做甚麼,一樣請參閱 USB HID 的規範,規範寫的應該會比我講得更清楚吧。

下面給出我寫好的透過 HID 讀取 EIZO Monitor 名稱的範例
程式的部分我就不講解了,假如你是有在寫 HID 程式的開發者,自然就會了解程式裡面
哪些東西可以搬到自己的專案用,我是用 MFC 寫,我只貼主要的 CPP 檔,一些標頭的宣告
就不貼了,這也是本篇的目的,給出一個實際的 Code,讓同樣 HID 的開發者們也可以從範例
上剪貼自己需要的部分進入自己的專案節省時間。

不過 標頭檔 H 裡面,有一個地方先貼給讀者,就是引入 HID API 常用的標頭檔

extern "C" {
#include "Hidsdi.h"
#include "SetupAPI.h"
#include "HidUsage.h"
#include "HidPi.h"
}

為什麼要特別講這個,因為 設備驅動的領域大部分都用 C 語言在寫,你沒有加上
extern "C" 編譯可能就會有問題,因為這些東西是在 DDK 裡面,這些並不是 SDK 裡面的 API
另外還記得要引入 setupapi.lib 與 hid.lib,DDK 的部分最好安裝 3790 以上的版本。

主要程式 (MFC的部分其實不太用理會看懂一些呼叫 HID 與 SetupDi APIs 的地方即可) :

#include "stdafx.h"
#include "HIDGet.h"
#include "HIDGetDlg.h"
#include "Process.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

#define WM_THREADDATA   WM_USER + 1
#define WM_READDATA     WM_USER + 2

/////////////////////////////////////////////////////////////////////////////
// CHIDGetDlg dialog
struct _ThreadData
{
    HWND   hWnd;
    HANDLE hDev;
    char   cBuf[9];
    HANDLE hReadFinished;
}ThreadData;

LRESULT CALLBACK ReadThreadWindowProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    BOOL bRet;

    ResetEvent(ThreadData.hReadFinished);
    OVERLAPPED ol;
    ol.hEvent = ThreadData.hReadFinished;
    ol.Offset = 0;
    ol.OffsetHigh = 0;

    DWORD wResult, wByteRead;
    switch(msg)
    {
    case WM_CLOSE:
        CancelIo(ThreadData.hDev);
        CloseHandle(ThreadData.hReadFinished);
        DestroyWindow(hWnd);
        break;

    case WM_DESTROY:
        PostQuitMessage(0);
        break;

    case WM_READDATA:
        //ReadFile(ThreadData.hDev, ThreadData.cBuf, 9, &wByteRead, &ol);
        ThreadData.cBuf[0] = 50;
        bRet = HidD_GetFeature(ThreadData.hDev, ThreadData.cBuf, 9);
        //wResult = WaitForSingleObject(ThreadData.hReadFinished, 1000);
        if(/*wResult == WAIT_OBJECT_0*/ bRet == TRUE)
            ::PostMessage(ThreadData.hWnd, WM_THREADDATA, 0, 0);
        else if(/*wResult == WAIT_TIMEOUT*/ bRet == FALSE)
            ::PostMessage(hWnd, WM_READDATA, 0, 0);
        break;
    default:
        return DefWindowProc(hWnd, msg, wParam, lParam);
    }

    return 0;
}

void ReadThread(CHIDGetDlg* pDlg)
{
    WNDCLASSEX wndclass;
    wndclass.cbSize        = sizeof(WNDCLASSEX);
    wndclass.cbClsExtra    = 0;
    wndclass.cbWndExtra    = 0 ;
    wndclass.hbrBackground = NULL;
    wndclass.hCursor       = NULL;
    wndclass.hIcon         = NULL;
    wndclass.hIconSm       = NULL;
    wndclass.hInstance     = GetModuleHandle(NULL);
    wndclass.lpfnWndProc   = ReadThreadWindowProc;
    wndclass.lpszClassName = "ReadThread";
    wndclass.lpszMenuName  = NULL;
    wndclass.style         = 0 ;
    RegisterClassEx(&wndclass);

    HWND hReadThreadWindow = ::CreateWindow("ReadThread", "", WS_POPUP, 
                                            0, 0, 0, 0, NULL, NULL,
                                            GetModuleHandle(NULL), NULL);
    pDlg->SetReadThreadHWND(hReadThreadWindow);
    ThreadData.hReadFinished = CreateEvent(NULL, TRUE, FALSE, NULL);
    SetEvent(pDlg->m_hReadThreadCreated);

    // Start the message loop 
    MSG msg;
    while(GetMessage(&msg, NULL, NULL, NULL))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
}

CHIDGetDlg::CHIDGetDlg(CWnd* pParent /*=NULL*/) : CDialog(CHIDGetDlg::IDD, pParent)
{
    //{{AFX_DATA_INIT(CHIDGetDlg)
    // NOTE: the ClassWizard will add member initialization here
    //}}AFX_DATA_INIT
    // Note that LoadIcon does not require a subsequent DestroyIcon in Win32
    m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}

void CHIDGetDlg::DoDataExchange(CDataExchange* pDX)
{
    CDialog::DoDataExchange(pDX);
    //{{AFX_DATA_MAP(CHIDGetDlg)
    DDX_Control(pDX, IDC_DATA_ED, m_edData);
    //}}AFX_DATA_MAP
}

BEGIN_MESSAGE_MAP(CHIDGetDlg, CDialog)
    //{{AFX_MSG_MAP(CHIDGetDlg)
    ON_WM_PAINT()
    ON_WM_QUERYDRAGICON()
    ON_WM_CLOSE()
    //}}AFX_MSG_MAP
END_MESSAGE_MAP()

/////////////////////////////////////////////////////////////////////////////
// CHIDGetDlg message handlers

void CHIDGetDlg::SetReadThreadHWND(HWND hWnd)
{
    m_hReadThread = hWnd;
}

void CHIDGetDlg::CreateReadThread()
{
    ThreadData.hWnd = m_hWnd;
    m_hReadThreadCreated = CreateEvent(NULL, TRUE, FALSE, NULL);
    if(m_hReadThreadCreated)
    {
        DWORD threadID;
        if(_beginthreadex(NULL, 0, (unsigned int (WINAPI*)(PVOID))ReadThread, 
            this, 0, (unsigned int*)&threadID) != 0)
            WaitForSingleObject(m_hReadThreadCreated, INFINITE);

        CloseHandle(m_hReadThreadCreated);
    }
}

BOOL CHIDGetDlg::OnInitDialog()
{
    CDialog::OnInitDialog();

    // Set the icon for this dialog.  The framework does this automatically
    //  when the application's main window is not a dialog
    SetIcon(m_hIcon, TRUE);         // Set big icon
    SetIcon(m_hIcon, FALSE);        // Set small icon

    // TODO: Add extra initialization here
    GetDeviceHandle();
    CreateReadThread();
    ::PostMessage(m_hReadThread, WM_READDATA, 0, 0);

    return TRUE;  // return TRUE  unless you set the focus to a control
}

// If you add a minimize button to your dialog, you will need the code below
//  to draw the icon.  For MFC applications using the document/view model,
//  this is automatically done for you by the framework.

void CHIDGetDlg::OnPaint() 
{
    if (IsIconic())
    {
        CPaintDC dc(this); // device context for painting

        SendMessage(WM_ICONERASEBKGND, (WPARAM) dc.GetSafeHdc(), 0);

        // Center icon in client rectangle
        int cxIcon = GetSystemMetrics(SM_CXICON);
        int cyIcon = GetSystemMetrics(SM_CYICON);
        CRect rect;
        GetClientRect(&rect);
        int x = (rect.Width() - cxIcon + 1) / 2;
        int y = (rect.Height() - cyIcon + 1) / 2;

        // Draw the icon
        dc.DrawIcon(x, y, m_hIcon);
    }
    else
    {
        CDialog::OnPaint();
    }
}

// The system calls this to obtain the cursor to display while the user drags
//  the minimized window.
HCURSOR CHIDGetDlg::OnQueryDragIcon()
{
    return (HCURSOR) m_hIcon;
}

HANDLE CHIDGetDlg::GetDeviceHandle(GUID guid, HANDLE hDev, DWORD wDevice)
{
    SP_DEVICE_INTERFACE_DATA interfaceDev;
    interfaceDev.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);

    //Get interface
    DWORD wSize = 0;
    if(!SetupDiEnumDeviceInterfaces(hDev, NULL, &guid, wDevice, &interfaceDev) 
        || SetupDiGetDeviceInterfaceDetail(hDev, &interfaceDev, NULL, 0, &wSize, NULL))
        return INVALID_HANDLE_VALUE;

    //Create buffer
    SP_INTERFACE_DEVICE_DETAIL_DATA *pDeviceDetail;
    pDeviceDetail = (SP_INTERFACE_DEVICE_DETAIL_DATA*)malloc(wSize);
    pDeviceDetail->cbSize = sizeof(SP_INTERFACE_DEVICE_DETAIL_DATA);

    if(!SetupDiGetDeviceInterfaceDetail(hDev, &interfaceDev, pDeviceDetail, wSize, &wSize, NULL))
    {
        free(pDeviceDetail);
        return INVALID_HANDLE_VALUE;
    }

    //Get device handle
    HANDLE hDevice = CreateFile(pDeviceDetail->DevicePath, GENERIC_READ,
        FILE_SHARE_READ, NULL, OPEN_EXISTING,
        FILE_FLAG_OVERLAPPED, NULL);

    free(pDeviceDetail);
    return hDevice;
}

void CHIDGetDlg::Show(CString str,long n)
{
    CString strN;
    strN.Format("%d", n);
    m_edData.ReplaceSel(str + ":" + strN + "\r\n");
}

void CHIDGetDlg::Show(CString str, CString s)
{
    m_edData.ReplaceSel(str + ": " + s + "\r\n");
}

void CHIDGetDlg::Clear()
{
    int nStart, nStop;
    m_edData.GetSel(nStart, nStop);
    m_edData.SetSel(0, nStop);
    m_edData.Clear();
}

void CHIDGetDlg::GetDeviceHandle()
{
    //Get HID GUID
    GUID guid;
    HidD_GetHidGuid(&guid);

    //Get all present HID interface
    HDEVINFO hDeviceInfo = SetupDiGetClassDevs(&guid, NULL, NULL,
                                               DIGCF_PRESENT|DIGCF_DEVICEINTERFACE);
    if(hDeviceInfo == INVALID_HANDLE_VALUE) return;

    Clear();
    DWORD w = 0;
    while(1) /*for(DWORD w=0; w<20; w++)*/
    {
        if((ThreadData.hDev = GetDeviceHandle(guid, hDeviceInfo, /*w*/w++))!=INVALID_HANDLE_VALUE)
        {
            HIDD_ATTRIBUTES att;
            if(HidD_GetAttributes(ThreadData.hDev, &att))
            {
                PHIDP_PREPARSED_DATA pPreData;
                if(HidD_GetPreparsedData(ThreadData.hDev, &pPreData))
                {
                    HIDP_CAPS cap;
                    if(HidP_GetCaps(pPreData, &cap)==HIDP_STATUS_SUCCESS)
                    {
                        if(att.VendorID==0x056D && att.ProductID==0x0002 && att.VersionNumber==0x7530)
                        {
                            if(cap.Usage==0x01 && cap.UsagePage==0x80)
                            {
                                HidD_FreePreparsedData(pPreData);
                                break;
                            }
                        }
                    }
                    HidD_FreePreparsedData(pPreData);
                }
            }
            CloseHandle(ThreadData.hDev);
            ThreadData.hDev = INVALID_HANDLE_VALUE;
        }

        if(w > 65536)
        {
            CloseHandle(ThreadData.hDev);
            ThreadData.hDev = INVALID_HANDLE_VALUE;
            break;
        }
    }

    SetupDiDestroyDeviceInfoList(hDeviceInfo);
}

void CHIDGetDlg::OnClose() 
{
    // TODO: Add your message handler code here and/or call default
    ::SendMessage(m_hReadThread, WM_CLOSE, 0, 0);
    if(ThreadData.hDev!=INVALID_HANDLE_VALUE)
        CloseHandle(ThreadData.hDev);

    CDialog::OnClose();
}

LRESULT CHIDGetDlg::DefWindowProc(UINT message, WPARAM wParam, LPARAM lParam) 
{
    // TODO: Add your specialized code here and/or call the base class
    static long nCount = 0;

    if(message == WM_THREADDATA)
    {           
        CString str;
        str.Format("# of packet = %d, report id =%d, LCD = %c %c %c %c %c %c %c %c",
            nCount++, 
            ThreadData.cBuf[0], // report id = 50
            ThreadData.cBuf[1],
            ThreadData.cBuf[2],
            ThreadData.cBuf[3],
            ThreadData.cBuf[4],
            ThreadData.cBuf[5],
            ThreadData.cBuf[6],
            ThreadData.cBuf[7],
            ThreadData.cBuf[8]);

        Show("Data", str);
        if(nCount > 500)
        {
            Clear();
            nCount = 0;
        }

        ::PostMessage(m_hReadThread, WM_READDATA, 0, 0);
    }

    return CDialog::DefWindowProc(message, wParam, lParam);
}

上面程式關鍵的部分可以看到 我呼叫 HidD_GetFeature

    case WM_READDATA:
        //ReadFile(ThreadData.hDev, ThreadData.cBuf, 9, &wByteRead, &ol);
        ThreadData.cBuf[0] = 50;
        bRet = HidD_GetFeature(ThreadData.hDev, ThreadData.cBuf, 9);
        //wResult = WaitForSingleObject(ThreadData.hReadFinished, 1000);
        if(/*wResult == WAIT_OBJECT_0*/ bRet == TRUE)
            ::PostMessage(ThreadData.hWnd, WM_THREADDATA, 0, 0);
        else if(/*wResult == WAIT_TIMEOUT*/ bRet == FALSE)
            ::PostMessage(hWnd, WM_READDATA, 0, 0);
        break;


各位可以比較看看 VersionNumber 有沒有發現就是 Device Descriptor 裡面的 bcdDevice

可以看到 HidD_GetFeature 很方便,我先在 位元組 0 給想要的 Report ID,然後下命令,成功
的話傳回 TRUE,失敗的話傳回 FALSE,這個比用同步物件簡單多了,TRUE 的話就 Post
我自訂的 WM_THREADDATA 訊息,這邊就回到視窗程設的基礎,PostMessage 是將訊息
放入訊息佇列裡面然後函式就直接返回,不理會是否有被處理,應用程式可以
用 GetMessage 收到該訊息,相反的就是 Post 另外一個自訂訊息 WM_READDATA,這樣
可以使得 下一輪訊息迴圈有機會進入 WM_READDATA,總之假如你有看懂程式就會知道,
其實就會產生像下面這種循環 :


" 有機會進入 " 是一種用語,因為系統可不保證 GetMessage 從 Message Queue 拿到的訊息
就是你剛剛放進去的訊息,反正 GetMessage 輪詢個不停 總有會被處理到的時候。

事實上,主要我還有宣告一個隱形的視窗,然後才在這個隱形的視窗下的 WndProc 用訊息
傳遞的方式與主視窗和 Thread 通訊,透過自訂的視窗訊息控制來控制 Thread 抓取 Report 。

程式實際執行模樣


執行後可以看到程式透過 HID APIs 也讀取出螢幕名稱 S2243W 

最後稍微略提一下 USBlyzer,這是一種在開發 USB 很有用的工具,可以觀察主機端 USB 封包
的 IN/OUT ,還具有特性 USB Class 資料封包解碼的能力,這類工具在開發 USB 驅動或是應
用軟體都是不可或缺的工具,還會列出許多進階的資訊像 IRP,URB,Device Stack,
Pnp 屬性,許多分析功能在開發 USB 驅動都是不可或缺的資訊,在除錯方面會有很用


就講到這裡,本篇主要是針對給 USB HID 有一定了解的讀者,給出一個程式提供大家自己
需要來剪貼程式節省開發時間,裡面內容沒講的部分,那只得請讀者自己先去把規格 K 懂。

MFC 的部分也不要問我,用 MFC Dialog Base 純粹只是為了快速拖拉一個介面測試 HID API
用 WinMain 的古典形式寫當然也可以,比較耗工就是了。

最後提醒 你假如想要讀取的 HID 對像是標準的滑鼠鍵盤搖桿,你用上面的技術讀不出來
因為這些設備被系統視為特殊設備,而且他們的 HID 在 Usage 下面跟的 Collection 分類是
Physical 類型,因為這些 Input 設備連系統也一直不斷在輪詢使用它們,微軟對這些真正的輸入
設備提供了專用的 Raw Input API ,詳細情形直接參考 MSDN


主要原因就是有 Physical 的設備在 HID 驅動下還需要處理 Physical Descriptor 才抓到到下面
跟隨的資訊,你只用標準 HID APIs 是做不到這種功能,請乖乖改用 Raw Input API

HID APIs 讀取像下面這種標準型式沒問題 (Feature Report 令的方法也一樣)
反正用 HID Descriptor Tool  產生一下 剪剪貼貼就有了

Usage Page (vendor-defined)                      ff   a0
Usage          (vendor-defined)                     09  01
Collection    (Application)                           A1 01
    Usage (vendor-defined)                09 03
    Logical Minimum (0)                    15 00
    Logical Maximum (255)               26 00 ff
    Report Size  (8-bits)                    75 08
    Input (Data, Variable, Absolute)  81 02
End Collection                                            0c

Raw Input 主要分幾類講解

Registering for Raw Input

Performing a Standard Read of Raw Input

Performing a Buffered Read of Raw Input


其中

Performing a Standard Read of Raw Input

This sample shows how an application does an unbuffered (or standard) read of raw input from either a keyboard or mouse Human Interface Device (HID) and then prints out various information from the device.
我想上面講的很清楚了,想了解的讀者去看跟在下面的 Sample 就懂了
基本上就是要處理 WM_INPUT 訊息。

19 則留言:

  1. 這是我參考學長的程式後,再重新寫一次的程式,但是還是不能讀取呀!!!我有試過HidD_GetFeature但還是不行,我到底什麼地方錯了......QQ


    #include
    #include
    #include
    #include

    extern "C"
    {
    #include "C:\h\hidsdi.h"
    #include
    }
    #pragma comment(lib, "hid")
    #pragma comment(lib, "setupapi")
    #pragma comment(lib, "kernel32.lib")

    #include "stdafx.h"
    #include

    HANDLE Get_InterfaceHandle( GUID vGuid, DWORD instance);

    struct MyDevice
    {
    HWND hWnd;
    HANDLE hDev;
    HANDLE hReadFinished;
    char cBuf[9];
    };


    int _tmain(int argc, _TCHAR* argv[])
    {
    USHORT para[5];

    // para[0] = 0X1BCF;
    // para[1] = 0X0007;
    // para[2] = 0X0014;
    // para[3] = 0XF1F3;
    // para[4] = 0XF1F3;

    para[0] = 0X144F;
    para[1] = 0X7500;
    para[2] = 0X0099;
    para[3] = 0XFF00;
    para[4] = 0X0001;

    MyDevice myDevice;
    myDevice.hDev=NULL;
    myDevice.hReadFinished=NULL;
    myDevice.hWnd=NULL;
    myDevice.cBuf[0] = 0;
    myDevice.cBuf[1] = 0;
    myDevice.cBuf[2] = 0;
    myDevice.cBuf[3] = 0;
    myDevice.cBuf[4] = 0;
    myDevice.cBuf[5] = 0;
    myDevice.cBuf[6] = 0;
    myDevice.cBuf[7] = 0;
    myDevice.cBuf[8] = 0;


    GUID guid; //裝置
    DWORD wDevice = 0; //計數裝置
    HIDD_ATTRIBUTES att; //獲得PID、VID和版本


    HidD_GetHidGuid(&guid);
    PHIDP_PREPARSED_DATA pPreData;
    HIDP_CAPS cap;

    wchar_t mString[256]; //為了顯示裝置的製造商、產品和SerialNumber資訊
    char ch_buffer[256]; //為了顯示裝置的製造商、產品和SerialNumber資訊
    char blank[1]=""; //為了顯示裝置的製造商、產品和SerialNumber資訊

    DWORD wByteRead =0,wResult = 0; //讀值
    // ResetEvent(myDevice.hReadFinished);

    OVERLAPPED ol;
    // ol.hEvent = myDevice.hReadFinished;
    ol.Offset = 0;
    ol.OffsetHigh = 0;

    ol.hEvent = CreateEvent(NULL,FALSE,FALSE,NULL);

    回覆刪除
  2. while(1)
    {
    myDevice.hDev = Get_InterfaceHandle( guid, wDevice++);
    if ( myDevice.hDev == NULL)
    return 0;

    if (myDevice.hDev == INVALID_HANDLE_VALUE)
    {
    printf("Found the %dth HID interface\n", wDevice);
    printf("CreateFile open error!\n\n");
    }
    else
    {
    printf("Found the %dth HID interface\n", wDevice);
    if(HidD_GetAttributes(myDevice.hDev, &att))
    {
    if(HidD_GetPreparsedData(myDevice.hDev, &pPreData))
    {
    if(HidP_GetCaps(pPreData, &cap)==HIDP_STATUS_SUCCESS)
    {
    if ( HidD_GetManufacturerString( myDevice.hDev, mString, sizeof(mString)) )
    {
    if (wcstombs(ch_buffer, mString, 256) == -1) // -1 = conversion failure
    ch_buffer[0] =blank[0];
    printf("XXX Manufacturer: %s\n",ch_buffer);
    }

    if ( HidD_GetProductString( myDevice.hDev, mString, sizeof(mString)) )
    {
    if (wcstombs(ch_buffer, mString, 256) == -1) // -1 = conversion failure
    ch_buffer[0] =blank[0];
    printf("XXX Product: %s\n",ch_buffer);
    }

    if ( HidD_GetSerialNumberString( myDevice.hDev, mString, sizeof(mString)) )
    {
    if (wcstombs(ch_buffer, mString, 256) == -1) // -1 = conversion failure
    ch_buffer[0] =blank[0];
    printf("XXX Serial Number: %s\n",ch_buffer);
    }
    printf("VendorID: %x ProductID: %x VersionNumber: %x\n",att.VendorID,att.ProductID,att.VendorID);
    printf("Top level Usage page %xh, usage %xh\n", cap.UsagePage, cap.Usage);
    printf("InputReportByteLength %d\n", cap.InputReportByteLength);
    printf("OutputReportByteLength %d\n", cap.OutputReportByteLength);
    printf("FeatureReportByteLength %d\n\n", cap.FeatureReportByteLength);

    if(att.VendorID==para[0] && att.ProductID==para[1] && att.VersionNumber==para[2] && cap.UsagePage==para[3] && cap.Usage==para[4])
    {
    ReadFile(myDevice.hDev, myDevice.cBuf, cap.InputReportByteLength , &wByteRead, &ol);
    wResult = WaitForSingleObject(ol.hEvent, 10000);

    switch(wResult)
    {
    case WAIT_OBJECT_0: {
    // Success;
    // Use the report data;
    break;
    }
    case WAIT_TIMEOUT: {
    // Timeout error;
    //Cancel the read operation.
    CancelIo(myDevice.hDev);
    break;
    }
    default: {
    // Undefined error;
    //Cancel the read operation.
    CancelIo(myDevice.hDev);
    break;
    }
    }


    printf(" wByteRead: %d\n\n", wByteRead);

    // if(!HidD_GetInputReport(myDevice.hDev, myDevice.cBuf, 2))//cap.FeatureReportByteLength))
    if(!HidD_GetFeature(myDevice.hDev, myDevice.cBuf, 2))//cap.FeatureReportByteLength))
    break;
    else
    {

    printf(" Input report ID: %d\n\n", myDevice.cBuf[0]);

    if (wByteRead >= 1)
    {
    printf("***************************************\n");
    printf(" read success\n");
    printf("***************************************\n\n");
    printf(" Input report 1st byte: %x\n", myDevice.cBuf[1]);
    }
    if (wByteRead >= 2)
    printf(" Input report 2st byte: %x\n", myDevice.cBuf[2]);

    }
    }
    /* if(att.VendorID==0x056D && att.ProductID==0x0002 && att.VersionNumber==0x7530)
    {
    if(cap.Usage==0x01 && cap.UsagePage==0x80)
    {
    HidD_FreePreparsedData(pPreData);
    break;
    }
    }*/


    }
    HidD_FreePreparsedData(pPreData);
    }
    }
    else
    {
    printf("Could not get HID attributes.\n");
    printf("It may be a MOUSE or a KEYBOARD!\n\n");
    }
    }

    // CloseHandle(myDevice.hDev);
    // myDevice.hDev = INVALID_HANDLE_VALUE;

    CloseHandle( myDevice.hDev);
    }

    return 0;
    }

    回覆刪除
  3. HANDLE Get_InterfaceHandle( GUID vGuid, DWORD wDevice)
    {
    SP_DEVICE_INTERFACE_DATA interfaceDev; //裝置資訊 2
    interfaceDev.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
    DWORD wSize = 0;
    HANDLE hDev = NULL;

    // SP_INTERFACE_DEVICE_DETAIL_DATA *pDeviceDetail; //路徑 3、4
    // pDeviceDetail = (SP_INTERFACE_DEVICE_DETAIL_DATA*)malloc(wSize);
    // pDeviceDetail->cbSize = sizeof(SP_INTERFACE_DEVICE_DETAIL_DATA);

    /////////////////////////////////// 1 //////////////////////////////////////////
    HDEVINFO hDeviceInfo = SetupDiGetClassDevs(&vGuid, NULL, NULL,
    DIGCF_PRESENT|DIGCF_DEVICEINTERFACE);

    if(hDeviceInfo == INVALID_HANDLE_VALUE)
    {
    printf("No [Device Information Set] available for this GUID!\n");
    return hDev;
    }
    ///////////////////////////////// 2、3 //////////////////////////////////////////
    if(!SetupDiEnumDeviceInterfaces(hDeviceInfo, NULL, &vGuid, wDevice, &interfaceDev))
    {
    printf("No more interface available for this GUID!\n");
    SetupDiDestroyDeviceInfoList( hDeviceInfo);
    return hDev;
    }
    SetupDiGetDeviceInterfaceDetail(hDeviceInfo, &interfaceDev, NULL, 0, &wSize, NULL);

    PSP_DEVICE_INTERFACE_DETAIL_DATA pifPath =
    (PSP_DEVICE_INTERFACE_DETAIL_DATA) (new char[wSize]);
    pifPath->cbSize = sizeof( SP_DEVICE_INTERFACE_DETAIL_DATA);
    // printf("pifPath->cbSize: %d\n", pifPath->cbSize);
    /////////////////////////////////// 4 //////////////////////////////////////////
    if(!SetupDiGetDeviceInterfaceDetail(hDeviceInfo, &interfaceDev, pifPath, wSize, &wSize, NULL))
    {
    free(pifPath);
    return hDev;
    }

    // printf("Path of the interface Object is %s\n",pifPath->DevicePath);
    /////////////////////////////////// 5 //////////////////////////////////////////
    hDev = CreateFile(pifPath->DevicePath, GENERIC_READ,
    FILE_SHARE_READ, NULL, OPEN_EXISTING,
    FILE_FLAG_OVERLAPPED, NULL);

    // if ( hDev == INVALID_HANDLE_VALUE)
    // {
    // printf("CreateFile open error!\n");
    // }

    ////////////////////////////////////////////////////////////////////////////////////////

    free(pifPath);
    SetupDiDestroyDeviceInfoList( hDeviceInfo);
    return hDev;

    }

    回覆刪除
  4. 阿凱~~~ 韌體的部分 也有包嗎 ~~~
    0.0 HID 跟你使用的描述子有關
    是否韌體搭配那邊可能有問題呢? 要不要檢查一下

    我要明天 meeting 完才有空一行一行看你貼的 Code。
    被凹要做速度曲線追蹤 囧

    因為 GetFeature 是給 Feature Report 用的 API
    韌體要有設計才有用 USB 軟韌都要互相搭配才行

    因為根據你講說 讀取的 HID 設備 Handle 本身有抓到,但是資料老是抓不出來 ~~~
    檢查一下你的韌體吧 ~~~ 或是說原廠有沒有可以搭配 Work 的 Host 軟體
    用 USBlyzer 去檢查究竟是怎麼通訊的形式

    你假如確定韌體真的沒問題,就用我以前教你的老方法,用 Visual Studio
    開一個簡單的 Console 程式,先加設備列舉的部分,printf 看看有沒有問題
    ok 了 在加 CreateFile 打開設備的 Code, 最後才用 HID APIs 或 ReadFile WriteFile
    讀讀看設備,一步一步驗證 你進步得很快阿 不虧是我指定的傳人 哈哈

    總之 明天 meeting 完 我會仔細看你貼的 Code

    回覆刪除
  5. 還有 我稍微看你的程式 有呼叫 wcstombs
    我覺得你現在就要直接開始習慣用 Unicode 程式
    就不用老是轉來轉去,Unicode 版本的 printf
    是 wprintf,字串前面記得加 L, ex: L"我是阿凱"

    回覆刪除
  6. 哈哈哈,如果沒有學長之前的教導,想必我不能吸收這麼快的啦XD。我這邊再努力看看吧,感覺快出來了!!

    回覆刪除
    回覆
    1. 終於有東西了....感謝學長!!!
      我把程式搞好一點在PO給學長看:)

      刪除
  7. 其實發現我的CODE應該沒問題,只差在我那台裝置需要先輸入00000004他才會開始輸出資料,之前沒讀到資料就是差在這一步。

    回覆刪除
    回覆
    1. 喔 你這種是回應式的HID阿
      用 Input Report 與 Output Report 控制 USB HID

      USB 領域有嚴格定義 總是站在 Host 方向看
      Host ----> Device 叫做 OUT
      Host <---- Device 叫做 IN

      你只是又掛在 HID 的協定上做

      0.0 這個其實最原始的做法 就是用真正的 USB Bulk IN/OUT
      這種彈性大速度快,才能達到接近 1.2 MB/s 的速度 HID 不行
      不過看你的程式類型 也不像資料擷取卡 好像也不用快

      所以我才說幹嘛這麼累阿~~~~~~~
      要先 OUT 然後才 IN 的通訊型

      要嘛像我在韌體規劃用 Feature Report 就好了
      因為控制傳輸就有這種效果 不用在那邊 WriteFile ReadFile
      還要 Overlapped 多麻煩阿~~

      一個 GetFeature 就搞定了 而且還一樣跟你有 8 byte data 可以用

      要玩 OUT/IN 直接走原生的 USB Bulk 就好啦

      你遇到 Win9X 的老系統客戶 不支援 Interrupt Endpoint OUT
      那不就掰掰了嗎 ? ~~~~~~ 哈哈

      你是用哪家的晶片阿 阿凱 ?? 這麼神秘 不告訴我 哈哈

      總之 你要記住 要走 HID 的話 要就是 Input Report
      你的程式自己 Polling ,要走命令 就用 Feature Report

      我自己本人寫 HID 也不太用 Interrupt EP OUT

      刪除
    2. 我在Waft那邊上班啦,你問他可能比我還清楚XD,不過要用Feature Report的話要怎麼做呀?有需要到什麼東西嗎?而我一開始選用ReadFile和WriteFile的原因是因為前人是用這個,所以我也算是半COPY(只是他是用VB來寫),我對通訊這塊還是很陌生呀,所以你跟我說用USB Bulk來做我也不會做...。再說我也沒很多時間研究,感覺我已經快來不及了,我還要用QT去寫UI介面,這邊來動都還沒動QQ

      刪除
  8. 晶片是瑞薩的
    uart應該也是他自己內建的

    回覆刪除
  9. QT 阿; 哈哈 你還是得面對 UI 加油啦~~~~
    對 QT 框架不熟悉的話 而且趕時間的話
    就用 Designer ; 我的 QT 都一直用 4.6.3
    環境是 Qt4 Visual Studio Add-in
    與 Qt by Nokia 4.6.3 (VS2008 Open Source)
    現代這種繼承式的物件導向 UI 框架 大概都長得差不多
    wxWidget MFC QT Win32++ ; GTK 除外 他是 C Interface 型的 UI 框架
    Win32++ 又被稱為 Mini MFC 是有高手參考 MFC 原始碼 把常用的部分抽出來
    從新設計的小 MFC
    Borland 方面就是 VCL OWL 等等 當然還有許多 小公司設計的其他框架

    要用 Feature Report 就要調出韌體端原始碼修改阿 ; 在 Report Descriptor
    加入一 Feature Report ; 在韌體端找處理Get Report Request的原始碼 只是把承載的
    資料換成用 Feature Report 而已 ; 不過等你有自己需要 寫 USB 韌體可以
    再來討論 ; 你的案子趕的話 當然也是先把案子搞定 ; 我記得我以前在老師
    的公司上班 案子快來不及了 才說 要加一個 功能可以直接用網路把 資料輸出
    也是急急忙忙才匆忙找一本 Win32 Socket 翻翻有沒有類似的 sample 先可以
    work 在說; 其實你有沒有發現 你真的畢業出去了 真的就很難 好好地做下來
    把一門技術鑽研到精 ; 這就是我以前為何老是逼著你要學一點 ; 你現在 有 Win32
    的基礎 要學 QT 那是很簡單的事情 用繼承的方式寫 總比寫 Native API 簡單

    回覆刪除
  10. 瑞薩 MCU 晶片阿 ; 瑞薩的部分我只用過 H8S 系列
    瑞薩的產品蠻不錯學,產品的支援度都很完整
    不過 瑞薩 沒有做 Hi-Speed 以上的周邊控制晶片
    這點倒是蠻怪的地方,Hi-Speed 以上都只專攻 Host 控制晶片

    回覆刪除
  11. 我做了一個 USB HID DEVICE, HID report as follows:

    Usage Page (Vendor-Defined 1) 06 00 FF
    Usage (Vendor-Defined 2) 09 02
    Collection (Application) A1 01
    Logical Minimum (0) 15 00
    Logical Maximum (255) 26 FF 00
    Report Size (8) 75 08
    Report ID (1) 85 01
    Report Count (7) 95 07
    Usage (Vendor-Defined 2) 09 02
    Input (Data,Var,Abs,NWrp,Lin,Pref,NNul,Bit) 81 02
    Usage (Vendor-Defined 2) 09 02
    Output (Data,Var,Abs,NWrp,Lin,Pref,NNul,NVol,Bit) 91 02
    Report ID (2) 85 02
    Report Count (63) 95 3F
    Usage (Vendor-Defined 2) 09 02
    Input (Data,Var,Abs,NWrp,Lin,Pref,NNul,Bit) 81 02
    Usage (Vendor-Defined 2) 09 02
    Output (Data,Var,Abs,NWrp,Lin,Pref,NNul,NVol,Bit) 91 02
    End Collection C0
    USB HID device success send 8 bytes data to pc. (01 35 00 00 00 00 00 00)

    HidP_GetCaps(pPreData, &cap) get cap.InputReportByteLength = 64
    Part of the C++ source code:

    {
    memset(ReadReportBuffer, 0, sizeof(ReadReportBuffer));
    ReadFile(m_hReadHandle,
    ReadReportBuffer,
    64,
    NULL,
    &ReadOverlapped);
    }
    dObject = WaitForMultipleObjects(2, hArray, FALSE, INFINITE);
    if (dObject == WAIT_OBJECT_0)
    {
    if (!MyDevFound)//Device extract also set the event
    {
    continue;
    }
    GetOverlappedResult(m_hReadHandle,
    &ReadOverlapped,
    &nBytesRead,
    TRUE);//you can also set the last parameter False
    if (nBytesRead != 0)
    {
    m_strTemp = ReadReportBuffer;
    m_strLog.Format(_T("Read the Report Data Length is %d.(%02d:%02d:%02d)"),
    nBytesRead, sysTime.wHour, sysTime.wMinute, sysTime.wSecond);
    m_strLog += m_strTemp;
    Display_Info::SendEvent(m_strLog);
    //Display Receive Data
    pDlg->DisplayDataHex(ReadReportBuffer, nBytesRead);
    }

    why nBytesRead value = 64?
    讀進來的DATA有包含USB送的8byte DATA.

    回覆刪除
    回覆
    1. 當然是 64 因為您已經要求 ReadFile
      緩衝區要塞滿 64 的 byte 阿

      Windows 應用層不會去管設備端的韌體怎麼寫
      應用端設定的 Buffer 大小跟 USB 設備韌體無關

      並不是說用了 USB 就可以避開像 RS232 通訊 要收一堆
      然後掃描 Buffer 看看裡面有沒有對的 Pattern

      敝人自己做 USB Bulk IO 也是 Buffer 要設大 多收幾筆
      掃描 Pattern ,敝人通常 PC 端 USB Data Buffer
      會設定的比 韌體準備要傳過來的大 2 倍

      您用 TCP/IP 網路的話也是一樣 Pattern 掃描基本上都要

      因為這類的協定是 無邊界型式 並非像 CAN Bus 可以很準確
      兩邊設定 8 個 Byte 就是收 8 Byte CAN Bus 是 訊息邊界型式的協定

      USB 只有 Control 是 訊息管道 可以像 CANBus 一樣精準收發
      不用做 Pattern 掃描,其它的 串流管道 就沒有訊息邊界囉

      這就是敝人用 HID 喜歡用 Feature Report ,要用 IN OUT 我會
      直接走 Bulk

      ... 參考看看 ~~~

      刪除
  12. 想請問一下,用USB HID有辦直接控制電腦內鍵的蜂鳴器讓它響嗎?

    回覆刪除
  13. 不能直接控制,因為 蜂鳴器屬於 傳統 IO Port 的設備
    當然你可以設計一個 USB HID 設備並且寫支應用程式搭配
    讓應用程式去呼叫 Beep 這個 Windows API

    應用程式可以寫成服務型應用程式也就是進入點是 ServiceMain

    另外一種做法比較難但是比較直接
    寫驅動程式,因為驅動程式在 Ring0 裡面 為你的HID設備寫一支
    HID 過濾驅動 攔截 自己HID設備發來的指令 直接用 IO 指令 控制
    8253 讓 蜂鳴器發聲
    ... 參考看看

    回覆刪除
    回覆
    1. 了解,謝謝您的指教!

      刪除
  14. 請問~ 針對不同的endpoint該如何對hid下指令?
    如:endpoint 4 USB properties https://gyazo.com/bc23eec4683c91c93cbc7dbf0773b857

    回覆刪除