STM32G4 Anleitung
Dies ist der G4 spezifische Teil meiner STM32 Anleitungen.
Die STM32G4 Mikrocontroller haben einen ARM Cortex M4F Kern bis 170 MHz. Sie sind mit zahlreichen Funktionen zur schnellen Verarbeitung analoger Signale voll gestopft. Neben den üblichen ADC und DAC gibt es ein serielles Audio Interface (SAI), sowie Operationsverstärker und analoge Vergleicher. Mit DSP Befehlen und mehreren Koprozessoren (FPU, CORDIC, FMAC) lassen sich die Signale schnell und Energie-effizient berechnen. Die USB Schnittstelle unterstützt USB-PD (Power Delivery).
Trotz all dem wurde die Stromaufnahme im Vergleich zu älteren Mainstream Serien deutlich reduziert, so dass sie nahe an die "Low Power" Familie heran kommt.
Wenn du einen Touch Sensor Controller (TSC), mehr Operationsverstärker oder mehr analoge Vergleicher brauchst, dann schaue dir die STM32F3 Serie an. Wenn du billigere Mikrocontroller in dieser Leistungsklasse suchst, oder Ethernet brauchst, dann schau dir die STM32C5 Serie an.
Modelle
| Size | x6 | x8 | xB | xC | xE |
x: K = 32 Pins C = 48 Pins R = 64 Pins M = 80 Pins V = 100 Pins P = 121 Pins Q = 128 Pins |
|---|---|---|---|---|---|---|
| Flash | 32 KiB | 64 KiB | 128 KiB | 256 KiB | 512 KiB | |
| RAM ↴ | 32 KiB | 32 KiB | 32 KiB | 112 KiB | 112 KiB | |
| STM32G431 | ||||||
| STM32G4411 | ||||||
| STM32G491 | ||||||
| STM32G4A11 | ||||||
| RAM ↴ | 128 KiB | 128 KiB | 128 KiB | |||
| STM32G4732 | ||||||
| STM32G4831,2 | ||||||
| STM32G4742,3 | ||||||
| STM32G4841,2,3 | ||||||
| all | ||||||
- mit AES Verschlüsselung 128 und 256 Bit
- mit FSMC parallel memory and display interface
- mit hochauflösendem Timer
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.
Zusätzliche Dokumentation:
- STM32G4 series - PDF Documentation z.B. Application Notes
- STM32G4 Online Training
- Newlib Dokumentation der Standard-C Library
- Description of STM32G4 HAL and low-layer drivers Beschreibung des Cube HAL Frameworks
- STM32 Cortex-M4 MCUs and MPUs Programming Manual Beschreibung des CPU Kerns
- ARM Cortex M4 Technical Reference Manual Beschreibung des CPU Kerns
- The Definitive Guide to ARM Cortex-M3 and Cortex-M4 Processors von Joseph Yiu
Elektrische Daten
Alle hier STM32G4 Chips kann man mit 1,8 bis 3,6 Volt betreiben. Die Operationsverstärker brauchen mindestens 2,0 Volt und die USB Schnittstelle braucht 3,3 V.
Viele I/O Pins sind 5 V tolerant, sie sind im Datenblatt mit "FT" gekennzeichnet. Die Spannung darf dabei zu keinem Zeitpunkt höher als VDD + 4 V sein.
- Digitale Ausgänge dürfen nur im open-drain Modus höher als VDD gezogen werden.
- Die internen Pull Widerstände vertragen maximal 4 V (falls aktiviert)
- Analog genutzte Eingänge vertragen maximal 4 V
Die Ausgänge sind einzeln mit 20 mA und alle zusammen mit 100 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 zuschaltbaren internen Pull-Up und Pull-Down Widerstände haben ungefähr 40 kΩ. Alle I/O Pins haben ungefähr 5 pF Kapazität.
Der NRST (Reset) Pin hat intern einen Pull-Up Widerstand und kann sowohl vom Chip selber als auch von außen auf Low gezogen werden. Intern erzeugte Reset-Impulse sind garantiert mindestens 20 µs lang.
Ausnahmen:
Für die Pins PC13, PC14 und PC15 gelten folgende Einschränkungen:
- Sie dürfen bei HIGH Pegel keinen Strom liefern.
- Sie dürfen bei LOW Pegel maximal 3 mA aufnehmen.
- Sie dürfen nur mit maximal 2 MHz angesteuert werden und mit maximal 30 pF belastet werden.
Boards
Nucleo-G431RB
Das Nucleo-G431RB Board (aus der Nucleo-64 Reihe) ist ein hochwertiges Starter-Set zum günstigen Preis um 20 €.
- ARM Cortex-M4F Mikrocontroller Modell STM32G431RBT6
- max. 170 MHz
- 128 KiB Flash
- 32 KiB RAM
- 12 DMA Kanäle
- RTC mit Kalender
- 51 GPIO Pins auf den Stiftleisten
- 2·ADC 12 Bit mit insgesamt 23 Eingängen
- 4·DAC 12 Bit, davon 2 extern und 2 intern (für Operationsverstärker und Vergleicher)
- 3·Operationsverstärker
- 4·analoge Vergleicher
- 3·I²C
- 5·U(S)ART
- 3·SPI, 2 davon mit I²S
- 1·USB
- 1·CAN
- 1·SAI
- 8·16 Bit Timer
- 1·32 Bit Timer
- 24 MHz Hauptquarz und 32,768 kHz Uhrenquarz
- Eine programmierbare LED an PA5, die bei High Pegel leuchtet
- Ein programmierbarer Taster, der PC13 auf High zieht, mit Pull-Down Widerstand
- Reset Taster
- Stromversorgung wahlweise über USB, 7-12 V, 5 V oder 3,3 V
- Buchsenleisten für Arduino Shields (aber nur einige Pins vertragen 5 Volt!)
- ST-Link in Version 3E
- Zum Programmieren und Debuggen
- Virtueller Memory-Stick zum Upload von *.hex und *.bin
- Virtueller COM Port (USB-UART) verbunden mit USART2 oder LPUART1 (PA2, PA3), unterstützt bis zu 16 MBaud
- SWD/SWV Kommunikation bis zu 24 MHz
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.
Nucleo-G431KB
Das Nucleo-G431KB Board (aus der Nucleo-32 Reihe) ist deutlich kleiner, obwohl es ebenfalls einen ST-Link Adapter enthält. Es kostet üblicherweise etwa 15 €.
- ARM Cortex-M4F Mikrocontroller Modell STM32G431KBT6
- max. 170 MHz
- 128 KiB Flash
- 32 KiB RAM
- 12 DMA Kanäle
- RTC mit Kalender
- 22 GPIO Pins auf den Stiftleisten
- 2·ADC 12 Bit mit insgesamt 8 Eingängen
- 4·DAC 12 Bit, davon 2 extern und 2 intern (für Operationsverstärker und Vergleicher)
- 3·Operationsverstärker mit 8 Eingängen
- 2·analoge Vergleicher
- 1·I²C
- 2·U(S)ART
- 1·SPI, 2 davon mit I²S
- 1·USB
- 1·CAN
- 8·16 Bit Timer
- 1·32 Bit Timer
- 24 MHz Hauptquarz und 32,768 kHz Uhrenquarz
- Eine programmierbare LED an PB3, die bei High Pegel leuchtet
- Reset Taster
- Stromversorgung wahlweise über USB, 7-12 V, 5 V oder 3,3 V
- ST-Link Adapter in Version 3E
- Zum Programmieren und Debuggen
- Virtueller Memory-Stick zum Upload von *.hex und *.bin
- Virtueller COM Port (USB-UART) verbunden mit USART2 oder LPUART1 (PA2, PA3), unterstützt bis zu 16 MBaud
- SWD/SWV Kommunikation bis zu 24 MHz
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 STM32G431 Core Board
Das WeAct STM32G431 Core Board gibt es in den beiden gezeigten Varianten mit fast identischer Schaltung. Es wird bei Aliexpress ab 4 € angeboten.
- ARM Cortex-M4F Mikrocontroller Modell STM32G431CBU6
- max. 170 MHz
- 128 KiB Flash
- 32 KiB RAM
- 12 DMA Kanäle
- RTC mit Kalender
- 32 GPIO Pins auf den Stiftleisten
- +2 wenn man auf USB verzichtet (PA11, PA12)
- +2 wenn man die SWD Stifte (PA13, PA14) mit zählt
- Nur für das kurze Board:
- +2 wenn man auf den Uhrenquarz verzichtet (PC14, PC15)
- +1 wenn man PB8 (= Boot0!) mit verwendet
- 2·ADC 12 Bit mit insgesamt 16 Eingängen
- 4·DAC 12 Bit, davon 2 extern und 2 intern (für Operationsverstärker und Vergleicher)
- 3·Operationsverstärker
- 4·analoge Vergleicher
- 3·I²C
- 3·U(S)ART
- 3·SPI, 2 davon mit I²S
- 1·USB
- 1·CAN
- 1·SAI
- 7·16 Bit Timer
- 1·32 Bit Timer
- 8 MHz Hauptquarz und 32,768 kHz Uhrenquarz
- Drei Taster
- Boot: zieht PB8 = Boot0 auf High, mit Pull-Down Widerstand
- User: zieht PC13 auf High, ohne Pull-Down Widerstand
- Reset
- Eine programmierbare blaue LED an PC6, die bei High Pegel leuchtet
- Die Power LED ist rot
- Stromversorgung wahlweise über USB, 3,3 V oder 5 V
- Separate Stiftleiste für die SWD Schnittstelle zum Anschluss des Debuggers
Der Spannungsregler kann leicht überhitzen wenn man ihn mit zusätzlichen Verbrauchern belastet.
Die Lötbrücken sind ab Werk so eingestellt, dass der USB-C Anschluss ohne Power Delivery verwendet wird.
Schaltplan normale Version, lange Version
Beispielprogramm
Beispiel für einen einfachen LED-Blinker an PA5 und PC6 auf Basis des CMSIS:
#include "stm32g4xx.h"
// delay loop for the default 16 MHz
void delay(uint32_t msec) {
for (uint32_t j=0; j < msec * 3200; j++) {
__NOP();
}
}
int main() {
// Enable Port A and C
SET_BIT(RCC->AHB2ENR, RCC_AHB2ENR_GPIOAEN);
SET_BIT(RCC->AHB2ENR, RCC_AHB2ENR_GPIOCEN);
// PA5 and PC6 = Output for LEDs
MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE5, 0b01 << GPIO_MODER_MODE5_Pos);
MODIFY_REG(GPIOC->MODER, GPIO_MODER_MODE6, 0b01 << GPIO_MODER_MODE6_Pos);
while(1) {
// LED on
GPIOA->BSRR = GPIO_BSRR_BS5;
GPIOC->BSRR = GPIO_BSRR_BS6;
delay(500);
// LED off
GPIOA->BSRR = GPIO_BSRR_BR5;
GPIOC->BSRR = GPIO_BSRR_BR6;
delay(500);
}
}
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 | I²C | I²C | I²C | SPI | SPI | USB |
|---|---|---|---|---|---|---|---|---|---|
| TxD,RxD | TxD,RxD | TxD,RxD | SCL,SDA | SCL,SDA | SCL,SDA | NSS,SCK, |
NSS,SCK, |
D-,D+ | |
| STM32F431,441 | PA9,10 | PA2,3 | PC10,11 | PC4,PA8 | PC8,9 | PA4-7 | PB12-15 | PA11,PA12 | |
| STM32F47x,48x | PA9,10 | PA2,3 | PC10,11 | PC4,PA8 | PC8,9 | PC6,7 | PA4-7 | PB12-15 | PA11,PA12 |
| STM32F49x,4Ax | PA9,10 | PA2,3 | PC10,11 | PC4,PA8 | PC8,9 | PA4-7 | PB12-15 | PA11,PA12 |
Wenn der Bootloader mit USB Power-Delivery (PD) verwendet wird, muss zusätzlich auch die Dead-Battery (DB) Funktion verbunden sein.
| USB-C Signal | für Power-Delivery | für Dead-Battery |
|---|---|---|
| CC1 | PB6 | PA9 |
| CC2 | PB4 | PA10 |
| VBUS | PB2 über einen Spannungsteiler |
Koprozessoren
FPU
Die Fließkomma-Einheit (FPU) beschleunigt die vier Grundrechenarten und Quadratwurzeln mit dem 32 Bit Fließkommazahlen etwa um Faktor 12. Das ist ähnlich schnell wie 32 Bit integer. Die Programmiersprachen C und C++ kennen zwei Fließkomma-Typen:
- `float` mit 32 Bit (FPU freundlich)
- `double` mit 64 Bit (Standard)
Die FPU muss vor ihrer ersten Verwendung eingeschaltet werden, sonst bricht das Programm mit einer HardFault Exception ab. Am Besten erledigt man das in der Funktion `SystemInit()`, weil diese Funktion vor vor der Initialisierung globaler Objekte und 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:
- -mfpu=fpv4-sp-d16 -mfloat-abi=hard
Theoretisch könnte man die Berechnung per Software ohne FPU mit `-mfloat-abi=soft` bewirken, aber die vorkompilierte C-Bibliothek funktioniert damit nicht.
Optional kannst du die folgenden Linker-Optionen aktivieren, damit Fließkomma Parameter von `printf()` und `scanf()` unterstützt werden:
- -u _printf_float -u _scanf_float
Beim Aufruf von Interrupt-Handlern reserviert die FPU Platz auf dem Stack, um dort später ihre Register bei Bedarf zu sichern. Dieses Verhalten kann in den Bits ASPEN und LSPEN im Register FPU->FPCCR verändert werden (siehe Programming Manual).
CORDIC
Der "Coordinate Rotation Digital Computer" (CORDIC) berechnet
- trigonometrische Winkelfunktionen (sin, cos, tan)
- hyperbolische Winkelfunktionen (sinh, cosh, tanh)
- natürliche Logarithmen (ln)
- Exponenten (exp)
- Quadratwurzeln (sqrt)
Der CORDIC verwendet Festpunkt-Zahlen in den einfachen Q-Formaten:
- q1.15: 16-Bit, davon sind 15 fractional Bits
- q1.31: 32-Bit, davon sind 31 fractional Bits (aber die Präzision ist auf 20-Bit beschränkt)
FMAC
Der "Filter Math Accelerator" (FMAC) implementiert digitale Filter. Sie werden in dieser Präsentation und in der Application Note AN5305 zur HAL beschrieben. Die direkte Programmierung über Register ist im Referenzhandbuch Kapitel 18 "Filter math accelerator" beschrieben. Der FMAC wird typischerweise mit DMA kombiniert.
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. Die Programmierung über Register ist im Referenzhandbuch unter dem Begriff "Cyclic redundancy check calculation unit" beschrieben. Die CRC Einheit kann mit DMA gefüttert werden.
AES
Zur Verschlüsselung von Datenströmen haben die STM32G441, STM32G4A1, STM32G483 und STM32G484 einen AES Beschleuniger. Dieser unterstützt 128 und 256 Bit Schlüssel. Er wird in dieser Präsentation vorgestellt. Die direkte Programmierung über Register ist im Referenzhandbuch Kapitel 16 "AES hardware accelerator" beschrieben. Der AES Beschleuniger kann seinen Datenstrom mit DMA einlesen und ausgeben.
Unterbrechungen
Interrupt-Vektoren
Hinter den ARM Processor Exceptions enthält die Interrupt Vektor Tabelle der STM32G4 folgende Einträge:
| Address | CMSIS Interrupt Nr. | C Function | Description | EXTI Channel |
|---|---|---|---|---|
| 0x0040 | 0 | WWDG_IRQHandler() | Window Watchdog | |
| 0x0044 | 1 | PVD_PVM_IRQHandler() | PVD Programmable Voltage Detector through EXTI line | 16 |
| 0x0048 | 2 | TAMP_STAMP_IRQHandler() | Tamper and TimeStamp through EXTI line | 19 |
| 0x004C | 3 | RTC_WKUP_IRQHandler() | RTC Real Time Clock Wakeup timer through EXTI line | 20 |
| 0x0050 | 4 | FLASH_IRQHandler() | Flash | |
| 0x0054 | 5 | RCC_IRQHandler() | RCC Reset and Clock Control | |
| 0x0058 | 6 | EXTI0_IRQHandler() | EXTI Line 0 | 0 |
| 0x005C | 7 | EXTI1_IRQHandler() | EXTI Line 1 | 1 |
| 0x0060 | 8 | EXTI2_IRQHandler() | EXTI Line 2 | 2 |
| 0x0064 | 9 | EXTI3_IRQHandler() | EXTI Line 3 | 3 |
| 0x0068 | 10 | EXTI4_IRQHandler() | EXTI Line 4 | 4 |
| 0x006C | 11 | DMA1_CH1_IRQHandler() | DMA1 channel 1 | |
| 0x0070 | 12 | DMA1_CH2_IRQHandler() | DMA1 channel 2 | |
| 0x0074 | 13 | DMA1_CH3_IRQHandler() | DMA1 channel 3 | |
| 0x0078 | 14 | DMA1_CH4_IRQHandler() | DMA1 channel 4 | |
| 0x007C | 15 | DMA1_CH5_IRQHandler() | DMA1 channel 5 | |
| 0x0080 | 16 | DMA1_CH6_IRQHandler() | DMA1 channel 6 | |
| 0x0084 | 17 | DMA1_CH7_IRQHandler() | DMA1 channel 7 | |
| 0x0088 | 18 | ADC1_2_IRQHandler() | ADC1 and ADC2 | |
| 0x008C | 19 | USB_HP_IRQHandler() | USB High Priority | |
| 0x0090 | 20 | USB_LP_IRQHandler() | USB Low Priority | |
| 0x0094 | 21 | FDCAN1_IT0_IRQHandler() | FDCAN1 interrupt 0 | |
| 0x0098 | 22 | FDCAN1_IT1_IRQHandler() | FDCAN1 interrupt 1 | |
| 0x009C | 23 | EXTI9_5_IRQHandler() | EXTI Lines 5-9 | 5-9 |
| 0x00A0 | 24 | TIM1_BRK_TIM15_IRQHandler() | TIM1 Break and TIM15 | |
| 0x00A4 | 25 | TIM1_UP_TIM16_IRQHandler() | TIM1 Update and TIM16 | |
| 0x00A8 | 26 | TIM1_TRG_COM_IRQHandler() | TIM1 trigger and commutation, TIM17 interrupts, TIM1 Direction Change, TIM1 Index | |
| 0x00AC | 27 | TIM1_CC_IRQHandler() | TIM1 capture compare | |
| 0x00B0 | 28 | TIM2_IRQHandler() | TIM2 | |
| 0x00B4 | 29 | TIM3_IRQHandler() | TIM3 | |
| 0x00B8 | 30 | TIM4_IRQHandler() | TIM4 | |
| 0x00BC | 31 | I2C1_EV_IRQHandler() | I2C1 event | 23 |
| 0x00C0 | 32 | I2C1_ER_IRQHandler() | 2C1 error | |
| 0x00C4 | 33 | I2C2_EV_IRQHandler() | I2C2 event | 24 |
| 0x00C8 | 34 | I2C2_ER_IRQHandler() | I2C2 error | |
| 0x00CC | 35 | SPI1_IRQHandler() | SPI1 | |
| 0x00D0 | 36 | SPI2_IRQHandler() | SPI2 | |
| 0x00D4 | 37 | USART1_IRQHandler() | USART1 | 25 |
| 0x00D8 | 38 | USART2_IRQHandler() | USART2 | 26 |
| 0x00DC | 39 | USART3_IRQHandler() | USART3 | 28 |
| 0x00E0 | 40 | EXTI15_10_IRQHandler() | EXTI line 10-15 | 10-15 |
| 0x00E4 | 41 | RTC_ALARM_IRQHandler() | RTC alarms | 17 |
| 0x00E8 | 42 | USBWakeUP_IRQHandler() | USB wake-up from suspend | 18 |
| 0x00EC | 43 | TIM8_BRK_IRQHandler() | TIM8 Break, TIM8 Transition error, TIM8 Index error | |
| 0x00F0 | 44 | TIM8_UP_IRQHandler() | TIM8 Update | |
| 0x00F4 | 45 | TIM8_TRG_COM_IRQHandler() | TIM8 trigger and commutation, TIM8 Direction Change, TIM8 Index | |
| 0x00F8 | 46 | TIM8_CC_IRQHandler() | TIM8 capture compare | |
| 0x00FC | 47 | ADC3_IRQHandler() | ADC3 | |
| 0x0100 | 48 | FSMC_IRQHandler() | FSMC | |
| 0x0104 | 49 | LPTIM1_IRQHandler() | LPTIM1 | |
| 0x0108 | 50 | TIM5_IRQHandler() | TIM5 | |
| 0x010C | 51 | SPI3_IRQHandler() | SPI3 | |
| 0x0110 | 52 | UART4_IRQHandler() | UART4 | |
| 0x0114 | 53 | UART5_IRQHandler() | UART5 | |
| 0x0118 | 54 | TIM6_DACUNDER_IRQHandler() | TIM6 and DAC1/3 underrun | |
| 0x011C | 55 | TIM7_IRQHandler() | TIM7 and DAC2/4 underrun | |
| 0x0120 | 56 | DMA2_CH1_IRQHandler() | DMA2 channel 1 | |
| 0x0124 | 57 | DMA2_CH2_IRQHandler() | DMA2 channel 2 | |
| 0x0128 | 58 | DMA2_CH3_IRQHandler() | DMA2 channel 3 | |
| 0x012C | 59 | DMA2_CH4_IRQHandler() | DMA2 channel 4 | |
| 0x0130 | 60 | DMA2_CH5_IRQHandler() | DMA2 channel 5 | |
| 0x0134 | 61 | ADC4_IRQHandler() | ADC4 | |
| 0x0138 | 62 | ADC5_IRQHandler() | ADC5 | |
| 0x013C | 63 | UCPD1_IRQHandler() through EXTI line | UCPD1 | 43 |
| 0x0140 | 64 | COMP1_2_3_IRQHandler() | COMP1, COMP2 and COMP3 through EXTI line | 21, 22, 29 |
| 0x0144 | 65 | COMP4_5_6_IRQHandler() | COMP4, COMP5 and COMP6 through EXTI line | 30, 31, 32 |
| 0x0148 | 66 | COMP7_IRQHandler() | COMP7 through EXTI line | 33 |
| 0x014C | 67 | HRTIM_Master_IRQn_IRQHandler() | HRTIM master timer interrupt (hrtim_it1) | |
| 0x0150 | 68 | HRTIM_TIMA_IRQn_IRQHandler() | HRTIM timer A interrupt (hrtim_it2) | |
| 0x0154 | 69 | HRTIM_TIMB_IRQn_IRQHandler() | HRTIM timer B interrupt (hrtim_it3) | |
| 0x0158 | 70 | HRTIM_TIMC_IRQn_IRQHandler() | HRTIM timer C interrupt (hrtim_it4) | |
| 0x015C | 71 | HRTIM_TIMD_IRQn_IRQHandler() | HRTIM timer D interrupt (hrtim_it5) | |
| 0x0160 | 72 | HRTIM_TIME_IRQn_IRQHandler() | HRTIM timer E interrupt (hrtim_it6) | |
| 0x0164 | 73 | HRTIM_TIM_FLT_IRQn_IRQHandler() | HRTIM fault interrupt (hrtim_it8) | |
| 0x0168 | 74 | HRTIM_TIMF_IRQn_IRQHandler() | hrtim_it7 / HRTIM timer F interrupt | |
| 0x016C | 75 | CRS_IRQHandler() | CRS Clock Recovery System | |
| 0x0170 | 76 | SAI_IRQHandler() | SAI | |
| 0x0174 | 77 | TIM20_BRK_IRQHandler() | TIM20 Break, TIM20 Transition error, TIM20 Index error | |
| 0x0178 | 78 | TIM20_UP_IRQHandler() | TIM20 Update | |
| 0x017C | 79 | TIM20_TRG_COM_IRQHandler() | TIM20 Trigger and commutation, TIM20 Direction Change, TIM20 Index | |
| 0x0180 | 80 | TIM20_CC_IRQHandler() | TIM20 capture compare | |
| 0x0184 | 81 | FPU_IRQHandler() | Floating point | |
| 0x0188 | 82 | I2C4_EV_IRQHandler() | I2C4 event | 42 |
| 0x018C | 83 | I2C4_ER_IRQHandler() | I2C4 error | |
| 0x0190 | 84 | SPI4_IRQHandler() | SPI4 global | |
| 0x0194 | 85 | AES_IRQHandler() | AES | |
| 0x0198 | 86 | FDCAN2_IT0_IRQHandler() | FDCAN2 Interrupt 0 | |
| 0x019C | 87 | FDCAN2_IT1_IRQHandler() | FDCAN2 Interrupt 1 | |
| 0x01A0 | 88 | FDCAN3_IT0_IRQHandler() | FDCAN3 Interrupt 0 | |
| 0x01A4 | 89 | FDCAN3_IT1_IRQHandler() | FDCAN3 Interrupt 1 | |
| 0x01A8 | 90 | RNG_IRQHandler() | RNG global | |
| 0x01AC | 91 | LPUART_IRQHandler() | LPUART global | |
| 0x01B0 | 92 | I2C3_EV_IRQHandler() | I2C3 event | 27 |
| 0x01B4 | 93 | I2C3_ER_IRQHandler() | I2C3 error | |
| 0x01B8 | 94 | DMAMUX_OVR_IRQHandler() | DMAMUX overrun | |
| 0x01BC | 95 | QUADSPI_IRQHandler() | QUADSPI global | |
| 0x01C0 | 96 | DMA1_CH8_IRQHandler() | DMA1 channel 8 | |
| 0x01C4 | 97 | DMA2_CH6_IRQHandler() | DMA2 channel 6 | |
| 0x01C8 | 98 | DMA2_CH7_IRQHandler() | DMA2 channel 7 | |
| 0x01CC | 99 | DMA2_CH8_IRQHandler() | DMA2 channel 8 | |
| 0x01D0 | 100 | Cordic_IRQHandler() | Cordic | |
| 0x01D4 | 101 | FMAC_IRQHandler() | FMAC |
Extended Interrupts
Die externen Interrupt-Signale und auch einige interne durchlaufen einen erweiterten Schaltkreis, der zusätzliche Konfiguration erfordert. Du findest sie im Referenzhandbuch Kapitel 15.4 "EXTI interrupt/event line mapping".
Die Kanäle EXTI0 bis EXT15 sind für I/O-Pins reserviert. Jeden Kanal kann man in den Registern AFIO->EXTICR[0-3] genau einem Port zuweisen. Wenn man zum Beispiel Kanal 0 dem Port A zuweist (also PA0), kann man auf den anderen Ports das Bit 0 nicht mehr für Interrupts verwenden. Diese Einschränkung gilt für alle 16 Kanäle.
Interrupt Flanken
Viele erweiterte Unterbrechungen werden durch Flanken (Signalwechsel) ausgelöst. Man erkennt sie an den Bits im Register EXTI->RTSR oder EXTI->FTSR.
- Steigende Flanke: Wenn man im Register EXTI->RTSR1/2 ein Bit setzt, dann startet die Interruptroutine beim Signalwechsel von Low nach High.
- Fallende Flanke: Wenn man im Register EXTI->FTSR1/2 ein Bit setzt, dann startet die Interruptroutine beim Signalwechsel von High nach Low.
Durch die Verwendung von Flanken ist automatisch sicher gestellt, dass der Handler nicht immer wieder erneut ausgeführt wird, falls ein Interrupt Signal für längere Zeit ansteht.
Flanken gesteuerte Interrupts gehen nicht verloren, wenn das Signal schon wieder verschwindet bevor der Handler aufgerufen wurde. Der Interrupt-Controller merkt sich, dass da mal eine Anforderung vorlag, die noch nicht abgearbeitet wurde.
In diesem Zusammenhang ist das Register EXTI->PR1/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 "stm32g4xx.h"
// Delay loop for the default 16 MHz
void delay(uint32_t msec) {
for (uint32_t j=0; j < msec * 3200; j++) {
__NOP();
}
}
void EXTI15_10_IRQHandler() {
// Clear pending interrupt flag
// It is important that this is not the last command in the ISR
SET_BIT(EXTI->PR1, EXTI_PR1_PIF13);
// LED on
GPIOC->BSRR = GPIO_BSRR_BS6;
delay(100);
// LED off
GPIOC->BSRR = GPIO_BSRR_BR6;
}
int main() {
// Enable port C and system config
SET_BIT(RCC->AHB2ENR, RCC_AHB2ENR_GPIOCEN);
SET_BIT(RCC->APB2ENR, RCC_APB2ENR_SYSCFGEN);
// PC6 = Output for LED
MODIFY_REG(GPIOC->MODER, GPIO_MODER_MODE6, 0b01 << GPIO_MODER_MODE6_Pos);
// PC13 = Input with pull-down
MODIFY_REG(GPIOC->MODER, GPIO_MODER_MODE13, 0b00 << GPIO_MODER_MODE13_Pos);
MODIFY_REG(GPIOC->PUPDR, GPIO_PUPDR_PUPD13, 0b10 << GPIO_PUPDR_PUPD13_Pos);
// Assign EXTI13 to PC13 with rising edge
MODIFY_REG(SYSCFG->EXTICR[3], SYSCFG_EXTICR4_EXTI13, SYSCFG_EXTICR4_EXTI13_PC);
SET_BIT(EXTI->IMR1, EXTI_IMR1_IM13);
SET_BIT(EXTI->RTSR1, EXTI_RTSR1_RT13);
// Enable the interrupt handler call
NVIC_EnableIRQ(EXTI15_10_IRQn);
// Clear pending interrupt flag
SET_BIT(EXTI->PR1, EXTI_PR1_PIF13);
// Endless loop
while (1) {}
}
Event Masken
Ereignisse verwendet man üblicherweise, um die CPU aus aus einem Wait oder Sleep Zustand aufzuwecken. So hält zum Beispiel der Befehl `__WFE()` (aus dem Powermanagement) die CPU bis zum nächsten Ereignis an.
Standardmäßig sind alle Ereignisse maskiert. Man muss im Register EXTI->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 16 MHz 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 durch Setzen von Bits in den ENR Registern erledigt wird:
- RCC->APB1ENR1, RCC->APB1ENR2
- RCC->APB2ENR
- RCC->AHB1ENR
- RCC->AHB2ENR
- RCC->AHB3ENR
Jetzt kommt der komplizierte Teil. Der System-Takt (SYSCLK) kann aus folgenden Quellen bezogen werden:
- HSE Quarz-Oszillator (HSEBYP=0) oder externe Quelle(HSEBYP=1) mit 4-48 MHz (maximal 26 MHz im "Voltage Scaling Range 2" )
- HSI Interner 16 MHz Oszillator, auf 1% kalibriert (bei 3,3 V und 0 bis 90 °C), kalibriert sich mit CRS am USB Signal oder Uhrenquarz (LSE)
Für Watchog und Echtzeit-Uhr (RTC) gibt es weitere Taktgeber:
- LSE Ist ein guter 32 kHz Quarz-Oszillator für die Echtzeituhr
- LSI Ist ein interner Oszillator mit unghefähr 32 kHz für den Watchdog
- HSI48 Interner 48 MHz Oszillator, kalibriert sich selbst am USB Signal
Mehrere Vorteiler und ein PLL Multiplikator können kombiniert werden, um unterschiedliche Taktfrequenzen zu erreichen. Am besten schaut man sich das in STM32 Cube MX an. Dort kann man schön sehen, welche Parameter konfigurierbar sind und wie sie zusammen wirken.
Das folgende Bildschirmfoto zeigt die Standardvorgabe vom STM32G484VE nach einem Reset:
Die maximal zulässige Taktfrequenz hängt von der internen Spannungsversorgung "Voltage Scaling" ab:
- Range 1: High-Performance
- normal mode: 1,2 V für max. 150 MHz (Standardvorgabe nach Reset)
- boost mode: 1,28 V für max. 170 MHz
- Range 2: Low-Power
- 1,0 V für max. 26 MHz
- Das Umschalten der internen Spannungsversorgung erfordert eine besondere Prozedur, die im Referenzhandbuch Kapitel 6.1.5 "Dynamic voltage scaling management" beschrieben ist. Die dazu relevanten Bits sind VOS im PWR->CR1 Register, das R1MODE Bit in PWR->CR5 und das VOSF Bit in PWR->SR2.
- Abhängig von der Versorgungsspannung und Taktfrequenz müssen Wait-States im FLASH->ACR Register eingestellt werden. Siehe dazu das Kapitel 3.3.3 im Referenzhandbuch "Read access latency".
- Eine bereits aktivierte PLL muss deaktiviert werden, bevor man sie Umkonfigurieren kann.
Beispiel für den STM32G431CB, 64 MHz mit dem internen HSI Oszillator:
#include "stm32g4xx.h"
// The current clock frequency
uint32_t SystemCoreClock=16000000;
// Change system clock to 64 MHz using internal 16 MHz oscillator
void init_clock() {
// 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);
// Configure the PLL
MODIFY_REG(RCC->PLLCFGR, RCC_PLLCFGR_PLLSRC, RCC_PLLCFGR_PLLSRC_HSI); // PLL source is HSI
MODIFY_REG(RCC->PLLCFGR, RCC_PLLCFGR_PLLM, 0b0000 << RCC_PLLCFGR_PLLM_Pos); // PLLM divisor 1
MODIFY_REG(RCC->PLLCFGR, RCC_PLLCFGR_PLLN, 8 << RCC_PLLCFGR_PLLN_Pos); // PLLN multiplier 8
MODIFY_REG(RCC->PLLCFGR, RCC_PLLCFGR_PLLR, 0b00 << RCC_PLLCFGR_PLLR_Pos); // PLLR divisor 2
SET_BIT(RCC->PLLCFGR, RCC_PLLCFGR_PLLREN); // enable output R
// 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 system timer
SystemCoreClock=64000000;
// SysTick_Config(SystemCoreClock/1000); // comment out if you have no SysTick_Handler()
}
// Delay loop for 64 MHz
void delay(uint32_t msec) {
for (uint32_t j=0; j < msec * 12800; j++) {
__NOP();
}
}
int main() {
init_clock();
// Enable port C
SET_BIT(RCC->AHB2ENR, RCC_AHB2ENR_GPIOCEN);
// PC6 = Output for LED
MODIFY_REG(GPIOC->MODER, GPIO_MODER_MODE6, 0b01 << GPIO_MODER_MODE6_Pos);
// Endless loop
while (1) {
// LED on
GPIOC->BSRR = GPIO_BSRR_BS6;
delay(100);
// LED off
GPIOC->BSRR = GPIO_BSRR_BR6;
delay(900);
}
}
#include "stm32g4xx.h"
// The current clock frequency
uint32_t SystemCoreClock=16000000;
// Change system clock to 170 MHz using 8 MHz crystal
void init_clock() {
// Divide AHB clock by 2 before switching to high frequency > 150 MHz
MODIFY_REG(RCC->CFGR, RCC_CFGR_HPRE, RCC_CFGR_HPRE_DIV2);
// Wait until voltage regulator is stabilized
while(READ_BIT(PWR->SR2, PWR_SR2_VOSF)) {}
// Switch main regulator to boost mode
CLEAR_BIT(PWR->CR5, PWR_CR5_R1MODE);
// Wait until voltage regulator is stabilized
while(READ_BIT(PWR->SR2, PWR_SR2_VOSF)) {}
// Enable HSE oscillator
// SET_BIT(RCC->CR, RCC_CR_HSEBYP); // needed for external clock source (not crystal)
SET_BIT(RCC->CR, RCC_CR_HSEON);
// Wait until HSE oscillator is ready
while(!READ_BIT(RCC->CR, RCC_CR_HSERDY)) {}
// Switch to HSE oscillator
MODIFY_REG(RCC->CFGR, RCC_CFGR_SW, RCC_CFGR_SW_HSE);
// Wait until the switch is done
while ((RCC->CFGR & RCC_CFGR_SWS_Msk) != RCC_CFGR_SWS_HSE) {}
// 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 4 wait states
MODIFY_REG(FLASH->ACR, FLASH_ACR_LATENCY, 4 << FLASH_ACR_LATENCY_Pos);
// Configure the PLL
MODIFY_REG(RCC->PLLCFGR, RCC_PLLCFGR_PLLSRC, RCC_PLLCFGR_PLLSRC_HSE); // PLL source is HSE
MODIFY_REG(RCC->PLLCFGR, RCC_PLLCFGR_PLLM, 0b0001 << RCC_PLLCFGR_PLLM_Pos); // PLLM divisor 2
MODIFY_REG(RCC->PLLCFGR, RCC_PLLCFGR_PLLN, 85 << RCC_PLLCFGR_PLLN_Pos); // PLLN multiplier 85
MODIFY_REG(RCC->PLLCFGR, RCC_PLLCFGR_PLLR, 0b00 << RCC_PLLCFGR_PLLR_Pos); // PLLR divisor 2
SET_BIT(RCC->PLLCFGR, RCC_PLLCFGR_PLLREN); // enable output R
// 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 system timer
SystemCoreClock=170000000;
// SysTick_Config(SystemCoreClock/1000); // comment out if you have no SysTick_Handler()
// Wait for at least 1us
for (uint32_t j=0; j < 100; j++) {
__NOP();
}
// Do not divide AHB clock anymore
MODIFY_REG(RCC->CFGR, RCC_CFGR_HPRE, RCC_CFGR_HPRE_DIV1);
// Disable HSI oscillator
CLEAR_BIT(RCC->CR, RCC_CR_HSION);
}
// Delay loop for 170 MHz
void delay(uint32_t msec) {
for (uint32_t j=0; j < msec * 34000; j++) {
__NOP();
}
}
int main() {
init_clock();
// Enable port C
SET_BIT(RCC->AHB2ENR, RCC_AHB2ENR_GPIOCEN);
// PC6 = Output for LED
MODIFY_REG(GPIOC->MODER, GPIO_MODER_MODE6, 0b01 << GPIO_MODER_MODE6_Pos);
// Endless loop
while (1) {
// LED on
GPIOC->BSRR = GPIO_BSRR_BS6;
delay(100);
// LED off
GPIOC->BSRR = GPIO_BSRR_BR6;
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 "Input/output AC characteristics" im Datenblatt). Damit beeinflusst man, wie schnell die Spannung von Low nach High und zurück wechselt. Der maximale Laststrom wird dadurch nicht verändert. Im Sinne von elektromagnetischer Verträglichkeit soll man hier immer den niedrigsten Wert einstellen, der zur Anwendung passt.
Schaue Dir die Beschreibung der GPIO Register im Referenzhandbuch Kapitel 9.4 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_IN4 MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE3, 0b11 << GPIO_MODER_MODE3_Pos);
Das folgende Programm initialisiert den ADC (samt optionaler Kalibrierung für genauere Ergebnisse) und liest den Eingang PA3 wiederholt in die Variable a ein.
#include "stm32g4xx.h"
// delay loop for the default 16 MHz
void delay(uint32_t msec) {
for (uint32_t j=0; j < msec * 3200; j++) {
__NOP();
}
}
// Initialize ADC1 for single conversion mode
void init_analog() {
// Enable ADC
SET_BIT(RCC->AHB2ENR, RCC_AHB2ENR_ADC12EN);
// Exit ADC deep power down mode
CLEAR_BIT(ADC1->CR, ADC_CR_DEEPPWD);
// Enable ADC voltage regulator
SET_BIT(ADC1->CR, ADC_CR_ADVREGEN);
// Delay 1-2 ms
delay(2);
// ADC Clock = HCLK/4
MODIFY_REG(ADC12_COMMON->CCR, ADC_CCR_CKMODE, 0b11 << ADC_CCR_CKMODE_Pos);
// Single ended mode for all channels
ADC1->DIFSEL = 0;
// Start calibration for single ended mode
CLEAR_BIT(ADC1->CR, ADC_CR_ADCALDIF);
SET_BIT(ADC1->CR, ADC_CR_ADCAL);
// Wait until the calibration is finished
while (READ_BIT(ADC1->CR, ADC_CR_ADCAL));
// Clear the ready flag
SET_BIT(ADC1->ISR, ADC_ISR_ADRDY);
// Enable the ADC and wait until it is ready
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 for all channels
MODIFY_REG(ADC1->SMPR1, ADC_SMPR1_SMP0, 0b100 << ADC_SMPR1_SMP0_Pos);
MODIFY_REG(ADC1->SMPR1, ADC_SMPR1_SMP1, 0b100 << ADC_SMPR1_SMP1_Pos);
MODIFY_REG(ADC1->SMPR1, ADC_SMPR1_SMP2, 0b100 << ADC_SMPR1_SMP2_Pos);
MODIFY_REG(ADC1->SMPR1, ADC_SMPR1_SMP3, 0b100 << ADC_SMPR1_SMP3_Pos);
MODIFY_REG(ADC1->SMPR1, ADC_SMPR1_SMP4, 0b100 << ADC_SMPR1_SMP4_Pos);
MODIFY_REG(ADC1->SMPR1, ADC_SMPR1_SMP5, 0b100 << ADC_SMPR1_SMP5_Pos);
MODIFY_REG(ADC1->SMPR1, ADC_SMPR1_SMP6, 0b100 << ADC_SMPR1_SMP6_Pos);
MODIFY_REG(ADC1->SMPR1, ADC_SMPR1_SMP7, 0b100 << ADC_SMPR1_SMP7_Pos);
MODIFY_REG(ADC1->SMPR1, ADC_SMPR1_SMP8, 0b100 << ADC_SMPR1_SMP8_Pos);
MODIFY_REG(ADC1->SMPR1, ADC_SMPR1_SMP9, 0b100 << ADC_SMPR1_SMP9_Pos);
MODIFY_REG(ADC1->SMPR2, ADC_SMPR2_SMP10, 0b100 << ADC_SMPR2_SMP10_Pos);
MODIFY_REG(ADC1->SMPR2, ADC_SMPR2_SMP11, 0b100 << ADC_SMPR2_SMP11_Pos);
MODIFY_REG(ADC1->SMPR2, ADC_SMPR2_SMP12, 0b100 << ADC_SMPR2_SMP12_Pos);
MODIFY_REG(ADC1->SMPR2, ADC_SMPR2_SMP13, 0b100 << ADC_SMPR2_SMP13_Pos);
MODIFY_REG(ADC1->SMPR2, ADC_SMPR2_SMP14, 0b100 << ADC_SMPR2_SMP14_Pos);
MODIFY_REG(ADC1->SMPR2, ADC_SMPR2_SMP15, 0b100 << ADC_SMPR2_SMP15_Pos);
MODIFY_REG(ADC1->SMPR2, ADC_SMPR2_SMP16, 0b100 << ADC_SMPR2_SMP16_Pos);
MODIFY_REG(ADC1->SMPR2, ADC_SMPR2_SMP17, 0b100 << ADC_SMPR2_SMP17_Pos);
MODIFY_REG(ADC1->SMPR2, ADC_SMPR2_SMP18, 0b100 << ADC_SMPR2_SMP18_Pos);
}
// 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;
}
int main() {
init_analog();
// Enable port A
SET_BIT(RCC->AHB2ENR, RCC_AHB2ENR_GPIOAEN);
// Configure PA3 as analog input for ADC1_IN4
MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE3, 0b11 << GPIO_MODER_MODE3_Pos);
volatile uint32_t a;
while (1) {
a=read_analog(4);
}
}
Du kannst es im Debugger testen oder eine Textausgabe hinzufügen. Eine Eingangsspannung von 0-3,3 V führt zu einem Messwert von 0-4095.
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.- Der High-Resolution Timer kann auf 12 Ausgängen 16-Bit PWM erzeugen.
- Die Timer 1, 8 und 20 können jeweils auf 6 Ausgängen 16-Bit PWM erzeugen.
- Die Timer 2 und 5 können jeweils auf 4 Ausgängen 32-Bit PWM erzeugen.
- Die Timer 3 und 4 können jeweils auf 4 Ausgängen 16-Bit PWM erzeugen.
- Der Timer 15 kann auf 2 Ausgängen 16-Bit PWM erzeugen.
- Die Timer 16 und 17 können jeweils auf einem Ausgang 16-Bit PWM erzeugen.
Die Taktfrequenz der Timer wird normalerweise vom Systemtakt abgeleitet. Die Relevanten Register sind:
- RCC->PLLCFGR: PLL
- RCC->CFGR: AHB, APB1 und APB2 Prescaler
- TIMx->PSC: Timer Prescaler (hat nicht jeder Timer)
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 3 (PC6), um eine Leuchtdiode unterschiedlich hell leuchten zu lassen:
#include "stm32g4xx.h"
// delay loop for the default 16 MHz
void delay(uint32_t msec) {
for (uint32_t j=0; j < msec * 3200; j++) {
__NOP();
}
}
int main() {
// Enable port C and timer 3
SET_BIT(RCC->AHB2ENR, RCC_AHB2ENR_GPIOCEN);
SET_BIT(RCC->APB1ENR1, RCC_APB1ENR1_TIM3EN);
// PC6 = TIM3_CH1 alternate function (see data sheet)
MODIFY_REG(GPIOC->MODER, GPIO_MODER_MODE6, 0b10 << GPIO_MODER_MODE6_Pos); // alternate function mode
MODIFY_REG(GPIOC->AFR[0], GPIO_AFRL_AFSEL6, 2 << GPIO_AFRL_AFSEL6_Pos); // alternate function 2
// Timer 3 channel 1 compare mode = PWM1 with the required preload buffer enabled
MODIFY_REG(TIM3->CCMR1, TIM_CCMR1_OC1M, 0b110 << TIM_CCMR1_OC1M_Pos);
SET_BIT(TIM3->CCMR1, TIM_CCMR1_OC1PE);
// Timer 2 enable channel 1 output
SET_BIT(TIM3->CCER, TIM_CCER_CC1E);
// Timer 3 inverse polarity for channel 1
// SET_BIT(TIM3->CCER, TIM_CCER_CC1P);
// Timer 3 clock prescaler, divide clock by 16
TIM3->PSC = 15; // the PCLK2 clock is divided by this value +1
// Timer 3 auto reload register, defines the maximum value of the counter
TIM3->ARR = 50000; // 16000000/16/50000 = 20 pulses per second
// Timer 3 enable counter and auto-preload
SET_BIT(TIM3->CR1, TIM_CR1_CEN + TIM_CR1_ARPE);
// Start with short pulses / low brightness
uint32_t pulsewidth = 90;
// Endless loop
while(1) {
TIM3->CCR1 = pulsewidth;
delay(500);
// Increase brightness
pulsewidth=pulsewidth*2;
// start over when the maximum value has been reached
if (pulsewidth > TIM3->ARR) {
pulsewidth = 90;
}
}
}
Ich habe hier absichtlich eine sehr niedrige PWM Frequenz gewählt, damit man das Pulsieren der LED sehen kann. In einer realen Anwendung würde man natürich eine höhere PWM Frequenz über 100 Hz wählen.
Die Timer 1, 8, 15, 16, 17 und 20 können komplementäre Ausgangssignale mit Tot-Zeit erzeugen, was für den Eigenbau von H-Brücken nützlich ist.
Ich möchte darauf hinweisen, dass die Timer noch viele weitere Funktionen haben, die ich hier gar nicht alle zeigen kann. Mehr Informationen dazu gibt es zum Beispiel in der Application Note AN4776.
USART Schnittstelle
Je nach Taktfrequenz der Peripherie sind unterschiedliche Baudraten möglich. Beispiele:
- Bei 16 MHz: bis 1000000 Baud
- Bei 32 MHz: bis 2000000 Baud
- Bei 64 MHz: bis 4000000 Baud
- Bei 128 MHz: bis 8000000 Baud
Die serielle Schnittstelle USART1 liegt auf PA9 (TxD) und PA10 (RxD). Das Beispielprogramm sendet regelmäß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 "stm32g4xx.h"
uint32_t SystemCoreClock=16000000;
// delay loop for the default 16 MHz
void delay(uint32_t msec) {
for (uint32_t j=0; j < msec * 3200; j++) {
__NOP();
}
}
// Use serial port for standard output
int _write(int _, char *ptr, int len) {
for (int i=0; i<len; i++) {
// wait until TX buffer is empty
while(!(USART1->ISR & USART_ISR_TXE));
// write one character to the transmit data register
USART1->TDR = *ptr++;
}
return len;
}
// Called after each received character
void USART1_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, C and USART1
SET_BIT(RCC->AHB2ENR, RCC_AHB2ENR_GPIOAEN);
SET_BIT(RCC->AHB2ENR, RCC_AHB2ENR_GPIOCEN);
SET_BIT(RCC->APB2ENR, RCC_APB2ENR_USART1EN);
// PC6 = Output for LED
MODIFY_REG(GPIOC->MODER, GPIO_MODER_MODE6, 0b01 << GPIO_MODER_MODE6_Pos);
// Use system clock for USART1
MODIFY_REG(RCC->CCIPR, RCC_CCIPR_USART1SEL, 0b01 << RCC_CCIPR_USART1SEL_Pos);
// PA9 (TxD) shall use the alternate function 7 (see data sheet)
MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE9, 0b10 << GPIO_MODER_MODE9_Pos); // alternate function mode
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)
MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE10, 0b10 << GPIO_MODER_MODE10_Pos); // alternate function mode
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
GPIOC->BSRR = GPIO_BSRR_BS_6;
delay(500);
puts("Hello");
// LED off
GPIOC->BSRR = GPIO_BSRR_BR_6;
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 "stm32g4xx.h"
#include <stdbool.h>
// delay loop for the default 16 MHz
void delay(uint32_t msec) {
for (uint32_t j=0; j < msec * 3200; 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 HSI as clock source for the I²C interface
#ifdef I2C1
if (registerStruct==I2C1) {
MODIFY_REG(RCC->CCIPR, RCC_CCIPR_I2C1SEL, 0b00 << RCC_CCIPR_I2C1SEL_Pos);
SET_BIT(RCC->APB1ENR1, RCC_APB1ENR1_I2C1EN);
}
#endif
#ifdef I2C2
if (registerStruct==I2C2) {
MODIFY_REG(RCC->CCIPR, RCC_CCIPR_I2C2SEL, 0b00 << RCC_CCIPR_I2C2SEL_Pos);
SET_BIT(RCC->APB1ENR1, RCC_APB1ENR1_I2C2EN);
}
#endif
#ifdef I2C3
if (registerStruct==I2C3) {
MODIFY_REG(RCC->CCIPR, RCC_CCIPR_I2C3SEL, 0b00 << RCC_CCIPR_I2C3SEL_Pos);
SET_BIT(RCC->APB1ENR1, RCC_APB1ENR1_I2C3EN);
}
#endif
#ifdef I2C4
if (registerStruct==I2C4) {
MODIFY_REG(RCC->CCIPR2, RCC_CCIPR2_I2C4SEL, 0b00 << RCC_CCIPR2_I2C4SEL_Pos);
SET_BIT(RCC->APB1ENR2, RCC_APB1ENR2_I2C4EN);
}
#endif
// Disable the I²C peripheral
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;
MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_PRESC, (divisor-1) << I2C_TIMINGR_PRESC_Pos);
MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_SCLL, (i2c_clock/850000-1) << I2C_TIMINGR_SCLL_Pos);
MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_SCLH, (i2c_clock/850000-1) << I2C_TIMINGR_SCLH_Pos);
MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_SDADEL, (i2c_clock/8000000) << I2C_TIMINGR_SDADEL_Pos); // no -1 on purpose
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;
MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_PRESC, (divisor-1) << I2C_TIMINGR_PRESC_Pos);
MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_SCLL, (i2c_clock/200000-1) << I2C_TIMINGR_SCLL_Pos);
MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_SCLH, (i2c_clock/200000-1) << I2C_TIMINGR_SCLH_Pos);
MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_SDADEL, (i2c_clock/2000000) << I2C_TIMINGR_SDADEL_Pos); // no -1 on purpose
MODIFY_REG(registerStruct->TIMINGR, I2C_TIMINGR_SCLDEL, (i2c_clock/1000000-1) << I2C_TIMINGR_SCLDEL_Pos);
}
// Stop and Restart signals will be generated by software
CLEAR_BIT(registerStruct->CR2, I2C_CR2_AUTOEND);
// Enable the I²C peripheral
SET_BIT(registerStruct->CR1, I2C_CR1_PE);
}
/**
* Sub-Function of i2c_communicate.
* Configures number of data bytes to send or receive in the current block.
*/
void configureBlockSize(I2C_TypeDef* registerStruct, int size) {
if (size>255) {
// Set number of bytes to send or receive in this block
MODIFY_REG(registerStruct->CR2, I2C_CR2_NBYTES, 255 << I2C_CR2_NBYTES_Pos);
// Prepare to transfer more blocks after this one
SET_BIT(registerStruct->CR2, I2C_CR2_RELOAD);
}
else {
// Number of bytes to send or receive in the last block
MODIFY_REG(registerStruct->CR2, I2C_CR2_NBYTES, size << I2C_CR2_NBYTES_Pos);
// After this block, no more blocks will be transferred
CLEAR_BIT(registerStruct->CR2, I2C_CR2_RELOAD);
}
}
/**
* Perform an I²C transaction, which sends 0 or more data bytes, followed by receiving 0 or more data bytes.
*
* @param registerStruct May be either I2C1, I2C2, 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)
MODIFY_REG(registerStruct->CR2, I2C_CR2_SADD, slave_addr << 1);
// Send data
if (send_size>0) {
// Data direction
CLEAR_BIT(registerStruct->CR2, I2C_CR2_RD_WRN);
// Configure size of the first data block to send
configureBlockSize(registerStruct, send_size);
// Send start condition
SET_BIT(registerStruct->CR2, I2C_CR2_START);
// Send data
do {
// Check for error
if (READ_BIT(registerStruct->ISR, I2C_ISR_NACKF | I2C_ISR_ARLO)) {
goto error;
}
// Send one byte when ready
if (READ_BIT(registerStruct->ISR, I2C_ISR_TXIS)) {
registerStruct->TXDR = *((uint8_t*)send_buffer);
send_buffer++;
send_size--;
}
// Configure size of next block, if requested
if (READ_BIT(registerStruct->ISR, I2C_ISR_TCR)) {
configureBlockSize(registerStruct, send_size);
}
}
// Loop until the transfer is complete
while (!READ_BIT(registerStruct->ISR, I2C_ISR_TC));
}
// Sending succeeded, start counting the received bytes
receive_count=0;
// Receive data
if (receive_size>0) {
// Data direction
SET_BIT(registerStruct->CR2, I2C_CR2_RD_WRN);
// Configure size of the first data block to receive
configureBlockSize(registerStruct, receive_size);
// Send start or restart condition
SET_BIT(registerStruct->CR2, I2C_CR2_START);
// Receive data
do {
// Check for error
if (READ_BIT(registerStruct->ISR, I2C_ISR_ARLO)) {
goto error;
}
// Fetch one received byte when ready
if (READ_BIT(registerStruct->ISR, I2C_ISR_RXNE)) {
*((uint8_t*)receive_buffer) = registerStruct->RXDR;
receive_buffer++;
receive_count++;
receive_size--;
}
// Configure size of next block, if requested
if (READ_BIT(registerStruct->ISR, I2C_ISR_TCR)) {
configureBlockSize(registerStruct, receive_size);
}
}
// Loop until the transfer is complete
while (!READ_BIT(registerStruct->ISR, I2C_ISR_TC));
}
// Send stop condition
SET_BIT(registerStruct->CR2, I2C_CR2_STOP);
return receive_count;
error:
// Restart the I²C peripheral
CLEAR_BIT(registerStruct->CR1, I2C_CR1_PE);
SET_BIT(registerStruct->CR1, I2C_CR1_PE);
//ITM_SendString("I2C bus error!\n");
return receive_count;
}
/**
* Initialize the I/O pins.
*/
void init_io() {
// Enable port A and C
SET_BIT(RCC->AHB2ENR, RCC_AHB2ENR_GPIOAEN);
SET_BIT(RCC->AHB2ENR, RCC_AHB2ENR_GPIOCEN);
// PC6 = Output for LED
MODIFY_REG(GPIOC->MODER, GPIO_MODER_MODE6, 0b01 << GPIO_MODER_MODE6_Pos);
// I2C2 PA8=SDA, alternate function 4 open-drain (for external pull-ups)
MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE8, 0b10 << GPIO_MODER_MODE8_Pos); // alternate function mode
MODIFY_REG(GPIOA->AFR[1], GPIO_AFRH_AFSEL8, 4 << GPIO_AFRH_AFSEL8_Pos); // alternate function 4
SET_BIT(GPIOA->OTYPER, GPIO_OTYPER_OT8); // open-drain
// I2C2 PA9=SCL, alternate function 4 open-drain (for external pull-ups)
MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE9, 0b10 << GPIO_MODER_MODE9_Pos); // alternate function mode
MODIFY_REG(GPIOA->AFR[1], GPIO_AFRH_AFSEL9, 4 << GPIO_AFRH_AFSEL9_Pos); // alternate function 4
SET_BIT(GPIOA->OTYPER, GPIO_OTYPER_OT9); // open-drain
}
int main() {
init_io();
i2c_init(I2C2, true, 16000000);
while (1) {
// LED on
GPIOC->BSRR = GPIO_BSRR_BS6;
delay(10);
uint8_t send_buffer[]={0};
uint8_t receive_buffer[5];
i2c_communicate(I2C2, 8, send_buffer, 1, receive_buffer, 5);
// LED off
GPIOC->BSRR = GPIO_BSRR_BR6;
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, erfordert aber auch ein umfangreiches Softwarepaket. Beinahe alle Programmierer binden dazu fertige Bibliotheken in ihr Programm ein.
ST hat für das HAL Framework die USBD Middleware erstellt. Als Alternative dazu bietet sich TinyUSB an (siehe Beispiel auf der STM32C5 Seite). Weiter unten stelle ich einen weiteren besonders kompakten Code aus der Community vor.
Die USB Schnittstelle muss mit 48 MHz getaktet werden, vorzugsweise aus einem Quarz. Der interne 16 MHz HSI Oszilator ist ungeeignet. Der interne HSI48 Oszillator ist nur nur mit aktiviertem Clock-Recovery System (CRS) geeignet. Der APB1 Bus muss mit mindestens 10 MHz getaktet werden.
Die USB Buchse wird mit PA11 (D-) und PA12 (D+) verbunden. Es ist nicht nötig, den Modus (in, out, alternative) dieser Pins zu konfigurieren. Die USB Hardware steuert das automatisch.
Der Chip hat einen internen 1,5k Ω Pull-Up Widerstand an D+, der im Register USB->BCDR Bit 15 (DPPU) eingeschaltet wird. Indem man den Pull-Up Widerstand aus und wieder ein schaltet, kann man den "Host" Computer dazu bringen, das USB Gerät erneut zu erkennen, ohne das Kabel abstecken zu müssen.
USB und CAN können gleichzeitig verwendet werden.
Virtueller COM Port mit Cube HAL
Hier zeige ich, wie man mit Cube MX ein Projekt mit virtuellem COM Port (VCP, CDC) erstellt:
- Ein neues Projekt anlegen, dabei den richtigen Mikrocontroller einstellen.
- Im Reiter "Pinout&Configuration"
- Bei "System Core/SYS" soll die "Debug Option: Serial Wire" und "Timebase Source: SysTick" gewählt werden.
- Bei "Connectivity/USB" das "Device (FS)" einschalten.
- Bei "Middleware/USB_DEVICE", wo die Variante "Class for FS IP: Communication Device Class (Virtual Port COM)" gewählt werden muss.
- Suche Dir einen freien I/O Pin für die Status LED aus (bzw. nimm den Pin, der durch dein Board vorgegeben ist).
- Klicke auf den Pin und wähle die Betriebsart "GPIO Output".
- Gib dem Pin den Namen "LED" (rechte Maustaste, Enter User Label).
- Im Reiter "Clock Configuration" kontrollieren, ob HSI48 als Taktquelle für USB eingestellt wurde.
- Gebe dem Projekt im Reiter "Project Manager" einen Namen und stelle als Toolchain deine "STM32CubeIDE" ein.
Um Text vom Mikrocontroller an den PC zu senden, füge im Quelltext von main.c folgendes zwischen die "USER CODE" Markierungen ein:
/* USER CODE BEGIN Includes */
#include "usbd_cdc_if.h"
/* USER CODE END Includes */
...
/* USER CODE BEGIN WHILE */
while (1) {
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
// LED On
HAL_GPIO_WritePin(LED_GPIO_Port,LED_Pin,GPIO_PIN_SET);
HAL_Delay(500);
// LED Off
HAL_GPIO_WritePin(LED_GPIO_Port,LED_Pin,GPIO_PIN_RESET);
HAL_Delay(500);
// Send data
char msg[]="Hallo!\n";
CDC_Transmit_FS( (uint8_t*) msg, strlen(msg));
}
/* USER CODE END 3 */
Die Ausgabe kann man mit einem Terminal-Programm anzeigen, indem man damit den virtuellen COM Port öffnet. Die Baudrate wird igoriert. Unter Linux geht es auch ganz einfach mit dem Befehl `cat /dev/ttyACM0`. Bitte beachte meinen Hinweis zu CDC Geräten unter Linux, er erspart dir womöglich eine langwierige Fehlersuche.
Das Programm belegt etwa 20 KiB Flash und 5 KiB RAM. Davon dienen jeweils 1 KiB als Sendepuffer und Empfangspuffer (kann man ändern).
Virtueller COM Port ohne Cube HAL
Die USB CDC Implementierung in STM32G431CB_usb_test.zip stammt aus dem mikrocontroller.net Forum. Sie wurde ursprünglich vom Benutzer W.S. lizenzfrei veröffentlicht und dann von mehreren Mitgliedern verbessert. Der Quelltext ist sehr kompakt - nur zwei Dateien ohne weitere Abhängigkeiten. Das Programm belegt nur 4 KiB Flash und 600 Bytes RAM. Davon dienen jeweils 256 Byte als Sendepuffer und Empfangspuffer (kann man ändern).
Das Projekt wurde mit der STM32 Cube IDE für das WeAct STM32G431 Core Board erstellt. Ich gehe davon aus, daß der Code unverändert auf allen STM32G4 Modellen läuft. Du brauchst zum compilieren zusätzlich die CMSIS Header.
Das Programm lässt die LED an PC6 jede Sekunde aufblitzen und sendet dabei "Hello World!" über USB an den angeschlossenen Computer:
#include "stm32g4xx.h"
#include "usb.h"
// The current clock frequency
uint32_t SystemCoreClock=16000000;
// Interrupt handler
volatile uint32_t systick_count=0;
void SysTick_Handler(void) {
systick_count++;
}
// Delay some milliseconds
void delay(uint32_t ms) {
uint32_t start = systick_count;
while (systick_count-start<ms);
}
// Start the system timer and switch the HSI48 oscillator on.
// No need to change system clock because the default 16 MHz is fast enough.
void init_clock() {
// Start system timer
SysTick_Config(SystemCoreClock/1000);
// Switch the HSI48 oscillator for USB on
SET_BIT(RCC->CRRCR, RCC_CRRCR_HSI48ON);
// Wait until the HSI48 oscillator is ready
while(!READ_BIT(RCC->CRRCR, RCC_CRRCR_HSI48RDY)) {}
// Select HSI48 as clock source for USB
MODIFY_REG(RCC->CCIPR, RCC_CCIPR_CLK48SEL, 0b00 << RCC_CCIPR_CLK48SEL_Pos);
// Enable CRS with Auto-Trimming on SOF signal from USB host
SET_BIT(RCC->APB1ENR1, RCC_APB1ENR1_CRSEN);
SET_BIT(CRS->CR, CRS_CR_CEN + CRS_CR_AUTOTRIMEN);
}
void init_io() {
// Enable port C
SET_BIT(RCC->AHB2ENR, RCC_AHB2ENR_GPIOCEN);
// PC6 = Output for LED
MODIFY_REG(GPIOC->MODER, GPIO_MODER_MODE6, 0b01 << GPIO_MODER_MODE6_Pos);
// Enable USB with pull-up
SET_BIT(RCC->APB1ENR1, RCC_APB1ENR1_USBEN);
SET_BIT(USB->BCDR, USB_BCDR_DPPU);
}
int main() {
init_clock();
init_io();
UsbSetup();
while (1) {
// LED on
WRITE_REG(GPIOC->BSRR, GPIO_BSRR_BS6);
delay(100);
UsbSendStr("Hello World!\n",10);
// LED off
WRITE_REG(GPIOC->BSRR, GPIO_BSRR_BR6);
delay(900);
}
}
Die Ausgabe kann man mit einem Terminal-Programm anzeigen, indem man damit den virtuellen COM Port öffnet. Die Baudrate wird igoriert. Unter Linux geht es auch ganz einfach mit dem Befehl `cat /dev/ttyACM0`. Bitte beachte meinen Hinweis zu CDC Geräten unter Linux, er erspart dir womöglich eine langwierige Fehlersuche.
Nach der Initialisierung mittels `UsbSetup()` wird die Funktion `UsbSendStr()` benutzt, um Zeichenketten zu senden. UsbSetup setzt voraus, dass der Takt bereits korrekt konfiguriert ist. Die entsprechenden Zeilen habe ich oben fett hervorgehoben. Schaue für weitere Funktionen in die Datei usb.h.
Damit die Ausgabefunktionen der Standard C Library (z.B. `printf, puts, putchar`) auf USB geleitet werden, kannst du folgende Funktion implementieren:
#include <stdio.h>
// Use the USB port for standard output
int _write(int _, char *ptr, int len) {
return UsbSendBytes(ptr, len, 10);
}
Für fortgeschrittene Programmierer hat Niklas Gürtler das USB-Tutorial mit STM32 im mikrocontroller.net Forum geschrieben. Er beschreibt dort detailliert, wie die USB Schnittstelle funktioniert.
Echtzeituhr
Die RTC besteht aus einem 32 kHz Quarz-Oszillator und einer Reihe verketteter Zähler, um auf Sekunden, Minuten, Stunden, Tage, Monate und Jahre zu kommen. Zwei Alarm-Zeiten sind programmierbar und die Uhr kann sich den Zeitstempel von einem Ereignis merken. Neben der Uhr enthält die batteriegepufferte Einheit 5 so genannte "Backup Register", wo man 16 Bit Werte speichern kann.
Nach einem Stromausfall ohne bzw. mit leerer Batterie werden die Zähler automatisch auf 0 gesetzt und die Uhr angehalten. Die STM32G4 enthalten eine Ladeschaltung für die Batterie, deren Ladestrom in zwei Stufen einstellbar ist.
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 init_rtc() {
// Enable the power interface
SET_BIT(RCC->APB1ENR1, RCC_APB1ENR1_PWREN);
// Enable access to the backup domain
SET_BIT(PWR->CR1, PWR_CR1_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, 0b01 << RCC_BDCR_RTCSEL_Pos);
// Enable the RTC
SET_BIT(RCC->BDCR, RCC_BDCR_RTCEN);
}
Aufwachen
Nachdem die RTC gestartet ist, kann man sie benutzen, um regelmäßige Unterbrechungen zu erzeugen. Diese wiederum können verwendet werden, um den Mikrocontroller aus Sleep, Stop und Standby Zuständen auf zu wecken. Im folgenden Anwendungsbeispiel wird die schlafende CPU jede Sekunde durch einen Interrupt aufgeweckt:
#include "stm32g4xx.h"
void init_io() {
// Enable port C
SET_BIT(RCC->AHB2ENR, RCC_AHB2ENR_GPIOCEN);
// PC6 = Output for LED
MODIFY_REG(GPIOC->MODER, GPIO_MODER_MODE6, 0b01 << GPIO_MODER_MODE6_Pos);
}
void init_wakeup() {
// Unlock the write protection
RTC->WPR = 0xCA;
RTC->WPR = 0x53;
// Stop the wakeup timer to allow configuration update
CLEAR_BIT(RTC->CR, RTC_CR_WUTE);
// Wait until the wakeup timer is ready for configuration update
while (!READ_BIT(RTC->ICSR, RTC_ICSR_WUTWF)) {};
// Clock source of the wakeup timer is 1 Hz
MODIFY_REG(RTC->CR, RTC_CR_WUCKSEL, 0b100 << RTC_CR_WUCKSEL_Pos);
// The wakeup period is 0+1 clock pulses
RTC->WUTR = 0;
// Enable the wakeup timer with interrupts
SET_BIT(RTC->CR, RTC_CR_WUTE + RTC_CR_WUTIE);
// Switch the write protection back on
RTC->WPR = 0xFF;
// Enable interrupt
SET_BIT(EXTI->IMR1, EXTI_IMR1_IM20);
SET_BIT(EXTI->RTSR1, EXTI_RTSR1_RT20);
NVIC_EnableIRQ(RTC_WKUP_IRQn);
// Clear (old) pending interrupt flag
SET_BIT(RTC->SCR, RTC_SCR_CWUTF); // Clear in RTC
SET_BIT(EXTI->PR1, EXTI_PR1_PIF20); // Clear in EXTI
}
void RTC_WKUP_IRQHandler() {
// Clear pending interrupt flag in EXTI
// It is important that this is not the last command in the ISR
SET_BIT(EXTI->PR1, EXTI_PR1_PIF20);
// Clear pending interrupt flag in RTC
SET_BIT(RTC->SCR, RTC_SCR_CWUTF);
}
int main() {
// Give the debugger time to connect
delay(1000);
init_io();
init_rtc();
init_wakeup();
while (1) {
// Enter sleep state
__WFI();
// LED on
WRITE_REG(GPIOC->BSRR, GPIO_BSRR_BS6);
delay(50);
// LED off
WRITE_REG(GPIOC->BSRR, GPIO_BSRR_BR6);
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 "stm32g4xx.h"
#include <stdio.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:
- SUB1H Subtrahiere eine Stunde
- ADD1H Addiere eine Stunde (für Sommerzeit)
RTC Beschreiben
Datum, Uhrzeit und einige Bits im Control Register sind ziemlich gut gegen versehentliche Änderungen geschützt. Sie lassen sich nur im sogenannten Initialisierungs-Modus beschreiben, wenn der Schreibgeschutz aufgehoben wurde.
Man darf die reservierten Bits nicht verändern. Außerdem muss man nach jedem Schreibzugriff eine Synchronisation der Schatten-Register auslösen und abwarten. Deswegen ist es gut alle Bits im RTC->TR bzw. RTC->DR Register gleichzeitig zu setzen.
Die folgende Prozedur ändert Datum und Uhrzeit unter Berücksichtigung der obigen Aspekte:
/**
* Write digits to the RTC time register in 24h format.
* @param ht tens of hour
* @param hu ones of hour
* @param 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
SET_BIT(RTC->ICSR, RTC_ICSR_INIT);
// Wait until the initialization mode is active
while (!READ_BIT(RTC->ICSR, RTC_ICSR_INITF)) {};
// The 24h format is already the default
// CLEAR_BIT(RTC->CR, RTC_CR_FMT);
// Update the time register
RTC->TR = tmp;
// Leave the initialization mode
CLEAR_BIT(RTC->ICSR, RTC_ICSR_INIT);
// Trigger a synchronization of the shadow registers
CLEAR_BIT(RTC->ICSR, RTC_ICSR_RSF);
// Wait until the shadow registers are synchronized
while (!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
SET_BIT(RTC->ICSR, RTC_ICSR_INIT);
// Wait until the initialization mode is active
while (!READ_BIT(RTC->ICSR, RTC_ICSR_INITF)) {};
// Update the time register
RTC->DR = tmp;
// Leave the initialization mode
CLEAR_BIT(RTC->ICSR, RTC_ICSR_INIT);
// Trigger a synchronization of the shadow registers
CLEAR_BIT(RTC->ICSR, RTC_ICSR_RSF);
// Wait until the shadow registers are synchronized
while (!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



