www.mikrocontroller.net

Soft-PWM

Inhaltsverzeichnis

[bearbeiten] Einleitung

PWM ist eine oft verwendete Funktion auf dem Gebiet der Mikrocontroller. Damit lassen sich vielfältige Aufgaben lösen, wie beispielsweise die Leistungssteuerung von Motoren, Helligkeitssteuerung von LEDs, Digital-Analog Wandlung und vieles mehr. Die meisten Mikrocontroller haben ein oder mehrere PWM-Module eingebaut, womit ohne CPU-Belastung eine PWM generiert werden kann. Jedoch kommt es bisweilen vor, daß die Anzahl der verfügbaren PWM-Kanäle nicht ausreicht. Dann muß eine Softwarelösung gefunden werden, bei der die CPU die PWM-Generierung vornimmt, allgemein Soft-PWM genannt.

[bearbeiten] Einfacher Lösungsansatz

Ein sehr einfacher Lösungsansatz findet sich im AVR-Tutorial: PWM. Hier wird schon ein Timer benutzt, um in regelmäßigen Abständen die PWM-Generierung durchzuführen. Damit verbleibt noch Rechenzeit für andere Aufgaben, außerdem wird die Programmierung wesentlich vereinfacht. Allerdings ist das Beispiel in ASM, hier soll das ganze in C gemacht werden.

Es soll nun eine 8 Bit PWM mit 100 Hz (PWM-Zyklus 10ms) und acht Kanälen generiert werden. Der verwendete Controller ist ein AVR vom Typ ATmega32, welcher mit dem internen RC-Oszillator auf 8 MHz getaktet wird. Das Programm kann jedoch problemlos an so ziemlich jeden anderen AVR angepaßt werden. Die Programme wurden mit dem Optimierungsgrad -Os compiliert.

[bearbeiten] Erster Versuch

Das Programm ist recht kurz und übersichtlich. Im Hauptprogramm wird der Timer 1 initialisiert und der Output Compare 1A als variabler Timer verwendet, wobei die Output Compare Funktion nicht mit dem IO-Pin verbunden ist. Im Interrupt, welcher regelmäßig aufgerufen wird, werden nun in einer Schleife alle acht Kanäle geprüft. Alle Kanäle werden auf HIGH gesetzt, welche eine PWM-Einstellung größer als der aktuelle Zykluszähler haben. Sinnvollerweise werden erst alle Kanäle geprüft und das Ergebnis zwischengespeichert, am Ende erfolgt nur ein Zugriff auf den Port.

/*
    Eine 8-kanalige PWM mit einfachem Lösungsansatz
 
    ATmega32 @ 8 MHz
 
*/
 
// Defines an den Controller und die Anwendung anpassen
 
#define F_CPU 8000000L                  // Systemtakt in Hz
#define F_PWM 100                       // PWM-Frequenz in Hz
#define PWM_STEPS 256                   // PWM-Schritte pro Zyklus(1..256)
#define PWM_PORT PORTD                  // Port für PWM
#define PWM_DDR DDRD                    // Datenrichtungsregister für PWM
 
// ab hier nichts ändern, wird alles berechnet
 
#define T_PWM (F_CPU/(F_PWM*PWM_STEPS)) // Systemtakte pro PWM-Takt
 
#if (T_PWM<(152+5))
    #error T_PWM zu klein, F_CPU muss vergrösst werden oder F_PWM oder PWM_STEPS verkleinert werden
#endif
 
// includes
 
#include <stdint.h>
#include <string.h>
#include <avr/io.h>
#include <avr/interrupt.h>
 
// globale Variablen
 
volatile uint8_t pwm_setting[8];                    // Einstellungen für die einzelnen PWM-Kanäle
 
// Timer 1 Output COMPARE A Interrupt
 
ISR(TIMER1_COMPA_vect) {
    static uint8_t pwm_cnt=0;
    uint8_t tmp=0, i=0, j=1;
 
    OCR1A += (uint16_t)T_PWM;
 
    for (; i<8; i++) {    
    	if (pwm_setting[i] > pwm_cnt) tmp |= j;
            j<<=1;
	}
    PWM_PORT = tmp;                         // PWMs aktualisieren
    if (pwm_cnt==(uint8_t)(PWM_STEPS-1))
        pwm_cnt=0;
    else
        pwm_cnt++;
}
 
int main(void) {
 
    // PWM einstellen
    
    PWM_DDR = 0xFF;         // Port als Ausgang
    
    // Timer 1 OCRA1, als variablem Timer nutzen
 
    TCCR1B = 1;             // Timer läuft mit vollem Systemtakt
    TIMSK |= (1<<OCIE1A);   // Interrupt freischalten
 
    sei();                  // Interrupts gloabl einschalten
 
/*********************************************************************/
// nur zum Testen, im Anwendungsfall löschen
 
    volatile uint8_t tmp;
const uint8_t t1[8]={27, 40, 3, 17, 150, 99, 5, 9};
const uint8_t t2[8]={27, 40, 3, 0, 150, 99, 5, 9};
const uint8_t t3[8]={27, 40, 3, 17, 3, 99, 3, 0};
const uint8_t t4[8]={0, 0, 0, 0, 0, 0, 0, 0};
const uint8_t t5[8]={0, 0, 0, 0, 0, 0, 0, 9};
const uint8_t t6[8]={33, 33, 33, 33, 33, 33, 33, 33};
 
// Messung der Interruptdauer
    tmp =0;
    tmp =0;
    tmp =0;
 
// Debug 
 
    memcpy(pwm_setting, t1, 8);
    
    memcpy(pwm_setting, t2, 8);
 
    memcpy(pwm_setting, t3, 8);
 
    memcpy(pwm_setting, t4, 8);
 
    memcpy(pwm_setting, t5, 8);
    
    memcpy(pwm_setting, t6, 8);
 
/*********************************************************************/
 
    return 0;
}

Im AVR-Studio kann man den Code simulieren. Wichtig ist hier vor allem die Ausführungszeit des Interrupts. Bei 100 Hz PWM-Frequenz und 256 Schritten pro PWM-Zyklus wird diese Funktion immerhin 25600 mal pro Sekunde aufgerufen (PWM-Takt 25,6 kHz), bei 8MHz Taktfrequenz stehen damit maximal 312 Takte zur Verfügung. Glücklicherweise ist die Funktion relativ kurz und der GCC leistet gute Arbeit. Der Interrupt benötigt hier 152 Takte, es verbleiben also jeweils 160 Takte zur Bearbeitung anderer Aufgaben. Das entspricht einer CPU-Belastung von ~49%. Das Programm benötigt 284 Byte Programmspeicher. Nicht schlecht für den Anfang.

[bearbeiten] Zweiter Versuch

Wo gibt es in diesem Programm noch Optimierungsmöglichkeiten? Nur im Interrupt, denn das ganze Programm besteht ja praktisch nur aus der Interruptroutine. Betrachten wir die Schleifen genauer müssen wir feststellen, daß die Indizierung von pwm_setting[] etwas Rechenzeit benötigt. Ebenso die Schiebeoperation von tmp, auch wenn das nur acht mal ein Takt ist. Wir können jetzt per Hand das machen, was der Compiler auch manchmal macht. Die Rede ist vom Loop-Unrolling. Dabei wird die Schleife durch mehrere diskrete Befehle ersetzt (entrollt). Der Vorteil dabei ist, daß die Befehle zur Berechnung und Prüfung der Zählvariable entfallen, außerdem können ggf. Werte im Voraus berechnet werden. Als Ergebnis hat man zwar ein etwas größeres Programm, doch das wird schneller ausgeführt! Ausserdem orientiert sich diese Version mehr am Original der Assemblerversion. Dadurch wird sie zusätzlich ein wenig kürzer und schneller.

/*
    Eine 8-kanalige PWM mit verbessertem Lösungsansatz
 
    ATmega32 @ 8 MHz
 
*/
 
// Defines an den Controller und die Anwendung anpassen
 
#define F_CPU 8000000L                  // Systemtakt in Hz
#define F_PWM 100                       // PWM-Frequenz in Hz
#define PWM_STEPS 256                   // PWM-Schritte pro Zyklus(1..256)
#define PWM_PORT PORTD                  // Port für PWM
#define PWM_DDR DDRD                    // Datenrichtungsregister für PWM
 
// ab hier nichts ändern, wird alles berechnet
 
#define T_PWM (F_CPU/(F_PWM*PWM_STEPS)) // Systemtakte pro PWM-Takt
 
#if (T_PWM<(93+5))
    #error T_PWM zu klein, F_CPU muss vergrösst werden oder F_PWM oder PWM_STEPS verkleinert werden
#endif
 
// includes
 
#include <stdint.h>
#include <string.h>
#include <avr/io.h>
#include <avr/interrupt.h>
 
// globale Variablen
 
volatile uint8_t pwm_setting[8];                    // Einstellungen für die einzelnen PWM-Kanäle
 
// Timer 1 Output COMPARE A Interrupt
 
ISR(TIMER1_COMPA_vect) {
    static uint8_t pwm_cnt=0;
    uint8_t tmp=0;
 
    OCR1A += (uint16_t)T_PWM;
        
    if (pwm_setting[0] > pwm_cnt) tmp |= (1<<0);
    if (pwm_setting[1] > pwm_cnt) tmp |= (1<<1);
    if (pwm_setting[2] > pwm_cnt) tmp |= (1<<2);
    if (pwm_setting[3] > pwm_cnt) tmp |= (1<<3);
    if (pwm_setting[4] > pwm_cnt) tmp |= (1<<4);
    if (pwm_setting[5] > pwm_cnt) tmp |= (1<<5);
    if (pwm_setting[6] > pwm_cnt) tmp |= (1<<6);
    if (pwm_setting[7] > pwm_cnt) tmp |= (1<<7);
    PWM_PORT = tmp;                         // PWMs aktualisieren
    if (pwm_cnt==(uint8_t)(PWM_STEPS-1))
        pwm_cnt=0;
    else
        pwm_cnt++;
}
 
int main(void) {
 
    // PWM einstellen
    
    PWM_DDR = 0xFF;         // Port als Ausgang
    
    // Timer 1 OCRA1, als variablem Timer nutzen
 
    TCCR1B = 1;             // Timer läuft mit vollem Systemtakt
    TIMSK |= (1<<OCIE1A);   // Interrupt freischalten
 
    sei();                  // Interrupts gloabl einschalten
 
/*********************************************************************/
// nur zum Testen, im Anwendungsfall löschen
 
    volatile uint8_t tmp;
const uint8_t t1[8]={27, 40, 3, 17, 150, 99, 5, 9};
const uint8_t t2[8]={27, 40, 3, 0, 150, 99, 5, 9};
const uint8_t t3[8]={27, 40, 3, 17, 3, 99, 3, 0};
const uint8_t t4[8]={0, 0, 0, 0, 0, 0, 0, 0};
const uint8_t t5[8]={0, 0, 0, 0, 0, 0, 0, 9};
const uint8_t t6[8]={33, 33, 33, 33, 33, 33, 33, 33};
 
// Messung der Interruptdauer
    tmp =0;
    tmp =0;
    tmp =0;
 
// Debug 
 
    memcpy(pwm_setting, t1, 8);
    
    memcpy(pwm_setting, t2, 8);
 
    memcpy(pwm_setting, t3, 8);
 
    memcpy(pwm_setting, t4, 8);
 
    memcpy(pwm_setting, t5, 8);
    
    memcpy(pwm_setting, t6, 8);
 
/*********************************************************************/
 
    return 0;
}

Mit dieser Interruptroutine werden nur noch 93 Takte benötigt, die CPU-Belastung verringert sich auf ~30%. Nicht schlecht. Der Programmcode steigt auf 324 Byte, aber das ist im Angesicht der Leistungsverbesserung zu verschmerzen. Weiter verringern kann man die CPU-Belastung durch eine niedrigere PWM-Frequenz oder eine geringere Anzahl Stufen der PWM. Wenn man beispielsweise mit 64 (6 Bit) statt 256 (8 Bit) Stufen auskommt verringert sich die Belastung um den Faktor 4.

[bearbeiten] Intelligenter Lösungsansatz

Wenn auch eine CPU-Last von 30% recht akzeptabel erscheint, so hat man doch noch irgendwie das Gefühl, daß es noch besser geht. Aber wie? Mein Mathematikprofessor pflegte in so einem Fall die Anwendung der „Methode des scharfen Blicks“ ™. Was passiert eigentlich während eines gesamten PWM-Zykluses?

  • Zu Beginn werden alle IO-Pins gesetzt, deren PWM-Einstellung nicht Null ist.
  • Die jeweiligen IO-Pins werden gelöscht, wenn der PWM-Zähler mit der PWM-Einstellung übereinstimmt

Ja klar, aber da wir nur acht Kanäle haben, gibt es maximal 8 Zeitpunkte, an denen ein Pin gelöscht werden muss. Wenn mehrere Kanäle die gleiche PWM-Einstellung haben sind es sogar noch weniger. Alle anderen Interrupts verursachen keinerlei Änderung der IO-Pins und verbrauchen eigentlich nur sinnlos Rechenzeit. Ein Skandal!

Was ist also zu tun? Wir wissen nun, daß es maximal 9 Ereignisse pro PWM-Zyklus gibt. Was ist damit zu machen?

  • Die PWM-Kanäle müssen in aufsteigender Folge sortiert werden.
  • Wenn mehrere PWM-Kanäle den gleichen PWM-Wert haben müssen sie zusammengefaßt werden
  • Die Zeitdifferenzen zwischen den einzelnen Ereignissen müssen berechnet werden

Das ist eigentlich schon alles. Praktisch ist das mit einigen Kniffligkeiten verbunden, aber die sind lösbar. Am Ende steht eine Interruptroutine, welche maximal 9 mal pro PWM-Zyklus aufgerufen wird und die jeweiligen IO-Pins setzt bzw. löscht. Eine normale Funktion wird benutzt, um die PWM-Einstellungen der acht Kanäle in die notwendigen Informationen für die Interruptroutine umzuwandeln. Das hat unter anderem den Vorteil, daß nur dann zusätzlich Rechenzeit benötigt wird, wenn sich die PWM-Einstellungen ändern.

/*
    Eine 8-kanalige PWM mit intelligentem Lösungsansatz
 
    ATmega32 @ 8 MHz
 
*/
 
// Defines an den Controller und die Anwendung anpassen
 
#define F_CPU         8000000L           // Systemtakt in Hz
#define F_PWM         100L               // PWM-Frequenz in Hz
#define PWM_PRESCALER 8                  // Vorteiler für den Timer
#define PWM_STEPS     256                // PWM-Schritte pro Zyklus(1..256)
#define PWM_PORT      PORTB              // Port für PWM
#define PWM_DDR       DDRB               // Datenrichtungsregister für PWM
#define PWM_CHANNELS  8                  // Anzahl der PWM-Kanäle
 
// ab hier nichts ändern, wird alles berechnet
 
#define T_PWM (F_CPU/(PWM_PRESCALER*F_PWM*PWM_STEPS)) // Systemtakte pro PWM-Takt
//#define T_PWM 1   //TEST
 
#if ((T_PWM*PWM_PRESCALER)<(111+5))
    #error T_PWM zu klein, F_CPU muss vergrösst werden oder F_PWM oder PWM_STEPS verkleinert werden
#endif
 
#if ((T_PWM*PWM_STEPS)>65535)
    #error Periodendauer der PWM zu gross! F_PWM oder PWM_PRESCALER erhöhen.   
#endif
// includes
 
#include <stdint.h>
#include <string.h>
#include <avr/io.h>
#include <avr/interrupt.h>
 
// globale Variablen
 
uint16_t pwm_timing[PWM_CHANNELS+1];          // Zeitdifferenzen der PWM Werte
uint16_t pwm_timing_tmp[PWM_CHANNELS+1];      
uint8_t  pwm_mask[PWM_CHANNELS+1];            // Bitmaske für PWM Bits, welche gelöscht werden sollen
uint8_t  pwm_mask_tmp[PWM_CHANNELS+1];        
uint8_t  pwm_setting[PWM_CHANNELS];           // Einstellungen für die einzelnen PWM-Kanäle
uint8_t  pwm_setting_tmp[PWM_CHANNELS+1];     // Einstellungen der PWM Werte, sortiert
 
volatile uint8_t pwm_cnt_max=1;               // Zählergrenze, Initialisierung mit 1 ist wichtig!
volatile uint8_t pwm_sync;                    // Update jetzt möglich
 
// Pointer für wechselseitigen Datenzugriff
 
uint16_t * isr_ptr_time  = pwm_timing;
uint16_t * main_ptr_time = pwm_timing_tmp;
uint8_t *  isr_ptr_mask  = pwm_mask;
uint8_t *  main_ptr_mask = pwm_mask_tmp;
 
// Zeiger austauschen
// das muss in einem Unterprogramm erfolgen,
// um eine Zwischenspeicherung durch den Compiler zu verhindern
 
void tausche_zeiger(void) {
    uint16_t * tmp_ptr16;
    uint8_t * tmp_ptr8;
 
    tmp_ptr16 = isr_ptr_time;
    isr_ptr_time = main_ptr_time;
    main_ptr_time = tmp_ptr16;
    tmp_ptr8 = isr_ptr_mask;
    isr_ptr_mask = main_ptr_mask;
    main_ptr_mask = tmp_ptr8;
}
 
// PWM Update, berechnet aus den PWM Einstellungen
// die neuen Werte für die Interruptroutine
 
void pwm_update(void) {
    uint8_t i, j, k, min;
    uint8_t tmp;
 
    // PWM Maske für Start berechnen
    // gleichzeitig die Bitmasken generieren und PWM Werte kopieren
 
    tmp=0;
    j = 1;
    for(i=1; i<=(PWM_CHANNELS); i++) {
        main_ptr_mask[i]=~j;                        // Maske zum Löschen der PWM Ausgänge
        pwm_setting_tmp[i] = pwm_setting[i-1];
        if (pwm_setting_tmp[i]!=0) tmp |= j;        // Maske zum setzen der IOs am PWM Start
        j <<= 1;
    }
    main_ptr_mask[0]=tmp;                           // PWM Start Daten 
 
    // PWM settings sortieren; Einfügesortieren
 
    for(i=1; i<=PWM_CHANNELS; i++) {
        min=255;
        k=i;
        for(j=i; j<=PWM_CHANNELS; j++) {
            if (pwm_setting_tmp[j]<min) {
                k=j;                                // Index und PWM-setting merken
                min = pwm_setting_tmp[j];
            }
        }
        if (k!=i) {
            // ermitteltes Minimum mit aktueller Sortiertstelle tauschen
            tmp = pwm_setting_tmp[k];
            pwm_setting_tmp[k] = pwm_setting_tmp[i];
            pwm_setting_tmp[i] = tmp;
            tmp = main_ptr_mask[k];
            main_ptr_mask[k] = main_ptr_mask[i];
            main_ptr_mask[i] = tmp;
        }
    }
 
    // Gleiche PWM-Werte vereinigen, ebenso den PWM-Wert 0 löschen falls vorhanden
 
    k=PWM_CHANNELS;             // PWM_CHANNELS Datensätze
    i=1;                        // Startindex
 
    while(k>i) {
        while ( ((pwm_setting_tmp[i]==pwm_setting_tmp[i+1]) || (pwm_setting_tmp[i]==0))  && (k>i) ) {
 
            // aufeinanderfolgende Werte sind gleich und können vereinigt werden
            // oder PWM Wert ist Null
            if (pwm_setting_tmp[i]!=0)
                main_ptr_mask[i+1] &= main_ptr_mask[i];        // Masken vereinigen
 
            // Datensatz entfernen,
            // Nachfolger alle eine Stufe hochschieben
            for(j=i; j<k; j++) {
                pwm_setting_tmp[j] = pwm_setting_tmp[j+1];
                main_ptr_mask[j] = main_ptr_mask[j+1];
            }
            k--;
        }
        i++;
    }
    
    // letzten Datensatz extra behandeln
    // Vergleich mit dem Nachfolger nicht möglich, nur löschen
    // gilt nur im Sonderfall, wenn alle Kanäle 0 sind
    if (pwm_setting_tmp[i]==0) k--;
 
    // Zeitdifferenzen berechnen
    
    if (k==0) { // Sonderfall, wenn alle Kanäle 0 sind
        main_ptr_time[0]=(uint16_t)T_PWM*PWM_STEPS/2;
        main_ptr_time[1]=(uint16_t)T_PWM*PWM_STEPS/2;
        k=1;
    }
    else {
        i=k;
        main_ptr_time[i]=(uint16_t)T_PWM*(PWM_STEPS-pwm_setting_tmp[i]);
        j=pwm_setting_tmp[i];
        i--;
        for (; i>0; i--) {
            main_ptr_time[i]=(uint16_t)T_PWM*(j-pwm_setting_tmp[i]);
            j=pwm_setting_tmp[i];
        }
        main_ptr_time[0]=(uint16_t)T_PWM*j;
    }
 
    // auf Sync warten
 
    pwm_sync=0;             // Sync wird im Interrupt gesetzt
    while(pwm_sync==0);
 
    // Zeiger tauschen
    cli();
    tausche_zeiger();
    pwm_cnt_max = k;
    sei();
}
 
// Timer 1 Output COMPARE A Interrupt
 
ISR(TIMER1_COMPA_vect) {
    static uint8_t pwm_cnt;
    uint8_t tmp;
 
    OCR1A += isr_ptr_time[pwm_cnt];
    tmp    = isr_ptr_mask[pwm_cnt];
    
    if (pwm_cnt == 0) {
        PWM_PORT = tmp;                         // Ports setzen zu Begin der PWM
        pwm_cnt++;
    }
    else {
        PWM_PORT &= tmp;                        // Ports löschen
        if (pwm_cnt == pwm_cnt_max) {
            pwm_sync = 1;                       // Update jetzt möglich
            pwm_cnt  = 0;
        }
        else pwm_cnt++;
    }
}
 
int main(void) {
 
    // PWM Port einstellen
    
    PWM_DDR = 0xFF;         // Port als Ausgang
    
    // Timer 1 OCRA1, als variablem Timer nutzen
 
    TCCR1B = 2;             // Timer läuft mit Prescaler 8
    TIMSK |= (1<<OCIE1A);   // Interrupt freischalten
 
    sei();                  // Interrupts gloabl einschalten
 
 
/******************************************************************/
// nur zum testen, in der Anwendung entfernen
/*
// Test values
volatile uint8_t tmp;
const uint8_t t1[8]={255, 40, 3, 17, 150, 99, 5, 9};
const uint8_t t2[8]={27, 40, 3, 0, 150, 99, 5, 9};
const uint8_t t3[8]={27, 40, 3, 17, 3, 99, 3, 0};
const uint8_t t4[8]={0, 0, 0, 0, 0, 0, 0, 0};
const uint8_t t5[8]={9, 1, 1, 1, 1, 1, 1, 1};
const uint8_t t6[8]={33, 33, 33, 33, 33, 33, 33, 33};
const uint8_t t7[8]={0, 0, 0, 0, 0, 0, 0, 88};
 
 
// Messung der Interruptdauer
    tmp =1;
    tmp =2;
    tmp =3;
 
// Debug 
 
    memcpy(pwm_setting, t1, 8);
    pwm_update();
 
    memcpy(pwm_setting, t2, 8);
    pwm_update();
 
    memcpy(pwm_setting, t3, 8);
    pwm_update();
 
    memcpy(pwm_setting, t4, 8);
    pwm_update();
 
    memcpy(pwm_setting, t5, 8);
    pwm_update();
    
    memcpy(pwm_setting, t6, 8);
    pwm_update();
    
    memcpy(pwm_setting, t7, 8);
    pwm_update();
*/
/******************************************************************/
 
    while(1);
    return 0;
}

Das Programm ist schon um einiges länger (968 Byte). Die Interruptroutine benötigt maximal 111 Takte und wird zwischen 2 bis 9 mal pro PWM-Zyklus aufgerufen. Zweimal, wenn alle PWM-Einstellungen gleich sind, 9 mal, wenn alle PWM-Einstellungen verschieden sind. Damit werden zwischen 222 bis 999 Takte benötigt, pro PWM-Zyklus, nicht pro PWM-Takt! Das entspricht einer CPU-Belastung von 0,3..1,2%! Standing Ovations! Die Funktion pwm_update() benötigt ca. 1500 bis 1800 Takte, das ist geringfügig abhängig von den PWM-Einstellungen, je nach dem ob die Daten schon sortiert sind und ob PWM-Werte mehrfach vorkommen. Bei einer Updaterate von 100 Hz (mehr ist physikalisch sinnlos) entspricht das einer CPU-Belastung von 2,3%, praktisch wird es wahrscheinlich weniger sein. Taktet man den AVR mit vollen 16 MHz halbiert sich die CPU-Belastung noch einmal. Beachtet werden sollte hier die Datenübergabe von der Funktion pwm_update() zur Interruptroutine. Hier werden jeweils zwei Zeiger verwendet, um auf Arrays zu zeigen. In zwei Arrays werden durch die Funktion die Berechnungen der neuen Daten vorgenommen. In den beiden anderen Arrays sthen die aktuellen Daten, mit welchen die ISR arbeitet. Um am Ende der Berechung ein relativ aufwändiges Kopieren der Daten zu vermeiden werden einfach die Zeiger vertauscht. Das ist wesentlich schneller als das Kopieren der Arrays! Im engischen spricht man hier von double buffering, also doppelter Pufferung. Dieses Prinzip wird oft angewendet. Würde man allerdings einfach am Ende die Zeiger tauschen käme es zu einem Crash! Der Interrupt kann jederzeit aktiv werden. Wenn dann die Zeiger nur halb kopiert sind greift die Interruptroutine auf zerstückelte Daten zu und macht Müll. Ebenso würde es zu Fehlfunktionen kommen, wenn während es PWM-Zyklus neue Daten in die Arrays kopiert werden. Das muß verhindert werden. Und zwar dadurch, daß über eine Variable eine Synchronisation durchgeführt wird. Diese wird am Ende des PWM-Zyklus gesetzt und signalisiert, daß neue Daten für den nächsten Zyklus kopiert werden können. Deshalb muss die Funktion pwm_update ggf. bis zu 1 vollen PWM-Zyklus warten, bis die Zeiger getauscht werden können. Wichtig ist dabei, daß die Variable pwm_sync, welche sowohl in der Funktion als auch im Interrupt geschrieben wird, als volatile deklariert wird. Denn sonst würde die Sequenz

pwm_sync=0;             // Sync wird im Interrupt gesetzt
while(pwm_sync==0);

zum Stehenbleiben der CPU führen, weil der Compiler erkennt, daß die Variable nie ungleich Null sein kann und damit die Schleife endlos ausgeführt wird. Der Compiler kann prinzipbedingt nicht automatisch erkennen, daß die Variable im Interrupt auf 1 gesetzt wird.

Bei dem schon recht hohen Prozessortakt von 8MHz un der relativ niedrigen PWM Frequenz von 100 Hz haben wir allerdings ein kleines Problem. Wenn beispielsweise nur ein Kanal den PWM-Wert 10 hat, alles anderen aber Null, dann passiert folgendes. Zum Begin der PWM wir der eine Kanal aktiviert. Jetzt wird per Timer für 10xT_PWM = 3120 Takte gewartet. Jetzt wird der eine Kanal gelöscht. Bis zum Begin des nächsten PWM-Zyklus muss jedoch noch (256-10)*T_PWM = 76752 Takte gewartet werden. Doch diese Zahl passt nicht mehr in eine 16 Bit Variable! Und damit kann sie auch nicht mit dem Timer verwendet werden. Der Ausweg heisst Vorteiler (engl. Prescaler). Damit kann man den Timer langsamer takten und somit die gleiche Wartezeit mit weniger Timertakten erzielen. Das wird hier gemacht. Zu beachten ist, dass die Einstellung im #define PWM_PRESCALER mit der realen Einstellung in TCCR1B übereinstimmen muss.

Eine Einschränkung gilt allerdings für alle Soft-PWMs. Die PWM-Frequenz muss niedrig genug sein, damit sich die Interrupts nicht überschneiden. D.h. der Wert T_PWM muß immer größer sein als die Anzahl Takte der Interruptroutine. Das wird im Quelltext mit Hilfe von #if . . #endif geprüft. Die +5 Takte sind eine Reserve. Dazu muß aber die Optimierung -Os eingeschaltet sein, sonst stimmen die Zahlen nicht!

[bearbeiten] Zusammenfassung

Durch kritische Analyse ist es möglich, eine Software-PWM drastisch zu verbessern und die CPU-Belastung auf ein verschwindend geringes Maß zu reduzieren.

Version Programmspeicher [Byte] CPU-Belastung [%]
1 284 49
2 324 30
3 968 0,3..1,2
webmaster@mikrocontroller.netImpressumWerbung auf Mikrocontroller.net