Startseite

STM32F3 Anleitung

In diesem Tutorial sammle ich Informationen zur Anwendung von STM32 Mikrocontrollern, Serie F3. Also technische Daten, Hinweise und Programmier-Beispiele, die beim Einstieg helfen könnten.

Modell Bezeichnungen

Der Hersteller hat sich für die Bezeichnung seiner Mikrocontroller folgendes Schema ausgedacht:

Dokumentationen

Die Pinbelegung und elektrischen Daten stehen im jeweiligen Datenblatt. Für den Programmierer ist das Reference Manual am wichtigsten, da es die I/O Funktionen und Register beschreibt. Im Errata Sheet beschreibt der Hersteller überraschende Einschränkungen und Fehler der Mikrochips, teilweise mit konkreten Workarounds.

Weiterführende Doku:

Elektrische Daten

Alle STM32F3 Chips kann man mit 2,0 bis 3,6 Volt betreiben. OPAMP und DAC benötigen aber mindestens 2,4 Volt und die USB Schnittstelle läuft nur mit 3,3 V.

Die Stromaufnahme ist im laufenden Betrieb mit 8bit Mikrocontrollern vergleichbar, im Stop Modus ist sie jedoch deutlich höher. Für langfristigen Batteriebetrieb wird daher auf die sparsame L0 Serie verwiesen.

Viele I/O Pins sind 5 V tolerant. Bei den STM32F303 Modellen sind das: PA8-15, PB3-9, PC7-12, PD0-7, PE0-7, PF0, PF1, PF6, PF9, PF10
Im open-drain Modus dürfen die 5 V toleranten Ausgänge durch externe Widerstände auf 5 V hoch gezogen werden.

Die Ausgänge sind einzeln mit 25 mA und alle zusammen mit 80 mA belastbar. Gültige Logikpegel sind aber nur bis 8 mA garantiert. Die Ausgänge sind nicht Kurzschluss-fest.

Die Eingänge sind sehr hochohmig (da CMOS) und haben einen Schmitt-Trigger. Die internen Pull-Up und Pull-Down Widerstände haben ungefähr 40 kΩ. Alle I/O Pins haben ungefähr 5 pF Kapazität. Die ESD Schutzdioden sind teilweise nur sehr bedingt belastbar.

Der NRST (Reset) Pin hat intern einen Pull-Up Widerstand und kann sowohl vom Chip selber als auch von außen auf Low gezogen werden. Intern erzeugte Reset-Impulse sind garantiert mindestens 20 µs lang.

Besondere Ausnahmen

Für die Pins PC13, PC14 und PC15 gelten folgende Einschränkungen: Hintergrund ist, dass diese drei Pins intern am (schwachen) Power-Switch der RTC hängen.

Boards

Nucleo-F303RE

Das Nucleo-F303RE Board (aus der Nucleo-64 Reihe) ist ein hochwertiges Starter-Set zum günstigen Preis um 15 €.

Der 8 MHz Hauptquarz befindet sich auf dem ST-Link Adapter, er versorgt beide Mikrocontroller. Wenn man den ST-Link abtrennt, muss man den Mikrocontroller mit seinem internen R/C Oszillator betreiben oder einen zusätzlichen Quarz in die verbleibende Platine einlöten.

Das User Manual enthält die vollständige Beschreibung des Boardes.

Nucleo-F303K8

Das Nucleo-F303K8 Board (aus der Nucleo-32 Reihe) ist deutlich kleiner, obwohl es ebenfalls einen ST-Link Adapter enthält.

Der eingebaute ST-Link Adapter kann nicht abgetrennt werden und er kann auch nicht zum Programmieren anderer Mikrocontroller verwendet werden.

Das User Manual enthält die vollständige Beschreibung des Boardes.

STM32F3 Discovery

Das STM32F3 Discovery Board bietet neben dem üblichen ST-Link Adapter eine USB-Buchse, die mit dem Target Mikrocontroller verbunden ist. Im Gegensatz zu den obigen Boards ist dieses mit Sensoren und mehr LEDs ausgestattet.

Das User Manual enthält die vollständige Beschreibung des Boardes.

STM32F303CCT6 Mini System Dev.board

Das STM32F303CCT6 Mini System Dev.board Board von RobotDyn scheint dem Blue-Pill Board nachempfunden zu sein, nur halt mit einem aktuelleren Mikrocontroller Modell.

Wenn man besonders dünne Stiftleisten verwendet, passt das Board in einen 40-poligen DIP Sockel.

Die Firmware installiert man wahlweise über USART1, USART2, oder USB mit dem fest installierten Bootloader oder über SWD mit einem ST-Link Adapter.

Der Boot1 Jumper (=PB2) kann im Normalbetrieb für eigene Zwecke verwendet werden.

Wenn der Uhrenquarz benutzt wird, soll man die Stifte an PC14 und PC15 entfernen, damit er stabil schwingt.

Ich bin noch auf der Suche nach dem Schaltplan.

Software

Entwicklungsumgebung

Nach einigen Versuchen mit anderen ebenfalls kostenlosen Alternativen bin ich bei der System Workbench for STM32 (SW4STM32) angelangt. Dieses Programmpaket enthält Eclipse, die GNU ARM Embedded Toolchain (gcc) und OpenOCD für's Debugging. Ich konnte es sowohl unter Windows als auch unter Linux ohne nennenswerte Schwierigkeiten installieren.

Gehe direkt nach der Installation der System Workbench ins Menü "Help/Check for Updates...", um Fehlerkorrekturen zu erhalten!

Ganz unten findest du Hinweise zur Entwicklungsumgebung Atollic TrueSTUDIO. Ich möchte aber darauf hinweisen, dass die Arbeitsschritte dieser Anleitung auf die System Workbench bezogen sind.

Hinweise für Linux

Bei Linux werden die Pakete libc6:i386 und lib32ncurses5 benötigt, damit es sowohl 64bit als auch 32bit Programme ausführen kann. Außerdem kann es notwendig sein die Verwendung der alten GTK-2.0 Library anstelle von GTK-3.0 zu forcieren, damit Dialogfenster korrekt dargestellt werden. Man erreicht das durch folgenden Eintrag in der Datei ~/.profile:
export SWT_GTK3=0
Diese Hinweise gelten ebenso für das Atollic TrueSTUDIO.

Zubehör

Optionale Software für Windows:

Optionale Software für Linux:

1) Die Programm-Version 2.0 habe ich mit Oracle Java 8 und diesem Start-Script ans Laufen gebracht.

ARM Libraries

Die Basis-Library für alle ARM Cortex-M Mikrocontroller heisst CMSIS-Core. Dabei handelt es sich im Grunde genommen um einen Haufen Definitionen für alle Register, damit man sie mit Namen statt über Hexadezimal-Codes ansprechen kann. Außerdem enthält die CMSIS wenige Hilfsfunktionen, die den ARM Kern konfigurieren. Die CMSIS ist von ARM spezifiziert und wird von allen Chip Herstellern in spezifischen Varianten bereit gestellt.

Darauf aufbauend stellt die Firma ST ihr proprietäres Cube HAL Framework bereit, das den Zugriff auf die Hardware-Funktionen durch Abstraktion erleichtern soll. Dazu gehört das Programm CubeMX, womit man Quelltext-Projekte einschließlich Code zur Konfiguration der I/O Funktionen und Taktversorgung erzeugt.

Ich empfehle, die Programmierung zunächst anhand des Referenzhandbuches (ohne HAL) zu lernen.

Projekt auf Basis von CMSIS erzeugen

Um ein neues Projekt ohne HAL anzulegen, geht man so vor:
  1. Lege mit dem Assistenten der IDE (File/New/Project) ein Arbeitsprojekt mit der Toolchain "Ac6 STM32 MCU GCC" und der Option "No Firmware" an.
  2. Kopiere aus meinem CMSIS-STM32 Paket oder dem STM32CubeF3 Paket zwei Verzeichnisse in dein Projekt:
    • (Drivers/)CMSIS/Include
    • (Drivers/)CMSIS/Device/ST/STM32F3xx/Include
  3. Füge die beiden Verzeichnisse zur Projektkonfiguration hinzu:
    • Rechte Maustaste auf den Projektnamen, dann auf Properties.
    • Gehe nach C/C++ Build / Settings / Tool Settings / MCU GCC Compiler / Includes
  4. In der Datei stm32f3xx.h sollst du unter dem Kommentar "Uncomment the line below according to the target STM32 device" (ungefähr Zeile 90) die zu deinem Mikrocontroller passende Zeile aktivieren.

Beispiel für einen einfachen LED-Blinker an PA5 auf Basis der CMSIS:

// Filename: main.c

#include <stdint.h>
#include "stm32f3xx.h"

// delay loop for the default 8 MHz CPU clock with optimizer enabled
void delay(uint32_t msec)
{
    for (uint32_t j=0; j<2000UL*msec; j++)
    {
        __NOP();
    }
}

int main(void)
{
    // Enable Port A
    SET_BIT(RCC->AHBENR, RCC_AHBENR_GPIOAEN);

    // PA5 = Output
    MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER5,  GPIO_MODER_MODER5_0);

    while(1)
    {
        // LED Pin -> High
        WRITE_REG(GPIOA->BSRR, GPIO_BSRR_BS_5);
        delay(500);

        // LED Pin -> Low
        WRITE_REG(GPIOA->BSRR, GPIO_BSRR_BR_5);
        delay(500);
    }
}
Ich weiß dass man Delays besser mit einem Timer realisiert. Hier wollte ich jedoch ein möglichst simples Programmbeispiel zeigen.

Um das Programm mit einem ST-Link Adapter in den Mikrocontroller zu übertragen klickt man in der IDE mit der rechten Maustaste auf den Projektnamen, dann auf Target/Program Chip...

Programmier- und Debug-Schnittstellen

SWJ Schnittstelle

Mit der SWJ Schnittstelle ü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.

SWJ ist die einzige Schnittstelle, die von der System Workbench direkt ohne zusätzliche Plugins unterstützt wird. Sie funktioniert unabhängig von Boot Modus, Taktquelle, Spannung und Temperatur. Die SWJ Schnittstelle ist während und nach einem Reset standardmäßig aktiviert, kann jedoch per Software deaktiviert werden.

Sie 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.

Die passenden Programmieradapter von ST heißen "ST-Link". Billige Nachbauten aus China sehen so aus:

Den ST-Link Adapter vom Nucleo-F303RE Board kann man abtrennen, um damit andere Mikrocontroller zu programmieren. Oder man zieht die beiden Jumper ab, wodurch die Leitungen SWCLK und SWDIO unterbrochen werden.

Er ist folgendermaßen mit dem Mikrocontroller verbunden:

ST-Link CN4MikrocontrollerBeschreibung
Pin 1VDDMisst die Spannungsversorgung, bei den hier dargestellten ST-Link Adaptern optional
Pin 2SWCLKPA14Serial Wire Clock
Pin 3GNDCommon Ground (Masse)
Pin 4SWDIOPA13Serial Wire Data
Pin 5NRSTReset Signal, optional siehe Verbindungsoptionen
Pin 6SWOPB3Serial Wire Output, optional siehe Ausgabe von Trace Meldungen

Das Programm "STM32 Cube Programmer" verlangt nach einem Firmware-Update, was auf beiden Adaptern problemlos klappt. Die anderen oben genannten Programme sind hingegen auch mit älteren Firmware Versionen zufrieden.

Unter Umständen erscheint in der in der System Workbench die folgende Meldung von OpenOCD:

Info: vid/pid are not identical <erwartete ID> <ID deines ST-Link>
Diese harmlose Information bedeutet, dass dein ST-Link Adapter nicht die erwartete Firmware-Version hat. Er funktioniert aber trotzdem. Wer die Meldung dennoch los werden will, sucht auf seiner Festplatte nach der Datei stlink.cfg. Im selben Verzeichnis befinden sich mehrere Versionen dieser Datei. Ersetze dort die erwartete ID durch die ID deines ST-Link Adapters.

Bei dem chinesischen Stick empfehle ich, die Rückseite der inneren Platine mit Gewebeband abzudecken, damit kein Kurzschluss zum Aluminium-Gehäuse entsteht. Man braucht am Target einen Reset-Taster, weil der Reset-Ausgang des Sticks nur bei STM8 funktioniert. Wer eine ruhige Hand hat, kann sich eine SWO Leitung nachrüsten:

Zwei dieser verbesserten Adapter verkaufe ich privat für jeweils 5 Euro.

Verbindungsoptionen

Die Software zum Programmieren und Debuggen unterstützt drei Optionen zum Verbindungsaufbau:

ModusBeschreibungEinschränkung
Hardware ResetDie Verbindung wird kurz nach dem Reset-Impuls geöffnet.Funktioniert nur mit verbundener NRST Leitung, aber das Programm darf nicht direkt beim Start die SWJ Schnittstelle deaktivieren oder in den Schlafmodus gehen.
Connect Under ResetDie Verbindung wird während des Reset-Impulses geöffnet.Funktioniert immer. Wenn die NRST Leitung nicht mit dem Programmieradapter verbunden ist, muss man den Reset Knopf manuell gedrückt halten und in dem Moment loslassen, wo "in procedure reset" erscheint.
Software System ResetDie SWJ Schnittstelle wird ohne Hardware-Reset geöffnet und dann ein Reset-Kommando abgesetzt.Funktioniert nicht, wenn das Programm die SWJ Schnittstelle deaktiviert oder einen Schlafmodus aktiviert hat.

Um zu verhindern, dass das installierte Programm startet und die Schnittstelle deaktiviert, kann man den Bootloader mittels Boot0=High aktivieren.

Die System Workbench benutzt beim Debuggen standardmäßig die Option "Hardware Reset".

Zum Ändern soll man den Debugger wenigstens einmal durch Klick auf den grünen Käfer gestartet haben, damit die IDE eine initiale Debug-Konfiguration anlegt. Gehe danach mit der rechten Maustaste auf das Projekt, dann auf Debug As/Debug Configurations.... Gehe in dem folgenden Dialog in den Debugger Tab und klicke dort auf den Knopf Show generator options... um die folgende Ansicht zu erhalten:

SWJ Deaktivieren

Um die betroffenen Pins für normale Ein-/Ausgabe zu verwenden, stellt man im Register GPIOx->MODER einfach den gewünschten Modus ein (Input, Output oder Analog).

Auch wenn PB3 per Software als normaler I/O Pin konfiguriert wurde, kann er trotzdem mit dem ST-Link Adapter als SWO Ausgang umgestellt werden.

Die SWJ Schnittstelle kann unabhängig von der Software-Konfiguration auf jeden Fall während des Reset geöffnet werden, siehe Verbindungsoptionen.

Trace Meldungen ausgeben

Man kann den SWO (=PB3) Ausgang des Mikrocontrollers dazu benutzen, Diagnose Meldungen auszugeben. Diese Schnittstelle ist effizienter als USART, weil ihre Bitrate höher ist (nämlich 1/4 der CPU Taktfrequenz) und weil sie über einen kleinen FIFO Puffer (10 Bytes) verfügt.

SWO wird mit dem ST-Link Adapter aktiviert. Dann ist PB3 vorübergehend ein Ausgang mit Ruhe-Pegel High. Während der seriellen Datenübertragung liefert er Low-Impulse.

Die CMSIS Funktion ITM_SendChar() gibt ein Zeichen auf der SWO Leitung aus. ITM bedeutet "Instrumentation Trace Message":

#include <stdio.h>
#include "stm32f3xx.h"

// delay loop for the default 8 MHz clock with optimizer enabled
void delay(uint32_t msec)
{
    for (uint32_t j=0; j<2000UL*msec; j++)
    {
        __NOP();
    }
}

// Output a trace message
void ITM_SendString(char *ptr)
{
    while (*ptr)
    {
        ITM_SendChar(*ptr);
        ptr++;
    }
}

int main(void)
{
    while (1)
    {
        ITM_SendString("Hello World!\n");
        delay(500);
    }
}
Damit das Programm compilierbar ist, trage in das Textfeld unter Properties/C/C++ Build/Settings/Tool Settings/MCU GCC Linker/Miscellaneous/Linker Flags -specs=nano.specs -specs=nosys.specs ein.

Damit die Ausgabefunktionen der Standard C Library (z.B. printf(), puts(), putchar()) funktionieren, musst du folgende Funktion implementieren:

// Redirect standard output to the trace SWO output
int _write(int file, char *ptr, int len)
{
    for (int i=0; i<len; i++)
    {
        ITM_SendChar(*ptr++);
    }
    return len;
}

Die System Workbench kann diese Meldungen nicht anzeigen, aber man kann den Debugger so konfigurieren, dass er sie in eine Datei schreibt. Das geht so:

Zuerst musst du das Projekt einmal debuggen (auf den grünen Käfer klicken), damit die IDE ein "Configuration Script" anlegt. Danach klicke mit der rechten Maustaste auf das Projekt, dann auf Debug As/Debug Configurations.... Gehe in dem folgenden Dialog in den Debugger Tab und stelle auf ein User defined configuration script um. Dann schließe den Dialog mit dem Button "Close":

Nun öffne dieses "Configuration Script" im Texteditor und hänge zwei Zeilen an:

tpiu config internal debug.txt uart off 8000000
itm port 0 on
Die Zahl 8000000 muss der CPU Taktfrequenz entsprechen.

Bei der nächsten debug Sitzung werden die Trace Meldungen dann in die Datei debug.txt (im Hauptverzeichnis des Projektes) geschrieben. Zur forlaufenden Anzeige benutze ich unter Windows den Befehl tail -f debug.txt in einem CygWin Fenster:

Die Datei enthält zwischen den Buchstaben nicht darstellbare Steuerzeichen. Falls tail diese unerwünscht anzeigt, kann man sie so heraus filtern: tail -f debug.txt | tr -dc '[:print:]\n'

Man kann die Schnittstelle auch so konfigurieren, dass sie die Meldungen im gleichen Format ausgibt, wie ein normaler serieller Port:

monitor tpiu config external uart off 8000000 2000000
itm port 0 on
Die Zahl 8000000 muss der CPU Taktfrequenz entsprechen, die Zahl 2000000 ist die serielle Baudrate. Nun kann man die Ausgabe mit einem gewöhnlichen USB-UART Adapter empfangen.

Außerhalb einer Debugger-Sitzung kann man das ST-Link Utility mit einem ST-Link Adapter zur Anzeige benutzen, und zwar über dem Menüpunkt ST-LINK/Printf via SWO Viewer. Stelle die richtige Taktfrequenz ein und klicke dann auf Start.

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, zum Debuggen eignet er sich jedoch nicht.

Der Bootloader unterstüzt folgende Anschlüsse:

ModellUSARTUSARTUSBI2C
TxD,RxDTxD,RxDD-,D+SCL,SDA
STM32F301xxPA9,10PA2,3
STM32F302x6 and x8PA9,10PA2,3PA11,12
STM32F302xB and xCPA9,10PD5,6PA11,12
STM32F302xD and xEPA9,10PA2,3PA11,12
STM32F303x6 and x8PA9,10PA2,3PB6,7
STM32F303xB and xCPA9,10PD5,6PA11,12
STM32F303xD and xEPA9,10PA2,3PA11,12
STM32F334xxPA9,10PA2,3PB6,7
STM32F373xxPA9,10PD5,6PA11,12

Der Bootloader wird durch einen Jumper am Pin Boot0 aktiviert. Dabei wird auch das Option-Byte Boot1 gelesen, welches mit der Programmier-Software einstellbar ist.

Boot0Boot1Starte von
LowegalFlash ab Adresse 0x0800 0000, gemappt auf 0x0000 0000
HighLowBootloader
HighHighRAM ab Adresse 0x2000 0000, gemappt auf 0x0000 0000

Boot0 und Boot1 werden beim Reset und beim Aufwachen aus dem Standby Modus gelesen. Um den Bootloader zu aktivieren, setzt man den Pin Boot0=High und dann drückt man den Reset Knopf.

Weitere Informationen zum Bootloader stehen in der Application Note AN2606.

Serieller Bootloader

Die Verbindung zum PC wird mit einem USB-UART Adapter wie diesem hergestellt:

Folgende Verbindungen sind nötig:

PC USB-UARTSTM32F3 USARTBeschreibung
TxDRxDDaten
RxDTxDDaten
GND GNDGemeinsame Masse

Als Taktquelle dient der interne R/C Oszillator, dessen Frequenz bei 3,3 V und Zimmertemperatur meistens ausreichend genau ist. Der Bootloader erkennt die Baudrate automatisch. Es werden 8 Datenbits und gerade Parität (even) verwendet.

USB Bootloader

Der USB Bootloader funktioniert nur, wenn ein Quarz mit 24, 18, 16, 12, 9, 8, 6, 4 oder 3 MHz angeschlossen ist. An PA12 gehört ein 1,5 kΩ Pull-Up Widerstand nach 3,3V. Weitere Anwendungshinweise findest du in der Application Note AN2606.

GCC Optionen

Newlib

Newlib ist die Standard-Bibliothek für Linux, während newlib-nano für Mikrocontroller optimiert wurde. Du kannst eine Menge Speicherplatz sparen, indem du auf die nano Version der Library wechselst. Trage dazu in das Textfeld unter Properties/C/C++ Build/Settings/Tool Settings/MCU GCC Linker/Miscellaneous/Linker Flags -specs=nano.specs ein.

Wenn das Programm die stdio.h Library benutzt, muss man entweder einige Funktionen zum Zugriff auf die Konsole implementieren oder durch die Option -specs=nosys.specs auf Dummy Funktionen zurückgreifen.

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 kB Flash Speicher. Für das Parsen von Fließkommazahlen benötigt man die Option -u _scanf_float.

Ich habe gemessen, wie viel Speicher die Funktionen puts() und printf() von der newlib-nano ohne Fließkomma-Unterstützung benötigen:

FunktionHeapStackCode (Flash)
puts()1468162532 Bytes
printf()1468164020 Bytes

Der Speicherbedarf 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, dann brechen die Funktionen mit einer HardFault Exception ab.

Assembler Listing

Wenn du sehen willst, welchen Assembler-Code der Compiler erzeugt, dann trage in in das Textfeld unter
Properties/C/C++ Build/Settings/Tool Settings/MCU GCC Compiler/Miscellaneous/Other Flags
-Wa,-adhlns="$(@:%.o=%.lst)"
ein. Du findest dann für jede Quell-Datei eine *.lst Datei im Verzeichnis Debug oder Release.

Optimizer

Unter Properties/C/C++ Build/Settings/Tool Settings/MCU GCC Compiler/Optimization kann man beeinflussen, mit welcher Strategie der Compiler das Programm optimiert (umstrukturiert).

OptionZielBeschreibung
-O0DebuggingKeine Optimierung. Der Assembler Code entspricht genau dem C-Code, was für den Debugger optimal ist. Aber das Programm läuft viel Langsamer, als mit Optimierung.
-O1CompilierzeitDer Compiler wendet nur einfache Optimierungen an, die er schnell umsetzen kann. Nicht empfehlenswert.
-O2GeschwindigkeitGute Geschwindigkeit, von ARM als Standardeinstellung empfohlen.
-O3GeschwindigkeitBeste Geschwindigkeit, unter Umständen wird der Code aber viel größer.
-OsCode-GrößeGute Geschwindigkeit bei möglichst geringer Code-Größe
-OgDebuggingDas Programm wird so weit wie möglich auf Geschwindigkeit optimiert, ohne den Debugger zu beeinträchtigen.

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 Optimizer bevorzugt CPU Register für Variablen. Unter Umständen entfernt der Optimizer ganze Prozeduren und ersetzt sie durch anderen (inline) Code. Wiederholschleifen werden teilweise durch eine längere Sequenz von Code ersetzt, wenn dadurch die Geschwindigkeit besser wird.

Die vollständige Liste der Optimierungen befindet sich hier.

Startup-Code

Im Gegensatz zu allen mir bekannten Compilern für 8bit Mikrocontroller befindet sich der Startup-Code und die Interrupt-Vektor Tabelle in editierbaren Dateien (sysmem.c und startup_stm32.s). Der Projekt-Assistent in der IDE legt diese Dateien automatisch an. Für erste Versuche kann man sie unverändert benutzen.

Falls vorhanden, führt der Startup-Code eine Funktion mit folgender Signatur aus, bevor statische Objekte konstruiert werden und bevor main() ausgeführt wird:

void SystemInit(void) {}
Bei großen C++ Projekten kann es sich lohnen, die Erhöhung der Taktfrequenz dort unterzubringen, denn dann startet das Programm schneller.

Speicher-Struktur

Obwohl der Prozessor in Harvard Architektur gestaltet ist, benutzt er einen gemeinsamen Adress-Raum für Programm, Daten und I/O Register. Dadurch können alle I/O 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 32bit groß.

Die Befehle sind teilweise 16bit und teilweise 32bit groß.

Daten werden als 8, 16 oder 32bit geladen. Sie müssen nicht zwingend an der 32bit Wortgröße ausgerichtet sein. Aber man erreicht bessere Geschwindigkeit, wenn 16bit Daten an 16bit Adressen und 32bit Daten an 32bit Adressen ausgerichtet sind. Der Compiler kümmert sich automatisch darum.

Die Register für I/O Funktionen sind überwiegend 32bit breit.

Der arm-gcc Compiler legt konstante Zeichenketten (im Gegensatz zum avr-gcc) in den Flash Speicher, ohne dafür RAM zu verbrauchen.

Funktionsaufrufe

Bei Funktionsaufrufen werden bis zu 4 Parameter durch Register übergeben. Dabei ist es vorteilhaft, sie als 32bit Typ zu deklarieren, um Konvertierungen zu vermeiden. Bei mehr als 4 Parametern wird das RAM zur Übergabe benutzt, dann sind 8bit, 16bit und 32bit Typen gleich langsam.

Der Rückgabewert einer Funktion wird ebenfalls in einem Register übermittelt und sollte daher 32bit groß sein, falls Geschwindigkeit wichtig ist.

Stack

Der Stapel speichert ausschließlich 32bit 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:

Interrupt-Vektoren

Der Flash-Speicher beginnt immer mit der Exception- und Interrupt-Vektor Liste. Jeder Eintrag in der Liste ist eine 32bit Sprungadresse. Diese ist im Referenzhandbuch Kapitel "Interrupt and exception vectors" dokumentiert.

Der Quelltext dazu befindet sich in der Datei startup/startup_stm32.s. Dort findest du die vorgegebenen Namen der C-Funktionen. Die folgende Tabelle gilt für alle STM32F3 Modelle:

AddressARM Exception Nr.CMSIS Interrupt Nr.ISR Handler FunctionDescription
ARM Processor Exceptions
0x0000Initial value for the stack pointer MSP.
0x0004Reset_HandlerInitial value for the program counter, points to assembler startup code.
0x00082-14NMI_Handler()Non maskable interrupt. The RCC Clock Security System (CSS) is linked to the NMI vector.
0x000C3-13HardFault_Handler()Hardware fault. Can optionally be splitted in the next 3 entries:
0x00104-12MemManage_Handler()Memory protection fault
0x00145-11BusFault_Handler()Pre-fetch or memory access fault
0x00186-10UsageFault_Handler()Undefined instruction, illegal unaligned access, invalid state, division by zero
0x001C7-9reserved
0x00208-8
0x00249-7
0x002810-6
0x002C11-5SVC_Handler()Supervisor Call, triggered by the SVC command (formerly known as SVI). Can be used to call operating system services.
0x003012-4reserved
0x003413-3
0x003814-2PendSV_Handler()Pendable Request for System Service. Triggered by the operating system by writing to the ICSR register, to switch the context.
0x003C15-1SysTick_Handler()Called when the systick timer reaches 0
STM32 Hardware Interrupts
0x00400WWDG_IRQHandler()Window Watchdog
0x00441PVD_IRQHandler()PVD through EXTI Line detection
0x00482TAMP_STAMP_IRQHandler()Tamper and TimeStamp through EXTI Line19
0x004C3RTC_WKUP_IRQHandler()RTC wakeup timer through EXTI Line20
0x00504FLASH_IRQHandler()Flash
0x00545RCC_IRQHandler()RCC
0x00586EXTI0_IRQHandler()EXTI Line0
0x005C7EXTI1_IRQHandler()EXTI Line1
0x00608EXTI2_TSC_IRQHandler()EXTI Line2 and Touch sensing
0x00649EXTI3_IRQHandler()EXTI Line3
0x006810EXTI4_IRQHandler()EXTI Line4
0x006C11DMA1_CH1_IRQHandler()DMA1 channel 1
0x007012DMA1_CH2_IRQHandler()DMA1 channel 2
0x007413DMA1_CH3_IRQHandler()DMA1 channel 3
0x007814DMA1_CH4_IRQHandler()DMA1 channel 4
0x007C15DMA1_CH5_IRQHandler()DMA1 channel 5
0x008016DMA1_CH6_IRQHandler()DMA1 channel 6
0x008417DMA1_CH7_IRQHandler()DMA1 channel 7
0x008818ADC1_2_IRQHandler()ADC1 and ADC2
0x008C19USB_HP_CAN_TX_IRQHandler()USB High Priority/CAN_TX
0x009020USB_LP_CAN_RX0_IRQHandler()USB Low Priority/CAN_RX0
0x009421CAN_RX1_IRQHandler()CAN_RX1
0x009822CAN_SCE_IRQHandler()CAN_SCE
0x009C23EXTI9_5_IRQHandler()EXTI Line[9:5]
0x00A024TIM1_BRK_TIM15_IRQHandler()TIM1 Break/TIM15
0x00A425TIM1_UP_TIM16_IRQHandler()TIM1 Update/TIM16
0x00A826TIM1_TRG_COM_TIM17_IRQHandler()TIM1 trigger and commutation/TIM17
0x00AC27TIM1_CC_IRQHandler()TIM1 capture compare
0x00B028TIM2_IRQHandler()TIM2
0x00B429TIM3_IRQHandler()TIM3
0x00B830TIM4_IRQHandler()TIM4
0x00BC31I2C1_EV_EXTI23_IRQHandler()I2C1 event and EXTI Line23
0x00C032I2C1_ER_IRQHandler()I2C1 error
0x00C433I2C2_EV_EXTI24_IRQHandler()I2C2 event and EXTI Line24
0x00C834I2C2_ER_IRQHandler()I2C2 error
0x00CC35SPI1_IRQHandler()SPI1
0x00D036SPI2_IRQHandler()SPI2
0x00D437USART1_EXTI25_IRQHandler()USART1 and EXTI Line 25
0x00D838USART2_EXTI26_IRQHandler()USART2 and EXTI Line 26
0x00DC29USART3_EXTI28_IRQHandler()USART3 and EXTI Line 28
0x00E040EXTI15_10_IRQHandler()EXTI Line[15:10]
0x00E441RTCAlarm_IRQHandler()RTC alarm
0x00E842USB_WKUP_IRQHandler()USB wakeup from Suspend (EXTI line 18)
0x00EC43TIM8_BRK_IRQHandler()TIM8 break
0x00F044TIM8_UP_IRQHandler()TIM8 update
0x00F445TIM8_TRG_COM_IRQHandler()TIM8 Trigger and commutation
0x00F846TIM8_CC_IRQHandler()TIM8 capture compare
0x00FC47ADC3_IRQHandler()ADC3
0x010048FMC_IRQHandler()FMC
0x010449reserved
0x010850
0x010C51SPI3_IRQHandler()SPI3
0x011052UART4_EXTI34_IRQHandler()UART4 and EXTI Line 34
0x011453UART5_EXTI35_IRQHandler()UART5 and EXTI Line 35
0x011854TIM6_DACUNDER_IRQHandler()TIM6 and DAC1 underrun
0x011C55TIM7_IRQHandler()TIM7
0x012056DMA2_CH1_IRQHandler()DMA2 channel1
0x012457DMA2_CH2_IRQHandler()DMA2 channel2
0x012858DMA2_CH3_IRQHandler()DMA2 channel3
0x012C59DMA2_CH4_IRQHandler()DMA2 channel4
0x013060DMA2_CH5_IRQHandler()DMA2 channel5
0x013461ADC4_IRQHandler()ADC4
0x013862reserved
0x013C63
0x014064COMP123_IRQHandler()COMP1, COMP2 and COMP3 combined with EXTI Lines 21, 22 and 29
0x014465COMP456_IRQHandler()COMP4, COMP5 and COMP6 combined with EXTI Lines 30, 31 and 32
0x014866COMP7_IRQHandler()COMP7 combined with EXTI Line 33
0x014C67reserved
0x015068
0x015469
0x015870
0x015C71
0x016072I2C3_EV_IRQHandler()I2C3 Event
0x016473I2C3_ER_IRQHandler()I2C3 Error
0x016874USB_HP_IRQHandler()USB High priority
0x016C75USB_LP_IRQHandler()USB Low priority
0x017076USB_WKUP_EXTI_IRQHandler()USB wake up from Suspend and EXTI Line 18
0x017477TIM20_BRK_IRQHandler()TIM20 Break
0x017878TIM20_UP_IRQHandler()TIM20 Upgrade
0x017C79TIM20_TRG_COM_IRQHandler()TIM20 Trigger and Commutation
0x018080TIM20_CC_IRQHandler()TIM20 Capture Compare
0x018481FPU_IRQHandler()Floating point
0x018882reserved
0x018C83
0x019084SPI4_IRQHandler()SPI4 SPI4 Global

Bei STM32F334 gilt jedoch abweichend:

AddressARM Exception Nr.CMSIS Interrupt Nr.ISR Handler FunctionDescription
0x011854TIM6_DAC1_IRQHandler()TIM6 global and DAC1 underrun
0x011C55TIM7_DAC2_IRQHandler()TIM7 global and DAC2 underrun
0x014064COMP2_IRQHandler()COMP2 combined with EXTI Lines
0x014465COMP4_6_IRQHandler()COMP4 and COMP6 combined with EXTI Lines 30 and 32 respectively
0x014C67HRTIM_Master_IRQHandler()HRTIM master timer
0x015068HRTIM_TIMA_IRQHandler()HRTIM timer A
0x015469HRTIM_TIMB_IRQHandler()HRTIM timer B
0x015870HRTIM_TIMC_IRQHandler()HRTIM timer C
0x015C71HRTIM_TIMD_IRQHandler()HRTIM timer D
0x016072HRTIM_TIME_IRQHandler()HRTIM timer E
0x016473HRTIM_TIM_FLT_IRQHandler()HRTIM fault
Achtung: In der startup_stm32.h von der System Workbench sind die Handler vom HR-Timer anderen (falschen) Adressen zugewiesen!

Über das SCB->VTOR Register kann man den Ort der Liste verändern um sie z.B. ins RAM zu verschieben.

Interrupt Controller

Der Interrupt-Controller NVIC steuert die Verarbeitung von Unterbrechungs-Signalen. Er ist Bestandteil des ARM Kerns.

Solche 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.

Reset, NMI und HardFault haben immer die höchste Priorität, bei allen anderen kann man die Priorität einstellen. Interrupt-Handler können durch höher priorisierte Interrupts unterbrochen werden.

Die wichtigsten CMSIS Funktionen zur Konfiguration des Interrupt-Systems sind:

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.

Extended Interrupts

Die externen Interrupt-Signale und auch einige interne durchlaufen einen erweiterten Schaltkreis, der zusätzliche Konfiguration erfordert. Man erkennt sie am Stichwort EXTI.

Externe Interrupt-Signale von I/O Pins werden sehr flexibel durch die Register SYSCFG->EXTICR[0-3] mit den Kanälen des Interrupt-Controllers verbunden. Die internen Unterbrechungen sind hingegen fest einem Kanal zugeordnet.

Interrupt Flanken

Viele erweiterte Unterbrechungen werden durch Flanken (Signalwechsel) ausgelöst. Man erkennt sie an den Bits im Register EXTI->RTSR oder EXTI->FTSR.

Durch die Verwendung von Flanken ist automatisch sicher gestellt, dass der Handler nicht immer wieder erneut ausgeführt wird, falls ein Interrupt Signal für längere Zeit ansteht.

Flanken gesteuerte Interrupts gehen nicht verloren, wenn das Signal schon wieder verschwindet bevor der Handler aufgerufen wurde. Der Interrupt-Controller merkt sich, dass da mal eine Anforderung vorlag, die noch nicht abgearbeitet wurde.

In diesem Zusammenhang ist das Register EXTI->PR (bzw. EXTI->PR2) wichtig. Dort merkt sich der Interrupt-Controller den Zustand. Während der Initialisierung muss man das Bit zurück setzen, um sicher zu stellen, dass die erste Flanke zuverlässig erkannt wird. Man schreibt eine 1 in das jeweilige Bit, damit es auf 0 zurück gesetzt wird.

Interrupt Masken

Im Register EXTI->IMR (bzw. EXTI->IMR2) werden Unterbrechungen maskiert. Man muss hier eine 1 in das jeweilige Bit schreiben, damit eine Interrupt-Leitung wirksam wird. Standardmäßig sind die meisten Interrupts maskiert, sind also ohne Wirkung.

Das folgende Beispiel löst einen Interrupt aus, wenn das Signal an PC13 von Low nach High wechselt. Beim Nucleo-Board ist PC13 mit dem blauen Taster verbunden.

#include <stdio.h>
#include "stm32f3xx.h"

// Output a trace message
void ITM_SendString(char *ptr)
{
    while (*ptr)
    {
        ITM_SendChar(*ptr);
        ptr++;
    }
}

void EXTI15_10_IRQHandler()
{
    // Clear pending interrupt flag
    SET_BIT(EXTI->PR, EXTI_PR_PR13);

    // Output a trace message
    ITM_SendString("irq\n");
}

int main(void)
{
    // Enable clock of I/O features
    SET_BIT(RCC->AHBENR, RCC_AHBENR_GPIOCEN);
    SET_BIT(RCC->APB2ENR, RCC_APB2ENR_SYSCFGEN);

    // Assign PC13 to EXTI0 with rising edge
    MODIFY_REG(SYSCFG->EXTICR[3], SYSCFG_EXTICR4_EXTI13, SYSCFG_EXTICR4_EXTI13_PC);
    SET_BIT(EXTI->IMR, EXTI_IMR_MR13);
    SET_BIT(EXTI->RTSR, EXTI_RTSR_TR13);
    NVIC_EnableIRQ(EXTI15_10_IRQn);

    // Clear pending interrupt flag
    SET_BIT(EXTI->PR, EXTI_PR_PR13);

    // Endless loop
    while (1) {}
}

Event Masken

Neben den Unterbrechungen steuert der NVIC auch Ereignisse. Ereignisse verwendet man üblicherweise, um die CPU aus aus einem Wait- oder Sleep- Zustand aufzuwecken. So hält zum Beispiel der Befehl __WFE() die CPU bis zum nächsten Ereignis an. Siehe Absatz Powermanagement.

Standardmäßig sind alle Ereignisse maskiert. Man muss im Register EXTI->EMR (bzw. EXTI->EMR2) das entsprechende Bit auf 1 setzen, um ein EXTI Signal als Ereignis zu behandeln.

Taktgeber

Ich habe ziemlich oft gelesen, dass das komplexe System zur Takterzeugung für Anfänger ein großes Hindernis sei. Das sehe ich anders, denn nach einem Reset wird der Mikrocontroller automatisch mit seinem internen 8 MHz R/C Oszillator getaktet. Damit kann man schon eine Menge sinnvoller Programme schreiben.

Die Taktsignale für den ARM Kern (dazu gehört auch der SysTick Timer), sowie RAM und Flash sind automatisch aktiviert. Alle anderen Komponenten muss man ggf. selbst einschalten, was ganz einfach durch Setzen von Bits in den Registern RCC->APB1ENR, RCC->APB2ENR und RCC->AHBENR erledigt wird.

Jetzt kommt der komplizierte Teil. Der System-Takt (SYSCLK) kann aus folgenden Quellen bezogen werden:

Für Watchog und Echtzeit-Uhr (RTC) sind weitere Quellen vorgesehen:

Mehrere Vorteiler und ein PLL Multiplikator können kombiniert werden, um unterschiedliche Taktfrequenzen zu erreichen. Am besten schaut man sich das mal in CubeMX an. Dort kann man schön sehen, welche Parameter konfigurierbar sind und wie sie zusammen wirken.

Das folgende Bild zeigt die Standardvorgabe vom STM32F303x6 und x8 nach einem Reset:

Das folgende Bild zeigt die Standardvorgabe vom STM32F303xB und xC nach einem Reset:

Das folgende Bild zeigt die Standardvorgabe vom STM32F303xD und xE nach einem Reset:

Wenn man den Systemtakt über 24 MHz erhöht, muss man für den Flash Speicher 1 Wait-State einstellen. Bei mehr als 48 MHz sind 2 Wait-States nötig. Bei mehr als 36 MHz ist außerdem ein Vorteiler für den internen APB1 Bus (auch low-speed I/O genannt) einzustellen.

Beispiel für den STM32F303xD und xE, 64 MHz mit dem internen HSI Oszillator:

// The current clock frequency
uint32_t SystemCoreClock=8000000;

// Change system clock to 64 MHz using internal 8 MHz R/C oscillator
// Called by Assembler startup code
void SystemInit(void)
{
    // Flash latency 2 wait states
    MODIFY_REG(FLASH->ACR, FLASH_ACR_LATENCY, FLASH_ACR_LATENCY_1);

    // 64 MHz using the 8 MHz/2 HSI oscillator with 16x PLL, lowspeed I/O runs at 32 MHz
    WRITE_REG(RCC->CFGR, RCC_CFGR_PLLMUL16 + RCC_CFGR_PPRE1_DIV2);

    // Enable PLL
    SET_BIT(RCC->CR, RCC_CR_PLLON);

    // Wait until PLL is ready
    while(!READ_BIT(RCC->CR, RCC_CR_PLLRDY)) {}

    // Select PLL as clock source
    MODIFY_REG(RCC->CFGR, RCC_CFGR_SW, RCC_CFGR_SW_PLL);


    // Update variable
    SystemCoreClock=64000000;
}
Die obige Delay Schleife läuft danach allerdings nicht 8x schneller, sondern nur 6x schneller. Der Grund dafür ist, dass der Flash jetzt mit 2 Waitstates betrieben werden muss und der Prefetch-Buffer (der dies ausgleicht) nur direkt aufeinanderfolgende Befehle optimiert. Bei jeden Rücksprung in der Schleife wird der Prefetch-Buffer geleert.

Durch die Angabe __attribute__((section(".data"))) könnte man Funktionen etwas schneller ohne Waitstate aus dem RAM ausführen, da dieser ohne Waitstates arbeitet.

Beispiel für den STM32F303xD und xE, 72 MHz mit einem 8 MHz Quarz (HSE Oszillator):

// The current clock frequency
uint32_t SystemCoreClock=8000000;

// Change system clock to 72 MHz using 8 MHz crystal
// Called by Assembler startup code
void SystemInit(void)
{
    // Flash latency 2 wait states
    MODIFY_REG(FLASH->ACR, FLASH_ACR_LATENCY, FLASH_ACR_LATENCY_1);
    
    // Enable HSE oscillator
    SET_BIT(RCC->CR, RCC_CR_HSEON);

    // Wait until HSE oscillator is ready
    while(!READ_BIT(RCC->CR, RCC_CR_HSERDY)) {}

    // 72 MHz using the 8 MHz HSE oscillator with 9x PLL, lowspeed I/O runs at 36 MHz
    WRITE_REG(RCC->CFGR, RCC_CFGR_PLLSRC + RCC_CFGR_PLLMUL9 + RCC_CFGR_PPRE1_DIV2);

    // Enable PLL
    SET_BIT(RCC->CR, RCC_CR_PLLON);

    // Wait until PLL is ready
    while(!READ_BIT(RCC->CR, RCC_CR_PLLRDY)) {}

    // Select PLL as clock source
    MODIFY_REG(RCC->CFGR, RCC_CFGR_SW, RCC_CFGR_SW_PLL);

    // Update variable
    SystemCoreClock=72000000;
}

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.

#include <stdint.h>
#include "stm32f3xx.h"

// The current clock frequency
uint32_t SystemCoreClock=8000000;

// Counts milliseconds
volatile uint32_t systick_count=0;

// Interrupt handler
void SysTick_Handler(void)
{
    systick_count++;
}

// Delay some milliseconds.
// Note that effective time may be up to 1ms shorter than requested.
void delay_ms(int ms)
{
    uint32_t start=systick_count;
    while (systick_count-start<ms);
}

int main(void)
{
    // Initialize the timer for 1 ms intervals
    SysTick_Config(SystemCoreClock/1000);
    
    // Delay 2 seconds
    delay_ms(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.

Digitale Pins

Digitale I/O Pins können erst benutzt werden, nachdem man ihre Takversorgung im Register RCC->IOPENR eingeschaltet hat. Standardmäßig sind fast alle I/O Pins als digitaler Eingang konfiguriert. Um deren Status abzufragen, liest man das jeweilige GPIOx->IDR Register.

Im Register GPIOx->MODER konfiguriert man einen Pin als Ausgang oder für alternativen Funktionen (wie z.B. serieller Port oder PWM Timer). Wenn man einen I/O Pin für alternative Funktionen verwendet, muss man außerdem in GPIOx->AFR[0] oder GPIOx->AFR[1] einstellen, welche alternative Funktion das sein soll.

Direkte Schreibzugriffe sind über das Register GPIOx->ODR möglich. Um einzelne Pins atomar auf High oder Low zu schalten, verwendet man jedoch das GPIOx->BSRR Register.

Im Register GPIOx->OSPEEDR kann man die maximale Frequenz der Ausgänge auf 2, 10 oder 50 MHz einstellen. Damit beeinflusst man die Geschwindigkeit, mit der die Spannung von Low nach High (und zurück) wechselt. Der maximale Laststrom wird dadurch nicht verändert. Im Sinne von elektromagnetischer Verträglichkeit soll man hier immer den niedrigsten Wert einstellen, der zur Anwendung passt.

Im Register GPIOx->OTYPER kann man I/O Pins auf den Open-Drain Modus umkonfigurieren und im Register GPIOx->PUPDR kann man optional interne Pull-Up oder Pull-Down Widerstände einschalten.

Schaue Dir die Beschreibung der GPIO Register im Referenzhandbuch an. Die alternativen Funktionen der GPIO Pins sind im Datenblatt des Mikrocontrollers unter dem Stichwort "alternate functions" tabellarisch beschrieben.

Analoge Eingänge

Fast I/O Pins sind standardmäßig für digitale Eingabe konfiguriert. Für analoge Nutzung setzt man beide Bits im GPIOx->MODER Register:
// Configure PA4 as analog input
MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER4, GPIO_MODER_MODER4_0 + GPIO_MODER_MODER4_1);

Initialisierung des ADC1 für einzelne Lesezugriffe:

// Initialize the ADC1 for single conversion mode
void init_analog()
{
    // Enable clock for ADC
    SET_BIT(RCC->AHBENR, RCC_AHBENR_ADC12EN);

    // Disable the ADC
    if (READ_BIT(ADC1->ISR, ADC_ISR_ADRDY))
    {
        SET_BIT(ADC1->ISR, ADC_ISR_ADRDY);
    }
    if (READ_BIT(ADC1->CR, ADC_CR_ADEN))
    {
        SET_BIT(ADC1->CR, ADC_CR_ADDIS);
    }

    // Wait until ADC is disabled
    while (READ_BIT(ADC1->CR, ADC_CR_ADEN));

    // Enable ADC voltage regulator
    MODIFY_REG(ADC1->CR, ADC_CR_ADVREGEN, 0);
    MODIFY_REG(ADC1->CR, ADC_CR_ADVREGEN, ADC_CR_ADVREGEN_0);

    // Delay 1-2 ms
    delay_ms(2);

    // ADC Clock = HCLK/4
    MODIFY_REG(ADC12_COMMON->CCR, ADC12_CCR_CKMODE, ADC12_CCR_CKMODE_0 + ADC12_CCR_CKMODE_1);

    // Single ended mode for all channels
    WRITE_REG(ADC1->DIFSEL,0);

    // Start calibration for single ended mode
    CLEAR_BIT(ADC1->CR, ADC_CR_ADCALDIF);
    SET_BIT(ADC1->CR, ADC_CR_ADCAL);

    // Wait until the calibration is finished
    while (READ_BIT(ADC1->CR, ADC_CR_ADCAL));

    // Clear the ready flag
    SET_BIT(ADC1->ISR, ADC_ISR_ADRDY);

    // Enable the ADC repeatedly until success (workaround from errata)
    do
    {
        SET_BIT(ADC1->CR, ADC_CR_ADEN);
    }
    while (!READ_BIT(ADC1->ISR, ADC_ISR_ADRDY));

    // Select software start trigger
    MODIFY_REG(ADC1->CFGR, ADC_CFGR_EXTEN, 0);

    // Select single conversion mode
    CLEAR_BIT(ADC1->CFGR, ADC_CFGR_CONT);

    // Set sample time to 32 cycles
    MODIFY_REG(ADC1->SMPR1, ADC_SMPR1_SMP1, ADC_SMPR1_SMP1_2);
}

Lesen eines analogen Eingangs von ADC1:

// Read from an analog input of ADC1
uint32_t read_analog(uint32_t channel)
{
    // Number of channels to convert: 1
    MODIFY_REG(ADC1->SQR1, ADC_SQR1_L, 0);
    
    // Select the channel
    MODIFY_REG(ADC1->SQR1, ADC_SQR1_SQ1, channel<<ADC_SQR1_SQ1_Pos);

    // Clear the finish flag
    CLEAR_BIT(ADC1->ISR, ADC_ISR_EOC);

    // Start a conversion
    SET_BIT(ADC1->CR, ADC_CR_ADSTART);

    // Wait until the conversion is finished
    while (!READ_BIT(ADC1->ISR, ADC_ISR_EOC));
    while (READ_BIT(ADC1->CR, ADC_CR_ADSTART));

    // Return the lower 12 bits of the result
    return ADC1->DR & 0b111111111111;
}

Achtung: Die Channel Nummern sind ein bisschen seltsam. PA3 = ADC1_IN4 aber die zugehörige Channel Nummer ist 5!

Der ADC kann so konfiguriert werden, dass er mehrere Eingänge kontinuierlich liest. Mittels DMA können die Messergebnisse automatisch ins RAM übertragen werden. Dafür habe ich hier kein Beispiel parat.

PWM Ausgänge

Die Timer 1, 8 und 20 können jeweils 6 PWM Signale erzeugen. Damit kann man z.B. die Helligkeit von Lampen oder die Drehzahl von Motoren steuern. Die Timer 2, 3 und 4 haben jeweils vier PWM Kanäle. Die Timer 15, 16 und 17 haben jeweils 2 PWM Kanäle.

Der Timer 2 hat als einziger 32bit Auflösung für maximal 4294967295 Stufen, die anderen haben mit 16bit maximal 65535 Stufen.

Die Taktfrequenz der Timer wird normalerweise vom Systemtakt abgeleitet und kann durch den AHB Prescaler, den ABP2 Prescaler (beide im Register RCC->CFGR), sowie dem Timer Prescaler in TIMx->PSC reduziert werden. Bei einigen Timern kann man für höhere Taktfrequenzen die PLL im RCC->CFGR3 Register auswählen, was maximal 144 MHz ergibt. Der STM32F334 ist noch schneller, aber auf den gehe ich hier nicht weiter ein.

Der Timer zählt fortlaufend von 0 an hoch bis zum Maximum, welches durch das TIMx->ARR Register festgelegt wird. Wenn der Maximalwert als 32768 festgelegt wird, können die Ausgangsimpulse wahlweise 1 bis 32768 Takte breit sein.

Für jeden Ausgang gibt es ein Vergleichs-Register TIMx->CCRy welches bestimmt, wie breit die Ausgangsimpulse sein sollen. Beim Extremwert 0 ist der Ausgang ständig Low. Bei Werten größer dem Maximum (in TIMx->ARR) ist der Ausgang ständig High. Mit der option "inverse polarity" im Register TIMx->CCER können die Ausgänge umgepolt werden, so dass sie Low-Impule liefern.

Das folgende Beispielprogramm benutzt einen Ausgang von Timer 2 (PA5), um eine Leuchtdiode unterschiedlich hell flimmern zu lassen:

#include "stm32f3xx.h"

// delay loop for the default 8 MHz clock with optimizer enabled
void delay(uint32_t msec)
{
    for (uint32_t j=0; j<2000UL*msec; j++)
    {
        __NOP();
    }
}

int main(void)
{
    // Enable Port A
    SET_BIT(RCC->AHBENR, RCC_AHBENR_GPIOAEN);

    // PA5 = TIM2_CH1 alternate function 1 (see data sheet)
    MODIFY_REG(GPIOA->AFR[0], GPIO_AFRL_AFRL5, 1UL<<GPIO_AFRL_AFRL5_Pos);
    MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER5, GPIO_MODER_MODER5_1);

    // Enable timer 2
    SET_BIT(RCC->APB1ENR, RCC_APB1ENR_TIM2EN);

    // Timer 2 channel 1 compare mode = PWM1 with the required preload buffer enabled
    MODIFY_REG(TIM2->CCMR1, TIM_CCMR1_OC1M + TIM_CCMR1_OC1PE, TIM_CCMR1_OC1M_2 + TIM_CCMR1_OC1M_1 + TIM_CCMR1_OC1PE);

    // Timer 2 enable channel 1 output
    SET_BIT(TIM2->CCER, TIM_CCER_CC1E);

    // Timer 2 inverse polarity for channel 1
    // SET_BIT(TIM2->CCER, TIM_CCER_CC1P);

    // Timer 2 clock prescaler, the PCLK2 clock is divided by this value +1.
    TIM2->PSC = 9; // divide clock by 10

    // Timer 2 auto reload register, defines the maximum value of the counter in PWM mode.
    TIM2->ARR = 32768; // 8000000/10/32768 = 27 pulses per second

    // Timer 2 enable counter and auto-preload
    SET_BIT(TIM2->CR1, TIM_CR1_CEN + TIM_CR1_ARPE);

    // endless loop
    while(1)
    {
        // Change the brightness of the LED (PA5) in 16 steps
        for (int i=0; i<=15; i++)
        {
            // Timer 2 channel 1 set PWM pulse width
            TIM2->CCR1 = (1<<i);

            delay(500);
        }
    }
}

Ich habe hier absichtlich eine sehr niedrige PWM Frequenz gewählt, damit man das Flackern der LED sehen kann. In einer realen Anwendung würde man natürich eine höhere PWM Frequenz über 100 Hz wählen.

Die Timer 1, 8, 15, 16, 17 und 20 können komplementäre Ausgangssignale mit Tot-Zeit erzeugen, was für den Eigenbau von H-Brücken nützlich ist.

Ich möchte darauf hinweisen, dass die Timer noch viele weitere Funktionen haben, die ich hier gar nicht alle zeigen kann. Mehr Informationen dazu gibt es zum Beispiel in der Application Note AN4776.

USART Schnittstelle

Der interne HSI Oszillator ist häufig aber nicht immer stabil genug, um die USART Schnittstellen zu betreiben. Es empfiehlt sich daher, auf eine externe Quelle (HSE) umzuschalten. In den folgenden Beispielen nutze ich der Einfachheit halber trotzdem den internen HSI Oszillator.

Je nach Taktfrequenz der Peripherie sind unterschiedliche Baudraten möglich:

Die serielle Schnittstelle USART1 liegt auf PA9 (TxD) und PA10 (RxD). Das folgende Beispielprogramm sendet regemäßig "Hello World!" aus. Außerdem sendet es alle empfangenen Zeichen als Echo wieder zurück. Das Senden findet direkt statt (ggf. mit Warteschleife) während der Empfang interrupt-gesteuert stattfindet:

#include <stdio.h>
#include "stm32f3xx.h"

uint32_t SystemCoreClock=8000000;

// Delay loop for the default 8 MHz CPU clock with optimizer enabled
void delay(uint32_t msec)
{
    for (uint32_t j=0; j<2000UL*msec; j++)
    {
        __NOP();
    }
}

// Redirect standard output to the serial port
int _write(int file, char *ptr, int len)
{
    for (int i=0; i<len; i++)
    {
        while(!(USART1->ISR & USART_ISR_TXE));
        USART1->TDR = *ptr++;
    }
    return len;
}

// Called after each received character
void USART1_EXTI25_IRQHandler(void)
{
    // read the received character
    char received=USART1->RDR;

    // send echo back
    while(!(USART1->ISR & USART_ISR_TXE));
    USART1->TDR = received;
}

int main(void)
{
    // Enable clock for Port A
    SET_BIT(RCC->AHBENR, RCC_AHBENR_GPIOAEN);

    // PA5 = Output for the LED
    MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER5, GPIO_MODER_MODER5_0);

    // Use system clock for USART1
    SET_BIT(RCC->APB2ENR, RCC_APB2ENR_USART1EN);
    MODIFY_REG(RCC->CFGR3, RCC_CFGR3_USART1SW, RCC_CFGR3_USART1SW_0);

    // PA9 (TxD) shall use the alternate function 7 (see data sheet)
    MODIFY_REG(GPIOA->AFR[1], GPIO_AFRH_AFRH1, 7UL<<GPIO_AFRH_AFRH1_Pos);
    MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER9, GPIO_MODER_MODER9_1);

    // PA10 (RxD) shall use the alternate function 7 (see data sheet)
    MODIFY_REG(GPIOA->AFR[1], GPIO_AFRH_AFRH2, 7UL<<GPIO_AFRH_AFRH2_Pos);
    MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER10, GPIO_MODER_MODER10_1);

    // Set baudrate
    USART1->BRR = (SystemCoreClock / 2400);

    // Enable transmitter, receiver and receive-interrupt of USART1
    USART1->CR1 = USART_CR1_UE + USART_CR1_TE + USART_CR1_RE + USART_CR1_RXNEIE;

    // Enable interrupt in NVIC
    NVIC_EnableIRQ(USART1_IRQn);

    while (1)
    {
        // LED on
        WRITE_REG(GPIOA->BSRR, GPIO_BSRR_BS_5);
        delay(500);

        puts("Hello");

        // LED off
        WRITE_REG(GPIOA->BSRR, GPIO_BSRR_BR_5);
        delay(500);
    }
}
Damit das Programm compilierbar ist, trage in das Textfeld unter Properties/C/C++ Build/Settings/Tool Settings/MCU GCC Linker/Miscellaneous/Linker Flags -specs=nano.specs -specs=nosys.specs ein.

Bei der Ausgabe von Text mit printf() und putchar() ist zu Beachten, dass die newlib Library die Zeichen in einem Puffer ansammelt, bis dieser entweder voll ist oder ein Zeilenumbruch erfolgt. Mit fflush(stdout) kann man erzwingen, dass die Ausgabe sofort erfolgt.

Jetzt kommt ein Beispiel für die zweite serielle Schnittstelle. Beim Nucleo-64 Board ist USART2 mit dem ST-Link Adapter verbunden, der diese wiederum über USB an einen virtuellen COM Port weiter leitet:

ST-Link CN3STM32F1 USART2Beschreibung
TxDRxD (=PA3)Daten
RxDTxD (=PA2)Daten
GND GNDGemeinsame Masse

Der ST-Link v2.1 unterstützt 600 bis 2000000 Baud.

#include <stdio.h>
#include "stm32f3xx.h"

uint32_t SystemCoreClock=8000000;

// Delay loop for the default 8 MHz CPU clock with optimizer enabled
void delay(uint32_t msec)
{
    for (uint32_t j=0; j<2000UL*msec; j++)
    {
        __NOP();
    }
}

// Redirect standard output to the serial port
int _write(int file, char *ptr, int len)
{
    for (int i=0; i<len; i++)
    {
        while(!(USART2->ISR & USART_ISR_TXE));
        USART2->TDR = *ptr++;
    }
    return len;
}

// Called after each received character
void USART2_EXTI26_IRQHandler(void)
{
    // read the received character
    char received=USART2->RDR;

    // send echo back
    while(!(USART2->ISR & USART_ISR_TXE));
    USART2->TDR = received;
}

int main(void)
{
    // Enable clock for Port A
    SET_BIT(RCC->AHBENR, RCC_AHBENR_GPIOAEN);

    // PA5 = Output for the LED
    MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER5, GPIO_MODER_MODER5_0);

    // Use system clock for USART2
    SET_BIT(RCC->APB1ENR, RCC_APB1ENR_USART2EN);
    MODIFY_REG(RCC->CFGR3, RCC_CFGR3_USART2SW, RCC_CFGR3_USART2SW_0);

    // PA2 (TxD) shall use the alternate function 7 (see data sheet)
    MODIFY_REG(GPIOA->AFR[0], GPIO_AFRL_AFRL2, 7UL<<GPIO_AFRL_AFRL2_Pos);
    MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER2, GPIO_MODER_MODER2_1);

    // PA3 (RxD) shall use the alternate function 7 (see data sheet)
    MODIFY_REG(GPIOA->AFR[0], GPIO_AFRL_AFRL3, 7UL<<GPIO_AFRL_AFRL3_Pos);
    MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER3, GPIO_MODER_MODER3_1);

    // Set baudrate
    USART2->BRR = (SystemCoreClock / 2400);

    // Enable transmitter, receiver and receive-interrupt of USART2
    USART2->CR1 = USART_CR1_UE + USART_CR1_TE + USART_CR1_RE + USART_CR1_RXNEIE;

    // Enable interrupt in NVIC
    NVIC_EnableIRQ(USART2_IRQn);

    while (1)
    {
        // LED an
        WRITE_REG(GPIOA->BSRR, GPIO_BSRR_BS_5);
        delay(500);

        puts("Hello");

        // LED aus
        WRITE_REG(GPIOA->BSRR, GPIO_BSRR_BR_5);
        delay(500);
    }
}

USB Schnittstelle

Die USB Schnittstelle erfordert ein umfangreiches Softwarepaket. Beinahe alle Programmierer binden daher fertige Implementierungen in ihr Programm ein.

Die USB Schnittstelle funktioniert Interrupt-getrieben. Immer wenn die Hardware ein kleines Datenpaket gesendet oder empfangen hat, löst sie einen Interrupt aus. Der Interrupthandler hat die Aufgabe, Anfragen des Host zu beantworten und Nutzdaten mit dem Pufferspeicher auszutauschen. Wenn man den Mikrocontroller beim Debuggen anhält, fällt die USB Schnittstelle sofort aus.

Der Systemtakt muss entweder 48 MHz oder 72 MHz betragen und aus einem Quarz gewonnen werden. Der USB Clock Prescaler wird dementsprechend auf 1 oder 1,5 gestellt, um die USB Schnittstelle mit 48 MHz zu takten. Der APB Bus muss mit mindestens 10 MHz getaktet werden.

Beim STM32F302x6/x8 teilt sich der USB Port seinen Speicher mit CAN, daher kann man die beiden Schnittstellen nicht gleichzeitig verwenden. Die STM32F301 und STM32F303kx haben keinen USB Port.

Die USB Buchse wird mit PA11 (D-) und PA12 (D+) verbunden. Es ist nicht nötig, den Modus (in, out, alternative) und Typ dieser Pins zu konfigurieren.

An D+ gehört ein 1,5 kΩ Pull-Up Widerstand auf 3,3 V, welcher dem Host Computer signalisiert, dass ein Gerät angeschlossen wurde. Manche Boards schalten den Widerstand mit einen I/O Pin ein. Dadurch kann man den Host Computer dazu bringen, das USB Gerät erneut zu erkennen, ohne das Kabel abstecken zu müssen.

Virtueller COM-Port mit Cube HAL

Auf der Webseite von STM gibt es den "STM32 Virtual COM Port Driver" zum Herunterladen, aber den braucht man gar nicht, da alle aktuellen Betriebssysteme den CDC Standardtreiber bereits enthalten.

Mit dem Programm CubeMX kann man sich ein Projekt mit USB Unterstützung zusammenklicken. Das geht so:

Probiere das Programm zunächst ohne Änderungen aus. Der PC sollte im Gerätemanager einen neuen virtuellen COM Anschluss mit dem Namen "Serielles USB-Gerät (COMx)" oder "STMicroelectronics Virtual COM Port (COMx)" anzeigen.

Um Text vom Mikrocontroller an den PC zu senden, füge im Quelltext von main.c folgendes zwischen die genannten Markierungen ein:

/* USER CODE BEGIN Includes */

#include "usbd_cdc_if.h"

/* USER CODE END Includes */

...

  /* USER CODE BEGIN WHILE */
  while (1)
  {
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */

        // LED On
        HAL_GPIO_WritePin(LED_GPIO_Port,LED_Pin,GPIO_PIN_SET);
        HAL_Delay(500);

        // LED Off
        HAL_GPIO_WritePin(LED_GPIO_Port,LED_Pin,GPIO_PIN_RESET);
        HAL_Delay(500);

        // Send data
        char msg[]="Hallo!";
        CDC_Transmit_FS( (uint8_t*) msg, strlen(msg));

  }
  /* USER CODE END 3 */

Die LED blinkt im Sekundentakt. Starte ein Terminalprogramm (z.B. Hammer Terminal) und öffne den virtuellen COM-Port. Die Baudrate spielt keine Rolle. Du empfängst jetzt jede Sekunde den Text "Hallo!".

Das Programm belegt etwa 13 kB Flash und 4,5 kB RAM.

Damit die Ausgabefunktionen der Standard C Library (z.B. printf, puts, putchar) funktionieren, musst du folgende Funktion implementieren:

// Redirect standard output to the USB port
int _write(int file, char *ptr, int len)
{
    CDC_Transmit_FS( (uint8_t*) ptr, len);
    return len;
}

Virtueller COM-Port ohne Cube HAL

Der Benutzer W.S. hat im mikrocontroller.net Forum eine USB CDC Implementierung zur freien Verwendung veröffentlicht, die ebenfalls ohne Treiberinstallation funktioniert. Der Quelltext ist sehr kompakt - nur zwei Dateien ohne weitere Anhängigkeiten. Ich habe diesen Code in ein Projekt für die System Workbench eingebaut und für das Nucleo-F303RE Board angepasst.

Das Programm lässt die LED an Anschluss PA5 jede Sekunde aufblitzen und sendet dabei "Hello World!" über USB an den angeschlossenen Computer. Es belegt nur 8 kB Flash und 1,7 kB RAM. Davon dienen jeweils 256 Byte als Sendepuffer und als Empfangspuffer (kann man ändern).

#include <stdio.h>
#include "stm32f3xx.h"
#include "usb.h"

// The current clock frequency
uint32_t SystemCoreClock=8000000;

// Counts milliseconds
volatile uint32_t systick_count=0;

// Interrupt handler
void SysTick_Handler(void)
{
    systick_count++;
}

// Delay some milliseconds
// Note that effective time may be up to 1ms shorter than requested.
void delay_ms(int ms)
{
    uint32_t start=systick_count;
    while (systick_count-start<ms);
}

// Change system clock to 48 MHz using 8 MHz crystal
// Called by Assembler startup code
void SystemInit(void)
{
    // Flash latency 2 wait states
    MODIFY_REG(FLASH->ACR, FLASH_ACR_LATENCY, FLASH_ACR_LATENCY_1);

    // Enable HSE oscillator
    SET_BIT(RCC->CR, RCC_CR_HSEON);

    // Wait until HSE oscillator is ready
    while(!READ_BIT(RCC->CR, RCC_CR_HSERDY)) {}

    // 48 MHz using the 8 MHz HSE oscillator with 6x PLL, lowspeed I/O runs at 24 MHz
    WRITE_REG(RCC->CFGR, RCC_CFGR_PLLSRC + RCC_CFGR_PLLMUL6 + RCC_CFGR_PPRE1_DIV2);

    // Enable PLL
    SET_BIT(RCC->CR, RCC_CR_PLLON);

    // Wait until PLL is ready
    while(!READ_BIT(RCC->CR, RCC_CR_PLLRDY)) {}

    // Select PLL as clock source
    MODIFY_REG(RCC->CFGR, RCC_CFGR_SW, RCC_CFGR_SW_PLL);

    // Set USB prescaler to 1  (skip in case of 72 MHz)
    MODIFY_REG(RCC->CFGR, RCC_CFGR_USBPRE, RCC_CFGR_USBPRE);

    // Update variable
    SystemCoreClock=48000000;
}

void init_io()
{
    // Enable USB
    SET_BIT(RCC->APB1ENR, RCC_APB1ENR_USBEN);

    // Enable Port A
    SET_BIT(RCC->AHBENR, RCC_AHBENR_GPIOAEN);

    // PA5 = Output
    MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER5, GPIO_MODER_MODER5_0);
}

// Redirect standard output to the USB port
int _write(int file, char *ptr, int len)
{
    for (int i=0; i<len; i++)
    {
        UsbCharOut(*ptr++);
    }
    return len;
}

int main(void)
{
    // Initialize system timer
    SysTick_Config(SystemCoreClock/1000);
    
    init_io();
    UsbSetup();

    while (1)
    {
        // LED On (High)
        WRITE_REG(GPIOA->BSRR, GPIO_BSRR_BS_5);
        delay_ms(100);

        puts("Hello World!");

        // LED Off (Low)
        WRITE_REG(GPIOA->BSRR, GPIO_BSRR_BR_5);
        delay_ms(1000);
    }
}

Die Ausgabe kann man mit einem Terminal-Programm anzeigen oder unter Linux mit dem Befehl: cat /dev/ttyACM0.

Nach der Initialisierung mittels UsbSetup() wird die Funktion UsbCharOut() benutzt, um einzelne Zeichen zu senden. UsbSetup setzt voraus, dass der Systemtakt bereits korrekt konfiguriert ist. Die entsprechenden Zeilen habe ich oben fett hervorgehoben.

Bei der Ausgabe von Text mit printf() und putchar() ist zu Beachten, dass die newlib Library die Zeichen in einem Puffer ansammelt, bis dieser entweder voll ist oder ein Zeilenumbruch erfolgt. Mit fflush(stdout) kann man erzwingen, dass die Ausgabe sofort erfolgt.

Für fortgeschrittene Programmierer hat Niklas Gürtler das USB-Tutorial mit STM32 im mikrocontroller.net Forum geschrieben. Er beschreibt dort detailliert, wie die USB Schnittstelle funktioniert.

Echtzeituhr

Die RTC besteht aus einem 32 kHz Quarz-Oszillator und einer Reihe verketteter Zähler, um auf Sekunden, Minuten, Stunden, Tage, Monate und Jahre zu kommen. Der Oszillator von der RTC läuft schon ohne Kalibrierung wesentlich geauer, als der Haupt-Quarz.

Zwei Alarm-Zeiten sind programmierbar und die Uhr kann sich den Zeitstempel von einem Ereignis merken.

Nach einem Stromausfall ohne bzw. mit leerer Batterie werden die Zähler automatisch auf 0 gesetzt und die Uhr angehalten.

Neben der Uhr enthält die batteriegepufferte Einheit je nach Modell 5 oder 16 so genannte Backup Register (mit je 32bit Breite) wo man an Daten ablegen kann.

Der LSE Oszillator nutzt nur sehr wenig Energie und ist daher bei falscher Beschaltung störanfällig. Die Leitungen zum sorgfältig ausgewählten Quarz sollen so kurz wie möglich sein und die Kapazitäten müssen genau berechnet werden.

Der Anschluss PC13 wird ebenfalls durch den Low-Power Schaltkreis der RTC versorgt, also auch durch die Backup Batterie. Er soll daher nur mit niedriger Frequenz geschaltet werden und bei High Pegel nicht belastet werden.

Die Application Note AN4759 beschreibt, wie man die RTC benutzt. Wenn der Systemtakt geringer ist als 230 kHz, dann muss man die Verwendung der Schatten-Register deaktivieren. Ich den folgenden Beispielen gehe ich davon aus, dass der Systemtak hoch genug ist.

RTC starten

Nach einem Stromausfall ist die Uhr zunächst gestoppt. Man kann sie per Software so starten:
void initRtc()
{
    // Enable the power interface
    SET_BIT(RCC->APB1ENR, RCC_APB1ENR_PWREN);

    // Enable access to the backup domain
    SET_BIT(PWR->CR, PWR_CR_DBP);

    // Enable LSE oscillator with medium driver power
    MODIFY_REG(RCC->BDCR, RCC_BDCR_LSEDRV, RCC_BDCR_LSEDRV_1);
    SET_BIT(RCC->BDCR, RCC_BDCR_LSEON);

    // Wait until LSE oscillator is ready
    while(!READ_BIT(RCC->BDCR, RCC_BDCR_LSERDY)) {}

    // Select LSE as clock source for the RTC
    MODIFY_REG(RCC->BDCR, RCC_BDCR_RTCSEL, RCC_BDCR_RTCSEL_LSE);

    // Enable the RTC
    SET_BIT(RCC->BDCR, RCC_BDCR_RTCEN);
}

Aufwachen

Nachdem die RTC gestartet ist, kann man sie benutzen, um regelmäßige Unterbrechungen zu erzeugen. Diese wiederum können verwendet werden, um den Mikrocontroller aus Sleep, Stop und Standby Zuständen auf zu wecken.

Das folgende ausführbare Beispiel zeigt, wie man damit die LED auf dem Nucleo-F303RE Board im Sekundentakt blinken lässt:

#include "stm32f3xx.h"
#include <stdio.h>

uint32_t SystemCoreClock=8000000;

// Redirect standard output to the serial port
int _write(int file, char *ptr, int len)
{
    for (int i=0; i<len; i++)
    {
        while(!(USART2->ISR & USART_ISR_TXE));
        USART2->TDR = *ptr++;
    }
    return len;
}

void init_io()
{
    // Enable Port A
    SET_BIT(RCC->AHBENR, RCC_AHBENR_GPIOAEN);

    // PA5 = Output for the LED
    MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER5, GPIO_MODER_MODER5_0);

    // PA2 (TxD) shall use the alternate function 7 (ssee data sheet)
    MODIFY_REG(GPIOA->AFR[0], GPIO_AFRL_AFRL2, 7U<<GPIO_AFRL_AFRL2_Pos);
    MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER2, GPIO_MODER_MODER2_1);
}

void initSerial()
{
    // Use system clock for USART2
    SET_BIT(RCC->APB1ENR, RCC_APB1ENR_USART2EN);
    MODIFY_REG(RCC->CFGR3, RCC_CFGR3_USART2SW, RCC_CFGR3_USART2SW_0);

    // Set baudrate
    USART2->BRR = (SystemCoreClock / 2400);

    // Enable transmitter of USART2
    USART2->CR1 = USART_CR1_UE + USART_CR1_TE;
}

void initRtc()
{
    // Enable the power interface
    SET_BIT(RCC->APB1ENR, RCC_APB1ENR_PWREN);

    // Enable access to the backup domain
    SET_BIT(PWR->CR, PWR_CR_DBP);

    // Enable LSE oscillator with medium driver power
    MODIFY_REG(RCC->BDCR, RCC_BDCR_LSEDRV, RCC_BDCR_LSEDRV_1);
    SET_BIT(RCC->BDCR, RCC_BDCR_LSEON);

    // Wait until LSE oscillator is ready
    while(!READ_BIT(RCC->BDCR, RCC_BDCR_LSERDY)) {}

    // Select LSE as clock source for the RTC
    MODIFY_REG(RCC->BDCR, RCC_BDCR_RTCSEL, RCC_BDCR_RTCSEL_LSE);

    // Enable the RTC
    SET_BIT(RCC->BDCR, RCC_BDCR_RTCEN);
}

void initWakeup()
{
    // Unlock the write protection
    WRITE_REG(RTC->WPR, 0xCA);
    WRITE_REG(RTC->WPR, 0x53);

    // Stop the wakeup timer to allow configuration update
    CLEAR_BIT(RTC->CR, RTC_CR_WUTE);

    // Wait until the wakeup timer is ready for configuration update
    while (!READ_BIT(RTC->ISR, RTC_ISR_WUTWF)) {};

    // Clock source of the wakeup timer is 1 Hz
    MODIFY_REG(RTC->CR, RTC_CR_WUCKSEL, RTC_CR_WUCKSEL_2);

    // The wakeup period is 0+1 clock pulses
    WRITE_REG(RTC->WUTR,0);

    // Enable the wakeup timer with interrupts
    SET_BIT(RTC->CR, RTC_CR_WUTE + RTC_CR_WUTIE);

    // Switch the write protection back on
    WRITE_REG(RTC->WPR, 0xFF);

    // Enable EXTI20 interrupt on rising edge
    SET_BIT(EXTI->IMR, EXTI_IMR_MR20);
    SET_BIT(EXTI->RTSR, EXTI_RTSR_TR20);
    NVIC_EnableIRQ(RTC_WKUP_IRQn);

    // Clear (old) pending interrupt flag
    CLEAR_BIT(RTC->ISR, RTC_ISR_WUTF);  // Clear in RTC
    SET_BIT(EXTI->PR, EXTI_PR_PR20);    // Clear in NVIC
}

void RTC_WKUP_IRQHandler(void)
{
    // Clear interrupt flag
    CLEAR_BIT(RTC->ISR, RTC_ISR_WUTF);  // Clear in RTC
    SET_BIT(EXTI->PR, EXTI_PR_PR20);    // Clear in NVIC

    // Toggle LED
    GPIOA->ODR ^= GPIO_ODR_5;
}


int main(void)
{
    init_io();
    initRtc();
    initWakeup();
    initSerial();
    while(1)
    {
        puts("Hello");

        // Enter sleep mode
        __WFI(); 
    }
}  

Achtung: Der Name des Interrupt-Handlers und die Kanal Nummer (hier 20) variieren je nach STM32 Modell. Schaue dazu in die Datei startup_stm32.s und in das Referenzhandbuch Kapitel "External and internal interrupt/event line mapping".

Während der Interrupt-Handler die LED blinken lässt, gibt das Hauptprogramm den Text "Hallo" auf dem Seriellen Port aus und legt sich dann schlafen. Das Interrupt-Signal der RTC weckt die CPU im Sekundentakt wieder auf.

RTC Lesen

Man kann die Uhrzeit und das Datum direkt aus den entsprechenden Registern auslesen. Die Hardware verwendet dabei Schatten-Register, die automatisch mit der langsamen RTC synchronisiert werden.

Das folgende Beispiel baut auf dem vorherigen Beispiel auf:

int main(void)
{
    init_io();
    initRtc();
    initWakeup();
    initSerial();
    while(1)
    {
        // Extract digits from the RTC time register
        uint8_t ht=  (RTC->TR & RTC_TR_HT)  >> RTC_TR_HT_Pos;
        uint8_t hu=  (RTC->TR & RTC_TR_HU)  >> RTC_TR_HU_Pos;
        uint8_t mnt= (RTC->TR & RTC_TR_MNT) >> RTC_TR_MNT_Pos;
        uint8_t mnu= (RTC->TR & RTC_TR_MNU) >> RTC_TR_MNU_Pos;
        uint8_t st=  (RTC->TR & RTC_TR_ST)  >> RTC_TR_ST_Pos;
        uint8_t su=  (RTC->TR & RTC_TR_SU)  >> RTC_TR_SU_Pos;

        // Print the time
        printf("Time: %d%d:%d%d:%d%d\n", ht,hu, mnt,mnu, st,su);

        // Extract digits from the RTC date register
        uint8_t yt= (RTC->DR & RTC_DR_YT) >> RTC_DR_YT_Pos;
        uint8_t yu= (RTC->DR & RTC_DR_YU) >> RTC_DR_YU_Pos;
        uint8_t mt= (RTC->DR & RTC_DR_MT) >> RTC_DR_MT_Pos;
        uint8_t mu= (RTC->DR & RTC_DR_MU) >> RTC_DR_MU_Pos;
        uint8_t dt= (RTC->DR & RTC_DR_DT) >> RTC_DR_DT_Pos;
        uint8_t du= (RTC->DR & RTC_DR_DU) >> RTC_DR_DU_Pos;

        // Print the date
        printf("Date: %d%d-%d%d-%d%d\n", yt,yu, mt,mu, dt,du);

        // Enter sleep mode
        __WFI();
    }
}

Im Control Register RTC->CR kann man die Anzeige der Zeit beeinflussen:

RTC Beschreiben

Datum, Uhrzeit und einige Bits im Control Register sind ziemlich gut gegen versehentliche Änderungen geschützt. Sie lassen sich nur im sogenannten Initialisierungs-Modus beschreiben, wenn der Schreibgeschutz aufgehoben wurde.

Man darf die reservierten Bits nicht verändern. Außerdem muss man nach jedem Schreibzugriff eine Synchronisation der Schatten-Register auslösen und abwarten. Deswegen ist es gut alle Bits im RTC->TR bzw. RTC->DR Register gleichzeitig zu setzen.

Die folgende Prozedur ändert Datum und Uhrzeit unter Berücksichtigung der obigen Aspekte:

/**
 * Write digits to the RTC time register in 24h format.
 * @param ht tens of hour
 * @param hu ones of hours
 * @param mnt tens of minutes
 * @param mnu ones of minutes
 * @param st tens of seconds
 * @param su ones of seconds
 */
void RTC_write_time(uint8_t ht, uint8_t hu, uint8_t mnt, uint8_t mnu, uint8_t st, uint8_t su)
{
    // Calculate the new value for the time register
    uint32_t tmp=READ_REG(RTC->TR);
    tmp &= ~(RTC_TR_HT+RTC_TR_HU+RTC_TR_MNT+RTC_TR_MNU+RTC_TR_ST+RTC_TR_SU+RTC_TR_PM); // Keep only the reserved bits
    tmp += (uint32_t) ht << RTC_TR_HT_Pos;
    tmp += (uint32_t) hu << RTC_TR_HU_Pos;
    tmp += (uint32_t) mnt << RTC_TR_MNT_Pos;
    tmp += (uint32_t) mnu << RTC_TR_MNU_Pos;
    tmp += (uint32_t) st << RTC_TR_ST_Pos;
    tmp += (uint32_t) su << RTC_TR_SU_Pos;

    // Unlock the write protection
    WRITE_REG(RTC->WPR, 0xCA);
    WRITE_REG(RTC->WPR, 0x53);

    // Enter initialization mode
    SET_BIT(RTC->ISR, RTC_ISR_INIT);

    // Wait until the initialization mode is active
    while (!READ_BIT(RTC->ISR, RTC_ISR_INITF)) {};

    // The 24h format is already the default
    // CLEAR_BIT(RTC->CR, RTC_CR_FMT);

    // Update the time register
    WRITE_REG(RTC->TR,tmp);

    // Leave the initialization mode
    CLEAR_BIT(RTC->ISR, RTC_ISR_INIT);

    // Trigger a synchronization of the shadow registers
    CLEAR_BIT(RTC->ISR, RTC_ISR_RSF);

    // Wait until the shadow registers are synchronized
    while (!READ_BIT(RTC->ISR, RTC_ISR_RSF)) {};

    // Switch the write protection back on
    WRITE_REG(RTC->WPR, 0xFF);
}

/**
 * Write digits to the RTC date register.
 * @param yt tens of year
 * @param yu ones of year
 * @param mt tens of month
 * @param mu ones of month
 * @param dt tens of day
 * @param du ones of day
 * @param wdu week day (1-7)
 */
void RTC_write_date(uint8_t yt, uint8_t yu, uint8_t mt, uint8_t mu, uint8_t dt, uint8_t du, uint8_t wdu)
{
    // Calculate the new value for the date register
    uint32_t tmp=READ_REG(RTC->DR);
    tmp &= ~(RTC_DR_YT+RTC_DR_YU+RTC_DR_MT+RTC_DR_MU+RTC_DR_DT+RTC_DR_DU+RTC_DR_WDU); // Keep only the reserved bits
    tmp += (uint32_t) yt << RTC_DR_YT_Pos;
    tmp += (uint32_t) yu << RTC_DR_YU_Pos;
    tmp += (uint32_t) mt << RTC_DR_MT_Pos;
    tmp += (uint32_t) mu << RTC_DR_MU_Pos;
    tmp += (uint32_t) dt << RTC_DR_DT_Pos;
    tmp += (uint32_t) du << RTC_DR_DU_Pos;
    tmp += (uint32_t) wdu << RTC_DR_WDU_Pos;

    // Unlock the write protection
    WRITE_REG(RTC->WPR, 0xCA);
    WRITE_REG(RTC->WPR, 0x53);

    // Enter initialization mode
    SET_BIT(RTC->ISR, RTC_ISR_INIT);

    // Wait until the initialization mode is active
    while (!READ_BIT(RTC->ISR, RTC_ISR_INITF)) {};

    // Update the time register
    WRITE_REG(RTC->DR,tmp);

    // Leave the initialization mode
    CLEAR_BIT(RTC->ISR, RTC_ISR_INIT);

    // Trigger a synchronization of the shadow registers
    CLEAR_BIT(RTC->ISR, RTC_ISR_RSF);

    // Wait until the shadow registers are synchronized
    while (!READ_BIT(RTC->ISR, RTC_ISR_RSF)) {};

    // Switch the write protection back on
    WRITE_REG(RTC->WPR, 0xFF);
}


int main(void)
{
    initRtc();
    ...

    // Change the time to 18:33:45
    RTC_write_time(1,8, 3,3, 4,5);

    // Change the date to 19-03-25 (25th March 2019), 1=monday
    RTC_write_date(1,9, 0,3, 2,5, 1);

    ...
}

RTC kalibrieren

Die RTC erreicht normalerweise ohne Kalibrierung eine Abweichung von maximal zwei Sekunden pro Tag. Durch Kalibrierung kann man die Genauigkeit weiter verbessern. Sollte deine Uhr ohne Kalibrierung um mehr als 5 Sekunden pro Tag abweichen, liegt mit Sicherheit ein Hardwarefehler vor.

Um die Uhr ohne teure Messinstrumente grob zu kalibrieren lässt man sie einige Tage lang laufen und ermittelt dabei ihre Abweichung von der soll-Geschwindigkeit durch Vergleich mit einem präzisen Zeitserver.

Das RTC->CALR Register kann erst nach Deaktivierung des Schreibschutzes verändert werden. Wenn die Uhr zu langsam läuft, setzt man das Bit CALP, um die Uhr genau 42,206 Sekunden pro Tag zu beschleunigen. Dann reduziert man ihre Geschwindigkeit durch den Wert in den CALM Bits. Jede Stufe dort entspricht 0,0824 Sekunden pro Tag.

Wenn die Uhr zum Beispiel 4 Sekunden pro Tag zu langsam wäre, ergäbe sich folgende Rechnung:

Abweichung:                  4,000 Sekunden
CALP:                      -42,206 Sekunden    (setze RTC->CALR.CALP auf 1)
CALM:     464 * 0,0824 =   +38,234 Sekunden    (setze RTC->CALR.CALM auf 464)
===========================================
Summe:                      +0,028 Sekunden
Es bleibt eine Ungenauigkeit von +0,028 Sekunden pro Tag übrig.

Power Management

Indem man Taktsignale für I/O Funktionen deaktiviert oder verlangsamt, spart man bereits eine Menge Strom. Darüber hinaus gibt es die folgenden besondere Zustände für den ARM Kern:

ModusEintrittAufwachenBeschreibung
WFI Sleep__WFI()Interrupt*Warte auf Interrupt. Nur die CPU wird angehalten.
WFE Sleep__WFE()Interrupt* oder EreignisWarte auf Ereignis. Nur die CPU wird angehalten.
StopPDDS=1, LPDS=1, SLEEPDEEP=1 + __WFI() oder __WFE()EXTI Leitung, configuriert im EXTI Register. Taktsignale von HSI/HSE sind deaktiviert. Die I/O Pins, Register und RAM bleiben unverändert. Debugging ist nicht möglich.
StandbyPDDS=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() augerufen wird, wird nur 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.

Atollic TrueSTUDIO

Die ehemals kostenpflichtige Entwicklungsumgebung Atollic TrueSTUDIO ist seit Januar 2018 kostenlos verfügbar. Nach der Übernahme durch die Firma ST wurde das Lizenzmodell geändert und die Unterstützung für andere ARM Controller entfernt. TrueSTUDIO basiert ebenfalls auf Eclipse, der GNU ARM Embedded Toolchain (gcc) und OpenOCD für's Debugging.

Im Vergleich zur System Workbench sind mir nur wenige Unterschiede aufgefallen:

TrueSTUDIO kann Projekte importieren, die zuvor mit der System Workbench erstellt wurden. Sie werden dabei automatisch konvertiert.

Keil MDK-Arm

Diese Entwicklungsumgebung scheint unter Profis beliebt zu sein. Die kostenlose Lite Version mit Einschränkung auf 32 kB Flash kann man hier herunterladen. Keil enthält einen eigenen Compiler und eine andere C Library, so dass meine Informationen bezüglich gcc und der Newlib nicht passen.