Despro - The microcomputer of yesterday
In late 2021, I stumbled upon the Desmos global math art contest, a yearly challenge to make a compelling piece of art with the online Desmos graphing calculator. My entry was a virtual microcomputer styled after the Kaypro II “luggable” computer from the early eighties.
Embedded below is the computer I made, the “Despro”. Try pressing 1
on the on-screen keyboard to load the example program. When you’re done, load an empty file to shutdown.
Read on to find out how I built it.
Text in Desmos
The first challenge when making a computer in a graphing calculator is the display. While Desmos has facilities for displaying textual labels, there isn’t a straightforward way to process them, and any attempts at alignment will quickly fall apart when the canvas is zoomed or panned. My solution? Draw the glyphs manually by creating a 14 segment display out of polygons. With a bit of data entry, you can display retro-looking monospace text by making each character into its own function.
To display text from a buffer, simply make a function that selects a character from a map. The below Desmos function displays the character one, two, or three at the specified position on the graph. The function used in the finished computer was extended to the entire alphabet and punctuation.
$r_{l}\left(i_{n},x_{p},y_{p}\right)=\left\{i_{n}=1:l_{1}\left(x_{p},y_{p}\right),i_{n}=2:l_{2}\left(x_{p},y_{p}\right),i_{n}=3:l_{3}\left(x_{p},y_{p}\right)\right\}$
$i_n$
is the index in the character map,$x_p$
is the x position on the graph, and$y_p$
is the y position on the graph.
The character map differs from ASCII. For example, the alphabet starts at 11 instead of 65.
Memory addressing in Desmos
In Desmos, the available datatypes are lists, colors, functions and arbitrary precision numbers. Because lists in Desmos are limited to 10 thousand elements, the Despro’s main memory has 16 banks or “pages”. Each cell in memory is a floating point number, and I will refer to them as “words” from now on. Any given word in memory can be indexed by its page and word indices. In addition to the main banks, devices also have seperate banks, laid out as shown below.
bank | device |
---|---|
-5 | keyboard |
-3 | disk |
-2 | clock |
-1 | config |
Instruction decoding in Desmos
The Despro uses single-word instructions with subsequent arguments that can take a variable number of words. The opcode itself can have a variable number of flags, which are represented as a binary number added to the base opcode number.
For example, the halt
instruction has no flags and no arguments, so it is represented as simply $[0]$. Instructions with flags get slightly more complicated.
Desmos does not let you address numbers bitwise, so the Despro uses the following formula to convert opcodes to base two.
$$t_{oBase}\left(x_{p},r_{1}\right)=\left[\operatorname{floor}\left(\frac{\operatorname{mod}\left(x_{p},r_{1}^{\left(i_{r}+1\right)}\right)}{r_{1}^{i_{r}}}\right)\operatorname{for}i_{r}=\left[0…\operatorname{floor}\left(\frac{\log\left(x_{p}\right)}{\log\left(r_{1}\right)}\right)\right]\right]$$
An example of an instruction with flags is load
. The possible flags are:
- in relative
- in indirect
- out relative
- out indirect
Since load
’s base opcode is 1, a relative load to an absolute location would be represented as 1+8, or $[9, \text{args}]$ in memory.
.db 42,
ld(r) 1,-1,2,1; load 42 into bank 2, word 1
Arguments are placed sequencially in memory, so the full full load
instruction shown above would be represented as $[9,1,-1,2,1]$.
Other instructions have different amounts of arguments or flags, but they are represented in memory using the same method.
An operating system in Desmos
Program headers
On Linux (as an example), most programs are stored in ELF files, which have a header that contains information about a program’s size and type.
Because there is only one kind of Despro program described in this format, the headers are fairly simple.
The first six words of the buffer are the program’s name and the one after is the program’s length.
After that there are four words of padding and the program itself starts at word 12.
The “padding” is used by the program loader as a pseudo stack. The first word is used to store the page that the program is linked against, and the remaining three words happen to correspond precisely with the length of a jump. Instead of maintaining a call stack, the OS puts the reset vector into the start of the loaded program, so every program can exit
by jumping to word 9 in its initial page.
.db 18,15,22,22,25,33,
.db len(74)
.db 5,53,10,1,
This example header is from the hello world program, with the reset vector pointing to bank 10 slot 1
Loading programs
When the Despro boots up, it goes to the fault vector at 10:1
. 10:1
contains the 17-word bootloader which loads the operating system into page 1 and runs it. The operating system then looks through page 5-8 for programs and displays their names using the format described above. Running programs is as simple as jumping to them (word 12), as programs are expected to be agnostic to what page they are in. Currently, all programs copy themselves to page one as a first step, primarily to ease hand-assembling them.
Hello World!
Given what we know, let’s try printing out “Hello World” as a smoketest. No assembler exists for the Despro, but I’ll use an assembly syntax similar to FASM because I find it familiar.
The program makes the following assumptions:
- Page three is zeroed
- The display points at page two
First, let’s make a program header that displays HELLOW as a name in the operating system.
.db 18,15,22,22,25,33, ; "HELLOW"
.db len(74)
.db 5,53,10,1, ; jp 10:1
Before starting our program proper, we copy the program from where it is to the first page to simplify subsequent loads and stores.
ldr 0,1,len(74),1,1 ; copy to zero page
jp 1,21
Next, we blit HELLO WORLD
by first copying from a zeroed page to all of vram, then copying the message to the start of vram.
ldr 3,1,420,2,1 ; clear display
ldr(ir) 0,+17,9,2,31 ; disp hello world
jrnz 0,23
.db (42) 0,0,1,22,25,0,33,25,28,22,14,
.db 26,28,15,29,29,0,43,27,43,
Finally, we handle exiting. In this case, we continually poll if the most recent key was Q
. If so, we jump to the reset vector in the program header.
cmp(xr) =,1,-2,-5,1 ; input q?
jp 0,9 ; quit
jnc 0,39 ; recheck
After assembling (by hand) you get this (75 words):
$d_{isk}=\left[ 18,15,22,22,25,33,74, 5,53,10,1, 17,0,1,74,1,1,53,1,21,17,3,1,420,2,1, 18,0,15,11,2,1, 18,0,20,9,2,31, 54,0,23, 18,15,22,22,25,0,33,25,28,22,14, 26,28,15,29,29,0,43,27,43,34,2,1,-2,-5,1, 49,0,9, 54,0,-9,0,0\right]$
You can copy the above variable into a Despro and run it out of $d_{isk}$
, or run it out of slot 1, as this is the only existing Despro program aside from its operating system.
Inspired? Try making your own programs. I included the opcode list and memory map (which are also in the graph) below for reference.
Appendix: Instruction list
{}: opcode
() : length
halt {0} (1)
load {1} (4)
flags: (in relative, in pointer, out relative, out pointer)
arguments: (in page, in word, out page, out word)
loadrepeat {17} (5)
flags: (in relative, in pointer, out relative, out pointer)
arguments: (in page, in word, length, out page, out word)
compare {33} (5)
flags: (x relative, x pointer, y relative, y pointer)
arguments: (operation, x page, x word, y page, y word)
index | operation |
---|---|
0 | false |
1 | true |
2 | == |
3 | != |
4 | >= |
5 | <= |
6 | > |
7 | < |
8 | ! |
Appendix: Memory map
page | device |
---|---|
-5 | keyboard |
-3 | disk |
-2 | clock |
-1 | config |
1 | program init |
Appendix: Memory map (-5)
Keyboard
word | description |
---|---|
1 | most recent character typed |
Appendix: Memory map (-1)
Configuration
word | description |
---|---|
1 | display columns |
2 | display rows |
3 | phosphor red |
4 | phosphor green |
5 | phosphor blue |
6 | vram page |
7 | keyboard page |
8 | program counter (page) |
9 | program counter (word) |
13 | comparison result |