소프트웨어 개발자를 위한 USB: 사용자 공간 USB 드라이버 작성 입문
(werwolv.net)- USB 드라이버 개발은 커널 수준의 작업으로 여겨지지만, 실제로는 소켓 프로그래밍과 유사한 난이도로 사용자 공간에서도 구현 가능함
- libusb를 사용하면 커널 코드를 작성하지 않고도 장치 열거, 제어 전송, 데이터 송수신을 모두 수행할 수 있음
- USB 통신은 Control, Bulk, Interrupt, Isochronous 네 가지 전송형과 IN/OUT 방향으로 구성되며, 각 엔드포인트는 단방향 통로로 동작함
- Android 기기의 Fastboot 프로토콜을 예시로, Bulk 엔드포인트를 통해 명령과 응답을 주고받는 과정을 코드로 시연함
- 사용자 공간에서도 완전한 USB 드라이버를 구현할 수 있으며, 모든 USB 프로토콜은 동일한 기본 구조를 공유함
소개
- USB 장치용 드라이버는 커널 코드를 다뤄야 한다는 인식 때문에 어렵게 느껴지지만, 실제로는 소켓을 사용하는 애플리케이션 수준의 복잡도임
- 하드웨어 경험이 많지 않은 개발자도 사용자 공간에서 USB를 다루는 방법을 익힐 수 있음
- USB의 세부 동작을 다루는 자료가 존재하지만 초보자에게는 접근이 어려움
- USB 사용에는 임베디드 시스템 수준의 지식이 필요하지 않으며, 네트워크 소켓처럼 접근 가능함
USB 장치
- 예제로 부트로더 모드의 Android 스마트폰을 사용
- 쉽게 구할 수 있고, 프로토콜이 단순하며, OS에 기본 드라이버가 없어 실험에 적합함
- 부트로더 모드 진입은 기기마다 다르며, 일반적으로 전원 버튼과 볼륨 버튼 조합으로 가능함
장치 수동 열거
- 열거(Enumeration) 는 호스트가 장치 정보를 요청해 자신을 식별하는 과정으로, 장치 연결 시 자동 수행됨
- 표준 장치는 USB 클래스를 기반으로 드라이버를 자동 로드하고, 벤더 전용 장치는
VID(Vendor ID)와PID(Product ID)를 사용함 - Linux에서는
lsusb명령으로 장치 정보를 확인 가능- 예시:
ID 18d1:4ee0 Google Inc. Nexus/Pixel Device (fastboot) -
18d1은 Google의 VID,4ee0은 Nexus/Pixel 부트로더의 PID
- 예시:
-
lsusb -t명령으로 클래스와 드라이버 상태를 확인 가능-
Class=Vendor Specific Class,Driver=[none]으로 표시되어 OS가 드라이버를 로드하지 않음
-
- Windows에서는 Device Manager나 USB Device Tree Viewer로 동일한 정보 확인 가능
libusb로 장치 열거
- libusb 라이브러리를 사용하면 커널 코드를 작성하지 않고 사용자 공간에서 USB 장치와 통신 가능
-
libusb_hotplug_register_callback()으로 특정VID:PID조합의 장치가 연결될 때 콜백을 실행하도록 설정 - 프로그램 실행 후 장치 연결 시
"Device plugged in!"메시지 출력 - Linux에서는 기본적으로 작동하며, 필요 시
libusb_detach_kernel_driver()로 커널 드라이버를 분리 가능 - Windows에서는
Winusb.sys드라이버가 필요하며, 없을 경우 Zadig 도구로 수동 교체 가능
장치와의 통신
- USB 장치와의 첫 통신은 Control 엔드포인트(주소 0x00) 를 통해 수행
-
libusb_control_transfer()로 표준 요청(GET_STATUS) 을 전송해 장치 상태를 읽음- 예시 응답:
01 00→ 첫 바이트는 Self-Powered, 두 번째는 Remote Wakeup 미지원
- 예시 응답:
- 이후 GET_DESCRIPTOR 요청으로 장치 디스크립터를 가져올 수 있음
- 반환된 데이터에는
idVendor,idProduct,bDeviceClass등 장치 정보가 포함됨
- 반환된 데이터에는
-
lsusb -v명령으로 모든 디스크립터(장치, 구성, 인터페이스, 엔드포인트 등)를 상세히 확인 가능- 예시:
Android Fastboot인터페이스에 Bulk IN(0x81), Bulk OUT(0x02) 엔드포인트 존재
- 예시:
엔드포인트
- 엔드포인트는 네트워크 포트와 유사한 개념으로, 장치가 데이터를 송수신하는 통로
- 디스크립터에 각 엔드포인트의 종류와 방향이 정의되어 있음
-
Control 전송형
- 모든 장치에 하나 존재하며 주소는 항상
0x00 - 초기 설정 및 장치 정보 요청에 사용
- 인터페이스에 속하지 않고 장치 자체의 일부로 존재
- 모든 장치에 하나 존재하며 주소는 항상
-
Bulk 전송형
- 대용량 비실시간 데이터 전송에 사용
- 예: Mass Storage, CDC-ACM(시리얼), RNDIS(이더넷)
- 대역폭은 높지만 우선순위는 낮음
-
Interrupt 전송형
- 소량의 저지연 데이터 전송에 사용
- 키보드, 마우스 등에서 버튼 입력을 빠르게 폴링
- 실제 하드웨어 인터럽트는 아니며, 호스트가 주기적으로 요청함
-
Isochronous 전송형
- 시간 민감한 대용량 데이터(오디오, 비디오 스트리밍)에 사용
- 지연이 발생하면 품질 저하가 즉시 드러남
- libusb에서는 비동기 방식으로 처리
-
IN / OUT 방향
- USB는 호스트 중심 구조로, 장치는 요청을 받기 전에는 데이터를 전송하지 않음
-
IN: 호스트가 데이터를 받는 방향 -
OUT: 호스트가 데이터를 보내는 방향 - 엔드포인트 주소의 최상위 비트(MSB)가
1이면 IN,0이면 OUT - 최대 127개의 사용자 정의 엔드포인트 사용 가능 (
0x00은 Control 전용) - 엔드포인트는 단방향이며, Fastboot 인터페이스처럼 IN/OUT 쌍으로 구성됨
Fastboot 프로토콜
-
Fastboot는 Android 부트로더 통신 프로토콜로, 명령 문자열을 보내고 4바이트 상태 코드와 데이터를 받는 구조
- 예:
-
Host: "getvar:version"→Client: "OKAY0.4" -
Host: "getvar:nonexistant"→Client: "OKAY"
-
- 예:
- libusb를 이용해 Fastboot 명령을 전송하는 코드 예시
- 인터페이스 0을
libusb_claim_interface()로 확보 -
"getvar:version"명령을 Bulk OUT(0x02) 엔드포인트로 전송 - Bulk IN(0x81) 엔드포인트로 응답 수신
- 출력 예시:
Request: getvar:version Response: OKAY0.4 -
OKAY는 성공 상태,0.4는 Fastboot 버전
- 인터페이스 0을
마무리
- 커널 코드를 작성하지 않고 사용자 공간에서 완전한 USB 드라이버를 구현 가능
- 모든 USB 드라이버는 동일한 기본 원리를 따르며, 프로토콜만 다름
- 복잡한 프로토콜(MTP 등)도 기본 구조는 동일하며, 소켓 통신과 유사한 개념으로 접근 가능함
Hacker News 의견들
-
마침 타이밍이 완벽했음. 곧 MOTU MIDI Express XT를 지역 Guitar Center에서 받을 예정임
중고 장비라 법적으로 일정 기간 보류해야 해서 기다리는 중임. 문제는 이 장비가 표준 MIDI-over-USB가 아니라 독자 프로토콜을 써서, Linux나 OpenBSD, Haiku 같은 내 시스템에서는 USB로 바로 쓸 수 없다는 점임
당장은 단순히 신스 모듈과 컨트롤러 간 라우팅만 필요하니 괜찮지만, PC 쪽에서도 작동하게 만들면 좋겠음
기존 Linux 드라이버가 있긴 한데, 안정성도 불확실하고 XT 지원 여부도 애매함. 커널 패닉 문제는 해결됐다지만 이슈가 남아 있음
그래서 LibUSB 기반 사용자 공간 드라이버를 직접 만들어보려 함. MIDI 포트를 노출하고 라우팅 툴링을 추가하면 꽤 유용할 듯함- Guitar Center의 대기 기간은 단순히 도난 여부 확인 때문만은 아님. 법적으로 전당포(pawn shop) 처럼 일정 기간 판매 금지 의무가 있어서, 원 소유자가 되찾을 수 있는 기간이 지나야 판매 가능함
- 나도 같은 장비를 써서 그 드라이버를 AUR에 패키징했음. 바이너리 블롭은 작동 안 했지만, 단순 MIDI 라우터로 쓰기엔 충분함
-
Go 언어로 이런 걸 해보고 싶다면, cgo 없이 USB 접근이 가능한 go-usb 라이브러리를 만들어둠
나는 이걸로 UVC 장치를 다루는 go-uvc도 개발했음- Rust에서는 nusb를 추천함
-
나도 최근 Macbook M3에서 usbip 시스템을 비슷한 방식으로 구현 중임
다만 최신 macOS에서는 제한이 있음. 시스템이 인식하는 USB 장치에 대해선 libusb 기반 사용자 공간 드라이버를 빌드할 수 없고, 보안 기능을 수동으로 꺼야만 가능함- 드라이버 오버라이드는 한 계층만 조정하면 되므로 완화 가능함
-
이 접근법은 결국 USB 드라이버가 애플리케이션 코드 역할도 하는 셈임. 즉, 드라이버라기보다 라이브러리+프로그램에 가까움
예를 들어 USB-Ethernet 장치를 OS의 네트워크 어댑터로 연결하려면 어떻게 해야 할지 궁금함- 표준화된 장치는 보통 USB/CDC/ECM이나 RNDIS를 써서 자동 인식됨. 사용자 공간 접근은 오히려 비표준 장치에 유용함. Windows에서는 드라이버 서명 없이 libusb로 이식성 있게 구현 가능함
- Linux에서는 tun/tap 장치를 만들어 사용자 공간에서 커널과 통신하거나, 다른 서브시스템도 사용자 공간에서 돌려야 함
-
이 글을 몇 년 전에 읽었더라면 노트북 기능을 리버스 엔지니어링할 때 훨씬 쉬웠을 것임. 특히 키보드 LED 제어 프로그램은 지금도 내가 가장 좋아하는 프로젝트 중 하나임
-
정말 유용한 입문서였음. 저수준 하드웨어 API를 다루는 건 어렵지만 보람 있음. 현대 OS의 추상화 계층 덕분에 쉬워졌지만, 그 아래를 이해하는 건 여전히 중요함
-
C++ 코드가 이상해 보였음. 화살표 문자를 직접 입력할 수 있는 키보드는 본 적이 없음
- 그건 프로그래밍 폰트의 합자(ligature) 임. 복사하면 실제로는
->로 보임. 최신 C++의 trailing return type 문법임 - 일부 개발자는 합자 폰트를 선호함. 두 문자를 하나의 글리프로 합쳐줌
- Compose 키를 설정하면 어떤 키보드로도 “→” 입력 가능함
- 결국 그냥
"->"임. 폰트가 그걸 화살표로 렌더링할 뿐임
- 그건 프로그래밍 폰트의 합자(ligature) 임. 복사하면 실제로는
-
USB 장치가 DMA를 지원하는지 궁금했음. 호스트를 통해서만 가능한지, 아니면 장치가 직접 메모리에 접근하는지도 알고 싶음
- USB 장치는 PCIe나 FireWire처럼 호스트 메모리에 직접 접근하지 않음. 대신 XHCI 컨트롤러가 DMA를 수행하고, 대부분의 장치 컨트롤러는 자체 RAM과 USB 간 DMA를 지원함
- 모든 전송은 호스트가 주도함. 장치가 먼저 데이터를 보내는 것처럼 보여도 실제로는 호스트가 요청함. 직접 DMA는 보안상 큰 위험 요소가 됨
-
예전에 간단한 USB 장치를 만들려 했는데, 디스크립터(descriptor) 작성법에 대한 정보가 거의 없었음. 대부분 “비슷한 장치 찾아서 복사하고 수정해보라”는 식이었음. USB가 정말 훌륭한 표준인지 의문이었음
- 나도 디스크립터가 신비로웠는데, 결국 고정된 바이너리 구조체라는 걸 깨달음. 각 USB 클래스가 명시한 필드와 엔드포인트만 맞추면 인식됨
- USB는 괜찮지만, 전기적 측면에서 USB 1/2는 진정한 차동 신호가 아님
- 튜토리얼 자료는 거의 없지만, 대기업 표준치고는 꽤 합리적임. 다만 선택지가 너무 많아 관련 스펙을 많이 읽어야 함
-
“USB 장치 드라이버를 직접 작성하라”는 요청을 받는다면, 그 장치를 돌려주고 가상 COM 포트로 처리할 수 없는지 먼저 확인하겠음