Source: https://mokxi.com/docs/editor/assembly
Updated: 2026-10-26

Getting started The editor Placing parts Wiring Sticky notes Drawing on the canvas Tidying up Pinned comments Frames Reusable blocks Making chips Custom chips The code editor and compiling Writing assembly Run and the serial monitor The serial plotter The operating point, sweeps and frequency response The debugger Kix, Mokxi's AI helper WiFi, simulated Real internet Bluetooth, simulated The properties panel Undo, copy and paste Keyboard shortcuts Saving and autosave Sharing a project Exporting PNG and SVG Send a program to a real board VS Code and GitHub with Mokxi Run a prebuilt firmware SPICE netlists Working on a phone Install it as an app Boards Parts Accounts and plans Live collaboration Classrooms Lessons and the learning game Hobby builds Troubleshooting Reference

Docs / The editor / Writing assembly

# Writing assembly

Assembly is a first-class way to program a board here: a .S tab beside your
sketch, or a whole project with no Arduino core under it.

## Why bother

Because some things are only true one instruction at a time. A course that
teaches the AVR or the Cortex-M asks for a delay loop counted in cycles, a
vector table written by hand, and a register set you can read at a halt. All
of that works in the browser, on the same simulated chips the sketches run on,
with nothing to install.

Two ways in:

- Assembly beside a sketch. Add a .S or .s tab and call it from C with
extern "C". The Arduino core is still there, so Serial, millis() and
pinMode() keep working.

- Assembly with no Arduino core. Pick a starter under Assembly (no
Arduino) in the board's Firmware panel. The board's files become one
main.S, and nothing but your own code and the startup code is linked.

## .S and .s tabs

Press the + on the file strip and name the file. The prompt says what it
takes: "New file name, like config.h, helpers.cpp or delay.S". Anything ending
.S or .s is assembly, and it compiles and links with the .ino and .cpp
tabs of the same board, exactly as the Arduino IDE builds a .S next to a
sketch.

The two spellings are not the same file:

- .S , capital S, goes through the C preprocessor first. #include <avr/io.h>, #define, #if and // comments all work, and
__ASSEMBLER__ is set. This is the one to use.

- .s , lower case, is handed to the assembler as it is. No includes, no
macros.

A new .S or .s tab does not start empty. It opens with one working
function the sketch can call, in the board's own instruction set: AVR on the
Uno, the Nano, the Mega, the Leonardo and the ATtiny85, and ARM Thumb on the
Pico, the STM32 boards, the Uno R4, the micro:bit and the XIAO. The comment
above it lists the calling convention, so the rules are in front of you while
you write. Delete it or build on it.

Assembly tabs get syntax highlighting of their own, and the breakpoint gutter
beside the line numbers, the same as a sketch.

## Calling between C and assembly

A function written in assembly is a .global label, and C reaches it through
extern "C" so the name is not mangled. It works both ways: assembly can call
a C function the same way.

The rules are the chip's own, not Mokxi's, so a function that follows them
also works on a real board.

On the AVR boards (the compiler calls this the GCC AVR calling
convention):

- Arguments go in r24, then r22, then r20, and down in twos. A two-byte
argument takes a pair, low byte first: an int in r25:r24.

- The result comes back in r24 for a byte, r25:r24 for two bytes, and
r25:r22 for four.

- r1 must be zero when you return. The compiler assumes it everywhere, so a
mul that dirties it needs clr r1 after.

- r2 to r17, r28 and r29 belong to the caller. Push them if you use
them, and pop them before ret. Everything else is yours to clobber.

On the ARM boards (the standard Arm procedure call standard):

- Arguments go in r0 to r3, and the result comes back in r0.

- r4 to r11 belong to the caller: push them and pop them.

- Return with bx lr, and mark the label .thumb_func.

## avr/io.h in a .S file

On an AVR board #include <avr/io.h> in a .S gives the register and bit
names, and it behaves exactly as avr-libc's does. That matters, because one
rule catches nearly everyone:

A register name is a data address, not an I/O address. PORTB is 0x25.
The I/O address is 32 lower, so in, out, sbi, cbi, sbic and sbis
want _SFR_IO_ADDR(PORTB), which is 0x05:

If you would rather write the plain names everywhere, put one line above the
include and they become I/O addresses instead, as avr-gcc's own startup code
does:

The rest of the header works too:

- Bit names. DDB5, PINB5, TXEN0, WGM12, OCIE1A and the others
are their bit numbers, so (1 << TXEN0) reads the way the data sheet does.

- Vector names. TIMER1_COMPA_vect is __vector_11, so .global TIMER1_COMPA_vect followed by the label makes an interrupt handler the
vector table jumps to. End it with reti.

- __zero_reg__ is r1 and __tmp_reg__ is r0, under those names.

- Registers past the low 64, such as UCSR0B and TIMSK1, are memory
mapped: read them with lds and write them with sts.

## Inline assembly

A few instructions inside a C function do not need a tab of their own.
asm volatile with constraints works on the AVR and the ARM boards:

The simulator counts cycles, so a hand-written delay really is as long as the
data sheet says. A loop of nop, dec and brne, timed with Timer 1 at the
full clock, comes out at exactly the count the ATmega328P data sheet gives its
instructions: an lds, an ldi and 50 turns of the loop measure 202 cycles,
not 201 and not 203. That is what makes counting cycles worth teaching here.

## Assembly with no Arduino core

Open the board's properties panel and look in the
Firmware section. On the Arduino Uno, the Nano, the Pro Mini at 16 MHz, the
Raspberry Pi Pico and the STM32F411 Black Pill there is a group called
Assembly (no Arduino) with four starters in it:

Starter
What it does

Blink in assembly
Toggles the board's LED every 250 ms from a counted delay loop.

Button to LED in assembly
The LED follows a button held to ground by its pull-up.

UART hello in assembly
Prints a line on the serial monitor twice a second.

Timer interrupt in assembly
A timer interrupt toggles the LED, and the main loop only idles.

Pick one and the board's files become a single main.S. If there is work of
your own on the board, you are asked first. After that the panel also offers
Your assembly (main.S), so the board stays in assembly when you come back.

There is no Arduino core in the build: no setup(), no loop(), no
millis() tick and no Serial. What gets linked is only this:

- On an AVR board, your file plus the startup code, which is what
avr-libc's gcrt1.o is to avr-gcc main.S. It is the vector table with
every __vector_n a weak reti you can override, then r1 cleared, the
stack set to RAMEND, .data copied out of flash, .bss cleared, and a
call to your main. So you write main and whichever handlers you want,
under avr-libc's vector names. If your file defines its own .vectors
section, you get no startup code at all and the whole program is yours.

- On the Pico and the Black Pill, nothing but your file. Your file is the
vector table: .word _stack_top on the Pico or .word _estack on the Black
Pill, then .word Reset_Handler, then the handlers you want, each one
.thumb_func.

What the starters set up, if you want to read the registers along with them:

- AVR blink toggles PB5 by writing a 1 to PINB5, which the ATmega328P
reads as "toggle", and delays with an outer and an inner loop: "250 ms at
16 MHz is 4,000,000 cycles: 40 times 100,000", and the inner loop is four
cycles a turn.

- AVR button reads PD2 with its pull-up on, so a press reads 0, and uses
sbic to skip a line.

- AVR UART sets USART0 to 9600 baud with UBRR 103, keeps the text in
.progmem and reads it a byte at a time with lpm.

- AVR timer runs Timer 1 in CTC mode at the clock over 256 with OCR1A at
31,249, which is every 0.5 s, and toggles the LED in
TIMER1_COMPA_vect.

- Pico takes IO_BANK0 and PADS_BANK0 (and UART0 for the UART one)
out of RESETS, drives GPIO25 through SIO at 0xD0000000, delays on
TIMERAWL, talks on UART0 on GPIO0 at 115200, and uses the TIMER's alarm 0
IRQ for the timer starter.

- Black Pill turns on the clocks in RCC_AHB1ENR, drives PC13, which is
lit when low, reads PA0 with its pull-up, prints on USART1 on PA9 at 115200
with BRR 729, and runs SysTick at 1 kHz.

Press Run and the board runs it. The serial monitor, the scope, the logic
analyzer and the parts on the canvas all work as usual, because the simulated
chip does not know or care what language made the image.

## When it does not assemble

Assembler errors land on the right tab and the right line, with a sentence in
plain English under them saying what to write instead. A few of them:

- ldi r3, 5 says LDI works only on the upper registers, r16 to r31, and to
mov the value down afterwards if you need it low.

- lid r16, 5 says "lid" is not an AVR instruction, and reminds you that a
label ends in a colon and a directive starts with a dot.

- A label written without its colon is named back to you with the colon added.

- A conditional branch that cannot reach its target is placed on its line by
the linker, with the fix: brne 1f, rjmp far, 1:.

Four of them are checks avr-as makes that LLVM's assembler does not, so Mokxi
makes them itself, before any tool runs. Without them a mistake would assemble
quietly into the wrong program:

- ldi r16, 300 does not fit in a byte. Load the halves with lo8() and
hi8().

- sbi PORTB, 5 uses the data address 0x25 where sbi wants an I/O
address. Write _SFR_IO_ADDR(PORTB).

- add r16, 5 reads as add r16, r5, because ADD takes two registers. There
is no ADDI: subtract the negative with subi r16, -5.

- out PORTB, r16 without __SFR_OFFSET 0 is a warning rather than an error,
because it does assemble: it just writes a different register than the one
named.

in and out past I/O address 63, sbiw and adiw past 63, and a bit
number past 7 are caught the same way, each with the instruction to use
instead.

On the Thumb boards the common ones are a high register on a Cortex-M0+, which
reaches only r0 to r7, a 32-bit Thumb-2 form on a Cortex-M0+, which has
only the 16-bit set, a branch out of range, a missing Reset_Handler and a
missing main.

## Stepping through instructions

Everything in the debugger works on assembly.
Breakpoints go in the gutter of a .S tab and stop on that line, because the
line numbers the assembler records are the tab's own, not the preprocessed
copy's.

The Debug tab has a Disassembly section. At a halt it lists the
instructions around the program counter: the address, the bytes, the
instruction in llvm-objdump's spelling, and the source line each one starts,
which is a button that takes you to it. The instruction about to run is
marked. Step instruction runs exactly one machine instruction, where Step
over and Step into still move a whole source line at a time.

Registers read as the core's own. sreg on an AVR is also spelled out as its
flags, ITHSVNZC, with a dot for each one that is clear, and xpsr on an ARM
is spelled out as N Z C V Q, because eight bits are not a number.

## Limits

- Assembly with no Arduino core is offered on the Uno, the Nano, the Pro
Mini, the Pico and the STM32F411 Black Pill. Every other board still takes
.S and .s tabs beside a sketch.

- The ESP32 family is not included. Clang's own Xtensa assembler is not
complete for hand-written files, so those boards compile C and C++ only.

- The Pico W has no starters. Its LED is on the wireless chip rather than
a GPIO pin, and that chip is not simulated. Use a plain Pico.

- A project is flat. File names carry no directories, so an include path
is one directory deep.

## Related

- The code editor and compiling

- The debugger

- The properties panel

- AVR assembly in the browser: the blink
starter line by line, cycle counting and a timer interrupt

- Compiling in the browser: how the .S is preprocessed,
assembled and linked

Previous: The code editor and compiling Next: Run and the serial monitor

### On this page

Why bother .S and .s tabs Calling between C and assembly avr/io.h in a .S file Inline assembly Assembly with no Arduino core When it does not assemble Stepping through instructions Limits Related
