STM32 Anleitungen
Diese Webseiten sollen dabei helfen, von einem anderen Mikrocontroller nach STM32 zu wechseln. Ich erkläre ihre Eigenschaften, zeige umfangreich kommentierte Code-Beispiele und gebe ganz viele Hinweise auf nützliche Dokumentation. Für vier Serien habe ich spezifische Seiten erstellt:
- STM32L0 mit ARM Cortex M0+ bis 32 MHz, low power mit EEPROM
- STM32F1 mit ARM Cortex M3 bis 72 MHz, alt
- STM32F3 mit ARM Cortex M4F bis 72 MHz, mixed signal
- STM32G4 mit ARM Cortex M4F bis 170 MHz, mixed signal, stark und sparsam
In den folgenden Absätzen findest du allgemeine Informationen, die für alle STM32 Serien gelten.
Modelle
Der Hersteller gruppiert seine Mikrocontroller in Familien und Serien. Innerhalb jeder Serie gibt es zahlreiche Modelle. Im Jahr 2026 wurden etwa 1600 unterschiedliche Modelle angeboten.
Portfolio
Auszug aus dem Portfolio, sortiert nach DMIPS1:
(Ultra) Low Power Familie
Beispiel: Das Modell STM32F103C8T6 hat einen ARM Cortex M3 Kern bis 72 MHz, 48 Pins, 64 KiB Flash, und ein QFP Gehäuse für -40 bis +85 °C |
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 = |
|
|
Temperatur 6 = -40 bis +85 °C 7 = -40 bis +105 °C 3 = -40 bis +125 °C |
Der Product selector soll bei der Auswahl eines geeigneten Modells helfen. Dazu kann ich folgende Tipps geben:
Die STM32 sind ausgesprochen langlebig - nur wenige Serien wurden abgekündigt. Sogar die älteste STM32F1 Serie ist immer noch in Produktion. Wer sich an neue Modelle heran wagt, bekommt für's gleiche Geld mehr Leistung, Speicher und Funktionen. Allerdings gibt es dazu weniger Anleitungen. Auch der Hersteller braucht Zeit, um Fehler im Errata zu dokumentieren oder im Chip zu beheben.
Benutze beim Lernen am Besten genau den Mikrocontroller der Anleitung, oder zumindest einen aus gleichen Serie. Denn bestehende Programme laufen mit Sicherheit nicht "einfach so" auf anderen Serien.
Für neue Projekte bevorzuge ich die preisgünstige Mainstream Familie, außer wenn geringe Stromaufnahme oder hohe Leistung wichtig ist. Bei Mouser bekommst du einen schnellen Überblick über aktuelle Preise. Filtere dort nach der gewünschten Serie.
Die "Low Power" Familie ist für Batteriebetrieb gut, vor allem wenn man die meiste Zeit im Stop Zustand wartet. Ihre USB Schnittstelle kann unabhängig vom Rest mit einer anderen (meist höheren) Spannung versorgt werden. Bei den STM32L0 und STM32L1 Serien gibt es integriertes EEPROM.
Die "mixed signal" Serien haben besondere Stärken zur Verarbeitung analoger Signale: Operationsverstärker, Komparatoren, DSP Befehle, und neben der FPU weitere Koprozessoren.
Mit den "C" Serien versucht STMicroelectronics, gegen chinesische Konkurrenten zu bestehen. Sie sind funktional abgespeckt, um billiger zu sein. Die STM32C5 werden vom Arduino Core noch nicht unterstützt (Stand Mai 2026), siehe Supported boards.
Alle STM32 haben eine RTC, ADC, DMA Controller, und natürlich auch viele Timer und diverse serielle Schnittstellen. Alle Modelle unterstützen das In-System Debugging. Die Versorgungsspannung ist normalerweise 2 bis 3,3 Volt. Es gibt keine 5 Volt Modelle, aber viele digitale I/O Pins tolerieren 5 Volt.
ARM Cortex-M Kerne
| Cortex | DMIPS1 | Eigenschaften |
|---|---|---|
| M0, M0+ | 0,96 | Weil der M0 aus besonders wenigen Transistoren besteht, nimmt er wenig Strom auf und ist billig. Auf DSP Befehle und FPU muss man dafür verzichten. Dividieren kann er nur indirekt, viel langsamer als multiplizieren. Außerdem hat er weniger Funktionen zum Debuggen, z.B. keinen TRACESWO Ausgang. |
| M3, M4 | 1,25 | Der M3 ist der älteste ARM Cortex-M Kern. Mit dem M4 kamen optionale DSP Befehle und die FPU dazu. |
| M7 | 2,14 | Der M7 ist der schnellste Kern. Unterm Strich wird er allerdings von M55 mit mehr MHz übertroffen. |
| M33 | 1,5 | Der M33 wurde als Nachfolger zu den M0 bis M4 platziert. Der M33 schafft alle Nachteile des M0 ab, ist dafür aber größer, teurer und braucht mehr Strom. |
| M55 | 1,69 | Der M55 wird mit sehr hohen Taktfrequenzen angeboten. Er ist etwas Energie-Effizienter als der M7. |
Weitere Unterschiede sind in der Arm Cortex-M Processor Comparison Table zusammen gefasst.
Kerne mit einem "F" hinter der Nummer haben eine FPU. Sie beschleunigt die Berechnung von 32 Bit float etwa auf das gleiche Tempo wie 32 Bit integer. Die FPU vom Cortex M7F und M55F kann auch 64 Bit double.
- DMIPS = Millionen Integer Anweisungen pro Sekunde pro Megahertz nach dem Dhrystone Benchmark. DSP und FPU Befehle sind nicht berücksichtigt.
Boards
Die originalen Boards zun Lernen heißen "Nucleo" und "Discovery". Sie haben alle einen ST-Link als Programmieradapter/Debugger, der auch einen USB-UART für serielle Kommunikation zum PC enthält. Die kleinen Nucleo-32 und Nucleo-64 Boards sind minimalistisch ausgestattet, während die größeren Nucleo-144 und Discovery Boards zusätzliche Peripherie wie Displays, Sensoren und Ethernet zum Ausprobieren haben.Mit vielen Boards kann man extern angeschlossene Mikrocontroller programmieren, indem man die rot eingekreisten Jumper mit der auffälligen Beschriftung entfernt. Bei manchen kann man den Programmieradapter sogar abschneiden.
Bei Aliexpress und anderen Händlern findest du unter dem Begriff STM32 Core Board weitere Boards. Das Angebot wechselt ständig. Die Händler bieten dazu passende Programmieradapter als USB Stick an.
Software
Entwicklungsumgebungen
- STM32 Cube IDE empfohlen für den Anfang, Hinweise zur IDE
- Arduino IDE für das Arduino Framework, Hinweise dazu
- 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 mit ST-Link Adapter und alle Bootloader Schnittstellen, sowie zur Diagnose mittels Trace SWO Leitung
- STM32 Cube MX Code-Generator für das Cube HAL Framework
Alte Tools:
- Hammer Terminal für UART und virtuelle COM Ports
- ST-Link Utility zum Flashen mit ST-Link Adapter und Anzeigen von Trace Meldungen, nur Windows
- Flash Loader Demonstrator zum Flashen über den seriellen Bootloader, nur Windows
- DfuSe zum Flashen über den USB Bootloader, nur Windows
- ST-Link Tools Open-source Alternative zum ST-Link Utility
- STM32 Flash für den seriellen Bootloader und I²C
Bibliotheken
Die Basis-Bibliothek für alle ARM Cortex-M Mikrocontroller heißt CMSIS-Core. Sie enthält Definitionen für die Register vom ARM Kern (Adressen und Bits), sowie ein paar Hilfsfunktionen zu dessen Konfiguration. Jeder Chip-Hersteller ergänzt es mit sogenannten "device files" für die Teile, die um den Kern herum gebaut wurden. Für alle oben genannten STM32 Serien habe ich diese Dateien in CMSIS-STM32.zip zusammen getragen. Zuletzt aktualisiert am 07.03.2026.
Für Modelle mit DSP Befehlssatz stellt ARM zusäzlich die dazu passende CMSIS-DSP Bibliothek (mit arm_math.h) bereit. Das CMSIS-RTOS wird von einigen Anwendungen als Betriebssystem verwendet.
Darauf aufbauend stellt die Firma STMicroelectronics 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.
Du kannst die von ST bereit gestellten Bibliotheken auf der Seite STM32Cube MCU Package herunter laden. Darin sind auch einige ARM Bibliotheken enthalten. Das Programm Cube MX ruft sie automatisch ab.
Das Arduino Framework legt eine weitere Abstraktions-Schicht über LL und Cube HAL, um die Programmierung unterschiedlicher Mikrocontroller von vielen Herstellern zu vereinheitlichen. Mehr dazu dort.
Schließlich gibt es noch die Standard Bibliotheken der Programmiersprachen C und C++, die der Compiler mit sich bringt.
⚠️ Der Cube HAL und der Arduino Core sind im Vergleich zu ihrer Komplexität nur sehr knapp dokumentiert. Die Grundlagen lernt man besser ohne Framework. Du wirst die Grundlagen brauchen, um fehlende Informationen aus den Quelltexten der Frameworks heraus lesen zu können.
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) {
(void)file; // unused
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.
Reset und Taktgeber
Die Standard-Beschaltung des NRST Pins besteht aus einem Pull-Up Widerstand und einem Reset-Taster. Beim Anlegen der Versorgungsspannung erzeugt der Mikrocontroller das Reset Signal selbst intern. Dabei zieht er den Pin für ≥ 20 µs auf LOW. Deswegen gehört da kein Kondensator dran!
Das Grundgerüst der Taktversorgung sieht so aus:
Nach dem Reset wird der Mikrocontroller automatisch mit einem internen Oszillator (HSI oder MSI) getaktet, meistens mit 16 MHz. Mit einem konfigurierbaren PLL Schaltkreis lässt sich die Oszillator-Frequenz um ein Vielfaches erhöhen. Der so gewonnene Systemtakt für die CPU lässt sich danach durch diverse Teiler herab setzen, um einzelne Peripheriegruppen langsamer zu takten. Zum Teil ist das technisch nötig, aber man kann es auch tun, um Strom zu sparen. Nach einem Reset sind PLL und die Teiler deaktiviert. Der ganze Mikrocontroller läuft also zunächst direkt mit der Frequenz vom internen Oszillator.
Per software kann man auf einen anderen Oszillator umschalten. Der HSE Eingang kann wahlweise als Quarz-Oszillator oder mit einem externen Signal betrieben werden, einstellbar durch das HSEBYP Bit.
Dieses RCC System ist bei jeder Serie anders aufgebaut, deswegen kann ich hier nur auf das jeweilige Referenzhandbuch verweisen. Mit dem Programm STM32 Cube MX lasse ich ich mir die Einstellungen gerne grafisch anzeigen, auch wenn gerade kein Interesse an dessen Code-Generator besteht.
Der Flash Speicher ist viel langsamer als die CPU, meist im Bereich zwischen 16 bis 30 MHz. Wenn die CPU schneller getaktet werden soll, muss man vorher "Wait States" konfigurieren, damit die CPU beim Zugriff auf den Flash Speicher entsprechend ausgebremst wird. Zum Ausgleich ist der Flash Speicher mit 64 oder 128 Bit wesentlich "breiter" als die Befehle der CPU, und alle STM32 Modelle haben einen Puffer, der soweit möglich ein paar Befehle im Voraus aus dem Flash holt, während die CPU beschäftigt ist. Die meisten Programmteile kann die CPU also doch mit ihrer vollen Taktfrequenz abarbeiten.
STM32 Mikrocontroller laufen intern mit knapp über 1 Volt. Manche brauchen für die hohen Taktfrequenzen eine höhere Spannung. Bei betroffenen Serien ist die Prozedur zur Konfiguration des internen Spannungsreglers im Referenzhandbuch unter dem Begriff "Dynamic Voltage Scaling" beschrieben.
Im Gegensatz zu den vielen 8 Bit Mikrocontrollern sind bei STM32 alle Peripherie-Einheiten zunächst aus geschaltet. Das heisst technisch: sie sind vom Takt abgehängt. Bei den "Low Power" Serien ist teilweise auch ihre Stromversorgung deaktiviert. Nach einem Reset ist nur der ARM Kern (CPU, Systick-Timer, Interrupt-Controller und Debugger Schnittstelle) benutzbar. Alles andere musst du vor der Verwendung in den "ENR" Registern des RCC selbst einschalten.
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. Im Sleep, Stop und Standby Zustand kann der Debugger keine Verbindung aufbauen.
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:
| PC Programmieradapter | Mikrocontroller | Beschreibung | |
|---|---|---|---|
| SWDIO | ↔ | SWDIO (PA13) | Daten |
| SWCLK | → | SWCLK (PA14) | Takt |
| GND | GND | Masse |
ST-Link Adapter
Der dazu passende Programmieradapter 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. Als bessere Alternative werden die J-Link von Segger empfohlen, allerdings nicht für die Arduino IDE (weil STM32duino nur mit ST-Link debuggen kann).
Er ist folgendermaßen mit dem Mikrocontroller verbunden:
| Mikrocontroller | SWD CN4 | Beschreibung | |
|---|---|---|---|
| VDD | → | Pin 1 | Misst die Spannungsversorgung der Zielschaltung, optional |
| SWCLK (PA14) | ← | Pin 2 | Takt |
| GND | Pin 3 | Masse | |
| SWDIO (PA13) | ↔ | Pin 4 | Daten |
| NRST | ← | Pin 5 | Reset Signal, optional siehe Verbindungsoptionen in der Cube IDE |
| SWO (PB3) | → | Pin 6 | Serial Wire Output, optional siehe Trace Meldungen ausgeben |
Außerdem enthalten diese ST-Link Platinen auch einen USB-UART Adapter für 600 bis 2000000 Baud mit den Anschlüssen Tx und Rx auf CN3. Sie sind in der Regel mit USART2 (PA2, PA3) vom Mikrocontroller verbunden.
Mit den beiden eingekreisten Jumpern wird die Verbindung von SWCLK und SWDIO zum Target unterbrochen. Dann kann der ST-Link andere STM32 Mikrocontroller programmieren und debuggen. Bei vielen Nucleo-64/144 Boards kann man den ST-Link sogar an den Sollbruchstellen abschneiden um ihn ganz unabhängig zu benutzen.
Manche 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.
Beim unten gezeigten ST-Link v2 Stick sind folgende Eigenarten zu beachten:
- Der RST Pin funktioniert nur mit STM8, ist für STM32 also nutzlos.
- Die 3.3V Pins messen nicht die Versorgungsspannung, sondern liefern eine, aber nur sehr gering belastbar. Besser nicht benutzen.
- Die 5.0V Pins sind ohne jeden Schutz direkt mit dem USB Stecker verbunden. Besser nicht benutzen.
- Wer eine ruhige Hand hat, kann sich eine SWO Leitung wie gezeigt nachrüsten.
Trace Meldungen ausgeben
Mikrocontroller mit Cortex-M3 und höher können auf PB3 Diagnose Daten über das laufende Programm ausgeben. Wenn die Funktion mit dem Debugger aktiviert wird, dient PB3 vorübergehend als "Serial Wire Output" (SWO). Eine typische Anwendung ist die Ausgabe von "Instrumentation Trace Messages" (ITM): einfache Text-Meldungen, die man sonst auf einem UART ausgeben würde.
Die CMSIS Funktion ITM_SendChar() gibt ein Zeichen auf der SWO Leitung aus. Anwendungsbeispiel:
#include "stm32g4xx.h"
#include <stdio.h>
#include <stdarg.h>
// Output an ITM message
void debug_print(const char *ptr) {
while (*ptr) {
ITM_SendChar((uint32_t)*ptr++);
}
}
// Like printf() but using ITM. Max. 81 characters.
void debug_printf(const char* format, ...) {
// Skip if ITM is not enabled
if ((ITM->TCR & ITM_TCR_ITMENA_Msk) && (ITM->TER & 1UL)) {
char buffer[82]; // change the size if needed
va_list args;
va_start(args, format);
vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
debug_print(buffer);
}
}
int main() {
...
debug_print("Hello World!\n");
int i=42;
debug_printf("value of i is %d\n",i);
}
Die Ausgabe kann man mit dem ST-Link Utility anzeigen, und zwar über dem Menüpunkt "ST-LINK/Printf via SWO Viewer", oder mit dem STM32 Cube Programmer über den SWV (Serial Wire Viewer) Button am rechten Rand. Für den J-Link Adapter kannst du den J-Link SWO Viewer benutzen. Siehe auch meine Anleitung zur Anzeige innerhalb der Cube IDE.
Bootloader
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"). Auf dem PC kannst du dazu den STM32 Cube Programmer benutzen. Zum Debuggen ist der Bootloader nicht geeignet.
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 | Masse |
Der Bootloader erkennt die Baudrate automatisch. Es werden 8 Datenbits und gerade Parität (even) verwendet.
USB Bootloader
Am USB Anschluss verwendet der Bootloader das DFU Protokoll in Version 1.1. Folgende Anschlüsse werden verwendet:
| PC | Mikrocontroller | Beschreibung | |
|---|---|---|---|
| D- | ↔ | DM (PA11) | Daten |
| D+ | ↔ | DP (PA12) | Daten |
| GND | GND | Masse |
USB wird nicht von allen Bootloadern unterstützt. Bei Mikrocontrollern ohne internen Pull-Up Widerstand (z.B. STM32F105/107 und STM32F3) muss man die D+ Leitung mit einem 1,5 kΩ Widerstand auf 3,3V hoch ziehen. Beim STM32F105/107 muss außerdem VBUS (5V) mit PA9 verbunden sein.
Unter Linux muss man die Software mit sudo starten, damit man auf das USB Device 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.
In dem Zusammenhang möchte ich noch einen kleinen gcc spezifischen Trick erwähnen: Durch die Angabe __attribute__((section(".data"))) kann man zeitkritische Funktionen ins RAM verlagern, wo sie ohne Wait States mit exakt vorhersagbarer Geschwindigkeit ablaufen.
Startup-Code
Die IDE generiert eine Assembler Datei namens startup_stm32.s (oder so ähnlich), 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, falls vorhanden. Sie wird sehr früh ausgeführt, noch bevor statische und globale Variablen initialisiert sind und bevor main() ausgeführt wird. Große C++ Projekte starten schneller, wenn man dort den Code zur Erhöhung der Taktfrequenz unterbringt. Dies ist auch ein guter Platz, um die FPU einzuschalten, falls vorhanden:
void SystemInit() {
// Switch the FPU on
SCB->CPACR = 0x00F00000;
}
Speicher-Struktur
Der Prozessor benutzt 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-Speicher 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 automatisch 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 | 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 durch Konfiguration im Register SCB->SHCSR in die drei folgenden Einträge aufgesplittet werden, jedoch nicht bei Cortex-M0(+): |
| 0x0010 | 4 | -12 | MemManage_Handler() | · Speicherschutzfehler |
| 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 | DebugMon_Handler() | Debug monitor, existiert nicht bei Cortex-M0(+) |
| 0x0034 | 13 | -3 | reserviert | |
| 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 für die integrierte Peripherie. Sie sind im jeweiligen Referenzhandbuch in der "vector table" aufgelistet. Dazu gehört auch je nach Serie die Tabelle "EXTI lines connections" oder das Kapitel "External interrupt/event line mapping".
Interrupt Handler sind ganz normale C Funktionen. Sie brauchen keine besondere Kennzeichnung für den Compiler. Beispiel:
void HardFault_Handler() {
...
}
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. Das 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. Sie sind im Referenzhandbuch in der "EXTI lines connections" Tabelle aufgelistet.
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 Timer wird mit der CMSIS Funktion SysTick_Config() gestartet. Ein Anwendungsbeispiel:
#include "stm32f3xx.h"
// The current clock frequency
uint32_t SystemCoreClock=8000000;
// Interrupt handler
volatile uint32_t systick_count=0;
void SysTick_Handler() {
systick_count++;
}
// Delay some milliseconds.
void delay(uint32_t ms) {
uint32_t start=systick_count;
while (systick_count-start < ms);
}
int main() {
// Start the system timer with 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 Zustand 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:
| Zustand | Beschreibung | Eintritt | Aufwachen |
|---|---|---|---|
| WFI Sleep | Warte auf Interrupt. Nur die CPU wird angehalten. | __WFI() Befehl | Interrupt |
| WFE Sleep | Warte auf Ereignis. Nur die CPU wird angehalten. | __WFE() Befehl | Interrupt oder Ereignis |
| Stop | Die HSI und HSE Oszillatoren sind deaktiviert. Low Power Timer und UART können weiter laufen. Die I/O Pins, Register und RAM bleiben unverändert. Debuggen ist nicht möglich. | Siehe "Power Control" im Referenzhandbuch und Tips for using STM32 low-power modes | EXTI Interrupt und spezifische Ereignisse. Bei STM32U3 auch steigende Flanke am gewählten Wakeup Pin. Nach dem Aufwachen muss das Taktsystem erneut konfiguriert werden. |
| Standby | Die HSI und HSE Oszillatoren sind deaktiviert. RAM Inhalte gehen verloren. Nur die Backup Register in der RTC bleiben erhalten. Alle Ausgänge werden deaktivert. Debuggen ist nicht möglich. | Steigende Flanke am gewählten Wakeup Pin, RTC Alarm, externer Reset, Watchdog Reset. Beim Aufwachen startet der Mikrocontroller wie nach einem Reset neu. |
Im allen vier Zuständen kann der Debugger keine Verbindung aufbauen. Aber eine bereits bestehende Verbindung bleibt im Sleep Zustand (nicht im Stop und Standby) erhalten.
Im Stop Zustand können die "Low Power" Modelle zeigen, warum sie so heißen. Dort sind sie erheblich sparsamer, als die anderen STM32 Familien.
Wenn der WFI/WFE Sleep Zustand 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 Zustand 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 Zustand 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.
Andere Anleitungen
Hier sammle ich Verweise zu weiteren empfehlenswerten Anleitungen.
- STM32 Online trainings (von ST)
- STM32 tutorials von Prof. Laurent Latorre, für STM32F072
- A bare metal programming guide von Sergey Lyubka, für STM32F429
- Einblick in die moderne Elektronik ohne viel Theorie mein Buch, für STM32F103 und STM32F303






