Monday, August 20, 2012

To condense fact from the vapor of nuance

My first challenge was capturing the audio signal using the analog to digital converter running on lm4f120xl controller.  A quick bit of datasheet reading told me that there are two hardware limitations to the ADC that I have to worry about.  For one, the ADC expects a signal range of 0 to 3.3V.  Any voltages that dip below ground cause problems or are ignored.  The second limitation is that the ADC uses a switched capacitor array to implement its successive approximation register.  As a result, we have to have a low impedance signal feeding the ADC.

I talked with a few hardware knowledgeable friends of mine about this, and eventually came up with the following circuit to handle the above constraints:
The idea here is that the resistors will bias any input signal to between the ADC's maximum input voltage and ground.  The opamp will then amplify the signal enough to easily charge the switched capacitor array, giving us a good enough signal for our ADC to work with.  I dusted off my wiring kit, made a quick trip to Fry's, fired up the oscilloscope, and poked around enough to get myself confident that my line level input (courtesy of a male to male 1/8 inch audio cable, a stereo headphone jack, and my laptop's audio out) was being properly biased and conditioned.

I couldn't find a rail-to-rail opamp at Fry's, so I used this little number instead, which has a maximum output of Vcc-1.7.  Fortunately, I had a 5V bus I could tie into.

Hardware being handled, it was time for software!

I had played around with the ADCs on an lm4f232 a bit, so it didn't take much to get me to a point where I could capture the signal and print its level in Volts on one of the UARTs.  I figured there was no need to get fancy (yet), so I just used sequencer 3 of the ADC (which is limited to a one sample FIFO) to capture based on a software trigger.  Throw it in a while loop and UARTprintf ad nauseum.

Next was figuring out how to set up a controlled sampling frequency.  My first thought was to set up a continuous timer to countdown at 44.6 kHz (for some reason I picked 44.6 instead of 44.1... friends don't let friends drink and divide), throwing an interrupt every time it hit 0.  I was poking through the driverlib api for the timer getting this set up when I stumbled across the TimerControlTrigger function, which according to the documentation "Enables or disables the ADC trigger output."  It turns out that the hardware is already capable of doing what I had planned on doing through software: use a timer to trigger a capture in the ADC module.  Happy day!

A bit of debugging, a bit of re-reading, and another night of playing around with the launchpad got me to the point where I could use a timer to trigger an ADC capture, then on the ADC interrupt move the data into a sample array, then use a second timer to print out that array once per second.  The data looked good, though I couldn't think of a good way to verify that I was capturing with the expected sampling rate.

A normal person might have called it a day at that, but I long ago decided that the kind of person who spends his weekends and free nights messing about with microcontrollers is a far cry from a normal person.  The method of having the ADC interrupt copy the sample from the FIFO to the sample array reeked of inefficiency, especially when the documentation for the uDMA engine was tantalizingly sitting in the periphery of my vision.

For those not familiar with it, a uDMA engine can automatically move data from one place in memory to another, without needing the main processor to intervene.  As a result, you can do things like copying data from a peripheral's register map directly into a variable that's been allocated somewhere on your stack, which happens to be exactly what I wanted to do.

More datasheet reading, searching through old forum posts, and snooping around random example code led me to the confident belief that I could use the sequencer capture complete signal to trigger a uDMA transfer at the hardware level that would leave me with a much more efficient system.  My goal was to have a single "go" bit hit, then receive an interrupt SAMPLE_SIZE/44600 seconds later (44,600 samples/sec, SAMPLE_SIZE samples per signal processing loop), at which time the data would all be nicely arranged in my buffer.  Another night spend coding, and I was able to realize this goal.  The only limiting factor was that the uDMA engine has a max transaction limit of 1024, meaning that I would have to re-initiate the uDMA transfer after each 1024 samples until my buffer was full.  Irritating, but it only took a few lines of code, and was easily managed inside the interrupt handler.

After all of that, which took me about a week and half to get through (one weekend on the hardware, about another week on the software), I was very efficiently capturing audio data at an easily configured sampling frequency.  Woo and hoo!

In summation, my signal chain at this point was:
Timer0 triggers ADC0 capture; ADC0 capture is fed by custom circuit; ADC0 capture complete triggers uDMA transfer; uDMA transfer triggers interrupt, at which point our signal has been captured and moved into our sample buffer.

No comments:

Post a Comment