Learn

AVR assembly in the browser, on a simulated Arduino Uno

Beginner

Or see every lesson.

  • 455parts on the bench
  • 43boards running now
  • 1.00xreal time, on every board
Arduino Uno: an LED on pin 13, blinkinglive0.000 s 0.00x
Click to open it in the editor
The built-in blink, in C, on an LED and a 220 ohm resistor on pin 13. Open it in the editor and pick Blink in assembly to run the same LED from main.S.

An Arduino Uno is an ATmega328P, and under every sketch it runs AVR machine code: 32 registers, a flag byte, and instructions that each take a known number of clock cycles. Mokxi lets you write that layer directly. Add a .S tab beside your sketch, or make the whole board an assembly project with no Arduino core at all, and press Run.

This page walks through the blink the editor hands you: what each instruction does, why PORTB is two different numbers, and how a counted loop becomes exactly 250 milliseconds. Then a timer interrupt, and the debugger reading it back one instruction at a time.

The circuit above is the ordinary built-in blink, written in C. To run the assembly version, open it in the editor, select the Uno, and pick **Blink in assembly** under **Assembly (no Arduino)** in the Firmware panel. The board’s files become a single main.S.

Blink in assembly, line by line

Nothing is hidden under this program: no setup(), no loop(), no millis() interrupt in the background. The startup code clears r1, puts the stack at the top of RAM and calls main. After that, every cycle the chip spends is one you wrote. #include <avr/io.h> works because a .S file goes through the C preprocessor first, and it gives you the register and bit names.

sbi sets one bit of one I/O register. The first makes PB5, the pin marked 13, an output. The second is the trick worth knowing: writing a 1 into a bit of PINB toggles the matching bit of PORTB, in one two-cycle instruction with no read and no mask. Then rcall calls the delay, ret comes back, and rjmp loop goes round again.

main.S: the Blink starter, as the Firmware panel hands it to you
; Blink the LED on pin 13 (PB5) every 250 ms, in AVR assembly.
; No Arduino core: the startup code clears r1, puts the stack at RAMEND
; and calls main, as avr-gcc's does.

#include <avr/io.h>

    .section .text
    .global main
main:
    sbi  _SFR_IO_ADDR(DDRB), DDB5     ; PB5 is an output
loop:
    sbi  _SFR_IO_ADDR(PINB), PINB5    ; writing 1 to PINB5 toggles PB5
    rcall delay_250ms
    rjmp loop

; 250 ms at 16 MHz is 4,000,000 cycles: 40 times 100,000.
; The inner loop is 4 cycles a turn (SBIW 2, BRNE 2), 25,000 turns.
delay_250ms:
    ldi  r18, 40
outer:
    ldi  r24, lo8(25000)
    ldi  r25, hi8(25000)
inner:
    sbiw r24, 1
    brne inner
    dec  r18
    brne outer
    ret

The registers, and which ones you may use

The chip has 32 general registers, r0 to r31, each one byte, reached in one cycle. The upper half is special: ldi, which loads a constant, works only on r16 to r31, which is why the blink keeps its counters in r18, r24 and r25. Write ldi r3, 5 and the build stops with ldi works only on r16 to r31, and r3 is not one of them.

Two registers are spoken for: r1 must hold zero, because the C compiler assumes it everywhere, and r0 is the scratch byte. Then there is SREG, eight flags set by the last instruction that did arithmetic. sbiw sets Z when its result reaches zero, and brne is simply "branch if Z is clear". sbi and cbi change no flags at all. The debugger spells sreg out as ITHSVNZC, with a dot for each flag that is clear.

The AVR registers a first assembly program meets
RegisterWhat it isWatch out for
r0The scratch byte, `__tmp_reg__`. `lpm` lands here by default.The compiler uses it, so no value survives a call.
r1Always zero, `__zero_reg__`.`mul` writes into r1:r0, so `clr r1` after one.
r2 to r17General, but the caller’s.Push and pop them if your function uses them.
r16 to r31The upper half.`ldi`, `cpi`, `subi`, `andi` and `ori` work only here.
r18 to r27, r30, r31Free inside your function.A C function you call may clobber them.
r24, r26, r28, r30The pairs `adiw` and `sbiw` work on.r24:r25 also carries a 16-bit argument and result.
X, Y, Zr26:r27, r28:r29, r30:r31: the pointers.Only Z reads flash, with `lpm`. Y is C’s frame pointer.
SREGThe flags: I T H S V N Z C.Set by arithmetic and compares, not by `sbi`, `ldi` or `mov`.

I/O addresses and data addresses: why PORTB is 0x25 and 0x05

This is the one that costs people an evening. In a .S file, #include <avr/io.h> defines PORTB as its data address, 0x25, which is what lds and sts want. The short instructions, in, out, sbi, cbi, sbic and sbis, want the I/O address instead, which is 32 lower: 0x05. So you wrap the name in _SFR_IO_ADDR().

Get it wrong and Mokxi stops the build before any tool runs, because the mistake would otherwise assemble into a program that writes the wrong register. sbi PORTB, 5 gives PORTB is the data address 0x25 here, and sbi takes its I/O address: write _SFR_IO_ADDR(PORTB), which is 0x05. Registers above I/O address 63, such as TCCR1B, have no short form at all: use lds and sts.

The same bit, three ways
; As avr-libc defines them: a name is a data address.
    sbi  _SFR_IO_ADDR(DDRB), DDB5   ; I/O 0x05, bit 5
    lds  r16, PORTB                 ; data 0x25
    sts  PORTB, r16

; Or move every name down to its I/O address, once:
#define __SFR_OFFSET 0
#include <avr/io.h>
    sbi  DDRB, DDB5                 ; now this is right

Counting cycles: 4,000,000 of them for 250 ms

There is no delay() here, so the delay is a loop you count out by hand. The Uno runs at 16 MHz, so one cycle is 62.5 nanoseconds and 250 milliseconds is 4,000,000 cycles. The blink’s inner loop is two instructions: sbiw r24, 1 takes two cycles, and brne inner takes two when it branches. One turn is four cycles, 25,000 turns is 100,000 cycles, and 40 passes of that is the 4,000,000. The outer loop adds about 120 cycles in all, which is three parts in a hundred thousand.

The simulator spends the cycles the data sheet gives each instruction, so you can prove this from a sketch. Set Timer 1 to the full clock, so it counts once per CPU cycle, read it, run a known loop, read it again. Fifty turns of nop, dec and brne is 50 times 4 less the one the last brne saves by falling through, which is 199, plus 2 for the lds and 1 for the ldi. It prints 202, every time.

The long version agrees: 160 passes of a 25,000-turn loop is 16,000,000 cycles, and micros() puts it between 1,000,000 and 1,015,000 microseconds. The extra half a percent is Timer 0’s own interrupt, the one that keeps millis() going, and a real Uno loses it too.

What the blink’s instructions cost on an ATmega328P, in clock cycles
InstructionCyclesIn the blink
`ldi`1Loads the two loop counters.
`sbi`, `cbi`2Makes PB5 an output, then toggles it.
`sbiw`2Takes 1 off the 16-bit counter in r24:r25.
`dec`1Takes 1 off the outer counter in r18.
`brne`2 taken, 1 notCloses both loops: two plus two is the four-cycle turn.
`rjmp`2Goes back to the top of the main loop.
`rcall`3Calls the delay.
`ret`4Comes back from it.
sketch.ino: timing a loop against Timer 1, which prints 202
void setup() {
  Serial.begin(9600);
  TCCR1A = 0;
  TCCR1B = _BV(CS10);  // clk/1: one count per CPU cycle
  uint8_t start, stop, n;
  noInterrupts();
  asm volatile(
    "lds %[start], %[tcnt]\n\t"
    "ldi %[n], 50\n"
    "1:\n\t"
    "nop\n\t"
    "dec %[n]\n\t"
    "brne 1b\n\t"
    "lds %[stop], %[tcnt]\n\t"
    : [start] "=&r"(start), [stop] "=&r"(stop), [n] "=&d"(n)
    : [tcnt] "i"(&TCNT1L));
  interrupts();
  Serial.println((uint8_t)(stop - start));  // 202
}

void loop() {}

A timer interrupt instead of a delay loop

A counted loop is a fine way to learn cycles and a poor way to keep time, because the processor can do nothing else while it counts. Below, Timer 1 runs in CTC mode at the clock divided by 256, counting 62,500 times a second, so a compare value of 31,249 lands every half second. OCIE1A in TIMSK1 asks for the interrupt and sei lets interrupts happen at all.

TIMER1_COMPA_vect is a name from avr/io.h, and it is really __vector_11, the eleventh entry of the vector table. Declare it .global, define the label, and the startup code’s table jumps to it. Every vector with no handler of yours gets a weak reti, so a stray interrupt returns quietly. Finish with reti, not ret. This handler fits in three lines only because sbi changes no flags.

main.S: the Timer interrupt starter
; Timer 1 interrupts twice a second and the handler toggles pin 13 (PB5).
; main only sets the timer up; the LED is all the interrupt's work.

#include <avr/io.h>

    .section .text
    .global main
main:
    sbi  _SFR_IO_ADDR(DDRB), DDB5
    ; CTC mode, clock / 256: 16 MHz / 256 = 62,500 counts a second,
    ; so a compare at 31,249 is every 0.5 s.
    ldi  r16, hi8(31249)
    sts  OCR1AH, r16                  ; high byte first
    ldi  r16, lo8(31249)
    sts  OCR1AL, r16
    clr  r16
    sts  TCCR1A, r16
    ldi  r16, (1 << WGM12) | (1 << CS12)
    sts  TCCR1B, r16
    ldi  r16, (1 << OCIE1A)           ; interrupt on compare match A
    sts  TIMSK1, r16
    sei
idle:
    rjmp idle

; TIMER1_COMPA_vect is __vector_11; the vector table jumps here.
    .global TIMER1_COMPA_vect
TIMER1_COMPA_vect:
    sbi  _SFR_IO_ADDR(PINB), PINB5    ; toggle the LED (SBI changes no flags)
    reti

Calling assembly from a sketch

You do not have to give up the Arduino core. Keep sketch.ino, add a tab called blink.S, and the two are compiled and linked together, as the Arduino IDE builds a .S beside a sketch. Serial, millis() and pinMode() keep working.

The join is extern "C", which stops C++ from mangling the name, and the AVR calling convention, which decides where the values are. Arguments arrive in r24, then r22, then r20; a result goes back in r24, or r25:r24 when it is two bytes. r1 must be zero again on return, and r2 to r17, r28 and r29 belong to the caller. It works in both directions: here the sketch calls led_toggle, and led_toggle calls bump, an ordinary C function back in the sketch.

blink.S: a tab the sketch calls, which calls the sketch back
#include <avr/io.h>

; void led_toggle(void): toggle pin 13, then count it in C.
    .global led_toggle
led_toggle:
    sbi  _SFR_IO_ADDR(PINB), PINB5
    call bump                 ; a C function of the sketch
    ret

The sketch beside it

Run it with an LED and a 220 ohm resistor on pin 13: the serial monitor prints 1, 2, 3, 4, 5 while the LED toggles every 250 milliseconds. This is the Arduino core with one function written in assembly. For a few instructions, inline asm volatile with constraints is less ceremony, as in the cycle-counting sketch above.

sketch.ino: the other half
extern "C" void led_toggle(void);
extern "C" volatile int bumps;
volatile int bumps = 0;
extern "C" void bump(void) { bumps++; }

void setup() {
  Serial.begin(9600);
  pinMode(13, OUTPUT);
}

void loop() {
  led_toggle();
  Serial.println(bumps);
  delay(250);
}

Watching it in the debugger

This is where assembly in a simulator beats assembly on a bench. Click the gutter beside a line of your .S tab and a breakpoint goes there, the same as on a line of C, because the line numbers the build records are the tab’s own. Press Run and the board stops on it, circuit and all.

The Debug tab shows where it stopped, the core’s own registers with sreg spelled out as its flags, a watch list for globals, and Disassembly: the instructions around the program counter, each row an address, the bytes, the instruction and the source line it came from. Halt on the sbi line of the tab above and the row reads sbi 0x3, 0x5, which is what _SFR_IO_ADDR(PINB), PINB5 really meant. Step instruction runs exactly one instruction and redraws the list.

When it does not assemble, and what else runs

Errors land on the right tab and line with a sentence under them. A misspelled instruction says "lid" is not an AVR instruction and reminds you that a label ends in a colon. add r16, 5 is the quiet one: LLVM’s assembler reads it as add r16, r5, so Mokxi stops instead with add takes two registers, and 5 is a constant and points at subi r16, -5, because the AVR has no ADDI.

Assembly is not only an AVR thing here. A .S or .s tab works on every board that compiles, and the Pico and the STM32F411 Black Pill have the same four starters in ARM Thumb, where your file is the whole program, vector table and all. The Nano and the 16 MHz Pro Mini are the same chip as the Uno. The Writing assembly article in the help center, linked below, is the reference for the rest.

Questions

Can I really write assembly for an Arduino in a browser?

Yes. Add a tab ending .S or .s and it is assembled and linked with your sketch, the way the Arduino IDE builds one. Or pick a starter under "Assembly (no Arduino)" in the board’s Firmware panel and the whole board becomes one main.S with no Arduino core under it.

What is the difference between a .S file and a .s file?

A capital .S goes through the C preprocessor first, so #include <avr/io.h> and #define work in it. A lower case .s is handed straight to the assembler as written.

Why does sbi PORTB, 5 fail when PORTB is the right register?

In a .S file the name PORTB is its data address, 0x25, as in avr-libc, and sbi wants the I/O address, 0x05. Write _SFR_IO_ADDR(PORTB), or put #define __SFR_OFFSET 0 above the include.

Are the cycle counts the same as on a real chip?

Yes, for the instructions. The simulated ATmega328P spends the cycles the data sheet gives each one, so 50 turns of nop, dec and brne count 202 on Timer 1 at the full clock.

Can I step through assembly one instruction at a time?

Yes. Breakpoints work on the lines of a .S tab, and the Debug tab shows the instructions around the halt with the source line each came from. Step instruction runs exactly one of them.

Build this for real

Open the editor, change a value and watch the number move with it. Nothing to install, and no account needed.