Timer sind in Bezug auf Embedded Systems ein essentielles Thema. Diese werden beispielsweise bei wiederkehrenden Events, Zeitzählung und Ausgangssignalen verwendet. In diesem Tutorial steuern wir die Lichtintensität einer LED zeitabhängig. Im Detail verwenden wir einen Timer um ein PWM Signal zu generieren und einen anderen um die PWM Werte zu verändern.
Was ist PWM!?
Eine einfache Erklärung ist, dass die Pulsweitenmodulation eine Möglichkeit bietet, analoge Werte an digitalen Ausgängen zu generieren. Ein normaler GPIO Ausgang kann nur High oder Low sein. Daher ist es nur über die Variation der Zeit möglich analoge Werte darzustellen. Durch die Nutzung von PWM wird ein Ausgang alternierend ein- und ausgeschalten. Der Durchschnitt dieser Kurvenform liegt somit zwischen High und Low.
Tastverhältnis und PWM Frequenz sind die wichtigsten Parameter für uns. Das Verhältnis der Einschaltzeit über die Periodendauer wird Tastverhältnis genannt. Bei einem beispielhaften Tastverhältnis von 20% ist gemeint, dass ein Signal für 20% der Periodendauer ein und für 80% aus ist. Die PWM Frequenz definiert die Periodendauer des Signales.
Wenn du mehr über PWM erfahren List folge diesem Link.
Timer Einstellungen
PWM Timer
Bevor wir STM32CubeMX öffnen müssen wir die Einstellungen definieren, die wir für unsere Applikation benötigen. Ein Timer übernimmt das PWM Signal. In unserer Anwendung werden wir eine Variable benutzen die Werte zwischen 0 und 100 annimmt. Diese Variable definiert das Tastverhältnis des PWM Signals. Zusätzlich definieren wir eine PWM Frequenz von 20 kHz weil mit dieser sicher kein Flimmern entsteht. Ein zweiter Timer ändert das Tastverhältnis um 5% alle 50 ms. Zusätzlich soll die Richtung automatisch umschalten. Diese Kombination erzeugt einen Lichteffekt der wie ein langsames auf- und abschwingen aussieht.
Nun da wir das Verhalten dargestellt haben können wir das Tool STM32CubeMX starten und ein neues Projekt für unseren Mikrocontroller erstellen. Wie das gemacht wird erfahrt ihr aus meinem vorigen Blog Post hier. Im Pinout, klicken wir auf PB3 und wählen „TIM2_CH2“.

Danach klicken wir auf Timers – TIM2 und wählen „PWM Generation CH2“ bei Channel 2. Zusätzlich wählen wir „Internal Clock“ als Clock Source.

Nun müssen wir die Timer Einstellungen vornehmen. In der „Clock Configuration“ sehen wir, dass für die Timer ein Takt von 8 MHz aktiv ist. Wenn wir nun eine PWM Frequenz von 20 kHz benötigen müssen wir den Clock um 400 (8e6 Hz / 20e3 Hz) dividieren. Zusätzlich wollen wir die Taktfrequenz mit den Nummern 0 – 100 einstellen. Dafür setzen wir die „Counter Period“ auf 100. Nun errechnet sich der „Prescaler“ mit 4 (400 / 100). Das Produkt von „Prescaler“ und „Counter Period“ zeigen nun den Wert 400 wie oben berechnet.

Nun wechseln wir zu den NVIC Einstellungen und aktivieren den „TIM2 global interrupt“. Wir benutzen diesen Interrupt später um das Tastverhältnis dynamisch zu ändern.

Fading Timer
Als nächstes, klicken wir auf Timers – TIM1 und aktivieren „Internal Clock“ als „Clock Source“. Wir haben oben eine Periodendauer von 50 ms definiert. Das übersetzt sich in eine Frequenz von 20 Hz. Aus diesem Grund benötigen wir einen „Prescaler“ von 400.000 (8e6 Hz / 20 Hz). Die höchste Nummer die der „Prescaler“ annehmen kann ist 65.000 also müssen wir den Faktor aufteilen. Ich have einen „Prescaler“ von 800 und eine „Counter Period“ von 500 ausgewählt.

Nachdem wir die Änderungen durchgeführt haben klicken wir auf NVIC Settings und aktivieren „TIM1 update und TIM16 interrupts“.

Nun sind die Einstellungen in STM32CubeMX fertig. Gebt eure projektbezogenen Daten im „Project Manager“ ein und drückt „Generate Code“. Nach diesem Schritt öffnen wir das eben erstellte Projekt in SW4STM32 oder jeder anderen IDE die ihr benutzt.
Hinzufügen von User Code
Wenn wir unseren Code zu dem generierten hinzufügen müssen wir immer vorsichtig sein und in den dafür vorgesehenen User-Bereichen bleiben. Weil diese Bereiche relativ einfach zu finden sind, ist es auch einfach meinen Code-Änderungen zu folgen. Die Bereiche sind immer gleich benannt. Um zum Beispiel das nächste Code-Stippet zu finden sucht einfach nach „BEGIN PV“ in der benannten Datei und ihr kommt direkt zu dem Punkt an dem ich den Code eingefügt habe.
main.c
Als Erstes erzeugen wir in der Datei main.c eine neue Variable ui8TimPulse. Diese repräsentiert das Tastverhältnis des PWM Signals.
/* USER CODE BEGIN PV */
uint8_t ui8TimPulse = 50; // set duty cycle to 50% initially
/* USER CODE END PV */
Als nächstes müssen wir, in der selben Datei, die Timer 1 und 2 sowie das PWM Modul starten. Unten seht ihr wie das gemacht wird. HAL_TIM_Base_Start_IT startet die Zeitbasis des, jeweils im Argument definierten, Timers. Es aktiviert auch die Interrupts die von dem Timer benutzt werden.HAL_TIM_PWM_Start startet den PWM spezifischen Teil des Timers. TIM_CHANNEL_2 entspricht dem GPIO Pin der in STM32CubeMX ausgewählt wurde.
/* USER CODE BEGIN 2 */
HAL_TIM_Base_Start_IT(&htim1);
HAL_TIM_Base_Start_IT(&htim2);
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2);
/* USER CODE END 2 */
main.h
In der Datei main.h fügen wir einige Konstanten hinzu um magische Zahlen zu vermeiden. Die Konstante PWM_MIN definiert den minimal möglichen Wert, PWM_MAX ist der maximal mögliche PWM Wert und PWM_MIN_CHANGE ist der Prozentsatz der bei jeder Addition oder Subtraktion vom Tastverhältnis verwendet wird.
/* USER CODE BEGIN EC */
#define PWM_MIN 4
#define PWM_MAX 100
#define PWM_MIN_CHANGE 5
/* USER CODE END EC */
stm32f3xx_it.c
Als nächstes deklarieren wir die Variable ui8TimPulse und definieren eine neue Enumeration in der Datei stm32f3xx_it.c. Die Enumeration wir für die Richtungsbestimmung verwendet.
/* USER CODE BEGIN EV */ extern uint8_t ui8TimPulse; enum{ UP=0, DOWN }eDir; /* USER CODE END EV */
Wir bleiben in dieser Datei und gehen hinunter zur TIM2_IRQHandler Funktion. Hier ist der perfekte Ort um das Tastverhältnis bei jedem Timer-Event neu zu setzen. TIM2->CCR2 gibt uns direkten Zugriff auf das „Capture Compare Register 2“ vom Timer 2. Dieser wird verwendet um das Tastverhältnis des PWM Signals zu steuern. Die Variable htim2.Init.Period beinhaltet den initialisierten „Counter Period“ Wert von oben.
/* USER CODE BEGIN TIM2_IRQn 0 */
TIM2->CCR2 = (htim2.Init.Period * ui8TimPulse) / 100u;
/* USER CODE END TIM2_IRQn 0 */
Der Code oben wäre schon genug um die LED bei konstanter Helligkeit zu betreiben. Die aktive Helligkeit kann statisch über die Variable ui8TimPulse verändert werden. Wir allerdings wollen das Tastverhältnis dynamisch ändern.
Die Funktion TIM1_UP_TIM16_IRQHandler wird jedesmal aufgerufen wenn die „Capture Compare Bedingung“ erfüllt wurde. In unserem Fall passiert dies alle 50 ms. Bei jedem interrupt prüfen wir zuerst die Richtung. Danach prüfen wir ob das Ende erreicht wurde Wenn das der Fall ist ändern wir die Richtung, andererseits inkrementieren/dekrementieren wir das Tastverhältnis.
/* USER CODE BEGIN TIM1_UP_TIM16_IRQn 0 */
if (eDir == UP) {
if( ui8TimPulse >= PWM_MAX )
eDir = DOWN;
else
ui8TimPulse += PWM_MIN_CHANGE;
} else {
if ( ui8TimPulse <= PWM_MIN )
eDir = UP;
else
ui8TimPulse -= PWM_MIN_CHANGE;
}
/* USER CODE END TIM1_UP_TIM16_IRQn 0 */
Nun ist das Programm fertig um kompiliert und auf den Mikrocontroller programmiert zu werden. Wenn ihr alles richtig gemacht habt solltet ihr nun das gleiche Ergebnis wie im Video unten sehen.
Zusammenfassung
Dieser Blog Post erklärte die Grundlagen von PWM und auch die verschiedenen Anwendungen für Timer. Wie bereits im GPIO Tutorial gezeigt, wird auch hier fast die gesamte Konfiguration in STM32CubeMX vorgenommen. Nur der Start der Timer und unsere eigene Anwendung wurde direkt als Code implementiert. Diese Kombination macht es sehr leicht und schnell funktionierenden Code zu schreiben.
Wie immer könnt ihr eure Fragen gerne in den Kommentaren stellen, ich werde diese schnellstmöglich beantworten Auch wenn euch Verbesserungen einfallen könnt ihr diese gerne posten.