Stopwatch for Pinewood Derby

Cool, I’ll work on that. Thanks for getting me so far along!

I’m not sure I follow about the timeout and retriggering the start. I realize I can’t currently restart the race without all four lanes finishing, but I’m not sure how and where to implement a timeout such that it would allow a reset. I’m guess it would be where your comment “// still needs a timeout…” is, but I guess I’m still too much of a noob to pick it up.

On another note, I’ve researched the [publish feature now available][1]. I’m thinking I’d turn on the hotspot on my phone, have the spark and my laptop use that hotspot, publish the times, then host the page on my google site. That way, people could even pull up the times on their own smartphone. Just exploring some possibilities. In all actuality, I can use @BDub’s code the way it is and be fine.
[1]: http://blog.spark.io/2014/03/11/spark-publish/

Ok, I do like the idea of displaying times as soon as they come in… and this would also seem to imply first time to come in would be 1st place, second would be 2nd place… however… it don’t work that way.

The interrupts do a GREAT job of capturing the times as fast as possible, but the loop() still loops fairly slowly and checks the state machine to know when things complete and when to display info. If all of the cars finished around the same time, and we were checking to see if (timeStart1 != timeEnd1) separately from the other 3, we could potentially be checking #3, when #1 just got interrupted. So then if #4 finished after #1 as we are finishing our #3 check, #4 would be displayed as next to finish, while in reality #1 was.

Posting the results to the serial port make it kind of hard to clear the “display” and refresh it, but if you were using an LCD or typical display… as you get results, you can check the time verses other finished times and Bubble Sort the results as you “know better”. I think this would look pretty cool too. This is not super trivial to implement though, but the concept is simple enough.

I’ll first add the timeout for you… you can adjust it if need be.

EDIT: Ok I edited the post up above with code, and added the timeout. I think all by itself it works pretty good without bubble sorting the results. That said, it would be really nice to add a 1st, 2nd, 3rd, 4th place marker on each lane. Maybe in place of the “(A6)” debugging info. I.e. “Lane 1 (2nd): 2.3334343535” This should be easy enough. You wanna try to add it?

Well, I was just informed of the actual date of the pinewood derby: a week from tomorrow; So all the frills are going to have to wait as I’ll need to dedicate all my time to actually building the sensor housings.

Also, I think I understand the timeout you added. Basically, you assume the car won’t finish if it takes longer than 10 seconds. I’m going to have to bump that up since it’s quite possible that some cars won’t make it down the track in under 10 seconds (it’s 40-50 feet long). I guess I can’t bump it up too far (probably 20 seconds) since I don’t have the finish times popping out as each car finishes.

EDIT: Ok, I need to go back to circuits class, because the way I thought it would be wired up is not working. Wouldn’t I just connect the input pins to ground via the photo resistor? For some reason, with one lane using a photo resistor and the rest all tied together and getting grounded manually, the lane with the photo resistor always times out (disqualified or DNF).

Another EDIT: I guess I could always bypass the pull up resistors and provide my own as shown in this example: http://www.acroname.com/examples/basic/ex008-read-photoresistor-reflex_i1.jpg But isn’t that point of using the pull up resistors? I guess this example technically shows a pull down resistor, but wouldn’t it be the same except with the position of the resistor and the photoresistor switched?

Basically the internal pullups are weak high value pullups that I turned on to make testing easy. They are 40k ohms +/- 10k ohms. Then I just needed to short the input to GND with a piece of wire to trigger the input.

You'll likely want to design something to give you a TTL level compatible output. Looking at the datasheet:

I typically design to < 0.8V LOW and > 2.0V HIGH...

So you need to figure out how the resistance of your LDR responds to light, and design a resistor divider that will allow it to swing to these voltage levels. < 0.8V LOW when light is NOT shining on it and > 2.0V HIGH when light IS shinning on it. Typically LDRs drop their resistance with more light, so your LDR would pull up to 3.3V and your other resistor would pull down to GND (with the way the code is set up now).

The last time I did anything with LDRs, was a speed monitor I made using a laser beam. I was shinning it across a road to the detector. It was timing how long between breaks in the beam, and that divided by the distance between wheels = speed. I had to add a comparator to the LDR/resistor divider that added hysteresis to the signal. This made things more deterministic for the microcontroller.

Ok, that makes a lot of sense. I had thought that you were only using the pull ups for testing, but wasn’t sure.
Here’s what I’m thinking of building, physically:

Then all I have to do is vary the resistor value until I get the TTL levels you describe.
I’d also need to change the setup function to this, right? (Or would I have to change them so as to not use the pullups?)

// Interrupt Driven Pinewood Derby Timer
// BDub @ Technobly.com 3/19/2014
//
// All inputs are pulled high with internal pullups.
// GND D2 to reset and run the timer for all lanes.
// GND A6, A7, D3 and D4 to stop the timer for each
// lane and display the results.
//
// D0-D3 were initially chosen, but because 
// individual control of dis/en'abling their
// interrupt handlers was not available, A6, A7
// D3 and D4 were chosen instead.  D0, D1, D2
// A0, A1, A3, A4 are all tied to one interrupt
// handler, so for the one remaining input any 
// of these will do.  D2 was chosen to allow
// for the most flexibility of the remaining inputs.
// 
// EXTI_ClearITPendingBit() is necessary to clear
// the interrupt Pending register, or interrupts 
// will fire immediately after enabling the 
// interrupt handlers again.
//
// DWT->CYCCNT was used for the timer instead of 
// micros() which wraps this hardware counter and
// returns the number of microseconds associated
// with it; because it's not clean to handle the 
// case where micros() wraps at 59.652323 seconds.
// This give us 72x more resolution on the timing
// anyway and it's super easy to deal with wrapping.
// It just works out through subtraction of unsigned
// 32-bit variables; as long as you don't time 
// something longer than 59.652323 seconds.
//
//==================================================

#include "application.h"

void startRace(void);
void lane1(void);
void lane2(void);
void lane3(void);
void lane4(void);
bool showStartMsg = false;
bool raceEnded = true; // start off assuming the race has not started.
volatile uint32_t timeStart1;
volatile uint32_t timeStart2;
volatile uint32_t timeStart3;
volatile uint32_t timeStart4;
volatile uint32_t timeEnd1;
volatile uint32_t timeEnd2;
volatile uint32_t timeEnd3;
volatile uint32_t timeEnd4;
uint32_t startTime;
const uint32_t DISQUALIFIED_TIME = 20 * 1000;  // in milliseconds (20 seconds)

void setup()
{
  Serial.begin(115200);
  while(!Serial.available()) SPARK_WLAN_Loop(); // Waiting for user to open terminal and press ENTER
  // Enter waiting state, waiting for D2 to go low.
  Serial.println("==================================================");
  Serial.println("Waiting for race to start.");

  //setup pins
  pinMode(D2, INPUT); // startRace trigger
  pinMode(A6, INPUT); // lane1
  pinMode(A7, INPUT); // lane2
  pinMode(D3, INPUT); // lane3
  pinMode(D4, INPUT); // lane4

  //setup actions to perform on interrupts
  attachInterrupt(D2, startRace, FALLING); // startRace
  attachInterrupt(A6, lane1, FALLING); // lane1
  attachInterrupt(A7, lane2, FALLING); // lane2
  attachInterrupt(D3, lane3, FALLING); // lane3
  attachInterrupt(D4, lane4, FALLING); // lane4

  //Stop listening to interrupts on all lanes
  NVIC_DisableIRQ(EXTI0_IRQn); // A6 "Lane 1"
  NVIC_DisableIRQ(EXTI1_IRQn); // A7 "Lane 2"
  NVIC_DisableIRQ(EXTI4_IRQn); // D3 "Lane 3"
  NVIC_DisableIRQ(EXTI3_IRQn); // D4 "Lane 4"
}

void loop()
{
  //wait for race to end
  if(!raceEnded) {
    //wait for all lanes to have times (this includes any empty lanes, which will timeout at 20 seconds)
    if((timeEnd1 != timeStart1 && timeEnd2 != timeStart2 && timeEnd3 != timeStart3 && timeEnd4 != timeStart4) || (millis() - startTime) > DISQUALIFIED_TIME) {

      double tempTime;

      Serial.println("Race Finished!");
      Serial.println("======= Lane Times in seconds =======");
      
      //output lane times
      Serial.print("Lane 1: "); 
      tempTime = (double)(timeEnd1 - timeStart1)/72000000.0;
      if(tempTime != 0.0) Serial.println(tempTime,8);
      else Serial.println("DNF");
      delay(50);
      Serial.print("Lane 2: "); 
      tempTime = (double)(timeEnd2 - timeStart2)/72000000.0;
      if(tempTime != 0.0) Serial.println(tempTime,8);
      else Serial.println("DNF");
      delay(50);
      Serial.print("Lane 3: "); 
      tempTime = (double)(timeEnd3 - timeStart3)/72000000.0;
      if(tempTime != 0.0) Serial.println(tempTime,8);
      else Serial.println("DNF");
      delay(50);
      Serial.print("Lane 4: "); 
      tempTime = (double)(timeEnd4 - timeStart4)/72000000.0;
      if(tempTime != 0.0) Serial.println(tempTime,8);
      else Serial.println("DNF");
      delay(50);
      
      //cleanup
      raceEnded = true; // prevents results from being displayed over and over  
      EXTI_ClearITPendingBit(EXTI_Line5); // D2 "startRace"
      NVIC_EnableIRQ(EXTI9_5_IRQn); // D2
      
      // Enter waiting state, waiting for D2 to go low.
      Serial.println("==================================================");
      Serial.println("Waiting for race to start.");
    }
  }

  if(showStartMsg) {
    Serial.println("==================================================");
    Serial.println("==================================================");
    Serial.println("==================================================");
    Serial.println("==================================================");
    Serial.println("==================================================");
    Serial.println("==================================================");
    Serial.println("==================================================");
    Serial.println("==================================================");
    Serial.println("==================================================");
    Serial.println("==================================================");
    Serial.println("==================================================");
    Serial.println("==================================================");
    Serial.println("==================================================");
    Serial.println("==================================================");
    Serial.println("==================================================");
    Serial.println("==================================================");
    Serial.println("==================================================");
    Serial.println("Race started!");
    showStartMsg = false;
    raceEnded = false;
    startTime = millis(); // Capture the rough start time, for disqualification timer
  }
}

void lane1()
{
  NVIC_DisableIRQ(EXTI0_IRQn); // stop listening for an interrupt on A6
  timeEnd1 = DWT->CYCCNT; //get the lane time (instead of using micros();)
}

void lane2()
{
  NVIC_DisableIRQ(EXTI1_IRQn); // stop listening for an interrupt on A7
  timeEnd2 = DWT->CYCCNT; //get the lane time (instead of using micros();)
}

void lane3()
{
  NVIC_DisableIRQ(EXTI4_IRQn); // stop listening for an interrupt on D3
  timeEnd3 = DWT->CYCCNT; //get the lane time (instead of using micros();)
}

void lane4()
{
  NVIC_DisableIRQ(EXTI3_IRQn); // stop listening for an interrupt on D4
  timeEnd4 = DWT->CYCCNT; //get the lane time (instead of using micros();)
}

void startRace()
{
  NVIC_DisableIRQ(EXTI9_5_IRQn); // stop listening for an interrupt on D2
  timeStart1 = DWT->CYCCNT; // get the start time for each lane
  timeStart2 = timeStart1; // set the start time for each line the same
  timeStart3 = timeStart1; // set the start time for each line the same
  timeStart4 = timeStart1; // set the start time for each line the same
  timeEnd1 = timeStart1; // set the end time to the start time temporarily
  timeEnd2 = timeStart1; // set the end time to the start time temporarily
  timeEnd3 = timeStart1; // set the end time to the start time temporarily
  timeEnd4 = timeStart1; // set the end time to the start time temporarily
  EXTI_ClearITPendingBit(EXTI_Line0); // Reset the interrupt for A6 "Lane 1"
  EXTI_ClearITPendingBit(EXTI_Line1); // Reset the interrupt for A7 "Lane 2"
  EXTI_ClearITPendingBit(EXTI_Line4); // Reset the interrupt for D3 "Lane 3"
  EXTI_ClearITPendingBit(EXTI_Line3); // Reset the interrupt for D4 "Lane 4"
  NVIC_EnableIRQ(EXTI0_IRQn); // Start listening for an interrupt on A6 "Lane 1"
  NVIC_EnableIRQ(EXTI1_IRQn); // Start listening for an interrupt on A7 "Lane 2"
  NVIC_EnableIRQ(EXTI4_IRQn); // Start listening for an interrupt on D3 "Lane 3"
  NVIC_EnableIRQ(EXTI3_IRQn); // Start listening for an interrupt on D4 "Lane 4"
  showStartMsg = true; //indicate that the race has started
}

Here is the code I used to test the photoresistors to make sure the swing was enough into the TTL ranges to register. After I got everything hooked up and LED flashlights (just as cheap as lasers but more visible), I ran this code and triggered each sensor just to make sure everything works.

void setup()
{
  Serial.begin(115200);
  while(!Serial.available()) SPARK_WLAN_Loop(); // Open terminal and press ENTER
  Serial.println("Reading...");
  pinMode(D2, INPUT);
  pinMode(D3, INPUT);
  pinMode(D4, INPUT);
  pinMode(A6, INPUT);
  pinMode(A7, INPUT);
  
}

void loop()
{
    Serial.print("D2: ");Serial.println(digitalRead(D2));
    Serial.print("D3: ");Serial.println(digitalRead(D3));
    Serial.print("D4: ");Serial.println(digitalRead(D4));
    Serial.print("A6: ");Serial.println(digitalRead(A6));
    Serial.print("A7: ");Serial.println(digitalRead(A7));
    Serial.println("====================");
    delay(1000);
}

Personally I would go for phototransistors over the LDR CdS cells. The LDR/photo resistors always seem slow to me.

Also shielding whatever you use from ambient light by recessing them below the surface a bit would be good since you don’t want a stray shadow crossing the finish line tripping the timer (assuming your are providing LED light from above).

I was originally thinking the same thing, but then I remembered my laser speed trap. I was measuring cars accurately traveling 60MPH, using only microsecond precision. Here with the Core we have 1/72nd of a microsecond precision. But do make sure your sensors aren't too slow to respond. I would agree phototransistors would be a better sensor. However, if you used small red laser diodes from cheapie laser pointers, you will drive the LDR to change faster than with something like a white LED. The other cool thing is with lasers, it's a medium your racers can see is working... so they get to feel like they actually went though a finish line of sorts.

Yes, and your setup() looks right, just pinMode(xx, INPUT);

I would suggest hooking the LDRs or whatever sensor to the 3V3* line for extra filtering.

This is probably further reinforcement for using lasers :smile:

Just don’t forget to put frikin’ sharks with those laser beams! :open_mouth:

1 Like

El cheapo lasers:

http://www.goldmine-elec-products.com/prodinfo.asp?number=G19601

I know you are on a tight deadline now, so these might not make it.

You could also pick up cheap cat toy lasers or keychain lasers from some place like Walmart. Use a rubber band or tape or binder clip to hold down the button.

Thanks for the help guys, I edited my post above with the final circuit diagram I’m using to show the final code that will be used during the test. I ended up going with el cheapo LED flashlights from the hardware store (actually novelty shotgun shell shaped!) as the light source. I ran a phone cable up to the start gate and have tested several dozen races. All is working! I’m going to be hooking up my laptop to a projector and putting putty in full screen to display the race times (configured putty for large font so it’s basically one race per screen page).

Final setup is tonight and the race is tomorrow. All your help is very much appreciated guys! Maybe after the race is over I can get to work on the publish() version of the code so it can all be wireless and web based.

EDIT:
Oh, and I ended up using 10K resistors. Most of the photoresistors swung from 10K to about 1K, so they were sufficient. The starting gate actually swung from 10K to 0 Ohms!

2 Likes

Shoot some video of your work in action!

I will! Race is tomorrow night. I’m also doing a write-up on my blog.

2 Likes

Alright, I finally got around to doing the video. Here’s the final code (haven’t been back in a while, is there a way to share the code directly from the build section?).

4 Likes

@sweenig, that makes all of the time I spent helping you on this completely worth it… times 2! :wink: Great job! Those kids sounded completely thrilled to be racing Pinewood Derby cars.

1 Like

Yeah, well, apparently, we did too good. I’ve now been asked to be the cub master. This should be fun!

4 Likes

// D0-D3 were initially chosen, but because
// individual control of dis/en’abling their
// interrupt handlers was not available, A6, A7
// D3 and D4 were chosen instead. D0, D1, D2
// A0, A1, A3, A4 are all tied to one interrupt
// handler, so for the one remaining input any
// of these will do. D2 was chosen to allow
// for the most flexibility of the remaining inputs.

Does that mean using ports " A6, A7 D3 and D4" as compared to D0, D1, D2, A0, A1, A3, A4" to capture Parallel interrupts will be more effective??

As I coded it, yes. With attachInterrupt(), I don’t think so since I was seeing issues as commented.

Hello…

I am trying to capture the state change of a normal on/off switch. I have written the below code for that.

pinMode(D4, INPUT_PULLDOWN);    

attachInterrupt(D4, changeStateD4,  CHANGE);
    
void changeStateD4()  // Map Interrupt on D4 to Trigger Action on D7 
{
     Serial.println("D4 Interrupt Fired");
        
     stateD4 = digitalRead(D7);
     if (stateD4 == 1)
     {
         digitalWrite(D7, LOW);
     }
     else if (stateD4 == 0)
     {
         digitalWrite(D7, HIGH);
      }
}

But, I am facing following problem - the interrupt function is getting fired multiple times on single state change. For example, when I am changing the switch from Off state to On state, multiple times the interrupt function is getting executed, which is leading to unproductive results. Is there anything I am missing int he code to is there another way to do this. Please suggest.

Hi @santanu

Mechanical switches have a phenomenon called “bounce” where when they are switched they can hit each contact point multiple times very quickly.

You can debounce in software using an elapsed time (5ms-10ms is usually good) or you can debounce in hardware.

Here’s a link to a tutorial on Arduino debouncing which should give you some ideas.

http://arduino.cc/en/Tutorial/Debounce