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.
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.
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.
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).
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.
Asmonite hat eine Befehlsreferenz eingebaut: Setz den Cursor auf einen Befehl und drück F1.
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.
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).
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.
Ö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.
Das kleinste Programm, das im Debugger etwas Sichtbares tut:
; 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:
.target pico2 sagt, für welches Board das Programm ist. Asmonite prüft das Programm nach dem Assemblieren dagegen (der richtige Befehlssatz, eine Adresse, die das Board hat, die Größe) und weigert sich, es auf ein anderes Board zu schreiben..arch thumb wählt den Befehlssatz. Ein Cortex-M führt nur Thumb-Code aus..imagebase 0x20000000 ist die Adresse, an die das erste Byte des Programms kommt: der Anfang des RAMs. Labels im Programm bekommen ihre Adressen von dort an._start: ist ein Label, ein Name für die Adresse der nächsten Zeile. Der Debugger startet das Programm bei _start.nop (no operation) tut nichts, und b $ ist ein Sprung nach $, der Adresse der Zeile selbst: eine Endlosschleife. Ein Mikrocontroller-Programm endet nie; es gibt nichts, wohin es zurückkehren könnte.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.
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.
Solange das Programm angehalten ist:
R0 bis R12, SP, LR, PC und XPSR mit seinen Flags. Ein Wert, der sich mit dem letzten Schritt geändert hat, ist hervorgehoben. Du kannst einen neuen Wert in ein Register tippen.SP aufwärts.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.
Der Cortex-M hat 16 Register mit 32 Bit:
| Register | Zweck |
|---|---|
r0 – r12 | allgemein: Werte, Adressen, Zähler. Viele 16-Bit-Befehle können nur r0 – r7 benutzen (die unteren Register). |
r13 = sp | der Stack-Pointer (6.3) |
r14 = lr | das Link-Register: die Rücksprungadresse eines Aufrufs (6.4) |
r15 = pc | der 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.
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:
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.)
Ein Pin des RP2350 geht auf seinem Weg zum Kern durch drei Blöcke:
0x40038000): die elektrische Seite jedes Pins: Treiberstärke, Pull-ups, Eingang ein. Beim RP2350 beginnt jedes Pad isoliert (Bit 8, ISO, seines Registers); das Programm löscht es. GPIO 25 hat das Register bei Offset 0x68.0x40028000): welche Peripherie den Pin treibt. Sein Register GPIO25_CTRL (Offset 0xcc) bekommt die Funktionsnummer 5, die SIO.0xd0000000): die einfache Ein- und Ausgabe, die die Kerne in einem Takt erreichen. GPIO_OE_SET (Offset 0x38) schaltet den Ausgangstreiber der Pins ein, deren Bits 1 sind.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:
; 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.
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, r2 | r0 = r1 + r2 |
sub r0, r1, #4 | r0 = r1 − 4 |
and r0, r0, #0xff | Bits 0 bis 7 behalten, die anderen löschen |
orr r0, r0, #4 | Bit 2 setzen (oder) |
bic r0, r0, #0x100 | Bit 8 löschen (bit clear: und mit der invertierten Konstante) |
eor r0, r0, r1 | exklusives Oder: die Bits kippen, die in r1 1 sind |
lsl r0, r1, #2 | um 2 nach links schieben: r0 = r1 × 4 (lsr, asr: nach rechts, ohne oder mit Vorzeichen) |
mov r0, r1 | kopieren |
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).
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):
; 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.
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:
| Sprung | Springt, wenn | Flags |
|---|---|---|
beq / bne | gleich / ungleich (null / nicht null) | Z = 1 / Z = 0 |
bhs / blo | ohne Vorzeichen größer oder gleich / kleiner | C = 1 / C = 0 |
bhi / bls | ohne Vorzeichen größer / kleiner oder gleich | |
bge / blt | mit Vorzeichen größer oder gleich / kleiner | N = V / N ≠ V |
bgt / ble | mit Vorzeichen größer / kleiner oder gleich | |
bmi / bpl | negativ / positiv oder null | N = 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:
; 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.
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:
; 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:
; 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:
; 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:
; 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
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:
; 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).
Der Cortex-M hat einen Adressraum von 4 GB für alles: Code, Daten und Peripherie. Der RP2350 benutzt diese Teile davon:
| Adresse | Was |
|---|---|
0x00000000 | das Boot-ROM (32 KB): der Code, der den Chip startet |
0x10000000 | der Flash (4 MB beim Pico 2), gelesen über einen Cache (XIP: execute in place) |
0x20000000 – 0x20081fff | das RAM (520 KB) |
0x40000000 | die Peripherie (RESETS, IO_BANK0, PADS_BANK0, CLOCKS, TIMER0 ...) |
0xd0000000 | die SIO |
0xe0000000 | die 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.
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:
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).
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.
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:
_start + 1), weil der Kern Thumb-Code ausführt (6.1).Weil ein Programm in Asmonite der Reihe nach geschrieben wird, genügt es, sie zuerst hinzuschreiben:
; 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:
; 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
Die Beispiele haben von hier an dieselbe Reihenfolge, und die ist eine gute Gewohnheit:
.target, .arch, .imagebaseequ): Adressen und Bits der Peripherie, Zahlen des Programms_start und das Hauptprogrammldr, nach align 4Die 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.
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:
; 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.
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.
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:
; ARM assembly tutorial for Asmonite, example 10: functions (Raspberry Pi Pico 2, Arm Cortex-M33). ; Adapted from the ARM-ASM-Tutorial by Niklas Guertler (examples OffsetAddressing and BlinkFunctions); ; see LICENSE.txt. ; ; "bl name" (branch with link) jumps to a function and puts the address behind it into lr; "bx lr" jumps ; back there. A function that calls another one must save lr first ("push {lr}") and take it back at the ; end ("pop {pc}" puts it straight into pc: that is the return). The LED blinks 5 times, then the program ; waits in "b $". F8 (Step over) runs a whole call; F7 goes into it. .target pico2 .arch thumb .imagebase 0x20000000 RESETS_BASE equ 0x40020000 RESETS_RESET_DONE equ 0x08 RESET_BITS equ (1 << 6) | (1 << 9) ; IO_BANK0, PADS_BANK0 IO_BANK0_BASE equ 0x40028000 GPIO25_CTRL equ 0xcc FUNCSEL_SIO equ 5 PADS_BANK0_BASE equ 0x40038000 PADS_GPIO25 equ 0x68 PAD_ISO equ 1 << 8 SIO_BASE equ 0xd0000000 SIO_GPIO_OUT_SET equ 0x18 SIO_GPIO_OUT_CLR equ 0x20 SIO_GPIO_OE_SET equ 0x38 ALIAS_CLR equ 0x3000 DELAY_COUNT equ 1000000 _start: bl enable_gpio ; IO_BANK0 and PADS_BANK0 out of reset bl configure_led ; GPIO 25: SIO, output movs r5, #5 ; r5 = number of flashes bl blink b $ ; ---- blink r5 times; changes r0 .. r5 ---- blink: push {lr} ; blink calls delay: keep its own return address ldr r0, lit_sio ldr r1, lit_led_bit ldr r3, lit_delay_value blink_loop: str r1, [r0, #SIO_GPIO_OUT_SET] bl delay str r1, [r0, #SIO_GPIO_OUT_CLR] bl delay subs r5, r5, #1 bne blink_loop pop {pc} ; return ; ---- take IO_BANK0 and PADS_BANK0 out of reset; changes r0 .. r2 ---- enable_gpio: ldr r1, lit_resets_clr ldr r0, lit_reset_bits str r0, [r1] ; RESETS.RESET through its clear alias ldr r1, lit_resets enable_wait: ldr r2, [r1, #RESETS_RESET_DONE] and r2, r2, r0 cmp r2, r0 bne enable_wait bx lr ; ---- GPIO 25: no pad isolation, function SIO, output; changes r0, r1 ---- configure_led: ldr r1, lit_pads_clr mov r0, #PAD_ISO str r0, [r1, #PADS_GPIO25] ldr r1, lit_io_bank0 movs r0, #FUNCSEL_SIO str r0, [r1, #GPIO25_CTRL] ldr r1, lit_sio ldr r0, lit_led_bit str r0, [r1, #SIO_GPIO_OE_SET] bx lr ; ---- wait: count r3 down in r4; changes r4 ---- delay: mov r4, r3 delay_loop: subs r4, r4, #1 bne delay_loop bx lr align 4 lit_resets: dd RESETS_BASE lit_resets_clr: dd RESETS_BASE + ALIAS_CLR lit_reset_bits: dd RESET_BITS lit_pads_clr: dd PADS_BANK0_BASE + ALIAS_CLR lit_io_bank0: dd IO_BANK0_BASE lit_sio: dd SIO_BASE lit_led_bit: dd 1 << 25 lit_delay_value: dd DELAY_COUNT
blink 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.
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):
r0 – r3, das Ergebnis kommt in r0 zurück (ein 64-Bit-Ergebnis in r0 und r1).r0 – r3 und r12 darf eine Funktion ändern; der Aufrufer darf sich nach einem Aufruf nicht auf sie verlassen.r4 – r11 müssen ihre alten Werte haben, wenn die Funktion zurückkehrt. Eine Funktion, die sie benutzt, legt sie am Anfang auf den Stack und holt sie am Ende zurück.sp ein Vielfaches von 8: eine gerade Zahl von Registern auf den Stack legen und zurückholen.; 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:
; 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
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.
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:
; 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.
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.
; 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
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.
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], #4 | r0; danach r0 = r0 + 4 (Post-Index) |
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:
; 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:
; 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
Ein Befehl ist 2 oder 4 Bytes lang; eine beliebige 32-Bit-Zahl passt nicht hinein. Es gibt vier Wege drumherum:
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.
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.
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.
; 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.
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:
; 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
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.
; 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:
; 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.
; ARM assembly tutorial for Asmonite, example 18 for the Blue Pill (STM32F103, Arm Cortex-M3). ; Adapted from the ARM-ASM-Tutorial by Niklas Guertler (example BlinkSysTickInterrupt); see ..\LICENSE.txt. ; ; The SysTick interrupt example on the chip of the original tutorial. The SysTick, the vector table, VTOR and ; the IT blocks are the same as on the Pico 2 (the Cortex-M3 and the Cortex-M33 share them); only the LED is ; switched the way of the STM32: GPIOC_BSRR, bit 29 clears PC13 (LED on), bit 13 sets it (LED off). ; The program runs in the RAM; the core runs from the internal 8 MHz oscillator (HSI) after a reset, so an ; event of the SysTick (1500000 cycles) is about 0.19 s. .target stm32f103 .arch thumb .imagebase 0x20000000 RCC_APB2ENR equ 0x40021018 IOPCEN equ 1 << 4 GPIOC_BASE equ 0x40011000 GPIO_CRH equ 0x04 GPIO_BSRR equ 0x10 LED_ON equ 1 << 29 ; BR13: PC13 low LED_OFF equ 1 << 13 ; BS13: PC13 high SCS_BASE equ 0xe000e000 SYST_CSR equ 0x10 SYST_RVR equ 0x14 SYST_CVR equ 0x18 SCB_VTOR equ 0xd08 TIMER_VALUE equ 1500000 SRAM_END equ 0x20005000 vectors: dd SRAM_END ; 0: the stack pointer at the start dd _start + 1 ; 1: reset dd hang + 1 ; 2: NMI dd hang + 1 ; 3: HardFault dd hang + 1 ; 4: MemManage dd hang + 1 ; 5: BusFault dd hang + 1 ; 6: UsageFault dd 0, 0, 0, 0 ; 7 .. 10: reserved dd hang + 1 ; 11: SVCall dd hang + 1 ; 12: DebugMonitor dd 0 ; 13: reserved dd hang + 1 ; 14: PendSV dd systick_handler + 1 ; 15: SysTick _start: ldr r0, lit_scs adr r1, vectors str r1, [r0, #SCB_VTOR] ; the vector table is ours adr r0, blink_step movs r1, #0 strb r1, [r0] ; blink_step = 0 adr r2, blink_table ldrb r1, [r2] adr r0, timer_events strb r1, [r0] ; timer_events = the first entry of the table bl setup_led ldr r0, lit_gpioc ldr r1, lit_led_on str r1, [r0, #GPIO_BSRR] ; the first step: LED on ldr r0, lit_timer_value bl start_systick cpsie i ; allow interrupts (PRIMASK = 0) sleep_loop: wfi ; sleep until an interrupt b sleep_loop ; ---- called by the SysTick at every event ---- systick_handler: adr r0, timer_events ldrb r3, [r0] subs r3, r3, #1 ; one event less itt ne strbne r3, [r0] ; not yet 0: store it bne handler_done ; and return adr r0, blink_step ldrb r1, [r0] adds r1, r1, #1 ; the next entry cmp r1, #(blink_table_end - blink_table) it hs movhs r1, #0 ; after the last one: from the start again strb r1, [r0] adr r2, blink_table ldrb r3, [r2, r1] ; its number of events adr r0, timer_events strb r3, [r0] tst r1, #1 ; even entry: on, odd entry: off ite eq ldreq r2, lit_led_on ldrne r2, lit_led_off ldr r0, lit_gpioc str r2, [r0, #GPIO_BSRR] handler_done: bx lr ; ---- start the SysTick with its interrupt: r0 = cycles between two events ---- start_systick: ldr r1, lit_scs str r0, [r1, #SYST_RVR] movs r0, #0 str r0, [r1, #SYST_CVR] movs r0, #7 ; ENABLE | TICKINT | CLKSOURCE str r0, [r1, #SYST_CSR] bx lr ; ---- the clock of port C on, PC13 an output (push-pull, 2 MHz); changes r0, r1 ---- setup_led: ldr r1, lit_rcc_apb2enr ldr r0, [r1] orr r0, r0, #IOPCEN str r0, [r1] ldr r1, lit_gpioc ldr r0, [r1, #GPIO_CRH] bic r0, r0, #0x00f00000 orr r0, r0, #0x00200000 str r0, [r1, #GPIO_CRH] bx lr hang: b hang blink_table: db 2, 2, 2, 2, 2, 2 db 5, 2, 5, 2, 5, 2 db 2, 2, 2, 2, 2, 8 blink_table_end: blink_step: db 0 timer_events: db 0 align 4 lit_rcc_apb2enr: dd RCC_APB2ENR lit_gpioc: dd GPIOC_BASE lit_led_on: dd LED_ON lit_led_off: dd LED_OFF lit_scs: dd SCS_BASE lit_timer_value: dd TIMER_VALUE
GNU as 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.
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.
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.
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).
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.
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).
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:
; 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
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):
; 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.
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.
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.
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.
; 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.
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.
; 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
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.