1.6 ⏱️ Frequenz, Perioden- und Impulsdauer messen

[de.wikipedia.org/wiki/Frequenz 🔗]

Die Frequenz f eines Signals ist die Anzahl der Wiederholungen pro Sekunde.
Im Beispiel ein Rechtecksignal. Die Periodendauer T ist dabei z.B. die Zeit von steigender zur steigender Flanke.

Um die Frequenz zu messen kann z.B. die Anzahl der steigenden Taktflanken in einer Sekunde gezählt werden.
Bei niedrigen Frequenzen wird eher die Periodendauer T gemessen.
Oft ist auch die Länge des positiven und negativen Impulses von Interesse.

Auf dem Baseshield ist ein Frequenzgenerator mit NE555 verbaut, dessen Frequenz wir nun messen wollen. Dazu muss neben der Taste PA10 ein Jumper gesetzt werden, damit der Ausgang mit PA1 verbunden wird. Mit dem Poti unter dem LCD-Display kann die Frequenz verändert werden.

Frequenz und Periodendauer
Frequenz und Periodendauer

Frequenz messen

  • Eine ISR zählt in einer Variablen zaehler die Positiven Taktflanken
  • Ein Timer löst jede Sekunde eine ISR aus:
    • Der Wert von zaehler enthält die Frequenz und wird gespeichert
    • zaehler wird wieder auf 0 gesetzt.

Vervollständigen Sie den Vorgabecode…

Frequenz messen
Frequenz messen
#define P_FREQUENZ PA1 // Frequenz zu messen
static HardwareTimer mytimer = HardwareTimer(TIM3);  // Timerinstanz sowie Timerauswahl
volatile unsigned long zaehler=0;  // Flanken zählen
volatile unsigned long frequenz=0; // ermittelte Frequenz

void isr_Messen(){    // Jede Sekunde aufrufen
  ...
  Serial.printf("Frequenz %d Hz \n",frequenz);
}
void isr_Zaehlen(){   // zählt bei jeder steigenden Taktflake
  ...
}
void setup(){
  Serial.begin (9600); //Serielle kommunikation starten
  pinMode(P_FREQUENZ, INPUT);
  attachInterrupt (digitalPinToInterrupt (P_FREQUENZ), isr_Zaehlen, RISING); // steigende Taktflanken zählen
  ...                                  // ISR jede Sekunde
  mytimer.attachInterrupt(isr_Messen); // Timer ISR einstellen
  mytimer.resume();  // Timer aktivieren
}
void loop(){}
Lösung
#define P_FREQUENZ PA1 // Frequenz zu messen
static HardwareTimer mytimer = HardwareTimer(TIM3);  // Timerinstanz sowie Timerauswahl
volatile unsigned long zaehler=0;  // Flanken zählen
volatile unsigned long frequenz=0; // ermittelte Frequenz

void isr_Messen(){    // Jede Sekunde aufrufen
  frequenz = zaehler; // gemessene Frequenz sichern
  zaehler=0;
  Serial.printf("Frequenz %d Hz \n",frequenz);
}
void isr_Zaehlen(){   // zählt bei jeder steigenden Taktflake
  zaehler++;
}
void setup(){
  Serial.begin (9600); //Serielle kommunikation starten
  pinMode(P_FREQUENZ, INPUT);
  attachInterrupt (digitalPinToInterrupt (P_FREQUENZ), isr_Zaehlen, RISING); // steigende Taktflanken zählen
  mytimer.setOverflow(1000000, MICROSEC_FORMAT);// ISR jede Sekunde
  mytimer.attachInterrupt(isr_Messen); // Timer ISR einstellen
  mytimer.resume();  // Timer aktivieren
}
void loop(){}

Periodendauer messen

Bei niedrigen Frequenzen wird die Zeit zwischen den Flanken gemessen.
Dazu wäre eine gute Zeitbasis z.B. µs zählen praktisch. Ich möchte 2 Lösungsansätze vorstellen.

  1. Einen Timer mit 1 MHz laufen lassen und dessen Zählwert verwenden.
  2. Die Arduino-Funktion micros() verwenden, wie funktioniert sie genau?
  3. Die Hardware die Periodendauer messen lassen, kenne ich von AVR, müsste mit STM32 auch möglich sein?
Periodendauer messen
Periodendauer messen

P1. Timer mit 1 MHz zum Messen laufen lassen

Notwendige Befehle stehen nicht in der Formelsammlung. Infos siehe:
[github.com/stm32duino/Arduino_Core_STM32/wiki/HardwareTimer-library]

  • Ein Timer zählt mit 1 MHz die Mikrosekunden (CNT).
  • Der Overflowwert (ARR) ist maximal eingestellt, damit er nicht vorzeitig auf 0 gesetzt wird. (Prüfen, ob das notwendig ist)
  • Eine ISR wird bei steigender Taktflanke von PA1 ausgelöst.
    • Dabei wird der Timerwert in einer Variable periode gespeichert.
    • Der Timerwert wird wieder auf 0 gesetzt.

❓ Welche maximale Periodendauer kann so gemessen werden?

Lösung

Bei einem 16Bit Counter 216-1 µs = 65535 µs

#define P_FREQUENZ PA1 // Frequenz zu messen
static HardwareTimer mytimer = HardwareTimer(TIM3);  // Timerinstanz sowie Timerauswahl
volatile unsigned long periode=0; // ermittelte Periodendauer

void isr_Periode(){ // bei steigendenr Taktflake aufrufen
  periode= mytimer.getCount(); // Timer Wert speichern
  mytimer.setCount(0); // Timer auf 0 setzen
}
void setup(){
  Serial.begin (9600); //Serielle Kommunikation starten
  pinMode(P_FREQUENZ, INPUT);
  attachInterrupt (digitalPinToInterrupt (P_FREQUENZ), isr_Periode, RISING); // steigende Taktflanken zählen
  mytimer.setOverflow(0x10000);// auf Maximum setzen
  mytimer.setPrescaleFactor(32); // 32MHz Frequenz durch 32 Teilen -> 1µs
  mytimer.resume(); // Timer starten
}
void loop(){
  Serial.printf("Periodendauer %d µs \n",periode);
  delay(1000);
}

P2. Mit micros() Zeiten messen

micros() steht nicht in der Formelsammlung Info: [arduino.cc/reference/en/language/functions/time/micros/]

micros() zählt die Mikrosekunden seit Systemstart in einer uint32_t Variablen. Beim STM32 werden dabei SystemTicks verwendet. [www.mystm32.de/doku.php?id=systemtickc 🔗]

Info: getCurrentMicros()
uint32_t getCurrentMicros(void){
  uint32_t m0 = HAL_GetTick();
  __IO uint32_t u0 = SysTick->VAL;
  uint32_t m1 = HAL_GetTick();
  __IO uint32_t u1 = SysTick->VAL;
  const uint32_t tms = SysTick->LOAD + 1;

  if (m1 != m0) {
    return (m1 * 1000 + ((tms - u1) * 1000) / tms);
  } else {
    return (m0 * 1000 + ((tms - u0) * 1000) / tms);
  }
}
  1. ❓Nach welcher Zeit springt der micros() wieder auf 0?
  2. ❓Welche Auswirkung hat dies auf die Messung wenn der Sprung auf 0 während der Messung geschieht?
Lösung
  1. Datentyp unsigned long ist uint32_t: 32 Bit ohne Vorzeichen. 232 µs / 106 / 60 = 71,58 min = 1,19 h
  2. Es wäre zeitneu < zeitmarke, somit würde eine negative periode ausgerechnet aber als vorzeichenlose Zahl interpretiert werden.
#define P_FREQUENZ PA1 // Frequenz zu messen
volatile unsigned long periode = 0; // Periodendauer

void isr_Periode(){ // bei steigendenr Taktflake aufrufen
  static unsigned long zeitmarke = 0; // persistente Variable
  unsigned long zeitneu = micros();   // Zeit jetzt
  periode = zeitneu-zeitmarke; // Periodendauer speichern
  zeitmarke = zeitneu;
}
void setup(){
  Serial.begin (9600); // Serielle Kommunikation starten
  pinMode(P_FREQUENZ, INPUT);
  attachInterrupt (digitalPinToInterrupt (P_FREQUENZ), isr_Periode, RISING); // steigende Taktflanken
}
void loop(){
  Serial.printf("Periodendauer %d µS \n",periode);
  delay(1000);
}

P3. Mit Timer-Hardwarefunktion in CaptureCompare-Register messen

Beispiel aus der STM32-Lib: InputCapture.ino🔗
HardwareTimer.cpp🔗

Die PWM-Kanäle-Pins des Timers können auch als Auslöser zur Aufnahme (Capture) des aktuellen Zählerstandes in ein CaptureCompare-Register verwendet werden. Wenn z.B. eine steigende Flanke passiert wird ein „Foto“ des aktuellen Zählerstandes gespeichert. Da der Zähler weiter zählt muss sich auch gemerkt werden, ob zwischen zwei Aufnahmen ein Überlauf statt gefunden hat.

Der Frequenzgenerator auf dem SturmBoard ist über einen Jumper an PA1 angeschlossen. In der Formelsammlung oder hier nach dem Timer und dem dazugehörigen Kanal suchen: TIM2 Kanal 2 oder TIM5 Kanal 2 sind möglich.

Mögliche Auslöseeinstellungen für setMode():

  • TIMER_INPUT_CAPTURE_RISING
  • TIMER_INPUT_CAPTURE_FALLING
  • TIMER_INPUT_CAPTURE_BOTHEDGE
  • TIMER_INPUT_FREQ_DUTY_MEASUREMENT

Hinweis: In setMode(..) wird pinMode(pin,INPUT) aufgerufen. Falls PullUP/PullDown eingeschaltet werden sollen muss dies danach gemacht werden.

#define P_FREQUENZ PA1 // TIM2 Kanal 2 oder TIM5 Kanal 2
#define KANAL 2
static HardwareTimer mytimer = HardwareTimer(TIM2);  // Timerinstanz sowie Timerauswahl
volatile uint32_t periode=0; // ermittelte Periodendauer
volatile uint32_t letzteAufnahme=0; // letzer Wert
volatile uint32_t ueberlauf=0;     // gab es Überlauf?

void isr_Capture(){ // bei Auslösung aufrufen
  uint32_t aufnahme = mytimer.getCaptureCompare(KANAL);
  if (aufnahme > letzteAufnahme){
    periode = aufnahme - letzteAufnahme;
  }
  else{
    periode = 0x10000 + aufnahme - letzteAufnahme; 
  }
  letzteAufnahme = aufnahme;
  ueberlauf = 0;
}
void isr_Overflow(){ // wenn der Zähler überläuft
  ueberlauf++;
  if (ueberlauf > 1) periode = 0; // bei mehr Überläufen stimmt die Rechnung nicht mehr
}
void setup(){
  Serial.begin (9600); //Serielle Kommunikation starten
  mytimer.setOverflow(0x10000);// auf Maximum setzen
  mytimer.setPrescaleFactor(32); // 32MHz Frequenz durch 32 Teilen -> 1µs
  mytimer.attachInterrupt(isr_Overflow); // ISR für den Timer-Überlauf
  mytimer.setMode(KANAL,TIMER_INPUT_CAPTURE_RISING,P_FREQUENZ); // den Auslöser einstellen
  mytimer.attachInterrupt(KANAL,isr_Capture); // ISR für Aufnahme bearbeiten
  mytimer.resume(); // Timer starten
}
void loop(){
  Serial.printf("Periodendauer %d µs \n",periode);
  delay(1000);
}
VorhabenSyntaxErläuterungen
Kanal für Input Capture einstellenmytimer.setMode(channel,mode,pin);channel gibt den Timer-Kanal vor: 1..4
mode gibt die Auslösebedingung vor:
TIMER_INPUT_CAPTURE_RISING
TIMER_INPUT_CAPTURE_FALLING
TIMER_INPUT_CAPTURE_BOTHEDGE
TIMER_INPUT_FREQ_DUTY_MEASUREMENT
pin gibt den Pinnamen an, Bsp: PA1
ISR bei Capture-Ereignis ausführenmytimer.attachInterrupt(channel,isr);channel gibt den Timer-Kanal vor: 1..4
isr ist aufzurufende ISR, Bsp: isr_Capture
Capture-Wert lesenn = mytimer.getCaptureCompare(channel);channel gibt den Timer-Kanal vor: 1..4

Impulsdauer messen

Nun soll die Impulsdauer des positiven und negativen Impulses gemessen werden, also wie lange ist das Signal High und wie lange ist es Low.
Es gibt 4 Möglichkeiten dies zu tun:

  1. Arduino pulseIn() Funktion
  2. Impulsdauer mit ext. ISR und micros() messen
  3. Impulsdauer mit ext. ISR und Timer messen
  4. 🤯 Impulsdauer mit Timer-Hardwarefunktion messen

I1. Arduino pulseIn() Funktion

Die Arduino-API bietet eine Funktion pulseIn() an:
www.arduino.cc/reference/en/language/functions/advanced-io/pulseIn/ 🔗
Arduino pulseIn() function | roboticsbackend🔗
Hier ein Programm, dass mit pulseIn() die positive und negative Impulslänge im Sekundentakt auf der seriellen Schnittstelle ausgibt.

#define P_FREQUENZ PA1 // Frequenz zu messen
unsigned long impulsdauerPos=0; // positive Impulsdauer
unsigned long impulsdauerNeg=0; // negative Impulsdauer

void setup(){
  Serial.begin (9600);
  pinMode(P_FREQUENZ, INPUT);
}
void loop(){
  impulsdauerPos=pulseIn(P_FREQUENZ,HIGH);
  impulsdauerNeg=pulseIn(P_FREQUENZ,LOW);
  Serial.printf("Impulsdauer pos %d µs neg %d µs\n",impulsdauerPos,impulsdauerNeg);
  delay(1000);
}

Schauen Sie sich die Doku und Definition von pulseIn() an (rechte Maus und „Go to Definition“).

  1. ❓ Blockiert pulseIn() den Programmablauf?
  2. ❓ Es gibt einen optionalen dritten Parameter timeout. Welcher Wert wird beim Überschreiten der timeout-Zeit zurück gegeben?
    Welchen Default-Wert hat dieser Parameter und wie lange ist dadurch die längste Zeit, die ohne Angabe dieses Parameters gemessen werden kann?
  3. ❓ Welche zeitbestimmende Funktion wird in pulseIn() verwendet und welche Auswirkung hat dies auf die Genauigkeit der Messung?
Lösungen
  1. Ja, solange die Messung stattfindet.
  2. Beim Überschreiten von timeout wird 0 zurück gegeben.
    Der Default-Wert von timeout ist 1000000. Die längste Messzeit ist dabei 1 Sekunde.
  3. pulseIn() verwendet micros() und ist dabei auf die Genauigkeit dieser Funktion angewiesen, hängt vom µC ab
    https://docs.arduino.cc/language-reference/en/functions/time/micros/🔗

I2. Impulsdauer mit ext. ISR und micros() messen

Die Impulsdauern eines Signals ohne Unterbrechungen zu messen ist nicht schwer:
Ein externer Interrupt wird bei jeder Flanke des Signals ausgelöst und die vergangene Zeit seit der letzen Flanke der jeweiligen Impulsdauer zugeordnet.
🖥 Vervollständigen Sie den vorgegebenen Code:

#define P_FREQUENZ PA1 // Frequenz zu messen
volatile unsigned long impulsdauerPos=0; // Dauer positiver Impuls
volatile unsigned long impulsdauerNeg=0; // Dauer negativer Impuls

void isr_Flankenwechsel(){ // bei jeder Flanke aufgerufen
  static unsigned long zeitmarke = 0;   // lokale persistente Zeitmarke
  unsigned long zeitneu = micros();     // Zeit jetzt gleich festhalten
  ...
}
void setup(){
  Serial.begin (9600);
  pinMode(P_FREQUENZ, INPUT);
  attachInterrupt (digitalPinToInterrupt (P_FREQUENZ), isr_Flankenwechsel, CHANGE); // wechselnde Flanken
}
void loop(){
  Serial.printf("Impulsdauer pos %d µs neg %d µs\n",impulsdauerPos,impulsdauerNeg);
  delay(1000);
}
Lösung Code
#define P_FREQUENZ PA1 // Frequenz zu messen
volatile unsigned long impulsdauerPos=0; // Dauer positiver Impuls
volatile unsigned long impulsdauerNeg=0; // Dauer negativer Impuls

void isr_Flankenwechsel(){ // bei jeder Flanke aufgerufen
  static unsigned long zeitmarke = 0;   // lokale persistente Zeitmarke
  unsigned long zeitneu = micros();     // Zeit jetzt gleich festhalten
  if(digitalRead(P_FREQUENZ)){        // war steigende Flanke
    impulsdauerNeg = zeitneu-zeitmarke; // Dauer negativer Impuls
  }else{                                // war fallende Flanke
    impulsdauerPos=zeitneu-zeitmarke;   // Dauer positiver Impuls
  }
  zeitmarke=zeitneu;
}
void setup(){
  Serial.begin (9600); //Serielle kommunikation starten
  pinMode(P_FREQUENZ, INPUT);
  attachInterrupt (digitalPinToInterrupt (P_FREQUENZ), isr_Flankenwechsel, CHANGE); // wechselnde Flanken
}
void loop(){
  Serial.printf("Impulsdauer pos %d µs neg %d µs\n",impulsdauerPos,impulsdauerNeg);
  delay(1000);
}
  1. ❓ Angenommen, die Werte der Impulsdauern schwanken innerhalb einer Sekunde, welche Werte werden jeweils ausgegeben?
  2. ❓ Wie genau sind die Werte der Impulsdauern, wovon hängt die Genauigkeit ab?
Lösungen
  1. Es wird jede Sekunde jeweils der letzte Wert der Messung ausgegeben.
  2. Die Genauigkeit hängt der Zeit bis in die ISR gesprungen wird und von der Genauigkeit der micros() Funktion ab.

I3. Impulsdauer mit ext. ISR und Timer messen

Um die Ungenauigkeit von micros() zu vermeiden kann als Zeitbasis ein Timer mit 1 µs Takt verwendet werden.
🖥 Erstellen Sie eine Lösung dafür. Tipp: Timer mit 1 MHz zum Messen laufen lassen

Lösungsvorschlag Code
#define P_FREQUENZ PA1 // Frequenz zu messen
static HardwareTimer mytimer = HardwareTimer(TIM3);  // Timerinstanz sowie Timerauswahl
volatile unsigned long periode = 0;      // Periodendauer
volatile unsigned long impulsdauerPos=0; // Dauer positiver Impuls
volatile unsigned long impulsdauerNeg=0; // Dauer negativer Impuls

void isr_Flankenwechsel(){ // bei jeder Flanke aufgerufen
  static unsigned long zeitmarke = 0;      // lokale persistente Zeitmarke
  unsigned long zeit = mytimer.getCount(); // Zeit jetzt gleich festhalten
  if(digitalRead(P_FREQUENZ)){           // war steigende Flanke
    mytimer.setCount(0);                   // Timer auf 0 setzen
    impulsdauerNeg = zeit-zeitmarke;       // Dauer negativer Impuls
    periode = zeit;                        // Periodendauer
  }else{                                   // war fallende Flanke
    impulsdauerPos=zeit;                   // Dauer positiver Impuls
    zeitmarke=zeit;                        // Zeit merken
  }
}
void setup(){
  Serial.begin (9600); //Serielle Kommunikation starten
  pinMode(P_FREQUENZ, INPUT);
  attachInterrupt (digitalPinToInterrupt (P_FREQUENZ), isr_Flankenwechsel, CHANGE); // steigende Taktflanken zählen
  mytimer.setOverflow(0xFFFF,TICK_FORMAT);// auf Maximum setzen
  mytimer.setPrescaleFactor(32); // 32MHz Frequenz durch 32 Teilen -> 1µs
  mytimer.resume(); // Timer starten
}
void loop(){
  Serial.printf("Impulsdauer pos %d µs neg %d µs Summe %d Periodendauer %d\n",impulsdauerPos,impulsdauerNeg,(impulsdauerPos+impulsdauerNeg),periode);
  delay(1000);
}
  1. ❓ Welche maximalen Werte können ermittelt werden?
  2. ❓ Welche Faktoren beeinträchtigen die Genauigkeit der Messung?
Lösungen
  1. Bei einem 16Bit-Timer 216 µs.
  2. Die Zeit zwischen Flankenwechsel und Einlesen des Timers, die Zeit bis zum Zurücksetzen des Timers bei steigender Flanke.

I4. 🤯 Impulsdauer mit Timer-Hardwarefunktion messen

Zum genauen Messen von Frequenzen und Periodendauern kann bei vielen µC die Timerhardware in einen entsprechenden Modus gebracht werden. Dabei wird ohne Umweg über ISRs der Zählerstand durch den Zustand eines Eingangspins von der Timerhardware gesichert und kann dann in der Software ausgewertet werden.
In der Doku der STM32-API findet sich Frequency_Dutycycle_measurement.ino🔗 wie funktioniert das?

🔊 Entfernung messen mit Ultraschallsensor HC-SR04 (HP17-2)

ToDo: Bilder von den Sensoren und der Rückseite machen!
Auf Funduino.de gibt es einen Sketch Nr.11 Entfernung messen, diesen habe ich für unser Board mit dem STM32 angepasst. Auf Seite 10 der Boardbeschreibung wird gezeigt wie ein Ultraschallmodul HC-SR04 verwendet werden kann. An einem Trigger-Pin wird ein mindestens 10µs langes Startsignal angelegt und nach kurzer Zeit erhält man am Echo-Pin ein Signal dessen Dauer proportional zur Entfernung ist. Links auf mezdata.de zum Sensor.

Oszillogramm Funduino-Modul
Oszillogramm Funduino-Modul
Oszillogramm Amazon-Modul
Oszillogramm Amazon-Modul

Messung mit dem Funduinomodul:
C1 (Channel1) , Gelb ist das Triggersignal, C2, blau das Echosignal. Entfernung ca. 1m.
Gefahr für den STM durch zu hohe Ausgangsspannung des Echo-Signals? Der STM arbeitet mit 3,3V und das Signal hat 5V, es könnte der Eingang beschädigt werden? Entwarnung: Habe zwischen Modul und Eingang einen 10kΩ Widerstand geschaltet und das Signal zeigt die Spannung am Eingang: 5,14V also fließt kein gefährlicher Strom.
Messung mit dem Amazonmodul:
Echosignal kommt später und hat nur 3,4V
Ist wohl ein anderer µC verbaut..

Sensor testen mit Seriellem Plotter

#define TRIGGER PA10 // Pin zum Auslösen der Messung
#define ECHO PC9     // Rückgabepin für Impulslänge
unsigned long dauer=0;        // long sind 32 Bit für Messung der Impulslänge in µs
unsigned long entfernung=0;   // in mm
void setup(){
  Serial.begin (9600);
  pinMode(TRIGGER, OUTPUT); // Trigger-Pin ist ein Ausgang
  pinMode(ECHO, INPUT);     // Echo-Pin ist ein Eingang
}
void loop(){
  digitalWrite(TRIGGER, HIGH);
  delayMicroseconds(20);       // 10 µs minimal, 20 µs zur Sicherheit
  digitalWrite(TRIGGER, LOW);  // Jetzt fängt die Messung an
  dauer = pulseIn(ECHO, HIGH); // Mit dem Befehl „pulseIn“ zählt der Mikrokontroller die Zeit in Mikrosekunden, bis der Schall zum Ultraschallsensor zurückkehrt.
  //entfernung = (dauer/2) * 0.03432; // numerisch nicht geschickte Berechnung in cm
  entfernung = dauer*0.3434/2; // Berechnung in mm
  //Serial.printf("Dauer: %5d µs, Entfernung: %d mm\n",dauer,entfernung); // Ausgabe serieller Monitor
  Serial.printf("Dauer:%d,Entfernung:%d\n",dauer,entfernung); // Ausgabe serieller Plotter
  delay(500); // 0,5s
}
Ausgabe auf Seriellem Plotter
Ausgabe auf Seriellem Plotter

Wenn im zerklüftetem Zimmer gemessen wird, können die Ultraschallimpulse wild reflektiert werden und führen an manchen Positionen zu unsteten Ergebnissen.

Wie ist der Minimalwert von dauer?

Weitere Infos zu dem Modul:

Theorie hinter der Messung

Schallgeschwindigkeit🔗 in trockener Luft bei 20 °CWegZeit
$ c_{s} = 343,4 m/s $$ s = c_{s} * t $$ t = s / c_{s} $

pulseIn(ECHO, HIGH) gibt die Länge des positiven Impulses in µs zurück. Die Entfernung zum Objekt ist der halbe Wert der Strecke, die der Schall zurücklegt.

1 cm5 cm10 cm1 m2 m5 m10 m
58,2 µs291,2 µs582,4 µs5824,1 µs11648,2 µs29120,5 µs58241,1 µs = 58,2 ms
Impulslänge bei verschiedenen Entfernungen

Den maximalen Wert des Ultraschallsensors habe ich mit Finger auf den Empfänger legen ermittelt (kein Signal kommt zurück): <190000 -> <32,3 m. Solange der Impuls bei pulseIn() gemessen wird blockiert die Funktion weiteren Programmfluss, d.h. das Programm wird bis zum Ende der Messung unterbrochen. Deshalb kann bei pulseIn() als optionaler dritter Parameter noch ein Timeout in µs angegeben werden, wird diese Zeit überschritten wird 0 zurück gegeben.

Numerische Tricks bei der Berechnung der Entfernung

Beim Funduino-Sketch wird die Entfernung sinngemäß so berechnet: entfernung = (dauer/2) * 0.03434; // nach Wikipedia bei 20°C: * 0.03434
Dauer ist der µs Wert des Impulses als ganze Zahl. Wird eine ganze Zahl durch eine ganze Zahl (2) geteilt, werden dabei die Nachkommastellen abgeschnitten, es wird nicht gerundet. Ausserdem wurde auf cm gerechnet und dabei wird auch weiter Genauigkeit verschwendet, der verwendete Datentyp bietet mehr als ausreichend Platz für Millimetergenauigkeit. Beispiel für Entfernung 5 cm:

Berechnung nach Funduino entfernung = (dauer/2) * 0.03434: 291/2 als Ganzzahldivision = 145 *0.03434 = 4,9793 aber in eine Ganzzahl gewandelt -> 4 cm
Berechnung numerisch genauer entfernung = dauer*0.3434/2: 291*0.3434 = 99,9294 / 2 = 49,96 in eine Ganzzahl gewandelt -> 49 mm -> 4,9 cm

Messungen filtern und glätten

Eine Wissenschaft für sich! Wenn man die Messergebnisse mit dem Seriellem Plotter in unterschiedlichen Positionen verfolgt, können wilde Ausreißer beobachtet werden. Für den folgenden Anwendungsfall sollen Entfernungen über 2 m ignoriert und die verbleibenden Messungen gemittelt werden:

  • Bei pulseIn() timeout passend einstellen.
  • Entfernung mitteln mit entfernung = (entfernung + dauer*0.3434/2)/2;
  • Der Serielle Plotter soll nur die Entfernung in mm anzeigen.

🖥 Erstellen Sie den Code und testen Sie ihn.

Lösungsvorschlag
#define TRIGGER PA10 // Pin zum Auslösen der Messung
#define ECHO PC9     // Rückgabepin für Impulslänge
unsigned long dauer=0;        // long sind 32 Bit für Messung der Impulslänge in µs
unsigned long entfernung=0;   // in mm
void setup(){
  Serial.begin (9600); //Serielle Kommunikation starten, damit man sich später die Werte am serial monitor ansehen kann.
  pinMode(TRIGGER, OUTPUT); // Trigger-Pin ist ein Ausgang
  pinMode(ECHO, INPUT);     // Echo-Pin ist ein Eingang
}
void loop(){
  digitalWrite(TRIGGER, HIGH);
  delayMicroseconds(20);       // 10 µs minimal, 20 zur Sicherheit
  digitalWrite(TRIGGER, LOW);  // Jetzt fängt die Messung an
  dauer = pulseIn(ECHO, HIGH,11648); // Mit dem Befehl „pulseIn“ zählt der Mikrokontroller die Zeit in Mikrosekunden, bis der Schall zum Ultraschallsensor zurückkehrt.
  if (dauer!=0){
    entfernung = (entfernung + dauer*0.3434/2)/2; // Berechnung in mm
  }
  Serial.printf("Entfernung:%d\n",entfernung); // Ausgabe serieller Plotter
  delay(500); // 0,5s
}

Eine stärkere Glättung könnte mit diesem Ausdruck erfolgen: entfernung = (entfernung*4 + dauer*0.3434/2)/5;

Ohne pulseIn() und delay() nur mit Timern und ext. Interrupt

Die Messungen werden mit einem Timer startMessung alle 300 ms durch isr_startMessung() ausgelöst. Die Pulslänge wird mit einem Timer messTimer in µs gemessen. Dauert dies zu lange wird eine isr_messTimer_overflow() ausgelöst. Das Echo-Signal wird mit einer ISR isr_echo_change() ausgewertet. Nach dem Auslösen einer Messung können 3 Dinge passieren:

Mögliche Abläufe
Zustandsdiagramm
Zustandsdiagramm

🖥 Ergänzen Sie den vorgegebenen Code und testen Sie das Programm.

#define TRIGGER PA10 // Pin zum Auslösen der Messung
#define ECHO PC9     // Rückgabepin für Impulslänge
#define ECHO_RISE digitalRead(ECHO) // wenn nach CHANGE High ist
static HardwareTimer startMessungTimer = HardwareTimer(TIM3);  // Timerinstanz für Messungswiederholung
static HardwareTimer messTimer = HardwareTimer(TIM4);  // Timerinstanz für Pulsdauermessung
enum zustandstyp {MESSEN_ENTRY,MESSEN,MESSUNG};
zustandstyp zustand=MESSEN_ENTRY;

unsigned long entfernung=0;   // in mm
void setup(){
  Serial.begin (9600); //Serielle kommunikation starten, damit man sich später die Werte am serial monitor ansehen kann.
  pinMode(TRIGGER, OUTPUT); // Trigger-Pin ist ein Ausgang
  pinMode(ECHO, INPUT);     // Echo-Pin ist ein Eingang
  startMessungTimer.setPrescaleFactor(?);         // 1ms Takt
  startMessungTimer.setOverflow(?);                 // 300 ms
  startMessungTimer.?;
  startMessungTimer.?;                         // Timer starten
  messTimer.setPrescaleFactor(?);                    // 1 µs Takt
  messTimer.setOverflow(12000);                       // bei mehr als ? m
  messTimer.attachInterrupt(isr_messTimer_overflow);  // wenn Messung zu lange dauert
  attachInterrupt (digitalPinToInterrupt (ECHO), isr_echo_change, CHANGE); // wenn sich am ECHO-Pin was tut
}
void isr_startMessung(){ // alle 300 ms eine neue Messung durch ISR ausgelöst
  ?
}
void isr_messTimer_overflow(){ // Wenn Pulsmessung zu lange dauert
  zustand = MESSEN_ENTRY;
}
void isr_echo_change(){ // Wenn sich am ECHO-Pin was tut
  ?
}
void starteMessung(){ // Impuls senden und messTimer klar machen
  digitalWrite(TRIGGER, HIGH);
  delayMicroseconds(20);       // 10 µs minimal, 20 zur Sicherheit
  digitalWrite(TRIGGER, LOW);  // Jetzt fängt die Messung an
  messTimer.setCount(0);
  messTimer.resume();
}
void auswertenMessung(){ // Messung erfolgreich auswerten
  entfernung = (entfernung + messTimer.getCount()*0.3434/2)/2; // Berechnung in mm
  Serial.printf("Entfernung:%d\n",entfernung); // Ausgabe serieller Plotter
}
void loop(){
  switch(zustand){
    ?
  }
}
Mögliche Lösung
#define TRIGGER PA10 // Pin zum Auslösen der Messung
#define ECHO PC9     // Rückgabepin für Impulslänge
#define ECHO_RISE digitalRead(ECHO) // wenn nach CHANGE High ist
static HardwareTimer startMessungTimer = HardwareTimer(TIM3);  // Timerinstanz für Messungswiederholung
static HardwareTimer messTimer = HardwareTimer(TIM4);  // Timerinstanz für Pulsdauermessung
enum zustandstyp {MESSEN_ENTRY,MESSEN,MESSUNG};
zustandstyp zustand=MESSEN_ENTRY;

unsigned long entfernung=0;   // in mm
void setup(){
  Serial.begin (9600); //Serielle kommunikation starten, damit man sich später die Werte am serial monitor ansehen kann.
  pinMode(TRIGGER, OUTPUT); // Trigger-Pin ist ein Ausgang
  pinMode(ECHO, INPUT);     // Echo-Pin ist ein Eingang
  startMessungTimer.setPrescaleFactor(32000);         // 1ms Takt
  startMessungTimer.setOverflow(300);                 // 300 ms
  startMessungTimer.attachInterrupt(isr_startMessung);
  startMessungTimer.resume();                         // Timer starten
  messTimer.setPrescaleFactor(32);                    // 1 µs Takt
  messTimer.setOverflow(12000);                       // bei mehr als ? m
  messTimer.attachInterrupt(isr_messTimer_overflow);  // wenn Messung zu lange dauert
  attachInterrupt (digitalPinToInterrupt (ECHO), isr_echo_change, CHANGE); // wenn sich am ECHO-Pin was tut
}
void isr_startMessung(){ // alle 300 ms eine neue Messung durch ISR ausgelöst
  if(zustand==MESSEN){
    starteMessung();
    zustand = MESSUNG;
  }  
}
void isr_messTimer_overflow(){ // Wenn Pulsmessung zu lange dauert
  zustand = MESSEN_ENTRY;
}
void isr_echo_change(){ // Wenn sich am ECHO-Pin was tut
  if (zustand!=MESSUNG)return; // keine Messung
  if(ECHO_RISE){
    messTimer.setCount(0); // Timer zurück setzen
  } else {  // Falling
    auswertenMessung();
    zustand = MESSEN_ENTRY;
  }
}
void starteMessung(){ // Impuls senden und messTimer klar machen
  digitalWrite(TRIGGER, HIGH);
  delayMicroseconds(20);       // 10 µs minimal, 20 zur Sicherheit
  digitalWrite(TRIGGER, LOW);  // Jetzt fängt die Messung an
  messTimer.setCount(0);
  messTimer.resume();
}
void auswertenMessung(){ // Messung erfolgreich auswerten
  entfernung = (entfernung + messTimer.getCount()*0.3434/2)/2; // Berechnung in mm
  Serial.printf("Entfernung:%d\n",entfernung); // Ausgabe serieller Plotter
}
void loop(){
  switch(zustand){
    case MESSEN_ENTRY:
      messTimer.pause();
      zustand = MESSEN;
      break;
  }
}

🅿️ Einparkhilfe mit Leuchtband und Dezimeteranzeige

Mit dem Code aus der letzten Aufgabe soll eine Einparkhilfe entwickelt werden. Die Einparkhilfe verwendet bei der Ausgabe für Entfernungen über 16 cm eine gemultiplexte zweistellige 7-Segmentanzeige und schaltet bei geringerer Entfernung auf eine Leuchtbandanzeige um (isr_ausgeben()).

Zustandsdiagramm Vorgabe
Zustandsdiagramm Vorgabe
  • Mit dem entprellten lowaktiven UserButton an PC13 wird sie an- und ausgeschaltet. Der Tastendruck löst eine ISR isr_taster() aus(im Zustandsdiagramm ergänzen).
  • Die Timer-ISR isr_ausgeben() wird für die Ausgabe mit 100 Hz im Messbetrieb aufgerufen. Dafür ist ein Timer ausgebenTimer reserviert (siehe Codeschnipsel). Dieser Timer wird Zustand RUHE an- und ausgeschaltet. Da die Funktionsweise der Ausgabe-ISR unabhängig vom Zustand ist, wurde die ISR nicht ins Zustandsdiagramm übernommen.

🖥 Ergänzen Sie das Zustandsdiagramm.

🖥 Erweitern Sie den Code.

#define TASTER PC13 // Entprellter UserButton active low
static HardwareTimer startMessungTimer = HardwareTimer(TIM3);  // Timerinstanz für Messungswiederholung
static HardwareTimer messTimer = HardwareTimer(TIM4);  // Timerinstanz für Pulsdauermessung
static HardwareTimer ausgebenTimer = HardwareTimer(TIM5);  // Timerinstanz für Anzeige
enum zustandstyp {RUHE_ENTRY,RUHE,MESSEN_ENTRY,MESSEN,MESSUNG};
zustandstyp zustand=RUHE_ENTRY;
int bcd_7seg[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x40}; // Umrechnung
int leuchtband[]={0b10101010,0b1,0b11,0b111,0b1111,0b11111,0b111111,0b1111111,0b11111111};
Lösung Zustandsdiagramm
Zustandsdiagramm Einparkhilfe
Zustandsdiagramm Einparkhilfe
Lösungsvorschlag Code
#define TRIGGER PA10 // Pin zum Auslösen der Messung
#define ECHO PC9     // Rückgabepin für Impulslänge
#define ECHO_RISE digitalRead(ECHO) // wenn nach CHANGE High ist
#define TASTER PC13 // Entprellter UserButton active low
static HardwareTimer startMessungTimer = HardwareTimer(TIM3);  // Timerinstanz für Messungswiederholung
static HardwareTimer messTimer = HardwareTimer(TIM4);  // Timerinstanz für Pulsdauermessung
static HardwareTimer ausgebenTimer = HardwareTimer(TIM5);  // Timerinstanz für Anzeige
enum zustandstyp {RUHE_ENTRY,RUHE,MESSEN_ENTRY,MESSEN,MESSUNG};
zustandstyp zustand=RUHE_ENTRY;
int bcd_7seg[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x40}; // Umrechnung
int leuchtband[]={0b10101010,0b1,0b11,0b111,0b1111,0b11111,0b111111,0b1111111,0b11111111};

unsigned long entfernung=0;   // in mm
void setup(){
  Serial.begin (9600); //Serielle kommunikation starten, damit man sich später die Werte am serial monitor ansehen kann.
  pinMode(TRIGGER, OUTPUT); // Trigger-Pin ist ein Ausgang
  pinMode(ECHO, INPUT);     // Echo-Pin ist ein Eingang
  pinMode(TASTER, INPUT);   // UserButton
  GPIOC->MODER = 0x5555; // PC0..PC7 als Ausgang
  pinMode(PC11,OUTPUT);  // Einer
  pinMode(PC12,OUTPUT);  // Zehner
  startMessungTimer.setPrescaleFactor(32000);         // 1ms Takt
  startMessungTimer.setOverflow(300);                 // 300 ms
  startMessungTimer.attachInterrupt(isr_startMessung);
  startMessungTimer.resume();                         // Timer starten
  messTimer.setPrescaleFactor(32);                    // 1 µs Takt
  messTimer.setOverflow(12000);                       // bei mehr als ? m
  messTimer.attachInterrupt(isr_messTimer_overflow);  // wenn Messung zu lange dauert
  attachInterrupt (digitalPinToInterrupt (ECHO), isr_echo_change, CHANGE); // wenn sich am ECHO-Pin was tut
  ausgebenTimer.setPrescaleFactor(32000);             // 1 ms Takt
  ausgebenTimer.setOverflow(10);                      // alle 10 ms 100 Hz
  ausgebenTimer.attachInterrupt(isr_ausgeben);
  attachInterrupt (digitalPinToInterrupt (TASTER), isr_taster, FALLING); // wenn Taster gedrückt wird
}
void isr_startMessung(){ // alle 300 ms eine neue Messung durch ISR ausgelöst
  if(zustand==MESSEN){
    starteMessung();
    zustand = MESSUNG;
  }  
}
bool fehlmessung=false;
void isr_messTimer_overflow(){ // Wenn Pulsmessung zu lange dauert
  fehlmessung=true;
  zustand = MESSEN_ENTRY;
}
void isr_echo_change(){ // Wenn sich am ECHO-Pin was tut
  if (zustand!=MESSUNG)return; // keine Messung
  if(ECHO_RISE){
    messTimer.setCount(0); // Timer zurück setzen
  } else {  // Falling
    auswertenMessung();
    zustand = MESSEN_ENTRY;
  }
}
void starteMessung(){ // Impuls senden und messTimer klar machen
  digitalWrite(TRIGGER, HIGH);
  delayMicroseconds(20);       // 10 µs minimal, 20 zur Sicherheit
  digitalWrite(TRIGGER, LOW);  // Jetzt fängt die Messung an
  messTimer.setCount(0);
  messTimer.resume();
}
void auswertenMessung(){ // Messung erfolgreich auswerten
  entfernung = (entfernung + messTimer.getCount()*0.3434/2)/2; // Berechnung in mm
  Serial.printf("Entfernung:%d\n",entfernung); // Ausgabe serieller Plotter
  fehlmessung=false;
}
void isr_ausgeben(){ // 100Hz
  static bool einer = true; // Einerstelle ist dran
  int ausgabe;
  if(fehlmessung){
    GPIOC->ODR = bcd_7seg[10] | (einer?(1<<11):(1<<12));
  }
  else if(entfernung <= 160){
    GPIOC->ODR = leuchtband[entfernung/20];
  } else{
    ausgabe = (entfernung+50)/100; // in Dezimeter runden
    if (einer){
      GPIOC->ODR = bcd_7seg[ausgabe%10] | (1<<11); // Einer einschalten
    } else{ // gerade
      GPIOC->ODR = bcd_7seg[ausgabe/10] | (1<<7) |(1<<12); // Dezimalpunkt und Zehner einschalten
    }
  }
  einer = !einer;
}
void isr_taster(){
  if(zustand==RUHE){
    zustand=MESSEN_ENTRY;
    ausgebenTimer.resume(); // Ausgabe starten
  } else{
    zustand=RUHE_ENTRY;
  }
}
void anzeigeAus(){
  GPIOC->ODR = 0;
}
void loop(){
  switch(zustand){
    case RUHE_ENTRY:
      ausgebenTimer.pause();
      anzeigeAus();
      zustand=RUHE;
      break;
    case MESSEN_ENTRY:
      messTimer.pause();
      zustand = MESSEN;
      break;
  }
}