DDS audio generator made from PIC Microcontroller

I needed a sine wave generator and I didn’t want a whole lot of parts……. so I thought I would try making a Direct Digital Synthesizer out of a cheapo 8 pin micro controller.

I chose a 12F1822 PIC, but any micro with a PWM generator and an ADC will work.

A number of AVR and Microchip products are available. The 12f683 or the ATTINY85  are other devices that meet the basic  requirement. If using an AVR the code will have to changed but you can get the basic idea from the PIC implementation. If  you use another PIC, the code may need some minor tweaks , such as the pin assignments and/or the config register settings.

I settled on the  12f1822 for the following reasons: its cheap, its small, my C compiler supports it, it has an internal clk that runs up to 32MHz – which allows for 8 bit PWM at greater than 100kHz, which makes filtering the output trivial.  Also it is very power efficient.

The way DDS works  is by cycling through a look up table of values representing a sine wave(or any other wave form you choose… a triangle for example). These values range from 0-255 (8bit) and there are 256 values in  the table. Each value is loaded into a PWM generator in succession which varies the duty cycle of a roughly 100KHz PWM signal. The faster you  cycle through the values, the higher the frequency is generated.

I don’t want to explain all the details of how  DDS works- so go here to learn more about DDS:

Click to access MT-085.pdf

Here are the basics of my design:

I use the 10 bit ADC input to read a voltage of a POT and convert this to a number from 0 -1024. I use the PWM generator as a DAC to reproduce the sampled analog wave form. Then there is a simple filter to remove the high frequency artifacts caused by the 100Khz PWM signal. Every cycle of the PWM generator’s overflow flag – I add the value of the ADC to a 32 bit register which acts as a phase accumulator. The most significant byte of the phase accumulator is loaded as the address for the look up table value and as this value increments every cycle, a new value is loaded. If a smaller value is loaded into the phase accumulator, the frequency is lower. With a larger number loaded every cycle the frequency increases. The frequency resolution is given by 1/  (PWM period/2^32). This gives resolution less than a milli Hertz! The ADC value can be scaled as needed to get you in the frequency range of interest.

A note about my lookup table. The PWM generator will create distortion if the look up table values are not naturalized. This is because as the look up values are going up and down they should shift from when the off time occurs(either before the on time or after). So if you just generate the sine values they will work but not as well as they could. The solution is to naturalize the look up values. Which is to say shift them slightly. This process and how I did it is discussed here:

Also I scaled my sinewave to 90% of max and shifted it up 5%.  This makes it so that the PWM has no values to close to the extreme edges of its range. Also the output is from 1-4 volts instead of going all the way down to 0 volts. This way a single supply active filter can be employed.

Originally, I used a 12f683 PIC running at 8Mhz but could only support 20KHz PWM at 8bit resolution. The circuit worked but made it harder to filter the output and limited the frequency range to about 1Khz. the part I  used the 12f1822 has an internal clk that works up to 32 MHz. This allows a much faster PWM rate. Now I can get anywhere from .00005Hz to 5Khz output with a simple filter.

You can use an external crystal and get a more accurate and stable output, but the internal oscillator works pretty well. The harmonics are all 40db or more down from the fundamental.

Video demo:


C CODE: (compiled with MikroC ) will compile on freeware version with room to spare

Note!!!!! There is new code and is available here:

#define ADCStart ADCON0.b1 = 1 //set this bit to begin ADC conversion
const unsigned char sine[256] =
{// sine wave 8 bit resolution scaled to 90% max val
119,116,113,111,108,105,103,100, 97, 95, 92, 89, 87, 84, 82, 79,
77, 74, 72, 69, 67, 64, 62, 60, 58, 55, 53, 51, 49, 47, 45, 43,
41, 39, 37, 36, 34, 32, 31, 29, 28, 26, 25, 24, 22, 21, 20, 19,
18, 17, 16, 15, 15, 14, 13, 13, 12, 12, 12, 11, 11, 11, 11, 11,
11, 11, 11, 12, 12, 12, 13, 14, 14, 15, 16, 16, 17, 18, 19, 21,
22, 23, 24, 26, 27, 29, 30, 32, 33, 35, 37, 39, 41, 43, 45, 47,
49, 51, 53, 56, 58, 60, 63, 65, 68, 70, 73, 75, 78, 81, 83, 86,
89, 92, 94, 97, 100,103,106,108,111,114,117,120,123,126,129,130};
////////////////////////////////Global variable here//////////////////////////////////////////////
long PhaseAccum;//phase accumilator generates the cycle rate for lookup table
//loading therby changing frequency. The MSbyte is used to provide the byte address
//of the look up table value to be used.
long PhaseShift;//value added to PhaseAccum every PWM cycle. This makes the waveform
//lookup faster or slower – which changes frequency.
void ADCRead(){
ADCStart; //start ADC conversion
while(ADCON0.b1); //wait till done
((char *)&PhaseShift)[1] = ADRESL; //load ADRESL into PhaseShift
((char *)&PhaseShift)[2] = ADRESH; //load ADRESH into PhaseShift
////////////////////////////////PIC Config routine here////////////////////////////////////
void Init_Main(){
//PIC12F1822 specific config
OPTION_REG = 0b10000000; // disable internal pull ups
OSCCON = 0b11110000; //8MHz clk //32Mhz pll
TRISA = 0b00011000; // configure IO/a2d(gpio0) and mclr set as inputs
T2CON = 0b00000100;// TMR2 ON, postscale 1:1, prescale 1:1
PR2 = (0x50);// sets PWM rate to approx 98.5KHz with 32Mhz internal oscillator
CCP1CON = 0b00001111;// CCP1 ON, and set to simple PWM mode
PhaseShift = 0x00000000;//frequency values loaded into
ANSELA = 0b00010000; //select RA4 as A2D input 32Mhz clk
ADCON0 = 0b00001101; // configure ADC
ADCON1 = 0b00100000; // configure ADC
////////////////////////////main program loop here/////////////////////////////////
void main() {
Init_Main();//configure part
while(1){ //alway do this
while(!PIR1.TMR2IF);// wait for TMR2 cycle to restart
CCPR1L = (sine[((char *)&PhaseAccum)[3]]) >> 2;// load MSbits 7-2 duty cycle value into CCPRIL
CCP1CON ^=((sine[((char *)&PhaseAccum)[3]]) & 0x03) << 4;// load in bits 1-0 into 5 and 4 of CCP1CON
//////duty cycle value byte is now loaded for next cycle comming//////
ADCRead(); //read ADC here to get value into PhaseShift to change Freq
PhaseAccum = PhaseAccum + ((PhaseShift << 5) + 1); //move PhaseAccum through waveform values
//”<<5″ can be more or less and sets the frequency sweep range
// the +1 is just so there is never 0;
PIR1.TMR2IF = 0; // clear TMR2 int flag

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s