Windows 환경에서의 USB HID 통신 클래스 설계 및 구현

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)

장치 연결 및 데이터 송수신은 CreateFileReadFile/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;
}

위 구현에서 SetupDiGetClassDevsHidD_GetAttributes 함수를 통해 시스템에 연결된 장치 중 특정 VID/PID를 가진 장치를 필터링하여 찾아내고, 장치 경로(Device Path)를 확보한 후 CreateFile을 통해 통신 핸들을 얻는 과정이 핵심입니다. 비동기 처리를 위해 FILE_FLAG_OVERLAPPED 플래그를 사용하여 읽기/쓰기 작업을 수행함으로써 메인 스레드의 블로킹을 방지합니다.

태그: USB HID WindowsAPI Win32 C++

6월 8일 17:38에 게시됨