Microfirmware for the SidecarTridge Multi-device by Neil Rackett
MD/JS turns your SidecarT into a persistent JavaScript Worker for the Atari ST, enabling you to:
- Upload JavaScript source from your ST
- Call named functions with JSON arguments
- Use ES5.1, ES6/2015 and most ES2020 features
- Load text or JSON files using
fetch() - Read JSON results
Within these limits:
- 32KB JavaScript heap (48KB if compiled without fetch)
- 2KB result buffer
- fetch() using GET over HTTP only
To see MD/JS in action, simply install the microfirmware, open the cartridge icon on your ST's desktop (lower case c drive) and run MDJSDEMO.PRG.
If you'd like to integrating MD/JS into your own ST apps, instructions are below.
MD/JS Code is an example GEM application that you can download from the releases page to edit and run JavaScript source code on your Atari ST.
A self-playing Pong demo that pits a native 68000 AI (your ST) against a JavaScript AI running on your SidecarT (the JS). The JS "brain" is uploaded at start-up and drives its paddle through the non-blocking async API — mdjs_call_async → mdjs_status → mdjs_result — so the ST keeps rendering a smooth 50 fps while Core 1 predicts where the ball is going. A compact worked example of driving MD/JS from a real-time loop; source in examples/stjspong/, or build it with make examples.
- SidecarTridge Multi-device (RP2040-based ROM cartridge emulator)
- Atari ST, STE, MegaST, or MegaSTE
- Raspberry Pi Debug Probe or Picoprobe for flashing/debugging (optional but recommended for development)
🎉 MD/JS is now available in the official SidecarTridge Multi-device catalogue! 🎉
- Install MD/JS using your SidecarT's web interface.
- Download
MDJSCODE.PRGdemo app and example JS files from the releases page and save them to a floppy or hard disk. - Enjoy!
Manual installation
- Download the latest files from the releases page.
- Copy the
.uf2and.jsonfiles to the/appsfolder of your SidecarT's microSD card. - On the Booster screen, press ESC for the app list and select the MD/JS app.
- To return to Booster, turn on your ST while holding the SELECT button on your SidecarT.
You'll know if the microfirmware is working because you'll see "MD/JS: JavaScript Worker is ready" on your ST's screen as it boots.
The RP2040 runs a full JerryScript ES.next runtime (48 KB heap) on Core 1, with Core 0 continuing to service the cartridge bus to avoid blocking.
Atari ST (68000) RP2040
──────────────── ──────
mdjs_ping() ──CMD 0x10──► Core 0: tprotocol decode
mdjs_upload(src) ──CMD 0x11──► ↓ multicore FIFO
mdjs_call(f, a, r) ──CMD 0x12──► Core 1: JerryScript runtime
mdjs_reset() ──CMD 0x13──► ↓ jerry_parse / jerry_run / jerry_call
mdjs_call_async(f, a) ──CMD 0x14──► ↓ result → ROM-in-RAM @ $FAF100
mdjs_poll() ──CMD 0x15──► Core 0: writes random token (unblocks ST)
mdjs_result(r) ◄──────────── Status byte @ $FAF008 (no bus transaction)
The result buffer is mapped into the ST's ROM4 address space at $FAF100 and is directly readable with move.b instructions — the RP2040 pre-swaps bytes so character reads come out correctly. Use the C API (mdjs_result()) if you'd rather not handle that yourself. The async status byte lives at $FAF008 — a zero-overhead read.
Include mdjs.h and link against mdjs.c and sidecart_stubs.S in your ST project.
#include "mdjs.h"
/* Check the worker is present */
if (mdjs_ping() != 0) {
/* No SidecarTridge / worker not running */
}
/* Upload JavaScript source (evaluated immediately) */
mdjs_upload("function greet(name) { return 'Hello, ' + name + '!'; }");
/* Call a function — args as a JSON array, result as a JSON value */
char result[256];
mdjs_call("greet", "[\"World\"]", result, sizeof(result));
/* result == "\"Hello, World!\"" */
/* Clear the JS context and start fresh */
mdjs_reset();All functions return 0 on success, non-zero on timeout or error. Results are NUL-terminated strings in the caller-supplied buffer. mdjs_upload handles chunking automatically — just pass the full source string.
mdjs_ping() returns quickly whether or not a worker is present — it checks a readiness flag before issuing the protocol command, so detection never blocks waiting for a timeout.
mdjs_call blocks the 68000 until the RP2040 finishes executing the JavaScript. For long-running functions you can use the non-blocking variant instead:
/* Submit the call and return immediately */
int err = mdjs_call_async("heavyCalc", "[1000]");
if (err != 0) { /* busy or protocol error */ }
/* Do other work while JS runs on Core 1 */
while (mdjs_status() == MDJS_STATUS_BUSY) {
do_other_work();
}
if (mdjs_status() == MDJS_STATUS_DONE) {
char result[256];
mdjs_result(result, sizeof(result));
/* result now contains the JSON return value */
}mdjs_status() is a zero-overhead single byte read from MDJS_STATUS_ADDR ($FAF008) — no bus transaction. Only one async call can be in flight at a time; submitting a second returns MDJS_STATUS_BUSY immediately.
Every command-emitting call (upload, call, call_async, reset) first busy-waits a short settle to avoid a command race right after the worker boots. In a warm loop that already spaces its calls — e.g. a game firing a prediction every few frames — that wait is pure overhead (tens of milliseconds, enough to stall a per-frame loop). Once the worker is known-good, switch it off:
mdjs_upload(brain); /* first commands settle normally — cold-boot safe */
mdjs_set_settle(0); /* warm now: no per-call stall */
for (;;) {
mdjs_call_async("tick", state); /* fires immediately, no settle */
/* ... render a frame, poll mdjs_status() ... */
}mdjs_set_settle(iterations) takes a raw busy-loop count (0 = off); MDJS_SETTLE_DEFAULT is the default and mdjs_get_settle() reads the current value. A flaky SidecarTridge can raise it instead.
| Parameter | Limit / address |
|---|---|
| JS source per upload | Up to ~16 KB (8 chunks × 2096 bytes) |
| Function name | 63 characters |
| Result JSON | 2048 bytes |
| Args JSON | 2031 bytes (max, with 63-char function name) |
| JerryScript heap | 48 KB |
| Result buffer (ST) | $FAF100 (ROM4 + 0xF100) |
| Async status byte (ST) | $FAF008 (ROM4 + 0xF008) |
To build or monitor the microfirmware, the following make targets are available:
# Production build
make
# Debug build
make debug
# Monitor debug build over UART
make uartIf you'd like more information about coding for the SidecarT, the docs are here.
Source code is licensed under the GNU General Public License v3.0. See LICENSE for the full text.

