USB Mouse 사용하기

USB Mouse 사용하기

2025년 3월 22일

ref #


마우스 지원을 위한 개념 #

USB 마우스 지원을 위해서는 정말 많은 작업이 필요하다.

일하면서 작업하긴 했지만, 주말 이틀이면 충분할 줄 알았던 작업이 드라이버를 가져다 쓰고도 폴링 마우스 지원에만 일주일이나 걸렸다.

전체적인 동작 구조 #

  1. 마우스 MCU 펌웨어(타겟드라이버)는 특정 주기마다 광센서의 움직임이나 버튼 입력을 주기적으로 스캔해서 HID 리포트 포맷으로 내부 버퍼에 저장해둔다.

    • 일부 정밀도가 필요한 게이밍 마우스 등 에서는 데이터가 누락되는 것을 방지하기 위해 누산을 하기도 하지만, 일반적으로는 polling 요청 주기가 짧기 때문에 직전에 검사한 값만 저장하는 경우가 많다.
  2. 호스트의 HID 디바이스 클래스 드라이버(usbhid)가 USB 버스 드라이버(usbcore)를 통해 USB 호스트 컨트롤러 드라이버(xhci_hcd)에 Interrupt IN URB 요청을 보내면 USB 컨트롤러(xHCI 하드웨어)는 특정 주기마다 HID 리포트 요청 명령을 보낸다.

  3. 컨트롤러는 USB케이블을 통해 마우스에 Interrupt IN 패킷을 주기적으로 전송하고 마우스에서 HID 리포트를 전달받는다.

  4. (인터럽트 방식) USB 컨트롤러는 PCIe 버스를 통해 메모리에 직접 쓰고(DMA) CPU에 인터럽트를 보내 호스트 HID 드라이버(usbhid)가 HDI 리포트를 파싱한 후 커널 input subsystem으로 전달하여 OS 입력 이벤트로 처리되도록 한다.

    • (폴링방식) 폴링방식은 Interrupt IN에 대한 응답이 왔을 때 CPU에 인터럽트를 보내지 않고 그냥 반복문 안에서 CPU가 메모리를 계속 확인하여 이벤트를 처리하는 방식이다.

USB #

통신 방법 #

eaa7633e-1951-4e66-af3c-5eaa28f9d74f

USB는 시리얼 버스 규격이고 시리얼 통신은 하나의 신호선을 사용해 일반적으로 클럭당 1bit 씩 전송하는 통신 방식이다.

병렬 통신보다 이론상 느릴것 같지만, 동기화나 간섭 문제로 램-CPU 같은 가까운 거리가 아니라 USB같은 외부 통신에서는 시리얼 통신이 더 빠르다.

각 엔드포인트에서 클럭(baud rate) 신호를 맞춰줘야하는 비동기 시리얼 방식(UART가 대표적)이 있고 클럭 선까지 연결되어 클럭에 맞춰 데이터를 해석하는 동기 시리얼 방식이 있는데, USB는 동기 시리얼 통신에 속한다.

사실 USB 선에서 D+, D-를 각각 데이터, 클럭 신호를 전송할수도 있었겠지만, NRZI + 비트스터핑 기법을 사용해서 데이터 신호에 클럭을 합쳐서 빠르고 안정적으로 전송할 수 있도록 구현되어 있다.


컨트롤러 #

USB 컨트롤러는 PC의 메인보드에 있는 반도체 칩이며, USB 통신을 전기적/프로토콜적으로 처리하여 USB 장치와 통신한 내용을 CPU 대신 데이터를 메모리에 전송한다.

컨트롤러는 USB 버전 별로 다른 규격이 있으며, 요즘은 USB 3.x 까지 호환되는 xHCI 규격이 많이 탑재되어 있다.

탑재된 컨트롤러 규격마다 컨트롤러 드라이버를 구현해야한다.


드라이버 역할 #

82cd05bd-a4b7-41b2-8c96-7e30f5a43e5c

  • PCI 버스 드라이버: USB 드라이버의 일부가 아니지만, 부팅 시 PCI 디바이스(USB 컨트롤러)를 인식하고 컨트롤러 드라이버와 연결해주고, CPU와의 병렬 데이터 송수신 통로(시스템 버스)를 제공하여 어떤 장치가 시스템 버스를 사용할지 결정해준다.
  • USB 컨트롤러 드라이버: 실제 물리적 장치인 USB 컨트롤러를 제어할 수 있도록 사양에 맞게 구현된다. USB 컨트롤러 칩의 메모리 맵 레지스터에 값을 써서 컨트롤러의 회로가 값을 읽으며 동작한다.
  • USB 버스 드라이버: 컨트롤러 드라이버의 기능을 이용해 USB 클래스 드라이버에게 API 를 제공하는 역할을 한다. xHCI, EHCI 등 모든 USB 컨트롤러 드라이버를 하나의 API로 통일시켜주는 역할이다.
  • USB 클래스 드라이버: USB 타깃의 종류마다 하나씩 버스 드라이버의 API를 호출하면서 기능을 구현한다. 키보드 마우스 등의 HID 클래스, 오디오 클래스, 스토리지 클래스 등 각 클래스마다의 실제 기능을 구현하는 드라이버이다.

PCI #

PCI는 부품과 메인보드의 CPU, 메모리를 연결하기 위한 버스이다.
xHCI, 네트워크 카드, 사운드카드, NVMe SSD, GPU 등의 장치가 모두 PCI에 연결되어 CPU는 PCI 장치를 메모리처럼 접근할 수 있게 된다. (메모리 매핑 I/O)

PCI Configuration space #

PCI 디바이스의 각 기능과 관련된 정보가 적혀있으며, 총 256 byte 중 0x40 byte 까지는 헤더로 모든 PCI 디바이스가 지켜야하는 헤더이고, 0x40~0xFF 는 디바이스마다 자유롭게 사용된다.

이 헤더에서 벤더ID와 클래스 필드를 조사하면 어떤 장치인지 알아올 수 있다.

헤더는 2가지 타입이 있고, Header Type 필드를 조사해서 확인할 수 있다.

324272d9-a3f7-464c-b56b-6439bf2d6adf

보통은 왼쪽의 타입이고, 다른 PCI 버스 장치를 연결해주는 브리지 장치들은 오른쪽 타입헤더를 사용한다.


PCI Configuration Space 접근 #

PCI 정보를 가져오기 위해서는 포트 방식과 ECAM 방식 모두 있지만, OVMF 는 기본적으로 레거시 PCI 버스를 가정하여 포트 방식을 사용한다.

x86_64 CPU에서 0x0000 ~ 0xFFFF 주소를 in/out 명령어로 접근하게 되면, IO 포트 주소에 접근할 수 있게 되며, 이 주소는 RAM과 같은 또다른 메모리 칩이 아니라 미리 정의(하드와이어)된 구조대로 사용하면 메인보드를 통해 하드웨어 장치 소켓의 레지스터나 버퍼와 직접 통신이 가능한 명령어 전송 통로 같은거라고 보면 된다.

40187d18-d1fb-48e3-b97e-5b769dd16578

정확히 말하면 I/O 포트 주소에 in, out 명령으로 접근하면 메인보드의 PCH(=SouthBridge) 칩셋으로 전달되고, PCH 칩셋이 I/O 포트 주소를 디코딩하여 적절한 장치의 컨트롤러 레지스터에서 값을 읽거나 쓰게되며 장치는 하드와이어된 로직에 맞춰 회로가 동작한다.

CONFIG_ADDRESS 0xCF8나 CONFIG_DATA 0xCFC 는 PCI를 위한 고정 I/O 포트 주소이며 이 레지스터를 이용해 원하는 PCI Configuration Space 에 접근할 수 있게 된다.

CONFIG_ADDRESS 레지스터에 원하는 위치를 지정하고, CONFIG_DATA를 읽고 쓰는 것으로 접근할 수 있다.

 1;31 30       24 23    16 15   11 10    8 7      2 1 0
 2; +-+----------+--------+-------+-------+--------+--+
 3; |1| Reserved | Bus #  | Dev # | Func# | Offset |00|
 4; +-+----------+--------+-------+-------+--------+--+
 5; Bus 0, Device 1, Function 0, Register 0x00 (Vendor ID & Device ID)
 6mov eax, 0x80001000  ; Enable Bit(31) + Bus(0) + Device(1) + Function(0) + Offset(0x00)
 7mov dx, 0xCF8        ; CONFIG_ADDRESS 포트 번호
 8out dx, eax          ; CONFIG_ADDRESS(0xCF8)에 주소 설정
 9; 원하는 정보를 컨트롤러가 세팅해둠
10mov dx, 0xCFC        ; CONFIG_DATA 포트 번호
11in eax, dx           ; CONFIG_DATA(0xCFC)에서 Vendor ID, Device ID 읽기
12mov ebx, eax         ; 결과를 EBX에 저장

CONFIG_ADDRESS, CONFIG_DATA #

3e2e1d96-9d36-4dac-b527-b370d212f7d0

여러개의 PCI 버스에서 하나의 버스당 32개의 PCI 디바이스가 연결되며, 하나의 PCI 디바이스는 최대 8개의 펑션을 가질 수 있으며 펑션은 연속적으로 존재하는건 아니다.

위에서 펑션마다 256byte짜리 PCI Configuration Space 구조체가 있다고 이야기 했다.
이 256byte를 4byte로 나누면 64개가 되는데, 이 4byte 단위가 Register이다.

  • Enable (1bit) : 1 로 세팅하면 CONFIG_DATA에 쓴 값이 PCI Configure space 에 전송된다.
  • Reserved (7bit) : 예약공간
  • Bus Number (8bit) : 접근하려는 PCI 버스 번호 0x00~0xFF
  • Device Number (5bit) : 접근하려는 장치 번호 0x0~0x1F
  • Function Number (3bit) : 해당 장치의 기능 번호 0x0~0x7
  • Register Offset (6bit) : 해당 기능의 PCI 구조체 DWORD 단위 오프셋. 0x00~0x3F
  • Padding (2bit) : 4byte 정렬을 위해 존재하는 패딩 값

결국엔 이 Register 오프셋을 선택해서 CONFIG_ADDRESS 값을 세팅하고 in 명령으로 0xCF8 에 넣는다는 의미는 선택한 장치-기능의 PCI Configuration Space 구조체를 4byte 단위로 선택한 것이다.

이후 CONFIG_DATA 값을 out으로 읽어오면 해당 레지스터를 읽어올 수 있고, 세팅하는 것도 마찬가지이다.

장치는 같기 때문에 PCI 헤더의 일부가 중복될 순 있지만, OS, 펌웨어, 드라이버 관점에서는 각 펑션이 논리적인 독립된 별도의 장치라고 판단하며 실제로 device id, class code 도 전부 다른 값이 들어간다. (논리적 장치라고 하는것도 웃긴게 내부적으론 칩이 따로 있긴 하겠지)

lspci 명령으로 확인할 수 있으며, [PCI Bus]:[Device].[function] 형태로 해석할 수 있다.

495020e3-2db7-4bec-ab6d-083324ecdd12


폴링 방식 구현 #

대략적인 흐름은 아래와 같다.

  • PCI 버스에 연결된 PCI 디바이스 전체를 열거한다.
  • 열거된 디바이스 중 xHC를 찾는다.
  • xHC를 초기화한다.
  • USB 버스에서 마우스를 찾는다.
  • 마우스를 초기화한다
  • 마우스로부터 데이터를 수신한다.

1. PCI Configure Space 접근 함수들 구현 #

PCI I/O 포트에 값을 읽고 쓰는 함수 #

in, out 함수는 어셈블리 명령이기 때문에 어셈블리로 구현해야 하고, C++ 에서 호출할 수 있도록 인터페이스를 만들어둬야 한다.

1#pragma once
2
3#include <stdint.h>
4
5extern "C" {
6  void IoOut32(uint16_t addr, uint32_t data);
7  uint32_t IoIn32(uint16_t addr);
8}

호출규약에 따라 RDI, RSI에 첫번째, 두번째 인자가 전달되기 때문에 레지스터에서 값을 가져오고 in, out 명령을 실행한다. 리턴값은 RAX에 저장한다.

 1; asmfunc.asm
 2;
 3; System V AMD64 Calling Convention
 4; Registers: RDI, RSI, RDX, RCX, R8, R9
 5
 6bits 64
 7section .text
 8
 9; out 명령은 I/O 포트 주소인 op1 에 op2 의 값을 밀어넣는 명령이다.
10global IoOut32  ; void IoOut32(uint16_t addr, uint32_t data);
11IoOut32:
12    mov dx, di    ; dx = addr
13    mov eax, esi  ; eax = data
14    out dx, eax
15    ret
16
17; in 명령은 I/O 포트 주소인 op1 에서 데이터를 읽어 op2 에 저장하는 명령이다.
18global IoIn32  ; uint32_t IoIn32(uint16_t addr);
19IoIn32:
20    mov dx, di    ; dx = addr
21    in eax, dx
22    ret

원하는 논리장치 주소를 세팅하는 함수 #

CONFIG_ADDRESS 포맷에 맞게 원하는 장치의 레지스터를 선택할 수 있도록 주소를 변환한다.

 1uint32_t MakeAddress(uint8_t bus, uint8_t device,
 2                      uint8_t function, uint8_t reg_addr) {
 3  auto shl = [](uint32_t x, unsigned int bits) {
 4    return x << bits;
 5  };
 6
 7  return shl(1, 31)
 8      | shl(bus, 16)
 9      | shl(device, 11)
10      | shl(function, 8)
11      | (reg_addr & 0xfcu);    // 레지스터 오프셋 1byte 중 맨 마지막 2bit는 패딩
12}
13
14// 위의 기능들을 모두 합쳐서 원하는 장치의 VendorId를 읽어오는 함수를 구현
15uint16_t ReadVendorId(uint8_t bus, uint8_t device, uint8_t function) {
16  // IoOut32(0xcf8, addr)
17  WriteAddress(MakeAddress(bus, device, function, 0x00))
18  // IoIn32(0xcfc)
19  // 4byte를 LittleEndian 으로 꺼냈지만 하위 2byte만 VendorId 이다.
20  return ReadData() & 0xffffu;   
21}
22
23uint16_t ReadDeviceId(uint8_t bus, uint8_t device, uint8_t function) {
24  WriteAddress(MakeAddress(bus, device, function, 0x00))
25  // VendorId와 같은곳에서 꺼냈지만 상위 2byte만 DeviceId이다. 
26  return ReadData() >> 16;
27}

다중 기능 장치인지 판단하는 함수 #

PCI Conf Space Header Type 필드의 첫번째 비트(0x80)를 검사하면 장치에 기능이 몇개인지 확인할 수 있다.

39ffbfd5-7319-427c-8eaf-bd0710dae8b8

1uint8_t ReadHeaderType(uint8_t bus, uint8_t device, uint8_t function) {
2  WriteAddress(MakeAddress(bus, device, function, 0x0c));
3  return (ReadData() >> 16) & 0xffu;
4}
5
6bool IsSingleFunctionDevice(uint8_t header_type) {
7  return (header_type & 0x80u) == 0;
8}

다른 PCI 를 연결해주는 브리지인 경우를 찾기 #

PCI 장치에는 브리지도 연결될 수 있기때문에 다른 PCI 버스 장치에 대해서도 재귀적으로 스캔을 해야한다.

맨 위에서 보여줬던 PCI Conf Space 의 오른쪽 타입 헤더를 사용하기 때문에 연결된 버스의 수를 확인할 수 있다.

1uint32_t ReadBusNumbers(uint8_t bus, uint8_t device, uint8_t function) {
2  WriteAddress(MakeAddress(bus, device, function, 0x18));
3  return ReadData();
4}

2. PCI 스캔 함수 구현 #

PCI에 연결된 전체 장치들을 스캔해서 발견된 장치를 devices에 저장한다.

1struct Device {
2  uint8_t bus, device, function, header_type;
3};
4
5// 글로벌 변수를 헤더파일에서 정의할땐 inline을 붙인다. (이전에는 extern)
6inline std::array<Device, 32> devices;
7inline int num_device;
8
9Error ScanAllBus();

스캔함수 #

외부로 노출된 ScanAllBus 라는 함수는 자세히 보면 Bus 0 / Device 0 / Function 0 만 확인하는 것을 알 수 있다.

0번째 버스의 0번째 디바이스 0번째 펑션은 호스트 브릿지라고 말하며, CPU와 직접적으로 연결된 메인 PCI 브릿지를 의미하고 0번째 버스를 관리하고 있다.

책에서는 이 브릿지가 멀티펑션이라면 n번째 펑션은 n번째 버스를 관리한다고 되어있어서 아래와 같은 코드가 작성되어 있는데 GPT는 아니라고 한다.

어차피 잘못된 장치면 에러가 발생해서 devices에 추가되지 않기 때문에 상관없긴 하다.

 1Error ScanAllBus() {
 2  num_device = 0;
 3
 4  auto header_type = ReadHeaderType(0, 0, 0);
 5  uint8_t functions = IsSingleFunctionDevice(header_type) ? 1 : 8;
 6  for (uint8_t function = 0; function < functions; ++function) {
 7    // 싱글펑션은 무조건 0번에 있다.
 8    // 멀티펑션인 경우 펑션은 순서대로라는 보장이 없어서 확인되면 ScanBus 호출
 9    if (ReadVendorId(0, 0, function) == 0xffffu) {
10      continue;
11    }
12    // 멀티펑션인 경우 펑션n은 버스n의 호스트브릿지가 된다고 저자가 주장한다. 
13    if (auto err = ScanBus(function)) {
14      return err;
15    }
16  }
17  return Error::kSuccess;
18}

ScanBus는 그냥 해당 버스의 디바이스 32개를 전부 검사하는 코드이고, ScanDevice는 해당 디바이스의 전체 펑션을 검사하는 코드이다.


ScanFunction #

0번째 버스에 대해서는 전체 디바이스.펑션을 검사하고 그 이후부터는 펑션에서 발견된 장치가 브릿지인 경우에만 해당 세컨더리 버스 번호를 찾아서 연결된 버스의 전체 디바이스.펑션을 재귀적으로 찾는 방식을 사용한다.

장치가 발견되면 AddDevice 를 호출해서 찾은 디바이스 목록에 추가한다. 꼭 말단 장치가 아니더라도 전부 추가한다. (브릿지도 표시되도록 장치 목록에 추가)

 1// 디바이스 객체를 생성해서 전역 디바이스 목록에 추가 
 2Error AddDevice(uint8_t bus, uint8_t device,
 3                uint8_t function, uint8_t header_type) {
 4  if (num_device == devices.size()) {
 5    return Error::kFull;
 6  }
 7
 8  devices[num_device] = Device{bus, device, function, header_type};
 9  ++num_device;
10  return Error::kSuccess;
11}
12
13// 펑션(내부칩? 논리적장치?)을 검사해서 발견된 장치 추가
14Error ScanFunction(uint8_t bus, uint8_t device, uint8_t function) {
15  auto header_type = ReadHeaderType(bus, device, function);
16  if (auto err = AddDevice(bus, device, function, header_type)) {
17    return err;
18  }
19
20  auto class_code = ReadClassCode(bus, device, function);
21  uint8_t base = (class_code >> 24) & 0xffu;
22  uint8_t sub = (class_code >> 16) & 0xffu;
23  // 만약 PCI 브릿지 클래스인 경우
24  if (base == 0x06u && sub == 0x04u) {
25    // 세컨더리 버스 번호를 찾아서 재귀적으로 스캔한다.
26    auto bus_numbers = ReadBusNumbers(bus, device, function);
27    uint8_t secondary_bus = (bus_numbers >> 8) & 0xffu;
28    return ScanBus(secondary_bus);
29  }
30
31  return Error::kSuccess;
32}

PCI 장치 스캔해서 목록 출력 #

 1// main.cpp
 2// 전체 버스를 스캔하고, 각 장치의 VendorId, ClassCode, HeaderType을 출력한다.
 3  auto err = pci::ScanAllBus();
 4  printk("ScanAllBus: %s\n", err.Name());
 5
 6  for (int i = 0; i < pci::num_device; ++i) {
 7    const auto& dev = pci::devices[i];
 8    auto vendor_id = pci::ReadVendorId(dev.bus, dev.device, dev.function);
 9    auto class_code = pci::ReadClassCode(dev.bus, dev.device, dev.function);
10    printk("%d.%d.%d: vend %04x, class %08x, head %02x\n",
11        dev.bus, dev.device, dev.function,
12        vendor_id, class_code, dev.header_type);
13  }

a0287d10-b3c4-4837-b728-fbac4548a48a


3. 마우스 장치 찾기 #

위에서 구현한 장치 스캔 함수를 한번 호출하고 나면 pci::devices 에 장치들이 리스트로 저장되고, pci::num_device 에 장치 수가 저장된다.
스캔한 장치들의 클래스코드를 보면서 USB xHCI 장치를 찾아낼 수 있다.

xHCI 장치 찾기 #

 1// main.cpp
 2pci::Device* xhc_dev = nullptr;
 3for (int i = 0; i < pci::num_device; ++i) {
 4  // base 0xc : 시리얼 버스 장치 
 5  // sub  0x3 : USB 장치
 6  // interface 0x30 : xHCI 장치
 7  // 이 세 조건이 맞아야 USB xHCI 장치가 된다. 
 8  if (pci::devices[i].class_code.Match(0x0cu, 0x03u, 0x30u)) {
 9    xhc_dev = &pci::devices[i];
10    // Intel 의 VendorId 가 보이면 break 처리해서 Intel 제품을 우선처리
11    if (0x8086 == pci::ReadVendorId(*xhc_dev)) {
12      break;
13    }
14  }
15}

BAR와 MMIO #

BAR(Base Address Register) 는 PCI Configuration Space 의 헤더에서 확인할 수 있는 영역이며, 32bit 크기의 6개 (192byte 고정크기) 로 이뤄져 있으며 메모리주소를 저장하고 있다. 메모리 주소이기 때문에 32bit 는 4byte씩 6개까지 사용할 수 있고, 64bit는 8byte씩 3개만 사용할 수 있다.

a5742039-734c-45bd-b156-2fb1ae53b283

BAR는 하위 4bit는 플래그이며 나머지 상위 주소는 플래그를 의미한다. 페이지 단위로 정렬되기 때문에 8bit는 0으로 고정값이 된다.

32bit BAR의 경우엔 20bit 주소, 8bit 고정, 4bit 플래그가 되고
64bit BAR의 경우엔 BAR0 이 32bit BAR와 동일한 구조로 만들어지고 BAR1은 상위 주소를 가리키게 된다.
3번째 비트(0x100)가 1인 경우엔 64bit BAR로 해석해야한다.

BAR에 저장되는 주소는 메모리 주소는 해당 PCI 장치의 IO 레지스터 공간의 베이스 주소를 의미하고, 0번째 비트가 0 인 경우 MMIO, 1인경우 IO 포트매핑을 의미한다.

IO 포트 매핑은 PCI에서 확인했듯 in, out 명령어로 하드웨어와 통신하는 방식이고, MMIO(Memory Mapped IO) 는 일반 메모리 공간을 IO 포트매핑처럼 예약해서 mov 명령으로 하드웨어와 통신하는 방법이다.

 1// pci.cpp
 2  constexpr uint8_t CalcBarAddress(unsigned int bar_index) {
 3    // PCI Conf Space의 0x10 부터 시작하며 한개당 4byte 크기
 4    return 0x10 + 4 * bar_index;
 5  }
 6
 7  WithError<uint64_t> ReadBar(Device& device, unsigned int bar_index) {
 8    // 6번 인덱스는 없음. 64bit BAR를 가져오려 해도 인덱스는 0,2,4 로 접근한다. 
 9    if (bar_index >= 6) {
10      return {0, MAKE_ERROR(Error::kIndexOutOfRange)};
11    }
12    const auto addr = CalcBarAddress(bar_index);
13    const auto bar = ReadConfReg(device, addr);   // BAR 값 읽어옴
14
15    // flag 값을 확인해서 3번째 bit가 0이면 32bit BAR로 판단하고 바로 주소값을 리턴
16    if ((bar & 4u) == 0) {
17      return {bar, MAKE_ERROR(Error::kSuccess)};
18    }
19
20    // 64bit BAR는 0,2,4 인덱스밖에 접근할 수 없다.
21    if (bar_index >= 5) {
22      return {0, MAKE_ERROR(Error::kIndexOutOfRange)};
23    }
24
25    // 상위 BAR를 읽어온다 (1,3,5 인덱스)
26    const auto bar_upper = ReadConfReg(device, addr + 4);
27    return {
28      // (상위 BAR << 32) | 하위 BAR
29      bar | (static_cast<uint64_t>(bar_upper) << 32),
30      MAKE_ERROR(Error::kSuccess)
31    };
32  }

64bit BAR0 에 저장된 값을 읽어오는 코드이다. xHCI는 관례상 BAR0이 MMIO 주소이지만, 필수적인 조건은 아니기 때문에 스캔하는 코드를 작성해도 좋다.

1  const WithError<uint64_t> xhc_bar = pci::ReadBar(*xhc_dev, 0);
2  Log(kDebug, "ReadBar: %s\n", xhc_bar.error.Name());
3  // 0xf의 역과 & 하는 이유는 하위 4bit는 플래그 값으로 베이스주소가 오염됐기 때문
4  const uint64_t xhc_mmio_base = xhc_bar.value & ~static_cast<uint64_t>(0xf);
5  Log(kDebug, "xHC mmio_base = %08lx\n", xhc_mmio_base);

1e22589d-4ffe-4d9e-8bdd-95d166b7dc3a


4. USB 초기화 및 장치 조작 #

드라이버를 직접 구현하면 좋겠지만 그건 나중으로 미루고 책 저자의 드라이버를 가져다가 사용했다.

그냥 가져다 쓰면 여러가지 에러를 많이 만날 수 있다.
가짜로 만들어뒀던 new, delete를 지우고 libc.so 를 지원하기 위해 더미함수를 newlib_support.c 에 구현했던 것 처럼 libc++.so를 지원하기 위한 libcxx_support.cpp 를 구현해야 한다.
이후 링커 옵션에 -lc++ 을 추가해서 libc++.so 를 연결하면 준비는 다 됐다.

드라이버 코드와 헤더까지 컴파일 대상에 넣고 컴파일 하면 되고, main 함수에서 드라이버 초기화와 사용하는 코드를 넣으면 끝난다.


과정 요약 #

위에서 IO포트를 이용해서 PCI 버스에서 연결된 장치를 조회한 것처럼 xHCI 장치도 비슷하게 동작한다. xHCI의 MMIO 주소를 찾아온 것도 레지스터를 조작해서 장치를 컨트롤하기 위해서이다.

xHCI는 표준규격이며, MMIO 구성이 모두 동일하다. 물론 xHCI가 아니라면(OHCI,EHCI) MMIO 구성이 달라질 수 있다.

2755259c-40ed-4158-b41a-e2febfb3fe2e ReactOS의 xHCI용 드라이버 개발 개요

xHCI 초기화 → 연결된 USB 장치 감지 → 마우스 찾기 → Interrupt IN 엔드포인트 설정
→ 이후에는 xHCI 컨트롤러가 설정된 interval 마다 마우스에게 Interrupt IN 요청
→ 마우스는 샘플링에서 변화가 있을 때(마우스 타겟 드라이버에 데이터가 쌓였을때) 응답
→ xHCI 컨트롤러는 HID 리포트를 메인메모리에 저장하고 TRB를 이벤트링(메인메모리)에 작성 (DMA)
→ CPU는 이벤트링을 확인해서 마우스 이벤트 처리 (polling 방식은 이벤트가 없을때 빠져나가고 주기적으로 다시 확인)

이 모든 작업이 xHCI의 MMIO 주소를 드라이버에 전달해주기만 하면 드라이버가 알아서 처리하게 된다.

 1  usb::xhci::Controller xhc{xhc_mmio_base};
 2  xhc.Run();
 3
 4  usb::HIDMouseDriver::default_observer = MouseObserver;
 5  for (int i = 1; i <= xhc.MaxPorts(); ++i) {
 6    auto port = xhc.PortAt(i);
 7
 8    if (port.IsConnected()) {
 9      if (auto err = ConfigurePort(xhc, port)) {
10        Log(kError, "failed to configure port: %s at %s:%d\n",
11            err.Name(), err.File(), err.Line());
12        continue;
13      }
14    }
15  }
16
17// HID 이벤트를 계속해서 요청하고 처리하는 함수를 호출한다.
18  while (1) {
19    // 이 함수는 xhc와 같은 공간(usb::xhci)에 정의되어 있다
20    if (auto err = ProcessEvent(xhc)) {
21      Log(kError, "Error while ProcessEvent: %s at %s:%d\n",
22          err.Name(), err.File(), err.Line());
23    }
24  }

ProcessEvent 코드를 보면 HasFront() 로 이벤트링을 확인하고 값이 없으면 처리하지 않고 리턴되는 것을 알 수 있다. HID 리포트가 응답이 될때만 TRB가 xHCI에 의해 DMA로 이벤트링에 작성된다.

 1  Error ProcessEvent(Controller& xhc) {
 2    if (!xhc.PrimaryEventRing()->HasFront()) {
 3      return MAKE_ERROR(Error::kSuccess);
 4    }
 5
 6    Error err = MAKE_ERROR(Error::kNotImplemented);
 7    auto event_trb = xhc.PrimaryEventRing()->Front();
 8    if (auto trb = TRBDynamicCast<TransferEventTRB>(event_trb)) {
 9      err = OnEvent(xhc, *trb);
10    } else if (auto trb = TRBDynamicCast<PortStatusChangeEventTRB>(event_trb)) {
11      err = OnEvent(xhc, *trb);
12    } else if (auto trb = TRBDynamicCast<CommandCompletionEventTRB>(event_trb)) {
13      err = OnEvent(xhc, *trb);
14    }
15    xhc.PrimaryEventRing()->Pop();
16
17    return err;
18  }

더 자세한 내용은 나중에 드라이버를 직접 구현할때 정리할 것이다.

마우스가 잘 동작한다!
사실 이 방식은 busy wating 상태라서 CPU의 점유를 항상 먹고 CPU 스케줄링에 따라 이벤트처리가 지연될 수 있다는 문제점이 있다.

d7323103-5032-48ec-8d2f-9023288b7780

comments powered by Disqus