feat: Initialize Local APIC and IO APIC
This commit is contained in:
parent
b277334069
commit
5313892f80
@ -35,6 +35,9 @@ lazy_static! {
|
||||
idt.simd_floating_point.set_handler_fn(simd_floating_point_handler);
|
||||
idt.virtualization.set_handler_fn(virtualization_handler);
|
||||
|
||||
// IRQ 32 start
|
||||
idt[33].set_handler_fn(keyboard_interrupt_handler); // IRQ 1 Keyboard
|
||||
|
||||
idt
|
||||
};
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ use x86_64::structures::idt::{InterruptStackFrame, PageFaultErrorCode};
|
||||
use x86_64::VirtAddr;
|
||||
use crate::kprintln;
|
||||
use crate::{log_trace, log_debug, log_info, log_warn, log_error, log_fatal};
|
||||
use crate::hal::cpu;
|
||||
use crate::hal::{cpu, lapic};
|
||||
use crate::mm::paging;
|
||||
|
||||
/// Divide Error (#DE)
|
||||
@ -224,4 +224,20 @@ pub extern "x86-interrupt" fn virtualization_handler(stack_frame: InterruptStack
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Timer interrupt, Keyboard interrupt
|
||||
// TODO Timer interrupt, Keyboard interrupt
|
||||
|
||||
pub extern "x86-interrupt" fn keyboard_interrupt_handler(stack_frame: InterruptStackFrame) {
|
||||
use x86_64::instructions::port::Port;
|
||||
|
||||
unsafe {
|
||||
// 读取键盘扫描码
|
||||
let mut port = Port::new(0x60);
|
||||
let scancode: u8 = port.read();
|
||||
|
||||
// 传递给键盘驱动处理
|
||||
crate::drivers::keyboard::handle_scancode(scancode);
|
||||
}
|
||||
|
||||
// 发送 EOI
|
||||
lapic::send_eoi();
|
||||
}
|
||||
184
kernel/src/drivers/keyboard.rs
Normal file
184
kernel/src/drivers/keyboard.rs
Normal file
@ -0,0 +1,184 @@
|
||||
// kernel/src/drivers/keyboard.rs
|
||||
use spin::Mutex;
|
||||
use crate::{kprintln, log_debug, log_info};
|
||||
|
||||
/// Keyboard scancode to ASCII mapping table (US keyboard layout)
|
||||
static SCANCODE_TO_ASCII: [u8; 128] = [
|
||||
0, 27, b'1', b'2', b'3', b'4', b'5', b'6', // 0x00-0x07
|
||||
b'7', b'8', b'9', b'0', b'-', b'=', 8, b'\t', // 0x08-0x0F (8=Backspace)
|
||||
b'q', b'w', b'e', b'r', b't', b'y', b'u', b'i', // 0x10-0x17
|
||||
b'o', b'p', b'[', b']', b'\n', 0, b'a', b's', // 0x18-0x1F (Ctrl)
|
||||
b'd', b'f', b'g', b'h', b'j', b'k', b'l', b';', // 0x20-0x27
|
||||
b'\'',b'`', 0, b'\\',b'z', b'x', b'c', b'v', // 0x28-0x2F (LShift)
|
||||
b'b', b'n', b'm', b',', b'.', b'/', 0, b'*', // 0x30-0x37 (RShift)
|
||||
0, b' ', 0, 0, 0, 0, 0, 0, // 0x38-0x3F (Alt, CapsLock, F1-F5)
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 0x40-0x47 (F6-F10, NumLock, ScrollLock)
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 0x48-0x4F (Home, Up, PgUp, -, Left, ...)
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 0x50-0x57
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 0x58-0x5F
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 0x60-0x67
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 0x68-0x6F
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 0x70-0x77
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 0x78-0x7F
|
||||
];
|
||||
|
||||
/// Character mapping when Shift key is pressed
|
||||
static SCANCODE_TO_ASCII_SHIFT: [u8; 128] = [
|
||||
0, 27, b'!', b'@', b'#', b'$', b'%', b'^', // 0x00-0x07
|
||||
b'&', b'*', b'(', b')', b'_', b'+', 8, b'\t', // 0x08-0x0F
|
||||
b'Q', b'W', b'E', b'R', b'T', b'Y', b'U', b'I', // 0x10-0x17
|
||||
b'O', b'P', b'{', b'}', b'\n', 0, b'A', b'S', // 0x18-0x1F
|
||||
b'D', b'F', b'G', b'H', b'J', b'K', b'L', b':', // 0x20-0x27
|
||||
b'"', b'~', 0, b'|', b'Z', b'X', b'C', b'V', // 0x28-0x2F
|
||||
b'B', b'N', b'M', b'<', b'>', b'?', 0, b'*', // 0x30-0x37
|
||||
0, b' ', 0, 0, 0, 0, 0, 0, // 0x38-0x3F
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 0x40-0x47
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 0x48-0x4F
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 0x50-0x57
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 0x58-0x5F
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 0x60-0x67
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 0x68-0x6F
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 0x70-0x77
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // 0x78-0x7F
|
||||
];
|
||||
|
||||
/// Numpad scancode mapping
|
||||
static NUMPAD_SCANCODE_TO_ASCII: [u8; 128] = {
|
||||
let mut map = [0u8; 128];
|
||||
map[0x47] = b'7';
|
||||
map[0x48] = b'8';
|
||||
map[0x49] = b'9';
|
||||
map[0x4B] = b'4';
|
||||
map[0x4C] = b'5';
|
||||
map[0x4D] = b'6';
|
||||
map[0x4F] = b'1';
|
||||
map[0x50] = b'2';
|
||||
map[0x51] = b'3';
|
||||
map[0x52] = b'0';
|
||||
map[0x53] = b'.';
|
||||
map[0x4A] = b'-';
|
||||
map[0x4E] = b'+';
|
||||
map[0x37] = b'*';
|
||||
map[0x35] = b'/';
|
||||
map
|
||||
};
|
||||
|
||||
/// Keyboard state
|
||||
struct KeyboardState {
|
||||
shift_pressed: bool,
|
||||
ctrl_pressed: bool,
|
||||
alt_pressed: bool,
|
||||
caps_lock: bool,
|
||||
num_lock: bool,
|
||||
}
|
||||
|
||||
impl KeyboardState {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
shift_pressed: false,
|
||||
ctrl_pressed: false,
|
||||
alt_pressed: false,
|
||||
caps_lock: false,
|
||||
num_lock: true, // Usually enabled by default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static KEYBOARD_STATE: Mutex<KeyboardState> = Mutex::new(KeyboardState::new());
|
||||
|
||||
/// Handle keyboard scancode
|
||||
pub fn handle_scancode(raw: u8) {
|
||||
let mut state = KEYBOARD_STATE.lock();
|
||||
|
||||
let key_released = (raw & 0x80) != 0;
|
||||
let scancode = raw & 0x7F;
|
||||
|
||||
// Handle modifier keys
|
||||
match scancode {
|
||||
0x2A | 0x36 => { state.shift_pressed = !key_released; return; } // Shift
|
||||
0x1D => { state.ctrl_pressed = !key_released; return; } // Ctrl
|
||||
0x38 => { state.alt_pressed = !key_released; return; } // Alt
|
||||
0x3A => { if !key_released { state.caps_lock = !state.caps_lock; } return; } // Caps Lock
|
||||
0x45 => { if !key_released { state.num_lock = !state.num_lock; } return; } // Num Lock
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if key_released { return; }
|
||||
|
||||
// When NumLock is off: Numpad outputs arrow or control keys
|
||||
if !state.num_lock {
|
||||
match scancode {
|
||||
0x47 => { kprintln!("[Home]"); return; }
|
||||
0x48 => { kprintln!("[Up]"); return; }
|
||||
0x49 => { kprintln!("[PgUp]"); return; }
|
||||
0x4B => { kprintln!("[Left]"); return; }
|
||||
0x4C => { kprintln!("[Center]"); return; }
|
||||
0x4D => { kprintln!("[Right]"); return; }
|
||||
0x4F => { kprintln!("[End]"); return; }
|
||||
0x50 => { kprintln!("[Down]"); return; }
|
||||
0x51 => { kprintln!("[PgDn]"); return; }
|
||||
0x52 => { kprintln!("[Insert]"); return; }
|
||||
0x53 => { kprintln!("[Delete]"); return; }
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle numpad output based on NumLock state
|
||||
let ascii = if state.num_lock && NUMPAD_SCANCODE_TO_ASCII[scancode as usize] != 0 {
|
||||
NUMPAD_SCANCODE_TO_ASCII[scancode as usize]
|
||||
} else if state.shift_pressed {
|
||||
SCANCODE_TO_ASCII_SHIFT[scancode as usize]
|
||||
} else {
|
||||
SCANCODE_TO_ASCII[scancode as usize]
|
||||
};
|
||||
|
||||
if ascii == 0 {
|
||||
return; // Unmapped key
|
||||
}
|
||||
|
||||
// Handle Caps Lock (affects letters only)
|
||||
let ascii = if state.caps_lock && ascii.is_ascii_alphabetic() {
|
||||
if state.shift_pressed {
|
||||
ascii.to_ascii_lowercase()
|
||||
} else {
|
||||
ascii.to_ascii_uppercase()
|
||||
}
|
||||
} else {
|
||||
ascii
|
||||
};
|
||||
|
||||
// Handle Ctrl combinations
|
||||
if state.ctrl_pressed {
|
||||
match ascii {
|
||||
b'c' | b'C' => { kprintln!("^C"); return; }
|
||||
b'd' | b'D' => { kprintln!("^D"); return; }
|
||||
b'l' | b'L' => {
|
||||
crate::tty::tty::clear(0x000000);
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Output character
|
||||
print_char(ascii);
|
||||
}
|
||||
|
||||
/// Print a character to the screen
|
||||
fn print_char(c: u8) {
|
||||
use crate::kprint;
|
||||
|
||||
if c == b'\n' {
|
||||
kprintln!();
|
||||
} else if c == 8 {
|
||||
// TODO: Implement backspace functionality
|
||||
kprint!("\x08");
|
||||
} else if c.is_ascii_graphic() || c == b' ' {
|
||||
kprint!("{}", c as char);
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize keyboard driver
|
||||
pub fn init() {
|
||||
log_info!("Keyboard driver initialized");
|
||||
}
|
||||
1
kernel/src/drivers/mod.rs
Normal file
1
kernel/src/drivers/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod keyboard;
|
||||
@ -12,6 +12,16 @@ pub struct CureAcpiHandler {
|
||||
physical_memory_offset: u64,
|
||||
}
|
||||
|
||||
pub struct AcpiInfo {
|
||||
pub revision: u8,
|
||||
pub boot_processor: Option<u32>,
|
||||
pub cpu_count: usize,
|
||||
pub has_apic: bool,
|
||||
pub has_hpet: bool,
|
||||
pub local_apic_address: Option<u64>,
|
||||
pub io_apics: alloc::vec::Vec<(u64, u8, u32)>, // (address, id, gsi_base)
|
||||
}
|
||||
|
||||
impl CureAcpiHandler {
|
||||
pub const fn new(physical_memory_offset: u64) -> Self {
|
||||
Self {
|
||||
@ -187,14 +197,6 @@ impl Handler for CureAcpiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AcpiInfo {
|
||||
pub revision: u8,
|
||||
pub boot_processor: Option<u32>,
|
||||
pub cpu_count: usize,
|
||||
pub has_apic: bool,
|
||||
pub has_hpet: bool,
|
||||
}
|
||||
|
||||
pub fn init(rsdp_addr: u64, physical_memory_offset: u64) -> Option<AcpiInfo> {
|
||||
|
||||
let handler = CureAcpiHandler::new(physical_memory_offset);
|
||||
@ -214,7 +216,6 @@ pub fn init(rsdp_addr: u64, physical_memory_offset: u64) -> Option<AcpiInfo> {
|
||||
}
|
||||
}
|
||||
};
|
||||
// kprintln!(" ACPI Revision: {}", tables.rsdp_revision);
|
||||
|
||||
let platform = match AcpiPlatform::new(tables, handler) {
|
||||
Ok(platform) => platform,
|
||||
@ -240,25 +241,33 @@ pub fn init(rsdp_addr: u64, physical_memory_offset: u64) -> Option<AcpiInfo> {
|
||||
};
|
||||
|
||||
// 檢查中斷模型
|
||||
let has_apic = match &platform.interrupt_model {
|
||||
let (has_apic, local_apic_addr, io_apics_info) = match &platform.interrupt_model {
|
||||
InterruptModel::Apic(apic) => {
|
||||
log_info!("Local APIC Address: {:#x}", apic.local_apic_address);
|
||||
log_info!("IO APICs: {} controller(s)", apic.io_apics.len());
|
||||
|
||||
let mut io_apics = alloc::vec::Vec::new();
|
||||
|
||||
for (i, io_apic) in apic.io_apics.iter().enumerate() {
|
||||
log_info!("IO APIC {}: ID={}, Address={:#x}, GSI Base={}",
|
||||
i, io_apic.id, io_apic.address, io_apic.global_system_interrupt_base);
|
||||
|
||||
io_apics.push((
|
||||
io_apic.address as u64,
|
||||
io_apic.id,
|
||||
io_apic.global_system_interrupt_base,
|
||||
));
|
||||
}
|
||||
|
||||
true
|
||||
(true, Some(apic.local_apic_address as u64), io_apics)
|
||||
}
|
||||
InterruptModel::Unknown => {
|
||||
log_warn!("Interrupt Model: Unknown (not APIC)");
|
||||
false
|
||||
(false, None, alloc::vec::Vec::new())
|
||||
}
|
||||
_ => {
|
||||
log_warn!("Interrupt Model: Other");
|
||||
false
|
||||
(false, None, alloc::vec::Vec::new())
|
||||
}
|
||||
};
|
||||
|
||||
@ -299,6 +308,8 @@ pub fn init(rsdp_addr: u64, physical_memory_offset: u64) -> Option<AcpiInfo> {
|
||||
cpu_count,
|
||||
has_apic,
|
||||
has_hpet,
|
||||
local_apic_address: local_apic_addr,
|
||||
io_apics: io_apics_info,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,306 @@
|
||||
// kernel/src/hal/ioapic.rs
|
||||
use x86_64::{PhysAddr, VirtAddr};
|
||||
use spin::Mutex;
|
||||
use alloc::vec::Vec;
|
||||
use crate::{log_trace, log_debug, log_info, log_warn, log_error};
|
||||
use crate::mm::vma;
|
||||
|
||||
/// IO APIC register selector
|
||||
const IOREGSEL: u32 = 0x00;
|
||||
const IOWIN: u32 = 0x10;
|
||||
|
||||
/// IO APIC register index
|
||||
#[allow(dead_code)]
|
||||
mod reg {
|
||||
pub const ID: u8 = 0x00;
|
||||
pub const VER: u8 = 0x01;
|
||||
pub const ARB: u8 = 0x02;
|
||||
pub const REDTBL_BASE: u8 = 0x10;
|
||||
}
|
||||
|
||||
/// Redirection Entry flag
|
||||
#[allow(dead_code)]
|
||||
mod redir_flags {
|
||||
pub const MASKED: u64 = 1 << 16;
|
||||
pub const TRIGGER_LEVEL: u64 = 1 << 15;
|
||||
pub const TRIGGER_EDGE: u64 = 0;
|
||||
pub const POLARITY_LOW: u64 = 1 << 13;
|
||||
pub const POLARITY_HIGH: u64 = 0;
|
||||
pub const DEST_LOGICAL: u64 = 1 << 11;
|
||||
pub const DEST_PHYSICAL: u64 = 0;
|
||||
pub const DELIVERY_FIXED: u64 = 0 << 8;
|
||||
pub const DELIVERY_LOWEST: u64 = 1 << 8;
|
||||
}
|
||||
|
||||
pub struct IoApic {
|
||||
base_vaddr: VirtAddr,
|
||||
id: u8,
|
||||
gsi_base: u32,
|
||||
max_redirection_entries: u8,
|
||||
}
|
||||
|
||||
static IO_APICS: Mutex<Vec<IoApic>> = Mutex::new(Vec::new());
|
||||
|
||||
impl IoApic {
|
||||
/// Create IO APIC from physical address
|
||||
pub unsafe fn new(base_paddr: PhysAddr, id: u8, gsi_base: u32) -> Self {
|
||||
let base_vaddr = vma::phys_to_virt(base_paddr.as_u64());
|
||||
|
||||
log_debug!("IO APIC {} physical base: {:#x}", id, base_paddr.as_u64());
|
||||
log_debug!("IO APIC {} virtual base: {:#x}", id, base_vaddr.as_u64());
|
||||
log_debug!("IO APIC {} GSI base: {}", id, gsi_base);
|
||||
|
||||
let mut ioapic = Self {
|
||||
base_vaddr,
|
||||
id,
|
||||
gsi_base,
|
||||
max_redirection_entries: 0,
|
||||
};
|
||||
|
||||
// Read version information to get the maximum number of redirect entries
|
||||
let version = ioapic.read(reg::VER);
|
||||
ioapic.max_redirection_entries = ((version >> 16) & 0xFF) as u8 + 1;
|
||||
|
||||
ioapic
|
||||
}
|
||||
|
||||
/// Read IO APIC registers
|
||||
unsafe fn read(&mut self, reg: u8) -> u32 {
|
||||
let regsel_addr = self.base_vaddr.as_u64() + IOREGSEL as u64;
|
||||
let win_addr = self.base_vaddr.as_u64() + IOWIN as u64;
|
||||
|
||||
core::ptr::write_volatile(regsel_addr as *mut u32, reg as u32);
|
||||
core::ptr::read_volatile(win_addr as *const u32)
|
||||
}
|
||||
|
||||
/// Writing to IO APIC registers
|
||||
unsafe fn write(&mut self, reg: u8, value: u32) {
|
||||
let regsel_addr = self.base_vaddr.as_u64() + IOREGSEL as u64;
|
||||
let win_addr = self.base_vaddr.as_u64() + IOWIN as u64;
|
||||
|
||||
core::ptr::write_volatile(regsel_addr as *mut u32, reg as u32);
|
||||
core::ptr::write_volatile(win_addr as *mut u32, value);
|
||||
}
|
||||
|
||||
/// Read redirection table entries
|
||||
unsafe fn read_redirection_entry(&mut self, irq: u8) -> u64 {
|
||||
if irq >= self.max_redirection_entries {
|
||||
log_warn!("IRQ {} out of range for IO APIC {}", irq, self.id);
|
||||
return 0;
|
||||
}
|
||||
|
||||
let low_reg = reg::REDTBL_BASE + (irq * 2);
|
||||
let high_reg = low_reg + 1;
|
||||
|
||||
let low = self.read(low_reg) as u64;
|
||||
let high = self.read(high_reg) as u64;
|
||||
|
||||
(high << 32) | low
|
||||
}
|
||||
|
||||
/// Write redirection table entry
|
||||
unsafe fn write_redirection_entry(&mut self, irq: u8, entry: u64) {
|
||||
if irq >= self.max_redirection_entries {
|
||||
log_warn!("IRQ {} out of range for IO APIC {}", irq, self.id);
|
||||
return;
|
||||
}
|
||||
|
||||
let low_reg = reg::REDTBL_BASE + (irq * 2);
|
||||
let high_reg = low_reg + 1;
|
||||
|
||||
let low = entry as u32;
|
||||
let high = (entry >> 32) as u32;
|
||||
|
||||
self.write(high_reg, high);
|
||||
self.write(low_reg, low);
|
||||
}
|
||||
|
||||
/// Initialize the IO APIC
|
||||
pub unsafe fn init(&mut self) {
|
||||
// Mask all interrupts
|
||||
for irq in 0..self.max_redirection_entries {
|
||||
let entry = redir_flags::MASKED;
|
||||
self.write_redirection_entry(irq, entry);
|
||||
}
|
||||
|
||||
log_info!("IO APIC {} initialized, {} entries", self.id, self.max_redirection_entries);
|
||||
}
|
||||
|
||||
/// Configure IRQ redirection
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `irq`: IRQ number (0-23)
|
||||
/// - `vector`: Interrupt vector number (32-255)
|
||||
/// - `dest_apic_id`: Destination Local APIC ID
|
||||
/// - `level_triggered`: true = level triggered, false = edge triggered
|
||||
/// - `active_low`: true = active low, false = active high
|
||||
pub unsafe fn set_irq_redirect(
|
||||
&mut self,
|
||||
irq: u8,
|
||||
vector: u8,
|
||||
dest_apic_id: u8,
|
||||
level_triggered: bool,
|
||||
active_low: bool,
|
||||
) {
|
||||
let mut entry: u64 = 0;
|
||||
|
||||
// Set target APIC ID (bits 56-63)
|
||||
entry |= (dest_apic_id as u64) << 56;
|
||||
|
||||
// Set the trigger mode
|
||||
if level_triggered {
|
||||
entry |= redir_flags::TRIGGER_LEVEL;
|
||||
}
|
||||
|
||||
// Setting Polarity
|
||||
if active_low {
|
||||
entry |= redir_flags::POLARITY_LOW;
|
||||
}
|
||||
|
||||
// Set the delivery mode to Fixed
|
||||
entry |= redir_flags::DELIVERY_FIXED;
|
||||
|
||||
// Set the target mode to Physical
|
||||
entry |= redir_flags::DEST_PHYSICAL;
|
||||
|
||||
// Set vector number
|
||||
entry |= vector as u64;
|
||||
|
||||
// Write redirection table (unmask)
|
||||
self.write_redirection_entry(irq, entry);
|
||||
|
||||
log_info!(
|
||||
"IO APIC {} IRQ {} -> Vector {} (APIC {}, {}, {})",
|
||||
self.id,
|
||||
irq,
|
||||
vector,
|
||||
dest_apic_id,
|
||||
if level_triggered { "level" } else { "edge" },
|
||||
if active_low { "low" } else { "high" }
|
||||
);
|
||||
}
|
||||
|
||||
/// masked IRQ
|
||||
pub unsafe fn mask_irq(&mut self, irq: u8) {
|
||||
let mut entry = self.read_redirection_entry(irq);
|
||||
entry |= redir_flags::MASKED;
|
||||
self.write_redirection_entry(irq, entry);
|
||||
|
||||
log_debug!("IO APIC {} IRQ {} masked", self.id, irq);
|
||||
}
|
||||
|
||||
/// unmasked IRQ
|
||||
pub unsafe fn unmask_irq(&mut self, irq: u8) {
|
||||
let mut entry = self.read_redirection_entry(irq);
|
||||
entry &= !redir_flags::MASKED;
|
||||
self.write_redirection_entry(irq, entry);
|
||||
|
||||
log_debug!("IO APIC {} IRQ {} unmasked", self.id, irq);
|
||||
}
|
||||
|
||||
/// Print IO APIC information
|
||||
pub unsafe fn print_info(&mut self) {
|
||||
let id = self.read(reg::ID) >> 24;
|
||||
let version = self.read(reg::VER);
|
||||
let apic_ver = version & 0xFF;
|
||||
|
||||
log_info!("IO APIC {} ID: {}", self.id, id);
|
||||
log_info!("IO APIC {} Version: {:#x}", self.id, apic_ver);
|
||||
log_info!("IO APIC {} Max Redirection Entries: {}", self.id, self.max_redirection_entries);
|
||||
log_info!("IO APIC {} GSI Base: {}", self.id, self.gsi_base);
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize a single IO APIC (using mapped virtual address)
|
||||
pub unsafe fn init_single_ioapic(base_vaddr: VirtAddr, id: u8, gsi_base: u32) {
|
||||
let mut ioapic = IoApic {
|
||||
base_vaddr,
|
||||
id,
|
||||
gsi_base,
|
||||
max_redirection_entries: 0,
|
||||
};
|
||||
|
||||
// Read version information to get the maximum number of redirect entries
|
||||
let version = ioapic.read(reg::VER);
|
||||
ioapic.max_redirection_entries = ((version >> 16) & 0xFF) as u8 + 1;
|
||||
|
||||
ioapic.init();
|
||||
ioapic.print_info();
|
||||
|
||||
IO_APICS.lock().push(ioapic);
|
||||
}
|
||||
|
||||
/// Initialize all IO APICs (from physical address, need to be mapped first)
|
||||
pub unsafe fn init_io_apics(io_apics: &[(PhysAddr, u8, u32)]) {
|
||||
let mut apics = IO_APICS.lock();
|
||||
|
||||
for (paddr, id, gsi_base) in io_apics {
|
||||
let mut ioapic = IoApic::new(*paddr, *id, *gsi_base);
|
||||
ioapic.init();
|
||||
ioapic.print_info();
|
||||
apics.push(ioapic);
|
||||
}
|
||||
|
||||
log_info!("All IO APICs initialized");
|
||||
}
|
||||
|
||||
/// Configuring IRQ Redirection
|
||||
pub fn set_irq_redirect(
|
||||
irq: u8,
|
||||
vector: u8,
|
||||
dest_apic_id: u8,
|
||||
level_triggered: bool,
|
||||
active_low: bool,
|
||||
) {
|
||||
unsafe {
|
||||
let mut apics = IO_APICS.lock();
|
||||
|
||||
// Find the IO APIC responsible for this IRQ
|
||||
for ioapic in apics.iter_mut() {
|
||||
let gsi_start = ioapic.gsi_base as u8;
|
||||
let gsi_end = gsi_start + ioapic.max_redirection_entries;
|
||||
|
||||
if irq >= gsi_start && irq < gsi_end {
|
||||
let local_irq = irq - gsi_start;
|
||||
ioapic.set_irq_redirect(local_irq, vector, dest_apic_id, level_triggered, active_low);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
log_warn!("No IO APIC found for IRQ {}", irq);
|
||||
}
|
||||
}
|
||||
|
||||
/// Block IRQ
|
||||
pub fn mask_irq(irq: u8) {
|
||||
unsafe {
|
||||
let mut apics = IO_APICS.lock();
|
||||
for ioapic in apics.iter_mut() {
|
||||
let gsi_start = ioapic.gsi_base as u8;
|
||||
let gsi_end = gsi_start + ioapic.max_redirection_entries;
|
||||
|
||||
if irq >= gsi_start && irq < gsi_end {
|
||||
let local_irq = irq - gsi_start;
|
||||
ioapic.mask_irq(local_irq);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Unmask IRQ
|
||||
pub fn unmask_irq(irq: u8) {
|
||||
unsafe {
|
||||
let mut apics = IO_APICS.lock();
|
||||
for ioapic in apics.iter_mut() {
|
||||
let gsi_start = ioapic.gsi_base as u8;
|
||||
let gsi_end = gsi_start + ioapic.max_redirection_entries;
|
||||
|
||||
if irq >= gsi_start && irq < gsi_end {
|
||||
let local_irq = irq - gsi_start;
|
||||
ioapic.unmask_irq(local_irq);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
190
kernel/src/hal/lapic.rs
Normal file
190
kernel/src/hal/lapic.rs
Normal file
@ -0,0 +1,190 @@
|
||||
// kernel/src/hal/lapic.rs
|
||||
use x86_64::{PhysAddr, VirtAddr};
|
||||
use spin::Mutex;
|
||||
use crate::{log_trace, log_debug, log_info, log_warn, log_error};
|
||||
use crate::mm::vma;
|
||||
|
||||
/// Local APIC register offset
|
||||
#[repr(u32)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[allow(dead_code)]
|
||||
enum ApicRegister {
|
||||
Id = 0x20,
|
||||
Version = 0x30,
|
||||
TaskPriority = 0x80,
|
||||
ProcessorPriority = 0xA0,
|
||||
Eoi = 0xB0,
|
||||
LogicalDestination = 0xD0,
|
||||
DestinationFormat = 0xE0,
|
||||
SpuriousInterruptVector = 0xF0,
|
||||
ErrorStatus = 0x280,
|
||||
LvtTimer = 0x320,
|
||||
LvtThermalSensor = 0x330,
|
||||
LvtPerformanceCounter = 0x340,
|
||||
LvtLint0 = 0x350,
|
||||
LvtLint1 = 0x360,
|
||||
LvtError = 0x370,
|
||||
TimerInitialCount = 0x380,
|
||||
TimerCurrentCount = 0x390,
|
||||
TimerDivideConfig = 0x3E0,
|
||||
}
|
||||
|
||||
/// APIC configuration flags
|
||||
#[allow(dead_code)]
|
||||
mod flags {
|
||||
pub const APIC_ENABLE: u32 = 0x100;
|
||||
pub const APIC_SW_ENABLE: u32 = 0x100;
|
||||
pub const APIC_SPURIOUS_ALL: u32 = 0xFF;
|
||||
|
||||
pub const LVT_MASKED: u32 = 1 << 16;
|
||||
pub const LVT_TIMER_PERIODIC: u32 = 1 << 17;
|
||||
}
|
||||
|
||||
pub struct LocalApic {
|
||||
base_vaddr: VirtAddr,
|
||||
}
|
||||
|
||||
static LOCAL_APIC: Mutex<Option<LocalApic>> = Mutex::new(None);
|
||||
|
||||
impl LocalApic {
|
||||
/// Create Local APIC from physical address
|
||||
pub unsafe fn new(base_paddr: PhysAddr) -> Self {
|
||||
let base_vaddr = vma::phys_to_virt(base_paddr.as_u64());
|
||||
|
||||
log_debug!("Local APIC physical base: {:#x}", base_paddr.as_u64());
|
||||
log_debug!("Local APIC virtual base: {:#x}", base_vaddr.as_u64());
|
||||
|
||||
Self { base_vaddr }
|
||||
}
|
||||
|
||||
/// Read APIC registers
|
||||
unsafe fn read(&self, reg: ApicRegister) -> u32 {
|
||||
let addr = self.base_vaddr.as_u64() + reg as u64;
|
||||
core::ptr::read_volatile(addr as *const u32)
|
||||
}
|
||||
|
||||
/// Write to APIC register
|
||||
unsafe fn write(&mut self, reg: ApicRegister, value: u32) {
|
||||
let addr = self.base_vaddr.as_u64() + reg as u64;
|
||||
core::ptr::write_volatile(addr as *mut u32, value);
|
||||
}
|
||||
|
||||
/// Initialize Local APIC
|
||||
pub unsafe fn init(&mut self) {
|
||||
// Enable APIC (via Spurious Interrupt Vector Register)
|
||||
let spurious = flags::APIC_SW_ENABLE | 0xFF; // IRQ 0xFF 作为 spurious vector
|
||||
self.write(ApicRegister::SpuriousInterruptVector, spurious);
|
||||
|
||||
// Set task priority to 0 (accept all interrupts)
|
||||
self.write(ApicRegister::TaskPriority, 0);
|
||||
|
||||
// Configure LVT entries - mask all local interrupts by default
|
||||
self.write(ApicRegister::LvtTimer, flags::LVT_MASKED);
|
||||
self.write(ApicRegister::LvtLint0, flags::LVT_MASKED);
|
||||
self.write(ApicRegister::LvtLint1, flags::LVT_MASKED);
|
||||
self.write(ApicRegister::LvtError, flags::LVT_MASKED);
|
||||
self.write(ApicRegister::LvtPerformanceCounter, flags::LVT_MASKED);
|
||||
self.write(ApicRegister::LvtThermalSensor, flags::LVT_MASKED);
|
||||
|
||||
log_info!("Local APIC initialized");
|
||||
}
|
||||
|
||||
/// Obtaining the APIC ID
|
||||
pub unsafe fn id(&self) -> u32 {
|
||||
self.read(ApicRegister::Id) >> 24
|
||||
}
|
||||
|
||||
/// Get the APIC version
|
||||
pub unsafe fn version(&self) -> u32 {
|
||||
self.read(ApicRegister::Version)
|
||||
}
|
||||
|
||||
/// Send EOI (End of Interrupt)
|
||||
pub unsafe fn send_eoi(&mut self) {
|
||||
self.write(ApicRegister::Eoi, 0);
|
||||
}
|
||||
|
||||
/// Configuring Timers
|
||||
pub unsafe fn setup_timer(&mut self, vector: u8, divide_config: u32, initial_count: u32) {
|
||||
// Setting the crossover
|
||||
self.write(ApicRegister::TimerDivideConfig, divide_config);
|
||||
|
||||
// Setting the LVT Timer (periodic mode)
|
||||
let lvt = flags::LVT_TIMER_PERIODIC | (vector as u32);
|
||||
self.write(ApicRegister::LvtTimer, lvt);
|
||||
|
||||
// Set the initial count
|
||||
self.write(ApicRegister::TimerInitialCount, initial_count);
|
||||
|
||||
log_info!("Local APIC timer configured: vector={}, count={}", vector, initial_count);
|
||||
}
|
||||
|
||||
pub unsafe fn print_info(&self) {
|
||||
let id = self.id();
|
||||
let version = self.version();
|
||||
let max_lvt = (version >> 16) & 0xFF;
|
||||
let apic_version = version & 0xFF;
|
||||
|
||||
log_info!("Local APIC ID: {}", id);
|
||||
log_info!("Local APIC Version: {:#x}", apic_version);
|
||||
log_info!("Max LVT Entry: {}", max_lvt);
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize Local APIC (using mapped virtual address)
|
||||
pub unsafe fn init_local_apic_with_vaddr(base_vaddr: VirtAddr) {
|
||||
let mut apic = LocalApic { base_vaddr };
|
||||
apic.init();
|
||||
apic.print_info();
|
||||
|
||||
*LOCAL_APIC.lock() = Some(apic);
|
||||
}
|
||||
|
||||
/// Initialize Local APIC (from physical address, needs to be mapped first)
|
||||
pub unsafe fn init_local_apic(base_paddr: PhysAddr) {
|
||||
let base_vaddr = vma::phys_to_virt(base_paddr.as_u64());
|
||||
init_local_apic_with_vaddr(base_vaddr);
|
||||
}
|
||||
|
||||
/// Send EOI
|
||||
pub fn send_eoi() {
|
||||
unsafe {
|
||||
if let Some(apic) = LOCAL_APIC.lock().as_mut() {
|
||||
apic.send_eoi();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the Local APIC ID
|
||||
pub fn get_apic_id() -> Option<u32> {
|
||||
unsafe {
|
||||
LOCAL_APIC.lock().as_ref().map(|apic| apic.id())
|
||||
}
|
||||
}
|
||||
|
||||
/// Disable legacy 8259 PIC
|
||||
/// This function should be called before using the APIC to avoid conflicts.
|
||||
pub fn disable_legacy_pic() {
|
||||
use crate::hal::io::io_port_wb;
|
||||
|
||||
unsafe {
|
||||
// Remap PIC to unused interrupt vector
|
||||
// Master PIC
|
||||
io_port_wb(0x20, 0x11); // ICW1: initialization
|
||||
io_port_wb(0x21, 0x20); // ICW2: Interrupt vector offset (32-39)
|
||||
io_port_wb(0x21, 0x04); // ICW3: Tell the Master PIC Slave to be on IRQ2
|
||||
io_port_wb(0x21, 0x01); // ICW4: 8086 mode
|
||||
|
||||
// Slave PIC
|
||||
io_port_wb(0xA0, 0x11); // ICW1: initialization
|
||||
io_port_wb(0xA1, 0x28); // ICW2: Interrupt vector offset (40-47)
|
||||
io_port_wb(0xA1, 0x02); // ICW3: Tell the Slave PIC to connect to Master IRQ2
|
||||
io_port_wb(0xA1, 0x01); // ICW4: 8086 mode
|
||||
|
||||
// Block all IRQs (disable PIC)
|
||||
io_port_wb(0x21, 0xFF);
|
||||
io_port_wb(0xA1, 0xFF);
|
||||
}
|
||||
|
||||
log_info!("Legacy 8259 PIC disabled");
|
||||
}
|
||||
@ -2,5 +2,5 @@ pub mod io;
|
||||
pub mod cpu;
|
||||
pub mod acpi;
|
||||
pub mod rtc;
|
||||
pub mod apic;
|
||||
mod ioapic;
|
||||
pub mod lapic;
|
||||
pub mod ioapic;
|
||||
@ -1,7 +1,7 @@
|
||||
// kernel/src/k_init.rs (Updated with Logger)
|
||||
// kernel/src/kernel/k_init.rs
|
||||
use bootloader_api::BootInfo;
|
||||
use bootloader_api::info::MemoryRegionKind;
|
||||
use x86_64::structures::paging::{OffsetPageTable, Page };
|
||||
use x86_64::structures::paging::OffsetPageTable;
|
||||
use x86_64::{PhysAddr, VirtAddr};
|
||||
use crate::mm::{allocator::{heap, frame, pmm}, vmm, paging, vma};
|
||||
use crate::arch::amd64::{gdt, idt};
|
||||
@ -11,7 +11,8 @@ use crate::kernel::k_main;
|
||||
use crate::klibc::logger::{init, LogLevel, LoggerConfig};
|
||||
use crate::klibc::malloc;
|
||||
use crate::{log_debug, log_error, log_info, log_trace, log_warn};
|
||||
use crate::hal::{acpi, rtc};
|
||||
use crate::drivers::keyboard;
|
||||
use crate::hal::{acpi, lapic, rtc};
|
||||
|
||||
fn _logger_init() {
|
||||
init(
|
||||
@ -132,13 +133,11 @@ fn _boot_report(memory_regions: &bootloader_api::info::MemoryRegions, physical_m
|
||||
log_info!("Heap Start: {:#x}", vma::HEAP_START);
|
||||
log_info!("Heap Size: {} KiB", vma::HEAP_SIZE / 1024);
|
||||
|
||||
if let Some(stats) = pmm::get_memory_stats() {
|
||||
log_info!("Total Memory: {} MiB", stats.total_memory / (1024 * 1024));
|
||||
log_info!("Free Memory: {} MiB", stats.free_memory / (1024 * 1024));
|
||||
}
|
||||
vma::print_info();
|
||||
vmm::get_vmm_stats().print();
|
||||
}
|
||||
|
||||
fn _acpi_init(rsdp_addr: Option<u64>, physical_memory_offset: u64) {
|
||||
fn _acpi_init(rsdp_addr: Option<u64>, physical_memory_offset: u64) -> Option<acpi::AcpiInfo> {
|
||||
kprintln!();
|
||||
|
||||
if let Some(rsdp) = rsdp_addr {
|
||||
@ -146,20 +145,82 @@ fn _acpi_init(rsdp_addr: Option<u64>, physical_memory_offset: u64) {
|
||||
|
||||
if let Some(acpi_info) = acpi::init(rsdp, physical_memory_offset) {
|
||||
acpi::print_info(&acpi_info);
|
||||
return Some(acpi_info);
|
||||
} else {
|
||||
log_warn!("ACPI initialization failed");
|
||||
}
|
||||
} else {
|
||||
log_warn!("RSDP not provided by bootloader");
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn _post_init() {
|
||||
fn _post_init(
|
||||
acpi_info: Option<&acpi::AcpiInfo>,
|
||||
mapper: &mut OffsetPageTable,
|
||||
frame_allocator: &mut frame::BootInfoFrameAllocator,
|
||||
) {
|
||||
kprintln!();
|
||||
log_info!("Post Initialization");
|
||||
// TODO: 釋放 bootloader 佔用的內存
|
||||
|
||||
// 初始化 APIC/IOAPIC (如果有的話)
|
||||
if let Some(info) = acpi_info {
|
||||
if info.has_apic {
|
||||
lapic::disable_legacy_pic();
|
||||
|
||||
unsafe {
|
||||
if let Some(local_apic_addr) = info.local_apic_address {
|
||||
log_info!("Initializing Local APIC...");
|
||||
log_debug!("Mapping Local APIC physical address: {:#x}", local_apic_addr);
|
||||
|
||||
if let Some(vaddr) = vmm::map_device_memory(
|
||||
PhysAddr::new(local_apic_addr),
|
||||
4096,
|
||||
mapper,
|
||||
frame_allocator,
|
||||
) {
|
||||
log_debug!("Local APIC mapped to virtual address: {:#x}", vaddr.as_u64());
|
||||
|
||||
lapic::init_local_apic_with_vaddr(vaddr);
|
||||
|
||||
if let Some(apic_id) = lapic::get_apic_id() {
|
||||
log_info!("Current Local APIC ID: {}", apic_id);
|
||||
}
|
||||
} else {
|
||||
log_error!("Failed to map Local APIC memory");
|
||||
}
|
||||
}
|
||||
|
||||
if !info.io_apics.is_empty() {
|
||||
log_info!("Initializing IO APICs...");
|
||||
|
||||
for (paddr, id, gsi_base) in &info.io_apics {
|
||||
log_debug!("Mapping IO APIC {} at physical address: {:#x}", id, paddr);
|
||||
|
||||
if let Some(vaddr) = vmm::map_device_memory(
|
||||
PhysAddr::new(*paddr),
|
||||
4096,
|
||||
mapper,
|
||||
frame_allocator,
|
||||
) {
|
||||
log_debug!("IO APIC {} mapped to virtual address: {:#x}", id, vaddr.as_u64());
|
||||
crate::hal::ioapic::init_single_ioapic(vaddr, *id, *gsi_base);
|
||||
} else {
|
||||
log_error!("Failed to map IO APIC {} memory", id);
|
||||
}
|
||||
}
|
||||
|
||||
log_info!("All IO APICs initialized");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log_warn!("APIC not available, using legacy PIC (not implemented yet)");
|
||||
}
|
||||
}
|
||||
|
||||
rtc::init();
|
||||
keyboard::init();
|
||||
log_debug!("Cleanup completed");
|
||||
}
|
||||
|
||||
@ -182,13 +243,12 @@ pub fn _kernel_init(boot_info: &'static mut BootInfo) -> ! {
|
||||
&boot_info.memory_regions,
|
||||
physical_memory_offset
|
||||
);
|
||||
let acpi_info = _acpi_init(rsdp_addr, physical_memory_offset);
|
||||
|
||||
_post_init(acpi_info.as_ref(), &mut mapper, &mut frame_allocator);
|
||||
|
||||
_boot_report(&boot_info.memory_regions, physical_memory_offset);
|
||||
|
||||
_acpi_init(rsdp_addr, physical_memory_offset);
|
||||
|
||||
_post_init();
|
||||
|
||||
kprintln!();
|
||||
kprintln!("========================================");
|
||||
kprintln!(" Kernel Initialization Complete! ");
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
// kernel/src/k_main.rs
|
||||
// kernel/src/kernel/k_main.rs
|
||||
use crate::hal::{cpu, rtc};
|
||||
use crate::kprintln;
|
||||
use crate::{log_trace, log_debug, log_info, log_warn, log_error, log_fatal};
|
||||
use crate::mm::{vma, vmm};
|
||||
use crate::mm::allocator::pmm;
|
||||
|
||||
pub fn _kernel_main() -> ! {
|
||||
kprintln!();
|
||||
@ -18,8 +19,10 @@ pub fn _kernel_main() -> ! {
|
||||
|
||||
kprintln!();
|
||||
|
||||
vma::print_info();
|
||||
vmm::get_vmm_stats().print();
|
||||
if let Some(stats) = pmm::get_memory_stats() {
|
||||
log_info!("Total Memory: {} MiB", stats.total_memory / (1024 * 1024));
|
||||
log_info!("Free Memory: {} MiB", stats.free_memory / (1024 * 1024));
|
||||
}
|
||||
|
||||
kprintln!();
|
||||
|
||||
@ -32,33 +35,45 @@ pub fn _kernel_main() -> ! {
|
||||
log_debug!("CR3: 0x{:016x}", cpu::cpu_r_cr3());
|
||||
log_debug!("CR4: 0x{:016x}", cpu::cpu_r_cr4());
|
||||
|
||||
kprintln!();
|
||||
|
||||
if let Some(apic_id) = crate::hal::lapic::get_apic_id() {
|
||||
log_info!("Configuring hardware interrupts...");
|
||||
log_info!("Current CPU APIC ID: {}", apic_id);
|
||||
|
||||
|
||||
// kprintln!();
|
||||
// kprintln!("=== Logger Level Demonstration ===");
|
||||
// kprintln!();
|
||||
//
|
||||
// log_trace!("TRACE: This is a trace message (lowest priority)");
|
||||
// log_debug!("DEBUG: Detailed debugging information");
|
||||
// log_info!("INFO: General information about system operation");
|
||||
// log_warn!("WARN: Warning message - something might be wrong");
|
||||
// log_error!("ERROR: Error occurred but system can continue");
|
||||
// log_fatal!("FATAL: Critical error (highest priority)");
|
||||
//
|
||||
// kprintln!();
|
||||
//
|
||||
// log_info!("Starting system services...");
|
||||
// log_debug!("Loading drivers...");
|
||||
// log_trace!(" Scanning PCI bus");
|
||||
// log_trace!("Initializing USB controller");
|
||||
// log_debug!("Drivers loaded successfully");
|
||||
//
|
||||
// log_info!("System is ready!");
|
||||
log_info!("Setting up keyboard interrupt (IRQ 1 -> Vector 33)");
|
||||
crate::hal::ioapic::set_irq_redirect(
|
||||
1, // IRQ number (keyboard)
|
||||
33, // Interrupt vector number
|
||||
apic_id as u8, // APIC ID of target CPU
|
||||
false, // Edge triggered (false = edge, true = level)
|
||||
false // Active high (false = high, true = low)
|
||||
);
|
||||
|
||||
// Unmask IRQ 1 (enable keyboard interrupt)
|
||||
crate::hal::ioapic::unmask_irq(1);
|
||||
log_info!("Keyboard interrupt unmasked");
|
||||
|
||||
// Enable CPU interrupts
|
||||
cpu::cpu_enable_interrupts();
|
||||
|
||||
log_info!("CPU interrupts enabled");
|
||||
|
||||
kprintln!();
|
||||
log_info!("Interrupt system ready!");
|
||||
|
||||
} else {
|
||||
log_error!("APIC not available, cannot enable keyboard");
|
||||
}
|
||||
|
||||
kprintln!();
|
||||
log_warn!("Entering idle loop");
|
||||
log_info!("System initialization complete!");
|
||||
log_warn!("Entering idle loop...");
|
||||
kprintln!();
|
||||
|
||||
loop {
|
||||
// Until the next interrupt occurs
|
||||
cpu::cpu_halt();
|
||||
}
|
||||
}
|
||||
@ -18,7 +18,7 @@ pub fn _print(args: fmt::Arguments) {
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! print {
|
||||
macro_rules! kprint {
|
||||
($($arg:tt)*) => ($crate::klibc::print::_print(format_args!($($arg)*)));
|
||||
}
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@ pub mod arch;
|
||||
pub mod mm;
|
||||
pub mod tty;
|
||||
pub mod kernel;
|
||||
pub mod drivers;
|
||||
|
||||
use hal::cpu;
|
||||
const CONFIG: BootloaderConfig = {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user