Awesome Open Source
Awesome Open Source

zkeme80 - a Forth-based OS for the TI-84+ calculator

Build Status

OS screenshot OS animation

TLDR: assembler.scm is the assembler, zkeme80.scm is the OS. To build the rom, run make build. There are no dependencies apart from a recent version of Guile, supporting the modules bytevectors and sfri-9 records. Other Scheme implementations have not been tested.

Alternatively, if you're using the Nix package manager on macOS or Linux, running nix-build -A runit && ./result in the root of this repository builds the OS and emulator, then runs it.

Why another OS for the TI-84+?

The TI tinkering community has long loathed the proprietary nature of the default TI-OS. Few projects have attempted to create a viable alternative, fewer have matured to a usable state, and none are currently able to actually let you use the calculator as a calculator.

If you've been looking at operating systems for the TI-84+, chances are you've come across KnightOS. It's well developed and has plenty of Unix-like features such as filesystems and tasks, and even a C compiler. But maybe that's not what you want. You want a minimal operating system that allows you to extend it in any way you wish, bonus points if you don't need to know Z80 assembly to do so.

zkeme80 is that operating system, a minimal core with a mostly ANS standard conforming Forth interpreter/compiler. From words covering sprites and graphics, to text and memory access, everything you need to make the next hit Snake clone or RPN-based math layer is already there. zkeme80 lowers the barrier of entry for customizing an operating system and enable rapid development cycles. Below the Forth layer, you'll find two lowest level and highest level languages, Z80 assembly and Scheme. The best assembler is an extensible one, where writing macros should be a joy, not a pain, and Scheme has that macro system.

On my MacBook Pro 11,1 running NixOS it takes around 13.5 seconds (real time) to compile the operating system for the first time with make build, and subsequent builds involving only changes to .fs files take around 0.5 seconds (real time).

Why Forth?

OS development is hard, doubly so if you're using assembly. Keep track of calling conventions, or which routines preserve which registers is a tedious and error-prone task. Nested loops and switch statements are out of the window. And most importantly, it isn't easy to allow the user to extend the operating system. Forth changes that. It's just as low level as assembly, but it can be as high level as you want. Want exceptions? They're already there! Want garbage collection and memory safety? Roll your own! See forth.scm for more than 200 examples of Forth words. If you're not familiar with Forth, I highly recommend Starting Forth by Leo Brodie. Get it here.

Notes on standard-compliance

Some words are not standard. This is because I copied them from my other Forth/Z80 project, which itself is based on jonesforth. However, I did consult the ANS standard to incorporate some of their good ideas. For instance, the test suite currently found in bootstrap-flash4.fs is only a very slight (sans the floating point stuff) adaptation of the offical test suite. The current version of the operating system runs a series of tests to check the correctness of the word environment. As time goes on I may consider making more words standard-conforming.

Did you write all of this?

Most of the assembly code outside of forth.scm was taken from SmileyOS, which itself is based on an older version of the KnightOS Kernel. I chose SmileyOS because it was the most "minimal" needed to get nasty stuff such as locking/unlocking flash, display routines, key routines etc. out of the way. Code here that doesn't exist in SmileyOS was taken from public sources, including the current version of KnightOS. The rest of the operating system is of my own design.

Building and running the operating system

Using the Makefile

Running make build should make generate a file called zkeme80.rom in the same directory. Simply pass that file to an emulator such as jsTIfied (works in the browser) and start playing around!

Running just make builds and runs the project, but assumes that you have already properly built tielm and can run it with tielm2 on the shell, and have Guile installed. Be warned, though, tilem is tricky to build and you have to enable all sorts of flags and install dependencies. If anyone knows a good emulator for macOS, please let me know.

Using the Nix package manager (macOS or Linux)

If you're using the Nix package manager, just clone the repository and run the following to compile and build the assembler, operating system, and emulator. It will automatically run the ROM when done. Props to clever on #nixos for figuring out how to build tilem.

# With flakes
$ nix run
# Without flakes
$ nix-build && ./result

Files included

  • assembler.scm assembles s-exp style assembly code into binary. Simply run (load "assembler.scm") into your Scheme REPL and run(assemble-prog sample-prog) to see the binary data. Run (assemble-to-file sample-prog "out.bin") to write a binary file.
  • zkeme80.scm is the Forth-based operating system. Load zkeme80.scm then run (make-rom "zkeme80.rom") to output binary to a file zkeme80.rom.

Design of the assembler

The assembler's core uses pattern matching. The program counter is implemented as a mutable Scheme object *pc*. Labels are kept in a global alist *labels*. To allow for the use of jumps that refer to labels declared after it, we use multiple passes. The assembler is designed to be extensible from various levels; the source code of the assembler, pass 1 and pass 2. Each layer can be extended using the full power of Scheme.

The extensible nature of the assembler means that users can add whatever features they desire that were not built in already, for instance, re-targeting the assembler or adding missing instructions.

Structure of assembly programs

Assembly programs consist of a list of elements that are either expressions or procedures.

Pass 1

Handling expressions

Each expression of a program is passed to assemble-expr (which also checks if they're well-formed). assemble-expr returns a record type that has the following fields (for a normal instruction):

Record entry Type Description
length integer The length of the instruction, in bytes.
gen-instr lambda Thunk that computes the actual instruction bytes.

The use of converting expressions into record types like this allows us to compute the length of the program (and resolve look ahead labels).

Handling procedures

Procedures (Scheme objects that satisfy the predicate procedure?) that are embedded in a program must be able to be run without any arguments, and return either () or an instruction record. This is the main extension mechanism for the assembler. For instance, in macros.scm there is a procedure called fill-until-end which creates a list of bytes so that the total binary is #x100000 bytes long.

Pass 2

Once the program makes it through Pass 1, we perform code generation and label resolution. All instruction records are required to have a length property that tells in advance how many bytes will be generated from the thunk. Consistency between this number and what the thunk outputs is checked. Each instruction record is also checked that it generates only unsigned 8-bit integers. The result is flattened into a list of unsigned numbers, which can be manipulated as the user wishes.


The debugging process is pretty simple. One just has to write a valid Z80 assembly program in my s-exp format and run it through a disassembler then compare the output. If you're feeling particularly brave you may skip this step and try your program out on a Z80 chip.

Assembler Limitations

There is currently no instruction encoding (like the file) that the assembler accepts, so to add new instructions the current workflow is to look at relevant portions of the Z80 data sheet and write new cases in the pattern matcher. Adding such an encoding would allow the assembler to be retargeted.

Get A Weekly Email With Trending Projects For These Topics
No Spam. Unsubscribe easily at any time.
Assembly (14,049
Scheme (4,065
Nix (2,585
Forth (989
Assembler (755
Z80 (337
Related Projects