Innovating with an Arduino: Traffic Light Noise Meter

IMG_20150514_080633392

The End Product

At Jive we have an open loft style development environment with great equipment and a great view of Mount Timpanogos.  We employ a lot of passionate developers and engage in a lot of pair programming.  While we do our best to use dedicated breakout rooms for discussions, the noise level in the office sometimes gets to be a bit much.  When our Scrum master cum culture coach announced an Arduino based hackathon challenge for the dev team, I knew what must be built.  A subtle device to provide a feedback loop for the noise level in the office.  Nothing says subtle like a 6-foot tall bright orange traffic light glowing red in your direction.

The Materials

Jive would provide one Arduino Uno and a USB cable… after that we were free to forge our own path.  Aside from the Uno, this build used the materials listed below.  The stand materials are not included here.  As the property management company at our office frowns upon us putting large holes in the walls and the project needs to move to different areas as teams shift around, I opted to build a stand for the project.  You may opt to skip the stand and hang the project from the ceiling / wall or to simply set the project on a table.

The Tools

  • 1 x Electrical Multi-Tool
  • 1 x Drill
  • 1 x Drill Bit Set (Standard or Metric will do)
  • 1 x Dremel and Cutting Disc
  • 1 x Digital Multi-Meter
  • 1 x Soldering Iron

The Source

The complete source code for the project is available at GitHub under the ASL 2.0 license.  You’ll need to add the ArduinoCrashMonitor and ArduinoFFT libraries to the Arduino IDE to build the source.

Learning to Crawl – Controlling LEDs Via Wave Form Peak-to-Peak Amplitude

Screen Shot 2015-05-07 at 8.36.56 PM

Arduino and LED Schematic

Encouraged by evidence of successful signal processing projects on the web, such as this Adafruit tutorial, I figured this project would be an easy slam dunk with minimal understanding of signal processing required.  Since I was eager to start development before my hardware had arrived, I used http://123d.circuits.io/ as an initial development environment to simulate the basic concepts.  The first order of business was to prove that I could measure peak-to-peak amplitudes and trigger digital outputs based on the peak-to-peak amplitude.  The project would use the built-in Arduino library methods for reading analog to digital converter (ADC) levels and writing to the digital pins.  The initial circuit demonstrates the ability to read inputs and activate LEDs directly from the Arduino’s digital pins.  The schematic above and image below illustrate the test setup wiring.  The microphone was wired to the 3.3V supply with the signal output going to ADC pin 0.  The microphone generates a signal from rail-to-rail, 0V to 3.3V in this case, so 3.3V must also be used as the ADC reference voltage via the AREF pin on the Arduino.  The relay shield I purchased uses pins 2, 7, 8 and 10 so the test LEDs were wired to these output pins for initial testing.

Screen Shot 2015-05-07 at 9.46.27 PM

Arduino Test with Wave Form Generator

Now that the outputs and power were wired, I needed a way to simulate an audio input signal.  The 123d simulator does not have a predefined model for the microphone so I needed a way to generate wave forms for testing.  The initial hope was to use peak-to-peak amplitude to determine the level of noise in the office so I wired up a simple oscillator circuit and oscilloscope on the breadboard in order to simulate the microphone input with an approximate 3.3V peak-to-peak voltage centered around a Vcc / 2 offset.  I was only interested in peak-to-peak levels so frequency and shape of the generated wave form did not matter, only the amplitude of the wave form was important.  You can run this simulation at http://123d.circuits.io/circuits/791860-arduinovation.

IMG_20150316_202613135

Arduino, Microphone, and LEDs in Project Box

Following the initial success with the simulator, and the arrival of my parts order, I proceeded to wire up the Arduino, LEDs, and microphone for some real world testing.  I knew that testing would eventually progress to using line voltages so I assembled the test rig in a makeshift project box so I could easily transport the project to and from the office while not worrying about exposed line voltage wiring while testing.

Real world testing led to the introduction of an exponential moving average calculation over the samples to smooth and damp the input while providing reasonable reaction times to changes in the audio signal.  Even with smoothing of the samples, a certain amount of hysteresis was also required to prevent flapping between display states.  Testing also showed that using peak-to-peak amplitude of an audio signal is not the best means to analyze the perceived noise level in an environment.  The following code snippet contains my initial approach to reading the input and managing the display updates based on peak-to-peak signal amplitude.

const unsigned long UNSIGNED_LONG_MAX = 4294967295;


// Index into RELAY_PINS to control the light of the corresponding color.
const int GREEN = 0;
const int YELLOW = 1;
const int RED = 2;

// The digital IO pin numbers used by the relay board.
const int RELAY_PINS[] = { 2, 7, 8 };

// The interval, in milliseconds, that the display level is evaluated for
// a change that impacts the displayed state when in a blinking state
// and the display is illuminated.
const unsigned long BLINK_ILLUMINATED_LEVEL_UPDATE_INTERVAL = 750;

// The interval, in milliseconds, that the display level is evaluated for
// a change that impacts the displayed state when in a blinking state
// and the display is not illuminated.
const unsigned long BLINK_EXTINGUISHED_LEVEL_UPDATE_INTERVAL = 1000
  - BLINK_ILLUMINATED_LEVEL_UPDATE_INTERVAL;

// The OK display level.
const byte OK_LEVEL = 0;
// The INFO display level.
const byte INFO_LEVEL = 1;
// The WARN display level.
const byte WARN_LEVEL = 2;
// The WARN PLUS display level.
const byte WARN_PLUS_LEVEL = 3;
// The STFU DAVE display level.
const byte STFU_DAVE_LEVEL = 4;

// The interval, in milliseconds, over which individual samples are taken to create a single
// peak-to-peak amplitude reading used in calculating the damped amplitude reading.
const long SAMPLE_INTERVAL = 50;

// The time, in milliseconds, for which a sample is averaged into the calculated
// input level.
const unsigned long SAMPLE_READING_WINDOW = 5000;

//////////////////////////////////////////////////////

// The last time, in millis since starup, that the input level was evaluated.
unsigned long previousSampleUpdateMillis = 0;

int sampleMax = 0;
int sampleMin = 1023;

// The moving average of input levels (0-99).
float inputLevel = 0;

// The current display level.
byte displayLevel = OK_LEVEL;

// The last time, in milliseconds since starup, that the display level was evaluated.
unsigned long previousDisplayLevelUpdateMillis = 0;

// The interval, in milliseconds, at which the display level will next be evaluated.
unsigned long displayLevelUpdateInterval = 0;

void setup() {
  
  // Set the analog reference voltage to be externally provided.
  analogReference(EXTERNAL);

  // Initialize the serial port so we can have debug logging.
  Serial.begin(9600);

  // Set the digital IO pins to output mode for the relay board control pins and
  // make sure everything is dark.
  for (int i = 0; i < 3; i++)  {
    pinMode(RELAY_PINS[i], OUTPUT);
    digitalWrite(RELAY_PINS[i], LOW);
  }
  
  // Set the analog IO pin to input mode for the mic.
  pinMode(A0, INPUT);
}

void loop() {
  unsigned long currentMillis = millis();
  
  // Sample and determine min and max levels for sample window //

  int sample = analogRead(A0);

  // Analog IO pin readings are from 0 to 1023, ignore anything else.
  if (sample >= 0 && sample <= 1023) {
    if (sample > sampleMax) {
      sampleMax = sample;
    } else if (sample < sampleMin) {
      sampleMin = sample;
    }
  }

  // Sample interval has elapsed, calculate the sample's peak-to-peak and add it into the damped
  // peak-to-peak amplitude value.
  if (currentMillis - previousSampleUpdateMillis > SAMPLE_INTERVAL) {

    // Simplify things by scaling the sample peak-to-peak to a 0-99 scale.
    long scaledPeakToPeakSample = map(sampleMax - sampleMin, 0, 1023, 0, 99);
    
    Serial.println(scaledPeakToPeakSample);

    // http://en.wikipedia.org/wiki/Low-pass_filter#Simple_infinite_impulse_response_filter
    // http://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average
    // http://en.wikipedia.org/wiki/Moving_average#Application_to_measuring_computer_performance
    inputLevel = inputLevel + alpha(currentMillis, previousSampleUpdateMillis, SAMPLE_READING_WINDOW)
        * (scaledPeakToPeakSample - inputLevel);

    // Serial.print("Scalled input level [");
    // Serial.print(inputLevel);
    // Serial.println("].");

    // Reset sampling state  ////////////////////////////////////
    previousSampleUpdateMillis = currentMillis;
    sampleMax = 0;
    sampleMin = 1023;


    // Calculate the new display state //////////////////////////
    
    byte newDisplayLevel;
    
    if (inputLevel > 80) {
      newDisplayLevel = STFU_DAVE_LEVEL;
    } else if (inputLevel > 60) {
      newDisplayLevel = WARN_PLUS_LEVEL;
    } else if (inputLevel > 40) {
      newDisplayLevel = WARN_LEVEL;
    } else if (inputLevel > 20) {
      newDisplayLevel = INFO_LEVEL;
    } else {
      newDisplayLevel = OK_LEVEL;
    }

    if (newDisplayLevel != displayLevel) {
      previousDisplayLevelUpdateMillis = 0;
      displayLevel = newDisplayLevel;
      displayLevelUpdateInterval = 0;
      Serial.print("Set display level to [");
      Serial.println(displayLevel);
      Serial.print("]");
    }
  }

  // Drive the display as needed /////////////////////////////////

  if (currentMillis - previousDisplayLevelUpdateMillis > displayLevelUpdateInterval) {

    previousDisplayLevelUpdateMillis = currentMillis;

    switch (displayLevel) {
      default:
      case OK_LEVEL:
        turnOtherLightsOff(GREEN);
        turnLightOn(GREEN);
        // No need to check again until the display level changes.
        displayLevelUpdateInterval = UNSIGNED_LONG_MAX;
        break;
      case INFO_LEVEL:
        turnOtherLightsOff(YELLOW);
        displayLevelUpdateInterval = toggleLight(YELLOW);
        break;
      case WARN_LEVEL:
        turnOtherLightsOff(YELLOW);
        turnLightOn(YELLOW);
        // No need to check again until the display level changes.
        displayLevelUpdateInterval = UNSIGNED_LONG_MAX;
        break;
      case WARN_PLUS_LEVEL:
        turnOtherLightsOff(RED);
        displayLevelUpdateInterval = toggleLight(RED);
        break;
      case STFU_DAVE_LEVEL:
        turnOtherLightsOff(RED);
        turnLightOn(RED);
        // No need to check again until the display level changes.
        displayLevelUpdateInterval = UNSIGNED_LONG_MAX;
        break;
    }
  }
}

void turnLightOn(byte light) {
  digitalWrite(RELAY_PINS[light], HIGH);
}

void turnLightOff(byte light) {
  digitalWrite(RELAY_PINS[light], LOW);
}

void turnOtherLightsOff(byte light) {
  for (int i = 0; i < 3; i++)  {
    if (i != light) {
      digitalWrite(RELAY_PINS[i], LOW);
    }
  }
}

unsigned long toggleLight(byte light) {
  if (digitalRead(RELAY_PINS[light])) {
    digitalWrite(RELAY_PINS[light], 0);
    return BLINK_EXTINGUISHED_LEVEL_UPDATE_INTERVAL;
  }
  else
  {
    digitalWrite(RELAY_PINS[light], 1);
    return BLINK_ILLUMINATED_LEVEL_UPDATE_INTERVAL;
  }
}

float alpha(unsigned long currentTimeMillis, unsigned long lastSampleTimeMillis, unsigned long readingWindow) {
  return 1.00 - pow(2.71828182845904523536, -1.00
      * (((currentTimeMillis - lastSampleTimeMillis)) / (float) readingWindow));
}

Learning to Walk – Using Fast Fourier Transform Based Analysis

After mixed success with my first attempt to measure the noise level in an environment, I wanted to try a more complete view of the audio signal.  After doing some research, I found tutorials providing evidence that the Arduino can sample fast enough and accurately enough to perform a Fast Fourier Transform (FFT) over the frequencies that the human ear can hear.

While these articles were great news, using the single sample ADC read method in the Arduino library was not going to provide sufficient sampling frequencies to get the level of accuracy I was looking for. After a lot of time with AVR238P reference manual and the great oscilloscope tutorial mentioned above, I fully grasped the configuration of the ADC circuitry on the Arduino.  The following code fragment illustrates the configuration of the ADC in free-run mode with the use of an interrupt to signal the readiness of a sample. In free-run mode, the ADC samples the signal as fast as it can. When a sample is ready, the ADC fires an interrupt to signal to the application that a new sample is ready.  The interrupt is handled by an interrupt service routine (ISR).  When the ADC interrupt triggers, the processor pauses the main loop of the application, interrupting it, whenever a new sample is ready.  The processor then executes the appropriate ISR.  When the ISR completes, the main loop of the application resumes execution at the point where it was interrupted.  In this way, the main loop does not need to wait for an ADC sample to become ready and can focus solely on updating the digital outputs based on the last calculated noise level.

/**
 * Initializes the ADC system.
 */
void initAdc() {
  // Turn off global interrupts.
  cli();

  // ADC Setup ///////////////////

  // Disable digital input buffer on ADC pins to reduce noise.
  // See section 24.9.5 of the AVR238P datasheet.
  sbi(DIDR0,ADC0D);
  sbi(DIDR0,ADC1D);
  sbi(DIDR0,ADC2D);
  sbi(DIDR0,ADC3D);
  sbi(DIDR0,ADC4D);
  sbi(DIDR0,ADC5D);

  // ADCSRA /////////////
  // 1 1 1 0 1 1 0 1
  //
  // 7 6 5 4 3 2 1 0
  // A A A A A A A A
  // D D D D D D D D
  // E S A I I P P P
  // N C T F E S S S
  //     E     2 1 0

  // Turn on the ADC.
  // See section 24.9.2 of the AVR238P datasheet.
  sbi(ADCSRA, ADEN);
  sbi(ADCSRA, ADSC);

  // Set the ADC to auto-trigger based on the ADTS bits of the ADCSRV SRF.  This enables
  // us to choose free-run mode rather than single conversion mode for sampling.
  // See sections 24.3 and 24.9.2 of the AVR238P datasheet.
  sbi(ADCSRA, ADATE);

  // Enable the ADC Conversion Complete Interrupt on conversion completion.  Note this is only
  // effective if the I bit of the SREG (global interrupt enable) is also enabled.
  // See sections 24.9.2 of the AVR238P datasheet.
  sbi(ADCSRA, ADIE);

  // Set the ADC prescaler select bits to increase the ADC speed.  A division factor
  // of 32 is chosen to take us to a ADC clock of 500KHz.  We lose a fraction of a bit of
  // accuracy but get a theoretical effective sampling rate of 38.5 KHz.  More than enough
  // to capture the frequency range of sounds that we are interested in.  There is a drop
  // in effective bits in the conversion, although not significant per the datasheet.
  // Emperical data from other sources indicate that the drop in effective bits is on the
  // order of magnitude of < 0.5 bits; however, it crosses the half bit mark between 9 and
  // 10 bits, thus effectively removing an entire effective bit.  We will just ignore this
  // and use all 10 bits as it does seem to cut down on the noise in the lower frequency bands.
  // See sections 24.9.2 of the AVR238P datasheet.
  sbi(ADCSRA, ADPS2);
  cbi(ADCSRA, ADPS1);
  sbi(ADCSRA, ADPS0);

  // ADCSRB /////////////
  // 0 0 0 0 0 0 0 0
  //
  // 7 6 5 4 3 2 1 0
  //           A A A
  //           D D D
  //           T T T
  //           S S S
  //           2 1 0

  // Set the ADC Auto Trigger Source to be free running mode.  The ADC will perform conversions
  // continuosly while ADSC of ADCSRA is high and ADC is enabled.
  // See section 24.9.4 of the AVR238P datasheet.
  cbi(ADCSRB, ADTS2);
  cbi(ADCSRB, ADTS1);
  cbi(ADCSRB, ADTS0);

  // ADMUX ////////////
  // See section 24.9.1 of the AVR238P datasheet.
  // 0 0 0 0 0 0 0 0
  // R R A   M M M M
  // E E D   U U U U
  // F F L   X X X X
  // S S A   3 2 1 0
  // 1 0 R

  // Set an external reference voltage.
  cbi(ADMUX, REFS1);
  cbi(ADMUX, REFS0);

  // Set the results of a conversion to be left-aligned, MSBs appear in ADCH.
  // See sections 24.2 and 24.9.1 of the AVR238P datasheet.
  cbi(ADMUX, ADLAR);

  // Set ADC0 as the input channel in the ADC muxer.
  cbi(ADMUX, MUX3);
  cbi(ADMUX, MUX2);
  cbi(ADMUX, MUX1);
  cbi(ADMUX, MUX0);

  // Turn on global interrupts.
  sei();
}

The code below illustrates the ISR that handles the new ADC sample and updates the weighted average used to update the display in the main loop. The ISR collects samples until the fft_input array is full and then uses the FFT library methods to perform the FFT. The FFT results represent the magnitude of the waveform within a given frequency range. Summing the magnitude of each range in the FFT output gives an overall representation of the amount of noise is the room. While the perception of the loudness of an audio tone of the same amplitude is not uniform across the entire frequency range of human hearing, introducing A-weighting to the FFT results seemed unnecessary for version one of the project.  This iteration of the code also introduces a different weighting factor to the smoothing algorithm such that increases in noise in the environment impact the system more quickly than decreases in noise in the environment.  Put simply, the noise meter was setup to be quick to anger.

/**
 * Interrupt handler for ADC sample ready.
 */
ISR(ADC_vect) {
  // Read low to high per the atomic access control rules for the ADC.  Reading ADCL locks these
  // registers while reading ADCH releases these registers to the ADC again for writing.
  byte sampleLowByte = ADCL; 
  byte sampleHighByte = ADCH;
  int sample = (sampleHighByte << 8) | sampleLowByte;

  // Now massage the sample into the format that FFT wants it in.
  
  // Shift sample down by DC bias (1024 / 2 in ADC speak).
  sample -= 0x0200;
  // Slide the sample left the remaining 6 bits to fill the 16b int with our
  // 10b sample
  sample <<= 6;

  // This bin is the real part bin.  It gets the converted sample value.
  fft_input[sampleCounter] = sample;

  // This bin is the imaginary part bin.  It gets 0s, always.
  fft_input[sampleCounter + 1] = 0;

  // Don't forget to skip 2 at a time since we fill two indexes in the
  // array per sample.
  sampleCounter += 2;

  if (sampleCounter == FFT_SAMPLE_COUNTER_THRESHOLD) {
    // Do the FFT
    fft_window();
    fft_reorder();
    fft_run();
    fft_mag_log();

    // CPU_CLOCK / ADC_PRESCALER / CYCLES_PER_SAMPLE / FHT_N
    // 
    // Fs = 16MHz / 32 / 13 = ~38KHz
    // Df = Fs / 256 = 150Hz bandwidth per bin
    //
    // We have 128 bins each 150Hz wide for a total range of ~19KHz.
    //

    int magnitude = 0;
    
    for (int i = 1; i < FFT_OUTPUT_BINS; i++) {
      magnitude += fft_log_out[i];
    }
    
    // Uncomment to send binary FFT data out the serial line for use in Pd
    //      Serial.write(255);
    //      Serial.write(fft_log_out, 128);

    // Now we are going to adjust the running smoothed value.  We use different smoothing
    // factors for increases and decreases so that we are less sensitive to dropping volume levels
    // than we are to rising levels.
    //
    // Math is happening here...
    // http://en.wikipedia.org/wiki/Low-pass_filter#Simple_infinite_impulse_response_filter
    // http://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average
    // http://en.wikipedia.org/wiki/Moving_average#Application_to_measuring_computer_performance
    
    if (smoothedLevel < magnitude) {
      smoothedLevel = smoothedLevel + alpha(currentMillis, previousSampleUpdateMillis, RISING_LEVEL_W)
              * (magnitude - smoothedLevel);
    } else {
      smoothedLevel = smoothedLevel + alpha(currentMillis, previousSampleUpdateMillis, FALLING_LEVEL_W)
              * (magnitude - smoothedLevel);
    }

    previousSampleUpdateMillis = currentMillis;
    sampleCounter = 0;
  }
  ...
}

float alpha(unsigned long currentTimeMillis, unsigned long lastSampleTimeMillis, unsigned long readingWindow) {
 // Per http://en.wikipedia.org/wiki/Moving_average#Application_to_measuring_computer_performance
 return 1.00 - pow(2.71828182845904523536, -1.00
 * (((currentTimeMillis - lastSampleTimeMillis)) / (float) readingWindow));
}

 Power Walking – Putting the Pieces Together

IMG_20150316_201439731 (1)

AREF Pin on DFRobot Relay Shield Shorted to Ground

With a working noise level meter and output control code, it was time to introduce the relay shield to the Arduino.  I placed the relay shield on top of the Arduino and switched to powering the assembly with the external power supply as the relay shield requires more power than the USB connector can supply.  I wired the LEDs to the relay pins at this point in order to keep the assembly portable and retain a visual indicator while testing.  The unit powered up fine but the microphone no longer provided an audio signal.  Befuddled by the sudden setback, I double checked all the wiring and then broke out the multimeter.  I found that the 3.3V pin was being pulled down to ground.  Further investigation revealed that the relay shield had an internal short somewhere.  After some less than helpful communication with DFRobot customer support, I chose to cut the pin off the relay shield and use a jumper wire directly to the Arduino to provide the AREF voltage rather than risk getting another defective board.

The images below show the assembled Arduino and relay shield stack, the power supply, the test LED circuit, and the microphone circuit.  Note the white jumper wire between the 3.3v pin on the relay shield and the AREF pin on the Arduino.  The jumper bypasses the shorted pin on the relay shield.

IMG_20150426_100424988

Arduino, Power Supply, and Relay Shield Assembled in Project Box

Screen Shot 2015-05-08 at 7.15.33 PM

LED Test Circuit and Microphone Wiring

Running a 5K – Assembly and Making it Work with Line Voltage

2015-04-11

Masking and Painting

With a reasonably sound hardware and software base, it was time to assemble the pieces into position.  My traffic light is a Crouse-Hinds model with an aluminum body and 8 inch LED light units.  As with anything that has spent a couple years outside, the paint was a little oxidized.  I disassembled the light to remove the LED units and wiring from the enclosure.  After some quick cleanup with fine grit sandpaper, a good wipe down with a lint free cloth to remove the sanding dust, and some masking of the rubber seals and door hardware, I coated the enclosure with Ace Hardware School Bus Yellow spray paint.  It isn’t a perfect match for traffic light yellow, but it is a good approximation and readily available.

The next step was to mount the microphone to the signal enclosure.  The center compartment of the signal contained the original power distribution block for the signal, leaving the upper and lower compartments for the Arduino and relay shield.  I opted to mount the microphone under the signal as it would blend in well with the stand.  Mounting the Arduino and relay shield stack in the lower compartment would also reduce the length of the signal wire for the microphone and allow for the desired use of the power distribution block in the center compartment.

Microphone in Hammond Project Box Test Mounted to Signal

Microphone in Hammond Project Box Test Mounted to Signal

Using the rough standard size equivalent of a 3mm bit, I drilled two holes through the Hammond project box, through the stand, and into the bottom compartment of the signal.  I then drilled a larger hole between the two smaller holes to pass the microphone wires through.  This hole needs to be large enough to accommodate three 22 gauge wires as well as to feed the connectors on the ends of the wires through.

IMG_20150510_130854313

Microphone in Project Box Lid

The microphone itself can be mounted in the Hammond project box lid by drilling a hole of the same diameter as the microphone and pressing the microphone into the hole.  The microphone is not centered in the break out board that it is mounted to and the header pins also add to the length of the assembly.  For these reasons, the hole for the microphone is not centered vertically in the project box lid.  Note that it is a tight fit and the threaded portion of the project box will likely interfere with the bolt used to mount the box to the signal.  I had just enough room to squeeze the bolt through.  A single centered mounting bolt and an offset passage for the wiring would make for easier fabrication if I were to do it over again.

I used a  combination of 3mm standoffs and J-B Weld to mount the Arduino and relay shield stack into the enclosure in the lower most compartment.  I sanded the areas of the enclosure where the J-B Weld would be used down to bare metal, set the Arduino and relay shield stack into place, and then used a flat head screwdriver tip to coat the standoffs and compartment walls with J-B Weld.  It isn’t pretty, but it is functional.

IMG_20150426_201821219

Arduino and Relay Shield Stack Mounted in Lower Compartment

Ideally the power supply would be directly mounted to the metal enclosure with some thermal paste to aid in heat dissipation; however, the power supply did not provide good mounting holes that would work inside the signal enclosure.  I decided to use heavy duty hook and loop strips to fasten the power supply to the signal.  I put the power supply in the center compartment along with the bulk of the line voltage wiring.

IMG_20150426_202157412

Power Supply, Line Voltage Pigtails, and Enclosure Grounding

I wired the extension cord directly to the screw terminals of the power distribution block and used the 0.25 inch female disconnects with shrink wrap to create pigtails to feed the power supply, ground, and relays / neutrals for the LED units in the signal.  Each LED unit and the power supply need a neutral, white wire, for a total of four 18 gauge wires connected to the extension cord’s neutral wire.  Each relay controlling an LED unit and the power supply needs a hot/load, black wire, for a total of four 18 gauge wires connected to the extension cord’s load wire.  The signal enclosure and stand are metal.  To reduce the risk of electrical shock and fire, I grounded the enclosure.  The power supply also provides a ground terminal for a total of two green 18 gauge wires connected to the extension cord’s ground wire.

The final step for the initial power up test was to wire the line voltage wiring for the LED units to the relays.  The black load wires go to the COM, or common, terminals on the relay shields while the load wire for each LED unit goes to the NO, or normally open, terminal.  The normally open terminal is not connected to the COM terminal unless the relay is activated by the Arduino.

IMG_20150510_123813052

Line Voltage Wiring to Relay

Screen Shot 2015-05-11 at 11.54.02 PM

It Lives! (For Values of Lives That Include Frequent Reboots)

Once the wiring was double and triple checked, I dared to power it on.  Nothing melted, there was no smoke, and the breaker didn’t trip-leaving me stranded in a dark garage surrounded by blackness and expensive things to knock over.  So far so good.  Some initial testing looked quite promising, but problems soon appeared.

Shin splints – Actually Making it Work with Line Voltage

As would be expected, there were some issues on first startup.  For starters, closing the signal door for the compartment containing the Arduino immediately raised the baseline noise level read by the microphone.  Secondly, the Arduino began to reset itself periodically.  And finally, the blink mode where an LED was cycled on and off introduced a jump in the audio signal that was preventing the graceful decay of the noise level by introducing spikes in the audio signal.  I was expecting some interference from the LED units and line voltage wiring, but this was more than I had anticipated.

IMG_20150505_230906952

Shielding the LED Units

Since closing the signal door caused a large amount of interference in of itself, I decided to isolate the LED units with some shielding.  There were suddenly a number of issues with the project with the introduction of line voltage so I opted to isolate the LED units from everything rather than isolating the signal wire from the microphone.  Using sheets of aluminum foil, I wrapped the back of each LED unit, extending the foil under the retaining clips.  As the clips screw into the door and make direct contact with the signal’s metal frame, the foil was effectively grounded.  This step helped to cut down on the jumps in the signal when the relays were switched during blink mode and when the signal doors were closed.  However, at this point I still had reboot issues and occasional hangs in the application.  I added a crash monitoring tool to the application to pinpoint the cause of the hangs and reboots.  The crash monitoring tool uses the watchdog interrupt of the Arduino’s processor to trigger an ISR that records the program execution location before rebooting the Arduino in the event of a hung program.  The execution instruction of the application at which the watch dog interrupt triggered is printed to the serial port the next time the Arduino boots.  Even with the crash monitoring tool, I was unable to pinpoint a consistent location for application crashes and hangs.  I also examined free memory at various locations in the application; however, there did not appear to be an out of memory issue.  Furthermore, not all reboots triggered the watch dog timer.  I examined the power supply for ripples in the voltage and for brownouts but I was unable to identify problems there.  I also examined the microphone wires for voltage spikes and other odd signals but also came up empty handed there as well.

IMG_20150505_230906952 (1)

Relay Shield Isolated from the Arduino with Shielded Low Voltage Wiring

Running low on ideas, I decided to move the relay shield away from the Arduino to isolate the Arduino from as much electrical noise as possible.  Moving the relay shield required long runs of low voltage wires in parallel with the line voltage wiring in the signal.  To avoid introducing even more electrical noise into the circuits, I wrapped the power supply and digital signal wires from the Arduino to the relay shield in aluminum foil, wrapped the foil in solid core copper conductor, and screwed the conductor to the signal enclosure, thus grounding the shielding.  The shielded wiring was wrapped in wire loom to protect the foil from damage while routing it through the signal.  Moving the relay shield away from the Arduino appeared to help improve reliability.

I also reviewed the project code and ensured that interrupts were disabled during debug output using the serial port as I feared that interleaved use of the serial port by the main program loop and the ADC ISR might be causing reboots in addition to corrupted output in the serial monitor.

Running a Half Marathon – Wrapping Things Up

IMG_20150511_195337694 (1)

Isolating the Relay Shield from the Signal Enclosure With Standoffs and Foam

IMG_20150511_195222771 (1)

Isolating the Relay Shield from the Signal Enclosure with ABS Scrap and a Grommet

At this point, the project was working well enough to finalize the mounting of the relay shield.  As the mechanical relays make an audible clicking sound during actuation, I wanted to isolate the vibration from the signal enclosure to make the system as audibly quiet as possible during operation.  I used more standoffs and the mounting holes in the relay shield to affix the shield to a foam block with more J-B weld.  The foam block was then affixed to the signal using more hook and loop strips.  The relay shield only has two mounting holes on one end so a different solution was needed for the opposite end of the shield.  I used a scrap of ABS plastic along with a spare rubber grommet to support the end of the shield.  I cut a notch in the plastic large enough to accommodate the grommet and used J-B Weld to affix the plastic to the signal enclosure.  The shield fit snuggly in the circumferential groove of the grommet.

IMG_20150511_195410440

Sensitivity Adjuster Installed in Lower Compartment

Earlier testing in the office indicated that varying levels of ambient noise from the HVAC system would require a sensitivity adjuster to accommodate quick adjustments without needing to break out a USB cable to reprogram the Arduino.  The sensitivity adjuster was constructed using an additional ADC input on the Arduino and an adjustable voltage supply created with a potentiometer.  I had a spare 5KΩ potentiometer from an earlier project that I used to build the adjustment knob.  The potentiometer provided 3 outputs that serve as a ready made voltage divider.  However, I wanted to keep the 3.3V supply powering the microphone and AREF as isolated as possible from outside influence.  I therefore chose to use additional resistors with the potentiometer to provide a ~3.3V adjustment range powered by the 5V supply on the Arduino.  Creating a voltage divider using the potentiometer center and end pins along with a 3.3KΩ resistor provided a range of 0V to approximately 3.01V.  With the Arduino AREF voltage of 3.3V, this range was more than adequate to provide sensitivity adjustments while also avoiding voltages in excess of 3.3V that would create a dead spot in the adjuster.  Since the Arduino’s microprocessor only has one actual ADC that is fed by multiple input pins through a multiplexer, using a second ADC pin required switching the ADC multiplexer between pins.  While the reference manual provides guidance on when to switch the multiplexer to ensure accurate readings for each input pin, there are a number of timing and circuitry considerations to account for when switching the multiplexer.  To avoid the need to account for these considerations, the first two samples taken after switching between the microphone input and the sensitivity adjustment input were discarded.  Empirical evidence indicated that discarding 2 samples provided consistent readings from the sensitivity adjuster and the microphone.

The following code fragment illustrates how the samples from the sensitivity adjuster are smoothed, how the smoothed level is evaluated with hysteresis, and how the new thresholds are calculated.  The samples are collected into the FFT array in order to save memory; however, no FFT analysis is performed on the sensitivity adjuster samples.


/**
 * Interrupt handler for ADC sample ready.
 */
ISR(ADC_vect) {
  unsigned long currentMillis = millis();

  // Read low to high per the atomic access control rules for the ADC. Reading ADCL locks these
  // registers while reading ADCH releases these registers to the ADC again for writing.
  byte sampleLowByte = ADCL; 
  byte sampleHighByte = ADCH;
  int sample = (sampleHighByte << 8) | sampleLowByte;
 
  if (!ignoreConversion) {

    if (inputSource == INPUT_SOURCE_ADJUSTER) {
    // Now massage the sample into a format that we can shove into the FFT input and calculate
    // an arithmatic mean on when we have enough samples.

    fft_input[sampleCounter] = sample;

    sampleCounter += 1;

    if (sampleCounter == ADJUST_SAMPLE_COUNTER_THRESHOLD) {

      int sum = 0;

      for (int i = 0; i < ADJUST_SAMPLE_COUNTER_THRESHOLD; i++) {
        sum += fft_input[i];
      }

      int newAdjusterLevel = sum / ADJUST_SAMPLE_COUNTER_THRESHOLD;

      // Docs on abs say to not do maths inside of the brackets so assign this
      // to an intermediate value. http://www.arduino.cc/en/Reference/Abs
      int delta = newAdjusterLevel - adjusterLevel;

      if (abs(delta) > 2)
      {
        adjusterLevel = newAdjusterLevel;

        int mappedAdjusterLevel = map(adjusterLevel, 0, 1024, 0, 512);

        infoThreshold = 2000 + mappedAdjusterLevel;
        infoReleaseThreshold = infoThreshold - (THRESHOLD_RELEASE_DELTA * 0.75);

        warnThreshold = infoThreshold + THRESHOLD_DELTA;
        warnReleaseThreshold = warnThreshold - THRESHOLD_RELEASE_DELTA;

        warnPlusThreshold = warnThreshold + THRESHOLD_DELTA;
        warnPlusReleaseThreshold = warnPlusThreshold - THRESHOLD_RELEASE_DELTA;

        stfuDaveThreshold = warnPlusThreshold + THRESHOLD_DELTA;
        stfuDaveReleaseThreshold = stfuDaveThreshold - THRESHOLD_RELEASE_DELTA;

        // Bring the smoothed average up to baseline quickly.
        if (smoothedLevel == 0.0) {
          smoothedLevel = infoThreshold - THRESHOLD_DELTA;
        }

        Serial.print(F("Set baseline threshold: "));
        Serial.println(infoThreshold);
      }

      sampleCounter = 0;
      setAdcInput(INPUT_SOURCE_LEVEL);
    }
  }  ...
}

The complete source code for the project is available from GitHub under the ASL 2.0.

Running a Marathon – Where to Go From Here

While the project was an overall success, the LED units are amazingly bright.  So bright that they are uncomfortable to look at straight on.  I plan to apply tinted vinyl to the LED unit lenses to reduce overall brightness for more comfortable use in the office.

In the future, the following basic enhancements will help to make the project easier to use:

  1. Mount the sensitivity adjuster to allow for adjustments without opening the enclosure.
  2. Implement A-weighting on the FFT results and re-tune the sensitivity adjuster to account for unperceived noise reduction.
  3. Fine tuning of the alpha factor in the audio signal smoothing function to improve feedback timing.
  4. Add shielding to the microphone wiring to further reduce electrical interference.

Some more advanced features to consider are:

  1. Ethernet / Wi-Fi enabled data export for integration with Jive’s metrics aggregation system.  Real-time and historical dashboards for the noise levels in the office would be interesting.
Advertisements
This entry was posted in Uncategorized and tagged , , . Bookmark the permalink.

2 Responses to Innovating with an Arduino: Traffic Light Noise Meter

  1. Jeremy Minto says:

    Love the post, but have a question. I just received the same relay shield but doesn’t seem to be actually firing the relays? I see the little green LED come on when the output to the relay is high but the relay itself doesn’t actuate. I tried to see from your pictures are you connecting a separate power source to the “Servo power” connection? From what I have gathered these are what actually power the relay coils but I seem to be having no luck with or without that hooked up.

    • David Valeri says:

      If you are powering the Arduino via USB or possibly a 5v wall wart supply, the supply cannot source enough current and/or voltage to actuate the relays. The LEDs will work like you said, but the relays will not actually trigger. You might hear a soft click and the LEDs likely dim when you trigger one, but they won’t actually close.

      For initial development, I powered the entire thing with a 9V battery via the grnd and Vin pins. The 9V battery was sufficient to power the Arduino and actuate the relays. The final product is powered via a 9V power supply through the same pins.

      This is the power supply I used: http://www.ebay.com/itm/370929625866?_trksid=p2057872.m2749.l2649&ssPageName=STRK%3AMEBIDX%3AIT

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s