Forum: Mikrocontroller und Digitale Elektronik AVR, ATmega48, PWM, FastPWM, Problem


von Martin Baum (Gast)


Lesenswert?

Hallo, ich bin noch recht frisch in der MC Welt.

Ich möchte sechs Servo-Motoren mit einem ATmega48 betreiben.

Dazu benötige ich ein Fast-PWM Signal, mit einem Grundtakt von 20 ms und 
variablen Impulslängen (1-Signal) von 1 ms bis 2 ms.

Ich habe ein erstes Experiment mit Timer1 gemacht:

#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/signal.h>
#include <avr/delay.h>

// Geblinkt wird PortB.1 (push-pull)
#define PAD_LED  1
#define PORT_LED PORTB
#define DDR_LED  DDRB

#define F_CPU    1000000

uint8_t temp;

void timer1_init() {
   TCCR1A = (1 << WGM12) | (1 << WGM11) | (1 << WGM10) | (1 << COM1A1);
   TCCR1B =  (1 << CS11);

   OCR1A = 62;
   TIMSK1 |= (1 << OCIE1A);
}

SIGNAL (SIG_OUTPUT_COMPARE1A) {
   if ((PAD_LED << PORT_LED) == 1) {
      PORT_LED |= (1 << PAD_LED);
   } else {
      PORT_LED &= ~(1 << PAD_LED);
   }
}

int main() {
   DDR_LED |= (1 << PAD_LED);
   timer1_init();
   sei();
   while (1) {
      temp = 0;
      while(temp < 6) {
         _delay_ms(262);
         temp++;
      }
      if (OCR1A == 62) OCR1A = 125;
      else OCR1A = 62;
   }
   return 0;
}

Ich benutze Mode 7. Somit habe ich FastPWM mit einer Auflösung N von 
1024 (10 Bit). Mein CPU-Takt (F_CPU) sind die internen 1 MHz. Und einen 
eingestellten Vorteiler von 8 (CS11).

Grundfrequenz = (F_CPU / Vorteiler) / (N * 2) = (1e+06 Hz / 8) / (1024 * 
2) = ~61 Hz. --> 16,4 ms.

Impulslänge 1 ms: 1024 / 16.4 * 1 = 62
Impulslänge 2 ms: 1024 / 16.4 * 2 = 125

Die 16,4 ms Näherungsweise an meinen 20 ms. OCR1A wechselt testweise 
zwischen 62 und 125.


Nun möchte ich die Grundfrequenz aber genau einstellen. Dazu möchte ich 
Mode 14 verwenden und TOP über ICR1 festlegen:

void timer1_init() {
   TCCR1A = (1 << WGM13) | (1 << WGM12) | (1 << WGM11) | (1 << COM1A1);
   TCCR1B =  (1 << CS11);
   ICR1 = 0x03FF;
   OCR1A = 40;
   TIMSK1 |= (1 << OCIE1A);
}

Ich habe ICR1 auf den ursprünglichen Wert von Mode 7 gesetzt. Nur ist 
das Ergebnis nicht mit dem oberen Programm identisch. Der Grundtakt 
beträgt nu 8.2 ms, also doppelt so schnell. Auch Veränderungen an ICR1 
haben keine Auswirkungen. Die Impulslänge wechselt aber noch korrekt 
zwischen 1 ms und 2 ms.

Wo ist mein Fehler? Wieso wird ICR1 nicht ausgewertet? Ich habe es auch 
mit ICR1H = 0x3 und ICR1L = 0xFF versucht, was das gleiche Ergebnis 
lieferte.

Viele Gruesse
  Martin (martin.baum AT berlin.de)

von Martin Baum (Gast)


Lesenswert?

Weiterhin habe ich noch eine Frage. ;-)

Ich möchte 6 Servo-Motoren betreiben. Dies könnte ich mit allen 6 
PWM-Kanälen des ATmega48 machen. Ist die Ansteuerung von Timer0 und 
Timer2 ähnlich?

Meine alternative Idee war es, die Grundfrequenz meines Timer1 nicht auf 
20 ms, sondern auf 20/6 ms zu stellen.

Da die benötigte Impulslänge 2 ms beträgt und die Taktlänge insgesamt 
20ms lang ist, könnte ich innerhalb eines Taktes auch alle Impulse 
Zeitversetzt anordnen. Die kleine Verzögerungen wäre vernachlässigbar.

Sprich, ich bekomme alle 20/6 ms einen Interrupt und steuer in der ISR 
immer einen anderen Ausgang des Port C an, an dehnen die Motoren hängen. 
Ist das pragmatisch oder ist Verwendung aller Timer und PWM-Ausgänge zu 
empfehlen?

Vielen Dank für eure Tips.
  Martin

von johnny.m (Gast)


Lesenswert?

Dir ist klar, dass WGM12 und WGM13 in TCCR1B stehen und nicht in TCCR1A?

von johnny.m (Gast)


Lesenswert?

> Ist die Ansteuerung von Timer0 und Timer2 ähnlich?
Timer 0 und 2 sind 8-Bit-Timer. In der Hinsicht ist da schon ein 
erheblicher Unterschied in der Konfiguration.

von Martin Baum (Gast)


Lesenswert?

Ja, danke. Ich habe mein TCCR1B auch gerade entdeckt. :-(

void timer1_init() {
   TCCR1A = (1 << WGM11) | (1 << COM1A1);
   TCCR1B = (1 << WGM12) | (1 << WGM13) | (1 << CS11);
   ICR1 = 0x0800;
   OCR1A = 80;
   TIMSK1 |= (1 << OCIE1A);
}

Funktioniert nun korrekt. Nun wird alle 16,4 ms ein Interrupt 
ausgeführt.

Da komme ich zu meinem nächsten Problem, 6 Motoren zu betreiben.

// Motoren haengen alle an Port C
#define DDR_MOTOR DDRC
#define PORT_MOTOR PORTC
#define MOTOR_1 5
#define MOTOR_2 4

uint8_t motor; // motor = 0;

SIGNAL (SIG_OUTPUT_COMPARE1A) {
  motor++;
  PORT_MOTOR &= ~(1 << MOTOR_1); // Motor1 aus
  PORT_MOTOR &= ~(1 << MOTOR_2); // Motor2 aus
  if (motor == 1) {
    PORT_MOTOR |= (1 << MOTOR_1); // Motor1 ein
  } else {
    motor = 0;
    PORT_MOTOR |= (1 << MOTOR_2);  // Motor2 ein
  }
}

Dies ist natürlich alles Schwachsinnig, da der Interrupt nur alle 16 ms 
kommt und die Ausgangspins MOTOR_1, ... in dieser Zeit nicht verändert 
werden. Erst beim nächsten Interrupt. Nur der PWM-Ausgang verhält sich 
korrekt.


Wie kann ich sinnvoll 6 Ausgangspins mit PWM-Signalen beschalten? Geht 
das Softwaremäßig über Interrupts wie oben beschrieben oder nur durch 6 
echte PWM-Ausgänge?

Möglichkeit 1: Ich erzeuge einen normalen Takt, der z.B alle 16/120 ms 
einen Interrupt auslöst. Die ersten 20 ISR-Aufrufe sind für Motor1 und 
bieten im eine Auflösung von 20. Die nächsten 20 ISR-Aufrufe sind für 
Motor2, usw.

Möglichkeit 2: Alle Timer mit PWM-Ausgängen verwenden.

Was ist sinnvoller? Danke schon einmal für weitere Tips.

von Jens P. (jmoney)


Lesenswert?

Die Idee mit dem Umschalten finde ich sogar sehr gut. Benutz doch dazu 
den Timer Overflow Interrupt. Ein shift und eine Kontrolle, ob du schon 
im 7. Bit angelangt bist, sollte genügen. Dann muss natürlich noch der 
PWM-Wert für den entsprechenden Pin aktualisiert werden.

von Hannes L. (hannes)


Lesenswert?

> Wie kann ich sinnvoll 6 Ausgangspins mit PWM-Signalen beschalten? Geht
> das Softwaremäßig über Interrupts wie oben beschrieben oder nur durch 6
> echte PWM-Ausgänge?

Falls Du mit "PWM" Signale für Modellbauservos meinst, dann geht das 
auch in Software ohne Verwendung der PWM-Features der Timer. Ein 
Beispiel mit Mega48 findest Du hier:
http://www.hanneslux.de/avr/mobau/7ksend/7ksend02.html

...

von Hobbytroniker (Gast)


Lesenswert?

@ Martin Baum:
Wenn Du sonst nichts auf dem µC groß zu tun hast kannst Du eine Software 
PWM realisieren, die dann auf beliebigen Pins ausgegeben werden kann.
Dazu reicht ein Timer aus und in dessen ISR mußt Du die Pins 
entsprechend toggeln, so wie Du das ja schon tust ;)
Ansonsten bleibt Dir nichts anderes übrig als die PWM-Pins zu nehmen, 
also PB1, PB2, PB3, PD3, PD5 und PD6 !
Ich persönlich würde die Hardware PWM nehmen und alle Timer als 8bit 
Fast PWM initialisieren.
Nach Deinem Code zu urteilen benutzt Du den internen RC-Oszillator bei 
1MHz.
Wenn Du einen externen Quarz mit z.B. 8MHz nimmst hast Du mehr Luft und 
einen genaueren Takt = Zeiten ;)

von Martin Baum (Gast)


Lesenswert?

Danke für die ganzen Tips. :-)

Ich habe mich nu für einen normalen Takt des Timer1 entschieden und 
möchte die PWM-Signale softwaremäßig generieren. Dies funktioniert 
soweit auch.

Die Impulslänge (und damit die Stellung der Motoren) möchte ich in der 
main() Funktion verändern. Dazu habe ich testweise die Variable 
pos_motor1. Diese soll alle paar Sekunden zwischen 12 und 22 wechseln:

#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/signal.h>
#include <avr/delay.h>

// Motoren haengen alle an Port C
#define DDR_MOTOR DDRC
#define PORT_MOTOR PORTC
#define MOTOR_1 5
#define MOTOR_2 4

#define MOTOR_1_EIN PORT_MOTOR |= (1 << MOTOR_1)
#define MOTOR_1_AUS PORT_MOTOR &= ~(1 << MOTOR_1)

#define MOTOR_2_EIN PORT_MOTOR |= (1 << MOTOR_2)
#define MOTOR_2_AUS PORT_MOTOR &= ~(1 << MOTOR_2)

#define F_CPU    1000000

uint8_t temp;
uint8_t motor;
uint8_t pos_motor1;

void timer1_init () {
   TCCR1A = (1 << COM1A1);
   TCCR1B = (1 << WGM12) | (1 << CS11);
   OCR1A = 9;
   TIMSK1 |= (1 << OCIE1A);
}

SIGNAL (SIG_OUTPUT_COMPARE1A) {
   motor++;
   if ((motor > 0) && (motor < pos_motor1)) // 0,9 - 1,7 ms
      MOTOR_1_EIN;
   else
      MOTOR_1_AUS;

   if ((motor > 40) && (motor < 40+22))  // 1,7 ms
      MOTOR_2_EIN;
   else
      MOTOR_2_AUS;

   // [ ... weitere Motoren ... ]

   if (motor >= 480) motor = 0; // nach 20 ms von vorne
}

int main () {
   DDR_MOTOR |= (1 << MOTOR_1) | (1 << MOTOR_2);
   timer1_init ();
   sei ();
   motor = 0;
   pos_motor1 = 22;
   while (1) {

      temp = 0;             /* Pause */
      while (temp < 6) {
         _delay_ms (262);
         temp++;
      }

      if (pos_motor1 == 22) pos_motor1 = 12;
      else pos_motor1 = 22;
   }
   return 0;
}

Leider wird aber in der while-Schleife der Wert von pos_motor1 nicht 
mehr veraendert oder zumindest im Interrupt nicht beruecksichtigt. Die 
Zuweisung in der main()-Funktion ueber der while-Schleife funktioniert 
noch.

Der Vergleich von pos_motor1 in der ISR sollte doch problemlos 
funktionieren, oder?

Viele Gruesse
  Martin

von johnny.m (Gast)


Lesenswert?

Ein Problem könnte sein, dass Deine globalen Variablen, auf die Du 
sowohl in der ISR als auch im Hauptprogramm zugreifst, nicht "volatile" 
deklariert sind. Das kann dazu führen, dass Zugriffe auf die Variablen 
wegoptimiert werden, was bedeuten würde, dass u.U. das Hauptprogramm gar 
nicht mitbekommt, dass sich etwas tut. Also zumindest motor und 
pos_motor mit dem Typqualifizierer volatile versehen:
1
volatile uint8_t pos_motor1, motor;

BTW: "SIGNAL" ist mittlerweile veraltet. Es wird zwar (afaik) von der 
aktuellen WINAVR-Version noch unterstützt, Du solltest aber wenns geht 
im Hinblick auf zukünftige Projekte auf das aktuelle "ISR" umsteigen.

von Martin Baum (Gast)


Lesenswert?

Mit "volatile" verändert sich das Verhalten nicht. :-((

Das "SIGNAL" veraltet ist, hatte ich bereits gelesen, doch habe ich mit 
meinem avr-gcc (GCC) 4.0.2 es bisher leider nicht mit "ISR" geschafft. 
Und ist momentan auch nicht so wichtig, da das Projekt eh bald wieder 
vorbei ist. ;-)

Weitere Tips für oberes Problem sind gern willkommen.

Martin

von Martin Baum (Gast)


Lesenswert?

Das zeitversetzte schalten der Ausgänge ist ja nicht mehr notwendig. Und 
mit der vereinfachten if-Anweisung funktioniert es nun problemlos. :-)

SIGNAL (SIG_OUTPUT_COMPARE1A) {
   motor++;
   if (motor < pos_motor1)
      MOTOR_1_EIN;
   else
      MOTOR_1_AUS;

   if (motor < pos_motor2)
      MOTOR_2_EIN;
   else
      MOTOR_2_AUS;

   if (motor >= 480) motor = 0;
}

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.