Startseite

Notizen zur STM32F1 Serie

Auf dieser Seite sammle ich Informationen zur Anwendung von STM32 F1 Mikrocontrollern. Also technische Daten, Hinweise und Programmier-Beispiele, die beim Einstieg helfen könnten. Für mich ist diese Seite eine Erinnerungshilfe, aber ich denke, dass auch andere Anfänger hier nützliche Anleitungen finden werden.

Wer noch keine ARM Mikrocontroller programmiert hat, dem empfehle ich das Buch Einblick in die moderne Elektronik ohne viel Theorie.

Modell Bezeichnungen

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

Mainstream Familie
  • STM32F0 Serie: ARM Cortex M0 bis 48MHz ohne FPU
  • STM32F1 Serie: ARM Cortex M3 bis 72MHz ohne FPU

    STM32F100Value line
    24MHz mit Motorsteuerung und CEC Funktionen
    STM32F101Access line
    36MHz
    STM32F102USB Access line
    48MHz mit USB
    STM32F103Performance line
    72MHz mit Motorsteuerung, USB und CAN
    STM32F105Connectivity line
    72MHz mit Ethernet, CAN und USB 2.0 OTG
    STM32F107

  • STM32F3 Serie: ARM Cortex M4 bis 72MHz mit FPU

High Performance Familie

  • STM32F2 Serie: ARM Cortex M3 bis 120MHz ohne FPU
  • STM32F4 Serie: ARM Cortex M4 bis 180MHz mit FPU
  • STM32F7 Serie: ARM Cortex M7 bis 216MHz mit FPU
  • STM32H7 Serie: ARM Cortex M7 bis 400MHz mit FPU

Low Power Familie

  • STM32L0 Serie: ARM Cortex M0 bis 48MHz ohne FPU
  • STM32L1 Serie: ARM Cortex M3 bis 72MHz ohne FPU
  • STM32L4 Serie: ARM Cortex M4 bis 180MHz mit FPU
Anzahl der Pins
  • T = 36 Pins
  • C = 48 Pins
  • R = 64 Pins
  • V = 100 Pins
  • Z = 144 Pins
Flash Größe
4 = 16kBLow Density
6 = 32kB
8 = 64kBMedium Density
B = 128kB
C = 256kBHigh Density
D = 384kB
E = 512kB
F = 768kBXL Density
G = 1MB
Gehäuse
Temperaturbereich
  • 6 = -40 bis +85°C
  • 7 = -40 bis +105°C

Der STM32F103C8T6 ist also aus der Mainstream Familie, Performance line, Medium Density. Er läuft mit bis zu 72Mhz, hat 48 Pins, 64kB Flash Speicher, ein LQFP Gehäuse und eignet sich für -40 bis +85°C.

Hardware

Nucleo-F103RB Board

Das Nucleo-F103RB Board (alias Nucleo-64 mit STM32F103RBT6) ist ein hochwertiges Starter-Set zum günstigem Preis um 15€. Die Besonderheit bei diesem Board ist, dass man einen ST-Link Adapter und einen USB-UART quasi dazu geschenkt bekommt.

Der 8MHz 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.

STM32 Nucleo-64 Manual Technische Beschreibung des "Nucleo" Boardes

Blue Pill Board

Das sogenannte "Blue Pill" Board aus China findet mant bei AliExpress besser als "STM32F103C8T6 Board". Es kostet nur etwa 2€, ein absoluter Knallerpreis. Wenn man besonders dünne Stiftleisten verwendet, passt das Board in einen 40-poligen DIP Sockel.

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

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

Es wird geraten, den Rahmen der empfindlichen USB Buchse nachzulöten. Man darf das Board nicht gleichzeitig über USB und ein 5V Netzteil versorgen (Kurzschluss). Eine zusätzliche 3,3V Versorgung ist hingegen in Ordnung.

Der Widerstand R10 ist oft falsch mit 10kΩ statt dem richtigen 1,5kΩ bestückt, so dass der USB Port unzuverlässig funktioniert. Zur Korrektur kann man zusätzlich einen 1,8kΩ Widerstand parallel schalten (von PA12 nach 3,3V).

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

Es wurde mehrfach berichtet, daß diese Boards mit aktiviertem Schreibschutz verkauft wurden. Das kann man über den SWD Anschluss oder über den seriellen Bootloader in den Option Bytes wieder entsperren.

Schaltpan des "Blue Pill" Boardes

Maple Mini Board

Das LeafLabs Maple Mini Board aus China bekommt man ab 3€. Es ist mit dem selben Mikrocontroller bestückt, unterscheidet sich jedoch bei der Pinbelegung, und Stromversorgung. Dessen RTC ist mangels Quarz nicht benutzbar. Es hat keine SWD Stiftleiste, die entsprechenden Signale (SWD und SWCLK) sind jedoch auf einer der beiden Stiftleisten herausgeführt. Dieses Board passt nicht in 40 polige DIP Sockel.

Die aufgedruckte Beschriftung der Pins entspricht nicht dem Datenblatt, sondern bezieht sich auf das Arduino Framework.

Die Firmware installiert man wahlweise über USART1 mit dem fest installierten seriellen Bootloader oder über SWD mit einem ST-Link Adapter. Beim originalen Maple Mini Board (das man nicht mehr kaufen kann) war ein USB Bootloader für Arduino vorinstalliert, das ist bei den Nachbauten leider nicht immer der Fall.

Der Spannungsregler kann leicht überhitzen wenn man ihn bei mehr als 7V Versorgungsspannung mit zusätzlichen Verbrauchern belastet. Im Gegensatz zum "Blue Pill" Board darf man dieses Board gleichzeitig über USB und ein 5V Netzteil versorgen.

Ein High Pegel an PB9 schaltet den USB Anschluss (Pull-Up Widerstand) ein.

STM32duino Wiki: Maple Mini Technische Beschreibung des "Maple Mini" Boardes

STM32F103VET6 Minimum System Board

Dieses Board aus China bekommt man ab 9€. Es ist offensichtlich eine größere Variante vom "Blue Pill" Board. Ich habe es noch nicht ausprobiert.

Die Firmware installiert man wahlweise über USART1 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.

Man darf das Board nicht gleichzeitig über USB und ein 5V Netzteil versorgen (Kurzschluss). Eine zusätzliche 3,3V Versorgung ist hingegen in Ordnung.

Wenn USB verwendet wird, empfehle ich, den Pull-Up Widerstand an PA12 (D+) zu überprüfen. Er ist im Schaltplan mit 4,7kΩ eingezeichnet, es sollten aber 1,5kΩ sein.

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

Schaltpan des STM32F103VET6 Minimum System Board

Elektrische Daten

Alle STM32F1 Chips kann man mit 2,0 bis 3,6 Volt betreiben. Der ADC benötigt aber mindestens 2,4 Volt und die USB Schnittstelle läuft nur mit 3,3V.

Die Stromaufnahme ist mit 8bit Mikrocontrollern vergleichbar. Im Stop Modus ist die Stromaufnahme jedoch mit ~20µA sehr viel höher. Für langfristigen Batteriebetrieb wird daher auf die sparsamere STM32L1 oder L4 Serie verwiesen.

Viele I/O Pins sind 5V tolerant. Bei den STM32F103 Modellen sind das: PA8-15, PB2-4, PB6-15, PC6-12, PD0-15, PE0-15, PF0-5, PF11-15, PG0-15.

Die 5V toleranten I/O Pins sind durch ESD Schutzdioden gegen negative Eingangsspannung geschützt. Diese vertragen einzeln 5mA, alle zusammen jedoch nur maximal 25mA. Stromfluss durch Überspannung über 5,5V ist nicht zulässig.

Die nicht 5V toleranten I/O Pins sind durch ESD Schutzdioden sowohl gegen negative Eingangsspannung als auch gegen Überspannung (höher als die Versorgungsspannung) geschützt. Auch hier sind einzeln 5mA, für alle zusammen jedoch nur maximal 25mA zulässig.

Die Ausgänge sind einzeln mit 20mA belastbar, gültige Logikpegel sind bis 8mA garantiert. Jedoch dürfen alle Ausgänge zusammen nur mit maximal 150mA belastet werden. Im open-drain Modus dürfen die 5V toleranten Ausgänge durch externe Widerstände auf 5V gezogen werden. Die Ausgänge sind nicht Kurzschlussfest.

Die internen Pull-Up und Pull-Down Widerstände haben typischerweise 40kΩ. Alle I/O Pins haben typischerweise 5pF Kapazität.

Der NRST 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 mindestens 20µs lang.

Besondere Ausnahmen

Die Pins PC13, PC14 und PC15 dürfen bei High keinen Gleichstrom liefern und bei Low nur 3mA aufnehmen. Außerdem dürfen sie nur mit maximal 2MHz angesteuert werden und mit maximal 30pF belastet werden.

Die ESD Schutzdioden von PA4, PA5, PC13, PC14 und PC15 darf man nicht belasten.

Dokumentationen

Das STM32F1 Reference Manual ist das wichtigste Handbuch für den Programmierer. Die Pinbelegung und elektrischen Daten findet man im jeweiligen Datenblatt:

Density Low Medium High XL
Flash Size 16kB 32kB 64kB 128kB 256kB 384kB 512kB 768kB 1MB
⬐Model→ x4 x6 x8 xB xC xD xE xF xG
Value line STM32F100 Datenblatt Datenblatt
Access line STM32F101 Datenblatt Datenblatt Datenblatt Datenblatt
USB Access line STM32F102 Datenblatt Datenblatt
Performance line STM32F103 Datenblatt Datenblatt Datenblatt Datenblatt
Connectivity line STM32F105 Datenblatt
STM32F107

Werte für x: T = 36 Pins, C = 48 Pins, R = 64 Pins, V = 100 Pins, Z = 144 Pins

Optional:

Software

Nach einigen Versuchen mit anderen ebenfalls kostenlosen Alternativen bin ich bei der System Workbench for STM32 angelangt. Dieses Programmpaket enthält die Entwicklungsumgebung 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.

Ganz unten findest du Hinweise zu anderen Entwicklungsumgebungen: Arduino IDE und Atollic TrueSTUDIO.

Optionale Software für Windows:

Optionale Software für Linux:

Bei Linux wird multiarch-support benötigt, damit es sowohl 64bit als auch 32bit Programme ausführen kann.

Achtung: Direkt nach der Installation der System Workbench gehe ins Menü "Help/Check for Updates...", um Fehlerkorrekturen zu erhalten!

Die Basis-Library für alle ARM Controller 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 eine kleine Menge Hilfsfunktionen, um den ARM Kern zu konfigurieren. Die CMSIS ist von ARM spezifiziert und wird von allen Chip Herstellern in spezifischen Varianten bereit gestellt.

Bei der Cube HAL handelt es sich um ein Framework, das den Zugriff auf die Hardware-Funktionen der STM32 Mikrocontroller durch Abstraktion erleichtern soll. Dazu bietet der Hersteller Erweiterungen für RTOS (Real Time Operating System), USB, TCP/IP, Filesysteme usw. an, allerdings nicht alle kostenlos. Dazu gehört das Programm CubeMX, mit dem man sich HAL-basierte Projekte einschließlich Initialisierungs-Routine für I/O Pins und Takterzeugung zusammen klickt. Die Cube HAL basiert auf und enthält die CMSIS Library.

Man sollte bedenken, dass Cube HAL ausschließlich auf STM32 Controllern läuft. Jeder Chip Hersteller kocht hier sein eigenes Süppchen und Frameworks dieser Art sind erfahrungsgemäß recht kurzlebig. Unabhängig davon halte ich auch sonst nicht viel von der Cube HAL. Denn mann muss das Referenzhandbuch ohnehin lesen, um den Chip richtig zu benutzen. Wenn man das getan hat, kennt man alle Register und Bits mit Namen. Warum sollte man einen Abstraktions-Layer darüber legen, der den selben Einstellungen neue Namen gibt und die Zusammenhänge zwischen den Registern verschleiert? Die HAL macht das Programm weder übersichtlicher noch vom konkreten Mikrocontroller-Modell unabhängig. Das wurde bei Arduino deutlich besser gemacht.

Die Standard Peripheral Library (SPL, StdPeriphLib) ist eine veraltete Hardware Abstraktion, die immer noch in vielen Beispielen im Internet verwendet wird. Sie basiert ebenfalls auf CMSIS und war im Zeitraum von 2009 bis 2015 aktuell. Laut Hersteller soll man die SPL nicht mehr verwenden.

Projekt auf Basis von CMSIS erzeugen

Der Assistent in der IDE kann nur neue Projekte anlegen die entweder Cube HAL oder die alte StdPeriph Library benutzen. Programme auf Basis der CMSIS Library sind deutlich schlanker und auf ARM Controller anderer Hersteller portierbar. Aber der Assistent der IDE kann solche Projekte nicht direkt anlegen. Man muss dazu folgendermaßen vorgehen:
  1. Lege mit dem Assistenten der IDE das eigentliche Arbeitsprojekt mit der Option "No Firmware" an.
  2. Kopiere das ganze CMSIS Verzeichnis aus diesem Beispielprojekt in dein Arbeitsprojekt.
  3. Füge die beiden Verzeichnisse (CMSIS/core und CMSIS/device) zur Projektkonfiguration hinzu. Das geht so: Rechte Maustaste auf den Projektnamen, dann Properties/C/C++ Build/Settings/Tool Settings/MCU GCC Compiler/Includes/Include paths.
  4. In der Datei stm32f1xx.h habe ich unter dem Kommentar "Uncomment the line below according to the target STM32 device" (ungefähr Zeile 90) die zu meinem Mikrocontroller passende Zeile #define STM32F103xB aktiviert.
Beispiel für einen einfachen LED-Blinker an PA5 und PC13 (src/main.c):
#include <stdint.h>
#include "stm32f1xx.h"

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

int main(void)
{
    // Enable Port A and C
    SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN);
    SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPCEN);

    // PA5 and PC13 = Output
    MODIFY_REG(GPIOA->CRL, GPIO_CRL_CNF5  + GPIO_CRL_MODE5,  GPIO_CRL_MODE5_0);
    MODIFY_REG(GPIOC->CRH, GPIO_CRH_CNF13 + GPIO_CRH_MODE13, GPIO_CRH_MODE13_0);

    while(1)
    {
        // Set PA5 and PC13 to HIGH
        WRITE_REG(GPIOA->BSRR, GPIO_BSRR_BS5);
        WRITE_REG(GPIOC->BSRR, GPIO_BSRR_BS13);
        delay(500);

        // Reset PA5 and PC13 to LOW
        WRITE_REG(GPIOA->BSRR, GPIO_BSRR_BR5);
        WRITE_REG(GPIOC->BSRR, GPIO_BSRR_BR13);
        delay(500);
    }
}
Um das Programm in den Mikrocontroller zu übertragen klickt man in der IDE mit der rechten Maustaste auf den Projektnamen, dann Target/Program Chip... wählen. Siehe dazu auch Programmier- und Debug-Schnittstellen.

Die Delays realisiert man besser mit einem Timer. Ich wollte hier jedoch ein möglichst simples Programmbeispiel zeigen.

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 9kB 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() ohne Fließkomma-Unterstützung benötigen:

FunktionCode (Flash)Statische DatenHeap (malloc)Stack
puts(char*)218401468 oder 436112
printf(char*)220401468 oder 436112
printf(char*,args)357201468 oder 436304

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 1:1 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.
-O2GeschwindigkeitBessere Performance, von ARM als Standardeinstellung empfohlen.
-O3GeschwindigkeitBeste Performance, unter Umständen wird der Code aber viel größer. Diese Stufe ist weniger erprobt.
-OsCode-GrößeGeringe Code-Größe auf Kosten der Performance.
-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 Performance besser wird.

Die Vollständige Liste der Optimierungen befindet sich hier.

Programmier- und Debug-Schnittstellen

Boot Modi

Über zwei Eingänge legt man fest, von welcher Quelle der Mikrocontroller booten soll. Die Pins werden beim Reset und beim Aufwachen aus dem Standby Modus gelesen.

Boot0Boot1 (=PB2)Boot von
LowegalNormaler Flash Speicher, ab Adresse 0x0000 0000
HighLowBootloader
HighHighInternes RAM, ab Adresse 0x2000 0000

Der fest eingebaute Bootloader ermöglicht Zugriff auf den Flash Speicher, das RAM und die Option Bytes über den seriellen Anschluss USART1 (bei den großen Modellen auch USB und CAN). Der Bootloader kann weder gelöscht noch verändert werden.

Um den Bootloader auf dem µC zu aktivieren, setzt man Boot0=High und Boot1=Low. Dann drückt man den Reset Knopf.

Serieller Bootloader

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

Folgende Verbindungen sind nötig:

PC USB-UARTSTM32F1 USART1Beschreibung
TxDRxD (=PA10)Daten
RxDTxD (=PA9)Daten
GND GNDGemeinsame Masse

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

Bei niedriger Übertragungsrate (bis 115200 Baud) verwende ich gerne 1kΩ Schutzwiderstände in den beiden Signal-Leitungen.

USB Bootloader

Beim STM32F105 und STM32F107 unterstützt der fest eingebaute Bootloader auch USB. Dazu ist ein Quarz mit 8MHz, 14.7456MHz oder 25MHz notwendig, und die Spannungsversorgung muss 3,3V sein.

Chips mit Date-Code älter als 937 haben laut AN2606 gewisse Einschränkungen. Ich habe den fest eingebauten USB Bootloader noch nicht ausprobiert.

Im Arduino Umfeld gibt es für einige STM32F103 Boards den STM32duino Bootloader, der das Programmieren über USB ermöglicht. Dieser kann aber nur mit der Arduino Software verwendet werden.

SWJ Schnittstelle

Über die SWJ Schnittstelle kann man den Mikrocontroller programmieren und debuggen. Sie funktioniert unabhängig von Boot Modus, Taktquelle, Spannung und Temperatur und ist damit die zuverlässigste Programmier-Schnittstelle.

Die SWJ Schnittstelle ist während und nach einem Reset standardmäßig aktiviert, kann jedoch per Software deaktiviert werden. Sie unterstützt zwei Übertragunsprotokolle, nämlich JTAG und SWD. Das neuere SWD Protokoll wird bevorzugt, da es weniger Leitungen benötigt.

ST-Link Adapter

Um die SWJ Schnittstelle mit einem PC zu verbinden, benötigt man einen "ST-Link" Adapter. Zum Beispiel so ein billig Teil aus China:

Den ST-Link Adapter vom Nucleo-64 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
Pin 2SWCLKPA14Serial Wire Clock
Pin 3GNDCommon Ground (Masse)
Pin 4SWDIOPA13Serial Wire Data
Pin 5NRSTReset Signal
Pin 6SWOPB3Serial Wire Output

SWCLK, GND und SWDIO müssen auf jeden Fall mit dem Mikrocontroller verbunden werden. Wenn NRST nicht verbunden ist, braucht man einen Reset-Knopf, der beim Start des Programmiervorgangs gedrückt wird. Siehe auch Debuggen ohne NRST.

Die optionale SWO (auch TRACESWO oder SWV genannte) Leitung kann zur Ausgabe von Trace Meldungen verwendet werden.

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

Bei dem chinesischen Produkt soll man die Rückseite der inneren Platine mit Isolierband abdecken, damit kein Kurzschluss zum Aluminium-Gehäuse entsteht. Sein Reset-Ausgang funktioniert nur bei STM8. Außerdem vermisse ich einen SWO Anschluss, aber wer eine ruhige Hand hat, kann die Leitung direkt an den Chip löten:

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

SWJ Deaktivieren

Um die betroffenen Pins für normale Ein-/Ausgabe zu verwenden, kann man die Schnittstelle im AFIO->MAPR Register deaktivieren, nachdem das AFIOEN Bit eingeschaltet wurde.
// Enable clock for alternate functions
SET_BIT(RCC->APB2ENR, RCC_APB2ENR_AFIOEN);
    
// Disable both SWD and JTAG to free PA13, PA14, PA15, PB3 and PB4
MODIFY_REG(AFIO->MAPR, AFIO_MAPR_SWJ_CFG, AFIO_MAPR_SWJ_CFG_DISABLE); 

or:

// Disable JTAG only to free PA15, PB3* and PB4. SWD remains active
MODIFY_REG(AFIO->MAPR, AFIO_MAPR_SWJ_CFG, AFIO_MAPR_SWJ_CFG_JTAGDISABLE); 
*) PB3 kann trotzdem noch mit dem ST-Link Adapter als SWO Ausgang konfiguriert werden.

Debuggen ohne NRST

Der Debugger unterstützt drei Optionen zum Verbindungsaufbau:

Normalerweise benutzt der Debugger die Option "Hardware Reset". Wenn die NRST Leitung nicht verbunden ist, muss man eine der beiden anderen Optionen verwenden. Für einen manuellen Reset per Taster eignet sich die Option "Connect Under Reset" am besten.

Um dort hin zu kommen 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 dann 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:

Dort kannst du die Reset-Methode wie gewünscht einstellen.

Trace Meldungen ausgeben

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

SWO wird mit dem ST-Link Adapter aktiviert und konfiguriert. Dann ist PB3 vorübergehend ein Ausgang mit Ruhe-Pegel High. Während der seriellen Datenübertragung liefert er Low-Impulse. Wenn man später die Verbindung des Debuggers schließt, arbeitet der Anschluss wieder als normaler programmierbarer I/O Pin.

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

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

// delay loop for default 8MHz clock with optimizer enabled
void delay(uint16_t msec)
{
    for (uint32_t j=0; j<2000*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.

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. Die Datei enthält zwischen den Buchstaben nicht darstellbare Steuerzeichen. Zur forlaufenden Anzeige benutze ich den Befehl tail -f debug.txt in einem CygWin Fenster:

Unter Linux benutze ich den Befehl: 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.

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.

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 8MHz R/C Oszillator getaktet. Damit kann man schon eine Menge sinnvolle 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 STM32F100, STM32F101, STM32F102 und STM32F103 nach einem Reset:

 

Bei den Modellen STM32F105 und STM32F107 ist es im unteren Bereich etwas komplexer:

Achtung, kleiner Fallstrick: PLLMUL kann bei den Modellen STM32F100 bis 103 auf 2 bis 16 gestellt werden, aber bei den Modellen STM32F105 und STM32F107 nur auf 4 bis 9.

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

Beispiel für 64 MHz mit dem internen HSI Oszillator (nicht für STM32F105, STM32F107):

// The current clock frequency
uint32_t SystemCoreClock=8000000;

// Change system clock to 64MHz using 8MHz 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);

    // 64MHz using the 8MHz/2 HSI oscillator with 16x PLL, lowspeed I/O runs at 32MHz
    WRITE_REG(RCC->CFGR, RCC_CFGR_PLLMULL16 + 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 (wegen 8 versus 64MHz), 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 aus dem RAM ausführen, da dieser ohne Waitstates arbeitet.

Beispiel für 48MHz mit einem 8MHz Quarz (HSE Oszillator):

// The current clock frequency
uint32_t SystemCoreClock=8000000;

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

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

    // 48MHz using the 8MHz HSE oscillator with 6x PLL, lowspeed I/O runs at 24MHz
    WRITE_REG(RCC->CFGR, RCC_CFGR_PLLSRC + RCC_CFGR_PLLMULL6 + 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=48000000;
}

Beispiel für 72MHz mit einem 8MHz Quarz (HSE Oszillator):

// The current clock frequency
uint32_t SystemCoreClock=8000000;

// Change system clock to 72MHz using 8MHz 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)) {}

    // 72MHz using the 8MHz HSE oscillator with 9x PLL, lowspeed I/O runs at 36MHz
    WRITE_REG(RCC->CFGR, RCC_CFGR_PLLSRC + RCC_CFGR_PLLMULL9 + 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;
}

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 Performance, wenn 16bit Daten an 16bit Adressen und 32bit Daten an 32bit Adressen ausgerichtet sind. Der Compiler kümmert sich automatisch darum.

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 Performance wichtig ist.

Die Register für I/O Funktionen sind fast alle 16 Bit breit.

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 C Funktionen:

Interrupt Vektoren

Der Flash-Speicher beginnt immer mit der Exception- und Interrupt-Vektor Tabelle. Der Quelltext dazu befindet sich in der Datei startup/startup_stm32.s. Dort findest du die vorgegebenen Namen für einige Interrupt-Handler. Eventuell fehlende Interrupt-Handler kann man hier bei Bedarf selbst ergänzen.

Jeder Eintrag in der Tabelle ist eine 32bit Sprungadresse. Bei allen Sprungadressen muss das Bit 0 gesetzt sein, es wird aber immer an die Adresse davor gesprungen. Der Wert 0x00001001 führt zu einem Sprung nach Adresse 0x00001000. Der Compiler kümmert sich automatisch darum.

Die folgende Tabelle gilt für alle STM32F100 bis 103 mit maximal 512kB Flash. Für die größeren Modelle siehe STM32F1 Reference Manual Kapitel: Interrupt and exception vectors.

AdresseARM Exception Nr.CMSIS Interrupt Nr.KürzelC-FunktionBeschreibung
ARM Prozessor Exceptions
0x0000MSPStartwert für den Stapelzeiger MSP nach einem Reset.
0x00041ResetReset_HandlerStartwert für den Programmzähler PC nach einem Reset
0x00082-14NMINMI_Handler()Nicht maskierbarer Interrupt, wird vom RCC Clock Security System verwendet.
0x000c3-13HardFaultHardFault_Handler()Hardware Fehler. Für die folgenden drei Fehler kann man optional spezifische Exceptions aktivieren:
0x00104-12MemManageMemManage_Handler()Memory protection fault
0x00145-11BusFaultBusFault_Handler()Pre-fetch or memory access fault
0x00186-10UsageFaultUsageFault_Handler()Undefined instruction, illegal unaligned access, invalid state, division by zero
0x001creserviert
0x0020
0x0024
0x0028
0x002c11-5SVCallSVC_Handler()Supervisor Call, durch den SVC Befehl (früher SWI genannt) ausgelöst. Wird von Programmen genutzt, um Funktionen des Betriebssystems aufzurufen.
0x003012-4DebugDebugMon_Handler()Debug Monitor für softwarebasiertes Debugging (selten genutzt)
0x0034reserviert
0x003814-2PendSVPendSV_Handler()Pendable Request for System Service. Wird vom Betriebssystem durch Beschreiben des ICSR Registers ausgelöst, um den Kontext umzuschalten.
0x003c15-1SYSTICKSysTick_Handler()Wird aufgerufen, wenn der Systemtimer den Wert 0 erreicht.
STM32 Hardware Interrupts
0x00400WWDGWWDG_IRQHandler()Window Watchdog
0x00441PVDPVD_IRQHandler()PVD through EXTI Line detectio
0x00482TAMPERTAMPER_IRQHandler()Sabotage Signal vom Tamper Eingang
0x004C3RTCRTC_IRQHandler()Echtzeituhr
0x00504FLASHFLASH_IRQHandler()Schreibzugriff auf Flash wurde beendet
0x00545RCCRCC_IRQHandler()Wenn ein Oszillator oder die PLL bereit ist
0x00586EXTI0EXTI0_IRQHandler()Externer Interrupt Eingang 0
0x005C7EXTI1EXTI1_IRQHandler()Externer Interrupt Eingang 1
0x00608EXTI2EXTI2_IRQHandler()Externer Interrupt Eingang 2
0x00649EXTI3EXTI3_IRQHandler()Externer Interrupt Eingang 3
0x006810EXTI4EXTI4_IRQHandler()Externer Interrupt Eingang 4
0x006C11DMA1_Channel1DMA1_Channel1_IRQHandler()DMA Controller 2 Channel 1
0x007012DMA1_Channel2DMA1_Channel2_IRQHandler()DMA Controller 2 Channel 2
0x007413DMA1_Channel3DMA1_Channel3_IRQHandler()DMA Controller 2 Channel 3
0x007814DMA1_Channel4DMA1_Channel4_IRQHandler()DMA Controller 2 Channel 4
0x007C15DMA1_Channel5DMA1_Channel5_IRQHandler()DMA Controller 2 Channel 5
0x008016DMA1_Channel6DMA1_Channel6_IRQHandler()DMA Controller 2 Channel 6
0x008417DMA1_Channel7DMA1_Channel7_IRQHandler()DMA Controller 2 Channel 7
0x008818ADC1_2ADC1_2_IRQHandler()ADC1 and ADC2
0x008C19USB_HP_CAN_TXUSB_HP_CAN_TX_IRQHandler()USB High Priority or CAN TX
0x009020USB_LP_CAN_RX0USB_LP_CAN_RX0_IRQHandler()USB Low Priority or CAN RX
0x009421CAN_RX1CAN_RX1_IRQHandler()
0x009822CAN_SCECAN_SCE_IRQHandler()
0x009C23EXTI9_5EXTI9_5_IRQHandler()Externer Interrupt Eingang 5 bis 9
0x00A024TIM1_BRKTIM1_BRK_IRQHandler()Timer 1 Break
0x00A425TIM1_UPTIM1_UP_IRQHandler()Timer 1 Update
0x00A826TIM1_TRG_COMTIM1_TRG_COM_IRQHandler()Timer 1 Trigger and Commutation
0x00AC27TIM1_CCTIM1_CC_IRQHandler()Timer 1 Capture Compare
0x00B028TIM2TIM2_IRQHandler()Timer 2
0x00B429TIM3TIM3_IRQHandler()Timer 3
0x00B830TIM4TIM4_IRQHandler()Timer 4
0x00BC31I2C1_EVI2C1_EV_IRQHandler()I²C 1 event
0x00C032I2C1_ERI2C1_ER_IRQHandler()I²C 1 error
0x00C433I2C2_EVI2C2_EV_IRQHandler()I²C 2 event
0x00C834I2C2_ERI2C2_ER_IRQHandler()I²C 2 error
0x00CC35SPI1SPI1_IRQHandler()
0x00D036SPI2SPI2_IRQHandler()
0x00D437USART1USART1_IRQHandler()
0x00D838USART2USART2_IRQHandler()
0x00DC39USART3USART3_IRQHandler()
0x00E040EXTI15_10EXTI15_10_IRQHandler()Externer Interrupt Eingang 10 bis 15
0x00E441RTCAlarmRTCAlarm_IRQHandler()RTC alarm through EXTI line
0x00E842USBWakeupUSBWakeup_IRQHandler()USB wakeup from suspend through EXTI line
0x00EC43TIM8_BRKTIM8_BRK_IRQHandler()Timer 8 Break
0x00F044TIM8_UTIM8_UP_IRQHandler()Timer 8 Update
0x00F445TIM8_TRG_COMTIM8_TRG_COM_IRQHandler()Timer 8 Trigger and Commutation
0x00F846TIM8_CCTIM8_CC_IRQHandler()Timer 8 Capture Compare
0x00FC47ADC3ADC3_IRQHandler()
0x010048FSMCFSMC_IRQHandler()
0x010449SDIOSDIO_IRQHandler()
0x010850TIM5TIM5_IRQHandler()Timer 5
0x010C51SPI3SPI3_IRQHandler()
0x011052UART4UART4_IRQHandler()
0x011453UART5UART5_IRQHandler()
0x011854TIM6TIM6_IRQHandler()Timer 6
0x011C55TIM7TIM6_IRQHandler()Timer 7
0x012056DMA2_Channel1DMA2_Channel1_IRQHandler()DMA Controller 2 Channel 1
0x012457DMA2_Channel2DMA2_Channel2_IRQHandler()DMA Controller 2 Channel 2
0x012858DMA2_Channel3DMA2_Channel3_IRQHandler()DMA Controller 2 Channel 3
0x012C59DMA2_Channel4_5DMA2_Channel4_5_IRQHandler()DMA Controller 2 Channel 4 and 5

Über das VTOR Register kann man den Ort der Vektortabelle verändern und sie z.B. ins RAM verschieben.

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

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

Um Interrupts zu erlauben, muss man beide Funktionen NVIC_EnableIRQ() und __enable_irq() aufrufen.

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 "stm32f1xx.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++;
}

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

Standardmäßig sind alle I/O Pins als digitaler Eingang konfiguriert. Um deren Status abzufragen, liest man das jeweilige IDR Register (z.B. GPIOx->IDR).

Bevor man einen Pin als Ausgang verwendet, muss man ihn im Register GPIOx->CRL (für Pin 0-7) oder GPIOx->CRH (für Pin 8-15) konfigurieren. Hierbei wird zwischen normaler Ausgabe und alternativen Funktionen (wie z.B. serieller Port oder PWM Timer) unterschieden.

Für jeden Ausgang kann man die maximale frequenz 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 elektromagentischer Verträglichkeit soll man hier immer den niedrigsten Wert einstellen, der zur Anwendung passt.

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.

Schaue Dir die Beschreibung der GPIO Register CRL, CRH, IDR, ODR und BSRR im Referenzhandbuch an.

Analoge Eingänge

Die Taktfrequenz des Systems wird standardmäßig durch 2 geteilt um den ADC zu betreiben. Er funktioniert mit maximal 14MHz, daher ist es notwendig den Vorteiler im Register RCC->CFGR zu ändern, wenn der Systemtakt über 28MHz liegt.

Bevor man einen Pin als analogen Eingang verwendet, muss man ihn im Register GPIOx->CRL (für Pin 0-7) oder GPIOx->CRH (für Pin 8-15) konfigurieren. Zum Beispiel so für PA0:

    // Configure PA0 as analog input
    MODIFY_REG(GPIOA->CRL, GPIO_CRL_CNF0 + GPIO_CRL_MODE0, 0);

Initialisierung des ADC für einzelne Lesezugriffe:

// Initialize the ADC for single conversion mode
void init_analog()
{
    // Divide APB2 clock frequency by 8
    MODIFY_REG(RCC->CFGR, RCC_CFGR_ADCPRE, RCC_CFGR_ADCPRE_0 + RCC_CFGR_ADCPRE_1);
    
    // Enable Power for ADC
    SET_BIT(RCC->APB2ENR, RCC_APB2ENR_ADC1EN);

    // Switch the ADC on
    SET_BIT(ADC1->CR2, ADC_CR2_ADON);

    // Select software start trigger
    MODIFY_REG(ADC1->CR2, ADC_CR2_EXTSEL, ADC_CR2_EXTSEL_0 + ADC_CR2_EXTSEL_1 + ADC_CR2_EXTSEL_2);

    // Set sample time to 41.5 cycles
    MODIFY_REG(ADC1->SMPR2, ADC_SMPR2_SMP0, ADC_SMPR2_SMP0_2);

    // Delay 20 ms
    uint32_t start=systick_count;
    while (systick_count-start<20);

    // Start calibration
    SET_BIT(ADC1->CR2, ADC_CR2_ADON + ADC_CR2_CAL);

    // Wait until the calibration is finished
    while (READ_BIT(ADC1->CR2, ADC_CR2_CAL));
}

Lesen eines analogen Eingangs:

// Read from an analog input
uint16_t read_analog(int channel)
{
    // Select the channel
    MODIFY_REG(ADC1->SQR3, ADC_SQR3_SQ1, channel);

    // Clear the finish flag
    CLEAR_BIT(ADC1->SR, ADC_SR_EOC);

    // Start a conversion
    // These two bits must be set by individual commands!
    SET_BIT(ADC1->CR2, ADC_CR2_ADON);
    SET_BIT(ADC1->CR2, ADC_CR2_SWSTART);

    // Wait until the conversion is finished
    while (!READ_BIT(ADC1->SR, ADC_SR_EOC));

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

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

Jeder Timer kann bis zu vier PWM Signale mit maximal 16bit Auflösung (65535 Stufen) erzeugen. Damit kann man z.B. die Helligkeit von Lampen oder die Drehzahl von Motoren steuern.

Der Timer zählt fortlaufend von 0 an hoch bis zum Maximum, welches durch das TIMx->ARR Register festgelegt wird. Die Taktfrequenz wird vom Systemtakt abgeleitet und kann durch den ABP2 Prescaler (im Register RCC->CFGR) und den Timer Prescaler TIMx->PSC reduziert werden.

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.

Wenn also der Maximalwert als 50000 festgelegt wird, können die Ausgangsimpulse wahlweise 1 bis 50000 Takte breit sein. Mit der option "inverse polarity" im Register TIMx->CCER können die Ausgänge umgepolt werden, so dass sie im Low-Impule liefern.

Das folgende Beispielprogramm benutzt die Ausgänge von Timer 3 (PA6, PA7, PB0 und PB1), um dort vier angeschlossene Leuchtdioden unterschiedlich hell flimmern zu lassen:

#include "stm32f1xx.h"

void init_io()
{
    // Enable Port A, B and alternate functions
    SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN);
    SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPBEN);
    SET_BIT(RCC->APB2ENR, RCC_APB2ENR_AFIOEN);

    // PA6 = Timer 3 channel 1 alternate function output
    MODIFY_REG(GPIOA->CRL, GPIO_CRL_CNF6 + GPIO_CRL_MODE6, GPIO_CRL_CNF6_1 + GPIO_CRL_MODE6_0);

    // PA7 = Timer 3 channel 2 alternate function output
    MODIFY_REG(GPIOA->CRL, GPIO_CRL_CNF7 + GPIO_CRL_MODE7, GPIO_CRL_CNF7_1 + GPIO_CRL_MODE7_0);

    // PB0 = Timer 3 channel 3 alternate function output
    MODIFY_REG(GPIOB->CRL, GPIO_CRL_CNF0 + GPIO_CRL_MODE0, GPIO_CRL_CNF0_1 + GPIO_CRL_MODE0_0);

    // PB1 = Timer 3 channel 4 alternate function output
    MODIFY_REG(GPIOB->CRL, GPIO_CRL_CNF1 + GPIO_CRL_MODE1, GPIO_CRL_CNF1_1 + GPIO_CRL_MODE1_0);
}

void init_timer3_for_pwm()
{
    // Enable timer 3
    SET_BIT(RCC->APB1ENR, RCC_APB1ENR_TIM3EN);

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

    // Timer 3 channel 2 compare mode=PWM1 with the required preload buffer enabled
    MODIFY_REG(TIM3->CCMR1, TIM_CCMR1_OC2M + TIM_CCMR1_OC2PE, 
        TIM_CCMR1_OC2M_2 + TIM_CCMR1_OC2M_1 + TIM_CCMR1_OC2PE);

    // Timer 3 channel 3 compare mode = PWM1 with the required preload buffer enabled
    MODIFY_REG(TIM3->CCMR2, TIM_CCMR2_OC3M + TIM_CCMR1_OC1PE, 
        TIM_CCMR2_OC3M_2 + TIM_CCMR2_OC3M_1 + TIM_CCMR1_OC1PE);

    // Timer 3 channel 4 compare mode = PWM1 with the required preload buffer enabled
    MODIFY_REG(TIM3->CCMR2, TIM_CCMR2_OC4M + TIM_CCMR1_OC1PE, 
       TIM_CCMR2_OC4M_2 + TIM_CCMR2_OC4M_1 + TIM_CCMR1_OC1PE);

    // Timer 3 enable all four compare outputs
    SET_BIT(TIM3->CCER, TIM_CCER_CC1E + TIM_CCER_CC2E + TIM_CCER_CC3E + TIM_CCER_CC4E);

    // Timer 3 inverse polarity for all four compare outputs
    // SET_BIT(TIM3->CCER, TIM_CCER_CC1P + TIM_CCER_CC2P + TIM_CCER_CC3P + TIM_CCER_CC4P);

    // Timer 3 auto reload register, defines the maximum value of the counter in PWM mode.
    TIM3->ARR = 50000;

    // Timer 3 clock prescaler, the APB2 clock is divided by this value +1.
    TIM3->PSC = 0; // divide clock by 1 --> 160 pulses per second at 8Mhz

    // Timer 3 enable counter and auto-preload
    SET_BIT(TIM3->CR1, TIM_CR1_CEN + TIM_CR1_ARPE);
}

int main(void)
{
    init_io();
    init_timer3_for_pwm();

    // set PWM pulse width of all four outputs
    TIM3->CCR1 = 40;
    TIM3->CCR2 = 400;
    TIM3->CCR3 = 4000;
    TIM3->CCR4 = 40000; 
}

Die Timer 1 und 8 können komplementäre Ausgangssignale mit Tot-Zeit erzeugen, was für den Eigenbau von H-Brücken nützlich sein kann. Allerdings kollidieren die Ausgänge vom Timer 1 teilweise mit dem USB Port und der Timer 8 existiert nur bei den größeren High und XL Density Modellen.

Ich möchte darauf hinweisen, dass die Timer noch viele weitere Funktionen haben, die ich hier gar nicht alle zeigen kann.

Echtzeituhr

Die RTC kann dazu verwendet werden, um eine Uhr zu bauen. Technisch gesehen handelt es sich nur um einen simplen batteriebetriebenen Zähler mit Quarz-Oszillator, der üblicherweise genauer läuft, als der Systemtakt. Ein einstellbarer Vorteiler erzeugt den Sekunden-Takt, dahinter kommt ein 32bit Zähler. Uhrzeit und Datum muss man ggf. per Software anhand des Zählerstandes berechnen.

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 auch 10 so genannte Backup Register (mit je 16bit Breite) wo man an Daten ablegen kann. Das letzte Backup Register Nr. 9 ist für den Bootloader reserviert.

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.

RTC Initialisieren

Dieses Beispiel initialisiert die RTC so, dass jede Sekunde ein Interrupt aufgerufen wird. In der ISR wird die LED an Port PA5 getoggelt, so dass sie blinkt.
#include "stm32f1xx.h"

void initRtc()
{
    // Enable the backup domain
    SET_BIT(RCC->APB1ENR, RCC_APB1ENR_BKPEN + RCC_APB1ENR_PWREN);

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

    // Enable LSE oscillator
    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);

    // Wait until RTC is synchronized
    while(!READ_BIT(RTC->CRL, RTC_CRL_RSF)) {}

    // Wait until last write operation is done
    while(!READ_BIT(RTC->CRL, RTC_CRL_RTOFF)) {}

    // Enable second interrupt
    SET_BIT(RTC->CRH,RTC_CRH_SECIE);

    // Wait until last write operation is done
    while(!READ_BIT(RTC->CRL, RTC_CRL_RTOFF)) {}

    // Enter configuration mode
    SET_BIT(RTC->CRL,RTC_CRL_CNF);

    // Divide oscillator frequency by 32767+1 to get seconds
    RTC->PRLL=32767;
    RTC->PRLH=0;

    // Leave configuration mode
    CLEAR_BIT(RTC->CRL,RTC_CRL_CNF);

    // Wait until last write operation is done
    while(!READ_BIT(RTC->CRL, RTC_CRL_RTOFF)) {}

    // Enable interrupt in NVIC
    NVIC_EnableIRQ(3);
    __enable_irq();
}

void init_io()
{
    // Enable Port A
    SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN);

    // PA5 = Output
    MODIFY_REG(GPIOA->CRL, GPIO_CRL_CNF5 + GPIO_CRL_MODE5, GPIO_CRL_MODE5_0);
}

void RTC_Handler(void)
{
    // Toggle LED
    GPIOA->ODR ^= GPIO_ODR_ODR5;

    // Clear interrupt flag
    CLEAR_BIT(RTC->CRL,RTC_CRL_SECF);
}

int main(void)
{
    init_io();
    initRtc();
    while(1){};
}

In der Interrupt-Vektor Tabelle in startup/startup_stm32.s muss ein Eintrag für den "RTC_Handler" hinzugefügt werden, falls nicht vorhanden.

RTC lesen

Das Auslesen der Uhrzeit (also des Sekundenzählers) erfordert zwei Lesezugriffe zu je 16 bit. Es kann passieren, dass der Zähler zwischen den beiden Lesezugriffen verändert wird, was zu völlig falschen Ergebnissen führt. Um diesen Fehler sicher zu umgehen, liest man den Sekundenzähler wiederholt aus, bis man zweimal hintereinander den selben Wert erhält.
uint32_t read_rtc()
{
    // Wait until RTC is synchronized
    while(!READ_BIT(RTC->CRL, RTC_CRL_RSF)) {}

    // Repeat until got the same value 2 times.
    uint32_t old=0;
    uint32_t new=0;
    do
    {
        old=new;
        new = (((uint32_t) RTC->CNTH) << 16) | ((uint32_t)RTC->CNTL);        
    }
    while (old != new);
    return new;
}

RTC beschreiben

Beim Schreiben verlangt die RTC folgende Prozedur:
void update_rtc(uint32_t seconds)
{
    // Wait until last write operation is done
    while(!READ_BIT(RTC->CRL, RTC_CRL_RTOFF)) {}

    // Enter configuration mode
    SET_BIT(RTC->CRL,RTC_CRL_CNF);

    RTC->CNTH = (uint16_t)(seconds >> 16);
    RTC->CNTL = (uint16_t)(seconds & 0xFFFF);

    // Leave configuration mode
    CLEAR_BIT(RTC->CRL,RTC_CRL_CNF);

    // Wait until last write operation is done
    while(!READ_BIT(RTC->CRL, RTC_CRL_RTOFF)) {}
}

RTC Kalibrieren

Ohne Kalibrierung stellt man den Vorteiler wie oben gezeigt auf 32767, die Frequenz des Quarzes wird dann durch 32767+1 geteilt. Meine Testkandidaten hatten dabei 1-2 Sekunden Abweichung pro Tag, wobei die billigen Blue-Pill Boards überraschenderweise am besten abschnitten.

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.

Dann verringert man ggf. den Vorteiler RTC->PRLL so weit, dass die Uhr gerade eben etwas zu schnell läuft. Jede Verringerung um 1 macht die Uhr 2,637 Sekunden pro Tag schneller. Anschließend stellt man im Register BKP->RTCCR einen Korrekturwert (0-127) ein, wobei jede Stufe die Uhr um 0,082s pro Tag verlangsamt.

Wenn die Uhr zum Beispiel extreme 4 Sekunden pro Tag zu langsam wäre, würde man den Vorteiler um 2 verringern und dann den Korrekturwert auf 15 stellen:

Vorteiler:          2 * +2,637 = +5,274 Sekunden  (also RTC->PRLL = 32767 - 2)
Korrekturwert:     15 * -0,082 = -1,230 Sekunden  (also BKP->RTCCR = 15)
================================================
Summe:                           +4,044 Sekunden

USART Schnittstelle

Beim Nucleo64 Board ist die serielle Schnittstelle 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

Das folgende Beispielprogramm gibt "Hello World!" auf USART2 aus und schickt danach alle empfangenen Zeichen als echo an den PC zurück. Das Senden findet hier direkt statt (ggf. mit Warteschleife) während der Empfang interrupt-gesteuert stattfindet:

#include <stdio.h>
#include "stm32f1xx.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->SR & USART_SR_TXE));
        USART2->DR = *ptr++;
    }
    return len;
}

// called after each received character
void USART2_IRQHandler(void)
{
    char received=USART2->DR;

    // send echo back
    while(!(USART2->SR & USART_SR_TXE));
    USART2->DR = received;
}

int main(void)
{
    // Enable clock for Port A and USART2
    SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN);
    SET_BIT(RCC->APB1ENR, RCC_APB1ENR_USART2EN);

    // PA2 (TxD) shall use the alternate function with push-pull
    MODIFY_REG(GPIOA->CRL, GPIO_CRL_CNF2 + GPIO_CRL_MODE2, GPIO_CRL_CNF2_1 + GPIO_CRL_MODE2_1);

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

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

    // Enable interrupt in NVIC
    NVIC_EnableIRQ(38);
    __enable_irq();

    printf("Hello World!\n");
    while (1) {};
}
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.

USB Schnittstelle

Die USB Schnittstelle erfordert ein umfangreiches Softwarepaket, welches man sich nicht "mal eben schnell" aus dem Ärmel schüttelt. Beinahe alle Programmierer binden daher fertige Implementierungen in ihr Programm ein. Da das Nucleo-64 Board keine USB Buchse am Mikrocontroller hat, habe ich die folgenden Schritte mit dem "Blue Pill" Board ausprobiert

USB und CAN schließen sich gegenseitig aus, man kann nur eine davon gleichzeitig nutzen.

Der Systemtakt muss entweder 48MHz oder 72MHz betragen und aus einem Quarz gewonnen werden. Der USB Clock Prescaler muss je nach Modell und Systemtakt so eingestellt werden:

48MHz72Mhz
STM32F10011.5
STM32F101
STM32F102
STM32F103
STM32F10523
STM32F107

Die USB Buchse ist beim STM32F103 mit PB11 (D-) und PB12 (D+) verbunden. An D+ befindet sich ein 1,5kΩ Pull-Up Widerstand, welcher dem Host Computer signalisiert, dass ein Gerät angeschlossen wurde.

Manche Boards schalten den Widerstand durch einen Transistor. Dadurch kann man den Host Computer dazu bringen, das Gerät erneut zu erkennen, ohne das Kabel abstecken zu müssen. Beim "Maple Mini" Board schaltet PB9=High den Widerstand ein.

Beim "Blue Pill" Board ist dieser Widerstand fest mit 3,3V verbunden. Darum muss man bei diesem Board das USB Kabel nach jedem Firmware-Upload abstecken und wieder anschließen.

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_RESET);
        HAL_Delay(500);

        // LED Off
        HAL_GPIO_WritePin(LED_GPIO_Port,LED_Pin,GPIO_PIN_SET);
        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!".

Diese Variante belegt etwa 13kB Flash und 4,5kB RAM.

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. Ich habe diesen Code hier in ein Projekt für die System Workbench und das "Blue Pill" Board eingebaut.

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

#include <stdio.h>
#include "stm32f1xx.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
void delay_ms(int ms)
{
    uint32_t start=systick_count;
    while (systick_count-start<ms);
}

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

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

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

    // 48MHz using the 8MHz HSE oscillator with 6x PLL, lowspeed I/O runs at 24MHz
    WRITE_REG(RCC->CFGR, RCC_CFGR_PLLSRC + RCC_CFGR_PLLMULL6 + 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
    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, B and C
    SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN);
    SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPBEN);
    SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPCEN);

    // PC13 = Output for the LED
    MODIFY_REG(GPIOC->CRH, GPIO_CRH_CNF13 + GPIO_CRH_MODE13, GPIO_CRH_MODE13_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)
{
    init_io();
    UsbSetup();

    // Initialize system timer
    SysTick_Config(SystemCoreClock/1000);

    while (1)
    {
        // LED On
        WRITE_REG(GPIOC->BSRR,GPIO_BSRR_BR13);
        delay_ms(100);

        puts("Hello World!");

        // LED Off
        WRITE_REG(GPIOC->BSRR,GPIO_BSRR_BS13);
        delay_ms(1000);
    }
}

Nach der Initialisierung mittels UsbSetup() wird die Funktion UsbCharOut() benutzt, um einzelne Zeichen zu senden.

UsbSetup setzt voraus, dass Systemtakt und Anschluss bereits korrekt konfiguriert sind. Die entsprechenden Zeilen habe ich oben fett hervorgehoben. Außerdem muss man den Interrupt-Handler USB_LP_CAN_RX0_IRQHandler in die Datei startup/startup_stm32.s eintragen, fall noch nicht vorhanden.

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. Sein Beispielprogramm implementiert einen dreifachen USB-UART Adapter in C++. Ich habe es hier für die System Workbench und das Blue-Pill Board angepasst. Funktioniert prima, verstanden habe ich den Code jedoch (noch) nicht.

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 SBC->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 SBC->SCR Register das Bit 4 (SEVONPEND) gesetzt ist, löst ein anstehender Interrupt zugleich ein Ereignis aus, selbst wenn der Interrupt nicht freigeschaltet ist.

Arduino Umgebung

Man kann einige STM32 Mikrocontroller mit der STM32 Erweiterung für Arduino programmieren. Mit dem Arduino Framework ist das Programmieren einfacher, aber man kann nicht alle Funktionen des Chips ausnutzen.

Installiere zuerst die aktuelle Arduino IDE. Danach muss man über Werkzeuge/Board/Boardverwalter die Unterstützung für Arduino SAM Boards hinzufügen, darin befindet der benötigte Compiler für ARM Cortex-M3 und M4 Mikrocontroller. Diese Erweiterung unterstützt nur das "Arduino Due" Board. Für weitere Boards installierst du jetzt noch die STM32 Erweiterung.

Damit erzeugte Sketche enthalten immer die Serial Klasse für den virtuellen COM Port, was etwa 14kB Flash und 2,8kB RAM belegt. Die echten seriellen Ports sind über die Klassen Serial1, Serial2 und Serial3 ansprechbar.

Arduino basiert leider nicht auf der CMSIS, daher heißen dort die Konstanten für die Register und Bitmasken etwas anders. Schau dazu ggf. in die Datei Arduino_STM32-master\STM32F1\system\libmaple\stm32f1\include\series\gpio.h.

Die Pins PA13, PA14, PA15, PB3 and PB4 sind standardmäßig für die Debug Schnittstelle reserviert. Das kann man so ändern:

void setup()
{
    // Disable both SWD and JTAG to free PA13, PA14, PA15, PB3 and PB4
    afio_cfg_debug_ports(AFIO_DEBUG_NONE);
    
    or:      
    
    // Disable JTAG only to free PA15, PB3 and PB4. SWD remains active
    afio_cfg_debug_ports(AFIO_DEBUG_SW_ONLY);
}

STM32duino Bootloader

Optional kannst du dein Board mit dem STM32duino Bootloader ausstatten, damit der Chip auch über seinen USB Anschluss programmierbar ist. Es handelt sich hierbei um eine verbesserte Version des Maple Bootloaders.

Der dazugehörige Windows Treiber befindet sich in der STM32 Erweiterung im Verzeichnis Arduino_STM32-master\drivers\win\maple-dfu. Für Linux braucht man keinen Treiber.

Der Bootloader erscheint im Gerätemanager als "libusb-win32 devices/Maple DFU" und lässt die LED schnell blinken, solange er aktiv ist. Nach einer Sekunde startet er den installierten Sketch (falls vorhanden) wobei der Bootloader aus dem Windows Gerätemanager wieder verschwindet. Stattdessen erscheint dann der virtuelle COM-Port des Sketches.

Um den Bootloader zu aktivieren, drückt man den Reset-Knopf während die Arduino IDE versucht, den Sketch hochzuladen.

Außerhalb der Arduino IDE kann man den Bootloader mit dem Programm
Arduino_STM32-master\tools\win\dfu-util-0.9-win64\dfu-util.exe und
Arduino_STM32-master\tools\win\maple_upload.bat verwenden.

Virtueller COM-Port mit Arduino

Der virtuelle COM-Port ist fester Bestandteil der STM32 Erweiterung für Arduino, deswegen ist er sehr einfach zu programmieren. Die für serielle Ports übliche Initialisierungs-Prozedur entfällt:
void setup() 
{
    // PC13 is connected to the status LED
    pinMode(PC13, OUTPUT);
}

void loop() 
{
    digitalWrite(PC13, LOW);
    Serial.println("Tick");
    delay(500);

    digitalWrite(PC13, HIGH);
    Serial.println("Tack");
    delay(500);
}
Die Ausgabe kannst du im Seriellen Port Monitor der Arduino IDE sehen. Falls du lieber das Hammer Terminal benutzt, musst du dort den "DTR" Knopf unterhalb des Ausgabefensters einschalten.

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.

Diese IDE basiert wie die System Workbench ebenfalls auf Eclipse, der GNU ARM Embedded Toolchain (gcc) und OpenOCD für's Debugging. Auch hier sind die Binaries teilweise in 64bit und teilweise in 32bit, so dass man unter Linux die "multiarch" Unterstützung aktivieren muss.

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

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

Bei mir wurden unter Linux sämtliche Dialogfenster nicht korrekt dargestellt und die IDE stürzte ständig ab. Ich konnte dies beheben, indem ich die Verwendung der alten GTK-2.0 Library anstelle von GTK-3.0 forcierte. Man erreicht das durch folgenden Eintrag in der Datei ~/.profile:

export SWT_GTK3=0