20.10.2018
800 x 600 1024 x 768 STM32 Tutorial// Vorwort 2018
Seit dem Verfassen dieses Tutorials im Jahr 2012/13 hat sich im Bereich der STM32 einiges getan. Hier die wichtigsten Hinweise:
- Die Entwicklung von CoIDE ist eingestellt worden. Aktuell ist die Seite coocox.org nicht einmal mehr aufrufbar. Stattdessen empfehle ich "TrueStudio for STM32" von Atollic. Atollic wurde Ende 2017 von STMicroelectronics übernommen und kurze Zeit darauf wurde die Pro-Version der IDE ohne Beschränkungen für STM32 verfügbar.
- Die "Standard Peripheral Library" ist veraltet und wurde von ST durch die "HAL-Library" ersetzt. Für ältere Mikrocontroller, wie z.B. den STM32F103 ist die Standard Peripheral Library noch zum Download verfügbar, neuere Modelle, wie z.B. der STM32L433, werden nur noch von der HAL-Library unterstützt.
- Mit "STM32CubeMX" wurde ein Tool geschaffen, mit welchem Softwareprojekte über eine grafische Oberfläche konfiguriert werden können. CubeMX generiert alle notwendigen Projektfiles, sowie Code zur Initialisierung der Peripherien im Mikrocontroller. Damit kann in kurzer Zeit ein lauffähiges Projekt erzeugt werden, was insbesondere bei Einsteigern die Frustration massiv senken dürfte. ;)
Ich selbst empfehle alle neuen Projekte mit CubeMX und der HAL-Lib aufzubauen. Einige Bedenkenträger sehen das kritisch, z.B. da die HAL sehr viel Overhead enthält und damit die Performance senkt. Das Argument ist richtig, jedoch in vielen Fällen nicht relevant. Ob mein Mikrocontroller 10 ms für die Initialisierung beim Start benötigt oder 100 ms, spielt in vielen Projekten keine Rolle. Relevant wird es bei HAL-Funktionen, welche im Betrieb mit hoher Wiederholungsrate aufgerufen werden. In diesem Fall hindert dich aber natürlich niemand daran eigene Routinen zu schreiben, welche nur die nötigsten Instruktionen enthalten. Auch die Standard Peripheral Library ist in dieser Hinsicht nicht optimal, sie hat weniger Overhead, aber sie hat Overhead. Der gravierende Vorteil von CubeMX und HAL ist, dass du im Regelfall weniger Energie in nicht-zeitkritische Funktionen investieren musst und dich dafür mehr auf die Optimierung der wirklich relevanten Module fokussieren kannst.
Ist das folgende Tutorial damit inzwischen überholt und nutzlos geworden? Ich denke nein, allerdings macht es inzwischen wenig Sinn den Code als direkte Vorlage zu benutzen und zu kopieren. Stattdessen versuch besser die Erklärungen nur nachzuvollziehen und die Codebeispiele zu verstehen. Die Hardware ist ja weiterhin die gleiche und wenn dir deren Funktionsweise klar ist, weißt du auch, wie die Optionen in CubeMX zu wählen sind.
Damit viel Erfolg mit den STM32!
Kurze Unterbrechung für Werbung...
...und schon geht es weiter mit den STM32. ;)
-----------------------------
Im folgenden Tutorial werde ich versuchen alle Grundlagen zum praktischen Einsatz der STM32 Mikrocontroller von STMicroelectronics zu
vermitteln. Dabei bemühe ich mich den Anteil theoretischer Beschreibungen möglichst gering zu halten. Beispielsweise
soll im Vordergrund stehen, wie das SPI-Interface der STM32 verwendet wird anstatt auf die Funktionsweise von SPI
einzugehen – hierfür gibt es bereits reichlich gute Informationsquellen im Netz.
Zum Verständnis werden Kenntnisse der Programmiersprache C sowie der prinzipielle Umgang mit Mikrocontrollern
vorausgesetzt.
Zugehöriger Thread auf mikrocontroller.net: STM32 Tutorial
Hinweis:
Die Codes lassen sich innerhalb einer Familie (z.B. STM32F1xx) gut übernehmen, da die selbe Standard Peripheral Library genutzt wird und sich lediglich die Pinbelegung unterscheidet sowie bestimmte Hardware unter Umständen nicht vorhanden ist. Für die anderen Familien (STM32L1xx, STM32F0xx, STM32F2xx, STM32F4xx) sind viele weitere nicht unwesentliche Anpassungen notwendig.
- 1 IDE, Programmer & Eval-Boards
- 2 Wichtige Dokumente
- 3 Erster Start: LED einschalten
- 4 Taktfrequenz einstellen
- 5 Standard Peripheral Library
- 6 RCC – Reset and clock control
- 7 GPIO/AFIO – General-purpose and alternate-function I/Os
- 8 General-purpose timers
- 9 Interrupts
- 10 I2C
- 11 CAN
- 12 SPI
- 13 ADC
- 14 DMA
- 15 Häufige Fehler
- 16 SPI mit DMA
- 17 Independent Watchdog (IWDG)
- 18 Option Bytes
- 19 System Timer
- 20 RS485
- 21 DAC
- 22 I2C & DMA
Zum Flashen der STM32 benutze ich den ST-Link V2 von STMicroelectronics. Er stellt eine günstige Lösung zum Programmieren und Debuggen aller STM8 sowie STM32 über JTAG/SWD/SWIM dar.
Als Eval-Boards kann ich das STM32VL Discovery für den ersten Einstieg empfehlen. Auf dem Board befindet sich der STM32F100RB sowie der ST-Link Programmer/Debugger mit USB-Schnittstelle, so dass sofort losgelegt werden kann. Wer sich etwas mehr Peripherie wünscht, der kann einen Blick auf das Olimexino-STM32 Maple werfen.
Die oben genannte Hardware ist beispielsweise bei Watterott electronic zu erhalten.
Als Entwicklungsumgebung verwende ich CooCox. Es ist leicht einzurichten und man kann schnell mit den ersten Projekten beginnen. CoIDE ist, wie der Name schon sagt, die IDE – hier wird später der Programmcode geschrieben. Wichtig ist, dass zusätzlich der ARM GCC Compiler installiert wird. Eine Schritt-für-Schritt-Anleitung ist auf der coocox.org vorhanden. Zum Flashen der Software in den µC wird noch CoFlash benötigt. Es unterstützt unter anderem den oben genannten ST-Link.
// 2 Wichtige Dokumente
Bevor es nun richtig los geht, hier noch eine Liste zu den wichtigsten Informationsquellen die man früher oder später brauchen wird, wenn man sich genauer mit den STM32 auseinandersetzen muss:
- Datenblatt (STM32F103RB)
Beschreibung des konkreten Chips für Pinbelegung etc. - Reference Manual (STM32F103RB)
Ausführliche Beschreibung der Module einer Familie. Unter Umständen sind nicht alle Module im eingesetzten Chip vorhanden – siehe Datenblatt. - Programming Manual (Cortex-M3)
Enthält beispielsweise Informationen zum Interrupt Controller (NVIC). - Standard Peripheral Library (STM32F10x)
Im Gegensatz zu beispielsweise AVRs sollte man die Register der STM32 nicht direkt ansprechen, sondern über Funktionen der Standard Peripheral Library. Sie ist auf www.st.com zusammen mit einer Dokumentation (Datei: stm32f10x_stdperiph_lib_um.chm) herunterladbar.
// 3 Erster Start: LED einschalten
In diesem Beispiel soll lediglich eine LED per Software eingeschaltet werden um die Schritte zur Erstellung eines Projekts in CoIDE zu zeigen, sowie das Flashen mit CoFlash zu testen.
..\main.c
STM32F100RB
#include "stm32f10x_conf.h" int main(void) { GPIO_InitTypeDef GPIO_InitStructure; SystemInit(); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); GPIO_WriteBit(GPIOC, GPIO_Pin_9, Bit_SET); while(1) { } }
In den nachfolgenden Screenshots ist Schritt für Schritt am Beispiel des STM32VL Discovery gezeigt, welche Einstellungen vorgenommen werden müssen und wie der Code auf den Chip geladen wird. Wird der ST-Link und ein anderes Eval-Board/eine eigene Schaltung verwendet, so muss im dritten und achten Schritt lediglich ein anderer Chip ausgewählt werden und in main.c GPIOC sowie Pin_9 unter Umständen angepasst werden.
Um sich später das Umschalten auf das Programm CoFlash zu sparen sollte man CoIDE so einstellen, dass dort mittels eines Klicks der Prozessor geflashed werden kann. Dazu wählt man im Menü Debug/Debug Configuration aus und wählt für sein Projekt bei Adapter "ST-Link". Über Flash/Programm Download wird nun direkt CoFlash angesteuert (das Fenster von CoFlash muss geschlossen sein). Alternativ kann man den entsprechenden Button unterhalb der Menüleiste anklicken.
C99: Ich empfehle den Compiler im C99 Modus zu benutzen. Ansonsten können beispielsweise in for-Schleifen keine Variablen deklariert werden. Dazu geht man auf Project/Configuration und fügt am Ende des Textfeldes neben "Compiler" noch den Parameter -std=c99 ein.
// 4 Taktfrequenz einstellen
Bevor man seinen eigenen Code in der main-Funktionn ausführt, sollte man die Funktion SystemInit() aufrufen. Sie ist in der Datei system_stm32f10x.c implementiert. Hier werden die Taktquelle, der PLL-Faktor und Prescaler für AHB/APBx festgelegt, sowie etliche andere Dinge, die für einen korrekten Start notwendig sind.
Standardmäßig wird der Takt auf 72 MHz eingestellt, unter der Annahme, dass ein 8 MHz Quarz (HSE_VALUE) angeschlossen ist. Achtung: Diese hohe Taktfrequenz wird nicht von jedem Chip unterstützt! Genaueres ist den Kommentaren innerhalb der Datei zu entnehmen.
Will man den Systemtakt auf 8/24/36/48/56 MHz umstellen, so muss nur die entsprechende Zeile einkommentiert und die Zeile für 72 MHz wieder auskommentiert werden. Der Block vor #else wird compiliert, falls ein STM32 der Value Line ausgewählt wurde.
..\cmsis_boot\system_stm32f10x.c
STM32F103RB
#if defined (STM32F10X_LD_VL) || (defined STM32F10X_MD_VL) || (defined STM32F10X_HD_VL) /* #define SYSCLK_FREQ_HSE HSE_VALUE */ #define SYSCLK_FREQ_24MHz 24000000 #else /* #define SYSCLK_FREQ_HSE HSE_VALUE */ /* #define SYSCLK_FREQ_24MHz 24000000 */ /* #define SYSCLK_FREQ_36MHz 36000000 */ /* #define SYSCLK_FREQ_48MHz 48000000 */ /* #define SYSCLK_FREQ_56MHz 56000000 */ #define SYSCLK_FREQ_72MHz 72000000 #endif
Möchte man eine von den oben angegebenen Werten abweichende Frequenz konfigurieren, so wird die Angelegenheit etwas umständlicher. Doch zunächst einmal ein paar Möglichkeiten und Einschränkungen im Überblick.
Es stehen drei Quellen für den Systemtakt (SYSCLK) zur Wahl:
- HSI: high speed internal clock – interner RC Oszillator mit 8 MHz
- HSE: high speed external clock – erzeugt durch einen Quarz (3...25 MHz) oder ein externe Taktquelle (bis 50 MHz)
- PLL: phase lock loop – Multipliziert die Frequenz der Quelle mit 2...16. Als Quelle dient entweder HSI/2 oder HSE. Achtung: In der Connectivity Line stehen anderen Faktoren zur Verfügung sowie weitere Optionen.
Aus SYSCLK wird über einen Vorteiler (1, 2, 4, ... 512) der Takt für AHB (advanced high performance bus) erzeugt. Aus diesem wiederum werden über zwei Vorteiler (1, 2, 4, 8, 16) die Takte für APB1 und APB2 (advanced peripheral bus) generiert. Zu beachten sind die maximalen Frequenzen (liegen je nach Serie niedriger):
- SYSCLK/AHB/APB2: 72 MHz
- APB1: 36 MHz
Je nach Chip gibt es noch weitere Möglichkeiten. Es ist in jedem Fall sinnvoll das Datenblatt zu lesen.
Als Beispiel werde ich nun zeigen, wie man bei einem STM32F103 die Taktfrequenz auf 64 MHz umkonfiguriert (8 MHz Quarz). APB1 wird auf 32 MHz heruntergeteilt.
Als erstes fügt man ein Define für die neue Frequenz ein.
..\cmsis_boot\system_stm32f10x.c
STM32F103RB
#if defined (STM32F10X_LD_VL) || (defined STM32F10X_MD_VL) || (defined STM32F10X_HD_VL) /* #define SYSCLK_FREQ_HSE HSE_VALUE */ #define SYSCLK_FREQ_24MHz 24000000 #else /* #define SYSCLK_FREQ_HSE HSE_VALUE */ /* #define SYSCLK_FREQ_24MHz 24000000 */ /* #define SYSCLK_FREQ_36MHz 36000000 */ /* #define SYSCLK_FREQ_48MHz 48000000 */ /* #define SYSCLK_FREQ_56MHz 56000000 */ #define SYSCLK_FREQ_64MHz 64000000 /* #define SYSCLK_FREQ_72MHz 72000000 */ #endif
CoIDE hinterlegt allen Code grau, der aufgrund fehlender Defines nicht aktiv ist. Daher kann man die Datei jetzt gut durchgehen und an den entscheidenden Stellen sich Ausschnitte aus dem alten Code kopieren und für die gewünschten 64 MHz anpassen. Folgende Codeschnipsel müssen hinzugefügt werden.
..\cmsis_boot\system_stm32f10x.c
STM32F103RB
#elif defined SYSCLK_FREQ_64MHz uint32_t SystemCoreClock = SYSCLK_FREQ_64MHz; /*!< System Clock Frequency (Core Clock) */
#elif defined SYSCLK_FREQ_64MHz static void SetSysClockTo64(void);
#elif defined SYSCLK_FREQ_64MHz SetSysClockTo64();
Als letzten Schritt muss man noch die Funktion SetSysClockTo64(void) erstellen. Am einfachsten geht dies durch Kopieren der Funktion SetSysClockTo72(void) inklusive #elif defined SYSCLK_FREQ_72MHz. Anschließend benennt man die Funktion+Define um und passt folgende Zeile an:
..\cmsis_boot\system_stm32f10x.c
STM32F103RB
RCC->CFGR |= (uint32_t)(RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9);
zu
RCC->CFGR |= (uint32_t)(RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL8);
Hiermit hat man den Faktor für die PLL von 9 auf 8 gestellt (8 MHz * 8 = 64 MHz). Es bleibt noch die Frage wie man die Vorteiler für AHB, APB1 und APB2 ändert. In diesem Fall ist bereits alles richtig, da der Vorteiler für APB1 bei der 72 MHz Konfiguration bereits auf 2 gesetzt war. Will man die Werte ändern, so sind folgende Zeilen entsprechend zu ändern:
..\cmsis_boot\system_stm32f10x.c
STM32F103RB
/* HCLK = SYSCLK */ RCC->CFGR |= (uint32_t)RCC_CFGR_HPRE_DIV1; /* PCLK2 = HCLK */ RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE2_DIV1; /* PCLK1 = HCLK */ RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE1_DIV2;
// 5 Standard Peripheral Library
Die Standard Peripheral Library stellt alle Funktionen dar um komfortabel auf die Hardware zuzugreifen. Somit müssen die Register im µC selbst nicht direkt im eigenen Code angesprochen werden. In CoIDE können die einzelnen Bestandteile der Bibliothek je nach Bedarf ausgewählt werden.
Danach müssen noch die Header-Dateien durch einkommentieren eingebunden werden.
..\cmsis_boot\stm32f10x_conf.h
STM32F103RB
/* Includes -----------------------------------------------------------*/ /* Uncomment the line below to enable peripheral header file inclusion */ /* #include "stm32f10x_adc.h" */ /* #include "stm32f10x_bkp.h" */ #include "stm32f10x_can.h" /* #include "stm32f10x_cec.h" */ /* #include "stm32f10x_crc.h" */ /* #include "stm32f10x_dac.h" */ /* #include "stm32f10x_dbgmcu.h" */ #include "stm32f10x_dma.h" /* #include "stm32f10x_exti.h" */ /* #include "stm32f10x_flash.h" */ /* #include "stm32f10x_fsmc.h" */ #include "stm32f10x_gpio.h" #include "stm32f10x_i2c.h" /* #include "stm32f10x_iwdg.h" */ /* #include "stm32f10x_pwr.h" */ #include "stm32f10x_rcc.h" /* #include "stm32f10x_rtc.h" */ /* #include "stm32f10x_sdio.h" */ #include "stm32f10x_spi.h" /* #include "stm32f10x_tim.h" */ #include "stm32f10x_usart.h" /* #include "stm32f10x_wwdg.h" */ /* #include "misc.h" */ /* High level functions for NVIC and SysTick (add-on to CMSIS functions) */
// 6 RCC – Reset and clock control
Um ein beliebiges Peripheriemodul nutzen zu können muss diesem zuerst ein Taktsignal zur Verfügung gestellt werden.
..\main.c
STM32F103RB
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
Takt für IO-Port A aktivieren
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE);
Takt für IO-Port A und B aktivieren
RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);
Takt für CAN Interface aktivieren
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
Takt für DMA-Controller 1 aktivieren
Welches Modul an welchem Bus betrieben wird ist dem Datenblatt zu entnehmen. Möchte man den Takt eines Moduls wieder deaktivieren, so ist als zweiter Parameter DISABLE zu übergeben
// 7 GPIO/AFIO – General-purpose and alternate-function I/Os
Um einen Port nutzen zu können muss zunächst sein Taktsignal, wie zuvor beschrieben, aktiviert werden – da dies prinzipiell bei allen Modulen der Fall ist, werde ich in den nächsten Abschnitten nicht mehr explizit darauf eingehen. Zusätzlich muss jeder Pin initialisiert werden. Hierfür steht die Funktion GPIO_Init (GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_InitStruct) zur Verfügung. Der erste Parameter gibt dabei den Namen des Ports an. Über den zweiten Parameter wird Adresse einer GPIO_InitTypeDef-Struktur übergeben. In dieser Struktur wird das Verhalten des/der Pins festgelegt.
..\main.c
STM32F103RB
#include "stm32f10x_conf.h" int main(void) { GPIO_InitTypeDef GPIO_InitStructure; SystemInit(); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_WriteBit(GPIOB, GPIO_Pin_0, Bit_SET); while(1){} }
Konfigurieren von PB0 und PB1 als Ausgang und Setzen von PB0 auf High-Level
Die Struktur GPIO_InitTypeDef besitzt drei Elemente:
- GPIO_Mode
mögliche Werte:
GPIO_Mode_AIN – Analoger Eingang
GPIO_Mode_IN_FLOATING – floatender Eingang
GPIO_Mode_IPD – Eingang mit Pull-Down
GPIO_Mode_IPU – Eingang mit Pull-Up
GPIO_Mode_Out_OD – Ausgang Open-Drain
GPIO_Mode_Out_PP – Ausgang Push-Pull
GPIO_Mode_AF_OD – Alternative Funktion Open-Drain
GPIO_Mode_AF_PP – Alternative Funktion Push-Pull
Letztere zwei Werte müssen gewählt werden, falls der Pin als Ausgang eines Moduls wie etwas USART, SPI, I22, etc. genutzt wird. - GPIO_Pin
mögliche Werte:
GPIO_Pin_x – x: 0...15
GPIO_Pin_All – alle Pins des Ports
Es können mehrere Pins über | gleichzeitig konfiguriert werden. - GPIO_Speed
mögliche Werte:
GPIO_Speed_2MHz
GPIO_Speed_10MHz
GPIO_Speed_50MHz
Nur bei Ausgängen relevant. Maximale Frequenz des Ausgangssignals.
Einzelne Bits können über die Funktion GPIO_WriteBit gesetzt oder gelöscht werden. Weitere Funktionen zeigt der folgende Code-Ausschnitt.
..\main.c
STM32F103RB
uint8_t dataByte; uint16_t dataHalfWord; // Setzen von PA0 und PA2 GPIO_SetBits(GPIOA, GPIO_Pin_0 | GPIO_Pin_2); // Löschen von PA1 und PA3 GPIO_ResetBits(GPIOA, GPIO_Pin_1 | GPIO_Pin_3); // 0x1234 auf PORTB schreiben GPIO_Write(GPIOB, 0x1234); // Lesen des Bits PC0 dataByte = GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_0); // Lesen des Bits PC0 aus dem Output Register dataByte = GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_0); // Lesen von PORTC dataHalfWord = GPIO_ReadInputData(GPIOC); // Lesen von PORTC aus dem Output Register dataHalfWord = GPIO_ReadOutputData(GPIOC);
Für viele Peripherie-Module kann zwischen verschiedenen Alternativen der zugehörigen Pins gewählt werden:
..\main.c
STM32F103RB
// default: PB6 - I2C1_SCL; PB7 - I2C1_SDA // remap: PB8 - I2C1_SCL; PB9 - I2C1_SDA GPIO_PinRemapConfig(GPIO_Remap_I2C1, ENABLE);
Remap der I2C1 Pins beim STM32F103
Eine vollständige Liste der möglichen Werte für GPIO_PinRemapConfig (uint32_t GPIO_Remap, FunctionalState NewState) ist der Dokumentation der Standard Peripheral Library zu entnehmen. Welche Remaps am konkreten Chip möglich sind, kann man im zugehörigen Datenblatt nachlesen.
// 8 General-purpose timers
// 8.1 PWM
Die Timer der STM32 besitzen relativ viele Features, weshalb ich mich in diesem Abschnitt zunächst auf die Konfiguration in Hinblick auf die Ausgabe eines PWM Signals beschränken werde. Es soll auf PA0 mittels TIM2 ein 1 kHz Signal mit einem Tastverhältnis von 10% generiert werden:
..\main.c
STM32F103RB
GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBase_InitStructure; TIM_OCInitTypeDef TIM_OC_InitStructure; SystemInit(); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); TIM_TimeBase_InitStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBase_InitStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBase_InitStructure.TIM_Period = 999; TIM_TimeBase_InitStructure.TIM_Prescaler = 71; TIM_TimeBaseInit(TIM2, &TIM_TimeBase_InitStructure); TIM_OC_InitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OC_InitStructure.TIM_OCIdleState = TIM_OCIdleState_Reset; TIM_OC_InitStructure.TIM_OCNIdleState = TIM_OCNIdleState_Set; TIM_OC_InitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC_InitStructure.TIM_OCNPolarity = TIM_OCNPolarity_High; TIM_OC_InitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OC_InitStructure.TIM_OutputNState = TIM_OutputNState_Disable; TIM_OC_InitStructure.TIM_Pulse = 100; TIM_OC1Init(TIM2, &TIM_OC_InitStructure); TIM_Cmd(TIM2, ENABLE);
Zunächst wird wie gewohnt der Takt für sowohl den PortA, also auch für Timer 2 aktiviert. Zu beachten ist, dass der Takt von APB1 automatisch mit dem Faktor 2 multipliziert wird, falls der APB1 Prescaler einen von 1 abweichenden Wert hat. In diesem Fall wurde gemäß den default-Einstellungen der 72 MHz Takt auf 36 MHz für APB1 heruntergeteilt. Folglich ist der Takt an Timer 2 wieder 72 MHz.
Der Code danach initialisiert PA0 als Alternative Function Output.
In den nächsten zwei Blöcken werden die Register für die Zeitbasis sowie den Output Compare Channel konfiguriert.
Mit TIM_CMD(...) wird der Timer aktiviert.
TIM_TimeBaseInitTypeDef:
- TIM_ClockDivision
Vorteiler, falls der Timer seinen Takt von einem externen Eingang bekommt. Andere Werte haben keinen Einfluss auf den Takt, falls der interne Takt CK_INT benutzt wird - was hier der Fall ist. - TIM_CounterMode
mögliche Werte:
TIM_CounterMode_Up
TIM_CounterMode_Down
TIM_CounterMode_CenterAligned1
TIM_CounterMode_CenterAligned2
TIM_CounterMode_CenterAligned3
- TIM_Period
Der Timer zählt von 0 bis zu diesem Wert. Oder anders herum, falls ein anderer Counter Mode gewählt wurde. Maximal 216 = 65535. - TIM_Prescaler
Teilt den Eingangstakt des Timers herunter. Teiler: TIM_Prescaler + 1. Beispielsweise wird der Takt durch zwei geteilt, wenn TIM_Prescaler = 1. Maximal 216 = 65535.
TIM_OCInitTypeDef:
- TIM_OCMode
In Hinblick auf die PWM-Funktionalität können die beiden Werte TIM_OCMode_PWM1 und TIM_OCMode_PWM2 gewählt werden. Bei PWM1 liegt der Ausgang während der Pulsdauer auf high und anschließend auf low, falls die restlichen Einstellungen wie oben sind. PWM2 erzeugt die dazu invertierte Signalform. - TIM_OCIdleState
Zustand des OC Pins im Idle State - siehe Hinweis unten. - TIM_OCNIdleState
Zustand des OCN Pins im Idle State - siehe Hinweis unten. - TIM_OCPolarity
Hier kann die Polarität des OC Ausgangs umgedreht werden. - TIM_OCNPolarity
Hier kann die Polarität des OCN Ausgangs umgedreht werden. Hinweis: Haben OC und OCN die selbe Polaritätseinstellung, so ist OCN gegenüber OC bereits invertiert. - TIM_OutputState
Aktiverten/Deaktivieren des OC Ausgangs. - TIM_OutputNState
Aktiverten/Deaktivieren des OCN Ausgangs. - TIM_Pulse
Pulsweite des Signals.
Hinweise:
Bei den Timern 1, 8, 15, 16 und 17 müssen die PWM Ausgänge zusätzlich mit der Funktion TIM_CtrlPWMOutputs(...) aktiviert werden.
Nur, falls der Timer aktiviert, aber die PWM-Outputs deaktiviert sind kommen die Einstellungen zum Idle State zum Tragen.
Die OCN Ausgänge gibt es allgemein nur bei den Timern 1, 8, 15, 16 und 17.
Die Einstellungen des Output Compare Channels können analog für alle drei weiteren zur Verfügung stehenden Kanäle gesetzt werden. Es können daher beispielsweise vier PWM Ausgänge mit unabhängigem Tastverhältnis bei gleicher Frequenz realisiert werden.
Hierfür existieren neben TIM_OC1Init(...) noch die Funktionen TIM_OC2Init(...), TIM_OC3Init(...) und TIM_OC4Init(...).
// 8.2 Encoder Interface
TIM1 & TIM8, sowie TIM2 ... TIM5 haben jeweils einen Quadraturencoder eingebaut und bieten somit die Möglichkeit die Signale eines Inkrementalgebers direkt auszuwerten.
Der Timer zählt dabei auf Grundlage der Signale TI1 und TI2, welche im einfachsten Fall den Pegeln an TIMx_CH1 sowie TIMx_CH2 entsprechen.
..\main.c
STM32F103RB
GPIO_InitTypeDef GPIO_InitStructure; SystemInit(); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Falling); TIM_Cmd(TIM3, ENABLE);
Der Encoder des Timers wird über die Funktion TIM_EncoderInterfaceConfig(...) vorbereitet.
Der zweite Parameter gibt den Encoder Mode an. Mögliche Werte: TIM_EncoderMode_TIx – bei x = 1 oder x = 2 zählt der Timer bei Flanken auf TI1 bzw. TI2; bei x = 12 zählt er auf Flanken auf TI1 und TI2. Letzterer Modus ist dann zu wählen, wenn in beide Richtungen gezählt werden soll und man zwei Signalleitungen hat.
Parameter drei und vier definieren die Polarität der einzelnen Eingänge. Es kann entweder TIM_ICPolarity_Falling oder TIM_ICPolarity_Rising übergeben werden. Durch umdrehen der Polarität eines Signals kann die Richtung eines Quadratursignals invertiert werden.
Der aktuelle Zählerstand wird mit TIM_GetCounter(...) abgerufen.
..\main.c
STM32F103RB
uint16_t counter = TIM_GetCounter(TIM3);
// 9 Interrupts
// 9.1 Externe Interrupts
Um die Funktionen für Externe Interrupts nutzen zu können, müssen im CoIDE Repository EXTI und MISC selektiert werden und in der Datei stm32f10x_conf.h die entsprechenden Zeilen einkommentiert werden. Als Vorlage für die Interrupt Service Routines sollte man sich die Dateien stm32f10x_it.c sowie stm32f10x_it.h aus der Standard Peripheral Library (Project\STM32F10x_StdPeriph_Template) ins Hauptverzeichnis seines Projekts kopieren. Die Headerdatei muss anschließend noch mit #include "stm32f10x_it.h" in stm32f10x_conf.h eingebunden werden.
Das folgende Programm soll bei jeder steigenden Flanke an PA0 einen Interrupt auslösen. Es ist kompatibel zum STM32VL Discovery.
..\main.c
STM32F100RB
#include "stm32f10x_conf.h" int main(void) { GPIO_InitTypeDef GPIO_InitStructure; EXTI_InitTypeDef EXTI_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; SystemInit(); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOC | RCC_APB2Periph_AFIO, ENABLE); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0); EXTI_InitStructure.EXTI_Line = EXTI_Line0; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure); NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x0F; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x0F; NVIC_Init(&NVIC_InitStructure); while(1){} }
Für externe Interrupts stehen insgesamt 16 Interrupt Lines zur Verfügung. Jede Line kann mit einem beliebigen Port verknüpft werden, wobei die Pin-Nummer bereits festgelegt ist. EXTI0 kann mit PA0, PB0, ... verschaltet werden, jedoch beispielsweise nicht mit PA1. Die Funktion GPIO_EXTILineConfig weißt in obigem Code die EXTI0-Line PORTA, also PA0 zu. Anmerkung: Es sind noch 3 oder 4 weitere EXTI Lines vorhanden, deren Quellen nicht konfigurierbar sind – siehe Reference Manual.
EXTI_Init(EXTI_InitTypeDef *EXTI_InitStruct) initialisiert die EXTI Line. Die Strukturelemente sind selbsterklärend. EXTI_Mode kann alternativ auf EXTI_Mode_Event gesetzt werden – Events werde ich in einem anderen Kapitel behandeln. EXTI_Trigger legt fest, auf welcher Flanke am Pin der Interrupt ausgelöst werden soll.
Der Block danach stellt den NVIC (Nested Vectored Interrupt Controller) ein – das Modul, das für die Interrupt-Verwaltung zuständig ist. Kleiner Tipp: Informationen zum NVIC stehen im Programming Manual, nicht im Reference Manual.
NVIC_IRQChannel gibt an, welcher Interrupt Vektor initialisiert werden soll. Beim Hardware-Design sollte man beachten, dass lediglich EXTI0 ... EXTI4 separate Interrupt Vektoren besetzten. EXTI9_5 und EXTI15_10 fassen die restlichen EXTI Lines zusammen.
NVIC_IRQChannelPreemptionPriority legt den Preemption Priority Level des Interrupts zwischen 0 und 15 fest. Interrupts mit einer niedrigeren Nummer besitzen einen höhere Priorität. Wird ein Interrupt mit einer höheren Priorität ausgelöst, während ein Interrupt einer niedrigeren Priorität abgearbeitet wird, so wird letzterer unterbrochen und die Abarbeitung des Interrupt Handlers des höher priorisierten Interrupts gestartet.
NVIC_IRQChannelSubPriority legt zusätzlich zu jeder Gruppe eines bestimmten Preemption Priority Levels einen Sub Priority Level fest. Falls mehrere Interrupts der gleichen Preemption Priority in der Warteschlange stehen, so wird der Interrupt mit dem niedrigeren Sub Priority Level zuerst ausgeführt. Dieser wird allerdings nicht von einem Interrupt einer höheren Sub Priority unterbrochen, falls dieser die gleiche Preemption Priority besitzt.
Es stehen allerdings nicht gleichzeitig für Sub und Preemption Priority alle Werte von 0 bis 15 zur Verfügung, da im STM32 nur insgesamt 4 Prioritätsbits vorhanden sind. Diese werden mit der Funktion NVIC_PriorityGroupConfig(...) auf Sub und Preemption Priority aufgeteilt. Im Beispiel werden alle vier Bits für den Preemption Priority Level belegt – es wird also nicht zwischen verschiedenen Sub Priorities unterschieden.
Der Interrupt ist nun soweit fertig konfiguriert. Im nächsten Schritt muss noch der Code des zugehörigen Interrupt Handlers geschrieben werden. Dazu fügt man den folgenden Codeausschnitt in die Datei stm32f10x_it.c ein. Die exakten Bezeichnungen der Funktionsköpfe der verschiedenen Interrupt Handler kann man in der Datei startup_stm32f10x_xx_xx.c nachschlagen.
Zu Beginn des Interrupt Handlers muss das Pending Bit gelöscht werden. Schreibt man diese Anweisung in die letzte Zeile, bevor der Interrupt Handler verlassen wird, so führt dies zu Problemen, falls der Code-Optimierer eingeschaltet ist.
Für praktische Anwendungen sollte der Code hinsichtlich Tastenentprellung noch verfeinert werden.
..\stm32f10x_it.c
STM32F100RB
void EXTI0_IRQHandler(void){ EXTI_ClearITPendingBit(EXTI_Line0); if(GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_9)){ GPIO_WriteBit(GPIOC, GPIO_Pin_9, RESET); }else{ GPIO_WriteBit(GPIOC, GPIO_Pin_9, SET); } }
// 9.2 Timer Interrupts
In diesem Beispiel wird Timer 2 so konfiguriert, dass er alle 500 ms (72 MHz Prozessortakt) einen Interrupt generiert. Im Interrupt Handler wird dann eine LED an PA5 je nach vorherigem Zustand entweder ein- oder ausgeschaltet.
..\main.c
STM32F103RB
#include "stm32f10x_conf.h" int main(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBase_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; SystemInit(); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); TIM_TimeBase_InitStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBase_InitStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBase_InitStructure.TIM_Period = 1999; TIM_TimeBase_InitStructure.TIM_Prescaler = 17999; TIM_TimeBaseInit(TIM2, &TIM_TimeBase_InitStructure); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x0F; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x0F; NVIC_Init(&NVIC_InitStructure); TIM_Cmd(TIM2, ENABLE); while(1){} }
Die Konfiguration läuft ähnlich wie bei externen Interrupts ab. Zuerst muss dem Modul, das den Interrupt auslösen soll, mitgeteilt werden, bei welchem/welchen Ereignis/Ereignissen es dies tun soll. Mit Hilfe der Funktion TIM_ITConfig wird hier das Update-Ereignis ausgewählt. Es tritt immer dann auf, wenn der Timer sein Zählregister mit dem Auto-Reload Register (TIM_Period) aktualisiert, also den höchsten Zählwert erreicht hat und von 0 mit dem Zählvorgang beginnt.
Im NVIC muss man anschließend noch den Interrupt aktivieren.
Als Letztes fehlt nur noch der Code für den Interrupt Handler.
..\stm32f10x_it.c
STM32F103RB
void TIM2_IRQHandler(void){ TIM_ClearITPendingBit(TIM2, TIM_IT_Update); if(GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_5)){ GPIO_WriteBit(GPIOA, GPIO_Pin_5, RESET); }else{ GPIO_WriteBit(GPIOA, GPIO_Pin_5, SET); } }
// 10 I2C
// 10.1 Konfiguration
..\main.c
STM32F103RB
GPIO_InitTypeDef GPIO_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; I2C_InitTypeDef I2C_InitStructure; SystemInit(); RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_PinRemapConfig(GPIO_Remap_I2C1, ENABLE); NVIC_InitStructure.NVIC_IRQChannel = I2C1_EV_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); NVIC_InitStructure.NVIC_IRQChannel = I2C1_ER_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); I2C_DeInit(I2C1); I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; I2C_InitStructure.I2C_ClockSpeed = 100000; I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; I2C_InitStructure.I2C_OwnAddress1 = 0; I2C_Init(I2C1, &I2C_InitStructure); I2C_ITConfig(I2C1, I2C_IT_EVT, ENABLE); I2C_ITConfig(I2C1, I2C_IT_BUF, ENABLE); I2C_ITConfig(I2C1, I2C_IT_ERR, ENABLE); I2C_Cmd(I2C1, ENABLE);
I2C_InitTypeDef:
- I2C_Ack
Beim Empfang der eigenen Adresse oder eines Datenbytes wird ein Acknowledgement Bit gesendet, falls I2C_Ack_Enable. - I2C_AcknowledgedAddress
Bitlänge der eigenen Adresse im Slave Mode. Mögliche Werte: I2C_AcknowledgedAddress_7bit, I2C_AcknowledgedAddress_10bit - I2C_ClockSpeed
Frequenz der Taktleitung in Hz. Hinweis: Wird nicht die Standard-Konfiguration verwendet, sondern z.B. ein 16 MHz Quarz, so ist die interne Berechnung falsch und die resultierende Frequenz weicht vom angegebenen Wert ab. - I2C_DutyCycle
Legt das Verhältnis von low zu high im Fast-Mode (ab 100kHz) fest. Mögliche Werte: I2C_DutyCycle_16_9 (Tlow/Thigh = 16/9), I2C_DutyCycle_2 (Tlow/Thigh = 2) - I2C_Mode
I2C / SMBus – Mögliche Werte: I2C_Mode_I2C, I2C_Mode_SMBusDevice, I2C_Mode_SMBusHost - I2C_OwnAddress1
Definition der ersten eigenen Adresse – im STM32 kann zusätzlich eine zweite eigene Adresse verwendet werden. Wertebereich: 0 ... 127 / 0 ... 1023 je nach zuvor definiertem Adressbereich. Die Einstellung ist nur sinnvoll, falls der STM32 als Slave arbeitet.
Wichtig: Damit das I2C-Modul richtig funktioniert, muss es vor der Initialisierung mit der Funktion I2C_DeInit(...) deinitialisiert werden.
Mit I2C_ITConfig(...) werden die drei verschiedenen Interrupt-Quellen aktiviert. Der Event Interrupt wird ausgelöst, wenn im Master Mode das Start-Bit oder die Adresse gesendet wurde oder eine Byte Übertragung vollständig ist. Im Slave Mode wird er beim Empfang der eigenen Adresse oder eines Stop-Bits sowie wenn ein Byte übertragen wurde getriggert. Der Buffer Interrupt tritt auf, falls der Receive Buffer ein neues Byte enthält oder der Transmit Buffer leer wird. Event- und Buffer-Interrupts landen im selben Interrupt Vektor.
Zusätzlich ist ein Error Interrupt Vektor vorhanden.
Die Routinen des I2C-Moduls sollen hier exemplarisch in die Datei i2c.c ausgelagert werden. Dazu wird innerhalb der Interrupt Handler jeweils eine zugehörige Funktion der Datei i2c.c aufgerufen.
..\stm32f10x_it.c
STM32F103RB
void I2C1_EV_IRQHandler(void){ i2c_handleEventInterrupt(); } void I2C1_ER_IRQHandler(void){ i2c_handleErrorInterrupt(); }
// 10.2 Senden & Empfangen (STM32 = Master)
..\i2c.h
STM32F103RB
#include "stm32f10x_conf.h" void i2c_handleEventInterrupt(void); void i2c_handleErrorInterrupt(void); void i2c_create(I2C_TypeDef * I2Cx); void i2c_writeByte(uint8_t address, uint8_t byte); void i2c_writeTwoBytes(uint8_t address, uint8_t byte1, uint8_t byte0); void i2c_readTwoBytes(uint8_t address); uint16_t i2c_getData(void);
Damit die nachfolgenden Funktionen sowohl in main.c als auch stm32f10x_it.c sichtbar sind werden sie in der Header-Datei i2c.h deklariert. i2c.h muss danach noch mit #include "i2c.h" in stm32f10x_conf.h eingebunden werden.
..\i2c.c
STM32F103RB
#include "i2c.h" I2C_TypeDef * I2C_Module; volatile uint8_t deviceAddress; volatile uint8_t dataByte1; volatile uint8_t dataByte0; volatile uint8_t receivedDataByte1; volatile uint8_t receivedDataByte0; volatile uint8_t i2cDirectionWrite; volatile uint8_t i2cByteCounter; volatile uint8_t i2cBusyFlag; void i2c_writeByte(uint8_t address, uint8_t byte){ while(i2cBusyFlag){} while(I2C_GetFlagStatus(I2C_Module,I2C_FLAG_BUSY)){} deviceAddress = address; dataByte0 = byte; i2cDirectionWrite = 1; i2cBusyFlag = 1; i2cByteCounter = 1; I2C_GenerateSTART(I2C_Module, ENABLE); } void i2c_writeTwoBytes(uint8_t address, uint8_t byte1, uint8_t byte0){ while(i2cBusyFlag){} while(I2C_GetFlagStatus(I2C_Module,I2C_FLAG_BUSY)){} deviceAddress = address; dataByte1 = byte1; dataByte0 = byte0; i2cDirectionWrite = 1; i2cBusyFlag = 1; i2cByteCounter = 2; I2C_GenerateSTART(I2C_Module, ENABLE); } void i2c_readTwoBytes(uint8_t address){ while(i2cBusyFlag){} while(I2C_GetFlagStatus(I2C_Module,I2C_FLAG_BUSY)){} deviceAddress = address; i2cDirectionWrite = 0; i2cBusyFlag = 1; i2cByteCounter = 2; I2C_AcknowledgeConfig(I2C_Module, ENABLE); I2C_GenerateSTART(I2C_Module, ENABLE); } void i2c_create(I2C_TypeDef * I2Cx){ I2C_Module = I2Cx; i2cBusyFlag = 0; } uint16_t i2c_getData(void){ return (receivedDataByte1 << 8) | receivedDataByte0; } // ISR void i2c_handleEventInterrupt(void){ if(I2C_GetFlagStatus(I2C_Module, I2C_FLAG_SB) == SET){ if(i2cDirectionWrite){ // STM32 Transmitter I2C_Send7bitAddress(I2C_Module, deviceAddress, I2C_Direction_Transmitter); }else{ // STM32 Receiver I2C_Send7bitAddress(I2C_Module, deviceAddress, I2C_Direction_Receiver); } }else if(I2C_GetFlagStatus(I2C_Module, I2C_FLAG_ADDR) == SET || I2C_GetFlagStatus(I2C_Module, I2C_FLAG_BTF) == SET){ I2C_ReadRegister(I2C_Module, I2C_Register_SR1); I2C_ReadRegister(I2C_Module, I2C_Register_SR2); if(i2cDirectionWrite){ // STM32 Transmitter if(i2cByteCounter == 2){ I2C_SendData(I2C_Module, dataByte1); i2cByteCounter--; }else if(i2cByteCounter == 1){ I2C_SendData(I2C_Module, dataByte0); i2cByteCounter--; }else{ I2C_GenerateSTOP(I2C_Module, ENABLE); i2cBusyFlag = 0; } } }else if(I2C_GetFlagStatus(I2C_Module, I2C_FLAG_RXNE) == SET){ // STM32 Receiver I2C_ReadRegister(I2C_Module, I2C_Register_SR1); I2C_ReadRegister(I2C_Module, I2C_Register_SR2); i2cByteCounter--; if(i2cByteCounter == 1){ I2C_AcknowledgeConfig(I2C_Module, DISABLE); I2C_GenerateSTOP(I2C_Module, ENABLE); receivedDataByte1 = I2C_ReceiveData(I2C_Module); }else{ receivedDataByte0 = I2C_ReceiveData(I2C_Module); i2cBusyFlag = 0; } } } // ISR void i2c_handleErrorInterrupt(void){ I2C_GenerateSTOP(I2C_Module, ENABLE); i2cBusyFlag = 0; I2C_ClearFlag(I2C_Module, I2C_FLAG_AF); I2C_ClearFlag(I2C_Module, I2C_FLAG_ARLO); I2C_ClearFlag(I2C_Module, I2C_FLAG_BERR); }
Die Funktionen i2c_writeByte(...), i2c_writeTwoBytes(...) und i2c_readTwoBytes(...) sind ähnlich implementiert. Zunächst wird so lange gewartet, bis die Variable i2cBusyFlag den Wert 0 enthält um bereits aktive Transfers nicht zu unterbrechen. Dem gleichen Zweck dient die nachfolgende while-Schleife, die ein Status-Flag des I2C-Moduls abfragt. Für den praktischen Einsatz sollte man in den Schleifen noch einen Timeout-Zähler abfragen, damit der Prozessor nicht hängen bleiben kann. Alternative könnte mit return auch direkt aus der Funktion gesprungen werden, falls das I2C-Modul beschäftigt ist, so dass der Prozessor in der Zwischenzeit andere Arbeiten erledigen kann.
Anschließend werden alle Parameter in Variablen zwischengespeichert, da diese Werte innerhalb der Interrupts gebraucht werden. Die nachfolgenden Flags / Counter sollten selbsterklärend sein.
Am Ende der Funktionen wird dem I2C-Modul mitgeteilt, dass es ein Start-Signal senden soll. Aufgrund der Konfiguration der Interrupts werden diese danach automatisch angesprungen.
i2c_create(...) muss im Hauptprogramm mit dem gewünschten I2C-Modul aufgerufen werden. Alternativ könnte man das Modul auch per #define festlegen um den Code portable zu halten.
i2c_getData(...) gibt die empfangenen Daten zurück.
Im Event Interrupt Handler wird geprüft, ob das SB-Flag gesetzt ist. Ist dies der Fall, so erwartet das I2C-Modul im nächsten Schritt die Adresse des anzusprechenden Devices. Ist andernfalls eines der Flags ADDR (Adresse gesendet – ADD10 zuvor gesetzt nach erstem Byte einer 10-Bit Adresse) oder BTF (Byte Transfer Finished) gesetzt, so wird der Block nach dem ersten else if ausgeführt.
Hier darf das Auslesen der Register SR1 und SR2 nicht vergessen werden. Ansonsten werden die Flags nicht gelöscht und die nachfolgende Kommunikation funktioniert nicht mehr. Bei eingeschaltetem Code-Optimizer muss verhindert werden, dass dieser den Code wegoptimiert, z.B. durch Entgegennahme des Rückgabewerts von einer volatile-Variable.
Der Teil zum Senden von Bytes ist relativ einfach aufgebaut. Ist der Byte-Counter größer 0, so werden nacheinander weitere Bytes ins I2C-Modul geschoben. Ansonsten wird ein Stop-Signal übertragen und die Variable i2cBusyFlag auf 0 gesetzt um neue Transfers freizugeben.
Im Empfangsmodus setzt das I2C-Modul das Flag RXNE, wenn es ein Byte vollständig erhalten hat und löst hier zusätzlich einen Interrupt aus. Beim Empfangen von Bytes ist auf das richtige Timing zu achten. Die Befehle zum Senden des NACK-Bits sowie des Stop-Signals müssen direkt vor dem Lesen des vorletzten Bytes gegeben werden. Soll nur ein Byte empfangen werden, so müssen diese Maßnahmen bereits nach dem Senden der Adresse eingeleitet werden.
Sollte während der Kommunikation ein Fehler auftreten, so wird der Error Interrupt Handler aufgerufen. Dies kann z.B. der Fall sein, wenn es kein Device gibt, dass auf die gesendete Adresse ein ACK-Bit setzt. Im Handler sollte daher der Bus mittels eines Stop-Signals wieder frei gegeben werden. Ebenfalls müssen die Error-Flags gelöscht werden.
// 11 CAN
// 11.1 Konfiguration
..\main.c
STM32F103RB
GPIO_InitTypeDef GPIO_InitStructure; CAN_InitTypeDef CAN_InitStructure; SystemInit(); RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_PinRemapConfig(GPIO_Remap1_CAN1 , ENABLE); CAN_InitStructure.CAN_Prescaler = 2; CAN_InitStructure.CAN_SJW = CAN_SJW_2tq; CAN_InitStructure.CAN_BS1 = CAN_BS1_11tq; CAN_InitStructure.CAN_BS2 = CAN_BS2_4tq; CAN_InitStructure.CAN_Mode = CAN_Mode_Normal; CAN_InitStructure.CAN_TTCM = DISABLE; CAN_InitStructure.CAN_ABOM = DISABLE; CAN_InitStructure.CAN_AWUM = DISABLE; CAN_InitStructure.CAN_NART = ENABLE; CAN_InitStructure.CAN_RFLM = DISABLE; CAN_InitStructure.CAN_TXFP = DISABLE; CAN_Init(CAN1, &CAN_InitStructure);
Die Bitrate des CAN-Buses errechnet sich aus APB1 / CAN_Prescaler / Summe der Zeitquanten. Es gibt drei Segmente von Zeitquanten, die in folgender Reihenfolge auftreten: SYNC_SEG (beim STM32 immer 1), BS1 und BS2. Der APB1 Takt des Beispielcodes beträgt 32 MHz. Folglich ist die CAN-Bitrate 32 MHz / 2 / (1 + 11 + 4) = 1 MBit/s.
Der Sample Point liegt zwischen BS1 und BS2 und sollte bei ca. 75 % sein.
Hier: (1 tq + 11 tq) / (1 tq + 11 tq + 4 tq) = 75 %.
Bei der Hardwarebeschaltung ist zu beachten, dass auch zu Testzwecken die Leitungen TX und RX (evtl. über Pegelwandler und Terminierung) miteinander verbunden sind. Andernfalls kann sich das CAN-Modul nicht selbst "hören" und wird keine vollständigen Frames senden.
CAN_InitTypeDef:
- CAN_Prescaler
Vorteiler – Wertebereich: 1 ... 1024 - CAN_SJW
Synchronisation Jump Width – das CAN-Modul kann ein Bit um diesen Wert zur Resynchronisation verlängern oder kürzen, Wertebereich: CAN_SJW_1tq ... CAN_SJW_4tq - CAN_BS1
Time Quantum in Bit Segment 1 – Wertebereich: CAN_BS1_1tq ... CAN_BS1_16tq - CAN_BS2
Time Quantum in Bit Segment 2 – Wertebereich: CAN_BS2_1tq ... CAN_BS2_8tq - CAN_Mode
mögliche Werte: CAN_Mode_Normal, CAN_Mode_LoopBack, CAN_Mode_Silent, CAN_Mode_Silent_LoopBack
Alle Modes neben Normal dienen zu Testzwecken. Dabei kann das CAN-Modul seine gesendeten Nachrichten selbst empfangen ohne dabei zu senden, ohne Nachrichten vom Bus zu empfangen oder beides. Genaueres ist dem Reference Manual zu entnehmen. - CAN_TTCM
Time Triggered Communication Mode – das CAN-Modul speichert die Zeiten, wenn eine Nachricht gesendet wird oder eine Nachricht empfangen wird, falls auf ENABLE gesetzt. Wie diese auszulesen sind steht im Reference Manual. - CAN_ABOM
Automatic Bus-Off Management – Der Bus-Off State wird automatisch verlassen, wenn 128 mal 11 rezessive Bits auf dem Bus erkannt werden, falls auf ENABLE gesetzt. Andernfalls muss das CAN-Modul manuell neu initialisiert werden. Der Bus-Off State wird durch 255 Übertragungsfehler beim Senden ausgelöst und verhindert das weitere Senden und Empfangen von Nachrichten. - CAN_AWUM
Automatic Wakeup Mode – legt fest, wie der Sleep Mode verlassen wird, DISABLE bedeutet, dass die Software durch löschen des SLEEP Bits das Modul wecken muss, ENABLE bedeutet, dass das Modul automatisch geweckt wird, wenn auf dem CAN-Bus eine Nachricht ankommt. - CAN_NART
No Automatic Retransmission – Falls auf ENABLE gesetzt, wird jede Nachricht nur einmal gesendet, egal ob dabei ein Fehler auftrat. Ansonsten versucht das CAN-Modul die Nachricht so lange zu senden, bis kein Übertragungsfehler mehr auftritt oder der Bus-Off State ausgelöst wird. - CAN_RFLM
Receive FIFO Locked Mode – definiert, was passieren soll, wenn der Empfangs-FIFO überläuft. DISABLE: Eine neu eintreffenden Nachricht überschreibt die vorher angekommene, im FIFO liegende, Nachricht. ENABLE: Eine neu eintreffende Nachricht wird verworfen. - CAN_TXFP
Transmit FIFO Priority – legt die Reihenfolge fest, nach der Nachrichten gesendet werden, entweder nach Identifier (DISABLE) oder chronologisch (ENABLE).
..\main.c
STM32F103RB
... CanTxMsg canMessage; canMessage.StdId = 0x123; canMessage.ExtId = 0; canMessage.RTR = CAN_RTR_DATA; canMessage.IDE = CAN_ID_STD; canMessage.DLC = 8; canMessage.Data[0] = 0; canMessage.Data[1] = 1; canMessage.Data[2] = 2; canMessage.Data[3] = 3; canMessage.Data[4] = 4; canMessage.Data[5] = 5; canMessage.Data[6] = 6; canMessage.Data[7] = 7; CAN_Transmit(CAN1, &canMessage);
CanTxMsg:
- StdId
Standard Identifier – Wertebereich: 0 ... 0x7FF - ExtId
Extended Identifier – Wertebereich: 0 ... 0x1FFFFFFF - RTR
Remote Transmission Request. Kennzeichnet ein Remote-Frame, falls auf CAN_RTR_Remote gesetzt. - IDE
Wahl ob der Standard oder Extended Identifier für die Nachricht benutzt werden soll. - DLC
Länge der Nutzdaten in Byte – Wertebereich: 0 ... 8 - Data[8]
Nutzdaten. Data[0] wird zuerst gesendet. Ist DLC kleiner als 8, so werden die letzten Bytes nicht übertragen.
while(!(CAN1->TSR & CAN_TSR_TME0 || CAN1->TSR & CAN_TSR_TME1 || CAN1->TSR & CAN_TSR_TME2)){}
Vor dem Senden sollte man prüfen, ob mindestens eine der drei Transmit Mailboxes frei ist. Ist keine Mailbox mehr frei und man versucht trotzdem zu Senden, so verschwindet die Nachricht im Nirwana. Der obige Code verbleibt dazu so lange in einer while-Schleife, bis zumindest eines der entsprechenden Flags auf 1 steht. Diese Lösung sollte zumindest noch durch einen Timeout-Zähler ergänzt werden um sicher zu stellen, dass die Schleife irgendwann verlassen wird. Ist beispielsweise CAN_NART auf DISABLE gesetzt und es gibt im Netzwerk keine weiteren aktiven CAN-Knoten, so werden die Transmit Mailboxes unter Umständen nie wieder frei.
// 11.3 Nachrichten empfangen
Da beim CAN-Bus alle Daten als Broadcast übertragen werden, müssen die einzelnen Knoten bei jeder Nachricht die ID prüfen um festzustellen, ob die Nachricht interessante Informationen enthält oder ignoriert werden kann. Um die CPU zu entlasten können bei den STM32 Hardwarefilter eingestellt werden. Hierfür sind 14 "Filter Banks" vorgesehen (28 in connectivity line devices). Jede Filter Bank besteht aus zwei 32-Bit Registern (CAN_FxR1 & CAN_FxR2), die jeweils in einer der folgenden Varianten eingesetzt werden können:
- 1x Extended ID und 1x Mask
ID gibt an, welche IDs weitergeleitet werden. Mask legt fest, welche Bits in ID zur Filterung berücksichtigt werden – dadurch kann mittels einer einzigen Filter Bank ein Bereich mit mehreren IDs freigegeben werden. - 2x Extended ID
- 2x Standard ID und 2x Mask
- 4x Standard ID
Die folgende Grafik zeigt die Bedeutung der Bits von CAN_FxR1 im 32-Bit Modus. CAN_FxR2 ist hat exakt den gleichen Aufbau.
Der 16-Bit Modus verdoppelt die Anzahl der Filter Banks, kann aber nur für Standard IDs sinnvoll genutzt werden.
Die Grafiken sind dem Reference Manual (RM0008) aus Seite 640 entnommen. Weitere Details sind dort nachzulesen.
Pro CAN Modul existieren zwei FIFOs, die jeweils Platz für drei empfangene Messages bieten. Jede Filter Bank wird einem FIFO zugeordnet. Dort landen diejenigen Messages, die vom Filter "durchgelassen" worden sind. Jeder FIFO hat seinen eigenen Interrupt Vektor.
..\main.c
STM32F103RB
int main(void) { ... NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = USB_LP_CAN1_RX0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE); CAN_FilterInitTypeDef CAN_FilterInitStructure; CAN_FilterInitStructure.CAN_FilterNumber = 0; CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask; CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit; CAN_FilterInitStructure.CAN_FilterIdHigh = 0x0123 << 5; CAN_FilterInitStructure.CAN_FilterIdLow = 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0xFFFF; CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0xFFFF; CAN_FilterInitStructure.CAN_FilterFIFOAssignment = 0; CAN_FilterInitStructure.CAN_FilterActivation = ENABLE; CAN_FilterInit(&CAN_FilterInitStructure); while (1) {} } void USB_LP_CAN1_RX0_IRQHandler(void) { CanRxMsg RxMessage; CAN_Receive(CAN1, CAN_FIFO0, &RxMessage); if (RxMessage.Data[0] == 1) { GPIO_WriteBit(GPIOA, GPIO_Pin_5, Bit_SET); } else { GPIO_WriteBit(GPIOA, GPIO_Pin_5, Bit_RESET); } }
In diesem Beispiel ist ein Filter auf die Standard-ID 0x123 definiert. Alle Nachrichten mit anderen IDs werden von der Hardware ignoriert. Der Filter ist FIFO 0 zugewiesen. Nach dem Empfang der Nachricht wird der Interrupt Handler aufgerufen. Dieser kopiert die Daten aus dem FIFO in die Struktur RxMessage und schaltet anschließend eine LED an PA5 entsprechend Byte 0 ein oder aus.
CAN_FilterInitTypeDef:
- CAN_FilterNumber
Einzustellende Filter Bank, Werte: 0...13 (0...27 bei connectivity line devices) - CAN_FilterMode
CAN_FilterMode_IdList (2 Extended IDs / 4 Standard IDs) oder CAN_FilterMode_IdMask (ID & Mask) - CAN_FilterScale
CAN_FilterScale_16bit oder CAN_FilterScale_32bit (16-/32-Bit-Modus, siehe oben) - CAN_FilterIdHigh
Obere 16 Bit von CAN_FxR1 (32-Bit Modus)
Untere 16 Bit von CAN_FxR2 (16-Bit Modus) - CAN_FilterIdLow
Untere 16 Bit von CAN_FxR1 - CAN_FilterMaskIdHigh
Obere 16 Bit von CAN_FxR2 - CAN_FilterMaskIdLow
Untere 16 Bit von CAN_FxR2 (32-Bit Modus)
Obere 16 Bit von CAN_FxR1 (16-Bit Modus) - CAN_FilterFIFOAssignment
Zugeordneter FIFO, Werte: 0 oder 1 - CAN_FilterActivation
Gibt an, ob der Filter eingeschaltet werden soll. Werte: ENABLE oder DISABLE
In connectivity line devices sind 28 Filter Banks vorhanden. Standardmäßig sind Filter 0...13 CAN1 und 14...27 CAN2 zugeordnet. Die Grenze, ab der die Filter Banks für CAN2 genutzt werden, kann mit der Funktion CAN_SlaveStartBank(uint8_t CAN_BankNumber) auf 1...27 verschoben werden.
Zum Schluss sind hier noch ein paar Beispiele zu den verschiedenen Kombinationen bei der Filterinitialisierung. Durch die Bitverschiebungen, RTR & IDE Flags sowie verschiedene Bedeutungen je nach Filterbreite sind die Bezeichnungen der Strukturelemente CAN_FilterIDHigh/Low und CAN_FilterMaskIDHigh/Low leider nicht mehr sehr anschaulich. ST hat sich wahrscheinlich aus Gründen der Performance hierfür entschieden.
STM32F103RB
// Alle IDs CAN_FilterInitTypeDef CAN_FilterInitStructure; CAN_FilterInitStructure.CAN_FilterNumber = 0; CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask; CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit; CAN_FilterInitStructure.CAN_FilterIdHigh = 0x0000; CAN_FilterInitStructure.CAN_FilterIdLow = 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x0000; CAN_FilterInitStructure.CAN_FilterFIFOAssignment = 0; CAN_FilterInitStructure.CAN_FilterActivation = ENABLE; CAN_FilterInit(&CAN_FilterInitStructure); // 0x12345670 ... 0x1234567F (Mask: 0xFFFFFFF0UL) CAN_FilterInitTypeDef CAN_FilterInitStructure; CAN_FilterInitStructure.CAN_FilterNumber = 0; CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask; CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit; CAN_FilterInitStructure.CAN_FilterIdHigh = (0x12345670UL << 3) >> 16; CAN_FilterInitStructure.CAN_FilterIdLow = 0x12345670UL << 3 | 4; CAN_FilterInitStructure.CAN_FilterMaskIdHigh = (0xFFFFFFF0UL << 3) >> 16; CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0xFFFFFFF0UL << 3 | 4; CAN_FilterInitStructure.CAN_FilterFIFOAssignment = 0; CAN_FilterInitStructure.CAN_FilterActivation = ENABLE; CAN_FilterInit(&CAN_FilterInitStructure); // 0x12345678 & 0x11223344 CAN_FilterInitTypeDef CAN_FilterInitStructure; CAN_FilterInitStructure.CAN_FilterNumber = 0; CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdList; CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit; CAN_FilterInitStructure.CAN_FilterIdHigh = (0x12345678UL << 3) >> 16; CAN_FilterInitStructure.CAN_FilterIdLow = 0x12345678UL << 3 | 4; CAN_FilterInitStructure.CAN_FilterMaskIdHigh = (0x11223344L << 3) >> 16; CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x11223344UL << 3 | 4; CAN_FilterInitStructure.CAN_FilterFIFOAssignment = 0; CAN_FilterInitStructure.CAN_FilterActivation = ENABLE; CAN_FilterInit(&CAN_FilterInitStructure); // 0x100 ... 0x11F (Mask: 0xFE0), 0x200 nur RTR; CAN_FilterInitTypeDef CAN_FilterInitStructure; CAN_FilterInitStructure.CAN_FilterNumber = 0; CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask; CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_16bit; CAN_FilterInitStructure.CAN_FilterIdHigh = 0x100 << 5; CAN_FilterInitStructure.CAN_FilterIdLow = 0x200 << 5 | 0x10; CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0xFE0 << 5; CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0xFFF << 5 | 0x10; CAN_FilterInitStructure.CAN_FilterFIFOAssignment = 0; CAN_FilterInitStructure.CAN_FilterActivation = ENABLE; CAN_FilterInit(&CAN_FilterInitStructure); // 0x001, 0x011, 0x200 nur RTR, 0x333 CAN_FilterInitTypeDef CAN_FilterInitStructure; CAN_FilterInitStructure.CAN_FilterNumber = 0; CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdList; CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_16bit; CAN_FilterInitStructure.CAN_FilterIdHigh = 0x001 << 5; CAN_FilterInitStructure.CAN_FilterIdLow = 0x011 << 5; CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x200 << 5 | 0x10; CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x333 << 5; CAN_FilterInitStructure.CAN_FilterFIFOAssignment = 0; CAN_FilterInitStructure.CAN_FilterActivation = ENABLE; CAN_FilterInit(&CAN_FilterInitStructure);
// 12 SPI
// 12.1 Konfiguration
..\main.c
STM32F103RB
GPIO_InitTypeDef GPIO_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; SPI_InitTypeDef SPI_InitStructure; SystemInit(); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO | RCC_APB2Periph_SPI1, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_WriteBit(GPIOA, GPIO_Pin_8, SET); NVIC_InitStructure.NVIC_IRQChannel = SPI1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_32; SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; SPI_InitStructure.SPI_CRCPolynomial = 0; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_Init(SPI1, &SPI_InitStructure); SPI_I2S_ITConfig(SPI1, SPI_I2S_IT_RXNE, ENABLE); SPI_Cmd(SPI1, ENABLE);
Pin-Konfiguration: PA8 = Chip Select, PA7 = MOSI, PA6 = MISO, PA5 = SCK
SPI_InitTypeDef:
- SPI_BaudRatePrescaler
Teiler für das Clock-Signal aus APB2. Mögliche Werte: SPI_BaudRatePrescaler_x mit x = 2, 4, 8, 16, 32, 64, 128, 256 - SPI_CPHA
Flanke der Clock-Leitung an der die Daten übernommen werden sollen. Mögliche Werte: SPI_CPHA_1Edge (CPHA=0), SPI_CPHA_2Edge (CPHA=1) - SPI_CPOL
Spannungslevel der Clock-Leitung im Ruhezustand. Mögliche Werte: SPI_CPOL_Low (CPOL=0), SPI_CPOL_High (CPOL=1) - SPI_CRCPolynomial
Das SPI-Modul kann während dem Datenempfang einen CRC Wert berechnen. Das Polynom dazu kann hier definiert werden. Je nach eingestellter Wortgröße (siehe unten) wird entweder ein CRC-8 oder CRC-16 Wert berechnet. - SPI_DataSize
Größe des in einem Stück gesendeten Datenworts – hieraus ergibt sich die effektive Breite des Daten-Registers, das zum Empfangen und Senden der Daten genutzt wird. Mögliche Werte: SPI_DataSize_8b, SPI_DataSize_16b - SPI_Direction
Wahl zwischen uni- und bidirektionalem Datenmodus. Mögliche Werte: SPI_Direction_1Line_Rx, SPI_Direction_1Line_Tx, SPI_Direction_2Lines_FullDuplex, SPI_Direction_2Lines_RxOnly - SPI_FirstBit
Bit-Reihenfolge. Mögliche Werte: SPI_FirstBit_LSB, SPI_FirstBit_MSB - SPI_Mode
Legt fest, ob sich der STM32 als Master oder Slave verhält. Mögliche Werte: SPI_Mode_Master, SPI_Mode_Slave - SPI_NSS
Slave Select Management – Die Chip Select Funktion des SPI-Moduls kann über den Pin NSS direkt gesteuert werden. Diese Funktion ist gut zu gebrauchen, wenn der STM32 als Slave arbeitet oder in einem Multi-Master System eingesetzt wird. Ansonsten kann man jeden beliebigen anderen Pin für das Chip Select Signal verwenden. Mit SPI_NSS_Soft ist der Pin als normaler GPIO-Pin nutzbar. Alternativwert: SPI_NSS_Hard. Die genaue Funktionalität ist dem Reference Manual zu entnehmen.
..\stm32f10x_it.c
STM32F103RB
void SPI1_IRQHandler(void){ spi_handleSPI1Interrupt(); }
// 12.2 Senden & Empfangen (STM32 = Master)
..\spi.h
STM32F103RB
#include "stm32f10x_conf.h" void spi_create(SPI_TypeDef * SPIx, GPIO_TypeDef * CS_GPIOx, uint16_t CS_GPIO_Pin); void spi_handleSPI1Interrupt(void); void spi_writeTwoBytes(uint8_t byte1, uint8_t byte0);
Der Beispielscode soll zunächst nur drei öffentliche Funktionen haben: spi_create(...) legt das SPI-Modul und den Chip Select Pin fest. spi_handleSPI1Interrupt() wird vom Interrupt Handler aufgerufen, falls ein Interrupt aufgetreten ist. spi_writeTwoBytes(...) sendet zwei Bytes und speichert die zwei empfangenen Bytes im Puffer. Für die Rückgabe der empfangenen Daten können bei Bedarf noch weitere Funktionen geschrieben werden.
..\spi.c
STM32F103RB
#include "spi.h" #define BUFFER_SIZE 2 SPI_TypeDef * SPI_Module; GPIO_TypeDef * CS_GPIO; uint16_t CS_GPIO_Pin; uint8_t spiRxCounter; uint8_t spiTxCounter; uint8_t spiBusyFlag; uint8_t spiDataBuffer[BUFFER_SIZE]; void spi_create(SPI_TypeDef * SPIx, GPIO_TypeDef * CS_GPIOx, uint16_t CS_GPIO_Pin_x){ SPI_Module = SPIx; CS_GPIO = CS_GPIOx; CS_GPIO_Pin = CS_GPIO_Pin_x; spiBusyFlag = 0; } void spi_enableTxInterrupt(void){ SPI_I2S_ITConfig(SPI1, SPI_I2S_IT_TXE, ENABLE); } void spi_disableTxInterrupt(void){ SPI_I2S_ITConfig(SPI1, SPI_I2S_IT_TXE, DISABLE); } void spi_chipSelect(void){ GPIO_WriteBit(CS_GPIO, CS_GPIO_Pin, RESET); } void spi_chipDeselect(void){ GPIO_WriteBit(CS_GPIO, CS_GPIO_Pin, SET); } void spi_writeTwoBytes(uint8_t byte1, uint8_t byte0){ while(spiBusyFlag){} spiTxCounter = 2; spiRxCounter = 2; spiBusyFlag = 1; spiDataBuffer[0] = byte0; spiDataBuffer[1] = byte1; spi_chipSelect(); spi_enableTxInterrupt(); } void spi_handleSPI1Interrupt(void){ if(SPI_I2S_GetFlagStatus(SPI_Module, SPI_I2S_FLAG_RXNE) == SET){ // Receive Buffer Not Empty spiRxCounter--; spiDataBuffer[spiRxCounter] = SPI_I2S_ReceiveData(SPI_Module); if(spiRxCounter == 0){ spi_chipDeselect(); spiBusyFlag = 0; } }else if(SPI_I2S_GetFlagStatus(SPI_Module, SPI_I2S_FLAG_TXE) == SET){ // Transmit Buffer Empty if(spiTxCounter != 0){ SPI_I2S_SendData(SPI_Module, spiDataBuffer[spiTxCounter - 1]); spiTxCounter--; }else{ spi_disableTxInterrupt(); } } }
Der Code sollte weitgehend selbsterklärend sein, wenn man das Kapitel zum I2C-Modul verstanden hat, weshalb ich hier nur noch auf ein paar Eigenheiten des SPI-Moduls eingehen werde.
Wenn der Sende-Puffer leer ist wird das TXE-Flag gesetzt und periodisch ein Interrupt generiert. Aus diesem Grund ist das Tx-Interrupt vor dem Sendevorgang deaktiviert und muss nach dem Schreiben des letzten Bytes in den Sende-Puffer deaktiviert werden. Folglich ist auch die Reihenfolge der "if"-"else if"-Abfrage entscheidend. Würde man zuerst das TXE-Flag prüfen, so könnte das letzte empfangene Byte nicht abgeholt werden.
Das Timing zum Abholen der empfangen Bytes ist ebenfalls ein nicht unkritischer Faktor. Aufgrund der Struktur der Puffer im SPI-Modul stehen bereits zwei Bytes zum Senden in der Warteschlange, bevor das erste Byte empfangen wurde. Wird das erste empfangene Byte nicht ausgelesen, bevor das zweite Byte vollständig übertragen wurde, so wird es vom neuen zweiten empfangenen Byte überschrieben. Falls weitere Interrupts im STM32 aktiviert sind, so sollte das SPI-Modul eine sehr hohe Priorität haben, falls nicht sichergestellt werden kann, dass andere Interrupts nicht zu viel Rechenzeit in Anspruch nehmen.
Falls für das Timing-Problem keine Lösung gefunden werden kann, so sollte der DMA-Controller eingesetzt werden. Dieser eignet sich besonders für große Datenmengen sehr gut, da der Prozessor dann nicht mit der Abarbeitung der Interrupts beschäftigt wird. Ich werde darauf in einem späteren Kapitel eingehen.
..\main.c
STM32F103RB
... spi_create(SPI1, GPIOA, GPIO_Pin_8); spi_writeTwoBytes(0x12, 0x34);
// 13 ADC
// 13.1 Einführung
Der folgende Code zeigt ein Minimalbeispiel zum kontinuierlichen Auslesen von analogen Werten. Anschließend werde ich wichtige Verbesserungen und weitere Möglichkeiten des A/D-Konverters beschreiben.
..\main.c
STM32F103RB
GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; SystemInit(); RCC_ADCCLKConfig(RCC_PCLK2_Div6); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC | RCC_APB2Periph_AFIO | RCC_APB2Periph_ADC1, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_NbrOfChannel = 1; ADC_InitStructure.ADC_ScanConvMode = DISABLE; ADC_Init(ADC1, &ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_10, 1, ADC_SampleTime_1Cycles5); ADC_Cmd(ADC1, ENABLE); ADC_SoftwareStartConvCmd(ADC1, ENABLE);
Pin-Konfiguration: Als Eingang dient PC0, der hier als ADC12_IN10 des A/D-Wandlers genutzt wird.
RCC_ADCCLKConfig(...) legt den Vorteiler für den ADC Takt fest. Der ADC Takt wird aus dem Takt von APB2 erzeugt und darf höchstens 14 MHz betragen. Mögliche Werte für den Teiler sind 2, 4, 6 und 8. Hier: 72 MHz / 6 = 12 MHz.
ADC_InitTypeDef:
- ADC_ContinuousConvMode
Falls ENABLE, so wird nach dem Beenden einer Wandlung automatisch die nächste gestartet. - ADC_DataAlign
Mögliche Werte: ADC_DataAlign_Left, ADC_DataAlign_Right – Die 12 Bit Ergebnisse werden entweder links- oder rechtsbündig im 16 Bit Register abgelegt. Bei Injected Group Channels (mehr dazu später) ist zu beachten, dass im höchstwertigen Bit immer ein Vorzeichen gespeichert wird. Der 12 Bit Wert im linksbündig ausgerichteten Fall ist also um 1 Bit nach rechts verschoben. (siehe Reference Manual) - ADC_ExternalTrigConv
Gibt an, welches Externe Signal den Start einer Wandlung auslösen soll. - ADC_Mode
Falls ADC1 und ADC2 gleichzeitig genutzt werden, so kann mit diesem Element grob gesagt deren Interaktion bzgl. des Triggerverhaltens definiert werden. Beispielsweise können die zwei Module auf das selbe Trigger-Signal gleichzeitig reagieren und sind somit synchron. Die etlichen verschiedenen Möglichkeiten sind im Reference Manual sehr schön grafisch dargestellt. - ADC_NbrOfChannel
Anzahl der Kanäle in der Regular Channel Group (siehe unten) - ADC_ScanConvMode
Falls ENABLE, so werden mehrere Kanäle im "Scan-Modus" gewandelt.(siehe unten)
ADC_RegularChannelConfig(...) wählt Kanal 10 (PC0) aus, ADC_Cmd(...) aktiviert den Wandler ADC1 und ADC_SoftwareStartConvCmd(...) triggert die erste Wandlung.
Mit der Funktion ADC_GetConversionValue(...) wird der aktuelle Wert der letzten Wandlung abgerufen.
..\main.c
STM32F103RB
uint16_t value = ADC_GetConversionValue(ADC1);
Der obige Code kann z.B. genutzt werden um in einer Schleife periodisch den Wert eines Potis zu bestimmen, er ist jedoch eher ungünstig, wenn alle einzelnen Messwerte verarbeitet werden müssen. Auch falls man mehrere Werte mitteln möchte gibt es bessere Lösungen.
// 13.2 KalibrierungDie ADC-Wandler der STM32 haben eine Funktion um sich selbst zu Kalibrieren. Es wird empfohlen diese einmal nach dem Einschalten des Wandlers auszuführen um so deren Genauigkeit zu steigern.
..\main.c
STM32F103RB
... ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); ADC_SoftwareStartConvCmd(ADC1, ENABLE); ...
Nach dem Einschalten (mit oder ohne Kalibrierung) braucht der ADC eine Stabilisierungszeit, bis er genaue Ergebnisse liefert. Die Dauer von tSTAB kann im Datenblatt nachgelesen werden. Sie beträgt beispielsweise beim STM32F103 maximal 1 µs.
// 13.3 Regular & Injected Group
Die ADCs der STM32 besitzen jeweils einen Multiplexer um verschiedene analoge Eingänge nutzen zu können.
Die Umschaltung von einem Kanal zum nächsten kann automatisiert erfolgen, so dass die Erfassung mehrerer analoger Signale sehr komfortabel wird. Dazu werden verschiedene Kanäle in einer Gruppe organisiert, die bei aktiviertem Scan-Mode abgearbeitet wird.
Neben der Regular Group, die aus bis zu 16 Kanälen bestehen kann gibt es die Injected Group mit maximal 4 Kanälen. Wird die Messung der Injected Group Channels getriggert, während gerade die Kanäle der Regular Group abgetastet werden, so unterbricht der ADC die aktuelle Wandlung. Es werden anschließend alle Kanäle der Injected Group erfasst, bevor mit der Wandlung des zuletzt unterbrochenen Kanals neu begonnen wird.
Im folgenden Programm wird der ADC zunächst für zwei Kanäle konfiguriert. Ebenfalls muss der Scan Mode aktiviert werden. Im Scan Mode können die Daten nicht mehr über Interrupts sicher ausgelesen werden, da die einzelnen Kanäle in schneller Folge gewandelt werden. Falls die Verzögerung bis zur Abholung durch die Routinen eines Interrupts zu lange ist, werden alte Daten überschrieben. Die Lösung besteht in der Nutzung des Direct Memory Access Controllers (DMA), den ich allerdings erst im nachfolgenden Kapitel genauer beschreiben werde.
..\main.c
STM32F103RB
... ADC_InitStructure.ADC_NbrOfChannel = 2; ADC_InitStructure.ADC_ScanConvMode = ENABLE; ...
Die einzelnen analogen Kanäle werden mit ADC_RegularChannelConfig(...) der Regular Group hinzugefügt. Im zweiten Parameter wird der ADC Kanal angegeben, im dritten Parameter die Folgenummer von 1 bis 16 in der die Kanäle durchlaufen werden und im vierten Parameter die Sample Time.
Mögliche Werte der Sample Time in Cycles: 1.5, 7.5, 13.5, 28.5, 41.5 55.5, 71.5, 239.5
Um die gesamte Zeit für die Wandlung eines Kanals zu berechnen müssen noch 12.5 Cycles dazuaddiert werden.
Die Dauer eins Cycles entspricht der Periodendauer von ADCCLK. Die maximale zulässige Frequenz für ADCCLK ist 14 MHz. Folglich liegt die kürzeste mögliche Zeit zur Wandlung eines Kanals bei (1.5 + 12.5) / 14 MHz = 1 µs.
..\main.c
STM32F103RB
ADC_RegularChannelConfig(ADC1, ADC_Channel_10, 1, ADC_SampleTime_1Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_11, 2, ADC_SampleTime_1Cycles5);
Die Injected Group kann auf ähnliche Weise wie folgt konfiguriert werden. Ihre Länge ist über die Funktion ADC_InjectedSequencerLengthConfig(...) einzustellen.
..\main.c
STM32F103RB
ADC_InjectedSequencerLengthConfig(ADC1, 1); ADC_InjectedChannelConfig(ADC1, ADC_Channel_12, 1, ADC_SampleTime_1Cycles5);
Die Injected Channels bieten zusätzlich die Möglichkeit einen Offset vom Ergebnis der Wandlung abzuziehen. Dadurch kann man vorzeichenbehaftete Werte erhalten, weshalb in den Ergebnisregistern Vorzeichenbits vorgesehen sind. Der Offset kann für jeden Injected Channel getrennt definiert werden.
Für das Ergebnis jedes Injected Channels gibt es ebenfalls separate Register, so dass man hier auf den DMA verzichten kann bzw. sogar muss.
..\main.c
STM32F103RB
ADC_SetInjectedOffset(ADC1, ADC_InjectedChannel_1, 0x0800);
..\main.c
STM32F103RB
uint16_t value = ADC_GetInjectedConversionValue(ADC1, 1);
// 13.4 Triggerung
Die Triggerung zur Wandlung einer Group wird für Regular und Injected Group getrennt festgelegt.
Um per Software triggern zu können, müssen die Werte explizit auf ..._None gesetzt werden.
..\main.c
STM32F103RB
... ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ... ADC_ExternalTrigInjectedConvConfig(ADC1, ADC_ExternalTrigInjecConv_None);
Alternativ stehen verschiedene Timer-Events zur Triggerung zur Verfügung, sowie Exti-Line 11 (Regular Group) und Exti-Line 15 (Injected Group) bei ADC1/ADC2. Für ADC3 können nur Timer-Events gewählt werden. Genaueres steht im Reference Manual.
Die Triggerung für nicht-Software Events muss anschließend jeweils noch wie folgt aktiviert werden.
..\main.c
STM32F103RB
ADC_ExternalTrigConvCmd(ADC1, ENABLE); ADC_ExternalTrigInjectedConvCmd(ADC1, ENABLE);
Zur Software Triggerung stehen zwei Funktionen bereit.
..\main.c
STM32F103RB
ADC_SoftwareStartConvCmd(ADC1, ENABLE); ADC_SoftwareStartInjectedConvCmd(ADC1, ENABLE);
Falls die Regular Group kontinuierlich nach einmaliger Triggerung durchlaufen werden soll, so kann man das Element ADC_ContinuousConvMode von ADC_InitTypeDef bei der Initialisierung auf ENABLE setzen. Mittels ADC_AutoInjectedConvCmd(ADC1, ENABLE); wird der ADC so konfiguriert, dass nach dem Abarbeiten der Regular Group sofort die Injected Group getriggert wird. In dieser Kombination können alle bis zu 20 ADC Kanäle nacheinander kontinuierlich erfasst werden.
// 13.5 Discontinuous ModeNach den bisherigen Erkenntnissen wird nach einer Triggerung eine komplette Group (Scan-Mode vorausgesetzt) in einem Stück verarbeitet. Die STM32 bieten jedoch auch eine Möglichkeit sowohl Regular als auch Injected Group in weitere Stücke zu "zerteilen". Im Discontinuous Mode kann man eine Sequenz der Länge 1 bis 8 für die Regular Group (Injected Group Sequenz Länge immer 1) festlegen. Sind beispielsweise 8 Kanäle vorhanden und die Sequenz Länge ist 3, so werden nach dem ersten Trigger Signal die ersten drei Kanäle gewandelt. Danach wartet der ADC auf die nächste Triggerung, bis er mit den folgenden drei Kanälen fortfährt. Nach der dritten Triggerung werden die letzten zwei Kanäle gewandelt – es findet kein Überlauf statt, der erste Kanal in der Group gehört also nicht zur dritten Sequenz.
..\main.c
STM32F103RB
ADC_DiscModeChannelCountConfig(ADC1, 1); ADC_DiscModeCmd(ADC1, ENABLE); ADC_InjectedDiscModeCmd(ADC1, ENABLE);
// 13.6 Interrupts
Der ADC Interrupt kann durch drei verschiedene Events ausgelöst werden, die getrennt voneinander aktiviert werden können:
- Wandlung der Regular Group abgeschlossen
- Wandlung der Injected Group abgeschlossen
- Statusbit des Analog Watchdog gesetzt
Wichtig: Die Interrupts von ADC1 und ADC2 sind dem selben Interrupt Vektor zugeordnet. ADC3, falls vorhanden, hat seinen eigenen Interrupt Vektor.
..\main.c
STM32F103RB
ADC_ITConfig(ADC1, ADC_IT_EOC, ENABLE); ADC_ITConfig(ADC1, ADC_IT_JEOC, ENABLE); ADC_ITConfig(ADC1, ADC_IT_AWD, ENABLE);
..\stm32f10x_it.c
STM32F103RB
void ADC1_2_IRQHandler(void){ }
// 13.7 Analog Watchdog
Für jedes ADC Modul kann eine obere und untere Schranke eingestellt werden. Erreicht einer der ausgewählten Eingänge einen Wert außerhalb dieser Schranken, so wird das AWD Flag gesetzt und ein Interrupt ausgelöst, falls aktiviert. Das Flag muss per Software wieder rückgesetzt werden.
Als Eingang kann entweder mit ADC_AnalogWatchdogSingleChannelConfig(...) ein bestimmter Kanal gewählt werden (Regular/Injected/Beide) oder alle Eingänge (Regular/Injected/Beide) überwacht werden.
..\main.c
STM32F103RB
ADC_AnalogWatchdogThresholdsConfig(ADC1, 0x0CCC, 0x0333); ADC_AnalogWatchdogCmd(ADC1, ADC_AnalogWatchdog_SingleRegEnable); ADC_AnalogWatchdogSingleChannelConfig(ADC1, ADC_Channel_10);
// 13.8 Dual ADC Mode
Im Dual ADC Mode reagieren ADC1 und ADC2 auf das selbe Triggersignal.
Für ADC2 ist keine Möglichkeit vorgesehen dessen gewandelte Daten (regular Channels) per DMA direkt abzuholen. Benutzt man jedoch den Dual ADC Mode, so werden die Messergebnisse von ADC2 in den oberen 16 Bit des 32 Bit Datenregisters von ADC1 gespeichert. Dieses kann anschließend über den DMA-Controller erreicht werden.
Im folgenden Beispiel sollen die Regular Groups von ADC1 und ADC2 simultan erfasst werden. Die Triggerung erfolgt per Software.
Im Reference Manual wird darauf hingewiesen, dass der selbe Channel nicht gleichzeitig von ADC1 und ADC2 gesampelt werden soll. Ebenfalls sollten die Regular Groups von ADC1 und ADC2 gleich lang sein oder das Triggerinterval groß genug sein, da ansonsten der ADC mit der kürzeren Sequenz erneut getriggert werden könnte, bevor der andere ADC fertig ist.
..\main.c
STM32F103RB
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_Mode = ADC_Mode_RegSimult; ADC_InitStructure.ADC_NbrOfChannel = 1; ADC_InitStructure.ADC_ScanConvMode = DISABLE; ADC_Init(ADC1, &ADC_InitStructure); ADC_Init(ADC2, &ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_10, 1, ADC_SampleTime_1Cycles5); ADC_RegularChannelConfig(ADC2, ADC_Channel_11, 1, ADC_SampleTime_1Cycles5); ADC_ExternalTrigConvCmd(ADC1, ENABLE); ADC_ExternalTrigConvCmd(ADC2, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_Cmd(ADC2, ENABLE); ... // Kalibrierung ADC_SoftwareStartConvCmd(ADC1, ENABLE);
Die Initialisierung ist zunächst die Gleiche wie bei nur einem unabhängigen ADC, mit dem Unterschied, dass ADC_Mode den Wert ADC_Mode_RegSimult zugewiesen bekommt. ADC2 wird mit den selben Werten initialisiert.
Möchte man externe Triggerung benutzen, so muss diese für ADC1 eingestellt werden und ADC2 muss auf ADC_ExternalTrigConv_None konfiguriert werden.
Auch wenn die ADCs per Software getriggert werden, so muss in jedem Fall die externe Triggerung für beide ADCs aktiviert werden.
Der Kalibrierungsvorgang muss logischerweise für beide ADC separat ausgeführt werden.
..\main.c
STM32F103RB
uint32_t value = ADC_GetConversionValue(ADC1); uint16_t valueADC1 = value; uint16_t valueADC2 = value >> 16;
Exemplarisches Auslesen: In der Praxis ist DMA zu bevorzugen.
Neben dem Regular Simultaneous Mode stehen fünf Weitere zur Auswahl, die sich teils auf die Injected Channel Group beziehen und auch kombiniert werden können. Weitere Informationen gibt es im Reference Manual. Eine ausführlichere Beschreibung der ADC Modes findet man in der Application Note AN3116.
// 14 DMA
Der Direct Memory Access Controller ermöglicht es Daten zwischen Peripherie und Speicher zu übertragen, ohne dabei die CPU direkt zu belasten.
Das nachfolgende Beispiel greift nochmals den Code aus Kapitel 13.1 auf. Er wird um einen Channel erweitert und über DMA werden die gesampelten Daten automatisch in ein Array kopiert.
..\main.c
STM32F103RB
GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; DMA_InitTypeDef DMA_InitStructure; uint16_t ADCBuffer[] = {0xAAAA, 0xAAAA, 0xAAAA}; SystemInit(); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_InitStructure.DMA_BufferSize = 2; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)ADCBuffer; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_Init(DMA1_Channel1, &DMA_InitStructure); DMA_Cmd(DMA1_Channel1, ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC | RCC_APB2Periph_AFIO | RCC_APB2Periph_ADC1, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_NbrOfChannel = 2; ADC_InitStructure.ADC_ScanConvMode = ENABLE; ADC_Init(ADC1, &ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_10, 1, ADC_SampleTime_1Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_11, 2, ADC_SampleTime_1Cycles5); ADC_Cmd(ADC1, ENABLE); ADC_DMACmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); ADC_SoftwareStartConvCmd(ADC1, ENABLE);
Zu Testzwecken wird das Array ADCBuffer um ein Element zu groß gewählt und mit einer definierten Bitfolge initialisiert. Dadurch kann man prüfen, ob wirklich neue Daten geschrieben worden sind und der DMA Controller nicht zu weit im Speicher schreibt.
DMA_InitTypeDef:
- DMA_BufferSize
Anzahl der zu übertragenden Daten. Wertebereich: 0...65535. Die Daten werden nicht gepuffert, hier ist der Bezeichner etwas unglücklich gewählt. - DMA_DIR
Mögliche Werte: DMA_DIR_PeripheralDST - Peripherie ist Ziel der Übertragung; DMA_DIR_PeripheralSRC - Peripherie ist Quelle der Übertragung - DMA_M2M
Gibt an, ob die Transfers von Speicher zu Speicher stattfinden werden. In diesem Fall erfolgt die "Triggerung" zwischen zwei Datenwörtern automatisch. Mögliche Werte: DMA_M2M_Disable, DMA_M2M_Enable - DMA_MemoryBaseAddr
32-Bit Basis Adresse im Speicher, also die Adresse der Speicherzelle, in die das erste Byte geschrieben wird. - DMA_MemoryDataSize
Größe eines "Wortes" auf der Seite des Speichers. Mögliche Werte: DMA_MemoryDataSize_Byte (8 Bit), DMA_MemoryDataSize_HalfWord (16 Bit), DMA_MemoryDataSize_Word (32 Bit) - DMA_MemoryInc
Falls DMA_MemoryInc_Enable wird der Speicher-Adresszeiger automatisch nach jeder Übertragung um die zuvor festgelegte Wortbreite erhöht. Alternativwert: DMA_MemoryInc_Disable - DMA_Mode
Falls DMA_Mode_Circular, so startet der DMA-Controller erneut von der Basis Adresse, wenn sein interner Datenzähler den Wert 0 erreicht. Der Datenzähler wird dabei wieder auf die zuvor eingestellte "BufferSize" gesetzt.
Alternativ: DMA_Mode_Normal - Es finden keine weiteren Transfers statt, nachdem die Anzahl in "BufferSize" an Werten übertragen wurde. Im Memory-to-Memory Mode ist dies der einzig mögliche Wert. - DMA_PeripheralBaseAddr
32-Bit Basis Adresse der Peripherie. - DMA_PeripheralDataSize
Mögliche Werte: DMA_PeripheralDataSize_Byte, DMA_PeripheralDataSize_HalfWord, DMA_PeripheralDataSize_Word - DMA_PeripheralInc
Mögliche Werte: DMA_PeripheralInc_Disable, DMA_PeripheralInc_Enable - DMA_Priority
Die DMA Requests mit der höheren Priorität werden zuerst abgearbeitet. Es kann zwischen vier Werten gewählt werden: DMA_Priority_Low, DMA_Priority_Medium, DMA_Priority_High, DMA_Priority_VeryHigh
Mit DMA_Init(...) wird der entsprechende DMA Channel initialisiert. Welche Hardware mit welchem Channel verbunden ist, kann man dem Reference Manual entnehmen. Dabei ist zu beachten, dass es für jeden Channel mehrere mögliche Quellen gibt, die aber niemals gleichzeitig genutzt werden dürfen.
Über ADC_DMACmd(ADC1, ENABLE) wird dem ADC Modul mitgeteilt, dass es DMA Requests an den DMA Controller senden soll, wenn es mit einer Wandlung fertig ist.
Hinweise:
In bestimmten STM32 gibt es zwei DMA Controller (DMA1, DMA2).
Der DMA Controller kann die CPU anhalten, falls beide das selbe Ziel (Speicher/Peripherie) gleichzeitig ansprechen wollen. Dabei wird jedoch über Round-Robin Scheduling gewährleistet, dass mindestens die halbe Bandbreite für die CPU verfübar bleibt.
// 15 Häufige Fehler
Im Folgenden werde ich einige Fehler auflisten, die einem des öfteren unterlaufen und zu eigentlich unnötiger Sucherei führen können:
- Taktung vergessen - z.B. RCC_APB2PeriphClockCmd(...)
- Aktivieren der Hardware vergessen - z.B. DMA_Cmd(...)
- Neben dem aktivieren eines Interrupts an der Perpherie vergessen den NVIC passend dazu einzustellen.
// 16 SPI mit DMA
Dieser Code zeigt, wie man das SPI Modul über Direct Memory Access mit Daten versorgt und dadurch den Prozessor entlastet.
..\main.c
STM32F103RB
GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; DMA_InitTypeDef DMA_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; uint16_t SPIBuffer[] = {0xAAAA, 0xAAAA, 0xAAAA}; SystemInit(); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_15; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_WriteBit(GPIOB, GPIO_Pin_12, SET); SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_32; SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; SPI_InitStructure.SPI_CRCPolynomial = 0; SPI_InitStructure.SPI_DataSize = SPI_DataSize_16b; SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_Init(SPI2, &SPI_InitStructure); SPI_I2S_DMACmd(SPI2, SPI_I2S_DMAReq_Tx, ENABLE); SPI_I2S_DMACmd(SPI2, SPI_I2S_DMAReq_Rx, ENABLE); SPI_Cmd(SPI2, ENABLE); // DMA Channel 4 - SPI RX DMA_InitStructure.DMA_BufferSize = 0; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)SPIBuffer; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&SPI2->DR; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_Init(DMA1_Channel4, &DMA_InitStructure); DMA_ITConfig(DMA1_Channel4, DMA_IT_TC, ENABLE); // DMA Channel 5 - SPI TX DMA_InitStructure.DMA_BufferSize = 0; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)SPIBuffer; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&SPI2->DR; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_Init(DMA1_Channel5, &DMA_InitStructure); NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel4_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); DMA_Cmd(DMA1_Channel4, DISABLE); DMA_Cmd(DMA1_Channel5, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel4, 2); DMA_SetCurrDataCounter(DMA1_Channel5, 2); SPIBuffer[0] = 0x1234; SPIBuffer[1] = 0x5678; // Chip Select Low GPIO_WriteBit(GPIOB, GPIO_Pin_12, RESET); DMA_Cmd(DMA1_Channel4, ENABLE); DMA_Cmd(DMA1_Channel5, ENABLE);
Pin-Konfiguration: PB12 = Chip Select, PB15 = MOSI, PB14 = MISO, PB13 = SCK
..\stm32f10x_it.c
STM32F103RB
void DMA1_Channel4_IRQHandler(void){ spi_handleDMA1Ch4Interrupt(); DMA_ClearFlag(DMA1_FLAG_TC4); }
..\main.c
STM32F103RB
void spi_handleDMA1Ch4Interrupt(void){ // Chip Select High GPIO_WriteBit(GPIOB, GPIO_Pin_12, SET); }
Die Initialisierung der GPIO-Pins und des SPI Moduls kann aus Kapitel 12 übernommen werden. Einziger Unterschied ist, dass keine Interrupts mehr aktiviert werden, sondern DMA Requests mittels SPI_I2S_DMACmd(...).
Die Beschreibung der einzelnen Elemente von DMA_InitStructure kann Kapitel 14 entnommen werden. Für SPI werden die beiden Channels 4 und 5 genutzt. Möchte man nur senden, oder nur empfangen, so kann ein Channel eingespart werden.
Über DMA_DIR wird die Datenrichtung eingestellt: Channel 4 vom SPI zum Speicher; Channel 5 vom Speicher zum SPI.
DMA_Mode muss auf DMA_Mode_Normal gesetzt werden. Im Unterschied zum ADC aus Kapitel 14 möchte man hier nicht kontinuierlich Daten übertragen, sondern nur, wenn noch Datenframes erwartet werden.
Für Channel 4 werden der Transfer-Complete-Interrrupt eingeschaltet um damit später die Umschaltung des Chip Select Pins vorzunehmen.
Mit dem letzte Codeblock kann ein Transfer gestartet werden. In der Praxis wird man diesen Abschnitt besser in eine Funktion verpacken. Zunächst müssen die DMA-Channels deaktiviert werden und über DMA_SetCurrDataCounter(...) wird mit dem zweiten Parameter die Anzahl der Wörter eingestellt, die übertragen werden sollen. Anschließend wird die Chip Select Leitung auf Low gezogen und durch das Aktivieren der DMA-Channels der Transfer gestartet. Sicherheitshalber sollte man Channel 4 (RX) vor Channel 5 (TX) einschalten. Tritt zwischen den zwei Funktionsaufrufen ein Interrupt auf, der den Programmfluss für längere Zeit unterbricht, so würde andernfalls das SPI Modul mit dem Senden beginnen, Channel 5 wäre aber noch nicht oder zu spät für das Entgegennehmen der empfangenen Daten bereit.
Der Interrupt von Channel 4 (RX) wird ausgelöst, sobald das letzte Byte empfangen wurde, der gesamte SPI Transfer also abgeschlossen ist. Jetzt kann die Chip Select Leitung wieder auf high gelegt werden. Channel 5 (TX) sollte in keinem Fall für diesen Zweck genutzt werden, weil dieser zu früh mit der Arbeit fertig ist. Falls man Channel 4 für andere Zwecke als SPI nutzen muss, so kann man beispielsweise mittels des SPI-Interrupt und einem Byte-Zähler die entsprechende Funktionalität implementieren.
// 17 Independent Watchdog (IWDG)
Bei den STM32 sind zwei verschiedene Watchdogs vorhanden. Hier geht es zunächst um den Independent Watchdog. Er wird vom internen 40 kHz RC Oszillator (LSI) getaktet. Der Takt ist relativ ungenau (30kHz ... 60kHz), jedoch eignet sich der IWDG gut um unabhängig vom Hauptprogramm zu laufen und dieses im Fehlerfall zu resetten.
..\main.c
STM32F100RB
IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable); IWDG_SetPrescaler(IWDG_Prescaler_16); IWDG_SetReload(2500); IWDG_ReloadCounter(); IWDG_Enable();
Über den Befehl IWDG_WriteAccessCmd(...) wird der Schreibzugriff auf die Register für Prescale und Reload Value aktiviert. Die Schreibrechte bleiben erhalten, bis entweder IWDG_ReloadCounter() oder IWDG_Enable() aufgerufen wird.
Über IWDG_SetPrescaler(...) kann der 40 kHz Takt um den Faktor 4, 8, 16, ..., 256 heruntergeteilt werden.
Mit IWDG_SetReload(...) wird der Wert eingestellt, ab dem der Watchdog herunterzählt. Erreicht er den Wert 0 erfolgt ein Reset. Möglicher Wertebereich: 0...4095.
IWDG_ReloadCounter() setzt den Watchdog wieder auf seinen eingestellten Maximalwert. Die Funktion muss periodisch im Hauptprogramm aufgerufen werden um im Normalbetrieb einen Reset zu verhindern.
IWDG_Enable() aktiviert den Watchdog. Über die Option Bytes kann der IWDG auch dauerhaft eingeschaltet werden.
Hinweis: Falls im Programm die Prescale/Reload Werte dynamisch geändert werden, so müssen zwischen zwei Wertänderungen mindestens 5 RC Cycles abgewartet werden (der IWDG muss dazu aktiviert sein), andernfalls werden die neuen Werte nicht übernommen. Um zu prüfen, ob der IWDG bereit ist neue Werte entgegenzunehmen, können die Status Bits RVU (reload value update) und PVU (prescaler value update) abgefragt werden. Die Bits müssen 0 sein, so dass ein neues Update erfolgen kann.
..\main.c
STM32F100RB
while(IWDG_GetFlagStatus(IWDG_FLAG_PVU) == SET); IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable); IWDG_SetPrescaler(IWDG_Prescaler_64);
// 18 Option Bytes
Programming Manual PM0075 (STM32F10xxx)
Im Flash der STM32 sind mehrere Option Bytes vorhanden, über die bestimmte Einstellungen festgelegt werden:
-
Read Out Protection
Falls aktiviert, wird das Auslesen des Flash Speichers verhindert. -
Brownout Level
Beim Einschalten der Betriebsspannung wird der Chip solange im Reset-Zustand gehalten, bis der hier eingestellt Spannungswert erreicht ist. Fällt VDD im Betrieb unter diesen Wert, so wird ebenfalls ein Reset ausgelöst. -
Hardware/Software Watchdog
Das Bit WDG_SW ist standardmäßig gesetzt, wodurch der IWDG beim Start nicht automatisch aktiviert ist, sondern erst per Software eingeschaltet werden muss. Ist das Bit gelöscht, so nimmt der IWDG direkt nach dem Einschalten/Reset seine Arbeit auf. -
Reset Generation bei Standby/Stop Mode
Falls nRST_STDBY/nRST_STOP nicht gesetzt ist, so wird ein Reset ausgelöst, falls der Standby/Stop Mode betreten wird. -
User Data Bytes
Zwei Bytes für beliebige Daten wie z.B. Seriennummern. -
Write Protection
Die einzelnen Pages des Flash können vor Schreibzugriffen geschützt werden. Damit kann beispielsweise verhindert werden, dass sich Programme selbst löschen, die schreibend auf den Flash zugreifen.
Die Option Bytes können mit CoFlash derzeit nicht direkt beschrieben werden. Falls man im Besitz eines ST-Link ist, so kann zum Ändern der Parameter das Tool "STM32 ST-LINK utility" benutzt werden:
Je nach eingesetztem Mikrocontroller sind einige der obigen Optionen nicht vorhanden oder es gibt zusätzliche Einstellmöglichkeiten. Genaueres ist UM0892 (STM32 ST-LINK Utility software description) zu entnehmen.
// 19 System Timer
Hinweis: Die Informationen zum System Timer stehen nicht im Reference sondern im Programming Manual (PM0056 für Cortex-M3).
Der System Timer ist fester Bestandteil des Cortex-M3-Kerns und ist für den Systemtakt eines RTOS oder um einfach mehrere Tasks nacheinander anzustoßen vorgesehen. Sein Zählregister ist 24-Bit breit.
Im nachfolgenden Beispiel wird über den System Timer eine LED an PC8 mit 1Hz betrieben. Der Systemtakt beträgt 24 MHz.
..\main.c
STM32F100RB
int main(void) { GPIO_InitTypeDef GPIO_InitStructure; SystemInit(); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); SysTick_Config(1499999); SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8); NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); NVIC_SetPriority(SysTick_IRQn, 14); while (1) {} } void SysTick_Handler(void) { if (GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_8)) { GPIO_WriteBit(GPIOC, GPIO_Pin_8, Bit_RESET); } else { GPIO_WriteBit(GPIOC, GPIO_Pin_8, Bit_SET); } }
SysTick_Config(...) stellt den Wert ein, ab dem der Timer herunterzählt. Maximalwert: 0xFFFFFF. Zusätzlich aktiviert die Funktion den System Timer sowie seinen Exception Handler. Sie ist Bestandteil des CMSIS.
Der System Timer wird entweder direkt vom AHB mit seinem Takt versorgt oder es wird ein Vorteiler mit dem Wert 8 zwischengeschaltet. Der Vorteiler kann über die Funktion SysTick_CLKSourceConfig(...) aktiviert/deaktiviert werden. Die Funktion muss unbedingt nach SysTick_Config(...) aufgerufen werden.
Standardmäßig hat die System Timer Exception die niedrigste Priorität 15 und wird hier exemplarisch auf 14 gesetzt. Dazu wird die Funktion NVIC_SetPriority(...) aufgerufen, die ebenfalls zum CMSIS gehört. Auch hier muss darauf geachtet werden, dass die Funktion erst nach SysTick_Config(...) aufgerufen wird.
// 20 RS485
Als RS485 Transceiver wird der SN65HVD1781 mit der folgenden Beschaltung genutzt.
PA0: CPU_RS485_DE
PA2: CPU_RS485_D
PA3: CPU_RS485_R
..\main.c
STM32F103RB
int main(void) { SystemInit(); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; /* DE Pin */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); /* TX Pin */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); /* RX Pin */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); NVIC_InitTypeDef NVIC_InitStructure; /* DMA Channel 6 (USART RX) */ NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel6_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); /* USART2 (TX)*/ NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); uint8_t usart_receive_array[] = {0xAA, 0xAA, 0xAA}; uint8_t usart_transmit_array[] = {0x12, 0x34, 0x56}; DMA_InitTypeDef DMA_InitStructure; /* DMA 1, Channel 7 for USART2 TX */ DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(USART2->DR); DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t) &usart_transmit_array; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize = 0; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel7, &DMA_InitStructure); /* DMA 1, Channel 6 for USART2 RX */ DMA_DeInit(DMA1_Channel6); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(USART2->DR); DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t) &usart_receive_array; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize = 0; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel6, &DMA_InitStructure); DMA_ITConfig(DMA1_Channel6, DMA_IT_TC, ENABLE); /* USART */ USART_InitTypeDef USART_InitStructure; USART_InitStructure.USART_BaudRate = 57600; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART2, &USART_InitStructure); USART_DMACmd(USART2, USART_DMAReq_Tx, ENABLE); USART_DMACmd(USART2, USART_DMAReq_Rx, ENABLE); USART_ITConfig(USART2, USART_IT_TC, ENABLE); USART_Cmd(USART2, ENABLE); /* start transmission */ DMA_Cmd(DMA1_Channel6, DISABLE); DMA_Cmd(DMA1_Channel7, DISABLE); GPIO_WriteBit(GPIOA, GPIO_Pin_0, SET); // DE Pin high DMA_SetCurrDataCounter(DMA1_Channel6, 2); DMA_SetCurrDataCounter(DMA1_Channel7, 3); DMA_ClearFlag(DMA1_FLAG_TC6); DMA_ClearFlag(DMA1_FLAG_TC7); DMA_Cmd(DMA1_Channel6, ENABLE); DMA_Cmd(DMA1_Channel7, ENABLE); while(1){} } void USART2_IRQHandler(void){ USART_ClearITPendingBit(USART2, USART_IT_TC); GPIO_WriteBit(GPIOA, GPIO_Pin_0, RESET); // DE Pin low } void DMA1_Channel6_IRQHandler(void){ /* all data received */ DMA_ClearFlag(DMA1_FLAG_TC6); }
USART_InitTypeDef:
- USART_BaudRate
Hier wird direkt die gewünschte Baudrate angegeben. Zu beachten ist, dass in der Berechnung der Registerwerte in USART_Init(...) vom Wert HSE_VALUE bzw. HSI_VALUE aus stm32f10x.h ausgegangen wird. Benutzt man z.B. einen Quarz mit einer Frequenz ungleich 8MHz als Taktversorgung, so muss HSE_VALUE angepasst werden. - USART_WordLength
Hier kann für die Wortbreite zwischen 8 und 9 Bit gewählt werden. - USART_StopBits
Hier kann die "Anzahl" der Stop Bits festgelegt werden:
1 und 2 für USART etc.
0,5 und 1,5 für Smartcard Mode - USART_Parity
Einstellung für das Parity-Bit: USART_Parity_No, USART_Parity_Even oder USART_Parity_Odd - USART_HardwareFlowControl
Für Datenflusssteuerung können die Anschlüsse CTS und RTS mit dieser Option einzeln aktiviert werden. Dieses Feature steht nicht bei allen USART Modulen zu Verfügung. - USART_Mode
Legt fest, ob der Empfangs-, Sendemodus oder beide aktiviert werden.
Mit USART_ITConfig(...) wird der Transmission-Complete Interrupt eingeschaltet. Im Interrupt-Handler wird der DE Pin auf low gezogen um nach dem Senden aller Frames wieder in den Empfangsmodus zu wechseln.
Die Sequenz nach /* start transmission */ packt man am besten in eine Funktion und ruft diese für jede Übertragung mit neuen Parametern auf.
Zunächst müssen beide DMA Channels deaktiviert werden, damit der Transfer nicht unkontrolliert beginnen kann.
Anschließend wird der DE Pin auf high gelegt um den Transceiver-Chip in den Sendemodus zu versetzen.
Mit den folgenden zwei Funktionen wird der Zähler der DMA-Channels eingestellt. DMA Channel 6 ist für den Empfang zuständig, sein Zähler muss also so groß sein, wie die Anzahl der erwarteten Frames, die nach dem Sendevorgang empfangen werden sollen. DMA Channel 7 versorgt das USART Modul entsprechend mit zu sendenden Daten, hier also 3 Bytes in Folge.
Zuletzt werden die Transmission-Complete Flags der DMA Channels gelöscht und anschließend die beiden Channels aktiviert, womit der Übertragungsvorgang gestartet wird.
In diesem Beispiel werden also die drei Bytes 0x12, 0x34 und 0x56 gesendet und anschließend zwei Bytes empfangen, falls das angeschlossene Gerät korrekt antwortet. Die empfangenen Daten liegen danach im Array usart_receive_array und können abgeholt werden, sobald der DMA1_Channel6_IRQHandler aufgerufen wurde.
// 21 DAC
// 21.1 Einführung
Mit dem DAC können analoge Signale generiert werden. Die Referenzen für die Ausgangsspannung sind VSSA und VREF+, wobei der Spannungsbereich von VREF+ auf 2,4 V ... VDDA eingeschränkt ist. Bei Chips in kleinen Gehäuseformen wie dem hier verwendeten STM32F100RB in LQFP64 ist VREF+ nicht nach außen geführt, sondern direkt mit VDDA verbunden.
Dieses Beispiel erzeugt eine Spannung von 0,5 * VREF+ an Pin PA4.
..\main.c
STM32F100RB
GPIO_InitTypeDef GPIO_InitStructure; DAC_InitTypeDef DAC_InitStructure; SystemInit(); RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; GPIO_Init(GPIOA, &GPIO_InitStructure); DAC_InitStructure.DAC_LFSRUnmask_TriangleAmplitude = DAC_LFSRUnmask_Bit0; DAC_InitStructure.DAC_OutputBuffer = DAC_OutputBuffer_Disable; DAC_InitStructure.DAC_Trigger = DAC_Trigger_None; DAC_InitStructure.DAC_WaveGeneration = DAC_WaveGeneration_None; DAC_Init(DAC_Channel_1, &DAC_InitStructure); DAC_SetChannel1Data(DAC_Align_12b_R, 0x7FF); DAC_Cmd(DAC_Channel_1, ENABLE);
Nachdem der DAC Channel aktiviert wurde ist der zugehörige Pin automatisch mit dem DAC verbunden. Trotzdem wird empfohlen den Pin als analogen Eingang zu konfigurieren um unerwünschten parasitären Stromverbrauch zu verhindern.
DAC_InitTypeDef:
- DAC_LFSRUnmask_TriangleAmplitude
Der DAC kann in einen Modus versetzt werden, in dem er Rauschen oder ein Dreieckssignal erzeugt. Hier kann die "LFSR Maske" für den Rauschgenerator bzw. die Amplitude des Dreieckssignals definiert werden. Wird in diesem Beispiel nicht genutzt. - DAC_OutputBuffer
Über diese Option kann dem DAC ein Puffer nachgeschaltet werden um die Ausgangsimpedanz auf max. 15 kΩ zur verringern. Der Nachteil ist, dass der Ausgangsbereich auf 0,2 V ... VDDA - 0,2 V eingeschränkt wird. Ohne Ausgangspuffer darf die resistive Last am Ausgang 1,5 MΩ nicht unterschreiten, um noch eine Genauigkeit von 1% zu erreichen. - DAC_Trigger
Der DAC arbeitet mit einem Schattenregister, das die Werte aus dem Datenregister wahlweise erst nach einem Triggerimpuls übernimmt. Als Trigger können verschiedene Timer, EXTI Line 9, Software oder keiner ausgewählt werden. In letzterem Fall werden die Werte sofort übernommen. - DAC_WaveGeneration
Mögliche Werte: DAC_WaveGeneration_Noise (Rauschgenerator), DAC_WaveGeneration_Triangle (Dreiecksgenerator), DAC_WaveGeneration_None ("normaler" Modus)
Mit DAC_SetChannel1Data(...) kann ein Wert ins Datenregister von Channel 1 des DAC geschrieben werden. Über den ersten Parameter wird festgelegt, ob der 8-Bit Modus oder der 12-Bit Modus (links-/rechtsbündig) verwendet wird.
Über DAC_Cmd(...) wird der DAC Channel 1 aktiviert.
// 21.2 Dual DAC Channel Conversion
Im Dual DAC Channel Mode können beide Kanäle über ein gemeinsames 32-Bit Register mit Daten versorgt werden. Der wirkliche Nutzen wird erst später in Zusammenhang mit DMA ersichtliche, da über diese Funktion ein DMA-Channel gespart werden kann. Auch in diesem Modus können beide DAC Channels unabhängig voneinander getriggert werden. Die verschiedenen Möglichkeiten sind im Reference Manual aufgeführt.
..\main.c
STM32F100RB
GPIO_InitTypeDef GPIO_InitStructure; DAC_InitTypeDef DAC_InitStructure; SystemInit(); RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5; GPIO_Init(GPIOA, &GPIO_InitStructure); DAC_InitStructure.DAC_LFSRUnmask_TriangleAmplitude = DAC_LFSRUnmask_Bit0; DAC_InitStructure.DAC_OutputBuffer = DAC_OutputBuffer_Disable; DAC_InitStructure.DAC_Trigger = DAC_Trigger_None; DAC_InitStructure.DAC_WaveGeneration = DAC_WaveGeneration_None; DAC_Init(DAC_Channel_1, &DAC_InitStructure); DAC_Init(DAC_Channel_2, &DAC_InitStructure); DAC_SetDualChannelData(DAC_Align_12b_R, 0x7FF, 0x3FF); DAC_Cmd(DAC_Channel_1, ENABLE); DAC_Cmd(DAC_Channel_2, ENABLE);
// 21.3 DMA
In diesem Beispiel wird an PA5 ein Sinus und an PA4 ein Kosinus mit jeweils 10kHz generiert. Alle Datentransfers werden nach der Initialisierung über DMA ausgeführt.
..\main.c
STM32F100RB
const uint16_t sinTable[32] = { 2047, 2447, 2831, 3185, 3498, 3750, 3939, 4056, 4095, 4056, 3939, 3750, 3495, 3185, 2831, 2447, 2047, 1647, 1263, 909, 599, 344, 155, 38, 0, 38, 155, 344, 599, 909, 1263, 1647}; uint32_t sinCosTable[32]; int main(void) { GPIO_InitTypeDef GPIO_InitStructure; DAC_InitTypeDef DAC_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; DMA_InitTypeDef DMA_InitStructure; SystemInit(); for(uint8_t i = 0; i < 32; i++){ sinCosTable[i] = sinTable[i] << 16; } for(uint8_t i = 8; i < 32; i++){ sinCosTable[i - 8] |= sinTable[i]; } for(uint8_t i = 0; i < 8; i++){ sinCosTable[i + 24] |= sinTable[i]; } RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC | RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5; GPIO_Init(GPIOA, &GPIO_InitStructure); TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInitStructure.TIM_Period = 74; TIM_TimeBaseInitStructure.TIM_Prescaler = 0; TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure); TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update); DAC_InitStructure.DAC_LFSRUnmask_TriangleAmplitude = DAC_LFSRUnmask_Bit0; DAC_InitStructure.DAC_OutputBuffer = DAC_OutputBuffer_Disable; DAC_InitStructure.DAC_Trigger = DAC_Trigger_T2_TRGO; DAC_InitStructure.DAC_WaveGeneration = DAC_WaveGeneration_None; DAC_Init(DAC_Channel_1, &DAC_InitStructure); DAC_Init(DAC_Channel_2, &DAC_InitStructure); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(DAC->DHR12RD); DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)&sinCosTable; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize = 32; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word; DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel3, &DMA_InitStructure); DMA_Cmd(DMA1_Channel3, ENABLE); DAC_Cmd(DAC_Channel_1, ENABLE); DAC_Cmd(DAC_Channel_2, ENABLE); DAC_DMACmd(DAC_Channel_1, ENABLE); TIM_Cmd(TIM2, ENABLE); while (1) {} }
Das Feld sinTable enthält 32 12-Bit-Werte mit denen ein Sinus angenähert wird. Über drei for-Schleifen werden die beiden 16-Bit Hälfte von sinCosTable gefüllt. In die oberen 16-Bit wird die Sinus-Tabelle kopiert, in die unteren 16-Bit die gleiche Tabelle jedoch um 8 Werte verschoben (90° Phasenversatz).
Die Taktvorgabe für das Laden des nächsten Wertes in den DAC stellt Timer 2 bereit. Der Systemtakt beträgt 24 MHz und wird mit dem Faktor 75 auf 320 kHz geteilt. Da eine Periode 32 Einzelwerte enthält ergibt sich somit eine Frequenz von 10 kHz. Als Triggersignal wird das Update Event gewählt.
Da der Dual DAC Channel Mode genutzt wird muss nur ein DMA Channel belegt werden, hier Channel 3.
// 22 I2C & DMA
In diesem Kapitel wird der Einsatz von I2C in Verbindung mit DMA demonstriert. An den STM32 ist ein Temperatursensor TCN75 von Microchip angeschlossen.
In der Application Note AN2824 wird der genaue Ablauf beschrieben um das I2C-Modul per DMA mit Daten zu versorgen.
Durch die Nutzung von DMA wird das zeitkritische Auslesen/Schreiben des Datenregisters sowie Setzen des NACK-Bits beim Datenempfang vermieden. Folglich sind keine hoch priorisierten Interrupts mehr notwendig.
Im Master Receiver Mode ist zu beachten, dass zwingend mindestens zwei Bytes empfangen werden müssen.
Um das Temperatur-Register des TCN auszulesen muss zunächst ein Register-Pointer auf 0 gesetzt werden und anschließend ein Lese-Transfer gestartet werden. Weitere Details stehen im Datenblatt.
..\main.c
STM32F103RB
volatile uint8_t i2cDirectionWrite; uint8_t i2cRxBuffer[] = { 0xAA, 0xAA }; uint8_t i2cTxBuffer[] = { 0x00 }; int main(void) { GPIO_InitTypeDef GPIO_InitStructure; I2C_InitTypeDef I2C_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; DMA_InitTypeDef DMA_InitStructure; SystemInit(); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); I2C_DeInit(I2C2); I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; I2C_InitStructure.I2C_ClockSpeed = 400000; I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; I2C_InitStructure.I2C_OwnAddress1 = 0; I2C_Init(I2C2, &I2C_InitStructure); I2C_ITConfig(I2C2, I2C_IT_EVT, ENABLE); I2C_Cmd(I2C2, ENABLE); I2C_DMACmd(I2C2, ENABLE); NVIC_InitStructure.NVIC_IRQChannel = I2C2_EV_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); // DMA Channel 4 - I2C2 TX DMA_InitStructure.DMA_BufferSize = 0; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t) i2cTxBuffer; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t) & I2C2->DR; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_Init(DMA1_Channel4, &DMA_InitStructure); // DMA Channel 5 - I2C2 RX DMA_InitStructure.DMA_BufferSize = 0; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t) i2cRxBuffer; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t) & I2C2->DR; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_Init(DMA1_Channel5, &DMA_InitStructure); DMA_ITConfig(DMA1_Channel5, DMA_IT_TC, ENABLE); NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel5_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); while (1) { for (volatile uint32_t i = 0; i < 1000000; i++); i2cDirectionWrite = 1; DMA_SetCurrDataCounter(DMA1_Channel4, 1); DMA_SetCurrDataCounter(DMA1_Channel5, 2); I2C_GenerateSTART(I2C2, ENABLE); } } void I2C2_EV_IRQHandler(void) { if (I2C_GetFlagStatus(I2C2, I2C_FLAG_SB) == SET) { if (i2cDirectionWrite) { // STM32 Transmitter I2C_Send7bitAddress(I2C2, 0x9E, I2C_Direction_Transmitter); } else { // STM32 Receiver I2C_Send7bitAddress(I2C2, 0x9E, I2C_Direction_Receiver); } } else if (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) == SUCCESS) { if (i2cDirectionWrite) { // STM32 Transmitter DMA_Cmd(DMA1_Channel4, ENABLE); } } else if (I2C_GetFlagStatus(I2C2, I2C_FLAG_BTF)) { if (i2cDirectionWrite) { // STM32 Transmitter DMA_Cmd(DMA1_Channel5, ENABLE); I2C_DMALastTransferCmd(I2C2, ENABLE); I2C_GenerateSTART(I2C2, ENABLE); i2cDirectionWrite = 0; I2C_ClearFlag(I2C2, I2C_FLAG_BTF); } } } void DMA1_Channel5_IRQHandler(void) { DMA_ClearFlag(DMA1_FLAG_TC5); I2C_GenerateSTOP(I2C2, ENABLE); DMA_Cmd(DMA1_Channel4, DISABLE); DMA_Cmd(DMA1_Channel5, DISABLE); // Transmission of CAN Message }
Zunächst wird die Variable i2cDirectionWrite deklariert. Sie gibt später an, ob sich das Modul im Schreib(1)- oder Lesemodus(0) befindet. Des weiteren werden zwei Felder für die zu sendenden/empfangenden Daten definiert.
Wichtig: Bei der Konfiguration des I2C-Moduls dürfen Buffer-Events (I2C_IT_BUF) nicht aktiviert werden.
Über die Funktion I2C_DMACmd(...) wird das Modul dazu veranlasst DMA-Requests zu erzeugen.
Für den DMA Controller werden hier die Channels 4 und 5 belegt. Um am Ende des I2C-Transfers ein STOP-Signal zu senden und die Weiterverarbeitung der Daten anzustoßen wird bei Channel 5 der Transmission-Complete-Interrupt aktiviert.
Nach der Konfiguration befindet sich der Prozessor in einer Endlosschleife in der periodisch der Datentransfer angestoßen wird. Dazu werden die Daten-Zähler der DMA-Channels gesetzt und anschließend ein START-Signal gesendet.
Wurde START-Signal erfolgreich übertragen, so wird vom I2C ein Interrupt ausgelöst und über I2C_Send7bitAddress(...) die Slave-Adresse gesendet (hier 0x4F, für die Funktion um 1 nach links geshiftet).
Nach dem Übertragen der Slave Adresse wird erneut ein Interrupt ausgelöst und zum Senden der Daten der DMA-Channel 4 eingeschaltet.
Wenn alle Bytes gesendet worden sind wird das BTF-Flag (-> Interrupt) gesetzt. Im folgenden soll ein REPEAT-START Signal gesendet werden und im anschließenden Read-Transfer die Daten gelesen werden. Hierfür wird DMA-Channel 5 aktiviert und anschließend ein neuer Transfer mit I2C_GenerateSTART(...) gestartet. Die Variable i2cDirectionWrite wird auf 0 gesetzt um später in den Zweig zu springen, der die Adresse mit gesetztem R/W-Bit sendet. I2C_DMALastTransferCmd(...) weißt das I2C-Modul an nach dem letzten Byte ein NACK zu senden.
Ist der Datenempfang mit der vorgegebenen Anzahl von Bytes abgeschlossen, so wird der Transfer-Complete-Interrupt des DMA Channels 5 ausgelöst. Mit I2C_GenerateSTOP(...) wird ein STOP-Signal übertragen und anschließend werden die beiden DMA-Channels deaktiviert. Im Interrupt-Handler können noch weitere Funktionen aufgerufen/Flags gesetzt werden um eine Verarbeitung der empfangenen Daten anzustoßen (z.B. Senden einer CAN Message).
Ein vollständiges CoIDE-Projekt inklusive CAN-Transfer kann hier heruntergeladen werden.