ATtiny13A - No se puede generar software PWM con modo CTC

Estoy tratando de hacer una luz LED RGB con control remoto usando un ATtiny13A.

Sé que el ATtiny85 es más adecuado para este propósito, y sé que eventualmente no podré ajustar todo el código, pero por ahora mi principal preocupación es generar un software PWM usando interrupciones en el modo CTC.

No puedo operar en ningún otro modo (excepto PWM rápido con OCR0Aas TOPque es básicamente lo mismo) porque el código del receptor IR que estoy usando necesita una frecuencia de 38 kHz que genera usando CTC y OCR0A=122.

Así que estoy tratando (y he visto a personas mencionar esto en Internet) usar las interrupciones Output Compare Apara Output Compare Bgenerar un PWM de software.

OCR0A, que también es utilizado por el código IR, determina la frecuencia, que no me importa. Y OCR0Bdetermina el ciclo de trabajo del PWM que usaré para cambiar los colores de los LED.

Espero poder obtener un PWM con un ciclo de trabajo de 0-100% cambiando el OCR0Bvalor de 0a OCR0A. Este es mi entendimiento de lo que debería suceder:

la forma de onda

Pero lo que realmente está sucediendo es esto (esto es de la simulación Proteus ISIS):

Como puede ver a continuación, puedo obtener un ciclo de trabajo del 25% al ​​75%, pero para ~0-25% y ~75-100%, la forma de onda simplemente se atasca y no cambia.

Línea AMARILLA: Hardware PWM

Línea ROJA: Software PWM con ciclo de trabajo fijo

Línea VERDE: software PWM con ciclo de trabajo variable

Resultados del osciloscopio

Y aquí está mi código:

#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
}
¿Puedo preguntar por qué no puede usar hardware PWM? La razón que das no tiene ningún sentido. La única razón para no usar hardware es si necesita una interfaz SPI o una interrupción externa.
@Maple Estoy tratando de controlar un LED RGB, así que necesito 3 señales PWM, una para cada color. OCR0Aes utilizado por el código IR, por lo que solo tengo OCR0B. Estoy tratando de usarlo para generar software PWM en 3 pines que no son PWM.
El software PWM de 38 kHz no funcionará. Eso es demasiado rápido para el MCU.
@JimmyB ¿Puedes explicar más? Funciona con un reloj de 9,6 MHz y la rutina de interrupción es muy corta. ¿Por qué 38kHz es demasiado rápido?
Puede (y lo ha hecho) ejecutar un ISR @ 38kHz. Pero para cualquier ciclo de trabajo que no sea del 50 %, necesitará una frecuencia más alta. Ejemplo: Para 25% a 38kHz, debe poder manejar dos interrupciones sucesivas dentro de un marco de tiempo de 38kHz/25%=152kHz. Eso deja solo alrededor de 63 ciclos de reloj de CPU (9600kHz/152kHz) para el ISR. Con un ciclo de trabajo del 10%, le quedan 25 relojes de CPU para el ISR.
No especificó la frecuencia PWM deseada. Para controlar el brillo, no necesitará estar cerca de los 38 kHz. 100Hz puede ser suficiente. Le sugiero que use la frecuencia de 38 kHz (IR) como el ciclo de trabajo más bajo para su software PWM e implemente el PWM como un múltiplo de eso, por ejemplo, 256, de modo que el ciclo de trabajo más bajo sea 1/256 (un período de reloj de 38 kHz) y el el más alto (por debajo del 100%) es (255/256), igual a 255 períodos de reloj de 38kHz. Esto le da un PWM de 8 bits a (38000/256)~148Hz.
@JimmyB ¡Gracias! Explica por qué la señal está bien cuando tiene un ciclo de trabajo de ~50%, pero a medida que se acerca a ambos extremos, comienza a actuar de manera extraña. Probaré su solución y veré si me da un PWM lo suficientemente bueno. Y sí, como dijiste, la frecuencia no me importa, siempre y cuando no haga que las luces se pongan nerviosas.
Por cierto, ¡buen trabajo al publicar esa pregunta, especialmente la imagen de alcance animada muestra perfectamente el efecto de que el ISR se vuelve demasiado lento en algún momento!
Re: "Es posible que eventualmente no pueda ajustar todo el código", AFAIK the delay.h vincula la biblioteca de punto flotante a la aplicación. No use _delay_ms en ATTiny.
@Maple No lo haré. Es solo para el propósito de prueba.

Respuestas (2)

Un PWM de software mínimo podría verse así:

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

Su programa establece dutyCycleel valor deseado y el ISR emite la señal PWM correspondiente. dutyCyclees a uint16_tpara permitir valores entre 0 y 256 inclusive; 256 es mayor que cualquier valor posible de currentPwmCounty, por lo tanto, proporciona un ciclo de trabajo completo del 100 %.

Si no necesita el 0 % (o el 100 %), puede reducir algunos ciclos usando un ciclo uint8_tde 0trabajo de 1/256 y 255100 % o 00 % y 255un ciclo de trabajo de 255/ 256.

Todavía no tienes mucho tiempo en un ISR de 38kHz; utilizando un pequeño ensamblador en línea, probablemente pueda reducir el recuento de ciclos del ISR entre 1/3 y 1/2. Alternativa: ejecute su código PWM solo cada dos desbordamientos del temporizador, reduciendo a la mitad la frecuencia PWM.

Si tiene varios canales PWM y los pines que está utilizando PMW están todos en el mismo PORT, también puede recopilar todos los estados de los pines en una variable y finalmente enviarlos al puerto en un paso que luego solo necesita la lectura de- port, and-with-mask, or-with-new-state, write-to-port una vez en lugar de una vez por pin/canal .

Ejemplo:

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

Este código asigna el ciclo de trabajo a una 1salida lógica en los pines; si sus LED tienen 'lógica negativa' (LED encendido cuando el pin está bajo ), puede invertir la polaridad de la señal PWM simplemente cambiando if (cnt < dutyCycle...)a if (cnt >= dutyCycle...).

Guau, eres increíble. Me preguntaba si mi comprensión de lo que me dijiste que hiciera era correcta y ahora hay esta respuesta altamente informativa con ejemplos y todo. Gracias de nuevo.
Solo una cosa más, entendí esto correctamente: si tuviera que hacer el PWM cada dos desbordamientos del temporizador, pondría un ifen la rutina de interrupción para ejecutar solo el código PWM cada dos veces. Al hacer esto, si mi código PWM tarda demasiado y se pierde la próxima interrupción de desbordamiento, entonces mi programa estará bien porque la próxima interrupción no iba a hacer nada de todos modos. ¿Es eso lo que querías decir?
Sí, esto es lo que quise decir, perdón por ser tan breve al respecto. El ISR debería ser lo suficientemente rápido para no perder ninguna interrupción en primer lugar, pero incluso cuando lo es, gastar el 90 % del tiempo de la CPU en un ISR tampoco puede ser bueno, por lo que podría reducirlo casi a la mitad saltándose el ' lógica compleja cada interrupción dejando más tiempo para otras tareas.

Como comentó @JimmyB, la frecuencia PWM es demasiado alta.

Parece que las interrupciones tienen una latencia total de una cuarta parte del ciclo PWM.

Cuando se superpone, el ciclo de trabajo se fija dado por la latencia total, ya que la segunda interrupción se pone en cola y se ejecuta después de que se sale de la primera.

El ciclo de trabajo mínimo de PWM viene dado por el porcentaje total de latencia de interrupción en el período de PWM. La misma lógica se aplica al ciclo de trabajo máximo de PWM.

Mirando los gráficos, el ciclo de trabajo mínimo es de alrededor del 25%, y luego la latencia total debe ser ~ 1/(38000*4) = 6,7 µs.

Como consecuencia, el período mínimo de PWM es de 256*6,7 µs = 1715 µs y una frecuencia máxima de 583 Hz.

Algunas explicaciones más sobre posibles parches a alta frecuencia:

La interrupción tiene dos ventanas ciegas cuando no se puede hacer nada, entrando y saliendo de la interrupción cuando se guarda y recupera el contexto. Dado que su código es bastante simple, sospecho que esto toma una buena parte de la latencia.

Una solución para omitir los valores bajos seguirá teniendo una latencia al menos al salir de la interrupción y entrar en la siguiente interrupción, por lo que el ciclo de trabajo mínimo no será el esperado.

Siempre que no sea inferior a un paso de PWM, el ciclo de trabajo de PWM comenzará con un valor más alto. Sólo una ligera mejora de lo que tienes ahora.

Veo que ya usa el 25 % del tiempo del procesador en interrupciones, entonces, ¿por qué no usa el 50 % o más, deja la segunda interrupción y solo agrupa para el indicador de comparación? Si usa valores solo hasta 128, tendrá solo hasta un 50% de ciclo de trabajo, pero con la latencia de dos instrucciones que podrían optimizarse en ensamblador.