【转】簡單講講 USB Human Interface Device

原地址http://213style.blogspot.com/2013/09/usb-human-interface-device.html

恩,發本文的原因是看到了以前畢業的朋友在旁邊的對話框問了一些問題,我想這些問題

 
不是三言兩語可以解釋完畢,但是我也不想又太細究 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 Transfer 與 Interrupt 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 訊息。

转载于:https://www.cnblogs.com/libra13179/p/7259007.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值