1.7 💯 PWM (PulsWeitenModulation)

Synopsis: [de.wikipedia.org/wiki/Pulsdauermodulation 🔗]
Wieder so ein „explosives“ 🧨 Thema! Sobald man sich näher damit beschäftigt kann es spannend aber auch kompliziert werden…

  • Verwendung
    • Leistung steuern
    • Töne ausgeben
    • Servos ansteuern
  • Per Timer-Hardware an bestimmten Pins möglich
    • Arduino: analogWrite(…)…
      • analogWrite(…)
      • analogWriteResolution(…)
      • analogWriteFrequency(…)
      • einfach aber sehr eingeschränkt und ggfs. mit Nebenwirkungen.
    • STM32-API Befehle
      • setPWM(…)
      • setMode(…)
      • setOverflow(…)
      • setCaptureCompare(…)
    • µC-Register wissend selber einstellen
      • AVR
      • STM32
  • Per Software an allen Pins möglich

Arduino: analogWrite()

Lesen: [arduino.cc/reference/en/language/functions/analog-io/analogWrite/ 🔗] [docs.arduino.cc/learn/microcontrollers/analog-output/ 🔗]

Die Helligkeit der LED an PC7 soll durch den low-aktiven prellfreien UserButton an PC13 in den Stufen
0% -> 25% -> 50% -> 75% -> 100% -> 0% usw. mit einer ISR isr_UserB() verändert werden, Testcode:

#define USER_B PC13 // User-Button prellfrei low aktiv
#define LED PC7     // AusgabePin
#define MAX_A 255   // Maximaler Analogwert
volatile unsigned int stufe=0; // Globale Variable für Stufe

int analogwert[]={0,MAX_A*.25,MAX_A*.50,MAX_A*.75,MAX_A}; // 0..100%
//int analogwert[]={0,63,127,191,255};    // Analogstufen
int anzahl = sizeof(analogwert)/sizeof(analogwert[0]);    // Anzahl der Array-Einträge

void isr_UserB(){  // ISR für Helligkeit verändern
  if(stufe<anzahl-1) stufe++;
  else stufe = 0;
  analogWrite(LED,analogwert[stufe]); // Arduino-Funktion für PWM Ausgabe an bestimmten Port-Pins
  Serial.printf("Stufe %d Analogwert %d \n",stufe,analogwert[stufe]);
}
void setup(){
  Serial.begin (9600); // Serielle kommunikation starten
  pinMode(USER_B, INPUT);
  attachInterrupt (digitalPinToInterrupt (USER_B), isr_UserB, FALLING); // fallende Flanke
}
void loop(){}

Hinweis: pinMode(LED,OUTPUT); ist zur Ausgabe nicht notwendig
🖥 Testen Sie das Programm.

Messen des Ausgabe-Signals mit Oszilloskop

Wie sieht das ausgegebene Signal aus? Messen wir es mit einem Oszilloskop!

Oszillogramm für 25%
Oszillogramm für 25%
Oszillogramm für 50%
Oszillogramm für 50%

Die Frequenz ist 1 kHz, die Genauigkeit ist bedingt durch nur 256 Stufen von 0..255 nicht besonders hoch.

PWM besser machen?

Es gibt zwei Einstellmöglichkeiten für die PWM-Ausgabe mit analogWrite(..):

analogWriteResolution(n); // n=8..16

Gibt die Bit-Auflösung des AnalogWertes an. Bei 8 Bit ist der Maximalwert 255, bei 16 Bit 65535 für 100%.
Bei 16 Bit Auflösung ist die Genauigkeit höher.

Testcode
#define USER_B PC13 // User-Button prellfrei low aktiv
#define LED PC7     // AusgabePin
#define MAX_A 65535   // Maximaler Analogwert
volatile unsigned int stufe=0; // Globale Variable für Stufe
int analogwert[]={0,MAX_A*.25,MAX_A*.50,MAX_A*.75,MAX_A}; // 0..100%
int anzahl = sizeof(analogwert)/sizeof(analogwert[0]); // Anzahl der Array-Einträge

void isr_UserB(){  // ISR für Helligkeit verändern
  if(stufe<anzahl-1) stufe++;
  else stufe = 0;
  analogWrite(LED,analogwert[stufe]); // Arduino-Funktion für PWM Ausgabe an bestimmten Port-Pins
  Serial.printf("Stufe %d Analogwert %d \n",stufe,analogwert[stufe]);
}
void setup(){
  Serial.begin (9600); // Serielle kommunikation starten
  pinMode(USER_B, INPUT);
  attachInterrupt (digitalPinToInterrupt (USER_B), isr_UserB, FALLING); // fallende Flanke
  pinMode(LED,OUTPUT);
  analogWriteResolution(16);
  //analogWriteFrequency(10000);
}
void loop(){}
Oszillogramm für 25% mit 16Bit
Oszillogramm für 25% mit 16Bit

PWM Frequenz ändern bei STM32

analogWriteFrequency(10000);

Die Ausgabefrequenz kann damit angepasst werden. Wie ist der Bereich? Wie genau ist die Ausgabe? Nachteilig ist, dass die Anpassungen immer für alle analogWrite(..) gelten. //github.com/stm32duino/Arduino_Core_STM32/wiki/API#analog🔗

Welche Ausgänge können PWM?

Nicht mit allen Ausgängen kann per Hardware PWM betrieben werden. Hier ein Überblick welche Ausgänge geeignet sind: mezmedia.de/etc/hardware/stm32-nucleo-l152re/

Umrechungsfunktion map(…)

Oft müssen Wertebereiche in andere Bereiche umgerechnet werden, es gibt bei Arduino eine Funktion dafür [www.arduino.cc/reference/en/language/functions/math/map/ 🔗] damit wäre eine einfache Lösung für 0..100% auf 0..255:

analogWert = map(helligkeit,0,100,0,255); // Arduino-Umrechnungsfunktion

Falls diese Funktion in der Formelsammlung auftaucht, dürfen Sie sie auch problemlos verwenden…

🐤 Bei Dir piepts wohl? Tonausgabe auf Piezo-Lautsprecher!

Piezo-Lautsprecher an PA7/D11
Piezo-Lautsprecher an PA7/D11

Ein Piezo-Lautsprecher📖 soll einen Ton mit Frequenz f = 440 Hz ausgeben. Er ist zwischen GND und PA7/D11 angeschlossen. Am Pin wird dazu ein Rechtecksignal📖 erzeugt. Wie lange ist die Periodendauer T des Signals? Wie lange ist der Pin 1 bzw. 0?
$ T = \frac{1}{f}= \frac{1}{440Hz} = 2.27 ms $
So könnte das realisiert werden:

Arduino tone()-Funktion

Synopsis: tone()-Funktion🔗

#define USER_B PC13 // User-Button prellfrei low aktiv
#define P_PIEZO PA7     // AusgabePin
#define MAX_A 255   // Maximaler Analogwert
volatile unsigned int note=0; // Globale Variable für Note
const unsigned int ton[] ={262,277,293,311,330,349,370,392,415,440,466,494}; // Frequenzen
int anzahl = sizeof(ton)/sizeof(ton[0]); // Anzahl der Array-Einträge

void isr_UserB(){  // ISR für Helligkeit verändern
  if(note<anzahl-1) note++;
  else note = 0;
  tone(P_PIEZO,ton[note]);
  Serial.printf("Note %d Analogwert %d \n",note,ton[note]);
}
void setup(){
  Serial.begin (9600); // Serielle kommunikation starten
  pinMode(USER_B, INPUT);
  attachInterrupt (digitalPinToInterrupt (USER_B), isr_UserB, FALLING); // fallende Flanke
}
void loop(){}

analogWrite() missbrauchen

#define USER_B PC13 // User-Button prellfrei low aktiv
#define P_PIEZO PA7     // AusgabePin
#define MAX_A 255   // Maximaler Analogwert
volatile unsigned int note=0; // Globale Variable für Note
const unsigned int ton[] ={262,277,293,311,330,349,370,392,415,440,466,494}; // Frequenzen
int anzahl = sizeof(ton)/sizeof(ton[0]); // Anzahl der Array-Einträge

void isr_UserB(){  // ISR für Helligkeit verändern
  if(note<anzahl-1) note++;
  else note = 0;
  analogWriteFrequency(ton[note]);
  analogWrite(P_PIEZO,MAX_A/2); // muss ausgegeben werden sonst ändert sich die Frequenz nicht.
  Serial.printf("Note %d Analogwert %d \n",note,ton[note]);
}
void setup(){
  Serial.begin (9600); // Serielle kommunikation starten
  pinMode(USER_B, INPUT);
  attachInterrupt (digitalPinToInterrupt (USER_B), isr_UserB, FALLING); // fallende Flanke
  analogWrite(P_PIEZO,MAX_A/2); // 50%
}
void loop(){}

Wie genau ist die Ausgabe?

STM32-Timer-Hardware für PWM verwenden

Synopsis: https://github.com/stm32duino/Arduino_Core_STM32/wiki/HardwareTimer-library🔗
An bestimmten Timer-Pins des µC können von der Timer-Hardware Signale ausgegeben werden. Dadurch muss sich nicht die CPU darum kümmern. So funktioniert es prinzipiell:

PWM mit Timer-Hardware
PWM mit Timer-Hardware

Den Timer bis zu einem Wert Overflow zählen lassen und dann wieder auf 0 springen, wie beim Interrupt. Der Overflow-Wert gibt die Periodendauer vor. Beim Sprung auf 0 den Ausgabe-Pin auf 1 schalten. Ein Vergleichsregister das CaptionCompareRegister (CRR) gibt den Wert vor bei dem der Ausgabe-Pin wieder auf 0 geschaltet wird, es gibt die Einschaltdauer, Impulsdauer vor. Das Verhältnis der Impulsdauer zur Periodendauer ist der Tastgrad (engl. dutycycle). 50% dutycycle bedeutet 50% der Zeit ist der Ausgabepin 1 50% 0.
Der Lautsprecher ist an PA7/D11 angeschlossen. Welcher Timer und welcher Kanal kommt dafür in Frage? Gewählt: TIM3_CH2.

#define USER_B PC13 // User-Button prellfrei low aktiv
#define P_PIEZO PA7     // AusgabePin
static HardwareTimer mytimer = HardwareTimer(TIM3);  // Timerinstanz sowie Timerauswahl
# define KANAL 2        // CH2
int einschaltdauer = 50;// 50% dutycycle halbe Zeit an halbe aus
volatile unsigned int note=0; // Globale Variable für Note
const unsigned int ton[] ={262,277,293,311,330,349,370,392,415,440,466,494}; // Frequenzen
int anzahl = sizeof(ton)/sizeof(ton[0]); // Anzahl der Array-Einträge

void isr_UserB(){  // ISR für Helligkeit verändern
  if(note<anzahl-1) note++;
  else note = 0;
  mytimer.setPWM(KANAL,P_PIEZO,ton[note],einschaltdauer);
  Serial.printf("Note %d Analogwert %d \n",note,ton[note]);
  Serial.printf("Prescaler %5d Overflow %5d Compare %5d\n",mytimer.getPrescaleFactor(),mytimer.getOverflow(),mytimer.getCaptureCompare(KANAL));
}
void setup(){
  Serial.begin (9600); // Serielle kommunikation starten
  pinMode(USER_B, INPUT);
  attachInterrupt (digitalPinToInterrupt (USER_B), isr_UserB, FALLING); // fallende Flanke
}
void loop(){}

Am seriellen Monitor werden neben der Tonfrequenz auch die Einstellungen ausgegeben: Prescaler 2 Overflow 35555 Compare 17777
Prima, die API nimmt uns die Rechenarbeit ab.

PWM mit 0,1,99 und 100% ?

#include <Arduino.h>
static HardwareTimer mytimer = HardwareTimer(TIM3);  // Timerinstanz sowie Timerauswahl

#define USER_BTN PC13  // Entpreller lowaktiver UserButton auf dem Board
#define P_PIEZO PA7  // Lautsprecher an PA7/D11 -> TIM3_CH2
#define KANAL 2        // CH2
int prozent = 50;// 50% dutycycle halbe Zeit an halbe aus
int frequenz = 440;     // 440 Hz
void ausgebenTimerWerte(){
  Serial.printf("Prescaler %5d Overflow %5d Compare %5d\n",mytimer.getPrescaleFactor(),mytimer.getOverflow(),mytimer.getCaptureCompare(KANAL));
}
void isr_userB(){  // Werte verändern und testen
  switch (prozent){
    case 0: prozent=1; break;
    case 1: prozent=99; break;
    case 99: prozent=100; break;
    default: prozent=0;
  }
  mytimer.setCaptureCompare(KANAL,prozent,PERCENT_COMPARE_FORMAT); // Impulsbreite einstellen
  ausgebenTimerWerte();
}
void setup() {
  Serial.begin (9600); //Serielle Kommunikation starten
  pinMode(USER_BTN, INPUT);
  attachInterrupt (digitalPinToInterrupt (USER_BTN), isr_userB, FALLING);
  mytimer.setPWM(KANAL, P_PIEZO, frequenz, prozent);
  ausgebenTimerWerte();
}
void loop() {
}

Bei Impulsbreite 0% ist der PWM-Ausgang dauerhaft low, bei Impulsbreite 100% ist der PWM-Ausgang dauerhaft high. Betrachten Sie auch die Ausgaben auf dem Seriellen Monitor.

PWM mit eigener Software

Wenn die Ausgabe nicht mit der Timerhardware möglich ist, kann das Signal auch selbst erzeugt werden.

ToDo: Beispiel für Motoransteuerung bei RoboCar..

🦆 Alle meine Entchen spielen

Die Noten in eine Tonhöhe also Frequenz umsetzen. Hier eine mögliche Umsetzung [Gleichstufige Stimmung]:

index0123456789101213
Tonc‘cis‘d‘es‘e‘f‘fis‘g‘as‘a‘b‘h‘c“
Frequenz262277293311330349370392415440466494523
Frequenzen der Töne
#include <Arduino.h>
static HardwareTimer mytimer = HardwareTimer(TIM3);  // Timerinstanz sowie Timerauswahl

#define USER_BTN PC13  // Entpreller lowaktiver UserButton auf dem Board
#define PIEZO_PIN PA7  // Lautsprecher an PA7/D11 -> TIM3_CH2
#define KANAL 2        // CH2
#define HUELLKURVE 0   // besserer Sound mit Hüllkurve
//const unsigned int ton[] ={264,275,297,317,330,352,367,396,422,440,475,495}; // Frequenzen
const unsigned int ton[] ={262,277,293,311,330,349,370,392,415,440,466,494}; // Frequenzen
const unsigned char kurve[]  = {25,13,8,4,2,1,0}; // Huellkurve
const unsigned char melodie[]  ={0,2,4,5,7,7,9,9,9,9,7,9,9,9,9,7,5,5,5,5,4,4,7,7,7,7,0}; // alle meine Entchen
const unsigned char lange[]    ={2,2,2,2,4,4,2,2,2,2,4,2,2,2,2,4,2,2,2,2,4,4,2,2,2,2,4}; // Tonlaengen

void isr_userB(){  // Abschalten können!
  static bool spielt = true;
  if (spielt){
    mytimer.pause();
    spielt=false;
  } else{
    mytimer.resume();
    spielt=true;
  }
}
void setup() {
  pinMode(USER_BTN, INPUT);
  attachInterrupt (digitalPinToInterrupt (USER_BTN), isr_userB, FALLING);
  mytimer.setPWM(KANAL, PIEZO_PIN, 440, 50); // Initialisierung mit 440 Hz
}
void loop() {
  unsigned char d,i,j,l;
  unsigned int k;
  for (i=0;i<sizeof(melodie);i++){ // spiele die Melodie
    k = ton[melodie[i]]; // hole die Frequenz
    mytimer.setOverflow(k,HERTZ_FORMAT);  // Frequenz einstellen
    mytimer.setCaptureCompare(KANAL,50,PERCENT_COMPARE_FORMAT); // Signalbreite einstellen
    delay(20);
#if HUELLKURVE == 1    // mit Hüllkurve
    for(j=0;j<sizeof(kurve);j++){ // Huellkurve anwenden: Signalbreite schmäler
       mytimer.setCaptureCompare(KANAL,kurve[j],PERCENT_COMPARE_FORMAT);
      for (d=0;d<lange[i];d++){ // Tonlaenge
        delay(20);
      }
    }
 #else                 // ohne Hüllkurve
    for (d=0;d<lange[i];d++){ // Tonlaenge
      delay(100);
    }
    mytimer.setCaptureCompare(KANAL,0,PERCENT_COMPARE_FORMAT);
    delay(80);          // Pause bis naechster Ton
  #endif  
  }
  delay(300);  // Pause bis Melodie wieder startet
}

Mit Hüllkurve hört es sich besser an: #define HUELLKURVE 1 setzen.
Durch eine geringere Pulsbreite kann die Lautsprecherausgabe leiser werden..
Wie ich Sie kenne werden Sie nun begeistert mit neuen Melodien den Unterrichtsraum beglücken 😜.

🎸 Gitarrenstimmhilfe (HP15-2)

Zum Stimmen von Gitarren soll ein µC Referenzfrequenzen an PA7/D11 ausgeben. Das Signal entspricht den Tonfrequenzen der sechs Gitarrensaiten. In der Tabelle sind die Frequenzen und die Ausgaben auf der 7-Segment-LED-Anzeige aufgeführt.

Gitarrenstimmhilfe
Gitarrenstimmhilfe

Die Tonauswahl soll über den prellfreien Low-aktiven Taster an PC13 erfolgen. Der Reset-Taster setzt die Stimmhilfe wieder zurück. Nach Einschalten/Reset wird kein Ton ausgegeben und in der 7 Segment Anzeige leuchtet „-„. Mit jedem Tastendruck auf PC13 wird der jeweils nächste Ton ausgewählt, dies geschieht mit einer ISR. Nach e‘ soll die Auswahl wieder mit E beginnen:
(Ruhe->E->A->d->g->h->e‘->E->…).

Töne genau erzeugen, Prescaler und und Overflow ermitteln

Viele Töne (E,d,h,e‘) lassen sich nicht mit dem Herz-Format genau einstellen, da als Eingabe ganze Zahlen vorgesehen sind. Daher wird im Tick-Format Prescaler und Overflow verwendet. Hier eine Beispielrechnung wie die Werte ermittelt werden können:

  1. $ \textbf{Taktzahl} = TimerCLK / Tonfrequenz = 32MHz / 82,41Hz = 388302,39 $
  2. $ \textbf{Prescaler: } Taktzahl / MaxOverflow = {388302,39 / 65536} = 5,92 \textbf{ Aufrunden: 6} $
  3. $ \textbf{Overflow: } Taktzahl / Prescaler = 388302,39 / 6 = \textbf{64717} $
  4. $ \textbf{Probe: } TimerCLK / Prescaler / Overflow = 32MHz / 6 / 64717 = \textbf{82,41Hz} $

✍️ Ermitteln Sie die zu erwartenden Werte für Prescaler und Overflow für die anderen Frequenzen.
Tipp: 🖥 Verwenden Sie eine Tabellenkalkulation.
Der Prescaler-Wert ändert sich, es müssten zwei Felder verwendet werden um die Einstellungen zu speichern. Wie genau wären die Frequenzen, wenn der Prescaler-Wert von 82,41 Hz beibehalten und nur die Overflow-Werte geändert werden würden?
✍️ 🖥 Ermitteln Sie die Overflow-Werte für die Frequenzen und bestimmen Sie die Ausgabefrequenz.

Lösung
Einstellungen für Prescaler und Overflow
Einstellungen für Prescaler und Overflow

Werte für 7 Segmentanzeige bestimmen

Bestimmen sie die Werte für die Ausgabe auf der 7 Segmentanzeige, füllen Sie die Tabelle aus:

Sieben Segment Anzeige
Sieben Segment Anzeige
Ausgabeg PC6 f PC5e PC4d PC3c PC2b PC1a PC0Hex
10000000x40
E
A
d
g
h
e
Werte für 7-Segment Anzeige
Lösung Umwandlung für 7-Segment Anzeige
Ausgabeg PC6 f PC5e PC4d PC3c PC2b PC1a PC0Hex
10000000x40
E11110010x79
A11101110x77
d10111100x5e
g11011110x6f
h11101000x74
e11110110x7b
Werte für 7Segment Anzeige

Code Vorgabe vervollständigen

#include <Arduino.h>
static HardwareTimer mytimer = HardwareTimer(TIM3);  // Timerinstanz sowie Timerauswahl

#define USER_BTN PC13  // Entpreller lowaktiver UserButton auf dem Board
#define PIEZO_PIN PA7  // Lautsprecher an PA7/D11 -> TIM3_CH2
#define KANAL 2        // CH2

const unsigned char anzeige[] ={...}; // 7Segmentausgaben für E..e
const unsigned int ton_overflow[] ={64717,...}; // Overflow-Werte für E..e
unsigned char ton=0;

void isr_userB(){  // nächsten Ton ausgeben
  GPIOC->ODR = anzeige[ton]; // ton auf 7Seg. ausgeben
  digitalWrite(PC11,HIGH);   // Einer einschalten
  mytimer.setOverflow(ton_overflow[ton], TICK_FORMAT); // Frequenz einstellen
  mytimer.setCaptureCompare(KANAL,50,PERCENT_COMPARE_FORMAT); // Impulsbreite einstellen 0..100
  mytimer.resume();  // Timer aktivieren
  if (ton>=5) ton=0;
  else ton++;
}
void setup() {
  pinMode(PC0, OUTPUT);  // ohne diese Zeile klappts nicht
  GPIOC->MODER = 0x5555; // PC0..PC7 als Ausgang
  pinMode(PC11,OUTPUT); // Einer Ausgang
  pinMode(USER_BTN, INPUT);
  attachInterrupt (digitalPinToInterrupt (USER_BTN), isr_userB, FALLING);
  GPIOC->ODR = 0x40; // - ausgeben
  digitalWrite(PC11,HIGH);  // Einer einschalten
  mytimer.setPrescaleFactor(?);  // Prescaler einstellen
  mytimer.setMode(KANAL, TIMER_OUTPUT_COMPARE_PWM1, PIEZO_PIN); // Den PWM-Ausgang einstellen
}
void loop() {
}
Lösungsvorschlag
#include <Arduino.h>
static HardwareTimer mytimer = HardwareTimer(TIM3);  // Timerinstanz sowie Timerauswahl

#define USER_BTN PC13  // Entpreller lowaktiver UserButton auf dem Board
#define PIEZO_PIN PA7  // Lautsprecher an PA7/D11 -> TIM3_CH2
#define KANAL 2        // CH2

const unsigned char anzeige[] ={0x79,0x77,0x5e,0x6f,0x74,0x7b}; // 7Segmentausgaben für E..e
const unsigned int ton_overflow[] ={64717,48485,36323,27211,21598,16180}; // Overflow-Werte für E..e
unsigned char ton=0;

void isr_userB(){  // nächsten Ton ausgeben
  GPIOC->ODR = anzeige[ton]; // ton auf 7Seg. ausgeben
  digitalWrite(PC11,HIGH);   // Einer einschalten
  mytimer.setOverflow(ton_overflow[ton], TICK_FORMAT); // Frequenz einstellen
  mytimer.setCaptureCompare(KANAL,50,PERCENT_COMPARE_FORMAT); // Impulsbreite einstellen 0..100
  mytimer.resume();  // Timer aktivieren
  if (ton>=5) ton=0;
  else ton++;
}
void setup() {
  pinMode(PC0, OUTPUT);  // ohne diese Zeile klappts nicht
  GPIOC->MODER = 0x5555; // PC0..PC7 als Ausgang
  pinMode(PC11,OUTPUT); // Einer Ausgang
  pinMode(USER_BTN, INPUT);
  attachInterrupt (digitalPinToInterrupt (USER_BTN), isr_userB, FALLING);
  GPIOC->ODR = 0x40; // - ausgeben
  digitalWrite(PC11,HIGH);  // Einer einschalten
  mytimer.setPrescaleFactor(6);  // Prescaler einstellen
  mytimer.setMode(KANAL, TIMER_OUTPUT_COMPARE_PWM1, PIEZO_PIN); // Den PWM-Ausgang einstellen
}
void loop() {
}