ATtiny13A - Impossible de générer un logiciel PWM avec le mode CTC

8

J'essaie de créer une lumière LED RGB à télécommande à l'aide d'un ATtiny13A.

Je sais que l'Attiny85 est mieux adapté à cette fin, et je sais que je ne pourrai peut-être pas éventuellement adapter tout le code, mais pour l'instant ma principale préoccupation est de générer un logiciel PWM utilisant des interruptions en mode CTC.

Je ne peux pas fonctionner dans un autre mode (sauf pour PWM rapide avec OCR0Aas TOPqui est fondamentalement la même chose) parce que le code du récepteur IR que j'utilise a besoin d'une fréquence de 38 kHz qu'il génère en utilisant CTC et OCR0A=122.

J'essaie donc (et j'ai vu des gens le mentionner sur Internet) d'utiliser les interruptions Output Compare Aet Output Compare Bpour générer un logiciel PWM.

OCR0A, qui est également utilisé par le code IR, détermine la fréquence, ce qui m'est égal. Et OCR0B, détermine le rapport cyclique du PWM que je vais utiliser pour changer les couleurs des LED.

Je m'attends à pouvoir obtenir un PWM avec un rapport cyclique de 0 à 100% en changeant la OCR0Bvaleur de 0à OCR0A. Voici ma compréhension de ce qui devrait arriver:

La forme d'onde

Mais ce qui se passe réellement est le suivant (ceci provient de la simulation Proteus ISIS):

Comme vous pouvez le voir ci-dessous, je peux obtenir un rapport cyclique d'environ 25% à 75% mais pour ~ 0-25% et ~ 75-100% la forme d'onde est juste bloquée et ne change pas.

Ligne JAUNE: Matériel PWM

Ligne ROUGE: Logiciel PWM avec rapport cyclique fixe

Ligne VERTE: logiciel PWM avec cycle d'utilisation variable

Résultats de l'oscilloscope

Et voici mon 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
}
Pouria P
la source
Puis-je vous demander pourquoi vous ne pouvez pas utiliser le PWM matériel? La raison pour laquelle vous donnez n'a aucun sens. La seule raison de ne pas utiliser de matériel est si vous avez besoin d'une interface SPI ou d'une interruption externe.
Maple
@Maple J'essaie de contrôler une LED RGB, j'ai donc besoin de 3 signaux PWM, un pour chaque couleur. OCR0Aest utilisé par le code IR donc je n'ai que OCR0B. J'essaie de l'utiliser pour générer un logiciel PWM sur 3 broches non PWM.
Pouria P
Le logiciel PWM à 38 kHz ne fonctionnera pas. C'est trop rapide pour le MCU.
JimmyB
1
Vous pouvez (et vous l'avez fait) exécuter un ISR à 38 kHz. Mais pour tout cycle d'utilisation autre que 50%, vous aurez besoin d'une fréquence plus élevée. Exemple: Pour 25% à 38 kHz, vous devez être capable de gérer deux interruptions successives dans un délai de 38 kHz / 25% = 152 kHz. Cela ne laisse que 63 cycles d'horloge du processeur (9600 kHz / 152 kHz) pour l'ISR. À un rapport cyclique de 10%, il vous reste 25 horloges CPU pour l'ISR.
JimmyB
3
Vous n'avez pas spécifié la fréquence PWM souhaitée. Pour le contrôle de la luminosité, vous n'aurez pas besoin d'être proche de 38 kHz. 100 Hz peut être suffisant. Je vous suggère d'utiliser la fréquence de 38 kHz (IR) comme le rapport cyclique le plus bas pour votre logiciel PWM et de mettre en œuvre le PWM comme un multiple de cela, par exemple 256, de sorte que le rapport cyclique le plus bas soit 1/256 (une période d'horloge de 38 kHz) et le la plus élevée (inférieure à 100%) est (255/256), égale à 255 périodes d'horloge à 38 kHz. Cela vous donne un PWM 8 bits à (38000/256) ~ 148 Hz.
JimmyB

Réponses:

8

Un PWM logiciel minimal pourrait ressembler à ceci:

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

Votre programme est réglé dutyCyclesur la valeur souhaitée et l'ISR émet le signal PWM correspondant. dutyCycleest un uint16_tpour permettre des valeurs comprises entre 0 et 256 inclus; 256 est plus grand que toute valeur possible currentPwmCountet fournit ainsi un cycle de service complet de 100%.

Si vous n'avez pas besoin de 0% (ou 100%), vous pouvez raser certains cycles en utilisant un uint8_tafin que soit 0un cycle d'utilisation de 1/256 255soit 100% soit 00% 255soit un cycle d'utilisation de 255 / 256.

Vous n'avez toujours pas beaucoup de temps dans un ISR à 38 kHz; en utilisant un petit assembleur en ligne, vous pouvez probablement réduire le nombre de cycles de l'ISR de 1/3 à 1/2. Alternative: exécutez votre code PWM uniquement tous les autres dépassements de minuterie, réduisant de moitié la fréquence PWM.

Si vous avez plusieurs canaux PWM et que les broches que vous utilisez PMW sont toutes identiques, PORTvous pouvez également collecter tous les états des broches dans une variable et enfin les sortir sur le port en une seule étape qui n'a alors besoin que de la lecture de- port, et-avec-masque, ou-avec-nouvel état, écriture sur port une fois au lieu d' une fois par broche / canal .

Exemple:

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

Ce code mappe le rapport cyclique à une 1sortie logique sur les broches; si vos LED ont une «logique négative» (LED allumée lorsque la broche est faible ), vous pouvez inverser la polarité du signal PWM en changeant simplement if (cnt < dutyCycle...)en if (cnt >= dutyCycle...).

JimmyB
la source
Wow tu es génial. Je me demandais si ma compréhension de ce que vous m'avez dit de faire était correcte et maintenant il y a cette réponse très informative avec des exemples et tout. Merci encore.
Pouria P
Encore une chose, ai-je bien compris: si je faisais le PWM tous les autres débordements de temporisateur, je mettrais un ifdans la routine d'interruption pour exécuter uniquement le code PWM une fois sur deux. En faisant cela si mon code PWM prend trop de temps et que la prochaine interruption de débordement est manquée, mon programme ira bien car la prochaine interruption n'allait rien faire de toute façon. C'est ce que vous vouliez dire?
Pouria P
Oui, c'est ce que je voulais dire, désolé d'être si bref à ce sujet. L'ISR devrait être assez rapide pour ne manquer aucune interruption, mais même quand c'est le cas, passer 90% du temps CPU dans un ISR peut ne pas être bon non plus, vous pouvez donc réduire cela presque de moitié en sautant le ' logique complexe toutes les deux interruptions laissant plus de temps pour d'autres tâches.
JimmyB
2

Comme l'a commenté @JimmyB, la fréquence PWM est trop élevée.

Il semble que les interruptions aient une latence totale d'un quart du cycle PWM.

En cas de chevauchement, le rapport cyclique est fixé par la latence totale, car la deuxième interruption est mise en file d'attente et exécutée après la sortie de la première.

Le rapport cyclique PWM minimum est donné par le pourcentage de latence d'interruption total dans la période PWM. La même logique s'applique au cycle de service PWM maximal.

En regardant les graphiques, le rapport cyclique minimum est d'environ 25%, puis la latence totale doit être ~ 1 / (38000 * 4) = 6,7 µs.

En conséquence, la période PWM minimale est de 256 * 6,7 µs = 1715 µs et 583 Hz de fréquence maximale.

Quelques explications supplémentaires sur les correctifs possibles à haute fréquence:

L'interruption a deux fenêtres aveugles lorsque rien ne peut être fait, entrant en fin de l'interruption lorsque le contexte est enregistré et récupéré. Puisque votre code est assez simple, je soupçonne que cela prend une bonne partie de la latence.

Une solution pour ignorer les valeurs faibles aura toujours une latence au moins aussi élevée que la sortie de l'interruption et l'entrée dans l'interruption suivante, de sorte que le rapport cyclique minimum ne sera pas comme prévu.

Tant qu'il ne s'agit pas d'une étape PWM, le rapport cyclique PWM commencera à une valeur plus élevée. Juste une légère amélioration par rapport à ce que vous avez maintenant.

Je vois que vous utilisez déjà 25% du temps processeur dans les interruptions, alors pourquoi n'en utilisez-vous pas 50% ou plus, laissez la deuxième interruption et regroupez-vous simplement pour l'indicateur de comparaison. Si vous n'utilisez que des valeurs jusqu'à 128, vous n'aurez que 50% de rapport cyclique, mais avec la latence de deux instructions qui pourraient être optimisées dans l'assembleur.

Dorian
la source