ATmega328p Sinusgenerator, Frequenzbegrenzung bei 1200Hz


Ich implementiere derzeit einen Sinusgenerator auf ATmega328p (16 MHz). Mein Projekt basiert hauptsächlich auf diesem Artikel https://makezine.com/projects/make-35/advanced-arduino-sound-synthesis/ .

Kurz gesagt, ich habe zwei Timer. Der erste (pwm_timer) zählt von 0 bis 255 und setzt den Ausgangspin basierend auf dem Wert im OCR2ARegister, wodurch ein PWM-Signal erzeugt wird. Der zweite Timer (sample_timer) verwendet den Interrupt (ISR), um den Wert von zu ändern OCR2A. ISR tritt auf, wenn der Timer-Wert derselbe ist wie der Wert im OCR1ARegister, danach wird der Timer auf Null gesetzt.

Das ist also mein Code:

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

Theoretisch sollte die Frequenz des Ausgangssignals dieser Formel entsprechen 16Mhz / (LOOKUP_TABLE_LENGTH * OCR1A). Wir sollten also eine Sinuswelle OCR1A = 100erhalten . 625HzUnd das gilt bis ~1200Hz (OCR1A = 52). Danach OCR1Ableibt unabhängig vom Wert von - die Ausgangsfrequenz gleich. Die Frage ist warum?

Ich denke, das Problem liegt in der Ausführungszeit von ISR. Gibt es eine Möglichkeit, es zu beschleunigen, vielleicht den Code zu optimieren? Vielleicht sollte ich es in Assembler schreiben?

Ich weiß, dass ich die Frequenz erhöhen kann, indem ich die Länge der Nachschlagetabelle verringere, aber ich möchte wirklich bei 256 Samples bleiben.

Randnotiz. Mir ist klar, dass das Hinzufügen einiger asm(“NOP”)in die Hauptschleife die Frequenz ein wenig erhöht (1250 Hz). Vielleicht ist dieses while(1) auch quilty?

1247HzErgebnis des obigen Codes.

AbtastfrequenzFoto zeigt, dass die Abtastfrequenz richtig ist (16000000 / 256 = 62500).

Mein Mikrocontroller ist "Arduino Nano3". IDE – Atmel Studio.

Vielen Dank für Ihre Zeit.

Aktualisierung:

  • Ich überprüfe die Frequenz mit DSO Shell und Lautsprecher mit Spektrumanalysator.
  • Ein abnehmender Wert von Widerstand und Kapazität fügt wenige Hertz hinzu, ist aber vernachlässigbar.
  • Die Demontage zeigt, dass ISR nicht nur MOVeine Anweisung ist.
    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 
Verringern Sie diesen Kondensator auf 10n und/oder verringern Sie den Widerstand. Der Tiefpass RC könnte schuld sein.
Post aktualisiert @Wossname
Beitrag aktualisiert @JanDorniak
Danke für das Update. Ich denke nicht, dass die ISR schuld ist. Was ISRs angeht, ist das ein sehr leichtes.
Ich bin immer noch überrascht, dass der ISR so aufgebläht ist, aber ich habe nicht genug AVR-Erfahrung, um zu beurteilen, ob es notwendig ist.

Antworten (2)

Für 1200 Hz und eine 256-Lookup-Tabelle haben Sie 16000000/(256*1200) = 52 Zyklen zwischen Interrupts.

Wenn Sie die Schritte im Interrupt-ASM-Code zählen, sind Sie an der untersten Grenze, wenn nicht darunter.

In der Hauptschleife gibt es einen Sprung, der zwei Zyklen benötigt. Wenn Sie Nops hinzufügen, tritt der Sprung weniger oft auf, deshalb haben Sie die winzige Verbesserung.

Sie können den Interrupt-Code in die Hauptschleife verschieben, um einige Zyklen zu sparen (bis zu dreimal weniger), da PUSHs und POPs langsamer sind. Verwenden Sie dann nop's, um die gewünschte Frequenz zu erhalten. Deaktivieren Sie jeden Interrupt.

Es gibt auch eine große Einschränkung, die immer noch vorhanden ist: Wie können Sie eine PWM mit 256 Schritten nach nur 52 Zyklen aktualisieren? Auch wenn Sie die Länge der Nachschlagetabelle nicht reduzieren möchten, werden viele Eingaben an PWM tatsächlich ignoriert.

Da Sie außer der Wertaktualisierung nichts tun können, können Sie einen Widerstands-DAC am digitalen Anschluss improvisieren.

Das erklärt viel, danke @Dorian.

Beachten Sie neben dem, was @Dorian sagt, dass Sie den PWM-Timer und den Sampling-Timer mit derselben Frequenz betreiben. Sie haben alle 256 CPU-Zyklen einen PWM-Zyklus. Wenn Sie das PWM-Tastverhältnis häufiger als alle 256 CPU-Zyklen ändern, treten im schnellen PWM-Modus Störungen/Verzerrungen im Ausgang auf.

Um die Probleme zu entschärfen, könnten Sie in einem ersten Schritt einen Tiefpassfilter (RC) am PWM-Ausgang hinzufügen, um aus einer 50%-PWM von x Hz ein sinusähnliches Signal von x Hz zu erzeugen, wodurch die Nachschlagetabelle umgangen wird. Oder verwenden Sie einen Tiefpass mit höherer Frequenz und reduzieren Sie die Nachschlagetabelle auf beispielsweise 4 oder 8 Einträge, reduzieren Sie die ISR-Frequenz auf das 4- oder 8-fache der Ausgangsfrequenz (statt 256x) und lassen Sie den Filter die Übergänge zwischen den Schritten glätten.

Als Alternative könnten Sie sich die ATtiny2/4/85-Chips ansehen, die einen "echten" schnellen PWM-Off-Timer mit bis zu 64 MHz bieten.