Action disabled: revisions

ARM Interrupts in C

Der STM32F4 verfügt über fast 100 mögliche Unterbrechungsereignisse. Theoretisch kann ein ARM bis 240 Ereignisse managen. Mit dieser Menge an möglichen Interrupts und seinem praktisch im Kern eingebetteten schnellen Interruptcontroller NVIC kann der Cortex M4 vor lauter Unterbrechungskraft kaum laufen. Die Auswahl und Konfiguration konkreter Interrupts kann für den Entwickler aber recht kniffelig werden. Vor allem die möglichen Querbeziehungen zu anderen Bausteinen sind nur schwer zu überblicken. Dazu sei an dieser Stelle auf ein nettes Werkzeug von ST verwiesen. Den ST-MicroXplorer. Diesen finden Sie ebenfalls in SiSy unter dem Menüpunkt Werkzeuge. Er ermöglicht es dem Anwender, eine gewisse Auswahl an Gerätekonfigurationen zu erarbeiten und bezogen auf das PinOut des Controllers, Konflikte und Einschränkungen zu erkennen. Wir beginnen mit der Programmierung eines einfachen Timer-Ereignisses.

Um die Interrupt-Programmierung des ARM kennen zu lernen, nehmen wir uns eine verhältnissmäßig einfache Aufgabe vor. Lassen wir eine LED blinken. Die LED über einen Digital-Port ein-, aus- und umzuschalten beherrschen wir bereits. Unsere gewünschte Blinkfrequenz soll ein Timer generieren. Der ARM verfügt über unterschiedlich komplexe Timer. Zum Einstieg suchen wir uns am Besten einen einfachen Timer aus. Timer, die nur über elementare Funktionen verfügen, werden beim STM32F4 als Basic-Timer bezeichnet. Einer der Basic-Timer ist TIM7. Dieser soll uns ein 100-Millisekunden-Ereignis liefern. Im Ereignishandler togglen wir dann unsere blaue LED.

Der Blick ins Datenblatt verrät uns, dass der Basic-Timer TIM7 am Peripherie-Bus APB1 hängt. Somit ist TIM7 der erste Baustein, den wir über einen Peripherie-Bus ansprechen. Ein weiterer neuer Baustein muss diesmal von uns programmiert werden, der Interruptcontroller NVIC. Diesem müssen wir beibringen, dass wir das Timer-Ereignis als Unterbrechungsanforderung behandeln möchten. Fassen wir die Aufgabenstellung kurz zusammen:

  1. GPIOD mit Takt versorgen
  2. Pin D15 als Ausgang konfigurieren
  3. TIM7 auf 100 ms Ereignis konfigurieren
  4. TIM7 Interrupt im NVIC initialisieren
  5. eine ISR für den TIM7 Interrupt schreiben
  6. LED an D15 togglen

Detaillierte Informationen zur Programmierung der Timer finden sie nicht im Datenblatt, sondern in der STM32-Timer-Übersicht und im STM32-Referenzhandbuch.

Falls das Tutorial-Projekt nicht offen ist, öffnen Sie dies. Legen Sie bitte ein neues kleines Programm an und laden das Grundgerüst ARM C++ Anwendung. Beachten Sie die Einstellungen für die Zielplattform STM32F4-Discovery.

Erstellen Sie die Programmkopfdokumentation. Übersetzen und Übertragen Sie das noch leere Programm auf den Controller, um die Verbindung zu testen.

//----------------------------------------------------------------------
// Titel     : Beispiel BasicTimer in SiSy STM32
//----------------------------------------------------------------------
// Funktion  : blaue blaue LED blinkt
// Schaltung : LED an PD15
//----------------------------------------------------------------------
// Hardware  : STM32F4 Discovery
// Takt      : 168 MHz
// Sprache   : ARM C++
// Datum     : 29.08.2012
// Version   : 1
// Autor     : Alexander Huwaldt
//----------------------------------------------------------------------

Bei der Lösungsdiskussion konzentrieren wir uns auf die neuen Aspekte. Diese betreffen vor allem den Timer und den Interruptcontroller.

Für das Einschalten des GPIO-Taktes haben wir die Funktion RCC_AHB1PeriphClockCmd kennengelernt. Aus der Systematik der Namensgebung sollte sich jetzt auch ohne langes Studium der Treiber-API von ST, der rechte Befehl für unseren Timer an APB1, finden lassen. Wenn wir RCC_AP eingeben springt uns die gewünschte Funktion schon ins Auge.

Die Funktion RCC_APB1PeriphClockCmd erwartet wie gehabt zwei Parameter. Zum einen die Identifikation des Gerätes und als Zweites den neuen Takt-Status für das Gerät, in unserem Fall ein ENABLE, um den Takt einzuschalten.

// Takt für Timer 7 einschalten
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM7, ENABLE);

Nun ist das 100 ms Ereignis zu konfigurieren.

Vom GPIO kennen wir das Konzept, die Initialisierung von Bausteinen über Strukturen zu realisieren. Dazu waren folgende Schritte typisch:

  1. Initialisierungsstruktur anlegen
  2. diese Struktur ggf. vorinitialisieren
  3. den Strukturelementen die nötigen Werte zuweisen
  4. den gewünschten Baustein mit der Struktur initialisieren

Die Struktur für die Basis-Initialisierungen von Timern hat den etwas sperrigen Typbezeichner TIM_TimeBaseInitTypeDef. Von dieser Struktur brauchen wir eine Instanz.

TIM_TimeBaseInitTypeDef timInitStruct;

Die Elemente dieser Struktur erlauben uns, bezogen auf den Basis-Takt, für den Timer sehr exakt das gewünschte Ereignis zu konfigurieren. Dazu ist jedoch ein Blick ins Datenblatt (Seite 29) erforderlich:

Der Timer TIM7 wird demnach mit 84 MHz getaktet. Darauf bauen jetzt unsere Überlegungen zum konfigurieren des Timerereignisses auf.

Das Strukturelement TIM_CounterMode legt fest, wie der Zähler arbeitet. Die beiden einfachsten Modi sind TIM_CounterMode_Up oder TIM_CounterMode_Down. Der Timer TIM7 ist laut der eben eingesehen Tabelle ein reiner Up-Conter.

timInitStruct.TIM_CounterMode = TIM_CounterMode_Up;

Das Strukturelement TIM_ClockDivision ermöglicht es, bei Bedarf den Basistakt des Timers grob mit den Teilern 2 oder 4 vorzuteilen.

timInitStruct.TIM_ClockDivision = TIM_CKD_DIV1;

Wir brauchen den Takt von 84 MHz nicht vorteilen, um ein 100 ms Ereignis zu generieren. Der eigentliche Prescaler und der Vergleichswert für die Timerperiode reichen aus, um die gewünschte Konfiguration zu erzielen.

Das Strukturelement TIM_Prescaler ist ein 16 Bit Wert und erlaubt laut Hilfe beliebige Werte zwischen 0 und 65535 (0x0000 - 0xFFFF). Mit dem Vorteilerwert 1000 takten wir den Timer schon mal auf 84000 Hz herunter.

timInitStruct.TIM_Prescaler = 1000;

Um jetzt aus den 84 kHz ein 100 ms Ereignis zu erhalten, lassen wir den Timer von 0 bis 8400 zählen. Das Strukturelement TIM_Period erlaubt ebenfalls Werte von 0 bis 65535.

timInitStruct.TIM_Period = 8400;

Die so vorbereitete Initialisierungsstruktur übergeben wir mit der Funktion TIM_TimeBaseInit an den gewünschten Timer TIM7.

TIM_TimeBaseInit(TIM7, &timInitStruct);

Der Timer könnte ab diesem Moment, genau so wie in der Struktur konfiguriert, arbeiten. Das würde er dann „stumm“ und fleißig vor sich hin tun und wir könnten den aktuellen Timerwert, zum Beispiel im Polling, überwachen. Die Zielstellung ist jedoch ein Interrupt auszulösen. Dafür steht die Funktion TIM_ITConfig zur Verfügung. Diese erlaubt für Timer die Ereignisse

  • TIM_IT_Break, bei PWM-Störung einer Motorsteuerung
  • TIM_IT_CCn, Capture Compare z.B. Impulse erfassen zur Auswertung von Encodern
  • TIM_IT_COM, Kommunikationsereignis im Six-Step-Mode zur Schrittmotorensteuerung
  • TIM_IT_Trigger, zur Synchronisation von mehreren Timern
  • TIM_IT_Update, Zählerregister wird neu geladen

Das für unseren Fall geeignete Unterbrechungsereignis ist TIM_IT_Update. Es wird ausgelöst, wenn die Timerperiode erreicht ist und das Zählregister neu geladen wird.

TIM_ITConfig(TIM7, TIM_IT_Update, ENABLE);

Der NVIC (Nested Vectored Interrupt Controller) zeichnet sich durch seine enge Kopplung an den Kern des Prozessors aus. Ergebnis sind extrem kurze Latenzzeiten der Interrupts. Ein weiterer Schritt zu einem erwachsenen Interruptcontroller ist die echte Vektorisierung im Vergleich z.B. zu älterer ARM oder dem AVR. Das bedeutet, dass in der Vektortabelle keine Sprungbefehle mehr stehen, sondern Adressen, welche vom Kern quasi direkt in den Programmcounter übernommen werden können. Die Arbeit mit Adressen erleichtert auch die Priorisierung und Neuordnung der Vektoren zur Laufzeit. Beim NVIC ist die Priorität einer Interruptanforderung nicht über die Position in der Tabelle definiert. Interrupts können in Kanälen gruppiert werden. Diese Gruppen erhalten eine Gruppen-Priorität. Innerhalb einer solchen Gruppe können ebenfalls Sub-Prioritäten vergeben werden. Die Priorität wird beim Cortex mit 4 Bit codiert und ist somit von 0-15 abstufbar. Der höchste Wert besitzt dabei die kleinste Priorität. Die Ausnutzung weiterer Fähigkeiten des NVIC sind dann wohl eher Echtbetriebssystemen vorbehalten. Die Möglichkeit zur dynamischen Neupriorisierung und Verkettung von Interrupts sowie die automatische Speicherung und Wiederherstellung von Prozessorzuständen schaffen Voraussetzungen für ein effektives Multitaskingbetriebssystem.

Die Initialisierungsstuktur für den NVIC hat den Typbezeichner NVIC_InitTypeDef. Langsam gewöhnen wir uns an die Systematik der Bezeichner für die Initalisierungsstrukturen. Diese bestehen immer aus Gerätenamen_InitTypeDef.

NVIC_InitTypeDef NVIC_InitStructure;

Das Strukturelement NVIC_IRQChannel legt fest, welcher IRQ jetzt konfiguriert wird. Die Identifikation des Ereignisses setzt sich wie folgt zusammen: Gerät_IRQn. Wir hatten uns für den Timer TIM7 entschieden.

NVIC_InitStructure.NVIC_IRQChannel = TIM7_IRQn;

Das Strukturelement NVIC_IRQChannelCmd legt fest, welchen neuen Zustand der ausgewählte IRQ erhalten soll. Wir möchten diesen einschalten, also weisen wir ein ENABLE zu.

NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;

Mit dem Strukturelement NVIC_IRQChannelPreemptionPriority weisen wir dem Kanal seine Priorität zu. So wichtig ist unsere LED nicht, also geben wir dieser die geringste Priorität.

NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x0F;

Für das Strukturelement NVIC_IRQChannelSubPriority gelten die gleichen Überlegungen.

NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x0F;

Damit sollten wir alles Notwendige für die Initialisierung festgelegt haben und können diese durchführen. Die Funktionen, in die wir unsere Initialisierungsstrukturen hineinwerfen, folgen auch immer der gleichen Benennungssystematik Gerät_Init.

NVIC_Init(&NVIC_InitStructure);

Der Timer hat seinen Takt und ist konfiguriert. Der Interrupt wurde initialisiert und der NVIC angewiesen wie er damit umzugehen hat. Jetzt ist es wohl Zeit auf den Startknopf unserer Stoppuhr zu drücken. Das Einschalten des so vorbereiteten Timers erfolgt mit der Funktion TIM_Cmd. Diese erwartet zwei Parameter, den Timer und das Kommando ENABLE oder DISABLE.

TIM_Cmd(TIM7, ENABLE);

Ganz so schnell schießen die Preußen nun aber auch wieder nicht. Es ist natürlich nötig erst noch eine Funktion zur Ereignisbehandlung zu schreiben. Derartige Funktionen sind als extern „C“ void zu deklarieren und besitzen fest vorgegebene Namen, die es dem Compiler ermöglichen, die ISR dem richtigen Interrupt zuzuordnen. Die Namen der Eventhandler folgen immer dem Muster Gerät_IRQHandler. Wenn wir in SiSy die Zeichenfolge TIM7_ eingeben können wir in der angebotenen Liste den Namen der ISR auswählen.

extern "C" void TIM7_IRQHandler()
{  
	GPIO_ToggleBits(GPIOD,GPIO_Pin_15);		
	TIM_ClearITPendingBit(TIM7, TIM_IT_Update);
}

Interrupt Service Routinen sind dafür verantwortlich, dem Kern mitzuteilen, dass der Interrupt adäquat behandelt wurde. Das geschieht bei Timern mit der Funktion TIM_ClearITPendingBit. Das einfache RETI (Maschinenbefehl Return from Interrupt) wie beim AVR reicht nicht mehr aus. Sinn des expliziten „Aufräumens“ ist, das beim Verketteten Interrupts mehrerer Quellen alle Quellen und Handler die vollständige Abarbeitung quittieren. Wird der Interrupt nicht als erledigt gemeldet, löst der NVIC diesen sofort wieder aus!

Jetzt sollte alles beieinander sein und wir tasten uns an die Lösung der Aufgabe heran. Schreiben Sie zuerst die Kommentare WAS in welcher Reihenfolge, also WANN zu tun ist. Beachten Sie in welchen Programmbereichen, also WO die einzelnen Aktionen zukünftig ausgeführt werden sollen.

//----------------------------------------------------------------------
// Titel     : ENTWURF Beispiel BasicTimer in SiSy STM32
//----------------------------------------------------------------------
// Funktion  : blaue blaue LED blinkt
// Schaltung : LED an PD15
//----------------------------------------------------------------------
// Hardware  : STM32F4 Discovery
// Takt      : 168 MHz
// Sprache   : ARM C++
// Datum     : 29.08.2012
// Version   : ENTWURF
// Autor     : Alexander Huwaldt
//----------------------------------------------------------------------
 
#include <stddef.h>
#include <stdlib.h>
#include "hardware.h"
 
void initApplication()
{
 
	SysTick_Config(SystemCoreClock/100);
	// weitere Initialisierungen durchführen
 
	// GPIOD Takt einschalten 
	// Konfiguriere PD15 als Ausgang
	// Takt für Timer 7 einschalten
	// Timer7 konfigurieren
	// Timer7 einschalten
	// Interruptcontroller konfigurieren
}
 
int main(void)
{
	SystemInit();
	initApplication();
	while(true)
	{
		//leer
	}
}
 
extern "C" void SysTick_Handler(void)
{
	//leer		
}
 
// hier ISR für Timer7 schreiben

Sie wissen ja, tief durchatmen, noch mal drüber schauen und dann selbst und bewusst die Befehlszeilen eingeben.

//----------------------------------------------------------------------
// Titel     : Beispiel BasicTimer in SiSy STM32
//----------------------------------------------------------------------
// Funktion  : blaue blaue LED blinkt
// Schaltung : LED an PD15
//----------------------------------------------------------------------
// Hardware  : STM32F4 Discovery
// Takt      : 168 MHz
// Sprache   : ARM C++
// Datum     : 29.08.2012
// Version   : 1
// Autor     : Alexander Huwaldt
//----------------------------------------------------------------------
 
#include <stddef.h>
#include <stdlib.h>
#include "hardware.h"
 
void initApplication()
{
 
	SysTick_Config(SystemCoreClock/100);
	// weitere Initialisierungen durchführen
 
	// GPIOD Takt einschalten 
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD, ENABLE);
 
	// Konfiguriere PD15 
	GPIO_InitTypeDef  GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
	GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
	GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
	GPIO_Init(GPIOD, &GPIO_InitStructure);
 
	// Takt für Timer 7 einschalten
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM7, ENABLE);
 
	// Timer7 konfigurieren
	TIM_TimeBaseInitTypeDef TIM_TimeBase_InitStructure;
	TIM_TimeBase_InitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
	TIM_TimeBase_InitStructure.TIM_CounterMode = TIM_CounterMode_Up;
	TIM_TimeBase_InitStructure.TIM_Period = 4200;
	TIM_TimeBase_InitStructure.TIM_Prescaler = 1000;
	TIM_TimeBaseInit(TIM7, &TIM_TimeBase_InitStructure);
	TIM_ITConfig(TIM7, TIM_IT_Update, ENABLE);
 
	// Timer7 einschalten
 	TIM_Cmd(TIM7, ENABLE);
 
	// Interruptcontroller konfigurieren
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = TIM7_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x0F;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x0F;
	NVIC_Init(&NVIC_InitStructure);
 
}
 
int main(void)
{
	SystemInit();
	initApplication();
	while(true)
	{
		//leer
	}
}
 
extern "C" void SysTick_Handler(void)
{
	//leer		
}
 
extern "C" void TIM7_IRQHandler()
{  
	GPIO_ToggleBits(GPIOD,GPIO_Pin_15);		
	TIM_ClearITPendingBit(TIM7, TIM_IT_Update);
}

Übersetzen und übertragen Sie das Programm. Testen Sie die Anwendung.

Videozusammenfassung

Das war ein ziemlicher Aufwand den wir betrieben haben, um eine LED blinken zu lassen. Beachten sie jedoch, dass wir dies mit einem Interrupt gelöst haben. Dieses Programmiermodell ist zwar aufwändig, aber auch mächtig. Hier dieser Abschnitt wiederum als Videozusammenfassung.

Nächstes Thema