A JavaScript runtime for RP2040 and RP2350 microcontrollers, in the same spirit as Node.js for servers.
Docs: https://mcu-js.github.io/ while the canonical mcujs.org domain finishes DNS cutover.
- USB Flash Drive: Mount your Pico as a USB drive and drop in your
index.js - Serial REPL: Interactive JavaScript console over USB serial
- Hardware APIs: GPIO, PWM, I2C, SPI, ADC, and NeoPixel
- CommonJS Modules: Use
require()for modular code with/lib/module resolution - Minimal Footprint: Built on JerryScript for embedded systems
Run scripts/boards.sh --table for the release board list. Current board IDs are:
picopico2pico2_wwaveshare_rp2040_zerowaveshare_rp2040_pizerowaveshare_rp2040_touch_lcd_1.28waveshare_rp2350_lcd_1.47_awaveshare_rp2350_touch_lcd_1.69adafruit_feather_rp2040
- Download the latest
.uf2file for your board from Releases - Hold BOOTSEL and connect your board via USB
- Drag the
.uf2file to theRPI-RP2drive on RP2040 boards or theRP2350drive on RP2350 boards - The board will reboot and appear as a USB drive named "MCUJS"
- Create an
index.jsfile on the drive:
// Blink the onboard LED
const LED = 25;
GPIO.init(LED, GPIO.OUTPUT);
setInterval(() => {
GPIO.toggle(LED);
}, 500);
console.log('Blinking!');- Reset the board - your code runs automatically!
Connect to the Pico's serial port (115200 baud) for an interactive JavaScript console:
mcujs v0.1.0 on pico
> console.log('Hello!')
Hello!
undefined
> GPIO.set(25, true)
undefined
> 2 + 2
4
- Command history: Use Up/Down arrow keys to browse previous commands
- Line editing: Left/Right arrows, Home/End, Backspace anywhere in line
- Tab completion: Press Tab to auto-complete (e.g.,
cons<Tab>→console)
| Command | Description |
|---|---|
.help |
Show available commands |
.info |
Show board info (chip, memory, filesystem) |
.ls |
List files on the device |
.cat FILE |
Display file contents |
.rm FILE |
Delete a file |
.run FILE |
Execute a JavaScript file |
.multiline [FILE] |
Multi-line input (end with .end) |
.uf2 |
Reboot into UF2 mode (prompted) |
.uf2! |
Reboot into UF2 mode immediately |
.usbreset |
Reset USB connection (reboot) |
The .info command includes the current build ID (version + git SHA).
Hold the BOOTSEL button during power-on to skip index.js auto-run. This allows recovery from scripts with infinite loops without reflashing.
index.js runs immediately on boot; the REPL banner prints the first time a CDC serial connection is opened.
console.log('message');
console.warn('warning');
console.error('error');const GPIO = require('gpio');
GPIO.init(pin, GPIO.OUTPUT); // or GPIO.INPUT, GPIO.INPUT_PULLUP, GPIO.INPUT_PULLDOWN
GPIO.set(pin, true); // Set high
GPIO.set(pin, false); // Set low
GPIO.get(pin); // Read pin state (boolean)
GPIO.toggle(pin); // Toggle outputconst id = setTimeout(callback, ms);
clearTimeout(id);
const id = setInterval(callback, ms);
clearInterval(id);const PWM = require('pwm');
PWM.init(pin, frequency); // Initialize PWM on pin
PWM.setDuty(pin, duty); // duty: 0-65535 or 0.0-1.0
PWM.stop(pin);const I2C = require('i2c');
I2C.init(bus, sda, scl, baudrate);
I2C.write(bus, address, data); // data: array of bytes
I2C.read(bus, address, length); // returns array of bytesconst SPI = require('spi');
SPI.init(bus, sck, mosi, miso, baudrate);
SPI.transfer(bus, data); // returns received dataconst fs = require('fs');
fs.readFileSync(path); // Read file as string
fs.writeFileSync(path, data); // Write string to file
fs.appendFileSync(path, data); // Append to file
fs.existsSync(path); // Check if file exists
fs.unlinkSync(path); // Delete file
fs.readdirSync(path); // List directory (returns array)
fs.statSync(path); // Get file info {size, isFile, isDirectory}
fs.renameSync(oldPath, newPath); // Rename/move file
fs.mkdirSync(path); // Create directoryFilesystem capacity is derived from the remaining flash after the firmware image and EEPROM reservation.
// Relative imports
const utils = require('./utils'); // ./utils.js
const helper = require('../lib/helper'); // ../lib/helper.js
// Absolute imports
const config = require('/config'); // /config.js
// Bare module imports (searches /lib/)
const math = require('math'); // /lib/math.js
// JSON imports
const config = require('./config.json'); // Parsed as JSON
const pkg = require('package'); // /lib/package.json (if no .js found)
// CommonJS exports
// In /lib/math.js:
exports.add = (a, b) => a + b;
exports.PI = 3.14159;
// Or use module.exports:
module.exports = { add, PI };
// Module info available inside modules:
console.log(__filename); // e.g., "/lib/math.js"
console.log(__dirname); // e.g., "/lib"board.name; // Board name (e.g., "pico")
board.chip; // Chip (e.g., "RP2040")
board.ledPin; // Onboard LED pin number (-1 if none)
board.led(true); // Control onboard LED
board.led(); // Read LED state
board.neopixelPin; // Onboard NeoPixel pin (if present)
board.neopixelLength; // Onboard NeoPixel count (if present)
board.neopixel([255, 80, 10]); // Convenience for onboard NeoPixel
board.neopixel({ r: 255, g: 80, b: 10 });
board.neopixel([[255, 0, 0], [0, 255, 0]]); // Truncated to onboard length
board.freeMemory(); // Free JS heap memory in bytes
board.uniqueId(); // Board unique ID (hex string)
board.millis(); // Milliseconds since boot
board.delay(ms); // Blocking delay
board.reset(); // Reset USB connection (reboot)
board.enterUf2(); // Reboot into UF2 bootloaderMissing color values default to 0. Extra pixels are ignored. Object inputs are RGB; array inputs follow the active neopixel.init() order. Array-of-objects stays RGB.
const adc = require('adc');
adc.readPin(26); // Raw ADC reading (0-4095)
adc.readChannel(0); // Raw ADC reading by channel
adc.readVoltagePin(26); // Voltage using 3.3V reference
adc.readVoltageChannel(0); // Voltage using 3.3V reference
adc.readTempC(); // Internal temperature sensor (°C)
adc.TEMP; // Internal temperature channel
adc.VSYS; // VSYS channelprocess.version; // mcujs version (e.g., "v0.1.0")
process.arch; // CPU architecture (e.g., "RP2040")
process.platform; // Always "mcujs"
process.versions; // {mcujs, jerryscript, "pico-sdk", tinyusb}const neopixel = require('neopixel');
neopixel.init({ pin: 16, length: 1, order: 'GRB' });
// order can be "GRB" (default) or "RGB"
neopixel.setPixel(0, 255, 80, 10);
neopixel.show();const { builtinModules } = require('mcujs:module');
// alias: require('node:module')
// builtinModules includes: fs, process, gpio, pwm, i2c, spi, adc, neopixelThe board appears as both a USB serial device and a USB flash drive (composite device). There are some sync considerations:
| Direction | Behavior |
|---|---|
| Host → Device | Files copied via USB are immediately visible to JavaScript after using REPL commands (.ls, .cat, .run) |
| Device → Host | Files written from JavaScript (e.g., fs.writeFileSync()) persist correctly but may not appear on the host until you remount or replug |
Why? Linux aggressively caches FAT filesystem directories. When the device writes files internally, the host doesn't know to refresh its cache.
Workaround: After writing files from JavaScript, either:
- Remount on Linux:
udisksctl unmount -b /dev/sdX1 && udisksctl mount -b /dev/sdX1 - Or simply unplug and replug the Pico
scripts/verify-release.sh --allow-dirty
./build.sh pico
bun run e2eBuild every release board and package deterministic artifacts:
scripts/release.shSee CONTRIBUTING.md, RELEASING.md, and the Docusaurus docs under docs/docs/ for the full contributor workflow.
Files written from JavaScript are always persisted to flash immediately - they will survive power cycles even if not yet visible on the host.
- Docker (recommended) or:
- ARM GCC toolchain (
gcc-arm-none-eabi) - CMake 3.13+
- Pico SDK 2.2.0
- ARM GCC toolchain (
# Build for Pico
./build.sh pico
# Build for Pico 2
./build.sh pico2
# Build all boards
./build.sh allOutput files are written to build/ as mcujs-<version>-<board>.uf2.
The Bun test suite builds firmware, flashes UF2 if needed, and exercises REPL, filesystem, and JS APIs.
bun run e2eRequirements:
- Pico connected via USB (CDC + MSC visible)
bun,udisksctl, andlsblkavailable
export PICO_SDK_PATH=/path/to/pico-sdk
mkdir build && cd build
cmake -DBOARD=pico ..
make -j$(nproc)| Board ID | Board | Chip | Flash | Notes |
|---|---|---|---|---|
pico |
Raspberry Pi Pico | RP2040 | 2MB | Onboard LED |
pico2 |
Raspberry Pi Pico 2 | RP2350 | 4MB | Onboard LED |
pico2_w |
Raspberry Pi Pico 2 W | RP2350 | 4MB | CYW43 LED support |
waveshare_rp2040_zero |
Waveshare RP2040-Zero | RP2040 | 2MB | Onboard NeoPixel |
waveshare_rp2040_pizero |
Waveshare RP2040-PiZero | RP2040 | 16MB | DVI/HDMI output |
waveshare_rp2040_touch_lcd_1.28 |
Waveshare RP2040 Touch LCD 1.28 | RP2040 | 4MB | Round LCD, touch, IMU |
waveshare_rp2350_lcd_1.47_a |
Waveshare RP2350-LCD-1.47-A | RP2350 | 16MB | LCD, NeoPixel |
waveshare_rp2350_touch_lcd_1.69 |
Waveshare RP2350-Touch-LCD-1.69 | RP2350 | 16MB | LCD, touch, IMU, buzzer |
adafruit_feather_rp2040 |
Adafruit Feather RP2040 | RP2040 | 8MB | NeoPixel, STEMMA QT |
mcujs/
├── host/ # Engine boundary, module loader, and bindings
│ ├── engine.* # JerryScript adapter
│ ├── module_loader.c # CommonJS require() implementation
│ └── bindings/ # Native API bindings (GPIO, I2C, fs, etc.)
├── javascript/ # JerryScript build adapter
├── board/ # Board-specific configurations by board ID
│ └── <board-id>/ # board_config.h and board_config.cmake
├── src/ # Core firmware
│ ├── usb/ # USB CDC + MSC composite device
│ └── filesystem/ # FAT12 filesystem with subdirectory support
├── examples/ # Example JavaScript programs
└── scripts/ # Board registry, verification, and release tooling
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
MIT License - see LICENSE for details.
- JerryScript - Lightweight JavaScript engine
- Raspberry Pi Pico SDK
- TinyUSB - USB stack
- FatFS - FAT filesystem