STM32F1 Anleitung

Dies ist der F1 spezifische Teil meiner STM32 Anleitungen.

Dies sind die ältesten STM32. Obwohl sie seit 2007 auf dem Markt sind, werden sie immer noch produziert. Allerdings sind die Nachfolger preisgünstiger und besser. Ich würde die STM32F1 darum nur noch als Ersatzteil verwenden.

ST empfiehlt die Pin-kompatiblen STM32F3 als Ersatz. Die Application Note AN4228 beschreibt, was beim Wechsel von F1 zu F3 zu beachten ist.

Modelle

Die STM32F1 Serie hat einen ARM Cortex M3 Kern ohne FPU bis 72 MHz.
STM32F100 Value line
24 MHz mit CEC
STM32F101 Access line
36 MHz (die Basis-Version)
STM32F102 USB Access line
48 MHz mit USB
STM32F103 Performance line
72 MHz mit USB und CAN
STM32F105 Connectivity line
72 MHz mit USB OTG und CAN.
Der F107 hat zusätzlich Ethernet.
STM32F107

Size ↱ x4 x6 x8 xB xC xD xE xF xG x:
C = 48 Pins
T = 36 Pins
R = 64 Pins
V = 100 Pins
Z = 144 Pins
Density Low Medium High XL
Flash 16 KiB 32 KiB 64 KiB 128 KiB 256 KiB 384 KiB 512 KiB 768 KiB 1024 KiB
RAM 4 KiB 4 KiB 8 KiB 8 KiB 24 KiB 32 KiB 32 KiB
STM32F100 Datasheet, Errata Datasheet, Errata Reference manual
RAM 4 KiB 6 KiB 10 KiB 16 KiB 32 KiB 48 KiB 48 KiB 80 KiB 80 KiB Reference manual
STM32F101 Datasheet, Errata Datasheet, Errata Datasheet, Errata Datasheet, Errata
STM32F102 Datasheet, Errata Datasheet, Errata
RAM 6 KiB 10 KiB 20 KiB 20 KiB 48 KiB 64 KiB 64 KiB 96 KiB 96 KiB
STM32F103 Datasheet, Errata Datasheet, Errata Datasheet, Errata Datasheet, Errata
RAM 64 KiB 64 KiB 64 KiB
STM32F105 Datasheet, Errata
STM32F107

Die Pinbelegung und elektrischen Daten findet man 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 STM32F1 Chips kann man mit 2,0 bis 3,6 Volt betreiben. Der ADC benötigt aber mindestens 2,4 Volt und die USB Schnittstelle läuft nur mit 3,3 V.

Die Stromaufnahme ist im laufenden Betrieb mit 8 Bit Mikrocontrollern vergleichbar, im Stop Modus ist sie jedoch wesentlich höher. Für Batteriebetrieb empfehle ich die sparsame L0 Serie oder die leistungsstärkere und dennoch sparsamere G4 Serie.

Viele I/O Pins sind 5 V tolerant, sie sind im Datenblatt mit "FT" gekennzeichnet. Im open-drain Modus dürfen die 5 V toleranten Ausgänge durch externe Widerstände auf 5 V hoch gezogen werden. Analoge Eingänge vertragen maximal die gleiche Spannung wie am VDDA Pin.

Die Ausgänge sind einzeln mit 25 mA belastbar, aber insgesamt muss die Stromaufnahme des Chips unter 150 mA bleiben. Gültige Logikpegel sind nur bis 8 mA garantiert. Die Ausgänge sind nicht Kurzschluss-fest.

Die Eingänge sind sehr hochohmig (da CMOS) und haben einen Schmitt-Trigger. Die ESD Schutzdioden sind teilweise nur sehr bedingt belastbar. Die internen Pull-Up und Pull-Down Widerstände haben ungefähr 40 kΩ. Alle I/O Pins haben ungefähr 5 pF Kapazität.

Der NRST (Reset) Pin hat intern einen Pull-Up Widerstand und kann sowohl vom Chip selber als auch von außen auf Low gezogen werden. Intern erzeugte Reset-Impulse sind garantiert mindestens 20 µs lang.

Ausnahmen:

Für die Pins PC13, PC14 und PC15 gelten folgende Einschränkungen:

Hintergrund ist, dass diese drei Pins intern am (schwachen) Power-Switch der RTC hängen.

Boards

Nucleo-F103RB

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

Der 8 MHz Hauptquarz befindet sich auf dem ST-Link Adapter, er versorgt beide Mikrocontroller. Wenn man den ST-Link abtrennt, muss man den Mikrocontroller mit seinem internen R/C Oszillator betreiben oder einen zusätzlichen Quarz in die verbleibende Platine einlöten.

Der I²C Anschluss an der rechten Arduino Buchsenleiste (beschriftet als SCL/D15 und SDA/D14) erfordert die Verwendung von I2C1 mit "remapped" Pin Konfiguration. Die beiden Stifte Rx/D0 und Tx/D1 am rechten Arduino Connector haben keine Funktion.

Das User Manual enthält die vollständige Beschreibung des Boardes.

Blue Pill Board

Das Blue Pill Board aus China bekommt man ab 1,50 €, allerdings oft mit gefälschten Chips bestückt. Das schwarze Board von RobotDyn wurde nach meinem Kenntnisstand hingegen bisher immer mit originalen Chips bestückt.

Wenn man besonders dünne Stiftleisten verwendet, passt das Board in einen 40-poligen DIP Sockel.

Der Boot1 Jumper (=PB2) kann im Normalbetrieb für eigene Zwecke verwendet werden.

Wenn der Uhrenquarz benutzt wird, soll man die Stifte an PC14 und PC15 entfernen, damit er stabil schwingt.

Weil beim Blue-Pill Board der 5 V Anschluss direkt mit der USB Buchse verbunden ist, soll bei Nutzung von USB nicht gleichzeitig ein 5 V Netzteil angeschlossen werden. Das schwarze Board von RobotDyn hat eine Diode dazwischen, deswegen gilt die Einschränkung dort nicht. Ein 3,3V Netzteil ist bei beiden Boards zulässig.

Schaltplan vom Blue Pill Board, und vom schwarzen Board. Siehe auch mein Buch Einblick in die moderne Elektronik.

Fälschungen

Seit einigen Jahren wird der Markt mit gefälschten STM32F103 Chips geflutet. Am stärksten betroffen sind die Varianten C8T6 und CBT6 mit 48 Pins. In den Fälschungen befindet sich oft ein APM32F103, GD32F103, CKS32F103, HK32F103, CH32F103, CS32F103, BLM32F103 oder MM32F103, meist umgelabelt als STM32. Dazu wurden folgende Mängel gemeldet:

Die genannten Mängel treten nur teilweise auf (nie alle zusammen). Das größte Problem dabei ist, dass man nicht weiß, welche einem verkauft werden.

Beispielprogramm

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

#include <stdint.h>
#include "stm32f1xx.h"

// delay loop for 8 MHz CPU clock with optimizer enabled
void delay(uint32_t msec) {
    for (uint32_t j=0; j < msec * 2000; j++) {
        __NOP();
    }
}

int main() {
    // Enable Port A and C
    SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN);
    SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPCEN);

    // PA5 = Output for first LED
    MODIFY_REG(GPIOA->CRL, GPIO_CRL_CNF5,   0b00 << GPIO_CRL_CNF5_Pos);
    MODIFY_REG(GPIOA->CRL, GPIO_CRL_MODE5,  0b01 << GPIO_CRL_MODE5_Pos);
    
    // PC13 = Output for second LED
    MODIFY_REG(GPIOC->CRH, GPIO_CRH_CNF13,  0b00 << GPIO_CRH_CNF13_Pos);
    MODIFY_REG(GPIOC->CRH, GPIO_CRH_MODE13, 0b01 << GPIO_CRH_MODE13_Pos);

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

        // Reset LED pin to LOW
        GPIOA->BSRR = GPIO_BSRR_BR5;
        GPIOC->BSRR = GPIO_BSRR_BR13;        
        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, kann man die Schnittstelle im AFIO->MAPR Register deaktivieren, nachdem das AFIOEN Bit eingeschaltet wurde.

// Enable alternate functions
SET_BIT(RCC->APB2ENR, RCC_APB2ENR_AFIOEN);
    
// Disable both SWD and JTAG to free PA13, PA14, PA15, PB3 and PB4
MODIFY_REG(AFIO->MAPR, AFIO_MAPR_SWJ_CFG, AFIO_MAPR_SWJ_CFG_DISABLE); 

or:

// Disable JTAG only to free PA15, PB3*, PB4. SWD remains active
MODIFY_REG(AFIO->MAPR, AFIO_MAPR_SWJ_CFG, AFIO_MAPR_SWJ_CFG_JTAGDISABLE); 
*) PB3 kann trotzdem noch mit dem ST-Link Adapter als SWO Ausgang konfiguriert werden.

Boot Loader

Der Bootloader vom STM32F1 nutzt den Anschluss USART1. Die STM32F105 und 107 Modelle unterstützen darüber hinaus auch USB und CAN, wenn ein Quarz mit 8 MHz, 14.7456 MHz oder 25 MHz angeschlossen ist. Der USB Anschluss benötigt an D+ (PA12) einen 1,5 kΩ Pull-Up Widerstand nach 3,3 V.

Zur Konfiguration des Bootloaders dienen die beiden Pins Boot0 und Boot1:

Boot0 Boot1 (=PB2) Starte von
Low egal Flash ab Adresse 0x0800 0000, gemappt auf 0x0000 0000
High Low Bootloader
High High RAM ab Adresse 0x2000 0000, nicht gemappt

Unterbrechungen

Interrupt-Vektoren

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

Address CMSIS Interrupt Nr. C Function Description EXTI Channel
0x0040 0 WWDG_IRQHandler() Window Watchdog
0x0044 1 PVD_IRQHandler() PVD 16
0x0048 2 TAMPER_IRQHandler() Tamper
0x004C 3 RTC_IRQHandler() RTC
0x0050 4 FLASH_IRQHandler() Flash
0x0054 5 RCC_IRQHandler() RCC
0x0058 6 EXTI0_IRQHandler() EXTI line 0 0
0x005C 7 EXTI1_IRQHandler() EXTI line 1 1
0x0060 8 EXTI2_IRQHandler() EXTI line 2 2
0x0064 9 EXTI3_IRQHandler() EXTI line 3 3
0x0068 10 EXTI4_IRQHandler() EXTI line 4 4
0x006C 11 DMA1_Channel1_IRQHandler() DMA1 Channel 1
0x0070 12 DMA1_Channel2_IRQHandler() DMA1 Channel 2
0x0074 13 DMA1_Channel3_IRQHandler() DMA1 Channel 3
0x0078 14 DMA1_Channel4_IRQHandler() DMA1 channel 4
0x007C 15 DMA1_Channel5_IRQHandler() DMA1 Channel 5
0x0080 16 DMA1_Channel6_IRQHandler() DMA1 Channel 6
0x0084 17 DMA1_Channel7_IRQHandler() DMA1 Channel 7
0x0088 18 ADC1_2_IRQHandler() ADC1 and ADC2
0x008C 19 USB_HP_CAN_TX_IRQHandler() USB High Priority or CAN TX
0x0090 20 USB_LP_CAN_RX0_IRQHandler() USB Low Priority or CAN RX
0x0094 21 CAN_RX1_IRQHandler() CAN1 RX1
0x0098 22 CAN_SCE_IRQHandler() CAN1 SCE
0x009C 23 EXTI9_5_IRQHandler() EXTI lines 5-9 5-9
0x00A0 24 TIM1_BRK_IRQHandler() TIM1 Break
0x00A4 25 TIM1_UP_IRQHandler() TIM1 Update
0x00A8 26 TIM1_TRG_COM_IRQHandler() TIM1 Trigger and Commutation
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() I²C1 event
0x00C0 32 I2C1_ER_IRQHandler() I²C1 error
0x00C4 33 I2C2_EV_IRQHandler() I²C2 event
0x00C8 34 I2C2_ER_IRQHandler() I²C2 error
0x00CC 35 SPI1_IRQHandler() SPI1
0x00D0 36 SPI2_IRQHandler() SPI2
0x00D4 37 USART1_IRQHandler() USART1
0x00D8 38 USART2_IRQHandler() USART2
0x00DC 39 USART3_IRQHandler() USART3
0x00E0 40 EXTI15_10_IRQHandler() EXTI lines 10-15 10-15
0x00E4 41 RTCAlarm_IRQHandler() RTC alarm 17
0x00E8 42 USBWakeup_IRQHandler() USB wakeup from suspend 18
0x00EC 43 TIM8_BRK_IRQHandler() Timer 8 Break
0x00F0 44 TIM8_UP_IRQHandler() Timer 8 Update
0x00F4 45 TIM8_TRG_COM_IRQHandler() Timer 8 Trigger and Commutation
0x00F8 46 TIM8_CC_IRQHandler() Timer 8 Capture Compare
0x00FC 47 ADC3_IRQHandler() ADC3
0x0100 48 FSMC_IRQHandler() FSMC
0x0104 49 SDIO_IRQHandler() SDIO
0x0108 50 TIM5_IRQHandler() TIM5
0x010C 51 SPI3_IRQHandler() SPI3
0x0110 52 UART4_IRQHandler() UART4
0x0114 53 UART5_IRQHandler() UART5
0x0118 54 TIM6_IRQHandler() TIM6
0x011C 55 TIM6_IRQHandler() TIM7
0x0120 56 DMA2_Channel1_IRQHandler() DMA2 Channel 1
0x0124 57 DMA2_Channel2_IRQHandler() DMA2 Channel 2
0x0128 58 DMA2_Channel3_IRQHandler() DMA2 Channel 3
0x012C 59 DMA2_Channel4_5_IRQHandler() or DMA2_Channel4_IRQHandler() DMA2 Channel 4 and Channel 5
0x0130 60 DMA2_Channel5_IRQHandler() DMA2 Channel 5
0x0134 61 ETH_IRQHandler() Ethernet
0x0138 62 ETH_WKUP_IRQHandler() Ethernet Wakeup 19
0x013C 63 CAN2_TX_IRQHandler() CAN2 TX
0x0140 64 CAN2_RX0_IRQHandler() CAN2 RX0
0x0144 65 CAN2_RX1_IRQHandler() CAN2 RX1
0x0148 66 CAN2_SCE_IRQHandler() CAN2 SCE
0x014C 67 OTG_FS_IRQHandler() USB On The Go FS

Extended Interrupts

Die externen Interrupt-Signale und auch einige interne durchlaufen einen erweiterten Schaltkreis, der zusätzliche Konfiguration erfordert. Sie sind im Referenzhandbuch Kapitel "External interrupt/event line mapping" dokumentiert.

Die Kanäle EXTI0 bis EXT15 sind für I/O-Pins reserviert. Jeden Kanal kann man in den Registern AFIO->EXTICR[0-3] genau einem Port zuweisen. Wenn man zum Beispiel Kanal 0 dem Port A zuweist (also PA0), kann man auf den anderen Ports das Bit 0 nicht mehr für Interrupts verwenden. Diese Einschränkung gilt für alle 16 Kanäle.

Die anderen Kanäle findest du oben in der Interrupt-Vektoren Tabelle wieder.

Interrupt Flanken

Viele erweiterte Unterbrechungen werden durch Flanken (Signalwechsel) ausgelöst. Man erkennt sie an den Bits im Register EXTI->RTSR oder EXTI->FTSR.

Durch die Verwendung von Flanken ist automatisch sicher gestellt, dass der Handler nicht immer wieder erneut ausgeführt wird, falls ein Interrupt Signal für längere Zeit ansteht.

Flanken gesteuerte Interrupts gehen nicht verloren, wenn das Signal schon wieder verschwindet bevor der Handler aufgerufen wurde. Der Interrupt-Controller merkt sich, dass da mal eine Anforderung vorlag, die noch nicht abgearbeitet wurde.

In diesem Zusammenhang ist das Register EXTI->PR wichtig. Dort merkt sich der Interrupt-Controller den Zustand. Während der Initialisierung muss man das Bit zurück setzen, um sicher zu stellen, dass die erste Flanke zuverlässig erkannt wird. Man schreibt eine 1 in das jeweilige Bit, damit es auf 0 zurück gesetzt wird. Innerhalb der ISR muss man das Flag ebenfalls zurück setzen, am Besten ganz am Anfang. Am Ende der ISR wäre zu spät, da dieses Signal etwas verzögert verarbeitet wird.

Interrupt Masken

Im Register EXTI->IMR werden Unterbrechungen maskiert. Man muss hier eine 1 in das jeweilige Bit schreiben, damit eine Interrupt-Leitung wirksam wird. Standardmäßig sind die meisten Interrupts maskiert, sind also ohne Wirkung.

Das folgende Beispiel löst einen Interrupt aus, wenn das Signal an PC13 von Low nach High wechselt. Beim Nucleo-Board ist PC13 mit dem blauen Taster verbunden.

#include <stdio.h>
#include "stm32f1xx.h"

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

void EXTI15_10_IRQHandler() {
    // Clear pending interrupt flag
    // It is important that this is not the last command in the ISR
    SET_BIT(EXTI->PR, EXTI_PR_PR13);

    // Output a trace message
    ITM_SendString("irq\n");
}

int main() {
    // Enable port C and alternate functions
    SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPCEN);
    SET_BIT(RCC->APB2ENR, RCC_APB2ENR_AFIOEN);

    // Assign EXTI13 to PC13 with rising edge
    MODIFY_REG(AFIO->EXTICR[3], AFIO_EXTICR4_EXTI13, AFIO_EXTICR4_EXTI13_PC);
    SET_BIT(EXTI->IMR, EXTI_IMR_MR13);
    SET_BIT(EXTI->RTSR, EXTI_RTSR_TR13);
    
    // Enable the interrupt handler call
    NVIC_EnableIRQ(EXTI15_10_IRQn);

    // Clear pending interrupt flag
    SET_BIT(EXTI->PR, EXTI_PR_PR13);

    // Endless loop
    while (1) {}
}

Event Masken

Ereignisse verwendet man üblicherweise, um die CPU aus aus einem Wait- oder Sleep- Zustand aufzuwecken. So hält zum Beispiel der Befehl __WFE() (aus dem Powermanagement) die CPU bis zum nächsten Ereignis an.

Standardmäßig sind alle Ereignisse maskiert. Man muss im Register EXTI->EMR das entsprechende Bit auf 1 setzen, um ein EXTI Signal als Ereignis zu behandeln.

Taktgeber

Ich habe ziemlich oft gelesen, dass das komplexe System zur Takterzeugung für Anfänger ein großes Hindernis sei. Das sehe ich anders, denn nach einem Reset wird der Mikrocontroller automatisch mit seinem internen 8 MHz R/C Oszillator getaktet. Damit kann man schon eine Menge sinnvoller Programme schreiben.

Die Taktsignale für den ARM Kern, sowie RAM und Flash sind automatisch aktiviert. Alle anderen Komponenten muss man ggf. selbst einschalten, was ganz einfach durch Setzen von Bits in den Registern RCC->APB1ENR, RCC->APB2ENR und RCC->AHBENR erledigt wird.

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

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

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

Das folgende Bild zeigt die Standardvorgabe vom STM32F100, STM32F101, STM32F102 und STM32F103 nach einem Reset:

 

Bei den Modellen STM32F105 und STM32F107 ist es im unteren Bereich etwas komplexer:

Achtung:

Beispiel für die maximalen möglichen 64 MHz mit dem internen HSI Oszillator (gilt nicht für STM32F105, STM32F107):

// The current clock frequency
uint32_t SystemCoreClock=8000000;

// Change system clock to 64 MHz using internal 8 MHz R/C Oscillator
void init_clock() {
    // Because the debugger switches PLL on, we may need to switch
    // back to the HSI oscillator before we can configure the PLL

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

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

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

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

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

    // Wait until the PLL is fully stopped
    while(READ_BIT(RCC->CR, RCC_CR_PLLRDY)) {}
    
    // Flash latency 2 wait states
    MODIFY_REG(FLASH->ACR, FLASH_ACR_LATENCY, 2 << FLASH_ACR_LATENCY_Pos);

    // 64 MHz using the 8 MHz/2 HSI oscillator with 16x PLL, lowspeed I/O runs at 32 MHz
    RCC->CFGR = RCC_CFGR_PLLMULL16 + RCC_CFGR_PPRE1_DIV2;

    // Enable PLL
    SET_BIT(RCC->CR, RCC_CR_PLLON);

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

    // Select PLL as clock source
    MODIFY_REG(RCC->CFGR, RCC_CFGR_SW, RCC_CFGR_SW_PLL);

    // Update variable
    SystemCoreClock=64000000;
}

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

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

Beispiel für 72 MHz mit einem 8 MHz Quarz (HSE Oszillator):

// The current clock frequency
uint32_t SystemCoreClock=8000000;

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

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

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

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

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

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

    // Wait until the PLL is fully stopped
    while(READ_BIT(RCC->CR, RCC_CR_PLLRDY)) {}
    
    // Flash latency 2 wait states
    MODIFY_REG(FLASH->ACR, FLASH_ACR_LATENCY, 2 << FLASH_ACR_LATENCY_Pos);

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

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

    // 72 MHz using the 8 MHz HSE oscillator with 9x PLL, lowspeed I/O runs at 36 MHz
    RCC->CFGR =  RCC_CFGR_PLLSRC + RCC_CFGR_PLLMULL9 + RCC_CFGR_PPRE1_DIV2;

    // Enable PLL
    SET_BIT(RCC->CR, RCC_CR_PLLON);

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

    // Select PLL as clock source
    MODIFY_REG(RCC->CFGR, RCC_CFGR_SW, RCC_CFGR_SW_PLL);

    // Update variable
    SystemCoreClock=72000000;
    
    // Disable the HSI oscillator
    CLEAR_BIT(RCC->CR, RCC_CR_HSION);
}

Digitale Pins

Generell können alle I/O Pins erst benutzt werden, nachdem man den Port im Register RCC->APB2ENR eingeschaltet hat. Dort muss man auch das AFIOEN Bit einschalten, es sei denn man benutzt gar keine alternative Funktion.

Standardmäßig sind fast alle I/O Pins als digitaler Eingang konfiguriert. Um deren Status abzufragen, liest man das jeweilige GPIOx->IDR Register.

Im Register GPIOx->CRL (für Pin 0-7) oder GPIOx->CRH (für Pin 8-15) konfiguriert man einen Pin als Ausgang oder für alternative Funktionen (z.B. serieller Port oder PWM Timer).

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.

Für jeden Ausgang kann man dort die maximale Frequenz auf 2, 10 oder 50 MHz einstellen. Damit beeinflusst man die Geschwindigkeit, mit der die Spannung von Low nach High und zurück wechselt. Der maximale Laststrom wird dadurch nicht verändert. Im Sinne von elektromagnetischer Verträglichkeit soll man hier immer den niedrigsten Wert einstellen, der zur Anwendung passt.

Schaue Dir die Beschreibung der GPIO Register im Referenzhandbuch an!

Analoge Eingänge

Die Taktfrequenz des Systems wird standardmäßig durch 2 geteilt um den ADC zu betreiben. Er funktioniert mit maximal 14 MHz, daher ist es notwendig den Vorteiler im Register RCC->CFGR zu ändern, wenn der Systemtakt über 28 MHz liegt.

Bevor man einen Pin als analogen Eingang verwendet, muss man ihn im Register GPIOx->CRL (für Pin 0-7) oder GPIOx->CRH (für Pin 8-15) konfigurieren. Zum Beispiel:

// Configure PA1 as analog input ADC12_IN1
MODIFY_REG(GPIOA->CRL, GPIO_CRL_CNF1,  0b00 << GPIO_CRL_CNF1_Pos);
MODIFY_REG(GPIOA->CRL, GPIO_CRL_MODE1, 0b00 << GPIO_CRL_MODE1_Pos);

Initialisierung des ADC für einzelne Lesezugriffe:

// Initialize the ADC for single conversion mode
void init_analog() {
    // Divide APB2 clock frequency by 8
    MODIFY_REG(RCC->CFGR, RCC_CFGR_ADCPRE, RCC_CFGR_ADCPRE_DIV8);
    
    // Enable ADC
    SET_BIT(RCC->APB2ENR, RCC_APB2ENR_ADC1EN);

    // Switch the ADC on
    SET_BIT(ADC1->CR2, ADC_CR2_ADON);

    // Select software start trigger
    MODIFY_REG(ADC1->CR2, ADC_CR2_EXTSEL, 0b111 << ADC_CR2_EXTSEL_Pos);

    // Set sample time to 41.5 cycles
    MODIFY_REG(ADC1->SMPR2, ADC_SMPR2_SMP0, 0b100 << ADC_SMPR2_SMP0_Pos);

    // Delay 20 ms
    delay(20);

    // Start calibration
    SET_BIT(ADC1->CR2, ADC_CR2_ADON + ADC_CR2_CAL);

    // Wait until the calibration is finished
    while (READ_BIT(ADC1->CR2, ADC_CR2_CAL));
}

Lesen eines analogen Eingangs:

// Read from an analog input
uint16_t read_analog(int channel) {
    // Number of channels to convert: 1
    MODIFY_REG(ADC1->SQR1, ADC_SQR1_L, 0 << ADC_SQR1_L_Pos);  // does one conversion more than configured here
    
    // Select the channel
    MODIFY_REG(ADC1->SQR3, ADC_SQR3_SQ1, channel);

    // Clear the finish flag
    CLEAR_BIT(ADC1->SR, ADC_SR_EOC);

    // Start a conversion
    // These two bits must be set by individual commands!
    SET_BIT(ADC1->CR2, ADC_CR2_ADON);
    SET_BIT(ADC1->CR2, ADC_CR2_SWSTART);

    // Wait until the conversion is finished
    while (!READ_BIT(ADC1->SR, ADC_SR_EOC));

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

Der ADC kann so konfiguriert werden, dass er mehrere Eingänge kontinuierlich liest. Mittels DMA können die Messergebnisse automatisch ins RAM übertragen werden. Dafür habe ich hier kein Beispiel parat.

PWM Ausgänge

Die Timer 1 bis 8 können jeweils 4 PWM Signale mit maximal 16 Bit Auflösung (65535 Stufen) erzeugen. Damit kann man z.B. die Helligkeit von Lampen oder die Drehzahl von Motoren steuern.

Die Timer 9 bis 14 haben jeweils nur 2 PWM Kanäle.

Die Taktfrequenz der Timer wird vom Systemtakt abgeleitet und kann durch den ABP2 Prescaler (im Register RCC->CFGR) und den Timer Prescaler TIMx->PSC reduziert werden.

Der Timer zählt fortlaufend von 0 an hoch bis zum Maximum, welches durch das TIMx->ARR Register festgelegt wird. Wenn der Maximalwert als 50000 festgelegt wird, können die Ausgangsimpulse wahlweise 1 bis 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 die Ausgänge von Timer 3 (PA6, PA7, PB0 und PB1), um dort vier angeschlossene Leuchtdioden unterschiedlich hell leuchten zu lassen:

#include "stm32f1xx.h"

void init_io() {
    // Enable Port A, B and alternate functions
    SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN);
    SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPBEN);
    SET_BIT(RCC->APB2ENR, RCC_APB2ENR_AFIOEN);

    // PA6 = Timer 3 channel 1 alternate function output
    MODIFY_REG(GPIOA->CRL, GPIO_CRL_CNF6,  0b10 << GPIO_CRL_CNF6_Pos);
    MODIFY_REG(GPIOA->CRL, GPIO_CRL_MODE6, 0b01 << GPIO_CRL_MODE6_Pos);

    // PA7 = Timer 3 channel 2 alternate function output
    MODIFY_REG(GPIOA->CRL, GPIO_CRL_CNF7,  0b10 << GPIO_CRL_CNF7_Pos);
    MODIFY_REG(GPIOA->CRL, GPIO_CRL_MODE7, 0b01 << GPIO_CRL_MODE7_Pos);

    // PB0 = Timer 3 channel 3 alternate function output
    MODIFY_REG(GPIOB->CRL, GPIO_CRL_CNF0,  0b10 << GPIO_CRL_CNF0_Pos);
    MODIFY_REG(GPIOB->CRL, GPIO_CRL_MODE0, 0b01 << GPIO_CRL_MODE0_Pos);

    // PB1 = Timer 3 channel 4 alternate function output
    MODIFY_REG(GPIOB->CRL, GPIO_CRL_CNF1,  0b10 << GPIO_CRL_CNF1_Pos);
    MODIFY_REG(GPIOB->CRL, GPIO_CRL_MODE1, 0b01 << GPIO_CRL_MODE1_Pos);
}

void init_timer3_for_pwm() {
    // Enable timer 3
    SET_BIT(RCC->APB1ENR, RCC_APB1ENR_TIM3EN);

    // 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 3 channel 2 compare mode=PWM1 with the required preload buffer enabled
    MODIFY_REG(TIM3->CCMR1, TIM_CCMR1_OC2M, 0b110 << TIM_CCMR1_OC2M_2_Pos);
    SET_BIT(TIM3->CCMR1, TIM_CCMR1_OC2PE);

    // Timer 3 channel 3 compare mode = PWM1 with the required preload buffer enabled
    MODIFY_REG(TIM3->CCMR2, TIM_CCMR2_OC3M, 0b110 << TTIM_CCMR2_OC3M_Pos);
    SET_BIT(TIM3->CCMR2, TIM_CCMR1_OC1PE);

    // Timer 3 channel 4 compare mode = PWM1 with the required preload buffer enabled
    MODIFY_REG(TIM3->CCMR2, TIM_CCMR2_OC4M, 0b110 << TTIM_CCMR2_OC4M_2_Pos);
    SET_BIT(TIM3->CCMR2, TIM_CCMR1_OC1PE);

    // Timer 3 enable all four compare outputs
    SET_BIT(TIM3->CCER, TIM_CCER_CC1E + TIM_CCER_CC2E + TIM_CCER_CC3E + TIM_CCER_CC4E);

    // Timer 3 inverse polarity for all four compare outputs
    // SET_BIT(TIM3->CCER, TIM_CCER_CC1P + TIM_CCER_CC2P + TIM_CCER_CC3P + TIM_CCER_CC4P);
   
    // Timer 3 auto reload register, defines the maximum value of the counter
    TIM3->ARR = 50000; // 8000000/50000 = 160 pulses per second
    
    // Timer 3 clock prescaler, the APB2 clock is divided by this value +1.
    TIM3->PSC = 0; // divide clock by 1 

    // Timer 3 enable counter and auto-preload
    SET_BIT(TIM3->CR1, TIM_CR1_CEN + TIM_CR1_ARPE);
}

int main() {
    init_io();
    init_timer3_for_pwm();

    // set PWM pulse width of all four outputs
    TIM3->CCR1 = 40;
    TIM3->CCR2 = 400;
    TIM3->CCR3 = 4000;
    TIM3->CCR4 = 40000; 
}

Die Timer 1 und 8 können komplementäre Ausgangssignale mit Tot-Zeit erzeugen, was für den Eigenbau von H-Brücken nützlich ist. Allerdings kollidieren die Ausgänge vom Timer 1 teilweise mit dem USB Port und der Timer 8 existiert nur bei den größeren High und XL Density Modellen.

Ich möchte darauf hinweisen, dass die Timer noch viele weitere Funktionen haben, die ich hier gar nicht alle zeigen kann. Mehr Informationen dazu gibt es zum Beispiel in der Application Note AN4776.

USART Schnittstelle

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

Je nach Taktfrequenz der Peripherie sind unterschiedliche Baudraten möglich. Beispiele:

Beim Nucleo-64 Board ist die serielle Schnittstelle USART2 mit dem ST-Link Adapter verbunden, der diese wiederum über USB an einen virtuellen COM Port weiter leitet:

ST-Link CN3 STM32F1 USART2 Beschreibung
TxD RxD (=PA3) Daten
RxD TxD (=PA2) Daten
GND GND Gemeinsame Masse

Der ST-Link v2.1 unterstützt 600 bis 2000000 Baud.

Das folgende Beispielprogramm gibt "Hello World!" auf USART2 aus und schickt danach alle empfangenen Zeichen als echo an den PC zurück. Das Senden findet hier direkt statt (ggf. mit Warteschleife) während der Empfang interrupt-gesteuert stattfindet:

#include <stdio.h>
#include "stm32f1xx.h"

uint32_t SystemCoreClock=8000000;

// Use serial port for standard output
int _write(int file, char *ptr, int len) {
    for (int i=0; i<len; i++) {

        // wait until TX buffer is empty
        while(!(USART2->SR & USART_SR_TXE));

        // write one character to the transmit data register
        USART2->DR = *ptr++;
    }
    return len;
}

// called after each received character
void USART2_IRQHandler() {
    char received=USART2->DR;

    // send echo back
    while(!(USART2->SR & USART_SR_TXE));
    USART2->DR = received;
}

int main() {
    // Enable port A, alternate functions and USART2
    SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN);
    SET_BIT(RCC->APB2ENR, RCC_APB2ENR_AFIOEN);
    SET_BIT(RCC->APB1ENR, RCC_APB1ENR_USART2EN);

    // PA2 (TxD) shall use the alternate function with push-pull
    MODIFY_REG(GPIOA->CRL, GPIO_CRL_CNF2,  0b10 << GPIO_CRL_CNF2_Pos);
    MODIFY_REG(GPIOA->CRL, GPIO_CRL_MODE2, 0b10 << GPIO_CRL_MODE2_Pos);

    // Enable transmitter, receiver and receive-interrupt of USART2
    USART2->CR1 = USART_CR1_UE + USART_CR1_TE + USART_CR1_RE + USART_CR1_RXNEIE;

    // Set baudrate, assuming that USART2 is clocked with 
    // the same frequency as the CPU core (no prescalers).
    USART2->BRR = (SystemCoreClock / 9600);
    
    // With > 36 MHz system clock, the USART2 receives usually half of it:
    // USART2->BRR = (SystemCoreClock / 2 / 9600);

    // Enable interrupt in NVIC
    NVIC_EnableIRQ(USART2_IRQn);

    printf("Hello World!\n");
    while (1) {};
}

I²C Bus

Der I²C Bus ist eine beliebte Schnittstelle für die Anbindung von Peripherie über kurze Leitungen, wie Port-Erweiterungen, Sensoren und Batterie-Management. Siehe Spezifikation von Philips/NXP.

Der Bus besteht aus den beiden Leitungen SDA und SCL. An beide Leitungen gehört jeweils ein Pull-Up Widerstand, typischerweise mit 4,7 kΩ bei 5 V oder 2,7 kΩ bei 3,3 V. Die STM32F1 Mikrocontroller haben zwei I²C Busse, beide unterstützen 3,3 V und 5 V Pegel, aber nur wenige Slaves sind so flexibel. Die Signale durchlaufen interne Filter zur Entstörung.

Signal I2C1 normal I2C1 remapped I2C2
SCL Takt PB6 PB8 PB10
SDA Daten PB7 PB9 PB11

Zum Remappen kann man das Bit I2C1_REMAP im Register AFIO->MAPR setzen. Nach der Initialisierung der I²C Schnittstelle (nicht vorher!) muss man die betroffen I/O Pins im Register GPIOB->CRL bzw. GPIOB->CRH als "Alternate function output open-drain 2 MHz" konfigurieren.

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 <stdbool.h>
#include "stm32f1xx.h"

/**
 * Initialize the I²C interface.
 *
 * @param registerStruct May be either I2C1 (SCL=PB6, SDA=PB7) or I2C2 (SCL=PB10, SDA=PB11)
 * @param remap Whether to remap I2C1 to the alternative pins (SCL=PB8, SDA=PB9).
 * @param fastMode false=100 kHz, true=400 kHz
 * @param apb1_clock clock frequency of APB1 peripherals
 */
void i2c_init(I2C_TypeDef* registerStruct, bool remap, bool fastMode, uint32_t apb1_clock) {
    
    // Enable port B and alternate functions
    SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPBEN);
    SET_BIT(RCC->APB2ENR, RCC_APB2ENR_AFIOEN);
    
    // Enable the I²C interface
    if (registerStruct==I2C1) {
        SET_BIT(RCC->APB1ENR, RCC_APB1ENR_I2C1EN);
    }
    else if (registerStruct==I2C2) {
        SET_BIT(RCC->APB1ENR, RCC_APB1ENR_I2C2EN);
    }
    
    // Disable the peripheral
    CLEAR_BIT(registerStruct->CR1, I2C_CR1_PE);

    // Configure timing
    MODIFY_REG(registerStruct->CR2, I2C_CR2_FREQ, apb1_clock/1000000);
    if (fastMode) {
        MODIFY_REG(registerStruct->CCR, I2C_CCR_CCR, apb1_clock/800000);
        MODIFY_REG(registerStruct->TRISE, I2C_TRISE_TRISE, apb1_clock/4000000+1);
    }
    else {
        MODIFY_REG(registerStruct->CCR, I2C_CCR_CCR, apb1_clock/200000);
        MODIFY_REG(registerStruct->TRISE, I2C_TRISE_TRISE, apb1_clock/1000000+1);
    }

    // Enable the peripheral
    SET_BIT(registerStruct->CR1, I2C_CR1_PE);

    // Configure the I/O pins for alternate function open-drain 2 MHz
    if (registerStruct==I2C1) {
        if (remap) {
            // PB8=SCL, PB9=SDA
            SET_BIT(AFIO->MAPR, AFIO_MAPR_I2C1_REMAP);
            MODIFY_REG(GPIOB->CRH, GPIO_CRH_CNF8,  0b11 << GPIO_CRH_CNF8_Pos);
            MODIFY_REG(GPIOB->CRH, GPIO_CRH_MODE8, 0b10 << GPIO_CRH_MODE8_Pos);            
            MODIFY_REG(GPIOB->CRH, GPIO_CRH_CNF9,  0b11 << GPIO_CRH_CNF9_Pos);
            MODIFY_REG(GPIOB->CRH, GPIO_CRH_MODE9, 0b10 << GPIO_CRH_MODE9_Pos);
        }
        else {
            // PB6=SCL, PB7=SDA
            CLEAR_BIT(AFIO->MAPR, AFIO_MAPR_I2C1_REMAP);
            MODIFY_REG(GPIOB->CRL, GPIO_CRL_CNF6,  0b11 << GPIO_CRL_CNF6_Pos);
            MODIFY_REG(GPIOB->CRL, GPIO_CRL_MODE6, 0b10 << GPIO_CRL_MODE6_Pos);
            MODIFY_REG(GPIOB->CRL, GPIO_CRL_CNF7,  0b11 << GPIO_CRL_CNF7_Pos);
            MODIFY_REG(GPIOB->CRL, GPIO_CRL_MODE7, 0b10 << GPIO_CRL_MODE7_Pos);
        }
    }
    else if (registerStruct==I2C2) {
        // PB10=SCL, PB11=SDA
        MODIFY_REG(GPIOB->CRH, GPIO_CRH_CNF10,  0b11 << GPIO_CRH_CNF10_Pos);
        MODIFY_REG(GPIOB->CRH, GPIO_CRH_MODE10, 0b10 << GPIO_CRH_MODE10_Pos);
        MODIFY_REG(GPIOB->CRH, GPIO_CRH_CNF11,  0b11 << GPIO_CRH_CNF11_Pos);
        MODIFY_REG(GPIOB->CRH, GPIO_CRH_MODE11, 0b10 << GPIO_CRH_MODE11_Pos);
    }
}


/**
 * 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 or I2C2
 * @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) {

    // Quick return if nothing to do
    if (send_size==0 && receive_size==0) {
        return 0;
    }
    
    int receive_count=-1;

    // shift the 7 Bit address to the right position
    slave_addr=slave_addr << 1;

    // Send data
    if (send_size>0) {
        // Send START and slave address
        SET_BIT(registerStruct->CR1, I2C_CR1_START);              // send START condition
        while (!READ_BIT(registerStruct->SR1, I2C_SR1_SB));       // wait until START has been generated
        registerStruct->DR = slave_addr;                          // send slave address
        while (!READ_BIT(registerStruct->SR1, I2C_SR1_ADDR)) {    // wait until address has been sent
            if (READ_BIT(registerStruct->SR1, I2C_SR1_AF)) {
                // did not receive ACK after address
                goto error;
            }
        }

        registerStruct->SR2;                                      // clear ADDR
        while (send_size>0) {
            registerStruct->DR = *((uint8_t*)send_buffer);        // send 1 byte from buffer
            while (!READ_BIT(registerStruct->SR1, I2C_SR1_TXE)) { // wait until Tx register is empty 
                if (READ_BIT(registerStruct->SR1, I2C_SR1_AF)) {
                    // did not receive ACK after data byte
                    goto error;
                }
            }
            send_buffer++;
            send_size--;
        }
        while (!READ_BIT(registerStruct->SR1, I2C_SR1_BTF)) {     // wait until last byte transfer has finished 
            if (READ_BIT(registerStruct->SR1, I2C_SR1_AF)) {      // did not receive ACK after data byte
                goto error;
            }
        }
    }
    
    // Sending succeeded, start counting the received bytes
    receive_count=0;

    CLEAR_BIT(registerStruct->CR1, I2C_CR1_POS);                  // POS=0
    SET_BIT(registerStruct->CR1, I2C_CR1_ACK);                    // acknowledge each byte

    // Receive data
    // The procedure includes workaround as described in AN2824
    if (receive_size>0) {
        // Send (RE-)START and slave address
        SET_BIT(registerStruct->CR1, I2C_CR1_START);              // send START condition
        while (!READ_BIT(registerStruct->SR1, I2C_SR1_SB));       // wait until START has been generated
        registerStruct->DR = slave_addr+1;                        // send slave address + read mode
        while (!READ_BIT(registerStruct->SR1, I2C_SR1_ADDR)) {    // wait until address has been sent 
            if (READ_BIT(registerStruct->SR1, I2C_SR1_AF)) {
                // did not receive ACK after address
                goto error;
            }
        }

        if (receive_size>2) {
            registerStruct->SR2;                                       // clear ADDR
            while (receive_size>3) {
                while (!READ_BIT(registerStruct->SR1, I2C_SR1_RXNE));  // wait until a data byte has been received
                *((uint8_t*)receive_buffer) = registerStruct->DR;      // read data
                receive_size--;
                receive_count++;
                receive_buffer++;
            }
            while (!READ_BIT(registerStruct->SR1, I2C_SR1_BTF));  // wait until 2 bytes are received
            CLEAR_BIT(registerStruct->CR1, I2C_CR1_ACK);          // prepare to send a NACK
            *((uint8_t*)receive_buffer) = registerStruct->DR;     // read the penultimate data byte
            receive_size--;
            receive_count++;
            receive_buffer++;
            __disable_irq(); {
                SET_BIT(registerStruct->CR1, I2C_CR1_STOP);       // prepare to send a STOP condition
                *((uint8_t*)receive_buffer) = registerStruct->DR; // read the last data byte
                receive_size--;
                receive_count++;
                receive_buffer++;
            }
            __enable_irq();
        }
        else if (receive_size==2) {
            SET_BIT(registerStruct->CR1, I2C_CR1_POS);            // NACK shall be applied to the next 
                                                                  // byte, not the current byte
            __disable_irq();
            {
                registerStruct->SR2;                              // clear ADDR
                CLEAR_BIT(registerStruct->CR1, I2C_CR1_ACK);      // prepare to send a NACK
            }
            __enable_irq();
            while (!READ_BIT(registerStruct->SR1, I2C_SR1_BTF));  // wait until 2 bytes are received
            __disable_irq();
            {
                SET_BIT(registerStruct->CR1, I2C_CR1_STOP);       // prepare to send a STOP condition
                *((uint8_t*)receive_buffer) = registerStruct->DR; // read the penultimate data byte
                receive_size--;
                receive_count++;
                receive_buffer++;
             }
            __enable_irq();
        }
        else if (receive_size==1) {
            CLEAR_BIT(registerStruct->CR1, I2C_CR1_ACK);          // prepare to send a NACK
            __disable_irq();
            {
                registerStruct->SR2;                              // clear ADDR
                SET_BIT(registerStruct->CR1, I2C_CR1_STOP);       // prepare to send a STOP condition
            }
            __enable_irq();
            while (!READ_BIT(registerStruct->SR1, I2C_SR1_RXNE)); // wait until a data byte has been received
        }

        *((uint8_t*)receive_buffer) = registerStruct->DR;         // read the last data byte
        receive_size--;
        receive_count++;
        receive_buffer++;
        while (READ_BIT(registerStruct->SR1, I2C_CR1_STOP));      // wait until STOP has been generated
    }

    else if (receive_size==0) {
        SET_BIT(registerStruct->CR1, I2C_CR1_STOP);               // send STOP condition
        while (READ_BIT(registerStruct->CR1, I2C_CR1_STOP));      // wait until STOP has been generated
    }

    return receive_count;

    error:
    SET_BIT(registerStruct->CR1, I2C_CR1_STOP);                   // send STOP condition
    while (READ_BIT(registerStruct->CR1, I2C_CR1_STOP));          // wait until STOP has been generated
    CLEAR_BIT(registerStruct->CR1, I2C_CR1_PE);                   // restart the I²C interface clear all error flags
    SET_BIT(registerStruct->CR1, I2C_CR1_PE);
    //ITM_SendString("I2C bus error!\n");
    return receive_count;
}

Die Funktion liefert nach der Übertragung die Anzahl der empfangenen Bytes zurück, oder -1 wenn das Senden fehlschlug. Der Code muss so komplex sein, um Bugs in der I²C Schnittstelle des STM32F1 zu umgehen.

Anwendungsbeispiel:

int main() {
    // Initialize I2C1, no pin remapping, no fast mode, APB1 clock is 8 MHz
    i2c_init(I2C1, false, false, 8000000);

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

Das obige Beispiel sendet ein Byte {0} an den Slave mit der Adresse 8. Danach werden 5 Bytes vom Slave empfangen, falls er antwortet.

USB Schnittstelle

Die USB Schnittstelle erfordert ein umfangreiches Softwarepaket. Beinahe alle Programmierer binden daher fertige Implementierungen in ihr Programm ein.

Die USB Schnittstelle funktioniert Interrupt-getrieben. Immer wenn die Hardware ein kleines Datenpaket gesendet oder empfangen hat, löst sie einen Interrupt aus. Der Interrupthandler hat die Aufgabe, Anfragen des Host zu beantworten und Nutzdaten mit dem Pufferspeicher auszutauschen. Wenn man den Mikrocontroller beim Debuggen anhält, fällt die USB Schnittstelle sofort aus.

USB und CAN schließen sich gegenseitig aus, man kann nur eine davon gleichzeitig nutzen.

Der Systemtakt muss entweder 48 MHz oder 72 MHz betragen und aus einem Quarz gewonnen werden. Der USB Clock Prescaler wird dementsprechend auf 1 oder 1,5 gestellt, um die USB Schnittstelle mit 48 MHz zu takten.

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.

An D+ gehört ein 1,5 kΩ Pull-Up Widerstand auf 3,3 V, welcher dem Host Computer signalisiert, dass ein Gerät angeschlossen wurde. Manche Boards schalten den Widerstand mit einen I/O Pin ein. Dadurch kann man den Host Computer dazu bringen, das USB Gerät erneut zu erkennen, ohne das Kabel abstecken zu müssen.

Bitte beachte meinen Hinweis zu CDC Geräten unter Linux, er erspart dir womöglich eine langwierige Fehlersuche.

Virtueller COM Port mit Cube HAL

Mit Cube MX kann man sich ein Projekt mit USB Unterstützung zusammen klicken. Das geht so:

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

Diese Variante belegt etwa 20 KiB Flash und 5 KiB RAM. Davon dienen jeweils 1 KiB als Sendepuffer und Empfangspuffer (kann man ändern). Die Ausgabe kann man mit einem Terminal-Programm anzeigen, indem man damit den virtuellen COM Port (VCP) öffnet. Die Baudrate wird igoriert. Unter Linux geht es auch ganz simpel mit dem Befehl cat /dev/ttyACM0.

Damit die Ausgabefunktionen der Standard C Library (z.B. printf, puts, putchar) auf USB geleitet werden, kannst du folgende Funktion implementieren:

#include <stdio.h>

// Use the USB port for standard output
int _write(int file, char *ptr, int len) {
    CDC_Transmit_FS( (uint8_t*) ptr, len);
    return len;
}

Virtueller COM Port ohne Cube HAL

Die USB CDC Implementierung in STM32F103_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 Blue Pill Board (STM32F103C8) erstellt, und unverändert auch auf STM32F103CB und STM32F103RB getestet. Ich gehe davon aus, dass der Code nach kleinen Anpassungen auch für STM32F102 geeignet ist.

Das Programm lässt die LED an PC13 jede Sekunde aufblitzen und sendet dabei "Hello World!" über USB an den angeschlossenen Computer:

#include <stdio.h>
#include "stm32f1xx.h"
#include "usb.h"

// The current clock frequency
uint32_t SystemCoreClock=8000000;

// Counts milliseconds
volatile uint32_t systick_count=0;

// Interrupt handler
void SysTick_Handler() {
    systick_count++;
}

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

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

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

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

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

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

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

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

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

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

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

    // 48Mhz using the 8Mhz HSE oscillator with 6x PLL, lowspeed I/O runs at 24Mhz
    RCC->CFGR =  RCC_CFGR_PLLSRC + RCC_CFGR_PLLMULL6 + RCC_CFGR_PPRE1_DIV2;

    // Enable PLL
    SET_BIT(RCC->CR, RCC_CR_PLLON);

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

    // Select PLL as clock source
    MODIFY_REG(RCC->CFGR, RCC_CFGR_SW, RCC_CFGR_SW_PLL);
    
    // Update variable
    SystemCoreClock=48000000;
    
    // Disable HSI oscillator
    SET_BIT(RCC->CR, RCC_CR_HSION);
   
    // Set USB prescaler to 1 for 48 MHz clock
    SET_BIT(RCC->CFGR, RCC_CFGR_USBPRE);
}

void init_io() {
    // Enable USB
    SET_BIT(RCC->APB1ENR, RCC_APB1ENR_USBEN);

    // Enable Port A and C
    SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN);
    SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPCEN);

    // PA5 = Output (LED on Nucleo-64 board)
    MODIFY_REG(GPIOA->CRL, GPIO_CRL_CNF5,  0b00 << GPIO_CRL_CNF5_Pos);
    MODIFY_REG(GPIOA->CRL, GPIO_CRL_MODE5, 0b01 << GPIO_CRL_MODE5_Pos);    

    // PC13 = Output (LED on Blue-Pill board)
    MODIFY_REG(GPIOC->CRH, GPIO_CRH_CNF13,  0b00 << GPIO_CRH_CNF13_Pos);
    MODIFY_REG(GPIOC->CRH, GPIO_CRH_MODE13, 0b01 << GPIO_CRH_MODE13_Pos);
}

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

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

    while (1) {
        // LED On
        GPIOA->BSRR = GPIO_BSRR_BS5;
        GPIOC->BSRR = GPIO_BSRR_BR13;
        delay(100);

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

        // LED Off
        GPIOA->BSRR = GPIO_BSRR_BR5;
        GPIOC->BSRR = GPIO_BSRR_BS13;
        delay(900);
    }

}

Die Ausgabe kann man mit einem Terminal-Programm anzeigen, indem man damit den virtuellen COM Port (VCP) öffnet. Die Baudrate wird igoriert. Unter Linux geht es auch ganz simpel mit dem Befehl cat /dev/ttyACM0.

Nach der Initialisierung mittels UsbSetup() wird die Funktion UsbSendStr() benutzt, um Zeichenketten zu senden. UsbSetup setzt voraus, dass der Takt bereits korrekt konfiguriert ist. Die entsprechenden Zeilen habe ich oben fett hervorgehoben. Schaue für weitere Funktionen in die Datei usb.h.

Damit die Ausgabefunktionen der Standard C Library (z.B. printf, puts, putchar) auf USB geleitet werden, kannst du folgende Funktion implementieren:

#include <stdio.h>

// Use the USB port for standard output
int _write(int file, char *ptr, int len) {
    return UsbSendBytes(ptr, len, 10);
}

Für fortgeschrittene Programmierer hat Niklas Gürtler das USB-Tutorial mit STM32 im mikrocontroller.net Forum geschrieben. Er beschreibt dort detailliert, wie die USB Schnittstelle des STM32F103 funktioniert und liefert als Anwendungsbeispiel einen nützlichen 3-Fach USB-UART Adapter. Dieser funktioniert dank CDC Standard ohne Treiberinstallation. Ich habe das Projekt für die Cube IDE und Blue Pill Boards angepasst: Download.

Echtzeituhr

Die RTC kann dazu verwendet werden, um eine Uhr zu bauen. Technisch gesehen handelt es sich nur um einen simplen batteriebetriebenen Zähler mit 32 kHz Quarz-Oszillator, der üblicherweise genauer läuft, als der Systemtakt. Ein einstellbarer Vorteiler erzeugt den Sekunden-Takt, dahinter kommt ein 32 Bit Zähler. Uhrzeit und Datum muss man ggf. per Software anhand des Zählerstandes berechnen.

Nach einem Stromausfall ohne bzw. mit leerer Batterie werden die Zähler automatisch auf 0 gesetzt und die Uhr angehalten.

Neben der Uhr enthält die batteriegepufferte Einheit je nach Modell 10 oder 42 so genannte "Backup Register", wo man 16 Bit Werte speichern kann.

Der LSE Oszillator nutzt nur sehr wenig Energie und ist daher bei falscher Beschaltung störanfällig. Die Leitungen zum sorgfältig ausgewählten Quarz sollen so kurz wie möglich sein und die Kapazitäten müssen genau berechnet werden.

Der Anschluss PC13 wird ebenfalls durch den Low-Power Schaltkreis der RTC versorgt, also auch durch die Backup Batterie. Er soll daher nur mit niedriger Frequenz geschaltet werden und bei High Pegel nicht belastet werden.

RTC Initialisieren

Dieses Beispiel initialisiert die RTC so, dass jede Sekunde ein Interrupt aufgerufen wird. In der ISR wird die LED an Port PA5 getoggelt, so dass sie blinkt.

#include "stm32f1xx.h"

void init_io() {
    // Enable Port A
    SET_BIT(RCC->APB2ENR, RCC_APB2ENR_IOPAEN);

    // PA5 = Output
    MODIFY_REG(GPIOA->CRL, GPIO_CRL_CNF5,  0b00 << GPIO_CRL_CNF5_Pos);
    MODIFY_REG(GPIOA->CRL, GPIO_CRL_MODE5, 0b01 << GPIO_CRL_MODE5_Pos);
}

void initRtc() {
    // Enable the backup domain
    SET_BIT(RCC->APB1ENR, RCC_APB1ENR_BKPEN);
    SET_BIT(RCC->APB1ENR, RCC_APB1ENR_PWREN);

    // Enable write access to the backup domain
    SET_BIT(PWR->CR, PWR_CR_DBP);

    // Enable LSE oscillator
    SET_BIT(RCC->BDCR, RCC_BDCR_LSEON);

    // Wait until LSE oscillator is ready
    while(!READ_BIT(RCC->BDCR, RCC_BDCR_LSERDY)) {}

    // Select LSE as clock source for the RTC
    MODIFY_REG(RCC->BDCR, RCC_BDCR_RTCSEL, RCC_BDCR_RTCSEL_LSE);

    // Enable the RTC
    SET_BIT(RCC->BDCR, RCC_BDCR_RTCEN);

    // Wait until RTC is synchronized
    while(!READ_BIT(RTC->CRL, RTC_CRL_RSF)) {}

    // Wait until last write operation is done
    while(!READ_BIT(RTC->CRL, RTC_CRL_RTOFF)) {}

    // Enable second interrupt
    SET_BIT(RTC->CRH,RTC_CRH_SECIE);

    // Wait until last write operation is done
    while(!READ_BIT(RTC->CRL, RTC_CRL_RTOFF)) {}

    // Enter configuration mode
    SET_BIT(RTC->CRL,RTC_CRL_CNF);

    // Divide oscillator frequency by 32767+1 to get seconds
    RTC->PRLL=32767;
    RTC->PRLH=0;

    // Leave configuration mode
    CLEAR_BIT(RTC->CRL,RTC_CRL_CNF);

    // Wait until last write operation is done
    while(!READ_BIT(RTC->CRL, RTC_CRL_RTOFF)) {}

    // Enable interrupt in NVIC
    NVIC_EnableIRQ(RTC_IRQn);
}

void RTC_IRQHandler() {
    // Clear interrupt flag
    CLEAR_BIT(RTC->CRL,RTC_CRL_SECF);

    // Note: After clearing the interrupt flag, give the RTC at
    // least 5 clock cycles time before leaving the ISR, otherwise 
    // the ISR gets executed twice.

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

int main() {
    init_io();
    initRtc();
    while(1){};
}

In der Interrupt-Vektor Tabelle in startup/startup_stm32.s muss ein Eintrag für den "RTC_IRQHandler" hinzugefügt werden, falls nicht vorhanden.

RTC lesen

Das Auslesen der Uhrzeit (also des Sekundenzählers) erfordert zwei Lesezugriffe zu je 16 bit. Es kann passieren, dass der Zähler zwischen den beiden Lesezugriffen verändert wird, was zu völlig falschen Ergebnissen führt. Um diesen Fehler sicher zu umgehen, liest man den Sekundenzähler wiederholt aus, bis man zweimal hintereinander den selben Wert erhält.

uint32_t read_rtc() {
    // Wait until RTC is synchronized
    while(!READ_BIT(RTC->CRL, RTC_CRL_RSF)) {}

    // Repeat until got 2x the same value.
    uint32_t old=0;
    uint32_t new=0;
    do {
        old=new;
        new = (((uint32_t) RTC->CNTH) << 16) | ((uint32_t)RTC->CNTL);        
    }
    while (old != new);
    return new;
}

RTC beschreiben

Beim Schreiben verlangt die RTC folgende Prozedur:

void update_rtc(uint32_t seconds) {
    // Wait until last write operation is done
    while(!READ_BIT(RTC->CRL, RTC_CRL_RTOFF)) {}

    // Enter configuration mode
    SET_BIT(RTC->CRL,RTC_CRL_CNF);

    RTC->CNTH = (uint16_t)(seconds >> 16);
    RTC->CNTL = (uint16_t)(seconds & 0xFFFF);

    // Leave configuration mode
    CLEAR_BIT(RTC->CRL,RTC_CRL_CNF);

    // Wait until last write operation is done
    while(!READ_BIT(RTC->CRL, RTC_CRL_RTOFF)) {}
}

RTC Kalibrieren

Ohne Kalibrierung stellt man den Vorteiler wie oben gezeigt auf 32767, die Frequenz des Quarzes wird dann durch 32767+1 geteilt. Meine Blue Pill Boards hatten dabei 1-2 Sekunden Abweichung pro Tag. Sollte deine Uhr ohne Kalibrierung um mehr als 5 Sekunden pro Tag abweichen, liegt mit Sicherheit ein Hardwarefehler vor.

Um die Uhr ohne teure Messinstrumente grob zu kalibrieren lässt man sie einige Tage lang laufen und ermittelt dabei ihre Abweichung von der soll-Geschwindigkeit durch Vergleich mit einem präzisen Zeitserver.

Dann stellt man den Vorteiler RTC->PRLL so ein, dass die Uhr gerade eben etwas zu schnell läuft. Jede Verringerung um 1 macht die Uhr 2,637 Sekunden pro Tag schneller. Anschließend stellt man im Register BKP->RTCCR einen Korrekturwert (0-127) ein, wobei jede Stufe die Uhr um 0,082s pro Tag verlangsamt.

Wenn die Uhr zum Beispiel 4 Sekunden pro Tag zu langsam wäre, würde man den Vorteiler um 2 verringern und dann den Korrekturwert auf 15 stellen:

Abweichung:                         4,000 Sekunden
Vorteiler:          -2 * 2,637 =   -5,274 Sekunden  (setze RTC->PRLL = 32767 - 2)
Korrekturwert:      16 * 0,082 =   +1,312 Sekunden  (setze BKP->RTCCR = 16)
==================================================
Summe:                              0,038 Sekunden
Es bleibt eine Ungenauigkeit von 0,038 Sekunden pro Tag übrig.

Arduino

Mit dem Arduino Framework ist das Programmieren einfach, aber die Programme sind größer, langsamer, und man kann nicht alle Funktionen des Chips ausnutzen. Andererseits hindert die IDE niemanden daran, am Framework vorbei zu programieren. Ein großer Vorteil ist die Verfügbarkeit zahlreicher Bibliotheken. Falls du Arduino mit STM32F1 ausprobieren möchtest, solltest du die bewegte Historie von STM32duino kennen:

Das Arduino Framework begann 2005 mit kleinen 8 Bit AVR Mikrocontrollern als Fork von Wiring (die ganze hässliche Story).

Die ersten inoffizellen 32 Bit Boards kamen 2009 von der Firma Leaflabs: Das "Maple" Board und das kleinere "Maple Mini", beide mit STM32F103C8T6. Leaflabs musste dazu einen anderen Compiler und eigene Bibliotheken in die Arduino IDE integrieren. Kurz danach kam das ähnliche Blue Pill Board auf den Markt, das heute noch angeboten wird.

Das erste offizielle 32 Bit Board von Arduino selbst war das Arduino Due von 2012. Dieses Board wurde von den Makern weitgehend ignoriert, vielleicht weil es viel zu teuer ist.

Als Leaflabs den Produktsupport für die Maple Boards im Jahr 2016 beendete, übernahm Roger Clarks wesentliche Teile des Codes in sein STM32duino Projekt. Er hielt damit die Unterstützung für STM32F103 am Leben. Der Compiler vom Arduino Due Board unterstützt auch STM32, so dass Roger diesen Compiler nun mit benutzen konnte. Dan Drown hat den Code von Roger Clarks so verpackt, dass man ihn bequem mit dem Boardmanager der IDE installieren kann.

Im Jahr 2018 übernahme die Firma ST die Rechte am STM32duino Projekt und schrieb es auf Basis des Cube HAL Frameworks um. So konnte die Firma ST schnell fast alle STM32 Modelle unterstützen. Das neue STM32duino ist nur teilweise zum alten STM32duino kompatibel. Die damit erzeugten Programme sind allerdings größer und etwas langsamer.

Wenn du im Internet nach Anleitungen über STM32 in Arduino suchst, musst du zwischen den STM32duino Versionen von Roger Clarks und ST unterscheiden. Beide werden heute noch werwendet. Roger Clarks hat sein Projekt zwar inzwischen in "Arduino STM32" umbenannt, doch die meisten Leute sind beim ursprünglichen Namen "STM32duino" geblieben.

STM32duino von ST

So installierst du den neuen STM32duino Core von ST:

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

Links zur Dokumentation von ST und zur Dokumentation von Arduino.

STM32duino von Roger Clarks

So installierst du den alten STM32duino Core von Roger Clarks:

Links zur Dokumentation von Roger Clarks und zur Dokumentation von Arduino.

Die Pins PA13, PA14, PA15, PB3 und PB4 sind standardmäßig für die ungenutzte JTAG Schnittstelle reserviert. Das kann man so ändern:

void setup() {
    // Disable both SWD and JTAG to free PA13, PA14, PA15, PB3 and PB4
    afio_cfg_debug_ports(AFIO_DEBUG_NONE);
    
    or:      
    
    // Disable JTAG only to free PA15, PB3 and PB4. SWD remains active
    afio_cfg_debug_ports(AFIO_DEBUG_SW_ONLY);
}

Dieser Arduino Core basiert nicht auf der CMSIS, daher heißen dort die Konstanten für die Register und Bitmasken etwas anders. Werfe dazu ggf. einen Blick in die Dateien packages/stm32duino/hardware/STM32F1/2022.9.26/system/libmaple/stm32f1/include/series/*.h

STM32duino Bootloader

Für einige STM32F103 Boards steht der STM32duino Bootloader zur Verfügung, der das Hochladen von Programmen über den USB Anschluss des Mikrocontrollers ermöglicht. Er belegt die ersten 8 KiB vom Flash Speicher. Für das Blue Pill Board ist die Datei generic_boot20_pc13.bin die richtige.

Der STM32duino Bootloader ist nur eine Sekunde lang nach dem Loslassen des Reset-Knopfes aktiv. Dies zeigt er durch schnelles Blinken der Status-LED an. Innerhalb dieser kurzen Zeit kann man mit der Arduino IDE einen Sketch hochladen. Im Gerätemanager von Windows erscheint der Bootloader als "libusb-win32 devices/Maple DFU". Nach einer Sekunde verschwindet er wieder, danach erscheint der virtuelle serielle COM Port des Sketches. Wegen des schwierigen Timings finde ich den Bootloader unhandlich und benutze ihn nicht gerne.

Damit Windows den USB Bootloader wie gezeigt erkennt, braucht es einen Treiber aus dem Arduino_STM32 Paket. Führe zur Installation die Datei drivers/win/install_driver.bat aus. Linux und Mac OS brauchen keinen Treiber. Zum Hochladen wird das Programm dfu-util verwendet, welches in beiden STM32duino Cores enthalten ist (keine separate Installation nötig).

Manche STM32 Modelle (z.B. STM32L072 und STM32F303) besitzen schon ab Werk einen DFU Bootloader, der den verfügbaren Flash Speicher nicht verkleinert.

Serielle Ports in Arduino

Beim STM32duino Core von Roger Clarks ist das "Serial" Objekt ein virtueller COM Port über den USB Anschluss des Mikrocontrollers (PA11,PA12). Für die echten seriellen Schnittstellen (USART) gibt es die Objekte Serial1, Serial2 und Serial3.

Beim STM32duino Core von ST legt man in der Board-Konfiguration fest, ob das generische "Serial" Objekt angelegt werden soll:

Für die übrigen seriellen Ports muss man Instanzen von HardwareSerial anlegen. Kopiervorlage:
HardwareSerial Serial1(PA10,PA9);
HardwareSerial Serial2(PA3,PA2);
HardwareSerial Serial3(PB11,PB10);
HardwareSerial Serial4(PC11,PC10);
HardwareSerial Serial5(PD2,PC12);

void setup() {
    Serial1.begin(115200); 
    Serial2.begin(115200); 
    Serial3.begin(115200);
    Serial4.begin(115200);
    Serial5.begin(115200);
}

Virtueller COM Port in Arduino

Der virtuelle COM Port über USB (CDC generic serial) ist integraler Bestandteil von STM32duino, deswegen ist er sehr einfach zu programmieren. Die Einstellung der Baudrate kann entfallen, weil sie keine Rolle spielt. Ein kompletter Beispiel-Sketch:

void setup() {
    // PA5 is connected to the status LED
    pinMode(PA5, OUTPUT);
}

void loop() {
    digitalWrite(PA5, LOW);
    Serial.println("Tick");
    delay(500);

    digitalWrite(PA5, HIGH);
    Serial.println("Tack");
    delay(500);
}

Die Ausgabe kann man mit dem seriellen Monitor oder einem Terminal-Programm anzeigen, indem man damit den virtuellen COM Port (VCP) öffnet. Die Baudrate wird ignoriert. Im Terminalprogramm muss das DTR Signal eingeschaltet sein (deswegen geht es hier nicht einfach mit dem cat Befehl). Wenn der PC die Zeichen nicht abholt, wird nach ein paar Sekunden der Puffer voll, und dann bleibt der Mikrocontroller in einer Warteschleife hängen (die LED hört auf zu blinken).

STM32 Anleitungen