STM32F3 Anleitung
Dies ist der F3 spezifische Teil meiner STM32 Anleitungen.
Die STM32F3 Mikrocontroller wurden als Nachfolger der alten STM32F1 Serie platziert. Sie sind weitgehend Pin-kompatibel, haben dafür aber mehr Funktionen und eine FPU. Die interne Peripherie und ihre Dokumentation wurde deutlich spürbar aufgeräumt.
Die Application Note AN4228 beschreibt, was beim Wechsel von F1 zu F3 zu beachten ist.
Modelle
Die STM32F3 Serie hat einen ARM Cortex M4F Kern bis 72 MHz.| STM32F301 | Access line (die Basis-Version) |
| STM32F302 | USB & CAN line mit USB und CAN |
| STM32F303 | Performance line mit USB, CAN und mehr analoge Features |
| STM32F334 | Digital Power line mit CAN und Hi-res Timer |
| STM32F373 | Precision Measurement line mit USB, CAN, CEC und 16 Bit Sigma Delta ADC |
| Size ↱ | x4 | x6 | x8 | xB | xC | xD | xE |
x: K = 32 Pins C = 48 Pins R = 64 Pins V = 100 Pins Z = 144 Pins |
|---|---|---|---|---|---|---|---|---|
| Flash | 16 KiB | 32 KiB | 64 KiB | 128 KiB | 256 KiB | 384 KiB | 512 KiB | |
| RAM | 16 KiB | 16 KiB | 16 KiB | 40 KiB | 64 KiB | 64 KiB | 64 KiB | |
| STM32F301 | , | |||||||
| STM32F302 | , | , | , | |||||
| RAM | 16 KiB | 16 KiB | 16 KiB | 40 KiB | 64 KiB | 80 KiB | 80 KiB | |
| STM32F303 | , | , | , | |||||
| STM32F334 | , | |||||||
| RAM | 32 KiB | 32 KiB | 32 KiB | |||||
| STM32F373 | , | |||||||
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.
Weiter führende Doku:
- STM32F3 series - PDF Documentation z.B. Application Notes
- STM32 Cortex-M4 MCUs Programming Manual
- STM32 F3 HAL User Manual Beschreibung des Cube HAL Frameworks
- Newlib Dokumentation der Standard-C Library
- Einblick in die moderne Elektronik ohne viel Theorie Anleitung für den Einstieg
- The Definitive Guide to ARM Cortex-M3 and Cortex-M4 Processors von Joseph Yiu
- ARM Cortex M4 Technical Reference Manual Beschreibung des CPU Kerns
Elektrische Daten
Alle hier gezeigten STM32F3 Chips kann man mit 2,0 bis 3,6 Volt betreiben. OPAMP und DAC benötigen aber mindestens 2,4 Volt und die USB Schnittstelle läuft nur mit 3,3 V.
Die Stromaufnahme ist im laufenden Betrieb mit 8 Bit Mikrocontrollern vergleichbar, im Stop Modus ist sie jedoch wesentlich höher. Für Batteriebetrieb empfehle ich die sparsame L0 Serie oder die leistungsstärkere und dennoch sparsamere G4 Serie.
Viele I/O Pins sind 5 V tolerant, sie sind im Datenblatt mit "FT" 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.
Die Ausgänge sind einzeln mit 25 mA und alle zusammen mit 80 mA belastbar. Gültige Logikpegel sind aber nur bis 8 mA garantiert. Die Ausgänge sind nicht Kurzschluss-fest.
Die Eingänge sind sehr hochohmig (da CMOS) und haben einen Schmitt-Trigger. Die 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.
Ausnahmen:
Für die Pins PC13, PC14 und PC15 gelten folgende Einschränkungen:
- Sie dürfen bei HIGH Pegel keinen Strom liefern.
- Sie dürfen bei Low Pegel maximal 3mA aufnehmen.
- Sie dürfen nur mit maximal 2 MHz angesteuert werden und mit maximal 30 pF belastet werden.
Hintergrund ist, dass diese drei Pins intern am (schwachen) Power-Switch der RTC hängen.
Boards
Nucleo-F303RE
Das Nucleo-F303RE Board (aus der Nucleo-64 Reihe) ist ein hochwertiges Starter-Set zum günstigen Preis um 18 €.
- ARM Cortex-M4F Mikrocontroller Modell STM32F303RET6
- max. 72 MHz
- 512 KiB Flash
- 80 KiB RAM
- 12 DMA Kanäle
- RTC mit Kalender
- 51 GPIO Pins auf Stiftleisten herausgeführt
- 4·ADC 12 Bit, 2·DAC 12 Bit, 3·I²C, 5·USART, 4·SPI, 1·USB, 1·CAN, 8·16 Bit Timer, 1·32 Bit Timer
- 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 zum Upload von *.hex und *.bin
- Virtueller COM Port (USB-UART) verbunden mit USART2 (PA2/PA3), 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. Siehe auch mein Buch Einblick in die moderne Elektronik.
Nucleo-F303K8
Das Nucleo-F303K8 Board (aus der Nucleo-32 Reihe) ist deutlich kleiner, obwohl es ebenfalls einen ST-Link Adapter enthält. Es kostet üblicherweise etwa 15 €.
- ARM Cortex-M4F Mikrocontroller Modell STM32F303K8T6
- 7 DMA Kanäle
- RTC mit Kalender
- auf den Stiftleisten
- 22 GPIO Pins
- 7·16 Bit Timer mit 6 PWM Ausgängen
- 2·ADC 12 Bit mit 8 Eingängen
- 2·DAC 12 Bit mit 3 Ausgängen
- 1·USART
- 1·OPAMP
- 1·I²C
- 1·SPI
- 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 und Debuggen
- Virtueller Memory-Stick zum Upload von *.hex und *.bin
- Virtueller COM Port (USB-UART) verbunden mit USART2 (PA2, PA15), unterstützt 600 bis 2000000 Baud
- Kein Trace-SWO
Der eingebaute ST-Link Adapter kann nicht abgetrennt werden und er kann auch nicht zum Programmieren anderer Mikrocontroller verwendet werden.
Das User Manual enthält die vollständige Beschreibung des Boardes.
STM32F3 Discovery
Das STM32F3 Discovery Board bietet neben dem üblichen ST-Link Adapter eine USB-Buchse, die mit dem Target Mikrocontroller verbunden ist. Es ist ab 20 € zu haben.
- ARM Cortex-M4F Mikrocontroller Modell STM32F303VCT6
- max. 72 MHz
- 256 KiB Flash
- 48 KiB RAM
- 12 DMA Kanäle
- RTC mit Kalender
- 87 GPIO Pins auf Stiftleisten herausgeführt
- 2·ADC 12 Bit, 2·DAC 12 Bit, 2·I²C, 5·USART, 3·SPI, 1·USB, 1·CAN, 9·16 Bit Timer, 1·32 Bit Timer
- 8 MHz Hauptquarz, man kann einen 32,768 kHz Uhrenquarz nachrüsten
- Acht programmierbare LEDs an PE9, PE8, PE10, PE15, PE11, PE14, PE12 und PE13, die bei High Pegel leuchten
- Ein programmierbarer Taster, der PA0 auf High zieht, mit Pull-Up Widerstand
- Reset Taster
- Gyroskop-Sensor L3GD20 an I²C oder SPI an PE3 und PA5-7, sowie zwei Interrupts and PE0 und PE1.
- Beschleunigungs- und Kompass-Sensor LSM303DLHC an I²C PB6 und PB7, sowie zwei Interrupts and PE4 und PE5.
- Stromversorgung wahlweise über USB, 5 V oder 3,3 V
- ST-Link Adapter in Version 2.0-A oder 2.0-B
- Zum Programmieren und Debuggen
- Nur v2.0-B: Virtueller Memory-Stick zum Upload von *.hex und *.bin
- Nur v2.0-B: Virtueller COM Port (USB-UART) verbunden mit USART1 (PC4, PC5), unterstützt 600 bis 2000000 Baud
- Der ST-Link kann für alle STM32 Mikrocontroller benutzt werden
Das User Manual enthält die vollständige Beschreibung des Boardes.
STM32F303CCT6 Mini System Dev.board
Das STM32F303CCT6 Board von RobotDyn ist dem Blue-Pill Board nachempfunden, nur mit einem aktuelleren Mikrocontroller Modell.
- ARM Cortex-M4F Mikrocontroller Modell STM32F303CCT6
- max. 72 MHz
- 256 KiB Flash
- 48 KiB RAM
- 12 DMA Kanäle
- RTC mit Kalender
- auf den Stiftleisten
- 28 GPIO Pins
- +2 wenn man auf USB verzichtet (PA11, PA12)
- +2 wenn man die SWD Stifte (PA13, PA14) mit zählt
- +2 wenn man auf den Uhrenquarz verzichtet (PC14, PC15)
- 7·16 Bit Timer mit 26 PWM Ausgängen
- 1·32 Bit Timer mit 3 PWM Ausgängen
- 3·ADC 12 Bit mit 14 Eingängen
- 1·DAC 12 Bit mit 2 Ausgängen
- 3·OPAMP
- 2·I²C
- 3·USART
- 3·SPI
- 1·USB oder CAN
- 28 GPIO Pins
- 8 MHz Hauptquarz und 32,768 kHz Uhrenquarz (auf der Rückseite)
- Ein Jumper für den Boot Modus und ein Jumper an PB2 für freie Verwendung
- Eine programmierbare blaue LED an PC13, die bei Low Pegel leuchtet
- Die Power LED ist grün
- Reset Taster
- Stromversorgung wahlweise über USB, 3,3 V oder 5 V
- Separate Stiftleiste für die SWD Schnittstelle zum Anschluss des Debuggers
Das Board ist etwas schmaler, als das altbekannte Blue-Pill Board. Die Stiftleisten haben trotzdem den gleichen Abstand und die gleiche Pinbelegung. Der Uhrenquarz wurde auf die Rückseite verlegt.
Wenn man besonders dünne Stiftleisten verwendet, passt das Board in einen 40-poligen DIP Sockel.
Wenn der Uhrenquarz benutzt wird, soll man die Stifte an PC14 und PC15 entfernen, damit er stabil schwingt.
Der Spannungsregler kann leicht überhitzen wenn man ihn mit zusätzlichen Verbrauchern belastet.
Beispielprogramm
Beispiel für einen einfachen LED-Blinker an PA5 und PC13 auf Basis der CMSIS:
#include "stm32f3xx.h"
// delay loop for the default 8 MHz CPU clock with optimizer enabled
void delay(uint32_t msec) {
for (uint32_t j=0; j < msec * 2000; j++) {
__NOP();
}
}
int main() {
// Enable Port A and C
SET_BIT(RCC->AHBENR, RCC_AHBENR_GPIOAEN);
SET_BIT(RCC->AHBENR, RCC_AHBENR_GPIOCEN);
// PA5 and PC13 = Output for LEDs
MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER5, 0b01 << GPIO_MODER_MODER5_Pos);
MODIFY_REG(GPIOC->MODER, GPIO_MODER_MODER13, 0b01 << GPIO_MODER_MODER13_Pos);
while(1) {
// Set LED pin to HIGH
GPIOA->BSRR = GPIO_BSRR_BS_5;
GPIOC->BSRR = GPIO_BSRR_BS_13;
delay(500);
// Reset LED pin to LOW
GPIOA->BSRR = GPIO_BSRR_BR_5;
GPIOC->BSRR = GPIO_BSRR_BR_13;
delay(500);
}
}
Mathematische Koprozessoren
FPU
Die Fließkomma-Einheit (FPU) beschleunigt die vier Grundrechenarten und Quadratwurzeln mit dem Datentyp float etwa um das 12-Fache. Das ist ähnlich schnell wie 32 Bit integer. Der C/C++ Compiler und die zugehörige C Bibliothek nutzen die FPU automatisch für float. Double Berechnungen sind hingegen drastisch langsamer, weil die FPU diesen Datentyp nicht unterstützt.
Beachte dass Fließkomma-Literale in C/C++ standardmäßig double sind. Um den Datentyp float zu erzwingen schreibt man sie mit dem Suffix "f", zum Beispiel 3.1416f. Beachte auch, dass die normalen Funktionen der <math.h> Bibliothek auf double basieren. Für float musst du die "f" Versionen verwenden, z.B. roundf() anstatt round().
Funktionen mit float und alle Interrupt-Handler sichern die Register der FPU auf den Stack. Die FPU muss schon vor deren Aufruf eingeschaltet sein, sonst bricht das Programm dabei mit einer HardFault Exception ab. Am Besten schaltet man sie daher in SystemInit() ein. Diese Funktion wird vor main() und vor der Initialisierung globaler Objekte ausgeführt:
void SystemInit() {
// Switch the FPU on
SCB->CPACR = 0x00F00000;
}
Die relevanten Compiler-Optionen zur Nutzung der FPU werden von der Cube IDE wie folgt vorgegeben:
- -mfpu=fpv4-sp-d16 -mfloat-abi=hard
Theoretisch könnte man den Compiler mit float-abi=soft auf die weniger effiziente Berechnung in Software umstellen, aber damit funktioniert die vorkompilierte C-Bibliothek nicht.
Optional kannst du die folgenden Linker-Optionen aktivieren, damit printf() und scanf() float Parameter unterstützen:
- -u _printf_float -u _scanf_float
Beim Aufruf von Interrupt-Handlern reserviert die FPU Platz auf dem Stack, um dort später ihre Register bei Bedarf zu sichern. Dieses Verhalten kann in den Bits ASPEN und LSPEN im Register FPU->FPCCR verändert werden (siehe Programming Manual).
CRC
Zur schnellen Berechnung von CRC Prüfsummen enthält der Mikrocontroller eine eigene Peripherie, die in der Application Note AN4187 vorgestellt wird. Das Reference Manual beschreibt die Programmierung über Register im Kapitel "Cyclic redundancy check calculation unit".
Programmier- und Debug-Schnittstellen
SWJ Deaktivieren
Standardmäßig sind nach einem Reset sowohl SWD als auch JTAG aktiviert. Um die betroffenen Pins für normale Ein-/Ausgabe zu verwenden, stellt man im Register GPIOx->MODER einfach den gewünschten Modus ein (Input, Output oder Analog).
Auch wenn PB3 per Software als normaler I/O Pin konfiguriert wurde, kann er trotzdem mit dem ST-Link Adapter als SWO Ausgang umgestellt werden.
Boot Loader
Der Bootloader unterstützt folgende Anschlüsse:| Modell | USART | USART | USB | I²C |
|---|---|---|---|---|
| TxD,RxD | TxD,RxD | D-,D+ | SCL,SDA | |
| STM32F301xx | PA9,10 | PA2,3 | ||
| STM32F302x6 and x8 | PA9,10 | PA2,3 | PA11,12 | |
| STM32F302xB and xC | PA9,10 | PD5,6 | PA11,12 | |
| STM32F302xD and xE | PA9,10 | PA2,3 | PA11,12 | |
| STM32F303x6 and x8 | PA9,10 | PA2,3 | PB6,7 | |
| STM32F303xB and xC | PA9,10 | PD5,6 | PA11,12 | |
| STM32F303xD and xE | PA9,10 | PA2,3 | PA11,12 | |
| STM32F334xx | PA9,10 | PA2,3 | PB6,7 | |
| STM32F373xx | PA9,10 | PD5,6 | PA11,12 |
Unterbrechungen
Interrupt-Vektoren
Hinter den ARM Processor Exceptions enthält die Interrupt Vektor Tabelle der STM32F3 folgende Einträge:
| Address | CMSIS Interrupt Nr. | C Function | Description | EXTI Channel |
|---|---|---|---|---|
| 0x0040 | 0 | WWDG_IRQHandler() | Window Watchdog | |
| 0x0044 | 1 | PVD_IRQHandler() | PVD | 16 |
| 0x0048 | 2 | TAMP_STAMP_IRQHandler() | Tamper and TimeStamp | 19 |
| 0x004C | 3 | RTC_WKUP_IRQHandler() | RTC wakeup | 20 |
| 0x0050 | 4 | FLASH_IRQHandler() | Flash | |
| 0x0054 | 5 | RCC_IRQHandler() | RCC | |
| 0x0058 | 6 | EXTI0_IRQHandler() | EXTI line 0 | 0 |
| 0x005C | 7 | EXTI1_IRQHandler() | EXTI line 1 | 1 |
| 0x0060 | 8 | EXTI2_TSC_IRQHandler() | EXTI line 2 and Touch sensing | 2 |
| 0x0064 | 9 | EXTI3_IRQHandler() | EXTI line 3 | 3 |
| 0x0068 | 10 | EXTI4_IRQHandler() | EXTI line 4 | 4 |
| 0x006C | 11 | DMA1_CH1_IRQHandler() | DMA1 channel 1 | |
| 0x0070 | 12 | DMA1_CH2_IRQHandler() | DMA1 channel 2 | |
| 0x0074 | 13 | DMA1_CH3_IRQHandler() | DMA1 channel 3 | |
| 0x0078 | 14 | DMA1_CH4_IRQHandler() | DMA1 channel 4 | |
| 0x007C | 15 | DMA1_CH5_IRQHandler() | DMA1 channel 5 | |
| 0x0080 | 16 | DMA1_CH6_IRQHandler() | DMA1 channel 6 | |
| 0x0084 | 17 | DMA1_CH7_IRQHandler() | DMA1 channel 7 | |
| 0x0088 | 18 | ADC1_2_IRQHandler() | ADC1 and ADC2 | |
| 0x008C | 19 | USB_HP_CAN_TX_IRQHandler() | USB High Priority and CAN_TX | |
| 0x0090 | 20 | USB_LP_CAN_RX0_IRQHandler() | USB Low Priority and CAN_RX0 | |
| 0x0094 | 21 | CAN_RX1_IRQHandler() | CAN_RX1 | |
| 0x0098 | 22 | CAN_SCE_IRQHandler() | CAN_SCE | |
| 0x009C | 23 | EXTI9_5_IRQHandler() | EXTI lines 5-9 | 5-9 |
| 0x00A0 | 24 | TIM1_BRK_TIM15_IRQHandler() | TIM1 Break and TIM15 | |
| 0x00A4 | 25 | TIM1_UP_TIM16_IRQHandler() | TIM1 Update and TIM16 | |
| 0x00A8 | 26 | TIM1_TRG_COM_TIM17_IRQHandler() | TIM1 trigger and commutation, and TIM17 | |
| 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_EXTI23_IRQHandler() | I2C1 event | 23 |
| 0x00C0 | 32 | I2C1_ER_IRQHandler() | I2C1 error | |
| 0x00C4 | 33 | I2C2_EV_EXTI24_IRQHandler() | I2C2 event | 24 |
| 0x00C8 | 34 | I2C2_ER_IRQHandler() | I2C2 error | |
| 0x00CC | 35 | SPI1_IRQHandler() | SPI1 | |
| 0x00D0 | 36 | SPI2_IRQHandler() | SPI2 | |
| 0x00D4 | 37 | USART1_EXTI25_IRQHandler() | USART1 | 25 |
| 0x00D8 | 38 | USART2_EXTI26_IRQHandler() | USART2 | 26 |
| 0x00DC | 39 | USART3_EXTI28_IRQHandler() | USART3 | 28 |
| 0x00E0 | 40 | EXTI15_10_IRQHandler() | EXTI lines 10-15 | 10-15 |
| 0x00E4 | 41 | RTCAlarm_IRQHandler() | RTC alarm | 17 |
| 0x00E8 | 42 | USB_WKUP_IRQHandler() | USB wakeup from Suspend | 18 |
| 0x00EC | 43 | TIM8_BRK_IRQHandler() | TIM8 break | |
| 0x00F0 | 44 | TIM8_UP_IRQHandler() | TIM8 update | |
| 0x00F4 | 45 | TIM8_TRG_COM_IRQHandler() | TIM8 Trigger and commutation | |
| 0x00F8 | 46 | TIM8_CC_IRQHandler() | TIM8 capture compare | |
| 0x00FC | 47 | ADC3_IRQHandler() | ADC3 | |
| 0x0100 | 48 | FMC_IRQHandler() | FMC | |
| 0x0104 | 49 | reserved | ||
| 0x0108 | 50 | reserved | ||
| 0x010C | 51 | SPI3_IRQHandler() | SPI3 | |
| 0x0110 | 52 | UART4_EXTI34_IRQHandler() | UART4 | 34 |
| 0x0114 | 53 | UART5_EXTI35_IRQHandler() | UART5 | 35 |
| 0x0118 | 54 | TIM6_DACUNDER_IRQHandler() | TIM6 and DAC1 underrun | |
| 0x011C | 55 | TIM7_IRQHandler() | TIM7 | |
| 0x0120 | 56 | DMA2_CH1_IRQHandler() | DMA2 channel 1 | |
| 0x0124 | 57 | DMA2_CH2_IRQHandler() | DMA2 channel 2 | |
| 0x0128 | 58 | DMA2_CH3_IRQHandler() | DMA2 channel 3 | |
| 0x012C | 59 | DMA2_CH4_IRQHandler() | DMA2 channel 4 | |
| 0x0130 | 60 | DMA2_CH5_IRQHandler() | DMA2 channel 5 | |
| 0x0134 | 61 | ADC4_IRQHandler() | ADC4 | |
| 0x0138 | 62 | reserved | ||
| 0x013C | 63 | reserved | ||
| 0x0140 | 64 | COMP123_IRQHandler() | COMP1, COMP2 and COMP3 | 21, 22, 29 |
| 0x0144 | 65 | COMP456_IRQHandler() | COMP4, COMP5 and COMP6 | 30, 31, 32 |
| 0x0148 | 66 | COMP7_IRQHandler() | COMP7 | 33 |
| 0x014C | 67 | reserved | ||
| 0x0150 | 68 | reserved | ||
| 0x0154 | 69 | reserved | ||
| 0x0158 | 70 | reserved | ||
| 0x015C | 71 | reserved | ||
| 0x0160 | 72 | I2C3_EV_IRQHandler() | I2C3 Event | 27 |
| 0x0164 | 73 | I2C3_ER_IRQHandler() | I2C3 Error | |
| 0x0168 | 74 | USB_HP_IRQHandler() | alternative USB High priority (if SYSCGFG->CFGR1 Bit 5 USB_IT_RMP is set) | |
| 0x016C | 75 | USB_LP_IRQHandler() | alternative USB Low priority (if SYSCGFG->CFGR1 Bit 5 USB_IT_RMP is set) | |
| 0x0170 | 76 | USB_WKUP_EXTI_IRQHandler() | alternative USB wake up from Suspend (if SYSCGFG->CFGR1 Bit 5 USB_IT_RMP is set) | 18 |
| 0x0174 | 77 | TIM20_BRK_IRQHandler() | TIM20 Break | |
| 0x0178 | 78 | TIM20_UP_IRQHandler() | TIM20 Upgrade | |
| 0x017C | 79 | TIM20_TRG_COM_IRQHandler() | TIM20 Trigger and Commutation | |
| 0x0180 | 80 | TIM20_CC_IRQHandler() | TIM20 Capture Compare | |
| 0x0184 | 81 | FPU_IRQHandler() | Floating point | |
| 0x0188 | 82 | reserved | ||
| 0x018C | 83 | reserved | ||
| 0x0190 | 84 | SPI4_IRQHandler() | SPI4 Global |
Beim STM32F334 gilt jedoch abweichend:
| Address | CMSIS Interrupt Nr. | ISR Handler Function | Description | EXTI Kanal |
|---|---|---|---|---|
| 0x0118 | 54 | TIM6_DAC1_IRQHandler() | TIM6 global and DAC1 underrun | |
| 0x011C | 55 | TIM7_DAC2_IRQHandler() | TIM7 global and DAC2 underrun | |
| 0x0140 | 64 | COMP2_IRQHandler() | COMP2 | 22 |
| 0x0144 | 65 | COMP4_6_IRQHandler() | COMP4 and COMP6 | 30, 32 |
| 0x014C | 67 | HRTIM_Master_IRQHandler() | HRTIM master timer | |
| 0x0150 | 68 | HRTIM_TIMA_IRQHandler() | HRTIM timer A | |
| 0x0154 | 69 | HRTIM_TIMB_IRQHandler() | HRTIM timer B | |
| 0x0158 | 70 | HRTIM_TIMC_IRQHandler() | HRTIM timer C | |
| 0x015C | 71 | HRTIM_TIMD_IRQHandler() | HRTIM timer D | |
| 0x0160 | 72 | HRTIM_TIME_IRQHandler() | HRTIM timer E | |
| 0x0164 | 73 | HRTIM_TIM_FLT_IRQHandler() | HRTIM fault |
Extended Interrupts
Die externen Interrupt-Signale und auch einige interne durchlaufen einen erweiterten Schaltkreis, der zusätzliche Konfiguration erfordert. Sie sind im Referenzhandbuch Kapitel "External and internal interrupt/event line mapping" dokumentiert.
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.
Die anderen Kanäle findest du oben in der Interrupt-Vektoren Tabelle wieder.
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 (bzw. EXTI->RTSR2) ein Bit setzt, dann startet die Interruptroutine beim Signalwechsel von Low nach High.
- Fallende Flanke: Wenn man im Register EXTI->FTSR (bzw. EXTI->FTSR2) ein Bit setzt, dann startet die Interruptroutine beim Signalwechsel von High nach Low.
Durch die Verwendung von Flanken ist automatisch sicher gestellt, dass der Handler nicht immer wieder erneut ausgeführt wird, falls ein Interrupt Signal für längere Zeit ansteht.
Flanken gesteuerte Interrupts gehen nicht verloren, wenn das Signal schon wieder verschwindet bevor der Handler aufgerufen wurde. Der Interrupt-Controller merkt sich, dass da mal eine Anforderung vorlag, die noch nicht abgearbeitet wurde.
In diesem Zusammenhang ist das Register EXTI->PR (bzw. EXTI->PR2) wichtig. Dort merkt sich der Interrupt-Controller den Zustand. Während der Initialisierung muss man das Bit zurück setzen, um sicher zu stellen, dass die erste Flanke zuverlässig erkannt wird. Man schreibt eine 1 in das jeweilige Bit, damit es auf 0 zurück gesetzt wird. 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 (bzw. EXTI->IMR2) werden Unterbrechungen maskiert. Man muss hier eine 1 in das jeweilige Bit schreiben, damit eine Interrupt-Leitung wirksam wird. Standardmäßig sind die meisten Interrupts maskiert, sind also ohne Wirkung.
Das folgende Beispiel löst einen Interrupt aus, wenn das Signal an PC13 von Low nach High wechselt. Beim Nucleo-Board ist PC13 mit dem blauen Taster verbunden.
#include "stm32f3xx.h"
// Output a trace message
void ITM_SendString(char *ptr) {
while (*ptr) {
ITM_SendChar(*ptr);
ptr++;
}
}
void EXTI15_10_IRQHandler() {
// Clear pending interrupt flag
// 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 port C and system config
SET_BIT(RCC->AHBENR, RCC_AHBENR_GPIOCEN);
SET_BIT(RCC->APB2ENR, RCC_APB2ENR_SYSCFGEN);
// 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_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) {}
}
Event Masken
Ereignisse verwendet man üblicherweise, um die CPU aus aus einem Wait- oder Sleep- Zustand aufzuwecken. So hält zum Beispiel der Befehl __WFE() (aus dem Powermanagement) die CPU bis zum nächsten Ereignis an.
Standardmäßig sind alle Ereignisse maskiert. Man muss im Register EXTI->EMR (bzw. EXTI->EMR2) das entsprechende Bit auf 1 setzen, um ein EXTI Signal als Ereignis zu behandeln.
Taktgeber
Ich habe ziemlich oft gelesen, dass das komplexe System zur Takterzeugung für Anfänger ein großes Hindernis sei. Das sehe ich anders, denn nach einem Reset wird der Mikrocontroller automatisch mit seinem internen 8 MHz R/C Oszillator getaktet. Damit kann man schon eine Menge sinnvoller Programme schreiben.
Die Taktsignale für den ARM Kern, 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:
- HSE Externe Quelle mit 1-32 MHz oder Oszillator für Quarz oder Keramik Resonator mit 4-32 MHz
- HSI Interner 8 MHz R/C Oszillator, auf 1% kalibriert (bei 3,3 V und 25 °C)
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 mit ca. 40 kHz für den Watchdog, 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 STM32F303x6 und x8 nach einem Reset:
Das folgende Bild zeigt die Standardvorgabe vom STM32F303xB und xC nach einem Reset:
Das folgende Bild zeigt die Standardvorgabe vom STM32F303xD und xE nach einem Reset:
Achtung:
- Wenn die PLL bereits aktiv ist, muss man sie vor dem Umkonfigurieren zuerst deaktivieren.
- Bei mehr als 24 MHz Systemtakt, muss man für den Flash Speicher 1 Wait-State einstellen.
- Bei mehr als 36 MHz ist außerdem ein Vorteiler für den internen APB1 Bus (auch low-speed I/O genannt) einzustellen.
- Bei mehr als 48 MHz Systemtakt, muss man für den Flash Speicher 2 Wait-States 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 STM32F303xD und xE, 64 MHz mit dem internen HSI Oszillator:
// The current clock frequency
uint32_t SystemCoreClock=8000000;
// Change system clock to 64 MHz using internal 8 MHz R/C oscillator
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 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
RCC->CFGR = RCC_CFGR_PLLMUL16 + RCC_CFGR_PPRE1_DIV2;
// Enable PLL
SET_BIT(RCC->CR, RCC_CR_PLLON);
// Wait until PLL is ready
while(!READ_BIT(RCC->CR, RCC_CR_PLLRDY)) {}
// Select PLL as clock source
MODIFY_REG(RCC->CFGR, RCC_CFGR_SW, RCC_CFGR_SW_PLL);
// Update variable
SystemCoreClock=64000000;
}
Durch die Angabe __attribute__((section(".data"))) könnte man Funktionen etwas schneller ohne Waitstate aus dem RAM ausführen, da dieser ohne Waitstates arbeitet.
Beispiel für den STM32F303xD und xE, 72 MHz mit einem 8 MHz Quarz (HSE Oszillator):
// The current clock frequency
uint32_t SystemCoreClock=8000000;
// Change system clock to 72 MHz using 8 MHz crystal
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 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
RCC->CFGR = RCC_CFGR_SWS_HSE + RCC_CFGR_PLLSRC_HSE_PREDIV + RCC_CFGR_PLLMUL9 + RCC_CFGR_PPRE1_DIV2;
// Enable PLL
SET_BIT(RCC->CR, RCC_CR_PLLON);
// Wait until PLL is ready
while(!READ_BIT(RCC->CR, RCC_CR_PLLRDY)) {}
// Select PLL as clock source
MODIFY_REG(RCC->CFGR, RCC_CFGR_SW, RCC_CFGR_SW_PLL);
// Update variable
SystemCoreClock=72000000;
// Disable the HSI oscillator
CLEAR_BIT(RCC->CR, RCC_CR_HSION);
}
Digitale Pins
Generell können alle I/O Pins erst benutzt werden, nachdem man den Port im Register RCC->AHBENR eingeschaltet hat.
Standardmäßig sind fast alle I/O Pins als digitaler Eingang konfiguriert. Um deren Status abzufragen, liest man das jeweilige GPIOx->IDR Register.
Im Register GPIOx->MODER konfiguriert man einen Pin als Ausgang oder für alternative Funktionen. Für letztere muss man außerdem in GPIOx->AFR[0] oder GPIOx->AFR[1] einstellen, welche alternative Funktion das sein soll (z.B. serieller Port oder PWM Timer). Die alternativen Funktionen sind im Datenblatt unter dem Stichwort "alternate functions" tabellarisch beschrieben.
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->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.
Im Register GPIOx->OSPEEDR kann man die Geschwindigkeit der Ausgänge in drei Stufen einstellen (Siehe "Input/output AC characteristics" im Datenblatt). Damit beeinflusst man, wie schnell 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.
Schaue Dir die Beschreibung der GPIO Register im Referenzhandbuch an!
Analoge Eingänge
Fast alle I/O Pins sind standardmäßig für digitale Eingabe konfiguriert. Für analoge Nutzung setzt man beide Bits im GPIOx->MODER Register:
// Configure PA1 as analog input for ADC1_IN2 MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER1, 0b11 << GPIO_MODER_MODER1_Pos);
Initialisierung des ADC1 für einzelne Lesezugriffe:
// Initialize the ADC1 for single conversion mode
void init_analog() {
// Enable ADC
SET_BIT(RCC->AHBENR, RCC_AHBENR_ADC12EN);
// Disable the ADC
if (READ_BIT(ADC1->ISR, ADC_ISR_ADRDY)) {
SET_BIT(ADC1->ISR, ADC_ISR_ADRDY);
}
if (READ_BIT(ADC1->CR, ADC_CR_ADEN)) {
SET_BIT(ADC1->CR, ADC_CR_ADDIS);
}
// Wait until ADC is disabled
while (READ_BIT(ADC1->CR, ADC_CR_ADEN));
// Enable ADC voltage regulator (this sequence is really necessary)
MODIFY_REG(ADC1->CR, ADC_CR_ADVREGEN, 0b00 << ADC_CR_ADVREGEN_Pos);
MODIFY_REG(ADC1->CR, ADC_CR_ADVREGEN, 0b01 << ADC_CR_ADVREGEN_Pos);
// Delay 1-2 ms
delay(2);
// ADC Clock = HCLK/4
MODIFY_REG(ADC12_COMMON->CCR, ADC12_CCR_CKMODE, 0b11 << ADC12_CCR_CKMODE_Pos);
// Single ended mode for all channels
ADC1->DIFSEL = 0;
// Start calibration for single ended mode
CLEAR_BIT(ADC1->CR, ADC_CR_ADCALDIF);
SET_BIT(ADC1->CR, ADC_CR_ADCAL);
// Wait until the calibration is finished
while (READ_BIT(ADC1->CR, ADC_CR_ADCAL));
// Clear the ready flag
SET_BIT(ADC1->ISR, ADC_ISR_ADRDY);
// Enable the ADC repeatedly until success (workaround from errata)
do {
SET_BIT(ADC1->CR, ADC_CR_ADEN);
}
while (!READ_BIT(ADC1->ISR, ADC_ISR_ADRDY));
// Select software start trigger
MODIFY_REG(ADC1->CFGR, ADC_CFGR_EXTEN, 0b00 << ADC_CFGR_EXTEN_Pos);
// Select single conversion mode
CLEAR_BIT(ADC1->CFGR, ADC_CFGR_CONT);
// Set sample time to 32 cycles
MODIFY_REG(ADC1->SMPR1, ADC_SMPR1_SMP1, 0b100 << ADC_SMPR1_SMP1_Pos);
}
Lesen eines analogen Eingangs von ADC1:
// Read from an analog input of ADC1
uint32_t read_analog(uint32_t channel) {
// Number of channels to convert: 1
MODIFY_REG(ADC1->SQR1, ADC_SQR1_L, 0 << ADC_SQR1_L_Pos); // ADC does one conversion more than configured here
// Select the channel
MODIFY_REG(ADC1->SQR1, ADC_SQR1_SQ1, channel << ADC_SQR1_SQ1_Pos);
// Clear the finish flag
CLEAR_BIT(ADC1->ISR, ADC_ISR_EOC);
// Start a conversion
SET_BIT(ADC1->CR, ADC_CR_ADSTART);
// Wait until the conversion is finished
while (!READ_BIT(ADC1->ISR, ADC_ISR_EOC));
while (READ_BIT(ADC1->CR, ADC_CR_ADSTART));
// Return the lower 12 bits of the result
return ADC1->DR & 0b111111111111;
}
Der ADC kann so konfiguriert werden, dass er mehrere Eingänge kontinuierlich liest. Mittels DMA können die Messergebnisse automatisch ins RAM übertragen werden. Dafür habe ich hier kein Beispiel parat.
PWM Ausgänge
Die Timer 1, 8 und 20 können jeweils 6 PWM Signale erzeugen. Damit kann man z.B. die Helligkeit von Lampen oder die Drehzahl von Motoren steuern. Die Timer 2, 3 und 4 haben jeweils vier PWM Kanäle. Die Timer 15, 16 und 17 haben jeweils 2 PWM Kanäle.
Der Timer 2 hat als einziger 32 Bit Auflösung für maximal 4294967295 Stufen, die anderen haben mit 16 Bit maximal 65535 Stufen.
Die Taktfrequenz der Timer wird normalerweise vom Systemtakt abgeleitet und kann durch den AHB Prescaler, den ABP2 Prescaler (beide im Register RCC->CFGR), sowie dem Timer Prescaler in TIMx->PSC reduziert werden. Bei einigen Timern kann man für höhere Taktfrequenzen die PLL im RCC->CFGR3 Register auswählen, was maximal 144 MHz ergibt. Der STM32F334 ist noch schneller, aber auf den gehe ich hier nicht weiter ein.
Der Timer zählt fortlaufend von 0 an hoch bis zum Maximum, welches durch das TIMx->ARR Register festgelegt wird. Wenn der Maximalwert als 50000 festgelegt wird, können die Ausgangsimpulse wahlweise 1 bis 5000 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-Impulse liefern.
Das folgende Beispielprogramm benutzt einen Ausgang von Timer 2 (PA5), um eine Leuchtdiode unterschiedlich hell leuchten zu lassen:
#include "stm32f3xx.h"
// delay loop for the default 8 MHz clock with optimizer enabled
void delay(uint32_t msec) {
for (uint32_t j=0; j < msec * 2000; j++) {
__NOP();
}
}
int main() {
// Enable port A and timer 2
SET_BIT(RCC->AHBENR, RCC_AHBENR_GPIOAEN);
SET_BIT(RCC->APB1ENR, RCC_APB1ENR_TIM2EN);
// PA5 = TIM2_CH1 alternate function 1 (see data sheet)
MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER5, 0b10 << GPIO_MODER_MODER5_Pos);
MODIFY_REG(GPIOA->AFR[0], GPIO_AFRL_AFRL5, 1 << GPIO_AFRL_AFRL5_Pos);
// 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 = 7; // divide clock by 8
// Timer 2 auto reload register, defines the maximum value of the counter
TIM2->ARR = 50000; // 8000000/10/50000 = 20 pulses per second
// Timer 2 enable counter and auto-preload
SET_BIT(TIM2->CR1, TIM_CR1_CEN + TIM_CR1_ARPE);
// Start with short pulses / low brightness
uint32_t pulsewidth = 90;
// endless loop
while(1) {
TIM2->CCR1 = pulsewidth;
delay(500);
// increase brightness
pulsewidth=pulsewidth*2;
// start over when the maximum value has been reached
if (pulsewidth > TIM2->ARR) {
pulsewidth = 90;
}
}
}
Ich habe hier absichtlich eine sehr niedrige PWM Frequenz gewählt, damit man das Pulsieren der LED sehen kann. In einer realen Anwendung würde man natürich eine höhere PWM Frequenz über 100 Hz wählen.
Die Timer 1, 8, 15, 16, 17 und 20 können komplementäre Ausgangssignale mit Tot-Zeit erzeugen, was für den Eigenbau von H-Brücken nützlich ist.
Ich möchte darauf hinweisen, dass die Timer noch viele weitere Funktionen haben, die ich hier gar nicht alle zeigen kann. Mehr Informationen dazu gibt es zum Beispiel in der Application Note AN4776.
USART Schnittstelle
Je nach Taktfrequenz der Peripherie sind unterschiedliche Baudraten möglich. Beispiele:
- Bei 8 MHz: 150 bis 500000 Baud
- Bei 16 MHz: 300 bis 1000000 Baud
- Bei 32 MHz: 600 bis 2000000 Baud
- Bei 64 MHz: 1200 bis 4000000 Baud
- Bei 72 MHz: 2400 bis 4000000 Baud (nur USART1)
Die serielle Schnittstelle USART1 liegt auf PA9 (TxD) und PA10 (RxD). Das 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 "stm32f3xx.h"
uint32_t SystemCoreClock=8000000;
// Delay loop for the default 8 MHz CPU clock with optimizer enabled
void delay(uint32_t msec) {
for (uint32_t j=0; j < msec * 2000; j++) {
__NOP();
}
}
// Use serial port for standard output
int _write(int file, char *ptr, int len) {
for (int i=0; i<len; i++) {
// wait until TX buffer is empty
while(!(USART1->ISR & USART_ISR_TXE));
// write one character to the transmit data register
USART1->TDR = *ptr++;
}
return len;
}
// Called after each received character
void USART1_EXTI25_IRQHandler() {
// read the received character
char received=USART1->RDR;
// send echo back
while(!(USART1->ISR & USART_ISR_TXE));
USART1->TDR = received;
}
int main() {
// Enable port A and USART1
SET_BIT(RCC->AHBENR, RCC_AHBENR_GPIOAEN);
SET_BIT(RCC->APB2ENR, RCC_APB2ENR_USART1EN);
// PA5 = Output for the LED
MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER5, 0b01 << GPIO_MODER_MODER5_Pos);
// Use system clock for USART1
MODIFY_REG(RCC->CFGR3, RCC_CFGR3_USART1SW, 0b01 << RCC_CFGR3_USART1SW_Pos);
// PA9 (TxD) shall use the alternate function 7 (see data sheet)
MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER9, 0b10 << GPIO_MODER_MODER9_Pos);
MODIFY_REG(GPIOA->AFR[1], GPIO_AFRH_AFRH1, 7 << GPIO_AFRH_AFRH1_Pos);
// PA10 (RxD) shall use the alternate function 7 (see data sheet)
MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER10, 0b10 << GPIO_MODER_MODER10_Pos);
MODIFY_REG(GPIOA->AFR[1], GPIO_AFRH_AFRH2, 7 << GPIO_AFRH_AFRH2_Pos);
// Set baudrate
USART1->BRR = (SystemCoreClock / 9600);
// 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
GPIOA->BSRR = GPIO_BSRR_BS_5;
delay(500);
puts("Hello");
// LED off
GPIOA->BSRR = GPIO_BSRR_BR_5;
delay(500);
}
}
Jetzt kommt ein Beispiel für die zweite serielle Schnittstelle. Beim Nucleo-64 Board ist USART2 (PA2, PA3) mit dem ST-Link Adapter verbunden, der diese wiederum über USB an einen virtuellen COM Port weiter leitet:
#include <stdio.h>
#include "stm32f3xx.h"
uint32_t SystemCoreClock=8000000;
// Delay loop for the default 8 MHz CPU clock with optimizer enabled
void delay(uint32_t msec) {
for (uint32_t j=0; j < msec * 2000; j++) {
__NOP();
}
}
// Use serial port for standard output
int _write(int file, char *ptr, int len) {
for (int i=0; i<len; i++) {
// wait until TX buffer is empty
while(!(USART2->ISR & USART_ISR_TXE));
// write one character to the transmit data register
USART2->TDR = *ptr++;
}
return len;
}
// Called after each received character
void USART2_EXTI26_IRQHandler() {
// read the received character
char received=USART2->RDR;
// send echo back
while(!(USART2->ISR & USART_ISR_TXE));
USART2->TDR = received;
}
int main() {
// Enable port A and USART1
SET_BIT(RCC->AHBENR, RCC_AHBENR_GPIOAEN);
SET_BIT(RCC->APB1ENR, RCC_APB1ENR_USART2EN);
// PA5 = Output for the LED
MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER5, 0b01 << GPIO_MODER_MODER5_Pos);
// Use system clock for USART2
MODIFY_REG(RCC->CFGR3, RCC_CFGR3_USART2SW, 0b01 << RCC_CFGR3_USART2SW_Pos);
// PA2 (TxD) shall use the alternate function 7 (see data sheet)
MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER2, 0b10 << GPIO_MODER_MODER2_Pos);
MODIFY_REG(GPIOA->AFR[0], GPIO_AFRL_AFRL2, 7 << GPIO_AFRL_AFRL2_Pos);
// PA3 (RxD) shall use the alternate function 7 (see data sheet)
MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER3, 0b10 << GPIO_MODER_MODER3_Pos);
MODIFY_REG(GPIOA->AFR[0], GPIO_AFRL_AFRL3, 7 << GPIO_AFRL_AFRL3_Pos);
// Set baudrate, assuming that USART2 is clocked with
// the same frequency as the CPU core (no prescaler)
USART2->BRR = (SystemCoreClock / 9600);
// Enable transmitter, receiver and receive-interrupt of USART2
USART2->CR1 = USART_CR1_UE + USART_CR1_TE + USART_CR1_RE + USART_CR1_RXNEIE;
// Enable interrupt in NVIC
NVIC_EnableIRQ(USART2_IRQn);
while (1) {
// LED an
GPIOA->BSRR = GPIO_BSRR_BS_5;
delay(500);
puts("Hello");
// LED aus
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. Die Signale durchlaufen interne Filter zur Entstörung.
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²C2 auf einem STM32F303CC:
/**
* Initialize the I/O pins.
*/
init_io() {
// Enable Port A
SET_BIT(RCC->AHBENR, RCC_AHBENR_GPIOAEN);
// I2C2 PA9=SCL, alternate function 4 open-drain
MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER9, 0b10 << GPIO_MODER_MODER9_Pos);
MODIFY_REG(GPIOA->AFR[1], GPIO_AFRH_AFRH1, 4 << GPIO_AFRH_AFRH1_Pos);
SET_BIT(GPIOA->OTYPER, GPIO_OTYPER_OT_9);
// I2C2 PA10=SDA, alternate function 4 open-drain
MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER10, 0b10 << GPIO_MODER_MODER10_Pos);
MODIFY_REG(GPIOA->AFR[1], GPIO_AFRH_AFRH2, 4 << GPIO_AFRH_AFRH2_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 7 Bit 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.
* HSI must be on because it is used as clock source.
*
* @param registerStruct May be either I2C1, I2C2 or I2C3
* @param fastMode false=100 kHz, true=400 kHz
*/
void i2c_init(I2C_TypeDef* registerStruct, bool fastMode) {
// Select HSI as clock source for the I²C interface
#ifdef I2C1
if (registerStruct==I2C1) {
CLEAR_BIT(RCC->CFGR3, RCC_CFGR3_I2C1SW);
SET_BIT(RCC->APB1ENR, RCC_APB1ENR_I2C1EN);
}
#endif
#ifdef I2C2
if (registerStruct==I2C2) {
CLEAR_BIT(RCC->CFGR3, RCC_CFGR3_I2C2SW);
SET_BIT(RCC->APB1ENR, RCC_APB1ENR_I2C2EN);
}
#endif
#ifdef I2C3
if (registerStruct==I2C3) {
CLEAR_BIT(RCC->CFGR3, RCC_CFGR3_I2C3SW);
SET_BIT(RCC->APB1ENR, RCC_APB1ENR_I2C3EN);
}
#endif
// Disable the I²C peripheral
CLEAR_BIT(registerStruct->CR1, I2C_CR1_PE);
// Configure timing
if (fastMode) {
MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_PRESC, 0x00 << I2C_TIMINGR_PRESC_Pos);
MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_SCLL, 0x09 << I2C_TIMINGR_SCLL_Pos);
MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_SCLH, 0x03 << I2C_TIMINGR_SCLH_Pos);
MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_SDADEL, 0x01 << I2C_TIMINGR_SDADEL_Pos);
MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_SCLDEL, 0x03 << I2C_TIMINGR_SCLDEL_Pos);
}
else {
MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_PRESC, 0x01 << I2C_TIMINGR_PRESC_Pos);
MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_SCLL, 0x13 << I2C_TIMINGR_SCLL_Pos);
MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_SCLH, 0x0F << I2C_TIMINGR_SCLH_Pos);
MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_SDADEL, 0x02 << I2C_TIMINGR_SDADEL_Pos);
MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_SCLDEL, 0x04 << I2C_TIMINGR_SCLDEL_Pos);
}
// Stop and Restart will be generated by software
CLEAR_BIT(registerStruct->CR2, I2C_CR2_AUTOEND);
// Enable the I²C 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 7 Bit 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);
// 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)) {
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 byte when ready
if (READ_BIT(registerStruct->ISR, I2C_ISR_RXNE)) {
*((uint8_t*)receive_buffer) = 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 I²C peripheral
CLEAR_BIT(registerStruct->CR1, I2C_CR1_PE);
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. Anwendungsbeispiel:
int main() {
init_io();
i2c_init(I2C2, false);
uint8_t send_buffer[]={0};
uint8_t receive_buffer[5];
i2c_communicate(I2C2, 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, falls er antwortet.
USB Schnittstelle
Die USB Schnittstelle erfordert ein umfangreiches Softwarepaket. Beinahe alle Programmierer binden daher fertige Implementierungen in ihr Programm ein.
Die USB Schnittstelle funktioniert Interrupt-getrieben. Immer wenn die Hardware ein kleines Datenpaket gesendet oder empfangen hat, löst sie einen Interrupt aus. Der Interrupthandler hat die Aufgabe, Anfragen des Host zu beantworten und Nutzdaten mit dem Pufferspeicher auszutauschen. Wenn man den Mikrocontroller beim Debuggen anhält, fällt die USB Schnittstelle sofort aus.
Der Systemtakt muss entweder 48 MHz oder 72 MHz betragen und aus einem Quarz gewonnen werden. Der USB Clock Prescaler wird dementsprechend auf 1 oder 1,5 gestellt, um die USB Schnittstelle mit 48 MHz zu takten. Der APB Bus muss mit mindestens 10 MHz getaktet werden.
Die USB Buchse wird mit PA11 (D-) und PA12 (D+) verbunden.
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.
Bei allen STM32F3 können die USB und CAN Schnittstellen gleichzeitig verwendet werden. Der Pufferspeicher (außerhalb des RAM) ist so organisiert:
- STM32F302xB/C, STM32F303xB/C und STM32F373: haben 512 Bytes für USB + 512 Bytes für CAN.
- STM32F302x6/8, STM32F302xD/E und STM32F303xD/E: haben 1024 Bytes gemeinsam genutzten Speicher. Bei gleichzeitiger Nutzung belegt CAN die letzten 256 Bytes.
Bitte beachte meinen Hinweis zu CDC Geräten unter Linux, er erspart dir womöglich eine langwierige Fehlersuche.
Virtueller COM Port mit Cube HAL
Mit Cube MX kann man sich ein Projekt mit USB Unterstützung zusammen klicken. Das geht so:
- Ein neues Projekt anlegen, dabei den richtigen Mikrocontroller einstellen.
- Im Reiter Pinout & Configuration
- Bei System Core/RCC "High Speed Clock (HSE): Crystal/Ceramic Resonator" einstellen.
- Bei System Core/SYS soll die "Debug Option: Serial Wire" und "Timebase Source: SysTick" gewählt werden.
- Bei Connectivity/USB das "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 den HSE Oszillator als Taktquelle wählen. Die Quarzfrequenz muss stimmen und mittels PLL auf 48 MHz erhöht werden.
- Gebe dem Projekt im Reiter Project Manager einen Namen und stelle als Toolchain deine "STM32CubeIDE" ein.
Um Text vom Mikrocontroller an den PC zu senden, füge im Quelltext von main.c folgendes zwischen die "USER CODE" 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!\n";
CDC_Transmit_FS( (uint8_t*) msg, strlen(msg));
}
/* USER CODE END 3 */
Das Programm belegt etwa 20 KiB Flash und 5 KiB RAM. Davon dienen jeweils 1 KiB als Sendepuffer und Empfangspuffer (kann man ändern). Die Ausgabe kann man mit einem Terminal-Programm anzeigen, indem man damit den virtuellen COM Port (VCP) öffnet. Die Baudrate wird igoriert. Unter Linux geht es auch ganz simpel mit dem Befehl cat /dev/ttyACM0.
Damit die Ausgabefunktionen der Standard C Library (z.B. printf, puts, putchar) auf USB geleitet werden, kannst du folgende Funktion implementieren:
#include <stdio.h>
// Use the USB port for standard output
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 Implementierungen in
- STM32F303RE_usb_test.zip (für Nucleo-F303RE Board) und
- STM32F303CC_usb_test.zip (für das STM32F303CCT6 Board von RobotDyn)
stammen aus dem mikrocontroller.net Forum. Sie wurden 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. Das Programm belegt nur 4 KiB Flash und 600 Bytes RAM. Davon dienen jeweils 256 Byte als Sendepuffer und Empfangspuffer (kann man ändern).
Die Projekte wurden mit der STM32 Cube IDE erstellt. Ich gehe davon aus, daß der Code nach kleinen Anpassung auf allen STM32F3 Modellen läuft.
Das folgende Programm lässt die LED an PC13 jede Sekunde aufblitzen und sendet dabei "Hello World!" über USB an den angeschlossenen Computer:
#include <stdio.h>
#include "stm32f3xx.h"
#include "usb.h"
// The current clock frequency
uint32_t SystemCoreClock=8000000;
// Counts milliseconds
volatile uint32_t systick_count=0;
// Interrupt handler
void SysTick_Handler() {
systick_count++;
}
// Delay some milliseconds
void delay(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 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
RCC->CFGR = RCC_CFGR_SWS_HSI + RCC_CFGR_PLLSRC_HSE_PREDIV + RCC_CFGR_PLLMUL6 + RCC_CFGR_PPRE1_DIV2;
// Enable PLL
SET_BIT(RCC->CR, RCC_CR_PLLON);
// Wait until PLL is ready
while(!READ_BIT(RCC->CR, RCC_CR_PLLRDY)) {}
// Select PLL as clock source
MODIFY_REG(RCC->CFGR, RCC_CFGR_SW, RCC_CFGR_SW_PLL);
// Update variable
SystemCoreClock=48000000;
// Disable HSI oscillator
CLEAR_BIT(RCC->CR, RCC_CR_HSION);
// Set USB prescaler to 1 for 48 MHz clock
MODIFY_REG(RCC->CFGR, RCC_CFGR_USBPRE, RCC_CFGR_USBPRE_DIV1);
}
void init_io() {
// Enable USB
SET_BIT(RCC->APB1ENR, RCC_APB1ENR_USBEN);
// Enable Port A and C
SET_BIT(RCC->AHBENR, RCC_AHBENR_GPIOAEN);
SET_BIT(RCC->AHBENR, RCC_AHBENR_GPIOCEN);
// PC13 = Output (for the LED)
MODIFY_REG(GPIOC->MODER, GPIO_MODER_MODER13, 0b01 << GPIO_MODER_MODER13_Pos);
// The following lines are not needed on STM32F303xD and xE
// PA11 = USB D-, alternate function 14 push/pull (see data sheet)
MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER11, 0b10 << GPIO_MODER_MODER11_Pos);
MODIFY_REG(GPIOA->AFR[1], GPIO_AFRH_AFRH3, 14 << GPIO_AFRH_AFRH3_Pos);
// PA12 = USB D+, alternate function 14 push/pull (see data sheet)
MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER12, 0b10 << GPIO_MODER_MODER12_Pos);
MODIFY_REG(GPIOA->AFR[1], GPIO_AFRH_AFRH4, 14 << GPIO_AFRH_AFRH4_Pos);
}
int main() {
init_clock();
init_io();
UsbSetup();
// Initialize system timer
SysTick_Config(SystemCoreClock/1000);
while (1) {
// LED On (Low)
GPIOC->BSRR = GPIO_BSRR_BR_13;
delay(100);
UsbSendStr("Hello World!\n",10);
// LED Off (High)
GPIOC->BSRR = GPIO_BSRR_BS_13;
delay(900);
}
}
Die Ausgabe kann man mit einem Terminal-Programm anzeigen, indem man damit den virtuellen COM Port (VCP) öffnet. Die Baudrate wird igoriert. Unter Linux geht es auch ganz simpel mit dem Befehl cat /dev/ttyACM0.
Nach der Initialisierung mittels UsbSetup() wird die Funktion UsbSendStr() benutzt, um Zeichenketten zu senden. UsbSetup setzt voraus, dass der Takt bereits korrekt konfiguriert ist. Die entsprechenden Zeilen habe ich oben fett hervorgehoben. Schaue für weitere Funktionen in die Datei usb.h.
Damit die Ausgabefunktionen der Standard C Library (z.B. printf, puts, putchar) auf USB geleitet werden, kannst du folgende Funktion implementieren:
#include <stdio.h>
// Use the USB port for standard output
int _write(int file, char *ptr, int len) {
return UsbSendBytes(ptr, len, 10);
}
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 16 so genannte "Backup Register", in denen 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->BDCR, RCC_BDCR_LSEDRV, 0b10 << RCC_BDCR_LSEDRV_Pos);
SET_BIT(RCC->BDCR, RCC_BDCR_LSEON);
// Wait until LSE oscillator is ready
while(!READ_BIT(RCC->BDCR, RCC_BDCR_LSERDY)) {}
// Select LSE as clock source for the RTC
MODIFY_REG(RCC->BDCR, RCC_BDCR_RTCSEL, RCC_BDCR_RTCSEL_LSE);
// Enable the RTC
SET_BIT(RCC->BDCR, RCC_BDCR_RTCEN);
}
Aufwachen
Nachdem die RTC gestartet ist, kann man sie benutzen, um regelmäßige Unterbrechungen zu erzeugen. Diese wiederum können verwendet werden, um den Mikrocontroller aus Sleep, Stop und Standby Zuständen auf zu wecken.
Im folgenden Anwendungsbeispiel für das Nucleo-F303RE Board wird die schlafende CPU jede Sekunde aufgeweckt:
#include "stm32f3xx.h"
#include <stdio.h>
uint32_t SystemCoreClock=8000000;
// Use serial port for standard output
int _write(int file, char *ptr, int len) {
for (int i=0; i<len; i++) {
// wait until TX buffer is empty
while(!(USART2->ISR & USART_ISR_TXE));
// write one character to the transmit data register
USART2->TDR = *ptr++;
}
return len;
}
void init_io() {
// Enable Port A and USART2
SET_BIT(RCC->AHBENR, RCC_AHBENR_GPIOAEN);
SET_BIT(RCC->APB1ENR, RCC_APB1ENR_USART2EN);
// PA5 = Output for the LED
MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER5, 0b01 << GPIO_MODER_MODER5_Pos);
// PA2 (TxD) shall use the alternate function 7 (see data sheet)
MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER2, 0b10 << GPIO_MODER_MODER2_Pos);
MODIFY_REG(GPIOA->AFR[0], GPIO_AFRL_AFRL2, 7U << GPIO_AFRL_AFRL2_Pos);
// Use system clock for USART2
MODIFY_REG(RCC->CFGR3, RCC_CFGR3_USART2SW, 0b01 << RCC_CFGR3_USART2SW_Pos);
// Set baudrate
USART2->BRR = (SystemCoreClock / 9600);
// Enable transmitter of USART2
USART2->CR1 = USART_CR1_UE + USART_CR1_TE;
}
void initRtc() {
// Enable the power interface
SET_BIT(RCC->APB1ENR, RCC_APB1ENR_PWREN);
// Enable access to the backup domain
SET_BIT(PWR->CR, PWR_CR_DBP);
// Enable LSE oscillator with medium driver power
MODIFY_REG(RCC->BDCR, RCC_BDCR_LSEDRV, 0b10 << RCC_BDCR_LSEDRV_Pos);
SET_BIT(RCC->BDCR, RCC_BDCR_LSEON);
// Wait until LSE oscillator is ready
while(!READ_BIT(RCC->BDCR, RCC_BDCR_LSERDY)) {}
// Select LSE as clock source for the RTC
MODIFY_REG(RCC->BDCR, RCC_BDCR_RTCSEL, RCC_BDCR_RTCSEL_LSE);
// Enable the RTC
SET_BIT(RCC->BDCR, RCC_BDCR_RTCEN);
}
void initWakeup() {
// Unlock the write protection
RTC->WPR = 0xCA;
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
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
RTC->WPR = 0xFF;
// Enable EXTI20 interrupt on rising edge
SET_BIT(EXTI->IMR, EXTI_IMR_MR20);
SET_BIT(EXTI->RTSR, EXTI_RTSR_TR20);
NVIC_EnableIRQ(RTC_WKUP_IRQn);
// Clear (old) pending interrupt flag
CLEAR_BIT(RTC->ISR, RTC_ISR_WUTF); // Clear in RTC
SET_BIT(EXTI->PR, EXTI_PR_PR20); // Clear in EXTI
}
void RTC_WKUP_IRQHandler() {
// Clear interrupt flag
CLEAR_BIT(RTC->ISR, RTC_ISR_WUTF); // Clear in RTC
SET_BIT(EXTI->PR, EXTI_PR_PR20); // Clear in EXTI, it is important that this is not the last command in the ISR
// Toggle LED
GPIOA->ODR ^= GPIO_ODR_5;
}
int main() {
// Give the debugger time to connect
delay(5000);
init_io();
initRtc();
initWakeup();
while(1) {
puts("Hello");
// Enter sleep mode
__WFI();
}
}
⭐ Da dein Programm die CPU schlafen legt, empfiehlt sich ein 5s Delay ganz am Anfang der main() Funktion. Der Debugger bekommt dadurch genug Zeit, sich vorher zu verbinden. Wenn du einen Debugger ohne Reset-Leitung verwendest, musst du den Reset-Taster drücken, um die CPU aufzuwecken.
⚠️ Der Name des Interrupt-Handlers und die Kanal Nummer (hier 20) variieren je nach STM32 Modell. Schaue dazu in die Datei startup_stm32.s und in das Referenzhandbuch Kapitel "External and internal interrupt/event line mapping".
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() {
// Give the debugger time to connect
delay(5000);
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 hour
* @param mt tens of minute
* @param mu ones of minute
* @param st tens of second
* @param su ones of second
*/
void RTC_write_time(uint8_t ht, uint8_t hu, uint8_t mt, uint8_t mu, uint8_t st, uint8_t su) {
// Calculate the new value for the time register
uint32_t tmp = 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) mt << RTC_TR_MNT_Pos;
tmp += (uint32_t) mu << RTC_TR_MNU_Pos;
tmp += (uint32_t) st << RTC_TR_ST_Pos;
tmp += (uint32_t) su << RTC_TR_SU_Pos;
// Unlock the write protection
RTC->WPR = 0xCA;
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
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
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 = 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
RTC->WPR = 0xCA;
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
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
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
Arduino
Mit dem Arduino Framework ist das Programmieren einfach, aber die Programme sind größer, langsamer, und man kann nicht alle Funktionen des Chips ausnutzen. Andererseits hindert die IDE niemanden daran, am Framework vorbei zu programieren. 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.
Links zur Dokumentation von ST und zur Dokumentation von Arduino.
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 Serial3(PB11,PB10);
HardwareSerial Serial4(PC11,PC10);
HardwareSerial Serial5(PD2,PC12);
void setup() {
Serial1.begin(115200);
Serial2.begin(115200);
Serial3.begin(115200);
Serial4.begin(115200);
Serial5.begin(115200);
}
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 kann man mit dem seriellen Monitor oder einem Terminal-Programm anzeigen, indem man damit den virtuellen COM Port (VCP) öffnet. Die Baudrate wird ignoriert. Im Terminalprogramm muss das DTR Signal eingeschaltet sein (deswegen geht es hier nicht einfach mit dem cat Befehl). Wenn der PC die Zeichen nicht abholt, wird nach ein paar Sekunden der Puffer voll, und dann bleibt der Mikrocontroller in einer Warteschleife hängen (die LED hört auf zu blinken).



