Technische Daten, Hinweise und Programmier-Beispiele auf Basis der CMSIS für STM32F1 Mikrocontroller.
Meine Codeschnipsel sollen bei den ersten Schritten helfen, den Mikrocontroller kennen zu lernen und das Arbeiten mit dem Referenzhandbuch zu erlernen. Sie sind daher absichtlich minimalistisch gehalten, dafür umso besser kommentiert.
Der Hersteller hat sich für die Bezeichnung seiner Mikrocontroller folgendes Schema ausgedacht:
Low Power Familie
STM32L1 Serie: ARM Cortex M3 bis 32 MHz STM32L4 Serie: ARM Cortex M4F bis 80 MHz STM32L5 Serie: ARM Cortex M33F bis 110 MHz STM32U5 Serie: ARM Cortex M33F bis 160 MHz
Mainstream Familie
STM32F0 Serie: ARM Cortex M0 bis 48 MHz
STM32G0 Serie: ARM Cortex M0+ bis 64 MHz
STM32F1 Serie: ARM Cortex M3 bis 72 MHz
: ARM Cortex M4F bis 72 MHz
STM32G4 Serie: ARM Cortex M4F bis 170 MHz High Performance Familie
STM32F2 Serie: ARM Cortex M3 bis 120 MHz
STM32F4 Serie: ARM Cortex M4F bis 180 MHz STM32F7 Serie: ARM Cortex M7F bis 216 MHz STM32H7 Serie: ARM Cortex M7F bis 400 MHz |
Hinter der Modellnummer: | |||||||||||
Anzahl der Pins
T = 36 Pins
C = 48 Pins R = 64 Pins V = 100 Pins Z = 144 Pins |
||||||||||||
Flash Größe
4 = 16 kB
6 = 32 kB 8 = 64 kB B = 128 kB C = 256 kB D = 384 kB E = 512 kB F = 768 kB G = 1024 kB |
||||||||||||
Gehäuse
H =
T = U = Y = |
||||||||||||
Temperaturbereich
6 = -40 bis +85 °C
7 = -40 bis +105 °C |
Beispiel: Der STM32F103C8T6 hat 48 Pins, 64 kB Flash, 16kB RAM, ein QFP Gehäuse und ist für -40 bis +85 °C
Die Pinbelegung und elektrischen Daten findet man im jeweiligen Datenblatt. Für den Programmierer ist das Reference Manual am wichtigsten, da es die I/O Funktionen und Register beschreibt. Im Errata Sheet beschreibt der Hersteller überraschende Einschränkungen und Fehler der Mikrochips, teilweise mit konkreten Workarounds.
Density | Low | Medium | High | XL | ||||||
Flash Size | 16 kB | 32 kB | 64 kB | 128 kB | 256 kB | 384 kB | 512 kB | 768 kB | 1024 kB | |
RAM Size | 4 kB | 10 kB | 20 kB | 20 kB | 64 kB | 64 kB | 64 kB | 96 kB | 96 kB | |
Model ↱ | x4 | x6 | x8 | xB | xC | xD | xE | xF | xG | |
---|---|---|---|---|---|---|---|---|---|---|
Value line | STM32F100 | |||||||||
Access line | STM32F101 | |||||||||
USB Access line | STM32F102 | |||||||||
Performance line | STM32F103 | |||||||||
All lines above | ||||||||||
Connectivity line | STM32F105 | , | ||||||||
STM32F107 | ||||||||||
All | , |
Weiterführende Doku:
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,3 V.
Die Stromaufnahme ist im laufenden Betrieb mit 8bit Mikrocontrollern vergleichbar, im Stop Modus ist sie jedoch wesentlich höher. Für langfristigen Batteriebetrieb wird daher auf die sparsame L0 Serie verwiesen.
Viele I/O Pins sind 5 V 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.
Im open-drain Modus dürfen die 5 V toleranten Ausgänge durch externe Widerstände auf 5 V hoch
gezogen werden.
Die Ausgänge sind einzeln mit 25 mA belastbar, aber insgesamt muss die Stromaufnahme des Chips unter 150 mA bleiben. Gültige Logikpegel sind nur bis 8 mA garantiert. Die Ausgänge sind nicht Kurzschluss-fest.
Die Eingänge sind sehr hochohmig (da CMOS) und haben einen Schmitt-Trigger. Die ESD Schutzdioden sind teilweise nur sehr bedingt belastbar. Die internen Pull-Up und Pull-Down Widerstände haben ungefähr 40 kΩ. Alle I/O Pins haben ungefähr 5 pF Kapazität.
Der NRST (Reset) Pin hat intern einen Pull-Up Widerstand und kann sowohl vom Chip selber als auch von außen auf Low gezogen werden. Intern erzeugte Reset-Impulse sind garantiert mindestens 20 µs lang.
Für die Pins PC13, PC14 und PC15 gelten folgende Einschränkungen:
Hintergrund ist, dass diese drei Pins intern am (schwachen) Power-Switch der RTC hängen.
Das Nucleo-F103RB Board (aus der Nucleo-64 Reihe) ist ein hochwertiges Starter-Set zum günstigen Preis um 15 €.
Der 8 MHz Hauptquarz befindet sich auf dem ST-Link Adapter, er versorgt beide Mikrocontroller. Wenn man den ST-Link abtrennt, muss man den Mikrocontroller mit seinem internen R/C Oszillator betreiben oder einen zusätzlichen Quarz in die verbleibende Platine einlöten.
Der I²C Anschluss an der rechten Arduino Buchsenleiste (beschriftet als SCL/D15 und SDA/D14) erfordert die Verwendung von I2C1 mit "remapped" Pin Konfiguration. Die beiden Stifte Rx/D0 und Tx/D1 am rechten Arduino Connector haben keine Funktion.
Das User Manual enthält die vollständige Beschreibung des Boardes.
Das LeafLabs Maple Mini Board ist nach meinem Kenntnisstand das erste STM32 Board für Arduino gewesen. Es wird nicht mehr produziert, aber man bekommt chinesische Nachbauten davon ab 3,50 €. Die RTC ist mangels Quarz nicht benutzbar.
Es hat keinen separaten SWD Anschluss, die nötigen 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. Die originalen Maple Mini Boards wurden stets mit Arduino USB Bootloader verkauft, das ist bei den Nachbauten leider nicht immer der Fall.
Der Spannungsregler kann leicht überhitzen wenn man ihn bei mehr als 7 V Versorgungsspannung mit zusätzlichen Verbrauchern belastet.
Ein High Pegel an PB9 schaltet den Pull-Up Widerstand vom USB Anschluss aus.
Link zum Schaltplan des Boardes.
Das Blue Pill Board aus China bekommt man ab 2 €, ein absolutes Schnäppchen. Allerdings wird es fast immer mit gefälschten Chips bestückt. Das schwarze dazu kompatible Board von RobotDyn wurde nach meinem Kenntnisstand hingegen bisher immer mit originalen Chips bestückt.
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.
Wenn der Uhrenquarz benutzt wird, soll man die Stifte an PC14 und PC15 entfernen, damit er stabil schwingt.
Weil beim Blue-Pill Board der 5 V Anschluss direkt mit der USB Buchse verbunden ist, soll bei Nutzung von USB nicht gleichzeitig ein 5 V Netzteil verwendet werden. Das schwarze Board von RobotDyn hat eine Diode dazwischen, deswegen gilt die Einschränkung dort nicht. Ein 3,3V Netzteil ist bei beiden Boards zulässig.
Schaltplan vom Blue Pill Board, und vom schwarzen Board.
Dieses Board 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.
Wenn der Uhrenquarz benutzt wird, soll man die Stifte an PC14 und PC15 entfernen, damit er stabil schwingt.
Schaltplan vom STM32F103VET6 Minimum System Development Board
In den Fälschungen befindet sich oft ein APM32F103, GD32F103, CKS32F103, HK32F103, CH32F103, CS32F103, BLM32F103 oder MM32F103, meist umgelabelt als STM32. Dazu wurden folgende Mängel gemeldet:
Die genannten Mängel treten nur teilweise auf (nie alle zusammen). Das größte Problem dabei ist, dass man nicht weiß, welche einem verkauft werden.
Entwicklungsumgebungen (du brauchst nur eine):
Weitere Software:
Die Basis-Library für alle ARM Cortex-M Mikrocontroller heisst CMSIS Core. Dabei handelt es sich im Grunde genommenum einen Haufen Definitionen für alle Register, damit man sie mit Namen statt über Hexadezimal-Codes ansprechen kann. Außerdem enthält die CMSIS wenige Hilfsfunktionen, die den ARM Kern konfigurieren. Die CMSIS ist von ARM spezifiziert und wird von allen Chip Herstellern in spezifischen Varianten bereit gestellt. ST hat ein paar hilfreiche Makros hinzugefügt, wie MODIFY_REG().
Darauf aufbauend stellt die Firma ST ihr proprietäres Cube HAL Framework bereit, das die Wiederverwendbarkeit von Code beim Wechsel auf andere STM32 Modelle erleichtern soll. Dazu gehört das Programm Cube MX (welches in die IDE integriert wurde), womit man Quelltext-Projekte einschließlich Code zur Konfiguration der I/O Funktionen und Taktversorgung erzeugt.
Ich empfehle, die Programmierung zunächst anhand des Referenzhandbuches (ohne HAL) zu lernen. So lernt man die Grundlagen, mit denen man die Funktionsweise der knapp dokumentierten HAL besser durchblickt.
Von 2007 bis 2011 war die inzwischen veraltete Standard Peripheral Library (SPL) im Umlauf. Einige Beispiele im Internet beruhen noch darauf. In neuen Projekten sollte man sie nicht mehr verwenden.
Der arm-gcc Compiler bringt die Standard-C Bibliotheken mit, denen ich weiter unten ein eigenes Kapitel gewidmet habe.
Beispiel für einen einfachen LED-Blinker an PA5 und PC13 auf Basis der CMSIS:
// Filename: main.c #include <stdint.h> #include "stm32f1xx.h" // delay loop for 8 MHz CPU clock with optimizer enabled void delay(uint32_t msec) { for (uint32_t j=0; j < msec * 2000; j++) { __NOP(); } } int main() { // Enable Port A, C and alternate functions SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN + RCC_APB2ENR_IOPCEN); // PA5 = Output for LED MODIFY_REG(GPIOA->CRL, GPIO_CRL_CNF5, 0b00 << GPIO_CRL_CNF5_Pos); MODIFY_REG(GPIOA->CRL, GPIO_CRL_MODE5, 0b01 << GPIO_CRL_MODE5_Pos); // PC13 = Output for LED MODIFY_REG(GPIOC->CRH, GPIO_CRH_CNF13, 0b00 << GPIO_CRH_CNF13_Pos); MODIFY_REG(GPIOC->CRH, GPIO_CRH_MODE13, 0b01 << GPIO_CRH_MODE13_Pos); while(1) { // LED Pin -> High WRITE_REG(GPIOA->BSRR, GPIO_BSRR_BS5); WRITE_REG(GPIOC->BSRR, GPIO_BSRR_BS13); delay(500); // LED Pin -> Low WRITE_REG(GPIOA->BSRR, GPIO_BSRR_BR5); WRITE_REG(GPIOC->BSRR, GPIO_BSRR_BR13); delay(500); } }
Ich weiß dass man Delays besser mit einem Timer realisiert. Hier wollte ich jedoch ein möglichst simples Programmbeispiel zeigen.
Mit dem SWJ Debug Port übertragt man das fertige Programm in den Mikrocontroller und kann es im laufenden Betrieb untersuchen. Zum Beispiel kann man das Programm jederzeit anhalten und dann den Inhalt des Speichers anschauen. Wenn es hängt, kann der Debugger anzeigen, wo das passiert. Der Debugger kann sogar melden, wenn ausgewählte Variablen verändert werden.
SWJ ist die einzige Schnittstelle, die von der Cube IDE unterstützt wird. Sie funktioniert unabhängig von Boot Modus, Taktquelle, Spannung und Temperatur. Die SWJ Schnittstelle ist nach dem Reset standardmäßig aktiviert, kann jedoch per Software deaktiviert werden.
Achtung: Die Schnittstelle funktioniert nicht im Stop, Standby und Sleep Modus!
Die SWJ Schnittstelle unterstützt zwei Übertragungsprotokolle: JTAG und SWD. Das neuere SWD Protokoll wird bevorzugt, da es schneller ist und nur drei Leitungen benötigt: GND, SWDIO und SWCLK.
Der dazu passende USB Adapter heisst "ST-Link", es muss Version 2.0 oder neuer sein. Man kann den ST-Link Adapter des Nucleo-64 Boardes 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 CN4 | Mikrocontroller | Beschreibung | ||
---|---|---|---|---|
Pin 1 | ← | VDD | Misst die Spannungsversorgung der Zielschaltung, optional | |
Pin 2 | → | SWCLK | PA14 | Serial Wire Clock |
Pin 3 | GND | Common Ground (Masse) | ||
Pin 4 | ↔ | SWDIO | PA13 | Serial Wire Data In and Out |
Pin 5 | → | NRST | Reset Signal, optional siehe Verbindungsoptionen in der Cube IDE | |
Pin 6 | ← | SWO | PB3 | Serial Wire Output, optional siehe Trace Meldungen ausgeben |
Außerdem enthalten diese ST-Link Modelle auch einen USB-UART Adapter mit den Anschlüssen Tx und Rx.
Die Cube Programme verlangen nach einem Firmware-Update, was auch auf nicht originalen ST-Links in der Regel problemlos klappt. Die anderen oben genannten Programme sind hingegen auch mit älteren Firmware Versionen zufrieden.
Viele Windows Programme benötigen den libusb-win32 Treiber, um den ST-Link anzusteuern. Falls dieser fehlt oder nicht richtig geladen wird, siehe hier.
Bei den chinesischen ST-Link v2 Sticks empfehle ich, die Rückseite der Platine innen mit Pappkarton abzudecken, damit kein Kurzschluss zum Aluminium-Gehäuse entsteht. Der Reset-Ausgang dieser Sticks funktioniert nur mit STM8! Wer eine ruhige Hand hat, kann sich eine SWO Leitung nachrüsten:
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*, PB4. SWD remains active MODIFY_REG(AFIO->MAPR, AFIO_MAPR_SWJ_CFG, AFIO_MAPR_SWJ_CFG_JTAGDISABLE);
Man kann den SWO (=PB3) Ausgang des Mikrocontrollers dazu benutzen, Diagnose Meldungen auszugeben. Diese Schnittstelle ist effizienter als USART, weil ihre Bitrate höher ist (nämlich 1/4 der CPU Taktfrequenz) und weil sie über einen kleinen FIFO Puffer (10 Bytes) verfügt.
SWO wird mit dem ST-Link Adapter aktiviert und empfangen. Dann ist PB3 vorübergehend ein Ausgang mit Ruhe-Pegel High. Während der seriellen Datenübertragung liefert er Low-Impulse.
Die CMSIS Funktion ITM_SendChar() gibt ein Zeichen auf der SWO Leitung aus. ITM bedeutet "Instrumentation Trace Message":
#include <stdint.h> #include "stm32f1xx.h" // delay loop for default 8 MHz clock with optimizer enabled void delay(uint32_t msec) { for (uint32_t j=0; j < msec * 2000; j++) { __NOP(); } } // Output a trace message void ITM_SendString(char *ptr) { while (*ptr) { ITM_SendChar(*ptr); ptr++; } } int main() { while (1) { ITM_SendString("Hello World!\n"); delay(500); } }
Neben der SWJ Schnittstelle haben alle STM32 auch einen unveränderlichen Bootloader, über den man Programme hochladen kann. Er ermöglicht Zugriff auf den Flash Speicher, das RAM und die Option Bytes, zum Debuggen eignet er sich jedoch nicht.
Der Bootloader nutzt den Anschluss USART1. Die STM32F105 und 107 Modelle unterstützen auch USB und CAN.
Zur Konfiguration des Bootloaders dienen die beiden Pins Boot0 und Boot1:
Boot0 | Boot1 (=PB2) | Starte von |
---|---|---|
Low | egal | Flash ab Adresse 0x0800 0000, gemappt auf 0x0000 0000 |
High | Low | Bootloader |
High | High | RAM ab Adresse 0x2000 0000, nicht gemappt |
Boot0 und Boot1 werden beim Reset und beim Aufwachen aus dem Standby Modus gelesen. Um den Bootloader zu aktivieren, setzt man den Pin Boot0=High, Boot1=Low und dann drückt man den Reset Knopf.
Weitere Informationen zum Bootloader stehen in der Application Note AN2606.
Die Verbindung zum PC wird mit einem USB-UART Adapter wie diesem hergestellt:
Folgende Verbindungen sind nötig:
PC USB-UART | STM32F1 | Beschreibung | |
---|---|---|---|
TxD | → | RxD (=PA10) | Daten |
RxD | ← | TxD (=PA9) | Daten |
GND | GND | Gemeinsame Masse |
Als Taktquelle dient der interne R/C Oszillator, dessen Frequenz bei 3,3 V und Zimmertemperatur meistens ausreichend genau ist. Der Bootloader erkennt die Baudrate automatisch. Es werden 8 Datenbits und gerade Parität (even) verwendet.
Beim STM32F105 und 107 unterstützt der Bootloader auch USB. Dazu ist ein Quarz mit 8 MHz, 14.7456 MHz oder 25 MHz notwendig, und die Spannungsversorgung muss 3,3 V sein. Weitere Anwendungshinweise findest du in der Application Note AN2606.
Für Arduino gibt es für einige STM32F103 Boards den STM32duino Bootloader, der das Programmieren über USB ermöglicht. Der kann ohne Arduino aber nur mit zusätzlichem Aufwand verwendet werden, weil dann dein eigenes Programm nicht am Anfang des Flash Speichers beginnt, sondern hinter dem Bootloader.
Achtung: Keil MDK verwendet einen anderen Compiler, für den dieses Kapitel nicht passt.
Der arm-gcc Compiler bringt standardmäßig zwei C Bibliotheken mit: Newlib ist die Standard-Bibliothek von Linux, während die newlib-nano von ARM für deren 32 Bit Mikrocontroller optimiert wurde. Du kannst eine Menge Speicherplatz sparen, indem du auf die kleinere Version der Library wechselst. Dazu dient das Linker-Flag -specs=nano.specs.
Wenn das Programm die stdio.h Library benutzt, muss man entweder einige Funktionen zum Zugriff auf die Konsole implementieren oder mittels Linker-Flag -specs=nosys.specs auf Dummy Funktionen zurückgreifen.
Wenn man Fließkommazahlen ausgeben möchte, muss man bei der newlib-nano zusätzlich die Option -u _printf_float angeben. Das kostet zusätzlich rund 9 kB Flash Speicher. Für das Parsen von Fließkommazahlen benötigt man die Option -u _scanf_float.
Ich habe gemessen, wie viel Speicher die Funktionen puts() und printf() von der newlib-nano ohne Fließkomma-Unterstützung benötigen:
Funktion | Heap | Stack | Code (Flash) |
---|---|---|---|
puts() | 1468 | 16 | 2528 Bytes |
printf() | 1468 | 16 | 3880 Bytes |
Das ist erheblich mehr, als bei AVR Mikrocontrollern. 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.
Da Mikrocontroller im Gegensatz zu größeren Computern kein Standard Ausgabegerät (wie Bildschirm oder Terminal) haben, muss man die Funktion _write() selber implementieren. Erst danch funktionieren printf(), puts() und putchar():
int _write(int file, char *ptr, int len) { for (int i=0; i<len; i++) { char c=*ptr++; // hier das Zeichen c irgendwo ausgeben } return len; }
Weiter unten findest du konkrete Beispiele für Ausgaben über serielle Ports und USB. Bei der Ausgabe von Text mit printf() und putchar() ist zu beachten, dass die Zeichen in einem Puffer gesammelt werden, bis dieser entweder voll ist oder ein Zeilenvorschub (\n) erfolgt. Mit fflush(stdout) kann man erzwingen, dass die Ausgabe sofort erfolgt. puts() ist nicht betroffen, weil es immer einen Zeilenvorschub anhängt.
Wenn du sehen willst, welchen Assembler-Code der Compiler erzeugt, benutze die Compiler-Optionen
-g -Wa,-adhlns="$(@:%.o=%.lst)"
Du findest dann für jede Quell-Datei eine *.lst Datei im Verzeichnis Debug oder Release.
In den Einstellungen des Compilers kann man beeinflussen, mit welcher Strategie der Compiler das Programm optimiert (umstrukturiert).
Option | Beschreibung |
---|---|
-O0 | Keine Optimierung. Der Assembler Code entspricht genau dem C-Code. Aber das Programm läuft viel Langsamer, als mit Optimierung. |
-Og | Das Programm wird ein bisschen optimiert, ohne den Debugger zu beeinträchtigen. |
-O1 | Nur einfache Optimierungen, der Assembler-Code entspricht strukturell weitgehend dem Quelltext. |
-Os | Gute Geschwindigkeit bei möglichst geringer Code-Größe. |
-O2 | Gute Geschwindigkeit. |
-O3 | Beste Geschwindigkeit, unter Umständen wird der Code aber viel größer. |
Optimierter Code stimmt stellenweise nicht mehr mit dem C-Quelltext überein, was die Benutzung des Debuggers beeinträchtigt. Zum Beispiel kann der Debugger nur Variablen anzeigen, die eine Adresse im RAM haben, aber der optimierende Compiler bevorzugt CPU Register. Der Compiler ersetzt manchmal ganze Prozeduren durch inline Code, und Schleifen durch völlig anderen Code, der das gleiche Ergebnis produziert.
Damit der Debugger funktioniert, braucht man die Option -g. Sie veranlasst den Compiler dazu, Informationen für den Debugger in die *.elf Datei zu schreiben. Auf die Geschwindigkeit und Größe im Flash hat -g keinen Einfluss.
Ich bevorzuge -O1 -g, da der so erzeugte Code nur marginal langsamer ist, als in den höheren Stufen. Der Code lässt sich dennoch weitgehend debuggen und die Assembler Listings sind überschaubar. Weniger gut finde ich die Vorgabe der IDE, wo zwischen Debug- und Release-Modus unterschieden wird. Denn dann kann es passieren, dass ein Code beim Debugging prima läuft, und später in Produktion überraschende Fehler zeigt. Es geht dabei nicht nur darum dem Compiler zu vertrauen, sondern dass feine Unterschiede im Timing manchmal erhebliche Probleme (Race Conditions) auslösen, die man gerne möglichst früh bemerken will. Ich möchte nicht wochenlang eine Debug-Version des Programm testen, um am Ende eine andere Release-Version abzuliefern.
Die vollständige Liste der Optimierungen befindet sich hier.
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() { ... }
Bei großen C++ Projekten kann es sich lohnen, die Erhöhung der Taktfrequenz dort unterzubringen, denn dann startet das Programm schneller.
Obwohl der Prozessor in Harvard Architektur gestaltet ist, benutzt er einen gemeinsamen Adress-Raum für Programm, Daten und I/O Register. Dadurch können alle I/O Register über Zeiger angesprochen werden und der Prozessor kann Code sowohl aus dem RAM als auch aus dem (Flash-) ROM ausführen. Adressen und Zeiger sind 32bit groß.
Die Befehle sind teilweise 16bit und teilweise 32bit groß.
Daten werden als 8, 16 oder 32bit geladen. Sie müssen nicht zwingend an der 32bit Wortgröße ausgerichtet sein. Aber man erreicht bessere Geschwindigkeit, wenn 16bit Daten an 16bit Adressen und 32bit Daten an 32bit Adressen ausgerichtet sind. Der Compiler kümmert sich automatisch darum.
Die Register für I/O Funktionen sind fast alle 16bit breit, mache haben 32bit.
Schreibzugriffe auf die Register finden asynchron statt. So kann die CPU zum Beispiel z.B. ein langsames Register am APB1 Bus beschreiben und noch bevor dies fertig ist ein anderes Register am AHB oder APB2 Bus ansprechen. Mit dem Befehl __DSB() zwischen zwei Register-Zugriffen stellt man sicher, dass sie ohne Überlappung nacheinander stattfinden.
Im Gegensatz zum avr-gcc belegt der arm-gcc für konstante Zeichenketten kein RAM. Der Flash Speicher kann direkt wie RAM gelesen werden, allerdings mit maximal 24 MHz (höhere Taktfrequenzen erfordern wait states).
Bei Funktionsaufrufen werden bis zu 4 Parameter durch Register übergeben. Dabei ist es vorteilhaft, sie als 32bit Typ zu deklarieren, um Konvertierungen zu vermeiden. Bei mehr als 4 Parametern wird das RAM zur Übergabe benutzt, dann sind 8bit, 16bit und 32bit Typen gleich langsam.
Der Rückgabewert einer Funktion wird ebenfalls in einem Register übermittelt und sollte daher 32bit groß sein, falls Geschwindigkeit wichtig ist.
Der Stapel speichert ausschließlich 32bit Werte. Bei jedem PUSH wird der Stapelzeiger (SP) zuerst um 4 verringert und dann wird das Wort an diese Adresse abgelegt. Der Stapelzeiger zeigt also immer auf die zuletzt belegte Adresse im RAM.
Es gibt zwei Stapelzeiger MSP und PSP, zwischen denen man umschalten kann. Der Prozessor startet mit dem MSP, welcher durch das erste Wort in der Interrupt-Vektor Tabelle (an Adresse 0) initialisiert wird. Der alternative Stapelzeiger PSP wird von Betriebssystemen genutzt, um den Programmen separate Stapel zuzuweisen. Unter dem Namen SP spricht man immer den Stapelzeiger an, der durch das SPEL Bit im CONTROL Register ausgewählt wurde (0=MSP (default), 1=PSP).
Relevante CMSIS Funktionen:
Der Flash-Speicher beginnt immer mit der Exception- und Interrupt-Vektor Liste. Jeder Eintrag in der Liste ist eine 32bit Sprungadresse. Diese ist im Referenzhandbuch Kapitel "Interrupt and exception vectors" dokumentiert.
Der Quelltext dazu befindet sich in der Datei startup/startup_stm32.s. Dort findest du die vorgegebenen Namen der C-Funktionen. Die folgende Tabelle gilt für alle STM32F1 Modelle:
Address | ARM Exception Nr. | CMSIS Interrupt Nr. | ISR Handler Function | Description |
---|---|---|---|---|
ARM Processor Exceptions | ||||
0x0000 | Initial value for the stack pointer MSP. | |||
0x0004 | Reset_Handler | Initial value for the program counter, points to assembler startup code. | ||
0x0008 | 2 | -14 | NMI_Handler() | Non maskable interrupt. The RCC Clock Security System (CSS) is linked to the NMI vector. |
0x000C | 3 | -13 | HardFault_Handler() | Hardware fault. Can optionally be splitted in the next 3 entries: |
0x0010 | 4 | -12 | MemManage_Handler() | Memory protection fault |
0x0014 | 5 | -11 | BusFault_Handler() | Pre-fetch or memory access fault |
0x0018 | 6 | -10 | UsageFault_Handler() | Undefined instruction, illegal unaligned access, invalid state, division by zero |
0x001C | 7 | -9 | reserved | |
0x0020 | 8 | -8 | ||
0x0024 | 9 | -7 | ||
0x0028 | 10 | -6 | ||
0x002C | 11 | -5 | SVC_Handler() | Supervisor Call, triggered by the SVC command (formerly known as SVI). Can be used to call operating system services. |
0x0030 | 12 | -4 | reserved | |
0x0034 | 13 | -3 | ||
0x0038 | 14 | -2 | PendSV_Handler() | Pendable Request for System Service. Triggered by the operating system by writing to the ICSR register, to switch the context. |
0x003C | 15 | -1 | SysTick_Handler() | Called when the systick timer reaches 0 |
STM32 Hardware Interrupts | ||||
0x0040 | 0 | WWDG_IRQHandler() | Window Watchdog | |
0x0044 | 1 | PVD_IRQHandler() | PVD through EXTI Line detection | |
0x0048 | 2 | TAMPER_IRQHandler() | Tamper | |
0x004C | 3 | RTC_IRQHandler() | RTC | |
0x0050 | 4 | FLASH_IRQHandler() | Flash | |
0x0054 | 5 | RCC_IRQHandler() | RCC | |
0x0058 | 6 | EXTI0_IRQHandler() | EXTI Line0 | |
0x005C | 7 | EXTI1_IRQHandler() | EXTI Line1 | |
0x0060 | 8 | EXTI2_IRQHandler() | EXTI Line2 | |
0x0064 | 9 | EXTI3_IRQHandler() | EXTI Line3 | |
0x0068 | 10 | EXTI4_IRQHandler() | EXTI Line4 | |
0x006C | 11 | DMA1_Channel1_IRQHandler() | DMA1 Channel1 | |
0x0070 | 12 | DMA1_Channel2_IRQHandler() | DMA1 Channel2 | |
0x0074 | 13 | DMA1_Channel3_IRQHandler() | DMA1 Channel3 | |
0x0078 | 14 | DMA1_Channel4_IRQHandler() | DMA1 Channel4 | |
0x007C | 15 | DMA1_Channel5_IRQHandler() | DMA1 Channel5 | |
0x0080 | 16 | DMA1_Channel6_IRQHandler() | DMA1 Channel6 | |
0x0084 | 17 | DMA1_Channel7_IRQHandler() | DMA1 Channel7 | |
0x0088 | 18 | ADC1_2_IRQHandler() | ADC1 and ADC2 | |
0x008C | 19 | USB_HP_CAN_TX_IRQHandler() | USB High Priority or CAN TX | |
0x0090 | 20 | USB_LP_CAN_RX0_IRQHandler() | USB Low Priority or CAN RX | |
0x0094 | 21 | CAN_RX1_IRQHandler() | CAN1 RX1 | |
0x0098 | 22 | CAN_SCE_IRQHandler() | CAN1 SCE | |
0x009C | 23 | EXTI9_5_IRQHandler() | EXTI Line[9:5] | |
0x00A0 | 24 | TIM1_BRK_IRQHandler() | TIM1 Break | |
0x00A4 | 25 | TIM1_UP_IRQHandler() | TIM1 Update | |
0x00A8 | 26 | TIM1_TRG_COM_IRQHandler() | TIM1 Trigger and Commutation | |
0x00AC | 27 | TIM1_CC_IRQHandler() | TIM1 Capture Compare | |
0x00B0 | 28 | TIM2_IRQHandler() | TIM2 | |
0x00B4 | 29 | TIM3_IRQHandler() | TIM3 | |
0x00B8 | 30 | TIM4_IRQHandler() | TIM4 | |
0x00BC | 31 | I2C1_EV_IRQHandler() | I²C1 event | |
0x00C0 | 32 | I2C1_ER_IRQHandler() | I²C1 error | |
0x00C4 | 33 | I2C2_EV_IRQHandler() | I²C2 event | |
0x00C8 | 34 | I2C2_ER_IRQHandler() | I²C2 error | |
0x00CC | 35 | SPI1_IRQHandler() | SPI1 | |
0x00D0 | 36 | SPI2_IRQHandler() | SPI2 | |
0x00D4 | 37 | USART1_IRQHandler() | USART1 | |
0x00D8 | 38 | USART2_IRQHandler() | USART2 | |
0x00DC | 39 | USART3_IRQHandler() | USART3 | |
0x00E0 | 40 | EXTI15_10_IRQHandler() | EXTI Line[15:10] | |
0x00E4 | 41 | RTCAlarm_IRQHandler() | RTC alarm through EXTI line | |
0x00E8 | 42 | USBWakeup_IRQHandler() | USB wakeup from suspend through EXTI line | |
0x00EC | 43 | TIM8_BRK_IRQHandler() | Timer 8 Break | |
0x00F0 | 44 | TIM8_UP_IRQHandler() | Timer 8 Update | |
0x00F4 | 45 | TIM8_TRG_COM_IRQHandler() | Timer 8 Trigger and Commutation | |
0x00F8 | 46 | TIM8_CC_IRQHandler() | Timer 8 Capture Compare | |
0x00FC | 47 | ADC3_IRQHandler() | ADC3 | |
0x0100 | 48 | FSMC_IRQHandler() | FSMC | |
0x0104 | 49 | SDIO_IRQHandler() | SDIO | |
0x0108 | 50 | TIM5_IRQHandler() | TIM5 | |
0x010C | 51 | SPI3_IRQHandler() | SPI3 | |
0x0110 | 52 | UART4_IRQHandler() | UART4 | |
0x0114 | 53 | UART5_IRQHandler() | UART5 | |
0x0118 | 54 | TIM6_IRQHandler() | TIM6 | |
0x011C | 55 | TIM6_IRQHandler() | TIM7 | |
0x0120 | 56 | DMA2_Channel1_IRQHandler() | DMA2 Channel1 | |
0x0124 | 57 | DMA2_Channel2_IRQHandler() | DMA2 Channel2 | |
0x0128 | 58 | DMA2_Channel3_IRQHandler() | DMA2 Channel3 | |
0x012C | 59 | DMA2_Channel4_5_IRQHandler() or DMA2_Channel4_IRQHandler() | DMA2 Channel4 (and Channel 5) | |
0x0130 | 60 | DMA2_Channel5_IRQHandler() | DMA2 Channel5 | |
0x0134 | 61 | ETH_IRQHandler() | Ethernet | |
0x0138 | 62 | ETH_WKUP_IRQHandler() | Ethernet Wakeup through EXTI line | |
0x013C | 63 | CAN2_TX_IRQHandler() | CAN2 TX | |
0x0140 | 64 | CAN2_RX0_IRQHandler() | CAN2 RX0 | |
0x0144 | 65 | CAN2_RX1_IRQHandler() | CAN2 RX1 | |
0x0148 | 66 | CAN2_SCE_IRQHandler() | CAN2 SCE | |
0x014C | 67 | OTG_FS_IRQHandler() | USB On The Go FS |
Über das SCB->VTOR Register kann man den Ort der Liste verändern um sie z.B. ins RAM zu verschieben.
Der Interrupt-Controller NVIC steuert die Verarbeitung von Unterbrechungs-Signalen. Er ist Bestandteil des ARM Kerns.
Solche Signale werden von interner oder externer Hardware ausgelöst, wenn bestimmte Ereignisse auftreten. Sie können dazu genutzt werden, das laufende Programm vorübergehend zu unterbrechen und stattdessen eine besondere Funktion auszuführen, die Interrupt-Handler oder Interrupt-Service-Routine (ISR) genannt wird.
Reset, NMI und HardFault haben immer die höchste Priorität, bei allen anderen kann man die Priorität einstellen. Interrupt-Handler können durch höher priorisierte Interrupts unterbrochen werden.
Die wichtigsten CMSIS Funktionen zur Konfiguration des Interrupt-Systems sind:
Um Unterbrechungen temporär zu verbieten (zum Beispiel für exklusiven Zugriff auf Daten oder Schnittstellen), wird folgende Vorgehensweise empfohlen:
uint32_t backup = __get_PRIMASK(); __set_PRIMASK(1); ... do some work ... __set_PRIMASK(backup);
Normale Unterbrechungen sind durch Pegel gesteuert. Wenn das Signal auf High steht, wird der zugeordnete Handler möglichst bald ausgeführt. Während der Interrupt-Handler läuft, muss das Signal wieder auf Low zurück zurück gehen, sonst wird der Interrupt-Handler nach seinem Ende gleich wieder aufgerufen.
Wenn das Signal zu früh verschwindet, während der Interrupt-Controller nach einer Gelegenheit sucht, den Interrupt-Handler auszuführen, geht es verloren. Der Interrupt-Handler wird dann nicht ausgeführt.
Die externen Interrupt-Signale und auch einige interne durchlaufen einen erweiterten Schaltkreis, der zusätzliche Konfiguration erfordert. Man erkennt sie am Stichwort EXTI.
Die Kanäle EXTI0 bis EXT15 sind für I/O-Pins reserviert. Jeden Kanal kann man in den Registern AFIO->EXTICR[0-3] genau einem Port zuweisen. Wenn man zum Beispiel Kanal 0 dem Port A zuweist (also PA0), kann man auf den anderen Ports das Bit 0 nicht mehr für Interrupts verwenden. Diese Einschränkung gilt für alle 16 Kanäle.
Viele erweiterte Unterbrechungen werden durch Flanken (Signalwechsel) ausgelöst. Man erkennt sie an den Bits im Register EXTI->RTSR oder EXTI->FTSR.
Durch die Verwendung von Flanken ist automatisch sicher gestellt, dass der Handler nicht immer wieder erneut ausgeführt wird, falls ein Interrupt Signal für längere Zeit ansteht.
Flanken gesteuerte Interrupts gehen nicht verloren, wenn das Signal schon wieder verschwindet bevor der Handler aufgerufen wurde. Der Interrupt-Controller merkt sich, dass da mal eine Anforderung vorlag, die noch nicht abgearbeitet wurde.
In diesem Zusammenhang ist das Register EXTI->PR wichtig. Dort merkt sich der Interrupt-Controller den Zustand. Während der Initialisierung muss man das Bit zurück setzen, um sicher zu stellen, dass die erste Flanke zuverlässig erkannt wird. Man schreibt eine 1 in das jeweilige Bit, damit es auf 0 zurück gesetzt wird. Innerhalb der ISR muss man das Flag ebenfalls zurück setzen, am Besten ganz am Anfang. Am Ende der ISR wäre zu spät, da dieses Signal etwas verzögert verarbeitet wird.
Im Register EXTI->IMR werden Unterbrechungen maskiert. Man muss hier eine 1 in das jeweilige Bit schreiben, damit eine Interrupt-Leitung wirksam wird. Standardmäßig sind die meisten Interrupts maskiert, sind also ohne Wirkung.
Das folgende Beispiel löst einen Interrupt aus, wenn das Signal an PC13 von Low nach High wechselt. Beim Nucleo-Board ist PC13 mit dem blauen Taster verbunden.
#include <stdio.h> #include "stm32f1xx.h" // Output a trace message void ITM_SendString(char *ptr) { while (*ptr) { ITM_SendChar(*ptr); ptr++; } } void EXTI15_10_IRQHandler() { // Clear pending interrupt flag // It is important that this is not the last command in the ISR SET_BIT(EXTI->PR, EXTI_PR_PR13); // Output a trace message ITM_SendString("irq\n"); } int main() { // Enable clock for Port C and alternate functions SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPCEN + RCC_APB2ENR_AFIOEN); // Assign EXTI13 to PC13 with rising edge MODIFY_REG(AFIO->EXTICR[3], AFIO_EXTICR4_EXTI13, AFIO_EXTICR4_EXTI13_PC); SET_BIT(EXTI->IMR, EXTI_IMR_MR13); SET_BIT(EXTI->RTSR, EXTI_RTSR_TR13); // Enable the interrupt handler call NVIC_EnableIRQ(EXTI15_10_IRQn); // Clear pending interrupt flag SET_BIT(EXTI->PR, EXTI_PR_PR13); // Endless loop while (1) {} }
Neben den Unterbrechungen steuert der NVIC auch Ereignisse. Ereignisse verwendet man üblicherweise, um die CPU aus aus einem Wait- oder Sleep- Zustand aufzuwecken. So hält zum Beispiel der Befehl __WFE() die CPU bis zum nächsten Ereignis an. Siehe Absatz Powermanagement.
Standardmäßig sind alle Ereignisse maskiert. Man muss im Register EXTI->EMR das entsprechende Bit auf 1 setzen, um ein EXTI Signal als Ereignis zu behandeln.
Ich habe ziemlich oft gelesen, dass das komplexe System zur Takterzeugung für Anfänger ein großes Hindernis sei. Das sehe ich anders, denn nach einem Reset wird der Mikrocontroller automatisch mit seinem internen 8 MHz R/C Oszillator getaktet. Damit kann man schon eine Menge sinnvoller Programme schreiben.
Die Taktsignale für den ARM Kern (dazu gehört auch der SysTick Timer), sowie RAM und Flash sind automatisch aktiviert. Alle anderen Komponenten muss man ggf. selbst einschalten, was ganz einfach durch Setzen von Bits in den Registern RCC->APB1ENR, RCC->APB2ENR und RCC->AHBENR erledigt wird.
Jetzt kommt der komplizierte Teil. Der System-Takt (SYSCLK) kann aus folgenden Quellen bezogen werden:
Für Watchog und Echtzeit-Uhr (RTC) sind weitere Quellen vorgesehen:
Mehrere Vorteiler und ein PLL Multiplikator können kombiniert werden, um unterschiedliche Taktfrequenzen zu erreichen. Am besten schaut man sich das mal in Cube MX 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:
Beispiel für die maximalen möglichen 64 MHz mit dem internen HSI Oszillator (gilt nicht für STM32F105, STM32F107):
// The current clock frequency uint32_t SystemCoreClock=8000000; // Change system clock to 64 MHz using internal 8 MHz R/C Oscillator void init_clock() { // Because the debugger switches PLL on, we may need to switch // back to the HSI oscillator before we can configure the PLL // Enable HSI oscillator SET_BIT(RCC->CR, RCC_CR_HSION); // Wait until HSI oscillator is ready while(!READ_BIT(RCC->CR, RCC_CR_HSIRDY)) {} // Switch to HSI oscillator MODIFY_REG(RCC->CFGR, RCC_CFGR_SW, RCC_CFGR_SW_HSI); // Wait until the switch is done while ((RCC->CFGR & RCC_CFGR_SWS_Msk) != RCC_CFGR_SWS_HSI) {} // Disable the PLL CLEAR_BIT(RCC->CR, RCC_CR_PLLON); // Wait until the PLL is fully stopped while(READ_BIT(RCC->CR, RCC_CR_PLLRDY)) {} // Flash latency 2 wait states MODIFY_REG(FLASH->ACR, FLASH_ACR_LATENCY, 2 << FLASH_ACR_LATENCY_Pos); // 64 MHz using the 8 MHz/2 HSI oscillator with 16x PLL, lowspeed I/O runs at 32 MHz WRITE_REG(RCC->CFGR, RCC_CFGR_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, 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 aufeinander folgende Befehle optimiert. Bei jeden Rücksprung in der Schleife wird der Prefetch-Buffer geleert.
Durch die Angabe __attribute__((section(".data"))) könnte man Funktionen etwas schneller ohne Waitstate aus dem RAM ausführen, da dieser ohne Waitstates arbeitet.
Beispiel für 72 MHz mit einem 8 MHz Quarz (HSE Oszillator):
// The current clock frequency uint32_t SystemCoreClock=8000000; // Change system clock to 72 MHz using 8 MHz crystal void init_clock() { // Because the debugger switches PLL on, we may need to switch // back to the HSI oscillator before we can configure the PLL // Enable HSI oscillator SET_BIT(RCC->CR, RCC_CR_HSION); // Wait until HSI oscillator is ready while(!READ_BIT(RCC->CR, RCC_CR_HSIRDY)) {} // Switch to HSI oscillator MODIFY_REG(RCC->CFGR, RCC_CFGR_SW, RCC_CFGR_SW_HSI); // Wait until the switch is done while ((RCC->CFGR & RCC_CFGR_SWS_Msk) != RCC_CFGR_SWS_HSI) {} // Disable the PLL CLEAR_BIT(RCC->CR, RCC_CR_PLLON); // Wait until the PLL is fully stopped while(READ_BIT(RCC->CR, RCC_CR_PLLRDY)) {} // Flash latency 2 wait states MODIFY_REG(FLASH->ACR, FLASH_ACR_LATENCY, 2 << FLASH_ACR_LATENCY_Pos); // Enable HSE oscillator SET_BIT(RCC->CR, RCC_CR_HSEON); // Wait until HSE oscillator is ready while(!READ_BIT(RCC->CR, RCC_CR_HSERDY)) {} // 72 MHz using the 8 MHz HSE oscillator with 9x PLL, lowspeed I/O runs at 36 MHz WRITE_REG(RCC->CFGR, RCC_CFGR_PLLSRC + RCC_CFGR_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; // Disable the HSI oscillator CLEAR_BIT(RCC->CR, RCC_CR_HSION); }
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() { systick_count++; } // Delay some milliseconds. // Note that effective time may be up to 1ms shorter than requested. void delay_ms(int ms) { uint32_t start=systick_count; while (systick_count-start<ms); } int main() { // Initialize the timer for 1 ms intervals SysTick_Config(SystemCoreClock/1000); // Delay 2 seconds delay_ms(2000); ... }
Wenn der Prozessor beim Debuggen angehalten wird, hält auch dieser Timer an. Im WFI und WFE Sleep Modus läuft der SysTick Timer weiter.
Generell könne alle I/O Pins können erst benutzt werden, nachdem man ihre Taktversorgung im Register RCC->APB2ENR eingeschaltet hat. Dort muss man auch das AFIOEN Bit einschalten, es sei denn man benutzt gar keine alternative Funktion.
Standardmäßig sind fast alle I/O Pins als digitaler Eingang konfiguriert. Um deren Status abzufragen, liest man das jeweilige GPIOx->IDR Register.
Im Register GPIOx->CRL (für Pin 0-7) oder GPIOx->CRH (für Pin 8-15) konfiguriert man einen Pin als Ausgang oder für alternativen Funktionen (wie z.B. serieller Port oder PWM Timer).
Für jeden Ausgang kann man dort 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 elektromagnetischer 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.
Die Taktfrequenz des Systems wird standardmäßig durch 2 geteilt um den ADC zu betreiben. Er funktioniert mit maximal 14 MHz, daher ist es notwendig den Vorteiler im Register RCC->CFGR zu ändern, wenn der Systemtakt über 28 MHz 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:
// Configure PA1 as analog input ADC12_IN1 MODIFY_REG(GPIOA->CRL, GPIO_CRL_CNF1, 0b00 << GPIO_CRL_CNF1_Pos); MODIFY_REG(GPIOA->CRL, GPIO_CRL_MODE1, 0b00 << GPIO_CRL_MODE1_Pos);
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_DIV8); // Enable clock 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, 0b111 << ADC_CR2_EXTSEL_Pos); // Set sample time to 41.5 cycles MODIFY_REG(ADC1->SMPR2, ADC_SMPR2_SMP0, 0b100 << ADC_SMPR2_SMP0_Pos); // Delay 20 ms delay_ms(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) { // Number of channels to convert: 1 MODIFY_REG(ADC1->SQR1, ADC_SQR1_L, 0 << ADC_SQR1_L_Pos); // does one conversion more than configured here // 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.
Die Timer 1 bis 8 können jeweils 4 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.
Die Timer 9 bis 14 haben jeweils nur 2 PWM Kanäle.
Die Taktfrequenz der Timer wird vom Systemtakt abgeleitet und kann durch den ABP2 Prescaler (im Register RCC->CFGR) und den Timer Prescaler TIMx->PSC reduziert werden.
Der Timer zählt fortlaufend von 0 an hoch bis zum Maximum, welches durch das TIMx->ARR Register festgelegt wird. Wenn der Maximalwert als 50000 festgelegt wird, können die Ausgangsimpulse wahlweise 1 bis 50000 Takte breit sein.
Für jeden Ausgang gibt es ein Vergleichs-Register TIMx->CCRy welches bestimmt, wie breit die Ausgangsimpulse sein sollen. Beim Extremwert 0 ist der Ausgang ständig Low. Bei Werten größer dem Maximum (in TIMx->ARR) ist der Ausgang ständig High. Mit der option "inverse polarity" im Register TIMx->CCER können die Ausgänge umgepolt werden, so dass sie Low-Impule liefern.
Das folgende Beispielprogramm benutzt 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 + RCC_APB2ENR_IOPBEN + RCC_APB2ENR_AFIOEN); // PA6 = Timer 3 channel 1 alternate function output MODIFY_REG(GPIOA->CRL, GPIO_CRL_CNF6, 0b10 << GPIO_CRL_CNF6_Pos); MODIFY_REG(GPIOA->CRL, GPIO_CRL_MODE6, 0b01 << GPIO_CRL_MODE6_Pos); // PA7 = Timer 3 channel 2 alternate function output MODIFY_REG(GPIOA->CRL, GPIO_CRL_CNF7, 0b10 << GPIO_CRL_CNF7_Pos); MODIFY_REG(GPIOA->CRL, GPIO_CRL_MODE7, 0b01 << GPIO_CRL_MODE7_Pos); // PB0 = Timer 3 channel 3 alternate function output MODIFY_REG(GPIOB->CRL, GPIO_CRL_CNF0, 0b10 << GPIO_CRL_CNF0_Pos); MODIFY_REG(GPIOB->CRL, GPIO_CRL_MODE0, 0b01 << GPIO_CRL_MODE0_Pos); // PB1 = Timer 3 channel 4 alternate function output MODIFY_REG(GPIOB->CRL, GPIO_CRL_CNF1, 0b10 << GPIO_CRL_CNF1_Pos); MODIFY_REG(GPIOB->CRL, GPIO_CRL_MODE1, 0b01 << GPIO_CRL_MODE1_Pos); } 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, 0b110 << TIM_CCMR1_OC1M_Pos); SET_BIT(TIM3->CCMR1, TIM_CCMR1_OC1PE); // Timer 3 channel 2 compare mode=PWM1 with the required preload buffer enabled MODIFY_REG(TIM3->CCMR1, TIM_CCMR1_OC2M, 0b110 << TIM_CCMR1_OC2M_2_Pos); SET_BIT(TIM3->CCMR1, TIM_CCMR1_OC2PE); // Timer 3 channel 3 compare mode = PWM1 with the required preload buffer enabled MODIFY_REG(TIM3->CCMR2, TIM_CCMR2_OC3M, 0b110 << TTIM_CCMR2_OC3M_Pos); SET_BIT(TIM3->CCMR2, TIM_CCMR1_OC1PE); // Timer 3 channel 4 compare mode = PWM1 with the required preload buffer enabled MODIFY_REG(TIM3->CCMR2, TIM_CCMR2_OC4M, 0b110 << TTIM_CCMR2_OC4M_2_Pos); SET_BIT(TIM3->CCMR2, 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; // 8000000/50000 = 160 pulses per second // Timer 3 clock prescaler, the APB2 clock is divided by this value +1. TIM3->PSC = 0; // divide clock by 1 // Timer 3 enable counter and auto-preload SET_BIT(TIM3->CR1, TIM_CR1_CEN + TIM_CR1_ARPE); } int main() { 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 ist. 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. Mehr Informationen dazu gibt es zum Beispiel in der Application Note AN4776.
Der interne HSI Oszillator ist häufig aber nicht immer stabil genug, um die USART Schnittstellen zu betreiben. Es empfiehlt sich daher, auf eine externe Quelle (HSE) umzuschalten. In dem folgenden Beispiel nutze ich der Einfachheit halber trotzdem den internen HSI Oszillator.
Je nach Taktfrequenz der Peripherie sind unterschiedliche Baudraten möglich:
Beim Nucleo-64 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 CN3 | STM32F1 USART2 | Beschreibung | |
---|---|---|---|
TxD | → | RxD (=PA3) | Daten |
RxD | ← | TxD (=PA2) | Daten |
GND | GND | Gemeinsame Masse |
Der ST-Link v2.1 unterstützt 600 bis 2000000 Baud.
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() { char received=USART2->DR; // send echo back while(!(USART2->SR & USART_SR_TXE)); USART2->DR = received; } int main() { // Enable clock for Port A, alternate functions and USART2 SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN + RCC_APB2ENR_AFIOEN); SET_BIT(RCC->APB1ENR, RCC_APB1ENR_USART2EN); // PA2 (TxD) shall use the alternate function with push-pull MODIFY_REG(GPIOA->CRL, GPIO_CRL_CNF2, 0b10 << GPIO_CRL_CNF2_Pos); MODIFY_REG(GPIOA->CRL, GPIO_CRL_MODE2, 0b10 << GPIO_CRL_MODE2_Pos); // Enable transmitter, receiver and receive-interrupt of USART2 USART2->CR1 = USART_CR1_UE + USART_CR1_TE + USART_CR1_RE + USART_CR1_RXNEIE; // Set baudrate, assuming that USART2 is clocked with // the same frequency as the CPU core (no prescalers). USART2->BRR = (SystemCoreClock / 9600); // With > 36 MHz system clock, the USART2 receives usually half of it: // USART2->BRR = (SystemCoreClock / 2 / 9600); // Enable interrupt in NVIC NVIC_EnableIRQ(USART2_IRQn); printf("Hello World!\n"); while (1) {}; }
Der I²C Bus ist eine beliebte Schnittstelle für die Anbindung von Peripherie über kurze Leitungen, wie Port-Erweiterungen, Sensoren und Batterie-Management. Siehe Spezifikation von Philips/NXP.
Der Bus besteht aus den beiden Leitungen SDA und SCL. An beide Leitungen gehört jeweils ein Pull-Up Widerstand, typischerweise mit 4,7 kΩ bei 5 V oder 2,7 kΩ bei 3,3 V. Die STM32F1 Mikrocontroller haben zwei I²C Busse, beide unterstützen 3,3 V und 5 V Pegel, aber nur wenige Slaves sind so flexibel.
Signal | I2C1 normal | I2C1 remapped | I2C2 |
---|---|---|---|
SCL Takt | PB6 | PB8 | PB10 |
SDA Daten | PB7 | PB9 | PB11 |
Zum Remappen kann man das Bit I2C1_REMAP im Register AFIO->MAPR setzen. Nach der Initialisierung der I²C Schnittstelle (nicht vorher!) muss man die betroffen I/O Pins im Register GPIOB->CRL bzw. GPIOB->CRH als "Alternate function output open-drain 2 MHz" konfigurieren.
Normalerweise hat man einen zentralen Master, der viele Slaves ansteuert. Jeder Slave hat eine eigene eindeutige 7bit Adresse. Innerhalb einer Transaktion kann der Master 0 oder mehr Bytes an den Slave senden und danach 0 oder mehr Bytes vom Slave empfangen. Der folgende Code kann dazu für den Master verwendet werden:
#include <stdint.h> #include <stdbool.h> #include "stm32f1xx.h" /** * Initialize the I²C interface. * * @param registerStruct May be either I2C1 (SCL=PB6, SDA=PB7) or I2C2 (SCL=PB10, SDA=PB11) * @param remap Whether to remap I2C1 to the alternative pins (SCL=PB8, SDA=PB9). * @param fastMode false=100 kHz, true=400 kHz * @param apb1_clock clock frequency of APB1 peripherals */ void i2c_init(I2C_TypeDef* registerStruct, bool remap, bool fastMode, uint32_t apb1_clock) { // Enable clock for Port B and alternate functions SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPBEN + RCC_APB2ENR_AFIOEN); // Enable clock for the I2C interface if (registerStruct==I2C1) { SET_BIT(RCC->APB1ENR, RCC_APB1ENR_I2C1EN); } else if (registerStruct==I2C2) { SET_BIT(RCC->APB1ENR, RCC_APB1ENR_I2C2EN); } // Disable the peripheral CLEAR_BIT(registerStruct->CR1, I2C_CR1_PE); // Configure timing MODIFY_REG(registerStruct->CR2, I2C_CR2_FREQ, apb1_clock/1000000); if (fastMode) { MODIFY_REG(registerStruct->CCR, I2C_CCR_CCR, apb1_clock/800000); MODIFY_REG(registerStruct->TRISE, I2C_TRISE_TRISE, apb1_clock/4000000+1); } else { MODIFY_REG(registerStruct->CCR, I2C_CCR_CCR, apb1_clock/200000); MODIFY_REG(registerStruct->TRISE, I2C_TRISE_TRISE, apb1_clock/1000000+1); } // Enable the peripheral SET_BIT(registerStruct->CR1, I2C_CR1_PE); // Configure the I/O pins for alternate function open-drain 2 MHz if (registerStruct==I2C1) { if (remap) { // PB8=SCL, PB9=SDA SET_BIT(AFIO->MAPR, AFIO_MAPR_I2C1_REMAP); MODIFY_REG(GPIOB->CRH, GPIO_CRH_CNF8, 0b11 << GPIO_CRH_CNF8_Pos); MODIFY_REG(GPIOB->CRH, GPIO_CRH_MODE8, 0b10 << GPIO_CRH_MODE8_Pos); MODIFY_REG(GPIOB->CRH, GPIO_CRH_CNF9, 0b11 << GPIO_CRH_CNF9_Pos); MODIFY_REG(GPIOB->CRH, GPIO_CRH_MODE9, 0b10 << GPIO_CRH_MODE9_Pos); } else { // PB6=SCL, PB7=SDA CLEAR_BIT(AFIO->MAPR, AFIO_MAPR_I2C1_REMAP); MODIFY_REG(GPIOB->CRL, GPIO_CRL_CNF6, 0b11 << GPIO_CRL_CNF6_Pos); MODIFY_REG(GPIOB->CRL, GPIO_CRL_MODE6, 0b10 << GPIO_CRL_MODE6_Pos); MODIFY_REG(GPIOB->CRL, GPIO_CRL_CNF7, 0b11 << GPIO_CRL_CNF7_Pos); MODIFY_REG(GPIOB->CRL, GPIO_CRL_MODE7, 0b10 << GPIO_CRL_MODE7_Pos); } } else if (registerStruct==I2C2) { // PB10=SCL, PB11=SDA MODIFY_REG(GPIOB->CRH, GPIO_CRH_CNF10, 0b11 << GPIO_CRH_CNF10_Pos); MODIFY_REG(GPIOB->CRH, GPIO_CRH_MODE10, 0b10 << GPIO_CRH_MODE10_Pos); MODIFY_REG(GPIOB->CRH, GPIO_CRH_CNF11, 0b11 << GPIO_CRH_CNF11_Pos); MODIFY_REG(GPIOB->CRH, GPIO_CRH_MODE11, 0b10 << GPIO_CRH_MODE11_Pos); } } /** * Perform an I²C transaction, which sends 0 or more data bytes, followed by receiving 0 or more data bytes. * * @param registerStruct May be either I2C1 or I2C2 * @param slave_addr 7bit slave address (will be shifted within this function) * @param send_buffer Points to the buffer that contains the data bytes that shall be sent (may be 0 if not used) * @param send_size Number of bytes to send * @param receive_buffer Points to the buffer that will be filled with the received bytes (may be 0 if not used) * @param receive_size Number of bytes to receive * @return Number of received data bytes, or -1 if sending failed */ int i2c_communicate(I2C_TypeDef* registerStruct, uint8_t slave_addr, void* send_buffer, int send_size, void* receive_buffer, int receive_size) { // Quick return if nothing to do if (send_size==0 && receive_size==0) { return 0; } int receive_count=-1; // shift the 7bit address to the right position slave_addr=slave_addr << 1; // Send data if (send_size>0) { // Send START and slave address SET_BIT(registerStruct->CR1, I2C_CR1_START); // send START condition while (!READ_BIT(registerStruct->SR1, I2C_SR1_SB)); // wait until START has been generated WRITE_REG(registerStruct->DR,slave_addr); // send slave address while (!READ_BIT(registerStruct->SR1, I2C_SR1_ADDR)) // wait until address has been sent { if (READ_BIT(registerStruct->SR1, I2C_SR1_AF)) { // did not receive ACK after address goto error; } } READ_REG(registerStruct->SR2); // clear ADDR while (send_size>0) { WRITE_REG(registerStruct->DR,*((uint8_t*)send_buffer)); // send 1 byte from buffer while (!READ_BIT(registerStruct->SR1, I2C_SR1_TXE)) // wait until Tx register is empty { if (READ_BIT(registerStruct->SR1, I2C_SR1_AF)) { // did not receive ACK after data byte goto error; } } send_buffer++; send_size--; } while (!READ_BIT(registerStruct->SR1, I2C_SR1_BTF)) // wait until last byte transfer has finished { if (READ_BIT(registerStruct->SR1, I2C_SR1_AF)) { // did not receive ACK after data byte goto error; } } } // Sending succeeded, start counting the received bytes receive_count=0; CLEAR_BIT(registerStruct->CR1, I2C_CR1_POS); // POS=0 SET_BIT(registerStruct->CR1, I2C_CR1_ACK); // acknowledge each byte // Receive data // The procedure includes workaround as described in AN2824 if (receive_size>0) { // Send (RE-)START and slave address SET_BIT(registerStruct->CR1, I2C_CR1_START); // send START condition while (!READ_BIT(registerStruct->SR1, I2C_SR1_SB)); // wait until START has been generated WRITE_REG(registerStruct->DR,slave_addr+1); // send slave address + read mode while (!READ_BIT(registerStruct->SR1, I2C_SR1_ADDR)) // wait until address has been sent { if (READ_BIT(registerStruct->SR1, I2C_SR1_AF)) { // did not receive ACK after address goto error; } } if (receive_size>2) { READ_REG(registerStruct->SR2); // clear ADDR while (receive_size>3) { while (!READ_BIT(registerStruct->SR1, I2C_SR1_RXNE)); // wait until a data byte has been received *((uint8_t*)receive_buffer)=READ_REG(registerStruct->DR); // read data receive_size--; receive_count++; receive_buffer++; } while (!READ_BIT(registerStruct->SR1, I2C_SR1_BTF)); // wait until 2 bytes are received CLEAR_BIT(registerStruct->CR1, I2C_CR1_ACK); // prepare to send a NACK *((uint8_t*)receive_buffer)=READ_REG(registerStruct->DR); // read the penultimate data byte receive_size--; receive_count++; receive_buffer++; __disable_irq(); { SET_BIT(registerStruct->CR1, I2C_CR1_STOP); // prepare to send a STOP condition *((uint8_t*)receive_buffer)=READ_REG(registerStruct->DR); // read the last data byte receive_size--; receive_count++; receive_buffer++; } __enable_irq(); } else if (receive_size==2) { SET_BIT(registerStruct->CR1, I2C_CR1_POS); // NACK shall be applied to the next // byte, not the current byte __disable_irq(); { READ_REG(registerStruct->SR2); // clear ADDR CLEAR_BIT(registerStruct->CR1, I2C_CR1_ACK); // prepare to send a NACK } __enable_irq(); while (!READ_BIT(registerStruct->SR1, I2C_SR1_BTF)); // wait until 2 bytes are received __disable_irq(); { SET_BIT(registerStruct->CR1, I2C_CR1_STOP); // prepare to send a STOP condition *((uint8_t*)receive_buffer)=READ_REG(registerStruct->DR); // read the penultimate data byte receive_size--; receive_count++; receive_buffer++; } __enable_irq(); } else if (receive_size==1) { CLEAR_BIT(registerStruct->CR1, I2C_CR1_ACK); // prepare to send a NACK __disable_irq(); { READ_REG(registerStruct->SR2); // clear ADDR SET_BIT(registerStruct->CR1, I2C_CR1_STOP); // prepare to send a STOP condition } __enable_irq(); while (!READ_BIT(registerStruct->SR1, I2C_SR1_RXNE)); // wait until a data byte has been received } *((uint8_t*)receive_buffer)=READ_REG(registerStruct->DR); // read the last data byte receive_size--; receive_count++; receive_buffer++; while (READ_BIT(registerStruct->SR1, I2C_CR1_STOP)); // wait until STOP has been generated } else if (receive_size==0) { SET_BIT(registerStruct->CR1, I2C_CR1_STOP); // send STOP condition while (READ_BIT(registerStruct->CR1, I2C_CR1_STOP)); // wait until STOP has been generated } return receive_count; error: SET_BIT(registerStruct->CR1, I2C_CR1_STOP); // send STOP condition while (READ_BIT(registerStruct->CR1, I2C_CR1_STOP)); // wait until STOP has been generated CLEAR_BIT(registerStruct->CR1, I2C_CR1_PE); // restart the I2C interface clear all error flags SET_BIT(registerStruct->CR1, I2C_CR1_PE); //ITM_SendString("I2C bus error!\n"); return receive_count; }
Die Funktion liefert nach der Übertragung die Anzahl der empfangenen Bytes zurück, oder -1 wenn das Senden fehlschlug. Der Code muss so komplex sein, um Bugs in der I²C Schnittstelle des STM32F1 zu umgehen.
Anwendungsbeispiel:
int main() { // Initialize I2C1, no pin remapping, no fast mode, APB1 clock is 8 MHz i2c_init(I2C1, false, false, 8000000); uint8_t send_buffer[]={0}; uint8_t receive_buffer[5]; i2c_communicate(I2C1, 8, send_buffer, 1, receive_buffer, 5); }
Das obige Beispiel sendet ein Byte {0} an den Slave mit der Adresse 8. Danach werden 5 Bytes vom Slave empfangen.
Die USB Schnittstelle erfordert ein umfangreiches Softwarepaket. Beinahe alle Programmierer binden daher fertige Implementierungen in ihr Programm ein.
Die USB Schnittstelle funktioniert Interrupt-getrieben. Immer wenn die Hardware ein kleines Datenpaket gesendet oder empfangen hat, löst sie einen Interrupt aus. Der Interrupthandler hat die Aufgabe, Anfragen des Host zu beantworten und Nutzdaten mit dem Pufferspeicher auszutauschen. Wenn man den Mikrocontroller beim Debuggen anhält, fällt die USB Schnittstelle sofort aus.
USB und CAN schließen sich gegenseitig aus, man kann nur eine davon gleichzeitig nutzen.
Der Systemtakt muss entweder 48 MHz oder 72 MHz betragen und aus einem Quarz gewonnen werden. Der USB Clock Prescaler wird dementsprechend auf 1 oder 1,5 gestellt, um die USB Schnittstelle mit 48 MHz zu takten.
Die USB Buchse wird mit PA11 (D-) und PA12 (D+) verbunden. Es ist nicht nötig, den Modus (in, out, alternative) dieser Pins zu konfigurieren.
An D+ gehört ein 1,5 kΩ Pull-Up Widerstand auf 3,3 V, welcher dem Host Computer signalisiert, dass ein Gerät angeschlossen wurde. Manche Boards schalten den Widerstand mit einen I/O Pin ein. Dadurch kann man den Host Computer dazu bringen, das USB Gerät erneut zu erkennen, ohne das Kabel abstecken zu müssen.
Beim "Maple Mini" Board schaltet PB9=High den Widerstand aus.
Bitte beachte meinen Hinweis zu CDC Geräten unter Linux, er erspart dir womöglich eine langwierige Fehlersuche.
Auf der Webseite von STM gibt es den "STM32 Virtual COM Port Driver" zum Herunterladen, aber den braucht man seit Windows 8 nicht mehr. Linux und Mac brauchen auch keine Treiber-Installation.
Mit der STM32 Cube IDE/Cube MX kann man sich ein Projekt mit USB Unterstützung zusammenklicken. Das geht so:
Probiere das Programm zunächst ohne Änderungen aus. Der PC sollte im Gerätemanager einen neuen virtuellen COM Anschluss mit dem Namen "Serielles USB-Gerät (COMx)" oder "STMicroelectronics Virtual COM Port (COMx)" anzeigen.
Um Text vom Mikrocontroller an den PC zu senden, füge im Quelltext von main.c folgendes zwischen die genannten Markierungen ein:
/* USER CODE BEGIN Includes */ #include "usbd_cdc_if.h" /* USER CODE END Includes */ ... /* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ // LED On HAL_GPIO_WritePin(LED_GPIO_Port,LED_Pin,GPIO_PIN_SET); HAL_Delay(500); // LED Off HAL_GPIO_WritePin(LED_GPIO_Port,LED_Pin,GPIO_PIN_RESET); HAL_Delay(500); // Send data char msg[]="Hallo!"; CDC_Transmit_FS( (uint8_t*) msg, strlen(msg)); } /* USER CODE END 3 */
Diese Variante belegt etwa 20 kB Flash und 5 kB RAM. Davon dienen jeweils 1 kB als Sendepuffer und Empfangspuffer (kann man ändern). die Ausgabe kann man mit einem Terminal-Programm (Baudrate ist egal) anzeigen.
Damit die Ausgabefunktionen der Standard C Library (z.B. printf, puts, putchar) auf USB geleitet werden, kannst du folgende Funktion implementieren:
// Redirect standard output to the USB port int _write(int file, char *ptr, int len) { CDC_Transmit_FS( (uint8_t*) ptr, len); return len; }
Die USB CDC Implementierung in STM32F103_usb_test.zip stammt aus dem mikrocontroller.net Forum. Sie wurde ursprünglich vom Benutzer W.S. lizenzfrei veröffentlicht und dann von mehreren Mitgliedern verbessert. Der Quelltext ist sehr kompakt - nur zwei Dateien ohne weitere Abhängigkeiten. Auf dem PC braucht man dazu keine Treiber-Installation, im Fall von Windows ist mindestens Version 8 erforderlich.
Das Projekt wurde mit der "STM32 Cube IDE" für das Nucleo-F103RB Board und das Blue Pill Board erstellt. Ich gehe davon aus, daß der Code auf allen STM32F1 Modellen (mit USB) läuft, außer auf STM32F105 und STM32F107, denn die haben eine andere USB Schnittstelle. Die Initialisierung in der main.c muss aber ans jeweilige Modell angepasst werden.
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 5 kB Flash und 600 Bytes RAM. Davon dienen jeweils 256 Byte als Sendepuffer und 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() { 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 void init_clock() { // Because the debugger switches PLL on, we may need to switch // back to the HSI oscillator before we can configure the PLL // Enable HSI oscillator SET_BIT(RCC->CR, RCC_CR_HSION); // Wait until HSI oscillator is ready while(!READ_BIT(RCC->CR, RCC_CR_HSIRDY)) {} // Switch to HSI oscillator MODIFY_REG(RCC->CFGR, RCC_CFGR_SW, RCC_CFGR_SW_HSI); // Wait until the switch is done while ((RCC->CFGR & RCC_CFGR_SWS_Msk) != RCC_CFGR_SWS_HSI) {} // Disable the PLL CLEAR_BIT(RCC->CR, RCC_CR_PLLON); // Wait until the PLL is fully stopped while(READ_BIT(RCC->CR, RCC_CR_PLLRDY)) {} // Flash latency 1 wait state MODIFY_REG(FLASH->ACR, FLASH_ACR_LATENCY, 1 << FLASH_ACR_LATENCY_Pos); // 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; // Set USB prescaler to 1 for 48 MHz clock SET_BIT(RCC->CFGR, RCC_CFGR_USBPRE); } void init_io() { // Enable USB SET_BIT(RCC->APB1ENR, RCC_APB1ENR_USBEN); // Enable Port A and C SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN + RCC_APB2ENR_IOPCEN); // PA5 = Output (LED on Nucleo-64 board) MODIFY_REG(GPIOA->CRL, GPIO_CRL_CNF5, 0b00 << GPIO_CRL_CNF5_Pos); MODIFY_REG(GPIOA->CRL, GPIO_CRL_MODE5, 0b01 << GPIO_CRL_MODE5_Pos); // PC13 = Output (LED on Blue-Pill board) MODIFY_REG(GPIOC->CRH, GPIO_CRH_CNF13, 0b00 << GPIO_CRH_CNF13_Pos); MODIFY_REG(GPIOC->CRH, GPIO_CRH_MODE13, 0b01 << GPIO_CRH_MODE13_Pos); } int main() { init_clock(); init_io(); UsbSetup(); // Initialize system timer SysTick_Config(SystemCoreClock/1000); while (1) { // LED On WRITE_REG(GPIOA->BSRR, GPIO_BSRR_BS5); WRITE_REG(GPIOC->BSRR, GPIO_BSRR_BR13); delay_ms(100); UsbSendStr("Hello World!\n",10); // LED Off WRITE_REG(GPIOA->BSRR, GPIO_BSRR_BR5); WRITE_REG(GPIOC->BSRR, GPIO_BSRR_BS13); delay_ms(900); } }
Die Ausgabe kann man mit einem Terminal-Programm (Baudrate ist egal) anzeigen.
Nach der Initialisierung mittels UsbSetup() wird die Funktion UsbSendStr() benutzt, um Zeichenketten zu senden. UsbSetup setzt voraus, dass der Systemtakt bereits korrekt konfiguriert ist. Die entsprechenden Zeilen habe ich oben fett hervorgehoben.
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. Als Anwendungsbeispiel zeigt er einen nützlichen 3-Fach USB-UART Adapter, den ich für die Eclipse basierten Entwickungsumgebungen und das Blue Pill Board angepasst habe.
Die RTC kann dazu verwendet werden, um eine Uhr zu bauen. Technisch gesehen handelt es sich nur um einen simplen batteriebetriebenen Zähler mit 32 kHz 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 je nach Modell 10 oder 42 so genannte "Backup Register", wo man 16 Bit Werte speichern kann.
Der LSE Oszillator nutzt nur sehr wenig Energie und ist daher bei falscher Beschaltung störanfällig. Die Leitungen zum sorgfältig ausgewählten Quarz sollen so kurz wie möglich sein und die Kapazitäten müssen genau berechnet werden.
Der Anschluss PC13 wird ebenfalls durch den Low-Power Schaltkreis der RTC versorgt, also auch durch die Backup Batterie. Er soll daher nur mit niedriger Frequenz geschaltet werden und bei High Pegel nicht belastet werden.
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 init_io() { // Enable Port A SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN); // PA5 = Output MODIFY_REG(GPIOA->CRL, GPIO_CRL_CNF5, 0b00 << GPIO_CRL_CNF5_Pos); MODIFY_REG(GPIOA->CRL, GPIO_CRL_MODE5, 0b01 << GPIO_CRL_MODE5_Pos); } 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(RTC_IRQn); } void RTC_IRQHandler() { // Clear interrupt flag CLEAR_BIT(RTC->CRL,RTC_CRL_SECF); // Note: After clearing the interrupt flag, give the RTC at // least 5 clock cycles time before leaving the ISR, otherwise // the ISR gets executed twice. // Toggle LED GPIOA->ODR ^= GPIO_ODR_ODR5; } int main() { init_io(); initRtc(); while(1){}; }
In der Interrupt-Vektor Tabelle in startup/startup_stm32.s muss ein Eintrag für den "RTC_IRQHandler" hinzugefügt werden, falls nicht vorhanden.
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 2x the same value. 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; }
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)) {} }
Ohne Kalibrierung stellt man den Vorteiler wie oben gezeigt auf 32767, die Frequenz des Quarzes wird dann durch 32767+1 geteilt. Meine Blue Pill Boards hatten dabei 1-2 Sekunden Abweichung pro Tag. Sollte deine Uhr ohne Kalibrierung um mehr als 5 Sekunden pro Tag abweichen, liegt mit Sicherheit ein Hardwarefehler vor.
Um die Uhr ohne teure Messinstrumente grob zu kalibrieren lässt man sie einige Tage lang laufen und ermittelt dabei ihre Abweichung von der soll-Geschwindigkeit durch Vergleich mit einem präzisen Zeitserver.
Dann stellt man den Vorteiler RTC->PRLL so ein, 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 4 Sekunden pro Tag zu langsam wäre, würde man den Vorteiler um 2 verringern und dann den Korrekturwert auf 15 stellen:
Abweichung: 4,000 Sekunden Vorteiler: -2 * 2,637 = -5,274 Sekunden (setze RTC->PRLL = 32767 - 2) Korrekturwert: 16 * 0,082 = +1,312 Sekunden (setze BKP->RTCCR = 16) ================================================== Summe: 0,038 Sekunden
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:
Modus | Eintritt | Aufwachen | Beschreibung |
---|---|---|---|
WFI Sleep | __WFI() | Interrupt | Warte auf Interrupt. Nur die CPU wird angehalten. |
WFE Sleep | __WFE() | Interrupt oder Ereignis | Warte auf Ereignis. Nur die CPU wird angehalten. |
Stop | PDDS=1, LPDS=1, SLEEPDEEP=1 + __WFI() oder __WFE() | EXTI Leitung, 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. |
Standby | PDDS=1, LPDS=0, SLEEPDEEP=1 + __WFI() oder __WFE() | Steigende Flanke an WKUP Pin, RTC Alarm, externer Reset am NRST, IWDG Reset. | Taktsignale von HSI/HSE sind deaktiviert. RAM Inhalte gehen verloren. Nur die Backup Register bleiben erhalten. Die I/O Pins werden hochohmig, außer TAMPER, WKUP und NRST. Debugging ist nicht möglich. Zum Aufwachen muss der Controller neu starten. |
Wenn der WFI/WFE Sleep innerhalb einer Interruptroutine aktiviert wurde, kann er nur durch einen höher priorisierten Interrupt aufgeweckt werden. Wenn gerade ein Ereignis ansteht, während __WFE() augerufen wird, wird nur das Ereignis gelöscht und kein Sleep Modus aktiviert.
Durch Setzen von Bit 1 (SLEEPONEXIT) im Register SCB->SCR aktiviert man die "Sleep on exit" Funktion. Diese bewirkt, dass der Prozessor nach Abarbeitung jeder Interruptroutine automatisch in den WFI Sleep Modus geht.
Wenn im SCB->SCR Register das Bit 4 (SEVONPEND) gesetzt ist, löst ein anstehender Interrupt zugleich ein Ereignis aus, selbst wenn der Interrupt nicht freigeschaltet ist.
Mit dem Arduino Framework ist das Programmieren einfach, aber man kann nicht alle Funktionen des Chips ausnutzen. Außerdem enthält die Arduino IDE keinen Debugger. Falls Du es trotzdem ausprobieren möchtest, solltest du die Geschichte von STM32duino kennen:
Das Arduino Framework war von Anfang an eine Umsetzung der Programmiersprache Wiring für ATmega Mikrocontroller. Obwohl Ardunio inzwischen auch einige 32bit Controller unterstützt, merkt man deutlich, dass Arduino primär für die Bedürfnisse und Fähigkeiten von 8bit ATmega Controllern gestaltet wurde.
Im Jahr 2009 erschien das Leaflabs Maple Board als erstes STM32 Board für Arduino. Nachdem der Hersteller den Produktsupport im Jahr 2016 beendete, startete Roger Clarks sein (inzwischen umbenanntes) STM32duino Projekt um die Arduino Unterstützung zumindest für den STM32F103 weiter leben zu lassen. Dazu hat er wesentliche Teile von Leaflabs' Code weiter verwendet. Dan Drown hat den Code von Roger Clarks so verpackt, dass man ihn mit dem Boardmanager der Arduino IDE installieren kann.
Im Jahr 2018 wurde das STM32duino Projekt vom Chiphersteller ST übernommen und auf Basis des Cube HAL Frameworks umgeschrieben. So konnte ST schnell sehr viele weitere STM32 Modelle unterstützen. Allerdings passen viele der alten Anleitungen und Beispielprogramme nicht dazu, und die damit erzeugten Programme sind tendenziell größer und langsamer, als mit dem alten Core von Roger Clarks.
Der virtuelle COM-Port ist fester Bestandteil von STM32duino, 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.
So installierst du den STM32duino Core von ST:
So installierst du den alten STM32duino Core von Roger Clarks:
Die Doku zu diesem Arduino Core befindet sich hier. Als Programmieradapter empfehle ich dazu einen originalen oder nachgemachten ST-Link v2.
Damit erzeugte Sketche enthalten immer die Serial Klasse für den virtuellen COM Port über USB. Die echten seriellen Ports sind über die Klassen Serial1, Serial2 und Serial3 ansprechbar.
Dieser Arduino Core basiert nicht auf der CMSIS, daher heißen dort die Konstanten für die Register und Bitmasken etwas anders. Werfe dazu ggf. einen Blick in die Dateien packages/stm32duino/hardware/STM32F1/2018.7.2/system/libmaple/stm32f1/include/series/*.h
Die Pins PA13, PA14, PA15, PB3 and PB4 sind standardmäßig für die ungenutzte 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); }
Für einige STM32F103 Boards steht der STM32duino Bootloader zur Verfügung. Er ermöglicht das Hochladen eigener Sketche (Programme) über den USB Anschluss. Der Bootloader belegt zusätzlich 8 kB vom Flash Speicher, die eigenen Sketche werden dahinter positioniert.
Für das Blue Pill Board ist die Datei generic_boot20_pc13.bin die richtige. Man installiert sie wahlweise über den seriellen Port mit einem USB-UART Adapter oder über den SWJ Debug Port mit einem ST-Link Adapter.
Der dazugehörige Windows Treiber für die Emulation der seriellen Schnittstelle befindet sich im Arduino_STM32 Paket. Führe zur Installation die Datei "Arduino_STM32-master/drivers/win/install_driver.bat" aus. Für Linux braucht man keinen Treiber zu installieren.
Das dazugehöringe Anwendungsprogramm zum Flashen heisst dfu-util und ist Bestandteil des Arduino Cores.
Der STM32duino Bootloader ist nur eine Sekunde lang nach dem Loslassen des Reset-Knopfes aktiv. Dies zeigt er durch schnelles Blinken der Status-LED an. Innerhalb dieser kurzen Zeit kann man mit der Arduino IDE einen Sketch hochladen. Im Gerätemanager von Windows erscheint der Bootloader als "libusb-win32 devices/Maple DFU". Nach einer Sekunde verschwindet er wieder, danach erscheint der virtuelle serielle COM-Port des Sketches.
Wegen dem schwierigen Timing finde ich den Bootloader unhandlich und nicht empfehlenswert.