Über den Beitrag
Im letzten Beitrag habe ich gezeigt, wie ihr mit der Arduino IDE Sketche auf STM32-Boards hochladet. In diesem Beitrag möchte ich nun einen Schritt weitergehen und einige ausgewählte Funktionen vorstellen. Dazu gehören einerseits grundlegende Features, die im Arduino-Funktionsumfang nicht zum Standard gehören, aber doch oft benötigt werden, wie etwa Timer, PWM oder verschiedene Energiesparmodi. Andererseits gehe ich bei bekannten Funktionen wie I²C, SPI oder Interrupts auf die Besonderheiten der STM32-Implementierung ein. Und schließlich gibt es noch die gewissen Extras, die nicht jedes Arduino-Board bietet, wie den Digital-Analog-Wandler (DAC) oder die Real-Time Clock (RTC).
Hier die Themen im Überblick:
- Boardauswahl für diesen Beitrag
- Hardware Timer Interrupts
- Pulsweitenmodulation (PWM)
- Analog-Digital-Wandler (ADC)
- Digital-Analog-Wandler (DAC)
- Externe Interrupts
- I²C
- SPI
- HardwareSerial
- SoftwareSerial
- Independent Watchdog (IWDG)
- Real Time Clock (RTC)
- Low-Power Modi
- Anhang – Erweiterter RTC-Sketch
Programmierebenen der SM32-Boards
Mikrocontroller (MCUs) lassen sich auf verschiedenen Abstraktionsebenen programmieren. Bei den STM32-MCUs sind das im Wesentlichen:
- Registerebene („Bare-Metal“, „Low-Level“): Dabei greift ihr direkt über Strukturen und Bitmasken auf die Register zu. Das ist sehr effizient, aber am wenigsten portabel.
- CMSIS (Cortex Microcontroller Software Interface Standard): Ist von ARM vorgegeben und wird ST mitgeliefert. Dort werden beispielsweise IRQ-Namen und Core-Funktionen definiert.
- ST eigene Abstraktionen:
- LL (Low-Layer): Immer noch nah an den Registern.
- HAL (Hardware Abstraction Layer): Höhere Abstraktion, höhere Portabilität, aber mehr Overhead.
- Frameworks / Cores für High-Level-Entwicklung: Hierzu gehört insbesondere das Arduino-Core Boardpaket von STM32duino, das ich schon im letzten Beitrag verwendet habe. HAL und LL sind die Grundlage dafür, „verstecken“ sich aber hinter den Arduino-typischen Funktionen.
In diesem Beitrag geht es nur um die höchste Ebene. Da ihr aber bei Beschäftigung mit den STM32-Boards sicherlich über Begriffe wie HAL, LL oder Bare-Metal stoßen werdet, wollte ich diese nicht unerwähnt lassen.
Boardauswahl für diesen Beitrag
Ich kann hier natürlich nicht sämtliche STM32-Boards besprechen, sondern habe dieselbe Auswahl wie im ersten Teil des Beitrages getroffen:
- Der „BluePill“ auf Basis des STM32F103C8
- Der „BlackPill“ auf Basis des STM32F411
- Das Nucleo-L432KC Board, als Vertreter eines Nucleo-32-Boards
- Das Nucleo-F446RE Board, als Vertreter eines Nucleo-64-Boards
- Der Arduino GIGA R1 WIFI, als Verterter eines STM32-basierten Arduinos
Der Fokus liegt dabei auf den ersten drei Boards.
Und noch ein Hinweis vorab: Wenn ich im Beitrag von „dem Blackpill“ oder „dem BluePill“ spreche, dann beziehe ich mich auf die o. g. Typen. Auch bei diesen Typen gibt es noch verschiedene Ausfertigungen oder Fakes. Deshalb kann es passieren, dass sich eure Boards in bestimmten Aspekten anders verhalten als hier besprochen.
Pinout der hier besprochenen Boards
„BluePill“ (STM32F103C8)
Das unten abgebildete Pinout-Diagramm für den BluePill habe ich aus diesem schönen Artikel von Renzo Mischianti. Wenn ihr tiefer in die spezifischen Eigenschaften der BluePill-Boards eintauchen wollt, dann kann ich Renzo Mischiantis Artikel sehr empfehlen.

Besonderheiten dieses BluePill-Boards:
- Einige Pins sind 5-Volt-tolerant (grüne Rechtecke), andere vertragen nur 3.3 Volt (rot-braune Rechtecke).
- Für PC13, PC14, PC 15 gilt:
- Sie dürfen im HIGH-Zustand keinen Strom liefern, d. h. ihr könnt den „HIGH“-Zustand als logisches Signal nutzen, aber zum Beispiel keine LED damit betreiben.
- Sie dürfen im LOW-Zustand maximal 3 mA aufnehmen (die LED an PC13 ist mit 3.3 Volt verbunden und leuchtet, wenn PC13 OUTPUT/LOW ist!).
- Sie dürfen nur mit maximal 2 MHz angesteuert und mit maximal 30 pF belastet werden.
- Das Board kann über den 3.3-Volt- oder den 5-Volt-Anschluss betrieben werden. VBAT versorgt lediglich die RTC und Backup-Register mit Strom.
„BlackPill“ STM32F411
Wie schon beim BluePill, habe ich mich auch beim BlackPill hinsichtlich des Pinout bei Renzo Mischianti bedient. Wenn ihr noch tiefer in die Details der BlackPill-Boards eintauchen wollt, dann empfehle ich auch hier Renzos Artikelserie. Dieser Artikel ist ein guter Startpunkt.

Besonderheiten dieses BlackPill-Boards: Es gelten die gleichen Einschränkungen und Anmerkungen wie für das BluePill Board, allerdings sind erheblich mehr Pins 5-Volt-tolerant.
Pinout Nucleo-L432KC
Das Nucleo-L432LC-Board versucht, eine gewisse Kompatibilität mit den Arduino Nano Boards zu erreichen. Das Pinout stimmt hinsichtlich der Pinbezeichnungen und der Funktionalitäten in weiten Teilen überein.

Besonderheiten des Nucleo-L432KC:
- Die GPIOs sind grundsätzlich nur 3.3-Volt-kompatibel.
- Die Pins D7 und D8 sind mit nichts verbunden. Ich komme bei den Solder Bridges weiter unten darauf zurück.
- Ihr könnt die Pins „3.3V“ und „5V“ nutzen, um damit andere Bauteile mit Strom zu versorgen. Der maximal zulässige Strom hängt von der Art der Stromversorgung des Boards ab. Siehe dazu das User Manual UM1956.
- Für die Versorgung des Boards über die 3.3V- und 5V-Pins müsst ihr die Konfiguration der Solder Bridges SBxy ändern (siehe weiter unten).
- Über VIN könnt ihr das Board mit 7 bis 12 Volt Spannung versorgen.
Pinout Nucleo-F446RE und Arduino GIGA R1 WIFI
Ein Pinout-Diagramm des Nucleo-F446RE findet ihr hier. Insgesamt stehen euch mit diesem Board 50 GPIOs zur Verfügung. Ein Pinout-Diagramm des Arduino GIGA R1 WIFI findet ihr hier auf den Arduinoseiten. Dieses Board besitzt sogar stolze 76 GPIOs.

Solder Bridges (Lötbrücken) der Nucleo-Boards
Wenn ihr euch das Nucleo-L432KC Board (oder andere Nucleo-Boards) anschaut, dann werdet ihr viele Lötbrücken erkennen, die mit „SBxy“ gekennzeichnet sind. Ein Teil davon ist offen, der andere ist über 0-Ω-Widerstände geschlossen. Eine detaillierte Beschreibung der Solder Bridges der Nucleo-32-Boards findet ihr im schon oben erwähnten UM1956. Hier einige ausgewählte Einstellungen:
- Um das Board über den 3.3-Volt-Pin mit Strom zu versorgen, müssen SB9 und SB14 offen sein. Als Einschränkung steht der boardinterne ST-LINK dann nicht zur Verfügung.
- Um das Board über den 5-Volt-Pin zu versorgen, muss SB9 offen sein. Der boardinterne ST-LINK steht dann so nicht zur Verfügung. Die Versorgung über den 5-Volt-Pin und Debugging über ST-LINK können kombiniert werden, dann ist allerdings eine bestimmte Prozedur einzuhalten (siehe wieder UM1956).
- Wenn ihr die Pins D7 und D8 als GPIOs verwenden wollt (PC14/PC15), dann müssen SB4, SB5, SB7 und SB17 offen sein; SB6 und SB8 müsst ihr schließen. Dann könnt ihr allerdings den externen Quarz für die RTC nicht mehr nutzen.
Ihr seht schon – es ist recht komplex. Und bevor ihr jetzt munter an eurem Board herumlötet, lest unbedingt das User Manual UM1956 aufmerksam!
Auch das Nucleo-F446RE Board besitzt sogar noch mehr Solder Bridges. Entsprechend gibt es auch hier Besonderheiten, wie beispielsweise die offenen Solder Bridges (SB48 / SB49) an PC14 und PC15. Erläuterungen dazu findet ihr im User Manual UM2324.
Hardware Timer Interrupts
Nun starten wir aber endlich mit der Praxis. Als Erstes schauen wir uns an, wie man Timer-Overflow-Interrupts mithilfe der HardwareTimer-Klasse programmiert. Die Definition der Klasse findet ihr in HardwareTimer.h. Diese Datei befindet sich im Verzeichnis …\Arduino15\packages\STMicroelectronics\hardware\stm32\2.11.0\libraries\SrcWrapper\inc. Die zugehörige Datei HardwareTimer.cpp befindet sich „in der Nähe“, unter …\SrcWrapper\src. Falls ihr tiefer in das Thema einsteigen und alle Funktionen der HardwareTimer-Klasse kennenlernen wollt, dann lohnt der Blick in diese Dateien.
Die STM32-MCUs besitzen mehrere Timer (TIMx). Welche das sind, findet ihr leicht im Datenblatt des zugrundeliegenden Mikrocontrollers. Dort steht auch, ob es sich um 16-Bit- oder 32-Bit-Timer handelt.
Ich verwende für die Beispiele bevorzugt den Timer TIM2, da er auf allen betrachteten Boards zur Verfügung steht. Auf dem BluePill ist TIM2 ein 16-Bit-Timer, auf dem BlackPill und dem Nucleo-L432KC ist es ein 32-Bit-Timer.
Ein Interrupt
Steuerung über den Prescalefaktor und Overflow-Wert
Schauen wir uns das erste Beispiel an:
HardwareTimer myTim(TIM2); void setup() { uint32_t prescaleFactor = 10000; // fits well to the BlackPill // uint32_t prescaleFactor = 7200; // fits well to the BluePill // uint32_t prescaleFactor = 8000; // fits well to the L432KC // uint32_t prescaleFactor = 8400; // fits well to the F446RE uint32_t timerOverflow = 10000; Serial.begin(115200); myTim.setPrescaleFactor(prescaleFactor); myTim.setOverflow(timerOverflow); myTim.attachInterrupt(onTimer); myTim.resume(); } void loop() {} void onTimer() { Serial.println("1 second"); }
Als Ausgabe erhaltet ihr auf dem seriellen Monitor sekündlich die Meldung „1 second“.
Erklärungen zum Sketch
Mit HardwareTimer myTim(TIM2)
erzeugt ihr das Objekt myTim und ordnet ihm TIM2 zu. Den Prescaler-Faktor legt ihr mit setPrescaleFactor()
fest. Er darf einen Wert zwischen 1 und 216 haben. Die Zählfrequenz des Timers ist der Systemtakt, geteilt durch den Prescalerfaktor. Ihr solltet ihn so wählen, dass ihr bequem damit rechnen könnt. Die Beispiele im Sketch regeln die Zählfrequenz auf 10 kHz herunter. Den Overflow legt ihr mit setOverflow()
fest. Bei 16-Bit-Timern ist der maximale Wert 216, bei 32-Bit-Timern ist es maximal 232-1, da timerOverflow
als 32-Bit-Zahl übergeben wird.
In dem obigen Beispiel ist Overflow 10000 und damit ist die Überlauffrequenz nach der folgenden Formel 1 Hz:
Entsprechend ist die minimale Interruptfrequenz bei Verwendung eines 16-Timers auf einem BlackPill Board 100 [MHz] / (216 * 216) = ~0.023 [Hz]. Für einen 32-Bit Timer wäre es ~3.55*10-7 [Hz], was einer Periode von ca. 32.5 Tagen entspricht.
Der Funktion attachInterrupt
übergebt ihr die auszuführende Interrupt-Serviceroutine (ISR). Mit resume()
startet ihr den Zähler bzw. setzt ihn fort.
Steuerung über die Periode
Alternativ legt ihr einfach die Periode fest. Dann müsst ihr nicht rechnen und könnt den Sketch auch unverändert auf andere Boards portieren:
HardwareTimer myTim(TIM2); void setup() { uint32_t timerOverflow = 1000000; Serial.begin(115200); myTim.setOverflow(timerOverflow, MICROSEC_FORMAT); myTim.attachInterrupt(onTimer); myTim.resume(); } void loop() {} void onTimer() { Serial.println("1 second"); }
Ich denke, der Sketch ist selbsterklärend.
Steuerung über die Frequenz
Als weitere Option könnt ihr auch die Überlauffrequenz festlegen. Allerdings ist dann 1 Hz die langsamste Einstellung, die ihr vornehmen könnt.
HardwareTimer myTim(TIM2); void setup() { uint32_t timerOverflow = 1; // 1 Hz is the slowest frequency Serial.begin(115200); myTim.setOverflow(timerOverflow, HERTZ_FORMAT); myTim.attachInterrupt(onTimer); myTim.resume(); } void loop() {} void onTimer() { Serial.println("1 second"); }
Mehrere Interrupts
Ihr könnt natürlich auch mehrere Interrupts einrichten. Hier ein Beispiel, das die Timerobjekte und ISR in kompakter Form als Arrays zusammenfasst. In den ISRs werden LEDs „getoggelt“. Falls ihr den Sketch ausprobieren wollt, dann hängt LEDs an die entsprechenden Pins.
HardwareTimer myTim0(TIM1); HardwareTimer myTim1(TIM2); HardwareTimer myTim2(TIM3); // TIM15 for L432KC; TIM13 for F446RE HardwareTimer myTim3(TIM4); // TIM16 for L432KC; TIM14 for F446RE HardwareTimer* myTimers[4] = {&myTim0, &myTim1, &myTim2, &myTim3}; const int ledPin[4] = {PA0, PA1, PA2, PA3}; // A0, A1, A2, A3: for L432KC uint32_t timerOverflow[4] = {500000, 1200000, 1700000, 2200000}; void onTimer0() { digitalWrite(ledPin[0], !digitalRead(ledPin[0])); } void onTimer1() { digitalWrite(ledPin[1], !digitalRead(ledPin[1])); } void onTimer2() { digitalWrite(ledPin[2], !digitalRead(ledPin[2])); } void onTimer3() { digitalWrite(ledPin[3], !digitalRead(ledPin[3])); } void (*onTimer[4])(void) = {onTimer0, onTimer1, onTimer2, onTimer3}; void setup () { for (int i=0; i<4; i++){ pinMode(ledPin[i], OUTPUT); myTimers[i]->setOverflow(timerOverflow[i], MICROSEC_FORMAT); myTimers[i]->attachInterrupt(onTimer[i]); myTimers[i]->resume(); } } void loop() {}
Timer Interrupts für das Arduino GIGA R1 WIFI Board
Da die HardwareTimer-Klasse fest im Boardpaket des STM32 Arduino Core Paketes verankert ist, funktioniert sie nicht mit dem Arduino GIGA R1 WIFI Board, wenn ihr das Standard Arduino Boardpaket verwendet.
Als Alternative steht euch die Bibliothek Portenta_H7_TimerInterrupt, die ihr über den Arduino Bibliotheksmanager installieren könnt, zur Verfügung. Sie wurde für das Portenta H7 Board geschrieben, funktioniert aber auch mit dem Arduino GIGA R1 WIFI, da beide denselben Mikrocontroller besitzen.
Beim Kompilieren der Beispielsketche bekommt ihr die Fehlermeldung: „This code is intended to run on the MBED ARDUINO_PORTENTA_H7 platform! Please check your Tools->Board setting“. Der Kompiler sagt euch aber auch, welche Datei diesen Fehler produziert hat. Es ist zum einen Portenta_H7_ISR_Timer.hpp, zum anderen Portenta_H7_TimerInterrupt.h.
Geht in die Dateien und sucht die Zeile (wahrscheinlich 30):
#if ( ( defined(ARDUINO_PORTENTA_H7_M7) || defined(ARDUINO_PORTENTA_H7_M4) ) && defined(ARDUINO_ARCH_MBED) )
Ändert sie in:
#if ( ( defined(ARDUINO_PORTENTA_H7_M7) || defined(ARDUINO_PORTENTA_H7_M4) || defined(ARDUINO_GIGA) ) && defined(ARDUINO_ARCH_MBED) )
In den Beispielsketchen könnt ihr ähnlich verfahren oder ihr kommentiert die relevanten Zeilen heraus. Dann sollte es gehen.
Pulsweitenmodulation (PWM)
Auch Pulsweitenmodulation (PWM) lässt sich mithilfe der HardwareTimer-Klasse bequem realisieren. Hier ein einfaches Beispiel:
HardwareTimer Timer(TIM1); void setup() { uint32_t channel = 3; int pwmPin = PA10; // D0 for L432KC; D2 for F446RE uint32_t frequency = 1; uint32_t dutyCycle = 20; // 20% Timer.setPWM(channel, pwmPin, frequency, dutyCycle); } void loop() {}
Die Ausgänge für die verschiedenen Timer und Kanäle sind bestimmten Pins zugeordnet. Für die hier besprochenen Boards findet ihr die Zuordnung in den Pinout-Diagrammen. Ansonsten schaut in das zugehörige Datenblatt. Wenn ihr das obige Programm hochladet und eine LED an den PWM-Pin hängt, dann werdet ihr sehen, wie diese im Sekundentakt für 200 ms aufleuchtet.
Man sollte erwarten, dass man am T1C3N-Ausgang (z. B. PB1 am BlackPill) automatisch eine invertierte Ausgabe erhält. Dem ist aber nicht so.
Ihr könnt bei Beendigung der Periode und/oder des DutyCyles Callbackfunktionen aufrufen, um weitere Aktionen zu veranlassen. Hier ein kleines Beispiel:
HardwareTimer Timer(TIM1); void setup() { uint32_t channel = 3; int pwmPin = PA10; // PA10 = D0 on the L432KC and D2 on the F446RE uint32_t frequency = 1; uint32_t dutyCycle = 20; // 20 % Serial.begin(115200); Timer.setPWM(channel, pwmPin, frequency, dutyCycle, periodCompleted, compareMatch); } void loop() {} void periodCompleted(){ Serial.print("Hi"); } void compareMatch(){ Serial.println("Ho"); }
Etwas mehr Einflussmöglichkeiten habt ihr mit dieser Variante:
HardwareTimer Timer(TIM1); void setup() { // uint32_t compareValue = 2000; Timer.setPrescaleFactor(10000); // -> 10 KHz für BlackPill (STMF411); adjust to your board Timer.setOverflow(10000); // -> 1 Hz Timer.setMode(3, TIMER_OUTPUT_COMPARE_PWM1, PA10); // channel, mode, pwmPin Timer.setCaptureCompare(3, 20, PERCENT_COMPARE_FORMAT); // channel, duty cycle (%), format // Timer.setCaptureCompare(3, compareValue); // channel, compareValue Timer.resume(); } void loop() {}
TIMER_OUTPUT_COMPARE_PWM1 ist einer der möglichen Timer-Modi. In HardwareTimer.h sind diese als enum (TimerModes_t) definiert. Schaut dort, wenn ihr die anderen Modi probieren wollt.
PERCENT_COMPARE_FORMAT ist eines der möglichen Compare-Formate. Diese sind im enum TimerCompareFormat_t
in HardwareTimer.h definiert. Wenn ihr setCaptureCompare()
kein Format übergebt, dann greift TICK_COMPARE_FORMAT als Default. In diesem Fall übergebt ihr den Comparewert als absoluten Zählerwert.
PWM mit dem Arduino GIGA R1 WIFI
Für PWM auf dem Arduino GIGA R1 WIFI könnt ihr die Bibliotheken Portenta_H7_PWM und Portenta_H7_Slow_PWM verwenden. Aber auch hier müsst ihr, wie zuvor für die Bibliothek Portenta_H7_TimerInterrupt beschrieben, die Borddefinition für den Arduino GIGA R1 WIFI ergänzen.
Analog-Digital-Wandler (ADC)
Als Nächstes schauen wir uns den ADC der STM32-Boards an. Hinsichtlich der Referenzspannung ist zu beachten:
- Die maximale Auflösung beträgt 12 Bit. Das BluePill- und das BlackPill-Board verfügen über keinen VREF-Pin. Grundsätzlich ist VDD die Standardreferenz.
- Beim Nucleo-L432KC gibt es zwar einen VREF-Pin, allerdings könnt ihr die Referenzspannung dort nur abgreifen, aber nicht anlegen.
- Beim Nucleo-F446RE lässt sich eine externe Referenz nutzen, wenn die Solder Bridges entsprechend gesetzt werden.
Die Programmierung erfolgt über die üblichen Arduinofunktionen. Für das Arduino GIGA R1 WIFI Board gibt es die Bibliothek Arduino_AdvanceAnalog, die die Nutzung seiner drei ADCs unterstützt.
Prüfung des ADC
Ich habe Spannungen zwischen 0 und 3.3 Volt angelegt, mit analogRead() gewandelt und das Ergebnis mit meinem Multimeter gegengeprüft. Von meinem Multimeter weiß ich, dass es sehr präzise ist. Ich habe jeweils 50 Messwerte gemittelt, was das Rauschen auf wenige Millivolt reduziert hat.
Hier der Sketch:
const float vRef = 3.3; void setup() { Serial.begin(115200); analogReadResolution(12); } void loop() { unsigned long int rawVal = 0; float voltage = 0.0; for (int i=0; i<50; i++) { rawVal += analogRead(PA1); delay(1); } voltage = (rawVal * vRef) / (4095 * 50); Serial.print("Raw: "); Serial.print(rawVal); Serial.print("\tVoltage [V]: "); Serial.println(voltage, 3); delay(1000); }
Und hier eine typische Ausgabe:

Und hier die Ergebnisse über den gesamten Spannungsbereich das BlackPill-Board:

Das Ergebnis sieht ziemlich gut aus. Die Linearität ist einwandfrei. Wenn ihr genau hinschaut, dann seht ihr, dass die Steigung der ADC-Gerade ein kleines bisschen geringer ist als die der Multimeter-Gerade. Der Grund war die Betriebsspannung des BlackPill-Boards, die nicht 3.3 Volt, sondern 3.311 Volt betrug. Nach Anpassung von vref
im obigen Sketch lagen die Geraden noch besser übereinander.
Ähnlich gute Ergebnisse gab es mit dem BluePill-Board (STM32F103C8) und dem Nucleo-L432KC.
Digital-Analog-Wandler DAC
Normalerweise erzeugt ihr mit analogWrite()
ein PWM-Signal. Wenn ihr aber analogWrite()
auf einen DAC-Pin anwendet, dann erhaltet ihr automatisch ein echtes analoges Signal. DAC-Ausgänge findet ihr an den Nucleo-Boards und dem Arduino GIGA R1 WIFI, nicht jedoch an BluePill und BlackPill.
Mit dem folgenden Sketch gebt ihr DAC-Rohwerte vor und erhaltet die berechneten Spannungen:
void setup() { Serial.begin(115200); analogWriteResolution(12); // 12 bit resolution } void loop() { float vref = 3.3; if(Serial.available()){ int rawVoltage = Serial.parseInt(); analogWrite(A3, rawVoltage); Serial.print(rawVoltage); Serial.print(" -> "); float voltage = (vref * rawVoltage / 4095.0); Serial.print(voltage, 3); Serial.println(" [V] (calc.)"); } }
Den Sketch habe ich genutzt, um die berechnete Spannung mit der tatsächlichen zu vergleichen. Hier das Ergebnis:

Über einen weiten Bereich stimmen die tatsächlichen Werte hervorragend mit den berechneten Werten überein. Nur an den Enden knickt die Kurve mit den tatsächlichen Werten weg. Es war mir nicht möglich, Spannungen kleiner 0.045 Volt (analogWrite(0)
) oder größer 3.28 Volt (analogWrite(4095)
) zu erzeugen. Abgesehen davon war ich mit dem Ergebnis sehr zufrieden. Wer es noch genauer haben möchte, der misst die tatsächliche Boardspannung an (V)REF und korrigiert vref
im obigen Sketch.
Auf dem Arduino GIGA R1 WIFI konnte ich Spannungen zwischen 0.02 Volt und 3.226 Volt erzeugen. Allerdings lag die Boardspannung hier auch nur bei 3.262 Volt.
Externe Interrupts
Ihr könnt an fast allen GPIOs externe Interrupts (EXTI) einrichten. Nur PB2 (auf dem BlackPill), PC13, PC14 und PC15 solltet ihr meiden.
Es gibt aber noch eine Einschränkung. Euch stehen 16 EXTI-Linien (0–15) zur Verfügung. Die EXTI-Linien können aber nur Pins zugeordnet werden, deren Zahl der EXTI-Linie entspricht. Also EXTI0 → PA0, PB0; EXTI1 → PA1, PB1; usw. Und pro Linie dürft ihr nur einen Interrupt zuweisen. Konsequenz: Interrupts an PA0 und PB1 könnt ihr parallel einrichten, Interrupts an PA0 und PB0 nicht.
Mit einem BlackPill-Board habe ich die Einrichtung von vier externen Interrupts ausprobiert. Jeder der vier Pins ist über einen Taster mit GND verbunden.

Die Pins werden in den Modus INPUT_PULLUP gebracht. Die zugehörigen Interrupts werden bei fallender Flanke ausgelöst, also bei Druck auf die Taster. Auf dem seriellen Monitor wird der auslösende Pin angezeigt. Hier der Sketch:
/* tested on a BlackPill (F411CE) */ int intPin[4] = {PA4, PA1, PB3, PB6}; volatile bool keyPressed = false; volatile int intKey = 0; void onKey0 () { keyPressed = true; intKey = 0; } void onKey1 () { keyPressed = true; intKey = 1; } void onKey2 () { keyPressed = true; intKey = 2; } void onKey3 () { keyPressed = true; intKey = 3; } void (*onKey[4])(void) = {onKey0, onKey1, onKey2, onKey3}; void setup() { Serial.begin(115200); for (int i=0; i<4; i++) { pinMode(intPin[i], INPUT_PULLUP); attachInterrupt (digitalPinToInterrupt(intPin[i]), onKey[i], FALLING); delay(10); } } void loop() { if (keyPressed) { Serial.print("Key "); Serial.print(intKey); Serial.println(" has been pressed"); delay(500); intKey = 0; keyPressed = false; } }
I2C
Auf dem BluePill (STM32F103C8) könnt ihr zwei I²C-Schnittstellen nutzen. Auf dem BlackPill (STM32F411), dem Nucleo-L432KC, dem Nucleo-F446RE und dem Arduino GIGA R1 WIFI sind es drei Schnittstellen. Wenn ihr nur eine Schnittstelle nutzt und nichts anderes definiert, dann befinden sich SDA und SCL an folgenden Pins:
- BluePill / BlackPill: SDA – PB7 / SCL – PB6
- Nucleo-L432KC: SDA – PB_7 (D4) / SCL – PB_6 (D5)
- Nucleo-F446RE: SDA – PB_9 (D14) / SCL – PB_8 (D15)
- Arduino GIGA R1 WIFI: SDA – PB11 (D20) / SCL – PH4 (D21)
Anschauungsbeispiel: MPU6000/MPU6050/MPU6500/MPU9250
Die Nutzung von I2C auf den STM32-Boards möchte ich anhand praktischer Beispiele erklären. Dazu habe ich die Beschleunigungssensoren MPU6000, MPU6050, MPU6500 bzw. MPU9250 ausgesucht. Die hier vorgestellten Sketche funktionieren für alle vier Sensoren. Vereinheitlicht bezeichne ich sie als „MPUXXXX“.
Das soll hier kein Beitrag über diese Sensoren werden, aber eine kurze Erklärung ist sicherlich nötig, damit wir uns gleich auf die relevanten I2C-Funktionen konzentrieren können.
- Die MPUXXXX werden durch einen Eintrag in ihr Power Management Register 1 (MPUXXXX_PWR_MGMT_1) geweckt und messen dann kontinuierlich die Beschleunigung in Richtung der x-, y- und z-Achse.
- Die Beschleunigungswerte sind 16-Bit-Zahlen, die „in einem Rutsch“ aus 6 Registern ausgelesen werden, beginnend mit dem Register MPUXXXX_ACCEL_XOUT_H (höherwertiges Byte der Beschleunigung in x-Richtung).
- Die 6 Bytes werden dann in die drei Beschleunigungswerte accX, accY und accZ umgerechnet.
- In den Beispielen beträgt die Auflösung +/- 2 g, d. h. 214 (= 16384) entspricht einem g.
Anschluss
Stellvertretend seht ihr hier zwei MPU6500/MPU920 an I2C-1 (default) und I2C-2 eines BlackPillboards:

Ein I²C‑Gerät an den Standardpins
Wenn ihr die Standardschnittstelle mit den Standardpins nutzt, dann gibt es hinsichtlich der I2C-Funktionen keine Überraschungen:
#include "Wire.h" #define MPUXXXX_ADDR 0x68 // Alternatively set AD0 to HIGH --> Address = 0x69 // MPUXXXX registers #define MPUXXXX_PWR_MGMT_1 0x6B #define MPUXXXX_ACCEL_XOUT_H 0x3B char result[7]; // for formatted output void setup() { Serial.begin(115200); Wire.begin(); writeRegister(MPUXXXX_PWR_MGMT_1, 0x00); // wake up MPUXXXX } void loop() { uint8_t data[6]; int16_t accX, accY, accZ; readRegister(MPUXXXX_ACCEL_XOUT_H, data, 6); accX = data[0] << 8 | data[1]; accY = data[2] << 8 | data[3]; accZ = data[4] << 8 | data[5]; Serial.print("AcX = "); Serial.print(toStr(accX)); Serial.print(" | AcY = "); Serial.print(toStr(accY)); Serial.print(" | AcZ = "); Serial.println(toStr(accZ)); delay(1000); } void writeRegister(uint8_t reg, uint8_t data) { Wire.beginTransmission(MPUXXXX_ADDR); Wire.write(reg); Wire.write(data); Wire.endTransmission(true); } void readRegister(uint8_t reg, uint8_t* buffer, uint8_t length) { Wire.beginTransmission(MPUXXXX_ADDR); Wire.write(reg); Wire.endTransmission(false); Wire.requestFrom((uint8_t)MPUXXXX_ADDR, length); for (int i=0; i<length; i++){ buffer[i] = Wire.read(); } } char* toStr(int16_t character) { sprintf(result, "%6d", character); return result; }
Der Vollständigkeit halber, hier noch eine Beispielausgabe:

Ein I²C‑Gerät an alternativen Pins
Wenn ihr alternative I²C-Pins nutzen wollt, dann müsst ihr ein TwoWire-Objekt erzeugen und ihm die Pins übergeben. Dabei spielt es keine Rolle, ob es sich um alternative Pins der I2C-Standardschnittstelle oder um alternative Schnittstellen handelt.
#include "Wire.h" #define MPUXXXX_ADDR 0x68 // Alternatively set AD0 to HIGH --> Address = 0x69 // MPUXXXX registers #define MPUXXXX_PWR_MGMT_1 0x6B #define MPUXXXX_ACCEL_XOUT_H 0x3B char result[7]; // for formatted output /* create a TwoWire object */ TwoWire myWire(PB9, PB8); // SDA, SCL void setup() { Serial.begin(115200); myWire.begin(); writeRegister(MPUXXXX_PWR_MGMT_1, 0x00); // wake up MPUXXXX } void loop() { uint8_t data[6]; int16_t accX, accY, accZ; readRegister(MPUXXXX_ACCEL_XOUT_H, data, 6); accX = data[0] << 8 | data[1]; accY = data[2] << 8 | data[3]; accZ = data[4] << 8 | data[5]; Serial.print("AcX = "); Serial.print(toStr(accX)); Serial.print(" | AcY = "); Serial.print(toStr(accY)); Serial.print(" | AcZ = "); Serial.println(toStr(accZ)); delay(1000); } void writeRegister(uint8_t reg, uint8_t data) { myWire.beginTransmission(MPUXXXX_ADDR); myWire.write(reg); myWire.write(data); myWire.endTransmission(true); } void readRegister(uint8_t reg, uint8_t* buffer, uint8_t length) { myWire.beginTransmission(MPUXXXX_ADDR); myWire.write(reg); myWire.endTransmission(false); myWire.requestFrom((uint8_t)MPUXXXX_ADDR, length); for (int i=0; i<length; i++){ buffer[i] = myWire.read(); } } char* toStr(int16_t character) { sprintf(result, "%6d", character); return result; }
Mehrere I²C‑Schnittstellen nutzen
Wenn ihr mehrere I2C-Schnittstellen nutzen wollt, dann müsst ihr für jede ein eigenes TwoWire-Objekt erschaffen. Wenn ihr Funktionen definiert, die die TwoWire-Objekte benutzen (so wie writeRegister()
und readRegister()
), dann übergebt ihr die TwoWire-Objekte als Pointer oder als Referenz.
#include "Wire.h" #define MPUXXXX_ADDR 0x68 // Alternatively set AD0 to HIGH --> Address = 0x69 // MPUXXXX registers #define MPUXXXX_PWR_MGMT_1 0x6B #define MPUXXXX_ACCEL_XOUT_H 0x3B char result[7]; // for formatted output /* create two TwoWire instances */ TwoWire Wire_1(PB7, PB6); // you could also use the default Wire object instead TwoWire Wire_2(PB3, PB10); void setup() { Serial.begin(115200); Wire_1.begin(); Wire_2.begin(); writeRegister(&Wire_1, MPUXXXX_PWR_MGMT_1, 0x00); // wake up MPUXXXX-1 writeRegister(&Wire_2, MPUXXXX_PWR_MGMT_1, 0x00); // wake up MPUXXXX-2 } void loop() { uint8_t data[6]; int16_t accX_1, accY_1, accZ_1, accX_2, accY_2, accZ_2; readRegister(&Wire_1, MPUXXXX_ACCEL_XOUT_H, data, 6); accX_1 = (data[0] << 8) | data[1]; accY_1 = (data[2] << 8) | data[3]; accZ_1 = (data[4] << 8) | data[5]; Serial.print("AcX_1 = "); Serial.print(toStr(accX_1)); Serial.print(" | AcY_1 = "); Serial.print(toStr(accY_1)); Serial.print(" | AcZ_1 = "); Serial.println(toStr(accZ_1)); Serial.flush(); readRegister(&Wire_2, MPUXXXX_ACCEL_XOUT_H, data, 6); accX_2 = (data[0] << 8) | data[1]; accY_2 = (data[2] << 8) | data[3]; accZ_2 = (data[4] << 8) | data[5]; Serial.print("AcX_2 = "); Serial.print(toStr(accX_2)); Serial.print(" | AcY_2 = "); Serial.print(toStr(accY_2)); Serial.print(" | AcZ_2 = "); Serial.println(toStr(accZ_2)); Serial.println(); delay(1000); } void writeRegister(TwoWire *_wire, uint8_t reg, uint8_t data) { _wire->beginTransmission(MPUXXXX_ADDR); _wire->write(reg); _wire->write(data); _wire->endTransmission(true); } void readRegister(TwoWire *_wire, uint8_t reg, uint8_t* buffer, uint8_t length) { _wire->beginTransmission(MPUXXXX_ADDR); _wire->write(reg); _wire->endTransmission(false); _wire->requestFrom((uint8_t)MPUXXXX_ADDR, length); for (int i=0; i<length; i++){ buffer[i] = _wire->read(); } } char* toStr(int16_t character) { sprintf(result, "%6d", character); return result; }
Hier die zugehörige Ausgabe:

Bibliotheken nutzen
In vielen Fällen werdet ihr für die Steuerung eurer I²C‑Geräte fertige Bibliotheken nutzen, wie z.B. meine Bibliothek MPU9250_WE. Die meisten Bibliotheken für I2C-Geräte erlauben es, TwoWire-Objekte zu übergeben. Das würde dann so oder so ähnlich aussehen:
TwoWire Wire_1(PB7, PB6); TwoWire Wire_2(PB3, PB10); MPU9250_WE myMPU9250_1 = MPU9250_WE(&Wire_1); MPU9250_WE myMPU9250_2 = MPU9250_WE(&Wire_2);
SPI
Für SPI ist das alles ähnlich. Auf dem BluePill-Board (STM32F103C8), dem Nucleo-L432KC und dem Arduino GIGA R1 WIFI stehen euch zwei SPI-Schnittstellen zur Verfügung. Auf dem BlackPill (STM32F411) und dem Nucleo-F446RE sind es vier. Die Standardpins sind:
- BluePill / BlackPill: SCLK – PA5 / MISO – PA6 / MOSI – PA7
- Nucleo-L432KC: SCLK – PB_3 (D13) / MISO – PB_4 (D12) / MOSI – PB_5 (D11)
- Nucleo-F446RE: SCLK – PA_5 (D13) / MISO – PA_6 (D12) / MOSI – PA_7 (D11)
- Arduino GIGA R1 WIFI: SCLK – PH6 (D13) / MISO – PJ11 (D12) / MOSI – PJ10 (D11)
Hier stellvertretend zwei MPU6500/MPU9250 an SPI1 (default) und SPI2 eines BlackPill-Boards:

Ein SPI-Gerät an den Standardpins
Der Sketch für die Nutzung der Standardpins birgt keine Überraschungen:
#include <SPI.h> // MPUXXXX registers #define MPUXXXX_PWR_MGMT_1 0x6B #define MPUXXXX_ACCEL_XOUT_H 0x3B char result[7]; // for formatted output const int CS_PIN = PA4; // Chip Select void setup() { Serial.begin(115200); pinMode(CS_PIN, OUTPUT); digitalWrite(CS_PIN, HIGH); SPI.begin(); writeRegister(MPUXXXX_PWR_MGMT_1, 0x00); // wake up MPUXXXX } void loop() { uint8_t data[6]; int16_t accX, accY, accZ; readRegisters(MPUXXXX_ACCEL_XOUT_H, data, 6); accX = (data[0] << 8) | data[1]; accY = (data[2] << 8) | data[3]; accZ = (data[4] << 8) | data[5]; Serial.print("AcX = "); Serial.print(toStr(accX)); Serial.print(" | AcY = "); Serial.print(toStr(accY)); Serial.print(" | AcZ = "); Serial.println(toStr(accZ)); delay(1000); } void writeRegister(uint8_t reg, uint8_t data) { SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0)); digitalWrite(CS_PIN, LOW); SPI.transfer(reg & 0x7F); // write access (MSB = 0) SPI.transfer(data); digitalWrite(CS_PIN, HIGH); } void readRegisters(uint8_t reg, uint8_t* buffer, uint8_t length) { SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0)); digitalWrite(CS_PIN, LOW); SPI.transfer(reg | 0x80); // read access (MSB = 1) for (uint8_t i = 0; i < length; i++) { buffer[i] = SPI.transfer(0x00); // send dummy byte } digitalWrite(CS_PIN, HIGH); } char* toStr(int16_t value) { sprintf(result, "%6d", value); return result; }
Ein SPI-Gerät an alternativen Pins
Wenn ihr alternative Pins nutzen wollt, dann müsst ihr diese mit SPI.setMOSI()
, SPI.setMISO()
und SPI.setSCLK()
setzen. So zum Beispiel für das BlackPill-Board:
..... const int CS_PIN = PA15; // Chip Select void setup() { Serial.begin(115200); pinMode(CS_PIN, OUTPUT); digitalWrite(CS_PIN, HIGH); SPI.setMOSI(PB5); // MOSI1-1 SPI.setMISO(PB4); // MISO1-3 SPI.setSCLK(PB3); // SCK1-3 SPI.begin(); .....
Es geht genauso, wenn ihr eine alternative SPI-Schnittstelle nutzen wollt. Hier beispielsweise an den SPI2 Pins eines BlackPill-Boards:
..... const int CS_PIN = PB9; // Chip Select void setup() { Serial.begin(115200); pinMode(CS_PIN, OUTPUT); digitalWrite(CS_PIN, HIGH); SPI.setMOSI(PB15); // MOSI2 SPI.setMISO(PB14); // MISO2 SPI.setSCLK(PB13); // SCK2-4 SPI.begin(); .....
Mehrere SPI-Schnittstellen nutzen
Wenn ihr mehrere SPI-Schnittstellen nutzen wollt, dann müsst ihr dafür eigene SPI-Objekte erzeugen, denen ihr die SPI-Pins mit dem Konstruktor übergebt. Merkwürdigerweise müsst ihr die SPI-Pins trotzdem noch einmal über die Funktionen setSCLK()
, setMISO()
und setMOSI()
festlegen.
Hier ein Beispiel, das sowohl auf dem BlackPill- als auch auf dem BluePill-Board funktioniert:
#include <SPI.h> // MPU6500 registers #define MPU6500_PWR_MGMT_1 0x6B #define MPU6500_ACCEL_XOUT_H 0x3B char result[7]; // for formatted output const int CS_PIN_1 = PA4; // chip select MPU6500-1 const int CS_PIN_2 = PB9; // chip select MPU6500-2 /* create two SPI instances */ SPIClass SPI_1(PA5, PA6, PA7); // SCLK, MISO, MOSI SPIClass SPI_2(PB13, PB14, PB15); // SCLK, MISO, MOSI void setup() { Serial.begin(115200); SPI_1.setMOSI(PA7); SPI_1.setMISO(PA6); SPI_1.setSCLK(PA5); SPI_2.setMOSI(PB15); SPI_2.setMISO(PB14); SPI_2.setSCLK(PB13); pinMode(CS_PIN_1, OUTPUT); digitalWrite(CS_PIN_1, HIGH); pinMode(CS_PIN_2, OUTPUT); digitalWrite(CS_PIN_2, HIGH); SPI_1.begin(); SPI_2.begin(); writeRegister(&SPI_1, CS_PIN_1, MPU6500_PWR_MGMT_1, 0x00); // wake up MPU6500-1 writeRegister(&SPI_2, CS_PIN_2, MPU6500_PWR_MGMT_1, 0x00); // wake up MPU6500-2 } void loop() { uint8_t data[6]; int16_t accX_1, accY_1, accZ_1, accX_2, accY_2, accZ_2; readRegisters(&SPI_1, CS_PIN_1, MPU6500_ACCEL_XOUT_H, data, 6); accX_1 = (data[0] << 8) | data[1]; accY_1 = (data[2] << 8) | data[3]; accZ_1 = (data[4] << 8) | data[5]; Serial.print("AcX_1 = "); Serial.print(toStr(accX_1)); Serial.print(" | AcY_1 = "); Serial.print(toStr(accY_1)); Serial.print(" | AcZ_1 = "); Serial.println(toStr(accZ_1)); Serial.flush(); readRegisters(&SPI_2, CS_PIN_2, MPU6500_ACCEL_XOUT_H, data, 6); accX_2 = (data[0] << 8) | data[1]; accY_2 = (data[2] << 8) | data[3]; accZ_2 = (data[4] << 8) | data[5]; Serial.print("AcX_2 = "); Serial.print(toStr(accX_2)); Serial.print(" | AcY_2 = "); Serial.print(toStr(accY_2)); Serial.print(" | AcZ_2 = "); Serial.println(toStr(accZ_2)); Serial.println(); delay(1000); } void writeRegister(SPIClass *_spi, int _csPin, uint8_t reg, uint8_t data) { _spi->beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0)); digitalWrite(_csPin, LOW); _spi->transfer(reg & 0x7F); // write access (MSB = 0) _spi->transfer(data); digitalWrite(_csPin, HIGH); } void readRegisters(SPIClass *_spi, int _csPin, uint8_t reg, uint8_t* buffer, uint8_t length) { _spi->beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0)); digitalWrite(_csPin, LOW); _spi->transfer(reg | 0x80); // read access (MSB = 1) for (uint8_t i = 0; i < length; i++) { buffer[i] = _spi->transfer(0x00); // send dummy byte } digitalWrite(_csPin, HIGH); } char* toStr(int16_t value) { sprintf(result, "%6d", value); return result; }
HardwareSerial
Im vorherigen Beitrag habe ich gezeigt, wie ihr das „Standard-Serial“ aktiviert und dafür sorgt, dass ihr den seriellen Monitor über USB erreicht. Aber die STM32-Boards haben mehrere HardwareSerial-Schnittstellen, die ihr nutzen könnt.
- BluePill (STM32F103C8) / BlackPill (STM32F411): 3
- Nucleo-L432KC: 2 (plus Standard-Serial über USB)
- Nucleo-F446RE: 6 (plus Standard-Serial über USB)
- Arduino GIGA R1 WIFI: 4 (plus Standard-Serial über USB)
Die zusätzlichen HardwareSerial-Schnittstellen zu nutzen, ist einfach. In einem praktischen Beispiel lassen wir ein BluePill- und ein BlackPill-Board über HardwareSerial kommunizieren.
Hier zunächst der Aufbau:

Und hier der Sketch, den ihr auf beide Boards hochladet:
HardwareSerial mySerial(PA3, PA2); // RX, TX void setup() { Serial.begin(115200); mySerial.begin(115200); } void loop() { if (mySerial.available()) { Serial.write(mySerial.read()); } if (Serial.available()) { mySerial.write(Serial.read()); } }
Für jeden Sketch macht ihr einen seriellen Monitor auf und könnt dann Nachrichten von einem zum anderen Monitor schicken.
Zusatzwissen: Die Standard-Serial Schnittstelle der Nucleo-Boards wird durch den Virtual COM Port des On-Board ST-LINK-Programmers realisiert.
Besonderheit des Arduino GIGA R1 WIFI
Wenn ihr den Arduino GIGA R1 WIFI mit dem Arduino Boardpaket nutzt, dann sind die HardwareSerial-Objekte samt Pins schon vordefiniert. Serial1 ist RX0/TX0 zugeordnet, Serial2 ist RX1/TX1 zugeordnet usw.
SoftwareSerial
Das Arduino-Core Paket von stm32duino beinhaltet eine SoftwareSerial-Bibliothek. Aber wenn möglich, dann solltet ihr immer mit den HardwareSerial-Schnittstellen arbeiten. Das ist grundsätzlich sicherer und ressourcenschonender.
Hinzu kommt, dass SoftwareSerial auf den STM32-Boards nach meiner Erfahrung nur mit niedrigen Baudraten fehlerfrei läuft. Auf meinen BluePill- und BlackPill-Boards funktionierte es nur bis 57600. Beim Nucleo-L432KC war bei 38400 Schluss.
Trotzdem hier noch ein Beispielsketch:
#include <SoftwareSerial.h> SoftwareSerial mySerial(A5, A6); // RX, TX void setup() { Serial.begin(9600); mySerial.begin(9600); } void loop() { if (mySerial.available()) { Serial.write(mySerial.read()); } if (Serial.available()) { mySerial.write(Serial.read()); } }
Independent Watchdog (IWDG)
Die STM32-Mikrocontroller haben einen Independent Watchdog (IWDG) und einen Window-Watchdog (WWDG). Der IWDG heißt independent (unabhängig), weil er komplett unabhängig vom Rest des Mikrocontrollers läuft. So unabhängig, dass er – einmal gestartet – nicht mehr per Software gestoppt werden kann. Der IWDG wird über einen Taktgeber gesteuert, der vom Systemtakt unabhängig ist. Das alles macht ihn ausgesprochen sicher.
Der Window-Watchdog hat seinen Namen von seiner Eigenschaft, nur in einem bestimmten Fenster „gefüttert“ werden zu können. WWDG und IWDG lösen einen Reset aus, wenn der Timeout erreicht ist. Aber nur der WWDG löst einen Reset aus, wenn er zu früh gefüttert wird.
Auch für den IWDG lässt sich eine Zeit definieren, in der sich der Watchdog nicht füttern lässt. Allerdings führt das verfrühte Füttern (der Reload) nicht zu einem Reset, sondern wird einfach nur ignoriert. Dieses verbotene Fenster lässt sich allerdings nicht bei allen STM32-Boards aktivieren.
Der IWDG hat im STM32-Boardpaket eine eigene Klasse spendiert bekommen, die sich einfach bedienen lässt. Für die Nutzung des WWDG müsst ihr eine Stufe tiefer auf die HAL-Funktionen gehen, was den Rahmen dieses Beitrags sprengen würde.
IWDG Beispielsketch
Nehmt ein STM32 Board eurer Wahl und hängt einen Taster an einen geeigneten Pin. Die andere Seite des Tasters verbindet ihr mit GND, also z. B. so:

Wenn ihr ein BlackPill-Board nehmt, dann braucht ihr für den Beispielsketch unten keinen Taster, sondern könnt die KEY-Taste nutzen. Sie hängt zwischen PA0 und GND.
Der Beispielsketch löst nach 10 Sekunden einen Reset aus, sofern der IWDG nicht durch einen Druck auf den Taster gefüttert wurde. Nach einem durch den IWDG verursachten Reset blinkt die Board-LED fünfmal kurz auf.
#include <IWatchdog.h> const int buttonPin = PA0; // BlackPill KEY pin const int ledPin = PC13; // Board LED void setup() { pinMode(ledPin, OUTPUT); digitalWrite(ledPin, HIGH); // BluePill/BlackPill board LED off pinMode(buttonPin, INPUT_PULLUP); if (IWatchdog.isReset(true)) { // LED blinks to indicate watchdog reset for (uint8_t i = 0; i < 5; i++) { digitalWrite(ledPin, LOW); delay(100); digitalWrite(ledPin, HIGH); delay(100); } } // Init the watchdog timer with 10 seconds timeout IWatchdog.begin(10000000); // timeout is uint32_t // 10 s timeout, but ignore reload with the first 5 s // IWatchdog.begin(10000000, 5000000); // does not work with all STM32 MCUs if (!IWatchdog.isEnabled()) { // LED blinks indefinitely while (1) { digitalWrite(ledPin, LOW); delay(500); digitalWrite(ledPin, HIGH); delay(500); } } } void loop() { // Compare current button state of the pushbutton value: if (digitalRead(buttonPin) == LOW) { digitalWrite(ledPin, LOW); delay(1000); digitalWrite(ledPin, HIGH); // Uncomment to change timeout value to 6 seconds //IWatchdog.set(6000000); // Reload the watchdog only when the button is pressed IWatchdog.reload(); } }
Erklärungen zum Beispielsketch:
Das IWDG-Objekt heißt IWatchdog und ist vordefiniert. Mit isReset()
prüft ihr, ob der IWDG einen Reset ausgelöst hat. Wenn ihr der Funktion true
übergebt, dann wird das Reset-Flag gelöscht. Übergebt ihr nichts oder false
, dann bleibt das Reset-Flag stehen.
Ihr startet den IWDG mit begin()
. Als Parameter übergebt ihr den Timeout in Mikrosekunden. Der optionale zweite Parameter legt die Länge des „verbotenen“ Fensters fest, in dem die Fütterung ignoriert wird. Wie schon erwähnt funktioniert das nicht mit allen STM32-Boards.
Jetzt fehlt nur noch:
isEnabled()
prüft, ob der IWDG aktiv ist.set()
erlaubt euch, den Timeout neu festzulegen.- Mit
reload()
füttert ihr den Watchdog, sodass die Timeoutperiode wieder von neuem beginnt.
Real Time Clock (RTC)
Die STM32-Boards bzw. die zugrunde liegenden Mikrocontroller besitzen eine Echtzeituhr. Die Zeit wird also nicht nur gemessen (das kann ja im Prinzip jeder Mikrocontroller), sondern als Jahr, Monat, Tag, Stunde, Minute und Sekunde verarbeitet.
Das stm32duino-Boardpaket hat keine integrierte RTC-Bibliothek. Ich empfehle die Verwendung von STM32duino RTC. Ihr könnt sie über den Bibliotheksmanager der Arduino IDE installieren.
Das Boardpaket für den Arduino GIGA R1 WIFI hat die RTC-Funktionen schon integriert. Im User Manual findet ihr sehr hilfreiche Beispielsketche. Ich gehe in diesem Beitrag nur auf die STM32duino_RTC Bibliothek ein.
Jede Uhr braucht einen Taktgeber. Dafür stehen euch drei Optionen zur Verfügung:
- LSI_CLOCK (Low Speed Internal Clock): interner, recht ungenauer Oszillator mit ca. 32 kHz. Dieser Taktgeber ist in der Bibliothek voreingestellt.
- LSE_CLOCK (Low Speed External Clock): die meisten STM32-Boards besitzen einen externen Quarz, der mit 32768 Hz schwingt. In der Regel sind diese sehr genau. Diese Option solltet ihr wählen.
- HSE_CLOCK (High-Speed External Clock): Nutzt den schnellen, externen Quarz auf dem Board (in der Regel 8 MHz).
- (HSI_CLOCK) (High-Speed Internal Clock): Schneller, interner Oszillator, ziemlich ungenau. Wird nicht von der Bibliothek unterstützt.
RTC-Beispielsketch
Die Bibliothek beinhaltet eine Reihe schöner Beispielsketche. Zum Einstieg empfehle ich SimpleRTC.ino. Ich denke, der Sketch ist selbsterklärend.
/* SimpleRTC This sketch shows how to configure the RTC and to display the date and time periodically Creation 12 Dec 2017 by Wi6Labs Modified 03 Jul 2020 by Frederic Pillon for STMicroelectronics This example code is in the public domain. https://github.com/stm32duino/STM32RTC */ #include <STM32RTC.h> /* Get the rtc object */ STM32RTC& rtc = STM32RTC::getInstance(); /* Change these values to set the current initial time */ const byte seconds = 0; const byte minutes = 0; const byte hours = 16; /* Change these values to set the current initial date */ /* Monday 15th June 2015 */ const byte weekDay = 1; const byte day = 15; const byte month = 6; const byte year = 15; void setup() { Serial.begin(9600); // Select RTC clock source: LSI_CLOCK, LSE_CLOCK or HSE_CLOCK. // By default the LSI is selected as source. //rtc.setClockSource(STM32RTC::LSE_CLOCK); rtc.begin(); // initialize RTC 24H format // Set the time rtc.setHours(hours); rtc.setMinutes(minutes); rtc.setSeconds(seconds); // Set the date rtc.setWeekDay(weekDay); rtc.setDay(day); rtc.setMonth(month); rtc.setYear(year); // you can use also //rtc.setTime(hours, minutes, seconds); //rtc.setDate(weekDay, day, month, year); } void loop() { // Print date... Serial.printf("%02d/%02d/%02d ", rtc.getDay(), rtc.getMonth(), rtc.getYear()); // ...and time Serial.printf("%02d:%02d:%02d.%03d\n", rtc.getHours(), rtc.getMinutes(), rtc.getSeconds(), rtc.getSubSeconds()); delay(1000); }
Hier die Ausgabe:

Es lohnt sich, auch die anderen Beispielsketche der Bibliothek durchzugehen. Unter anderem lernt ihr dort, wie ihr einen Alarminterrupt programmiert. Ferner empfehle ich einen Blick in das README.md der Bibliothek, denn dort sind alle Funktionen erklärt.
Was ich in der Bibliothek vermisste, war eine Routine zum Stellen der RTC nach der Kompilierzeit. Die RTC geht dann zwar ein paar Sekunden nach, aber man ist schon einmal in der Nähe der aktuellen Uhrzeit. Ich habe SimpleRTC.ino entsprechend erweitert. Ihr findet den Sketch im Anhang.
Wie genau ist die RTC?
Die Genauigkeit der RTC hängt von der Genauigkeit ihres Taktgebers ab. Mit dem internen Oszillator (LSI) war das Ergebnis miserabel. Die Abweichung betrug bei meinen Messungen bis zu einer Sekunde pro Minute! Mit der HSI-Clock sah es nicht besser aus.
Bessere Ergebnisse gab es hingegen mit dem externen Quarz (LSE). Die Abweichungen lagen im Bereich einiger Sekunden pro Tag – immerhin. Zum regelmäßigen Nachstellen würde sich ein DCF77-Modul oder – insbesondere für den Arduino GIGA R1 WIFI – ein NTP-Server anbieten.
Für das BluePill- und das BlackPill-Board wird an vielen Stellen empfohlen, die Pins PC14 und PC15 an der Pinleiste zu entfernen, damit der externe On-Board-Quarz ungestörter und damit genauer schwingen kann. Ich habe das aber nicht weiter untersucht.
Low-Power Modi
Wenn ihr für batteriebetriebene Projekte wertvollen Strom sparen wollt, dann stehen euch dazu verschiedene Energiesparmodi zur Verfügung. Diesbezüglich empfehle ich euch die Bibliothek STM32duino Low Power, die ihr über den Bibliotheksmanager der Arduino IDE installieren könnt.
Die verfügbaren Modi sind:
- idle(): Niedrige Aufwach-Latenz (µs-Bereich). Speicher und Spannungsversorgung bleiben erhalten. Minimale Energieeinsparung hauptsächlich beim Kern selbst.
- sleep(): Niedrige Aufwach-Latenz (µs-Bereich). Speicher und Spannungsversorgung bleiben erhalten. Minimale Energieeinsparung hauptsächlich beim Kern selbst, aber höher als im Idle-Modus.
- deepSleep(): Mittlere Aufwach-Latenz (ms-Bereich). Taktfrequenzen werden reduziert. Speicher und Spannungsversorgung bleiben erhalten. Wenn unterstützt, ist Peripherie-Wake-up möglich (UART, I2C …).
- shutdown(): Hohe Aufwach-Latenz (möglicherweise Hunderte von ms). Die Spannungsversorgung wird unterbrochen (außer im „Always-on“-Bereich), der Speicherinhalt geht verloren und das System wird neu gestartet.
Die „Weckmethoden“ sind:
- Feste Zeitspanne,
- RTC-Alarm,
- externer Interrupt an WakeUp-Pin,
- Peripherie-Ereignis (UART, I2C usw.).
Die Low-Power-Bibliothek ist mit einigen Beispielsketchen ausgestattet, die die verschiedenen Weckmethoden illustrieren. Hier ein Beispiel für das Aufwecken nach festgelegter Zeitspanne:
#include "STM32LowPower.h" const unsigned long sleepTime = 2000; // sleeping time in ms void setup() { pinMode(LED_BUILTIN, OUTPUT); for(int i=0; i<5; i++){ digitalWrite(LED_BUILTIN, LOW); delay(50); digitalWrite(LED_BUILTIN, HIGH); delay(50); } LowPower.begin(); } void loop() { digitalWrite(LED_BUILTIN, LOW); LowPower.deepSleep(sleepTime); // LowPower.shutdown(sleepTime); // try this instead of deepSleep digitalWrite(LED_BUILTIN, HIGH); LowPower.deepSleep(sleepTime); }
Der Sketch schaltet die Board-LED alle zwei Sekunden an bzw. aus. Dazwischen geht es in den DeepSleep-Modus. Wenn ihr Zeile 17 aus- und Zeile 18 entkommentiert, dann geht es stattdessen in den Shutdown Modus, der mit einem Reset endet. Den Reset erkennt ihr daran, dass setup()
ausgeführt wird und die LED schnell blinkt.
Stromverbrauch im Schlafmodus
Ich habe ein paar Strommessungen im DeepSleep- und im Shutdown-Modus mit BluePill- und BlackPill-Boards durchgeführt.
Da sich der Stromverbrauch der Power-LED im Shutdown-Modus besonders stark bemerkbar macht, habe ich die Versuche in diesem Modus zunächst mit LED durchgeführt und dann die LED vom Board entfernt.

Auf den Nucleo-Boards ist der ST-LINK ein erheblicher Stromfresser. Man kann ihn durch Einstellungen der Solder Bridges abklemmen, aber ich hatte schlicht keine Lust, das auszuprobieren. Auch den Arduino GIGA R1 WIFI habe ich diesbezüglich nicht unter die Lupe genommen.
Anhang – Erweiterter RTC-Sketch
Ich habe den Beispielsketch SimpleRTC.ino erweitert, sodass er die RTC nach dem Zeitpunkt des Kompilierens stellt. Dieser Zeitpunkt ist in den Zeichenketten __DATE__
und __TIME__
gespeichert. Beispiel: „Aug 11 2025“ und „22:17:25“. Es ist relativ einfach, daraus das Jahr, den Tag, die Stunden, Minuten und Sekunden zu extrahieren. __TIME__[1] ist beispielsweise die zweite Ziffer der Stunden, allerdings als ASCII-Code (z.B. 2 = ASCII-Code 50). Deswegen wird jeweils der ASCII-Code von 0 (= 48) abgezogen → __TIME__[1] – ‚0‘.
Der Monat wird durch einen schlichten Stringvergleich ermittelt. Die Berechnung des Wochentages ist ein wenig schwieriger. Darüber haben sich gottlob schon schlaue Leute Gedanken gemacht. Mehr dazu in diesem Wikipedia-Artikel. Ich verwende den Algorithmus von Tomohiko Sakamoto. Allerdings liefert er die Wochentage als Zahlen von 0 (= So) bis 6 (= Sa), und die STM32duino-RTC-Lib erwartet Zahlen von 1 (= Mo) bis 7 (= So). Das korrigiert Zeile 41.
Ich habe dann noch eine Funktion zum genaueren Stellen der Uhrzeit eingebaut. Stellt dazu im seriellen Monitor „Kein Zeilenende“ ein. Dann gebt die Uhrzeit im Format hh:mm:ss ein.
#include <STM32RTC.h> #define MONTH_IS(str) (__DATE__[0] == str[0] && __DATE__[1] == str[1] && __DATE__[2] == str[2]) /* get month from __DATE__ */ #define BUILD_MONTH ( \ MONTH_IS("Jan") ? 1 : \ MONTH_IS("Feb") ? 2 : \ MONTH_IS("Mar") ? 3 : \ MONTH_IS("Apr") ? 4 : \ MONTH_IS("May") ? 5 : \ MONTH_IS("Jun") ? 6 : \ MONTH_IS("Jul") ? 7 : \ MONTH_IS("Aug") ? 8 : \ MONTH_IS("Sep") ? 9 : \ MONTH_IS("Oct") ? 10 : \ MONTH_IS("Nov") ? 11 : \ MONTH_IS("Dec") ? 12 : 0 ) /* get day and year from __DATE__ */ #define BUILD_DAY (((__DATE__[4] == ' ') ? 0 : __DATE__[4] - '0') * 10 + (__DATE__[5] - '0')) #define BUILD_YEAR ((__DATE__[7]-'0') * 1000 + (__DATE__[8]-'0') * 100 + (__DATE__[9]-'0') * 10 + (__DATE__[10]-'0')) /* get hour, minute and second from __TIME__ */ #define BUILD_HOUR ((__TIME__[0]-'0')*10 + (__TIME__[1]-'0')) #define BUILD_MIN ((__TIME__[3]-'0')*10 + (__TIME__[4]-'0')) #define BUILD_SEC ((__TIME__[6]-'0')*10 + (__TIME__[7]-'0')) #define SAKAMOTO_T(m) \ ((m)==1?0:(m)==2?3:(m)==3?2:(m)==4?5:(m)==5?0:(m)==6?3:\ (m)==7?5:(m)==8?1:(m)==9?4:(m)==10?6:(m)==11?2:(m)==12?4:0) /* Sunday = 0, ..., Saturday = 6 (Original-Sakamoto) */ #define WD_SUN0(y,m,d) \ (((((y) - ((m) < 3)) \ + (((y) - ((m) < 3)) / 4) \ - (((y) - ((m) < 3)) / 100) \ + (((y) - ((m) < 3)) / 400) \ + SAKAMOTO_T(m) + (d)) % 7)) /* Needed for the STM32 RTC lib: Monday = 1, ..., Sunday = 7 */ #define WD_MON1(y,m,d) ((((WD_SUN0((y),(m),(d)) + 6) % 7) + 1)) #define BUILD_WEEKDAY WD_MON1(BUILD_YEAR, BUILD_MONTH, BUILD_DAY) char daysOfTheWeek[7][12] = {"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"}; /* Get the rtc object */ STM32RTC& rtc = STM32RTC::getInstance(); /* time and date variables */ byte seconds = BUILD_SEC; byte minutes = BUILD_MIN; byte hours = BUILD_HOUR; byte weekDay = BUILD_WEEKDAY; byte day = BUILD_DAY; byte month = BUILD_MONTH; byte year = BUILD_YEAR - 2000; void setup() { Serial.begin(115200); // Select RTC clock source: LSI_CLOCK, LSE_CLOCK or HSE_CLOCK. // By default the LSI is selected as source. rtc.setClockSource(STM32RTC::LSE_CLOCK); rtc.begin(); // initialize RTC 24H format // Set the time rtc.setHours(hours); rtc.setMinutes(minutes); rtc.setSeconds(seconds); // Set the date rtc.setWeekDay(weekDay); rtc.setDay(day); rtc.setMonth(month); rtc.setYear(year); // you can use also //rtc.setTime(hours, minutes, seconds); //rtc.setDate(weekDay, day, month, year); } void loop() { static unsigned long lastPrint = 0; if (millis() - lastPrint >= 1000){ // Print date... Serial.printf("%s %02d/%02d/%02d ", daysOfTheWeek[weekDay-1], rtc.getDay(), rtc.getMonth(), rtc.getYear()); // ...and time Serial.printf("%02d:%02d:%02d.%03d\n", rtc.getHours(), rtc.getMinutes(), rtc.getSeconds(), rtc.getSubSeconds()); lastPrint = millis(); } if (Serial.available()){ // set serial monitor to "No Line Ending" hours = Serial.parseInt(); minutes = Serial.parseInt(); seconds = Serial.parseInt(); rtc.setTime(hours, minutes, seconds); } }
So sollte dann die Ausgabe aussehen:
