6
\$\begingroup\$


I'm currently implementing sinewave generator on ATmega328p (16MHz). My project is mostly based on this article https://makezine.com/projects/make-35/advanced-arduino-sound-synthesis/.

In brief, I have two timers. First one (pwm_timer) counts up from 0 to 255 and sets output pin based on value in OCR2A register, which is creating PWM signal. Second timer (sample_timer) uses interrupt (ISR) to change value of OCR2A. ISR occurs when timer value is the same as value in OCR1A register, after that, timer is set to zero.

So this is my 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
    }
}

In theory, output signal frequency should be equal to this formula 16Mhz / (LOOKUP_TABLE_LENGTH * OCR1A). So for OCR1A = 100 we should obtain 625Hz sine wave. And this is true until ~1200Hz (OCR1A = 52). After that, regardless of value of OCR1A - output frequency stays the same. The question is why?

I think the problem lies in the execution time of ISR. Is there any way to speed it up, maybe optimize the code? Maybe I should write it in assembly?

I know I can ramp up frequency by decreasing length of look up table but I really want to stay with 256 samples.

Side note. I realize that adding some asm(“NOP”) into main loop increase frequency a little (1250Hz). Maybe this while(1) is quilty too?

1247Hz Result of the code above.

Sampling freq Photo showing that sampling frequency is right (16000000 / 256 = 62500).

My microcontroller is "Arduino Nano3". IDE - Atmel Studio.

Thank you for your time.

Updates:

  • I check frequency by using DSO Shell and speaker with spectrum analyzer.
  • Decreasing value of resistance and capacitance add few hertz but it is negligible.
  • Disassembly reveals that ISR is not simply MOV instruction.
    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 
\$\endgroup\$
  • \$\begingroup\$ Decrease that capacitor to 10n and/or decrease the resistance. The low pass RC might be the one to blame. \$\endgroup\$ – Jan Dorniak Apr 5 at 8:06
  • \$\begingroup\$ Post updated @Wossname \$\endgroup\$ – Batawi Apr 5 at 9:28
  • \$\begingroup\$ Post updated @JanDorniak \$\endgroup\$ – Batawi Apr 5 at 9:29
  • \$\begingroup\$ Thanks for the update. I do not think that the ISR is to blame. As ISRs go, that is a very lightweight one. \$\endgroup\$ – Wossname Apr 5 at 10:15
  • \$\begingroup\$ I'm still surprised that the ISR is so bloated, but I don't have enough AVR experience to judge if it's necessary. \$\endgroup\$ – pipe Apr 5 at 11:39
4
\$\begingroup\$

For 1200hz and a 256 lookup table you have 16000000/(256*1200) = 52 cycles between interrupts.

If you count the steps in the interrupt ASM code you're at the rock bottom limit if not below.

In the main loop there is a jump that needs two cycles, if you add nop's the jump will occur less often , that's why you have the tiny improvement.

You can move the interrupt code into the main loop to spare some cycles ( down to three times less) because PUSH's and POP's are slower. Then use nop's to obtain the desired frequency. Disable any interrupt.

There is also one big limitation that is still there , how can you update a 256 step PWM after only 52 cycles? Even if you don't want to reduce the look-up table length many writings to PWM are actually ignored.

Since there is nothing you can do except the value update you might improvise a resistor DAC on the digital port.

\$\endgroup\$
  • \$\begingroup\$ That explains a lot, thanks @Dorian. \$\endgroup\$ – Batawi Apr 6 at 9:09
4
\$\begingroup\$

Besides what @Dorian says, note that you are operating the PWM timer and the sampling timer at the same frequency. You have one PWM cycle every 256 CPU cycles. If you change the PWM duty cycle more frequently than every 256 CPU cycles, in fast PWM mode you will get glitches/distortions in the output.

To mitigate the problems, in a first step you could add a low-pass (RC) filter at the PWM output to create a sine-like signal of x Hz from a 50% PWM of x Hz, obviating the lookup table. Or use a higher frequency low-pass and reduce the lookup table to, say 4 or 8 entries, reducing the ISR frequency to 4 or 8x the output frequency (instead of 256x) and letting the filter smooth out the transitions between the steps.

As an alternative, you could look into the ATtiny2/4/85 chips which offer a "real" fast PWM off a timer running at up to 64MHz.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.