【RustでMikanOS!】USBマウスの取得【Day06b】
初めに
現在MikanOSをRustで挑戦しています。
ちなみに、MikanOSとは"ゼロからの OS 自作入門"という書籍上で実装するOSの名前です。
今回はday06bでやるPCIデバイスの探索と、その中からマウスを取得する処理の実装を行います。
書籍のほかに下記サイトを参考にしています。
OSDev-Pci
コンフィグレーションヘッダ
各PCIデバイスは256Bytesのコンフィグレーションヘッダを持ちます。
主に"Vendor ID"、"Class Code", "Sub Class"を読み取り、そのデバイスがマウスかどうかを判断します。詳しくは後程。
この空間を読み取るために2つのアクセス機構が提供されています。OSDevによると、メカニズム2は後方置換性のために残されているため、メカニズム1を推奨しているようです。
メカニズム1
OSDevの"Configuration Space Access Mechanism #1"が該当します。
このメカニズムは2つの手順から構成されます。
- コンフィグアドレスレジスタに読み込み先を表すデータを書き込む
- コンフィグデータレジスタから読み込み先のデータを読み込む
1では、デバイス、コンフィグレーション空間のオフセットなどを表すデータをレジスタに書き込みます。
このデータは以下に示す32Bitのデータ形式から構成されます。
Bit 31 | Bits 30-24 | Bits 23-16 | Bits 15-11 | Bits 10-8 | Bits 7-0 |
---|---|---|---|---|---|
Enable | 予約領域 | バス番号 | デバイスID | Function番号 | レジスタオフセット |
図にするとこのようになります。
1 コンフィグアドレスレジスタに読み込み先を表すデータを書き込む
2 コンフィグデータレジスタから読み込み先のデータを読み込む
コンフィグアドレスレジスタのコード
コンフィグアドレスレジスタのコードは以下になります。
as_data()は実際に書き込む際のデータを生成します。詳細はOSDevの"Configuration Space Access Mechanism #1"を参照してください。
ConfigAddrRegister
/// コンフィグアドレスレジスタに書き込むためのデータ
#[derive(Debug)]
pub struct ConfigAddrRegister {
register_offset: u32,
function: u32,
device_slot: u32,
bus: u32,
}
impl Debug for ConfigAddrRegister {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
write!(f, "config_addr=0b{:b}", self.as_data())
}
}
impl ConfigAddrRegister {
pub fn new(register_offset: u8, function: u8, device_slot: u8, bus: u8) -> Self {
Self {
register_offset: register_offset as u32,
function: function as u32,
device_slot: device_slot as u32,
bus: bus as u32,
}
}
pub fn bus(&self) -> u32 {
self.bus
}
pub fn device_slot(&self) -> u32 {
self.device_slot
}
pub fn function(&self) -> u32 {
self.function
}
pub fn register_offset(&self) -> u32 {
self.register_offset
}
pub fn register_offset_with_mask(&self) -> u32 {
// 下位2Bitは0である必要があるためビットマスク
self.register_offset() & 0xFC
}
pub fn as_data(&self) -> u32 {
let shift = |d: u32, shift_size: usize| (d << shift_size);
shift(1, 31)
| shift(self.bus(), 16)
| shift(self.device_slot(), 11)
| shift(self.function(), 8)
| self.register_offset_with_mask()
}
}
I/O命令
コンフィグアドレスレジスタとコンフィグデータレジスタにアクセスするには特殊な命令群を使う必要があります。
これらの命令はRustのコードでは表現できないため、アセンブラを使います。 Rustにはアセンブラを簡単に使うためにglobal_asm!というマクロがあるため、それを使います。
アセンブラのABIについては下記ページのX86-64を参照してください。
Out
global_asm!(
r#"
asm_io_out32: ; fn io_out32(addr: u16, data: u32)
mov dx, di ; 最初の引数部分(addr)がdiレジスタに書き込まれるため、dx=di
mov eax, esi ; 2番目の引数が(data)がesiレジスタに書き込まれるため、eax=esi
out dx, eax ; dxが保持するアドレスにeaxを出力
ret
"#
);
In
global_asm!(
r#"
asm_io_in32: fn io_in32(addr: u16) -> u32
mov dx, di ; dx = di
in eax, dx ; dxが保持するアドレスからeaxにデータを読み込む
ret ; eaxの値を返す
"#
);
Rustのコードで包む
次に、マクロで定義したアセンブラをRustのコードで包みます。
extern "C" {
/// 1 .dx=di
/// 2. eax=esi
/// 3. IOポートのアドレスdxにeaxを出力
/// 4. eaxの値がret(返り値になる)
fn asm_io_out32(addr: u16, data: u32);
/// 1 dx=di
/// 2.dxのIOポートアドレスの値をeaxに読み込む
/// 3.eaxの値がret(返り値になる)
fn asm_io_in32(addr: u16) -> u32;
}
fn io_out32(addr: u16, data: u32) {
unsafe { asm_io_out32(addr, data) }
}
fn io_in32(addr: u16) -> u32 {
unsafe { asm_io_in32(addr) }
}
コンフィグアドレスレジスタにout命令を伝えるための関数を定義しておきます。
このレジスタのアドレスは0x0CF8のため、それをio_out32の第1引数に指定すればいいですね。
pub fn write_config_addr(config_addr_register: ConfigAddrRegister) {
const CONFIG_ADDR_REGISTER_ADDR: u16 = 0x0CF8;
io_out32(CONFIG_ADDR_REGISTER_ADDR, config_addr_register.to_addr())
}
同様に、読み込む際はコンフィグデータレジスタからのため、次のような関数を定義しておきます。
pub fn fetch_config_data() -> u32 {
const CONFIG_DATA_REGISTER_ADDR: u16 = 0x0CFC;
io_in32(CONFIG_DATA_REGISTER_ADDR)
}
デバイス探索
OSDevの"Recursive Scan"の項目を参考に実装を進めました。
探索処理をするうえで重要になるのが主に以下です。
- デバイスが存在するか調べる必要があること
- バスにはデバイスが最大32個まで接続できること。
- デバイスがシングルファンクションかマルチファンクションの2種類があること、シングルファンクションはさらに3つに分類されること
それぞれについて説明します。
デバイスが存在するか調べる必要があること
VendorIdが0xFFの場合、そのデバイスは存在しないためイテレートの際はスキップします。
#[repr(transparent)]
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq)]
pub struct VendorId(u16);
impl VendorId {
pub fn new(vendor_id: u16) -> Self {
Self(vendor_id)
}
// この関数でチェックします。
pub fn valid_device(&self) -> bool {
self.0 != 0xFF
}
}
バスにはデバイスが最大32個まで接続できること。
同一のバスには最大32個までデバイスが接続できるため、探索する際は、指定したバス番号のデバイススロット0から31までをイテレートする必要があります。
このコードは以下になります。
#[derive(Debug)]
pub struct DeviceSlots {
bus: u8,
device_slot: u8,
}
impl Iterator for DeviceSlots {
type Item = Function;
fn next(&mut self) -> Option<Self::Item> {
const DEVICE_SLOT_SIZE: u8 = 32;
if DEVICE_SLOT_SIZE <= self.device_slot {
return None;
}
let config = ConfigurationSpace::try_new(self.bus, self.device_slot, 0);
let device = config.map(|c| c.cast_device());
self.device_slot += 1;
if let Some(device) = device {
return Some(device);
}
self.next()
}
}
デバイスがシングルファンクションかマルチファンクションの2種類があること、シングルファンクションはさらに3つに分類されること
HeaderTypeの最上位ビットが1のとき、そのデバイスはマルチファンクションで、最大8つの機能を持つことができます。
ファンクション番号はコンフィグアドレスレジスタのfunctionに対応します。
ビットが0の場合はシングルファンクションになります。
シングルファンクションの場合、更に以下の3つに分類されます。
- GeneralDevice
- PCI-toPCI Bridge
- PCI-to-CardBus Bridge(これは使用していません。)
PCI-toPCI Bridgeはコンフィグレーションヘッダ内にSecondaryBusNumberが宣言されているため、その番号のバスを探索する必要があります。
ファンクションタイプはNewPatternといわれる方法で専用の構造体を宣言して、そこから確認しています。
#[repr(transparent)]
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq)]
pub struct HeaderType(u8);
impl HeaderType {
pub fn new(header_type: u8) -> Self {
Self(header_type)
}
pub fn is_multiple_function(&self) -> bool {
last_bit(self.0) == 1
}
}
fn last_bit(header_type: u8) -> u8 {
header_type >> 7
}
以下はコンフィグレーション空間を取り扱う構造体と、ファンクション別にダウンキャスト(cast_device)するコードです。
CommonHeaderHoldable
pub trait CommonHeaderHoldable {
fn device_slot(&self) -> u16 {
convert_to_device_id(self.as_config_space().fetch_data_offset_at(0))
}
fn vendor_id(&self) -> VendorId {
VendorId::new(convert_to_vendor_id(
self.as_config_space().fetch_data_offset_at(0),
))
}
fn class_code(&self) -> ClassCode {
let code = self.as_config_space().fetch_data_offset_at(0x8);
ClassCode::try_from(convert_to_class_code(code)).unwrap_or(ClassCode::NoSupport)
}
fn sub_class(&self) -> Subclass {
let offset_8 = self.as_config_space().fetch_data_offset_at(0x08);
let sub_class = convert_to_sub_class(offset_8);
Subclass::from_class_code(self.class_code(), sub_class)
}
fn header_type(&self) -> HeaderType {
HeaderType::new(convert_to_header_type(
self.as_config_space().fetch_data_offset_at(0x0C),
))
}
fn as_config_space(&self) -> &ConfigurationSpace;
}
pub(crate) fn convert_to_vendor_id(data_offset_0: u32) -> u16 {
(data_offset_0 & 0xFF) as u16
}
pub(crate) fn convert_to_device_id(data_offset_0: u32) -> u16 {
(data_offset_0 >> 8) as u16
}
pub(crate) fn convert_to_class_code(data_offset_8: u32) -> u8 {
((data_offset_8 >> 24) & 0xFF) as u8
}
pub(crate) fn convert_to_sub_class(data_offset_8: u32) -> u8 {
((data_offset_8 >> 16) & 0xFF) as u8
}
pub(crate) fn convert_to_header_type(data_offset_c: u32) -> u8 {
((data_offset_c >> 16) & 0xff) as u8
}
ConfigurationSpace
#[derive(Clone, Debug)]
#[repr(C)]
pub struct ConfigurationSpace {
bus: u8,
device_slot: u8,
function: u8,
}
impl ConfigurationSpace {
pub fn try_new(bus: u8, device_slot: u8, function: u8) -> Option<Self> {
let me = ConfigurationSpace::new(bus, device_slot, function);
if me.vendor_id().valid_device() {
return Some(me);
} else {
None
}
}
pub fn cast_device(self) -> Function {
if self.header_type().is_multiple_function() {
Function::Multiple(MultipleFunctionDevice::new(self))
} else {
select_single_function_device(self)
}
}
pub fn bus(&self) -> u8 {
self.bus
}
pub fn device_slot(&self) -> u8 {
self.device_slot
}
pub fn function(&self) -> u8 {
self.function
}
pub(crate) fn fetch_data_offset_at(&self, offset: u8) -> u32 {
write_config_addr(self.config_addr_at(offset));
fetch_config_data()
}
fn new(bus: u8, device_slot: u8, function: u8) -> Self {
Self {
bus,
device_slot,
function,
}
}
fn config_addr_at(&self, offset: u8) -> ConfigAddrRegister {
ConfigAddrRegister::new(offset, self.function, self.device_slot, self.bus)
}
}
impl CommonHeaderHoldable for ConfigurationSpace {
fn as_config_space(&self) -> &ConfigurationSpace {
self
}
}
fn select_single_function_device(config_space: ConfigurationSpace) -> Function {
let device_header = if (config_space.class_code() == ClassCode::BridgeDevice)
&& (config_space.sub_class()) == Subclass::PciToPciBridge
{
SingleFunctionDevice::PciToPciBride(PciToPciBridgeHeader::new(config_space))
} else {
SingleFunctionDevice::General(GeneralHeader::new(config_space))
};
Single(device_header)
}
Functions
ファンクションを表すEnumや構造体
#[derive(Debug)]
pub enum Function {
Single(SingleFunctionDevice),
Multiple(MultipleFunctionDevice),
}
// single_function.rs
#[derive(Debug)]
pub enum SingleFunctionDevice {
General(GeneralHeader),
PciToPciBride(PciToPciBridgeHeader),
}
// multiple_function_device.rs
#[derive(Debug)]
pub struct MultipleFunctionDevice {
config_space: ConfigurationSpace,
function: u8,
}
impl MultipleFunctionDevice {
pub(crate) fn new(config_space: ConfigurationSpace) -> Self {
let function = config_space.function() + 1;
Self {
config_space,
function,
}
}
}
impl CommonHeaderHoldable for MultipleFunctionDevice {
fn as_config_space(&self) -> &ConfigurationSpace {
&self.config_space
}
}
impl Iterator for MultipleFunctionDevice {
type Item = Function;
fn next(&mut self) -> Option<Self::Item> {
const MAX_FUNCTION_SIZE: u8 = 8;
if MAX_FUNCTION_SIZE <= self.function {
return None;
}
let next_config_space = ConfigurationSpace::try_new(
self.config_space.bus(),
self.config_space.device_slot(),
self.function,
);
self.function += 1;
if let Some(next_config_space) = next_config_space.map(|c| c.cast_device()) {
return Some(next_config_space);
}
self.next()
}
}
デバイスの探索処理のコードを記します。
PciDeviceSearcher
pub struct PciDeviceSearcher {
vendor_id: Option<VendorId>,
device_slot: Option<u16>,
sub_class: Option<Subclass>,
class_code: Option<ClassCode>,
}
impl PciDeviceSearcher {
pub fn new() -> Self {
Self {
vendor_id: None,
device_slot: None,
sub_class: None,
class_code: None,
}
}
pub fn vendor_id(mut self, vendor_id: VendorId) -> Self {
self.vendor_id = Some(vendor_id);
self
}
pub fn device_slot(mut self, device_slot: u16) -> Self {
self.device_slot = Some(device_slot);
self
}
pub fn sub_class(mut self, sub_class: Subclass) -> Self {
self.sub_class = Some(sub_class);
self
}
pub fn class_code(mut self, class_code: ClassCode) -> Self {
self.class_code = Some(class_code);
self
}
pub fn search(&self) -> Option<ConfigurationSpace> {
find_pci_device_with(self)
}
}
fn find_pci_device_with(target: &PciDeviceSearcher) -> Option<ConfigurationSpace> {
find_from_device_slots(DeviceSlots::new(0), target)
}
fn find_from_device_slots(
mut device_slot: DeviceSlots,
target: &PciDeviceSearcher,
) -> Option<ConfigurationSpace> {
device_slot.find_map(|function| find_from_function(target, function))
}
fn find_from_function(
target: &PciDeviceSearcher,
function: Function,
) -> Option<ConfigurationSpace> {
match function {
Function::Single(single) => find_from_single(target, single),
Function::Multiple(multiple) => find_from_multiple(target, multiple),
}
}
fn find_from_single(
target: &PciDeviceSearcher,
single: SingleFunctionDevice,
) -> Option<ConfigurationSpace> {
match single {
SingleFunctionDevice::General(device) => get_if_target_device(target, &device),
SingleFunctionDevice::PciToPciBride(bridge) => find_within_bridge(target, bridge),
}
}
fn find_from_multiple(
target: &PciDeviceSearcher,
mut multiple_function_device: MultipleFunctionDevice,
) -> Option<ConfigurationSpace> {
multiple_function_device.find_map(|function| find_from_function(target, function))
}
fn find_within_bridge(
target: &PciDeviceSearcher,
bridge: PciToPciBridgeHeader,
) -> Option<ConfigurationSpace> {
if let Some(device) = get_if_target_device(target, &bridge) {
return Some(device);
}
bridge
.children()
.find_map(|function| find_from_function(target, function))
}
fn get_if_target_device(
target: &PciDeviceSearcher,
device: &impl CommonHeaderHoldable,
) -> Option<ConfigurationSpace> {
if let Some(vendor_id) = &target.vendor_id {
if device.vendor_id() != *vendor_id {
return None;
}
}
if let Some(device_slot) = &target.device_slot {
if device.device_slot() != *device_slot {
return None;
}
}
if let Some(class_code) = &target.class_code {
if device.class_code() != *class_code {
return None;
}
}
if let Some(sub_class) = &target.sub_class {
if device.sub_class() != *sub_class {
return None;
}
}
Some(device.as_config_space().clone())
}
ここまで出来たら後はカーネル側でサーチャーの呼び出しをするだけです。
// kernel/main.rs
let mouse = PciDeviceSearcher::new()
.class_code(ClassCode::SerialBus)
.sub_class(Subclass::Usb)
.search();
println!("find mouse = {:?}", mouse);
Qemuで実行した結果、ちゃんとマウスが取得できていることが確認できました!
次回
次回からはマウスドライバを実装していきます。
参考文献
Discussion