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マウスを使えるようにする。
- 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;
}
}
}
- 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)
- 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)
- 何かが接続されているポートの設定を行う
// #@@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)
- マウスを動かした時のイベントを処理する
// #@@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)