소프트웨어 개발자를 위한 USB: 사용자 공간 USB 드라이버 작성 입문

3 days ago 6
  • 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 ManagerUSB 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 버전

마무리

  • 커널 코드를 작성하지 않고 사용자 공간에서 완전한 USB 드라이버를 구현 가능
  • 모든 USB 드라이버는 동일한 기본 원리를 따르며, 프로토콜만 다름
  • 복잡한 프로토콜(MTP 등)도 기본 구조는 동일하며, 소켓 통신과 유사한 개념으로 접근 가능함
Read Entire Article