npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

stm32f1-emu

v2.0.1

Published

Full-system WASM emulator for the STM32F1 family (STM32F103 Blue Pill, F105, etc.) - run Arduino/STM32 firmware in Node.js or the browser

Readme

STM32 Bluepill Emulator

npm version npm downloads Live Demo License: MIT

A full-system emulator for the STM32F1 family (STM32F103C8 "Blue Pill", STM32F105, etc.) that runs real, unmodified Arduino / STM32Cube firmware in Node.js or the browser.

~23M instructions/sec headless (50M in ~2.1s, emulator.js 200M in 8.2s) and ~21M in headless Chromium (200M in 9.3s, SAB OFF) — the browser matches Node since the CAN-autopilot timing fix. The interactive page loop stays frame-budgeted (~8-9M headed, SAB ON +6.6%).


Install

npm install stm32f1-emu

Requires Node.js 18+ (ESM). In the browser, load via a <script> tag or bundler.


Quick Start

Node.js — High-Level API (recommended)

import { STM32F1 } from 'stm32f1-emu';
import { readFileSync } from 'fs';

// Load an ELF, BIN, or Intel HEX
const mcu = await STM32F1.fromELF(readFileSync('firmware.elf'));

// Or from raw binary / Intel HEX text
// const mcu = await STM32F1.fromBin(readFileSync('firmware.bin'));
// const mcu = await STM32F1.fromHex(readFileSync('firmware.hex'), 'utf8');

// Subscribe to USART TX (MCU → host)
mcu.usart1.onData = (byte) => process.stdout.write(String.fromCharCode(byte));

// Subscribe to GPIO changes
mcu.gpio.pin('C', 13).on('change', (high) => {
  console.log('PC13 LED:', high ? 'ON' : 'OFF');
});

// Run 1 million instructions (auto-drains events after each batch)
const result = await mcu.execute(1_000_000);
console.log(result.instCount, 'instructions executed');

// Inject bytes into UART RX (host → MCU)
mcu.uartRx(0x41); // send 'A'

// Read UART output
console.log(mcu.uartOutput);

// Clean up
mcu.close();

Node.js — Low-Level API

The low-level emulator is also available via the ./emulator sub-export:

import { createEmulator } from 'stm32f1-emu/emulator';
import { readFileSync } from 'fs';

const emu = await createEmulator({
  firmware: readFileSync('firmware.elf'),  // Uint8Array, string (HEX), or ArrayBuffer
});

const result = emu.run(10_000_000);  // run up to 10M instructions
console.log(result.instCount, 'instructions executed');

console.log(emu.getUartOutput());          // USART1 TX output
console.log(emu.gpioReadOutput(2, 13));    // PC13 level (port 2 = C)
emu.uartRxBytes([0x31, 0x32]);             // inject RX bytes
console.log(emu.getRegisters());           // { R0..R12, SP, LR, PC, xPSR }

emu.close();

Browser

<script type="module">
  import { STM32F1 } from 'stm32f1-emu';

  const mcu = await STM32F1.fromELF(
    await (await fetch('firmware.elf')).arrayBuffer()
  );

  mcu.usart1.onData = (b) => {
    document.getElementById('terminal').textContent += String.fromCharCode(b);
  };
  mcu.gpio.pin('C', 13).on('change', (high) => {
    document.getElementById('led').style.background = high ? '#f00' : '#300';
  });

  // Run 20K instructions per frame (~60 fps)
  function loop() {
    mcu.step(20_000);
    requestAnimationFrame(loop);
  }
  loop();
</script>

STM32F1 Wrapper API

The high-level STM32F1 class wraps the low-level emulator with a Wokwi-style event-driven API. All bus transactions (SPI, I2C, USART, ADC, TIM, etc.) are decoded into callbacks you subscribe to.

Creating an Instance

const mcu = await STM32F1.create({ firmware: buf, chip: 'stm32f103c8' });
const mcu = await STM32F1.fromELF(buf, opts?);
const mcu = await STM32F1.fromBin(buf, opts?);
const mcu = await STM32F1.fromHex(hexText, opts?);

| Option | Default | Description | |---|---|---| | firmware | empty | Uint8Array, ArrayBuffer, or Intel HEX string | | chip | 'stm32f103c8' | Chip ID or { name, svd } for SVD-based layout | | svd | null | SVD XML string (overrides chip) | | flash_size | 0x10000 | Flash region size (bytes) | | ram_size | 0x5000 | SRAM size (bytes) | | ext_devices | {} | External devices (see below) |

ext_devices:

{
  spi_flash:     [{ peripheral: 'SPI1', jedec_id: 0xEF4016, data: new Uint8Array(65536) }],
  i2c_eeprom:    [{ peripheral: 'I2C1', address: 0x50, data: new Uint8Array(1024) }],
  i2c_oled:      [{ peripheral: 'I2C1', address: 0x3C, width: 128, height: 64 }],
  lcd:           [{ peripheral: 'SPI1', cs: 'PA8' }],
  touchscreen:   [{ peripheral: 'SPI1', cs: 'PA1', touch_detected_pin: 'PC5' }],
  software_spi:  [{ name: 'FLASH', cs: 'PB12', clk: 'PB13', miso: 'PB14', mosi: 'PB15' }],
  fsmc_bank:     [{ name: 'FSMC.BANK1', data: new Uint8Array(65536) }],
  sd_card:       [{ peripheral: 'SDIO', data: new Uint8Array(1048576) }],
}

Execution

| Method | Returns | Description | |---|---|---| | execute(cycles) | { instCount, stopped } | Run N instructions + auto-drain events | | step(cycles) | { pc, instCount, stopped } | Single batch + auto-drain events | | stop() | void | Request stop of a running execute() loop | | close() | void | Unsubscribe listeners + tear down | | reset() | Promise<STM32F1> | Recreate emulator from scratch (reloads firmware) |

GPIO

const pin = mcu.gpio.pin('A', 5);  // or mcu.gpio.pin(0, 5)

| Method | Returns | Description | |---|---|---| | pin.on('change', cb) | () => void | Subscribe to output-level changes; returns unsubscribe | | pin.read() | 0 \| 1 | Driven output level | | pin.readInput() | 0 \| 1 | Input level | | pin.setInput(high) | void | Drive external input (e.g. button press) | | pin.setAnalog(val) | void | Set analog value (0–4095) |

USART

| Property / Method | Description | |---|---| | mcu.usart1 / usart2 / usart3 | USART wrappers | | usart.onData = (byte) => {} | TX callback (MCU → host) | | usart.send(string \| number[]) | Inject bytes into MCU RX | | usart.output | Accumulated TX string (getter) |

SPI

| Property / Method | Description | |---|---| | mcu.spi1mcu.spi6 | SPI wrappers | | spi.onTransfer = (ch, tx, rx) => {} | DR write callback | | spi.injectMiso([0xFF, ...]) | Queue MISO bytes for next transfer |

I2C

| Property / Method | Description | |---|---| | mcu.i2c1 / i2c2 / i2c3 | I2C wrappers | | i2c.onStart = (addr) => {} | Start condition callback | | i2c.onWrite = (byte) => {} | Byte write callback | | i2c.onRead = () => {} | Read request callback | | i2c.onStop = () => {} | Stop condition callback | | i2c.injectRx([0x55, ...]) | Queue RX bytes for next read |

Virtual-Peripheral Events

These callbacks fire on specific hardware events:

| Callback | Signature | Description | |---|---|---| | onExtiEdge | (line) => void | EXTI external interrupt edge detected | | onAdcDone | (adc, chan) => void | ADC conversion complete | | onTimUpdate | (tim) => void | Timer overflow (update event) | | onTimCapture | (tim, ch, value) => void | TIM input capture | | onDacWrite | (chan, value) => void | DAC output written | | onCrcResult | (value) => void | CRC calculation result read | | onRtcAlarm | (alarm) => void | RTC alarm triggered | | onWdogReset | (which) => void | Watchdog reset requested (1=IWDG, 2=WWDG) | | onCanTx | (can, id, len, data[8]) => void | CAN message transmitted | | onCanRx | (can, id, len, data[8]) => void | CAN message received | | onFsmcAccess | (bank, offset, write, size, value) => void | FSMC bus transaction |

Display Framebuffers

const oledFb = mcu._emu.i2cOledFb('I2C1', 0x3C);  // Uint8Array (page-major)
const lcdFb  = mcu._emu.lcdFb('SPI1');              // Uint8Array (128×64, 1B/pixel)

Symbol Resolution

mcu.setSymbols(mapText);              // load GNU ld .map text
console.log(mcu.resolveSymbol(pc));   // e.g. "main+0x1e" or null

Low-Level API

The createEmulator() function returns a Promise<BluepillEmulator>.

import { createEmulator } from 'stm32f1-emu';

const emu = await createEmulator({
  firmware: readFileSync('firmware.elf'),
  flash_size: 0x10000,
  ram_size: 0x5000,
  vector_table: 0x08000000,
  chip: 'stm32f103c8',
  ext_devices: {},
  verbose: false,
});

Execution

| Method | Returns | Description | |---|---|---| | run(maxInstructions?) | { totalSteps, instCount, stopped } | Run up to N instructions (0 = forever) | | step(maxBatch?) | { pc, instCount, stopped } | Run one batch (default 20K instructions) | | stop() | void | Request stop | | close() | void | Release the emulator instance (no-op teardown) |

Registers & Memory

| Method | Returns | Description | |---|---|---| | getRegisters() | { R0..R12, SP, LR, PC, xPSR } | All ARM registers | | getPc() | number | Current program counter | | getSp() | number | Current stack pointer | | setPc(pc) | void | Set PC (auto-ORs with 1 for Thumb) | | read32(addr) | number | Read 32-bit word from any address | | write32(addr, val) | void | Write 32-bit word to any address |

UART

| Method | Description | |---|---| | getUartOutput() | Accumulated USART1 TX output (string) | | uartRx(byte) | Inject one byte into USART1 RX | | uartRxBytes([...]) | Inject multiple bytes | | uartRxAddr(addr, byte) | Inject into a specific USART by base address | | rxPending() | Number of unread bytes in UART RX buffer |

GPIO

| Method | Description | |---|---| | gpioReadOutput(port, pin) | Read driven output level (port: 0=A, 1=B, 2=C) | | gpioReadInput(port, pin) | Read input level | | gpioSetInput(port, pin, value) | Drive external input | | gpioSetAnalog(port, pin, level) | Set analog value (0–4095) |

ADC / PWM / CAN

| Method | Description | |---|---| | setSimAdc(value) | Set simulated ADC value | | pwmDuty(addr, channel?) | PWM duty (0–100) of a timer channel | | canInjectMessage(addr, tir, tdtr, tdlr, tdhr) | Inject CAN message |

Peripheral Bus

| Method | Description | |---|---| | periphRead(addr, width?) | Raw peripheral register read | | periphWrite(addr, width, value) | Raw peripheral register write |

Bus Watchers / Events

| Method | Returns | Description | |---|---|---| | onPeriphWrite(fn) | () => void | Subscribe to ALL peripheral writes; returns unsubscribe | | onPinChange(fn) | () => void | Subscribe to chip-driven GPIO changes; returns unsubscribe | | drainEvents() | number[] | Drain virtual-peripheral transaction events (flat i32 array) | | takePinEvents() | number[] | Drain buffered pin-change events |

Virtual Device Injection

| Method | Description | |---|---| | spiInjectMiso(channel, bytes) | Queue MISO bytes for a SPI channel | | i2cInjectRx(channel, bytes) | Queue RX bytes for an I2C channel | | addJsPeripheral(base, size, read, write) | Register a custom peripheral on the bus |

Symbol Resolution

| Method | Description | |---|---| | setSymbols(list) | Set symbol table [{name, addr}] | | resolveSymbol(addr) | Resolve address → symbol name (e.g. "main+0x1e") | | getSymbolCount() | Number of loaded symbols |


CLI Usage

# Run a raw binary
npx stm32f1-emu firmware.bin [max_instructions]

# Run with config (YAML)
npx bluepill-emu --config=config.yaml

# Options
--regs              # Dump CPU registers at exit
--uart=0x40013800   # UART base address for stdin RX injection
--map=firmware.map  # Load symbol map for PC resolution
--verbose           # Print SP/PC at boot
--max=200000000     # Max instructions (default: 100M)

Config YAML example:

flash: 0x08000000
ram: 0x20000000
regions:
  - start: 0x08000000
    file: firmware.hex
ext_devices:
  spi_flash:
    - peripheral: SPI1
      jedec_id: 0xEF4016
      data: flash.bin
  i2c_eeprom:
    - peripheral: I2C1
      address: 0x50
      data: eeprom.bin

Firmware Formats

| Format | Description | How to Load | |---|---|---| | .bin | Raw binary (vector table at 0x08000000) | CLI, library, demo site | | .hex | Intel HEX (Arduino/STM32duino output) | Auto-detected (starts with :) | | .elf | ELF32 executable (segments + symbols) | Auto-detected by magic bytes | | .map | GNU ld linker map (not executable) | Pair with --map flag for symbol names |


WebSocket Bridge (headless Node + browser viewer)

# Start the server (runs the emulation loop, streams events over WebSocket)
node node_modules/stm32f1-emu/pkg/ws-server.mjs firmware.elf --port=8080

# Open the viewer in a browser
# http://localhost:8080/ws-viewer.html

The viewer renders a UART terminal, GPIO grid (click to toggle inputs), event log, and FPS counter. The server streams all virtual-peripheral events as JSON.


Development

# Rebuild Rust peripherals → WASM
PATH=/tmp/binaryen-version_132/bin:$PATH \
RUSTFLAGS="--remap-path-prefix=$HOME=/build" \
wasm-pack build --target web --out-dir pkg

# Run tests
node tests/test_all.mjs              # 277 unit tests
node tests/canary.mjs                # 39/39 firmware checks (~25s)
node tests/test_emulator_js.mjs      # browser run-loop path (200M, 39/39)
node tests/test_browser.mjs          # Playwright browser tests

# Run firmware directly
node pkg/cli.mjs firmware.elf
echo -n "AB" | node pkg/cli.mjs --config=config.yaml --max=200000000

Supported Peripherals

GPIO (A–D) with electrical model, USART1–3, SPI1–2, I2C1–2, TIM1–14 (PWM, input capture, external triggers, slave modes, DMA requests), ADC1–2 (real conversion timing, RC sample-and-hold, DAC→ADC loopback, external triggers), DAC1–2, DMA1 (7ch) + DMA2 (5ch), CAN1 (RX injection + filters), RTC (alarm), CRC, NVIC (priority dispatch + 64-IRQ budget), SysTick, SCB (deep sleep, SHPR routing, fault escalation), EXTI, AFIO (pin remap), BKP, WWDG, IWDG, PWR, FLASH, FSMC (NOR/NAND/PC-Card), SDIO (SDHC card image, CMD engine, DMA2 CH4), USB FS device (endpoints, packet memory, enumeration events).


License

MIT — see LICENSE.

Acknowledgements