<feed xmlns='http://www.w3.org/2005/Atom'>
<title>gbos.git/Makefile, branch master</title>
<subtitle>gbos - a tiny Unix-flavored OS for the Game Boy Color (kernel, shell, coreutils in C, RAM filesystem)</subtitle>
<id>https://git.sl0p.foo/gbos.git/atom/Makefile?h=master</id>
<link rel='self' href='https://git.sl0p.foo/gbos.git/atom/Makefile?h=master'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/'/>
<updated>2026-09-24T21:08:46Z</updated>
<entry>
<title>usr: sort, cmp, strings, du, time</title>
<updated>2026-09-24T21:08:46Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-24T21:08:46Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=4bfe20f88b93645f9a277c9903620d75f0ddc179'/>
<id>urn:sha1:4bfe20f88b93645f9a277c9903620d75f0ddc179</id>
<content type='text'>
- sort [-r] [-n]: everything in RAM, which bounds it - a task has 8 KiB and a
  file caps at 3 KiB, so 96 lines of 2400 bytes with room for the stack.
  Insertion sort over an index; at that size the simple thing is fast and
  costs far less ROM. `uniq -c f | sort -n -r` finally works.
  NB the key needs a scratch SLOT of its own: gt() compares table entries, so
  reusing slot i as scratch overwrites the entries being shifted into it.
- cmp: reports the first differing byte, silent when the files match. There
  was no way to check a copy on the machine - you had to haul both to a host.
- strings [-n N]: pairs with xxd now that /bin/&lt;prog&gt; is readable. It finds
  "gbos sm83 (Game Boy Color)" inside /bin/uname.
- du [dir]: 256-byte blocks, the unit df and the filesystem use. Bytes
  overflow - /bin alone is 52 x 16 KiB, which wrapped a 16-bit counter and
  printed 32768. Recurses by re-opendir'ing, since the kernel keeps only one
  "directory being listed" cursor.
- time CMD: 1/64 s resolution from the tick counter. It rebuilds the child's
  command line at $A000 - which is its own argument area, so everything is
  copied to the stack first.
</content>
</entry>
<entry>
<title>usr: touch, tr, rev, cut, uniq</title>
<updated>2026-09-24T20:45:04Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-24T20:45:04Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=4cd933009dd6ce87fe710acfca6b1f786d51040c'/>
<id>urn:sha1:4cd933009dd6ce87fe710acfca6b1f786d51040c</id>
<content type='text'>
    /# ls -l /bin &gt; b
    /# cut -d \s -f 1 b | uniq -c
    52 -

- touch FILE...: gbfs has no timestamps (an MBC5 cart has no RTC), so touch
  does the half that means something here - make an empty file. O_APPEND is
  exactly right: it creates, and it does not truncate what is already there.
- tr [-d] SET1 [SET2]: ranges (a-z), -d to delete, and a short SET2 padded
  with its last character, like tr(1). ASCII only - a 128-entry map.
- rev, uniq [-c]: one line each, from a file or stdin like the other filters.

Two things the tests caught. A bare "-" is a SET, not a flag, so `tr . -`
has to skip flag-matching only when a letter follows the dash. And uniq
compared an empty "line" against the last run at EOF and flushed a phantom
blank one - a stray "1" in uniq -c output.

tr and cut also take \s \t \n \r escapes, because the shell has no quoting:
`-d ' '` arrives as three tokens, so a space or tab delimiter was simply
untypeable. Shell quoting would be the better fix; this makes the tools
usable today without touching the parser.

Banks 49-53; 10 program slots left in the 1 MB ROM.
</content>
</entry>
<entry>
<title>usr: xxd - hex dump a file or stdin</title>
<updated>2026-09-24T11:56:42Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-24T11:56:42Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=0fa1b946ec5fd3f5cba49215ea9ab62c4004aa53'/>
<id>urn:sha1:0fa1b946ec5fd3f5cba49215ea9ab62c4004aa53</id>
<content type='text'>
Eight bytes per line rather than the usual sixteen: offset + 8 hex pairs +
the ASCII column is 38 of the terminal's 40 columns, where 16 would wrap
every line and make the dump unreadable. -c overrides it.

No -s (skip): there is no seek syscall, so it streams from the start - the
same reason tail(1) has to keep a ring.

    /# xxd h
    0000: 48 65 6c 6c 6f 20 67 62 Hello gb
    0008: 6f 73 0d 0a             os..

blk(1) could already peek at raw disk blocks; nothing could look inside a
file. Verified against known bytes, from stdin, with -c, through a pipe, and
on a missing file.
</content>
</entry>
<entry>
<title>usr: ls -l, df, mv, tail, tee, sleep, help, poweroff</title>
<updated>2026-09-24T10:48:51Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-24T10:48:51Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=65b53de14dc6918db39b6ee6dc032f805182daad'/>
<id>urn:sha1:65b53de14dc6918db39b6ee6dc032f805182daad</id>
<content type='text'>
Filling in the core utilities the box was missing:

- ls -l: type, links and size, now that stat() exists. The screen is 40
  columns, so it is "d  1   256 name", not the GNU sprawl. Sizes were
  previously unknowable except one file at a time with wc -c.
- df: blocks and inodes, used and free, on the 32 KiB cart disk.
- mv: no rename syscall, so it is cp + rm - and the copy has to finish before
  the source goes, or a failure loses the file.
- tail [-n N]: no seek syscall, so it streams the input through a ring of N
  lines rather than jumping to the end. N is capped by that ring.
- tee: the missing half of a pipeline - `cat big | tee copy | wc -l`.
- sleep: the thing every script needs; the kernel counts 1/64 s ticks.
- help: lists every command the shell knows, walked out of the kernel's own
  name table. The box previously had no way to tell you what it could do.
- poweroff: the only safe way to stop a gbos that has data on it. The cart's
  battery RAM is written when the emulator exits cleanly, so killing it loses
  the whole filesystem - it comes back "formatted". Found while testing /rc.
</content>
</entry>
<entry>
<title>usr: ircd - an IRC server running on the Game Boy</title>
<updated>2026-09-22T19:54:13Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-22T19:54:13Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=0a7ff09a910eaf5bcb0dc76d08dc5a6c9c239495'/>
<id>urn:sha1:0a7ff09a910eaf5bcb0dc76d08dc5a6c9c239495</id>
<content type='text'>
    /# ircd &amp;
    host$ irssi -c 10.0.0.2       /join #gbos
    other GB$ irc 10.0.0.2 gb2    /join #gbos

Implements the subset a real client needs to get in and talk: NICK, USER,
PING/PONG, JOIN, PART, PRIVMSG/NOTICE (to a channel or to a nick), NAMES and
QUIT, with the 001/004/375/372/376 numerics clients wait for on registration,
353/366 on join, and 421 for anything else. One channel per client keeps the
bookkeeping in fixed-size tables; this is a handheld with 8 KiB of task RAM.

MAXCL is 3, bounded by the kernel socket table (listener + client each take a
slot, the rest left for other programs). A fourth client is refused and the
daemon keeps serving - which is how the ~30s close() stall turned up.

Every event is logged to the LCD, so the Game Boy shows its own server
traffic: "+ alice", "alice joined", "&lt;alice&gt; hello".

Verified with real clients over the hub: two and three simultaneous hosts
registering, joining, channel relay (sender not echoed to itself), private
messages by nick, PING/PONG, unknown-command numerics, QUIT propagation, the
overflow refusal, and a second Game Boy running gbos's own irc(1) in the same
channel as a laptop client - messages crossing both ways between them.
</content>
</entry>
<entry>
<title>build: rebuild every object when include/*.inc changes</title>
<updated>2026-09-22T19:53:54Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-22T19:53:54Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=1e9604bb7f794fcea8edf2ce69d5701fb6e0fb3e'/>
<id>urn:sha1:1e9604bb7f794fcea8edf2ce69d5701fb6e0fb3e</id>
<content type='text'>
The pattern rule listed no header dependency, so editing a constant in
include/gbos.inc rebuilt only the .asm files that had themselves changed.
Bumping MAX_SOCKS left the kernel half-built against the old value - a
socket table sized 6 in socket.asm while dmesg still printed "4 sockets" -
and the resulting misbehaviour looked exactly like a logic bug in the new
socket code, which cost real time to chase.
</content>
</entry>
<entry>
<title>usr: httpd - the Game Boy serves its filesystem over HTTP</title>
<updated>2026-09-22T15:12:52Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-22T15:12:52Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=30e67fe0f4e66e2161ca61f870f8947d50287995'/>
<id>urn:sha1:30e67fe0f4e66e2161ca61f870f8947d50287995</id>
<content type='text'>
    /# httpd &amp;
    host$ curl http://10.0.0.2/        -&gt; a listing of the cart RAM
    host$ curl http://10.0.0.2/readme  -&gt; the file, from battery-backed SRAM

GET only. "/" serves /index.html if present, else a generated listing; any
directory lists with every entry linked, so you can click around a 1997
handheld's disk from a browser. Content type from the extension. No
Content-Length: HTTP/1.0 + Connection: close delimits the body by closing,
which suits a stack with one in-flight segment.

All output goes through one 200-byte buffer, so each net_send is a full
segment instead of one tiny segment per string.

It drains the whole request, not just the request line. That is not
politeness: leaving the rest unread means those segments are never
acknowledged, so the client retransmits headers for a minute, never sends
its FIN, and its dead connection keeps arriving at port 80 long after we
re-listen. One curl with 1.4 KB of headers stalled every following request
for 30-120s.

It deliberately does NOT read the keyboard either. Backgrounded - the useful
way to run it - every key it polled would be a key stolen from the shell's
prompt. It is a daemon: kill(1) stops it, and the kernel reclaims its socket.

Verified with curl: index, byte-exact file bodies, nested paths, directory
listings, 404, 501, a 1.4 KB request spanning segments, browser user-agents,
three parallel clients (0.1/1.1/2.1s), the shell staying usable alongside it,
and six kill/restart cycles - more than there are sockets.
</content>
</entry>
<entry>
<title>usr: grep, more, cp, nc - and make pollcon pump the link</title>
<updated>2026-09-22T14:02:55Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-22T14:02:55Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=43e43b6dcec29e2b008d20135f5d02523b62fbcf'/>
<id>urn:sha1:43e43b6dcec29e2b008d20135f5d02523b62fbcf</id>
<content type='text'>
The shell has had pipes for a while with nothing to pipe *through*, and
an 18-row screen with no way to stop output scrolling off it.

- grep [-vin] PAT [file]: substring match (no regex), stdin or a file,
  grep(1)'s exit convention. The other end of the pipe, at last.
- more [file]: pages 17 lines at a time, --More-- bar in white-on-blue,
  any key pages, q quits. Counts *screen* rows, so wrapped lines pay
  their real cost. Keys come from pollin/pollcon, not stdin, so
  `cat big | more` still has a keyboard.
- cp SRC DST: the fs has rm/mkdir but no way to duplicate a file. mv is
  this plus rm (there is no rename). Rejects SRC == DST, which would
  otherwise truncate the source via O_WRITE before reading a byte.
- nc [-s] HOST PORT: raw TCP. Interactive (START sends the line with a
  CRLF, /q quits) or -s to pump stdin and print the reply with its own
  wall-clock idle timeout, because the kernel's blocking recv counts
  pumps, not seconds. Generalizes what chat/irc hardcode:
  `nc towel.blinkenlights.nl 23` works.

sys_pollcon now pumps the link before draining the console ring. The
ring is filled *by* net_pump, so a program that never touches the
network polled a ring nothing would ever fill: host-injected keys
(gbtype/gbdemo) hung `more` forever, and the Game Boy stopped answering
pings for as long as it ran. KGetc already pumps for this reason.

New tools take banks 35-38. Locals over statics in all four: a program's
_DATA starts at $A000, where the shell leaves the command line.
</content>
</entry>
<entry>
<title>term: 7-color terminal (per-glyph fg+bg) + ANSI escapes; colorize irc</title>
<updated>2026-07-18T18:41:27Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-18T18:41:27Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=9e7d5f6e5e01b7473b84bc3449d7f8e0f78e7045'/>
<id>urn:sha1:9e7d5f6e5e01b7473b84bc3449d7f8e0f78e7045</id>
<content type='text'>
Fano-plane palette scheme: the 7 colors map onto 7 CGB BG palettes so
any color pair shares one palette; a tile's palette is picked from the
set of colors its two cells need (tables generated by tools/gencolor.py).
Per cell a packed (bg&lt;&lt;4)|fg byte lives alongside the char shadow, and
the glyph blitter steers glyph/empty pixels to each cell's fg/bg color
slots via plane masks.

An ANSI-ish CSI parser (ESC [ .. m) drives it; usr/ansi.c demos it and
the irc client now renders hashed nick colors, status dimming and a
channel-activity bar.
</content>
</entry>
<entry>
<title>kernel: a real timer source (64 Hz tick IRQ) + uptime(1)</title>
<updated>2026-07-18T08:10:17Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-18T08:10:04Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=0cc9b0a8bdf774ee376937d5a6767ccdced51245'/>
<id>urn:sha1:0cc9b0a8bdf774ee376937d5a6767ccdced51245</id>
<content type='text'>
The kernel had no clock: TimerISR was a reti stub, IEF_TIMER masked, and
IME was never enabled - the vectors were decorative. sys_sleep just polls
DIV deltas per-process; nothing counted globally.

Now: TAC runs the hardware timer at 16384 Hz with TMA=0, so TIMA overflows
at exactly 64 Hz; TimerISR increments a monotonic 32-bit wTicks (wraps
after ~2.1 years). Scheduling stays cooperative - the ISR is transparent.

Enabling IME in a kernel written for zero interrupts needs care wherever
SP points into memory whose bank is being switched (an IRQ pushes onto SP):
  - read_block/write_block map the disk bank over the $A000 window that
    holds the caller's stack -&gt; di/ei around the transfer (~2ms, well under
    the 15.6ms tick period, so no tick is ever lost)
  - hSwitchTo switches SVBK + cart-RAM banks under the outgoing stack -&gt;
    di on entry, ei once the incoming stack is mapped
  - fork already runs on KSTACK_TOP2 (fixed WRAM) - safe as-is
  - term_putc's SVBK switch only remaps $Dxxx, stacks live in $Axxx/$Cxxx

SYS_UPTIME (36) copies the counter (4B LE, di/ei so the read can't tear)
to a user buffer; libc gticks(); usr/uptime.c formats 'up [Nd] H:MM:SS'.
uptime avoids SDCC long div/shift entirely: sm83.lib modules link into
their own areas that land in the $A000 RAM window (latent build.sh trap,
documented there) - bytewise &gt;&gt;6 plus bounded subtraction loops instead.
</content>
</entry>
</feed>
