⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/arch/x86_64/
rtc.rs

1//! CMOS Real-Time Clock (RTC) reader for x86_64.
2//!
3//! Reads date/time from the MC146818-compatible CMOS RTC via I/O ports
4//! 0x70 (index) and 0x71 (data). Converts BCD-encoded values to binary
5//! and provides a Unix-epoch-relative timestamp for the panel clock.
6
7use core::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering};
8
9/// Seconds since Unix epoch at kernel boot time.
10static BOOT_EPOCH: AtomicU64 = AtomicU64::new(0);
11
12/// Millisecond-granularity uptime counter base (TSC ticks at boot).
13static BOOT_TSC: AtomicU64 = AtomicU64::new(0);
14
15/// Timezone offset from UTC in seconds (e.g., -18000 for EST/UTC-5).
16static TIMEZONE_OFFSET: AtomicI64 = AtomicI64::new(0);
17
18/// NTP time correction in milliseconds (positive = clock behind, negative =
19/// clock ahead).
20static NTP_CORRECTION_MS: AtomicI64 = AtomicI64::new(0);
21
22/// Whether an alarm interrupt is currently enabled.
23static ALARM_ENABLED: AtomicBool = AtomicBool::new(false);
24
25/// Alarm callback function pointer (set by `set_alarm`).
26static ALARM_CALLBACK: AtomicU64 = AtomicU64::new(0);
27
28/// Ioctl command: set alarm time.
29pub const RTC_ALM_SET: u32 = 0x01;
30/// Ioctl command: enable alarm interrupt.
31pub const RTC_AIE_ON: u32 = 0x02;
32/// Ioctl command: disable alarm interrupt.
33pub const RTC_AIE_OFF: u32 = 0x03;
34
35/// RTC time snapshot.
36#[derive(Debug, Clone, Copy)]
37pub struct RtcTime {
38    pub year: u16,
39    pub month: u8,
40    pub day: u8,
41    pub hour: u8,
42    pub minute: u8,
43    pub second: u8,
44}
45
46/// Read the CMOS RTC and initialize the boot epoch.
47pub fn init() {
48    let time = read_rtc();
49    let epoch = rtc_to_epoch(&time);
50    BOOT_EPOCH.store(epoch, Ordering::Relaxed);
51    BOOT_TSC.store(crate::arch::timer::read_hw_timestamp(), Ordering::Relaxed);
52    crate::println!(
53        "[RTC] Boot time: {:04}-{:02}-{:02} {:02}:{:02}:{:02} (epoch={})",
54        time.year,
55        time.month,
56        time.day,
57        time.hour,
58        time.minute,
59        time.second,
60        epoch,
61    );
62}
63
64/// Get current wall-clock seconds since Unix epoch.
65///
66/// Adds elapsed uptime (from TSC) to the boot-time RTC snapshot,
67/// then applies timezone offset and NTP correction.
68pub fn current_epoch_secs() -> u64 {
69    let boot = BOOT_EPOCH.load(Ordering::Relaxed);
70    let boot_tsc = BOOT_TSC.load(Ordering::Relaxed);
71    let now_tsc = crate::arch::timer::read_hw_timestamp();
72    let elapsed_ticks = now_tsc.saturating_sub(boot_tsc);
73    // Approximate TSC as ~1 GHz (typical for QEMU with KVM)
74    let elapsed_secs = elapsed_ticks / 1_000_000_000;
75    let base = boot + elapsed_secs;
76
77    // Apply NTP correction (milliseconds -> seconds, truncated)
78    let ntp_ms = NTP_CORRECTION_MS.load(Ordering::Relaxed);
79    let ntp_secs = ntp_ms / 1000;
80    let corrected = (base as i64).saturating_add(ntp_secs);
81
82    // Apply timezone offset
83    let tz_offset = TIMEZONE_OFFSET.load(Ordering::Relaxed);
84    let adjusted = corrected.saturating_add(tz_offset);
85
86    // Clamp to non-negative (Unix epoch cannot go before 1970)
87    if adjusted < 0 {
88        0
89    } else {
90        adjusted as u64
91    }
92}
93
94/// Read the RTC registers, handling BCD conversion and update-in-progress.
95pub fn read_rtc() -> RtcTime {
96    // Wait for any in-progress update to complete
97    wait_for_update();
98
99    let mut second = read_cmos(0x00);
100    let mut minute = read_cmos(0x02);
101    let mut hour = read_cmos(0x04);
102    let mut day = read_cmos(0x07);
103    let mut month = read_cmos(0x08);
104    let mut year = read_cmos(0x09);
105    let century = read_cmos(0x32); // Century register (ACPI FADT)
106    let reg_b = read_cmos(0x0B);
107
108    // BCD to binary conversion (if register B bit 2 is clear)
109    if reg_b & 0x04 == 0 {
110        second = bcd_to_bin(second);
111        minute = bcd_to_bin(minute);
112        hour = bcd_to_bin(hour & 0x7F) | (hour & 0x80); // preserve AM/PM bit
113        day = bcd_to_bin(day);
114        month = bcd_to_bin(month);
115        year = bcd_to_bin(year);
116    }
117
118    // 12-hour to 24-hour conversion (if register B bit 1 is clear)
119    if reg_b & 0x02 == 0 && hour & 0x80 != 0 {
120        hour = ((hour & 0x7F) + 12) % 24;
121    }
122
123    let century_val = if century > 0 {
124        if reg_b & 0x04 == 0 {
125            bcd_to_bin(century)
126        } else {
127            century
128        }
129    } else {
130        20 // Default to 21st century
131    };
132
133    let full_year = (century_val as u16) * 100 + (year as u16);
134
135    RtcTime {
136        year: full_year,
137        month,
138        day,
139        hour,
140        minute,
141        second,
142    }
143}
144
145/// Convert RTC time to seconds since Unix epoch (1970-01-01 00:00:00 UTC).
146fn rtc_to_epoch(t: &RtcTime) -> u64 {
147    // Days from 1970-01-01 to the start of the given year
148    let mut days: u64 = 0;
149    for y in 1970..t.year {
150        days += if is_leap(y) { 366 } else { 365 };
151    }
152    // Days in months of the current year
153    let month_days: [u8; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
154    for m in 1..t.month {
155        if m == 2 && is_leap(t.year) {
156            days += 29;
157        } else if (m as usize) <= 12 {
158            days += month_days[(m - 1) as usize] as u64;
159        }
160    }
161    days += (t.day as u64).saturating_sub(1);
162
163    days * 86400 + (t.hour as u64) * 3600 + (t.minute as u64) * 60 + (t.second as u64)
164}
165
166fn is_leap(y: u16) -> bool {
167    (y.is_multiple_of(4) && !y.is_multiple_of(100)) || y.is_multiple_of(400)
168}
169
170/// Wait until the RTC update-in-progress flag (register 0x0A bit 7) clears.
171fn wait_for_update() {
172    // Spin for at most ~2ms worth of iterations
173    for _ in 0..10_000 {
174        if read_cmos(0x0A) & 0x80 == 0 {
175            return;
176        }
177        core::hint::spin_loop();
178    }
179}
180
181/// Read a CMOS register.
182#[cfg(target_os = "none")]
183fn read_cmos(reg: u8) -> u8 {
184    // SAFETY: Ports 0x70/0x71 are the standard CMOS RTC index/data ports.
185    // Writing the register index to 0x70 and reading from 0x71 is the
186    // defined access protocol. NMI disable bit (0x80) is preserved as 0.
187    unsafe {
188        crate::arch::x86_64::outb(0x70, reg);
189        crate::arch::x86_64::inb(0x71)
190    }
191}
192
193/// Host stub: CMOS registers are not available on user-space targets.
194#[cfg(not(target_os = "none"))]
195fn read_cmos(_reg: u8) -> u8 {
196    0
197}
198
199/// Convert BCD-encoded byte to binary.
200fn bcd_to_bin(bcd: u8) -> u8 {
201    (bcd >> 4) * 10 + (bcd & 0x0F)
202}
203
204/// Convert binary value to BCD encoding.
205#[allow(dead_code)]
206fn bin_to_bcd(val: u8) -> u8 {
207    ((val / 10) << 4) | (val % 10)
208}
209
210// ---------------------------------------------------------------------------
211// Alarm registers
212// ---------------------------------------------------------------------------
213
214/// Set the CMOS alarm to fire at the given hour:minute:second.
215///
216/// This programs CMOS alarm registers 0x01 (seconds), 0x03 (minutes),
217/// 0x05 (hours) and enables the alarm interrupt bit in Register B.
218#[cfg(all(target_arch = "x86_64", target_os = "none"))]
219pub fn set_alarm(hour: u8, minute: u8, second: u8) {
220    let reg_b = read_cmos(0x0B);
221    let is_bcd = reg_b & 0x04 == 0;
222
223    let (s, m, h) = if is_bcd {
224        (bin_to_bcd(second), bin_to_bcd(minute), bin_to_bcd(hour))
225    } else {
226        (second, minute, hour)
227    };
228
229    write_cmos(0x01, s); // Alarm seconds
230    write_cmos(0x03, m); // Alarm minutes
231    write_cmos(0x05, h); // Alarm hours
232
233    // Enable alarm interrupt (Register B, bit 5)
234    let new_b = reg_b | 0x20;
235    write_cmos(0x0B, new_b);
236    ALARM_ENABLED.store(true, Ordering::Relaxed);
237
238    crate::println!(
239        "[RTC] Alarm set for {:02}:{:02}:{:02}",
240        hour,
241        minute,
242        second,
243    );
244}
245
246/// Disable the CMOS alarm interrupt.
247#[cfg(all(target_arch = "x86_64", target_os = "none"))]
248pub fn disable_alarm() {
249    let reg_b = read_cmos(0x0B);
250    write_cmos(0x0B, reg_b & !0x20); // Clear bit 5
251    ALARM_ENABLED.store(false, Ordering::Relaxed);
252}
253
254/// Non-x86_64 stub for `set_alarm`.
255#[cfg(not(all(target_arch = "x86_64", target_os = "none")))]
256pub fn set_alarm(_hour: u8, _minute: u8, _second: u8) {}
257
258/// Non-x86_64 stub for `disable_alarm`.
259#[cfg(not(all(target_arch = "x86_64", target_os = "none")))]
260pub fn disable_alarm() {
261    ALARM_ENABLED.store(false, Ordering::Relaxed);
262}
263
264/// Register a callback function pointer to invoke when the alarm fires.
265pub fn set_alarm_callback(callback: fn()) {
266    ALARM_CALLBACK.store(callback as usize as u64, Ordering::Relaxed);
267}
268
269// ---------------------------------------------------------------------------
270// IRQ 8 handler
271// ---------------------------------------------------------------------------
272
273/// RTC interrupt handler (IRQ 8, vector 40 on PIC / remapped on APIC).
274///
275/// Acknowledges the interrupt by reading Register C, then dispatches the
276/// alarm callback if an alarm interrupt occurred (bit 5 of Register C).
277#[cfg(all(target_arch = "x86_64", target_os = "none"))]
278pub fn rtc_interrupt_handler() {
279    // Reading Register C clears the interrupt flags and allows future IRQs.
280    let status = read_cmos(0x0C);
281
282    // Bit 5: alarm interrupt flag
283    if status & 0x20 != 0 && ALARM_ENABLED.load(Ordering::Relaxed) {
284        let cb_ptr = ALARM_CALLBACK.load(Ordering::Relaxed);
285        if cb_ptr != 0 {
286            // SAFETY: The callback was set via `set_alarm_callback` which takes
287            // a valid `fn()` pointer. We reconstruct it here.
288            let callback: fn() = unsafe { core::mem::transmute(cb_ptr as usize) };
289            callback();
290        }
291    }
292}
293
294/// Non-x86_64 stub for the RTC interrupt handler.
295#[cfg(not(all(target_arch = "x86_64", target_os = "none")))]
296pub fn rtc_interrupt_handler() {}
297
298// ---------------------------------------------------------------------------
299// Timezone configuration
300// ---------------------------------------------------------------------------
301
302/// Set the timezone offset from UTC in seconds.
303///
304/// Positive values are east of UTC, negative values west.
305/// Example: EST (UTC-5) = -18000, CET (UTC+1) = 3600.
306pub fn set_timezone_offset(seconds: i64) {
307    TIMEZONE_OFFSET.store(seconds, Ordering::Relaxed);
308}
309
310/// Get the current timezone offset in seconds.
311pub fn get_timezone_offset() -> i64 {
312    TIMEZONE_OFFSET.load(Ordering::Relaxed)
313}
314
315// ---------------------------------------------------------------------------
316// NTP integration point
317// ---------------------------------------------------------------------------
318
319/// Apply a time correction from an NTP client.
320///
321/// `offset_ms` is the difference between NTP server time and local time
322/// in milliseconds. Positive means local clock is behind (needs advancing),
323/// negative means local clock is ahead (needs slowing).
324///
325/// The correction is applied atomically and takes effect on the next call
326/// to `current_epoch_secs()`.
327pub fn set_time_correction(offset_ms: i64) {
328    NTP_CORRECTION_MS.store(offset_ms, Ordering::Relaxed);
329}
330
331/// Get the current NTP correction in milliseconds.
332pub fn get_time_correction() -> i64 {
333    NTP_CORRECTION_MS.load(Ordering::Relaxed)
334}
335
336// ---------------------------------------------------------------------------
337// /dev/rtc interface stubs
338// ---------------------------------------------------------------------------
339
340/// Alarm time for ioctl RTC_ALM_SET.
341#[derive(Debug, Clone, Copy)]
342pub struct RtcAlarm {
343    pub hour: u8,
344    pub minute: u8,
345    pub second: u8,
346}
347
348/// Read the current RTC time (/dev/rtc read interface).
349///
350/// Returns the current wall-clock time as an `RtcTime` struct.
351pub fn rtc_read() -> RtcTime {
352    read_rtc()
353}
354
355/// Perform an RTC ioctl operation.
356///
357/// Supported commands:
358/// - `RTC_ALM_SET`: Set alarm time (arg interpreted as `&RtcAlarm`)
359/// - `RTC_AIE_ON`: Enable alarm interrupt
360/// - `RTC_AIE_OFF`: Disable alarm interrupt
361///
362/// Returns 0 on success, -1 on invalid command.
363pub fn rtc_ioctl(cmd: u32, arg: u64) -> i32 {
364    match cmd {
365        RTC_ALM_SET => {
366            // arg is a pointer to RtcAlarm in the caller's address space.
367            // In kernel context we treat it as a direct struct pointer.
368            // SAFETY: arg is a valid pointer to an RtcAlarm struct from the caller's
369            // address space, verified by the syscall layer before reaching here.
370            let alarm = unsafe { &*(arg as *const RtcAlarm) };
371            if alarm.hour > 23 || alarm.minute > 59 || alarm.second > 59 {
372                return -1;
373            }
374            set_alarm(alarm.hour, alarm.minute, alarm.second);
375            0
376        }
377        RTC_AIE_ON => {
378            ALARM_ENABLED.store(true, Ordering::Relaxed);
379            0
380        }
381        RTC_AIE_OFF => {
382            disable_alarm();
383            0
384        }
385        _ => -1, // Unknown command
386    }
387}
388
389// ---------------------------------------------------------------------------
390// CMOS write helper
391// ---------------------------------------------------------------------------
392
393/// Write a value to a CMOS register.
394#[cfg(all(target_arch = "x86_64", target_os = "none"))]
395fn write_cmos(reg: u8, val: u8) {
396    // SAFETY: Ports 0x70/0x71 are the standard CMOS RTC index/data ports.
397    unsafe {
398        crate::arch::x86_64::outb(0x70, reg);
399        crate::arch::x86_64::outb(0x71, val);
400    }
401}
402
403/// Non-x86_64 stub (no-op).
404#[cfg(not(all(target_arch = "x86_64", target_os = "none")))]
405#[allow(dead_code)]
406fn write_cmos(_reg: u8, _val: u8) {}
407
408// ---------------------------------------------------------------------------
409// Tests
410// ---------------------------------------------------------------------------
411
412#[cfg(test)]
413mod tests {
414    use super::*;
415
416    #[test]
417    fn test_bcd_conversions() {
418        // BCD -> binary
419        assert_eq!(bcd_to_bin(0x59), 59);
420        assert_eq!(bcd_to_bin(0x23), 23);
421        assert_eq!(bcd_to_bin(0x00), 0);
422
423        // binary -> BCD
424        assert_eq!(bin_to_bcd(59), 0x59);
425        assert_eq!(bin_to_bcd(23), 0x23);
426        assert_eq!(bin_to_bcd(0), 0x00);
427
428        // Round-trip
429        for val in 0..=99u8 {
430            assert_eq!(bcd_to_bin(bin_to_bcd(val)), val);
431        }
432    }
433
434    #[test]
435    fn test_timezone_offset() {
436        // Default is 0 (UTC)
437        set_timezone_offset(0);
438        assert_eq!(get_timezone_offset(), 0);
439
440        // EST (UTC-5)
441        set_timezone_offset(-18000);
442        assert_eq!(get_timezone_offset(), -18000);
443
444        // CET (UTC+1)
445        set_timezone_offset(3600);
446        assert_eq!(get_timezone_offset(), 3600);
447
448        // Reset
449        set_timezone_offset(0);
450    }
451
452    #[test]
453    fn test_ntp_correction() {
454        // Default is 0
455        set_time_correction(0);
456        assert_eq!(get_time_correction(), 0);
457
458        // Clock behind by 500ms
459        set_time_correction(500);
460        assert_eq!(get_time_correction(), 500);
461
462        // Clock ahead by 200ms
463        set_time_correction(-200);
464        assert_eq!(get_time_correction(), -200);
465
466        // Reset
467        set_time_correction(0);
468    }
469
470    #[test]
471    fn test_rtc_to_epoch_known_dates() {
472        // 1970-01-01 00:00:00 = epoch 0
473        let t = RtcTime {
474            year: 1970,
475            month: 1,
476            day: 1,
477            hour: 0,
478            minute: 0,
479            second: 0,
480        };
481        assert_eq!(rtc_to_epoch(&t), 0);
482
483        // 2000-01-01 00:00:00 = 946684800
484        let t2 = RtcTime {
485            year: 2000,
486            month: 1,
487            day: 1,
488            hour: 0,
489            minute: 0,
490            second: 0,
491        };
492        assert_eq!(rtc_to_epoch(&t2), 946684800);
493
494        // 2026-03-05 12:30:45
495        let t3 = RtcTime {
496            year: 2026,
497            month: 3,
498            day: 5,
499            hour: 12,
500            minute: 30,
501            second: 45,
502        };
503        let epoch3 = rtc_to_epoch(&t3);
504        // Sanity check: should be > 2025-01-01 epoch (1735689600)
505        assert!(epoch3 > 1_735_689_600);
506    }
507
508    #[test]
509    fn test_rtc_read_returns_valid_time() {
510        let time = rtc_read();
511        // rtc_read() delegates to read_rtc() -- on host, CMOS reads return 0
512        // so we just check the struct is constructible and fields are in range
513        assert!(time.month <= 12);
514        assert!(time.day <= 31);
515        assert!(time.hour <= 23);
516        assert!(time.minute <= 59);
517        assert!(time.second <= 59);
518    }
519
520    #[test]
521    fn test_rtc_ioctl_invalid_command() {
522        assert_eq!(rtc_ioctl(0xFF, 0), -1);
523    }
524
525    #[test]
526    fn test_rtc_ioctl_alarm_set() {
527        let alarm = RtcAlarm {
528            hour: 14,
529            minute: 30,
530            second: 0,
531        };
532        let result = rtc_ioctl(RTC_ALM_SET, &alarm as *const RtcAlarm as u64);
533        assert_eq!(result, 0);
534
535        // Invalid alarm time
536        let bad_alarm = RtcAlarm {
537            hour: 25, // invalid
538            minute: 0,
539            second: 0,
540        };
541        let result2 = rtc_ioctl(RTC_ALM_SET, &bad_alarm as *const RtcAlarm as u64);
542        assert_eq!(result2, -1);
543    }
544
545    #[test]
546    fn test_rtc_ioctl_alarm_enable_disable() {
547        assert_eq!(rtc_ioctl(RTC_AIE_ON, 0), 0);
548        assert!(ALARM_ENABLED.load(Ordering::Relaxed));
549
550        assert_eq!(rtc_ioctl(RTC_AIE_OFF, 0), 0);
551        assert!(!ALARM_ENABLED.load(Ordering::Relaxed));
552    }
553
554    #[test]
555    fn test_alarm_callback_registration() {
556        fn dummy_callback() {}
557        set_alarm_callback(dummy_callback);
558        let stored = ALARM_CALLBACK.load(Ordering::Relaxed);
559        assert_ne!(stored, 0);
560        // Reset
561        ALARM_CALLBACK.store(0, Ordering::Relaxed);
562    }
563}