Pages: [1]   Go Down
Author Topic: a couple questions about FFT/FHT on Arduino  (Read 1022 times)
0 Members and 1 Guest are viewing this topic.
Offline Offline
Newbie
*
Karma: 0
Posts: 17
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Hello!

I am currently working on a project using an Arduino and trying to get it to recognize certain sounds. I originally started with the ADC in free running mode and just pulled the value at the max frequency allowed. This worked decently well but the input was noisey (using elecret mic) so I constructed a sallen key band pass filter to block out most frequencies above 1.3KHz and below 50Hz. This gave an extremely clean input, and using a processing sketch to visualize the data, a snore could easily be picked out. The 200Hz resolution was far from optimal though so instead brought the sampling rate down to ~4KHz. However after changing the sampling rate the graph no longer responds to snore samples and seems to be very unreliable.

My code currently looks like this:
Code:
#define LIN_OUT8 1
#define SCALE 256
#define FHT_N 256 // set to 256 point fht

#include <FHT.h> // include the library
#include <TimerOne.h>

int fht_mine[FHT_N];
short ADCcount;
byte handshakeComplete;

void setup()
{
  ADCcount = 0;
  handshakeComplete = 0;

  Timer1.initialize(250); //create timmer with period of 250us
  Timer1.attachInterrupt(pingADC); //attach function pingADC() to timer1

  TIMSK0 = 0; // turn off timer0 for lower jitter
  ADCSRA = 0xe5; // set the adc to free running mode
  ADMUX = 0x40; // use adc0
  DIDR0 = 0x01; // turn off the digital input for adc0

  Serial.begin(115200); // use the serial port

  establishConnection(); //run the function to create a connection with host program
}

void loop(){//nothing to see here
}

void establishConnection()
{
  while (Serial.available() <= 0) //wait for incoming byte (in this crude handshake we don't care what that byte is)
  {
    Serial.write('A'); // send a capital A
    delay(300);
  }
  Serial.flush();
  handshakeComplete = 1;
}

void pingADC()
{
  if (handshakeComplete == 0) //if no connection to host software has been made, just exit
  {
    return;
  }


  ADCSRA = 0xf5; // restart adc
  byte m = ADCL; // fetch adc data
  byte j = ADCH;
  int k = (j << 8) | m; // form into an int
  k -= 0x0200; // form into a signed int
  k <<= 6; // form into a 16b signed int
  fht_mine[ADCcount] = k; // put real data into bins

  if (ADCcount == (FHT_N - 1))
  {
    ADCcount = 0;
    
      cli();  // UDRE interrupt slows this way down on arduino1.0
      int i = 0;

      while (i < FHT_N) //copy all data over to another array in case timer is called again, therefor data will not be overwritten
      {
        fht_input[i] = fht_mine[i];
        ++i;
      }

      fht_window(); // window the data for better frequency response
      fht_reorder(); // reorder the data before doing the fht
      fht_run(); // process the data in the fht
      fht_mag_lin8();
      sei();


      for(i=0;i < FHT_N / 2;i++) //function to send FHT data over serial port
      {
        Serial.write(fht_lin_out8[i]*fht_lin_out8[i]);
        i++;
      }
      
      for(i=0;i < FHT_N / 2;i++) //function to send raw analog data over serial port
      {
        Serial.write(fht_mine[i]);
        i++;
      }
  }
  else
  {
    ++ADCcount; //increment count
  }
}


So basically what I am wondering is, because I lowered the sampling rate I also lowered the max frequency and increased resolution, but did that come at a cost of accuracy?

any help or suggestions on how to clean up the input would be greatly appreciated!
« Last Edit: January 14, 2014, 04:23:22 pm by beeedy » Logged

USA
Offline Offline
Sr. Member
****
Karma: 14
Posts: 370
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Tell more.  Your Sallen-Key is presumably a two-pole Butterworth bandpass filter, with corner frequencies at 50 Hz and 1300 Hz.  Is that right?  If so, you can't expect that the filter will completely eliminate signal components that are a little bit outside the passband.  You'll get a rolloff of about 6 dB per octave, so signal components around, say, 2000 Hz won't be reduced by even as much as half, and what remains of them will alias as lower frequencies.  If your filter is a two-pole some-other-kind-of-filter, it still won't be much better than that.

Unless you have some spectacular number of poles, you won't get a sharp cutoff at the corner frequency, anyway.  The Nyquist criterion says that the lowest sample rate required to avoid aliasing is twice the highest frequency with significant content in the input signal.  If there's significant content in the unfiltered signal at or a little above 1.3 kHz, there'll be significant content in the filtered signal, too, and it will alias.  Significance, of course, is in the eye of the beholder - it''ll depend on what you're doing with the results.  And, "a little above" might well mean "between 1.3 kHz and 13 kHz," depending on the sensitivity of the follow-on analysis.

You could quickly check whether this is a source of your difficulty by restoring the original sample frequency, leaving your bandpass filter in place and active, and examining the output of the FHT to see how much content appears outside the nominal passband of the filter.  If it's a significant fraction of what's appearing at the in-band frequencies, aliasing could be the problem, or maybe part of the problem.

Another valuable test would be to restore the sample frequency to the original high speed, and reduce by degrees until the effect becomes noticeable.  That might be the lowest frequency you can operate this rig, without beefing up the bandpass filter.

But, this is all conjecture.  Tell more about your setup.  Do some experiments, and tell about the results, too.

Function pingADC() looks to be used as an interrupt service routine, and the wrapper for that seems to be the TimerOne library.  I've no knowledge of that library, so I can't comment on whether it's been handled correctly.  Intuitively, it appears that the timer is initialized with 250; the library referenced in the Playground shows the argument to the initialization function to be in microseconds, and a 250 us yields a frequency of 4 kHz, rather than the 2.5 kHz referenced in the original post.  The comment says that the period is 0.1 seconds, which is looks to be very wrong wrong.  I'll ask that you try not to misdirect us with out-of-date comments next time.  

So, maybe I can comment on that code a little, but only intuitively.

I'll note that the pingADC() seems unduly long for an interrupt service routine, and that it seems to cell a bunch of FHT functions that are certain to take a disastrously long time for an interrupt service routine.  I'd prefer to see that the ISR note that the ADC buffer is full, and that the data be processed in loop().  It may work OK, since it looks like it doesn't call the long routines until the buffer is full, and running a long program at that time won't mess up data acquisition; it'll mess with other ISR-based stuff, though, like millis(), micros(), and Serial.

There's other stuff not to like.  It can wait.
« Last Edit: January 13, 2014, 12:52:23 am by tmd3 » Logged

Offline Offline
Newbie
*
Karma: 0
Posts: 17
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

First off I would like to apologize for the horrific code and many mistakes in the original posting. The code (with now correct comments) has been updated and a couple smaller details have been changed as pointed out by you. Thank you!

The Sallen-Key is a four-pole Butterworth bandpass filter that can be switched to two-pole and bypass with the flick of a switch (just to test different configurations). The corner frequencies are at 50Hz and 1.3KHz so yes you were correct! My original post may have seemed a little naive but I am well aware that I can't expect some magic "brick wall" that stops all frequencies above/below the corner frequencies.

I can verify the filter was working (and presumably still is) as when the ADC was being sampled at (if I remember correctly) 38KHz the appropriate drop off was seen from frequencies above 1.3KHz. To test this the mic/preamp circuit was replaced with the output of a function generator. Sine, Square, Saw, and Noise signals were all fed into the filter and all gave exactly the expected results on the output of the FHT from the Arduino.

As mentioned above, I apologize for the terrible code posted in the original post. It has been modified with correct comments and a little bit of reorganizing/cleaning.

The general idea behind the code is fairly straight forward. The TimerOne library creates an interrupt with a period of 250us (for ~4KHz sampling, sorry original post was incorrect with 2.5KHz). The function called, pingADC(), grabs the current value from the ADC and stores it in an array, or buffer if you will. Once this buffer has the correct amount of data in it, all of its contents are dumped into a second buffer (I will explain my reasoning behind this in a second). The collected data that was dumped into the second buffer is then run through and calculations are done on it by the FHT library. After all the calculations are done the data is sent over the serial port to a host program running on the computer, completing the process.

The reason for the second buffer is to secure the data. The FHT library takes much longer to run through all its calculations than 250us, this is something I am aware of. However, the thought was that if I create a second buffer to store the data, the interrupt can continue to call the function while the FHT is doing its calculations and there will be no unwanted modifications of data. After going back and reading through and calculating out the numbers, I realize that I will gather a full set of data every 64ms (250us * 256samples) and the FHT library takes 4.56ms to crunch this data so there really should never be a time when I am 'crunched' for time, or when the FHT is called twice. I am aware that the array fht_mine[] can not be trusted even though I am sending it over the port, but that is not of the essence.

Now this is all if my understanding of how interrupts work is correct. I am assuming that if I am some fraction of the way through a function (in this case pingADC) that I can call it again through an interrupt, and because it will take the quick route through the function (just recording a ADC value) it will return to the original instance of the function when done. If this thinking is incorrect please let me know!

One major flaw in the code above is that the ADC is  in freerunning mode, but is not synchronized to the Interrupt giving not so accurate (or sometimes incorrect) readings. Right now I am going to attempt to sync the ADC up with the Interrupt to fix this problem.

You mention at the bottom of your post that there are other 'things not to like'. Could you please expand upon this?
« Last Edit: January 13, 2014, 12:45:13 pm by beeedy » Logged

USA
Offline Offline
Sr. Member
****
Karma: 14
Posts: 370
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

The code (with now correct comments) has been updated and a couple smaller details have been changed ...
Please don't change the substance of posts that have already been answered; it makes it impossible for a later reader to follow the discussion.  See the sticky post, "How to use this forum - please read."  See item #14, which says, among other things,
Quote
Except for adding code tags, please don't modify earlier posts to make corrections. That makes the following posts read strangely. Post the modified code in a new reply.
For example, references to specific frequencies in the follow-up post won't make sense now that you've changed the sample rate to 4 kHz.  I won't complain if you fix spelling or grammar, or decide that you really didn't want to call somebody a "dweeb," and decide to substitute, "respected member of this community."  But it's best to leave the substance as it is.

Quote
You mention at the bottom of your post that there are other 'things not to like'. Could you please expand upon this?
For a start:
  • Variables that are modified in an interrupt service routine (ISR) aren't declared volatile.
  • The ISR is way too long.
  • The ADC is in free-running mode with a conversion every 26 us, but it's only being read every 250 us.
  • The ADC conversions and the readings aren't in sync with one another.

Fixing this program is a metaphorical elephant.  We can't eat it all at once; we'll have to take it a bite at a time.  Read this, Nick Gammon's thorough and clear description of interrupts on the Arduino's processor: http://www.gammon.com.au/forum/?id=11488
Logged

Offline Offline
Newbie
*
Karma: 0
Posts: 17
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

I apologize for updating the code/post to make it more readable for future readers. I felt is was more important to have the correct information directly available in the original post instead of having to dig through multiple posts just to discover a simple typo. If you would like to go ahead and edit your post to remove your correction you are more than welcome to, I don't see how correcting my typing mistake contributes to this forum now that I have fixed the typo.

Again, I apologize for my lack of knowledge on the subject of FHT and interrupts. If I knew everything there was to know about the subjects I obviously wouldn't be posting here asking for help. I found you elephant analogy offensive, and again I apologize that my program does not work perfectly.

On subject:
I am aware that the ADC and the interrupt are not in sync, in fact I mentioned this at the bottom of my previous post saying I was fixing that very issue. Thank you for pointing out that my variables are not volatile. The page you linked to help to elaborate on this and helped me to realize that one of my arrays could benefit from being marked as volatile.


  • The ISR is way too long.
Yes I realize the ISR is very long, much longer than recommended. I now realize however that all I need to add is the function call to interrupts (); so that I can recursively keep collecting data while the Atmega munches on previously collected data. I also realize the Serial.print() statements should not be in the ISR, but for testing purposes they seem to be just fine for now.

Theoretically the program should work. In practice, the program works. Thank you for your help in making me realize all I needed was to add a declaration to a variable and a function call. 
Logged

USA
Offline Offline
Sr. Member
****
Karma: 14
Posts: 370
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

I found your elephant analogy offensive ...
No offense was intended.  That's a common saying in my part of the United States; I suppose it's less common elsewhere.  A better expression of my intended meaning would be, "The issues involved here are complex, and they would be difficult to explain, or to understand, all at once.  We'll take it a step at a time."

It appears that this code was adapted from this link - http://wiki.openmusiclabs.com/wiki/FHTExample - and that it was transferred largely intact.  The original code operated the ADC in a polled, free-running mode, without any interrupts involved.  Adapting that code to an interrupt-driven application isn't trivial, and adapting it to run the ADC at a frequency that's not one of the prescaler's natural speeds is trickier yet. 

Good luck with your project.







Logged

Pages: [1]   Go Up
Jump to: