Synthesizer LFO / Function Generator

Building an arbitrary waveform generator to serve as an LFO in a Eurorack synthesizer

Similar projects worth following
I was having trouble building a satisfactory analog LFO from available components when I realized a microcontroller with a DAC would simplify things a whole lot.

Project Objectives

  • Generate Sine, Triangle, Sawtooth, and Square waves from 0.1 Hz to 1 KHz
  • Output unbiased AC signals 20vp-p
  • Compatible with Eurorack power supply
  • Control frequency with Eurorack compatible CV
  • Synchronize with Eurorack gating signal

  • DOOM Noises

    Steven Gann08/26/2022 at 23:29 0 comments

    Now that I have things where they should be, I made the ADC input adjust the frequency of the signal by introducing an a small delay in the main loop. The DAC is still updated at the same rate and the ADC is still read at the same rate, but the logic that advances the DAC along the selected LUT is slowed to produce the desired frequency.

    While I was at it, I did some cleaning up, removing unused code from earlier experiments, and added a couple new output functions. The first new function is a new LUT copied from the DOOM II source code. DOOM's RNG used a 256-byte LUT where the index would advance every time the anything in the game needed a "random" number. Used as a wavetable the resulting noise is a bit repetitive but it has a certain quality I like. The other new function also uses the DOOM LUT, but adds a 16-bit cycle counter and an XOR so the noise isn't so repetitive.

    These new functions, and the rest of the code, can be found in this commit on GitHub.

    Speaking of noise, I fed the DAC's output to a JBL powered speaker to see how it sounds. I know these speakers have a lot of DSP to clean up the signal, but the waveforms really do sound pretty great. The Sine wave has a high-frequency harmonic that only comes out when the frequency is very low, but otherwise things sound acceptable and maybe a simple low-pass filter will be enough to deal with that. After all, this is an LFO not a VCO, so these signals aren't intended for listening anyway!

    Another issue I noticed from monitoring the scope while listening on the speaker was that the DAC's output gets some noticeable distortion when the speaker is connected, I assume from some capacitive loading. I'll have to make sure the output is sufficiently buffered.

  • Of ISRs and DMA

    Steven Gann08/26/2022 at 05:35 0 comments

    So what do you do when your main loop gets in the way of running a specific function fast and regularly? You turn that function into an ISR triggered by a timer interrupt, of course!


    I moved the LUT code into the TMR0 ISR and set that timer to go about as fast as possible. This didn't quite work because it seems the interrupt was tripping again before the ISR actually finished. No problem, I'll just pause the timer and disable interrupts at the start of the ISR and put them back at the end. Timekeeping is a little off but at least it is regular.

    But... now the logic in the main loop doesn't seem to be running, or at least it is running very, very slowly. Why? Well when the ISR is running the main loop is paused and resumes when the ISR finishes, but this ISR is taking long enough that it usually triggers again almost immediately after returning. The main loop includes a NOP-based delay loop for simple button debouncing, so a loop that previously took 2ms to iterate now takes several minutes. I could slow down the TMR0 period so the ISR triggers less often, but if I slow it down enough to make a real difference my DAC update rate is WAY too slow. So maybe I can make the ISR faster? Why is it slow? Within the ISR I check a "mode" variable with an IF statement, then copy the value from the appropriate LUT array into the DAC's output register. And there's where it hangs. When I write directly to the DAC out register the CPU appears to hang out on that instruction for the DAC's settling time, way too long. If I read the ADC's input register it takes even longer!

     I need to eliminate this, but how?

    Let's try DMA

    Confession time: I've never used DMA before. I've read about it, I understand it in theory, but never actually used it in a project. The principle is pretty simple, a peripheral in the MCU is triggered by something and copies data from one region of memory to another, and that memory can be RAM or a peripheral's register. It is the closest thing you can get to async programming on a single-core processor.

    So I already have TMR0 configured to be nice and fast, so I set up DMA channel 0 to copy a single byte from a general purpose register (GPR) to the DAC's output register every time TMR0's interrupt triggers. I removed my TMR0 ISR and moved the LUT logic back into the main loop. Incredibly, I can let the main loop run as fast as it can and the DAC output keep s up without slowing it down! Well, not to slow. If I speed up TMR0 enough the main loop starts running noticeably slower so there's a tiny bit of overhead whenever the DMA is triggered, but an order of magnitude less than before.

    Next step is reading the ADC. This time I configured DMA channel 1 to copy two bytes from the ADC's input register to a GPR. The ADC takes a bit longer than the DAC, and frankly I don't need to read the ADC nearly as often as the DAC. Every 100ms is plenty fast! I configured TMR1 to trigger every 100ms and now I had zero-overhead ADC reads!

    This is a pretty big step forward. For testing, I set the ADC's value to adjust the rate of advancing the LUT's index for a primitive frequency control. I am able to hit over 5kHz before the sine wave is too aliased, 500% of my target frequency!

    Next step is going to be the analog parts, figuring out how to interface a 3.3v MCU to Eurorack voltage levels.

  • Basic Firmware Testing

    Steven Gann07/05/2022 at 20:49 0 comments

    Setting aside the analog concerns for now, I need to see if the MCU I'm using is even able to generate signals fast enough for my needs.

    To keep things fast, I decided to construct lookup tables (LUTs) for each function I need to generate, starting with the Sine wave since it is the most complex. I put together a spreadsheet in LibreOffice Calc that calculated a table of 256 8-bit values that map from 0 to 2pi radians, and a graph as a sanity check to make sure the values looked like a proper sine wave. I then copied the values into a header file in MPLAB and formatted them into an array of uint8_t. While I was at it, I repeated the process with sawtooth and triangle waves.

    With the Sine LUT prepared, I put together a very simple program to iterate over the LUT and push the values to the DAC. In the program's main loop I increment a uint8_t and use it as the index to grab a value from the LUT and push it to the DAC's value register, which should be as fast as physically possible. Without compiler optimizations enabled, I was able to get a fairly clean 5.24kHz. To get this sort of speed I'm using the PIC's internal oscillator with a PLL to push the clock even higher, which in theory might introduce a little jitter, but I'm not detecting any on my scope. 5kHz is pretty far outside of the frequency range I need so I should have a bit of time budget to do other things. There is a little aliasing in the DAC output, but a passive low-pass filter on the output will more than compensate for it.

    I proceeded to add the remaining function LUTs and a bit of logic to switch between them with the button on the Curiosity Nano I'm using. For reverse sawtooth I use the same LUT as sawtooth but subtract the index from 255, and for square wave I have no LUT and instead check the most-significant bit of the index variable and write either 255 or 0 to the DAC.

    Once this logic was implemented I ran into the first potential obstacle for this project. With just a couple of If statements and checking an I/O pin every iteration, the maximum frequency has dropped down to 3.46kHz, which is still fast enough but a significant enough decrease that adding more functionality may require careful planning and optimization.

  • Background and Planning

    Steven Gann06/23/2022 at 23:37 0 comments

    I've been slowly building a basic synthesizer rig from scratch, and one thing I've been needing is a module that can generate different waveforms at low frequencies to provide dynamic CV signals to other modules like VCFs, VCAs, etc. In a more traditional synthesizer rack this would be provided by an LFO module. There's many LFO module schematics around the Internet, but a lot of them either have serious design flaws like temperature sensitivity or distortion outside of specific frequency ranges, or they depend on expensive or hard-to-find components like temperature-compensated transistors, transconductance amplifiers, etc.

    After building a few simpler analog LFO designs, including one op-amp based and one using 555s, I had the idea to use a microcontroller with a DAC and ADC. DSP for audio is pretty hard to do on simpler MCUs, but generating a low-frequency waveform should be achievable!

    I grabbed a PIC18 Curiosity that I already had and wrote up a simple program to see how fast I could push a 256-sample sine wave out the 8-bit DAC and after a little experimenting I was able to get a fairly clean sine wave of about 1KHz. There's a tiny amount of aliasing but it is barely noticeable and I can probably clean it up later with a low-pass filter if I really need to. Since it's generation CV signals and not actual audio, it should be just fine!

    Now that I've confirmed the MCU I have handy will meet my needs, this leaves just a couple other challenges:

    1. The DAC's output is 0v to 3.3v, but CVs need to be -10v to 10v, adjustable since some modules like CVs as small as +/-2.5v for 1v/octave
    2. I'd like another CV (like from a sequencer or MIDI-CV module) to control the frequency, but the ADC is similarly limited to 0v to 3.3v and CVs can be as large as +/-12v
    3. I want to the CV input and output to be as robust as possible against overvoltage, shorts, etc.
    4. I want to use cheap, basic components I already have on hand since the Global Chip Shortage has made more specific components hard to get, so this means passives, discrete diodes and transistors, and op-amp

View all 4 project logs

Enjoy this project?



Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates