ATtiny13A - No se puede generar software PWM con modo CTC

8

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 PWM de software usando interrupciones en modo CTC.

No puedo operar en ningún otro modo (excepto para PWM rápido, OCR0Aya 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 yOCR0A=122 .

Así que estoy tratando (y he visto a gente mencionar esto en Internet) de usar Output Compare Ae Output Compare Binterrumpe para generar un software PWM.

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

Espero poder obtener un PWM con un ciclo de trabajo del 0-100% cambiando el OCR0Bvalor de 0a OCR0A. Esta es mi comprensión 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 está atascada 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
}
Pouria P
fuente
¿Puedo preguntar por qué no puede usar hardware PWM? La razón que estás dando 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
@Maple Estoy tratando de controlar un LED RGB, así que necesito 3 señales PWM, una para cada color. OCR0Aes usado por el código IR, así que solo tengo OCR0B. Estoy tratando de usarlo para generar software PWM en 3 pines que no son PWM.
Pouria P
El software PWM de 38kHz no funcionará. Eso es demasiado rápido para el MCU.
JimmyB
1
Puede (y lo ha hecho) ejecutar un ISR @ 38kHz. Pero para cualquier ciclo de trabajo que no sea el 50%, necesitará una frecuencia más alta. Ejemplo: para el 25% a 38 kHz, debe poder manejar dos interrupciones sucesivas dentro de un marco de tiempo de 38 kHz / 25% = 152 kHz. Eso deja solo unos 63 ciclos de reloj de la CPU (9600kHz / 152kHz) para el ISR. Con un ciclo de trabajo del 10%, le quedan 25 relojes de CPU para el ISR.
JimmyB
3
No especificó la frecuencia PWM deseada. Para el control del brillo no necesitará estar cerca de 38kHz. 100Hz pueden ser suficientes. Le sugiero que use la frecuencia de 38kHz (IR) como el ciclo de trabajo más bajo para su PWM de software 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 38kHz) y el el más alto (por debajo del 100%) es (255/256), igual a 255 períodos de reloj de 38 kHz. Esto le proporciona un PWM de 8 bits a (38000/256) ~ 148Hz.
JimmyB

Respuestas:

8

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 se establece dutyCycleen el valor deseado y el ISR emite la señal PWM correspondiente. dutyCyclees un uint16_tpara permitir valores entre 0 y 256 inclusive; 256 es mayor que cualquier valor posible decurrentPwmCount y, por lo tanto, proporciona un ciclo de trabajo completo del 100%.

Si no necesita 0% (o 100%), puede reducir algunos ciclos utilizando un uint8_tmodo que 0resulte en un ciclo de trabajo de 1/256 y 255sea ​​100% o 0sea ​​0% y255 sea ​​un ciclo de trabajo de 255 / 256

Todavía no tienes mucho tiempo en un ISR de 38kHz; Con un pequeño ensamblador en línea, probablemente pueda reducir el recuento de ciclos del ISR de 1/3 a 1/2. Alternativa: ejecute su código PWM solo cada dos segundos de desbordamiento 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 solo paso que solo necesita la lectura de puerto, y con máscara, o con nuevo estado, escribir en el puerto 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...).

JimmyB
fuente
Wow eres asombroso. Me preguntaba si mi comprensión de lo que me dijo que hiciera era correcta y ahora hay una respuesta altamente informativa con ejemplos y todo. Gracias de nuevo.
Pouria P
Solo una cosa más, entendí esto correctamente: si tuviera que hacer el PWM cada dos segundos de desbordamiento del temporizador, pondría una ifrutina de interrupción para ejecutar solo el código PWM en cualquier otro momento. 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?
Pouria P
Sí, esto es lo que quise decir, perdón por ser tan breve al respecto. El ISR debe ser lo suficientemente rápido como para no perder ninguna interrupción en primer lugar, pero incluso cuando lo sea, pasar el 90% del tiempo de CPU en un ISR tampoco puede ser bueno, por lo que podría reducirlo casi a la mitad omitiendo el ' lógica 'compleja' cada dos interrupciones dejando más tiempo para otras tareas.
JimmyB
2

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 superponen, 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 salga la primera.

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

Al observar 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 256 * 6.7 µs = 1715 µs y 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 el contexto se guarda y se recupera. 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 igual que al salir de la interrupción e ingresar a la siguiente interrupción, por lo que el ciclo de trabajo mínimo no será el esperado.

Mientras esto no sea menor que un paso PWM, el ciclo de trabajo PWM comenzará en un valor más alto. Solo una ligera mejora de lo que tienes ahora.

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

dorio
fuente