Project locks up after a bit, need help

I've been building up the project verifying everything works bit by bit, but now that it's pretty much done, it's acting weird and I can't seem to isolate the issue. This is the second time I've built this up with the same results so I think it's something with the electrical design. It was working solid when breadboarded so I think the software is good.

The project is a basic duck hunt game. I have audio files stored on a SD card that play music such as when the game starts, miss, game over, etc. I have an audio jack breakout that I connect to some computer speakers. There is a pushbutton that triggers LED lights connected to the relays to flash and an LDR that detects the lighting and can determine between a hit and a miss between the various lights. The relay boards use 12vdc for the relays and I'm switching 120vac. I have a connector on the board that allows me to quick disconnect the button and LDR, the connector also has 5vdc and ground needed for the button and LDR. I have a 12v power supply with a female connector I wired into the proto board. All of the grounds (except the SPI ground now) are tied together soldered into a single strip. The voltage regulator takes 12vdc and outputs 9vdc that I use for Vin on the arduino.

The basic problem is that after the project is powered on for a bit (15-30 seconds), it locks up. At first, the audio gets all garbled, then the button stops working properly, then everything stops working. If I reset it, the audio is garbled off the bat. If I leave everything off for 5 minutes, it'll work fine again for bit, then start having issues again. When unplugged for a while then connecting to a PC and watching the serial monitor, I can see the "SD ok" message from the void setup() routing, but once it starts glitching and I reset it without unplugging it for awhile, it always comes back SD fail. This is why I isolated the SPI GND earlier since I had isolated the issue to the card reader at that time.

Troubleshooting steps I've taken, as you can see I have a feeling it's a grounding issue:

  • Swapped what arduino pin I used to connect the proto board ground lead to. This seemed to make a difference, but it didn't solve the issue and was not repeatable.
  • Installed additional leads from the proto board ground strip to other arduino ground pins. This initially seemed to solve all these issues, but now the problem is back. This is why i've been chasing mostly grounding issues.
  • Removed the SPI GND from the ground strip and connected directly from the SD breakout board to the SPI GND on the arduino. This also made a significant difference in an earlier phase of the project.
  • Disconnected the relay board power and ground leads (leads to shift registers are still soldered in.
  • Installed a voltage regulator and supplied 9v to the arduino Vin rather than powering via USB from the PC

I'm using:
Arduino mega2560
Micro SD breakout board
2 x 74HC595 shift registers
2 x 12v 8 relay module
12v power supply
Voltage regulator module
Audio jack breakout board
Proto board for connections
Terminal strip blocks to create individual outlets that are switched by the relays using SPT-1 wiring and plugs

You state: "I think it's something with the electrical design." share with us a schematic of the design, not a frizzy picture showing all power and ground connections. My eyes are not good enough to see the design from here. You also tell us: "Troubleshooting steps I've taken, as you can see I have a feeling it's a grounding issue:". Actually I cannot see anything. What are you doing for decoupling?

I have no idea what I'm doing for decoupling, that is probably my problem. I've basically merged a bunch of tutorials and examples together. I'll work on a schematic and look into decoupling in the meantime. For what it's worth in the meantime, by probing some of the relays, it looks like the code is sometimes still running in the background and the issue is with communicating with the SD card. I disconnected everything from the relays because they would stop working and that is why I thought it was locked up.

Include a heartbeat LED in your code. That way you can easily tell if the processor has locked up.

My heartbeat code.

const byte heartbeatLedPin = 4;  // change this if necessary.  put an LED from the pin to ground
                                // with a current limit resistor
void setup()
{
   pinMode(heartbeatLedPin, OUTPUT);
}

void loop()
{
    hearbeat();
}

void hearbeat()
{
   static bool mode = false; 
   static unsigned long timer = 0;
   static unsigned long interval = 1000;
   if (millis() - timer >= interval)
   {
      timer = millis();
      digitalWrite(heartbeatLedPin, !digitalRead(heartbeatLedPin));
      if(mode == false)
      {         
         interval = 50;        
      }
      else
      {
         interval = 1000;
      }
      mode = !mode;
   }
}

I had to improvise a little since all the components weren't available. My apologies this is a mess, but hopefully it provides the detail needed to understand what I'm doing.

Duckhunt schematic.pdf (508.8 KB)

I took the shift register example from: https://www.arduino.cc/en/Tutorial/Foundations/ShiftOut

Reading the SD card and playing the music came from a bunch of examples and forum posts, but follows the basics from: Music Player Using Arduino : 5 Steps - Instructables

/*
Rev0:
The whole idea behind this program is to test to see if gun was pointed accurately at a target when the trigger was pulled using the Nintendo Duck Hunt technique.
When the trigger is pulled, the target LED is turned off and a measurement of the ambient light the gun sees is taken.
A margin is added to the ambient light to create a threshold, the margin is 10% of the ambient light.
Then the target light is turned on and another light measurement is taken.
If the light measured when the target light is on, then the result is a hit, otherwise it is a miss.

For multiple targets, ambient measurements are made between testing each target

Rev2:
This version has the target LED lit before trigger is pulled like it would be once a valid target is lit up.
This changes the delay timing requred for the LED to dim enough so that it is not continuing to dim when the target light measurement is made, which result in it being lower than the threshold and always a miss.

Rev3:
Only allow one shot per trigger pull, provide the result as soon as the trigger is pulled, but do not reset until the trigger is released

Rev4:
Removes the increasing margin for a static margin since this was preventing triggering off a reflection, which is how I plan to trigger them in the final design.
Maybe using a longer ambient delay would have worked as well, I may have to fiddle with these when we get to the final design.

Rev5:
Created arrays for targets and eliminate individual test and ambient states. Ambient measurements are only taken for valid targets.

Rev6:
1: Created a scoring scheme. 1 point per hit of a valid villain per level so 1 point per hit villain on level 1, 2 points per hit villain on level 2, etc.
2: Created a high score that is saved and retrieved from EEPROM
3: Created the GAMEOVER state. If out of misses, then the game is over. A miss is a villain that was never hit before its valid duration expires. If you hit a bystander, the game is immediately over.
4: Created a misses counter, but have not yet created the definitino and enforcement of a miss.

Rev7:
1: Created random times for targets to be displayed. The durations start at 10 seconds for level one and are cut by 1/4 for each level and the min/max allow for +/- 20%.
Meaning level 1 is 10 +/- 2 seconds, level 2 is 7.5 seconds +/- 1.5 seconds, level 3 is 5.625 +/- 1.125, etc.
2: A random draw based on the duration for targets displayed will also be used to set a timer for when the next valid target decision will be made.
This randomizes when the targets become valid, doesn't increase the pace of targets displayed if they are shot quickly, and can display the proper number of targets per duration (by dividing the number of targets based on the duration they are valid).
3: Inizialize the random seed generator using an anolog read of an unused pin will vary the pattern from one startup to the next.
4: Created a 1ms interrupt timer to create a timer that is used for all time based decisions
5: Create LEXTLEVEL state that decrements the amount of time between targets and the duration of those targets
6: Increment to the NEXTLEVEL every 30 seconds, the number of targets per 30 seconds is not limited

Rev8:
1: Fixed timer issue, when I created the counter in Rev7, it messed up the default timer used for functions such as "delay()"

Rev9:
1: Rearramged pins 11, 12, and 13 to allow for connecting an SD card to play .wav files
2: Added an SD card reader and the TMRpcm library to play wav files
3: Added sounds for gunshots, hitting a villain, hitting a bystander, game over, and new game

Rev10:
1: Dead end branch where I removed the TMRpcm library to troubleshoot the code bogging down and sometimes freezing, that didn't make any improvement.

Rev11:
1: Fixed the NEXTLEVEL counter logic so it now can get beyond level 2.
2: Fixed bug where you could get 2 simultaneous villains after killing them both in level 2 and beyond

Rev12:
1: Changed the duration until the next target after a valid target is killed

Rev13:
1: Transitioned from individual output pins to shift registers
2: Reassigned pins as required to switch to an Arduino Mega 2560

Things to do in future versions:

  • Create an display to show the current score and the high score
    */

#include <EEPROM.h>
#include <SD.h> // need to include the SD library
#define SD_ChipSelectPin 53 // using digital pin 4 on arduino nano 328, can use other pins
#include <TMRpcm.h> // also need to include this library...
#include <SPI.h>

//
enum states {
IDLE,
TEST,
RESULTS,
RESET,
NEXTLEVEL,
GAMEOVER,
NEWGAME
};

TMRpcm tmrpcm; // create an object for use in this sketch

// constants won't change
//const int SuccessLedPin = 10; // LED pin
const int ldrPin = A0; // LDR pin
const int buttonPin = 2; // Pushbutton pin
const int dataPin = 11; // Pin connected to DS of 74HC595 (Blue)
const int latchPin = 8; // Pin connected to ST_CP of 74HC595 (Green)
const int clockPin = 12; // Pin connected to SH_CP of 74HC595 (Yellow)
const int AudioPin = 46; // Pin connected to the audio output jack
const int numLedChannels = 1; // Number of 8 LED channel relays
const int margin = 50; // Adjust this as needed for accuracy. 50 seems to work well testing directly against bulbs, may need 5 for reflections

char numTargets = 8;
bool TargetVillain[16] = {true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false};
byte ledArray[2];
//const char TargetLedPin[16] = {8, 7, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10}; // UPDATE THESE!!! These are the pins associated with the outputs that control the relays
bool TargetValid[16] = {false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false}; // is the target a valid target, you can't hit valid targets, either villains or bystanders
bool TargetStatus[16] = {false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false}; // was the target hit or not?
unsigned long TargetDuration[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; // length of time the target will be valid
int ldrAmbient[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; // this is the count associated with the voltage of the Light Resistant Diode

// variables will change
char ButtonState = 0; // variable for reading the pushbutton status
char LastButtonState = 0; // variable for the previous reading of the pushbutton
bool FirstPass = false; // This allows only 1 shot per button press
bool hit = false; // If any of the targets were hit, show a hit
bool fail = false; // If any of the bystanders were hit, show a fail regardless of if a valid target was also hit
int score = 0; // Keeps track of the total score
int highScore; // High score that is retrieved from memory and stored each time a score is greater that the high Score
signed char misses = 10; // Allow 10 misses before the game is over
int ldrTrigger = 0; // reading of the LDR at trigger pull
char level = 1; // level of the game controls the difficulty
char nextTarget = 0;
unsigned int BaseDuration = 10; // Length of nominal time the target will be displayed for level 1
unsigned int RandomDuration = 0;
unsigned long counter = 0; // This will be a continuously increasing counter for the duration of the game
unsigned long temp1 = 0;
unsigned long temp2 = 0;
unsigned long temp3 = 0;
unsigned long temp4 = 0;
unsigned long nextSerial = 0; // For printing data to the serial monitor periodically

states state = NEWGAME;

void setup() {

tmrpcm.speakerPin = AudioPin; // 5,6,11 or 46 on Mega, 9 on Uno, Nano, etc
pinMode(AudioPin,OUTPUT); // Pin pairs: 9,10 Mega: 5-2,6-7,11-12,46-45

pinMode(latchPin, OUTPUT);
pinMode(clockPin, OUTPUT);
pinMode(dataPin, OUTPUT);

highScore = EEPROM.read(0); // Retrieve previous high score into memory
randomSeed(analogRead(1)); // Initialize the random seed generator by reading the #1 analog pin, which is unconnected to randomize draws between power ups.

Serial.begin(115200); // Start the serial connection

if (!SD.begin(SD_ChipSelectPin)) { // See if the card is present and can be initialized:
Serial.println("SD fail");
return; // don't do anything more if not
} else {
Serial.println("SD ok");
}

// for (char target=0; target<numTargets; target++) { // Set each of the TargetLedPins to OUTPUT
// pinMode(TargetLedPin[target], OUTPUT);
// digitalWrite(TargetLedPin[target], HIGH); // Blink LEDs to verify pins are wired correctly
// }
// delay(1000);
// for (char target=0; target<numTargets; target++) {
// digitalWrite(TargetLedPin[target], LOW); // Turn all LEDs back off after verifying they are wired correctly
// }
// pinMode(SuccessLedPin, OUTPUT);
pinMode(ldrPin, INPUT);
pinMode(buttonPin, INPUT);

TCCR2A=(1<<WGM21); // Set the CTC mode
OCR2A=0xF9; // Value for ORC0A for 1ms
TIMSK2|=(1<<OCIE2A); // Set the interrupt request
sei(); // Enable interrupt
TCCR2B|=(1<<CS22); // Set the prescale 1/64 clock // CS02 = 0, CS01 = 1, CS00 = 1
//TCCR2B|=(1<<CS20);
}

void loop() {

LastButtonState = ButtonState;
ButtonState = digitalRead(buttonPin);

// for (char target=0; target<numTargets; target++) {
// if (TargetValid[target]) {
// digitalWrite(TargetLedPin[target], HIGH);
// } else {
// digitalWrite(TargetLedPin[target], LOW);
// }
// }
updateLEDs();

switch (state) {
case IDLE:
if ((ButtonState == HIGH) && (LastButtonState == LOW)) {
FirstPass = 1;
state = TEST;
}
break;

case TEST:
tmrpcm.play((char *)"GS.WAV");
ldrTrigger = analogRead(ldrPin);                     // Measure current total light, this is what the gun sees at trigger pull and only measured once
for (char target=0; target<numTargets; target++) {   // Turn off each of the valid targets once at a time to measure the ambient light
  if (TargetValid[target]) {                         // For each valid target...
    //digitalWrite(TargetLedPin[target], LOW);         // Turn off the target LED to be able to make an ambient light measurement
    TargetValid[target] = false;
    updateLEDs();
    delay(20);                                       // Keep the LED off for 20ms to give them time to dim and the LDR to respond
    ldrAmbient[target] = analogRead(ldrPin);         // Measure current total light
    //digitalWrite(TargetLedPin[target], HIGH);        // Turn the target back on
    TargetValid[target] = true;
    updateLEDs();
    if (ldrTrigger > ldrAmbient[target] + margin) {  // If the light at trigger pull was greater than the ambient light with the target off plus a margin...
      TargetStatus[target] = true;                   // Then it was a hit on this target, regardless of it was a villain or bystander
    }
    //Serial.print("ldrAmbient"); Serial.print((uint8_t)target); Serial.print(": "); Serial.println(ldrAmbient[target]);
    //Serial.print("TargetStatus"); Serial.print((uint8_t)target); Serial.print(": "); Serial.println((uint8_t)TargetStatus[target]);
  }
}
//Serial.print("ldrTrigger: "); Serial.println(ldrTrigger);
state = RESULTS;
break;

case RESULTS:
  if (FirstPass) {
    for (char target=0; target<numTargets; target++) {                               // For each target
      if (TargetValid[target]) {                                                     // For each valid target
        //hit = hit | TargetStatus[target];
        if (TargetStatus[target] & TargetVillain[target]) {                          // If you hit a target and it is a villain
          tmrpcm.play((char *)"PT.WAV");
          TargetValid[target] = false;                                               // The villain is dead and therefore no longer valid after you hit it
          Serial.print("counter: "); Serial.println(counter);
          Serial.print("current target duration: "); Serial.println(TargetDuration[target]);

// temp1 = min(750, BaseDuration1000);
// temp2 = random(temp1, 3000);
// temp3 = counter+temp2;
// temp4 = min(TargetDuration[target], temp3);
// Serial.print("temp1: "); Serial.println(temp1);
// Serial.print("temp2: "); Serial.println(temp2);
// Serial.print("temp3: "); Serial.println(temp3);
// Serial.print("temp4: "); Serial.println(temp4);
// TargetDuration[target] = temp4; // Shorten the duration of the targets that were hit so it doesn't take so long between targets
TargetDuration[target] = min(TargetDuration[target], counter+random(min(750, BaseDuration
1000), 3000)); // Shorten the duration of the targets that were hit so it doesn't take so long between targets
Serial.print("new target duration: "); Serial.println(TargetDuration[target]);
score = score + level; // 1 point per target per level --> 1 point per target for level 1, 2 points per target for level 2, etc.
if (score > highScore) { // If the current score is higher than the high score
highScore = score; // Set the high score to the current score
EEPROM.write(0,highScore); // Write the new high score to EEPROM
}
} else if (TargetStatus[target]) { // If you didn't hit a villain, but you still hit something, then you hit a bystander
tmrpcm.play((char *)"DL.WAV");
fail = true; // You hit a bystander so you fail
//hit = false; // Do not count a hit against a bystander
misses = 0; // Set misses to zero, which will end the game
}
//Serial.print("Target: "); Serial.print((uint8_t)target);
//Serial.print(", TargetValid: "); Serial.print((uint8_t)TargetValid[target]);
//Serial.print(", TargetStatus: "); Serial.print((uint8_t)TargetStatus[target]);
//Serial.print(", TargetVillain: "); Serial.print((uint8_t)TargetVillain[target]);
//Serial.print(", Fail: "); Serial.println((uint8_t)fail);
}
FirstPass = 0;
}
Serial.print("Score: "); Serial.print(score); Serial.print(", High Score: "); Serial.println(highScore);
}
state = RESET;
break;

case RESET:
memset(TargetStatus, false, sizeof(TargetStatus));
memset(ldrAmbient, 0, sizeof(ldrAmbient));
//hit = false;
fail = false;
if ((ButtonState == LOW) && (LastButtonState == HIGH)) {
  state = IDLE;
}
break;

case NEXTLEVEL:
level++;                                                                             // Increase the level
BaseDuration = BaseDuration * 0.75;                                                  // Decrease the time targets are valid and the time between targets
if (level<=numTargets) {                                                             // Only add another target if there are additional targets still available
  nextTarget = (char)random(0,numTargets);                                           // Randomly choose a candidate for a new valid target
  while(TargetDuration[nextTarget]!=0) {                                             // Continue randomly choosing candidates for the new valid target until you find one that is currently not valid
    nextTarget = (char)random(0,numTargets);
  }
  TargetValid[nextTarget] = true;                                                    // Set the new random valid target
  RandomDuration = random(1000*BaseDuration*.8,1000*BaseDuration*1.2);               // Calculate the duration of this target
  TargetDuration[nextTarget] = counter + RandomDuration;                             // Set the duration for this target
}
state = IDLE;
break;

case GAMEOVER:
tmrpcm.play((char *)"LS.WAV");
while (tmrpcm.isPlaying()) {}
memset(TargetValid, false, sizeof(TargetValid));                                     // Reset TargetValid to all false
memset(TargetDuration, 0, sizeof(TargetDuration));                                   // Reset TargetDuration to all 0's
misses = 10;                                                                         // Reset misses
score = 0;                                                                           // Reset score to 0
level = 1;                                                                           // Reset to level 1
// Flash "Game Over"
// Flash score
// Flash "New Game" 
// Reset LCD
state = NEWGAME;  // set this to NEWGAME until a new game button is created
break;

case NEWGAME:
tmrpcm.play((char *)"NG.WAV");
while (tmrpcm.isPlaying()) {}
counter = 0;                                                                         // Reset the timer, I want to reset it here rather that GAMEOVER in case it hasn't been play for a while.
BaseDuration = 10;                                                                   // Length of nominal time the target will be displayed for level 1
nextSerial = 0;
nextTarget = (char)random(0,numTargets);                                             // Randomly choose a candidate for the next valid target
while(TargetDuration[nextTarget]!=0) {                                               // Continue randomly choosing candidates for the next valid target until you find one that is currently not valid
  nextTarget = (char)random(0,numTargets);
}
TargetValid[nextTarget] = true;                                                      // Set the next random valid target
RandomDuration = random(1000*BaseDuration*.8,1000*BaseDuration*1.2);                 // Calculate the duration of this target
TargetDuration[nextTarget] = counter + RandomDuration;                               // Set the duration for this target
state = IDLE;
break;

} // switch(state)

// Check to see if any of the villains have expired without being hit
for (char target=0; target<numTargets; target++) { // Loop through all numTargets targets
if (TargetDuration[target]>0) { // For each valid target...
if (counter > TargetDuration[target]) { // If the target's duration has expired
if (TargetVillain[target] & TargetValid[target]) { // If the valid target was a villain
misses--; // Count this as a miss
tmrpcm.play((char )"DL.WAV");
}
TargetValid[target] = false;
TargetDuration[target] = 0;
nextTarget = (char)random(0,numTargets); // Randomly choose a candidate for a new valid target
while(TargetDuration[nextTarget]!=0) { // Continue randomly choosing candidates for the new valid target until you find one that is currently not valid
nextTarget = (char)random(0,numTargets);
}
TargetValid[nextTarget] = true; // Set the new random valid target
RandomDuration = random(1000
BaseDuration*.8,1000BaseDuration1.2); // Calculate the duration of this target
TargetDuration[nextTarget] = counter + RandomDuration; // Set the duration for this target
}
}
}

// See if the level timer has expired
if (counter>(unsigned long)level100030) {
if (state==IDLE) {
state = NEXTLEVEL;
}
}

// See if game is over
if (misses <= 0) {
if (state==IDLE) {
state = GAMEOVER;
}
}

// if (counter>nextSerial) {
// Serial.print("Counter: "); Serial.println(counter);
// Serial.print("Level: "); Serial.println((uint8_t)level);
// Serial.print("Misses: "); Serial.println((uint8_t)misses);
// Serial.print("TargetValid: ");
// for (char target=0; target<numTargets; target++) {
// Serial.print((uint8_t)TargetValid[target]); Serial.print(",");
// }
// Serial.println("");
// Serial.print("TargetDuration: ");
// for (char target=0; target<numTargets; target++) {
// Serial.print(TargetDuration[target]); Serial.print(",");
// }
// Serial.println("");
// nextSerial = counter + 1000;
// }

} // void loop()

void updateLEDs() {
ledArray[0] = (TargetValid[7] << 7) | (TargetValid[6] << 6) | (TargetValid[5] << 5) | (TargetValid[4] <<4) | (TargetValid[3] << 3) | (TargetValid[2] << 2) | (TargetValid[1] << 1) | TargetValid[0];
ledArray[1] = (TargetValid[15] << 7) | (TargetValid[14] << 6) | (TargetValid[13] << 5) | (TargetValid[12] <<4) | (TargetValid[11] << 3) | (TargetValid[10] << 2) | (TargetValid[9] << 1) | TargetValid[8];
// Ensure clockPin is low prior to transmitting to clock on rising edges
digitalWrite(clockPin, LOW);
// Set the latchPin low while transmitting
digitalWrite(latchPin, LOW);
// Loop through each byte in the LED channel array
for (int j=0; j < numLedChannels; j++) {
// Shift out the bytes of the array from least to greatest and the bits in the byte from least to greatest
shiftOut(dataPin, clockPin, LSBFIRST, ledArray[j]);
}
// set the latchPin high when done transmitting
digitalWrite(latchPin, HIGH);
return;
}

ISR(TIMER2_COMPA_vect) {
counter++;
}