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 OCR2A
Register, 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 OCR1A
Register, 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 = 100
erhalten . 625Hz
Und das gilt bis ~1200Hz (OCR1A = 52)
. Danach OCR1A
bleibt 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?
Foto zeigt, dass die Abtastfrequenz richtig ist (16000000 / 256 = 62500).
Mein Mikrocontroller ist "Arduino Nano3". IDE – Atmel Studio.
Vielen Dank für Ihre Zeit.
Aktualisierung:
MOV
eine 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
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.
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.
jaskij
Batawi
Batawi
Benutzer98663
Rohr