고통 없는 USB 드라이버: libusb로 이론에서 코드까지
USB 장치용 드라이버를 작성하는 일은 저수준 코드의 복잡성 때문에 개발자들을 종종 위축시킵니다. 하지만 libusb와 같은 적절한 도구키트를 사용하면 소켓을 이용한 네트워크 프로그래밍만큼 간단해집니다. 이 글에서는 Android 장치의 bootloader 모드를 예로 들어 USB 장치용 드라이버를 만드는 과정을 안내하겠습니다.
USB 기초: 개발자가 알아야 할 것
USB 사양은 VID(Vendor ID)와 PID(Product ID)를 통해 장치를 식별하는 표준 메커니즘을 정의합니다. 이러한 식별자는 제조사(VID는 USB-IF에서, PID는 제조사 자체에서 부여)에 할당됩니다. 장치가 연결되면 호스트는 디스크립터를 요청하는데, 이는 펌웨어에 있는 바이너리 구조로 장치 클래스, 기능, 필요한 드라이버 정보가 포함되어 있습니다. 핵심: 대부분의 작업은 Winusb.sys(Windows)나 usbfs(Linux) 같은 표준 드라이버를 통해 사용자 공간에서 처리할 수 있으며, 커널 코드는 필요 없습니다.
중요 용어:
- Endpoint: 데이터 전송을 위한 논리 채널(Control, Bulk, Interrupt, Isochronous).
- Device Descriptor: VID/PID와 기본 매개변수를 포함한 루트 구조.
- Device Class: 표준화된 기능(HID, Mass Storage)이지만, 많은 장치가 Vendor Specific Class를 사용합니다.
장치 준비: 선택과 설정
bootloader(fastboot) 모드의 Android 장치를 예로 사용하겠습니다. 이 선택 이유:
- 접근성: 대부분의 스마트폰이 fastboot로 전환 지원.
- 간단한 프로토콜: Fastboot 문서가 공개적이고 최소주의적.
- 사전 설치 드라이버 없음: OS가 상호작용을 가로채지 않음.
Bootloader 모드로 전환하려면 보통 전원 켜는 동안 버튼 조합을 길게 누릅니다(예: 볼륨 업 + 전원). Linux에서 장치 식별은 lsusb로:
$ lsusb
Bus 008 Device 014: ID 18d1:4ee0 Google Inc. Nexus/Pixel Device (fastboot)
여기서 18d1은 Google의 VID, 4ee0은 fastboot의 PID입니다. Windows에서는 USB Device Tree Viewer로 유사 정보 확인. 핵심: OS가 드라이버를 로드하면(예: 장치 관리자에 ⚠️ 표시), Zadig으로 Winusb.sys로 교체해야 합니다.
장치 열거: 수동 분석에서 프로그램 구현까지
연결 시 열거 과정이 자동 시작됩니다. OS는 VID/PID와 장치 클래스를 분석해 드라이버를 선택합니다. 사용자 공간에서는 libusb로 구현합니다. 연결 핸들러 등록 코드:
#include <print>
#include <libusb-1.0/libusb.h>
auto hotplug_callback(
libusb_context *ctx,
libusb_device *device,
libusb_hotplug_event event,
void *user_data
) -> int {
std::println("Device plugged in!");
return 0;
}
auto main() -> int {
libusb_context *context = nullptr;
libusb_init(&context);
libusb_hotplug_callback_handle handle;
libusb_hotplug_register_callback(
context,
LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED,
LIBUSB_HOTPLUG_ENUMERATE,
0x18d1, 0x4ee0,
LIBUSB_HOTPLUG_MATCH_ANY,
hotplug_callback, nullptr,
&handle
);
while (true) {
if (libusb_handle_events(context) < 0) break;
}
libusb_hotplug_deregister_callback(context, handle);
libusb_exit(context);
}
이 코드는 libusb를 초기화하고 VID=0x18d1, PID=0x4ee0 장치의 콜백을 등록한 후 연결을 기다립니다. 성공 시 "Device plugged in!"을 출력합니다. Windows에서는 커널 드라이버 분리 필요할 수 있습니다:
libusb_detach_kernel_driver(handle, 0);
Control Endpoint를 통한 장치 통신
기본 상호작용에는 Control Endpoint(ID 0x00)를 사용하세요. 이는 서비스 요청을 위한 표준 채널입니다. GET_STATUS 요청 구현:
libusb_device_handle *handle = nullptr;
libusb_open(device, &handle);
std::vector<std::uint8_t> data(0xFF);
const auto result = libusb_control_transfer(
handle,
LIBUSB_ENDPOINT_IN | LIBUSB_RECIPIENT_DEVICE | LIBUSB_REQUEST_TYPE_STANDARD,
LIBUSB_REQUEST_GET_STATUS,
0x00, 0x00,
data.data(), data.size(),
1000
);
if (result >= 0) print_bytes(std::span(data).subspan(0, result));
libusb_close(handle);
성공 응답(예: 01 00)은 사양에 따라 디코딩: 첫 번째 바이트—전원 공급(1 = 배터리), 두 번째—원격 웨이크업 지원(0 = 미지원). Control Endpoint는 GET_DESCRIPTOR, SET_CONFIGURATION 같은 다른 표준 요청도 가능하며, 디스크립터 가져오기에 필수입니다.
디스크립터 요청: 장치 이해의 열쇠
디스크립터는 USB 장치 상호작용의 기반입니다. GET_DESCRIPTOR로 device descriptor 요청:
const auto result = libusb_control_transfer(
handle,
LIBUSB_ENDPOINT_IN | LIBUSB_RECIPIENT_DEVICE | LIBUSB_REQUEST_TYPE_STANDARD,
LIBUSB_REQUEST_GET_DESCRIPTOR,
(LIBUSB_DT_DEVICE << 8), // Tip deskriptora
0x00,
data.data(), data.size(),
1000
);
수신 데이터는 libusb_device_descriptor 구조와 일치하며, 다음 필드 포함:
bcdUSB: USB 사양 버전bMaxPacketSize0: Control Endpoint 최대 패킷 크기idVendor/idProduct: VID와 PIDbNumConfigurations: 구성 수
디스크립터 분석으로 지원 인터페이스와 엔드포인트 확인. 구성 디스크립터는 요청 유형을 LIBUSB_DT_CONFIG로 변경하고 wIndex에 구성 인덱스 지정.
핵심 포인트
- 사용자 공간 작업: 커널 코드 대신 libusb 사용—디버깅 쉽고 안전.
- VID/PID를 주요 식별자로: 드라이버 바인딩에 의존; 장치 클래스는 종종 유용하지 않음(Vendor Specific Class).
- Control Endpoint를 기본 도구로: 다른 엔드포인트 설정 전에 디스크립터 가져오고 장치 제어.
- 디스크립터를 진실의 원천으로: USB 사양에 구조 정의; 디코딩으로 장치 기능 전체 파악.
— Editorial Team
아직 댓글이 없습니다.