diff options
| author | salmonik <salmonik@tuta.io> | 2026-07-31 16:10:40 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-07-31 16:10:40 +0200 |
| commit | 8b1c1a23382806fcdbd3dbc7336a6ac2da8a995b (patch) | |
| tree | 5d7b039a1baf289638d34b6e2d66c81ba9117560 | |
| parent | style: rustfmt field alignment in RuntimeServices (diff) | |
| download | rs-dos-8b1c1a23382806fcdbd3dbc7336a6ac2da8a995b.tar.gz rs-dos-8b1c1a23382806fcdbd3dbc7336a6ac2da8a995b.zip | |
rtc + pci: CMOS clock with date command, PCI bus enumeration
- rtc.rs: read the PC RTC via CMOS ports 0x70/0x71 (UIP wait, BCD/12h
normalization, century sanity-check with 2000+ fallback); boot prints
the wall clock, shell gains 'date'
- pci.rs: legacy config-space access (0xCF8/0xCFC) with full enumeration
of buses 0-255; boot reports device count, shell gains 'pci' (lspci
style listing with class names)
- QEMU q35 verified: Q35 host bridge, stdvga, e1000, ICH9 LPC
| -rw-r--r-- | CONTEXT.md | 4 | ||||
| -rw-r--r-- | rsdos/src/main.rs | 13 | ||||
| -rw-r--r-- | rsnix/src/lib.rs | 2 | ||||
| -rw-r--r-- | rsnix/src/pci.rs | 125 | ||||
| -rw-r--r-- | rsnix/src/rtc.rs | 101 | ||||
| -rw-r--r-- | shell/src/lib.rs | 38 |
6 files changed, 282 insertions, 1 deletions
| @@ -96,6 +96,8 @@ Single Rust crate (`x86_64-unknown-uefi` target). Module breakdown: | |||
| 96 | allocator), bitmap page allocator | 96 | allocator), bitmap page allocator |
| 97 | - `paging.rs` — identity page-table helpers (map/unmap/query, large-page split) | 97 | - `paging.rs` — identity page-table helpers (map/unmap/query, large-page split) |
| 98 | - `power.rs` — `ResetSystem`-based shutdown, hlt loop fallback | 98 | - `power.rs` — `ResetSystem`-based shutdown, hlt loop fallback |
| 99 | - `rtc.rs` — CMOS/RTC clock (ports 0x70/0x71), BCD/12h normalization | ||
| 100 | - `pci.rs` — legacy PCI config space (0xCF8/0xCFC), full enumeration | ||
| 99 | - `gop.rs` — framebuffer access and text rendering | 101 | - `gop.rs` — framebuffer access and text rendering |
| 100 | - `font.rs` — 8×16 bitmap font data | 102 | - `font.rs` — 8×16 bitmap font data |
| 101 | - `ps2.rs` — PS/2 keyboard driver (polling) | 103 | - `ps2.rs` — PS/2 keyboard driver (polling) |
| @@ -120,6 +122,8 @@ Single Rust crate (`x86_64-unknown-uefi` target). Module breakdown: | |||
| 120 | ✅ **Shell** — `mmap` dumps the UEFI map, `stress` exercises alloc/free + growth | 122 | ✅ **Shell** — `mmap` dumps the UEFI map, `stress` exercises alloc/free + growth |
| 121 | ✅ **Poweroff** — `poweroff`/`exit` shut down via UEFI `ResetSystem` | 123 | ✅ **Poweroff** — `poweroff`/`exit` shut down via UEFI `ResetSystem` |
| 122 | (ACPI S5; `halt` keeps the plain hlt loop) | 124 | (ACPI S5; `halt` keeps the plain hlt loop) |
| 125 | ✅ **RTC** — CMOS clock read at boot and via `date` (BCD/12h handled) | ||
| 126 | ✅ **PCI** — port-based config access, full bus enumeration via `pci` | ||
| 123 | 127 | ||
| 124 | ## Target audience | 128 | ## Target audience |
| 125 | 129 | ||
diff --git a/rsdos/src/main.rs b/rsdos/src/main.rs index d9578e2..c35d88f 100644 --- a/rsdos/src/main.rs +++ b/rsdos/src/main.rs | |||
| @@ -141,6 +141,19 @@ pub extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut SystemT | |||
| 141 | rsnix::serial_println!("pg: self-test FAILED"); | 141 | rsnix::serial_println!("pg: self-test FAILED"); |
| 142 | } | 142 | } |
| 143 | 143 | ||
| 144 | let t = rsnix::rtc::now(); | ||
| 145 | rsnix::serial_println!( | ||
| 146 | "rtc: {}-{:02}-{:02} {:02}:{:02}:{:02}", | ||
| 147 | t.year, | ||
| 148 | t.month, | ||
| 149 | t.day, | ||
| 150 | t.hour, | ||
| 151 | t.minute, | ||
| 152 | t.second | ||
| 153 | ); | ||
| 154 | let devices = rsnix::pci::probe(); | ||
| 155 | rsnix::serial_println!("pci: {} device(s) on all buses", devices.len()); | ||
| 156 | |||
| 144 | rsnix::serial_println!("boot: entering shell"); | 157 | rsnix::serial_println!("boot: entering shell"); |
| 145 | shell::run(); | 158 | shell::run(); |
| 146 | } | 159 | } |
diff --git a/rsnix/src/lib.rs b/rsnix/src/lib.rs index 8c69b35..f84fa73 100644 --- a/rsnix/src/lib.rs +++ b/rsnix/src/lib.rs | |||
| @@ -7,7 +7,9 @@ pub mod gop; | |||
| 7 | pub mod interrupts; | 7 | pub mod interrupts; |
| 8 | pub mod memory; | 8 | pub mod memory; |
| 9 | pub mod paging; | 9 | pub mod paging; |
| 10 | pub mod pci; | ||
| 10 | pub mod power; | 11 | pub mod power; |
| 11 | pub mod ps2; | 12 | pub mod ps2; |
| 13 | pub mod rtc; | ||
| 12 | pub mod serial; | 14 | pub mod serial; |
| 13 | pub mod uefi; | 15 | pub mod uefi; |
diff --git a/rsnix/src/pci.rs b/rsnix/src/pci.rs new file mode 100644 index 0000000..2d201f3 --- /dev/null +++ b/rsnix/src/pci.rs | |||
| @@ -0,0 +1,125 @@ | |||
| 1 | // --------------------------------------------------------------------------- | ||
| 2 | // PCI config space — legacy port-based access (0xCF8 / 0xCFC). | ||
| 3 | // | ||
| 4 | // Enumeration walks buses 0-255 × devices 0-31 × functions 0-7. Port I/O | ||
| 5 | // stays usable after ExitBootServices, so the shell can list devices and | ||
| 6 | // future drivers (AHCI, network, ...) can find their controllers. | ||
| 7 | // --------------------------------------------------------------------------- | ||
| 8 | |||
| 9 | const CONFIG_ADDRESS: u16 = 0xCF8; | ||
| 10 | const CONFIG_DATA: u16 = 0xCFC; | ||
| 11 | |||
| 12 | #[derive(Debug, Clone, Copy)] | ||
| 13 | pub struct DeviceInfo { | ||
| 14 | pub bus: u8, | ||
| 15 | pub device: u8, | ||
| 16 | pub func: u8, | ||
| 17 | pub vendor: u16, | ||
| 18 | pub device_id: u16, | ||
| 19 | pub class: u8, | ||
| 20 | pub subclass: u8, | ||
| 21 | pub prog_if: u8, | ||
| 22 | pub header_type: u8, | ||
| 23 | } | ||
| 24 | |||
| 25 | fn outl(port: u16, val: u32) { | ||
| 26 | unsafe { | ||
| 27 | core::arch::asm!( | ||
| 28 | "out dx, eax", | ||
| 29 | in("dx") port, | ||
| 30 | in("eax") val, | ||
| 31 | options(nomem, nostack, preserves_flags) | ||
| 32 | ); | ||
| 33 | } | ||
| 34 | } | ||
| 35 | |||
| 36 | fn inl(port: u16) -> u32 { | ||
| 37 | let val: u32; | ||
| 38 | unsafe { | ||
| 39 | core::arch::asm!( | ||
| 40 | "in eax, dx", | ||
| 41 | out("eax") val, | ||
| 42 | in("dx") port, | ||
| 43 | options(nomem, nostack, preserves_flags) | ||
| 44 | ); | ||
| 45 | } | ||
| 46 | val | ||
| 47 | } | ||
| 48 | |||
| 49 | /// Read a dword from a device's config space (`reg` must be dword-aligned). | ||
| 50 | pub fn config_read(bus: u8, device: u8, func: u8, reg: u8) -> u32 { | ||
| 51 | let addr = 0x8000_0000 | ||
| 52 | | ((bus as u32) << 16) | ||
| 53 | | ((device as u32) << 11) | ||
| 54 | | ((func as u32) << 8) | ||
| 55 | | ((reg as u32) & 0xFC); | ||
| 56 | outl(CONFIG_ADDRESS, addr); | ||
| 57 | inl(CONFIG_DATA) | ||
| 58 | } | ||
| 59 | |||
| 60 | /// Enumerate every device/function present on all buses. | ||
| 61 | pub fn probe() -> alloc::vec::Vec<DeviceInfo> { | ||
| 62 | let mut out = alloc::vec::Vec::new(); | ||
| 63 | for bus in 0..=255u8 { | ||
| 64 | for dev in 0..32u8 { | ||
| 65 | for func in 0..8u8 { | ||
| 66 | let id = config_read(bus, dev, func, 0); | ||
| 67 | let vendor = (id & 0xFFFF) as u16; | ||
| 68 | if vendor == 0xFFFF { | ||
| 69 | if func == 0 { | ||
| 70 | break; // nothing on this device number | ||
| 71 | } | ||
| 72 | continue; | ||
| 73 | } | ||
| 74 | let hdr = config_read(bus, dev, func, 0x0C); | ||
| 75 | let header_type = (hdr & 0xFF) as u8; | ||
| 76 | let cc = config_read(bus, dev, func, 0x08); | ||
| 77 | out.push(DeviceInfo { | ||
| 78 | bus, | ||
| 79 | device: dev, | ||
| 80 | func, | ||
| 81 | vendor, | ||
| 82 | device_id: (id >> 16) as u16, | ||
| 83 | class: (cc >> 24) as u8, | ||
| 84 | subclass: (cc >> 16) as u8, | ||
| 85 | prog_if: (cc >> 8) as u8, | ||
| 86 | header_type, | ||
| 87 | }); | ||
| 88 | // A non-multifunction device has no functions 1-7. | ||
| 89 | if func == 0 && header_type & 0x80 == 0 { | ||
| 90 | break; | ||
| 91 | } | ||
| 92 | } | ||
| 93 | } | ||
| 94 | } | ||
| 95 | out | ||
| 96 | } | ||
| 97 | |||
| 98 | /// Short human name for a (class, subclass) pair. | ||
| 99 | pub fn class_name(class: u8, subclass: u8) -> &'static str { | ||
| 100 | match (class, subclass) { | ||
| 101 | (0x00, 0x01) => "VGA-compatible", | ||
| 102 | (0x00, _) => "Unclassified", | ||
| 103 | (0x01, 0x01) => "IDE controller", | ||
| 104 | (0x01, 0x06) => "SATA controller", | ||
| 105 | (0x01, 0x08) => "NVM controller", | ||
| 106 | (0x01, _) => "Mass storage", | ||
| 107 | (0x02, 0x00) => "Ethernet", | ||
| 108 | (0x02, _) => "Network", | ||
| 109 | (0x03, 0x00) => "VGA", | ||
| 110 | (0x03, _) => "Display", | ||
| 111 | (0x04, _) => "Multimedia", | ||
| 112 | (0x05, _) => "Memory controller", | ||
| 113 | (0x06, 0x00) => "Host bridge", | ||
| 114 | (0x06, 0x01) => "ISA bridge", | ||
| 115 | (0x06, 0x04) => "PCI-PCI bridge", | ||
| 116 | (0x06, _) => "Bridge", | ||
| 117 | (0x08, 0x80) => "PIC", | ||
| 118 | (0x0C, 0x03) => "USB controller", | ||
| 119 | (0x0C, 0x05) => "SMBus", | ||
| 120 | (0x0C, _) => "Serial bus", | ||
| 121 | (0x11, _) => "Signal processing", | ||
| 122 | (0xFF, _) => "Unassigned", | ||
| 123 | _ => "Other", | ||
| 124 | } | ||
| 125 | } | ||
diff --git a/rsnix/src/rtc.rs b/rsnix/src/rtc.rs new file mode 100644 index 0000000..535f32e --- /dev/null +++ b/rsnix/src/rtc.rs | |||
| @@ -0,0 +1,101 @@ | |||
| 1 | // --------------------------------------------------------------------------- | ||
| 2 | // RTC/CMOS clock — the standard PC real-time clock on ports 0x70/0x71. | ||
| 3 | // | ||
| 4 | // Reads the time registers (0x00-0x09). Values may be BCD or binary and | ||
| 5 | // hours may be 12- or 24-hour depending on status register B; we normalize | ||
| 6 | // to binary 24-hour. The century register (0x32) is used when it reports | ||
| 7 | // a sane value, otherwise we assume 2000+. NMI is masked (bit 7 set) while | ||
| 8 | // poking CMOS — this kernel has no NMI handler, so a stray NMI would fault. | ||
| 9 | // --------------------------------------------------------------------------- | ||
| 10 | |||
| 11 | pub struct DateTime { | ||
| 12 | pub year: u16, | ||
| 13 | pub month: u8, | ||
| 14 | pub day: u8, | ||
| 15 | pub hour: u8, | ||
| 16 | pub minute: u8, | ||
| 17 | pub second: u8, | ||
| 18 | } | ||
| 19 | |||
| 20 | /// Read one CMOS register. Writes bit 7 to the address port, which masks | ||
| 21 | /// NMIs during the access. | ||
| 22 | fn cmos_read(reg: u8) -> u8 { | ||
| 23 | unsafe { | ||
| 24 | core::arch::asm!( | ||
| 25 | "out dx, al", | ||
| 26 | in("dx") 0x70u16, | ||
| 27 | in("al") reg | 0x80, | ||
| 28 | options(nomem, nostack, preserves_flags) | ||
| 29 | ); | ||
| 30 | let val: u8; | ||
| 31 | core::arch::asm!( | ||
| 32 | "in al, dx", | ||
| 33 | out("al") val, | ||
| 34 | in("dx") 0x71u16, | ||
| 35 | options(nomem, nostack, preserves_flags) | ||
| 36 | ); | ||
| 37 | val | ||
| 38 | } | ||
| 39 | } | ||
| 40 | |||
| 41 | fn bcd_to_bin(v: u8) -> u8 { | ||
| 42 | (v & 0x0F) + (v >> 4) * 10 | ||
| 43 | } | ||
| 44 | |||
| 45 | /// Read the current wall-clock time from the RTC. | ||
| 46 | pub fn now() -> DateTime { | ||
| 47 | // Wait until an in-progress update finishes (status A, UIP bit 7). | ||
| 48 | while cmos_read(0x0A) & 0x80 != 0 { | ||
| 49 | core::hint::spin_loop(); | ||
| 50 | } | ||
| 51 | |||
| 52 | let reg_b = cmos_read(0x0B); | ||
| 53 | let bcd = reg_b & 0x04 == 0; // bit 2: 0 = BCD, 1 = binary | ||
| 54 | let hour12 = reg_b & 0x02 == 0; // bit 1: 1 = 24-hour, 0 = 12-hour | ||
| 55 | |||
| 56 | let mut second = cmos_read(0x00); | ||
| 57 | let mut minute = cmos_read(0x02); | ||
| 58 | let mut hour = cmos_read(0x04); | ||
| 59 | let mut day = cmos_read(0x07); | ||
| 60 | let mut month = cmos_read(0x08); | ||
| 61 | let mut year = cmos_read(0x09); | ||
| 62 | let century = cmos_read(0x32); | ||
| 63 | |||
| 64 | if bcd { | ||
| 65 | second = bcd_to_bin(second); | ||
| 66 | minute = bcd_to_bin(minute); | ||
| 67 | hour = bcd_to_bin(hour); | ||
| 68 | day = bcd_to_bin(day); | ||
| 69 | month = bcd_to_bin(month); | ||
| 70 | year = bcd_to_bin(year); | ||
| 71 | } | ||
| 72 | if hour12 { | ||
| 73 | let pm = hour & 0x80 != 0; // top bit of a 12-hour hour is the PM flag | ||
| 74 | hour &= 0x7F; | ||
| 75 | if hour == 12 { | ||
| 76 | hour = 0; // 12 AM/PM are both 0 in 24-hour form | ||
| 77 | } | ||
| 78 | if pm { | ||
| 79 | hour += 12; | ||
| 80 | } | ||
| 81 | } | ||
| 82 | let year = { | ||
| 83 | let full = century as u16 * 100 + year as u16; | ||
| 84 | if (1970..=2200).contains(&full) { | ||
| 85 | full | ||
| 86 | } else { | ||
| 87 | // Unreadable century register (QEMU returns garbage here) — | ||
| 88 | // assume 2000+ for the 2-digit year. | ||
| 89 | 2000 + year as u16 | ||
| 90 | } | ||
| 91 | }; | ||
| 92 | |||
| 93 | DateTime { | ||
| 94 | year, | ||
| 95 | month, | ||
| 96 | day, | ||
| 97 | hour, | ||
| 98 | minute, | ||
| 99 | second, | ||
| 100 | } | ||
| 101 | } | ||
diff --git a/shell/src/lib.rs b/shell/src/lib.rs index 5cab122..500eaae 100644 --- a/shell/src/lib.rs +++ b/shell/src/lib.rs | |||
| @@ -50,6 +50,8 @@ pub fn run() -> ! { | |||
| 50 | "mem" => cmd_mem(), | 50 | "mem" => cmd_mem(), |
| 51 | "mmap" => cmd_mmap(), | 51 | "mmap" => cmd_mmap(), |
| 52 | "stress" => cmd_stress(args), | 52 | "stress" => cmd_stress(args), |
| 53 | "date" => cmd_date(), | ||
| 54 | "pci" => cmd_pci(), | ||
| 53 | _ => { | 55 | _ => { |
| 54 | gop_println!("unknown command: {}", cmd); | 56 | gop_println!("unknown command: {}", cmd); |
| 55 | serial_println!("unknown command: {}", cmd); | 57 | serial_println!("unknown command: {}", cmd); |
| @@ -124,11 +126,13 @@ fn cmd_help() { | |||
| 124 | gop_println!(" mem — show memory info"); | 126 | gop_println!(" mem — show memory info"); |
| 125 | gop_println!(" mmap — dump the UEFI memory map"); | 127 | gop_println!(" mmap — dump the UEFI memory map"); |
| 126 | gop_println!(" stress — stress-test heap/page allocators"); | 128 | gop_println!(" stress — stress-test heap/page allocators"); |
| 129 | gop_println!(" date — show current date/time (RTC)"); | ||
| 130 | gop_println!(" pci — list PCI devices"); | ||
| 127 | gop_println!(" halt — halt the system (hlt loop)"); | 131 | gop_println!(" halt — halt the system (hlt loop)"); |
| 128 | gop_println!(" poweroff — shut down via ACPI (ResetSystem)"); | 132 | gop_println!(" poweroff — shut down via ACPI (ResetSystem)"); |
| 129 | gop_println!(" exit — alias for poweroff"); | 133 | gop_println!(" exit — alias for poweroff"); |
| 130 | serial_println!("available commands:"); | 134 | serial_println!("available commands:"); |
| 131 | serial_println!(" help, echo, clear, hello, info, mem, mmap, stress, halt, poweroff, exit"); | 135 | serial_println!(" help, echo, clear, hello, info, mem, mmap, stress, date, pci, halt, poweroff, exit"); |
| 132 | } | 136 | } |
| 133 | 137 | ||
| 134 | fn cmd_echo(args: &str) { | 138 | fn cmd_echo(args: &str) { |
| @@ -364,6 +368,38 @@ fn cmd_halt() -> ! { | |||
| 364 | rsnix::power::halt() | 368 | rsnix::power::halt() |
| 365 | } | 369 | } |
| 366 | 370 | ||
| 371 | fn cmd_date() { | ||
| 372 | let t = rsnix::rtc::now(); | ||
| 373 | let line = alloc::format!( | ||
| 374 | "{}-{:02}-{:02} {:02}:{:02}:{:02}", | ||
| 375 | t.year, t.month, t.day, t.hour, t.minute, t.second | ||
| 376 | ); | ||
| 377 | gop_println!("{}", line); | ||
| 378 | serial_println!("{}", line); | ||
| 379 | } | ||
| 380 | |||
| 381 | fn cmd_pci() { | ||
| 382 | let devices = rsnix::pci::probe(); | ||
| 383 | let mut out = alloc::string::String::new(); | ||
| 384 | use alloc::fmt::Write; | ||
| 385 | let _ = writeln!(out, "pci: {} device(s)", devices.len()); | ||
| 386 | let _ = writeln!(out, " bus dev fn class vendor:device"); | ||
| 387 | for d in &devices { | ||
| 388 | let _ = writeln!( | ||
| 389 | out, | ||
| 390 | " {:02x}:{:02x}.{} {:<16} {:04x}:{:04x}", | ||
| 391 | d.bus, | ||
| 392 | d.device, | ||
| 393 | d.func, | ||
| 394 | rsnix::pci::class_name(d.class, d.subclass), | ||
| 395 | d.vendor, | ||
| 396 | d.device_id | ||
| 397 | ); | ||
| 398 | } | ||
| 399 | gop_print!("{}", out); | ||
| 400 | serial_print!("{}", out); | ||
| 401 | } | ||
| 402 | |||
| 367 | fn cmd_poweroff() -> ! { | 403 | fn cmd_poweroff() -> ! { |
| 368 | gop_println!("shutting down..."); | 404 | gop_println!("shutting down..."); |
| 369 | serial_println!("shutting down..."); | 405 | serial_println!("shutting down..."); |
