STM32 Anleitungen
Technische Daten, Hinweise und Programmierbeispiele für STM32 Mikrocontroller. Diese Webseiten sollen dabei helfen, von einem anderen Mikrocontroller nach STM32 zu wechseln. Ich zeige kleine Code-Beispiele, die du mit der Dokumentation des Herstellers vergleichen kannst, um dich an dessen Stil zu gewöhnen.
Einsteigern empfehle ich immer noch, mit kleineren 8 Bit Mikrocontrollern anzufangen. Denn bei STM32 sind Funktionsumfang und Dokumentation (einige tausend Seiten!) überwältigend. Außerdem kommen ständig neue Modelle dazu.
STM32 Mikrocontroller werden üblicherweise in C oder C++ programmiert. Andere Programmiersprachen thematisiere ich hier nicht. Die STM32 Mikrocontroller funktionieren alle ähnlich, doch es gibt so viele Unterschiede im Detail, dass ich spezifische Seiten erstellen musste (es wäre sonst unübersichtlich geworden). Ich kenne die folgenden Serien:
- STM32L0 ARM Cortex M0+ bis 32 MHz, mit EEPROM
- STM32F1 ARM Cortex M3 bis 72 MHz, alt
- STM32F3 ARM Cortex M4F bis 72 MHz
- STM32G4 ARM Cortex M4F bis 170 MHz, stark und sparsam
Weitere Anleitungen:
- Einblick in die moderne Elektronik ohne viel Theorie (von mir, für STM32F103 und STM32F303)
- STM32 tutorials (Prof. Laurent Latorre, für STM32F072)
- A bare metal programming guide (Sergey Lyubka, für STM32F429)
Familien und Serien
(Ultra) Low Power Familie1
|
Hinter der Modellnummer: | |
|
Anzahl der Pins J = 8 Pins F = 20 Pins E = 24 Pins G = 28 pins K = 32 Pins T = 36 Pins C = 48 Pins R = 64/68 Pins M = 80 Pins V = 100 Pins P = 121 Pins Q = 128 Pins Z = 144 Pins A = 169 Pins I = 176 Pins B = 208 Pins N = 216 Pins L = 225 Pins X = 240/256 Pins |
||
|
Flash Größe 4 = 16 KiB 6 = 32 KiB 8 = 64 KiB B = 128 KiB Z = 192 KiB C = 256 KiB D = 384 KiB E = 512 KiB F = 768 KiB G = 1024 KiB H = 1536 KiB I = 2048 KiB |
||
|
Gehäuse P = T = U,Y = M = SON H,I,J,K = |
Temperaturbereich 6 = -40 bis +85 °C 7 = -40 bis +105 °C 3 = -40 bis +125 °C |
|
- Die alten L-Serien waren früher mal besonders sparsam, doch im Vergleich zu aktuellen (G0, G4, C0, C5, H5) sind sie es nicht mehr. Nur die neuen U-Serien sind noch sparsamer.
Zum Beispiel stehen die Buchstaben beim rechts gezeigten STM32F407VGT6 für:
- V = 100 Pins
- G = 1024 KiB Flash
- T = QFP Gehäuse
- 6 = -40 bis +85 °C
Der Product selector soll bei der Auswahl eines geeigneten Modells helfen.
Software
Entwicklungsumgebungen
- STM32 Cube IDE empfohlen für den Anfang, Hinweise zur IDE
- Arduino IDE für das Arduino Framework
- Arm Keil MDK für kommerzielle Nutzung kostenpflichtig, nur Windows
- IAR Embedded Workbench für alle kostenpflichtig, nur Windows
- Segger Embedded Studio für kommerzielle Nutzung kostenpflichtig
- VisualGDB Plugin für Microsoft Visual Studio, nur Windows
- stm32-for-vscode Plugin für Visual Studio Code
Tools
- STM32 Cube Programmer zum Flashen über JTAG, SWD, UART, USB, I²C, SPI, und CAN
- STM32 Cube MX Code-Generator für das Cube HAL Framework
- ST-Link Utility zum Anzeigen von Trace Meldungen und Flashen mit ST-Link Adapter, veraltet, nur Windows
- Flash Loader Demonstrator zum Flashen über den seriellen Bootloader, veraltet, nur Windows
- DfuSe zum Flashen über den USB Bootloader, veraltet, nur Windows
- ST-Link Tools Open-source Alternative zum ST-Link Utility
- STM32 Flash für den seriellen Bootloader
Bibliotheken
Die Basis-Library für alle ARM Cortex-M Mikrocontroller heisst CMSIS Core. Sie enthält Definitionen für die Register vom ARM Kern (Adressen und Bits), sowie ein paar Hilfsfunktionen zu dessen Konfiguration. Die CMSIS wird von allen Chip Herstellern um sogenannte "device files" ergänzt. Darin befinden sich Definitionen für die Peripherie, die der Chip-Hersteller um den ARM Kern herum gebaut hat.
Download: CMSIS Paket für alle oben genannten STM32 Serien aus den "STM32Cube MCU Paketen" extrahiert. Zuletzt aktualisiert am 07.03.2026.
Darauf aufbauend stellt die Firma ST ihre Low-Level (LL) Bibliothek bereit, welche die Zugriffe auf einzelne Register in Strukturen bündelt und dazu passende Hilfsfunktionen bereit stellt. Außerdem gibt es das Cube HAL Framework, das die Wiederverwendbarkeit von Code beim Wechsel auf andere STM32 Modelle erleichtern "soll" (tut es meiner Meinung nach nicht). Dazu gehört das Programm Cube MX, womit man Quelltext-Projekte einschließlich Code zur Konfiguration der Peripherie und Taktversorgung erzeugt. Leider ist die Cube HAL nur sehr spärlich dokumentiert.
Das Arduino Framework legt eine weitere Abstraktions-Schicht über LL und Cube HAL, um die Programmierung unterschiedlicher Mikrocontroller von vielen Herstellern zu vereinheitlichen. Dazu muss der Arduino Core für STM32 installiert werden. Leider ist der Arduino Core von ST weitgehend undokumentiert.
Schließlich gibt es noch die Standard Bibliotheken der Programmiersprachen C und C++, die der Compiler mit sich bringt.
⚠️ Ich empfehle, am Anfang mit der CMSIS ohne Framework zu arbeiten, um die Grundlagen zu lernen. Danach versteht man die knapp Dokumentierten Frameworks viel besser. Meine Code Beispiele in den gelben Kästen sind für CMSIS Projekte ohne Framework geschrieben, außer in den wenigen Kapiteln wo es ausdrücklich um Cube HAL order Arduino geht.
Newlib
Dieses Kapitel gilt für den arm-gcc. Die Entwicklungsumgebungen Arm Keil und IAR benutzen einen anderen Compiler.
Der arm-gcc bringt standardmäßig zwei C-Bibliotheken mit: Newlib ist die Standard-C Bibliothek von Linux, während die newlib-nano für Mikrocontroller optimiert wurde. Du kannst eine Menge Speicherplatz sparen, indem du auf die nano Version der Library wechselst. Dazu dient das Linker-Flag -specs=nano.specs, welches die Cube IDE bei neuen Projekten automatisch einstellt.
Wenn man Ein-/Ausgabe Funktionen benutzt müssen entsprechende Low-Level Funktionen implementiert werden. Zum Beispiel beruhen die C Funktionen puts(), printf() und putchar() intern alle auf _write(). Letztere bestimmt, wohin die Ausgabe geschrieben wird und ist natürlich projektspezifisch. Diese Funktionen sind in System Calls dokumentiert. Jetzt gibt es drei Möglichkeiten, damit umzugehen:
- Man programmiert alle benötigten System Calls selber. Der Linker teilt dir mit, welche das sind, z.B.: "undefined reference to _write".
- Man benutzt zusätzlich das Linker-Flag -specs=nosys.specs.
Dadurch wird die Bibliothek libnosys.a eingebunden, die für alle System Calls eine minimale
Implementierung enthält. Du musst dann nur noch die Funktionen überschreiben, die mehr
als "nichts" tun sollen.
Der Linker gibt evtl. Warnungen wie "_close is not implemented and will always fail" aus. - Die Cube IDE generiert die Dateien syscalls.c und sysmem.c mit minimalen Implementierungen. Sie zu editieren ist selten nötig, denn _read() und _write() kannst du in einer eigenen Datei überschreiben.
Beispiel für eine eigene _write() Funktion, die Textausgabe mittels puts(), printf() und putchar() ermöglicht:
int _write(int file, char *ptr, int len) {
for (int i=0; i<len; i++) {
char c=*ptr++;
// hier das Zeichen c irgendwo ausgeben
// z.B. auf einer seriellen Schnittstelle
}
return len;
}
Der RAM Bedarf von printf() ist unabhängig von der Anzahl der Argumente und Formatier-Optionen. Wenn weniger als 1468 Bytes Heap zur Verfügung stehen, belegt die Library stattdessen nur 436 Bytes und gibt dann jedes Zeichen einzeln mit _write() aus. Wenn weniger als 436 Bytes Heap zur Verfügung stehen, bricht die Funktionen mit einer HardFault Exception ab.
Bei der Ausgabe von Text mit printf() und putchar() ist zu beachten, dass die Zeichen in einem Puffer gesammelt werden, bis dieser entweder voll ist oder ein Zeilenvorschub (\n) erfolgt. Mit fflush(stdout) kann man erzwingen, dass die Ausgabe sofort erfolgt. puts() ist nicht betroffen, weil es immer einen Zeilenvorschub anhängt.
Wenn man Fließkommazahlen ausgeben möchte, muss man bei der newlib-nano zusätzlich die Option -u _printf_float angeben. Das kostet zusätzlich rund 9 KiB Flash Speicher. Für das Parsen von Fließkommazahlen benötigt man die Option -u _scanf_float.
Programmier- und Debug-Schnittstellen
SWJ Debug Port
Mit dem SWJ Debug Port übertragt man das fertige Programm in den Mikrocontroller und kann es im laufenden Betrieb untersuchen. Zum Beispiel kann man das Programm jederzeit anhalten und dann den Inhalt des Speichers anschauen. Wenn es hängt, kann der Debugger anzeigen, wo das passiert. Der Debugger kann sogar melden, wenn ausgewählte Variablen verändert werden.
Die SWJ Schnittstelle ist nach dem Reset standardmäßig aktiviert, kann jedoch per Software deaktiviert werden, um mehr freie Pins zu erhalten. Die Schnittstelle funktioniert nicht im Stop, Standby und Sleep Modus!
Die SWJ Schnittstelle unterstützt zwei Übertragungsprotokolle: JTAG und SWD. Das neuere SWD Protokoll wird bevorzugt, da es schneller ist und nur drei Leitungen benötigt: GND, SWDIO und SWCLK.
STLINK Adapter
Der dazu passende Programmiersadapter von ST heisst "ST-Link". Man kann ihn in verschiedenen Versionen kaufen, benötigt wird mindestens Version 2.0. Obwohl SWD und JTAG von ARM standardisiert sind, kann der ST-Link nur Mikrocontroller von ST programmieren. Eine flexiblere Alternative sind die J-Link Produkte von Segger.
Spartipp: Die ST-Link Adapter von alten Nucleo-64 Boards können externe Mikrocontroller programmieren. Dazu zieht man die beiden eingekreisten Jumper ab, wodurch die Leitungen SWCLK und SWDIO zum Target unterbrochen werden. Man kann den ST-Link sogar an den Sollbruchstellen abschneiden und dann ganz unabhängig benutzen:
Er ist folgendermaßen mit dem Mikrocontroller verbunden:
| ST-Link CN4 | Mikrocontroller | Beschreibung | ||
|---|---|---|---|---|
| Pin 1 | ← | VDD | Misst die Spannungsversorgung der Zielschaltung, optional | |
| Pin 2 | → | SWCLK | PA14 | Serial Wire Clock |
| Pin 3 | GND | Common Ground (Masse) | ||
| Pin 4 | ↔ | SWDIO | PA13 | Serial Wire Data In and Out |
| Pin 5 | → | NRST | Reset Signal, optional siehe Verbindungsoptionen in der Cube IDE | |
| Pin 6 | ← | TRACESWO | PB3 | Serial Wire Output, optional siehe Trace Meldungen ausgeben |
Außerdem enthalten diese ST-Link Platinen auch einen USB-UART Adapter mit den Anschlüssen Tx und Rx.
Viele Windows Programme benötigen den libusb-win32 Treiber, um den ST-Link anzusteuern.
Firmware-Updates kann man mit dem STM32 Cube Programmer beziehen und installieren. Sie beheben Fehler und unterstützen neue Mikrocontroller Modelle. Stecken Sie dazu den ST-Link ohne Verbindung zum Target an den PC. Auch die chinesischen Nachbauten lassen sich mit dem STM32 Cube Programmer aktualisieren.
Bei den unten gezeigten ST-Link v2 Sticks empfehle ich, die Rückseite der Platine innen mit Pappkarton abzudecken, damit kein Kurzschluss zum Aluminium-Gehäuse entsteht. Man kann sie einfach auseinander ziehen. Der Reset-Ausgang dieser Sticks funktioniert nur mit STM8! Wer eine ruhige Hand hat, kann sich eine TRACESWO Leitung nachrüsten:
Trace Meldungen ausgeben
Mikrocontroller mit Cortex-M3 und höher können auf dem TRACESWO Pin (auch SWO "Serial Wire Output" genannt) Diagnose Meldungen ausgeben. Diese Schnittstelle ist schneller als USART und hat einen kleinen Puffer (min. 10 Bytes).
Der TRACESWO Ausgang wird mit dem Debugger aktiviert. Dann ist der Pin (PB3) vorübergehend ein Ausgang mit Ruhe-Pegel High. Während der Datenübertragung liefert er Low-Impulse. Das entsprechende Programm zur Anzeige wird "Serial Wire Viewer" (SWV) genannt.
Die CMSIS Funktion ITM_SendChar() gibt ein Zeichen auf der SWO Leitung aus. ITM bedeutet "Instrumentation Trace Message". Das folgende Beispielprogramm gibt wiederholt die Zeichenkette "Hello World!" aus:
#include "stm32g4xx.h"
// Delay loop for the default 16 MHz
void delay(uint32_t msec) {
for (uint32_t j=0; j < msec * 3200; j++) {
__NOP();
}
}
// Output a trace message
void ITM_SendString(char *ptr) {
while (*ptr) {
ITM_SendChar(*ptr);
ptr++;
}
}
int main() {
while (1) {
ITM_SendString("Hello World!\n");
delay(500);
}
}
Siehe auch meine Hinweise zur Cube IDE, wie man die Meldungen anzeigt.
Boot Loader
Neben der SWJ Schnittstelle haben alle STM32 auch einen unveränderlichen Bootloader, über den man Programme hochladen kann. Er ermöglicht Zugriff auf den Flash Speicher, das RAM und die Option Bytes (auch bekannt als "Fuses"). Zum Debuggen eignet er sich nicht.
Der Bootloader unterstützt mindestens eine serielle Schnittstelle. Je nach Modell auch I²C, SPI, CAN und USB. Er wird aktiviert, indem man den Boot0 Pin auf High setzt und dann den Reset Taster drückt. Mehr dazu in der Application Note AN2606.
Serieller Bootloader
Die Verbindung zum PC wird mit einem USB-UART Adapter hergestellt. Folgende Verbindungen sind nötig:
| PC | Mikrocontroller | Beschreibung | |
|---|---|---|---|
| TxD | → | RxD | Daten |
| RxD | ← | TxD | Daten |
| GND | GND | Gemeinsame Masse |
Der Bootloader erkennt die Baudrate automatisch. Es werden 8 Datenbits und gerade Parität (even) verwendet.
USB Bootloader
Am USB Anschluss (meist PA11 und PA12) verwendet der Bootloader das DFU Protokoll. Unter Linux muss man die Software mit sudo starten, damit man darauf zugreifen darf. Oder man legt die Datei /etc/udev/rules.d/49-dfu.rules an, in der folgendes steht:
SUBSYSTEMS=="usb", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="df11", MODE="660", GROUP="plugdev", TAG+="uaccess"
GCC Optionen
Dieses Kapitel gilt für den arm-gcc. Die Entwicklungsumgebungen Arm Keil und IAR benutzen einen anderen Compiler.Assembler Listing
Wenn du sehen willst, welchen Assembler-Code der Compiler erzeugt, benutze die Compiler-Optionen -g -Wa,-adhlns="$(@:%.o=%.lst)".
Du findest dann für jede Quell-Datei eine *.lst Datei im Verzeichnis Debug oder Release.
Optimierungen
In den Einstellungen des Compilers kann man beeinflussen, mit welcher Strategie der Compiler das Programm optimiert.
| Option | Beschreibung |
|---|---|
| -O0 | Keine Optimierung. Der Assembler-Code entspricht genau dem C-Code. Aber das Programm läuft viel langsamer, als mit Optimierung. |
| -Og | Das Programm wird ein bisschen optimiert, ohne den Debugger zu beeinträchtigen. |
| -O1 | Nur einfache Optimierungen, der Assembler-Code entspricht strukturell weitgehend dem Quelltext. |
| -Os | Möglichst geringe Code-Größe. |
| -O2 | Gute Geschwindigkeit. |
| -O3 | Beste Geschwindigkeit, unter Umständen wird der Code aber viel größer. |
Optimierter Code stimmt stellenweise nicht mehr mit dem C-Quelltext überein, was die Benutzung des Debuggers beeinträchtigt. Zum Beispiel kann der Debugger nur Variablen anzeigen, die eine Adresse im RAM haben, aber der optimierende Compiler bevorzugt CPU Register. Der Compiler ersetzt manchmal ganze Prozeduren durch inline Code, und Schleifen durch völlig anderen Code, der das gleiche Ergebnis produziert.
Damit der Debugger funktioniert, braucht man die Option -g. Sie veranlasst den Compiler dazu, Informationen für den Debugger in die *.elf Datei zu schreiben. Auf die Geschwindigkeit und Größe im Flash hat -g keinen Einfluss.
Ich bevorzuge -O1 -g weil der so erzeugte Code nur marginal langsamer ist, als in den höheren Stufen. Der Code lässt sich dennoch weitgehend debuggen und die Assembler Listings sind überschaubar. Weniger gut finde ich die Vorgabe der IDE, wo zwischen Debug- und Release-Modus unterschieden wird. Ich möchte nicht wochenlang eine Debug-Version des Programms testen, um am Ende eine andere Release-Version abzuliefern. Es geht dabei nicht nur darum dem Compiler zu vertrauen, sondern dass feine Unterschiede im Timing manchmal Probleme (Race Conditions) auslösen, die man gerne möglichst früh schon während der Entwicklung bemerken will.
Die vollständige Liste der Optimierungen befindet sich hier.
Startup-Code
Die Cube IDE generiert eine Assembler Datei namens startup_stm32.s, welche die Interrupt-Vektor Tabelle und den ersten Code enthält, der nach einem Reset ausgeführt wird. Normalerweise ist es nicht nötig, sie zu editieren.
Dieser Startup-Code führt eine Funktion namens SystemInit() aus, bevor statische Objekte konstruiert werden und bevor main() ausgeführt wird. Bei großen C++ Projekten kann es sich lohnen, diese Funktion zu überschreiben, um dort die Erhöhung der Taktfrequenz unter zu bringen, denn dann startet das Programm schneller. Dies ist auch ein guter Platz, um die FPU einzuschalten (falls vorhanden):
void SystemInit() {
// Switch the FPU on
SCB->CPACR = 0x00F00000;
}
Speicher-Struktur
Obwohl der Prozessor in Harvard Architektur gestaltet ist, benutzt er einen gemeinsamen Adress-Raum für Programm, Daten und Peripherie Register. Dadurch können alle Peripherie Register über Zeiger angesprochen werden und der Prozessor kann Code sowohl aus dem RAM als auch aus dem (Flash-) ROM ausführen. Adressen und Zeiger sind 32 Bit groß.
Die Befehle sind teilweise 16 Bit und teilweise 32 Bit groß.
Daten werden als 8, 16 oder 32 Bit geladen. Sie müssen nicht zwingend an der 32 Bit Wortgröße ausgerichtet sein. Aber man erreicht bessere Geschwindigkeit, wenn 16 Bit Daten an 16 Bit Adressen und 32 Bit Daten an 32 Bit Adressen ausgerichtet sind. Der Compiler kümmert sich automatisch darum.
Die Peripherie Register sind überwiegend 32 Bit breit.
Schreibzugriffe auf die Peripherie finden asynchron statt. So kann die CPU zum Beispiel ein langsames Register am AHB Bus beschreiben und noch bevor dies fertig ist ein anderes Register am APB Bus ansprechen. Mit dem Befehl __DSB() zwischen zwei Register-Zugriffen stellt man sicher, dass sie ohne Überlappung nacheinander stattfinden.
Im Gegensatz zu AVR Mikrocontrollern liegen konstante Zeichenketten bei ARM im Flash Speicher, nicht im RAM.
Funktionsaufrufe
Bei Funktionsaufrufen werden bis zu 4 Parameter durch CPU-Register übergeben. Dabei ist es vorteilhaft, sie als 32 Bit Typ zu deklarieren, um Konvertierungen zu vermeiden. Bei mehr als 4 Parametern wird das RAM zur Übergabe benutzt, dann sind 8 Bit, 16 Bit und 32 Bit Typen gleich langsam.
Der Rückgabewert einer Funktion wird ebenfalls in einem CPU Register übermittelt und sollte daher 32 Bit groß sein, falls Geschwindigkeit wichtig ist.
Stack
Der Stapel speichert ausschließlich 32 Bit Werte. Bei jedem PUSH wird der Stapelzeiger (SP) zuerst um 4 verringert und dann wird das Wort an diese Adresse abgelegt. Der Stapelzeiger zeigt also immer auf die zuletzt belegte Adresse im RAM.
Es gibt zwei Stapelzeiger MSP und PSP, zwischen denen man umschalten kann. Der Prozessor startet mit dem MSP, welcher durch das erste Wort in der Interrupt-Vektor Tabelle (an Adresse 0) initialisiert wird. Der alternative Stapelzeiger PSP wird von Betriebssystemen genutzt, um den Programmen separate Stapel zuzuweisen. Unter dem Namen SP spricht man immer den Stapelzeiger an, der durch das SPEL Bit im CONTROL Register ausgewählt wurde (0=MSP (default), 1=PSP).
Relevante CMSIS Funktionen:
- __get_MSP()
- __set_MSP(addr)
- __get_PSP()
- __set_PSP(addr)
- __get_CONTROL()
- __set_CONTROL(value)
Interrupt-Vektoren
Interrupt-Signale werden von interner oder externer Hardware ausgelöst, wenn bestimmte Ereignisse auftreten. Sie können dazu genutzt werden, das laufende Programm vorübergehend zu unterbrechen und stattdessen eine besondere Funktion auszuführen, die Interrupt-Handler oder Interrupt-Service-Routine (ISR) genannt wird.
Der Flash-Speicher beginnt immer mit der Interrupt-Vektor Tabelle. Jeder Eintrag ist eine 32 Bit Sprungadresse. Diese ist im Referenzhandbuch Kapitel "Interrupt and exception vectors" dokumentiert. Der Quelltext dazu befindet sich in der Datei startup_stm32.s, welche von der Cube IDE beim Anlegen des Projektes generiert wird. Dort findest du die vorgegebenen Namen der C-Funktionen. Die ersten Einträge der Tabelle heissen "ARM Processor Exceptions" und sind bei allen STM32 Modellen gleich:
| Addresse | ARM Exception Nr. | CMSIS Interrupt Nr. | C Funktion | Beschreibung |
|---|---|---|---|---|
| 0x0000 | Anfangswert vom Stapelzeiger MSP | |||
| 0x0004 | Reset_Handler | Anfangswert vom Programmzähler, zeigt auf den Start-Code in Assembler | ||
| 0x0008 | 2 | -14 | NMI_Handler() | Nicht maskierbare Unterbrechung. Das "Clock Security System" (CSS) ist mit dem NMI Vektor verbunden. |
| 0x000C | 3 | -13 | HardFault_Handler() | Hardware Fehler. Kann optional (durch Konfiguration im Register SCB->SHCSR) in die drei folgenden Einträge aufgesplittet werden: |
| 0x0010 | 4 | -12 | MemManage_Handler() | · Speicherschutzfehler, existiert nicht auf Cortex M0(+) |
| 0x0014 | 5 | -11 | BusFault_Handler() | · Speicherzugriffsfehler |
| 0x0018 | 6 | -10 | UsageFault_Handler() | · Undefinierter Befehl, falsche Ausrichtung (alignment), ungültiger Status, Division durch Null |
| 0x001C | 7 | -9 | reserviert | |
| 0x0020 | 8 | -8 | ||
| 0x0024 | 9 | -7 | ||
| 0x0028 | 10 | -6 | ||
| 0x002C | 11 | -5 | SVC_Handler() | Supervisor Call, wird vom SVC Befehl ausgelöst. Wird in Kombination mit Betriebssystemen verwendet. |
| 0x0030 | 12 | -4 | reserviert | |
| 0x0034 | 13 | -3 | ||
| 0x0038 | 14 | -2 | PendSV_Handler() | Pendable Request for System Service. Wird vom Betriebssystem durch Beschreiben des ICSR Registers ausgelöst, um den Kontext zu wechseln. |
| 0x003C | 15 | -1 | SysTick_Handler() | Wird vom SysTick Timer aufgerufen, wenn er 0 erreicht. |
Danach folgen Modell-spezifische Interrupt-Vektoren, die ich in den Artikeln für STM32L0, STM32F1, STM32F3 und STM32G4 aufliste.
Interrupt Handler sind ganz normale C Funktionen. Sie brauchen keine besondere Kennzeichnung für den Compiler.
Interrupt Controller
Der Interrupt-Controller NVIC steuert die Verarbeitung von Unterbrechungs-Signalen. Er ist Bestandteil des ARM Kerns.
Laufende Interrupt-Handler können durch höher priorisierte Interrupts unterbrochen werden. Reset, NMI und HardFault haben immer die höchste Priorität (noch höher als 0), bei allen anderen kann man die Priorität einstellen. Die CMSIS enthält Funktionen zur Konfiguration des Interrupt-Controllers:
- NVIC_SetPriority(CMSIS Interrupt Nr, Priorität) Stelle die Priorität ein.
- STM32L0: 0=höchste, 3=niedrigste
- STM32F1: 0=höchste, 15=niedrigste
- STM32F3: 0=höchste, 15=niedrigste
- STM32G4: 0=höchste, 15=niedrigste
- NVIC_EnableIRQ(CMSIS Interrupt Nr) Erlaube Hardware Interrupt
- NVIC_DisableIRQ(CMSIS Interrupt Nr) Sperre Hardware Interrupt
- NVIC_SetPendingIRQ(CMSIS Interrupt Nr) Löse einen Interrupt aus
- NVIC_SystemReset() Löse einen System Reset aus
- __disable_irq() Sperre alle Interrupts
- __enable_irq() Hebe die Interrupt-Sperre auf
- __set_PRIMASK(1) Sperre normale Interrupts (ausgenommen: NMI und HardFault)
- __get_PRIMASK() Lese die aktuelle Prioritäten-Maske aus
Über das SCB->VTOR Register kann man den Ort der Liste verändern um sie z.B. ins RAM zu verschieben.
Um Unterbrechungen temporär zu verbieten (zum Beispiel für exklusiven Zugriff auf Daten oder Schnittstellen), wird folgende Vorgehensweise empfohlen:
uint32_t backup = __get_PRIMASK(); __set_PRIMASK(1); ... do some work ... __set_PRIMASK(backup);
Normale Unterbrechungen sind durch Pegel gesteuert. Wenn das Signal auf High steht, wird der zugeordnete Handler möglichst bald ausgeführt. Während der Interrupt-Handler läuft, muss das Signal wieder auf Low zurück zurück gehen, sonst wird der Interrupt-Handler nach seinem Ende gleich wieder aufgerufen.
Wenn das Signal zu früh verschwindet, während der Interrupt-Controller nach einer Gelegenheit sucht, den Interrupt-Handler auszuführen, geht es verloren. Der Interrupt-Handler wird dann nicht ausgeführt.
Einige Interrupt-Signale durchlaufen einen erweiterten Schaltkreis, der zusätzliche Konfiguration erfordert. Man erkennt sie am Stichwort EXTI.
SysTick Timer
Alle Cortex-M Prozessoren enthalten einen 24bit Timer, mit dem man die Systemzeit misst. Der Timer zählt die Taktimpulse des Prozessors herunter und löst bei jedem Überlauf einen Interrupt aus. Der Funktionsaufruf SysTick_Config(SystemCoreClock/1000) sorgt dafür, dass jede Millisekunde ein SysTick Interrupt ausgelöst wird. Ein Anwendungsbeispiel:
#include "stm32f3xx.h"
// The current clock frequency
uint32_t SystemCoreClock=8000000;
// Counts milliseconds
volatile uint32_t systick_count=0;
// Interrupt handler
void SysTick_Handler() {
systick_count++;
}
// Delay some milliseconds.
void delay(int ms) {
uint32_t start=systick_count;
while (systick_count-start < ms);
}
int main() {
// Initialize the timer for 1 ms intervals
SysTick_Config(SystemCoreClock/1000);
// Delay 2 seconds
delay(2000);
...
}
Wenn der Prozessor beim Debuggen angehalten wird, hält auch dieser Timer an. Im WFI und WFE Sleep Modus läuft der SysTick Timer weiter.
Power Management
Indem man Taktsignale für Peripherie deaktiviert oder verlangsamt, spart man bereits eine Menge Strom. Darüber hinaus gibt es die folgenden besondere Zustände für den ARM Kern:
| Modus | Eintritt | Aufwachen | Beschreibung |
|---|---|---|---|
| WFI Sleep | __WFI() | Interrupt | Warte auf Interrupt. Nur die CPU wird angehalten. |
| WFE Sleep | __WFE() | Interrupt oder Ereignis | Warte auf Ereignis. Nur die CPU wird angehalten. |
| Stop | PDDS=1, LPDS=1, SLEEPDEEP=1 + __WFI() oder __WFE() | EXTI Leitung, konfiguriert im EXTI Register. | Taktsignale von HSI/HSE sind deaktiviert. Die I/O Pins, Register und RAM bleiben unverändert. Debugging ist nicht möglich. |
| Standby | PDDS=1, LPDS=0, SLEEPDEEP=1 + __WFI() oder __WFE() | Steigende Flanke an WKUP Pin, RTC Alarm, externer Reset am NRST, IWDG Reset. | Taktsignale von HSI/HSE sind deaktiviert. RAM Inhalte gehen verloren. Nur die Backup Register bleiben erhalten. Die I/O Pins werden hochohmig, außer TAMPER, WKUP und NRST. Debugging ist nicht möglich. Zum Aufwachen muss der Controller neu starten. |
Wenn der WFI/WFE Sleep innerhalb einer Interruptroutine aktiviert wurde, kann er nur durch einen höher priorisierten Interrupt aufgeweckt werden. Wenn gerade ein Ereignis ansteht, während __WFE() aufgerufen wird, wird das Ereignis gelöscht und kein Sleep Modus aktiviert.
Durch Setzen von Bit 1 (SLEEPONEXIT) im Register SCB->SCR aktiviert man die "Sleep on exit" Funktion. Diese bewirkt, dass der Prozessor nach Abarbeitung jeder Interruptroutine automatisch in den WFI Sleep Modus geht.
Wenn im SCB->SCR Register das Bit 4 (SEVONPEND) gesetzt ist, löst ein anstehender Interrupt zugleich ein Ereignis aus, selbst wenn der Interrupt nicht freigeschaltet ist.


