CH372와 같은 USB 컨트롤러를 활용하여 장치를 제어할 때, 기본 펌웨어 모드 외에 벤더가 제공하는 DLL에 의존하지 않는 '외장 펌웨어 모드'를 사용할 수 있습니다. 이 방식은 Windows DDK(Driver Development Kit)에서 제공하는 표준 API를 사용하여 상위 애플리케이션을 직접 구현할 수 있다는 장점이 있습니다.
HID 통신 클래스 구조
USB HID 장치와의 상호작용을 캡슐화한 UsbHidManager 클래스는 비동기 I/O를 활용하여 데이터 수신을 위한 별도의 워커 스레드를 관리합니다.
헤더 파일 (UsbHidManager.h)
#pragma once
#include <windows.h>
#include <setupapi.h>
#include <hidsdi.h>
#pragma comment(lib, "hid.lib")
#pragma comment(lib, "setupapi.lib")
class UsbHidManager {
public:
UsbHidManager();
~UsbHidManager();
BOOL Initialize(HWND hOwner, DWORD vid, DWORD pid, UINT msgRecv, UINT msgConnect);
void Release();
UINT SendData(const BYTE* data, UINT length);
UINT GetData(BYTE* buffer);
private:
static DWORD WINAPI ListenThread(LPVOID param);
BOOL SetupDeviceInterface(DWORD vid, DWORD pid);
HWND m_hOwner;
HANDLE m_hRead, m_hWrite;
HANDLE m_hExitEvent;
OVERLAPPED m_ovRead, m_ovWrite;
UINT m_msgRecv, m_msgConnect;
HIDP_CAPS m_caps;
};
구현 핵심 로직 (UsbHidManager.cpp)
장치 연결 및 데이터 송수신은 CreateFile과 ReadFile/WriteFile을 사용합니다. HID 보고서는 일반적으로 첫 번째 바이트가 리포트 ID(0)이므로 데이터를 전송할 때 이를 고려해야 합니다.
UINT UsbHidManager::SendData(const BYTE* data, UINT length) {
if (!m_hWrite || length > 64) return 0;
BYTE report[65] = { 0 }; // 첫 바이트는 0으로 유지
memcpy(&report[1], data, length);
DWORD bytesWritten = 0;
if (WriteFile(m_hWrite, report, m_caps.OutputReportByteLength, NULL, &m_ovWrite)) {
WaitForSingleObject(m_ovWrite.hEvent, 2000);
GetOverlappedResult(m_hWrite, &m_ovWrite, &bytesWritten, TRUE);
}
return bytesWritten - 1;
}
DWORD WINAPI UsbHidManager::ListenThread(LPVOID param) {
UsbHidManager* pInstance = (UsbHidManager*)param;
BYTE buffer[65];
while (WaitForSingleObject(pInstance->m_hExitEvent, 0) == WAIT_TIMEOUT) {
DWORD bytesRead = 0;
if (ReadFile(pInstance->m_hRead, buffer, pInstance->m_caps.InputReportByteLength, NULL, &pInstance->m_ovRead)) {
WaitForSingleObject(pInstance->m_ovRead.hEvent, INFINITE);
GetOverlappedResult(pInstance->m_hRead, &pInstance->m_ovRead, &bytesRead, TRUE);
if (bytesRead > 1) {
// 수신된 데이터를 애플리케이션으로 통지
::PostMessage(pInstance->m_hOwner, pInstance->m_msgRecv, 0, 0);
}
}
}
return 0;
}
위 구현에서 SetupDiGetClassDevs와 HidD_GetAttributes 함수를 통해 시스템에 연결된 장치 중 특정 VID/PID를 가진 장치를 필터링하여 찾아내고, 장치 경로(Device Path)를 확보한 후 CreateFile을 통해 통신 핸들을 얻는 과정이 핵심입니다. 비동기 처리를 위해 FILE_FLAG_OVERLAPPED 플래그를 사용하여 읽기/쓰기 작업을 수행함으로써 메인 스레드의 블로킹을 방지합니다.