Forum: Mikrocontroller und Digitale Elektronik Hilfe, PWM-Messung - klappt nicht mehr als einmal (Interrupts falsch?)


von Veit H. (veith)


Lesenswert?

So Profis, ich benutze einen Atmega8 um ein 1khz PWM-Signal zu 
generieren und gleichzeitig ein anderes 1khz-Signal auszuwerten. Der 
Atmega läuft mit einem 8MHz-Oszillator, mit Prescaler 8 und im 
Phase-and-Frequency-Correct-Mode läuft der 16-Bit-Timer von 0 bis 500 
und zurück, also komme ich mit 1000 125us Schritten genau auf 1kHz und 
kann meine PWM schön einstellen. Das klappt so weit alles prima.

Jetzt wollte ich die Auflösung desselben Timers nutzen, um das 
Tastverhältnis eines 1kHz-Signals auszuwerten. Habe das Signal dazu an 
den Pin ICP1 des Atmega8 angeschlossen und kann die Flankenwechsel per 
Interrupt auswerten.

Nun ist es ja so, dass ich, um die Dauer zwischen der steigenden und 
fallenden Flanke zu bestimmen, wissen muss, ob der Counter gerade hoch 
oder runter zählt und ob die beiden Flankenwechsel in derselben 
Counterperiode (also 2*500=1000 Schritte) oder in zwei aufeinander 
folgenden Counterperioden stattgefunden haben.

Also habe ich zwei Interrupts bei Timer-Top und Timer-Bottom aktiviert, 
bei der ich eine Hilfsvariable setze, die mir anzeigt ob der Timer hoch 
oder runter zählt. Zusätzlich wird der Periodenzähler um eins erhöht, 
wenn der Counter wieder bei 0 beginnt:
1
volatile uint8_t isUpcounting;
2
volatile uint16_t pwm_period_counter;
3
4
/* Timer on Bottom */
5
ISR(TIMER1_OVF_vect)
6
{
7
  isUpcounting = 1;
8
  pwm_period_counter++;
9
}
10
11
/* Timer on top */
12
ISR(TIMER1_COMPA_vect)
13
{
14
  isUpcounting = 0;
15
}
(das ist irgendwie von hinten ins Knie geschossen mit diesen beiden 
Interrupts, bin für jeden Vereinfachungsvorschlag dankbar)

In der PWM-Messroutine wird der Interrupt auf steigende Flanke 
eingestellt, frei gegeben und auf Deaktivierung des Interrupts 
(geschieht in der Interrupt-Routine) gewartet.
Die zugehörige Interrupt-Routine speichert bei Aufruf den Timer-Wert, 
setzt den Periodenzähler auf 0 und stellt den Interrupt auf fallende 
Flanke. Beim nächsten Aufruf dieser Interrupt-Routine wird also der 
Timer-Wert der fallenden Flanke und die Anzahl der möglicherweise 
vergangenen Perioden gespeichert.
Anschließend wird der Interrup deaktiviert, damit nur eine Messung 
stattfindet.
Mit diesen Werten kann ich in der PWM-Messroutine prima die High- und 
Low-Dauer des Signals ausrechnen. Das klappt wunderbar - einmal. Wenn 
ich die Funktion mehrmals oder gar regelmäßig aufrufe, bekomme ich total 
unterschiedliche Werte. Ich vermute, das liegt an den Interrupts, dass 
ich irgendwas nicht beachtet habe. Jede Hilfe wäre hier echt toll, ich 
kann den Fehler alleine nicht finden.

Dieser Aufruf klappt (einmal halt)
1
uint8_t measurePWM = 1;
2
3
/*-- Main Routine --*/
4
while(1)
5
{
6
    if (measurePWM)
7
    {
8
      timediff = pwm_messen();
9
      measurePWM = 0;
10
    }
11
    // verschicke timediff-Wert per I2C
12
}


Dieser Aufruf klappt nicht
1
uint8_t measurePWM = 1;
2
3
/*-- Main Routine --*/
4
while(1)
5
{
6
      timediff = pwm_messen();
7
      // verschicke timediff-Wert per I2C
8
}

Dabei habe ich schon mit verschiedenen Delays gearbeitet, um 
möglicherweise unvollständigen Rechnungen und dem I2C-Bus genügend Zeit 
zu geben, alles vergebens :-(

Mein pwm_messen() sieht so aus (die PWM kommt durch einen Transistor 
invertiert am Atmega an, deshalb sind start und stopp vertauscht)
1
volatile uint16_t pwm_temp;
2
volatile uint16_t pwm_start;
3
volatile uint16_t pwm_stop;
4
volatile uint16_t numberPeriod;
5
6
uint16_t pwm_messen()
7
{
8
  uint16_t result = 0;
9
  
10
  /* Initialize Variables */
11
  pwm_temp = 0;
12
  pwm_start = 0;
13
  pwm_stop = 0;
14
  numberPeriod = 0;
15
16
  /* Trigger Interrupt on rising edge */
17
  TCCR1B |= (1 << ICES1);
18
19
  /* Enable Interrupt on ICP1 */
20
  TIMSK |= (1 << TICIE1);
21
22
  /* Wait for end of measure (disabled ICP-Interrupt) */
23
  while (TIMSK & (1 << TICIE1))
24
  {
25
    ;
26
  }
27
28
  /* Perhaps disable Interrupt once again? */
29
  TIMSK &= ~(1 << TICIE1);
30
31
  /* Calculate Timedifference */
32
  result = pwm_start + numberPeriod * 1000 - pwm_stop;
33
34
  return result;
35
}

Und meine Interrupt-Routine zum Messen so:
1
/* Interrupt routine to measure PWM */
2
ISR(TIMER1_CAPT_vect)
3
{
4
  /* capture PWM value */
5
  pwm_temp = ICR1;
6
7
  /* rising edge (inverted, means stop) */
8
  if (TCCR1B & (1 << ICES1))
9
  {
10
    /* Trigger Interrupt next time on falling edge */
11
    TCCR1B &= ~(1 << ICES1);
12
13
    /* Reset Period counter */
14
    pwm_period_counter = 0;
15
16
    /* Get Timer-Value in Dependence of up- or downcounting */
17
    if (isUpcounting)
18
    {
19
      pwm_stop = pwm_temp;
20
    }
21
    else
22
    {
23
      pwm_stop = 500 + (500 - pwm_temp);
24
    }
25
  }
26
  /* falling edge (inverted, means start) */
27
  else
28
  {
29
    /* Disable Interrupts on IPC1 */
30
    TIMSK &= ~(1 << TICIE1);
31
32
    /* Get Timer-Value in Dependence of up- or downcounting */
33
    if (isUpcounting)
34
    {
35
      pwm_start = pwm_temp;
36
    }
37
    else
38
    {
39
      pwm_start = 500 + (500 - pwm_temp);
40
    }
41
42
    /* Save value of period-counter */
43
    numberPeriod = pwm_period_counter;
44
  }
45
}

Liegt es daran, dass aus einer Interrupt-Routine keine Änderungen in den 
Konfigurationsregistern wie TIMSK vorgenommen werden können, da die nach 
der Routine wieder auf den vorherigen Stand gebracht werden?
Ich bin im Moment echt ratlos, woran es liegen könnte, dass nur bei 
einmaligem Aufruf der Wert korrekt ist.

von spess53 (Gast)


Lesenswert?

Hi

>In der PWM-Messroutine wird der Interrupt auf steigende Flanke
>eingestellt, frei gegeben und auf Deaktivierung des Interrupts
>(geschieht in der Interrupt-Routine) gewartet.

Geht nicht. ICES1 stellt nur ein, ob der Timer-Wert bei einer steigenden 
ober fallenden Flanke übernommen wird. Der Interrupt wird in beiden 
Fällen ausgelöst.

MfG Spess

von Veit H. (veith)


Lesenswert?

Ja okay. Dafür überprüfe ich ja in der Interrupt-Routine, ob der 
Auslöser von einer steigenden oder einer fallenden Flanke kommt.

In der Mess-Routine gebe ich den ICP1-Interrupt erst frei, nachdem ich 
ihn auf steigende Flanke eingestellt habe.

Hhmm, dann kann natürlich die fallende Flanke auch einen Interrupt 
auslösen und die Routine einen völlig falschen (ungültigen) Wert aus dem 
ICR-Register kopieren. Könnte erklären, weshalb ich manchmal genau den 
Abstand 1000 bekomme. Puh, ich bekomme mein Gehirn heute nicht mehr auf 
Trab und beschäftige mich morgen nochmal damit. Danke!

von spess53 (Gast)


Lesenswert?

Hi

Entschuldige. Die Aussage zu Interrupt von mir war falsch. Ich habe das 
jetzt noch mal am realen AVR getestet. Der Interrupt wird nur an der mit 
ICES1 eingestellten Flanke ausgelöst.

MfG Spess

von Peter D. (peda)


Lesenswert?

Veit H. schrieb:
> So Profis, ich benutze einen Atmega8 um ein 1khz PWM-Signal zu
> generieren und gleichzeitig ein anderes 1khz-Signal auszuwerten. Der
> Atmega läuft mit einem 8MHz-Oszillator, mit Prescaler 8 und im
> Phase-and-Frequency-Correct-Mode läuft

Warum?
Wie willst Du mit nem Vor-/Rückwärtszähler vernünftige ICP-Werte 
erhalten?

Wenn ich PWM und ICP benötige, nehme ich immer Mode 15, das geht 
einwandfrei.


Peter

von Veit H. (veith)


Lesenswert?

> Warum?
> Wie willst Du mit nem Vor-/Rückwärtszähler vernünftige ICP-Werte
> erhalten?
>
> Wenn ich PWM und ICP benötige, nehme ich immer Mode 15, das geht
> einwandfrei.

Ja, weil der 16Bit-Timer ja gleichzeitig ein 1kHz-Signal im 
Phase-and-Frequency-Correct-Mode generiert. Deswegen will ich den Timer 
ja gleichzeitig auch zum Messen nutzen.
Mit den anderen zwei 8Bit-Timern bekomme ich ja nicht so eine genaue 
Auflösung auf 125us (Umlaufzeit bei 1kHz = 1ms, kleinste PWM-Auflösung 
5% up von 1ms) hin.
Deswegen gehe ich ja den Umweg, festzustellen ob der Timer hoch oder 
runter zählt.

Gruß,
Veit

von Peter D. (peda)


Lesenswert?

Veit H. schrieb:
> Deswegen gehe ich ja den Umweg, festzustellen ob der Timer hoch oder
> runter zählt.

Nochmal warum?
Hast Du Dir Mode 15 überhaupt mal angeschaut?

In Mode 15 mußt Du natürlich Ausgang OC1B als PWM nehmen.


Peter

von Karl H. (kbuchegg)


Lesenswert?

Beim durchlesen ist mir jetzt nichts grossartiges in deinem Programm 
aufgefallen.
Aber die Frage muss schon sein: Warum ist es so wichtig, dass deine PWM 
Frequenz genau 1kHz ist. Bei einer PWM steckt ja die Information nicht 
in der Frequenz sondern in der Pulslänge. Normalerweise ist es daher 
ziemlich unkritisch, wenn die PWM Frequenz höher liegt (Kann natürlich 
auch sein, dass am anderen Ende der Leitung ein Gerät steckt, welches 
auf die 1kHz angewiesen ist, genau darum frage ich)

von Karl H. (kbuchegg)


Lesenswert?

1
  /* Calculate Timedifference */
2
  result = pwm_start + numberPeriod * 1000 - pwm_stop;

Ich denke auch, dass diese Berechnung nicht stimmt.
Mal mit ein paar Zahlen.
Angenommen der Startpuls kommt beim DownCounting und einem Wert von 100. 
Timer zählt weiter, dreht seine Richtung um und zählt wieder aufwärts. 
Der Stoppuls kommt bei 50.

Laut deiner Rechnung macht das ein result von 50  ( 100 + 0 * 1000 - 50 
).
Tatsächlich sind aber 150 Ticks vergangen (100 im Down, 50 im Up)


Mal dir einfach mal eine Linie auf, die den Zählumfang deines Zählers 
symbolisiert. Und dann spiel alle möglichen Kombinationen durch, die für 
das Auftreten des Start und Stop Pulses möglich sind. Interessant sind 
dabei weniger die Fälle, wenn Start und Stop Puls beim selben 
Timer-Sweep (also beide auf Up, oder beide auf Down) liegen, sondern die 
Fälle, in denen der Timer zwischendurch seine Richtung ändert.

Edit: Ich habe jetzt noch nicht eingerechnet, was du im Capture 
Interrupt mit den Start und Stop Werten gemacht hast. Aber das scheint 
auch nicht das Gewünschte zu leisten.

    pwm_start = 500 + (500 - pwm_temp);

Das heist im Downcounting, bei einem Timerstand von 100, registrierst du 
einen Start Wert von  500 + ( 500 - 100 ) = 900
pwm_stop ergibt sich im Beispiel zu 50

    pwm_start + numberPeriod * 1000 - pwm_stop;

   900 + 0 * 1000 - 50 = 850

Immer noch falsch. Da müsste 150 rauskommen.

von Veit H. (veith)


Lesenswert?

Karl heinz Buchegger schrieb:
>
1
>   /* Calculate Timedifference */
2
>   result = pwm_start + numberPeriod * 1000 - pwm_stop;
3
>
>
> Ich denke auch, dass diese Berechnung nicht stimmt.
> Mal mit ein paar Zahlen.
> Angenommen der Startpuls kommt beim DownCounting und einem Wert von 100.
> Timer zählt weiter, dreht seine Richtung um und zählt wieder aufwärts.
> Der Stoppuls kommt bei 50.
>
> Laut deiner Rechnung macht das ein result von 50  ( 100 + 0 * 1000 - 50
> ).
> Tatsächlich sind aber 150 Ticks vergangen (100 im Down, 50 im Up)

Hallo Karl Heinz, danke dass du dir die Mühe gemacht und mein Programm 
angeschaut hast. Ich gebe zu, es ist mit dem invertierten Start/Stopp 
sehr verwirrend. Die Sache ist, dass ein Transistor vor der Schaltung 
die einkommende PWM invertiert. In pwm_messen() wird ja zuerst auf eine 
steigende Flanke getriggert, pwm_stop kommt also vor pwm_start (hat also 
den kleineren Wert).

Also, um bei deinen Zahlenbeispiel zu bleiben, Start und Stopp kurz 
umgedreht: der Stopp-Puls (steigende Flanke) kommt beim Downcounting 
beim Timer-Wert 100. Der Timer-Wert steht in pwm_temp. Dann wird mit
1
/* Reset Period counter */
2
    pwm_period_counter = 0;
 der Periodencounter auf Null gesetzt und mit
1
if (isUpcounting)
2
    {
3
      pwm_stopp = pwm_temp;
4
    }
5
    else
6
    {
7
      pwm_stopp = 500 + (500 - pwm_temp);
8
    }
wird pwm_stopp = 500 + 500 - 100 = 900.

Der Timer dreht (startet wieder bei 0).
Der Interrupt zu
1
/* Timer on Bottom */
2
ISR(TIMER1_OVF_vect)
3
{
4
  isUpcounting = 1;
5
  pwm_period_counter++;
6
}
erhöht den Periodenzähler nun auf 1.

Der Start-Puls (steigende Flanke) kommt nun bei 50.
Mit
1
/* Get Timer-Value in Dependence of up- or downcounting */
2
    if (isUpcounting)
3
    {
4
      pwm_start = pwm_temp;
5
    }
6
    else
7
    {
8
      pwm_start = 500 + (500 - pwm_temp);
9
    }
10
11
    /* Save value of period-counter */
12
    numberPeriod = pwm_period_counter;
wird pwm_start = 50 und numberPeriod = 1;

In pwm_messen() wird mit
1
/* Calculate Timedifference */
2
  result = pwm_start + numberPeriod * 1000 - pwm_stop;
das result auf 50 + 1 * 1000 - 900 = 150 Ticks gerechnet.


Müsste doch stimmen oder?

Meine Vermutung, weshalb das nur beim einmaligen Durchlauf klappt ist, 
dass in pwm_messen() der TICIE1-Interrupt aktiviert und anschließend 
gewartet wird, bis dieser Interrupt aus seiner Routine heraus 
deaktiviert wird. Habe ich da einen Denkfehler?
1
  /* Trigger Interrupt on rising edge */
2
  TCCR1B |= (1 << ICES1);
3
4
  /* Enable Interrupt on ICP1 */
5
  TIMSK |= (1 << TICIE1);
6
7
  /* Wait for end of measure (disabled ICP-Interrupt) */
8
  while (TIMSK & (1 << TICIE1))
9
  {
10
    ;
11
  }

> Mal dir einfach mal eine Linie auf, die den Zählumfang deines Zählers
> symbolisiert. Und dann spiel alle möglichen Kombinationen durch, die für
> das Auftreten des Start und Stop Pulses möglich sind. Interessant sind
> dabei weniger die Fälle, wenn Start und Stop Puls beim selben
> Timer-Sweep (also beide auf Up, oder beide auf Down) liegen, sondern die
> Fälle, in denen der Timer zwischendurch seine Richtung ändert.

Ja, das habe ich auch bereits gemacht :-)

Vielleicht ist der Hinweis von Peter nicht verkehrt, im Prinzip spricht 
glaube ich wirklich nichts gegen den Mode15, vom Timing her müsste das 
ja auch passen.
Ich habe es glaube ich auch erst mit einem anderen Modus versucht und es 
hat nicht hingehauen, und mit dem Phase-and-Frequency Modus hat's dann 
plötzlich geklappt und so habe ich den genutzt.

Ich werde mal versuchen das auf Mode15 umzustricken, da spare ich mir 
einige Up- und Downcounting-Interrupts. Vielleicht ist das der Hinweis, 
den ich brauchte und woran ich im Laufe der Problembewältigung nicht 
mehr gedacht habe.

von Karl H. (kbuchegg)


Lesenswert?

Veit H. schrieb:

> angeschaut hast. Ich gebe zu, es ist mit dem invertierten Start/Stopp
> sehr verwirrend.

Dann nenne die Variablen um :-)
pwm_start ist der Zählerstand, wenn der Puls beginnt
pwm_stop ditto, wenn der Puls endet.

Alles andere führt nur zu Verwirrung :-)

> Die Sache ist, dass ein Transistor vor der Schaltung
> die einkommende PWM invertiert.

Wie das Signal physisch aussieht, braucht mich nicht zu interessieren. 
Bei pwm_start hat es begonnen, bei pwm_stop hat es aufgehört.


> Der Timer dreht (startet wieder bei 0).
> Der Interrupt zu
1
/* Timer on Bottom */
2
> ISR(TIMER1_OVF_vect)
3
> {
4
>   isUpcounting = 1;
5
>   pwm_period_counter++;
6
> }
> erhöht den Periodenzähler nun auf 1.

Moment, wir waren im Downcounting.
Da wird kein Overflow ausgelöst (oder irre ich mich da jetzt)


> Müsste doch stimmen oder?
Ähm. Ist mir jetzt ehrlich gesagt zu verwirrend mit deinen 
Start/Stop/Up/Down/Counter Verdrehungen :-)



Hast du es am anderen Ende auch ausprobiert?
Puls beginnt bei 850, Zähler zählt hoch, dreht um und der Puls endet bei 
750.

von Peter D. (peda)


Lesenswert?

Karl heinz Buchegger schrieb:
> Ähm. Ist mir jetzt ehrlich gesagt zu verwirrend mit deinen
> Start/Stop/Up/Down/Counter Verdrehungen :-)

Den besser geeigneten Mode 15 zu nehmen, war im wohl zu einfach.

Ich will damit aber nicht sagen, daß es in den anderen T1-Modies 
garnicht geht.


Peter

von Veit H. (veith)


Lesenswert?

So, ich habe heute auf den Modus 15 umgestellt, was schneller ging als 
ich dachte, allerdings mit demselben Ergebnis: beim einmaligen Aufrufen 
klappt's, in einer Schleife nicht.

Habe dann lange den (Denk-) Fehler gesucht und herum experimentiert, und 
als ich mir die beiden Trigger zur steigenden und fallenden Flanke auf 
zwei Pins ausgegeben und auf'm Oszillskop angeschaut habe, habe ich 
gemerkt, dass der Interrupt zum Flankenwechsel nicht zwei mal (steigend 
und fallend), sondern vier mal aufgetreten ist, zwischendurch also 
nochmal aufgerufen wurde.

Habe daraufhin meine Überlegung, von wegen Capture-Interrupt anschalten, 
zwei Flanken abwarten, ausschalten, Abstand messen, verworfen und das 
Programm so umstrukturiert, dass das PWM-Signal kontinuierlich berechnet 
wird und ich einfach den berechneten Abstand in meiner Main-Routine 
abfrage.
Das klappt nach den ersten Tests auch endlich gut und zufrieden 
stellend! Also vielen Dank für die Hinweise! (das invertierte 
start-stop-Getausche habe ich auch beseitigt)

Jetzt weiß ich leider nicht, wo genau der Fehler lag. Ich vermute, dass 
diese else-Bedingung zu schwach formuliert war:
1
/* Interrupt routine to measure PWM */
2
ISR(TIMER1_CAPT_vect)
3
{
4
  /* rising edge */
5
  if (TCCR1B & (1 << ICES1))
6
  {
7
    // do something
8
9
  }
10
11
  /* falling edge */
12
  else
13
  {
14
    // do other stuff
15
  }
16
}

Zur Sicherheit habe ich die Else-Bedingung mit
1
/* falling edge */
2
  else if( !(TCCR1B & (1 << ICES1)) )
 verstärkt und auch den Flanken-Trigger-Wechsel in die Interrupt-Routine 
statt in die Mess-Routine eingetragen.

Ich denke, dass es daran gelegen hat.

von Peter D. (peda)


Lesenswert?

Veit H. schrieb:
> Habe daraufhin meine Überlegung, von wegen Capture-Interrupt anschalten,
> zwei Flanken abwarten, ausschalten, Abstand messen, verworfen

Das geht schon, nur muß man eben damit rechnen, daß das Interruptflag 
auch bei abgeschaltetem Interrupt gesetzt wird.
Das muß auch so sein, damit man keine Interrupts verliert.
Bzw. man kann ja auch das Flag pollen.

Will man aber gewollt ältere Ereignisse nicht behandeln, muß man nur 
einfach das Interruptflag direkt vor der erneuten Freigabe löschen (beim 
AVR auf 1 setzen!).


> und das
> Programm so umstrukturiert, dass das PWM-Signal kontinuierlich berechnet
> wird und ich einfach den berechneten Abstand in meiner Main-Routine
> abfrage.

Und das hat sogar den Vorteil, daß man schneller das nächste Meßergebnis 
hat.


Peter

von STK500-Besitzer (Gast)


Lesenswert?

1
/* Interrupt routine to measure PWM */
2
ISR(TIMER1_CAPT_vect)
3
{
4
  /* capture PWM value */
5
  pwm_temp = ICR1;
6
7
  /* rising edge (inverted, means stop) */
8
  if (TCCR1B & (1 << ICES1))
9
  {
10
    /* Trigger Interrupt next time on falling edge */
11
    TCCR1B &= ~(1 << ICES1);
12
    ...
13
  }
14
  /* falling edge (inverted, means start) */
15
  else
16
  {
17
    /* Disable Interrupts on IPC1 */
18
    TIMSK &= ~(1 << TICIE1);
19
    ...
20
  }
21
}

kann man auch so lösen:
/* Interrupt routine to measure PWM */
ISR(TIMER1_CAPT_vect)
{
  /* capture PWM value */
  pwm_temp = ICR1;

  /* rising edge */
  if (TCCR1B & (1 << ICES1))
  {
    ...
  }
  /* falling edge */
  else
  {
    ...
  }
  /* toggle detecting edge */
    TCCR1B ^= (1 << ICES1);
}
[/c]

Da braucht man sich nicht um das Setzen der richtige Flankenrichtung zu 
kümmern. Das passiert einfach automatisch.
Und kürzer ist es auch noch... :)

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.