This text repeats my blog post on the subject.  But I wanted to repeat it here so I can also put links to my source code and show some other space grabbing info -

The blog post:

This is one of those “because I can” projects.  Can I generate a sine wave using a simple PIC employing no PWM and no DAC, no software timers or interrupts, just turning an output pin on and off?  We all know it can be done because our music players boast of the 1-bit DACs inside. 

Simplistically, we know if our output pin puts out 5 volts when ON and 0 volts when OFF, a 50% duty cycle should give us an average output voltage of 2.5.  But the actual method is a little cleverer than that, which is what makes it fun. I picked it up from Glen Leinweber, VE3DNL, a great ham tinkerer in the realms of RF and programming.

 The sequence of ones and zeroes is first calculated “off line” using a simple program in a high level language, QBASIC in my case.  Variables track the integral under the sine (actually cosine in my case) curve and the integrated value of all the bits generated previously.  If the value of the integrated bits is less than that of the cosine curve, the next bit is made a 1 and if it is more, the next bit is a 0.  In this way, the average value of the bit stream is tracking the value of the cosine wave as closely as possible.  Glen says this technique is an example of something called “sigma-delta coding”.

 There are some practical considerations related to our MCU chip, a 12F629 in my case.  My processor is running at 1 MHz and it can toggle my output pin in one execution cycle or 1 microsecond.  Naturally, I want my bits as narrow as possible for fine resolution.  The limitation is on the amount of program memory I have, which is 1024 bytes for the 12F629.  The program will simply output all the bits for one cycle and then repeat.  So the period of the cycle determines the program’s length.  Say I wanted a 1,000 Hz output.  The period is 1 millisecond, so I’d need roughly 1,000 instructions plus a little overhead.  You can see that lower pitched tones require more memory as their period is longer.

 I wanted to do an “A” musical note at 440 Hz but it wouldn’t fit so I wound up at “D” (587 Hz).  Even this wouldn’t fit without a little trickery since its period is 1,703 microseconds.  Turns out that there are long streams of 1s or 0s where the sine wave is at its positive and negative peaks.  So I can save memory by calling delay routines repetitively.  Actually, one routine with multiple entry points is even better.

Want to write a ~1000 line program by staring at a printout of 1,703 ones and zeros and writing the code to produce them?  Me neither.  So my QBASIC program also gets to write the bulk of the source code. After the bit sequence is stored in an array, the program then examines it for repeats and writes the code to implement the bit sequence efficiently.

I also wanted a way to turn the output on or off in response to user input (telegraph key?) on another pin.  To do this, I just wrote the code to sense the pin and if its state tells me to turn the sound off, I configure the output pin to be an input instead of an output. Each time through the code, it reads the control pin and configures the output pin accordingly. The sound generating code continues to run as always.  I had to count the number of cycles this code took and then insert it in place of an equal number of “time wasting” cycles in an area where the output pin is not changing.

OK, say I get it running.  How do I look at the output and verify that I’ve achieved my goal?  If I just look at the output pin with my o’scope, I’ll see a pulse train of varying density.  So just as I did in software, I have to integrate the pulse train in hardware by connecting a resistor and capacitor to the pin.  Here’s the tricky part:  the R/C network is a low pass filter, and with enough filtering even a square wave can be turned into a perfect sine.  So to make sure I’m not “cheating”, I make sure the corner frequency of my filter is much higher than my fundamental frequency.  The proof is in the pictures, with a shot of several cycles showing a nice sine wave, but a close-in zoom of part of the cycles show the jaggies caused by the individual bit transitions.

A fun variation might be to do a more complex waveform – say the sum of sine waves of 1,000 Hz and 1,500 Hz.  The period would be that of the difference frequency, 500 Hz and the resultant would be something you couldn’t fake with low pass filtering.  Combining two musically related notes would be even better – easier on the ears.

One more relevant bit of info.  Most PICs these days include high speed and fairly accurate oscillators you can use and save the price of a crystal and two I/O pins.  The thing is pretty accurate, but has frequency trimming registers if you want to get closer.  My ‘D’ note started off 8 Hz low with the factory value and I was able to trim it to within 1 Hz.

What’s it good for?  Don’t you hate that question?  But I guess it’s a stable audio sine wave source for the price ($2 or so?) of a simple PIC chip.  Not too flexible though – to change the frequency you have to re-run your QBASIC program to generate revised source code and then re-program the chip.

 Now the links to the code.  pic-sine.asm is the PIC assembler code.  1bitsine.bas is the QBASIC code that was used to generate the tedious part of the source code.

Oh, here's the link to the blog.  It's got a couple photos of the waveforms.  You may have to click the October 2009 entry to see it ...

http://wa5bdu.blogspot.com/

pic-sine.asm

1bitsine.bas

Oh yeah, I thought I'd show you what the bit pattern produced by 1bitsine.bas looks like:

11111111111111111111111111111111111111111111111111
11111111111111111111111111111111111111111111111111
11111110111111111111111111111111111011111111111111
11111011111111111111101111111111110111111111110111
11111101111111110111111110111111101111111011111101
11111011111011111101111011111011111011110111101111
01111011110111011110111011110111011101110111011011
10111011011101101110110111011011011011011011011011
01101101101101101011011010110110101101011010110101
10101011010101101010110101010101011010101010101010
10101010101010101010101010100101010101001010101001
01010010101001010010100101001010010010100100100101
00100100100100100100100100100100010010010010001001
00010001001000100010001000100010001000100001000100
00100001000100001000001000010000010000100000010000
01000001000000100000001000000010000000100000000100
00000001000000000100000000000010000000000000100000
00000000001000000000000000000001000000000000000000
00000000000001000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000
00000000000000000000000000000100000000000000000000
00000000000000000000000000001000000000000000000000
00010000000000000000001000000000000001000000000000
10000000000100000000010000000001000000010000000100
00000100000010000001000001000000100001000001000001
00001000010000100001000100001000100001000100010001
00010001000100010010001001000100100010010010010010
00100100100100100101001001001001010010010100101001
00101010010100101001010100101010100101010101001010
10101010101010101010101010101010101010101101010101
01101010101101010110101101010110101101011011010110
11010110110110101101101101101101101101101110110110
11011101101110110111011101101110111011101110111101
11011101111011101111011110111101111101111011111011
11011111101111101111110111111011111101111111011111
11101111111101111111110111111111101111111111101111
11111111110111111111111111101111111111111111111111
01111111111111111111111111111111111111011111111111
11111111111111111111111111111111111111111111111111
11111111111111111111111111

 Interesting, huh?