STM32C5 Anleitung

Dies ist der C5 spezifische Teil meiner STM32 Anleitungen.

Die STM32C5 Mikrocontroller haben einen ARM Cortex M33F Kern bis 144 MHz. Neben den üblichen ADC und DAC sind auch Operationsverstärker, Komparatoren und ein Delay Block vorhanden. Mit DSP Befehlen, einer FPU und dem CORDIC Koprozessor lassen sich die Signale schnell berechnen. Drei Modelle haben auch Ethernet.

Die STM32C5 Serie wurde 2026 als preisgünstige Alternative zu chinesischen Konkurrenten beworben. Sie kommt zusammen mit einer neuen HAL2, sowie dem neuen Cube MX2. Beide sind nicht abwärts-kompatibel. Arduino unterstützt die STM32C5 noch nicht (Stand Juni 2026, siehe Supported boards).

Wenn du mehr analoge Funktionen, einen FMAC oder USB-PD brauchst, dann schau dir die STM32G4 Serie an.

Modelle

Size xB xC xE xG x:
F = 20 Pins
E = 24 Pins
K = 32 Pins
C = 48 Pins
R = 64 Pins
M = 80 Pins
V = 100 Pins
Z = 144 Pins
Flash 128 KiB 256 KiB 512 KiB 1024 KiB
RAM ↴ 64 KiB 64 KiB
STM32C531 Datasheet Errata
STM32C5322
STM32C5422,3 Datasheet
RAM ↴ 128 KiB 128 KiB Errata
STM32C551 Datasheet
STM32C5521
STM32C5621,3 Datasheet
RAM ↴ 256 KiB 256 KiB Errata
STM32C5914 Datasheet
STM32C5932,4
STM32C5A32,3,4 Datasheet
all Reference Manual
  1. mit 1·CAN Bus
  2. mit 2·CAN Bus
  3. mit AES Verschlüsselung 128 und 256 Bit
  4. mit Ethernet

Die Pinbelegung und elektrischen Daten stehen im jeweiligen Datenblatt. Für den Programmierer ist das Reference Manual am wichtigsten, da es die interne Peripherie und Register beschreibt. Im Errata Sheet beschreibt der Hersteller überraschende Einschränkungen und Fehler der Mikrochips, teilweise mit konkreten Workarounds.

Weiter führende Doku:

Elektrische Daten

Alle hier gezeigten STM32C5 Chips kann man mit 2,7 bis 3,6 Volt betreiben. Die USB Schnittstelle braucht 3,3 V.

Der VCAP Pin muss mit einem 2,2 µF Kondensator beschaltet werden, um die interne Versorgungsspannung der CPU zu stabilisieren.

Die STM32C5 haben keinen separaten Batterieanschluss (VBAT) für die RTC!

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 20 mA und alle zusammen mit 140 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.

Ausnahme:

Pegelwechsel an PC13 können bei Modellen mit LQFP Gehäuse die Genauigkeit der RTC beeinträchtigen.

Boards

Nucleo-C542RC

Nucleo-C542RC

Das Nucleo-C542RC Board (aus der Nucleo-64 Reihe) ist ein hochwertiges Starter-Set zum günstigen Preis um 20 €.

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.

WeAct STM32C552VET6 100-Pin Core Board

WeAct STM32C552VET6 100-Pin Core Board

Das WeAct STM32C552VET6 100-Pin Core Board wird bei Aliexpress ab 6 € angeboten.

Der Spannungsregler kann leicht überhitzen wenn man ihn mit zusätzlichen Verbrauchern belastet.

Pinbelegung, Schaltplan

WeAct STM32C552CET6 48-Pin Core Board

WeAct STM32C552CET6 48-Pin Core Board

Auf Github habe habe ich Unterlagen zum WeAct STM32C552CET6 48-Pin Core Board gefunden. Es hat ungefähr das gleiche Format, wie das Blue Pill Board.

Der Spannungsregler kann leicht überhitzen wenn man ihn mit zusätzlichen Verbrauchern belastet.

Pinbelegung, Schaltplan

Beispielprogramm

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

#include "stm32c5xx.h"

// delay loop for the default 48 MHz
void delay(uint32_t msec) {
    for (uint32_t j=0; j < msec * 9600; j++) {
        __NOP();
    }
}

int main() {
    // Enable Port A and H
    STM32_SET_BIT(RCC->AHB2ENR, RCC_AHB2ENR_GPIOAEN);
    STM32_SET_BIT(RCC->AHB2ENR, RCC_AHB2ENR_GPIOHEN);

    // PA5 and PH15 = Output for LEDs
    STM32_MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE5,  0b01 << GPIO_MODER_MODE5_Pos);
    STM32_MODIFY_REG(GPIOH->MODER, GPIO_MODER_MODE15, 0b01 << GPIO_MODER_MODE15_Pos);

    while(1) {
        // Set LED pin to HIGH
        GPIOA->BSRR = GPIO_BSRR_BS5;
        GPIOH->BSRR = GPIO_BSRR_BS15;
        delay(500);

        // Reset LED pin to LOW
        GPIOA->BSRR = GPIO_BSRR_BR5;
        GPIOH->BSRR = GPIO_BSRR_BR15;
        delay(500);        
    }
}
Ich weiss dass man Delays besser mit einem Timer realisiert (wie dort). Hier wollte ich jedoch ein möglichst einfaches Programmbeispiel zeigen.

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, sofern nicht auch die SWD Pins deaktiviert wurden.

Boot Loader

Der Bootloader unterstützt folgende Anschlüsse:
Modell USART USART USART USART SPI SPI SPI USB CAN
TxD,RxD TxD,RxD TxD,RxD TxD,RxD NSS,SCK,MISO,MOSI NSS,SCK,MISO,MOSI NSS,SCK,MISO,MOSI D-,D+ Rx,Tx
STM32C531,532,542 PA9,10 PA2,3 PA0,1 20 und 24 Pins: PA15,PB3,PB4,PA7

ab 32 Pins: PA4-7
PB12-15 PA11,PA12 PB5,PB6
STM32C551,552,562 PA9,10 PA2,3 PD8,9 PA0,1 PA4-7 PB12-15 PB8,1,0,2 PA11,PA12 PB5,PB6
STM32F591,593,5A3 PA9,10 PA2,3 PD8,9 PA0,1 PA4-7 PB12-15 PB8,1,0,2 PA11,PA12 PB5,PB6
Der Bootloader benötigt keinen Quarz und keinen Pull-Up Widerstand am USB Port.

Der externe HSE Oszillator wird nur verwendet, wenn FDCAN in ENGI und Option Bytes eingeschaltet ist. In diesem Fall ein Quarz mit 12, 24 oder 48 MHz angeschlossen sein. Wenn der Oszillator nicht funktioniert, fällte der Bootloader automatisch auf den internen Oszillator zurück.

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:

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:

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

CORDIC

Der "Coordinate Rotation Digital Computer" (CORDIC) berechnet

viel schneller als die CPU. Der C/C++ Compiler kann den CORDIC allerdings nicht direkt nutzen. In der Application Note AN5325 wird beschrieben, wie man ihn mit den LL oder HAL Bibliotheken verwendet. Die direkte Programmierung über Register lässt sich gut von der LL Bibliothek abschauen und ist im Reference Manual Kapitel "CORDIC coprocessor" beschrieben. DMA wird unterstützt, um die selbe Operation auf mehrere Eingabewerte anzuwenden.

Der CORDIC verwendet Festpunkt-Zahlen in den einfachen Q-Formaten:

In beiden Formaten geht der Wertebereich von -1 bis annähernd +1. Auf der Webseite Q-format Converter & Calculator kann man das Format umrechnen.

CRC

Zur schnellen Berechnung von CRC Prüfsummen enthält der Mikrocontroller eine eigene Peripherie, die 60 mal schneller ist, als die CPU. Sie wird in der Application Note AN4187 vorgestellt. Das Reference Manual beschreibt die Programmierung über Register im Kapitel "Cyclic redundancy check calculation unit". Die CRC Einheit kann mit DMA gefüttert werden.

AES

Zur Verschlüsselung von Datenströmen haben die STM32C542, STM32C562 und STM32C5A3 einen AES Beschleuniger. Er wird in dieser Präsentation vom STM32G4 vorgestellt, ich glaube das passt auch zum STM32C5. Die direkte Programmierung über Register ist im Reference Manual Kapitel "AES hardware accelerator" beschrieben. Der AES Beschleuniger kann seinen Datenstrom mit DMA einlesen und ausgeben.

Unterbrechungen

Interrupt-Vektoren

⚠️ Die STM32 Cube IDE (2.2.0) erzeugt eine fehlerhafte Datei "startup_stm32c5xxxx.s" Die USB Funktion hat einen falschen Namen und einige Einträge fehlen (Stand 08.07.2026).

Hinter den ARM Processor Exceptions enthält die Interrupt Vektor Tabelle der STM32C5 folgende Einträge:

Address CMSIS Interrupt Nr. C Function Description EXTI Channel
0x0040 0 WWDG_IRQHandler() Window Watchdog
0x0044 1 PWR_PVD_IRQHandler() Power voltage monitor 16
0x0048 2 RTC_IRQHandler() RTC through EXTI line 17
0x004C 3 TAMP_IRQHandler() Tamper through EXTI line 18
0x0050 4 RAMCFG_IRQHandler() RAM configuration
0x0054 5 FLASH_IRQHandler() Flash
0x0058 6 RCC_IRQHandler() RCC
0x005C 7 EXTI0_IRQHandler() EXTI Line 0 0
0x0060 8 EXTI1_IRQHandler() EXTI Line 1 1
0x0064 9 EXTI2_IRQHandler() EXTI Line 2 2
0x0068 10 EXTI3_IRQHandler() EXTI Line 3 3
0x006C 11 EXTI4_IRQHandler() EXTI Line 4 4
0x0070 12 EXTI5_IRQHandler() EXTI Line 5 5
0x0074 13 EXTI6_IRQHandler() EXTI Line 6 6
0x0078 14 EXTI7_IRQHandler() EXTI Line 7 7
0x007C 15 EXTI8_IRQHandler() EXTI Line 8 8
0x0080 16 EXTI9_IRQHandler() EXTI Line 9 9
0x0084 17 EXTI10_IRQHandler() EXTI Line 10 10
0x0088 18 EXTI11_IRQHandler() EXTI Line 11 11
0x008C 19 EXTI12_IRQHandler() EXTI Line 12 12
0x0090 20 EXTI13_IRQHandler() EXTI Line 13 13
0x0094 21 EXTI14_IRQHandler() EXTI Line 14 14
0x0098 22 EXTI15_IRQHandler() EXTI Line 15 15
0x009C 23 LPDMA1_CH0_IRQHandler() LPDMA1 channel 0
0x00A0 24 LPDMA1_CH1_IRQHandler() LPDMA1 channel 1
0x00A4 25 LPDMA1_CH2_IRQHandler() LPDMA1 channel 2
0x00A8 26 LPDMA1_CH3_IRQHandler() LPDMA1 channel 3
0x00AC 27 LPDMA1_CH4_IRQHandler() LPDMA1 channel 4
0x00B0 28 LPDMA1_CH5_IRQHandler() LPDMA1 channel 5
0x00B4 29 LPDMA1_CH6_IRQHandler() LPDMA1 channel 6
0x00B8 30 LPDMA1_CH7_IRQHandler() LPDMA1 channel 7
0x00BC 31 IWDG_IRQHandler() Independent watchdog through EXTI line 35
0x00C0 32 ADC1_IRQHandler() ADC1
0x00C4 33 ADC2_IRQHandler() ADC2
0x00C8 34 FDCAN1_IT0_IRQHandler() FDCAN1 Interrupt 0
0x00CC 35 FDCAN1_IT1_IRQHandler() FDCAN1 Interrupt 1
0x00D0 36 TIM1_BRK_TERR_IERR_IRQHandler() TIM1 break, TIM1 transition error, TIM1 index error
0x00D4 37 TIM1_UPD_IRQHandler() TIM1 update
0x00D8 38 TIM1_TRGI_COM_DIR_IDX_IRQHandler() TIM1 trigger and commutation, TIM1 direction change, TIM1 index error
0x00DC 39 TIM1_CC_IRQHandler() TIM1 capture compare
0x00E0 40 TIM2_IRQHandler() TIM2
0x00E4 41 TIM5_IRQHandler() TIM5
0x00E8 42 TIM6_IRQHandler() TIM6
0x00EC 43 TIM7_IRQHandler() TIM7
0x00F0 44 I2C1_EV_IRQHandler() I2C1 event 20
0x00F4 45 I2C1_ERR_IRQHandler() I2C1 error
0x00F8 46 I3C1_EV_IRQHandler() I3C1 event 21
0x00FC 47 I3C1_ERR_IRQHandler() I3C1 error
0x0100 48 SPI1_IRQHandler() SPI1 22
0x0104 49 SPI2_IRQHandler() SPI2 23
0x0108 50 SPI3_IRQHandler() SPI3 24
0x010C 51 USART1_IRQHandler() USART1 25
0x0110 52 USART2_IRQHandler() USART2 26
0x0114 53 USART3_IRQHandler() USART3 27
0x0118 54 UART4_IRQHandler() UART4 28
0x011C 55 UART5_IRQHandler() UART5 29
0x0120 56 LPUART1_IRQHandler() LPUART1 30
0x0124 57 LPTIM1_IRQHandler() LPTIM1 through EXTI line 31
0x0128 58 TIM12_IRQHandler() TIM12
0x012C 59 TIM15_IRQHandler() TIM15
0x0130 60 TIM16_IRQHandler() TIM16
0x0134 61 TIM17_IRQHandler() TIM17
0x0138 62 USB_DRD_FS_IRQHandler() USB FS 32
0x013C 63 CRS_IRQHandler() Clock recovery system
0x0140 64 RNG_IRQHandler() RNG
0x0144 65 FPU_IRQHandler() Floating point
0x0148 66 ICACHE_IRQHandler() Instruction cache
0x014C 67 CORDIC_IRQHandler() CORDIC
0x0150 68 AES_IRQHandler() AES
0x0154 69 HASH_IRQHandler() HASH
0x0158 70 I2C2_EV_IRQHandler() I2C2 event 33
0x015C 71 I2C2_ERR_IRQHandler() I2C2 error
0x0160 72 TIM8_BRK_TERR_IERR_IRQHandler() TIM8 break, TIM8 transition error, TIM8 index error
0x0164 73 TIM8_UPD_IRQHandler() TIM8 update
0x0168 74 TIM8_TRGI_COM_DIR_IDX_IRQHandler() TIM8 trigger and commutation, TIM8 direction change, TIM8 index
0x016C 75 TIM8_CC_IRQHandler() TIM8 capture compare
0x0170 76 COMP1_IRQHandler() COMP1, COMP1 through EXTI line 34
0x0174 77 DAC1_IRQHandler() DAC1
0x0178 78 LPDMA2_CH0_IRQHandler() LPDMA2 channel 0
0x017C 79 LPDMA2_CH1_IRQHandler() LPDMA2 channel 1
0x0180 80 LPDMA2_CH2_IRQHandler() LPDMA2 channel 2
0x0184 81 LPDMA2_CH3_IRQHandler() LPDMA2 channel 3
0x0188 82 LPDMA2_CH4_IRQHandler() LPDMA2 channel 4
0x018C 83 LPDMA2_CH5_IRQHandler() LPDMA2 channel 5
0x0190 84 LPDMA2_CH6_IRQHandler() LPDMA2 channel 6
0x0194 85 LPDMA2_CH7_IRQHandler() LPDMA2 channel 7
0x0198 86 FDCAN2_IT0_IRQHandler() FDCAN2 Interrupt 0
0x019C 87 FDCAN2_IT1_IRQHandler() FDCAN2 Interrupt 1
0x01A0 88 COMP2_IRQHandler() COMP2 through EXTI line 36
0x01A4 89 TIM3_IRQHandler() TIM3
0x01A8 90 TIM4_IRQHandler() TIM4
0x01AC 91 XSPI1_IRQHandler() XSPI1
0x01B0 92 SAES_IRQHandler() SAES
0x01B4 93 PKA_IRQHandler() PKA
0x01B8 94 ETH1_IRQHandler() Ethernet
0x01BC 95 ETH1_WKUP_IRQHandler() Ethernet wakeup through EXTI line 39
0x01C0 96 USART6_IRQHandler() USART6 37
0x01C4 97 UART7_IRQHandler() UART7 38
0x01C8 98 ADC3_IRQHandler() ADC3

Extended Interrupts

Die externen Interrupt-Signale und auch einige interne durchlaufen einen erweiterten Schaltkreis, der zusätzliche Konfiguration erfordert. Sie sind im Referenzhandbuch Tabelle "EXTI line connections" 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.

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 sind die Register EXTI->RPR1/2 und EXTI->FPR1/2 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->IMR1/2 werden Unterbrechungen (und das Aufwachen) 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.

#include "stm32c5xx.h"

// Delay loop for the default 48 MHz
void delay(uint32_t msec) {
    for (uint32_t j=0; j < msec * 9600; j++) {
        __NOP();
    }
}

void EXTI13_IRQHandler() {
    // Clear pending interrupt flag
    // It is important that this is not the last command in the ISR
    STM32_SET_BIT(EXTI->RPR1, EXTI_RPR1_RPIF13);

    // Flash the LED on PH15
    GPIOH->BSRR = GPIO_BSRR_BS15;
    delay(100);
    GPIOH->BSRR = GPIO_BSRR_BR15;
}

int main() {
    // Enable Port C and H
    STM32_SET_BIT(RCC->AHB2ENR, RCC_AHB2ENR_GPIOCEN);
    STM32_SET_BIT(RCC->AHB2ENR, RCC_AHB2ENR_GPIOHEN);

    // PH15 = Output for LEDs
    STM32_MODIFY_REG(GPIOH->MODER, GPIO_MODER_MODE15, 0b01 << GPIO_MODER_MODE15_Pos);

    // PC13 = Input with pull-down
    STM32_MODIFY_REG(GPIOC->MODER, GPIO_MODER_MODE13, 0b00 << GPIO_MODER_MODE13_Pos);
    STM32_MODIFY_REG(GPIOC->PUPDR, GPIO_PUPDR_PUPD13, 0b10 << GPIO_PUPDR_PUPD13_Pos);

    // Assign EXTI13 to PC13 with rising edge
    STM32_MODIFY_REG(EXTI->EXTICR[3], EXTI_EXTICR4_EXTI13, 0x02 << EXTI_EXTICR4_EXTI13_Pos);
    STM32_SET_BIT(EXTI->IMR1, EXTI_IMR1_IM13);
    STM32_SET_BIT(EXTI->RTSR1, EXTI_RTSR1_RT13);

    // Enable the interrupt handler call
    NVIC_EnableIRQ(EXTI13_IRQn);

    // Clear pending interrupt flag
    STM32_SET_BIT(EXTI->RPR1, EXTI_RPR1_RPIF13);

    // Endless loop
    while (1) {}
}
Anmerkung: Da der Kontakt prellt, blitzt die LED manchmal auch beim Loslassen des Tasters auf.

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->EMR1/2 das entsprechende Bit auf 1 setzen, um ein EXTI Signal als Ereignis zu behandeln.

Taktgeber

Nach einem Reset wird der Mikrocontroller automatisch mit seinem internen Oszillator getaktet. Dieser hat 144 MHz und wird durch 3 geteilt, also 48 MHz. 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 durch Setzen von Bits in den ENR Registern erledigt wird:

Jetzt kommt der komplizierte Teil. Der System-Takt (SYSCLK) kann aus folgenden Quellen bezogen werden:

Für Watchog und Echtzeit-Uhr (RTC) gibt es weitere Taktgeber:

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

Das folgende Bildschirmfoto zeigt die Standardvorgabe vom STM32C5A3ZG nach einem Reset:

STM32C5A3ZG Clock

Achtung:

Beispiel für 144 MHz aus dem internen HSI Oszillator:

#include "stm32c5xx.h"

// The current clock frequency
uint32_t SystemCoreClock=48000000;

// Change system clock to 144 MHz using internal 144 MHz oscillator
void init_clock() {

    // Flash latency 4 wait states, and highest programming delay
    STM32_MODIFY_REG(FLASH->ACR, FLASH_ACR_LATENCY, 4 << FLASH_ACR_LATENCY_Pos);
    STM32_MODIFY_REG(FLASH->ACR, FLASH_ACR_WRHIGHFREQ, 0b10 << FLASH_ACR_WRHIGHFREQ_Pos);

    // Enable HSI Oscillator
    STM32_SET_BIT(RCC->CR1, RCC_CR1_HSISON);

    // Wait until HSI oscillator is ready
    while (!STM32_READ_BIT(RCC->CR1, RCC_CR1_HSISRDY)) {}

    // Switch to HSI oscillator
    STM32_MODIFY_REG(RCC->CFGR1, RCC_CFGR1_SW, 0b01 << RCC_CFGR1_SW_Pos);

    // Wait until the switch is done
    while ((RCC->CFGR1 & RCC_CFGR1_SWS_Msk) != 0b01 << RCC_CFGR1_SWS_Pos) {}

    // Update system timer
    SystemCoreClock=144000000;
    //SysTick_Config(SystemCoreClock/8/1000); // comment out if you have no SysTick_Handler()
}

// Delay loop for 144 MHz
void delay(uint32_t msec) {
    for (uint32_t j=0; j < msec * 28800; j++) {
        __NOP();
    }
}

int main() {
    init_clock();

    // Enable Port H
    STM32_SET_BIT(RCC->AHB2ENR, RCC_AHB2ENR_GPIOHEN);

    // PH15 = Output for LEDs
    STM32_MODIFY_REG(GPIOH->MODER, GPIO_MODER_MODE15, 0b01 << GPIO_MODER_MODE15_Pos);

    // Endless loop
    while (1) {
        GPIOH->BSRR = GPIO_BSRR_BS15;
        delay(100);
        GPIOH->BSRR = GPIO_BSRR_BR15;
        delay(900);
    }
}

Beispiel für 144 MHz mit 8 MHz Quarz:

#include "stm32c5xx.h"

// The current clock frequency
uint32_t SystemCoreClock=48000000;

// Change system clock to 144 MHz using HSE oscillator with 8 MHz crystal
void init_clock() {

    // Enable HSE Oscillator
    STM32_SET_BIT(RCC->CR1, RCC_CR1_HSEON);

    // Wait until HSE oscillator is ready
    while (!STM32_READ_BIT(RCC->CR1, RCC_CR1_HSERDY)) {}

    // Switch to HSE oscillator
    STM32_MODIFY_REG(RCC->CFGR1, RCC_CFGR1_SW, 0b10 << RCC_CFGR1_SW_Pos);

    // Wait until the switch is done
    while ((RCC->CFGR1 & RCC_CFGR1_SWS_Msk) != 0b10 << RCC_CFGR1_SWS_Pos) {}

    // Disable PSI oscillator
    STM32_CLEAR_BIT(RCC->CR1, RCC_CR1_PSISON);
    STM32_CLEAR_BIT(RCC->CR1, RCC_CR1_PSIDIV3ON);
    STM32_CLEAR_BIT(RCC->CR1, RCC_CR1_PSIKON);

    // Wait until PSI oscillator is fully stopped
    while (STM32_READ_BIT(RCC->CR1, RCC_CR1_PSISRDY)) {}

    // Flash latency 4 wait states, and highest programming delay
    STM32_MODIFY_REG(FLASH->ACR, FLASH_ACR_LATENCY, 4 << FLASH_ACR_LATENCY_Pos);
    STM32_MODIFY_REG(FLASH->ACR, FLASH_ACR_WRHIGHFREQ, 0b10 << FLASH_ACR_WRHIGHFREQ_Pos);

    // Configure PSI oscillator to produce 144 MHz using 8 MHz HSE as reference
    STM32_MODIFY_REG(RCC->CR2, RCC_CR2_PSIREFSRC, 0b00   << RCC_CR2_PSIREFSRC_Pos); // use HSE as reference
    STM32_MODIFY_REG(RCC->CR2, RCC_CR2_PSIREF,    0b001  << RCC_CR2_PSIREF_Pos);    // reference has 8 MHz
    STM32_MODIFY_REG(RCC->CR2, RCC_CR2_PSIFREQ,   0b01   << RCC_CR2_PSIFREQ_Pos);   // produce 144 MHz
    STM32_MODIFY_REG(RCC->CR2, RCC_CR2_PSIKDIV,   0b0000 << RCC_CR2_PSIKDIV_Pos);   // divide by 1

    // Enable PSI oscillator
    STM32_SET_BIT(RCC->CR1, RCC_CR1_PSISON);

    // Wait until PSI oscillator is ready
    while (!STM32_READ_BIT(RCC->CR1, RCC_CR1_PSISRDY)) {}

    // Switch to PSI oscillator
    STM32_MODIFY_REG(RCC->CFGR1, RCC_CFGR1_SW, 0b11 << RCC_CFGR1_SW_Pos);

    // Wait until the switch is done
    while ((RCC->CFGR1 & RCC_CFGR1_SWS_Msk) != 0b11 << RCC_CFGR1_SWS_Pos) {}

    // Update system timer
    SystemCoreClock=144000000;
    //SysTick_Config(SystemCoreClock/8/1000); // comment out if you have no SysTick_Handler()
}

// Delay loop for 144 MHz
void delay(uint32_t msec) {
    for (uint32_t j=0; j < msec * 28800; j++) {
        __NOP();
    }
}

int main() {
    // Enable Port H
    STM32_SET_BIT(RCC->AHB2ENR, RCC_AHB2ENR_GPIOHEN);

    // PH15 = Output for LEDs
    STM32_MODIFY_REG(GPIOH->MODER, GPIO_MODER_MODE15, 0b01 << GPIO_MODER_MODE15_Pos);

    // Endless loop
    while (1) {
        GPIOH->BSRR = GPIO_BSRR_BS15;
        delay(100);
        GPIOH->BSRR = GPIO_BSRR_BR15;
        delay(900);
    }
}

Digitale Pins

Generell können alle I/O Pins erst benutzt werden, nachdem man den Port im Register RCC->AHB2ENR eingeschaltet hat.

Standardmäßig sind fast alle I/O Pins für analoge Eingabe konfiguriert. Im Register GPIOx->MODER konfiguriert man einen Pin als Eingang, Ausgang oder für 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 port" tabellarisch beschrieben.

Um Eingänge abzufragen liest man das jeweilige GPIOx->IDR Register.

Direkte Schreibzugriffe sind über das Register GPIOx->ODR möglich. Um einzelne Pins atomar auf High oder Low zu schalten, verwendet man jedoch das GPIOx->BSRR Register.

Im Register GPIOx->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 vier Stufen einstellen (Siehe "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

Im Register GPIOx->MODER stellt man ein, welche Pins als analoger Eingang verwendet werden sollen. Die meisten Pins sind nach einem Reset bereits so eingestellt. Beispiel:

// Configure PA3 as analog input for ADC1_IN3
STM32_MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE3, 0b11 << GPIO_MODER_MODE3_Pos);

Der STM32C5 hat ein neues Register ADC1->PCSEL, wo man für jeden verwendeten externen Eingang ein Bit setzen muss. Die internen Kanäle (12 und 13) brauchen das nicht, allerdings haben diese weitere Bits im Register ADC12_COMMON->CCR.

Das folgende Programm initialisiert den ADC (samt optionaler Kalibrierung für genauere Ergebnisse) und liest den Eingang PA4 wiederholt in die Variable a ein. Ich habe einige Zeilen auskommentiert, weil das Programm nur diesen einen Kanal liest:

#include "stm32c5xx.h"

// Delay loop for 48 MHz
void delay(uint32_t msec) {
    for (uint32_t j=0; j < msec * 9600; j++) {
        __NOP();
    }
}

// Initialize ADC1 for single conversion mode
void init_analog() {

    // ADC and DAC Clock use HCLK/4 (12 MHz)
    STM32_MODIFY_REG(RCC->CCIPR2, RCC_CCIPR2_ADCDACSEL, 0b00 << RCC_CCIPR2_ADCDACSEL_Pos);
    STM32_MODIFY_REG(RCC->CCIPR2, RCC_CCIPR2_ADCDACPRE, 0b010 << RCC_CCIPR2_ADCDACPRE_Pos);

    // Enable ADC 1 and 2
    STM32_SET_BIT(RCC->AHB2ENR, RCC_AHB2ENR_ADC12EN);

    // Exit ADC deep power down mode
    STM32_CLEAR_BIT(ADC1->CR, ADC_CR_DEEPPWD);

    // Enable ADC voltage regulator
    STM32_SET_BIT(ADC1->CR, ADC_CR_ADVREGEN);

    // Delay 1-2 ms
    delay(2);

    // Stop the ADC
    STM32_CLEAR_BIT(ADC1->CR, ADC_CR_ADEN);

    // (Re-)start calibration for single ended mode
    STM32_CLEAR_BIT(ADC1->CR, ADC_CR_ADCAL);
    STM32_SET_BIT(ADC1->CR, ADC_CR_ADCAL);

    // Wait until the calibration is finished
    while (STM32_READ_BIT(ADC1->CR, ADC_CR_ADCAL));

    // Clear the ready flag
    STM32_SET_BIT(ADC1->ISR, ADC_ISR_ADRDY);

    // Enable the ADC and wait until it is ready
    STM32_SET_BIT(ADC1->CR, ADC_CR_ADEN);
    while (!STM32_READ_BIT(ADC1->ISR, ADC_ISR_ADRDY));

    // Select software start trigger
    STM32_MODIFY_REG(ADC1->CFGR1, ADC_CFGR1_EXTEN, 0b00 << ADC_CFGR1_EXTEN_Pos);

    // Select single conversion mode
    STM32_CLEAR_BIT(ADC1->CFGR1, ADC_CFGR1_CONT);

    // Set sample time to 48 clock cycles for the used channels
    // STM32_MODIFY_REG(ADC1->SMPR1, ADC_SMPR1_SMP0, 0b101 << ADC_SMPR1_SMP0_Pos);
    // STM32_MODIFY_REG(ADC1->SMPR1, ADC_SMPR1_SMP1, 0b101 << ADC_SMPR1_SMP1_Pos);
    // STM32_MODIFY_REG(ADC1->SMPR1, ADC_SMPR1_SMP2, 0b101 << ADC_SMPR1_SMP2_Pos);
    // STM32_MODIFY_REG(ADC1->SMPR1, ADC_SMPR1_SMP3, 0b101 << ADC_SMPR1_SMP3_Pos);
    STM32_MODIFY_REG(ADC1->SMPR1, ADC_SMPR1_SMP4, 0b101 << ADC_SMPR1_SMP4_Pos);
    // STM32_MODIFY_REG(ADC1->SMPR1, ADC_SMPR1_SMP5, 0b101 << ADC_SMPR1_SMP5_Pos);
    // STM32_MODIFY_REG(ADC1->SMPR1, ADC_SMPR1_SMP6, 0b101 << ADC_SMPR1_SMP6_Pos);
    // STM32_MODIFY_REG(ADC1->SMPR1, ADC_SMPR1_SMP7, 0b101 << ADC_SMPR1_SMP7_Pos);
    // STM32_MODIFY_REG(ADC1->SMPR1, ADC_SMPR1_SMP8, 0b101 << ADC_SMPR1_SMP8_Pos);
    // STM32_MODIFY_REG(ADC1->SMPR1, ADC_SMPR1_SMP9, 0b101 << ADC_SMPR1_SMP9_Pos);
    // STM32_MODIFY_REG(ADC1->SMPR2, ADC_SMPR2_SMP10, 0b101 << ADC_SMPR2_SMP10_Pos);
    // STM32_MODIFY_REG(ADC1->SMPR2, ADC_SMPR2_SMP11, 0b101 << ADC_SMPR2_SMP11_Pos);
    // STM32_MODIFY_REG(ADC1->SMPR2, ADC_SMPR2_SMP12, 0b101 << ADC_SMPR2_SMP12_Pos);
    // STM32_MODIFY_REG(ADC1->SMPR2, ADC_SMPR2_SMP13, 0b101 << ADC_SMPR2_SMP13_Pos);

    // Pre-Select all 14 input channels
    // STM32_MODIFY_REG(ADC1->PCSEL, ADC_PCSEL_PCSEL, 0b0011111111111111);

    // Pre-select only channel 4
    STM32_SET_BIT(ADC1->PCSEL, 1 << 4);

    // enable internal temperature sensor (channel 12)
    // STM32_SET_BIT(ADC12_COMMON->CCR, ADCC_CCR_TSEN);

    // enable internal reference (channel 13)
    // STM32_SET_BIT(ADC12_COMMON->CCR, ADCC_CCR_VREFEN);
}

// Read from an analog input of ADC1
uint32_t read_analog(uint32_t channel) {

    // Number of channels to convert: 1
    STM32_MODIFY_REG(ADC1->SQR1, ADC_SQR1_LEN, 0 << ADC_SQR1_LEN_Pos); // ADC does one conversion more than configured here

    // Select the channel
    STM32_MODIFY_REG(ADC1->SQR1, ADC_SQR1_SQ1, channel << ADC_SQR1_SQ1_Pos);

    // Clear the finish flag
    STM32_CLEAR_BIT(ADC1->ISR, ADC_ISR_EOC);

    // Start a conversion
    STM32_SET_BIT(ADC1->CR, ADC_CR_ADSTART);

    // Wait until the conversion is finished
    while (!STM32_READ_BIT(ADC1->ISR, ADC_ISR_EOC));
    while (STM32_READ_BIT(ADC1->CR, ADC_CR_ADSTART));

    // Return the lower 12 bits of the result
    return ADC1->DR & 0b111111111111;
}

int main() {
    init_analog();

    // Enable port A
    STM32_SET_BIT(RCC->AHB2ENR, RCC_AHB2ENR_GPIOAEN);

    // Configure PA4 as analog input for ADC1_IN4 (not necessary because that is the default)
    // STM32_MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE4, 0b11 << GPIO_MODER_MODE4_Pos);

    volatile uint32_t a;
    while (1) {
        a=read_analog(4);
    }
}

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

Mit PWM kann man z.B. die Helligkeit von Lampen oder die Drehzahl von Motoren steuern.

Die Taktfrequenz der Timer wird normalerweise vom Systemtakt abgeleitet. Die Relevanten Register sind:

Im PWM Modus zählen Timer fortlaufend von 0 an hoch bis zum Maximum, welches durch das TIMx->ARR Register festgelegt wird. Wenn der Maximalwert als 50000 festgelegt wird, können die Ausgangsimpulse wahlweise 1 bis 50000 Takte breit sein.

Für jeden Ausgang gibt es ein Vergleichs-Register TIMx->CCRy welches bestimmt, wie breit die Ausgangsimpulse sein sollen. Beim Extremwert 0 ist der Ausgang ständig Low. Bei Werten größer dem Maximum (in TIMx->ARR) ist der Ausgang ständig High. Mit der option "inverse polarity" im Register TIMx->CCER können die Ausgänge umgepolt werden, so dass sie Low-Impulse liefern.

Das folgende Beispielprogramm benutzt einen Ausgang von Timer 2 (PA5), um eine Leuchtdiode unterschiedlich hell leuchten zu lassen:

#include "stm32c5xx.h"

// Delay loop for 48 MHz
void delay(uint32_t msec) {
    for (uint32_t j=0; j < msec * 9600; j++) {
        __NOP();
    }
}

int main() {
    // Enable port A and timer 2
    STM32_SET_BIT(RCC->AHB2ENR, RCC_AHB2ENR_GPIOAEN);
    STM32_SET_BIT(RCC->APB1LENR, RCC_APB1LENR_TIM2EN);

    // PA5 = TIM2_CH1 alternate function 1 (see data sheet)
    STM32_MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE5, 0b10 << GPIO_MODER_MODE5_Pos);
    STM32_MODIFY_REG(GPIOA->AFR[0], GPIO_AFRL_AFSEL5,  1  << GPIO_AFRL_AFSEL5_Pos);

    // Timer 2 channel 1 compare mode = PWM1 with the required preload buffer enabled
    STM32_MODIFY_REG(TIM2->CCMR1, TIM_CCMR1_OC1M, 0b0110 << TIM_CCMR1_OC1M_Pos);
    STM32_SET_BIT(TIM2->CCMR1, TIM_CCMR1_OC1PE);

    // Timer 2 enable channel 1 output
    STM32_SET_BIT(TIM2->CCER, TIM_CCER_CC1E);

    // Timer 2 inverse polarity for channel 1
    // STM32_SET_BIT(TIM2->CCER, TIM_CCER_CC1P);

    // Timer 2 clock prescaler, divide clock by 8
    TIM2->PSC = 7; // the PCLK2 clock is divided by this value +1

    // 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
    STM32_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.

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:

Mit der Oversampling Option (OVER8=1) sind doppelt so hohe Baudraten möglich, allerdings ist dann die Berechung des Wertes für das BRR Register komplexer als im folgenden Beispiel.

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 "stm32c5xx.h"

uint32_t SystemCoreClock=48000000;

// Delay loop for 48 MHz
void delay(uint32_t msec) {
    for (uint32_t j=0; j < msec * 9600; j++) {
        __NOP();
    }
}

// Use serial port for standard output
int _write(int file, char *ptr, int len) {
    (void)file; // unused
    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_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, H and USART1
    STM32_SET_BIT(RCC->AHB2ENR, RCC_AHB2ENR_GPIOAEN);
    STM32_SET_BIT(RCC->AHB2ENR, RCC_AHB2ENR_GPIOHEN);
    STM32_SET_BIT(RCC->APB2ENR, RCC_APB2ENR_USART1EN);

    // PH15 = Output for LEDs
    STM32_MODIFY_REG(GPIOH->MODER, GPIO_MODER_MODE15, 0b01 << GPIO_MODER_MODE15_Pos);

    // Use system clock for USART1
    STM32_MODIFY_REG(RCC->CCIPR1, RCC_CCIPR1_USART1SEL, 0b00 << RCC_CCIPR1_USART1SEL_Pos);

    // PA9 (TxD) shall use the alternate function 7 (see data sheet)
    STM32_MODIFY_REG(GPIOA->MODER,  GPIO_MODER_MODE9,  0b10 << GPIO_MODER_MODE9_Pos); // alternate function mode
    STM32_MODIFY_REG(GPIOA->AFR[1], GPIO_AFRH_AFSEL9,  7    << GPIO_AFRH_AFSEL9_Pos); // alternate function 7

    // PA10 (RxD) shall use the alternate function 7 (see data sheet)
    STM32_MODIFY_REG(GPIOA->MODER,  GPIO_MODER_MODE10, 0b10 << GPIO_MODER_MODE10_Pos); // alternate function mode
    STM32_MODIFY_REG(GPIOA->AFR[1], GPIO_AFRH_AFSEL10, 7    << GPIO_AFRH_AFSEL10_Pos); // alternate function 7

    // 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
        GPIOH->BSRR = GPIO_BSRR_BS15;
        delay(500);

        puts("Hello");

        // LED off
        GPIOH->BSRR = GPIO_BSRR_BR15;
        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 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 STM32G4 Mikrocontroller haben bis zu vier 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.

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:

#include "stm32c5xx.h"
#include <stdbool.h>

// Delay loop for the default 48 MHz
void delay(uint32_t msec) {
    for (uint32_t j=0; j < msec * 9600; j++) {
        __NOP();
    }
}

/**
 * Initialize the I²C interface for master mode.
 *
 * The I/O port mode and alternate function must be configured already.
 *
 * @param registerStruct May be either I2C1, I2C2, I2C3 or I2C4
 * @param fastMode false=100 kHz, true=400 kHz
 * @param pclk1 clock frequency of APB1 peripherals, must be at least 1 MHz for normal mode and 4 MHz for fast mode.
 */
void i2c_init(I2C_TypeDef* registerStruct, bool fastMode, uint32_t pclk1) {

    // Select PCLK as clock source and enable I2C
    #ifdef I2C1
        if (registerStruct==I2C1) {
            STM32_MODIFY_REG(RCC->CCIPR2, RCC_CCIPR2_I2C1SEL, 0b00 << RCC_CCIPR2_I2C1SEL_Pos);
            STM32_SET_BIT(RCC->APB1LENR, RCC_APB1LENR_I2C1EN);
        }
    #endif
    #ifdef I2C2
        if (registerStruct==I2C2) {
            STM32_MODIFY_REG(RCC->CCIPR2, RCC_CCIPR2_I2C2SEL, 0b00 << RCC_CCIPR2_I2C2SEL_Pos);
            STM32_SET_BIT(RCC->APB1LENR, RCC_APB1LENR_I2C2EN);
        }
    #endif

    // Disable the I²C peripheral
    STM32_CLEAR_BIT(registerStruct->CR1, I2C_CR1_PE);

    // Configure timing
    if (fastMode) {

        // we want to clock the I2C peripheral with 8 MHz if possible (max. 64 MHz are allowed here)
        uint32_t divisor=pclk1/8000000;
        if (divisor<1) divisor=1;
        if (divisor>16) divisor=16;
        uint32_t i2c_clock=pclk1/divisor;

        STM32_MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_PRESC,  (divisor-1)           << I2C_TIMINGR_PRESC_Pos);
        STM32_MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_SCLL,   (i2c_clock/850000-1)  << I2C_TIMINGR_SCLL_Pos);
        STM32_MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_SCLH,   (i2c_clock/850000-1)  << I2C_TIMINGR_SCLH_Pos);
        STM32_MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_SDADEL, (i2c_clock/8000000)   << I2C_TIMINGR_SDADEL_Pos); // no -1 on purpose
        STM32_MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_SCLDEL, (i2c_clock/4000000-1) << I2C_TIMINGR_SCLDEL_Pos);
    }
    else {

        // we want to clock the I2C peripheral with 2 MHz if possible (max. 16 MHz are allowed here)
        uint32_t divisor=pclk1/2000000;
        if (divisor<1) divisor=1;
        if (divisor>16) divisor=16;
        uint32_t i2c_clock=pclk1/divisor;

        STM32_MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_PRESC,  (divisor-1)           << I2C_TIMINGR_PRESC_Pos);
        STM32_MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_SCLL,   (i2c_clock/200000-1)  << I2C_TIMINGR_SCLL_Pos);
        STM32_MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_SCLH,   (i2c_clock/200000-1)  << I2C_TIMINGR_SCLH_Pos);
        STM32_MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_SDADEL, (i2c_clock/2000000)   << I2C_TIMINGR_SDADEL_Pos); // no -1 on purpose
        STM32_MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_SCLDEL, (i2c_clock/1000000-1) << I2C_TIMINGR_SCLDEL_Pos);
    }

    // Stop and Restart signals will be generated by software
    STM32_CLEAR_BIT(registerStruct->CR2, I2C_CR2_AUTOEND);

    // Enable the I²C peripheral
    STM32_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
        STM32_MODIFY_REG(registerStruct->CR2, I2C_CR2_NBYTES, 255 << I2C_CR2_NBYTES_Pos);

        // Prepare to transfer more blocks after this one
        STM32_SET_BIT(registerStruct->CR2, I2C_CR2_RELOAD);
    }
    else {
        // Number of bytes to send or receive in the last block
        STM32_MODIFY_REG(registerStruct->CR2, I2C_CR2_NBYTES, size << I2C_CR2_NBYTES_Pos);

        // After this block, no more blocks will be transferred
        STM32_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, I2C3 or I2C4
 * @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)
    STM32_MODIFY_REG(registerStruct->CR2, I2C_CR2_SADD, slave_addr << 1);

    // Send data
    if (send_size>0) {
        // Data direction
        STM32_CLEAR_BIT(registerStruct->CR2, I2C_CR2_RD_WRN);

        // Configure size of the first data block to send
        configureBlockSize(registerStruct, send_size);

        // Send start condition
        STM32_SET_BIT(registerStruct->CR2, I2C_CR2_START);

        // Send data
        do {
            // Check for error
            if (STM32_READ_BIT(registerStruct->ISR, I2C_ISR_NACKF | I2C_ISR_ARLO)) {
                goto error;
            }

            // Send one byte when ready
            if (STM32_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 (STM32_READ_BIT(registerStruct->ISR, I2C_ISR_TCR)) {
                configureBlockSize(registerStruct, send_size);
            }
        }
        // Loop  until the transfer is complete
        while (!STM32_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
        STM32_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
        STM32_SET_BIT(registerStruct->CR2, I2C_CR2_START);

        // Receive data
        do {
            // Check for error
            if (STM32_READ_BIT(registerStruct->ISR, I2C_ISR_ARLO)) {
                goto error;
            }

            // Fetch one received byte when ready
            if (STM32_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 (STM32_READ_BIT(registerStruct->ISR, I2C_ISR_TCR)) {
                configureBlockSize(registerStruct, receive_size);
            }
        }
        // Loop  until the transfer is complete
        while (!STM32_READ_BIT(registerStruct->ISR, I2C_ISR_TC));
    }

    // Send stop condition
    STM32_SET_BIT(registerStruct->CR2, I2C_CR2_STOP);

    return receive_count;

    error:
    // Restart the I²C peripheral
    STM32_CLEAR_BIT(registerStruct->CR1, I2C_CR1_PE);
    STM32_SET_BIT(registerStruct->CR1, I2C_CR1_PE);
    //ITM_SendString("I2C bus error!\n");
    return receive_count;
}

/**
 * Initialize the I/O pins.
 */
void init_io() {
    // Enable port B
    STM32_SET_BIT(RCC->AHB2ENR, RCC_AHB2ENR_GPIOBEN);

    // I2C1 PB9=SDA, alternate function 4 open-drain (for external pull-ups)
    STM32_MODIFY_REG(GPIOB->MODER,  GPIO_MODER_MODE8, 0b10 << GPIO_MODER_MODE8_Pos); // alternate function mode
    STM32_MODIFY_REG(GPIOB->AFR[1], GPIO_AFRH_AFSEL8, 4    << GPIO_AFRH_AFSEL8_Pos); // alternate function 4
    STM32_SET_BIT(GPIOB->OTYPER, GPIO_OTYPER_OT8); // open-drain

    // I2C1 PB8=SCL, alternate function 4 open-drain (for external pull-ups)
    STM32_MODIFY_REG(GPIOB->MODER,  GPIO_MODER_MODE9, 0b10 << GPIO_MODER_MODE9_Pos); // alternate function mode
    STM32_MODIFY_REG(GPIOB->AFR[1], GPIO_AFRH_AFSEL9, 4    << GPIO_AFRH_AFSEL9_Pos); // alternate function 4
    STM32_SET_BIT(GPIOB->OTYPER, GPIO_OTYPER_OT9); // open-drain

    // Enable port H
    STM32_SET_BIT(RCC->AHB2ENR, RCC_AHB2ENR_GPIOHEN);

    // PH15 = Output for LEDs
    STM32_MODIFY_REG(GPIOH->MODER, GPIO_MODER_MODE15, 0b01 << GPIO_MODER_MODE15_Pos);
}

int main() {
    init_io();
    i2c_init(I2C1, false, 48000000);

    while (1) {
        // LED on
        GPIOH->BSRR = GPIO_BSRR_BS15;
        delay(10);

        uint8_t send_buffer[]={0};
        uint8_t receive_buffer[5];
        i2c_communicate(I2C1, 8, send_buffer, 1, receive_buffer, 5);

        // LED off
        GPIOH->BSRR = GPIO_BSRR_BR15;
        delay(90);
    }
}

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 besteht zum Teil aus Hardware, aber dazu gehört auch ein umfangreiches Softwarepaket. Beinahe alle Programmierer binden dazu fertige Bibliotheken in ihr Programm ein.

ST setzt seit den U5/H5/C5 Serien auf Microsofts USBX mit ThreadX oder FreeRTOS. Diese Kombination funktioniert völlig anders als die bisher bekannten USBD und USBH Bibliotheken. Erschwerend kommt hinzu, dass die neue HAL2 und Cube MX2 nicht mehr zu den bisherigen Anleitungen im Internet passen. Mir ist das zu kompliziert geworden.

Als Alternative dazu bietet sich TinyUSB von Ha Thach an. Man kann es wahlweise mit und ohne FreeRTOS verwenden.

Die USB Schnittstelle muss mit 48 MHz getaktet werden, vorzugsweise aus einem Quarz. Der interne HSI Oszillator ist nur mit aktiviertem Clock-Recovery System (CRS) geeignet. Der APB2 Bus muss mit mindestens 12 MHz getaktet werden.

Die USB Buchse wird mit PA11 (D-) und PA12 (D+) verbunden. Es ist nicht nötig, den Modus (in, out, alternative) dieser Pins zu konfigurieren.

Im Register USB_DRD_FS->BCDR Bit DPPU_DPD kann man Widerstände einschalten:

Indem man die Pull-Up Widerstände aus und wieder ein schaltet, kann man die Gegenseite dazu bringen, das USB Gerät erneut zu erkennen, ohne das Kabel abstecken zu müssen.

USB und CAN können gleichzeitig verwendet werden.

Virtueller COM Port mit TinyUSB

Hier zeige ich, wie man mit der STM32Cube IDE und TinyUSB ein Projekt mit virtuellem COM Port (VCP, CDC) erstellt. Du kannst es auch fertig downloaden.

#include <stdint.h>
#include "stm32c5xx.h"
#include "tusb.h"

// Interrupt handler for USB
void USB_DRD_FS_IRQHandler() {
    tud_int_handler(BOARD_TUD_RHPORT);
}

// Interrupt handler for USB (alternative name)
void USB_FS_IRQHandler() {
    tud_int_handler(BOARD_TUD_RHPORT);
}

// The current clock frequency
uint32_t SystemCoreClock=48000000;

// Interrupt handler for SysTick timer
volatile uint32_t systick_count=0;
void SysTick_Handler() {
    systick_count++;
}

// Delay some milliseconds.
void delay(uint32_t ms) {
    uint32_t start=systick_count;
    while (systick_count-start < ms);
}

// Derive 48 MHz USB clock from internal HSI oscillator with CRS.
// No need to change system clock because the default 48 MHz is fast enough.
void init_clock() {
    // Start system timer
    SysTick_Config(SystemCoreClock/1000);

    // Select HSI Div 3 as clock source for USB
    STM32_MODIFY_REG(RCC->CCIPR2, RCC_CCIPR2_CK48SEL, 0b10 <<  RCC_CCIPR2_CK48SEL_Pos);

    // Enable CRS with Auto-Trimming on SOF signal from USB host
    STM32_SET_BIT(RCC->APB1LENR, RCC_APB1LENR_CRSEN);
    STM32_SET_BIT(CRS->CR, CRS_CR_CEN + CRS_CR_AUTOTRIMEN);
}

void init_io() {
    // Enable port H
    STM32_SET_BIT(RCC->AHB2ENR, RCC_AHB2ENR_GPIOHEN);

    // PH15 = Output for LED
    STM32_MODIFY_REG(GPIOH->MODER, GPIO_MODER_MODE15, 0b01 << GPIO_MODER_MODE15_Pos);

    // Enable USB with pull-up
    STM32_SET_BIT(RCC->APB2ENR, RCC_APB2ENR_USBEN);
    STM32_SET_BIT(USB_DRD_FS->BCDR, USB_BCDR_DPPU_DPD);
}

// Blink the LED and send messages.
void main_task(void) {
    static enum {BEFORE, SEND, AFTER} state = BEFORE;
    static uint32_t waitSince = 0;

    switch (state) {

        case BEFORE:
            if (systick_count - waitSince >= 900) { // in 900ms

                // LED ON
                STM32_WRITE_REG(GPIOH->BSRR, GPIO_BSRR_BS15);

                waitSince = systick_count;
                state = SEND; // next state
            }
            break;

        case SEND:

            // Send messages via USB (each max. 64 characters)
            tud_cdc_n_write_str(0, "Hello world from CDC 0\n");
            tud_cdc_n_write_str(1, "Hello world from CDC 1\n");

            // Flush buffer
            tud_cdc_n_write_flush(0);
            tud_cdc_n_write_flush(1);

            state = AFTER; // next state
            break;

        case AFTER:
            if (systick_count - waitSince >= 100) { // in 100ms

                // LED OFF
                STM32_WRITE_REG(GPIOH->BSRR, GPIO_BSRR_BR15);

                waitSince = systick_count;
                state = BEFORE; // next state
            }
            break;
    }
}

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

 // Initialize tinyUSB
    tusb_rhport_init_t dev_init = {
        .role = TUSB_ROLE_DEVICE,
        .speed = TUSB_SPEED_AUTO
    };
    tusb_init(BOARD_TUD_RHPORT, &dev_init);

    while(1) {
        tud_task();
        main_task();
    }
}

Die Ausgabe kann man mit einem Terminal-Programm anzeigen, indem man damit den virtuellen COM Port öffnet. Die Baudrate wird igoriert. Bitte beachte meinen Hinweis zu CDC Geräten unter Linux, er erspart dir womöglich eine langwierige Fehlersuche.

Ich habe zwei Interrupthandler für USB geschrieben, weil die IDE je nach Modell den einen oder den anderen Namen vorgibt (siehe Datei startup_stm32c552xxxx.s). Mit dem obigen Code funktionieren beide Varianten.

Dieses Beispielprogramm richtet zwei (!) virtuelle COM Ports ein. Dafür ist CFG_TUD_CDC in tusb_config.h verantwortlich, sowie der USB Descriptor. Die Funktion tud_cdc_n_write_str() wartet nicht darauf, dass genug Platz im Puffer frei wird. Sie sendet nur so viel wie gerade möglich ist, maximal 64 Zeichen. Wenn kein Computer verbunden ist, laufen die gesendeten Zeichen ins Leere.

Das Projekt implementiert Multitasking mit einem Zustandsautomaten, wie auf ich ihn auf der Seite Multitasking in C und Arduino erklärt habe. Es führt zwei Threads aus: das eigene Hauptprogramm und den TinyUSB Treiber.

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. Zwei Alarm-Zeiten sind programmierbar und die Uhr kann sich den Zeitstempel von einem Ereignis merken.

Nach einem Stromausfall werden die Zähler automatisch auf 0 gesetzt und die Uhr angehalten.

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. Häufige Pegelwechsel an PC13 können den Oszillator beeinträchtigen.

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. Auf Boards mit 32,768 kHz Uhrenquarz kann man sie so starten:
// Start the RTC using 32 kHz crystal
void init_rtc() {
    // Enable APB bus interface to RTC
    STM32_SET_BIT(RCC->APB3ENR, RCC_APB3ENR_RTCAPBEN);

    // Enable write access to the RTC domain
    STM32_SET_BIT(PWR->RTCCR, PWR_RTCCR_DRTCP);

    // Enable LSE oscillator with medium-high drive
    STM32_MODIFY_REG(RCC->RTCCR, RCC_RTCCR_LSEDRV, 0b10 << RCC_RTCCR_LSEDRV_Pos);
    STM32_SET_BIT(RCC->RTCCR, RCC_RTCCR_LSEON);

    // Wait until LSE oscillator is ready
    while (!STM32_READ_BIT(RCC->RTCCR, RCC_RTCCR_LSERDY)) {}

    // Select LSE as clock source for the RTC
    STM32_MODIFY_REG(RCC->RTCCR, RCC_RTCCR_RTCSEL, 0b01 << RCC_RTCCR_RTCSEL_Pos);

    // Enable the RTC
    STM32_SET_BIT(RCC->RTCCR, RCC_RTCCR_RTCEN);
}

Auf Boards ohne Uhrenquarz aber mit 8 MHz Hauptquarz kann man sie so starten:

// Start the RTC using 8 MHz crystal
void init_rtc() {
    // Enable HSE Oscillator (if not already enabled)
    STM32_SET_BIT(RCC->CR1, RCC_CR1_HSEON);

    // Wait until HSE oscillator is ready
    while (!STM32_READ_BIT(RCC->CR1, RCC_CR1_HSERDY)) {}

    // Divide HSE by 8 to get 1 MHz for the RTC
    STM32_MODIFY_REG(RCC->CFGR1, RCC_CFGR1_RTCPRE, 8 << RCC_CFGR1_RTCPRE_Pos);

    // Enable APB bus interface to RTC
    STM32_SET_BIT(RCC->APB3ENR, RCC_APB3ENR_RTCAPBEN);

    // Enable write access to the RTC domain
    STM32_SET_BIT(PWR->RTCCR, PWR_RTCCR_DRTCP);

    // Select HSE as clock source for the RTC
    STM32_MODIFY_REG(RCC->RTCCR, RCC_RTCCR_RTCSEL, 0b11 << RCC_RTCCR_RTCSEL_Pos);

    // Enable the RTC
    STM32_SET_BIT(RCC->RTCCR, RCC_RTCCR_RTCEN);

    // Unlock the write protection
    RTC->WPR = 0xCA;
    RTC->WPR = 0x53;

    // Enter initialization mode
    STM32_SET_BIT(RTC->ICSR, RTC_ICSR_INIT);

    // Wait until the initialization mode is active
    while (!STM32_READ_BIT(RTC->ICSR, RTC_ICSR_INITF)) {};

    // Divide the 1 MHz by 100 and 10000 to get 1 Hz
    STM32_MODIFY_REG(RTC->PRER, RTC_PRER_PREDIV_A, 100-1   << RTC_PRER_PREDIV_A_Pos);
    STM32_MODIFY_REG(RTC->PRER, RTC_PRER_PREDIV_S, 10000-1 << RTC_PRER_PREDIV_S_Pos);

    // Leave initialization mode
    STM32_CLEAR_BIT(RTC->ICSR, RTC_ICSR_INIT);

    // Switch the write protection back on
    RTC->WPR = 0xFF;
}

⭐ Die Taktquelle der Uhr lässt sich immer nur einmal einstellen. Für einen Wechsel muss der Chip zwischendurch Stromlos gemacht werden.

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 wird die schlafende CPU jede Sekunde durch einen Interrupt aufgeweckt:

#include "stm32c5xx.h"

// delay loop for the default 48 MHz
void delay(uint32_t msec) {
    for (uint32_t j=0; j < msec * 9600; j++) {
        __NOP();
    }
}

void init_wakeup() {
    // Unlock the write protection
    RTC->WPR = 0xCA;
    RTC->WPR = 0x53;

    // Stop the wakeup timer to allow configuration update
    STM32_CLEAR_BIT(RTC->CR, RTC_CR_WUTE);

    // Wait until the wakeup timer is ready for configuration update
    while (!STM32_READ_BIT(RTC->ICSR, RTC_ICSR_WUTWF)) {};

    // Clock source of the wakeup timer is 1 Hz
    STM32_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
    STM32_SET_BIT(RTC->CR, RTC_CR_WUTE + RTC_CR_WUTIE);

    // Switch the write protection back on
    RTC->WPR = 0xFF;

    // Enable interrupt
    STM32_SET_BIT(EXTI->IMR1, EXTI_IMR1_IM17);
    NVIC_EnableIRQ(RTC_IRQn);

    // Clear (old) pending interrupt flag
    STM32_SET_BIT(RTC->SCR, RTC_SCR_CWUTF);
}

void RTC_IRQHandler() {
    // Clear pending interrupt flag in RTC
    STM32_SET_BIT(RTC->SCR, RTC_SCR_CWUTF);
}

void init_io() {
    // Enable port H
    STM32_SET_BIT(RCC->AHB2ENR,  RCC_AHB2ENR_GPIOHEN);

    // PH15 = Output for LED
    STM32_MODIFY_REG(GPIOH->MODER, GPIO_MODER_MODE15, 0b01 << GPIO_MODER_MODE15_Pos);
}

int main() {
    // Give the debugger time to connect
    delay(1000);

    init_io();
    init_rtc();
    init_wakeup();

    while (1) {
        // Enter sleep state
        __WFI();

        // Flash the LED shortly
        GPIOH->BSRR = GPIO_BSRR_BS15;
        delay(50);
        GPIOH->BSRR = GPIO_BSRR_BR15;
        delay(50);
    }
}

⭐ Da dein Programm die CPU schlafen legt, empfiehlt sich ein 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 beim Verbindungsaufbau ggf. den Reset-Taster drücken.

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 die obigen Funktionen auf:

#include "stm32c5xx.h"

int main() {
    init_rtc();

    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
        char buffer[20];
        snprintf(buffer,sizeof(buffer),"Time: %d%d:%d%d:%d%d", ht,hu, mnt,mnu, st,su);
        puts(buffer);

        // 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
        snprintf(buffer,sizeof(buffer),"Date: %d%d-%d%d-%d%d", yt,yu, mt,mu, dt,du);
        puts(buffer);

        // Wait a while
        delay(3000);
    }
}

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 mnt tens of minute
 * @param mnu 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 mnt, uint8_t mnu, 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) mnt << RTC_TR_MNT_Pos;
    tmp += (uint32_t) mnu << RTC_TR_MNU_Pos;
    tmp += (uint32_t) st << RTC_TR_ST_Pos;
    tmp += (uint32_t) su << RTC_TR_SU_Pos;

    // Unlock the write protection
    RTC->WPR = 0xCA;
    RTC->WPR = 0x53;

    // Enter initialization mode
    STM32_SET_BIT(RTC->ICSR, RTC_ICSR_INIT);

    // Wait until the initialization mode is active
    while (!STM32_READ_BIT(RTC->ICSR, RTC_ICSR_INITF)) {};

    // The 24h format is already the default
    // STM32_CLEAR_BIT(RTC->CR, RTC_CR_FMT);

    // Update the time register
    RTC->TR = tmp;

    // Leave the initialization mode
    STM32_CLEAR_BIT(RTC->ICSR, RTC_ICSR_INIT);

    // Trigger a synchronization of the shadow registers
    STM32_CLEAR_BIT(RTC->ICSR, RTC_ICSR_RSF);

    // Wait until the shadow registers are synchronized
    while (!STM32_READ_BIT(RTC->ICSR, RTC_ICSR_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
    STM32_SET_BIT(RTC->ICSR, RTC_ICSR_INIT);

    // Wait until the initialization mode is active
    while (!STM32_READ_BIT(RTC->ICSR, RTC_ICSR_INITF)) {};

    // Update the time register
    RTC->DR = tmp;

    // Leave the initialization mode
    STM32_CLEAR_BIT(RTC->ICSR, RTC_ICSR_INIT);

    // Trigger a synchronization of the shadow registers
    STM32_CLEAR_BIT(RTC->ICSR, RTC_ICSR_RSF);

    // Wait until the shadow registers are synchronized
    while (!STM32_READ_BIT(RTC->ICSR, RTC_ICSR_RSF)) {};

    // Switch the write protection back on
    RTC->WPR = 0xFF;
}

int main() {
    ...
    init_rtc();

    // Change the time to 18:33:45
    RTC_write_time(1,8, 3,3, 4,5);

    // Change the date to 26-02-21 (21th Feb. 2026), 6=saturday
    RTC_write_date(2,6, 0,2, 2,1, 6);

    ...
}

RTC kalibrieren

Die RTC weicht normalerweise höchstens zwei Sekunden pro Tag ab. Durch Kalibrierung kann man die Genauigkeit weiter verbessern. Sollte deine Uhr um mehr als 4 Sekunden pro Tag (=48 ppm) abweichen, liegt mit Sicherheit ein Hardwarefehler vor, der zuerst behoben werden muss.

⚠️ Das RTC->CALR Register kann erst nach Deaktivierung des Schreibschutzes verändert werden.

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.

Wenn die Uhr zu langsam läuft, setzt man das Bit CALP auf 1, so daß sie 42,188 Sekunden pro Tag schneller läuft. Wenn die Uhr schon ohne Kalibrierung zu schnell läuft, lässt man das Bit auf 0. Anschließend stellt man im CALM Feld einen Korrekturwert (0-511) ein, wobei jede Stufe die Uhr um 0,0824 Sekunden pro Tag verlangsamt.

Wenn die Uhr zum Beispiel 4 Sekunden pro Tag zu langsam wäre, ergäbe sich folgende Rechnung:

Error:                       4.000 Seconds
CALP:                1 =   -42.188 Seconds
CALM:     464 * 0.0824 =   +38.234 Seconds
===========================================
Sum:                         0.046 Seconds
Es bleibt eine Ungenauigkeit von 0,046 Sekunden pro Tag übrig.
STM32 Anleitungen