feat: extend rtc with RtcDateTime

This commit is contained in:
dualfroz committed 2026-09-01 21:13:01 +02:00
1 parent 8af9a09f89
commit 30188c37d0
2 files changed
+165 -1

No files matched your search

+65
View File
@@ -0,0 +1,65 @@
#ifndef FROSTBYTE_RTC_H
#define FROSTBYTE_RTC_H
#include "types.h"
/*
* CMOS real-time clock driver (MC146818-compatible, ports 0x70/0x71) --
* the first source of real wall-clock date/time this kernel has ever had.
* Every other notion of "time" in frostbyte (pit.h's tick counter,
* threadSleep, userAlarm) is relative to boot, not to the calendar; this
* is the piece that lets a process ask "what day is it" at all.
*
* Honest limitation, same spirit as syscall.c's own documented fork()-less
* design: the CMOS century register's index is not standardized (it is
* only discoverable via the ACPI FADT table, which this kernel does not
* parse), so rtcReadDateTime hardcodes the century to 20 -- correct for
* any date from 2000 through 2099, which covers every real boot this
* kernel will ever see, but not a documented general-purpose solution.
*/
typedef struct RtcDateTime {
int64 year; /* full year, e.g. 2026 */
int64 month; /* 1-12 */
int64 day; /* 1-31 */
int64 hour; /* 0-23, already normalized out of 12-hour/PM if needed */
int64 minute; /* 0-59 */
int64 second; /* 0-59 */
} RtcDateTime;
/* Converts a single CMOS BCD byte (e.g. 0x59) to its binary value (59).
Exposed on its own, like ata.c's ataModelString, because it is a real
piece of the conversion pipeline worth testing in isolation from any
hardware access. */
uint8 rtcBcdToBinary(uint8 bcd);
/* Reads the current date/time off the CMOS RTC. Polls Status Register A's
Update-In-Progress bit (bounded, like ata.c's ataPollTimeout) before
each of two back-to-back raw reads and only accepts the result once both
raw reads agree byte-for-byte, which rules out a read torn mid-tick by
the RTC's own once-a-second update. Returns false only if the two raw
reads never agreed within the bounded retry budget (never hangs). */
bool rtcReadDateTime(RtcDateTime* out);
/* Civil calendar date/time -> seconds since the Unix epoch
(1970-01-01T00:00:00Z), via Howard Hinnant's days_from_civil algorithm
(proleptic Gregorian, correct for any year the RtcDateTime fields above
can represent). Pure arithmetic, no hardware access -- deterministic and
testable without a real clock. */
int64 rtcEpochSeconds(const RtcDateTime* dt);
/* rtcReadDateTime + rtcEpochSeconds in one call, the usual entry point for
anything that just wants "now" as a plain integer (sysTime, syscall.c).
Returns 0 if the underlying read failed. */
int64 rtcNow(void);
/* Seconds since the Unix epoch -> civil calendar date/time, the exact
inverse of rtcEpochSeconds above (Howard Hinnant's civil_from_days,
proleptic Gregorian, same algorithm family as daysFromCivil in rtc.c).
Correct for any epochSeconds value representable in int64, including
negative ones (dates before 1970). Pure arithmetic, no hardware access
-- the piece sfs.h's on-disk mtime/ctime (raw epoch integers) needs to
become a human-readable date, e.g. for a shell `ls -l`. */
void rtcCivilFromEpoch(int64 epochSeconds, RtcDateTime* out);
#endif
+100 -1
View File
@@ -3,13 +3,78 @@
#include "types.h" #include "types.h"
#include "vfs.h" #include "vfs.h"
#include "prioq.h"
#include "isr.h"
#include "signalnum.h"
#include "mmap.h"
/* Default scheduling weight (xv6/CFS-style "nice" niceWeight -- higher runs
more often, not longer). threadSetPriority scales relative to this. */
#define schedDefaultWeight 1024u
/* Returned by threadCreate/threadCreateUser when the thread table is full
(see threadCapacity) -- never a valid thread id, since id 0 is always the
permanent idle thread. Callers must check for this before using the
result to index into anything thread-related. */
#define threadInvalidId 0xFFFFFFFFu
/* Capacity of Thread.name, including the terminator. Large enough for every
name this kernel currently produces (kernel thread literals, "/bin/..."
paths passed to threadCreateUser by spawnUser/doSpawn) with room to
spare; longer names are truncated, never overflowed. */
#define threadNameMax 32
/* Capacity of Thread.env below: envMaxVars is how many "NAME=VALUE" slots
a process may hold at once, envEntryMax is the max size in bytes of a
single slot INCLUDING both the '=' separator and the nul terminator
(so "NAME=VALUE\0" must fit in envEntryMax bytes). Generous enough for
anything this kernel's own shell/demos actually set (see user/envprint.c)
with room to spare, the same "small fixed table, not a growable heap
allocation" choice Thread.cwd already made for a single path. */
#define envMaxVars 8
#define envEntryMax 64
/* One bit per signal number in Thread.sigPending/sigBlocked, and the size
of Thread.sigHandlers[] below -- see signal.h for the actual signal
subsystem this supports. Defined here, not signal.h, purely to break the
include cycle (signal.h includes sched.h for the Thread type). */
#define sigMaxSignals 32
/* Depth of the per-thread nested-handler context stack (Thread.sigSavedRegs/
sigSavedBlocked below) -- how many signal handlers may genuinely interrupt
each other (handler A running, a second signal arrives and is not blocked
by A's own sa_mask, so handler B runs nested inside A, then A resumes once
B returns) before sigConsumePending (signal.c) refuses to deliver a new
one and simply leaves it pending. A real kernel bounds this too (rlimit
on the signal-frame stack it pushes into user memory); this one has no
user-memory signal frame to overflow at all (see signal.h's file comment
-- the saved context lives in the Thread struct itself), so the bound
exists purely to keep that struct's size fixed and to fail a runaway
signal-storm safely (further deliveries just wait) instead of unboundedly
growing kernel memory per thread. 4 is generous headroom for anything
this kernel's own handlers actually do (none nest more than one deep
today -- see user/nesttarget.c) without being large enough to make
Thread noticeably bigger than sigMaxSignals-worth of handlers already
makes it. */
#define sigMaxNestDepth 4
typedef enum { typedef enum {
threadUnused = 0, threadUnused = 0,
threadRunnable = 1, threadRunnable = 1,
threadRunning = 2, threadRunning = 2,
threadSleeping = 3, threadSleeping = 3,
threadDead = 4 threadDead = 4,
threadBlocked = 5,
/* Job-control stop (signal.h's SIGSTOP/SIGTSTP): excluded from
schedule()'s ready-queue rebuild exactly like Sleeping/Blocked
already are (only threadRunnable is ever inserted), so a stopped
thread simply never gets picked until threadContinue moves it back
to threadRunnable. Distinct from threadBlocked because it is NOT
waiting on a wait-channel condition -- schedWakeChannel must never
treat it as a match (see schedWakeChannel), only threadContinue
(an explicit SIGCONT, not "the resource became available") ever
moves a thread out of this state. */
threadStopped = 6
} ThreadState; } ThreadState;
typedef void (*ThreadEntry)(void); typedef void (*ThreadEntry)(void);
@@ -19,7 +84,41 @@ typedef struct Thread {
uint64 stackBase; uint64 stackBase;
uint64 kernelStackTop; uint64 kernelStackTop;
uint64 sleepUntil; uint64 sleepUntil;
/* Absolute pitTicks() deadline for a pending alarm(2)-style SIGALRM
(0 == no alarm pending), set by threadSetAlarm and consulted once
per timer tick by schedule() -- the exact same shape as sleepUntil
above, checked in the exact same loop, except expiry raises a real
signal (sigRaise, signal.h) against the owning thread instead of
just flipping its own ThreadState back to Runnable. Deliberately a
plain deadline, not a wait-channel/ThreadState change: unlike
threadSleep, a thread with a pending alarm keeps running (or
sleeping, or blocked on something unrelated) exactly as it already
was right up until the deadline, exactly like real alarm()'s timer
ticking down in the background regardless of what the process is
doing. */
uint64 alarmAt;
/* poll.h's sysPoll (syscall.c) uses the same restart-the-syscall park
pattern sysRead/sysWrite/sysWait already use, but a poll call can be
woken by any one of several unrelated fds, not one -- so instead of
parking on a single fd's own wait channel (vfsBlockingChannel),
sysPoll blocks every waiting thread on ONE shared channel
(schedPollChannel below) and simply re-checks its whole fd list from
scratch on every wake, spurious or not, exactly like the retry loop
every OTHER restartable syscall already has. pollActive marks "this
thread is mid a not-yet-satisfied sysPoll call" (so a retry entry
can tell itself apart from a genuinely fresh call); pollDeadline is
the absolute pitTicks() deadline schedule()'s own expiry scan forces
a wake at if no fd ever becomes ready first (0 == wait forever,
meaningful only while pollActive is true -- the same "0 reserved"
convention alarmAt above already uses, but here 0 is a real,
legal, requested state (a timeout of -1) rather than "nothing
pending"). */
bool pollActive;
uint64 pollDeadline;
uint64 brk; uint64 brk;
uint64 vruntime;
uint64 exitCode;
uint32 weight;
uint32 id; uint32 id;
ThreadState state; ThreadState state;
const char* name; const char* name;