ARM assembly on the Raspberry Pi Pico 2

A tutorial from the first instruction to interrupts and the clock, run and watched step by step with Asmonite.

Not the original text

This is not the text of Niklas Gürtler. It is a new text, written for the Raspberry Pi Pico 2 and Asmonite: it follows the chapters and the examples of his ARM-ASM-Tutorial (in German, for the STM32F103 and the GNU tools), but the wording is our own, and so are any mistakes in it. Published with his kind permission.

This tutorial follows the ARM-ASM-Tutorial by Niklas Gürtler on mikrocontroller.net, chapter by chapter. His tutorial is written for the STM32F103 and the GNU tools (assembler, linker, OpenOCD, GDB). This version uses the Raspberry Pi Pico 2 and Asmonite instead: one program assembles, loads and debugs, and all you need besides the board is a debug probe. Where the Pico (the first one) differs, a box says so, and another one shows the examples on the Blue Pill (STM32F103), the board of the original tutorial.

The example programs are adapted from his examples (MIT License, Copyright 2019 Niklas Gürtler, see LICENSE.txt); each one can be downloaded from its code block.

Contents

  1. 1 Introduction
    1. 1.1 Why assembly?
    2. 1.2 About Arm
    3. 1.3 Architectures and cores
  2. 2 Prerequisites
    1. 2.1 The board
    2. 2.2 Documentation
    3. 2.3 Debug probe
    4. 2.4 Software
  3. 3 Setup
    1. 3.1 Hardware
    2. 3.2 Software
  4. 4 Writing assembly programs
    1. 4.1 A first program
    2. 4.2 Loading the program
    3. 4.3 Using the debugger
    4. 4.4 Processor registers
    5. 4.5 Accessing the peripherals
    6. 4.6 Data processing
    7. 4.7 Reading peripheral registers
    8. 4.8 Jump instructions
    9. 4.9 Counting loops
    10. 4.10 Using the RAM
  5. 5 Memory management
    1. 5.1 The address space
    2. 5.2 The linker, and why Asmonite has none
    3. 5.3 Placing code and data
    4. 5.4 Program structure
  6. 6 More assembly techniques
    1. 6.1 The instruction set state
    2. 6.2 Constants
    3. 6.3 The stack
    4. 6.4 Function calls
    5. 6.5 Conditional execution
    6. 6.6 8 and 16 bit arithmetic
    7. 6.7 Alignment
    8. 6.8 Offset addressing
    9. 6.9 Iterating arrays
    10. 6.10 Literal loads
    11. 6.11 The SysTick timer
    12. 6.12 Exceptions and interrupts
    13. 6.13 Macros
    14. 6.14 Weak symbols
    15. 6.15 Symbol aliases
    16. 6.16 An improved vector table
    17. 6.17 .include
    18. 6.18 Local labels
    19. 6.19 Initializing the RAM
    20. 6.20 Interrupts of the peripherals
    21. 6.21 Analysis tools
    22. 6.22 Interfacing C code
    23. 6.23 The clock
    24. 6.24 A program template
  7. Credits

1 Introduction

1.1 Why assembly?

Hardly anybody writes a whole program in assembly any more: compilers produce good code, and C or Rust are much quicker to write. But knowing the machine level pays off all the same. You read the code a compiler made when a program behaves strangely or is too slow; you write the few lines that no high-level language can express (the start of a program, the switch between tasks, a special instruction); and you understand what a debugger shows you when there is no source to look at. A microcontroller is the best place to learn it: there is no operating system between you and the hardware, every register of every peripheral is an address you can read and write, and an LED shows at once whether your program does what you think.

The hard part is usually not the instructions. It is the set-up: the tools, the documentation, and the many small things a program must do before the first LED lights up. This tutorial goes through all of it with as little tooling as possible.

1.2 About Arm

Arm Ltd. does not make chips. It designs processor cores and licenses them to chip makers, who add memory and peripherals around them. So two chips with the same core, such as the Cortex-M33 in the RP2350 of the Pico 2 and in many other microcontrollers, run the same instructions, but their peripherals (the GPIO, the timers, the clock system) are completely different. That is why this tutorial has two kinds of documents: the Arm manuals for everything that belongs to the core, and the RP2350 datasheet for everything around it.

1.3 Architectures and cores

An architecture is the specification: the instructions, the registers, how exceptions work. A core is one implementation of it. The microcontroller cores of Arm are called Cortex-M, and they all run the Thumb instruction set, whose instructions are 16 or 32 bits long:

The RP2350 is unusual: it also has two RISC-V cores, and at start-up the boot ROM decides from the program in the flash which pair runs. This tutorial uses the Arm cores; Asmonite switches the chip over when needed (3.2).

2 Prerequisites

2.1 The board

A Raspberry Pi Pico 2 (RP2350, about 5 €). The W version works too, but its LED is on the wireless chip, not at GPIO 25, so the blink examples do not light it; connect an LED with a resistor (about 330 Ω) from GPIO 25 to ground, or change the pin in the examples. A first Raspberry Pi Pico (RP2040) works for most chapters as well; the boxes marked Pico (Cortex-M0+) show what is different, and three examples are in a version for it.

Blue Pill (STM32F103)

The board of the original tutorial: an STM32F103C8 with a Cortex-M3, 20 KB of RAM and 64 KB (in fact mostly 128 KB) of flash, its LED at PC13. Asmonite debugs it over the same probe (.target stm32f103), in the RAM and in the flash. The boxes like this one show three examples of the tutorial on it, close to the originals. Boards with a clone chip (CKS32, GD32, APM32) have another device id; Asmonite then says so and writes nothing.

2.2 Documentation

Asmonite has an instruction reference built in: put the cursor on an instruction and press F1.

2.3 Debug probe

The Pico 2 can be programmed without any probe: hold BOOTSEL while you plug it in and drag a UF2 file onto the drive that appears. But then you see nothing of what the program does. A debug probe lets the PC stop the core, run it instruction by instruction, and read and write its registers and memory, over two wires (SWD). Asmonite works with the Raspberry Pi Debug Probe (about 12 €) or with a second Pico running the debugprobe firmware; both are CMSIS-DAP probes, which Asmonite drives directly over USB.

2.4 Software

Only Asmonite. It assembles, checks the program against the board, writes it to the RAM or the flash over the probe and debugs it. There is no separate linker, no OpenOCD and no GDB, and nothing has to be installed: unpack the ZIP and start Asmonite.exe (Windows 10 or 11). A calculator with hexadecimal and binary is useful (the one of Windows has a Programmer mode).

3 Setup

3.1 Hardware

The Pico 2 has a three-pin DEBUG connector at its end, opposite the USB socket: SWCLK, GND, SWDIO. The Debug Probe comes with a cable for it; plug it into the port marked D of the probe. Then connect both to the PC: the probe with its USB cable, the Pico 2 with its own (for power). Nothing else is needed; the LED of the Pico 2 is at GPIO 25.

Blue Pill (STM32F103)

Its four pins at the end of the board are 3V3, SWIO, SWCLK and GND. Connect SWCLK, SWIO (to SWDIO of the probe) and GND, and leave 3V3 free: the Debug Probe gives no power, the Blue Pill gets it over its own USB socket.

3.2 Software

In Asmonite open Debug > Debug targets, choose Raspberry Pi Pico 2 and press Test the connection. It says whether the probe answers and which cores of the RP2350 run: runs its Arm cores or runs its RISC-V cores. A new Pico 2 often runs its RISC-V cores, or none, depending on what is in its flash. A program in the RAM cannot change that, because after a reset the boot ROM again starts the cores the flash program is for. So run the flash example 08_blink_flash.asm once first: when Asmonite writes it, it switches the chip to its Arm cores, and from then on the RAM examples work.

Pico (Cortex-M0+)

Choose Raspberry Pi Pico in Debug targets. The RP2040 has only Arm cores, so there is nothing to switch. Its debug connector has the same three pins.

Blue Pill (STM32F103)

Choose STM32F103 (Blue Pill) in Debug targets; Test the connection shows the device id (410 for the STM32F103C8/CB), the revision and the size of the flash. There is nothing to switch either.

4 Writing assembly programs

4.1 A first program

The smallest program that does something visible in the debugger:

01_empty.asm download
; ARM assembly tutorial for Asmonite, example 01: the smallest program (Raspberry Pi Pico 2, Arm Cortex-M33).
; Adapted from the ARM-ASM-Tutorial by Niklas Guertler (example EmptyProgram); see LICENSE.txt.
;
; The debugger loads it into the RAM at 0x20000000 and stops at _start (F7). Step with F7: "nop" does
; nothing, "b $" jumps to itself, for ever.
.target pico2
.arch thumb
.imagebase 0x20000000

_start:
    nop                         ; do nothing
    b    $                      ; endless loop: $ is the address of this line

Line by line:

Everything after a ; is a comment. Asmonite does not care about upper and lower case in instructions and labels.

GNU assembler

The original tutorial writes for the GNU assembler (as), whose syntax differs in details: comments start with @, the current address is . instead of $, and there are directives like .syntax unified and .cpu cortex-m3. Asmonite's syntax is close to NASM's; the boxes like this one point out the differences where they matter.

4.2 Loading the program

Save the program (Ctrl+S) and press F7 (Step into). Asmonite assembles it, connects to the probe, resets the core and halts it, writes the program into the RAM at 0x20000000, and stops at _start: the line is marked in the editor. Nothing was written to the flash, so after a reset the board runs whatever is in its flash again.

A program in the RAM is the quickest way to try things: loading takes a fraction of a second and the flash does not wear. A program for the flash (5.3.3) is written there by the same key, and then also runs by itself after a power cycle.

4.3 Using the debugger

While the program is stopped:

Step through the first program with F7: after nop, PC is 2 higher (a 16-bit instruction), and b $ stays where it is.

4.4 Processor registers

The Cortex-M has 16 registers of 32 bits:

RegisterUse
r0 – r12general purpose: values, addresses, counters. Many 16-bit instructions can only use r0 – r7 (the low registers).
r13 = spthe stack pointer (6.3)
r14 = lrthe link register: the return address of a call (6.4)
r15 = pcthe program counter: the address of the instruction being carried out

Besides them there is the program status register xPSR. Its top four bits are the flags that many instructions set and the conditional branches test: N (the result is negative, bit 31 is 1), Z (the result is zero), C (carry: an unsigned overflow, or the bit shifted out), V (overflow of a signed result).

All data processing happens in registers. Memory is only read with ldr (load register) and written with str (store register), always through an address in a register: ldr r0, [r1] reads the word at the address in r1 into r0; str r0, [r1] writes r0 to it. This is called a load/store architecture.

4.5 Accessing the peripherals

Every peripheral of the RP2350 is controlled through registers at fixed addresses: writing a word to 0xd0000018 sets output pins, reading 0x400b0028 gives the time in microseconds. For the core this is ordinary memory, read and written with ldr and str. The datasheet lists every block with its base address and the offsets of its registers.

To make the LED at GPIO 25 light up, four blocks are involved:

4.5.1 Taking blocks out of reset

After a reset most peripherals of the RP2350 are held in reset themselves, to save power; they do nothing and ignore writes. The block RESETS (base 0x40020000) controls this: each bit of its register RESET holds one block in reset while it is 1. The GPIO needs two of them: IO_BANK0 (bit 6) and PADS_BANK0 (bit 9). After clearing the bits, the program waits until the same bits are 1 in RESET_DONE (offset 8).

(The STM32F103 of the original tutorial switches the clock of a peripheral on instead; the idea is the same: a peripheral must be woken up before it can be used.)

4.5.2 Configuring the pin

A pin of the RP2350 passes three blocks on its way to the core:

4.5.3 Writing a pin

The SIO has a register for the output values (GPIO_OUT, offset 0x10) and three helpers that change only the bits that are 1 in the value written: GPIO_OUT_SET (0x18) sets them, GPIO_OUT_CLR (0x20) clears them, GPIO_OUT_XOR (0x28) toggles them. With bit 25 set in GPIO_OUT_SET the LED goes on. Here is all of it:

02_set_pin.asm download
; ARM assembly tutorial for Asmonite, example 02: switch on the LED (Raspberry Pi Pico 2, Arm Cortex-M33).
; Adapted from the ARM-ASM-Tutorial by Niklas Guertler (example SetPin); see LICENSE.txt.
;
; The LED of the Pico 2 is at GPIO 25. Four steps make the pin an output that is high:
;   1. take the blocks IO_BANK0 and PADS_BANK0 out of their reset (RESETS)
;   2. remove the isolation of the pad of GPIO 25 (PADS_BANK0)
;   3. give the pin to the SIO, the simple input/output block of the cores (IO_BANK0)
;   4. switch on the output driver and set the output to 1 (SIO)
; Every address and value is loaded from the table at the end ("ldr r1, name").
.target pico2
.arch thumb
.imagebase 0x20000000

_start:
    ldr  r1, resets_clr
    ldr  r0, reset_bits
    str  r0, [r1]               ; RESETS.RESET: clear bit 6 (IO_BANK0) and bit 9 (PADS_BANK0)
    ldr  r1, resets_done
wait_reset:
    ldr  r2, [r1]               ; RESETS.RESET_DONE: both bits must be 1
    and  r2, r2, r0
    cmp  r2, r0
    bne  wait_reset

    ldr  r1, pad25_clr
    ldr  r0, pad_iso
    str  r0, [r1]               ; PADS_BANK0.GPIO25: clear bit 8 (ISO)

    ldr  r1, gpio25_ctrl
    ldr  r0, funcsel_sio
    str  r0, [r1]               ; IO_BANK0.GPIO25_CTRL: function 5 = SIO

    ldr  r1, sio_oe_set
    ldr  r0, led_bit
    str  r0, [r1]               ; SIO.GPIO_OE_SET: GPIO 25 is an output
    ldr  r1, sio_out_set
    str  r0, [r1]               ; SIO.GPIO_OUT_SET: GPIO 25 is high, the LED is on

    b    $

    align 4
resets_clr:  dd 0x40023000      ; RESETS.RESET (0x40020000), +0x3000: the alias that clears bits
reset_bits:  dd 0x00000240      ; bit 6 IO_BANK0, bit 9 PADS_BANK0
resets_done: dd 0x40020008      ; RESETS.RESET_DONE
pad25_clr:   dd 0x4003b068      ; PADS_BANK0.GPIO25 (0x40038068), +0x3000: clear bits
pad_iso:     dd 0x00000100      ; bit 8 ISO
gpio25_ctrl: dd 0x400280cc      ; IO_BANK0.GPIO25_CTRL
funcsel_sio: dd 5               ; FUNCSEL 5: SIO
sio_oe_set:  dd 0xd0000038      ; SIO.GPIO_OE_SET
sio_out_set: dd 0xd0000018      ; SIO.GPIO_OUT_SET
led_bit:     dd 0x02000000      ; bit 25

The addresses and values are 32-bit numbers, too large to fit into an instruction. So they stand in a table after the code, each with a label, and ldr r1, resets_clr loads the word at the label resets_clr (6.10 explains how). align 4 makes sure that the table starts at an address that is a multiple of 4, as words must.

One more thing of the RP2350: every register of the peripherals has three more addresses, aliases, that change only some of its bits: +0x1000 toggles (xor) the bits written as 1, +0x2000 sets them, +0x3000 clears them. 0x40023000 is RESET through its clear alias: writing 0x240 there clears bits 6 and 9 and leaves the others alone. The pad of GPIO 25 is cleared the same way.

Press F7 a few times and watch the registers; after the last str the LED is on.

Pico (Cortex-M0+)

The RP2040 has other addresses: RESETS at 0x4000c000 with IO_BANK0 in bit 5 and PADS_BANK0 in bit 8, IO_BANK0 at 0x40014000, and the SIO registers GPIO_OUT_SET at 0x14, GPIO_OUT_CLR at 0x18, GPIO_OUT_XOR at 0x1c, GPIO_OE_SET at 0x24. Its pads are not isolated after a reset. The aliases are the same. See pico1/05_blink_delay.asm.

Blue Pill (STM32F103)

The STM32 does it its own way, the way of the original tutorial: a peripheral is woken up by switching on its clock (port C: bit 4 of RCC_APB2ENR at 0x40021018), a pin is configured with 4 bits in GPIOx_CRL (pins 0 to 7) or GPIOx_CRH (pins 8 to 15), and GPIOx_BSRR sets a pin with its bit and clears it with the bit 16 higher. The LED at PC13 is on when the pin is low. See bluepill/05_blink_delay.asm.

4.6 Data processing

The arithmetic and logic instructions mostly have three operands: a destination register and two sources, the second of which can be a register or a constant.

add r0, r1, r2r0 = r1 + r2
sub r0, r1, #4r0 = r1 − 4
and r0, r0, #0xffkeep bits 0 to 7, clear the others
orr r0, r0, #4set bit 2 (or)
bic r0, r0, #0x100clear bit 8 (bit clear: and with the inverted constant)
eor r0, r0, r1exclusive or: toggle the bits that are 1 in r1
lsl r0, r1, #2shift left by 2: r0 = r1 × 4 (lsr, asr: right, unsigned or signed)
mov r0, r1copy

An s at the end (adds, subs, ands, movs) makes the instruction set the flags as well. cmp r0, r1 subtracts only to set the flags and throws the result away; tst r0, #4 does the same with and.

GNU assembler

GNU as also takes two operands when the destination is the first source: and r0, #0xfffffff0 for and r0, r0, #0xfffffff0. Asmonite wants all three written, except for the 16-bit forms of the M0+ (ands r2, r0).

4.7 Reading peripheral registers

Often only a few bits of a register must change, and the others must stay as they are. Then the program reads the register, changes the bits in a core register, and writes the word back: read-modify-write. This version of the program does it for RESETS, the pad and GPIO25_CTRL (whose function number is in bits 0 to 4):

03_set_pin_rmw.asm download
; ARM assembly tutorial for Asmonite, example 03: read, change, write (Raspberry Pi Pico 2, Arm Cortex-M33).
; Adapted from the ARM-ASM-Tutorial by Niklas Guertler (example SetPin2); see LICENSE.txt.
;
; The same as 02_set_pin.asm, but a register is read, a few bits of it are changed with "bic" (bit clear)
; and "orr" (or), and the value is written back: the other bits keep their value. Example 02 used the
; clear alias of the RP2350 instead, which changes only the given bits by itself.
.target pico2
.arch thumb
.imagebase 0x20000000

_start:
    ldr  r1, resets_reset
    ldr  r0, [r1]               ; read RESETS.RESET
    bic  r0, r0, #0x240         ; clear bit 6 (IO_BANK0) and bit 9 (PADS_BANK0)
    str  r0, [r1]               ; write it back
    ldr  r1, resets_done
wait_reset:
    ldr  r2, [r1]
    and  r2, r2, #0x240
    cmp  r2, #0x240
    bne  wait_reset

    ldr  r1, pad25
    ldr  r0, [r1]               ; read PADS_BANK0.GPIO25
    bic  r0, r0, #0x100         ; clear ISO
    str  r0, [r1]

    ldr  r1, gpio25_ctrl
    ldr  r0, [r1]
    bic  r0, r0, #0x1f          ; FUNCSEL is bits 4..0
    orr  r0, r0, #5             ; 5 = SIO
    str  r0, [r1]

    ldr  r1, sio_oe_set
    ldr  r0, led_bit
    str  r0, [r1]               ; GPIO 25 is an output
    ldr  r1, sio_out_set
    str  r0, [r1]               ; and high

    b    $

    align 4
resets_reset: dd 0x40020000     ; RESETS.RESET
resets_done:  dd 0x40020008     ; RESETS.RESET_DONE
pad25:        dd 0x40038068     ; PADS_BANK0.GPIO25
gpio25_ctrl:  dd 0x400280cc     ; IO_BANK0.GPIO25_CTRL
sio_oe_set:   dd 0xd0000038     ; SIO.GPIO_OE_SET
sio_out_set:  dd 0xd0000018     ; SIO.GPIO_OUT_SET
led_bit:      dd 0x02000000     ; bit 25

Read-modify-write takes three instructions and is not safe when an interrupt changes the same register in between. That is why the RP2350 has the set and clear aliases: one str changes exactly the bits wanted.

4.8 Jump instructions

b label (branch) continues the program at the label. With a condition after the b it jumps only if the flags fit the condition, otherwise the next instruction follows:

BranchJumps ifFlags
beq / bneequal / not equal (zero / not zero)Z = 1 / Z = 0
bhs / blounsigned higher or same / lowerC = 1 / C = 0
bhi / blsunsigned higher / lower or same
bge / bltsigned greater or equal / less thanN = V / N ≠ V
bgt / blesigned greater / less or equal
bmi / bplnegative / positive or zeroN = 1 / N = 0

The wait loop in the examples above is such a branch: cmp r2, r0 compares, bne wait_reset jumps back while they differ. A loop without a condition makes the LED blink:

04_blink.asm download
; ARM assembly tutorial for Asmonite, example 04: on, off, again (Raspberry Pi Pico 2, Arm Cortex-M33).
; Adapted from the ARM-ASM-Tutorial by Niklas Guertler (examples Blink and Blink2); see LICENSE.txt.
;
; The LED is switched on and off in an endless loop. It runs so fast that the LED looks dimly lit: step
; through the loop with F7 and watch it change. 05_blink_delay.asm adds the waiting.
.target pico2
.arch thumb
.imagebase 0x20000000

_start:
    ldr  r1, resets_clr
    ldr  r0, reset_bits
    str  r0, [r1]               ; IO_BANK0 and PADS_BANK0 out of reset
    ldr  r1, resets_done
wait_reset:
    ldr  r2, [r1]
    and  r2, r2, r0
    cmp  r2, r0
    bne  wait_reset
    ldr  r1, pad25_clr
    mov  r0, #0x100
    str  r0, [r1]               ; pad of GPIO 25: no isolation
    ldr  r1, gpio25_ctrl
    movs r0, #5
    str  r0, [r1]               ; GPIO 25: SIO
    ldr  r1, sio_oe_set
    ldr  r0, led_bit
    str  r0, [r1]               ; GPIO 25: output

blink_loop:
    ldr  r1, sio_out_set
    ldr  r0, led_bit
    str  r0, [r1]               ; LED on
    ldr  r1, sio_out_clr
    ldr  r0, led_bit
    str  r0, [r1]               ; LED off
    b    blink_loop

    align 4
resets_clr:  dd 0x40023000
reset_bits:  dd 0x00000240
resets_done: dd 0x40020008
pad25_clr:   dd 0x4003b068
gpio25_ctrl: dd 0x400280cc
sio_oe_set:  dd 0xd0000038
sio_out_set: dd 0xd0000018      ; SIO.GPIO_OUT_SET
sio_out_clr: dd 0xd0000020      ; SIO.GPIO_OUT_CLR
led_bit:     dd 0x02000000

It blinks so fast that the LED only looks dim; step through the loop with F7 to see it change.

4.9 Counting loops

To slow the blinking down, the program counts a register down between the two writes. subs subtracts and sets the Z flag when the result is 0, bne jumps back while it is not:

05_blink_delay.asm download
; ARM assembly tutorial for Asmonite, example 05: blink with a waiting loop (Raspberry Pi Pico 2, Arm Cortex-M33).
; Adapted from the ARM-ASM-Tutorial by Niklas Guertler (example BlinkDelay); see LICENSE.txt.
;
; Between on and off the program counts r2 down from 1000000 to 0. "subs" subtracts and sets the flags;
; "bne" (branch if not equal) jumps back as long as the result is not 0.
.target pico2
.arch thumb
.imagebase 0x20000000

_start:
    ldr  r1, resets_clr
    ldr  r0, reset_bits
    str  r0, [r1]
    ldr  r1, resets_done
wait_reset:
    ldr  r2, [r1]
    and  r2, r2, r0
    cmp  r2, r0
    bne  wait_reset
    ldr  r1, pad25_clr
    mov  r0, #0x100
    str  r0, [r1]
    ldr  r1, gpio25_ctrl
    movs r0, #5
    str  r0, [r1]
    ldr  r1, sio_oe_set
    ldr  r0, led_bit
    str  r0, [r1]

blink_loop:
    ldr  r1, sio_out_set
    ldr  r0, led_bit
    str  r0, [r1]               ; LED on
    ldr  r2, delay_count
delay1:
    subs r2, r2, #1
    bne  delay1                 ; count down to 0

    ldr  r1, sio_out_clr
    ldr  r0, led_bit
    str  r0, [r1]               ; LED off
    ldr  r2, delay_count
delay2:
    subs r2, r2, #1
    bne  delay2

    b    blink_loop

    align 4
resets_clr:  dd 0x40023000
reset_bits:  dd 0x00000240
resets_done: dd 0x40020008
pad25_clr:   dd 0x4003b068
gpio25_ctrl: dd 0x400280cc
sio_oe_set:  dd 0xd0000038
sio_out_set: dd 0xd0000018
sio_out_clr: dd 0xd0000020
led_bit:     dd 0x02000000
delay_count: dd 1000000

The loop takes a few clock cycles per round; how long it takes in seconds depends on the clock of the core (6.23). The values that do not change can be loaded once before the loop:

06_blink_registers.asm download
; ARM assembly tutorial for Asmonite, example 06: keep the values in registers (Raspberry Pi Pico 2, Arm Cortex-M33).
; Adapted from the ARM-ASM-Tutorial by Niklas Guertler (example BlinkDelay2); see LICENSE.txt.
;
; The loop of 05_blink_delay.asm loads the same values again and again. Here they are loaded once into
; r0 .. r3 before the loop; "mov r4, r3" copies the count for each waiting loop.
.target pico2
.arch thumb
.imagebase 0x20000000

_start:
    ldr  r1, resets_clr
    ldr  r0, reset_bits
    str  r0, [r1]
    ldr  r1, resets_done
wait_reset:
    ldr  r2, [r1]
    and  r2, r2, r0
    cmp  r2, r0
    bne  wait_reset
    ldr  r1, pad25_clr
    mov  r0, #0x100
    str  r0, [r1]
    ldr  r1, gpio25_ctrl
    movs r0, #5
    str  r0, [r1]
    ldr  r1, sio_oe_set
    ldr  r0, led_bit
    str  r0, [r1]

    ldr  r0, sio_base           ; r0 = 0xd0000000, the SIO
    ldr  r1, led_bit            ; r1 = bit 25
    ldr  r3, delay_count        ; r3 = the count of a waiting loop
blink_loop:
    str  r1, [r0, #0x18]        ; GPIO_OUT_SET (0xd0000018): LED on
    mov  r4, r3
delay1:
    subs r4, r4, #1
    bne  delay1
    str  r1, [r0, #0x20]        ; GPIO_OUT_CLR (0xd0000020): LED off
    mov  r4, r3
delay2:
    subs r4, r4, #1
    bne  delay2
    b    blink_loop

    align 4
resets_clr:  dd 0x40023000
reset_bits:  dd 0x00000240
resets_done: dd 0x40020008
pad25_clr:   dd 0x4003b068
gpio25_ctrl: dd 0x400280cc
sio_oe_set:  dd 0xd0000038
sio_base:    dd 0xd0000000
led_bit:     dd 0x02000000
delay_count: dd 1000000

Here r0 holds the base address of the SIO, and the registers are reached by an offset: str r1, [r0, #0x18] writes to 0xd0000000 + 0x18. One base address for many registers is the usual way to work with a peripheral (6.8).

Pico (Cortex-M0+)

The same program for the RP2040, with its addresses and with ands r2, r0 (two operands) instead of and r2, r2, r0:

pico1/05_blink_delay.asm download
; ARM assembly tutorial for Asmonite, example 05 for the Raspberry Pi Pico (RP2040, Arm Cortex-M0+).
; Adapted from the ARM-ASM-Tutorial by Niklas Guertler (example BlinkDelay); see ..\LICENSE.txt.
;
; The same program as ..\05_blink_delay.asm. What is different on the Pico:
;   - other addresses: RESETS at 0x4000c000, IO_BANK0 at 0x40014000, other bits and SIO offsets, and the
;     pads have no isolation to remove
;   - the Cortex-M0+ knows only a part of the Thumb instructions (ARMv6-M): most of them work on r0 .. r7
;     only and are 16 bit long; "and" has two operands ("ands r2, r0": r2 = r2 and r0, flags set)
.target pico
.arch thumb
.imagebase 0x20000000

_start:
    ldr  r1, resets_clr
    ldr  r0, reset_bits
    str  r0, [r1]               ; IO_BANK0 and PADS_BANK0 out of reset
    ldr  r1, resets_done
wait_reset:
    ldr  r2, [r1]
    ands r2, r0
    cmp  r2, r0
    bne  wait_reset
    ldr  r1, gpio25_ctrl
    movs r0, #5
    str  r0, [r1]               ; GPIO 25: SIO
    ldr  r1, sio_oe_set
    ldr  r0, led_bit
    str  r0, [r1]               ; GPIO 25: output

blink_loop:
    ldr  r1, sio_out_set
    ldr  r0, led_bit
    str  r0, [r1]               ; LED on
    ldr  r2, delay_count
delay1:
    subs r2, r2, #1
    bne  delay1
    ldr  r1, sio_out_clr
    ldr  r0, led_bit
    str  r0, [r1]               ; LED off
    ldr  r2, delay_count
delay2:
    subs r2, r2, #1
    bne  delay2
    b    blink_loop

    align 4
resets_clr:  dd 0x4000f000      ; RESETS.RESET (0x4000c000), +0x3000: clear bits
reset_bits:  dd 0x00000120      ; bit 5 IO_BANK0, bit 8 PADS_BANK0
resets_done: dd 0x4000c008      ; RESETS.RESET_DONE
gpio25_ctrl: dd 0x400140cc      ; IO_BANK0.GPIO25_CTRL
sio_oe_set:  dd 0xd0000024      ; SIO.GPIO_OE_SET
sio_out_set: dd 0xd0000014      ; SIO.GPIO_OUT_SET
sio_out_clr: dd 0xd0000018      ; SIO.GPIO_OUT_CLR
led_bit:     dd 0x02000000      ; bit 25
delay_count: dd 1000000

Blue Pill (STM32F103)

Nearly the example BlinkDelay of the original tutorial, with the LED at PC13 instead of PA8, in the RAM:

bluepill/05_blink_delay.asm download
; ARM assembly tutorial for Asmonite, example 05 for the Blue Pill (STM32F103, Arm Cortex-M3).
; Adapted from the ARM-ASM-Tutorial by Niklas Guertler (example BlinkDelay); see ..\LICENSE.txt.
;
; The STM32F103 is the chip of the original tutorial, and this is nearly its example. Two differences:
;   - the LED of the Blue Pill is at PC13 (the original uses PA8), and it is on when the pin is low
;   - the program runs in the RAM (0x20000000): F7 loads it there and stops at _start
; The peripherals of the STM32 are switched on through their clock (RCC_APB2ENR, bit 4 for port C), and a
; pin is configured with 4 bits in GPIOx_CRL (pins 0..7) or GPIOx_CRH (pins 8..15): MODE (2 = output,
; 2 MHz) and CNF (0 = push-pull). GPIOx_BSRR sets a pin with its bit (0..15) and clears it with the bit 16
; higher.
.target stm32f103
.arch thumb
.imagebase 0x20000000

_start:
    ldr  r1, rcc_apb2enr
    ldr  r0, [r1]
    orr  r0, r0, #0x10
    str  r0, [r1]               ; set IOPCEN in RCC_APB2ENR: the clock of port C on

    ldr  r1, gpioc_crh
    ldr  r0, [r1]
    bic  r0, r0, #0x00f00000
    orr  r0, r0, #0x00200000
    str  r0, [r1]               ; CNF13:MODE13 in GPIOC_CRH = 2: PC13 is an output

blink_loop:
    ldr  r1, gpioc_bsrr
    ldr  r0, led_on
    str  r0, [r1]               ; BR13 in GPIOC_BSRR: PC13 low, LED on
    ldr  r2, delay_count
delay1:
    subs r2, r2, #1
    bne  delay1
    ldr  r1, gpioc_bsrr
    ldr  r0, led_off
    str  r0, [r1]               ; BS13 in GPIOC_BSRR: PC13 high, LED off
    ldr  r2, delay_count
delay2:
    subs r2, r2, #1
    bne  delay2
    b    blink_loop

    align 4
rcc_apb2enr: dd 0x40021018      ; RCC_APB2ENR
gpioc_crh:   dd 0x40011004      ; GPIOC_CRH
gpioc_bsrr:  dd 0x40011010      ; GPIOC_BSRR
led_on:      dd 0x20000000      ; BR13 (bit 29)
led_off:     dd 0x00002000      ; BS13 (bit 13)
delay_count: dd 500000

4.10 Using the RAM

The RAM holds the variables of a program. A program that runs in the RAM can simply have them behind its code: dd 0 reserves a word (4 bytes) with the value 0, dw a halfword (2 bytes), db a byte. adr r0, var1 puts the address of a label into a register:

07_ram_variables.asm download
; ARM assembly tutorial for Asmonite, example 07: variables in the RAM (Raspberry Pi Pico 2, Arm Cortex-M33).
; Adapted from the ARM-ASM-Tutorial by Niklas Guertler (example RAMVariables); see LICENSE.txt.
;
; The program is in the RAM itself, so its variables can simply follow it: "var1" is a word (dd, 4 bytes),
; "var2" a byte (db). "adr" puts the address of a label into a register. Watch the Memory view: set the
; address to var1 and step with F7.
.target pico2
.arch thumb
.imagebase 0x20000000

_start:
    adr  r0, var1               ; r0 = the address of var1
    ldr  r1, value
    str  r1, [r0]               ; var1 = 0x12345678
    ldr  r1, [r0]               ; read var1 back
    and  r1, r1, #0xff          ; keep bits 0..7: 0x78
    adr  r0, var2
    strb r1, [r0]               ; one byte into var2
    b    $

    align 4
value:  dd 0x12345678
var1:   dd 0                    ; 4 bytes
var2:   db 0                    ; 1 byte

Set the memory view to var1 and step: the bytes change with each str. Note the order: the word 0x12345678 is stored as 78 56 34 12, the lowest byte first (little endian). strb stores only the low byte of a register.

A program in the flash cannot do this, because the flash cannot be written like the RAM; its variables need addresses in the RAM of their own (6.19).

5 Memory management

5.1 The address space

The Cortex-M has one address space of 4 GB for everything: code, data and peripherals. The RP2350 uses these parts of it:

AddressWhat
0x00000000the boot ROM (32 KB): the code that starts the chip
0x10000000the flash (4 MB on the Pico 2), read through a cache (XIP: execute in place)
0x20000000 – 0x20081fffthe RAM (520 KB)
0x40000000the peripherals (RESETS, IO_BANK0, PADS_BANK0, CLOCKS, TIMER0 ...)
0xd0000000the SIO
0xe0000000the registers of the core itself (SysTick, NVIC, VTOR ...)

Pico (Cortex-M0+)

The RP2040 has 264 KB of RAM, from 0x20000000 to 0x20041fff, and 2 MB of flash. A flash program starts with 256 bytes of boot2 code that sets up the flash chip; Asmonite writes them for you with .boot2 (see examples\pico\blink_flash.asm).

Blue Pill (STM32F103)

The flash is at 0x08000000 (and mirrored at 0 after a reset), the RAM from 0x20000000 to 0x20004fff (20 KB), the peripherals from 0x40000000.

5.2 The linker, and why Asmonite has none

The GNU tools work in two steps. The assembler turns each source file into an object file whose addresses are still open; the linker then puts the object files together, decides where each part goes in memory, and fills in the addresses. A linker script tells it the memory of the chip and where code, constants and variables go. The original tutorial spends a chapter on it.

Asmonite takes a simpler road: a program is one file, and every byte of the output is written in it, in order. .imagebase says where the first byte goes; each further line follows the one before. So the questions a linker script answers are answered in the source itself:

5.3 Placing code and data

5.3.1 Reserving memory

In a RAM program, variables are reserved where they stand (dd, db, 4.10). In a flash program they need RAM addresses; these are given names with equ (6.19).

5.3.2 Symbols

A linker script defines symbols such as the end of the stack or the start of the data. In Asmonite these are constants (SRAM_END equ 0x20082000, 6.2) or differences of labels, which the assembler works out itself: data_end - data_start is the size of what is between the two labels.

5.3.3 Placing at a fixed address: a program for the flash

Some things must be at a fixed place. When the RP2350 starts from the flash, its boot ROM looks for two things at the start of the flash at 0x10000000:

Since a program in Asmonite is written in order, it is enough to write them first:

08_blink_flash.asm download
; ARM assembly tutorial for Asmonite, example 08: a program for the flash (Raspberry Pi Pico 2, Arm Cortex-M33).
; Adapted from the ARM-ASM-Tutorial by Niklas Guertler (examples LinkerScriptSymbols and
; LinkerScriptAbsolutePlacement); see LICENSE.txt.
;
; 06_blink_registers.asm as a program for the flash at 0x10000000: after a reset or a power cycle the Pico 2
; starts it by itself. F7 writes it into the flash over the debug probe and stops at _start. The file
; begins with two blocks that the boot ROM of the RP2350 reads:
;   - the vector table of the Cortex-M33: the first word is the start value of the stack pointer, the
;     second the address of the first instruction (+1: Thumb code), then the handlers of the exceptions
;   - the IMAGE_DEF block: says that this is a program for the Arm cores (the RP2350 has RISC-V cores too)
.target pico2
.arch thumb
.imagebase 0x10000000

SRAM_END equ 0x20082000         ; the end of the RAM: the stack grows down from here

vectors:
    dd SRAM_END                 ; stack pointer at the start
    dd _start + 1               ; reset: the first instruction (+1: Thumb code)
    dd hang + 1                 ; NMI
    dd hang + 1                 ; HardFault

image_def:
    dd 0xffffded3               ; start marker of the block
    db 0x42, 1                  ; item IMAGE_TYPE, 1 word
    dw 0x1021                   ; executable, Arm, Secure, RP2350
    db 0xff                     ; last item
    dw 1                        ; the size of the items so far, in words
    db 0
    dd 0                        ; no further block
    dd 0xab123579               ; end marker

_start:
    ldr  r1, resets_clr
    ldr  r0, reset_bits
    str  r0, [r1]
    ldr  r1, resets_done
wait_reset:
    ldr  r2, [r1]
    and  r2, r2, r0
    cmp  r2, r0
    bne  wait_reset
    ldr  r1, pad25_clr
    mov  r0, #0x100
    str  r0, [r1]
    ldr  r1, gpio25_ctrl
    movs r0, #5
    str  r0, [r1]
    ldr  r1, sio_oe_set
    ldr  r0, led_bit
    str  r0, [r1]

    ldr  r0, sio_base
    ldr  r1, led_bit
    ldr  r3, delay_count
blink_loop:
    str  r1, [r0, #0x18]        ; LED on
    mov  r4, r3
delay1:
    subs r4, r4, #1
    bne  delay1
    str  r1, [r0, #0x20]        ; LED off
    mov  r4, r3
delay2:
    subs r4, r4, #1
    bne  delay2
    b    blink_loop

hang:
    b    hang                   ; NMI and HardFault end here

    align 4
resets_clr:  dd 0x40023000
reset_bits:  dd 0x00000240
resets_done: dd 0x40020008
pad25_clr:   dd 0x4003b068
gpio25_ctrl: dd 0x400280cc
sio_oe_set:  dd 0xd0000038
sio_base:    dd 0xd0000000
led_bit:     dd 0x02000000
delay_count: dd 1000000

F7 writes it into the flash (only the sectors that changed) and stops at _start; the debugger takes the stack pointer from the vector table. After a reset or a power cycle the program starts by itself. Project > Save as UF2 file makes a file for the BOOTSEL drive from it.

If the chip ran its RISC-V cores before, writing this program switches it to its Arm cores (the output window says so); from then on the RAM examples work too.

Blue Pill (STM32F103)

The STM32 needs no IMAGE_DEF: the vector table at 0x08000000 is all, as in the programs of the original tutorial. Asmonite writes the flash through the flash controller of the chip (only the pages of 1 KB that changed), resets the chip and stops at _start:

bluepill/08_blink_flash.asm download
; ARM assembly tutorial for Asmonite, example 08 for the Blue Pill (STM32F103, Arm Cortex-M3).
; Adapted from the ARM-ASM-Tutorial by Niklas Guertler (examples BlinkDelay and LinkerScriptSymbols); see ..\LICENSE.txt.
;
; 05_blink_delay.asm for the flash at 0x08000000, the way all programs of the original tutorial run: F7
; writes it there (only the pages of 1 KB that differ), resets the chip and stops at _start; after a reset or
; a power cycle the Blue Pill starts it by itself. The STM32 needs no other block than the vector table of the
; Cortex-M3 at the start of the flash: the stack pointer (the end of the 20 KB of RAM), then the start address
; (+1: Thumb code). The original writes 0x20000400 there and reserves 0xe4 bytes for the other entries.
.target stm32f103
.arch thumb
.imagebase 0x08000000

vectors:
    dd 0x20005000               ; the stack pointer at the start: the end of the RAM
    dd _start + 1               ; reset: the first instruction (+1: Thumb code)
    dd hang + 1                 ; NMI
    dd hang + 1                 ; HardFault

_start:
    ldr  r1, rcc_apb2enr
    ldr  r0, [r1]
    orr  r0, r0, #0x10
    str  r0, [r1]               ; set IOPCEN in RCC_APB2ENR: the clock of port C on

    ldr  r1, gpioc_crh
    ldr  r0, [r1]
    bic  r0, r0, #0x00f00000
    orr  r0, r0, #0x00200000
    str  r0, [r1]               ; CNF13:MODE13 in GPIOC_CRH = 2: PC13 is an output

blink_loop:
    ldr  r1, gpioc_bsrr
    ldr  r0, led_on
    str  r0, [r1]               ; BR13 in GPIOC_BSRR: PC13 low, LED on
    ldr  r2, delay_count
delay1:
    subs r2, r2, #1
    bne  delay1
    ldr  r1, gpioc_bsrr
    ldr  r0, led_off
    str  r0, [r1]               ; BS13 in GPIOC_BSRR: PC13 high, LED off
    ldr  r2, delay_count
delay2:
    subs r2, r2, #1
    bne  delay2
    b    blink_loop

hang:
    b    hang

    align 4
rcc_apb2enr: dd 0x40021018      ; RCC_APB2ENR
gpioc_crh:   dd 0x40011004      ; GPIOC_CRH
gpioc_bsrr:  dd 0x40011010      ; GPIOC_BSRR
led_on:      dd 0x20000000      ; BR13 (bit 29)
led_off:     dd 0x00002000      ; BS13 (bit 13)
delay_count: dd 500000

5.4 Program structure

The examples from here on have the same order, which is a good habit:

  1. .target, .arch, .imagebase
  2. the constants (equ): addresses and bits of the peripherals, numbers of the program
  3. for a flash program, or one with interrupts: the vector table (and IMAGE_DEF)
  4. _start and the main program
  5. the functions
  6. tables and the constants for ldr, after align 4
  7. the variables

6 More assembly techniques

6.1 The instruction set state

The bigger Arm cores can run two instruction sets, the 32-bit Arm (A32) and Thumb, and bit 0 of a jump address chooses which one comes next: 1 for Thumb. A Cortex-M runs only Thumb, but the rule stays: an address of code that the core jumps to through a register or a table must have bit 0 set. That is why the vector table holds _start + 1, and why lr after a call shows an odd address. A jump to an even address on a Cortex-M ends in a HardFault. Labels used with b, bl, adr need nothing: the assembler handles them.

6.2 Constants

Numbers like 0x40020000 say nothing to a reader. name equ value gives a number a name; the line writes no bytes. The name can stand wherever a number stands, also in expressions with + - * / << >> & | ^ ~ and brackets:

09_blink_constants.asm download
; ARM assembly tutorial for Asmonite, example 09: names for the numbers (Raspberry Pi Pico 2, Arm Cortex-M33).
; Adapted from the ARM-ASM-Tutorial by Niklas Guertler (example BlinkConstants); see LICENSE.txt.
;
; "name equ value" gives a number a name; the line writes no bytes. The names can be used wherever a number
; stands: in an instruction (#PAD_ISO), in an address ([r0, #SIO_GPIO_OUT_SET]), in dd, and in expressions
; (RESETS_BASE + ALIAS_CLR, 1 << 25). The program is the same as 06_blink_registers.asm.
.target pico2
.arch thumb
.imagebase 0x20000000

RESETS_BASE         equ 0x40020000
RESETS_RESET        equ 0x00
RESETS_RESET_DONE   equ 0x08
RESET_IO_BANK0      equ 1 << 6
RESET_PADS_BANK0    equ 1 << 9
IO_BANK0_BASE       equ 0x40028000
GPIO25_CTRL         equ 0xcc
FUNCSEL_SIO         equ 5
PADS_BANK0_BASE     equ 0x40038000
PADS_GPIO25         equ 0x68
PAD_ISO             equ 1 << 8
SIO_BASE            equ 0xd0000000
SIO_GPIO_OUT_SET    equ 0x18
SIO_GPIO_OUT_CLR    equ 0x20
SIO_GPIO_OE_SET     equ 0x38
ALIAS_CLR           equ 0x3000      ; +0x3000: the alias of a register that clears bits
LED_PIN             equ 25
DELAY_COUNT         equ 1000000

_start:
    ldr  r1, lit_resets_clr
    ldr  r0, lit_reset_bits
    str  r0, [r1, #RESETS_RESET]
    ldr  r1, lit_resets
wait_reset:
    ldr  r2, [r1, #RESETS_RESET_DONE]
    and  r2, r2, r0
    cmp  r2, r0
    bne  wait_reset
    ldr  r1, lit_pads_clr
    mov  r0, #PAD_ISO
    str  r0, [r1, #PADS_GPIO25]
    ldr  r1, lit_io_bank0
    movs r0, #FUNCSEL_SIO
    str  r0, [r1, #GPIO25_CTRL]

    ldr  r0, lit_sio
    ldr  r1, lit_led_bit
    str  r1, [r0, #SIO_GPIO_OE_SET]
    ldr  r3, lit_delay_value
blink_loop:
    str  r1, [r0, #SIO_GPIO_OUT_SET]
    mov  r4, r3
delay1:
    subs r4, r4, #1
    bne  delay1
    str  r1, [r0, #SIO_GPIO_OUT_CLR]
    mov  r4, r3
delay2:
    subs r4, r4, #1
    bne  delay2
    b    blink_loop

    align 4
lit_resets:      dd RESETS_BASE
lit_resets_clr:  dd RESETS_BASE + ALIAS_CLR
lit_reset_bits:  dd RESET_IO_BANK0 | RESET_PADS_BANK0
lit_pads_clr:    dd PADS_BANK0_BASE + ALIAS_CLR
lit_io_bank0:    dd IO_BANK0_BASE
lit_sio:         dd SIO_BASE
lit_led_bit:     dd 1 << LED_PIN
lit_delay_value: dd DELAY_COUNT

Two things to note. Asmonite does not distinguish upper and lower case in names, so DELAY_COUNT and a label delay_count are the same name, and that is an error. The examples therefore start the labels of the constant table with lit_. And the offset in str r0, [r1, #RESETS_RESET] may be 0: that is the same as [r1].

GNU assembler

GNU as writes NAME = value or .equ NAME, value and keeps the case of names.

6.3 The stack

The stack is a region of RAM for values that must be kept for a while: return addresses, saved registers, local variables. sp points to the last value on it, and it grows downwards. push {r4, lr} lowers sp by 8 and stores the two registers there; pop {r4, lr} loads them back and raises sp again. The registers in the braces are always stored in the order of their numbers, the lowest at the lowest address, however they are written.

The stack pointer gets its start value from the first word of the vector table. A RAM program without a vector table gets one from the debugger (the end of the RAM). Watch the stack view while stepping through the next examples.

6.4 Function calls

bl name (branch with link) jumps to a function and puts the address of the next instruction into lr; bx lr jumps back there. Splitting the blink program into functions:

10_functions.asm download
; ARM assembly tutorial for Asmonite, example 10: functions (Raspberry Pi Pico 2, Arm Cortex-M33).
; Adapted from the ARM-ASM-Tutorial by Niklas Guertler (examples OffsetAddressing and BlinkFunctions);
; see LICENSE.txt.
;
; "bl name" (branch with link) jumps to a function and puts the address behind it into lr; "bx lr" jumps
; back there. A function that calls another one must save lr first ("push {lr}") and take it back at the
; end ("pop {pc}" puts it straight into pc: that is the return). The LED blinks 5 times, then the program
; waits in "b $". F8 (Step over) runs a whole call; F7 goes into it.
.target pico2
.arch thumb
.imagebase 0x20000000

RESETS_BASE         equ 0x40020000
RESETS_RESET_DONE   equ 0x08
RESET_BITS          equ (1 << 6) | (1 << 9)     ; IO_BANK0, PADS_BANK0
IO_BANK0_BASE       equ 0x40028000
GPIO25_CTRL         equ 0xcc
FUNCSEL_SIO         equ 5
PADS_BANK0_BASE     equ 0x40038000
PADS_GPIO25         equ 0x68
PAD_ISO             equ 1 << 8
SIO_BASE            equ 0xd0000000
SIO_GPIO_OUT_SET    equ 0x18
SIO_GPIO_OUT_CLR    equ 0x20
SIO_GPIO_OE_SET     equ 0x38
ALIAS_CLR           equ 0x3000
DELAY_COUNT         equ 1000000

_start:
    bl   enable_gpio            ; IO_BANK0 and PADS_BANK0 out of reset
    bl   configure_led          ; GPIO 25: SIO, output
    movs r5, #5                 ; r5 = number of flashes
    bl   blink
    b    $

; ---- blink r5 times; changes r0 .. r5 ----
blink:
    push {lr}                   ; blink calls delay: keep its own return address
    ldr  r0, lit_sio
    ldr  r1, lit_led_bit
    ldr  r3, lit_delay_value
blink_loop:
    str  r1, [r0, #SIO_GPIO_OUT_SET]
    bl   delay
    str  r1, [r0, #SIO_GPIO_OUT_CLR]
    bl   delay
    subs r5, r5, #1
    bne  blink_loop
    pop  {pc}                   ; return

; ---- take IO_BANK0 and PADS_BANK0 out of reset; changes r0 .. r2 ----
enable_gpio:
    ldr  r1, lit_resets_clr
    ldr  r0, lit_reset_bits
    str  r0, [r1]               ; RESETS.RESET through its clear alias
    ldr  r1, lit_resets
enable_wait:
    ldr  r2, [r1, #RESETS_RESET_DONE]
    and  r2, r2, r0
    cmp  r2, r0
    bne  enable_wait
    bx   lr

; ---- GPIO 25: no pad isolation, function SIO, output; changes r0, r1 ----
configure_led:
    ldr  r1, lit_pads_clr
    mov  r0, #PAD_ISO
    str  r0, [r1, #PADS_GPIO25]
    ldr  r1, lit_io_bank0
    movs r0, #FUNCSEL_SIO
    str  r0, [r1, #GPIO25_CTRL]
    ldr  r1, lit_sio
    ldr  r0, lit_led_bit
    str  r0, [r1, #SIO_GPIO_OE_SET]
    bx   lr

; ---- wait: count r3 down in r4; changes r4 ----
delay:
    mov  r4, r3
delay_loop:
    subs r4, r4, #1
    bne  delay_loop
    bx   lr

    align 4
lit_resets:      dd RESETS_BASE
lit_resets_clr:  dd RESETS_BASE + ALIAS_CLR
lit_reset_bits:  dd RESET_BITS
lit_pads_clr:    dd PADS_BANK0_BASE + ALIAS_CLR
lit_io_bank0:    dd IO_BANK0_BASE
lit_sio:         dd SIO_BASE
lit_led_bit:     dd 1 << 25
lit_delay_value: dd DELAY_COUNT

6.4.1 Using the stack for functions

blink calls delay, and that bl overwrites lr, the way back to _start. So blink pushes lr first, and at the end pop {pc} loads the saved address straight into the program counter: that is the return. With F8 (Step over) a whole call runs at once; F7 goes into it.

6.4.2 The calling convention

When functions are written by different people, or by a compiler, they must agree on where arguments and results go and which registers a function may change. For Arm this is the AAPCS (Procedure Call Standard for the Arm Architecture):

11_calling_convention.asm download
; ARM assembly tutorial for Asmonite, example 11: the calling convention (Raspberry Pi Pico 2, Arm Cortex-M33).
; Adapted from the ARM-ASM-Tutorial by Niklas Guertler (examples BlinkFunctionCallingConvention 1 to 3);
; see LICENSE.txt.
;
; The rules of the AAPCS, the convention that compilers for Arm follow too:
;   - the first four arguments go in r0 .. r3, the result comes back in r0
;   - a function may change r0 .. r3 and r12 freely; the caller must not count on them
;   - r4 .. r11 must have their old value at the return: a function that uses them saves them on the
;     stack first ("push {r4, r5, lr}") and takes them back at the end ("pop {r4, r5, pc}")
;   - sp stays aligned to 8 bytes at a call (push and pop an even number of registers)
; blink(count in r0) calls delay(iterations in r0) and keeps its own values in r4 and r5 over the calls.
.target pico2
.arch thumb
.imagebase 0x20000000

RESETS_BASE         equ 0x40020000
RESETS_RESET_DONE   equ 0x08
RESET_BITS          equ (1 << 6) | (1 << 9)     ; IO_BANK0, PADS_BANK0
IO_BANK0_BASE       equ 0x40028000
GPIO25_CTRL         equ 0xcc
FUNCSEL_SIO         equ 5
PADS_BANK0_BASE     equ 0x40038000
PADS_GPIO25         equ 0x68
PAD_ISO             equ 1 << 8
SIO_BASE            equ 0xd0000000
SIO_GPIO_OUT_SET    equ 0x18
SIO_GPIO_OUT_CLR    equ 0x20
SIO_GPIO_OE_SET     equ 0x38
ALIAS_CLR           equ 0x3000
DELAY_COUNT         equ 1000000

_start:
    bl   setup_led
    movs r0, #5                 ; argument: 5 flashes
    bl   blink
    b    $

; ---- blink(r0 = number of flashes) ----
blink:
    push {r4, r5, r6, lr}       ; r4 .. r6 are ours now; 4 registers: sp stays aligned to 8
    mov  r4, r0                 ; r4 = flashes left (survives the calls of delay)
    ldr  r5, lit_sio
    ldr  r6, lit_led_bit
blink_loop:
    str  r6, [r5, #SIO_GPIO_OUT_SET]
    ldr  r0, lit_delay_value    ; argument of delay
    bl   delay                  ; may change r0 .. r3: nothing of ours is there
    str  r6, [r5, #SIO_GPIO_OUT_CLR]
    ldr  r0, lit_delay_value
    bl   delay
    subs r4, r4, #1
    bne  blink_loop
    pop  {r4, r5, r6, pc}       ; old r4 .. r6 back, return

; ---- delay(r0 = iterations) ----
delay:
    subs r0, r0, #1
    bne  delay
    bx   lr

; ---- IO_BANK0 and PADS_BANK0 out of reset, GPIO 25: SIO, output; changes r0 .. r2 ----
setup_led:
    ldr  r1, lit_resets_clr
    ldr  r0, lit_reset_bits
    str  r0, [r1]
    ldr  r1, lit_resets
setup_wait:
    ldr  r2, [r1, #RESETS_RESET_DONE]
    and  r2, r2, r0
    cmp  r2, r0
    bne  setup_wait
    ldr  r1, lit_pads_clr
    mov  r0, #PAD_ISO
    str  r0, [r1, #PADS_GPIO25]
    ldr  r1, lit_io_bank0
    movs r0, #FUNCSEL_SIO
    str  r0, [r1, #GPIO25_CTRL]
    ldr  r1, lit_sio
    ldr  r0, lit_led_bit
    str  r0, [r1, #SIO_GPIO_OE_SET]
    bx   lr

    align 4
lit_resets:      dd RESETS_BASE
lit_resets_clr:  dd RESETS_BASE + ALIAS_CLR
lit_reset_bits:  dd RESET_BITS
lit_pads_clr:    dd PADS_BANK0_BASE + ALIAS_CLR
lit_io_bank0:    dd IO_BANK0_BASE
lit_sio:         dd SIO_BASE
lit_led_bit:     dd 1 << 25
lit_delay_value: dd DELAY_COUNT

Following the convention costs a few pushes, but it pays off: every function can be used everywhere without knowing its inside, and assembly functions can be called from C and the other way round.

Pico (Cortex-M0+)

The convention is the same, but push and pop take only r0 – r7 plus lr or pc, and an offset in str can be at most 124, so GPIO25_CTRL (0xcc) needs its whole address:

pico1/11_calling_convention.asm download
; ARM assembly tutorial for Asmonite, example 11 for the Raspberry Pi Pico (RP2040, Arm Cortex-M0+).
; Adapted from the ARM-ASM-Tutorial by Niklas Guertler (examples BlinkFunctionCallingConvention 1 to 3);
; see ..\LICENSE.txt.
;
; The same program as ..\11_calling_convention.asm; the calling convention is the same on the M0+. What
; is different: "push" and "pop" take only r0 .. r7 and lr (push) or pc (pop); "mov r4, r0" works with
; all registers, but "movs" with a number only with r0 .. r7 (0 .. 255).
.target pico
.arch thumb
.imagebase 0x20000000

RESETS_BASE         equ 0x4000c000
RESETS_RESET_DONE   equ 0x08
RESET_BITS          equ (1 << 5) | (1 << 8)     ; IO_BANK0, PADS_BANK0
IO_BANK0_BASE       equ 0x40014000
GPIO25_CTRL         equ 0xcc
FUNCSEL_SIO         equ 5
SIO_BASE            equ 0xd0000000
SIO_GPIO_OUT_SET    equ 0x14
SIO_GPIO_OUT_CLR    equ 0x18
SIO_GPIO_OE_SET     equ 0x24
ALIAS_CLR           equ 0x3000
DELAY_COUNT         equ 1000000

_start:
    bl   setup_led
    movs r0, #5
    bl   blink
    b    $

; ---- blink(r0 = number of flashes) ----
blink:
    push {r4, r5, r6, lr}
    mov  r4, r0
    ldr  r5, lit_sio
    ldr  r6, lit_led_bit
blink_loop:
    str  r6, [r5, #SIO_GPIO_OUT_SET]
    ldr  r0, lit_delay_value
    bl   delay
    str  r6, [r5, #SIO_GPIO_OUT_CLR]
    ldr  r0, lit_delay_value
    bl   delay
    subs r4, r4, #1
    bne  blink_loop
    pop  {r4, r5, r6, pc}

; ---- delay(r0 = iterations) ----
delay:
    subs r0, r0, #1
    bne  delay
    bx   lr

; ---- IO_BANK0 and PADS_BANK0 out of reset, GPIO 25: SIO, output; changes r0 .. r2 ----
setup_led:
    ldr  r1, lit_resets_clr
    ldr  r0, lit_reset_bits
    str  r0, [r1]
    ldr  r1, lit_resets
setup_wait:
    ldr  r2, [r1, #RESETS_RESET_DONE]
    ands r2, r0
    cmp  r2, r0
    bne  setup_wait
    ldr  r1, lit_gpio25_ctrl      ; the M0+: an offset of at most 124 in str, so the whole address
    movs r0, #FUNCSEL_SIO
    str  r0, [r1]
    ldr  r1, lit_sio
    ldr  r0, lit_led_bit
    str  r0, [r1, #SIO_GPIO_OE_SET]
    bx   lr

    align 4
lit_resets:      dd RESETS_BASE
lit_resets_clr:  dd RESETS_BASE + ALIAS_CLR
lit_reset_bits:  dd RESET_BITS
lit_gpio25_ctrl: dd IO_BANK0_BASE + GPIO25_CTRL
lit_sio:         dd SIO_BASE
lit_led_bit:     dd 1 << 25
lit_delay_value: dd DELAY_COUNT

6.5 Conditional execution

6.5.1 Conditions

The conditions of the branches (4.8) are also the conditions of conditional execution: eq ne hs lo mi pl vs vc hi ls ge lt gt le. Which one to use depends on whether the compared values are unsigned (hs lo hi ls) or signed (ge lt gt le): 0xffffffff is the largest unsigned number but −1 signed.

6.5.2 The IT instruction

A branch around a single instruction is wasteful. Thumb-2 has it (if-then): the next one to four instructions are carried out only if a condition holds. it lt covers one instruction, itt lt two, ite lt one with the condition and one with the opposite (else). Each instruction in the block carries its condition in its name:

14_conditions.asm download
; ARM assembly tutorial for Asmonite, example 14: conditions and IT blocks (Raspberry Pi Pico 2, Arm Cortex-M33).
; Adapted from the ARM-ASM-Tutorial by Niklas Guertler (section "Conditional Execution"); see LICENSE.txt.
;
; "cmp" sets the flags N, Z, C, V; a branch with a condition (beq, bne, blo, bge ...) looks at them. In
; Thumb-2 a few instructions after "it" (if-then) are carried out only if a condition holds: "ite ge"
; = if greater or equal then the next instruction, else the one after it. The conditions of the
; instructions in the block must match the IT ("movge", then "movlt": the opposite).
; Step through with F7 and watch r0 .. r3 and the flags in the register view.
.target pico2
.arch thumb
.imagebase 0x20000000

_start:
    movs r0, #7
    movs r1, #12
    bl   max                    ; r0 = max(7, 12) = 12
    mov  r2, r0

    mvn  r0, #4                 ; r0 = -5 (mvn: all bits inverted, ~4 = -5)
    bl   abs                    ; r0 = 5
    mov  r3, r0

    movs r0, #200
    bl   clamp100               ; r0 = 100
    b    $

; ---- r0 = max(r0, r1), signed ----
max:
    cmp  r0, r1
    it   lt
    movlt r0, r1                ; only if r0 < r1
    bx   lr

; ---- r0 = |r0| ----
abs:
    cmp  r0, #0
    it   lt
    rsblt r0, r0, #0            ; r0 = 0 - r0, only if r0 < 0
    bx   lr

; ---- r0 = r0 if it is below 100, else 100 (unsigned); "ite": then and else ----
clamp100:
    cmp  r0, #100
    ite  hs
    movhs r0, #100              ; r0 >= 100 (unsigned: hs = higher or same)
    addlo r0, r0, #0            ; else: keep r0 (the else part of the example)
    bx   lr

Inside an IT block, movlt and addlo do not set the flags (no s), so all instructions of the block see the same flags.

Pico (Cortex-M0+)

ARMv6-M has no it: a conditional branch jumps around the instruction instead. pico1/18_systick_interrupt.asm shows it.

6.6 8 and 16 bit arithmetic

The registers are 32 bits wide, and arithmetic is always done in 32 bits. Smaller values come in through the loads: ldrb and ldrh load a byte or a halfword and fill the upper bits with zeros, ldrsb and ldrsh with copies of the sign bit. strb and strh store the low 8 or 16 bits, and the rest is cut off. uxtb, uxth, sxtb, sxth do the same extension from register to register.

15_bytes_halfwords.asm download
; ARM assembly tutorial for Asmonite, example 15: 8 and 16 bit values (Raspberry Pi Pico 2, Arm Cortex-M33).
; Adapted from the ARM-ASM-Tutorial by Niklas Guertler (sections "8/16 bit arithmetic", "Alignment",
; "Offset addressing"); see LICENSE.txt.
;
; The registers are always 32 bit. A byte or a halfword is loaded into the low bits: "ldrb"/"ldrh" fill
; the rest with zeros, "ldrsb"/"ldrsh" with copies of the top bit (the sign). "uxtb" and "sxth" do the same
; with a register. "strb"/"strh" store the low 8 or 16 bits. Arithmetic is done in 32 bit; the result is
; cut when it is stored. Step with F7 and look at r0 .. r5 and at the memory from "values".
.target pico2
.arch thumb
.imagebase 0x20000000

_start:
    adr  r6, values
    ldrb  r0, [r6]              ; 0xf0: 240
    ldrsb r1, [r6]              ; 0xf0 as a signed byte: -16 (0xfffffff0)
    ldrh  r2, [r6, #2]          ; 0x8001: 32769
    ldrsh r3, [r6, #2]          ; -32767 (0xffff8001)
    adds  r4, r0, #0x20         ; 240 + 32 = 272 = 0x110: the register keeps 9 bits
    strb  r4, [r6, #4]          ; the byte gets 0x10: the top bit is lost
    uxtb  r5, r4                ; 0x10, the same cut in a register
    sxth  r5, r2                ; the halfword in r2 with its sign: -32767
    strh  r5, [r6, #6]          ; 0x8001 again
    b     $

    align 4
values:
    db 0xf0, 0                  ; offset 0: a byte, then one more to keep the halfword aligned
    dw 0x8001                   ; offset 2: a halfword (at an even address)
    db 0, 0                     ; offset 4: strb writes here
    dw 0                        ; offset 6: strh writes here

6.7 Alignment

A word should be at an address that is a multiple of 4, a halfword at an even address. The Cortex-M33 can read and write unaligned words in the RAM, more slowly; the M0+ cannot at all (it raises a HardFault), and neither can the instructions that load several registers or the literal loads. So after bytes or text, align 4 fills with zeros up to the next multiple of 4 before the next word. Instructions themselves are always at even addresses.

6.8 Offset addressing

The address in a load or store can have more parts:

[r0, #8]r0 + 8; r0 stays (offset)
[r0, r1]r0 + r1
[r0, r1, lsl #2]r0 + r1 × 4: element number r1 of an array of words
[r0, #4]!r0 + 4, and r0 becomes that address (pre-index)
[r0], #4r0; afterwards r0 = r0 + 4 (post-index)

6.9 Iterating arrays

Arrays are rows of values in memory. The blink pattern SOS (short, short, short, long, long, long, short ...) is a table of on and off times, read with an index:

12_blink_pattern.asm download
; ARM assembly tutorial for Asmonite, example 12: a table of words (Raspberry Pi Pico 2, Arm Cortex-M33).
; Adapted from the ARM-ASM-Tutorial by Niklas Guertler (examples BlinkPattern and BlinkPattern2);
; see LICENSE.txt.
;
; The lengths of the on and off times come from a table: three short, three long, three short (SOS).
; "ldr r0, [r7, r8, lsl #2]" reads the word number r8 of the table at r7: the address is r7 + r8 * 4.
; "cmp" and "blo" (branch if lower, unsigned) end the loop after the last entry. The entry count is the
; size of the table divided by 4: (blink_table_end - blink_table) / 4, worked out by the assembler.
.target pico2
.arch thumb
.imagebase 0x20000000

RESETS_BASE         equ 0x40020000
RESETS_RESET_DONE   equ 0x08
RESET_BITS          equ (1 << 6) | (1 << 9)     ; IO_BANK0, PADS_BANK0
IO_BANK0_BASE       equ 0x40028000
GPIO25_CTRL         equ 0xcc
FUNCSEL_SIO         equ 5
PADS_BANK0_BASE     equ 0x40038000
PADS_GPIO25         equ 0x68
PAD_ISO             equ 1 << 8
SIO_BASE            equ 0xd0000000
SIO_GPIO_OUT_SET    equ 0x18
SIO_GPIO_OUT_CLR    equ 0x20
SIO_GPIO_OE_SET     equ 0x38
ALIAS_CLR           equ 0x3000

_start:
    bl   setup_led
    bl   blink_pattern
    b    $

; ---- blink once through the table ----
blink_pattern:
    push {r4, r5, r6, r7, r8, r9, r10, lr}
    ldr  r4, lit_sio
    ldr  r5, lit_led_bit
    adr  r7, blink_table        ; r7 = the address of the table
    movs r8, #0                 ; r8 = the number of the entry
    movs r9, #(blink_table_end - blink_table) / 4
pattern_loop:
    str  r5, [r4, #SIO_GPIO_OUT_SET]
    ldr  r0, [r7, r8, lsl #2]   ; the on time
    add  r8, r8, #1
    bl   delay
    str  r5, [r4, #SIO_GPIO_OUT_CLR]
    ldr  r0, [r7, r8, lsl #2]   ; the off time
    add  r8, r8, #1
    bl   delay
    cmp  r8, r9
    blo  pattern_loop
    pop  {r4, r5, r6, r7, r8, r9, r10, pc}

delay:
    subs r0, r0, #1
    bne  delay
    bx   lr

; ---- IO_BANK0 and PADS_BANK0 out of reset, GPIO 25: SIO, output; changes r0 .. r2 ----
setup_led:
    ldr  r1, lit_resets_clr
    ldr  r0, lit_reset_bits
    str  r0, [r1]
    ldr  r1, lit_resets
setup_wait:
    ldr  r2, [r1, #RESETS_RESET_DONE]
    and  r2, r2, r0
    cmp  r2, r0
    bne  setup_wait
    ldr  r1, lit_pads_clr
    mov  r0, #PAD_ISO
    str  r0, [r1, #PADS_GPIO25]
    ldr  r1, lit_io_bank0
    movs r0, #FUNCSEL_SIO
    str  r0, [r1, #GPIO25_CTRL]
    ldr  r1, lit_sio
    ldr  r0, lit_led_bit
    str  r0, [r1, #SIO_GPIO_OE_SET]
    bx   lr

    align 4
blink_table:
    dd 1000000, 1000000, 1000000, 1000000, 1000000, 1000000
    dd 2500000, 1000000, 2500000, 1000000, 2500000, 1000000
    dd 1000000, 1000000, 1000000, 1000000, 1000000, 4000000
blink_table_end:

lit_resets:      dd RESETS_BASE
lit_resets_clr:  dd RESETS_BASE + ALIAS_CLR
lit_reset_bits:  dd RESET_BITS
lit_pads_clr:    dd PADS_BANK0_BASE + ALIAS_CLR
lit_io_bank0:    dd IO_BANK0_BASE
lit_sio:         dd SIO_BASE
lit_led_bit:     dd 1 << 25

The number of entries is worked out by the assembler from two labels. With bytes instead of words the table is a quarter of the size, and a post-index load walks through it:

13_blink_bytes.asm download
; ARM assembly tutorial for Asmonite, example 13: a table of bytes (Raspberry Pi Pico 2, Arm Cortex-M33).
; Adapted from the ARM-ASM-Tutorial by Niklas Guertler (example BlinkPattern2 and the sections on 8-bit
; values, alignment and arrays); see LICENSE.txt.
;
; The table holds bytes now: each one is a multiple of 500000 iterations, so 2 is short and 5 is long.
; "ldrb r0, [r7], #1" loads one byte from the address in r7 and then adds 1 to r7 (post-index): r7 walks
; through the table. The loop ends when r7 reaches blink_table_end. "mul" multiplies.
; After a table of bytes the next word must start at an address that is a multiple of 4: "align 4".
.target pico2
.arch thumb
.imagebase 0x20000000

RESETS_BASE         equ 0x40020000
RESETS_RESET_DONE   equ 0x08
RESET_BITS          equ (1 << 6) | (1 << 9)     ; IO_BANK0, PADS_BANK0
IO_BANK0_BASE       equ 0x40028000
GPIO25_CTRL         equ 0xcc
FUNCSEL_SIO         equ 5
PADS_BANK0_BASE     equ 0x40038000
PADS_GPIO25         equ 0x68
PAD_ISO             equ 1 << 8
SIO_BASE            equ 0xd0000000
SIO_GPIO_OUT_SET    equ 0x18
SIO_GPIO_OUT_CLR    equ 0x20
SIO_GPIO_OE_SET     equ 0x38
ALIAS_CLR           equ 0x3000
UNIT                equ 500000

_start:
    bl   setup_led
    bl   blink_pattern
    b    $

blink_pattern:
    push {r4, r5, r6, r7, r8, lr}
    ldr  r4, lit_sio
    ldr  r5, lit_led_bit
    ldr  r6, lit_unit
    adr  r7, blink_table
    adr  r8, blink_table_end
pattern_loop:
    str  r5, [r4, #SIO_GPIO_OUT_SET]
    ldrb r0, [r7], #1           ; the on time in units, r7 = r7 + 1
    mul  r0, r0, r6
    bl   delay
    str  r5, [r4, #SIO_GPIO_OUT_CLR]
    ldrb r0, [r7], #1           ; the off time
    mul  r0, r0, r6
    bl   delay
    cmp  r7, r8
    blo  pattern_loop
    pop  {r4, r5, r6, r7, r8, pc}

delay:
    subs r0, r0, #1
    bne  delay
    bx   lr

; ---- IO_BANK0 and PADS_BANK0 out of reset, GPIO 25: SIO, output; changes r0 .. r2 ----
setup_led:
    ldr  r1, lit_resets_clr
    ldr  r0, lit_reset_bits
    str  r0, [r1]
    ldr  r1, lit_resets
setup_wait:
    ldr  r2, [r1, #RESETS_RESET_DONE]
    and  r2, r2, r0
    cmp  r2, r0
    bne  setup_wait
    ldr  r1, lit_pads_clr
    mov  r0, #PAD_ISO
    str  r0, [r1, #PADS_GPIO25]
    ldr  r1, lit_io_bank0
    movs r0, #FUNCSEL_SIO
    str  r0, [r1, #GPIO25_CTRL]
    ldr  r1, lit_sio
    ldr  r0, lit_led_bit
    str  r0, [r1, #SIO_GPIO_OE_SET]
    bx   lr

    align 4
blink_table:
    db 2, 2, 2, 2, 2, 2
    db 5, 2, 5, 2, 5, 2
    db 2, 2, 2, 2, 2, 8
blink_table_end:
    align 4
lit_resets:      dd RESETS_BASE
lit_resets_clr:  dd RESETS_BASE + ALIAS_CLR
lit_reset_bits:  dd RESET_BITS
lit_pads_clr:    dd PADS_BANK0_BASE + ALIAS_CLR
lit_io_bank0:    dd IO_BANK0_BASE
lit_sio:         dd SIO_BASE
lit_led_bit:     dd 1 << 25
lit_unit:        dd UNIT

6.10 Literal loads

An instruction is 2 or 4 bytes long; it cannot hold an arbitrary 32-bit number. There are four ways around this:

6.10.1 The mov instruction

movs r0, #200 loads 0 to 255. The 32-bit mov takes more: any 8-bit value shifted to any position (0x00ab0000, 0x3fc), or one of the patterns 0x00XY00XY, 0xXY00XY00, 0xXYXYXYXY; mvn loads the inverse of such a value.

6.10.2 The movw and movt instructions

movw r0, #0x5678 loads any 16-bit number (and clears the upper half), movt r0, #0x1234 sets the upper half and keeps the lower one. Two instructions, 8 bytes, any number, no memory access.

6.10.3 PC-relative loads

ldr r0, name loads the word at the label name. The instruction holds only the distance from the program counter to the word, so the constant must be near: up to 1020 bytes ahead for the 16-bit form, ±4095 bytes for the 32-bit one. That is what all examples here do. adr r0, name loads the address of the label instead of the word there.

16_constants.asm download
; ARM assembly tutorial for Asmonite, example 16: ways to load a constant (Raspberry Pi Pico 2, Arm Cortex-M33).
; Adapted from the ARM-ASM-Tutorial by Niklas Guertler (section "Literal loads"); see LICENSE.txt.
;
; An instruction is 2 or 4 bytes long, so a 32-bit constant does not fit into it. Four ways:
;   1. "mov"/"movs" with a small number, or a number of the form "8 bits, shifted" (0x00ab0000,
;      0xab00ab00, 0xabababab ...): one instruction
;   2. "movw" (the low 16 bits) and "movt" (the high 16 bits): two instructions, any number
;   3. "ldr r0, name": the word at the label name, relative to the pc; the constant stands behind the code
;      (GNU as writes this for "ldr r0, =value" and collects the constants itself; in Asmonite they are
;      written out)
;   4. "adr r0, name": not a value but the address of a label, relative to the pc
.target pico2
.arch thumb
.imagebase 0x20000000

_start:
    movs r0, #200               ; 1. 0 .. 255
    mov  r1, #0x00ab0000        ; 1. 8 bits, shifted
    mov  r2, #0xab00ab00        ; 1. a pattern of bytes
    movw r3, #0x5678            ; 2. low half, the high half becomes 0
    movt r3, #0x1234            ; 2. high half: r3 = 0x12345678
    ldr  r4, lit_big            ; 3. from the word behind the code
    adr  r5, lit_big            ; 4. the address of that word
    b    $

    align 4
lit_big: dd 0xdeadbeef

GNU assembler

GNU as has a shortcut for this: ldr r0, =0x40021018 puts the number into a literal pool by itself and loads it from there; .ltorg says where the pool goes. Asmonite has no such shortcut: the constants are written out, with their labels, as every other byte of the program. It is more to type, but you see exactly what is in memory.

6.11 The SysTick timer

A counting loop wastes the core, and its time changes with the clock and the code around it. Every Cortex-M has a simple timer for this, the SysTick: a 24-bit counter that counts down with the clock of the core and starts again from a reload value. Each time it passes zero, it sets a flag (COUNTFLAG, bit 16 of SYST_CSR), which reading the register clears. Its three registers are in the System Control Space of the core at 0xe000e010:

17_systick.asm download
; ARM assembly tutorial for Asmonite, example 17: wait with the SysTick timer (Raspberry Pi Pico 2, Arm Cortex-M33).
; Adapted from the ARM-ASM-Tutorial by Niklas Guertler (example BlinkSysTick); see LICENSE.txt.
;
; Every Cortex-M has the SysTick: a 24-bit counter that counts down with the clock of the core and starts
; again at the reload value. Each time it passes 0 it sets the bit COUNTFLAG (bit 16 of SYST_CSR); reading
; SYST_CSR clears it. The waiting does not depend on how fast the loop is any more: wait_systick waits for
; r0 of these events. The pattern comes from a table of bytes, as in 13_blink_bytes.asm.
;   SYST_CSR (0xe000e010): bit 0 ENABLE, bit 1 TICKINT (interrupt, see 18), bit 2 CLKSOURCE (1: the core clock)
;   SYST_RVR (0xe000e014): the reload value; SYST_CVR (0xe000e018): the counter (writing clears it)
.target pico2
.arch thumb
.imagebase 0x20000000

RESETS_BASE         equ 0x40020000
RESETS_RESET_DONE   equ 0x08
RESET_BITS          equ (1 << 6) | (1 << 9)     ; IO_BANK0, PADS_BANK0
IO_BANK0_BASE       equ 0x40028000
GPIO25_CTRL         equ 0xcc
FUNCSEL_SIO         equ 5
PADS_BANK0_BASE     equ 0x40038000
PADS_GPIO25         equ 0x68
PAD_ISO             equ 1 << 8
SIO_BASE            equ 0xd0000000
SIO_GPIO_OUT_SET    equ 0x18
SIO_GPIO_OUT_CLR    equ 0x20
SIO_GPIO_OE_SET     equ 0x38
ALIAS_CLR           equ 0x3000
SCS_BASE            equ 0xe000e000      ; the System Control Space of the core
SYST_CSR            equ 0x10
SYST_RVR            equ 0x14
SYST_CVR            equ 0x18
COUNTFLAG           equ 1 << 16
TIMER_VALUE         equ 1500000         ; clock cycles between two events

_start:
    bl   setup_led
    ldr  r0, lit_timer_value
    bl   start_systick
    bl   blink_pattern
    b    $

blink_pattern:
    push {r4, r5, r6, r7, r8, lr}
    ldr  r4, lit_sio
    ldr  r5, lit_led_bit
    adr  r7, blink_table
    adr  r8, blink_table_end
pattern_loop:
    str  r5, [r4, #SIO_GPIO_OUT_SET]
    ldrb r0, [r7], #1
    bl   wait_systick
    str  r5, [r4, #SIO_GPIO_OUT_CLR]
    ldrb r0, [r7], #1
    bl   wait_systick
    cmp  r7, r8
    blo  pattern_loop
    pop  {r4, r5, r6, r7, r8, pc}

; ---- start the SysTick: r0 = cycles between two events ----
start_systick:
    ldr  r1, lit_scs
    str  r0, [r1, #SYST_RVR]
    movs r0, #0
    str  r0, [r1, #SYST_CVR]    ; start from the reload value
    movs r0, #5                 ; ENABLE | CLKSOURCE, no interrupt
    str  r0, [r1, #SYST_CSR]
    bx   lr

; ---- wait for r0 events of the SysTick ----
wait_systick:
    ldr  r1, lit_scs
wait_loop:
    ldr  r2, [r1, #SYST_CSR]    ; reading clears COUNTFLAG
    tst  r2, #COUNTFLAG
    beq  wait_loop
    subs r0, r0, #1
    bne  wait_loop
    bx   lr

; ---- IO_BANK0 and PADS_BANK0 out of reset, GPIO 25: SIO, output; changes r0 .. r2 ----
setup_led:
    ldr  r1, lit_resets_clr
    ldr  r0, lit_reset_bits
    str  r0, [r1]
    ldr  r1, lit_resets
setup_wait:
    ldr  r2, [r1, #RESETS_RESET_DONE]
    and  r2, r2, r0
    cmp  r2, r0
    bne  setup_wait
    ldr  r1, lit_pads_clr
    mov  r0, #PAD_ISO
    str  r0, [r1, #PADS_GPIO25]
    ldr  r1, lit_io_bank0
    movs r0, #FUNCSEL_SIO
    str  r0, [r1, #GPIO25_CTRL]
    ldr  r1, lit_sio
    ldr  r0, lit_led_bit
    str  r0, [r1, #SIO_GPIO_OE_SET]
    bx   lr

    align 4
blink_table:
    db 2, 2, 2, 2, 2, 2
    db 5, 2, 5, 2, 5, 2
    db 2, 2, 2, 2, 2, 8
blink_table_end:
    align 4
lit_resets:      dd RESETS_BASE
lit_resets_clr:  dd RESETS_BASE + ALIAS_CLR
lit_reset_bits:  dd RESET_BITS
lit_pads_clr:    dd PADS_BANK0_BASE + ALIAS_CLR
lit_io_bank0:    dd IO_BANK0_BASE
lit_sio:         dd SIO_BASE
lit_led_bit:     dd 1 << 25
lit_scs:         dd SCS_BASE
lit_timer_value: dd TIMER_VALUE

6.12 Exceptions and interrupts

Polling a flag still keeps the core busy. With an interrupt the hardware calls a function by itself when something happens, and the core can sleep in between (wfi: wait for interrupt). On a Cortex-M, interrupts are one kind of exception; the others are the reset, faults (HardFault and the like) and the system calls.

When an exception happens, the core pushes r0 – r3, r12, lr, pc and xPSR on the stack itself, puts a special value into lr, and jumps to the address in the vector table: entry 15 for the SysTick, entry 16 + n for interrupt n of the peripherals. The handler is an ordinary function following the calling convention, and bx lr with that special value makes the core restore the saved registers and continue where it was interrupted.

The core finds the vector table through the register VTOR (0xe000ed08). A program in the RAM writes the address of its own table there. And before it sleeps, it allows interrupts with cpsie i (it clears PRIMASK, the switch that blocks them all): after a reset they are allowed, but the boot ROM may leave them blocked, for instance after it wrote the flash.

18_systick_interrupt.asm download
; ARM assembly tutorial for Asmonite, example 18: the SysTick interrupt (Raspberry Pi Pico 2, Arm Cortex-M33).
; Adapted from the ARM-ASM-Tutorial by Niklas Guertler (example BlinkSysTickInterrupt, sections
; "Exceptions & Interrupts" and "Improved vector table"); see LICENSE.txt.
;
; Now the core does not wait at all: it sleeps in "wfi" (wait for interrupt), and the SysTick calls the
; function systick_handler by itself at each event (TICKINT, bit 1 of SYST_CSR). The core finds the
; handler in the vector table: entry 15 is the SysTick. The table is at the start of the program, in the
; RAM at 0x20000000; the program writes this address into VTOR (0xe000ed08), the register that tells the
; core where its table is. An exception saves r0 .. r3, r12, lr, pc and xPSR on the stack by itself, so the
; handler is an ordinary function that ends with "bx lr".
; The state lives in two bytes in the RAM: blink_step (the entry of the table) and timer_events (events
; left for this entry). Set a breakpoint in systick_handler (F5) and press F9.
.target pico2
.arch thumb
.imagebase 0x20000000

RESETS_BASE         equ 0x40020000
RESETS_RESET_DONE   equ 0x08
RESET_BITS          equ (1 << 6) | (1 << 9)     ; IO_BANK0, PADS_BANK0
IO_BANK0_BASE       equ 0x40028000
GPIO25_CTRL         equ 0xcc
FUNCSEL_SIO         equ 5
PADS_BANK0_BASE     equ 0x40038000
PADS_GPIO25         equ 0x68
PAD_ISO             equ 1 << 8
SIO_BASE            equ 0xd0000000
SIO_GPIO_OUT_SET    equ 0x18
SIO_GPIO_OUT_CLR    equ 0x20
SIO_GPIO_OE_SET     equ 0x38
ALIAS_CLR           equ 0x3000
SCS_BASE            equ 0xe000e000
SYST_CSR            equ 0x10
SYST_RVR            equ 0x14
SYST_CVR            equ 0x18
SCB_VTOR            equ 0xd08
COUNTFLAG           equ 1 << 16
TIMER_VALUE         equ 1500000
SRAM_END            equ 0x20082000

vectors:
    dd SRAM_END                 ;  0: the stack pointer at the start
    dd _start + 1               ;  1: reset
    dd hang + 1                 ;  2: NMI
    dd hang + 1                 ;  3: HardFault
    dd hang + 1                 ;  4: MemManage
    dd hang + 1                 ;  5: BusFault
    dd hang + 1                 ;  6: UsageFault
    dd hang + 1                 ;  7: SecureFault
    dd 0, 0, 0                  ;  8 .. 10: reserved
    dd hang + 1                 ; 11: SVCall
    dd hang + 1                 ; 12: DebugMonitor
    dd 0                        ; 13: reserved
    dd hang + 1                 ; 14: PendSV
    dd systick_handler + 1      ; 15: SysTick

_start:
    ldr  r0, lit_scs
    adr  r1, vectors
    str  r1, [r0, #SCB_VTOR]    ; the vector table is ours
    adr  r0, blink_step
    movs r1, #0
    strb r1, [r0]               ; blink_step = 0
    adr  r2, blink_table
    ldrb r1, [r2]               ; the first entry of the table
    adr  r0, timer_events
    strb r1, [r0]
    bl   setup_led
    ldr  r0, lit_sio
    ldr  r1, lit_led_bit
    str  r1, [r0, #SIO_GPIO_OUT_SET]   ; the first step: LED on
    ldr  r0, lit_timer_value
    bl   start_systick
    cpsie i                     ; allow interrupts (PRIMASK = 0): the boot ROM may leave them off
sleep_loop:
    wfi                         ; sleep until an interrupt
    b    sleep_loop

; ---- called by the SysTick at every event ----
systick_handler:
    adr  r0, timer_events
    ldrb r3, [r0]
    subs r3, r3, #1             ; one event less
    itt  ne
    strbne r3, [r0]             ; not yet 0: store it
    bne  handler_done           ; and return
    adr  r0, blink_step
    ldrb r1, [r0]
    adds r1, r1, #1             ; the next entry
    cmp  r1, #(blink_table_end - blink_table)
    it   hs
    movhs r1, #0                ; after the last one: from the start again
    strb r1, [r0]
    adr  r2, blink_table
    ldrb r3, [r2, r1]           ; its number of events
    adr  r0, timer_events
    strb r3, [r0]
    ldr  r0, lit_sio
    ldr  r2, lit_led_bit
    tst  r1, #1                 ; even entry: on, odd entry: off
    ite  eq
    streq r2, [r0, #SIO_GPIO_OUT_SET]
    strne r2, [r0, #SIO_GPIO_OUT_CLR]
handler_done:
    bx   lr

; ---- start the SysTick with its interrupt: r0 = cycles between two events ----
start_systick:
    ldr  r1, lit_scs
    str  r0, [r1, #SYST_RVR]
    movs r0, #0
    str  r0, [r1, #SYST_CVR]
    movs r0, #7                 ; ENABLE | TICKINT | CLKSOURCE
    str  r0, [r1, #SYST_CSR]
    bx   lr

hang:
    b    hang

; ---- IO_BANK0 and PADS_BANK0 out of reset, GPIO 25: SIO, output; changes r0 .. r2 ----
setup_led:
    ldr  r1, lit_resets_clr
    ldr  r0, lit_reset_bits
    str  r0, [r1]
    ldr  r1, lit_resets
setup_wait:
    ldr  r2, [r1, #RESETS_RESET_DONE]
    and  r2, r2, r0
    cmp  r2, r0
    bne  setup_wait
    ldr  r1, lit_pads_clr
    mov  r0, #PAD_ISO
    str  r0, [r1, #PADS_GPIO25]
    ldr  r1, lit_io_bank0
    movs r0, #FUNCSEL_SIO
    str  r0, [r1, #GPIO25_CTRL]
    ldr  r1, lit_sio
    ldr  r0, lit_led_bit
    str  r0, [r1, #SIO_GPIO_OE_SET]
    bx   lr

blink_table:
    db 2, 2, 2, 2, 2, 2
    db 5, 2, 5, 2, 5, 2
    db 2, 2, 2, 2, 2, 8
blink_table_end:
blink_step:   db 0
timer_events: db 0
    align 4
lit_resets:      dd RESETS_BASE
lit_resets_clr:  dd RESETS_BASE + ALIAS_CLR
lit_reset_bits:  dd RESET_BITS
lit_pads_clr:    dd PADS_BANK0_BASE + ALIAS_CLR
lit_io_bank0:    dd IO_BANK0_BASE
lit_sio:         dd SIO_BASE
lit_led_bit:     dd 1 << 25
lit_scs:         dd SCS_BASE
lit_timer_value: dd TIMER_VALUE

Set a breakpoint in systick_handler with F5 and run with F9: the program stops there at every event, and the stack view shows the eight registers the core pushed.

Pico (Cortex-M0+)

The same on the RP2040: without IT blocks, without post-index loads, and with fewer entries in the vector table. adr works only forwards and on addresses that are multiples of 4 there:

pico1/18_systick_interrupt.asm download
; ARM assembly tutorial for Asmonite, example 18 for the Raspberry Pi Pico (RP2040, Arm Cortex-M0+).
; Adapted from the ARM-ASM-Tutorial by Niklas Guertler (example BlinkSysTickInterrupt); see ..\LICENSE.txt.
;
; The same program as ..\18_systick_interrupt.asm. What is different on the M0+:
;   - no IT blocks: a condition is a branch around the instruction ("bne handler_done")
;   - no "ldrb r1, [r0], #1" (post-index) and no "tst r1, #1" with a number: a register holds the 1
;   - the vector table of the M0+ has fewer entries (no MemManage, BusFault, UsageFault, SecureFault,
;     DebugMonitor); the SysTick is entry 15 as well
.target pico
.arch thumb
.imagebase 0x20000000

RESETS_BASE         equ 0x4000c000
RESETS_RESET_DONE   equ 0x08
RESET_BITS          equ (1 << 5) | (1 << 8)
IO_BANK0_BASE       equ 0x40014000
GPIO25_CTRL         equ 0xcc
FUNCSEL_SIO         equ 5
SIO_BASE            equ 0xd0000000
SIO_GPIO_OUT_SET    equ 0x14
SIO_GPIO_OUT_CLR    equ 0x18
SIO_GPIO_OE_SET     equ 0x24
ALIAS_CLR           equ 0x3000
SCS_BASE            equ 0xe000e000
SYST_CSR            equ 0x10
SYST_RVR            equ 0x14
SYST_CVR            equ 0x18
TIMER_VALUE         equ 1500000
SRAM_END            equ 0x20042000

vectors:
    dd SRAM_END                 ;  0: the stack pointer at the start
    dd _start + 1               ;  1: reset
    dd hang + 1                 ;  2: NMI
    dd hang + 1                 ;  3: HardFault
    dd 0, 0, 0, 0, 0, 0, 0      ;  4 .. 10: reserved on the M0+
    dd hang + 1                 ; 11: SVCall
    dd 0, 0                     ; 12, 13: reserved
    dd hang + 1                 ; 14: PendSV
    dd systick_handler + 1      ; 15: SysTick

_start:
    ldr  r0, lit_vtor
    ldr  r1, lit_vectors        ; the M0+: adr only forwards, so the address from a word
    str  r1, [r0]               ; VTOR (0xe000ed08): our vector table
    adr  r0, blink_step
    movs r1, #0
    strb r1, [r0]
    adr  r2, blink_table
    ldrb r1, [r2]
    adr  r0, timer_events
    strb r1, [r0]
    bl   setup_led
    ldr  r0, lit_sio
    ldr  r1, lit_led_bit
    str  r1, [r0, #SIO_GPIO_OUT_SET]
    ldr  r0, lit_timer_value
    bl   start_systick
    cpsie i                     ; allow interrupts (PRIMASK = 0): the boot ROM may leave them off
sleep_loop:
    wfi
    b    sleep_loop

systick_handler:
    adr  r0, timer_events
    ldrb r3, [r0]
    subs r3, r3, #1
    beq  next_step              ; 0: the next entry of the table
    strb r3, [r0]               ; else store the rest and return
    bx   lr
next_step:
    adr  r0, blink_step
    ldrb r1, [r0]
    adds r1, r1, #1
    cmp  r1, #(blink_table_end - blink_table)
    blo  step_ok
    movs r1, #0
step_ok:
    strb r1, [r0]
    adr  r2, blink_table
    ldrb r3, [r2, r1]
    adr  r0, timer_events
    strb r3, [r0]
    ldr  r0, lit_sio
    ldr  r2, lit_led_bit
    movs r3, #1
    tst  r1, r3                 ; even entry: on, odd entry: off
    bne  led_off
    str  r2, [r0, #SIO_GPIO_OUT_SET]
    bx   lr
led_off:
    str  r2, [r0, #SIO_GPIO_OUT_CLR]
    bx   lr

start_systick:
    ldr  r1, lit_scs
    str  r0, [r1, #SYST_RVR]
    movs r0, #0
    str  r0, [r1, #SYST_CVR]
    movs r0, #7
    str  r0, [r1, #SYST_CSR]
    bx   lr

hang:
    b    hang

setup_led:
    ldr  r1, lit_resets_clr
    ldr  r0, lit_reset_bits
    str  r0, [r1]
    ldr  r1, lit_resets
setup_wait:
    ldr  r2, [r1, #RESETS_RESET_DONE]
    ands r2, r0
    cmp  r2, r0
    bne  setup_wait
    ldr  r1, lit_gpio25_ctrl      ; the M0+: an offset of at most 124 in str, so the whole address
    movs r0, #FUNCSEL_SIO
    str  r0, [r1]
    ldr  r1, lit_sio
    ldr  r0, lit_led_bit
    str  r0, [r1, #SIO_GPIO_OE_SET]
    bx   lr

    align 4                     ; the M0+: adr wants an address that is a multiple of 4
blink_table:
    db 2, 2, 2, 2, 2, 2
    db 5, 2, 5, 2, 5, 2
    db 2, 2, 2, 2, 2, 8
blink_table_end:
    align 4
blink_step:   db 0
    align 4
timer_events: db 0
    align 4
lit_resets:      dd RESETS_BASE
lit_resets_clr:  dd RESETS_BASE + ALIAS_CLR
lit_reset_bits:  dd RESET_BITS
lit_gpio25_ctrl: dd IO_BANK0_BASE + GPIO25_CTRL
lit_sio:         dd SIO_BASE
lit_led_bit:     dd 1 << 25
lit_scs:         dd SCS_BASE
lit_vtor:        dd 0xe000ed08
lit_vectors:     dd vectors
lit_timer_value: dd TIMER_VALUE

Blue Pill (STM32F103)

The Cortex-M3 has the same SysTick, vector table, VTOR and IT blocks; the example BlinkSysTickInterrupt of the original tutorial with its ite eq / ldreq / ldrne, only the LED switched at PC13. While the core sleeps in wfi, the debugger can still reach it: Asmonite sets the bits of DBGMCU_CR that keep the debug port alive in the sleep modes.

bluepill/18_systick_interrupt.asm download
; ARM assembly tutorial for Asmonite, example 18 for the Blue Pill (STM32F103, Arm Cortex-M3).
; Adapted from the ARM-ASM-Tutorial by Niklas Guertler (example BlinkSysTickInterrupt); see ..\LICENSE.txt.
;
; The SysTick interrupt example on the chip of the original tutorial. The SysTick, the vector table, VTOR and
; the IT blocks are the same as on the Pico 2 (the Cortex-M3 and the Cortex-M33 share them); only the LED is
; switched the way of the STM32: GPIOC_BSRR, bit 29 clears PC13 (LED on), bit 13 sets it (LED off).
; The program runs in the RAM; the core runs from the internal 8 MHz oscillator (HSI) after a reset, so an
; event of the SysTick (1500000 cycles) is about 0.19 s.
.target stm32f103
.arch thumb
.imagebase 0x20000000

RCC_APB2ENR         equ 0x40021018
IOPCEN              equ 1 << 4
GPIOC_BASE          equ 0x40011000
GPIO_CRH            equ 0x04
GPIO_BSRR           equ 0x10
LED_ON              equ 1 << 29         ; BR13: PC13 low
LED_OFF             equ 1 << 13         ; BS13: PC13 high
SCS_BASE            equ 0xe000e000
SYST_CSR            equ 0x10
SYST_RVR            equ 0x14
SYST_CVR            equ 0x18
SCB_VTOR            equ 0xd08
TIMER_VALUE         equ 1500000
SRAM_END            equ 0x20005000

vectors:
    dd SRAM_END                 ;  0: the stack pointer at the start
    dd _start + 1               ;  1: reset
    dd hang + 1                 ;  2: NMI
    dd hang + 1                 ;  3: HardFault
    dd hang + 1                 ;  4: MemManage
    dd hang + 1                 ;  5: BusFault
    dd hang + 1                 ;  6: UsageFault
    dd 0, 0, 0, 0               ;  7 .. 10: reserved
    dd hang + 1                 ; 11: SVCall
    dd hang + 1                 ; 12: DebugMonitor
    dd 0                        ; 13: reserved
    dd hang + 1                 ; 14: PendSV
    dd systick_handler + 1      ; 15: SysTick

_start:
    ldr  r0, lit_scs
    adr  r1, vectors
    str  r1, [r0, #SCB_VTOR]    ; the vector table is ours
    adr  r0, blink_step
    movs r1, #0
    strb r1, [r0]               ; blink_step = 0
    adr  r2, blink_table
    ldrb r1, [r2]
    adr  r0, timer_events
    strb r1, [r0]               ; timer_events = the first entry of the table
    bl   setup_led
    ldr  r0, lit_gpioc
    ldr  r1, lit_led_on
    str  r1, [r0, #GPIO_BSRR]   ; the first step: LED on
    ldr  r0, lit_timer_value
    bl   start_systick
    cpsie i                     ; allow interrupts (PRIMASK = 0)
sleep_loop:
    wfi                         ; sleep until an interrupt
    b    sleep_loop

; ---- called by the SysTick at every event ----
systick_handler:
    adr  r0, timer_events
    ldrb r3, [r0]
    subs r3, r3, #1             ; one event less
    itt  ne
    strbne r3, [r0]             ; not yet 0: store it
    bne  handler_done           ; and return
    adr  r0, blink_step
    ldrb r1, [r0]
    adds r1, r1, #1             ; the next entry
    cmp  r1, #(blink_table_end - blink_table)
    it   hs
    movhs r1, #0                ; after the last one: from the start again
    strb r1, [r0]
    adr  r2, blink_table
    ldrb r3, [r2, r1]           ; its number of events
    adr  r0, timer_events
    strb r3, [r0]
    tst  r1, #1                 ; even entry: on, odd entry: off
    ite  eq
    ldreq r2, lit_led_on
    ldrne r2, lit_led_off
    ldr  r0, lit_gpioc
    str  r2, [r0, #GPIO_BSRR]
handler_done:
    bx   lr

; ---- start the SysTick with its interrupt: r0 = cycles between two events ----
start_systick:
    ldr  r1, lit_scs
    str  r0, [r1, #SYST_RVR]
    movs r0, #0
    str  r0, [r1, #SYST_CVR]
    movs r0, #7                 ; ENABLE | TICKINT | CLKSOURCE
    str  r0, [r1, #SYST_CSR]
    bx   lr

; ---- the clock of port C on, PC13 an output (push-pull, 2 MHz); changes r0, r1 ----
setup_led:
    ldr  r1, lit_rcc_apb2enr
    ldr  r0, [r1]
    orr  r0, r0, #IOPCEN
    str  r0, [r1]
    ldr  r1, lit_gpioc
    ldr  r0, [r1, #GPIO_CRH]
    bic  r0, r0, #0x00f00000
    orr  r0, r0, #0x00200000
    str  r0, [r1, #GPIO_CRH]
    bx   lr

hang:
    b    hang

blink_table:
    db 2, 2, 2, 2, 2, 2
    db 5, 2, 5, 2, 5, 2
    db 2, 2, 2, 2, 2, 8
blink_table_end:
blink_step:   db 0
timer_events: db 0
    align 4
lit_rcc_apb2enr: dd RCC_APB2ENR
lit_gpioc:       dd GPIOC_BASE
lit_led_on:      dd LED_ON
lit_led_off:     dd LED_OFF
lit_scs:         dd SCS_BASE
lit_timer_value: dd TIMER_VALUE

6.13 Macros

GNU as can define macros: a name for a group of lines with parameters, written out wherever the name is used. Asmonite has no macros (and no preprocessor); it keeps to the principle that the source shows every instruction. Repeated sequences become functions, which also saves memory; repeated numbers become constants.

6.14 Weak symbols

With the GNU linker a vector table can name handlers that may or may not exist: a weak symbol gets a default (an empty handler) unless another file defines it. A program in Asmonite is one file, so the vector table simply names the handler it has, or hang for all others, as in the examples.

6.15 Symbol aliases

Two names for the same address: in Asmonite a constant can be set to a label (RESET_HANDLER equ _start), or two labels can stand on consecutive lines.

6.16 An improved vector table

The vector table of the Cortex-M33 has 16 entries for the core and one for each interrupt of the chip (52 on the RP2350). The examples write only as many entries as they need; the last one is the highest exception they use. A complete table for every exception of the core is in the template (6.24), and VTOR wants the table at an address that is a multiple of 128 (the start of the RAM and of the flash are).

6.17 .include

GNU as can paste another file into the source (.include "stm32f103.inc"), which the original tutorial uses for the register definitions. Asmonite has no include; each example here is complete in itself and carries the constants it needs.

6.18 Local labels

GNU as has numbered labels (1:, b 1b back, b 1f forward) that can be used again and again. In Asmonite every label is unique in a file; the examples give the labels of a function its name as a prefix (setup_wait, pattern_loop).

6.19 Initializing the RAM

A program in the flash has its variables in the RAM, and after a power cycle the RAM holds random values. So the program sets them up at its start, as the start-up code of every C program does:

The program below is the SysTick interrupt example for the flash. The RAM addresses of the variables are constants worked out from the position of their start values in the flash:

19_init_ram.asm download
; ARM assembly tutorial for Asmonite, example 19: start-up code for the flash (Raspberry Pi Pico 2, Arm Cortex-M33).
; Adapted from the ARM-ASM-Tutorial by Niklas Guertler (examples BlinkInitRAM and BlinkStartup, section
; "Initializing RAM"); see LICENSE.txt.
;
; 18_systick_interrupt.asm for the flash. A program in the flash cannot keep its variables there (the
; flash cannot be written like the RAM), and after a power cycle the RAM holds random values. So the
; program sets up its variables itself at the start, as the start-up code of a C program does:
;   - "data": variables with a start value. The start values stand in the flash (data_load ..
;     data_load_end); a loop copies them into the RAM at RAM_DATA.
;   - "bss": variables that start at 0. A loop fills their place in the RAM with zeros.
; The addresses of the variables in the RAM are constants (equ), worked out from the offsets of the start
; values in the flash: TIMER_EVENTS = RAM_DATA + (d_timer_events - data_load).
; The vector table is at 0x10000000, the start of the flash; the program writes this address into VTOR.
.target pico2
.arch thumb
.imagebase 0x10000000

RESETS_BASE         equ 0x40020000
RESETS_RESET_DONE   equ 0x08
RESET_BITS          equ (1 << 6) | (1 << 9)     ; IO_BANK0, PADS_BANK0
IO_BANK0_BASE       equ 0x40028000
GPIO25_CTRL         equ 0xcc
FUNCSEL_SIO         equ 5
PADS_BANK0_BASE     equ 0x40038000
PADS_GPIO25         equ 0x68
PAD_ISO             equ 1 << 8
SIO_BASE            equ 0xd0000000
SIO_GPIO_OUT_SET    equ 0x18
SIO_GPIO_OUT_CLR    equ 0x20
SIO_GPIO_OE_SET     equ 0x38
ALIAS_CLR           equ 0x3000
SCS_BASE            equ 0xe000e000
SYST_CSR            equ 0x10
SYST_RVR            equ 0x14
SYST_CVR            equ 0x18
SCB_VTOR            equ 0xd08
TIMER_VALUE         equ 1500000
SRAM_END            equ 0x20082000

RAM_DATA            equ 0x20000000                          ; the "data" variables in the RAM
DATA_SIZE           equ data_load_end - data_load           ; a multiple of 4 (align 4 at the end)
TIMER_EVENTS        equ RAM_DATA + (d_timer_events - data_load)
BSS_START           equ RAM_DATA + DATA_SIZE                ; the "bss" variables behind them
BLINK_STEP          equ BSS_START + 0                       ; 1 byte
BSS_SIZE            equ 4

vectors:
    dd SRAM_END                 ;  0: the stack pointer at the start
    dd _start + 1               ;  1: reset
    dd hang + 1                 ;  2: NMI
    dd hang + 1                 ;  3: HardFault
    dd hang + 1                 ;  4: MemManage
    dd hang + 1                 ;  5: BusFault
    dd hang + 1                 ;  6: UsageFault
    dd hang + 1                 ;  7: SecureFault
    dd 0, 0, 0                  ;  8 .. 10: reserved
    dd hang + 1                 ; 11: SVCall
    dd hang + 1                 ; 12: DebugMonitor
    dd 0                        ; 13: reserved
    dd hang + 1                 ; 14: PendSV
    dd systick_handler + 1      ; 15: SysTick

image_def:                      ; for the boot ROM: a program for the Arm cores
    dd 0xffffded3
    db 0x42, 1
    dw 0x1021
    db 0xff
    dw 1
    db 0
    dd 0
    dd 0xab123579

_start:
    ldr  r0, lit_scs
    adr  r1, vectors
    str  r1, [r0, #SCB_VTOR]

    ; data: copy the start values from the flash into the RAM, a word at a time
    ldr  r0, lit_ram_data       ; r0 = where to (RAM)
    ldr  r1, lit_data_size
    add  r1, r1, r0             ; r1 = the end
    adr  r2, data_load          ; r2 = where from (flash)
    b    copy_check
copy_loop:
    ldr  r3, [r2], #4
    str  r3, [r0], #4
copy_check:
    cmp  r0, r1
    blo  copy_loop

    ; bss: zeros
    ldr  r0, lit_bss_start
    ldr  r1, lit_bss_size
    add  r1, r1, r0
    movs r2, #0
    b    zero_check
zero_loop:
    str  r2, [r0], #4
zero_check:
    cmp  r0, r1
    blo  zero_loop

    bl   setup_led
    ldr  r0, lit_sio
    ldr  r1, lit_led_bit
    str  r1, [r0, #SIO_GPIO_OUT_SET]
    ldr  r0, lit_timer_value
    bl   start_systick
    cpsie i                     ; allow interrupts (PRIMASK = 0): the boot ROM may leave them off
sleep_loop:
    wfi
    b    sleep_loop

systick_handler:
    ldr  r0, lit_timer_events
    ldrb r3, [r0]
    subs r3, r3, #1
    itt  ne
    strbne r3, [r0]
    bne  handler_done
    ldr  r0, lit_blink_step
    ldrb r1, [r0]
    adds r1, r1, #1
    cmp  r1, #(blink_table_end - blink_table)
    it   hs
    movhs r1, #0
    strb r1, [r0]
    adr  r2, blink_table
    ldrb r3, [r2, r1]
    ldr  r0, lit_timer_events
    strb r3, [r0]
    ldr  r0, lit_sio
    ldr  r2, lit_led_bit
    tst  r1, #1
    ite  eq
    streq r2, [r0, #SIO_GPIO_OUT_SET]
    strne r2, [r0, #SIO_GPIO_OUT_CLR]
handler_done:
    bx   lr

start_systick:
    ldr  r1, lit_scs
    str  r0, [r1, #SYST_RVR]
    movs r0, #0
    str  r0, [r1, #SYST_CVR]
    movs r0, #7
    str  r0, [r1, #SYST_CSR]
    bx   lr

hang:
    b    hang

; ---- IO_BANK0 and PADS_BANK0 out of reset, GPIO 25: SIO, output; changes r0 .. r2 ----
setup_led:
    ldr  r1, lit_resets_clr
    ldr  r0, lit_reset_bits
    str  r0, [r1]
    ldr  r1, lit_resets
setup_wait:
    ldr  r2, [r1, #RESETS_RESET_DONE]
    and  r2, r2, r0
    cmp  r2, r0
    bne  setup_wait
    ldr  r1, lit_pads_clr
    mov  r0, #PAD_ISO
    str  r0, [r1, #PADS_GPIO25]
    ldr  r1, lit_io_bank0
    movs r0, #FUNCSEL_SIO
    str  r0, [r1, #GPIO25_CTRL]
    ldr  r1, lit_sio
    ldr  r0, lit_led_bit
    str  r0, [r1, #SIO_GPIO_OE_SET]
    bx   lr

blink_table:
    db 2, 2, 2, 2, 2, 2
    db 5, 2, 5, 2, 5, 2
    db 2, 2, 2, 2, 2, 8
blink_table_end:

    align 4
data_load:                      ; the start values of the "data" variables
d_timer_events: db 2
    align 4
data_load_end:

lit_resets:      dd RESETS_BASE
lit_resets_clr:  dd RESETS_BASE + ALIAS_CLR
lit_reset_bits:  dd RESET_BITS
lit_pads_clr:    dd PADS_BANK0_BASE + ALIAS_CLR
lit_io_bank0:    dd IO_BANK0_BASE
lit_sio:         dd SIO_BASE
lit_led_bit:     dd 1 << 25
lit_scs:          dd SCS_BASE
lit_timer_value:  dd TIMER_VALUE
lit_ram_data:     dd RAM_DATA
lit_data_size:    dd DATA_SIZE
lit_bss_start:    dd BSS_START
lit_bss_size:     dd BSS_SIZE
lit_timer_events: dd TIMER_EVENTS
lit_blink_step:   dd BLINK_STEP

6.20 Interrupts of the peripherals

The peripherals of the chip have interrupts too. Three switches must be on for one to reach the core: the interrupt in the peripheral itself, its line in the core's interrupt controller (the NVIC, register NVIC_ISER0 at 0xe000e100, one bit per interrupt), and its entry in the vector table. Here the alarm of TIMER0, a microsecond timer of the RP2350, toggles the LED four times a second. Its microseconds are counted from clk_ref, which after a reset runs from the inexact ring oscillator; so the program first starts the 12 MHz crystal and lets clk_ref run from it (the first two steps of 6.23):

20_timer_interrupt.asm download
; ARM assembly tutorial for Asmonite, example 20: an interrupt of a peripheral (Raspberry Pi Pico 2, Arm Cortex-M33).
; Adapted from the ARM-ASM-Tutorial by Niklas Guertler (example BlinkTIM1, section "Peripheral
; interrupts"); see LICENSE.txt.
;
; The interrupt comes from the TIMER0 of the RP2350 now, not from the SysTick of the core. TIMER0 counts
; microseconds (64 bit); an alarm fires when the low 32 bits reach the value in ALARM0. Three switches
; must be on for the interrupt to reach the core:
;   1. the alarm itself: TIMER0.INTE bit 0
;   2. the interrupt controller of the core (NVIC): TIMER0_IRQ_0 is interrupt 0, NVIC_ISER0 bit 0
;   3. its entry in the vector table: entry 16 + 0 = 16
; The handler clears the interrupt (TIMER0.INTR bit 0, write 1), sets the next alarm and toggles the LED.
; The microsecond comes from the TICKS block: TIMER0 gets a tick every CYCLES cycles of clk_ref. After a reset
; clk_ref runs from the ring oscillator, whose frequency is not exact, so the program first starts the 12 MHz
; crystal and lets clk_ref run from it (steps 1 and 2 of 21_clock.asm); then CYCLES = 12 is one microsecond.
.target pico2
.arch thumb
.imagebase 0x20000000

RESETS_BASE         equ 0x40020000
RESETS_RESET_DONE   equ 0x08
RESET_BITS          equ (1 << 6) | (1 << 9)     ; IO_BANK0, PADS_BANK0
IO_BANK0_BASE       equ 0x40028000
GPIO25_CTRL         equ 0xcc
FUNCSEL_SIO         equ 5
PADS_BANK0_BASE     equ 0x40038000
PADS_GPIO25         equ 0x68
PAD_ISO             equ 1 << 8
SIO_BASE            equ 0xd0000000
SIO_GPIO_OUT_SET    equ 0x18
SIO_GPIO_OUT_CLR    equ 0x20
SIO_GPIO_OE_SET     equ 0x38
ALIAS_CLR           equ 0x3000
RESET_TIMER0        equ 1 << 23
TIMER0_BASE         equ 0x400b0000
TIMER_ALARM0        equ 0x10
TIMER_TIMERAWL      equ 0x28
TIMER_INTR          equ 0x3c
TIMER_INTE          equ 0x40
TICKS_BASE          equ 0x40108000
TICKS_TIMER0_CTRL   equ 0x18
TICKS_TIMER0_CYCLES equ 0x1c
CLK_REF_MHZ         equ 12            ; clk_ref in MHz: the crystal of the Pico 2
XOSC_BASE           equ 0x40048000
XOSC_CTRL           equ 0x00
XOSC_STATUS         equ 0x04
XOSC_STARTUP        equ 0x0c
XOSC_RANGE_1_15MHZ  equ 0xaa0
XOSC_ENABLE_1_15MHZ equ (0xfab << 12) | 0xaa0
XOSC_STARTUP_DELAY  equ 282           ; 282 * 256 cycles: about 6 ms (as the Pico SDK)
CLOCKS_BASE         equ 0x40010000
CLK_REF_CTRL        equ 0x30
CLK_REF_SELECTED    equ 0x38
SCS_BASE            equ 0xe000e000
NVIC_ISER0          equ 0x100
SCB_VTOR            equ 0xd08
INTERVAL_US         equ 250000        ; a quarter of a second
SIO_GPIO_OUT_XOR    equ 0x28
SRAM_END            equ 0x20082000

vectors:
    dd SRAM_END                 ;  0: the stack pointer at the start
    dd _start + 1               ;  1: reset
    dd hang + 1                 ;  2: NMI
    dd hang + 1                 ;  3: HardFault
    dd hang + 1, hang + 1, hang + 1, hang + 1   ; 4 .. 7: MemManage, BusFault, UsageFault, SecureFault
    dd 0, 0, 0                  ;  8 .. 10: reserved
    dd hang + 1, hang + 1, 0    ; 11 .. 13: SVCall, DebugMonitor, reserved
    dd hang + 1, hang + 1       ; 14, 15: PendSV, SysTick
    dd timer_handler + 1        ; 16: interrupt 0, TIMER0_IRQ_0

_start:
    ldr  r0, lit_scs
    adr  r1, vectors
    str  r1, [r0, #SCB_VTOR]
    bl   setup_led
    bl   clk_ref_xosc

    ; TIMER0 out of reset
    ldr  r1, lit_resets_clr
    ldr  r0, lit_reset_timer0
    str  r0, [r1]
    ldr  r1, lit_resets
timer_reset_wait:
    ldr  r2, [r1, #RESETS_RESET_DONE]
    tst  r2, r0
    beq  timer_reset_wait

    ; one tick each microsecond
    ldr  r1, lit_ticks
    movs r0, #CLK_REF_MHZ
    str  r0, [r1, #TICKS_TIMER0_CYCLES]
    movs r0, #1
    str  r0, [r1, #TICKS_TIMER0_CTRL]

    ; the first alarm, the interrupt on in the timer and in the NVIC
    ldr  r4, lit_timer0
    ldr  r0, [r4, #TIMER_TIMERAWL]
    ldr  r1, lit_interval
    add  r0, r0, r1
    str  r0, [r4, #TIMER_ALARM0]
    movs r0, #1
    str  r0, [r4, #TIMER_INTE]
    ldr  r1, lit_scs
    str  r0, [r1, #NVIC_ISER0]  ; bit 0: interrupt 0
    cpsie i                     ; allow interrupts (PRIMASK = 0): the boot ROM may leave them off
sleep_loop:
    wfi
    b    sleep_loop

; ---- interrupt 0: the alarm 0 of TIMER0 ----
timer_handler:
    ldr  r0, lit_timer0
    movs r1, #1
    str  r1, [r0, #TIMER_INTR]  ; clear the interrupt
    ldr  r1, [r0, #TIMER_ALARM0]
    ldr  r2, lit_interval
    add  r1, r1, r2
    str  r1, [r0, #TIMER_ALARM0]  ; the next alarm, INTERVAL_US after this one
    ldr  r0, lit_sio
    ldr  r1, lit_led_bit
    str  r1, [r0, #SIO_GPIO_OUT_XOR]  ; toggle the LED
    bx   lr

; ---- start the crystal, clk_ref from it (12 MHz); changes r0, r1 ----
clk_ref_xosc:
    ldr  r1, lit_xosc
    movw r0, #XOSC_RANGE_1_15MHZ
    str  r0, [r1, #XOSC_CTRL]   ; the frequency range first
    movw r0, #XOSC_STARTUP_DELAY
    str  r0, [r1, #XOSC_STARTUP]
    ldr  r0, lit_xosc_enable
    str  r0, [r1, #XOSC_CTRL]   ; then on
xosc_wait:
    ldr  r0, [r1, #XOSC_STATUS]
    cmp  r0, #0
    bge  xosc_wait              ; bit 31 STABLE
    ldr  r1, lit_clocks
    movs r0, #2                 ; SRC 2: XOSC
    str  r0, [r1, #CLK_REF_CTRL]
ref_wait:
    ldr  r0, [r1, #CLK_REF_SELECTED]
    cmp  r0, #1 << 2
    bne  ref_wait
    bx   lr

hang:
    b    hang

; ---- IO_BANK0 and PADS_BANK0 out of reset, GPIO 25: SIO, output; changes r0 .. r2 ----
setup_led:
    ldr  r1, lit_resets_clr
    ldr  r0, lit_reset_bits
    str  r0, [r1]
    ldr  r1, lit_resets
setup_wait:
    ldr  r2, [r1, #RESETS_RESET_DONE]
    and  r2, r2, r0
    cmp  r2, r0
    bne  setup_wait
    ldr  r1, lit_pads_clr
    mov  r0, #PAD_ISO
    str  r0, [r1, #PADS_GPIO25]
    ldr  r1, lit_io_bank0
    movs r0, #FUNCSEL_SIO
    str  r0, [r1, #GPIO25_CTRL]
    ldr  r1, lit_sio
    ldr  r0, lit_led_bit
    str  r0, [r1, #SIO_GPIO_OE_SET]
    bx   lr

    align 4
lit_resets:      dd RESETS_BASE
lit_resets_clr:  dd RESETS_BASE + ALIAS_CLR
lit_reset_bits:  dd RESET_BITS
lit_pads_clr:    dd PADS_BANK0_BASE + ALIAS_CLR
lit_io_bank0:    dd IO_BANK0_BASE
lit_sio:         dd SIO_BASE
lit_led_bit:     dd 1 << 25
lit_scs:          dd SCS_BASE
lit_reset_timer0: dd RESET_TIMER0
lit_ticks:        dd TICKS_BASE
lit_timer0:       dd TIMER0_BASE
lit_interval:     dd INTERVAL_US
lit_xosc:         dd XOSC_BASE
lit_xosc_enable:  dd XOSC_ENABLE_1_15MHZ
lit_clocks:       dd CLOCKS_BASE

The handler must clear the interrupt in the peripheral (here: write 1 to INTR); otherwise it is called again at once.

6.21 Analysis tools

The original tutorial introduces the GNU tools for looking into a program: the disassembler objdump, readelf, nm, addr2line, objcopy. In Asmonite most of this is part of the debugger: the memory view with the switch Code shows the instructions at any address, the registers and the stack are always in view, and Project > Save as UF2 file writes the file for the BOOTSEL drive. For programs you did not write, Asmonite comes with Dismonite, a disassembler that reads binaries and analyses them.

6.22 Interfacing C code

Assembly and C are often mixed: C for the most of a program, assembly for a few functions. The calling convention (6.4.2) is what makes this work. Asmonite does not compile C for the Arm cores, so this part of the original tutorial has no counterpart here; with the Pico SDK an assembly file (.S) can be added to a C project and its functions called like C functions.

6.23 The clock

After a reset the RP2350 runs from its ring oscillator (ROSC), whose frequency is not exact. The Pico 2 has a 12 MHz crystal (XOSC), and from it a PLL makes the 150 MHz the chip is made for. The steps: start the crystal and wait until it is stable (about 6 ms: the crystal of the Pico 2 starts slowly, and with too short a wait the chip may hang); let clk_ref run from it; switch clk_sys to clk_ref while the PLL changes; set the PLL to 12 MHz × 125 = 1500 MHz and divide by 5 and by 2; switch clk_sys to the PLL.

21_clock.asm download
; ARM assembly tutorial for Asmonite, example 21: the clock (Raspberry Pi Pico 2, Arm Cortex-M33).
; Adapted from the ARM-ASM-Tutorial by Niklas Guertler (section "Clock configuration"); see LICENSE.txt.
;
; After a reset the RP2350 runs from its ring oscillator (ROSC), a few MHz that depend on the chip and the
; temperature. The program switches to the 12 MHz crystal of the Pico 2 (XOSC) and from there, with the
; PLL, to 150 MHz, the speed the RP2350 is made for:
;   1. start the XOSC and wait until it is stable
;   2. clk_ref from the XOSC (12 MHz)
;   3. clk_sys back to clk_ref while the PLL changes
;   4. PLL_SYS: 12 MHz / 1 (REFDIV) * 125 (FBDIV) = 1500 MHz (the VCO), / 5 / 2 (POSTDIV1, POSTDIV2) = 150 MHz
;   5. clk_sys from the PLL
; Then a SysTick blink as in 17_systick.asm: the SysTick counts the core clock, so with TIMER_VALUE =
; 15000000 an event is 0.1 s. The program runs in the RAM: the flash would need slower timing at 150 MHz.
.target pico2
.arch thumb
.imagebase 0x20000000

RESETS_BASE         equ 0x40020000
RESETS_RESET_DONE   equ 0x08
RESET_BITS          equ (1 << 6) | (1 << 9)     ; IO_BANK0, PADS_BANK0
IO_BANK0_BASE       equ 0x40028000
GPIO25_CTRL         equ 0xcc
FUNCSEL_SIO         equ 5
PADS_BANK0_BASE     equ 0x40038000
PADS_GPIO25         equ 0x68
PAD_ISO             equ 1 << 8
SIO_BASE            equ 0xd0000000
SIO_GPIO_OUT_SET    equ 0x18
SIO_GPIO_OUT_CLR    equ 0x20
SIO_GPIO_OE_SET     equ 0x38
ALIAS_CLR           equ 0x3000
RESET_PLL_SYS       equ 1 << 14
XOSC_BASE           equ 0x40048000
XOSC_CTRL           equ 0x00
XOSC_STATUS         equ 0x04
XOSC_STARTUP        equ 0x0c
XOSC_RANGE_1_15MHZ  equ 0xaa0                     ; FREQ_RANGE 1 .. 15 MHz
XOSC_ENABLE_1_15MHZ equ (0xfab << 12) | 0xaa0     ; and ENABLE
XOSC_STARTUP_DELAY  equ 282                       ; 282 * 256 cycles of 12 MHz: about 6 ms, as the Pico SDK
                                                  ; (1 ms is too short: the crystal starts slowly)
CLOCKS_BASE         equ 0x40010000
CLK_REF_CTRL        equ 0x30
CLK_REF_SELECTED    equ 0x38
CLK_SYS_CTRL        equ 0x3c
CLK_SYS_SELECTED    equ 0x44
PLL_SYS_BASE        equ 0x40050000
PLL_CS              equ 0x00
PLL_PWR             equ 0x04
PLL_FBDIV_INT       equ 0x08
PLL_PRIM            equ 0x0c
PLL_PWR_PD          equ 1 << 0
PLL_PWR_POSTDIVPD   equ 1 << 3
PLL_PWR_VCOPD       equ 1 << 5
SCS_BASE            equ 0xe000e000
SYST_CSR            equ 0x10
SYST_RVR            equ 0x14
SYST_CVR            equ 0x18
COUNTFLAG           equ 1 << 16
TIMER_VALUE         equ 15000000                  ; 0.1 s at 150 MHz

_start:
    bl   clock_150mhz
    bl   setup_led
    ldr  r0, lit_timer_value
    bl   start_systick
    ldr  r4, lit_sio
    ldr  r5, lit_led_bit
blink_loop:
    str  r5, [r4, #SIO_GPIO_OUT_SET]
    movs r0, #2
    bl   wait_systick
    str  r5, [r4, #SIO_GPIO_OUT_CLR]
    movs r0, #8
    bl   wait_systick
    b    blink_loop

; ---- XOSC, PLL_SYS 150 MHz, clk_sys from it; changes r0 .. r3 ----
clock_150mhz:
    ; 1. the crystal
    ldr  r1, lit_xosc
    movw r0, #XOSC_RANGE_1_15MHZ
    str  r0, [r1, #XOSC_CTRL]   ; the frequency range first
    movw r0, #XOSC_STARTUP_DELAY
    str  r0, [r1, #XOSC_STARTUP]
    ldr  r0, lit_xosc_enable
    str  r0, [r1, #XOSC_CTRL]   ; then on
xosc_wait:
    ldr  r0, [r1, #XOSC_STATUS]
    cmp  r0, #0
    bge  xosc_wait              ; bit 31 STABLE: the word is negative when it is set
    ; 2. clk_ref = XOSC (SRC 2)
    ldr  r1, lit_clocks
    movs r0, #2
    str  r0, [r1, #CLK_REF_CTRL]
ref_wait:
    ldr  r0, [r1, #CLK_REF_SELECTED]
    cmp  r0, #1 << 2            ; one bit for each source: bit 2 = XOSC
    bne  ref_wait
    ; 3. clk_sys = clk_ref (SRC 0) while the PLL changes
    movs r0, #0
    str  r0, [r1, #CLK_SYS_CTRL]
sys_ref_wait:
    ldr  r0, [r1, #CLK_SYS_SELECTED]
    cmp  r0, #1                 ; bit 0: clk_ref
    bne  sys_ref_wait
    ; 4. PLL_SYS: reset, then 12 MHz * 125 = 1500 MHz, / 5 / 2 = 150 MHz
    ldr  r2, lit_resets
    ldr  r3, lit_reset_pll
    ldr  r0, lit_resets_set
    str  r3, [r0]               ; PLL_SYS into reset (the set alias) ...
    ldr  r0, lit_resets_clr
    str  r3, [r0]               ; ... and out again
pll_reset_wait:
    ldr  r0, [r2, #RESETS_RESET_DONE]
    tst  r0, r3
    beq  pll_reset_wait
    ldr  r2, lit_pll_sys
    movs r0, #1
    str  r0, [r2, #PLL_CS]      ; REFDIV 1
    movs r0, #125
    str  r0, [r2, #PLL_FBDIV_INT]
    ldr  r0, [r2, #PLL_PWR]
    bic  r0, r0, #PLL_PWR_PD | PLL_PWR_VCOPD
    str  r0, [r2, #PLL_PWR]     ; power on the PLL and its VCO
pll_lock_wait:
    ldr  r0, [r2, #PLL_CS]
    cmp  r0, #0
    bge  pll_lock_wait          ; bit 31 LOCK
    ldr  r0, lit_postdiv
    str  r0, [r2, #PLL_PRIM]    ; POSTDIV1 5, POSTDIV2 2
    ldr  r0, [r2, #PLL_PWR]
    bic  r0, r0, #PLL_PWR_POSTDIVPD
    str  r0, [r2, #PLL_PWR]     ; power on the post dividers
    ; 5. clk_sys = the aux source (SRC 1), which is PLL_SYS (AUXSRC 0)
    movs r0, #1
    str  r0, [r1, #CLK_SYS_CTRL]
sys_pll_wait:
    ldr  r0, [r1, #CLK_SYS_SELECTED]
    cmp  r0, #2                 ; bit 1: the aux source
    bne  sys_pll_wait
    bx   lr

start_systick:
    ldr  r1, lit_scs
    str  r0, [r1, #SYST_RVR]
    movs r0, #0
    str  r0, [r1, #SYST_CVR]
    movs r0, #5
    str  r0, [r1, #SYST_CSR]
    bx   lr

wait_systick:
    ldr  r1, lit_scs
wait_loop:
    ldr  r2, [r1, #SYST_CSR]
    tst  r2, #COUNTFLAG
    beq  wait_loop
    subs r0, r0, #1
    bne  wait_loop
    bx   lr

; ---- IO_BANK0 and PADS_BANK0 out of reset, GPIO 25: SIO, output; changes r0 .. r2 ----
setup_led:
    ldr  r1, lit_resets_clr
    ldr  r0, lit_reset_bits
    str  r0, [r1]
    ldr  r1, lit_resets
setup_wait:
    ldr  r2, [r1, #RESETS_RESET_DONE]
    and  r2, r2, r0
    cmp  r2, r0
    bne  setup_wait
    ldr  r1, lit_pads_clr
    mov  r0, #PAD_ISO
    str  r0, [r1, #PADS_GPIO25]
    ldr  r1, lit_io_bank0
    movs r0, #FUNCSEL_SIO
    str  r0, [r1, #GPIO25_CTRL]
    ldr  r1, lit_sio
    ldr  r0, lit_led_bit
    str  r0, [r1, #SIO_GPIO_OE_SET]
    bx   lr

    align 4
lit_resets:      dd RESETS_BASE
lit_resets_clr:  dd RESETS_BASE + ALIAS_CLR
lit_reset_bits:  dd RESET_BITS
lit_pads_clr:    dd PADS_BANK0_BASE + ALIAS_CLR
lit_io_bank0:    dd IO_BANK0_BASE
lit_sio:         dd SIO_BASE
lit_led_bit:     dd 1 << 25
lit_resets_set:  dd RESETS_BASE + 0x2000
lit_reset_pll:   dd RESET_PLL_SYS
lit_xosc:        dd XOSC_BASE
lit_xosc_enable: dd XOSC_ENABLE_1_15MHZ
lit_clocks:      dd CLOCKS_BASE
lit_pll_sys:     dd PLL_SYS_BASE
lit_postdiv:     dd (5 << 16) | (2 << 12)
lit_scs:         dd SCS_BASE
lit_timer_value: dd TIMER_VALUE

The SysTick counts the core clock, so the blink times of this program are exact now: 0.2 s on, 0.8 s off.

6.24 A program template

To start your own programs: a template with a complete vector table, the start, the LED set-up and a delay function, ready to be filled at main.

22_template.asm download
; ARM assembly tutorial for Asmonite, example 22: a template for your own programs (Raspberry Pi Pico 2,
; Arm Cortex-M33).
; Adapted from the ARM-ASM-Tutorial by Niklas Guertler (example ProjectTemplate); see LICENSE.txt.
;
; Copy this file and write your program at "main". It runs in the RAM (F7 loads it and stops at _start);
; for the flash change .imagebase to 0x10000000 and put the IMAGE_DEF block of 08_blink_flash.asm behind
; the vector table. What is here:
;   - the vector table with a handler for every exception of the core (they end in "hang", where the
;     debugger shows you which one it was: look at the register IPSR, the low bits of xPSR)
;   - VTOR, the stack pointer and the start-up of the "data" and "bss" variables are not needed in the RAM:
;     the variables are part of the program, see 07_ram_variables.asm
;   - setup_led and the delay function, and the constants of the blocks used so far
.target pico2
.arch thumb
.imagebase 0x20000000

RESETS_BASE         equ 0x40020000
RESETS_RESET_DONE   equ 0x08
RESET_BITS          equ (1 << 6) | (1 << 9)     ; IO_BANK0, PADS_BANK0
IO_BANK0_BASE       equ 0x40028000
GPIO25_CTRL         equ 0xcc
FUNCSEL_SIO         equ 5
PADS_BANK0_BASE     equ 0x40038000
PADS_GPIO25         equ 0x68
PAD_ISO             equ 1 << 8
SIO_BASE            equ 0xd0000000
SIO_GPIO_OUT_SET    equ 0x18
SIO_GPIO_OUT_CLR    equ 0x20
SIO_GPIO_OUT_XOR    equ 0x28
SIO_GPIO_OE_SET     equ 0x38
ALIAS_CLR           equ 0x3000
SCS_BASE            equ 0xe000e000
SCB_VTOR            equ 0xd08
SRAM_END            equ 0x20082000

vectors:
    dd SRAM_END                 ;  0: the stack pointer at the start
    dd _start + 1               ;  1: reset
    dd hang + 1                 ;  2: NMI
    dd hang + 1                 ;  3: HardFault
    dd hang + 1, hang + 1, hang + 1, hang + 1   ; 4 .. 7: MemManage, BusFault, UsageFault, SecureFault
    dd 0, 0, 0                  ;  8 .. 10: reserved
    dd hang + 1, hang + 1, 0    ; 11 .. 13: SVCall, DebugMonitor, reserved
    dd hang + 1, hang + 1       ; 14, 15: PendSV, SysTick

_start:
    ldr  r0, lit_scs
    adr  r1, vectors
    str  r1, [r0, #SCB_VTOR]    ; our vector table
    bl   setup_led

main:
    ; ---- your program ----
    ldr  r4, lit_sio
    ldr  r5, lit_led_bit
main_loop:
    str  r5, [r4, #SIO_GPIO_OUT_XOR]
    ldr  r0, lit_delay_value
    bl   delay
    b    main_loop

; ---- delay(r0 = iterations) ----
delay:
    subs r0, r0, #1
    bne  delay
    bx   lr

; ---- IO_BANK0 and PADS_BANK0 out of reset, GPIO 25: SIO, output; changes r0 .. r2 ----
setup_led:
    ldr  r1, lit_resets_clr
    ldr  r0, lit_reset_bits
    str  r0, [r1]
    ldr  r1, lit_resets
setup_wait:
    ldr  r2, [r1, #RESETS_RESET_DONE]
    and  r2, r2, r0
    cmp  r2, r0
    bne  setup_wait
    ldr  r1, lit_pads_clr
    mov  r0, #PAD_ISO
    str  r0, [r1, #PADS_GPIO25]
    ldr  r1, lit_io_bank0
    movs r0, #FUNCSEL_SIO
    str  r0, [r1, #GPIO25_CTRL]
    ldr  r1, lit_sio
    ldr  r0, lit_led_bit
    str  r0, [r1, #SIO_GPIO_OE_SET]
    bx   lr

hang:
    b    hang                   ; an exception without its own handler

    align 4
lit_resets:      dd RESETS_BASE
lit_resets_clr:  dd RESETS_BASE + ALIAS_CLR
lit_reset_bits:  dd RESET_BITS
lit_pads_clr:    dd PADS_BANK0_BASE + ALIAS_CLR
lit_io_bank0:    dd IO_BANK0_BASE
lit_sio:         dd SIO_BASE
lit_led_bit:     dd 1 << 25
lit_scs:         dd SCS_BASE
lit_delay_value: dd 1000000

; ---- variables ----
counter: dd 0

Credits

This tutorial is an adaptation of the ARM-ASM-Tutorial by Niklas Gürtler on mikrocontroller.net (discussion: forum thread, code: github.com/Erlkoenig90/ArmAsmTutorial). The structure and the course of the examples follow his tutorial; the text is not his: it is written anew for the Raspberry Pi Pico 2 and Asmonite, and published with his kind permission. The example programs are adapted from his under the MIT License: LICENSE.txt.