ゼロからOS自作入門 6章

マウス入力とPCI
マウス入力を受け付ける

準備
$ unlink MikanLoaderPkg # 前に残っていたシンボリックリンクを削除
$ ln -s /mnt/30DayOS/workspace/ch06/MikanLoaderPkg/ ./
$ source edksetup.sh
$ build

マウスカーソル
フォントと同じ

テンプレートは勉強しないといけないな
付録か
この実装自体はテンプレートの形が珍しいだけで難しくない

とりあえず絵を書くだけだから特に苦労する所なし
あとでリファクタリング対象だけどまずはOK
extern "C" void KernelMain(const FrameBufferConfig& frame_buffer_config) {
PixelWriter* writer = GetWriter(frame_buffer_config);
// Base Screen
const int kFrameWidth =
static_cast<int>(frame_buffer_config.horizontal_resolution);
const int kFrameHeight =
static_cast<int>(frame_buffer_config.vertical_resolution);
// 上の青い部分
FillRectangle(*writer, {0, 0}, {kFrameWidth, kFrameHeight - 50},
kDesktopBGColor);
// 下のバー(とりあえず全体を塗りつぶす)
FillRectangle(*writer, {0, kFrameHeight - 50}, {kFrameWidth, 50},
kDesktopBlackColor);
// 左下のグレーの部分(下のバーを上書き)
FillRectangle(*writer, {0, kFrameHeight - 50}, {kFrameWidth / 5, 50},
kDesktopDarkGrayColor);
// 左下の四角枠(更に上書き)
DrawRectangle(*writer, {10, kFrameHeight - 40}, {30, 30},
kDesktopLightGrayColor);
console = new (console_buf) Console{*writer, {0, 0, 0}, {255, 255, 255}};
printk("Welcome to MikanOS!\n");
// Write Mouse Cursol
for (int y = 0; y < kMouseCursorHeight; ++y) {
for (int x = 0; x < kMouseCursorWidth; ++x) {
if (mouse_cursor_shape[y][x] == '@') {
writer->Write(200 + x, 100 + y, kMouseEdgeColor);
} else if (mouse_cursor_shape[y][x] == '.') {
writer->Write(200 + x, 100 + y, kMouseInsideColor);
}
}
}
while (1) __asm__("hlt");
}

USBホストドライバ

- 仕様書

- USBホストコントローラ
- USB機器とOSの橋渡し
- 通常はチップが搭載される
- ドライバ
- ハードウェアを制御するソフトウェア
説明 | |
---|---|
ホストコントローラドライバ | ホストコントローラを制御するファームウェア |
USBバスドライバ | ホストコントローラの詳細を隠してUSB規格で決められたAPIを提供 |
クラスドライバ | USBターゲット毎に用意するファームウェア |

ホストコントローラドライバは規格毎に作る必要がある
図のようにEHCI/OHCI/UHCI/xHCHIがある

PCIデバイスの探索
やることリスト
- PCIバスに接続されたPCIデバイスを全て列挙する
- 列挙されたデバイス一覧からxHCを見つける
- xHCを初期化する
- USBバス上でマウスを探す
- マウスを初期化する
- マウスからデータを受信する
xHC(Extensible Host Controller)
米インテル(Intel)社が仕様を策定・公開しており、様々な企業がこの仕様に準拠したコントローラICを開発している。USB 3.0で導入されたSuperSpeed USB(5Gbps)での高速な通信に対応する

- PCIコンフィギュレーション空間を読むためにCONFIG_ADDRESSレジスタとCONFIG_DATAレジスタを使う
- CONFIG_ADDRESSレジスタ(0x0cf8、32bit、Read/Write可)
- bit0-1:0に固定
- bit2-7:レジスタアドレス
- bit8-10:機能番号
- bit11-15:デバイス番号
- bit16-23:バス番号
- bit24-30:リザーブで、0に固定
- bit31:イネーブルビットで、1に固定
- CONFIG_DATAレジスタ(0x0cfc~0x0cff、任意のサイズ、Read/Write可)
- CONFIG_ADDRESSレジスタのイネーブルビットが0の場合は、ここはCONFIG_DATAレジスタにはならない
- CONFIG_ADDRESSレジスタ(0x0cf8、32bit、Read/Write可)
- CONFIG_ADDRESSレジスタを設定する
- デバイスのバス番号・デバイス番号・機能番号をせっていする
- アクセスしたい領域のオフセット値を設定する
- イネーブルビットを1に設定する
- CONFIG_DATAレジスタに対して読み込みまたは書き込みを行う
読み書きが終わった後にはイネーブルビットは0にする

ラムダ式も勉強しないといけないけど、ここは簡単

IOポートの書き込みはアセンブラを使うのか
bits 64
section .text
global IoOut32 ; void IoOut32(uint16_t addr, uint32_t data);
IoOut32:
mov dx, di ; dx = addr
mov eax, esi ; eax = data
out dx, eax
ret
global IoIn32 ; uint32_t IoIn32(uint16_t addr);
IoIn32:
mov dx, di ; dx = addr
in eax, dx
ret
IOアドレス空間はメモリアドレス空間とは完全に別のアドレス空間である。メモリアドレス空間はメインメモリ用で、IOアドレス空間は周辺機器用に用意されている。
この2つの操作を分離する必要がある。また、C++ではIOアドレス空間への書き込みは出来ないのでアセンブリ言語で表現する
X86アセンブラ/x86アーキテクチャを見てもDXレジスタは16bit何だよな
うまいとビット演算をしているんだな。凄いな。
- eax、esiについて
レジスタの名前 | 主な用途 |
---|---|
EAX | 計算の途中の値を覚えておくのに使います HSPのシステム変数STATに値を返したりするのにも使います |
EBX | 計算の途中の値を覚えておく場所がEAXの他にも必要なときに使います |
ECX | ある処理を繰り返したい時、回数をカウントするのに使います |
EDX | 計算の途中の値を覚えておく場所がEAXの他にも必要なときに使います |
ESI | メモリのアドレスを覚えておくのに使います |
EDI | メモリのアドレスを覚えておくのに使います |
ESP | スタックポインタのアドレスを覚えておくのに使います |
EBP | スタックフレームのアドレスを覚えておくのに使います |
EFLAGS | CPUの状態を表すフラグが入っています 計算用には使用できません |
- dx/diについて
64 ビット | 32 ビット | 16 ビット | 8 ビット |
---|---|---|---|
RAX | EAX | AX | AH, AL |
RBX | EBX | BX | BH, BL |
RCX | ECX | CX | CH, CL |
RDX | EDX | DX | DH, DL |
RSI | ESI | SI | |
RDI | EDI | DI | |
RBP | EBP | BP | |
RIP | EIP | IP | |
RSP | ESP | SP |

void WriteAddress(uint32_t address) { IoOut32(kConfigAddress, address); }
void WriteData(uint32_t value) { IoOut32(kConfigData, value); }
uint32_t ReadData() { return IoIn32(kConfigData); }
uint16_t ReadVendorId(uint8_t bus, uint8_t device, uint8_t function) {
WriteAddress(MakeAddress(bus, device, function, 0x00));
return ReadData() & 0xffffu;
}
uint16_t ReadDeviceId(uint8_t bus, uint8_t device, uint8_t function) {
WriteAddress(MakeAddress(bus, device, function, 0x00));
return ReadData() >> 16;
}
uint8_t ReadHeaderType(uint8_t bus, uint8_t device, uint8_t function) {
WriteAddress(MakeAddress(bus, device, function, 0x0c));
return (ReadData() >> 16) & 0xffu;
}
uint32_t ReadClassCode(uint8_t bus, uint8_t device, uint8_t function) {
WriteAddress(MakeAddress(bus, device, function, 0x08));
return ReadData();
}
uint32_t ReadBusNumbers(uint8_t bus, uint8_t device, uint8_t function) {
WriteAddress(MakeAddress(bus, device, function, 0x18));
return ReadData();
}
このあたりを深堀りする

PCI Configuration Space Headerをそのまま読んでいるのか
uint16_t ReadVendorId(uint8_t bus, uint8_t device, uint8_t function) {
WriteAddress(MakeAddress(bus, device, function, 0x00));
return ReadData() & 0xffffu;
}

/** @brief PCI デバイスを操作するための基礎データを格納する
*
* バス番号,デバイス番号,ファンクション番号はデバイスを特定するのに必須.
* その他の情報は単に利便性のために加えてある.
* */
struct Device {
uint8_t bus, device, function, header_type;
};
/** @brief ScanAllBus() により発見された PCI デバイスの一覧 */
inline std::array<Device, 32> devices;
/** @brief devices の有効な要素の数 */
inline int num_device;
/** @brief PCI デバイスをすべて探索し devices に格納する
*
* バス 0 から再帰的に PCI デバイスを探索し,devices の先頭から詰めて書き込む.
* 発見したデバイスの数を num_devices に設定する.
*/
Error ScanAllBus();
これでdevice一覧を確保するため目の領域を確保するのか。
で、ScanAllBusで全てのデバイスを登録するのか。接続可能なデバイス数が16(bit11-15:デバイス番号)だからそんなに大きくならないのか
Error ScanAllBus() {
num_device = 0;
auto header_type = ReadHeaderType(0, 0, 0);
if (IsSingleFunctionDevice(header_type)) {
return ScanBus(0);
}
for (uint8_t function = 1; function < 8; ++function) {
if (ReadVendorId(0, 0, function) == 0xffffu) {
continue;
}
if (auto err = ScanBus(function)) {
return err;
}
}
return Error::kSuccess;
}

- 機能番号は、0~7の値を取り、一つのデバイスに複数の機能が搭載されている場合に、それらを区別して扱うための番号
- デバイスが存在するなら、まず機能番号0に何らかの機能が割り振られる
- 機能番号0のデバイスにマルチファンクションであることを示すビットが1になっている
- これを見付けたら機能番号1~7を検索する
- 二番目のファンクションの機能番号が1であるとは限らない
つまり、CONFIG_DATAのオフセット0x0cの23bit目が1ならマルチファンクションデバイスなので、1〜7のファンクション番号を調べる
bool IsSingleFunctionDevice(uint8_t header_type) {
return (header_type & 0x80u) == 0;
}

- マルチファンクションデバイス
- 機能番号の最上位ビットが1である
- 複数の機能を持っているデバイスである
- ホストブリッジは複数存在する
- ファンクション0はバス0、ファンクション1はバス1を担当する
- シングルファンクションデバイス
- 機能番号の最上位ビットが0である
- ホストブリッジはバス0を担当するホストブリッジである
このため、バスのスキャンは次の関数で実現できる
/** @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;
}

ベンダ番号が無効値「0xffffu」の場合は処理をしていない。有効値の場合にはデバイスを探索する
/** @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;
}

ここでもベンダ番号が有効値の場合には機能をスキャンしている
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
auto bus_numbers = ReadBusNumbers(bus, device, function);
uint8_t secondary_bus = (bus_numbers >> 8) & 0xffu;
return ScanBus(secondary_bus);
}
return Error::kSuccess;
}
おお。PCI-PCIブリッジがある場合には再帰的に処理をするのか
- 060400 PCI to AGP bridge
- 060401 PCI to PCI bridge(support subtractive decode) ?

最後にインクリメントか
定義時点で32個まで定義しているのでこれ以上は対応しないってやつか
/** @brief devices[num_device] に情報を書き込み num_deviceをインクリメントする.
*/
Error AddDevice(uint8_t bus, uint8_t device, uint8_t function,
uint8_t header_type) {
if (num_device == devices.size()) {
return Error::kFull;
}
devices[num_device] = Device{bus, device, function, header_type};
++num_device;
return Error::kSuccess;
}

ポーリングでマウス入力
- 列挙したPCIデバイスからxHCIを探して初期化してUSBマウスを使えるようにする
- マウスの動きに合わせてデバイスを動かす

// Intel 製を優先して xHC を探す
pci::Device* xhc_dev = nullptr;
for (int i = 0; i < pci::num_device; ++i) {
if (pci::devices[i].class_code.Match(0x0cu, 0x03u, 0x30u)) {
xhc_dev = &pci::devices[i];
if (0x8086 == pci::ReadVendorId(*xhc_dev)) {
break;
}
}
}
if (xhc_dev) {
Log(kInfo, "xHC has been found: %d.%d.%d\n",
xhc_dev->bus, xhc_dev->device, xhc_dev->function);
}
クラスコードはいかを指す
- 0c:シリアルバスのコントローラ
- 03:USBコントローラ
- 30:xHCI

-device nec-usb-xhci,id=xhci
QEMUにUSB xHCIを渡す run_image.sh

ちょっと直す。main.cppのPCIデバイス一覧の所
// show_devices
auto err = pci::ScanAllBus();
printk("ScanAllBus: %s\n", err.Name());
for (int i = 0; i < pci::num_device; ++i) {
const auto& dev = pci::devices[i];
auto vendor_id = pci::ReadVendorId(dev.bus, dev.device, dev.function);
auto class_code = pci::ReadClassCode(dev.bus, dev.device, dev.function);
printk(
"%d.%d.%d: vend %04x, class %08x, head %02x / base[%02x], sub[%02x], "
"interface[%02x]\n",
dev.bus, dev.device, dev.function, vendor_id, class_code,
dev.header_type, class_code.base, class_code.sub, class_code.interface);
}

/** @brief PCI デバイスのクラスコード */
struct ClassCode {
uint8_t base, sub, interface;
/** @brief ベースクラスが等しい場合に真を返す */
bool Match(uint8_t b) { return b == base; }
/** @brief ベースクラスとサブクラスが等しい場合に真を返す */
bool Match(uint8_t b, uint8_t s) { return Match(b) && s == sub; }
/** @brief ベース,サブ,インターフェースが等しい場合に真を返す */
bool Match(uint8_t b, uint8_t s, uint8_t i) {
return Match(b, s) && i == interface;
}
};
ふむ。同一性の確認を関数化しているのか

とりあえず諸々エラーが出たので修正
-
Makefile
- オプションが増えたりしているので内容を確認
-
ld.lld: error: undefined symbol: __cxa_pure_virtual
main.cppに以下の関数を定義
extern "C" void __cxa_pure_virtual() {
while (1) __asm__("hlt");
}
ld.lld: error: undefined symbol: _exit
ld.lld: error: undefined symbol: kill
ld.lld: error: undefined symbol: getpid
newlib_support.cに追加
#include <errno.h>
#include <sys/types.h>
void _exit(void) {
while (1) __asm__("hlt");
}
caddr_t sbrk(int incr) {
errno = ENOMEM;
return (caddr_t)-1;
}
int getpid(void) {
return 1;
}
int kill(int pid, int sig) {
errno = EINVAL;
return -1;
}

あとはコードをそのまま流用でOK

これはメモを書くのが捗る