STM32F3 Anleitung

Dies ist der F3 spezifische Teil meiner STM32 Anleitungen.

Modellreihen

Die STM32F3 Serie hat einen ARM Cortex M3 Kern mit FPU bis 72 MHz.
STM32F301 Access line
mit 12bit ADC, DAC und OP-AMP
STM32F302 USB & CAN line
mit 12bit ADC, USB, CAN, DAC und OP-AMP
STM32F303 Performance line
mehr analoge Features und verbesserte Performance, mit 12bit ADC, USB, CAN, DAC und OP-AMP
STM32F334 Digital Power line
besonders schnelle hoch auflösende Timer, 12bit ADC, CAN, DAC und OP-AMP
STM32F373 Precision Measurement line
mit 16 Bit sigma-delta ADC, USB, CAN, DAC und OP-AMP

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 16 kB 32 kB 64 kB 128 kB 256 kB 384 kB 512 kB
Model ↱ x4 x6 x8 xB xC xD xE
STM32F301 Datasheet, Errata Reference Manual
STM32F302 Datasheet, Errata Datasheet, Errata Datasheet, Errata Reference Manual
STM32F303 Datasheet, Errata Datasheet, Errata Datasheet, Errata Reference Manual
STM32F334 Datasheet, Errata Reference Manual
STM32F373 Datasheet, Errata Reference Manual

Weiter führende Doku:

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 deutlich höher. Für langfristigen Batteriebetrieb wird daher auf die sparsame L0 Serie verwiesen.

Viele 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.

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:

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 €.

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 €.

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.

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.

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.

Die Firmware installiert man wahlweise über USART1, USB oder über SWD mit einem ST-Link Adapter.

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.

Schaltplan

Beispielprogramm

Beispiel für einen einfachen LED-Blinker an PA5 und PC13 auf Basis der CMSIS:

#include <stdint.h>
#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 + 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
        WRITE_REG(GPIOA->BSRR, GPIO_BSRR_BS_5);
        WRITE_REG(GPIOC->BSRR, GPIO_BSRR_BS_13);
        delay(500);

        // Reset LED pin to LOW
        WRITE_REG(GPIOA->BSRR, GPIO_BSRR_BR_5);
        WRITE_REG(GPIOC->BSRR, GPIO_BSRR_BR_13);
        delay(500);
    }
}

Ich weiß dass man Delays besser mit einem Timer realisiert. Hier wollte ich jedoch ein möglichst simples Programmbeispiel zeigen.

Fließkomma-Einheit

Float hat ungefähr 6 genaue Stellen, während double Zahlen ungefähr 15 genaue Stellen haben.

Die FPU des Mikrocontrollers beschleunigt float Operationen um Faktor 10, das ergibt bei 72 MHz etwa 4500 Multiplikationen pro Millisekunde. Double Berechnungen muss die CPU jedoch ohne Hilfe der FPU berechnen, damit schafft sie nur 330 Multiplikationen pro Millisekunde.

Beachte dass Fließkomma-Literale standardmäßig double sind. Float Zahlen schreibt man mit dem Suffix "f", zum Beispiel 3.14159f. Beachte auch, dass die normalen Funktionen der <math.h> Bibliothek auf double basieren. Für float musst du die "f" Versionen nehmen, z.B. sinf() anstatt sin().

Funktionen mit Fließkomma Operationen greifen schon beim Eintritt auf die FPU zu. Daher muss die FPU schon vorher eingeschaltet sein, sonst bricht das Programm mit einer HardFault Exception ab. Am Besten erledigt man das in SystemInit(), weil diese Funktion vor main() ausgeführt wird.

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:

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. Das Programm stürzt damit schon vor Ausführung der main() Funktion ab.

Wenn man die FPU innerhalb von Interrupt-Routinen verwendet, müssen deren Register auf den Stack gesichert werden. Die FPU macht das automatisch, da nach einem Reset die dazu notwendigen Bits (LSPEN und ASPEN im Register FPU->FPCCR) bereits gesetzt sind (siehe Programming Manual).

Die folgenden Linker-Optionen aktivieren die Unterstützung von float in printf() und scanf():

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.

Trace Meldungen ausgeben

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

SWO wird mit dem ST-Link Adapter aktiviert und empfangen. Dann ist PB3 vorübergehend ein Ausgang mit Ruhe-Pegel High. Während der seriellen Datenübertragung liefert er Low-Impulse.

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

#include <stdint.h>
#include "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();
    }
}

// Output a trace message
void ITM_SendString(char *ptr) {
    while (*ptr) {
        ITM_SendChar(*ptr);
        ptr++;
    }
}

int main() {
    while (1) {
        ITM_SendString("Hello World!\n");
        delay(500);
    }
}

Siehe auch meine Hinweise zur Cube IDE, wie man die Meldungen damit anzeigt.

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 I2C
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

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:

USB-UART Adapter

Folgende Verbindungen sind nötig:

PC USB-UART STM32F3 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 meistens ausreichend genau ist. Der Bootloader erkennt die Baudrate automatisch. Es werden 8 Datenbits und gerade Parität (even) verwendet.

USB Bootloader

Der USB Bootloader funktioniert nur, wenn ein Quarz mit 24, 18, 16, 12, 9, 8, 6, 4 oder 3 MHz angeschlossen ist. An PA12 gehört ein 1,5 kΩ Pull-Up Widerstand nach 3,3V.

Um den USB Bootloader unter Linux nutzen zu können, muss man die Software von ST mit "sudo" starten. Weitere Anwendungshinweise findest du in der Application Note AN2606.

Unterbrechungen

Interrupt-Vektoren

Hinter den "ARM Processor Exceptions" in der Interrupt-Vektor Tabelle folgen Einträge, die für das STM32 Modell spezifisch sind:

Address CMSIS Interrupt Nr. ISR Handler Function Description
0x0040 0 WWDG_IRQHandler() Window Watchdog
0x0044 1 PVD_IRQHandler() PVD through EXTI Line detection
0x0048 2 TAMP_STAMP_IRQHandler() Tamper and TimeStamp through EXTI Line 19
0x004C 3 RTC_WKUP_IRQHandler() RTC wakeup timer through EXTI Line 20
0x0050 4 FLASH_IRQHandler() Flash
0x0054 5 RCC_IRQHandler() RCC
0x0058 6 EXTI0_IRQHandler() EXTI Line 0
0x005C 7 EXTI1_IRQHandler() EXTI Line 1
0x0060 8 EXTI2_TSC_IRQHandler() EXTI Line 2 and Touch sensing
0x0064 9 EXTI3_IRQHandler() EXTI Line 3
0x0068 10 EXTI4_IRQHandler() EXTI Line 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/CAN_TX
0x0090 20 USB_LP_CAN_RX0_IRQHandler() USB Low Priority/CAN_RX0
0x0094 21 CAN_RX1_IRQHandler() CAN_RX1
0x0098 22 CAN_SCE_IRQHandler() CAN_SCE
0x009C 23 EXTI9_5_IRQHandler() EXTI Line[9:5]
0x00A0 24 TIM1_BRK_TIM15_IRQHandler() TIM1 Break/TIM15
0x00A4 25 TIM1_UP_TIM16_IRQHandler() TIM1 Update/TIM16
0x00A8 26 TIM1_TRG_COM_TIM17_IRQHandler() TIM1 trigger and commutation/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 and EXTI Line 23
0x00C0 32 I2C1_ER_IRQHandler() I2C1 error
0x00C4 33 I2C2_EV_EXTI24_IRQHandler() I2C2 event and EXTI Line 24
0x00C8 34 I2C2_ER_IRQHandler() I2C2 error
0x00CC 35 SPI1_IRQHandler() SPI1
0x00D0 36 SPI2_IRQHandler() SPI2
0x00D4 37 USART1_EXTI25_IRQHandler() USART1 and EXTI Line 25
0x00D8 38 USART2_EXTI26_IRQHandler() USART2 and EXTI Line 26
0x00DC 39 USART3_EXTI28_IRQHandler() USART3 and EXTI Line 28
0x00E0 40 EXTI15_10_IRQHandler() EXTI Line[15:10]
0x00E4 41 RTCAlarm_IRQHandler() RTC alarm
0x00E8 42 USB_WKUP_IRQHandler() USB wakeup from Suspend through EXTI line 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
0x010C 51 SPI3_IRQHandler() SPI3
0x0110 52 UART4_EXTI34_IRQHandler() UART4 and EXTI Line 34
0x0114 53 UART5_EXTI35_IRQHandler() UART5 and EXTI Line 35
0x0118 54 TIM6_DACUNDER_IRQHandler() TIM6 and DAC1 underrun
0x011C 55 TIM7_IRQHandler() TIM7
0x0120 56 DMA2_CH1_IRQHandler() DMA2 channel1
0x0124 57 DMA2_CH2_IRQHandler() DMA2 channel2
0x0128 58 DMA2_CH3_IRQHandler() DMA2 channel3
0x012C 59 DMA2_CH4_IRQHandler() DMA2 channel4
0x0130 60 DMA2_CH5_IRQHandler() DMA2 channel5
0x0134 61 ADC4_IRQHandler() ADC4
0x0138 62 reserved
0x013C 63
0x0140 64 COMP123_IRQHandler() COMP1, COMP2 and COMP3 combined with EXTI Lines 21, 22 and 29
0x0144 65 COMP456_IRQHandler() COMP4, COMP5 and COMP6 combined with EXTI Lines 30, 31 and 32
0x0148 66 COMP7_IRQHandler() COMP7 combined with EXTI Line 33
0x014C 67 reserved
0x0150 68
0x0154 69
0x0158 70
0x015C 71
0x0160 72 I2C3_EV_IRQHandler() I2C3 Event
0x0164 73 I2C3_ER_IRQHandler() I2C3 Error
0x0168 74 USB_HP_IRQHandler() alternative USB High priority (see SYSCGFG->CFGR1 Bit 5 USB_IT_RMP)
0x016C 75 USB_LP_IRQHandler() alternative USB Low priority (see SYSCGFG->CFGR1 Bit 5 USB_IT_RMP)
0x0170 76 USB_WKUP_EXTI_IRQHandler() alternative USB wake up from Suspend through EXTI line 18 (see SYSCGFG->CFGR1 Bit 5 USB_IT_RMP)
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
0x0190 84 SPI4_IRQHandler() SPI4 SPI4 Global

Beim STM32F334 gilt jedoch abweichend:

Address CMSIS Interrupt Nr. ISR Handler Function Description
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 combined with EXTI Line 22
0x0144 65 COMP4_6_IRQHandler() COMP4 and COMP6 combined with EXTI Lines 30 and 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. 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.

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 <stdio.h>
#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 clock of I/O features
    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

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() (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:

Für Watchog und Echtzeit-Uhr (RTC) sind weitere Quellen vorgesehen:

Mehrere Vorteiler und ein PLL Multiplikator können kombiniert werden, um unterschiedliche Taktfrequenzen zu erreichen. Am besten schaut man sich das mal in Cube MX an. Dort kann man schön sehen, welche Parameter konfigurierbar sind und wie sie zusammen wirken.

Das folgende Bild zeigt die Standardvorgabe vom 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:

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
    WRITE_REG(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;
}
Die obige Delay Schleife läuft danach allerdings nicht 8x schneller, sondern nur 6x schneller. Der Grund dafür ist, dass der Flash jetzt mit 2 Waitstates betrieben werden muss und der Prefetch-Buffer (der dies ausgleicht) nur direkt aufeinander folgende Befehle optimiert. Bei jeden Rücksprung in der Schleife wird der Prefetch-Buffer geleert.

Durch die Angabe __attribute__((section(".data"))) könnte man Funktionen etwas schneller ohne Waitstate aus dem RAM ausführen, da dieser ohne Waitstates arbeitet.

Beispiel für 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
    WRITE_REG(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 können erst benutzt werden, nachdem man ihre Taktversorgung 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 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.

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 2, 10 oder 50 MHz einstellen. Damit beeinflusst man die Geschwindigkeit, mit der die Spannung von Low nach High (und zurück) wechselt. Der maximale Laststrom wird dadurch nicht verändert. Im Sinne von elektromagnetischer Verträglichkeit soll man hier immer den niedrigsten Wert einstellen, der zur Anwendung passt.

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" tabellarisch beschrieben.

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 clock for 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_ms(2);

    // ADC Clock = HCLK/4
    MODIFY_REG(ADC12_COMMON->CCR, ADC12_CCR_CKMODE, 0b11 << ADC12_CCR_CKMODE_Pos);

    // Single ended mode for all channels
    WRITE_REG(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 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 "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
    SET_BIT(RCC->AHBENR, RCC_AHBENR_GPIOAEN);

    // PA5 = TIM2_CH1 alternate function 1 (see data sheet)
    MODIFY_REG(GPIOA->AFR[0], GPIO_AFRL_AFRL5,  1    << GPIO_AFRL_AFRL5_Pos);
    MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER5, 0b10 << GPIO_MODER_MODER5_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 = 9; // divide clock by 10

    // Timer 2 auto reload register, defines the maximum value of the counter in PWM mode.
    TIM2->ARR = 32768; // 8000000/10/32768 = 27 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.

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

Der interne HSI Oszillator ist häufig aber nicht immer stabil genug, um die USART Schnittstellen zu betreiben. Es empfiehlt sich daher, auf eine externe Quelle (HSE) umzuschalten. In den folgenden Beispielen nutze ich der Einfachheit halber trotzdem den internen HSI Oszillator.

Je nach Taktfrequenz der Peripherie sind unterschiedliche 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 "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++) {
        while(!(USART1->ISR & USART_ISR_TXE));
        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 clock for Port A
    SET_BIT(RCC->AHBENR, RCC_AHBENR_GPIOAEN);

    // PA5 = Output for the LED
    MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER5, 0b01 << GPIO_MODER_MODER5_Pos);

    // Use system clock for USART1
    SET_BIT(RCC->APB2ENR, RCC_APB2ENR_USART1EN);
    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->AFR[1], GPIO_AFRH_AFRH1,  7    << GPIO_AFRH_AFRH1_Pos);
    MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER9, 0b10 << GPIO_MODER_MODER9_Pos);

    // PA10 (RxD) shall use the alternate function 7 (see data sheet)
    MODIFY_REG(GPIOA->AFR[1], GPIO_AFRH_AFRH2,   7    << GPIO_AFRH_AFRH2_Pos);
    MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER10, 0b10 << GPIO_MODER_MODER10_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
        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 "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++) {
        while(!(USART2->ISR & USART_ISR_TXE));
        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 clock for Port A
    SET_BIT(RCC->AHBENR, RCC_AHBENR_GPIOAEN);

    // PA5 = Output for the LED
    MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER5, 0b01 << GPIO_MODER_MODER5_Pos);

    // Use system clock for USART2
    SET_BIT(RCC->APB1ENR, RCC_APB1ENR_USART2EN);
    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->AFR[0], GPIO_AFRL_AFRL2,  7    << GPIO_AFRL_AFRL2_Pos);
    MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER2, 0b10 << GPIO_MODER_MODER2_Pos);

    // PA3 (RxD) shall use the alternate function 7 (see data sheet)
    MODIFY_REG(GPIOA->AFR[0], GPIO_AFRL_AFRL3,  7    << GPIO_AFRL_AFRL3_Pos);
    MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER3, 0b10 << GPIO_MODER_MODER3_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
        WRITE_REG(GPIOA->BSRR, GPIO_BSRR_BS_5);
        delay(500);

        puts("Hello");

        // LED aus
        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²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->AFR[1], GPIO_AFRH_AFRH1,  4    << GPIO_AFRH_AFRH1_Pos);
    MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER9, 0b10 << GPIO_MODER_MODER9_Pos);
    SET_BIT(GPIOA->OTYPER, GPIO_OTYPER_OT_9);

    // I2C2 PA10=SDA, alternate function 4 open-drain
    MODIFY_REG(GPIOA->AFR[1], GPIO_AFRH_AFRH2,   4    << GPIO_AFRH_AFRH2_Pos);
    MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER10, 0b10 << GPIO_MODER_MODER10_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) {
    // Enable clock for the I2C 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 I2C 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 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     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)) {
                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 byte 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);
    //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.

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:

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 Cube IDE kann man sich ein Projekt mit USB Unterstützung zusammenklicken. Das geht so:

Probiere das Programm zunächst ohne Änderungen aus. Der PC sollte im Gerätemanager einen neuen virtuellen COM Anschluss mit dem Namen "Serielles USB-Gerät (COMx)" oder "STMicroelectronics Virtual COM Port (COMx)" anzeigen.

Um Text vom Mikrocontroller an den PC zu senden, füge im Quelltext von main.c folgendes zwischen die genannten Markierungen ein:

/* USER CODE BEGIN Includes */

#include "usbd_cdc_if.h"

/* USER CODE END Includes */

...

  /* USER CODE BEGIN WHILE */
  while (1) {
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */

        // LED On
        HAL_GPIO_WritePin(LED_GPIO_Port,LED_Pin,GPIO_PIN_SET);
        HAL_Delay(500);

        // LED Off
        HAL_GPIO_WritePin(LED_GPIO_Port,LED_Pin,GPIO_PIN_RESET);
        HAL_Delay(500);

        // Send data
        char msg[]="Hallo!";
        CDC_Transmit_FS( (uint8_t*) msg, strlen(msg));

  }
  /* USER CODE END 3 */

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:

// Use 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

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. Auf dem PC braucht man dazu keine Treiber-Installation, im Fall von Windows ist mindestens Version 8 erforderlich.

Die Projekte wurden mit der "STM32 Cube IDE" erstellt. Ich gehe davon aus, daß der Code auf allen STM32F3 Modellen (mit USB) läuft. Die Initialisierung in der main.c und ein paar Einstellungen in den ersten Zeilen der usb.c müssen aber ans jeweilige Modell angepasst werden.

Das folgende Programm lässt die LED 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 "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_ms(int ms) {
    uint32_t start=systick_count;
    while (systick_count-start<ms);
}

// Change system clock to 48Mhz using 8Mhz crystal
void init_clock() {
    // Because the debugger switches PLL on, we may need to switch
    // back to the HSI oscillator before we can configure the PLL

    // Enable HSI oscillator
    SET_BIT(RCC->CR, RCC_CR_HSION);

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

    // Switch to HSI oscillator
    MODIFY_REG(RCC->CFGR, RCC_CFGR_SW, RCC_CFGR_SW_HSI);

    // Wait until the switch is done
    while ((RCC->CFGR & RCC_CFGR_SWS_Msk) != RCC_CFGR_SWS_HSI) {}

    // Disable the PLL
    CLEAR_BIT(RCC->CR, RCC_CR_PLLON);

    // Wait until PLL is fully stopped
    while(READ_BIT(RCC->CR, RCC_CR_PLLRDY)) {}

    // Flash latency 1 wait state
    MODIFY_REG(FLASH->ACR, FLASH_ACR_LATENCY, 1 << FLASH_ACR_LATENCY_Pos);

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

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

    // 48Mhz using the 8Mhz HSE oscillator with 6x PLL, lowspeed I/O runs at 24Mhz
    WRITE_REG(RCC->CFGR, RCC_CFGR_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;

    // 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 + 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->AFR[1], GPIO_AFRH_AFRH3,   14   << GPIO_AFRH_AFRH3_Pos);
    MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER11, 0b10 << GPIO_MODER_MODER11_Pos);

    // PA12 = USB D+, alternate function 14 push/pull (see data sheet)
    MODIFY_REG(GPIOA->AFR[1], GPIO_AFRH_AFRH4,   14   << GPIO_AFRH_AFRH4_Pos);
    MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER12, 0b10 << GPIO_MODER_MODER12_Pos);
}

int main() {
    init_clock();
    init_io();
    UsbSetup();

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

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

        UsbSendStr("Hello World!\n",10);

        // LED Off (High)
        WRITE_REG(GPIOC->BSRR, GPIO_BSRR_BS_13);
        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 je nach Modell 5 oder 16 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->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.

Das folgende ausführbare Beispiel zeigt, wie man damit die LED auf dem Nucleo-F303RE Board im Sekundentakt blinken lässt:

#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++) { 
        while(!(USART2->ISR & USART_ISR_TXE));
        USART2->TDR = *ptr++;
    }
    return len;
}

void init_io() {
    // Enable Port A
    SET_BIT(RCC->AHBENR, RCC_AHBENR_GPIOAEN);

    // 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->AFR[0], GPIO_AFRL_AFRL2,  7U   << GPIO_AFRL_AFRL2_Pos);
    MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODER2, 0b10 << GPIO_MODER_MODER2_Pos);
}

void initSerial() {
    // Use system clock for USART2
    SET_BIT(RCC->APB1ENR, RCC_APB1ENR_USART2EN);
    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
    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_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 NVIC
}

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 NVIC

    // Toggle LED
    GPIOA->ODR ^= GPIO_ODR_5;
}


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 Kapitel "External and internal interrupt/event line mapping".

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 mt= (RTC->TR & RTC_TR_MNT) >> RTC_TR_MNT_Pos;
        uint8_t mu= (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, mt,mu, 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:

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=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) 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
    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
Es bleibt eine Ungenauigkeit von 0,028 Sekunden pro Tag übrig.

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:

Damit hast du Zugriff auf das Arduino Framework, sowie die CMSIS und HAL Bibliotheken von ST.

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: Für die übrigen seriellen Ports muss man Instanzen von HardwareSerial anlegen. Kopiervorlage:
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 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.

STM32 Anleitungen