This 16th article in the series of “Do It Yourself: Electronics”, demonstrates generating sine wave using the AVR micro-controller ATmega16.
Pugs have been thinking of trying some resistor-capacitor (RC) based experiments in his room, the way he has done in his electronics lab. But for that he needed to have some sort of sine wave signal generator. As usual of Pugs, rather than buying a function (wave) generator, he thought of starting with creating a simple sine wave generator of his own, say at a fixed frequency of 1KHz, using his micro-controller ATmega16.
For that Pugs needed some sort of analog output generator. And, interestingly he remembered already creating a PWM-based digital to analog conversion (DAC) approximation with Rohit, in one of his previous articles. There they have generated the analog output voltage on OC0/PB3 using Timer0. However, as always, Pugs is looking for learning things he has not yet tried. So, this time, he thought of doing the analog voltage generation on OC2/PD7 using Timer2.
On deeper thought, it clicked to Pugs that for a sine wave one needs different analog outputs but in a periodic fashion. And to get a perfect periodicity, it would possibly need one more timer, say Timer0.
“So, this time why not try interrupt-based timer for the periodicity. I would also get a chance to write a interrupt handler”, flashed the thought through Pugs’ mind.
Triggered by these thoughts, here is what the initialization code written by Pugs for the Timer2 as PWM and Timer0 as 50us period timer, respectively:
#include <avr/io.h> #include <avr/interrupt.h> #define PRESCALAR_BITS 0b010 #define PRESCALAR 8 // Gives 1MHz clock void init_pwm(void) { DDRD |= 0b10000000; // PD7 OCR2 = 0; // Initialize to 0V output // Setup OC2 on PD7 TCCR2 = (1 << WGM21) | (1 << WGM20); /* Fast PWM */ TCCR2 |= (2 << COM20); /* Clear on Match */ TCCR2 |= (1 << CS20); /* No prescaling => Clock @ F_CPU */ // Starts the PWM } void init_timer(void) /* Setting Timer 0 for trigger every 50us */ { sei(); // Enable global interrupts TIMSK |= (1 << OCIE0); // Enable Compare Match interrupt /* * Pre-scaled clock = F_CPU / PRESCALAR * => Each timer counter increment takes PRESCALAR / F_CPU seconds * => Formula for timer expiry interval is: * OCR0 (top timer count) * PRESCALAR / F_CPU * Example: For F_CPU = 8MHz, PRESCALAR = 8 * Pre-scaled clock = 8MHz / 8 = 1MHz * => Each timer counter increment takes 1/1MHz = 1us * => Formula for timer expiry interval is OCR0 (top timer count) * 1us * Example: For 50us = OCR0 * 1us, i.e. OCR0 = 50 */ OCR0 = (F_CPU / PRESCALAR) / 20000; /* 1/20000s for 50us */ /* * Setting & Starting the Timer/Counter0 in CTC (Clear Timer on Compare) * (non-PWM) for controlling timer expiry interval, directly by the compare * register */ TCCR0 = (1 << WGM01) | (0 << WGM00) | PRESCALAR_BITS; }
Obviously, Pugs referred to the ATmega16 datasheet pg 71-86 & pg 117-134. But the earlier articles on DAC and Music Generation using micro-controller were quick starters.
Computing the sine wave
Now, why is the Timer0 period set to 50us, meaning it will trigger every 50us. Let’s understand that by computing the sine wave. To generate a sine wave, one needs to output various analog voltages – theoretically speaking infinite different values between -1 to +1 multiplied by the amplitude. But practically, in our case we can make it only finite, say 20 different values. So after these 20 different values, we would repeat them, thus generating a periodic approximate sine wave. And to achieve a 1KHz sine wave, or in other words sine wave with a period of 1 / 1KHz = 1ms, we would need to output these 20 values spread over this 1ms, meaning outputting 1 value every 1ms / 20 = 50us. And, setting the Timer0 to 50us, now means outputting 1 value every time the Timer0 handler is triggered.
Time-wise computation for the sine wave done. But what about the amplitude-wise values to output during those Timer0 handler triggers. For that, note that the output on the GPIOs of a micro-controller could be only between 0V to 5V, which maps to programming the OCR2 of Timer2 between 0 to 255. Then, how do we generate negative voltages for sine wave. A usual trick applied in such scenarios is shift the 0V to the midpoint 2.5V and then oscillate the sine wave between 0 & 5V. That translates to oscillating the OCR2 values between 0 to 255 with 127 as the midpoint. Great, so for that let’s have this small handy function set_amplitude_centred():
#define DC_OFF 127 // Centred #define AMP_FACTOR 5 // Should range from 1-12, for 8-bit values between 0 & 2*12*10 void set_amplitude_centred(uint8_t a) { OCR2 = (DC_OFF + a); // DC shift by peak to get it centred around DC_OFF }
With this, we could have the maximum sine amplitude of 127 units in terms of OCR2. So, we shall take some default, say 10, and control it by multiplying it with the AMP_FACTOR macro defined above. This allows us to have a maximum amplification factor of 12 for an amplification of 12 * 10 = 120 (< 127). Currently, just defined it to some intermediate value of 5.
All boundaries set, but what about the actual 20 values we need. For that, we have to use the sine function at intervals of 360° / 20 = 18°, i.e. / 10 – the values being 0, 0.3, 0.6, 0.8, 0.9, 1.0, 0.9, 0.8, 0.6, 0.3, 0, -0.3, -0.6, -0.8, -0.9, -1.0, -0.9, -0.8, -0.6, -0.3. Multiplying by 10 (our default amplitude), we get 0, 3, 6, 8, 9, 10, 9, 8, 6, 3, 0, -3, -6, -8, -9, -10, -9, -8, -6, -3. Hence, finally we need to set the corresponding value, centred with 127, to OCR2 on every Timer2 interrupt trigger.
Programming the sine wave
Here’s the final sine_wave.c coded with an empty “while 1” in main(), as all logic is handled in the interrupt handler specified by ISR().
/* * Generates sine wave @ 1KHz on OC2/PD7, using PWM on Timer 2 * * The timing is achieved using timer interrupt handler of Timer 0 triggering * at a 20KHz frequency, i.e. every 50us. */ #include <avr/io.h> #include <avr/interrupt.h> #include <util/delay.h> #define ARR_SIZE(a) (sizeof(a) / sizeof(*a)) #define PRESCALAR_BITS 0b010 #define PRESCALAR 8 // Gives 1MHz clock #define DC_OFF 127 // Centred #define AMP_FACTOR 5 // Should range from 1-12, for 8-bit values between 0 & 2*12*10 /* Length = l = 20. Period = l * 50us = 1000us = 1ms */ // 0, 18, 36, 54, 72, 90, 108, 126, 144, 162, 180, 198, 216, 234, 252, 270, 288, 306, // 324, 342 degrees int sine[] = {0, 3, 6, 8, 9, 10, 9, 8, 6, 3, 0, -3, -6, -8, -9, -10, -9, -8, -6, -3}; void set_amplitude_centred(uint8_t a) { OCR2 = (DC_OFF + a); // DC shift by peak to get it centred around DC_OFF } ISR(TIMER0_COMP_vect) { static int i = 0; set_amplitude_centred(AMP_FACTOR * sine[i]); if (++i == ARR_SIZE(sine)) i = 0; } void init_pwm(void) { DDRD |= 0b10000000; // PD7 OCR2 = 0; // Initialize to 0V output // Setup OC2 on PD7 TCCR2 = (1 << WGM21) | (1 << WGM20); /* Fast PWM */ TCCR2 |= (2 << COM20); /* Clear on Match */ TCCR2 |= (1 << CS20); /* No prescaling => Clock @ F_CPU */ // Starts the PWM } void init_timer(void) /* Setting Timer 0 for trigger every 50us */ { sei(); // Enable global interrupts TIMSK |= (1 << OCIE0); // Enable Compare Match interrupt /* * Pre-scaled clock = F_CPU / PRESCALAR * => Each timer counter increment takes PRESCALAR / F_CPU seconds * => Formula for timer expiry interval is: * OCR0 (top timer count) * PRESCALAR / F_CPU * Example: For F_CPU = 8MHz, PRESCALAR = 8 * Pre-scaled clock = 8MHz / 8 = 1MHz * => Each timer counter increment takes 1/1MHz = 1us * => Formula for timer expiry interval is OCR0 (top timer count) * 1us * Example: For 50us = OCR0 * 1us, i.e. OCR0 = 50 */ OCR0 = (F_CPU / PRESCALAR) / 20000; /* 1/20000s for 50us */ /* * Setting & Starting the Timer/Counter0 in CTC (Clear Timer on Compare) * (non-PWM) for controlling timer expiry interval, directly by the compare * register */ TCCR0 = (1 << WGM01) | (0 << WGM00) | PRESCALAR_BITS; } int main(void) { init_pwm(); init_timer(); while (1) { _delay_ms(500); } return 0; }
Then, Pugs compiled the program as follows (Note the value of F_CPU defined as 8MHz as in the previous article):
$ avr-gcc -mmcu=atmega16 -DF_CPU=8000000 -Os sine_wave.c -o sine_wave.elf $ avr-objcopy -O ihex sine_wave.elf sine_wave.hex
And finally, downloaded the sine_wave.hex into the ATmega16 with J1 shorted (same as in the previous articles), using the following command:
$ avrdude -c ponyser -P /dev/ttyUSB0 -p m16 -U flash:w:sine_wave.hex:i
Note that Pugs have used the same ATmega16, which has been modified for 8MHz internal clock, as in the previous article, by writing 0xE4 into the lfuse, using avrdude. Otherwise, you may have to issue the following command as well:
$ avrdude -c ponyser -P /dev/ttyUSB0 -p m16 -U lfuse:w:0xE4:m
Pugs then checked the DC & AC voltages between PD7 and GND pins of the micro-controller, after removing the short of jumper J1. They showed up as approximately 2.5V (average) and 0.7V (root-mean-square (RMS)), respectively. And it was a visual treat to view the sine wave on the home-made PC oscilloscope, as created in his previous PC Oscilloscope article.