撰寫與 USB 裝置通訊的 Windows 傳統型應用程式,最簡單的方式是使用 C/C++ WinUSB 範本。 針對此範本,您需要具有 Windows 驅動程式套件 (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 應用程式1',然後選取 新增>新專案,如下列螢幕快照所示。
在 [新增專案] 對話方塊中,於頂端的搜尋方塊中,再次輸入 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 應用程式1][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、test cert 和 .sys 檔案,以及任何其他必要檔案都存在於 \drivertest\drivers 資料夾 %systemdrive%。 裝置必須出現在設備管理器中,而不會發生錯誤。
在主電腦上,您會在 [輸出] 視窗中看到此訊息。
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
,請 > Inf2Cat > 一般 > [使用當地時間] > [是] 設定INF 套件項目設定。選取並按住(或以滑鼠右鍵按下)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 例程,並在用來安裝 Winusb.sys的 INF 檔案中指定裝置介面 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錯誤碼。 每次反覆運算時,都會遞增成員索引。
或者,您可以在呼叫端配置的 SP_DEVINFO_DATA 結構中,呼叫 SetupDiEnumDeviceInfo 列舉裝置資訊集,並傳回由索引指定的裝置介面元素相關信息。 接著,您可以在 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 要求傳送至裝置。
釋放設備句柄 - 請參閱 CloseDevice 在 device.cpp 中
樣板程式碼會實作程式碼以釋放裝置的檔案控制碼和 WinUSB 介面控制碼。
- CloseHandle 釋放 CreateFile所建立的句柄,如本逐步解說 建立裝置 檔案句柄一節中所述。
- zh-TW: WinUsb_Free 用於釋放由 WinUsb_Initialize 所傳回的裝置 WinUSB 介面句柄。
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;
}
後續步驟
接下來,請閱讀下列主題,以取得並傳送裝置資訊,並進行數據傳輸至裝置:
使用 WinUSB 函式 存取 USB 裝置
瞭解如何查詢裝置以取得 USB 特定資訊,例如裝置速度、介面描述元、相關端點及其管道。
-
從 USB 裝置的不時針端點來回傳輸數據。