Wednesday, October 19, 2016

Halloween Candy Cauldron V3

Cauldron V3 in-process.
The large speaker will be removed--replaced by the small green on in the enclosure.
The red FTDI Friend and USB cable are there for programming.
We'll shorten the wires, neaten it up, and put the cover on the enclosure.
The red LED is for testing--turned on before sleeping, turned off on waking.

When last we looked a this (last Halloween), I was having trouble making the electronics small. To recap, I started out with an +Arduino Uno, an SD card shield, +Adafruit Industries class D mono amp, and an HC-SR04 Ultrasonic Sensor.  Then someone reaches in to grab a piece of candy, the sensor reacts, code on the arduino then chooses one of nine sounds to play, via the amp and an 8 Ohm speaker.

Objective

The big issue is that the electronics are loose in the bottom of the cauldron, so I wanted to use a smaller board and smaller SD breakout (like a MicroSD), and put the whole thing in an enclosure. I tried the Adafruit Pro Trinket 5V last year and ran out of time before I could get it to work.

Turns out this was a stupid wiring trick. The circuit is powered by a 9V battery, run through a 7805 5V Voltage Regulator circuit.  The dumb thing I did last year was connect the BAT pin on the Trinket to the battery.  I'm not sure what, if anything, else I did wrong (I may have also connected that same pin to the 5V rail), but this year I connected the BAT pin to the 5V rail. Last year I got only static, this year it works. In both cases I powered the peripherals from the 5V rail (output of the VR circuit), to provide enough current, since the Trinket's 5V pin provides 150mA max.

Further, I wanted to put the Trinket to sleep while waiting for trick-or-treaters to save battery.

Video

Interrupts

Sleep mode turned out to be simple, but required some learning.  As described in this post on interrupts on the M0, I just completed my first successful Arduino project using interrupts.  So, I started with a __WFI(); instruction to wait for an interrupt. It turns out that different processors use different instructions, since interrupts are so hardware dependent, so this did not compile.

My main source of information was here. The avr\sleep.h library (which does not work with the M0), was the key, since the main differences between the 2 processor families is how you put the processor to sleep, and the sleep modes. On the M0, the top of loop tested to see if one of our interrupts had been fired, and if not issued a wfi--back to sleep.  If so, go on to process In the case of the Trinket, also at the top of loop, we attached the interrupt, enabled sleep, went to sleep, and on waking, disabled sleep and detached the interrupt. The code:
//*****sleep**************************
    digitalWrite(ledPin, HIGH);  //turn on the LED (testing)
    attachInterrupt(digitalPinToInterrupt(echoPin), echoISR, HIGH); //attach interrupt
    sleep_enable();          // enables the sleep bit in the mcucr register
    sleep_mode();            // here the device is actually put to sleep!!
//***********************************/

//wait here for echoPin interrupt--ignore all others
//*****wake up************************
    sleep_disable();         // first thing after waking from sleep: disable sleep...
    detachInterrupt(digitalPinToInterrupt(echoPin));      // disables interrupt because pin stays high
//***********************************/

echoPin is Trinket pin 3, the only one that sees interrupts
echoISR just sets a volatile boolean, handIn
The next code tests handIn to see if it was set, meaning that we woke up because of the interrupt we care about, then goes on to reset the boolean and process.

In set up, we enabled interrupts ("interrupts();") and set the sleep mode ("set_sleep_mode(SLEEP_MODE_IDLE);"). According to the article cited above, IDLE is the only mode that can be awakened by a mode other than LOW, and I thought since we want to be interrupted when echoPin goes HIGH, we're stuck with it, even though it saves the least power of all modes.

However, some experimentation yielded that LOW works similarly to HIGH on the attachInterrupt.  I tried SLEEP_MODE_PWR_DOWN, AND it seemed to sleep a little more, but the board hung with regularity, so I went back to IDLE.

I'm sure there are more and better ways to do this, but this is working for me.

Other Enhancements

I was never happy with how the code looked, There's a lot of function-specific processing that made the loop long. So, I broke out the code to calculate the distance based on the sensor pin values and the code to choose which sound to play and put them in functions.  Not rocket science, but good programming practice.

I replaced the 3.5" speaker with a 1.5". That allowed me to enclosed the speaker in the box, reducing wires. I also made it a little quieter--young trick-or-treaters don't like the loud version. Yes I know I can control the volume on the amp, but this works well.

Fritzing Diagram


Code

/*2014-10-05 Virgil Machine Halloween Candy Dish: Play random sound when kid (or greedy adult) reaches into bowl

2016-10-18 V3 VM  never got Trinket to work last year, this week I did (stupid wiring trick)
 also replaced external speaker with a 1.5" that dit in the enclosure
 Added interrupt  handling to put it to sleep to save battery (thanks to http://playground.arduino.cc/Learning/ArduinoSleepCode)
 had  to change echoPin to #3--only pin on Trinket that is interruptable; changed trigPin to 4 to be
side-by-side for wiring, added ledPin for testing
Also, put the song selection/playing and distance calculation code in functions to make loop more readable.

2015-10-14 V2 VM change to Pro Trinket from Uno to put the circuit in an enclosure
 Echo to 6 (Pro Trinket does not have pin 7)

2014-10-05 VM HC_SR04 Distance sensor code added
Neeed to modify pins for Halloween (13&12 used by SPI)

2014-04-26 VM Downloaded from Instructables
 HC-SR04 Ping distance sensor]
 VCC to arduino 5v GND to arduino GND
 Echo to Arduino pin 13 Trig to Arduino pin 12 (used 8&7 instead--need SPI pins for SD reader)
*/

/******* see virgilmachine.blogspot.com*****/

/*includes*/
#include    //SPI library
#include     //SD card library
#include //library for playing sound
#include  //sleep functions
#include  //interrupt functions

/*constants*/
#define SD_ChipSelectPin 10
#define echoPin 3 //so we can have an interrupt (D3 is the only one)
#define trigPin 4 //to be next to echo for wiring
#define ledPin  5

/*variables*/
int song = 0;   // song number for random function
volatile boolean handIn = false; //variables in ISR need to be volatile
/*objects*/
TMRpcm speaker;   // create an object for use in this sketch


void echoISR() //ISR for distrance sensor
{
    handIn = true;  //someone put his or her hand in
}

void setup(){
  randomSeed(analogRead(0));  //initialize random (A0 unconected)
  pinMode(trigPin, OUTPUT);   //pins for distance sensor
  pinMode(echoPin, INPUT);
  pinMode(ledPin, OUTPUT);   //LED fot testing
  digitalWrite(ledPin, LOW);  //default to off
  speaker.speakerPin = 9; //output to amp
  speaker.loop(0); //2014-10-05 do not play repeatedly
//  Serial.begin(9600); //Serial is for testing--comment to reduce time/power consumption
  if (!SD.begin(SD_ChipSelectPin))
     {  // see if the card is present and can be initialized:
//Serial.println("SD not initialized");
      return;   // don't do anything more if not
      }
//   else
//      {
//       Serial.println("SD initialized");
     //
//     }  
  speaker.volume(1);
//  speaker.setVolume(7);   //attempt to increase volume

interrupts(); //enable interrupts (should not need to do this, but just for drill...)
set_sleep_mode(SLEEP_MODE_IDLE);   // sleep mode is set here
//set_sleep_mode(SLEEP_MODE_PWR_DOWN);   // PWR_DOWN hangs periodically
}

void loop() {

//*****sleep**************************
    digitalWrite(ledPin, HIGH);  //turn on the LED (testing)
    attachInterrupt(digitalPinToInterrupt(echoPin), echoISR, LOW); //attach interrupt
    sleep_enable();          // enables the sleep bit in the mcucr register
    sleep_mode();            // here the device is actually put to sleep!!
//***********************************/

//wait here for echoPin interrupt--ignore all others
//*****wake up************************
    sleep_disable();         // first thing after waking from sleep: disable sleep...
    detachInterrupt(digitalPinToInterrupt(echoPin));      // disables interrupt because pin stays high
//***********************************/
  while (handIn) {   //someone wants candy, hcsr04 interrupted
    digitalWrite(ledPin, LOW);  //turn off LED (testing)
    handIn=(!handIn); //reset the boolean
//2014-10-05 If distance is <8in cm="" hand="" his="" in="" nbsp="" p="" put="" someone="">//2016-10-18 (the getDistance function calculates that)
    if (getDistance() < 20)
    {
      playSong(); //function to select and play a random song
    } //distance
      delay(2500);  //give the song a chance to play
  } //while

} //loop

void playSong() {
        song = random(1,10); //get random number from 1 to 9
//      Serial.print("song: "); //for testing
//      Serial.println(song); //for testing
      switch (song) {
        case 1:
          speaker.play("1.wav");
          break;
        case 2:
          speaker.play("2.wav");
          break;
        case 3:
          speaker.play("3.wav");
          break;
        case 4:
          speaker.play("4.wav");
          break;
        case 5:
          speaker.play("5.wav");
          break;
        case 6:
          speaker.play("6.wav");
          break;
        case 7:
          speaker.play("7.wav");
          break;
        case 8:
          speaker.play("8.wav");
          break;
        case 9:
          speaker.play("9.wav");
          break;
      } //switch/case
} //playSong

long getDistance() {
  long duration, distance;
  digitalWrite(trigPin, LOW);
  delayMicroseconds(2);
  digitalWrite(trigPin, HIGH);
//trigPin must me high for 10 microsecs to do ranging
  delayMicroseconds(10);
  digitalWrite(trigPin, LOW);
//duration is the time between pings of the sensor  in microseconds as returned on echoPin
  duration = pulseIn(echoPin, HIGH);
//duration is a round-trip, so divide by 2; speed of sound is 29.1 cm/microsec, so distance is in cm
  distance = (duration/2) / 29.1;
  return distance;
} //getDistance