ATtiny13A - Kann keine Software-PWM mit CTC-Modus erzeugen

Ich versuche, mit einem ATtiny13A ein ferngesteuertes RGB-LED-Licht herzustellen.

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

Ich kann in keinem anderen Modus arbeiten (außer Fast PWM mit OCR0Aas TOP, was im Grunde dasselbe ist), da der von mir verwendete IR-Empfängercode eine 38-kHz-Frequenz benötigt, die er mit CTC und generiert OCR0A=122.

Also versuche ich (und ich habe Leute gesehen, die dies im Internet erwähnt haben), die Interrupts Output Compare Aund zu verwenden Output Compare B, um eine Software-PWM zu erzeugen.

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

Ich erwarte, dass ich eine PWM mit einem Arbeitszyklus von 0-100% erhalten kann, indem ich den OCR0BWert von 0auf ändere OCR0A. Dies ist mein Verständnis dessen, was passieren sollte:

Die Wellenform

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

Wie Sie unten sehen können, kann ich einen Arbeitszyklus von etwa 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 Linie: Software-PWM mit festem Tastverhältnis

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
}
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, ist, wenn Sie eine SPI-Schnittstelle oder einen externen Interrupt benötigen.
@Maple Ich versuche, eine RGB-LED zu steuern, also brauche 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 erzeugen.
38-kHz-Software-PWM funktioniert nicht. Das ist zu schnell für die MCU.
@JimmyB Kannst du das näher erklären? Es läuft auf 9,6 MHz Takt und die Interrupt-Routine ist sehr kurz. Warum sind 38kHz zu schnell?
Sie können (und haben dies auch getan) eine ISR bei 38 kHz ausführen. Aber für jeden anderen Arbeitszyklus als 50 % benötigen Sie eine höhere Frequenz. Beispiel: Für 25 % @ 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 (9600kHz/152kHz) für die ISR übrig. Bei 10 % Einschaltdauer bleiben 25 CPU-Takte für die ISR übrig.
Sie haben die gewünschte PWM-Frequenz nicht angegeben. Für die Helligkeitssteuerung müssen Sie nicht annähernd 38 kHz erreichen. 100 Hz können ausreichend sein. Ich schlage vor, Sie verwenden die 38-kHz-Frequenz (IR) als niedrigstes Tastverhältnis für Ihre Software-PWM und implementieren die PWM als ein Vielfaches davon, z. B. 256, sodass das niedrigste Tastverhältnis 1/256 (eine 38-kHz-Taktperiode) und die am höchsten (unter 100 %) ist (255/256), gleich 255 38-kHz-Taktperioden. Dies gibt Ihnen eine 8-Bit-PWM bei (38000/256) ~ 148 Hz.
@JimmyB Danke! Es erklärt, warum das Signal in Ordnung ist, wenn es einen Arbeitszyklus von ~50% hat, aber wenn es sich beiden Extremen nähert, beginnt es sich seltsam zu verhalten. Ich werde Ihre Lösung ausprobieren und sehen, ob sie mir eine ausreichend gute PWM liefert. Und ja, wie Sie sagten, die Frequenz spielt für mich keine Rolle, solange sie die Lichter nicht nervös macht.
Übrigens, gute Arbeit beim Posten dieser Frage, insbesondere das animierte Oszilloskopbild zeigt perfekt den Effekt, wenn die ISR irgendwann zu langsam wird!
Betreff: "Ich kann möglicherweise nicht den gesamten Code anpassen", AFAIK, die delay.h-Verknüpfung der Gleitkommabibliothek mit der Anwendung. Verwenden Sie _delay_ms nicht in ATTiny.
@ Maple werde ich nicht. Es dient nur zu Testzwecken.

Antworten (2)

Eine minimale Software-PWM könnte so 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 dutyCycleauf den gewünschten Wert ein und der ISR gibt das entsprechende PWM-Signal aus. dutyCycleist a uint16_t, um Werte zwischen 0 und 256 einschließlich zuzulassen; 256 ist größer als jeder mögliche Wert von currentPwmCountund bietet somit volle 100 % Einschaltdauer.

Wenn Sie 0 % (oder 100 %) nicht benötigen, können Sie einige Zyklen mit a kürzen uint8_t, sodass entweder 0ein Arbeitszyklus von 1/256 und 255100 % oder 00 % und 255ein Arbeitszyklus von 255/ 256.

Sie haben immer noch nicht viel Zeit in einer 38-kHz-ISR; Mit einem kleinen Inline-Assembler können Sie die Zykluszahl des ISR wahrscheinlich 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-ing sind, alle auf dem gleichen PORTsind, können Sie auch die Zustände aller Pins in einer Variablen sammeln und sie schließlich in einem Schritt an den Port ausgeben, der dann nur das Auslesen benötigt. port, and-with-mask, or-with-new-state, einmal auf 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 das Tastverhältnis einem logischen 1Ausgang an den Pins zu; Wenn Ihre LEDs eine "negative Logik" haben (LED an, wenn der Pin niedrig ist ), können Sie die Polarität des PWM-Signals umkehren, indem Sie einfach if (cnt < dutyCycle...)auf wechseln if (cnt >= dutyCycle...).

Wow, du bist großartig. Ich habe mich gefragt, ob ich richtig verstanden habe, was Sie mir gesagt haben, und jetzt gibt es diese äußerst informative Antwort mit Beispielen und allem. Danke noch einmal.
Nur noch eine Sache, habe ich das richtig verstanden: Wenn ich den PWM bei jedem zweiten Timer-Überlauf ausfü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 verpasst wird, ist mein Programm in Ordnung, da der nächste Interrupt sowieso nichts bewirken würde. Ist es das, was du meintest?
Ja, das meinte ich, sorry, dass ich mich so kurz fasse. Die 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 einer ISR zu verbringen. Sie könnten dies also fast halbieren, indem Sie die ' komplexe' Logik jede zweite Unterbrechung, so dass mehr Zeit für andere Aufgaben bleibt.

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 ist das Tastverhältnis fest vorgegeben durch die Gesamtlatenzzeit, da der zweite Interrupt in die Warteschlange gestellt und ausgeführt wird, nachdem der erste verlassen wurde.

Das minimale PWM-Tastverhältnis ist durch den Prozentsatz der gesamten Interrupt-Latenz in der PWM-Periode gegeben. Die gleiche Logik gilt für das maximale PWM-Tastverhältnis.

Wenn man sich die Diagramme ansieht, liegt das minimale Tastverhältnis bei etwa 25 %, und dann muss die Gesamtlatenz ~ 1/(38000*4) = 6,7 µs betragen.

Folglich beträgt die minimale PWM-Periode 256*6,7 µs = 1715 µs und 583 Hz maximale Frequenz.

Einige weitere Erläuterungen zu möglichen Patches mit hoher Frequenz:

Der Interrupt hat zwei blinde Fenster, wenn nichts getan werden kann, und tritt in end ein und verlässt den Interrupt, wenn der Kontext gespeichert und wiederhergestellt wird. Da Ihr Code ziemlich einfach ist, vermute ich, dass dies einen guten Teil der Latenzzeit in Anspruch nimmt.

Eine Lösung zum Überspringen der niedrigen Werte wird immer noch eine Latenz haben, die mindestens so groß ist wie das Verlassen des Interrupts und das Eintreten in den nächsten Interrupt, sodass 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, also warum verwenden Sie nicht 50 % oder mehr davon, lassen den zweiten Interrupt und sammeln nur für das Vergleichs-Flag. Wenn Sie nur Werte bis 128 verwenden, haben Sie nur bis zu 50 % Arbeitszyklus, aber mit der Latenz von zwei Befehlen, die in Assembler optimiert werden könnten.