ATtiny13A - Não é possível gerar o software PWM com o modo CTC

8

Estou tentando criar uma luz LED RGB de controle remoto usando um ATtiny13A.

Sei que o ATtiny85 é mais adequado para essa finalidade e sei que talvez não consiga ajustar todo o código, mas, por enquanto, minha principal preocupação é gerar um PWM de software usando interrupções no modo CTC.

Não posso operar em nenhum outro modo (exceto no PWM rápido com OCR0Ao TOPqual é basicamente a mesma coisa) porque o código do receptor de IR que estou usando precisa de uma frequência de 38 kHz que gera usando CTC e OCR0A=122.

Então, estou tentando (e vi pessoas mencionarem isso na Internet) usar o Output Compare Ae Output Compare Binterrompe para gerar um PWM de software.

OCR0A, que também é usado pelo código IR, determina a frequência com a qual não me importo. E OCR0Bdetermina o ciclo de trabalho do PWM que utilizarei para alterar as cores dos LEDs.

Espero conseguir um PWM com ciclo de trabalho de 0 a 100%, alterando o OCR0Bvalor de 0para OCR0A. Esta é a minha compreensão do que deve acontecer:

A forma de onda

Mas o que realmente está acontecendo é isso (isto é, da simulação Proteus ISIS):

Como você pode ver abaixo, sou capaz de obter cerca de 25% a 75% do ciclo de trabalho, mas para ~ 0-25% e ~ 75-100% a forma de onda está presa e não muda.

Linha AMARELA: Hardware PWM

Linha RED: Software PWM com ciclo de serviço fixo

Linha VERDE: Software PWM com ciclo de trabalho variável

Resultados do osciloscópio

E aqui está o meu 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
fonte
Posso perguntar por que você não pode usar o hardware PWM? A razão que você está dando não faz nenhum sentido. O único motivo para não usar o hardware é se você precisa de interface SPI ou interrupção externa.
Maple
@ Maple Estou tentando controlar um LED RGB e preciso de 3 sinais PWM, um para cada cor. OCR0Aé usado pelo código IR, então eu só tenho OCR0B. Estou tentando usá-lo para gerar software PWM em 3 pinos não-PWM.
Pouria P
O software de 38kHz PWM não funcionará. Isso é rápido demais para o MCU.
21718 JimmyB
1
Você pode (e fez isso) executar um ISR a 38kHz. Mas, para qualquer ciclo de trabalho que não seja 50%, você precisará de uma frequência mais alta. Exemplo: para 25% a 38kHz, você deve poder lidar com duas interrupções sucessivas em um período de 38kHz / 25% = 152kHz. Isso deixa apenas cerca de 63 ciclos de clock da CPU (9600kHz / 152kHz) para o ISR. No ciclo de trabalho de 10%, você tem 25 relógios da CPU restantes para o ISR.
JimmyB
3
Você não especificou a frequência PWM desejada. Para controle de brilho, você não precisará estar perto de 38kHz. 100Hz pode ser suficiente. Sugiro que você use a frequência de 38kHz (IR) como o ciclo de trabalho mais baixo para o seu PWM de software e implemente o PWM como um múltiplo disso, por exemplo, 256, para que o ciclo de trabalho mais baixo seja 1/256 (um período de relógio de 38kHz) e o o mais alto (abaixo de 100%) é (255/256), igual a 255 períodos de clock de 38kHz. Isso fornece um PWM de 8 bits a (38000/256) ~ 148Hz.
JimmyB

Respostas:

8

Um PWM mínimo de software poderia ter esta aparência:

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

Seu programa define dutyCycleo valor desejado e o ISR emite o sinal PWM correspondente. dutyCycleé um uint16_tpara permitir valores entre 0 e 256 inclusive; 256 é maior que qualquer valor possível currentPwmCounte, portanto, fornece um ciclo de trabalho 100% completo.

Se você não precisar de 0% (ou 100%), poderá cortar alguns ciclos usando um uint8_tpara que 0resulte em um ciclo de trabalho de 1/256 e 255seja 100% ou 0seja 0% e 255seja um ciclo de trabalho de 255 / 256

Você ainda não tem muito tempo em um ISR de 38kHz; usando um pequeno montador em linha, você provavelmente pode reduzir a contagem de ciclos do ISR em 1/3 a 1/2. Alternativa: execute seu código PWM apenas em todos os outros temporizadores, reduzindo pela metade a frequência PWM.

Se você possui vários canais PWM e os pinos que você usa como PMW estão todos iguais, PORTvocê também pode coletar todos os estados dos pinos em uma variável e finalmente enviá-los para a porta em uma etapa que só precisa da leitura de porta, com máscara, ou com novo estado, gravação na porta uma vez em vez de uma vez por pino / canal .

Exemplo:

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

Esse código mapeia o ciclo de trabalho para uma 1saída lógica nos pinos; se seus LEDs tiverem 'lógica negativa' (LED aceso quando o pino estiver baixo ), você poderá inverter a polaridade do sinal PWM simplesmente mudando if (cnt < dutyCycle...)para if (cnt >= dutyCycle...).

JimmyB
fonte
Uau, você é incrível. Fiquei pensando se meu entendimento do que você me disse para fazer estava correto e agora há essa resposta altamente informativa com exemplos e tudo. Obrigado novamente.
Pouria P
Só mais uma coisa, eu entendi isso corretamente: se eu fizesse o PWM em qualquer outro temporizador, eu colocaria uma ifrotina de interrupção para executar apenas o código PWM em todas as outras vezes. Ao fazer isso, se meu código PWM demorar muito e a próxima interrupção de transbordamento for perdida, meu programa ficará bom porque a próxima interrupção não faria nada de qualquer maneira. É isso que você quis dizer?
Pouria P
Sim, foi isso que eu quis dizer, desculpe por ser tão breve sobre isso. O ISR deve ser rápido o suficiente para não perder nenhuma interrupção, mas mesmo assim, gastar 90% do tempo da CPU em um ISR também pode não ser bom; portanto, você pode reduzir isso quase pela metade pulando o botão ' lógica complexa todas as outras interrupções, deixando mais tempo para outras tarefas.
21818 JimmyB
2

Como o @JimmyB comentou, a frequência do PWM é muito alta.

Parece que as interrupções têm uma latência total de um quarto do ciclo do PWM.

Ao se sobrepor, o ciclo de serviço é fixo, dado pela latência total, pois a segunda interrupção é enfileirada e executada após a saída da primeira.

O ciclo de trabalho mínimo do PWM é fornecido pela porcentagem total de latência de interrupção no período do PWM. A mesma lógica se aplica ao ciclo de trabalho PWM máximo.

Observando os gráficos, o ciclo de trabalho mínimo é de cerca de 25% e a latência total deve ser ~ 1 / (38000 * 4) = 6,7 µs.

Como conseqüência, o período mínimo de PWM é de 256 * 6,7 µs = 1715 µs e frequência máxima de 583 Hz.

Mais algumas explicações sobre possíveis correções em alta frequência:

A interrupção possui duas janelas cegas quando nada pode ser feito, entrando e saindo da interrupção quando o contexto é salvo e recuperado. Como seu código é bastante simples, suspeito que isso considere boa parte da latência.

Uma solução para ignorar os valores baixos ainda terá uma latência, pelo menos, ao sair da interrupção e entrar na próxima interrupção, para que o ciclo de trabalho mínimo não seja o esperado.

Desde que isso não seja menor que uma etapa do PWM, o ciclo de trabalho do PWM começará com um valor mais alto. Apenas uma ligeira melhoria do que você tem agora.

Vejo que você já usa 25% do tempo do processador em interrupções; então, por que você não usa 50% ou mais dele, deixa a segunda interrupção e apenas agrupa o sinalizador de comparação. Se você usar valores apenas até 128, terá apenas até 50% de ciclo de serviço, mas com a latência de duas instruções que poderiam ser otimizadas no assembler.

Dorian
fonte