6 votos

Generador de ondas sinusoidales ATmega328p, con un tope de frecuencia de 1200Hz

Actualmente estoy implementando un generador de ondas sinusoidales en ATmega328p (16MHz). Mi proyecto se basa principalmente en este artículo https://makezine.com/projects/make-35/advanced-arduino-sound-synthesis/ .

En resumen, tengo dos temporizadores. El primero (pwm_timer) cuenta de 0 a 255 y establece el pin de salida basado en el valor en OCR2A registro, que está creando la señal PWM. El segundo temporizador (sample_timer) utiliza la interrupción (ISR) para cambiar el valor de OCR2A . El ISR se produce cuando el valor del temporizador es el mismo que el valor en OCR1A registro, después de eso, el temporizador se pone a cero.

Así que este es mi código:

#include <avr/io.h>
#include <avr/interrupt.h>
#include <math.h>

/******** Sine wave parameters ********/
#define PI2 6.283185 // 2*PI saves calculation later
#define AMP 127 // Scaling factor for sine wave
#define OFFSET 128 // Offset shifts wave to all >0 values

/******** Lookup table ********/
#define LENGTH 256 // Length of the wave lookup table
uint8_t wave[LENGTH]; // Storage for waveform
static uint8_t index = 0; // Points to each table entry

/******** Functions ********/
static inline void populate_lookup_table(void);
static inline void setup_pwm_timer(void);
static inline void setup_sample_timer(void);

int main(void)
{
    populate_lookup_table();
    setup_pwm_timer();
    setup_sample_timer();

    while(1)
    {
        asm("NOP");
        asm("NOP");
        asm("NOP");
        asm("NOP");
        asm("NOP");
        asm("NOP");
        asm("NOP");
        asm("NOP");
    }
}

ISR(TIMER1_COMPA_vect) // Called when TCNT1 == OCR1A
{
     OCR2A = wave[++index]; // Update the PWM output
}

static inline void setup_pwm_timer()
{
    TCCR2A = 0;
    TCCR2B = 0;

    TCCR2A |= _BV(WGM21) | _BV(WGM20); // Set fast PWM mode
    TCCR2A |= _BV(COM2A1); // Clear OC2A on Compare Match, set at BOTTOM
    TCCR2B |= _BV(CS20); // No prescaling
    OCR2A = 127; // Initial value
    DDRB |= _BV(PB3); // OC2A as output (pin 11)
}   

static inline void setup_sample_timer()
{
    TCCR1A = 0;
    TCCR1B = 0;
    TIMSK1 = 0;

    TCCR1B |= _BV(WGM12); // Set up in count-till-clear mode (CTC)
    TCCR1B |= _BV(CS10); // No prescaling
    TIMSK1 |= _BV(OCIE1A); // Enable output-compare interrupt
    OCR1A = 40; // Set frequency
    sei(); // Set global interrupt flag
}

static inline void populate_lookup_table()
{
    // Populate the waveform table with a sine wave
    int i;
    for (i=0; i<LENGTH; i++) // Step across wave table
    {
        float v = (AMP*sin((PI2/LENGTH)*i)); // Compute value
        wave[i] = (int)(v+OFFSET); // Store value as integer
    }
}

En teoría, la frecuencia de la señal de salida debería ser igual a esta fórmula 16Mhz / (LOOKUP_TABLE_LENGTH * OCR1A) . Así que para OCR1A = 100 deberíamos obtener 625Hz onda sinusoidal. Y esto es cierto hasta que ~1200Hz (OCR1A = 52) . Después, independientemente del valor de OCR1A - la frecuencia de salida se mantiene igual. La pregunta es ¿por qué?

Creo que el problema radica en el tiempo de ejecución del ISR. ¿Hay alguna manera de acelerarlo, tal vez optimizar el código? ¿Tal vez debería escribirlo en ensamblador?

Sé que puedo aumentar la frecuencia disminuyendo la longitud de la tabla de búsqueda, pero realmente quiero quedarme con 256 muestras.

Nota al margen. Me doy cuenta de que añadir algo de asm(“NOP”) en el bucle principal aumentar un poco la frecuencia (1250Hz). ¿Quizás este while(1) también es culpable?

1247Hz Resultado del código anterior.

Sampling freq Foto que muestra que la frecuencia de muestreo es correcta (16000000 / 256 = 62500).

Mi microcontrolador es "Arduino Nano3". IDE - Atmel Studio.

Gracias por su tiempo.

Actualizaciones:

  • Compruebo la frecuencia utilizando el DSO Shell y el altavoz con el analizador de espectro.
  • La disminución del valor de la resistencia y la capacitancia agrega algunos hertzios, pero es insignificante.
  • El desmontaje revela que el ISR no es simplemente MOV instrucción.

    40: {

    1f.92 PUSH R1 Push register on stack 0f.92 PUSH R0 Push register on stack 0f.b6 IN R0,0x3F In from I/O location 0f.92 PUSH R0 Push register on stack 11.24 CLR R1 Clear Register 8f.93 PUSH R24 Push register on stack ef.93 PUSH R30 Push register on stack ff.93 PUSH R31 Push register on stack 41: OCR2A = wave[++index]; // Update the PWM output e0.91.00.01 LDS R30,0x0100 Load direct from data space ef.5f SUBI R30,0xFF Subtract immediate e0.93.00.01 STS 0x0100,R30 Store direct to data space f0.e0 LDI R31,0x00 Load immediate ef.5f SUBI R30,0xFF Subtract immediate fe.4f SBCI R31,0xFE Subtract immediate with carry 80.81 LDD R24,Z+0 Load indirect with displacement 80.93.b3.00 STS 0x00B3,R24 Store direct to data space 42: } ff.91 POP R31 Pop register from stack ef.91 POP R30 Pop register from stack 8f.91 POP R24 Pop register from stack 0f.90 POP R0 Pop register from stack 0f.be OUT 0x3F,R0 Out to I/O location 0f.90 POP R0 Pop register from stack 1f.90 POP R1 Pop register from stack 18.95 RETI Interrupt return

0 votos

Disminuye ese condensador a 10n y/o disminuye la resistencia. El RC de paso bajo podría ser el culpable.

0 votos

Post actualizado @Wossname

0 votos

Post actualizado @JanDorniak

4voto

Mike Crittenden Puntos 2575

Para 1200hz y una tabla de búsqueda de 256 tienes 16000000/(256*1200) = 52 ciclos entre interrupciones.

Si cuentas los pasos del código ASM de interrupción estás en el límite inferior, si no por debajo.

En el bucle principal hay un salto que necesita dos ciclos, si añades nop's el salto ocurrirá menos veces , por eso tienes la pequeña mejora.

Puedes mover el código de interrupción al bucle principal para ahorrar algunos ciclos (hasta tres veces menos) porque los PUSH's y POP's son más lentos. Entonces usa nop's para obtener la frecuencia deseada. Desactiva cualquier interrupción.

También hay una gran limitación que sigue ahí, ¿cómo puedes actualizar un PWM de 256 pasos después de sólo 52 ciclos? Incluso si no quieres reducir la longitud de la tabla de consulta muchas escrituras al PWM son realmente ignoradas.

Como no se puede hacer nada más que la actualización del valor se podría improvisar una resistencia DAC en el puerto digital.

0 votos

Eso explica mucho, gracias @Dorian.

4voto

pgs Puntos 2491

Además de lo que dice @Dorian, ten en cuenta que estás operando el temporizador PWM y el temporizador de muestreo a la misma frecuencia. Tienes un ciclo PWM cada 256 ciclos de CPU. Si cambias el ciclo de trabajo PWM con más frecuencia que cada 256 ciclos de CPU, en el modo PWM rápido obtendrás glitches/distorsiones en la salida.

Para mitigar los problemas, en un primer paso podrías añadir un filtro de paso bajo (RC) a la salida del PWM para crear una señal sinusoidal de x Hz a partir de un PWM del 50% de x Hz, obviando la tabla de búsqueda. O utilizar un filtro de paso bajo de mayor frecuencia y reducir la tabla de búsqueda a, digamos, 4 u 8 entradas, reduciendo la frecuencia ISR a 4 u 8 veces la frecuencia de salida (en lugar de 256x) y dejando que el filtro suavice las transiciones entre los pasos.

Como alternativa, podrías buscar los chips ATtiny2/4/85 que ofrecen un PWM rápido "real" a partir de un temporizador que funciona hasta 64MHz.

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