STM32L0 Anleitung
Technische Daten, Hinweise und Programmier-Beispiele auf Basis der CMSIS für STM32L0 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.
Modell Bezeichnungen
Der Hersteller hat sich für die Bezeichnung seiner Mikrocontroller folgendes Schema ausgedacht:
Low Power Familie
STM32L0 Serie: ARM Cortex M0+ bis 32 MHz, mit EEPROM
STM32L1 Serie: ARM Cortex M3 bis 32 MHz
STM32L4 Serie: ARM Cortex M4 bis 80 MHz mit FPU STM32L5 Serie: ARM Cortex M3 bis 110 MHz mit FPU STM32U5 Serie: ARM Cortex M33 bis 160 MHz mit FPU 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 STM32F3 Serie: ARM Cortex M4 bis 72 MHz mit FPU STM32G4 Serie: ARM Cortex M4 bis 170 MHz mit FPU High Performance Familie
STM32F2 Serie: ARM Cortex M3 bis 120 MHz
STM32F4 Serie: ARM Cortex M4 bis 180 MHz mit FPU STM32F7 Serie: ARM Cortex M7 bis 216 MHz mit FPU STM32H7 Serie: ARM Cortex M7 bis 400 MHz mit FPU |
Hinter der Modellnummer: | ||||||||
Anzahl der Pins
D = 14 pins
E = 25 pins F = 20 Pins G = 28 pins K = 32 Pins T = 36 Pins C = 48 Pins R = 64 Pins V = 100 Pins |
|||||||||
Flash Größe
3 = 8 kB
4 = 16 kB 6 = 32 kB 8 = 64 kB B = 128 kB Z = 192 kB |
|||||||||
Gehäuse
H,I =
T = U = Y = P = |
|||||||||
Temperaturbereich
6 = -40 bis +85 °C
7 = -40 bis +105 °C 3 = -40 bis +125 °C |
Beispiel: Der STM32L010RBT6 hat 64 Pins, 128 kB Flash, ein QFP Gehäuse und ist für -40 bis +85 °C
-
Anmerkung zu den dreistelligen Taktfrequenzen:
Der Flash Speicher ist viel langsamer, daher profitieren nur wenige rechenintensive Befehle sowie Code im RAM von so hohen Taktfrequenzen.
Dokumentationen
Die Pinbelegung und elektrischen Daten stehen im jeweiligen Datenblatt. Für den Programmierer ist das Reference Manual am wichtigsten, da es die I/O Funktionen und Register beschreibt. Im Errata Sheet beschreibt der Hersteller überraschende Einschränkungen und Fehler der Mikrochips, teilweise mit konkreten Workarounds.
Flash Size | 8 kB | 16 kB | 32 kB | 64 kB | 128 kB | 192 kB | |||
RAM Size | 2 kB | 2 kB | 8 kB | 8 oder 20 kB | 20 kB | 20 kB | |||
Model ↱ | x3 | x4 | x6 | x8 | xB | xZ | |||
---|---|---|---|---|---|---|---|---|---|
Value line | STM32L010 | ||||||||
Access line | STM32L011 | ||||||||
STM32L021 | |||||||||
STM32L031 | |||||||||
STM32L041 | |||||||||
STM32L051 | |||||||||
STM32L071 | |||||||||
STM32L081 | |||||||||
USB line | STM32L052 | ||||||||
STM32L062 | |||||||||
STM32L072 | |||||||||
STM32L082 | |||||||||
USB & LCD line | STM32L053 | ||||||||
STM32L063 | |||||||||
STM32L073 | |||||||||
STM32L083 | |||||||||
alle |
Weiterführende Doku:
- Technische Dokumentationen von ST z.B. Application Notes
- STM32L0 HAL and Low Layer Drivers User Manual Beschreibung des Cube HAL Frameworks, das Cube MX verwendet
- Newlib Dokumentation der Standard-C Library
- The Definitive Guide to ARM Cortex-M0 and Cortex-M0+ processors von Joseph Yiu
- ARM Cortex M0+ Technical Reference Manual Beschreibung des ARM Kerns
Elektrische Daten
Alle STM32L0 Chips kann man mit 1,8 bis 3,6 Volt betreiben. Die USB Schnittstelle läuft nur mit 3,3 V.
Fast alle I/O Pins sind 5 V tolerant, sie sind im Datenblatt mit "FT" oder "FTf" gekennzeichnet. Im open-drain Modus dürfen die 5 V toleranten Ausgänge durch externe Widerstände auf 5 V hoch gezogen werden. Analoge Eingänge vertragen maximal die gleiche Spannung wie am VDDA Pin, wenn sie mit einem analogen (internen) Schaltkreis verbunden sind (z.B. dem ADC). Solange das nicht der Fall ist, vertragen die meisten auch 5V.
Die Ausgänge sind einzeln mit 16 mA und alle zusammen mit 90 mA belastbar. Gültige Logikpegel sind aber nur bis 8 mA garantiert. Die Ausgänge sind nicht Kurzschluss-fest.
Die Eingänge sind sehr hochohmig (da CMOS) und haben einen Schmitt-Trigger. Die ESD Schutzdioden sind teilweise nur sehr bedingt belastbar. Die internen Pull-Up und Pull-Down Widerstände haben ungefähr 45 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 nennt das Datenblatt (im Gegensatz zu vielen anderen Serien) keine besonderen Ausnahmen.
Boards
Nucleo-L073RZ

Das Nucleo-L073RZ Board (aus der Nucleo-64 Reihe) ist ein hochwertiges Starter-Set zum günstigen Preis um 18 €.
- ARM Cortex-M0+ Mikrocontroller Modell STM32L073RZT6
- max. 32 MHz
- 192 kB Flash
- 20 kB RAM
- 6 kB EEPROM
- 51·GPIO Pins auf Stiftleisten herausgeführt
- 1·ADC 12bit mit insgesamt 16 Eingängen
- 2·Analog comparator, 2·DAC, 3·I²C, 4·USART, 1·UART, 6·SPI, 1·USB, 7·DMA, 7·16bit Timer, RTC mit Kalender
- 8 MHz Hauptquarz und 32,768 kHz Uhrenquarz
- Eine programmierbare LED an PA5, die bei High Pegel leuchtet
- Ein programmierbarer Taster, der PC13 auf Low zieht, mit Pull-Up Widerstand
- Reset Taster
- Stromversorgung wahlweise über USB, 7-12 V, 5 V oder 3,3 V
- Buchsenleisten für Arduino Shields (aber nur einige Pins vertragen 5 Volt!)
- Abtrennbarer ST-Link Adapter in Version 2.1
- Zum Programmieren
- Zum Debuggen
- Virtueller Memory-Stick über den man Programme mit dem Dateimanager hochladen kann
- Virtueller COM Port (USB-UART) verbunden mit USART2, unterstützt 600 bis 2000000 Baud
- Der ST-Link kann für alle STM32 Mikrocontroller benutzt werden
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.
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.
Nucleo-L031K6

Das Nucleo-L031K6 Board (aus der Nucleo-32 Reihe) ist deutlich kleiner, obwohl es ebenfalls einen ST-Link Adapter enthält. Es kostet ungefährt 15 €.
- ARM Cortex-M4 Mikrocontroller Modell STM32L031K6T6
- max. 32 MHz
- 32 kB Flash
- 8 kB RAM
- 1 kB EEPROM
- 22·GPIO Pins auf Stiftleisten herausgeführt
- 2·ADC 12bit mit 8 Eingängen
- 2·Analog comparator, 1·I²C, 1·USART, 1·UART, 1·SPI, 7·DMA, 4·16bit Timer, RTC mit Kalender
- Kein USB
- 8 MHz Hauptquarz und 32,768 kHz Uhrenquarz
- Eine programmierbare LED an PB3, die bei High Pegel leuchtet
- Reset Taster
- Stromversorgung wahlweise über USB, 7-12 V, 5 V oder 3,3 V
- Arduino Nano kompatible Stiftleisten, aber nicht alle Pins vertragen 5V
- ST-Link Adapter in Version 2.1
- Zum Programmieren
- Zum Debuggen
- Virtueller Memory-Stick über den man Programme mit dem Dateimanager hochladen kann
- Virtueller COM Port (USB-UART) verbunden mit USART2, unterstützt 600 bis 2000000 Baud
Der eingebaute ST-Link Adapter kann nicht abgetrennt werden und er kann auch nicht zum Programmieren anderer Mikrocontroller verwendet werden.
Das User Manual enthält die vollständige Beschreibung des Boardes.
Software
Tools
Entwicklungsumgebungen (du brauchst nur eine):
- STM32 Cube IDE, enthält Cube MX, Hinweise dazu
- Eclipse Embedded CDT Plugins für Eclipse
- VisualGDB Plugin für Visual Studio (nur Windows)
- stm32-for-vscode Plugin für Visual Studio Code
- Keil MDK kostenpflichtig, für Profis (nur Windows)
- Arduino IDE für das Arduino Framework, Hinweise dazu
Weitere Software:
- STM32 Cube MX Code-Generator für HAL, wurde in die Cube IDE integriert
- STM32 Cube Programmer zum Flashen mit ST-Link Adapter, seriellem Port oder USB
- ST-Link Utility zum Anzeigen von Trace Meldungen und Flashen mit ST-Link Adapter (veraltet, nur Windows)
- Flash Loader Demonstrator zum Flashen über den seriellen Bootloader (veraltet, nur Windows)
- DfuSe zum Flashen über den USB Bootloader (veraltet, nur Windows)
- ST-Link Tool zum Flashen mit ST-Link Adapter
- STM32 Flash zum Flashen über den seriellen Bootloader
- CMSIS Core Paket für die STM32 C, F, G, H, L und U Serien (Stand: Mai 2024)
Bibliotheken
Die Basis-Library für alle ARM Cortex-M Mikrocontroller heisst CMSIS Core. Dabei handelt es sich im Grunde genommen um einen Haufen Definitionen für alle Register, damit man sie mit Namen statt über Hexadezimal-Codes ansprechen kann. Außerdem enthält die CMSIS wenige Hilfsfunktionen, die den ARM Kern konfigurieren. Die CMSIS ist von ARM spezifiziert und wird von allen Chip Herstellern in spezifischen Varianten bereit gestellt. 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.
Der arm-gcc Compiler bringt die Standard-C Bibliotheken mit, denen ich weiter unten ein eigenes Kapitel gewidmet habe.
Beispielprogramm
Beispiel für einen einfachen LED-Blinker an PA5 auf Basis der CMSIS:
// Filename: main.c #include <stdint.h> #include "stm32l0xx.h" // delay loop for the default 2.1 MHz CPU clock with optimizer enabled void delay(uint32_t msec) { for (uint32_t j=0; j < msec * 419; j++) { __NOP(); } } int main() { // Enable Port A SET_BIT(RCC->IOPENR, RCC_IOPENR_GPIOAEN); // PA5 = Output MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE5, 0b01 << GPIO_MODER_MODE5_Pos); while(1) { // LED Pin -> High WRITE_REG(GPIOA->BSRR, GPIO_BSRR_BS_5); delay(500); // LED Pin -> Low WRITE_REG(GPIOA->BSRR, GPIO_BSRR_BR_5); delay(500); } }
Ich weiß dass man Delays besser mit einem Timer realisiert. Hier wollte ich jedoch ein möglichst simples Programmbeispiel zeigen.
Programmier- und Debug-Schnittstellen
SWJ Debug Port
Mit dem SWJ Debug Port übertragt man das fertige Programm in den Mikrocontroller und kann es im laufenden Betrieb untersuchen. Zum Beispiel kann man das Programm jederzeit anhalten und dann den Inhalt des Speichers anschauen. Wenn es hängt, kann der Debugger anzeigen, wo das passiert. Der Debugger kann sogar melden, wenn ausgewählte Variablen verändert werden.
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 ARM Cortex M0 Kern hat keine Funktion zur Ausgabe von Trace Meldungen (ITM, Trace-SWO).
ST-Link Adapter
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 | Reserviert für Serial Wire Output. Diese Funktion gibt es beim Cortex-M0 Kern nicht. Beim Debuggen von anderen Mikrocontrollern mit Cortex-M3 (oder Nachfolger) kann man den Pin aber nutzen. |
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!
SWJ Deaktivieren
Um die betroffenen Pins für normale Ein-/Ausgabe zu verwenden, stellt man im Register GPIOx->MODER einfach den gewünschten Modus ein (Input, Output oder Analog).
Boot Loader
Neben der SWJ Schnittstelle haben alle STM32 auch einen unveränderlichen Bootloader, über den man Programme hochladen kann. Er ermöglicht Zugriff auf den Flash Speicher, das RAM und die Option Bytes, zum Debuggen eignet er sich jedoch nicht.
Der Bootloader unterstüzt folgende Anschlüsse:
Modell | USART | USART | USB | SPI | SPI | I2C | I2C |
---|---|---|---|---|---|---|---|
TxD,RxD | TxD,RxD | D-,D+ | NSS,SCK, |
NSS,SCK, |
SCL,SDA | SCL,SDA | |
STM32L010x4,x6 | PA2,3 | PA4-7 | |||||
STM32L010x8,xB | PA2,3 | PA4-7 | PB6,7 | ||||
STM32L011 | PA9,10 | PA2,3 | PA4-71 | ||||
STM32L021 | PA9,10 | PA2,3 | PA4-71 | ||||
STM32L031 | PA9,10 | PA2,3 | PA4-7 | ||||
STM32L041 | PA9,10 | PA2,3 | PA4-7 | ||||
STM32L051 | PA9,10 | PA2,3 | PA4-7 | PB12-15 | |||
STM32L071 | PA9,10 | PA2,3 | PA4-7 | PB12-15 | PB6,7 | PB10,11 | |
STM32L081 | PA9,10 | PA2,3 | PA4-7 | PB12-15 | PB6,7 | PB10,11 | |
STM32L052 | PA9,10 | PA2,3 | PA4-7 | PB12-15 | |||
STM32L062 | PA9,10 | PA2,3 | PA4-7 | ||||
STM32L072 | PA9,10 | PA2,3 | PA11,12 | ||||
STM32L082 | PA9,10 | PA2,3 | PA11,12 | ||||
STM32L053 | PA9,10 | PA2,3 | PA4-7 | PB12-15 | |||
STM32L063 | PA9,10 | PA2,3 | PA4-7 | PB12-15 | |||
STM32L073 | PA9,10 | PA2,3 | PA11,12 | ||||
STM32L083 | PA9,10 | PA2,3 | PA11,12 |
- Bei TSOP14 Gehäuse: PA4,PA7,PA13,PA14
Zur Konfiguration des Bootloaders dient der Boot0 Pin und das Flag Boot1 in den Option Bytes (die mit der Programmiersoftware einstellbar sind):
Boot0 | Boot1 | Starte von |
---|---|---|
Low | egal | Flash ab Adresse 0x0800 0000, gemappt auf 0x0000 0000 |
High | Low | Bootloader |
High | High | RAM ab Adresse 0x2000 0000, gemappt auf 0x0000 0000 |
Boot0 und Boot1 werden beim Reset und beim Aufwachen aus dem Standby Modus gelesen. Um den Bootloader zu aktivieren, setzt man den Pin Boot0=High und dann drückt man den Reset Knopf.
Weitere Informationen zum Bootloader stehen in der Application Note AN2606.
Serieller Bootloader
Die Verbindung zum PC wird mit einem USB-UART Adapter wie diesem hergestellt:
Folgende Verbindungen sind nötig:
PC USB-UART | STM32L0 USART | Beschreibung | |
---|---|---|---|
TxD | → | RxD | Daten |
RxD | ← | TxD | Daten |
GND | GND | Gemeinsame Masse |
Als Taktquelle dient der interne R/C Oszillator, dessen Frequenz bei 3,3 V und Zimmertemperatur ausreichend genau ist. Der Bootloader erkennt die Baudrate automatisch. Es werden 8 Datenbits und gerade Parität (even) verwendet.
USB Bootloader
Der USB Bootloader des STM32L0 benötigt weder Quarz noch den sonst üblichen externen Pull-Up Widerstand. Weitere Anwendungshinweise findest du in der Application Note AN2606.
GCC Optionen
Achtung: Keil MDK verwendet einen anderen Compiler, für den dieses Kapitel nicht passt.
Newlib
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 | 2700 Bytes |
printf() | 1468 | 16 | 4284 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.
Assembler Listing
Wenn du sehen willst, welchen Assembler-Code der Compiler erzeugt, benutze die Compiler-Optionen
-g -Wa,-adhlns="$(@:%.o=%.lst)"
Du findest dann für jede Quell-Datei eine *.lst Datei im Verzeichnis Debug oder Release.
Optimierungen
In den Einstellungen des Compilers kann man beeinflussen, mit welcher Strategie der Compiler das Programm optimiert (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. 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.
Startup-Code
Im Gegensatz zu allen mir bekannten Compilern für 8bit Mikrocontroller befindet sich der Startup-Code und die Interrupt-Vektor Tabelle in editierbaren Dateien (sysmem.c und startup_stm32.s). Der Projekt-Assistent in der IDE legt diese Dateien automatisch an. Für erste Versuche kann man sie unverändert benutzen.
Falls vorhanden, führt der Startup-Code eine Funktion mit folgender Signatur aus, bevor statische Objekte konstruiert werden und bevor main() ausgeführt wird:
void SystemInit() { ... }
Bei großen C++ Projekten kann es sich lohnen, die Erhöhung der Taktfrequenz dort unterzubringen, denn dann startet das Programm schneller.
Speicher-Struktur
Obwohl der Prozessor in Harvard Architektur gestaltet ist, benutzt er einen gemeinsamen Adress-Raum für Programm, Daten und I/O Register. Dadurch können alle I/O Register über Zeiger angesprochen werden und der Prozessor kann Code sowohl aus dem RAM als auch aus dem (Flash-) ROM ausführen. Adressen und Zeiger sind 32bit groß.
Die Befehle sind teilweise 16bit und teilweise 32bit groß.
Daten werden als 8, 16 oder 32bit geladen. Sie müssen nicht zwingend an der 32bit Wortgröße ausgerichtet sein. Aber man erreicht bessere Geschwindigkeit, wenn 16bit Daten an 16bit Adressen und 32bit Daten an 32bit Adressen ausgerichtet sind. Der Compiler kümmert sich automatisch darum.
Die Register für I/O Funktionen sind überwiegend 32bit breit.
Schreibzugriffe auf die Register finden asynchron statt. So kann die CPU zum Beispiel 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 16 MHz (höhere Taktfrequenzen erfordern wait states).
Funktionsaufrufe
Bei Funktionsaufrufen werden bis zu 4 Parameter durch Register übergeben. Dabei ist es vorteilhaft, sie als 32bit Typ zu deklarieren, um Konvertierungen zu vermeiden. Bei mehr als 4 Parametern wird das RAM zur Übergabe benutzt, dann sind 8bit, 16bit und 32bit Typen gleich langsam.
Der Rückgabewert einer Funktion wird ebenfalls in einem Register übermittelt und sollte daher 32bit groß sein, falls Geschwindigkeit wichtig ist.
Stack
Der Stapel speichert ausschließlich 32bit Werte. Bei jedem PUSH wird der Stapelzeiger (SP) zuerst um 4 verringert und dann wird das Wort an diese Adresse abgelegt. Der Stapelzeiger zeigt also immer auf die zuletzt belegte Adresse im RAM.
Es gibt zwei Stapelzeiger MSP und PSP, zwischen denen man umschalten kann. Der Prozessor startet mit dem MSP, welcher durch das erste Wort in der Interrupt-Vektor Tabelle (an Adresse 0) initialisiert wird. Der alternative Stapelzeiger PSP wird von Betriebssystemen genutzt, um den Programmen separate Stapel zuzuweisen. Unter dem Namen SP spricht man immer den Stapelzeiger an, der durch das SPEL Bit im CONTROL Register ausgewählt wurde (0=MSP (default), 1=PSP).
Relevante CMSIS Funktionen:
- __get_MSP()
- __set_MSP(addr)
- __get_PSP()
- __set_PSP(addr)
- __get_CONTROL()
- __set_CONTROL(value)
Interrupt-Vektoren
Der Flash-Speicher beginnt immer mit der Exception- und Interrupt-Vektor Liste. Jeder Eintrag in der Liste ist eine 32bit Sprungadresse. Diese ist im Referenzhandbuch Kapitel "Interrupt and exception vectors" dokumentiert.
Der Quelltext dazu befindet sich in der Datei startup/startup_stm32.s. Dort findest du die vorgegebenen Namen der C-Funktionen. Die folgende Tabelle gilt für alle STM32L0 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. |
0x0010 | 4 | -12 | reserved | |
0x0014 | 5 | -11 | ||
0x0018 | 6 | -10 | ||
0x001C | 7 | -9 | ||
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 | RTC_IRQHandler() | RTC through EXTI17/19/20 line and LSE CSS through EXTI 19 line | |
0x004C | 3 | FLASH_IRQHandler() | Flash memory and data EEPROM | |
0x0050 | 4 | RCC_IRQHandler() | RCC and CRS | |
0x0054 | 5 | EXTI0_1_IRQHandler() | EXTI Line0 and 1 | |
0x0058 | 6 | EXTI2_3_IRQHandler() | EXTI Line2 and 3 | |
0x005C | 7 | EXTI4_15_IRQHandler() | EXTI Line4 to 15 | |
0x0060 | 8 | TSC_IRQHandler() | Touch sense controller | |
0x0064 | 9 | DMA1_Channel1_IRQHandler() | DMA1 Channel1 | |
0x0068 | 10 | DMA1_Channel2_3_IRQHandler() | DMA1 Channel2 and 3 | |
0x006C | 11 | DMA1_Channel4_7_IRQHandler() | DMA1 Channel4 to 7 | |
0x0070 | 12 | ADC_COMP_IRQHandler() | ADC and comparator through EXTI21 and 2 | |
0x0074 | 13 | LPTIM1_IRQHandler() | LPTIMER1 through EXTI2 | |
0x0078 | 14 | USART4_USART5_IRQHandler() | USART4/USART5 | |
0x007C | 15 | TIM2_IRQHandler() | TIMER2 | |
0x0080 | 16 | TIM3_IRQHandler() | TIMER3 | |
0x0084 | 17 | TIM6_DAC_IRQHandler() | TIMER6 and DAC | |
0x0088 | 18 | TIM7_IRQHandler() | TIMER7 | |
0x008C | 19 | reserved | ||
0x0090 | 20 | TIM21_IRQHandler() | TIMER21 | |
0x0094 | 21 | I2C3_IRQHandler() | I2C3 | |
0x0098 | 22 | TIM22_IRQHandler() | TIMER22 | |
0x009C | 23 | I2C1_IRQHandler() | I2C1 through EXTI23 | |
0x00A0 | 24 | I2C2_IRQHandler() | I2C2 | |
0x00A4 | 25 | SPI1_IRQHandler() | SPI1 | |
0x00A8 | 26 | SPI2_IRQHandler() | SPI2 | |
0x00AC | 27 | USART1_IRQHandler() | USART1 through EXTI25 | |
0x00B0 | 28 | USART2_IRQHandler() | USART2 through EXTI26 | |
0x00B4 | 29 | AES_RNG_LPUART1_IRQHandler() | LPUART1 through EXTI28 + AES + RNG | |
0x00B8 | 30 | LCD_IRQHandler() | LCD | |
0x00BC | 31 | USB_IRQHandler() | USB event through EXTI18 |
Über das SCB->VTOR Register kann man den Ort der Liste verändern um sie z.B. ins RAM zu verschieben.
Interrupt Controller
Der Interrupt-Controller NVIC steuert die Verarbeitung von Unterbrechungs-Signalen. Er ist Bestandteil des ARM Kerns.
Solche Signale werden von interner oder externer Hardware ausgelöst, wenn bestimmte Ereignisse auftreten. Sie können dazu genutzt werden, das laufende Programm vorübergehend zu unterbrechen und stattdessen eine besondere Funktion auszuführen, die Interrupt-Handler oder Interrupt-Service-Routine (ISR) genannt wird.
Reset, NMI und HardFault haben immer die höchste Priorität, bei allen anderen kann man die Priorität einstellen. Interrupt-Handler können durch höher priorisierte Interrupts unterbrochen werden.
Die wichtigsten CMSIS Funktionen zur Konfiguration des Interrupt-Systems sind:
- NVIC_SetPriority(CMSIS Interrupt Nr, Priorität) Stelle die Priorität ein (0=höchste, 3=niedrigste)
- NVIC_EnableIRQ(CMSIS Interrupt Nr) Erlaube Hardware Interrupt
- NVIC_DisableIRQ(CMSIS Interrupt Nr) Sperre Hardware Interrupt
- NVIC_SetPendingIRQ(CMSIS Interrupt Nr) Löse einen Interrupt aus
- NVIC_SystemReset() Löse einen System Reset aus
- __disable_irq() Sperre alle Interrupts
- __enable_irq() Hebe die Interrupt-Sperre auf
- __set_PRIMASK(1) Sperre normale Interrupts (ausgenommen: NMI und HardFault)
- __get_PRIMASK() Lese die aktuelle Prioritäten-Maske aus
Um Unterbrechungen temporär zu verbieten (zum Beispiel für exklusiven Zugriff auf Daten oder Schnittstellen), wird folgende Vorgehensweise empfohlen:
uint32_t backup = __get_PRIMASK(); __set_PRIMASK(1); ... do some work ... __set_PRIMASK(backup);
Normale Unterbrechungen sind durch Pegel gesteuert. Wenn das Signal auf High steht, wird der zugeordnete Handler möglichst bald ausgeführt. Während der Interrupt-Handler läuft, muss das Signal wieder auf Low zurück zurück gehen, sonst wird der Interrupt-Handler nach seinem Ende gleich wieder aufgerufen.
Wenn das Signal zu früh verschwindet, während der Interrupt-Controller nach einer Gelegenheit sucht, den Interrupt-Handler auszuführen, geht es verloren. Der Interrupt-Handler wird dann nicht ausgeführt.
Extended Interrupts
Die externen Interrupt-Signale und auch einige interne durchlaufen einen erweiterten Schaltkreis, der zusätzliche Konfiguration erfordert. Man erkennt sie am Stichwort EXTI.
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.
Interrupt Flanken
Viele erweiterte Unterbrechungen werden durch Flanken (Signalwechsel) ausgelöst. Man erkennt sie an den Bits im Register EXTI->RTSR oder EXTI->FTSR.
- Steigende Flanke: Wenn man im Register EXTI->RTSR ein Bit setzt, dann löst ein Signalwechsel von Low nach High die Unterbrechung aus.
- Fallende Flanke: Wenn man im Register EXTI->FTSR ein Bit setzt, dann startet löst ein Signalwechsel von High nach Low die Unterbrechung aus.
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.
Interrupt Masken
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 "stm32l0xx.h" void EXTI4_15_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); // Toggle LED GPIOA->ODR ^= GPIO_ODR_OD5; } int main() { // Enable clock of I/O features SET_BIT(RCC->IOPENR, RCC_IOPENR_GPIOAEN + RCC_IOPENR_GPIOCEN); SET_BIT(RCC->APB2ENR, RCC_APB2ENR_SYSCFGEN); // PA5 = Output for the LED MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE5, 0b01 << GPIO_MODER_MODE5_Pos); // PC13 = Input MODIFY_REG(GPIOC->MODER, GPIO_MODER_MODE13, 0b00 << GPIO_MODER_MODE13_Pos); // Assign EXTI13 to PC13 with rising edge MODIFY_REG(SYSCFG->EXTICR[3], SYSCFG_EXTICR4_EXTI13, SYSCFG_EXTICR4_EXTI13_PC); SET_BIT(EXTI->IMR, EXTI_IMR_IM13); SET_BIT(EXTI->RTSR, EXTI_RTSR_TR13); // Enable the interrupt handler call NVIC_EnableIRQ(EXTI4_15_IRQn); // Clear pending interrupt flag SET_BIT(EXTI->PR, EXTI_PR_PR13); // Endless loop while (1) {} }
Event Masken
Neben den Unterbrechungen steuert der NVIC auch Ereignisse. Ereignisse verwendet man üblicherweise, um die CPU aus aus einem Wait- oder Sleep- Zustand aufzuwecken. So hält zum Beispiel der Befehl __WFE() die CPU bis zum nächsten Ereignis an. Siehe Absatz Powermanagement.
Standardmäßig sind alle Ereignisse maskiert. Man muss im Register EXTI->EMR das entsprechende Bit auf 1 setzen, um ein EXTI Signal als Ereignis zu behandeln.
Taktgeber
Ich habe ziemlich oft gelesen, dass das komplexe System zur Takterzeugung für Anfänger ein großes Hindernis sei. Das sehe ich anders, denn nach einem Reset wird der Mikrocontroller automatisch mit seinem internen 2,097 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->IOPENR, RCC->APB1ENR, RCC->APB2ENR und RCC->AHBENR erledigt wird.
Jetzt kommt der komplizierte Teil. Der System-Takt (SYSCLK) kann aus folgenden Quellen bezogen werden:
- HSE Externe Quelle mit 1-32 MHz oder Oszillator für Quarz oder Keramik Resonator mit 1-25 MHz
- MSI Interner 2,097 MHz R/C Oszillator, auf 0,5% kalibriert (bei 3,3 V und 25 °C)
- HSI16 Interner 16 MHz R/C Oszillator, auf 1% kalibriert (bei 3,3 V und 25 °C)
- HSI48 Interner 48 MHz R/C Oszillator für USB, kalibriert sich selbst
- 65,536 kHz
- 131,072 kHz
- 262,144 kHz
- 524,288 kHz
- 1,048 MHz
- 2,097 MHz (Vorgabe nach Reset und Aufwachen aus dem Standby)
- 4,194 MHz
Für Watchog und Echtzeit-Uhr (RTC) sind weitere Quellen vorgesehen:
- LSE Ist ein guter 32 kHz Quarz-Oszillator für die Echtzeituhr
- LSI Ist ein interner R/C Oszillator für den Watchdog, mit ungefähr 37 kHz, nicht kalibriert
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 STM32L073RZ nach einem Reset:
Achtung:
- Wenn die PLL bereits aktiv ist, muss man sie vor dem Umkonfigurieren zuerst deaktivieren.
- Wenn man den Systemtakt über 16 MHz erhöht, muss man für den Flash Speicher 1 Wait-State einstellen.
- Wenn man den APB1 oder APB2 Prescaler auf einen Teilerfaktor größer als /1 einstellt, dann wird die Taktfrequenz für die Timer nochmal verdoppelt.
Beispiel für den STM32L073RZ, 32 MHz mit dem internen HSI16 Oszillator:
// The current clock frequency uint32_t SystemCoreClock=2097000; // Change system clock to 32 MHz using internal 16 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 1 wait state SET_BIT(FLASH->ACR, FLASH_ACR_LATENCY); // 32 MHz using the 16 MHz HSI oscillator multiply by 4 divide by 2 WRITE_REG(RCC->CFGR, RCC_CFGR_PLLSRC_HSI + RCC_CFGR_PLLMUL4 + RCC_CFGR_PLLDIV2); // 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=32000000; // Switch the MSI oscillator off CLEAR_BIT(RCC->CR, RCC_CR_MSION); }
Die obige Delay Schleife läuft danach allerdings nicht 16x schneller, sondern nur 10x schneller. Der Grund dafür ist, dass der Flash jetzt mit 1 Waitstate 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 den STM32L073RZ, 32 MHz mit einem 8 MHz Quarz (HSE Oszillator):
// The current clock frequency uint32_t SystemCoreClock=2097000; // Change system clock to 32 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 1 wait state SET_BIT(FLASH->ACR, FLASH_ACR_LATENCY); // Enable HSE oscillator SET_BIT(RCC->CR, RCC_CR_HSEON); // Wait until HSE oscillator is ready while(!READ_BIT(RCC->CR, RCC_CR_HSERDY)) {} // 32 MHz using the 8 MHz HSE oscillator multiply by 8 divide by 2 WRITE_REG(RCC->CFGR, RCC_CFGR_PLLSRC_HSE + RCC_CFGR_PLLMUL8 + RCC_CFGR_PLLDIV2); // 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=32000000; // Switch the MSI oscillator off CLEAR_BIT(RCC->CR, RCC_CR_MSION); // Switch the HSI oscillator off CLEAR_BIT(RCC->CR, RCC_CR_HSION); }
SysTick Timer
Alle Cortex-M Prozessoren enthalten einen 24bit Timer, mit dem man die Systemzeit misst. Der Timer zählt die Taktimpulse des Prozessors herunter und löst bei jedem Überlauf einen Interrupt aus. Der Funktionsaufruf SysTick_Config(SystemCoreClock/1000) sorgt dafür, dass jede Millisekunde ein SysTick Interrupt ausgelöst wird.
#include <stdint.h> #include "stm32l0xx.h" // The current clock frequency uint32_t SystemCoreClock=2097000; // 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.
Digitale Pins
Generell können alle I/O Pins können erst benutzt werden, nachdem man ihre Taktversorgung im Register RCC->IOPENR eingeschaltet hat. Standardmäßig sind fast alle I/O Pins für analoge Eingabe konfiguriert.
Im Register GPIOx->MODER konfiguriert man einen Pin als Eingang, Ausgang oder für alternativen Funktionen (wie z.B. serieller Port oder PWM Timer). Wenn man einen I/O Pin für alternative Funktionen verwendet, muss man außerdem in GPIOx->AFR[0] oder GPIOx->AFR[1] einstellen, welche alternative Funktion das sein soll.
Um Eingänge abzufragen, liest man das jeweilige GPIOx->IDR Register.
Direkte Schreibzugriffe sind über das Register GPIOx->ODR möglich. Um einzelne Pins atomar auf High oder Low zu schalten, verwendet man jedoch das GPIOx->BSRR Register.
Im Register GPIOx->OSPEEDR kann man die maximale Frequenz der Ausgänge auf 400 kHz, 2 MHz, 10 MHz oder 35 MHz einstellen. Damit beeinflusst man die Geschwindigkeit, mit der die Spannung von Low nach High (und zurück) wechselt. Der maximale Laststrom wird dadurch nicht verändert. Im Sinne von elektromagnetischer Verträglichkeit soll man hier immer den niedrigsten Wert einstellen, der zur Anwendung passt.
Im Register GPIOx->OTYPER kann man I/O Pins auf den Open-Drain Modus umkonfigurieren und im Register GPIOx->PUPDR kann man optional interne Pull-Up oder Pull-Down Widerstände einschalten.
Schaue Dir die Beschreibung der GPIO Register im Referenzhandbuch an. Die alternativen Funktionen der GPIO Pins sind im Datenblatt des Mikrocontrollers unter dem Stichwort "alternate functions port" tabellarisch beschrieben.
Analoge Eingänge
Alle I/O Pins sind standardmäßig für analoge Eingabe konfguriert. Wenn man sie zwischenzeitlich digital verwendet hat, stellt man das so zurück:
// Configure PA1 as analog input for ADC_IN1 MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE1, 0b11 << GPIO_MODER_MODE1_Pos);
Initialisierung des ADC1 für einzelne Lesezugriffe:
void init_analog() { // Enable clock for ADC SET_BIT(RCC->APB2ENR, RCC_APB2ENR_ADCEN); // Disable the ADC if (READ_BIT(ADC1->ISR, ADC_ISR_ADRDY)) { SET_BIT(ADC1->ISR, ADC_ISR_ADRDY); } if (READ_BIT(ADC1->CR, ADC_CR_ADEN)) { SET_BIT(ADC1->CR, ADC_CR_ADDIS); } // Wait until ADC is disabled while (READ_BIT(ADC1->CR, ADC_CR_ADEN)); // Enable ADC voltage regulator (this sequence is really necessary) CLEAR_BIT(ADC1->CR, ADC_CR_ADVREGEN); SET_BIT(ADC1->CR, ADC_CR_ADVREGEN); // Delay 1-2 ms delay_ms(2); // Set low frequency mode (required if ADC clock is < 3,5MHz) SET_BIT(ADC->CCR,ADC_CCR_LFMEN); // ADC Clock = PCLK/2 MODIFY_REG(ADC1->CFGR2, ADC_CFGR2_CKMODE, 0b01 << ADC_CFGR2_CKMODE_Pos); // Start calibration SET_BIT(ADC1->CR, ADC_CR_ADCAL); // Wait until the calibration is finished while (READ_BIT(ADC1->CR, ADC_CR_ADCAL)); // Clear the ready flag SET_BIT(ADC1->ISR, ADC_ISR_ADRDY); // Enable the ADC and wait until is ready SET_BIT(ADC1->CR, ADC_CR_ADEN); while (!READ_BIT(ADC1->ISR, ADC_ISR_ADRDY)); // Select software start trigger MODIFY_REG(ADC1->CFGR1, ADC_CFGR1_EXTEN, 0b00 << ADC_CFGR1_EXTEN_Pos); // Select single conversion mode CLEAR_BIT(ADC1->CFGR1, ADC_CFGR1_CONT); // Set sample time to 19.5 cycles MODIFY_REG(ADC1->SMPR, ADC_SMPR_SMP, 0b100 << ADC_SMPR_SMP_Pos); }
Lesen eines analogen Eingangs von ADC1:
// Read from an analog input of ADC1 uint32_t read_analog(uint32_t channel) { // Select the channel WRITE_REG(ADC1->CHSELR, 1UL<<channel); // Clear the finish flag CLEAR_BIT(ADC1->ISR, ADC_ISR_EOC); // Start a conversion SET_BIT(ADC1->CR, ADC_CR_ADSTART); // Wait until the conversion is finished while (!READ_BIT(ADC1->ISR, ADC_ISR_EOC)); while (READ_BIT(ADC1->CR, ADC_CR_ADSTART)); // Return the lower 12 bits of the result return ADC1->DR & 0b111111111111; }
Der ADC kann so konfiguriert werden, dass er mehrere Eingänge kontinuierlich liest. Mittels DMA können die Messergebnisse automatisch ins RAM übertragen werden. Dafür habe ich hier kein Beispiel parat.
PWM Ausgänge
Die Timer 2 und 3 können jeweils 4 PWM Signale erzeugen. Damit kann man z.B. die Helligkeit von Lampen oder die Drehzahl von Motoren steuern. Die Timer 21 und 21 haben jeweils zwei PWM Kanäle. Der low-power Timer LPTIM hat nur einen PWM Ausgang.
Alle Timer können mit ihren 16bit die Pulsbreite in maximal 65535 Stufen modulieren.
Die Taktfrequenz der Timer wird vom Systemtakt abgeleitet und kann durch den AHB Prescaler, den ABP2 Prescaler (beide im Register RCC->CFGR), sowie dem Timer Prescaler in TIMx->PSC reduziert werden.
Der low-power timer unterstütz als Taktquelle wahlweise LSI, LSE, HSI, HSE und PCKL1 (das ist der Takt vom APB1 Bus).
Der Timer zählt fortlaufend von 0 an hoch bis zum Maximum, welches durch das TIMx->ARR Register festgelegt wird. Wenn der Maximalwert als 32768 festgelegt wird, können die Ausgangsimpulse wahlweise 1 bis 32768 Takte breit sein.
Für jeden Ausgang gibt es ein Vergleichs-Register TIMx->CCRy welches bestimmt, wie breit die Ausgangsimpulse sein sollen. Beim Extremwert 0 ist der Ausgang ständig Low. Bei Werten größer dem Maximum (in TIMx->ARR) ist der Ausgang ständig High. Mit der option "inverse polarity" im Register TIMx->CCER können die Ausgänge umgepolt werden, so dass sie Low-Impule liefern.
Das folgende Beispielprogramm benutzt einen Ausgang von Timer 2 (PA5), um eine Leuchtdiode unterschiedlich hell flimmern zu lassen:
#include "stm32l0xx.h" // delay loop for default 2.1 MHz clock with optimizer enabled void delay(uint32_t msec) { for (uint32_t j=0; j < msec * 419; j++) { __NOP (); } } int main() { // Enable Port A SET_BIT(RCC->IOPENR, RCC_IOPENR_GPIOAEN); // PA5 = TIM2_CH1 alternate function 5 (see data sheet) MODIFY_REG(GPIOA->AFR[0], GPIO_AFRL_AFSEL5, 5 << GPIO_AFRL_AFSEL5_Pos); MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE5, 0b10 << GPIO_MODER_MODE5_Pos); // Enable timer 2 SET_BIT(RCC->APB1ENR, RCC_APB1ENR_TIM2EN); // Timer 2 channel 1 compare mode = PWM1 with the required preload buffer enabled MODIFY_REG(TIM2->CCMR1, TIM_CCMR1_OC1M, 0b110 << TIM_CCMR1_OC1M_Pos); SET_BIT(TIM2->CCMR1, TIM_CCMR1_OC1PE); // Timer 2 enable channel 1 output SET_BIT(TIM2->CCER, TIM_CCER_CC1E); // Timer 2 inverse polarity for channel 1 // SET_BIT(TIM2->CCER, TIM_CCER_CC1P); // Timer 2 clock prescaler, the PCLK2 clock is divided by this value +1. TIM2->PSC = 2; // divide clock by 3 // Timer 2 auto reload register, defines the maximum value of the counter in PWM mode. TIM2->ARR = 32768; // 2097000/3/32768 = 21 pulses per second // Timer 2 enable counter and auto-preload SET_BIT(TIM2->CR1, TIM_CR1_CEN + TIM_CR1_ARPE); // endless loop while(1) { // Change the brightness of the LED (PA5) in 16 steps for (int i=0; i<=15; i++) { // Timer 2 channel 1 set PWM pulse width TIM2->CCR1 = (1<<i); delay(500); } } }
Ich habe hier absichtlich eine sehr niedrige PWM Frequenz gewählt, damit man das Flackern der LED sehen kann. In einer realen Anwendung würde man natürich eine höhere PWM Frequenz über 100 Hz wählen.
Ich möchte darauf hinweisen, dass die Timer noch viele weitere Funktionen haben, die ich hier gar nicht alle zeigen kann. Mehr Informationen dazu gibt es zum Beispiel in der Application Note AN4776.
USART Schnittstelle
Je nach Taktfrequenz der Peripherie sind unterschiedliche Baudraten möglich:
- Bei 2.1 MHz: 75 bis 115200 Baud
- Bei 8 MHz: 150 bis 500000 Baud
- Bei 32 MHz: 600 bis 2000000 Baud
Mit der Oversampling Option (OVER8=1) sind doppelt so hohe Baudraten möglich.
Die serielle Schnittstelle USART1 liegt auf PA9 (TxD) und PA10 (RxD). Das folgende Beispielprogramm sendet regemäßig "Hello" aus. Außerdem sendet es alle empfangenen Zeichen als Echo wieder zurück. Das Senden findet direkt statt (ggf. mit Warteschleife) während der Empfang interrupt-gesteuert stattfindet:
#include <stdio.h> #include "stm32l0xx.h" uint32_t SystemCoreClock=2097000; // delay loop for the default 2.1 MHz CPU clock with optimizer enabled void delay(uint32_t msec) { for (uint32_t j=0; j < msec * 419; j++) { __NOP(); } } // Redirect standard output to the serial port int _write(int file, char *ptr, int len) { for (int i=0; i<len; i++) { while(!(USART1->ISR & USART_ISR_TXE)); USART1->TDR = *ptr++; } return len; } // Called after each received character void USART1_IRQHandler() { // read the received character char received=USART1->RDR; // send echo back while(!(USART1->ISR & USART_ISR_TXE)); USART1->TDR = received; } int main() { // Enable clock for Port A SET_BIT(RCC->IOPENR, RCC_IOPENR_GPIOAEN); // PA5 = Output for the LED MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE5, 0b01 << GPIO_MODER_MODE5_Pos); // Use system clock for USART1 SET_BIT(RCC->APB2ENR, RCC_APB2ENR_USART1EN); MODIFY_REG(RCC->CCIPR, RCC_CCIPR_USART1SEL, RCC_CCIPR_USART1SEL_0); // PA9 (TxD) shall use the alternate function 4 (see data sheet) MODIFY_REG(GPIOA->AFR[1], GPIO_AFRH_AFSEL9, 4 << GPIO_AFRH_AFSEL9_Pos); MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE9, 0b10 << GPIO_MODER_MODE9_Pos); // PA10 (RxD) shall use the alternate function 4 (see data sheet) MODIFY_REG(GPIOA->AFR[1], GPIO_AFRH_AFSEL10, 4 << GPIO_AFRH_AFSEL10_Pos); MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE10, 0b10 << GPIO_MODER_MODE10_Pos); // Set baudrate USART1->BRR = (SystemCoreClock / 2400); // Enable transmitter, receiver and receive-interrupt of USART1 USART1->CR1 = USART_CR1_UE + USART_CR1_TE + USART_CR1_RE + USART_CR1_RXNEIE; // Enable interrupt in NVIC NVIC_EnableIRQ(USART1_IRQn); while (1) { // LED on WRITE_REG(GPIOA->BSRR, GPIO_BSRR_BS_5); delay(500); puts("Hello"); // LED off WRITE_REG(GPIOA->BSRR, GPIO_BSRR_BR_5); delay(500); } }
Jetzt kommt ein Beispiel für die zweite serielle Schnittstelle. Beim Nucleo-64 Board ist USART2 mit dem ST-Link Adapter verbunden, der diese wiederum über USB an einen virtuellen COM Port weiter leitet:
ST-Link 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.
#include <stdio.h> #include "stm32l0xx.h" uint32_t SystemCoreClock=2097000; // delay loop for the default 2.1 MHz CPU clock with optimizer enabled void delay(uint32_t msec) { for (uint32_t j=0; j < msec * 419; j++) { __NOP(); } } // Redirect standard output to the serial port int _write(int file, char *ptr, int len) { for (int i=0; i<len; i++) { while(!(USART2->ISR & USART_ISR_TXE)); USART2->TDR = *ptr++; } return len; } // Called after each received character void USART2_IRQHandler() { // read the received character char received=USART2->RDR; // send echo back while(!(USART2->ISR & USART_ISR_TXE)); USART2->TDR = received; } int main() { // Enable clock for Port A SET_BIT(RCC->IOPENR, RCC_IOPENR_GPIOAEN); // PA5 = Output for the LED MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE5, 0b01 << GPIO_MODER_MODE5_Pos); // Use system clock for USART2 SET_BIT(RCC->APB1ENR, RCC_APB1ENR_USART2EN); MODIFY_REG(RCC->CCIPR, RCC_CCIPR_USART2SEL, 0b01 << RCC_CCIPR_USART2SEL_Pos); // PA2 (TxD) shall use the alternate function 4 (see data sheet) MODIFY_REG(GPIOA->AFR[0], GPIO_AFRL_AFSEL2, 4 << GPIO_AFRL_AFSEL2_Pos); MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE2, 0b10 << GPIO_MODER_MODE2_Pos); // PA3 (RxD) shall use the alternate function 4 (see data sheet) MODIFY_REG(GPIOA->AFR[0], GPIO_AFRL_AFSEL3, 4 << GPIO_AFRL_AFSEL3_Pos); MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE3, 0b10 << GPIO_MODER_MODE3_Pos); // Set baudrate, assuming that USART2 is clocked with // the same frequency as the CPU core USART2->BRR = (SystemCoreClock / 2400); // Enable transmitter, receiver and receive-interrupt of USART2 USART2->CR1 = USART_CR1_UE + USART_CR1_TE + USART_CR1_RE + USART_CR1_RXNEIE; // Enable interrupt in NVIC NVIC_EnableIRQ(USART2_IRQn); while (1) { // LED on WRITE_REG(GPIOA->BSRR, GPIO_BSRR_BS_5); delay(500); puts("Hello"); // LED off WRITE_REG(GPIOA->BSRR, GPIO_BSRR_BR_5); delay(500); } }
I²C Bus
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 STM32F3 Mikrocontroller haben bis zu drei I²C Busse, alle unterstützen 3,3 V und 5 V Pegel, aber nur wenige Slaves sind so flexibel.
Bevor man einen I²C Anschluss benutzen kann, muss man bei den betroffenen Pins (SDA und SCL) die alternative Funktion im GPIOx->AFR und GPIOx->MODER einstellen. Außerdem muss der Pin im GPIOx->OTYPER Register auf Open-Drain Modus eingestellt werden. Hier ist ein Beispiel für I²C1 auf einem STM32L073:
/** * Initialize the I/O pins. */ init_io() { // Enable Port A SET_BIT(RCC->IOPENR, RCC_IOPENR_GPIOAEN); // PA5 = Output for the LED MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE5, 0b01 << GPIO_MODER_MODE5_Pos); // I2C1 PA9=SCL, alternate function 6 open-drain MODIFY_REG(GPIOA->AFR[1], GPIO_AFRH_AFSEL9, 6 << GPIO_AFRH_AFSEL9_Pos); MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE9, 0b10 << GPIO_MODER_MODE9_Pos); SET_BIT(GPIOA->OTYPER, GPIO_OTYPER_OT_9); // I2C1 PA10=SDA, alternate function 6 open-drain MODIFY_REG(GPIOA->AFR[1], GPIO_AFRH_AFSEL10, 6 << GPIO_AFRH_AFSEL10_Pos); MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE10, 0b10 << GPIO_MODER_MODE10_Pos); SET_BIT(GPIOA->OTYPER, GPIO_OTYPER_OT_10); }
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:
/** * Initialize the I²C interface for master mode. * * The I/O port mode and alternate function must be configured already. * APB1 clock must be at least 2 MHz, all timings are derived from it. * * @param registerStruct May be either I2C1, I2C2 or I2C3 * @param fastMode false=100 kHz, true=400 kHz * @param apb1_clock clock frequency of APB1 peripherals */ void i2c_init(I2C_TypeDef* registerStruct, bool fastMode, uint32_t apb1_clock) { // Enable clock for the I2C interface #ifdef I2C1 if (registerStruct==I2C1) { SET_BIT(RCC->APB1ENR, RCC_APB1ENR_I2C1EN); } #endif #ifdef I2C2 if (registerStruct==I2C2) { SET_BIT(RCC->APB1ENR, RCC_APB1ENR_I2C2EN); } #endif #ifdef I2C3 if (registerStruct==I2C3) { SET_BIT(RCC->APB1ENR, RCC_APB1ENR_I2C3EN); } #endif // Disable the I2C peripheral CLEAR_BIT(registerStruct->CR1, I2C_CR1_PE); // Configure timing if (fastMode) { // i2c clock must be <= 32 MHz otherwise the SCLDEL value would not fit into the register uint32_t prescaler=apb1_clock/8000000; if (prescaler<1) prescaler=1; uint32_t i2c_clock=apb1_clock/prescaler; MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_PRESC, (prescaler-1) << I2C_TIMINGR_PRESC_Pos); MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_SCLL, (i2c_clock/800000-1) << I2C_TIMINGR_SCLL_Pos); MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_SCLH, (i2c_clock/2000000-1) << I2C_TIMINGR_SCLH_Pos); MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_SDADEL, (i2c_clock/4000000) << I2C_TIMINGR_SDADEL_Pos); // no -1 on purpose MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_SCLDEL, (i2c_clock/2000000-1) << I2C_TIMINGR_SCLDEL_Pos); } else { // i2c clock must be <= 12.8 MHz otherwise the SCLDEL value would not fit into the register uint32_t prescaler=apb1_clock/4000000; if (prescaler<1) prescaler=1; uint32_t i2c_clock=apb1_clock/prescaler; MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_PRESC, (prescaler-1) << I2C_TIMINGR_PRESC_Pos); MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_SCLL, (i2c_clock/200000-1) << I2C_TIMINGR_SCLL_Pos); MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_SCLH, (i2c_clock/250000-1) << I2C_TIMINGR_SCLH_Pos); MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_SDADEL, (i2c_clock/2000000) << I2C_TIMINGR_SDADEL_Pos); // no -1 on purpose MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_SCLDEL, (i2c_clock/800000-1) << I2C_TIMINGR_SCLDEL_Pos); } // Stop and Restart will be generated by software CLEAR_BIT(registerStruct->CR2, I2C_CR2_AUTOEND); // Enable the I2C peripheral SET_BIT(registerStruct->CR1, I2C_CR1_PE); } /** * Sub-Function of i2c_communicate. * Configures number of data bytes to send or receive in the current block. */ void configureBlockSize(I2C_TypeDef* registerStruct, int size) { if (size>255) { // Set number of bytes to send or receive in this block MODIFY_REG(registerStruct->CR2, I2C_CR2_NBYTES, 255 << I2C_CR2_NBYTES_Pos); // Prepare to transfer more blocks after this one SET_BIT(registerStruct->CR2, I2C_CR2_RELOAD); } else { // Number of bytes to send or receive in the last block MODIFY_REG(registerStruct->CR2, I2C_CR2_NBYTES, size << I2C_CR2_NBYTES_Pos); // After this block, no more blocks will be transferred CLEAR_BIT(registerStruct->CR2, I2C_CR2_RELOAD); } } /** * 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, I2C2 or I2C3 * @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) { int receive_count=-1; // Set slave address (shifted 1 bit to the left) MODIFY_REG(registerStruct->CR2, I2C_CR2_SADD, slave_addr << (1+I2C_CR2_SADD_Pos)); // Send data if (send_size>0) { // Data direction CLEAR_BIT(registerStruct->CR2, I2C_CR2_RD_WRN); // Configure size of the first data block to send configureBlockSize(registerStruct, send_size); // Send start condition SET_BIT(registerStruct->CR2, I2C_CR2_START); // Send data do { // Check for error if (READ_BIT(registerStruct->ISR, I2C_ISR_NACKF | I2C_ISR_ARLO)) { goto error; } // Send one byte when ready if (READ_BIT(registerStruct->ISR, I2C_ISR_TXIS)) { WRITE_REG(registerStruct->TXDR, *((uint8_t*)send_buffer)); send_buffer++; send_size--; } // Configure size of next block, if requested if (READ_BIT(registerStruct->ISR, I2C_ISR_TCR)) { configureBlockSize(registerStruct, send_size); } } // Loop until the transfer is complete while (!READ_BIT(registerStruct->ISR, I2C_ISR_TC)); } // Sending succeeded, start counting the received bytes receive_count=0; // Receive data if (receive_size>0) { // Data direction SET_BIT(registerStruct->CR2, I2C_CR2_RD_WRN); // Configure size of the first data block to receive configureBlockSize(registerStruct, receive_size); // Send start or restart condition SET_BIT(registerStruct->CR2, I2C_CR2_START); // Receive data do { // Check for error if (READ_BIT(registerStruct->ISR, I2C_ISR_ARLO)) { goto error; } // Fetch one received bytes when ready if (READ_BIT(registerStruct->ISR, I2C_ISR_RXNE)) { *((uint8_t*)receive_buffer)=READ_REG(registerStruct->RXDR); receive_buffer++; receive_count++; receive_size--; } // Configure size of next block, if requested if (READ_BIT(registerStruct->ISR, I2C_ISR_TCR)) { configureBlockSize(registerStruct, receive_size); } } // Loop until the transfer is complete while (!READ_BIT(registerStruct->ISR, I2C_ISR_TC)); } // Send stop condition SET_BIT(registerStruct->CR2, I2C_CR2_STOP); return receive_count; error: // Restart the I2C peripheral CLEAR_BIT(registerStruct->CR1, I2C_CR1_PE); SET_BIT(registerStruct->CR1, I2C_CR1_PE); return receive_count; }
Die Funktion liefert nach der Übertragung die Anzahl der empfangenen Bytes zurück, oder -1 wenn das Senden fehlschlug. Anwendungsbeispiel:
int main() { init_io(); i2c_init(I2C1, false, 2100000); 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.
USB Schnittstelle
Die USB Schnittstelle erfordert ein umfangreiches Softwarepaket. Beinahe alle Programmierer binden daher fertige Implementierungen in ihr Programm ein.
Die USB Schnittstelle funktioniert Interrupt-getrieben. Immer wenn die Hardware ein kleines Datenpaket gesendet oder empfangen hat, löst sie einen Interrupt aus. Der Interrupthandler hat die Aufgabe, Anfragen des Host zu beantworten und Nutzdaten mit dem Pufferspeicher auszutauschen. Wenn man den Mikrocontroller beim Debuggen anhält, fällt die USB Schnittstelle sofort aus.
Der Takt für die USB Schnittstelle wird normalerweise vom HSI48 Oszillator bezogen, der sich selbst kalibriert. Es ist kein Quarz nötig. Der APB Bus muss mit mindestens 10 MHz getaktet werden.
Die USB Buchse wird mit PA11 (D-) und PA12 (D+) verbunden. Es ist nicht nötig, den Modus (in, out, alternative) und Typ dieser Pins zu konfigurieren.
Der Chip hat einen internen 1,5k Ω Pull-Up Widerstand an D+, der im Register USB->BCDR Bit 15 (DPPU) eingeschaltet wird. Indem man den Pull-Up Widerstand aus und wieder ein schaltet, kann man den Host Computer dazu bringen, das USB Gerät erneut zu erkennen, ohne das Kabel abstecken zu müssen.
Bitte beachte meinen Hinweis zu CDC Geräten unter Linux, er erspart dir womöglich eine langwierige Fehlersuche.
Virtueller COM Port mit Cube HAL
Auf der Webseite von STM gibt es den "STM32 Virtual COM Port Driver" zum Herunterladen, aber den braucht man 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:
- Ein neues Projekt anlegen, dabei den richtigen Mikrocontroller einstellen.
- Im Reiter Pinout&Configuration
- Bei System Core/SYS soll die Debug Option: Serial Wire und Timebase Source: SysTick gewählt werden.
- Bei Connectivity/USB/Device (FS) einschalten.
- Bei Middleware/USB_DEVICE, wo die Variante Class for FS IP: Communication Device Class (Virtual Port COM) gewählt werden muss.
- Suche Dir einen freien I/O Pin für die Status LED aus (bzw. nimm den Pin, der durch dein
Board vorgegeben ist).
- Klicke auf den Pin und wähle die Betriebsart GPIO Output.
- Gib dem Pin den Namen "LED" (rechte Maustaste, Enter User Label).
- Im Reiter Clock Configuration kontrollieren, ob HSI48 als Taktquelle für USB eingestellt wurde, und HSI16 als Taktquelle für die CPU.
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 */
Das Programm 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; }
Virtueller COM Port ohne Cube HAL
Die USB CDC Implementierung in STM32L073_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-L073RZ Board erstellt. Ich gehe davon aus, daß der USB Code auf allen STM32L0 und STM32F0 Modellen läuft. Die Initialisierung in der main.c muss aber ans jeweilige Modell angepasst werden.
Das Programm lässt die LED an Anschluss PA5 jede Sekunde aufblitzen und sendet dabei "Hello World!" über USB an den angeschlossenen Computer. Es belegt nur 5 kB Flash und 600 Bytes RAM. Davon dienen jeweils 256 Byte als Sendepuffer und Empfangspuffer (kann man ändern).
#include <stdio.h> #include "stm32l0xx.h" #include "usb.h" // The current clock frequency uint32_t SystemCoreClock=2097000; // 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); } // Change system clock to 32 MHz using internal 16 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 1 wait state SET_BIT(FLASH->ACR, FLASH_ACR_LATENCY); // 32 MHz using the 16 MHz HSI oscillator multiply by 4 divide by 2 WRITE_REG(RCC->CFGR, RCC_CFGR_PLLSRC_HSI + RCC_CFGR_PLLMUL4 + RCC_CFGR_PLLDIV2); // Switch the PLL on SET_BIT(RCC->CR, RCC_CR_PLLON); // Wait until the PLL is ready while(!READ_BIT(RCC->CR, RCC_CR_PLLRDY)) {} // Select the PLL as clock source MODIFY_REG(RCC->CFGR, RCC_CFGR_SW, RCC_CFGR_SW_PLL); // Update variable SystemCoreClock=32000000; // Switch the voltage reference for the HSI48 oscillator on SET_BIT(RCC->APB2ENR, RCC_APB2ENR_SYSCFGEN); SET_BIT(SYSCFG->CFGR3, SYSCFG_CFGR3_ENREF_HSI48); // Wait until the voltage reference is ready while(!READ_BIT(SYSCFG->CFGR3, SYSCFG_CFGR3_VREFINT_RDYF)) {} // Switch the HSI48 oscillator on SET_BIT(RCC->CRRCR, RCC_CRRCR_HSI48ON); // Wait until the HSI48 oscillator is ready while(!READ_BIT(RCC->CRRCR, RCC_CRRCR_HSI48RDY)) {} // Select the HSI48 oscillator as clock source for USB SET_BIT(RCC->CCIPR, RCC_CCIPR_HSI48SEL); } void init_io() { // Enable Port A SET_BIT(RCC->IOPENR, RCC_IOPENR_GPIOAEN); // PA5 = Output MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE5, 0b01 << GPIO_MODER_MODE5_Pos); // Enable USB SET_BIT(RCC->APB1ENR, RCC_APB1ENR_USBEN); // Enable the internal Pull-Up resistor for USB SET_BIT(USB->BCDR, USB_BCDR_DPPU); } int main() { init_clock(); init_io(); UsbSetup(); // Initialize system timer SysTick_Config(SystemCoreClock/1000); while (1) { // LED On (High) WRITE_REG(GPIOA->BSRR, GPIO_BSRR_BS_5); delay_ms(100); UsbSendStr("Hello World!\n",10); // LED Off (Low) WRITE_REG(GPIOA->BSRR, GPIO_BSRR_BR_5); delay_ms(900); } }
Die Ausgabe kann man mit einem Terminal-Programm 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.
Echtzeituhr
Die RTC besteht aus einem 32 kHz Quarz-Oszillator und einer Reihe verketteter Zähler, um auf Sekunden, Minuten, Stunden, Tage, Monate und Jahre zu kommen. Der Oszillator von der RTC läuft schon ohne Kalibrierung wesentlich geauer, als der Haupt-Quarz.
Zwei Alarm-Zeiten sind programmierbar und die Uhr kann sich den Zeitstempel von einem Ereignis merken.
Nach einem Stromausfall ohne bzw. mit leerer Batterie werden die Zähler automatisch auf 0 gesetzt und die Uhr angehalten.
Neben der Uhr enthält die batteriegepufferte Einheit 5 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.
Die Application Note AN4759 beschreibt, wie man die RTC benutzt. Wenn der Systemtakt geringer ist als 230 kHz, dann muss man die Verwendung der Schatten-Register deaktivieren. In den folgenden Beispielen gehe ich davon aus, dass der Systemtak hoch genug ist.
RTC starten
Nach einem Stromausfall ist die Uhr zunächst gestoppt. Man kann sie per Software so starten:
void initRtc() { // Enable the power interface SET_BIT(RCC->APB1ENR, RCC_APB1ENR_PWREN); // Enable access to the backup domain SET_BIT(PWR->CR, PWR_CR_DBP); // Enable LSE oscillator with medium driver power MODIFY_REG(RCC->CSR, RCC_CSR_LSEDRV, 0b10 << RCC_CSR_LSEDRV_Pos); SET_BIT(RCC->CSR, RCC_CSR_LSEON); // Wait until LSE oscillator is ready while(!READ_BIT(RCC->CSR, RCC_CSR_LSERDY)) {} // Select LSE as clock source for the RTC MODIFY_REG(RCC->CSR, RCC_CSR_RTCSEL, RCC_CSR_RTCSEL_LSE); // Enable the RTC SET_BIT(RCC->CSR, RCC_CSR_RTCEN); }
Aufwachen
Nachdem die RTC gestartet ist, kann man sie benutzen, um regelmäßige Unterbrechungen zu erzeugen. Diese wiederum können verwendet werden, um den Mikrocontroller aus Sleep, Stop und Standby Zuständen auf zu wecken.
Das folgende ausführbare Beispiel zeigt, wie man damit die LED auf dem Nucleo-L073RZ Board im Sekundentakt blinken lässt:
#include "stm32l0xx.h" #include <stdio.h> uint32_t SystemCoreClock=2097000; // Redirect standard output to the serial port int _write(int file, char *ptr, int len) { for (int i=0; i<len; i++) { while(!(USART2->ISR & USART_ISR_TXE)); USART2->TDR = *ptr++; } return len; } void init_io() { // Enable Port A SET_BIT(RCC->IOPENR, RCC_IOPENR_GPIOAEN); // PA5 = Output for the LED MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE5, 0b01 << GPIO_MODER_MODE5_Pos); // PA2 (TxD) shall use the alternate function 4 (see data sheet) MODIFY_REG(GPIOA->AFR[0], GPIO_AFRL_AFSEL2, 4U << GPIO_AFRL_AFSEL2_Pos); MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE2, 0b10 << GPIO_MODER_MODE2_Pos); } void initSerial() { // Use system clock for USART2 SET_BIT(RCC->APB1ENR, RCC_APB1ENR_USART2EN); MODIFY_REG(RCC->CCIPR, RCC_CCIPR_USART2SEL, 0b01 << RCC_CCIPR_USART2SEL_Pos); // Set baudrate USART2->BRR = (SystemCoreClock / 2400); // Enable transmitter of USART2 USART2->CR1 = USART_CR1_UE + USART_CR1_TE + USART_CR1_RE + USART_CR1_RXNEIE; } void initRtc() { // Enable the power interface SET_BIT(RCC->APB1ENR, RCC_APB1ENR_PWREN); // Enable access to the backup domain SET_BIT(PWR->CR, PWR_CR_DBP); // Enable LSE oscillator with medium driver power MODIFY_REG(RCC->CSR, RCC_CSR_LSEDRV, 0b10 << RCC_CSR_LSEDRV_Pos); SET_BIT(RCC->CSR, RCC_CSR_LSEON); // Wait until LSE oscillator is ready while(!READ_BIT(RCC->CSR, RCC_CSR_LSERDY)) {} // Select LSE as clock source for the RTC MODIFY_REG(RCC->CSR, RCC_CSR_RTCSEL, RCC_CSR_RTCSEL_LSE); // Enable the RTC SET_BIT(RCC->CSR, RCC_CSR_RTCEN); } void initWakeup() { // Unlock the write protection WRITE_REG(RTC->WPR, 0xCA); WRITE_REG(RTC->WPR, 0x53); // Stop the wakeup timer to allow configuration update CLEAR_BIT(RTC->CR, RTC_CR_WUTE); // Wait until the wakeup timer is ready for configuration update while (!READ_BIT(RTC->ISR, RTC_ISR_WUTWF)) {}; // Clock source of the wakeup timer is 1 Hz MODIFY_REG(RTC->CR, RTC_CR_WUCKSEL, 0b100 << RTC_CR_WUCKSEL_Pos); // The wakeup period is 0+1 clock pulses WRITE_REG(RTC->WUTR,0); // Enable the wakeup timer with interrupts SET_BIT(RTC->CR, RTC_CR_WUTE + RTC_CR_WUTIE); // Switch the write protection back on WRITE_REG(RTC->WPR, 0xFF); // Enable EXTI20 interrupt on rising edge SET_BIT(EXTI->IMR, EXTI_IMR_IM20); SET_BIT(EXTI->RTSR, EXTI_RTSR_TR20); NVIC_EnableIRQ(RTC_IRQn); // Clear (old) pending interrupt flag CLEAR_BIT(RTC->ISR, RTC_ISR_WUTF); // Clear in RTC SET_BIT(EXTI->PR, EXTI_PR_PR20); // Clear in NVIC } void RTC_IRQHandler() { // Clear interrupt flag CLEAR_BIT(RTC->ISR, RTC_ISR_WUTF); // Clear in RTC SET_BIT(EXTI->PR, EXTI_PR_PR20); // Clear in NVIC // Toggle LED GPIOA->ODR ^= GPIO_ODR_OD5; } int main() { init_io(); initRtc(); initWakeup(); initSerial(); while(1) { puts("Hello"); // Enter sleep mode __WFI(); } }
Achtung: Der Name des Interrupt-Handlers und die Kanal Nummer (hier 20) variieren je nach STM32 Modell. Schaue dazu in die Datei startup_stm32.s und in das Referenzhandbuch Tabelle "EXTI lines connections".
Während der Interrupt-Handler die LED blinken lässt, gibt das Hauptprogramm den Text "Hallo" auf dem Seriellen Port aus und legt sich dann schlafen. Das Interrupt-Signal der RTC weckt die CPU im Sekundentakt wieder auf.
RTC Lesen
Man kann die Uhrzeit und das Datum direkt aus den entsprechenden Registern auslesen. Die Hardware verwendet dabei Schatten-Register, die automatisch mit der langsamen RTC synchronisiert werden.
Das folgende Beispiel baut auf dem vorherigen Beispiel auf:
int main() { init_io(); initRtc(); initWakeup(); initSerial(); while(1) { // Extract digits from the RTC time register uint8_t ht= (RTC->TR & RTC_TR_HT) >> RTC_TR_HT_Pos; uint8_t hu= (RTC->TR & RTC_TR_HU) >> RTC_TR_HU_Pos; uint8_t mnt= (RTC->TR & RTC_TR_MNT) >> RTC_TR_MNT_Pos; uint8_t mnu= (RTC->TR & RTC_TR_MNU) >> RTC_TR_MNU_Pos; uint8_t st= (RTC->TR & RTC_TR_ST) >> RTC_TR_ST_Pos; uint8_t su= (RTC->TR & RTC_TR_SU) >> RTC_TR_SU_Pos; // Print the time printf("Time: %d%d:%d%d:%d%d\n", ht,hu, mnt,mnu, st,su); // Extract digits from the RTC date register uint8_t yt= (RTC->DR & RTC_DR_YT) >> RTC_DR_YT_Pos; uint8_t yu= (RTC->DR & RTC_DR_YU) >> RTC_DR_YU_Pos; uint8_t mt= (RTC->DR & RTC_DR_MT) >> RTC_DR_MT_Pos; uint8_t mu= (RTC->DR & RTC_DR_MU) >> RTC_DR_MU_Pos; uint8_t dt= (RTC->DR & RTC_DR_DT) >> RTC_DR_DT_Pos; uint8_t du= (RTC->DR & RTC_DR_DU) >> RTC_DR_DU_Pos; // Print the date printf("Date: %d%d-%d%d-%d%d\n", yt,yu, mt,mu, dt,du); // Enter sleep mode __WFI(); } }
Im Control Register RTC->CR kann man die Anzeige der Zeit beeinflussen:
- SUB1H Subtrahiere eine Stunde
- ADD1H Addiere eine Stunde (für Sommerzeit)
RTC Beschreiben
Datum, Uhrzeit und einige Bits im Control Register sind ziemlich gut gegen versehentliche Änderungen geschützt. Sie lassen sich nur im sogenannten Initialisierungs-Modus beschreiben, wenn der Schreibgeschutz aufgehoben wurde.
Man darf die reservierten Bits nicht verändern. Außerdem muss man nach jedem Schreibzugriff eine Synchronisation der Schatten-Register auslösen und abwarten. Deswegen ist es gut alle Bits im RTC->TR bzw. RTC->DR Register gleichzeitig zu setzen.
Die folgende Prozedur ändert Datum und Uhrzeit unter Berücksichtigung der obigen Aspekte:
/** * Write digits to the RTC time register in 24h format. * @param ht tens of hour * @param hu ones of hours * @param mnt tens of minutes * @param mnu ones of minutes * @param st tens of seconds * @param su ones of seconds */ void RTC_write_time(uint8_t ht, uint8_t hu, uint8_t mnt, uint8_t mnu, uint8_t st, uint8_t su) { // Calculate the new value for the time register uint32_t tmp=READ_REG(RTC->TR); tmp &= ~(RTC_TR_HT+RTC_TR_HU+RTC_TR_MNT+RTC_TR_MNU+RTC_TR_ST+RTC_TR_SU+RTC_TR_PM); // Keep only the reserved bits tmp += (uint32_t) ht << RTC_TR_HT_Pos; tmp += (uint32_t) hu << RTC_TR_HU_Pos; tmp += (uint32_t) mnt << RTC_TR_MNT_Pos; tmp += (uint32_t) mnu << RTC_TR_MNU_Pos; tmp += (uint32_t) st << RTC_TR_ST_Pos; tmp += (uint32_t) su << RTC_TR_SU_Pos; // Unlock the write protection WRITE_REG(RTC->WPR, 0xCA); WRITE_REG(RTC->WPR, 0x53); // Enter initialization mode SET_BIT(RTC->ISR, RTC_ISR_INIT); // Wait until the initialization mode is active while (!READ_BIT(RTC->ISR, RTC_ISR_INITF)) {}; // The 24h format is already the default // CLEAR_BIT(RTC->CR, RTC_CR_FMT); // Update the time register WRITE_REG(RTC->TR,tmp); // Leave the initialization mode CLEAR_BIT(RTC->ISR, RTC_ISR_INIT); // Trigger a synchronization of the shadow registers CLEAR_BIT(RTC->ISR, RTC_ISR_RSF); // Wait until the shadow registers are synchronized while (!READ_BIT(RTC->ISR, RTC_ISR_RSF)) {}; // Switch the write protection back on WRITE_REG(RTC->WPR, 0xFF); } /** * Write digits to the RTC date register. * @param yt tens of year * @param yu ones of year * @param mt tens of month * @param mu ones of month * @param dt tens of day * @param du ones of day * @param wdu week day (1-7) */ void RTC_write_date(uint8_t yt, uint8_t yu, uint8_t mt, uint8_t mu, uint8_t dt, uint8_t du, uint8_t wdu) { // Calculate the new value for the date register uint32_t tmp=READ_REG(RTC->DR); tmp &= ~(RTC_DR_YT+RTC_DR_YU+RTC_DR_MT+RTC_DR_MU+RTC_DR_DT+RTC_DR_DU+RTC_DR_WDU); // Keep only the reserved bits tmp += (uint32_t) yt << RTC_DR_YT_Pos; tmp += (uint32_t) yu << RTC_DR_YU_Pos; tmp += (uint32_t) mt << RTC_DR_MT_Pos; tmp += (uint32_t) mu << RTC_DR_MU_Pos; tmp += (uint32_t) dt << RTC_DR_DT_Pos; tmp += (uint32_t) du << RTC_DR_DU_Pos; tmp += (uint32_t) wdu << RTC_DR_WDU_Pos; // Unlock the write protection WRITE_REG(RTC->WPR, 0xCA); WRITE_REG(RTC->WPR, 0x53); // Enter initialization mode SET_BIT(RTC->ISR, RTC_ISR_INIT); // Wait until the initialization mode is active while (!READ_BIT(RTC->ISR, RTC_ISR_INITF)) {}; // Update the time register WRITE_REG(RTC->DR,tmp); // Leave the initialization mode CLEAR_BIT(RTC->ISR, RTC_ISR_INIT); // Trigger a synchronization of the shadow registers CLEAR_BIT(RTC->ISR, RTC_ISR_RSF); // Wait until the shadow registers are synchronized while (!READ_BIT(RTC->ISR, RTC_ISR_RSF)) {}; // Switch the write protection back on WRITE_REG(RTC->WPR, 0xFF); } int main() { initRtc(); ... // Change the time to 18:33:45 RTC_write_time(1,8, 3,3, 4,5); // Change the date to 19-03-25 (25th March 2019), 1=monday RTC_write_date(1,9, 0,3, 2,5, 1); ... }
RTC kalibrieren
Die RTC erreicht normalerweise ohne Kalibrierung eine Abweichung von maximal zwei Sekunden pro Tag. Durch Kalibrierung kann man die Genauigkeit weiter verbessern. Sollte deine Uhr ohne Kalibrierung um mehr als 5 Sekunden pro Tag abweichen, liegt mit Sicherheit ein Hardwarefehler vor.
Um die Uhr ohne teure Messinstrumente grob zu kalibrieren lässt man sie einige Tage lang laufen und ermittelt dabei ihre Abweichung von der soll-Geschwindigkeit durch Vergleich mit einem präzisen Zeitserver.
Das RTC->CALR Register kann erst nach Deaktivierung des Schreibschutzes verändert werden. Wenn die Uhr zu langsam läuft, setzt man das Bit CALP, um die Uhr genau 42,206 Sekunden pro Tag zu beschleunigen. Dann reduziert man ihre Geschwindigkeit durch den Wert in den CALM Bits. Jede Stufe dort entspricht 0,0824 Sekunden pro Tag.
Wenn die Uhr zum Beispiel 4 Sekunden pro Tag zu langsam wäre, ergäbe sich folgende Rechnung:
Abweichung: 4,000 Sekunden CALP: -42,206 Sekunden (setze RTC->CALR.CALP auf 1) CALM: 464 * 0,0824 = +38,234 Sekunden (setze RTC->CALR.CALM auf 464) =========================================== Summe: 0,028 Sekunden
Power Management
Indem man Taktsignale für I/O Funktionen deaktiviert oder verlangsamt, spart man bereits eine Menge Strom. Darüber hinaus gibt es die folgenden besondere Zustände für den ARM Kern:
Modus | Eintritt | Aufwachen | Beschreibung |
---|---|---|---|
Low-power run | LPSDSR=1 and LPRUN=1 | LPRUN=0 | Der interne Spannungsregler spart Strom, Taktfrequenz und Peripheriefunktionen sind eingeschränkt |
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. |
Die beiden Sleep Modi unterstützen eine Low-power Variante wenn LPSDSR=1 gesetzt ist. Der interne Spannungsregler spart dann Strom und der Takt vom Flash Speicher wird zusätzlich angehalten.
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.
Arduino
Mit dem Arduino Framework ist das Programmieren einfach, aber es ist langsamer, und man kann nicht alle Funktionen des Chips ausnutzen. Ein großer Vorteil ist die Verfügbarkeit zahlreicher Bibliotheken. Falls Du Arduino mit STM32 ausprobieren möchtest, kannst du so anfangen:
- Installiere die Arduino IDE
- Gehe ins Menü Datei/Voreinstellungen. Gebe ins Feld "Zusätzliche Boardverwalter-URLs" die Adresse
https://github.com/
stm32duino/ BoardManagerFiles/ raw/ main/ package_stmicroelectronics_index.json ein. - Gehe ins Menü Werkzeuge/Board/Boardverwalter um die "STM32 MCU based boards" von STMicroelectronics zu installieren.
Serielle Ports in Arduino
In der Board-Konfiguration legt man fest, ob das generische "Serial" Objekt angelegt werden soll:- CDC generic serial: meint einen virtuellen COM Port über den USB Anschluss des Mikrocontrollers (PA11,PA12).
- Enabled generic serial: meint den bevorzugten seriellen Port des Boardes. Beim Nucleo64 Board ist dann Serial=Serial2 (denn USART2 ist mit dem ST-Link Adapter verbunden).
HardwareSerial Serial1(PA10,PA9); HardwareSerial Serial2(PA3,PA2); HardwareSerial Serial4(PC11,PC10); HardwareSerial Serial5(PB4,PB3); HardwareSerial SerialLP1(PB11,PB10); void setup() { Serial1.begin(115200); Serial2.begin(115200); Serial4.begin(115200); Serial5.begin(115200); SerialLP1.begin(9600); }
Virtueller COM Port in Arduino
Der virtuelle COM Port über USB (CDC generic serial) ist integraler Bestandteil von STM32duino, deswegen ist er sehr einfach zu programmieren. Die Einstellung der Baudrate kann entfallen, weil sie keine Rolle spielt. Ein kompletter Beispiel-Sketch:
void setup() { // PA5 is connected to the status LED pinMode(PA5, OUTPUT); } void loop() { digitalWrite(PA5, LOW); Serial.println("Tick"); delay(500); digitalWrite(PA5, 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.