ARM-Assembler auf dem Raspberry Pi Pico 2

Ein Tutorial von der ersten Anweisung bis zu Interrupts und dem Takt, jedes Beispiel Schritt für Schritt in Asmonite ausgeführt und beobachtet.

Nicht der Originaltext

Das ist nicht der Text von Niklas Gürtler. Es ist ein neuer Text, geschrieben für den Raspberry Pi Pico 2 und Asmonite: Er folgt den Kapiteln und den Beispielen seines ARM-ASM-Tutorials (für den STM32F103 und die GNU-Werkzeuge), aber die Worte sind unsere eigenen, und Fehler darin ebenso. Veröffentlicht mit seiner freundlichen Erlaubnis.

Dieses Tutorial folgt Kapitel für Kapitel dem ARM-ASM-Tutorial von Niklas Gürtler auf mikrocontroller.net. Sein Tutorial ist für den STM32F103 und die GNU-Werkzeuge geschrieben (Assembler, Linker, OpenOCD, GDB). Diese Fassung nimmt stattdessen den Raspberry Pi Pico 2 und Asmonite: Ein Programm assembliert, lädt und debuggt, und außer dem Board brauchst du nur eine Debug-Probe. Wo der Pico (der erste) anders ist, sagt es ein Kasten, und ein weiterer zeigt die Beispiele auf der Blue Pill (STM32F103), dem Board des ursprünglichen Tutorials.

Die Beispielprogramme sind nach seinen Beispielen angepasst (MIT-Lizenz, Copyright 2019 Niklas Gürtler, siehe LICENSE.txt); jedes lässt sich an seinem Codeblock herunterladen. Ihre Kommentare sind englisch, wie bei allen Beispielen von Asmonite.

Inhalt

  1. 1 Einführung
    1. 1.1 Warum Assembler?
    2. 1.2 Über Arm
    3. 1.3 Architekturen und Kerne
  2. 2 Voraussetzungen
    1. 2.1 Das Board
    2. 2.2 Dokumentation
    3. 2.3 Debug-Probe
    4. 2.4 Software
  3. 3 Aufbau
    1. 3.1 Hardware
    2. 3.2 Software
  4. 4 Assemblerprogramme schreiben
    1. 4.1 Ein erstes Programm
    2. 4.2 Das Programm laden
    3. 4.3 Den Debugger benutzen
    4. 4.4 Prozessorregister
    5. 4.5 Auf die Peripherie zugreifen
    6. 4.6 Datenverarbeitung
    7. 4.7 Peripherieregister lesen
    8. 4.8 Sprungbefehle
    9. 4.9 Zählschleifen
    10. 4.10 Das RAM benutzen
  5. 5 Speicherverwaltung
    1. 5.1 Der Adressraum
    2. 5.2 Der Linker, und warum Asmonite keinen hat
    3. 5.3 Code und Daten platzieren
    4. 5.4 Aufbau eines Programms
  6. 6 Weitere Assembler-Techniken
    1. 6.1 Der Befehlssatz-Zustand
    2. 6.2 Konstanten
    3. 6.3 Der Stack
    4. 6.4 Funktionsaufrufe
    5. 6.5 Bedingte Ausführung
    6. 6.6 Rechnen mit 8 und 16 Bit
    7. 6.7 Ausrichtung
    8. 6.8 Adressierung mit Offset
    9. 6.9 Arrays durchlaufen
    10. 6.10 Konstanten laden
    11. 6.11 Der Timer SysTick
    12. 6.12 Ausnahmen und Interrupts
    13. 6.13 Makros
    14. 6.14 Schwache Symbole
    15. 6.15 Symbol-Aliase
    16. 6.16 Eine bessere Vektortabelle
    17. 6.17 .include
    18. 6.18 Lokale Labels
    19. 6.19 Das RAM vorbereiten
    20. 6.20 Interrupts der Peripherie
    21. 6.21 Werkzeuge zur Analyse
    22. 6.22 C-Code anbinden
    23. 6.23 Der Takt
    24. 6.24 Eine Programmvorlage
  7. Dank und Quellen

1 Einführung

1.1 Warum Assembler?

Kaum jemand schreibt heute noch ein ganzes Programm in Assembler: Compiler erzeugen guten Code, und C oder Rust sind viel schneller geschrieben. Die Maschinenebene zu kennen, lohnt sich trotzdem. Du liest den Code, den ein Compiler gemacht hat, wenn ein Programm sich seltsam verhält oder zu langsam ist; du schreibst die paar Zeilen, die keine Hochsprache ausdrücken kann (den Start eines Programms, den Wechsel zwischen Aufgaben, einen besonderen Befehl); und du verstehst, was ein Debugger zeigt, wenn es keinen Quelltext zum Nachsehen gibt. Ein Mikrocontroller ist der beste Ort, es zu lernen: Kein Betriebssystem steht zwischen dir und der Hardware, jedes Register jeder Peripherie ist eine Adresse, die du lesen und schreiben kannst, und eine LED zeigt sofort, ob dein Programm tut, was du denkst.

Das Schwierige sind meist nicht die Befehle. Es ist das Drumherum: die Werkzeuge, die Dokumentation und die vielen Kleinigkeiten, die ein Programm erledigen muss, bevor die erste LED leuchtet. Dieses Tutorial geht das alles mit so wenig Werkzeug wie möglich durch.

1.2 Über Arm

Arm Ltd. baut keine Chips. Die Firma entwirft Prozessorkerne und vergibt Lizenzen an Chiphersteller, die Speicher und Peripherie um sie herum bauen. Zwei Chips mit demselben Kern, etwa dem Cortex-M33 im RP2350 des Pico 2 und in vielen anderen Mikrocontrollern, führen also dieselben Befehle aus, aber ihre Peripherie (GPIO, Timer, das Taktsystem) ist völlig verschieden. Deshalb hat dieses Tutorial zwei Arten von Dokumenten: die Handbücher von Arm für alles, was zum Kern gehört, und das Datenblatt des RP2350 für alles drumherum.

1.3 Architekturen und Kerne

Eine Architektur ist die Beschreibung: die Befehle, die Register, wie Ausnahmen funktionieren. Ein Kern ist eine Umsetzung davon. Die Mikrocontroller-Kerne von Arm heißen Cortex-M, und alle führen den Befehlssatz Thumb aus, dessen Befehle 16 oder 32 Bit lang sind:

Der RP2350 ist ungewöhnlich: Er hat zusätzlich zwei RISC-V-Kerne, und beim Start entscheidet das Boot-ROM anhand des Programms im Flash, welches Paar läuft. Dieses Tutorial nimmt die Arm-Kerne; Asmonite schaltet den Chip um, wenn nötig (3.2).

2 Voraussetzungen

2.1 Das Board

Ein Raspberry Pi Pico 2 (RP2350, etwa 5 €). Die W-Version geht auch, aber ihre LED hängt am Funkchip, nicht an GPIO 25, die Blink-Beispiele schalten sie also nicht; schließ eine LED mit Widerstand (etwa 330 Ω) von GPIO 25 nach Masse an, oder ändere den Pin in den Beispielen. Ein erster Raspberry Pi Pico (RP2040) geht für die meisten Kapitel ebenfalls; die Kästen mit Pico (Cortex-M0+) zeigen, was anders ist, und drei Beispiele gibt es in einer Fassung für ihn.

Blue Pill (STM32F103)

Das Board des ursprünglichen Tutorials: ein STM32F103C8 mit einem Cortex-M3, 20 KB RAM und 64 KB (tatsächlich meist 128 KB) Flash, seine LED an PC13. Asmonite debuggt es über dieselbe Probe (.target stm32f103), im RAM und im Flash. Die Kästen wie dieser zeigen drei Beispiele des Tutorials darauf, nah an den Originalen. Boards mit einem nachgebauten Chip (CKS32, GD32, APM32) haben eine andere Geräte-ID; Asmonite sagt das dann und schreibt nichts.

2.2 Dokumentation

Asmonite hat eine Befehlsreferenz eingebaut: Setz den Cursor auf einen Befehl und drück F1.

2.3 Debug-Probe

Der Pico 2 lässt sich ganz ohne Probe programmieren: BOOTSEL beim Einstecken gedrückt halten und eine UF2-Datei auf das Laufwerk ziehen, das erscheint. Dann siehst du aber nichts davon, was das Programm tut. Eine Debug-Probe lässt den PC den Kern anhalten, Befehl für Befehl ausführen und seine Register und seinen Speicher lesen und schreiben, über zwei Drähte (SWD). Asmonite arbeitet mit der Raspberry Pi Debug Probe (etwa 12 €) oder mit einem zweiten Pico mit der Firmware debugprobe; beide sind CMSIS-DAP-Proben, die Asmonite direkt über USB ansteuert.

2.4 Software

Nur Asmonite. Es assembliert, prüft das Programm gegen das Board, schreibt es über die Probe ins RAM oder in den Flash und debuggt es. Es gibt keinen eigenen Linker, kein OpenOCD und kein GDB, und nichts muss installiert werden: das ZIP entpacken und Asmonite.exe starten (Windows 10 oder 11). Ein Taschenrechner mit Hexadezimal- und Binärzahlen ist nützlich (der von Windows hat einen Modus Programmierer).

3 Aufbau

3.1 Hardware

Der Pico 2 hat einen dreipoligen Anschluss DEBUG an seinem Ende, gegenüber der USB-Buchse: SWCLK, GND, SWDIO. Zur Debug Probe gehört ein Kabel dafür; steck es in den Anschluss D der Probe. Dann beides an den PC: die Probe mit ihrem USB-Kabel, den Pico 2 mit seinem eigenen (für den Strom). Mehr braucht es nicht; die LED des Pico 2 hängt an GPIO 25.

Blue Pill (STM32F103)

Ihre vier Pins am Ende des Boards sind 3V3, SWIO, SWCLK und GND. Verbinde SWCLK, SWIO (mit SWDIO der Probe) und GND, und lass 3V3 frei: Die Debug Probe liefert keinen Strom, die Blue Pill bekommt ihn über ihre eigene USB-Buchse.

3.2 Software

Öffne in Asmonite Debug > Debug targets, wähl Raspberry Pi Pico 2 und drück Test the connection. Es sagt, ob die Probe antwortet und welche Kerne des RP2350 laufen: runs its Arm cores oder runs its RISC-V cores. Ein neuer Pico 2 lässt oft seine RISC-V-Kerne laufen, oder keine, je nachdem, was in seinem Flash steht. Ein Programm im RAM kann das nicht ändern, denn nach einem Reset startet das Boot-ROM wieder die Kerne, für die das Flash-Programm ist. Lass deshalb zuerst einmal das Flash-Beispiel 08_blink_flash.asm laufen: Wenn Asmonite es schreibt, schaltet es den Chip auf seine Arm-Kerne, und von da an gehen die RAM-Beispiele.

Pico (Cortex-M0+)

Wähl Raspberry Pi Pico in Debug targets. Der RP2040 hat nur Arm-Kerne, es gibt also nichts umzuschalten. Sein Debug-Anschluss hat dieselben drei Pins. Ein Programm im RAM geht beim Pico sogar ganz ohne Probe, über seinen USB-Boot (BOOTSEL), mit Breakpoints und Schritten, nur ohne Pause; das beschreibt die Anleitung picoboot.md.

Blue Pill (STM32F103)

Wähl STM32F103 (Blue Pill) in Debug targets; Test the connection zeigt die Geräte-ID (410 für den STM32F103C8/CB), die Revision und die Größe des Flashs. Umzuschalten gibt es auch hier nichts.

4 Assemblerprogramme schreiben

4.1 Ein erstes Programm

Das kleinste Programm, das im Debugger etwas Sichtbares tut:

01_empty.asm herunterladen
; 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

Zeile für Zeile:

Alles nach einem ; ist ein Kommentar. Groß- und Kleinschreibung spielt für Asmonite bei Befehlen und Labels keine Rolle.

GNU-Assembler

Das ursprüngliche Tutorial schreibt für den GNU-Assembler (as), dessen Syntax sich in Einzelheiten unterscheidet: Kommentare beginnen mit @, die aktuelle Adresse ist . statt $, und es gibt Direktiven wie .syntax unified und .cpu cortex-m3. Die Syntax von Asmonite ist nah an der von NASM; die Kästen wie dieser weisen auf die Unterschiede hin, wo sie wichtig sind.

4.2 Das Programm laden

Speichere das Programm (Strg+S) und drück F7 (Step into). Asmonite assembliert es, verbindet sich mit der Probe, setzt den Kern zurück und hält ihn an, schreibt das Programm ins RAM bei 0x20000000 und hält bei _start: Die Zeile ist im Editor markiert. In den Flash wurde nichts geschrieben; nach einem Reset läuft auf dem Board also wieder, was in seinem Flash steht.

Ein Programm im RAM ist der schnellste Weg, etwas auszuprobieren: Das Laden dauert einen Bruchteil einer Sekunde, und der Flash nutzt sich nicht ab. Ein Programm für den Flash (5.3.3) wird mit derselben Taste dorthin geschrieben und läuft dann auch nach dem Aus- und Einschalten von selbst.

4.3 Den Debugger benutzen

Solange das Programm angehalten ist:

Geh mit F7 durch das erste Programm: Nach nop ist PC um 2 höher (ein 16-Bit-Befehl), und b $ bleibt, wo es ist.

4.4 Prozessorregister

Der Cortex-M hat 16 Register mit 32 Bit:

RegisterZweck
r0 – r12allgemein: Werte, Adressen, Zähler. Viele 16-Bit-Befehle können nur r0 – r7 benutzen (die unteren Register).
r13 = spder Stack-Pointer (6.3)
r14 = lrdas Link-Register: die Rücksprungadresse eines Aufrufs (6.4)
r15 = pcder Programmzähler: die Adresse des Befehls, der gerade ausgeführt wird

Dazu kommt das Programmstatusregister xPSR. Seine obersten vier Bits sind die Flags, die viele Befehle setzen und die bedingten Sprünge prüfen: N (das Ergebnis ist negativ, Bit 31 ist 1), Z (das Ergebnis ist null), C (Carry: ein Überlauf ohne Vorzeichen, oder das hinausgeschobene Bit), V (Überlauf eines Ergebnisses mit Vorzeichen).

Jede Datenverarbeitung geschieht in Registern. Speicher wird nur mit ldr (load register) gelesen und mit str (store register) geschrieben, immer über eine Adresse in einem Register: ldr r0, [r1] liest das Wort an der Adresse in r1 nach r0; str r0, [r1] schreibt r0 dorthin. Das nennt man eine Load/Store-Architektur.

4.5 Auf die Peripherie zugreifen

Jede Peripherie des RP2350 wird über Register an festen Adressen gesteuert: Ein Wort nach 0xd0000018 zu schreiben setzt Ausgangspins, 0x400b0028 zu lesen gibt die Zeit in Mikrosekunden. Für den Kern ist das gewöhnlicher Speicher, gelesen und geschrieben mit ldr und str. Das Datenblatt führt jeden Block mit seiner Basisadresse und den Offsets seiner Register auf.

Damit die LED an GPIO 25 leuchtet, sind vier Blöcke beteiligt:

4.5.1 Blöcke aus dem Reset holen

Nach einem Reset werden die meisten Peripherieblöcke des RP2350 selbst im Reset gehalten, um Strom zu sparen; sie tun nichts und übergehen Schreibzugriffe. Das steuert der Block RESETS (Basis 0x40020000): Jedes Bit seines Registers RESET hält einen Block im Reset, solange es 1 ist. Die GPIO braucht zwei davon: IO_BANK0 (Bit 6) und PADS_BANK0 (Bit 9). Nachdem es die Bits gelöscht hat, wartet das Programm, bis dieselben Bits in RESET_DONE (Offset 8) 1 sind.

(Der STM32F103 des ursprünglichen Tutorials schaltet stattdessen den Takt einer Peripherie ein; die Idee ist dieselbe: Eine Peripherie muss geweckt werden, bevor man sie benutzen kann.)

4.5.2 Den Pin einstellen

Ein Pin des RP2350 geht auf seinem Weg zum Kern durch drei Blöcke:

4.5.3 Einen Pin schreiben

Die SIO hat ein Register für die Ausgabewerte (GPIO_OUT, Offset 0x10) und drei Helfer, die nur die Bits ändern, die im geschriebenen Wert 1 sind: GPIO_OUT_SET (0x18) setzt sie, GPIO_OUT_CLR (0x20) löscht sie, GPIO_OUT_XOR (0x28) kippt sie. Mit Bit 25 in GPIO_OUT_SET geht die LED an. Hier ist alles zusammen:

02_set_pin.asm herunterladen
; 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

Die Adressen und Werte sind 32-Bit-Zahlen, zu groß für einen Befehl. Deshalb stehen sie in einer Tabelle hinter dem Code, jede mit einem Label, und ldr r1, resets_clr lädt das Wort beim Label resets_clr (wie, erklärt 6.10). align 4 sorgt dafür, dass die Tabelle an einer Adresse beginnt, die ein Vielfaches von 4 ist, wie es Wörter brauchen.

Noch etwas vom RP2350: Jedes Register der Peripherie hat drei weitere Adressen, Aliase, die nur einige seiner Bits ändern: +0x1000 kippt (xor) die als 1 geschriebenen Bits, +0x2000 setzt sie, +0x3000 löscht sie. 0x40023000 ist RESET über seinen Lösch-Alias: 0x240 dorthin geschrieben löscht die Bits 6 und 9 und lässt die anderen in Ruhe. Das Pad von GPIO 25 wird genauso gelöscht.

Drück ein paarmal F7 und beobachte die Register; nach dem letzten str leuchtet die LED.

Pico (Cortex-M0+)

Der RP2040 hat andere Adressen: RESETS bei 0x4000c000 mit IO_BANK0 in Bit 5 und PADS_BANK0 in Bit 8, IO_BANK0 bei 0x40014000, und die SIO-Register GPIO_OUT_SET bei 0x14, GPIO_OUT_CLR bei 0x18, GPIO_OUT_XOR bei 0x1c, GPIO_OE_SET bei 0x24. Seine Pads sind nach einem Reset nicht isoliert. Die Aliase sind dieselben. Siehe pico1/05_blink_delay.asm.

Blue Pill (STM32F103)

Der STM32 macht es auf seine Art, der Art des ursprünglichen Tutorials: Eine Peripherie wird geweckt, indem man ihren Takt einschaltet (Port C: Bit 4 von RCC_APB2ENR bei 0x40021018), ein Pin wird mit 4 Bits in GPIOx_CRL (Pins 0 bis 7) oder GPIOx_CRH (Pins 8 bis 15) eingestellt, und GPIOx_BSRR setzt einen Pin mit seinem Bit und löscht ihn mit dem Bit, das 16 höher liegt. Die LED an PC13 leuchtet, wenn der Pin auf low ist. Siehe bluepill/05_blink_delay.asm.

4.6 Datenverarbeitung

Die Rechen- und Logikbefehle haben meist drei Operanden: ein Zielregister und zwei Quellen, von denen die zweite ein Register oder eine Konstante sein kann.

add r0, r1, r2r0 = r1 + r2
sub r0, r1, #4r0 = r1 − 4
and r0, r0, #0xffBits 0 bis 7 behalten, die anderen löschen
orr r0, r0, #4Bit 2 setzen (oder)
bic r0, r0, #0x100Bit 8 löschen (bit clear: und mit der invertierten Konstante)
eor r0, r0, r1exklusives Oder: die Bits kippen, die in r1 1 sind
lsl r0, r1, #2um 2 nach links schieben: r0 = r1 × 4 (lsr, asr: nach rechts, ohne oder mit Vorzeichen)
mov r0, r1kopieren

Ein s am Ende (adds, subs, ands, movs) lässt den Befehl auch die Flags setzen. cmp r0, r1 subtrahiert nur, um die Flags zu setzen, und wirft das Ergebnis weg; tst r0, #4 tut dasselbe mit and.

GNU-Assembler

GNU as nimmt auch zwei Operanden, wenn das Ziel die erste Quelle ist: and r0, #0xfffffff0 für and r0, r0, #0xfffffff0. Asmonite will mit einer Konstanten alle drei geschrieben haben; mit Registern nimmt es beide Formen (ands r2, r0 und ands r2, r2, r0).

4.7 Peripherieregister lesen

Oft sollen sich nur ein paar Bits eines Registers ändern, und die anderen sollen bleiben, wie sie sind. Dann liest das Programm das Register, ändert die Bits in einem Register des Kerns und schreibt das Wort zurück: Read-Modify-Write. Diese Fassung des Programms macht es für RESETS, das Pad und GPIO25_CTRL (dessen Funktionsnummer in den Bits 0 bis 4 steht):

03_set_pin_rmw.asm herunterladen
; 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 braucht drei Befehle und ist nicht sicher, wenn ein Interrupt dasselbe Register dazwischen ändert. Deshalb hat der RP2350 die Setz- und Lösch-Aliase: Ein str ändert genau die gewünschten Bits.

4.8 Sprungbefehle

b label (branch) setzt das Programm beim Label fort. Mit einer Bedingung hinter dem b springt es nur, wenn die Flags zur Bedingung passen, sonst folgt der nächste Befehl:

SprungSpringt, wennFlags
beq / bnegleich / ungleich (null / nicht null)Z = 1 / Z = 0
bhs / bloohne Vorzeichen größer oder gleich / kleinerC = 1 / C = 0
bhi / blsohne Vorzeichen größer / kleiner oder gleich
bge / bltmit Vorzeichen größer oder gleich / kleinerN = V / N ≠ V
bgt / blemit Vorzeichen größer / kleiner oder gleich
bmi / bplnegativ / positiv oder nullN = 1 / N = 0

Die Warteschleife in den Beispielen oben ist so ein Sprung: cmp r2, r0 vergleicht, bne wait_reset springt zurück, solange sie sich unterscheiden. Eine Schleife ohne Bedingung lässt die LED blinken:

04_blink.asm herunterladen
; 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

Sie blinkt so schnell, dass die LED nur schwach zu leuchten scheint; geh mit F7 durch die Schleife, um sie wechseln zu sehen.

4.9 Zählschleifen

Um das Blinken zu verlangsamen, zählt das Programm zwischen den beiden Schreibzugriffen ein Register herunter. subs subtrahiert und setzt das Z-Flag, wenn das Ergebnis 0 ist, bne springt zurück, solange es das nicht ist:

05_blink_delay.asm herunterladen
; 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

Die Schleife braucht ein paar Takte pro Runde; wie lange sie in Sekunden dauert, hängt vom Takt des Kerns ab (6.23). Die Werte, die sich nicht ändern, lassen sich einmal vor der Schleife laden:

06_blink_registers.asm herunterladen
; 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

Hier hält r0 die Basisadresse der SIO, und die Register werden über einen Offset erreicht: str r1, [r0, #0x18] schreibt nach 0xd0000000 + 0x18. Eine Basisadresse für viele Register ist der übliche Weg, mit einer Peripherie zu arbeiten (6.8).

Pico (Cortex-M0+)

Dasselbe Programm für den RP2040, mit seinen Adressen und mit ands r2, r0 (zwei Operanden) statt and r2, r2, r0:

pico1/05_blink_delay.asm herunterladen
; 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)

Fast das Beispiel BlinkDelay des ursprünglichen Tutorials, mit der LED an PC13 statt PA8, im RAM:

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

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

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

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

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

4.10 Das RAM benutzen

Das RAM hält die Variablen eines Programms. Ein Programm, das im RAM läuft, kann sie einfach hinter seinem Code haben: dd 0 reserviert ein Wort (4 Bytes) mit dem Wert 0, dw ein Halbwort (2 Bytes), db ein Byte. adr r0, var1 legt die Adresse eines Labels in ein Register:

07_ram_variables.asm herunterladen
; 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

Stell die Speicheransicht auf var1 und geh Schritt für Schritt: Die Bytes ändern sich mit jedem str. Achte auf die Reihenfolge: Das Wort 0x12345678 wird als 78 56 34 12 gespeichert, das niedrigste Byte zuerst (Little Endian). strb speichert nur das unterste Byte eines Registers.

Ein Programm im Flash kann das nicht, weil sich der Flash nicht wie das RAM beschreiben lässt; seine Variablen brauchen eigene Adressen im RAM (6.19).

5 Speicherverwaltung

5.1 Der Adressraum

Der Cortex-M hat einen Adressraum von 4 GB für alles: Code, Daten und Peripherie. Der RP2350 benutzt diese Teile davon:

AdresseWas
0x00000000das Boot-ROM (32 KB): der Code, der den Chip startet
0x10000000der Flash (4 MB beim Pico 2), gelesen über einen Cache (XIP: execute in place)
0x20000000 – 0x20081fffdas RAM (520 KB)
0x40000000die Peripherie (RESETS, IO_BANK0, PADS_BANK0, CLOCKS, TIMER0 ...)
0xd0000000die SIO
0xe0000000die Register des Kerns selbst (SysTick, NVIC, VTOR ...)

Pico (Cortex-M0+)

Der RP2040 hat 264 KB RAM, von 0x20000000 bis 0x20041fff, und 2 MB Flash. Ein Flash-Programm beginnt mit 256 Bytes boot2-Code, der den Flash-Chip einrichtet; Asmonite schreibt sie für dich mit .boot2 (siehe examples\pico\blink_flash.asm).

Blue Pill (STM32F103)

Der Flash liegt bei 0x08000000 (und nach einem Reset gespiegelt bei 0), das RAM von 0x20000000 bis 0x20004fff (20 KB), die Peripherie ab 0x40000000.

5.2 Der Linker, und warum Asmonite keinen hat

Die GNU-Werkzeuge arbeiten in zwei Schritten. Der Assembler macht aus jeder Quelldatei eine Objektdatei, deren Adressen noch offen sind; der Linker setzt dann die Objektdateien zusammen, entscheidet, wohin jeder Teil im Speicher kommt, und trägt die Adressen ein. Ein Linker-Skript sagt ihm, welchen Speicher der Chip hat und wohin Code, Konstanten und Variablen gehören. Das ursprüngliche Tutorial widmet dem ein Kapitel.

Asmonite geht einen einfacheren Weg: Ein Programm ist eine Datei, und jedes Byte der Ausgabe steht darin, der Reihe nach. .imagebase sagt, wohin das erste Byte kommt; jede weitere Zeile folgt der davor. Die Fragen, die ein Linker-Skript beantwortet, beantwortet also die Quelle selbst:

5.3 Code und Daten platzieren

5.3.1 Speicher reservieren

In einem RAM-Programm werden Variablen dort reserviert, wo sie stehen (dd, db, 4.10). In einem Flash-Programm brauchen sie RAM-Adressen; die bekommen mit equ einen Namen (6.19).

5.3.2 Symbole

Ein Linker-Skript legt Symbole fest, etwa das Ende des Stacks oder den Anfang der Daten. In Asmonite sind das Konstanten (SRAM_END equ 0x20082000, 6.2) oder Differenzen von Labels, die der Assembler selbst ausrechnet: data_end - data_start ist die Größe dessen, was zwischen den beiden Labels steht.

5.3.3 An eine feste Adresse: ein Programm für den Flash

Manches muss an einer festen Stelle stehen. Wenn der RP2350 aus dem Flash startet, sucht sein Boot-ROM am Anfang des Flashs bei 0x10000000 nach zwei Dingen:

Weil ein Programm in Asmonite der Reihe nach geschrieben wird, genügt es, sie zuerst hinzuschreiben:

08_blink_flash.asm herunterladen
; 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 schreibt es in den Flash (nur die Sektoren, die sich geändert haben) und hält bei _start; den Stack-Pointer nimmt der Debugger aus der Vektortabelle. Nach einem Reset oder dem Aus- und Einschalten startet das Programm von selbst. Project > Save as UF2 file macht daraus eine Datei für das BOOTSEL-Laufwerk.

Ließ der Chip vorher seine RISC-V-Kerne laufen, schaltet ihn das Schreiben dieses Programms auf seine Arm-Kerne um (das Ausgabefenster sagt es); von da an gehen auch die RAM-Beispiele.

Blue Pill (STM32F103)

Der STM32 braucht kein IMAGE_DEF: Die Vektortabelle bei 0x08000000 ist alles, wie in den Programmen des ursprünglichen Tutorials. Asmonite schreibt den Flash über den Flash-Controller des Chips (nur die Seiten zu 1 KB, die sich geändert haben), setzt den Chip zurück und hält bei _start:

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

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

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

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

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

hang:
    b    hang

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

5.4 Aufbau eines Programms

Die Beispiele haben von hier an dieselbe Reihenfolge, und die ist eine gute Gewohnheit:

  1. .target, .arch, .imagebase
  2. die Konstanten (equ): Adressen und Bits der Peripherie, Zahlen des Programms
  3. bei einem Flash-Programm oder einem mit Interrupts: die Vektortabelle (und IMAGE_DEF)
  4. _start und das Hauptprogramm
  5. die Funktionen
  6. Tabellen und die Konstanten für ldr, nach align 4
  7. die Variablen

6 Weitere Assembler-Techniken

6.1 Der Befehlssatz-Zustand

Die größeren Arm-Kerne können zwei Befehlssätze ausführen, das 32-Bit-Arm (A32) und Thumb, und Bit 0 einer Sprungadresse wählt, welcher als Nächstes kommt: 1 für Thumb. Ein Cortex-M führt nur Thumb aus, aber die Regel bleibt: Eine Code-Adresse, zu der der Kern über ein Register oder eine Tabelle springt, muss Bit 0 gesetzt haben. Deshalb steht in der Vektortabelle _start + 1, und deshalb zeigt lr nach einem Aufruf eine ungerade Adresse. Ein Sprung an eine gerade Adresse endet auf einem Cortex-M in einem HardFault. Labels bei b, bl, adr brauchen nichts: Darum kümmert sich der Assembler.

6.2 Konstanten

Zahlen wie 0x40020000 sagen einem Leser nichts. name equ wert gibt einer Zahl einen Namen; die Zeile schreibt keine Bytes. Der Name kann überall stehen, wo eine Zahl steht, auch in Ausdrücken mit + - * / << >> & | ^ ~ und Klammern:

09_blink_constants.asm herunterladen
; 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

Zwei Dinge sind zu beachten. Asmonite unterscheidet bei Namen nicht zwischen Groß- und Kleinschreibung, DELAY_COUNT und ein Label delay_count sind also derselbe Name, und das ist ein Fehler. Die Beispiele lassen die Labels der Konstantentabelle deshalb mit lit_ beginnen. Und der Offset in str r0, [r1, #RESETS_RESET] darf 0 sein: Das ist dasselbe wie [r1].

GNU-Assembler

GNU as schreibt NAME = wert oder .equ NAME, wert und unterscheidet Groß- und Kleinschreibung bei Namen.

6.3 Der Stack

Der Stack ist ein Bereich des RAMs für Werte, die eine Weile aufgehoben werden müssen: Rücksprungadressen, gesicherte Register, lokale Variablen. sp zeigt auf den letzten Wert darauf, und er wächst nach unten. push {r4, lr} senkt sp um 8 und speichert die beiden Register dort; pop {r4, lr} lädt sie zurück und hebt sp wieder an. Die Register in den Klammern werden immer in der Reihenfolge ihrer Nummern gespeichert, das niedrigste an der niedrigsten Adresse, egal wie sie geschrieben sind.

Seinen Startwert bekommt der Stack-Pointer aus dem ersten Wort der Vektortabelle. Ein RAM-Programm ohne Vektortabelle bekommt einen vom Debugger (das Ende des RAMs). Beobachte die Stack-Ansicht, während du durch die nächsten Beispiele gehst.

6.4 Funktionsaufrufe

bl name (branch with link) springt in eine Funktion und legt die Adresse des nächsten Befehls in lr; bx lr springt dorthin zurück. Das Blink-Programm in Funktionen aufgeteilt:

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

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

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

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

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

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

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

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

6.4.1 Den Stack für Funktionen benutzen

blink ruft delay auf, und dieses bl überschreibt lr, den Rückweg nach _start. Deshalb legt blink zuerst lr auf den Stack, und am Ende lädt pop {pc} die gesicherte Adresse direkt in den Programmzähler: Das ist der Rücksprung. Mit F8 (Step over) läuft ein ganzer Aufruf auf einmal; F7 geht hinein.

6.4.2 Die Aufrufkonvention

Wenn Funktionen von verschiedenen Leuten geschrieben werden, oder von einem Compiler, müssen sie sich einigen, wo Argumente und Ergebnisse stehen und welche Register eine Funktion ändern darf. Für Arm ist das der AAPCS (Procedure Call Standard for the Arm Architecture):

11_calling_convention.asm herunterladen
; 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

Die Konvention einzuhalten kostet ein paar push, aber es zahlt sich aus: Jede Funktion lässt sich überall benutzen, ohne ihr Inneres zu kennen, und Assembler-Funktionen lassen sich aus C aufrufen und umgekehrt.

Pico (Cortex-M0+)

Die Konvention ist dieselbe, aber push und pop nehmen nur r0 – r7 plus lr oder pc, und ein Offset in str darf höchstens 124 sein, also braucht GPIO25_CTRL (0xcc) seine ganze Adresse:

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

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

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

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

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

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

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

6.5 Bedingte Ausführung

6.5.1 Bedingungen

Die Bedingungen der Sprünge (4.8) sind auch die Bedingungen der bedingten Ausführung: eq ne hs lo mi pl vs vc hi ls ge lt gt le. Welche du nimmst, hängt davon ab, ob die verglichenen Werte ohne Vorzeichen (hs lo hi ls) oder mit Vorzeichen (ge lt gt le) gemeint sind: 0xffffffff ist die größte Zahl ohne Vorzeichen, aber mit Vorzeichen −1.

6.5.2 Der Befehl IT

Ein Sprung um einen einzigen Befehl herum ist Verschwendung. Thumb-2 hat it (if-then): Die nächsten ein bis vier Befehle werden nur ausgeführt, wenn eine Bedingung gilt. it lt umfasst einen Befehl, itt lt zwei, ite lt einen mit der Bedingung und einen mit der umgekehrten (else). Jeder Befehl im Block trägt seine Bedingung im Namen:

14_conditions.asm herunterladen
; 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

Innerhalb eines IT-Blocks setzen movlt und addlo die Flags nicht (kein s), damit alle Befehle des Blocks dieselben Flags sehen.

Pico (Cortex-M0+)

ARMv6-M hat kein it: Ein bedingter Sprung springt stattdessen um den Befehl herum. pico1/18_systick_interrupt.asm zeigt es.

6.6 Rechnen mit 8 und 16 Bit

Die Register sind 32 Bit breit, und gerechnet wird immer mit 32 Bit. Kleinere Werte kommen über die Ladebefehle herein: ldrb und ldrh laden ein Byte oder ein Halbwort und füllen die oberen Bits mit Nullen, ldrsb und ldrsh mit Kopien des Vorzeichenbits. strb und strh speichern die unteren 8 oder 16 Bit, der Rest wird abgeschnitten. uxtb, uxth, sxtb, sxth erweitern genauso von Register zu Register.

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

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

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

6.7 Ausrichtung

Ein Wort sollte an einer Adresse stehen, die ein Vielfaches von 4 ist, ein Halbwort an einer geraden Adresse. Der Cortex-M33 kann nicht ausgerichtete Wörter im RAM lesen und schreiben, langsamer; der M0+ kann es gar nicht (er löst einen HardFault aus), und die Befehle, die mehrere Register laden, und die Literal-Ladebefehle können es auch nicht. Nach Bytes oder Text füllt align 4 daher mit Nullen bis zum nächsten Vielfachen von 4 auf, bevor das nächste Wort kommt. Befehle selbst stehen immer an geraden Adressen.

6.8 Adressierung mit Offset

Die Adresse in einem Lade- oder Speicherbefehl kann mehr Teile haben:

[r0, #8]r0 + 8; r0 bleibt (Offset)
[r0, r1]r0 + r1
[r0, r1, lsl #2]r0 + r1 × 4: Element Nummer r1 eines Arrays von Wörtern
[r0, #4]!r0 + 4, und r0 wird zu dieser Adresse (Pre-Index)
[r0], #4r0; danach r0 = r0 + 4 (Post-Index)

6.9 Arrays durchlaufen

Arrays sind Reihen von Werten im Speicher. Das Blinkmuster SOS (kurz, kurz, kurz, lang, lang, lang, kurz ...) ist eine Tabelle von An- und Aus-Zeiten, gelesen mit einem Index:

12_blink_pattern.asm herunterladen
; 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

Die Zahl der Einträge rechnet der Assembler aus zwei Labels aus. Mit Bytes statt Wörtern ist die Tabelle ein Viertel so groß, und ein Ladebefehl mit Post-Index geht sie durch:

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

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

_start:
    bl   setup_led
    bl   blink_pattern
    b    $

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

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

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

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

6.10 Konstanten laden

Ein Befehl ist 2 oder 4 Bytes lang; eine beliebige 32-Bit-Zahl passt nicht hinein. Es gibt vier Wege drumherum:

6.10.1 Der Befehl mov

movs r0, #200 lädt 0 bis 255. Das 32-Bit-mov nimmt mehr: einen beliebigen 8-Bit-Wert an eine beliebige Stelle geschoben (0x00ab0000, 0x3fc), oder eines der Muster 0x00XY00XY, 0xXY00XY00, 0xXYXYXYXY; mvn lädt das Inverse eines solchen Werts.

6.10.2 Die Befehle movw und movt

movw r0, #0x5678 lädt eine beliebige 16-Bit-Zahl (und löscht die obere Hälfte), movt r0, #0x1234 setzt die obere Hälfte und behält die untere. Zwei Befehle, 8 Bytes, jede Zahl, kein Speicherzugriff.

6.10.3 Laden relativ zum PC

ldr r0, name lädt das Wort beim Label name. Der Befehl enthält nur den Abstand vom Programmzähler zum Wort, also muss die Konstante in der Nähe stehen: bis zu 1020 Bytes voraus bei der 16-Bit-Form, ±4095 Bytes bei der 32-Bit-Form. So machen es alle Beispiele hier. adr r0, name lädt statt des Worts die Adresse des Labels.

16_constants.asm herunterladen
; 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 hat dafür eine Abkürzung: ldr r0, =0x40021018 legt die Zahl selbst in einen Literal-Pool und lädt sie von dort; .ltorg sagt, wohin der Pool kommt. Asmonite hat keine solche Abkürzung: Die Konstanten werden ausgeschrieben, mit ihren Labels, wie jedes andere Byte des Programms. Das ist mehr zu tippen, aber du siehst genau, was im Speicher steht.

6.11 Der Timer SysTick

Eine Zählschleife verschwendet den Kern, und ihre Zeit ändert sich mit dem Takt und dem Code drumherum. Jeder Cortex-M hat dafür einen einfachen Timer, den SysTick: einen 24-Bit-Zähler, der mit dem Takt des Kerns herunterzählt und von einem Nachladewert neu beginnt. Jedes Mal, wenn er durch null geht, setzt er ein Flag (COUNTFLAG, Bit 16 von SYST_CSR), das beim Lesen des Registers gelöscht wird. Seine drei Register liegen im System Control Space des Kerns bei 0xe000e010:

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

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

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

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

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

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

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

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

6.12 Ausnahmen und Interrupts

Ein Flag abzufragen hält den Kern immer noch beschäftigt. Mit einem Interrupt ruft die Hardware von selbst eine Funktion auf, wenn etwas geschieht, und der Kern kann dazwischen schlafen (wfi: wait for interrupt). Beim Cortex-M sind Interrupts eine Art von Ausnahme (Exception); die anderen sind der Reset, Fehler (HardFault und Verwandte) und die Systemaufrufe.

Tritt eine Ausnahme ein, legt der Kern r0 – r3, r12, lr, pc und xPSR selbst auf den Stack, legt einen besonderen Wert in lr und springt an die Adresse in der Vektortabelle: Eintrag 15 für den SysTick, Eintrag 16 + n für Interrupt n der Peripherie. Der Handler ist eine gewöhnliche Funktion nach der Aufrufkonvention, und bx lr mit diesem besonderen Wert lässt den Kern die gesicherten Register zurückholen und dort weitermachen, wo er unterbrochen wurde.

Der Kern findet die Vektortabelle über das Register VTOR (0xe000ed08). Ein Programm im RAM schreibt die Adresse seiner eigenen Tabelle dorthin. Und bevor es schläft, erlaubt es Interrupts mit cpsie i (es löscht PRIMASK, den Schalter, der sie alle sperrt): Nach einem Reset sind sie erlaubt, aber das Boot-ROM kann sie gesperrt lassen, etwa nachdem es den Flash geschrieben hat.

18_systick_interrupt.asm herunterladen
; 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

Setz mit F5 einen Breakpoint in systick_handler und lass mit F9 laufen: Das Programm hält bei jedem Ereignis dort an, und die Stack-Ansicht zeigt die acht Register, die der Kern abgelegt hat.

Pico (Cortex-M0+)

Dasselbe auf dem RP2040: ohne IT-Blöcke, ohne Ladebefehle mit Post-Index und mit weniger Einträgen in der Vektortabelle. adr geht dort nur vorwärts und nur auf Adressen, die Vielfache von 4 sind:

pico1/18_systick_interrupt.asm herunterladen
; 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)

Der Cortex-M3 hat denselben SysTick, dieselbe Vektortabelle, VTOR und IT-Blöcke; das Beispiel BlinkSysTickInterrupt des ursprünglichen Tutorials mit seinem ite eq / ldreq / ldrne, nur die LED an PC13 geschaltet. Während der Kern in wfi schläft, erreicht ihn der Debugger trotzdem: Asmonite setzt die Bits von DBGMCU_CR, die den Debug-Port in den Schlafzuständen am Leben halten.

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

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

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

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

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

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

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

hang:
    b    hang

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

6.13 Makros

GNU as kann Makros definieren: einen Namen für eine Gruppe von Zeilen mit Parametern, die überall dort ausgeschrieben wird, wo der Name steht. Asmonite hat keine Makros (und keinen Präprozessor); es hält sich an den Grundsatz, dass die Quelle jeden Befehl zeigt. Wiederholte Abfolgen werden zu Funktionen, was auch Speicher spart; wiederholte Zahlen werden zu Konstanten.

6.14 Schwache Symbole

Mit dem GNU-Linker kann eine Vektortabelle Handler nennen, die es geben kann oder nicht: Ein schwaches (weak) Symbol bekommt einen Standard (einen leeren Handler), wenn keine andere Datei es definiert. Ein Programm in Asmonite ist eine Datei, also nennt die Vektortabelle einfach den Handler, den es hat, oder hang für alle anderen, wie in den Beispielen.

6.15 Symbol-Aliase

Zwei Namen für dieselbe Adresse: In Asmonite kann eine Konstante auf ein Label gesetzt werden (RESET_HANDLER equ _start), oder zwei Labels stehen in aufeinanderfolgenden Zeilen.

6.16 Eine bessere Vektortabelle

Die Vektortabelle des Cortex-M33 hat 16 Einträge für den Kern und einen für jeden Interrupt des Chips (52 beim RP2350). Die Beispiele schreiben nur so viele Einträge, wie sie brauchen; der letzte ist die höchste Ausnahme, die sie benutzen. Eine vollständige Tabelle für jede Ausnahme des Kerns steht in der Vorlage (6.24), und VTOR will die Tabelle an einer Adresse, die ein Vielfaches von 128 ist (der Anfang des RAMs und des Flashs sind es).

6.17 .include

GNU as kann eine andere Datei in die Quelle einfügen (.include "stm32f103.inc"), was das ursprüngliche Tutorial für die Registerdefinitionen nutzt. Asmonite hat kein Include; jedes Beispiel hier ist in sich vollständig und bringt die Konstanten mit, die es braucht.

6.18 Lokale Labels

GNU as hat nummerierte Labels (1:, b 1b zurück, b 1f vorwärts), die sich immer wieder verwenden lassen. In Asmonite ist jedes Label in einer Datei eindeutig; die Beispiele geben den Labels einer Funktion deren Namen als Vorsilbe (setup_wait, pattern_loop).

6.19 Das RAM vorbereiten

Ein Programm im Flash hat seine Variablen im RAM, und nach dem Einschalten stehen im RAM zufällige Werte. Also richtet das Programm sie an seinem Anfang ein, wie es der Startcode jedes C-Programms tut:

Das Programm unten ist das SysTick-Interrupt-Beispiel für den Flash. Die RAM-Adressen der Variablen sind Konstanten, ausgerechnet aus der Lage ihrer Startwerte im Flash:

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

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

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

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

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

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

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

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

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

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

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

hang:
    b    hang

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

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

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

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

6.20 Interrupts der Peripherie

Auch die Peripherie des Chips hat Interrupts. Drei Schalter müssen an sein, damit einer den Kern erreicht: der Interrupt in der Peripherie selbst, seine Leitung im Interrupt-Controller des Kerns (dem NVIC, Register NVIC_ISER0 bei 0xe000e100, ein Bit pro Interrupt) und sein Eintrag in der Vektortabelle. Hier schaltet der Alarm von TIMER0, einem Mikrosekunden-Timer des RP2350, die LED viermal pro Sekunde um. Seine Mikrosekunden werden aus clk_ref gezählt, das nach einem Reset vom ungenauen Ringoszillator läuft; deshalb startet das Programm zuerst den 12-MHz-Quarz und lässt clk_ref von ihm laufen (die ersten beiden Schritte von 6.23):

20_timer_interrupt.asm herunterladen
; 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

Der Handler muss den Interrupt in der Peripherie löschen (hier: 1 nach INTR schreiben); sonst wird er sofort wieder aufgerufen.

6.21 Werkzeuge zur Analyse

Das ursprüngliche Tutorial stellt die GNU-Werkzeuge vor, mit denen man in ein Programm hineinsieht: den Disassembler objdump, readelf, nm, addr2line, objcopy. In Asmonite ist das meiste davon Teil des Debuggers: Die Speicheransicht mit dem Schalter Code zeigt die Befehle an jeder Adresse, die Register und der Stack sind immer zu sehen, und Project > Save as UF2 file schreibt die Datei für das BOOTSEL-Laufwerk. Für Programme, die du nicht selbst geschrieben hast, bringt Asmonite Dismonite mit, einen Disassembler, der Binärdateien liest und analysiert.

6.22 C-Code anbinden

Assembler und C werden oft gemischt: C für den größten Teil eines Programms, Assembler für ein paar Funktionen. Die Aufrufkonvention (6.4.2) macht das möglich. Asmonite übersetzt kein C für die Arm-Kerne, dieser Teil des ursprünglichen Tutorials hat hier also kein Gegenstück; mit dem Pico SDK lässt sich eine Assembler-Datei (.S) zu einem C-Projekt hinzufügen und ihre Funktionen wie C-Funktionen aufrufen.

6.23 Der Takt

Nach einem Reset läuft der RP2350 von seinem Ringoszillator (ROSC), dessen Frequenz nicht genau ist. Der Pico 2 hat einen 12-MHz-Quarz (XOSC), und aus ihm macht eine PLL die 150 MHz, für die der Chip gebaut ist. Die Schritte: den Quarz starten und warten, bis er stabil ist (etwa 6 ms: Der Quarz des Pico 2 schwingt langsam an, und bei zu kurzem Warten kann der Chip hängen bleiben); clk_ref von ihm laufen lassen; clk_sys auf clk_ref schalten, solange sich die PLL ändert; die PLL auf 12 MHz × 125 = 1500 MHz stellen und durch 5 und durch 2 teilen; clk_sys auf die PLL schalten.

21_clock.asm herunterladen
; 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

Der SysTick zählt den Takt des Kerns, also stimmen die Blinkzeiten dieses Programms jetzt genau: 0,2 s an, 0,8 s aus.

6.24 Eine Programmvorlage

Für den Anfang eigener Programme: eine Vorlage mit einer vollständigen Vektortabelle, dem Start, dem Einrichten der LED und einer Verzögerungsfunktion, bereit, bei main gefüllt zu werden.

22_template.asm herunterladen
; 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

Dank und Quellen

Dieses Tutorial ist eine Bearbeitung des ARM-ASM-Tutorials von Niklas Gürtler auf mikrocontroller.net (Diskussion: Forumsthread, Code: github.com/Erlkoenig90/ArmAsmTutorial). Der Aufbau und der Verlauf der Beispiele folgen seinem Tutorial; der Text ist nicht seiner: Er ist neu geschrieben für den Raspberry Pi Pico 2 und Asmonite und mit seiner freundlichen Erlaubnis veröffentlicht. Die Beispielprogramme sind nach seinen unter der MIT-Lizenz angepasst: LICENSE.txt.