8 votos

ATtiny13A - No se puede generar PWM por software con el modo CTC

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

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

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

Así que estoy intentando (y he visto que la gente lo menciona en Internet) utilizar el Output Compare A y Output Compare B interrupciones para generar un PWM por software.

OCR0A que también utiliza el código IR, determina la frecuencia, que no me interesa. Y OCR0B determina el ciclo de trabajo del PWM que utilizaré para cambiar los colores del LED.

Espero poder conseguir un PWM con un ciclo de trabajo de 0-100% cambiando el OCR0B valor de 0 a OCR0A . Esto es lo que yo entiendo que debería ocurrir:

The wave form

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

Como se puede ver a continuación, soy capaz de obtener alrededor de 25%-75% de ciclo de trabajo, pero para ~0-25% y ~75-100% la forma de la onda es simplemente atascado 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

Oscilloscope results

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
}

0 votos

¿Puedo preguntar por qué no se puede utilizar el PWM por hardware? La razón que das no tiene ningún sentido. La única razón para no usar hardware es si necesitas una interfaz SPI o una interrupción externa.

0 votos

@Maple Estoy tratando de controlar un LED RGB por lo que necesito 3 señales PWM, una para cada color. OCR0A es utilizado por el código IR así que sólo tengo OCR0B . Estoy tratando de usarlo para generar PWM por software en 3 pines no PWM.

0 votos

El software PWM de 38kHz no funcionará. Es demasiado rápido para la MCU.

8voto

pgs Puntos 2491

Un PWM mínimo por software podría tener este aspecto:

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

Si no necesita el 0% (o el 100%) puede reducir algunos ciclos utilizando un uint8_t para que 0 resulta en un ciclo de trabajo de 1/256 y 255 es del 100% o 0 es el 0% y 255 es un ciclo de trabajo de 255/256.

Todavía no tienes mucho tiempo en un ISR de 38kHz; usando un poco de ensamblador en línea probablemente puedas reducir el conteo de ciclos del ISR en 1/3 a 1/2. Alternativa: Ejecuta tu código PWM sólo cada dos desbordamientos del temporizador, reduciendo a la mitad la frecuencia PWM.

Si tienes varios canales PWM y los pines que estás PMW-ing están todos en el mismo PORT también puede recoger todos los estados de los pines en una variable y finalmente enviarlos al puerto en un solo paso, lo cual sólo necesita las funciones read-from-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 lógica 1 salida en los pines; si sus LEDs tienen "lógica negativa" (el LED se enciende cuando el pin está bajo ), puede invertir la polaridad de la señal PWM simplemente cambiando if (cnt < dutyCycle...) a if (cnt >= dutyCycle...) .

0 votos

Vaya, eres increíble. Me preguntaba si mi comprensión de lo que me dijiste que hiciera era correcta y ahora está esta respuesta tan informativa con ejemplos y todo. Gracias de nuevo.

0 votos

Sólo una cosa más, ¿he entendido bien esto? Si tuviera que hacer el PWM cada dos desbordamientos del temporizador pondría un if en la rutina de interrupción para que sólo se ejecute el código PWM cada dos veces. Haciendo esto, si mi código PWM tarda demasiado y la siguiente interrupción por desbordamiento se pierde, entonces mi programa estará bien porque la siguiente interrupción no iba a hacer nada de todos modos. ¿Es eso lo que quieres decir?

0 votos

Sí, esto es lo que quería decir, perdón por ser tan breve al respecto. El ISR debería ser lo suficientemente rápido como 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 puede no ser bueno tampoco, por lo que podrías reducirlo casi a la mitad saltando la lógica "compleja" cada dos interrupciones dejando más tiempo para otras tareas.

2voto

Mike Crittenden Puntos 2575

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

Parece que las interrupciones tienen una latencia total de un cuarto 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 la salida de la primera.

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

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

En consecuencia, el período mínimo de PWM es de 256*6,7 µs = 1715 µs y la 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. Como tu código es bastante simple sospecho que esto se lleva una buena parte de la latencia.

Una solución para omitir los valores bajos todavía tendrá una latencia al menos como 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.

Mientras no sea inferior a un paso PWM, el ciclo de trabajo PWM comenzará en un valor superior. Sólo una ligera mejora de lo que tienes ahora.

Veo que ya usas el 25% del tiempo del procesador en interrupciones, así que por qué no usas el 50% o más, dejas la segunda interrupción y sólo haces un pool para la bandera de comparación. Si usas valores sólo hasta 128 tendrás sólo hasta un 50% de ciclo de trabajo, pero con la latencia de dos instrucciones que podrían ser optimizadas en ensamblador.

i-Ciencias.com

I-Ciencias es una comunidad de estudiantes y amantes de la ciencia en la que puedes resolver tus problemas y dudas.
Puedes consultar las preguntas de otros usuarios, hacer tus propias preguntas o resolver las de los demás.

Powered by:

X