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.
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.
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.
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).
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.
Asmonite has an instruction reference built in: put the cursor on an instruction and press F1.
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.
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).
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.
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.
The smallest program that does something visible in the debugger:
; 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:
.target pico2 says which board the program is for. Asmonite checks the program against it after assembling (the right instruction set, an address the board has, the size) and refuses to write it to a different board..arch thumb chooses the instruction set. A Cortex-M runs Thumb code only..imagebase 0x20000000 is the address where the first byte of the program goes: the start of the RAM. Labels in the program get addresses from there._start: is a label, a name for the address of the next line. The debugger starts the program at _start.nop (no operation) does nothing, and b $ is a branch to $, the address of the line itself: an endless loop. A microcontroller program never ends; there is nothing to return to.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.
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.
While the program is stopped:
R0 to R12, SP, LR, PC and XPSR with its flags. A value that changed with the last step is highlighted. You can type a new value into a register.SP up.Step through the first program with F7: after nop, PC is 2 higher (a 16-bit instruction), and b $ stays where it is.
The Cortex-M has 16 registers of 32 bits:
| Register | Use |
|---|---|
r0 – r12 | general purpose: values, addresses, counters. Many 16-bit instructions can only use r0 – r7 (the low registers). |
r13 = sp | the stack pointer (6.3) |
r14 = lr | the link register: the return address of a call (6.4) |
r15 = pc | the 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.
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:
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.)
A pin of the RP2350 passes three blocks on its way to the core:
0x40038000): the electrical side of each pin: drive strength, pull-ups, input enable. On the RP2350 every pad starts isolated (bit 8, ISO, of its register); the program clears it. GPIO 25 has the register at offset 0x68.0x40028000): which of the peripherals drives the pin. Its register GPIO25_CTRL (offset 0xcc) gets the function number 5, the SIO.0xd0000000): the simple input and output that the cores reach in one cycle. GPIO_OE_SET (offset 0x38) switches the output driver of the pins whose bits are 1 on.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:
; 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.
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, r2 | r0 = r1 + r2 |
sub r0, r1, #4 | r0 = r1 − 4 |
and r0, r0, #0xff | keep bits 0 to 7, clear the others |
orr r0, r0, #4 | set bit 2 (or) |
bic r0, r0, #0x100 | clear bit 8 (bit clear: and with the inverted constant) |
eor r0, r0, r1 | exclusive or: toggle the bits that are 1 in r1 |
lsl r0, r1, #2 | shift left by 2: r0 = r1 × 4 (lsr, asr: right, unsigned or signed) |
mov r0, r1 | copy |
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).
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):
; 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.
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:
| Branch | Jumps if | Flags |
|---|---|---|
beq / bne | equal / not equal (zero / not zero) | Z = 1 / Z = 0 |
bhs / blo | unsigned higher or same / lower | C = 1 / C = 0 |
bhi / bls | unsigned higher / lower or same | |
bge / blt | signed greater or equal / less than | N = V / N ≠ V |
bgt / ble | signed greater / less or equal | |
bmi / bpl | negative / positive or zero | N = 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:
; 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.
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:
; 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:
; 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:
; 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:
; 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
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:
; 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).
The Cortex-M has one address space of 4 GB for everything: code, data and peripherals. The RP2350 uses these parts of it:
| Address | What |
|---|---|
0x00000000 | the boot ROM (32 KB): the code that starts the chip |
0x10000000 | the flash (4 MB on the Pico 2), read through a cache (XIP: execute in place) |
0x20000000 – 0x20081fff | the RAM (520 KB) |
0x40000000 | the peripherals (RESETS, IO_BANK0, PADS_BANK0, CLOCKS, TIMER0 ...) |
0xd0000000 | the SIO |
0xe0000000 | the 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.
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:
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).
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.
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:
_start + 1), because the core runs Thumb code (6.1).Since a program in Asmonite is written in order, it is enough to write them first:
; 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:
; 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
The examples from here on have the same order, which is a good habit:
.target, .arch, .imagebaseequ): addresses and bits of the peripherals, numbers of the program_start and the main programldr, after align 4The 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.
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:
; 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.
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.
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:
; 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
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.
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):
r0 – r3, the result is returned in r0 (a 64-bit result in r0 and r1).r0 – r3 and r12 may be changed by a function; the caller must not rely on them after a call.r4 – r11 must have their old values when the function returns. A function that uses them pushes them at the start and pops them at the end.sp is a multiple of 8: push and pop an even number of registers.; 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:
; 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
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.
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:
; 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.
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.
; 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
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.
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], #4 | r0; afterwards r0 = r0 + 4 (post-index) |
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:
; 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:
; 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
An instruction is 2 or 4 bytes long; it cannot hold an arbitrary 32-bit number. There are four ways around this:
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.
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.
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.
; 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.
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:
; 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
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.
; 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:
; 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.
; 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
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.
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.
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.
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).
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.
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).
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:
; 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
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):
; 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.
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.
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.
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.
; 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.
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.
; 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
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.