Tiny JVM
A compact virtual machine that runs the same program two ways: as WebAssembly in this page, and as firmware on a microcontroller. The program is written in a small language of its own and turned into bytecode by a toolchain written in Java.
The idea
"Java" and "embedded" usually sit in two different projects. This one fuses them into a single artifact. A tiny stack-based interpreter (a few hundred lines of portable C) executes a fixed bytecode. Because the interpreter is just C with no OS calls, the exact same source compiles to two targets: a WebAssembly module that runs in the browser, and a firmware image that runs on an ESP32 in the Wokwi simulator.
The bytecode is produced by a small compiler written in Java: a lexer, a recursive-descent parser, and a code generator. Writing that toolchain is the Java half of the project; keeping the VM small enough to fit a microcontroller is the embedded half.
How it works
Five stages, from source text to a running program:
The same bytecode file is byte-for-byte identical on both targets. The over-temp-alarm sample below reads a sensor and drives an output pin; run it in the browser and the pin writes go to a fake console, flash it to the Wokwi board and they light a real LED in the simulation.
Instruction set
One-byte opcodes, a value stack of 32-bit integers, up to 256 local-variable slots. This table is the contract the C VM and the Java code generator both target.
| Opcode | Name | Operand | Effect |
|---|---|---|---|
| 0x01 | PUSH | int32 (LE) | push a constant onto the stack |
| 0x02 | POP | -- | discard the top of stack |
| 0x03 | DUP | -- | duplicate the top of stack |
| 0x10 | ADD | -- | a, b -> a + b |
| 0x11 | SUB | -- | a, b -> a - b |
| 0x12 | MUL | -- | a, b -> a * b |
| 0x13 | DIV | -- | a, b -> a / b (trap on 0) |
| 0x20 | LOAD | uint8 slot | push local variable #slot |
| 0x21 | STORE | uint8 slot | pop into local variable #slot |
| 0x30 | JMP | int16 offset | unconditional branch |
| 0x31 | JZ | int16 offset | branch if top of stack == 0 |
| 0x32 | JNZ | int16 offset | branch if top of stack != 0 |
| 0x40 | CALL | uint16 addr | call function at bytecode addr |
| 0x41 | RET | -- | return from function |
| 0x50 | -- | pop and emit to the output console | |
| 0x51 | PINMODE | -- | mode, pin -> configure a GPIO pin |
| 0x52 | DWRITE | -- | value, pin -> drive a GPIO pin |
| 0x53 | DREAD | -- | pin -> push the pin's current level |
| 0xFF | HALT | -- | stop the VM |
Sample programs
Written in the project's own language. The demo will let you pick one, compile it in the browser, and step the VM one instruction at a time with the stack visible.
Fibonacci
Shows: loops, locals, arithmetic
let a = 0;
let b = 1;
let i = 0;
while (i < 15) {
let t = a + b;
a = b;
b = t;
i = i + 1;
}
print a;
Factorial (recursive)
Shows: function calls, the call stack, RET
fn fact(n) {
if (n == 0) { return 1; }
return n * fact(n - 1);
}
print fact(6);
Over-temp alarm
Shows: the same bytecode driving a GPIO on the Wokwi board
# reads a simulated sensor on pin 34, lights pin 2 when it's hot
pinMode(2, OUTPUT);
while (1) {
let c = digitalRead(34);
digitalWrite(2, c > 28);
}
What's built so far
This pass is scaffolding: the project's page, its listing, and the written specification. Still to come, in build order:
- The C VM: interpreter loop, stack, locals, calls, GPIO hooks.
- The Java toolchain: lexer, parser, code generator, a small CLI.
- The WebAssembly build and the interactive stepper on this page.
- The Wokwi project and an embedded view of it running the same bytecode.