ATtiny13A - Software-PWM kann im CTC-Modus nicht generiert werden

8

Ich versuche, mit einem ATtiny13A eine ferngesteuerte RGB-LED zu erstellen.

Ich weiß, dass der ATtiny85 für diesen Zweck besser geeignet ist, und ich weiß, dass ich möglicherweise nicht in der Lage bin, den gesamten Code anzupassen, aber im Moment ist es mein Hauptanliegen, eine Software-PWM mithilfe von Interrupts im CTC-Modus zu generieren.

Ich kann nicht in einem anderen Modus betrieben werden (außer für schnellen PWM mit OCR0Awie TOPdas ist im Grunde dasselbe ist) , weil der IR - Empfänger Code verwende ich eine 38 kHz Frequenz benötigt , die es mit Hilfe erzeugt CTC und OCR0A=122.

Also versuche ich (und ich habe gesehen, dass Leute dies im Internet erwähnen) die Output Compare Aund Output Compare BInterrupts zu verwenden, um eine Software-PWM zu generieren.

OCR0A, der auch vom IR-Code verwendet wird, bestimmt die Frequenz, die mir egal ist. Und OCR0Bbestimmt den Arbeitszyklus der PWM, die ich zum Ändern der LED-Farben verwenden werde.

Ich erwarte, dass ich eine PWM mit einem Tastverhältnis von 0-100% erhalten kann, indem ich den OCR0BWert von 0auf ändere OCR0A. So verstehe ich, was passieren soll:

Die Wellenform

Aber was tatsächlich passiert, ist Folgendes (dies stammt aus der Proteus ISIS-Simulation):

Wie Sie unten sehen können, kann ich einen Arbeitszyklus von 25% bis 75% erreichen, aber für ~ 0-25% und ~ 75-100% bleibt die Wellenform einfach hängen und ändert sich nicht.

GELBE Linie: Hardware PWM

ROTE Leitung: Software-PWM mit festem Arbeitszyklus

GRÜNE Linie: Software-PWM mit variierendem Arbeitszyklus

Oszilloskop Ergebnisse

Und hier ist mein Code:

#ifndef        F_CPU
    #define        F_CPU        (9600000UL) // 9.6 MHz
#endif

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

int main(void)
{
    cli();

    TCCR0A = 0x00;                        // Init to zero
    TCCR0B = 0x00;

    TCCR0A |= (1<<WGM01);                 // CTC mode
    TCCR0A |= (1<<COM0A0);                // Toggle OC0A on compare match (50% PWM on PINB0)
                                          // => YELLOW line on oscilloscope

    TIMSK0 |= (1<<OCIE0A) | (1<<OCIE0B);  // Compare match A and compare match B interrupt enabled

    TCCR0B |= (1<<CS00);                  // Prescalar 1

    sei();

    DDRB = 0xFF;                          // All ports output


    while (1)
    {
        OCR0A = 122;                      // This is the value I'll be using in my main program
        for(int i=0; i<OCR0A; i++)
        {
            OCR0B = i;                    // Should change the duty cycle
            _delay_ms(2);
        }
    }
}


ISR(TIM0_COMPA_vect){
    PORTB ^= (1<<PINB3);                  // Toggle PINB3 on compare match (50% <SOFTWARE> PWM on PINB3)
                                          // =>RED line on oscilloscope
    PORTB &= ~(1<<PINB4);                 // PINB4 LOW
                                          // =>GREEN line on oscilloscope
}

ISR(TIM0_COMPB_vect){
    PORTB |= (1<<PINB4);                  // PINB4 HIGH
}
Pouria P.
quelle
Darf ich fragen, warum Sie Hardware-PWM nicht verwenden können? Der Grund, den Sie angeben, ergibt keinen Sinn. Der einzige Grund, keine Hardware zu verwenden, besteht darin, dass Sie eine SPI-Schnittstelle oder einen externen Interrupt benötigen.
Maple
@Maple Ich versuche, eine RGB-LED zu steuern, daher benötige ich 3 PWM-Signale, eines für jede Farbe. OCR0Awird vom IR-Code verwendet, also habe ich nur OCR0B. Ich versuche, damit Software-PWM auf 3 Nicht-PWM-Pins zu generieren.
Pouria P
38kHz Software PWM funktioniert nicht. Das ist zu schnell für die MCU.
JimmyB
1
Sie können (und haben dies getan) einen ISR bei 38 kHz ausführen. Für einen anderen Arbeitszyklus als 50% benötigen Sie jedoch eine höhere Frequenz. Beispiel: Für 25% bei 38 kHz müssen Sie in der Lage sein, zwei aufeinanderfolgende Interrupts innerhalb eines Zeitrahmens von 38 kHz / 25% = 152 kHz zu verarbeiten. Damit bleiben nur etwa 63 CPU-Taktzyklen (9600 kHz / 152 kHz) für den ISR. Bei einem Arbeitszyklus von 10% haben Sie noch 25 CPU-Takte für den ISR.
JimmyB
3
Sie haben die gewünschte PWM-Frequenz nicht angegeben. Für die Helligkeitsregelung müssen Sie nicht in der Nähe von 38 kHz sein. 100 Hz können ausreichend sein. Ich schlage vor, dass Sie die Frequenz von 38 kHz (IR) als niedrigstes Tastverhältnis für Ihre Software-PWM verwenden und die PWM als ein Vielfaches davon implementieren, z. B. 256, sodass das niedrigste Tastverhältnis 1/256 (eine 38-kHz-Taktperiode) beträgt Der höchste Wert (unter 100%) ist (255/256), was 255 38-kHz-Taktperioden entspricht. Dies gibt Ihnen eine 8-Bit-PWM bei (38000/256) ~ 148 Hz.
JimmyB

Antworten:

8

Eine minimale Software-PWM könnte folgendermaßen aussehen:

volatile uint16_t dutyCycle;


uint8_t currentPwmCount;

ISR(TIM0_COMPA_vect){
  const uint8_t cnt = currentPwmCount + 1; // will overflow from 255 to 0
  currentPwmCount = cnt;
  if ( cnt <= dutyCyle ) {
    // Output 0 to pin
  } else {
    // Output 1 to pin
  }
}

Ihr Programm stellt dutyCycleden gewünschten Wert ein und der ISR gibt das entsprechende PWM-Signal aus. dutyCycleist a uint16_t, um Werte zwischen 0 und einschließlich 256 zu berücksichtigen; 256 ist größer als jeder mögliche Wert von currentPwmCountund bietet somit einen vollen Arbeitszyklus von 100%.

Wenn Sie nicht 0% (oder 100%) benötigen, können Sie einige Zyklen mit a rasieren, uint8_tsodass entweder 0ein Arbeitszyklus von 1/256 entsteht und 255100% oder 00% und 255ein Arbeitszyklus von 255 / beträgt. 256.

In einem 38-kHz-ISR haben Sie immer noch nicht viel Zeit. Mit einem kleinen Inline-Assembler können Sie wahrscheinlich die Zykluszahl des ISR um 1/3 bis 1/2 reduzieren. Alternative: Führen Sie Ihren PWM-Code nur bei jedem zweiten Timer-Überlauf aus und halbieren Sie die PWM-Frequenz.

Wenn Sie mehrere PWM-Kanäle haben und die Pins, die Sie PMW-fähig sind, alle gleich sind PORT, können Sie auch alle Pins-Zustände in einer Variablen sammeln und sie schließlich in einem Schritt an den Port ausgeben, der dann nur das Auslesen benötigt. Port und mit Maske oder mit neuem Status einmal in Port schreiben statt einmal pro Pin / Kanal .

Beispiel:

volatile uint8_t dutyCycleRed;
volatile uint8_t dutyCycleGreen;
volatile uint8_t dutyCycleBlue;

#define PIN_RED (0) // Example: Red on Pin 0
#define PIN_GREEN (4) // Green on pin 4
#define PIN_BLUE (7) // Blue on pin 7

#define BIT_RED (1<<PIN_RED)
#define BIT_GREEN (1<<PIN_GREEN)
#define BIT_BLUE (1<<PIN_BLUE)

#define RGB_PORT_MASK ((uint8_t)(~(BIT_RED | BIT_GREEN | BIT_BLUE)))

uint8_t currentPwmCount;

ISR(TIM0_COMPA_vect){
  uint8_t cnt = currentPwmCount + 1;
  if ( cnt > 254 ) {
    /* Let the counter overflow from 254 -> 0, so that 255 is never reached
       -> duty cycle 255 = 100% */
    cnt = 0;
  }
  currentPwmCount = cnt;
  uint8_t output = 0;
  if ( cnt < dutyCycleRed ) {
    output |= BIT_RED;
  }
  if ( cnt < dutyCycleGreen ) {
    output |= BIT_GREEN;
  }
  if ( cnt < dutyCycleBlue ) {
    output |= BIT_BLUE;
  }

  PORTx = (PORTx & RGB_PORT_MASK) | output;
}

Dieser Code ordnet den Arbeitszyklus einem logischen 1Ausgang an den Pins zu. wenn Ihre LEDs ‚negative Logik‘ haben (LED leuchtet , wenn der Stift ist niedrig ), können Sie die Polarität des PWM - Signals invertieren , indem einfach if (cnt < dutyCycle...)auf if (cnt >= dutyCycle...).

JimmyB
quelle
Wow du bist großartig. Ich habe mich gefragt, ob mein Verständnis von dem, was Sie mir gesagt haben, richtig ist, und jetzt gibt es diese äußerst informative Antwort mit Beispielen und allem. Danke noch einmal.
Pouria P
Nur noch eine Sache, habe ich das richtig verstanden: Wenn ich die PWM bei jedem zweiten Timer-Überlauf durchführen würde, würde ich eine ifin die Interrupt-Routine einfügen, um nur den PWM-Code jedes zweite Mal auszuführen. Wenn mein PWM-Code zu lange dauert und der nächste Überlauf-Interrupt übersehen wird, ist mein Programm in Ordnung, da der nächste Interrupt sowieso nichts bewirken würde. Ist es das, was du meintest?
Pouria P
Ja, das habe ich gemeint, tut mir leid, dass ich so kurz darüber bin. Der ISR sollte schnell genug sein, um überhaupt keinen Interrupt zu verpassen, aber selbst wenn dies der Fall ist, ist es möglicherweise auch nicht gut, 90% der CPU-Zeit in einem ISR zu verbringen. Sie können dies also fast halbieren, indem Sie das '. komplexe Logik jede zweite Unterbrechung lässt mehr Zeit für andere Aufgaben.
JimmyB
2

Wie @JimmyB kommentierte, ist die PWM-Frequenz zu hoch.

Es scheint, dass die Interrupts eine Gesamtlatenz von einem Viertel des PWM-Zyklus haben.

Bei Überlappung wird das Tastverhältnis durch die Gesamtlatenz festgelegt, da der zweite Interrupt nach dem Verlassen des ersten in die Warteschlange gestellt und ausgeführt wird.

Das minimale PWM-Tastverhältnis ergibt sich aus dem Prozentsatz der gesamten Interrupt-Latenz in der PWM-Periode. Die gleiche Logik gilt für das maximale PWM-Tastverhältnis.

In den Diagrammen liegt der minimale Arbeitszyklus bei etwa 25%, und dann muss die Gesamtlatenz ~ 1 / (38000 * 4) = 6,7 µs betragen.

Infolgedessen beträgt die minimale PWM-Periode 256 · 6,7 us = 1715 us und eine maximale Frequenz von 583 Hz.

Weitere Erklärungen zu möglichen Patches mit hoher Frequenz:

Der Interrupt verfügt über zwei blinde Fenster, in denen nichts getan werden kann. Wenn der Kontext gespeichert und wiederhergestellt wird, wird der Interrupt beendet. Da Ihr Code ziemlich einfach ist, vermute ich, dass dies einen guten Teil der Latenz beansprucht.

Eine Lösung zum Überspringen der niedrigen Werte hat immer noch eine Latenz, mindestens so wie das Verlassen des Interrupts und das Eingeben des nächsten Interrupts, so dass das minimale Tastverhältnis nicht wie erwartet ist.

Solange dies nicht weniger als ein PWM-Schritt ist, beginnt das PWM-Tastverhältnis bei einem höheren Wert. Nur eine leichte Verbesserung gegenüber dem, was Sie jetzt haben.

Ich sehe, dass Sie bereits 25% der Prozessorzeit in Interrupts verwenden. Warum verwenden Sie nicht 50% oder mehr davon, lassen den zweiten Interrupt und bündeln nur für das Vergleichsflag. Wenn Sie nur Werte bis zu 128 verwenden, haben Sie nur einen Arbeitszyklus von bis zu 50%, jedoch mit der Latenz von zwei Anweisungen, die im Assembler optimiert werden könnten.

Dorian
quelle