Forum: Mikrocontroller und Digitale Elektronik PWM Nebensprechen


von Timo N. (tnn85)


Lesenswert?

Hallo,

ich baue gerade für meinen Neffen ein Kettenfahrzeug mit zwei über 
ATmega32 + Doppel-H-Brücke angesteuerten Getriebemotoren.

Die H-Brücke:
https://de.aliexpress.com/item/Hohe-Qualit-t-Neueste-H-Br-cke-Dual-Motor-Treiber-Laufwerk-Modul-Board-DC-MOSFET-IRF3205/32896741176.html

erfordert zwei DIR und zwei PWM-Signale für links und rechts.
Eigentlich kein Problem für mich.
Ich nehme also den Timer1 vom ATmega32, 16Mhz, P&F Correct PWM, Output 
Compare über OC1A und OC1B, ICR1 als TOP (=1000 > 8khz PWM Frequenz) für 
PWM und zwei normale IOs für DIR.

Variiert werden OCR1A und OCR1B in der while(1)-Schleife in der main.c. 
cli() wird davor aufgerufen und sei() danach, denn die Werte für OCR1A 
und OCR1B stammen vom ADC (Free Running Mode, 125khz Sampling Rate, 
Interrupt) an dem an zwei Kanälen je ein Poti 5k hängt.
Folgende Funktion kümmert sich um die Berechnung von OCR1A und B statt:
(Parameter "steering_poti_value" ist der 10-Bit-Wert von den ADCs, für 
links und rechts wird die Funktion zweimal aufgerufen)
1
//Park position: kleinere Werte bedeuten rückwärts, größere Werte bedeuten vorwärts
2
#define ZERO_SPEED_POS_VALUE 511
3
//Toleranzbereich des Potentiometerwerts um die Null-, Max- und Min-Position.
4
#define TOLERANCE 25
5
#define MAX_ADC_VALUE 1023
6
#define MAX_SPEED_VALUE 1000
7
8
unsigned int calc_speed_cmd (uint16_t steering_poti_value)
9
{  
10
  if (steering_poti_value > (ZERO_SPEED_POS_VALUE + TOLERANCE)){
11
    return ( (uint16_t) (float)(steering_poti_value - ZERO_SPEED_POS_VALUE) *  (float)MAX_SPEED_VALUE / (float)(MAX_ADC_VALUE - ZERO_SPEED_POS_VALUE));
12
  }
13
  if (steering_poti_value < (ZERO_SPEED_POS_VALUE - TOLERANCE)){
14
    return ( (uint16_t) ((float)MAX_SPEED_VALUE - ((float)steering_poti_value *  (float)MAX_SPEED_VALUE / (float)ZERO_SPEED_POS_VALUE)));
15
  }
16
  return 0;
17
}


Nun zum Problem. Wie im Video 
(https://1drv.ms/v/s!ApNuZK_DFrUOgfM83u_v-ly2vpnh0g) stelle ich bis zur 
Zeitmarke 0:20 nur den einen PWM Kanal (CH2 auf dem Oszi) mit dem Poti 
ein. Ich fahre einmal von der Mittelstellung auf den unteren Anschlag 
vom Poti(0 Ohm) und dann über die Mittelstellung zum oberen  Anschlag 
(5kOhm) und wieder zurück zur Mitte. Dann stelle ich ab 0:20 den zweiten 
PWM Kanal (CH1) auf ein von der Mittelstellung abweichenden Wert und 
wiederhole die Prozedur mit dem ersten Kanal (CH2 auf Oszi). Ab der 
Zeitmarke 0:37 ist zu sehen ist, dass ich den Kanal nicht mehr richtig 
getriggert bekomme. Ich vermute, dass es an einem Übersprechen des 
anderen Kanals liegt. Ich habe extra alles von den PWM-Pins abgelötet, 
um durch die Leiterbahnen/Jumperkabel keine kapazitive Kopplung zwischen 
die beiden Kanäle zu bekommen - bringt aber nichts.

Liegt dies dann überhaupt noch an einem elektrischen Problem oder soll 
ich in der Software suchen?

Kann gerne den ganzen Sourcecode posten, aber der ist halt für einen 
Beitrag recht umfangreich.

von Wolfgang (Gast)


Lesenswert?

Timo N. schrieb:
> Liegt dies dann überhaupt noch an einem elektrischen Problem oder soll
> ich in der Software suchen?

Das werden dir dein Debugger und Kontrollausgaben verraten.
Guck dir die Werteverteilung an, die von deinem ADC geliefert wird und 
was calc_speed_cmd() daraus für Steuerwerte generiert.

Zum Prüfen des Algorithmus muss die ganze Sache vielleicht nicht mit 
125khz laufen.
Wie schnell willst du eigenlich am Poti reißen, damit eine Abtastrate 
von 125kSa/s irgendwelchen Informationsgewinn bringt. Ist sicher, dass 
dein ADC schnell genug getaktet wird, um das zu schaffen? Lässt du nach 
einer Umschaltung des Multiplexers genug Zeit, damit sich am Eingang des 
ADC die Spannung an den neuen Wert ausreichend genau anpassen kann?

von Timo N. (tnn85)


Lesenswert?

Ich habe keinen Debugger. Ausgabe über UART könnte ich machen.


125khz ist die geringste Frequenz, die ich mit 16Mhz bekommen kann (128 
Prescaler). Hab die Werte nun auch noch durch eine gleitende 
Mittelwertberechnung gejagt (Mittelwert aus 16).

Ich habe festgestellt, dass ich nur bei Werten unterhalb des ADC Werts 
von 511 Probleme mit dem "Übersprechen" habe. Das würde für einen 
Softwarefehler sprechen (Die Spannung des Spannungsteiler am Poti ist 
stabil).

Was meinst du mit genügend Zeit?
ADC läuft im Free Running Modus:
1
//ADC für kontinuierliche Messung konfigurieren
2
ADMUX = (1 << REFS0); // external voltage reference on AVCC pin with external capacitor at AREF pin
3
4
#if (F_CPU == 4000000)
5
ADCSRA = (1 << ADEN) | (1 << ADSC) | (1 << ADATE) | (1 << ADPS2) | (1 << ADPS0) | (1 << ADIE); // ADC enable, start conversion, clk/32 -> ADC frequency = 125 khz, ADC interrupt enabled (for 4 Mhz F_CPU)
6
#else 
7
ADCSRA = (1 << ADEN) | (1 << ADSC) | (1 << ADATE) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0) | (1 << ADIE);// ADC enable, start conversion, clk/128 -> ADC frequency = 125 khz, ADC interrupt enabled (for 16 Mhz F_CPU and other frequencies)
8
#endif
9
    
10
#if defined(__AVR_ATmega32__)
11
SFIOR  = 0; // ADC Auto Trigger off -> Free Running mode
12
#elif defined(__AVR_ATmega1284P__)
13
ADCSRB = 0; // ADC Auto Trigger off -> Free Running mode
14
#endif

von Christian S. (roehrenvorheizer)


Lesenswert?

Hallo,

vielleicht verwechselst Du den ADC-Takt mit der Samplingrate.

"By default, the successive approximation circuitry requires an input 
clock frequency between 50 kHz and 200 kHz to get maximum resolution. If 
a lower resolution than 10 bits is needed, the input clock frequency to 
the ADC can be higher than 200 kHz to get a higher sample rate.
The ADC module contains a prescaler, which generates an acceptable ADC 
clock frequency from any CPU frequency above 100 kHz. The prescaling is 
set by the ADPS bits in ADCSRA. The prescaler starts counting from the 
moment the ADC is switched on by setting the ADEN bit in ADCSRA. The 
prescaler keeps running for as long as the ADEN bit is set, and is 
continuously reset when ADEN is low."

MfG

von Timo N. (tnn85)


Lesenswert?

Ja ok. Die Sampling Rate ist dann 125khz / 13 (weil im Free Running Mode 
13 ADC Clock Cycles für eine Conversion benötigt werden).

Das habe ich in meinem ersten Post falsch geschrieben. Hast du Recht.

von Peter D. (peda)


Lesenswert?

Den Free Running Mode nehme ich nur, wenn nur ein Eingang gemessen 
werden soll.
Mußt Du aber den MUX umstellen, dann kann das im Free Running Mode in 
die Hose gehen, wenn der Interrupt nicht rechtzeitig aufgerufen wird. 
Daher starte ich dann erst die Messung, nachdem der MUX umgeschaltet 
wurde.

von Timo N. (tnn85)


Lesenswert?

Dachte so funktioniert es?
ADC_UPDATE wird extern auf 0 gesetzt (ist ein Flag um zu sehen, dass 
neue Daten existieren
AddToFloagAvg fügt den neuen wert in den Ringbuffer für die gleitende 
Mittelwertberechnung ein.

1
ISR(ADC_vect)
2
{
3
  ADC_UPDATE = 1;
4
  if ( ADMUX & (1 << MUX0) ) {
5
    //STEERING_POTI_L = ADCL + (ADCH << 8);
6
    AddToFloatAvg(&STEERING_POTI_L_AVG,  ADCL + (ADCH << 8));
7
    ADMUX &= ~(1 << MUX0); //löscht MUX0 Bit: ADC4 Eingang als nächstes lesen
8
  }
9
  else {
10
    //STEERING_POTI_R = ADCL + (ADCH << 8);
11
    AddToFloatAvg(&STEERING_POTI_R_AVG, ADCL + (ADCH << 8));
12
    ADMUX |= (1 << MUX0); //setzt MUX0 Bit: ADC5 Eingang als nächstes lesen
13
  }
14
}


Ich habe den gleitenden Mittelwert jetzt von 16 Werten auf 256 Werte 
erhöht. Nun habe ich keine Störungen mehr.
Ich würde es aber gerne Wissen, warum ich so ausreißer bei den 
ADC-Werten habe (daran lag es ja anscheinend).

von Peter D. (peda)


Lesenswert?

Timo N. schrieb:
> AddToFloatAvg(&STEERING_POTI_L_AVG,  ADCL + (ADCH << 8));

Damit ist die Reihenfolge undefiniert, aber sie ist wichtig: "ADCL must 
be read first, then ADCH".
Laß es daher den Compiler machen:
1
AddToFloatAvg(&STEERING_POTI_L_AVG,  ADC);

von Reiner_Gast (Gast)


Lesenswert?

Aus dem Datenblatt geht hervor:

If both ADATE and ADEN is written to one, an interrupt event can occur 
at any time. If the
ADMUX Register is changed in this period, the user cannot tell if the 
next conversion is based
on the old or the new settings. ADMUX can be safely updated in the 
following ways:
1. When ADATE or ADEN is cleared.
2. During conversion, minimum one ADC clock cycle after the trigger 
event.
3. After a conversion, before the Interrupt Flag used as trigger source 
is cleared.
When updating ADMUX in one of these conditions, the new settings will 
affect the next ADC
conversion.

D.h. es kann sein, dass zum Zeitpunkt der Umschaltung des Multiplexers 
schon die nächste Messung gestartet wurde und ggf. verfälscht wird.

Evtl. hilft es hier nach dem Umschalten der Multiplexers erstmal eine 
"Opfer"-Messung des ADC durchzuführen und zu verwerfen.

von Timo N. (tnn85)


Lesenswert?

Tja, ich weiß nicht wie ich es sonst machen soll.
Ich warte ja auf den Interrupt und schaue darauf wie die ADMUX Bits 
gesetzt sind. Dann lese ich entsprechend den Wert und schalte dann 
gleich auch um.

Du meinst dann wohl, dass ich mir hier ein Flag setzten soll
1
ISR(ADC_vect)
2
{
3
  if (OPFERMESSUNG_COMPLETE)
4
    ADC_UPDATE = 1;
5
    if ( ADMUX & (1 << MUX0) ) {
6
      //STEERING_POTI_L = ADCL + (ADCH << 8);
7
8
      AddToFloatAvg(&STEERING_POTI_L_AVG,  ADCL + (ADCH << 8));
9
      ADMUX &= ~(1 << MUX0); //löscht MUX0 Bit: ADC4 Eingang als nächstes lesen
10
      OPFERMESSUNG_COMPLETE = 0;
11
    }
12
    else {
13
      //STEERING_POTI_R = ADCL + (ADCH << 8);
14
      AddToFloatAvg(&STEERING_POTI_R_AVG, ADCL + (ADCH << 8));
15
      ADMUX |= (1 << MUX0); //setzt MUX0 Bit: ADC5 Eingang als nächstes lesen
16
      OPFERMESSUNG_COMPLETE = 0;
17
    }
18
   else
19
     OPFERMESSUNG_COMPLETE = 1;
20
}

Dann würde immer eine Messung durchlaufen, Interrupt getriggert, aber 
der Wert verworfen. Richtig?

von Reiner_Gast (Gast)


Lesenswert?

Das würde ich ggf. so schreiben:
1
ISR(ADC_vect)
2
{
3
  if (OPFERMESSUNG_COMPLETE) {
4
    OPFERMESSUNG_COMPLETE--;
5
  } else {
6
    ADC_UPDATE = 1;
7
    if ( ADMUX & (1 << MUX0) ) {
8
      AddToFloatAvg(&STEERING_POTI_L_AVG,  ADC);
9
      ADMUX &= ~(1 << MUX0); //löscht MUX0 Bit: ADC4 Eingang als nächstes lesen
10
    } else {
11
      AddToFloatAvg(&STEERING_POTI_R_AVG, ADC);
12
      ADMUX |= (1 << MUX0); //setzt MUX0 Bit: ADC5 Eingang als nächstes lesen
13
    }
14
    OPFERMESSUNG_COMPLETE=1;
15
}

von Matthias S. (Firma: matzetronics) (mschoeldgen)


Lesenswert?

Timo N. schrieb:
> AddToFloatAvg(&STEERING_POTI_L_AVG,  ADCL + (ADCH << 8));

Die grosse Frage ist, wie lange diese Routine dauert. Ansonsten würde 
ich den ADC nur einmal triggern beim Initialisieren und ihn dann immer 
am Ende der ISR wieder starten. So ist gewährleistet, das der nächste 
ADC-IRQ erst kommt, wenn der hier fertig bearbeitet ist.

von Timo N. (tnn85)


Lesenswert?

Naja, viel macht die nicht 
(https://rn-wissen.de/wiki/index.php?title=Gleitender_Mittelwert_in_C).

Ich frage mich eben, warum überhaupt FreeRunning-Mode, wenn es nicht 
genutzt werden kann. Das kann ich mir nicht vorstellen.
Soll der Modus nur ohne Multiplexing (also nur für einen Kanal) gedacht 
sein? Fraglich.

Ich habe ja jetzt mit dem Gleitenden Mittelwert über 255 Werte ein 
akzeptables Ergebnis. Eventuell versuche ich es noch mit der Lösung, die 
Reiner_Gast vorgeschlagen hat.

von Matthias S. (Firma: matzetronics) (mschoeldgen)


Lesenswert?

Check doch mal bitte die Hardware am ADC Eingang. Das sollte wenig 
Quellimpedanz haben (5k Poti wäre aber ok) und vorzugsweise noch einen 
kleinen externen C (z.B. 1nF - 3,3nF) direkt an jedem ADC Eingang. Dann 
wird das Übersprechen recht klein.

von Irgendwer (Gast)


Lesenswert?

So als Vorschlag in preudocode

<xxx> = optional

global mux
global machwas
global adc_ergebnis[2]
<global muell_messung>

ISR_Timer_alle_25ms (so wird jeder mux z.b. 20 mal je sek. aktualisiert)
  Start ADC
end

ISR_ADC_fertig
  <if (!muell_messung)>
    Read and Save Ergebnis [mux]
    if mux==0
      mux==1
      Set_Mux(2)
      <muell_messung=1>
      <Start ADC>
    else
      mux==0
      Set_Mux(1)
      <muell_messung=1>
      <Start ADC>
    endif
    machwas = 1
  <else>
    <muell_messung=0>
end

main
  init ADC
  init Timer

  adc_ergebnis[2]=0
  mux=0
  machwas=1 (je nachdem ob gleich was zu tun ist oder erst nach der 
ersten messung)
  muell_messung=1
  start ADC

  start timer

  do
    if (machwas)
      tu_irgendwas()
      machwas = 0
    endif
    schlafe_bis_zum_nächsten_event()
  while_endlos

end

Damit laufen die ADC messungen und das umschalten der Mux schön synchron 
und nach dem jeweiligen umschalten ist auch etwas Zeit für die S&H sich 
auf den neuen Wert einzustellen. Zur Not je nach Prozessor nach dem 
umschalten der Mux erst noch eine "Opfermessung" zum wegwerfen.
Und viel mehr Hektik ist beim abfragen eines Potis meistens auch 
überhaupt nicht notwendig - schon garnicht im kHz Bereich.

von Einer K. (Gast)


Lesenswert?

Timo N. schrieb:
> Ich frage mich eben, warum überhaupt FreeRunning-Mode, wenn es nicht
> genutzt werden kann.
Natürlich kann der genutzt werden.
Schließlich ist er der Modus mit der höchsten Sampelrate.

Aber nur eben auf einem Kanal, ohne Verluste.


Merke --> Du brauchst den free running Mode nicht.
Denn ebenso gut, kannst du in der ISR den Kanal umschalten und dann die 
Messung wieder starten.
Die paar Samples, welche du dadurch verlierst, sind doch bei einem Poti 
egal...

Timo N. schrieb:
> Ich habe ja jetzt mit dem Gleitenden Mittelwert über 255 Werte ein
> akzeptables Ergebnis.
Du arbeitest mit korrupten Daten.
Das muss doch nicht, oder?

Timo N. schrieb:
> Eventuell versuche ich es noch mit der Lösung, die
> Reiner_Gast vorgeschlagen hat.
Es gibt einige bessere Lösungen, als deine Variante.

von Timo N. (tnn85)


Lesenswert?

Hallo Fanboy,

klar sind die paar Samples egal. Ging nur um Grundsätzliches. Kann ja 
gut sein, dass ich wann anders wirklich brauche.

Wieso sind gefilterte Daten = korrupte Daten? Die paar Samples die 
"falsch" gesamplet wurden, fallen bei 255 Werten nicht ins Gewicht. 
Deswegen ja Filtern.


Es gibt immer bessere Varianten. Schon klar.

von Einer K. (Gast)


Lesenswert?

Timo N. schrieb:
> Wieso sind gefilterte Daten = korrupte Daten?

Du filterst korrupte Daten.
Schrott sammeln, und dann wieder gerade rechnen.
Kostet Zeit und Speicher.
Was du nicht machen müsstest, wenn du "geschickter" sampeln würdest.


Timo N. schrieb:
> klar sind die paar Samples egal.
Ja dann....
Steht doch einer anderen Strategie nichts mehr im Wege....

von Peter D. (peda)


Lesenswert?

Irgendwer schrieb:
> Zur Not je nach Prozessor nach dem
> umschalten der Mux erst noch eine "Opfermessung" zum wegwerfen.

Der AVR braucht keine Opfermessung.
Wichtig ist aber, die Reihenfolge beim Lesen von ADC einzuhalten!
ADCH wird beim Lesen von ADCL gelatcht.
Und wenn man die Messung nicht freerunning, sondern erst nach MUX 
umschalten startet, dann kann sich auch nichts in die Quere kommen.

Der Mittelwert über 256 Messungen verschleiert nur, daß da was nicht 
stimmt.

von Matthias S. (Firma: matzetronics) (mschoeldgen)


Lesenswert?

Hier die ADC Kernroutine meines neusten Projektes - eine Herdsteuerung 
mit 4 ADC Kanälen (allerdings nur 8 Bit, weil ich es besser hier nicht 
brauche):
1
/* ADC complete interrupt 
2
 * this version scans 4 channels (0 to 3) and 
3
 * places the results in globals channel1 to channel4 */
4
ISR(ADC_vect)
5
{
6
  switch (ADMUX)
7
    {
8
      case ADMUX_CH1:
9
            channel1 = (channel1 + ADCH) >> 1; // do some averaging with the last value obtained
10
          ADMUX = ADMUX_CH2;
11
          break;
12
      case ADMUX_CH2:
13
          channel2 = (channel2 + ADCH) >> 1;
14
          ADMUX = ADMUX_CH3; 
15
          break;        
16
      case ADMUX_CH3:
17
          channel3 = (channel3 + ADCH) >> 1;
18
          ADMUX = ADMUX_CH4; 
19
          break;        
20
      case ADMUX_CH4:
21
          channel4 = (channel4 + ADCH) >> 1;
22
          ADMUX = ADMUX_CH1; 
23
          break;        
24
      default: ADMUX = ADMUX_CH1;
25
        break;
26
   }
27
// restart the ADC
28
   ADCSRA = (1 << ADEN) | (1 << ADSC) | (0 << ADATE) | (1 << ADIF) | (1 << ADIE) | ADC_PRESCALER;
29
}
Dazu gehören noch ein paar defines, die ich nicht poste, weil es nur ums 
Ausleseprinzip geht.

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.