Saturday, December 15, 2012

The Writeup 2: Electric Boogaloo


Hello everyone!  I have some new, very exciting updates to my project!

For those who aren't familiar with my previous works, my last big project was a frequency analyzer I made using a Stellaris Launchpad and an 8x8 LED panel MSP430 Launchpad booster pack that I repurposed for Stellaris.

Well, I've made a few upgrades since then :)








Project Overview

  • Timer triggered ADC for signal capture at user specified frequency (4 KHz - 80 KHz, defaults to 26 KHz)
  • Smart uDMA function determines, based on sampling frequency, whether to do signal processing on a fresh array of new data or rotate in a smaller amount of new data for each DSP loop
  • 2048 point FFT for frequency bin calculation
  • ARM CMSIS DSP library used for Hamming Window application and FFT calculation
  • Touchscreen used for variable minimum display frequency, maximum display frequency, sampling frequency, number of display elements on screen (aka number of bars), debug information, and visual effects
  • Dynamic calculations made based on current sampling frequency and max/min display frequencies to generate a logarithmic scale for frequency axis.
  • Timer controlled 18 FPS display rate (adjustable to up to 30 FPS at compile time)

Hardware

Audio Capture

The first big update to my project is on the signal capture side.  I've always been much more of a software guy, but I figured this would be an excellent opportunity to brush the rust off of my board layout skillset.

In order to get the Launchpad to be able to read line level audio, it is necessary to add a DC bias to the audio signal.  Line level audio is centered at ground and has a ~1.6 V swing, which is a problem because the ADC peripherals on the Launchpad can only take input signals between 3.3 V and ground.  To fix this, I use a simple circuit (a capacitor and two resistors) to get the signal centered at 1.65 V with a 1.6 V swing.


Instead of using a breadboard for this, though, I decided to draw up the schematic using Eagle's schematic capture tool, then do a simple board layout (again using Eagle).


Once the board layout was done, I uploaded the schematic to Batch PCB.  I was originally planning on ordering a few boards from there, but one of my coworkers found out about what I was doing and offered to pay for my board to be fabricated by Advanced Circuits instead.  As much as I like supporting batch PCB, I'm not one to turn down free fabrication :)







Overall, I was very happy with how the board ended up turning out.  For anyone interested in using/modifying/viewing my raw board design files, they can be found along with the bill of materials for my board in the hardware directory of my github.

Display

The next big change, and arguably the most impressive addition to my project, was the display.  Kentec just recently released a 3.5 inch, 320 x 240 16 bit color display, complete with resistive touchscreen overlay on a booster pack for about $50.  Needless to say, as soon as I found out about its existence, I incessantly pestered my contact on the Stellaris applications team until he let me have one to play with.  I had to make some changes to the sample driver provided for the display.  Specifically, the touch screen uses a timer to trigger ADC captures, which interfered with my audio capture functions.  With a few modifications, though, I was able to integrate the Kentec booster pack into my project pretty easily.

Software

The addition of the touch screen allowed for a much, much wider scope of functionality for my software.  I had originally designed my code such that the sampling frequency and display parameters were easy to change at compile time via a few pre-processor macros, but it took a bit of work to get those options all configurable at run time in an intuitive fashion.  That said, the software I wrote for the LMF120 is now responsible for the following functions:
  • ADC Sampling at a user defined frequency frequency
  • Digital Signal Processing on the captured audio data (2048 point FFT)
  • GPIO based communication with the Kentec display
  • Graphical User Interface for changing display and DSP parameters at runtime
I'm a software developer by day, so I did all that I could to make those steps run as efficiently as possible.  I worked primarily in Code Composer Studio, and my source code is available on github.

Audio Capture

For the ADC interaction, I ended up using three separate peripherals to give me an incredibly software efficient audio capture process.  First, I set up a timer to count down at the sampling frequency (defaulted to 26 KHz).  I set up the timer to trigger an ADC capture at the hardware level.  The ADC capture complete signal was set up to initiate a uDMA transfer, which was configured to move the data from the ADC capture FIFO to a global array of samples.  The result is very, very software efficient; One line of code starts the sampling timer, and SAMPLE_SIZE/26000 seconds later a software interrupt occurs, letting you know that the data has been captured and is ready in the sample array.  I was very proud of the efficiency of this process :-)

I did encounter some difficulty when trying to run my code at low sampling frequencies.  The flow of the above method is basically
  1. Capture Data
  2. Halt Capture
  3. Process Data
  4. Start Next Capture
  5. Display Data
The problem with this becomes obvious in the example of using 4 KHz for the sampling frequency.  The 2048 point FFT requires the use of 2048 samples, and the above method means that every time we want to do a new signal processing loop, we have to get a fresh 2048 samples.  If we're sampling at 4 KHz, that means we have at most about 2 sets of fresh samples per second to process.  That means we can only update our display at 2 frames per second.
To get around this, I have two different uDMA algorithms.  The first (fast) method is as described above.  The second (slow) method decreases the uDMA size to 256 and uses a ping pong buffer to store the data.  When the uDMA transfer for the ping buffer is complete, the uDMA engine switches to storing data in the poing buffer.  While the pong buffer is filling, the data in the sample array is shifted left by 1792 samples, then the data from the ping buffer is copied into the top 256 spots of the sample array.  The joys of flexibility :)

Digital Signal Processing

I am fortunate in that I've been playing around with audio hardware for years in the form of running soundboards and messing around with studio mixing and recording.  My last DSP class was a long, long time ago, but my interest in audio gave me a decent foundation in audio based DSP.  I am also fortunate in that ARM has a DSP library specifically designed to take advantage of the M4F's floating point instructions.  TI even has an application note detailing how to build that DSP library in Code Composer Studio.  From what I've heard, the amazingly handsome, charismatic, genius of an engineer who wrote the note did an incredible job on it, and it is wonderfully thorough and well written.  It might also be a little out of date (as ARM released a new version of CMSIS since the app note was published), but it's still quite useful.

With those tools at my disposal, the DSP portion of my code wasn't too difficult to figure out once I wrapped my head around CMSIS.  Step one is to multiply the samples by a windowing function.  I chose a hamming window, because I vaguely remember hearing that that was good for audio.  Next, I take the FFT of the input data.  I'm pretty proud of this part as well; the M4F ended up being powerful enough to take a 2048 point fft on the input data, which gives you 1024 frequency bins, each of which represents the energy found in a 12.7 Hz wide frequency band.  So once I have the fft output, I take the complex magnitude of each sample, giving me the total power found in each frequency bin.

Touch Screen/Configuration

Once I had the data captured and processed, I needed a way for the user to configure how it would be displayed.  I ended up using three screens total for my project: the main "Frequency Display" screen and two Configuration screens.  The first configuration screen is used to modify various frequency parameters and the number of display elements to use.  This is done using StellarisWare's graphics library, using slider widgets and pushbutton widgets.  Each parameter has a slider that can be used for setting the value, and a + and - button that can be used to fine tune the values.  On this screen, the user can change the maximum display frequency, the minimum display frequency, the sampling frequency, and the number of display elements (aka the number of bars) used on the display.  Once these have been entered, my code determines which bars need to correspond to which bins in order to best facilitate a logarithmic frequency display.  This normally means the lower 1/3 of the bars each represent less than 10 bins worth of FFT data, and the upper bars contain the summation of at times hundreds of bins.  If the user ever tries to get the screen to display at a greater granularity than possible (like having only 10 Hz of separation between lower and upper frequency while running at a 40 KHz sampling rate), the display will automatically be set to use as many bars as possible while still not displaying over the user's requested frequency range.

Display

The communication to the Kentec display is handled via a parallel GPIO interface.  I started out using Stellaris' Graphics Library (grlib) for everything, and was quite impressed with it.  Going into this project, I had almost no experience working with grlib.  Despite that, it ended up only taking me three hours worth of work to go from using my 8x8 LED display to having 8 bars of generic height behaving as desired on the Kentec display.  I like to think that I'm a pretty decent programmer, but I honestly feel that getting that far that fast is much more a sign of a well designed library than it is a sign of an intelligent coder.

For actually sending data to the display, I was able to use a driver that Kentec provided.  Sadly, I had to make a few modifications; the Kentec driver uses the same timer to control the same ADC sequencer that I was using for my audio capture, so I ended up changing to a different timer that performed a processor triggered ADC capture for the Kentec display.  Also, the display's draw functions all operated by drawing horizontal lines, which is nonideal if you're trying to draw lots of thin vertical lines, so I added in a function to allow for drawing images and lines by column as opposed to by row.  In hindsight, I think the display allows for changing from "portrait" to "landscape" orientation, so I probably could have just rotated my image and set the display to landscaped, but it's a bit late for that realization now.


With my modifications to the Kentec driver, I was able to get around 30-40 frames per second (depending on sampling frequency).  I found that the display looked a bit better if I cut it back to 15 frames per second though.  The instantaneous nature of the FFT meant that if I updated as fast as data was being processed, impulses in various frequency ranges would not remain on the display long enough for my eye to catch them and make sense of them.  I use a simple timer peripheral to keep the frame rate constant over time.

Credits, development time, and shaggy hair

All photographs on this post were taken by my wonderful wife, whose work can be found on her website.

In total, this project took about a month and a half to get from my last release to its current state.  I could likely have finished it in less time, but I have also been working on another big project in my free time lately; raising my first daughter :)


She was born in mid October, meaning she was about 3 weeks old when I started working on the code for this release.  She tends to fall asleep pretty easily when she's in a baby bjorn-style harness, so most of my coding was done between 9 and midnight while wearing a ten pound child on my chest :)  The unfortunate downside to this has been that between my real 8-5 job at TI (which actually has nothing to do with Launchpad), working on this project, and keeping that little lady happy, I haven't quite been able to maintain the sharp look I used to sport.  So my apologies if I look a bit shaggy in the explanation video for this one... it's been a few days since I've had a chance to have a proper shave :)

14 comments:

  1. Hey there,

    This is a very very well done and amazing project, resect!

    Would you be open to make such a demo using ChibiOS RTOS with it's official LCD library?

    ---> http://chibios-gfx.com

    ReplyDelete
  2. Very well made, congrats!

    Have you tried to determine if you are suffering aliasing artifacts?

    You are not low-pass filtering the input signal, and IIRC, the ADC inside the Stellaris doesn't include a low-pass filter.

    If you have a signal generator, when using 26 kHz sampling rate, you can test for aliases for example generating a 19 kHz sine. If the signal appears around 6 kHz, then you need to low-pass filter the input signal before feeding it to the ADC!

    ReplyDelete
  3. Hey there,

    In your code:
    arm_rfft_f32(&fftStructure, g_fFFTResult, g_fFFTResult);

    you are using the same destination buffer as your source.
    However, the rrft function takes nsamples input and generates 2*nsamples output...

    Could you comment?

    ReplyDelete
  4. Techtu: I think I'd pass on that idea. For one, I'm pretty memory limited as is (currently, my project runs about 25 bytes shy of the 32 KB RAM limit on the launchpad), so adding something as heavy as an RTOS sounds like it'd be an issue :(

    Doragasu: I don't think I've seen any aliasing artifacts, which surprised me. My main method of testing the validity of my DSP functions was to play a sine wave at a known frequency and print out (on the debug UART) the frequency bin that sees the most energy at any given time. I have been dependably able to sweep from 40 Hz to Nyquist for sampling frequencies from 20K to 40K without seeing any issues (though this test only checks for where the largest spike is; if I were getting a smaller spike at an erroneous frequency band, I wouldn't notice it with this test). I was hoping to add a lowpass filter on the input, but trying to figure out how to do that while still allowing for changing the sampling frequency at run time was a problem I didn't quite have the resources to tackle.

    Unknown: We're working on a uC that's limited to 32K of RAM, so I had to do some things like this to save memory. The audio data (2048 samples) is stored as an array of 16 bit values at capture time. I have to convert these to floats and center them around 0 before feeding them to the f32 rfft function in the DSP loop. If I were to allocate another array for this purpose, it would take up an additional 2048 * 4 bytes = 8K of memory that is already scarce. I played around with the fft function, and found that having it pull data from elements 0:nsamples and place the result in 0:2*nsamples didn't cause any issues.

    ReplyDelete
  5. Hi,

    Thanks for your reply :).
    Actually I am more or less doing the same as you (that's why I was having a look) on a 40.96 k10 microcontroller that has only 16KB of ram.
    As my q15 2048 pts DSP computation in my case only take ~9ms I figured out the min memory you need is 8KB for the "working buffer" + 1.5*4K for the samples buffer (as you finish your algo before half of the samples are collected.

    That may be a way to go for you perhaps?
    In my case, proceeding as you did generated some artefacts.

    Cheers!

    ReplyDelete
  6. Hello Jordan,

    I bought both the LM4F120XL Launchpad and the Kentec display that you have used in your project. Then installed CCS V5.3, downloaded your freq_analysis project, compiled it without errors (after updating CCS) and tried to run it, without having the audio input small board connected (not ready yet).

    What happens is that the display becomes of an uniform medium gray, equal on all the surface, and it flickers randomly in intensity. I placed a breakpoint where its initialization is done, and the program hits that breakpoint, as it should.

    Do you have any suggestions on further actions I can do to have your program running as it is shown in the YouTube video ?

    Of course without the audio board I do not expect to see the magnitude bars, but at least the configuration buttons should be there.

    Thanks,
    Alberto

    ReplyDelete
    Replies
    1. Jordan,

      forget what I wrote... now it works, after having corrected a cockpit error... -:)

      Thanks for this example, which I will try to reuse to build with the Launchpad a Software Defined Radio, complete with the quadrature downsampling to baseband and AM/SSB/CW demodulation, with the possibility to do the tuning interactively on the touch screen display.

      The problem is not the DSP code, I have already done that on a Pentium, but interfacing with the display, setting up the two ADCs which must sample in sync, setting up the timers, the ISV, etc. etc. I hope your code can help me in this. Maybe I will bother you for clarifications (if you agree...)

      Thanks
      Alberto

      Delete
    2. Good to hear you're up and running. A caution about the ADC sampling though: you can only configure one timer to trigger all ADCs, so if you need two ADCs sampling at different rates, you'll want to set the slower of the two to be processor triggered and call the initiate capture function in the ISR for the appropriate timer.

      Delete
    3. Thanks for the suggestion Jordan, but actually what I need is to sample the I and Q channels of a quadrature mixer, and the sampling must be done at the same rate on both channels, and at the same time.

      From your experience, do you think that when sampling at 48kHz the processor is fast enough to compute a 2048 bin complex FFT, do some additional computations and drive the display?
      This without losing any single buffers, otherwise the audio would be chopped and unusable.

      Thanks
      Alberto

      Delete
  7. this is awesome!!! sorry for asking but how do I download the full project?

    ReplyDelete
  8. This comment has been removed by the author.

    ReplyDelete
  9. I've tried every way i can think of to copy paste, save raw source, and every time I try and build it in CCS the first nearly 100 lines (dsp.c, freq_analyzer.c, Kentec320x240ssd2119_8bit.c) give me various errors. (unrecognized token, null (zero) character ignored, unrecognized processing directive) I got the Arm5 compiler and I'm usein CCS Version: 5.2.1.00018. Any idea what i'm doin wrong here or are you able to provide a link to a zip with the unadultered source code in it??

    Thanks =D

    ReplyDelete
  10. Thanks for your excellent job. This has helped me a lot.
    However, I found there did exist a problem when you were using arm_rfft_f32, which set the source and destination address as the same.
    If you give the system a sine wave input with the frequency at Fin, you will find two peaks in the frequency domain. One at Fin and the other one at (Fs/2-Fin).
    If we track the source code of the function arm_rfft_f32, the explanation can be found. In the forward FFT process, it calls three functions: arm_radix4_butterfly_f32, arm_bitreversal_f32 and arm_split_rfft_f32. The source and destination address of the first two functions are the same, but for the last one, they should be different.

    For your application, it may be OK, but for some others, this usage may cause some unexpected results. Due to the memory limitation, if you want to reuse the same buffer, you could consider the function arm_cfft_radix2_f32. Although the speed may be not that kind of fast and the input array needs to be reorganized, the result is good.

    Thanks a lot~

    ReplyDelete