根據 WinUSB 範本撰寫 Windows 傳統型應用程式
撰寫與 USB 裝置通訊的 Windows 傳統型應用程式最簡單的方式是使用 C/C++ WinUSB 範本。 針對此範本,您需要具有 Windows Driver Kit (WDK 的整合環境,) (搭配適用於 Windows) 和 Microsoft Visual Studio (Professional 或 Ultimate) 的偵錯工具。 您可以使用範本作為起點。
開始之前
- 若要設定集成開發環境,請先安裝 Microsoft Visual Studio Ultimate 2019 或 Microsoft Visual Studio Professional 2019,然後安裝 WDK。 您可以在 WDK 下載頁面上找到如何設定 Visual Studio 和 WDK 的相關信息。
- 當您安裝 WDK 時,會包含適用於 Windows 的偵錯工具。 如需詳細資訊,請參閱 下載及安裝適用於 Windows 的偵錯工具。
建立 WinUSB 應用程式
若要從範本建立應用程式:
在 [ 新增專案] 對話框的頂端搜尋方塊中,輸入 USB。
在中間窗格中,選取 [WinUSB 應用程式] ([通用]) 。
選取 [下一步] 。
輸入專案名稱、選擇儲存位置,然後選取 [ 建立]。
下列螢幕快照顯示 WinUSB 應用程式 (通用) 範本的 [新增專案] 對話方塊。
本主題假設Visual Studio專案名稱是 USB Application1。
Visual Studio 會建立一個專案和方案。 您可以在 [方案總管] 視窗中看到方案、專案和屬於項目的檔案,如下列螢幕快照所示。 (如果看不到 方案總管 視窗,請從 [檢視] 功能表選擇 [方案總管]。) 解決方案包含名為 USB Application1 的 C++ 應用程式專案。
USB Application1 專案具有應用程式的來源檔案。 如果您想要查看應用程式原始程式碼,您可以開啟任何出現在 [原始程式檔] 底下的檔案。
將驅動程式套件專案新增至方案。 選取並按住 (或以滑鼠右鍵按兩下) 解決方案 (解決方案 『USB Application1』) ,然後選取 [ 新增>專案 ],如下列螢幕快照所示。
在 [ 新增專案 ] 對話框的頂端搜尋方塊中,再次輸入 USB。
在中間窗格中,選取 [WinUSB INF 驅動程式套件]。
選取 [下一步] 。
輸入專案名稱,然後選取 [ 建立]。
下列螢幕快照顯示 WinUSB INF 驅動程式套件範本的 [新增專案] 對話框。
本主題假設Visual Studio專案名稱是 USB Application1套件。
USB Application1 套件專案包含 INF 檔案,用來將 Microsoft 提供的 Winusb.sys 驅動程式安裝為設備驅動器。
您的 方案總管 現在應該包含這兩個專案,如下列螢幕快照所示。
在 INF 檔案的 USBApplication1.inf 中,找出下列程式代碼:
%DeviceName% =USB_Install, USB\VID_vvvv&PID_pppp
以裝置的硬體標識碼取代VID_vvvv&PID_pppp。 從裝置管理員 取得硬體標識碼。 在 裝置管理員 中,檢視裝置屬性。 在 [ 詳細數據] 索引 標籤上,檢視 [硬體 標識符] 屬性值。
在 [方案總管] 視窗中,選取並按住 (,或以) 滑鼠右鍵按兩下 [解決方案 'USB Application1'] (2 個專案) ,然後選擇 [Configuration Manager]。 為應用程式專案和封裝項目選擇組態和平臺。 在此練習中,我們選擇 [偵錯] 和 [x64],如下列螢幕快照所示。
建置、部署和偵錯專案
到目前為止,在此練習中,您已使用 Visual Studio 來建立專案。 接下來,您必須設定裝置所連線的裝置。 此範本需要將 Winusb 驅動程式安裝為裝置的驅動程式。
您的測試與偵錯環境可以有:
兩部計算機設定:主計算機和目標計算機。 您可以在主電腦上的 Visual Studio 中開發及建置專案。 調試程式會在主計算機上執行,而且可在 Visual Studio 使用者介面中使用。 當您測試和偵錯應用程式時,驅動程式會在目標計算機上執行。
單一計算機設定:您的目標和主機會在一部計算機上執行。 您可以在 Visual Studio 中開發和建置專案,並執行調試程式和應用程式。
您可以遵循下列步驟來部署、安裝、載入和偵錯您的應用程式和驅動程式:
兩部計算機設定
- 依照布建 計算機以進行驅動程式部署和測試中的指示來布建目標計算機。 注意: 布建會在名為 WDKRemoteUser 的目標電腦上建立使用者。 布建完成後,您會看到使用者切換至 WDKRemoteUser。
- 在主計算機上,在 Visual Studio 中開啟您的方案。
- 在 main.cpp 中,於 OpenDevice 呼叫之前新增這一行。
system ("pause")
此行會導致應用程式在啟動時暫停。 這在遠端偵錯中很有用。
- 在 pch.h 中,包含這一行:
#include <cstdlib>
在上一個步驟中呼叫時,需要
system()
這個 include 語句。在 [方案總管] 視窗中,選取並按住 (,或以滑鼠右鍵按兩下 [) USB Application1 套件],然後選擇 [屬性]。
在 [USB Application1 封裝屬性頁 ] 視窗的左窗格中,流覽至 [ 組態屬性 > 驅動程式安裝 > 部署],如下列螢幕快照所示。
在 部署之前,請檢查移除先前的驅動程式版本。
針對 [遠端電腦名稱],選取您為測試和偵錯設定的計算機名稱。 在此練習中,我們使用名為 dbg-target 的計算機。
選取 [安裝/重新安裝並驗證]。 選取 [套用]。
在屬性頁面中,流覽至 [ 組態屬性 > 偵錯],然後選取 [Windows – 遠端調試程序偵錯工具],如下列螢幕快照所示。
從 [組建] 功能表中選取 [組建解決方案]。 Visual Studio 會在 [ 輸出 ] 視窗中顯示建置進度。 (如果看不到 [輸出] 視窗,請選擇 [檢視] 功能表的 [輸出]。) 在此練習中,我們已針對執行 Windows 10 的 x64 系統建置專案。
從 [建置] 功能選取 [部署方案]。
在目標電腦上,您會看到驅動程式安裝腳本正在執行。 驅動程式檔案會複製到目標計算機上的 %Systemdrive%\drivertest\drivers 資料夾。 確認 .inf、.cat、測試憑證和 .sys 檔案,以及任何其他必要檔案都存在 %systemdrive%\drivertest\drivers 資料夾。 裝置必須出現在 裝置管理員 中,而不會發生錯誤。
在主電腦上,您會在 [ 輸出 ] 視窗中看到此訊息。
Deploying driver files for project
"<path>\visual studio 14\Projects\USB Application1\USB Application1 Package\USB Application1 Package.vcxproj".
Deployment may take a few minutes...
========== Build: 1 succeeded, 0 failed, 1 up-to-date, 0 skipped ==========
若要進行應用程式偵錯
在主計算機上,流覽至方案資料夾中的 x64 > Win8.1Debug 。
將應用程式可執行檔 UsbApplication1.exe 複製到目標計算機。
在目標電腦上啟動應用程式。
在主計算機上,從 [ 偵 錯] 功能表中,選取 [ 附加至進程]。
在視窗中,選取 [Windows 使用者模式調試程式 ] ([適用於 Windows 的偵錯工具]) 作為傳輸和目標計算機的名稱,在此案例中為 dbg-target,如此影像所示的限定符。
從 [可用的進程 ] 列表中選取應用程式,然後選取 [ 附加]。 您現在可以使用 [即時運算視窗 ] 或 [偵錯] 選單中的選項進行 偵 錯。
上述指示會使用 適用於 Windows 的偵錯工具 – 遠端調試程式來偵錯應用程式。 如果您想要使用 遠端 Windows 調試程式 (Visual Studio 隨附的調試程式) ,請遵循下列指示:
- 在目標電腦上,將 msvsmon.exe 新增至允許透過防火牆的應用程式清單。
- 啟動位於 C:\DriverTest\msvsmon\msvsmon.exe 的Visual Studio遠端偵錯監視器。
- 建立工作資料夾,例如 C:\remotetemp。
- 將應用程式可執行檔 UsbApplication1.exe 複製到目標電腦上的工作資料夾。
- 在主計算機上的 Visual Studio 中,以滑鼠右鍵按兩下 USB Application1 套件 專案,然後選取 [ 卸除專案]。
- 選取並按住 (或以滑鼠右鍵按兩下) USB Application1 專案,在專案屬性中展開 [ 組態屬性 ] 節點,然後選取 [ 偵錯]。
- 將 [調試程式] 變更為 [遠端 Windows 調試程式]。
- 依照在本機 建置之專案的遠端偵錯中提供的指示,變更專案設定以在遠端電腦上執行可執行檔。 請確定 工作目錄 和 遠端命令 屬性會反映目標電腦上的資料夾。
- 若要對應用程式進行偵錯,請在 [ 建 置] 功能選取 [ 開始偵錯],或按 F5。
單一電腦設定:
若要建置您的應用程式和驅動程式安裝套件,請從 [建置] 功能表選擇 [建置方案]。 Visual Studio 會在 [ 輸出 ] 視窗中顯示建置進度。 (如果看不到 [輸出] 視窗,請從 [檢視] 功能表選擇 [輸出]。) 在此練習中,我們已針對執行 Windows 10 的 x64 系統建置專案。
若要查看建置的驅動程式套件,請在 Windows 檔案總管中流覽至 USB Application1 資料夾,然後流覽至 x64 > 偵 > 錯 USB Application1 套件。 驅動程式套件包含數個檔案:MyDriver.inf 是 Windows 安裝驅動程式時所使用的資訊檔案,mydriver.cat 是安裝程式用來驗證驅動程式套件測試簽章的類別目錄檔案。 這些檔案會顯示在下列螢幕快照中。
套件中未包含任何驅動程序檔案。 這是因為 INF 檔案參考 Windows\System32 資料夾中的內建驅動程式 Winusb.sys。
手動安裝驅動程式。 在 裝置管理員 中,藉由在封裝中指定 INF 來更新驅動程式。 指向位於方案資料夾中的驅動程式套件,如上一節所示。 如果您看到錯誤
DriverVer set to a date in the future
,請設定 INF 套件項目設定 > Inf2Cat > 一般 > 使用本地時間 > 是。選取並按住 (或以滑鼠右鍵按兩下) USB Application1 專案,在專案屬性中展開 [ 組態屬性 ] 節點,然後選取 [ 偵錯]。
將 [調試程式] 變更為 [本機 Windows 調試程式]。
選取並按住 (或以滑鼠右鍵按兩下 [USB Application1 套件] 專案) ,然後選取 [ 卸除專案]。
若要對應用程式進行偵錯,請在 [ 建 置] 功能選取 [ 開始偵錯],或按 F5。
範本程式代碼討論
範本是傳統型應用程式的起點。 USB Application1 專案有來源檔案 device.cpp 和 main.cpp。
main.cpp 檔案包含應用程式進入點,_tmain。 device.cpp 包含開啟和關閉裝置句柄的所有協助程式函式。
範本也有名為 device.h 的頭檔。 此檔案包含裝置介面 GUID 的定義, (稍後討論) 和儲存應用程式所取得資訊的DEVICE_DATA結構。 例如,它會儲存 OpenDevice 取得並用於後續作業的 WinUSB 介面句柄。
typedef struct _DEVICE_DATA {
BOOL HandlesOpen;
WINUSB_INTERFACE_HANDLE WinusbHandle;
HANDLE DeviceHandle;
TCHAR DevicePath[MAX_PATH];
} DEVICE_DATA, *PDEVICE_DATA;
取得裝置的實例路徑 - 請參閱 device.cpp 中的 RetrieveDevicePath
若要存取 USB 裝置,應用程式會呼叫 CreateFile,為裝置建立有效的檔案句柄。 針對該呼叫,應用程式必須取得裝置路徑實例。 若要取得裝置路徑,應用程式會使用 SetupAPI 例程,並指定 INF 檔案中用來安裝 Winusb.sys 的裝置介面 GUID。 Device.h 會宣告名為 GUID_DEVINTERFACE_USBApplication1 的 GUID 常數。 藉由使用這些例程,應用程式會列舉指定裝置介面類別中的所有裝置,並擷取裝置的裝置路徑。
HRESULT
RetrieveDevicePath(
_Out_bytecap_(BufLen) LPTSTR DevicePath,
_In_ ULONG BufLen,
_Out_opt_ PBOOL FailureDeviceNotFound
)
/*++
Routine description:
Retrieve the device path that can be used to open the WinUSB-based device.
If multiple devices have the same device interface GUID, there is no
guarantee of which one will be returned.
Arguments:
DevicePath - On successful return, the path of the device (use with CreateFile).
BufLen - The size of DevicePath's buffer, in bytes
FailureDeviceNotFound - TRUE when failure is returned due to no devices
found with the correct device interface (device not connected, driver
not installed, or device is disabled in Device Manager); FALSE
otherwise.
Return value:
HRESULT
--*/
{
BOOL bResult = FALSE;
HDEVINFO deviceInfo;
SP_DEVICE_INTERFACE_DATA interfaceData;
PSP_DEVICE_INTERFACE_DETAIL_DATA detailData = NULL;
ULONG length;
ULONG requiredLength=0;
HRESULT hr;
if (NULL != FailureDeviceNotFound) {
*FailureDeviceNotFound = FALSE;
}
//
// Enumerate all devices exposing the interface
//
deviceInfo = SetupDiGetClassDevs(&GUID_DEVINTERFACE_USBApplication1,
NULL,
NULL,
DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
if (deviceInfo == INVALID_HANDLE_VALUE) {
hr = HRESULT_FROM_WIN32(GetLastError());
return hr;
}
interfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
//
// Get the first interface (index 0) in the result set
//
bResult = SetupDiEnumDeviceInterfaces(deviceInfo,
NULL,
&GUID_DEVINTERFACE_USBApplication1,
0,
&interfaceData);
if (FALSE == bResult) {
//
// We would see this error if no devices were found
//
if (ERROR_NO_MORE_ITEMS == GetLastError() &&
NULL != FailureDeviceNotFound) {
*FailureDeviceNotFound = TRUE;
}
hr = HRESULT_FROM_WIN32(GetLastError());
SetupDiDestroyDeviceInfoList(deviceInfo);
return hr;
}
//
// Get the size of the path string
// We expect to get a failure with insufficient buffer
//
bResult = SetupDiGetDeviceInterfaceDetail(deviceInfo,
&interfaceData,
NULL,
0,
&requiredLength,
NULL);
if (FALSE == bResult && ERROR_INSUFFICIENT_BUFFER != GetLastError()) {
hr = HRESULT_FROM_WIN32(GetLastError());
SetupDiDestroyDeviceInfoList(deviceInfo);
return hr;
}
//
// Allocate temporary space for SetupDi structure
//
detailData = (PSP_DEVICE_INTERFACE_DETAIL_DATA)
LocalAlloc(LMEM_FIXED, requiredLength);
if (NULL == detailData)
{
hr = E_OUTOFMEMORY;
SetupDiDestroyDeviceInfoList(deviceInfo);
return hr;
}
detailData->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
length = requiredLength;
//
// Get the interface's path string
//
bResult = SetupDiGetDeviceInterfaceDetail(deviceInfo,
&interfaceData,
detailData,
length,
&requiredLength,
NULL);
if(FALSE == bResult)
{
hr = HRESULT_FROM_WIN32(GetLastError());
LocalFree(detailData);
SetupDiDestroyDeviceInfoList(deviceInfo);
return hr;
}
//
// Give path to the caller. SetupDiGetDeviceInterfaceDetail ensured
// DevicePath is NULL-terminated.
//
hr = StringCbCopy(DevicePath,
BufLen,
detailData->DevicePath);
LocalFree(detailData);
SetupDiDestroyDeviceInfoList(deviceInfo);
return hr;
}
在上述函式中,應用程式會藉由呼叫下列例程來取得裝置路徑:
SetupDiGetClassDevs 可取得 裝置資訊集的句柄、陣列,其中包含符合指定裝置介面類別之所有已安裝裝置的相關信息,GUID_DEVINTERFACE_USBApplication1。 陣列中稱為 裝置介面 的每個元素都會對應至已安裝並向系統註冊的裝置。 裝置介面類別是藉由傳遞您在 INF 檔案中定義的裝置介面 GUID 來識別。 函式會將 HDEVINFO 句柄傳回給裝置資訊集。
SetupDiEnumDeviceInterfaces 可列舉裝置資訊集中的裝置介面,並取得裝置介面的相關信息。
此呼叫需要下列專案:
初始化的呼叫端配置 SP_DEVICE_INTERFACE_DATA 結構,其 cbSize 成員設定為 結構的大小。
步驟 1 的 HDEVINFO 句柄。
您在 INF 檔案中定義的裝置介面 GUID。
SetupDiEnumDeviceInterfaces 會查閱裝置資訊集陣列,以取得裝置介面的指定索引,並使用介面的基本數據填入初始化 的SP_DEVICE_INTERFACE_DATA 結構。
若要列舉裝置資訊集中的所有裝置介面,請在迴圈中呼叫 SetupDiEnumDeviceInterfaces ,直到函式傳回 FALSE ,並ERROR_NO_MORE_ITEMS失敗的錯誤碼。 呼叫 GetLastError 即可擷取ERROR_NO_MORE_ITEMS錯誤碼。 每次反覆運算時,遞增成員索引。
或者,您也可以呼叫 SetupDiEnumDeviceInfo 來列舉裝置資訊集,並在呼叫者配置的 SP_DEVINFO_DATA 結構中傳回裝置介面元素的相關信息。 接著,您可以在 SetupDiEnumDeviceInterfaces 函式的 DeviceInfoData 參數中傳遞這個結構的參考。
SetupDiGetDeviceInterfaceDetail 以取得裝置介面的詳細數據。 資訊會在 SP_DEVICE_INTERFACE_DETAIL_DATA 結構中傳回。 因為 SP_DEVICE_INTERFACE_DETAIL_DATA 結構的大小不同, 所以會呼叫 SetupDiGetDeviceInterfaceDetail 兩次。 第一次呼叫會取得要配置給 SP_DEVICE_INTERFACE_DETAIL_DATA 結構的緩衝區大小。 第二個呼叫會填滿配置緩衝區,其中包含介面的詳細資訊。
- 呼叫 SetupDiGetDeviceInterfaceDetail ,並將 DeviceInterfaceDetailData 參數設定為 NULL。 函式會在 requiredlength 參數中傳回正確的緩衝區大小。 此呼叫失敗,並出現錯誤碼ERROR_INSUFFICIENT_BUFFER。 這是預期的錯誤碼。
- 根據在 requiredlength 參數中擷取的正確緩衝區大小,為SP_DEVICE_INTERFACE_DETAIL_DATA結構配置記憶體。
- 再次呼叫 SetupDiGetDeviceInterfaceDetail ,並將參考傳遞給 DeviceInterfaceDetailData 參數中的初始化結構。 當函式傳回時,結構會填入介面的詳細資訊。 裝置路徑位於 SP_DEVICE_INTERFACE_DETAIL_DATA 結構的 DevicePath 成員中。
建立裝置的檔案句柄
請參閱 device.cpp 中的 OpenDevice。
若要與裝置互動,需要 WinUSB 介面句柄,才能處理裝置上第一個 (預設) 介面。 範本程式代碼會取得檔句柄和 WinUSB 介面句柄,並將其儲存在DEVICE_DATA結構中。
HRESULT
OpenDevice(
_Out_ PDEVICE_DATA DeviceData,
_Out_opt_ PBOOL FailureDeviceNotFound
)
/*++
Routine description:
Open all needed handles to interact with the device.
If the device has multiple USB interfaces, this function grants access to
only the first interface.
If multiple devices have the same device interface GUID, there is no
guarantee of which one will be returned.
Arguments:
DeviceData - Struct filled in by this function. The caller should use the
WinusbHandle to interact with the device, and must pass the struct to
CloseDevice when finished.
FailureDeviceNotFound - TRUE when failure is returned due to no devices
found with the correct device interface (device not connected, driver
not installed, or device is disabled in Device Manager); FALSE
otherwise.
Return value:
HRESULT
--*/
{
HRESULT hr = S_OK;
BOOL bResult;
DeviceData->HandlesOpen = FALSE;
hr = RetrieveDevicePath(DeviceData->DevicePath,
sizeof(DeviceData->DevicePath),
FailureDeviceNotFound);
if (FAILED(hr)) {
return hr;
}
DeviceData->DeviceHandle = CreateFile(DeviceData->DevicePath,
GENERIC_WRITE | GENERIC_READ,
FILE_SHARE_WRITE | FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
NULL);
if (INVALID_HANDLE_VALUE == DeviceData->DeviceHandle) {
hr = HRESULT_FROM_WIN32(GetLastError());
return hr;
}
bResult = WinUsb_Initialize(DeviceData->DeviceHandle,
&DeviceData->WinusbHandle);
if (FALSE == bResult) {
hr = HRESULT_FROM_WIN32(GetLastError());
CloseHandle(DeviceData->DeviceHandle);
return hr;
}
DeviceData->HandlesOpen = TRUE;
return hr;
}
- 應用程式會呼叫 CreateFile ,藉由指定稍早擷取的裝置路徑來建立裝置的檔案句柄。 它會使用 FILE_FLAG_OVERLAPPED 旗標,因為 WinUSB 相依於此設定。
- 藉由使用裝置的檔案句柄,應用程式會建立 WinUSB 介面句柄。 WinUSB 函式 會使用此句柄來識別目標裝置,而不是檔案句柄。 若要取得 WinUSB 介面句柄,應用程式會藉由傳遞檔句柄 來呼叫 WinUsb_Initialize 。 使用後續呼叫中收到的句柄,從裝置取得資訊,以及將 I/O 要求傳送至裝置。
釋放裝置句柄 - 請參閱 device.cpp 中的 CloseDevice
範本程式代碼會實作程序代碼,以釋放裝置的檔案句柄和 WinUSB 介面句柄。
- CloseHandle 以釋放 CreateFile 所建立的句柄,如本逐步解說 之裝置的建立檔案句柄 一節所述。
- WinUsb_Free 釋放裝置的 WinUSB 介面句柄, WinUsb_Initialize 所傳 回。
VOID
CloseDevice(
_Inout_ PDEVICE_DATA DeviceData
)
/*++
Routine description:
Perform required cleanup when the device is no longer needed.
If OpenDevice failed, do nothing.
Arguments:
DeviceData - Struct filled in by OpenDevice
Return value:
None
--*/
{
if (FALSE == DeviceData->HandlesOpen) {
//
// Called on an uninitialized DeviceData
//
return;
}
WinUsb_Free(DeviceData->WinusbHandle);
CloseHandle(DeviceData->DeviceHandle);
DeviceData->HandlesOpen = FALSE;
return;
}
下一步
接下來,請閱讀下列主題,以傳送取得裝置資訊,並將數據傳輸傳送至裝置:
-
瞭解如何查詢裝置以取得 USB 特定資訊,例如裝置速度、介面描述元、相關端點及其管道。
-
從 USB 裝置的時序端點來回傳輸數據。