Virtual microcontroller
Run real compiled Arduino Uno and Nano .hex files on an emulated ATmega328P at 16 MHz, or MicroPython on a virtual Pico, with LEDs, buttons, pots, a buzzer, a servo, a 7-segment digit, NeoPixels decoded from their bit timing, an SSD1306 OLED decoded from its I²C commands, an HC-SR04, a DHT22 and a BME280 wired to the pins — plus a serial monitor, registers, a logic-analyser strip and VCD export.
Added
Works with
- Board pinouts IoT and embeddedthe Uno and Nano pin maps and functions behind the board drawing here
- Serial monitor and ESP flasher IoT and embeddedthe same serial monitor for a real board once the sketch works here
- ARM Thumb assembly playground IoT and embeddedan ARM Cortex-M instruction-level simulator, for comparing with the AVR here
- Pico hardware bench IoT and embeddeddrives a real Pico running MicroPython — this page runs a pretend one first
- LED strip and pixel planner Electronicsplans the power and layout for a strip that the NeoPixel examples here drive
- Logic analyser IoT and embeddedOpen in logic analyser hands it the recorded pin transitions, named by pin
- Electronics course Electronicsa course whose microcontroller unit runs these example sketches — GPIO, PWM, UART, servos, displays
- LED effect studio IoT and embeddeddesign an LED effect and export the NeoPixel sketch to compile and run on the emulated Uno here
How to use it
A microcontroller on the page. Pick an Arduino Uno or Nano and load a .hex file that the
Arduino IDE compiled: the ATmega328P it was built for is emulated instruction by instruction at
16 MHz, in real time, with its timers, serial port, ADC, SPI and I²C. Or pick MicroPython and
write Python for a virtual Raspberry Pi Pico, run by the official MicroPython WebAssembly port.
Either way, the program drives virtual parts wired to the board’s pins, and you can watch the pins,
the registers and the serial port as it runs. Twelve Arduino examples (with their source) and ten
MicroPython examples are included.
How to use it
-
Choose a board at the top: Arduino Uno, Arduino Nano, or MicroPython (Pico).
-
Load a program.
-
Arduino: choose an example and press Load example, or open your own
.hex. In the Arduino IDE, set the board to Arduino Uno (or Nano) and use Sketch ▸ Export Compiled Binary; it writessketch.ino.hexnext to the sketch. With arduino-cli:arduino-cli compile -b arduino:avr:uno -e. Drop the file on the Program panel, open it, or give a URL (the server has to allow cross-origin requests). The file with the bootloader works too; the CPU starts at address 0, which is your sketch. -
MicroPython: edit the program (or load an example) and press Run (Ctrl/⌘-Enter). Stop raises KeyboardInterrupt in it. When it isn’t running, the input line under the console is a REPL with the same
machinemodule.
-
-
Wire parts. Each example adds the parts it expects. Add a part adds more: an LED, an RGB LED, a pushbutton or latching switch, a potentiometer, a light sensor (LDR), a piezo buzzer, a servo, a 7-segment digit, WS2812 NeoPixels (strip, ring or matrix), an SSD1306 OLED, an HC-SR04 ultrasonic sensor, a DHT22 and a BME280. Choose each part’s pins from its lists; the board drawing outlines the pins in use, and clicking a pin shows its port bit, timer, ADC channel and state. On the Arduino the I²C parts sit on A4/A5, where the chip’s TWI is.
-
Run it. Press the buttons, move the sliders and watch. Sound: on plays the buzzer through your speakers. The speed readout shows how close to real time the emulator is keeping up (100 % means 16 million cycles of AVR code per second of your time); Pause, Step (one instruction) and +1 ms help when debugging, and the speed list slows it down to 1/10 or 1/100.
-
Look inside.
-
Serial monitor: what the sketch prints with
Serial, at the baud rate it set up (read from UBRR0). Type a line to send it, with the line ending you choose. For MicroPython this is the console and REPL. -
Logic analyser: a live trace of up to eight pins, with every transition timed to the CPU cycle. Download VCD saves it for GTKWave or PulseView; Open in logic analyser opens it in the logic analyser with the channels named after the pins.
-
Registers & buses: r0–r31, PC, SP and SREG, the PORT/DDR/PIN bits of ports B, C and D, the three timers' clock, count and compare registers, the USART baud rate, the ADC, TWI and SPI state, the last I²C transactions byte by byte, and the SSD1306 commands decoded by name.
-
The Share link button copies a link with the board, the example, the parts and their pins (and your MicroPython program).
The Arduino examples
Each one was written for this page and compiled with arduino-cli 1.5.1 and the arduino:avr 1.8.8
core; the source is under Sketch source once loaded, and build.sh in
the examples folder rebuilds them all.
Blink |
LED 13, once a second, with |
Fade |
|
Button |
A button from pin 2 to GND with |
AnalogReadSerial |
A pot on A0 and an LDR on A1 read by the 10-bit ADC and printed; the pot dims pin 9. |
Tone melody |
|
Servo sweep |
The Servo library’s 544–2400 µs pulses every 20 ms on pin 9. |
NeoPixel rainbow |
Adafruit_NeoPixel’s cycle-counted assembly driving a 16-LED ring on pin 6. |
OLED hello |
A minimal SSD1306 driver (no library) over Wire: text, a counter, a bar and a ball. |
Ultrasonic |
An HC-SR04 on pins 9/10, timed with |
RGB LED + 7-segment |
A digit counting on pins 2–8 and an RGB LED on Timer1/Timer2 PWM pins 9–11. |
Weather |
A BME280 with Bosch’s integer compensation, and a DHT22 read by timing its 40 bits. |
Serial commands |
Reads lines from the serial monitor: |
How the Arduino side works
The CPU is avr8js (MIT licence), the open-source AVR emulator
behind Wokwi, loaded from jsDelivr. It executes the AVR instruction set with the datasheet’s cycle
counts and models the ATmega328P’s GPIO ports, the three timers (including the PWM compare
outputs), the USART, the ADC, SPI, TWI, the EEPROM and the watchdog. Each animation frame the page
works out how many cycles should have passed at 16 MHz and runs that many, within a time budget,
so a fast computer keeps up in real time with room to spare; if it can’t keep up, the readout says
so and the program simply runs slower — its own idea of time (millis()) stays correct, because
that is counted in cycles.
Everything the parts see comes from pin transitions stamped with the CPU cycle:
-
LED brightness is the fraction of time the pin is high: from the last full PWM period when the pin is toggling (so 490 Hz PWM at OCR 80 shows 31 %), or the time-weighted level over the frame when it isn’t.
-
The buzzer plays a square wave at the pin’s toggling frequency, measured from rising edge to rising edge.
-
The servo reads the width of each high pulse and maps 544–2400 µs onto 0–180°, as the Servo library does (
MIN_PULSE_WIDTHandMAX_PULSE_WIDTHin Servo.h). -
NeoPixels are decoded from the bit timing: every high pulse longer than 0.5625 µs is a 1 and every shorter one a 0 (the WS2812B datasheet gives T0H = 0.4 µs and T1H = 0.8 µs, ±150 ns; the Adafruit library at 16 MHz makes 5 and 13 cycles, 0.31 and 0.81 µs, which the strip’s card shows), 24 bits per LED in green–red–blue order, and a low gap of more than 50 µs latches the frame.
-
Buttons drive their pin low (to GND) or high (to VCC with a 10 kΩ pull-down). A pin that nothing drives reads high if the sketch turned on the internal pull-up and low otherwise.
-
Pots and the LDR set the ADC input voltage (AVcc = 5 V reference). The LDR is a GL5528-style part (about 10 kΩ at 10 lux, γ = 0.7) in a divider with 10 kΩ to ground.
-
HC-SR04: a trigger pulse of at least 8 µs (the datasheet asks for 10) starts a ping; ECHO rises about 460 µs later, after the 40 kHz burst, and stays high for the round trip at the speed of sound, 331.3 + 0.606·T m/s — 58 µs per centimetre at 20 °C. Out of range it gives the module’s 38 ms time-out pulse.
-
DHT22: when the sketch has held DATA low for at least 0.8 ms and releases it, the sensor sends the AM2302 datasheet’s reply — 80 µs low, 80 µs high, then 40 bits of 50 µs low and 27 µs (0) or 70 µs (1) high: humidity×10, temperature×10 with a sign bit, and a checksum.
-
I²C parts answer the TWI directly (avr8js hands each start, address, byte and stop to the page), so the bus doesn’t appear on the pins. The SSD1306 model follows the datasheet’s control bytes (Co and D/C#), every command with its argument count, the 1 KB GDDRAM in eight pages, and horizontal, vertical and page addressing; segment remap, COM scan direction, start line, inverse, contrast and display on/off change what the glass shows. The BME280 has chip ID 0x60, a set of typical calibration constants, the control registers and burst reads; the raw readings are searched so that Bosch’s integer compensation formulas (datasheet BST-BME280-DS002, section 4.2.3) give the temperature, pressure and humidity on its sliders.
How the MicroPython side works
MicroPython here is the official WebAssembly port
(npm @micropython/micropython-webassembly-pyscript 1.27.0, MIT), loaded from jsDelivr. The
modules machine (Pin with pulls and irq, Signal, PWM, ADC, I2C/SoftI2C, Timer,
time_pulse_us), neopixel, dht and ssd1306 are shims written for this page that drive the
same virtual parts; the OLED gets real SSD1306 command and data bytes through I2C.writeto, so it
goes through the same decoder as the Arduino’s. The ADC is 12-bit scaled to 16, against 3.3 V, and
ADC(4) is the RP2040’s temperature sensor reading 27 °C.
A browser page has one thread, and a Python while True: loop with time.sleep() in it would
freeze it. The WebAssembly port can’t pause in the middle of an ordinary function, but it can
await, so before running your program the page rewrites it (the rewrite is shown under What
actually runs):
-
time.sleep,sleep_msandsleep_usbecomeawaitof an asyncio sleep, so the page keeps running while the program waits — in real time; -
every
whileandforloop gets anawait _mcu.tick(), which hands control back to the page about every 15 ms and is where Stop raises KeyboardInterrupt; -
functions that sleep or loop, and functions that call them, become
async def, and their calls are awaited (through a helper that passes ordinary values straight through).
Interrupt handlers (Pin.irq) and Timer callbacks run between those yields, as soft IRQs do.
Generators and special methods such as init can’t be made to wait, so a sleep inside one
returns at once; the console lists any such place. Timing is wall-clock time: a sleep can run a
few milliseconds long, sleep_us under 2 ms doesn’t wait at all, and pins change only between
yields, so the MicroPython trace on the logic analyser shows Python’s own timing, not
cycle-accurate signals. time_pulse_us on an HC-SR04’s echo pin returns the echo time for the
distance on its slider instead of timing a pulse.
Limitations
-
The Arduino emulation is the ATmega328P only (Uno, Nano, Pro Mini and other 16 MHz 328P boards); a Mega, Leonardo or ESP32 sketch won’t run. Programs that need hardware this page doesn’t have (an SD card, Wi-Fi, a particular display other than the SSD1306) run, but get no answer from it.
-
Pins have logic levels, not voltages: there are no currents, resistor values or short circuits, and an LED with no resistor works perfectly. Use the circuit simulator for the analogue side.
-
SPI bytes are shown on the Registers tab, but no SPI part answers (MISO reads 0xFF).
-
The emulator has been checked against the examples here (and the tests below), not every library. If a sketch behaves differently from a real board, I’d like to hear about it.
Tests
The page’s logic is tested in node: the Intel HEX parser (checksums, extended addresses, the examples), the WS2812 decoder on a synthetic stream of pin transitions, the SSD1306 command and framebuffer decoder, the servo pulse measurement, the BME280 compensation round trip, the DHT22 and HC-SR04 timing, VCD output and the MicroPython rewrite. If avr8js can be loaded, the tests also run the compiled examples: Blink must toggle pin 13 once a second, Fade must show 490 Hz PWM, the servo must sweep, the NeoPixel ring must decode, the OLED must show text, and the weather station must print the BME280 and DHT22 values set on the virtual parts.
Feedback is a gift. Something to improve, a feature to add, or a bug? Tell me.
Runs entirely in your browser; nothing you type leaves this page.