Saturday, March 27, 2021

Atmel 328 based Dirty Digital LFO, written in AVR-C--WORKS!

Hello again, and welcome to the newly downgraded binary mind of AudioDiWHY. This time I'm building a Digital LFO to put some of the Pure C skills I've been learning over the past couple of months to the test. Good news--on the bench this Digital Low Frequency Oscillator--DLFO--works!  

If you want to look at the code, download the github zipped C code here and read on.....

The breadboard DLFO is all software and an Atmel 328P MCU--with a bare minimum of external components.....


Best I can remember, the first synthesizer module I ever "designed" was an analog LFO (read more here). For that I cobbled together circuit fragments found online and experimented with the circuit on the bench. I did this until I had something I liked. 

Now, many years later, I wanted to see if I could do this again but with minimal analog circuitry. And to make things even more challenging, all code for this Digital LFO is written using AVR C.  

I also wanted control voltages to determine the LFO's output frequency, something the analog circuit couldn't do.

The AudioDiWhy analog LFO, circa 2003, with a new front panel.....with SuperD! Livery--thanks Iggy for analog assist!


OK, first up: I needed a development platform. The main MCU used for the project was an 8 bit Atmel 328P; it's reasonably simple to program, well documented, inexpensive, can be found ready to use on a USD$10 Arduino Uno R3 clone (which means not having to wire up a crystal and so on). 

The 328P has 10 bit ADC's already built in; good! I still needed an external DAC, and SPI is easy to use, so I used a Microchip MCP4921. It's 12 bits--that means the waveforms the DLFO creates will have 2^12 or a maximum of 4096 voltage divisions. That resolution I figure will be good enough.

 

A-DiWHY: "Hey Mom! Check out the 4921 it's PDIP! It's SPI!"  Mom: "get a damn job!"

The rest of the development platform I used is the same one I've used for the past few posts: a Windows 10 NUC PC; Atmel Studio 7, and an Atmel Ice programmer. 

UNO to 4921 DAC wiring. For CV analog in, controlling the frequency at output, I used PortC pin 1


But! the 328P doesn't natively support I2S, 44.1K sample rates, or anything like that.  How am I going to make a voltage to low frequency converter out of it?  

Turns out, there are lots of ways.....

First, I needed a steady clock upon which to base the rest of the design. There are 3 timers in the 328P-- 2x 8-bit timers and one 16-bit timer. I was a bit intimidated by AVR timers at first, but after some reading it turns out it's actually not that complex. The  timer can be set up with just a few lines of code--we want to use "CTC mode" for this project--a great web page about AVR timers is here.  

The code fragment I ended up using:


//********TIMER************

// Set the Timer 0 Mode to CTC

TCCR0A |= (1 << WGM01);


// Set the value that you want to count to

OCR0A = 0xFF;


TIMSK0 |= (1 << OCIE0A);    //Interrupt fires when counter matches OCR0A value above.


sei();         //enable interrupts  

// in atmel s7 sei() is flagged as red squiggle due to 

//"intellisence" beautifying,but will still compile. Doh!



    //TCCR0B |= (1 << CS02); DO NOT USE, this turns off clock

TCCR0B |= (1 << CS01);  

TCCR0B &= ~(1 << CS00);

    //********TIMER************

OK what next: we need the timer to signal the LFO to do something.  For that, I needed to use Interrupts. Wait: Interrupts? They always intimidated me while using Arduino sketch--they never made much sense and I followed code examples until they sort of worked--but here, I had to know what I was doing. 

Good news: turns about again, after some reading: not that complex.  A good video about how 328P timers can be used to create interrupts is here; a good web page is here

Each time the 8 bit timer (I used timer 0, but any of the 3 would have worked) I had the code throw a hardware interrupt. That creates the steady heartbeat for the rest of the design.

Now we have a counter that counts upwards until timer0 matches the value in register OCR0A, then, it throws an interrupt and starts the process over. 

Next  I declared a volatile unsigned variable 16 bits wide called "c".  Remember in C you have to think carefully about variable casting!  The interrupt routine is a simple increment statement for this global variable:

ISR (TIMER0_COMPA_vect)  // timer0 overflow interrupt

{

c++;

}

(There is something strangely poetic about literally using c++ in a program written entirely in C right?  Wait, I need more fumes....)

OK we now have a timer that creates an interrupt on a regular basis, incrementing a global variable--what's next?  

Lots of ways to go here, the most common is to a create a values that represent the waveform, then walk the array and send each value to a DAC.  But I decided to go a different route: a cheap and dirty (sounding) way to control frequency from CV is to allow a certain number of interrupts to pass before incrementing or decrementing a value to be sent to the DAC.  If I wanted the waveform to increment from say 2000 to 2001, I write the 12 bit value for 2001 to the 4921 after waiting for a certain amount of interrupts to occur.  More interrupts means a lower frequencly waveform at output, less interrupts means a higher frequency. 

That means: for a square wave, I can send 0xFFF for on and 0x000 for off after some number of interrupts pass. For a ramp, increment by one, and then when 4096 is reached, start over at zero.  For a triangle, count up then down again.

If you're still trying to figure out my approach, let's look more closely at an example--let's create a ramp wave. 

For the first 12 interrupts of a ramp wave looks like this:


By incrementing the global variable c by 1, 4096 times, we can get a pretty decent ramp wave.


And in general the scheme for determining frequency at output works like this:

....let interrupts pass, then take action in code to send data to a DAC. Here we see a really simple triangle wave being created with a few bits and a few interrupts. In reality, we have 4096 steps along the Y axis and time, in the form of our interrupt "heatbeat", exists along the X axis. 

A problem with this methodology: CV control natively operates "backwards"--a CV ADC reading of 1023  says "let 1023 interrupts pass before incrementing the DAC value" which means 5V at CV in produces the slowest LFO frequency.  That's not what we expect from a run of the mill audio synthesizer LFO so that needs to be fixed.  

Again, lots of ways to fix this--I could have used an external op amp inverter for CV instance, but trying to do this all in software I subtracted 1024 from the value read at the ADC input--easy!:

uint16_t rate = 0;

rate = 1024-freq;

That was really simple, and worked.....now CV fully CCW is slowest and CV "fully CW" is fastest.

Next, let's get some output going.....after studying the 3921 datasheet I created a simple .h and .c file to control the DAC to do the heavy lifting at output. Find that in the github zip, here. With all this, all sorts of waveforms can be generated, and I can reuse the 4921.c and .h files in other projects--cool!.

The final part of this is to control the maximum frequency the 5V CV generates.  This took some trial and error and going over and over the math, but eventually I found an unexpected trick: I can add to the value sent to the DAC to increment the maximum speed at the expense of the waveform's resolution.  There are limits here of course--I can run the triangle wave so fast that it starts to distort because too much data is dropped--but overall this software kludge (?) worked, and for the square wave, the value of this "add" has the added benefit of determining the maximum frequency to about 5%.

count = count + WFMFREQ;

uint8_t CMSB = count >> 8;

uint8_t CLSB = count & 0xFF;

write4921(CMSB,CLSB);

OK, so what's next?  I need to create clamped hardware buffers for CV to not fry anything inside or outside this circuit.  I will also probably tie the "rate" value WFMFREQ to a pot or switch. Update--no, I solved this a different way, post for final software build is coming soon.

In the meantime, the DLFO in its current baby-step form produces decent waveforms--I have created functions for Tri, square, and ramp, and creating more--saw, random, expo, etc., should be pretty easy.







the problem--or strength--of this design is that for some waveforms at some freqencies you end up with very few sample points. So I expect the output of this LFO to be "gritty", but for now, that's OK.  Maybe just stop here--do I really need more LFO's?  Not sure. These last 10 days or so was an exceptionally good learning experience and now perhaps I am no longer a full "C newbie". Getting another stupid LFO to work in my rack almost doesn't matter at this point.  Update 4-8-21: right, but I had to see this through. PCBs designed and off for fab. Assuming I have a morning or two, I will build it to see how this design can be further refined.  UPDATE!  embedded C  DIRT-LFO works!  Post is here

OK enough C for one day. If you want to stretch out your brain a bit, maybe you want to put Sketch aside and try something like this. Leave the fumes aside for a week? For me anyway, it was a lot of fun. AHOLA!





Monday, March 8, 2021

AD9833VCO: Putting C to work!

Last time we looked at libraries, written in C, to assist in the difficult transition from Arduino Sketch programming language and IDE to using purely C for coding the microcontrollers in our audio projects. 

This time, let's use this new (for me anyway) methodology to create a low cost, low parts count audio voltage controlled oscillator ("VCO") using Analog Device's AD9833 and an Arduino Uno.  

DiWHY: Like so many Audio DiY'ers, I am a huge fan of Arduino and its super easy Sketch Language.  But why make things easy?  I'd like to know more about the programming languages used to write sketch itself--C and C++--and hopefully gain a deeper understanding of how embedded systems work. 

History-onics: To understand this post you may want to skim my first post about using C instead of Sketch, here; related posts include creating a development platform and setting up an UNO for C programming, here, and writing/cobbling together C libraries for communication (SPI, I2C to name two): here.

All hail!! Dennis Ritchie, the father of C. Did he know that someday C would be a dominant language for embedded systems? 


As far as C programming: no way around it, you have to learn it at least to an intermediate level if you really want to stop programming in sketch and use Pure C.  

But if I can do it anyone can. Covid? No commute! I have time! 

I took an online coarse (C++ although I am coding here in C) and did a lot of reading; for me, C required more forethought and attention to detail when creating working code vs. languages without type casting like PHP.  

But when writing programs for MPUs like the ATTINY85, with its limited memory and relatively scant on-board peripherals, maybe programming discipline is a good thing. 

Indeed, after working with C a bit, I can see why its brevity and modularity make it a good fit for embedded systems and a top choice for professionals. And having to think carefully about variables before using them kept me honest while coding, which isn't the worst thing. It's all good--whatever works for you? read more here.

For this proof of concept I am using an Atmel 328P and its built in 10 bit ADC peripheral to read control voltages.  The MCU then uses the SPI protocol to change the output frequency of an Analog Devices 9833 function generator IC. For convenience the Atmel MPU is on an Arduino UNO R3 but it could have almost as easily been on a breadboard with minimal support components.


AD9833 breakout board--3 waveforms, tops out at about 12Mhz, frequency accurate to 28 bits--$7USD or so; are you kidding me??


The AD9833 is a popular audio IC, and most of the AD9833 projects online use the Arduino sketch language and a preexisting Arduino library like the one here to build a 9833 function generator. 

I also foun a MIDI controlled AD9833 VCO (post here--but I don't think you can call that a VCO? This is a MIDI-CO? DCO. Whatever you want to call it, it is Cool!)  

Without doubt, there is a large number of Arduino AD9833 audio projects already in existence.

Let's do this but without Sketch....

Good news, I got this to work!

 The UNO's analog input uses an ADC C library, here;  the MCU to AD9833 IC uses a SPI C library (here). 

If you want to make this work on the bench, I recommend getting an AD9833 breakout board datasheet here--a breakout board will save time soldering tiny SMD parts.  

As far as the code: main.c calls all of it. It's all on github, here.

For debugging I used the UNO's USB hooked up to the Windows 10 system using the printf library (here) and the free Windows terminal program "tera term".

To review: the development platform used:

  • Atmel Studio7, get that here.
  • Atmel ICE programmer, information here.
  • An Arduino Uno clone (I could have used a Atmega328P chip on a breadboard, but I had some clone Unos so it seemed easiest to just use that....) 
  • An analog devices AD9833 breakout board.

Hardware used to test (on a breadboard, and I am terrible at breadboarding, but this is really easy) is really simple:    

On the bench, looks like this:

No, this VCO isn't 1V/octave and can't go from 0 to 60 in 2.3 seconds.  Maybe later.  I am just seeing if I can get the basics to work. 


The built-in Analog to digital converter on an UNO is 10 bit (0-1023; maps from 0V to 5V at input); in this proof of concept maps the ADC value to frequency, so the VCO goes from about 1hz to about 1K, here is a snippet of that:

     while (1) 

    {     

    CV = analogRead10bit();

     adfreq = get_ad_freq(CV);

             adfreq = adfreq + 0x4000;

             LSB_L = (adfreq & 0xFF00) >> 8;

     LSB_R = adfreq & 0x00FF;

 

      SPI_TransferTx16(LSB_L,LSB_R); //freq LSB (L,R)adjust freq slightly  

      SPI_TransferTx16(0x40,0x00); // freq MSB (L,R) // adjust freq greatly  

      SPI_TransferTx16(192,0);  //phase val 0

      SPI_TransferTx16(32,0); // exit reset mode  


}

}

C-CODE: I've added a github repository, "AD9833POC", where I capture all the code used so far. Get that here

I also created a short Python program that allows you to extract the 16 bit LSB freq0 register setting in hex from your desired frequency--I found this code very useful while debugging; add 0x4000 to the output and it's ready to be fed into the AD9833:

def frequency(freq):

a = (freq * 268435456) / 25000000
b = int(a)
c = hex(b)
print(b)
print(c)
frequency(535)

Going forward, I can see a lot ways to have fun with this core VCO: for instance, make 4-6 of this simple circuit for some sort of FM based patchable tone generator?  I also found that if the CV is left floating, it makes a pretty crazy random low frequency tone generator....might be fun to see what can be done with that....and I see a few things on the web about using the AD9833 as an LFO. Before this goes into your rack you'll need to buffer the CV input and AD9833 output, and also boost the amplitude of the AD9833's sine and triangle waveforms since they are like 0 to 1.5V P/P, but that's easy enough, it's a few op amps and resistors.

Further thoughts: a 14 bit ADC would do increase the frequency response to 16Khz or so, without having to modify the code a lot, since everything would stay in the LSB frequency0 register for the 9833.  Yes, you have to dig into the AD9833 datasheet a lot to know what that means....  but as a proof of concept this is good enough. I have so many projects waiting around right now, now sure what I'll try next!  

C-ya later? overall these "pure C" posts/learnings have taken me a long way....

Getting this far, in terms of setting up the AVR C environment, learning the C language, cobbling together the libraries, etc., has been a lot of work. No doubt: Arduino audio projects are almost always easier and quicker to craft and debug vs. a project purely written in C. 

No harm in going to back to Sketch..... 

But after I got over the steep part of this it became a bit easier...the optimized C code runs fast and takes up almost no processor memory-- I can read Arduino libraries that used to seem like black boxes and pretty much understand how they work--I feel good about this, and I see myself coding more in C in the coming weeks and months. 

And.....do I dare say it? Someone has to say it: Solving programming puzzles in C is super frustratingly fun!

This has helped me with my day job as well, since now I can read open source C and C++ programs that have nothing to do with audio and try to understand what's going on there as well. Who would have thought? 

 

A guy OK with C tries to learn C++. Bjane me Up, Stroustruppy!

Why no posts so far for 3-2024?  I have been woodshedding, brushing up on C++ skills.  What got me to finally start digging into C++? I was ...