0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

mikanOS 6章

Posted at

6.2 USBホストドライバ

USBマウスに対応する
→Universal Serial Bus
シリアルバス: バスには様々な機器からの信号がのる。シリアルとは、1本の信号線を使って、信号が1bitずつ送られるって意味。

USBホストコントローラー: USB機器とOSを繋ぐための制御チップ。OSはこのコントローラーを制御することで、USB機器と通信できるようになる。
ドライバ:USBホストコントローラーのような制御チップを操作するためのソフトウェアのこと。

  • USBドライバの階層
    • クラスドライバ: USBターゲットの種類ごと。キーボードやマウスとかオーディオ機器とか。
    • USBバスドライバ: ホストコントローラーの詳細を隠してUSB規格で定められたAPIを提供する。e.g.) USBターゲットがどんな機器かを識別するGET_DESCRIPTORとか。
    • ホストコントローラドライバ: ホストコントローラーを制御する。
  • PCIバスドライバ?

ホストコントローラードライバはホストコントローラーの規格別に作る必要がある。この本だとxHCLのみ対応。これに準拠したホストコントローラのことをxHCと呼ぶ。

6.3 PCIデバイスの探索

USBドライバを使ってマウスからデータを入力する。

流れ

  • PCIバスに接続されたPCIデバイスを全て列挙する。
    • PCI: 部品とマザーボードを繋ぐための規格。xHCとかGPUとかネットワークカードとかがこれによって接続されている。
  • 列挙されたデバイスの中からxHCを探す。
  • xHCを初期化。
  • USBバス上で、マウスを探す。
  • マウスの初期化。
  • マウスからデータを受信。

各PCIデバイスはPCIコンフィギュレーション空間を持っている。これを読むには、CONFIG_ADDRESSレジスタと、CONFIG_DATAレジスタを使う。CONFIG_ADDRESSレジスタに読み書きしたいPCIコンフィギュレーション空間の位置を設定してから、CONFIG_DATAを読み書きすることで、PCIコンフィギュレーション空間を読み書きできる。

  • CONFIG_ADDRESSの構造:
    ビット位置31: Enable bit
    30-24: 予約領域
    23-16: バス番号(0-31)
    15-11: デバイス番号(0-31 1つのバスは最大32個のデバイスを持てる)
    10-9: ファンクション番号(0-7 1つのデバイスには8つのファンクションを持てる。)
    7-0: レジスタオフセット(0-255 4バイト単位のオフセット)
  // #@@range_begin(make_address)
  /** @brief CONFIG_ADDRESS 用の 32 ビット整数を生成する */
  uint32_t MakeAddress(uint8_t bus, uint8_t device,
                       uint8_t function, uint8_t reg_addr) {
    // xを左にbitsだけだけビットシフト
    // ラムダ式は関数の内部で定義できる。
    auto shl = [](uint32_t x, unsigned int bits) {
        return x << bits;
    };

    return shl(1, 31)  // enable bit
        | shl(bus, 16)
        | shl(device, 11)
        | shl(function, 8)
        | (reg_addr & 0xfcu);
  }
  // #@@range_end(make_address)
  • CONFIG_DATAとCONFIG_ADDRESSレジスタを読み書きする方法

メモリアドレス空間は、メインメモリ用。IOアドレス空間は周辺機器用という区別があり、PCIコンフィギュレーション空間は周辺機器なのでIOアドレス空間につながっている。

  • IOアドレス空間
    • コンピュータシステムでCPUが外部デバイス(キーボード、マウス、ディスクドライブなど)と通信するために使用する特別なアドレス空間
global IoOut32  ; void IoOut32(uint16_t addr, uint32_t data);
IoOut32:
// 引数で指定されたaddrに対して、32ビット整数dataを出力する。ABIの仕様により、引数addrはRDIレジスタ、dataはRSIレジスタに設定される。DIはRDIの下位16bit、ESIはRSIの下位32bit.
    mov dx, di    ; dx = addr
    mov eax, esi  ; eax = data
    out dx, eax ; IOMEM[dx] = eax
    ret

global IoIn32  ; uint32_t IoIn32(uint16_t addr);
IoIn32:
    mov dx, di    ; dx = addr
    in eax, dx ; eax = IOMEM[dx]. eaxに設定された値が戻り値になる。
    ret

これらを用いて下記のようにコンフィギュレーション空間を読みとる。
CONFIF_ADDRESSとかCONFIG_DATAレジスタとかは、IOアドレス空間の0xc8fと0xcfcに存在する。

  /** @brief CONFIG_ADDRESS レジスタの IO ポートアドレス */
  const uint16_t kConfigAddress = 0x0cf8;
  /** @brief CONFIG_DATA レジスタの IO ポートアドレス */
  const uint16_t kConfigData = 0x0cfc;

IOアドレス空間の読み書きには、専用のIO命令が必要。C++では表現できないのでアセンブリで書く。

  // #@@range_begin(config_addr_data)
  // CONFIG_ADDRESSにアドレスを書き込む。
  void WriteAddress(uint32_t address) {
    IoOut32(kConfigAddress, address);
  }

  void WriteData(uint32_t value) {
    IoOut32(kConfigData, value);
  }

  // CONFIG_DATAに値が出力されるからそれを読み取る。
  uint32_t ReadData() {
    return IoIn32(kConfigData);
  }

 // ベンダIDはレジスタオフセット0にあるからでオフセット0x00を指定。
  uint16_t ReadVendorId(uint8_t bus, uint8_t device, uint8_t function) {
    WriteAddress(MakeAddress(bus, device, function, 0x00));
    return ReadData() & 0xffffu;
  }
  // #@@range_end(config_addr_data)

(IOポートアドレスとかよくわかんない)

  • PCIデバイスに繋がったデバイスを再帰的に探索する。
Error ScanAllBus() {
    num_device = 0;
    // バス0、デバイス0は必ずホストブリッジ。ホストブリッジのヘッダタイプを読み取る。ファンクション0のホストブリッジが単機能デバイスだったらそれはバス0を担当しているからScanBus(0)でok。他にもファンクションがあったら、ファンクション番号が担当バス番号を表す。
    auto header_type = ReadHeaderType(0, 0, 0);
    if (IsSingleFunctionDevice(header_type)) {
      return ScanBus(0);
    }

    for (uint8_t function = 1; function < 8; ++function) {
    // ホストブリッジのファンクション番号が担当バス番号を表すから、ScaBus(function)てしてる。
      if (ReadVendorId(0, 0, function) == 0xffffu) { // 実際のデバイスがあるかどうか。
        continue;
      }
      if (auto err = ScanBus(function)) {
        return err;
      }
    }
    return Error::kSuccess;
  }
  // #@@range_begin(scan_bus)
/** @brief 指定のバス番号の各デバイスをスキャンする.
 * 有効なデバイスを見つけたら ScanDevice を実行する.
 */
Error ScanBus(uint8_t bus) {
  for (uint8_t device = 0; device < 32; ++device) {
    if (ReadVendorId(bus, device, 0) == 0xffffu) {
      continue;
    }
    // 実際のデバイスがある。
    if (auto err = ScanDevice(bus, device)) {
      return err;
    }
  }
  return Error::kSuccess;
}
// #@@range_end(scan_bus)
  // #@@range_begin(scan_device)
/** @brief 指定のデバイス番号の各ファンクションをスキャンする.
 * 有効なファンクションを見つけたら ScanFunction を実行する.
 */
Error ScanDevice(uint8_t bus, uint8_t device) {
  if (auto err = ScanFunction(bus, device, 0)) {
    return err;
  }
  if (IsSingleFunctionDevice(ReadHeaderType(bus, device, 0))) {
    return Error::kSuccess;
  }

  for (uint8_t function = 1; function < 8; ++function) {
    if (ReadVendorId(bus, device, function) == 0xffffu) {
      continue;
    }
    if (auto err = ScanFunction(bus, device, function)) {
      return err;
    }
  }
  return Error::kSuccess;
}
// #@@range_end(scan_device)
  // #@@range_begin(scan_function)
  /** @brief 指定のファンクションを devices に追加する.
   * もし PCI-PCI ブリッジなら,セカンダリバスに対し ScanBus を実行する
   */
  Error ScanFunction(uint8_t bus, uint8_t device, uint8_t function) {
    auto header_type = ReadHeaderType(bus, device, function);
    if (auto err = AddDevice(bus, device, function, header_type)) {
      return err;
    }

    auto class_code = ReadClassCode(bus, device, function);
    uint8_t base = (class_code >> 24) & 0xffu;
    uint8_t sub = (class_code >> 16) & 0xffu;

    if (base == 0x06u && sub == 0x04u) {
      // standard PCI-PCI bridge 2つのPCIバスを繋ぐ。
      auto bus_numbers = ReadBusNumbers(bus, device, function);
      uint8_t secondary_bus = (bus_numbers >> 8) & 0xffu;
      return ScanBus(secondary_bus);
    }

    return Error::kSuccess;
  }
  // #@@range_end(scan_function)

まとめ

やったこと: PCIバスに接続されたPCIデバイスを全て列挙する。
How?:

  • ホストブリッジ(バス0, デバイス0)が単機能かどうかによって処理を分ける。

    • 単機能だったらScanBus(0)をしておしまい。
    • 多機能だったらScanBus(1~7)をして各バスを見て回る。
  • ScanBus→ScanDevice→ScanFunctionの順に進み、ScanFunction内で有効なデバイスだったら、devicesに追加する。

そもそもどうやってPCIデバイスを探すのか?:

  • 各PCIデバイスはPCIコンフィギュレーション空間を持っていて、其れをみることによりPCIデバイスに関する基本的な情報がわかる。
  • CONFIG_ADDRESSレジスタに見たいPCIコンフィギュレーション空間の位置を指定してやることで、CONFIG_DATAレジスタを読み書きしてPCIコンフィギュレーション空間を読み書きできる。

(無名名前空間とかinlineとかって何?なんのために?)

6.4 ポーリングでマウス入力

goal: 列挙したPCIデバイスからxHCを探して初期化し、USBマウスを使えるようにする。

  1. xHCを探す
  // #@@range_begin(find_xhc)
  // Intel 製を優先して xHC を探す
  pci::Device* xhc_dev = nullptr;
  for (int i = 0; i < pci::num_device; ++i) {
    // クラスコードが下記のやつがxHC
    if (pci::devices[i].class_code.Match(0x0cu, 0x03u, 0x30u)) {
      xhc_dev = &pci::devices[i];

      // Intel製のxHC
      if (0x8086 == pci::ReadVendorId(*xhc_dev)) {
        break;
      }
    }
  }
  1. xHCのレジスタ群が配置されてるメモリアドレスを取得する。
  • MMIO(memory mapper IO)
    • メモリと同じように読み書きできるレジスタ。CPU内蔵のレジスタはRAXとかの名前で読み書きするけど、MMIOはメモリと同じように読み書きできるレジスタ(?)
    • MMIOアドレスはPCIコンフィギュレーション空間のBAR0に記録されることになっている。
  // #@@range_begin(read_bar)
  const WithError<uint64_t> xhc_bar = pci::ReadBar(*xhc_dev, 0);
  Log(kDebug, "ReadBar: %s\n", xhc_bar.error.Name());
  const uint64_t xhc_mmio_base = xhc_bar.value & ~static_cast<uint64_t>(0xf);
  Log(kDebug, "xHC mmio_base = %08lx\n", xhc_mmio_base);
  // #@@range_end(read_bar)
  1. xHCの初期化

Intel製のxHCだった場合のみ、SwitchEhci2Xhciを呼び出す。

    // #@@range_begin(init_xhc)
  usb::xhci::Controller xhc{xhc_mmio_base};

  if (0x8086 == pci::ReadVendorId(*xhc_dev)) {
    SwitchEhci2Xhci(*xhc_dev);
  }
  {
    auto err = xhc.Initialize();
    Log(kDebug, "xhc.Initialize: %s\n", err.Name());
  }

  Log(kInfo, "xHC starting\n");
  xhc.Run();
  // #@@range_end(init_xhc)
  1. 何かが接続されているポートの設定を行う
// #@@range_begin(configure_port)
  usb::HIDMouseDriver::default_observer = MouseObserver;

  for (int i = 1; i <= xhc.MaxPorts(); ++i) {
    auto port = xhc.PortAt(i);
    Log(kDebug, "Port %d: IsConnected=%d\n", i, port.IsConnected());

    if (port.IsConnected()) {
      if (auto err = ConfigurePort(xhc, port)) {
        Log(kError, "failed to configure port: %s at %s:%d\n",
            err.Name(), err.File(), err.Line());
        continue;
      }
    }
  }
  // #@@range_end(configure_port)
  1. マウスを動かした時のイベントを処理する
  // #@@range_begin(receive_event)
  while (1) {
    if (auto err = ProcessEvent(xhc)) {
      Log(kError, "Error while ProcessEvent: %s at %s:%d\n",
          err.Name(), err.File(), err.Line());
    }
  }
  // #@@range_end(receive_event)
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?