🚩 核心目標

今天的開發讓 SmallOS 擁有了「時間感」。我們不再只是被動地回應鍵盤輸入,而是讓內核主動感知時間的流逝。我們啟用了 可編程間隔定時器 (PIT) 來產生週期的系統滴答(Ticker),並透過訪問 CMOS 獲取真實世界的即時時間(Wall Clock Time)。


1. 基礎理論:可編程間隔定時器 (PIT 8254)

PIT 是 x86 系統的時間心臟,以固定的 1,193,181 Hz 頻率運行。我們透過設置 Divisor (除數) 來決定每次中斷的時間間隔。

理解 Divisor 的運作


2. 代碼實作:核心定時器

我們在中斷處理程序(ISR)中維護了一個全域計數器,並將其即時更新在螢幕的右上角。

#include "timer.h"
#include "io.h"
#include "isr.h"
#include "display.h"

static u32 tick = 0; // 記錄系統啟動以來跳動了多少次

u32 get_ticker()
{
    return tick;
}

void sleep(u32 ticksToWait)
{
    u32 endTick = tick + ticksToWait;
    while (tick < endTick)
    {
        //make cpu goes to hlt mode, and will come back when next interval, like timer comes
        __asm__ __volatile__("hlt");
    }
}

static void timer_callback(u32) {
    tick++;

    // 因為我們設置了沒秒100次中斷,所以這裡基本就是1秒刷一次
    // 但是這裡沒有處理tick溢出問題
    if (tick % 100 == 0)
    {
        // 每秒更新一次螢幕右上角
        u32 seconds = tick / 100;
        draw_time_at_corner(seconds);
    }
}

void init_timer(u32 freq) {
    tick = 0;
    
    // 註冊 IRQ 0 (中斷號 32)
    register_interrupt_handler(0, timer_callback);

    // 計算除數
    // 1193180 is the cpu freq for qemu only
    u32 divisor = 1193180 / freq;

    // 發送命令字 (0x36 代表:通道0、存取LOBYTE/HIBYTE、模式3平方波、二進制)
    port_byte_out(TIMER_CONTROL_REG, 0x36);

    // 分兩次發送除數的低 8 位元與高 8 位元
    port_byte_out(COUNTR_0_REG, (u8)(divisor & 0xFF));
    port_byte_out(COUNTR_0_REG, (u8)((divisor >> 8) & 0xFF));
}

3. CMOS 訪問:實現 time 命令

為了讓 Shell 能夠顯示當前日期與時間,我們需要讀取硬體上的 CMOS

#include "io.h" 
#include "types.h"
#include "cmos.h"
#include "util.h"

// 檢查 CMOS 是否正在更新(若在更新中讀取會拿到錯誤數值)
int is_updating() {
    port_byte_out(CMOS_ADDR, RTC_STATUS_A);
    return (port_byte_in(CMOS_DATA) & 0x80);
}

u8 read_rtc_register(int reg) {
    port_byte_out(CMOS_ADDR, reg);
    return port_byte_in(CMOS_DATA);
}

// BCD 轉十進位
u8 bcd_to_bin(u8 bcd) {
    return ((bcd >> 4) * 10) + (bcd & 0x0F);
}

void get_rtc_time_string(char *buffer) 
{
    u8 second, minute, hour, day, month, year;
    
    // 防震盪讀取邏輯
    while (is_updating());
    second = read_rtc_register(RTC_SECONDS);
    minute = read_rtc_register(RTC_MINUTES);
    hour = read_rtc_register(RTC_HOURS);
    day  = read_rtc_register(RTC_DAY); // RTC_DAY_OF_MONTH
    month = read_rtc_register(RTC_MONTH); // RTC_MONTH
    year  = read_rtc_register(RTC_YEAR); // RTC_YEAR    

    u8 regB = read_rtc_register(RTC_STATUS_B);

    // 格式轉換
    if (!(regB & 0x04)) {
        second = bcd_to_bin(second);
        minute = bcd_to_bin(minute);
        hour = bcd_to_bin(hour);
        day  = bcd_to_bin(day);
        month = bcd_to_bin(month);
        year = bcd_to_bin(year);        
    }

    // 簡單的手動格式化 (YYYY-MM-dd HH:MM:SS)
    // 假設 buffer 至少有 20 bytes 空間
    // 年份處理 (假設是 20xx 年, 我們至少還有74年是正確的)
    buffer[0] = '2';
    buffer[1] = '0';
    buffer[2] = (year / 10) + '0';
    buffer[3] = (year % 10) + '0';
    buffer[4] = '-';
    
    // 月
    buffer[5] = (month / 10) + '0';
    buffer[6] = (month % 10) + '0';
    buffer[7] = '-';
    
    // 日
    buffer[8] = (day / 10) + '0';
    buffer[9] = (day % 10) + '0';
    buffer[10] = ' ';
    
    // 時:分:秒
    buffer[11] = (hour / 10) + '0';
    buffer[12] = (hour % 10) + '0';
    buffer[13] = ':';
    buffer[14] = (minute / 10) + '0';
    buffer[15] = (minute % 10) + '0';
    buffer[16] = ':';
    buffer[17] = (second / 10) + '0';
    buffer[18] = (second % 10) + '0';
    buffer[19] = '\\0';
}