457 lines
22 KiB
Markdown
457 lines
22 KiB
Markdown
mu808: an even more ultralight numeric VM designed to run everywhere
|
|
====================================================================
|
|
mu808 (always lowercase, pronounced as _micro-bob_) is a minimalist virtual
|
|
machine specification that is based on the
|
|
[1V0](https://github.com/KedalionDaimon/1V0)
|
|
and [808UL](https://codeberg.org/luxferre/808UL) ideas but has an even
|
|
leaner instruction set and is even easier to implement in any modern or even old
|
|
programming language.
|
|
|
|
Overview
|
|
--------
|
|
mu808 is a Harvard-architecture virtual machine with separate memory space for
|
|
programs and data. The only available data type is a floating point number,
|
|
although some implementations may use fixed point numbers instead.
|
|
|
|
Unlike 808UL, mu808 strictly defines support of up to 16384 data cells and
|
|
up to 16383 program steps, with the program step 0 being unavailable and the
|
|
data cell 0 always returning 0.
|
|
|
|
On the machine start, both memory areas are initialized with zeroes and the
|
|
interactive mode is entered which accepts instructions from the platform's
|
|
standard input.
|
|
|
|
### Overall features
|
|
|
|
* 16 runtime instructions (from 0 to 15, including NOP as instruction 0);
|
|
* ability to enter integer commands (positive and negative) into program memory;
|
|
* ability to enter floating (or fixed) point numbers (positive and negative) at
|
|
program runtime;
|
|
* immutability of the data address 0 (must always return 0 when accessed).
|
|
|
|
The following things are implementation-dependent:
|
|
|
|
* instruction and data internal representation;
|
|
* tracing capabilities;
|
|
* runlimit (the amount of program steps before forcefully halting the program).
|
|
|
|
mu808 instruction set
|
|
---------------------
|
|
Unlike 808UL, mu808 only has a core instruction set. Also, regardless of the
|
|
instruction number, all instructions strictly contain five numbers separated
|
|
by a whitespace:
|
|
|
|
* `[lno]` - instruction number,
|
|
* `[cmd]` - command,
|
|
* `[arg1]` - argument 1,
|
|
* `[arg2]` - argument 2,
|
|
* `[arg3]` - argument 3.
|
|
|
|
Any mu808 implementation must not distinguish between a whitespace and a newline
|
|
character: both of them, if supported, must be used to delimit individual parts
|
|
of an instruction and not instructions as a whole. Instructions as a whole,
|
|
however, are separated naturally by reading five delimited numbers at a time.
|
|
|
|
This rule applies to the plaintext format of the machine code. See the section
|
|
about mu808 file formats to find out about the binary machine code format.
|
|
|
|
### Instruction number semantics
|
|
|
|
* Any positive integer: the instruction is written into the program memory under
|
|
this address (any previous instruction at this address is overwritten).
|
|
* 0: the instruction is NOT written into the program memory and is immediately
|
|
executed instead. If the instruction is a jump instruction that jumps into a
|
|
valid positive address, program execution in the program memory begins until
|
|
the program memory is exhausted.
|
|
* -1: displays a range of instructions from address `[cmd]` to `[arg1]` (incl).
|
|
* -2: if `[cmd]` is 0, clears a range of instructions from address `[arg1]` to
|
|
`[arg2]` (inclusively), otherwise clears a range of data from address `[cmd]`
|
|
to `[arg2]` (inclusively).
|
|
* -3: if supported, turns off execution tracing output if the `[cmd]` value is
|
|
equal to 0, otherwise turns it on.
|
|
* -4: if supported, changes the program runlimit to the value of `[cmd]`.
|
|
* -5: if supported, the VM exits to the OS environment, otherwise resets to
|
|
its initial state (both memory areas filled with zeroes).
|
|
|
|
For the data entry and display, a runtime command is required instead
|
|
(see below for details).
|
|
|
|
### Runtime command set
|
|
|
|
The following commands are required for all mu808 ports and implementations.
|
|
|
|
The following notation is in place for this list:
|
|
|
|
* `A1` - address passed as the first command argument;
|
|
* `A2` - address passed as the second command argument;
|
|
* `A3` - address passed as the third command argument;
|
|
* `V1` - numeric value stored at the address `A1`;
|
|
* `V2` - numeric value stored at the address `A2`;
|
|
* `V3` - numeric value stored at the address `A3`.
|
|
|
|
The mu808 runtime command list follows along with their mnemonics.
|
|
|
|
* 0 (NOP): No operation. The instruction is ignored regardless of parameters.
|
|
* 1 (JMP): Jump. This is the most complicated operation in the list, see the
|
|
next section ("Jump operation semantics") for the full explanation how it
|
|
works.
|
|
* 2 (IAT): Indirect addressing toggle. This instruction overrides the next
|
|
instruction by providing `V1`, `V2` and `V3` as the parameters for the next
|
|
instruction's command. The actual command parameters provided with the next
|
|
instruction will be ignored.
|
|
* 3 (OUT): Output. Print the range of values stored from the address `A1` to
|
|
the address `A2` (inclusively) to the port number in `A3` (0 is the platform's
|
|
standard output).
|
|
* 4 (INP): Input. Prompt the user to enter the range of values to be stored from
|
|
the address `A1` to the address `A2` (inclusively) from the port number in
|
|
`A3` (0 is the platform's standard input).
|
|
* 5 (SET): Set value at address. Interpret `A1` and `A2` as two direct integer
|
|
values and then set the value at `A3` to
|
|
`(A1 mod 10000) + (A2 mod 10000) / 10000`.
|
|
* 6 (CPY): Direct/indirect value copy. If `A1` is 0, then set the value at `A3`
|
|
to `A2`. If `A1` is 1, set the value at `A3` to `V2`. Otherwise, set the value
|
|
at `V3` to the contents of the address `V2` (converted to integer).
|
|
* 7 (FMA): Fused multiply-add. Set the value at `A3` to `V1 + (V2 * V3)`.
|
|
* 8 (SUB): Subtract. Set the value at `A3` to `V1 - V2`.
|
|
* 9 (DIV): Divide. Set the value at `A3` to 0 if `V2` is 0, otherwise set it to
|
|
`V1 / V2`.
|
|
* 10 (MDF): Modulo/floor. Interpret `V1` and `V2` as integers and set the value
|
|
at `A3` to `V1 mod V2` if `V2` is not 0, otherwise set the result to the
|
|
integer part of `V1`.
|
|
* 11 (ABS): Absolute value. Set the value at `A3` to `|V2|`.
|
|
* 12 (SQR): Square root. Set the value at `A3` to the square root of `|V2|`.
|
|
* 13 (NEL): Natural exponent/logarithm. If the value of `A1` is 0, set the value
|
|
at `A3` to `e ** V2`, otherwise set it to `ln |V2|`.
|
|
* 14 (TRI): Trigonometric function. If the value of `A1` is 0, set the value at
|
|
`A3` to `sin V2`, if the value of `A1` is 1, set the value at `A3` to
|
|
`cos V2`, otherwise set it to `arctg V2`. The parameters are given in radians.
|
|
* 15 (RND): Random number. Set the value at `A3` to a random integer number
|
|
between `V1` and `V2` (inclusively).
|
|
|
|
### Jump operation semantics
|
|
|
|
The jump operation (instruction command 1) in mu808 is almost identical to the
|
|
corresponding operation in 808UL, which, in turn, was directly modeled after
|
|
the corresponding operation in the 1V0 TZ IV variant. It allows for all kinds
|
|
of conditional and unconditional, direct and indirect jumps based on three
|
|
input parameters.
|
|
|
|
Using the same notation as above, the jump operation semantics is as follows:
|
|
|
|
* If `V2` == 0 and `A1` == 0, or
|
|
* if `V2` > 0 and `A1` == 1, or
|
|
* if `V2` < 0 and `A1` == 2, or
|
|
* if `V2` >= 0 and `A1` == 3, or
|
|
* if `V2` <= 0 and `A1` == 4, or
|
|
* if `V2` != 0 and `A1` == 5, or
|
|
* if `A1` == 6,
|
|
|
|
then jump to the address `A3`.
|
|
|
|
* If `V2` == 0 and `A1` == 7, or
|
|
* if `V2` > 0 and `A1` == 8, or
|
|
* if `V2` < 0 and `A1` == 9, or
|
|
* if `V2` >= 0 and `A1` == 10, or
|
|
* if `V2` <= 0 and `A1` == 11, or
|
|
* if `V2` != 0 and `A1` == 12, or
|
|
* if `A1` == 13,
|
|
|
|
then jump to the address `V3` (converting it to an integer first).
|
|
|
|
**Note** that the order of operands has changed compared to the 1V0/808UL
|
|
specifications: here, it's `[selector] [value] [jumpaddr]` as opposed to
|
|
`[value] [jumpaddr] [selector]`. This has been done for more convenient
|
|
jump programming by selecting the condition as the first operand.
|
|
|
|
### Port input and output
|
|
|
|
For the commands 3 and 4, mu808 supports optional non-zero port numbers, where
|
|
port 1 is the character output port and port 2 is the character input port
|
|
respectively. Additional custom ports may be implementation-defined.
|
|
|
|
File and data formats related to mu808
|
|
--------------------------------------
|
|
This specification also defines several file formats that can be used for mu808
|
|
programming process.
|
|
|
|
### Plain text machine code format (MU8)
|
|
|
|
This is the main format to store and enter mu808 programs in. It essentially
|
|
duplicates what's being entered into the mu808 REPL, with the exception of the
|
|
negative instruction numbers. Unlike 808UL, it only allows specifying digits and
|
|
whitespace in the file. Implementations are only required to support the space
|
|
character (32, 0x20) as the delimiter, but should also support newline, tabs and
|
|
other kinds of whitespace in the same way.
|
|
|
|
The recommended file suffix for mu808 plain text machine code is .mu8.
|
|
|
|
### Binary machine code format (MU8B)
|
|
|
|
Unlike 808UL, mu808 also defines a MU8B binary format to store every instruction
|
|
in 64 bits of data in the following order (for easier processing):
|
|
|
|
* 4 higher bits are unused,
|
|
* 4 bits are used for the command opcode (0 to 15),
|
|
* 14 bits are used for the instruction number (can't be 0),
|
|
* 14 bits are used for the command argument 1,
|
|
* 14 bits are used for the command argument 2,
|
|
* 14 bits are used for the command argument 3.
|
|
|
|
Hence, every instruction can only contain the number from 0 to 16383 in any of
|
|
its command arguments, and from 1 to 16383 in its instruction number field.
|
|
|
|
The recommended file suffix for mu808 binaries is .mu8b or .bin. Implementors
|
|
are encouraged (but not required) to provide tools for conversion between the
|
|
binary and text representations of mu808 machine code.
|
|
|
|
When exporting the entire program memory to the MU8B format, NOPs may or may
|
|
not be omitted. If a mu808 implementation supports MU8B file import, it must
|
|
behave the same way as if loading MU8 (plain text) machine code with the same
|
|
instructions or entering them manually.
|
|
|
|
### Assembly source code file format (MU8A)
|
|
|
|
In addition to direct machine code in the plain text or MU8B formats, the mu808
|
|
VM also allows using an assembly-like language to write programs using labels
|
|
and the above mnemonics (case-insensitive). The recommended file suffix is
|
|
.mu8a.
|
|
|
|
An assembly line looks like this (the optional parts are enclosed in square
|
|
brackets): `[:lbl] MNEMONIC arg1 arg2 arg3 [;comment]`. Labels are optional but
|
|
must start with a colon (`:`) and be on the same line before the instruction
|
|
they label. The mnemonics are specified above in the core opcode list.
|
|
|
|
Here, the exact assembly algorithm is specified step-by-step for each line in
|
|
the MU8A assembly file to convert it into a plain text based machine code
|
|
representation:
|
|
|
|
1. Remove all comments (starting with `;` until the end of the line).
|
|
2. Record the current line number N (not counting completely empty lines),
|
|
starting with 1.
|
|
3. Split the line into space-delimited fields (1-based numbering as well).
|
|
4. Check if the first field starts with `:`. If so, mark the mapping between
|
|
the field text and the number N, then remove the field from the set
|
|
(so that the field 2 becomes field 1 and so on).
|
|
5. If the field 1 is not already a number, replace the field 1 mnemonic text
|
|
with the numeric opcode if it can be found. If it's not a number and the
|
|
mnemonic cannot be found, report an error and halt the process.
|
|
6. Prepend N and the space to the current line and write the result into the
|
|
target file.
|
|
7. After the steps 1 to 6 are complete for every line, replace every label
|
|
occurrence in the mapping with the corresponding number in the target file.
|
|
|
|
Examples
|
|
--------
|
|
The [examples](examples/) subdirectory contains several MU8A source code file
|
|
examples for mu808 (some of which are ports of the same 808UL example programs),
|
|
namely:
|
|
|
|
* [Compound interest calculator](examples/compound.mu8a),
|
|
* [Linear regression calculator](examples/linreg.mu8a),
|
|
* [Hellorld!](examples/hellorld.mu8a) (a tribute to @UsagiElectric)
|
|
(requires I/O port 1 support),
|
|
* [FizzBuzz classic program](examples/fizzbuzz.mu8a)
|
|
(requires I/O port 1 support),
|
|
* [Bulls and Cows game](examples/moo.mu8a),
|
|
* [Lunar Lander game](examples/lunar.mu8a).
|
|
|
|
You can assemble them using any of the reference assemblers provided within the
|
|
repository, or even by hand (by numbering lines, resolving the labels and
|
|
replacing mnemonics with corresponding opcodes). The "Hellorld!" example only
|
|
works on the platforms supporting I/O port 1 (character output).
|
|
|
|
If you just want to test an implementation, assembled MU8 machine code files
|
|
(in the plaintext format) are stored in the `examples/assembled` subdirectory.
|
|
After loading into the REPL, you can run each of them with the `0 1 0 0 1` or
|
|
`0 1 6 0 1` sequence.
|
|
|
|
Reference implementations
|
|
-------------------------
|
|
|
|
### mu808 VM reference implementations
|
|
|
|
* [Python 3 implementation](mu808.py): the first one where all prototyping and
|
|
initial spec development has been done. Also compatible with MicroPython an
|
|
tested on it. Supports the entire documented mu808 instruction set, including
|
|
the two extended I/O ports (requires termios library to work with the
|
|
character input port correctly on desktop OSes but falls back to a simple
|
|
stream file read in case the library is not found). Accepts a file name to
|
|
preload a MU8 (plain text) machine code program from the OS command line.
|
|
* [ISO C99 implementation](mu808.c): supports the entire specification the
|
|
same way as the Python implementation, and also makes use of the optimized
|
|
`fma()` call that appeared in the C99 standard. Accepts a file name to preload
|
|
a MU8 machine code program from the OS command line. Compile the source with:
|
|
`cc -std=c99 -O2 -s -lm -o mu808 mu808.c`
|
|
* [POSIX AWK implementation](mu808.awk): supports the entire mu808 specification
|
|
sans the (non-portable) I/O port 2. Run it like this:
|
|
`LC_ALL=C awk -f mu808.awk [- input_program.mu8]`, where the dash-delimited
|
|
program file name is optional and used for preloading MU8 program files into
|
|
VM's memory. The Busybox AWK implementation is compact but quite slow, so use
|
|
it as a last resort solution when no other programming environment is available.
|
|
* [Web (HTML5/JS) implementation](https://mu808.luxferre.top) aka mu8web:
|
|
supports the entire mu808 spec except the I/O port 2 and offers a way to
|
|
load program files in the same MU8 format via a convenient UI.
|
|
Uses a convoluted flow to achieve true synchronous input.
|
|
Since there's no exiting to the OS environment, the -5 instruction number
|
|
resets the VM instead (by zeroing out both memory areas and the counter).
|
|
Entry conventions within UI are pretty much the same as for 808UL. Tested
|
|
on various devices with physical keypads and touch screens. The source code
|
|
can be found in the [mu8web](mu8web/) directory of this repo.
|
|
|
|
### Assembler reference implementations
|
|
|
|
* [Python 3](mu808asm.py): supports assembling MU8A source files into both
|
|
plain text (MU8) and binary (MU8B) machine code formats. Also supports
|
|
disassembling MU8 and MU8B files into their MU8A sources and converting
|
|
machine code between MU8 and MU8B formats. Additionally, this assembler
|
|
implementation provides the `t74` mode to convert plaintext MU8 machine code
|
|
into the format suitable for entering into TI-74 BASIC for running on the
|
|
TI-74 port of mu808 (see below).
|
|
|
|
These lists are going to be expanded as soon as new reference implementations
|
|
appear.
|
|
|
|
### Web reference implementation keyboard controls
|
|
|
|
The virtual keyboard (unavailable on the smallest screens) is duplicated with
|
|
the following keys at all times in the mu808 Web reference implementation:
|
|
|
|
* `0`-`9`: decimal digits input;
|
|
* `*` (star), comma, dot: decimal point input;
|
|
* Space, `+`, `/`, down arrow, Call key (if present): whitespace input;
|
|
* Backspace, Delete, left arrow, `-`: minus or backspace input (as the minus sign
|
|
can only be entered at the beginning of the instruction or data input line);
|
|
* `#` (pound, hash sign), Enter: newline input;
|
|
* `l`, `L`, up arrow, Left Soft key (if present): open a program file loading dialogue.
|
|
|
|
### 12-key (DTMF) input method proposal
|
|
|
|
For hobbyist projects and other environments where all input is limited to 12
|
|
keys of the standard phone keypad, the following convention is recommended:
|
|
|
|
* the `*` (star) key is inputting a minus if this is the first position in
|
|
the entry, otherwise it's inputting a decimal point;
|
|
* the `#` (pound) key actually confirms entering a single number field, and
|
|
all instructions are required to contain exactly five numbers.
|
|
|
|
Retro/minimal platform implementations
|
|
--------------------------------------
|
|
Here's the list of known/existing mu808 implementations that adhere to the
|
|
specification to most extent and run compatible machine code but cannot
|
|
provide full operating environment functionality due to the limits of the
|
|
platform itself.
|
|
|
|
### TI-74 BASIC
|
|
|
|
The [mu808.b74](mu808.b74) file contains a BASIC port of mu808 for the Texas
|
|
Instruments TI-74 portable computer (tested on the TI-74S variant). Due to
|
|
the resource constraints, the following limitations apply:
|
|
|
|
* only 149 program steps and 256 data cells supported (adjustable at line 110),
|
|
* no interactive mode (the `RUN` command directly executes the predefined
|
|
program in the VM),
|
|
* the program itself is entered into the DATA statements in the so-called
|
|
MU74 format (see below),
|
|
* no support for I/O port 2, tracing or intruction dumping,
|
|
* no boundary checks for the addresses inside the program.
|
|
|
|
Also, since the VM runs on top of a BASIC interpreter, program execution is
|
|
extremely slow most of the time. Keep in mind, this is more of a proof of
|
|
concept than a viable solution, and using the "native" TI BASIC is preferred
|
|
for any serious computing on that machine.
|
|
|
|
In order to store your programs on the device to run via this mu808 VM version,
|
|
you need to enter them in DATA statements in two-number pairs starting from
|
|
`DATA 0,0` at the BASIC line 5000. The last `DATA` line of the program must be
|
|
`DATA -1,0`. In between these two, every program instruction `lno cmd a1 a2 a3`
|
|
must be compressed into the form:
|
|
```
|
|
[5000+lno] DATA [cmd*10000 + a1], [a2 * 10000 + a3]
|
|
```
|
|
This storage format is called MU74. For example, the compound interest
|
|
calculator from the official mu808 examples will transform into:
|
|
```
|
|
5000 DATA 0,0
|
|
5001 DATA 50100,1
|
|
5002 DATA 40002,30000
|
|
5003 DATA 90002,10004
|
|
5004 DATA 50001,5
|
|
5005 DATA 70005,40005
|
|
5006 DATA 130001,50005
|
|
5007 DATA 70000,30005
|
|
5008 DATA 130000,50005
|
|
5009 DATA 30005,50000
|
|
5010 DATA -1,0
|
|
```
|
|
To make the conversion easier, the official mu808 assembler, `mu808asm.py`, now
|
|
supports the `t74` mode that accepts a plain MU8 machine code file and outputs
|
|
the same program in the MU74 format.
|
|
|
|
FAQ
|
|
---
|
|
|
|
### Why another 1V0 derivative that's even code-incompatible with 808UL?
|
|
|
|
808UL has served its purpose of fulfilling the author's vision on what an
|
|
1V0-like (virtual) system should look like. However, its ISA still somewhat
|
|
lacked rigidity and refinedness in terms of an optimal functionality
|
|
distribution: there were a lot of instructions that didn't use all three
|
|
command arguments, there were some instructions that could be parameterized
|
|
better to avoid functionality duplication, and dedicated port I/O subcommands
|
|
seemed like a hack on top of already available I/O routines. The mu808 ISA does
|
|
a pretty good job at polishing those bits while retaining all the main features
|
|
of the 808UL specification, also adding much stricter constraints on how the
|
|
operating environment is to be organized.
|
|
|
|
### What can be considered a minimum compatible mu808 VM implementation?
|
|
|
|
A minimum implementation must implement all runtime instruction commands (from 0
|
|
to 15 inclusive) and the interactive loop that accepts the following instruction
|
|
numbers: any positive number, 0, -1, -2 and -5.
|
|
|
|
A minimum implementation is required to support floating point numbers as a data
|
|
type, or, in case it's technically impossible, fixed point numbers with at least
|
|
two digits after the decimal point (four or more recommended).
|
|
|
|
### 808UL supported sparse instructions, is this feature gone in mu808?
|
|
|
|
No, not at all. It's the MU8A assembly language that doesn't (yet) support this
|
|
feature and autonumbers instructions based on their position in the source code.
|
|
When writing machine code directly, you can specify any instruction numbers you
|
|
like in the program.
|
|
|
|
### Why combine multiplication and addition into a single instruction command?
|
|
|
|
Because it is faster on modern architectures if both of them are required, and
|
|
doesn't add any complexity if only one of them is. Additionally, the FMA
|
|
operation is included in IEEE 754-2008, and all reference mu808 implementations
|
|
are using it in case the target platform allows them to.
|
|
|
|
### How to do simple addition and simple multiplication, given only the FMA?
|
|
|
|
It might seem inconvenient at first, but the FMA operation is quite capable.
|
|
First off, you already have a constant 0 always stored in 0, but it's advisable
|
|
to store a constant 1 into some unused location, like 99. You can do this e.g.
|
|
with the `set 1 0 99` operation. Then, let's go with three cases (assuming we
|
|
have stored 1 into the data memory location 99):
|
|
|
|
1. Incrementing a memory location is as easy as `fma 99 99 [loc]`.
|
|
2. Adding two numbers at loc1 and loc2 into loc2: `fma [loc1] 99 [loc2]`.
|
|
3. Multiplying two numbers at loc1 and loc2 into loc2: `fma 0 [loc1] [loc2]`.
|
|
|
|
The only inconvenience with the FMA operation is that you need to sacrifice one
|
|
of the operand locations to store the result. If you want to fully emulate the
|
|
808UL's behavior, then you have to copy one of the operands into the target
|
|
memory location first and then use it as the last parameter to the FMA command.
|
|
|
|
### Why is the "Hellorld!" example much shorter than in 808UL?
|
|
|
|
Because mu808 unifies the standard and port I/O, thus allowing to specify a
|
|
range of memory locations to the port output routine as well. Since the example
|
|
arranges all the message bytes in order, a single call to the output command
|
|
will suffice.
|
|
|
|
Credits
|
|
-------
|
|
Created by Luxferre in 2025, released into public domain with no warranties.
|