Xtal-locked DDS audio sine generator with just a PIC 18F1320

Discussion in 'The Projects Forum' started by THE_RB, Dec 25, 2011.

  1. THE_RB

    Thread Starter AAC Fanatic!

    Feb 11, 2008
    Hi everybody, here is a PIC-based nice accurate audio sine generator with a good range 5Hz to 20kHz.

    It should be very cheap and easy to build and is a project I was meaning to attempt for a while now (since I posted my "high-accuracy 1kHz sine generator" some months back).

    It is finished and tested. All you need is the PIC, 10MHz xtal and a couple of discretes to make the RC filter.


    The output sine is very nice (due to the 400kHz DDS and PWM), and so is frequency stability especially as you can set the frequency with buttons then it remains very stable.

    My web page;
    has the complete project details and a free HEX file for anyone that wants to build one.

    I'm also appreciative of filter suggestions (like last time!) as this project is probably worth having a nice adjustable low-pass filter to give a really good sine output.

    Merry Christmas! :)

    (Additon 30th Dec) The firmware now has 4 waveforms; Sine, Square, Sawtooth, Triangle. These are optional and only require 2 extra switches.
    Last edited: Dec 30, 2011
  2. crutschow


    Mar 14, 2008
    Looks like a neat generator. Now all you need is a digital readout to show the set frequency.;)
  3. THE_RB

    Thread Starter AAC Fanatic!

    Feb 11, 2008
    Hi Crutschow, actually that's pretty trivial for me to add (with a 16x2 text LCD) but I left it out of this project as that adds a heap of extra build complexity for beginners. It's quite daunting wiring up all the LCD wires and contrast pot etc and takes a project a bit beyond the beginner stage.

    I guess I wanted to release something a bit like a dedicated chip, so they just need a chip and xtal and couple buttons etc and it works. Especially now the signal generator ICs are all becoming obsolete. :)
  4. thatoneguy

    AAC Fanatic!

    Feb 19, 2009
    You say 10Mhz XTAL, but diagram shows 20Mhz :confused:
  5. Wendy


    Mar 24, 2008
    Ever think about posting some of these in the Completed Projects forum?

    We are always getting requests for VCO type circuits, if that chip has a voltage input then it should be able to be programmed to vary over whatever range selected.
  6. nsaspook

    AAC Fanatic!

    Aug 27, 2009
    The 18F1320 is a great PIC chip. If you could also provide a HEX file configured for EC external clock input on your project page that would be great. I have several 10mhz clock modules that are factory trimmed to frequency and having a sync (Fosc/4) source could be useful.
    Last edited: Dec 25, 2011
  7. THE_RB

    Thread Starter AAC Fanatic!

    Feb 11, 2008
    To Thatoneguy; Yes you are right the xtal is actually 10MHz. I later fixed that schematic error and put on my web page but the iage above is the old one, (forum cached) sorry I can't change it.

    To BillMarsden; Hmm, I didn't think of the projects section because I don't often go there. It's a bit dead. :)

    This one is not real suitable for a VCO as the PIC is very busy doing the DDS math and no time left over for reading and processing the ADC input.

    I'll keep in mind the need for a VCO, which should be very easy as a squarewave output (which frees up lots of PIC time compared to a sine). The trouble really is the range? Everyone that wants a VCO has different needs as to what freq it should make for what input voltage they want to give it. I think it would be hard to do a "one project fits all" solution?

    To NSAspook; Thanks for the suggestion! :) You can run it as is by removing the xtal and 22pF caps and injecting your external 10MHz into the PIC OSC1 pin (pin 16). That is the clock input, the OSC2 pin is the clock output. Provided you can inject 10MHz at a few volts p/p into OSC1 the PIC will accept that just fine.

    But I'll still make an "external clock" variation and release a second HEX file, in the next couple of days.
  8. atferrari

    AAC Fanatic!

    Jan 6, 2004
    Where do you buy that 10uF non polarized cap?
  9. THE_RB

    Thread Starter AAC Fanatic!

    Feb 11, 2008
    I have many parts drawers full of every electro cap value, including a few drawers of NP electro values. I assume it was just ordered from a supplier here in Australia (Altronics, Jaycar?), every now and then I check if any drawers look bare and just write some more caps on the parts order.

    to NSAspook; Sorry I forgot to mention before, the project requires the PIC's internal clock PLL to make the internal 40MHz clock from the 10MHz xtal. There is not other config setting to do the job, so I can't make another HEX file.

    However like I described above you can use HSPLL mode with an external clock in signal.
    nsaspook likes this.
  10. MMcLaren

    Well-Known Member

    Feb 14, 2010
    As an assembly language programmer and fellow "cycle counter", I'd just like to point out to members of the forum that what Roman (THE_RB) has accomplished is pretty incredible. Specifically, fitting 32-bit DDS phase accumulator code, sine array (table) lookup, duty cycle update across two registers, and switch press detection, all in a 25 cycle DDS loop is pretty darned amazing.

    I also happen to know that Roman has invested a great deal of time spanning several months researching how to come up with a filter design that represents the best compromise in price and performance.

    Bravo Roman! Thank you.



    Would you mind if I borrowed your filter design and tried to duplicate your project on a little eight pin 12F1822 running at 32 MHz? The challenge for me would be to see how close I can come to your 400 kHz (2.5 us) dds loop timing, which would be 20 cycles at Fosc = 32 MHz.


    Happy Holidays everyone.

    Cheerful regards, Mike
  11. THE_RB

    Thread Starter AAC Fanatic!

    Feb 11, 2008
    Thanks for the nice words Mike! :)

    I'm not sure that I did "months" of the filter design! Re my other 1kHz sine project all the clever filter analysis stuff was done by other people. I just settled on the filter values the old-school way, by patching resistors and caps in circuit and looking at the scope. Same deal with this one, 20 minutes of parts swapping and I said "filter is now good enough!". ;)

    Re the 12F1822, that's a nice PIC! I have not used one before but the enhanced instruction set looks like it will make the job easier. Particularly the add with carry (as I'm sure you know) will help with the 32bit addition.

    Feel free to see what it will do, and post here if you have a good result as it will give people options of which one to build. :)

    20 cycles may be do-able if you use an interrupt for button press. I didn't use ints for that as the int is not a good way to detect button being held down, only the initial button / transition.

    Just keep in mind with 20 cycles your max PWM (vert) resolution comes down to 80 units (maybe less if you want to avoid clipping) so that can cause issues at low frequencies.
    MMcLaren likes this.
  12. MMcLaren

    Well-Known Member

    Feb 14, 2010
    I apologize. I assumed the experience gained while doing research and while working on the filter design for your 1 kHz project had contributed to the filter design for this project.

    Yes, that new instruction is one of many wonderful new instructions and capabilities in the "enhanced mid-range" devices.

    Thank you. I'm thinking about a Serial/USB 12F1822 design and perhaps an 18 pin 16F1827 design that would support serial/usb and/or LCD. USB capability can be had for the cost of a 3.5mm stereo jack on the project board and a $3 Nokia CA-42 usb-to-serial adapter cable clone. Both of those chips are less expensive than the 18F1320, and both designs would eliminate the "blind tuning" problem.

    I did it! I've got a 20 cycle 400 kHz DDS routine! I'm not sure if you could call it "clever" coding but it did take every trick in my arsenal and the invention of one or two new ones as well (grin).

    I don't use interrupts for a button press. The routine will detect either a serial PIR1.RCIF flag or an INTCON.IOCIF flag, but not both. It's a way to detect input and fall out of the DDS loop where I will manage switches (with "repeat" capability) or serial input. As you mentioned over on another forum, your DDS loop is interrupted when handling switches and updating the phase accumulator tuning word, and my code is similar in that respect.

    Do you think I'll have problems going from 100 duty cycle steps down to 80 duty cycle steps? I didn't think it would make too much difference since I've had good results with only 64 duty cycles steps in the past (at a lower and harder to filter DDS frequency).

    Thanks for the great project ideas...

    Cheerful regards, Mike
    Last edited: Dec 27, 2011
  13. THE_RB

    Thread Starter AAC Fanatic!

    Feb 11, 2008
    No you are right of course, this filter was much easier to tune based on experience from the last one. Knowing how much load the PIC outputs would tolerate and roughly how much L to add without affecting the filter linearity too much. :)

    Sounds good, I look forward to seeing what you come up with! I considered making it serial controlled or adding LCD (as I said to Crutschow) but both options put it outside my original design goal of a beginner friendly single chip "sinewave signal generator" project.

    The blind tuning is not really a problem, many beginner level sine wave generators just have a "blind" knob anyway, and in use for testing audio amps freq response, class-B biasing etc people would generally have a 'scope there too.

    Nice work! I see you reduced the 32bit dds down to a 24bit dds, that saves 2 instructions, and I'll guess you tweaked the bit loading for CCP1CON as that would be the obvious place to save a couple more insts. It's a good result Mike! :)

    I'm not sure the reduced vertical resolution is a "problem" you just have to make do with what the chip will do. Watch for clipping or peak distortion caused by PWM breach, really a good sine from PWM needs the PWM to be constrained within about 5% to 95% so the filter ripple does not clip. So that gives you a vert resolution of about 74 units or so. 72 may be better.

    Assuming you tune the filter for roughly mid-range frequencies it will take out all the vertical stepping in mid and high freq bands. But the stepping will remain at low frequencies, on mine frequencies from about 150Hz down all had the vertical stepping apparent. Fixing that would be as easy as switching in a larger cap in the filter when low frequencies are needed, but even without a fix the sine quality is still very high (especially compared to analogue sine ICs).
  14. MMcLaren

    Well-Known Member

    Feb 14, 2010
    The 20 cycle DDS routine is using a full 32 bit phase accumulator, Roman. Based on your descriptions, I believe I've duplicated the functionality of your 400 kHz 32 bit DDS routine, but, I did it using 20% fewer cycles (grin).

    Yes, I optimized the operation for loading CCP1CON with the two least significant duty cycle bits. If you're interested, here's an excerpt of my 256 element const sine-to-duty cycle array which should give you an idea of what I'm doing;

    Code ( (Unknown Language)):
    1. ;******************************************************************
    2. ;  duty cycle bits are placed in each 14 bit array element using
    3. ;  pattern '--765432 xx10xxxx' with CCP1CON register bits placed
    4. ;  in the 'x' bit positions.  This allows writing the CCPR1L and
    5. ;  the CCP1CON registers directly from EEDATH and EEDATL.
    6. ;
    7. sinprep macro   dcy
    8.         dw     (dcy>>2)*256 | (((dcy%4)<<4)|12)
    9.         endm
    11.         org     0x700
    12. sine
    13.         sinprep(039)            ; 000  +0
    14.         sinprep(040)            ; 001  +0.24541229e-1
    15.         sinprep(041)            ; 002  +0.49067674e-1
    16.         sinprep(042)            ; 003  +0.73564564e-1
    17.         sinprep(043)            ; 004  +0.9801714e-1
    18.         sinprep(044)            ; 005  +0.12241068
    19.         sinprep(045)            ; 006  +0.14673047
    20.         sinprep(046)            ; 007  +0.17096189
    Thanks for the input on the 80 step PWM.

    Cheerful regards, Mike
    Last edited: Dec 28, 2011
  15. MMcLaren

    Well-Known Member

    Feb 14, 2010
    Forgive me. I should not have characterized that as a problem. KISS is good and, as you mentioned before, your digital tuning provides much more accurate control compared to what you would get from a similar piece of equipment that only has a coarse tuning knob. In fact, your project kind of opens up the door for a simple companion 6 or 8 digit frequency counter project (grin)...

    All the best, Mike
  16. THE_RB

    Thread Starter AAC Fanatic!

    Feb 11, 2008
    I only spent enough time to get it down to 25 cycles to make the math really neat and get an even 400kHz DDS, and I knew it would be possible to keep it 32bit and still shave off a couple of cycles, but removing 5 cycles is pretty impressive! :)

    You said in the other thread you used a 24bit DDS process? (only 6 cycles needed for the DDS addition) which would have made it possible to get it down to 20 cycles I think with some tuning. But how on earth did you get it down to 20 cycles with a full 32bit DDS process?

    Just a quick add up;
    branch loop = 2
    32bit dds = 8
    table read, loadw+call+retlw = 5
    test input pin (you said no ints) = 1 (total 16)

    which leaves 4 cycles for converting the table value in W into the 2 formats and load the 2 registers CCPR1L and CCP1CON. Even with your trick of having the data in the table in a special ddXXdddd format I still can't see that you would be able to do that in 4 cycles! :eek:

    Come on, 'fess up! ;)
  17. MMcLaren

    Well-Known Member

    Feb 14, 2010
    Only impressive? You won't share some of that "clever" adjective you lavish upon yourself on your project web page (lol)?

    Actually I said something like "I have a 20 cycle routine that matches the performance of your 400 kHz routine". I probably should have said "I've duplicated your 32-bit 400 kHz routine". Later in that same post I said I'd like to try a 24-bit phase accumulator and an 8.388608 MHz crystal, so I can see where I may have contributed to the confusion.
    It's actually eighteen cycles with two 'nop' instructions to fill it out to twenty cycles (grin), and I would be happy to share...

    Yep, the branch loop is 2 cycles and the 32 bit addition is 8 cycles. I believe a table read is 7 cycles, not 5, but I'm not using a table read. My sine array is a 256 element const array of 14-bit memory words which can be loaded into EEDATH and EEDATL in 3 cycles. And, yep, I use 1 cycle to test for a switch press. So the tally is 14 cycles so far.

    I skipped all the WREG wrangling that you're probably doing and simply use 4 cycles to copy EEDATH and EEDATL into CCPR1L and CCP1CON. Tally is 18 cycles. Throw in two "nop" instructions to fill it out to 20 cycles.

    I'd love to see how you did it in your program too, Roman. Anyway, here's my DDS loop which I hope may be of benefit to someone besides just you and me;

    Code ( (Unknown Language)):
    1. ;
    2. ;  20 cycle (2.5 us) 32-bit 400-kHz DDS loop (32 MHz clock)
    3. ;
    4. ddsloop
    5.         movf    phase+0,W       ;                                 |B3
    6.         addwf   accum+0,F       ;                                 |B3
    7.         movf    phase+1,W       ;                                 |B3
    8.         addwfc  accum+1,F       ;                                 |B3
    9.         movf    phase+2,W       ;                                 |B3
    10.         addwfc  accum+2,F       ;                                 |B3
    11.         movf    phase+3,W       ;                                 |B3
    12.         addwfc  EEADRL,F        ; accum+3 (sine array index)      |B3
    13.         bsf     EECON1,RD       ; initiate read operation         |B3
    14.         nop                     ; required nop                    |B3
    15.         nop                     ; required nop                    |B3
    16.         movf    EEDATH,W        ; the b7-b2 duty cycle bits       |B3
    17.         movwf   INDF0           ; CCPR1L = EEDATH                 |B3
    18.         movf    EEDATL,W        ; the b1-b0 duty cycle bits       |B3
    19.         movwf   INDF1           ; CCP1CON = EEDATL                |B3
    20.         nop                     ;                                 |B3
    21.         nop                     ;                                 |B3
    22.         btfss   INTCON,IOCIF    ; switch ioc? yes, skip, else     |B3
    23.         bra     ddsloop         ;                                 |B3
    24. input
  18. jameschristian


    Nov 24, 2011
    Roman, on the page you linked to early on in this post, you mention incorporating an adjustable filter. Where would that go in the circuit?

    Also, I have a limited understanding of assembly code, mostly from what I was taught about the 68HC11 at school, and can barely decipher what you programmed. Can you recommend any good sources for learning more about PIC assembly?

    Edit: found this page for more on pic assembly: http://www.mstracey.btinternet.co.uk/pictutorial/progtut2.htm
    Last edited: Dec 29, 2011
  19. THE_RB

    Thread Starter AAC Fanatic!

    Feb 11, 2008
    Hey "pretty impressive! :)" is a much > compliment than the low level adjective "clever". But I know you're a friend and only teasing. ;)

    I must say that is a very nice use of the 14bit dual-register program memory read using EEADRL. I didn't know the 12F1822 would do that! Bah! Enhanced instruction sets are cheating! ;)

    That is an impressive 16F instruction set addition, it beats the TABLRD on the 18F hands down as that can only read 8bits at a time from program memory so it almost gives the little 12F PIC some of the capabilities of the 16bit DSPics.

    But I have to say the pretty impressive > clever "trick" I liked the most in your code was the way you used the SFR EEADRL as the top byte of the 32bit accumulator. That is so simple as to be elegant. It would also save 2 cycles in my code, as it could be applied to the 18F too. With a couple of other places I know my code could be tweaked it would probably allow it to reduce down to 20 cycles too to give a 500kHz PWM but I'm not sure if there would be any gain in that, and there would definitely be a vertical resolution loss so I think that might be counterproductive.

    I'll nitpick that one because it doesn't "match the performance" it matches the freq but has 20% less vertical resolution. In signal generation that can be significant although I agree it's less ciritcal in this case and for a budget sine signal generator app you came close to matching the performance. ;)
  20. MMcLaren

    Well-Known Member

    Feb 14, 2010
    For the benefit of AAC members who don't know the inside joke. In the past over on the Electro-Tech forum I've accused Roman of elevating self-promotion to an art form. If you read through his web page he's always patting himself on the back and telling us how clever he is, and I admit, deservedly so. But, I still think it's a slightly annoying personality trait and so I tease him about it every once in awhile.

    Thank you for the compliments, Roman. I'll post more musings later, if you don't mind. Right now I've got to run and pickup books for winter semester, which starts on January 9th...

    Happy Holidays everyone.

    Cheerful regards, Mike
    Last edited: Dec 29, 2011