ATtiny13A - Impossibile generare software PWM con modalità CTC


8

Sto cercando di creare una luce LED RGB con telecomando usando un ATtiny13A.

So che ATtiny85 è più adatto a questo scopo e so che alla fine potrei non essere in grado di adattare l'intero codice, ma per ora la mia preoccupazione principale è generare un PWM software usando gli interrupt in modalità CTC.

Non posso operare in qualsiasi altra modalità (ad eccezione di PWM veloce con OCR0Aquanto TOPche è fondamentalmente la stessa cosa) perché il codice ricevitore IR sto usando ha bisogno di una frequenza di 38 kHz, che genera con CTC e OCR0A=122.

Quindi sto cercando di (e ho visto persone menzionarlo su Internet) usare gli interrupt Output Compare Ae Output Compare Bper generare un software PWM.

OCR0A, che viene utilizzato anche dal codice IR, determina la frequenza, che non mi interessa. E OCR0Bdetermina il duty cycle del PWM che userò per cambiare i colori dei LED.

Mi aspetto di poter ottenere un PWM con duty cycle 0-100% modificando il OCR0Bvalore da 0a OCR0A. Questa è la mia comprensione di ciò che dovrebbe accadere:

La forma d'onda

Ma ciò che sta realmente accadendo è questo (questo è dalla simulazione ISIS di Proteus):

Come puoi vedere di seguito, sono in grado di ottenere circa il 25% -75% di duty cycle ma per ~ 0-25% e ~ 75-100% la forma d'onda è bloccata e non cambia.

Linea GIALLA: Hardware PWM

Linea ROSSA: software PWM con ciclo di lavoro fisso

Linea VERDE: software PWM con ciclo di lavoro variabile

Risultati dell'oscilloscopio

Ed ecco il mio codice:

#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
}

Posso chiederti perché non è possibile utilizzare PWM hardware? Il motivo che stai dando non ha alcun senso. L'unico motivo per non usare l'hardware è se hai bisogno di un'interfaccia SPI o di un interrupt esterno.
Maple,

@Maple Sto cercando di controllare un LED RGB, quindi ho bisogno di 3 segnali PWM, uno per ogni colore. OCR0Aviene utilizzato dal codice IR, quindi ho solo OCR0B. Sto cercando di usarlo per generare software PWM su 3 pin non PWM.
Pouria P,

Il software PWM a 38 kHz non funzionerà. È troppo veloce per l'MCU.
JimmyB,

1
Puoi (e averlo fatto) eseguire un ISR a 38kHz. Ma per qualsiasi ciclo di lavoro diverso dal 50% è necessaria una frequenza più elevata. Esempio: per il 25% a 38 kHz è necessario essere in grado di gestire due interruzioni successive in un intervallo di tempo di 38 kHz / 25% = 152 kHz. Ciò lascia solo circa 63 cicli di clock della CPU (9600kHz / 152kHz) per l'ISR. Al duty cycle del 10% sono rimasti solo 25 clock della CPU per l'ISR.
JimmyB,

3
Non è stata specificata la frequenza PWM desiderata. Per il controllo della luminosità non dovrai essere vicino a 38 kHz. 100Hz possono essere sufficienti. Ti suggerisco di utilizzare la frequenza 38kHz (IR) come ciclo di lavoro più basso per il tuo software PWM e implementare PWM come un multiplo di quello, ad esempio 256, in modo che il ciclo di lavoro più basso sia 1/256 (un periodo di clock di 38kHz) e il il più alto (inferiore al 100%) è (255/256), pari a 255 periodi di clock a 38 kHz. Questo ti dà un PWM a 8 bit a (38000/256) ~ 148Hz.
JimmyB,

Risposte:


8

Un software PWM minimo potrebbe apparire così:

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
  }
}

Il programma imposta dutyCycleil valore desiderato e l'ISR emette il segnale PWM corrispondente. dutyCycleè un uint16_tper consentire valori compresi tra 0 e 256 inclusi; 256 è più grande di qualsiasi valore possibile currentPwmCounte fornisce quindi un ciclo di lavoro completo al 100%.

Se non è necessario lo 0% (o il 100%), è possibile eseguire la rasatura di alcuni cicli utilizzando a in uint8_tmodo che il 0risultato sia un ciclo di lavoro di 1/256 ed 255è 100% o 0è 0% ed 255è un ciclo di lavoro di 255 / 256.

Non hai ancora molto tempo in un ISR a 38 kHz; usando un piccolo assemblatore in linea probabilmente si può tagliare il conteggio dei cicli dell'ISR da 1/3 a 1/2. Alternativa: esegui il tuo codice PWM solo ogni altro overflow del timer, dimezzando la frequenza PWM.

Se disponi di più canali PWM e i pin che stai utilizzando PMW sono tutti uguali PORT, puoi anche raccogliere tutti gli stati dei pin in una variabile e infine inviarli alla porta in un solo passaggio che richiede solo la lettura da porta, e-con-maschera, o-con-nuovo-stato, scrittura su porta una volta anziché una volta per pin / canale .

Esempio:

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;
}

Questo codice mappa il duty cycle su 1un'uscita logica sui pin; se i tuoi LED hanno "logica negativa" (LED acceso quando il pin è basso ), puoi invertire la polarità del segnale PWM semplicemente cambiando if (cnt < dutyCycle...)in if (cnt >= dutyCycle...).


Wow, sei fantastico. Mi chiedevo se la mia comprensione di ciò che mi hai detto di fare fosse corretta e ora c'è questa risposta altamente istruttiva con esempi e tutto. Grazie ancora.
Pouria P,

Solo un'altra cosa, l'ho capito bene: se dovessi fare il PWM ogni altro overflow del timer, inserirei una ifroutine di interrupt per eseguire il codice PWM solo ogni volta. In questo modo se il mio codice PWM impiega troppo tempo e manca il prossimo interrupt di overflow, allora il mio programma andrà bene perché il prossimo interrupt non avrebbe comunque fatto nulla. È quello che volevi dire?
Pouria P,

Sì, questo è ciò che intendevo, mi dispiace di essere così breve. L'ISR dovrebbe essere abbastanza veloce da non perdere alcun interruzione in primo luogo, ma anche quando lo è, passare il 90% del tempo della CPU in un ISR potrebbe non essere neanche buono, quindi potresti tagliarlo quasi della metà saltando il ' logica complessa ogni altro interrupt lasciando più tempo per altre attività.
JimmyB,

2

Come ha commentato @JimmyB, la frequenza PWM è troppo alta.

Sembra che gli interrupt abbiano una latenza totale di un quarto del ciclo PWM.

In caso di sovrapposizione, il ciclo di lavoro viene fissato in base alla latenza totale, poiché il secondo interrupt viene messo in coda ed eseguito dopo l'uscita dal primo.

Il ciclo di lavoro PWM minimo è dato dalla percentuale di latenza di interruzione totale nel periodo PWM. La stessa logica si applica al ciclo di lavoro PWM massimo.

Guardando i grafici, il ciclo di lavoro minimo è di circa il 25%, quindi la latenza totale deve essere ~ 1 / (38000 * 4) = 6,7 µs.

Di conseguenza il periodo PWM minimo è 256 * 6,7 µs = 1715 µs e frequenza massima di 583 Hz.

Altre spiegazioni sulle possibili patch ad alta frequenza:

L'interrupt ha due finestre cieche quando non è possibile eseguire alcuna operazione, entrando in end uscendo dall'interrupt quando il contesto viene salvato e ripristinato. Dal momento che il tuo codice è piuttosto semplice, sospetto che ciò richieda una buona parte della latenza.

Una soluzione per saltare i valori bassi avrà comunque una latenza almeno quanto uscire dall'interrupt e immettere l'interrupt successivo in modo che il ciclo di lavoro minimo non sia come previsto.

Finché questo non è inferiore a un passaggio PWM, il ciclo di lavoro PWM inizierà a un valore più elevato. Solo un leggero miglioramento rispetto a quello che hai adesso.

Vedo che usi già il 25% del tempo del processore negli interrupt, quindi perché non usi il 50% o più di esso, lascia il secondo interrupt e usa solo il pool per il flag di confronto. Se si utilizzano solo valori fino a 128, si avrà solo un ciclo di lavoro fino al 50%, ma con la latenza di due istruzioni che potrebbero essere ottimizzate nell'assemblatore.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.