今天的開發讓 SmallOS 擁有了「時間感」。我們不再只是被動地回應鍵盤輸入,而是讓內核主動感知時間的流逝。我們啟用了 可編程間隔定時器 (PIT) 來產生週期的系統滴答(Ticker),並透過訪問 CMOS 獲取真實世界的即時時間(Wall Clock Time)。
PIT 是 x86 系統的時間心臟,以固定的 1,193,181 Hz 頻率運行。我們透過設置 Divisor (除數) 來決定每次中斷的時間間隔。
我們在中斷處理程序(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));
}
為了讓 Shell 能夠顯示當前日期與時間,我們需要讀取硬體上的 CMOS。
0x70 選擇索引(Index),再從 0x71 讀取數值。0x23 代表 23 秒),必須轉換為整數才能顯示。#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';
}