aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoruser <user@clank>2026-09-24 23:08:54 +0200
committeruser <user@clank>2026-09-24 23:08:54 +0200
commitbe1a8f74e48d4ed2ae3a0b78a7137b4bc41a2969 (patch)
tree25477cc16331b6e919deab918eecfcc4edebc0cb
parentusr: sort, cmp, strings, du, time (diff)
downloadgbos-master.tar.gz
gbos-master.tar.xz
gbos-master.zip
usr: ntpdate + date - the Game Boy knows what day it isHEADmaster
An MBC5 cart has no RTC, so SNTP is the only route to a wall clock. ntpdate does one 48-byte UDP exchange on port 123 and hands the stamp to the kernel; SYS_SETTIME keeps it with the tick it arrived at, and SYS_GETTIME adds the elapsed ticks back, so date(1) is just formatting. /# ntpdate ntpdate: clock set from pool.ntp.org /# date Sep 24 21:01:23 UTC 2026 (host said 21:01:26) The clock does not survive a power cycle - which is exactly what /rc is for: `ntpdate` there and the machine boots knowing the time. All of it is bounded subtraction, no long division or shifts: SDCC pulls those from sm83.lib, whose modules link into the $A000 RAM window. The month tables are string literals for the same family of reason - an initialized array goes to _INITIALIZED at $A000 and the image overflows its bank ("buffer too small" out of makebin), which is how this first failed to link.
-rw-r--r--README.md12
-rw-r--r--sl0ppty.layout.kdl15
-rw-r--r--slosh.layout8
-rw-r--r--usr/date.c87
-rw-r--r--usr/ntpdate.c81
5 files changed, 199 insertions, 4 deletions
diff --git a/README.md b/README.md
index d0c3e8b..bfd19da 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,10 @@ into the on-screen console. Regenerate it anytime with `tools/gbdemo` (below).*
`more` to page anything longer than the 18-row screen.
- **Shell scripts**: a shell fed from a file (`sh < script`) runs it with no
prompt and no echo, and init runs **`/rc`** at boot if the cart has one —
- so a Game Boy can come up with its own startup script.
+ so a Game Boy can come up with its own startup script (`dhcp`, then
+ `ntpdate`, and it knows the date).
+- **A real clock**: no RTC on the cart, so `ntpdate` fetches the time over
+ SNTP and `date` prints it — verified against the host's clock to the second.
- **Networking, for real**: the kernel owns SLIP over the link port, IPv4,
ICMP, UDP and TCP; userland gets BSD-ish sockets via one syscall. On top:
`dhcp` (boot-time lease), `ping`, `nslookup` (real DNS), `wget` (HTTP),
@@ -44,7 +47,8 @@ into the on-screen console. Regenerate it anytime with `tools/gbdemo` (below).*
- **40×18 color terminal** on the LCD (4×8 font, two chars per tile, ANSI-ish
colors) + an **on-screen keyboard** on the window layer (SELECT toggles it).
- **Persistent filesystem** on battery-backed cart RAM: inodes, nested
- directories, per-process cwd — survives power-off (files up to 3 KiB).
+ directories, hard links (`ln`), per-process cwd — survives power-off
+ (files up to 3 KiB).
`ls -l` and `df` report sizes and free space; `poweroff` is the only safe
way to stop the machine (the battery save is written on a clean exit).
- **`/bin` in the tree**: the builtins are not files on the disk, but they
@@ -53,8 +57,8 @@ into the on-screen console. Regenerate it anytime with `tools/gbdemo` (below).*
bank. Read-only, since it is ROM.
- **A C toolchain**: userland is plain C compiled with SDCC (`sm83`); ~50
tools ship in `usr/` (`cat`, `wc`, `head`, `tail`, `grep`, `cp`, `mv`,
- `tee`, `tr`, `cut`, `uniq`, `rev`, `more`, `ls`, `df`, `xxd`,
- `touch`, `sleep`, `uptime`, …) — `help` lists them all on
+ `tee`, `tr`, `cut`, `uniq`, `rev`, `sort`, `cmp`, `strings`, `more`, `ls`,
+ `ln`, `df`, `du`, `xxd`, `touch`, `sleep`, `time`, `clear`, `uptime`, …) — `help` lists them all on
the Game Boy itself. Adding a program is one `.c` file + a table entry.
## Quick start
diff --git a/sl0ppty.layout.kdl b/sl0ppty.layout.kdl
new file mode 100644
index 0000000..f767add
--- /dev/null
+++ b/sl0ppty.layout.kdl
@@ -0,0 +1,15 @@
+// What this project needs open, written by `save-workspace`.
+// Checked by `sl0ppty --check`; opened by `open-workspace`.
+layout {
+ tab name="gbos" split="cols" {
+ pane weight=2600 split="rows" {
+ pane weight=1000 cwd="." command="omp -c" purpose="agent:main" suspended=true focus=true
+ pane weight=410 cwd="." purpose="shell:gbos"
+ }
+ pane weight=2200 split="rows" {
+ pane weight=3000 cwd="." command="~/dev/gbc/build/sl0pboy --kitty 3 --kitty-b64 --sock /tmp/sl0pboy.sock gbos.gb" purpose="service:emu" suspended=true
+ pane weight=1000 cwd="." command="make" purpose="task:build" suspended=true
+ pane weight=800 cwd="." command="sudo tools/gbhub --daemon" purpose="service:gbhub" suspended=true
+ }
+ }
+}
diff --git a/slosh.layout b/slosh.layout
new file mode 100644
index 0000000..b00c43c
--- /dev/null
+++ b/slosh.layout
@@ -0,0 +1,8 @@
+// What this project needs open, written by `save-workspace`.
+// Checked by `slosh --check`; opened by `open-workspace`.
+layout {
+ tab name="gbos" split="cols" {
+ pane weight=1000 cwd="."
+ pane weight=1000 cwd="." command="tig" suspended=true focus=true
+ }
+}
diff --git a/usr/date.c b/usr/date.c
new file mode 100644
index 0000000..57108ab
--- /dev/null
+++ b/usr/date.c
@@ -0,0 +1,87 @@
+#include "gbos.h"
+/* date: print the wall clock, UTC.
+ *
+ * The kernel hands back unix seconds (SYS_GETTIME); turning that into a date
+ * is all bounded subtraction - no long division or shifts, which SDCC would
+ * pull out of sm83.lib and link into the $A000 RAM window (see usr/uptime.c
+ * and usr/build.sh). ~20k rounds for the day loop is a fraction of a second.
+ *
+ * Says so plainly when the clock has never been set: run ntpdate first. */
+
+/* No initialized arrays: SDCC puts those in _INITIALIZED at $A000, the RAM
+ window, and the image then overflows its bank ("buffer too small" out of
+ makebin - see usr/ansi.c and usr/build.sh). String literals live in _CODE,
+ so the tables are literals indexed by hand. */
+#define MDAYS "\x1f\x1c\x1f\x1e\x1f\x1e\x1f\x1f\x1e\x1f\x1e\x1f"
+#define MNAME "JanFebMarAprMayJunJulAugSepOctNovDec"
+
+static void two(unsigned int v) {
+ if (v < 10) putc('0');
+ putu(v);
+}
+static unsigned char leap(unsigned int y) {
+ if (y & 3) return 0;
+ if (y == 2100) return 0; /* good enough until then */
+ return 1;
+}
+
+void main(void) {
+ unsigned char t[4];
+ union {
+ unsigned long l;
+ unsigned char b[4];
+ } u;
+ unsigned long sec;
+ unsigned int days = 0, year = 1970, h, m, i, dim;
+
+ if (!gettime(t)) {
+ puts("date: clock not set (run ntpdate)");
+ nl();
+ sexit(1);
+ }
+ u.b[0] = t[0];
+ u.b[1] = t[1];
+ u.b[2] = t[2];
+ u.b[3] = t[3];
+ sec = u.l;
+
+ while (sec >= 86400UL) {
+ sec -= 86400UL;
+ days++;
+ }
+ h = 0;
+ while (sec >= 3600UL) {
+ sec -= 3600UL;
+ h++;
+ }
+ m = 0;
+ while (sec >= 60UL) {
+ sec -= 60UL;
+ m++;
+ }
+
+ for (;;) { /* years, then months */
+ unsigned int len = leap(year) ? 366 : 365;
+ if (days < len) break;
+ days -= len;
+ year++;
+ }
+ for (i = 0; i < 12; i++) {
+ dim = (unsigned char)MDAYS[i];
+ if (i == 1 && leap(year)) dim = 29;
+ if (days < dim) break;
+ days -= dim;
+ }
+ writes(MNAME + i * 3, 3);
+ putc(' ');
+ putu(days + 1);
+ putc(' ');
+ two(h);
+ putc(':');
+ two(m);
+ putc(':');
+ two((unsigned int)sec);
+ puts(" UTC ");
+ putu(year);
+ nl();
+}
diff --git a/usr/ntpdate.c b/usr/ntpdate.c
new file mode 100644
index 0000000..bbeeb92
--- /dev/null
+++ b/usr/ntpdate.c
@@ -0,0 +1,81 @@
+#include "resolve.h"
+/* ntpdate [SERVER]: set the clock from SNTP (default pool.ntp.org).
+ *
+ * An MBC5 cart has no RTC, so this is the only way gbos can know the real
+ * time. One 48-byte UDP exchange with port 123: the reply's transmit
+ * timestamp is seconds since 1900, and unix time is that minus 2208988800.
+ * The kernel keeps the stamp plus the tick it arrived at (SYS_SETTIME), so
+ * date(1) can add the elapsed ticks back.
+ *
+ * The clock does not survive a power cycle - put ntpdate in /rc. */
+
+#define NTP_PORT 123
+#define UNIX_EPOCH_HI 0x8383 /* 2208988800 = 0x83AA7E80 */
+
+static unsigned char pkt[48];
+static unsigned char namebuf[64];
+static unsigned char srv[4];
+static unsigned char now[4];
+
+void main(void) {
+ char args[40];
+ unsigned char s, n, i;
+
+ {
+ char *a = getargs();
+ for (i = 0; a[i] && i < 39; i++) args[i] = a[i];
+ args[i] = 0;
+ }
+ if (!args[0]) {
+ const char *d = "pool.ntp.org";
+ for (i = 0; d[i]; i++) args[i] = d[i];
+ args[i] = 0;
+ }
+ for (i = 0; args[i] && i < 63; i++) namebuf[i] = (unsigned char)args[i];
+ namebuf[i] = 0;
+
+ if (!resolve((char *)namebuf, srv)) {
+ puts("ntpdate: cannot resolve ");
+ puts((char *)namebuf);
+ nl();
+ sexit(1);
+ }
+ for (i = 0; i < 48; i++) pkt[i] = 0;
+ pkt[0] = 0x1B; /* LI=0, VN=3, mode=3 (client) */
+
+ s = net_socket(SOCK_UDP);
+ if (s == 0xFF) {
+ puts("ntpdate: no socket");
+ nl();
+ sexit(1);
+ }
+ net_bind(s, 40123);
+ net_connect(s, srv, NTP_PORT);
+ net_send(s, pkt, 48);
+ n = net_recv(s, pkt, 48);
+ net_close(s);
+ if (n == 0xFF || n < 48) {
+ puts("ntpdate: no reply");
+ nl();
+ sexit(1);
+ }
+
+ /* transmit timestamp: seconds since 1900 at bytes 40..43, big-endian.
+ unix = ntp - 2208988800 ($83AA7E80), done bytewise - no long math. */
+ {
+ unsigned char b0 = pkt[43], b1 = pkt[42], b2 = pkt[41], b3 = pkt[40];
+ unsigned int t;
+ t = (unsigned int)b0 - 0x80;
+ now[0] = (unsigned char)t;
+ t = (unsigned int)b1 - 0x7E - ((t >> 8) & 1);
+ now[1] = (unsigned char)t;
+ t = (unsigned int)b2 - 0xAA - ((t >> 8) & 1);
+ now[2] = (unsigned char)t;
+ t = (unsigned int)b3 - 0x83 - ((t >> 8) & 1);
+ now[3] = (unsigned char)t;
+ }
+ settime(now);
+ puts("ntpdate: clock set from ");
+ puts((char *)namebuf);
+ nl();
+}