duty cycle messen eines PWM-Signals bei 20kHz

Hallo,

ich bin auf der Suche nach einer Methode um den Duty Cycle eines PWM-Signals zu messen.
Auf der Suche nach der richtigen Methode kam ich auf pulseIN, diese Methode funktioniert sehr gut, jedoch ist die Messung zu ungenau, da die Periodendauer bei 20kHz nur 50µs (Mycrosekunden) beträgt.
Die Funktion pulseIn misst den Impuls in µs, was für meinen fall zu ungenau ist.

Das Programm soll auf einem ESP32 Dev Modul laufen.

Folgendes Programm habe ich breits geschrieben und ausprobiert (jedoch zu ungenau):

#define PWM_Channel 0
#define frequency 1000
#define resolution 8
#define PWM_Pin 2

float duration_on = 0;
float duration_off = 0;

void setup() {
  Serial.begin(115200);
  Serial.println("started");
  
  pinMode(34, INPUT);
  
  ledcSetup(PWM_Channel, frequency, resolution);
  ledcAttachPin(PWM_Pin, PWM_Channel);
  delay(100);
  ledcWrite(PWM_Channel, 0);
}

void loop() {
  duration_on = pulseIn(34, HIGH);
  duration_off = pulseIn(34, LOW);

  Serial.print("Duration-ON: ");
  Serial.println(duration_on);
  Serial.print("Duration-OFF: ");
  Serial.println(duration_off);
  Serial.println();
}

Frage: Kann man diese Funktion genauer machen, oder gibt es eine andere Möglichkeit das Signal genauer zu messen? Gut wäre eine Messgenauigkeit von 100ns.

Ich würde das per IRQ messen.

Die Idee hatte ich auch schon, jedoch wie soll ich da eine Zeit messen?

In der Arduino Reference steht, dass die millis() und micros() Funktion nicht funktioniert (also nicht weiter gezählt wird).

Also hab ich keinerlei Anhaltspunkte, wie ich eine solche kurze Zeit auswerten kann.
Weiters zählt die Funktion micros() "nur" in µs und nicht in gewünschten ns.

Weiters zählt die Funktion micros() "nur" in µs und nicht in gewünschten ns.

Das wird schwierig.
Schneller und genauer als micros() wirst du nur, wenn du direkt Ports abfragst und weisst, wie viele Takte deine Schleife braucht. (Und wie viel Zeit diese Takte brauchen.)
Dabei wird das Ganze Hardware-Spezifisch und du solltest ausnutzen, dass ein ESP32 eigentlich gar kein Arduino ist. :slight_smile:

In einer IRQ kannst du das Ergebnis von micros() natürlich abfragen. Dies wird sich allerdings innerhalb desselben IRQ-Aufrufs nicht ändern.

Das hab ich fast befürchtet, das direkte Port abfragen ist ja nicht die große Herausforderung, jedoch weiß ich nicht wie lange und wie viele Tackte das Programm benötigt zum durchlaufen.

Und wenn das Programm dann mal einen Tackt länger benötigt, dann stimmt meine Berechnung wieder nicht...... -> Teufelskreis.

Dabei wird das Ganze Hardware-Spezifisch und du solltest ausnutzen, dass ein ESP32 eigentlich gar kein Arduino ist. :)
Was meinst du damit? Wie soll ich das ausnutzen?

Hallo,

Und wenn Du das PWM glättest und den Wert analog misst. Ist aber sicher uncool :wink:

Heinz

Das wäre generell keine schlechte Idee, jedoch weiß ich nicht genau mit welche Frequenz das Signal kommt.
Ich weiß nur, dass es eine Frequenz von ungefähr 18kHz sind... können aber auch 20kHz sein.... oder 16kHz und ein Signal glätten das +-2kHz hat, ist fast unmöglich.

Aber danke für die Idee :slight_smile:

Rentner:
Hallo,

Und wenn Du das PWM glättest und den Wert analog misst. Ist aber sicher uncool :wink:

Heinz

Och ist doch viel zu ungenau... [IRONIEMODUS AUS]

Andyy:
Ich weiß nur, dass es eine Frequenz von ungefähr 18kHz sind... können aber auch 20kHz sein.... oder 16kHz und ein Signal glätten das +-2kHz hat, ist fast unmöglich.
Aber danke für die Idee :slight_smile:

Aha.

Woher kommt den Dein Falschwissen??
Grüße Uwe

Hallo,
@Andyy
also wenn ein mittels RC geglättetes Signal bei 5 KHz glatt ist sehe ich eigendlich keinen Grund warum es bei 20KHz nicht mehr glatt sein sollte.

Heinz

Hallo,

nimm einen Arduino Nano Every (den Originalen) und konfiguriere den TCB für den Pulse-width measurement oder Frequency and pulse-width measurement Modus und das Event System entsprechend.

Ein optimales Ergebnis zwischen genügender Glättung und schnellstmöglicher Ausgangsspannungsänderung bei Änderung des PWM Wertes ergibt spezifische Bauteilauswahl in Funktion der Frequenz. Wobei wenig Unterschied zwischen 18 und 20 oder 16 Khz besteht

Wie schnell soll Dein die Messung des PWM Signals eine Änderung des PWM Signals erkennen?

Grüße Uwe

Im Bereich GPIO & RTC GPIO findest Du ein Beispiel, das mit leichten Anpassungen auch in der Arduino-IDE kompiliert und auf einem ESP32 läuft. Ob es die Messung beschleunigt, vermag ich aber nicht zu versprechen.

/* ESP32 GPIO Example
   This example code is in the Public Domain (or CC0 licensed, at your option.)
   Unless required by applicable law or agreed to in writing, this
   software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
   CONDITIONS OF ANY KIND, either express or implied.
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/gpio.h"

/**
   Brief:
   This test code shows how to configure gpio and how to use gpio interrupt.

   GPIO status:
   GPIO18: output
   GPIO19: output
   GPIO4:  input, pulled up, interrupt from rising edge and falling edge
   GPIO5:  input, pulled up, interrupt from rising edge.

   Test:
   Connect GPIO18 with GPIO4
   Connect GPIO19 with GPIO5
   Generate pulses on GPIO18/19, that triggers interrupt on GPIO4/5

*/

#define GPIO_OUTPUT_IO_0    18
#define GPIO_OUTPUT_IO_1    19
#define GPIO_OUTPUT_PIN_SEL  ((1ULL<<GPIO_OUTPUT_IO_0) | (1ULL<<GPIO_OUTPUT_IO_1))
#define GPIO_INPUT_IO_0     gpio_num_t(4)
#define GPIO_INPUT_IO_1     gpio_num_t(5)
#define GPIO_INPUT_PIN_SEL  ((1ULL<<GPIO_INPUT_IO_0) | (1ULL<<GPIO_INPUT_IO_1))
#define ESP_INTR_FLAG_DEFAULT 0

int cnt = 0;
static xQueueHandle gpio_evt_queue = NULL;

static void IRAM_ATTR gpio_isr_handler(void* arg)
{
  uint32_t gpio_num = (uint32_t) arg;
  xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL);
}

static void gpio_task_example(void* arg)
{
  gpio_num_t io_num;
  for (;;) {
    if (xQueueReceive(gpio_evt_queue, &io_num, portMAX_DELAY)) {
      printf("GPIO[%d] intr, val: %d\n", io_num, gpio_get_level(io_num));
    }
  }
}

void setup(void)
{
  gpio_config_t io_conf;
  //disable interrupt
  io_conf.intr_type = GPIO_INTR_DISABLE;
  //set as output mode
  io_conf.mode = GPIO_MODE_OUTPUT;
  //bit mask of the pins that you want to set,e.g.GPIO18/19
  io_conf.pin_bit_mask = GPIO_OUTPUT_PIN_SEL;
  //disable pull-down mode
  io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
  //disable pull-up mode
  io_conf.pull_up_en = GPIO_PULLUP_DISABLE;
  //configure GPIO with the given settings
  gpio_config(&io_conf);

  //interrupt of rising edge
  io_conf.intr_type = GPIO_INTR_POSEDGE;
  //bit mask of the pins, use GPIO4/5 here
  io_conf.pin_bit_mask = GPIO_INPUT_PIN_SEL;
  //set as input mode
  io_conf.mode = GPIO_MODE_INPUT;
  //enable pull-up mode
  io_conf.pull_up_en = GPIO_PULLUP_ENABLE;
  gpio_config(&io_conf);

  //change gpio intrrupt type for one pin
  gpio_set_intr_type(GPIO_INPUT_IO_0, GPIO_INTR_ANYEDGE);

  //create a queue to handle gpio event from isr
  gpio_evt_queue = xQueueCreate(10, sizeof(uint32_t));
  //start gpio task
  xTaskCreate(gpio_task_example, "gpio_task_example", 2048, NULL, 10, NULL);

  //install gpio isr service
  gpio_install_isr_service(ESP_INTR_FLAG_DEFAULT);
  //hook isr handler for specific gpio pin
  gpio_isr_handler_add(GPIO_INPUT_IO_0, gpio_isr_handler, (void*) GPIO_INPUT_IO_0);
  //hook isr handler for specific gpio pin
  gpio_isr_handler_add(GPIO_INPUT_IO_1, gpio_isr_handler, (void*) GPIO_INPUT_IO_1);

  //remove isr handler for gpio number.
  gpio_isr_handler_remove(GPIO_INPUT_IO_0);
  //hook isr handler for specific gpio pin again
  gpio_isr_handler_add(GPIO_INPUT_IO_0, gpio_isr_handler, (void*) GPIO_INPUT_IO_0);

  printf("Minimum free heap size: %d bytes\n", esp_get_minimum_free_heap_size());
}

void loop() 
{
  printf("cnt: %d\n", cnt++);
  vTaskDelay(1000 / portTICK_RATE_MS);
  gpio_set_level(gpio_num_t(GPIO_OUTPUT_IO_0), cnt % 2);
  gpio_set_level(gpio_num_t(GPIO_OUTPUT_IO_1), cnt % 2);
}

Also hab ich keinerlei Anhaltspunkte, wie ich eine solche kurze Zeit auswerten kann.
Weiters zählt die Funktion micros() “nur” in µs und nicht in gewünschten ns.

Wenn du auf Arduino-Level bleiben willst, sind ganzzahlige micros() die kleinste Einheit.

Hier mal ein Beispiel, wie ein Arduino seine PWM-Pulslängen messen könnte.
In micros natürlich, und aufsummieren vieler Zyklen erhöht eigentlich auch nicht die Genauigkeit

const byte Pin=2; // Für IRHandler brauchbarer Pin, mit dem zu messenden PWM Signal 
const byte TestPin=6; // Test PWM Signal (ein PWM-fähiger Pin)  
const unsigned long SAMPLES=1000;  // Mittelwertbildung und Anzeigezyklus

//including Stream out
template<class T> inline Print& operator<< (Print &obj, T arg) { obj.print(arg); return obj; }
const char endl {'\n'};

void setup() {
  Serial.begin(9600);
  Serial << F("Start: ") << __FILE__ << " SAMPLES=" <<SAMPLES << endl; 
  attachInterrupt(digitalPinToInterrupt(Pin), IRHandler, CHANGE);

  //  analogWrite(TestPin, 64);
}

volatile unsigned long count;
volatile unsigned long high;  // gesammelte Zeit (µs)
volatile unsigned long low;   // gesammelte Zeit (µs)

void IRHandler() {  // bei jedem Flankenwechsel
 static bool running = false;
 static unsigned long t0; // Zeit letzte Flanke
 static bool f = false;  // zur Unterscheidung der zwei Flanken
 unsigned long t = micros();
 // Startphase
 if (!running && f) { t0=t; f=false; running = true;  return;}
 if (!running && (digitalRead(Pin)==HIGH)) {f=true; return;}
 // Start mit LOW-Phase
 
 // laufend: t, t0 sind aktuell
 if (!f) { low  += t - t0; t0 = t; f=true; }
 else    { high += t - t0; t0 = t; f=false; count++;}  
}

void loop() {
  int testdata = millis()/1000 % 100;   // Testsignal : je sek +=1  (0 .. 99)
  analogWrite(TestPin, testdata*256/100); // Testsignal ausgeben 
  
  unsigned long c, hi, lo; // lokale Kopien
  noInterrupts();
  if (count >= SAMPLES) {
    c = count; count = 0;
    hi=high; high = 0;
    lo= low; low = 0;
    interrupts();
    Serial << c << " ";  // Damit count überhaupt verwendet wird
    showResults(hi, lo); 
  } else interrupts();

  // delay(100); // schneller brauchen wir es nicht, auch bei Hoher PWM-Frequenz
}

void showResults(unsigned long h, unsigned long l) {
  // Minimale Auswertung
  Serial << "  PWM = " << h << "/" << h+l << " = " << (float)(h)/(h+l) << endl;
}

Wenn du auf Arduino-Level bleiben willst, sind ganzzahlige micros() die kleinste Einheit.

Zählt micros() auf 16Mhz getakteten Arduinos nicht in 4µS Schritten?
Grüße Uwe

Danke für die vielen Vorschläge!

Leider kenn ich mich für die Programme von @michael_x und @agmue zu wenig mit der tieferen Materie aus. Danke aber für die Beispielcodes.

@uwfed Macht die Frequenz nicht einen drastischen Unterschied bei der Bauteil Auswahl? (bei einem simplen RC-Tiefpass) Ich habe mal einen einfachen Tiefpass simuliert mit berechneten Werte, und da schwankt die Spannung doch noch sehr stark, dass man mit diesem Signal keinen Motordrehzahl vorgeben kann.

@Doc_Arduino was macht der Arduino Nano Every anders als ein Board mit dem esp32 ?

Zählt micros() auf 16Mhz getakteten Arduinos nicht in 4µS Schritten?

Auch wieder wahr. Und beim ESP32 ? Wer weiß da was?

aufsummieren vieler Zyklen erhöht eigentlich auch nicht die Genauigkeit

Auch das stimmt so krass nicht, andererseits.

Aussage bleibt allerdings, dass --wenn man am ESP32 mit ns rechnen will-- das nicht auf Arduino-Level geht. Mal eher bei espressif direkt schauen...

Das Verhalten eines RC-Tiefpass hängt nun mal am Wert von R*C. Fragt sich halt, wie groß man den machen will, um absichtlichen Änderungen nicht zu träge zu folgen. Aber ja, das Tastverhältnis eines PWM-Signals sollte man besser durch Zeitmessungen bestimmen. Den ersten Tip von Rentner mit "... uncool :)" habe ich auch als Scherz aufgefasst.

Wenn man so kleine Zeiten genau messen will, kommt man um das ausnutzen der internen Timer nicht herum. Der ATMega hat bei seinen Timern einen capture-Modus, bei dem durch eine externes Signal der Zählerstand gespeichert werden kann. Datenblatt, Kapitel 16.6:

The Timer/Counter incorporates an Input Capture unit that can capture external events and give them a timestamp indicating time of occurrence. The external signal indicating an event, or multiple events, can be applied
via the ICP1 pin or alternatively, via the analog-comparator unit. The time-stamps can then be used to calculate
frequency, duty-cycle, and other features of the signal applied. Alternatively the time-stamps can be used for
creating a log of the events

Damit kann man sowas auch auf einem ATMega mit 16Mhz schon mit einer Auflösung im sub µs-Bereich messen ( kommt auch auf die kleinste vorkommende Pulsbreite an ).

Wenn ich mich recht erinnere hat der ESP32 auch recht leistungsfähige Timer, und ich gehe deshalb davon aus, dass das auch damit möglich ist. Aufgrund der höheren Taktfrequenz sicher auch noch mit besserer Auflösung.

Man komm dann aber nicht darum herum, sich mit den HW-Internas des Prozessers zu beschäftigen, und das Datenblatt zu studieren.

@Uwe: ja, die micros()-Auflösung bei den Arduinos mit 16MHz Prozessor ist 4µs. Das ist der Takt mit dem der Timer 0 betrieben wird.

Hallo,

was meinst Du mit Motordrehzahl vorgeben , glaubst Du der Motor ändert seine Drehzahl in einem Bruchteil von 20 KHz.

duty Cycle , als Verhältniss der H Zeit zur gesamt Zeit ist identisch mit dem Mittelwert der gemessenen Spannung. Ich hab jetzt mal in meiner Wühlkiste ein C und ein R gegriffen, und einen kleinen Sketch geschrieben. PWM ausgeben , analog messen über das RC Glied.

letztlich hängt es davon ab wie schnell Du messen willst, bzw wie schnell sich der PWM Wert ändert. Insofern kann man das noch bessser machen.

C=47uF R= 1KOhm

Versuch auf einem UNO

Heinz

/*Ausgegeben wir ein PWM Signal auf Pin3
 * mittels einem RC Glied wird die ausgegebene 
 * Spannung über den Analogeingang A0 gemessen.
 * R=1KOhm C=47uF 
 * Anzeige im Plotter möglich
 * Hardware UNO
 */

const byte pinOut = 3;
const byte pinIn = A0;
int anaIn, anaOut;
bool toggle;
const byte wert1=25; // Werte für die Ausgabe
const byte wert2 =225;
uint32_t altzeit;


void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
  altzeit=10000;
  
}

void loop() {
  // put  main code here, to run repeatedly:

  if (millis() - altzeit >= 10000) {
    altzeit = millis();
    toggle=!toggle; // toggle alle 10s
    if (toggle)anaOut=wert1;
    else anaOut=wert2;
   analogWrite(pinOut, anaOut);
  }
  anaIn = analogRead(pinIn)/4; // einlesen auf gleicher Scala
  Serial.print(anaOut); Serial.print("\t");
  Serial.println(anaIn);
  delay(100);// damit die serielle nicht überläüft

}

Andyy:
@Doc_Arduino was macht der Arduino Nano Every anders als ein Board mit dem esp32 ?

Was soll ich darauf antworten? Das sind zwei völlig verschiedene Controller.
Ich trete nicht in Konkurrenz zum ESP. Ich weiß nicht was die Timer eines ESP können.
Ich weiß was die Timer der megaAVR0 Serie können. Die TCBn können zum Bsp. die Pulsweite und Frequenz in Hardware messen. Bspw. mit Prescaler 1 Takt genau bis zu einer Puls/Periodendauer von 4,095ms. Das würde ausreichen um bis runter auf 245Hz Takt fein (62,5ns) zu messen.
Egal was du machst, du musst die Hardware direkt programmieren. Dazu solltest du dich damit befassen, sonst springst du ins kalte Wasser und siehst kein Land.