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:

  1. 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.
  2. 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.
  3. 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

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:


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

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

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:

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_OCInitTypeDef:

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:

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:

  // 11.2 Nachrichten senden

..\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:

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:

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:

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:

..\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_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 Kalibrierung

Die 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 Mode

Nach 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:

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:

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:




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

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:

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:

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.