Stopwatch for Pinewood Derby


#25

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.


#26

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);
}

Connecting a Rover 5 controller, need 1 more interrupt
#27

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).


#28

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:


#29

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


#30

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.


#31

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.


#32

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!


#33

Shoot some video of your work in action!


#34

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


#35

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?).


#36

@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.


#37

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


#38

// 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??


#39

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


#40

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.


#41

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


#42

Was about to say the same thing. Isn’t there a new feature in the firmware that does software debounce? Maybe i’m thinking of the RPi.


#43

There is no built-in software debounce that I know about. You can use a timer but often a 0.1uF capacitor and a pull-up resistor work just as well and are easier.


#44

It is pinewood derby time and I have been following this conversation with great interest. I have adapted the code to go with a two track setup. From my testing it seems as though it is only accurate to +/- 0.02 seconds and it always seems that lane one always wins.

For my testing I have the sensors for lanes one and two under the same light source. After triggering the start of the race, I wait about 5 seconds and then trigger the end of the race by turning off the light source. This should give basically equal times to both lanes. However there is always a difference and lane one is almost always approximately 0.2 seconds faster.

Am I seeing the difference due to the SparkCore microprocessor speeds in reading the different pins?