Objective

The objective of this project was to build a sensor that was capable of identifying when my smoke alarms or security alarms activated. This could then be used to send me an SMS messages when the house was vacant. The approach I took was to use a microphone to pick up all of the sounds in my house, and develop software for the Arduino Mega to filter the sound looking specifically for the high pitched beeps that smoke alarms emit. When the Arduino detects the smoke alarm beeps it sends an alarm message over mySensors wireless network to my RaspberryPi from where I can initiate SMS messages.

Components

The project required only three components.

  • Arduino Mega. For most of my projects an Arduino Nano is sufficient, but this project would require some complex software with significant storage for variables, specifically arrays. The Mega has 8KB of RAM available for variables vs 2KB for the Nano. The clock speed of the controller was also an important consideration as we needed to sample the sound fast enough to isolate high frequency sounds. Both the Nano and Mega have a clock speed of 16 MHz, which was sufficient.
  • Microphone. For the microphone I used a MAX9814 Electrete Microphone Amplifier (https://datasheets.maximintegrated.com/en/ds/MAX9814.pdf) . What makes this ideal for this project is the build in automatic gain control. I tested several microphones and none were able to isolate the specific sound of a burglar alarm unless they were within a few yards of the sensor. By using the maximum 60 dB gain configuration option, I found the MAX9814 was able to detect fire alarms more than 35 feet away across three rooms of my house.
  • Wireless Transceiver. As this sensor is connected to mySensors wireless home automation network described in the tutorials, I used a standard NRF24L01, 2.4GHz wireless transceiver. As the picture shows, this was slightly modified by soldering a 47 uF electrolytic capacitor across the power supply pins. This is recommended in many posts to maintain a steady 3.3V supply to the transceiver, and is especially important if you configure your mySensors software to use maximum power setting.

Below is the wiring diagram connecting all the components.

How it Works

The key to the project was the use of something called Fast Fourier Transform (FFT) algorithms to analyze the sound wave coming from the microphone. FFT takes a sound wave and breaks it down into its component frequencies, providing a volume level specific to each frequency. The diagram below shows what this looks like for the high pitched 3.1 kHz sound emited by a standard fire alarm.

The graph on the left is the signal from the microphone which is wired to A01 pin of the Arduino. Special code had to be written to access the AO pin data directly from the ADC Register as the standard alalogRead() function is too slow. Also the FFT algorithm requires the sound wave to be sampled at regular intervals (104 micoseconds in this case), which was achieved using the Arduino interrupt capabilities. Details of the code needed to achieve this is described later.

128 sound samples were then fed to the FFT algorithm and the output is an array that is shown in the right hand graph. The x axis is the array index which represents specific frequency ranges. e.g. index 83 represents the sound with frequency range 3.02-3.25 kHz . As you can see the FFT analysis of the Fire Alarm beep sound generates a peak in sound volume at the 3.1 Khz frequency. This is exactly what we would predict, as the majority of smoke alarms are designed to emit an alarm sound at this frequency.

Now that we have the means of detecting loud volumes at a specific frequency, the next step is to differentiate if this is coming from a fire alarm or other source, e.g. a baby crying, dog howling, or UFO landing ! My first attempt was to try and recognize the specific beeping sound that an alarm makes, e.g. three beeps, pause, three beeps, pause etc. However it was difficult to create an algorithm that would be both reliable and flexible to different beeping patterns from different smoke alarm manufacturers. So the final solution was to simply count the number of times a peak in sound volume at 3.1 kHz was detected over a 10 second period. The result is shown below.

The orange bars show the peak volume being detected at the 3.1 KHz frequency, and the x axis is each successive cycle through the total Arduino code contained in loop() . The data clearly shows a succession of fast beeps followed by a pause. The approach I used was to count how many times the 3.1 kHz volume exceeded a threshold value (35 in this example) over 500 program cycles, which represents approx. 10 seconds of elapsed time. If this counter (beepCounter) exceeds a threshold (200 in this example) then we can assume the sound was generated by a fire alarm. The critics out there would recognize that this approach is not without its faults. What we actually doing is saying anytime we hear loud sound at 3.1 kHz for more than 40% of the time over a 10 second period then we can assume it came from a fire alarm. So yes its not perfect, but so far I haven’t found many other sources of such such sound that would create false alarms.

Reconfiguring to detected Security Alarms

What makes this approach so versatile is that it can be easily reconfigured to detected other sources of sound frequency. The diagrams below are for my ADT security alarm which generates repeating high pitched 2 kHz beeps when it activates. In this example I have annotated the FFT output graph on the right to show more details of how the 2 kHz peak volume is isolated. To improve detection reliability I widened the frequency detection range by looking for peak volume between FFT output array indices 52-56 (1.96-2.09 kHz) . Next, rather than simply use the peak volume within the detection range, I measured it relative to the average background volume, which was calculated by taking ± 3 samples outside of this range, identified by the yellow circles in the graph. Finally I passed this relative maxVol through a rolling average filter of the last 10 values to eliminate any sensor noise.

The pattern of repeating beeps is clearly visible in the graph below, and an alarm is considered to be active once 220 beeps are detected over the last 500 program cycles.

ADC Sampling for FFT analysis

As described earlier, for FFT analysis to work we need two things, fast an regular sampling of the sound sensor. To achieve this we need to delve into the inner workings of how the Arduino reads analogue values from pins A0 to A7 and performs a conversion to an 10 bit integer. Three register control this process, ADMUX, ADCSRA and ADCSRB, and by setting the bits of these registers we change how the ADC operates. For a full description of these registers checkout this article (ADC Register Description : Arduino / ATmega328p – Arnab Kumar Das) . The diagram below shows the settings used for this project, and a brief description of the key values. Setting or clearing register bits is achieved with some simple code in the setup() function. For example, the code below sets the bit for REFS0 to 1, and clears the REFS0 bit in the ADMUX register

  ADMUX |= B01000000;
  ADMUX &= B01111111;

REGSITER SETTINGS TO PUT ADC INTO FREERUNNING MODE

MUX0-3: Though the Arduino has 8 analogue pins, there is only one ADC processor. By setting these MUX bits to 0000 we connect the ADC to the A0 pin.

ADPS0-2: The Arduino Nano processes uses a 16 Mhz system clock, but in order to get 10 bit precision from the ADC conversion the ADC has to run slower than the system clock. Setting ADPS bits to 111 sets the ADC scale factor to 128 , i.e. the ADC clock runs at 16MHz/128 = 125 kHz. It takes 13 clock cycles for the ADC to sample the A0 pin and complete its conversion. This means the fastest the ADC will read and convert voltage at the A0 pin is 125kHz / 13 = 9.6 kHz. This is adequate for our purposes as the the maximum frequency the FFT algorithm will detect is 1/2 the ADC processing rate i.e. the FFT will be able to detect sound frequencies up to 9.6kHz/2 = 4.8kHz. If you want to detect frequencies higher than this then you can reduce the AD scale factor to smaller values (e.g. setting ADPS bits to 110 configures the ADC scale value to 64, twice as fast as default, allowing the FFT algorithm to detect frequencies up to 9.6 kHZ ). However there is a trade off in loss of accuracy in measuring the voltage as you run the ADC faster.

ADATE and ADTS0-2: The default operating mode for the ADC to is to wait for a programing command to initiate the sample/conversion process. However by setting ADATE to 1 and ADTS bits to 000 , we configure the ADC to operate in free running mode. In this mode as soon as the ADC conversion is complete it starts the next cycle. Thus we are guaranteed a sampling rate of 9.6 kHz.

ADIE: Operating the ADC in free running mode means we will need to know when each conversion is complete. Setting ADIE = 1 achieves this by enabling interrupts each time an ADC conversion cycle finishes. This interrupt runs a function ISR(ADC_vect) that we can program to read the new ADC values. Below is the code used in our program. Note that the ADC result is stored in two registers ADCL and ADCH that are combined into a single 16 bit integer, soundSensorVol.

ISR(ADC_vect) {
  
      soundSensorVol = ADCL | (ADCH << 8);   // read ADC value
      ADCdataReady = 1;                      // set flag so main program in loop() knows knew ADC value has been read                
}  

The interrupt function runs independent of the main program loop(), and simply stores new vales from the ADC as soon as they are available. We access these variables within the main program using a while loop.

  for (int i=0 ; i<FFT_N ; i+=2){
    while (!ADCdataReady);  // wait for ADC value to be ready. see function ISR(ADC_vect)
    ADCdataReady = false;
    fft_input[i] = soundSensorVol; // put real data into even bins
    fft_input[i+1] = 0; // set odd bins to 0
  }

ADC values are stored in array fft_input[], and once the array is full we proceed to perform the FFT analysis. Note that the FFT algorthim requires the data to be stored only in even indexes in the array. Odd indexes need to be set to 0.

There are many FFT algorithm libraries available for Arduino. The one I elected to use was from http://wiki.openmusiclabs.com/wiki/ArduinoFFT . Download the ZIP files and unzip them to your Arduino library folder. The FFT algorithm is then run with three commands that are explained on the web site. The result from the analysis is stored in array fft_log_out[]

   fft_reorder(); // reorder the data before doing the fft
   fft_run();     // process the data with fft
   fft_mag_log(); // take the output of the fft

SOFTWARE

Below is the full program source code. The program uses the mySensors library to setup wireless connectivity, which is described in more details in the tutorials. The node is set to 16 in this example, and one child sensor tag is created MyMessage msgAlarm(CHILD_ID_ALARM, V_STATUS). I have tested the code and found it works reliably in detecting fire alarms sounding from over 35 feet from the sensor.

/*
Fire/Security alarm sound detector uses FFT algorthm to analyse sound from microphone connected to A0. 
Configuration parameters,

FFT_BIN_LOW_LIMIT and FFT_BIN_HIGH_LIMIT set the frequency range for detection. 

SOUND_VOLUME_THRESHOLD sets the volume above which the beepCounter increments (also the Yellow LED on pin 22 lights)
BEEP_COUNTER_THRESHOLD sets the value above which the beepCounter generates an alarm (also the red LED on pin 23 lights)
*/

// ========================= FAST FOURIER TRANSORM VARIABLES =========================

#define LOG_OUT 1 // use the log output function
#define FFT_N 256 // set input array fft_input to 256 points (128 cound sample)

#include <FFT.h> // include the library http://wiki.openmusiclabs.com/wiki/ArduinoFFT

// =========================== MYSENSORS VARIABLES ====================================

#define MY_DEBUG
#define MY_RADIO_NRF24
#define MY_RF24_PA_LEVEL RF24_PA_HIGH
#define MY_NODE_ID 16
#define MY_PARENT_NODE_IS_STATIC
#define MY_PARENT_NODE_ID 0

#include <MySensors.h>

#define YELLOW_LED_PIN   22  // LED turned on when volume at selected frequency exceeds threshold SOUND_VOLUME_THRESHOLD 
#define RED_LED_PIN      23  // LED turned on when number of detected beeps exceeds threshold BEEP_COUNTER_THRESHOLD  

#define CHILD_ID_ALARM                    0      // 1 = FIRE alarm detected

MyMessage msgAlarm(CHILD_ID_ALARM, V_STATUS);                        //  1 = Fire alarm detected

// ============================FIRE ALARM VARIABLES ===================================
//     note: fire alarms beep typical frequency is 3.1 kHz 

int FFT_BIN_LOW_LIMIT         = 80;  // Equates to a sound frequency of 3.02 kHz
int FFT_BIN_HIGH_LIMIT        = 85;  // Equates to a sound frequency of 3.25 kHz
int SOUND_VOLUME_THRESHOLD    = 35;  // Level (volume) above which sound is recognised as a Beep (range 0-255)
int BEEP_COUNTER_THRESHOLD    = 200; // number of times Beep frequency was detected


int j;
byte relativeMaxVol = 0;
byte avgBackgroundVol = 0;
byte maxVol =0;
byte maxBin = 0;

byte            filteredMaxVol[10];                        // array to average volume measurements to filter out noise
byte            filterCounter = 0;   
unsigned int    filterSum     = 0;  

#define LOOP_COUNTER_LIMIT  500                            // As each program cycle takes approx 21 msec, an array of 500 holds approx 10 seconds 
byte            rollingSoundPeaks[LOOP_COUNTER_LIMIT];     // Holds previous values of maxVol. Used to calculate beepCounter. 
unsigned int    loopCounter = 0;                           // loop counter for above array

volatile bool ADCdataReady = false;                       // variable is declared with type 'volatile' as it is used in an interupt function
volatile int  soundSensorVol = 0;                         // variable is declared with type 'volatile' as it is used in an interupt function

unsigned int    beepCounter = 0;                         // counts the number of times over the last 500 loop cycles that a 3.1kHz bin volume sound level > SOUND_VOLUME_THRESHOLD was detected.
bool            fire_alarm_triggered = false;            // If beepCounter > BEEP_COUNTER_THRESHOLD then we have a fire alarm. (e.g. if beeps last more than approx 1.5 seconds over the last 7.5 seconds)
bool            prior_fire_alarm_triggered = false;

void checkForBeeps();         // function to analyse fft_log_out[] array to identify if alarm beep sound has been actiev for sufficient time to trigger an alarm 

   
// ====================================================================================   

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

  Serial.println(F("------ INITIALIZING -------"));

  pinMode(YELLOW_LED_PIN,OUTPUT);  
  pinMode(RED_LED_PIN,OUTPUT);    

  digitalWrite(YELLOW_LED_PIN,HIGH);  // turn on during initialization
  digitalWrite(RED_LED_PIN,HIGH);     // turn on during initialization

  sendSketchInfo("Fire Alarm", "1.0");
  present(CHILD_ID_ALARM, S_BINARY);
  
// -------------------------- SETUP ADC TO READ ANALOG PIN 0 IN FREERUNNING MODE -----------------

  // clear ADLAR in ADMUX to right-adjust the result
  // ADCL will contain lower 8 bits, ADCH upper 2 (in last two bits)
  ADMUX &amp;= B11011111;
 
  // Set REFS1..0 in ADMUX to set ref voltage to default 5V
  ADMUX |= B01000000;
  ADMUX &amp;= B01111111;
   
  // SET ADC to read from Anaogue 0 pin
  ADMUX &amp;= B11110000;
 
  // Set ADEN in ADCSRA to enable the ADC.
  // Note, this instruction takes 12 ADC clocks to execute
  ADCSRA |= B10000000;
 
  // Set ADATE in ADCSRA to enable auto-triggering.
  ADCSRA |= B00100000;
 
  // Clear ADTS2..0 in ADCSRB (0x7B) to set trigger mode to free running.
  // This means that as soon as an ADC has finished, the next will be
  // immediately started.
  ADCSRB &amp;= B11111000;
 
  // Set the Prescaler to 128 (16000KHz/128 = 125KHz)
  ADCSRA |= B00000111;
 
  // Set ADIE in ADCSRA to enable the ADC interrupt.
  // Without this, the internal interrupt will not trigger.
  ADCSRA |= B00001000;
 
  // Enable global interrupts
  // AVR macro included in <avr/interrupts.h>, which the Arduino IDE
  // supplies by default.
  sei();
 
  // Kick off the first ADC
  // Set ADSC in ADCSRA to start the ADC conversion
  ADCSRA |=B01000000;


  digitalWrite(YELLOW_LED_PIN,LOW);
  digitalWrite(RED_LED_PIN,LOW);
  
  Serial.println(F("------ INITIALIZING DONE-------"));

}

// ====================================================================================

void loop() {
  
  // collect sound data from ADC that is in free running mode.
  
  for (int i=0 ; i<FFT_N ; i+=2){
    while (!ADCdataReady);  // wait for ADC value to be ready. see function ISR(ADC_vect)
    ADCdataReady = false;
    fft_input[i] = soundSensorVol; // put real data into even bins
    fft_input[i+1] = 0; // set odd bins to 0
  }
  
  // process fft_input[] array with fft algorithm
  
   fft_reorder(); // reorder the data before doing the fft
   fft_run();     // process the data with fft
   fft_mag_log(); // take the output of the fft

  // analyze fft_log_out[] array to detect fire alarm beeps
  
   checkForBeeps();       

}


// ====================================================================================

void checkForBeeps(){

  // ---- identify the maximum volume (maxVol) relative to background  -------
  
  avgBackgroundVol = ( fft_log_out[FFT_BIN_LOW_LIMIT - 1 ]  + fft_log_out[FFT_BIN_LOW_LIMIT - 2 ] + fft_log_out[FFT_BIN_LOW_LIMIT - 3 ] ) / 3 ;      // average backgroud sound volume at lower frequency range than smoke alarm
  
  maxVol =0;

  for (j = FFT_BIN_LOW_LIMIT ; j <= FFT_BIN_HIGH_LIMIT ; j ++) {   
    relativeMaxVol = 0;
    if (fft_log_out[j] >  avgBackgroundVol) {   
      relativeMaxVol =  fft_log_out[j] - avgBackgroundVol ; 
    }
    
    if(  relativeMaxVol > maxVol){ 
      maxVol = relativeMaxVol ;
    }
  } 

  // ---- Use roling average to filter maxVol -------
  
  filteredMaxVol[filterCounter] = maxVol;
  filterCounter++;
  if(filterCounter >= 10) {
    filterCounter = 0;
  }
  
  filterSum = 0;
  for (j = 0 ; j <= 10 ; j++) {
    filterSum += filteredMaxVol[j];
  }
  maxVol = filterSum / 10 ;

  // ---- Turn Yellow LED on/off depending on detected volume exceeding threshold -------

  if(maxVol > SOUND_VOLUME_THRESHOLD){
      digitalWrite(YELLOW_LED_PIN,HIGH);
  }
  else{
      digitalWrite(YELLOW_LED_PIN,LOW);  
  }

  // ---- Store maxVol in a rolling array 'rollingSoundPeaks' -------


  rollingSoundPeaks[loopCounter] = maxVol;
  loopCounter++;
  
  if( loopCounter >= LOOP_COUNTER_LIMIT ) { 
    loopCounter = 0 ;    
  } 

  // ---- analyze rollingSound[] to calculate beepCounter -------
 
  beepCounter = 0;
  
  for (j = 0 ; j < LOOP_COUNTER_LIMIT ; j ++) {
    if( rollingSoundPeaks[j] > SOUND_VOLUME_THRESHOLD ){
      beepCounter++;
    }
  }

  // ----------------- CHECK FOR FIRE ALARM  -----------------------------
    
  prior_fire_alarm_triggered = fire_alarm_triggered; 
  
  if(beepCounter > BEEP_COUNTER_THRESHOLD) {
    fire_alarm_triggered = true; 
    digitalWrite(RED_LED_PIN,HIGH);   
  }else {
    fire_alarm_triggered = false;
    digitalWrite(RED_LED_PIN,LOW); 
  }

  if( prior_fire_alarm_triggered ^ fire_alarm_triggered ) {      // use XOR to check for change in fire alarm  
    send(msgAlarm.set(fire_alarm_triggered));                    // send new value of fire alarm to gateway
  }

}  

// ====================================================================================

ISR(ADC_vect) {
  
      soundSensorVol = ADCL | (ADCH << 8);   // read ADC value
      ADCdataReady = 1;                      // set flag so main program in loop() knows knew ADC value has been read                
}  

Leave a Reply

Your email address will not be published. Required fields are marked *